slice-machine-ui 2.16.2-alpha.jp-cr-empty-customtypes.1 → 2.16.2-alpha.jp-cr-ui-fix-invalid-fields-checked.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.
@@ -22,6 +22,7 @@ import {
22
22
  } from "@prismicio/editor-ui";
23
23
  import {
24
24
  CustomType,
25
+ DynamicWidget,
25
26
  Group,
26
27
  Link,
27
28
  LinkConfig,
@@ -260,20 +261,13 @@ export function ContentRelationshipFieldPicker(
260
261
  function ContentRelationshipFieldPickerContent(
261
262
  props: ContentRelationshipFieldPickerProps,
262
263
  ) {
263
- const { value, onChange } = props;
264
- const { allCustomTypes, pickedCustomTypes } = useCustomTypes(value);
264
+ const { value: linkCustomtypes, onChange } = props;
265
+ const { allCustomTypes, pickedCustomTypes } = useCustomTypes(linkCustomtypes);
265
266
 
266
- const fieldCheckMap = value
267
- ? convertLinkCustomtypesToFieldCheckMap(value)
267
+ const fieldCheckMap = linkCustomtypes
268
+ ? convertLinkCustomtypesToFieldCheckMap({ linkCustomtypes, allCustomTypes })
268
269
  : {};
269
270
 
270
- useEffect(() => {
271
- // Make sure the field has a customtypes property. We did not create it
272
- // before for new Content Relationship fields, but now we do, and we want to
273
- // correct most of the cases where it's missing.
274
- if (!value) onChange([]);
275
- }, [value]);
276
-
277
271
  function onCustomTypesChange(id: string, newCustomType: PickerCustomType) {
278
272
  // The picker does not handle strings (custom type ids), as it's only meant
279
273
  // to pick fields from custom types (objects). So we need to merge it with
@@ -281,22 +275,24 @@ function ContentRelationshipFieldPickerContent(
281
275
  // represent new types added without any picked fields.
282
276
  onChange(
283
277
  mergeAndConvertCheckMapToLinkCustomtypes({
284
- existingLinkCustomtypes: value,
285
- previousPickerCustomtypes: fieldCheckMap,
286
- customTypeId: id,
278
+ fieldCheckMap,
287
279
  newCustomType,
280
+ linkCustomtypes,
281
+ customTypeId: id,
288
282
  }),
289
283
  );
290
284
  }
291
285
 
292
286
  function addCustomType(id: string) {
293
- const newFields = value ? [...value, id] : [id];
287
+ const newFields = linkCustomtypes ? [...linkCustomtypes, id] : [id];
294
288
  onChange(newFields);
295
289
  }
296
290
 
297
291
  function removeCustomType(id: string) {
298
- if (value) {
299
- onChange(value.filter((existingCt) => getId(existingCt) !== id));
292
+ if (linkCustomtypes) {
293
+ onChange(
294
+ linkCustomtypes.filter((existingCt) => getId(existingCt) !== id),
295
+ );
300
296
  }
301
297
  }
302
298
 
@@ -416,8 +412,9 @@ function ContentRelationshipFieldPickerContent(
416
412
  rel="noopener noreferrer"
417
413
  style={{ color: "inherit", textDecoration: "underline" }}
418
414
  >
419
- Please provide your feedback here.
415
+ Please provide your feedback here
420
416
  </a>
417
+ .
421
418
  </Text>
422
419
  </Box>
423
420
  </Box>
@@ -470,20 +467,20 @@ function AddTypeButton(props: AddTypeButtonProps) {
470
467
  </Button>
471
468
  );
472
469
 
473
- const disabledButton = (
474
- <Box>
475
- <Tooltip
476
- content="No type available"
477
- side="bottom"
478
- align="start"
479
- disableHoverableContent
480
- >
481
- {triggerButton}
482
- </Tooltip>
483
- </Box>
484
- );
485
-
486
- if (allCustomTypes.length === 0) return disabledButton;
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
+ }
487
484
 
488
485
  return (
489
486
  <Box>
@@ -908,7 +905,7 @@ function getTypeFormatLabel(format: CustomType["format"]) {
908
905
  }
909
906
 
910
907
  /** Retrieves all existing page & custom types. */
911
- function useCustomTypes(value: LinkCustomtypes | undefined): {
908
+ function useCustomTypes(linkCustomtypes: LinkCustomtypes | undefined): {
912
909
  /** Every existing custom type, used to discover nested custom types down the tree and the add type dropdown. */
913
910
  allCustomTypes: CustomType[];
914
911
  /** The custom types that are already picked. */
@@ -920,14 +917,14 @@ function useCustomTypes(value: LinkCustomtypes | undefined): {
920
917
  void revalidateGetCustomTypes();
921
918
  }, []);
922
919
 
923
- if (!value) {
920
+ if (!linkCustomtypes) {
924
921
  return {
925
922
  allCustomTypes,
926
923
  pickedCustomTypes: [],
927
924
  };
928
925
  }
929
926
 
930
- const pickedCustomTypes = value.flatMap(
927
+ const pickedCustomTypes = linkCustomtypes.flatMap(
931
928
  (pickedCt) => allCustomTypes.find((ct) => ct.id === getId(pickedCt)) ?? [],
932
929
  );
933
930
 
@@ -950,92 +947,212 @@ function resolveContentRelationshipCustomTypes(
950
947
  * Converts a Link config `customtypes` ({@link LinkCustomtypes}) structure into
951
948
  * picker fields check map ({@link PickerCustomTypes}).
952
949
  */
953
- export function convertLinkCustomtypesToFieldCheckMap(
954
- customTypes: LinkCustomtypes,
955
- ): PickerCustomTypes {
956
- return customTypes.reduce<PickerCustomTypes>((customTypes, customType) => {
950
+ export function convertLinkCustomtypesToFieldCheckMap(args: {
951
+ linkCustomtypes: LinkCustomtypes;
952
+ allCustomTypes?: CustomType[];
953
+ }): PickerCustomTypes {
954
+ const { linkCustomtypes, allCustomTypes } = args;
955
+
956
+ const checkMap = linkCustomtypes.reduce((customTypes, customType) => {
957
957
  if (typeof customType === "string") return customTypes;
958
958
 
959
- customTypes[customType.id] = customType.fields.reduce<PickerCustomType>(
960
- (customTypeFields, field) => {
961
- if (typeof field === "string") {
962
- // Regular field
963
- customTypeFields[field] = { type: "checkbox", value: true };
964
- } else if ("fields" in field && field.fields !== undefined) {
965
- // Group field
966
- customTypeFields[field.id] = createGroupFieldCheckMap(field);
967
- } else if ("customtypes" in field && field.customtypes !== undefined) {
968
- // Content relationship field
969
- customTypeFields[field.id] =
970
- createContentRelationshipFieldCheckMap(field);
959
+ const existingCt = allCustomTypes?.find((ct) => ct.id === customType.id);
960
+ if (allCustomTypes && !existingCt) return customTypes;
961
+
962
+ const ctFlatFieldMap = getCustomTypeFlatFieldMap(existingCt);
963
+
964
+ const customTypeFields = customType.fields.reduce((fields, field) => {
965
+ // Check if the field exists
966
+ const existingField = ctFlatFieldMap.get(getId(field));
967
+ if (allCustomTypes && !existingField) return fields;
968
+
969
+ // Regular field
970
+ if (typeof field === "string") {
971
+ // Check if the field matched the existing one in the custom type
972
+ if (allCustomTypes && existingField && existingField.type === "Group") {
973
+ return fields;
971
974
  }
972
975
 
973
- return customTypeFields;
974
- },
975
- {},
976
- );
976
+ fields[field] = { type: "checkbox", value: true };
977
+ return fields;
978
+ }
979
+
980
+ // Group field
981
+ if ("fields" in field && field.fields !== undefined) {
982
+ // Check if the field matched the existing one in the custom type
983
+ if (allCustomTypes && existingField && existingField.type !== "Group") {
984
+ return fields;
985
+ }
986
+
987
+ const groupFieldCheckMap = createGroupFieldCheckMap({
988
+ group: field,
989
+ allCustomTypes,
990
+ ctFlatFieldMap,
991
+ });
992
+
993
+ if (groupFieldCheckMap) {
994
+ fields[field.id] = groupFieldCheckMap;
995
+ }
996
+
997
+ return fields;
998
+ }
999
+
1000
+ // Content relationship field
1001
+ if ("customtypes" in field && field.customtypes !== undefined) {
1002
+ // Check if the field matched the existing one in the custom type
1003
+ if (
1004
+ allCustomTypes &&
1005
+ existingField &&
1006
+ !isContentRelationshipField(existingField)
1007
+ ) {
1008
+ return fields;
1009
+ }
1010
+
1011
+ const crFieldCheckMap = createContentRelationshipFieldCheckMap({
1012
+ field,
1013
+ allCustomTypes,
1014
+ });
1015
+
1016
+ if (crFieldCheckMap) {
1017
+ fields[field.id] = crFieldCheckMap;
1018
+ }
1019
+
1020
+ return fields;
1021
+ }
1022
+
1023
+ return fields;
1024
+ }, {} as PickerCustomType);
1025
+
1026
+ if (Object.keys(customTypeFields).length > 0) {
1027
+ customTypes[customType.id] = customTypeFields;
1028
+ }
1029
+
977
1030
  return customTypes;
978
- }, {});
1031
+ }, {} as PickerCustomTypes);
1032
+
1033
+ return checkMap;
979
1034
  }
980
1035
 
981
- function createGroupFieldCheckMap(
982
- group: LinkCustomtypesGroupFieldValue,
983
- ): PickerFirstLevelGroupField {
984
- return {
985
- type: "group",
986
- value: group.fields.reduce<PickerFirstLevelGroupFieldValue>(
987
- (fields, field) => {
988
- if (typeof field === "string") {
989
- // Regular field
990
- fields[field] = { type: "checkbox", value: true };
991
- } else if ("customtypes" in field && field.customtypes !== undefined) {
992
- // Content relationship field
993
- fields[field.id] = createContentRelationshipFieldCheckMap(field);
994
- }
1036
+ function createGroupFieldCheckMap(args: {
1037
+ group: LinkCustomtypesGroupFieldValue;
1038
+ allCustomTypes?: CustomType[];
1039
+ ctFlatFieldMap: CustomTypeFlatFieldMap;
1040
+ }): PickerFirstLevelGroupField | undefined {
1041
+ const { group, ctFlatFieldMap, allCustomTypes } = args;
995
1042
 
1043
+ const fieldEntries = group.fields.reduce((fields, field) => {
1044
+ // Check if the field exists
1045
+ const existingField = ctFlatFieldMap.get(`${group.id}.${getId(field)}`);
1046
+ if (allCustomTypes && !existingField) return fields;
1047
+
1048
+ // Regular field
1049
+ if (typeof field === "string") {
1050
+ // Check if the field matched the existing one in the custom type
1051
+ if (allCustomTypes && existingField && existingField.type === "Group") {
996
1052
  return fields;
997
- },
998
- {},
999
- ),
1053
+ }
1054
+
1055
+ fields[field] = { type: "checkbox", value: true };
1056
+ return fields;
1057
+ }
1058
+
1059
+ // Content relationship field
1060
+ if ("customtypes" in field && field.customtypes !== undefined) {
1061
+ // Check if the field matched the existing one in the custom type
1062
+ if (
1063
+ allCustomTypes &&
1064
+ existingField &&
1065
+ !isContentRelationshipField(existingField)
1066
+ ) {
1067
+ return fields;
1068
+ }
1069
+
1070
+ const crFieldCheckMap = createContentRelationshipFieldCheckMap({
1071
+ field,
1072
+ allCustomTypes,
1073
+ });
1074
+
1075
+ if (crFieldCheckMap) {
1076
+ fields[field.id] = crFieldCheckMap;
1077
+ }
1078
+
1079
+ return fields;
1080
+ }
1081
+
1082
+ return fields;
1083
+ }, {} as PickerFirstLevelGroupFieldValue);
1084
+
1085
+ if (Object.keys(fieldEntries).length === 0) return undefined;
1086
+
1087
+ return {
1088
+ type: "group",
1089
+ value: fieldEntries,
1000
1090
  };
1001
1091
  }
1002
1092
 
1003
- function createContentRelationshipFieldCheckMap(
1004
- field: LinkCustomtypesContentRelationshipFieldValue,
1005
- ): PickerContentRelationshipField {
1006
- const crField: PickerContentRelationshipField = {
1007
- type: "contentRelationship",
1008
- value: {},
1009
- };
1010
- const crFieldCustomTypes = crField.value;
1093
+ function createContentRelationshipFieldCheckMap(args: {
1094
+ field: LinkCustomtypesContentRelationshipFieldValue;
1095
+ allCustomTypes?: CustomType[];
1096
+ }): PickerContentRelationshipField | undefined {
1097
+ const { field, allCustomTypes } = args;
1098
+
1099
+ const fieldEntries = field.customtypes.reduce((customTypes, customType) => {
1100
+ if (typeof customType === "string") return customTypes;
1011
1101
 
1012
- for (const customType of field.customtypes) {
1013
- if (typeof customType === "string") continue;
1102
+ const existingCt = allCustomTypes?.find((ct) => ct.id === customType.id);
1103
+ if (allCustomTypes && !existingCt) return customTypes;
1014
1104
 
1015
- crFieldCustomTypes[customType.id] ??= {};
1016
- const customTypeFields = crFieldCustomTypes[customType.id];
1105
+ const ctFlatFieldMap = getCustomTypeFlatFieldMap(existingCt);
1017
1106
 
1018
- for (const nestedField of customType.fields) {
1107
+ const ctFields = customType.fields.reduce((nestedFields, nestedField) => {
1108
+ // Regular field
1019
1109
  if (typeof nestedField === "string") {
1020
- // Regular field
1021
- customTypeFields[nestedField] = { type: "checkbox", value: true };
1022
- } else {
1023
- // Group field
1024
- const groupFieldsEntries = nestedField.fields.map(
1025
- (field) => [field, { type: "checkbox", value: true }] as const,
1026
- );
1110
+ // Check if the field matched the existing one in the custom type
1111
+ if (allCustomTypes && !ctFlatFieldMap.has(nestedField)) {
1112
+ return nestedFields;
1113
+ }
1114
+
1115
+ nestedFields[nestedField] = { type: "checkbox", value: true };
1116
+ return nestedFields;
1117
+ }
1027
1118
 
1028
- if (groupFieldsEntries.length > 0) {
1029
- customTypeFields[nestedField.id] = {
1030
- type: "group",
1031
- value: Object.fromEntries(groupFieldsEntries),
1032
- };
1119
+ // Group field
1120
+ const groupFields = nestedField.fields.reduce((fields, field) => {
1121
+ // Check if the field matched the existing one in the custom type
1122
+ if (
1123
+ allCustomTypes &&
1124
+ !ctFlatFieldMap.has(`${nestedField.id}.${field}`)
1125
+ ) {
1126
+ return fields;
1033
1127
  }
1128
+
1129
+ fields[field] = { type: "checkbox", value: true };
1130
+ return fields;
1131
+ }, {} as PickerLeafGroupFieldValue);
1132
+
1133
+ if (Object.keys(groupFields).length > 0) {
1134
+ nestedFields[nestedField.id] = {
1135
+ type: "group",
1136
+ value: groupFields,
1137
+ };
1034
1138
  }
1139
+
1140
+ return nestedFields;
1141
+ }, {} as PickerNestedCustomTypeValue);
1142
+
1143
+ if (Object.keys(ctFields).length > 0) {
1144
+ customTypes[customType.id] = ctFields;
1035
1145
  }
1036
- }
1037
1146
 
1038
- return crField;
1147
+ return customTypes;
1148
+ }, {} as PickerContentRelationshipFieldValue);
1149
+
1150
+ if (Object.keys(fieldEntries).length === 0) return undefined;
1151
+
1152
+ return {
1153
+ type: "contentRelationship",
1154
+ value: fieldEntries,
1155
+ };
1039
1156
  }
1040
1157
 
1041
1158
  /**
@@ -1044,27 +1161,22 @@ function createContentRelationshipFieldCheckMap(
1044
1161
  * made correctly and that the order is preserved.
1045
1162
  */
1046
1163
  function mergeAndConvertCheckMapToLinkCustomtypes(args: {
1047
- existingLinkCustomtypes: LinkCustomtypes | undefined;
1048
- previousPickerCustomtypes: PickerCustomTypes;
1164
+ linkCustomtypes: LinkCustomtypes | undefined;
1165
+ fieldCheckMap: PickerCustomTypes;
1049
1166
  newCustomType: PickerCustomType;
1050
1167
  customTypeId: string;
1051
1168
  }): LinkCustomtypes {
1052
- const {
1053
- existingLinkCustomtypes,
1054
- previousPickerCustomtypes,
1055
- newCustomType,
1056
- customTypeId,
1057
- } = args;
1169
+ const { linkCustomtypes, fieldCheckMap, newCustomType, customTypeId } = args;
1058
1170
 
1059
1171
  const result: NonReadonly<LinkCustomtypes> = [];
1060
1172
  const pickerLinkCustomtypes = convertFieldCheckMapToLinkCustomtypes({
1061
- ...previousPickerCustomtypes,
1173
+ ...fieldCheckMap,
1062
1174
  [customTypeId]: newCustomType,
1063
1175
  });
1064
1176
 
1065
- if (!existingLinkCustomtypes) return pickerLinkCustomtypes;
1177
+ if (!linkCustomtypes) return pickerLinkCustomtypes;
1066
1178
 
1067
- for (const existingLinkCt of existingLinkCustomtypes) {
1179
+ for (const existingLinkCt of linkCustomtypes) {
1068
1180
  const existingPickerLinkCt = pickerLinkCustomtypes.find((ct) => {
1069
1181
  return getId(ct) === getId(existingLinkCt);
1070
1182
  });
@@ -1236,6 +1348,10 @@ export function countPickedFields(
1236
1348
  );
1237
1349
  }
1238
1350
 
1351
+ function isContentRelationshipField(field: DynamicWidget): field is Link {
1352
+ return field.type === "Link" && field.config?.select === "document";
1353
+ }
1354
+
1239
1355
  /**
1240
1356
  * Check if the field is a Content Relationship Link with a **single** custom
1241
1357
  * type. CRs with multiple custom types are not currently supported (legacy).
@@ -1244,34 +1360,78 @@ function isContentRelationshipFieldWithSingleCustomtype(
1244
1360
  field: NestableWidget | Group,
1245
1361
  ): field is Link {
1246
1362
  return !!(
1247
- field.type === "Link" &&
1248
- field.config?.select === "document" &&
1363
+ isContentRelationshipField(field) &&
1249
1364
  field.config?.customtypes &&
1250
1365
  field.config.customtypes.length === 1
1251
1366
  );
1252
1367
  }
1253
1368
 
1369
+ type CustomTypeFlatFieldMap = {
1370
+ get: (fieldId: string) => DynamicWidget | undefined;
1371
+ has: (fieldId: string) => boolean;
1372
+ };
1373
+
1374
+ /**
1375
+ * Util that flattens a custom type's fields into a map of field ids to fields and
1376
+ * returns functions to get and check if a field exists by key.
1377
+ * For group fields, the key is separated by a dot (e.g. "group.fieldId").
1378
+ */
1379
+ function getCustomTypeFlatFieldMap(
1380
+ customType: CustomType | undefined,
1381
+ ): CustomTypeFlatFieldMap {
1382
+ if (!customType) return { get: () => undefined, has: () => false };
1383
+
1384
+ const ctFieldMap = Object.values(customType.json).reduce((acc, tabFields) => {
1385
+ for (const [fieldId, field] of Object.entries(tabFields)) {
1386
+ if (!isValidField(fieldId, field)) continue;
1387
+
1388
+ if (field.type === "Group") {
1389
+ acc.set(fieldId, field);
1390
+
1391
+ if (!field.config?.fields) continue;
1392
+
1393
+ for (const [groupId, group] of Object.entries(field.config.fields)) {
1394
+ if (!isValidField(groupId, group)) continue;
1395
+ acc.set(`${fieldId}.${groupId}`, group);
1396
+ }
1397
+ } else {
1398
+ acc.set(fieldId, field);
1399
+ }
1400
+ }
1401
+ return acc;
1402
+ }, new Map<string, DynamicWidget>());
1403
+
1404
+ return {
1405
+ get: (fieldId) => ctFieldMap.get(fieldId),
1406
+ has: (fieldId) => ctFieldMap.has(fieldId),
1407
+ };
1408
+ }
1409
+
1254
1410
  function getCustomTypeStaticFields(customType: CustomType) {
1255
1411
  return Object.values(customType.json).flatMap((tabFields) => {
1256
1412
  return Object.entries(tabFields).flatMap(([fieldId, field]) => {
1257
- if (
1258
- field.type !== "Slices" &&
1259
- field.type !== "Choice" &&
1260
- // Filter out uid fields because it's a special field returned by the
1261
- // API and is not part of the data object in the document.
1262
- // We also filter by key "uid", because (as of the time of writing
1263
- // this), creating any field with that API id will result in it being
1264
- // used for metadata.
1265
- (field.type !== "UID" || fieldId !== "uid")
1266
- ) {
1267
- return { fieldId, field: field as NestableWidget | Group };
1268
- }
1269
-
1270
- return [];
1413
+ return isValidField(fieldId, field) ? { fieldId, field } : [];
1271
1414
  });
1272
1415
  });
1273
1416
  }
1274
1417
 
1418
+ function isValidField(
1419
+ fieldId: string,
1420
+ field: DynamicWidget,
1421
+ ): field is NestableWidget | Group {
1422
+ return (
1423
+ field.type !== "Slices" &&
1424
+ field.type !== "Choice" &&
1425
+ // We don't display uid fields because they're a special field returned by
1426
+ // the API and they're not included in the document data object.
1427
+ // We also filter by key "uid", because (as of the time of writing this)
1428
+ // creating any field with that API id will result in it being used for
1429
+ // metadata, regardless of its type.
1430
+ field.type !== "UID" &&
1431
+ fieldId !== "uid"
1432
+ );
1433
+ }
1434
+
1275
1435
  function getGroupFields(group: Group) {
1276
1436
  if (!group.config?.fields) return [];
1277
1437
  return Object.entries(group.config.fields).map(([fieldId, field]) => {