Skip to content

Commit 3eb8507

Browse files
awaseemcharislam
andauthored
Feature: Abililty to duplicate triggers from the dashboard (supabase#39828)
* updated to support on close and duplicate * updated formatting * added confirmation panel for triggers * updated form to select the correct table for defaults * updated to support trigger intial tables selected * updated to mark select field as dirty * Update apps/studio/components/interfaces/Database/Triggers/TriggerSheet.tsx Co-authored-by: Charis <[email protected]> * Update apps/studio/components/interfaces/Database/Triggers/TriggerSheet.tsx Co-authored-by: Charis <[email protected]> * updated to remove undefined error --------- Co-authored-by: Charis <[email protected]>
1 parent 0a99c0e commit 3eb8507

File tree

4 files changed

+112
-31
lines changed

4 files changed

+112
-31
lines changed

apps/studio/components/interfaces/Database/Triggers/TriggerSheet.tsx

Lines changed: 58 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import {
4040
TRIGGER_ORIENTATIONS,
4141
TRIGGER_TYPES,
4242
} from './Triggers.constants'
43+
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
4344

4445
const formId = 'create-trigger'
4546

@@ -75,20 +76,27 @@ const defaultValues: z.infer<typeof FormSchema> = {
7576

7677
interface TriggerSheetProps {
7778
selectedTrigger?: PostgresTrigger
79+
isDuplicatingTrigger?: boolean
7880
open: boolean
79-
setOpen: (val: boolean) => void
81+
onClose: () => void
8082
}
8183

82-
export const TriggerSheet = ({ selectedTrigger, open, setOpen }: TriggerSheetProps) => {
84+
export const TriggerSheet = ({
85+
selectedTrigger,
86+
isDuplicatingTrigger,
87+
open,
88+
onClose,
89+
}: TriggerSheetProps) => {
8390
const { data: project } = useSelectedProjectQuery()
8491

92+
const [isClosingPanel, setIsClosingPanel] = useState(false)
8593
const [showFunctionSelector, setShowFunctionSelector] = useState(false)
8694

8795
const { mutate: createDatabaseTrigger, isLoading: isCreating } = useDatabaseTriggerCreateMutation(
8896
{
89-
onSuccess: (res) => {
90-
toast.success(`Successfully created trigger ${res.name}`)
91-
setOpen(false)
97+
onSuccess: () => {
98+
toast.success(`Successfully created trigger`)
99+
onClose()
92100
},
93101
onError: (error) => {
94102
toast.error(`Failed to create trigger: ${error.message}`)
@@ -97,9 +105,9 @@ export const TriggerSheet = ({ selectedTrigger, open, setOpen }: TriggerSheetPro
97105
)
98106
const { mutate: updateDatabaseTrigger, isLoading: isUpdating } = useDatabaseTriggerUpdateMutation(
99107
{
100-
onSuccess: (res) => {
101-
toast.success(`Successfully updated trigger ${res.name}`)
102-
setOpen(false)
108+
onSuccess: () => {
109+
toast.success(`Successfully updated trigger`)
110+
onClose()
103111
},
104112
onError: (error) => {
105113
toast.error(`Failed to update trigger: ${error.message}`)
@@ -117,7 +125,7 @@ export const TriggerSheet = ({ selectedTrigger, open, setOpen }: TriggerSheetPro
117125
const tables = data
118126
.sort((a, b) => a.schema.localeCompare(b.schema))
119127
.filter((a) => !protectedSchemas.find((s) => s.name === a.schema))
120-
const isEditing = !!selectedTrigger
128+
const isEditing = !isDuplicatingTrigger && !!selectedTrigger
121129

122130
const form = useForm<z.infer<typeof FormSchema>>({
123131
mode: 'onSubmit',
@@ -127,6 +135,10 @@ export const TriggerSheet = ({ selectedTrigger, open, setOpen }: TriggerSheetPro
127135
})
128136
const { function_name, function_schema } = form.watch()
129137

138+
function isClosingSidePanel() {
139+
form.formState.isDirty ? setIsClosingPanel(true) : onClose()
140+
}
141+
130142
const onSubmit: SubmitHandler<z.infer<typeof FormSchema>> = async (values) => {
131143
if (!project) return console.error('Project is required')
132144
const { tableId, ...payload } = values
@@ -151,7 +163,16 @@ export const TriggerSheet = ({ selectedTrigger, open, setOpen }: TriggerSheetPro
151163
if (open && isSuccess) {
152164
form.clearErrors()
153165

154-
if (isEditing) {
166+
if (isDuplicatingTrigger && selectedTrigger) {
167+
const initalSelectedTable = tables.find((t) => t.name === selectedTrigger.table)
168+
169+
form.reset({
170+
...selectedTrigger,
171+
tableId: initalSelectedTable?.id.toString(),
172+
table: initalSelectedTable?.name,
173+
schema: initalSelectedTable?.schema,
174+
})
175+
} else if (isEditing) {
155176
form.reset(selectedTrigger)
156177
} else if (tables.length > 0) {
157178
form.reset({
@@ -167,13 +188,15 @@ export const TriggerSheet = ({ selectedTrigger, open, setOpen }: TriggerSheetPro
167188

168189
return (
169190
<>
170-
<Sheet open={open} onOpenChange={setOpen}>
191+
<Sheet open={open} onOpenChange={isClosingSidePanel}>
171192
<SheetContent size="lg" className="flex flex-col gap-0">
172193
<SheetHeader>
173194
<SheetTitle>
174-
{isEditing
175-
? `Edit database trigger: ${selectedTrigger.name}`
176-
: 'Create a new database trigger'}
195+
{isDuplicatingTrigger
196+
? 'Duplicate trigger'
197+
: isEditing
198+
? `Edit database trigger: ${selectedTrigger.name}`
199+
: 'Create a new database trigger'}
177200
</SheetTitle>
178201
</SheetHeader>
179202

@@ -250,10 +273,12 @@ export const TriggerSheet = ({ selectedTrigger, open, setOpen }: TriggerSheetPro
250273
<Select_Shadcn_
251274
defaultValue={field.value}
252275
onValueChange={(val) => {
276+
// mark table ID as dirty to trigger validation
277+
field.onChange(val)
253278
const table = tables.find((x) => x.id.toString() === val)
254279
if (table) {
255-
form.setValue('table', table.name)
256-
form.setValue('schema', table.schema)
280+
form.setValue('table', table.name, { shouldDirty: true })
281+
form.setValue('schema', table.schema, { shouldDirty: true })
257282
}
258283
}}
259284
>
@@ -446,14 +471,30 @@ export const TriggerSheet = ({ selectedTrigger, open, setOpen }: TriggerSheetPro
446471
type="default"
447472
htmlType="reset"
448473
disabled={isCreating || isUpdating}
449-
onClick={() => setOpen(false)}
474+
onClick={onClose}
450475
>
451476
Cancel
452477
</Button>
453478
<Button form={formId} htmlType="submit" loading={isCreating || isUpdating}>
454479
{isEditing ? 'Save' : 'Create'} trigger
455480
</Button>
456481
</SheetFooter>
482+
483+
<ConfirmationModal
484+
visible={isClosingPanel}
485+
title="Discard changes"
486+
confirmLabel="Discard"
487+
onCancel={() => setIsClosingPanel(false)}
488+
onConfirm={() => {
489+
setIsClosingPanel(false)
490+
onClose()
491+
}}
492+
>
493+
<p className="text-sm text-foreground-light">
494+
There are unsaved changes. Are you sure you want to close the panel? Your changes will
495+
be lost.
496+
</p>
497+
</ConfirmationModal>
457498
</SheetContent>
458499
</Sheet>
459500

apps/studio/components/interfaces/Database/Triggers/TriggersList/TriggerList.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { PermissionAction } from '@supabase/shared-types/out/constants'
22
import { includes, sortBy } from 'lodash'
3-
import { Check, Edit, Edit2, MoreVertical, Trash, X } from 'lucide-react'
3+
import { Check, Copy, Edit, Edit2, MoreVertical, Trash, X } from 'lucide-react'
44

55
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
66
import { useDatabaseTriggersQuery } from 'data/database-triggers/database-triggers-query'
@@ -13,6 +13,7 @@ import {
1313
DropdownMenu,
1414
DropdownMenuContent,
1515
DropdownMenuItem,
16+
DropdownMenuSeparator,
1617
DropdownMenuTrigger,
1718
TableCell,
1819
TableRow,
@@ -21,20 +22,23 @@ import {
2122
TooltipTrigger,
2223
} from 'ui'
2324
import { generateTriggerCreateSQL } from './TriggerList.utils'
25+
import { PostgresTrigger } from '@supabase/postgres-meta'
2426

2527
interface TriggerListProps {
2628
schema: string
2729
filterString: string
2830
isLocked: boolean
29-
editTrigger: (trigger: any) => void
30-
deleteTrigger: (trigger: any) => void
31+
editTrigger: (trigger: PostgresTrigger) => void
32+
duplicateTrigger: (trigger: PostgresTrigger) => void
33+
deleteTrigger: (trigger: PostgresTrigger) => void
3134
}
3235

3336
const TriggerList = ({
3437
schema,
3538
filterString,
3639
isLocked,
3740
editTrigger,
41+
duplicateTrigger,
3842
deleteTrigger,
3943
}: TriggerListProps) => {
4044
const { data: project } = useSelectedProjectQuery()
@@ -197,6 +201,11 @@ const TriggerList = ({
197201
<Edit size={14} />
198202
<p>Edit with Assistant</p>
199203
</DropdownMenuItem>
204+
<DropdownMenuItem className="space-x-2" onClick={() => duplicateTrigger(x)}>
205+
<Copy size={14} />
206+
<p>Duplicate trigger</p>
207+
</DropdownMenuItem>
208+
<DropdownMenuSeparator />
200209
<DropdownMenuItem className="space-x-2" onClick={() => deleteTrigger(x)}>
201210
<Trash stroke="red" size={14} />
202211
<p>Delete trigger</p>

apps/studio/components/interfaces/Database/Triggers/TriggersList/TriggersList.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,14 @@ import Link from 'next/link'
3737
interface TriggersListProps {
3838
createTrigger: () => void
3939
editTrigger: (trigger: PostgresTrigger) => void
40+
duplicateTrigger: (trigger: PostgresTrigger) => void
4041
deleteTrigger: (trigger: PostgresTrigger) => void
4142
}
4243

4344
const TriggersList = ({
4445
createTrigger = noop,
4546
editTrigger = noop,
47+
duplicateTrigger = noop,
4648
deleteTrigger = noop,
4749
}: TriggersListProps) => {
4850
const { data: project } = useSelectedProjectQuery()
@@ -232,6 +234,7 @@ const TriggersList = ({
232234
filterString={filterString}
233235
isLocked={isSchemaLocked}
234236
editTrigger={editTrigger}
237+
duplicateTrigger={duplicateTrigger}
235238
deleteTrigger={deleteTrigger}
236239
/>
237240
</TableBody>

apps/studio/pages/project/[ref]/database/triggers.tsx

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ const TriggersPage: NextPageWithLayout = () => {
2121
const isInlineEditorEnabled = useIsInlineEditorEnabled()
2222

2323
const [selectedTrigger, setSelectedTrigger] = useState<PostgresTrigger>()
24+
const [isDuplicatingTrigger, setIsDuplicatingTrigger] = useState<boolean>(false)
25+
2426
const [showCreateTriggerForm, setShowCreateTriggerForm] = useState<boolean>(false)
2527
const [showDeleteTriggerForm, setShowDeleteTriggerForm] = useState<boolean>(false)
2628

@@ -53,11 +55,34 @@ const TriggersPage: NextPageWithLayout = () => {
5355
}
5456
}
5557

58+
const duplicateTrigger = (trigger: PostgresTrigger) => {
59+
setIsDuplicatingTrigger(true)
60+
61+
const dupTrigger = {
62+
...trigger,
63+
name: `${trigger.name}_duplicate`,
64+
}
65+
66+
if (isInlineEditorEnabled) {
67+
setSelectedTriggerForEditor(dupTrigger)
68+
setEditorPanelOpen(true)
69+
} else {
70+
setSelectedTrigger(dupTrigger)
71+
setShowCreateTriggerForm(true)
72+
}
73+
}
74+
5675
const deleteTrigger = (trigger: PostgresTrigger) => {
5776
setSelectedTrigger(trigger)
5877
setShowDeleteTriggerForm(true)
5978
}
6079

80+
const resetEditorPanel = () => {
81+
setIsDuplicatingTrigger(false)
82+
setEditorPanelOpen(false)
83+
setSelectedTriggerForEditor(undefined)
84+
}
85+
6186
if (isPermissionsLoaded && !canReadTriggers) {
6287
return <NoPermission isFullPage resourceText="view database triggers" />
6388
}
@@ -75,6 +100,7 @@ const TriggersPage: NextPageWithLayout = () => {
75100
<TriggersList
76101
createTrigger={createTrigger}
77102
editTrigger={editTrigger}
103+
duplicateTrigger={duplicateTrigger}
78104
deleteTrigger={deleteTrigger}
79105
/>
80106
</div>
@@ -83,7 +109,11 @@ const TriggersPage: NextPageWithLayout = () => {
83109
<TriggerSheet
84110
selectedTrigger={selectedTrigger}
85111
open={showCreateTriggerForm}
86-
setOpen={setShowCreateTriggerForm}
112+
onClose={() => {
113+
setIsDuplicatingTrigger(false)
114+
setShowCreateTriggerForm(false)
115+
}}
116+
isDuplicatingTrigger={isDuplicatingTrigger}
87117
/>
88118
<DeleteTrigger
89119
trigger={selectedTrigger}
@@ -93,14 +123,8 @@ const TriggersPage: NextPageWithLayout = () => {
93123

94124
<EditorPanel
95125
open={editorPanelOpen}
96-
onRunSuccess={() => {
97-
setEditorPanelOpen(false)
98-
setSelectedTriggerForEditor(undefined)
99-
}}
100-
onClose={() => {
101-
setEditorPanelOpen(false)
102-
setSelectedTriggerForEditor(undefined)
103-
}}
126+
onRunSuccess={resetEditorPanel}
127+
onClose={resetEditorPanel}
104128
initialValue={
105129
selectedTriggerForEditor
106130
? generateTriggerCreateSQL(selectedTriggerForEditor)
@@ -111,12 +135,16 @@ execute function function_name();`
111135
}
112136
label={
113137
selectedTriggerForEditor
114-
? `Edit trigger "${selectedTriggerForEditor.name}"`
138+
? isDuplicatingTrigger
139+
? `Duplicate trigger "${selectedTriggerForEditor.name}"`
140+
: `Edit trigger "${selectedTriggerForEditor.name}"`
115141
: 'Create new database trigger'
116142
}
117143
initialPrompt={
118144
selectedTriggerForEditor
119-
? `Update the database trigger "${selectedTriggerForEditor.name}" to...`
145+
? isDuplicatingTrigger
146+
? `Duplicate the database trigger "${selectedTriggerForEditor.name}" to...`
147+
: `Update the database trigger "${selectedTriggerForEditor.name}" to...`
120148
: 'Create a new database trigger that...'
121149
}
122150
/>

0 commit comments

Comments
 (0)