Skip to content

Commit

Permalink
Proposal: simplify navigation logic (#713)
Browse files Browse the repository at this point in the history
* refactor: remove SubflowState

* test: add unit tests for navItemsFromDoneStates() + stepStates()
  • Loading branch information
chohner committed May 10, 2024
1 parent facaaaa commit 841c141
Show file tree
Hide file tree
Showing 9 changed files with 408 additions and 260 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,13 @@ import { eigentumDone, eigentumZusammenfassungDone } from "./navStatesEigentum";
import { einkommenDone as einkommenDoneGuard } from "./guards";
import type { BeratungshilfeFinanzielleAngaben } from "./context";

export type SubflowState = "Done" | "Open";

export type FinanzielleAngabenGuard =
GenericGuard<BeratungshilfeFinanzielleAngaben>;

export const einkommenDone: FinanzielleAngabenGuard = ({ context }) =>
einkommenDoneGuard({ context });

const partnerDone: FinanzielleAngabenGuard = ({ context }) =>
export const partnerDone: FinanzielleAngabenGuard = ({ context }) =>
(context.staatlicheLeistungen != undefined &&
hasStaatlicheLeistungen({ context })) ||
["no", "widowed"].includes(context.partnerschaft ?? "") ||
Expand All @@ -25,7 +23,7 @@ const hasStaatlicheLeistungen: FinanzielleAngabenGuard = ({ context }) =>
context.staatlicheLeistungen == "buergergeld" ||
context.staatlicheLeistungen == "grundsicherung";

const kinderDone: FinanzielleAngabenGuard = ({ context }) =>
export const kinderDone: FinanzielleAngabenGuard = ({ context }) =>
context.hasKinder == "no" || context.kinder !== undefined;

const wohnungAloneDone: FinanzielleAngabenGuard = ({ context }) =>
Expand All @@ -39,18 +37,21 @@ const wohnungWithOthersDone: FinanzielleAngabenGuard = ({ context }) =>
context.apartmentCostOwnShare !== undefined &&
context.apartmentCostFull !== undefined;

const wohnungDone: FinanzielleAngabenGuard = ({ context }) =>
export const wohnungDone: FinanzielleAngabenGuard = ({ context }) =>
context.livingSituation !== undefined &&
context.apartmentSizeSqm !== undefined &&
(wohnungAloneDone({ context }) || wohnungWithOthersDone({ context }));

const andereUnterhaltszahlungenDone: FinanzielleAngabenGuard = ({ context }) =>
export const andereUnterhaltszahlungenDone: FinanzielleAngabenGuard = ({
context,
}) =>
(context.staatlicheLeistungen != undefined &&
hasStaatlicheLeistungen({ context })) ||
context.hasWeitereUnterhaltszahlungen == "no" ||
(context.unterhaltszahlungen !== undefined &&
context.unterhaltszahlungen.length > 0);
const ausgabenDone: FinanzielleAngabenGuard = ({ context }) => {

export const ausgabenDone: FinanzielleAngabenGuard = ({ context }) => {
return (
context.hasAusgaben === "no" ||
(context.hasAusgaben === "yes" &&
Expand All @@ -59,30 +60,6 @@ const ausgabenDone: FinanzielleAngabenGuard = ({ context }) => {
);
};

const subflowDoneConfig: Record<string, FinanzielleAngabenGuard> = {
einkommen: einkommenDone,
partner: partnerDone,
kinder: kinderDone,
eigentum: eigentumDone,
"eigentum-zusammenfassung": eigentumZusammenfassungDone,
wohnung: wohnungDone,
"andere-unterhaltszahlungen": andereUnterhaltszahlungenDone,
ausgaben: ausgabenDone,
};

export const beratungshilfeFinanzielleAngabenSubflowState = (
context: BeratungshilfeFinanzielleAngaben,
subflowId: string,
): SubflowState => {
if (
subflowId in subflowDoneConfig &&
subflowDoneConfig[subflowId]({ context })
) {
return "Done";
}
return "Open";
};

export const beratungshilfeFinanzielleAngabeDone: GenericGuard<
BeratungshilfeFinanzielleAngaben
> = ({ context }) => {
Expand Down
32 changes: 26 additions & 6 deletions app/models/flows/beratungshilfeFormular/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,12 @@ import { beratungshilfeAbgabeGuards } from "./abgabe/guards";
import abgabeFlow from "./abgabe/flow.json";
import { type BeratungshilfeFinanzielleAngaben } from "./finanzielleAngaben/context";
import {
beratungshilfeFinanzielleAngabeDone,
beratungshilfeFinanzielleAngabenSubflowState,
andereUnterhaltszahlungenDone,
ausgabenDone,
einkommenDone,
kinderDone,
partnerDone,
wohnungDone,
} from "./finanzielleAngaben/navStates";
import {
type BeratungshilfePersoenlicheDaten,
Expand All @@ -41,6 +45,10 @@ import {
eigentumZusammenfassungShowWarnings,
} from "./stringReplacements";
import { finanzielleAngabenArrayConfig } from "./finanzielleAngaben/arrayConfiguration";
import {
eigentumDone,
eigentumZusammenfassungDone,
} from "./finanzielleAngaben/navStatesEigentum";

export const beratungshilfeFormular = {
cmsSlug: "form-flow-pages",
Expand All @@ -60,15 +68,27 @@ export const beratungshilfeFormular = {
meta: { done: rechtsproblemDone },
}),
"finanzielle-angaben": _.merge(finanzielleAngabenFlow, {
meta: {
done: beratungshilfeFinanzielleAngabeDone,
subflowState: beratungshilfeFinanzielleAngabenSubflowState,
states: {
einkommen: { meta: { done: einkommenDone } },
partner: { meta: { done: partnerDone } },
kinder: { meta: { done: kinderDone } },
"andere-unterhaltszahlungen": {
meta: { done: andereUnterhaltszahlungenDone },
},
eigentum: { meta: { done: eigentumDone } },
eigentumZusammenfassung: {
meta: { done: eigentumZusammenfassungDone },
},
wohnung: { meta: { done: wohnungDone } },
ausgaben: { meta: { done: ausgabenDone } },
},
}),
"persoenliche-daten": _.merge(persoenlicheDatenFlow, {
meta: { done: beratungshilfePersoenlicheDatenDone },
}),
abgabe: abgabeFlow,
abgabe: _.merge(abgabeFlow, {
meta: { done: () => false },
}),
},
}),
guards: {
Expand Down
5 changes: 0 additions & 5 deletions app/models/flows/flows.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,3 @@ export const flows = {
"fluggastrechte/vorabcheck": fluggastrechteVorabcheck,
"fluggastrechte/formular": fluggastrechtFlow,
} as const satisfies Record<FlowId, Flow>;

export function getSubflowsEntries(config: Config) {
if (!config.states) return [];
return Object.entries(config.states).filter(([, state]) => "states" in state);
}
13 changes: 7 additions & 6 deletions app/routes/shared/formular.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { validatedSession } from "~/services/security/csrf.server";
import { throw404IfFeatureFlagEnabled } from "~/services/errorPages/throw404";
import { logError } from "~/services/logging";
import { getMigrationData } from "~/services/session.server/crossFlowMigration";
import { navItemsFromFlowSpecifics } from "~/services/flowNavigation.server";
import { navItemsFromStepStates } from "~/services/flowNavigation.server";
import type { z } from "zod";
import type { CollectionSchemas } from "~/services/cms/schemas";
import { getButtonNavigationProps } from "~/util/buttonProps";
Expand Down Expand Up @@ -161,11 +161,12 @@ export const loader = async ({
backDestination: backDestinationWithArrayIndexes,
});

const navItems = navItemsFromFlowSpecifics(
stepId,
flowController,
navigationStrings,
);
const navItems =
navItemsFromStepStates(
stepId,
flowController.stepStates(),
navigationStrings,
) ?? [];

const migrationData = await getMigrationData(
stepId,
Expand Down
68 changes: 60 additions & 8 deletions app/services/flow/server/buildFlowController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import {
stepIdToPath,
} from "~/services/flow/stepIdConverter";
import type { Context } from "~/models/flows/contexts";
import type { SubflowState } from "~/models/flows/beratungshilfeFormular/finanzielleAngaben/navStates";
import type { GenericGuard, Guards } from "~/models/flows/guards.server";
import _ from "lodash";
import type { ArrayConfig } from "~/services/array";
Expand Down Expand Up @@ -44,8 +43,6 @@ type Meta = {
progressPosition: number | undefined;
isUneditable: boolean | undefined;
done: GenericGuard<Context>;
subflowState: (context: Context, subflowId: string) => SubflowState;
subflowDone: (context: Context, subflowId: string) => boolean | undefined;
arrays?: Record<string, ArrayConfig>;
};

Expand Down Expand Up @@ -127,6 +124,63 @@ function getInitial(machine: FlowStateMachine) {
return stateValueToStepIds(initialSnapshot.value).pop();
}

export type StepState = {
stepId: string;
isDone: boolean;
isReachable: boolean;
isUneditable: boolean;
url: string;
subStates?: StepState[];
};

function stepStates(
stateNode: FlowStateMachine["states"][string],
reachableSteps: string[],
): StepState[] {
// Recurse a statenode until encountering a done function or no more substates are left
// For each encountered statenode a StepState object is returned, containing whether the state is reachable, done and its URL
const context = (stateNode.machine.config.context ?? {}) as Context;

const statesWithDoneFunctionOrSubstates = Object.values(
stateNode.states ?? {},
).filter((state) => state.meta?.done || Object.keys(state.states).length > 0);

Check warning on line 146 in app/services/flow/server/buildFlowController.ts

View workflow job for this annotation

GitHub Actions / code-quality / npm run lint

Unsafe member access .done on an `any` value

return statesWithDoneFunctionOrSubstates.map((state) => {
const stepId = stateValueToStepIds(pathToStateValue(state.path))[0];
const meta = state.meta as Meta | undefined;
const hasDoneFunction = meta?.done !== undefined;
const isUneditable = Boolean(meta?.isUneditable);
const reachableSubStates = stepStates(state, reachableSteps).filter(
(state) => state.isReachable,
);

// Ignore subflows if empty or parent state has done function
if (hasDoneFunction || reachableSubStates.length === 0) {
const initial = state.config.initial as string | undefined;
const initialStepId = initial ? `${stepId}/${initial}` : stepId;

return {
url: `${state.machine.id}${initialStepId}`,
isDone: hasDoneFunction ? meta.done({ context }) : false,
stepId,
isUneditable,
isReachable: reachableSteps.includes(initialStepId),
};
}

return {
url: `${state.machine.id}${stepId}`,
isDone: reachableSubStates.every((state) => state.isDone),
stepId,
isUneditable,
isReachable: reachableSubStates.length > 0,
subStates: reachableSubStates,
};
});
}

export type FlowController = ReturnType<typeof buildFlowController>;

export const buildFlowController = ({
config,
data: context = {},
Expand All @@ -141,22 +195,20 @@ export const buildFlowController = ({
guards,
}).createMachine({ ...config, context });
const baseUrl = config.id ?? "";
const reachableSteps = getSteps(machine); // depends on context

return {
getMeta: (currentStepId: string) => metaFromStepId(machine, currentStepId),
getRootMeta: () => rootMeta(machine),
stepStates: () => stepStates(machine.root, reachableSteps),
isDone: (currentStepId: string) =>
Boolean(metaFromStepId(machine, currentStepId)?.done({ context })),
getSubflowState: (currentStepId: string, subflowId: string) =>
metaFromStepId(machine, currentStepId)?.subflowState(context, subflowId),
isUneditable: (currentStepId: string) =>
Boolean(metaFromStepId(machine, currentStepId)?.isUneditable),
getConfig: () => config,
isFinal: (currentStepId: string) => isFinalStep(machine, currentStepId),
isReachable: (currentStepId: string) => {
// depends on context
const steps = getSteps(machine);
return steps.includes(currentStepId);
return reachableSteps.includes(currentStepId);
},
getPrevious: (stepId: string) => {
const backArray = transitionDestinations(
Expand Down

0 comments on commit 841c141

Please sign in to comment.