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
@@ -1,5 +1,6 @@
1
- import { getRegisteredHooks } from './registry'
1
+ import { getRegisteredAdminHooks, getRegisteredHooks } from './registry'
2
2
  import type {
3
+ AdminEventContext,
3
4
  AfterLifecycleHookEvent,
4
5
  BeforeLifecycleHookEvent,
5
6
  LifecycleHookAbort,
@@ -33,19 +34,122 @@ export async function runBeforeHooks(
33
34
  }
34
35
  }
35
36
 
36
- export function runAfterHooks(event: AfterLifecycleHookEvent, context: LifecycleHookContext): void {
37
- const handlers = getRegisteredHooks(context.entityType, event)
37
+ function getAdminActionFromLifecycleEvent(
38
+ event: AfterLifecycleHookEvent
39
+ ): AdminEventContext['action'] | null {
40
+ if (event === 'afterCreate') return 'create'
41
+ if (event === 'afterUpdate') return 'update'
42
+ if (event === 'afterDelete') return 'delete'
43
+ if (event === 'afterPublish') return 'publish'
44
+ if (event === 'afterUnpublish') return 'unpublish'
45
+ return null
46
+ }
47
+
48
+ function getTargetLabel(data: Record<string, unknown> | undefined): string | null {
49
+ if (!data) return null
50
+
51
+ for (const fieldName of ['title', 'name', 'label', 'slug', 'email', 'filename']) {
52
+ const value = data[fieldName]
53
+ if (typeof value === 'string' && value.trim()) return value
54
+ }
55
+
56
+ return null
57
+ }
58
+
59
+ function getChangedFieldsFromLifecycleContext(
60
+ event: AfterLifecycleHookEvent,
61
+ context: LifecycleHookContext
62
+ ): string[] {
63
+ if (context.changedFields) return context.changedFields
64
+ if (event === 'afterDelete') return []
65
+
66
+ return Object.keys(context.input ?? {})
67
+ .filter((fieldName) => fieldName !== 'id')
68
+ .sort((a, b) => a.localeCompare(b))
69
+ }
70
+
71
+ function toAdminEventContext(
72
+ event: AfterLifecycleHookEvent,
73
+ context: LifecycleHookContext
74
+ ): AdminEventContext | null {
75
+ const action = getAdminActionFromLifecycleEvent(event)
76
+ if (!action) return null
77
+
78
+ return {
79
+ category: 'entity',
80
+ action,
81
+ targetType: context.entityType,
82
+ targetId: context.entityId ?? null,
83
+ targetLabel: context.targetLabel ?? getTargetLabel(context.data),
84
+ user: context.user,
85
+ timestamp: context.timestamp,
86
+ sourceType: context.sourceType ?? 'admin_ui',
87
+ ipAddress: context.ipAddress ?? null,
88
+ userAgent: context.userAgent ?? null,
89
+ changedFields: getChangedFieldsFromLifecycleContext(event, context),
90
+ metadata: {
91
+ entityType: context.entityType,
92
+ ...(context.metadata ?? {})
93
+ }
94
+ }
95
+ }
96
+
97
+ function runSettledHook<Context>(
98
+ handler: (context: Context) => void | Promise<void>,
99
+ context: Context
100
+ ): Promise<void> {
101
+ try {
102
+ return Promise.resolve(handler(context))
103
+ } catch (error) {
104
+ return Promise.reject(error)
105
+ }
106
+ }
107
+
108
+ export function runAdminEventHooks(context: AdminEventContext): void {
109
+ const handlers = getRegisteredAdminHooks({
110
+ category: context.category,
111
+ action: context.action
112
+ })
38
113
  if (handlers.length === 0) return
39
114
 
40
- void Promise.allSettled(handlers.map((handler) => handler(context))).then((results) => {
41
- for (const result of results) {
42
- if (result.status === 'rejected') {
43
- console.error('Lifecycle hook failed:', {
44
- entityType: context.entityType,
45
- event,
46
- error: result.reason
47
- })
115
+ void Promise.allSettled(handlers.map((handler) => runSettledHook(handler, context))).then(
116
+ (results) => {
117
+ for (const result of results) {
118
+ if (result.status === 'rejected') {
119
+ console.error('Admin event hook failed:', {
120
+ category: context.category,
121
+ action: context.action,
122
+ targetType: context.targetType,
123
+ targetId: context.targetId,
124
+ error: result.reason
125
+ })
126
+ }
48
127
  }
49
128
  }
50
- })
129
+ )
130
+ }
131
+
132
+ export function runAfterHooks(event: AfterLifecycleHookEvent, context: LifecycleHookContext): void {
133
+ const handlers = getRegisteredHooks(context.entityType, event)
134
+
135
+ if (handlers.length > 0) {
136
+ void Promise.allSettled(handlers.map((handler) => runSettledHook(handler, context))).then(
137
+ (results) => {
138
+ for (const result of results) {
139
+ if (result.status === 'rejected') {
140
+ console.error('Lifecycle hook failed:', {
141
+ entityType: context.entityType,
142
+ event,
143
+ error: result.reason
144
+ })
145
+ }
146
+ }
147
+ }
148
+ )
149
+ }
150
+
151
+ const adminEvent = toAdminEventContext(event, context)
152
+ if (adminEvent) {
153
+ runAdminEventHooks(adminEvent)
154
+ }
51
155
  }
@@ -1,3 +1,8 @@
1
+ import type {
2
+ AuditLogAction,
3
+ AuditLogCategory,
4
+ AuditLogSourceType
5
+ } from '@admin/actions/audit/types'
1
6
  import type { User } from '@admin/auth'
2
7
 
3
8
  export type LifecycleHookEvent =
@@ -22,6 +27,32 @@ export interface LifecycleHookContext {
22
27
  entityId?: string | undefined
23
28
  input?: Record<string, unknown> | undefined
24
29
  data?: Record<string, unknown> | undefined
30
+ changedFields?: string[] | undefined
31
+ sourceType?: AuditLogSourceType | undefined
32
+ ipAddress?: string | null | undefined
33
+ userAgent?: string | null | undefined
34
+ targetLabel?: string | null | undefined
35
+ metadata?: Record<string, unknown> | undefined
36
+ }
37
+
38
+ export interface AdminEventDescriptor {
39
+ category?: AuditLogCategory
40
+ action?: AuditLogAction
41
+ }
42
+
43
+ export interface AdminEventContext {
44
+ category: AuditLogCategory
45
+ action: AuditLogAction
46
+ targetType: string
47
+ targetId?: string | null | undefined
48
+ targetLabel?: string | null | undefined
49
+ user?: User | null | undefined
50
+ timestamp: string
51
+ sourceType?: AuditLogSourceType | undefined
52
+ ipAddress?: string | null | undefined
53
+ userAgent?: string | null | undefined
54
+ changedFields?: string[] | undefined
55
+ metadata?: Record<string, unknown> | undefined
25
56
  }
26
57
 
27
58
  export interface LifecycleHookAbort {
@@ -37,3 +68,5 @@ export type AfterLifecycleHookHandler = (context: LifecycleHookContext) => void
37
68
 
38
69
  export type LifecycleHookHandler<Event extends LifecycleHookEvent> =
39
70
  Event extends BeforeLifecycleHookEvent ? BeforeLifecycleHookHandler : AfterLifecycleHookHandler
71
+
72
+ export type AdminEventHookHandler = (context: AdminEventContext) => void | Promise<void>
@@ -0,0 +1,96 @@
1
+ 'use client'
2
+
3
+ import type { AuditLogEntry } from '@admin/actions/audit'
4
+ import { Badge } from '@admin/components/ui/badge'
5
+ import {
6
+ Drawer,
7
+ DrawerContent,
8
+ DrawerDescription,
9
+ DrawerHeader,
10
+ DrawerTitle
11
+ } from '@admin/components/ui/drawer'
12
+ import { ScrollArea } from '@admin/components/ui/scroll-area'
13
+ import { AuditLogDetailRow } from './audit-log-detail-row'
14
+
15
+ interface AuditLogDetailDrawerProps {
16
+ auditLog: AuditLogEntry | null
17
+ open: boolean
18
+ onOpenChange: (open: boolean) => void
19
+ }
20
+
21
+ function formatDateTime(value: string): string {
22
+ const date = new Date(value)
23
+ if (Number.isNaN(date.getTime())) return value
24
+
25
+ return new Intl.DateTimeFormat('en-US', {
26
+ dateStyle: 'medium',
27
+ timeStyle: 'medium'
28
+ }).format(date)
29
+ }
30
+
31
+ function formatMetadata(metadata: Record<string, unknown> | null): string {
32
+ if (!metadata || Object.keys(metadata).length === 0) return '{}'
33
+ return JSON.stringify(metadata, null, 2)
34
+ }
35
+
36
+ export function AuditLogDetailDrawer({ auditLog, open, onOpenChange }: AuditLogDetailDrawerProps) {
37
+ return (
38
+ <Drawer direction="right" open={open} onOpenChange={onOpenChange} modal>
39
+ <DrawerContent className="h-full sm:max-w-xl">
40
+ <DrawerHeader>
41
+ <DrawerTitle>Audit Event</DrawerTitle>
42
+ <DrawerDescription>
43
+ {auditLog ? `${auditLog.category}.${auditLog.action}` : 'No event selected'}
44
+ </DrawerDescription>
45
+ </DrawerHeader>
46
+ <ScrollArea className="min-h-0 flex-1">
47
+ {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>
90
+ </div>
91
+ ) : null}
92
+ </ScrollArea>
93
+ </DrawerContent>
94
+ </Drawer>
95
+ )
96
+ }
@@ -0,0 +1,17 @@
1
+ import type { ReactNode } from 'react'
2
+
3
+ interface AuditLogDetailRowProps {
4
+ label: string
5
+ value: ReactNode
6
+ }
7
+
8
+ export function AuditLogDetailRow({ label, value }: AuditLogDetailRowProps) {
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>
14
+ <dd className="min-w-0 text-sm text-foreground break-words">{value || 'Unknown'}</dd>
15
+ </div>
16
+ )
17
+ }
@@ -0,0 +1,152 @@
1
+ 'use client'
2
+
3
+ import {
4
+ AUDIT_LOG_ACTIONS,
5
+ AUDIT_LOG_CATEGORIES,
6
+ type AuditLogAction,
7
+ type AuditLogCategory
8
+ } from '@admin/actions/audit/types'
9
+ import { Button } from '@admin/components/ui/button'
10
+ import { Input } from '@admin/components/ui/input'
11
+ import {
12
+ Select,
13
+ SelectContent,
14
+ SelectItem,
15
+ SelectTrigger,
16
+ SelectValue
17
+ } from '@admin/components/ui/select'
18
+ import { RotateCcw } from 'lucide-react'
19
+
20
+ const ALL_FILTER_VALUE = '_all'
21
+
22
+ interface AuditLogFiltersProps {
23
+ search: string
24
+ category: string
25
+ action: string
26
+ targetType: string
27
+ actor: string
28
+ createdAtFrom: string
29
+ createdAtTo: string
30
+ activeFilterCount: number
31
+ onSearchChange: (value: string) => void
32
+ onCategoryChange: (value: AuditLogCategory | '') => void
33
+ onActionChange: (value: AuditLogAction | '') => void
34
+ onTargetTypeChange: (value: string) => void
35
+ onActorChange: (value: string) => void
36
+ onCreatedAtFromChange: (value: string) => void
37
+ onCreatedAtToChange: (value: string) => void
38
+ onClear: () => void
39
+ }
40
+
41
+ function formatFilterLabel(value: string): string {
42
+ return value
43
+ .split('_')
44
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
45
+ .join(' ')
46
+ }
47
+
48
+ export function AuditLogFilters({
49
+ search,
50
+ category,
51
+ action,
52
+ targetType,
53
+ actor,
54
+ createdAtFrom,
55
+ createdAtTo,
56
+ activeFilterCount,
57
+ onSearchChange,
58
+ onCategoryChange,
59
+ onActionChange,
60
+ onTargetTypeChange,
61
+ onActorChange,
62
+ onCreatedAtFromChange,
63
+ onCreatedAtToChange,
64
+ onClear
65
+ }: AuditLogFiltersProps) {
66
+ return (
67
+ <div className="flex flex-col gap-2 border-b border-border bg-background px-4 py-3">
68
+ <div className="grid gap-2 md:grid-cols-[minmax(180px,1.2fr)_repeat(5,minmax(132px,1fr))_auto]">
69
+ <Input
70
+ value={search}
71
+ onChange={(event) => onSearchChange(event.target.value)}
72
+ placeholder="Search logs"
73
+ className="h-8"
74
+ />
75
+ <Select
76
+ value={category || ALL_FILTER_VALUE}
77
+ onValueChange={(value) =>
78
+ onCategoryChange(value === ALL_FILTER_VALUE ? '' : (value as AuditLogCategory))
79
+ }
80
+ >
81
+ <SelectTrigger className="h-8">
82
+ <SelectValue placeholder="Category" />
83
+ </SelectTrigger>
84
+ <SelectContent>
85
+ <SelectItem value={ALL_FILTER_VALUE}>All categories</SelectItem>
86
+ {AUDIT_LOG_CATEGORIES.map((value) => (
87
+ <SelectItem key={value} value={value}>
88
+ {formatFilterLabel(value)}
89
+ </SelectItem>
90
+ ))}
91
+ </SelectContent>
92
+ </Select>
93
+ <Select
94
+ value={action || ALL_FILTER_VALUE}
95
+ onValueChange={(value) =>
96
+ onActionChange(value === ALL_FILTER_VALUE ? '' : (value as AuditLogAction))
97
+ }
98
+ >
99
+ <SelectTrigger className="h-8">
100
+ <SelectValue placeholder="Action" />
101
+ </SelectTrigger>
102
+ <SelectContent>
103
+ <SelectItem value={ALL_FILTER_VALUE}>All actions</SelectItem>
104
+ {AUDIT_LOG_ACTIONS.map((value) => (
105
+ <SelectItem key={value} value={value}>
106
+ {formatFilterLabel(value)}
107
+ </SelectItem>
108
+ ))}
109
+ </SelectContent>
110
+ </Select>
111
+ <Input
112
+ value={targetType}
113
+ onChange={(event) => onTargetTypeChange(event.target.value)}
114
+ placeholder="Target type"
115
+ className="h-8"
116
+ />
117
+ <Input
118
+ value={actor}
119
+ onChange={(event) => onActorChange(event.target.value)}
120
+ placeholder="Actor"
121
+ className="h-8"
122
+ />
123
+ <div className="grid grid-cols-2 gap-2 md:col-span-2">
124
+ <Input
125
+ type="date"
126
+ value={createdAtFrom}
127
+ onChange={(event) => onCreatedAtFromChange(event.target.value)}
128
+ className="h-8"
129
+ aria-label="Audit log start date"
130
+ />
131
+ <Input
132
+ type="date"
133
+ value={createdAtTo}
134
+ onChange={(event) => onCreatedAtToChange(event.target.value)}
135
+ className="h-8"
136
+ aria-label="Audit log end date"
137
+ />
138
+ </div>
139
+ <Button
140
+ type="button"
141
+ variant="outline"
142
+ size="icon-sm"
143
+ disabled={activeFilterCount === 0}
144
+ onClick={onClear}
145
+ aria-label="Clear audit log filters"
146
+ >
147
+ <RotateCcw />
148
+ </Button>
149
+ </div>
150
+ </div>
151
+ )
152
+ }