apostrophe 3.8.1 → 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 (37) hide show
  1. package/.github/workflows/main.yml +45 -0
  2. package/CHANGELOG.md +22 -0
  3. package/README.md +1 -2
  4. package/modules/@apostrophecms/area/ui/apos/apps/AposAreas.js +15 -10
  5. package/modules/@apostrophecms/asset/index.js +28 -2
  6. package/modules/@apostrophecms/attachment/index.js +0 -4
  7. package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocEditor.vue +0 -1
  8. package/modules/@apostrophecms/i18n/i18n/en.json +8 -0
  9. package/modules/@apostrophecms/i18n/i18n/es.json +1 -1
  10. package/modules/@apostrophecms/i18n/index.js +26 -5
  11. package/modules/@apostrophecms/job/index.js +10 -17
  12. package/modules/@apostrophecms/modal/ui/apos/components/AposDocsManagerToolbar.vue +1 -1
  13. package/modules/@apostrophecms/module/index.js +1 -4
  14. package/modules/@apostrophecms/notification/ui/apos/components/AposNotification.vue +1 -1
  15. package/modules/@apostrophecms/page/index.js +47 -22
  16. package/modules/@apostrophecms/page-type/index.js +5 -1
  17. package/modules/@apostrophecms/piece-type/index.js +5 -0
  18. package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManager.vue +2 -1
  19. package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManagerSelectBox.vue +27 -24
  20. package/modules/@apostrophecms/schema/index.js +0 -18
  21. package/modules/@apostrophecms/schema/ui/apos/components/AposArrayEditor.vue +9 -3
  22. package/modules/@apostrophecms/schema/ui/apos/components/AposInputPassword.vue +11 -4
  23. package/modules/@apostrophecms/schema/ui/apos/components/AposInputRange.vue +2 -2
  24. package/modules/@apostrophecms/schema/ui/apos/components/AposInputWrapper.vue +0 -2
  25. package/modules/@apostrophecms/schema/ui/apos/components/AposLogo.vue +1 -1
  26. package/modules/@apostrophecms/schema/ui/apos/components/AposLogoIcon.vue +1 -1
  27. package/modules/@apostrophecms/schema/ui/apos/components/AposLogoPadless.vue +1 -1
  28. package/modules/@apostrophecms/schema/ui/apos/components/AposSearchList.vue +0 -1
  29. package/modules/@apostrophecms/search/index.js +53 -33
  30. package/modules/@apostrophecms/task/index.js +5 -1
  31. package/modules/@apostrophecms/template/index.js +5 -11
  32. package/modules/@apostrophecms/ui/ui/apos/components/AposMinMaxCount.vue +9 -3
  33. package/modules/@apostrophecms/ui/ui/apos/mixins/AposPublishMixin.js +3 -2
  34. package/modules/@apostrophecms/widget-type/ui/apos/mixins/AposWidgetMixin.js +5 -27
  35. package/package.json +1 -1
  36. package/.circleci/config.yml +0 -94
  37. 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,27 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.9.0 - 2021-12-08
4
+
5
+ ### Adds
6
+
7
+ * 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.
8
+ * 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.
9
+ * Adds `maxUi`, `maxLabel`, `minUi`, and `minLabel` localization strings for array input and other UI.
10
+
11
+ ### Fixes
12
+
13
+ * 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.
14
+ * Fixes string interpolation for the confirmation modal when publishing a page that has an unpublished parent page.
15
+ * 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.
16
+ * 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.
17
+ * 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.
18
+ * Actually registers piece types for site search unless the `searchable` option is `false`.
19
+ * Fixes the methods required for the search `index` task.
20
+
21
+ ### Changes
22
+
23
+ * Adds localization keys for the password field component's min and max error messages.
24
+
3
25
  ## 3.8.1 - 2021-11-23
4
26
 
5
27
  ### Fixes
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">
@@ -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() {
@@ -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
  };
@@ -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({
@@ -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
  },
@@ -5,7 +5,7 @@
5
5
  v-if="displayedItems"
6
6
  label="apostrophe:select"
7
7
  type="outline"
8
- text-color="var(--a-primary)"
8
+ text-color="var(--a-base-1)"
9
9
  :icon-only="true"
10
10
  :icon="checkboxIcon"
11
11
  @click="$emit('select-click')"
@@ -292,10 +292,7 @@ module.exports = {
292
292
  // your version wins if it exists.
293
293
  //
294
294
  // You MUST pass req as the first argument. This allows
295
- // internationalization/localization to work. If you
296
- // are writing a Nunjucks helper function, use
297
- // self.partial instead. This method is primarily used
298
- // to implement routes that respond with HTML fragments.
295
+ // internationalization/localization to work.
299
296
  //
300
297
  // All properties of `data` appear in Nunjucks templates as
301
298
  // properties of the `data` object. Nunjucks helper functions
@@ -173,7 +173,7 @@ export default {
173
173
  this.job.processed = job.processed;
174
174
  this.job.percentage = job.percentage;
175
175
 
176
- if (this.job.processed < this.job.total) {
176
+ if (this.job.processed < this.job.total && !job.ended) {
177
177
  await new Promise(resolve => {
178
178
  setTimeout(resolve, 500);
179
179
  });
@@ -1385,6 +1385,24 @@ database.`);
1385
1385
  // home page.
1386
1386
  async serveGetPage(req) {
1387
1387
  req.slug = req.params[0];
1388
+ self.normalizeSlug(req);
1389
+ // Had to change the URL, so redirect to it. TODO: this
1390
+ // contains an assumption that we are mounted at /
1391
+ if (req.slug !== req.params[0]) {
1392
+ req.redirect = req.slug;
1393
+ }
1394
+ const builders = self.getServePageBuilders();
1395
+ const query = self.find(req);
1396
+ query.applyBuilders(builders);
1397
+ self.matchPageAndPrefixes(query, req.slug);
1398
+ await self.emit('serveQuery', query);
1399
+ req.data.bestPage = await query.toObject();
1400
+ self.evaluatePageMatch(req);
1401
+ },
1402
+ // Normalize req.slug to account for unneeded trailing whitespace,
1403
+ // trailing slashes other than the root, and double slash based open
1404
+ // redirect attempts
1405
+ normalizeSlug(req) {
1388
1406
  // Fix common screwups in URLs: leading/trailing whitespace,
1389
1407
  // presence of trailing slashes (but always restore the
1390
1408
  // leading slash). Express leaves escape codes uninterpreted
@@ -1399,18 +1417,6 @@ database.`);
1399
1417
  if (!req.slug.length || req.slug.charAt(0) !== '/') {
1400
1418
  req.slug = '/' + req.slug;
1401
1419
  }
1402
- // Had to change the URL, so redirect to it. TODO: this
1403
- // contains an assumption that we are mounted at /
1404
- if (req.slug !== req.params[0]) {
1405
- return req.res.redirect(req.slug);
1406
- }
1407
- const builders = self.getServePageBuilders();
1408
- const query = self.find(req);
1409
- query.applyBuilders(builders);
1410
- self.matchPageAndPrefixes(query, req.slug);
1411
- await self.emit('serveQuery', query);
1412
- req.data.bestPage = await query.toObject();
1413
- self.evaluatePageMatch(req);
1414
1420
  },
1415
1421
  // Remove trailing slashes from a slug. This is factored out
1416
1422
  // so that it can be overridden, for instance by the
@@ -1446,8 +1452,27 @@ database.`);
1446
1452
  mode: 'draft',
1447
1453
  locale: req.locale
1448
1454
  });
1449
- await self.serveGetPage(testReq);
1450
- await self.emit('serve', testReq);
1455
+ let again;
1456
+ do {
1457
+ again = false;
1458
+ await self.serveGetPage(testReq);
1459
+ await self.emit('serve', testReq);
1460
+ if (testReq.res.redirectedTo) {
1461
+ again = true;
1462
+ testReq.url = testReq.res.redirectedTo;
1463
+ const qat = testReq.url.indexOf('?');
1464
+ if (qat >= 0) {
1465
+ testReq.slug = testReq.url.substring(0, qat);
1466
+ } else {
1467
+ testReq.slug = testReq.url;
1468
+ }
1469
+ testReq.path = testReq.slug;
1470
+ testReq.params = {
1471
+ 0: testReq.path
1472
+ };
1473
+ testReq.res.redirectedTo = null;
1474
+ }
1475
+ } while (again);
1451
1476
  if (self.isFound(testReq)) {
1452
1477
  req.redirect = self.apos.url.build(req.url, {
1453
1478
  aposMode: 'draft'
@@ -1473,14 +1498,6 @@ database.`);
1473
1498
  },
1474
1499
  async serveDeliver(req, err) {
1475
1500
  let providePage = true;
1476
- // A2 treats req as a notepad of things we'd
1477
- // like to happen in res; that allows various
1478
- // pageServe methods to override each other.
1479
- // Now we're finally ready to enact those
1480
- // things on res
1481
- if (req.contentType) {
1482
- req.res.setHeader('Content-Type', req.contentType);
1483
- }
1484
1501
  if (req.statusCode) {
1485
1502
  req.res.statusCode = req.statusCode;
1486
1503
  }
@@ -1500,6 +1517,14 @@ database.`);
1500
1517
  }
1501
1518
  return req.res.redirect(status, req.redirect);
1502
1519
  }
1520
+ // Apostrophe treats req as a notepad of things we'd
1521
+ // like to happen in res; that allows various
1522
+ // pageServe methods to override each other.
1523
+ // Now we're finally ready to enact those
1524
+ // things on res
1525
+ if (req.contentType) {
1526
+ req.res.setHeader('Content-Type', req.contentType);
1527
+ }
1503
1528
  // Handle 500 errors
1504
1529
  if (err) {
1505
1530
  self.apos.util.error(err);
@@ -152,7 +152,11 @@ module.exports = {
152
152
  type: 1
153
153
  }).toArray();
154
154
  throw self.apos.error('invalid', {
155
- unpublishedAncestors: draftAncestors.filter(draftAncestor => !publishedAncestors.find(publishedAncestor => draftAncestor.aposDocId === publishedAncestor.aposDocId))
155
+ unpublishedAncestors: draftAncestors.filter(draftAncestor => {
156
+ return !publishedAncestors.find(publishedAncestor => {
157
+ return draftAncestor.aposDocId === publishedAncestor.aposDocId;
158
+ });
159
+ })
156
160
  });
157
161
  }
158
162
  }
@@ -514,6 +514,11 @@ module.exports = {
514
514
  ...properties
515
515
  }));
516
516
  }
517
+ },
518
+ '@apostrophecms/search:determineTypes': {
519
+ checkSearchable(types) {
520
+ self.searchDetermineTypes(types);
521
+ }
517
522
  }
518
523
  };
519
524
  },
@@ -481,10 +481,11 @@ export default {
481
481
  }
482
482
  });
483
483
  } catch (error) {
484
- apos.notify('Batch operation {{ operation }} failed.', {
484
+ apos.notify('apostrophe:errorBatchOperationNoti', {
485
485
  interpolate: { operation: label },
486
486
  type: 'danger'
487
487
  });
488
+ console.error(error);
488
489
  }
489
490
  }
490
491
  },
@@ -4,26 +4,26 @@
4
4
  :duration="300"
5
5
  >
6
6
  <div
7
- v-if="selectedState === 'checked' || allPiecesSelection.isSelected"
7
+ v-show="showSelectAll"
8
8
  class="apos-select-box"
9
9
  >
10
10
  <div class="apos-select-box__content">
11
11
  <p class="apos-select-box__text">
12
12
  {{ selectBoxMessage }}
13
- <button
13
+ <AposButton
14
14
  v-if="!allPiecesSelection.isSelected"
15
- class="apos-select-box__select-all"
15
+ type="subtle" :modifiers="['inline', 'small', 'no-motion']"
16
+ :label="selectBoxMessageButton" class="apos-select-box__select-all"
16
17
  @click="$emit('select-all')"
17
- >
18
- {{ selectBoxMessageButton }}
19
- </button>
20
- <button
18
+ text-color="var(--a-primary)"
19
+ />
20
+ <AposButton
21
21
  v-else
22
- class="apos-select-box__select-all"
22
+ type="subtle" :modifiers="['inline', 'small', 'no-motion']"
23
+ label="apostrophe:clearSelection" class="apos-select-box__select-all"
24
+ text-color="var(--a-primary)"
23
25
  @click="clearSelection"
24
- >
25
- {{ $t('apostrophe:clearSelection') }}.
26
- </button>
26
+ />
27
27
  </p>
28
28
  </div>
29
29
  </div>
@@ -56,6 +56,15 @@ export default {
56
56
  },
57
57
  emits: [ 'select-all', 'clear-select', 'set-all-pieces-selection' ],
58
58
  computed: {
59
+ showSelectAll() {
60
+ if (
61
+ this.allPiecesSelection.isSelected ||
62
+ (this.selectedState === 'checked' && this.allPiecesSelection.total > this.displayedItems)
63
+ ) {
64
+ return true;
65
+ }
66
+ return false;
67
+ },
59
68
  selectBoxMessage () {
60
69
  const checkedCount = this.checkedIds.length;
61
70
  const showAllWord = (checkedCount === this.allPiecesSelection.total) &&
@@ -104,35 +113,29 @@ export default {
104
113
  .apos-select-box {
105
114
  box-sizing: border-box;
106
115
  overflow: hidden;
107
- height: 5rem;
108
- transition: all 0.3s linear;
116
+ max-height: 65px;
117
+ transition: max-height 200ms ease-in;
109
118
 
110
119
  &.collapse-enter, &.collapse-leave-to {
111
- height: 0;
120
+ max-height: 0;
112
121
  }
113
122
 
114
123
  &__content {
115
124
  display: flex;
116
125
  align-items: center;
117
126
  justify-content: center;
118
- background-color: var(--a-base-9);
127
+ background-color: var(--a-base-10);
119
128
  margin-top: 1rem;
120
129
  color: var(--a-text-primary);
130
+ border-radius: var(--a-border-radius);
121
131
  }
122
132
 
123
133
  &__text {
124
- @include type-large;
134
+ @include type-base;
125
135
  }
126
136
 
127
137
  &__select-all {
128
- color: var(--a-primary);
129
- cursor: pointer;
130
- margin-left: 0.4rem;
131
- border: none;
132
-
133
- &:hover {
134
- text-decoration: underline;
135
- }
138
+ margin-left: $spacing-half;
136
139
  }
137
140
  }
138
141
  </style>
@@ -1292,24 +1292,6 @@ module.exports = {
1292
1292
  return field.group && field.group.name === defaultGroup.name;
1293
1293
  }));
1294
1294
 
1295
- _.each(schema, function (field) {
1296
-
1297
- // A field can have a custom template, which can be a
1298
- // template name (relative to the @apostrophecms/schema module)
1299
- // or a function (called to render it)
1300
-
1301
- if (field.template) {
1302
- if (typeof field.template === 'string') {
1303
- field.partial = self.partialer(field.template);
1304
- delete field.template;
1305
- } else {
1306
- field.partial = field.template;
1307
- delete field.template;
1308
- }
1309
- }
1310
-
1311
- });
1312
-
1313
1295
  // Shallowly clone the fields. This allows modules
1314
1296
  // like workflow to patch schema fields of various modules
1315
1297
  // without inadvertently impacting other apos instances
@@ -150,7 +150,9 @@ export default {
150
150
  return (this.field.schema || []).filter(field => apos.schema.components.fields[field.type]);
151
151
  },
152
152
  countLabel() {
153
- return `${this.next.length} Added`;
153
+ return this.$t('apostrophe:numberAdded', {
154
+ count: this.next.length
155
+ });
154
156
  },
155
157
  // Here in the array editor we use effectiveMin to factor in the
156
158
  // required property because there is no other good place to do that,
@@ -158,14 +160,18 @@ export default {
158
160
  // representation of "required".
159
161
  minLabel() {
160
162
  if (this.effectiveMin) {
161
- return `Min: ${this.effectiveMin}`;
163
+ return this.$t('apostrophe:minUi', {
164
+ number: this.effectiveMin
165
+ });
162
166
  } else {
163
167
  return false;
164
168
  }
165
169
  },
166
170
  maxLabel() {
167
171
  if ((typeof this.field.max) === 'number') {
168
- return `Max: ${this.field.max}`;
172
+ return this.$t('apostrophe:maxUi', {
173
+ number: this.field.max
174
+ });
169
175
  } else {
170
176
  return false;
171
177
  }
@@ -28,7 +28,7 @@ import AposInputMixin from 'Modules/@apostrophecms/schema/mixins/AposInputMixin'
28
28
  export default {
29
29
  name: 'AposInputPassword',
30
30
  mixins: [ AposInputMixin ],
31
- emits: ['return'],
31
+ emits: [ 'return' ],
32
32
  computed: {
33
33
  tabindex () {
34
34
  return this.field.disableFocus ? '-1' : '0';
@@ -43,12 +43,20 @@ export default {
43
43
  }
44
44
  if (this.field.min) {
45
45
  if (value.length && (value.length < this.field.min)) {
46
- return { message: `Minimum of ${this.field.min} characters` };
46
+ return {
47
+ message: this.$t('apostrophe:passwordErrorMin', {
48
+ min: this.field.min
49
+ })
50
+ };
47
51
  }
48
52
  }
49
53
  if (this.field.max) {
50
54
  if (value.length && (value.length > this.field.max)) {
51
- return { message: `Maximum of ${this.field.max} characters` };
55
+ return {
56
+ message: this.$t('apostrophe:passwordErrorMax', {
57
+ max: this.field.max
58
+ })
59
+ };
52
60
  }
53
61
  }
54
62
  return false;
@@ -56,4 +64,3 @@ export default {
56
64
  }
57
65
  };
58
66
  </script>
59
-
@@ -20,13 +20,13 @@
20
20
  <div class="apos-range__scale">
21
21
  <span>
22
22
  <span class="apos-sr-only">
23
- Min:
23
+ {{ $t('apostrophe:minLabel') }}
24
24
  </span>
25
25
  {{ minLabel }}
26
26
  </span>
27
27
  <span>
28
28
  <span class="apos-sr-only">
29
- Max:
29
+ {{ $t('apostrophe:maxLabel') }}
30
30
  </span>
31
31
  {{ maxLabel }}
32
32
  </span>
@@ -2,7 +2,6 @@
2
2
  <div class="apos-field__wrapper">
3
3
  <component :is="wrapEl" :class="classList">
4
4
  <div class="apos-field__info">
5
- <!-- TODO i18n -->
6
5
  <component
7
6
  v-if="field.label" :class="{'apos-sr-only': field.hideLabel }"
8
7
  class="apos-field__label"
@@ -32,7 +31,6 @@
32
31
  />
33
32
  </span>
34
33
  </component>
35
- <!-- TODO i18n -->
36
34
  <p
37
35
  v-if="(field.help || field.htmlHelp) && !displayOptions.helpTooltip"
38
36
  class="apos-field__help"
@@ -60,4 +60,4 @@
60
60
  fill="url(#triangle)"
61
61
  />
62
62
  </svg>
63
- </template>
63
+ </template>
@@ -57,4 +57,4 @@
57
57
  fill="url(#triangle)"
58
58
  />
59
59
  </svg>
60
- </template>
60
+ </template>
@@ -54,4 +54,4 @@
54
54
  fill="url(#triangle)"
55
55
  />
56
56
  </svg>
57
- </template>
57
+ </template>
@@ -148,6 +148,5 @@ export default {
148
148
  &.apos-search__item--disabled {
149
149
  @include disabled;
150
150
  }
151
-
152
151
  }
153
152
  </style>
@@ -44,6 +44,7 @@
44
44
  // are edge cases not relevant enough to explicitly offer a filter for, but
45
45
  // which should nevertheless be included in results.
46
46
 
47
+ const { stripIndent } = require('common-tags');
47
48
  const _ = require('lodash');
48
49
 
49
50
  module.exports = {
@@ -98,37 +99,7 @@ module.exports = {
98
99
  },
99
100
  '@apostrophecms/doc-type:beforeSave': {
100
101
  indexDoc(req, doc) {
101
-
102
- const texts = self.getSearchTexts(doc);
103
-
104
- _.each(texts, function (text) {
105
- if (text.text === undefined) {
106
- text.text = '';
107
- }
108
- });
109
-
110
- const highTexts = _.filter(texts, function (text) {
111
- return text.weight > 10;
112
- });
113
-
114
- const searchSummary = _.map(_.filter(texts, function (text) {
115
- return !text.silent;
116
- }), function (text) {
117
- return text.text;
118
- }).join(' ');
119
- const highText = self.boilTexts(highTexts);
120
- const lowText = self.boilTexts(texts);
121
- const titleSortified = self.apos.util.sortify(doc.title);
122
- const highWords = _.uniq(highText.split(/ /));
123
-
124
- // merge our doc with its various search texts
125
- _.assign(doc, {
126
- titleSortified: titleSortified,
127
- highSearchText: highText,
128
- highSearchWords: highWords,
129
- lowSearchText: lowText,
130
- searchSummary: searchSummary
131
- });
102
+ self.indexDoc(req, doc);
132
103
  }
133
104
  }
134
105
  };
@@ -261,13 +232,58 @@ module.exports = {
261
232
  self.dispatch('/', self.indexPage);
262
233
  },
263
234
 
235
+ indexDoc(req, doc) {
236
+
237
+ const texts = self.getSearchTexts(doc);
238
+
239
+ _.each(texts, function (text) {
240
+ if (text.text === undefined) {
241
+ text.text = '';
242
+ }
243
+ });
244
+
245
+ const highTexts = _.filter(texts, function (text) {
246
+ return text.weight > 10;
247
+ });
248
+
249
+ const searchSummary = _.map(_.filter(texts, function (text) {
250
+ return !text.silent;
251
+ }), function (text) {
252
+ return text.text;
253
+ }).join(' ');
254
+ const highText = self.boilTexts(highTexts);
255
+ const lowText = self.boilTexts(texts);
256
+ const titleSortified = self.apos.util.sortify(doc.title);
257
+ const highWords = _.uniq(highText.split(/ /));
258
+
259
+ // merge our doc with its various search texts
260
+ _.assign(doc, {
261
+ titleSortified: titleSortified,
262
+ highSearchText: highText,
263
+ highSearchWords: highWords,
264
+ lowSearchText: lowText,
265
+ searchSummary: searchSummary
266
+ });
267
+ },
268
+
264
269
  // Indexes just one document as part of the implementation of the
265
270
  // `@apostrophecms/search:index` task. This isn't the method you want to
266
271
  // override. See `indexDoc` and `getSearchTexts`
267
272
 
268
273
  async indexTaskOne(req, doc) {
269
274
  self.indexDoc(req, doc);
270
- return self.apos.doc.db.updateOne({ _id: doc._id }, doc);
275
+
276
+ return self.apos.doc.db.updateOne({
277
+ _id: doc._id
278
+ }, {
279
+ $set: {
280
+ titleSortified: doc.titleSortified,
281
+ highSearchText: doc.highSearchText,
282
+ highSearchWords: doc.highSearchWords,
283
+ lowSearchText: doc.lowSearchText,
284
+ searchSummary: doc.searchSummary
285
+ }
286
+ });
271
287
  },
272
288
 
273
289
  // Returns texts which are a reasonable basis for
@@ -348,7 +364,11 @@ module.exports = {
348
364
  tasks(self) {
349
365
  return {
350
366
  index: {
351
- usage: 'Rebuild the search index. Normally this happens automatically.\nThis should only be needed if you have changed the\n"searchable" property for various fields or types.',
367
+ usage: stripIndent`
368
+ Rebuild the search index. Normally this happens automatically.
369
+ This should only be needed if you have changed the"searchable" property
370
+ for various fields or types.
371
+ `,
352
372
  task(argv) {
353
373
  const req = self.apos.task.getReq();
354
374
  return self.apos.migration.eachDoc({}, _.partial(self.indexTaskOne, req));
@@ -190,7 +190,11 @@ module.exports = {
190
190
  role: options.role
191
191
  }
192
192
  }),
193
- res: {},
193
+ res: {
194
+ redirect(url) {
195
+ req.res.redirectedTo = url;
196
+ }
197
+ },
194
198
  t(key, options = {}) {
195
199
  return self.apos.i18n.i18next.t(key, {
196
200
  ...options,
@@ -1,5 +1,5 @@
1
1
  // Implements template rendering via Nunjucks. **You should use the
2
- // `self.render` and `self.partial` methods of *your own* module**,
2
+ // `self.render` method of *your own* module**,
3
3
  // which exist courtesy of [@apostrophecms/module](../@apostrophecms/module/index.html)
4
4
  // and invoke methods of this module more conveniently for you.
5
5
  //
@@ -21,7 +21,7 @@
21
21
  // you have a custom version of Nunjucks that is compatible.
22
22
  //
23
23
  // ### `viewsFolderFallback`: specifies a folder to be checked for templates
24
- // if they are not found in the module that called `self.render` or `self.partial`
24
+ // if they are not found in the module that called `self.render`
25
25
  // or those it extends. This is a handy place for project-wide macro files.
26
26
  // Often set to `__dirname + '/views'` in `app.js`.
27
27
 
@@ -203,7 +203,7 @@ module.exports = {
203
203
 
204
204
  async renderForModule(req, name, data, module) {
205
205
  if (typeof req !== 'object') {
206
- throw new Error('The first argument to module.render must be req. If you are trying to implement a Nunjucks helper function, use module.partial.');
206
+ throw new Error('The first argument to module.render must be req.');
207
207
  }
208
208
  return self.renderBody(req, 'file', name, data, module);
209
209
  },
@@ -214,7 +214,7 @@ module.exports = {
214
214
 
215
215
  async renderStringForModule(req, s, data, module) {
216
216
  if (typeof req !== 'object') {
217
- throw new Error('The first argument to module.render must be req. If you are trying to implement a Nunjucks helper function, use module.partial.');
217
+ throw new Error('The first argument to module.render must be req.');
218
218
  }
219
219
  return self.renderBody(req, 'string', s, data, module);
220
220
  },
@@ -289,12 +289,6 @@ module.exports = {
289
289
 
290
290
  args.data = merged;
291
291
 
292
- // // Allows templates to render other templates in an independent
293
- // // nunjucks environment, rather than including them
294
- // args.partial = function(name, data) {
295
- // return self.partialForModule(name, data, module);
296
- // };
297
-
298
292
  if (req.data) {
299
293
  _.defaults(merged, req.data);
300
294
  }
@@ -334,7 +328,7 @@ module.exports = {
334
328
 
335
329
  // Fetch a nunjucks environment in which `include`, `extends`, etc. search
336
330
  // the views directories of the specified module and its ancestors.
337
- // Typically you will call `self.render` or `self.partial` on your module
331
+ // Typically you will call `self.render` on your module
338
332
  // object rather than calling this directly.
339
333
  //
340
334
  // `req` is effectively here for bc purposes only. This method
@@ -61,7 +61,9 @@ export default {
61
61
  return maxError;
62
62
  },
63
63
  countLabel() {
64
- return `${this.value.length} Added`;
64
+ return this.$t('apostrophe:numberAdded', {
65
+ count: this.value.length
66
+ });
65
67
  },
66
68
  // Here in the array editor we use effectiveMin to factor in the
67
69
  // required property because there is no other good place to do that,
@@ -69,14 +71,18 @@ export default {
69
71
  // representation of "required".
70
72
  minLabel() {
71
73
  if (this.effectiveMin) {
72
- return `Min: ${this.effectiveMin}`;
74
+ return this.$t('apostrophe:minUi', {
75
+ number: this.effectiveMin
76
+ });
73
77
  } else {
74
78
  return false;
75
79
  }
76
80
  },
77
81
  maxLabel() {
78
82
  if ((typeof this.field.max) === 'number') {
79
- return `Max: ${this.field.max}`;
83
+ return this.$t('apostrophe:maxUi', {
84
+ number: this.field.max
85
+ });
80
86
  } else {
81
87
  return false;
82
88
  }
@@ -49,9 +49,10 @@ export default {
49
49
  if ((e.name === 'invalid') && e.body && e.body.data && e.body.data.unpublishedAncestors) {
50
50
  if (await apos.confirm({
51
51
  heading: 'apostrophe:unpublishedParent',
52
- description: 'apostrophe:unpublishedParentDescription',
52
+ description: 'apostrophe:unpublishedParentDescription'
53
+ }, {
53
54
  interpolate: {
54
- unpublishedParents: this.$t(e.body.data.unpublishedAncestors.map(page => page.title).join(this.$t('apostrophe:listJoiner')))
55
+ unpublishedParents: e.body.data.unpublishedAncestors.map(page => page.title).join(this.$t('apostrophe:listJoiner'))
55
56
  }
56
57
  })) {
57
58
  try {
@@ -22,17 +22,11 @@ export default {
22
22
  },
23
23
  data() {
24
24
  return {
25
- rendered: '...',
26
- playerOpts: null,
27
- playerEl: null
25
+ rendered: '...'
28
26
  };
29
27
  },
30
28
  mounted() {
31
29
  this.renderContent();
32
- this.playerOpts = apos.util.widgetPlayers[this.type] || null;
33
- },
34
- updated () {
35
- this.runPlayer();
36
30
  },
37
31
  computed: {
38
32
  moduleOptions() {
@@ -41,7 +35,7 @@ export default {
41
35
  },
42
36
  methods: {
43
37
  async renderContent() {
44
- const self = this;
38
+ apos.bus.$emit('widget-rendering');
45
39
  const parameters = {
46
40
  _docId: this.docId,
47
41
  widget: this.value,
@@ -51,7 +45,6 @@ export default {
51
45
  try {
52
46
  if (this.rendering && (isEqual(this.rendering.parameters, parameters))) {
53
47
  this.rendered = this.rendering.html;
54
- this.runPlayer();
55
48
  } else {
56
49
  this.rendered = '...';
57
50
  this.rendered = await apos.http.post(`${apos.area.action}/render-widget?aposEdit=1&aposMode=draft`, {
@@ -59,11 +52,10 @@ export default {
59
52
  body: parameters
60
53
  });
61
54
  }
62
- // Wait for reactivity to populate v-html so the
63
- // AposAreas manager can spot any new area divs.
64
- // This will also run the player
55
+ // Wait for reactivity to render v-html so that markup is
56
+ // in the DOM before hinting that it might be time to prepare
57
+ // sub-area editors and run players
65
58
  setTimeout(function() {
66
- self.setPlayerEl();
67
59
  apos.bus.$emit('widget-rendered');
68
60
  }, 0);
69
61
  } catch (e) {
@@ -71,20 +63,6 @@ export default {
71
63
  console.error('Unable to render widget. Possibly the schema has been changed and the existing widget does not pass validation.', e);
72
64
  }
73
65
  },
74
- setPlayerEl() {
75
- if (this.playerOpts) {
76
- const el = this.$el.querySelector(this.playerOpts.selector);
77
- if (el && this.playerOpts.player) {
78
- this.playerEl = el;
79
- }
80
- }
81
- },
82
- runPlayer() {
83
- if (this.playerEl && !this.playerEl.aposWidgetPlayed) {
84
- this.playerOpts.player(this.playerEl);
85
- this.playerEl.aposWidgetPlayed = true;
86
- }
87
- },
88
66
  clicked(e) {
89
67
  // If you do not want a particular click to swap to the edit view
90
68
  // for your widget, you should make sure it does not bubble
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apostrophe",
3
- "version": "3.8.1",
3
+ "version": "3.9.0",
4
4
  "description": "The Apostrophe Content Management System.",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -1,94 +0,0 @@
1
- version: 2
2
- jobs:
3
- build-node14-mongo5:
4
- docker:
5
- - image: circleci/node:14-browsers
6
- - image: mongo:5.0
7
- steps:
8
- - checkout
9
- - run:
10
- name: update-npm
11
- command: 'sudo npm install -g npm@7'
12
- - restore_cache:
13
- key: dependency-cache-{{ checksum "package.json" }}
14
- - run:
15
- name: install-npm-wee
16
- command: npm install
17
- - save_cache:
18
- key: dependency-cache-{{ checksum "package.json" }}
19
- paths:
20
- - ./node_modules
21
- - run:
22
- name: test
23
- command: npm test
24
- build-node14-mongo44:
25
- docker:
26
- - image: circleci/node:14-browsers
27
- - image: mongo:4.4
28
- steps:
29
- - checkout
30
- - run:
31
- name: update-npm
32
- command: 'sudo npm install -g npm@7'
33
- - restore_cache:
34
- key: dependency-cache-{{ checksum "package.json" }}
35
- - run:
36
- name: install-npm-wee
37
- command: npm install
38
- - save_cache:
39
- key: dependency-cache-{{ checksum "package.json" }}
40
- paths:
41
- - ./node_modules
42
- - run:
43
- name: test
44
- command: npm test
45
- build-node14-mongo42:
46
- docker:
47
- - image: circleci/node:14-browsers
48
- - image: mongo:4.2
49
- steps:
50
- - checkout
51
- - run:
52
- name: update-npm
53
- command: 'sudo npm install -g npm@7'
54
- - restore_cache:
55
- key: dependency-cache-{{ checksum "package.json" }}
56
- - run:
57
- name: install-npm-wee
58
- command: npm install
59
- - save_cache:
60
- key: dependency-cache-{{ checksum "package.json" }}
61
- paths:
62
- - ./node_modules
63
- - run:
64
- name: test
65
- command: npm test
66
- build-node12:
67
- docker:
68
- - image: circleci/node:12-browsers
69
- - image: mongo:3.6.11
70
- steps:
71
- - checkout
72
- - run:
73
- name: update-npm
74
- command: "sudo npm install -g npm"
75
- - restore_cache:
76
- key: dependency-cache-{{ .Branch }}-{{ checksum "package-lock.json" }}
77
- - run:
78
- name: install-npm-wee
79
- command: npm install
80
- - save_cache:
81
- key: dependency-cache-{{ .Branch }}-{{ checksum "package-lock.json" }}
82
- paths:
83
- - ./node_modules
84
- - run:
85
- name: test
86
- command: npm test
87
- workflows:
88
- version: 2
89
- build:
90
- jobs:
91
- - build-node14-mongo5
92
- - build-node14-mongo44
93
- - build-node14-mongo42
94
- - build-node12
package/.scratch.md DELETED
@@ -1,2 +0,0 @@
1
- - `/deep/ .apos-button` to `:deep(.apos-button)`
2
- - Remove v-popover, v-tooltip, VueClickOutsideElement