betterstart-cli 0.0.27 → 0.0.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/dist/assets/adapters/next/integrations/mailchimp/actions/mailchimp.ts +0 -1
  2. package/dist/assets/adapters/next/integrations/vercel-blob/actions/vercel-blob.ts +39 -0
  3. package/dist/assets/adapters/next/integrations/vercel-blob/integration.ts +36 -0
  4. package/dist/assets/adapters/next/plugins/blog/schemas/menus.json +1 -1
  5. package/dist/assets/adapters/next/plugins/blog/schemas/posts.json +1 -1
  6. package/dist/assets/adapters/next/templates/init/admin-globals.css +1 -1
  7. package/dist/assets/adapters/next/templates/init/components/layouts/admin-nav-link.tsx +12 -2
  8. package/dist/assets/adapters/next/templates/init/components/layouts/admin-settings-sidebar.tsx +75 -0
  9. package/dist/assets/adapters/next/templates/init/components/layouts/admin-sidebar-nav-link.tsx +12 -2
  10. package/dist/assets/adapters/next/templates/init/components/layouts/admin-sidebar.tsx +2 -8
  11. package/dist/assets/adapters/next/templates/init/components/shared/data-table/data-grid.tsx +14 -5
  12. package/dist/assets/adapters/next/templates/init/components/shared/data-table/data-table.tsx +111 -112
  13. package/dist/assets/adapters/next/templates/init/components/shared/dev-mode/dev-mode-types.ts +4 -0
  14. package/dist/assets/adapters/next/templates/init/components/shared/dev-mode-integrate.tsx +1 -1
  15. package/dist/assets/adapters/next/templates/init/components/shared/entity-versions/entity-versions-drawer.tsx +1 -1
  16. package/dist/assets/adapters/next/templates/init/components/shared/media/media-url-importer.tsx +45 -47
  17. package/dist/assets/adapters/next/templates/init/components/shared/page-header.tsx +1 -1
  18. package/dist/assets/adapters/next/templates/init/data/webhook-events.ts +5 -0
  19. package/dist/assets/adapters/next/templates/init/hooks/use-table-utils.ts +27 -7
  20. package/dist/assets/adapters/next/templates/init/hooks/use-webhooks.ts +211 -0
  21. package/dist/assets/adapters/next/templates/init/lib/actions/audit/types.ts +2 -0
  22. package/dist/assets/adapters/next/templates/init/lib/actions/forms/index.ts +0 -1
  23. package/dist/assets/adapters/next/templates/init/lib/actions/forms/types.ts +0 -4
  24. package/dist/assets/adapters/next/templates/init/lib/actions/forms/upsert-form-settings.ts +0 -2
  25. package/dist/assets/adapters/next/templates/init/lib/actions/webhooks/create-webhook.ts +55 -0
  26. package/dist/assets/adapters/next/templates/init/lib/actions/webhooks/delete-webhook.ts +26 -0
  27. package/dist/assets/adapters/next/templates/init/lib/actions/webhooks/dispatch.ts +135 -0
  28. package/dist/assets/adapters/next/templates/init/lib/actions/webhooks/get-webhook-deliveries.ts +124 -0
  29. package/dist/assets/adapters/next/templates/init/lib/actions/webhooks/get-webhook-secret.ts +28 -0
  30. package/dist/assets/adapters/next/templates/init/lib/actions/webhooks/get-webhook-subscriptions.ts +32 -0
  31. package/dist/assets/adapters/next/templates/init/lib/actions/webhooks/get-webhooks.ts +154 -0
  32. package/dist/assets/adapters/next/templates/init/lib/actions/webhooks/index.ts +30 -0
  33. package/dist/assets/adapters/next/templates/init/lib/actions/webhooks/internal-get-active-endpoints.ts +20 -0
  34. package/dist/assets/adapters/next/templates/init/lib/actions/webhooks/regenerate-webhook-secret.ts +35 -0
  35. package/dist/assets/adapters/next/templates/init/lib/actions/webhooks/register.ts +70 -0
  36. package/dist/assets/adapters/next/templates/init/lib/actions/webhooks/send-test-webhook.ts +81 -0
  37. package/dist/assets/adapters/next/templates/init/lib/actions/webhooks/types.ts +104 -0
  38. package/dist/assets/adapters/next/templates/init/lib/actions/webhooks/update-webhook.ts +72 -0
  39. package/dist/assets/adapters/next/templates/init/lib/db/core/schema.ts +71 -2
  40. package/dist/assets/adapters/next/templates/init/lib/lifecycle-hooks/register.ts +2 -0
  41. package/dist/assets/adapters/next/templates/init/lib/lifecycle-hooks/runner.ts +3 -1
  42. package/dist/assets/adapters/next/templates/init/lib/lifecycle-hooks/types.ts +6 -1
  43. package/dist/assets/adapters/next/templates/init/pages/auth-gate-rsc.tsx +1 -1
  44. package/dist/assets/adapters/next/templates/init/pages/media/media-page-content.tsx +1 -1
  45. package/dist/assets/adapters/next/templates/init/pages/media/media-page-skeleton.tsx +1 -1
  46. package/dist/assets/adapters/next/templates/init/pages/settings/audit-log/audit-log-columns.tsx +150 -0
  47. package/dist/assets/adapters/next/templates/init/pages/settings/audit-log/audit-log-page-content.tsx +137 -0
  48. package/dist/assets/adapters/next/templates/init/pages/{audit-log → settings/audit-log}/audit-log-page-skeleton.tsx +1 -1
  49. package/dist/assets/adapters/next/templates/init/pages/settings/audit-log/audit-log-table.tsx +295 -0
  50. package/dist/assets/adapters/next/templates/init/pages/settings/forms/edit-form-notifications-dialog.tsx +128 -0
  51. package/dist/assets/adapters/next/templates/init/pages/settings/forms/forms-settings-page-content.tsx +111 -0
  52. package/dist/assets/adapters/next/templates/init/pages/settings/forms/forms-settings-page-skeleton.tsx +7 -0
  53. package/dist/assets/adapters/next/templates/init/pages/settings/forms/forms-settings-page.tsx +14 -0
  54. package/dist/assets/adapters/next/templates/init/pages/settings/settings-layout.tsx +10 -0
  55. package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/delete-webhook-dialog.tsx +82 -0
  56. package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/webhook-actions.tsx +53 -0
  57. package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/webhook-enabled-switch.tsx +18 -0
  58. package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/webhook-endpoint-dialog.tsx +370 -0
  59. package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/webhooks-columns.tsx +258 -0
  60. package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/webhooks-logs-columns.tsx +200 -0
  61. package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/webhooks-logs-page-content.tsx +117 -0
  62. package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/webhooks-logs-page.tsx +14 -0
  63. package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/webhooks-logs-table.tsx +253 -0
  64. package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/webhooks-page-content.tsx +161 -0
  65. package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/webhooks-page-skeleton.tsx +7 -0
  66. package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/webhooks-page.tsx +14 -0
  67. package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/webhooks-table.tsx +264 -0
  68. package/dist/assets/adapters/next/templates/init/pages/users/users-page-skeleton.tsx +1 -1
  69. package/dist/assets/adapters/next/templates/init/pages/users/users-table.tsx +103 -109
  70. package/dist/assets/adapters/next/templates/init/types/webhooks.ts +7 -0
  71. package/dist/assets/adapters/next/templates/init/utils/webhook/signature.ts +13 -0
  72. package/dist/assets/shared-assets/react-admin/ui/card.tsx +1 -1
  73. package/dist/assets/shared-assets/react-admin/ui/drawer.tsx +2 -2
  74. package/dist/assets/shared-assets/react-admin/ui/input-group.tsx +3 -3
  75. package/dist/assets/shared-assets/react-admin/ui/sidebar.tsx +1 -1
  76. package/dist/cli.js +2191 -1304
  77. package/dist/cli.js.map +1 -1
  78. package/package.json +1 -1
  79. package/dist/assets/adapters/next/templates/init/lib/actions/forms/test-form-webhook.ts +0 -40
  80. package/dist/assets/adapters/next/templates/init/pages/audit-log/audit-log-page-content.tsx +0 -550
  81. package/dist/assets/adapters/next/templates/init/utils/webhook/webhook.ts +0 -28
  82. /package/dist/assets/adapters/next/templates/init/pages/{audit-log → settings/audit-log}/audit-log-detail-drawer.tsx +0 -0
  83. /package/dist/assets/adapters/next/templates/init/pages/{audit-log → settings/audit-log}/audit-log-detail-row.tsx +0 -0
  84. /package/dist/assets/adapters/next/templates/init/pages/{audit-log → settings/audit-log}/audit-log-page.tsx +0 -0
@@ -0,0 +1,53 @@
1
+ 'use client'
2
+
3
+ import type { WebhookEndpointListItem } from '@admin/actions/webhooks'
4
+ import { Button } from '@admin/components/ui/button'
5
+ import {
6
+ DropdownMenu,
7
+ DropdownMenuContent,
8
+ DropdownMenuItem,
9
+ DropdownMenuTrigger
10
+ } from '@admin/components/ui/dropdown-menu'
11
+ import { useSendTestWebhook } from '@admin/hooks/use-webhooks'
12
+ import { LoaderCircle, MoreHorizontal } from 'lucide-react'
13
+
14
+ interface WebhookActionsProps {
15
+ webhook: WebhookEndpointListItem
16
+ onEdit: (webhook: WebhookEndpointListItem) => void
17
+ onDelete: (webhook: WebhookEndpointListItem) => void
18
+ }
19
+
20
+ export function WebhookActions({ webhook, onEdit, onDelete }: WebhookActionsProps) {
21
+ const sendTest = useSendTestWebhook()
22
+
23
+ return (
24
+ <DropdownMenu>
25
+ <DropdownMenuTrigger asChild>
26
+ <Button
27
+ variant="ghost"
28
+ className="size-8 p-0"
29
+ aria-label={`Open webhook actions for ${webhook.name}`}
30
+ >
31
+ <span className="sr-only">Open webhook actions</span>
32
+ {sendTest.isPending ? (
33
+ <LoaderCircle className="size-4 animate-spin" />
34
+ ) : (
35
+ <MoreHorizontal className="size-4" />
36
+ )}
37
+ </Button>
38
+ </DropdownMenuTrigger>
39
+ <DropdownMenuContent align="end">
40
+ <DropdownMenuItem
41
+ disabled={sendTest.isPending}
42
+ onSelect={() => sendTest.mutate(webhook.id)}
43
+ >
44
+ Test
45
+ </DropdownMenuItem>
46
+ <DropdownMenuItem onSelect={() => onEdit(webhook)}>Edit</DropdownMenuItem>
47
+ <DropdownMenuItem variant="destructive" onSelect={() => onDelete(webhook)}>
48
+ Delete
49
+ </DropdownMenuItem>
50
+ </DropdownMenuContent>
51
+ </DropdownMenu>
52
+ )
53
+ }
@@ -0,0 +1,18 @@
1
+ 'use client'
2
+
3
+ import type { WebhookEndpointListItem } from '@admin/actions/webhooks'
4
+ import { Switch } from '@admin/components/ui/switch'
5
+ import { useToggleWebhookEnabled } from '@admin/hooks/use-webhooks'
6
+
7
+ export function WebhookEnabledSwitch({ webhook }: { webhook: WebhookEndpointListItem }) {
8
+ const toggleEnabled = useToggleWebhookEnabled()
9
+
10
+ return (
11
+ <Switch
12
+ checked={webhook.enabled}
13
+ disabled={toggleEnabled.isPending}
14
+ onCheckedChange={(checked) => toggleEnabled.mutate({ id: webhook.id, enabled: checked })}
15
+ aria-label={`Toggle ${webhook.name}`}
16
+ />
17
+ )
18
+ }
@@ -0,0 +1,370 @@
1
+ 'use client'
2
+
3
+ import type { WebhookEndpointListItem } from '@admin/actions/webhooks'
4
+ import {
5
+ createWebhook,
6
+ getWebhookSecret,
7
+ regenerateWebhookSecret,
8
+ sendTestWebhook,
9
+ updateWebhook
10
+ } from '@admin/actions/webhooks'
11
+ import { Button } from '@admin/components/ui/button'
12
+ import { Checkbox } from '@admin/components/ui/checkbox'
13
+ import {
14
+ Dialog,
15
+ DialogBody,
16
+ DialogContent,
17
+ DialogDescription,
18
+ DialogFooter,
19
+ DialogHeader,
20
+ DialogTitle
21
+ } from '@admin/components/ui/dialog'
22
+ import {
23
+ Form,
24
+ FormControl,
25
+ FormField,
26
+ FormItem,
27
+ FormLabel,
28
+ FormMessage
29
+ } from '@admin/components/ui/form'
30
+ import { Input } from '@admin/components/ui/input'
31
+ import { Switch } from '@admin/components/ui/switch'
32
+ import { webhookEventSources } from '@admin/data/webhook-events'
33
+ import { useWebhookSubscriptions } from '@admin/hooks/use-webhooks'
34
+ import { standardSchemaResolver } from '@hookform/resolvers/standard-schema'
35
+ import { useMutation, useQueryClient } from '@tanstack/react-query'
36
+ import { LoaderCircle } from 'lucide-react'
37
+ import * as React from 'react'
38
+ import { useForm } from 'react-hook-form'
39
+ import { toast } from 'sonner'
40
+ import { z } from 'zod/v3'
41
+
42
+ const endpointSchema = z.object({
43
+ name: z.string().trim().min(1, 'Name is required').max(200, 'Name is too long'),
44
+ url: z.string().trim().url('Enter a valid URL'),
45
+ enabled: z.boolean(),
46
+ events: z.array(z.string())
47
+ })
48
+
49
+ type EndpointFormValues = z.infer<typeof endpointSchema>
50
+
51
+ interface WebhookEndpointDialogProps {
52
+ webhook: WebhookEndpointListItem | null
53
+ open: boolean
54
+ onOpenChange: (open: boolean) => void
55
+ }
56
+
57
+ export function WebhookEndpointDialog({ webhook, open, onOpenChange }: WebhookEndpointDialogProps) {
58
+ const queryClient = useQueryClient()
59
+ const isEdit = webhook !== null
60
+ const { data: subscriptions } = useWebhookSubscriptions()
61
+ const subscribedEvents = React.useMemo(
62
+ () =>
63
+ webhook
64
+ ? (subscriptions ?? [])
65
+ .filter((entry) => entry.webhookId === webhook.id)
66
+ .map((entry) => entry.event)
67
+ : [],
68
+ [subscriptions, webhook]
69
+ )
70
+ const [selectedEvents, setSelectedEvents] = React.useState<string[] | null>(null)
71
+ const events = selectedEvents ?? subscribedEvents
72
+ const [secret, setSecret] = React.useState<string | null>(null)
73
+
74
+ const formSources = webhookEventSources.filter((source) => source.kind === 'form')
75
+ const contentSources = webhookEventSources.filter((source) => source.kind !== 'form')
76
+
77
+ const form = useForm<EndpointFormValues>({
78
+ resolver: standardSchemaResolver(endpointSchema),
79
+ defaultValues: {
80
+ name: webhook?.name ?? '',
81
+ url: webhook?.url ?? '',
82
+ enabled: webhook?.enabled ?? true,
83
+ events: subscribedEvents
84
+ }
85
+ })
86
+
87
+ const saveMutation = useMutation({
88
+ mutationFn: async (values: EndpointFormValues) => {
89
+ const endpointValues = {
90
+ name: values.name,
91
+ url: values.url,
92
+ enabled: values.enabled
93
+ }
94
+ const result = webhook
95
+ ? await updateWebhook(webhook.id, { ...endpointValues, events })
96
+ : await createWebhook({ ...endpointValues, events })
97
+ if (!result.success) {
98
+ throw new Error(result.error || 'Failed to save endpoint')
99
+ }
100
+
101
+ return result
102
+ },
103
+ onSuccess: async () => {
104
+ toast.success(isEdit ? 'Endpoint saved' : 'Endpoint created')
105
+ await queryClient.invalidateQueries({ queryKey: ['webhooks'] })
106
+ await queryClient.invalidateQueries({
107
+ queryKey: ['webhook-subscriptions']
108
+ })
109
+ onOpenChange(false)
110
+ },
111
+ onError: (error: Error) => {
112
+ toast.error(error.message || 'An unexpected error occurred')
113
+ }
114
+ })
115
+
116
+ const revealSecretMutation = useMutation({
117
+ mutationFn: async () => {
118
+ if (!webhook) throw new Error('Save the endpoint first')
119
+ const result = await getWebhookSecret(webhook.id)
120
+ if (!result.success || !result.secret) {
121
+ throw new Error(result.error || 'Failed to fetch signing secret')
122
+ }
123
+
124
+ return result.secret
125
+ },
126
+ onSuccess: (value) => setSecret(value),
127
+ onError: (error: Error) => {
128
+ toast.error(error.message || 'Failed to fetch signing secret')
129
+ }
130
+ })
131
+
132
+ const regenerateSecretMutation = useMutation({
133
+ mutationFn: async () => {
134
+ if (!webhook) throw new Error('Save the endpoint first')
135
+ const result = await regenerateWebhookSecret(webhook.id)
136
+ if (!result.success || !result.secret) {
137
+ throw new Error(result.error || 'Failed to regenerate signing secret')
138
+ }
139
+
140
+ return result.secret
141
+ },
142
+ onSuccess: (value) => {
143
+ setSecret(value)
144
+ toast.success('Signing secret regenerated')
145
+ },
146
+ onError: (error: Error) => {
147
+ toast.error(error.message || 'Failed to regenerate signing secret')
148
+ }
149
+ })
150
+
151
+ const testMutation = useMutation({
152
+ mutationFn: async () => {
153
+ if (!webhook) throw new Error('Save the endpoint first')
154
+ const result = await sendTestWebhook(webhook.id)
155
+ if (!result.success) {
156
+ throw new Error(result.error || 'Test webhook failed')
157
+ }
158
+
159
+ return result
160
+ },
161
+ onSuccess: async (result) => {
162
+ toast.success(
163
+ result.statusCode ? `Test delivered (status ${result.statusCode})` : 'Test delivered'
164
+ )
165
+ await queryClient.invalidateQueries({ queryKey: ['webhook-deliveries'] })
166
+ },
167
+ onError: (error: Error) => {
168
+ toast.error(error.message || 'Test webhook failed')
169
+ }
170
+ })
171
+
172
+ const isPending = saveMutation.isPending
173
+
174
+ function toggleEvent(event: string, checked: boolean) {
175
+ const nextEvents = checked ? [...events, event] : events.filter((entry) => entry !== event)
176
+ setSelectedEvents(nextEvents)
177
+ form.setValue('events', nextEvents)
178
+ }
179
+
180
+ return (
181
+ <Dialog open={open} onOpenChange={onOpenChange}>
182
+ <DialogContent className="sm:max-w-135">
183
+ <Form {...form}>
184
+ <form onSubmit={form.handleSubmit((values) => saveMutation.mutate(values))}>
185
+ <DialogHeader>
186
+ <DialogTitle>
187
+ {isEdit ? `Edit endpoint — ${webhook.name}` : 'Add endpoint'}
188
+ </DialogTitle>
189
+ <DialogDescription>
190
+ Events are delivered as signed JSON POST requests
191
+ </DialogDescription>
192
+ </DialogHeader>
193
+
194
+ <DialogBody>
195
+ <div className="space-y-4">
196
+ <FormField
197
+ control={form.control}
198
+ name="name"
199
+ render={({ field }) => (
200
+ <FormItem>
201
+ <FormLabel>Name</FormLabel>
202
+ <FormControl>
203
+ <Input placeholder="Zapier" disabled={isPending} {...field} />
204
+ </FormControl>
205
+ <FormMessage />
206
+ </FormItem>
207
+ )}
208
+ />
209
+ <FormField
210
+ control={form.control}
211
+ name="url"
212
+ render={({ field }) => (
213
+ <FormItem>
214
+ <FormLabel>URL</FormLabel>
215
+ <FormControl>
216
+ <Input
217
+ type="url"
218
+ placeholder="https://example.com/webhooks"
219
+ disabled={isPending}
220
+ {...field}
221
+ />
222
+ </FormControl>
223
+ <FormMessage />
224
+ </FormItem>
225
+ )}
226
+ />
227
+ <FormField
228
+ control={form.control}
229
+ name="enabled"
230
+ render={({ field }) => (
231
+ <FormItem className="flex items-center justify-between rounded-md border border-border px-3 py-2">
232
+ <FormLabel>Enabled</FormLabel>
233
+ <FormControl>
234
+ <Switch
235
+ checked={field.value}
236
+ onCheckedChange={field.onChange}
237
+ disabled={isPending}
238
+ />
239
+ </FormControl>
240
+ </FormItem>
241
+ )}
242
+ />
243
+ {isEdit && (
244
+ <div className="space-y-2 rounded-md border border-border p-3">
245
+ <p className="text-sm font-medium leading-none">Signing secret</p>
246
+ <div className="flex items-center gap-2">
247
+ <span className="min-w-0 flex-1 truncate font-mono text-xs">
248
+ {secret ?? 'whsec_••••••••••••••••'}
249
+ </span>
250
+ <Button
251
+ type="button"
252
+ variant="outline"
253
+ size="sm"
254
+ disabled={revealSecretMutation.isPending || secret !== null}
255
+ onClick={() => revealSecretMutation.mutate()}
256
+ >
257
+ Reveal
258
+ </Button>
259
+ <Button
260
+ type="button"
261
+ variant="outline"
262
+ size="sm"
263
+ disabled={regenerateSecretMutation.isPending}
264
+ onClick={() => regenerateSecretMutation.mutate()}
265
+ >
266
+ {regenerateSecretMutation.isPending && (
267
+ <LoaderCircle className="animate-spin" />
268
+ )}
269
+ Regenerate
270
+ </Button>
271
+ </div>
272
+ <p className="text-xs text-muted-foreground">
273
+ Requests carry x-webhook-id, x-webhook-timestamp, and an HMAC-SHA256
274
+ x-webhook-signature of the body.
275
+ </p>
276
+ </div>
277
+ )}
278
+ {(formSources.length > 0 || contentSources.length > 0) && (
279
+ <FormField
280
+ control={form.control}
281
+ name="events"
282
+ render={() => (
283
+ <React.Fragment>
284
+ {formSources.length > 0 && (
285
+ <fieldset className="space-y-2 rounded-md border border-border p-3">
286
+ <legend className="px-1 text-xs font-medium uppercase tracking-wide text-muted-foreground">
287
+ Events · Forms
288
+ </legend>
289
+ {formSources.flatMap((source) =>
290
+ source.events.map((event) => (
291
+ <FormItem key={event} className="flex items-center gap-2 space-y-0">
292
+ <FormControl>
293
+ <Checkbox
294
+ checked={events.includes(event)}
295
+ onCheckedChange={(checked) =>
296
+ toggleEvent(event, checked === true)
297
+ }
298
+ disabled={isPending}
299
+ />
300
+ </FormControl>
301
+ <FormLabel className="font-mono text-xs font-normal">
302
+ {event}
303
+ </FormLabel>
304
+ </FormItem>
305
+ ))
306
+ )}
307
+ </fieldset>
308
+ )}
309
+ {contentSources.length > 0 && (
310
+ <fieldset className="space-y-2 rounded-md border border-border p-3">
311
+ <legend className="px-1 text-xs font-medium uppercase tracking-wide text-muted-foreground">
312
+ Events · Entities
313
+ </legend>
314
+ {contentSources.flatMap((source) =>
315
+ source.events.map((event) => (
316
+ <FormItem key={event} className="flex items-center gap-2 space-y-0">
317
+ <FormControl>
318
+ <Checkbox
319
+ checked={events.includes(event)}
320
+ onCheckedChange={(checked) =>
321
+ toggleEvent(event, checked === true)
322
+ }
323
+ disabled={isPending}
324
+ />
325
+ </FormControl>
326
+ <FormLabel className="font-mono text-xs font-normal">
327
+ {event}
328
+ </FormLabel>
329
+ </FormItem>
330
+ ))
331
+ )}
332
+ </fieldset>
333
+ )}
334
+ </React.Fragment>
335
+ )}
336
+ />
337
+ )}
338
+ {formSources.length === 0 && contentSources.length === 0 && (
339
+ <p className="text-sm text-muted-foreground">
340
+ No schemas yet — generate an entity or form schema to subscribe this endpoint to
341
+ its events.
342
+ </p>
343
+ )}
344
+ </div>
345
+ </DialogBody>
346
+
347
+ <DialogFooter showCloseButton={true}>
348
+ {isEdit && (
349
+ <Button
350
+ type="button"
351
+ variant="outline"
352
+ size="lg"
353
+ disabled={testMutation.isPending}
354
+ onClick={() => testMutation.mutate()}
355
+ >
356
+ {testMutation.isPending && <LoaderCircle className="animate-spin" />}
357
+ Send test
358
+ </Button>
359
+ )}
360
+ <Button type="submit" disabled={isPending} size="lg">
361
+ {isPending && <LoaderCircle className="animate-spin" />}
362
+ {isEdit ? 'Save endpoint' : 'Create endpoint'}
363
+ </Button>
364
+ </DialogFooter>
365
+ </form>
366
+ </Form>
367
+ </DialogContent>
368
+ </Dialog>
369
+ )
370
+ }