apostrophe 3.41.1 → 3.42.0

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 (43) hide show
  1. package/CHANGELOG.md +45 -1
  2. package/modules/@apostrophecms/admin-bar/index.js +1 -0
  3. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextBar.vue +1 -1
  4. package/modules/@apostrophecms/asset/lib/globalIcons.js +3 -0
  5. package/modules/@apostrophecms/doc/ui/apos/apps/AposDoc.js +42 -0
  6. package/modules/@apostrophecms/doc-type/index.js +82 -51
  7. package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocEditor.vue +10 -3
  8. package/modules/@apostrophecms/file/index.js +2 -1
  9. package/modules/@apostrophecms/file-tag/index.js +2 -1
  10. package/modules/@apostrophecms/i18n/i18n/en.json +115 -109
  11. package/modules/@apostrophecms/i18n/i18n/es.json +83 -78
  12. package/modules/@apostrophecms/i18n/i18n/fr.json +89 -84
  13. package/modules/@apostrophecms/i18n/i18n/pt-BR.json +81 -76
  14. package/modules/@apostrophecms/i18n/i18n/sk.json +91 -86
  15. package/modules/@apostrophecms/image/index.js +5 -1
  16. package/modules/@apostrophecms/image/ui/apos/components/AposMediaManager.vue +6 -1
  17. package/modules/@apostrophecms/image-tag/index.js +2 -1
  18. package/modules/@apostrophecms/modal/ui/apos/mixins/AposDocsManagerMixin.js +35 -2
  19. package/modules/@apostrophecms/modal/ui/apos/mixins/AposEditorMixin.js +2 -2
  20. package/modules/@apostrophecms/page/index.js +8 -4
  21. package/modules/@apostrophecms/piece-type/index.js +24 -3
  22. package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManager.vue +3 -63
  23. package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManagerDisplay.vue +9 -2
  24. package/modules/@apostrophecms/piece-type/ui/apos/components/AposUtilityOperations.vue +126 -0
  25. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue +10 -10
  26. package/modules/@apostrophecms/rich-text-widget/ui/apos/tiptap-extensions/Default.js +44 -48
  27. package/modules/@apostrophecms/schema/index.js +105 -35
  28. package/modules/@apostrophecms/schema/lib/addFieldTypes.js +21 -2
  29. package/modules/@apostrophecms/schema/ui/apos/components/AposInputArray.vue +339 -112
  30. package/modules/@apostrophecms/schema/ui/apos/components/AposInputRelationship.vue +72 -20
  31. package/modules/@apostrophecms/schema/ui/apos/components/AposInputSelect.vue +6 -0
  32. package/modules/@apostrophecms/schema/ui/apos/components/AposInputString.vue +1 -1
  33. package/modules/@apostrophecms/schema/ui/apos/components/AposInputWrapper.vue +7 -1
  34. package/modules/@apostrophecms/schema/ui/apos/components/AposSchema.vue +18 -4
  35. package/modules/@apostrophecms/schema/ui/apos/components/AposSearchList.vue +111 -30
  36. package/modules/@apostrophecms/submitted-draft/index.js +1 -1
  37. package/modules/@apostrophecms/ui/ui/apos/components/AposSlatList.vue +5 -1
  38. package/modules/@apostrophecms/ui/ui/apos/lib/i18next.js +1 -0
  39. package/modules/@apostrophecms/user/index.js +2 -1
  40. package/modules/@apostrophecms/widget-type/index.js +6 -3
  41. package/package.json +15 -15
  42. package/test/pieces.js +726 -13
  43. package/test/schemas.js +392 -22
@@ -505,19 +505,16 @@ module.exports = {
505
505
  if (convert) {
506
506
  try {
507
507
  await convert(req, field, data, destination);
508
- } catch (e) {
509
- if (Array.isArray(e)) {
508
+ } catch (error) {
509
+ if (Array.isArray(error)) {
510
510
  const invalid = self.apos.error('invalid', {
511
- errors: e
511
+ errors: error
512
512
  });
513
513
  invalid.path = field.name;
514
514
  errors.push(invalid);
515
515
  } else {
516
- if ((typeof e) !== 'string') {
517
- self.apos.util.error(e + '\n\n' + e.stack);
518
- }
519
- e.path = field.name;
520
- errors.push(e);
516
+ error.path = field.name;
517
+ errors.push(error);
521
518
  }
522
519
  }
523
520
  }
@@ -526,19 +523,26 @@ module.exports = {
526
523
  const errorsList = [];
527
524
 
528
525
  for (const error of errors) {
529
- const isVisible = await self.isVisible(req, schema, destination, error.path);
530
-
531
- if ((error.name === 'required' || error.name === 'mandatory') && !isVisible) {
532
- // It is not reasonable to enforce required for
533
- // fields hidden via conditional fields
534
- continue;
526
+ if ((error.name === 'required' || error.name === 'mandatory')) {
527
+ // `self.isVisible` will only throw for required fields that have
528
+ // an external condition containing an unknown module or method:
529
+ const isVisible = await self.isVisible(req, schema, destination, error.path);
530
+
531
+ if (!isVisible) {
532
+ // It is not reasonable to enforce required for
533
+ // fields hidden via conditional fields
534
+ continue;
535
+ }
535
536
  }
536
537
 
538
+ if (!Array.isArray(error) && typeof error !== 'string') {
539
+ self.apos.util.error(error + '\n\n' + error.stack);
540
+ }
537
541
  errorsList.push(error);
538
542
  }
539
543
 
540
544
  if (errorsList.length) {
541
- throw errors;
545
+ throw errorsList;
542
546
  }
543
547
  },
544
548
 
@@ -547,18 +551,30 @@ module.exports = {
547
551
 
548
552
  async isVisible(req, schema, object, name) {
549
553
  const conditionalFields = {};
554
+ const errors = {};
555
+
550
556
  while (true) {
551
557
  let change = false;
552
558
  for (const field of schema) {
553
559
  if (field.if) {
554
- const result = await evaluate(field.if, field.name, field.moduleName);
555
- const previous = conditionalFields[field.name];
556
- if (previous !== result) {
557
- change = true;
560
+ try {
561
+ const result = await evaluate(field.if, field.name, field.moduleName);
562
+ const previous = conditionalFields[field.name];
563
+ if (previous !== result) {
564
+ change = true;
565
+ }
566
+ conditionalFields[field.name] = result;
567
+ } catch (error) {
568
+ errors[field.name] = error;
558
569
  }
559
- conditionalFields[field.name] = result;
560
570
  }
561
571
  }
572
+
573
+ // send the error related to the given field via the `name` param
574
+ if (errors[name]) {
575
+ throw errors[name];
576
+ }
577
+
562
578
  if (!change) {
563
579
  break;
564
580
  }
@@ -588,7 +604,13 @@ module.exports = {
588
604
  // - `if: { 'moduleName:methodName()': 'expected value' }`
589
605
  // Checking if key ends with a closing parenthesis here to throw later if any argument is passed.
590
606
  if (key.endsWith(')')) {
591
- const externalConditionResult = await self.evaluateExternalCondition(req, key, fieldName, fieldModuleName, object._id);
607
+ let externalConditionResult;
608
+
609
+ try {
610
+ externalConditionResult = await self.evaluateMethod(req, key, fieldName, fieldModuleName, object._id);
611
+ } catch (error) {
612
+ throw self.apos.error('invalid', error.message);
613
+ }
592
614
 
593
615
  if (externalConditionResult !== val) {
594
616
  result = false;
@@ -613,11 +635,15 @@ module.exports = {
613
635
  }
614
636
  },
615
637
 
616
- async evaluateExternalCondition(req, conditionKey, fieldName, fieldModuleName, docId = null) {
617
- const [ methodDefinition ] = conditionKey.split('(');
638
+ async evaluateMethod(req, methodKey, fieldName, fieldModuleName, docId = null, optionalParenthesis = false) {
639
+ const [ methodDefinition, rest ] = methodKey.split('(');
640
+ const hasParenthesis = rest !== undefined;
618
641
 
619
- if (!conditionKey.endsWith('()')) {
620
- self.apos.util.warn(`Warning in the \`if\` definition of the "${fieldName}" field: "${methodDefinition}()" should not be passed any argument.`);
642
+ if (!hasParenthesis && !optionalParenthesis) {
643
+ throw new Error(`The method "${methodDefinition}" defined in the "${fieldName}" field should be written with parenthesis: "${methodDefinition}()".`);
644
+ }
645
+ if (hasParenthesis && !methodKey.endsWith('()')) {
646
+ self.apos.util.warn(`The method "${methodDefinition}" defined in the "${fieldName}" field should be written without argument: "${methodDefinition}()".`);
621
647
  }
622
648
 
623
649
  const [ methodName, moduleName = fieldModuleName ] = methodDefinition
@@ -627,9 +653,9 @@ module.exports = {
627
653
  const module = self.apos.modules[moduleName];
628
654
 
629
655
  if (!module) {
630
- throw new Error(`Error in the \`if\` definition of the "${fieldName}" field: "${moduleName}" module not found.`);
656
+ throw new Error(`The "${moduleName}" module defined in the "${fieldName}" field does not exist.`);
631
657
  } else if (!module[methodName]) {
632
- throw new Error(`Error in the \`if\` definition of the "${fieldName}" field: "${methodName}" method not found in "${moduleName}" module.`);
658
+ throw new Error(`The "${methodName}" method from "${moduleName}" module defined in the "${fieldName}" field does not exist.`);
633
659
  }
634
660
 
635
661
  return module[methodName](req, { docId });
@@ -910,17 +936,37 @@ module.exports = {
910
936
  // Currently `req` does not impact this, but that may change.
911
937
 
912
938
  prepareForStorage(req, doc) {
939
+ const can = (field) => {
940
+ return (!field.withType && !field.editPermission && !field.viewPermission) ||
941
+ (field.withType && self.apos.permission.can(req, 'view', field.withType)) ||
942
+ (field.editPermission && self.apos.permission.can(req, field.editPermission.action, field.editPermission.type)) ||
943
+ (field.viewPermission && self.apos.permission.can(req, field.viewPermission.action, field.viewPermission.type)) ||
944
+ false;
945
+ };
946
+
913
947
  const handlers = {
914
948
  arrayItem: (field, object) => {
949
+ if (!can(field)) {
950
+ return;
951
+ }
952
+
915
953
  object._id = object._id || self.apos.util.generateId();
916
954
  object.metaType = 'arrayItem';
917
955
  object.scopedArrayName = field.scopedArrayName;
918
956
  },
919
957
  object: (field, object) => {
958
+ if (!can(field)) {
959
+ return;
960
+ }
961
+
920
962
  object.metaType = 'object';
921
963
  object.scopedObjectName = field.scopedObjectName;
922
964
  },
923
965
  relationship: (field, doc) => {
966
+ if (!can(field)) {
967
+ return;
968
+ }
969
+
924
970
  doc[field.idsStorage] = doc[field.name].map(relatedDoc => self.apos.doc.toAposDocId(relatedDoc));
925
971
  if (field.fieldsStorage) {
926
972
  const fieldsById = doc[field.fieldsStorage] || {};
@@ -1169,7 +1215,7 @@ module.exports = {
1169
1215
  },
1170
1216
 
1171
1217
  // Validates a single schema field. See `validate`.
1172
- validateField(field, options) {
1218
+ validateField(field, options, parent = null) {
1173
1219
  const fieldType = self.fieldTypes[field.type];
1174
1220
  if (!fieldType) {
1175
1221
  fail('Unknown schema field type.');
@@ -1183,6 +1229,15 @@ module.exports = {
1183
1229
  if (field.if && field.if.$or && !Array.isArray(field.if.$or)) {
1184
1230
  fail(`$or conditional must be an array of conditions. Current $or configuration: ${JSON.stringify(field.if.$or)}`);
1185
1231
  }
1232
+ if (!field.editPermission && field.permission) {
1233
+ field.editPermission = field.permission;
1234
+ }
1235
+ if (options.type !== 'doc type' && (field.editPermission || field.viewPermission)) {
1236
+ warn(`editPermission or viewPermission must be defined on doc-type schemas only, "${options.type}" provided`);
1237
+ }
1238
+ if (options.type === 'doc type' && (field.editPermission || field.viewPermission) && parent) {
1239
+ warn(`editPermission or viewPermission must be defined on root fields only, provided on "${parent.name}.${field.name}"`);
1240
+ }
1186
1241
  if (fieldType.validate) {
1187
1242
  fieldType.validate(field, options, warn, fail);
1188
1243
  }
@@ -1193,8 +1248,12 @@ module.exports = {
1193
1248
  self.apos.util.error(format(s));
1194
1249
  }
1195
1250
  function format(s) {
1251
+ const fieldName = parent && parent.name
1252
+ ? `${parent.name}.${field.name}`
1253
+ : field.name;
1254
+
1196
1255
  return stripIndents`
1197
- ${options.type} ${options.subtype}, ${field.type} field "${field.name}":
1256
+ ${options.type} ${options.subtype}, ${field.type} field "${fieldName}":
1198
1257
 
1199
1258
  ${s}
1200
1259
 
@@ -1520,9 +1579,16 @@ module.exports = {
1520
1579
  },
1521
1580
 
1522
1581
  async getChoices(req, field) {
1523
- return typeof field.choices === 'string'
1524
- ? self.apos.modules[field.moduleName][field.choices](req)
1525
- : field.choices;
1582
+ if (typeof field.choices !== 'string') {
1583
+ return field.choices;
1584
+ }
1585
+
1586
+ try {
1587
+ const result = await self.evaluateMethod(req, field.choices, field.name, field.moduleName, null, true);
1588
+ return result;
1589
+ } catch (error) {
1590
+ throw self.apos.error('invalid', error.message);
1591
+ }
1526
1592
  }
1527
1593
 
1528
1594
  };
@@ -1542,7 +1608,11 @@ module.exports = {
1542
1608
  ) {
1543
1609
  throw self.apos.error('invalid');
1544
1610
  }
1545
- choices = await self.apos.modules[field.moduleName][field.choices](req, { docId });
1611
+ try {
1612
+ choices = await self.evaluateMethod(req, field.choices, field.name, field.moduleName, docId, true);
1613
+ } catch (error) {
1614
+ throw self.apos.error('invalid', error.message);
1615
+ }
1546
1616
  if (Array.isArray(choices)) {
1547
1617
  return {
1548
1618
  choices
@@ -1559,8 +1629,8 @@ module.exports = {
1559
1629
  const field = self.getFieldById(fieldId);
1560
1630
 
1561
1631
  try {
1562
- const result = await self.evaluateExternalCondition(req, conditionKey, field.name, field.moduleName, docId);
1563
- return result;
1632
+ const result = await self.evaluateMethod(req, conditionKey, field.name, field.moduleName, docId);
1633
+ return { result };
1564
1634
  } catch (error) {
1565
1635
  throw self.apos.error('invalid', error.message);
1566
1636
  }
@@ -752,6 +752,19 @@ module.exports = (self) => {
752
752
  if ((field.max !== undefined) && (results.length > field.max)) {
753
753
  throw self.apos.error('max');
754
754
  }
755
+ if (data.length && field.schema && field.schema.length) {
756
+ const { name: uniqueFieldName, label: uniqueFieldLabel } = field.schema.find(subfield => subfield.unique) || [];
757
+ if (uniqueFieldName) {
758
+ const duplicates = data
759
+ .map(item => Array.isArray(item[uniqueFieldName])
760
+ ? item[uniqueFieldName][0]._id
761
+ : item[uniqueFieldName])
762
+ .filter((item, index, array) => array.indexOf(item) !== index);
763
+ if (duplicates.length) {
764
+ throw self.apos.error('duplicate', `${req.t(uniqueFieldLabel)} in ${req.t(field.label)} must be unique`);
765
+ }
766
+ }
767
+ }
755
768
  if (errors.length) {
756
769
  throw errors;
757
770
  }
@@ -766,7 +779,7 @@ module.exports = (self) => {
766
779
  },
767
780
  validate: function (field, options, warn, fail) {
768
781
  for (const subField of field.schema || field.fields.add) {
769
- self.validateField(subField, options);
782
+ self.validateField(subField, options, field);
770
783
  }
771
784
  },
772
785
  register: function (metaType, type, field) {
@@ -833,7 +846,7 @@ module.exports = (self) => {
833
846
  },
834
847
  validate: function (field, options, warn, fail) {
835
848
  for (const subField of field.schema || field.fields.add) {
836
- self.validateField(subField, options);
849
+ self.validateField(subField, options, field);
837
850
  }
838
851
  },
839
852
  isEqual(req, field, one, two) {
@@ -1042,6 +1055,12 @@ module.exports = (self) => {
1042
1055
  field.postprocessor = field.postprocessor || withTypeManager.options.relationshipPostprocessor;
1043
1056
  field.editorLabel = field.editorLabel || withTypeManager.options.relationshipEditorLabel;
1044
1057
  field.editorIcon = field.editorIcon || withTypeManager.options.relationshipEditorIcon;
1058
+ field.suggestionLabel = field.suggestionLabel || withTypeManager.options.relationshipSuggestionLabel;
1059
+ field.suggestionHelp = field.suggestionHelp || withTypeManager.options.relationshipSuggestionHelp;
1060
+ field.suggestionLimit = field.suggestionLimit || withTypeManager.options.relationshipSuggestionLimit;
1061
+ field.suggestionSort = field.suggestionSort || withTypeManager.options.relationshipSuggestionSort;
1062
+ field.suggestionIcon = field.suggestionIcon || withTypeManager.options.relationshipSuggestionIcon;
1063
+ field.suggestionFields = field.suggestionFields || withTypeManager.options.relationshipSuggestionFields;
1045
1064
 
1046
1065
  if (!field.schema && !Array.isArray(field.withType)) {
1047
1066
  const fieldsOption = withTypeManager.options.relationshipFields;