apostrophe 3.17.0 → 3.18.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 (97) hide show
  1. package/.editorconfig +3 -0
  2. package/.eslintrc +4 -3
  3. package/.github/workflows/main.yml +2 -2
  4. package/.stylelintrc +12 -2
  5. package/CHANGELOG.md +34 -2
  6. package/defaults.js +2 -2
  7. package/index.js +124 -33
  8. package/lib/escape-host.js +8 -0
  9. package/lib/mongodb-connect.js +55 -0
  10. package/lib/opentelemetry.js +144 -0
  11. package/modules/@apostrophecms/area/ui/apos/apps/AposAreas.js +2 -0
  12. package/modules/@apostrophecms/area/ui/apos/components/AposAreaEditor.vue +20 -8
  13. package/modules/@apostrophecms/area/ui/apos/components/AposAreaWidget.vue +10 -0
  14. package/modules/@apostrophecms/asset/lib/globalIcons.js +1 -0
  15. package/modules/@apostrophecms/attachment/index.js +81 -29
  16. package/modules/@apostrophecms/db/index.js +7 -10
  17. package/modules/@apostrophecms/doc/index.js +138 -23
  18. package/modules/@apostrophecms/doc-type/index.js +162 -63
  19. package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocContextMenu.vue +39 -1
  20. package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocEditor.vue +11 -1
  21. package/modules/@apostrophecms/email/index.js +1 -1
  22. package/modules/@apostrophecms/express/index.js +2 -2
  23. package/modules/@apostrophecms/http/index.js +2 -1
  24. package/modules/@apostrophecms/i18n/i18n/en.json +10 -0
  25. package/modules/@apostrophecms/i18n/i18n/es.json +7 -0
  26. package/modules/@apostrophecms/i18n/i18n/pt-BR.json +7 -0
  27. package/modules/@apostrophecms/i18n/i18n/sk.json +7 -0
  28. package/modules/@apostrophecms/image/index.js +182 -1
  29. package/modules/@apostrophecms/image/ui/apos/apps/AposImageRelationshipQueryFilter.js +13 -0
  30. package/modules/@apostrophecms/image/ui/apos/components/AposImageCropper.vue +460 -0
  31. package/modules/@apostrophecms/image/ui/apos/components/AposImageRelationshipEditor.vue +510 -0
  32. package/modules/@apostrophecms/image/ui/apos/components/AposMediaManager.vue +5 -1
  33. package/modules/@apostrophecms/image/ui/apos/lib/aspectRatios.js +26 -0
  34. package/modules/@apostrophecms/image-widget/views/widget.html +5 -2
  35. package/modules/@apostrophecms/modal/ui/apos/mixins/AposEditorMixin.js +45 -1
  36. package/modules/@apostrophecms/module/index.js +98 -17
  37. package/modules/@apostrophecms/module/lib/events.js +46 -11
  38. package/modules/@apostrophecms/page/index.js +55 -22
  39. package/modules/@apostrophecms/piece-page-type/index.js +1 -0
  40. package/modules/@apostrophecms/piece-type/index.js +13 -4
  41. package/modules/@apostrophecms/piece-type/ui/apos/components/AposRelationshipEditor.vue +2 -2
  42. package/modules/@apostrophecms/rich-text-widget/index.js +1 -3
  43. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue +4 -0
  44. package/modules/@apostrophecms/schema/index.js +79 -73
  45. package/modules/@apostrophecms/schema/ui/apos/components/AposInputArea.vue +10 -0
  46. package/modules/@apostrophecms/schema/ui/apos/components/AposInputObject.vue +22 -3
  47. package/modules/@apostrophecms/schema/ui/apos/components/AposInputRelationship.vue +72 -36
  48. package/modules/@apostrophecms/schema/ui/apos/components/AposInputSelect.vue +7 -26
  49. package/modules/@apostrophecms/schema/ui/apos/components/AposInputString.vue +8 -0
  50. package/modules/@apostrophecms/schema/ui/apos/components/AposSchema.vue +45 -15
  51. package/modules/@apostrophecms/task/index.js +106 -52
  52. package/modules/@apostrophecms/template/index.js +111 -76
  53. package/modules/@apostrophecms/template/lib/custom-tags/component.js +42 -22
  54. package/modules/@apostrophecms/ui/ui/apos/components/AposSelect.vue +61 -0
  55. package/modules/@apostrophecms/ui/ui/apos/components/AposSlat.vue +46 -11
  56. package/modules/@apostrophecms/ui/ui/apos/components/AposSlatList.vue +10 -0
  57. package/modules/@apostrophecms/ui/ui/apos/components/AposTreeHeader.vue +2 -22
  58. package/modules/@apostrophecms/ui/ui/apos/utils/index.js +9 -0
  59. package/modules/@apostrophecms/widget-type/index.js +2 -23
  60. package/modules/@apostrophecms/widget-type/ui/apos/components/AposWidget.vue +1 -1
  61. package/modules/@apostrophecms/widget-type/ui/apos/components/AposWidgetEditor.vue +20 -1
  62. package/modules/@apostrophecms/widget-type/ui/apos/mixins/AposWidgetMixin.js +0 -9
  63. package/package.json +16 -12
  64. package/scripts/lint-i18n.js +2 -2
  65. package/test/assets.js +2 -1
  66. package/test/attachments.js +119 -26
  67. package/test/bundle.js +1 -1
  68. package/test/content-i18n.js +6 -6
  69. package/test/docs.js +244 -4
  70. package/test/draft-published.js +41 -41
  71. package/test/express.js +1 -1
  72. package/test/http.js +2 -2
  73. package/test/images.js +94 -4
  74. package/test/job.js +1 -1
  75. package/test/locks.js +1 -1
  76. package/test/middleware-and-route-order.js +3 -3
  77. package/test/pages-public-api.js +48 -4
  78. package/test/pages-rest.js +20 -20
  79. package/test/pages.js +377 -11
  80. package/test/parked-pages.js +1 -1
  81. package/test/permissions.js +10 -10
  82. package/test/pieces-public-api.js +130 -6
  83. package/test/pieces.js +247 -60
  84. package/test/recursionGuard.js +6 -6
  85. package/test/restApiRoutes.js +6 -6
  86. package/test/schemaBuilders.js +7 -7
  87. package/test/schemas.js +59 -59
  88. package/test/search.js +3 -3
  89. package/test/soft-redirects.js +13 -13
  90. package/test/static-i18n.js +1 -1
  91. package/test/templates.js +10 -10
  92. package/test/urls.js +2 -2
  93. package/test/users.js +21 -21
  94. package/test/utils.js +13 -13
  95. package/test/widgets.js +2 -2
  96. package/test-lib/util.js +2 -5
  97. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidget.vue +0 -26
@@ -1012,12 +1012,12 @@ module.exports = {
1012
1012
  // so consider that too
1013
1013
  const withType = field.name.replace(/^_/, '').replace(/s$/, '');
1014
1014
  if (!_.find(self.apos.doc.managers, { name: withType })) {
1015
- fail('withType property is missing. Hint: it must match the "name" property of a doc type. Or omit it and give your relationship the same name as the other type, with a leading _ and optional trailing s.');
1015
+ fail('withType property is missing. Hint: it must match the name of a doc type module. Or omit it and give your relationship the same name as the other type, with a leading _ and optional trailing s.');
1016
1016
  }
1017
1017
  field.withType = withType;
1018
1018
  }
1019
1019
  if (!field.withType) {
1020
- fail('withType property is missing. Hint: it must match the "name" property of a doc type.');
1020
+ fail('withType property is missing. Hint: it must match the name of a doc type module.');
1021
1021
  }
1022
1022
  if (Array.isArray(field.withType)) {
1023
1023
  _.each(field.withType, function (type) {
@@ -1025,6 +1025,18 @@ module.exports = {
1025
1025
  });
1026
1026
  } else {
1027
1027
  lintType(field.withType);
1028
+ const withTypeManager = self.apos.doc.getManager(field.withType);
1029
+ field.editor = field.editor || withTypeManager.options.relationshipEditor;
1030
+ field.postprocessor = field.postprocessor || withTypeManager.options.relationshipPostprocessor;
1031
+ field.editorLabel = field.editorLabel || withTypeManager.options.relationshipEditorLabel;
1032
+ field.editorIcon = field.editorIcon || withTypeManager.options.relationshipEditorIcon;
1033
+
1034
+ if (!field.schema && !Array.isArray(field.withType)) {
1035
+ const fieldsOption = withTypeManager.options.relationshipFields;
1036
+ const fields = fieldsOption && fieldsOption.add;
1037
+ field.fields = fields && klona(fields);
1038
+ field.schema = self.fieldsToArray(`Relationship field ${field.name}`, field.fields);
1039
+ }
1028
1040
  }
1029
1041
  if (field.schema && !field.fieldsStorage) {
1030
1042
  field.fieldsStorage = field.name.replace(/^_/, '') + 'Fields';
@@ -1204,52 +1216,7 @@ module.exports = {
1204
1216
  options.alterFields(schema);
1205
1217
  }
1206
1218
 
1207
- // always make sure there is a default group
1208
- let groups = [ {
1209
- name: defaultGroup.name,
1210
- label: defaultGroup.label,
1211
- fields: _.map(schema, 'name')
1212
- } ];
1213
-
1214
- // if we are getting arrangeFields and it's not empty
1215
- if (options.arrangeFields && options.arrangeFields.length > 0) {
1216
- // if it's full of strings, use them for the default group
1217
- if (_.isString(options.arrangeFields[0])) {
1218
- groups[0].fields = options.arrangeFields; // if it's full of objects, those are groups, so use them
1219
- } else if (_.isPlainObject(options.arrangeFields[0])) {
1220
- // reset the default group's fields, but keep it around,
1221
- // in case they have fields they forgot to put in a group
1222
- groups[0].fields = [];
1223
- groups = groups.concat(options.arrangeFields);
1224
- }
1225
- }
1226
-
1227
- // If there is a later group with the same name, the later
1228
- // one wins and the earlier is forgotten. Otherwise you can't
1229
- // ever toss a field out of a group without putting it into
1230
- // another one, which makes it impossible to un-group a
1231
- // field and have it appear outside of tabs in the interface.
1232
- //
1233
- // A reconfigured group is ordered to the bottom of the list
1234
- // of groups again, which has the intended effect if you
1235
- // arrange all of the groups in your module config. However
1236
- // it comes before any groups with the `last: true` flag that
1237
- // were not reconfigured. Reconfiguring a group without that
1238
- // flag clears it.
1239
-
1240
- const newGroups = [];
1241
- _.each(groups, function (group) {
1242
- const index = _.findIndex(newGroups, { name: group.name });
1243
- if (index !== -1) {
1244
- newGroups.splice(index, 1);
1245
- }
1246
- let i = _.findIndex(newGroups, { last: true });
1247
- if (i === -1) {
1248
- i = groups.length;
1249
- }
1250
- newGroups.splice(i, 0, group);
1251
- });
1252
- groups = newGroups;
1219
+ const groups = self.composeGroups(schema, options.arrangeFields);
1253
1220
 
1254
1221
  // all fields in the schema will end up in this variable
1255
1222
  let newSchema = [];
@@ -1349,6 +1316,56 @@ module.exports = {
1349
1316
  return schema;
1350
1317
  },
1351
1318
 
1319
+ composeGroups (schema, arrangeFields) {
1320
+ // always make sure there is a default group
1321
+ let groups = [ {
1322
+ name: defaultGroup.name,
1323
+ label: defaultGroup.label,
1324
+ fields: _.map(schema, 'name')
1325
+ } ];
1326
+
1327
+ // if we are getting arrangeFields and it's not empty
1328
+ if (arrangeFields && arrangeFields.length > 0) {
1329
+ // if it's full of strings, use them for the default group
1330
+ if (_.isString(arrangeFields[0])) {
1331
+ groups[0].fields = arrangeFields; // if it's full of objects, those are groups, so use them
1332
+ } else if (_.isPlainObject(arrangeFields[0])) {
1333
+ // reset the default group's fields, but keep it around,
1334
+ // in case they have fields they forgot to put in a group
1335
+ groups[0].fields = [];
1336
+ groups = groups.concat(arrangeFields);
1337
+ }
1338
+ }
1339
+
1340
+ // If there is a later group with the same name, the later
1341
+ // one wins and the earlier is forgotten. Otherwise you can't
1342
+ // ever toss a field out of a group without putting it into
1343
+ // another one, which makes it impossible to un-group a
1344
+ // field and have it appear outside of tabs in the interface.
1345
+ //
1346
+ // A reconfigured group is ordered to the bottom of the list
1347
+ // of groups again, which has the intended effect if you
1348
+ // arrange all of the groups in your module config. However
1349
+ // it comes before any groups with the `last: true` flag that
1350
+ // were not reconfigured. Reconfiguring a group without that
1351
+ // flag clears it.
1352
+
1353
+ const newGroups = [];
1354
+ _.each(groups, function (group) {
1355
+ const index = _.findIndex(newGroups, { name: group.name });
1356
+ if (index !== -1) {
1357
+ newGroups.splice(index, 1);
1358
+ }
1359
+ let i = _.findIndex(newGroups, { last: true });
1360
+ if (i === -1) {
1361
+ i = groups.length;
1362
+ }
1363
+ newGroups.splice(i, 0, group);
1364
+ });
1365
+
1366
+ return newGroups;
1367
+ },
1368
+
1352
1369
  // Recursively set moduleName property of the field and any subfields,
1353
1370
  // as might be found in array or object fields. `module` is an actual module
1354
1371
  setModuleName(field, module) {
@@ -1693,7 +1710,6 @@ module.exports = {
1693
1710
  }
1694
1711
  const find = options.find;
1695
1712
  const builders = options.builders || {};
1696
- const hints = options.hints || {};
1697
1713
  const getCriteria = options.getCriteria || {};
1698
1714
  await method(items, idsStorage, fieldsStorage, objectField, ids => {
1699
1715
  const idsCriteria = {};
@@ -1712,8 +1728,6 @@ module.exports = {
1712
1728
  // Builders hardcoded as part of this relationship's options don't
1713
1729
  // require any sanitization
1714
1730
  query.applyBuilders(builders);
1715
- // Hints, on the other hand, must be sanitized
1716
- query.applyBuildersSafely(hints);
1717
1731
  return query.toArray();
1718
1732
  }, self.apos.doc.toAposDocId);
1719
1733
  },
@@ -1854,8 +1868,7 @@ module.exports = {
1854
1868
 
1855
1869
  const options = {
1856
1870
  find: find,
1857
- builders: { relationships: withRelationshipsNext[relationship._dotPath] || false },
1858
- hints: {}
1871
+ builders: { relationships: withRelationshipsNext[relationship._dotPath] || false }
1859
1872
  };
1860
1873
  const subname = relationship.name + ':' + type;
1861
1874
  const _relationship = _.assign({}, relationship, {
@@ -1871,13 +1884,6 @@ module.exports = {
1871
1884
  if (_relationship.buildersByType && _relationship.buildersByType[type]) {
1872
1885
  _.extend(options.builders, _relationship.buildersByType[type]);
1873
1886
  }
1874
- if (_relationship.hints) {
1875
- _.extend(options.hints, _relationship.hints);
1876
- }
1877
- if (_relationship.hintsByType && _relationship.hintsByType[type]) {
1878
- _.extend(options.hints, _relationship.hints);
1879
- _.extend(options.hints, _relationship.hintsByType[type]);
1880
- }
1881
1887
  await self.apos.util.recursionGuard(req, `${_relationship.type}:${_relationship.withType}`, () => {
1882
1888
  // Allow options to the getter to be specified in the schema,
1883
1889
  return self.fieldTypes[_relationship.type].relate(req, _relationship, _objects, options);
@@ -1912,8 +1918,7 @@ module.exports = {
1912
1918
 
1913
1919
  const options = {
1914
1920
  find: find,
1915
- builders: { relationships: withRelationshipsNext[relationship._dotPath] || false },
1916
- hints: {}
1921
+ builders: { relationships: withRelationshipsNext[relationship._dotPath] || false }
1917
1922
  };
1918
1923
 
1919
1924
  // Allow options to the get() method to be
@@ -1921,9 +1926,6 @@ module.exports = {
1921
1926
  if (relationship.builders) {
1922
1927
  _.extend(options.builders, relationship.builders);
1923
1928
  }
1924
- if (relationship.hints) {
1925
- _.extend(options.hints, relationship.hints);
1926
- }
1927
1929
 
1928
1930
  // Allow options to the getter to be specified in the schema
1929
1931
  await self.apos.util.recursionGuard(req, `${relationship.type}:${relationship.withType}`, () => {
@@ -2479,25 +2481,24 @@ module.exports = {
2479
2481
  return idsStorageFields[name] || name;
2480
2482
  }
2481
2483
  },
2482
- groupsToArray(groups) {
2484
+ groupsToArray(groups = {}) {
2483
2485
  return Object.keys(groups).map(name => ({
2484
2486
  name,
2485
2487
  ...groups[name]
2486
2488
  }));
2487
2489
  },
2488
- fieldsToArray(context, fields) {
2490
+ fieldsToArray(context, fields = {}) {
2489
2491
  const result = [];
2490
2492
  for (const name of Object.keys(fields)) {
2491
2493
  const field = {
2492
2494
  name,
2493
2495
  ...fields[name]
2494
2496
  };
2497
+ const fieldTypesWithSchemas = [ 'object', 'array', 'relationship' ];
2495
2498
  // TODO same for relationship schemas but they are being refactored in another PR
2496
- if ((field.type === 'object') || (field.type === 'array') || (field.type === 'relationship')) {
2497
- if (field.type !== 'relationship') {
2498
- if (!field.fields) {
2499
- throw new Error(`${context}: the subfield ${name} requires a 'fields' property, with an 'add' subproperty containing its own fields.`);
2500
- }
2499
+ if (fieldTypesWithSchemas.includes(field.type)) {
2500
+ if (field.type !== 'relationship' && !field.fields) {
2501
+ throw new Error(`${context}: the subfield ${name} requires a 'fields' property, with an 'add' subproperty containing its own fields.`);
2501
2502
  }
2502
2503
  if (field.fields) {
2503
2504
  if (!field.fields.add) {
@@ -2507,11 +2508,16 @@ module.exports = {
2507
2508
  throw new Error(`${context}: the subfield ${name} must have a 'fields' property with an 'add' subproperty containing its own fields.`);
2508
2509
  }
2509
2510
  }
2510
- field.schema = self.fieldsToArray(context, field.fields.add || {});
2511
+
2512
+ field.schema = self.compose({
2513
+ addFields: self.fieldsToArray(context, field.fields.add),
2514
+ arrangeFields: self.groupsToArray(field.fields.group)
2515
+ });
2511
2516
  }
2512
2517
  }
2513
2518
  result.push(field);
2514
2519
  }
2520
+
2515
2521
  return result;
2516
2522
  },
2517
2523
  // Array "managers" currently offer just a schema property, for parallelism
@@ -22,6 +22,7 @@
22
22
  :id="next._id"
23
23
  :field-id="field._id"
24
24
  :field="field"
25
+ :generation="generation"
25
26
  @changed="changed"
26
27
  />
27
28
  </div>
@@ -37,6 +38,15 @@ import cuid from 'cuid';
37
38
  export default {
38
39
  name: 'AposInputArea',
39
40
  mixins: [ AposInputMixin ],
41
+ props: {
42
+ generation: {
43
+ type: Number,
44
+ required: false,
45
+ default() {
46
+ return null;
47
+ }
48
+ }
49
+ },
40
50
  data () {
41
51
  return {
42
52
  next: this.value.data || this.getEmptyValue(),
@@ -14,6 +14,7 @@
14
14
  :schema="field.schema"
15
15
  :trigger-validation="triggerValidation"
16
16
  :utility-rail="false"
17
+ :generation="generation"
17
18
  v-model="schemaInput"
18
19
  ref="schema"
19
20
  />
@@ -25,13 +26,21 @@
25
26
 
26
27
  <script>
27
28
  import AposInputMixin from 'Modules/@apostrophecms/schema/mixins/AposInputMixin.js';
28
- import { klona } from 'klona';
29
29
 
30
30
  export default {
31
31
  name: 'AposInputObject',
32
32
  mixins: [ AposInputMixin ],
33
+ props: {
34
+ generation: {
35
+ type: Number,
36
+ required: false,
37
+ default() {
38
+ return null;
39
+ }
40
+ }
41
+ },
33
42
  data () {
34
- const next = this.value ? this.value.data : (this.field.def || {});
43
+ const next = this.getNext();
35
44
  return {
36
45
  schemaInput: {
37
46
  data: next
@@ -42,6 +51,12 @@ export default {
42
51
  watch: {
43
52
  schemaInput() {
44
53
  this.next = this.schemaInput.data;
54
+ },
55
+ generation() {
56
+ this.next = this.getNext();
57
+ this.schemaInput = {
58
+ data: this.next
59
+ };
45
60
  }
46
61
  },
47
62
  methods: {
@@ -49,6 +64,10 @@ export default {
49
64
  if (this.schemaInput.hasErrors) {
50
65
  return 'invalid';
51
66
  }
67
+ },
68
+ // Return next at mount or when generation changes
69
+ getNext() {
70
+ return this.value ? this.value.data : (this.field.def || {});
52
71
  }
53
72
  }
54
73
  };
@@ -64,4 +83,4 @@ export default {
64
83
  .apos-input-object ::v-deep .apos-schema .apos-field {
65
84
  margin-bottom: 30px;
66
85
  }
67
- </style>
86
+ </style>
@@ -43,6 +43,8 @@
43
43
  :value="next"
44
44
  :disabled="field.readOnly"
45
45
  :has-relationship-schema="!!field.schema"
46
+ :editor-label="field.editorLabel"
47
+ :editor-icon="field.editorIcon"
46
48
  />
47
49
  <AposSearchList
48
50
  :list="searchList"
@@ -57,6 +59,7 @@
57
59
 
58
60
  <script>
59
61
  import AposInputMixin from 'Modules/@apostrophecms/schema/mixins/AposInputMixin';
62
+ import { klona } from 'klona';
60
63
 
61
64
  export default {
62
65
  name: 'AposInputRelationship',
@@ -64,18 +67,21 @@ export default {
64
67
  emits: [ 'input' ],
65
68
  data () {
66
69
  const next = (this.value && Array.isArray(this.value.data))
67
- ? this.value.data : (this.field.def || []);
70
+ ? klona(this.value.data) : (klona(this.field.def) || []);
71
+
72
+ // Remember relationship subfield values even if a document
73
+ // is temporarily deselected, easing the user's pain if they
74
+ // inadvertently deselect something for a moment
75
+ const subfields = Object.fromEntries(
76
+ (next || []).filter(doc => doc._fields)
77
+ .map(doc => [ doc._id, doc._fields ])
78
+ );
79
+
68
80
  return {
69
81
  searchTerm: '',
70
82
  searchList: [],
71
83
  next,
72
- // Remember relationship subfield values even if a document
73
- // is temporarily deselected, easing the user's pain if they
74
- // inadvertently deselect something for a moment
75
- subfields: Object.fromEntries((this.next || [])
76
- .filter(doc => doc._fields)
77
- .map(doc => [ doc._id, doc._fields ])
78
- ),
84
+ subfields,
79
85
  disabled: false,
80
86
  searching: false,
81
87
  choosing: false,
@@ -129,18 +135,16 @@ export default {
129
135
  }
130
136
  }
131
137
  },
138
+ mounted () {
139
+ this.checkLimit();
140
+ },
132
141
  methods: {
133
142
  validate(value) {
143
+ this.checkLimit();
144
+
134
145
  if (this.field.required && !value.length) {
135
146
  return { message: 'required' };
136
147
  }
137
- if (this.limitReached) {
138
- this.searchTerm = 'Limit reached!';
139
- this.disabled = true;
140
- } else {
141
- this.searchTerm = '';
142
- this.disabled = false;
143
- }
144
148
 
145
149
  if (this.field.min && this.field.min > value.length) {
146
150
  return { message: `minimum of ${this.field.min} required` };
@@ -148,31 +152,54 @@ export default {
148
152
 
149
153
  return false;
150
154
  },
155
+ checkLimit() {
156
+ if (this.limitReached) {
157
+ this.searchTerm = 'Limit reached!';
158
+ } else if (this.searchTerm === 'Limit reached!') {
159
+ this.searchTerm = '';
160
+ }
161
+
162
+ this.disabled = !!this.limitReached;
163
+ },
151
164
  updateSelected(items) {
152
165
  this.next = items;
153
166
  },
154
167
  async input () {
155
- if (!this.searching) {
156
- if (this.searchTerm.length) {
157
- this.searching = true;
158
- const list = await apos.http.get(`${apos.modules[this.field.withType].action}?autocomplete=${this.searchTerm}`, {
159
- busy: false,
160
- draft: true
161
- });
162
- // filter items already selected
163
- this.searchList = list.results.filter(item => {
164
- return !this.next.map(i => i._id).includes(item._id);
165
- }).map(item => {
166
- return {
167
- ...item,
168
- disabled: this.disableUnpublished && !item.lastPublishedAt
169
- };
170
- });
171
- this.searching = false;
172
- } else {
173
- this.searchList = [];
174
- }
168
+ if (this.searching) {
169
+ return;
170
+ }
171
+
172
+ if (!this.searchTerm.length) {
173
+ this.searchList = [];
174
+ return;
175
+ }
176
+
177
+ const qs = {
178
+ autocomplete: this.searchTerm
179
+ };
180
+
181
+ if (this.field.withType === '@apostrophecms/image') {
182
+ apos.bus.$emit('piece-relationship-query', qs);
175
183
  }
184
+
185
+ this.searching = true;
186
+ const list = await apos.http.get(
187
+ apos.modules[this.field.withType].action,
188
+ {
189
+ busy: false,
190
+ draft: true,
191
+ qs
192
+ }
193
+ );
194
+ // filter items already selected
195
+ this.searchList = list.results
196
+ .filter(item => !this.next.map(i => i._id).includes(item._id))
197
+ .map(item => ({
198
+ ...item,
199
+ disabled: this.disableUnpublished && !item.lastPublishedAt
200
+ }));
201
+
202
+ this.searching = false;
176
203
  },
177
204
  handleFocusOut() {
178
205
  // hide search list when click outside the input
@@ -198,11 +225,15 @@ export default {
198
225
  }
199
226
  },
200
227
  async editRelationship (item) {
201
- const result = await apos.modal.execute('AposRelationshipEditor', {
228
+ const editor = this.field.editor || 'AposRelationshipEditor';
229
+
230
+ const result = await apos.modal.execute(editor, {
202
231
  schema: this.field.schema,
232
+ item,
203
233
  title: item.title,
204
234
  value: item._fields
205
235
  });
236
+
206
237
  if (result) {
207
238
  const index = this.next.findIndex(_item => _item._id === item._id);
208
239
  this.$set(this.next, index, {
@@ -210,6 +241,11 @@ export default {
210
241
  _fields: result
211
242
  });
212
243
  }
244
+ },
245
+ getEditRelationshipLabel () {
246
+ if (this.field.editor === 'AposImageRelationshipEditor') {
247
+ return 'apostrophe:editImageAdjustments';
248
+ }
213
249
  }
214
250
  }
215
251
  };
@@ -7,26 +7,13 @@
7
7
  :display-options="displayOptions"
8
8
  >
9
9
  <template #body>
10
- <div class="apos-input-wrapper">
11
- <select
12
- class="apos-input apos-input--select" :id="uid"
13
- @change="change($event.target.value)"
14
- :disabled="field.readOnly"
15
- >
16
- <option
17
- v-for="choice in choices" :key="JSON.stringify(choice.value)"
18
- :value="JSON.stringify(choice.value)"
19
- :selected="choice.value === value.data"
20
- >
21
- {{ $t(choice.label) }}
22
- </option>
23
- </select>
24
- <AposIndicator
25
- icon="menu-down-icon"
26
- class="apos-input-icon"
27
- :icon-size="20"
28
- />
29
- </div>
10
+ <AposSelect
11
+ :icon="icon"
12
+ :choices="choices"
13
+ :disabled="field.readOnly"
14
+ :selected="value.data"
15
+ @change="change"
16
+ />
30
17
  </template>
31
18
  </AposInputWrapper>
32
19
  </template>
@@ -105,9 +92,3 @@ export default {
105
92
  }
106
93
  };
107
94
  </script>
108
-
109
- <style lang="scss" scoped>
110
- .apos-input-icon {
111
- @include apos-transition();
112
- }
113
- </style>
@@ -172,6 +172,14 @@ export default {
172
172
  if ((s == null) || (s === '')) {
173
173
  return s;
174
174
  } else {
175
+ // The native parse float converts 3.0 to 3 and makes
176
+ // next to become integer. In theory we don't need parseFloat
177
+ // as the value is natively guarded by the browser 'number' type.
178
+ // However we need a float value sent to the backend
179
+ // and we force that when focus is lost.
180
+ if (this.focus && `${s}`.match(/\.[0]*$/)) {
181
+ return s;
182
+ }
175
183
  return parseFloat(s);
176
184
  }
177
185
  } else {
@@ -1,5 +1,26 @@
1
1
  <!--
2
- AposSchema takes an array of fields with their values, renders their inputs, and emits their `input` events
2
+ AposSchema takes an array of fields (`schema`), renders their inputs,
3
+ and emits a new object with a `value` subproperty and a `hasErrors`
4
+ subproperty via the input event whenever the value of a field
5
+ or subfield changes.
6
+
7
+ At mount time the fields are initialized from the subproperties of the
8
+ `value.data` prop.
9
+
10
+ For performance reasons, this component is not strictly v-model compliant.
11
+ While all changes will emit an outgoing `input` event, the
12
+ incoming `value` prop only updates the fields in three situations:
13
+
14
+ 1. At mount time, to set the initial values of the fields.
15
+
16
+ 2. When `value.data._id` changes (an entirely different document is in play).
17
+
18
+ 3. When the optional prop `generation` changes to a new number. This
19
+ prop is also passed on to the individual input field components.
20
+
21
+ If you need to force an update from the calling component, increment the
22
+ `generation` prop. This should be done only if the value has changed for
23
+ an external reason.
3
24
  -->
4
25
  <template>
5
26
  <div class="apos-schema">
@@ -20,6 +41,7 @@
20
41
  :server-error="fields[field.name].serverError"
21
42
  :doc-id="docId"
22
43
  :ref="field.name"
44
+ :generation="generation"
23
45
  />
24
46
  </div>
25
47
  </div>
@@ -35,6 +57,13 @@ export default {
35
57
  type: Object,
36
58
  required: true
37
59
  },
60
+ generation: {
61
+ type: Number,
62
+ required: false,
63
+ default() {
64
+ return null;
65
+ }
66
+ },
38
67
  schema: {
39
68
  type: Array,
40
69
  required: true
@@ -135,22 +164,23 @@ export default {
135
164
  schema() {
136
165
  this.populateDocData();
137
166
  },
138
- value: {
139
- deep: true,
140
- handler(newVal, oldVal) {
141
- // The doc might be swapped out completely in cases such as the media
142
- // library editor. Repopulate the fields if that happens.
143
- if (
144
- // If the fieldState had been cleared and there's new populated data
145
- (!this.fieldState._id && newVal.data._id) ||
146
- // or if there *is* active fieldState, but the new data is a new doc
147
- (this.fieldState._id && newVal.data._id !== this.fieldState._id.data)
148
- ) {
149
- // repopulate the schema.
150
- this.populateDocData();
151
- }
167
+ 'value.data._id'(_id) {
168
+ // The doc might be swapped out completely in cases such as the media
169
+ // library editor. Repopulate the fields if that happens.
170
+ if (
171
+ // If the fieldState had been cleared and there's new populated data
172
+ (!this.fieldState._id && _id) ||
173
+ // or if there *is* active fieldState, but the new data is a new doc
174
+ (this.fieldState._id && _id !== this.fieldState._id.data)
175
+ ) {
176
+ // repopulate the schema.
177
+ this.populateDocData();
152
178
  }
153
179
  },
180
+ generation() {
181
+ // repopulate the schema.
182
+ this.populateDocData();
183
+ },
154
184
  conditionalFields(newVal, oldVal) {
155
185
  for (const field in oldVal) {
156
186
  if (!this.fieldState[field] || (newVal[field] === oldVal[field]) || !this.fieldState[field].ranValidation) {