betterstart-cli 0.0.12 → 0.0.13

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 (117) hide show
  1. package/dist/assets/adapters/next/plugins/blog/plugin.ts +1 -1
  2. package/dist/assets/adapters/next/plugins/blog/schemas/menus.json +115 -0
  3. package/dist/assets/adapters/next/plugins/blog/schemas/posts.json +2 -7
  4. package/dist/assets/adapters/next/templates/init/components/layouts/admin-sidebar.tsx +2 -2
  5. package/dist/assets/adapters/next/templates/init/components/shared/dev-mode/lifecycle-hooks-tab.tsx +2 -2
  6. package/dist/assets/adapters/next/templates/init/components/shared/dev-mode/snippets-tab.tsx +1 -1
  7. package/dist/assets/adapters/next/templates/init/components/shared/dev-mode-integrate.tsx +1 -1
  8. package/dist/assets/adapters/next/templates/init/components/shared/entity-versions/entity-versions-drawer.tsx +1 -1
  9. package/dist/assets/adapters/next/templates/init/components/shared/media/edit-media-dialog-content.tsx +2 -2
  10. package/dist/assets/adapters/next/templates/init/components/shared/media/media-url-importer.tsx +1 -1
  11. package/dist/assets/adapters/next/templates/init/components/shared/page-header.tsx +20 -17
  12. package/dist/assets/adapters/next/templates/init/data/navigation.ts +2 -1
  13. package/dist/assets/adapters/next/templates/init/hooks/content-editor/use-content-editor-slash-menu.ts +28 -2
  14. package/dist/assets/adapters/next/templates/init/hooks/content-editor/use-content-editor.ts +22 -1
  15. package/dist/assets/adapters/next/templates/init/hooks/use-audit-log.ts +23 -0
  16. package/dist/assets/adapters/next/templates/init/lib/actions/audit/cleanup-audit-logs.ts +63 -0
  17. package/dist/assets/adapters/next/templates/init/lib/actions/audit/get-audit-logs.ts +97 -0
  18. package/dist/assets/adapters/next/templates/init/lib/actions/audit/index.ts +17 -0
  19. package/dist/assets/adapters/next/templates/init/lib/actions/audit/record-audit-event.ts +37 -0
  20. package/dist/assets/adapters/next/templates/init/lib/actions/audit/register.ts +13 -0
  21. package/dist/assets/adapters/next/templates/init/lib/actions/audit/request-metadata.ts +37 -0
  22. package/dist/assets/adapters/next/templates/init/lib/actions/audit/types.ts +72 -0
  23. package/dist/assets/adapters/next/templates/init/lib/actions/forms/upsert-form-settings.ts +3 -0
  24. package/dist/assets/adapters/next/templates/init/lib/actions/media/create-media.ts +19 -0
  25. package/dist/assets/adapters/next/templates/init/lib/actions/media/delete-media-bulk.ts +23 -0
  26. package/dist/assets/adapters/next/templates/init/lib/actions/media/delete-media.ts +21 -0
  27. package/dist/assets/adapters/next/templates/init/lib/actions/media/update-media.ts +23 -1
  28. package/dist/assets/adapters/next/templates/init/lib/actions/profile/update-email.ts +17 -0
  29. package/dist/assets/adapters/next/templates/init/lib/actions/upload/upload-files.ts +3 -0
  30. package/dist/assets/adapters/next/templates/init/lib/actions/upload/upload-media-from-url.ts +3 -0
  31. package/dist/assets/adapters/next/templates/init/lib/actions/users/create-user.ts +19 -1
  32. package/dist/assets/adapters/next/templates/init/lib/actions/users/delete-user.ts +23 -1
  33. package/dist/assets/adapters/next/templates/init/lib/actions/users/update-user-role.ts +23 -1
  34. package/dist/assets/adapters/next/templates/init/lib/db/core/schema.ts +49 -0
  35. package/dist/assets/adapters/next/templates/init/lib/lifecycle-hooks/index.ts +6 -1
  36. package/dist/assets/adapters/next/templates/init/lib/lifecycle-hooks/register.ts +3 -0
  37. package/dist/assets/adapters/next/templates/init/lib/lifecycle-hooks/registry.ts +36 -1
  38. package/dist/assets/adapters/next/templates/init/lib/lifecycle-hooks/runner.ts +116 -12
  39. package/dist/assets/adapters/next/templates/init/lib/lifecycle-hooks/types.ts +33 -0
  40. package/dist/assets/adapters/next/templates/init/pages/audit-log/audit-log-detail-drawer.tsx +96 -0
  41. package/dist/assets/adapters/next/templates/init/pages/audit-log/audit-log-detail-row.tsx +17 -0
  42. package/dist/assets/adapters/next/templates/init/pages/audit-log/audit-log-filters.tsx +152 -0
  43. package/dist/assets/adapters/next/templates/init/pages/audit-log/audit-log-page-content.tsx +279 -0
  44. package/dist/assets/adapters/next/templates/init/pages/audit-log/audit-log-page-skeleton.tsx +19 -0
  45. package/dist/assets/adapters/next/templates/init/pages/audit-log/audit-log-page.tsx +14 -0
  46. package/dist/assets/adapters/next/templates/init/pages/dashboard-page.tsx +1 -1
  47. package/dist/assets/adapters/next/templates/init/pages/forgot-password-page-skeleton.tsx +1 -1
  48. package/dist/assets/adapters/next/templates/init/pages/login-page-skeleton.tsx +1 -1
  49. package/dist/assets/adapters/next/templates/init/pages/reset-password-page-skeleton.tsx +1 -1
  50. package/dist/assets/adapters/next/templates/init/utils/audit/audit.ts +54 -0
  51. package/dist/assets/adapters/next/templates/init/utils/editor/tiptap.ts +0 -24
  52. package/dist/assets/shared-assets/react-admin/custom/content-editor/horizontal-rule-button.tsx +1 -1
  53. package/dist/assets/shared-assets/react-admin/custom/content-editor/index.tsx +6 -0
  54. package/dist/assets/shared-assets/react-admin/custom/content-editor/main-toolbar-content.tsx +1 -1
  55. package/dist/assets/shared-assets/react-admin/custom/content-editor/math-bubble-menu.tsx +1 -1
  56. package/dist/assets/shared-assets/react-admin/custom/content-editor/math-button.tsx +1 -1
  57. package/dist/assets/shared-assets/react-admin/custom/content-editor/math-editor-controls.tsx +27 -40
  58. package/dist/assets/shared-assets/react-admin/custom/content-editor/math-popover-button.tsx +0 -1
  59. package/dist/assets/shared-assets/react-admin/custom/content-editor/mobile-toolbar-content.tsx +4 -4
  60. package/dist/assets/shared-assets/react-admin/custom/content-editor/mode-toggle-button.tsx +1 -1
  61. package/dist/assets/shared-assets/react-admin/custom/content-editor/selection-bubble-menu.tsx +45 -8
  62. package/dist/assets/shared-assets/react-admin/custom/content-editor/slash-command-menu.tsx +153 -27
  63. package/dist/assets/shared-assets/react-admin/custom/content-editor/source-mode-dropdown-button.tsx +1 -1
  64. package/dist/assets/shared-assets/react-admin/custom/content-editor/source-mode.tsx +9 -9
  65. package/dist/assets/shared-assets/react-admin/custom/content-editor/table-bubble-menu.tsx +1 -1
  66. package/dist/assets/shared-assets/react-admin/custom/content-editor/table-button.tsx +9 -4
  67. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/blockquote-button/blockquote-button.tsx +1 -1
  68. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/code-block-button/code-block-button.tsx +1 -1
  69. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/color-highlight-popover/color-highlight-popover-button.tsx +1 -1
  70. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/color-highlight-popover/color-highlight-popover-content.tsx +1 -1
  71. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/color-highlight-popover/color-highlight-popover.tsx +11 -2
  72. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/color-highlight-popover/source-color-highlight-popover.tsx +1 -1
  73. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/heading-dropdown-menu/heading-dropdown-menu-item.tsx +1 -1
  74. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/heading-dropdown-menu/heading-dropdown-menu.tsx +9 -4
  75. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/link-popover/link-button.tsx +1 -1
  76. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/link-popover/link-control-popover.tsx +2 -4
  77. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/link-popover/link-main.tsx +19 -45
  78. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/link-popover/link-popover.tsx +1 -1
  79. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/list-dropdown-menu/list-dropdown-menu-item.tsx +1 -1
  80. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/list-dropdown-menu/list-dropdown-menu.tsx +9 -4
  81. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/mark-button/mark-button.tsx +1 -1
  82. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/undo-redo-button/undo-redo-button.tsx +1 -1
  83. package/dist/assets/shared-assets/react-admin/custom/date-range-picker.tsx +3 -3
  84. package/dist/assets/shared-assets/react-admin/custom/gallery-field.tsx +2 -2
  85. package/dist/assets/shared-assets/react-admin/custom/media-gallery-field.tsx +2 -2
  86. package/dist/assets/shared-assets/react-admin/custom/placeholder-card.tsx +1 -1
  87. package/dist/assets/shared-assets/react-admin/custom/placeholder.tsx +1 -1
  88. package/dist/assets/shared-assets/react-admin/custom/sortable-gallery-item.tsx +1 -1
  89. package/dist/assets/shared-assets/react-admin/schema.json +0 -23
  90. package/dist/assets/shared-assets/react-admin/ui/accordion.tsx +1 -1
  91. package/dist/assets/shared-assets/react-admin/ui/alert.tsx +1 -1
  92. package/dist/assets/shared-assets/react-admin/ui/button.tsx +18 -18
  93. package/dist/assets/shared-assets/react-admin/ui/card.tsx +3 -3
  94. package/dist/assets/shared-assets/react-admin/ui/chart.tsx +1 -1
  95. package/dist/assets/shared-assets/react-admin/ui/combobox.tsx +29 -5
  96. package/dist/assets/shared-assets/react-admin/ui/command.tsx +1 -1
  97. package/dist/assets/shared-assets/react-admin/ui/context-menu.tsx +6 -6
  98. package/dist/assets/shared-assets/react-admin/ui/drawer.tsx +7 -3
  99. package/dist/assets/shared-assets/react-admin/ui/dropdown-menu.tsx +32 -17
  100. package/dist/assets/shared-assets/react-admin/ui/empty.tsx +2 -2
  101. package/dist/assets/shared-assets/react-admin/ui/field.tsx +1 -1
  102. package/dist/assets/shared-assets/react-admin/ui/hover-card.tsx +1 -1
  103. package/dist/assets/shared-assets/react-admin/ui/input-group.tsx +1 -1
  104. package/dist/assets/shared-assets/react-admin/ui/item.tsx +2 -2
  105. package/dist/assets/shared-assets/react-admin/ui/menubar.tsx +7 -7
  106. package/dist/assets/shared-assets/react-admin/ui/navigation-menu.tsx +4 -4
  107. package/dist/assets/shared-assets/react-admin/ui/popover.tsx +1 -1
  108. package/dist/assets/shared-assets/react-admin/ui/resizable.tsx +1 -1
  109. package/dist/assets/shared-assets/react-admin/ui/select.tsx +2 -2
  110. package/dist/assets/shared-assets/react-admin/ui/sidebar.tsx +3 -3
  111. package/dist/assets/shared-assets/react-admin/ui/skeleton.tsx +1 -1
  112. package/dist/assets/shared-assets/react-admin/ui/tabs.tsx +2 -2
  113. package/dist/assets/shared-assets/react-admin/ui/textarea.tsx +1 -1
  114. package/dist/assets/shared-assets/react-admin/ui/tooltip.tsx +1 -1
  115. package/dist/cli.js +2769 -802
  116. package/dist/cli.js.map +1 -1
  117. package/package.json +1 -1
@@ -0,0 +1,279 @@
1
+ 'use client'
2
+
3
+ import type { AuditLogEntry, AuditLogQuery } from '@admin/actions/audit'
4
+ import { PageHeader } from '@admin/components/shared/page-header'
5
+ import { Badge } from '@admin/components/ui/badge'
6
+ import { Button } from '@admin/components/ui/button'
7
+ import {
8
+ Table,
9
+ TableBody,
10
+ TableCell,
11
+ TableHead,
12
+ TableHeader,
13
+ TableRow
14
+ } from '@admin/components/ui/table'
15
+ import { useAuditLogs, useCleanupAuditLogs } from '@admin/hooks/use-audit-log'
16
+ import { LoaderCircle, Trash2 } from 'lucide-react'
17
+ import { parseAsInteger, parseAsString, useQueryStates } from 'nuqs'
18
+ import * as React from 'react'
19
+ import { toast } from 'sonner'
20
+ import { AuditLogDetailDrawer } from './audit-log-detail-drawer'
21
+ import { AuditLogFilters } from './audit-log-filters'
22
+
23
+ const DEFAULT_PAGE_SIZE = 25
24
+
25
+ const auditLogParsers = {
26
+ q: parseAsString.withDefault(''),
27
+ category: parseAsString.withDefault(''),
28
+ action: parseAsString.withDefault(''),
29
+ targetType: parseAsString.withDefault(''),
30
+ actor: parseAsString.withDefault(''),
31
+ createdAtFrom: parseAsString.withDefault(''),
32
+ createdAtTo: parseAsString.withDefault(''),
33
+ page: parseAsInteger.withDefault(1)
34
+ }
35
+
36
+ function formatDateTime(value: string): string {
37
+ const date = new Date(value)
38
+ if (Number.isNaN(date.getTime())) return value
39
+
40
+ return new Intl.DateTimeFormat('en-US', {
41
+ month: 'short',
42
+ day: '2-digit',
43
+ year: 'numeric',
44
+ hour: 'numeric',
45
+ minute: '2-digit'
46
+ }).format(date)
47
+ }
48
+
49
+ function formatEventLabel(auditLog: AuditLogEntry): string {
50
+ return `${auditLog.category}.${auditLog.action}`
51
+ }
52
+
53
+ export function AuditLogPageContent() {
54
+ const [selectedAuditLog, setSelectedAuditLog] = React.useState<AuditLogEntry | null>(null)
55
+ const [filters, setFilters] = useQueryStates(auditLogParsers)
56
+ const safePage = Math.max(1, filters.page)
57
+ const query = React.useMemo<AuditLogQuery>(
58
+ () => ({
59
+ search: filters.q || undefined,
60
+ category: filters.category ? (filters.category as AuditLogQuery['category']) : undefined,
61
+ action: filters.action ? (filters.action as AuditLogQuery['action']) : undefined,
62
+ targetType: filters.targetType || undefined,
63
+ actor: filters.actor || undefined,
64
+ createdAtFrom: filters.createdAtFrom || undefined,
65
+ createdAtTo: filters.createdAtTo || undefined,
66
+ page: safePage,
67
+ pageSize: DEFAULT_PAGE_SIZE
68
+ }),
69
+ [filters, safePage]
70
+ )
71
+ const auditLogsQuery = useAuditLogs(query)
72
+ const cleanupMutation = useCleanupAuditLogs()
73
+ const auditLogs = auditLogsQuery.data?.auditLogs ?? []
74
+ const total = auditLogsQuery.data?.total ?? 0
75
+ const totalPages = auditLogsQuery.data?.totalPages ?? 1
76
+ const activeFilterCount = [
77
+ filters.q,
78
+ filters.category,
79
+ filters.action,
80
+ filters.targetType,
81
+ filters.actor,
82
+ filters.createdAtFrom || filters.createdAtTo
83
+ ].filter(Boolean).length
84
+
85
+ function updateFilter(updates: Partial<typeof filters>) {
86
+ void setFilters({ ...updates, page: 1 })
87
+ }
88
+
89
+ function clearFilters() {
90
+ void setFilters({
91
+ q: null,
92
+ category: null,
93
+ action: null,
94
+ targetType: null,
95
+ actor: null,
96
+ createdAtFrom: null,
97
+ createdAtTo: null,
98
+ page: 1
99
+ })
100
+ }
101
+
102
+ function runCleanup() {
103
+ cleanupMutation.mutate(undefined, {
104
+ onSuccess: (result) => {
105
+ if (!result.success) {
106
+ toast.error(result.error ?? 'Failed to clean up audit logs')
107
+ return
108
+ }
109
+ if (result.skipped) {
110
+ toast.info('Audit retention is not enabled')
111
+ return
112
+ }
113
+ toast.success(`Deleted ${result.deletedCount ?? 0} audit log rows`)
114
+ },
115
+ onError: () => {
116
+ toast.error('Failed to clean up audit logs')
117
+ }
118
+ })
119
+ }
120
+
121
+ return (
122
+ <>
123
+ <PageHeader
124
+ title="Audit Log"
125
+ actions={
126
+ <Button
127
+ type="button"
128
+ variant="outline"
129
+ size="sm"
130
+ disabled={cleanupMutation.isPending}
131
+ onClick={runCleanup}
132
+ >
133
+ {cleanupMutation.isPending ? <LoaderCircle className="animate-spin" /> : <Trash2 />}
134
+ Cleanup
135
+ </Button>
136
+ }
137
+ />
138
+ <main className="flex min-h-0 flex-1 flex-col">
139
+ <AuditLogFilters
140
+ search={filters.q}
141
+ category={filters.category}
142
+ action={filters.action}
143
+ targetType={filters.targetType}
144
+ actor={filters.actor}
145
+ createdAtFrom={filters.createdAtFrom}
146
+ createdAtTo={filters.createdAtTo}
147
+ activeFilterCount={activeFilterCount}
148
+ onSearchChange={(value) => updateFilter({ q: value })}
149
+ onCategoryChange={(value) => updateFilter({ category: value })}
150
+ onActionChange={(value) => updateFilter({ action: value })}
151
+ onTargetTypeChange={(value) => updateFilter({ targetType: value })}
152
+ onActorChange={(value) => updateFilter({ actor: value })}
153
+ onCreatedAtFromChange={(value) => updateFilter({ createdAtFrom: value })}
154
+ onCreatedAtToChange={(value) => updateFilter({ createdAtTo: value })}
155
+ onClear={clearFilters}
156
+ />
157
+ <div className="min-h-0 flex-1 overflow-auto px-4 py-3">
158
+ <div className="overflow-hidden rounded-lg border border-border bg-card">
159
+ <Table>
160
+ <TableHeader>
161
+ <TableRow>
162
+ <TableHead className="w-[180px]">Time</TableHead>
163
+ <TableHead className="w-[160px]">Event</TableHead>
164
+ <TableHead>Target</TableHead>
165
+ <TableHead className="w-[220px]">Actor</TableHead>
166
+ <TableHead className="w-[180px]">Source</TableHead>
167
+ <TableHead className="w-[180px]">Fields</TableHead>
168
+ </TableRow>
169
+ </TableHeader>
170
+ <TableBody>
171
+ {auditLogsQuery.isPending ? (
172
+ <TableRow>
173
+ <TableCell colSpan={6} className="h-40 text-center text-muted-foreground">
174
+ Loading audit logs...
175
+ </TableCell>
176
+ </TableRow>
177
+ ) : null}
178
+ {!auditLogsQuery.isPending && auditLogs.length === 0 ? (
179
+ <TableRow>
180
+ <TableCell colSpan={6} className="h-40 text-center text-muted-foreground">
181
+ No audit logs found.
182
+ </TableCell>
183
+ </TableRow>
184
+ ) : null}
185
+ {auditLogs.map((auditLog) => (
186
+ <TableRow
187
+ key={auditLog.id}
188
+ className="cursor-pointer"
189
+ onClick={() => setSelectedAuditLog(auditLog)}
190
+ >
191
+ <TableCell className="whitespace-nowrap text-muted-foreground">
192
+ {formatDateTime(auditLog.createdAt)}
193
+ </TableCell>
194
+ <TableCell>
195
+ <Badge variant="secondary">{formatEventLabel(auditLog)}</Badge>
196
+ </TableCell>
197
+ <TableCell className="min-w-[220px]">
198
+ <div className="flex min-w-0 flex-col">
199
+ <span className="truncate font-medium">
200
+ {auditLog.targetLabel ?? auditLog.targetId ?? 'Unknown'}
201
+ </span>
202
+ <span className="truncate text-xs text-muted-foreground">
203
+ {auditLog.targetType}
204
+ </span>
205
+ </div>
206
+ </TableCell>
207
+ <TableCell className="min-w-[180px]">
208
+ <div className="flex min-w-0 flex-col">
209
+ <span className="truncate">
210
+ {auditLog.actorEmail ?? auditLog.actorName ?? 'Unknown'}
211
+ </span>
212
+ <span className="truncate text-xs text-muted-foreground">
213
+ {auditLog.actorRole ?? 'No role'}
214
+ </span>
215
+ </div>
216
+ </TableCell>
217
+ <TableCell className="min-w-[160px] text-muted-foreground">
218
+ <div className="truncate">{auditLog.sourceType}</div>
219
+ <div className="truncate text-xs">{auditLog.ipAddress ?? 'No IP'}</div>
220
+ </TableCell>
221
+ <TableCell className="min-w-[160px]">
222
+ {auditLog.changedFields.length > 0 ? (
223
+ <div className="flex flex-wrap gap-1">
224
+ {auditLog.changedFields.slice(0, 3).map((field) => (
225
+ <Badge key={field} variant="outline">
226
+ {field}
227
+ </Badge>
228
+ ))}
229
+ {auditLog.changedFields.length > 3 ? (
230
+ <Badge variant="outline">+{auditLog.changedFields.length - 3}</Badge>
231
+ ) : null}
232
+ </div>
233
+ ) : (
234
+ <span className="text-muted-foreground">None</span>
235
+ )}
236
+ </TableCell>
237
+ </TableRow>
238
+ ))}
239
+ </TableBody>
240
+ </Table>
241
+ </div>
242
+ </div>
243
+ <div className="flex h-[53px] shrink-0 items-center justify-between border-t border-border bg-background px-4">
244
+ <p className="text-sm text-muted-foreground">{total} rows</p>
245
+ <div className="flex items-center gap-2">
246
+ <Button
247
+ type="button"
248
+ variant="outline"
249
+ size="sm"
250
+ disabled={safePage <= 1}
251
+ onClick={() => void setFilters({ page: safePage - 1 })}
252
+ >
253
+ Previous
254
+ </Button>
255
+ <span className="min-w-20 text-center text-sm text-muted-foreground">
256
+ {safePage} / {totalPages}
257
+ </span>
258
+ <Button
259
+ type="button"
260
+ variant="outline"
261
+ size="sm"
262
+ disabled={safePage >= totalPages}
263
+ onClick={() => void setFilters({ page: safePage + 1 })}
264
+ >
265
+ Next
266
+ </Button>
267
+ </div>
268
+ </div>
269
+ </main>
270
+ <AuditLogDetailDrawer
271
+ auditLog={selectedAuditLog}
272
+ open={!!selectedAuditLog}
273
+ onOpenChange={(open) => {
274
+ if (!open) setSelectedAuditLog(null)
275
+ }}
276
+ />
277
+ </>
278
+ )
279
+ }
@@ -0,0 +1,19 @@
1
+ import { PageHeader } from '@admin/components/shared/page-header'
2
+ import { Skeleton } from '@admin/components/ui/skeleton'
3
+
4
+ export function AuditLogPageSkeleton() {
5
+ return (
6
+ <>
7
+ <PageHeader title="Audit Log" />
8
+ <main className="flex min-h-0 flex-1 flex-col gap-4 p-4">
9
+ <div className="grid gap-2 md:grid-cols-4">
10
+ <Skeleton className="h-9" />
11
+ <Skeleton className="h-9" />
12
+ <Skeleton className="h-9" />
13
+ <Skeleton className="h-9" />
14
+ </div>
15
+ <Skeleton className="h-[520px]" />
16
+ </main>
17
+ </>
18
+ )
19
+ }
@@ -0,0 +1,14 @@
1
+ import { requireRole, UserRole } from '@admin/auth/middleware'
2
+ import * as React from 'react'
3
+ import { AuditLogPageContent } from './audit-log-page-content'
4
+ import { AuditLogPageSkeleton } from './audit-log-page-skeleton'
5
+
6
+ export default async function AuditLogPage() {
7
+ await requireRole([UserRole.ADMIN])
8
+
9
+ return (
10
+ <React.Suspense fallback={<AuditLogPageSkeleton />}>
11
+ <AuditLogPageContent />
12
+ </React.Suspense>
13
+ )
14
+ }
@@ -63,7 +63,7 @@ export default function DashboardPage() {
63
63
  <div className="flex flex-col w-full gap-1">
64
64
  {envVariables.map((env) => (
65
65
  <div
66
- className="flex flex-col w-full bg-card border shadow-card border-border rounded-lg pl-4 pr-2 py-2"
66
+ className="flex flex-col w-full bg-card border shadow-card border-border rounded-xl pl-4 pr-2 py-2"
67
67
  key={env.name}
68
68
  >
69
69
  <div className="flex items-center justify-between">
@@ -1,6 +1,6 @@
1
1
  export function ForgotPasswordPageSkeleton() {
2
2
  return (
3
- <div className="overflow-hidden rounded-lg border bg-card">
3
+ <div className="overflow-hidden rounded-xl border bg-card">
4
4
  <div className="grid md:grid-cols-[5fr_7fr]">
5
5
  <div className="hidden min-h-[460px] bg-muted md:block" />
6
6
  <div className="flex min-h-[460px] flex-col gap-10 p-6 md:p-20">
@@ -1,6 +1,6 @@
1
1
  export function LoginPageSkeleton() {
2
2
  return (
3
- <div className="overflow-hidden rounded-lg border bg-card">
3
+ <div className="overflow-hidden rounded-xl border bg-card">
4
4
  <div className="grid md:grid-cols-[5fr_7fr]">
5
5
  <div className="hidden min-h-[540px] bg-muted md:block" />
6
6
  <div className="flex min-h-[540px] flex-col gap-10 p-6 md:p-20">
@@ -1,6 +1,6 @@
1
1
  export function ResetPasswordPageSkeleton() {
2
2
  return (
3
- <div className="overflow-hidden rounded-lg border bg-card">
3
+ <div className="overflow-hidden rounded-xl border bg-card">
4
4
  <div className="grid md:grid-cols-[5fr_7fr]">
5
5
  <div className="hidden min-h-[520px] bg-muted md:block" />
6
6
  <div className="flex min-h-[520px] flex-col gap-10 p-6 md:p-20">
@@ -0,0 +1,54 @@
1
+ const REDACTED_FIELD_PATTERNS = [/password/i, /token/i, /secret/i, /key/i]
2
+ const SYSTEM_FIELD_NAMES = new Set([
3
+ 'id',
4
+ 'createdAt',
5
+ 'updatedAt',
6
+ 'createdBy',
7
+ 'updatedBy',
8
+ 'createdByUser',
9
+ 'updatedByUser'
10
+ ])
11
+
12
+ function shouldTrackField(fieldName: string): boolean {
13
+ if (SYSTEM_FIELD_NAMES.has(fieldName)) return false
14
+ return !REDACTED_FIELD_PATTERNS.some((pattern) => pattern.test(fieldName))
15
+ }
16
+
17
+ function normalizeComparableValue(value: unknown): string {
18
+ if (value instanceof Date) return value.toISOString()
19
+ if (Array.isArray(value)) {
20
+ return JSON.stringify(value.map((item) => normalizeComparableValue(item)))
21
+ }
22
+ if (value && typeof value === 'object') {
23
+ const entries = Object.entries(value as Record<string, unknown>)
24
+ .sort(([a], [b]) => a.localeCompare(b))
25
+ .map(([key, item]) => [key, normalizeComparableValue(item)])
26
+ return JSON.stringify(entries)
27
+ }
28
+
29
+ return JSON.stringify(value ?? null)
30
+ }
31
+
32
+ export function getTouchedFieldNames(input: Record<string, unknown> | null | undefined): string[] {
33
+ if (!input) return []
34
+
35
+ return Object.keys(input)
36
+ .filter(shouldTrackField)
37
+ .sort((a, b) => a.localeCompare(b))
38
+ }
39
+
40
+ export function getChangedFieldNames(
41
+ previous: Record<string, unknown> | null | undefined,
42
+ next: Record<string, unknown> | null | undefined
43
+ ): string[] {
44
+ if (!next) return []
45
+
46
+ return Object.keys(next)
47
+ .filter(shouldTrackField)
48
+ .filter((fieldName) => {
49
+ const previousValue = previous?.[fieldName]
50
+ const nextValue = next[fieldName]
51
+ return normalizeComparableValue(previousValue) !== normalizeComparableValue(nextValue)
52
+ })
53
+ .sort((a, b) => a.localeCompare(b))
54
+ }
@@ -12,7 +12,6 @@ import {
12
12
  type TableRect
13
13
  } from '@tiptap/pm/tables'
14
14
  import { type Editor, findParentNodeClosestToPos, type NodeWithPos } from '@tiptap/react'
15
- import type { CSSProperties } from 'react'
16
15
  import {
17
16
  type ContentEditorInlineMathDelimiter,
18
17
  type ContentEditorMathKind,
@@ -146,29 +145,6 @@ export function runSlashCommand(
146
145
  return command.run(editor)
147
146
  }
148
147
 
149
- export function getSlashMenuStyle(rect: DOMRect): CSSProperties {
150
- const margin = 8
151
- const menuWidth = 288
152
- const menuMaxHeight = 320
153
-
154
- if (typeof window === 'undefined') {
155
- return {
156
- left: rect.left,
157
- position: 'fixed',
158
- top: rect.bottom + margin
159
- }
160
- }
161
-
162
- const maxLeft = Math.max(margin, window.innerWidth - menuWidth - margin)
163
- const maxTop = Math.max(margin, window.innerHeight - menuMaxHeight - margin)
164
-
165
- return {
166
- left: Math.min(Math.max(margin, rect.left), maxLeft),
167
- position: 'fixed',
168
- top: Math.min(Math.max(margin, rect.bottom + margin), maxTop)
169
- }
170
- }
171
-
172
148
  function isContentEditorTableAlignment(value: unknown): value is ContentEditorTableAlignment {
173
149
  return value === 'left' || value === 'center' || value === 'right'
174
150
  }
@@ -16,7 +16,7 @@ export function HorizontalRuleButton({ editor }: { editor: Editor | null }) {
16
16
  title="Divider"
17
17
  onClick={() => editor?.chain().focus().setHorizontalRule().run()}
18
18
  >
19
- <Minus className="tiptap-button-icon" />
19
+ <Minus />
20
20
  </Button>
21
21
  )
22
22
  }
@@ -39,6 +39,7 @@ export function ContentEditor({
39
39
 
40
40
  const {
41
41
  activeSlashIndex,
42
+ closeSlashMenu,
42
43
  editor,
43
44
  filteredSlashCommands,
44
45
  handleModeChange,
@@ -47,7 +48,9 @@ export function ContentEditor({
47
48
  mode,
48
49
  selectSlashCommand,
49
50
  setActiveSlashIndex,
51
+ setSlashSearch,
50
52
  slashMenu,
53
+ slashSearchQuery,
51
54
  sourceValue,
52
55
  sourceSelectionRequest
53
56
  } = useContentEditor({
@@ -123,8 +126,11 @@ export function ContentEditor({
123
126
  activeIndex={activeSlashIndex}
124
127
  commands={filteredSlashCommands}
125
128
  onActiveIndexChange={setActiveSlashIndex}
129
+ onClose={closeSlashMenu}
126
130
  onCommandSelect={selectSlashCommand}
131
+ onSearchChange={setSlashSearch}
127
132
  slashMenu={slashMenu}
133
+ searchQuery={slashSearchQuery}
128
134
  />
129
135
 
130
136
  <EditorContent
@@ -103,7 +103,7 @@ export function MainToolbarContent({
103
103
  mediaInsertion.insertMedia()
104
104
  }}
105
105
  >
106
- <ImagePlusIcon className="tiptap-button-icon" />
106
+ <ImagePlusIcon />
107
107
  <span className="tiptap-button-text">Media</span>
108
108
  </Button>
109
109
  ) : null}
@@ -63,7 +63,7 @@ export function MathBubbleMenu({ editor }: { editor: Editor }) {
63
63
  role="toolbar"
64
64
  aria-label="Math formatting toolbar"
65
65
  className={cn(
66
- 'flex items-center gap-1 overflow-x-auto border-border rounded-lg border bg-popover p-0 text-popover-foreground shadow-menu [scrollbar-width:none] [&::-webkit-scrollbar]:hidden',
66
+ 'flex items-center gap-1 overflow-x-auto border-border rounded-xl border bg-popover p-0 text-popover-foreground shadow-menu [scrollbar-width:none] [&::-webkit-scrollbar]:hidden',
67
67
  'relative z-50'
68
68
  )}
69
69
  >
@@ -13,7 +13,7 @@ export function MathButton({ children, text, ...props }: MathButtonProps) {
13
13
  <Button type="button" variant="ghost" size={text ? 'sm' : 'icon-sm'} {...props}>
14
14
  {children ?? (
15
15
  <>
16
- <SigmaIcon className="tiptap-button-icon" />
16
+ <SigmaIcon />
17
17
  {text ? <span className="tiptap-button-text">{text}</span> : null}
18
18
  </>
19
19
  )}
@@ -1,16 +1,13 @@
1
1
  'use client'
2
2
 
3
3
  import { Button } from '@admin/components/ui/button'
4
- import { Separator } from '@admin/components/ui/separator'
5
4
  import { Textarea } from '@admin/components/ui/textarea'
6
- import { cn } from '@admin/utils/shared/cn'
7
- import { CornerDownLeftIcon, TrashIcon } from 'lucide-react'
5
+ import { TrashIcon } from 'lucide-react'
8
6
  import * as React from 'react'
9
7
 
10
8
  export interface MathEditorControlsProps {
11
9
  disabled?: boolean
12
10
  initialValue?: string
13
- layout?: 'popover' | 'toolbar'
14
11
  onApply: (value: string) => boolean | undefined
15
12
  onComplete?: () => void
16
13
  onRemove?: () => boolean | undefined
@@ -19,14 +16,19 @@ export interface MathEditorControlsProps {
19
16
  export function MathEditorControls({
20
17
  disabled,
21
18
  initialValue = '',
22
- layout = 'toolbar',
23
19
  onApply,
24
20
  onComplete,
25
21
  onRemove
26
22
  }: MathEditorControlsProps) {
27
23
  const [value, setValue] = React.useState(initialValue)
24
+ const textareaRef = React.useRef<HTMLTextAreaElement>(null)
28
25
  const canApply = Boolean(value.trim()) && !disabled
29
- const isPopoverLayout = layout === 'popover'
26
+
27
+ React.useEffect(() => {
28
+ if (disabled) return
29
+
30
+ textareaRef.current?.focus({ preventScroll: true })
31
+ }, [disabled])
30
32
 
31
33
  const applyMath = React.useCallback(() => {
32
34
  if (!canApply) return
@@ -60,57 +62,42 @@ export function MathEditorControls({
60
62
  )
61
63
 
62
64
  return (
63
- <div
64
- className={
65
- isPopoverLayout
66
- ? 'flex w-72 max-w-[calc(100vw-2rem)] flex-col gap-3 p-3'
67
- : 'flex min-w-72 max-w-[calc(100vw-2rem)] items-start gap-2 p-2'
68
- }
69
- >
65
+ <div className="flex w-72 flex-col gap-3 p-4">
70
66
  <Textarea
67
+ ref={textareaRef}
71
68
  value={value}
72
69
  onChange={(event) => setValue(event.target.value)}
73
70
  onKeyDown={handleKeyDown}
74
71
  placeholder="Paste LaTeX..."
75
72
  disabled={disabled}
76
- autoFocus
77
73
  className="min-h-24 resize-y rounded-md text-sm"
78
74
  />
79
75
 
80
- <div
81
- className={cn({
82
- 'flex flex-col gap-2': isPopoverLayout,
83
- 'flex shrink-0 items-center gap-1': !isPopoverLayout
84
- })}
85
- >
76
+ <div className="flex items-center flex-wrap gap-1">
77
+ {onRemove ? (
78
+ <Button
79
+ type="button"
80
+ variant="destructive"
81
+ disabled={disabled}
82
+ aria-label="Remove math"
83
+ title="Remove"
84
+ onClick={removeMath}
85
+ >
86
+ <TrashIcon />
87
+ </Button>
88
+ ) : null}
89
+
86
90
  <Button
87
91
  type="button"
88
- variant={isPopoverLayout ? 'default' : 'ghost'}
89
- size={isPopoverLayout ? 'sm' : 'icon-sm'}
92
+ variant="default"
93
+ className="flex-1"
90
94
  disabled={!canApply}
91
95
  aria-label="Enter math"
92
96
  title="Enter"
93
97
  onClick={applyMath}
94
98
  >
95
- {isPopoverLayout ? 'Enter' : <CornerDownLeftIcon className="tiptap-button-icon" />}
99
+ Insert
96
100
  </Button>
97
-
98
- {onRemove ? (
99
- <>
100
- <Separator orientation="vertical" decorative className="h-6 my-auto self-center" />
101
- <Button
102
- type="button"
103
- variant="ghost"
104
- size="icon-sm"
105
- disabled={disabled}
106
- aria-label="Remove math"
107
- title="Remove"
108
- onClick={removeMath}
109
- >
110
- <TrashIcon className="tiptap-button-icon" />
111
- </Button>
112
- </>
113
- ) : null}
114
101
  </div>
115
102
  </div>
116
103
  )
@@ -49,7 +49,6 @@ export function MathPopoverButton({
49
49
  <PopoverContent align="start" className="w-auto gap-0 p-0">
50
50
  <MathEditorControls
51
51
  disabled={disabled || !canInsert}
52
- layout="popover"
53
52
  onApply={applyMath}
54
53
  onComplete={() => setIsOpen(false)}
55
54
  />
@@ -27,13 +27,13 @@ export function MobileToolbarContent({
27
27
  aria-label="Back to formatting toolbar"
28
28
  title="Back"
29
29
  >
30
- <ArrowLeftIcon className="tiptap-button-icon" />
30
+ <ArrowLeftIcon />
31
31
  {type === 'highlighter' ? (
32
- <HighlighterIcon className="tiptap-button-icon" />
32
+ <HighlighterIcon />
33
33
  ) : type === 'math' ? (
34
- <SigmaIcon className="tiptap-button-icon" />
34
+ <SigmaIcon />
35
35
  ) : (
36
- <LinkIcon className="tiptap-button-icon" />
36
+ <LinkIcon />
37
37
  )}
38
38
  </Button>
39
39
  </div>
@@ -22,7 +22,7 @@ export function ModeToggleButton({
22
22
  onClick={onClick}
23
23
  className="min-w-40"
24
24
  >
25
- <FileCode2 className="tiptap-button-icon" />
25
+ <FileCode2 />
26
26
  <span className="tiptap-button-text">{label}</span>
27
27
  </Button>
28
28
  )