slice-machine-ui 2.16.2-beta.9 → 2.17.1-alpha.jp-cr-ui-nested-ct-remove-section.1
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/out/404.html +1 -1
- package/out/_next/static/0SPBnBOIdcPusg2ceVdAq/_buildManifest.js +1 -0
- package/out/_next/static/chunks/248-03446cd9e9f13730.js +1 -0
- package/out/_next/static/chunks/268-6a9214b97195af9c.js +1 -0
- package/out/_next/static/chunks/33641354.3864aefb6106ae71.js +28 -0
- package/out/_next/static/chunks/34-e684c5fd75cc9dd0.js +1 -0
- package/out/_next/static/chunks/630-4a8f19a6a113700f.js +1 -0
- package/out/_next/static/chunks/647-7b9b5aa9468f9e4b.js +1 -0
- package/out/_next/static/chunks/882-151468121d542ed6.js +1 -0
- package/out/_next/static/chunks/pages/_app-15912b10c760cd68.js +708 -0
- package/out/_next/static/chunks/pages/changelog-063c5e11dfc8fd55.js +1 -0
- package/out/_next/static/chunks/pages/changes-564336edb0ed18b0.js +1 -0
- package/out/_next/static/chunks/pages/custom-types/{[customTypeId]-389d1b7a492fb3e7.js → [customTypeId]-a408f5a660e096a6.js} +1 -1
- package/out/_next/static/chunks/pages/{custom-types-2a5fd94ee42ba593.js → custom-types-5acd56959b60346f.js} +1 -1
- package/out/_next/static/chunks/pages/{index-02dd147957c8b40f.js → index-0d8cb369de720a35.js} +1 -1
- package/out/_next/static/chunks/pages/labs-9630bfb1005be02b.js +1 -0
- package/out/_next/static/chunks/pages/page-types/{[pageTypeId]-3589bd1f9138a97b.js → [pageTypeId]-f5e851ebe35049a8.js} +1 -1
- package/out/_next/static/chunks/pages/settings-01f4aeb9112a1f87.js +1 -0
- package/out/_next/static/chunks/pages/slices/[lib]/[sliceName]/[variation]/simulator-5008e29008aa04f4.js +1 -0
- package/out/_next/static/chunks/pages/slices/[lib]/[sliceName]/[variation]-bd5e45632c419567.js +1 -0
- package/out/_next/static/chunks/pages/slices-4a60cd5f2c71327e.js +1 -0
- package/out/_next/static/chunks/webpack-b3522fdebabf510a.js +1 -0
- package/out/_next/static/css/cc9b10286400c2b9.css +1 -0
- package/out/changelog.html +1 -1
- package/out/changes.html +1 -1
- package/out/custom-types/[customTypeId].html +1 -1
- package/out/custom-types.html +1 -1
- package/out/index.html +1 -1
- package/out/labs.html +1 -1
- package/out/page-types/[pageTypeId].html +1 -1
- package/out/settings.html +1 -1
- package/out/slices/[lib]/[sliceName]/[variation]/simulator.html +1 -1
- package/out/slices/[lib]/[sliceName]/[variation].html +1 -1
- package/out/slices.html +1 -1
- package/package.json +8 -8
- package/src/features/builder/fields/contentRelationship/ContentRelationshipFieldPicker.tsx +1498 -0
- package/src/features/builder/fields/contentRelationship/Hint.tsx +28 -0
- package/src/features/builder/fields/contentRelationship/__tests__/ContentRelationshipFieldPicker.test.ts +1323 -0
- package/src/features/customTypes/customTypesTable/CustomTypesTable.tsx +2 -4
- package/src/features/customTypes/customTypesTable/{useCustomTypes.ts → useCustomTypesAutoRevalidation.tsx} +1 -34
- package/src/features/customTypes/useCustomTypes.ts +48 -0
- package/src/legacy/lib/builders/common/Zone/Card/components/Hints/index.tsx +12 -1
- package/src/legacy/lib/models/common/widgets/ContentRelationship/Form.tsx +35 -77
- package/src/legacy/lib/models/common/widgets/ContentRelationship/index.ts +46 -15
- package/src/legacy/lib/models/common/widgets/Link/index.ts +1 -1
- package/src/utils/isValidObject.ts +32 -0
- package/src/utils/tracking/getLinkTrackingProperties.ts +28 -0
- package/src/utils/tracking/trackFieldAdded.ts +2 -5
- package/src/utils/tracking/trackFieldUpdated.ts +2 -5
- package/out/_next/static/atP3j_itxjBtvIysQoCxK/_buildManifest.js +0 -1
- package/out/_next/static/chunks/268-6d5fc7642a1b87c8.js +0 -1
- package/out/_next/static/chunks/34-2911c905c8a6e9d9.js +0 -1
- package/out/_next/static/chunks/630-93339694ef30b82d.js +0 -1
- package/out/_next/static/chunks/867-8164160c810122c6.js +0 -1
- package/out/_next/static/chunks/882-28837678beff7e51.js +0 -1
- package/out/_next/static/chunks/895-8c214ba470a4e23c.js +0 -1
- package/out/_next/static/chunks/pages/_app-2400eb8f7c5ef3cb.js +0 -657
- package/out/_next/static/chunks/pages/changelog-80a618708f44f25f.js +0 -1
- package/out/_next/static/chunks/pages/changes-d40c17939854b984.js +0 -1
- package/out/_next/static/chunks/pages/labs-c6df252ea5d8fb6f.js +0 -1
- package/out/_next/static/chunks/pages/settings-170379902605f38a.js +0 -1
- package/out/_next/static/chunks/pages/slices/[lib]/[sliceName]/[variation]/simulator-063fa88ba75f483e.js +0 -1
- package/out/_next/static/chunks/pages/slices/[lib]/[sliceName]/[variation]-6de02b8ed13b680d.js +0 -1
- package/out/_next/static/chunks/pages/slices-071bb494907adf0f.js +0 -1
- package/out/_next/static/chunks/webpack-2f903acb0cccbf9e.js +0 -1
- package/out/_next/static/css/9c9a7de81f9ac811.css +0 -1
- /package/out/_next/static/{atP3j_itxjBtvIysQoCxK → 0SPBnBOIdcPusg2ceVdAq}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,1498 @@
|
|
|
1
|
+
import { pluralize } from "@prismicio/editor-support/String";
|
|
2
|
+
import {
|
|
3
|
+
Alert,
|
|
4
|
+
AnimatedSuspense,
|
|
5
|
+
Badge,
|
|
6
|
+
Box,
|
|
7
|
+
Button,
|
|
8
|
+
DropdownMenu,
|
|
9
|
+
DropdownMenuContent,
|
|
10
|
+
DropdownMenuItem,
|
|
11
|
+
DropdownMenuLabel,
|
|
12
|
+
DropdownMenuTrigger,
|
|
13
|
+
Icon,
|
|
14
|
+
IconButton,
|
|
15
|
+
Skeleton,
|
|
16
|
+
Text,
|
|
17
|
+
TextOverflow,
|
|
18
|
+
Tooltip,
|
|
19
|
+
TreeView,
|
|
20
|
+
TreeViewCheckbox,
|
|
21
|
+
TreeViewSection,
|
|
22
|
+
} from "@prismicio/editor-ui";
|
|
23
|
+
import {
|
|
24
|
+
CustomType,
|
|
25
|
+
DynamicWidget,
|
|
26
|
+
Group,
|
|
27
|
+
Link,
|
|
28
|
+
LinkConfig,
|
|
29
|
+
NestableWidget,
|
|
30
|
+
} from "@prismicio/types-internal/lib/customtypes";
|
|
31
|
+
import { useEffect } from "react";
|
|
32
|
+
|
|
33
|
+
import { ErrorBoundary } from "@/ErrorBoundary";
|
|
34
|
+
import {
|
|
35
|
+
revalidateGetCustomTypes,
|
|
36
|
+
useCustomTypes as useCustomTypesRequest,
|
|
37
|
+
} from "@/features/customTypes/useCustomTypes";
|
|
38
|
+
import { isValidObject } from "@/utils/isValidObject";
|
|
39
|
+
|
|
40
|
+
type NonReadonly<T> = { -readonly [P in keyof T]: T[P] };
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Picker fields check map types. Used internally to keep track of the checked
|
|
44
|
+
* fields in the TreeView, as it's easier to handle objects than arrays and
|
|
45
|
+
* also ensures field uniqueness.
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* {
|
|
49
|
+
* author: {
|
|
50
|
+
* fullName: {
|
|
51
|
+
* type: "checkbox",
|
|
52
|
+
* value: true,
|
|
53
|
+
* },
|
|
54
|
+
* awards: {
|
|
55
|
+
* type: "group",
|
|
56
|
+
* value: {
|
|
57
|
+
* date: {
|
|
58
|
+
* type: "checkbox",
|
|
59
|
+
* value: true,
|
|
60
|
+
* },
|
|
61
|
+
* awardsCr: {
|
|
62
|
+
* type: "contentRelationship",
|
|
63
|
+
* value: {
|
|
64
|
+
* award: {
|
|
65
|
+
* title: {
|
|
66
|
+
* type: "checkbox",
|
|
67
|
+
* value: true,
|
|
68
|
+
* },
|
|
69
|
+
* issuer: {
|
|
70
|
+
* type: "group",
|
|
71
|
+
* value: {
|
|
72
|
+
* name: {
|
|
73
|
+
* type: "checkbox",
|
|
74
|
+
* value: true,
|
|
75
|
+
* },
|
|
76
|
+
* },
|
|
77
|
+
* },
|
|
78
|
+
* },
|
|
79
|
+
* },
|
|
80
|
+
* },
|
|
81
|
+
* },
|
|
82
|
+
* },
|
|
83
|
+
* professionCr: {
|
|
84
|
+
* type: "contentRelationship",
|
|
85
|
+
* value: {
|
|
86
|
+
* profession: {
|
|
87
|
+
* name: {
|
|
88
|
+
* type: "checkbox",
|
|
89
|
+
* value: true,
|
|
90
|
+
* },
|
|
91
|
+
* areas: {
|
|
92
|
+
* type: "group",
|
|
93
|
+
* value: {
|
|
94
|
+
* name: {
|
|
95
|
+
* type: "checkbox",
|
|
96
|
+
* value: true,
|
|
97
|
+
* },
|
|
98
|
+
* },
|
|
99
|
+
* },
|
|
100
|
+
* },
|
|
101
|
+
* },
|
|
102
|
+
* },
|
|
103
|
+
* },
|
|
104
|
+
* }
|
|
105
|
+
**/
|
|
106
|
+
interface PickerCustomTypes {
|
|
107
|
+
[customTypeId: string]: PickerCustomType;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
interface PickerCustomType {
|
|
111
|
+
[fieldId: string]: PickerCustomTypeValue;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
type PickerCustomTypeValue =
|
|
115
|
+
| PickerCheckboxField
|
|
116
|
+
| PickerFirstLevelGroupField
|
|
117
|
+
| PickerContentRelationshipField;
|
|
118
|
+
|
|
119
|
+
interface PickerCheckboxField {
|
|
120
|
+
type: "checkbox";
|
|
121
|
+
value: boolean;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
interface PickerFirstLevelGroupField {
|
|
125
|
+
type: "group";
|
|
126
|
+
value: PickerFirstLevelGroupFieldValue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
interface PickerLeafGroupField {
|
|
130
|
+
type: "group";
|
|
131
|
+
value: PickerLeafGroupFieldValue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
interface PickerLeafGroupFieldValue {
|
|
135
|
+
[fieldId: string]: PickerCheckboxField;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
interface PickerFirstLevelGroupFieldValue {
|
|
139
|
+
[fieldId: string]: PickerCheckboxField | PickerContentRelationshipField;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
interface PickerContentRelationshipField {
|
|
143
|
+
type: "contentRelationship";
|
|
144
|
+
value: PickerContentRelationshipFieldValue;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
interface PickerContentRelationshipFieldValue {
|
|
148
|
+
[customTypeId: string]: PickerNestedCustomTypeValue;
|
|
149
|
+
}
|
|
150
|
+
interface PickerNestedCustomTypeValue {
|
|
151
|
+
[fieldId: string]: PickerCheckboxField | PickerLeafGroupField;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Content relationship Link customtypes property structure.
|
|
156
|
+
*
|
|
157
|
+
* @example
|
|
158
|
+
* [
|
|
159
|
+
* {
|
|
160
|
+
* id: "author",
|
|
161
|
+
* fields: [
|
|
162
|
+
* "fullName",
|
|
163
|
+
* {
|
|
164
|
+
* id: "awards",
|
|
165
|
+
* fields: [
|
|
166
|
+
* "date",
|
|
167
|
+
* {
|
|
168
|
+
* id: "awardsCr",
|
|
169
|
+
* customtypes: [
|
|
170
|
+
* {
|
|
171
|
+
* id: "award",
|
|
172
|
+
* fields: [
|
|
173
|
+
* "title",
|
|
174
|
+
* {
|
|
175
|
+
* id: "issuer",
|
|
176
|
+
* fields: ["name"],
|
|
177
|
+
* },
|
|
178
|
+
* ],
|
|
179
|
+
* },
|
|
180
|
+
* ],
|
|
181
|
+
* },
|
|
182
|
+
* ],
|
|
183
|
+
* },
|
|
184
|
+
* {
|
|
185
|
+
* id: "professionCr",
|
|
186
|
+
* customtypes: [
|
|
187
|
+
* {
|
|
188
|
+
* id: "profession",
|
|
189
|
+
* fields: [
|
|
190
|
+
* "name",
|
|
191
|
+
* {
|
|
192
|
+
* id: "areas",
|
|
193
|
+
* fields: ["name"],
|
|
194
|
+
* },
|
|
195
|
+
* ],
|
|
196
|
+
* },
|
|
197
|
+
* ],
|
|
198
|
+
* },
|
|
199
|
+
* ],
|
|
200
|
+
* },
|
|
201
|
+
* ]
|
|
202
|
+
*/
|
|
203
|
+
type LinkCustomtypes = NonNullable<LinkConfig["customtypes"]>;
|
|
204
|
+
|
|
205
|
+
type LinkCustomtypesFields = Exclude<
|
|
206
|
+
LinkCustomtypes[number],
|
|
207
|
+
string
|
|
208
|
+
>["fields"][number];
|
|
209
|
+
|
|
210
|
+
type LinkCustomtypesContentRelationshipFieldValue = Exclude<
|
|
211
|
+
LinkCustomtypesFields,
|
|
212
|
+
string | { fields: unknown }
|
|
213
|
+
>;
|
|
214
|
+
|
|
215
|
+
type LinkCustomtypesGroupFieldValue = Exclude<
|
|
216
|
+
LinkCustomtypesFields,
|
|
217
|
+
string | { customtypes: unknown }
|
|
218
|
+
>;
|
|
219
|
+
|
|
220
|
+
interface ContentRelationshipFieldPickerProps {
|
|
221
|
+
value: LinkCustomtypes | undefined;
|
|
222
|
+
onChange: (fields: LinkCustomtypes) => void;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function ContentRelationshipFieldPicker(
|
|
226
|
+
props: ContentRelationshipFieldPickerProps,
|
|
227
|
+
) {
|
|
228
|
+
return (
|
|
229
|
+
<ErrorBoundary
|
|
230
|
+
renderError={() => (
|
|
231
|
+
<Box alignItems="center" gap={8}>
|
|
232
|
+
<Icon name="alert" size="small" color="tomato10" />
|
|
233
|
+
<Text color="tomato10">Error loading your types</Text>
|
|
234
|
+
</Box>
|
|
235
|
+
)}
|
|
236
|
+
>
|
|
237
|
+
<AnimatedSuspense
|
|
238
|
+
fallback={
|
|
239
|
+
<Box flexDirection="column" position="relative">
|
|
240
|
+
<Skeleton height={240} />
|
|
241
|
+
<Box
|
|
242
|
+
position="absolute"
|
|
243
|
+
top="50%"
|
|
244
|
+
left="50%"
|
|
245
|
+
transform="translate(-50%, -50%)"
|
|
246
|
+
alignItems="center"
|
|
247
|
+
gap={8}
|
|
248
|
+
>
|
|
249
|
+
<Icon name="autorenew" size="small" color="grey11" />
|
|
250
|
+
<Text color="grey11">Loading your types...</Text>
|
|
251
|
+
</Box>
|
|
252
|
+
</Box>
|
|
253
|
+
}
|
|
254
|
+
>
|
|
255
|
+
<ContentRelationshipFieldPickerContent {...props} />
|
|
256
|
+
</AnimatedSuspense>
|
|
257
|
+
</ErrorBoundary>
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function ContentRelationshipFieldPickerContent(
|
|
262
|
+
props: ContentRelationshipFieldPickerProps,
|
|
263
|
+
) {
|
|
264
|
+
const { value: linkCustomtypes, onChange } = props;
|
|
265
|
+
const { allCustomTypes, pickedCustomTypes } = useCustomTypes(linkCustomtypes);
|
|
266
|
+
|
|
267
|
+
const fieldCheckMap = linkCustomtypes
|
|
268
|
+
? convertLinkCustomtypesToFieldCheckMap({ linkCustomtypes, allCustomTypes })
|
|
269
|
+
: {};
|
|
270
|
+
|
|
271
|
+
function onCustomTypesChange(id: string, newCustomType: PickerCustomType) {
|
|
272
|
+
// The picker does not handle strings (custom type ids), as it's only meant
|
|
273
|
+
// to pick fields from custom types (objects). So we need to merge it with
|
|
274
|
+
// the existing value, which can have strings in the first level that
|
|
275
|
+
// represent new types added without any picked fields.
|
|
276
|
+
onChange(
|
|
277
|
+
mergeAndConvertCheckMapToLinkCustomtypes({
|
|
278
|
+
fieldCheckMap,
|
|
279
|
+
newCustomType,
|
|
280
|
+
linkCustomtypes,
|
|
281
|
+
customTypeId: id,
|
|
282
|
+
}),
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function addCustomType(id: string) {
|
|
287
|
+
const newFields = linkCustomtypes ? [...linkCustomtypes, id] : [id];
|
|
288
|
+
onChange(newFields);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function removeCustomType(id: string) {
|
|
292
|
+
if (linkCustomtypes) {
|
|
293
|
+
onChange(
|
|
294
|
+
linkCustomtypes.filter((existingCt) => getId(existingCt) !== id),
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return (
|
|
300
|
+
<Box
|
|
301
|
+
overflow="hidden"
|
|
302
|
+
flexDirection="column"
|
|
303
|
+
border
|
|
304
|
+
borderRadius={6}
|
|
305
|
+
width="100%"
|
|
306
|
+
>
|
|
307
|
+
<Box
|
|
308
|
+
border={{ bottom: true }}
|
|
309
|
+
padding={{ inline: 16, bottom: 16, top: 12 }}
|
|
310
|
+
flexDirection="column"
|
|
311
|
+
gap={8}
|
|
312
|
+
>
|
|
313
|
+
{pickedCustomTypes.length > 0 ? (
|
|
314
|
+
<>
|
|
315
|
+
<Box flexDirection="column">
|
|
316
|
+
<Text variant="h4" color="grey12">
|
|
317
|
+
Allowed type
|
|
318
|
+
</Text>
|
|
319
|
+
<Text color="grey11">
|
|
320
|
+
Select a single type that editors can link to in the Page
|
|
321
|
+
Builder.
|
|
322
|
+
<br />
|
|
323
|
+
For the selected type, choose which fields to include in the API
|
|
324
|
+
response.
|
|
325
|
+
</Text>
|
|
326
|
+
{pickedCustomTypes.length > 1 && (
|
|
327
|
+
<Box margin={{ block: 12 }}>
|
|
328
|
+
<Alert
|
|
329
|
+
color="warn"
|
|
330
|
+
icon="alert"
|
|
331
|
+
subtitle={
|
|
332
|
+
<>
|
|
333
|
+
<Text color="inherit" variant="bold">
|
|
334
|
+
Legacy mode. Keep only one type to enable the improved
|
|
335
|
+
Content Relationship feature.
|
|
336
|
+
</Text>
|
|
337
|
+
<br />
|
|
338
|
+
<a
|
|
339
|
+
href="https://prismic.io/docs/fields/content-relationship"
|
|
340
|
+
target="_blank"
|
|
341
|
+
rel="noopener noreferrer"
|
|
342
|
+
style={{
|
|
343
|
+
color: "inherit",
|
|
344
|
+
textDecoration: "none",
|
|
345
|
+
fontWeight: "bold",
|
|
346
|
+
display: "flex",
|
|
347
|
+
alignItems: "center",
|
|
348
|
+
gap: 4,
|
|
349
|
+
}}
|
|
350
|
+
>
|
|
351
|
+
<Text color="inherit" variant="bold">
|
|
352
|
+
See documentation
|
|
353
|
+
</Text>
|
|
354
|
+
<Icon
|
|
355
|
+
name="arrowForward"
|
|
356
|
+
size="small"
|
|
357
|
+
color="inherit"
|
|
358
|
+
/>
|
|
359
|
+
</a>
|
|
360
|
+
</>
|
|
361
|
+
}
|
|
362
|
+
/>
|
|
363
|
+
</Box>
|
|
364
|
+
)}
|
|
365
|
+
</Box>
|
|
366
|
+
{pickedCustomTypes.map((customType) => (
|
|
367
|
+
<Box
|
|
368
|
+
key={customType.id}
|
|
369
|
+
gap={4}
|
|
370
|
+
padding={8}
|
|
371
|
+
border
|
|
372
|
+
borderRadius={6}
|
|
373
|
+
borderColor="grey6"
|
|
374
|
+
backgroundColor="white"
|
|
375
|
+
justifyContent="space-between"
|
|
376
|
+
>
|
|
377
|
+
{pickedCustomTypes.length > 1 ? (
|
|
378
|
+
<Text>{customType.id}</Text>
|
|
379
|
+
) : (
|
|
380
|
+
<TreeView>
|
|
381
|
+
<TreeViewCustomType
|
|
382
|
+
customType={customType}
|
|
383
|
+
onChange={(value) =>
|
|
384
|
+
onCustomTypesChange(customType.id, value)
|
|
385
|
+
}
|
|
386
|
+
fieldCheckMap={fieldCheckMap[customType.id] ?? {}}
|
|
387
|
+
allCustomTypes={allCustomTypes}
|
|
388
|
+
/>
|
|
389
|
+
</TreeView>
|
|
390
|
+
)}
|
|
391
|
+
|
|
392
|
+
<IconButton
|
|
393
|
+
icon="close"
|
|
394
|
+
size="small"
|
|
395
|
+
onClick={() => removeCustomType(customType.id)}
|
|
396
|
+
sx={{ height: 24, width: 24 }}
|
|
397
|
+
hiddenLabel="Remove type"
|
|
398
|
+
/>
|
|
399
|
+
</Box>
|
|
400
|
+
))}
|
|
401
|
+
</>
|
|
402
|
+
) : (
|
|
403
|
+
<EmptyView onSelect={addCustomType} allCustomTypes={allCustomTypes} />
|
|
404
|
+
)}
|
|
405
|
+
</Box>
|
|
406
|
+
<Box backgroundColor="white" flexDirection="column" padding={12}>
|
|
407
|
+
<Text variant="normal" color="grey11">
|
|
408
|
+
Have ideas for improving this field?{" "}
|
|
409
|
+
<a
|
|
410
|
+
href="https://community.prismic.io/t/content-relationship-share-your-requests-and-feedback/19843"
|
|
411
|
+
target="_blank"
|
|
412
|
+
rel="noopener noreferrer"
|
|
413
|
+
style={{ color: "inherit", textDecoration: "underline" }}
|
|
414
|
+
>
|
|
415
|
+
Please provide your feedback here
|
|
416
|
+
</a>
|
|
417
|
+
.
|
|
418
|
+
</Text>
|
|
419
|
+
</Box>
|
|
420
|
+
</Box>
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
type EmptyViewProps = {
|
|
425
|
+
onSelect: (customTypeId: string) => void;
|
|
426
|
+
allCustomTypes: CustomType[];
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
function EmptyView(props: EmptyViewProps) {
|
|
430
|
+
const { allCustomTypes, onSelect } = props;
|
|
431
|
+
|
|
432
|
+
return (
|
|
433
|
+
<Box
|
|
434
|
+
flexDirection="column"
|
|
435
|
+
gap={8}
|
|
436
|
+
alignItems="center"
|
|
437
|
+
padding={{ block: 24 }}
|
|
438
|
+
>
|
|
439
|
+
<Box flexDirection="column" alignItems="center" gap={4}>
|
|
440
|
+
<Text variant="h5" color="grey12">
|
|
441
|
+
No type selected
|
|
442
|
+
</Text>
|
|
443
|
+
<Text color="grey11" component="p" align="center">
|
|
444
|
+
Select the type editors can link to.
|
|
445
|
+
<br />
|
|
446
|
+
Then, choose which fields to return in the API.
|
|
447
|
+
</Text>
|
|
448
|
+
</Box>
|
|
449
|
+
<Box>
|
|
450
|
+
<AddTypeButton allCustomTypes={allCustomTypes} onSelect={onSelect} />
|
|
451
|
+
</Box>
|
|
452
|
+
</Box>
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
type AddTypeButtonProps = {
|
|
457
|
+
onSelect: (customTypeId: string) => void;
|
|
458
|
+
allCustomTypes: CustomType[];
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
function AddTypeButton(props: AddTypeButtonProps) {
|
|
462
|
+
const { allCustomTypes, onSelect } = props;
|
|
463
|
+
|
|
464
|
+
const triggerButton = (
|
|
465
|
+
<Button startIcon="add" color="grey" disabled={allCustomTypes.length === 0}>
|
|
466
|
+
Add type
|
|
467
|
+
</Button>
|
|
468
|
+
);
|
|
469
|
+
|
|
470
|
+
if (allCustomTypes.length === 0) {
|
|
471
|
+
return (
|
|
472
|
+
<Box>
|
|
473
|
+
<Tooltip
|
|
474
|
+
content="No type available"
|
|
475
|
+
side="bottom"
|
|
476
|
+
align="start"
|
|
477
|
+
disableHoverableContent
|
|
478
|
+
>
|
|
479
|
+
{triggerButton}
|
|
480
|
+
</Tooltip>
|
|
481
|
+
</Box>
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
return (
|
|
486
|
+
<Box>
|
|
487
|
+
<DropdownMenu>
|
|
488
|
+
<DropdownMenuTrigger>{triggerButton}</DropdownMenuTrigger>
|
|
489
|
+
<DropdownMenuContent maxHeight={400} minWidth={256} align="center">
|
|
490
|
+
<DropdownMenuLabel>
|
|
491
|
+
<Text color="grey11">Types</Text>
|
|
492
|
+
</DropdownMenuLabel>
|
|
493
|
+
{allCustomTypes.map((customType) => (
|
|
494
|
+
<DropdownMenuItem
|
|
495
|
+
key={customType.id}
|
|
496
|
+
onSelect={() => onSelect(customType.id)}
|
|
497
|
+
>
|
|
498
|
+
<Box alignItems="center" justifyContent="space-between" gap={8}>
|
|
499
|
+
<TextOverflow>
|
|
500
|
+
<Text>{customType.id}</Text>
|
|
501
|
+
</TextOverflow>
|
|
502
|
+
<Badge
|
|
503
|
+
title={
|
|
504
|
+
<Text variant="extraSmall" color="purple11">
|
|
505
|
+
{getTypeFormatLabel(customType.format)}
|
|
506
|
+
</Text>
|
|
507
|
+
}
|
|
508
|
+
color="purple"
|
|
509
|
+
size="small"
|
|
510
|
+
/>
|
|
511
|
+
</Box>
|
|
512
|
+
</DropdownMenuItem>
|
|
513
|
+
))}
|
|
514
|
+
</DropdownMenuContent>
|
|
515
|
+
</DropdownMenu>
|
|
516
|
+
</Box>
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
interface TreeViewCustomTypeProps {
|
|
521
|
+
customType: CustomType;
|
|
522
|
+
fieldCheckMap: PickerCustomType;
|
|
523
|
+
onChange: (newValue: PickerCustomType) => void;
|
|
524
|
+
allCustomTypes: CustomType[];
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function TreeViewCustomType(props: TreeViewCustomTypeProps) {
|
|
528
|
+
const {
|
|
529
|
+
customType,
|
|
530
|
+
fieldCheckMap: customTypeFieldsCheckMap,
|
|
531
|
+
onChange: onCustomTypeChange,
|
|
532
|
+
allCustomTypes,
|
|
533
|
+
} = props;
|
|
534
|
+
|
|
535
|
+
const renderedFields = getCustomTypeStaticFields(customType).map(
|
|
536
|
+
([fieldId, field]) => {
|
|
537
|
+
// Group field
|
|
538
|
+
|
|
539
|
+
if (field.type === "Group") {
|
|
540
|
+
const onGroupFieldChange = (
|
|
541
|
+
newGroupFields: PickerFirstLevelGroupFieldValue,
|
|
542
|
+
) => {
|
|
543
|
+
onCustomTypeChange({
|
|
544
|
+
...customTypeFieldsCheckMap,
|
|
545
|
+
[fieldId]: { type: "group", value: newGroupFields },
|
|
546
|
+
});
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
const groupFieldCheckMap = customTypeFieldsCheckMap[fieldId] ?? {};
|
|
550
|
+
|
|
551
|
+
return (
|
|
552
|
+
<TreeViewFirstLevelGroupField
|
|
553
|
+
key={fieldId}
|
|
554
|
+
group={field}
|
|
555
|
+
groupId={fieldId}
|
|
556
|
+
onChange={onGroupFieldChange}
|
|
557
|
+
fieldCheckMap={
|
|
558
|
+
groupFieldCheckMap.type === "group"
|
|
559
|
+
? groupFieldCheckMap.value
|
|
560
|
+
: {}
|
|
561
|
+
}
|
|
562
|
+
allCustomTypes={allCustomTypes}
|
|
563
|
+
/>
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Content relationship field with custom types
|
|
568
|
+
|
|
569
|
+
if (isContentRelationshipFieldWithSingleCustomtype(field)) {
|
|
570
|
+
const onContentRelationshipFieldChange = (
|
|
571
|
+
newCrFields: PickerContentRelationshipFieldValue,
|
|
572
|
+
) => {
|
|
573
|
+
onCustomTypeChange({
|
|
574
|
+
...customTypeFieldsCheckMap,
|
|
575
|
+
[fieldId]: {
|
|
576
|
+
type: "contentRelationship",
|
|
577
|
+
value: newCrFields,
|
|
578
|
+
},
|
|
579
|
+
});
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
const crFieldCheckMap = customTypeFieldsCheckMap[fieldId] ?? {};
|
|
583
|
+
|
|
584
|
+
return (
|
|
585
|
+
<TreeViewContentRelationshipField
|
|
586
|
+
key={fieldId}
|
|
587
|
+
field={field}
|
|
588
|
+
fieldId={fieldId}
|
|
589
|
+
onChange={onContentRelationshipFieldChange}
|
|
590
|
+
fieldCheckMap={
|
|
591
|
+
crFieldCheckMap.type === "contentRelationship"
|
|
592
|
+
? crFieldCheckMap.value
|
|
593
|
+
: {}
|
|
594
|
+
}
|
|
595
|
+
allCustomTypes={allCustomTypes}
|
|
596
|
+
/>
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// Regular field
|
|
601
|
+
|
|
602
|
+
const onCheckedChange = (newValue: boolean) => {
|
|
603
|
+
onCustomTypeChange({
|
|
604
|
+
...customTypeFieldsCheckMap,
|
|
605
|
+
[fieldId]: { type: "checkbox", value: newValue },
|
|
606
|
+
});
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
return (
|
|
610
|
+
<TreeViewCheckbox
|
|
611
|
+
key={fieldId}
|
|
612
|
+
title={fieldId}
|
|
613
|
+
checked={customTypeFieldsCheckMap[fieldId]?.value === true}
|
|
614
|
+
onCheckedChange={onCheckedChange}
|
|
615
|
+
/>
|
|
616
|
+
);
|
|
617
|
+
},
|
|
618
|
+
);
|
|
619
|
+
|
|
620
|
+
const exposedFieldsCount = countPickedFields(customTypeFieldsCheckMap);
|
|
621
|
+
|
|
622
|
+
return (
|
|
623
|
+
<TreeViewSection
|
|
624
|
+
key={customType.id}
|
|
625
|
+
title={customType.id}
|
|
626
|
+
subtitle={
|
|
627
|
+
exposedFieldsCount.pickedFields > 0
|
|
628
|
+
? getPickedFieldsLabel(
|
|
629
|
+
exposedFieldsCount.pickedFields,
|
|
630
|
+
"returned in the API",
|
|
631
|
+
)
|
|
632
|
+
: "(No fields returned in the API)"
|
|
633
|
+
}
|
|
634
|
+
badge={getTypeFormatLabel(customType.format)}
|
|
635
|
+
defaultOpen
|
|
636
|
+
>
|
|
637
|
+
{renderedFields.length > 0 ? renderedFields : <NoFieldsAvailable />}
|
|
638
|
+
</TreeViewSection>
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
interface TreeViewContentRelationshipFieldProps {
|
|
643
|
+
fieldId: string;
|
|
644
|
+
field: Link;
|
|
645
|
+
fieldCheckMap: PickerContentRelationshipFieldValue;
|
|
646
|
+
onChange: (newValue: PickerContentRelationshipFieldValue) => void;
|
|
647
|
+
allCustomTypes: CustomType[];
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function TreeViewContentRelationshipField(
|
|
651
|
+
props: TreeViewContentRelationshipFieldProps,
|
|
652
|
+
) {
|
|
653
|
+
const {
|
|
654
|
+
field,
|
|
655
|
+
fieldId,
|
|
656
|
+
fieldCheckMap: crFieldsCheckMap,
|
|
657
|
+
onChange: onCrFieldChange,
|
|
658
|
+
allCustomTypes,
|
|
659
|
+
} = props;
|
|
660
|
+
|
|
661
|
+
if (!field.config?.customtypes) return null;
|
|
662
|
+
|
|
663
|
+
const resolvedCustomTypes = resolveContentRelationshipCustomTypes(
|
|
664
|
+
field.config.customtypes,
|
|
665
|
+
allCustomTypes,
|
|
666
|
+
);
|
|
667
|
+
|
|
668
|
+
if (resolvedCustomTypes.length !== 1) return null;
|
|
669
|
+
|
|
670
|
+
const [customType] = resolvedCustomTypes;
|
|
671
|
+
|
|
672
|
+
if (typeof customType === "string") return null;
|
|
673
|
+
|
|
674
|
+
const onNestedCustomTypeChange = (
|
|
675
|
+
newNestedCustomTypeFields: PickerNestedCustomTypeValue,
|
|
676
|
+
) => {
|
|
677
|
+
onCrFieldChange({
|
|
678
|
+
...crFieldsCheckMap,
|
|
679
|
+
[customType.id]: newNestedCustomTypeFields,
|
|
680
|
+
});
|
|
681
|
+
};
|
|
682
|
+
|
|
683
|
+
const nestedCtFieldsCheckMap = crFieldsCheckMap[customType.id] ?? {};
|
|
684
|
+
|
|
685
|
+
const renderedFields = getCustomTypeStaticFields(customType).map(
|
|
686
|
+
([fieldId, field]) => {
|
|
687
|
+
// Group field
|
|
688
|
+
|
|
689
|
+
if (field.type === "Group") {
|
|
690
|
+
const onGroupFieldsChange = (
|
|
691
|
+
newGroupFields: PickerLeafGroupFieldValue,
|
|
692
|
+
) => {
|
|
693
|
+
onNestedCustomTypeChange({
|
|
694
|
+
...nestedCtFieldsCheckMap,
|
|
695
|
+
[fieldId]: { type: "group", value: newGroupFields },
|
|
696
|
+
});
|
|
697
|
+
};
|
|
698
|
+
|
|
699
|
+
const groupFieldCheckMap = nestedCtFieldsCheckMap[fieldId] ?? {};
|
|
700
|
+
|
|
701
|
+
return (
|
|
702
|
+
<TreeViewLeafGroupField
|
|
703
|
+
key={fieldId}
|
|
704
|
+
group={field}
|
|
705
|
+
groupId={fieldId}
|
|
706
|
+
onChange={onGroupFieldsChange}
|
|
707
|
+
fieldCheckMap={
|
|
708
|
+
groupFieldCheckMap.type === "group"
|
|
709
|
+
? groupFieldCheckMap.value
|
|
710
|
+
: {}
|
|
711
|
+
}
|
|
712
|
+
/>
|
|
713
|
+
);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// Regular field
|
|
717
|
+
|
|
718
|
+
const onCheckedChange = (newChecked: boolean) => {
|
|
719
|
+
onNestedCustomTypeChange({
|
|
720
|
+
...nestedCtFieldsCheckMap,
|
|
721
|
+
[fieldId]: { type: "checkbox", value: newChecked },
|
|
722
|
+
});
|
|
723
|
+
};
|
|
724
|
+
|
|
725
|
+
return (
|
|
726
|
+
<TreeViewCheckbox
|
|
727
|
+
key={fieldId}
|
|
728
|
+
title={fieldId}
|
|
729
|
+
checked={nestedCtFieldsCheckMap[fieldId]?.value === true}
|
|
730
|
+
onCheckedChange={onCheckedChange}
|
|
731
|
+
/>
|
|
732
|
+
);
|
|
733
|
+
},
|
|
734
|
+
);
|
|
735
|
+
|
|
736
|
+
return (
|
|
737
|
+
<TreeViewSection
|
|
738
|
+
key={customType.id}
|
|
739
|
+
// @ts-expect-error - TODO: Fix this when we are able to release editor packages
|
|
740
|
+
title={
|
|
741
|
+
<Text>
|
|
742
|
+
{fieldId} <Text color="grey11">→ {customType.id}</Text>
|
|
743
|
+
</Text>
|
|
744
|
+
}
|
|
745
|
+
subtitle={getPickedFieldsLabel(
|
|
746
|
+
countPickedFields(nestedCtFieldsCheckMap).pickedFields,
|
|
747
|
+
)}
|
|
748
|
+
badge={getTypeFormatLabel(customType.format)}
|
|
749
|
+
>
|
|
750
|
+
{renderedFields.length > 0 ? renderedFields : <NoFieldsAvailable />}
|
|
751
|
+
</TreeViewSection>
|
|
752
|
+
);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
function NoFieldsAvailable() {
|
|
756
|
+
return <Text color="grey11">No available fields to select</Text>;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
interface TreeViewLeafGroupFieldProps {
|
|
760
|
+
group: Group;
|
|
761
|
+
groupId: string;
|
|
762
|
+
fieldCheckMap: PickerLeafGroupFieldValue;
|
|
763
|
+
onChange: (newValue: PickerLeafGroupFieldValue) => void;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function TreeViewLeafGroupField(props: TreeViewLeafGroupFieldProps) {
|
|
767
|
+
const {
|
|
768
|
+
group,
|
|
769
|
+
groupId,
|
|
770
|
+
fieldCheckMap: groupFieldsCheckMap,
|
|
771
|
+
onChange: onGroupFieldChange,
|
|
772
|
+
} = props;
|
|
773
|
+
|
|
774
|
+
if (!group.config?.fields) return null;
|
|
775
|
+
|
|
776
|
+
const renderedFields = getGroupFields(group).map(({ fieldId }) => {
|
|
777
|
+
const onCheckedChange = (newChecked: boolean) => {
|
|
778
|
+
onGroupFieldChange({
|
|
779
|
+
...groupFieldsCheckMap,
|
|
780
|
+
[fieldId]: { type: "checkbox", value: newChecked },
|
|
781
|
+
});
|
|
782
|
+
};
|
|
783
|
+
|
|
784
|
+
return (
|
|
785
|
+
<TreeViewCheckbox
|
|
786
|
+
key={fieldId}
|
|
787
|
+
title={fieldId}
|
|
788
|
+
checked={groupFieldsCheckMap[fieldId]?.value === true}
|
|
789
|
+
onCheckedChange={onCheckedChange}
|
|
790
|
+
/>
|
|
791
|
+
);
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
return (
|
|
795
|
+
<TreeViewSection
|
|
796
|
+
key={groupId}
|
|
797
|
+
title={groupId}
|
|
798
|
+
subtitle={getPickedFieldsLabel(
|
|
799
|
+
countPickedFields(groupFieldsCheckMap).pickedFields,
|
|
800
|
+
)}
|
|
801
|
+
badge="Group"
|
|
802
|
+
>
|
|
803
|
+
{renderedFields.length > 0 ? renderedFields : <NoFieldsAvailable />}
|
|
804
|
+
</TreeViewSection>
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
interface TreeViewFirstLevelGroupFieldProps {
|
|
809
|
+
group: Group;
|
|
810
|
+
groupId: string;
|
|
811
|
+
fieldCheckMap: PickerFirstLevelGroupFieldValue;
|
|
812
|
+
onChange: (newValue: PickerFirstLevelGroupFieldValue) => void;
|
|
813
|
+
allCustomTypes: CustomType[];
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
function TreeViewFirstLevelGroupField(
|
|
817
|
+
props: TreeViewFirstLevelGroupFieldProps,
|
|
818
|
+
) {
|
|
819
|
+
const {
|
|
820
|
+
group,
|
|
821
|
+
groupId,
|
|
822
|
+
fieldCheckMap: groupFieldsCheckMap,
|
|
823
|
+
onChange: onGroupFieldChange,
|
|
824
|
+
allCustomTypes,
|
|
825
|
+
} = props;
|
|
826
|
+
|
|
827
|
+
const renderedFields = getGroupFields(group).map(({ fieldId, field }) => {
|
|
828
|
+
// Content relationship field with custom types
|
|
829
|
+
|
|
830
|
+
if (isContentRelationshipFieldWithSingleCustomtype(field)) {
|
|
831
|
+
const onContentRelationshipFieldChange = (
|
|
832
|
+
newCrFields: PickerContentRelationshipFieldValue,
|
|
833
|
+
) => {
|
|
834
|
+
onGroupFieldChange({
|
|
835
|
+
...groupFieldsCheckMap,
|
|
836
|
+
[fieldId]: {
|
|
837
|
+
type: "contentRelationship",
|
|
838
|
+
value: newCrFields,
|
|
839
|
+
},
|
|
840
|
+
});
|
|
841
|
+
};
|
|
842
|
+
|
|
843
|
+
const crFieldCheckMap = groupFieldsCheckMap[fieldId] ?? {};
|
|
844
|
+
|
|
845
|
+
return (
|
|
846
|
+
<TreeViewContentRelationshipField
|
|
847
|
+
key={fieldId}
|
|
848
|
+
field={field}
|
|
849
|
+
fieldId={fieldId}
|
|
850
|
+
fieldCheckMap={
|
|
851
|
+
crFieldCheckMap.type === "contentRelationship"
|
|
852
|
+
? crFieldCheckMap.value
|
|
853
|
+
: {}
|
|
854
|
+
}
|
|
855
|
+
onChange={onContentRelationshipFieldChange}
|
|
856
|
+
allCustomTypes={allCustomTypes}
|
|
857
|
+
/>
|
|
858
|
+
);
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// Regular field
|
|
862
|
+
|
|
863
|
+
const onCheckedChange = (newChecked: boolean) => {
|
|
864
|
+
onGroupFieldChange({
|
|
865
|
+
...groupFieldsCheckMap,
|
|
866
|
+
[fieldId]: { type: "checkbox", value: newChecked },
|
|
867
|
+
});
|
|
868
|
+
};
|
|
869
|
+
|
|
870
|
+
return (
|
|
871
|
+
<TreeViewCheckbox
|
|
872
|
+
key={fieldId}
|
|
873
|
+
title={fieldId}
|
|
874
|
+
checked={groupFieldsCheckMap[fieldId]?.value === true}
|
|
875
|
+
onCheckedChange={onCheckedChange}
|
|
876
|
+
/>
|
|
877
|
+
);
|
|
878
|
+
});
|
|
879
|
+
|
|
880
|
+
return (
|
|
881
|
+
<TreeViewSection
|
|
882
|
+
key={groupId}
|
|
883
|
+
title={groupId}
|
|
884
|
+
subtitle={getPickedFieldsLabel(
|
|
885
|
+
countPickedFields(groupFieldsCheckMap).pickedFields,
|
|
886
|
+
)}
|
|
887
|
+
badge="Group"
|
|
888
|
+
>
|
|
889
|
+
{renderedFields.length > 0 ? renderedFields : <NoFieldsAvailable />}
|
|
890
|
+
</TreeViewSection>
|
|
891
|
+
);
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
function getPickedFieldsLabel(count: number, suffix = "selected") {
|
|
895
|
+
if (count === 0) return undefined;
|
|
896
|
+
return `(${count} ${pluralize(count, "field", "fields")} ${suffix})`;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
function getTypeFormatLabel(format: CustomType["format"]) {
|
|
900
|
+
return format === "page" ? "Page type" : "Custom type";
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
/** Retrieves all existing page & custom types. */
|
|
904
|
+
function useCustomTypes(linkCustomtypes: LinkCustomtypes | undefined): {
|
|
905
|
+
/** Every existing custom type, used to discover nested custom types down the tree and the add type dropdown. */
|
|
906
|
+
allCustomTypes: CustomType[];
|
|
907
|
+
/** The custom types that are already picked. */
|
|
908
|
+
pickedCustomTypes: CustomType[];
|
|
909
|
+
} {
|
|
910
|
+
const { customTypes: allCustomTypes } = useCustomTypesRequest();
|
|
911
|
+
|
|
912
|
+
useEffect(() => {
|
|
913
|
+
void revalidateGetCustomTypes();
|
|
914
|
+
}, []);
|
|
915
|
+
|
|
916
|
+
if (!linkCustomtypes) {
|
|
917
|
+
return {
|
|
918
|
+
allCustomTypes,
|
|
919
|
+
pickedCustomTypes: [],
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
const pickedCustomTypes = linkCustomtypes.flatMap(
|
|
924
|
+
(pickedCt) => allCustomTypes.find((ct) => ct.id === getId(pickedCt)) ?? [],
|
|
925
|
+
);
|
|
926
|
+
|
|
927
|
+
return {
|
|
928
|
+
allCustomTypes,
|
|
929
|
+
pickedCustomTypes,
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
function resolveContentRelationshipCustomTypes(
|
|
934
|
+
linkCustomtypes: LinkCustomtypes,
|
|
935
|
+
allCustomTypes: CustomType[],
|
|
936
|
+
): CustomType[] {
|
|
937
|
+
return linkCustomtypes.flatMap((linkCustomtype) => {
|
|
938
|
+
return allCustomTypes.find((ct) => ct.id === getId(linkCustomtype)) ?? [];
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/**
|
|
943
|
+
* Converts a Link config `customtypes` ({@link LinkCustomtypes}) structure into
|
|
944
|
+
* picker fields check map ({@link PickerCustomTypes}).
|
|
945
|
+
*/
|
|
946
|
+
export function convertLinkCustomtypesToFieldCheckMap(args: {
|
|
947
|
+
linkCustomtypes: LinkCustomtypes;
|
|
948
|
+
allCustomTypes?: CustomType[];
|
|
949
|
+
}): PickerCustomTypes {
|
|
950
|
+
const { linkCustomtypes, allCustomTypes } = args;
|
|
951
|
+
|
|
952
|
+
// If allCustomTypes is undefined, avoid checking if the fields exist.
|
|
953
|
+
const shouldValidate = allCustomTypes !== undefined;
|
|
954
|
+
|
|
955
|
+
const checkMap = linkCustomtypes.reduce<PickerCustomTypes>(
|
|
956
|
+
(customTypes, customType) => {
|
|
957
|
+
if (typeof customType === "string") return customTypes;
|
|
958
|
+
|
|
959
|
+
let ctFlatFieldMap: Record<string, NestableWidget | Group> = {};
|
|
960
|
+
|
|
961
|
+
if (shouldValidate) {
|
|
962
|
+
const existingCt = allCustomTypes.find((c) => c.id === customType.id);
|
|
963
|
+
// Exit early if the custom type doesn't exist
|
|
964
|
+
if (!existingCt) return customTypes;
|
|
965
|
+
|
|
966
|
+
ctFlatFieldMap = getCustomTypeStaticFieldsMap(existingCt);
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
const customTypeFields = customType.fields.reduce<PickerCustomType>(
|
|
970
|
+
(fields, field) => {
|
|
971
|
+
// Check if the field exists (only if validating)
|
|
972
|
+
const existingField = ctFlatFieldMap[getId(field)];
|
|
973
|
+
if (shouldValidate && existingField === undefined) return fields;
|
|
974
|
+
|
|
975
|
+
// Regular field
|
|
976
|
+
if (typeof field === "string") {
|
|
977
|
+
// Check if the field matched the existing one in the custom type (only if validating)
|
|
978
|
+
if (
|
|
979
|
+
shouldValidate &&
|
|
980
|
+
existingField !== undefined &&
|
|
981
|
+
existingField.type === "Group"
|
|
982
|
+
) {
|
|
983
|
+
return fields;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
fields[field] = { type: "checkbox", value: true };
|
|
987
|
+
return fields;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
// Group field
|
|
991
|
+
if ("fields" in field && field.fields !== undefined) {
|
|
992
|
+
// Check if the field matched the existing one in the custom type (only if validating)
|
|
993
|
+
if (
|
|
994
|
+
shouldValidate &&
|
|
995
|
+
existingField !== undefined &&
|
|
996
|
+
existingField.type !== "Group"
|
|
997
|
+
) {
|
|
998
|
+
return fields;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
const groupFieldCheckMap = createGroupFieldCheckMap({
|
|
1002
|
+
group: field,
|
|
1003
|
+
allCustomTypes,
|
|
1004
|
+
ctFlatFieldMap,
|
|
1005
|
+
});
|
|
1006
|
+
|
|
1007
|
+
if (groupFieldCheckMap) {
|
|
1008
|
+
fields[field.id] = groupFieldCheckMap;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
return fields;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// Content relationship field
|
|
1015
|
+
if ("customtypes" in field && field.customtypes !== undefined) {
|
|
1016
|
+
// Check if the field matched the existing one in the custom type (only if validating)
|
|
1017
|
+
if (
|
|
1018
|
+
shouldValidate &&
|
|
1019
|
+
existingField !== undefined &&
|
|
1020
|
+
!isContentRelationshipField(existingField)
|
|
1021
|
+
) {
|
|
1022
|
+
return fields;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
const crFieldCheckMap = createContentRelationshipFieldCheckMap({
|
|
1026
|
+
field,
|
|
1027
|
+
allCustomTypes,
|
|
1028
|
+
});
|
|
1029
|
+
|
|
1030
|
+
if (crFieldCheckMap) {
|
|
1031
|
+
fields[field.id] = crFieldCheckMap;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
return fields;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
return fields;
|
|
1038
|
+
},
|
|
1039
|
+
{},
|
|
1040
|
+
);
|
|
1041
|
+
|
|
1042
|
+
if (Object.keys(customTypeFields).length > 0) {
|
|
1043
|
+
customTypes[customType.id] = customTypeFields;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
return customTypes;
|
|
1047
|
+
},
|
|
1048
|
+
{},
|
|
1049
|
+
);
|
|
1050
|
+
|
|
1051
|
+
return checkMap;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
function createGroupFieldCheckMap(args: {
|
|
1055
|
+
group: LinkCustomtypesGroupFieldValue;
|
|
1056
|
+
allCustomTypes?: CustomType[];
|
|
1057
|
+
ctFlatFieldMap: Record<string, NestableWidget | Group>;
|
|
1058
|
+
}): PickerFirstLevelGroupField | undefined {
|
|
1059
|
+
const { group, ctFlatFieldMap, allCustomTypes } = args;
|
|
1060
|
+
|
|
1061
|
+
// If allCustomTypes is undefined, avoid checking if the fields exist.
|
|
1062
|
+
const shouldValidate = allCustomTypes !== undefined;
|
|
1063
|
+
|
|
1064
|
+
const fieldEntries = group.fields.reduce<PickerFirstLevelGroupFieldValue>(
|
|
1065
|
+
(fields, field) => {
|
|
1066
|
+
// Check if the field exists (only if validating)
|
|
1067
|
+
const existingField = getGroupFieldFromMap(
|
|
1068
|
+
ctFlatFieldMap,
|
|
1069
|
+
group.id,
|
|
1070
|
+
getId(field),
|
|
1071
|
+
);
|
|
1072
|
+
if (shouldValidate && !existingField) return fields;
|
|
1073
|
+
|
|
1074
|
+
// Regular field
|
|
1075
|
+
if (typeof field === "string") {
|
|
1076
|
+
// Check if the field matched the existing one in the custom type (only if validating)
|
|
1077
|
+
if (
|
|
1078
|
+
shouldValidate &&
|
|
1079
|
+
existingField !== undefined &&
|
|
1080
|
+
existingField.type === "Group"
|
|
1081
|
+
) {
|
|
1082
|
+
return fields;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
fields[field] = { type: "checkbox", value: true };
|
|
1086
|
+
return fields;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
// Content relationship field
|
|
1090
|
+
if ("customtypes" in field && field.customtypes !== undefined) {
|
|
1091
|
+
// Check if the field matched the existing one in the custom type (only if validating)
|
|
1092
|
+
if (
|
|
1093
|
+
shouldValidate &&
|
|
1094
|
+
existingField !== undefined &&
|
|
1095
|
+
!isContentRelationshipField(existingField)
|
|
1096
|
+
) {
|
|
1097
|
+
return fields;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
const crFieldCheckMap = createContentRelationshipFieldCheckMap({
|
|
1101
|
+
field,
|
|
1102
|
+
allCustomTypes,
|
|
1103
|
+
});
|
|
1104
|
+
|
|
1105
|
+
if (crFieldCheckMap) {
|
|
1106
|
+
fields[field.id] = crFieldCheckMap;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
return fields;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
return fields;
|
|
1113
|
+
},
|
|
1114
|
+
{},
|
|
1115
|
+
);
|
|
1116
|
+
|
|
1117
|
+
if (Object.keys(fieldEntries).length === 0) return undefined;
|
|
1118
|
+
|
|
1119
|
+
return {
|
|
1120
|
+
type: "group",
|
|
1121
|
+
value: fieldEntries,
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
function createContentRelationshipFieldCheckMap(args: {
|
|
1126
|
+
field: LinkCustomtypesContentRelationshipFieldValue;
|
|
1127
|
+
allCustomTypes?: CustomType[];
|
|
1128
|
+
}): PickerContentRelationshipField | undefined {
|
|
1129
|
+
const { field, allCustomTypes } = args;
|
|
1130
|
+
|
|
1131
|
+
// If allCustomTypes is undefined, avoid checking if the fields exists.
|
|
1132
|
+
const shouldValidate = allCustomTypes !== undefined;
|
|
1133
|
+
|
|
1134
|
+
const fieldEntries =
|
|
1135
|
+
field.customtypes.reduce<PickerContentRelationshipFieldValue>(
|
|
1136
|
+
(customTypes, customType) => {
|
|
1137
|
+
if (typeof customType === "string") return customTypes;
|
|
1138
|
+
|
|
1139
|
+
let ctFlatFieldMap: Record<string, NestableWidget | Group> = {};
|
|
1140
|
+
|
|
1141
|
+
if (shouldValidate) {
|
|
1142
|
+
const existingCt = allCustomTypes.find((c) => c.id === customType.id);
|
|
1143
|
+
// Exit early if the custom type doesn't exist
|
|
1144
|
+
if (!existingCt) return customTypes;
|
|
1145
|
+
|
|
1146
|
+
ctFlatFieldMap = getCustomTypeStaticFieldsMap(existingCt);
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
const ctFields = customType.fields.reduce<PickerNestedCustomTypeValue>(
|
|
1150
|
+
(nestedFields, nestedField) => {
|
|
1151
|
+
// Regular field
|
|
1152
|
+
if (typeof nestedField === "string") {
|
|
1153
|
+
const existingField = ctFlatFieldMap[nestedField];
|
|
1154
|
+
|
|
1155
|
+
// Check if the field matched the existing one in the custom type (only if validating)
|
|
1156
|
+
if (
|
|
1157
|
+
shouldValidate &&
|
|
1158
|
+
(existingField === undefined || existingField.type === "Group")
|
|
1159
|
+
) {
|
|
1160
|
+
return nestedFields;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
nestedFields[nestedField] = { type: "checkbox", value: true };
|
|
1164
|
+
return nestedFields;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
if ("fields" in nestedField && nestedField.fields !== undefined) {
|
|
1168
|
+
// Group field
|
|
1169
|
+
const groupFields =
|
|
1170
|
+
nestedField.fields.reduce<PickerLeafGroupFieldValue>(
|
|
1171
|
+
(groupFields, groupField) => {
|
|
1172
|
+
const existingField = getGroupFieldFromMap(
|
|
1173
|
+
ctFlatFieldMap,
|
|
1174
|
+
nestedField.id,
|
|
1175
|
+
groupField,
|
|
1176
|
+
);
|
|
1177
|
+
|
|
1178
|
+
// Check if the field matched the existing one in the custom type (only if validating)
|
|
1179
|
+
if (
|
|
1180
|
+
shouldValidate &&
|
|
1181
|
+
(existingField === undefined ||
|
|
1182
|
+
existingField.type === "Group")
|
|
1183
|
+
) {
|
|
1184
|
+
return groupFields;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
groupFields[groupField] = { type: "checkbox", value: true };
|
|
1188
|
+
return groupFields;
|
|
1189
|
+
},
|
|
1190
|
+
{},
|
|
1191
|
+
);
|
|
1192
|
+
|
|
1193
|
+
if (Object.keys(groupFields).length > 0) {
|
|
1194
|
+
nestedFields[nestedField.id] = {
|
|
1195
|
+
type: "group",
|
|
1196
|
+
value: groupFields,
|
|
1197
|
+
};
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
return nestedFields;
|
|
1202
|
+
},
|
|
1203
|
+
{},
|
|
1204
|
+
);
|
|
1205
|
+
|
|
1206
|
+
if (Object.keys(ctFields).length > 0) {
|
|
1207
|
+
customTypes[customType.id] = ctFields;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
return customTypes;
|
|
1211
|
+
},
|
|
1212
|
+
{},
|
|
1213
|
+
);
|
|
1214
|
+
|
|
1215
|
+
if (Object.keys(fieldEntries).length === 0) return undefined;
|
|
1216
|
+
|
|
1217
|
+
return {
|
|
1218
|
+
type: "contentRelationship",
|
|
1219
|
+
value: fieldEntries,
|
|
1220
|
+
};
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
/**
|
|
1224
|
+
* Merges the existing Link `customtypes` array with the picker state, ensuring
|
|
1225
|
+
* that conversions from to string (custom type id) to object and vice versa are
|
|
1226
|
+
* made correctly and that the order is preserved.
|
|
1227
|
+
*/
|
|
1228
|
+
function mergeAndConvertCheckMapToLinkCustomtypes(args: {
|
|
1229
|
+
linkCustomtypes: LinkCustomtypes | undefined;
|
|
1230
|
+
fieldCheckMap: PickerCustomTypes;
|
|
1231
|
+
newCustomType: PickerCustomType;
|
|
1232
|
+
customTypeId: string;
|
|
1233
|
+
}): LinkCustomtypes {
|
|
1234
|
+
const { linkCustomtypes, fieldCheckMap, newCustomType, customTypeId } = args;
|
|
1235
|
+
|
|
1236
|
+
const result: NonReadonly<LinkCustomtypes> = [];
|
|
1237
|
+
const pickerLinkCustomtypes = convertFieldCheckMapToLinkCustomtypes({
|
|
1238
|
+
...fieldCheckMap,
|
|
1239
|
+
[customTypeId]: newCustomType,
|
|
1240
|
+
});
|
|
1241
|
+
|
|
1242
|
+
if (!linkCustomtypes) return pickerLinkCustomtypes;
|
|
1243
|
+
|
|
1244
|
+
for (const existingLinkCt of linkCustomtypes) {
|
|
1245
|
+
const existingPickerLinkCt = pickerLinkCustomtypes.find((ct) => {
|
|
1246
|
+
return getId(ct) === getId(existingLinkCt);
|
|
1247
|
+
});
|
|
1248
|
+
|
|
1249
|
+
if (existingPickerLinkCt !== undefined) {
|
|
1250
|
+
// Custom type with exposed fields, keep the customtypes object
|
|
1251
|
+
result.push(existingPickerLinkCt);
|
|
1252
|
+
} else if (getId(existingLinkCt) === customTypeId) {
|
|
1253
|
+
// Custom type that had exposed fields, but now has none, change to string
|
|
1254
|
+
result.push(getId(existingLinkCt));
|
|
1255
|
+
} else {
|
|
1256
|
+
// Custom type without exposed fields, keep the string
|
|
1257
|
+
result.push(existingLinkCt);
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
return result;
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
/**
|
|
1265
|
+
* Converts a picker fields check map structure ({@link PickerCustomTypes}) into
|
|
1266
|
+
* Link config `customtypes` ({@link LinkCustomtypes}) and filter out empty Custom
|
|
1267
|
+
* types.
|
|
1268
|
+
*/
|
|
1269
|
+
function convertFieldCheckMapToLinkCustomtypes(
|
|
1270
|
+
checkMap: PickerCustomTypes,
|
|
1271
|
+
): LinkCustomtypes {
|
|
1272
|
+
return Object.entries(checkMap).flatMap<LinkCustomtypes[number]>(
|
|
1273
|
+
([ctId, ctFields]) => {
|
|
1274
|
+
const fields = Object.entries(ctFields).flatMap<
|
|
1275
|
+
| string
|
|
1276
|
+
| LinkCustomtypesContentRelationshipFieldValue
|
|
1277
|
+
| LinkCustomtypesGroupFieldValue
|
|
1278
|
+
>(([fieldId, fieldValue]) => {
|
|
1279
|
+
// First level group field
|
|
1280
|
+
if (fieldValue.type === "group") {
|
|
1281
|
+
const fields = Object.entries(fieldValue.value).flatMap<
|
|
1282
|
+
string | LinkCustomtypesContentRelationshipFieldValue
|
|
1283
|
+
>(([fieldId, fieldValue]) => {
|
|
1284
|
+
if (fieldValue.type === "checkbox") {
|
|
1285
|
+
return fieldValue.value ? fieldId : [];
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
const customTypes = createContentRelationshipLinkCustomtypes(
|
|
1289
|
+
fieldValue.value,
|
|
1290
|
+
);
|
|
1291
|
+
|
|
1292
|
+
return customTypes.length > 0
|
|
1293
|
+
? { id: fieldId, customtypes: customTypes }
|
|
1294
|
+
: [];
|
|
1295
|
+
});
|
|
1296
|
+
|
|
1297
|
+
return fields.length > 0 ? { id: fieldId, fields } : [];
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
// Content relationship field
|
|
1301
|
+
if (fieldValue.type === "contentRelationship") {
|
|
1302
|
+
const customTypes = createContentRelationshipLinkCustomtypes(
|
|
1303
|
+
fieldValue.value,
|
|
1304
|
+
);
|
|
1305
|
+
|
|
1306
|
+
return customTypes.length > 0
|
|
1307
|
+
? { id: fieldId, customtypes: customTypes }
|
|
1308
|
+
: [];
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
// Regular field
|
|
1312
|
+
return fieldValue.value ? fieldId : [];
|
|
1313
|
+
});
|
|
1314
|
+
|
|
1315
|
+
return fields.length > 0 ? { id: ctId, fields } : [];
|
|
1316
|
+
},
|
|
1317
|
+
);
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
function createContentRelationshipLinkCustomtypes(
|
|
1321
|
+
value: PickerContentRelationshipFieldValue,
|
|
1322
|
+
): LinkCustomtypesContentRelationshipFieldValue["customtypes"] {
|
|
1323
|
+
return Object.entries(value).flatMap(
|
|
1324
|
+
([nestedCustomTypeId, nestedCustomTypeFields]) => {
|
|
1325
|
+
const fields = Object.entries(nestedCustomTypeFields).flatMap(
|
|
1326
|
+
([nestedFieldId, nestedFieldValue]) => {
|
|
1327
|
+
// Leaf group field
|
|
1328
|
+
if (nestedFieldValue.type === "group") {
|
|
1329
|
+
const nestedGroupFields = Object.entries(
|
|
1330
|
+
nestedFieldValue.value,
|
|
1331
|
+
).flatMap<string>(([fieldId, fieldValue]) => {
|
|
1332
|
+
// Regular field
|
|
1333
|
+
return fieldValue.type === "checkbox" && fieldValue.value
|
|
1334
|
+
? fieldId
|
|
1335
|
+
: [];
|
|
1336
|
+
});
|
|
1337
|
+
|
|
1338
|
+
return nestedGroupFields.length > 0
|
|
1339
|
+
? { id: nestedFieldId, fields: nestedGroupFields }
|
|
1340
|
+
: [];
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
return nestedFieldValue.value ? nestedFieldId : [];
|
|
1344
|
+
},
|
|
1345
|
+
);
|
|
1346
|
+
|
|
1347
|
+
return fields.length > 0 ? { id: nestedCustomTypeId, fields } : [];
|
|
1348
|
+
},
|
|
1349
|
+
);
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
type CountPickedFieldsResult = {
|
|
1353
|
+
pickedFields: number;
|
|
1354
|
+
nestedPickedFields: number;
|
|
1355
|
+
};
|
|
1356
|
+
|
|
1357
|
+
/**
|
|
1358
|
+
* Generic recursive function that goes down the fields check map and counts all
|
|
1359
|
+
* the properties that are set to true, which correspond to selected fields.
|
|
1360
|
+
*
|
|
1361
|
+
* Distinguishes between all picked fields and nested picked fields within a
|
|
1362
|
+
* content relationship field.
|
|
1363
|
+
*
|
|
1364
|
+
* It's not type safe, but checks the type of the values at runtime so that
|
|
1365
|
+
* it only recurses into valid objects, and only counts checkbox fields.
|
|
1366
|
+
*/
|
|
1367
|
+
export function countPickedFields(
|
|
1368
|
+
fields: Record<string, unknown> | undefined,
|
|
1369
|
+
isNested = false,
|
|
1370
|
+
): CountPickedFieldsResult {
|
|
1371
|
+
if (!fields) return { pickedFields: 0, nestedPickedFields: 0 };
|
|
1372
|
+
|
|
1373
|
+
return Object.values(fields).reduce<CountPickedFieldsResult>(
|
|
1374
|
+
(result, value) => {
|
|
1375
|
+
if (!isValidObject(value)) return result;
|
|
1376
|
+
|
|
1377
|
+
if ("type" in value && value.type === "checkbox") {
|
|
1378
|
+
const isChecked = Boolean(value.value);
|
|
1379
|
+
if (!isChecked) return result;
|
|
1380
|
+
|
|
1381
|
+
return {
|
|
1382
|
+
pickedFields: result.pickedFields + 1,
|
|
1383
|
+
nestedPickedFields: result.nestedPickedFields + (isNested ? 1 : 0),
|
|
1384
|
+
};
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
if ("type" in value && value.type === "contentRelationship") {
|
|
1388
|
+
const { pickedFields, nestedPickedFields } = countPickedFields(
|
|
1389
|
+
value,
|
|
1390
|
+
true,
|
|
1391
|
+
);
|
|
1392
|
+
|
|
1393
|
+
return {
|
|
1394
|
+
pickedFields: result.pickedFields + pickedFields,
|
|
1395
|
+
nestedPickedFields: result.nestedPickedFields + nestedPickedFields,
|
|
1396
|
+
};
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
const { pickedFields, nestedPickedFields } = countPickedFields(
|
|
1400
|
+
value,
|
|
1401
|
+
isNested,
|
|
1402
|
+
);
|
|
1403
|
+
|
|
1404
|
+
return {
|
|
1405
|
+
pickedFields: result.pickedFields + pickedFields,
|
|
1406
|
+
nestedPickedFields: result.nestedPickedFields + nestedPickedFields,
|
|
1407
|
+
};
|
|
1408
|
+
},
|
|
1409
|
+
{
|
|
1410
|
+
pickedFields: 0,
|
|
1411
|
+
nestedPickedFields: 0,
|
|
1412
|
+
},
|
|
1413
|
+
);
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
function isContentRelationshipField(field: DynamicWidget): field is Link {
|
|
1417
|
+
return field.type === "Link" && field.config?.select === "document";
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
/**
|
|
1421
|
+
* Check if the field is a Content Relationship Link with a **single** custom
|
|
1422
|
+
* type. CRs with multiple custom types are not currently supported (legacy).
|
|
1423
|
+
*/
|
|
1424
|
+
function isContentRelationshipFieldWithSingleCustomtype(
|
|
1425
|
+
field: NestableWidget | Group,
|
|
1426
|
+
): field is Link {
|
|
1427
|
+
return !!(
|
|
1428
|
+
isContentRelationshipField(field) &&
|
|
1429
|
+
field.config?.customtypes &&
|
|
1430
|
+
field.config.customtypes.length === 1
|
|
1431
|
+
);
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
/**
|
|
1435
|
+
* Flattens all custom type tabs and fields into an array of [fieldId, field] tuples.
|
|
1436
|
+
* Also filters out invalid fields.
|
|
1437
|
+
*/
|
|
1438
|
+
function getCustomTypeStaticFields(
|
|
1439
|
+
customType: CustomType,
|
|
1440
|
+
): [fieldId: string, field: NestableWidget | Group][] {
|
|
1441
|
+
return Object.values(customType.json).flatMap((tabFields) => {
|
|
1442
|
+
return Object.entries(tabFields).flatMap<[string, NestableWidget | Group]>(
|
|
1443
|
+
([fieldId, field]) => {
|
|
1444
|
+
return isValidField(fieldId, field) ? [[fieldId, field]] : [];
|
|
1445
|
+
},
|
|
1446
|
+
);
|
|
1447
|
+
});
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
/**
|
|
1451
|
+
* Flattens all custom type tabs and fields into a map of field ids to fields.
|
|
1452
|
+
* Also filters out invalid fields.
|
|
1453
|
+
*/
|
|
1454
|
+
function getCustomTypeStaticFieldsMap(
|
|
1455
|
+
customType: CustomType,
|
|
1456
|
+
): Record<string, NestableWidget | Group> {
|
|
1457
|
+
return Object.fromEntries(getCustomTypeStaticFields(customType));
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
function getGroupFieldFromMap(
|
|
1461
|
+
flattenFields: Record<string, NestableWidget | Group>,
|
|
1462
|
+
groupId: string,
|
|
1463
|
+
fieldId: string,
|
|
1464
|
+
) {
|
|
1465
|
+
const group = flattenFields[groupId];
|
|
1466
|
+
if (group === undefined || group.type !== "Group") return undefined;
|
|
1467
|
+
return group.config?.fields?.[fieldId];
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
function isValidField(
|
|
1471
|
+
fieldId: string,
|
|
1472
|
+
field: DynamicWidget,
|
|
1473
|
+
): field is NestableWidget | Group {
|
|
1474
|
+
return (
|
|
1475
|
+
field.type !== "Slices" &&
|
|
1476
|
+
field.type !== "Choice" &&
|
|
1477
|
+
// We don't display uid fields because they're a special field returned by
|
|
1478
|
+
// the API and they're not included in the document data object.
|
|
1479
|
+
// We also filter by key "uid", because (as of the time of writing this)
|
|
1480
|
+
// creating any field with that API id will result in it being used for
|
|
1481
|
+
// metadata, regardless of its type.
|
|
1482
|
+
field.type !== "UID" &&
|
|
1483
|
+
fieldId !== "uid"
|
|
1484
|
+
);
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
function getGroupFields(group: Group) {
|
|
1488
|
+
if (!group.config?.fields) return [];
|
|
1489
|
+
return Object.entries(group.config.fields).map(([fieldId, field]) => {
|
|
1490
|
+
return { fieldId, field: field as NestableWidget };
|
|
1491
|
+
});
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
/** If it's a string, return it, otherwise return the `id` property. */
|
|
1495
|
+
function getId<T extends string | { id: string }>(customType: T): string {
|
|
1496
|
+
if (typeof customType === "string") return customType;
|
|
1497
|
+
return customType.id;
|
|
1498
|
+
}
|