betterstart-cli 0.0.13 → 0.0.14

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 (76) hide show
  1. package/dist/assets/adapters/next/plugins/blog/schemas/menus.json +51 -11
  2. package/dist/assets/adapters/next/templates/init/components/layouts/admin-sidebar.tsx +7 -1
  3. package/dist/assets/adapters/next/templates/init/components/layouts/sidebar-branding.tsx +1 -1
  4. package/dist/assets/adapters/next/templates/init/components/shared/data-table/data-grid.tsx +107 -0
  5. package/dist/assets/adapters/next/templates/init/components/shared/data-table/data-table.tsx +58 -45
  6. package/dist/assets/adapters/next/templates/init/components/shared/dev-mode/lifecycle-hooks-tab.tsx +3 -3
  7. package/dist/assets/adapters/next/templates/init/components/shared/dev-mode/snippets-tab.tsx +1 -1
  8. package/dist/assets/adapters/next/templates/init/components/shared/entity-filters-bar.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/sort-indicator.tsx +7 -3
  12. package/dist/assets/adapters/next/templates/init/data/navigation.ts +2 -0
  13. package/dist/assets/adapters/next/templates/init/hooks/content-editor/use-table-button.ts +85 -0
  14. package/dist/assets/adapters/next/templates/init/lib/actions/audit/get-audit-logs.ts +41 -16
  15. package/dist/assets/adapters/next/templates/init/lib/actions/audit/index.ts +2 -0
  16. package/dist/assets/adapters/next/templates/init/lib/actions/audit/types.ts +5 -0
  17. package/dist/assets/adapters/next/templates/init/lib/lifecycle-hooks/runner.ts +2 -1
  18. package/dist/assets/adapters/next/templates/init/lib/lifecycle-hooks/types.ts +2 -1
  19. package/dist/assets/adapters/next/templates/init/pages/audit-log/audit-log-detail-drawer.tsx +51 -43
  20. package/dist/assets/adapters/next/templates/init/pages/audit-log/audit-log-detail-row.tsx +2 -4
  21. package/dist/assets/adapters/next/templates/init/pages/audit-log/audit-log-page-content.tsx +465 -179
  22. package/dist/assets/adapters/next/templates/init/pages/audit-log/audit-log-page-skeleton.tsx +3 -15
  23. package/dist/assets/adapters/next/templates/init/pages/dashboard-page.tsx +1 -1
  24. package/dist/assets/adapters/next/templates/init/pages/forgot-password-page-skeleton.tsx +1 -1
  25. package/dist/assets/adapters/next/templates/init/pages/login-page-skeleton.tsx +1 -1
  26. package/dist/assets/adapters/next/templates/init/pages/reset-password-page-skeleton.tsx +1 -1
  27. package/dist/assets/adapters/next/templates/init/pages/users/users-page-content.tsx +83 -2
  28. package/dist/assets/adapters/next/templates/init/pages/users/users-table.tsx +126 -78
  29. package/dist/assets/adapters/next/templates/init/utils/editor/tiptap.ts +7 -8
  30. package/dist/assets/adapters/next/templates/init/utils/form/list-field.ts +17 -0
  31. package/dist/assets/adapters/next/templates/init/utils/user/user.ts +70 -0
  32. package/dist/assets/shared-assets/react-admin/custom/content-editor/math-bubble-menu.tsx +1 -1
  33. package/dist/assets/shared-assets/react-admin/custom/content-editor/selection-bubble-menu.tsx +1 -1
  34. package/dist/assets/shared-assets/react-admin/custom/content-editor/source-mode.tsx +0 -2
  35. package/dist/assets/shared-assets/react-admin/custom/content-editor/table-button.tsx +17 -65
  36. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/link-popover/link-control-popover.tsx +3 -8
  37. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/link-popover/link-popover.tsx +1 -2
  38. package/dist/assets/shared-assets/react-admin/custom/date-range-picker.tsx +1 -1
  39. package/dist/assets/shared-assets/react-admin/custom/dynamic-list-field.tsx +20 -8
  40. package/dist/assets/shared-assets/react-admin/custom/gallery-field.tsx +2 -2
  41. package/dist/assets/shared-assets/react-admin/custom/media-gallery-field.tsx +2 -2
  42. package/dist/assets/shared-assets/react-admin/custom/nested-object-list-field.tsx +211 -0
  43. package/dist/assets/shared-assets/react-admin/custom/placeholder-card.tsx +1 -1
  44. package/dist/assets/shared-assets/react-admin/custom/placeholder.tsx +1 -1
  45. package/dist/assets/shared-assets/react-admin/custom/sortable-gallery-item.tsx +1 -1
  46. package/dist/assets/shared-assets/react-admin/schema.json +136 -0
  47. package/dist/assets/shared-assets/react-admin/ui/accordion.tsx +1 -1
  48. package/dist/assets/shared-assets/react-admin/ui/alert.tsx +1 -1
  49. package/dist/assets/shared-assets/react-admin/ui/card.tsx +2 -2
  50. package/dist/assets/shared-assets/react-admin/ui/chart.tsx +1 -1
  51. package/dist/assets/shared-assets/react-admin/ui/checkbox.tsx +1 -1
  52. package/dist/assets/shared-assets/react-admin/ui/combobox.tsx +2 -2
  53. package/dist/assets/shared-assets/react-admin/ui/command.tsx +1 -1
  54. package/dist/assets/shared-assets/react-admin/ui/context-menu.tsx +6 -6
  55. package/dist/assets/shared-assets/react-admin/ui/drawer.tsx +2 -2
  56. package/dist/assets/shared-assets/react-admin/ui/dropdown-menu.tsx +6 -6
  57. package/dist/assets/shared-assets/react-admin/ui/empty.tsx +2 -2
  58. package/dist/assets/shared-assets/react-admin/ui/field.tsx +1 -1
  59. package/dist/assets/shared-assets/react-admin/ui/form.tsx +1 -1
  60. package/dist/assets/shared-assets/react-admin/ui/hover-card.tsx +1 -1
  61. package/dist/assets/shared-assets/react-admin/ui/input-group.tsx +1 -1
  62. package/dist/assets/shared-assets/react-admin/ui/item.tsx +2 -2
  63. package/dist/assets/shared-assets/react-admin/ui/menubar.tsx +7 -7
  64. package/dist/assets/shared-assets/react-admin/ui/navigation-menu.tsx +4 -4
  65. package/dist/assets/shared-assets/react-admin/ui/popover.tsx +1 -1
  66. package/dist/assets/shared-assets/react-admin/ui/resizable.tsx +1 -1
  67. package/dist/assets/shared-assets/react-admin/ui/select.tsx +2 -2
  68. package/dist/assets/shared-assets/react-admin/ui/sidebar.tsx +3 -3
  69. package/dist/assets/shared-assets/react-admin/ui/skeleton.tsx +1 -1
  70. package/dist/assets/shared-assets/react-admin/ui/tabs.tsx +2 -2
  71. package/dist/assets/shared-assets/react-admin/ui/textarea.tsx +1 -1
  72. package/dist/assets/shared-assets/react-admin/ui/tooltip.tsx +1 -1
  73. package/dist/cli.js +2688 -1644
  74. package/dist/cli.js.map +1 -1
  75. package/package.json +10 -2
  76. package/dist/assets/adapters/next/templates/init/pages/audit-log/audit-log-filters.tsx +0 -152
@@ -3,19 +3,21 @@
3
3
  import { requireRole, UserRole } from '@admin/auth/middleware'
4
4
  import db from '@admin/db'
5
5
  import { auditLog } from '@admin/db/schema'
6
- import { and, desc, eq, gte, ilike, lt, or, sql } from 'drizzle-orm'
6
+ import { and, asc, desc, eq, gte, ilike, lt, or, sql } from 'drizzle-orm'
7
7
  import type { AuditLogEntry, AuditLogPage, AuditLogQuery } from './types'
8
8
 
9
- const DEFAULT_AUDIT_LOG_PAGE = 1
10
- const DEFAULT_AUDIT_LOG_PAGE_SIZE = 25
9
+ const DEFAULT_AUDIT_LOG_PAGE = 0
10
+ const DEFAULT_AUDIT_LOG_PAGE_SIZE = 20
11
11
  const MAX_AUDIT_LOG_PAGE_SIZE = 100
12
12
 
13
13
  function clampPage(value: number | undefined): number {
14
14
  if (typeof value !== 'number' || !Number.isFinite(value)) return DEFAULT_AUDIT_LOG_PAGE
15
- return Math.max(DEFAULT_AUDIT_LOG_PAGE, Math.floor(value))
15
+ return Math.max(0, Math.floor(value))
16
16
  }
17
17
 
18
- function clampPageSize(value: number | undefined): number {
18
+ function normalizePageSize(value: number | undefined): number | null {
19
+ if (value === -1) return null
20
+
19
21
  if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
20
22
  return DEFAULT_AUDIT_LOG_PAGE_SIZE
21
23
  }
@@ -23,6 +25,28 @@ function clampPageSize(value: number | undefined): number {
23
25
  return Math.min(MAX_AUDIT_LOG_PAGE_SIZE, Math.max(1, Math.floor(value)))
24
26
  }
25
27
 
28
+ function getAuditLogOrderBy(query: AuditLogQuery) {
29
+ const sortFn = query.dir === 'asc' ? asc : desc
30
+
31
+ switch (query.sort) {
32
+ case 'event':
33
+ return [sortFn(auditLog.category), sortFn(auditLog.action), desc(auditLog.createdAt)]
34
+ case 'target':
35
+ return [
36
+ sortFn(auditLog.targetType),
37
+ sortFn(auditLog.targetLabel),
38
+ sortFn(auditLog.targetId),
39
+ desc(auditLog.createdAt)
40
+ ]
41
+ case 'actor':
42
+ return [sortFn(auditLog.actorEmail), sortFn(auditLog.actorName), desc(auditLog.createdAt)]
43
+ case 'sourceType':
44
+ return [sortFn(auditLog.sourceType), desc(auditLog.createdAt)]
45
+ default:
46
+ return [sortFn(auditLog.createdAt)]
47
+ }
48
+ }
49
+
26
50
  function getExclusiveDateUpperBound(value: string | undefined): string | null {
27
51
  if (!value) return null
28
52
  const date = new Date(value)
@@ -34,8 +58,8 @@ function getExclusiveDateUpperBound(value: string | undefined): string | null {
34
58
 
35
59
  async function queryAuditLogs(query: AuditLogQuery = {}): Promise<AuditLogPage> {
36
60
  const page = clampPage(query.page)
37
- const pageSize = clampPageSize(query.pageSize)
38
- const offset = (page - 1) * pageSize
61
+ const pageSize = normalizePageSize(query.pageSize)
62
+ const offset = pageSize ? page * pageSize : 0
39
63
  const conditions = []
40
64
 
41
65
  if (query.category) conditions.push(eq(auditLog.category, query.category))
@@ -70,24 +94,25 @@ async function queryAuditLogs(query: AuditLogQuery = {}): Promise<AuditLogPage>
70
94
  }
71
95
 
72
96
  const where = conditions.length > 0 ? and(...conditions) : undefined
97
+ const orderBy = getAuditLogOrderBy(query)
98
+ const orderedQuery = db
99
+ .select()
100
+ .from(auditLog)
101
+ .where(where)
102
+ .orderBy(...orderBy)
73
103
  const [rows, countResult] = await Promise.all([
74
- db
75
- .select()
76
- .from(auditLog)
77
- .where(where)
78
- .orderBy(desc(auditLog.createdAt))
79
- .limit(pageSize)
80
- .offset(offset),
104
+ pageSize ? orderedQuery.limit(pageSize).offset(offset) : orderedQuery,
81
105
  db.select({ count: sql<number>`count(*)` }).from(auditLog).where(where)
82
106
  ])
83
107
  const total = Number(countResult[0]?.count ?? 0)
108
+ const resolvedPageSize = pageSize ?? total
84
109
 
85
110
  return {
86
111
  auditLogs: rows as AuditLogEntry[],
87
112
  total,
88
113
  page,
89
- pageSize,
90
- totalPages: Math.max(1, Math.ceil(total / pageSize))
114
+ pageSize: resolvedPageSize,
115
+ totalPages: pageSize ? Math.max(1, Math.ceil(total / pageSize)) : 1
91
116
  }
92
117
  }
93
118
 
@@ -6,6 +6,8 @@ export type {
6
6
  AuditLogEntry,
7
7
  AuditLogPage,
8
8
  AuditLogQuery,
9
+ AuditLogSortDirection,
10
+ AuditLogSortField,
9
11
  AuditLogSourceType,
10
12
  CleanupAuditLogsResult
11
13
  } from './types'
@@ -1,5 +1,6 @@
1
1
  export const AUDIT_LOG_CATEGORIES = [
2
2
  'entity',
3
+ 'form',
3
4
  'settings',
4
5
  'user',
5
6
  'auth',
@@ -19,6 +20,8 @@ export const AUDIT_LOG_SOURCE_TYPES = ['admin_ui', 'server_action', 'system_job'
19
20
  export type AuditLogCategory = (typeof AUDIT_LOG_CATEGORIES)[number]
20
21
  export type AuditLogAction = (typeof AUDIT_LOG_ACTIONS)[number]
21
22
  export type AuditLogSourceType = (typeof AUDIT_LOG_SOURCE_TYPES)[number]
23
+ export type AuditLogSortField = 'createdAt' | 'event' | 'target' | 'actor' | 'sourceType'
24
+ export type AuditLogSortDirection = 'asc' | 'desc'
22
25
 
23
26
  export interface AuditLogEntry {
24
27
  id: string
@@ -49,6 +52,8 @@ export interface AuditLogQuery {
49
52
  createdAtTo?: string
50
53
  page?: number
51
54
  pageSize?: number
55
+ sort?: AuditLogSortField
56
+ dir?: AuditLogSortDirection
52
57
  }
53
58
 
54
59
  export interface AuditLogPage {
@@ -76,7 +76,7 @@ function toAdminEventContext(
76
76
  if (!action) return null
77
77
 
78
78
  return {
79
- category: 'entity',
79
+ category: context.schemaType === 'form' ? 'form' : 'entity',
80
80
  action,
81
81
  targetType: context.entityType,
82
82
  targetId: context.entityId ?? null,
@@ -89,6 +89,7 @@ function toAdminEventContext(
89
89
  changedFields: getChangedFieldsFromLifecycleContext(event, context),
90
90
  metadata: {
91
91
  entityType: context.entityType,
92
+ schemaType: context.schemaType ?? 'entity',
92
93
  ...(context.metadata ?? {})
93
94
  }
94
95
  }
@@ -21,8 +21,9 @@ export type BeforeLifecycleHookEvent = Extract<LifecycleHookEvent, `before${stri
21
21
  export type AfterLifecycleHookEvent = Extract<LifecycleHookEvent, `after${string}`>
22
22
 
23
23
  export interface LifecycleHookContext {
24
- user: User
24
+ user: User | null
25
25
  entityType: string
26
+ schemaType?: 'entity' | 'form' | undefined
26
27
  timestamp: string
27
28
  entityId?: string | undefined
28
29
  input?: Record<string, unknown> | undefined
@@ -2,6 +2,7 @@
2
2
 
3
3
  import type { AuditLogEntry } from '@admin/actions/audit'
4
4
  import { Badge } from '@admin/components/ui/badge'
5
+ import { Card, CardContent, CardHeader, CardTitle } from '@admin/components/ui/card'
5
6
  import {
6
7
  Drawer,
7
8
  DrawerContent,
@@ -43,50 +44,57 @@ export function AuditLogDetailDrawer({ auditLog, open, onOpenChange }: AuditLogD
43
44
  {auditLog ? `${auditLog.category}.${auditLog.action}` : 'No event selected'}
44
45
  </DrawerDescription>
45
46
  </DrawerHeader>
46
- <ScrollArea className="min-h-0 flex-1">
47
+ <ScrollArea className="min-h-0 flex-1 px-6">
47
48
  {auditLog ? (
48
- <div className="p-4">
49
- <dl className="rounded-lg border border-border bg-card px-4">
50
- <AuditLogDetailRow label="Time" value={formatDateTime(auditLog.createdAt)} />
51
- <AuditLogDetailRow label="Category" value={auditLog.category} />
52
- <AuditLogDetailRow label="Action" value={auditLog.action} />
53
- <AuditLogDetailRow
54
- label="Target"
55
- value={auditLog.targetLabel ?? auditLog.targetId}
56
- />
57
- <AuditLogDetailRow label="Target Type" value={auditLog.targetType} />
58
- <AuditLogDetailRow label="Target ID" value={auditLog.targetId} />
59
- <AuditLogDetailRow
60
- label="Actor"
61
- value={auditLog.actorEmail ?? auditLog.actorName ?? auditLog.actorUserId}
62
- />
63
- <AuditLogDetailRow label="Actor Role" value={auditLog.actorRole} />
64
- <AuditLogDetailRow label="Source" value={auditLog.sourceType} />
65
- <AuditLogDetailRow label="IP" value={auditLog.ipAddress} />
66
- <AuditLogDetailRow label="User Agent" value={auditLog.userAgent} />
67
- <AuditLogDetailRow
68
- label="Fields"
69
- value={
70
- auditLog.changedFields.length > 0 ? (
71
- <div className="flex flex-wrap gap-1">
72
- {auditLog.changedFields.map((field) => (
73
- <Badge key={field} variant="secondary">
74
- {field}
75
- </Badge>
76
- ))}
77
- </div>
78
- ) : (
79
- 'None'
80
- )
81
- }
82
- />
83
- </dl>
84
- <div className="mt-4 rounded-lg border border-border bg-card p-4">
85
- <h3 className="mb-2 text-sm font-medium">Metadata</h3>
86
- <pre className="max-h-72 overflow-auto rounded-md bg-muted p-3 text-xs leading-relaxed">
87
- {formatMetadata(auditLog.metadata)}
88
- </pre>
89
- </div>
49
+ <div className="flex flex-col w-full gap-4">
50
+ <Card>
51
+ <CardHeader>
52
+ <CardTitle>Event Details</CardTitle>
53
+ </CardHeader>
54
+
55
+ <CardContent className="p-0!">
56
+ <AuditLogDetailRow label="Time" value={formatDateTime(auditLog.createdAt)} />
57
+ <AuditLogDetailRow label="Category" value={auditLog.category} />
58
+ <AuditLogDetailRow label="Action" value={auditLog.action} />
59
+ <AuditLogDetailRow
60
+ label="Target"
61
+ value={auditLog.targetLabel ?? auditLog.targetId}
62
+ />
63
+ <AuditLogDetailRow label="Target Type" value={auditLog.targetType} />
64
+ <AuditLogDetailRow label="Target ID" value={auditLog.targetId} />
65
+ <AuditLogDetailRow
66
+ label="Actor"
67
+ value={auditLog.actorEmail ?? auditLog.actorName ?? auditLog.actorUserId}
68
+ />
69
+ <AuditLogDetailRow label="Actor Role" value={auditLog.actorRole} />
70
+ <AuditLogDetailRow label="Source" value={auditLog.sourceType} />
71
+ <AuditLogDetailRow label="IP" value={auditLog.ipAddress} />
72
+ <AuditLogDetailRow label="User Agent" value={auditLog.userAgent} />
73
+ <AuditLogDetailRow
74
+ label="Fields"
75
+ value={
76
+ auditLog.changedFields.length > 0 ? (
77
+ <div className="flex flex-wrap gap-1">
78
+ {auditLog.changedFields.map((field) => (
79
+ <Badge key={field} variant="secondary">
80
+ {field}
81
+ </Badge>
82
+ ))}
83
+ </div>
84
+ ) : (
85
+ 'None'
86
+ )
87
+ }
88
+ />
89
+ </CardContent>
90
+ </Card>
91
+
92
+ <Card>
93
+ <CardHeader>
94
+ <CardTitle>Event Metadata</CardTitle>
95
+ </CardHeader>
96
+ <CardContent>{formatMetadata(auditLog.metadata)}</CardContent>
97
+ </Card>
90
98
  </div>
91
99
  ) : null}
92
100
  </ScrollArea>
@@ -7,10 +7,8 @@ interface AuditLogDetailRowProps {
7
7
 
8
8
  export function AuditLogDetailRow({ label, value }: AuditLogDetailRowProps) {
9
9
  return (
10
- <div className="grid grid-cols-[120px_1fr] gap-3 border-b border-border/70 py-3 last:border-b-0">
11
- <dt className="text-xs font-medium uppercase tracking-normal text-muted-foreground">
12
- {label}
13
- </dt>
10
+ <div className="grid grid-cols-[120px_1fr] gap-3 border-b border-border py-2 px-4 last:border-b-0">
11
+ <dt className="text-sm font-medium text-muted-foreground">{label}</dt>
14
12
  <dd className="min-w-0 text-sm text-foreground break-words">{value || 'Unknown'}</dd>
15
13
  </div>
16
14
  )