2026-01-07 13:33:23 +01:00
|
|
|
import {Form, FormControl, FormField, FormItem, FormLabel, FormMessage, useZodForm} from "@/components/ui/form";
|
2025-11-29 22:31:32 +01:00
|
|
|
import {InfoIcon, Plus, Trash2} from "lucide-react";
|
2025-11-23 17:13:07 +01:00
|
|
|
import {useFieldArray} from "react-hook-form";
|
|
|
|
|
import {DatabaseWith} from "@/db/schema/07_database";
|
|
|
|
|
import {NotificationChannel} from "@/db/schema/09_notification-channel";
|
|
|
|
|
import {Label} from "@/components/ui/label";
|
|
|
|
|
import {Button} from "@/components/ui/button";
|
2026-05-24 17:09:12 +02:00
|
|
|
import {ButtonWithLoading} from "@/components/common/button-with-loading";
|
2025-11-23 17:13:07 +01:00
|
|
|
import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from "@/components/ui/select";
|
2026-05-24 17:09:12 +02:00
|
|
|
import {MultiSelect} from "@/components/common/multi-select";
|
2026-02-10 11:44:45 +01:00
|
|
|
import {useMutation, useQueryClient} from "@tanstack/react-query";
|
2025-11-23 17:13:07 +01:00
|
|
|
import {toast} from "sonner";
|
2025-11-29 22:31:32 +01:00
|
|
|
import {Switch} from "@/components/ui/switch";
|
2026-01-07 13:33:23 +01:00
|
|
|
import {Card} from "@/components/ui/card";
|
|
|
|
|
import Link from "next/link";
|
2026-01-11 11:57:18 +01:00
|
|
|
import {useIsMobile} from "@/hooks/use-mobile";
|
2026-02-13 11:24:53 +01:00
|
|
|
import {useRouter} from "next/navigation";
|
2026-01-14 21:54:13 +01:00
|
|
|
import {
|
|
|
|
|
ChannelKind,
|
|
|
|
|
getChannelIcon,
|
|
|
|
|
getChannelTextBasedOnKind
|
2026-05-24 17:09:12 +02:00
|
|
|
} from "@/features/channel/channels-helpers";
|
2026-01-14 21:54:13 +01:00
|
|
|
import {StorageChannel} from "@/db/schema/12_storage-channel";
|
|
|
|
|
import {
|
2026-03-15 12:02:00 +01:00
|
|
|
EVENT_KIND_BACKUP_ONLY_OPTIONS,
|
2026-01-14 21:54:13 +01:00
|
|
|
EVENT_KIND_OPTIONS,
|
|
|
|
|
PoliciesSchema,
|
|
|
|
|
PoliciesType,
|
|
|
|
|
PolicyType
|
2026-05-24 17:09:12 +02:00
|
|
|
} from "@/features/database/channels-policy.schema";
|
2026-01-14 21:54:13 +01:00
|
|
|
import {
|
|
|
|
|
createAlertPoliciesAction, createStoragePoliciesAction, deleteAlertPoliciesAction, deleteStoragePoliciesAction,
|
|
|
|
|
updateAlertPoliciesAction, updateStoragePoliciesAction
|
2026-05-24 17:09:12 +02:00
|
|
|
} from "@/features/database/channels-policy.action";
|
|
|
|
|
import {backupOnly} from "@/features/database/database-tabs";
|
2025-11-23 17:13:07 +01:00
|
|
|
|
2026-01-14 21:54:13 +01:00
|
|
|
type ChannelPoliciesFormProps = {
|
2025-11-23 17:13:07 +01:00
|
|
|
onSuccess?: () => void;
|
2026-01-14 21:54:13 +01:00
|
|
|
channels: NotificationChannel[] | StorageChannel[];
|
2025-11-23 17:13:07 +01:00
|
|
|
database: DatabaseWith;
|
2026-01-14 21:54:13 +01:00
|
|
|
kind: ChannelKind
|
2025-11-23 17:13:07 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
2026-01-14 21:54:13 +01:00
|
|
|
export const ChannelPoliciesForm = ({
|
|
|
|
|
database,
|
|
|
|
|
channels,
|
|
|
|
|
onSuccess,
|
|
|
|
|
kind
|
|
|
|
|
}: ChannelPoliciesFormProps) => {
|
2026-02-10 11:44:45 +01:00
|
|
|
const queryClient = useQueryClient();
|
2026-02-13 11:24:53 +01:00
|
|
|
const router = useRouter();
|
2026-01-14 21:54:13 +01:00
|
|
|
const isMobile = useIsMobile();
|
|
|
|
|
const channelText = getChannelTextBasedOnKind(kind);
|
2025-12-07 10:53:08 +01:00
|
|
|
|
2026-03-15 12:05:48 +01:00
|
|
|
const isBackupOnly = backupOnly.some((type) => database.dbms === type);
|
2026-03-15 12:02:00 +01:00
|
|
|
|
|
|
|
|
|
2026-01-14 21:54:13 +01:00
|
|
|
const organizationChannels = channels.map(c => c.id);
|
|
|
|
|
|
|
|
|
|
const filterByChannel = <T, K extends keyof T>(
|
|
|
|
|
items: T[] | undefined | null,
|
|
|
|
|
channelKey: K
|
|
|
|
|
): T[] => items?.filter(item => organizationChannels.includes(item[channelKey] as string)) ?? [];
|
|
|
|
|
|
|
|
|
|
const formattedAlertPolicies = filterByChannel(database.alertPolicies, "notificationChannelId")
|
|
|
|
|
.map(({notificationChannelId, eventKinds, enabled}) => ({
|
|
|
|
|
channelId: notificationChannelId,
|
|
|
|
|
eventKinds,
|
|
|
|
|
enabled
|
2025-11-23 17:13:07 +01:00
|
|
|
}));
|
2026-01-14 21:54:13 +01:00
|
|
|
|
|
|
|
|
const formattedStoragePolicies = filterByChannel(database.storagePolicies, "storageChannelId")
|
|
|
|
|
.map(({storageChannelId, enabled}) => ({
|
|
|
|
|
channelId: storageChannelId,
|
|
|
|
|
enabled
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
const defaultPolicies: PolicyType[] =
|
|
|
|
|
kind === "notification"
|
|
|
|
|
? formattedAlertPolicies
|
|
|
|
|
: formattedStoragePolicies.map(({ channelId, enabled }) => ({ channelId, enabled }));
|
2025-11-23 17:13:07 +01:00
|
|
|
|
|
|
|
|
const form = useZodForm({
|
2026-01-14 21:54:13 +01:00
|
|
|
schema: PoliciesSchema,
|
|
|
|
|
defaultValues: { policies: defaultPolicies },
|
|
|
|
|
context: { kind }
|
2025-11-23 17:13:07 +01:00
|
|
|
});
|
|
|
|
|
|
2026-01-14 21:54:13 +01:00
|
|
|
const {fields, append, remove} = useFieldArray({ control: form.control, name: "policies" });
|
2025-11-23 17:13:07 +01:00
|
|
|
|
2026-01-14 21:54:13 +01:00
|
|
|
const addPolicy = () => append({channelId: "", eventKinds: [], enabled: true});
|
|
|
|
|
const removePolicyHandler = (index: number) => remove(index);
|
|
|
|
|
const onCancel = () => { form.reset(); onSuccess?.(); };
|
2025-11-23 17:13:07 +01:00
|
|
|
|
|
|
|
|
const mutation = useMutation({
|
2026-01-14 21:54:13 +01:00
|
|
|
mutationFn: async ({policies}: PoliciesType) => {
|
|
|
|
|
const payload = policies.map(p => kind === "notification" ? p : { ...p, eventKinds: undefined });
|
2025-11-23 17:13:07 +01:00
|
|
|
|
2026-01-14 21:54:13 +01:00
|
|
|
const policiesToAdd = payload.filter(
|
|
|
|
|
(policy) => !defaultPolicies.some((a) => a.channelId === policy.channelId)
|
|
|
|
|
);
|
|
|
|
|
const policiesToRemove = defaultPolicies.filter(
|
|
|
|
|
(policy) => !payload.some((v) => v.channelId === policy.channelId)
|
|
|
|
|
);
|
|
|
|
|
const policiesToUpdate = payload.filter((policy) => {
|
|
|
|
|
const existing = defaultPolicies.find((a) => a.channelId === policy.channelId);
|
2025-11-23 17:13:07 +01:00
|
|
|
return existing &&
|
2026-01-14 21:54:13 +01:00
|
|
|
(existing.eventKinds !== policy.eventKinds || existing.enabled !== policy.enabled);
|
|
|
|
|
});
|
|
|
|
|
const promises = kind === "notification"
|
|
|
|
|
? [
|
|
|
|
|
policiesToAdd.length > 0 ? await createAlertPoliciesAction({databaseId: database.id, alertPolicies: policiesToAdd}) : null,
|
|
|
|
|
policiesToUpdate.length > 0 ? await updateAlertPoliciesAction({databaseId: database.id, alertPolicies: policiesToUpdate}) : null,
|
|
|
|
|
policiesToRemove.length > 0 ? await deleteAlertPoliciesAction({databaseId: database.id, alertPolicies: policiesToRemove}) : null,
|
|
|
|
|
]
|
|
|
|
|
: [
|
|
|
|
|
policiesToAdd.length > 0 ? await createStoragePoliciesAction({databaseId: database.id, storagePolicies: policiesToAdd}) : null,
|
|
|
|
|
policiesToUpdate.length > 0 ? await updateStoragePoliciesAction({databaseId: database.id, storagePolicies: policiesToUpdate}) : null,
|
|
|
|
|
policiesToRemove.length > 0 ? await deleteStoragePoliciesAction({databaseId: database.id, storagePolicies: policiesToRemove}) : null,
|
|
|
|
|
];
|
2025-11-23 17:13:07 +01:00
|
|
|
|
2026-01-14 21:54:13 +01:00
|
|
|
const results = await Promise.allSettled(promises);
|
2025-11-23 17:13:07 +01:00
|
|
|
const rejected = results.find((r): r is PromiseRejectedResult => r.status === "rejected");
|
2026-01-14 21:54:13 +01:00
|
|
|
if (rejected) throw new Error(rejected.reason?.message || "Network or server error");
|
2025-11-23 17:13:07 +01:00
|
|
|
|
|
|
|
|
const failedActions = results
|
|
|
|
|
.filter((r): r is PromiseFulfilledResult<any> => r.status === "fulfilled")
|
|
|
|
|
.map(r => r.value)
|
2026-01-14 21:54:13 +01:00
|
|
|
.filter((v): v is { data: { success: false; actionError: any } } => v !== null && v.data?.success === false);
|
2025-11-23 17:13:07 +01:00
|
|
|
|
2026-01-14 21:54:13 +01:00
|
|
|
if (failedActions.length > 0) throw new Error(failedActions[0].data.actionError?.message || "One or more operations failed");
|
2025-11-23 17:13:07 +01:00
|
|
|
return {success: true};
|
|
|
|
|
},
|
2026-02-11 12:10:33 +01:00
|
|
|
onSuccess: () => {
|
|
|
|
|
toast.success("Policies saved successfully");
|
|
|
|
|
queryClient.invalidateQueries({queryKey: ["database-data", database.id]});
|
2026-02-13 11:24:53 +01:00
|
|
|
router.refresh();
|
2026-02-10 11:44:45 +01:00
|
|
|
},
|
2026-01-14 21:54:13 +01:00
|
|
|
onError: (error: any) => { toast.error(error.message || "Failed to save policies"); },
|
2025-11-23 17:13:07 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return (
|
2026-01-14 21:54:13 +01:00
|
|
|
<Form form={form} className="flex flex-col gap-6" onSubmit={
|
|
|
|
|
async (values) => {
|
|
|
|
|
|
|
|
|
|
if (kind === "notification") {
|
|
|
|
|
for (const policy of values.policies) {
|
|
|
|
|
if (!policy.eventKinds || policy.eventKinds.length === 0) {
|
|
|
|
|
toast.error("Please select at least one event for all notification policies");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await mutation.mutateAsync(values)
|
|
|
|
|
}
|
|
|
|
|
}>
|
2025-11-23 17:13:07 +01:00
|
|
|
<div className="space-y-4">
|
|
|
|
|
<div className="flex items-center justify-between">
|
2026-01-07 13:33:23 +01:00
|
|
|
<div>
|
2026-01-14 21:54:13 +01:00
|
|
|
<Label className="text-base font-medium">
|
|
|
|
|
Configure {kind === "notification" ? "Alerts" : "Storages"}
|
|
|
|
|
</Label>
|
2026-01-11 11:57:18 +01:00
|
|
|
{!isMobile && (
|
|
|
|
|
<p className="text-xs text-muted-foreground mt-1">
|
2026-01-14 21:54:13 +01:00
|
|
|
{kind === "notification"
|
|
|
|
|
? "Choose which channels receive notifications for specific events."
|
|
|
|
|
: "Choose which storage to use with your database"}
|
2026-01-11 11:57:18 +01:00
|
|
|
</p>
|
|
|
|
|
)}
|
2026-01-07 13:33:23 +01:00
|
|
|
</div>
|
2025-11-23 17:13:07 +01:00
|
|
|
<Button
|
2026-01-14 21:54:13 +01:00
|
|
|
disabled={fields.length >= channels.length || channels.length === 0}
|
2025-11-23 17:13:07 +01:00
|
|
|
type="button"
|
2026-01-07 13:33:23 +01:00
|
|
|
size="sm"
|
|
|
|
|
className="h-8"
|
2026-01-14 21:54:13 +01:00
|
|
|
onClick={addPolicy}>
|
|
|
|
|
<Plus className="w-4 h-4 mr-1.5"/> Add Policy
|
2025-11-23 17:13:07 +01:00
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="space-y-3 w-full">
|
2026-01-14 21:54:13 +01:00
|
|
|
{channels.length === 0 ? (
|
|
|
|
|
<div className="flex flex-col items-center justify-center p-8 border border-dashed rounded-xl bg-muted/20 text-center gap-2">
|
2026-01-07 13:33:23 +01:00
|
|
|
<InfoIcon className="h-8 w-8 text-muted-foreground/50"/>
|
2026-01-14 21:54:13 +01:00
|
|
|
<p className="font-medium text-sm text-foreground">No channels</p>
|
2026-01-07 13:33:23 +01:00
|
|
|
<p className="text-xs text-muted-foreground max-w-xs">
|
2026-01-14 21:54:13 +01:00
|
|
|
Please <Link href={`/dashboard/settings`} className="underline underline-offset-4 hover:text-primary transition-colors">
|
|
|
|
|
configure {channelText.toLowerCase()} channels
|
|
|
|
|
</Link> in your organization settings first.
|
2026-01-07 13:33:23 +01:00
|
|
|
</p>
|
2025-11-23 17:13:07 +01:00
|
|
|
</div>
|
|
|
|
|
) : fields.length === 0 ? (
|
2026-01-14 21:54:13 +01:00
|
|
|
<div className="flex flex-col items-center justify-center p-8 border border-dashed rounded-xl bg-muted/20 text-center gap-2">
|
2026-01-07 13:33:23 +01:00
|
|
|
<div className="h-8 w-8 rounded-full bg-primary/10 flex items-center justify-center">
|
|
|
|
|
<Plus className="h-4 w-4 text-primary"/>
|
|
|
|
|
</div>
|
2026-01-14 21:54:13 +01:00
|
|
|
<p className="font-medium text-sm text-foreground">No policies</p>
|
2026-01-07 13:33:23 +01:00
|
|
|
<p className="text-xs text-muted-foreground">
|
2026-01-14 21:54:13 +01:00
|
|
|
{kind === "notification" ? `Click "Add Policy" to start receiving notifications.` : `Click "Add Policy" to use this storage.`}
|
2026-01-07 13:33:23 +01:00
|
|
|
</p>
|
2025-11-23 17:13:07 +01:00
|
|
|
</div>
|
|
|
|
|
) : (
|
2026-01-07 13:33:23 +01:00
|
|
|
<div className="grid gap-4">
|
|
|
|
|
{fields.map((field, index) => (
|
2026-01-11 11:57:18 +01:00
|
|
|
<Card key={field.id} className="p-4 transition-all hover:border-primary/50 relative group min-w-0 overflow-hidden">
|
2026-01-07 13:33:23 +01:00
|
|
|
<div className="flex flex-col gap-4">
|
2026-01-11 11:57:18 +01:00
|
|
|
<div className="flex flex-row gap-2 items-start md:items-end flex-nowrap min-w-0 ">
|
|
|
|
|
<div className="flex-1 min-w-0 flex flex-col gap-1.5">
|
2026-01-07 13:33:23 +01:00
|
|
|
<Label className="text-[10px] font-bold text-muted-foreground uppercase tracking-widest pl-0.5">
|
2026-01-14 21:54:13 +01:00
|
|
|
{channelText} Channel
|
2026-01-07 13:33:23 +01:00
|
|
|
</Label>
|
|
|
|
|
<FormField
|
|
|
|
|
control={form.control}
|
2026-01-14 21:54:13 +01:00
|
|
|
name={`policies.${index}.channelId`}
|
|
|
|
|
render={({field}) => {
|
|
|
|
|
const selectedIds = form.watch("policies").map((a: PolicyType) => a.channelId).filter(Boolean);
|
|
|
|
|
const availableChannels = channels.filter(
|
|
|
|
|
(channel) => channel.id.toString() === field.value?.toString() || !selectedIds.includes(channel.id.toString())
|
2026-01-07 13:33:23 +01:00
|
|
|
);
|
2026-01-14 21:54:13 +01:00
|
|
|
const selectedChannel = channels.find(c => c.id === field.value);
|
2025-11-23 17:13:07 +01:00
|
|
|
|
2026-01-07 13:33:23 +01:00
|
|
|
return (
|
2026-01-11 11:57:18 +01:00
|
|
|
<FormItem className="space-y-0 min-w-0">
|
|
|
|
|
<Select onValueChange={field.onChange} value={field.value?.toString() || ""}>
|
2026-01-07 13:33:23 +01:00
|
|
|
<FormControl>
|
2026-01-11 11:57:18 +01:00
|
|
|
<SelectTrigger className="h-9 w-full bg-background border-input min-w-0">
|
2026-01-07 13:33:23 +01:00
|
|
|
<SelectValue placeholder="Select channel">
|
2026-01-11 11:57:18 +01:00
|
|
|
{selectedChannel && (
|
|
|
|
|
<div className="flex items-center gap-2 min-w-0 w-full">
|
|
|
|
|
<div className="flex items-center justify-center h-4 w-4 shrink-0">
|
2026-01-13 22:22:01 +01:00
|
|
|
{getChannelIcon(selectedChannel.provider)}
|
2026-01-07 13:33:23 +01:00
|
|
|
</div>
|
2026-01-11 11:57:18 +01:00
|
|
|
<span className="truncate font-medium text-sm min-w-0">
|
2026-01-14 21:54:13 +01:00
|
|
|
{selectedChannel.name}
|
2026-01-11 11:57:18 +01:00
|
|
|
</span>
|
|
|
|
|
<span className="shrink-0 text-[9px] bg-secondary px-1.5 py-0.5 rounded text-muted-foreground font-mono uppercase">
|
2026-01-14 21:54:13 +01:00
|
|
|
{selectedChannel.provider}
|
2026-01-11 11:57:18 +01:00
|
|
|
</span>
|
2026-01-07 13:33:23 +01:00
|
|
|
</div>
|
2026-01-11 11:57:18 +01:00
|
|
|
)}
|
2026-01-07 13:33:23 +01:00
|
|
|
</SelectValue>
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
</FormControl>
|
|
|
|
|
<SelectContent>
|
2026-01-14 21:54:13 +01:00
|
|
|
{availableChannels.map(channel => (
|
2026-01-11 11:57:18 +01:00
|
|
|
<SelectItem key={channel.id.toString()} value={channel.id.toString()}>
|
|
|
|
|
<div className="flex items-center gap-2 w-full min-w-0">
|
2026-01-14 21:54:13 +01:00
|
|
|
<div className="text-muted-foreground scale-90 shrink-0">{getChannelIcon(channel.provider)}</div>
|
2026-01-11 11:57:18 +01:00
|
|
|
<span className="font-medium truncate min-w-0">{channel.name}</span>
|
2026-01-14 21:54:13 +01:00
|
|
|
<span className="text-xs text-muted-foreground ml-2 capitalize shrink-0">({channel.provider})</span>
|
2026-01-07 13:33:23 +01:00
|
|
|
</div>
|
|
|
|
|
</SelectItem>
|
|
|
|
|
))}
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
<FormMessage className="mt-1"/>
|
|
|
|
|
</FormItem>
|
|
|
|
|
);
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
2026-01-14 21:54:13 +01:00
|
|
|
|
2026-01-11 11:57:18 +01:00
|
|
|
<div className="flex flex-col gap-1.5 shrink-0">
|
2026-01-14 21:54:13 +01:00
|
|
|
<Label className="text-[10px] font-bold text-muted-foreground uppercase tracking-widest pl-0.5">Status</Label>
|
2026-01-11 11:57:18 +01:00
|
|
|
<FormField
|
2026-01-07 13:33:23 +01:00
|
|
|
control={form.control}
|
2026-01-14 21:54:13 +01:00
|
|
|
name={`policies.${index}.enabled`}
|
|
|
|
|
render={({field}) => (
|
2026-01-07 13:33:23 +01:00
|
|
|
<FormItem className="space-y-0">
|
2025-11-23 17:13:07 +01:00
|
|
|
<FormControl>
|
2026-01-11 11:57:18 +01:00
|
|
|
<div className="flex items-center h-9 px-1 md:px-3 rounded-md border border-input bg-background justify-between min-w-0">
|
|
|
|
|
{!isMobile && (
|
|
|
|
|
<Label htmlFor={`switch-${index}`} className="text-xs cursor-pointer font-medium text-foreground mr-2">
|
|
|
|
|
{field.value ? "Active" : "Off"}
|
|
|
|
|
</Label>
|
|
|
|
|
)}
|
|
|
|
|
<Switch checked={field.value} onCheckedChange={field.onChange} id={`switch-${index}`} className="scale-75 origin-right"/>
|
2026-01-07 13:33:23 +01:00
|
|
|
</div>
|
2025-11-23 17:13:07 +01:00
|
|
|
</FormControl>
|
2026-01-07 13:33:23 +01:00
|
|
|
</FormItem>
|
|
|
|
|
)}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
2026-01-14 21:54:13 +01:00
|
|
|
|
2026-01-11 11:57:18 +01:00
|
|
|
<div className="flex flex-col gap-1.5 shrink-0 mt-auto">
|
2026-01-14 21:54:13 +01:00
|
|
|
<Button type="button" variant="outline" size="icon"
|
|
|
|
|
className="h-9 w-9 text-muted-foreground hover:text-destructive hover:border-destructive/50 hover:bg-destructive/10 transition-colors border-input bg-background"
|
|
|
|
|
onClick={() => removePolicyHandler(index)}>
|
2026-01-07 13:33:23 +01:00
|
|
|
<Trash2 className="w-4 h-4"/>
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-01-14 21:54:13 +01:00
|
|
|
|
|
|
|
|
{kind === "notification" && (
|
|
|
|
|
<FormField
|
|
|
|
|
control={form.control}
|
|
|
|
|
name={`policies.${index}.eventKinds`}
|
|
|
|
|
render={({field}) => (
|
|
|
|
|
<FormItem className="space-y-1.5 min-w-0">
|
|
|
|
|
<FormLabel className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">Trigger Events</FormLabel>
|
|
|
|
|
<FormControl>
|
|
|
|
|
<div className="max-w-full overflow-hidden">
|
|
|
|
|
<MultiSelect
|
2026-03-15 12:02:00 +01:00
|
|
|
options={isBackupOnly ? EVENT_KIND_BACKUP_ONLY_OPTIONS : EVENT_KIND_OPTIONS}
|
2026-01-14 21:54:13 +01:00
|
|
|
onValueChange={field.onChange}
|
|
|
|
|
defaultValue={field.value ?? []}
|
|
|
|
|
placeholder={isMobile ? "Select events..." : "Select events to trigger notifications..."}
|
|
|
|
|
variant="inverted"
|
|
|
|
|
animation={0}
|
|
|
|
|
className="bg-background/50 w-full min-w-0 flex-wrap"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</FormControl>
|
|
|
|
|
<FormMessage/>
|
|
|
|
|
</FormItem>
|
|
|
|
|
)}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
2025-11-23 17:13:07 +01:00
|
|
|
</div>
|
2026-01-07 13:33:23 +01:00
|
|
|
</Card>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
2025-11-23 17:13:07 +01:00
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-01-07 13:33:23 +01:00
|
|
|
<div className="flex gap-3 justify-end pt-2 border-t mt-2">
|
2026-01-14 21:54:13 +01:00
|
|
|
<ButtonWithLoading variant="outline" type="button" onClick={onCancel}>Cancel</ButtonWithLoading>
|
|
|
|
|
<ButtonWithLoading isPending={mutation.isPending}>Save Changes</ButtonWithLoading>
|
2025-11-23 17:13:07 +01:00
|
|
|
</div>
|
|
|
|
|
</Form>
|
|
|
|
|
);
|
2026-01-07 13:33:23 +01:00
|
|
|
};
|