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
@@ -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
@@ -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,
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export const OnboardingChecklist = () => {
|
|||||||
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",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user