betterstart-cli 0.0.98 → 0.0.100
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/api/upload-route.ts +2 -1
- package/dist/assets/adapters/next/templates/init/components/layouts/admin-settings-sidebar.tsx +2 -2
- package/dist/assets/adapters/next/templates/init/components/layouts/admin-sign-out.tsx +27 -8
- package/dist/assets/adapters/next/templates/init/components/shared/data-table/data-table.tsx +3 -1
- package/dist/assets/adapters/next/templates/init/components/shared/delete-dialog.tsx +3 -2
- package/dist/assets/adapters/next/templates/init/components/shared/entity-versions/entity-version-restore-dialog.tsx +4 -3
- package/dist/assets/adapters/next/templates/init/components/shared/entity-versions/entity-versions-button.tsx +5 -2
- package/dist/assets/adapters/next/templates/init/components/shared/media/edit-media-dialog-content.tsx +11 -5
- package/dist/assets/adapters/next/templates/init/components/shared/media/media-delete-dialog.tsx +6 -4
- package/dist/assets/adapters/next/templates/init/components/shared/media/media-delete-drawer.tsx +12 -8
- package/dist/assets/adapters/next/templates/init/components/shared/media/media-empty-state.tsx +6 -5
- package/dist/assets/adapters/next/templates/init/components/shared/media/media-grid-pagination.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/components/shared/media/media-url-importer.tsx +5 -3
- package/dist/assets/adapters/next/templates/init/components/shared/search-input.tsx +2 -0
- package/dist/assets/adapters/next/templates/init/components/shared/sort-order-dialog.tsx +12 -7
- package/dist/assets/adapters/next/templates/init/hooks/use-entity-versions.ts +2 -1
- package/dist/assets/adapters/next/templates/init/lib/actions/audit/cleanup-audit-logs.ts +2 -1
- package/dist/assets/adapters/next/templates/init/lib/actions/forms/upsert-form-settings.ts +2 -1
- package/dist/assets/adapters/next/templates/init/lib/actions/media/create-media.ts +2 -1
- package/dist/assets/adapters/next/templates/init/lib/actions/media/delete-media-bulk.ts +2 -1
- package/dist/assets/adapters/next/templates/init/lib/actions/media/delete-media.ts +2 -1
- package/dist/assets/adapters/next/templates/init/lib/actions/media/update-media.ts +2 -1
- package/dist/assets/adapters/next/templates/init/lib/actions/profile/change-own-password.ts +2 -1
- package/dist/assets/adapters/next/templates/init/lib/actions/profile/update-email.ts +2 -1
- package/dist/assets/adapters/next/templates/init/lib/actions/upload/upload-media-from-url.ts +2 -2
- package/dist/assets/adapters/next/templates/init/lib/actions/users/change-user-password.ts +2 -1
- package/dist/assets/adapters/next/templates/init/lib/actions/users/create-user.ts +2 -1
- package/dist/assets/adapters/next/templates/init/lib/actions/users/delete-user.ts +2 -1
- package/dist/assets/adapters/next/templates/init/lib/actions/users/update-user-role.ts +2 -1
- package/dist/assets/adapters/next/templates/init/lib/actions/webhooks/create-webhook.ts +2 -1
- package/dist/assets/adapters/next/templates/init/lib/actions/webhooks/delete-webhook.ts +2 -1
- package/dist/assets/adapters/next/templates/init/lib/actions/webhooks/get-webhook-secret.ts +2 -1
- package/dist/assets/adapters/next/templates/init/lib/actions/webhooks/regenerate-webhook-secret.ts +2 -1
- package/dist/assets/adapters/next/templates/init/lib/actions/webhooks/send-test-webhook.ts +2 -1
- package/dist/assets/adapters/next/templates/init/lib/actions/webhooks/update-webhook.ts +2 -1
- package/dist/assets/adapters/next/templates/init/pages/forgot-password-form.tsx +7 -7
- package/dist/assets/adapters/next/templates/init/pages/login-form.tsx +7 -7
- package/dist/assets/adapters/next/templates/init/pages/login-page.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/pages/media/media-page-content.tsx +5 -4
- package/dist/assets/adapters/next/templates/init/pages/profile/profile-form.tsx +13 -10
- package/dist/assets/adapters/next/templates/init/pages/reset-password-form.tsx +5 -5
- package/dist/assets/adapters/next/templates/init/pages/settings/audit-log/audit-log-table.tsx +3 -1
- package/dist/assets/adapters/next/templates/init/pages/settings/forms/form-notifications-drawer.tsx +10 -5
- package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/delete-webhook-dialog.tsx +19 -10
- package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/webhook-actions.tsx +9 -4
- package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/webhook-endpoint-dialog.tsx +26 -20
- package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/webhooks-logs-page-content.tsx +4 -4
- package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/webhooks-logs-table.tsx +3 -1
- package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/webhooks-table.tsx +3 -1
- package/dist/assets/adapters/next/templates/init/pages/users/change-password-dialog.tsx +6 -5
- package/dist/assets/adapters/next/templates/init/pages/users/create-user-dialog.tsx +5 -5
- package/dist/assets/adapters/next/templates/init/pages/users/delete-user-dialog.tsx +6 -4
- package/dist/assets/adapters/next/templates/init/pages/users/edit-role-dialog.tsx +9 -5
- package/dist/assets/adapters/next/templates/init/pages/users/users-table.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/utils/error/action-error.ts +35 -0
- package/dist/assets/shared-assets/react-admin/schema.json +8 -0
- package/dist/assets/shared-assets/react-admin/ui/sonner.tsx +3 -8
- package/dist/assets/shared-assets/react-admin/ui/spinner.tsx +1 -1
- package/dist/{chunk-AIQWGMU7.js → chunk-45Z6XDG5.js} +419 -83
- package/dist/chunk-45Z6XDG5.js.map +1 -0
- package/dist/cli.js +2027 -2391
- package/dist/cli.js.map +1 -1
- package/dist/{template-reader-F2VFSDKV.js → template-reader-J6G7N5V6.js} +2 -2
- package/package.json +1 -1
- package/dist/chunk-AIQWGMU7.js.map +0 -1
- /package/dist/{template-reader-F2VFSDKV.js.map → template-reader-J6G7N5V6.js.map} +0 -0
|
@@ -8,6 +8,7 @@ import db from '@admin/db'
|
|
|
8
8
|
import { adminMedia } from '@admin/db/schema'
|
|
9
9
|
import { runAdminEventHooks } from '@admin/lib/lifecycle-hooks'
|
|
10
10
|
import { getTouchedFieldNames } from '@admin/utils/audit/audit'
|
|
11
|
+
import { toActionError } from '@admin/utils/error/action-error'
|
|
11
12
|
import { mapMediaRow, selectMedia } from '@admin/utils/media/query'
|
|
12
13
|
import { validateFiles } from '@admin/utils/validation/validation'
|
|
13
14
|
import { eq } from 'drizzle-orm'
|
|
@@ -151,7 +152,7 @@ export async function POST(request: NextRequest) {
|
|
|
151
152
|
|
|
152
153
|
return NextResponse.json({ success: true, files: uploadedFiles })
|
|
153
154
|
} catch (error) {
|
|
154
|
-
const message = error
|
|
155
|
+
const message = toActionError(error, 'Failed to upload files')
|
|
155
156
|
return NextResponse.json({ success: false, error: message }, { status: 500 })
|
|
156
157
|
}
|
|
157
158
|
}
|
package/dist/assets/adapters/next/templates/init/components/layouts/admin-settings-sidebar.tsx
CHANGED
|
@@ -23,12 +23,12 @@ const settingsNav = [
|
|
|
23
23
|
exact: true
|
|
24
24
|
},
|
|
25
25
|
{
|
|
26
|
-
label: '
|
|
26
|
+
label: 'Webhooks',
|
|
27
27
|
href: '/admin/settings/webhooks',
|
|
28
28
|
exact: true
|
|
29
29
|
},
|
|
30
30
|
{
|
|
31
|
-
label: '
|
|
31
|
+
label: 'Deliveries',
|
|
32
32
|
href: '/admin/settings/webhooks/logs',
|
|
33
33
|
exact: true
|
|
34
34
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
+
import * as React from 'react'
|
|
3
4
|
import { signOut } from '@admin/auth/client'
|
|
4
5
|
import { Button } from '@admin/components/ui/button'
|
|
5
6
|
import { DropdownMenuItem } from '@admin/components/ui/dropdown-menu'
|
|
7
|
+
import { Spinner } from '@admin/components/ui/spinner'
|
|
6
8
|
import { cn } from '@admin/utils/shared/cn'
|
|
7
9
|
import { LogOut } from 'lucide-react'
|
|
8
10
|
import { useRouter } from 'next/navigation'
|
|
@@ -15,10 +17,13 @@ export function AdminSignOut({
|
|
|
15
17
|
mode?: 'menu-item' | 'button'
|
|
16
18
|
}) {
|
|
17
19
|
const router = useRouter()
|
|
20
|
+
const [isPending, startTransition] = React.useTransition()
|
|
18
21
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
+
function handleSignOut() {
|
|
23
|
+
startTransition(async () => {
|
|
24
|
+
await signOut()
|
|
25
|
+
router.push('/admin/login')
|
|
26
|
+
})
|
|
22
27
|
}
|
|
23
28
|
|
|
24
29
|
if (mode === 'button') {
|
|
@@ -27,18 +32,32 @@ export function AdminSignOut({
|
|
|
27
32
|
type="button"
|
|
28
33
|
variant="ghost"
|
|
29
34
|
onClick={handleSignOut}
|
|
35
|
+
disabled={isPending}
|
|
36
|
+
aria-busy={isPending}
|
|
30
37
|
className={cn('justify-between', className)}
|
|
31
38
|
>
|
|
32
|
-
<span>
|
|
33
|
-
|
|
39
|
+
<span>{isPending ? 'Signing out…' : 'Sign out'}</span>
|
|
40
|
+
{isPending ? (
|
|
41
|
+
<Spinner className="size-4" />
|
|
42
|
+
) : (
|
|
43
|
+
<LogOut strokeWidth={1.2} className="size-4" />
|
|
44
|
+
)}
|
|
34
45
|
</Button>
|
|
35
46
|
)
|
|
36
47
|
}
|
|
37
48
|
|
|
38
49
|
return (
|
|
39
|
-
<DropdownMenuItem
|
|
40
|
-
|
|
41
|
-
|
|
50
|
+
<DropdownMenuItem
|
|
51
|
+
onSelect={(event) => {
|
|
52
|
+
event.preventDefault()
|
|
53
|
+
handleSignOut()
|
|
54
|
+
}}
|
|
55
|
+
disabled={isPending}
|
|
56
|
+
aria-busy={isPending}
|
|
57
|
+
className={className}
|
|
58
|
+
>
|
|
59
|
+
<span>{isPending ? 'Signing out…' : 'Sign out'}</span>
|
|
60
|
+
{isPending ? <Spinner className="size-4" /> : <LogOut strokeWidth={1.2} className="size-4" />}
|
|
42
61
|
</DropdownMenuItem>
|
|
43
62
|
)
|
|
44
63
|
}
|
package/dist/assets/adapters/next/templates/init/components/shared/data-table/data-table.tsx
CHANGED
|
@@ -186,7 +186,9 @@ export function DataTable<TData extends RowData, TValue>({
|
|
|
186
186
|
className="flex h-24 items-center justify-center text-center"
|
|
187
187
|
style={{ gridColumn: '1 / -1' }}
|
|
188
188
|
>
|
|
189
|
-
<div className="text-destructive">
|
|
189
|
+
<div className="text-destructive">
|
|
190
|
+
Failed to load these results. Refresh to try again.
|
|
191
|
+
</div>
|
|
190
192
|
</DataGridCell>
|
|
191
193
|
</DataGridRow>
|
|
192
194
|
) : table.getRowModel().rows?.length ? (
|
|
@@ -46,7 +46,7 @@ export function DeleteDialog({
|
|
|
46
46
|
Delete {count} {label}?
|
|
47
47
|
</AlertDialogTitle>
|
|
48
48
|
<AlertDialogDescription>
|
|
49
|
-
This
|
|
49
|
+
This permanently deletes {targetLabel} and cannot be undone.
|
|
50
50
|
</AlertDialogDescription>
|
|
51
51
|
</AlertDialogHeader>
|
|
52
52
|
<AlertDialogFooter>
|
|
@@ -57,8 +57,9 @@ export function DeleteDialog({
|
|
|
57
57
|
onConfirm()
|
|
58
58
|
}}
|
|
59
59
|
disabled={isPending}
|
|
60
|
+
aria-busy={isPending}
|
|
60
61
|
>
|
|
61
|
-
{isPending ? 'Deleting
|
|
62
|
+
{isPending ? 'Deleting…' : `Delete ${count} ${label}`}
|
|
62
63
|
</AlertDialogAction>
|
|
63
64
|
</AlertDialogFooter>
|
|
64
65
|
</AlertDialogContent>
|
|
@@ -57,8 +57,8 @@ export function EntityVersionRestoreDialog({
|
|
|
57
57
|
<AlertDialogHeader>
|
|
58
58
|
<AlertDialogTitle>Restore v{version.version}?</AlertDialogTitle>
|
|
59
59
|
<AlertDialogDescription>
|
|
60
|
-
This
|
|
61
|
-
|
|
60
|
+
This replaces the current content with v{version.version}. The current content is saved
|
|
61
|
+
as a new version first.
|
|
62
62
|
</AlertDialogDescription>
|
|
63
63
|
</AlertDialogHeader>
|
|
64
64
|
<AlertDialogFooter>
|
|
@@ -70,8 +70,9 @@ export function EntityVersionRestoreDialog({
|
|
|
70
70
|
handleRestore()
|
|
71
71
|
}}
|
|
72
72
|
disabled={isPending}
|
|
73
|
+
aria-busy={isPending}
|
|
73
74
|
>
|
|
74
|
-
{isPending ? 'Restoring
|
|
75
|
+
{isPending ? 'Restoring…' : `Restore v${version.version}`}
|
|
75
76
|
</AlertDialogAction>
|
|
76
77
|
</AlertDialogFooter>
|
|
77
78
|
</AlertDialogContent>
|
|
@@ -4,6 +4,7 @@ import * as React from 'react'
|
|
|
4
4
|
import type { EntityVersionWithAuthor } from '@admin/actions/entity-versions'
|
|
5
5
|
import { EntityVersionsDrawer } from '@admin/components/shared/entity-versions/entity-versions-drawer'
|
|
6
6
|
import { Button } from '@admin/components/ui/button'
|
|
7
|
+
import { Spinner } from '@admin/components/ui/spinner'
|
|
7
8
|
import { useEntityVersions } from '@admin/hooks/use-entity-versions'
|
|
8
9
|
import { History } from 'lucide-react'
|
|
9
10
|
|
|
@@ -53,10 +54,12 @@ export function EntityVersionsButton({
|
|
|
53
54
|
variant="ghost"
|
|
54
55
|
size="sm"
|
|
55
56
|
onClick={() => handleOpenChange(true)}
|
|
57
|
+
disabled={isPending}
|
|
58
|
+
aria-busy={isPending}
|
|
56
59
|
className="ml-auto shrink-0"
|
|
57
60
|
>
|
|
58
|
-
<History className="size-4" />
|
|
59
|
-
Versions
|
|
61
|
+
{isPending ? <Spinner className="size-4" /> : <History className="size-4" />}
|
|
62
|
+
{isPending ? 'Loading Versions…' : 'Versions'}
|
|
60
63
|
</Button>
|
|
61
64
|
<EntityVersionsDrawer
|
|
62
65
|
open={open}
|
|
@@ -10,9 +10,10 @@ import { Card, CardContent, CardFooter } from '@admin/components/ui/card'
|
|
|
10
10
|
import { DrawerContent, DrawerFooter, DrawerHeader, DrawerTitle } from '@admin/components/ui/drawer'
|
|
11
11
|
import { Form, FormControl, FormField, FormItem, FormLabel } from '@admin/components/ui/form'
|
|
12
12
|
import { Input } from '@admin/components/ui/input'
|
|
13
|
+
import { Spinner } from '@admin/components/ui/spinner'
|
|
13
14
|
import { useCopyToClipboard } from '@admin/hooks/use-copy-to-clipboard'
|
|
14
15
|
import { format } from 'date-fns'
|
|
15
|
-
import { Check, ClockPlus, Copy, Link2,
|
|
16
|
+
import { Check, ClockPlus, Copy, Link2, UserRoundCheck } from 'lucide-react'
|
|
16
17
|
import Link from 'next/link'
|
|
17
18
|
import { useForm } from 'react-hook-form'
|
|
18
19
|
import { toast } from 'sonner'
|
|
@@ -97,7 +98,7 @@ export function EditMediaDialogContent({
|
|
|
97
98
|
/>
|
|
98
99
|
<div className="relative z-10 flex min-h-0 flex-1 flex-col gap-3 overflow-hidden">
|
|
99
100
|
<DrawerHeader>
|
|
100
|
-
<DrawerTitle>
|
|
101
|
+
<DrawerTitle>Edit media</DrawerTitle>
|
|
101
102
|
</DrawerHeader>
|
|
102
103
|
<Form {...form}>
|
|
103
104
|
<form
|
|
@@ -196,9 +197,14 @@ export function EditMediaDialogContent({
|
|
|
196
197
|
</Form>
|
|
197
198
|
|
|
198
199
|
<DrawerFooter>
|
|
199
|
-
<Button
|
|
200
|
-
|
|
201
|
-
|
|
200
|
+
<Button
|
|
201
|
+
type="submit"
|
|
202
|
+
form={formId}
|
|
203
|
+
disabled={isPending || isDeleting || !isDirty}
|
|
204
|
+
aria-busy={isPending}
|
|
205
|
+
>
|
|
206
|
+
{isPending && <Spinner />}
|
|
207
|
+
{isPending ? 'Saving…' : 'Save'}
|
|
202
208
|
</Button>
|
|
203
209
|
<MediaDeleteDrawer
|
|
204
210
|
media={media}
|
package/dist/assets/adapters/next/templates/init/components/shared/media/media-delete-dialog.tsx
CHANGED
|
@@ -42,9 +42,10 @@ export function MediaDeleteDialog({ media, disabled = false, onDeleted }: MediaD
|
|
|
42
42
|
return
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
toast.error(result.error ?? 'Failed to delete media')
|
|
45
|
+
toast.error(result.error ?? 'Failed to delete this media')
|
|
46
46
|
} catch (error) {
|
|
47
|
-
|
|
47
|
+
console.error(error)
|
|
48
|
+
toast.error('Failed to delete this media. Try again.')
|
|
48
49
|
}
|
|
49
50
|
})
|
|
50
51
|
}
|
|
@@ -61,7 +62,7 @@ export function MediaDeleteDialog({ media, disabled = false, onDeleted }: MediaD
|
|
|
61
62
|
<AlertDialogHeader>
|
|
62
63
|
<AlertDialogTitle>Delete this media?</AlertDialogTitle>
|
|
63
64
|
<AlertDialogDescription>
|
|
64
|
-
This
|
|
65
|
+
This deletes {media.filename} from the media library. The stored file is not deleted.
|
|
65
66
|
</AlertDialogDescription>
|
|
66
67
|
</AlertDialogHeader>
|
|
67
68
|
<AlertDialogFooter>
|
|
@@ -69,12 +70,13 @@ export function MediaDeleteDialog({ media, disabled = false, onDeleted }: MediaD
|
|
|
69
70
|
<AlertDialogAction
|
|
70
71
|
variant="destructive"
|
|
71
72
|
disabled={isPending}
|
|
73
|
+
aria-busy={isPending}
|
|
72
74
|
onClick={(event) => {
|
|
73
75
|
event.preventDefault()
|
|
74
76
|
void handleDelete()
|
|
75
77
|
}}
|
|
76
78
|
>
|
|
77
|
-
{isPending ? 'Deleting
|
|
79
|
+
{isPending ? 'Deleting…' : 'Delete media'}
|
|
78
80
|
</AlertDialogAction>
|
|
79
81
|
</AlertDialogFooter>
|
|
80
82
|
</AlertDialogContent>
|
package/dist/assets/adapters/next/templates/init/components/shared/media/media-delete-drawer.tsx
CHANGED
|
@@ -4,7 +4,8 @@ import * as React from 'react'
|
|
|
4
4
|
import type { AdminMedia } from '@admin/types'
|
|
5
5
|
import { deleteMedia } from '@admin/actions/media'
|
|
6
6
|
import { Button } from '@admin/components/ui/button'
|
|
7
|
-
import {
|
|
7
|
+
import { Spinner } from '@admin/components/ui/spinner'
|
|
8
|
+
import { Trash2, TriangleAlert } from 'lucide-react'
|
|
8
9
|
import { toast } from 'sonner'
|
|
9
10
|
import { Drawer as DrawerPrimitive } from 'vaul'
|
|
10
11
|
|
|
@@ -72,9 +73,10 @@ export function MediaDeleteDrawer({
|
|
|
72
73
|
return
|
|
73
74
|
}
|
|
74
75
|
|
|
75
|
-
toast.error(result.error ?? 'Failed to delete media')
|
|
76
|
+
toast.error(result.error ?? 'Failed to delete this media')
|
|
76
77
|
} catch (error) {
|
|
77
|
-
|
|
78
|
+
console.error(error)
|
|
79
|
+
toast.error('Failed to delete this media. Try again.')
|
|
78
80
|
}
|
|
79
81
|
})
|
|
80
82
|
}
|
|
@@ -98,7 +100,7 @@ export function MediaDeleteDrawer({
|
|
|
98
100
|
<DrawerPrimitive.Trigger asChild>
|
|
99
101
|
<Button variant="destructive" disabled={disabled || isPending || !container}>
|
|
100
102
|
<Trash2 />
|
|
101
|
-
Delete
|
|
103
|
+
Delete media
|
|
102
104
|
</Button>
|
|
103
105
|
</DrawerPrimitive.Trigger>
|
|
104
106
|
<DrawerPrimitive.Portal>
|
|
@@ -117,10 +119,11 @@ export function MediaDeleteDrawer({
|
|
|
117
119
|
<TriangleAlert className="size-5" />
|
|
118
120
|
</div>
|
|
119
121
|
<DrawerPrimitive.Title className="font-heading text-base font-medium text-foreground">
|
|
120
|
-
Delete this
|
|
122
|
+
Delete this media?
|
|
121
123
|
</DrawerPrimitive.Title>
|
|
122
124
|
<DrawerPrimitive.Description className="mt-1 text-sm text-muted-foreground">
|
|
123
|
-
This
|
|
125
|
+
This deletes {media.filename} from the media library. The stored file is not
|
|
126
|
+
deleted.
|
|
124
127
|
</DrawerPrimitive.Description>
|
|
125
128
|
<div className="mt-5 grid grid-cols-2 gap-2">
|
|
126
129
|
<Button
|
|
@@ -134,9 +137,10 @@ export function MediaDeleteDrawer({
|
|
|
134
137
|
variant="destructive"
|
|
135
138
|
onClick={() => void handleDelete()}
|
|
136
139
|
disabled={isPending}
|
|
140
|
+
aria-busy={isPending}
|
|
137
141
|
>
|
|
138
|
-
{isPending && <
|
|
139
|
-
{isPending ? 'Deleting
|
|
142
|
+
{isPending && <Spinner />}
|
|
143
|
+
{isPending ? 'Deleting…' : 'Delete media'}
|
|
140
144
|
</Button>
|
|
141
145
|
</div>
|
|
142
146
|
</div>
|
package/dist/assets/adapters/next/templates/init/components/shared/media/media-empty-state.tsx
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import { PlaceholderCard } from '@admin/components/custom/placeholder-card'
|
|
4
4
|
import { Button } from '@admin/components/ui/button'
|
|
5
|
-
import {
|
|
5
|
+
import { Spinner } from '@admin/components/ui/spinner'
|
|
6
|
+
import { ImageIcon } from 'lucide-react'
|
|
6
7
|
|
|
7
8
|
interface MediaEmptyStateProps {
|
|
8
9
|
variant: 'no-results' | 'loading' | 'empty'
|
|
@@ -13,7 +14,7 @@ export function MediaEmptyState({ variant, onClearSearch }: MediaEmptyStateProps
|
|
|
13
14
|
if (variant === 'loading') {
|
|
14
15
|
return (
|
|
15
16
|
<div className="flex h-48 items-center justify-center">
|
|
16
|
-
<
|
|
17
|
+
<Spinner />
|
|
17
18
|
</div>
|
|
18
19
|
)
|
|
19
20
|
}
|
|
@@ -22,8 +23,8 @@ export function MediaEmptyState({ variant, onClearSearch }: MediaEmptyStateProps
|
|
|
22
23
|
return (
|
|
23
24
|
<PlaceholderCard
|
|
24
25
|
icon={<ImageIcon className="size-10 text-muted-foreground" strokeWidth={1} />}
|
|
25
|
-
title="No
|
|
26
|
-
description="Upload
|
|
26
|
+
title="No media yet"
|
|
27
|
+
description="Upload an image, video, or document to get started."
|
|
27
28
|
/>
|
|
28
29
|
)
|
|
29
30
|
}
|
|
@@ -31,7 +32,7 @@ export function MediaEmptyState({ variant, onClearSearch }: MediaEmptyStateProps
|
|
|
31
32
|
return (
|
|
32
33
|
<PlaceholderCard
|
|
33
34
|
icon={<ImageIcon className="size-10 text-foreground" strokeWidth={1} />}
|
|
34
|
-
title="No
|
|
35
|
+
title="No media matches your search"
|
|
35
36
|
description="Try adjusting your search or filters."
|
|
36
37
|
action={
|
|
37
38
|
onClearSearch ? (
|
package/dist/assets/adapters/next/templates/init/components/shared/media/media-grid-pagination.tsx
CHANGED
|
@@ -38,7 +38,7 @@ export function MediaGridPagination({
|
|
|
38
38
|
const pageSizeValue = String(pageSize)
|
|
39
39
|
const isFirstPage = page <= 1
|
|
40
40
|
const isLastPage = page >= pageCount
|
|
41
|
-
const perPage = '
|
|
41
|
+
const perPage = 'Rows per page'
|
|
42
42
|
|
|
43
43
|
const handlePageSizeChange = React.useCallback(
|
|
44
44
|
(value: string) => {
|
package/dist/assets/adapters/next/templates/init/components/shared/media/media-url-importer.tsx
CHANGED
|
@@ -9,10 +9,11 @@ import {
|
|
|
9
9
|
InputGroupButton,
|
|
10
10
|
InputGroupInput
|
|
11
11
|
} from '@admin/components/ui/input-group'
|
|
12
|
+
import { Spinner } from '@admin/components/ui/spinner'
|
|
12
13
|
import { getMediaUrlHelpText, getMediaUrlPlaceholder } from '@admin/utils/media/media'
|
|
13
14
|
import { cn } from '@admin/utils/shared/cn'
|
|
14
15
|
import { isValidUrl } from '@admin/utils/validation/validation'
|
|
15
|
-
import {
|
|
16
|
+
import { Upload } from 'lucide-react'
|
|
16
17
|
|
|
17
18
|
interface MediaUrlImporterProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
18
19
|
onImportFromUrl: (url: string) => Promise<UploadFileResult>
|
|
@@ -119,10 +120,11 @@ export function MediaUrlImporter({ onImportFromUrl, accept, className }: MediaUr
|
|
|
119
120
|
variant="outline"
|
|
120
121
|
onClick={() => void handleImport()}
|
|
121
122
|
disabled={isImporting}
|
|
123
|
+
aria-busy={isImporting}
|
|
122
124
|
className="h-full px-5"
|
|
123
125
|
>
|
|
124
|
-
{isImporting ? <
|
|
125
|
-
Import
|
|
126
|
+
{isImporting ? <Spinner /> : <Upload />}
|
|
127
|
+
{isImporting ? 'Importing…' : 'Import'}
|
|
126
128
|
</InputGroupButton>
|
|
127
129
|
</InputGroupAddon>
|
|
128
130
|
</InputGroup>
|
|
@@ -74,6 +74,8 @@ export function SearchInput({
|
|
|
74
74
|
size="icon-sm"
|
|
75
75
|
className="absolute top-1/2 right-2 size-7 -translate-y-1/2 text-foreground"
|
|
76
76
|
disabled={isPending}
|
|
77
|
+
aria-busy={isPending}
|
|
78
|
+
aria-label="Submit search"
|
|
77
79
|
>
|
|
78
80
|
<CornerDownLeft className="size-4" />
|
|
79
81
|
</Button>
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
DrawerTitle
|
|
15
15
|
} from '@admin/components/ui/drawer'
|
|
16
16
|
import { ScrollArea } from '@admin/components/ui/scroll-area'
|
|
17
|
+
import { Spinner } from '@admin/components/ui/spinner'
|
|
17
18
|
import {
|
|
18
19
|
closestCenter,
|
|
19
20
|
DndContext,
|
|
@@ -29,7 +30,6 @@ import {
|
|
|
29
30
|
verticalListSortingStrategy
|
|
30
31
|
} from '@dnd-kit/sortable'
|
|
31
32
|
import { useInfiniteQuery, useQueryClient } from '@tanstack/react-query'
|
|
32
|
-
import { LoaderCircle } from 'lucide-react'
|
|
33
33
|
import { toast } from 'sonner'
|
|
34
34
|
import { DragOverlayItem } from './sort-order-drag-overlay-item'
|
|
35
35
|
import { SortableItem } from './sort-order-item'
|
|
@@ -171,7 +171,7 @@ export function SortOrderDialog({
|
|
|
171
171
|
toast.error(result.error || 'Failed to save sort order')
|
|
172
172
|
}
|
|
173
173
|
} catch {
|
|
174
|
-
toast.error('
|
|
174
|
+
toast.error('Failed to save the new order. Try again.')
|
|
175
175
|
}
|
|
176
176
|
})
|
|
177
177
|
}, [hasChanges, isSaving, orderedItems, onSave, onOpenChange])
|
|
@@ -200,7 +200,7 @@ export function SortOrderDialog({
|
|
|
200
200
|
</DrawerHeader>
|
|
201
201
|
{isLoading ? (
|
|
202
202
|
<div className="flex min-h-0 flex-1 items-center justify-center">
|
|
203
|
-
<
|
|
203
|
+
<Spinner />
|
|
204
204
|
</div>
|
|
205
205
|
) : (
|
|
206
206
|
<ScrollArea className="min-h-0 flex-1">
|
|
@@ -215,16 +215,21 @@ export function SortOrderDialog({
|
|
|
215
215
|
</SortableContext>
|
|
216
216
|
{hasNextPage && (
|
|
217
217
|
<div ref={sentinelRef} className="flex items-center justify-center py-3">
|
|
218
|
-
{isFetchingNextPage && <
|
|
218
|
+
{isFetchingNextPage && <Spinner />}
|
|
219
219
|
</div>
|
|
220
220
|
)}
|
|
221
221
|
</div>
|
|
222
222
|
</ScrollArea>
|
|
223
223
|
)}
|
|
224
224
|
<DrawerFooter>
|
|
225
|
-
<Button
|
|
226
|
-
{
|
|
227
|
-
{
|
|
225
|
+
<Button
|
|
226
|
+
onClick={handleSave}
|
|
227
|
+
disabled={!hasChanges || isSaving}
|
|
228
|
+
aria-busy={isSaving}
|
|
229
|
+
size="lg"
|
|
230
|
+
>
|
|
231
|
+
{isSaving && <Spinner />}
|
|
232
|
+
{isSaving ? 'Saving…' : 'Save'}
|
|
228
233
|
</Button>
|
|
229
234
|
<DrawerClose asChild>
|
|
230
235
|
<Button variant="outline">Close</Button>
|
|
@@ -19,7 +19,8 @@ export function useEntityVersions({ entityId, getVersionsAction }: UseEntityVers
|
|
|
19
19
|
const nextVersions = await getVersionsAction(entityId)
|
|
20
20
|
setVersions(nextVersions)
|
|
21
21
|
} catch (error) {
|
|
22
|
-
|
|
22
|
+
console.error(error)
|
|
23
|
+
toast.error('Failed to load versions. Try again.')
|
|
23
24
|
}
|
|
24
25
|
})
|
|
25
26
|
}, [entityId, getVersionsAction])
|
|
@@ -6,6 +6,7 @@ import { requireRole, UserRole } from '@admin/auth/middleware'
|
|
|
6
6
|
import db from '@admin/db'
|
|
7
7
|
import { auditLog } from '@admin/db/schema'
|
|
8
8
|
import { runAdminEventHooks } from '@admin/lib/lifecycle-hooks'
|
|
9
|
+
import { toActionError } from '@admin/utils/error/action-error'
|
|
9
10
|
import { lt } from 'drizzle-orm'
|
|
10
11
|
import { getAuditRequestMetadata } from './request-metadata'
|
|
11
12
|
|
|
@@ -57,7 +58,7 @@ export async function cleanupAuditLogs(): Promise<CleanupAuditLogsResult> {
|
|
|
57
58
|
} catch (error) {
|
|
58
59
|
return {
|
|
59
60
|
success: false,
|
|
60
|
-
error: error
|
|
61
|
+
error: toActionError(error, 'Failed to clean up audit logs')
|
|
61
62
|
}
|
|
62
63
|
}
|
|
63
64
|
}
|
|
@@ -4,6 +4,7 @@ import type { FormSettingsData, FormSettingsResult, UpsertFormSettingsInput } fr
|
|
|
4
4
|
import { requireRole, UserRole } from '@admin/auth/middleware'
|
|
5
5
|
import db from '@admin/db'
|
|
6
6
|
import { formSettings } from '@admin/db/schema'
|
|
7
|
+
import { toActionError } from '@admin/utils/error/action-error'
|
|
7
8
|
import { updateTag } from 'next/cache'
|
|
8
9
|
import { formSettingsCacheTags } from './types'
|
|
9
10
|
|
|
@@ -35,7 +36,7 @@ export async function upsertFormSettings(
|
|
|
35
36
|
console.error(`Error upserting form settings for ${formName}:`, error)
|
|
36
37
|
return {
|
|
37
38
|
success: false,
|
|
38
|
-
error: error
|
|
39
|
+
error: toActionError(error, 'Failed to save form settings')
|
|
39
40
|
}
|
|
40
41
|
}
|
|
41
42
|
}
|
|
@@ -7,6 +7,7 @@ import db from '@admin/db'
|
|
|
7
7
|
import { adminMedia } from '@admin/db/schema'
|
|
8
8
|
import { runAdminEventHooks } from '@admin/lib/lifecycle-hooks'
|
|
9
9
|
import { getTouchedFieldNames } from '@admin/utils/audit/audit'
|
|
10
|
+
import { toActionError } from '@admin/utils/error/action-error'
|
|
10
11
|
import { updateTag } from 'next/cache'
|
|
11
12
|
import { getMediaById } from './get-media-by-id'
|
|
12
13
|
import { mediaCacheTags } from './types'
|
|
@@ -53,7 +54,7 @@ export async function createMedia(data: CreateMediaInput): Promise<CreateMediaRe
|
|
|
53
54
|
} catch (error) {
|
|
54
55
|
return {
|
|
55
56
|
success: false,
|
|
56
|
-
error: error
|
|
57
|
+
error: toActionError(error, 'Failed to create media record')
|
|
57
58
|
}
|
|
58
59
|
}
|
|
59
60
|
}
|
|
@@ -6,6 +6,7 @@ import { requireRole, UserRole } from '@admin/auth/middleware'
|
|
|
6
6
|
import db from '@admin/db'
|
|
7
7
|
import { adminMedia } from '@admin/db/schema'
|
|
8
8
|
import { runAdminEventHooks } from '@admin/lib/lifecycle-hooks'
|
|
9
|
+
import { toActionError } from '@admin/utils/error/action-error'
|
|
9
10
|
import { inArray } from 'drizzle-orm'
|
|
10
11
|
import { updateTag } from 'next/cache'
|
|
11
12
|
import { mediaCacheTags } from './types'
|
|
@@ -46,7 +47,7 @@ export async function deleteMediaBulk(ids: string[]): Promise<DeleteMediaBulkRes
|
|
|
46
47
|
} catch (error) {
|
|
47
48
|
return {
|
|
48
49
|
success: false,
|
|
49
|
-
error: error
|
|
50
|
+
error: toActionError(error, 'Failed to delete media')
|
|
50
51
|
}
|
|
51
52
|
}
|
|
52
53
|
}
|
|
@@ -6,6 +6,7 @@ import { requireRole, UserRole } from '@admin/auth/middleware'
|
|
|
6
6
|
import db from '@admin/db'
|
|
7
7
|
import { adminMedia } from '@admin/db/schema'
|
|
8
8
|
import { runAdminEventHooks } from '@admin/lib/lifecycle-hooks'
|
|
9
|
+
import { toActionError } from '@admin/utils/error/action-error'
|
|
9
10
|
import { eq } from 'drizzle-orm'
|
|
10
11
|
import { updateTag } from 'next/cache'
|
|
11
12
|
import { mediaCacheTags } from './types'
|
|
@@ -37,7 +38,7 @@ export async function deleteMedia(id: string): Promise<DeleteMediaResult> {
|
|
|
37
38
|
} catch (error) {
|
|
38
39
|
return {
|
|
39
40
|
success: false,
|
|
40
|
-
error: error
|
|
41
|
+
error: toActionError(error, 'Failed to delete media')
|
|
41
42
|
}
|
|
42
43
|
}
|
|
43
44
|
}
|
|
@@ -7,6 +7,7 @@ import db from '@admin/db'
|
|
|
7
7
|
import { adminMedia } from '@admin/db/schema'
|
|
8
8
|
import { runAdminEventHooks } from '@admin/lib/lifecycle-hooks'
|
|
9
9
|
import { getChangedFieldNames } from '@admin/utils/audit/audit'
|
|
10
|
+
import { toActionError } from '@admin/utils/error/action-error'
|
|
10
11
|
import { eq } from 'drizzle-orm'
|
|
11
12
|
import { updateTag } from 'next/cache'
|
|
12
13
|
import { mediaCacheTags } from './types'
|
|
@@ -51,7 +52,7 @@ export async function updateMedia(
|
|
|
51
52
|
} catch (error) {
|
|
52
53
|
return {
|
|
53
54
|
success: false,
|
|
54
|
-
error: error
|
|
55
|
+
error: toActionError(error, 'Failed to update media')
|
|
55
56
|
}
|
|
56
57
|
}
|
|
57
58
|
}
|
|
@@ -5,6 +5,7 @@ import { getAuditRequestMetadata } from '@admin/actions/audit/request-metadata'
|
|
|
5
5
|
import { auth } from '@admin/auth'
|
|
6
6
|
import { getSession } from '@admin/auth/middleware'
|
|
7
7
|
import { runAdminEventHooks } from '@admin/lib/lifecycle-hooks'
|
|
8
|
+
import { toActionError } from '@admin/utils/error/action-error'
|
|
8
9
|
import { headers } from 'next/headers'
|
|
9
10
|
import { invalidateUsersCache } from './invalidate-users-cache'
|
|
10
11
|
|
|
@@ -53,7 +54,7 @@ export async function changeOwnPassword(
|
|
|
53
54
|
} catch (error) {
|
|
54
55
|
return {
|
|
55
56
|
success: false,
|
|
56
|
-
error: error
|
|
57
|
+
error: toActionError(error, 'Failed to change password')
|
|
57
58
|
}
|
|
58
59
|
}
|
|
59
60
|
}
|
|
@@ -7,6 +7,7 @@ import { getSession } from '@admin/auth/middleware'
|
|
|
7
7
|
import db from '@admin/db'
|
|
8
8
|
import { user } from '@admin/db/schema'
|
|
9
9
|
import { runAdminEventHooks } from '@admin/lib/lifecycle-hooks'
|
|
10
|
+
import { toActionError } from '@admin/utils/error/action-error'
|
|
10
11
|
import { eq } from 'drizzle-orm'
|
|
11
12
|
import { headers } from 'next/headers'
|
|
12
13
|
import { invalidateUsersCache } from './invalidate-users-cache'
|
|
@@ -68,7 +69,7 @@ export async function updateEmail(
|
|
|
68
69
|
} catch (error) {
|
|
69
70
|
return {
|
|
70
71
|
success: false,
|
|
71
|
-
error: error
|
|
72
|
+
error: toActionError(error, 'Failed to update email')
|
|
72
73
|
}
|
|
73
74
|
}
|
|
74
75
|
}
|
package/dist/assets/adapters/next/templates/init/lib/actions/upload/upload-media-from-url.ts
CHANGED
|
@@ -33,7 +33,7 @@ export async function uploadMediaFromUrl(
|
|
|
33
33
|
if (!response.ok) {
|
|
34
34
|
return {
|
|
35
35
|
success: false,
|
|
36
|
-
error:
|
|
36
|
+
error: "Couldn't reach that URL. Check the link and try again."
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
|
|
@@ -43,7 +43,7 @@ export async function uploadMediaFromUrl(
|
|
|
43
43
|
.toLowerCase()
|
|
44
44
|
|
|
45
45
|
if (!contentType.startsWith('image/') && !contentType.startsWith('video/')) {
|
|
46
|
-
return { success: false, error:
|
|
46
|
+
return { success: false, error: 'That link is not an image or video.' }
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
const filename = getRemoteFilename(url, contentType)
|