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.
- package/dist/assets/adapters/next/plugins/blog/plugin.ts +1 -1
- package/dist/assets/adapters/next/plugins/blog/schemas/menus.json +155 -0
- package/dist/assets/adapters/next/plugins/blog/schemas/posts.json +2 -7
- package/dist/assets/adapters/next/templates/init/components/layouts/admin-sidebar.tsx +9 -3
- package/dist/assets/adapters/next/templates/init/components/layouts/sidebar-branding.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/components/shared/data-table/data-grid.tsx +107 -0
- package/dist/assets/adapters/next/templates/init/components/shared/data-table/data-table.tsx +58 -45
- package/dist/assets/adapters/next/templates/init/components/shared/dev-mode/lifecycle-hooks-tab.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/components/shared/dev-mode-integrate.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/components/shared/entity-filters-bar.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/components/shared/entity-versions/entity-versions-drawer.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/components/shared/page-header.tsx +20 -17
- package/dist/assets/adapters/next/templates/init/components/shared/sort-indicator.tsx +7 -3
- package/dist/assets/adapters/next/templates/init/data/navigation.ts +4 -1
- package/dist/assets/adapters/next/templates/init/hooks/content-editor/use-content-editor-slash-menu.ts +28 -2
- package/dist/assets/adapters/next/templates/init/hooks/content-editor/use-content-editor.ts +22 -1
- package/dist/assets/adapters/next/templates/init/hooks/content-editor/use-table-button.ts +85 -0
- package/dist/assets/adapters/next/templates/init/hooks/use-audit-log.ts +23 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/audit/cleanup-audit-logs.ts +63 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/audit/get-audit-logs.ts +122 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/audit/index.ts +19 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/audit/record-audit-event.ts +37 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/audit/register.ts +13 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/audit/request-metadata.ts +37 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/audit/types.ts +77 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/forms/upsert-form-settings.ts +3 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/media/create-media.ts +19 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/media/delete-media-bulk.ts +23 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/media/delete-media.ts +21 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/media/update-media.ts +23 -1
- package/dist/assets/adapters/next/templates/init/lib/actions/profile/update-email.ts +17 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/upload/upload-files.ts +3 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/upload/upload-media-from-url.ts +3 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/users/create-user.ts +19 -1
- package/dist/assets/adapters/next/templates/init/lib/actions/users/delete-user.ts +23 -1
- package/dist/assets/adapters/next/templates/init/lib/actions/users/update-user-role.ts +23 -1
- package/dist/assets/adapters/next/templates/init/lib/db/core/schema.ts +49 -0
- package/dist/assets/adapters/next/templates/init/lib/lifecycle-hooks/index.ts +6 -1
- package/dist/assets/adapters/next/templates/init/lib/lifecycle-hooks/register.ts +3 -0
- package/dist/assets/adapters/next/templates/init/lib/lifecycle-hooks/registry.ts +36 -1
- package/dist/assets/adapters/next/templates/init/lib/lifecycle-hooks/runner.ts +117 -12
- package/dist/assets/adapters/next/templates/init/lib/lifecycle-hooks/types.ts +35 -1
- package/dist/assets/adapters/next/templates/init/pages/audit-log/audit-log-detail-drawer.tsx +104 -0
- package/dist/assets/adapters/next/templates/init/pages/audit-log/audit-log-detail-row.tsx +15 -0
- package/dist/assets/adapters/next/templates/init/pages/audit-log/audit-log-page-content.tsx +565 -0
- package/dist/assets/adapters/next/templates/init/pages/audit-log/audit-log-page-skeleton.tsx +7 -0
- package/dist/assets/adapters/next/templates/init/pages/audit-log/audit-log-page.tsx +14 -0
- package/dist/assets/adapters/next/templates/init/pages/users/users-page-content.tsx +83 -2
- package/dist/assets/adapters/next/templates/init/pages/users/users-table.tsx +126 -78
- package/dist/assets/adapters/next/templates/init/utils/audit/audit.ts +54 -0
- package/dist/assets/adapters/next/templates/init/utils/editor/tiptap.ts +7 -32
- package/dist/assets/adapters/next/templates/init/utils/form/list-field.ts +17 -0
- package/dist/assets/adapters/next/templates/init/utils/user/user.ts +70 -0
- package/dist/assets/shared-assets/react-admin/custom/content-editor/horizontal-rule-button.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/content-editor/index.tsx +6 -0
- package/dist/assets/shared-assets/react-admin/custom/content-editor/main-toolbar-content.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/content-editor/math-button.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/content-editor/math-editor-controls.tsx +27 -40
- package/dist/assets/shared-assets/react-admin/custom/content-editor/math-popover-button.tsx +0 -1
- package/dist/assets/shared-assets/react-admin/custom/content-editor/mobile-toolbar-content.tsx +4 -4
- package/dist/assets/shared-assets/react-admin/custom/content-editor/mode-toggle-button.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/content-editor/selection-bubble-menu.tsx +45 -8
- package/dist/assets/shared-assets/react-admin/custom/content-editor/slash-command-menu.tsx +153 -27
- package/dist/assets/shared-assets/react-admin/custom/content-editor/source-mode-dropdown-button.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/content-editor/source-mode.tsx +9 -11
- package/dist/assets/shared-assets/react-admin/custom/content-editor/table-bubble-menu.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/content-editor/table-button.tsx +24 -67
- package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/blockquote-button/blockquote-button.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/code-block-button/code-block-button.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/color-highlight-popover/color-highlight-popover-button.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/color-highlight-popover/color-highlight-popover-content.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/color-highlight-popover/color-highlight-popover.tsx +11 -2
- package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/color-highlight-popover/source-color-highlight-popover.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/heading-dropdown-menu/heading-dropdown-menu-item.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/heading-dropdown-menu/heading-dropdown-menu.tsx +9 -4
- package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/link-popover/link-button.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/link-popover/link-control-popover.tsx +5 -12
- package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/link-popover/link-main.tsx +19 -45
- package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/link-popover/link-popover.tsx +2 -3
- package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/list-dropdown-menu/list-dropdown-menu-item.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/list-dropdown-menu/list-dropdown-menu.tsx +9 -4
- package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/mark-button/mark-button.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/undo-redo-button/undo-redo-button.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/date-range-picker.tsx +2 -2
- package/dist/assets/shared-assets/react-admin/custom/dynamic-list-field.tsx +20 -8
- package/dist/assets/shared-assets/react-admin/custom/media-gallery-field.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/nested-object-list-field.tsx +211 -0
- package/dist/assets/shared-assets/react-admin/schema.json +136 -23
- package/dist/assets/shared-assets/react-admin/ui/button.tsx +18 -18
- package/dist/assets/shared-assets/react-admin/ui/card.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/checkbox.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/combobox.tsx +27 -3
- package/dist/assets/shared-assets/react-admin/ui/drawer.tsx +9 -5
- package/dist/assets/shared-assets/react-admin/ui/dropdown-menu.tsx +27 -12
- package/dist/assets/shared-assets/react-admin/ui/form.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/sidebar.tsx +4 -4
- package/dist/cli.js +5396 -2385
- package/dist/cli.js.map +1 -1
- 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
|
-
<
|
|
21
|
-
|
|
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
|
-
|
|
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,
|
|
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>({
|
|
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 [
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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:
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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=
|
|
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
|
-
<
|
|
144
|
+
<DataGrid
|
|
104
145
|
containerClassName="min-h-0 flex-1"
|
|
105
|
-
|
|
146
|
+
gridTemplateColumns={gridTemplateColumns}
|
|
106
147
|
style={{ width: table.getTotalSize() }}
|
|
107
148
|
>
|
|
108
|
-
<
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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
|
-
</
|
|
197
|
+
</DataGridHead>
|
|
163
198
|
)
|
|
164
199
|
})}
|
|
165
|
-
</
|
|
200
|
+
</DataGridRow>
|
|
166
201
|
))}
|
|
167
|
-
</
|
|
168
|
-
<
|
|
202
|
+
</DataGridHeader>
|
|
203
|
+
<DataGridBody>
|
|
169
204
|
{isPending ? (
|
|
170
|
-
<
|
|
171
|
-
<
|
|
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
|
-
</
|
|
174
|
-
</
|
|
211
|
+
</DataGridCell>
|
|
212
|
+
</DataGridRow>
|
|
175
213
|
) : error ? (
|
|
176
|
-
<
|
|
177
|
-
<
|
|
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
|
-
</
|
|
180
|
-
</
|
|
220
|
+
</DataGridCell>
|
|
221
|
+
</DataGridRow>
|
|
181
222
|
) : table.getRowModel().rows?.length ? (
|
|
182
223
|
table.getRowModel().rows.map((row) => (
|
|
183
|
-
<
|
|
224
|
+
<DataGridRow key={row.id} className="hover:bg-muted/50">
|
|
184
225
|
{row.getVisibleCells().map((cell) => (
|
|
185
|
-
<
|
|
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
|
-
</
|
|
237
|
+
</DataGridCell>
|
|
198
238
|
))}
|
|
199
|
-
</
|
|
239
|
+
</DataGridRow>
|
|
200
240
|
))
|
|
201
241
|
) : (
|
|
202
|
-
<
|
|
203
|
-
<
|
|
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
|
-
</
|
|
206
|
-
</
|
|
248
|
+
</DataGridCell>
|
|
249
|
+
</DataGridRow>
|
|
207
250
|
)}
|
|
208
|
-
</
|
|
209
|
-
</
|
|
251
|
+
</DataGridBody>
|
|
252
|
+
</DataGrid>
|
|
210
253
|
</CardContent>
|
|
211
254
|
</Card>
|
|
212
255
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
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
|
+
}
|