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
View File
@@ -112,5 +112,3 @@ Distributed under the Apache License. See `LICENSE.txt` for more details.
[ShadcnUI-url]: https://ui.shadcn.com/ [ShadcnUI-url]: https://ui.shadcn.com/
[Docker-url]: https://www.docker.com/ [Docker-url]: https://www.docker.com/
+6 -1
View File
@@ -14,7 +14,12 @@ const queryClient = new QueryClient();
export const Providers = (props: ProviderProps) => { export const Providers = (props: ProviderProps) => {
return ( return (
<Suspense fallback={null}> <Suspense fallback={null}>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem> <ThemeProvider
attribute="class"
defaultTheme="light"
enableSystem
storageKey="portabase_theme"
>
<ThemeMetaUpdater /> <ThemeMetaUpdater />
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<ErrorLayout> <ErrorLayout>
@@ -2,6 +2,7 @@ export const STEP_IDS = {
LOGIN: "login", LOGIN: "login",
ACCOUNT_INFO: "account-info", ACCOUNT_INFO: "account-info",
SECURITY: "security", SECURITY: "security",
PREFERENCES: "preferences",
ORG_CREATE: "org-create", ORG_CREATE: "org-create",
//INVITE_MEMBERS: "invite-members", //INVITE_MEMBERS: "invite-members",
NOTIFIER: "notifier", NOTIFIER: "notifier",
@@ -19,6 +20,7 @@ export const STEP_ORDER: string[] = [
STEP_IDS.LOGIN, STEP_IDS.LOGIN,
STEP_IDS.ACCOUNT_INFO, STEP_IDS.ACCOUNT_INFO,
STEP_IDS.SECURITY, STEP_IDS.SECURITY,
STEP_IDS.PREFERENCES,
STEP_IDS.ORG_CREATE, STEP_IDS.ORG_CREATE,
//STEP_IDS.INVITE_MEMBERS, //STEP_IDS.INVITE_MEMBERS,
STEP_IDS.NOTIFIER, STEP_IDS.NOTIFIER,
@@ -61,9 +61,9 @@ export const OnboardingChecklist = () => {
<p <p
className={cn( className={cn(
"text-sm pb-4", "text-sm pb-4 ",
isCompleted && "text-zinc-400", isCompleted && "text-zinc-400",
isCurrent && "text-white font-medium", isCurrent && "text-primary font-medium",
!isCompleted && !isCurrent && "text-zinc-600", !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); const org = await getUserOwnOrganization(user.id);
if (!org) { if (!org) {
meta.resumeStepId = "org-create"; meta.resumeStepId = "preferences";
return { stepId: "org-create", flowData: { meta } }; return { stepId: "preferences", flowData: { meta } };
} }
const orgData = { id: org.id, name: org.name }; 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 { StepLogin } from "@/features/onboarding/steps/step-login";
import { StepAccountInfo } from "@/features/onboarding/steps/step-account-info"; import { StepAccountInfo } from "@/features/onboarding/steps/step-account-info";
import { StepSecurity } from "@/features/onboarding/steps/step-security"; 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 { StepOrgCreate } from "@/features/onboarding/steps/step-org-create";
import { StepInviteMembers } from "@/features/onboarding/steps/step-invite-members"; import { StepInviteMembers } from "@/features/onboarding/steps/step-invite-members";
import { StepNotifier } from "@/features/onboarding/steps/step-notifier"; import { StepNotifier } from "@/features/onboarding/steps/step-notifier";
@@ -20,19 +21,26 @@ export const onboardingSteps: OnboardingStep[] = [
component: StepLogin, component: StepLogin,
isSkippable: false, isSkippable: false,
nextStep: (ctx: any) => nextStep: (ctx: any) =>
ctx.flowData?.meta?.hasExistingUsers ? "org-create" : "account-info", ctx.flowData?.meta?.hasExistingUsers ? "preferences" : "account-info",
}, },
{ {
id: "account-info", id: "account-info",
component: StepAccountInfo, component: StepAccountInfo,
isSkippable: false, isSkippable: false,
nextStep: (ctx: any) => nextStep: (ctx: any) =>
ctx.flowData?.meta?.passkeyEnabled ? "org-create" : "security", ctx.flowData?.meta?.passkeyEnabled ? "preferences" : "security",
}, },
{ {
id: "security", id: "security",
component: StepSecurity, component: StepSecurity,
isSkippable: true, isSkippable: true,
skipToStep: "preferences",
nextStep: "preferences",
},
{
id: "preferences",
component: StepPreferences,
isSkippable: true,
skipToStep: "org-create", skipToStep: "org-create",
nextStep: "org-create", nextStep: "org-create",
}, },
@@ -70,7 +70,16 @@ export const StepAgentCreate = () => {
</Button> </Button>
</div> </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 Continue
</Button> </Button>
</div> </div>
@@ -35,6 +35,8 @@ export const StepDefaults = () => {
const [storageId, setStorageId] = useState<string | undefined>( const [storageId, setStorageId] = useState<string | undefined>(
existingDefaults.storageId || undefined, existingDefaults.storageId || undefined,
); );
const [notifierOpen, setNotifierOpen] = useState(false);
const [storageOpen, setStorageOpen] = useState(false);
const selectNotifier = async (value: string) => { const selectNotifier = async (value: string) => {
const prev = notifierId; 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 selectStorage = async (value: string) => {
const prev = storageId; const prev = storageId;
setStorageId(value); 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 () => { const onContinue = async () => {
await updateContext({ await updateContext({
flowData: { flowData: {
@@ -102,8 +144,10 @@ export const StepDefaults = () => {
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>Default notifier</Label> <Label>Default notifier</Label>
<Select <Select
value={selectedNotifier ? notifierId : undefined} value={selectedNotifier ? notifierId : ""}
onValueChange={selectNotifier} onValueChange={selectNotifier}
open={notifierOpen}
onOpenChange={setNotifierOpen}
disabled={notifiers.length === 0} disabled={notifiers.length === 0}
> >
<SelectTrigger className="w-full"> <SelectTrigger className="w-full">
@@ -128,7 +172,17 @@ export const StepDefaults = () => {
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{notifiers.map((n) => ( {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="flex items-center gap-2 w-full min-w-0">
<div className="text-muted-foreground scale-90 shrink-0"> <div className="text-muted-foreground scale-90 shrink-0">
{getChannelIcon(n.provider)} {getChannelIcon(n.provider)}
@@ -149,8 +203,10 @@ export const StepDefaults = () => {
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>Default storage</Label> <Label>Default storage</Label>
<Select <Select
value={selectedStorage ? storageId : undefined} value={selectedStorage ? storageId : ""}
onValueChange={selectStorage} onValueChange={selectStorage}
open={storageOpen}
onOpenChange={setStorageOpen}
disabled={storages.length === 0} disabled={storages.length === 0}
> >
<SelectTrigger className="w-full"> <SelectTrigger className="w-full">
@@ -175,7 +231,17 @@ export const StepDefaults = () => {
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{storages.map((s) => ( {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="flex items-center gap-2 w-full min-w-0">
<div className="text-muted-foreground scale-90 shrink-0"> <div className="text-muted-foreground scale-90 shrink-0">
{getChannelIcon(s.provider)} {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 const [updatedSettings] = await db
.update(drizzleDb.schemas.setting) .update(drizzleDb.schemas.setting)
.set(withUpdatedAt({ .set(withUpdatedAt({
defaultStorageChannelId: data.storageChannelId, defaultStorageChannelId: data.storageChannelId ?? null,
encryption: data.encryption, encryption: data.encryption,
})) }))
.where(eq(drizzleDb.schemas.setting.name, name)) .where(eq(drizzleDb.schemas.setting.name, name))
@@ -1,7 +1,7 @@
import {z} from "zod"; import {z} from "zod";
export const DefaultStorageSchema = z.object({ export const DefaultStorageSchema = z.object({
storageChannelId: z.string(), storageChannelId: z.string().optional().nullable(),
encryption: z.boolean(), encryption: z.boolean(),
}); });