@tinacms/app 2.5.4 → 2.5.6
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/CHANGELOG.md +21 -0
- package/package.json +11 -4
- package/src/lib/expand-query.ts +22 -3
- package/src/lib/graphql-reducer.ts +59 -18
- package/src/lib/preview-origin.test.ts +147 -0
- package/src/lib/preview-origin.ts +61 -0
- package/src/lib/util.ts +4 -3
- package/vitest.config.ts +10 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# @tinacms/app
|
|
2
2
|
|
|
3
|
+
## 2.5.6
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#7056](https://github.com/tinacms/tinacms/pull/7056) [`c491fc5`](https://github.com/tinacms/tinacms/commit/c491fc55e612725f5d775eeb1fdf3f8ba82314fa) Thanks [@Aibono1225](https://github.com/Aibono1225)! - Harden cross-window message handling and rich-text URL sanitization.
|
|
8
|
+
|
|
9
|
+
Adds stricter origin/source checks for trusted message flows, use explicit target origins for preview iframe message, and applies URL sanitization to slatejson rich-text parsing and default rich-text link/image rendering.
|
|
10
|
+
|
|
11
|
+
- Updated dependencies [[`42760d8`](https://github.com/tinacms/tinacms/commit/42760d8f5afd201107e27e274308af37f96ba8d0), [`c491fc5`](https://github.com/tinacms/tinacms/commit/c491fc55e612725f5d775eeb1fdf3f8ba82314fa)]:
|
|
12
|
+
- tinacms@3.9.3
|
|
13
|
+
- @tinacms/mdx@2.1.7
|
|
14
|
+
|
|
15
|
+
## 2.5.5
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- [#6681](https://github.com/tinacms/tinacms/pull/6681) [`95b7523`](https://github.com/tinacms/tinacms/commit/95b75237cb91ec3dc5dac9ce52359f9786072502) Thanks [@isaaclombardssw](https://github.com/isaaclombardssw)! - Fix click-to-edit for fields in connection query results (e.g. ruleConnection)
|
|
20
|
+
|
|
21
|
+
- Updated dependencies []:
|
|
22
|
+
- tinacms@3.9.2
|
|
23
|
+
|
|
3
24
|
## 2.5.4
|
|
4
25
|
|
|
5
26
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tinacms/app",
|
|
3
|
-
"version": "2.5.
|
|
3
|
+
"version": "2.5.6",
|
|
4
4
|
"main": "src/main.tsx",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"devDependencies": {
|
|
7
7
|
"@types/react": "^18.3.18",
|
|
8
8
|
"@types/react-dom": "^18.3.5",
|
|
9
|
-
"
|
|
9
|
+
"happy-dom": "15.10.2",
|
|
10
|
+
"typescript": "^5.7.3",
|
|
11
|
+
"vite": "^5.4.14",
|
|
12
|
+
"vitest": "^2.1.9"
|
|
10
13
|
},
|
|
11
14
|
"peerDependencies": {
|
|
12
15
|
"react": ">=18.3.1 <20.0.0",
|
|
@@ -26,11 +29,15 @@
|
|
|
26
29
|
"react-router-dom": "^6.30.3",
|
|
27
30
|
"typescript": "^5.7.3",
|
|
28
31
|
"zod": "^3.24.2",
|
|
29
|
-
"@tinacms/mdx": "2.1.
|
|
30
|
-
"tinacms": "3.9.
|
|
32
|
+
"@tinacms/mdx": "2.1.7",
|
|
33
|
+
"tinacms": "3.9.3"
|
|
31
34
|
},
|
|
32
35
|
"repository": {
|
|
33
36
|
"url": "https://github.com/tinacms/tinacms.git",
|
|
34
37
|
"directory": "packages/@tinacms/app"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"test": "vitest run",
|
|
41
|
+
"test-watch": "vitest"
|
|
35
42
|
}
|
|
36
43
|
}
|
package/src/lib/expand-query.ts
CHANGED
|
@@ -3,14 +3,17 @@ import * as G from 'graphql';
|
|
|
3
3
|
export const expandQuery = ({
|
|
4
4
|
schema,
|
|
5
5
|
documentNode,
|
|
6
|
+
includeNodeMetadata,
|
|
6
7
|
}: {
|
|
7
8
|
schema: G.GraphQLSchema;
|
|
8
9
|
documentNode: G.DocumentNode;
|
|
10
|
+
includeNodeMetadata?: boolean;
|
|
9
11
|
}): G.DocumentNode => {
|
|
10
12
|
const documentNodeWithTypenames = addTypenameToDocument(documentNode);
|
|
11
13
|
return addMetaFieldsToQuery(
|
|
12
14
|
documentNodeWithTypenames,
|
|
13
|
-
new G.TypeInfo(schema)
|
|
15
|
+
new G.TypeInfo(schema),
|
|
16
|
+
includeNodeMetadata
|
|
14
17
|
);
|
|
15
18
|
};
|
|
16
19
|
|
|
@@ -114,7 +117,8 @@ const addMetadataField = (
|
|
|
114
117
|
|
|
115
118
|
const addMetaFieldsToQuery = (
|
|
116
119
|
documentNode: G.DocumentNode,
|
|
117
|
-
typeInfo: G.TypeInfo
|
|
120
|
+
typeInfo: G.TypeInfo,
|
|
121
|
+
includeNodeMetadata?: boolean
|
|
118
122
|
) => {
|
|
119
123
|
const addMetaFields: G.VisitFn<G.ASTNode, G.FieldNode> = (
|
|
120
124
|
node: G.FieldNode
|
|
@@ -126,7 +130,11 @@ const addMetaFieldsToQuery = (
|
|
|
126
130
|
kind: 'SelectionSet',
|
|
127
131
|
selections: [],
|
|
128
132
|
}),
|
|
129
|
-
selections: [
|
|
133
|
+
selections: [
|
|
134
|
+
...(node.selectionSet?.selections || []),
|
|
135
|
+
...metaFields,
|
|
136
|
+
...(includeNodeMetadata ? nodeMetadataFields : []),
|
|
137
|
+
],
|
|
130
138
|
},
|
|
131
139
|
};
|
|
132
140
|
};
|
|
@@ -218,6 +226,17 @@ const metaFields: G.SelectionNode[] =
|
|
|
218
226
|
// @ts-ignore
|
|
219
227
|
node.definitions[0].selectionSet.selections;
|
|
220
228
|
|
|
229
|
+
const nodeMetadataFragment = G.parse(`
|
|
230
|
+
query Sample {
|
|
231
|
+
...on Document {
|
|
232
|
+
_tina_metadata
|
|
233
|
+
_content_source
|
|
234
|
+
}
|
|
235
|
+
}`);
|
|
236
|
+
const nodeMetadataFields: G.SelectionNode[] =
|
|
237
|
+
// @ts-ignore
|
|
238
|
+
nodeMetadataFragment.definitions[0].selectionSet.selections;
|
|
239
|
+
|
|
221
240
|
export const isNodeType = (type: G.GraphQLOutputType) => {
|
|
222
241
|
const namedType = G.getNamedType(type);
|
|
223
242
|
if (G.isInterfaceType(namedType)) {
|
|
@@ -24,6 +24,11 @@ import { z } from 'zod';
|
|
|
24
24
|
import { FormifyCallback, createForm, createGlobalForm } from './build-form';
|
|
25
25
|
import { showErrorModal } from './errors';
|
|
26
26
|
import { expandQuery, isConnectionType, isNodeType } from './expand-query';
|
|
27
|
+
import {
|
|
28
|
+
getExpectedPreviewOrigin,
|
|
29
|
+
isFromTrustedPreviewOrigin,
|
|
30
|
+
postMessageToPreview,
|
|
31
|
+
} from './preview-origin';
|
|
27
32
|
import type {
|
|
28
33
|
Payload,
|
|
29
34
|
PostMessage,
|
|
@@ -200,6 +205,13 @@ export const useGraphQLReducer = (
|
|
|
200
205
|
|
|
201
206
|
const activeField = searchParams.get('active-field');
|
|
202
207
|
|
|
208
|
+
// Origin of the preview document we load in the iframe. Used to validate
|
|
209
|
+
// inbound messages and as the explicit `targetOrigin` for outbound ones.
|
|
210
|
+
const expectedOrigin = React.useMemo(
|
|
211
|
+
() => getExpectedPreviewOrigin(url),
|
|
212
|
+
[url]
|
|
213
|
+
);
|
|
214
|
+
|
|
203
215
|
React.useEffect(() => {
|
|
204
216
|
const run = async () => {
|
|
205
217
|
return Promise.all(
|
|
@@ -483,11 +495,15 @@ export const useGraphQLReducer = (
|
|
|
483
495
|
});
|
|
484
496
|
}
|
|
485
497
|
}
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
498
|
+
postMessageToPreview(
|
|
499
|
+
iframe.current?.contentWindow,
|
|
500
|
+
{
|
|
501
|
+
type: 'updateData',
|
|
502
|
+
id: payload.id,
|
|
503
|
+
data: result.data,
|
|
504
|
+
},
|
|
505
|
+
expectedOrigin
|
|
506
|
+
);
|
|
491
507
|
}
|
|
492
508
|
cms.dispatch({
|
|
493
509
|
type: 'form-lists:add',
|
|
@@ -502,6 +518,7 @@ export const useGraphQLReducer = (
|
|
|
502
518
|
[
|
|
503
519
|
resolvedDocuments.map((doc) => doc._internalSys.path).join('.'),
|
|
504
520
|
activeField,
|
|
521
|
+
expectedOrigin,
|
|
505
522
|
]
|
|
506
523
|
);
|
|
507
524
|
|
|
@@ -527,6 +544,17 @@ export const useGraphQLReducer = (
|
|
|
527
544
|
|
|
528
545
|
const handleMessage = React.useCallback(
|
|
529
546
|
(event: MessageEvent<PostMessage>) => {
|
|
547
|
+
// Validate the sender before reading `event.data`: only the preview
|
|
548
|
+
// iframe we loaded is trusted to drive the reducer.
|
|
549
|
+
if (
|
|
550
|
+
!isFromTrustedPreviewOrigin({
|
|
551
|
+
event,
|
|
552
|
+
expectedOrigin,
|
|
553
|
+
peerWindow: iframe.current?.contentWindow,
|
|
554
|
+
})
|
|
555
|
+
) {
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
530
558
|
if (event.data.type === 'user-select-form') {
|
|
531
559
|
const incoming = event.data.formId;
|
|
532
560
|
// Buffer until `forms:add` resolves it if the form isn't built yet.
|
|
@@ -538,15 +566,23 @@ export const useGraphQLReducer = (
|
|
|
538
566
|
type: 'set-quick-editing-supported',
|
|
539
567
|
value: event.data.value,
|
|
540
568
|
});
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
569
|
+
postMessageToPreview(
|
|
570
|
+
iframe.current?.contentWindow,
|
|
571
|
+
{
|
|
572
|
+
type: 'quickEditEnabled',
|
|
573
|
+
value: cms.state.sidebarDisplayState === 'open',
|
|
574
|
+
},
|
|
575
|
+
expectedOrigin
|
|
576
|
+
);
|
|
545
577
|
}
|
|
546
578
|
if (event?.data?.type === 'isEditMode') {
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
579
|
+
postMessageToPreview(
|
|
580
|
+
iframe.current?.contentWindow,
|
|
581
|
+
{
|
|
582
|
+
type: 'tina:editMode',
|
|
583
|
+
},
|
|
584
|
+
expectedOrigin
|
|
585
|
+
);
|
|
550
586
|
}
|
|
551
587
|
if (event.data.type === 'field:selected') {
|
|
552
588
|
const [queryId, eventFieldName] = event.data.fieldName.split('---');
|
|
@@ -597,7 +633,7 @@ export const useGraphQLReducer = (
|
|
|
597
633
|
// });
|
|
598
634
|
// }
|
|
599
635
|
},
|
|
600
|
-
[cms, JSON.stringify(results)]
|
|
636
|
+
[cms, JSON.stringify(results), expectedOrigin]
|
|
601
637
|
);
|
|
602
638
|
|
|
603
639
|
React.useEffect(() => {
|
|
@@ -627,11 +663,15 @@ export const useGraphQLReducer = (
|
|
|
627
663
|
}, [cms.state.forms.length, pendingPrimaryId, activateFormByWireId]);
|
|
628
664
|
|
|
629
665
|
React.useEffect(() => {
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
666
|
+
postMessageToPreview(
|
|
667
|
+
iframe.current?.contentWindow,
|
|
668
|
+
{
|
|
669
|
+
type: 'quickEditEnabled',
|
|
670
|
+
value: cms.state.sidebarDisplayState === 'open',
|
|
671
|
+
},
|
|
672
|
+
expectedOrigin
|
|
673
|
+
);
|
|
674
|
+
}, [cms.state.sidebarDisplayState, expectedOrigin]);
|
|
635
675
|
|
|
636
676
|
React.useEffect(() => {
|
|
637
677
|
cms.dispatch({ type: 'set-edit-mode', value: 'visual' });
|
|
@@ -942,6 +982,7 @@ const expandPayload = async (
|
|
|
942
982
|
const expandedDocumentNodeForResolver = expandQuery({
|
|
943
983
|
schema: schemaForResolver,
|
|
944
984
|
documentNode,
|
|
985
|
+
includeNodeMetadata: true,
|
|
945
986
|
});
|
|
946
987
|
const expandedQueryForResolver = G.print(expandedDocumentNodeForResolver);
|
|
947
988
|
return { ...payload, expandedQuery, expandedData, expandedQueryForResolver };
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
getExpectedPreviewOrigin,
|
|
4
|
+
isFromTrustedPreviewOrigin,
|
|
5
|
+
postMessageToPreview,
|
|
6
|
+
} from './preview-origin';
|
|
7
|
+
|
|
8
|
+
const ADMIN = 'https://admin.example';
|
|
9
|
+
const PREVIEW = 'https://preview.example';
|
|
10
|
+
|
|
11
|
+
// A stand-in for the iframe's content window; we only need an identity to
|
|
12
|
+
// compare `event.source` against.
|
|
13
|
+
const makePeerWindow = () =>
|
|
14
|
+
({ postMessage: vi.fn() }) as unknown as Window & {
|
|
15
|
+
postMessage: ReturnType<typeof vi.fn>;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
describe('getExpectedPreviewOrigin', () => {
|
|
19
|
+
it('resolves a relative admin URL to the admin origin', () => {
|
|
20
|
+
expect(getExpectedPreviewOrigin('/posts/hello', ADMIN)).toBe(ADMIN);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('keeps the origin of an absolute preview URL', () => {
|
|
24
|
+
expect(getExpectedPreviewOrigin(`${PREVIEW}/posts/hello`, ADMIN)).toBe(
|
|
25
|
+
PREVIEW
|
|
26
|
+
);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('isFromTrustedPreviewOrigin', () => {
|
|
31
|
+
it('ignores messages from an untrusted origin', () => {
|
|
32
|
+
const peerWindow = makePeerWindow();
|
|
33
|
+
const event = {
|
|
34
|
+
origin: 'https://evil.example',
|
|
35
|
+
source: peerWindow,
|
|
36
|
+
data: { type: 'open' },
|
|
37
|
+
} as unknown as MessageEvent;
|
|
38
|
+
|
|
39
|
+
expect(
|
|
40
|
+
isFromTrustedPreviewOrigin({
|
|
41
|
+
event,
|
|
42
|
+
expectedOrigin: PREVIEW,
|
|
43
|
+
peerWindow,
|
|
44
|
+
})
|
|
45
|
+
).toBe(false);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('accepts messages from the trusted origin and peer window', () => {
|
|
49
|
+
const peerWindow = makePeerWindow();
|
|
50
|
+
const event = {
|
|
51
|
+
origin: PREVIEW,
|
|
52
|
+
source: peerWindow,
|
|
53
|
+
data: { type: 'open' },
|
|
54
|
+
} as unknown as MessageEvent;
|
|
55
|
+
|
|
56
|
+
expect(
|
|
57
|
+
isFromTrustedPreviewOrigin({
|
|
58
|
+
event,
|
|
59
|
+
expectedOrigin: PREVIEW,
|
|
60
|
+
peerWindow,
|
|
61
|
+
})
|
|
62
|
+
).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('ignores the trusted origin from the wrong source window', () => {
|
|
66
|
+
const peerWindow = makePeerWindow();
|
|
67
|
+
const otherFrame = makePeerWindow();
|
|
68
|
+
const event = {
|
|
69
|
+
origin: PREVIEW,
|
|
70
|
+
source: otherFrame,
|
|
71
|
+
data: { type: 'open' },
|
|
72
|
+
} as unknown as MessageEvent;
|
|
73
|
+
|
|
74
|
+
expect(
|
|
75
|
+
isFromTrustedPreviewOrigin({
|
|
76
|
+
event,
|
|
77
|
+
expectedOrigin: PREVIEW,
|
|
78
|
+
peerWindow,
|
|
79
|
+
})
|
|
80
|
+
).toBe(false);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('does not read event.data before validating the origin', () => {
|
|
84
|
+
const peerWindow = makePeerWindow();
|
|
85
|
+
const event = {
|
|
86
|
+
origin: 'https://evil.example',
|
|
87
|
+
source: peerWindow,
|
|
88
|
+
// Reading `data` would throw — proves the guard never touches the
|
|
89
|
+
// payload when the origin is untrusted.
|
|
90
|
+
get data(): unknown {
|
|
91
|
+
throw new Error('event.data must not be read before origin validation');
|
|
92
|
+
},
|
|
93
|
+
} as unknown as MessageEvent;
|
|
94
|
+
|
|
95
|
+
expect(() =>
|
|
96
|
+
isFromTrustedPreviewOrigin({
|
|
97
|
+
event,
|
|
98
|
+
expectedOrigin: PREVIEW,
|
|
99
|
+
peerWindow,
|
|
100
|
+
})
|
|
101
|
+
).not.toThrow();
|
|
102
|
+
expect(
|
|
103
|
+
isFromTrustedPreviewOrigin({
|
|
104
|
+
event,
|
|
105
|
+
expectedOrigin: PREVIEW,
|
|
106
|
+
peerWindow,
|
|
107
|
+
})
|
|
108
|
+
).toBe(false);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('falls back to origin-only when no peer window handle is available', () => {
|
|
112
|
+
const event = {
|
|
113
|
+
origin: PREVIEW,
|
|
114
|
+
source: makePeerWindow(),
|
|
115
|
+
data: { type: 'open' },
|
|
116
|
+
} as unknown as MessageEvent;
|
|
117
|
+
|
|
118
|
+
expect(
|
|
119
|
+
isFromTrustedPreviewOrigin({
|
|
120
|
+
event,
|
|
121
|
+
expectedOrigin: PREVIEW,
|
|
122
|
+
peerWindow: null,
|
|
123
|
+
})
|
|
124
|
+
).toBe(true);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe('postMessageToPreview', () => {
|
|
129
|
+
it('posts with the exact expected targetOrigin, not a wildcard', () => {
|
|
130
|
+
const peerWindow = makePeerWindow();
|
|
131
|
+
const message = { type: 'updateData', id: 'q1', data: {} };
|
|
132
|
+
|
|
133
|
+
postMessageToPreview(peerWindow, message, PREVIEW);
|
|
134
|
+
|
|
135
|
+
expect(peerWindow.postMessage).toHaveBeenCalledWith(message, PREVIEW);
|
|
136
|
+
expect(peerWindow.postMessage).not.toHaveBeenCalledWith(
|
|
137
|
+
expect.anything(),
|
|
138
|
+
'*'
|
|
139
|
+
);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('no-ops when there is no peer window', () => {
|
|
143
|
+
expect(() =>
|
|
144
|
+
postMessageToPreview(null, { type: 'updateData' }, PREVIEW)
|
|
145
|
+
).not.toThrow();
|
|
146
|
+
});
|
|
147
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilities for securing the admin <-> preview iframe postMessage channel.
|
|
3
|
+
*
|
|
4
|
+
* The trusted peer is the preview iframe loaded by the admin. Messages are only
|
|
5
|
+
* trusted when they match the expected preview origin and, when available, the
|
|
6
|
+
* iframe's exact `contentWindow`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Returns the origin expected for the preview iframe.
|
|
11
|
+
*
|
|
12
|
+
* Relative URLs resolve against `baseOrigin`; absolute URLs keep their own
|
|
13
|
+
* origin.
|
|
14
|
+
*/
|
|
15
|
+
export const getExpectedPreviewOrigin = (
|
|
16
|
+
url: string,
|
|
17
|
+
baseOrigin: string = typeof window !== 'undefined'
|
|
18
|
+
? window.location.origin
|
|
19
|
+
: ''
|
|
20
|
+
): string => {
|
|
21
|
+
try {
|
|
22
|
+
return new URL(url, baseOrigin || undefined).origin;
|
|
23
|
+
} catch {
|
|
24
|
+
return baseOrigin;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Checks whether a MessageEvent came from the trusted preview iframe.
|
|
30
|
+
*
|
|
31
|
+
* `event.data` is intentionally not read here, so callers can run this check
|
|
32
|
+
* before trusting the payload.
|
|
33
|
+
*/
|
|
34
|
+
export const isFromTrustedPreviewOrigin = ({
|
|
35
|
+
event,
|
|
36
|
+
expectedOrigin,
|
|
37
|
+
peerWindow,
|
|
38
|
+
}: {
|
|
39
|
+
event: MessageEvent;
|
|
40
|
+
expectedOrigin: string;
|
|
41
|
+
peerWindow: Window | null | undefined;
|
|
42
|
+
}): boolean => {
|
|
43
|
+
if (!expectedOrigin) return false;
|
|
44
|
+
if (event.origin !== expectedOrigin) return false;
|
|
45
|
+
if (peerWindow && event.source !== peerWindow) return false;
|
|
46
|
+
return true;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Sends a message to the preview iframe with a strict target origin.
|
|
51
|
+
*
|
|
52
|
+
* No-ops when the iframe window or origin is missing, and never uses `"*"`.
|
|
53
|
+
*/
|
|
54
|
+
export const postMessageToPreview = (
|
|
55
|
+
peerWindow: Window | null | undefined,
|
|
56
|
+
message: unknown,
|
|
57
|
+
expectedOrigin: string
|
|
58
|
+
): void => {
|
|
59
|
+
if (!peerWindow || !expectedOrigin) return;
|
|
60
|
+
peerWindow.postMessage(message, expectedOrigin);
|
|
61
|
+
};
|
package/src/lib/util.ts
CHANGED
|
@@ -93,9 +93,10 @@ export const getDeepestMetadata = (state: Object, complexKey: string): any => {
|
|
|
93
93
|
}
|
|
94
94
|
const value = current[key];
|
|
95
95
|
if (value?._tina_metadata) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if (complexKey === value._tina_metadata?.prefix && metadata) {
|
|
96
|
+
if (value._tina_metadata.id === null) {
|
|
97
|
+
// Placeholder metadata from connection/list types — skip it
|
|
98
|
+
} else if (complexKey === value._tina_metadata?.prefix && metadata) {
|
|
99
|
+
// Reference field — keep the parent form's metadata
|
|
99
100
|
} else {
|
|
100
101
|
metadata = value._tina_metadata;
|
|
101
102
|
}
|