slice-machine-ui 2.16.2-alpha.jp-cr-empty-customtypes.2 → 2.16.2-alpha.jp-cr-ui-fix-invalid-fields-checked.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.
@@ -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,11 +261,11 @@ 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
271
  function onCustomTypesChange(id: string, newCustomType: PickerCustomType) {
@@ -274,22 +275,24 @@ function ContentRelationshipFieldPickerContent(
274
275
  // represent new types added without any picked fields.
275
276
  onChange(
276
277
  mergeAndConvertCheckMapToLinkCustomtypes({
277
- existingLinkCustomtypes: value,
278
- previousPickerCustomtypes: fieldCheckMap,
279
- customTypeId: id,
278
+ fieldCheckMap,
280
279
  newCustomType,
280
+ linkCustomtypes,
281
+ customTypeId: id,
281
282
  }),
282
283
  );
283
284
  }
284
285
 
285
286
  function addCustomType(id: string) {
286
- const newFields = value ? [...value, id] : [id];
287
+ const newFields = linkCustomtypes ? [...linkCustomtypes, id] : [id];
287
288
  onChange(newFields);
288
289
  }
289
290
 
290
291
  function removeCustomType(id: string) {
291
- if (value) {
292
- onChange(value.filter((existingCt) => getId(existingCt) !== id));
292
+ if (linkCustomtypes) {
293
+ onChange(
294
+ linkCustomtypes.filter((existingCt) => getId(existingCt) !== id),
295
+ );
293
296
  }
294
297
  }
295
298
 
@@ -409,8 +412,9 @@ function ContentRelationshipFieldPickerContent(
409
412
  rel="noopener noreferrer"
410
413
  style={{ color: "inherit", textDecoration: "underline" }}
411
414
  >
412
- Please provide your feedback here.
415
+ Please provide your feedback here
413
416
  </a>
417
+ .
414
418
  </Text>
415
419
  </Box>
416
420
  </Box>
@@ -463,20 +467,20 @@ function AddTypeButton(props: AddTypeButtonProps) {
463
467
  </Button>
464
468
  );
465
469
 
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;
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
+ }
480
484
 
481
485
  return (
482
486
  <Box>
@@ -901,7 +905,7 @@ function getTypeFormatLabel(format: CustomType["format"]) {
901
905
  }
902
906
 
903
907
  /** Retrieves all existing page & custom types. */
904
- function useCustomTypes(value: LinkCustomtypes | undefined): {
908
+ function useCustomTypes(linkCustomtypes: LinkCustomtypes | undefined): {
905
909
  /** Every existing custom type, used to discover nested custom types down the tree and the add type dropdown. */
906
910
  allCustomTypes: CustomType[];
907
911
  /** The custom types that are already picked. */
@@ -913,14 +917,14 @@ function useCustomTypes(value: LinkCustomtypes | undefined): {
913
917
  void revalidateGetCustomTypes();
914
918
  }, []);
915
919
 
916
- if (!value) {
920
+ if (!linkCustomtypes) {
917
921
  return {
918
922
  allCustomTypes,
919
923
  pickedCustomTypes: [],
920
924
  };
921
925
  }
922
926
 
923
- const pickedCustomTypes = value.flatMap(
927
+ const pickedCustomTypes = linkCustomtypes.flatMap(
924
928
  (pickedCt) => allCustomTypes.find((ct) => ct.id === getId(pickedCt)) ?? [],
925
929
  );
926
930
 
@@ -943,92 +947,212 @@ function resolveContentRelationshipCustomTypes(
943
947
  * Converts a Link config `customtypes` ({@link LinkCustomtypes}) structure into
944
948
  * picker fields check map ({@link PickerCustomTypes}).
945
949
  */
946
- export function convertLinkCustomtypesToFieldCheckMap(
947
- customTypes: LinkCustomtypes,
948
- ): PickerCustomTypes {
949
- 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) => {
950
957
  if (typeof customType === "string") return customTypes;
951
958
 
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);
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;
964
974
  }
965
975
 
966
- return customTypeFields;
967
- },
968
- {},
969
- );
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
+
970
1030
  return customTypes;
971
- }, {});
1031
+ }, {} as PickerCustomTypes);
1032
+
1033
+ return checkMap;
972
1034
  }
973
1035
 
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
- }
1036
+ function createGroupFieldCheckMap(args: {
1037
+ group: LinkCustomtypesGroupFieldValue;
1038
+ allCustomTypes?: CustomType[];
1039
+ ctFlatFieldMap: CustomTypeFlatFieldMap;
1040
+ }): PickerFirstLevelGroupField | undefined {
1041
+ const { group, ctFlatFieldMap, allCustomTypes } = args;
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;
988
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") {
989
1052
  return fields;
990
- },
991
- {},
992
- ),
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,
993
1090
  };
994
1091
  }
995
1092
 
996
- function createContentRelationshipFieldCheckMap(
997
- field: LinkCustomtypesContentRelationshipFieldValue,
998
- ): PickerContentRelationshipField {
999
- const crField: PickerContentRelationshipField = {
1000
- type: "contentRelationship",
1001
- value: {},
1002
- };
1003
- 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;
1004
1101
 
1005
- for (const customType of field.customtypes) {
1006
- if (typeof customType === "string") continue;
1102
+ const existingCt = allCustomTypes?.find((ct) => ct.id === customType.id);
1103
+ if (allCustomTypes && !existingCt) return customTypes;
1007
1104
 
1008
- crFieldCustomTypes[customType.id] ??= {};
1009
- const customTypeFields = crFieldCustomTypes[customType.id];
1105
+ const ctFlatFieldMap = getCustomTypeFlatFieldMap(existingCt);
1010
1106
 
1011
- for (const nestedField of customType.fields) {
1107
+ const ctFields = customType.fields.reduce((nestedFields, nestedField) => {
1108
+ // Regular field
1012
1109
  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
- );
1110
+ // Check if the field matched the existing one in the custom type
1111
+ if (allCustomTypes && !ctFlatFieldMap.has(nestedField)) {
1112
+ return nestedFields;
1113
+ }
1020
1114
 
1021
- if (groupFieldsEntries.length > 0) {
1022
- customTypeFields[nestedField.id] = {
1023
- type: "group",
1024
- value: Object.fromEntries(groupFieldsEntries),
1025
- };
1115
+ nestedFields[nestedField] = { type: "checkbox", value: true };
1116
+ return nestedFields;
1117
+ }
1118
+
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;
1026
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
+ };
1027
1138
  }
1139
+
1140
+ return nestedFields;
1141
+ }, {} as PickerNestedCustomTypeValue);
1142
+
1143
+ if (Object.keys(ctFields).length > 0) {
1144
+ customTypes[customType.id] = ctFields;
1028
1145
  }
1029
- }
1030
1146
 
1031
- 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
+ };
1032
1156
  }
1033
1157
 
1034
1158
  /**
@@ -1037,27 +1161,22 @@ function createContentRelationshipFieldCheckMap(
1037
1161
  * made correctly and that the order is preserved.
1038
1162
  */
1039
1163
  function mergeAndConvertCheckMapToLinkCustomtypes(args: {
1040
- existingLinkCustomtypes: LinkCustomtypes | undefined;
1041
- previousPickerCustomtypes: PickerCustomTypes;
1164
+ linkCustomtypes: LinkCustomtypes | undefined;
1165
+ fieldCheckMap: PickerCustomTypes;
1042
1166
  newCustomType: PickerCustomType;
1043
1167
  customTypeId: string;
1044
1168
  }): LinkCustomtypes {
1045
- const {
1046
- existingLinkCustomtypes,
1047
- previousPickerCustomtypes,
1048
- newCustomType,
1049
- customTypeId,
1050
- } = args;
1169
+ const { linkCustomtypes, fieldCheckMap, newCustomType, customTypeId } = args;
1051
1170
 
1052
1171
  const result: NonReadonly<LinkCustomtypes> = [];
1053
1172
  const pickerLinkCustomtypes = convertFieldCheckMapToLinkCustomtypes({
1054
- ...previousPickerCustomtypes,
1173
+ ...fieldCheckMap,
1055
1174
  [customTypeId]: newCustomType,
1056
1175
  });
1057
1176
 
1058
- if (!existingLinkCustomtypes) return pickerLinkCustomtypes;
1177
+ if (!linkCustomtypes) return pickerLinkCustomtypes;
1059
1178
 
1060
- for (const existingLinkCt of existingLinkCustomtypes) {
1179
+ for (const existingLinkCt of linkCustomtypes) {
1061
1180
  const existingPickerLinkCt = pickerLinkCustomtypes.find((ct) => {
1062
1181
  return getId(ct) === getId(existingLinkCt);
1063
1182
  });
@@ -1229,6 +1348,10 @@ export function countPickedFields(
1229
1348
  );
1230
1349
  }
1231
1350
 
1351
+ function isContentRelationshipField(field: DynamicWidget): field is Link {
1352
+ return field.type === "Link" && field.config?.select === "document";
1353
+ }
1354
+
1232
1355
  /**
1233
1356
  * Check if the field is a Content Relationship Link with a **single** custom
1234
1357
  * type. CRs with multiple custom types are not currently supported (legacy).
@@ -1237,34 +1360,78 @@ function isContentRelationshipFieldWithSingleCustomtype(
1237
1360
  field: NestableWidget | Group,
1238
1361
  ): field is Link {
1239
1362
  return !!(
1240
- field.type === "Link" &&
1241
- field.config?.select === "document" &&
1363
+ isContentRelationshipField(field) &&
1242
1364
  field.config?.customtypes &&
1243
1365
  field.config.customtypes.length === 1
1244
1366
  );
1245
1367
  }
1246
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
+
1247
1410
  function getCustomTypeStaticFields(customType: CustomType) {
1248
1411
  return Object.values(customType.json).flatMap((tabFields) => {
1249
1412
  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 [];
1413
+ return isValidField(fieldId, field) ? { fieldId, field } : [];
1264
1414
  });
1265
1415
  });
1266
1416
  }
1267
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
+
1268
1435
  function getGroupFields(group: Group) {
1269
1436
  if (!group.config?.fields) return [];
1270
1437
  return Object.entries(group.config.fields).map(([fieldId, field]) => {