mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
* add: test btn * fix * fix --------- Co-authored-by: Théo LAGACHE <theo.lagache@soluce-technologies.com>
253 lines
10 KiB
TypeScript
253 lines
10 KiB
TypeScript
"use client";
|
|
|
|
import {useRouter} from "next/navigation";
|
|
import {useMutation} from "@tanstack/react-query";
|
|
import {Form, FormControl, FormField, FormItem, FormLabel, FormMessage, useZodForm} from "@/components/ui/form";
|
|
import {ButtonWithLoading} from "@/components/common/button-with-loading";
|
|
import {Input} from "@/components/ui/input";
|
|
|
|
import {
|
|
addStorageChannelAction, updateStorageChannelAction
|
|
} from "@/features/channel/components/storages/channel.action";
|
|
import {toast} from "sonner";
|
|
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
|
import {Button} from "@/components/ui/button";
|
|
import {NotificationChannelWith} from "@/db/schema/09_notification-channel";
|
|
import {useEffect} from "react";
|
|
import {cn} from "@/lib/utils";
|
|
import {Card} from "@/components/ui/card";
|
|
import {ArrowLeft} from "lucide-react";
|
|
import {StorageChannelWith} from "@/db/schema/12_storage-channel";
|
|
import {
|
|
NotificationChannelFormSchema, NotificationChannelFormType, StorageChannelFormSchema, StorageChannelFormType
|
|
} from "@/features/channel/schemas/channel-form.schema";
|
|
import {
|
|
ChannelKind,
|
|
renderChannelForm
|
|
} from "@/features/channel/components/channels-helpers";
|
|
import {storageProviders} from "@/features/channel/components/channels-storage-helper";
|
|
import {notificationProviders} from "@/features/channel/components/channels-notification-helper";
|
|
import {
|
|
ChannelTestButton
|
|
} from "@/features/channel/components/channel-test-button";
|
|
import {
|
|
addNotificationChannelAction, updateNotificationChannelAction
|
|
} from "@/features/channel/components/notifications/channel.action";
|
|
|
|
|
|
type NotifierFormProps = {
|
|
onSuccessAction?: () => void;
|
|
organization?: OrganizationWithMembers;
|
|
defaultValues?: NotificationChannelWith | StorageChannelWith
|
|
adminView?: boolean
|
|
kind: ChannelKind
|
|
};
|
|
|
|
export const ChannelForm = ({onSuccessAction, organization, defaultValues, kind}: NotifierFormProps) => {
|
|
|
|
const router = useRouter();
|
|
const isCreate = !Boolean(defaultValues);
|
|
|
|
const form = useZodForm({
|
|
schema: kind == "notification" ? NotificationChannelFormSchema : StorageChannelFormSchema,
|
|
// @ts-expect-error — defaultValues type is a union of notification/storage channel types
|
|
defaultValues: {...defaultValues},
|
|
});
|
|
|
|
useEffect(() => {
|
|
form.reset(defaultValues ? {...defaultValues} : {enabled: true});
|
|
}, [defaultValues]);
|
|
|
|
const mutationAddNotificationChannel = useMutation({
|
|
mutationFn: async (values: NotificationChannelFormType | StorageChannelFormType) => {
|
|
|
|
const payload = {
|
|
data: values,
|
|
...(organization && {organizationId: organization.id}),
|
|
...((defaultValues && {id: defaultValues.id}))
|
|
};
|
|
|
|
|
|
let result: any;
|
|
|
|
if (kind === "notification") {
|
|
// @ts-expect-error — payload type varies between notification and storage
|
|
result = isCreate ? await addNotificationChannelAction(payload) : await updateNotificationChannelAction(payload);
|
|
} else if (kind === "storage") {
|
|
// @ts-expect-error — payload type varies between notification and storage
|
|
result = isCreate ? await addStorageChannelAction(payload) : await updateStorageChannelAction(payload);
|
|
} else {
|
|
toast.error("An error occurred");
|
|
return;
|
|
}
|
|
|
|
const inner = result?.data;
|
|
|
|
if (inner?.success) {
|
|
toast.success(inner.actionSuccess?.message);
|
|
isCreate && onSuccessAction?.();
|
|
router.refresh();
|
|
} else {
|
|
toast.error(inner?.actionError?.message);
|
|
isCreate && onSuccessAction?.();
|
|
}
|
|
}
|
|
});
|
|
|
|
const provider = form.watch("provider");
|
|
const channelTypes = kind == "notification" ? notificationProviders : storageProviders
|
|
const selectedProviderDetails = channelTypes.find(t => t.value === provider);
|
|
|
|
if (isCreate && !provider) {
|
|
return (
|
|
<div className="grid grid-cols-2 sm:grid-cols-3 gap-4 py-4">
|
|
{channelTypes.filter(p => p.value != "local").map((type) => {
|
|
const Icon = type.icon;
|
|
return (
|
|
<Card
|
|
key={type.value}
|
|
className={cn(
|
|
"relative flex flex-col items-center justify-center gap-3 p-4 transition-all border",
|
|
type.preview
|
|
? "cursor-not-allowed bg-muted/60 border-border text-muted-foreground"
|
|
: "cursor-pointer bg-background hover:bg-accent/40 hover:border-primary/50"
|
|
)}
|
|
onClick={() => {
|
|
if (type.preview) return;
|
|
form.setValue("provider", type.value as any);
|
|
form.setValue("config", {});
|
|
}}
|
|
>
|
|
|
|
{type.preview && (
|
|
<div
|
|
className="absolute bottom-0 right-0 overflow-hidden w-20 h-20 pointer-events-none">
|
|
<div
|
|
className="absolute bottom-0 right-0 w-38 h-5 flex items-center justify-center
|
|
transform -rotate-45 translate-x-14 -translate-y-4"
|
|
style={{backgroundColor: "#FE6702"}}
|
|
>
|
|
<span className="text-white text-[7px] pr-3 font-medium text-center w-full">COMING SOON</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
<div
|
|
className={cn(
|
|
"h-10 w-10 rounded-full flex items-center justify-center transition-colors",
|
|
type.preview
|
|
? "bg-muted"
|
|
: "bg-accent text-accent-foreground"
|
|
)}
|
|
>
|
|
<Icon
|
|
className={cn(
|
|
"h-6 w-6",
|
|
type.preview ? "text-muted-foreground" : "text-foreground"
|
|
)}
|
|
/>
|
|
</div>
|
|
<span
|
|
className={cn(
|
|
"font-medium text-sm text-center leading-tight",
|
|
type.preview ? "text-muted-foreground" : "text-foreground"
|
|
)}
|
|
>
|
|
{type.label}
|
|
</span>
|
|
</Card>
|
|
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Form
|
|
form={form}
|
|
className="flex flex-col gap-4"
|
|
onSubmit={async (values) => {
|
|
await mutationAddNotificationChannel.mutateAsync(values);
|
|
}}
|
|
>
|
|
<div className="flex items-center gap-3 mb-2 p-3 bg-secondary/30 rounded-lg border border-border">
|
|
{selectedProviderDetails && (
|
|
<div
|
|
className="h-10 w-10 bg-background rounded-full flex items-center justify-center border border-border shadow-sm">
|
|
<selectedProviderDetails.icon className="h-5 w-5"/>
|
|
</div>
|
|
)}
|
|
<div className="flex-1">
|
|
<p className="text-sm font-medium">Configuring {selectedProviderDetails?.label}</p>
|
|
<p className="text-xs text-muted-foreground">{isCreate ? "New Channel" : "Edit Channel"}</p>
|
|
</div>
|
|
{isCreate && (
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => {
|
|
form.setValue("provider", undefined as any);
|
|
form.setValue("config", undefined);
|
|
}}
|
|
>
|
|
<ArrowLeft className="h-4 w-4 mr-2"/>
|
|
Change
|
|
</Button>
|
|
)}
|
|
</div>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name="name"
|
|
render={({field}) => (
|
|
<FormItem>
|
|
<FormLabel>Channel Name *</FormLabel>
|
|
<FormControl>
|
|
<Input {...field} placeholder={`e.g. ${selectedProviderDetails?.label} Channel`}
|
|
value={field.value ?? ""}/>
|
|
</FormControl>
|
|
<FormMessage/>
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name="provider"
|
|
render={({field}) => (
|
|
<input type="hidden" {...field} value={field.value || ""}/>
|
|
)}
|
|
/>
|
|
|
|
{renderChannelForm(provider, form)}
|
|
|
|
<div className="flex justify-between mt-4">
|
|
<div>
|
|
<ChannelTestButton
|
|
kind={kind}
|
|
organizationId={organization?.id}
|
|
channel={form.getValues()}
|
|
/>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
onClick={() => {
|
|
onSuccessAction?.();
|
|
form.reset();
|
|
}}
|
|
>
|
|
Cancel
|
|
</Button>
|
|
<ButtonWithLoading isPending={mutationAddNotificationChannel.isPending}>
|
|
{isCreate ? "Add" : "Save"} Channel
|
|
</ButtonWithLoading>
|
|
</div>
|
|
|
|
</div>
|
|
</Form>
|
|
);
|
|
};
|