import { zodResolver } from '@hookform/resolvers/zod' import { Terminal } from 'lucide-react' import { useEffect, useState } from 'react' import { SubmitHandler, useForm } from 'react-hook-form' import { toast } from 'sonner' import * as z from 'zod' import { PostgresTrigger } from '@supabase/postgres-meta' 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, type ConfirmOnCloseModalProps } from 'hooks/ui/useConfirmOnClose' import { useProtectedSchemas } from 'hooks/useProtectedSchemas' import { Button, Checkbox_Shadcn_, cn, Form_Shadcn_, FormControl_Shadcn_, FormField_Shadcn_, Input_Shadcn_, Select_Shadcn_, SelectContent_Shadcn_, SelectItem_Shadcn_, SelectTrigger_Shadcn_, SelectValue_Shadcn_, Separator, Sheet, SheetContent, SheetFooter, SheetHeader, SheetTitle, } from 'ui' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import ChooseFunctionForm from './ChooseFunctionForm' import { TRIGGER_ENABLED_MODES, TRIGGER_EVENTS, TRIGGER_ORIENTATIONS, TRIGGER_TYPES, } from './Triggers.constants' 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?: PostgresTrigger 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), defaultValues, }) const { function_name, function_schema } = form.watch() const { confirmOnClose, modalProps: closeConfirmationModalProps } = 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_ENABLED_MODES.find((option) => option.value === field.value) ?.label } {TRIGGER_ENABLED_MODES.map((option) => (

{option.label}

{option.description}

))}
)} /> ) : ( <> ( { // mark table ID as dirty to trigger validation field.onChange(val) const table = tables.find((x) => x.id.toString() === val) if (table) { form.setValue('table', table.name, { shouldDirty: true }) form.setValue('schema', table.schema, { shouldDirty: true }) } }} > {tables.map((table) => ( {table.schema}. {table.name} ))} )} /> ( {TRIGGER_EVENTS.map((event) => ( ( { return checked ? field.onChange([...field.value, event.value]) : field.onChange( field.value?.filter((value) => value !== event.value) ) }} /> )} /> ))} )} /> ( {TRIGGER_TYPES.find((option) => option.value === field.value)?.label} {TRIGGER_TYPES.map((option) => (

{option.label}

{option.description}

))}
)} /> ( { TRIGGER_ORIENTATIONS.find((option) => option.value === field.value) ?.label } {TRIGGER_ORIENTATIONS.map((option) => (

{option.label}

{option.description}

))}
)} /> (

Function to trigger

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

{function_schema} . {function_name}

)}
)} /> )}
{ form.setValue('function_name', fn.name) form.setValue('function_schema', fn.schema) }} /> ) } const CloseConfirmationModal = ({ visible, onClose, onCancel }: ConfirmOnCloseModalProps) => (

There are unsaved changes. Are you sure you want to close the panel? Your changes will be lost.

)