diff --git a/src/features/onboarding/hooks/use-add-storage.ts b/src/features/onboarding/hooks/use-add-storage.ts new file mode 100644 index 00000000..3f41f995 --- /dev/null +++ b/src/features/onboarding/hooks/use-add-storage.ts @@ -0,0 +1,40 @@ +// src/features/onboarding/hooks/use-add-storage.ts +"use client"; + +import { useMutation } from "@tanstack/react-query"; +import { useOnboarding } from "@onboardjs/react"; +import { toast } from "sonner"; +import { addStorageChannelAction } from "@/features/channel/storages/channel.action"; +import type { OnboardingChannel } from "@/features/onboarding/types"; + +type StorageInput = { + provider: string; + name: string; + config: Record; + label: string; +}; + +export const useAddStorage = () => { + const { state, updateContext } = useOnboarding(); + + return useMutation({ + mutationFn: async ({ provider, name, config, label }: StorageInput) => { + const orgId = (state?.context.flowData.org as any)?.id as string | undefined; + const result = await addStorageChannelAction({ + 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 storage"); + + const channel: OnboardingChannel = { id: inner.value.id, provider, label, name, config }; + const storages = [ + ...((state?.context.flowData.storages ?? []) as OnboardingChannel[]), + channel, + ]; + await updateContext({ flowData: { ...state?.context.flowData, storages } }); + return channel; + }, + onError: (err: Error) => toast.error(err.message), + }); +}; diff --git a/src/features/onboarding/hooks/use-remove-storage.ts b/src/features/onboarding/hooks/use-remove-storage.ts new file mode 100644 index 00000000..b017e631 --- /dev/null +++ b/src/features/onboarding/hooks/use-remove-storage.ts @@ -0,0 +1,25 @@ +// src/features/onboarding/hooks/use-remove-storage.ts +"use client"; + +import { useMutation } from "@tanstack/react-query"; +import { useOnboarding } from "@onboardjs/react"; +import { toast } from "sonner"; +import { removeStorageChannelAction } from "@/features/channel/storages/channel.action"; +import type { OnboardingChannel } from "@/features/onboarding/types"; + +export const useRemoveStorage = () => { + 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 removeStorageChannelAction({ organizationId: orgId, id }); + if (result?.data?.success === false) throw new Error("Failed to remove storage"); + const storages = ( + (state?.context.flowData.storages ?? []) as OnboardingChannel[] + ).filter((c) => c.id !== id); + await updateContext({ flowData: { ...state?.context.flowData, storages } }); + }, + onError: (err: Error) => toast.error(err.message), + }); +}; diff --git a/src/features/onboarding/steps/step-storage.tsx b/src/features/onboarding/steps/step-storage.tsx index b2a45409..1b63e1b4 100644 --- a/src/features/onboarding/steps/step-storage.tsx +++ b/src/features/onboarding/steps/step-storage.tsx @@ -4,228 +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 { storageProviders } from "@/features/channel/channels-storage-helper"; import { renderChannelForm } from "@/features/channel/channels-helpers"; import { StorageChannelFormSchema } from "@/features/channel/channel-form.schema"; -import { OnboardingChannel } from "@/features/onboarding/types"; -import { addStorageChannelAction, removeStorageChannelAction } from "@/features/channel/storages/channel.action"; +import { useAddStorage } from "@/features/onboarding/hooks/use-add-storage"; +import { useRemoveStorage } from "@/features/onboarding/hooks/use-remove-storage"; +import type { OnboardingChannel } from "@/features/onboarding/types"; type Phase = { kind: "grid" } | { kind: "configuring"; provider: string }; export const StepStorage = () => { - const { next, updateContext, state } = useOnboarding(); - const orgId = (state?.context.flowData.org as any)?.id as string | undefined; - const [phase, setPhase] = useState({ kind: "grid" }); - const existingStorages = (state?.context.flowData.storages ?? []) as OnboardingChannel[]; - const [channels, setChannels] = useState(existingStorages); - const [submitting, setSubmitting] = useState(false); + const { next, updateContext, state } = useOnboarding(); + const storages = (state?.context.flowData.storages ?? []) as OnboardingChannel[]; + const [phase, setPhase] = useState({ kind: "grid" }); + const form = useZodForm({ schema: StorageChannelFormSchema }); - const form = useZodForm({ schema: StorageChannelFormSchema }); + const addStorage = useAddStorage(); + const removeStorage = useRemoveStorage(); - 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 (storages.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 removeStorageChannelAction({ organizationId: orgId, id }); - if (result?.data?.success === false) { - toast.error("Failed to remove storage"); - setChannels(channels); - } else { - await updateContext({ flowData: { ...state?.context.flowData, storages: updated } }); - } - }; + const onContinue = async () => { + await updateContext({ flowData: { ...state?.context.flowData, storages } }); + await next(); + }; - const onContinue = async () => { - await updateContext({ flowData: { ...state?.context.flowData, storages: channels } }); - await next(); - }; - - if (phase.kind === "configuring") { - const providerDetails = storageProviders.find((p) => p.value === phase.provider); - const Icon = providerDetails?.icon; - - return ( -
-
- {Icon && ( -
- -
- )} -

- Configuring {providerDetails?.label} -

- -
-
{ - setSubmitting(true); - try { - const result = await addStorageChannelAction({ - 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 storage"); - return; - } - setChannels((prev) => [ - ...prev, - { - id: inner.value!.id, - provider: values.provider, - label: providerDetails?.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 — filter "local" (no useful config) and preview providers - const availableProviders = storageProviders.filter( - (p) => !p.preview && p.value !== "local" - ); - const configuredProviderIds = channels.map((c) => c.provider); + if (phase.kind === "configuring") { + const providerDetails = storageProviders.find((p) => p.value === phase.provider); + const Icon = providerDetails?.icon; return ( -
-
-

Connect a storage

-

- Optional — choose where backups and files are stored. -

+
+
+ {Icon && ( +
+
- - {channels.length > 0 && ( -
- {channels.map((ch) => { - const details = storageProviders.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 = storageProviders.find((p) => p.value === values.provider); + addStorage.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 availableProviders = storageProviders.filter((p) => !p.preview && p.value !== "local"); + const configuredProviderIds = storages.map((c) => c.provider); + + return ( +
+
+

Connect a storage

+

+ Optional — choose where backups and files are stored. +

+
+ + {storages.length > 0 && ( +
+ {storages.map((ch) => { + const details = storageProviders.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 ( + + ); + })} +
+ + +
+ ); };