diff options
| author | Max Nanis | 2025-06-06 22:40:41 +0700 |
|---|---|---|
| committer | Max Nanis | 2025-06-06 22:40:41 +0700 |
| commit | 257bc2f85b71a8564e95a8e6ba39ab0b00e022df (patch) | |
| tree | c44ced82b5f5f99e7e7bb9cffcd444a41b8a648c /src/models | |
| parent | 696dee6a6a9506fcf771d0ec4911dcc82a279fda (diff) | |
| download | panel-ui-257bc2f85b71a8564e95a8e6ba39ab0b00e022df.tar.gz panel-ui-257bc2f85b71a8564e95a8e6ba39ab0b00e022df.zip | |
Question.active state (clear naming on getInitialQuestion and getNextQuestion). Explicit use of return null as an option for answerSlice. Saving motion. Questions rolling window. Question count badge to sidebar.
Diffstat (limited to 'src/models')
| -rw-r--r-- | src/models/answerSlice.ts | 21 | ||||
| -rw-r--r-- | src/models/questionSlice.ts | 58 |
2 files changed, 50 insertions, 29 deletions
diff --git a/src/models/answerSlice.ts b/src/models/answerSlice.ts index aada48c..cd20dbc 100644 --- a/src/models/answerSlice.ts +++ b/src/models/answerSlice.ts @@ -1,4 +1,6 @@ import {createSelector, createSlice, PayloadAction} from '@reduxjs/toolkit' +import {Selector} from 'react-redux' + // import {Answer} from "@/models/answer.ts"; // import {stringify} from "querystring"; import {RootState} from '@/store'; // your root state type @@ -183,24 +185,13 @@ export const { } = answerSlice.actions; export default answerSlice.reducer -export const answerForQuestion = (state: RootState, question: ProfileQuestion) => state.answers[question.question_id] ?? { - values: [], - error_msg: "", - complete: false, - processing: false -} as Answer; -export const makeSelectChoicesByQuestion = (question: ProfileQuestion) => +export const selectAnswerForQuestion = ( + question: ProfileQuestion +): Selector<RootState, Answer | null> => createSelector( (state: RootState) => state.answers, (answers) => { - // const question = questions.find(q => q.id === questionId); - // return question?.choices ?? []; - return answers[question.question_id] ?? { - values: [], - error_msg: "", - complete: false, - processing: false - } as Answer; + return answers[question.question_id] || null } );
\ No newline at end of file diff --git a/src/models/questionSlice.ts b/src/models/questionSlice.ts index ec9f84a..9543088 100644 --- a/src/models/questionSlice.ts +++ b/src/models/questionSlice.ts @@ -2,6 +2,8 @@ import {createSelector, createSlice, PayloadAction} from '@reduxjs/toolkit' import type {RootState} from '@/store' import {UpkQuestion} from "@/api"; import {Answer} from "@/models/answerSlice.ts"; +import {assert} from "@/lib/utils.ts" +import {Selector} from "react-redux"; export interface ProfileQuestion extends UpkQuestion { active: false @@ -59,11 +61,18 @@ export const selectQuestions = (state: RootState) => state.questions export const selectActiveQuestion = (state: RootState) => state.questions.find(i => i.active) export const selectAnswers = (state: RootState) => state.answers -export const selectNextAvailableQuestion = createSelector( +export const selectFirstAvailableQuestion = createSelector( [selectQuestions, selectAnswers], - (questions, answers) => { + (questions, answers): Selector<RootState, ProfileQuestion | null> => { + /* This is used when the app loads up the Questions page and we + need to find the first Question that we'll present to + the Respondent. + + If there are any questions marked as active, show that + first. However, if there are not.. go ahead and search for + the next Question without an Answer or an Answer that isn't complete + */ - // -- Check if there are any questions marked as active const active = questions.find(q => q.active) if (active) { return active @@ -71,23 +80,44 @@ export const selectNextAvailableQuestion = createSelector( let res = questions.filter(q => { const a: Answer | undefined = answers[q.question_id] - return !a || a.complete === false + return !a || !a.complete }) - // return res.reduce((min, q) => - // !min || q.order < min.order ? q : min, - // null as typeof res[0] | null - // ) return res[0] || null } ) +export const selectNextAvailableQuestion = createSelector( + [selectQuestions, selectAnswers], + (questions, answers): Selector<RootState, ProfileQuestion | null> => { + /* This takes the current active position and finds the next available + question to answer. -export const selectQuestionById = (questionId: string) => - createSelector( - (state: RootState) => state.questions, - (questions) => { - return questions.find(q => q.question_id === questionId); + Check if there are any questions marked as active. If there are not, + the Questions page didn't load yet and/or we don't know what the Respondent + is currently looking at... so we can't determine what is next. Immediately fail. + */ + const active = questions.find(q => q.active) + assert(active, "Must have an active Question") + const active_index = questions.findIndex(q => q.question_id === active.question_id) + + // Find any Questions without Answers, or Answers that are not complete + // that are positioned after the currently active Question + let found = questions.find((q, q_idx) => { + const a: Answer | undefined = answers[q.question_id] + return q_idx > active_index && (!a || !a.complete) + }) + + if (!found) { + // No eligible questions were found after the current active position, so + // go back and look for any before the current active position. + found = questions.find((q, q_idx) => { + const a: Answer | undefined = answers[q.question_id] + return q_idx < active_index && (!a || !a.complete) + }) } - );
\ No newline at end of file + + return found || null + } +) |
