apostrophe 3.8.0 → 3.10.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 (53) hide show
  1. package/.github/workflows/main.yml +45 -0
  2. package/CHANGELOG.md +52 -0
  3. package/README.md +1 -2
  4. package/lib/moog.js +26 -1
  5. package/modules/@apostrophecms/admin-bar/ui/apos/apps/AposAdminBar.js +7 -1
  6. package/modules/@apostrophecms/any-page-type/index.js +2 -0
  7. package/modules/@apostrophecms/area/ui/apos/apps/AposAreas.js +28 -11
  8. package/modules/@apostrophecms/asset/index.js +35 -4
  9. package/modules/@apostrophecms/asset/lib/webpack/apos/webpack.config.js +2 -2
  10. package/modules/@apostrophecms/asset/lib/webpack/src/webpack.config.js +1 -1
  11. package/modules/@apostrophecms/attachment/index.js +0 -4
  12. package/modules/@apostrophecms/busy/ui/apos/apps/AposBusy.js +3 -1
  13. package/modules/@apostrophecms/doc-type/index.js +27 -20
  14. package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocEditor.vue +0 -1
  15. package/modules/@apostrophecms/i18n/i18n/en.json +8 -0
  16. package/modules/@apostrophecms/i18n/i18n/es.json +1 -1
  17. package/modules/@apostrophecms/i18n/index.js +26 -5
  18. package/modules/@apostrophecms/job/index.js +10 -17
  19. package/modules/@apostrophecms/login/ui/apos/apps/AposLogin.js +3 -2
  20. package/modules/@apostrophecms/modal/ui/apos/apps/AposModals.js +8 -5
  21. package/modules/@apostrophecms/modal/ui/apos/components/AposDocsManagerToolbar.vue +1 -1
  22. package/modules/@apostrophecms/module/index.js +1 -4
  23. package/modules/@apostrophecms/notification/ui/apos/apps/AposNotification.js +3 -1
  24. package/modules/@apostrophecms/notification/ui/apos/components/AposNotification.vue +1 -1
  25. package/modules/@apostrophecms/page/index.js +47 -22
  26. package/modules/@apostrophecms/page-type/index.js +5 -1
  27. package/modules/@apostrophecms/piece-type/index.js +5 -0
  28. package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManager.vue +2 -1
  29. package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManagerSelectBox.vue +27 -24
  30. package/modules/@apostrophecms/rich-text-widget/index.js +2 -1
  31. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue +9 -2
  32. package/modules/@apostrophecms/schema/index.js +7 -19
  33. package/modules/@apostrophecms/schema/ui/apos/components/AposArrayEditor.vue +9 -3
  34. package/modules/@apostrophecms/schema/ui/apos/components/AposInputCheckboxes.vue +3 -3
  35. package/modules/@apostrophecms/schema/ui/apos/components/AposInputPassword.vue +11 -4
  36. package/modules/@apostrophecms/schema/ui/apos/components/AposInputRange.vue +2 -2
  37. package/modules/@apostrophecms/schema/ui/apos/components/AposInputWrapper.vue +0 -2
  38. package/modules/@apostrophecms/schema/ui/apos/components/AposLogo.vue +1 -1
  39. package/modules/@apostrophecms/schema/ui/apos/components/AposLogoIcon.vue +1 -1
  40. package/modules/@apostrophecms/schema/ui/apos/components/AposLogoPadless.vue +1 -1
  41. package/modules/@apostrophecms/schema/ui/apos/components/AposSearchList.vue +0 -1
  42. package/modules/@apostrophecms/search/index.js +53 -33
  43. package/modules/@apostrophecms/task/index.js +5 -1
  44. package/modules/@apostrophecms/template/index.js +5 -11
  45. package/modules/@apostrophecms/ui/ui/apos/components/AposMinMaxCount.vue +9 -3
  46. package/modules/@apostrophecms/ui/ui/apos/mixins/AposPublishMixin.js +3 -2
  47. package/modules/@apostrophecms/widget-type/index.js +9 -0
  48. package/modules/@apostrophecms/widget-type/ui/apos/mixins/AposWidgetMixin.js +5 -27
  49. package/package.json +3 -3
  50. package/test/moog.js +48 -0
  51. package/test/pieces.js +17 -0
  52. package/.circleci/config.yml +0 -94
  53. package/.scratch.md +0 -2
@@ -0,0 +1,45 @@
1
+ # This is a basic workflow to help you get started with Actions
2
+
3
+ name: Tests
4
+
5
+ # Controls when the action will run.
6
+ on:
7
+ push:
8
+ branches: [ '*' ]
9
+ pull_request:
10
+ branches: [ '*' ]
11
+
12
+ # Allows you to run this workflow manually from the Actions tab
13
+ workflow_dispatch:
14
+
15
+ # A workflow run is made up of one or more jobs that can run sequentially or in parallel
16
+ jobs:
17
+ # This workflow contains a single job called "build"
18
+ build:
19
+ # The type of runner that the job will run on
20
+ runs-on: ubuntu-latest
21
+ strategy:
22
+ matrix:
23
+ node-version: [12, 14, 16]
24
+ mongodb-version: [4.2, 4.4, 5.0]
25
+
26
+ # Steps represent a sequence of tasks that will be executed as part of the job
27
+ steps:
28
+ - name: Git checkout
29
+ uses: actions/checkout@v2
30
+
31
+ - name: Use Node.js ${{ matrix.node-version }}
32
+ uses: actions/setup-node@v1
33
+ with:
34
+ node-version: ${{ matrix.node-version }}
35
+
36
+ - name: Start MongoDB
37
+ uses: supercharge/mongodb-github-action@1.3.0
38
+ with:
39
+ mongodb-version: ${{ matrix.mongodb-version }}
40
+
41
+ - run: npm install
42
+
43
+ - run: npm test
44
+ env:
45
+ CI: true
package/CHANGELOG.md CHANGED
@@ -1,5 +1,57 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.10.0 - 2021-12-22
4
+
5
+ ### Fixes
6
+
7
+ * `slug` type fields can now have an empty string or `null` as their `def` value without the string `'none'` populating automatically.
8
+ * The `underline` feature works properly in tiptap toolbar configuration.
9
+ * Required checkbox fields now properly prevent editor submission when empty.
10
+ * Pins `vue-click-outside-element` to a version that does not attempt to use `eval` in its distribution build, which is incompatible with a strict Content Security Policy.
11
+
12
+ ### Adds
13
+
14
+ * Adds a `last` option to fields. Setting `last: true` on a field puts that field at the end of the field's widget order. If more than one field has that option active the true last item will depend on general field registration order. If the field is ordered with the `fields.order` array or field group ordering, those specified orders will take precedence.
15
+
16
+ ### Changes
17
+
18
+ * Adds deprecation notes to the widget class methods `getWidgetWrapperClasses` and `getWidgetClasses` from A2.
19
+ * Adds a deprecation note to the `reorganize` query builder for the next major version.
20
+ * Uses the runtime build of Vue. This has major performance and bundle size benefits, however it does require changes to Apostrophe admin UI apps that use a `template` property (components should require no changes, just apps require an update). These apps must now use a `render` function instead. Since custom admin UI apps are not yet a documented feature we do not regard this as a bc break.
21
+ * Compatible with the `@apostrophecms/security-headers` module, which supports a strict `Content-Security-Policy`.
22
+ * Adds a deprecation note to the `addLateCriteria` query builder.
23
+ * Updates the `toCount` doc type query method to use Math.ceil rather than Math.floor plus an additional step.
24
+
25
+ ## 3.9.0 - 2021-12-08
26
+
27
+ ### Adds
28
+
29
+ * Developers can now override any Vue component of the ApostropheCMS admin UI by providing a component of the same name in the `ui/apos/components` folder of their own module. This is not always the best approach, see the documentation for details.
30
+ * When running a job, we now trigger the notification before to run the job, this way the progress notification ID is available from the job and the notification can be dismissed if needed.
31
+ * Adds `maxUi`, `maxLabel`, `minUi`, and `minLabel` localization strings for array input and other UI.
32
+
33
+ ### Fixes
34
+
35
+ * Fully removes references to the A2 `self.partial` module method. It appeared only once outside of comments, but was not actually used by the UI. The `self.render` method should be used for simple template rendering.
36
+ * Fixes string interpolation for the confirmation modal when publishing a page that has an unpublished parent page.
37
+ * No more "cannot set headers after they are sent to the client" and "req.res.redirect not defined" messages when handling URLs with extra trailing slashes.
38
+ * The `apos.util.runPlayers` method is not called until all of the widgets in a particular tree of areas and sub-areas have been added to the DOM. This means a parent area widget player will see the expected markup for any sub-widgets when the "Edit" button is clicked.
39
+ * Properly activates the `apostropheI18nDebugPlugin` i18next debugging plugin when using the `APOS_SHOW_I18N` environment variable. The full set of l10n emoji indicators previously available for the UI is now available for template and server-side strings.
40
+ * Actually registers piece types for site search unless the `searchable` option is `false`.
41
+ * Fixes the methods required for the search `index` task.
42
+
43
+ ### Changes
44
+
45
+ * Adds localization keys for the password field component's min and max error messages.
46
+
47
+ ## 3.8.1 - 2021-11-23
48
+
49
+ ### Fixes
50
+
51
+ * The search field of the pieces manager modal works properly. Thanks to [Miro Yovchev](https://github.com/myovchev) for pointing out the issue and providing a solution.
52
+ * Fixes a bug in `AposRichTextWidgetEditor.vue` when a rich text widget was specifically configured with an empty array as the `styles` option. In that case a new empty rich text widget will initiate with an empty paragraph tag.
53
+ * The`fieldsPresent` method that is used with the `presentFieldsOnly` option in doc-type was broken, looking for properties in strings and wasn't returning anything.
54
+
3
55
  ## 3.8.0 - 2021-11-15
4
56
 
5
57
  ### Adds
package/README.md CHANGED
@@ -1,5 +1,4 @@
1
-
2
- [![CircleCI](https://circleci.com/gh/apostrophecms/apostrophe/tree/main.svg?style=svg)](https://circleci.com/gh/apostrophecms/apostrophe/tree/main)
1
+ ![Unit Tests](https://github.com/apostrophecms/apostrophe/actions/workflows/main.yml/badge.svg)
3
2
  [![Chat on Discord](https://img.shields.io/discord/517772094482677790.svg)](https://chat.apostrophecms.org)
4
3
 
5
4
  <p align="center">
package/lib/moog.js CHANGED
@@ -179,17 +179,42 @@ module.exports = function(options) {
179
179
  ...properties.add
180
180
  };
181
181
  }
182
+
183
+ const lastFields = Object.entries(that[cascade])
184
+ .filter(([ field, val ]) => val.last === true)
185
+ .map(([ field, val ]) => field);
186
+
182
187
  if (properties.remove) {
183
188
  for (const field of properties.remove) {
184
189
  delete that[cascade][field];
185
190
  }
186
191
  }
187
192
  if (properties.order) {
193
+ // 1. Ordered fields
194
+ // 2. Other fields not marked as last
195
+ // 3. Fields marked as last
188
196
  that[cascade] = Object.fromEntries([
189
197
  ...properties.order.map(field => [ field, that[cascade][field] ]),
190
- ...Object.keys(that[cascade]).filter(field => !properties.order.includes[field]).map(field => [ field, that[cascade][field] ])
198
+ ...Object.keys(that[cascade])
199
+ .filter(field => {
200
+ return !properties.order.includes(field) &&
201
+ !lastFields.includes(field);
202
+ })
203
+ .map(field => [ field, that[cascade][field] ]),
204
+ ...lastFields.filter(field => !properties.order.includes(field))
205
+ .map(field => [ field, that[cascade][field] ])
206
+ ]);
207
+ } else if (lastFields.length > 0) {
208
+ // 1. Fields not marked as last
209
+ // 2. Fields marked as last
210
+ that[cascade] = Object.fromEntries([
211
+ ...Object.keys(that[cascade])
212
+ .filter(field => !lastFields.includes(field))
213
+ .map(field => [ field, that[cascade][field] ]),
214
+ ...lastFields.map(field => [ field, that[cascade][field] ])
191
215
  ]);
192
216
  }
217
+
193
218
  if (properties.group) {
194
219
  const groups = klona(that[`${cascade}Groups`]);
195
220
  for (const value of Object.values(properties.group)) {
@@ -10,7 +10,13 @@ export default function() {
10
10
  return window.apos;
11
11
  }
12
12
  },
13
- template: '<component :is="`TheAposAdminBar`" :items="apos.adminBar.items" />'
13
+ render: function (h) {
14
+ return h('TheAposAdminBar', {
15
+ props: {
16
+ items: apos.adminBar.items
17
+ }
18
+ });
19
+ }
14
20
  });
15
21
  }
16
22
  };
@@ -287,6 +287,8 @@ module.exports = {
287
287
  // are suitable for display in the reorganize view.
288
288
  // The only pages excluded are those with a `reorganize`
289
289
  // property explicitly set to `false`.
290
+ // NOTE: This query builder is deprecated and will be removed in the
291
+ // next major version.
290
292
  reorganize: {
291
293
  def: null,
292
294
  finalize() {
@@ -3,27 +3,32 @@ import { klona } from 'klona';
3
3
 
4
4
  export default function() {
5
5
 
6
+ let widgetsRendering = 0;
7
+
6
8
  createWidgetClipboardApp();
7
9
 
8
- prepareAreas();
10
+ createAreaApps();
9
11
 
10
12
  document.documentElement.style.setProperty('--a-widget-margin', apos.ui.widgetMargin);
11
13
 
14
+ apos.bus.$on('widget-rendering', function() {
15
+ widgetsRendering++;
16
+ });
17
+
12
18
  apos.bus.$on('widget-rendered', function() {
13
- prepareAreas();
19
+ widgetsRendering--;
20
+ createAreaAppsAndRunPlayersIfDone();
14
21
  });
22
+
15
23
  apos.bus.$on('refreshed', function() {
16
- prepareAreas();
24
+ createAreaAppsAndRunPlayersIfDone();
17
25
  });
18
26
 
19
- function prepareAreas() {
20
- // Doing this first allows markup to be captured for the editor
21
- // before players alter it
27
+ function createAreaAppsAndRunPlayersIfDone() {
22
28
  createAreaApps();
23
- // Even though we invoke the player directly from
24
- // the widget mixin used for editable widgets, we still have to
25
- // call runPlayers eventually to account for any foreign area widgets
26
- apos.util.runPlayers();
29
+ if (widgetsRendering === 0) {
30
+ apos.util.runPlayers();
31
+ }
27
32
  }
28
33
 
29
34
  function createAreaApps() {
@@ -106,7 +111,19 @@ export default function() {
106
111
  renderings
107
112
  };
108
113
  },
109
- template: `<${component} :options="options" :items="items" :choices="choices" :id="$data.id" :docId="$data.docId" :fieldId="fieldId" :renderings="renderings" />`
114
+ render(h) {
115
+ return h(component, {
116
+ props: {
117
+ options: this.options,
118
+ items: this.items,
119
+ choices: this.choices,
120
+ id: this.id,
121
+ docId: this.docId,
122
+ fieldId: this.fieldId,
123
+ renderings: this.renderings
124
+ }
125
+ });
126
+ }
110
127
  });
111
128
  }
112
129
  }
@@ -192,7 +192,10 @@ module.exports = {
192
192
  let iconImports, componentImports, tiptapExtensionImports, appImports, indexJsImports, indexSassImports;
193
193
  if (options.apos) {
194
194
  iconImports = getIcons();
195
- componentImports = getImports(`${source}/components`, '*.vue', { registerComponents: true });
195
+ componentImports = getImports(`${source}/components`, '*.vue', {
196
+ registerComponents: true,
197
+ importLastVersion: true
198
+ });
196
199
  tiptapExtensionImports = getImports(`${source}/tiptap-extensions`, '*.js', { registerTiptapExtensions: true });
197
200
  appImports = getImports(`${source}/apps`, '*.js', {
198
201
  invokeApps: true,
@@ -441,6 +444,25 @@ module.exports = {
441
444
  paths: []
442
445
  };
443
446
 
447
+ if (options.importLastVersion) {
448
+ // Reverse the list so we can easily find the last configured import
449
+ // of a given component, allowing "improve" modules to win over
450
+ // the originals when shipping an override of a Vue component
451
+ // with the same name, and filter out earlier versions
452
+ components.reverse();
453
+ const seen = new Set();
454
+ components = components.filter(component => {
455
+ const name = getComponentName(component, options);
456
+ if (seen.has(name)) {
457
+ return false;
458
+ }
459
+ seen.add(name);
460
+ return true;
461
+ });
462
+ // Put the components back in their original order
463
+ components.reverse();
464
+ }
465
+
444
466
  components.forEach((component, i) => {
445
467
  if (options.requireDefaultExport) {
446
468
  if (!fs.readFileSync(component, 'utf8').match(/export[\s\n]+default/)) {
@@ -453,7 +475,7 @@ module.exports = {
453
475
  }
454
476
  }
455
477
  const jsFilename = JSON.stringify(component);
456
- const name = require('path').basename(component).replace(/\.\w+/, '') + (options.enumerateImports ? `_${i}` : '');
478
+ const name = getComponentName(component, options, i);
457
479
  const jsName = JSON.stringify(name);
458
480
  output.paths.push(component);
459
481
  const importCode = `
@@ -492,6 +514,10 @@ module.exports = {
492
514
 
493
515
  return pkgTimestamp > parseInt(timestamp);
494
516
  }
517
+
518
+ function getComponentName(component, options, i) {
519
+ return require('path').basename(component).replace(/\.\w+/, '') + (options.enumerateImports ? `_${i}` : '');
520
+ }
495
521
  }
496
522
  }
497
523
  };
@@ -726,8 +752,7 @@ module.exports = {
726
752
  if (!self.shouldRefreshOnRestart()) {
727
753
  return '';
728
754
  }
729
- const script = fs.readFileSync(path.join(__dirname, '/lib/refresh-on-restart.js'), 'utf8');
730
- return self.apos.template.safe(`<script data-apos-refresh-on-restart="${self.action}/restart-id">\n${script}</script>`);
755
+ return self.apos.template.safe(`<script data-apos-refresh-on-restart="${self.action}/restart-id" src="${self.action}/refresh-on-restart"></script>`);
731
756
  },
732
757
  url(path) {
733
758
  return `${self.getAssetBaseUrl()}${path}`;
@@ -739,6 +764,12 @@ module.exports = {
739
764
  return;
740
765
  }
741
766
  return {
767
+ get: {
768
+ refreshOnRestart(req) {
769
+ req.res.setHeader('content-type', 'text/javascript');
770
+ return fs.readFileSync(path.join(__dirname, '/lib/refresh-on-restart.js'), 'utf8');
771
+ }
772
+ },
742
773
  // Use a POST route so IE11 doesn't cache it
743
774
  post: {
744
775
  async restartId(req) {
@@ -28,7 +28,7 @@ module.exports = ({
28
28
  optimization: {
29
29
  minimize: process.env.NODE_ENV === 'production'
30
30
  },
31
- devtool: 'eval-source-map',
31
+ devtool: 'source-map',
32
32
  output: {
33
33
  path: outputPath,
34
34
  filename: outputFilename
@@ -42,7 +42,7 @@ module.exports = ({
42
42
  resolve: {
43
43
  extensions: [ '*', '.js', '.vue', '.json' ],
44
44
  alias: {
45
- vue$: 'vue/dist/vue.esm.js',
45
+ vue$: 'vue/dist/vue.runtime.esm.js',
46
46
  // resolve apostrophe modules
47
47
  Modules: path.resolve(modulesDir)
48
48
  },
@@ -34,7 +34,7 @@ module.exports = ({
34
34
  optimization: {
35
35
  minimize: process.env.NODE_ENV === 'production'
36
36
  },
37
- devtool: 'eval-source-map',
37
+ devtool: 'source-map',
38
38
  output: {
39
39
  path: outputPath,
40
40
  filename: outputFilename
@@ -230,7 +230,6 @@ module.exports = {
230
230
  addFieldType() {
231
231
  self.apos.schema.addFieldType({
232
232
  name: self.name,
233
- partial: self.fieldTypePartial,
234
233
  convert: self.convert,
235
234
  index: self.index,
236
235
  register: self.register
@@ -274,9 +273,6 @@ module.exports = {
274
273
  await self.db.replaceOne({ _id: info._id }, info);
275
274
  object[field.name] = info;
276
275
  },
277
- fieldTypePartial(data) {
278
- return self.partial('attachment', data);
279
- },
280
276
  index(value, field, texts) {
281
277
  const silent = field.silent === undefined ? true : field.silent;
282
278
  texts.push({
@@ -3,6 +3,8 @@ import Vue from 'Modules/@apostrophecms/ui/lib/vue';
3
3
  export default function() {
4
4
  return new Vue({
5
5
  el: '#apos-busy',
6
- template: '<component :is="`TheAposBusy`" />'
6
+ render: function (h) {
7
+ return h('TheAposBusy');
8
+ }
7
9
  });
8
10
  };
@@ -545,8 +545,11 @@ module.exports = {
545
545
 
546
546
  await self.apos.schema.convert(req, schema, input, doc);
547
547
 
548
- doc.copyOfId = copyOf && copyOf._id;
549
548
  if (copyOf) {
549
+ if (copyOf._id) {
550
+ doc.copyOfId = copyOf._id;
551
+ }
552
+
550
553
  self.apos.schema.regenerateIds(req, fullSchema, doc);
551
554
  }
552
555
  },
@@ -554,17 +557,9 @@ module.exports = {
554
557
  // taking into account issues like relationship fields keeping their data in
555
558
  // a separate ids property, etc.
556
559
  fieldsPresent(input) {
557
- const schema = self.schema;
558
- const output = [];
559
- for (const field of schema) {
560
- if (field.type.name.substring(0, 5) === '_relationship') {
561
- if (_.has(input, field.idsStorage)) {
562
- output.push(field.name);
563
- }
564
- } else {
565
- output.push(field.name);
566
- }
567
- }
560
+ return self.schema
561
+ .filter((field) => _.has(input, field.name))
562
+ .map((field) => field.name);
568
563
  },
569
564
  // Returns a query that finds docs the current user can edit. Unlike
570
565
  // find(), this query defaults to including docs in the archive. Subclasses
@@ -1157,8 +1152,10 @@ module.exports = {
1157
1152
 
1158
1153
  // `.addLateCriteria({...})` provides an object to be merged directly into the final
1159
1154
  // criteria object that will go to MongoDB. This is to be used only
1160
- // in cases where MongoDB forbids the use of an operator inside
1161
- // `$and`, such as the `$near` operator.
1155
+ // in cases where MongoDB forbids the use of an operator inside `$and`.
1156
+ //
1157
+ // TODO: Since `$near` can now be used in `$and` operators, this query
1158
+ // builder is deprecated and should be removed in the 4.x major version.
1162
1159
  addLateCriteria: {
1163
1160
  set(c) {
1164
1161
  let lateCriteria = query.get('lateCriteria');
@@ -1274,6 +1271,14 @@ module.exports = {
1274
1271
  if (query.get('search')) {
1275
1272
  // MongoDB mandates this if we want to sort on search result quality
1276
1273
  projection.textScore = { $meta: 'textScore' };
1274
+ } else if (projection.textScore) {
1275
+ // Gracefully elide the textScore projection when it is not useful and
1276
+ // would cause an error anyway.
1277
+ //
1278
+ // This allows the reuse of the `project()` value passed to one query
1279
+ // in a second query without worrying about whether the second query
1280
+ // contains a search or not
1281
+ delete projection.textScore;
1277
1282
  }
1278
1283
  query.set('project', projection);
1279
1284
  }
@@ -1417,7 +1422,7 @@ module.exports = {
1417
1422
  }
1418
1423
  },
1419
1424
 
1420
- // `.permission('admin')` would limit the returned docs to those for which the
1425
+ // `.permission('edit')` would limit the returned docs to those for which the
1421
1426
  // user associated with the query's `req` has the named permission.
1422
1427
  // By default, `view` is checked for. You might want to specify
1423
1428
  // `edit`.
@@ -1655,7 +1660,11 @@ module.exports = {
1655
1660
  const _req = query.req.clone({
1656
1661
  mode: 'published'
1657
1662
  });
1658
- const publishedDocs = await self.find(_req)._ids(results.map(result => result._id.replace(':draft', ':published'))).project(query.get('project')).toArray();
1663
+ const publishedDocs = await self.find(_req)
1664
+ ._ids(results.map(result => {
1665
+ return result._id.replace(':draft', ':published');
1666
+ })).project(query.get('project')).toArray();
1667
+
1659
1668
  for (const doc of results) {
1660
1669
  const publishedDoc = publishedDocs.find(publishedDoc => doc.aposDocId === publishedDoc.aposDocId);
1661
1670
  doc._publishedDoc = publishedDoc;
@@ -2145,10 +2154,8 @@ module.exports = {
2145
2154
  const count = await mongo.count();
2146
2155
  if (query.get('perPage')) {
2147
2156
  const perPage = query.get('perPage');
2148
- let totalPages = Math.floor(count / perPage);
2149
- if (count % perPage) {
2150
- totalPages++;
2151
- }
2157
+ const totalPages = Math.ceil(count / perPage);
2158
+
2152
2159
  query.set('totalPages', totalPages);
2153
2160
  }
2154
2161
  return count;
@@ -162,7 +162,6 @@ export default {
162
162
  return `${this.moduleAction}/${this.docId}`;
163
163
  },
164
164
  tooltip() {
165
- // TODO I18N
166
165
  let msg;
167
166
  if (this.errorCount) {
168
167
  msg = {
@@ -124,6 +124,7 @@
124
124
  "errorPageMessage": "An error has occurred",
125
125
  "errorPageStatusCode": "500",
126
126
  "errorPageTitle": "An error has occurred",
127
+ "errorBatchOperationNoti": "Batch operation {{ operation }} failed.",
127
128
  "everythingElse": "Everything Else",
128
129
  "exit": "Exit",
129
130
  "fetchPublishedVersionFailed": "An error occurred fetching the published version of the document.",
@@ -176,6 +177,8 @@
176
177
  "manageDocType": "Manage {{ type }}",
177
178
  "manageDraftSubmissions": "Manage Draft Submissions",
178
179
  "managePages": "Manage Pages",
180
+ "maxLabel": "Max:",
181
+ "maxUi": "Max: {{ number }}",
179
182
  "mediaCreatedDate": "Uploaded: {{ createdDate }}",
180
183
  "mediaDimensions": "Dimensions: {{ width }} 𝗑 {{ height }}",
181
184
  "mediaFileSize": "File Size: {{ fileSize }}",
@@ -183,6 +186,8 @@
183
186
  "mediaMB": "{{ size }}MB",
184
187
  "mediaUploadViaDrop": "Drop ’em when you’re ready",
185
188
  "mediaUploadViaExplorer": "Or click to open the file explorer",
189
+ "minLabel": "Min:",
190
+ "minUi": "Min: {{ number }}",
186
191
  "modify": "Modify",
187
192
  "modifyOrDelete": "Modify / Delete",
188
193
  "moreOptions": "More Options",
@@ -213,6 +218,7 @@
213
218
  "notYetPublished": "This document hasn't been published yet.",
214
219
  "nudgeDown": "Nudge Down",
215
220
  "nudgeUp": "Nudge Up",
221
+ "numberAdded": "{{ count }} Added",
216
222
  "office": "Office",
217
223
  "openGlobal": "Open Global Site Settings",
218
224
  "page": "Page",
@@ -248,6 +254,8 @@
248
254
  "richTextUndo": "Undo",
249
255
  "richTextStyleConfigWarning": "Misconfigured rich text style: label: {{ label }}, {{ tag }}",
250
256
  "password": "Password",
257
+ "passwordErrorMin": "Minimum of {{ min }} characters",
258
+ "passwordErrorMax": "Maximum of {{ max }} characters",
251
259
  "passwordResetRequest": "Your request to reset your password on {{ site }}",
252
260
  "pasteWidget": "Paste {{ widget }}",
253
261
  "pending": "Pending",
@@ -204,7 +204,7 @@
204
204
  "notFoundPageStatusCode": "404",
205
205
  "notFoundPageTitle": "404 - Página no encontrada",
206
206
  "notInLocale": "La página actual no existe en {{ label }}. ¿Traducir la versión desde la configuración regional {{ currentLocale }}?",
207
- "noTypeFound": "Ningún {{ type }} Ecnontrado",
207
+ "noTypeFound": "Ningún {{ type }} Encontrado",
208
208
  "parentNotLocalized": "Primero traduzca la configuración regional de la página principal",
209
209
  "notYetPublished": "Este documento aún no ha sido publicado.",
210
210
  "nudgeDown": "Mover Hacia Arriba",
@@ -13,11 +13,27 @@ const ExpressSessionCookie = require('express-session/session/cookie');
13
13
 
14
14
  const apostropheI18nDebugPlugin = {
15
15
  type: 'postProcessor',
16
- name: 'apostrophei18nDebugPlugin',
16
+ name: 'apostropheI18nDebugPlugin',
17
17
  process(value, key, options, translator) {
18
- // For ease of tracking down which phrases were
19
- // actually passed through i18next
20
- return `🌍 ${value}`;
18
+ // The key is passed as an array (theoretically to include multiple keys).
19
+ // We confirm that and grab the primary one for comparison.
20
+ const l10nKey = Array.isArray(key) ? key[0] : key;
21
+
22
+ if (value === l10nKey) {
23
+ if (l10nKey.match(/^\S+:/)) {
24
+ // The l10n key does not have a value assigned (or the key is
25
+ // actually the same as the phrase). The key seems to have a
26
+ // namespace, so might be from the Apostrophe UI.
27
+ return `❌ ${value}`;
28
+ } else {
29
+ // The l10n key does not have a value assigned (or the key is
30
+ // actually the same as the phrase). It is in the default namespace.
31
+ return `🕳 ${value}`;
32
+ }
33
+ } else {
34
+ // The phrase is fully localized.
35
+ return `🌍 ${value}`;
36
+ }
21
37
  }
22
38
  };
23
39
 
@@ -73,7 +89,12 @@ module.exports = {
73
89
  if (self.show) {
74
90
  self.i18next.use(apostropheI18nDebugPlugin);
75
91
  }
76
- await self.i18next.init();
92
+
93
+ const i18nextOptions = self.show ? {
94
+ postProcess: 'apostropheI18nDebugPlugin'
95
+ } : {};
96
+
97
+ await self.i18next.init(i18nextOptions);
77
98
  self.addInitialResources();
78
99
  self.enableBrowserData();
79
100
  },
@@ -189,38 +189,31 @@ module.exports = {
189
189
  async run(req, doTheWork, options = {}) {
190
190
  const res = req.res;
191
191
  let job;
192
- let notification;
193
192
  let total;
194
193
 
195
194
  try {
196
195
  job = await self.start(options);
197
- run();
198
196
 
199
- // Trigger the "in progress" notification.
200
- notification = await self.triggerNotification(req, 'progress', {
201
- // It's only relevant to pass a job ID to the notification if
202
- // the notification will show progress. Without a total number we
203
- // can't show progress.
204
- jobId: total && job._id,
205
- count: total
197
+ const notification = await self.triggerNotification(req, 'progress', {
198
+ jobId: job._id
206
199
  });
207
200
 
201
+ run({ notificationId: notification.noteId });
202
+
208
203
  return {
209
204
  jobId: job._id
210
205
  };
211
206
  } catch (err) {
212
207
  self.apos.util.error(err);
213
- if (job) {
214
- // Not a lot we can do about this since we already
215
- // stopped talking to the user
216
- self.apos.util.error(err);
217
- } else {
208
+
209
+ if (!job) {
218
210
  // If we never made a job, then we never responded
211
+ // otherwise we already stopped talking to the user
219
212
  return res.status(500).send('error');
220
213
  }
221
214
  }
222
215
 
223
- async function run() {
216
+ async function run(info) {
224
217
  let results;
225
218
  let good = false;
226
219
  try {
@@ -240,7 +233,7 @@ module.exports = {
240
233
  setResults (_results) {
241
234
  results = _results;
242
235
  }
243
- });
236
+ }, info);
244
237
  good = true;
245
238
  } finally {
246
239
  await self.end(job, good, results);
@@ -253,7 +246,7 @@ module.exports = {
253
246
  // Dismiss the progress notification. It will delay 4 seconds
254
247
  // because "completed" notification will dismiss in 5 and we want
255
248
  // to maintain the feeling of process order for users.
256
- await self.apos.notification.dismiss(req, notification.noteId, 4000);
249
+ await self.apos.notification.dismiss(req, info.notificationId, 4000);
257
250
  }
258
251
  }
259
252
  },