betterstart-cli 0.0.12 → 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 (99) 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 +155 -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 +9 -3
  5. package/dist/assets/adapters/next/templates/init/components/layouts/sidebar-branding.tsx +1 -1
  6. package/dist/assets/adapters/next/templates/init/components/shared/data-table/data-grid.tsx +107 -0
  7. package/dist/assets/adapters/next/templates/init/components/shared/data-table/data-table.tsx +58 -45
  8. package/dist/assets/adapters/next/templates/init/components/shared/dev-mode/lifecycle-hooks-tab.tsx +1 -1
  9. package/dist/assets/adapters/next/templates/init/components/shared/dev-mode-integrate.tsx +1 -1
  10. package/dist/assets/adapters/next/templates/init/components/shared/entity-filters-bar.tsx +1 -1
  11. package/dist/assets/adapters/next/templates/init/components/shared/entity-versions/entity-versions-drawer.tsx +1 -1
  12. package/dist/assets/adapters/next/templates/init/components/shared/page-header.tsx +20 -17
  13. package/dist/assets/adapters/next/templates/init/components/shared/sort-indicator.tsx +7 -3
  14. package/dist/assets/adapters/next/templates/init/data/navigation.ts +4 -1
  15. package/dist/assets/adapters/next/templates/init/hooks/content-editor/use-content-editor-slash-menu.ts +28 -2
  16. package/dist/assets/adapters/next/templates/init/hooks/content-editor/use-content-editor.ts +22 -1
  17. package/dist/assets/adapters/next/templates/init/hooks/content-editor/use-table-button.ts +85 -0
  18. package/dist/assets/adapters/next/templates/init/hooks/use-audit-log.ts +23 -0
  19. package/dist/assets/adapters/next/templates/init/lib/actions/audit/cleanup-audit-logs.ts +63 -0
  20. package/dist/assets/adapters/next/templates/init/lib/actions/audit/get-audit-logs.ts +122 -0
  21. package/dist/assets/adapters/next/templates/init/lib/actions/audit/index.ts +19 -0
  22. package/dist/assets/adapters/next/templates/init/lib/actions/audit/record-audit-event.ts +37 -0
  23. package/dist/assets/adapters/next/templates/init/lib/actions/audit/register.ts +13 -0
  24. package/dist/assets/adapters/next/templates/init/lib/actions/audit/request-metadata.ts +37 -0
  25. package/dist/assets/adapters/next/templates/init/lib/actions/audit/types.ts +77 -0
  26. package/dist/assets/adapters/next/templates/init/lib/actions/forms/upsert-form-settings.ts +3 -0
  27. package/dist/assets/adapters/next/templates/init/lib/actions/media/create-media.ts +19 -0
  28. package/dist/assets/adapters/next/templates/init/lib/actions/media/delete-media-bulk.ts +23 -0
  29. package/dist/assets/adapters/next/templates/init/lib/actions/media/delete-media.ts +21 -0
  30. package/dist/assets/adapters/next/templates/init/lib/actions/media/update-media.ts +23 -1
  31. package/dist/assets/adapters/next/templates/init/lib/actions/profile/update-email.ts +17 -0
  32. package/dist/assets/adapters/next/templates/init/lib/actions/upload/upload-files.ts +3 -0
  33. package/dist/assets/adapters/next/templates/init/lib/actions/upload/upload-media-from-url.ts +3 -0
  34. package/dist/assets/adapters/next/templates/init/lib/actions/users/create-user.ts +19 -1
  35. package/dist/assets/adapters/next/templates/init/lib/actions/users/delete-user.ts +23 -1
  36. package/dist/assets/adapters/next/templates/init/lib/actions/users/update-user-role.ts +23 -1
  37. package/dist/assets/adapters/next/templates/init/lib/db/core/schema.ts +49 -0
  38. package/dist/assets/adapters/next/templates/init/lib/lifecycle-hooks/index.ts +6 -1
  39. package/dist/assets/adapters/next/templates/init/lib/lifecycle-hooks/register.ts +3 -0
  40. package/dist/assets/adapters/next/templates/init/lib/lifecycle-hooks/registry.ts +36 -1
  41. package/dist/assets/adapters/next/templates/init/lib/lifecycle-hooks/runner.ts +117 -12
  42. package/dist/assets/adapters/next/templates/init/lib/lifecycle-hooks/types.ts +35 -1
  43. package/dist/assets/adapters/next/templates/init/pages/audit-log/audit-log-detail-drawer.tsx +104 -0
  44. package/dist/assets/adapters/next/templates/init/pages/audit-log/audit-log-detail-row.tsx +15 -0
  45. package/dist/assets/adapters/next/templates/init/pages/audit-log/audit-log-page-content.tsx +565 -0
  46. package/dist/assets/adapters/next/templates/init/pages/audit-log/audit-log-page-skeleton.tsx +7 -0
  47. package/dist/assets/adapters/next/templates/init/pages/audit-log/audit-log-page.tsx +14 -0
  48. package/dist/assets/adapters/next/templates/init/pages/users/users-page-content.tsx +83 -2
  49. package/dist/assets/adapters/next/templates/init/pages/users/users-table.tsx +126 -78
  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 +7 -32
  52. package/dist/assets/adapters/next/templates/init/utils/form/list-field.ts +17 -0
  53. package/dist/assets/adapters/next/templates/init/utils/user/user.ts +70 -0
  54. package/dist/assets/shared-assets/react-admin/custom/content-editor/horizontal-rule-button.tsx +1 -1
  55. package/dist/assets/shared-assets/react-admin/custom/content-editor/index.tsx +6 -0
  56. package/dist/assets/shared-assets/react-admin/custom/content-editor/main-toolbar-content.tsx +1 -1
  57. package/dist/assets/shared-assets/react-admin/custom/content-editor/math-button.tsx +1 -1
  58. package/dist/assets/shared-assets/react-admin/custom/content-editor/math-editor-controls.tsx +27 -40
  59. package/dist/assets/shared-assets/react-admin/custom/content-editor/math-popover-button.tsx +0 -1
  60. package/dist/assets/shared-assets/react-admin/custom/content-editor/mobile-toolbar-content.tsx +4 -4
  61. package/dist/assets/shared-assets/react-admin/custom/content-editor/mode-toggle-button.tsx +1 -1
  62. package/dist/assets/shared-assets/react-admin/custom/content-editor/selection-bubble-menu.tsx +45 -8
  63. package/dist/assets/shared-assets/react-admin/custom/content-editor/slash-command-menu.tsx +153 -27
  64. package/dist/assets/shared-assets/react-admin/custom/content-editor/source-mode-dropdown-button.tsx +1 -1
  65. package/dist/assets/shared-assets/react-admin/custom/content-editor/source-mode.tsx +9 -11
  66. package/dist/assets/shared-assets/react-admin/custom/content-editor/table-bubble-menu.tsx +1 -1
  67. package/dist/assets/shared-assets/react-admin/custom/content-editor/table-button.tsx +24 -67
  68. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/blockquote-button/blockquote-button.tsx +1 -1
  69. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/code-block-button/code-block-button.tsx +1 -1
  70. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/color-highlight-popover/color-highlight-popover-button.tsx +1 -1
  71. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/color-highlight-popover/color-highlight-popover-content.tsx +1 -1
  72. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/color-highlight-popover/color-highlight-popover.tsx +11 -2
  73. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/color-highlight-popover/source-color-highlight-popover.tsx +1 -1
  74. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/heading-dropdown-menu/heading-dropdown-menu-item.tsx +1 -1
  75. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/heading-dropdown-menu/heading-dropdown-menu.tsx +9 -4
  76. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/link-popover/link-button.tsx +1 -1
  77. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/link-popover/link-control-popover.tsx +5 -12
  78. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/link-popover/link-main.tsx +19 -45
  79. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/link-popover/link-popover.tsx +2 -3
  80. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/list-dropdown-menu/list-dropdown-menu-item.tsx +1 -1
  81. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/list-dropdown-menu/list-dropdown-menu.tsx +9 -4
  82. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/mark-button/mark-button.tsx +1 -1
  83. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/undo-redo-button/undo-redo-button.tsx +1 -1
  84. package/dist/assets/shared-assets/react-admin/custom/date-range-picker.tsx +2 -2
  85. package/dist/assets/shared-assets/react-admin/custom/dynamic-list-field.tsx +20 -8
  86. package/dist/assets/shared-assets/react-admin/custom/media-gallery-field.tsx +1 -1
  87. package/dist/assets/shared-assets/react-admin/custom/nested-object-list-field.tsx +211 -0
  88. package/dist/assets/shared-assets/react-admin/schema.json +136 -23
  89. package/dist/assets/shared-assets/react-admin/ui/button.tsx +18 -18
  90. package/dist/assets/shared-assets/react-admin/ui/card.tsx +1 -1
  91. package/dist/assets/shared-assets/react-admin/ui/checkbox.tsx +1 -1
  92. package/dist/assets/shared-assets/react-admin/ui/combobox.tsx +27 -3
  93. package/dist/assets/shared-assets/react-admin/ui/drawer.tsx +9 -5
  94. package/dist/assets/shared-assets/react-admin/ui/dropdown-menu.tsx +27 -12
  95. package/dist/assets/shared-assets/react-admin/ui/form.tsx +1 -1
  96. package/dist/assets/shared-assets/react-admin/ui/sidebar.tsx +4 -4
  97. package/dist/cli.js +5396 -2385
  98. package/dist/cli.js.map +1 -1
  99. package/package.json +10 -2
@@ -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
+ }
@@ -1,12 +1,83 @@
1
1
  'use client'
2
2
 
3
+ import { EntityFiltersBar, type FilterDef } from '@admin/components/shared/entity-filters-bar'
3
4
  import { PageHeader } from '@admin/components/shared/page-header'
5
+ import { UserRole } from '@admin/types/auth'
6
+ import { useIsFetching } from '@tanstack/react-query'
7
+ import { parseAsInteger, parseAsString, useQueryState, useQueryStates } from 'nuqs'
4
8
  import React from 'react'
5
9
  import { columns } from './columns'
6
10
  import { CreateUserDialog } from './create-user-dialog'
7
11
  import { UsersTable } from './users-table'
8
12
 
9
13
  export function UsersPageContent() {
14
+ const isSearchFetching = useIsFetching({ queryKey: ['users'] }) > 0
15
+ const [search, setSearch] = useQueryState('q', parseAsString.withDefault(''))
16
+ const [, setPageIndex] = useQueryState('page', parseAsInteger.withDefault(0))
17
+ const [{ role, status }, setFilters] = useQueryStates({
18
+ role: parseAsString.withDefault(''),
19
+ status: parseAsString.withDefault('')
20
+ })
21
+ const activeFilterCount = [search, role, status].filter(Boolean).length
22
+ const filters = [
23
+ {
24
+ key: 'status',
25
+ label: 'Status',
26
+ type: 'select',
27
+ value: status,
28
+ options: [
29
+ { label: 'Verified', value: 'verified' },
30
+ { label: 'Unverified', value: 'unverified' }
31
+ ]
32
+ },
33
+ {
34
+ key: 'role',
35
+ label: 'Role',
36
+ type: 'select',
37
+ value: role,
38
+ options: [
39
+ { label: 'Admin', value: UserRole.ADMIN },
40
+ { label: 'Editor', value: UserRole.EDITOR },
41
+ { label: 'Member', value: UserRole.MEMBER }
42
+ ]
43
+ }
44
+ ] satisfies FilterDef[]
45
+
46
+ const handleSearch = React.useCallback(
47
+ (value: string | null) => {
48
+ React.startTransition(() => {
49
+ void setPageIndex(0)
50
+ void setSearch(value)
51
+ })
52
+ },
53
+ [setSearch, setPageIndex]
54
+ )
55
+
56
+ const handleFilterChange = React.useCallback(
57
+ (updates: Record<string, string | null>) => {
58
+ const nextFilters: { role?: string | null; status?: string | null } = {}
59
+ if ('role' in updates) nextFilters.role = updates.role
60
+ if ('status' in updates) nextFilters.status = updates.status
61
+
62
+ React.startTransition(() => {
63
+ void setPageIndex(0)
64
+ void setFilters(nextFilters)
65
+ })
66
+ },
67
+ [setFilters, setPageIndex]
68
+ )
69
+
70
+ const handleClearAll = React.useCallback(() => {
71
+ React.startTransition(() => {
72
+ void setPageIndex(0)
73
+ void setSearch(null)
74
+ void setFilters({
75
+ role: null,
76
+ status: null
77
+ })
78
+ })
79
+ }, [setFilters, setSearch, setPageIndex])
80
+
10
81
  return (
11
82
  <React.Fragment>
12
83
  <PageHeader
@@ -17,8 +88,18 @@ export function UsersPageContent() {
17
88
  </div>
18
89
  }
19
90
  />
20
- <main className="space-y-4 pt-4 pb-6 px-4">
21
- <UsersTable columns={columns} />
91
+ <EntityFiltersBar
92
+ filters={filters}
93
+ activeFilterCount={activeFilterCount}
94
+ onFilterChange={handleFilterChange}
95
+ onClearAll={handleClearAll}
96
+ search={search}
97
+ searchPlaceholder="Search users..."
98
+ isPending={isSearchFetching}
99
+ onSearch={handleSearch}
100
+ />
101
+ <main className="space-y-4 px-4 pb-4 flex-1">
102
+ <UsersTable columns={columns} search={search} status={status} role={role} />
22
103
  </main>
23
104
  </React.Fragment>
24
105
  )
@@ -1,64 +1,102 @@
1
1
  'use client'
2
2
 
3
3
  import { authClient } from '@admin/auth/client'
4
+ import {
5
+ DataGrid,
6
+ DataGridBody,
7
+ DataGridCell,
8
+ DataGridHead,
9
+ DataGridHeader,
10
+ DataGridRow
11
+ } from '@admin/components/shared/data-table/data-grid'
4
12
  import { DataTablePagination } from '@admin/components/shared/data-table/data-table-pagination'
5
13
  import { Card, CardContent } from '@admin/components/ui/card'
6
- import {
7
- Table,
8
- TableBody,
9
- TableCell,
10
- TableHead,
11
- TableHeader,
12
- TableRow
13
- } from '@admin/components/ui/table'
14
14
  import { useTableUtils } from '@admin/hooks/use-table-utils'
15
15
  import { useUsers } from '@admin/hooks/use-users'
16
16
  import type { User } from '@admin/types/auth'
17
17
  import { cn } from '@admin/utils/shared/cn'
18
- import {
19
- type ColumnDef,
20
- type ColumnFiltersState,
21
- flexRender,
22
- getCoreRowModel,
23
- getFilteredRowModel,
24
- getPaginationRowModel,
25
- getSortedRowModel,
26
- type SortingState,
27
- useReactTable
28
- } from '@tanstack/react-table'
18
+ import { getUsersPage } from '@admin/utils/user/user'
19
+ import { type ColumnDef, flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table'
29
20
  import { GripVertical } from 'lucide-react'
30
- import { parseAsInteger, useQueryState } from 'nuqs'
21
+ import { parseAsInteger, parseAsString, useQueryStates } from 'nuqs'
31
22
  import * as React from 'react'
32
23
 
33
- interface UsersTableProps<TValue> {
24
+ interface UsersTableProps<TValue> extends React.HTMLAttributes<HTMLDivElement> {
34
25
  columns: ColumnDef<User, TValue>[]
26
+ role?: string
27
+ search?: string
28
+ status?: string
35
29
  }
36
30
 
37
- export function UsersTable<TValue>({ columns }: UsersTableProps<TValue>) {
31
+ export function UsersTable<TValue>({
32
+ columns,
33
+ role,
34
+ search,
35
+ status,
36
+ className
37
+ }: UsersTableProps<TValue>) {
38
+ const defaultPageSize = 20
38
39
  const { data: session } = authClient.useSession()
39
40
  const { data, error, isPending } = useUsers()
40
- const [sorting, setSorting] = React.useState<SortingState>([])
41
- const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([])
42
- const [pageIndex, setPageIndex] = useQueryState('page', parseAsInteger.withDefault(0))
43
- const [pageSize, setPageSize] = useQueryState('size', parseAsInteger.withDefault(20))
44
- const effectivePageSize = pageSize === -1 ? Number.MAX_SAFE_INTEGER : pageSize
41
+ const [{ dir: sortDir, page: pageIndex, size: pageSize, sort: sortField }, setTableQuery] =
42
+ useQueryStates({
43
+ dir: parseAsString.withDefault(''),
44
+ page: parseAsInteger.withDefault(0),
45
+ size: parseAsInteger.withDefault(defaultPageSize),
46
+ sort: parseAsString.withDefault('')
47
+ })
48
+ const safePageIndex = Number.isFinite(pageIndex) && pageIndex > 0 ? pageIndex : 0
49
+ const safePageSize =
50
+ pageSize === -1 || (Number.isFinite(pageSize) && pageSize > 0) ? pageSize : defaultPageSize
51
+ const tablePage = React.useMemo(
52
+ () =>
53
+ getUsersPage(data?.users ?? [], {
54
+ pageIndex: safePageIndex,
55
+ pageSize: safePageSize,
56
+ role,
57
+ search,
58
+ sortDir: sortDir === 'desc' ? 'desc' : sortDir === 'asc' ? 'asc' : undefined,
59
+ sortField: sortField || undefined,
60
+ status
61
+ }),
62
+ [data?.users, role, safePageIndex, safePageSize, search, sortDir, sortField, status]
63
+ )
64
+ const tableData = tablePage.users
65
+ const totalRows = tablePage.total
66
+ const effectivePageSize = safePageSize === -1 ? totalRows : safePageSize
67
+ const pageCount = effectivePageSize > 0 ? Math.ceil(totalRows / effectivePageSize) : 1
68
+ const shouldShowPagination = totalRows > defaultPageSize
45
69
  const {
46
70
  columnSizing,
47
71
  handleColumnSizingChange,
48
72
  handlePaginationChange,
73
+ handleSortingChange,
49
74
  hoveredColumnId,
50
75
  registerTable,
51
76
  resetColumnSizingToContainer,
77
+ sorting,
52
78
  setHoveredColumnId,
53
79
  tableContainerRef
54
80
  } = useTableUtils<User>({
55
- pageIndex,
81
+ pageIndex: safePageIndex,
56
82
  pageSize: effectivePageSize,
57
- setPageIndex
83
+ setPageIndex: (nextPageIndex) => setTableQuery({ page: nextPageIndex }),
84
+ setSortingParams: ({
85
+ pageIndex: nextPageIndex,
86
+ sortDir: nextSortDir,
87
+ sortField: nextSortField
88
+ }) =>
89
+ setTableQuery({
90
+ dir: nextSortDir,
91
+ page: nextPageIndex,
92
+ sort: nextSortField
93
+ }),
94
+ sortDir,
95
+ sortField
58
96
  })
59
97
 
60
98
  const table = useReactTable({
61
- data: data?.users ?? [],
99
+ data: tableData,
62
100
  columns,
63
101
  columnResizeMode: 'onChange',
64
102
  defaultColumn: {
@@ -66,11 +104,11 @@ export function UsersTable<TValue>({ columns }: UsersTableProps<TValue>) {
66
104
  size: 220
67
105
  },
68
106
  getCoreRowModel: getCoreRowModel(),
69
- getPaginationRowModel: getPaginationRowModel(),
70
- onSortingChange: setSorting,
71
- getSortedRowModel: getSortedRowModel(),
72
- onColumnFiltersChange: setColumnFilters,
73
- getFilteredRowModel: getFilteredRowModel(),
107
+ manualPagination: true,
108
+ manualSorting: true,
109
+ pageCount,
110
+ rowCount: totalRows,
111
+ onSortingChange: handleSortingChange,
74
112
  onColumnSizingChange: handleColumnSizingChange,
75
113
  onPaginationChange: handlePaginationChange,
76
114
  meta: {
@@ -86,33 +124,31 @@ export function UsersTable<TValue>({ columns }: UsersTableProps<TValue>) {
86
124
  },
87
125
  state: {
88
126
  sorting,
89
- columnFilters,
90
127
  columnSizing,
91
128
  pagination: {
92
- pageIndex,
129
+ pageIndex: safePageIndex,
93
130
  pageSize: effectivePageSize
94
131
  }
95
132
  }
96
133
  })
97
134
  registerTable(table)
135
+ const gridTemplateColumns = table
136
+ .getVisibleLeafColumns()
137
+ .map((column) => `${column.getSize()}px`)
138
+ .join(' ')
98
139
 
99
140
  return (
100
- <div className="flex min-h-[calc(100svh-184px)] flex-col gap-6 pt-px pb-2">
141
+ <div className={cn('flex flex-col gap-6 pt-px pb-2 h-full', className)}>
101
142
  <Card className="min-h-0 flex-1 p-0">
102
143
  <CardContent ref={tableContainerRef} className="flex min-h-0 flex-1 flex-col p-0">
103
- <Table
144
+ <DataGrid
104
145
  containerClassName="min-h-0 flex-1"
105
- className="table-fixed"
146
+ gridTemplateColumns={gridTemplateColumns}
106
147
  style={{ width: table.getTotalSize() }}
107
148
  >
108
- <colgroup>
109
- {table.getVisibleLeafColumns().map((column) => (
110
- <col key={column.id} style={{ width: column.getSize() }} />
111
- ))}
112
- </colgroup>
113
- <TableHeader>
149
+ <DataGridHeader>
114
150
  {table.getHeaderGroups().map((headerGroup) => (
115
- <TableRow key={headerGroup.id}>
151
+ <DataGridRow key={headerGroup.id}>
116
152
  {headerGroup.headers.map((header, headerIndex) => {
117
153
  const resizeLabel =
118
154
  typeof header.column.columnDef.header === 'string'
@@ -122,7 +158,7 @@ export function UsersTable<TValue>({ columns }: UsersTableProps<TValue>) {
122
158
  const canResizeColumn =
123
159
  header.column.getCanResize() && nextHeader?.column.getCanResize() === true
124
160
  return (
125
- <TableHead
161
+ <DataGridHead
126
162
  key={header.id}
127
163
  className="group/resize-header relative overflow-hidden"
128
164
  data-column-id={header.column.id}
@@ -132,7 +168,6 @@ export function UsersTable<TValue>({ columns }: UsersTableProps<TValue>) {
132
168
  current === header.column.id ? null : current
133
169
  )
134
170
  }
135
- style={{ width: header.getSize() }}
136
171
  >
137
172
  {header.isPlaceholder
138
173
  ? null
@@ -159,30 +194,36 @@ export function UsersTable<TValue>({ columns }: UsersTableProps<TValue>) {
159
194
  />
160
195
  </button>
161
196
  ) : null}
162
- </TableHead>
197
+ </DataGridHead>
163
198
  )
164
199
  })}
165
- </TableRow>
200
+ </DataGridRow>
166
201
  ))}
167
- </TableHeader>
168
- <TableBody>
202
+ </DataGridHeader>
203
+ <DataGridBody>
169
204
  {isPending ? (
170
- <TableRow>
171
- <TableCell colSpan={columns.length} className="h-24 text-center">
205
+ <DataGridRow>
206
+ <DataGridCell
207
+ className="flex h-24 items-center justify-center text-center"
208
+ style={{ gridColumn: '1 / -1' }}
209
+ >
172
210
  <div className="text-muted-foreground">Loading users...</div>
173
- </TableCell>
174
- </TableRow>
211
+ </DataGridCell>
212
+ </DataGridRow>
175
213
  ) : error ? (
176
- <TableRow>
177
- <TableCell colSpan={columns.length} className="h-24 text-center">
214
+ <DataGridRow>
215
+ <DataGridCell
216
+ className="flex h-24 items-center justify-center text-center"
217
+ style={{ gridColumn: '1 / -1' }}
218
+ >
178
219
  <div className="text-destructive">Error: {error.message}</div>
179
- </TableCell>
180
- </TableRow>
220
+ </DataGridCell>
221
+ </DataGridRow>
181
222
  ) : table.getRowModel().rows?.length ? (
182
223
  table.getRowModel().rows.map((row) => (
183
- <TableRow key={row.id}>
224
+ <DataGridRow key={row.id} className="hover:bg-muted/50">
184
225
  {row.getVisibleCells().map((cell) => (
185
- <TableCell
226
+ <DataGridCell
186
227
  key={cell.id}
187
228
  data-column-id={cell.column.id}
188
229
  onMouseEnter={() => setHoveredColumnId(cell.column.id)}
@@ -191,31 +232,38 @@ export function UsersTable<TValue>({ columns }: UsersTableProps<TValue>) {
191
232
  current === cell.column.id ? null : current
192
233
  )
193
234
  }
194
- style={{ width: cell.column.getSize() }}
195
235
  >
196
236
  {flexRender(cell.column.columnDef.cell, cell.getContext())}
197
- </TableCell>
237
+ </DataGridCell>
198
238
  ))}
199
- </TableRow>
239
+ </DataGridRow>
200
240
  ))
201
241
  ) : (
202
- <TableRow>
203
- <TableCell colSpan={columns.length} className="h-24 text-center">
242
+ <DataGridRow>
243
+ <DataGridCell
244
+ className="flex h-24 items-center justify-center text-center"
245
+ style={{ gridColumn: '1 / -1' }}
246
+ >
204
247
  No users found.
205
- </TableCell>
206
- </TableRow>
248
+ </DataGridCell>
249
+ </DataGridRow>
207
250
  )}
208
- </TableBody>
209
- </Table>
251
+ </DataGridBody>
252
+ </DataGrid>
210
253
  </CardContent>
211
254
  </Card>
212
255
 
213
- <DataTablePagination
214
- table={table}
215
- paginationMode="uncontrolled"
216
- pageSize={pageSize}
217
- setPageSize={setPageSize}
218
- />
256
+ {shouldShowPagination ? (
257
+ <DataTablePagination
258
+ table={table}
259
+ paginationMode="controlled"
260
+ pageSize={safePageSize}
261
+ pageIndex={safePageIndex}
262
+ pageCount={pageCount}
263
+ setPageIndex={(nextPageIndex) => setTableQuery({ page: nextPageIndex })}
264
+ setPageSize={(nextPageSize) => setTableQuery({ page: 0, size: nextPageSize })}
265
+ />
266
+ ) : null}
219
267
  </div>
220
268
  )
221
269
  }
@@ -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
  }
@@ -662,8 +638,6 @@ export function findNodePosition(props: {
662
638
  let foundNode: PMNode | null = null
663
639
 
664
640
  editor.state.doc.descendants((currentNode, pos) => {
665
- // TODO: Needed?
666
- // if (currentNode.type && currentNode.type.name === node!.type.name) {
667
641
  if (currentNode === node) {
668
642
  foundPos = pos
669
643
  foundNode = currentNode
@@ -808,13 +782,14 @@ export function isAllowedUri(uri: string | undefined, protocols?: ProtocolConfig
808
782
 
809
783
  return (
810
784
  !uri ||
811
- uri.replace(ATTR_WHITESPACE, '').match(
812
- new RegExp(
813
- // eslint-disable-next-line no-useless-escape
814
- `^(?:(?:${allowedProtocols.join('|')}):|[^a-z]|[a-z0-9+.-]+(?:[^a-z+.-:]|$))`,
815
- 'i'
785
+ uri
786
+ .replace(ATTR_WHITESPACE, '')
787
+ .match(
788
+ new RegExp(
789
+ `^(?:(?:${allowedProtocols.join('|')}):|[^a-z]|[a-z0-9+.-]+(?:[^a-z+.-:]|$))`,
790
+ 'i'
791
+ )
816
792
  )
817
- )
818
793
  )
819
794
  }
820
795
 
@@ -0,0 +1,17 @@
1
+ export function cloneDefaultValue(value: unknown): unknown {
2
+ if (Array.isArray(value)) return value.map(cloneDefaultValue)
3
+
4
+ if (value && typeof value === 'object') {
5
+ const clonedEntries = Object.entries(value as Record<string, unknown>).map(
6
+ ([key, childValue]) => [key, cloneDefaultValue(childValue)] as const
7
+ )
8
+ return Object.fromEntries(clonedEntries)
9
+ }
10
+
11
+ return value
12
+ }
13
+
14
+ export function createListItemKey(): string {
15
+ const randomKey = Math.random().toString(36).slice(2)
16
+ return globalThis.crypto?.randomUUID?.() ?? randomKey
17
+ }
@@ -1,3 +1,17 @@
1
+ import type { User, UsersPage } from '@admin/types/auth'
2
+
3
+ interface GetUsersPageOptions {
4
+ pageIndex: number
5
+ pageSize: number
6
+ role?: string
7
+ search?: string
8
+ sortDir?: 'asc' | 'desc'
9
+ sortField?: string
10
+ status?: string
11
+ }
12
+
13
+ const USER_SORT_FIELDS = new Set(['createdAt', 'email', 'emailVerified', 'role'])
14
+
1
15
  export function getUserInitials(nameOrEmail: string): string {
2
16
  const parts = nameOrEmail.includes('@')
3
17
  ? nameOrEmail.split('@')[0].split(/[._-]/)
@@ -9,3 +23,59 @@ export function getUserInitials(nameOrEmail: string): string {
9
23
  .map((part) => part[0].toUpperCase())
10
24
  .join('')
11
25
  }
26
+
27
+ function normalizeUserFilterValue(value: string | null | undefined): string {
28
+ return (value ?? '').trim().toLowerCase()
29
+ }
30
+
31
+ function getUserSortValue(user: User, sortField: string): number | string {
32
+ if (sortField === 'createdAt') return Date.parse(user.createdAt)
33
+ if (sortField === 'emailVerified') return user.emailVerified ? 1 : 0
34
+ if (sortField === 'role') return user.role.toLowerCase()
35
+ return user.email.toLowerCase()
36
+ }
37
+
38
+ function compareUserSortValues(a: number | string, b: number | string): number {
39
+ if (typeof a === 'number' && typeof b === 'number') return a - b
40
+ return String(a).localeCompare(String(b))
41
+ }
42
+
43
+ export function getUsersPage(users: User[], options: GetUsersPageOptions): UsersPage {
44
+ const search = normalizeUserFilterValue(options.search)
45
+ const role = normalizeUserFilterValue(options.role)
46
+ const status = normalizeUserFilterValue(options.status)
47
+ const sortField =
48
+ options.sortField && USER_SORT_FIELDS.has(options.sortField) ? options.sortField : ''
49
+ const sortDirection = options.sortDir === 'desc' ? -1 : 1
50
+ const filteredUsers = users.filter((user) => {
51
+ const matchesSearch =
52
+ !search ||
53
+ user.email.toLowerCase().includes(search) ||
54
+ user.name.toLowerCase().includes(search)
55
+ const matchesRole = !role || user.role.toLowerCase() === role
56
+ const matchesStatus =
57
+ !status ||
58
+ (status === 'verified' && user.emailVerified) ||
59
+ (status === 'unverified' && !user.emailVerified)
60
+
61
+ return matchesSearch && matchesRole && matchesStatus
62
+ })
63
+
64
+ if (sortField) {
65
+ filteredUsers.sort((a, b) => {
66
+ const comparison = compareUserSortValues(
67
+ getUserSortValue(a, sortField),
68
+ getUserSortValue(b, sortField)
69
+ )
70
+
71
+ return comparison * sortDirection
72
+ })
73
+ }
74
+
75
+ const total = filteredUsers.length
76
+ const pageSize = options.pageSize === -1 ? total : Math.max(options.pageSize, 1)
77
+ const start = Math.max(options.pageIndex, 0) * pageSize
78
+ const pageUsers = pageSize > 0 ? filteredUsers.slice(start, start + pageSize) : filteredUsers
79
+
80
+ return { users: pageUsers, total }
81
+ }
@@ -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
  }