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.
- package/dist/assets/adapters/next/templates/init/pages/settings/forms/form-notifications-drawer.tsx +146 -0
- package/dist/assets/adapters/next/templates/init/pages/settings/forms/forms-settings-columns.tsx +31 -0
- package/dist/assets/adapters/next/templates/init/pages/settings/forms/forms-settings-page-content.tsx +39 -90
- package/dist/assets/adapters/next/templates/init/pages/settings/forms/forms-settings-table.tsx +87 -0
- package/dist/cli.js +1328 -1220
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/dist/assets/adapters/next/templates/init/pages/settings/forms/edit-form-notifications-dialog.tsx +0 -128
package/dist/assets/adapters/next/templates/init/pages/settings/forms/form-notifications-drawer.tsx
ADDED
|
@@ -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
|
+
}
|
package/dist/assets/adapters/next/templates/init/pages/settings/forms/forms-settings-columns.tsx
ADDED
|
@@ -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 {
|
|
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 [
|
|
13
|
+
const [selectedKey, setSelectedKey] = React.useState<string | null>(null)
|
|
29
14
|
|
|
30
|
-
const
|
|
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="
|
|
36
|
-
<
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
{
|
|
95
|
-
<
|
|
96
|
-
key={
|
|
97
|
-
formLabel={
|
|
98
|
-
settingsKey={
|
|
99
|
-
initialEmails={
|
|
100
|
-
|
|
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)
|
|
54
|
+
if (!nextOpen) setSelectedKey(null)
|
|
106
55
|
}}
|
|
107
56
|
/>
|
|
108
|
-
)}
|
|
57
|
+
) : null}
|
|
109
58
|
</React.Fragment>
|
|
110
59
|
)
|
|
111
60
|
}
|
package/dist/assets/adapters/next/templates/init/pages/settings/forms/forms-settings-table.tsx
ADDED
|
@@ -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
|
+
}
|