create-nextblock 0.14.2 → 0.14.5
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]/page.tsx +7 -2
- package/templates/nextblock-template/app/[slug]/page.utils.ts +8 -3
- package/templates/nextblock-template/app/actions/postActions.ts +3 -0
- package/templates/nextblock-template/app/actions/visibilityActions.ts +210 -0
- package/templates/nextblock-template/app/actions/visualEditingActions.test.ts +83 -3
- package/templates/nextblock-template/app/actions/visualEditingActions.ts +34 -14
- package/templates/nextblock-template/app/api/ai/global-agent/route.ts +45 -0
- package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +736 -117
- package/templates/nextblock-template/app/api/view/route.ts +114 -0
- package/templates/nextblock-template/app/article/[slug]/page.utils.ts +1 -2
- package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +2 -2
- package/templates/nextblock-template/app/cms/blocks/components/ColumnEditor.tsx +17 -22
- package/templates/nextblock-template/app/cms/blocks/components/EditableBlock.tsx +13 -19
- package/templates/nextblock-template/app/cms/blocks/editors/HeadingBlockEditor.tsx +45 -34
- package/templates/nextblock-template/app/cms/components/DraftStatusActions.tsx +10 -0
- package/templates/nextblock-template/app/cms/components/VisibilityBadge.tsx +62 -0
- package/templates/nextblock-template/app/cms/components/VisibilityControl.tsx +528 -0
- package/templates/nextblock-template/app/cms/pages/[id]/edit/EditPageClient.tsx +33 -17
- package/templates/nextblock-template/app/cms/pages/[id]/edit/page.tsx +19 -7
- package/templates/nextblock-template/app/cms/pages/actions.ts +17 -10
- package/templates/nextblock-template/app/cms/pages/components/PageForm.tsx +7 -29
- package/templates/nextblock-template/app/cms/pages/page.tsx +6 -19
- package/templates/nextblock-template/app/cms/posts/[id]/edit/page.tsx +42 -18
- package/templates/nextblock-template/app/cms/posts/actions.ts +16 -27
- package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +3 -60
- package/templates/nextblock-template/app/cms/posts/page.tsx +6 -13
- package/templates/nextblock-template/app/cms/products/[id]/edit/page.tsx +63 -9
- package/templates/nextblock-template/app/cms/revisions/RevisionHistoryButton.tsx +66 -32
- package/templates/nextblock-template/app/cms/revisions/actions.ts +332 -285
- package/templates/nextblock-template/app/cms/revisions/service.test.ts +498 -0
- package/templates/nextblock-template/app/cms/revisions/service.ts +549 -471
- package/templates/nextblock-template/app/cms/revisions/utils.ts +304 -132
- package/templates/nextblock-template/app/cms/settings/global-css/components/ThemeEditor.tsx +382 -0
- package/templates/nextblock-template/app/cms/settings/global-css/components/ThemeManager.tsx +267 -0
- package/templates/nextblock-template/app/cms/settings/global-css/page.tsx +40 -24
- package/templates/nextblock-template/app/cms/settings/global-css/theme-actions.ts +259 -0
- package/templates/nextblock-template/app/layout.tsx +49 -0
- package/templates/nextblock-template/app/lib/sitemap-utils.ts +6 -4
- package/templates/nextblock-template/app/lib/ucp/server.ts +4 -1
- package/templates/nextblock-template/app/page.tsx +6 -3
- package/templates/nextblock-template/app/product/[slug]/page.tsx +27 -3
- package/templates/nextblock-template/app/providers.tsx +16 -4
- package/templates/nextblock-template/components/blocks/renderers/HeadingBlockRenderer.tsx +7 -10
- package/templates/nextblock-template/components/theme-icon.tsx +78 -0
- package/templates/nextblock-template/components/theme-switcher.tsx +85 -90
- package/templates/nextblock-template/components/visual-editing/NextblockVisualEditing.tsx +4 -1
- package/templates/nextblock-template/context/ThemeCatalogContext.tsx +44 -0
- package/templates/nextblock-template/docs/03-CMS-AND-EDITOR.md +77 -0
- package/templates/nextblock-template/docs/04-DATABASE-AND-AUTH.md +7 -2
- package/templates/nextblock-template/lib/blocks/blockColors.test.ts +114 -0
- package/templates/nextblock-template/lib/blocks/blockColors.ts +132 -0
- package/templates/nextblock-template/lib/blocks/blockRegistry.ts +17 -1
- package/templates/nextblock-template/lib/cms-transfer/server.ts +13 -0
- package/templates/nextblock-template/lib/full-backup/server.ts +1 -0
- package/templates/nextblock-template/lib/publishing/viewUrl.ts +26 -0
- package/templates/nextblock-template/lib/search/server.ts +3 -0
- package/templates/nextblock-template/lib/setup/migrations-bundle.ts +102 -87
- package/templates/nextblock-template/lib/themes/buildThemeCss.test.ts +163 -0
- package/templates/nextblock-template/lib/themes/buildThemeCss.ts +124 -0
- package/templates/nextblock-template/lib/themes/tokenColor.ts +31 -0
- package/templates/nextblock-template/lib/themes/tokens.ts +143 -0
- package/templates/nextblock-template/lib/visual-editing/mutations.ts +4 -1
- package/templates/nextblock-template/lib/visual-editing/product-drafts.ts +46 -1
- package/templates/nextblock-template/next-env.d.ts +2 -2
- package/templates/nextblock-template/package.json +1 -1
- package/templates/nextblock-template/scripts/verify-site-themes.ts +63 -0
- package/templates/nextblock-template/tsconfig.tsbuildinfo +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { verifyPackageOnline, getActiveLanguagesServerSide } from '@nextblock-cms/db/server';
|
|
2
2
|
import { redirect, notFound } from 'next/navigation';
|
|
3
3
|
import Link from 'next/link';
|
|
4
|
-
import { ArrowLeft, ChevronDown } from 'lucide-react';
|
|
4
|
+
import { ArrowLeft, ChevronDown, Eye, FilePenLine } from 'lucide-react';
|
|
5
5
|
import {
|
|
6
6
|
Badge,
|
|
7
7
|
Button,
|
|
@@ -11,7 +11,6 @@ import {
|
|
|
11
11
|
DropdownMenuLabel,
|
|
12
12
|
DropdownMenuSeparator,
|
|
13
13
|
DropdownMenuButtonTrigger,
|
|
14
|
-
ViewLiveButton,
|
|
15
14
|
} from '@nextblock-cms/ui';
|
|
16
15
|
import ProductFormClientShell from '../../ProductFormClientShell';
|
|
17
16
|
import {
|
|
@@ -22,7 +21,6 @@ import {
|
|
|
22
21
|
getStoreConfigStatus,
|
|
23
22
|
normalizeCurrencyRecord,
|
|
24
23
|
updateProductAction,
|
|
25
|
-
publishProductAction,
|
|
26
24
|
getCategoriesWithCount,
|
|
27
25
|
getProductCategories,
|
|
28
26
|
} from '@nextblock-cms/ecommerce/server';
|
|
@@ -33,6 +31,9 @@ import {
|
|
|
33
31
|
} from '../../productFormData';
|
|
34
32
|
import { CortexAiPageContextRegistrar } from '../../../components/CortexAiPageContext';
|
|
35
33
|
import BlockEditorArea from '../../../blocks/components/BlockEditorArea';
|
|
34
|
+
import VisibilityControl from '../../../components/VisibilityControl';
|
|
35
|
+
import RevisionHistoryButton from '../../../revisions/RevisionHistoryButton';
|
|
36
|
+
import { buildViewUrl } from '../../../../../lib/publishing/viewUrl';
|
|
36
37
|
|
|
37
38
|
export default async function EditProductPage({
|
|
38
39
|
params,
|
|
@@ -112,6 +113,17 @@ export default async function EditProductPage({
|
|
|
112
113
|
const buildTranslationCreateHref = (languageId: number) =>
|
|
113
114
|
`/cms/products/new?from_group=${product.translation_group_id}&target_lang_id=${languageId}`;
|
|
114
115
|
const globalAttributes = buildGlobalAttributesForForm(globalAttributesRaw || []);
|
|
116
|
+
const productLanguage = languages.find((language) => language.id === product.language_id);
|
|
117
|
+
|
|
118
|
+
// The Status select used to sit inside ProductForm, where submitting with
|
|
119
|
+
// status "active" was blocked unless the payment provider was enabled and
|
|
120
|
+
// configured. Publishing now happens outside the form, so that guard has to
|
|
121
|
+
// travel with it — a product nobody can pay for should not reach the storefront.
|
|
122
|
+
const productProvider = product.payment_provider as 'stripe' | 'freemius' | undefined;
|
|
123
|
+
const publishBlockedReason =
|
|
124
|
+
productProvider && (!enabledProviders[productProvider] || !configStatus[productProvider].hasKeys)
|
|
125
|
+
? `${productProvider === 'stripe' ? 'Stripe' : 'Freemius'} must be enabled and fully configured before this product can be published.`
|
|
126
|
+
: null;
|
|
115
127
|
|
|
116
128
|
const { data: draftData } = await supabase
|
|
117
129
|
.from('product_drafts')
|
|
@@ -270,13 +282,55 @@ export default async function EditProductPage({
|
|
|
270
282
|
) : null}
|
|
271
283
|
|
|
272
284
|
{product.slug ? (
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
285
|
+
<>
|
|
286
|
+
<Button variant="secondary" asChild>
|
|
287
|
+
<a
|
|
288
|
+
href={buildViewUrl({
|
|
289
|
+
path: `/product/${product.slug}`,
|
|
290
|
+
languageCode: productLanguage?.code ?? null,
|
|
291
|
+
draft: true,
|
|
292
|
+
})}
|
|
293
|
+
target="_blank"
|
|
294
|
+
rel="noopener noreferrer"
|
|
295
|
+
>
|
|
296
|
+
<FilePenLine className="mr-2 h-4 w-4" /> Preview
|
|
297
|
+
</a>
|
|
298
|
+
</Button>
|
|
299
|
+
{product.status === 'active' ? (
|
|
300
|
+
<Button variant="outline" asChild>
|
|
301
|
+
<a
|
|
302
|
+
href={buildViewUrl({
|
|
303
|
+
path: `/product/${product.slug}`,
|
|
304
|
+
languageCode: productLanguage?.code ?? null,
|
|
305
|
+
})}
|
|
306
|
+
target="_blank"
|
|
307
|
+
rel="noopener noreferrer"
|
|
308
|
+
>
|
|
309
|
+
<Eye className="mr-2 h-4 w-4" /> View Live
|
|
310
|
+
</a>
|
|
311
|
+
</Button>
|
|
312
|
+
) : null}
|
|
313
|
+
</>
|
|
279
314
|
) : null}
|
|
315
|
+
|
|
316
|
+
<RevisionHistoryButton parentType="product" parentId={product.id} />
|
|
317
|
+
|
|
318
|
+
<VisibilityControl
|
|
319
|
+
type="product"
|
|
320
|
+
id={product.id}
|
|
321
|
+
status={product.status}
|
|
322
|
+
publishedAt={(product as { published_at?: string | null }).published_at ?? null}
|
|
323
|
+
publicPath={`/product/${product.slug}`}
|
|
324
|
+
languageName={
|
|
325
|
+
productLanguage
|
|
326
|
+
? `${productLanguage.name} (${productLanguage.code.toUpperCase()})`
|
|
327
|
+
: undefined
|
|
328
|
+
}
|
|
329
|
+
translationGroupId={product.translation_group_id}
|
|
330
|
+
languages={languages}
|
|
331
|
+
hasDraft={hasDraft}
|
|
332
|
+
publishBlockedReason={publishBlockedReason}
|
|
333
|
+
/>
|
|
280
334
|
</div>
|
|
281
335
|
</div>
|
|
282
336
|
|
|
@@ -12,17 +12,28 @@ import {
|
|
|
12
12
|
DialogHeader,
|
|
13
13
|
DialogTitle,
|
|
14
14
|
} from "@nextblock-cms/ui";
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
listPageRevisions,
|
|
17
|
+
listPostRevisions,
|
|
18
|
+
listProductRevisions,
|
|
19
|
+
restorePageVersion,
|
|
20
|
+
restorePostVersion,
|
|
21
|
+
restoreProductVersion,
|
|
22
|
+
comparePageVersion,
|
|
23
|
+
comparePostVersion,
|
|
24
|
+
compareProductVersion,
|
|
25
|
+
} from './actions';
|
|
16
26
|
import { useRouter } from 'next/navigation';
|
|
17
27
|
import JsonDiffView from './JsonDiffView';
|
|
18
28
|
import { formatDistanceToNow } from 'date-fns';
|
|
19
29
|
import { toast } from 'react-hot-toast';
|
|
20
30
|
|
|
21
|
-
type ParentType = 'page' | 'post';
|
|
31
|
+
type ParentType = 'page' | 'post' | 'product';
|
|
22
32
|
|
|
23
33
|
interface RevisionHistoryButtonProps {
|
|
24
34
|
parentType: ParentType;
|
|
25
|
-
|
|
35
|
+
/** Pages and posts are keyed by bigint; products by uuid. */
|
|
36
|
+
parentId: number | string;
|
|
26
37
|
}
|
|
27
38
|
|
|
28
39
|
type RevisionItem = {
|
|
@@ -32,6 +43,28 @@ type RevisionItem = {
|
|
|
32
43
|
created_at: string;
|
|
33
44
|
author_id: string | null;
|
|
34
45
|
author?: { full_name?: string | null } | null;
|
|
46
|
+
has_changes?: boolean;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
type ListResult = { error: string } | { revisions: unknown[]; currentVersion: number | null; totalPages: number };
|
|
50
|
+
type RestoreResult = { error: string } | { success: true; version: number };
|
|
51
|
+
type CompareResult = { success: true; current: unknown; target: unknown } | { error: string };
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Exhaustive dispatch, one entry per parent type.
|
|
55
|
+
*
|
|
56
|
+
* Deliberately a lookup rather than an `if page / else` chain: with three parent types a
|
|
57
|
+
* bare `else` silently routes products into the post actions, which type-checks cleanly
|
|
58
|
+
* because both take an id and a version.
|
|
59
|
+
*/
|
|
60
|
+
const REVISION_API: Record<ParentType, {
|
|
61
|
+
list: (id: any, page: number, start?: string, end?: string) => Promise<ListResult>;
|
|
62
|
+
restore: (id: any, version: number) => Promise<RestoreResult>;
|
|
63
|
+
compare: (id: any, version: number) => Promise<CompareResult>;
|
|
64
|
+
}> = {
|
|
65
|
+
page: { list: listPageRevisions, restore: restorePageVersion, compare: comparePageVersion },
|
|
66
|
+
post: { list: listPostRevisions, restore: restorePostVersion, compare: comparePostVersion },
|
|
67
|
+
product: { list: listProductRevisions, restore: restoreProductVersion, compare: compareProductVersion },
|
|
35
68
|
};
|
|
36
69
|
|
|
37
70
|
import { Input } from "@nextblock-cms/ui";
|
|
@@ -53,20 +86,17 @@ export default function RevisionHistoryButton({ parentType, parentId }: Revision
|
|
|
53
86
|
const router = useRouter();
|
|
54
87
|
const [compareLoading, setCompareLoading] = useState(false);
|
|
55
88
|
const [activeCompareVersion, setActiveCompareVersion] = useState<number | null>(null);
|
|
89
|
+
const [restoringVersion, setRestoringVersion] = useState<number | null>(null);
|
|
56
90
|
const [leftText, setLeftText] = useState<string | null>(null);
|
|
57
91
|
const [rightText, setRightText] = useState<string | null>(null);
|
|
58
92
|
|
|
93
|
+
const api = REVISION_API[parentType];
|
|
94
|
+
|
|
59
95
|
const loadRevisions = async (pageToLoad: number, start?: string, end?: string) => {
|
|
60
96
|
setLoading(true);
|
|
61
97
|
setError(null);
|
|
62
98
|
try {
|
|
63
|
-
|
|
64
|
-
// Pass date range to actions
|
|
65
|
-
if (parentType === 'page') {
|
|
66
|
-
res = await listPageRevisions(parentId, pageToLoad, start, end);
|
|
67
|
-
} else {
|
|
68
|
-
res = await listPostRevisions(parentId, pageToLoad, start, end);
|
|
69
|
-
}
|
|
99
|
+
const res = await api.list(parentId, pageToLoad, start, end);
|
|
70
100
|
|
|
71
101
|
if ('error' in res) {
|
|
72
102
|
setError(res.error ?? 'Unknown error');
|
|
@@ -122,37 +152,35 @@ export default function RevisionHistoryButton({ parentType, parentId }: Revision
|
|
|
122
152
|
const handleRestore = (version: number) => {
|
|
123
153
|
setMessage(null);
|
|
124
154
|
setError(null);
|
|
155
|
+
setRestoringVersion(version);
|
|
125
156
|
startTransition(async () => {
|
|
126
157
|
try {
|
|
127
|
-
const res =
|
|
128
|
-
? await restorePageVersion(parentId, version)
|
|
129
|
-
: await restorePostVersion(parentId, version);
|
|
158
|
+
const res = await api.restore(parentId, version);
|
|
130
159
|
if ('error' in res) {
|
|
131
160
|
setError(res.error ?? 'Unknown error');
|
|
132
161
|
toast.error(res.error ?? 'Failed to restore version');
|
|
133
162
|
return;
|
|
134
163
|
}
|
|
135
|
-
setMessage(
|
|
164
|
+
setMessage(`Version ${version} restored as version ${res.version}.`);
|
|
136
165
|
toast.success('Version restored successfully');
|
|
137
166
|
router.refresh();
|
|
138
167
|
setTimeout(() => setOpen(false), 800);
|
|
139
168
|
} catch (e: unknown) {
|
|
140
169
|
setError(e instanceof Error ? e.message : 'Failed to restore version');
|
|
141
170
|
toast.error(e instanceof Error ? e.message : 'Failed to restore version');
|
|
171
|
+
} finally {
|
|
172
|
+
setRestoringVersion(null);
|
|
142
173
|
}
|
|
143
174
|
});
|
|
144
175
|
};
|
|
145
176
|
|
|
146
|
-
type CompareResult = { success: true; current: unknown; target: unknown } | { error: string };
|
|
147
177
|
const handleCompare = async (version: number) => {
|
|
148
178
|
setError(null);
|
|
149
179
|
setMessage(null);
|
|
150
180
|
setActiveCompareVersion(version);
|
|
151
181
|
setCompareLoading(true);
|
|
152
182
|
try {
|
|
153
|
-
const res
|
|
154
|
-
? await comparePageVersion(parentId, version)
|
|
155
|
-
: await comparePostVersion(parentId, version);
|
|
183
|
+
const res = await api.compare(parentId, version);
|
|
156
184
|
if ('error' in res) {
|
|
157
185
|
setError(res.error);
|
|
158
186
|
setLeftText(null);
|
|
@@ -245,25 +273,31 @@ export default function RevisionHistoryButton({ parentType, parentId }: Revision
|
|
|
245
273
|
</div>
|
|
246
274
|
|
|
247
275
|
<div className="flex gap-2">
|
|
248
|
-
{
|
|
276
|
+
{/* A row with no differences against the live content is labelled, not
|
|
277
|
+
hidden: `invisible` left an interactive-looking blank where the
|
|
278
|
+
buttons should be, which reads as a broken dialog. */}
|
|
279
|
+
{!isCurrent && rev.has_changes === false && (
|
|
280
|
+
<span className="text-xs text-muted-foreground italic px-2 self-center">Identical to current</span>
|
|
281
|
+
)}
|
|
282
|
+
{!isCurrent && rev.has_changes !== false && (
|
|
249
283
|
<>
|
|
250
|
-
<Button
|
|
251
|
-
variant="outline"
|
|
252
|
-
size="sm"
|
|
253
|
-
onClick={() => handleCompare(rev.version)}
|
|
254
|
-
disabled={compareLoading && activeCompareVersion === rev.version
|
|
255
|
-
className=
|
|
284
|
+
<Button
|
|
285
|
+
variant="outline"
|
|
286
|
+
size="sm"
|
|
287
|
+
onClick={() => handleCompare(rev.version)}
|
|
288
|
+
disabled={compareLoading && activeCompareVersion === rev.version}
|
|
289
|
+
className="h-8 px-2 text-xs text-blue-600 border-blue-200 hover:bg-blue-50 hover:text-blue-700"
|
|
256
290
|
>
|
|
257
291
|
{compareLoading && activeCompareVersion === rev.version ? <><Spinner className="mr-1 h-3 w-3" /> compare</> : 'Compare'}
|
|
258
292
|
</Button>
|
|
259
|
-
<Button
|
|
260
|
-
variant="destructive"
|
|
261
|
-
size="sm"
|
|
262
|
-
onClick={() => handleRestore(rev.version)}
|
|
263
|
-
disabled={isPending
|
|
264
|
-
className=
|
|
293
|
+
<Button
|
|
294
|
+
variant="destructive"
|
|
295
|
+
size="sm"
|
|
296
|
+
onClick={() => handleRestore(rev.version)}
|
|
297
|
+
disabled={isPending}
|
|
298
|
+
className="h-8 px-2 text-xs"
|
|
265
299
|
>
|
|
266
|
-
{isPending ? <Spinner className="h-3 w-3" /> : 'Restore'}
|
|
300
|
+
{isPending && restoringVersion === rev.version ? <Spinner className="h-3 w-3" /> : 'Restore'}
|
|
267
301
|
</Button>
|
|
268
302
|
</>
|
|
269
303
|
)}
|