create-nextblock 0.16.4 → 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 +8096 -7198
- 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 -3
- 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/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-document.ts +4 -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 +42 -12
- 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,160 +1,160 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import React, { useEffect, useMemo, useState } from "react";
|
|
4
|
-
import { Loader2 } from "lucide-react";
|
|
5
|
-
import { DynamicLayoutEngine } from "../../../../components/renderers/DynamicLayoutEngine";
|
|
6
|
-
|
|
7
|
-
// Module-level cache so every block preview shares a single network request for
|
|
8
|
-
// the custom block definitions instead of re-fetching per card.
|
|
9
|
-
type EditorDefinition = {
|
|
10
|
-
slug: string;
|
|
11
|
-
name: string;
|
|
12
|
-
description?: string | null;
|
|
13
|
-
fields: any[];
|
|
14
|
-
layout_schema: any;
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
const definitionCache: { definitions: EditorDefinition[]; promise: Promise<EditorDefinition[]> | null } = {
|
|
18
|
-
definitions: [],
|
|
19
|
-
promise: null,
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
function loadCustomBlockDefinitions(): Promise<EditorDefinition[]> {
|
|
23
|
-
if (definitionCache.definitions.length > 0) {
|
|
24
|
-
return Promise.resolve(definitionCache.definitions);
|
|
25
|
-
}
|
|
26
|
-
if (!definitionCache.promise) {
|
|
27
|
-
definitionCache.promise = fetch("/api/custom-blocks/editor-definitions")
|
|
28
|
-
.then((res) => (res.ok ? res.json() : { definitions: [] }))
|
|
29
|
-
.then((data) => {
|
|
30
|
-
definitionCache.definitions = (data?.definitions ?? []) as EditorDefinition[];
|
|
31
|
-
return definitionCache.definitions;
|
|
32
|
-
})
|
|
33
|
-
.catch(() => []);
|
|
34
|
-
}
|
|
35
|
-
return definitionCache.promise;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function useCustomBlockDefinition(slug: string) {
|
|
39
|
-
const [definitions, setDefinitions] = useState<EditorDefinition[]>(definitionCache.definitions);
|
|
40
|
-
const [loading, setLoading] = useState(definitionCache.definitions.length === 0);
|
|
41
|
-
|
|
42
|
-
useEffect(() => {
|
|
43
|
-
let cancelled = false;
|
|
44
|
-
loadCustomBlockDefinitions().then((defs) => {
|
|
45
|
-
if (!cancelled) {
|
|
46
|
-
setDefinitions(defs);
|
|
47
|
-
setLoading(false);
|
|
48
|
-
}
|
|
49
|
-
});
|
|
50
|
-
return () => {
|
|
51
|
-
cancelled = true;
|
|
52
|
-
};
|
|
53
|
-
}, []);
|
|
54
|
-
|
|
55
|
-
const definition = useMemo(
|
|
56
|
-
() => definitions.find((def) => def.slug === slug) ?? null,
|
|
57
|
-
[definitions, slug]
|
|
58
|
-
);
|
|
59
|
-
|
|
60
|
-
return { definition, loading };
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Resolve db_relation field values into records so the preview matches the front
|
|
64
|
-
// end (including relation images), mirroring the server-side hydration.
|
|
65
|
-
function useResolvedRelations(definition: EditorDefinition | null, content: Record<string, any>) {
|
|
66
|
-
const [resolvedRelations, setResolvedRelations] = useState<Record<string, any>>({});
|
|
67
|
-
|
|
68
|
-
const relationSignature = useMemo(() => {
|
|
69
|
-
if (!definition) return "";
|
|
70
|
-
return definition.fields
|
|
71
|
-
.filter((field) => field.type === "db_relation")
|
|
72
|
-
.map((field) => `${field.key}:${JSON.stringify(content?.[field.key] ?? null)}`)
|
|
73
|
-
.join("|");
|
|
74
|
-
}, [definition, content]);
|
|
75
|
-
|
|
76
|
-
useEffect(() => {
|
|
77
|
-
if (!definition) return;
|
|
78
|
-
const relationFields = definition.fields.filter((field) => field.type === "db_relation");
|
|
79
|
-
if (relationFields.length === 0) {
|
|
80
|
-
setResolvedRelations({});
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
let cancelled = false;
|
|
85
|
-
(async () => {
|
|
86
|
-
const next: Record<string, any> = {};
|
|
87
|
-
await Promise.all(
|
|
88
|
-
relationFields.map(async (field) => {
|
|
89
|
-
const raw = content?.[field.key];
|
|
90
|
-
const values = Array.isArray(raw) ? raw : raw ? [raw] : [];
|
|
91
|
-
if (values.length === 0) return;
|
|
92
|
-
|
|
93
|
-
const params = new URLSearchParams({
|
|
94
|
-
table: field.table,
|
|
95
|
-
valueColumn: field.value_column || "id",
|
|
96
|
-
displayColumn: field.display_column || "title",
|
|
97
|
-
values: values.map(String).join(","),
|
|
98
|
-
limit: String(values.length),
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
try {
|
|
102
|
-
const res = await fetch(`/api/custom-blocks/db-relations?${params.toString()}`, {
|
|
103
|
-
cache: "no-store",
|
|
104
|
-
});
|
|
105
|
-
if (!res.ok) return;
|
|
106
|
-
const data = await res.json();
|
|
107
|
-
const items = data?.items ?? [];
|
|
108
|
-
next[field.key] = field.multiple ? items : items[0] ?? null;
|
|
109
|
-
} catch {
|
|
110
|
-
// Leave the relation unresolved; the engine falls back gracefully.
|
|
111
|
-
}
|
|
112
|
-
})
|
|
113
|
-
);
|
|
114
|
-
|
|
115
|
-
if (!cancelled) {
|
|
116
|
-
setResolvedRelations(next);
|
|
117
|
-
}
|
|
118
|
-
})();
|
|
119
|
-
|
|
120
|
-
return () => {
|
|
121
|
-
cancelled = true;
|
|
122
|
-
};
|
|
123
|
-
}, [definition, relationSignature]);
|
|
124
|
-
|
|
125
|
-
return resolvedRelations;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
export interface CustomBlockEditorPreviewProps {
|
|
129
|
-
blockType: string;
|
|
130
|
-
content: Record<string, any>;
|
|
131
|
-
fallback: React.ReactNode;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
export function CustomBlockEditorPreview({ blockType, content, fallback }: CustomBlockEditorPreviewProps) {
|
|
135
|
-
const { definition, loading } = useCustomBlockDefinition(blockType);
|
|
136
|
-
const resolvedRelations = useResolvedRelations(definition, content);
|
|
137
|
-
|
|
138
|
-
if (!definition) {
|
|
139
|
-
if (loading) {
|
|
140
|
-
return (
|
|
141
|
-
<div className="flex items-center gap-2 py-3 text-xs text-muted-foreground">
|
|
142
|
-
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
|
143
|
-
Loading block preview…
|
|
144
|
-
</div>
|
|
145
|
-
);
|
|
146
|
-
}
|
|
147
|
-
return <>{fallback}</>;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
return (
|
|
151
|
-
// pointer-events-none lets clicks fall through to the card so editing still opens.
|
|
152
|
-
<div className="pointer-events-none w-full overflow-hidden">
|
|
153
|
-
<DynamicLayoutEngine
|
|
154
|
-
fields={definition.fields}
|
|
155
|
-
layoutSchema={definition.layout_schema}
|
|
156
|
-
data={{ ...(content || {}), resolved_relations: resolvedRelations }}
|
|
157
|
-
/>
|
|
158
|
-
</div>
|
|
159
|
-
);
|
|
160
|
-
}
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useMemo, useState } from "react";
|
|
4
|
+
import { Loader2 } from "lucide-react";
|
|
5
|
+
import { DynamicLayoutEngine } from "../../../../components/renderers/DynamicLayoutEngine";
|
|
6
|
+
|
|
7
|
+
// Module-level cache so every block preview shares a single network request for
|
|
8
|
+
// the custom block definitions instead of re-fetching per card.
|
|
9
|
+
type EditorDefinition = {
|
|
10
|
+
slug: string;
|
|
11
|
+
name: string;
|
|
12
|
+
description?: string | null;
|
|
13
|
+
fields: any[];
|
|
14
|
+
layout_schema: any;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const definitionCache: { definitions: EditorDefinition[]; promise: Promise<EditorDefinition[]> | null } = {
|
|
18
|
+
definitions: [],
|
|
19
|
+
promise: null,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function loadCustomBlockDefinitions(): Promise<EditorDefinition[]> {
|
|
23
|
+
if (definitionCache.definitions.length > 0) {
|
|
24
|
+
return Promise.resolve(definitionCache.definitions);
|
|
25
|
+
}
|
|
26
|
+
if (!definitionCache.promise) {
|
|
27
|
+
definitionCache.promise = fetch("/api/custom-blocks/editor-definitions")
|
|
28
|
+
.then((res) => (res.ok ? res.json() : { definitions: [] }))
|
|
29
|
+
.then((data) => {
|
|
30
|
+
definitionCache.definitions = (data?.definitions ?? []) as EditorDefinition[];
|
|
31
|
+
return definitionCache.definitions;
|
|
32
|
+
})
|
|
33
|
+
.catch(() => []);
|
|
34
|
+
}
|
|
35
|
+
return definitionCache.promise;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function useCustomBlockDefinition(slug: string) {
|
|
39
|
+
const [definitions, setDefinitions] = useState<EditorDefinition[]>(definitionCache.definitions);
|
|
40
|
+
const [loading, setLoading] = useState(definitionCache.definitions.length === 0);
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
let cancelled = false;
|
|
44
|
+
loadCustomBlockDefinitions().then((defs) => {
|
|
45
|
+
if (!cancelled) {
|
|
46
|
+
setDefinitions(defs);
|
|
47
|
+
setLoading(false);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
return () => {
|
|
51
|
+
cancelled = true;
|
|
52
|
+
};
|
|
53
|
+
}, []);
|
|
54
|
+
|
|
55
|
+
const definition = useMemo(
|
|
56
|
+
() => definitions.find((def) => def.slug === slug) ?? null,
|
|
57
|
+
[definitions, slug]
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
return { definition, loading };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Resolve db_relation field values into records so the preview matches the front
|
|
64
|
+
// end (including relation images), mirroring the server-side hydration.
|
|
65
|
+
function useResolvedRelations(definition: EditorDefinition | null, content: Record<string, any>) {
|
|
66
|
+
const [resolvedRelations, setResolvedRelations] = useState<Record<string, any>>({});
|
|
67
|
+
|
|
68
|
+
const relationSignature = useMemo(() => {
|
|
69
|
+
if (!definition) return "";
|
|
70
|
+
return definition.fields
|
|
71
|
+
.filter((field) => field.type === "db_relation")
|
|
72
|
+
.map((field) => `${field.key}:${JSON.stringify(content?.[field.key] ?? null)}`)
|
|
73
|
+
.join("|");
|
|
74
|
+
}, [definition, content]);
|
|
75
|
+
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
if (!definition) return;
|
|
78
|
+
const relationFields = definition.fields.filter((field) => field.type === "db_relation");
|
|
79
|
+
if (relationFields.length === 0) {
|
|
80
|
+
setResolvedRelations({});
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let cancelled = false;
|
|
85
|
+
(async () => {
|
|
86
|
+
const next: Record<string, any> = {};
|
|
87
|
+
await Promise.all(
|
|
88
|
+
relationFields.map(async (field) => {
|
|
89
|
+
const raw = content?.[field.key];
|
|
90
|
+
const values = Array.isArray(raw) ? raw : raw ? [raw] : [];
|
|
91
|
+
if (values.length === 0) return;
|
|
92
|
+
|
|
93
|
+
const params = new URLSearchParams({
|
|
94
|
+
table: field.table,
|
|
95
|
+
valueColumn: field.value_column || "id",
|
|
96
|
+
displayColumn: field.display_column || "title",
|
|
97
|
+
values: values.map(String).join(","),
|
|
98
|
+
limit: String(values.length),
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
const res = await fetch(`/api/custom-blocks/db-relations?${params.toString()}`, {
|
|
103
|
+
cache: "no-store",
|
|
104
|
+
});
|
|
105
|
+
if (!res.ok) return;
|
|
106
|
+
const data = await res.json();
|
|
107
|
+
const items = data?.items ?? [];
|
|
108
|
+
next[field.key] = field.multiple ? items : items[0] ?? null;
|
|
109
|
+
} catch {
|
|
110
|
+
// Leave the relation unresolved; the engine falls back gracefully.
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
if (!cancelled) {
|
|
116
|
+
setResolvedRelations(next);
|
|
117
|
+
}
|
|
118
|
+
})();
|
|
119
|
+
|
|
120
|
+
return () => {
|
|
121
|
+
cancelled = true;
|
|
122
|
+
};
|
|
123
|
+
}, [definition, relationSignature]);
|
|
124
|
+
|
|
125
|
+
return resolvedRelations;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface CustomBlockEditorPreviewProps {
|
|
129
|
+
blockType: string;
|
|
130
|
+
content: Record<string, any>;
|
|
131
|
+
fallback: React.ReactNode;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function CustomBlockEditorPreview({ blockType, content, fallback }: CustomBlockEditorPreviewProps) {
|
|
135
|
+
const { definition, loading } = useCustomBlockDefinition(blockType);
|
|
136
|
+
const resolvedRelations = useResolvedRelations(definition, content);
|
|
137
|
+
|
|
138
|
+
if (!definition) {
|
|
139
|
+
if (loading) {
|
|
140
|
+
return (
|
|
141
|
+
<div className="flex items-center gap-2 py-3 text-xs text-muted-foreground">
|
|
142
|
+
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
|
143
|
+
Loading block preview…
|
|
144
|
+
</div>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
return <>{fallback}</>;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
// pointer-events-none lets clicks fall through to the card so editing still opens.
|
|
152
|
+
<div className="pointer-events-none w-full overflow-hidden">
|
|
153
|
+
<DynamicLayoutEngine
|
|
154
|
+
fields={definition.fields}
|
|
155
|
+
layoutSchema={definition.layout_schema}
|
|
156
|
+
data={{ ...(content || {}), resolved_relations: resolvedRelations }}
|
|
157
|
+
/>
|
|
158
|
+
</div>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
@@ -1,167 +1,167 @@
|
|
|
1
|
-
// app/cms/blocks/editors/DynamicCustomBlockEditor.tsx
|
|
2
|
-
"use client";
|
|
3
|
-
|
|
4
|
-
import React, { useEffect, useState } from "react";
|
|
5
|
-
import { Label } from "@nextblock-cms/ui";
|
|
6
|
-
import { Input } from "@nextblock-cms/ui";
|
|
7
|
-
import { Textarea } from "@nextblock-cms/ui";
|
|
8
|
-
import { BlockEditorProps } from "../components/BlockEditorModal";
|
|
9
|
-
import { DBRelationSelect } from "../../custom-blocks/components/DBRelationSelect";
|
|
10
|
-
import { ImageR2Picker } from "../../custom-blocks/components/ImageR2Picker";
|
|
11
|
-
import type { CustomBlockField } from "@nextblock-cms/utils";
|
|
12
|
-
|
|
13
|
-
type CustomBlockDefinitionResponse = {
|
|
14
|
-
id: string;
|
|
15
|
-
slug: string;
|
|
16
|
-
name: string;
|
|
17
|
-
fields: CustomBlockField[];
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
// Fallback raw-JSON editor for block types with no custom definition.
|
|
21
|
-
function JsonFallbackEditor({
|
|
22
|
-
content,
|
|
23
|
-
onChange,
|
|
24
|
-
}: {
|
|
25
|
-
content: Record<string, any>;
|
|
26
|
-
onChange: (next: any) => void;
|
|
27
|
-
}) {
|
|
28
|
-
const [value, setValue] = useState(() => JSON.stringify(content ?? {}, null, 2));
|
|
29
|
-
const [error, setError] = useState<string | null>(null);
|
|
30
|
-
|
|
31
|
-
useEffect(() => {
|
|
32
|
-
setValue(JSON.stringify(content ?? {}, null, 2));
|
|
33
|
-
setError(null);
|
|
34
|
-
}, [content]);
|
|
35
|
-
|
|
36
|
-
return (
|
|
37
|
-
<div className="space-y-2 p-4 border rounded-xl bg-card/50">
|
|
38
|
-
<Label htmlFor="custom-block-json-fallback" className="text-xs font-semibold">
|
|
39
|
-
Block Content (JSON)
|
|
40
|
-
</Label>
|
|
41
|
-
<Textarea
|
|
42
|
-
id="custom-block-json-fallback"
|
|
43
|
-
value={value}
|
|
44
|
-
spellCheck={false}
|
|
45
|
-
className="min-h-[320px] font-mono text-xs"
|
|
46
|
-
onChange={(e) => {
|
|
47
|
-
const next = e.target.value;
|
|
48
|
-
setValue(next);
|
|
49
|
-
try {
|
|
50
|
-
onChange(JSON.parse(next));
|
|
51
|
-
setError(null);
|
|
52
|
-
} catch (parseError) {
|
|
53
|
-
setError(parseError instanceof Error ? parseError.message : "Invalid JSON");
|
|
54
|
-
}
|
|
55
|
-
}}
|
|
56
|
-
/>
|
|
57
|
-
{error && <p className="text-xs text-destructive">{error}</p>}
|
|
58
|
-
</div>
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export default function DynamicCustomBlockEditor({
|
|
63
|
-
block,
|
|
64
|
-
content,
|
|
65
|
-
onChange,
|
|
66
|
-
}: BlockEditorProps<Record<string, any>>) {
|
|
67
|
-
const [definition, setDefinition] = useState<CustomBlockDefinitionResponse | null>(null);
|
|
68
|
-
const [loading, setLoading] = useState(true);
|
|
69
|
-
|
|
70
|
-
useEffect(() => {
|
|
71
|
-
async function loadDefinition() {
|
|
72
|
-
try {
|
|
73
|
-
const response = await fetch("/api/custom-blocks/editor-definitions");
|
|
74
|
-
if (response.ok) {
|
|
75
|
-
const payload = await response.json();
|
|
76
|
-
const blockType = block.block_type || (block as any).type;
|
|
77
|
-
const def = payload.definitions?.find((d: any) => d.slug === blockType);
|
|
78
|
-
if (def) {
|
|
79
|
-
setDefinition(def);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
} catch (err) {
|
|
83
|
-
console.error("Failed to load custom block definition for editor:", err);
|
|
84
|
-
} finally {
|
|
85
|
-
setLoading(false);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
loadDefinition();
|
|
89
|
-
}, [block.block_type, (block as any).type]);
|
|
90
|
-
|
|
91
|
-
if (loading) {
|
|
92
|
-
return <div className="p-4 text-center text-sm text-muted-foreground">Loading custom block settings...</div>;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// No matching custom block definition (e.g. a non-custom block type routed
|
|
96
|
-
// here as a fallback). Degrade gracefully to a raw JSON editor instead of an
|
|
97
|
-
// error so content stays editable.
|
|
98
|
-
if (!definition) {
|
|
99
|
-
return <JsonFallbackEditor content={content} onChange={onChange} />;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const handleFieldChange = (key: string, val: any) => {
|
|
103
|
-
onChange({ ...content, [key]: val });
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
return (
|
|
107
|
-
<div className="space-y-4 p-4 border rounded-xl bg-card/50">
|
|
108
|
-
<div className="pb-2 border-b">
|
|
109
|
-
<h4 className="text-sm font-bold text-slate-800 dark:text-slate-200">{definition.name}</h4>
|
|
110
|
-
<p className="text-xs text-muted-foreground">Custom Block Properties</p>
|
|
111
|
-
</div>
|
|
112
|
-
|
|
113
|
-
{definition.fields.map((field) => {
|
|
114
|
-
const fieldVal = content[field.key];
|
|
115
|
-
const fieldId = `custom-block-editor-${definition.slug}-${field.key}`;
|
|
116
|
-
|
|
117
|
-
return (
|
|
118
|
-
<div key={field.key} className="space-y-1 pt-2">
|
|
119
|
-
<Label htmlFor={fieldId} className="text-xs font-semibold text-slate-700 dark:text-slate-300">
|
|
120
|
-
{field.label} {field.required && <span className="text-destructive">*</span>}
|
|
121
|
-
</Label>
|
|
122
|
-
|
|
123
|
-
{field.type === "rich-text" ? (
|
|
124
|
-
<Textarea
|
|
125
|
-
id={fieldId}
|
|
126
|
-
value={fieldVal || ""}
|
|
127
|
-
onChange={(e) => handleFieldChange(field.key, e.target.value)}
|
|
128
|
-
placeholder={field.placeholder}
|
|
129
|
-
className="min-h-24 text-sm"
|
|
130
|
-
/>
|
|
131
|
-
) : field.type === "image_r2" ? (
|
|
132
|
-
<ImageR2Picker
|
|
133
|
-
value={fieldVal && typeof fieldVal === "object" && "url" in fieldVal ? fieldVal : null}
|
|
134
|
-
onChange={(val) => handleFieldChange(field.key, val)}
|
|
135
|
-
accept={field.accept}
|
|
136
|
-
maxBytes={field.max_bytes}
|
|
137
|
-
/>
|
|
138
|
-
) : field.type === "db_relation" ? (
|
|
139
|
-
<DBRelationSelect
|
|
140
|
-
table={field.table}
|
|
141
|
-
value={
|
|
142
|
-
field.multiple
|
|
143
|
-
? Array.isArray(fieldVal) ? fieldVal.map(String) : []
|
|
144
|
-
: fieldVal ? String(fieldVal) : null
|
|
145
|
-
}
|
|
146
|
-
onChange={(val) => handleFieldChange(field.key, val)}
|
|
147
|
-
multiple={field.multiple}
|
|
148
|
-
displayColumn={field.display_column}
|
|
149
|
-
valueColumn={field.value_column}
|
|
150
|
-
filters={field.filters}
|
|
151
|
-
/>
|
|
152
|
-
) : (
|
|
153
|
-
<Input
|
|
154
|
-
id={fieldId}
|
|
155
|
-
type="text"
|
|
156
|
-
value={fieldVal || ""}
|
|
157
|
-
onChange={(e) => handleFieldChange(field.key, e.target.value)}
|
|
158
|
-
placeholder={field.placeholder}
|
|
159
|
-
className="h-9"
|
|
160
|
-
/>
|
|
161
|
-
)}
|
|
162
|
-
</div>
|
|
163
|
-
);
|
|
164
|
-
})}
|
|
165
|
-
</div>
|
|
166
|
-
);
|
|
167
|
-
}
|
|
1
|
+
// app/cms/blocks/editors/DynamicCustomBlockEditor.tsx
|
|
2
|
+
"use client";
|
|
3
|
+
|
|
4
|
+
import React, { useEffect, useState } from "react";
|
|
5
|
+
import { Label } from "@nextblock-cms/ui";
|
|
6
|
+
import { Input } from "@nextblock-cms/ui";
|
|
7
|
+
import { Textarea } from "@nextblock-cms/ui";
|
|
8
|
+
import { BlockEditorProps } from "../components/BlockEditorModal";
|
|
9
|
+
import { DBRelationSelect } from "../../custom-blocks/components/DBRelationSelect";
|
|
10
|
+
import { ImageR2Picker } from "../../custom-blocks/components/ImageR2Picker";
|
|
11
|
+
import type { CustomBlockField } from "@nextblock-cms/utils";
|
|
12
|
+
|
|
13
|
+
type CustomBlockDefinitionResponse = {
|
|
14
|
+
id: string;
|
|
15
|
+
slug: string;
|
|
16
|
+
name: string;
|
|
17
|
+
fields: CustomBlockField[];
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// Fallback raw-JSON editor for block types with no custom definition.
|
|
21
|
+
function JsonFallbackEditor({
|
|
22
|
+
content,
|
|
23
|
+
onChange,
|
|
24
|
+
}: {
|
|
25
|
+
content: Record<string, any>;
|
|
26
|
+
onChange: (next: any) => void;
|
|
27
|
+
}) {
|
|
28
|
+
const [value, setValue] = useState(() => JSON.stringify(content ?? {}, null, 2));
|
|
29
|
+
const [error, setError] = useState<string | null>(null);
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
setValue(JSON.stringify(content ?? {}, null, 2));
|
|
33
|
+
setError(null);
|
|
34
|
+
}, [content]);
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<div className="space-y-2 p-4 border rounded-xl bg-card/50">
|
|
38
|
+
<Label htmlFor="custom-block-json-fallback" className="text-xs font-semibold">
|
|
39
|
+
Block Content (JSON)
|
|
40
|
+
</Label>
|
|
41
|
+
<Textarea
|
|
42
|
+
id="custom-block-json-fallback"
|
|
43
|
+
value={value}
|
|
44
|
+
spellCheck={false}
|
|
45
|
+
className="min-h-[320px] font-mono text-xs"
|
|
46
|
+
onChange={(e) => {
|
|
47
|
+
const next = e.target.value;
|
|
48
|
+
setValue(next);
|
|
49
|
+
try {
|
|
50
|
+
onChange(JSON.parse(next));
|
|
51
|
+
setError(null);
|
|
52
|
+
} catch (parseError) {
|
|
53
|
+
setError(parseError instanceof Error ? parseError.message : "Invalid JSON");
|
|
54
|
+
}
|
|
55
|
+
}}
|
|
56
|
+
/>
|
|
57
|
+
{error && <p className="text-xs text-destructive">{error}</p>}
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export default function DynamicCustomBlockEditor({
|
|
63
|
+
block,
|
|
64
|
+
content,
|
|
65
|
+
onChange,
|
|
66
|
+
}: BlockEditorProps<Record<string, any>>) {
|
|
67
|
+
const [definition, setDefinition] = useState<CustomBlockDefinitionResponse | null>(null);
|
|
68
|
+
const [loading, setLoading] = useState(true);
|
|
69
|
+
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
async function loadDefinition() {
|
|
72
|
+
try {
|
|
73
|
+
const response = await fetch("/api/custom-blocks/editor-definitions");
|
|
74
|
+
if (response.ok) {
|
|
75
|
+
const payload = await response.json();
|
|
76
|
+
const blockType = block.block_type || (block as any).type;
|
|
77
|
+
const def = payload.definitions?.find((d: any) => d.slug === blockType);
|
|
78
|
+
if (def) {
|
|
79
|
+
setDefinition(def);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
} catch (err) {
|
|
83
|
+
console.error("Failed to load custom block definition for editor:", err);
|
|
84
|
+
} finally {
|
|
85
|
+
setLoading(false);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
loadDefinition();
|
|
89
|
+
}, [block.block_type, (block as any).type]);
|
|
90
|
+
|
|
91
|
+
if (loading) {
|
|
92
|
+
return <div className="p-4 text-center text-sm text-muted-foreground">Loading custom block settings...</div>;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// No matching custom block definition (e.g. a non-custom block type routed
|
|
96
|
+
// here as a fallback). Degrade gracefully to a raw JSON editor instead of an
|
|
97
|
+
// error so content stays editable.
|
|
98
|
+
if (!definition) {
|
|
99
|
+
return <JsonFallbackEditor content={content} onChange={onChange} />;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const handleFieldChange = (key: string, val: any) => {
|
|
103
|
+
onChange({ ...content, [key]: val });
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<div className="space-y-4 p-4 border rounded-xl bg-card/50">
|
|
108
|
+
<div className="pb-2 border-b">
|
|
109
|
+
<h4 className="text-sm font-bold text-slate-800 dark:text-slate-200">{definition.name}</h4>
|
|
110
|
+
<p className="text-xs text-muted-foreground">Custom Block Properties</p>
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
{definition.fields.map((field) => {
|
|
114
|
+
const fieldVal = content[field.key];
|
|
115
|
+
const fieldId = `custom-block-editor-${definition.slug}-${field.key}`;
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<div key={field.key} className="space-y-1 pt-2">
|
|
119
|
+
<Label htmlFor={fieldId} className="text-xs font-semibold text-slate-700 dark:text-slate-300">
|
|
120
|
+
{field.label} {field.required && <span className="text-destructive">*</span>}
|
|
121
|
+
</Label>
|
|
122
|
+
|
|
123
|
+
{field.type === "rich-text" ? (
|
|
124
|
+
<Textarea
|
|
125
|
+
id={fieldId}
|
|
126
|
+
value={fieldVal || ""}
|
|
127
|
+
onChange={(e) => handleFieldChange(field.key, e.target.value)}
|
|
128
|
+
placeholder={field.placeholder}
|
|
129
|
+
className="min-h-24 text-sm"
|
|
130
|
+
/>
|
|
131
|
+
) : field.type === "image_r2" ? (
|
|
132
|
+
<ImageR2Picker
|
|
133
|
+
value={fieldVal && typeof fieldVal === "object" && "url" in fieldVal ? fieldVal : null}
|
|
134
|
+
onChange={(val) => handleFieldChange(field.key, val)}
|
|
135
|
+
accept={field.accept}
|
|
136
|
+
maxBytes={field.max_bytes}
|
|
137
|
+
/>
|
|
138
|
+
) : field.type === "db_relation" ? (
|
|
139
|
+
<DBRelationSelect
|
|
140
|
+
table={field.table}
|
|
141
|
+
value={
|
|
142
|
+
field.multiple
|
|
143
|
+
? Array.isArray(fieldVal) ? fieldVal.map(String) : []
|
|
144
|
+
: fieldVal ? String(fieldVal) : null
|
|
145
|
+
}
|
|
146
|
+
onChange={(val) => handleFieldChange(field.key, val)}
|
|
147
|
+
multiple={field.multiple}
|
|
148
|
+
displayColumn={field.display_column}
|
|
149
|
+
valueColumn={field.value_column}
|
|
150
|
+
filters={field.filters}
|
|
151
|
+
/>
|
|
152
|
+
) : (
|
|
153
|
+
<Input
|
|
154
|
+
id={fieldId}
|
|
155
|
+
type="text"
|
|
156
|
+
value={fieldVal || ""}
|
|
157
|
+
onChange={(e) => handleFieldChange(field.key, e.target.value)}
|
|
158
|
+
placeholder={field.placeholder}
|
|
159
|
+
className="h-9"
|
|
160
|
+
/>
|
|
161
|
+
)}
|
|
162
|
+
</div>
|
|
163
|
+
);
|
|
164
|
+
})}
|
|
165
|
+
</div>
|
|
166
|
+
);
|
|
167
|
+
}
|