Change state handling on onboarding

This commit is contained in:
Silvano Cerza 2025-01-21 23:39:05 +01:00
parent ee5a5f4bfb
commit ee1bc30912

View file

@ -1,14 +1,28 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
type Step = const STEPS = [
| "welcome" "welcome",
| "repo" "repo",
| "token" "token",
| "folders" "folders",
| "sync" "sync",
| "interface" "interface",
| "first_sync" "first_sync",
| "done"; "done",
] as const;
type Step = (typeof STEPS)[number];
type StepData = {
repo?: { owner: string; repo: string; branch: string };
token?: { token: string };
folders?: { repoFolder: string; vaultFolder: string };
sync?: {
mode: "manual" | "interval";
syncOnStart: boolean;
onConflict: "ignore" | "ask" | "overwrite";
};
};
const WelcomeStepComponent = () => { const WelcomeStepComponent = () => {
return ( return (
@ -34,20 +48,12 @@ const WelcomeStepComponent = () => {
}; };
const RepoStepComponent = ({ const RepoStepComponent = ({
setIsValid, values = { owner: "", repo: "", branch: "" },
onChange,
}: { }: {
setIsValid: (valid: boolean) => void; values?: { owner: string; repo: string; branch: string };
onChange: (values: { owner: string; repo: string; branch: string }) => void;
}) => { }) => {
const [owner, setOwner] = useState("");
const [repo, setRepo] = useState("");
const [branch, setBranch] = useState("");
useEffect(() => {
setIsValid(
owner.trim() !== "" && repo.trim() !== "" && branch.trim() !== "",
);
}, [owner, repo, branch]);
return ( return (
<div <div
style={{ style={{
@ -86,24 +92,24 @@ const RepoStepComponent = ({
spellCheck="false" spellCheck="false"
placeholder="Owner" placeholder="Owner"
style={{ width: "100%" }} style={{ width: "100%" }}
value={owner} value={values.owner}
onChange={(e) => setOwner(e.target.value)} onChange={(e) => onChange({ ...values, owner: e.target.value })}
/> />
<input <input
type="text" type="text"
spellCheck="false" spellCheck="false"
placeholder="Repository" placeholder="Repository"
style={{ width: "100%" }} style={{ width: "100%" }}
value={repo} value={values.repo}
onChange={(e) => setRepo(e.target.value)} onChange={(e) => onChange({ ...values, repo: e.target.value })}
/> />
<input <input
type="text" type="text"
spellCheck="false" spellCheck="false"
placeholder="Branch" placeholder="Branch"
style={{ width: "100%" }} style={{ width: "100%" }}
value={branch} value={values.branch}
onChange={(e) => setBranch(e.target.value)} onChange={(e) => onChange({ ...values, branch: e.target.value })}
/> />
</div> </div>
</div> </div>
@ -111,16 +117,12 @@ const RepoStepComponent = ({
}; };
const TokenStepComponent = ({ const TokenStepComponent = ({
setIsValid, values = { token: "" },
onChange,
}: { }: {
setIsValid: (valid: boolean) => void; values?: { token: string };
onChange: (values: { token: string }) => void;
}) => { }) => {
const [token, setToken] = useState("");
useEffect(() => {
setIsValid(token.trim() !== "");
}, [token]);
return ( return (
<div <div
style={{ style={{
@ -164,14 +166,20 @@ const TokenStepComponent = ({
spellCheck="false" spellCheck="false"
placeholder="Token" placeholder="Token"
style={{ width: "100%" }} style={{ width: "100%" }}
value={token} value={values.token}
onChange={(e) => setToken(e.target.value)} onChange={(e) => onChange({ token: e.target.value })}
/> />
</div> </div>
); );
}; };
const FoldersStepComponent = () => { const FoldersStepComponent = ({
values = { repoFolder: "", vaultFolder: "" },
onChange,
}: {
values?: { repoFolder: string; vaultFolder: string };
onChange: (values: { repoFolder: string; vaultFolder: string }) => void;
}) => {
return ( return (
<div <div
style={{ style={{
@ -200,12 +208,16 @@ const FoldersStepComponent = () => {
type="text" type="text"
spellCheck="false" spellCheck="false"
placeholder="Repository folder" placeholder="Repository folder"
value={values.repoFolder}
onChange={(e) => onChange({ ...values, repoFolder: e.target.value })}
style={{ width: "100%" }} style={{ width: "100%" }}
/> />
<input <input
type="text" type="text"
spellCheck="false" spellCheck="false"
placeholder="Vault folder" placeholder="Vault folder"
value={values.vaultFolder}
onChange={(e) => onChange({ ...values, vaultFolder: e.target.value })}
style={{ width: "100%" }} style={{ width: "100%" }}
/> />
</div> </div>
@ -213,13 +225,21 @@ const FoldersStepComponent = () => {
); );
}; };
const SyncSettingsStepComponent = () => { const SyncSettingsStepComponent = ({
const [syncMode, setSyncMode] = useState<"manual" | "interval">("manual"); values = { mode: "manual", syncOnStart: false, onConflict: "overwrite" },
const [syncOnStart, setSyncOnStart] = useState(false); onChange,
const [onConflict, setOnConflict] = useState<"ignore" | "ask" | "overwrite">( }: {
"overwrite", values?: {
); mode: "manual" | "interval";
syncOnStart: boolean;
onConflict: "ignore" | "ask" | "overwrite";
};
onChange: (values: {
mode: "manual" | "interval";
syncOnStart: boolean;
onConflict: "ignore" | "ask" | "overwrite";
}) => void;
}) => {
return ( return (
<div <div
style={{ style={{
@ -236,18 +256,25 @@ const SyncSettingsStepComponent = () => {
</p> </p>
<select <select
className="dropdown" className="dropdown"
value={syncMode} value={values.mode}
onChange={(e) => setSyncMode(e.target.value)} onChange={(e) =>
onChange({
...values,
mode: e.target.value as typeof values.mode,
})
}
> >
<option value="manual">Manually</option> <option value="manual">Manually</option>
<option value="interval">On Interval</option> <option value="interval">On Interval</option>
</select> </select>
<p>Optionally you can sync every time you open Obsidian.</p> <p>Optionally you can sync every time you open Obsidian.</p>
<div <div
className={`checkbox-container ${syncOnStart ? "is-enabled" : ""}`} className={`checkbox-container ${values.syncOnStart ? "is-enabled" : ""}`}
onClick={() => setSyncOnStart(!syncOnStart)} onClick={() =>
onChange({ ...values, syncOnStart: !values.syncOnStart })
}
> >
<input type="checkbox" checked={syncOnStart} /> <input type="checkbox" readOnly={true} checked={values.syncOnStart} />
</div> </div>
<p>In case of conflicts you can choose how to handle them.</p> <p>In case of conflicts you can choose how to handle them.</p>
<p> <p>
@ -257,8 +284,13 @@ const SyncSettingsStepComponent = () => {
<select <select
className="dropdown" className="dropdown"
disabled={true} disabled={true}
value={onConflict} value={values.onConflict}
onChange={(e) => setOnConflict(e.target.value)} onChange={(e) =>
onChange({
...values,
onConflict: e.target.value as typeof values.onConflict,
})
}
> >
<option value="ignore">Ignore remote file</option> <option value="ignore">Ignore remote file</option>
<option value="ask">Ask</option> <option value="ask">Ask</option>
@ -306,57 +338,51 @@ const FirstSyncStepComponent = ({
}; };
const OnBoardingComponent = () => { const OnBoardingComponent = () => {
const [step, setStep] = useState<Step>("welcome");
// It starts as true because the first step is just a welcome message // It starts as true because the first step is just a welcome message
// so the button is already enabled. // so the button is already enabled.
const [isValid, setIsValid] = useState(true); const [isValid, setIsValid] = useState<boolean>(true);
const [step, setStep] = useState<Step>("welcome");
const [stepData, setStepData] = useState<StepData>({});
const updateStepData = (step: Step, data: StepData[keyof StepData]) => {
setStepData((stepData) => {
return {
...stepData,
[step]: data,
};
});
};
const isStepValid = (step: Step) => {
switch (step) {
// Only these steps have required data, the others don't
// so the step is automatically valid
case "repo":
const { owner, repo, branch } = stepData.repo ?? {};
return Boolean(owner && repo && branch);
case "token":
return Boolean(stepData.token?.token);
case "sync":
default:
return true;
}
};
useEffect(() => {
setIsValid(isStepValid(step));
}, [step, stepData]);
const previousStep = () => { const previousStep = () => {
setStep((step) => { setStep((currentStep) => {
switch (step) { const currentIndex = STEPS.indexOf(currentStep);
case "welcome": return STEPS[currentIndex - 1] ?? STEPS[0];
return "welcome";
case "repo":
// Reset the valid state to true since we come back to welcome
setIsValid(true);
return "welcome";
case "token":
return "repo";
case "folders":
return "token";
case "sync":
return "folders";
case "interface":
return "sync";
case "first_sync":
return "interface";
case "done":
return "done";
}
}); });
}; };
const nextStep = () => { const nextStep = () => {
setStep((step) => { setStep((currentStep) => {
switch (step) { const currentIndex = STEPS.indexOf(currentStep);
case "welcome": return STEPS[currentIndex + 1] ?? STEPS[STEPS.length - 1];
return "repo";
case "repo":
return "token";
case "token":
return "folders";
case "folders":
return "sync";
case "sync":
return "interface";
case "interface":
return "first_sync";
case "first_sync":
return "done";
case "done":
return "done";
}
}); });
}; };
@ -365,13 +391,33 @@ const OnBoardingComponent = () => {
case "welcome": case "welcome":
return <WelcomeStepComponent />; return <WelcomeStepComponent />;
case "repo": case "repo":
return <RepoStepComponent setIsValid={setIsValid} />; return (
<RepoStepComponent
values={stepData.repo}
onChange={(data) => updateStepData("repo", data)}
/>
);
case "token": case "token":
return <TokenStepComponent setIsValid={setIsValid} />; return (
<TokenStepComponent
values={stepData.token}
onChange={(data) => updateStepData("token", data)}
/>
);
case "folders": case "folders":
return <FoldersStepComponent />; return (
<FoldersStepComponent
values={stepData.folders}
onChange={(data) => updateStepData("folders", data)}
/>
);
case "sync": case "sync":
return <SyncSettingsStepComponent />; return (
<SyncSettingsStepComponent
values={stepData.sync}
onChange={(data) => updateStepData("sync", data)}
/>
);
case "first_sync": case "first_sync":
return <FirstSyncStepComponent setIsValid={setIsValid} />; return <FirstSyncStepComponent setIsValid={setIsValid} />;
// case "done": // case "done":