fix(web): allow returning home from questionnaires
CI / ruff · pytest (pull_request) Successful in 2m28s

Keep the shared dialog and submitted answers while leaving the focus screen. Remove the history trap and wire the text-step home action.

Refs #85
This commit is contained in:
Pavel Eliseev
2026-10-04 16:14:37 +03:00
parent 2a2fdb171e
commit a4ee3d25f0
2 changed files with 15 additions and 15 deletions
+1
View File
@@ -204,6 +204,7 @@ export const ru = {
toIntake: "Пройти первичный опрос", toIntake: "Пройти первичный опрос",
}, },
flow: { flow: {
home: "На главный экран",
question: (n: number, total: number) => `Вопрос ${n} из ${total}`, question: (n: number, total: number) => `Вопрос ${n} из ${total}`,
period: "За последний месяц", period: "За последний месяц",
textPlaceholder: "Напишите несколько слов", textPlaceholder: "Напишите несколько слов",
+14 -15
View File
@@ -265,7 +265,7 @@ function FreeText({
{skip.label} {skip.label}
</button> </button>
) : ( ) : (
<button type="button" className="btn btn--ghost" onClick={onExit}> <button type="button" className="btn btn--ghost" disabled={busy} onClick={onExit}>
{ru.flow.exit} {ru.flow.exit}
</button> </button>
)} )}
@@ -362,6 +362,7 @@ function SurveyAct({
press, press,
say, say,
onPause, onPause,
onHome,
}: { }: {
step: Survey; step: Survey;
stepKey: string; stepKey: string;
@@ -369,6 +370,7 @@ function SurveyAct({
press: (b: DialogButton) => void; press: (b: DialogButton) => void;
say: (text: string) => void; say: (text: string) => void;
onPause: (b: DialogButton) => void; onPause: (b: DialogButton) => void;
onHome: () => void;
}) { }) {
let answer: ReactNode; let answer: ReactNode;
switch (step.input) { switch (step.input) {
@@ -381,7 +383,7 @@ function SurveyAct({
break; break;
case "text": case "text":
answer = ( answer = (
<FreeText key={stepKey} skip={step.skip} busy={busy} onSend={say} onSkip={press} onExit={() => undefined} /> <FreeText key={stepKey} skip={step.skip} busy={busy} onSend={say} onSkip={press} onExit={onHome} />
); );
break; break;
default: default:
@@ -522,17 +524,6 @@ export function Flow() {
navigate("/today", { replace: true }); navigate("/today", { replace: true });
}, [step, stepKey, navigate, toast]); }, [step, stepKey, navigate, toast]);
// Questionnaire: answers are saved on the server per question, and there is no going back to change one.
// The browser «back» is absorbed by a duplicate history entry of this same screen.
const testing = step?.kind === "survey" || step?.kind === "choice" || step?.kind === "consent";
useEffect(() => {
if (!testing) return;
window.history.pushState(null, "", window.location.href);
const stay = () => window.history.pushState(null, "", window.location.href);
window.addEventListener("popstate", stay);
return () => window.removeEventListener("popstate", stay);
}, [testing]);
const press = (b: DialogButton) => void dialog.send({ type: "button", button: b.data }, b.label); const press = (b: DialogButton) => void dialog.send({ type: "button", button: b.data }, b.label);
// «Продолжить позже»: answers are already saved on the server — pause and go straight home, no confirmation. // «Продолжить позже»: answers are already saved on the server — pause and go straight home, no confirmation.
const pause = (b: DialogButton) => const pause = (b: DialogButton) =>
@@ -588,7 +579,7 @@ export function Flow() {
) : item ? ( ) : item ? (
<Progress n={item.index} total={item.total} adaptive={item.adaptive} /> <Progress n={item.index} total={item.total} adaptive={item.adaptive} />
) : null; ) : null;
const top = <TopBar onBack={testing ? undefined : home}>{progress}</TopBar>; const top = progress ? <TopBar>{progress}</TopBar> : null;
// Phone: one column. Desktop: question and context on the left, answers and actions on the right. // Phone: one column. Desktop: question and context on the left, answers and actions on the right.
let lead: ReactNode; let lead: ReactNode;
let act: ReactNode; let act: ReactNode;
@@ -702,7 +693,7 @@ export function Flow() {
break; break;
case "survey": case "survey":
lead = <SurveyLead step={step} catalog={catalog} />; lead = <SurveyLead step={step} catalog={catalog} />;
act = <SurveyAct step={step} stepKey={stepKey} busy={dialog.busy} press={press} say={say} onPause={pause} />; act = <SurveyAct step={step} stepKey={stepKey} busy={dialog.busy} press={press} say={say} onPause={pause} onHome={home} />;
break; break;
case "choice": case "choice":
lead = <Head title={step.title} lead={step.lead} />; lead = <Head title={step.title} lead={step.lead} />;
@@ -725,6 +716,14 @@ export function Flow() {
return ( return (
<AppShell focus bare sos={step?.kind !== "crisis"}> <AppShell focus bare sos={step?.kind !== "crisis"}>
<nav className="flow-nav" aria-label={ru.flow.home}>
{/* Leaving does not send an answer or clear pending: Today can reopen this same question.
Wait for an in-flight answer before leaving so its result has reached the shared dialog. */}
<button type="button" className="btn btn--ghost btn--auto" disabled={dialog.busy} onClick={home}>
<Icon name="home" size={20} />
{ru.flow.home}
</button>
</nav>
{content} {content}
</AppShell> </AppShell>
); );