import { zodResolver } from '@hookform/resolvers/zod' import type { PGTrigger } from '@supabase/pg-meta' import { Terminal } from 'lucide-react' import { useEffect, useState } from 'react' import { SubmitHandler, useForm } from 'react-hook-form' import { toast } from 'sonner' import { Button, Checkbox, cn, Form, FormControl, FormField, Input, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Separator, Sheet, SheetContent, SheetFooter, SheetHeader, SheetTitle, } from 'ui' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import * as z from 'zod' import ChooseFunctionForm from './ChooseFunctionForm' import { TRIGGER_ENABLED_MODES, TRIGGER_EVENTS, TRIGGER_ORIENTATIONS, TRIGGER_TYPES, } from './Triggers.constants' import { DiscardChangesConfirmationDialog } from '@/components/ui-patterns/Dialogs/DiscardChangesConfirmationDialog' import FormBoxEmpty from '@/components/ui/FormBoxEmpty' import { useDatabaseTriggerCreateMutation } from '@/data/database-triggers/database-trigger-create-mutation' import { useDatabaseTriggerUpdateMutation } from '@/data/database-triggers/database-trigger-update-mutation' import { useTablesQuery } from '@/data/tables/tables-query' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' import { useConfirmOnClose } from '@/hooks/ui/useConfirmOnClose' import { useProtectedSchemas } from '@/hooks/useProtectedSchemas' const formId = 'create-trigger' const FormSchema = z.object({ name: z .string() .min(1, 'Please provide a name for your trigger') .regex(/^\S+$/, 'Name should not contain spaces or whitespaces'), schema: z.string(), table: z.string(), activation: z.enum(['BEFORE', 'AFTER', 'INSTEAD OF']), enabled_mode: z.enum(['ORIGIN', 'REPLICA', 'ALWAYS', 'DISABLED']), orientation: z.enum(['ROW', 'STATEMENT']), function_name: z.string().min(1, 'Please select a database function for your trigger to call'), function_schema: z.string(), events: z.array(z.string()).min(1, 'Please select at least one event'), // For UI handling, not to be passed to the final request tableId: z.string().optional(), }) const defaultValues: z.infer = { name: '', schema: '', table: '', activation: 'AFTER', orientation: 'ROW', function_name: '', function_schema: '', enabled_mode: 'ORIGIN', events: [], } interface TriggerSheetProps { selectedTrigger?: PGTrigger isDuplicatingTrigger?: boolean open: boolean onClose: () => void } export const TriggerSheet = ({ selectedTrigger, isDuplicatingTrigger, open, onClose, }: TriggerSheetProps) => { const { data: project } = useSelectedProjectQuery() const [showFunctionSelector, setShowFunctionSelector] = useState(false) const { mutate: createDatabaseTrigger, isPending: isCreating } = useDatabaseTriggerCreateMutation( { onSuccess: () => { toast.success(`Successfully created trigger`) onClose() }, onError: (error) => { toast.error(`Failed to create trigger: ${error.message}`) }, } ) const { mutate: updateDatabaseTrigger, isPending: isUpdating } = useDatabaseTriggerUpdateMutation( { onSuccess: () => { toast.success(`Successfully updated trigger`) onClose() }, onError: (error) => { toast.error(`Failed to update trigger: ${error.message}`) }, } ) const { data = [], isSuccess: isSuccessTables } = useTablesQuery({ projectRef: project?.ref, connectionString: project?.connectionString, }) const { data: protectedSchemas, isSuccess: isSuccessProtectedSchemas } = useProtectedSchemas() const isSuccess = isSuccessTables && isSuccessProtectedSchemas const tables = data .sort((a, b) => a.schema.localeCompare(b.schema)) .filter((a) => !protectedSchemas.find((s) => s.name === a.schema)) const isEditing = !isDuplicatingTrigger && !!selectedTrigger const form = useForm>({ mode: 'onSubmit', reValidateMode: 'onSubmit', resolver: zodResolver(FormSchema as any), defaultValues, }) const { function_name, function_schema } = form.watch() const { confirmOnClose, handleOpenChange, modalProps } = useConfirmOnClose({ checkIsDirty: () => form.formState.isDirty, onClose, }) const onSubmit: SubmitHandler> = async (values) => { if (!project) return console.error('Project is required') const { tableId, ...payload } = values if (isEditing) { updateDatabaseTrigger({ projectRef: project?.ref, connectionString: project?.connectionString, originalTrigger: selectedTrigger, payload: { name: payload.name, enabled_mode: payload.enabled_mode }, }) } else { createDatabaseTrigger({ projectRef: project?.ref, connectionString: project?.connectionString, payload, }) } } useEffect(() => { if (open && isSuccess) { form.clearErrors() if (isDuplicatingTrigger && selectedTrigger) { const initalSelectedTable = tables.find((t) => t.name === selectedTrigger.table) form.reset({ ...selectedTrigger, tableId: initalSelectedTable?.id.toString(), table: initalSelectedTable?.name, schema: initalSelectedTable?.schema, }) } else if (isEditing) { form.reset(selectedTrigger) } else if (tables.length > 0) { form.reset({ ...defaultValues, tableId: tables[0].id.toString(), table: tables[0].name, schema: tables[0].schema, }) } } // eslint-disable-next-line react-hooks/exhaustive-deps }, [open, isSuccess]) return ( <> {isDuplicatingTrigger ? 'Duplicate trigger' : isEditing ? `Edit database trigger: ${selectedTrigger.name}` : 'Create a new database trigger'}
( )} /> {isEditing ? ( ( )} /> ) : ( <> ( )} /> ( {TRIGGER_EVENTS.map((event) => ( ( { return checked ? field.onChange([...field.value, event.value]) : field.onChange( field.value?.filter((value) => value !== event.value) ) }} /> )} /> ))} )} /> ( )} /> ( )} /> (

Function to trigger

{function_name.length === 0 ? ( ) : (

{function_schema} . {function_name}

)}
)} /> )}
{ form.setValue('function_name', fn.name, { shouldDirty: true }) form.setValue('function_schema', fn.schema, { shouldDirty: true }) }} /> ) }