diff --git a/src/features/onboarding/hooks/use-add-notifier.ts b/src/features/onboarding/hooks/use-add-notifier.ts new file mode 100644 index 00000000..f8dc2c70 --- /dev/null +++ b/src/features/onboarding/hooks/use-add-notifier.ts @@ -0,0 +1,40 @@ +// src/features/onboarding/hooks/use-add-notifier.ts +"use client"; + +import { useMutation } from "@tanstack/react-query"; +import { useOnboarding } from "@onboardjs/react"; +import { toast } from "sonner"; +import { addNotificationChannelAction } from "@/features/channel/notifications/channel.action"; +import type { OnboardingChannel } from "@/features/onboarding/types"; + +type NotifierInput = { + provider: string; + name: string; + config: Record; + label: string; +}; + +export const useAddNotifier = () => { + const { state, updateContext } = useOnboarding(); + + return useMutation({ + mutationFn: async ({ provider, name, config, label }: NotifierInput) => { + const orgId = (state?.context.flowData.org as any)?.id as string | undefined; + const result = await addNotificationChannelAction({ + organizationId: orgId, + data: { provider: provider as any, name, config: config as any, enabled: true }, + }); + const inner = result?.data; + if (!inner?.success || !inner.value) throw new Error("Failed to save channel"); + + const channel: OnboardingChannel = { id: inner.value.id, provider, label, name, config }; + const notifiers = [ + ...((state?.context.flowData.notifiers ?? []) as OnboardingChannel[]), + channel, + ]; + await updateContext({ flowData: { ...state?.context.flowData, notifiers } }); + return channel; + }, + onError: (err: Error) => toast.error(err.message), + }); +}; diff --git a/src/features/onboarding/hooks/use-remove-notifier.ts b/src/features/onboarding/hooks/use-remove-notifier.ts new file mode 100644 index 00000000..836a9bbf --- /dev/null +++ b/src/features/onboarding/hooks/use-remove-notifier.ts @@ -0,0 +1,28 @@ +// src/features/onboarding/hooks/use-remove-notifier.ts +"use client"; + +import { useMutation } from "@tanstack/react-query"; +import { useOnboarding } from "@onboardjs/react"; +import { toast } from "sonner"; +import { removeNotificationChannelAction } from "@/features/channel/notifications/channel.action"; +import type { OnboardingChannel } from "@/features/onboarding/types"; + +export const useRemoveNotifier = () => { + const { state, updateContext } = useOnboarding(); + + return useMutation({ + mutationFn: async (id: string) => { + const orgId = (state?.context.flowData.org as any)?.id as string | undefined; + const result = await removeNotificationChannelAction({ + organizationId: orgId, + notificationChannelId: id, + }); + if (result?.data?.success === false) throw new Error("Failed to remove channel"); + const notifiers = ( + (state?.context.flowData.notifiers ?? []) as OnboardingChannel[] + ).filter((c) => c.id !== id); + await updateContext({ flowData: { ...state?.context.flowData, notifiers } }); + }, + onError: (err: Error) => toast.error(err.message), + }); +}; diff --git a/src/features/onboarding/steps/step-notifier.tsx b/src/features/onboarding/steps/step-notifier.tsx index d1aab036..7d3ab79e 100644 --- a/src/features/onboarding/steps/step-notifier.tsx +++ b/src/features/onboarding/steps/step-notifier.tsx @@ -4,227 +4,194 @@ import { useState } from "react"; import { useOnboarding } from "@onboardjs/react"; import { ArrowLeft, Check, X } from "lucide-react"; -import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { - useZodForm, - Form, - FormField, - FormItem, - FormLabel, - FormControl, - FormMessage, + useZodForm, + Form, + FormField, + FormItem, + FormLabel, + FormControl, + FormMessage, } from "@/components/ui/form"; import { notificationProviders } from "@/features/channel/channels-notification-helper"; import { renderChannelForm } from "@/features/channel/channels-helpers"; import { NotificationChannelFormSchema } from "@/features/channel/channel-form.schema"; -import { OnboardingChannel } from "@/features/onboarding/types"; -import { addNotificationChannelAction, removeNotificationChannelAction } from "@/features/channel/notifications/channel.action"; +import { useAddNotifier } from "@/features/onboarding/hooks/use-add-notifier"; +import { useRemoveNotifier } from "@/features/onboarding/hooks/use-remove-notifier"; +import type { OnboardingChannel } from "@/features/onboarding/types"; type Phase = { kind: "grid" } | { kind: "configuring"; provider: string }; export const StepNotifier = () => { - const { next, updateContext, state } = useOnboarding(); - const orgId = (state?.context.flowData.org as any)?.id as string | undefined; - const [phase, setPhase] = useState({ kind: "grid" }); - const existingNotifiers = (state?.context.flowData.notifiers ?? []) as OnboardingChannel[]; - const [channels, setChannels] = useState(existingNotifiers); - const [submitting, setSubmitting] = useState(false); + const { next, updateContext, state } = useOnboarding(); + const notifiers = (state?.context.flowData.notifiers ?? []) as OnboardingChannel[]; + const [phase, setPhase] = useState({ kind: "grid" }); + const form = useZodForm({ schema: NotificationChannelFormSchema }); - const form = useZodForm({ schema: NotificationChannelFormSchema }); + const addNotifier = useAddNotifier(); + const removeNotifier = useRemoveNotifier(); - const startConfiguring = (provider: string) => { - if (channels.some((c) => c.provider === provider)) return; - form.reset({ provider, enabled: true, name: "", config: {} } as any); - setPhase({ kind: "configuring", provider }); - }; + const startConfiguring = (provider: string) => { + if (notifiers.some((c) => c.provider === provider)) return; + form.reset({ provider, enabled: true, name: "", config: {} } as any); + setPhase({ kind: "configuring", provider }); + }; - const removeChannel = async (id: string) => { - const updated = channels.filter((c) => c.id !== id); - setChannels(updated); - const result = await removeNotificationChannelAction({ organizationId: orgId, notificationChannelId: id }); - if (result?.data?.success === false) { - toast.error("Failed to remove channel"); - setChannels(channels); - } else { - await updateContext({ flowData: { ...state?.context.flowData, notifiers: updated } }); - } - }; + const onContinue = async () => { + await updateContext({ flowData: { ...state?.context.flowData, notifiers } }); + await next(); + }; - const onContinue = async () => { - await updateContext({ flowData: { ...state?.context.flowData, notifiers: channels } }); - await next(); - }; - - if (phase.kind === "configuring") { - const providerDetails = notificationProviders.find((p) => p.value === phase.provider); - const Icon = providerDetails?.icon; - - return ( -
-
- {Icon && ( -
- -
- )} -

- Configuring {providerDetails?.label} -

- -
-
{ - setSubmitting(true); - try { - const details = notificationProviders.find((p) => p.value === values.provider); - const result = await addNotificationChannelAction({ - organizationId: orgId, - data: { provider: values.provider as any, name: values.name, config: values.config as any, enabled: true }, - }); - const inner = result?.data; - if (!inner?.success || !inner.value) { - toast.error("Failed to save channel"); - return; - } - setChannels((prev) => [ - ...prev, - { - id: inner.value!.id, - provider: values.provider, - label: details?.label ?? values.provider, - name: values.name, - config: values.config as Record, - }, - ]); - form.reset({ enabled: true } as any); - setPhase({ kind: "grid" }); - } finally { - setSubmitting(false); - } - }} - > - ( - - Channel name * - - - - - - )} - /> - ( - - )} - /> - {renderChannelForm(phase.provider, form)} - - -
- ); - } - - // Phase: grid - const configuredProviderIds = channels.map((c) => c.provider); - const availableProviders = notificationProviders.filter((p) => !p.preview); + if (phase.kind === "configuring") { + const providerDetails = notificationProviders.find((p) => p.value === phase.provider); + const Icon = providerDetails?.icon; return ( -
-
-

Connect a notifier

-

- Optional — get notified about backups, restores and health checks. -

+
+
+ {Icon && ( +
+
- - {channels.length > 0 && ( -
- {channels.map((ch) => { - const details = notificationProviders.find((p) => p.value === ch.provider); - const Icon = details?.icon; - return ( -
- {Icon && ( -
- -
- )} - - {ch.name}{" "} - ({ch.label}) - - -
- ); - })} -
- )} - -
- {availableProviders.map((provider) => { - const Icon = provider.icon; - const isConfigured = configuredProviderIds.includes(provider.value); - return ( - - ); - })} -
- - + )} +

Configuring {providerDetails?.label}

+
+
{ + const details = notificationProviders.find((p) => p.value === values.provider); + addNotifier.mutate( + { + provider: values.provider, + name: values.name, + config: values.config, + label: details?.label ?? values.provider, + }, + { + onSuccess: () => { + form.reset({ enabled: true } as any); + setPhase({ kind: "grid" }); + }, + }, + ); + }} + > + ( + + Channel name * + + + + + + )} + /> + ( + + )} + /> + {renderChannelForm(phase.provider, form)} + + +
); + } + + const configuredProviderIds = notifiers.map((c) => c.provider); + const availableProviders = notificationProviders.filter((p) => !p.preview); + + return ( +
+
+

Connect a notifier

+

+ Optional — get notified about backups, restores and health checks. +

+
+ + {notifiers.length > 0 && ( +
+ {notifiers.map((ch) => { + const details = notificationProviders.find((p) => p.value === ch.provider); + const Icon = details?.icon; + return ( +
+ {Icon && ( +
+ +
+ )} + + {ch.name} ({ch.label}) + + +
+ ); + })} +
+ )} + +
+ {availableProviders.map((provider) => { + const Icon = provider.icon; + const isConfigured = configuredProviderIds.includes(provider.value); + return ( + + ); + })} +
+ + +
+ ); };