mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-02 22:52:58 +00:00
fix(app): ignore a late Finish once the user left the last setup step
Finish called complete(), which ignored the step visit, so pressing Back while Orchestration applied its choices still closed setup and recorded it as done. next(visit) now finishes setup from the last step, so a late Finish is dropped the same way a late Continue already was.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
createOnboardingModel,
|
||||
type OnboardingCompletionStorage,
|
||||
@@ -134,7 +134,6 @@ describe("onboarding model", () => {
|
||||
advance(model);
|
||||
advance(model);
|
||||
advance(model);
|
||||
advance(model);
|
||||
expect(model.getState()).toMatchObject({ stepIndex: 3 });
|
||||
model.goTo("providers");
|
||||
expect(model.getState()).toMatchObject({ stepIndex: 1 });
|
||||
@@ -162,6 +161,25 @@ describe("onboarding model", () => {
|
||||
expect(model.getState()).toMatchObject({ status: "open", stepIndex: 2 });
|
||||
});
|
||||
|
||||
it("moving on from the last step finishes setup, unless the user left that step first", async () => {
|
||||
const storage = new MemoryCompletionStorage();
|
||||
const model = await openModel(storage);
|
||||
model.applySetupHost(FRESH_HOST, { includePhoneStep: false });
|
||||
model.goTo("orchestration");
|
||||
|
||||
// Finish is applying choices when the user goes back to Projects.
|
||||
const finishingFrom = currentVisit(model);
|
||||
model.back();
|
||||
model.next(finishingFrom);
|
||||
expect(model.getState()).toMatchObject({ status: "open", stepIndex: 2 });
|
||||
expect(storage.completed).toBe(false);
|
||||
|
||||
advance(model);
|
||||
advance(model);
|
||||
await vi.waitFor(() => expect(storage.completed).toBe(true));
|
||||
expect(model.getState()).toEqual({ status: "hidden" });
|
||||
});
|
||||
|
||||
it("records completion when setup closes, so it does not open again", async () => {
|
||||
const storage = new MemoryCompletionStorage();
|
||||
const model = await openModel(storage);
|
||||
|
||||
@@ -46,8 +46,9 @@ export interface OnboardingModel {
|
||||
applySetupHost(host: OnboardingSetupHost | null, options: OnboardingStepOptions): void;
|
||||
reopen(serverId: string, options: OnboardingStepOptions): void;
|
||||
/**
|
||||
* Moves on from the step visit the caller saw. A no-op once that visit ended, so an action that
|
||||
* finishes late never moves a step the user left or a setup they closed and reopened.
|
||||
* Moves on from the step visit the caller saw; from the last step, that finishes setup. A no-op
|
||||
* once that visit ended, so an action that finishes late never moves a step the user left or
|
||||
* closes a setup they went back in, or closed and reopened.
|
||||
*/
|
||||
next(visit: number): void;
|
||||
back(): void;
|
||||
@@ -129,6 +130,12 @@ export function createOnboardingModel(input: {
|
||||
|
||||
const ready = loadCompletion();
|
||||
|
||||
async function complete() {
|
||||
completion = "completed";
|
||||
setState({ status: "hidden" });
|
||||
await input.storage.markCompleted();
|
||||
}
|
||||
|
||||
return {
|
||||
ready,
|
||||
getState: () => state,
|
||||
@@ -146,7 +153,9 @@ export function createOnboardingModel(input: {
|
||||
setState(openState(serverId, options));
|
||||
},
|
||||
next(visit) {
|
||||
if (state.status === "open" && state.visit === visit) moveTo(state.stepIndex + 1);
|
||||
if (state.status !== "open" || state.visit !== visit) return;
|
||||
if (state.stepIndex === state.steps.length - 1) void complete();
|
||||
else moveTo(state.stepIndex + 1);
|
||||
},
|
||||
back() {
|
||||
if (state.status === "open") moveTo(state.stepIndex - 1);
|
||||
@@ -156,10 +165,6 @@ export function createOnboardingModel(input: {
|
||||
const stepIndex = state.steps.indexOf(step);
|
||||
if (stepIndex >= 0) moveTo(stepIndex);
|
||||
},
|
||||
async complete() {
|
||||
completion = "completed";
|
||||
setState({ status: "hidden" });
|
||||
await input.storage.markCompleted();
|
||||
},
|
||||
complete,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -42,11 +42,8 @@ export function OnboardingModal({ model, state }: { model: OnboardingModel; stat
|
||||
}, [model]);
|
||||
const handleBack = useCallback(() => model.back(), [model]);
|
||||
const { visit } = state;
|
||||
// Moving on from the last step finishes setup; any step's own work runs before that.
|
||||
const handleAdvance = useCallback(() => {
|
||||
if (isLast) void model.complete();
|
||||
else model.next(visit);
|
||||
}, [isLast, model, visit]);
|
||||
// A step's own work runs before it moves on; from the last step, moving on finishes setup.
|
||||
const handleAdvance = useCallback(() => model.next(visit), [model, visit]);
|
||||
const handleAddSuggestions = useCallback(() => {
|
||||
void (async () => {
|
||||
if (await addChecked()) handleAdvance();
|
||||
|
||||
Reference in New Issue
Block a user