create-nextblock 0.2.31 → 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/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/lib/blocks/blockRegistry.ts +29 -29
- package/templates/nextblock-template/package.json +1 -1
package/package.json
CHANGED
package/scripts/sync-template.js
CHANGED
|
@@ -36,16 +36,17 @@ const UI_PROXY_MODULES = [
|
|
|
36
36
|
'ui',
|
|
37
37
|
];
|
|
38
38
|
|
|
39
|
-
const IGNORED_SEGMENTS = new Set([
|
|
40
|
-
'node_modules',
|
|
41
|
-
'.git',
|
|
42
|
-
'.next',
|
|
43
|
-
'dist',
|
|
44
|
-
'tmp',
|
|
45
|
-
'coverage',
|
|
46
|
-
'backup',
|
|
47
|
-
'backups',
|
|
48
|
-
]);
|
|
39
|
+
const IGNORED_SEGMENTS = new Set([
|
|
40
|
+
'node_modules',
|
|
41
|
+
'.git',
|
|
42
|
+
'.next',
|
|
43
|
+
'dist',
|
|
44
|
+
'tmp',
|
|
45
|
+
'coverage',
|
|
46
|
+
'backup',
|
|
47
|
+
'backups',
|
|
48
|
+
]);
|
|
49
|
+
|
|
49
50
|
|
|
50
51
|
async function ensureTemplateSync() {
|
|
51
52
|
const sourceExists = await fs.pathExists(SOURCE_DIR);
|
|
@@ -64,7 +65,7 @@ async function ensureTemplateSync() {
|
|
|
64
65
|
);
|
|
65
66
|
|
|
66
67
|
await fs.ensureDir(TARGET_DIR);
|
|
67
|
-
await
|
|
68
|
+
await emptyDirWithRetry(TARGET_DIR);
|
|
68
69
|
|
|
69
70
|
await fs.copy(SOURCE_DIR, TARGET_DIR, {
|
|
70
71
|
dereference: true,
|
|
@@ -79,10 +80,10 @@ async function ensureTemplateSync() {
|
|
|
79
80
|
},
|
|
80
81
|
});
|
|
81
82
|
|
|
82
|
-
await ensureEnvExample();
|
|
83
|
-
await ensureTemplateGitignore();
|
|
84
|
-
await ensureGlobalStyles();
|
|
85
|
-
await ensureClientTranslations();
|
|
83
|
+
await ensureEnvExample();
|
|
84
|
+
await ensureTemplateGitignore();
|
|
85
|
+
await ensureGlobalStyles();
|
|
86
|
+
await ensureClientTranslations();
|
|
86
87
|
await sanitizeBlockEditorImports();
|
|
87
88
|
await sanitizeUiImports();
|
|
88
89
|
await ensureUiProxies();
|
|
@@ -92,7 +93,7 @@ async function ensureTemplateSync() {
|
|
|
92
93
|
console.log(chalk.green('Template sync complete.'));
|
|
93
94
|
}
|
|
94
95
|
|
|
95
|
-
async function ensureEnvExample() {
|
|
96
|
+
async function ensureEnvExample() {
|
|
96
97
|
const envTargets = [
|
|
97
98
|
resolve(REPO_ROOT, '.env.example'),
|
|
98
99
|
resolve(REPO_ROOT, '.env.exemple'),
|
|
@@ -118,42 +119,42 @@ NEXT_PUBLIC_URL=http://localhost:3000
|
|
|
118
119
|
`;
|
|
119
120
|
|
|
120
121
|
await fs.writeFile(destination, placeholder);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
async function ensureTemplateGitignore() {
|
|
124
|
-
const destination = resolve(TARGET_DIR, '.gitignore');
|
|
125
|
-
const content = `.DS_Store
|
|
126
|
-
node_modules
|
|
127
|
-
dist
|
|
128
|
-
.next
|
|
129
|
-
out
|
|
130
|
-
build
|
|
131
|
-
coverage
|
|
132
|
-
*.log
|
|
133
|
-
logs
|
|
134
|
-
npm-debug.log*
|
|
135
|
-
yarn-debug.log*
|
|
136
|
-
yarn-error.log*
|
|
137
|
-
pnpm-debug.log*
|
|
138
|
-
|
|
139
|
-
.env
|
|
140
|
-
.env.*
|
|
141
|
-
.env.local
|
|
142
|
-
.env.development.local
|
|
143
|
-
.env.production.local
|
|
144
|
-
.env.test.local
|
|
145
|
-
|
|
146
|
-
.vscode
|
|
147
|
-
.idea
|
|
148
|
-
.swp
|
|
149
|
-
*.sw?
|
|
150
|
-
|
|
151
|
-
supabase/.temp
|
|
152
|
-
supabase/.branches
|
|
153
|
-
`;
|
|
154
|
-
await fs.outputFile(destination, content);
|
|
155
|
-
}
|
|
156
|
-
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function ensureTemplateGitignore() {
|
|
125
|
+
const destination = resolve(TARGET_DIR, '.gitignore');
|
|
126
|
+
const content = `.DS_Store
|
|
127
|
+
node_modules
|
|
128
|
+
dist
|
|
129
|
+
.next
|
|
130
|
+
out
|
|
131
|
+
build
|
|
132
|
+
coverage
|
|
133
|
+
*.log
|
|
134
|
+
logs
|
|
135
|
+
npm-debug.log*
|
|
136
|
+
yarn-debug.log*
|
|
137
|
+
yarn-error.log*
|
|
138
|
+
pnpm-debug.log*
|
|
139
|
+
|
|
140
|
+
.env
|
|
141
|
+
.env.*
|
|
142
|
+
.env.local
|
|
143
|
+
.env.development.local
|
|
144
|
+
.env.production.local
|
|
145
|
+
.env.test.local
|
|
146
|
+
|
|
147
|
+
.vscode
|
|
148
|
+
.idea
|
|
149
|
+
.swp
|
|
150
|
+
*.sw?
|
|
151
|
+
|
|
152
|
+
supabase/.temp
|
|
153
|
+
supabase/.branches
|
|
154
|
+
`;
|
|
155
|
+
await fs.outputFile(destination, content);
|
|
156
|
+
}
|
|
157
|
+
|
|
157
158
|
async function ensureGlobalStyles() {
|
|
158
159
|
const destination = resolve(TARGET_DIR, 'app/globals.css');
|
|
159
160
|
|
|
@@ -312,3 +313,20 @@ ensureTemplateSync().catch((error) => {
|
|
|
312
313
|
console.error(chalk.red(error instanceof Error ? error.message : String(error)));
|
|
313
314
|
process.exit(1);
|
|
314
315
|
});
|
|
316
|
+
|
|
317
|
+
async function emptyDirWithRetry(dir, retries = 5, delay = 1000) {
|
|
318
|
+
for (let i = 0; i < retries; i++) {
|
|
319
|
+
try {
|
|
320
|
+
await fs.emptyDir(dir);
|
|
321
|
+
return;
|
|
322
|
+
} catch (err) {
|
|
323
|
+
if (i === retries - 1) throw err;
|
|
324
|
+
if (err.code === 'EBUSY' || err.code === 'EPERM') {
|
|
325
|
+
console.log(chalk.yellow(`Locked file encountered. Retrying in ${delay}ms... (${i + 1}/${retries})`));
|
|
326
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
327
|
+
} else {
|
|
328
|
+
throw err;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
@@ -5,22 +5,22 @@ import { createClient } from "@nextblock-cms/db/server";
|
|
|
5
5
|
import { revalidatePath } from "next/cache";
|
|
6
6
|
import type { Database, Json } from "@nextblock-cms/db";
|
|
7
7
|
import { getInitialContent, isValidBlockType } from "../../../lib/blocks/blockRegistry";
|
|
8
|
-
import { getFullPageContent, getFullPostContent } from "../revisions/utils";
|
|
8
|
+
import { getFullPageContent, getFullPostContent, type FullPageContent, type FullPostContent } from "../revisions/utils";
|
|
9
9
|
import { createPageRevision, createPostRevision } from "../revisions/service";
|
|
10
10
|
|
|
11
11
|
type Block = Database['public']['Tables']['blocks']['Row'];
|
|
12
12
|
type BlockType = Database['public']['Tables']['blocks']['Row']['block_type'];
|
|
13
13
|
|
|
14
14
|
// Helper to verify user can edit the parent (page/post)
|
|
15
|
-
async function canEditParent(
|
|
16
|
-
supabase: ReturnType<typeof createClient>,
|
|
17
|
-
userId: string,
|
|
18
|
-
pageId?: number | null,
|
|
19
|
-
postId?: number | null
|
|
20
|
-
): Promise<boolean> {
|
|
21
|
-
void pageId;
|
|
22
|
-
void postId;
|
|
23
|
-
const { data: profile } = await supabase
|
|
15
|
+
async function canEditParent(
|
|
16
|
+
supabase: ReturnType<typeof createClient>,
|
|
17
|
+
userId: string,
|
|
18
|
+
pageId?: number | null,
|
|
19
|
+
postId?: number | null
|
|
20
|
+
): Promise<boolean> {
|
|
21
|
+
void pageId;
|
|
22
|
+
void postId;
|
|
23
|
+
const { data: profile } = await supabase
|
|
24
24
|
.from("profiles")
|
|
25
25
|
.select("role")
|
|
26
26
|
.eq("id", userId)
|
|
@@ -160,7 +160,7 @@ export async function updateBlock(blockId: number, newContent: unknown, pageId?:
|
|
|
160
160
|
return { error: "Block not found." };
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
-
let prevContentAggregate:
|
|
163
|
+
let prevContentAggregate: FullPageContent | FullPostContent | null = null;
|
|
164
164
|
if (existingBlock.page_id) {
|
|
165
165
|
prevContentAggregate = await getFullPageContent(existingBlock.page_id);
|
|
166
166
|
} else if (existingBlock.post_id) {
|
|
@@ -189,7 +189,7 @@ export async function updateBlock(blockId: number, newContent: unknown, pageId?:
|
|
|
189
189
|
} else if (existingBlock.post_id) {
|
|
190
190
|
const nextContentAggregate = await getFullPostContent(existingBlock.post_id, { overrideBlockId: blockId, overrideBlockContent: newContent });
|
|
191
191
|
if (nextContentAggregate) {
|
|
192
|
-
await createPostRevision(existingBlock.post_id, user.id, prevContentAggregate as
|
|
192
|
+
await createPostRevision(existingBlock.post_id, user.id, prevContentAggregate as FullPostContent, nextContentAggregate as FullPostContent);
|
|
193
193
|
}
|
|
194
194
|
}
|
|
195
195
|
}
|
|
@@ -250,7 +250,7 @@ export async function deleteBlock(blockId: number, pageId?: number | null, postI
|
|
|
250
250
|
return { error: "Block not found." };
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
-
let previousAggregate:
|
|
253
|
+
let previousAggregate: FullPageContent | FullPostContent | null = null;
|
|
254
254
|
if (existingBlock.page_id) {
|
|
255
255
|
previousAggregate = await getFullPageContent(existingBlock.page_id);
|
|
256
256
|
} else if (existingBlock.post_id) {
|
|
@@ -274,7 +274,7 @@ export async function deleteBlock(blockId: number, pageId?: number | null, postI
|
|
|
274
274
|
} else if (existingBlock.post_id) {
|
|
275
275
|
const nextAggregate = await getFullPostContent(existingBlock.post_id, { excludeDeletedBlockId: blockId });
|
|
276
276
|
if (nextAggregate) {
|
|
277
|
-
await createPostRevision(existingBlock.post_id, user.id, previousAggregate as
|
|
277
|
+
await createPostRevision(existingBlock.post_id, user.id, previousAggregate as FullPostContent, nextAggregate as FullPostContent);
|
|
278
278
|
}
|
|
279
279
|
}
|
|
280
280
|
}
|
|
@@ -418,7 +418,7 @@ export async function copyBlocksFromLanguage(
|
|
|
418
418
|
console.warn("Could not fetch target post slug for revalidation:", postError);
|
|
419
419
|
} else {
|
|
420
420
|
targetSlug = postData.slug;
|
|
421
|
-
if (targetSlug) revalidatePath(`/article/${targetSlug}`);
|
|
421
|
+
if (targetSlug) revalidatePath(`/article/${targetSlug}`);
|
|
422
422
|
}
|
|
423
423
|
revalidatePath(`/cms/posts/${parentId}/edit`); // Revalidate edit page
|
|
424
424
|
}
|
|
@@ -21,6 +21,8 @@ interface BackgroundSelectorProps {
|
|
|
21
21
|
onChange: (newBackground: SectionBlockContent["background"]) => void;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
type ChangeEventLike = { target: { name: string; value: string } };
|
|
25
|
+
|
|
24
26
|
export default function BackgroundSelector({ background, onChange }: BackgroundSelectorProps) {
|
|
25
27
|
|
|
26
28
|
const backgroundType = background?.type || "none";
|
|
@@ -127,12 +129,12 @@ export default function BackgroundSelector({ background, onChange }: BackgroundS
|
|
|
127
129
|
}
|
|
128
130
|
};
|
|
129
131
|
|
|
130
|
-
const handleBackgroundPropertyChange = (e:
|
|
132
|
+
const handleBackgroundPropertyChange = (e: ChangeEventLike) => {
|
|
131
133
|
const { name, value } = e.target;
|
|
132
134
|
onChange({ ...background, [name]: value });
|
|
133
135
|
};
|
|
134
136
|
|
|
135
|
-
const handleOverlayGradientChange = (e:
|
|
137
|
+
const handleOverlayGradientChange = (e: ChangeEventLike) => {
|
|
136
138
|
const { name, value } = e.target;
|
|
137
139
|
if (background?.type === "image" && background.image) {
|
|
138
140
|
const { image } = background;
|
|
@@ -170,7 +172,7 @@ export default function BackgroundSelector({ background, onChange }: BackgroundS
|
|
|
170
172
|
<div className="space-y-4">
|
|
171
173
|
<div className="grid gap-2">
|
|
172
174
|
<Label>Background Type</Label>
|
|
173
|
-
<Select value={backgroundType} onValueChange={(v) => handleTypeChange(v as
|
|
175
|
+
<Select value={backgroundType} onValueChange={(v) => handleTypeChange(v as SectionBlockContent["background"]["type"])}>
|
|
174
176
|
<SelectTrigger className="w-full max-w-[250px]"><SelectValue placeholder="Select type" /></SelectTrigger>
|
|
175
177
|
<SelectContent>
|
|
176
178
|
<SelectItem value="none">None</SelectItem>
|
|
@@ -184,7 +186,7 @@ export default function BackgroundSelector({ background, onChange }: BackgroundS
|
|
|
184
186
|
<Label htmlFor="min_height">Minimum Height (e.g., 250px)</Label>
|
|
185
187
|
<div className="flex items-center gap-2">
|
|
186
188
|
<Input id="min_height" name="min_height" value={minHeight} onChange={(e) => setMinHeight(e.target.value)} placeholder="e.g., 250px" className="max-w-[200px]" />
|
|
187
|
-
<Button type="button" variant="ghost" size="icon" onClick={() => handleBackgroundPropertyChange({ target: { name: "min_height", value: minHeight } }
|
|
189
|
+
<Button type="button" variant="ghost" size="icon" onClick={() => handleBackgroundPropertyChange({ target: { name: "min_height", value: minHeight } })} disabled={!hasMinHeightChanged} title="Save Minimum Height">
|
|
188
190
|
<Save className={cn("h-5 w-5", hasMinHeightChanged && "text-green-600")} />
|
|
189
191
|
</Button>
|
|
190
192
|
</div>
|
|
@@ -272,7 +274,7 @@ export default function BackgroundSelector({ background, onChange }: BackgroundS
|
|
|
272
274
|
]}
|
|
273
275
|
/>
|
|
274
276
|
</div>
|
|
275
|
-
<Button size="icon" variant="ghost" onClick={() => handleOverlayGradientChange({ target: { name: "direction", value: overlayDirection } }
|
|
277
|
+
<Button size="icon" variant="ghost" onClick={() => handleOverlayGradientChange({ target: { name: "direction", value: overlayDirection } })} disabled={!hasOverlayDirectionChanged} title="Save Overlay Direction">
|
|
276
278
|
<Save className={cn("h-5 w-5 mt-[1.3rem]", hasOverlayDirectionChanged && "text-green-600")} />
|
|
277
279
|
</Button>
|
|
278
280
|
</div>
|
|
@@ -280,12 +282,12 @@ export default function BackgroundSelector({ background, onChange }: BackgroundS
|
|
|
280
282
|
<ColorPicker
|
|
281
283
|
label="Start Color"
|
|
282
284
|
color={selectedImage.overlay.gradient?.stops?.[0]?.color || "rgba(0,0,0,0.5)"}
|
|
283
|
-
onChange={(color) => handleOverlayGradientChange({ target: { name: "startColor", value: color } }
|
|
285
|
+
onChange={(color) => handleOverlayGradientChange({ target: { name: "startColor", value: color } })}
|
|
284
286
|
/>
|
|
285
287
|
<ColorPicker
|
|
286
288
|
label="End Color"
|
|
287
289
|
color={selectedImage.overlay.gradient?.stops?.[1]?.color || "rgba(0,0,0,0)"}
|
|
288
|
-
onChange={(color) => handleOverlayGradientChange({ target: { name: "endColor", value: color } }
|
|
290
|
+
onChange={(color) => handleOverlayGradientChange({ target: { name: "endColor", value: color } })}
|
|
289
291
|
/>
|
|
290
292
|
</div>
|
|
291
293
|
</div>
|
|
@@ -300,7 +302,7 @@ export default function BackgroundSelector({ background, onChange }: BackgroundS
|
|
|
300
302
|
label="Direction"
|
|
301
303
|
tooltipContent="Select a preset or enter a custom angle like '45deg' or 'to top left'. See MDN's linear-gradient docs for more options."
|
|
302
304
|
value={background.gradient?.direction || "to right"}
|
|
303
|
-
onChange={(value: string) => handleBackgroundGradientChange({ target: { name: "direction", value } }
|
|
305
|
+
onChange={(value: string) => handleBackgroundGradientChange({ target: { name: "direction", value } })}
|
|
304
306
|
options={[
|
|
305
307
|
{ value: "to right", label: "To Right" },
|
|
306
308
|
{ value: "to left", label: "To Left" },
|
|
@@ -315,12 +317,12 @@ export default function BackgroundSelector({ background, onChange }: BackgroundS
|
|
|
315
317
|
<ColorPicker
|
|
316
318
|
label="Start Color"
|
|
317
319
|
color={background.gradient?.stops?.[0]?.color || "#3b82f6"}
|
|
318
|
-
onChange={(color) => handleBackgroundGradientChange({ target: { name: "startColor", value: color } }
|
|
320
|
+
onChange={(color) => handleBackgroundGradientChange({ target: { name: "startColor", value: color } })}
|
|
319
321
|
/>
|
|
320
322
|
<ColorPicker
|
|
321
323
|
label="End Color"
|
|
322
324
|
color={background.gradient?.stops?.[1]?.color || "#8b5cf6"}
|
|
323
|
-
onChange={(color) => handleBackgroundGradientChange({ target: { name: "endColor", value: color } }
|
|
325
|
+
onChange={(color) => handleBackgroundGradientChange({ target: { name: "endColor", value: color } })}
|
|
324
326
|
/>
|
|
325
327
|
</div>
|
|
326
328
|
</div>
|
|
@@ -328,8 +330,8 @@ export default function BackgroundSelector({ background, onChange }: BackgroundS
|
|
|
328
330
|
</div>
|
|
329
331
|
</TooltipProvider>
|
|
330
332
|
);
|
|
331
|
-
const handleBackgroundGradientChange = (e:
|
|
332
|
-
const { name, value } = e.target
|
|
333
|
+
const handleBackgroundGradientChange = (e: ChangeEventLike) => {
|
|
334
|
+
const { name, value } = e.target;
|
|
333
335
|
if (backgroundType !== 'gradient') return;
|
|
334
336
|
const current = background.gradient || { type: 'linear' as const, direction: 'to right', stops: [ { color: '#3b82f6', position: 0 }, { color: '#8b5cf6', position: 100 } ] };
|
|
335
337
|
if (name === 'direction') {
|
|
@@ -82,7 +82,7 @@ export default function BlockEditorArea({ parentId, parentType, initialBlocks, l
|
|
|
82
82
|
const [activeBlock, setActiveBlock] = useState<Block | null>(null);
|
|
83
83
|
const [insertionIndex, setInsertionIndex] = useState<number | null>(null);
|
|
84
84
|
const [editingNestedBlockInfo, setEditingNestedBlockInfo] = useState<EditingNestedBlockInfo | null>(null);
|
|
85
|
-
const [NestedBlockEditorComponent, setNestedBlockEditorComponent] = useState<ComponentType<
|
|
85
|
+
const [NestedBlockEditorComponent, setNestedBlockEditorComponent] = useState<ComponentType<Record<string, unknown>> | null>(null);
|
|
86
86
|
const [tempNestedBlockContent, setTempNestedBlockContent] = useState<Json | null>(null);
|
|
87
87
|
|
|
88
88
|
useEffect(() => {
|
|
@@ -129,41 +129,41 @@ export default function BlockEditorArea({ parentId, parentType, initialBlocks, l
|
|
|
129
129
|
debouncedSave(updatedBlock);
|
|
130
130
|
};
|
|
131
131
|
|
|
132
|
-
const DynamicTextBlockEditor = dynamic(() => import('../editors/TextBlockEditor').then(mod => mod.default), { loading: () => <p>Loading editor...</p> });
|
|
133
|
-
const DynamicHeadingBlockEditor = dynamic(() => import('../editors/HeadingBlockEditor').then(mod => mod.default), { loading: () => <p>Loading editor...</p> });
|
|
134
|
-
const DynamicImageBlockEditor = dynamic(() => import('../editors/ImageBlockEditor').then(mod => mod.default), { loading: () => <p>Loading editor...</p> });
|
|
135
|
-
const DynamicButtonBlockEditor = dynamic(() => import('../editors/ButtonBlockEditor').then(mod => mod.default), { loading: () => <p>Loading editor...</p> });
|
|
136
|
-
const DynamicPostsGridBlockEditor = dynamic(() => import('../editors/PostsGridBlockEditor').then(mod => mod.default), { loading: () => <p>Loading editor...</p> });
|
|
137
|
-
const DynamicVideoEmbedBlockEditor = dynamic(() => import('../editors/VideoEmbedBlockEditor').then(mod => mod.default), { loading: () => <p>Loading editor...</p> });
|
|
138
|
-
const DynamicSectionBlockEditor = dynamic(() => import('../editors/SectionBlockEditor').then(mod => mod.default), { loading: () => <p>Loading editor...</p> });
|
|
132
|
+
const DynamicTextBlockEditor = dynamic(() => import('../editors/TextBlockEditor').then(mod => mod.default), { loading: () => <p>Loading editor...</p> });
|
|
133
|
+
const DynamicHeadingBlockEditor = dynamic(() => import('../editors/HeadingBlockEditor').then(mod => mod.default), { loading: () => <p>Loading editor...</p> });
|
|
134
|
+
const DynamicImageBlockEditor = dynamic(() => import('../editors/ImageBlockEditor').then(mod => mod.default), { loading: () => <p>Loading editor...</p> });
|
|
135
|
+
const DynamicButtonBlockEditor = dynamic(() => import('../editors/ButtonBlockEditor').then(mod => mod.default), { loading: () => <p>Loading editor...</p> });
|
|
136
|
+
const DynamicPostsGridBlockEditor = dynamic(() => import('../editors/PostsGridBlockEditor').then(mod => mod.default), { loading: () => <p>Loading editor...</p> });
|
|
137
|
+
const DynamicVideoEmbedBlockEditor = dynamic(() => import('../editors/VideoEmbedBlockEditor').then(mod => mod.default), { loading: () => <p>Loading editor...</p> });
|
|
138
|
+
const DynamicSectionBlockEditor = dynamic(() => import('../editors/SectionBlockEditor').then(mod => mod.default), { loading: () => <p>Loading editor...</p> });
|
|
139
139
|
|
|
140
140
|
useEffect(() => {
|
|
141
141
|
if (editingNestedBlockInfo) {
|
|
142
142
|
const blockType = editingNestedBlockInfo.blockData.block_type;
|
|
143
|
-
let SelectedEditor: React.ComponentType<
|
|
143
|
+
let SelectedEditor: React.ComponentType<Record<string, unknown>> | null = null;
|
|
144
144
|
|
|
145
145
|
try {
|
|
146
146
|
switch (blockType) {
|
|
147
147
|
case 'text':
|
|
148
|
-
SelectedEditor = DynamicTextBlockEditor
|
|
148
|
+
SelectedEditor = DynamicTextBlockEditor as unknown as ComponentType<Record<string, unknown>>;
|
|
149
149
|
break;
|
|
150
150
|
case 'heading':
|
|
151
|
-
SelectedEditor = DynamicHeadingBlockEditor
|
|
151
|
+
SelectedEditor = DynamicHeadingBlockEditor as unknown as ComponentType<Record<string, unknown>>;
|
|
152
152
|
break;
|
|
153
153
|
case 'image':
|
|
154
|
-
SelectedEditor = DynamicImageBlockEditor
|
|
154
|
+
SelectedEditor = DynamicImageBlockEditor as unknown as ComponentType<Record<string, unknown>>;
|
|
155
155
|
break;
|
|
156
156
|
case 'button':
|
|
157
|
-
SelectedEditor = DynamicButtonBlockEditor
|
|
157
|
+
SelectedEditor = DynamicButtonBlockEditor as unknown as ComponentType<Record<string, unknown>>;
|
|
158
158
|
break;
|
|
159
159
|
case 'posts_grid':
|
|
160
|
-
SelectedEditor = DynamicPostsGridBlockEditor
|
|
160
|
+
SelectedEditor = DynamicPostsGridBlockEditor as unknown as ComponentType<Record<string, unknown>>;
|
|
161
161
|
break;
|
|
162
162
|
case 'video_embed':
|
|
163
|
-
SelectedEditor = DynamicVideoEmbedBlockEditor
|
|
163
|
+
SelectedEditor = DynamicVideoEmbedBlockEditor as unknown as ComponentType<Record<string, unknown>>;
|
|
164
164
|
break;
|
|
165
165
|
case 'section':
|
|
166
|
-
SelectedEditor = DynamicSectionBlockEditor
|
|
166
|
+
SelectedEditor = DynamicSectionBlockEditor as unknown as ComponentType<Record<string, unknown>>;
|
|
167
167
|
break;
|
|
168
168
|
default:
|
|
169
169
|
console.warn(`No dynamic editor configured for nested block type: ${blockType}`);
|
|
@@ -389,7 +389,10 @@ export default function BlockEditorArea({ parentId, parentType, initialBlocks, l
|
|
|
389
389
|
parentBlockId: parentBlockIdStr,
|
|
390
390
|
columnIndex,
|
|
391
391
|
blockIndexInColumn,
|
|
392
|
-
blockData:
|
|
392
|
+
blockData: {
|
|
393
|
+
...nestedBlockData,
|
|
394
|
+
content: nestedBlockData.content as unknown as Json
|
|
395
|
+
},
|
|
393
396
|
});
|
|
394
397
|
} else {
|
|
395
398
|
console.error("Nested block not found at specified indices:", { parentBlockIdStr, columnIndex, blockIndexInColumn });
|
|
@@ -444,7 +447,7 @@ export default function BlockEditorArea({ parentId, parentType, initialBlocks, l
|
|
|
444
447
|
onContentChange={handleContentChange}
|
|
445
448
|
onDelete={async (blockIdToDelete) => {
|
|
446
449
|
startTransition(async () => {
|
|
447
|
-
const result = await import("../actions").then(({ deleteBlock }) =>
|
|
450
|
+
const result = await import("../actions").then(({ deleteBlock }) =>
|
|
448
451
|
deleteBlock(
|
|
449
452
|
blockIdToDelete,
|
|
450
453
|
parentType === "page" ? parentId : null,
|
|
@@ -472,8 +475,8 @@ export default function BlockEditorArea({ parentId, parentType, initialBlocks, l
|
|
|
472
475
|
<EditableBlock
|
|
473
476
|
block={activeBlock}
|
|
474
477
|
className="h-full"
|
|
475
|
-
onDelete={() => {}}
|
|
476
|
-
onContentChange={() => {}}
|
|
478
|
+
onDelete={() => {}}
|
|
479
|
+
onContentChange={() => {}}
|
|
477
480
|
/>
|
|
478
481
|
</div>
|
|
479
482
|
) : null}
|
|
@@ -522,7 +525,7 @@ export default function BlockEditorArea({ parentId, parentType, initialBlocks, l
|
|
|
522
525
|
if (blockType === "posts_grid") {
|
|
523
526
|
const fullBlockForEditor: Block = {
|
|
524
527
|
block_type: editingNestedBlockInfo.blockData.block_type,
|
|
525
|
-
content: tempNestedBlockContent,
|
|
528
|
+
content: tempNestedBlockContent as Json,
|
|
526
529
|
id: 0, // Temporary ID for nested blocks
|
|
527
530
|
language_id: languageId,
|
|
528
531
|
order: 0, // Temporary order for nested blocks
|
|
@@ -33,7 +33,7 @@ type BlockEditorModalProps = {
|
|
|
33
33
|
isOpen: boolean;
|
|
34
34
|
onClose: () => void;
|
|
35
35
|
onSave: (updatedContent: unknown) => void;
|
|
36
|
-
EditorComponent: LazyExoticComponent<ComponentType<
|
|
36
|
+
EditorComponent: LazyExoticComponent<ComponentType<any>>;
|
|
37
37
|
};
|
|
38
38
|
|
|
39
39
|
export function BlockEditorModal({
|
|
@@ -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');
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import { createClient } from "@nextblock-cms/db/server";
|
|
5
5
|
import type { Json } from "@nextblock-cms/db";
|
|
6
|
-
import { compare, applyPatch } from 'fast-json-patch';
|
|
6
|
+
import { compare, applyPatch, type Operation } from 'fast-json-patch';
|
|
7
7
|
import type { FullPageContent, FullPostContent } from './utils';
|
|
8
8
|
|
|
9
9
|
|
|
@@ -120,8 +120,8 @@ export async function restorePageToVersion(pageId: number, targetVersion: number
|
|
|
120
120
|
|
|
121
121
|
for (const r of diffs || []) {
|
|
122
122
|
if (r.revision_type === 'diff') {
|
|
123
|
-
const ops = r.content as
|
|
124
|
-
const result = applyPatch(content as
|
|
123
|
+
const ops = r.content as unknown as Operation[];
|
|
124
|
+
const result = applyPatch(content as unknown as Record<string, unknown>, ops, /*validate*/ false, /*mutateDocument*/ true);
|
|
125
125
|
content = result.newDocument as unknown as FullPageContent;
|
|
126
126
|
} else {
|
|
127
127
|
content = r.content as unknown as FullPageContent;
|
|
@@ -208,8 +208,8 @@ export async function restorePostToVersion(postId: number, targetVersion: number
|
|
|
208
208
|
|
|
209
209
|
for (const r of diffs || []) {
|
|
210
210
|
if (r.revision_type === 'diff') {
|
|
211
|
-
const ops = r.content as
|
|
212
|
-
const result = applyPatch(content as
|
|
211
|
+
const ops = r.content as unknown as Operation[];
|
|
212
|
+
const result = applyPatch(content as unknown as Record<string, unknown>, ops, /*validate*/ false, /*mutateDocument*/ true);
|
|
213
213
|
content = result.newDocument as unknown as FullPostContent;
|
|
214
214
|
} else {
|
|
215
215
|
content = r.content as unknown as FullPostContent;
|
|
@@ -296,8 +296,8 @@ export async function reconstructPageVersionContent(pageId: number, targetVersio
|
|
|
296
296
|
|
|
297
297
|
for (const r of diffs || []) {
|
|
298
298
|
if (r.revision_type === 'diff') {
|
|
299
|
-
const ops = r.content as
|
|
300
|
-
const result = applyPatch(content as
|
|
299
|
+
const ops = r.content as unknown as Operation[];
|
|
300
|
+
const result = applyPatch(content as unknown as Record<string, unknown>, ops, false, true);
|
|
301
301
|
content = result.newDocument as unknown as FullPageContent;
|
|
302
302
|
} else {
|
|
303
303
|
content = r.content as unknown as FullPageContent;
|
|
@@ -333,8 +333,8 @@ export async function reconstructPostVersionContent(postId: number, targetVersio
|
|
|
333
333
|
|
|
334
334
|
for (const r of diffs || []) {
|
|
335
335
|
if (r.revision_type === 'diff') {
|
|
336
|
-
const ops = r.content as
|
|
337
|
-
const result = applyPatch(content as
|
|
336
|
+
const ops = r.content as unknown as Operation[];
|
|
337
|
+
const result = applyPatch(content as unknown as Record<string, unknown>, ops, false, true);
|
|
338
338
|
content = result.newDocument as unknown as FullPostContent;
|
|
339
339
|
} else {
|
|
340
340
|
content = r.content as unknown as FullPostContent;
|
|
@@ -59,7 +59,7 @@ export async function getTranslations() {
|
|
|
59
59
|
|
|
60
60
|
export async function updateTranslation(prevState: unknown, formData: FormData) {
|
|
61
61
|
const supabase = createClient();
|
|
62
|
-
const data = Object.fromEntries(formData
|
|
62
|
+
const data = Object.fromEntries(formData.entries());
|
|
63
63
|
const key = data.key as string;
|
|
64
64
|
|
|
65
65
|
if (!key) {
|
|
@@ -13,6 +13,7 @@ export default async function EditLogoPage(props: { params: Promise<{ id: string
|
|
|
13
13
|
return (
|
|
14
14
|
<div>
|
|
15
15
|
<h1 className="text-2xl font-semibold mb-6">Edit Logo</h1>
|
|
16
|
+
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
|
|
16
17
|
<LogoForm logo={logo} action={updateLogo as any} />
|
|
17
18
|
</div>
|
|
18
19
|
)
|
|
@@ -117,21 +117,21 @@ export interface SectionBlockContent {
|
|
|
117
117
|
solid_color?: string;
|
|
118
118
|
min_height?: string;
|
|
119
119
|
gradient?: Gradient;
|
|
120
|
-
image?: {
|
|
121
|
-
media_id: string;
|
|
122
|
-
object_key: string;
|
|
123
|
-
alt_text?: string;
|
|
124
|
-
width?: number;
|
|
125
|
-
height?: number;
|
|
126
|
-
blur_data_url?: string;
|
|
127
|
-
size: 'cover' | 'contain';
|
|
128
|
-
position: 'center' | 'top' | 'bottom' | 'left' | 'right';
|
|
129
|
-
quality?: number | null;
|
|
130
|
-
overlay?: {
|
|
131
|
-
type: 'gradient';
|
|
132
|
-
gradient: Gradient;
|
|
133
|
-
};
|
|
134
|
-
};
|
|
120
|
+
image?: {
|
|
121
|
+
media_id: string;
|
|
122
|
+
object_key: string;
|
|
123
|
+
alt_text?: string;
|
|
124
|
+
width?: number;
|
|
125
|
+
height?: number;
|
|
126
|
+
blur_data_url?: string;
|
|
127
|
+
size: 'cover' | 'contain';
|
|
128
|
+
position: 'center' | 'top' | 'bottom' | 'left' | 'right';
|
|
129
|
+
quality?: number | null;
|
|
130
|
+
overlay?: {
|
|
131
|
+
type: 'gradient';
|
|
132
|
+
gradient: Gradient;
|
|
133
|
+
};
|
|
134
|
+
};
|
|
135
135
|
};
|
|
136
136
|
/** Responsive column configuration */
|
|
137
137
|
responsive_columns: {
|
|
@@ -149,7 +149,7 @@ export interface SectionBlockContent {
|
|
|
149
149
|
/** Array of blocks within columns - 2D array where each index represents a column */
|
|
150
150
|
column_blocks: Array<Array<{
|
|
151
151
|
block_type: BlockType;
|
|
152
|
-
content: Record<string,
|
|
152
|
+
content: Record<string, unknown>;
|
|
153
153
|
temp_id?: string; // For client-side management before save
|
|
154
154
|
}>>;
|
|
155
155
|
}
|
|
@@ -211,7 +211,7 @@ export interface ContentPropertyDefinition {
|
|
|
211
211
|
/** Human-readable description of the property */
|
|
212
212
|
description?: string;
|
|
213
213
|
/** Default value for the property */
|
|
214
|
-
default?:
|
|
214
|
+
default?: unknown;
|
|
215
215
|
/** For union types, the possible values */
|
|
216
216
|
unionValues?: readonly string[];
|
|
217
217
|
/** For array types, the type of array elements */
|
|
@@ -221,7 +221,7 @@ export interface ContentPropertyDefinition {
|
|
|
221
221
|
min?: number;
|
|
222
222
|
max?: number;
|
|
223
223
|
pattern?: string;
|
|
224
|
-
enum?: readonly
|
|
224
|
+
enum?: readonly unknown[];
|
|
225
225
|
};
|
|
226
226
|
}
|
|
227
227
|
|
|
@@ -229,7 +229,7 @@ export interface ContentPropertyDefinition {
|
|
|
229
229
|
* Enhanced block definition interface with generic type parameter
|
|
230
230
|
* Links the TypeScript interface to the block definition for better type safety
|
|
231
231
|
*/
|
|
232
|
-
export interface BlockDefinition<T =
|
|
232
|
+
export interface BlockDefinition<T = unknown> {
|
|
233
233
|
/** The unique identifier for the block type */
|
|
234
234
|
type: BlockType;
|
|
235
235
|
/** User-friendly display name for the block */
|
|
@@ -799,7 +799,7 @@ export function getBlockDefinition(blockType: BlockType): BlockDefinition | unde
|
|
|
799
799
|
* @param blockType - The type of block to get initial content for
|
|
800
800
|
* @returns The initial content object or undefined if block type not found
|
|
801
801
|
*/
|
|
802
|
-
export function getInitialContent(blockType: BlockType):
|
|
802
|
+
export function getInitialContent(blockType: BlockType): unknown {
|
|
803
803
|
return blockRegistry[blockType]?.initialContent;
|
|
804
804
|
}
|
|
805
805
|
|
|
@@ -870,7 +870,7 @@ export type AllBlockContent =
|
|
|
870
870
|
*/
|
|
871
871
|
export function validateBlockContent(
|
|
872
872
|
blockType: BlockType,
|
|
873
|
-
content: Record<string,
|
|
873
|
+
content: Record<string, unknown>
|
|
874
874
|
): {
|
|
875
875
|
isValid: boolean;
|
|
876
876
|
errors: string[];
|
|
@@ -972,9 +972,9 @@ export function getRequiredProperties(blockType: BlockType): string[] {
|
|
|
972
972
|
const schema = getContentSchema(blockType);
|
|
973
973
|
if (!schema) return [];
|
|
974
974
|
|
|
975
|
-
return Object.entries(schema)
|
|
976
|
-
.filter(([, def]) => def.required)
|
|
977
|
-
.map(([name]) => name);
|
|
975
|
+
return Object.entries(schema)
|
|
976
|
+
.filter(([, def]) => def.required)
|
|
977
|
+
.map(([name]) => name);
|
|
978
978
|
}
|
|
979
979
|
|
|
980
980
|
/**
|
|
@@ -984,13 +984,13 @@ export function getRequiredProperties(blockType: BlockType): string[] {
|
|
|
984
984
|
* @param blockType - The type of block
|
|
985
985
|
* @returns Complete default content object
|
|
986
986
|
*/
|
|
987
|
-
export function generateDefaultContent(blockType: BlockType): Record<string,
|
|
987
|
+
export function generateDefaultContent(blockType: BlockType): Record<string, unknown> {
|
|
988
988
|
const schema = getContentSchema(blockType);
|
|
989
|
-
const initialContent = getInitialContent(blockType) || {};
|
|
989
|
+
const initialContent = getInitialContent(blockType) || ({} as Record<string, unknown>);
|
|
990
990
|
|
|
991
|
-
if (!schema) return initialContent
|
|
991
|
+
if (!schema) return initialContent as Record<string, unknown>;
|
|
992
992
|
|
|
993
|
-
const defaultContent: Record<string,
|
|
993
|
+
const defaultContent: Record<string, unknown> = { ...(initialContent as Record<string, unknown>) };
|
|
994
994
|
|
|
995
995
|
for (const [propertyName, propertyDef] of Object.entries(schema)) {
|
|
996
996
|
if (defaultContent[propertyName] === undefined && propertyDef.default !== undefined) {
|
|
@@ -999,4 +999,4 @@ export function generateDefaultContent(blockType: BlockType): Record<string, any
|
|
|
999
999
|
}
|
|
1000
1000
|
|
|
1001
1001
|
return defaultContent;
|
|
1002
|
-
}
|
|
1002
|
+
}
|