betterstart-cli 0.0.95 → 0.0.96
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/integrations/_shared/actions/object-key.ts +16 -0
- package/dist/assets/adapters/next/integrations/r2/actions/r2.ts +2 -10
- package/dist/assets/adapters/next/integrations/r2/integration.ts +4 -0
- package/dist/assets/adapters/next/integrations/railway-bucket/actions/railway-bucket.ts +2 -10
- package/dist/assets/adapters/next/integrations/railway-bucket/integration.ts +4 -0
- package/dist/assets/adapters/next/integrations/vercel-blob/actions/vercel-blob.ts +2 -10
- package/dist/assets/adapters/next/integrations/vercel-blob/integration.ts +4 -0
- package/dist/assets/adapters/next/templates/init/api/upload-route.ts +37 -13
- package/dist/assets/adapters/next/templates/init/hooks/content-editor/use-color-highlight.ts +10 -64
- package/dist/assets/adapters/next/templates/init/hooks/content-editor/use-content-editor-source-mode.tsx +11 -4
- package/dist/assets/adapters/next/templates/init/lib/actions/audit/get-audit-logs.ts +1 -9
- package/dist/assets/adapters/next/templates/init/lib/actions/media/create-media.ts +6 -6
- package/dist/assets/adapters/next/templates/init/lib/actions/media/update-media.ts +5 -5
- package/dist/assets/adapters/next/templates/init/lib/actions/webhooks/get-webhook-deliveries.ts +1 -10
- package/dist/assets/adapters/next/templates/init/lib/actions/webhooks/get-webhooks.ts +1 -10
- package/dist/assets/adapters/next/templates/init/pages/settings/forms/forms-settings-page-content.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/utils/date/date.ts +15 -0
- package/dist/assets/adapters/next/templates/init/utils/editor/content-editor.ts +98 -25
- package/dist/assets/adapters/next/templates/init/utils/editor/markdown.ts +5 -2
- package/dist/assets/shared-assets/react-admin/custom/content-editor/selection-bubble-menu.tsx +2 -2
- package/dist/assets/shared-assets/react-admin/custom/content-editor/source-mode.tsx +4 -2
- package/dist/assets/shared-assets/react-admin/custom/media-field.tsx +2 -2
- package/dist/assets/shared-assets/react-admin/schema.json +1143 -876
- package/dist/{chunk-3CLINNJL.js → chunk-LGZBMR7E.js} +33 -9
- package/dist/chunk-LGZBMR7E.js.map +1 -0
- package/dist/cli.js +2338 -2872
- package/dist/cli.js.map +1 -1
- package/dist/{template-reader-PVN53GS7.js → template-reader-WRTNGRZV.js} +2 -2
- package/package.json +1 -1
- package/dist/chunk-3CLINNJL.js.map +0 -1
- /package/dist/{template-reader-PVN53GS7.js.map → template-reader-WRTNGRZV.js.map} +0 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto'
|
|
2
|
+
|
|
3
|
+
function sanitizeFilename(filename: string): string {
|
|
4
|
+
return filename.replace(/[^a-zA-Z0-9.-]/g, '_')
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Object-key naming scheme for every upload, shared by all storage providers.
|
|
9
|
+
*
|
|
10
|
+
* Single owner: a project that switches storage providers must keep producing
|
|
11
|
+
* consistent keys. This previously existed as an identical copy inside each of
|
|
12
|
+
* the R2, Railway Bucket, and Vercel Blob payloads.
|
|
13
|
+
*/
|
|
14
|
+
export function buildStorageObjectKey(filename: string, prefix = 'uploads'): string {
|
|
15
|
+
return `${prefix}/${Date.now()}-${randomUUID()}-${sanitizeFilename(filename)}`
|
|
16
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { buildStorageObjectKey } from '@admin/actions/storage/object-key'
|
|
2
2
|
import type { AdminStorageProvider, AdminStorageUploadInput } from '@admin/actions/storage/types'
|
|
3
3
|
import { PutObjectCommand, S3Client } from '@aws-sdk/client-s3'
|
|
4
4
|
|
|
@@ -14,14 +14,6 @@ function getConfig() {
|
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
function sanitizeFilename(filename: string): string {
|
|
18
|
-
return filename.replace(/[^a-zA-Z0-9.-]/g, '_')
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function buildKey(filename: string, prefix = 'uploads'): string {
|
|
22
|
-
return `${prefix}/${Date.now()}-${randomUUID()}-${sanitizeFilename(filename)}`
|
|
23
|
-
}
|
|
24
|
-
|
|
25
17
|
function getClient(): S3Client {
|
|
26
18
|
if (r2Client) {
|
|
27
19
|
return r2Client
|
|
@@ -58,7 +50,7 @@ export const r2StorageProvider: AdminStorageProvider = {
|
|
|
58
50
|
throw new Error('Cloudflare R2 is not fully configured.')
|
|
59
51
|
}
|
|
60
52
|
|
|
61
|
-
const key =
|
|
53
|
+
const key = buildStorageObjectKey(input.filename, input.prefix)
|
|
62
54
|
await getClient().send(
|
|
63
55
|
new PutObjectCommand({
|
|
64
56
|
Bucket: config.bucketName,
|
|
@@ -9,6 +9,10 @@ export const r2Integration: BetterstartIntegrationDefinition = {
|
|
|
9
9
|
dependencies: [],
|
|
10
10
|
conflicts: ['vercel-blob', 'railway-bucket'],
|
|
11
11
|
files: [
|
|
12
|
+
{
|
|
13
|
+
outputPath: 'lib/actions/storage/object-key.ts',
|
|
14
|
+
templatePath: '../_shared/actions/object-key.ts'
|
|
15
|
+
},
|
|
12
16
|
{
|
|
13
17
|
outputPath: 'lib/actions/r2/provider.ts',
|
|
14
18
|
templatePath: 'actions/r2.ts'
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { buildStorageObjectKey } from '@admin/actions/storage/object-key'
|
|
2
2
|
import type { AdminStorageProvider, AdminStorageUploadInput } from '@admin/actions/storage/types'
|
|
3
3
|
import { GetObjectCommand, PutObjectCommand, S3Client } from '@aws-sdk/client-s3'
|
|
4
4
|
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'
|
|
@@ -16,14 +16,6 @@ function getConfig() {
|
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
function sanitizeFilename(filename: string): string {
|
|
20
|
-
return filename.replace(/[^a-zA-Z0-9.-]/g, '_')
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function buildKey(filename: string, prefix = 'uploads'): string {
|
|
24
|
-
return `${prefix}/${Date.now()}-${randomUUID()}-${sanitizeFilename(filename)}`
|
|
25
|
-
}
|
|
26
|
-
|
|
27
19
|
function stableReadUrl(key: string): string {
|
|
28
20
|
return `/api/admin/storage/${Buffer.from(key, 'utf-8').toString('base64url')}`
|
|
29
21
|
}
|
|
@@ -63,7 +55,7 @@ export const railwayBucketStorageProvider = {
|
|
|
63
55
|
throw new Error('Railway Bucket is not fully configured.')
|
|
64
56
|
}
|
|
65
57
|
|
|
66
|
-
const key =
|
|
58
|
+
const key = buildStorageObjectKey(input.filename, input.prefix)
|
|
67
59
|
await getClient().send(
|
|
68
60
|
new PutObjectCommand({
|
|
69
61
|
Bucket: config.bucketName,
|
|
@@ -9,6 +9,10 @@ export const railwayBucketIntegration: BetterstartIntegrationDefinition = {
|
|
|
9
9
|
dependencies: [],
|
|
10
10
|
conflicts: ['r2', 'vercel-blob'],
|
|
11
11
|
files: [
|
|
12
|
+
{
|
|
13
|
+
outputPath: 'lib/actions/storage/object-key.ts',
|
|
14
|
+
templatePath: '../_shared/actions/object-key.ts'
|
|
15
|
+
},
|
|
12
16
|
{
|
|
13
17
|
outputPath: 'lib/actions/railway-bucket/provider.ts',
|
|
14
18
|
templatePath: 'actions/railway-bucket.ts'
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { buildStorageObjectKey } from '@admin/actions/storage/object-key'
|
|
2
2
|
import type { AdminStorageProvider, AdminStorageUploadInput } from '@admin/actions/storage/types'
|
|
3
3
|
import { put } from '@vercel/blob'
|
|
4
4
|
|
|
@@ -6,14 +6,6 @@ function getToken(): string {
|
|
|
6
6
|
return process.env.BLOB_READ_WRITE_TOKEN?.trim() ?? ''
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
function sanitizeFilename(filename: string): string {
|
|
10
|
-
return filename.replace(/[^a-zA-Z0-9.-]/g, '_')
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function buildKey(filename: string, prefix = 'uploads'): string {
|
|
14
|
-
return `${prefix}/${Date.now()}-${randomUUID()}-${sanitizeFilename(filename)}`
|
|
15
|
-
}
|
|
16
|
-
|
|
17
9
|
export const vercelBlobStorageProvider: AdminStorageProvider = {
|
|
18
10
|
id: 'vercel-blob',
|
|
19
11
|
isConfigured() {
|
|
@@ -24,7 +16,7 @@ export const vercelBlobStorageProvider: AdminStorageProvider = {
|
|
|
24
16
|
throw new Error('Vercel Blob is not configured.')
|
|
25
17
|
}
|
|
26
18
|
|
|
27
|
-
const key =
|
|
19
|
+
const key = buildStorageObjectKey(input.filename, input.prefix)
|
|
28
20
|
const blob = await put(key, input.buffer, {
|
|
29
21
|
access: 'public',
|
|
30
22
|
contentType: input.contentType,
|
|
@@ -9,6 +9,10 @@ export const vercelBlobIntegration: BetterstartIntegrationDefinition = {
|
|
|
9
9
|
dependencies: [],
|
|
10
10
|
conflicts: ['r2', 'railway-bucket'],
|
|
11
11
|
files: [
|
|
12
|
+
{
|
|
13
|
+
outputPath: 'lib/actions/storage/object-key.ts',
|
|
14
|
+
templatePath: '../_shared/actions/object-key.ts'
|
|
15
|
+
},
|
|
12
16
|
{
|
|
13
17
|
outputPath: 'lib/actions/vercel-blob/provider.ts',
|
|
14
18
|
templatePath: 'actions/vercel-blob.ts'
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import { getAuditRequestMetadata } from '@admin/actions/audit/request-metadata'
|
|
1
2
|
import { mediaCacheTags } from '@admin/actions/media/types'
|
|
2
3
|
import { saveUpload } from '@admin/actions/storage'
|
|
3
|
-
import {
|
|
4
|
+
import { requireRole, UserRole } from '@admin/auth/middleware'
|
|
4
5
|
import db from '@admin/db'
|
|
5
6
|
import { adminMedia } from '@admin/db/schema'
|
|
7
|
+
import { runAdminEventHooks } from '@admin/lib/lifecycle-hooks'
|
|
6
8
|
import type { UploadedFile } from '@admin/types'
|
|
9
|
+
import { getTouchedFieldNames } from '@admin/utils/audit/audit'
|
|
7
10
|
import { mapMediaRow, selectMedia } from '@admin/utils/media/query'
|
|
8
11
|
import { validateFiles } from '@admin/utils/validation/validation'
|
|
9
12
|
import { eq } from 'drizzle-orm'
|
|
@@ -11,6 +14,8 @@ import { revalidateTag } from 'next/cache'
|
|
|
11
14
|
import { type NextRequest, NextResponse } from 'next/server'
|
|
12
15
|
|
|
13
16
|
export async function POST(request: NextRequest) {
|
|
17
|
+
const actor = await requireRole([UserRole.ADMIN, UserRole.EDITOR])
|
|
18
|
+
|
|
14
19
|
try {
|
|
15
20
|
const formData = await request.formData()
|
|
16
21
|
const prefix = formData.get('prefix')?.toString() || 'uploads'
|
|
@@ -56,8 +61,7 @@ export async function POST(request: NextRequest) {
|
|
|
56
61
|
}
|
|
57
62
|
|
|
58
63
|
const uploadedFiles: UploadedFile[] = []
|
|
59
|
-
const
|
|
60
|
-
const actorId = session?.user?.id ?? null
|
|
64
|
+
const auditRequestMetadata = await getAuditRequestMetadata()
|
|
61
65
|
|
|
62
66
|
for (const [index, file] of files.entries()) {
|
|
63
67
|
const arrayBuffer = await file.arrayBuffer()
|
|
@@ -74,21 +78,25 @@ export async function POST(request: NextRequest) {
|
|
|
74
78
|
const heightStr = formData.get(`height${index}`)?.toString()
|
|
75
79
|
const now = new Date().toISOString()
|
|
76
80
|
|
|
81
|
+
const mediaValues = {
|
|
82
|
+
url,
|
|
83
|
+
filename: file.name,
|
|
84
|
+
contentType: file.type,
|
|
85
|
+
size: file.size,
|
|
86
|
+
width: widthStr ? Number(widthStr) : null,
|
|
87
|
+
height: heightStr ? Number(heightStr) : null,
|
|
88
|
+
alt: null,
|
|
89
|
+
tags: null
|
|
90
|
+
}
|
|
91
|
+
|
|
77
92
|
const [created] = await db
|
|
78
93
|
.insert(adminMedia)
|
|
79
94
|
.values({
|
|
80
|
-
|
|
81
|
-
filename: file.name,
|
|
82
|
-
contentType: file.type,
|
|
83
|
-
size: file.size,
|
|
84
|
-
width: widthStr ? Number(widthStr) : null,
|
|
85
|
-
height: heightStr ? Number(heightStr) : null,
|
|
86
|
-
alt: null,
|
|
87
|
-
tags: null,
|
|
95
|
+
...mediaValues,
|
|
88
96
|
createdAt: now,
|
|
89
97
|
updatedAt: now,
|
|
90
|
-
createdBy:
|
|
91
|
-
updatedBy:
|
|
98
|
+
createdBy: actor.id,
|
|
99
|
+
updatedBy: actor.id
|
|
92
100
|
})
|
|
93
101
|
.returning({ id: adminMedia.id })
|
|
94
102
|
|
|
@@ -99,6 +107,8 @@ export async function POST(request: NextRequest) {
|
|
|
99
107
|
)
|
|
100
108
|
}
|
|
101
109
|
|
|
110
|
+
// Route handlers cannot call updateTag(), so revalidateTag() with an
|
|
111
|
+
// immediate expiry is the equivalent invalidation used by the actions.
|
|
102
112
|
revalidateTag(mediaCacheTags.all, { expire: 0 })
|
|
103
113
|
revalidateTag(mediaCacheTags.byId(created.id), { expire: 0 })
|
|
104
114
|
|
|
@@ -112,6 +122,20 @@ export async function POST(request: NextRequest) {
|
|
|
112
122
|
|
|
113
123
|
const media = mapMediaRow(mediaRow)
|
|
114
124
|
|
|
125
|
+
runAdminEventHooks({
|
|
126
|
+
category: 'media',
|
|
127
|
+
action: 'create',
|
|
128
|
+
targetType: 'media',
|
|
129
|
+
targetId: media.id,
|
|
130
|
+
targetLabel: media.filename,
|
|
131
|
+
user: actor,
|
|
132
|
+
timestamp: now,
|
|
133
|
+
sourceType: 'admin_ui' as const,
|
|
134
|
+
changedFields: getTouchedFieldNames(mediaValues),
|
|
135
|
+
metadata: { contentType: media.contentType, size: media.size },
|
|
136
|
+
...auditRequestMetadata
|
|
137
|
+
})
|
|
138
|
+
|
|
115
139
|
uploadedFiles.push({
|
|
116
140
|
id: media.id,
|
|
117
141
|
key,
|
package/dist/assets/adapters/next/templates/init/hooks/content-editor/use-color-highlight.ts
CHANGED
|
@@ -4,6 +4,11 @@ import { useIsBreakpoint } from '@admin/hooks/content-editor/use-is-breakpoint'
|
|
|
4
4
|
// --- Hooks ---
|
|
5
5
|
import { useTiptapEditor } from '@admin/hooks/content-editor/use-tiptap-editor'
|
|
6
6
|
// --- Lib ---
|
|
7
|
+
import {
|
|
8
|
+
CONTENT_EDITOR_HIGHLIGHT_COLOR_OPTIONS,
|
|
9
|
+
CONTENT_EDITOR_HIGHLIGHT_COLORS,
|
|
10
|
+
type ContentEditorHighlightColorOption
|
|
11
|
+
} from '@admin/utils/editor/content-editor'
|
|
7
12
|
import {
|
|
8
13
|
isExtensionAvailable,
|
|
9
14
|
isMarkInSchema,
|
|
@@ -16,69 +21,10 @@ import { useCallback, useEffect, useState } from 'react'
|
|
|
16
21
|
import { useHotkeys } from 'react-hotkeys-hook'
|
|
17
22
|
|
|
18
23
|
export const COLOR_HIGHLIGHT_SHORTCUT_KEY = 'mod+shift+h'
|
|
19
|
-
export const HIGHLIGHT_COLORS =
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
colorValue: '#ffffff',
|
|
24
|
-
border: 'var(--tt-bg-color-contrast)'
|
|
25
|
-
},
|
|
26
|
-
{
|
|
27
|
-
label: 'Gray background',
|
|
28
|
-
value: 'var(--tt-color-highlight-gray)',
|
|
29
|
-
colorValue: '#f8f8f7',
|
|
30
|
-
border: 'var(--tt-color-highlight-gray-contrast)'
|
|
31
|
-
},
|
|
32
|
-
{
|
|
33
|
-
label: 'Brown background',
|
|
34
|
-
value: 'var(--tt-color-highlight-brown)',
|
|
35
|
-
colorValue: '#f4eeee',
|
|
36
|
-
border: 'var(--tt-color-highlight-brown-contrast)'
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
label: 'Orange background',
|
|
40
|
-
value: 'var(--tt-color-highlight-orange)',
|
|
41
|
-
colorValue: '#fbecdd',
|
|
42
|
-
border: 'var(--tt-color-highlight-orange-contrast)'
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
label: 'Yellow background',
|
|
46
|
-
value: 'var(--tt-color-highlight-yellow)',
|
|
47
|
-
colorValue: '#fef9c3',
|
|
48
|
-
border: 'var(--tt-color-highlight-yellow-contrast)'
|
|
49
|
-
},
|
|
50
|
-
{
|
|
51
|
-
label: 'Green background',
|
|
52
|
-
value: 'var(--tt-color-highlight-green)',
|
|
53
|
-
colorValue: '#dcfce7',
|
|
54
|
-
border: 'var(--tt-color-highlight-green-contrast)'
|
|
55
|
-
},
|
|
56
|
-
{
|
|
57
|
-
label: 'Blue background',
|
|
58
|
-
value: 'var(--tt-color-highlight-blue)',
|
|
59
|
-
colorValue: '#e0f2fe',
|
|
60
|
-
border: 'var(--tt-color-highlight-blue-contrast)'
|
|
61
|
-
},
|
|
62
|
-
{
|
|
63
|
-
label: 'Purple background',
|
|
64
|
-
value: 'var(--tt-color-highlight-purple)',
|
|
65
|
-
colorValue: '#f3e8ff',
|
|
66
|
-
border: 'var(--tt-color-highlight-purple-contrast)'
|
|
67
|
-
},
|
|
68
|
-
{
|
|
69
|
-
label: 'Pink background',
|
|
70
|
-
value: 'var(--tt-color-highlight-pink)',
|
|
71
|
-
colorValue: '#fcf1f6',
|
|
72
|
-
border: 'var(--tt-color-highlight-pink-contrast)'
|
|
73
|
-
},
|
|
74
|
-
{
|
|
75
|
-
label: 'Red background',
|
|
76
|
-
value: 'var(--tt-color-highlight-red)',
|
|
77
|
-
colorValue: '#ffe4e6',
|
|
78
|
-
border: 'var(--tt-color-highlight-red-contrast)'
|
|
79
|
-
}
|
|
80
|
-
]
|
|
81
|
-
export type HighlightColor = (typeof HIGHLIGHT_COLORS)[number]
|
|
24
|
+
export const HIGHLIGHT_COLORS = CONTENT_EDITOR_HIGHLIGHT_COLORS.map(
|
|
25
|
+
(name) => CONTENT_EDITOR_HIGHLIGHT_COLOR_OPTIONS[name]
|
|
26
|
+
)
|
|
27
|
+
export type HighlightColor = ContentEditorHighlightColorOption
|
|
82
28
|
|
|
83
29
|
export type HighlightMode = 'mark' | 'node'
|
|
84
30
|
|
|
@@ -133,7 +79,7 @@ export function pickHighlightColorsByValue(values: string[]) {
|
|
|
133
79
|
const colorMap = new Map(HIGHLIGHT_COLORS.map((color) => [color.value, color]))
|
|
134
80
|
return values
|
|
135
81
|
.map((value) => colorMap.get(value))
|
|
136
|
-
.filter((color): color is
|
|
82
|
+
.filter((color): color is ContentEditorHighlightColorOption => !!color)
|
|
137
83
|
}
|
|
138
84
|
|
|
139
85
|
/**
|
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
} from '@admin/utils/editor/markdown'
|
|
27
27
|
import { createMarkdownTextFromUrl } from '@admin/utils/editor/source-media'
|
|
28
28
|
import { sanitizeUrl } from '@admin/utils/editor/tiptap'
|
|
29
|
-
import { redo, undo } from '@codemirror/commands'
|
|
29
|
+
import { redo, redoDepth, undo, undoDepth } from '@codemirror/commands'
|
|
30
30
|
import { markdown } from '@codemirror/lang-markdown'
|
|
31
31
|
import { Decoration, EditorView, type ViewUpdate, WidgetType } from '@codemirror/view'
|
|
32
32
|
import { type QueryClient, QueryClientProvider, useQueryClient } from '@tanstack/react-query'
|
|
@@ -147,6 +147,8 @@ export function useContentEditorSourceMode({
|
|
|
147
147
|
} | null>(null)
|
|
148
148
|
const [markdownLinkUrl, setMarkdownLinkUrl] = React.useState<string | null>(null)
|
|
149
149
|
const [isMarkdownLinkActive, setIsMarkdownLinkActive] = React.useState(false)
|
|
150
|
+
const [canUndoMarkdownChange, setCanUndoMarkdownChange] = React.useState(false)
|
|
151
|
+
const [canRedoMarkdownChange, setCanRedoMarkdownChange] = React.useState(false)
|
|
150
152
|
const { resolvedTheme } = useTheme()
|
|
151
153
|
const editorTheme = React.useMemo(
|
|
152
154
|
() =>
|
|
@@ -313,8 +315,11 @@ export function useContentEditorSourceMode({
|
|
|
313
315
|
[onChange, onSelectionChange]
|
|
314
316
|
)
|
|
315
317
|
|
|
316
|
-
const
|
|
318
|
+
const handleEditorUpdate = React.useCallback(
|
|
317
319
|
(viewUpdate: ViewUpdate) => {
|
|
320
|
+
setCanUndoMarkdownChange(undoDepth(viewUpdate.state) > 0)
|
|
321
|
+
setCanRedoMarkdownChange(redoDepth(viewUpdate.state) > 0)
|
|
322
|
+
|
|
318
323
|
if (!viewUpdate.selectionSet) return
|
|
319
324
|
syncMarkdownLinkState(viewUpdate.view)
|
|
320
325
|
onSelectionChange?.(viewUpdate.state.selection.main.from)
|
|
@@ -348,7 +353,7 @@ export function useContentEditorSourceMode({
|
|
|
348
353
|
() => [
|
|
349
354
|
markdown(),
|
|
350
355
|
EditorView.lineWrapping,
|
|
351
|
-
EditorView.updateListener.of(
|
|
356
|
+
EditorView.updateListener.of(handleEditorUpdate),
|
|
352
357
|
EditorView.theme({
|
|
353
358
|
'&': {
|
|
354
359
|
backgroundColor: 'var(--background)',
|
|
@@ -397,7 +402,7 @@ export function useContentEditorSourceMode({
|
|
|
397
402
|
}),
|
|
398
403
|
...mediaGalleryExtensions
|
|
399
404
|
],
|
|
400
|
-
[
|
|
405
|
+
[handleEditorUpdate, mediaGalleryExtensions]
|
|
401
406
|
)
|
|
402
407
|
|
|
403
408
|
const insertInline = React.useCallback(
|
|
@@ -614,6 +619,8 @@ export function useContentEditorSourceMode({
|
|
|
614
619
|
applyHighlight,
|
|
615
620
|
applyStrike,
|
|
616
621
|
applyUnderline,
|
|
622
|
+
canRedoMarkdownChange,
|
|
623
|
+
canUndoMarkdownChange,
|
|
617
624
|
codeMirrorExtensions,
|
|
618
625
|
editorRef,
|
|
619
626
|
editorTheme,
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { requireRole, UserRole } from '@admin/auth/middleware'
|
|
4
4
|
import db from '@admin/db'
|
|
5
5
|
import { auditLog } from '@admin/db/schema'
|
|
6
|
+
import { getExclusiveDateUpperBound } from '@admin/utils/date/date'
|
|
6
7
|
import { and, asc, desc, eq, gte, ilike, lt, or, sql } from 'drizzle-orm'
|
|
7
8
|
import type { AuditLogEntry, AuditLogPage, AuditLogQuery } from './types'
|
|
8
9
|
|
|
@@ -47,15 +48,6 @@ function getAuditLogOrderBy(query: AuditLogQuery) {
|
|
|
47
48
|
}
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
function getExclusiveDateUpperBound(value: string | undefined): string | null {
|
|
51
|
-
if (!value) return null
|
|
52
|
-
const date = new Date(value)
|
|
53
|
-
if (Number.isNaN(date.getTime())) return null
|
|
54
|
-
|
|
55
|
-
date.setUTCDate(date.getUTCDate() + 1)
|
|
56
|
-
return date.toISOString()
|
|
57
|
-
}
|
|
58
|
-
|
|
59
51
|
async function queryAuditLogs(query: AuditLogQuery = {}): Promise<AuditLogPage> {
|
|
60
52
|
const page = clampPage(query.page)
|
|
61
53
|
const pageSize = normalizePageSize(query.pageSize)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use server'
|
|
2
2
|
|
|
3
3
|
import { getAuditRequestMetadata } from '@admin/actions/audit/request-metadata'
|
|
4
|
-
import {
|
|
4
|
+
import { requireRole, UserRole } from '@admin/auth/middleware'
|
|
5
5
|
import db from '@admin/db'
|
|
6
6
|
import { adminMedia } from '@admin/db/schema'
|
|
7
7
|
import { runAdminEventHooks } from '@admin/lib/lifecycle-hooks'
|
|
@@ -11,9 +11,9 @@ import { getMediaById } from './get-media-by-id'
|
|
|
11
11
|
import { type CreateMediaInput, type CreateMediaResult, mediaCacheTags } from './types'
|
|
12
12
|
|
|
13
13
|
export async function createMedia(data: CreateMediaInput): Promise<CreateMediaResult> {
|
|
14
|
+
const actor = await requireRole([UserRole.ADMIN, UserRole.EDITOR])
|
|
15
|
+
|
|
14
16
|
try {
|
|
15
|
-
const session = await getSession()
|
|
16
|
-
const actorId = session?.user?.id ?? null
|
|
17
17
|
const now = new Date().toISOString()
|
|
18
18
|
const auditRequestMetadata = await getAuditRequestMetadata()
|
|
19
19
|
const [row] = await db
|
|
@@ -22,8 +22,8 @@ export async function createMedia(data: CreateMediaInput): Promise<CreateMediaRe
|
|
|
22
22
|
...data,
|
|
23
23
|
createdAt: now,
|
|
24
24
|
updatedAt: now,
|
|
25
|
-
createdBy:
|
|
26
|
-
updatedBy:
|
|
25
|
+
createdBy: actor.id,
|
|
26
|
+
updatedBy: actor.id
|
|
27
27
|
})
|
|
28
28
|
.returning({ id: adminMedia.id })
|
|
29
29
|
if (row?.id) {
|
|
@@ -38,7 +38,7 @@ export async function createMedia(data: CreateMediaInput): Promise<CreateMediaRe
|
|
|
38
38
|
targetType: 'media',
|
|
39
39
|
targetId: media.id,
|
|
40
40
|
targetLabel: media.filename,
|
|
41
|
-
user:
|
|
41
|
+
user: actor,
|
|
42
42
|
timestamp: now,
|
|
43
43
|
sourceType: 'admin_ui' as const,
|
|
44
44
|
changedFields: getTouchedFieldNames(data as unknown as Record<string, unknown>),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use server'
|
|
2
2
|
|
|
3
3
|
import { getAuditRequestMetadata } from '@admin/actions/audit/request-metadata'
|
|
4
|
-
import {
|
|
4
|
+
import { requireRole, UserRole } from '@admin/auth/middleware'
|
|
5
5
|
import db from '@admin/db'
|
|
6
6
|
import { adminMedia } from '@admin/db/schema'
|
|
7
7
|
import { runAdminEventHooks } from '@admin/lib/lifecycle-hooks'
|
|
@@ -15,9 +15,9 @@ export async function updateMedia(
|
|
|
15
15
|
id: string,
|
|
16
16
|
data: { alt?: string; tags?: string[] }
|
|
17
17
|
): Promise<UpdateMediaResult> {
|
|
18
|
+
const actor = await requireRole([UserRole.ADMIN, UserRole.EDITOR])
|
|
19
|
+
|
|
18
20
|
try {
|
|
19
|
-
const session = await getSession()
|
|
20
|
-
const actorId = session?.user?.id ?? null
|
|
21
21
|
const now = new Date().toISOString()
|
|
22
22
|
const auditRequestMetadata = await getAuditRequestMetadata()
|
|
23
23
|
const [previousMedia] = await db.select().from(adminMedia).where(eq(adminMedia.id, id)).limit(1)
|
|
@@ -26,7 +26,7 @@ export async function updateMedia(
|
|
|
26
26
|
.set({
|
|
27
27
|
...data,
|
|
28
28
|
updatedAt: now,
|
|
29
|
-
updatedBy:
|
|
29
|
+
updatedBy: actor.id
|
|
30
30
|
})
|
|
31
31
|
.where(eq(adminMedia.id, id))
|
|
32
32
|
updateTag(mediaCacheTags.all)
|
|
@@ -37,7 +37,7 @@ export async function updateMedia(
|
|
|
37
37
|
targetType: 'media',
|
|
38
38
|
targetId: id,
|
|
39
39
|
targetLabel: previousMedia?.filename ?? id,
|
|
40
|
-
user:
|
|
40
|
+
user: actor,
|
|
41
41
|
timestamp: now,
|
|
42
42
|
sourceType: 'admin_ui' as const,
|
|
43
43
|
changedFields: getChangedFieldNames(
|
package/dist/assets/adapters/next/templates/init/lib/actions/webhooks/get-webhook-deliveries.ts
CHANGED
|
@@ -3,19 +3,10 @@
|
|
|
3
3
|
import { requireRole, UserRole } from '@admin/auth/middleware'
|
|
4
4
|
import db from '@admin/db'
|
|
5
5
|
import { webhookDeliveries, webhooks } from '@admin/db/schema'
|
|
6
|
+
import { getExclusiveDateUpperBound } from '@admin/utils/date/date'
|
|
6
7
|
import { and, asc, desc, eq, gte, ilike, lt, or, sql } from 'drizzle-orm'
|
|
7
8
|
import type { WebhookDeliveriesPage, WebhookDeliveriesQuery } from './types'
|
|
8
9
|
|
|
9
|
-
function getExclusiveDateUpperBound(value?: string): string | null {
|
|
10
|
-
if (!value) return null
|
|
11
|
-
|
|
12
|
-
const date = new Date(`${value}T00:00:00.000Z`)
|
|
13
|
-
if (Number.isNaN(date.getTime())) return null
|
|
14
|
-
|
|
15
|
-
date.setUTCDate(date.getUTCDate() + 1)
|
|
16
|
-
return date.toISOString().slice(0, 10)
|
|
17
|
-
}
|
|
18
|
-
|
|
19
10
|
const SORTABLE_COLUMNS = {
|
|
20
11
|
createdAt: webhookDeliveries.createdAt,
|
|
21
12
|
event: webhookDeliveries.event,
|
|
@@ -3,19 +3,10 @@
|
|
|
3
3
|
import { requireRole, UserRole } from '@admin/auth/middleware'
|
|
4
4
|
import db from '@admin/db'
|
|
5
5
|
import { webhookDeliveries, webhookSubscriptions, webhooks } from '@admin/db/schema'
|
|
6
|
+
import { getExclusiveDateUpperBound } from '@admin/utils/date/date'
|
|
6
7
|
import { and, asc, count, desc, eq, gte, ilike, isNull, lt, or, sql } from 'drizzle-orm'
|
|
7
8
|
import type { WebhooksPage, WebhooksQuery } from './types'
|
|
8
9
|
|
|
9
|
-
function getExclusiveDateUpperBound(value?: string): string | null {
|
|
10
|
-
if (!value) return null
|
|
11
|
-
|
|
12
|
-
const date = new Date(`${value}T00:00:00.000Z`)
|
|
13
|
-
if (Number.isNaN(date.getTime())) return null
|
|
14
|
-
|
|
15
|
-
date.setUTCDate(date.getUTCDate() + 1)
|
|
16
|
-
return date.toISOString().slice(0, 10)
|
|
17
|
-
}
|
|
18
|
-
|
|
19
10
|
// Deliberately uncached: filters and sorting depend on delivery rows written
|
|
20
11
|
// inside after(), where cache invalidation is unavailable, so cached reads
|
|
21
12
|
// could never be refreshed.
|
|
@@ -36,7 +36,7 @@ export function FormsSettingsPageContent() {
|
|
|
36
36
|
return (
|
|
37
37
|
<React.Fragment>
|
|
38
38
|
<PageHeader title="Form Settings" />
|
|
39
|
-
<main className="space-y-4 px-4 pb-4 flex-1">
|
|
39
|
+
<main className="space-y-4 px-4 pb-4 flex-1 pt-4">
|
|
40
40
|
<FormsSettingsTable
|
|
41
41
|
columns={columns}
|
|
42
42
|
rows={rows}
|
|
@@ -8,6 +8,21 @@ const DATE_RANGE_TIME_PATTERN = /^([01]\d|2[0-3]):([0-5]\d)$/
|
|
|
8
8
|
export const DATE_RANGE_START_TIME = '00:00'
|
|
9
9
|
export const DATE_RANGE_END_TIME = '23:59'
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Turn an inclusive `YYYY-MM-DD` "to date" filter into the exclusive upper bound
|
|
13
|
+
* that follows it. The input is always read as UTC so the same filter selects the
|
|
14
|
+
* same rows regardless of the viewer's timezone.
|
|
15
|
+
*/
|
|
16
|
+
export function getExclusiveDateUpperBound(value?: string): string | null {
|
|
17
|
+
if (!value) return null
|
|
18
|
+
|
|
19
|
+
const date = new Date(`${value}T00:00:00.000Z`)
|
|
20
|
+
if (Number.isNaN(date.getTime())) return null
|
|
21
|
+
|
|
22
|
+
date.setUTCDate(date.getUTCDate() + 1)
|
|
23
|
+
return date.toISOString().slice(0, 10)
|
|
24
|
+
}
|
|
25
|
+
|
|
11
26
|
export function parseDateInputValue(value?: string): Date | undefined {
|
|
12
27
|
if (!value) return undefined
|
|
13
28
|
|