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.
- package/dist/assets/adapters/next/plugins/blog/plugin.ts +1 -1
- package/dist/assets/adapters/next/plugins/blog/schemas/menus.json +115 -0
- package/dist/assets/adapters/next/plugins/blog/schemas/posts.json +2 -7
- package/dist/assets/adapters/next/templates/init/components/layouts/admin-sidebar.tsx +2 -2
- package/dist/assets/adapters/next/templates/init/components/shared/dev-mode/lifecycle-hooks-tab.tsx +2 -2
- package/dist/assets/adapters/next/templates/init/components/shared/dev-mode/snippets-tab.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/components/shared/dev-mode-integrate.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/components/shared/entity-versions/entity-versions-drawer.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/components/shared/media/edit-media-dialog-content.tsx +2 -2
- package/dist/assets/adapters/next/templates/init/components/shared/media/media-url-importer.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/components/shared/page-header.tsx +20 -17
- package/dist/assets/adapters/next/templates/init/data/navigation.ts +2 -1
- package/dist/assets/adapters/next/templates/init/hooks/content-editor/use-content-editor-slash-menu.ts +28 -2
- package/dist/assets/adapters/next/templates/init/hooks/content-editor/use-content-editor.ts +22 -1
- package/dist/assets/adapters/next/templates/init/hooks/use-audit-log.ts +23 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/audit/cleanup-audit-logs.ts +63 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/audit/get-audit-logs.ts +97 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/audit/index.ts +17 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/audit/record-audit-event.ts +37 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/audit/register.ts +13 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/audit/request-metadata.ts +37 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/audit/types.ts +72 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/forms/upsert-form-settings.ts +3 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/media/create-media.ts +19 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/media/delete-media-bulk.ts +23 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/media/delete-media.ts +21 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/media/update-media.ts +23 -1
- package/dist/assets/adapters/next/templates/init/lib/actions/profile/update-email.ts +17 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/upload/upload-files.ts +3 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/upload/upload-media-from-url.ts +3 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/users/create-user.ts +19 -1
- package/dist/assets/adapters/next/templates/init/lib/actions/users/delete-user.ts +23 -1
- package/dist/assets/adapters/next/templates/init/lib/actions/users/update-user-role.ts +23 -1
- package/dist/assets/adapters/next/templates/init/lib/db/core/schema.ts +49 -0
- package/dist/assets/adapters/next/templates/init/lib/lifecycle-hooks/index.ts +6 -1
- package/dist/assets/adapters/next/templates/init/lib/lifecycle-hooks/register.ts +3 -0
- package/dist/assets/adapters/next/templates/init/lib/lifecycle-hooks/registry.ts +36 -1
- package/dist/assets/adapters/next/templates/init/lib/lifecycle-hooks/runner.ts +116 -12
- package/dist/assets/adapters/next/templates/init/lib/lifecycle-hooks/types.ts +33 -0
- package/dist/assets/adapters/next/templates/init/pages/audit-log/audit-log-detail-drawer.tsx +96 -0
- package/dist/assets/adapters/next/templates/init/pages/audit-log/audit-log-detail-row.tsx +17 -0
- package/dist/assets/adapters/next/templates/init/pages/audit-log/audit-log-filters.tsx +152 -0
- package/dist/assets/adapters/next/templates/init/pages/audit-log/audit-log-page-content.tsx +279 -0
- package/dist/assets/adapters/next/templates/init/pages/audit-log/audit-log-page-skeleton.tsx +19 -0
- package/dist/assets/adapters/next/templates/init/pages/audit-log/audit-log-page.tsx +14 -0
- package/dist/assets/adapters/next/templates/init/pages/dashboard-page.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/pages/forgot-password-page-skeleton.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/pages/login-page-skeleton.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/pages/reset-password-page-skeleton.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/utils/audit/audit.ts +54 -0
- package/dist/assets/adapters/next/templates/init/utils/editor/tiptap.ts +0 -24
- package/dist/assets/shared-assets/react-admin/custom/content-editor/horizontal-rule-button.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/content-editor/index.tsx +6 -0
- package/dist/assets/shared-assets/react-admin/custom/content-editor/main-toolbar-content.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/content-editor/math-bubble-menu.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/content-editor/math-button.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/content-editor/math-editor-controls.tsx +27 -40
- package/dist/assets/shared-assets/react-admin/custom/content-editor/math-popover-button.tsx +0 -1
- package/dist/assets/shared-assets/react-admin/custom/content-editor/mobile-toolbar-content.tsx +4 -4
- package/dist/assets/shared-assets/react-admin/custom/content-editor/mode-toggle-button.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/content-editor/selection-bubble-menu.tsx +45 -8
- package/dist/assets/shared-assets/react-admin/custom/content-editor/slash-command-menu.tsx +153 -27
- package/dist/assets/shared-assets/react-admin/custom/content-editor/source-mode-dropdown-button.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/content-editor/source-mode.tsx +9 -9
- package/dist/assets/shared-assets/react-admin/custom/content-editor/table-bubble-menu.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/content-editor/table-button.tsx +9 -4
- package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/blockquote-button/blockquote-button.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/code-block-button/code-block-button.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/color-highlight-popover/color-highlight-popover-button.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/color-highlight-popover/color-highlight-popover-content.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/color-highlight-popover/color-highlight-popover.tsx +11 -2
- package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/color-highlight-popover/source-color-highlight-popover.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/heading-dropdown-menu/heading-dropdown-menu-item.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/heading-dropdown-menu/heading-dropdown-menu.tsx +9 -4
- package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/link-popover/link-button.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/link-popover/link-control-popover.tsx +2 -4
- package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/link-popover/link-main.tsx +19 -45
- package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/link-popover/link-popover.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/list-dropdown-menu/list-dropdown-menu-item.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/list-dropdown-menu/list-dropdown-menu.tsx +9 -4
- package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/mark-button/mark-button.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/undo-redo-button/undo-redo-button.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/date-range-picker.tsx +3 -3
- package/dist/assets/shared-assets/react-admin/custom/gallery-field.tsx +2 -2
- package/dist/assets/shared-assets/react-admin/custom/media-gallery-field.tsx +2 -2
- package/dist/assets/shared-assets/react-admin/custom/placeholder-card.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/placeholder.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/sortable-gallery-item.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/schema.json +0 -23
- package/dist/assets/shared-assets/react-admin/ui/accordion.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/alert.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/button.tsx +18 -18
- package/dist/assets/shared-assets/react-admin/ui/card.tsx +3 -3
- package/dist/assets/shared-assets/react-admin/ui/chart.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/combobox.tsx +29 -5
- package/dist/assets/shared-assets/react-admin/ui/command.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/context-menu.tsx +6 -6
- package/dist/assets/shared-assets/react-admin/ui/drawer.tsx +7 -3
- package/dist/assets/shared-assets/react-admin/ui/dropdown-menu.tsx +32 -17
- package/dist/assets/shared-assets/react-admin/ui/empty.tsx +2 -2
- package/dist/assets/shared-assets/react-admin/ui/field.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/hover-card.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/input-group.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/item.tsx +2 -2
- package/dist/assets/shared-assets/react-admin/ui/menubar.tsx +7 -7
- package/dist/assets/shared-assets/react-admin/ui/navigation-menu.tsx +4 -4
- package/dist/assets/shared-assets/react-admin/ui/popover.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/resizable.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/select.tsx +2 -2
- package/dist/assets/shared-assets/react-admin/ui/sidebar.tsx +3 -3
- package/dist/assets/shared-assets/react-admin/ui/skeleton.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/tabs.tsx +2 -2
- package/dist/assets/shared-assets/react-admin/ui/textarea.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/tooltip.tsx +1 -1
- package/dist/cli.js +2769 -802
- package/dist/cli.js.map +1 -1
- 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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
}
|
|
@@ -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
|
|
@@ -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-
|
|
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
|
|
16
|
+
<SigmaIcon />
|
|
17
17
|
{text ? <span className="tiptap-button-text">{text}</span> : null}
|
|
18
18
|
</>
|
|
19
19
|
)}
|
package/dist/assets/shared-assets/react-admin/custom/content-editor/math-editor-controls.tsx
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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=
|
|
89
|
-
|
|
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
|
-
|
|
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
|
)
|
package/dist/assets/shared-assets/react-admin/custom/content-editor/mobile-toolbar-content.tsx
CHANGED
|
@@ -27,13 +27,13 @@ export function MobileToolbarContent({
|
|
|
27
27
|
aria-label="Back to formatting toolbar"
|
|
28
28
|
title="Back"
|
|
29
29
|
>
|
|
30
|
-
<ArrowLeftIcon
|
|
30
|
+
<ArrowLeftIcon />
|
|
31
31
|
{type === 'highlighter' ? (
|
|
32
|
-
<HighlighterIcon
|
|
32
|
+
<HighlighterIcon />
|
|
33
33
|
) : type === 'math' ? (
|
|
34
|
-
<SigmaIcon
|
|
34
|
+
<SigmaIcon />
|
|
35
35
|
) : (
|
|
36
|
-
<LinkIcon
|
|
36
|
+
<LinkIcon />
|
|
37
37
|
)}
|
|
38
38
|
</Button>
|
|
39
39
|
</div>
|