slice-machine-ui 2.16.2-beta.17 → 2.16.2-beta.3

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.
Files changed (68) hide show
  1. package/out/404.html +1 -1
  2. package/out/_next/static/WcTCr5PY9CMXQCakT6VQd/_buildManifest.js +1 -0
  3. package/out/_next/static/chunks/268-6d5fc7642a1b87c8.js +1 -0
  4. package/out/_next/static/chunks/34-2911c905c8a6e9d9.js +1 -0
  5. package/out/_next/static/chunks/630-93339694ef30b82d.js +1 -0
  6. package/out/_next/static/chunks/867-8164160c810122c6.js +1 -0
  7. package/out/_next/static/chunks/882-28837678beff7e51.js +1 -0
  8. package/out/_next/static/chunks/895-8c214ba470a4e23c.js +1 -0
  9. package/out/_next/static/chunks/{main-c46f4dcf6e3174bd.js → main-1c0e53194ff2e726.js} +1 -1
  10. package/out/_next/static/chunks/pages/_app-3fd13526dea9f1d4.js +657 -0
  11. package/out/_next/static/chunks/pages/changelog-80a618708f44f25f.js +1 -0
  12. package/out/_next/static/chunks/pages/changes-d40c17939854b984.js +1 -0
  13. package/out/_next/static/chunks/pages/custom-types/{[customTypeId]-a408f5a660e096a6.js → [customTypeId]-389d1b7a492fb3e7.js} +1 -1
  14. package/out/_next/static/chunks/pages/{custom-types-5acd56959b60346f.js → custom-types-2a5fd94ee42ba593.js} +1 -1
  15. package/out/_next/static/chunks/pages/{index-0d8cb369de720a35.js → index-02dd147957c8b40f.js} +1 -1
  16. package/out/_next/static/chunks/pages/labs-c6df252ea5d8fb6f.js +1 -0
  17. package/out/_next/static/chunks/pages/page-types/{[pageTypeId]-f5e851ebe35049a8.js → [pageTypeId]-3589bd1f9138a97b.js} +1 -1
  18. package/out/_next/static/chunks/pages/settings-170379902605f38a.js +1 -0
  19. package/out/_next/static/chunks/pages/slices/[lib]/[sliceName]/[variation]/simulator-063fa88ba75f483e.js +1 -0
  20. package/out/_next/static/chunks/pages/slices/[lib]/[sliceName]/[variation]-6de02b8ed13b680d.js +1 -0
  21. package/out/_next/static/chunks/pages/slices-071bb494907adf0f.js +1 -0
  22. package/out/_next/static/chunks/webpack-2f903acb0cccbf9e.js +1 -0
  23. package/out/_next/static/css/9c9a7de81f9ac811.css +1 -0
  24. package/out/changelog.html +1 -1
  25. package/out/changes.html +1 -1
  26. package/out/custom-types/[customTypeId].html +1 -1
  27. package/out/custom-types.html +1 -1
  28. package/out/index.html +1 -1
  29. package/out/labs.html +1 -1
  30. package/out/page-types/[pageTypeId].html +1 -1
  31. package/out/settings.html +1 -1
  32. package/out/slices/[lib]/[sliceName]/[variation]/simulator.html +1 -1
  33. package/out/slices/[lib]/[sliceName]/[variation].html +1 -1
  34. package/out/slices.html +1 -1
  35. package/package.json +8 -8
  36. package/src/features/customTypes/customTypesTable/CustomTypesTable.tsx +4 -2
  37. package/src/features/customTypes/customTypesTable/{useCustomTypesAutoRevalidation.tsx → useCustomTypes.ts} +34 -1
  38. package/src/legacy/lib/builders/common/Zone/Card/components/Hints/index.tsx +1 -12
  39. package/src/legacy/lib/models/common/widgets/ContentRelationship/Form.tsx +77 -35
  40. package/src/legacy/lib/models/common/widgets/ContentRelationship/index.ts +15 -46
  41. package/src/legacy/lib/models/common/widgets/Link/index.ts +1 -1
  42. package/src/utils/tracking/trackFieldAdded.ts +5 -2
  43. package/src/utils/tracking/trackFieldUpdated.ts +5 -2
  44. package/out/_next/static/YlBfAcqDSP3Tctj3WT7xv/_buildManifest.js +0 -1
  45. package/out/_next/static/chunks/248-03446cd9e9f13730.js +0 -1
  46. package/out/_next/static/chunks/268-6a9214b97195af9c.js +0 -1
  47. package/out/_next/static/chunks/33641354.3864aefb6106ae71.js +0 -28
  48. package/out/_next/static/chunks/34-e684c5fd75cc9dd0.js +0 -1
  49. package/out/_next/static/chunks/630-a8b2c2d022cc9450.js +0 -1
  50. package/out/_next/static/chunks/647-7b9b5aa9468f9e4b.js +0 -1
  51. package/out/_next/static/chunks/882-151468121d542ed6.js +0 -1
  52. package/out/_next/static/chunks/pages/_app-e3fa2a0a57c571ce.js +0 -708
  53. package/out/_next/static/chunks/pages/changelog-063c5e11dfc8fd55.js +0 -1
  54. package/out/_next/static/chunks/pages/changes-564336edb0ed18b0.js +0 -1
  55. package/out/_next/static/chunks/pages/labs-9630bfb1005be02b.js +0 -1
  56. package/out/_next/static/chunks/pages/settings-01f4aeb9112a1f87.js +0 -1
  57. package/out/_next/static/chunks/pages/slices/[lib]/[sliceName]/[variation]/simulator-5008e29008aa04f4.js +0 -1
  58. package/out/_next/static/chunks/pages/slices/[lib]/[sliceName]/[variation]-bd5e45632c419567.js +0 -1
  59. package/out/_next/static/chunks/pages/slices-4a60cd5f2c71327e.js +0 -1
  60. package/out/_next/static/chunks/webpack-b3522fdebabf510a.js +0 -1
  61. package/out/_next/static/css/cc9b10286400c2b9.css +0 -1
  62. package/src/features/builder/fields/contentRelationship/ContentRelationshipFieldPicker.tsx +0 -1279
  63. package/src/features/builder/fields/contentRelationship/Hint.tsx +0 -28
  64. package/src/features/builder/fields/contentRelationship/__tests__/ContentRelationshipFieldPicker.test.ts +0 -265
  65. package/src/features/customTypes/useCustomTypes.ts +0 -48
  66. package/src/utils/isValidObject.ts +0 -32
  67. package/src/utils/tracking/getLinkTrackingProperties.ts +0 -26
  68. /package/out/_next/static/{YlBfAcqDSP3Tctj3WT7xv → WcTCr5PY9CMXQCakT6VQd}/_ssgManifest.js +0 -0
@@ -1,1279 +0,0 @@
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
- Group,
26
- Link,
27
- LinkConfig,
28
- NestableWidget,
29
- } from "@prismicio/types-internal/lib/customtypes";
30
- import { useEffect } from "react";
31
-
32
- import { ErrorBoundary } from "@/ErrorBoundary";
33
- import {
34
- revalidateGetCustomTypes,
35
- useCustomTypes as useCustomTypesRequest,
36
- } from "@/features/customTypes/useCustomTypes";
37
- import { isValidObject } from "@/utils/isValidObject";
38
-
39
- type NonReadonly<T> = { -readonly [P in keyof T]: T[P] };
40
-
41
- /**
42
- * Picker fields check map types. Used internally to keep track of the checked
43
- * fields in the TreeView, as it's easier to handle objects than arrays and
44
- * also ensures field uniqueness.
45
- *
46
- * @example
47
- * {
48
- * author: {
49
- * fullName: {
50
- * type: "checkbox",
51
- * value: true,
52
- * },
53
- * awards: {
54
- * type: "group",
55
- * value: {
56
- * date: {
57
- * type: "checkbox",
58
- * value: true,
59
- * },
60
- * awardsCr: {
61
- * type: "contentRelationship",
62
- * value: {
63
- * award: {
64
- * title: {
65
- * type: "checkbox",
66
- * value: true,
67
- * },
68
- * issuer: {
69
- * type: "group",
70
- * value: {
71
- * name: {
72
- * type: "checkbox",
73
- * value: true,
74
- * },
75
- * },
76
- * },
77
- * },
78
- * },
79
- * },
80
- * },
81
- * },
82
- * professionCr: {
83
- * type: "contentRelationship",
84
- * value: {
85
- * profession: {
86
- * name: {
87
- * type: "checkbox",
88
- * value: true,
89
- * },
90
- * areas: {
91
- * type: "group",
92
- * value: {
93
- * name: {
94
- * type: "checkbox",
95
- * value: true,
96
- * },
97
- * },
98
- * },
99
- * },
100
- * },
101
- * },
102
- * },
103
- * }
104
- **/
105
- interface PickerCustomTypes {
106
- [customTypeId: string]: PickerCustomType;
107
- }
108
-
109
- interface PickerCustomType {
110
- [fieldId: string]: PickerCustomTypeValue;
111
- }
112
-
113
- type PickerCustomTypeValue =
114
- | PickerCheckboxField
115
- | PickerFirstLevelGroupField
116
- | PickerContentRelationshipField;
117
-
118
- interface PickerCheckboxField {
119
- type: "checkbox";
120
- value: boolean;
121
- }
122
-
123
- interface PickerFirstLevelGroupField {
124
- type: "group";
125
- value: PickerFirstLevelGroupFieldValue;
126
- }
127
-
128
- interface PickerLeafGroupField {
129
- type: "group";
130
- value: PickerLeafGroupFieldValue;
131
- }
132
-
133
- interface PickerLeafGroupFieldValue {
134
- [fieldId: string]: PickerCheckboxField;
135
- }
136
-
137
- interface PickerFirstLevelGroupFieldValue {
138
- [fieldId: string]: PickerCheckboxField | PickerContentRelationshipField;
139
- }
140
-
141
- interface PickerContentRelationshipField {
142
- type: "contentRelationship";
143
- value: PickerContentRelationshipFieldValue;
144
- }
145
-
146
- interface PickerContentRelationshipFieldValue {
147
- [customTypeId: string]: PickerNestedCustomTypeValue;
148
- }
149
- interface PickerNestedCustomTypeValue {
150
- [fieldId: string]: PickerCheckboxField | PickerLeafGroupField;
151
- }
152
-
153
- /**
154
- * Content relationship Link customtypes property structure.
155
- *
156
- * @example
157
- * [
158
- * {
159
- * id: "author",
160
- * fields: [
161
- * "fullName",
162
- * {
163
- * id: "awards",
164
- * fields: [
165
- * "date",
166
- * {
167
- * id: "awardsCr",
168
- * customtypes: [
169
- * {
170
- * id: "award",
171
- * fields: [
172
- * "title",
173
- * {
174
- * id: "issuer",
175
- * fields: ["name"],
176
- * },
177
- * ],
178
- * },
179
- * ],
180
- * },
181
- * ],
182
- * },
183
- * {
184
- * id: "professionCr",
185
- * customtypes: [
186
- * {
187
- * id: "profession",
188
- * fields: [
189
- * "name",
190
- * {
191
- * id: "areas",
192
- * fields: ["name"],
193
- * },
194
- * ],
195
- * },
196
- * ],
197
- * },
198
- * ],
199
- * },
200
- * ]
201
- */
202
- type LinkCustomtypes = NonNullable<LinkConfig["customtypes"]>;
203
-
204
- type LinkCustomtypesFields = Exclude<
205
- LinkCustomtypes[number],
206
- string
207
- >["fields"][number];
208
-
209
- type LinkCustomtypesContentRelationshipFieldValue = Exclude<
210
- LinkCustomtypesFields,
211
- string | { fields: unknown }
212
- >;
213
-
214
- type LinkCustomtypesGroupFieldValue = Exclude<
215
- LinkCustomtypesFields,
216
- string | { customtypes: unknown }
217
- >;
218
-
219
- interface ContentRelationshipFieldPickerProps {
220
- value: LinkCustomtypes | undefined;
221
- onChange: (fields: LinkCustomtypes) => void;
222
- }
223
-
224
- export function ContentRelationshipFieldPicker(
225
- props: ContentRelationshipFieldPickerProps,
226
- ) {
227
- return (
228
- <ErrorBoundary
229
- renderError={() => (
230
- <Box alignItems="center" gap={8}>
231
- <Icon name="alert" size="small" color="tomato10" />
232
- <Text color="tomato10">Error loading your types</Text>
233
- </Box>
234
- )}
235
- >
236
- <AnimatedSuspense
237
- fallback={
238
- <Box flexDirection="column" position="relative">
239
- <Skeleton height={240} />
240
- <Box
241
- position="absolute"
242
- top="50%"
243
- left="50%"
244
- transform="translate(-50%, -50%)"
245
- alignItems="center"
246
- gap={8}
247
- >
248
- <Icon name="autorenew" size="small" color="grey11" />
249
- <Text color="grey11">Loading your types...</Text>
250
- </Box>
251
- </Box>
252
- }
253
- >
254
- <ContentRelationshipFieldPickerContent {...props} />
255
- </AnimatedSuspense>
256
- </ErrorBoundary>
257
- );
258
- }
259
-
260
- function ContentRelationshipFieldPickerContent(
261
- props: ContentRelationshipFieldPickerProps,
262
- ) {
263
- const { value, onChange } = props;
264
- const { allCustomTypes, pickedCustomTypes } = useCustomTypes(value);
265
-
266
- const fieldCheckMap = value
267
- ? convertLinkCustomtypesToFieldCheckMap(value)
268
- : {};
269
-
270
- function onCustomTypesChange(id: string, newCustomType: PickerCustomType) {
271
- // The picker does not handle strings (custom type ids), as it's only meant
272
- // to pick fields from custom types (objects). So we need to merge it with
273
- // the existing value, which can have strings in the first level that
274
- // represent new types added without any picked fields.
275
- onChange(
276
- mergeAndConvertCheckMapToLinkCustomtypes({
277
- existingLinkCustomtypes: value,
278
- previousPickerCustomtypes: fieldCheckMap,
279
- customTypeId: id,
280
- newCustomType,
281
- }),
282
- );
283
- }
284
-
285
- function addCustomType(id: string) {
286
- const newFields = value ? [...value, id] : [id];
287
- onChange(newFields);
288
- }
289
-
290
- function removeCustomType(id: string) {
291
- if (value) {
292
- onChange(value.filter((existingCt) => getId(existingCt) !== id));
293
- }
294
- }
295
-
296
- return (
297
- <Box
298
- overflow="hidden"
299
- flexDirection="column"
300
- border
301
- borderRadius={6}
302
- width="100%"
303
- >
304
- <Box
305
- border={{ bottom: true }}
306
- padding={{ inline: 16, bottom: 16, top: 12 }}
307
- flexDirection="column"
308
- gap={8}
309
- >
310
- {pickedCustomTypes.length > 0 ? (
311
- <>
312
- <Box flexDirection="column">
313
- <Text variant="h4" color="grey12">
314
- Allowed type
315
- </Text>
316
- <Text color="grey11">
317
- Select a single type that editors can link to in the Page
318
- Builder.
319
- <br />
320
- For the selected type, choose which fields to include in the API
321
- response.
322
- </Text>
323
- {pickedCustomTypes.length > 1 && (
324
- <Box margin={{ block: 12 }}>
325
- <Alert
326
- color="warn"
327
- icon="alert"
328
- subtitle={
329
- <>
330
- <Text color="inherit" variant="bold">
331
- Legacy mode. Keep only one type to enable the improved
332
- Content Relationship feature.
333
- </Text>
334
- <br />
335
- <a
336
- href="https://prismic.io/docs/fields/content-relationship"
337
- target="_blank"
338
- rel="noopener noreferrer"
339
- style={{
340
- color: "inherit",
341
- textDecoration: "none",
342
- fontWeight: "bold",
343
- display: "flex",
344
- alignItems: "center",
345
- gap: 4,
346
- }}
347
- >
348
- <Text color="inherit" variant="bold">
349
- See documentation
350
- </Text>
351
- <Icon
352
- name="arrowForward"
353
- size="small"
354
- color="inherit"
355
- />
356
- </a>
357
- </>
358
- }
359
- />
360
- </Box>
361
- )}
362
- </Box>
363
- {pickedCustomTypes.map((customType) => (
364
- <Box
365
- key={customType.id}
366
- gap={4}
367
- padding={8}
368
- border
369
- borderRadius={6}
370
- borderColor="grey6"
371
- backgroundColor="white"
372
- justifyContent="space-between"
373
- >
374
- {pickedCustomTypes.length > 1 ? (
375
- <Text>{customType.id}</Text>
376
- ) : (
377
- <TreeView>
378
- <TreeViewCustomType
379
- customType={customType}
380
- onChange={(value) =>
381
- onCustomTypesChange(customType.id, value)
382
- }
383
- fieldCheckMap={fieldCheckMap[customType.id] ?? {}}
384
- allCustomTypes={allCustomTypes}
385
- />
386
- </TreeView>
387
- )}
388
-
389
- <IconButton
390
- icon="close"
391
- size="small"
392
- onClick={() => removeCustomType(customType.id)}
393
- sx={{ height: 24, width: 24 }}
394
- hiddenLabel="Remove type"
395
- />
396
- </Box>
397
- ))}
398
- </>
399
- ) : (
400
- <EmptyView onSelect={addCustomType} allCustomTypes={allCustomTypes} />
401
- )}
402
- </Box>
403
- <Box backgroundColor="white" flexDirection="column" padding={12}>
404
- <Text variant="normal" color="grey11">
405
- Have ideas for improving this field?{" "}
406
- <a
407
- href="https://community.prismic.io/t/content-relationship-share-your-requests-and-feedback/19843"
408
- target="_blank"
409
- rel="noopener noreferrer"
410
- style={{ color: "inherit", textDecoration: "underline" }}
411
- >
412
- Please provide your feedback here.
413
- </a>
414
- </Text>
415
- </Box>
416
- </Box>
417
- );
418
- }
419
-
420
- type EmptyViewProps = {
421
- onSelect: (customTypeId: string) => void;
422
- allCustomTypes: CustomType[];
423
- };
424
-
425
- function EmptyView(props: EmptyViewProps) {
426
- const { allCustomTypes, onSelect } = props;
427
-
428
- return (
429
- <Box
430
- flexDirection="column"
431
- gap={8}
432
- alignItems="center"
433
- padding={{ block: 24 }}
434
- >
435
- <Box flexDirection="column" alignItems="center" gap={4}>
436
- <Text variant="h5" color="grey12">
437
- No type selected
438
- </Text>
439
- <Text color="grey11" component="p" align="center">
440
- Select the type editors can link to.
441
- <br />
442
- Then, choose which fields to return in the API.
443
- </Text>
444
- </Box>
445
- <Box>
446
- <AddTypeButton allCustomTypes={allCustomTypes} onSelect={onSelect} />
447
- </Box>
448
- </Box>
449
- );
450
- }
451
-
452
- type AddTypeButtonProps = {
453
- onSelect: (customTypeId: string) => void;
454
- allCustomTypes: CustomType[];
455
- };
456
-
457
- function AddTypeButton(props: AddTypeButtonProps) {
458
- const { allCustomTypes, onSelect } = props;
459
-
460
- const triggerButton = (
461
- <Button startIcon="add" color="grey" disabled={allCustomTypes.length === 0}>
462
- Add type
463
- </Button>
464
- );
465
-
466
- const disabledButton = (
467
- <Box>
468
- <Tooltip
469
- content="No type available"
470
- side="bottom"
471
- align="start"
472
- disableHoverableContent
473
- >
474
- {triggerButton}
475
- </Tooltip>
476
- </Box>
477
- );
478
-
479
- if (allCustomTypes.length === 0) return disabledButton;
480
-
481
- return (
482
- <Box>
483
- <DropdownMenu>
484
- <DropdownMenuTrigger>{triggerButton}</DropdownMenuTrigger>
485
- <DropdownMenuContent maxHeight={400} minWidth={256} align="center">
486
- <DropdownMenuLabel>
487
- <Text color="grey11">Types</Text>
488
- </DropdownMenuLabel>
489
- {allCustomTypes.map((customType) => (
490
- <DropdownMenuItem
491
- key={customType.id}
492
- onSelect={() => onSelect(customType.id)}
493
- >
494
- <Box alignItems="center" justifyContent="space-between" gap={8}>
495
- <TextOverflow>
496
- <Text>{customType.id}</Text>
497
- </TextOverflow>
498
- <Badge
499
- title={
500
- <Text variant="extraSmall" color="purple11">
501
- {getTypeFormatLabel(customType.format)}
502
- </Text>
503
- }
504
- color="purple"
505
- size="small"
506
- />
507
- </Box>
508
- </DropdownMenuItem>
509
- ))}
510
- </DropdownMenuContent>
511
- </DropdownMenu>
512
- </Box>
513
- );
514
- }
515
-
516
- interface TreeViewCustomTypeProps {
517
- customType: CustomType;
518
- fieldCheckMap: PickerCustomType;
519
- onChange: (newValue: PickerCustomType) => void;
520
- allCustomTypes: CustomType[];
521
- }
522
-
523
- function TreeViewCustomType(props: TreeViewCustomTypeProps) {
524
- const {
525
- customType,
526
- fieldCheckMap: customTypeFieldsCheckMap,
527
- onChange: onCustomTypeChange,
528
- allCustomTypes,
529
- } = props;
530
-
531
- const renderedFields = getCustomTypeStaticFields(customType).map(
532
- ({ fieldId, field }) => {
533
- // Group field
534
-
535
- if (field.type === "Group") {
536
- const onGroupFieldChange = (
537
- newGroupFields: PickerFirstLevelGroupFieldValue,
538
- ) => {
539
- onCustomTypeChange({
540
- ...customTypeFieldsCheckMap,
541
- [fieldId]: { type: "group", value: newGroupFields },
542
- });
543
- };
544
-
545
- const groupFieldCheckMap = customTypeFieldsCheckMap[fieldId] ?? {};
546
-
547
- return (
548
- <TreeViewFirstLevelGroupField
549
- key={fieldId}
550
- group={field}
551
- groupId={fieldId}
552
- onChange={onGroupFieldChange}
553
- fieldCheckMap={
554
- groupFieldCheckMap.type === "group"
555
- ? groupFieldCheckMap.value
556
- : {}
557
- }
558
- allCustomTypes={allCustomTypes}
559
- />
560
- );
561
- }
562
-
563
- // Content relationship field with custom types
564
-
565
- if (isContentRelationshipFieldWithSingleCustomtype(field)) {
566
- const onContentRelationshipFieldChange = (
567
- newCrFields: PickerContentRelationshipFieldValue,
568
- ) => {
569
- onCustomTypeChange({
570
- ...customTypeFieldsCheckMap,
571
- [fieldId]: {
572
- type: "contentRelationship",
573
- value: newCrFields,
574
- },
575
- });
576
- };
577
-
578
- const crFieldCheckMap = customTypeFieldsCheckMap[fieldId] ?? {};
579
-
580
- return (
581
- <TreeViewContentRelationshipField
582
- key={fieldId}
583
- field={field}
584
- fieldId={fieldId}
585
- onChange={onContentRelationshipFieldChange}
586
- fieldCheckMap={
587
- crFieldCheckMap.type === "contentRelationship"
588
- ? crFieldCheckMap.value
589
- : {}
590
- }
591
- allCustomTypes={allCustomTypes}
592
- />
593
- );
594
- }
595
-
596
- // Regular field
597
-
598
- const onCheckedChange = (newValue: boolean) => {
599
- onCustomTypeChange({
600
- ...customTypeFieldsCheckMap,
601
- [fieldId]: { type: "checkbox", value: newValue },
602
- });
603
- };
604
-
605
- return (
606
- <TreeViewCheckbox
607
- key={fieldId}
608
- title={fieldId}
609
- checked={customTypeFieldsCheckMap[fieldId]?.value === true}
610
- onCheckedChange={onCheckedChange}
611
- />
612
- );
613
- },
614
- );
615
-
616
- const exposedFieldsCount = countPickedFields(customTypeFieldsCheckMap);
617
-
618
- return (
619
- <TreeViewSection
620
- key={customType.id}
621
- title={customType.id}
622
- subtitle={
623
- exposedFieldsCount.pickedFields > 0
624
- ? getPickedFieldsLabel(
625
- exposedFieldsCount.pickedFields,
626
- "returned in the API",
627
- )
628
- : "(No fields returned in the API)"
629
- }
630
- badge={getTypeFormatLabel(customType.format)}
631
- defaultOpen
632
- >
633
- {renderedFields.length > 0 ? renderedFields : <NoFieldsAvailable />}
634
- </TreeViewSection>
635
- );
636
- }
637
-
638
- interface TreeViewContentRelationshipFieldProps {
639
- fieldId: string;
640
- field: Link;
641
- fieldCheckMap: PickerContentRelationshipFieldValue;
642
- onChange: (newValue: PickerContentRelationshipFieldValue) => void;
643
- allCustomTypes: CustomType[];
644
- }
645
-
646
- function TreeViewContentRelationshipField(
647
- props: TreeViewContentRelationshipFieldProps,
648
- ) {
649
- const {
650
- field,
651
- fieldId,
652
- fieldCheckMap: crFieldsCheckMap,
653
- onChange: onCrFieldChange,
654
- allCustomTypes,
655
- } = props;
656
-
657
- if (!field.config?.customtypes) return null;
658
-
659
- const resolvedCustomTypes = resolveContentRelationshipCustomTypes(
660
- field.config.customtypes,
661
- allCustomTypes,
662
- );
663
-
664
- if (resolvedCustomTypes.length === 0) return null;
665
-
666
- return (
667
- <TreeViewSection
668
- title={fieldId}
669
- subtitle={getPickedFieldsLabel(
670
- countPickedFields(crFieldsCheckMap).pickedFields,
671
- )}
672
- >
673
- {resolvedCustomTypes.map((customType) => {
674
- if (typeof customType === "string") return null;
675
-
676
- const onNestedCustomTypeChange = (
677
- newNestedCustomTypeFields: PickerNestedCustomTypeValue,
678
- ) => {
679
- onCrFieldChange({
680
- ...crFieldsCheckMap,
681
- [customType.id]: newNestedCustomTypeFields,
682
- });
683
- };
684
-
685
- const nestedCtFieldsCheckMap = crFieldsCheckMap[customType.id] ?? {};
686
-
687
- const renderedFields = getCustomTypeStaticFields(customType).map(
688
- ({ fieldId, field }) => {
689
- // Group field
690
-
691
- if (field.type === "Group") {
692
- const onGroupFieldsChange = (
693
- newGroupFields: PickerLeafGroupFieldValue,
694
- ) => {
695
- onNestedCustomTypeChange({
696
- ...nestedCtFieldsCheckMap,
697
- [fieldId]: { type: "group", value: newGroupFields },
698
- });
699
- };
700
-
701
- const groupFieldCheckMap = nestedCtFieldsCheckMap[fieldId] ?? {};
702
-
703
- return (
704
- <TreeViewLeafGroupField
705
- key={fieldId}
706
- group={field}
707
- groupId={fieldId}
708
- onChange={onGroupFieldsChange}
709
- fieldCheckMap={
710
- groupFieldCheckMap.type === "group"
711
- ? groupFieldCheckMap.value
712
- : {}
713
- }
714
- />
715
- );
716
- }
717
-
718
- // Regular field
719
-
720
- const onCheckedChange = (newChecked: boolean) => {
721
- onNestedCustomTypeChange({
722
- ...nestedCtFieldsCheckMap,
723
- [fieldId]: { type: "checkbox", value: newChecked },
724
- });
725
- };
726
-
727
- return (
728
- <TreeViewCheckbox
729
- key={fieldId}
730
- title={fieldId}
731
- checked={nestedCtFieldsCheckMap[fieldId]?.value === true}
732
- onCheckedChange={onCheckedChange}
733
- />
734
- );
735
- },
736
- );
737
-
738
- return (
739
- <TreeViewSection
740
- key={customType.id}
741
- title={customType.id}
742
- subtitle={getPickedFieldsLabel(
743
- countPickedFields(nestedCtFieldsCheckMap).pickedFields,
744
- )}
745
- badge={getTypeFormatLabel(customType.format)}
746
- >
747
- {renderedFields.length > 0 ? renderedFields : <NoFieldsAvailable />}
748
- </TreeViewSection>
749
- );
750
- })}
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(value: 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 (!value) {
917
- return {
918
- allCustomTypes,
919
- pickedCustomTypes: [],
920
- };
921
- }
922
-
923
- const pickedCustomTypes = value.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
- localCustomTypes: CustomType[],
936
- ): CustomType[] {
937
- return linkCustomtypes.flatMap((linkCustomtype) => {
938
- return localCustomTypes.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(
947
- customTypes: LinkCustomtypes,
948
- ): PickerCustomTypes {
949
- return customTypes.reduce<PickerCustomTypes>((customTypes, customType) => {
950
- if (typeof customType === "string") return customTypes;
951
-
952
- customTypes[customType.id] = customType.fields.reduce<PickerCustomType>(
953
- (customTypeFields, field) => {
954
- if (typeof field === "string") {
955
- // Regular field
956
- customTypeFields[field] = { type: "checkbox", value: true };
957
- } else if ("fields" in field && field.fields !== undefined) {
958
- // Group field
959
- customTypeFields[field.id] = createGroupFieldCheckMap(field);
960
- } else if ("customtypes" in field && field.customtypes !== undefined) {
961
- // Content relationship field
962
- customTypeFields[field.id] =
963
- createContentRelationshipFieldCheckMap(field);
964
- }
965
-
966
- return customTypeFields;
967
- },
968
- {},
969
- );
970
- return customTypes;
971
- }, {});
972
- }
973
-
974
- function createGroupFieldCheckMap(
975
- group: LinkCustomtypesGroupFieldValue,
976
- ): PickerFirstLevelGroupField {
977
- return {
978
- type: "group",
979
- value: group.fields.reduce<PickerFirstLevelGroupFieldValue>(
980
- (fields, field) => {
981
- if (typeof field === "string") {
982
- // Regular field
983
- fields[field] = { type: "checkbox", value: true };
984
- } else if ("customtypes" in field && field.customtypes !== undefined) {
985
- // Content relationship field
986
- fields[field.id] = createContentRelationshipFieldCheckMap(field);
987
- }
988
-
989
- return fields;
990
- },
991
- {},
992
- ),
993
- };
994
- }
995
-
996
- function createContentRelationshipFieldCheckMap(
997
- field: LinkCustomtypesContentRelationshipFieldValue,
998
- ): PickerContentRelationshipField {
999
- const crField: PickerContentRelationshipField = {
1000
- type: "contentRelationship",
1001
- value: {},
1002
- };
1003
- const crFieldCustomTypes = crField.value;
1004
-
1005
- for (const customType of field.customtypes) {
1006
- if (typeof customType === "string") continue;
1007
-
1008
- crFieldCustomTypes[customType.id] ??= {};
1009
- const customTypeFields = crFieldCustomTypes[customType.id];
1010
-
1011
- for (const nestedField of customType.fields) {
1012
- if (typeof nestedField === "string") {
1013
- // Regular field
1014
- customTypeFields[nestedField] = { type: "checkbox", value: true };
1015
- } else {
1016
- // Group field
1017
- const groupFieldsEntries = nestedField.fields.map(
1018
- (field) => [field, { type: "checkbox", value: true }] as const,
1019
- );
1020
-
1021
- if (groupFieldsEntries.length > 0) {
1022
- customTypeFields[nestedField.id] = {
1023
- type: "group",
1024
- value: Object.fromEntries(groupFieldsEntries),
1025
- };
1026
- }
1027
- }
1028
- }
1029
- }
1030
-
1031
- return crField;
1032
- }
1033
-
1034
- /**
1035
- * Merges the existing Link `customtypes` array with the picker state, ensuring
1036
- * that conversions from to string (custom type id) to object and vice versa are
1037
- * made correctly and that the order is preserved.
1038
- */
1039
- function mergeAndConvertCheckMapToLinkCustomtypes(args: {
1040
- existingLinkCustomtypes: LinkCustomtypes | undefined;
1041
- previousPickerCustomtypes: PickerCustomTypes;
1042
- newCustomType: PickerCustomType;
1043
- customTypeId: string;
1044
- }): LinkCustomtypes {
1045
- const {
1046
- existingLinkCustomtypes,
1047
- previousPickerCustomtypes,
1048
- newCustomType,
1049
- customTypeId,
1050
- } = args;
1051
-
1052
- const result: NonReadonly<LinkCustomtypes> = [];
1053
- const pickerLinkCustomtypes = convertFieldCheckMapToLinkCustomtypes({
1054
- ...previousPickerCustomtypes,
1055
- [customTypeId]: newCustomType,
1056
- });
1057
-
1058
- if (!existingLinkCustomtypes) return pickerLinkCustomtypes;
1059
-
1060
- for (const existingLinkCt of existingLinkCustomtypes) {
1061
- const existingPickerLinkCt = pickerLinkCustomtypes.find((ct) => {
1062
- return getId(ct) === getId(existingLinkCt);
1063
- });
1064
-
1065
- if (existingPickerLinkCt !== undefined) {
1066
- // Custom type with exposed fields, keep the customtypes object
1067
- result.push(existingPickerLinkCt);
1068
- } else if (getId(existingLinkCt) === customTypeId) {
1069
- // Custom type that had exposed fields, but now has none, change to string
1070
- result.push(getId(existingLinkCt));
1071
- } else {
1072
- // Custom type without exposed fields, keep the string
1073
- result.push(existingLinkCt);
1074
- }
1075
- }
1076
-
1077
- return result;
1078
- }
1079
-
1080
- /**
1081
- * Converts a picker fields check map structure ({@link PickerCustomTypes}) into
1082
- * Link config `customtypes` ({@link LinkCustomtypes}) and filter out empty Custom
1083
- * types.
1084
- */
1085
- function convertFieldCheckMapToLinkCustomtypes(
1086
- checkMap: PickerCustomTypes,
1087
- ): LinkCustomtypes {
1088
- return Object.entries(checkMap).flatMap<LinkCustomtypes[number]>(
1089
- ([ctId, ctFields]) => {
1090
- const fields = Object.entries(ctFields).flatMap<
1091
- | string
1092
- | LinkCustomtypesContentRelationshipFieldValue
1093
- | LinkCustomtypesGroupFieldValue
1094
- >(([fieldId, fieldValue]) => {
1095
- // First level group field
1096
- if (fieldValue.type === "group") {
1097
- const fields = Object.entries(fieldValue.value).flatMap<
1098
- string | LinkCustomtypesContentRelationshipFieldValue
1099
- >(([fieldId, fieldValue]) => {
1100
- if (fieldValue.type === "checkbox") {
1101
- return fieldValue.value ? fieldId : [];
1102
- }
1103
-
1104
- const customTypes = createContentRelationshipLinkCustomtypes(
1105
- fieldValue.value,
1106
- );
1107
-
1108
- return customTypes.length > 0
1109
- ? { id: fieldId, customtypes: customTypes }
1110
- : [];
1111
- });
1112
-
1113
- return fields.length > 0 ? { id: fieldId, fields } : [];
1114
- }
1115
-
1116
- // Content relationship field
1117
- if (fieldValue.type === "contentRelationship") {
1118
- const customTypes = createContentRelationshipLinkCustomtypes(
1119
- fieldValue.value,
1120
- );
1121
-
1122
- return customTypes.length > 0
1123
- ? { id: fieldId, customtypes: customTypes }
1124
- : [];
1125
- }
1126
-
1127
- // Regular field
1128
- return fieldValue.value ? fieldId : [];
1129
- });
1130
-
1131
- return fields.length > 0 ? { id: ctId, fields } : [];
1132
- },
1133
- );
1134
- }
1135
-
1136
- function createContentRelationshipLinkCustomtypes(
1137
- value: PickerContentRelationshipFieldValue,
1138
- ): LinkCustomtypesContentRelationshipFieldValue["customtypes"] {
1139
- return Object.entries(value).flatMap(
1140
- ([nestedCustomTypeId, nestedCustomTypeFields]) => {
1141
- const fields = Object.entries(nestedCustomTypeFields).flatMap(
1142
- ([nestedFieldId, nestedFieldValue]) => {
1143
- // Leaf group field
1144
- if (nestedFieldValue.type === "group") {
1145
- const nestedGroupFields = Object.entries(
1146
- nestedFieldValue.value,
1147
- ).flatMap<string>(([fieldId, fieldValue]) => {
1148
- // Regular field
1149
- return fieldValue.type === "checkbox" && fieldValue.value
1150
- ? fieldId
1151
- : [];
1152
- });
1153
-
1154
- return nestedGroupFields.length > 0
1155
- ? { id: nestedFieldId, fields: nestedGroupFields }
1156
- : [];
1157
- }
1158
-
1159
- return nestedFieldValue.value ? nestedFieldId : [];
1160
- },
1161
- );
1162
-
1163
- return fields.length > 0 ? { id: nestedCustomTypeId, fields } : [];
1164
- },
1165
- );
1166
- }
1167
-
1168
- type CountPickedFieldsResult = {
1169
- pickedFields: number;
1170
- nestedPickedFields: number;
1171
- };
1172
-
1173
- /**
1174
- * Generic recursive function that goes down the fields check map and counts all
1175
- * the properties that are set to true, which correspond to selected fields.
1176
- *
1177
- * Distinguishes between all picked fields and nested picked fields within a
1178
- * content relationship field.
1179
- *
1180
- * It's not type safe, but checks the type of the values at runtime so that
1181
- * it only recurses into valid objects, and only counts checkbox fields.
1182
- */
1183
- export function countPickedFields(
1184
- fields: Record<string, unknown> | undefined,
1185
- isNested = false,
1186
- ): CountPickedFieldsResult {
1187
- if (!fields) return { pickedFields: 0, nestedPickedFields: 0 };
1188
-
1189
- return Object.values(fields).reduce<CountPickedFieldsResult>(
1190
- (result, value) => {
1191
- if (!isValidObject(value)) return result;
1192
-
1193
- if ("type" in value && value.type === "checkbox") {
1194
- const isChecked = Boolean(value.value);
1195
- if (!isChecked) return result;
1196
-
1197
- return {
1198
- pickedFields: result.pickedFields + 1,
1199
- nestedPickedFields: result.nestedPickedFields + (isNested ? 1 : 0),
1200
- };
1201
- }
1202
-
1203
- if ("type" in value && value.type === "contentRelationship") {
1204
- const { pickedFields, nestedPickedFields } = countPickedFields(
1205
- value,
1206
- true,
1207
- );
1208
-
1209
- return {
1210
- pickedFields: result.pickedFields + pickedFields,
1211
- nestedPickedFields: result.nestedPickedFields + nestedPickedFields,
1212
- };
1213
- }
1214
-
1215
- const { pickedFields, nestedPickedFields } = countPickedFields(
1216
- value,
1217
- isNested,
1218
- );
1219
-
1220
- return {
1221
- pickedFields: result.pickedFields + pickedFields,
1222
- nestedPickedFields: result.nestedPickedFields + nestedPickedFields,
1223
- };
1224
- },
1225
- {
1226
- pickedFields: 0,
1227
- nestedPickedFields: 0,
1228
- },
1229
- );
1230
- }
1231
-
1232
- /**
1233
- * Check if the field is a Content Relationship Link with a **single** custom
1234
- * type. CRs with multiple custom types are not currently supported (legacy).
1235
- */
1236
- function isContentRelationshipFieldWithSingleCustomtype(
1237
- field: NestableWidget | Group,
1238
- ): field is Link {
1239
- return !!(
1240
- field.type === "Link" &&
1241
- field.config?.select === "document" &&
1242
- field.config?.customtypes &&
1243
- field.config.customtypes.length === 1
1244
- );
1245
- }
1246
-
1247
- function getCustomTypeStaticFields(customType: CustomType) {
1248
- return Object.values(customType.json).flatMap((tabFields) => {
1249
- return Object.entries(tabFields).flatMap(([fieldId, field]) => {
1250
- if (
1251
- field.type !== "Slices" &&
1252
- field.type !== "Choice" &&
1253
- // Filter out uid fields because it's a special field returned by the
1254
- // API and is not part of the data object in the document.
1255
- // We also filter by key "uid", because (as of the time of writing
1256
- // this), creating any field with that API id will result in it being
1257
- // used for metadata.
1258
- (field.type !== "UID" || fieldId !== "uid")
1259
- ) {
1260
- return { fieldId, field: field as NestableWidget | Group };
1261
- }
1262
-
1263
- return [];
1264
- });
1265
- });
1266
- }
1267
-
1268
- function getGroupFields(group: Group) {
1269
- if (!group.config?.fields) return [];
1270
- return Object.entries(group.config.fields).map(([fieldId, field]) => {
1271
- return { fieldId, field: field as NestableWidget };
1272
- });
1273
- }
1274
-
1275
- /** If it's a string, return it, otherwise return the `id` property. */
1276
- function getId<T extends string | { id: string }>(customType: T): string {
1277
- if (typeof customType === "string") return customType;
1278
- return customType.id;
1279
- }