apostrophe 3.6.0 → 3.9.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 (71) hide show
  1. package/.eslintrc +4 -0
  2. package/.github/workflows/main.yml +45 -0
  3. package/CHANGELOG.md +92 -3
  4. package/README.md +2 -3
  5. package/index.js +104 -3
  6. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextBar.vue +5 -1
  7. package/modules/@apostrophecms/area/ui/apos/apps/AposAreas.js +15 -10
  8. package/modules/@apostrophecms/asset/index.js +105 -15
  9. package/modules/@apostrophecms/attachment/index.js +1 -4
  10. package/modules/@apostrophecms/db/index.js +5 -6
  11. package/modules/@apostrophecms/doc/index.js +2 -0
  12. package/modules/@apostrophecms/doc-type/index.js +39 -16
  13. package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocContextMenu.vue +13 -1
  14. package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocEditor.vue +0 -1
  15. package/modules/@apostrophecms/i18n/i18n/en.json +23 -4
  16. package/modules/@apostrophecms/i18n/i18n/es.json +1 -2
  17. package/modules/@apostrophecms/i18n/i18n/pt-BR.json +0 -1
  18. package/modules/@apostrophecms/i18n/i18n/sk.json +3 -4
  19. package/modules/@apostrophecms/i18n/index.js +36 -6
  20. package/modules/@apostrophecms/image/ui/apos/components/AposMediaManager.vue +6 -3
  21. package/modules/@apostrophecms/image-widget/index.js +2 -1
  22. package/modules/@apostrophecms/image-widget/views/widget.html +12 -2
  23. package/modules/@apostrophecms/job/index.js +165 -220
  24. package/modules/@apostrophecms/login/index.js +0 -15
  25. package/modules/@apostrophecms/migration/index.js +1 -1
  26. package/modules/@apostrophecms/modal/ui/apos/components/AposDocsManagerToolbar.vue +151 -61
  27. package/modules/@apostrophecms/modal/ui/apos/components/AposModalConfirm.vue +8 -6
  28. package/modules/@apostrophecms/modal/ui/apos/mixins/AposDocsManagerMixin.js +12 -15
  29. package/modules/@apostrophecms/module/index.js +1 -4
  30. package/modules/@apostrophecms/notification/index.js +116 -8
  31. package/modules/@apostrophecms/notification/ui/apos/components/AposNotification.vue +89 -11
  32. package/modules/@apostrophecms/notification/ui/apos/components/TheAposNotifications.vue +1 -1
  33. package/modules/@apostrophecms/page/index.js +84 -52
  34. package/modules/@apostrophecms/page-type/index.js +5 -1
  35. package/modules/@apostrophecms/piece-type/index.js +183 -61
  36. package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManager.vue +180 -50
  37. package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManagerDisplay.vue +1 -3
  38. package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManagerSelectBox.vue +141 -0
  39. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue +35 -6
  40. package/modules/@apostrophecms/schema/index.js +81 -25
  41. package/modules/@apostrophecms/schema/ui/apos/components/AposArrayEditor.vue +9 -3
  42. package/modules/@apostrophecms/schema/ui/apos/components/AposInputAttachment.vue +11 -160
  43. package/modules/@apostrophecms/schema/ui/apos/components/AposInputPassword.vue +11 -4
  44. package/modules/@apostrophecms/schema/ui/apos/components/AposInputRange.vue +2 -2
  45. package/modules/@apostrophecms/schema/ui/apos/components/AposInputSelect.vue +24 -6
  46. package/modules/@apostrophecms/schema/ui/apos/components/AposInputSlug.vue +0 -4
  47. package/modules/@apostrophecms/schema/ui/apos/components/AposInputString.vue +0 -7
  48. package/modules/@apostrophecms/schema/ui/apos/components/AposInputWrapper.vue +0 -2
  49. package/modules/@apostrophecms/schema/ui/apos/components/AposLogo.vue +1 -1
  50. package/modules/@apostrophecms/schema/ui/apos/components/AposLogoIcon.vue +1 -1
  51. package/modules/@apostrophecms/schema/ui/apos/components/AposLogoPadless.vue +1 -1
  52. package/modules/@apostrophecms/schema/ui/apos/components/AposSearchList.vue +0 -1
  53. package/modules/@apostrophecms/search/index.js +53 -33
  54. package/modules/@apostrophecms/task/index.js +7 -3
  55. package/modules/@apostrophecms/template/index.js +7 -11
  56. package/modules/@apostrophecms/ui/ui/apos/components/AposButton.vue +5 -0
  57. package/modules/@apostrophecms/ui/ui/apos/components/AposCellContextMenu.vue +1 -1
  58. package/modules/@apostrophecms/ui/ui/apos/components/AposFile.vue +205 -0
  59. package/modules/@apostrophecms/ui/ui/apos/components/AposMinMaxCount.vue +9 -3
  60. package/modules/@apostrophecms/ui/ui/apos/lib/i18next.js +16 -2
  61. package/modules/@apostrophecms/ui/ui/apos/mixins/AposPublishMixin.js +3 -2
  62. package/modules/@apostrophecms/ui/ui/apos/scss/global/_tables.scss +4 -3
  63. package/modules/@apostrophecms/util/ui/src/util.js +15 -0
  64. package/modules/@apostrophecms/widget-type/index.js +1 -1
  65. package/modules/@apostrophecms/widget-type/ui/apos/mixins/AposWidgetMixin.js +5 -19
  66. package/package.json +2 -2
  67. package/test/job.js +224 -0
  68. package/test/pieces.js +34 -0
  69. package/test-lib/util.js +32 -0
  70. package/.circleci/config.yml +0 -94
  71. package/.scratch.md +0 -2
@@ -2,7 +2,12 @@ const _ = require('lodash');
2
2
 
3
3
  module.exports = {
4
4
  extend: '@apostrophecms/doc-type',
5
- cascades: [ 'filters', 'columns', 'batchOperations' ],
5
+ cascades: [
6
+ 'filters',
7
+ 'columns',
8
+ 'batchOperations',
9
+ 'utilityOperations'
10
+ ],
6
11
  options: {
7
12
  perPage: 10,
8
13
  quickCreate: true,
@@ -74,7 +79,7 @@ module.exports = {
74
79
  ],
75
80
  // TODO: Delete `allowedInChooser` if not used.
76
81
  allowedInChooser: false,
77
- def: true
82
+ def: null
78
83
  },
79
84
  archived: {
80
85
  label: 'apostrophe:archive',
@@ -96,44 +101,47 @@ module.exports = {
96
101
  }
97
102
  }
98
103
  },
104
+ utilityOperations: {},
99
105
  batchOperations: {
100
106
  add: {
101
107
  archive: {
102
108
  label: 'apostrophe:archive',
103
- inputType: 'radio',
104
- unlessFilter: {
105
- archived: true
109
+ messages: {
110
+ progress: 'Archiving {{ type }}...',
111
+ completed: 'Archived {{ count }} {{ type }}.'
112
+ },
113
+ icon: 'archive-arrow-down-icon',
114
+ if: {
115
+ archived: false
116
+ },
117
+ modalOptions: {
118
+ title: 'apostrophe:archiveType',
119
+ description: 'apostrophe:archivingBatchConfirmation',
120
+ confirmationButton: 'apostrophe:archivingBatchConfirmationButton'
106
121
  }
107
122
  },
108
123
  restore: {
109
124
  label: 'apostrophe:restore',
110
- unlessFilter: {
111
- archived: false
112
- }
113
- },
114
- visibility: {
115
- label: 'apostrophe:visibility',
116
- requiredField: 'visibility',
117
- fields: {
118
- add: {
119
- visibility: {
120
- type: 'select',
121
- label: 'apostrophe:visibilityLabel',
122
- def: 'public',
123
- choices: [
124
- {
125
- value: 'public',
126
- label: 'apostrophe:public'
127
- },
128
- {
129
- value: 'loginRequired',
130
- label: 'apostrophe:loginRequired'
131
- }
132
- ]
133
- }
134
- }
125
+ messages: {
126
+ progress: 'Restoring {{ type }}...',
127
+ completed: 'Restoring {{ count }} {{ type }}.'
128
+ },
129
+ icon: 'archive-arrow-up-icon',
130
+ if: {
131
+ archived: true
132
+ },
133
+ modalOptions: {
134
+ title: 'apostrophe:restoreType',
135
+ description: 'apostrophe:restoreBatchConfirmation',
136
+ confirmationButton: 'apostrophe:restoreBatchConfirmationButton'
135
137
  }
136
138
  }
139
+ },
140
+ group: {
141
+ more: {
142
+ icon: 'dots-vertical-icon',
143
+ operations: []
144
+ }
137
145
  }
138
146
  },
139
147
  init(self) {
@@ -179,7 +187,6 @@ module.exports = {
179
187
  if (self.apos.launder.boolean(req.query['render-areas']) === true) {
180
188
  await self.apos.area.renderDocsAreas(req, result.results);
181
189
  }
182
- self.apos.attachment.all(result.results, { annotate: true });
183
190
  if (query.get('choicesResults')) {
184
191
  result.choices = query.get('choicesResults');
185
192
  }
@@ -259,6 +266,58 @@ module.exports = {
259
266
  }
260
267
  return self.publish(req, draft);
261
268
  },
269
+ async archive (req) {
270
+ if (!Array.isArray(req.body._ids)) {
271
+ throw self.apos.error('invalid');
272
+ }
273
+
274
+ req.body._ids = req.body._ids.map(_id => {
275
+ return self.inferIdLocaleAndMode(req, _id);
276
+ });
277
+
278
+ return self.apos.modules['@apostrophecms/job'].runBatch(
279
+ req,
280
+ req.body._ids,
281
+ async function(req, id) {
282
+ const piece = await self.findOneForEditing(req, { _id: id });
283
+
284
+ if (!piece) {
285
+ throw self.apos.error('notfound');
286
+ }
287
+
288
+ piece.archived = true;
289
+ await self.update(req, piece);
290
+ }, {
291
+ action: 'archive'
292
+ }
293
+ );
294
+ },
295
+ async restore (req) {
296
+ if (!Array.isArray(req.body._ids)) {
297
+ throw self.apos.error('invalid');
298
+ }
299
+
300
+ req.body._ids = req.body._ids.map(_id => {
301
+ return self.inferIdLocaleAndMode(req, _id);
302
+ });
303
+
304
+ return self.apos.modules['@apostrophecms/job'].runBatch(
305
+ req,
306
+ req.body._ids,
307
+ async function(req, id) {
308
+ const piece = await self.findOneForEditing(req, { _id: id });
309
+
310
+ if (!piece) {
311
+ throw self.apos.error('notfound');
312
+ }
313
+
314
+ piece.archived = false;
315
+ await self.update(req, piece);
316
+ }, {
317
+ action: 'restore'
318
+ }
319
+ );
320
+ },
262
321
  ':_id/localize': async (req) => {
263
322
  const _id = self.inferIdLocaleAndMode(req, req.params._id);
264
323
  const draft = await self.findOneForEditing(req.clone({
@@ -388,20 +447,77 @@ module.exports = {
388
447
  },
389
448
  'apostrophe:modulesRegistered': {
390
449
  composeBatchOperations() {
391
- self.batchOperations = Object.keys(self.batchOperations).map(key => ({
392
- name: key,
393
- ...self.batchOperations[key]
394
- })).filter(batchOperation => {
395
- if (batchOperation.requiredField && !_.find(self.schema, { name: batchOperation.requiredField })) {
396
- return false;
397
- }
398
- if (batchOperation.onlyIf) {
399
- if (!batchOperation.onlyIf(self.name)) {
400
- return false;
450
+ const groupedOperations = Object.entries(self.batchOperations)
451
+ .reduce((acc, [ opName, properties ]) => {
452
+ // Check if there is a required schema field for this batch operation.
453
+ const requiredFieldNotFound = properties.requiredField && !self.schema
454
+ .some((field) => field.name === properties.requiredField);
455
+
456
+ if (requiredFieldNotFound) {
457
+ return acc;
401
458
  }
459
+ // Find a group for the operation, if there is one.
460
+ const associatedGroup = getAssociatedGroup(opName);
461
+ const currentOperation = {
462
+ action: opName,
463
+ ...properties
464
+ };
465
+ const { action, ...props } = getOperationOrGroup(
466
+ currentOperation,
467
+ associatedGroup,
468
+ acc
469
+ );
470
+
471
+ return {
472
+ ...acc,
473
+ [action]: {
474
+ ...props
475
+ }
476
+ };
477
+ }, {});
478
+
479
+ self.batchOperations = Object.entries(groupedOperations)
480
+ .map(([ action, properties ]) => ({
481
+ action,
482
+ ...properties
483
+ }));
484
+
485
+ function getOperationOrGroup (currentOp, [ groupName, groupProperties ], acc) {
486
+ if (!groupName) {
487
+ // Operation is not grouped. Return it as it is.
488
+ return currentOp;
402
489
  }
403
- return true;
404
- });
490
+
491
+ // Return the operation group with the new operation added.
492
+ return {
493
+ name: groupName,
494
+ ...groupProperties,
495
+ operations: [
496
+ ...(acc[groupName] && acc[groupName].operations) || [],
497
+ currentOp
498
+ ]
499
+ };
500
+ }
501
+
502
+ // Returns the object entry, e.g., `[groupName, { ...groupProperties }]`
503
+ function getAssociatedGroup (operation) {
504
+ return Object.entries(self.batchOperationsGroups)
505
+ .find(([ _key, { operations } ]) => {
506
+ return operations.includes(operation);
507
+ }) || [];
508
+ }
509
+ },
510
+ composeUtilityOperations() {
511
+ self.utilityOperations = Object.entries(self.utilityOperations || {})
512
+ .map(([ action, properties ]) => ({
513
+ action,
514
+ ...properties
515
+ }));
516
+ }
517
+ },
518
+ '@apostrophecms/search:determineTypes': {
519
+ checkSearchable(types) {
520
+ self.searchDetermineTypes(types);
405
521
  }
406
522
  }
407
523
  };
@@ -538,33 +654,37 @@ module.exports = {
538
654
  // Pass `req`, the `name` of a configured batch operation, and
539
655
  // and a function that accepts (req, piece, data),
540
656
  // and returns a promise to perform the modification on that
541
- // one piece (including calling`update` if appropriate).
657
+ // one piece (including calling `update` if appropriate).
542
658
  //
543
659
  // `data` is an object containing any schema fields specified
544
660
  // for the batch operation. If there is no schema it will be
545
661
  // an empty object.
546
662
  //
547
- // Replies immediately to the request with `{ jobId: 'cxxxx' }`.
663
+ // Replies immediately to the request with `{ jobId: 'xxxxx' }`.
548
664
  // This can then be passed to appropriate browser-side APIs
549
665
  // to monitor progress.
550
666
  //
551
667
  // To avoid RAM issues with very large selections while ensuring
552
668
  // that all lifecycle events are fired correctly, the current
553
669
  // implementation processes the pieces in series.
554
- async batchSimpleRoute(req, name, change) {
555
- const batchOperation = _.find(self.batchOperations, { name: name });
556
- const schema = batchOperation.schema || [];
557
- const data = self.apos.schema.newInstance(schema);
558
- await self.apos.schema.convert(req, schema, req.body, data);
559
- await self.apos.modules['@apostrophecms/job'].run(req, one, { labels: { title: batchOperation.progressLabel || batchOperation.buttonLabel || batchOperation.label } });
560
- async function one(req, id) {
561
- const piece = self.findForEditing(req, { _id: id }).toObject();
562
- if (!piece) {
563
- throw self.apos.error('notfound');
564
- }
565
- await change(req, piece, data);
566
- }
567
- },
670
+ // TODO: restore this method when fully implemented.
671
+ // async batchSimpleRoute(req, name, change) {
672
+ // const batchOperation = _.find(self.batchOperations, { name: name });
673
+ // const schema = batchOperation.schema || [];
674
+ // const data = self.apos.schema.newInstance(schema);
675
+
676
+ // await self.apos.schema.convert(req, schema, req.body, data);
677
+ // await self.apos.modules['@apostrophecms/job'].runBatch(req, one, {
678
+ // // TODO: Update with new progress notification config
679
+ // });
680
+ // async function one(req, id) {
681
+ // const piece = self.findForEditing(req, { _id: id }).toObject();
682
+ // if (!piece) {
683
+ // throw self.apos.error('notfound');
684
+ // }
685
+ // await change(req, piece, data);
686
+ // }
687
+ // },
568
688
 
569
689
  // Accept a piece as untrusted input potentially
570
690
  // found in `input` (hint: you can pass `req.body`
@@ -765,7 +885,7 @@ module.exports = {
765
885
  return piece;
766
886
  },
767
887
  getRestQuery(req) {
768
- const query = self.find(req);
888
+ const query = self.find(req).attachments(true);
769
889
  query.applyBuildersSafely(req.query);
770
890
  if (!self.apos.permission.can(req, 'view-draft')) {
771
891
  if (!self.options.publicApiProjection) {
@@ -773,7 +893,7 @@ module.exports = {
773
893
  query.and({
774
894
  _id: null
775
895
  });
776
- } else {
896
+ } else if (!query.state.project) {
777
897
  query.project(self.options.publicApiProjection);
778
898
  }
779
899
  }
@@ -812,6 +932,7 @@ module.exports = {
812
932
  browserOptions.filters = self.filters;
813
933
  browserOptions.columns = self.columns;
814
934
  browserOptions.batchOperations = self.batchOperations;
935
+ browserOptions.utilityOperations = self.utilityOperations;
815
936
  browserOptions.insertViaUpload = self.options.insertViaUpload;
816
937
  browserOptions.quickCreate = !self.options.singleton && self.options.quickCreate && self.apos.permission.can(req, 'edit', self.name, 'draft');
817
938
  browserOptions.singleton = self.options.singleton;
@@ -828,6 +949,7 @@ module.exports = {
828
949
  editorModal: 'AposDocEditor',
829
950
  managerModal: 'AposDocsManager'
830
951
  });
952
+
831
953
  return browserOptions;
832
954
  },
833
955
  find(_super, req, criteria, projection) {
@@ -19,10 +19,10 @@
19
19
  </template>
20
20
  <template #primaryControls>
21
21
  <AposContextMenu
22
- v-if="moreMenu.menu.length"
23
- :button="moreMenu.button"
24
- :menu="moreMenu.menu"
25
- @item-clicked="moreMenuHandler"
22
+ v-if="utilityOperations.menu.length"
23
+ :button="utilityOperations.button"
24
+ :menu="utilityOperations.menu"
25
+ @item-clicked="utilityOperationsHandler"
26
26
  />
27
27
  <AposButton
28
28
  v-if="relationshipField"
@@ -66,19 +66,33 @@
66
66
  :selected-state="selectAllState"
67
67
  :total-pages="totalPages"
68
68
  :current-page="currentPage"
69
- :filters="moduleOptions.filters"
70
69
  :filter-choices="filterChoices"
71
70
  :filter-values="filterValues"
71
+ :filters="moduleOptions.filters"
72
72
  :labels="moduleLabels"
73
+ :displayed-items="items.length"
74
+ :is-relationship="!!relationshipField"
75
+ :checked-count="checked.length"
76
+ :batch-operations="moduleOptions.batchOperations"
73
77
  @select-click="selectAll"
74
78
  @search="search"
75
79
  @page-change="updatePage"
76
80
  @filter="filter"
81
+ @batch="handleBatchAction"
77
82
  :options="{
78
- disableUnchecked: maxReached(),
79
- hideSelectAll: !relationshipField
83
+ disableUnchecked: maxReached()
80
84
  }"
81
85
  />
86
+ <AposDocsManagerSelectBox
87
+ :selected-state="selectAllState"
88
+ :module-labels="moduleLabels"
89
+ :filter-values="filterValues"
90
+ :checked-ids="checked"
91
+ :all-pieces-selection="allPiecesSelection"
92
+ :displayed-items="items.length"
93
+ @select-all="selectAllPieces"
94
+ @set-all-pieces-selection="setAllPiecesSelection"
95
+ />
82
96
  </template>
83
97
  <template #bodyMain>
84
98
  <AposDocsManagerDisplay
@@ -90,7 +104,6 @@
90
104
  :options="{
91
105
  ...moduleOptions,
92
106
  disableUnchecked: maxReached(),
93
- hideCheckboxes: !relationshipField,
94
107
  disableUnpublished: disableUnpublished,
95
108
  manuallyPublished: manuallyPublished
96
109
  }"
@@ -136,7 +149,7 @@ export default {
136
149
  filterValues: {},
137
150
  queryExtras: {},
138
151
  holdQueries: false,
139
- moreMenu: {
152
+ utilityOperations: {
140
153
  button: {
141
154
  label: 'apostrophe:moreOperations',
142
155
  iconOnly: true,
@@ -145,7 +158,11 @@ export default {
145
158
  },
146
159
  menu: []
147
160
  },
148
- filterChoices: {}
161
+ filterChoices: {},
162
+ allPiecesSelection: {
163
+ isSelected: false,
164
+ total: 0
165
+ }
149
166
  };
150
167
  },
151
168
  computed: {
@@ -190,6 +207,16 @@ export default {
190
207
  },
191
208
  disableUnpublished() {
192
209
  return this.relationshipField && apos.modules[this.relationshipField.withType].localized;
210
+ },
211
+ selectAllChoice() {
212
+ const checkCount = this.checked.length;
213
+ const pageNotFullyChecked = this.items
214
+ .some((item) => !this.checked.includes(item._id));
215
+
216
+ return {
217
+ value: 'checked',
218
+ indeterminate: checkCount && pageNotFullyChecked
219
+ };
193
220
  }
194
221
  },
195
222
  created() {
@@ -206,17 +233,10 @@ export default {
206
233
  this.headers = this.computeHeaders();
207
234
  // Get the data. This will be more complex in actuality.
208
235
  this.modal.active = true;
209
- this.getPieces();
210
- if (this.relationshipField && this.moduleOptions.canEdit) {
211
- // Add computed singular label to context menu
212
- this.moreMenu.menu.unshift({
213
- action: 'new',
214
- label: {
215
- key: 'apostrophe:newDocType',
216
- type: this.$t(this.moduleLabels.singular)
217
- }
218
- });
219
- }
236
+ this.setUtilityOperations();
237
+ await this.getPieces();
238
+ await this.getAllPiecesTotal();
239
+
220
240
  apos.bus.$on('content-changed', this.getPieces);
221
241
  },
222
242
  destroyed() {
@@ -224,10 +244,13 @@ export default {
224
244
  apos.bus.$off('content-changed', this.getPieces);
225
245
  },
226
246
  methods: {
227
- moreMenuHandler(action) {
247
+ utilityOperationsHandler(action) {
228
248
  if (action === 'new') {
229
249
  this.create();
250
+ return;
230
251
  }
252
+
253
+ this.handleUtilityOperation(action);
231
254
  },
232
255
  setCheckedDocs(checked) {
233
256
  this.checkedDocs = checked;
@@ -238,6 +261,29 @@ export default {
238
261
  async create() {
239
262
  await this.edit(null);
240
263
  },
264
+ async handleUtilityOperation(action) {
265
+ const operation = this.utilityOperations.menu
266
+ .find((op) => op.action === action);
267
+
268
+ if (!operation) {
269
+ return;
270
+ }
271
+
272
+ const {
273
+ modal, ...modalOptions
274
+ } = operation.modalOptions || {};
275
+
276
+ if (modal) {
277
+ await apos.modal.execute(modal, {
278
+ moduleAction: this.moduleOptions.action,
279
+ action,
280
+ labels: this.moduleLabels,
281
+ messages: operation.messages,
282
+ ...modalOptions
283
+ });
284
+ }
285
+ },
286
+
241
287
  // If pieceOrId is null, a new piece is created
242
288
  async edit(pieceOrId) {
243
289
  let piece;
@@ -271,42 +317,68 @@ export default {
271
317
  async finishSaved() {
272
318
  await this.getPieces();
273
319
  },
274
- async getPieces () {
275
- if (this.holdQueries) {
276
- return;
277
- }
278
-
279
- this.holdQueries = true;
280
-
281
- const qs = {
320
+ async request (mergeOptions) {
321
+ const options = {
282
322
  ...this.filterValues,
283
- page: this.currentPage,
284
323
  ...this.queryExtras,
285
- // Also fetch published docs as _publishedDoc subproperties
324
+ ...mergeOptions,
286
325
  withPublished: 1
287
326
  };
288
327
 
289
328
  // Avoid undefined properties.
290
- for (const prop in qs) {
291
- if (qs[prop] === undefined) {
292
- delete qs[prop];
293
- };
329
+ const qs = Object.entries(options)
330
+ .reduce((acc, [ key, val ]) => ({
331
+ ...acc,
332
+ ...val !== undefined && { [key]: val }
333
+ }), {});
334
+
335
+ return apos.http.get(this.moduleOptions.action, {
336
+ qs,
337
+ busy: true,
338
+ draft: true
339
+ });
340
+ },
341
+ async getPieces () {
342
+ if (this.holdQueries) {
343
+ return;
294
344
  }
295
345
 
296
- const getResponse = await apos.http.get(
297
- this.moduleOptions.action, {
298
- busy: true,
299
- qs,
300
- draft: true
301
- }
302
- );
346
+ this.holdQueries = true;
303
347
 
304
- this.currentPage = getResponse.currentPage;
305
- this.totalPages = getResponse.pages;
306
- this.items = getResponse.results;
307
- this.filterChoices = getResponse.choices;
348
+ const {
349
+ currentPage, pages, results, choices
350
+ } = await this.request({
351
+ page: this.currentPage
352
+ });
353
+
354
+ this.currentPage = currentPage;
355
+ this.totalPages = pages;
356
+ this.items = results;
357
+ this.filterChoices = choices;
308
358
  this.holdQueries = false;
309
359
  },
360
+ async getAllPiecesTotal () {
361
+ const { count: total } = await this.request({ count: 1 });
362
+
363
+ this.setAllPiecesSelection({
364
+ isSelected: false,
365
+ total
366
+ });
367
+ },
368
+ async selectAllPieces () {
369
+ const { results: docs } = await this.request({
370
+ project: {
371
+ _id: 1
372
+ },
373
+ attachments: false,
374
+ perPage: this.allPiecesSelection.total
375
+ });
376
+
377
+ this.setAllPiecesSelection({
378
+ isSelected: true,
379
+ docs
380
+ });
381
+ },
310
382
  updatePage(num) {
311
383
  if (num) {
312
384
  this.currentPage = num;
@@ -325,6 +397,7 @@ export default {
325
397
  this.currentPage = 1;
326
398
 
327
399
  await this.getPieces();
400
+ await this.getAllPiecesTotal();
328
401
  },
329
402
  async filter(filter, value) {
330
403
  if (this.filterValues[filter] === value) {
@@ -334,10 +407,12 @@ export default {
334
407
  this.filterValues[filter] = value;
335
408
  this.currentPage = 1;
336
409
 
337
- this.getPieces();
410
+ await this.getPieces();
411
+ await this.getAllPiecesTotal();
338
412
  this.headers = this.computeHeaders();
339
- },
340
413
 
414
+ this.setCheckedDocs([]);
415
+ },
341
416
  shortcutNew(event) {
342
417
  const interesting = (event.keyCode === 78 || event.keyCode === 67); // C(reate) or N(ew)
343
418
  const topModal = apos.modal.stack[apos.modal.stack.length - 1] ? apos.modal.stack[apos.modal.stack.length - 1].id : null;
@@ -349,7 +424,6 @@ export default {
349
424
  this.create();
350
425
  }
351
426
  },
352
-
353
427
  bindShortcuts() {
354
428
  window.addEventListener('keydown', this.shortcutNew);
355
429
  },
@@ -376,6 +450,62 @@ export default {
376
450
  _fields: result
377
451
  });
378
452
  }
453
+ },
454
+ setAllPiecesSelection ({
455
+ isSelected, total, docs
456
+ }) {
457
+ if (typeof isSelected === 'boolean') {
458
+ this.allPiecesSelection.isSelected = isSelected;
459
+ }
460
+
461
+ if (typeof total === 'number') {
462
+ this.allPiecesSelection.total = total;
463
+ }
464
+
465
+ if (docs) {
466
+ this.setCheckedDocs(docs);
467
+ }
468
+ },
469
+ async handleBatchAction({
470
+ label, action, requestOptions = {}, messages
471
+ }) {
472
+ if (action) {
473
+ try {
474
+ await apos.http.post(`${this.moduleOptions.action}/${action}`, {
475
+ body: {
476
+ ...requestOptions,
477
+ _ids: this.checked,
478
+ messages: messages,
479
+ type: this.checked.length === 1 ? this.moduleLabels.singular
480
+ : this.moduleLabels.plural
481
+ }
482
+ });
483
+ } catch (error) {
484
+ apos.notify('apostrophe:errorBatchOperationNoti', {
485
+ interpolate: { operation: label },
486
+ type: 'danger'
487
+ });
488
+ console.error(error);
489
+ }
490
+ }
491
+ },
492
+ setUtilityOperations () {
493
+ const { utilityOperations } = this.moduleOptions;
494
+
495
+ const newPiece = {
496
+ action: 'new',
497
+ label: {
498
+ key: 'apostrophe:newDocType',
499
+ type: this.$t(this.moduleLabels.singular)
500
+ }
501
+ };
502
+
503
+ this.utilityOperations.menu = [
504
+ ...this.relationshipField && this.moduleOptions.canEdit
505
+ ? [ newPiece ] : [],
506
+ ...this.utilityOperations.menu,
507
+ ...(!this.relationshipField && Array.isArray(utilityOperations) && utilityOperations) || []
508
+ ];
379
509
  }
380
510
  }
381
511
  };