betterstart-cli 0.0.12 → 0.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/adapters/next/plugins/blog/plugin.ts +1 -1
- package/dist/assets/adapters/next/plugins/blog/schemas/menus.json +115 -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 +2 -2
- package/dist/assets/adapters/next/templates/init/components/shared/dev-mode/lifecycle-hooks-tab.tsx +2 -2
- package/dist/assets/adapters/next/templates/init/components/shared/dev-mode/snippets-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-versions/entity-versions-drawer.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/components/shared/media/edit-media-dialog-content.tsx +2 -2
- package/dist/assets/adapters/next/templates/init/components/shared/media/media-url-importer.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/components/shared/page-header.tsx +20 -17
- package/dist/assets/adapters/next/templates/init/data/navigation.ts +2 -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/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 +97 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/audit/index.ts +17 -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 +72 -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 +116 -12
- package/dist/assets/adapters/next/templates/init/lib/lifecycle-hooks/types.ts +33 -0
- package/dist/assets/adapters/next/templates/init/pages/audit-log/audit-log-detail-drawer.tsx +96 -0
- package/dist/assets/adapters/next/templates/init/pages/audit-log/audit-log-detail-row.tsx +17 -0
- package/dist/assets/adapters/next/templates/init/pages/audit-log/audit-log-filters.tsx +152 -0
- package/dist/assets/adapters/next/templates/init/pages/audit-log/audit-log-page-content.tsx +279 -0
- package/dist/assets/adapters/next/templates/init/pages/audit-log/audit-log-page-skeleton.tsx +19 -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/dashboard-page.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/pages/forgot-password-page-skeleton.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/pages/login-page-skeleton.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/pages/reset-password-page-skeleton.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/utils/audit/audit.ts +54 -0
- package/dist/assets/adapters/next/templates/init/utils/editor/tiptap.ts +0 -24
- 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-bubble-menu.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 -9
- 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 +9 -4
- 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 +2 -4
- 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 +1 -1
- 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 +3 -3
- package/dist/assets/shared-assets/react-admin/custom/gallery-field.tsx +2 -2
- package/dist/assets/shared-assets/react-admin/custom/media-gallery-field.tsx +2 -2
- package/dist/assets/shared-assets/react-admin/custom/placeholder-card.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/placeholder.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/sortable-gallery-item.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/schema.json +0 -23
- package/dist/assets/shared-assets/react-admin/ui/accordion.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/alert.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/button.tsx +18 -18
- package/dist/assets/shared-assets/react-admin/ui/card.tsx +3 -3
- package/dist/assets/shared-assets/react-admin/ui/chart.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/combobox.tsx +29 -5
- package/dist/assets/shared-assets/react-admin/ui/command.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/context-menu.tsx +6 -6
- package/dist/assets/shared-assets/react-admin/ui/drawer.tsx +7 -3
- package/dist/assets/shared-assets/react-admin/ui/dropdown-menu.tsx +32 -17
- package/dist/assets/shared-assets/react-admin/ui/empty.tsx +2 -2
- package/dist/assets/shared-assets/react-admin/ui/field.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/hover-card.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/input-group.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/item.tsx +2 -2
- package/dist/assets/shared-assets/react-admin/ui/menubar.tsx +7 -7
- package/dist/assets/shared-assets/react-admin/ui/navigation-menu.tsx +4 -4
- package/dist/assets/shared-assets/react-admin/ui/popover.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/resizable.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/select.tsx +2 -2
- package/dist/assets/shared-assets/react-admin/ui/sidebar.tsx +3 -3
- package/dist/assets/shared-assets/react-admin/ui/skeleton.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/tabs.tsx +2 -2
- package/dist/assets/shared-assets/react-admin/ui/textarea.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/tooltip.tsx +1 -1
- package/dist/cli.js +2769 -802
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
|
@@ -8,7 +8,7 @@ export const blogPlugin: BetterstartPluginDefinition = {
|
|
|
8
8
|
version: '1.0.0',
|
|
9
9
|
dependencies: [],
|
|
10
10
|
conflicts: [],
|
|
11
|
-
schemaFiles: ['posts.json'],
|
|
11
|
+
schemaFiles: ['posts.json', 'menus.json'],
|
|
12
12
|
files: [],
|
|
13
13
|
packageDependencies: ['katex', 'markdown-it@^13.0.2', 'markdown-it-dollarmath', 'sanitize-html'],
|
|
14
14
|
devDependencies: ['@types/markdown-it@13', '@types/sanitize-html'],
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "menus",
|
|
3
|
+
"label": "Menus",
|
|
4
|
+
"description": "Manage Menus",
|
|
5
|
+
"icon": "Menu",
|
|
6
|
+
"navGroup": {
|
|
7
|
+
"label": "Collections",
|
|
8
|
+
"position": 0
|
|
9
|
+
},
|
|
10
|
+
"navPosition": 1,
|
|
11
|
+
"slot": {
|
|
12
|
+
"main": {
|
|
13
|
+
"fields": [
|
|
14
|
+
{
|
|
15
|
+
"name": "title",
|
|
16
|
+
"type": "string",
|
|
17
|
+
"label": "Title",
|
|
18
|
+
"required": true
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"name": "slug",
|
|
22
|
+
"type": "string",
|
|
23
|
+
"label": "Slug",
|
|
24
|
+
"hint": "URL-friendly identifier (auto-generated from title)"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"name": "items",
|
|
28
|
+
"type": "list",
|
|
29
|
+
"label": "Items",
|
|
30
|
+
"hint": "Items in the navigation menu",
|
|
31
|
+
"fields": [
|
|
32
|
+
{
|
|
33
|
+
"name": "label",
|
|
34
|
+
"type": "string",
|
|
35
|
+
"label": "Label",
|
|
36
|
+
"required": true
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"name": "href",
|
|
40
|
+
"type": "string",
|
|
41
|
+
"label": "Href",
|
|
42
|
+
"required": true
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"name": "openInNewTab",
|
|
46
|
+
"type": "boolean",
|
|
47
|
+
"label": "Open in New Tab",
|
|
48
|
+
"hint": "Open this link in a new browser tab",
|
|
49
|
+
"default": false
|
|
50
|
+
}
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
]
|
|
54
|
+
},
|
|
55
|
+
"sidebar": {
|
|
56
|
+
"fields": [
|
|
57
|
+
{
|
|
58
|
+
"name": "generalSidebar",
|
|
59
|
+
"type": "section",
|
|
60
|
+
"heading": {
|
|
61
|
+
"icon": "bolt",
|
|
62
|
+
"title": "General"
|
|
63
|
+
},
|
|
64
|
+
"fields": [
|
|
65
|
+
{
|
|
66
|
+
"name": "description",
|
|
67
|
+
"type": "text",
|
|
68
|
+
"label": "Description",
|
|
69
|
+
"hint": "Internal note describing this menu's purpose"
|
|
70
|
+
}
|
|
71
|
+
]
|
|
72
|
+
}
|
|
73
|
+
]
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
"columns": [
|
|
77
|
+
{
|
|
78
|
+
"accessorKey": "title",
|
|
79
|
+
"header": "Title",
|
|
80
|
+
"type": "text",
|
|
81
|
+
"sortable": true,
|
|
82
|
+
"size": "xl"
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"accessorKey": "published",
|
|
86
|
+
"header": "Status",
|
|
87
|
+
"type": "boolean",
|
|
88
|
+
"sortable": true,
|
|
89
|
+
"size": "sm"
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"accessorKey": "updatedAt",
|
|
93
|
+
"header": "Updated",
|
|
94
|
+
"type": "date",
|
|
95
|
+
"sortable": true,
|
|
96
|
+
"size": "sm"
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"accessorKey": "createdAt",
|
|
100
|
+
"header": "Created",
|
|
101
|
+
"type": "date",
|
|
102
|
+
"sortable": true,
|
|
103
|
+
"size": "sm"
|
|
104
|
+
}
|
|
105
|
+
],
|
|
106
|
+
"actions": {
|
|
107
|
+
"create": true,
|
|
108
|
+
"edit": true,
|
|
109
|
+
"delete": true,
|
|
110
|
+
"draft": true
|
|
111
|
+
},
|
|
112
|
+
"search": {
|
|
113
|
+
"fields": ["title"]
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "posts",
|
|
3
3
|
"label": "Posts",
|
|
4
|
-
"description": "Manage blog posts
|
|
4
|
+
"description": "Manage blog posts",
|
|
5
5
|
"icon": "FileText",
|
|
6
6
|
"navGroup": {
|
|
7
|
-
"label": "
|
|
7
|
+
"label": "Collections",
|
|
8
8
|
"position": 0
|
|
9
9
|
},
|
|
10
10
|
"navPosition": 0,
|
|
@@ -160,10 +160,5 @@
|
|
|
160
160
|
},
|
|
161
161
|
"search": {
|
|
162
162
|
"fields": ["title", "excerpt"]
|
|
163
|
-
},
|
|
164
|
-
"autoSlugify": {
|
|
165
|
-
"enabled": true,
|
|
166
|
-
"sourceField": "title",
|
|
167
|
-
"targetField": "slug"
|
|
168
163
|
}
|
|
169
164
|
}
|
|
@@ -35,7 +35,7 @@ export function AdminSidebar(props: ComponentProps<typeof Sidebar>) {
|
|
|
35
35
|
<Fragment key={group.label ?? '_ungrouped'}>
|
|
36
36
|
<SidebarGroup className="py-0">
|
|
37
37
|
{group.label && <SidebarGroupLabel>{group.label}</SidebarGroupLabel>}
|
|
38
|
-
<SidebarMenu className="gap-
|
|
38
|
+
<SidebarMenu className="gap-0.5">
|
|
39
39
|
{group.items.map((item) => (
|
|
40
40
|
<SidebarMenuItem key={item.href}>
|
|
41
41
|
<SidebarNavLink href={item.href}>
|
|
@@ -51,7 +51,7 @@ export function AdminSidebar(props: ComponentProps<typeof Sidebar>) {
|
|
|
51
51
|
|
|
52
52
|
<SidebarGroup className="mt-auto gap-2 px-0">
|
|
53
53
|
<Separator />
|
|
54
|
-
<SidebarMenu className="px-2 gap-
|
|
54
|
+
<SidebarMenu className="px-2 gap-0.5">
|
|
55
55
|
<SidebarMenuItem>
|
|
56
56
|
<SidebarNavLink href="/admin/users">
|
|
57
57
|
<Users strokeWidth={2} />
|
package/dist/assets/adapters/next/templates/init/components/shared/dev-mode/lifecycle-hooks-tab.tsx
CHANGED
|
@@ -53,14 +53,14 @@ export function LifecycleHooksTab({ lifecycleHooks }: LifecycleHooksTabProps) {
|
|
|
53
53
|
|
|
54
54
|
if (!firstHook) {
|
|
55
55
|
return (
|
|
56
|
-
<div className="flex min-h-80 items-center justify-center rounded-
|
|
56
|
+
<div className="flex min-h-80 items-center justify-center rounded-xl border border-dashed border-border px-6 text-center text-sm text-muted-foreground">
|
|
57
57
|
Lifecycle hook examples are generated for entity schemas in v1.
|
|
58
58
|
</div>
|
|
59
59
|
)
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
return (
|
|
63
|
-
<Accordion type="single" defaultValue={firstHook.id} className="min-h-0 shrink-0 rounded-
|
|
63
|
+
<Accordion type="single" defaultValue={firstHook.id} className="min-h-0 shrink-0 rounded-xl">
|
|
64
64
|
{lifecycleHooks.map((hook) => {
|
|
65
65
|
const HookIcon = LIFECYCLE_HOOK_ICONS[hook.icon]
|
|
66
66
|
const phaseLabel = LIFECYCLE_HOOK_PHASE_LABELS[hook.phase]
|
package/dist/assets/adapters/next/templates/init/components/shared/dev-mode/snippets-tab.tsx
CHANGED
|
@@ -56,7 +56,7 @@ export function SnippetsTab({ snippets }: SnippetsTabProps) {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
return (
|
|
59
|
-
<Accordion type="single" defaultValue={firstSnippet.id} className="min-h-0 shrink-0 rounded-
|
|
59
|
+
<Accordion type="single" defaultValue={firstSnippet.id} className="min-h-0 shrink-0 rounded-xl">
|
|
60
60
|
{snippets.map((snippet) => {
|
|
61
61
|
const SnippetIcon = SNIPPET_ICONS[snippet.icon]
|
|
62
62
|
const snippetKindLabel = SNIPPET_KIND_LABELS[snippet.kind]
|
|
@@ -50,7 +50,7 @@ export function DevModeIntegrateButton({
|
|
|
50
50
|
<DrawerTrigger asChild>
|
|
51
51
|
<Button type="button" variant="outline">
|
|
52
52
|
<Code2 data-icon="inline-start" />
|
|
53
|
-
<span className="hidden sm:inline">
|
|
53
|
+
<span className="hidden sm:inline">Dev Mode</span>
|
|
54
54
|
</Button>
|
|
55
55
|
</DrawerTrigger>
|
|
56
56
|
<DrawerContent className="w-full overflow-hidden data-[vaul-drawer-direction=right]:sm:max-w-2xl data-[vaul-drawer-direction=right]:lg:max-w-4xl">
|
|
@@ -44,7 +44,7 @@ export function EntityVersionsDrawer({
|
|
|
44
44
|
return (
|
|
45
45
|
<Drawer direction="right" open={open} onOpenChange={onOpenChange} modal>
|
|
46
46
|
<DrawerContent className="h-full sm:max-w-lg">
|
|
47
|
-
<DrawerHeader
|
|
47
|
+
<DrawerHeader>
|
|
48
48
|
<DrawerTitle>Versions</DrawerTitle>
|
|
49
49
|
<DrawerDescription>
|
|
50
50
|
Review previous {entityType} versions and restore a saved state.
|
|
@@ -93,7 +93,7 @@ export function EditMediaDialogContent({
|
|
|
93
93
|
<DrawerContent className="h-full w-full overflow-hidden">
|
|
94
94
|
<div
|
|
95
95
|
ref={handleDrawerContainerRef}
|
|
96
|
-
className="pointer-events-none absolute inset-3 z-20 overflow-hidden rounded-
|
|
96
|
+
className="pointer-events-none absolute inset-3 z-20 overflow-hidden rounded-xl"
|
|
97
97
|
/>
|
|
98
98
|
<div className="relative z-10 flex min-h-0 flex-1 flex-col overflow-hidden">
|
|
99
99
|
<DrawerHeader>
|
|
@@ -107,7 +107,7 @@ export function EditMediaDialogContent({
|
|
|
107
107
|
>
|
|
108
108
|
<Card>
|
|
109
109
|
<CardContent className="p-0">
|
|
110
|
-
<div className="relative aspect-video overflow-hidden rounded-
|
|
110
|
+
<div className="relative aspect-video overflow-hidden rounded-xl bg-background">
|
|
111
111
|
<MediaPreview media={media} showMetadata />
|
|
112
112
|
</div>
|
|
113
113
|
</CardContent>
|
package/dist/assets/adapters/next/templates/init/components/shared/media/media-url-importer.tsx
CHANGED
|
@@ -90,7 +90,7 @@ export function MediaUrlImporter({ onImportFromUrl, accept, className }: MediaUr
|
|
|
90
90
|
return (
|
|
91
91
|
<div
|
|
92
92
|
className={cn(
|
|
93
|
-
'flex items-center justify-center transition-colors rounded-
|
|
93
|
+
'flex items-center justify-center transition-colors rounded-xl border bg-input border-border overflow-hidden h-full flex-col p-5 gap-5',
|
|
94
94
|
className
|
|
95
95
|
)}
|
|
96
96
|
>
|
|
@@ -22,22 +22,25 @@ export function PageHeader({
|
|
|
22
22
|
const router = useRouter()
|
|
23
23
|
|
|
24
24
|
return (
|
|
25
|
-
|
|
26
|
-
<
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
<
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
<
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
25
|
+
<>
|
|
26
|
+
<header className="fixed top-0 border-t right-0 left-(--admin-shell-content-left) z-20 grid h-14 grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] items-center border-b border-border/80 px-4 bg-sidebar transition-[left] duration-200 ease-linear">
|
|
27
|
+
<div className="flex w-full items-center justify-start gap-2">
|
|
28
|
+
{hasBackButton && (
|
|
29
|
+
<Button variant="ghost" size="icon" onClick={() => router.back()}>
|
|
30
|
+
<ChevronLeft className="size-4" />
|
|
31
|
+
</Button>
|
|
32
|
+
)}
|
|
33
|
+
</div>
|
|
34
|
+
<div className="flex min-w-0 items-center justify-center gap-2">
|
|
35
|
+
<h2 className="truncate text-sm font-medium tracking-tight">{title}</h2>
|
|
36
|
+
</div>
|
|
37
|
+
<div className="flex w-full min-w-0 items-center justify-end gap-2">
|
|
38
|
+
{children && children}
|
|
39
|
+
{search && search}
|
|
40
|
+
{actions && actions}
|
|
41
|
+
</div>
|
|
42
|
+
</header>
|
|
43
|
+
<div className="h-14 shrink-0" aria-hidden="true" />
|
|
44
|
+
</>
|
|
42
45
|
)
|
|
43
46
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AdminNavigationItem } from '@admin/types/navigation'
|
|
2
2
|
import { House, ImagePlay } from 'lucide-react'
|
|
3
|
+
import { menusNav } from './navigation/menus'
|
|
3
4
|
import { postsNav } from './navigation/posts'
|
|
4
5
|
|
|
5
6
|
export type { AdminNavigationItem } from '@admin/types/navigation'
|
|
@@ -26,7 +27,7 @@ function comparePositions(a?: number, b?: number): number {
|
|
|
26
27
|
return 0
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
const combinedNavigation = [...coreNav, ...postsNav]
|
|
30
|
+
const combinedNavigation = [...coreNav, ...menusNav, ...postsNav]
|
|
30
31
|
|
|
31
32
|
const dashboard = combinedNavigation.find((item) => item.href === '/admin')
|
|
32
33
|
const others = combinedNavigation.filter((item) => item.href !== '/admin')
|
|
@@ -15,12 +15,13 @@ import * as React from 'react'
|
|
|
15
15
|
|
|
16
16
|
export function useContentEditorSlashMenu(editorRef: React.RefObject<Editor | null>) {
|
|
17
17
|
const [slashMenu, setSlashMenu] = React.useState<SlashMenuState | null>(null)
|
|
18
|
+
const [slashSearchQuery, setSlashSearchQuery] = React.useState('')
|
|
18
19
|
const [activeSlashIndexState, setActiveSlashIndexState] = React.useState(0)
|
|
19
20
|
const slashMenuRef = React.useRef<SlashMenuState | null>(null)
|
|
20
21
|
const activeSlashIndexRef = React.useRef(0)
|
|
21
22
|
const filteredSlashCommands = React.useMemo(
|
|
22
|
-
() => getFilteredSlashCommands(
|
|
23
|
-
[
|
|
23
|
+
() => getFilteredSlashCommands(slashSearchQuery),
|
|
24
|
+
[slashSearchQuery]
|
|
24
25
|
)
|
|
25
26
|
const filteredSlashCommandsRef = React.useRef(filteredSlashCommands)
|
|
26
27
|
const activeSlashIndex =
|
|
@@ -37,6 +38,23 @@ export function useContentEditorSlashMenu(editorRef: React.RefObject<Editor | nu
|
|
|
37
38
|
isSameSlashMenuState(current, nextSlashMenu) ? current : nextSlashMenu
|
|
38
39
|
)
|
|
39
40
|
|
|
41
|
+
setSlashSearchQuery((current) => {
|
|
42
|
+
const currentSlashMenu = slashMenuRef.current
|
|
43
|
+
|
|
44
|
+
if (!nextSlashMenu) return ''
|
|
45
|
+
|
|
46
|
+
if (
|
|
47
|
+
currentSlashMenu &&
|
|
48
|
+
currentSlashMenu.from === nextSlashMenu.from &&
|
|
49
|
+
currentSlashMenu.to === nextSlashMenu.to &&
|
|
50
|
+
currentSlashMenu.query === nextSlashMenu.query
|
|
51
|
+
) {
|
|
52
|
+
return current
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return nextSlashMenu.query
|
|
56
|
+
})
|
|
57
|
+
|
|
40
58
|
setActiveSlashIndexState((current) =>
|
|
41
59
|
slashMenuRef.current?.query === nextSlashMenu?.query && nextSlashMenu ? current : 0
|
|
42
60
|
)
|
|
@@ -44,6 +62,12 @@ export function useContentEditorSlashMenu(editorRef: React.RefObject<Editor | nu
|
|
|
44
62
|
|
|
45
63
|
const closeSlashMenu = React.useCallback(() => {
|
|
46
64
|
setSlashMenu(null)
|
|
65
|
+
setSlashSearchQuery('')
|
|
66
|
+
setActiveSlashIndexState(0)
|
|
67
|
+
}, [])
|
|
68
|
+
|
|
69
|
+
const setSlashSearch = React.useCallback((query: string) => {
|
|
70
|
+
setSlashSearchQuery(query)
|
|
47
71
|
setActiveSlashIndexState(0)
|
|
48
72
|
}, [])
|
|
49
73
|
|
|
@@ -110,7 +134,9 @@ export function useContentEditorSlashMenu(editorRef: React.RefObject<Editor | nu
|
|
|
110
134
|
handleSlashKeyDown,
|
|
111
135
|
selectSlashCommand,
|
|
112
136
|
setActiveSlashIndex: setActiveSlashIndexState,
|
|
137
|
+
setSlashSearch,
|
|
113
138
|
slashMenu,
|
|
139
|
+
slashSearchQuery,
|
|
114
140
|
updateSlashMenu
|
|
115
141
|
}
|
|
116
142
|
}
|
|
@@ -66,6 +66,7 @@ export function useContentEditor({
|
|
|
66
66
|
const isInternalChange = React.useRef(false)
|
|
67
67
|
const isSyncingRichContent = React.useRef(false)
|
|
68
68
|
const editorRef = React.useRef<Editor | null>(null)
|
|
69
|
+
const isSlashMenuOpenRef = React.useRef(false)
|
|
69
70
|
const {
|
|
70
71
|
activeSlashIndex,
|
|
71
72
|
closeSlashMenu,
|
|
@@ -73,10 +74,14 @@ export function useContentEditor({
|
|
|
73
74
|
handleSlashKeyDown,
|
|
74
75
|
selectSlashCommand,
|
|
75
76
|
setActiveSlashIndex,
|
|
77
|
+
setSlashSearch,
|
|
76
78
|
slashMenu,
|
|
79
|
+
slashSearchQuery,
|
|
77
80
|
updateSlashMenu
|
|
78
81
|
} = useContentEditorSlashMenu(editorRef)
|
|
79
82
|
|
|
83
|
+
isSlashMenuOpenRef.current = slashMenu !== null
|
|
84
|
+
|
|
80
85
|
if (value !== lastValuePropRef.current) {
|
|
81
86
|
lastValuePropRef.current = value
|
|
82
87
|
latestMarkdownRef.current = value
|
|
@@ -158,7 +163,20 @@ export function useContentEditor({
|
|
|
158
163
|
updateSlashMenu(nextEditor)
|
|
159
164
|
},
|
|
160
165
|
onSelectionUpdate: ({ editor: nextEditor }) => updateSlashMenu(nextEditor),
|
|
161
|
-
onBlur:
|
|
166
|
+
onBlur: ({ event }) => {
|
|
167
|
+
const nextFocusTarget = event.relatedTarget
|
|
168
|
+
|
|
169
|
+
if (isSlashMenuOpenRef.current && !nextFocusTarget) return
|
|
170
|
+
|
|
171
|
+
if (
|
|
172
|
+
nextFocusTarget instanceof Element &&
|
|
173
|
+
nextFocusTarget.closest('[data-content-editor-slash-menu]')
|
|
174
|
+
) {
|
|
175
|
+
return
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
closeSlashMenu()
|
|
179
|
+
}
|
|
162
180
|
})
|
|
163
181
|
|
|
164
182
|
editorRef.current = editor
|
|
@@ -278,9 +296,12 @@ export function useContentEditor({
|
|
|
278
296
|
handleSourceChange,
|
|
279
297
|
handleSourceSelectionChange,
|
|
280
298
|
mode,
|
|
299
|
+
closeSlashMenu,
|
|
281
300
|
selectSlashCommand,
|
|
282
301
|
setActiveSlashIndex,
|
|
302
|
+
setSlashSearch,
|
|
283
303
|
slashMenu,
|
|
304
|
+
slashSearchQuery,
|
|
284
305
|
sourceValue: latestMarkdownRef.current,
|
|
285
306
|
sourceSelectionRequest,
|
|
286
307
|
syncRichContent
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import type { AuditLogPage, AuditLogQuery, CleanupAuditLogsResult } from '@admin/actions/audit'
|
|
4
|
+
import { cleanupAuditLogs, getAuditLogs } from '@admin/actions/audit'
|
|
5
|
+
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
|
6
|
+
|
|
7
|
+
export function useAuditLogs(query: AuditLogQuery) {
|
|
8
|
+
return useQuery<AuditLogPage>({
|
|
9
|
+
queryKey: ['audit-log', query],
|
|
10
|
+
queryFn: () => getAuditLogs(query)
|
|
11
|
+
})
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function useCleanupAuditLogs() {
|
|
15
|
+
const queryClient = useQueryClient()
|
|
16
|
+
|
|
17
|
+
return useMutation<CleanupAuditLogsResult>({
|
|
18
|
+
mutationFn: () => cleanupAuditLogs(),
|
|
19
|
+
onSuccess: () => {
|
|
20
|
+
void queryClient.invalidateQueries({ queryKey: ['audit-log'] })
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
'use server'
|
|
2
|
+
|
|
3
|
+
import { getSetting } from '@admin/actions/settings'
|
|
4
|
+
import { requireRole, UserRole } from '@admin/auth/middleware'
|
|
5
|
+
import db from '@admin/db'
|
|
6
|
+
import { auditLog } from '@admin/db/schema'
|
|
7
|
+
import { runAdminEventHooks } from '@admin/lib/lifecycle-hooks'
|
|
8
|
+
import { lt } from 'drizzle-orm'
|
|
9
|
+
import { getAuditRequestMetadata } from './request-metadata'
|
|
10
|
+
import type { CleanupAuditLogsResult } from './types'
|
|
11
|
+
|
|
12
|
+
const MS_PER_DAY = 24 * 60 * 60 * 1000
|
|
13
|
+
|
|
14
|
+
function getRetentionDays(value: unknown): number | null {
|
|
15
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) return null
|
|
16
|
+
const days = Math.floor(value)
|
|
17
|
+
return days > 0 ? days : null
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function cleanupAuditLogs(): Promise<CleanupAuditLogsResult> {
|
|
21
|
+
const actor = await requireRole([UserRole.ADMIN])
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const settings = await getSetting()
|
|
25
|
+
const retentionEnabled = settings?.auditRetentionEnabled === true
|
|
26
|
+
const retentionDays = getRetentionDays(settings?.auditRetentionDays)
|
|
27
|
+
|
|
28
|
+
if (!retentionEnabled || !retentionDays) {
|
|
29
|
+
return { success: true, deletedCount: 0, skipped: true }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const cutoff = new Date(Date.now() - retentionDays * MS_PER_DAY).toISOString()
|
|
33
|
+
const deletedRows = await db
|
|
34
|
+
.delete(auditLog)
|
|
35
|
+
.where(lt(auditLog.createdAt, cutoff))
|
|
36
|
+
.returning({ id: auditLog.id })
|
|
37
|
+
|
|
38
|
+
const requestMetadata = await getAuditRequestMetadata()
|
|
39
|
+
await runAdminEventHooks({
|
|
40
|
+
category: 'system',
|
|
41
|
+
action: 'cleanup',
|
|
42
|
+
targetType: 'audit-log',
|
|
43
|
+
targetLabel: 'Audit log retention cleanup',
|
|
44
|
+
user: actor,
|
|
45
|
+
timestamp: new Date().toISOString(),
|
|
46
|
+
sourceType: 'system_job',
|
|
47
|
+
changedFields: [],
|
|
48
|
+
metadata: {
|
|
49
|
+
cutoff,
|
|
50
|
+
deletedCount: deletedRows.length,
|
|
51
|
+
retentionDays
|
|
52
|
+
},
|
|
53
|
+
...requestMetadata
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
return { success: true, deletedCount: deletedRows.length, cutoff }
|
|
57
|
+
} catch (error) {
|
|
58
|
+
return {
|
|
59
|
+
success: false,
|
|
60
|
+
error: error instanceof Error ? error.message : 'Failed to clean up audit logs'
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
'use server'
|
|
2
|
+
|
|
3
|
+
import { requireRole, UserRole } from '@admin/auth/middleware'
|
|
4
|
+
import db from '@admin/db'
|
|
5
|
+
import { auditLog } from '@admin/db/schema'
|
|
6
|
+
import { and, desc, eq, gte, ilike, lt, or, sql } from 'drizzle-orm'
|
|
7
|
+
import type { AuditLogEntry, AuditLogPage, AuditLogQuery } from './types'
|
|
8
|
+
|
|
9
|
+
const DEFAULT_AUDIT_LOG_PAGE = 1
|
|
10
|
+
const DEFAULT_AUDIT_LOG_PAGE_SIZE = 25
|
|
11
|
+
const MAX_AUDIT_LOG_PAGE_SIZE = 100
|
|
12
|
+
|
|
13
|
+
function clampPage(value: number | undefined): number {
|
|
14
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) return DEFAULT_AUDIT_LOG_PAGE
|
|
15
|
+
return Math.max(DEFAULT_AUDIT_LOG_PAGE, Math.floor(value))
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function clampPageSize(value: number | undefined): number {
|
|
19
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
|
|
20
|
+
return DEFAULT_AUDIT_LOG_PAGE_SIZE
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return Math.min(MAX_AUDIT_LOG_PAGE_SIZE, Math.max(1, Math.floor(value)))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function getExclusiveDateUpperBound(value: string | undefined): string | null {
|
|
27
|
+
if (!value) return null
|
|
28
|
+
const date = new Date(value)
|
|
29
|
+
if (Number.isNaN(date.getTime())) return null
|
|
30
|
+
|
|
31
|
+
date.setUTCDate(date.getUTCDate() + 1)
|
|
32
|
+
return date.toISOString()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function queryAuditLogs(query: AuditLogQuery = {}): Promise<AuditLogPage> {
|
|
36
|
+
const page = clampPage(query.page)
|
|
37
|
+
const pageSize = clampPageSize(query.pageSize)
|
|
38
|
+
const offset = (page - 1) * pageSize
|
|
39
|
+
const conditions = []
|
|
40
|
+
|
|
41
|
+
if (query.category) conditions.push(eq(auditLog.category, query.category))
|
|
42
|
+
if (query.action) conditions.push(eq(auditLog.action, query.action))
|
|
43
|
+
if (query.targetType) conditions.push(ilike(auditLog.targetType, `%${query.targetType}%`))
|
|
44
|
+
if (query.actor) {
|
|
45
|
+
const actorPattern = `%${query.actor}%`
|
|
46
|
+
conditions.push(
|
|
47
|
+
or(ilike(auditLog.actorEmail, actorPattern), ilike(auditLog.actorName, actorPattern))
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
if (query.createdAtFrom) {
|
|
51
|
+
conditions.push(gte(auditLog.createdAt, query.createdAtFrom))
|
|
52
|
+
}
|
|
53
|
+
const createdAtExclusiveTo = getExclusiveDateUpperBound(query.createdAtTo)
|
|
54
|
+
if (createdAtExclusiveTo) {
|
|
55
|
+
conditions.push(lt(auditLog.createdAt, createdAtExclusiveTo))
|
|
56
|
+
}
|
|
57
|
+
if (query.search) {
|
|
58
|
+
const searchPattern = `%${query.search}%`
|
|
59
|
+
conditions.push(
|
|
60
|
+
or(
|
|
61
|
+
ilike(auditLog.targetId, searchPattern),
|
|
62
|
+
ilike(auditLog.targetLabel, searchPattern),
|
|
63
|
+
ilike(auditLog.actorEmail, searchPattern),
|
|
64
|
+
ilike(auditLog.actorName, searchPattern),
|
|
65
|
+
ilike(auditLog.sourceType, searchPattern),
|
|
66
|
+
ilike(auditLog.ipAddress, searchPattern),
|
|
67
|
+
ilike(auditLog.userAgent, searchPattern)
|
|
68
|
+
)
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const where = conditions.length > 0 ? and(...conditions) : undefined
|
|
73
|
+
const [rows, countResult] = await Promise.all([
|
|
74
|
+
db
|
|
75
|
+
.select()
|
|
76
|
+
.from(auditLog)
|
|
77
|
+
.where(where)
|
|
78
|
+
.orderBy(desc(auditLog.createdAt))
|
|
79
|
+
.limit(pageSize)
|
|
80
|
+
.offset(offset),
|
|
81
|
+
db.select({ count: sql<number>`count(*)` }).from(auditLog).where(where)
|
|
82
|
+
])
|
|
83
|
+
const total = Number(countResult[0]?.count ?? 0)
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
auditLogs: rows as AuditLogEntry[],
|
|
87
|
+
total,
|
|
88
|
+
page,
|
|
89
|
+
pageSize,
|
|
90
|
+
totalPages: Math.max(1, Math.ceil(total / pageSize))
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function getAuditLogs(query: AuditLogQuery = {}): Promise<AuditLogPage> {
|
|
95
|
+
await requireRole([UserRole.ADMIN])
|
|
96
|
+
return queryAuditLogs(query)
|
|
97
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export { cleanupAuditLogs } from './cleanup-audit-logs'
|
|
2
|
+
export { getAuditLogs } from './get-audit-logs'
|
|
3
|
+
export type {
|
|
4
|
+
AuditLogAction,
|
|
5
|
+
AuditLogCategory,
|
|
6
|
+
AuditLogEntry,
|
|
7
|
+
AuditLogPage,
|
|
8
|
+
AuditLogQuery,
|
|
9
|
+
AuditLogSourceType,
|
|
10
|
+
CleanupAuditLogsResult
|
|
11
|
+
} from './types'
|
|
12
|
+
export {
|
|
13
|
+
AUDIT_LOG_ACTIONS,
|
|
14
|
+
AUDIT_LOG_CATEGORIES,
|
|
15
|
+
AUDIT_LOG_SOURCE_TYPES,
|
|
16
|
+
auditLogCacheTags
|
|
17
|
+
} from './types'
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import db from '@admin/db'
|
|
2
|
+
import { auditLog } from '@admin/db/schema'
|
|
3
|
+
import type { AdminEventContext } from '@admin/lib/lifecycle-hooks/types'
|
|
4
|
+
|
|
5
|
+
const MAX_TARGET_TYPE_LENGTH = 100
|
|
6
|
+
const MAX_SOURCE_TYPE_LENGTH = 50
|
|
7
|
+
const MAX_IP_LENGTH = 100
|
|
8
|
+
const MAX_USER_AGENT_LENGTH = 500
|
|
9
|
+
|
|
10
|
+
function truncate(value: string | null | undefined, maxLength: number): string | null {
|
|
11
|
+
if (!value) return null
|
|
12
|
+
return value.length > maxLength ? value.slice(0, maxLength) : value
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function normalizeChangedFields(value: readonly string[] | undefined): string[] {
|
|
16
|
+
return [...new Set(value ?? [])].filter(Boolean).sort((a, b) => a.localeCompare(b))
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function recordAuditEvent(event: AdminEventContext): Promise<void> {
|
|
20
|
+
await db.insert(auditLog).values({
|
|
21
|
+
category: event.category,
|
|
22
|
+
action: event.action,
|
|
23
|
+
targetType: truncate(event.targetType, MAX_TARGET_TYPE_LENGTH) ?? 'unknown',
|
|
24
|
+
targetId: event.targetId ?? null,
|
|
25
|
+
targetLabel: event.targetLabel ?? null,
|
|
26
|
+
actorUserId: event.user?.id ?? null,
|
|
27
|
+
actorEmail: event.user?.email ?? null,
|
|
28
|
+
actorName: event.user?.name ?? null,
|
|
29
|
+
actorRole: typeof event.user?.role === 'string' ? event.user.role : null,
|
|
30
|
+
sourceType: truncate(event.sourceType, MAX_SOURCE_TYPE_LENGTH) ?? 'server_action',
|
|
31
|
+
ipAddress: truncate(event.ipAddress, MAX_IP_LENGTH),
|
|
32
|
+
userAgent: truncate(event.userAgent, MAX_USER_AGENT_LENGTH),
|
|
33
|
+
changedFields: normalizeChangedFields(event.changedFields),
|
|
34
|
+
metadata: event.metadata ?? null,
|
|
35
|
+
createdAt: event.timestamp
|
|
36
|
+
})
|
|
37
|
+
}
|