create-nextblock 0.2.33 → 0.2.34
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/templates/nextblock-template/app/[slug]/PageClientContent.tsx +67 -67
- package/templates/nextblock-template/app/[slug]/page.tsx +4 -4
- package/templates/nextblock-template/app/cms/blocks/actions.ts +5 -5
- package/templates/nextblock-template/app/cms/blocks/components/BackgroundSelector.tsx +348 -350
- package/templates/nextblock-template/app/cms/blocks/components/BlockEditorArea.tsx +13 -16
- package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +1 -1
- package/templates/nextblock-template/app/cms/blocks/components/ColumnEditor.tsx +24 -42
- package/templates/nextblock-template/app/cms/blocks/components/EditableBlock.tsx +6 -6
- package/templates/nextblock-template/app/cms/blocks/editors/SectionBlockEditor.tsx +35 -56
- package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +81 -81
- package/templates/nextblock-template/app/cms/media/actions.ts +12 -12
- 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 +120 -120
- package/templates/nextblock-template/app/cms/revisions/JsonDiffView.tsx +86 -87
- package/templates/nextblock-template/app/cms/revisions/RevisionHistoryButton.tsx +10 -16
- package/templates/nextblock-template/app/cms/revisions/service.ts +344 -344
- 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 +0 -1
- package/templates/nextblock-template/app/providers.tsx +2 -2
- package/templates/nextblock-template/components/BlockRenderer.tsx +9 -9
- package/templates/nextblock-template/components/ResponsiveNav.tsx +22 -22
- package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +12 -12
- package/templates/nextblock-template/components/blocks/renderers/ClientTextBlockRenderer.tsx +26 -26
- package/templates/nextblock-template/components/blocks/renderers/HeroBlockRenderer.tsx +41 -41
- package/templates/nextblock-template/components/blocks/renderers/ImageBlockRenderer.tsx +7 -7
- package/templates/nextblock-template/components/theme-switcher.tsx +78 -78
- package/templates/nextblock-template/eslint.config.mjs +35 -37
- package/templates/nextblock-template/lib/blocks/blockRegistry.ts +10 -10
- package/templates/nextblock-template/next-env.d.ts +6 -6
- package/templates/nextblock-template/package.json +1 -1
|
@@ -1,344 +1,344 @@
|
|
|
1
|
-
// apps/nextblock/app/cms/revisions/service.ts
|
|
2
|
-
"use server";
|
|
3
|
-
|
|
4
|
-
import { createClient } from "@nextblock-cms/db/server";
|
|
5
|
-
import type { Json } from "@nextblock-cms/db";
|
|
6
|
-
import { compare, applyPatch
|
|
7
|
-
import type { FullPageContent, FullPostContent } from './utils';
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
function shouldCreateSnapshot(currentVersion: number): boolean {
|
|
11
|
-
// Create a snapshot every 20 revisions
|
|
12
|
-
return currentVersion % 20 === 0;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export async function createPageRevision(
|
|
16
|
-
pageId: number,
|
|
17
|
-
authorId: string,
|
|
18
|
-
previousContent: FullPageContent,
|
|
19
|
-
newContent: FullPageContent
|
|
20
|
-
) {
|
|
21
|
-
const supabase = createClient();
|
|
22
|
-
|
|
23
|
-
// Get current version
|
|
24
|
-
const { data: page, error: pageError } = await supabase
|
|
25
|
-
.from('pages')
|
|
26
|
-
.select('version')
|
|
27
|
-
.eq('id', pageId)
|
|
28
|
-
.single();
|
|
29
|
-
if (pageError || !page) return { error: 'Page not found' } as const;
|
|
30
|
-
|
|
31
|
-
const nextVersion = (page.version ?? 1) + 1;
|
|
32
|
-
const makeSnapshot = shouldCreateSnapshot(page.version ?? 1) || nextVersion === 2; // ensure early snapshot cadence
|
|
33
|
-
|
|
34
|
-
const revisionType: 'snapshot' | 'diff' = makeSnapshot ? 'snapshot' : 'diff';
|
|
35
|
-
const content: Json = makeSnapshot ? (newContent as unknown as Json) : (compare(previousContent, newContent) as unknown as Json);
|
|
36
|
-
|
|
37
|
-
const { error: insertError } = await supabase.from('page_revisions').insert({
|
|
38
|
-
page_id: pageId,
|
|
39
|
-
author_id: authorId,
|
|
40
|
-
version: nextVersion,
|
|
41
|
-
revision_type: revisionType,
|
|
42
|
-
content,
|
|
43
|
-
});
|
|
44
|
-
if (insertError) return { error: `Failed to insert page revision: ${insertError.message}` } as const;
|
|
45
|
-
|
|
46
|
-
const { error: updateVersionError } = await supabase
|
|
47
|
-
.from('pages')
|
|
48
|
-
.update({ version: nextVersion })
|
|
49
|
-
.eq('id', pageId);
|
|
50
|
-
if (updateVersionError) return { error: `Failed to bump page version: ${updateVersionError.message}` } as const;
|
|
51
|
-
|
|
52
|
-
return { success: true as const, version: nextVersion };
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export async function createPostRevision(
|
|
56
|
-
postId: number,
|
|
57
|
-
authorId: string,
|
|
58
|
-
previousContent: FullPostContent,
|
|
59
|
-
newContent: FullPostContent
|
|
60
|
-
) {
|
|
61
|
-
const supabase = createClient();
|
|
62
|
-
|
|
63
|
-
const { data: post, error: postError } = await supabase
|
|
64
|
-
.from('posts')
|
|
65
|
-
.select('version')
|
|
66
|
-
.eq('id', postId)
|
|
67
|
-
.single();
|
|
68
|
-
if (postError || !post) return { error: 'Post not found' } as const;
|
|
69
|
-
|
|
70
|
-
const nextVersion = (post.version ?? 1) + 1;
|
|
71
|
-
const makeSnapshot = shouldCreateSnapshot(post.version ?? 1) || nextVersion === 2;
|
|
72
|
-
|
|
73
|
-
const revisionType: 'snapshot' | 'diff' = makeSnapshot ? 'snapshot' : 'diff';
|
|
74
|
-
const content: Json = makeSnapshot ? (newContent as unknown as Json) : (compare(previousContent, newContent) as unknown as Json);
|
|
75
|
-
|
|
76
|
-
const { error: insertError } = await supabase.from('post_revisions').insert({
|
|
77
|
-
post_id: postId,
|
|
78
|
-
author_id: authorId,
|
|
79
|
-
version: nextVersion,
|
|
80
|
-
revision_type: revisionType,
|
|
81
|
-
content,
|
|
82
|
-
});
|
|
83
|
-
if (insertError) return { error: `Failed to insert post revision: ${insertError.message}` } as const;
|
|
84
|
-
|
|
85
|
-
const { error: updateVersionError } = await supabase
|
|
86
|
-
.from('posts')
|
|
87
|
-
.update({ version: nextVersion })
|
|
88
|
-
.eq('id', postId);
|
|
89
|
-
if (updateVersionError) return { error: `Failed to bump post version: ${updateVersionError.message}` } as const;
|
|
90
|
-
|
|
91
|
-
return { success: true as const, version: nextVersion };
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export async function restorePageToVersion(pageId: number, targetVersion: number, authorId: string) {
|
|
95
|
-
const supabase = createClient();
|
|
96
|
-
|
|
97
|
-
// 1. Find latest snapshot at or before target
|
|
98
|
-
const { data: snapshot, error: snapshotError } = await supabase
|
|
99
|
-
.from('page_revisions')
|
|
100
|
-
.select('version, content, revision_type')
|
|
101
|
-
.eq('page_id', pageId)
|
|
102
|
-
.lte('version', targetVersion)
|
|
103
|
-
.eq('revision_type', 'snapshot')
|
|
104
|
-
.order('version', { ascending: false })
|
|
105
|
-
.limit(1)
|
|
106
|
-
.maybeSingle();
|
|
107
|
-
if (snapshotError || !snapshot) return { error: 'No snapshot found at or before target version.' } as const;
|
|
108
|
-
|
|
109
|
-
let content = snapshot.content as unknown as FullPageContent;
|
|
110
|
-
|
|
111
|
-
// 2. Fetch diffs up to target and apply
|
|
112
|
-
const { data: diffs, error: diffsError } = await supabase
|
|
113
|
-
.from('page_revisions')
|
|
114
|
-
.select('version, content, revision_type')
|
|
115
|
-
.eq('page_id', pageId)
|
|
116
|
-
.gt('version', snapshot.version)
|
|
117
|
-
.lte('version', targetVersion)
|
|
118
|
-
.order('version', { ascending: true });
|
|
119
|
-
if (diffsError) return { error: `Failed to fetch diffs: ${diffsError.message}` } as const;
|
|
120
|
-
|
|
121
|
-
for (const r of diffs || []) {
|
|
122
|
-
if (r.revision_type === 'diff') {
|
|
123
|
-
const ops = r.content as
|
|
124
|
-
const result = applyPatch(content as
|
|
125
|
-
content = result.newDocument as unknown as FullPageContent;
|
|
126
|
-
} else {
|
|
127
|
-
content = r.content as unknown as FullPageContent;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// Determine next version number (append a new revision for restored state)
|
|
132
|
-
const { data: pageRow } = await supabase
|
|
133
|
-
.from('pages')
|
|
134
|
-
.select('version')
|
|
135
|
-
.eq('id', pageId)
|
|
136
|
-
.single();
|
|
137
|
-
const newVersion = ((pageRow?.version as number | null) ?? 1) + 1;
|
|
138
|
-
|
|
139
|
-
// 3. Apply to DB: update page meta and replace blocks; bump to newVersion
|
|
140
|
-
const { error: updatePageError } = await supabase
|
|
141
|
-
.from('pages')
|
|
142
|
-
.update({
|
|
143
|
-
title: content.meta.title,
|
|
144
|
-
slug: content.meta.slug,
|
|
145
|
-
language_id: content.meta.language_id,
|
|
146
|
-
status: content.meta.status,
|
|
147
|
-
meta_title: content.meta.meta_title,
|
|
148
|
-
meta_description: content.meta.meta_description,
|
|
149
|
-
version: newVersion,
|
|
150
|
-
})
|
|
151
|
-
.eq('id', pageId);
|
|
152
|
-
if (updatePageError) return { error: `Failed to update page: ${updatePageError.message}` } as const;
|
|
153
|
-
|
|
154
|
-
// delete all existing blocks for this page then reinsert
|
|
155
|
-
const { error: deleteError } = await supabase.from('blocks').delete().eq('page_id', pageId);
|
|
156
|
-
if (deleteError) return { error: `Failed to clear blocks: ${deleteError.message}` } as const;
|
|
157
|
-
|
|
158
|
-
if (content.blocks.length > 0) {
|
|
159
|
-
const toInsert = content.blocks.map(b => ({
|
|
160
|
-
page_id: pageId,
|
|
161
|
-
post_id: null,
|
|
162
|
-
language_id: b.language_id,
|
|
163
|
-
block_type: b.block_type,
|
|
164
|
-
content: b.content,
|
|
165
|
-
order: b.order,
|
|
166
|
-
}));
|
|
167
|
-
const { error: insertError } = await supabase.from('blocks').insert(toInsert);
|
|
168
|
-
if (insertError) return { error: `Failed to insert blocks: ${insertError.message}` } as const;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// 4. Record a new snapshot revision representing the restored state at newVersion
|
|
172
|
-
const { error: revErr } = await supabase.from('page_revisions').insert({
|
|
173
|
-
page_id: pageId,
|
|
174
|
-
author_id: authorId,
|
|
175
|
-
version: newVersion,
|
|
176
|
-
revision_type: 'snapshot',
|
|
177
|
-
content: content as unknown as Json,
|
|
178
|
-
});
|
|
179
|
-
if (revErr) return { error: `Failed to write restored revision: ${revErr.message}` } as const;
|
|
180
|
-
|
|
181
|
-
return { success: true as const };
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
export async function restorePostToVersion(postId: number, targetVersion: number, authorId: string) {
|
|
185
|
-
const supabase = createClient();
|
|
186
|
-
|
|
187
|
-
const { data: snapshot, error: snapshotError } = await supabase
|
|
188
|
-
.from('post_revisions')
|
|
189
|
-
.select('version, content, revision_type')
|
|
190
|
-
.eq('post_id', postId)
|
|
191
|
-
.lte('version', targetVersion)
|
|
192
|
-
.eq('revision_type', 'snapshot')
|
|
193
|
-
.order('version', { ascending: false })
|
|
194
|
-
.limit(1)
|
|
195
|
-
.maybeSingle();
|
|
196
|
-
if (snapshotError || !snapshot) return { error: 'No snapshot found at or before target version.' } as const;
|
|
197
|
-
|
|
198
|
-
let content = snapshot.content as unknown as FullPostContent;
|
|
199
|
-
|
|
200
|
-
const { data: diffs, error: diffsError } = await supabase
|
|
201
|
-
.from('post_revisions')
|
|
202
|
-
.select('version, content, revision_type')
|
|
203
|
-
.eq('post_id', postId)
|
|
204
|
-
.gt('version', snapshot.version)
|
|
205
|
-
.lte('version', targetVersion)
|
|
206
|
-
.order('version', { ascending: true });
|
|
207
|
-
if (diffsError) return { error: `Failed to fetch diffs: ${diffsError.message}` } as const;
|
|
208
|
-
|
|
209
|
-
for (const r of diffs || []) {
|
|
210
|
-
if (r.revision_type === 'diff') {
|
|
211
|
-
const ops = r.content as
|
|
212
|
-
const result = applyPatch(content as
|
|
213
|
-
content = result.newDocument as unknown as FullPostContent;
|
|
214
|
-
} else {
|
|
215
|
-
content = r.content as unknown as FullPostContent;
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// Determine next version for post
|
|
220
|
-
const { data: postRow } = await supabase
|
|
221
|
-
.from('posts')
|
|
222
|
-
.select('version')
|
|
223
|
-
.eq('id', postId)
|
|
224
|
-
.single();
|
|
225
|
-
const newVersion = ((postRow?.version as number | null) ?? 1) + 1;
|
|
226
|
-
|
|
227
|
-
const { error: updatePostError } = await supabase
|
|
228
|
-
.from('posts')
|
|
229
|
-
.update({
|
|
230
|
-
title: content.meta.title,
|
|
231
|
-
slug: content.meta.slug,
|
|
232
|
-
language_id: content.meta.language_id,
|
|
233
|
-
status: content.meta.status,
|
|
234
|
-
meta_title: content.meta.meta_title,
|
|
235
|
-
meta_description: content.meta.meta_description,
|
|
236
|
-
excerpt: content.meta.excerpt,
|
|
237
|
-
published_at: content.meta.published_at,
|
|
238
|
-
feature_image_id: content.meta.feature_image_id,
|
|
239
|
-
version: newVersion,
|
|
240
|
-
})
|
|
241
|
-
.eq('id', postId);
|
|
242
|
-
if (updatePostError) return { error: `Failed to update post: ${updatePostError.message}` } as const;
|
|
243
|
-
|
|
244
|
-
const { error: deleteError } = await supabase.from('blocks').delete().eq('post_id', postId);
|
|
245
|
-
if (deleteError) return { error: `Failed to clear blocks: ${deleteError.message}` } as const;
|
|
246
|
-
|
|
247
|
-
if (content.blocks.length > 0) {
|
|
248
|
-
const toInsert = content.blocks.map(b => ({
|
|
249
|
-
page_id: null,
|
|
250
|
-
post_id: postId,
|
|
251
|
-
language_id: b.language_id,
|
|
252
|
-
block_type: b.block_type,
|
|
253
|
-
content: b.content,
|
|
254
|
-
order: b.order,
|
|
255
|
-
}));
|
|
256
|
-
const { error: insertError } = await supabase.from('blocks').insert(toInsert);
|
|
257
|
-
if (insertError) return { error: `Failed to insert blocks: ${insertError.message}` } as const;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
const { error: revErr } = await supabase.from('post_revisions').insert({
|
|
261
|
-
post_id: postId,
|
|
262
|
-
author_id: authorId,
|
|
263
|
-
version: newVersion,
|
|
264
|
-
revision_type: 'snapshot',
|
|
265
|
-
content: content as unknown as Json,
|
|
266
|
-
});
|
|
267
|
-
if (revErr) return { error: `Failed to write restored revision: ${revErr.message}` } as const;
|
|
268
|
-
|
|
269
|
-
return { success: true as const };
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
export async function reconstructPageVersionContent(pageId: number, targetVersion: number) {
|
|
273
|
-
const supabase = createClient();
|
|
274
|
-
|
|
275
|
-
const { data: snapshot, error: snapshotError } = await supabase
|
|
276
|
-
.from('page_revisions')
|
|
277
|
-
.select('version, content, revision_type')
|
|
278
|
-
.eq('page_id', pageId)
|
|
279
|
-
.lte('version', targetVersion)
|
|
280
|
-
.eq('revision_type', 'snapshot')
|
|
281
|
-
.order('version', { ascending: false })
|
|
282
|
-
.limit(1)
|
|
283
|
-
.maybeSingle();
|
|
284
|
-
if (snapshotError || !snapshot) return { error: 'No snapshot found at or before target version.' } as const;
|
|
285
|
-
|
|
286
|
-
let content = snapshot.content as unknown as FullPageContent;
|
|
287
|
-
|
|
288
|
-
const { data: diffs, error: diffsError } = await supabase
|
|
289
|
-
.from('page_revisions')
|
|
290
|
-
.select('version, content, revision_type')
|
|
291
|
-
.eq('page_id', pageId)
|
|
292
|
-
.gt('version', snapshot.version)
|
|
293
|
-
.lte('version', targetVersion)
|
|
294
|
-
.order('version', { ascending: true });
|
|
295
|
-
if (diffsError) return { error: `Failed to fetch diffs: ${diffsError.message}` } as const;
|
|
296
|
-
|
|
297
|
-
for (const r of diffs || []) {
|
|
298
|
-
if (r.revision_type === 'diff') {
|
|
299
|
-
const ops = r.content as
|
|
300
|
-
const result = applyPatch(content as
|
|
301
|
-
content = result.newDocument as unknown as FullPageContent;
|
|
302
|
-
} else {
|
|
303
|
-
content = r.content as unknown as FullPageContent;
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
return { success: true as const, content };
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
export async function reconstructPostVersionContent(postId: number, targetVersion: number) {
|
|
310
|
-
const supabase = createClient();
|
|
311
|
-
|
|
312
|
-
const { data: snapshot, error: snapshotError } = await supabase
|
|
313
|
-
.from('post_revisions')
|
|
314
|
-
.select('version, content, revision_type')
|
|
315
|
-
.eq('post_id', postId)
|
|
316
|
-
.lte('version', targetVersion)
|
|
317
|
-
.eq('revision_type', 'snapshot')
|
|
318
|
-
.order('version', { ascending: false })
|
|
319
|
-
.limit(1)
|
|
320
|
-
.maybeSingle();
|
|
321
|
-
if (snapshotError || !snapshot) return { error: 'No snapshot found at or before target version.' } as const;
|
|
322
|
-
|
|
323
|
-
let content = snapshot.content as unknown as FullPostContent;
|
|
324
|
-
|
|
325
|
-
const { data: diffs, error: diffsError } = await supabase
|
|
326
|
-
.from('post_revisions')
|
|
327
|
-
.select('version, content, revision_type')
|
|
328
|
-
.eq('post_id', postId)
|
|
329
|
-
.gt('version', snapshot.version)
|
|
330
|
-
.lte('version', targetVersion)
|
|
331
|
-
.order('version', { ascending: true });
|
|
332
|
-
if (diffsError) return { error: `Failed to fetch diffs: ${diffsError.message}` } as const;
|
|
333
|
-
|
|
334
|
-
for (const r of diffs || []) {
|
|
335
|
-
if (r.revision_type === 'diff') {
|
|
336
|
-
const ops = r.content as
|
|
337
|
-
const result = applyPatch(content as
|
|
338
|
-
content = result.newDocument as unknown as FullPostContent;
|
|
339
|
-
} else {
|
|
340
|
-
content = r.content as unknown as FullPostContent;
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
return { success: true as const, content };
|
|
344
|
-
}
|
|
1
|
+
// apps/nextblock/app/cms/revisions/service.ts
|
|
2
|
+
"use server";
|
|
3
|
+
|
|
4
|
+
import { createClient } from "@nextblock-cms/db/server";
|
|
5
|
+
import type { Json } from "@nextblock-cms/db";
|
|
6
|
+
import { compare, applyPatch } from 'fast-json-patch';
|
|
7
|
+
import type { FullPageContent, FullPostContent } from './utils';
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
function shouldCreateSnapshot(currentVersion: number): boolean {
|
|
11
|
+
// Create a snapshot every 20 revisions
|
|
12
|
+
return currentVersion % 20 === 0;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function createPageRevision(
|
|
16
|
+
pageId: number,
|
|
17
|
+
authorId: string,
|
|
18
|
+
previousContent: FullPageContent,
|
|
19
|
+
newContent: FullPageContent
|
|
20
|
+
) {
|
|
21
|
+
const supabase = createClient();
|
|
22
|
+
|
|
23
|
+
// Get current version
|
|
24
|
+
const { data: page, error: pageError } = await supabase
|
|
25
|
+
.from('pages')
|
|
26
|
+
.select('version')
|
|
27
|
+
.eq('id', pageId)
|
|
28
|
+
.single();
|
|
29
|
+
if (pageError || !page) return { error: 'Page not found' } as const;
|
|
30
|
+
|
|
31
|
+
const nextVersion = (page.version ?? 1) + 1;
|
|
32
|
+
const makeSnapshot = shouldCreateSnapshot(page.version ?? 1) || nextVersion === 2; // ensure early snapshot cadence
|
|
33
|
+
|
|
34
|
+
const revisionType: 'snapshot' | 'diff' = makeSnapshot ? 'snapshot' : 'diff';
|
|
35
|
+
const content: Json = makeSnapshot ? (newContent as unknown as Json) : (compare(previousContent, newContent) as unknown as Json);
|
|
36
|
+
|
|
37
|
+
const { error: insertError } = await supabase.from('page_revisions').insert({
|
|
38
|
+
page_id: pageId,
|
|
39
|
+
author_id: authorId,
|
|
40
|
+
version: nextVersion,
|
|
41
|
+
revision_type: revisionType,
|
|
42
|
+
content,
|
|
43
|
+
});
|
|
44
|
+
if (insertError) return { error: `Failed to insert page revision: ${insertError.message}` } as const;
|
|
45
|
+
|
|
46
|
+
const { error: updateVersionError } = await supabase
|
|
47
|
+
.from('pages')
|
|
48
|
+
.update({ version: nextVersion })
|
|
49
|
+
.eq('id', pageId);
|
|
50
|
+
if (updateVersionError) return { error: `Failed to bump page version: ${updateVersionError.message}` } as const;
|
|
51
|
+
|
|
52
|
+
return { success: true as const, version: nextVersion };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function createPostRevision(
|
|
56
|
+
postId: number,
|
|
57
|
+
authorId: string,
|
|
58
|
+
previousContent: FullPostContent,
|
|
59
|
+
newContent: FullPostContent
|
|
60
|
+
) {
|
|
61
|
+
const supabase = createClient();
|
|
62
|
+
|
|
63
|
+
const { data: post, error: postError } = await supabase
|
|
64
|
+
.from('posts')
|
|
65
|
+
.select('version')
|
|
66
|
+
.eq('id', postId)
|
|
67
|
+
.single();
|
|
68
|
+
if (postError || !post) return { error: 'Post not found' } as const;
|
|
69
|
+
|
|
70
|
+
const nextVersion = (post.version ?? 1) + 1;
|
|
71
|
+
const makeSnapshot = shouldCreateSnapshot(post.version ?? 1) || nextVersion === 2;
|
|
72
|
+
|
|
73
|
+
const revisionType: 'snapshot' | 'diff' = makeSnapshot ? 'snapshot' : 'diff';
|
|
74
|
+
const content: Json = makeSnapshot ? (newContent as unknown as Json) : (compare(previousContent, newContent) as unknown as Json);
|
|
75
|
+
|
|
76
|
+
const { error: insertError } = await supabase.from('post_revisions').insert({
|
|
77
|
+
post_id: postId,
|
|
78
|
+
author_id: authorId,
|
|
79
|
+
version: nextVersion,
|
|
80
|
+
revision_type: revisionType,
|
|
81
|
+
content,
|
|
82
|
+
});
|
|
83
|
+
if (insertError) return { error: `Failed to insert post revision: ${insertError.message}` } as const;
|
|
84
|
+
|
|
85
|
+
const { error: updateVersionError } = await supabase
|
|
86
|
+
.from('posts')
|
|
87
|
+
.update({ version: nextVersion })
|
|
88
|
+
.eq('id', postId);
|
|
89
|
+
if (updateVersionError) return { error: `Failed to bump post version: ${updateVersionError.message}` } as const;
|
|
90
|
+
|
|
91
|
+
return { success: true as const, version: nextVersion };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function restorePageToVersion(pageId: number, targetVersion: number, authorId: string) {
|
|
95
|
+
const supabase = createClient();
|
|
96
|
+
|
|
97
|
+
// 1. Find latest snapshot at or before target
|
|
98
|
+
const { data: snapshot, error: snapshotError } = await supabase
|
|
99
|
+
.from('page_revisions')
|
|
100
|
+
.select('version, content, revision_type')
|
|
101
|
+
.eq('page_id', pageId)
|
|
102
|
+
.lte('version', targetVersion)
|
|
103
|
+
.eq('revision_type', 'snapshot')
|
|
104
|
+
.order('version', { ascending: false })
|
|
105
|
+
.limit(1)
|
|
106
|
+
.maybeSingle();
|
|
107
|
+
if (snapshotError || !snapshot) return { error: 'No snapshot found at or before target version.' } as const;
|
|
108
|
+
|
|
109
|
+
let content = snapshot.content as unknown as FullPageContent;
|
|
110
|
+
|
|
111
|
+
// 2. Fetch diffs up to target and apply
|
|
112
|
+
const { data: diffs, error: diffsError } = await supabase
|
|
113
|
+
.from('page_revisions')
|
|
114
|
+
.select('version, content, revision_type')
|
|
115
|
+
.eq('page_id', pageId)
|
|
116
|
+
.gt('version', snapshot.version)
|
|
117
|
+
.lte('version', targetVersion)
|
|
118
|
+
.order('version', { ascending: true });
|
|
119
|
+
if (diffsError) return { error: `Failed to fetch diffs: ${diffsError.message}` } as const;
|
|
120
|
+
|
|
121
|
+
for (const r of diffs || []) {
|
|
122
|
+
if (r.revision_type === 'diff') {
|
|
123
|
+
const ops = r.content as any[];
|
|
124
|
+
const result = applyPatch(content as any, ops, /*validate*/ false, /*mutateDocument*/ true);
|
|
125
|
+
content = result.newDocument as unknown as FullPageContent;
|
|
126
|
+
} else {
|
|
127
|
+
content = r.content as unknown as FullPageContent;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Determine next version number (append a new revision for restored state)
|
|
132
|
+
const { data: pageRow } = await supabase
|
|
133
|
+
.from('pages')
|
|
134
|
+
.select('version')
|
|
135
|
+
.eq('id', pageId)
|
|
136
|
+
.single();
|
|
137
|
+
const newVersion = ((pageRow?.version as number | null) ?? 1) + 1;
|
|
138
|
+
|
|
139
|
+
// 3. Apply to DB: update page meta and replace blocks; bump to newVersion
|
|
140
|
+
const { error: updatePageError } = await supabase
|
|
141
|
+
.from('pages')
|
|
142
|
+
.update({
|
|
143
|
+
title: content.meta.title,
|
|
144
|
+
slug: content.meta.slug,
|
|
145
|
+
language_id: content.meta.language_id,
|
|
146
|
+
status: content.meta.status,
|
|
147
|
+
meta_title: content.meta.meta_title,
|
|
148
|
+
meta_description: content.meta.meta_description,
|
|
149
|
+
version: newVersion,
|
|
150
|
+
})
|
|
151
|
+
.eq('id', pageId);
|
|
152
|
+
if (updatePageError) return { error: `Failed to update page: ${updatePageError.message}` } as const;
|
|
153
|
+
|
|
154
|
+
// delete all existing blocks for this page then reinsert
|
|
155
|
+
const { error: deleteError } = await supabase.from('blocks').delete().eq('page_id', pageId);
|
|
156
|
+
if (deleteError) return { error: `Failed to clear blocks: ${deleteError.message}` } as const;
|
|
157
|
+
|
|
158
|
+
if (content.blocks.length > 0) {
|
|
159
|
+
const toInsert = content.blocks.map(b => ({
|
|
160
|
+
page_id: pageId,
|
|
161
|
+
post_id: null,
|
|
162
|
+
language_id: b.language_id,
|
|
163
|
+
block_type: b.block_type,
|
|
164
|
+
content: b.content,
|
|
165
|
+
order: b.order,
|
|
166
|
+
}));
|
|
167
|
+
const { error: insertError } = await supabase.from('blocks').insert(toInsert);
|
|
168
|
+
if (insertError) return { error: `Failed to insert blocks: ${insertError.message}` } as const;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// 4. Record a new snapshot revision representing the restored state at newVersion
|
|
172
|
+
const { error: revErr } = await supabase.from('page_revisions').insert({
|
|
173
|
+
page_id: pageId,
|
|
174
|
+
author_id: authorId,
|
|
175
|
+
version: newVersion,
|
|
176
|
+
revision_type: 'snapshot',
|
|
177
|
+
content: content as unknown as Json,
|
|
178
|
+
});
|
|
179
|
+
if (revErr) return { error: `Failed to write restored revision: ${revErr.message}` } as const;
|
|
180
|
+
|
|
181
|
+
return { success: true as const };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export async function restorePostToVersion(postId: number, targetVersion: number, authorId: string) {
|
|
185
|
+
const supabase = createClient();
|
|
186
|
+
|
|
187
|
+
const { data: snapshot, error: snapshotError } = await supabase
|
|
188
|
+
.from('post_revisions')
|
|
189
|
+
.select('version, content, revision_type')
|
|
190
|
+
.eq('post_id', postId)
|
|
191
|
+
.lte('version', targetVersion)
|
|
192
|
+
.eq('revision_type', 'snapshot')
|
|
193
|
+
.order('version', { ascending: false })
|
|
194
|
+
.limit(1)
|
|
195
|
+
.maybeSingle();
|
|
196
|
+
if (snapshotError || !snapshot) return { error: 'No snapshot found at or before target version.' } as const;
|
|
197
|
+
|
|
198
|
+
let content = snapshot.content as unknown as FullPostContent;
|
|
199
|
+
|
|
200
|
+
const { data: diffs, error: diffsError } = await supabase
|
|
201
|
+
.from('post_revisions')
|
|
202
|
+
.select('version, content, revision_type')
|
|
203
|
+
.eq('post_id', postId)
|
|
204
|
+
.gt('version', snapshot.version)
|
|
205
|
+
.lte('version', targetVersion)
|
|
206
|
+
.order('version', { ascending: true });
|
|
207
|
+
if (diffsError) return { error: `Failed to fetch diffs: ${diffsError.message}` } as const;
|
|
208
|
+
|
|
209
|
+
for (const r of diffs || []) {
|
|
210
|
+
if (r.revision_type === 'diff') {
|
|
211
|
+
const ops = r.content as any[];
|
|
212
|
+
const result = applyPatch(content as any, ops, /*validate*/ false, /*mutateDocument*/ true);
|
|
213
|
+
content = result.newDocument as unknown as FullPostContent;
|
|
214
|
+
} else {
|
|
215
|
+
content = r.content as unknown as FullPostContent;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Determine next version for post
|
|
220
|
+
const { data: postRow } = await supabase
|
|
221
|
+
.from('posts')
|
|
222
|
+
.select('version')
|
|
223
|
+
.eq('id', postId)
|
|
224
|
+
.single();
|
|
225
|
+
const newVersion = ((postRow?.version as number | null) ?? 1) + 1;
|
|
226
|
+
|
|
227
|
+
const { error: updatePostError } = await supabase
|
|
228
|
+
.from('posts')
|
|
229
|
+
.update({
|
|
230
|
+
title: content.meta.title,
|
|
231
|
+
slug: content.meta.slug,
|
|
232
|
+
language_id: content.meta.language_id,
|
|
233
|
+
status: content.meta.status,
|
|
234
|
+
meta_title: content.meta.meta_title,
|
|
235
|
+
meta_description: content.meta.meta_description,
|
|
236
|
+
excerpt: content.meta.excerpt,
|
|
237
|
+
published_at: content.meta.published_at,
|
|
238
|
+
feature_image_id: content.meta.feature_image_id,
|
|
239
|
+
version: newVersion,
|
|
240
|
+
})
|
|
241
|
+
.eq('id', postId);
|
|
242
|
+
if (updatePostError) return { error: `Failed to update post: ${updatePostError.message}` } as const;
|
|
243
|
+
|
|
244
|
+
const { error: deleteError } = await supabase.from('blocks').delete().eq('post_id', postId);
|
|
245
|
+
if (deleteError) return { error: `Failed to clear blocks: ${deleteError.message}` } as const;
|
|
246
|
+
|
|
247
|
+
if (content.blocks.length > 0) {
|
|
248
|
+
const toInsert = content.blocks.map(b => ({
|
|
249
|
+
page_id: null,
|
|
250
|
+
post_id: postId,
|
|
251
|
+
language_id: b.language_id,
|
|
252
|
+
block_type: b.block_type,
|
|
253
|
+
content: b.content,
|
|
254
|
+
order: b.order,
|
|
255
|
+
}));
|
|
256
|
+
const { error: insertError } = await supabase.from('blocks').insert(toInsert);
|
|
257
|
+
if (insertError) return { error: `Failed to insert blocks: ${insertError.message}` } as const;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const { error: revErr } = await supabase.from('post_revisions').insert({
|
|
261
|
+
post_id: postId,
|
|
262
|
+
author_id: authorId,
|
|
263
|
+
version: newVersion,
|
|
264
|
+
revision_type: 'snapshot',
|
|
265
|
+
content: content as unknown as Json,
|
|
266
|
+
});
|
|
267
|
+
if (revErr) return { error: `Failed to write restored revision: ${revErr.message}` } as const;
|
|
268
|
+
|
|
269
|
+
return { success: true as const };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export async function reconstructPageVersionContent(pageId: number, targetVersion: number) {
|
|
273
|
+
const supabase = createClient();
|
|
274
|
+
|
|
275
|
+
const { data: snapshot, error: snapshotError } = await supabase
|
|
276
|
+
.from('page_revisions')
|
|
277
|
+
.select('version, content, revision_type')
|
|
278
|
+
.eq('page_id', pageId)
|
|
279
|
+
.lte('version', targetVersion)
|
|
280
|
+
.eq('revision_type', 'snapshot')
|
|
281
|
+
.order('version', { ascending: false })
|
|
282
|
+
.limit(1)
|
|
283
|
+
.maybeSingle();
|
|
284
|
+
if (snapshotError || !snapshot) return { error: 'No snapshot found at or before target version.' } as const;
|
|
285
|
+
|
|
286
|
+
let content = snapshot.content as unknown as FullPageContent;
|
|
287
|
+
|
|
288
|
+
const { data: diffs, error: diffsError } = await supabase
|
|
289
|
+
.from('page_revisions')
|
|
290
|
+
.select('version, content, revision_type')
|
|
291
|
+
.eq('page_id', pageId)
|
|
292
|
+
.gt('version', snapshot.version)
|
|
293
|
+
.lte('version', targetVersion)
|
|
294
|
+
.order('version', { ascending: true });
|
|
295
|
+
if (diffsError) return { error: `Failed to fetch diffs: ${diffsError.message}` } as const;
|
|
296
|
+
|
|
297
|
+
for (const r of diffs || []) {
|
|
298
|
+
if (r.revision_type === 'diff') {
|
|
299
|
+
const ops = r.content as any[];
|
|
300
|
+
const result = applyPatch(content as any, ops, false, true);
|
|
301
|
+
content = result.newDocument as unknown as FullPageContent;
|
|
302
|
+
} else {
|
|
303
|
+
content = r.content as unknown as FullPageContent;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return { success: true as const, content };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export async function reconstructPostVersionContent(postId: number, targetVersion: number) {
|
|
310
|
+
const supabase = createClient();
|
|
311
|
+
|
|
312
|
+
const { data: snapshot, error: snapshotError } = await supabase
|
|
313
|
+
.from('post_revisions')
|
|
314
|
+
.select('version, content, revision_type')
|
|
315
|
+
.eq('post_id', postId)
|
|
316
|
+
.lte('version', targetVersion)
|
|
317
|
+
.eq('revision_type', 'snapshot')
|
|
318
|
+
.order('version', { ascending: false })
|
|
319
|
+
.limit(1)
|
|
320
|
+
.maybeSingle();
|
|
321
|
+
if (snapshotError || !snapshot) return { error: 'No snapshot found at or before target version.' } as const;
|
|
322
|
+
|
|
323
|
+
let content = snapshot.content as unknown as FullPostContent;
|
|
324
|
+
|
|
325
|
+
const { data: diffs, error: diffsError } = await supabase
|
|
326
|
+
.from('post_revisions')
|
|
327
|
+
.select('version, content, revision_type')
|
|
328
|
+
.eq('post_id', postId)
|
|
329
|
+
.gt('version', snapshot.version)
|
|
330
|
+
.lte('version', targetVersion)
|
|
331
|
+
.order('version', { ascending: true });
|
|
332
|
+
if (diffsError) return { error: `Failed to fetch diffs: ${diffsError.message}` } as const;
|
|
333
|
+
|
|
334
|
+
for (const r of diffs || []) {
|
|
335
|
+
if (r.revision_type === 'diff') {
|
|
336
|
+
const ops = r.content as any[];
|
|
337
|
+
const result = applyPatch(content as any, ops, false, true);
|
|
338
|
+
content = result.newDocument as unknown as FullPostContent;
|
|
339
|
+
} else {
|
|
340
|
+
content = r.content as unknown as FullPostContent;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return { success: true as const, content };
|
|
344
|
+
}
|