slice-machine-ui 2.16.2-alpha.jp-cr-ui-types-internal-version.1 → 2.16.2-alpha.jp-cr-ui-legacy-and-new-picker.2

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 (34) hide show
  1. package/out/404.html +1 -1
  2. package/out/_next/static/{bjRIqA2BKLEQ0oCFi096L → YQMbzR_y6FmVlx0NEHprH}/_buildManifest.js +1 -1
  3. package/out/_next/static/chunks/{268-046fd0c9d105117f.js → 268-6a9214b97195af9c.js} +1 -1
  4. package/out/_next/static/chunks/33641354.3864aefb6106ae71.js +28 -0
  5. package/out/_next/static/chunks/{34-7e02d3123542d645.js → 34-e684c5fd75cc9dd0.js} +1 -1
  6. package/out/_next/static/chunks/{630-6b2adc6e86debe3d.js → 630-db2634510e265e9a.js} +1 -1
  7. package/out/_next/static/chunks/{867-3f1b306bb3e20dec.js → 867-bb2db4d365ee7e40.js} +1 -1
  8. package/out/_next/static/chunks/{882-34af0755031a0763.js → 882-636039ab926dbc22.js} +1 -1
  9. package/out/_next/static/chunks/{895-63d0e67d0014ea28.js → 895-e5bf82a4d5d7c3e1.js} +1 -1
  10. package/out/_next/static/chunks/pages/{_app-80da54270aab8938.js → _app-a0f5d6072fbbb633.js} +64 -64
  11. package/out/_next/static/chunks/pages/{changelog-1f45bbf9399626c7.js → changelog-063c5e11dfc8fd55.js} +1 -1
  12. package/out/_next/static/chunks/pages/{changes-f331fca898fc0c3a.js → changes-564336edb0ed18b0.js} +1 -1
  13. package/out/_next/static/chunks/pages/{labs-7cf1a8180d15bc81.js → labs-9630bfb1005be02b.js} +1 -1
  14. package/out/_next/static/chunks/pages/{settings-9d6e23720fd95d41.js → settings-01f4aeb9112a1f87.js} +1 -1
  15. package/out/_next/static/chunks/pages/slices/[lib]/[sliceName]/[variation]/{simulator-35c5ab99351f266a.js → simulator-5008e29008aa04f4.js} +1 -1
  16. package/out/_next/static/chunks/pages/slices/[lib]/[sliceName]/{[variation]-b89f6513d4bdea03.js → [variation]-85ab73c10f8322e1.js} +1 -1
  17. package/out/_next/static/chunks/pages/{slices-097e95abd417dd2d.js → slices-4a60cd5f2c71327e.js} +1 -1
  18. package/out/_next/static/chunks/{webpack-a75559e2f94885b4.js → webpack-b3522fdebabf510a.js} +1 -1
  19. package/out/_next/static/css/{7393c1f29ee22bf7.css → cc9b10286400c2b9.css} +1 -1
  20. package/out/changelog.html +1 -1
  21. package/out/changes.html +1 -1
  22. package/out/custom-types/[customTypeId].html +1 -1
  23. package/out/custom-types.html +1 -1
  24. package/out/index.html +1 -1
  25. package/out/labs.html +1 -1
  26. package/out/page-types/[pageTypeId].html +1 -1
  27. package/out/settings.html +1 -1
  28. package/out/slices/[lib]/[sliceName]/[variation]/simulator.html +1 -1
  29. package/out/slices/[lib]/[sliceName]/[variation].html +1 -1
  30. package/out/slices.html +1 -1
  31. package/package.json +6 -6
  32. package/src/features/customTypes/fields/ContentRelationshipFieldPicker.tsx +721 -387
  33. package/out/_next/static/chunks/e1b0e87a.19cd118baf725e50.js +0 -28
  34. /package/out/_next/static/{bjRIqA2BKLEQ0oCFi096L → YQMbzR_y6FmVlx0NEHprH}/_ssgManifest.js +0 -0
@@ -1,23 +1,44 @@
1
1
  import { pluralize } from "@prismicio/editor-support/String";
2
+ import { revalidateData, useRequest } from "@prismicio/editor-support/Suspense";
2
3
  import {
4
+ AnimatedSuspense,
5
+ Badge,
3
6
  Box,
7
+ Button,
8
+ DropdownMenu,
9
+ DropdownMenuContent,
10
+ DropdownMenuItem,
11
+ DropdownMenuLabel,
12
+ DropdownMenuTrigger,
13
+ Icon,
14
+ IconButton,
15
+ Skeleton,
4
16
  Text,
17
+ TextOverflow,
18
+ Tooltip,
5
19
  TreeView,
6
20
  TreeViewCheckbox,
7
21
  TreeViewSection,
8
22
  } from "@prismicio/editor-ui";
9
- import { UID } from "@prismicio/types-internal/lib/customtypes";
10
- import { useMemo } from "react";
11
- import { useSelector } from "react-redux";
12
-
13
- import { CustomTypeSM, TabFields } from "@/legacy/lib/models/common/CustomType";
14
- import { selectAllCustomTypes } from "@/modules/availableCustomTypes";
23
+ import {
24
+ CustomType,
25
+ Group,
26
+ Link,
27
+ LinkConfig,
28
+ NestableWidget,
29
+ } from "@prismicio/types-internal/lib/customtypes";
30
+ import { useEffect, useState } from "react";
31
+
32
+ import { ErrorBoundary } from "@/ErrorBoundary";
33
+ import { managerClient } from "@/managerClient";
15
34
  import { isValidObject } from "@/utils/isValidObject";
16
35
 
36
+ type NonReadonly<T> = { -readonly [P in keyof T]: T[P] };
37
+
17
38
  /**
18
39
  * Picker fields check map types. Used internally to keep track of the checked
19
40
  * fields in the TreeView, as it's easier to handle objects than arrays and
20
- * also ensure field uniqueness.
41
+ * also ensures field uniqueness.
21
42
  *
22
43
  * @example
23
44
  * {
@@ -127,7 +148,7 @@ interface PickerNestedCustomTypeValue {
127
148
  }
128
149
 
129
150
  /**
130
- * Copy of types-internal Link customtypes.
151
+ * Content relationship Link customtypes property structure.
131
152
  *
132
153
  * @example
133
154
  * [
@@ -175,87 +196,174 @@ interface PickerNestedCustomTypeValue {
175
196
  * },
176
197
  * ]
177
198
  */
178
- type TICustomTypes = readonly (string | TICustomType)[];
179
-
180
- interface TICustomType {
181
- id: string;
182
- fields: readonly (
183
- | string
184
- | TIContentRelationshipFieldValue
185
- | TIGroupFieldValues
186
- )[];
187
- }
199
+ type LinkCustomtypes = NonNullable<LinkConfig["customtypes"]>;
188
200
 
189
- interface TICustomTypeRegularFieldValues {
190
- id: string;
191
- fields: readonly string[];
192
- }
201
+ type LinkCustomtypesFields = Exclude<
202
+ LinkCustomtypes[number],
203
+ string
204
+ >["fields"][number];
193
205
 
194
- interface TIGroupFieldValues {
195
- id: string;
196
- fields: readonly (string | TIContentRelationshipFieldValue)[];
197
- }
206
+ type LinkCustomtypesContentRelationshipFieldValue = Exclude<
207
+ LinkCustomtypesFields,
208
+ string | { fields: unknown }
209
+ >;
198
210
 
199
- interface TIContentRelationshipFieldValue {
200
- id: string;
201
- customtypes: readonly (string | TICustomTypeFieldValues)[];
202
- }
203
-
204
- interface TICustomTypeFieldValues {
205
- id: string;
206
- fields: readonly (string | TICustomTypeRegularFieldValues)[];
207
- }
211
+ type LinkCustomtypesGroupFieldValue = Exclude<
212
+ LinkCustomtypesFields,
213
+ string | { customtypes: unknown }
214
+ >;
208
215
 
209
216
  interface ContentRelationshipFieldPickerProps {
210
- value: TICustomTypes | undefined;
211
- onChange: (fields: TICustomTypes) => void;
217
+ value: LinkCustomtypes | undefined;
218
+ onChange: (fields: LinkCustomtypes) => void;
212
219
  }
213
220
 
214
221
  export function ContentRelationshipFieldPicker(
215
222
  props: ContentRelationshipFieldPickerProps,
223
+ ) {
224
+ return (
225
+ <ErrorBoundary
226
+ renderError={() => (
227
+ <Box alignItems="center" gap={8}>
228
+ <Icon name="alert" size="small" color="tomato10" />
229
+ <Text color="tomato10">Error loading your types</Text>
230
+ </Box>
231
+ )}
232
+ >
233
+ <AnimatedSuspense
234
+ fallback={
235
+ <Box flexDirection="column" position="relative">
236
+ <Skeleton height={240} />
237
+ <Box
238
+ position="absolute"
239
+ top="50%"
240
+ left="50%"
241
+ transform="translate(-50%, -50%)"
242
+ alignItems="center"
243
+ gap={8}
244
+ >
245
+ <Icon name="autorenew" size="small" color="grey11" />
246
+ <Text color="grey11">Loading your types...</Text>
247
+ </Box>
248
+ </Box>
249
+ }
250
+ >
251
+ <ContentRelationshipFieldPickerContent {...props} />
252
+ </AnimatedSuspense>
253
+ </ErrorBoundary>
254
+ );
255
+ }
256
+
257
+ function ContentRelationshipFieldPickerContent(
258
+ props: ContentRelationshipFieldPickerProps,
216
259
  ) {
217
260
  const { value, onChange } = props;
218
- const customTypes = useCustomTypes();
219
- const fieldCheckMap = value ? convertCustomTypesToFieldCheckMap(value) : {};
261
+ const { allCustomTypes, availableCustomTypes, pickedCustomTypes } =
262
+ useCustomTypes(value);
263
+
264
+ const [isNewType, setIsNewType] = useState(false);
265
+
266
+ const fieldCheckMap = value
267
+ ? convertLinkCustomtypesToFieldCheckMap(value)
268
+ : {};
220
269
 
221
- function onCustomTypesChange(customTypeId: string, value: PickerCustomType) {
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.
222
275
  onChange(
223
- convertFieldCheckMapToCustomTypes({
224
- ...fieldCheckMap,
225
- [customTypeId]: value,
276
+ mergeAndConvertCheckMapToLinkCustomtypes({
277
+ existingLinkCustomtypes: value,
278
+ previousPickerCustomtypes: fieldCheckMap,
279
+ customTypeId: id,
280
+ newCustomType,
226
281
  }),
227
282
  );
228
283
  }
229
284
 
285
+ function addCustomType(id: string) {
286
+ setIsNewType(true);
287
+ onChange([...(value ?? []), id]);
288
+ }
289
+
290
+ function removeCustomType(id: string) {
291
+ if (value) {
292
+ onChange(value.filter((existingCt) => getId(existingCt) !== id));
293
+ }
294
+ }
295
+
230
296
  return (
231
- <Box overflow="hidden" flexDirection="column" border borderRadius={6}>
297
+ <Box
298
+ overflow="hidden"
299
+ flexDirection="column"
300
+ border
301
+ borderRadius={6}
302
+ width="100%"
303
+ >
232
304
  <Box
233
305
  border={{ bottom: true }}
234
306
  padding={{ inline: 16, bottom: 16, top: 12 }}
235
307
  flexDirection="column"
236
308
  gap={8}
237
309
  >
238
- <Box flexDirection="column">
239
- <Text variant="h4" color="grey12">
240
- Types
241
- </Text>
242
- <Text color="grey12">
243
- Choose which fields you want to expose from the linked document.
244
- </Text>
245
- </Box>
246
- <TreeView
247
- title="Exposed fields"
248
- subtitle={`(${countPickedFields(fieldCheckMap)})`}
249
- >
250
- {customTypes.map((customType) => (
251
- <TreeViewCustomType
252
- key={customType.id}
253
- customType={customType}
254
- onChange={(value) => onCustomTypesChange(customType.id, value)}
255
- fieldCheckMap={fieldCheckMap[customType.id] ?? {}}
310
+ {pickedCustomTypes.length > 0 ? (
311
+ <>
312
+ <Box flexDirection="column">
313
+ <Text variant="h4" color="grey12">
314
+ Allowed Types
315
+ </Text>
316
+ <Text color="grey11">
317
+ Restrict the selection to specific types your content editors
318
+ can link to in the Page Builder.
319
+ <br />
320
+ For each type, choose which fields to expose in the API
321
+ response.
322
+ </Text>
323
+ </Box>
324
+ {pickedCustomTypes.map((customType) => (
325
+ <Box
326
+ key={customType.id}
327
+ gap={4}
328
+ padding={8}
329
+ border
330
+ borderRadius={6}
331
+ borderColor="grey6"
332
+ backgroundColor="white"
333
+ justifyContent="space-between"
334
+ >
335
+ <TreeView>
336
+ <TreeViewCustomType
337
+ customType={customType}
338
+ onChange={(value) =>
339
+ onCustomTypesChange(customType.id, value)
340
+ }
341
+ fieldCheckMap={fieldCheckMap[customType.id] ?? {}}
342
+ allCustomTypes={allCustomTypes}
343
+ isNewType={isNewType}
344
+ />
345
+ </TreeView>
346
+ <IconButton
347
+ icon="close"
348
+ size="small"
349
+ onClick={() => removeCustomType(customType.id)}
350
+ sx={{ height: 24, width: 24 }}
351
+ hiddenLabel="Remove type"
352
+ />
353
+ </Box>
354
+ ))}
355
+ <AddTypeButton
356
+ onSelect={addCustomType}
357
+ pickedCustomTypes={pickedCustomTypes}
358
+ availableCustomTypes={availableCustomTypes}
256
359
  />
257
- ))}
258
- </TreeView>
360
+ </>
361
+ ) : (
362
+ <EmptyView
363
+ onSelect={addCustomType}
364
+ availableCustomTypes={availableCustomTypes}
365
+ />
366
+ )}
259
367
  </Box>
260
368
  <Box backgroundColor="white" flexDirection="column" padding={12}>
261
369
  <Text variant="normal" color="grey11">
@@ -275,132 +383,276 @@ export function ContentRelationshipFieldPicker(
275
383
  );
276
384
  }
277
385
 
386
+ type EmptyViewProps = {
387
+ onSelect: (customTypeId: string) => void;
388
+ availableCustomTypes: CustomType[];
389
+ };
390
+
391
+ function EmptyView(props: EmptyViewProps) {
392
+ const { availableCustomTypes, onSelect } = props;
393
+
394
+ return (
395
+ <Box
396
+ flexDirection="column"
397
+ gap={8}
398
+ alignItems="center"
399
+ padding={{ block: 24 }}
400
+ >
401
+ <Box flexDirection="column" alignItems="center" gap={4}>
402
+ <Text variant="h5" color="grey12">
403
+ No types selected yet.
404
+ </Text>
405
+ <Text color="grey11" component="p" align="center">
406
+ Add one or more document types your content editors can link to.
407
+ <br />
408
+ For each type, select the fields to include in the API response (used
409
+ in your frontend queries).
410
+ </Text>
411
+ </Box>
412
+ <Box>
413
+ <AddTypeButton
414
+ availableCustomTypes={availableCustomTypes}
415
+ onSelect={onSelect}
416
+ />
417
+ </Box>
418
+ </Box>
419
+ );
420
+ }
421
+
422
+ type AddTypeButtonProps = {
423
+ onSelect: (customTypeId: string) => void;
424
+ disabled?: boolean;
425
+ availableCustomTypes: CustomType[];
426
+ pickedCustomTypes?: CustomType[];
427
+ };
428
+
429
+ function AddTypeButton(props: AddTypeButtonProps) {
430
+ const { availableCustomTypes, onSelect, pickedCustomTypes = [] } = props;
431
+
432
+ const triggerButton = (
433
+ <Button
434
+ startIcon="add"
435
+ color="grey"
436
+ disabled={availableCustomTypes.length === 0}
437
+ >
438
+ {pickedCustomTypes.length > 0 ? "Add another type" : "Add type"}
439
+ </Button>
440
+ );
441
+
442
+ const disabledButton = (
443
+ <Box>
444
+ <Tooltip
445
+ content="All available types have been added"
446
+ side="bottom"
447
+ align="start"
448
+ disableHoverableContent
449
+ >
450
+ {triggerButton}
451
+ </Tooltip>
452
+ </Box>
453
+ );
454
+
455
+ if (availableCustomTypes.length === 0) return disabledButton;
456
+
457
+ return (
458
+ <Box>
459
+ <DropdownMenu>
460
+ <DropdownMenuTrigger>{triggerButton}</DropdownMenuTrigger>
461
+ <DropdownMenuContent
462
+ maxHeight={400}
463
+ minWidth={256}
464
+ align={pickedCustomTypes.length > 0 ? "start" : "center"}
465
+ >
466
+ <DropdownMenuLabel>
467
+ <Text color="grey11">Types</Text>
468
+ </DropdownMenuLabel>
469
+ {availableCustomTypes.flatMap((customType) => (
470
+ <DropdownMenuItem
471
+ key={customType.id}
472
+ onSelect={() => onSelect(customType.id)}
473
+ >
474
+ <Box alignItems="center" justifyContent="space-between" gap={8}>
475
+ <TextOverflow>
476
+ <Text>{customType.id}</Text>
477
+ </TextOverflow>
478
+ <Badge
479
+ title={
480
+ <Text variant="extraSmall" color="purple11">
481
+ {getTypeFormatLabel(customType.format)}
482
+ </Text>
483
+ }
484
+ color="purple"
485
+ size="small"
486
+ />
487
+ </Box>
488
+ </DropdownMenuItem>
489
+ ))}
490
+ </DropdownMenuContent>
491
+ </DropdownMenu>
492
+ </Box>
493
+ );
494
+ }
495
+
278
496
  interface TreeViewCustomTypeProps {
279
- customType: TICustomType;
497
+ customType: CustomType;
280
498
  fieldCheckMap: PickerCustomType;
281
499
  onChange: (newValue: PickerCustomType) => void;
500
+ allCustomTypes: CustomType[];
501
+ isNewType: boolean;
282
502
  }
283
503
 
284
504
  function TreeViewCustomType(props: TreeViewCustomTypeProps) {
285
505
  const {
286
506
  customType,
287
- fieldCheckMap: customTypeFieldCheckMap,
507
+ fieldCheckMap: customTypeFieldsCheckMap,
288
508
  onChange: onCustomTypeChange,
509
+ allCustomTypes,
510
+ isNewType,
289
511
  } = props;
290
512
 
291
- return (
292
- <TreeViewSection
293
- key={customType.id}
294
- title={customType.id}
295
- subtitle={getExposedFieldsLabel(
296
- countPickedFields(customTypeFieldCheckMap),
297
- )}
298
- badge="Custom type"
299
- >
300
- {customType.fields.map((field) => {
301
- // Checkbox field
302
-
303
- if (typeof field === "string") {
304
- const checked = customTypeFieldCheckMap[field]?.value ?? false;
305
-
306
- const onCheckedChange = (newValue: boolean) => {
307
- onCustomTypeChange({
308
- ...customTypeFieldCheckMap,
309
- [field]: { type: "checkbox", value: newValue },
310
- });
311
- };
312
-
313
- return (
314
- <TreeViewCheckbox
315
- key={field}
316
- title={field}
317
- checked={checked === true}
318
- onCheckedChange={onCheckedChange}
319
- />
320
- );
321
- }
322
-
323
- const crOrGroupFieldCheckMap = customTypeFieldCheckMap[field.id] ?? {};
513
+ const renderedFields = getCustomTypeStaticFields(customType).map(
514
+ ({ fieldId, field }) => {
515
+ // Group field
324
516
 
325
- // Group field
517
+ if (isGroupField(field)) {
518
+ const onGroupFieldChange = (
519
+ newGroupFields: PickerFirstLevelGroupFieldValue,
520
+ ) => {
521
+ onCustomTypeChange({
522
+ ...customTypeFieldsCheckMap,
523
+ [fieldId]: { type: "group", value: newGroupFields },
524
+ });
525
+ };
326
526
 
327
- if ("fields" in field) {
328
- const onGroupFieldChange = (
329
- newGroupFields: PickerFirstLevelGroupFieldValue,
330
- ) => {
331
- onCustomTypeChange({
332
- ...customTypeFieldCheckMap,
333
- [field.id]: { type: "group", value: newGroupFields },
334
- });
335
- };
527
+ const groupFieldCheckMap = customTypeFieldsCheckMap[fieldId] ?? {};
336
528
 
337
- return (
338
- <TreeViewGroupField
339
- key={field.id}
340
- group={field}
341
- onChange={onGroupFieldChange}
342
- fieldCheckMap={
343
- crOrGroupFieldCheckMap.type === "group"
344
- ? crOrGroupFieldCheckMap.value
345
- : {}
346
- }
347
- />
348
- );
349
- }
529
+ return (
530
+ <TreeViewFirstLevelGroupField
531
+ key={fieldId}
532
+ group={field}
533
+ groupId={fieldId}
534
+ onChange={onGroupFieldChange}
535
+ fieldCheckMap={
536
+ groupFieldCheckMap.type === "group"
537
+ ? groupFieldCheckMap.value
538
+ : {}
539
+ }
540
+ allCustomTypes={allCustomTypes}
541
+ />
542
+ );
543
+ }
350
544
 
351
- // Content relationship field
545
+ // Content relationship field
352
546
 
547
+ if (isContentRelationshipField(field)) {
353
548
  const onContentRelationshipFieldChange = (
354
549
  newCrFields: PickerContentRelationshipFieldValue,
355
550
  ) => {
356
551
  onCustomTypeChange({
357
- ...customTypeFieldCheckMap,
358
- [field.id]: { type: "contentRelationship", value: newCrFields },
552
+ ...customTypeFieldsCheckMap,
553
+ [fieldId]: {
554
+ type: "contentRelationship",
555
+ value: newCrFields,
556
+ },
359
557
  });
360
558
  };
361
559
 
560
+ const crFieldCheckMap = customTypeFieldsCheckMap[fieldId] ?? {};
561
+
362
562
  return (
363
563
  <TreeViewContentRelationshipField
364
- key={field.id}
564
+ key={fieldId}
365
565
  field={field}
566
+ fieldId={fieldId}
366
567
  onChange={onContentRelationshipFieldChange}
367
568
  fieldCheckMap={
368
- crOrGroupFieldCheckMap.type === "contentRelationship"
369
- ? crOrGroupFieldCheckMap.value
569
+ crFieldCheckMap.type === "contentRelationship"
570
+ ? crFieldCheckMap.value
370
571
  : {}
371
572
  }
573
+ allCustomTypes={allCustomTypes}
372
574
  />
373
575
  );
374
- })}
576
+ }
577
+
578
+ // Regular field
579
+
580
+ const onCheckedChange = (newValue: boolean) => {
581
+ onCustomTypeChange({
582
+ ...customTypeFieldsCheckMap,
583
+ [fieldId]: { type: "checkbox", value: newValue },
584
+ });
585
+ };
586
+
587
+ return (
588
+ <TreeViewCheckbox
589
+ key={fieldId}
590
+ title={fieldId}
591
+ checked={customTypeFieldsCheckMap[fieldId]?.value === true}
592
+ onCheckedChange={onCheckedChange}
593
+ />
594
+ );
595
+ },
596
+ );
597
+
598
+ const exposedFieldsCount = countPickedFields(customTypeFieldsCheckMap);
599
+ return (
600
+ <TreeViewSection
601
+ key={customType.id}
602
+ title={customType.id}
603
+ subtitle={
604
+ exposedFieldsCount > 0
605
+ ? getExposedFieldsLabel(exposedFieldsCount)
606
+ : "(No fields returned in the API)"
607
+ }
608
+ badge={getTypeFormatLabel(customType.format)}
609
+ defaultOpen={isNewType}
610
+ >
611
+ {renderedFields.length > 0 ? (
612
+ renderedFields
613
+ ) : (
614
+ <Text color="grey11">No available fields to select</Text>
615
+ )}
375
616
  </TreeViewSection>
376
617
  );
377
618
  }
378
619
 
379
620
  interface TreeViewContentRelationshipFieldProps {
380
- field: TIContentRelationshipFieldValue;
621
+ fieldId: string;
622
+ field: Link;
381
623
  fieldCheckMap: PickerContentRelationshipFieldValue;
382
624
  onChange: (newValue: PickerContentRelationshipFieldValue) => void;
625
+ allCustomTypes: CustomType[];
383
626
  }
384
627
 
385
628
  function TreeViewContentRelationshipField(
386
629
  props: TreeViewContentRelationshipFieldProps,
387
630
  ) {
388
631
  const {
389
- field: crField,
632
+ field,
633
+ fieldId,
390
634
  fieldCheckMap: crFieldsCheckMap,
391
635
  onChange: onCrFieldChange,
636
+ allCustomTypes,
392
637
  } = props;
393
638
 
639
+ if (!field.config?.customtypes) return null;
640
+
641
+ const resolvedCustomTypes = resolveContentRelationshipCustomTypes(
642
+ field.config.customtypes,
643
+ allCustomTypes,
644
+ );
645
+
646
+ if (resolvedCustomTypes.length === 0) return null;
647
+
394
648
  return (
395
649
  <TreeViewSection
396
- title={crField.id}
650
+ title={fieldId}
397
651
  subtitle={getExposedFieldsLabel(countPickedFields(crFieldsCheckMap))}
398
652
  >
399
- {crField.customtypes.map((customType) => {
653
+ {resolvedCustomTypes.map((customType) => {
400
654
  if (typeof customType === "string") return null;
401
655
 
402
- const nestedCtFieldsCheckMap = crFieldsCheckMap[customType.id] ?? {};
403
-
404
656
  const onNestedCustomTypeChange = (
405
657
  newNestedCustomTypeFields: PickerNestedCustomTypeValue,
406
658
  ) => {
@@ -410,51 +662,29 @@ function TreeViewContentRelationshipField(
410
662
  });
411
663
  };
412
664
 
413
- return (
414
- <TreeViewSection
415
- key={customType.id}
416
- title={customType.id}
417
- subtitle={getExposedFieldsLabel(
418
- countPickedFields(nestedCtFieldsCheckMap),
419
- )}
420
- badge="Custom type"
421
- >
422
- {customType.fields.map((field) => {
423
- if (typeof field === "string") {
424
- const checked = nestedCtFieldsCheckMap[field]?.value === true;
425
-
426
- const onCheckedChange = (newChecked: boolean) => {
427
- onNestedCustomTypeChange({
428
- ...nestedCtFieldsCheckMap,
429
- [field]: { type: "checkbox", value: newChecked },
430
- });
431
- };
432
-
433
- return (
434
- <TreeViewCheckbox
435
- key={field}
436
- title={field}
437
- checked={checked}
438
- onCheckedChange={onCheckedChange}
439
- />
440
- );
441
- }
665
+ const nestedCtFieldsCheckMap = crFieldsCheckMap[customType.id] ?? {};
442
666
 
443
- const groupFieldCheckMap = nestedCtFieldsCheckMap[field.id] ?? {};
667
+ const renderedFields = getCustomTypeStaticFields(customType).map(
668
+ ({ fieldId, field }) => {
669
+ // Group field
444
670
 
671
+ if (isGroupField(field)) {
445
672
  const onGroupFieldsChange = (
446
673
  newGroupFields: PickerLeafGroupFieldValue,
447
674
  ) => {
448
675
  onNestedCustomTypeChange({
449
676
  ...nestedCtFieldsCheckMap,
450
- [field.id]: { type: "group", value: newGroupFields },
677
+ [fieldId]: { type: "group", value: newGroupFields },
451
678
  });
452
679
  };
453
680
 
681
+ const groupFieldCheckMap = nestedCtFieldsCheckMap[fieldId] ?? {};
682
+
454
683
  return (
455
- <TreeViewContentRelationshipFieldNestedGroup
456
- key={field.id}
684
+ <TreeViewLeafGroupField
685
+ key={fieldId}
457
686
  group={field}
687
+ groupId={fieldId}
458
688
  onChange={onGroupFieldsChange}
459
689
  fieldCheckMap={
460
690
  groupFieldCheckMap.type === "group"
@@ -463,7 +693,40 @@ function TreeViewContentRelationshipField(
463
693
  }
464
694
  />
465
695
  );
466
- })}
696
+ }
697
+
698
+ // Regular field
699
+
700
+ const onCheckedChange = (newChecked: boolean) => {
701
+ onNestedCustomTypeChange({
702
+ ...nestedCtFieldsCheckMap,
703
+ [fieldId]: { type: "checkbox", value: newChecked },
704
+ });
705
+ };
706
+
707
+ return (
708
+ <TreeViewCheckbox
709
+ key={fieldId}
710
+ title={fieldId}
711
+ checked={nestedCtFieldsCheckMap[fieldId]?.value === true}
712
+ onCheckedChange={onCheckedChange}
713
+ />
714
+ );
715
+ },
716
+ );
717
+
718
+ if (renderedFields.length === 0) return null;
719
+
720
+ return (
721
+ <TreeViewSection
722
+ key={customType.id}
723
+ title={customType.id}
724
+ subtitle={getExposedFieldsLabel(
725
+ countPickedFields(nestedCtFieldsCheckMap),
726
+ )}
727
+ badge={getTypeFormatLabel(customType.format)}
728
+ >
729
+ {renderedFields}
467
730
  </TreeViewSection>
468
731
  );
469
732
  })}
@@ -471,108 +734,128 @@ function TreeViewContentRelationshipField(
471
734
  );
472
735
  }
473
736
 
474
- interface TreeViewContentRelationshipFieldGroupProps {
475
- group: TICustomTypeRegularFieldValues;
737
+ interface TreeViewLeafGroupFieldProps {
738
+ group: Group;
739
+ groupId: string;
476
740
  fieldCheckMap: PickerLeafGroupFieldValue;
477
741
  onChange: (newValue: PickerLeafGroupFieldValue) => void;
478
742
  }
479
743
 
480
- function TreeViewContentRelationshipFieldNestedGroup(
481
- props: TreeViewContentRelationshipFieldGroupProps,
482
- ) {
744
+ function TreeViewLeafGroupField(props: TreeViewLeafGroupFieldProps) {
483
745
  const {
484
746
  group,
747
+ groupId,
485
748
  fieldCheckMap: groupFieldsCheckMap,
486
749
  onChange: onGroupFieldChange,
487
750
  } = props;
488
751
 
752
+ if (!group.config?.fields) return null;
753
+
754
+ const renderedFields = getGroupFields(group).map(({ fieldId }) => {
755
+ const onCheckedChange = (newChecked: boolean) => {
756
+ onGroupFieldChange({
757
+ ...groupFieldsCheckMap,
758
+ [fieldId]: { type: "checkbox", value: newChecked },
759
+ });
760
+ };
761
+
762
+ return (
763
+ <TreeViewCheckbox
764
+ key={fieldId}
765
+ title={fieldId}
766
+ checked={groupFieldsCheckMap[fieldId]?.value === true}
767
+ onCheckedChange={onCheckedChange}
768
+ />
769
+ );
770
+ });
771
+
772
+ if (renderedFields.length === 0) return null;
773
+
489
774
  return (
490
775
  <TreeViewSection
491
- key={group.id}
492
- title={group.id}
776
+ key={groupId}
777
+ title={groupId}
493
778
  subtitle={getExposedFieldsLabel(countPickedFields(groupFieldsCheckMap))}
494
779
  badge="Group"
495
780
  >
496
- {group.fields.map((field) => {
497
- const checked = groupFieldsCheckMap[field]?.value ?? false;
498
-
499
- const onCheckedChange = (newChecked: boolean) => {
500
- onGroupFieldChange({
501
- ...groupFieldsCheckMap,
502
- [field]: { type: "checkbox", value: newChecked },
503
- });
504
- };
505
-
506
- return (
507
- <TreeViewCheckbox
508
- key={field}
509
- title={field}
510
- checked={checked === true}
511
- onCheckedChange={onCheckedChange}
512
- />
513
- );
514
- })}
781
+ {renderedFields}
515
782
  </TreeViewSection>
516
783
  );
517
784
  }
518
785
 
519
- interface TreeViewGroupFieldProps {
520
- group: TIGroupFieldValues;
786
+ interface TreeViewFirstLevelGroupFieldProps {
787
+ group: Group;
788
+ groupId: string;
521
789
  fieldCheckMap: PickerFirstLevelGroupFieldValue;
522
790
  onChange: (newValue: PickerFirstLevelGroupFieldValue) => void;
791
+ allCustomTypes: CustomType[];
523
792
  }
524
793
 
525
- function TreeViewGroupField(props: TreeViewGroupFieldProps) {
794
+ function TreeViewFirstLevelGroupField(
795
+ props: TreeViewFirstLevelGroupFieldProps,
796
+ ) {
526
797
  const {
527
798
  group,
799
+ groupId,
528
800
  fieldCheckMap: groupFieldsCheckMap,
529
801
  onChange: onGroupFieldChange,
802
+ allCustomTypes,
530
803
  } = props;
531
804
 
532
- return (
533
- <TreeViewSection key={group.id} title={group.id} badge="Group">
534
- {group.fields.map((field) => {
535
- if (typeof field === "string") {
536
- const checked = groupFieldsCheckMap[field]?.value ?? false;
805
+ if (!group.config?.fields) return null;
537
806
 
538
- const onCheckedChange = (newChecked: boolean) => {
807
+ return (
808
+ <TreeViewSection
809
+ key={groupId}
810
+ title={groupId}
811
+ subtitle={getExposedFieldsLabel(countPickedFields(groupFieldsCheckMap))}
812
+ badge="Group"
813
+ >
814
+ {getGroupFields(group).map(({ fieldId, field }) => {
815
+ if (isContentRelationshipField(field)) {
816
+ const onContentRelationshipFieldChange = (
817
+ newCrFields: PickerContentRelationshipFieldValue,
818
+ ) => {
539
819
  onGroupFieldChange({
540
820
  ...groupFieldsCheckMap,
541
- [field]: { type: "checkbox", value: newChecked },
821
+ [fieldId]: {
822
+ type: "contentRelationship",
823
+ value: newCrFields,
824
+ },
542
825
  });
543
826
  };
544
827
 
828
+ const crFieldCheckMap = groupFieldsCheckMap[fieldId] ?? {};
829
+
545
830
  return (
546
- <TreeViewCheckbox
547
- key={field}
548
- title={field}
549
- checked={checked === true}
550
- onCheckedChange={onCheckedChange}
831
+ <TreeViewContentRelationshipField
832
+ key={fieldId}
833
+ field={field}
834
+ fieldId={fieldId}
835
+ fieldCheckMap={
836
+ crFieldCheckMap.type === "contentRelationship"
837
+ ? crFieldCheckMap.value
838
+ : {}
839
+ }
840
+ onChange={onContentRelationshipFieldChange}
841
+ allCustomTypes={allCustomTypes}
551
842
  />
552
843
  );
553
844
  }
554
845
 
555
- const crFieldCheckMap = groupFieldsCheckMap[field.id] ?? {};
556
-
557
- const onContentRelationshipFieldChange = (
558
- newCrFields: PickerContentRelationshipFieldValue,
559
- ) => {
846
+ const onCheckedChange = (newChecked: boolean) => {
560
847
  onGroupFieldChange({
561
848
  ...groupFieldsCheckMap,
562
- [field.id]: { type: "contentRelationship", value: newCrFields },
849
+ [fieldId]: { type: "checkbox", value: newChecked },
563
850
  });
564
851
  };
565
852
 
566
853
  return (
567
- <TreeViewContentRelationshipField
568
- key={field.id}
569
- field={field}
570
- fieldCheckMap={
571
- crFieldCheckMap.type === "contentRelationship"
572
- ? crFieldCheckMap.value
573
- : {}
574
- }
575
- onChange={onContentRelationshipFieldChange}
854
+ <TreeViewCheckbox
855
+ key={fieldId}
856
+ title={fieldId}
857
+ checked={groupFieldsCheckMap[fieldId]?.value === true}
858
+ onCheckedChange={onCheckedChange}
576
859
  />
577
860
  );
578
861
  })}
@@ -582,124 +865,72 @@ function TreeViewGroupField(props: TreeViewGroupFieldProps) {
582
865
 
583
866
  function getExposedFieldsLabel(count: number) {
584
867
  if (count === 0) return undefined;
585
- return `(${count} ${pluralize(count, "field", "fields")} exposed)`;
868
+ return `(${count} ${pluralize(
869
+ count,
870
+ "field",
871
+ "fields",
872
+ )} returned in the API)`;
586
873
  }
587
874
 
588
- /**
589
- * Get all the existing local custom types from the store and process them into
590
- * a single array to be rendered by the picker. For this we use the same as the
591
- * Link config `customtypes` structure {@link TICustomTypes}.
592
- */
593
- function useCustomTypes() {
594
- const allCustomTypes = useSelector(selectAllCustomTypes);
595
- const localCustomTypes = allCustomTypes.flatMap((ct) => {
596
- // In the store we have remote and local custom types, we want to show
597
- // the local ones, so that the user is able to create a content
598
- // relationship with custom types present on the user's computer (pushed
599
- // or not).
600
- return "local" in ct ? ct.local : [];
601
- });
602
-
603
- return useMemo(() => {
604
- const customTypes = localCustomTypes.flatMap<TICustomType>((customType) => {
605
- const tabFields = customType.tabs.flatMap((tab) => {
606
- return tab.value.flatMap((field) => {
607
- if (isUidField(field)) return [];
608
-
609
- // Check if it's a content relationship link/field
610
- if (
611
- field.value.type === "Link" &&
612
- field.value.config?.select === "document" &&
613
- field.value.config.customtypes
614
- ) {
615
- const resolvedFields = resolveContentRelationshipFields(
616
- field.value.config.customtypes,
617
- localCustomTypes,
618
- );
619
-
620
- return resolvedFields.length > 0
621
- ? { id: field.key, customtypes: resolvedFields }
622
- : [];
623
- }
875
+ function getTypeFormatLabel(format: CustomType["format"]) {
876
+ return format === "page" ? "Page type" : "Custom type";
877
+ }
624
878
 
625
- if (field.value.type === "Group" && field.value.config?.fields) {
626
- return {
627
- id: field.key,
628
- fields: field.value.config.fields.map((field) => field.key),
629
- };
630
- }
879
+ /** Retrieves all existing page & custom types. */
880
+ function useCustomTypes(value: LinkCustomtypes | undefined) {
881
+ const allCustomTypes = useRequest(getCustomTypes, []);
631
882
 
632
- return field.key;
633
- });
634
- });
883
+ useEffect(() => {
884
+ void revalidateData(getCustomTypes, []);
885
+ }, []);
635
886
 
636
- return tabFields.length > 0
637
- ? { id: customType.id, fields: tabFields }
638
- : [];
639
- });
887
+ if (!value) {
888
+ return {
889
+ allCustomTypes,
890
+ availableCustomTypes: allCustomTypes,
891
+ pickedCustomTypes: [],
892
+ };
893
+ }
640
894
 
641
- customTypes.sort((a, b) => a.id.localeCompare(b.id));
895
+ const pickedCustomTypes = value.flatMap(
896
+ (pickedCt) => allCustomTypes.find((ct) => ct.id === getId(pickedCt)) ?? [],
897
+ );
642
898
 
643
- return customTypes;
644
- }, [localCustomTypes]);
899
+ return {
900
+ allCustomTypes,
901
+ pickedCustomTypes,
902
+ availableCustomTypes: allCustomTypes.filter(
903
+ (ct) => pickedCustomTypes.some((pct) => pct.id === ct.id) === false,
904
+ ),
905
+ };
645
906
  }
646
907
 
647
- function resolveContentRelationshipFields(
648
- customTypesArray: TICustomTypes,
649
- localCustomTypes: CustomTypeSM[],
650
- ): TICustomTypeFieldValues[] {
651
- const fields = customTypesArray.flatMap<TICustomTypeFieldValues>(
652
- (customType) => {
653
- if (typeof customType === "string") return [];
654
-
655
- // Didn't find a custom type with a matching id in the store
656
- const matchingCustomType = localCustomTypes.find(
657
- (ct) => ct.id === customType.id,
658
- );
659
- if (!matchingCustomType) return [];
908
+ async function getCustomTypes(): Promise<CustomType[]> {
909
+ const { errors, models } =
910
+ await managerClient.customTypes.readAllCustomTypes();
660
911
 
661
- const tabFields = matchingCustomType.tabs.flatMap((tab) => {
662
- return tab.value.flatMap((field) => {
663
- if (isUidField(field)) return [];
664
-
665
- // Group inside content relationship field
666
- if (field.value.type === "Group" && field.value.config?.fields) {
667
- return {
668
- id: field.key,
669
- fields: field.value.config.fields.map((field) => field.key),
670
- };
671
- }
672
-
673
- return field.key;
674
- });
675
- });
912
+ if (errors.length > 0) throw errors;
913
+ return models.map(({ model }) => model);
914
+ }
676
915
 
677
- return tabFields.length > 0
678
- ? { id: customType.id, fields: tabFields }
679
- : [];
680
- },
681
- );
916
+ function resolveContentRelationshipCustomTypes(
917
+ customTypes: LinkCustomtypes,
918
+ localCustomTypes: CustomType[],
919
+ ): CustomType[] {
920
+ const fields = customTypes.flatMap<CustomType>((customType) => {
921
+ if (typeof customType === "string") return [];
922
+ return localCustomTypes.find((ct) => ct.id === customType.id) ?? [];
923
+ });
682
924
 
683
925
  return fields;
684
926
  }
685
927
 
686
- function isUidField(
687
- field: TabFields[number],
688
- ): field is { key: string; value: UID } {
689
- // Filter out uid fields because it's a special field returned by the
690
- // API and is not part of the data object in the document.
691
- // We also filter by key "uid", because (as of the time of writing
692
- // this), creating any field with that API id will result in it being
693
- // used for metadata.
694
- return field.key === "uid" && field.value.type === "UID";
695
- }
696
-
697
928
  /**
698
- * Converts a Link config `customtypes` ({@link TICustomTypes}) structure into
929
+ * Converts a Link config `customtypes` ({@link LinkCustomtypes}) structure into
699
930
  * picker fields check map ({@link PickerCustomTypes}).
700
931
  */
701
- function convertCustomTypesToFieldCheckMap(
702
- customTypes: TICustomTypes,
932
+ function convertLinkCustomtypesToFieldCheckMap(
933
+ customTypes: LinkCustomtypes,
703
934
  ): PickerCustomTypes {
704
935
  return customTypes.reduce<PickerCustomTypes>((customTypes, customType) => {
705
936
  if (typeof customType === "string") return customTypes;
@@ -711,10 +942,11 @@ function convertCustomTypesToFieldCheckMap(
711
942
  customTypeFields[field] = { type: "checkbox", value: true };
712
943
  } else if ("fields" in field && field.fields !== undefined) {
713
944
  // Group field
714
- customTypeFields[field.id] = createGroupField(field);
945
+ customTypeFields[field.id] = createGroupFieldCheckMap(field);
715
946
  } else if ("customtypes" in field && field.customtypes !== undefined) {
716
947
  // Content relationship field
717
- customTypeFields[field.id] = createNestedCustomTypeField(field);
948
+ customTypeFields[field.id] =
949
+ createContentRelationshipFieldCheckMap(field);
718
950
  }
719
951
 
720
952
  return customTypeFields;
@@ -725,8 +957,8 @@ function convertCustomTypesToFieldCheckMap(
725
957
  }, {});
726
958
  }
727
959
 
728
- function createGroupField(
729
- group: TIGroupFieldValues,
960
+ function createGroupFieldCheckMap(
961
+ group: LinkCustomtypesGroupFieldValue,
730
962
  ): PickerFirstLevelGroupField {
731
963
  return {
732
964
  type: "group",
@@ -737,7 +969,7 @@ function createGroupField(
737
969
  fields[field] = { type: "checkbox", value: true };
738
970
  } else if ("customtypes" in field && field.customtypes !== undefined) {
739
971
  // Content relationship field
740
- fields[field.id] = createNestedCustomTypeField(field);
972
+ fields[field.id] = createContentRelationshipFieldCheckMap(field);
741
973
  }
742
974
 
743
975
  return fields;
@@ -747,8 +979,8 @@ function createGroupField(
747
979
  };
748
980
  }
749
981
 
750
- function createNestedCustomTypeField(
751
- field: TIContentRelationshipFieldValue,
982
+ function createContentRelationshipFieldCheckMap(
983
+ field: LinkCustomtypesContentRelationshipFieldValue,
752
984
  ): PickerContentRelationshipField {
753
985
  const crField: PickerContentRelationshipField = {
754
986
  type: "contentRelationship",
@@ -785,78 +1017,134 @@ function createNestedCustomTypeField(
785
1017
  return crField;
786
1018
  }
787
1019
 
1020
+ /**
1021
+ * Merges the existing Link `customtypes` array with the picker state, ensuring
1022
+ * that conversions from to string (custom type id) to object and vice versa are
1023
+ * made correctly and that the order is preserved.
1024
+ */
1025
+ function mergeAndConvertCheckMapToLinkCustomtypes(args: {
1026
+ existingLinkCustomtypes: LinkCustomtypes | undefined;
1027
+ previousPickerCustomtypes: PickerCustomTypes;
1028
+ newCustomType: PickerCustomType;
1029
+ customTypeId: string;
1030
+ }): LinkCustomtypes {
1031
+ const {
1032
+ existingLinkCustomtypes,
1033
+ previousPickerCustomtypes,
1034
+ newCustomType,
1035
+ customTypeId,
1036
+ } = args;
1037
+
1038
+ const result: NonReadonly<LinkCustomtypes> = [];
1039
+ const pickerLinkCustomtypes = convertFieldCheckMapToLinkCustomtypes({
1040
+ ...previousPickerCustomtypes,
1041
+ [customTypeId]: newCustomType,
1042
+ });
1043
+
1044
+ if (!existingLinkCustomtypes) return pickerLinkCustomtypes;
1045
+
1046
+ for (const existingLinkCt of existingLinkCustomtypes) {
1047
+ const existingPickerLinkCt = pickerLinkCustomtypes.find((ct) => {
1048
+ return getId(ct) === getId(existingLinkCt);
1049
+ });
1050
+
1051
+ if (existingPickerLinkCt !== undefined) {
1052
+ // Custom type with exposed fields, keep the customtypes object
1053
+ result.push(existingPickerLinkCt);
1054
+ } else if (getId(existingLinkCt) === customTypeId) {
1055
+ // Custom type that had exposed fields, but now has none, change to string
1056
+ result.push(getId(existingLinkCt));
1057
+ } else {
1058
+ // Custom type without exposed fields, keep the string
1059
+ result.push(existingLinkCt);
1060
+ }
1061
+ }
1062
+
1063
+ return result;
1064
+ }
1065
+
788
1066
  /**
789
1067
  * Converts a picker fields check map structure ({@link PickerCustomTypes}) into
790
- * Link config `customtypes` ({@link TICustomTypes}) and filter out empty Custom
1068
+ * Link config `customtypes` ({@link LinkCustomtypes}) and filter out empty Custom
791
1069
  * types.
792
1070
  */
793
- function convertFieldCheckMapToCustomTypes(map: PickerCustomTypes) {
794
- return Object.entries(map).flatMap<TICustomType>(([ctId, ctFields]) => {
795
- const fields = Object.entries(ctFields).flatMap<
796
- string | TIContentRelationshipFieldValue | TIGroupFieldValues
797
- >(([fieldId, fieldValue]) => {
798
- if (fieldValue.type === "checkbox") {
799
- return fieldValue.value ? fieldId : [];
800
- }
1071
+ function convertFieldCheckMapToLinkCustomtypes(
1072
+ checkMap: PickerCustomTypes,
1073
+ ): LinkCustomtypes {
1074
+ return Object.entries(checkMap).flatMap<LinkCustomtypes[number]>(
1075
+ ([ctId, ctFields]) => {
1076
+ const fields = Object.entries(ctFields).flatMap<
1077
+ | string
1078
+ | LinkCustomtypesContentRelationshipFieldValue
1079
+ | LinkCustomtypesGroupFieldValue
1080
+ >(([fieldId, fieldValue]) => {
1081
+ // First level group field
1082
+ if (fieldValue.type === "group") {
1083
+ const fields = Object.entries(fieldValue.value).flatMap<
1084
+ string | LinkCustomtypesContentRelationshipFieldValue
1085
+ >(([fieldId, fieldValue]) => {
1086
+ if (fieldValue.type === "checkbox") {
1087
+ return fieldValue.value ? fieldId : [];
1088
+ }
801
1089
 
802
- if (fieldValue.type === "group") {
803
- const fields = Object.entries(fieldValue.value).flatMap<
804
- string | TIContentRelationshipFieldValue
805
- >(([fieldId, fieldValue]) => {
806
- if (fieldValue.type === "checkbox") {
807
- return fieldValue.value ? fieldId : [];
808
- }
1090
+ const customTypes = createContentRelationshipLinkCustomtypes(
1091
+ fieldValue.value,
1092
+ );
809
1093
 
810
- const customTypes = convertContentRelationshipFieldValueToCustomTypes(
1094
+ return customTypes.length > 0
1095
+ ? { id: fieldId, customtypes: customTypes }
1096
+ : [];
1097
+ });
1098
+
1099
+ return fields.length > 0 ? { id: fieldId, fields } : [];
1100
+ }
1101
+
1102
+ // Content relationship field
1103
+ if (fieldValue.type === "contentRelationship") {
1104
+ const customTypes = createContentRelationshipLinkCustomtypes(
811
1105
  fieldValue.value,
812
1106
  );
813
1107
 
814
1108
  return customTypes.length > 0
815
1109
  ? { id: fieldId, customtypes: customTypes }
816
1110
  : [];
817
- });
818
-
819
- return fields.length > 0 ? { id: fieldId, fields } : [];
820
- }
821
-
822
- // Content relationship field
823
- const customTypes = convertContentRelationshipFieldValueToCustomTypes(
824
- fieldValue.value,
825
- );
1111
+ }
826
1112
 
827
- return customTypes.length > 0
828
- ? { id: fieldId, customtypes: customTypes }
829
- : [];
830
- });
1113
+ // Regular field
1114
+ return fieldValue.value ? fieldId : [];
1115
+ });
831
1116
 
832
- return fields.length > 0 ? { id: ctId, fields } : [];
833
- });
1117
+ return fields.length > 0 ? { id: ctId, fields } : [];
1118
+ },
1119
+ );
834
1120
  }
835
1121
 
836
- function convertContentRelationshipFieldValueToCustomTypes(
1122
+ function createContentRelationshipLinkCustomtypes(
837
1123
  value: PickerContentRelationshipFieldValue,
838
- ): TICustomTypeFieldValues[] {
839
- return Object.entries(value).flatMap<TICustomTypeFieldValues>(
1124
+ ): LinkCustomtypesContentRelationshipFieldValue["customtypes"] {
1125
+ return Object.entries(value).flatMap(
840
1126
  ([nestedCustomTypeId, nestedCustomTypeFields]) => {
841
- const fields = Object.entries(nestedCustomTypeFields).flatMap<
842
- string | TICustomTypeRegularFieldValues
843
- >(([nestedFieldId, nestedFieldValue]) => {
844
- if (nestedFieldValue.type === "group") {
845
- const nestedGroupFields = Object.entries(
846
- nestedFieldValue.value,
847
- ).flatMap<string>(([fieldId, fieldValue]) => {
848
- return fieldValue.type === "checkbox" && fieldValue.value
849
- ? fieldId
850
- : [];
851
- });
1127
+ const fields = Object.entries(nestedCustomTypeFields).flatMap(
1128
+ ([nestedFieldId, nestedFieldValue]) => {
1129
+ // Leaf group field
1130
+ if (nestedFieldValue.type === "group") {
1131
+ const nestedGroupFields = Object.entries(
1132
+ nestedFieldValue.value,
1133
+ ).flatMap<string>(([fieldId, fieldValue]) => {
1134
+ // Regular field
1135
+ return fieldValue.type === "checkbox" && fieldValue.value
1136
+ ? fieldId
1137
+ : [];
1138
+ });
852
1139
 
853
- return nestedGroupFields.length > 0
854
- ? { id: nestedFieldId, fields: nestedGroupFields }
855
- : [];
856
- }
1140
+ return nestedGroupFields.length > 0
1141
+ ? { id: nestedFieldId, fields: nestedGroupFields }
1142
+ : [];
1143
+ }
857
1144
 
858
- return nestedFieldValue.value ? nestedFieldId : [];
859
- });
1145
+ return nestedFieldValue.value ? nestedFieldId : [];
1146
+ },
1147
+ );
860
1148
 
861
1149
  return fields.length > 0 ? { id: nestedCustomTypeId, fields } : [];
862
1150
  },
@@ -876,12 +1164,58 @@ function countPickedFields(
876
1164
  if (!fields) return 0;
877
1165
  return Object.values(fields).reduce<number>((count, value) => {
878
1166
  if (!isValidObject(value)) return count;
879
- if (isCheckboxField(value)) return count + (value.value ? 1 : 0);
1167
+ if (isCheckboxValue(value)) return count + (value.value ? 1 : 0);
880
1168
  return count + countPickedFields(value);
881
1169
  }, 0);
882
1170
  }
883
-
884
- function isCheckboxField(value: unknown): value is PickerCheckboxField {
1171
+ function isCheckboxValue(value: unknown): value is PickerCheckboxField {
885
1172
  if (!isValidObject(value)) return false;
886
1173
  return "type" in value && value.type === "checkbox";
887
1174
  }
1175
+
1176
+ function isGroupField(field: NestableWidget | Group): field is Group {
1177
+ return field.type === "Group";
1178
+ }
1179
+
1180
+ function isContentRelationshipField(
1181
+ field: NestableWidget | Group,
1182
+ ): field is Link {
1183
+ return (
1184
+ field.type === "Link" &&
1185
+ field.config?.select === "document" &&
1186
+ field.config?.customtypes !== undefined
1187
+ );
1188
+ }
1189
+
1190
+ function getCustomTypeStaticFields(customType: CustomType) {
1191
+ return Object.values(customType.json).flatMap((tabFields) => {
1192
+ return Object.entries(tabFields).flatMap(([fieldId, field]) => {
1193
+ if (
1194
+ field.type !== "Slices" &&
1195
+ field.type !== "Choice" &&
1196
+ // Filter out uid fields because it's a special field returned by the
1197
+ // API and is not part of the data object in the document.
1198
+ // We also filter by key "uid", because (as of the time of writing
1199
+ // this), creating any field with that API id will result in it being
1200
+ // used for metadata.
1201
+ (field.type !== "UID" || fieldId !== "uid")
1202
+ ) {
1203
+ return { fieldId, field: field as NestableWidget | Group };
1204
+ }
1205
+
1206
+ return [];
1207
+ });
1208
+ });
1209
+ }
1210
+
1211
+ function getGroupFields(group: Group) {
1212
+ if (!group.config?.fields) return [];
1213
+ return Object.entries(group.config.fields).map(([fieldId, field]) => {
1214
+ return { fieldId, field: field as NestableWidget };
1215
+ });
1216
+ }
1217
+ /** If it's a string, return it, otherwise return the `id` property. */
1218
+ function getId<T extends string | { id: string }>(customType: T): string {
1219
+ if (typeof customType === "string") return customType;
1220
+ return customType.id;
1221
+ }