mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
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:
co-authored by
Théo LAGACHE
Charles GTE
parent
9c2359e87b
commit
45acc781f2
@@ -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",
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user