@developer_tribe/react-builder 1.2.18 → 1.2.20
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/dist/build-components/OnboardDot/OnboardDotProps.generated.d.ts +2 -1
- package/dist/build-components/patterns.generated.d.ts +23 -8
- package/dist/index.cjs.js +3 -3
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.esm.js +1 -1
- package/dist/index.esm.js.map +1 -1
- package/dist/index.web.cjs.js +4 -4
- package/dist/index.web.cjs.js.map +1 -1
- package/dist/index.web.esm.js +3 -3
- package/dist/index.web.esm.js.map +1 -1
- package/dist/pages/ProjectDebug.d.ts +9 -1
- package/dist/pages/ProjectMigrationPage.d.ts +3 -1
- package/dist/pages/ProjectValidationPage.d.ts +3 -2
- package/dist/styles.css +1 -1
- package/dist/utils/applyJsonTransform.d.ts +13 -0
- package/dist/utils/repairNodeKeys.d.ts +11 -0
- package/dist/utils/safeJsonStringify.d.ts +1 -0
- package/dist/utils/wrapNodeInMain.d.ts +2 -0
- package/package.json +1 -1
- package/src/RenderPage.tsx +17 -46
- package/src/assets/meta.json +1 -1
- package/src/assets/samples/carousel-sample.json +51 -51
- package/src/assets/samples/paywall-1.json +77 -77
- package/src/assets/samples/paywall-2.json +76 -76
- package/src/assets/samples/simple-1.json +13 -13
- package/src/assets/samples/simple-2.json +97 -97
- package/src/assets/samples/unmigrated-builder-1.1.1.json +25 -25
- package/src/assets/samples/unmigrated-builder1.json +1 -1
- package/src/assets/samples/unvalidated-builder1.json +15 -15
- package/src/assets/samples/unvalidated-crash1.json +4 -4
- package/src/assets/samples/vpn-onboard-1.json +100 -78
- package/src/assets/samples/vpn-onboard-2.json +97 -75
- package/src/assets/samples/vpn-onboard-3.json +103 -79
- package/src/assets/samples/vpn-onboard-4.json +103 -79
- package/src/assets/samples/vpn-onboard-5.json +139 -108
- package/src/assets/samples/vpn-onboard-6.json +100 -81
- package/src/build-components/CarouselDots/CarouselDots.tsx +112 -12
- package/src/build-components/OnboardDot/OnboardDot.tsx +74 -40
- package/src/build-components/OnboardDot/OnboardDotProps.generated.ts +2 -1
- package/src/build-components/OnboardDot/pattern.json +28 -10
- package/src/build-components/PaywallProvider/PaywallProvider.tsx +2 -3
- package/src/build-components/Text/Text.tsx +4 -9
- package/src/build-components/patterns.generated.ts +23 -8
- package/src/build-components/useNode.ts +20 -4
- package/src/components/AttributesEditorPanel.tsx +13 -1
- package/src/components/Builder.tsx +19 -5
- package/src/components/EditorHeader.tsx +16 -6
- package/src/components/JsonTextEditor.tsx +41 -0
- package/src/pages/DebugJsonPage.tsx +104 -4
- package/src/pages/ProjectDebug.tsx +66 -28
- package/src/pages/ProjectMigrationPage.tsx +15 -0
- package/src/pages/ProjectPage.tsx +160 -23
- package/src/pages/ProjectValidationPage.tsx +64 -1
- package/src/styles/layout/_project-validation.scss +29 -0
- package/src/styles/utilities/_carousel.scss +0 -32
- package/src/utils/__special_exceptions.ts +9 -3
- package/src/utils/analyseNodeByPatterns.ts +16 -6
- package/src/utils/applyJsonTransform.ts +19 -0
- package/src/utils/novaToJson.ts +7 -3
- package/src/utils/repairNodeKeys.ts +90 -0
- package/src/utils/safeJsonStringify.ts +18 -0
- package/src/utils/wrapNodeInMain.ts +67 -0
|
@@ -1,10 +1,17 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
2
|
import type { Node } from '../types/Node';
|
|
3
|
+
import type { NodeData, NodeDefaultAttribute } from '../types/Node';
|
|
3
4
|
import type { AppConfig } from '../types/PreviewConfig';
|
|
4
5
|
import { Checkbox } from '../components/Checkbox';
|
|
5
6
|
import { JsonTextEditor } from '../components/JsonTextEditor';
|
|
6
7
|
import { analyseAndProccess } from '../utils/analyseNode';
|
|
7
8
|
import { logRenderStore } from '../utils/logRenderStore';
|
|
9
|
+
import { useRenderStore } from '../store';
|
|
10
|
+
import {
|
|
11
|
+
isNodeArray,
|
|
12
|
+
isNodeNullOrUndefined,
|
|
13
|
+
isNodeString,
|
|
14
|
+
} from '../utils/nodeGuards';
|
|
8
15
|
|
|
9
16
|
export type DebugJsonPageProps = {
|
|
10
17
|
data: Node | null | undefined;
|
|
@@ -36,6 +43,7 @@ export function DebugJsonPage({
|
|
|
36
43
|
setAppConfig,
|
|
37
44
|
logLabel,
|
|
38
45
|
}: DebugJsonPageProps) {
|
|
46
|
+
const setCurrent = useRenderStore((s) => s.setCurrent);
|
|
39
47
|
const canTogglePreviewMode = typeof setPreviewMode === 'function';
|
|
40
48
|
const canToggleTheme =
|
|
41
49
|
typeof setAppConfig === 'function' && typeof appConfig?.theme === 'string';
|
|
@@ -43,6 +51,81 @@ export function DebugJsonPage({
|
|
|
43
51
|
typeof setAppConfig === 'function' &&
|
|
44
52
|
typeof (appConfig as any)?.isRtl !== 'undefined';
|
|
45
53
|
|
|
54
|
+
const isRecord = (v: unknown): v is Record<string, unknown> =>
|
|
55
|
+
typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
56
|
+
|
|
57
|
+
const extractNode = (value: unknown): Node => {
|
|
58
|
+
// Allow both:
|
|
59
|
+
// - raw Node JSON
|
|
60
|
+
// - Project wrapper JSON { name, version, data: Node }
|
|
61
|
+
if (isRecord(value) && 'data' in value) {
|
|
62
|
+
return (value as any).data as Node;
|
|
63
|
+
}
|
|
64
|
+
return value as Node;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
68
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function migrateStyleToStyles(node: Node): Node {
|
|
72
|
+
if (isNodeNullOrUndefined(node) || isNodeString(node)) {
|
|
73
|
+
return node;
|
|
74
|
+
}
|
|
75
|
+
if (isNodeArray(node)) {
|
|
76
|
+
const arr = node as Node[];
|
|
77
|
+
return arr.map((n) => migrateStyleToStyles(n));
|
|
78
|
+
}
|
|
79
|
+
if (!isPlainObject(node)) {
|
|
80
|
+
return node;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const record = node as unknown as NodeData<NodeDefaultAttribute>;
|
|
84
|
+
const nextChildren = migrateStyleToStyles(record.children);
|
|
85
|
+
|
|
86
|
+
if (!isPlainObject(record.attributes)) {
|
|
87
|
+
return { ...record, children: nextChildren };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const attrs = { ...record.attributes };
|
|
91
|
+
if ('style' in attrs && isPlainObject(attrs.style)) {
|
|
92
|
+
attrs.styles = attrs.style;
|
|
93
|
+
delete attrs.style;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
...record,
|
|
98
|
+
children: nextChildren,
|
|
99
|
+
attributes: attrs,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const hasStyleAttribute = useMemo(() => {
|
|
104
|
+
function checkNode(n: Node): boolean {
|
|
105
|
+
if (isNodeNullOrUndefined(n) || isNodeString(n)) return false;
|
|
106
|
+
if (isNodeArray(n)) {
|
|
107
|
+
const arr = n as Node[];
|
|
108
|
+
return arr.some(checkNode);
|
|
109
|
+
}
|
|
110
|
+
if (!isPlainObject(n)) return false;
|
|
111
|
+
const record = n as unknown as NodeData<NodeDefaultAttribute>;
|
|
112
|
+
if (isPlainObject(record.attributes) && 'style' in record.attributes) {
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
return checkNode(record.children);
|
|
116
|
+
}
|
|
117
|
+
return checkNode(data);
|
|
118
|
+
}, [data]);
|
|
119
|
+
|
|
120
|
+
const handleFixStyleToStyles = () => {
|
|
121
|
+
if (!data) return;
|
|
122
|
+
const migrated = migrateStyleToStyles(data);
|
|
123
|
+
// Don't call analyseAndProccess here - let the user apply changes manually
|
|
124
|
+
// This avoids validation errors for unknown attributes that may exist
|
|
125
|
+
setData(migrated as Node);
|
|
126
|
+
setCurrent(migrated as Node);
|
|
127
|
+
};
|
|
128
|
+
|
|
46
129
|
return (
|
|
47
130
|
<>
|
|
48
131
|
<div className="modal__header localication-modal__header">
|
|
@@ -72,6 +155,17 @@ export function DebugJsonPage({
|
|
|
72
155
|
Log store
|
|
73
156
|
</button>
|
|
74
157
|
|
|
158
|
+
{hasStyleAttribute ? (
|
|
159
|
+
<button
|
|
160
|
+
type="button"
|
|
161
|
+
className="editor-button"
|
|
162
|
+
title="Migrate attributes.style to attributes.styles (schemaVersion=2)"
|
|
163
|
+
onClick={handleFixStyleToStyles}
|
|
164
|
+
>
|
|
165
|
+
Fix style → styles
|
|
166
|
+
</button>
|
|
167
|
+
) : null}
|
|
168
|
+
|
|
75
169
|
{onClose ? (
|
|
76
170
|
<button
|
|
77
171
|
type="button"
|
|
@@ -123,9 +217,15 @@ export function DebugJsonPage({
|
|
|
123
217
|
<JsonTextEditor
|
|
124
218
|
rootName="node"
|
|
125
219
|
value={data ?? ({} as any)}
|
|
126
|
-
onChange={(next) =>
|
|
127
|
-
|
|
128
|
-
|
|
220
|
+
onChange={(next) => {
|
|
221
|
+
const nodeCandidate = extractNode(next);
|
|
222
|
+
const processed = analyseAndProccess(
|
|
223
|
+
nodeCandidate as Node,
|
|
224
|
+
) as Node;
|
|
225
|
+
setData(processed);
|
|
226
|
+
// Keep selection in sync with the new root.
|
|
227
|
+
setCurrent(processed);
|
|
228
|
+
}}
|
|
129
229
|
className="localication-modal__json-editor"
|
|
130
230
|
/>
|
|
131
231
|
</div>
|
|
@@ -5,25 +5,7 @@ import type { Node } from '../types/Node';
|
|
|
5
5
|
import type { Product } from '../paywall/types/paywall-types';
|
|
6
6
|
import type { PaywallBenefits } from '../paywall/types/benefits';
|
|
7
7
|
import { useSyncHtmlThemeClass } from '../hooks/useSyncHtmlThemeClass';
|
|
8
|
-
|
|
9
|
-
function safeStringify(value: unknown): string {
|
|
10
|
-
try {
|
|
11
|
-
const seen = new WeakSet<object>();
|
|
12
|
-
return JSON.stringify(
|
|
13
|
-
value,
|
|
14
|
-
(_key, v) => {
|
|
15
|
-
if (typeof v === 'object' && v !== null) {
|
|
16
|
-
if (seen.has(v as object)) return '[Circular]';
|
|
17
|
-
seen.add(v as object);
|
|
18
|
-
}
|
|
19
|
-
return v;
|
|
20
|
-
},
|
|
21
|
-
2,
|
|
22
|
-
);
|
|
23
|
-
} catch (e) {
|
|
24
|
-
return `<< Unable to stringify value: ${String(e)} >>`;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
8
|
+
import { safeJsonStringify } from '../utils/safeJsonStringify';
|
|
27
9
|
|
|
28
10
|
async function copyTextToClipboard(text: string): Promise<boolean> {
|
|
29
11
|
try {
|
|
@@ -68,6 +50,24 @@ function isObject(value: unknown): value is Record<string, unknown> {
|
|
|
68
50
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
69
51
|
}
|
|
70
52
|
|
|
53
|
+
function extractNodeFromRawData(rawData: unknown): unknown {
|
|
54
|
+
// Support both:
|
|
55
|
+
// - raw node JSON (root has `type`)
|
|
56
|
+
// - full project JSON (root has `data`)
|
|
57
|
+
if (isObject(rawData)) {
|
|
58
|
+
if (typeof rawData.type === 'string' && rawData.type.trim()) return rawData;
|
|
59
|
+
const maybeData = rawData.data;
|
|
60
|
+
if (
|
|
61
|
+
isObject(maybeData) &&
|
|
62
|
+
typeof maybeData.type === 'string' &&
|
|
63
|
+
maybeData.type.trim()
|
|
64
|
+
) {
|
|
65
|
+
return maybeData;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return rawData;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
71
|
function getNodeType(value: unknown): string | null {
|
|
72
72
|
if (!isObject(value)) return null;
|
|
73
73
|
const t = value.type;
|
|
@@ -132,6 +132,14 @@ export type ProjectDebugProps = {
|
|
|
132
132
|
benefits: PaywallBenefits;
|
|
133
133
|
canvasBg?: string;
|
|
134
134
|
belowName?: ReactNode;
|
|
135
|
+
jsonEditor?: {
|
|
136
|
+
value: string;
|
|
137
|
+
onChange: (next: string) => void;
|
|
138
|
+
error?: string | null;
|
|
139
|
+
onSave?: () => void;
|
|
140
|
+
saveDisabled?: boolean;
|
|
141
|
+
saveLabel?: string;
|
|
142
|
+
};
|
|
135
143
|
};
|
|
136
144
|
|
|
137
145
|
export function ProjectDebug({
|
|
@@ -143,6 +151,7 @@ export function ProjectDebug({
|
|
|
143
151
|
benefits,
|
|
144
152
|
canvasBg,
|
|
145
153
|
belowName,
|
|
154
|
+
jsonEditor,
|
|
146
155
|
}: ProjectDebugProps) {
|
|
147
156
|
useSyncHtmlThemeClass();
|
|
148
157
|
const [previewError, setPreviewError] = useState<{
|
|
@@ -155,16 +164,18 @@ export function ProjectDebug({
|
|
|
155
164
|
'idle',
|
|
156
165
|
);
|
|
157
166
|
|
|
158
|
-
const json = useMemo(() =>
|
|
159
|
-
const
|
|
160
|
-
const
|
|
167
|
+
const json = useMemo(() => safeJsonStringify(rawData), [rawData]);
|
|
168
|
+
const jsonToCopy = jsonEditor ? jsonEditor.value : json;
|
|
169
|
+
const nodeRoot = useMemo(() => extractNodeFromRawData(rawData), [rawData]);
|
|
170
|
+
const previewData = nodeRoot as Node;
|
|
171
|
+
const rootType = useMemo(() => getNodeType(nodeRoot), [nodeRoot]);
|
|
161
172
|
const parsedValidation = useMemo(
|
|
162
173
|
() => parseValidationPrefix(validationError),
|
|
163
174
|
[validationError],
|
|
164
175
|
);
|
|
165
176
|
const validationContext = useMemo(
|
|
166
|
-
() => resolveNodeTypeAtPath(
|
|
167
|
-
[
|
|
177
|
+
() => resolveNodeTypeAtPath(nodeRoot, parsedValidation.path),
|
|
178
|
+
[nodeRoot, parsedValidation.path],
|
|
168
179
|
);
|
|
169
180
|
|
|
170
181
|
const validationSummary = useMemo(() => {
|
|
@@ -241,7 +252,7 @@ export function ProjectDebug({
|
|
|
241
252
|
type="button"
|
|
242
253
|
className="editor-button"
|
|
243
254
|
onClick={async () => {
|
|
244
|
-
const ok = await copyTextToClipboard(
|
|
255
|
+
const ok = await copyTextToClipboard(jsonToCopy);
|
|
245
256
|
if (!ok) return;
|
|
246
257
|
setJsonCopyState('copied');
|
|
247
258
|
window.setTimeout(() => setJsonCopyState('idle'), 900);
|
|
@@ -249,13 +260,40 @@ export function ProjectDebug({
|
|
|
249
260
|
>
|
|
250
261
|
{jsonCopyState === 'copied' ? 'Copied JSON' : 'Copy JSON'}
|
|
251
262
|
</button>
|
|
263
|
+
{jsonEditor?.onSave && (
|
|
264
|
+
<button
|
|
265
|
+
type="button"
|
|
266
|
+
className="editor-button"
|
|
267
|
+
disabled={jsonEditor.saveDisabled}
|
|
268
|
+
onClick={() => jsonEditor.onSave?.()}
|
|
269
|
+
>
|
|
270
|
+
{jsonEditor.saveLabel ?? 'Save'}
|
|
271
|
+
</button>
|
|
272
|
+
)}
|
|
252
273
|
</div>
|
|
253
274
|
{belowName && (
|
|
254
275
|
<div className="rb-project-debug__below-name">{belowName}</div>
|
|
255
276
|
)}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
277
|
+
{jsonEditor ? (
|
|
278
|
+
<>
|
|
279
|
+
<textarea
|
|
280
|
+
className="rb-project-debug__code-editor"
|
|
281
|
+
value={jsonEditor.value}
|
|
282
|
+
onChange={(e) => jsonEditor.onChange(e.target.value)}
|
|
283
|
+
spellCheck={false}
|
|
284
|
+
aria-label="Edit JSON"
|
|
285
|
+
/>
|
|
286
|
+
{jsonEditor.error ? (
|
|
287
|
+
<div className="rb-project-debug__json-error" role="alert">
|
|
288
|
+
{jsonEditor.error}
|
|
289
|
+
</div>
|
|
290
|
+
) : null}
|
|
291
|
+
</>
|
|
292
|
+
) : (
|
|
293
|
+
<pre className="rb-project-debug__code" tabIndex={0}>
|
|
294
|
+
{json}
|
|
295
|
+
</pre>
|
|
296
|
+
)}
|
|
259
297
|
</section>
|
|
260
298
|
|
|
261
299
|
<section
|
|
@@ -9,6 +9,7 @@ export type ProjectMigrationPageProps = {
|
|
|
9
9
|
|
|
10
10
|
projectVersion: string;
|
|
11
11
|
requiredVersion: string;
|
|
12
|
+
showFixVersionMeta?: boolean;
|
|
12
13
|
|
|
13
14
|
pendingMigrations: Array<{
|
|
14
15
|
id: string;
|
|
@@ -25,6 +26,7 @@ export type ProjectMigrationPageProps = {
|
|
|
25
26
|
migrating?: boolean;
|
|
26
27
|
onContinueWithoutValidation?: () => void;
|
|
27
28
|
onMigrateNow?: () => void;
|
|
29
|
+
onFixVersionMeta?: () => void;
|
|
28
30
|
};
|
|
29
31
|
|
|
30
32
|
export function ProjectMigrationPage({
|
|
@@ -32,6 +34,7 @@ export function ProjectMigrationPage({
|
|
|
32
34
|
rawData,
|
|
33
35
|
projectVersion,
|
|
34
36
|
requiredVersion,
|
|
37
|
+
showFixVersionMeta,
|
|
35
38
|
pendingMigrations,
|
|
36
39
|
products,
|
|
37
40
|
benefits,
|
|
@@ -40,6 +43,7 @@ export function ProjectMigrationPage({
|
|
|
40
43
|
migrating,
|
|
41
44
|
onContinueWithoutValidation,
|
|
42
45
|
onMigrateNow,
|
|
46
|
+
onFixVersionMeta,
|
|
43
47
|
}: ProjectMigrationPageProps) {
|
|
44
48
|
const message = `Migration required: project version ${projectVersion} is lower than ${requiredVersion}.`;
|
|
45
49
|
|
|
@@ -78,6 +82,17 @@ export function ProjectMigrationPage({
|
|
|
78
82
|
{migrating ? 'Migrating…' : 'Migrate now'}
|
|
79
83
|
</button>
|
|
80
84
|
|
|
85
|
+
{showFixVersionMeta && (
|
|
86
|
+
<button
|
|
87
|
+
type="button"
|
|
88
|
+
className="editor-button"
|
|
89
|
+
disabled={!!migrating}
|
|
90
|
+
onClick={() => onFixVersionMeta?.()}
|
|
91
|
+
>
|
|
92
|
+
Fix version meta
|
|
93
|
+
</button>
|
|
94
|
+
)}
|
|
95
|
+
|
|
81
96
|
<button
|
|
82
97
|
type="button"
|
|
83
98
|
className="editor-button"
|
|
@@ -36,6 +36,8 @@ import {
|
|
|
36
36
|
import type { Fonts } from '../types/Fonts';
|
|
37
37
|
import { useProjectFonts } from '../hooks/useProjectFonts';
|
|
38
38
|
import { resolveProjectForSave } from './projectPageUtils';
|
|
39
|
+
import { getDefaultProject } from '../utils/getDefaultProject';
|
|
40
|
+
import { CURRENT_PROJECT_VERSION } from '../migrations/migratePipe';
|
|
39
41
|
export type ProjectPageProps = {
|
|
40
42
|
project: Project;
|
|
41
43
|
onSaveProject: (project: Project) => void;
|
|
@@ -75,10 +77,6 @@ export function ProjectPage({
|
|
|
75
77
|
typography.fonts.find((f) => f?.isMain)?.name ??
|
|
76
78
|
typography.fonts[0]?.name;
|
|
77
79
|
useProjectFonts({ fonts: typography.fonts, appFont: resolvedAppFont });
|
|
78
|
-
const resolvedName = name ?? project.name;
|
|
79
|
-
const resolvedProjectColors = projectColors ?? project.projectColors;
|
|
80
|
-
const isEmptyProjectData =
|
|
81
|
-
isNodeNullOrUndefined(project.data) || isEmptyObject(project.data);
|
|
82
80
|
// useRenderStore will be removed
|
|
83
81
|
const {
|
|
84
82
|
current,
|
|
@@ -103,10 +101,15 @@ export function ProjectPage({
|
|
|
103
101
|
}));
|
|
104
102
|
const resolvedAppConfig = appConfig ?? storeAppConfig ?? defaultAppConfig;
|
|
105
103
|
const [overrideProject, setOverrideProject] = useState<Project | null>(null);
|
|
104
|
+
const activeProject = overrideProject ?? project;
|
|
105
|
+
const resolvedName = name ?? activeProject.name;
|
|
106
|
+
const resolvedProjectColors = projectColors ?? activeProject.projectColors;
|
|
107
|
+
const isEmptyProjectData =
|
|
108
|
+
isNodeNullOrUndefined(activeProject.data) ||
|
|
109
|
+
isEmptyObject(activeProject.data);
|
|
106
110
|
const [editorData, setEditorData] = useState<Node>(() => {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
return analyseAndProccess({ type: 'Main', children: [] }) as Node;
|
|
111
|
+
// Empty project: keep data null-ish, show empty state.
|
|
112
|
+
return null;
|
|
110
113
|
});
|
|
111
114
|
const [validationError, setValidationError] = useState<string | null>(null);
|
|
112
115
|
const [validationErrorStack, setValidationErrorStack] = useState<
|
|
@@ -221,7 +224,7 @@ export function ProjectPage({
|
|
|
221
224
|
setMinLoadingDelayDone(false);
|
|
222
225
|
const timer = setTimeout(() => setMinLoadingDelayDone(true), 1000);
|
|
223
226
|
return () => clearTimeout(timer);
|
|
224
|
-
}, [
|
|
227
|
+
}, [activeProject.data]);
|
|
225
228
|
|
|
226
229
|
useEffect(() => {
|
|
227
230
|
try {
|
|
@@ -234,7 +237,7 @@ export function ProjectPage({
|
|
|
234
237
|
setValidationError(null);
|
|
235
238
|
setValidationErrorStack(null);
|
|
236
239
|
// Version gate: if project is older than the current schema, show migration UI.
|
|
237
|
-
const pipe = getMigrationPipe(
|
|
240
|
+
const pipe = getMigrationPipe(activeProject);
|
|
238
241
|
if (!bypassValidation && pipe.required) {
|
|
239
242
|
setMigrationGate(pipe);
|
|
240
243
|
setEditorData(null);
|
|
@@ -245,13 +248,17 @@ export function ProjectPage({
|
|
|
245
248
|
if (bypassValidation) {
|
|
246
249
|
// Best-effort: let the user continue with the raw data even if invalid.
|
|
247
250
|
// This may still crash the preview, but it unblocks users for debugging.
|
|
248
|
-
setEditorData(
|
|
249
|
-
setCurrent(
|
|
251
|
+
setEditorData(activeProject.data as unknown as Node);
|
|
252
|
+
setCurrent(activeProject.data as unknown as Node);
|
|
250
253
|
return;
|
|
251
254
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
+
if (isEmptyProjectData) {
|
|
256
|
+
setEditorData(null);
|
|
257
|
+
setCurrent(null);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const inputNode: Node = activeProject.data as Node;
|
|
255
262
|
|
|
256
263
|
const processed = analyseAndProccess(inputNode);
|
|
257
264
|
if (!processed) return;
|
|
@@ -268,7 +275,7 @@ export function ProjectPage({
|
|
|
268
275
|
setEditorData(null);
|
|
269
276
|
setCurrent(null);
|
|
270
277
|
}
|
|
271
|
-
}, [
|
|
278
|
+
}, [activeProject, activeProject.data, bypassValidation, setCurrent]);
|
|
272
279
|
|
|
273
280
|
const showLoading =
|
|
274
281
|
!isEmptyProjectData && (editorData === null || !minLoadingDelayDone);
|
|
@@ -322,9 +329,10 @@ export function ProjectPage({
|
|
|
322
329
|
{migrationGate ? (
|
|
323
330
|
<ProjectMigrationPage
|
|
324
331
|
name={resolvedName}
|
|
325
|
-
rawData={
|
|
332
|
+
rawData={activeProject}
|
|
326
333
|
projectVersion={migrationGate.projectVersion}
|
|
327
334
|
requiredVersion={migrationGate.requiredVersion}
|
|
335
|
+
showFixVersionMeta={migrationGate.projectVersion === '0.0.0'}
|
|
328
336
|
pendingMigrations={migrationGate.pending.map((m) => ({
|
|
329
337
|
id: m.id,
|
|
330
338
|
title: m.title,
|
|
@@ -338,15 +346,37 @@ export function ProjectPage({
|
|
|
338
346
|
onContinueWithoutValidation={() => {
|
|
339
347
|
setBypassValidation(true);
|
|
340
348
|
setMigrationGate(null);
|
|
341
|
-
setEditorData(
|
|
342
|
-
setCurrent(
|
|
349
|
+
setEditorData(activeProject.data as unknown as Node);
|
|
350
|
+
setCurrent(activeProject.data as unknown as Node);
|
|
343
351
|
setMinLoadingDelayDone(true);
|
|
344
352
|
}}
|
|
345
353
|
onMigrateNow={() => {
|
|
346
354
|
try {
|
|
347
355
|
setIsMigrating(true);
|
|
356
|
+
const isRecord = (v: unknown): v is Record<string, unknown> =>
|
|
357
|
+
typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
358
|
+
const isNodeLike = (v: unknown): v is { type: string } =>
|
|
359
|
+
isRecord(v) &&
|
|
360
|
+
typeof v.type === 'string' &&
|
|
361
|
+
v.type.trim().length > 0;
|
|
362
|
+
|
|
363
|
+
// If the incoming "project" is actually a raw Node (no version),
|
|
364
|
+
// wrap it into a valid Project shape before migrating/saving.
|
|
365
|
+
const projectForMigration: Project =
|
|
366
|
+
isNodeLike(activeProject) &&
|
|
367
|
+
!(
|
|
368
|
+
isRecord(activeProject) &&
|
|
369
|
+
typeof (activeProject as any).version === 'string'
|
|
370
|
+
)
|
|
371
|
+
? getDefaultProject({
|
|
372
|
+
name: `imported-${Math.random().toString(36).slice(2, 8)}`,
|
|
373
|
+
version: CURRENT_PROJECT_VERSION,
|
|
374
|
+
data: activeProject as unknown as Node,
|
|
375
|
+
})
|
|
376
|
+
: activeProject;
|
|
377
|
+
|
|
348
378
|
const { project: migratedProject } =
|
|
349
|
-
runProjectMigrations(
|
|
379
|
+
runProjectMigrations(projectForMigration);
|
|
350
380
|
onSaveProject(migratedProject);
|
|
351
381
|
setOverrideProject(migratedProject);
|
|
352
382
|
setBypassValidation(true);
|
|
@@ -358,11 +388,61 @@ export function ProjectPage({
|
|
|
358
388
|
setIsMigrating(false);
|
|
359
389
|
}
|
|
360
390
|
}}
|
|
391
|
+
onFixVersionMeta={() => {
|
|
392
|
+
const isRecord = (v: unknown): v is Record<string, unknown> =>
|
|
393
|
+
typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
394
|
+
const isNodeLike = (v: unknown): v is { type: string } =>
|
|
395
|
+
isRecord(v) &&
|
|
396
|
+
typeof v.type === 'string' &&
|
|
397
|
+
v.type.trim().length > 0;
|
|
398
|
+
const isAllowedProjectType = (
|
|
399
|
+
v: unknown,
|
|
400
|
+
): v is 'paywall' | 'onboard' | 'other' =>
|
|
401
|
+
v === 'paywall' || v === 'onboard' || v === 'other';
|
|
402
|
+
|
|
403
|
+
const fixedName =
|
|
404
|
+
typeof (activeProject as any)?.name === 'string' &&
|
|
405
|
+
String((activeProject as any).name).trim()
|
|
406
|
+
? String((activeProject as any).name).trim()
|
|
407
|
+
: `imported-${Math.random().toString(36).slice(2, 8)}`;
|
|
408
|
+
|
|
409
|
+
const activeAny = (
|
|
410
|
+
isRecord(activeProject) ? activeProject : null
|
|
411
|
+
) as Record<string, unknown> | null;
|
|
412
|
+
const nodeCandidate = (
|
|
413
|
+
activeAny && 'data' in activeAny
|
|
414
|
+
? (activeAny as any).data
|
|
415
|
+
: isNodeLike(activeProject)
|
|
416
|
+
? (activeProject as unknown as Node)
|
|
417
|
+
: null
|
|
418
|
+
) as Node | null;
|
|
419
|
+
|
|
420
|
+
const fixedProject = getDefaultProject({
|
|
421
|
+
name: fixedName,
|
|
422
|
+
version: CURRENT_PROJECT_VERSION,
|
|
423
|
+
data: nodeCandidate,
|
|
424
|
+
appConfig: (activeAny as any)?.appConfig,
|
|
425
|
+
projectColors: (activeAny as any)?.projectColors,
|
|
426
|
+
type: isAllowedProjectType((activeAny as any)?.type)
|
|
427
|
+
? ((activeAny as any).type as any)
|
|
428
|
+
: undefined,
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
// This action only fixes project metadata. It intentionally does NOT
|
|
432
|
+
// validate/normalize node data (it might still be invalid).
|
|
433
|
+
onSaveProject(fixedProject);
|
|
434
|
+
setOverrideProject(fixedProject);
|
|
435
|
+
setBypassValidation(false);
|
|
436
|
+
setMigrationGate(null);
|
|
437
|
+
setValidationError(null);
|
|
438
|
+
setValidationErrorStack(null);
|
|
439
|
+
toast.success('Fixed version meta');
|
|
440
|
+
}}
|
|
361
441
|
/>
|
|
362
442
|
) : validationError ? (
|
|
363
443
|
<ProjectValidationPage
|
|
364
444
|
name={resolvedName}
|
|
365
|
-
rawData={
|
|
445
|
+
rawData={activeProject}
|
|
366
446
|
validationError={validationError}
|
|
367
447
|
validationErrorStack={validationErrorStack ?? undefined}
|
|
368
448
|
products={products}
|
|
@@ -372,10 +452,67 @@ export function ProjectPage({
|
|
|
372
452
|
setBypassValidation(true);
|
|
373
453
|
setValidationError(null);
|
|
374
454
|
setValidationErrorStack(null);
|
|
375
|
-
setEditorData(
|
|
376
|
-
setCurrent(
|
|
455
|
+
setEditorData(activeProject.data as unknown as Node);
|
|
456
|
+
setCurrent(activeProject.data as unknown as Node);
|
|
377
457
|
setMinLoadingDelayDone(true);
|
|
378
458
|
}}
|
|
459
|
+
onSaveEditedRawData={(nextRawData) => {
|
|
460
|
+
try {
|
|
461
|
+
const isRecord = (v: unknown): v is Record<string, unknown> =>
|
|
462
|
+
typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
463
|
+
|
|
464
|
+
// Accept both:
|
|
465
|
+
// - a raw Node (what the app uses internally)
|
|
466
|
+
// - a full Project-like wrapper { name, version, data: Node }
|
|
467
|
+
const parsed = nextRawData;
|
|
468
|
+
let nodeCandidate: unknown = parsed;
|
|
469
|
+
let nextName: string | undefined;
|
|
470
|
+
let nextVersion: string | undefined;
|
|
471
|
+
|
|
472
|
+
if (isRecord(parsed) && 'data' in parsed) {
|
|
473
|
+
nodeCandidate = (parsed as Record<string, unknown>).data;
|
|
474
|
+
const maybeName = (parsed as Record<string, unknown>).name;
|
|
475
|
+
const maybeVersion = (parsed as Record<string, unknown>)
|
|
476
|
+
.version;
|
|
477
|
+
if (typeof maybeName === 'string') nextName = maybeName;
|
|
478
|
+
if (typeof maybeVersion === 'string')
|
|
479
|
+
nextVersion = maybeVersion;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const processed = analyseAndProccess(nodeCandidate as Node);
|
|
483
|
+
if (!processed) throw new Error('Node is not valid');
|
|
484
|
+
|
|
485
|
+
const nextProject: Project = {
|
|
486
|
+
...activeProject,
|
|
487
|
+
...(nextName ? { name: nextName } : null),
|
|
488
|
+
...(nextVersion ? { version: nextVersion } : null),
|
|
489
|
+
data: nodeCandidate as Project['data'],
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
onSaveProject(nextProject);
|
|
493
|
+
setOverrideProject(nextProject);
|
|
494
|
+
setBypassValidation(false);
|
|
495
|
+
setValidationError(null);
|
|
496
|
+
setValidationErrorStack(null);
|
|
497
|
+
setEditorData(processed);
|
|
498
|
+
setCurrent(processed);
|
|
499
|
+
setMinLoadingDelayDone(true);
|
|
500
|
+
toast.success('Saved');
|
|
501
|
+
} catch (e) {
|
|
502
|
+
logger.error(
|
|
503
|
+
'ProjectPage',
|
|
504
|
+
'save JSON from validation failed',
|
|
505
|
+
e,
|
|
506
|
+
);
|
|
507
|
+
setValidationError(
|
|
508
|
+
e instanceof Error ? e.message : 'Node is not valid',
|
|
509
|
+
);
|
|
510
|
+
setValidationErrorStack(
|
|
511
|
+
e instanceof Error ? (e.stack ?? null) : null,
|
|
512
|
+
);
|
|
513
|
+
toast.error('Save failed');
|
|
514
|
+
}
|
|
515
|
+
}}
|
|
379
516
|
/>
|
|
380
517
|
) : (
|
|
381
518
|
<>
|
|
@@ -477,7 +614,7 @@ export function ProjectPage({
|
|
|
477
614
|
</div>
|
|
478
615
|
)}
|
|
479
616
|
{/* NOTE: In React Native, `products` should come from an IAP wrapper (e.g. `react-native-iap`). */}
|
|
480
|
-
{!showLoading &&
|
|
617
|
+
{!showLoading && (
|
|
481
618
|
<RenderPage
|
|
482
619
|
data={editorData}
|
|
483
620
|
name={resolvedName}
|