create-nextblock 0.2.30 → 0.2.33
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/package.json +1 -1
- package/scripts/sync-template.js +70 -52
- package/templates/nextblock-template/app/[slug]/page.tsx +55 -55
- package/templates/nextblock-template/app/cms/blocks/actions.ts +15 -15
- package/templates/nextblock-template/app/cms/blocks/components/BackgroundSelector.tsx +14 -12
- package/templates/nextblock-template/app/cms/blocks/components/BlockEditorArea.tsx +24 -21
- package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +1 -1
- package/templates/nextblock-template/app/cms/blocks/components/ColumnEditor.tsx +42 -24
- package/templates/nextblock-template/app/cms/blocks/components/EditableBlock.tsx +16 -16
- package/templates/nextblock-template/app/cms/blocks/editors/SectionBlockEditor.tsx +56 -35
- package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +1 -1
- package/templates/nextblock-template/app/cms/media/actions.ts +47 -47
- package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +3 -3
- package/templates/nextblock-template/app/cms/media/components/MediaUploadForm.tsx +1 -1
- package/templates/nextblock-template/app/cms/media/page.tsx +3 -3
- package/templates/nextblock-template/app/cms/revisions/JsonDiffView.tsx +8 -7
- package/templates/nextblock-template/app/cms/revisions/RevisionHistoryButton.tsx +16 -10
- package/templates/nextblock-template/app/cms/revisions/service.ts +9 -9
- package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +1 -1
- package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +1 -0
- package/templates/nextblock-template/eslint.config.mjs +14 -10
- package/templates/nextblock-template/lib/blocks/blockRegistry.ts +29 -29
- package/templates/nextblock-template/package.json +5 -3
|
@@ -28,6 +28,19 @@ interface SortableColumnBlockProps {
|
|
|
28
28
|
onClick: (e: React.MouseEvent<HTMLDivElement>) => void;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
type AnyBlockContent = {
|
|
32
|
+
html_content?: string;
|
|
33
|
+
text_content?: string;
|
|
34
|
+
level?: number;
|
|
35
|
+
alt_text?: string;
|
|
36
|
+
media_id?: string;
|
|
37
|
+
text?: string;
|
|
38
|
+
url?: string;
|
|
39
|
+
title?: string;
|
|
40
|
+
columns?: number;
|
|
41
|
+
postsPerPage?: number;
|
|
42
|
+
};
|
|
43
|
+
|
|
31
44
|
function SortableColumnBlock({ block, index, columnIndex, onEdit, onDelete, blockType, onClick }: SortableColumnBlockProps) {
|
|
32
45
|
const {
|
|
33
46
|
attributes,
|
|
@@ -85,30 +98,35 @@ function SortableColumnBlock({ block, index, columnIndex, onEdit, onDelete, bloc
|
|
|
85
98
|
</div>
|
|
86
99
|
</div>
|
|
87
100
|
<div className="text-xs text-gray-500 dark:text-gray-400">
|
|
88
|
-
{
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
101
|
+
{(() => {
|
|
102
|
+
const content = block.content as AnyBlockContent;
|
|
103
|
+
return (
|
|
104
|
+
<>
|
|
105
|
+
{block.block_type === 'text' && (
|
|
106
|
+
<div dangerouslySetInnerHTML={{ __html: (content.html_content || 'Empty text').substring(0, 50) + (content.html_content && content.html_content.length > 50 ? '...' : '') }} />
|
|
107
|
+
)}
|
|
108
|
+
{block.block_type === 'heading' && (
|
|
109
|
+
<div>H{content.level || 1}: {(content.text_content || 'Empty heading').substring(0, 30) + (content.text_content && content.text_content.length > 30 ? '...' : '')}</div>
|
|
110
|
+
)}
|
|
111
|
+
{block.block_type === 'image' && (
|
|
112
|
+
<div>Image: {content.alt_text || content.media_id ? 'Image selected' : 'No image selected'}</div>
|
|
113
|
+
)}
|
|
114
|
+
{block.block_type === 'button' && (
|
|
115
|
+
<div>Button: {content.text || 'No text'} → {content.url || '#'}</div>
|
|
116
|
+
)}
|
|
117
|
+
{block.block_type === 'video_embed' && (
|
|
118
|
+
<div>Video: {content.title || content.url || 'No URL set'}</div>
|
|
119
|
+
)}
|
|
120
|
+
{block.block_type === 'posts_grid' && (
|
|
121
|
+
<div>Posts Grid: {content.columns || 3} cols, {content.postsPerPage || 12} posts</div>
|
|
122
|
+
)}
|
|
123
|
+
</>
|
|
124
|
+
);
|
|
125
|
+
})()}
|
|
106
126
|
</div>
|
|
107
127
|
</div>
|
|
108
128
|
);
|
|
109
129
|
}
|
|
110
|
-
|
|
111
|
-
// Column editor component
|
|
112
130
|
export interface ColumnEditorProps {
|
|
113
131
|
columnIndex: number;
|
|
114
132
|
blocks: ColumnBlock[];
|
|
@@ -121,7 +139,7 @@ type EditingBlock = ColumnBlock & { index: number };
|
|
|
121
139
|
export default function ColumnEditor({ columnIndex, blocks, onBlocksChange, blockType }: ColumnEditorProps) {
|
|
122
140
|
const [editingBlock, setEditingBlock] = useState<EditingBlock | null>(null);
|
|
123
141
|
const [isBlockSelectorOpen, setIsBlockSelectorOpen] = useState(false);
|
|
124
|
-
const [LazyEditor, setLazyEditor] = useState<React.LazyExoticComponent<React.ComponentType<
|
|
142
|
+
const [LazyEditor, setLazyEditor] = useState<React.LazyExoticComponent<React.ComponentType<Record<string, unknown>>> | null>(null);
|
|
125
143
|
const [isConfirmOpen, setIsConfirmOpen] = useState(false);
|
|
126
144
|
const [blockToDeleteIndex, setBlockToDeleteIndex] = useState<number | null>(null);
|
|
127
145
|
|
|
@@ -134,7 +152,7 @@ export default function ColumnEditor({ columnIndex, blocks, onBlocksChange, bloc
|
|
|
134
152
|
const initialContent = getInitialContent(selectedBlockType);
|
|
135
153
|
const newBlock: ColumnBlock = {
|
|
136
154
|
block_type: selectedBlockType,
|
|
137
|
-
content: initialContent || {},
|
|
155
|
+
content: (initialContent as Record<string, unknown>) || {},
|
|
138
156
|
temp_id: `temp-${Date.now()}-${Math.random()}`
|
|
139
157
|
};
|
|
140
158
|
onBlocksChange([...blocks, newBlock]);
|
|
@@ -178,13 +196,13 @@ export default function ColumnEditor({ columnIndex, blocks, onBlocksChange, bloc
|
|
|
178
196
|
}
|
|
179
197
|
};
|
|
180
198
|
|
|
181
|
-
const handleSave = (newContent:
|
|
199
|
+
const handleSave = (newContent: unknown) => {
|
|
182
200
|
if (editingBlock === null) return;
|
|
183
201
|
|
|
184
202
|
const updatedBlocks = [...blocks];
|
|
185
203
|
updatedBlocks[editingBlock.index] = {
|
|
186
204
|
...updatedBlocks[editingBlock.index],
|
|
187
|
-
content: newContent,
|
|
205
|
+
content: newContent as Record<string, unknown>,
|
|
188
206
|
};
|
|
189
207
|
onBlocksChange(updatedBlocks);
|
|
190
208
|
setEditingBlock(null);
|
|
@@ -16,25 +16,25 @@ import { cn } from '@nextblock-cms/utils';
|
|
|
16
16
|
export interface EditableBlockProps {
|
|
17
17
|
block: Block;
|
|
18
18
|
onDelete: (blockId: number) => void;
|
|
19
|
-
onContentChange: (blockId: number, newContent: Record<string,
|
|
20
|
-
dragHandleProps?: Record<string,
|
|
19
|
+
onContentChange: (blockId: number, newContent: Record<string, unknown>) => void;
|
|
20
|
+
dragHandleProps?: Record<string, unknown>;
|
|
21
21
|
onEditNestedBlock?: (parentBlockId: string, columnIndex: number, blockIndexInColumn: number) => void;
|
|
22
22
|
className?: string;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
export default function EditableBlock({
|
|
26
|
-
block,
|
|
27
|
-
onDelete,
|
|
28
|
-
onContentChange,
|
|
29
|
-
dragHandleProps,
|
|
30
|
-
onEditNestedBlock,
|
|
31
|
-
className,
|
|
32
|
-
}: EditableBlockProps) {
|
|
33
|
-
void onEditNestedBlock;
|
|
34
|
-
// Move all hooks to the top before any conditional returns
|
|
25
|
+
export default function EditableBlock({
|
|
26
|
+
block,
|
|
27
|
+
onDelete,
|
|
28
|
+
onContentChange,
|
|
29
|
+
dragHandleProps,
|
|
30
|
+
onEditNestedBlock,
|
|
31
|
+
className,
|
|
32
|
+
}: EditableBlockProps) {
|
|
33
|
+
void onEditNestedBlock;
|
|
34
|
+
// Move all hooks to the top before any conditional returns
|
|
35
35
|
const [isConfigPanelOpen, setIsConfigPanelOpen] = useState(false);
|
|
36
36
|
const [editingBlock, setEditingBlock] = useState<Block | null>(null);
|
|
37
|
-
const [LazyEditor, setLazyEditor] = useState<LazyExoticComponent<ComponentType<
|
|
37
|
+
const [LazyEditor, setLazyEditor] = useState<LazyExoticComponent<ComponentType<Record<string, unknown>>> | null>(null);
|
|
38
38
|
|
|
39
39
|
const SectionEditor = useMemo(() => {
|
|
40
40
|
if (block?.block_type === 'section' || block?.block_type === 'hero') {
|
|
@@ -156,7 +156,7 @@ export default function EditableBlock({
|
|
|
156
156
|
{isSection ? (
|
|
157
157
|
<div className="mt-2 min-h-[200px]">
|
|
158
158
|
<Suspense fallback={<div className="flex justify-center items-center h-full"><p>Loading Editor...</p></div>}>
|
|
159
|
-
{SectionEditor && <SectionEditor block={block} content={block.content || {}} onChange={(newContent: Record<string,
|
|
159
|
+
{SectionEditor && <SectionEditor block={block} content={block.content || {}} onChange={(newContent: Record<string, unknown>) => onContentChange(block.id, newContent)} blockType={block.block_type as 'section' | 'hero'} isConfigPanelOpen={isConfigPanelOpen} />}
|
|
160
160
|
</Suspense>
|
|
161
161
|
</div>
|
|
162
162
|
) : renderPreview()}
|
|
@@ -170,8 +170,8 @@ export default function EditableBlock({
|
|
|
170
170
|
setEditingBlock(null);
|
|
171
171
|
setLazyEditor(null);
|
|
172
172
|
}}
|
|
173
|
-
onSave={(newContent:
|
|
174
|
-
onContentChange(block.id, newContent);
|
|
173
|
+
onSave={(newContent: unknown) => {
|
|
174
|
+
onContentChange(block.id, newContent as Record<string, unknown>);
|
|
175
175
|
setEditingBlock(null);
|
|
176
176
|
setLazyEditor(null);
|
|
177
177
|
}}
|
|
@@ -38,6 +38,19 @@ interface SectionBlockEditorProps {
|
|
|
38
38
|
blockType: 'section' | 'hero';
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
type AnyBlockContent = {
|
|
42
|
+
html_content?: string;
|
|
43
|
+
text_content?: string;
|
|
44
|
+
level?: number;
|
|
45
|
+
alt_text?: string;
|
|
46
|
+
media_id?: string;
|
|
47
|
+
text?: string;
|
|
48
|
+
url?: string;
|
|
49
|
+
title?: string;
|
|
50
|
+
columns?: number;
|
|
51
|
+
postsPerPage?: number;
|
|
52
|
+
};
|
|
53
|
+
|
|
41
54
|
export default function SectionBlockEditor({
|
|
42
55
|
content,
|
|
43
56
|
onChange,
|
|
@@ -67,8 +80,9 @@ export default function SectionBlockEditor({
|
|
|
67
80
|
}, [content]);
|
|
68
81
|
|
|
69
82
|
|
|
83
|
+
type ColumnBlock = SectionBlockContent['column_blocks'][0][0];
|
|
70
84
|
const [activeId, setActiveId] = useState<string | null>(null);
|
|
71
|
-
const [draggedBlock, setDraggedBlock] = useState<
|
|
85
|
+
const [draggedBlock, setDraggedBlock] = useState<ColumnBlock | null>(null);
|
|
72
86
|
|
|
73
87
|
// DND sensors for cross-column dragging
|
|
74
88
|
const sensors = useSensors(
|
|
@@ -292,40 +306,47 @@ return (
|
|
|
292
306
|
</span>
|
|
293
307
|
</div>
|
|
294
308
|
<div className="text-xs text-blue-600 dark:text-blue-400 mt-1">
|
|
295
|
-
{
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
draggedBlock.
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
309
|
+
{(() => {
|
|
310
|
+
const content = draggedBlock.content as AnyBlockContent;
|
|
311
|
+
return (
|
|
312
|
+
<>
|
|
313
|
+
{draggedBlock.block_type === "text" && (
|
|
314
|
+
<div
|
|
315
|
+
dangerouslySetInnerHTML={{
|
|
316
|
+
__html:
|
|
317
|
+
(
|
|
318
|
+
content.html_content || "Empty text"
|
|
319
|
+
).substring(0, 30) + "...",
|
|
320
|
+
}}
|
|
321
|
+
/>
|
|
322
|
+
)}
|
|
323
|
+
{draggedBlock.block_type === "heading" && (
|
|
324
|
+
<div>
|
|
325
|
+
H{content.level || 1}:{" "}
|
|
326
|
+
{(
|
|
327
|
+
content.text_content || "Empty heading"
|
|
328
|
+
).substring(0, 20) + "..."}
|
|
329
|
+
</div>
|
|
330
|
+
)}
|
|
331
|
+
{draggedBlock.block_type === "image" && (
|
|
332
|
+
<div>
|
|
333
|
+
Image: {content.alt_text || "No alt text"}
|
|
334
|
+
</div>
|
|
335
|
+
)}
|
|
336
|
+
{draggedBlock.block_type === "button" && (
|
|
337
|
+
<div>Button: {content.text || "No text"}</div>
|
|
338
|
+
)}
|
|
339
|
+
{draggedBlock.block_type === "video_embed" && (
|
|
340
|
+
<div>Video: {content.title || "No title"}</div>
|
|
341
|
+
)}
|
|
342
|
+
{draggedBlock.block_type === "posts_grid" && (
|
|
343
|
+
<div>
|
|
344
|
+
Posts Grid: {content.columns || 3} cols
|
|
345
|
+
</div>
|
|
346
|
+
)}
|
|
347
|
+
</>
|
|
348
|
+
);
|
|
349
|
+
})()}
|
|
329
350
|
</div>
|
|
330
351
|
</div>
|
|
331
352
|
) : null}
|
|
@@ -30,7 +30,7 @@ export default function TextBlockEditor({
|
|
|
30
30
|
}: BlockEditorProps<Partial<TextBlockContent>>) {
|
|
31
31
|
const labelId = useId();
|
|
32
32
|
const [pickerOpen, setPickerOpen] = useState(false);
|
|
33
|
-
const resolverRef = useRef<null | ((v:
|
|
33
|
+
const resolverRef = useRef<null | ((v: { src: string; alt?: string; width?: number | null; height?: number | null; blurDataURL?: string | null }) => void)>(null);
|
|
34
34
|
const R2_BASE_URL = process.env.NEXT_PUBLIC_R2_BASE_URL || '';
|
|
35
35
|
const openImagePicker = useCallback(() => {
|
|
36
36
|
setPickerOpen(true);
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import { createClient } from "@nextblock-cms/db/server";
|
|
5
5
|
import { revalidatePath } from "next/cache";
|
|
6
|
-
import type { Database } from "@nextblock-cms/db";
|
|
6
|
+
import type { Database, Json } from "@nextblock-cms/db";
|
|
7
7
|
import { encodedRedirect } from "@nextblock-cms/utils/server";
|
|
8
8
|
|
|
9
9
|
type Media = Database['public']['Tables']['media']['Row'];
|
|
@@ -111,7 +111,7 @@ export async function recordMediaUpload(payload: {
|
|
|
111
111
|
description: computedDescription,
|
|
112
112
|
width: primaryVariant.width,
|
|
113
113
|
height: primaryVariant.height,
|
|
114
|
-
variants: allVariantsToStore as
|
|
114
|
+
variants: allVariantsToStore as unknown as Json, // Store all variants including the original
|
|
115
115
|
blur_data_url: payload.blurDataUrl || null, // Store if provided
|
|
116
116
|
// Ensure all other required fields for 'Media' type are present or nullable
|
|
117
117
|
};
|
|
@@ -201,24 +201,24 @@ export async function deleteMediaItem(mediaId: string, objectKey: string) {
|
|
|
201
201
|
return encodedRedirect("error", "/cms/media", "Forbidden: Insufficient permissions.");
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
-
const { DeleteObjectCommand } = await import("@aws-sdk/client-s3");
|
|
205
|
-
const { getS3Client } = await import("@nextblock-cms/utils/server");
|
|
206
|
-
const s3Client = await getS3Client();
|
|
207
|
-
const R2_BUCKET_NAME = process.env.R2_BUCKET_NAME;
|
|
208
|
-
|
|
209
|
-
if (!R2_BUCKET_NAME) {
|
|
210
|
-
return encodedRedirect("error", "/cms/media", "R2 Bucket not configured for deletion.");
|
|
211
|
-
}
|
|
212
|
-
if (!s3Client) {
|
|
213
|
-
return encodedRedirect("error", "/cms/media", "R2 client is not configured for deletion.");
|
|
214
|
-
}
|
|
204
|
+
const { DeleteObjectCommand } = await import("@aws-sdk/client-s3");
|
|
205
|
+
const { getS3Client } = await import("@nextblock-cms/utils/server");
|
|
206
|
+
const s3Client = await getS3Client();
|
|
207
|
+
const R2_BUCKET_NAME = process.env.R2_BUCKET_NAME;
|
|
208
|
+
|
|
209
|
+
if (!R2_BUCKET_NAME) {
|
|
210
|
+
return encodedRedirect("error", "/cms/media", "R2 Bucket not configured for deletion.");
|
|
211
|
+
}
|
|
212
|
+
if (!s3Client) {
|
|
213
|
+
return encodedRedirect("error", "/cms/media", "R2 client is not configured for deletion.");
|
|
214
|
+
}
|
|
215
215
|
|
|
216
216
|
try {
|
|
217
217
|
const deleteCommand = new DeleteObjectCommand({
|
|
218
218
|
Bucket: R2_BUCKET_NAME,
|
|
219
219
|
Key: objectKey,
|
|
220
220
|
});
|
|
221
|
-
await s3Client.send(deleteCommand);
|
|
221
|
+
await s3Client.send(deleteCommand);
|
|
222
222
|
} catch (r2Error: unknown) {
|
|
223
223
|
console.error("Error deleting from R2:", r2Error);
|
|
224
224
|
// Decide if you want to proceed with DB deletion if R2 deletion fails
|
|
@@ -255,17 +255,17 @@ export async function deleteMultipleMediaItems(items: Array<{ id: string; object
|
|
|
255
255
|
return { error: "No items selected for deletion." };
|
|
256
256
|
}
|
|
257
257
|
|
|
258
|
-
const { DeleteObjectsCommand } = await import("@aws-sdk/client-s3"); // Use DeleteObjects for batch
|
|
259
|
-
const { getS3Client } = await import("@nextblock-cms/utils/server");
|
|
260
|
-
const s3Client = await getS3Client();
|
|
261
|
-
const R2_BUCKET_NAME = process.env.R2_BUCKET_NAME;
|
|
262
|
-
|
|
263
|
-
if (!R2_BUCKET_NAME) {
|
|
264
|
-
return { error: "R2 Bucket not configured for deletion." };
|
|
265
|
-
}
|
|
266
|
-
if (!s3Client) {
|
|
267
|
-
return { error: "R2 client is not configured for deletion." };
|
|
268
|
-
}
|
|
258
|
+
const { DeleteObjectsCommand } = await import("@aws-sdk/client-s3"); // Use DeleteObjects for batch
|
|
259
|
+
const { getS3Client } = await import("@nextblock-cms/utils/server");
|
|
260
|
+
const s3Client = await getS3Client();
|
|
261
|
+
const R2_BUCKET_NAME = process.env.R2_BUCKET_NAME;
|
|
262
|
+
|
|
263
|
+
if (!R2_BUCKET_NAME) {
|
|
264
|
+
return { error: "R2 Bucket not configured for deletion." };
|
|
265
|
+
}
|
|
266
|
+
if (!s3Client) {
|
|
267
|
+
return { error: "R2 client is not configured for deletion." };
|
|
268
|
+
}
|
|
269
269
|
|
|
270
270
|
const r2ObjectsToDelete = items.map(item => ({ Key: item.objectKey }));
|
|
271
271
|
const itemIdsToDelete = items.map(item => item.id);
|
|
@@ -345,18 +345,18 @@ export async function moveMultipleMediaItems(
|
|
|
345
345
|
};
|
|
346
346
|
const folder = sanitizeFolder(destinationFolder);
|
|
347
347
|
|
|
348
|
-
const { CopyObjectCommand, DeleteObjectCommand, ListObjectsV2Command, HeadObjectCommand } = await import("@aws-sdk/client-s3");
|
|
349
|
-
const { getS3Client } = await import("@nextblock-cms/utils/server");
|
|
350
|
-
const s3Client = await getS3Client();
|
|
351
|
-
const R2_BUCKET_NAME = process.env.R2_BUCKET_NAME;
|
|
352
|
-
const R2_PUBLIC_URL_BASE = process.env.NEXT_PUBLIC_R2_BASE_URL || '';
|
|
353
|
-
|
|
354
|
-
if (!R2_BUCKET_NAME) {
|
|
355
|
-
return { error: "R2 Bucket not configured for move." };
|
|
356
|
-
}
|
|
357
|
-
if (!s3Client) {
|
|
358
|
-
return { error: "R2 client is not configured for move." };
|
|
359
|
-
}
|
|
348
|
+
const { CopyObjectCommand, DeleteObjectCommand, ListObjectsV2Command, HeadObjectCommand } = await import("@aws-sdk/client-s3");
|
|
349
|
+
const { getS3Client } = await import("@nextblock-cms/utils/server");
|
|
350
|
+
const s3Client = await getS3Client();
|
|
351
|
+
const R2_BUCKET_NAME = process.env.R2_BUCKET_NAME;
|
|
352
|
+
const R2_PUBLIC_URL_BASE = process.env.NEXT_PUBLIC_R2_BASE_URL || '';
|
|
353
|
+
|
|
354
|
+
if (!R2_BUCKET_NAME) {
|
|
355
|
+
return { error: "R2 Bucket not configured for move." };
|
|
356
|
+
}
|
|
357
|
+
if (!s3Client) {
|
|
358
|
+
return { error: "R2 client is not configured for move." };
|
|
359
|
+
}
|
|
360
360
|
|
|
361
361
|
if (!items || items.length === 0) {
|
|
362
362
|
return { error: "No items selected for move." };
|
|
@@ -382,9 +382,9 @@ export async function moveMultipleMediaItems(
|
|
|
382
382
|
let newMainKey = `${folder}${getFilename(mediaRow.object_key)}`;
|
|
383
383
|
|
|
384
384
|
// Build list of keys to move: primary + variant keys (if any)
|
|
385
|
-
type Variant =
|
|
386
|
-
const oldVariants: Variant[] = Array.isArray(mediaRow.variants) ? (mediaRow.variants as Variant[]) :
|
|
387
|
-
(typeof mediaRow.variants === 'object' && mediaRow.variants !== null ? (mediaRow.variants as
|
|
385
|
+
type Variant = ImageVariant & { [k: string]: unknown };
|
|
386
|
+
const oldVariants: Variant[] = Array.isArray(mediaRow.variants) ? (mediaRow.variants as unknown as Variant[]) :
|
|
387
|
+
(typeof mediaRow.variants === 'object' && mediaRow.variants !== null ? (mediaRow.variants as unknown as Variant[]) : []);
|
|
388
388
|
|
|
389
389
|
const variantMoves = (oldVariants || []).map((v) => ({
|
|
390
390
|
oldKey: v.objectKey,
|
|
@@ -429,9 +429,9 @@ export async function moveMultipleMediaItems(
|
|
|
429
429
|
await s3Client.send(new DeleteObjectCommand({ Bucket: R2_BUCKET_NAME, Key: oldKey }));
|
|
430
430
|
movedKeys.add(oldKey);
|
|
431
431
|
if (isMain) mainMoved = true;
|
|
432
|
-
} catch (err:
|
|
433
|
-
const name = err?.name || '';
|
|
434
|
-
const message = err?.message || String(err);
|
|
432
|
+
} catch (err: unknown) {
|
|
433
|
+
const name = (err as Error)?.name || '';
|
|
434
|
+
const message = (err as Error)?.message || String(err);
|
|
435
435
|
if (isMain) {
|
|
436
436
|
// Main object missing: attempt fallback to any existing variant
|
|
437
437
|
let promoted = false;
|
|
@@ -499,7 +499,7 @@ export async function moveMultipleMediaItems(
|
|
|
499
499
|
.map((v) => {
|
|
500
500
|
const filename = getFilename(v.objectKey);
|
|
501
501
|
const updatedKey = `${folder}${filename}`;
|
|
502
|
-
const updated = { ...v, objectKey: updatedKey }
|
|
502
|
+
const updated = { ...v, objectKey: updatedKey };
|
|
503
503
|
if (R2_PUBLIC_URL_BASE) updated.url = `${R2_PUBLIC_URL_BASE}/${updatedKey}`;
|
|
504
504
|
return updated;
|
|
505
505
|
});
|
|
@@ -507,7 +507,7 @@ export async function moveMultipleMediaItems(
|
|
|
507
507
|
// Update DB
|
|
508
508
|
const { error: updateError } = await supabase
|
|
509
509
|
.from('media')
|
|
510
|
-
.update({ object_key: newMainKey, file_path: newMainKey, folder, variants: newVariants as
|
|
510
|
+
.update({ object_key: newMainKey, file_path: newMainKey, folder, variants: newVariants as unknown as Json })
|
|
511
511
|
.eq('id', item.id);
|
|
512
512
|
if (updateError) {
|
|
513
513
|
results.push({ id: item.id, ok: false, error: updateError.message });
|
|
@@ -515,8 +515,8 @@ export async function moveMultipleMediaItems(
|
|
|
515
515
|
}
|
|
516
516
|
|
|
517
517
|
results.push({ id: item.id, ok: true });
|
|
518
|
-
} catch (err:
|
|
519
|
-
const msg = err?.name && err?.message ? `${err.name}: ${err.message}` : (err?.message || String(err));
|
|
518
|
+
} catch (err: unknown) {
|
|
519
|
+
const msg = (err as Error)?.name && (err as Error)?.message ? `${(err as Error).name}: ${(err as Error).message}` : ((err as Error)?.message || String(err));
|
|
520
520
|
results.push({ id: item.id, ok: false, error: msg });
|
|
521
521
|
}
|
|
522
522
|
}
|
|
@@ -103,17 +103,17 @@ export default function MediaGridClient({ initialMediaItems, r2BaseUrl }: MediaG
|
|
|
103
103
|
let hadError = false;
|
|
104
104
|
for (const item of selectedItems) {
|
|
105
105
|
const res = await moveSingleMediaItem(item, dest);
|
|
106
|
-
if (
|
|
106
|
+
if ('error' in res && res.error) {
|
|
107
107
|
// Accumulate errors but keep going
|
|
108
108
|
hadError = true;
|
|
109
|
-
setMoveError((prev) => (prev ? prev + " | " : "") +
|
|
109
|
+
setMoveError((prev) => (prev ? prev + " | " : "") + res.error);
|
|
110
110
|
} else {
|
|
111
111
|
// Update local list
|
|
112
112
|
setMediaItems((prev) => prev.map((m) => {
|
|
113
113
|
if (m.id !== item.id) return m;
|
|
114
114
|
const filename = m.object_key.substring(m.object_key.lastIndexOf('/') + 1);
|
|
115
115
|
const folder = ensureFolderSlash(dest);
|
|
116
|
-
return { ...m, object_key: `${folder}${filename}`, folder }
|
|
116
|
+
return { ...m, object_key: `${folder}${filename}`, folder };
|
|
117
117
|
}));
|
|
118
118
|
}
|
|
119
119
|
moved += 1;
|
|
@@ -259,7 +259,7 @@ export default function MediaUploadForm({ onUploadSuccess, returnJustData, defau
|
|
|
259
259
|
}
|
|
260
260
|
|
|
261
261
|
} catch (err: unknown) {
|
|
262
|
-
const isRedirect = (err instanceof Error && err.message === 'NEXT_REDIRECT') || (typeof (err as
|
|
262
|
+
const isRedirect = (err instanceof Error && err.message === 'NEXT_REDIRECT') || (typeof (err as { digest?: unknown })?.digest === 'string' && ((err as { digest: string }).digest.startsWith('NEXT_REDIRECT')));
|
|
263
263
|
|
|
264
264
|
if (isRedirect && !returnJustData) {
|
|
265
265
|
setUploadStatus("success");
|
|
@@ -51,8 +51,8 @@ async function getDistinctFolders(search?: string): Promise<string[]> {
|
|
|
51
51
|
return [];
|
|
52
52
|
}
|
|
53
53
|
let folders = (data || [])
|
|
54
|
-
.map((r
|
|
55
|
-
.filter((f:
|
|
54
|
+
.map((r) => r.folder)
|
|
55
|
+
.filter((f): f is string => typeof f === 'string' && f.length > 0);
|
|
56
56
|
if (search && search.trim()) {
|
|
57
57
|
const t = search.trim().toLowerCase();
|
|
58
58
|
folders = folders.filter((f: string) => f.toLowerCase().includes(t));
|
|
@@ -71,7 +71,7 @@ async function getFolderCounts(): Promise<Record<string, number>> {
|
|
|
71
71
|
return {};
|
|
72
72
|
}
|
|
73
73
|
const counts: Record<string, number> = {};
|
|
74
|
-
(data || []).forEach((row
|
|
74
|
+
(data || []).forEach((row) => {
|
|
75
75
|
const f: string | null = row.folder;
|
|
76
76
|
if (!f || typeof f !== 'string' || f.length === 0) return;
|
|
77
77
|
const norm = f.endsWith('/') ? f : `${f}/`;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
// apps/nextblock/app/cms/revisions/JsonDiffView.tsx
|
|
2
1
|
"use client";
|
|
3
2
|
|
|
4
3
|
import React, { useMemo } from 'react';
|
|
@@ -11,10 +10,11 @@ interface JsonDiffViewProps {
|
|
|
11
10
|
rightTitle?: string;
|
|
12
11
|
}
|
|
13
12
|
|
|
14
|
-
function getByPointer(obj:
|
|
13
|
+
function getByPointer(obj: unknown, pointer: string): unknown {
|
|
15
14
|
if (!pointer || pointer === '/') return obj;
|
|
16
15
|
const parts = pointer.split('/').slice(1).map(p => p.replace(/~1/g, '/').replace(/~0/g, '~'));
|
|
17
|
-
|
|
16
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
17
|
+
let cur = obj as any;
|
|
18
18
|
for (const part of parts) {
|
|
19
19
|
if (cur == null) return undefined;
|
|
20
20
|
cur = cur[part];
|
|
@@ -22,7 +22,7 @@ function getByPointer(obj: any, pointer: string): any {
|
|
|
22
22
|
return cur;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
function safeStringify(v:
|
|
25
|
+
function safeStringify(v: unknown): string {
|
|
26
26
|
try {
|
|
27
27
|
return JSON.stringify(v, null, 2);
|
|
28
28
|
} catch {
|
|
@@ -32,10 +32,11 @@ function safeStringify(v: any): string {
|
|
|
32
32
|
|
|
33
33
|
export default function JsonDiffView({ oldValue, newValue, leftTitle = 'Current', rightTitle = 'Selected' }: JsonDiffViewProps) {
|
|
34
34
|
const { ops, oldObj } = useMemo(() => {
|
|
35
|
-
let a:
|
|
35
|
+
let a: unknown = null, b: unknown = null;
|
|
36
36
|
try { a = JSON.parse(oldValue); } catch { a = oldValue; }
|
|
37
37
|
try { b = JSON.parse(newValue); } catch { b = newValue; }
|
|
38
|
-
|
|
38
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
39
|
+
const operations: Operation[] = compare(a as any, b as any);
|
|
39
40
|
return { ops: operations, oldObj: a };
|
|
40
41
|
}, [oldValue, newValue]);
|
|
41
42
|
|
|
@@ -56,7 +57,7 @@ export default function JsonDiffView({ oldValue, newValue, leftTitle = 'Current'
|
|
|
56
57
|
{ops.map((op, idx) => {
|
|
57
58
|
const oldAtPath = op.op !== 'add' ? getByPointer(oldObj, op.path) : undefined;
|
|
58
59
|
const oldStr = op.op !== 'add' ? safeStringify(oldAtPath) : '';
|
|
59
|
-
const newStr = op.op !== 'remove' ? safeStringify((op as
|
|
60
|
+
const newStr = op.op !== 'remove' && 'value' in op ? safeStringify((op as { value: unknown }).value) : '';
|
|
60
61
|
return (
|
|
61
62
|
<div key={idx} className="rounded border">
|
|
62
63
|
<div className="px-2 py-1 border-b flex items-center gap-2 text-xs">
|
|
@@ -54,18 +54,24 @@ export default function RevisionHistoryButton({ parentType, parentId }: Revision
|
|
|
54
54
|
try {
|
|
55
55
|
if (parentType === 'page') {
|
|
56
56
|
const res = await listPageRevisions(parentId);
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
57
|
+
if ('error' in res) {
|
|
58
|
+
setError(res.error ?? 'Unknown error');
|
|
59
|
+
setRevisions(null);
|
|
60
|
+
setCurrentVersion(null);
|
|
61
|
+
} else {
|
|
62
|
+
setRevisions(res.revisions as unknown as RevisionItem[]);
|
|
63
|
+
setCurrentVersion(res.currentVersion ?? null);
|
|
64
|
+
}
|
|
65
65
|
} else {
|
|
66
66
|
const res = await listPostRevisions(parentId);
|
|
67
|
-
if ('error' in res) {
|
|
68
|
-
|
|
67
|
+
if ('error' in res) {
|
|
68
|
+
setError(res.error ?? 'Unknown error');
|
|
69
|
+
setRevisions(null);
|
|
70
|
+
setCurrentVersion(null);
|
|
71
|
+
} else {
|
|
72
|
+
setRevisions(res.revisions as unknown as RevisionItem[]);
|
|
73
|
+
setCurrentVersion(res.currentVersion ?? null);
|
|
74
|
+
}
|
|
69
75
|
}
|
|
70
76
|
} catch (e: unknown) {
|
|
71
77
|
setError(e instanceof Error ? e.message : 'Failed to load revisions');
|