create-nextblock 0.16.3 → 0.17.0
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/CLAUDE.md +22 -0
- package/docker-template/.env.docker.example +69 -69
- package/docker-template/docker/db/init/99-jwt.sql +6 -6
- package/docker-template/docker/db/init/99-roles.sql +25 -25
- package/docker-template/docker/kong/kong.yml +112 -112
- package/docker-template/scripts/docker-setup.mjs +310 -310
- package/package.json +1 -1
- package/templates/nextblock-template/.browserslistrc +11 -11
- package/templates/nextblock-template/.swcrc +30 -30
- package/templates/nextblock-template/CLAUDE.md +26 -0
- package/templates/nextblock-template/app/(auth-pages)/sign-up/page.tsx +46 -46
- package/templates/nextblock-template/app/(auth-pages)/two-factor/page.tsx +51 -51
- package/templates/nextblock-template/app/.well-known/ucp/route.ts +16 -16
- package/templates/nextblock-template/app/actions/consent.ts +57 -57
- package/templates/nextblock-template/app/actions/twoFactorEmail.ts +22 -22
- package/templates/nextblock-template/app/api/ai/cortex/build-widget/route.ts +153 -153
- package/templates/nextblock-template/app/api/ai/generate-blocks/route.ts +96 -96
- package/templates/nextblock-template/app/api/brand/email-logo/route.ts +48 -48
- package/templates/nextblock-template/app/api/checkout/freemius/sync/route.ts +29 -29
- package/templates/nextblock-template/app/api/cms/check-updates/route.ts +44 -44
- package/templates/nextblock-template/app/api/cms/full-backup/export/route.ts +33 -33
- package/templates/nextblock-template/app/api/cms/full-backup/restore/route.ts +63 -63
- package/templates/nextblock-template/app/api/cron/reset-sandbox/route.ts +270 -205
- package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +10388 -9315
- package/templates/nextblock-template/app/api/cron/sync-currencies/route.ts +39 -39
- package/templates/nextblock-template/app/api/custom-blocks/db-relations/route.ts +92 -92
- package/templates/nextblock-template/app/api/custom-blocks/editor-definitions/route.ts +43 -43
- package/templates/nextblock-template/app/api/media/library/route.ts +69 -69
- package/templates/nextblock-template/app/api/media/r2-presigned/route.ts +53 -53
- package/templates/nextblock-template/app/api/visual-editing/block-draft/route.ts +47 -47
- package/templates/nextblock-template/app/api/visual-editing/product-draft/route.ts +47 -47
- package/templates/nextblock-template/app/article/[slug]/PostClientContent.tsx +442 -441
- package/templates/nextblock-template/app/article/[slug]/page.utils.ts +4 -1
- package/templates/nextblock-template/app/checkout/UcpCartHydrator.tsx +20 -20
- package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +241 -241
- package/templates/nextblock-template/app/cms/blocks/components/CustomBlockEditorPreview.tsx +160 -160
- package/templates/nextblock-template/app/cms/blocks/editors/DynamicCustomBlockEditor.tsx +167 -167
- package/templates/nextblock-template/app/cms/components/ConnectGitHubButton.tsx +122 -122
- package/templates/nextblock-template/app/cms/components/CortexAiActiveContext.tsx +23 -23
- package/templates/nextblock-template/app/cms/components/SeoScoreBadge.tsx +3 -40
- package/templates/nextblock-template/app/cms/components/SystemAlertsBanner.tsx +112 -112
- package/templates/nextblock-template/app/cms/components/TablePagination.tsx +136 -136
- package/templates/nextblock-template/app/cms/components/TwoFactorReminderBanner.tsx +45 -45
- package/templates/nextblock-template/app/cms/components/github-connect-actions.ts +102 -102
- package/templates/nextblock-template/app/cms/components/system-alerts-actions.ts +31 -31
- package/templates/nextblock-template/app/cms/coupons/[id]/edit/page.tsx +16 -16
- package/templates/nextblock-template/app/cms/coupons/page.tsx +16 -16
- package/templates/nextblock-template/app/cms/custom-blocks/[id]/edit/page.tsx +66 -66
- package/templates/nextblock-template/app/cms/custom-blocks/actions.ts +519 -519
- package/templates/nextblock-template/app/cms/custom-blocks/components/BlocksLibraryTransferControls.tsx +256 -256
- package/templates/nextblock-template/app/cms/custom-blocks/components/DBRelationSelect.tsx +384 -384
- package/templates/nextblock-template/app/cms/custom-blocks/components/ImageR2Picker.tsx +221 -221
- package/templates/nextblock-template/app/cms/custom-blocks/new/page.tsx +12 -12
- package/templates/nextblock-template/app/cms/custom-blocks/page.tsx +438 -438
- package/templates/nextblock-template/app/cms/dashboard/components/DashboardComponents.tsx +200 -200
- package/templates/nextblock-template/app/cms/dashboard/components/DashboardOnboarding.tsx +130 -130
- package/templates/nextblock-template/app/cms/import-export/actions.ts +226 -226
- package/templates/nextblock-template/app/cms/pages/page.tsx +273 -273
- package/templates/nextblock-template/app/cms/posts/page.tsx +251 -251
- package/templates/nextblock-template/app/cms/products/ProductFormClientShell.tsx +19 -0
- package/templates/nextblock-template/app/cms/products/[id]/edit/page.tsx +14 -2
- package/templates/nextblock-template/app/cms/products/categories/page.tsx +12 -12
- package/templates/nextblock-template/app/cms/products/new/page.tsx +135 -135
- package/templates/nextblock-template/app/cms/promotions/PromotionsWorkspace.tsx +456 -456
- package/templates/nextblock-template/app/cms/promotions/actions.ts +115 -115
- package/templates/nextblock-template/app/cms/promotions/page.tsx +31 -31
- package/templates/nextblock-template/app/cms/settings/backup-restore/BackupRestoreWorkspace.tsx +1004 -1004
- package/templates/nextblock-template/app/cms/settings/backup-restore/page.tsx +29 -29
- package/templates/nextblock-template/app/cms/settings/bot-protection/page.tsx +24 -24
- package/templates/nextblock-template/app/cms/settings/email/page.tsx +28 -28
- package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +276 -276
- package/templates/nextblock-template/app/cms/settings/google-analytics/page.tsx +26 -26
- package/templates/nextblock-template/app/cms/settings/logos/components/DeleteLogoButton.tsx +21 -21
- package/templates/nextblock-template/app/cms/settings/logos/components/SetActiveLogoButton.tsx +42 -42
- package/templates/nextblock-template/app/cms/settings/logos/components/SiteSeoSettingsForm.tsx +133 -133
- package/templates/nextblock-template/app/cms/settings/privacy/page.tsx +27 -27
- package/templates/nextblock-template/app/cms/settings/registration/page.tsx +27 -27
- package/templates/nextblock-template/app/cms/settings/security/page.tsx +33 -33
- package/templates/nextblock-template/app/lib/site-settings.ts +105 -105
- package/templates/nextblock-template/app/lib/ucp/protocol.ts +190 -190
- package/templates/nextblock-template/app/lib/ucp/server.test.ts +56 -56
- package/templates/nextblock-template/app/setup/SetupWizard.tsx +678 -678
- package/templates/nextblock-template/app/setup/layout.tsx +13 -13
- package/templates/nextblock-template/app/setup/page.tsx +111 -111
- package/templates/nextblock-template/app/ucp/v1/carts/[id]/cancel/route.ts +38 -38
- package/templates/nextblock-template/app/ucp/v1/carts/[id]/route.ts +68 -68
- package/templates/nextblock-template/app/ucp/v1/carts/route.ts +35 -35
- package/templates/nextblock-template/app/ucp/v1/catalog/lookup/route.ts +35 -35
- package/templates/nextblock-template/app/ucp/v1/catalog/product/route.ts +35 -35
- package/templates/nextblock-template/app/ucp/v1/catalog/search/route.ts +34 -34
- package/templates/nextblock-template/components/CartTranslator.tsx +210 -210
- package/templates/nextblock-template/components/DeferredCartTranslator.tsx +51 -51
- package/templates/nextblock-template/components/DeferredGlobalSearch.tsx +68 -68
- package/templates/nextblock-template/components/DeferredGoogleAnalytics.tsx +70 -70
- package/templates/nextblock-template/components/FeatureImageHero.tsx +47 -47
- package/templates/nextblock-template/components/GlobalSearch.tsx +557 -557
- package/templates/nextblock-template/components/Header.tsx +38 -38
- package/templates/nextblock-template/components/PublicEnvBootstrap.tsx +30 -30
- package/templates/nextblock-template/components/ResponsiveNav.tsx +14 -14
- package/templates/nextblock-template/components/auth/AuthBotProtection.tsx +182 -182
- package/templates/nextblock-template/components/blocks/PostsGridClient.tsx +48 -48
- package/templates/nextblock-template/components/blocks/TestimonialBlock.tsx +9 -9
- package/templates/nextblock-template/components/blocks/publicRendererLoaders.ts +25 -25
- package/templates/nextblock-template/components/blocks/renderers/ButtonBlockRenderer.tsx +92 -92
- package/templates/nextblock-template/components/blocks/renderers/ClientTextBlockRenderer.tsx +11 -0
- package/templates/nextblock-template/components/blocks/renderers/PostsGridBlockRenderer.tsx +24 -24
- package/templates/nextblock-template/components/blocks/renderers/TestimonialBlockRenderer.tsx +57 -57
- package/templates/nextblock-template/components/blocks/renderers/inline/AlertWidgetRenderer.tsx +2 -2
- package/templates/nextblock-template/components/blocks/renderers/inline/CtaWidgetRenderer.tsx +2 -2
- package/templates/nextblock-template/components/media/YouTubeFacade.tsx +105 -105
- package/templates/nextblock-template/components/media/youtube-embed-replace.tsx +32 -32
- package/templates/nextblock-template/components/privacy/ConsentBanner.tsx +170 -170
- package/templates/nextblock-template/components/privacy/ConsentGatedAnalytics.tsx +70 -70
- package/templates/nextblock-template/components/renderers/CachedDynamicLayoutEngine.tsx +28 -28
- package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.test.tsx +166 -166
- package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.tsx +471 -471
- package/templates/nextblock-template/components/visual-editing/DeferredVisualEditing.tsx +21 -21
- package/templates/nextblock-template/context/language-rest-client.ts +32 -32
- package/templates/nextblock-template/docker/db/init/99-jwt.sql +6 -6
- package/templates/nextblock-template/docker/db/init/99-roles.sql +25 -25
- package/templates/nextblock-template/docker/kong/kong.yml +112 -112
- package/templates/nextblock-template/docs/02-ECOMMERCE-CAPABILITIES.md +364 -364
- package/templates/nextblock-template/docs/04-DATABASE-AND-AUTH.md +11 -0
- package/templates/nextblock-template/docs/07-BLOCK-SDK-AND-EXTENSIBILITY.md +146 -146
- package/templates/nextblock-template/docs/08-NEXTBLOCK-CORTEX-AI-ARCHITECTURE.md +19 -0
- package/templates/nextblock-template/docs/10-CUSTOM-BLOCKS.md +222 -222
- package/templates/nextblock-template/lib/app-secrets.ts +39 -39
- package/templates/nextblock-template/lib/auth/cookies.ts +47 -47
- package/templates/nextblock-template/lib/auth/crypto.ts +45 -45
- package/templates/nextblock-template/lib/auth/trustedDevices.ts +92 -92
- package/templates/nextblock-template/lib/blocks/README.md +13 -13
- package/templates/nextblock-template/lib/botProtection/verify.ts +134 -134
- package/templates/nextblock-template/lib/cms/payments-reminder.test.ts +135 -135
- package/templates/nextblock-template/lib/cms/payments-reminder.ts +105 -105
- package/templates/nextblock-template/lib/cms-transfer/types.ts +145 -145
- package/templates/nextblock-template/lib/custom-block-definitions.ts +87 -87
- package/templates/nextblock-template/lib/custom-block-r2-upload-shared.ts +178 -178
- package/templates/nextblock-template/lib/custom-block-r2-upload.test.ts +140 -140
- package/templates/nextblock-template/lib/custom-block-r2-upload.ts +88 -88
- package/templates/nextblock-template/lib/custom-block-relations.test.ts +227 -227
- package/templates/nextblock-template/lib/custom-block-relations.ts +279 -279
- package/templates/nextblock-template/lib/custom-block-safelist.ts +14 -14
- package/templates/nextblock-template/lib/editor/dynamic-extension-core.test.ts +172 -172
- package/templates/nextblock-template/lib/editor/dynamic-extension-core.ts +213 -213
- package/templates/nextblock-template/lib/editor/dynamic-extension-loader.ts +22 -22
- package/templates/nextblock-template/lib/editor/dynamic-extensions.tsx +193 -193
- package/templates/nextblock-template/lib/email/branding-format.test.ts +133 -133
- package/templates/nextblock-template/lib/email/branding-format.ts +123 -123
- package/templates/nextblock-template/lib/email/branding.ts +76 -76
- package/templates/nextblock-template/lib/full-backup/manifest.test.ts +121 -121
- package/templates/nextblock-template/lib/full-backup/manifest.ts +206 -206
- package/templates/nextblock-template/lib/logos/active-logo.ts +53 -53
- package/templates/nextblock-template/lib/media/resolveMediaUrl.ts +56 -54
- package/templates/nextblock-template/lib/media/youtube.ts +99 -99
- package/templates/nextblock-template/lib/onboarding/actions.ts +31 -31
- package/templates/nextblock-template/lib/privacy/consent-client.ts +57 -57
- package/templates/nextblock-template/lib/privacy/contact-emails.ts +64 -64
- package/templates/nextblock-template/lib/privacy/settings.ts +115 -115
- package/templates/nextblock-template/lib/privacy/types.ts +69 -69
- package/templates/nextblock-template/lib/promotions/server.test.ts +74 -74
- package/templates/nextblock-template/lib/promotions/server.ts +741 -741
- package/templates/nextblock-template/lib/resolve-block-relations.test.ts +142 -142
- package/templates/nextblock-template/lib/resolve-block-relations.ts +255 -255
- package/templates/nextblock-template/lib/seo/page-audit-context.tsx +3 -3
- package/templates/nextblock-template/lib/seo/page-document.test.ts +34 -0
- package/templates/nextblock-template/lib/seo/page-document.ts +3 -462
- package/templates/nextblock-template/lib/seo/robots-txt.ts +5 -4
- package/templates/nextblock-template/lib/setup/actions.ts +460 -460
- package/templates/nextblock-template/lib/setup/env-status.ts +125 -125
- package/templates/nextblock-template/lib/setup/env-write.ts +111 -111
- package/templates/nextblock-template/lib/setup/migrations-bundle.ts +50 -10
- package/templates/nextblock-template/lib/setup/provisioning.ts +59 -59
- package/templates/nextblock-template/lib/setup/schema-apply.ts +408 -408
- package/templates/nextblock-template/lib/setup/system-config.ts +105 -105
- package/templates/nextblock-template/lib/setup/types.ts +18 -18
- package/templates/nextblock-template/lib/storage/provider.ts +66 -66
- package/templates/nextblock-template/lib/storage/supabase-storage.ts +103 -103
- package/templates/nextblock-template/lib/updates/github-device.ts +206 -206
- package/templates/nextblock-template/lib/updates/repo-identity.ts +56 -56
- package/templates/nextblock-template/lib/visual-editing/draft-content.test.ts +105 -105
- package/templates/nextblock-template/lib/visual-editing/draft-route.test.ts +42 -42
- package/templates/nextblock-template/lib/visual-editing/edit-info.test.ts +143 -143
- package/templates/nextblock-template/lib/visual-editing/edit-info.ts +94 -94
- package/templates/nextblock-template/lib/visual-editing/product-drafts.test.ts +81 -81
- package/templates/nextblock-template/lib/zod-config.ts +5 -5
- package/templates/nextblock-template/next-env.d.ts +1 -0
- package/templates/nextblock-template/package.json +1 -1
- package/templates/nextblock-template/public/images/cortex_post.webp +0 -0
- package/templates/nextblock-template/public/images/update_nextblock.webp +0 -0
- package/templates/nextblock-template/scripts/docker-setup.mjs +310 -310
- package/templates/nextblock-template/scripts/validate-editor-block-schema.ts +112 -112
- package/templates/nextblock-template/scripts/verify-cortex-ai-build-widget.tsx +98 -98
- package/templates/nextblock-template/scripts/verify-cortex-ai-generate-blocks.ts +62 -62
- package/templates/nextblock-template/scripts/verify-cortex-ai-global-tools.ts +537 -537
- package/templates/nextblock-template/scripts/verify-cortex-ai-routing.ts +58 -58
- package/templates/nextblock-template/scripts/verify-custom-block-definitions.ts +188 -188
- package/templates/nextblock-template/scripts/verify-dynamic-custom-block-extensions.ts +123 -123
- package/templates/nextblock-template/scripts/verify-dynamic-layout-engine.tsx +133 -133
- package/templates/nextblock-template/scripts/verify-milestone-2-custom-blocks.ts +65 -65
- package/templates/nextblock-template/tools/deploy-supabase.js +159 -159
- package/templates/nextblock-template/types/jsdom.d.ts +6 -6
|
@@ -1,256 +1,256 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useRef, useState, useTransition } from "react";
|
|
4
|
-
import { toast } from "react-hot-toast";
|
|
5
|
-
import {
|
|
6
|
-
Alert,
|
|
7
|
-
AlertDescription,
|
|
8
|
-
Badge,
|
|
9
|
-
Button,
|
|
10
|
-
Dialog,
|
|
11
|
-
DialogContent,
|
|
12
|
-
DialogDescription,
|
|
13
|
-
DialogFooter,
|
|
14
|
-
DialogHeader,
|
|
15
|
-
DialogTitle,
|
|
16
|
-
Input,
|
|
17
|
-
Label,
|
|
18
|
-
Select,
|
|
19
|
-
SelectContent,
|
|
20
|
-
SelectItem,
|
|
21
|
-
SelectTrigger,
|
|
22
|
-
SelectValue,
|
|
23
|
-
Spinner,
|
|
24
|
-
} from "@nextblock-cms/ui";
|
|
25
|
-
import { Download, FileDown, Upload } from "lucide-react";
|
|
26
|
-
|
|
27
|
-
import type { CmsImportConflictMode, CmsImportSummary } from "../../../../lib/cms-transfer/types";
|
|
28
|
-
import {
|
|
29
|
-
applyBlocksLibraryImportAction,
|
|
30
|
-
dryRunBlocksLibraryImportAction,
|
|
31
|
-
exportBlocksLibraryAction,
|
|
32
|
-
} from "../actions";
|
|
33
|
-
|
|
34
|
-
interface BlocksLibraryTransferControlsProps {
|
|
35
|
-
onImported?: () => void;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function downloadTextFile(fileName: string, mimeType: string, content: string) {
|
|
39
|
-
const blob = new Blob([content], { type: `${mimeType};charset=utf-8` });
|
|
40
|
-
const url = URL.createObjectURL(blob);
|
|
41
|
-
const link = document.createElement("a");
|
|
42
|
-
link.href = url;
|
|
43
|
-
link.download = fileName;
|
|
44
|
-
document.body.appendChild(link);
|
|
45
|
-
link.click();
|
|
46
|
-
link.remove();
|
|
47
|
-
URL.revokeObjectURL(url);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function readFileAsText(file: File) {
|
|
51
|
-
return new Promise<string>((resolve, reject) => {
|
|
52
|
-
const reader = new FileReader();
|
|
53
|
-
reader.onload = () => resolve(String(reader.result || ""));
|
|
54
|
-
reader.onerror = () => reject(reader.error || new Error("Failed to read file."));
|
|
55
|
-
reader.readAsText(file);
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function SummaryPanel({ summary }: { summary: CmsImportSummary | null }) {
|
|
60
|
-
if (!summary) return null;
|
|
61
|
-
|
|
62
|
-
return (
|
|
63
|
-
<div className="space-y-3 rounded-md border bg-muted/20 p-3">
|
|
64
|
-
<div className="flex flex-wrap gap-2">
|
|
65
|
-
<Badge variant={summary.success ? "default" : "destructive"}>
|
|
66
|
-
{summary.success ? "Ready" : "Needs Fixes"}
|
|
67
|
-
</Badge>
|
|
68
|
-
<Badge variant="outline">{summary.totalRows} blocks</Badge>
|
|
69
|
-
<Badge variant="outline">{summary.created} create</Badge>
|
|
70
|
-
<Badge variant="outline">{summary.updated} update</Badge>
|
|
71
|
-
{summary.warnings.length > 0 ? (
|
|
72
|
-
<Badge variant="secondary">{summary.warnings.length} warnings</Badge>
|
|
73
|
-
) : null}
|
|
74
|
-
</div>
|
|
75
|
-
|
|
76
|
-
{summary.errors.length > 0 ? (
|
|
77
|
-
<Alert variant="destructive">
|
|
78
|
-
<AlertDescription>
|
|
79
|
-
{summary.errors.slice(0, 6).map((error) => (
|
|
80
|
-
<span key={`${error.row}-${error.message}`} className="block">
|
|
81
|
-
{error.row ? `Row ${error.row}: ` : ""}
|
|
82
|
-
{error.message}
|
|
83
|
-
</span>
|
|
84
|
-
))}
|
|
85
|
-
{summary.errors.length > 6 ? (
|
|
86
|
-
<span className="block">And {summary.errors.length - 6} more errors.</span>
|
|
87
|
-
) : null}
|
|
88
|
-
</AlertDescription>
|
|
89
|
-
</Alert>
|
|
90
|
-
) : null}
|
|
91
|
-
|
|
92
|
-
{summary.warnings.length > 0 ? (
|
|
93
|
-
<Alert>
|
|
94
|
-
<AlertDescription>
|
|
95
|
-
{summary.warnings.slice(0, 4).map((warning) => (
|
|
96
|
-
<span key={`${warning.row}-${warning.message}`} className="block">
|
|
97
|
-
{warning.message}
|
|
98
|
-
</span>
|
|
99
|
-
))}
|
|
100
|
-
</AlertDescription>
|
|
101
|
-
</Alert>
|
|
102
|
-
) : null}
|
|
103
|
-
</div>
|
|
104
|
-
);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
export function BlocksLibraryTransferControls({ onImported }: BlocksLibraryTransferControlsProps) {
|
|
108
|
-
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
109
|
-
const [open, setOpen] = useState(false);
|
|
110
|
-
const [bundleJson, setBundleJson] = useState("");
|
|
111
|
-
const [fileName, setFileName] = useState("");
|
|
112
|
-
const [conflictMode, setConflictMode] = useState<CmsImportConflictMode>("overwrite_existing");
|
|
113
|
-
const [summary, setSummary] = useState<CmsImportSummary | null>(null);
|
|
114
|
-
const [isPending, startTransition] = useTransition();
|
|
115
|
-
|
|
116
|
-
const exportLibrary = () => {
|
|
117
|
-
startTransition(async () => {
|
|
118
|
-
const result = await exportBlocksLibraryAction();
|
|
119
|
-
if (!result.success || !result.content || !result.fileName || !result.mimeType) {
|
|
120
|
-
toast.error(result.error || "Failed to export blocks library.");
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
downloadTextFile(result.fileName, result.mimeType, result.content);
|
|
124
|
-
toast.success("Blocks library exported.");
|
|
125
|
-
});
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
const reviewImport = () => {
|
|
129
|
-
if (!bundleJson.trim()) {
|
|
130
|
-
toast.error("Choose a blocks library JSON file first.");
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
startTransition(async () => {
|
|
135
|
-
const result = await dryRunBlocksLibraryImportAction({ bundleJson, conflictMode });
|
|
136
|
-
setSummary(result);
|
|
137
|
-
if (result.success) {
|
|
138
|
-
toast.success("Import review is ready.");
|
|
139
|
-
} else {
|
|
140
|
-
toast.error("Import has errors to fix.");
|
|
141
|
-
}
|
|
142
|
-
});
|
|
143
|
-
};
|
|
144
|
-
|
|
145
|
-
const applyImport = () => {
|
|
146
|
-
if (!summary?.success || !bundleJson.trim()) return;
|
|
147
|
-
|
|
148
|
-
startTransition(async () => {
|
|
149
|
-
const result = await applyBlocksLibraryImportAction({ bundleJson, conflictMode });
|
|
150
|
-
setSummary(result);
|
|
151
|
-
if (!result.success) {
|
|
152
|
-
toast.error("Import failed validation.");
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
toast.success("Blocks library import complete.");
|
|
157
|
-
setOpen(false);
|
|
158
|
-
setBundleJson("");
|
|
159
|
-
setFileName("");
|
|
160
|
-
setSummary(null);
|
|
161
|
-
onImported?.();
|
|
162
|
-
});
|
|
163
|
-
};
|
|
164
|
-
|
|
165
|
-
return (
|
|
166
|
-
<div className="flex flex-wrap items-center gap-2">
|
|
167
|
-
<Button type="button" variant="outline" size="sm" onClick={exportLibrary} disabled={isPending}>
|
|
168
|
-
<Download className="mr-2 h-4 w-4" />
|
|
169
|
-
Export
|
|
170
|
-
</Button>
|
|
171
|
-
<Dialog open={open} onOpenChange={setOpen}>
|
|
172
|
-
<Button type="button" variant="outline" size="sm" onClick={() => setOpen(true)}>
|
|
173
|
-
<Upload className="mr-2 h-4 w-4" />
|
|
174
|
-
Import
|
|
175
|
-
</Button>
|
|
176
|
-
<DialogContent className="max-w-2xl">
|
|
177
|
-
<DialogHeader>
|
|
178
|
-
<DialogTitle>Import Blocks Library</DialogTitle>
|
|
179
|
-
<DialogDescription>
|
|
180
|
-
Restore custom block definitions from a JSON file exported here or from a content backup.
|
|
181
|
-
</DialogDescription>
|
|
182
|
-
</DialogHeader>
|
|
183
|
-
|
|
184
|
-
<div className="space-y-4">
|
|
185
|
-
<div className="space-y-2">
|
|
186
|
-
<Label htmlFor="blocks-library-file">Blocks library JSON file</Label>
|
|
187
|
-
<Input
|
|
188
|
-
id="blocks-library-file"
|
|
189
|
-
ref={fileInputRef}
|
|
190
|
-
type="file"
|
|
191
|
-
accept=".json,application/json"
|
|
192
|
-
onChange={async (event) => {
|
|
193
|
-
const file = event.target.files?.[0];
|
|
194
|
-
setSummary(null);
|
|
195
|
-
if (!file) return;
|
|
196
|
-
setFileName(file.name);
|
|
197
|
-
try {
|
|
198
|
-
setBundleJson(await readFileAsText(file));
|
|
199
|
-
} catch {
|
|
200
|
-
toast.error("Failed to read JSON file.");
|
|
201
|
-
}
|
|
202
|
-
}}
|
|
203
|
-
/>
|
|
204
|
-
{fileName ? <p className="text-xs text-muted-foreground">{fileName}</p> : null}
|
|
205
|
-
</div>
|
|
206
|
-
|
|
207
|
-
<div className="space-y-2">
|
|
208
|
-
<Label>Existing blocks</Label>
|
|
209
|
-
<Select
|
|
210
|
-
value={conflictMode}
|
|
211
|
-
onValueChange={(value) => {
|
|
212
|
-
setConflictMode(value as CmsImportConflictMode);
|
|
213
|
-
setSummary(null);
|
|
214
|
-
}}
|
|
215
|
-
>
|
|
216
|
-
<SelectTrigger>
|
|
217
|
-
<SelectValue />
|
|
218
|
-
</SelectTrigger>
|
|
219
|
-
<SelectContent>
|
|
220
|
-
<SelectItem value="overwrite_existing">Overwrite blocks with the same slug</SelectItem>
|
|
221
|
-
<SelectItem value="create_new">Import every block as a new copy</SelectItem>
|
|
222
|
-
</SelectContent>
|
|
223
|
-
</Select>
|
|
224
|
-
<p className="text-xs text-muted-foreground">
|
|
225
|
-
Blocks are matched by slug. "Create new copies" imports each block under a fresh
|
|
226
|
-
<code className="mx-1">-copy</code> slug instead of replacing the existing one.
|
|
227
|
-
</p>
|
|
228
|
-
</div>
|
|
229
|
-
|
|
230
|
-
<SummaryPanel summary={summary} />
|
|
231
|
-
</div>
|
|
232
|
-
|
|
233
|
-
<DialogFooter className="gap-2 sm:gap-0">
|
|
234
|
-
<Button
|
|
235
|
-
type="button"
|
|
236
|
-
variant="outline"
|
|
237
|
-
onClick={reviewImport}
|
|
238
|
-
disabled={isPending || !bundleJson.trim()}
|
|
239
|
-
>
|
|
240
|
-
{isPending ? <Spinner className="mr-2 h-4 w-4" /> : <FileDown className="mr-2 h-4 w-4" />}
|
|
241
|
-
Review Import
|
|
242
|
-
</Button>
|
|
243
|
-
<Button
|
|
244
|
-
type="button"
|
|
245
|
-
onClick={applyImport}
|
|
246
|
-
disabled={isPending || !summary?.success || !bundleJson.trim()}
|
|
247
|
-
>
|
|
248
|
-
{isPending ? <Spinner className="mr-2 h-4 w-4" /> : <Upload className="mr-2 h-4 w-4" />}
|
|
249
|
-
Apply Import
|
|
250
|
-
</Button>
|
|
251
|
-
</DialogFooter>
|
|
252
|
-
</DialogContent>
|
|
253
|
-
</Dialog>
|
|
254
|
-
</div>
|
|
255
|
-
);
|
|
256
|
-
}
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useRef, useState, useTransition } from "react";
|
|
4
|
+
import { toast } from "react-hot-toast";
|
|
5
|
+
import {
|
|
6
|
+
Alert,
|
|
7
|
+
AlertDescription,
|
|
8
|
+
Badge,
|
|
9
|
+
Button,
|
|
10
|
+
Dialog,
|
|
11
|
+
DialogContent,
|
|
12
|
+
DialogDescription,
|
|
13
|
+
DialogFooter,
|
|
14
|
+
DialogHeader,
|
|
15
|
+
DialogTitle,
|
|
16
|
+
Input,
|
|
17
|
+
Label,
|
|
18
|
+
Select,
|
|
19
|
+
SelectContent,
|
|
20
|
+
SelectItem,
|
|
21
|
+
SelectTrigger,
|
|
22
|
+
SelectValue,
|
|
23
|
+
Spinner,
|
|
24
|
+
} from "@nextblock-cms/ui";
|
|
25
|
+
import { Download, FileDown, Upload } from "lucide-react";
|
|
26
|
+
|
|
27
|
+
import type { CmsImportConflictMode, CmsImportSummary } from "../../../../lib/cms-transfer/types";
|
|
28
|
+
import {
|
|
29
|
+
applyBlocksLibraryImportAction,
|
|
30
|
+
dryRunBlocksLibraryImportAction,
|
|
31
|
+
exportBlocksLibraryAction,
|
|
32
|
+
} from "../actions";
|
|
33
|
+
|
|
34
|
+
interface BlocksLibraryTransferControlsProps {
|
|
35
|
+
onImported?: () => void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function downloadTextFile(fileName: string, mimeType: string, content: string) {
|
|
39
|
+
const blob = new Blob([content], { type: `${mimeType};charset=utf-8` });
|
|
40
|
+
const url = URL.createObjectURL(blob);
|
|
41
|
+
const link = document.createElement("a");
|
|
42
|
+
link.href = url;
|
|
43
|
+
link.download = fileName;
|
|
44
|
+
document.body.appendChild(link);
|
|
45
|
+
link.click();
|
|
46
|
+
link.remove();
|
|
47
|
+
URL.revokeObjectURL(url);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function readFileAsText(file: File) {
|
|
51
|
+
return new Promise<string>((resolve, reject) => {
|
|
52
|
+
const reader = new FileReader();
|
|
53
|
+
reader.onload = () => resolve(String(reader.result || ""));
|
|
54
|
+
reader.onerror = () => reject(reader.error || new Error("Failed to read file."));
|
|
55
|
+
reader.readAsText(file);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function SummaryPanel({ summary }: { summary: CmsImportSummary | null }) {
|
|
60
|
+
if (!summary) return null;
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<div className="space-y-3 rounded-md border bg-muted/20 p-3">
|
|
64
|
+
<div className="flex flex-wrap gap-2">
|
|
65
|
+
<Badge variant={summary.success ? "default" : "destructive"}>
|
|
66
|
+
{summary.success ? "Ready" : "Needs Fixes"}
|
|
67
|
+
</Badge>
|
|
68
|
+
<Badge variant="outline">{summary.totalRows} blocks</Badge>
|
|
69
|
+
<Badge variant="outline">{summary.created} create</Badge>
|
|
70
|
+
<Badge variant="outline">{summary.updated} update</Badge>
|
|
71
|
+
{summary.warnings.length > 0 ? (
|
|
72
|
+
<Badge variant="secondary">{summary.warnings.length} warnings</Badge>
|
|
73
|
+
) : null}
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
{summary.errors.length > 0 ? (
|
|
77
|
+
<Alert variant="destructive">
|
|
78
|
+
<AlertDescription>
|
|
79
|
+
{summary.errors.slice(0, 6).map((error) => (
|
|
80
|
+
<span key={`${error.row}-${error.message}`} className="block">
|
|
81
|
+
{error.row ? `Row ${error.row}: ` : ""}
|
|
82
|
+
{error.message}
|
|
83
|
+
</span>
|
|
84
|
+
))}
|
|
85
|
+
{summary.errors.length > 6 ? (
|
|
86
|
+
<span className="block">And {summary.errors.length - 6} more errors.</span>
|
|
87
|
+
) : null}
|
|
88
|
+
</AlertDescription>
|
|
89
|
+
</Alert>
|
|
90
|
+
) : null}
|
|
91
|
+
|
|
92
|
+
{summary.warnings.length > 0 ? (
|
|
93
|
+
<Alert>
|
|
94
|
+
<AlertDescription>
|
|
95
|
+
{summary.warnings.slice(0, 4).map((warning) => (
|
|
96
|
+
<span key={`${warning.row}-${warning.message}`} className="block">
|
|
97
|
+
{warning.message}
|
|
98
|
+
</span>
|
|
99
|
+
))}
|
|
100
|
+
</AlertDescription>
|
|
101
|
+
</Alert>
|
|
102
|
+
) : null}
|
|
103
|
+
</div>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function BlocksLibraryTransferControls({ onImported }: BlocksLibraryTransferControlsProps) {
|
|
108
|
+
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
109
|
+
const [open, setOpen] = useState(false);
|
|
110
|
+
const [bundleJson, setBundleJson] = useState("");
|
|
111
|
+
const [fileName, setFileName] = useState("");
|
|
112
|
+
const [conflictMode, setConflictMode] = useState<CmsImportConflictMode>("overwrite_existing");
|
|
113
|
+
const [summary, setSummary] = useState<CmsImportSummary | null>(null);
|
|
114
|
+
const [isPending, startTransition] = useTransition();
|
|
115
|
+
|
|
116
|
+
const exportLibrary = () => {
|
|
117
|
+
startTransition(async () => {
|
|
118
|
+
const result = await exportBlocksLibraryAction();
|
|
119
|
+
if (!result.success || !result.content || !result.fileName || !result.mimeType) {
|
|
120
|
+
toast.error(result.error || "Failed to export blocks library.");
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
downloadTextFile(result.fileName, result.mimeType, result.content);
|
|
124
|
+
toast.success("Blocks library exported.");
|
|
125
|
+
});
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const reviewImport = () => {
|
|
129
|
+
if (!bundleJson.trim()) {
|
|
130
|
+
toast.error("Choose a blocks library JSON file first.");
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
startTransition(async () => {
|
|
135
|
+
const result = await dryRunBlocksLibraryImportAction({ bundleJson, conflictMode });
|
|
136
|
+
setSummary(result);
|
|
137
|
+
if (result.success) {
|
|
138
|
+
toast.success("Import review is ready.");
|
|
139
|
+
} else {
|
|
140
|
+
toast.error("Import has errors to fix.");
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const applyImport = () => {
|
|
146
|
+
if (!summary?.success || !bundleJson.trim()) return;
|
|
147
|
+
|
|
148
|
+
startTransition(async () => {
|
|
149
|
+
const result = await applyBlocksLibraryImportAction({ bundleJson, conflictMode });
|
|
150
|
+
setSummary(result);
|
|
151
|
+
if (!result.success) {
|
|
152
|
+
toast.error("Import failed validation.");
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
toast.success("Blocks library import complete.");
|
|
157
|
+
setOpen(false);
|
|
158
|
+
setBundleJson("");
|
|
159
|
+
setFileName("");
|
|
160
|
+
setSummary(null);
|
|
161
|
+
onImported?.();
|
|
162
|
+
});
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
return (
|
|
166
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
167
|
+
<Button type="button" variant="outline" size="sm" onClick={exportLibrary} disabled={isPending}>
|
|
168
|
+
<Download className="mr-2 h-4 w-4" />
|
|
169
|
+
Export
|
|
170
|
+
</Button>
|
|
171
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
172
|
+
<Button type="button" variant="outline" size="sm" onClick={() => setOpen(true)}>
|
|
173
|
+
<Upload className="mr-2 h-4 w-4" />
|
|
174
|
+
Import
|
|
175
|
+
</Button>
|
|
176
|
+
<DialogContent className="max-w-2xl">
|
|
177
|
+
<DialogHeader>
|
|
178
|
+
<DialogTitle>Import Blocks Library</DialogTitle>
|
|
179
|
+
<DialogDescription>
|
|
180
|
+
Restore custom block definitions from a JSON file exported here or from a content backup.
|
|
181
|
+
</DialogDescription>
|
|
182
|
+
</DialogHeader>
|
|
183
|
+
|
|
184
|
+
<div className="space-y-4">
|
|
185
|
+
<div className="space-y-2">
|
|
186
|
+
<Label htmlFor="blocks-library-file">Blocks library JSON file</Label>
|
|
187
|
+
<Input
|
|
188
|
+
id="blocks-library-file"
|
|
189
|
+
ref={fileInputRef}
|
|
190
|
+
type="file"
|
|
191
|
+
accept=".json,application/json"
|
|
192
|
+
onChange={async (event) => {
|
|
193
|
+
const file = event.target.files?.[0];
|
|
194
|
+
setSummary(null);
|
|
195
|
+
if (!file) return;
|
|
196
|
+
setFileName(file.name);
|
|
197
|
+
try {
|
|
198
|
+
setBundleJson(await readFileAsText(file));
|
|
199
|
+
} catch {
|
|
200
|
+
toast.error("Failed to read JSON file.");
|
|
201
|
+
}
|
|
202
|
+
}}
|
|
203
|
+
/>
|
|
204
|
+
{fileName ? <p className="text-xs text-muted-foreground">{fileName}</p> : null}
|
|
205
|
+
</div>
|
|
206
|
+
|
|
207
|
+
<div className="space-y-2">
|
|
208
|
+
<Label>Existing blocks</Label>
|
|
209
|
+
<Select
|
|
210
|
+
value={conflictMode}
|
|
211
|
+
onValueChange={(value) => {
|
|
212
|
+
setConflictMode(value as CmsImportConflictMode);
|
|
213
|
+
setSummary(null);
|
|
214
|
+
}}
|
|
215
|
+
>
|
|
216
|
+
<SelectTrigger>
|
|
217
|
+
<SelectValue />
|
|
218
|
+
</SelectTrigger>
|
|
219
|
+
<SelectContent>
|
|
220
|
+
<SelectItem value="overwrite_existing">Overwrite blocks with the same slug</SelectItem>
|
|
221
|
+
<SelectItem value="create_new">Import every block as a new copy</SelectItem>
|
|
222
|
+
</SelectContent>
|
|
223
|
+
</Select>
|
|
224
|
+
<p className="text-xs text-muted-foreground">
|
|
225
|
+
Blocks are matched by slug. "Create new copies" imports each block under a fresh
|
|
226
|
+
<code className="mx-1">-copy</code> slug instead of replacing the existing one.
|
|
227
|
+
</p>
|
|
228
|
+
</div>
|
|
229
|
+
|
|
230
|
+
<SummaryPanel summary={summary} />
|
|
231
|
+
</div>
|
|
232
|
+
|
|
233
|
+
<DialogFooter className="gap-2 sm:gap-0">
|
|
234
|
+
<Button
|
|
235
|
+
type="button"
|
|
236
|
+
variant="outline"
|
|
237
|
+
onClick={reviewImport}
|
|
238
|
+
disabled={isPending || !bundleJson.trim()}
|
|
239
|
+
>
|
|
240
|
+
{isPending ? <Spinner className="mr-2 h-4 w-4" /> : <FileDown className="mr-2 h-4 w-4" />}
|
|
241
|
+
Review Import
|
|
242
|
+
</Button>
|
|
243
|
+
<Button
|
|
244
|
+
type="button"
|
|
245
|
+
onClick={applyImport}
|
|
246
|
+
disabled={isPending || !summary?.success || !bundleJson.trim()}
|
|
247
|
+
>
|
|
248
|
+
{isPending ? <Spinner className="mr-2 h-4 w-4" /> : <Upload className="mr-2 h-4 w-4" />}
|
|
249
|
+
Apply Import
|
|
250
|
+
</Button>
|
|
251
|
+
</DialogFooter>
|
|
252
|
+
</DialogContent>
|
|
253
|
+
</Dialog>
|
|
254
|
+
</div>
|
|
255
|
+
);
|
|
256
|
+
}
|