betterstart-cli 0.0.91 → 0.0.93

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.
@@ -0,0 +1,146 @@
1
+ 'use client'
2
+
3
+ import { upsertFormSettings } from '@admin/actions/forms'
4
+ import { Button } from '@admin/components/ui/button'
5
+ import { Card, CardContent, CardHeader, CardTitle } from '@admin/components/ui/card'
6
+ import {
7
+ Drawer,
8
+ DrawerClose,
9
+ DrawerContent,
10
+ DrawerDescription,
11
+ DrawerFooter,
12
+ DrawerHeader,
13
+ DrawerTitle
14
+ } from '@admin/components/ui/drawer'
15
+ import {
16
+ Form,
17
+ FormControl,
18
+ FormDescription,
19
+ FormField,
20
+ FormItem,
21
+ FormLabel,
22
+ FormMessage
23
+ } from '@admin/components/ui/form'
24
+ import { ScrollArea } from '@admin/components/ui/scroll-area'
25
+ import { Textarea } from '@admin/components/ui/textarea'
26
+ import { standardSchemaResolver } from '@hookform/resolvers/standard-schema'
27
+ import { useMutation, useQueryClient } from '@tanstack/react-query'
28
+ import { LoaderCircle } from 'lucide-react'
29
+ import { useForm } from 'react-hook-form'
30
+ import { toast } from 'sonner'
31
+ import { z } from 'zod/v3'
32
+
33
+ const notificationsSchema = z.object({
34
+ notificationEmails: z.string().trim()
35
+ })
36
+
37
+ type NotificationsFormValues = z.infer<typeof notificationsSchema>
38
+
39
+ interface FormNotificationsDrawerProps {
40
+ formLabel: string
41
+ settingsKey: string
42
+ initialEmails: string
43
+ open: boolean
44
+ onOpenChange: (open: boolean) => void
45
+ }
46
+
47
+ export function FormNotificationsDrawer({
48
+ formLabel,
49
+ settingsKey,
50
+ initialEmails,
51
+ open,
52
+ onOpenChange
53
+ }: FormNotificationsDrawerProps) {
54
+ const queryClient = useQueryClient()
55
+
56
+ const form = useForm<NotificationsFormValues>({
57
+ resolver: standardSchemaResolver(notificationsSchema),
58
+ defaultValues: { notificationEmails: initialEmails }
59
+ })
60
+
61
+ const mutation = useMutation({
62
+ mutationFn: async (values: NotificationsFormValues) => {
63
+ const result = await upsertFormSettings(settingsKey, {
64
+ notificationEmails: values.notificationEmails || null
65
+ })
66
+ if (!result.success) {
67
+ throw new Error(result.error || 'Failed to save notification emails')
68
+ }
69
+
70
+ return result
71
+ },
72
+ onSuccess: async () => {
73
+ toast.success('Notification emails saved')
74
+ await queryClient.invalidateQueries({ queryKey: ['form-settings'] })
75
+ onOpenChange(false)
76
+ },
77
+ onError: (error: Error) => {
78
+ toast.error(error.message || 'An unexpected error occurred')
79
+ }
80
+ })
81
+
82
+ const isPending = mutation.isPending
83
+
84
+ return (
85
+ <Drawer direction="right" open={open} onOpenChange={onOpenChange} modal>
86
+ <DrawerContent className="h-full sm:max-w-xl">
87
+ <Form {...form}>
88
+ <form
89
+ onSubmit={form.handleSubmit((values) => mutation.mutate(values))}
90
+ className="flex min-h-0 flex-1 flex-col gap-4"
91
+ >
92
+ <DrawerHeader>
93
+ <DrawerTitle>{formLabel} notifications</DrawerTitle>
94
+ <DrawerDescription>
95
+ Send an email to these addresses when a submission arrives. Webhooks are managed on
96
+ the Webhooks tab.
97
+ </DrawerDescription>
98
+ </DrawerHeader>
99
+
100
+ <ScrollArea className="min-h-0 flex-1 px-6">
101
+ <Card>
102
+ <CardHeader>
103
+ <CardTitle>Notifications</CardTitle>
104
+ </CardHeader>
105
+ <CardContent>
106
+ <FormField
107
+ control={form.control}
108
+ name="notificationEmails"
109
+ render={({ field }) => (
110
+ <FormItem>
111
+ <FormLabel>Notification emails</FormLabel>
112
+ <FormControl>
113
+ <Textarea
114
+ placeholder="sales@example.com, team@example.com"
115
+ disabled={isPending}
116
+ {...field}
117
+ />
118
+ </FormControl>
119
+ <FormDescription>
120
+ Comma-separated email addresses. Leave empty to disable notifications.
121
+ </FormDescription>
122
+ <FormMessage />
123
+ </FormItem>
124
+ )}
125
+ />
126
+ </CardContent>
127
+ </Card>
128
+ </ScrollArea>
129
+
130
+ <DrawerFooter className="flex-row justify-end">
131
+ <DrawerClose asChild>
132
+ <Button type="button" variant="outline" size="lg" disabled={isPending}>
133
+ Cancel
134
+ </Button>
135
+ </DrawerClose>
136
+ <Button type="submit" disabled={isPending || !form.formState.isDirty} size="lg">
137
+ {isPending && <LoaderCircle className="animate-spin" />}
138
+ Save
139
+ </Button>
140
+ </DrawerFooter>
141
+ </form>
142
+ </Form>
143
+ </DrawerContent>
144
+ </Drawer>
145
+ )
146
+ }
@@ -0,0 +1,31 @@
1
+ 'use client'
2
+
3
+ import type { ColumnDef } from '@tanstack/react-table'
4
+
5
+ export interface FormSettingsRow {
6
+ settingsKey: string
7
+ label: string
8
+ notificationEmails: string | null
9
+ }
10
+
11
+ export function createFormSettingsColumns(isPending: boolean): ColumnDef<FormSettingsRow>[] {
12
+ return [
13
+ {
14
+ id: 'form',
15
+ header: 'Form',
16
+ cell: ({ row }) => <span className="truncate font-medium">{row.original.label}</span>
17
+ },
18
+ {
19
+ id: 'notificationEmails',
20
+ header: 'Notification emails',
21
+ cell: ({ row }) =>
22
+ isPending ? (
23
+ <span className="text-muted-foreground">Loading…</span>
24
+ ) : row.original.notificationEmails ? (
25
+ <span className="truncate font-mono text-xs">{row.original.notificationEmails}</span>
26
+ ) : (
27
+ <span className="text-muted-foreground">No emails configured</span>
28
+ )
29
+ }
30
+ ]
31
+ }
@@ -1,111 +1,60 @@
1
1
  'use client'
2
2
 
3
3
  import { PageHeader } from '@admin/components/shared/page-header'
4
- import { Button } from '@admin/components/ui/button'
5
- import {
6
- Card,
7
- CardContent,
8
- CardDescription,
9
- CardHeader,
10
- CardTitle
11
- } from '@admin/components/ui/card'
12
- import {
13
- Table,
14
- TableBody,
15
- TableCell,
16
- TableHead,
17
- TableHeader,
18
- TableRow
19
- } from '@admin/components/ui/table'
20
4
  import { webhookEventSources } from '@admin/data/webhook-events'
21
5
  import { useFormSettingsList } from '@admin/hooks/use-webhooks'
22
6
  import * as React from 'react'
23
- import { EditFormNotificationsDialog } from './edit-form-notifications-dialog'
7
+ import { FormNotificationsDrawer } from './form-notifications-drawer'
8
+ import { createFormSettingsColumns, type FormSettingsRow } from './forms-settings-columns'
9
+ import { FormsSettingsTable } from './forms-settings-table'
24
10
 
25
11
  export function FormsSettingsPageContent() {
26
- const formSources = webhookEventSources.filter((source) => source.kind === 'form')
27
12
  const { data: settings, isPending } = useFormSettingsList()
28
- const [editingKey, setEditingKey] = React.useState<string | null>(null)
13
+ const [selectedKey, setSelectedKey] = React.useState<string | null>(null)
29
14
 
30
- const editingSource = formSources.find((source) => source.settingsKey === editingKey)
15
+ const rows = React.useMemo<FormSettingsRow[]>(
16
+ () =>
17
+ webhookEventSources.flatMap((source) => {
18
+ if (source.kind !== 'form' || !source.settingsKey) return []
19
+
20
+ return [
21
+ {
22
+ settingsKey: source.settingsKey,
23
+ label: source.label,
24
+ notificationEmails:
25
+ settings?.find((entry) => entry.formName === source.settingsKey)
26
+ ?.notificationEmails ?? null
27
+ }
28
+ ]
29
+ }),
30
+ [settings]
31
+ )
32
+
33
+ const columns = React.useMemo(() => createFormSettingsColumns(isPending), [isPending])
34
+ const selectedRow = rows.find((row) => row.settingsKey === selectedKey) ?? null
31
35
 
32
36
  return (
33
37
  <React.Fragment>
34
38
  <PageHeader title="Form Settings" />
35
- <main className="mx-auto max-w-5xl w-full space-y-4 p-4">
36
- <Card>
37
- <CardHeader>
38
- <CardTitle>Form notifications</CardTitle>
39
- <CardDescription>
40
- Emails notified when a submission arrives. Webhooks are managed on the Webhooks tab.
41
- </CardDescription>
42
- </CardHeader>
43
- <CardContent>
44
- {formSources.length === 0 ? (
45
- <p className="text-sm text-muted-foreground">
46
- No forms yet. Create a form schema to configure its notifications here.
47
- </p>
48
- ) : (
49
- <Table>
50
- <TableHeader>
51
- <TableRow>
52
- <TableHead>Form</TableHead>
53
- <TableHead>Notification emails</TableHead>
54
- <TableHead className="w-20" />
55
- </TableRow>
56
- </TableHeader>
57
- <TableBody>
58
- {formSources.map((source) => {
59
- const formSettings = settings?.find(
60
- (entry) => entry.formName === source.settingsKey
61
- )
62
- return (
63
- <TableRow key={source.schema}>
64
- <TableCell className="font-medium">{source.label}</TableCell>
65
- <TableCell>
66
- {isPending ? (
67
- <span className="text-muted-foreground">Loading…</span>
68
- ) : formSettings?.notificationEmails ? (
69
- <span className="font-mono text-xs">
70
- {formSettings.notificationEmails}
71
- </span>
72
- ) : (
73
- <span className="text-muted-foreground">No emails configured</span>
74
- )}
75
- </TableCell>
76
- <TableCell className="text-right">
77
- <Button
78
- variant="outline"
79
- size="sm"
80
- onClick={() => setEditingKey(source.settingsKey ?? source.schema)}
81
- >
82
- Edit
83
- </Button>
84
- </TableCell>
85
- </TableRow>
86
- )
87
- })}
88
- </TableBody>
89
- </Table>
90
- )}
91
- </CardContent>
92
- </Card>
39
+ <main className="space-y-4 px-4 pb-4 flex-1">
40
+ <FormsSettingsTable
41
+ columns={columns}
42
+ rows={rows}
43
+ onRowClick={(row) => setSelectedKey(row.settingsKey)}
44
+ />
93
45
  </main>
94
- {editingSource?.settingsKey && (
95
- <EditFormNotificationsDialog
96
- key={editingSource.settingsKey}
97
- formLabel={editingSource.label}
98
- settingsKey={editingSource.settingsKey}
99
- initialEmails={
100
- settings?.find((entry) => entry.formName === editingSource.settingsKey)
101
- ?.notificationEmails ?? ''
102
- }
103
- open={editingKey !== null}
46
+ {selectedRow ? (
47
+ <FormNotificationsDrawer
48
+ key={selectedRow.settingsKey}
49
+ formLabel={selectedRow.label}
50
+ settingsKey={selectedRow.settingsKey}
51
+ initialEmails={selectedRow.notificationEmails ?? ''}
52
+ open={selectedKey !== null}
104
53
  onOpenChange={(nextOpen) => {
105
- if (!nextOpen) setEditingKey(null)
54
+ if (!nextOpen) setSelectedKey(null)
106
55
  }}
107
56
  />
108
- )}
57
+ ) : null}
109
58
  </React.Fragment>
110
59
  )
111
60
  }
@@ -0,0 +1,87 @@
1
+ 'use client'
2
+
3
+ import {
4
+ DataGrid,
5
+ DataGridBody,
6
+ DataGridCell,
7
+ DataGridHead,
8
+ DataGridHeader,
9
+ DataGridRow
10
+ } from '@admin/components/shared/data-table/data-grid'
11
+ import { cn } from '@admin/utils/shared/cn'
12
+ import { type ColumnDef, flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table'
13
+ import type * as React from 'react'
14
+ import type { FormSettingsRow } from './forms-settings-columns'
15
+
16
+ interface FormsSettingsTableProps extends React.HTMLAttributes<HTMLDivElement> {
17
+ columns: ColumnDef<FormSettingsRow>[]
18
+ rows: FormSettingsRow[]
19
+ onRowClick: (row: FormSettingsRow) => void
20
+ }
21
+
22
+ export function FormsSettingsTable({
23
+ columns,
24
+ rows,
25
+ onRowClick,
26
+ className
27
+ }: FormsSettingsTableProps) {
28
+ const table = useReactTable({
29
+ data: rows,
30
+ columns,
31
+ getCoreRowModel: getCoreRowModel()
32
+ })
33
+
34
+ return (
35
+ <div className={cn('flex flex-col gap-6 pt-px pb-2 h-full', className)}>
36
+ <DataGrid
37
+ containerClassName="min-h-0 flex-1"
38
+ gridTemplateColumns="minmax(200px, 1fr) minmax(240px, 2fr)"
39
+ >
40
+ <DataGridHeader>
41
+ {table.getHeaderGroups().map((headerGroup) => (
42
+ <DataGridRow key={headerGroup.id}>
43
+ {headerGroup.headers.map((header) => (
44
+ <DataGridHead key={header.id} className="overflow-hidden">
45
+ {header.isPlaceholder
46
+ ? null
47
+ : flexRender(header.column.columnDef.header, header.getContext())}
48
+ </DataGridHead>
49
+ ))}
50
+ </DataGridRow>
51
+ ))}
52
+ </DataGridHeader>
53
+ <DataGridBody>
54
+ {table.getRowModel().rows.length ? (
55
+ table.getRowModel().rows.map((row) => (
56
+ <DataGridRow
57
+ key={row.id}
58
+ className="cursor-pointer hover:bg-muted/50"
59
+ onClick={(event) => {
60
+ const target = event.target as HTMLElement
61
+ if (!event.currentTarget.contains(target)) return
62
+ if (target.closest('button, a, input, [role="checkbox"]')) return
63
+ onRowClick(row.original)
64
+ }}
65
+ >
66
+ {row.getVisibleCells().map((cell) => (
67
+ <DataGridCell key={cell.id} data-column-id={cell.column.id}>
68
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
69
+ </DataGridCell>
70
+ ))}
71
+ </DataGridRow>
72
+ ))
73
+ ) : (
74
+ <DataGridRow>
75
+ <DataGridCell
76
+ className="flex h-24 items-center justify-center text-center text-muted-foreground"
77
+ style={{ gridColumn: '1 / -1' }}
78
+ >
79
+ No forms yet. Create a form schema to configure its notifications here.
80
+ </DataGridCell>
81
+ </DataGridRow>
82
+ )}
83
+ </DataGridBody>
84
+ </DataGrid>
85
+ </div>
86
+ )
87
+ }