fix: onboarding (#354)

* fix

* fix: README.md

---------

Co-authored-by: Théo LAGACHE <theo.lagache@soluce-technologies.com>
Co-authored-by: Charles GTE <charlesgte31@gmail.com>
This commit is contained in:
Théo LAGACHE
2026-07-04 14:43:58 +02:00
committed by GitHub
co-authored by Théo LAGACHE Charles GTE
parent 9c2359e87b
commit 45acc781f2
11 changed files with 160 additions and 16 deletions
@@ -2,6 +2,7 @@ export const STEP_IDS = {
LOGIN: "login",
ACCOUNT_INFO: "account-info",
SECURITY: "security",
PREFERENCES: "preferences",
ORG_CREATE: "org-create",
//INVITE_MEMBERS: "invite-members",
NOTIFIER: "notifier",
@@ -19,6 +20,7 @@ export const STEP_ORDER: string[] = [
STEP_IDS.LOGIN,
STEP_IDS.ACCOUNT_INFO,
STEP_IDS.SECURITY,
STEP_IDS.PREFERENCES,
STEP_IDS.ORG_CREATE,
//STEP_IDS.INVITE_MEMBERS,
STEP_IDS.NOTIFIER,
@@ -61,9 +61,9 @@ export const OnboardingChecklist = () => {
<p
className={cn(
"text-sm pb-4",
"text-sm pb-4 ",
isCompleted && "text-zinc-400",
isCurrent && "text-white font-medium",
isCurrent && "text-primary font-medium",
!isCompleted && !isCurrent && "text-zinc-600",
)}
>
+2 -2
View File
@@ -84,8 +84,8 @@ export async function resolveOnboardingState(): Promise<ResolvedOnboardingState>
const org = await getUserOwnOrganization(user.id);
if (!org) {
meta.resumeStepId = "org-create";
return { stepId: "org-create", flowData: { meta } };
meta.resumeStepId = "preferences";
return { stepId: "preferences", flowData: { meta } };
}
const orgData = { id: org.id, name: org.name };
+10 -2
View File
@@ -2,6 +2,7 @@ import { OnboardingStep } from "@onboardjs/react";
import { StepLogin } from "@/features/onboarding/steps/step-login";
import { StepAccountInfo } from "@/features/onboarding/steps/step-account-info";
import { StepSecurity } from "@/features/onboarding/steps/step-security";
import { StepPreferences } from "@/features/onboarding/steps/step-preferences";
import { StepOrgCreate } from "@/features/onboarding/steps/step-org-create";
import { StepInviteMembers } from "@/features/onboarding/steps/step-invite-members";
import { StepNotifier } from "@/features/onboarding/steps/step-notifier";
@@ -20,19 +21,26 @@ export const onboardingSteps: OnboardingStep[] = [
component: StepLogin,
isSkippable: false,
nextStep: (ctx: any) =>
ctx.flowData?.meta?.hasExistingUsers ? "org-create" : "account-info",
ctx.flowData?.meta?.hasExistingUsers ? "preferences" : "account-info",
},
{
id: "account-info",
component: StepAccountInfo,
isSkippable: false,
nextStep: (ctx: any) =>
ctx.flowData?.meta?.passkeyEnabled ? "org-create" : "security",
ctx.flowData?.meta?.passkeyEnabled ? "preferences" : "security",
},
{
id: "security",
component: StepSecurity,
isSkippable: true,
skipToStep: "preferences",
nextStep: "preferences",
},
{
id: "preferences",
component: StepPreferences,
isSkippable: true,
skipToStep: "org-create",
nextStep: "org-create",
},
@@ -70,7 +70,16 @@ export const StepAgentCreate = () => {
</Button>
</div>
<Button type="button" onClick={() => next()}>
<Button
type="button"
onClick={() => next()}
disabled={!!name.trim() || createAgent.isPending}
title={
name.trim()
? "Add or clear the agent name before continuing"
: undefined
}
>
Continue
</Button>
</div>
@@ -35,6 +35,8 @@ export const StepDefaults = () => {
const [storageId, setStorageId] = useState<string | undefined>(
existingDefaults.storageId || undefined,
);
const [notifierOpen, setNotifierOpen] = useState(false);
const [storageOpen, setStorageOpen] = useState(false);
const selectNotifier = async (value: string) => {
const prev = notifierId;
@@ -56,6 +58,26 @@ export const StepDefaults = () => {
}
};
const clearNotifier = async () => {
const prev = notifierId;
setNotifierId(undefined);
try {
await updateNotificationSettingsAction({
name: "system",
data: { notificationChannelId: null },
});
await updateContext({
flowData: {
...state?.context.flowData,
defaults: { notifierId: undefined, storageId },
},
});
} catch {
setNotifierId(prev);
toast.error("Failed to clear default notifier");
}
};
const selectStorage = async (value: string) => {
const prev = storageId;
setStorageId(value);
@@ -76,6 +98,26 @@ export const StepDefaults = () => {
}
};
const clearStorage = async () => {
const prev = storageId;
setStorageId(undefined);
try {
await updateStorageSettingsAction({
name: "system",
data: { storageChannelId: null, encryption: false },
});
await updateContext({
flowData: {
...state?.context.flowData,
defaults: { notifierId, storageId: undefined },
},
});
} catch {
setStorageId(prev);
toast.error("Failed to clear default storage");
}
};
const onContinue = async () => {
await updateContext({
flowData: {
@@ -102,8 +144,10 @@ export const StepDefaults = () => {
<div className="flex flex-col gap-2">
<Label>Default notifier</Label>
<Select
value={selectedNotifier ? notifierId : undefined}
value={selectedNotifier ? notifierId : ""}
onValueChange={selectNotifier}
open={notifierOpen}
onOpenChange={setNotifierOpen}
disabled={notifiers.length === 0}
>
<SelectTrigger className="w-full">
@@ -128,7 +172,17 @@ export const StepDefaults = () => {
</SelectTrigger>
<SelectContent>
{notifiers.map((n) => (
<SelectItem key={n.id} value={n.id}>
<SelectItem
key={n.id}
value={n.id}
onPointerUp={(e) => {
if (n.id === notifierId) {
e.preventDefault();
clearNotifier();
setNotifierOpen(false);
}
}}
>
<div className="flex items-center gap-2 w-full min-w-0">
<div className="text-muted-foreground scale-90 shrink-0">
{getChannelIcon(n.provider)}
@@ -149,8 +203,10 @@ export const StepDefaults = () => {
<div className="flex flex-col gap-2">
<Label>Default storage</Label>
<Select
value={selectedStorage ? storageId : undefined}
value={selectedStorage ? storageId : ""}
onValueChange={selectStorage}
open={storageOpen}
onOpenChange={setStorageOpen}
disabled={storages.length === 0}
>
<SelectTrigger className="w-full">
@@ -175,7 +231,17 @@ export const StepDefaults = () => {
</SelectTrigger>
<SelectContent>
{storages.map((s) => (
<SelectItem key={s.id} value={s.id}>
<SelectItem
key={s.id}
value={s.id}
onPointerUp={(e) => {
if (s.id === storageId) {
e.preventDefault();
clearStorage();
setStorageOpen(false);
}
}}
>
<div className="flex items-center gap-2 w-full min-w-0">
<div className="text-muted-foreground scale-90 shrink-0">
{getChannelIcon(s.provider)}
@@ -0,0 +1,56 @@
"use client";
import { useOnboarding } from "@onboardjs/react";
import { useTheme } from "next-themes";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { authClient } from "@/lib/auth/auth-client";
import { ThemeKey, ThemeSelector } from "@/components/common/theme-selector";
export const StepPreferences = () => {
const { next, updateContext, state } = useOnboarding();
const { theme: currentTheme, setTheme } = useTheme();
const preferences = state?.context.flowData.preferences ?? {
theme: (currentTheme ?? "system") as ThemeKey,
};
const selectTheme = async (theme: ThemeKey) => {
setTheme(theme);
try {
await authClient.updateUser({ theme });
await updateContext({
flowData: {
...state?.context.flowData,
preferences: { ...preferences, theme },
},
});
} catch {
toast.error("Failed to save theme preference");
setTheme(preferences.theme as ThemeKey);
}
};
const onContinue = async () => {
await next();
};
return (
<div className="flex flex-col gap-6">
<div>
<h1 className="text-2xl font-semibold">Your preferences</h1>
<p className="text-sm text-muted-foreground mt-1">
Optional personalise your workspace.
</p>
</div>
<div className="flex flex-col gap-2">
<p className="text-sm font-medium">Theme</p>
<ThemeSelector value={preferences.theme} onSelect={selectTheme} />
</div>
<Button type="button" onClick={onContinue}>
Continue
</Button>
</div>
);
};
@@ -24,7 +24,7 @@ export const updateStorageSettingsAction = userAction
const [updatedSettings] = await db
.update(drizzleDb.schemas.setting)
.set(withUpdatedAt({
defaultStorageChannelId: data.storageChannelId,
defaultStorageChannelId: data.storageChannelId ?? null,
encryption: data.encryption,
}))
.where(eq(drizzleDb.schemas.setting.name, name))
@@ -1,7 +1,7 @@
import {z} from "zod";
export const DefaultStorageSchema = z.object({
storageChannelId: z.string(),
storageChannelId: z.string().optional().nullable(),
encryption: z.boolean(),
});