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
@@ -1,19 +1,7 @@
1
- import { PageHeader } from '@admin/components/shared/page-header'
2
- import { Skeleton } from '@admin/components/ui/skeleton'
3
-
4
1
  export function AuditLogPageSkeleton() {
5
2
  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
- </>
3
+ <div className="flex items-center justify-center h-48">
4
+ <div className="text-muted-foreground">Loading Audit Logs...</div>
5
+ </div>
18
6
  )
19
7
  }
@@ -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-xl pl-4 pr-2 py-2"
66
+ className="flex flex-col w-full bg-card border shadow-card border-border rounded-lg 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-xl border bg-card">
3
+ <div className="overflow-hidden rounded-lg 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-xl border bg-card">
3
+ <div className="overflow-hidden rounded-lg 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-xl border bg-card">
3
+ <div className="overflow-hidden rounded-lg 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">
@@ -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
  }
@@ -638,8 +638,6 @@ export function findNodePosition(props: {
638
638
  let foundNode: PMNode | null = null
639
639
 
640
640
  editor.state.doc.descendants((currentNode, pos) => {
641
- // TODO: Needed?
642
- // if (currentNode.type && currentNode.type.name === node!.type.name) {
643
641
  if (currentNode === node) {
644
642
  foundPos = pos
645
643
  foundNode = currentNode
@@ -784,13 +782,14 @@ export function isAllowedUri(uri: string | undefined, protocols?: ProtocolConfig
784
782
 
785
783
  return (
786
784
  !uri ||
787
- uri.replace(ATTR_WHITESPACE, '').match(
788
- new RegExp(
789
- // eslint-disable-next-line no-useless-escape
790
- `^(?:(?:${allowedProtocols.join('|')}):|[^a-z]|[a-z0-9+.-]+(?:[^a-z+.-:]|$))`,
791
- '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
+ )
792
792
  )
793
- )
794
793
  )
795
794
  }
796
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
+ }
@@ -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-xl border bg-popover p-0 text-popover-foreground shadow-menu [scrollbar-width:none] [&::-webkit-scrollbar]:hidden',
66
+ 'flex items-center gap-1 overflow-x-auto border-border rounded-lg 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
  >
@@ -72,7 +72,7 @@ export function SelectionBubbleMenu({ editor }: { editor: Editor }) {
72
72
  role="toolbar"
73
73
  aria-label="Link formatting toolbar"
74
74
  className={cn(
75
- 'flex items-center gap-1 overflow-x-auto border-border border bg-popover p-4 text-foreground shadow-2xl [scrollbar-width:none] [&::-webkit-scrollbar]:hidden rounded-xl',
75
+ 'flex items-center gap-1 overflow-x-auto border-border border bg-popover p-4 text-foreground shadow-2xl [scrollbar-width:none] [&::-webkit-scrollbar]:hidden rounded-lg',
76
76
  'relative z-10'
77
77
  )}
78
78
  >
@@ -105,7 +105,6 @@ export function SourceMode({
105
105
  isMarkdownLinkActive,
106
106
  markdownLinkUrl,
107
107
  openMediaGalleryAtCursor,
108
- openMarkdownLink,
109
108
  redoMarkdownChange,
110
109
  removeHighlight,
111
110
  removeMarkdownLink,
@@ -296,7 +295,6 @@ export function SourceMode({
296
295
  disabled={disabled}
297
296
  isActive={isMarkdownLinkActive}
298
297
  onOpenChange={handleMarkdownLinkPopoverOpenChange}
299
- openLink={openMarkdownLink}
300
298
  removeLink={removeMarkdownLink}
301
299
  setLink={setMarkdownLink}
302
300
  setUrl={setMarkdownLinkUrl}