@developer_tribe/react-builder 1.2.18 → 1.2.19
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/index.cjs.js +1 -1
- 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 +6 -6
- 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/build-components/PaywallProvider/PaywallProvider.tsx +2 -3
- package/src/build-components/Text/Text.tsx +4 -9
- 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 +24 -3
- 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/utils/__special_exceptions.ts +9 -3
- package/src/utils/applyJsonTransform.ts +19 -0
- package/src/utils/repairNodeKeys.ts +90 -0
- package/src/utils/safeJsonStringify.ts +18 -0
- package/src/utils/wrapNodeInMain.ts +67 -0
|
@@ -11,6 +11,8 @@ import { useLogRender } from '../utils/useLogRender';
|
|
|
11
11
|
import { getDefaultsForType, getPatternByType } from '../utils/patterns';
|
|
12
12
|
import { AddComponentModal } from '../modals/AddComponentModal';
|
|
13
13
|
import { BuilderButton } from './BuilderButton';
|
|
14
|
+
import { generateRandomKeyForNode } from '../utils/generateRandomKeyForNode';
|
|
15
|
+
import { collectNodeKeys } from '../utils/repairNodeKeys';
|
|
14
16
|
|
|
15
17
|
type BuilderEditorProps = {
|
|
16
18
|
data: Node;
|
|
@@ -156,6 +158,7 @@ export function Builder({
|
|
|
156
158
|
}: BuilderEditorProps) {
|
|
157
159
|
useLogRender('Builder');
|
|
158
160
|
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
|
|
161
|
+
const usedKeys = useMemo(() => collectNodeKeys(data), [data]);
|
|
159
162
|
const breadcrumbPath = useMemo(() => {
|
|
160
163
|
const path = findNodePath(data, current);
|
|
161
164
|
if (path.length) return path;
|
|
@@ -190,7 +193,8 @@ export function Builder({
|
|
|
190
193
|
|
|
191
194
|
const handleAddChild = useCallback(
|
|
192
195
|
(type: string) => {
|
|
193
|
-
const
|
|
196
|
+
const nextUsedKeys = new Set(usedKeys);
|
|
197
|
+
const nextChild = createDefaultNode(type, nextUsedKeys);
|
|
194
198
|
|
|
195
199
|
// Root (or selection) can be empty/null-ish: allow creating the first node.
|
|
196
200
|
if (isNodeNullOrUndefined(current)) {
|
|
@@ -247,7 +251,7 @@ export function Builder({
|
|
|
247
251
|
setData(updatedRoot);
|
|
248
252
|
setCurrent(updatedParent);
|
|
249
253
|
},
|
|
250
|
-
[current, data, setData, setCurrent],
|
|
254
|
+
[current, data, setData, setCurrent, usedKeys],
|
|
251
255
|
);
|
|
252
256
|
|
|
253
257
|
const allowedChildTypes = useMemo(
|
|
@@ -383,17 +387,26 @@ export function Builder({
|
|
|
383
387
|
return root;
|
|
384
388
|
}
|
|
385
389
|
|
|
386
|
-
function createDefaultNode(
|
|
390
|
+
function createDefaultNode(
|
|
391
|
+
type: string,
|
|
392
|
+
nextUsedKeys: Set<string>,
|
|
393
|
+
): NodeData<NodeDefaultAttribute> {
|
|
387
394
|
const pattern = getPatternByType(type)?.pattern;
|
|
388
395
|
const defaults = getDefaultsForType(type) ?? {};
|
|
389
396
|
const childrenSchema = pattern?.children as unknown;
|
|
397
|
+
let key = '';
|
|
398
|
+
do {
|
|
399
|
+
key = generateRandomKeyForNode(type);
|
|
400
|
+
} while (nextUsedKeys.has(key));
|
|
401
|
+
nextUsedKeys.add(key);
|
|
390
402
|
|
|
391
403
|
// Special-case: CarouselProvider MUST contain a Carousel container inside the viewport
|
|
392
404
|
// otherwise embla-carousel will crash (it expects viewport.firstChild.children).
|
|
393
405
|
if (type === 'CarouselProvider') {
|
|
394
406
|
return {
|
|
395
407
|
type,
|
|
396
|
-
|
|
408
|
+
key,
|
|
409
|
+
children: createDefaultNode('Carousel', nextUsedKeys),
|
|
397
410
|
attributes: { ...defaults },
|
|
398
411
|
} as NodeData<NodeDefaultAttribute>;
|
|
399
412
|
}
|
|
@@ -412,13 +425,14 @@ export function Builder({
|
|
|
412
425
|
children = null;
|
|
413
426
|
} else if (typeof childrenSchema === 'string') {
|
|
414
427
|
// Specific child type like 'CarouselItem' – seed with one child to match the pattern.
|
|
415
|
-
children = [createDefaultNode(childrenSchema)];
|
|
428
|
+
children = [createDefaultNode(childrenSchema, nextUsedKeys)];
|
|
416
429
|
} else {
|
|
417
430
|
children = null;
|
|
418
431
|
}
|
|
419
432
|
|
|
420
433
|
return {
|
|
421
434
|
type,
|
|
435
|
+
key,
|
|
422
436
|
children,
|
|
423
437
|
attributes: { ...defaults },
|
|
424
438
|
} as NodeData<NodeDefaultAttribute>;
|
|
@@ -3,6 +3,7 @@ import type { Device } from '../types/Device';
|
|
|
3
3
|
import type { Node } from '../types/Node';
|
|
4
4
|
import { copyNode } from '../utils/copyNode';
|
|
5
5
|
import { getDevices } from '../utils/getDevices';
|
|
6
|
+
import { collectNodeKeys, repairNodeKeys } from '../utils/repairNodeKeys';
|
|
6
7
|
import { useRenderStore } from '../store';
|
|
7
8
|
import { useLogRender } from '../utils/useLogRender';
|
|
8
9
|
import { DeviceButton } from './DeviceButton';
|
|
@@ -120,7 +121,9 @@ export function EditorHeader({
|
|
|
120
121
|
if (!current || !editorData || !setEditorData) return;
|
|
121
122
|
if (!copiedNode) return;
|
|
122
123
|
const cloned = JSON.parse(JSON.stringify(copiedNode)) as Node;
|
|
123
|
-
const
|
|
124
|
+
const usedKeys = collectNodeKeys(editorData);
|
|
125
|
+
const repaired = repairNodeKeys(cloned, usedKeys);
|
|
126
|
+
const updated = replaceNode(editorData, current, repaired);
|
|
124
127
|
useRenderStore.setState({
|
|
125
128
|
copiedNode: null,
|
|
126
129
|
});
|
|
@@ -129,7 +132,7 @@ export function EditorHeader({
|
|
|
129
132
|
// Important: selection is stored by reference. After replacing `current` in the tree,
|
|
130
133
|
// we must point selection to the new (cloned) node reference to keep "current node"
|
|
131
134
|
// in sync with what’s rendered/edited.
|
|
132
|
-
setCurrent(
|
|
135
|
+
setCurrent(repaired);
|
|
133
136
|
};
|
|
134
137
|
|
|
135
138
|
const cloneNode = (node: Node): Node =>
|
|
@@ -137,7 +140,7 @@ export function EditorHeader({
|
|
|
137
140
|
|
|
138
141
|
const handleReplaceFromSample = (sample: Project) => {
|
|
139
142
|
if (!setEditorData) return;
|
|
140
|
-
const next = cloneNode(sample.data);
|
|
143
|
+
const next = repairNodeKeys(cloneNode(sample.data));
|
|
141
144
|
setEditorData(next);
|
|
142
145
|
setCurrent(next);
|
|
143
146
|
if (sample.appConfig) setAppConfig(sample.appConfig);
|
|
@@ -148,6 +151,7 @@ export function EditorHeader({
|
|
|
148
151
|
const handlePasteFromSample = (sample: Project) => {
|
|
149
152
|
if (!current || !editorData || !setEditorData) return;
|
|
150
153
|
const incoming = cloneNode(sample.data);
|
|
154
|
+
const usedKeys = collectNodeKeys(editorData);
|
|
151
155
|
|
|
152
156
|
const isRecord = (v: unknown): v is Record<string, unknown> =>
|
|
153
157
|
typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
@@ -181,13 +185,19 @@ export function EditorHeader({
|
|
|
181
185
|
toast.error('Sample has no children to paste');
|
|
182
186
|
return;
|
|
183
187
|
}
|
|
188
|
+
const repairedPasteNodes = pasteNodes.map((node) =>
|
|
189
|
+
repairNodeKeys(node, usedKeys),
|
|
190
|
+
);
|
|
184
191
|
let nextChildren: Node;
|
|
185
192
|
if (!prevChildren) {
|
|
186
|
-
nextChildren =
|
|
193
|
+
nextChildren =
|
|
194
|
+
repairedPasteNodes.length === 1
|
|
195
|
+
? repairedPasteNodes[0]
|
|
196
|
+
: repairedPasteNodes;
|
|
187
197
|
} else if (Array.isArray(prevChildren)) {
|
|
188
|
-
nextChildren = [...prevChildren, ...
|
|
198
|
+
nextChildren = [...prevChildren, ...repairedPasteNodes];
|
|
189
199
|
} else {
|
|
190
|
-
nextChildren = [prevChildren, ...
|
|
200
|
+
nextChildren = [prevChildren, ...repairedPasteNodes];
|
|
191
201
|
}
|
|
192
202
|
|
|
193
203
|
const nextNode: Node = { ...current, children: nextChildren } as Node;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import React, { useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import type { Node } from '../types/Node';
|
|
3
|
+
import { wrapNodeInMain } from '../utils/wrapNodeInMain';
|
|
2
4
|
|
|
3
5
|
type JsonTextEditorProps = {
|
|
4
6
|
value: unknown;
|
|
@@ -74,6 +76,34 @@ export function JsonTextEditor({
|
|
|
74
76
|
}
|
|
75
77
|
};
|
|
76
78
|
|
|
79
|
+
const handleWrapInMain = () => {
|
|
80
|
+
try {
|
|
81
|
+
const parsed = JSON.parse(text) as unknown;
|
|
82
|
+
const isRecord = (v: unknown): v is Record<string, unknown> =>
|
|
83
|
+
typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
84
|
+
|
|
85
|
+
// Support both:
|
|
86
|
+
// - node JSON (wrap root)
|
|
87
|
+
// - project JSON (wrap `data`)
|
|
88
|
+
const nextValue =
|
|
89
|
+
isRecord(parsed) && 'data' in parsed
|
|
90
|
+
? {
|
|
91
|
+
...(parsed as any),
|
|
92
|
+
data: wrapNodeInMain((parsed as any).data as Node),
|
|
93
|
+
}
|
|
94
|
+
: wrapNodeInMain(parsed as Node);
|
|
95
|
+
|
|
96
|
+
setText(JSON.stringify(nextValue, null, 2));
|
|
97
|
+
setParseError(null);
|
|
98
|
+
setApplyError(null);
|
|
99
|
+
setParsedValue(nextValue);
|
|
100
|
+
// Intentionally NOT calling onChange here:
|
|
101
|
+
// user can review diff and press "Apply" explicitly.
|
|
102
|
+
} catch (e) {
|
|
103
|
+
setParseError(e instanceof Error ? e.message : 'Invalid JSON');
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
77
107
|
const headerLabel = rootName ? `${rootName}.json` : 'data.json';
|
|
78
108
|
|
|
79
109
|
return (
|
|
@@ -108,6 +138,17 @@ export function JsonTextEditor({
|
|
|
108
138
|
>
|
|
109
139
|
Format
|
|
110
140
|
</button>
|
|
141
|
+
{!readOnly && (
|
|
142
|
+
<button
|
|
143
|
+
type="button"
|
|
144
|
+
className="editor-button"
|
|
145
|
+
onClick={handleWrapInMain}
|
|
146
|
+
disabled={!onChange}
|
|
147
|
+
title={onChange ? 'Wrap root in Main and apply' : 'Read only'}
|
|
148
|
+
>
|
|
149
|
+
Wrap in Main
|
|
150
|
+
</button>
|
|
151
|
+
)}
|
|
111
152
|
{!readOnly && (
|
|
112
153
|
<button
|
|
113
154
|
type="button"
|
|
@@ -5,6 +5,7 @@ import { Checkbox } from '../components/Checkbox';
|
|
|
5
5
|
import { JsonTextEditor } from '../components/JsonTextEditor';
|
|
6
6
|
import { analyseAndProccess } from '../utils/analyseNode';
|
|
7
7
|
import { logRenderStore } from '../utils/logRenderStore';
|
|
8
|
+
import { useRenderStore } from '../store';
|
|
8
9
|
|
|
9
10
|
export type DebugJsonPageProps = {
|
|
10
11
|
data: Node | null | undefined;
|
|
@@ -36,6 +37,7 @@ export function DebugJsonPage({
|
|
|
36
37
|
setAppConfig,
|
|
37
38
|
logLabel,
|
|
38
39
|
}: DebugJsonPageProps) {
|
|
40
|
+
const setCurrent = useRenderStore((s) => s.setCurrent);
|
|
39
41
|
const canTogglePreviewMode = typeof setPreviewMode === 'function';
|
|
40
42
|
const canToggleTheme =
|
|
41
43
|
typeof setAppConfig === 'function' && typeof appConfig?.theme === 'string';
|
|
@@ -43,6 +45,19 @@ export function DebugJsonPage({
|
|
|
43
45
|
typeof setAppConfig === 'function' &&
|
|
44
46
|
typeof (appConfig as any)?.isRtl !== 'undefined';
|
|
45
47
|
|
|
48
|
+
const isRecord = (v: unknown): v is Record<string, unknown> =>
|
|
49
|
+
typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
50
|
+
|
|
51
|
+
const extractNode = (value: unknown): Node => {
|
|
52
|
+
// Allow both:
|
|
53
|
+
// - raw Node JSON
|
|
54
|
+
// - Project wrapper JSON { name, version, data: Node }
|
|
55
|
+
if (isRecord(value) && 'data' in value) {
|
|
56
|
+
return (value as any).data as Node;
|
|
57
|
+
}
|
|
58
|
+
return value as Node;
|
|
59
|
+
};
|
|
60
|
+
|
|
46
61
|
return (
|
|
47
62
|
<>
|
|
48
63
|
<div className="modal__header localication-modal__header">
|
|
@@ -123,9 +138,15 @@ export function DebugJsonPage({
|
|
|
123
138
|
<JsonTextEditor
|
|
124
139
|
rootName="node"
|
|
125
140
|
value={data ?? ({} as any)}
|
|
126
|
-
onChange={(next) =>
|
|
127
|
-
|
|
128
|
-
|
|
141
|
+
onChange={(next) => {
|
|
142
|
+
const nodeCandidate = extractNode(next);
|
|
143
|
+
const processed = analyseAndProccess(
|
|
144
|
+
nodeCandidate as Node,
|
|
145
|
+
) as Node;
|
|
146
|
+
setData(processed);
|
|
147
|
+
// Keep selection in sync with the new root.
|
|
148
|
+
setCurrent(processed);
|
|
149
|
+
}}
|
|
129
150
|
className="localication-modal__json-editor"
|
|
130
151
|
/>
|
|
131
152
|
</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"
|