create-nextblock 0.11.1 → 0.11.2

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.
Files changed (58) hide show
  1. package/package.json +1 -1
  2. package/templates/nextblock-template/app/actions/interactions.test.ts +301 -0
  3. package/templates/nextblock-template/app/actions/interactions.ts +372 -0
  4. package/templates/nextblock-template/app/api/ai/cortex/build-widget/route.ts +4 -4
  5. package/templates/nextblock-template/app/api/ai/generate-blocks/route.ts +2 -2
  6. package/templates/nextblock-template/app/api/ai/global-agent/route.ts +56 -57
  7. package/templates/nextblock-template/app/api/cron/reset-sandbox/route.ts +1 -1
  8. package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +837 -0
  9. package/templates/nextblock-template/app/article/[slug]/PostClientContent.tsx +6 -0
  10. package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +4 -0
  11. package/templates/nextblock-template/app/cms/components/ConnectGitHubButton.tsx +7 -2
  12. package/templates/nextblock-template/app/cms/components/github-connect-actions.ts +4 -0
  13. package/templates/nextblock-template/app/cms/interactions/InteractionsModerationClient.tsx +408 -0
  14. package/templates/nextblock-template/app/cms/interactions/page.tsx +51 -0
  15. package/templates/nextblock-template/app/cms/settings/cortex-ai/SandboxCortexAiSettingsClient.tsx +4 -3
  16. package/templates/nextblock-template/app/cms/settings/cortex-ai/StoredCortexAiSettingsClient.tsx +1 -1
  17. package/templates/nextblock-template/app/cms/settings/cortex-ai/actions.ts +3 -5
  18. package/templates/nextblock-template/app/cms/settings/cortex-ai/page.tsx +1 -1
  19. package/templates/nextblock-template/app/page.tsx +2 -2
  20. package/templates/nextblock-template/app/product/[slug]/page.tsx +2 -0
  21. package/templates/nextblock-template/components/AppShell.tsx +1 -1
  22. package/templates/nextblock-template/components/PostCommentsSection.tsx +369 -0
  23. package/templates/nextblock-template/components/ProductReviewsSection.tsx +419 -0
  24. package/templates/nextblock-template/components/blocks/renderers/ProductDetailsBlockRenderer.tsx +2 -0
  25. package/templates/nextblock-template/components/privacy/ConsentBanner.tsx +62 -19
  26. package/templates/nextblock-template/docs/08-NEXTBLOCK-CORTEX-AI-ARCHITECTURE.md +19 -19
  27. package/templates/nextblock-template/docs/10-CUSTOM-BLOCKS.md +4 -4
  28. package/templates/nextblock-template/lib/blocks/ProductGridBlock.tsx +2 -0
  29. package/templates/nextblock-template/lib/setup/actions.ts +3 -1
  30. package/templates/nextblock-template/lib/setup/migrations-bundle.ts +30 -0
  31. package/templates/nextblock-template/lib/updates/check-upstream.ts +38 -4
  32. package/templates/nextblock-template/package.json +2 -1
  33. package/templates/nextblock-template/scripts/verify-cortex-ai-build-widget.tsx +2 -4
  34. package/templates/nextblock-template/scripts/verify-cortex-ai-generate-blocks.ts +1 -1
  35. package/templates/nextblock-template/scripts/verify-cortex-ai-global-tools.ts +1 -1
  36. package/templates/nextblock-template/scripts/verify-cortex-ai-routing.ts +1 -1
  37. package/templates/nextblock-template/tsconfig.tsbuildinfo +1 -1
  38. package/templates/nextblock-template/lib/ai-block-generation.ts +0 -339
  39. package/templates/nextblock-template/lib/ai-client.ts +0 -247
  40. package/templates/nextblock-template/lib/ai-config.ts +0 -98
  41. package/templates/nextblock-template/lib/ai-cortex-widget-builder.ts +0 -125
  42. package/templates/nextblock-template/lib/ai-global-agent-custom-block-tools.ts +0 -363
  43. package/templates/nextblock-template/lib/ai-global-agent-db-tools.test.ts +0 -405
  44. package/templates/nextblock-template/lib/ai-global-agent-db-tools.ts +0 -1228
  45. package/templates/nextblock-template/lib/ai-global-agent-ecommerce.ts +0 -5
  46. package/templates/nextblock-template/lib/ai-global-agent-tools-stats.test.ts +0 -223
  47. package/templates/nextblock-template/lib/ai-global-agent-tools.test.ts +0 -2183
  48. package/templates/nextblock-template/lib/ai-global-agent-tools.ts +0 -4807
  49. package/templates/nextblock-template/lib/ai-key-crypto.test.ts +0 -70
  50. package/templates/nextblock-template/lib/ai-key-crypto.ts +0 -132
  51. package/templates/nextblock-template/lib/ai-model-catalog.test.ts +0 -49
  52. package/templates/nextblock-template/lib/ai-model-catalog.ts +0 -41
  53. package/templates/nextblock-template/lib/ai-model-registry.test.ts +0 -231
  54. package/templates/nextblock-template/lib/ai-model-registry.ts +0 -522
  55. package/templates/nextblock-template/lib/cortex-widget-registry.test.ts +0 -199
  56. package/templates/nextblock-template/lib/cortex-widget-registry.ts +0 -88
  57. package/templates/nextblock-template/lib/cortex-widget-schema.test.tsx +0 -237
  58. package/templates/nextblock-template/lib/cortex-widget-schema.ts +0 -393
@@ -1,2183 +0,0 @@
1
- import { describe, expect, it, vi } from 'vitest';
2
-
3
- vi.mock('@nextblock-cms/utils', async () => {
4
- const { z } = await import('zod');
5
-
6
- return {
7
- editorBlockDocumentSchema: z.object({
8
- content: z.array(z.any()).optional(),
9
- type: z.literal('doc'),
10
- }),
11
- minorUnitAmountToMajor: (amount: number) => amount / 100,
12
- };
13
- });
14
-
15
- vi.mock('@nextblock-cms/ecommerce', async () => {
16
- const { z } = await import('zod');
17
- const currencyPriceMapSchema = z.record(z.string(), z.coerce.number().min(0)).default({});
18
- const currencySalePriceMapSchema = z
19
- .record(z.string(), z.coerce.number().min(0).nullable())
20
- .default({});
21
-
22
- return {
23
- productSchema: z
24
- .object({
25
- description_json: z.any().optional(),
26
- freemius_plan_id: z.string().optional(),
27
- freemius_product_id: z.string().optional(),
28
- is_taxable: z.boolean(),
29
- language_id: z.coerce.number().int().min(1),
30
- meta_description: z.string().optional().nullable(),
31
- meta_title: z.string().optional().nullable(),
32
- payment_provider: z.enum(['stripe', 'freemius']),
33
- price: z.coerce.number().min(0),
34
- prices: currencyPriceMapSchema,
35
- product_media: z.array(z.object({ media_id: z.string() })).optional(),
36
- product_type: z.enum(['physical', 'digital']),
37
- sale_price: z.coerce.number().min(0).optional().nullable(),
38
- sale_prices: currencySalePriceMapSchema,
39
- short_description: z.string().optional(),
40
- sku: z.string().min(1),
41
- slug: z.string().min(1),
42
- status: z.enum(['draft', 'active', 'archived']),
43
- stock: z.coerce.number().int().min(0),
44
- title: z.string().min(1),
45
- upc: z.string().optional().nullable(),
46
- variation_attributes: z.array(z.any()).optional(),
47
- variants: z.array(z.any()).optional(),
48
- })
49
- .refine(
50
- (product) =>
51
- product.sale_price === null ||
52
- product.sale_price === undefined ||
53
- product.sale_price <= product.price,
54
- { path: ['sale_price'] }
55
- ),
56
- };
57
- });
58
-
59
- vi.mock('@nextblock-cms/ecommerce/server', async () => {
60
- const { z } = await import('zod');
61
- const currencyPriceMapSchema = z.record(z.string(), z.coerce.number().min(0)).default({});
62
- const currencySalePriceMapSchema = z
63
- .record(z.string(), z.coerce.number().min(0).nullable())
64
- .default({});
65
- const productSchema = z
66
- .object({
67
- description_json: z.any().optional(),
68
- freemius_plan_id: z.string().optional(),
69
- freemius_product_id: z.string().optional(),
70
- is_taxable: z.boolean(),
71
- language_id: z.coerce.number().int().min(1),
72
- meta_description: z.string().optional().nullable(),
73
- meta_title: z.string().optional().nullable(),
74
- payment_provider: z.enum(['stripe', 'freemius']),
75
- price: z.coerce.number().min(0),
76
- prices: currencyPriceMapSchema,
77
- product_media: z.array(z.object({ media_id: z.string() })).optional(),
78
- product_type: z.enum(['physical', 'digital']),
79
- sale_price: z.coerce.number().min(0).optional().nullable(),
80
- sale_prices: currencySalePriceMapSchema,
81
- short_description: z.string().optional(),
82
- sku: z.string().min(1),
83
- slug: z.string().min(1),
84
- status: z.enum(['draft', 'active', 'archived']),
85
- stock: z.coerce.number().int().min(0),
86
- title: z.string().min(1),
87
- upc: z.string().optional().nullable(),
88
- variation_attributes: z.array(z.any()).optional(),
89
- variants: z.array(z.any()).optional(),
90
- })
91
- .refine(
92
- (product) =>
93
- product.sale_price === null ||
94
- product.sale_price === undefined ||
95
- product.sale_price <= product.price,
96
- { path: ['sale_price'] }
97
- );
98
-
99
- return {
100
- createProduct: async (supabase: any, input: Record<string, any>) => {
101
- const { data } = await supabase
102
- .from('products')
103
- .insert({
104
- ...input,
105
- price: Math.round(Number(input.price || 0) * 100),
106
- sale_price:
107
- input.sale_price === null || input.sale_price === undefined
108
- ? null
109
- : Math.round(Number(input.sale_price) * 100),
110
- })
111
- .select('*');
112
-
113
- return data?.[0] ?? null;
114
- },
115
- productSchema,
116
- updateProduct: async (supabase: any, id: string, input: Record<string, any>) => {
117
- const { data } = await supabase
118
- .from('products')
119
- .update({
120
- ...input,
121
- price: Math.round(Number(input.price || 0) * 100),
122
- sale_price:
123
- input.sale_price === null || input.sale_price === undefined
124
- ? null
125
- : Math.round(Number(input.sale_price) * 100),
126
- })
127
- .eq('id', id)
128
- .select('*')
129
- .single();
130
-
131
- return data;
132
- },
133
- };
134
- });
135
-
136
- import {
137
- buildVisibleContactIntroActionPlan,
138
- executeCmsActionPlan,
139
- executeCreateCmsPage,
140
- executeCreateCmsPost,
141
- executeCreateCmsProduct,
142
- executeDeleteCmsItem,
143
- executeInsertContentBlock,
144
- executePrepareDeleteCmsItem,
145
- executeReadCurrentCmsItem,
146
- executeSearchDocumentation,
147
- executeSearchDocumentationWithTimeout,
148
- executeUpdateContentBlock,
149
- executeUpdateCmsItemField,
150
- executeUpdateCurrentCmsFields,
151
- executeUpdateFooter,
152
- executeUpdateNavigationBar,
153
- executeUpdateSectionColumnBlock,
154
- } from './ai-global-agent-tools';
155
-
156
- type MockRow = Record<string, any>;
157
-
158
- type MockDatabase = {
159
- blocks: MockRow[];
160
- currencies: MockRow[];
161
- languages: MockRow[];
162
- navigation_items: MockRow[];
163
- pages: MockRow[];
164
- posts: MockRow[];
165
- products: MockRow[];
166
- site_settings: MockRow[];
167
- };
168
-
169
- class MockQuery {
170
- private filters: Array<{ column: string; value: unknown }> = [];
171
- private limitCount: number | null = null;
172
- private operation: 'delete' | 'insert' | 'select' | 'update' | 'upsert' = 'select';
173
- private payload: MockRow | MockRow[] | null = null;
174
-
175
- constructor(
176
- private readonly database: MockDatabase,
177
- private readonly calls: MockRow[],
178
- private readonly table: keyof MockDatabase
179
- ) {}
180
-
181
- select(columns?: string) {
182
- this.calls.push({ columns, operation: 'select', table: this.table });
183
- return this;
184
- }
185
-
186
- eq(column: string, value: unknown) {
187
- this.filters.push({ column, value });
188
- return this;
189
- }
190
-
191
- limit(count: number) {
192
- this.limitCount = count;
193
- return this;
194
- }
195
-
196
- delete() {
197
- this.operation = 'delete';
198
- this.calls.push({ operation: 'delete', table: this.table });
199
- return this;
200
- }
201
-
202
- insert(payload: MockRow | MockRow[]) {
203
- this.operation = 'insert';
204
- this.payload = payload;
205
- this.calls.push({ operation: 'insert', payload, table: this.table });
206
- return this;
207
- }
208
-
209
- update(payload: MockRow) {
210
- this.operation = 'update';
211
- this.payload = payload;
212
- this.calls.push({ operation: 'update', payload, table: this.table });
213
- return this;
214
- }
215
-
216
- upsert(payload: MockRow | MockRow[]) {
217
- this.operation = 'upsert';
218
- this.payload = payload;
219
- this.calls.push({ operation: 'upsert', payload, table: this.table });
220
- return this;
221
- }
222
-
223
- order() {
224
- return this;
225
- }
226
-
227
- maybeSingle() {
228
- return this.execute().then((result) => ({
229
- data: result.data?.[0] ?? null,
230
- error: result.error,
231
- }));
232
- }
233
-
234
- single() {
235
- return this.execute().then((result) => ({
236
- data: Array.isArray(result.data) ? result.data[0] : result.data,
237
- error: result.error,
238
- }));
239
- }
240
-
241
- then<TResult1 = any, TResult2 = never>(
242
- onfulfilled?: ((value: any) => TResult1 | PromiseLike<TResult1>) | null,
243
- onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null
244
- ) {
245
- return this.execute().then(onfulfilled, onrejected);
246
- }
247
-
248
- private matchesFilters(row: MockRow) {
249
- return this.filters.every((filter) => row[filter.column] === filter.value);
250
- }
251
-
252
- private async execute() {
253
- if (this.operation === 'delete') {
254
- const beforeCount = this.database[this.table].length;
255
- this.database[this.table] = this.database[this.table].filter(
256
- (row) => !this.matchesFilters(row)
257
- );
258
-
259
- return {
260
- data: null,
261
- error: null,
262
- removed: beforeCount - this.database[this.table].length,
263
- };
264
- }
265
-
266
- if (this.operation === 'insert') {
267
- const rows = Array.isArray(this.payload) ? this.payload : [this.payload];
268
- const inserted = rows.filter(Boolean).map((row) => {
269
- const nextId =
270
- this.database[this.table].reduce((max, current) => Math.max(max, Number(current.id) || 0), 0) +
271
- 1;
272
- return { id: nextId, ...row };
273
- });
274
-
275
- this.database[this.table].push(...inserted);
276
-
277
- return {
278
- data: inserted,
279
- error: null,
280
- };
281
- }
282
-
283
- if (this.operation === 'update') {
284
- const payload = Array.isArray(this.payload) ? this.payload[0] : this.payload;
285
- const updated: MockRow[] = [];
286
-
287
- this.database[this.table] = this.database[this.table].map((row) => {
288
- if (!this.matchesFilters(row)) {
289
- return row;
290
- }
291
-
292
- const nextRow = { ...row, ...payload };
293
- updated.push(nextRow);
294
- return nextRow;
295
- });
296
-
297
- return {
298
- data: updated,
299
- error: null,
300
- };
301
- }
302
-
303
- if (this.operation === 'upsert') {
304
- const rows = Array.isArray(this.payload) ? this.payload : [this.payload];
305
-
306
- for (const row of rows.filter(Boolean)) {
307
- const existingIndex = this.database[this.table].findIndex(
308
- (current) => current.key && current.key === row.key
309
- );
310
-
311
- if (existingIndex >= 0) {
312
- this.database[this.table][existingIndex] = {
313
- ...this.database[this.table][existingIndex],
314
- ...row,
315
- };
316
- } else {
317
- this.database[this.table].push(row);
318
- }
319
- }
320
-
321
- return {
322
- data: rows,
323
- error: null,
324
- };
325
- }
326
-
327
- let data = this.database[this.table].filter((row) => this.matchesFilters(row));
328
-
329
- if (this.limitCount !== null) {
330
- data = data.slice(0, this.limitCount);
331
- }
332
-
333
- return {
334
- data,
335
- error: null,
336
- };
337
- }
338
- }
339
-
340
- function createMockSupabase(overrides?: Partial<MockDatabase>) {
341
- const calls: MockRow[] = [];
342
- const database: MockDatabase = {
343
- blocks: [],
344
- currencies: [{ code: 'USD', id: 1, is_active: true, is_default: true }],
345
- languages: [{ code: 'en', id: 1 }],
346
- navigation_items: [
347
- { id: 1, label: 'Old', language_id: 1, menu_key: 'HEADER', order: 0, url: '/old' },
348
- ],
349
- pages: [],
350
- posts: [],
351
- products: [],
352
- site_settings: [],
353
- ...overrides,
354
- };
355
-
356
- return {
357
- calls,
358
- database,
359
- supabase: {
360
- from: (table: string) => {
361
- if (!(table in database)) {
362
- throw new Error(`Unexpected mock table: ${table}`);
363
- }
364
-
365
- return new MockQuery(database, calls, table as keyof MockDatabase);
366
- },
367
- },
368
- };
369
- }
370
-
371
- function expectConfirmation(result: any) {
372
- expect(result).toMatchObject({
373
- mutationExecuted: false,
374
- requiresConfirmation: true,
375
- success: true,
376
- });
377
- expect(result.confirmationPhrase).toEqual(expect.stringMatching(/^CONFIRM .+ #[a-f0-9]{8}$/));
378
- }
379
-
380
- async function executeConfirmed(
381
- executor: (input: any, context?: any) => Promise<any>,
382
- input: any,
383
- context: any = {}
384
- ) {
385
- const preview = await executor(input, context);
386
- expectConfirmation(preview);
387
-
388
- return executor(input, {
389
- ...context,
390
- latestUserMessage: preview.confirmationPhrase,
391
- });
392
- }
393
-
394
- describe('Cortex AI global agent tool executors', () => {
395
- it('replaces the header navigation menu for the selected locale', async () => {
396
- const revalidated: string[] = [];
397
- const { database, supabase } = createMockSupabase();
398
-
399
- const result = await executeConfirmed(
400
- executeUpdateNavigationBar,
401
- {
402
- items: [
403
- {
404
- children: [{ label: 'Team', url: '/about/team' }],
405
- label: 'About',
406
- url: '/about',
407
- },
408
- { label: 'Contact', target: '_self', url: '/contact' },
409
- ],
410
- languageCode: 'en',
411
- mode: 'replace',
412
- },
413
- {
414
- revalidatePath: (path) => revalidated.push(path),
415
- supabase,
416
- }
417
- );
418
-
419
- expect(result).toEqual({
420
- insertedCount: 3,
421
- languageCode: 'en',
422
- menuKey: 'HEADER',
423
- mode: 'replace',
424
- skippedCount: 0,
425
- mutationExecuted: true,
426
- success: true,
427
- updatedCount: 0,
428
- });
429
- expect(database.navigation_items).toEqual([
430
- {
431
- id: 1,
432
- label: 'About',
433
- language_id: 1,
434
- menu_key: 'HEADER',
435
- order: 0,
436
- page_id: null,
437
- parent_id: null,
438
- url: '/about',
439
- },
440
- {
441
- id: 2,
442
- label: 'Team',
443
- language_id: 1,
444
- menu_key: 'HEADER',
445
- order: 0,
446
- page_id: null,
447
- parent_id: 1,
448
- url: '/about/team',
449
- },
450
- {
451
- id: 3,
452
- label: 'Contact',
453
- language_id: 1,
454
- menu_key: 'HEADER',
455
- order: 1,
456
- page_id: null,
457
- parent_id: null,
458
- url: '/contact',
459
- },
460
- ]);
461
- expect(revalidated).toEqual(['/', '/cms/navigation']);
462
- });
463
-
464
- it('appends header navigation items without clearing existing links', async () => {
465
- const { database, supabase } = createMockSupabase({
466
- navigation_items: [
467
- { id: 1, label: 'Home', language_id: 1, menu_key: 'HEADER', order: 0, url: '/' },
468
- { id: 2, label: 'Articles', language_id: 1, menu_key: 'HEADER', order: 1, url: '/articles' },
469
- ],
470
- });
471
-
472
- const result = await executeConfirmed(
473
- executeUpdateNavigationBar,
474
- {
475
- items: [{ label: 'Contact', url: '/contact' }],
476
- languageCode: 'en',
477
- mode: 'append',
478
- },
479
- { revalidatePath: () => undefined, supabase }
480
- );
481
-
482
- expect(result).toEqual({
483
- insertedCount: 1,
484
- languageCode: 'en',
485
- menuKey: 'HEADER',
486
- mode: 'append',
487
- mutationExecuted: true,
488
- skippedCount: 0,
489
- success: true,
490
- updatedCount: 0,
491
- });
492
- expect(database.navigation_items).toEqual([
493
- { id: 1, label: 'Home', language_id: 1, menu_key: 'HEADER', order: 0, url: '/' },
494
- { id: 2, label: 'Articles', language_id: 1, menu_key: 'HEADER', order: 1, url: '/articles' },
495
- {
496
- id: 3,
497
- label: 'Contact',
498
- language_id: 1,
499
- menu_key: 'HEADER',
500
- order: 2,
501
- page_id: null,
502
- parent_id: null,
503
- url: '/contact',
504
- },
505
- ]);
506
- });
507
-
508
- it('resolves language names when appending header navigation items', async () => {
509
- const { database, supabase } = createMockSupabase({
510
- languages: [
511
- { code: 'en', id: 1, is_active: true, name: 'English' },
512
- { code: 'fr', id: 2, is_active: true, name: 'French' },
513
- ],
514
- navigation_items: [
515
- { id: 1, label: 'Home', language_id: 1, menu_key: 'HEADER', order: 0, url: '/' },
516
- { id: 2, label: 'Accueil', language_id: 2, menu_key: 'HEADER', order: 0, url: '/' },
517
- ],
518
- });
519
-
520
- const result = await executeConfirmed(
521
- executeUpdateNavigationBar,
522
- {
523
- items: [{ label: 'Contact', target: '_self', url: 'mailto:info@nextblock.dev' }],
524
- languageCode: 'French',
525
- mode: 'append',
526
- },
527
- { revalidatePath: () => undefined, supabase }
528
- );
529
-
530
- expect(result).toEqual({
531
- insertedCount: 1,
532
- languageCode: 'fr',
533
- menuKey: 'HEADER',
534
- mode: 'append',
535
- mutationExecuted: true,
536
- skippedCount: 0,
537
- success: true,
538
- updatedCount: 0,
539
- });
540
- expect(database.navigation_items).toContainEqual({
541
- id: 3,
542
- label: 'Contact',
543
- language_id: 2,
544
- menu_key: 'HEADER',
545
- order: 1,
546
- page_id: null,
547
- parent_id: null,
548
- url: 'mailto:info@nextblock.dev',
549
- });
550
- });
551
-
552
- it('links AI-created translated navigation items to page and nav translation groups', async () => {
553
- const { database, supabase } = createMockSupabase({
554
- languages: [
555
- { code: 'en', id: 1, is_active: true, name: 'English' },
556
- { code: 'fr', id: 2, is_active: true, name: 'French' },
557
- ],
558
- navigation_items: [
559
- {
560
- id: 1,
561
- label: 'Contact Us',
562
- language_id: 1,
563
- menu_key: 'HEADER',
564
- order: 0,
565
- page_id: 1,
566
- translation_group_id: 'group-nav-contact',
567
- url: '/contact-us',
568
- },
569
- ],
570
- pages: [
571
- {
572
- id: 1,
573
- language_id: 1,
574
- slug: 'contact-us',
575
- title: 'Contact Us',
576
- translation_group_id: 'group-page-contact',
577
- },
578
- {
579
- id: 2,
580
- language_id: 2,
581
- slug: 'contactez-nous',
582
- title: 'Contactez-nous',
583
- translation_group_id: 'group-page-contact',
584
- },
585
- ],
586
- });
587
-
588
- const result = await executeConfirmed(
589
- executeUpdateNavigationBar,
590
- {
591
- items: [{ label: 'Contactez-nous', url: '/contactez-nous' }],
592
- languageCode: 'French',
593
- mode: 'append',
594
- },
595
- { revalidatePath: () => undefined, supabase }
596
- );
597
-
598
- expect(result).toMatchObject({
599
- insertedCount: 1,
600
- languageCode: 'fr',
601
- mutationExecuted: true,
602
- success: true,
603
- });
604
- expect(database.navigation_items[1]).toMatchObject({
605
- label: 'Contactez-nous',
606
- language_id: 2,
607
- menu_key: 'HEADER',
608
- page_id: 2,
609
- translation_group_id: 'group-nav-contact',
610
- url: '/contactez-nous',
611
- });
612
- });
613
-
614
- it('updates a single existing header navigation item without replacing the menu', async () => {
615
- const { database, supabase } = createMockSupabase({
616
- languages: [
617
- { code: 'en', id: 1, is_active: true, name: 'English' },
618
- { code: 'fr', id: 2, is_active: true, name: 'French' },
619
- ],
620
- navigation_items: [
621
- { id: 1, label: 'Accueil', language_id: 2, menu_key: 'HEADER', order: 0, url: '/' },
622
- {
623
- id: 2,
624
- label: 'Contact',
625
- language_id: 2,
626
- menu_key: 'HEADER',
627
- order: 1,
628
- url: 'mailto:info@nextblock.dev',
629
- },
630
- { id: 3, label: 'Articles', language_id: 2, menu_key: 'HEADER', order: 2, url: '/articles' },
631
- ],
632
- });
633
-
634
- const result = await executeConfirmed(
635
- executeUpdateNavigationBar,
636
- {
637
- items: [
638
- {
639
- label: 'Nous Contacter',
640
- target: '_self',
641
- url: 'mailto:info@nextblock.dev',
642
- },
643
- ],
644
- languageCode: 'French',
645
- match: { label: 'Contact' },
646
- mode: 'update',
647
- },
648
- { revalidatePath: () => undefined, supabase }
649
- );
650
-
651
- expect(result).toEqual({
652
- insertedCount: 0,
653
- languageCode: 'fr',
654
- menuKey: 'HEADER',
655
- mode: 'update',
656
- mutationExecuted: true,
657
- skippedCount: 0,
658
- success: true,
659
- updatedCount: 1,
660
- });
661
- expect(database.navigation_items).toEqual([
662
- { id: 1, label: 'Accueil', language_id: 2, menu_key: 'HEADER', order: 0, url: '/' },
663
- {
664
- id: 2,
665
- label: 'Nous Contacter',
666
- language_id: 2,
667
- menu_key: 'HEADER',
668
- order: 1,
669
- url: 'mailto:info@nextblock.dev',
670
- },
671
- { id: 3, label: 'Articles', language_id: 2, menu_key: 'HEADER', order: 2, url: '/articles' },
672
- ]);
673
- });
674
-
675
- it('refuses destructive partial header navigation replacements', async () => {
676
- const { database, supabase } = createMockSupabase({
677
- navigation_items: [
678
- { id: 1, label: 'Home', language_id: 1, menu_key: 'HEADER', order: 0, url: '/' },
679
- { id: 2, label: 'Articles', language_id: 1, menu_key: 'HEADER', order: 1, url: '/articles' },
680
- {
681
- id: 3,
682
- label: 'Contact',
683
- language_id: 1,
684
- menu_key: 'HEADER',
685
- order: 2,
686
- url: 'mailto:info@nextblock.dev',
687
- },
688
- ],
689
- });
690
-
691
- await expect(
692
- executeUpdateNavigationBar(
693
- {
694
- items: [{ label: 'Nous Contacter', url: 'mailto:info@nextblock.dev' }],
695
- languageCode: 'en',
696
- mode: 'replace',
697
- },
698
- { revalidatePath: () => undefined, supabase }
699
- )
700
- ).rejects.toThrow('Refusing destructive HEADER navigation replacement');
701
-
702
- expect(database.navigation_items).toEqual([
703
- { id: 1, label: 'Home', language_id: 1, menu_key: 'HEADER', order: 0, url: '/' },
704
- { id: 2, label: 'Articles', language_id: 1, menu_key: 'HEADER', order: 1, url: '/articles' },
705
- {
706
- id: 3,
707
- label: 'Contact',
708
- language_id: 1,
709
- menu_key: 'HEADER',
710
- order: 2,
711
- url: 'mailto:info@nextblock.dev',
712
- },
713
- ]);
714
- });
715
-
716
- it('skips duplicate header navigation append requests by URL', async () => {
717
- const { database, supabase } = createMockSupabase({
718
- navigation_items: [
719
- { id: 1, label: 'Home', language_id: 1, menu_key: 'HEADER', order: 0, url: '/' },
720
- {
721
- id: 2,
722
- label: 'Contact',
723
- language_id: 1,
724
- menu_key: 'HEADER',
725
- order: 1,
726
- url: 'mailto:info@nextblock.dev',
727
- },
728
- ],
729
- });
730
-
731
- const result = await executeConfirmed(
732
- executeUpdateNavigationBar,
733
- {
734
- items: [{ label: 'Contact', target: '_self', url: 'mailto:info@nextblock.dev' }],
735
- languageCode: 'en',
736
- mode: 'append',
737
- },
738
- { revalidatePath: () => undefined, supabase }
739
- );
740
-
741
- expect(result).toEqual({
742
- insertedCount: 0,
743
- languageCode: 'en',
744
- menuKey: 'HEADER',
745
- mode: 'append',
746
- mutationExecuted: true,
747
- skippedCount: 1,
748
- success: true,
749
- updatedCount: 0,
750
- });
751
- expect(database.navigation_items).toEqual([
752
- { id: 1, label: 'Home', language_id: 1, menu_key: 'HEADER', order: 0, url: '/' },
753
- {
754
- id: 2,
755
- label: 'Contact',
756
- language_id: 1,
757
- menu_key: 'HEADER',
758
- order: 1,
759
- url: 'mailto:info@nextblock.dev',
760
- },
761
- ]);
762
- });
763
-
764
- it('updates footer links and copyright settings', async () => {
765
- const { database, supabase } = createMockSupabase({
766
- navigation_items: [
767
- { id: 10, label: 'Old Footer', language_id: 1, menu_key: 'FOOTER', order: 0, url: '/old' },
768
- ],
769
- site_settings: [{ key: 'footer_copyright', value: { en: 'Old' } }],
770
- });
771
-
772
- const result = await executeConfirmed(
773
- executeUpdateFooter,
774
- {
775
- copyright: { en: '(c) {year} NextBlock. All rights reserved.' },
776
- languageCode: 'en',
777
- links: [{ label: 'Privacy', url: '/privacy' }],
778
- },
779
- { revalidatePath: () => undefined, supabase }
780
- );
781
-
782
- expect(result).toMatchObject({
783
- copyrightUpdated: true,
784
- footerNavigation: {
785
- insertedCount: 1,
786
- languageCode: 'en',
787
- menuKey: 'FOOTER',
788
- },
789
- mutationExecuted: true,
790
- success: true,
791
- });
792
- expect(database.navigation_items).toEqual([
793
- {
794
- id: 1,
795
- label: 'Privacy',
796
- language_id: 1,
797
- menu_key: 'FOOTER',
798
- order: 0,
799
- page_id: null,
800
- parent_id: null,
801
- url: '/privacy',
802
- },
803
- ]);
804
- expect(database.site_settings).toEqual([
805
- {
806
- key: 'footer_copyright',
807
- value: { en: '(c) {year} NextBlock. All rights reserved.' },
808
- },
809
- ]);
810
- });
811
-
812
- it('searches published documentation-like pages and posts', async () => {
813
- const { supabase } = createMockSupabase({
814
- pages: [
815
- {
816
- id: 1,
817
- meta_description: 'CMS setup, editor blocks, and Supabase auth.',
818
- slug: 'docs/setup',
819
- status: 'published',
820
- title: 'Setup Guide',
821
- },
822
- ],
823
- posts: [
824
- {
825
- excerpt: 'Use Supabase auth with profiles and roles in NextBlock.',
826
- id: 1,
827
- meta_description: null,
828
- slug: 'supabase-auth-guide',
829
- status: 'published',
830
- subtitle: null,
831
- title: 'Supabase Auth Guide',
832
- },
833
- {
834
- excerpt: 'Draft content should not be returned.',
835
- id: 2,
836
- slug: 'draft',
837
- status: 'draft',
838
- title: 'Draft',
839
- },
840
- ],
841
- });
842
-
843
- const result = await executeSearchDocumentation(
844
- { limit: 2, query: 'Supabase auth' },
845
- { supabase }
846
- );
847
-
848
- expect(result).toEqual({
849
- query: 'Supabase auth',
850
- results: [
851
- {
852
- excerpt: 'Use Supabase auth with profiles and roles in NextBlock.',
853
- source: 'post',
854
- title: 'Supabase Auth Guide',
855
- url: '/article/supabase-auth-guide',
856
- },
857
- {
858
- excerpt: 'CMS setup, editor blocks, and Supabase auth.',
859
- source: 'page',
860
- title: 'Setup Guide',
861
- url: '/docs/setup',
862
- },
863
- ],
864
- success: true,
865
- });
866
- });
867
-
868
- it('returns a fallback instead of hanging when documentation search is slow', async () => {
869
- const createHangingQuery = () =>
870
- ({
871
- eq() {
872
- return this;
873
- },
874
- limit() {
875
- return new Promise(() => undefined);
876
- },
877
- select() {
878
- return this;
879
- },
880
- }) as any;
881
-
882
- const result = await executeSearchDocumentationWithTimeout(
883
- { limit: 2, query: 'NextBlock project' },
884
- {
885
- supabase: {
886
- from: () => createHangingQuery(),
887
- },
888
- },
889
- 5
890
- );
891
-
892
- expect(result).toMatchObject({
893
- query: 'NextBlock project',
894
- results: [],
895
- success: false,
896
- timedOut: true,
897
- });
898
- });
899
-
900
- it('reads the current page context with block summaries', async () => {
901
- const { supabase } = createMockSupabase({
902
- blocks: [
903
- {
904
- block_type: 'text',
905
- content: { html_content: '<p>Hello</p>' },
906
- id: 11,
907
- language_id: 1,
908
- order: 2,
909
- page_id: 7,
910
- post_id: null,
911
- },
912
- {
913
- block_type: 'heading',
914
- content: { level: 2, text_content: 'Intro' },
915
- id: 10,
916
- language_id: 1,
917
- order: 1,
918
- page_id: 7,
919
- post_id: null,
920
- },
921
- ],
922
- pages: [
923
- {
924
- id: 7,
925
- language_id: 1,
926
- meta_description: null,
927
- slug: 'home',
928
- status: 'published',
929
- title: 'Home',
930
- },
931
- ],
932
- });
933
-
934
- const result = await executeReadCurrentCmsItem(
935
- { includeBlockContent: false, includeBlocks: true },
936
- {
937
- pageContext: { contentType: 'page', entityId: 7, slug: 'home', title: 'Home' },
938
- supabase,
939
- }
940
- );
941
-
942
- expect(result.success).toBe(true);
943
- expect(result.item.title).toBe('Home');
944
- expect(result.blocks).toEqual([
945
- {
946
- blockType: 'heading',
947
- content: undefined,
948
- id: 10,
949
- languageId: 1,
950
- order: 1,
951
- pageId: 7,
952
- postId: null,
953
- },
954
- {
955
- blockType: 'text',
956
- content: undefined,
957
- id: 11,
958
- languageId: 1,
959
- order: 2,
960
- pageId: 7,
961
- postId: null,
962
- },
963
- ]);
964
- });
965
-
966
- it('updates validated product fields including description_json', async () => {
967
- const revalidated: string[] = [];
968
- const { database, supabase } = createMockSupabase({
969
- products: [
970
- {
971
- description_json: null,
972
- id: 'prod_1',
973
- language_id: 1,
974
- meta_description: null,
975
- meta_title: null,
976
- short_description: 'Old short copy',
977
- slug: 'studio-tee',
978
- status: 'draft',
979
- title: 'Studio Tee',
980
- },
981
- ],
982
- });
983
- const descriptionJson = {
984
- content: [
985
- {
986
- content: [{ text: 'NextBlock tee description.', type: 'text' }],
987
- type: 'paragraph',
988
- },
989
- ],
990
- type: 'doc',
991
- };
992
-
993
- const result = await executeConfirmed(
994
- executeUpdateCurrentCmsFields,
995
- {
996
- fields: {
997
- description_json: descriptionJson,
998
- short_description: 'Soft cotton tee for builders.',
999
- status: 'active',
1000
- },
1001
- },
1002
- {
1003
- pageContext: {
1004
- contentType: 'product',
1005
- entityId: 'prod_1',
1006
- slug: 'studio-tee',
1007
- title: 'Studio Tee',
1008
- },
1009
- revalidatePath: (path) => revalidated.push(path),
1010
- supabase,
1011
- }
1012
- );
1013
-
1014
- expect(result).toMatchObject({
1015
- contentType: 'product',
1016
- entityId: 'prod_1',
1017
- mutationExecuted: true,
1018
- slug: 'studio-tee',
1019
- success: true,
1020
- updatedFields: ['description_json', 'short_description', 'status'],
1021
- });
1022
- expect(database.products[0]).toMatchObject({
1023
- description_json: descriptionJson,
1024
- short_description: 'Soft cotton tee for builders.',
1025
- status: 'active',
1026
- });
1027
- expect(revalidated).toEqual([
1028
- '/cms/products/prod_1/edit',
1029
- '/product/studio-tee',
1030
- '/cms/products',
1031
- ]);
1032
- });
1033
-
1034
- it('updates only blocks that belong to the current page context', async () => {
1035
- const { database, supabase } = createMockSupabase({
1036
- blocks: [
1037
- {
1038
- block_type: 'text',
1039
- content: { html_content: '<p>Old</p>' },
1040
- id: 12,
1041
- language_id: 1,
1042
- order: 0,
1043
- page_id: 7,
1044
- post_id: null,
1045
- },
1046
- ],
1047
- });
1048
-
1049
- await expect(
1050
- executeUpdateContentBlock(
1051
- {
1052
- blockId: 12,
1053
- blockType: 'text',
1054
- content: { html_content: '<p>Wrong page</p>' },
1055
- },
1056
- {
1057
- pageContext: { contentType: 'page', entityId: 8 },
1058
- supabase,
1059
- }
1060
- )
1061
- ).rejects.toThrow('does not belong to the current page');
1062
-
1063
- const result = await executeConfirmed(
1064
- executeUpdateContentBlock,
1065
- {
1066
- blockId: 12,
1067
- blockType: 'text',
1068
- content: { html_content: '<p>Updated</p>' },
1069
- },
1070
- {
1071
- pageContext: { contentType: 'page', entityId: 7, slug: 'docs/setup' },
1072
- revalidatePath: () => undefined,
1073
- supabase,
1074
- }
1075
- );
1076
-
1077
- expect(result).toMatchObject({
1078
- blockId: 12,
1079
- blockType: 'text',
1080
- contentUpdated: true,
1081
- mutationExecuted: true,
1082
- success: true,
1083
- });
1084
- expect(database.blocks[0].content).toEqual({ html_content: '<p>Updated</p>' });
1085
- });
1086
-
1087
- it('inserts a visible rich text block before the form on a page', async () => {
1088
- const { database, supabase } = createMockSupabase({
1089
- blocks: [
1090
- {
1091
- block_type: 'form',
1092
- content: {
1093
- fields: [],
1094
- recipient_email: 'info@nextblock.dev',
1095
- submit_button_text: 'Send Message',
1096
- success_message: 'Thanks',
1097
- },
1098
- id: 12,
1099
- language_id: 1,
1100
- order: 0,
1101
- page_id: 7,
1102
- post_id: null,
1103
- },
1104
- ],
1105
- pages: [
1106
- {
1107
- id: 7,
1108
- language_id: 1,
1109
- slug: 'contact-us',
1110
- title: 'Contact Us',
1111
- translation_group_id: 'group-contact',
1112
- },
1113
- ],
1114
- });
1115
-
1116
- const result = await executeConfirmed(
1117
- executeInsertContentBlock,
1118
- {
1119
- anchorBlockType: 'form',
1120
- block: {
1121
- blockType: 'text',
1122
- content: {
1123
- html_content:
1124
- '<h2>Let us help you move faster</h2><p>Tell us what you are building and the NextBlock team will get back to you.</p>',
1125
- },
1126
- },
1127
- contentType: 'page',
1128
- slug: 'contact-us',
1129
- position: 'before',
1130
- },
1131
- { revalidatePath: () => undefined, supabase }
1132
- );
1133
-
1134
- expect(result).toMatchObject({
1135
- blockType: 'text',
1136
- contentType: 'page',
1137
- entityId: 7,
1138
- mutationExecuted: true,
1139
- order: 0,
1140
- success: true,
1141
- });
1142
- expect(database.blocks).toHaveLength(2);
1143
- expect(database.blocks[0]).toMatchObject({
1144
- block_type: 'form',
1145
- order: 1,
1146
- });
1147
- expect(database.blocks[1]).toMatchObject({
1148
- block_type: 'text',
1149
- order: 0,
1150
- page_id: 7,
1151
- });
1152
- expect(database.blocks[1].content.html_content).toContain('Let us help you move faster');
1153
- });
1154
-
1155
- it('builds a deterministic action plan for visible English and French contact intro copy', () => {
1156
- const plan = buildVisibleContactIntroActionPlan(
1157
- 'can you add a title and description above the form on both contact pages english and french'
1158
- );
1159
-
1160
- expect(plan).toMatchObject({
1161
- actions: [
1162
- {
1163
- input: {
1164
- anchorBlockType: 'form',
1165
- contentType: 'page',
1166
- position: 'before',
1167
- slug: 'contact-us',
1168
- },
1169
- tool: 'insert_content_block',
1170
- },
1171
- {
1172
- input: {
1173
- anchorBlockType: 'form',
1174
- contentType: 'page',
1175
- position: 'before',
1176
- slug: 'contactez-nous',
1177
- },
1178
- tool: 'insert_content_block',
1179
- },
1180
- ],
1181
- summary:
1182
- 'Add visible title and description copy above the forms on the English and French Contact pages.',
1183
- });
1184
- expect(plan?.actions[0].input.block.content.html_content).toContain('<h2>');
1185
- expect(plan?.actions[1].input.block.content.html_content).toContain('<h2>');
1186
- });
1187
-
1188
- it('uses an action plan to add localized intro copy above forms on both translated pages', async () => {
1189
- const { database, supabase } = createMockSupabase({
1190
- blocks: [
1191
- {
1192
- block_type: 'form',
1193
- content: {
1194
- fields: [],
1195
- recipient_email: 'info@nextblock.dev',
1196
- submit_button_text: 'Send Message',
1197
- success_message: 'Thanks',
1198
- },
1199
- id: 12,
1200
- language_id: 1,
1201
- order: 0,
1202
- page_id: 7,
1203
- post_id: null,
1204
- },
1205
- {
1206
- block_type: 'form',
1207
- content: {
1208
- fields: [],
1209
- recipient_email: 'info@nextblock.dev',
1210
- submit_button_text: 'Envoyer',
1211
- success_message: 'Merci',
1212
- },
1213
- id: 13,
1214
- language_id: 2,
1215
- order: 0,
1216
- page_id: 8,
1217
- post_id: null,
1218
- },
1219
- ],
1220
- pages: [
1221
- {
1222
- id: 7,
1223
- language_id: 1,
1224
- slug: 'contact-us',
1225
- title: 'Contact Us',
1226
- translation_group_id: 'group-contact',
1227
- },
1228
- {
1229
- id: 8,
1230
- language_id: 2,
1231
- slug: 'contactez-nous',
1232
- title: 'Contactez-nous',
1233
- translation_group_id: 'group-contact',
1234
- },
1235
- ],
1236
- });
1237
-
1238
- const result = await executeConfirmed(
1239
- executeCmsActionPlan,
1240
- {
1241
- actions: [
1242
- {
1243
- input: {
1244
- anchorBlockType: 'form',
1245
- block: {
1246
- blockType: 'text',
1247
- content: {
1248
- html_content:
1249
- '<h2>Ready to talk?</h2><p>Share your goals and we will help you choose the right next step.</p>',
1250
- },
1251
- },
1252
- contentType: 'page',
1253
- position: 'before',
1254
- slug: 'contact-us',
1255
- },
1256
- tool: 'insert_content_block',
1257
- },
1258
- {
1259
- input: {
1260
- anchorBlockType: 'form',
1261
- block: {
1262
- blockType: 'text',
1263
- content: {
1264
- html_content:
1265
- '<h2>Prêt à discuter?</h2><p>Parlez-nous de vos objectifs et nous vous aiderons à choisir la prochaine étape.</p>',
1266
- },
1267
- },
1268
- contentType: 'page',
1269
- position: 'before',
1270
- slug: 'contactez-nous',
1271
- },
1272
- tool: 'insert_content_block',
1273
- },
1274
- ],
1275
- },
1276
- {
1277
- pageContext: {
1278
- contentType: 'page',
1279
- entityId: 7,
1280
- slug: 'contact-us',
1281
- title: 'Contact Us',
1282
- },
1283
- revalidatePath: () => undefined,
1284
- supabase,
1285
- }
1286
- );
1287
-
1288
- expect(result).toMatchObject({
1289
- actionCount: 2,
1290
- mutationExecuted: true,
1291
- success: true,
1292
- });
1293
- const englishTextBlocks = database.blocks.filter(
1294
- (block) => block.block_type === 'text' && block.page_id === 7
1295
- );
1296
- const frenchTextBlocks = database.blocks.filter(
1297
- (block) => block.block_type === 'text' && block.page_id === 8
1298
- );
1299
-
1300
- expect(englishTextBlocks).toHaveLength(1);
1301
- expect(frenchTextBlocks).toHaveLength(1);
1302
- expect(englishTextBlocks[0]).toMatchObject({ order: 0 });
1303
- expect(frenchTextBlocks[0]).toMatchObject({ order: 0 });
1304
- expect(database.blocks).toEqual(
1305
- expect.arrayContaining([
1306
- expect.objectContaining({ block_type: 'form', page_id: 7, order: 1 }),
1307
- expect.objectContaining({ block_type: 'form', page_id: 8, order: 1 }),
1308
- ])
1309
- );
1310
- });
1311
-
1312
- it('merges partial top-level block content with existing content before validation', async () => {
1313
- const { database, supabase } = createMockSupabase({
1314
- blocks: [
1315
- {
1316
- block_type: 'button',
1317
- content: { text: 'Old label', url: '/contact', variant: 'default' },
1318
- id: 14,
1319
- language_id: 1,
1320
- order: 0,
1321
- page_id: 7,
1322
- post_id: null,
1323
- },
1324
- ],
1325
- });
1326
-
1327
- const result = await executeConfirmed(
1328
- executeUpdateContentBlock,
1329
- {
1330
- blockId: 14,
1331
- blockType: 'button',
1332
- content: { text: 'Contact Us' },
1333
- },
1334
- {
1335
- pageContext: { contentType: 'page', entityId: 7, slug: 'home' },
1336
- revalidatePath: () => undefined,
1337
- supabase,
1338
- }
1339
- );
1340
-
1341
- expect(result).toMatchObject({
1342
- blockId: 14,
1343
- blockType: 'button',
1344
- contentUpdated: true,
1345
- mutationExecuted: true,
1346
- success: true,
1347
- });
1348
- expect(database.blocks[0].content).toMatchObject({
1349
- text: 'Contact Us',
1350
- url: '/contact',
1351
- variant: 'default',
1352
- });
1353
- });
1354
-
1355
- it('appends button-shaped content to a section block while preserving required layout fields', async () => {
1356
- const sectionContent = {
1357
- background: { type: 'none' },
1358
- column_blocks: [
1359
- [
1360
- {
1361
- block_type: 'text',
1362
- content: { html_content: '<p>Hero intro</p>' },
1363
- },
1364
- ],
1365
- ],
1366
- column_gap: 'md',
1367
- container_type: 'container',
1368
- padding: { bottom: 'lg', top: 'lg' },
1369
- responsive_columns: { desktop: 1, mobile: 1, tablet: 1 },
1370
- };
1371
- const { database, supabase } = createMockSupabase({
1372
- blocks: [
1373
- {
1374
- block_type: 'section',
1375
- content: sectionContent,
1376
- id: 8,
1377
- language_id: 1,
1378
- order: 0,
1379
- page_id: 7,
1380
- post_id: null,
1381
- },
1382
- ],
1383
- });
1384
-
1385
- const result = await executeConfirmed(
1386
- executeUpdateContentBlock,
1387
- {
1388
- blockId: 8,
1389
- blockType: 'section',
1390
- content: { text: 'Contact Us', url: '/contact', variant: 'default' },
1391
- },
1392
- {
1393
- pageContext: { contentType: 'page', entityId: 7, slug: 'articles' },
1394
- revalidatePath: () => undefined,
1395
- supabase,
1396
- }
1397
- );
1398
-
1399
- expect(result).toMatchObject({
1400
- blockId: 8,
1401
- blockType: 'section',
1402
- contentUpdated: true,
1403
- mutationExecuted: true,
1404
- success: true,
1405
- });
1406
- expect(database.blocks[0].content).toMatchObject({
1407
- background: { type: 'none' },
1408
- column_gap: 'md',
1409
- container_type: 'container',
1410
- padding: { bottom: 'lg', top: 'lg' },
1411
- responsive_columns: { desktop: 1, mobile: 1, tablet: 1 },
1412
- });
1413
- expect(database.blocks[0].content.column_blocks[0]).toHaveLength(2);
1414
- expect(database.blocks[0].content.column_blocks[0][1]).toMatchObject({
1415
- block_type: 'button',
1416
- content: { text: 'Contact Us', url: '/contact', variant: 'default' },
1417
- });
1418
- expect(database.blocks[0].content.column_blocks[0][1].temp_id).toEqual(expect.any(String));
1419
- });
1420
-
1421
- it('updates a validated nested section column block', async () => {
1422
- const sectionContent = {
1423
- background: { type: 'none' },
1424
- column_blocks: [
1425
- [
1426
- {
1427
- block_type: 'text',
1428
- content: { html_content: '<p>Old nested copy</p>' },
1429
- },
1430
- ],
1431
- ],
1432
- column_gap: 'md',
1433
- container_type: 'container',
1434
- padding: { bottom: 'md', top: 'md' },
1435
- responsive_columns: { desktop: 1, mobile: 1, tablet: 1 },
1436
- };
1437
- const { database, supabase } = createMockSupabase({
1438
- blocks: [
1439
- {
1440
- block_type: 'section',
1441
- content: sectionContent,
1442
- id: 20,
1443
- language_id: 1,
1444
- order: 0,
1445
- page_id: 7,
1446
- post_id: null,
1447
- },
1448
- ],
1449
- });
1450
-
1451
- const result = await executeConfirmed(
1452
- executeUpdateSectionColumnBlock,
1453
- {
1454
- blockIndex: 0,
1455
- blockType: 'text',
1456
- columnIndex: 0,
1457
- content: { html_content: '<p>New nested copy</p>' },
1458
- parentBlockId: 20,
1459
- },
1460
- {
1461
- pageContext: { contentType: 'page', entityId: 7, slug: 'home' },
1462
- revalidatePath: () => undefined,
1463
- supabase,
1464
- }
1465
- );
1466
-
1467
- expect(result).toMatchObject({
1468
- blockIndex: 0,
1469
- columnIndex: 0,
1470
- nestedBlockType: 'text',
1471
- parentBlockId: 20,
1472
- parentBlockType: 'section',
1473
- mutationExecuted: true,
1474
- success: true,
1475
- });
1476
- expect(database.blocks[0].content.column_blocks[0][0].content).toEqual({
1477
- html_content: '<p>New nested copy</p>',
1478
- });
1479
- });
1480
-
1481
- it('returns confirmation for existing mutating tools before changing data', async () => {
1482
- const { database, supabase } = createMockSupabase({
1483
- navigation_items: [
1484
- { id: 1, label: 'Home', language_id: 1, menu_key: 'HEADER', order: 0, url: '/' },
1485
- ],
1486
- });
1487
-
1488
- const result = await executeUpdateNavigationBar(
1489
- {
1490
- items: [{ label: 'Contact', url: '/contact' }],
1491
- languageCode: 'en',
1492
- mode: 'append',
1493
- },
1494
- { supabase }
1495
- );
1496
-
1497
- expectConfirmation(result);
1498
- expect(database.navigation_items).toEqual([
1499
- { id: 1, label: 'Home', language_id: 1, menu_key: 'HEADER', order: 0, url: '/' },
1500
- ]);
1501
- });
1502
-
1503
- it('prepares a multi-action CMS plan without mutating', async () => {
1504
- const { database, supabase } = createMockSupabase({
1505
- navigation_items: [
1506
- { id: 1, label: 'Home', language_id: 1, menu_key: 'HEADER', order: 0, url: '/' },
1507
- ],
1508
- });
1509
-
1510
- const preview = await executeCmsActionPlan(
1511
- {
1512
- actions: [
1513
- {
1514
- input: {
1515
- contactEmail: 'info@nextblock.dev',
1516
- title: 'Contact Us',
1517
- },
1518
- tool: 'create_cms_page',
1519
- },
1520
- {
1521
- input: {
1522
- items: [{ label: 'Contact Us', url: '/contact-us' }],
1523
- languageCode: 'en',
1524
- mode: 'append',
1525
- },
1526
- tool: 'update_navigation_bar',
1527
- },
1528
- ],
1529
- },
1530
- { actorUserId: 'admin_1', supabase }
1531
- );
1532
-
1533
- expectConfirmation(preview);
1534
- expect(preview.preview).toMatchObject({
1535
- actionCount: 2,
1536
- summary: 'Complete 2 CMS actions.',
1537
- });
1538
- expect(preview.preview.actionSummaries).toHaveLength(2);
1539
- expect(database.pages).toEqual([]);
1540
- expect(database.navigation_items).toEqual([
1541
- { id: 1, label: 'Home', language_id: 1, menu_key: 'HEADER', order: 0, url: '/' },
1542
- ]);
1543
- });
1544
-
1545
- it('confirms a create-page-plus-navigation action plan in order', async () => {
1546
- const { database, supabase } = createMockSupabase({
1547
- navigation_items: [
1548
- { id: 1, label: 'Home', language_id: 1, menu_key: 'HEADER', order: 0, url: '/' },
1549
- ],
1550
- });
1551
-
1552
- const result = await executeConfirmed(
1553
- executeCmsActionPlan,
1554
- {
1555
- actions: [
1556
- {
1557
- input: {
1558
- contactEmail: 'info@nextblock.dev',
1559
- title: 'Contact Us',
1560
- },
1561
- tool: 'create_cms_page',
1562
- },
1563
- {
1564
- input: {
1565
- items: [{ label: 'Contact Us', url: '/contact-us' }],
1566
- languageCode: 'en',
1567
- mode: 'append',
1568
- },
1569
- tool: 'update_navigation_bar',
1570
- },
1571
- ],
1572
- },
1573
- { actorUserId: 'admin_1', revalidatePath: () => undefined, supabase }
1574
- );
1575
-
1576
- expect(result).toMatchObject({
1577
- actionCount: 2,
1578
- editPath: '/cms/pages/1/edit',
1579
- mutationExecuted: true,
1580
- success: true,
1581
- });
1582
- expect(database.pages[0]).toMatchObject({
1583
- slug: 'contact-us',
1584
- title: 'Contact Us',
1585
- });
1586
- expect(database.navigation_items[1]).toMatchObject({
1587
- label: 'Contact Us',
1588
- language_id: 1,
1589
- menu_key: 'HEADER',
1590
- page_id: 1,
1591
- parent_id: null,
1592
- url: '/contact-us',
1593
- });
1594
- expect(database.navigation_items[1].translation_group_id).toEqual(expect.any(String));
1595
- });
1596
-
1597
- it('normalizes command-string action plans and links created language versions', async () => {
1598
- const { database, supabase } = createMockSupabase({
1599
- languages: [
1600
- { code: 'en', id: 1, is_active: true, name: 'English' },
1601
- { code: 'fr', id: 2, is_active: true, name: 'French' },
1602
- ],
1603
- navigation_items: [
1604
- { id: 1, label: 'Home', language_id: 1, menu_key: 'HEADER', order: 0, url: '/' },
1605
- { id: 2, label: 'Accueil', language_id: 2, menu_key: 'HEADER', order: 0, url: '/' },
1606
- ],
1607
- });
1608
-
1609
- const result = await executeConfirmed(
1610
- executeCmsActionPlan,
1611
- {
1612
- actions: [
1613
- "create_cms_page(title='Contact Us', slug='contact-us', contactEmail='info@nextblock.dev', blocks=[{'blockType': 'form', 'content': {'recipient_email': 'info@nextblock.dev', 'fields': [{'label': 'Name', 'type': 'text'}, {'label': 'Email', 'type': 'email'}, {'label': 'Message', 'type': 'textarea'}]}}])",
1614
- "update_navigation_bar(items=[{'label': 'Contact Us', 'url': '/contact-us'}], languageCode='en', mode='append')",
1615
- "create_cms_page(title='Contactez-nous', slug='contactez-nous', languageCode='fr', contactEmail='info@nextblock.dev', blocks=[{'blockType': 'form', 'content': {'recipient_email': 'info@nextblock.dev', 'fields': [{'label': 'Nom', 'type': 'text'}, {'label': 'Email', 'type': 'email'}, {'label': 'Message', 'type': 'textarea'}]}}])",
1616
- "update_navigation_bar(items=[{'label': 'Contactez-nous', 'url': '/contactez-nous'}], languageCode='fr', mode='append')",
1617
- ],
1618
- },
1619
- { actorUserId: 'admin_1', revalidatePath: () => undefined, supabase }
1620
- );
1621
-
1622
- expect(result).toMatchObject({
1623
- actionCount: 4,
1624
- editPath: '/cms/pages/1/edit',
1625
- mutationExecuted: true,
1626
- success: true,
1627
- });
1628
- expect(database.pages).toHaveLength(2);
1629
- expect(database.pages[0]).toMatchObject({
1630
- language_id: 1,
1631
- slug: 'contact-us',
1632
- title: 'Contact Us',
1633
- });
1634
- expect(database.pages[1]).toMatchObject({
1635
- language_id: 2,
1636
- slug: 'contactez-nous',
1637
- title: 'Contactez-nous',
1638
- translation_group_id: database.pages[0].translation_group_id,
1639
- });
1640
- expect(database.navigation_items[2]).toMatchObject({
1641
- label: 'Contact Us',
1642
- language_id: 1,
1643
- page_id: 1,
1644
- url: '/contact-us',
1645
- });
1646
- expect(database.navigation_items[3]).toMatchObject({
1647
- label: 'Contactez-nous',
1648
- language_id: 2,
1649
- page_id: 2,
1650
- translation_group_id: database.navigation_items[2].translation_group_id,
1651
- url: '/contactez-nous',
1652
- });
1653
- });
1654
-
1655
- it('confirms a navigation-only action plan without returning a navigation path', async () => {
1656
- const { supabase } = createMockSupabase({
1657
- navigation_items: [
1658
- { id: 1, label: 'Home', language_id: 1, menu_key: 'HEADER', order: 0, url: '/' },
1659
- ],
1660
- });
1661
-
1662
- const result = await executeConfirmed(
1663
- executeCmsActionPlan,
1664
- {
1665
- actions: [
1666
- {
1667
- input: {
1668
- items: [{ label: 'Contact', url: '/contact' }],
1669
- languageCode: 'en',
1670
- mode: 'append',
1671
- },
1672
- tool: 'update_navigation_bar',
1673
- },
1674
- ],
1675
- },
1676
- { actorUserId: 'admin_1', revalidatePath: () => undefined, supabase }
1677
- );
1678
-
1679
- expect(result).toMatchObject({
1680
- actionCount: 1,
1681
- mutationExecuted: true,
1682
- success: true,
1683
- });
1684
- expect(result.editPath).toBeUndefined();
1685
- expect(result.redirectPath).toBeUndefined();
1686
- });
1687
-
1688
- it('creates a confirmed Contact Us page with hero and form blocks', async () => {
1689
- const revalidated: string[] = [];
1690
- const { database, supabase } = createMockSupabase();
1691
- const input = {
1692
- contactEmail: 'info@nextblock.dev',
1693
- title: 'Contact Us',
1694
- };
1695
-
1696
- const preview = await executeCreateCmsPage(input, {
1697
- actorUserId: 'admin_1',
1698
- revalidatePath: (path) => revalidated.push(path),
1699
- supabase,
1700
- });
1701
-
1702
- expectConfirmation(preview);
1703
- expect(database.pages).toHaveLength(0);
1704
- expect(database.blocks).toHaveLength(0);
1705
-
1706
- const result = await executeCreateCmsPage(input, {
1707
- actorUserId: 'admin_1',
1708
- latestUserMessage: preview.confirmationPhrase,
1709
- revalidatePath: (path) => revalidated.push(path),
1710
- supabase,
1711
- });
1712
-
1713
- expect(result).toMatchObject({
1714
- blockCount: 2,
1715
- contentType: 'page',
1716
- editPath: '/cms/pages/1/edit',
1717
- entityId: 1,
1718
- mutationExecuted: true,
1719
- slug: 'contact-us',
1720
- success: true,
1721
- title: 'Contact Us',
1722
- });
1723
- expect(database.pages[0]).toMatchObject({
1724
- author_id: 'admin_1',
1725
- language_id: 1,
1726
- slug: 'contact-us',
1727
- status: 'draft',
1728
- title: 'Contact Us',
1729
- });
1730
- expect(database.blocks.map((block) => block.block_type)).toEqual(['section', 'form']);
1731
- expect(database.blocks[1].content).toMatchObject({
1732
- recipient_email: 'info@nextblock.dev',
1733
- submit_button_text: 'Send Message',
1734
- });
1735
- expect(revalidated).toEqual(['/cms/pages/1/edit', '/contact-us', '/cms/pages']);
1736
- });
1737
-
1738
- it('creates a translated page in the supplied translation group', async () => {
1739
- const { database, supabase } = createMockSupabase({
1740
- languages: [
1741
- { code: 'en', id: 1, is_active: true, name: 'English' },
1742
- { code: 'fr', id: 2, is_active: true, name: 'French' },
1743
- ],
1744
- pages: [
1745
- {
1746
- id: 1,
1747
- language_id: 1,
1748
- slug: 'contact-us',
1749
- title: 'Contact Us',
1750
- translation_group_id: 'group-contact',
1751
- },
1752
- ],
1753
- });
1754
-
1755
- const result = await executeConfirmed(
1756
- executeCreateCmsPage,
1757
- {
1758
- contactEmail: 'info@nextblock.dev',
1759
- languageCode: 'French',
1760
- slug: 'contactez-nous',
1761
- title: 'Contactez-nous',
1762
- translationGroupId: 'group-contact',
1763
- },
1764
- { actorUserId: 'admin_1', revalidatePath: () => undefined, supabase }
1765
- );
1766
-
1767
- expect(result).toMatchObject({
1768
- contentType: 'page',
1769
- entityId: 2,
1770
- mutationExecuted: true,
1771
- slug: 'contactez-nous',
1772
- success: true,
1773
- });
1774
- expect(database.pages[1]).toMatchObject({
1775
- language_id: 2,
1776
- slug: 'contactez-nous',
1777
- title: 'Contactez-nous',
1778
- translation_group_id: 'group-contact',
1779
- });
1780
- });
1781
-
1782
- it('refuses to create a second page translation for an existing group language', async () => {
1783
- const { database, supabase } = createMockSupabase({
1784
- languages: [
1785
- { code: 'en', id: 1, is_active: true, name: 'English' },
1786
- { code: 'fr', id: 2, is_active: true, name: 'French' },
1787
- ],
1788
- pages: [
1789
- {
1790
- id: 1,
1791
- language_id: 1,
1792
- slug: 'contact-us',
1793
- title: 'Contact Us',
1794
- translation_group_id: 'group-contact',
1795
- },
1796
- {
1797
- id: 2,
1798
- language_id: 2,
1799
- slug: 'contactez-nous',
1800
- title: 'Contactez-nous',
1801
- translation_group_id: 'group-contact',
1802
- },
1803
- ],
1804
- });
1805
-
1806
- const result = await executeCreateCmsPage(
1807
- {
1808
- languageCode: 'fr',
1809
- slug: 'contact-fr-copy',
1810
- title: 'Contact FR Copy',
1811
- translationGroupId: 'group-contact',
1812
- },
1813
- { actorUserId: 'admin_1', supabase }
1814
- );
1815
-
1816
- expect(result).toMatchObject({
1817
- duplicateTranslation: true,
1818
- mutationExecuted: false,
1819
- success: false,
1820
- });
1821
- expect(database.pages).toHaveLength(2);
1822
- });
1823
-
1824
- it('normalizes common AI-created heading and form block shapes before confirmation', async () => {
1825
- const { database, supabase } = createMockSupabase();
1826
- const input = {
1827
- blocks: [
1828
- {
1829
- blockType: 'heading' as const,
1830
- content: {
1831
- text: 'Contact Us',
1832
- },
1833
- },
1834
- {
1835
- blockType: 'form' as const,
1836
- content: {
1837
- fields: [
1838
- { label: 'Name', type: 'text' },
1839
- { label: 'Email', type: 'email' },
1840
- { label: 'Message', type: 'textarea' },
1841
- ],
1842
- recipient_email: 'info@nextblock.dev',
1843
- },
1844
- },
1845
- ],
1846
- title: 'Contact Us',
1847
- };
1848
-
1849
- const result = await executeConfirmed(
1850
- executeCreateCmsPage,
1851
- input,
1852
- { actorUserId: 'admin_1', revalidatePath: () => undefined, supabase }
1853
- );
1854
-
1855
- expect(result).toMatchObject({
1856
- blockCount: 2,
1857
- mutationExecuted: true,
1858
- slug: 'contact-us',
1859
- success: true,
1860
- });
1861
- expect(database.blocks[0].content).toMatchObject({
1862
- level: 1,
1863
- text_content: 'Contact Us',
1864
- });
1865
- expect(database.blocks[1].content).toMatchObject({
1866
- recipient_email: 'info@nextblock.dev',
1867
- submit_button_text: 'Send Message',
1868
- success_message: 'Thanks for reaching out. We will reply as soon as possible.',
1869
- });
1870
- expect(database.blocks[1].content.fields).toEqual([
1871
- expect.objectContaining({ field_type: 'text', is_required: true, label: 'Name', temp_id: 'field-1' }),
1872
- expect.objectContaining({ field_type: 'email', is_required: true, label: 'Email', temp_id: 'field-2' }),
1873
- expect.objectContaining({ field_type: 'textarea', is_required: true, label: 'Message', temp_id: 'field-3' }),
1874
- ]);
1875
- });
1876
-
1877
- it('creates confirmed posts and products with safe defaults', async () => {
1878
- const { database, supabase } = createMockSupabase();
1879
-
1880
- const postResult = await executeConfirmed(
1881
- executeCreateCmsPost,
1882
- {
1883
- excerpt: 'Latest launch details.',
1884
- title: 'Launch Notes',
1885
- },
1886
- { actorUserId: 'admin_1', revalidatePath: () => undefined, supabase }
1887
- );
1888
-
1889
- expect(postResult).toMatchObject({
1890
- contentType: 'post',
1891
- editPath: '/cms/posts/1/edit',
1892
- mutationExecuted: true,
1893
- slug: 'launch-notes',
1894
- success: true,
1895
- });
1896
- expect(database.posts[0]).toMatchObject({
1897
- author_id: 'admin_1',
1898
- slug: 'launch-notes',
1899
- status: 'draft',
1900
- title: 'Launch Notes',
1901
- });
1902
-
1903
- const productResult = await executeConfirmed(
1904
- executeCreateCmsProduct,
1905
- {
1906
- title: 'Studio Tee',
1907
- },
1908
- { revalidatePath: () => undefined, supabase }
1909
- );
1910
-
1911
- expect(productResult).toMatchObject({
1912
- contentType: 'product',
1913
- editPath: '/cms/products/1/edit',
1914
- mutationExecuted: true,
1915
- slug: 'studio-tee',
1916
- success: true,
1917
- });
1918
- expect(database.products[0]).toMatchObject({
1919
- is_taxable: true,
1920
- payment_provider: 'stripe',
1921
- price: 0,
1922
- product_type: 'physical',
1923
- sku: 'STUDIOTEE',
1924
- status: 'draft',
1925
- stock: 0,
1926
- });
1927
- });
1928
-
1929
- it('returns duplicate slug failures without mutating', async () => {
1930
- const { database, supabase } = createMockSupabase({
1931
- pages: [
1932
- {
1933
- id: 7,
1934
- language_id: 1,
1935
- slug: 'contact-us',
1936
- title: 'Contact Us',
1937
- },
1938
- ],
1939
- });
1940
-
1941
- const result = await executeCreateCmsPage(
1942
- {
1943
- contactEmail: 'info@nextblock.dev',
1944
- title: 'Contact Us',
1945
- },
1946
- { actorUserId: 'admin_1', supabase }
1947
- );
1948
-
1949
- expect(result).toMatchObject({
1950
- duplicate: true,
1951
- mutationExecuted: false,
1952
- success: false,
1953
- });
1954
- expect(database.pages).toHaveLength(1);
1955
- expect(database.blocks).toHaveLength(0);
1956
- });
1957
-
1958
- it('updates single fields with confirmation and status aliases', async () => {
1959
- const { database, supabase } = createMockSupabase({
1960
- languages: [
1961
- { code: 'en', id: 1, is_active: true, name: 'English' },
1962
- { code: 'fr', id: 2, is_active: true, name: 'French' },
1963
- ],
1964
- pages: [
1965
- {
1966
- id: 3,
1967
- language_id: 1,
1968
- slug: 'about',
1969
- status: 'draft',
1970
- title: 'About',
1971
- },
1972
- ],
1973
- });
1974
-
1975
- const result = await executeConfirmed(
1976
- executeUpdateCmsItemField,
1977
- {
1978
- contentType: 'page',
1979
- entityId: 3,
1980
- field: 'status',
1981
- value: 'public',
1982
- },
1983
- { revalidatePath: () => undefined, supabase }
1984
- );
1985
-
1986
- expect(result).toMatchObject({
1987
- contentType: 'page',
1988
- entityId: 3,
1989
- field: 'status',
1990
- mutationExecuted: true,
1991
- success: true,
1992
- });
1993
- expect(database.pages[0].status).toBe('published');
1994
-
1995
- const languageResult = await executeConfirmed(
1996
- executeUpdateCmsItemField,
1997
- {
1998
- contentType: 'page',
1999
- entityId: 3,
2000
- field: 'language',
2001
- value: 'French',
2002
- },
2003
- { revalidatePath: () => undefined, supabase }
2004
- );
2005
-
2006
- expect(languageResult).toMatchObject({
2007
- contentType: 'page',
2008
- field: 'language_id',
2009
- mutationExecuted: true,
2010
- success: true,
2011
- });
2012
- expect(database.pages[0].language_id).toBe(2);
2013
- });
2014
-
2015
- it('updates product prices through ecommerce helpers and refuses scheduled specials', async () => {
2016
- const product = {
2017
- id: 'prod_1',
2018
- is_taxable: true,
2019
- language_id: 1,
2020
- payment_provider: 'stripe',
2021
- price: 1000,
2022
- product_type: 'physical',
2023
- sale_price: null,
2024
- short_description: '',
2025
- sku: 'STUDIO-TEE',
2026
- slug: 'studio-tee',
2027
- status: 'draft',
2028
- stock: 0,
2029
- title: 'Studio Tee',
2030
- upc: '',
2031
- };
2032
- const { database, supabase } = createMockSupabase({
2033
- products: [product],
2034
- });
2035
-
2036
- const priceResult = await executeConfirmed(
2037
- executeUpdateCmsItemField,
2038
- {
2039
- contentType: 'product',
2040
- entityId: 'prod_1',
2041
- field: 'price',
2042
- value: 19.99,
2043
- },
2044
- { revalidatePath: () => undefined, supabase }
2045
- );
2046
-
2047
- expect(priceResult).toMatchObject({
2048
- contentType: 'product',
2049
- field: 'price',
2050
- mutationExecuted: true,
2051
- success: true,
2052
- });
2053
- expect(database.products[0].price).toBe(1999);
2054
-
2055
- const saleResult = await executeConfirmed(
2056
- executeUpdateCmsItemField,
2057
- {
2058
- contentType: 'product',
2059
- entityId: 'prod_1',
2060
- field: 'sale_price',
2061
- value: 9.99,
2062
- },
2063
- { revalidatePath: () => undefined, supabase }
2064
- );
2065
-
2066
- expect(saleResult).toMatchObject({
2067
- contentType: 'product',
2068
- field: 'sale_price',
2069
- mutationExecuted: true,
2070
- success: true,
2071
- });
2072
- expect(database.products[0].sale_price).toBe(999);
2073
-
2074
- const scheduledResult = await executeUpdateCmsItemField(
2075
- {
2076
- contentType: 'product',
2077
- endsAt: '2026-06-01',
2078
- entityId: 'prod_1',
2079
- field: 'sale_price',
2080
- startsAt: '2026-05-01',
2081
- value: 8.99,
2082
- },
2083
- { supabase }
2084
- );
2085
-
2086
- expect(scheduledResult).toMatchObject({
2087
- mutationExecuted: false,
2088
- success: false,
2089
- unsupported: true,
2090
- });
2091
- expect(database.products[0].sale_price).toBe(999);
2092
- });
2093
-
2094
- it('prepares and confirms deleting page translation groups and nav links', async () => {
2095
- const { database, supabase } = createMockSupabase({
2096
- navigation_items: [
2097
- { id: 1, label: 'Contact', language_id: 1, menu_key: 'HEADER', order: 0, url: '/contact' },
2098
- { id: 2, label: 'Contact FR', language_id: 2, menu_key: 'HEADER', order: 0, url: '/contactez-nous' },
2099
- ],
2100
- pages: [
2101
- {
2102
- id: 1,
2103
- language_id: 1,
2104
- slug: 'contact',
2105
- title: 'Contact',
2106
- translation_group_id: 'group-contact',
2107
- },
2108
- {
2109
- id: 2,
2110
- language_id: 2,
2111
- slug: 'contactez-nous',
2112
- title: 'Contactez-nous',
2113
- translation_group_id: 'group-contact',
2114
- },
2115
- ],
2116
- });
2117
-
2118
- const prepared = await executePrepareDeleteCmsItem(
2119
- { contentType: 'page', entityId: 1 },
2120
- { supabase }
2121
- );
2122
-
2123
- expectConfirmation(prepared);
2124
- expect(prepared).toMatchObject({
2125
- preparedDelete: true,
2126
- preview: {
2127
- affectedCount: 2,
2128
- collectionPath: '/cms/pages',
2129
- contentType: 'page',
2130
- navigationLinkCount: 2,
2131
- summary:
2132
- 'Delete page "Contact" (contact), including 2 language versions and 2 navigation links.',
2133
- },
2134
- });
2135
-
2136
- const result = await executeDeleteCmsItem(
2137
- { contentType: 'page', entityId: 1 },
2138
- {
2139
- latestUserMessage: prepared.confirmationPhrase,
2140
- revalidatePath: () => undefined,
2141
- supabase,
2142
- }
2143
- );
2144
-
2145
- expect(result).toMatchObject({
2146
- affectedCount: 2,
2147
- collectionPath: '/cms/pages',
2148
- contentType: 'page',
2149
- mutationExecuted: true,
2150
- redirectPath: '/cms/pages',
2151
- success: true,
2152
- });
2153
- expect(database.pages).toEqual([]);
2154
- expect(database.navigation_items).toEqual([]);
2155
- });
2156
-
2157
- it('deletes a confirmed product without deleting other products', async () => {
2158
- const { database, supabase } = createMockSupabase({
2159
- products: [
2160
- { id: 'prod_1', slug: 'studio-tee', title: 'Studio Tee' },
2161
- { id: 'prod_2', slug: 'studio-hat', title: 'Studio Hat' },
2162
- ],
2163
- });
2164
-
2165
- const result = await executeConfirmed(
2166
- executeDeleteCmsItem,
2167
- { contentType: 'product', entityId: 'prod_1' },
2168
- { revalidatePath: () => undefined, supabase }
2169
- );
2170
-
2171
- expect(result).toMatchObject({
2172
- affectedCount: 1,
2173
- collectionPath: '/cms/products',
2174
- contentType: 'product',
2175
- mutationExecuted: true,
2176
- redirectPath: '/cms/products',
2177
- success: true,
2178
- });
2179
- expect(database.products).toEqual([
2180
- { id: 'prod_2', slug: 'studio-hat', title: 'Studio Hat' },
2181
- ]);
2182
- });
2183
- });