apostrophe 3.4.0 → 3.7.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 (87) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/README.md +1 -1
  3. package/deploy-test-count +1 -1
  4. package/index.js +125 -5
  5. package/lib/moog-require.js +41 -3
  6. package/lib/moog.js +20 -8
  7. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposAdminBarLocale.vue +42 -23
  8. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextBar.vue +25 -13
  9. package/modules/@apostrophecms/area/index.js +9 -0
  10. package/modules/@apostrophecms/area/lib/custom-tags/area.js +1 -1
  11. package/modules/@apostrophecms/area/lib/custom-tags/widget.js +1 -1
  12. package/modules/@apostrophecms/area/ui/apos/apps/AposAreas.js +3 -0
  13. package/modules/@apostrophecms/area/ui/apos/components/AposAreaWidget.vue +6 -6
  14. package/modules/@apostrophecms/asset/index.js +8 -8
  15. package/modules/@apostrophecms/asset/lib/globalIcons.js +2 -0
  16. package/modules/@apostrophecms/asset/lib/webpack/src/webpack.scss.js +5 -2
  17. package/modules/@apostrophecms/doc/index.js +13 -3
  18. package/modules/@apostrophecms/doc-type/index.js +1 -1
  19. package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocEditor.vue +3 -0
  20. package/modules/@apostrophecms/i18n/i18n/en.json +11 -2
  21. package/modules/@apostrophecms/i18n/i18n/es.json +383 -0
  22. package/modules/@apostrophecms/i18n/i18n/pt-BR.json +380 -0
  23. package/modules/@apostrophecms/i18n/i18n/sk.json +381 -0
  24. package/modules/@apostrophecms/i18n/index.js +10 -1
  25. package/modules/@apostrophecms/i18n/ui/apos/components/AposI18nLocalize.vue +153 -121
  26. package/modules/@apostrophecms/image/index.js +2 -1
  27. package/modules/@apostrophecms/image/ui/apos/components/AposMediaManagerEditor.vue +24 -13
  28. package/modules/@apostrophecms/login/index.js +36 -17
  29. package/modules/@apostrophecms/login/ui/apos/components/TheAposLogin.vue +8 -0
  30. package/modules/@apostrophecms/migration/index.js +1 -1
  31. package/modules/@apostrophecms/modal/ui/apos/components/AposModal.vue +6 -2
  32. package/modules/@apostrophecms/modal/ui/apos/components/AposModalConfirm.vue +1 -1
  33. package/modules/@apostrophecms/modal/ui/apos/mixins/AposEditorMixin.js +6 -0
  34. package/modules/@apostrophecms/module/index.js +1 -1
  35. package/modules/@apostrophecms/permission/index.js +1 -1
  36. package/modules/@apostrophecms/permission/ui/apos/components/AposInputRole.vue +4 -2
  37. package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManager.vue +4 -1
  38. package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManagerDisplay.vue +1 -1
  39. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue +42 -10
  40. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapStyles.vue +3 -0
  41. package/modules/@apostrophecms/rich-text-widget/ui/apos/tiptap-extensions/Classes.js +6 -10
  42. package/modules/@apostrophecms/rich-text-widget/ui/apos/tiptap-extensions/Default.js +64 -0
  43. package/modules/@apostrophecms/rich-text-widget/ui/apos/tiptap-extensions/Document.js +15 -0
  44. package/modules/@apostrophecms/rich-text-widget/ui/apos/tiptap-extensions/Heading.js +23 -0
  45. package/modules/@apostrophecms/schema/index.js +97 -20
  46. package/modules/@apostrophecms/schema/ui/apos/components/AposArrayEditor.vue +1 -0
  47. package/modules/@apostrophecms/schema/ui/apos/components/AposInputArray.vue +4 -1
  48. package/modules/@apostrophecms/schema/ui/apos/components/AposInputRadio.vue +8 -5
  49. package/modules/@apostrophecms/schema/ui/apos/components/AposInputRelationship.vue +24 -2
  50. package/modules/@apostrophecms/schema/ui/apos/components/AposInputSelect.vue +24 -6
  51. package/modules/@apostrophecms/schema/ui/apos/components/AposInputSlug.vue +0 -4
  52. package/modules/@apostrophecms/schema/ui/apos/components/AposInputString.vue +0 -7
  53. package/modules/@apostrophecms/schema/ui/apos/components/AposSchema.vue +25 -3
  54. package/modules/@apostrophecms/schema/ui/apos/mixins/AposInputMixin.js +10 -2
  55. package/modules/@apostrophecms/template/index.js +61 -36
  56. package/modules/@apostrophecms/template/lib/custom-tags/component.js +1 -1
  57. package/modules/@apostrophecms/template/lib/custom-tags/render.js +6 -2
  58. package/modules/@apostrophecms/ui/index.js +6 -2
  59. package/modules/@apostrophecms/ui/ui/apos/components/AposButton.vue +16 -3
  60. package/modules/@apostrophecms/ui/ui/apos/components/AposCellContextMenu.vue +1 -1
  61. package/modules/@apostrophecms/ui/ui/apos/components/AposIndicator.vue +5 -0
  62. package/modules/@apostrophecms/ui/ui/apos/lib/i18next.js +16 -2
  63. package/modules/@apostrophecms/ui/ui/apos/scss/global/_tables.scss +4 -3
  64. package/modules/@apostrophecms/ui/ui/apos/scss/global/_theme.scss +3 -0
  65. package/modules/@apostrophecms/ui/ui/apos/scss/global/_widgets.scss +3 -0
  66. package/modules/@apostrophecms/ui/ui/apos/scss/global/import-all.scss +2 -1
  67. package/modules/@apostrophecms/user/index.js +21 -0
  68. package/modules/@apostrophecms/util/index.js +2 -2
  69. package/modules/@apostrophecms/util/ui/src/http.js +12 -8
  70. package/modules/@apostrophecms/widget-type/index.js +1 -1
  71. package/modules/@apostrophecms/widget-type/ui/apos/components/AposWidgetEditor.vue +1 -0
  72. package/package.json +3 -3
  73. package/test/extra_node_modules/improve-global/index.js +7 -0
  74. package/test/extra_node_modules/improve-piece-type/index.js +7 -0
  75. package/test/improve-overrides.js +30 -0
  76. package/test/login.js +183 -0
  77. package/test/modules/@apostrophecms/global/index.js +8 -0
  78. package/test/modules/fragment-all/views/aux-test.html +7 -0
  79. package/test/modules/fragment-all/views/fragment.html +5 -0
  80. package/test/moog.js +47 -0
  81. package/test/package.json +5 -4
  82. package/test/reverse-relationship.js +170 -0
  83. package/test/subdir-project/app.js +3 -0
  84. package/test/subdir-project.js +26 -0
  85. package/test/templates.js +7 -1
  86. package/test-lib/test.js +23 -12
  87. package/test-lib/util.js +1 -0
package/CHANGELOG.md CHANGED
@@ -1,9 +1,92 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.7.0 - 2021-10-28
4
+
5
+ ### Adds
6
+
7
+ * Schema select field choices can now be populated by a server side function, like an API call. Set the `choices` property to a method name of the calling module. That function should take a single argument of `req`, and return an array of objects with `label` and `value` properties. The function can be async and will be awaited.
8
+
9
+ * Apostrophe now has built-in support for the Node.js cluster module. If the `APOS_CLUSTER_PROCESSES` environment variable is set to a number, that number of child processes are forked, sharing the same listening port. If the variable is set to `0`, one process is forked for each CPU core, with a minimum of `2` to provide availability during restarts. If the variable is set to a negative number, that number is added to the number of CPU cores, e.g. `-1` is a good way to reserve one core for MongoDB if it is running on the same server. This is for production use only (`NODE_ENV=production`). If a child process fails it is restarted automatically.
10
+
11
+ ### Fixes
12
+
13
+ * Prevents double-escaping interpolated localization strings in the UI.
14
+ * Rich text editor style labels are now run through a localization method to get the translated strings from their l10n keys.
15
+ * Fixes README Node version requirement (Node 12+).
16
+ * The text alignment buttons now work immediately in a new rich text widget. Previously they worked only after manually setting a style or refreshing the page. Thanks to Michelin for their support of this fix.
17
+ * Users can now activate the built-in date and time editing popups of modern browsers when using the `date` and `time` schema field types.
18
+ * Developers can now `require` their project `app.js` file in the Node.js REPL for debugging and inspection. Thanks to [Matthew Francis Brunetti](https://github.com/zenflow).
19
+ * If a static text phrase is unavailable in both the current locale and the default locale, Apostrophe will always fall back to the `en` locale as a last resort, which ensures the admin UI works if it has not been translated.
20
+ * Developers can now `require` their project `app.js` in the Node.js REPL for debugging and inspection
21
+ * Ensure array field items have valid _id prop before storing. Thanks to Thanks to [Matthew Francis Brunetti](https://github.com/zenflow).
22
+
23
+ ### Changes
24
+
25
+ * In 3.x, `relationship` fields have an optional `builders` property, which replaces `filters` from 2.x, and within that an optional `project` property, which replaces `projection` from 2.x (to match MongoDB's `cursor.project`). Prior to this release leaving the old syntax in place could lead to severe performance problems due to a lack of projections. Starting with this release the 2.x syntax results in an error at startup to help the developer correct their code.
26
+ * The `className` option from the widget options in a rich text area field is now also applied to the rich text editor itself, for a consistently WYSIWYG appearance when editing and when viewing. Thanks to [Max Mulatz](https://github.com/klappradla) for this contribution.
27
+ * Adds deprecation notes to doc module `afterLoad` events, which are deprecated.
28
+ * Removes unused `afterLogin` method in the login module.
29
+
30
+ ## 3.6.0 - 2021-10-13
31
+
32
+ ### Adds
33
+
34
+ * The `context-editing` apostrophe admin UI bus event can now take a boolean parameter, explicitly indicating whether the user is actively typing or performing a similar active manipulation of controls right now. If a boolean parameter is not passed, the existing 1100-millisecond debounced timeout is used.
35
+ * Adds 'no-search' modifier to relationship fields as a UI simplification option.
36
+ * Fields can now have their own `modifiers` array. This is combined with the schema modifiers, allowing for finer grained control of field rendering.
37
+ * Adds a Slovak localization file. Activate the `sk` locale to use this. Many thanks to [Michael Huna](https://github.com/Miselrkba) for the contribution.
38
+ * Adds a Spanish localization file. Activate the `es` locale to use this. Many thanks to [Eugenio Gonzalez](https://github.com/egonzalezg9) for the contribution.
39
+ * Adds a Brazilian Portuguese localization file. Activate the `pt-BR` locale to use this. Many thanks to [Pietro Rutzen](https://github.com/pietro-rutzen) for the contribution.
40
+
41
+ ### Fixes
42
+
43
+ * Fixed missing translation for "New Piece" option on the "more" menu of the piece manager, seen when using it as a chooser.
44
+ * Piece types with relationships to multiple other piece types may now be configured in any order, relative to the other piece types. This sometimes appeared to be a bug in reverse relationships.
45
+ * Code at the project level now overrides code found in modules that use `improve` for the same module name. For example, options set by the `@apostrophecms/seo-global` improvement that ships with `@apostrophecms/seo` can now be overridden at project level by `/modules/@apostrophecms/global/index.js` in the way one would expect.
46
+ * Array input component edit button label is now propertly localized.
47
+ * A memory leak on each request has been fixed, and performance improved, by avoiding the use of new Nunjucks environments for each request. Thanks to Miro Yovchev for pointing out the leak.
48
+ * Fragments now have access to `__t()`, `getOptions` and other features passed to regular templates.
49
+ * Fixes field group cascade merging, using the original group label if none is given in the new field group configuration.
50
+ * If a field is conditional (using an `if` option), is required, but the condition has not been met, it no longer throws a validation error.
51
+ * Passing `busy: true` to `apos.http.post` and related methods no longer produces an error if invoked when logged out, however note that there will likely never be a UI for this when logged out, so indicate busy state in your own way.
52
+ * Bugs in document modification detection have been fixed. These bugs caused edge cases where modifications were not detected and the "Update" button did not appear, and could cause false positives as well.
53
+
54
+ ### Changes
55
+
56
+ * No longer logs a warning about no users if `testModule` is true on the app.
57
+
58
+ ## 3.5.0 - 2021-09-23
59
+
60
+ * Pinned dependency on `vue-material-design-icons` to fix `apos-build.js` build error in production.
61
+ * The file size of uploaded media is visible again when selected in the editor, and media information such as upload date, dimensions and file size is now properly localized.
62
+ * Fixes moog error messages to reflect the recommended pattern of customization functions only taking `self` as an argument.
63
+ * Rich Text widgets now instantiate with a valid element from the `styles` option rather than always starting with an unclassed `<p>` tag.
64
+ * Since version 3.2.0, apostrophe modules to be loaded via npm must appear as explicit npm dependencies of the project. This is a necessary security and stability improvement, but it was slightly too strict. Starting with this release, if the project has no `package.json` in its root directory, the `package.json` in the closest ancestor directory is consulted.
65
+ * Fixes a bug where having no project modules directory would throw an error. This is primarily a concern for module unit tests where there are no additional modules involved.
66
+ * `css-loader` now ignores `url()` in css files inside `assets` so that paths are left intact, i.e. `url(/images/file.svg)` will now find a static file at `/public/images/file.svg` (static assets in `/public` are served by `express.static`). Thanks to Matic Tersek.
67
+ * Restored support for clicking on a "foreign" area, i.e. an area displayed on the page whose content comes from a piece, in order to edit it in an appropriate way.
68
+ * Apostrophe module aliases and the data attached to them are now visible immediately to `ui/src/index.js` JavaScript code, i.e. you can write `apos.alias` where `alias` matches the `alias` option configured for that module. Previously one had to write `apos.modules['module-name']` or wait until next tick. However, note that most modules do not push any data to the browser when a user is not logged in. You can do so in a custom module by calling `self.enableBrowserData('public')` from `init` and implementing or extending the `getBrowserData(req)` method (note that page, piece and widget types already have one, so it is important to extend in those cases).
69
+ * `options.testModule` works properly when implementing unit tests for an npm module that is namespaced.
70
+
71
+ ### Changes
72
+
73
+ * Cascade grouping (e.g., grouping fields) will now concatenate a group's field name array with the field name array of an existing group of the same name. Put simply, if a new piece module adds their custom fields to a `basics` group, that field will be added to the default `basics` group fields. Previously the new group would have replaced the old, leaving inherited fields in the "Ungrouped" section.
74
+ * AposButton's `block` modifier now less login-specific
75
+
76
+ ### Adds
77
+
78
+ * Rich Text widget's styles support a `def` property for specifying the default style the editor should instantiate with.
79
+ * A more helpful error message if a field of type `area` is missing its `options` property.
80
+
81
+ ## 3.4.1 - 2021-09-13
82
+
83
+ No changes. Publishing to correctly mark the latest 3.x release as "latest" in npm.
84
+
3
85
  ## 3.4.0 - 2021-09-13
4
86
 
5
87
  ### Security
6
88
 
89
+ * Changing a user's password or marking their account as disabled now immediately terminates any active sessions or bearer tokens for that user. Thanks to Daniel Elkabes for pointing out the issue. To ensure all sessions have the necessary data for this, all users logged in via sessions at the time of this upgrade will need to log in again.
7
90
  * Users with permission to upload SVG files were previously able to do so even if they contained XSS attacks. In Apostrophe 3.x, the general public so far never has access to upload SVG files, so the risk is minor but could be used to phish access from an admin user by encouraging them to upload a specially crafted SVG file. While Apostrophe typically displays SVG files using the `img` tag, which ignores XSS vectors, an XSS attack might still be possible if the image were opened directly via the Apostrophe media library's convenience link for doing so. All SVG uploads are now sanitized via DOMPurify to remove XSS attack vectors. In addition, all existing SVG attachments not already validated are passed through DOMPurify during a one-time migration.
8
91
 
9
92
  ### Fixes
@@ -30,12 +113,14 @@
30
113
  * Lints module names for `apostrophe-` prefixes even if they don't have a module directory (e.g., only in `app.js`).
31
114
  * Starts all `warnDev` messages with a line break and warning symbol (⚠️) to stand out in the console.
32
115
  * `apos.util.onReady` aliases `apos.util.onReadyAndRefresh` for brevity. The `apos.util.onReadyAndRefresh` method name will be deprecated in the next major version.
116
+ * Adds a developer setting that applies a margin between parent and child areas, allowing developers to change the default spacing in nested areas.
33
117
 
34
118
  ### Changes
35
119
 
36
120
  * Removes the temporary `trace` method from the `@apostrophecms/db` module.
37
121
  * Beginning with this release, the `apostrophe:modulesReady` event has been renamed `apostrophe:modulesRegistered`, and the `apostrophe:afterInit` event has been renamed `apostrophe:ready`. This better reflects their actual roles. The old event names are accepted for backwards compatibility. See the documentation for more information.
38
122
  * Only autofocuses rich text editors when they are empty.
123
+ * Nested areas now have a vertical margin applied when editing, allowing easier access to the parent area's controls.
39
124
 
40
125
  ## 3.3.1 - 2021-09-01
41
126
 
package/README.md CHANGED
@@ -43,7 +43,7 @@ We recommend installing the following with [Homebrew](https://brew.sh/) on macOS
43
43
 
44
44
  | Software | Minimum Version | Notes
45
45
  | ------------- | ------------- | -----
46
- | Node.js | 10.x | Or better
46
+ | Node.js | 12.x | Or better
47
47
  | npm | 6.x | Or better
48
48
  | MongoDB | 3.6 | Or better
49
49
  | Imagemagick | Any | Faster image uploads, GIF support (optional)
package/deploy-test-count CHANGED
@@ -1 +1 @@
1
- 2
1
+ 6
package/index.js CHANGED
@@ -2,11 +2,43 @@ const path = require('path');
2
2
  const _ = require('lodash');
3
3
  const argv = require('boring')({ end: true });
4
4
  const fs = require('fs');
5
+ const { stripIndent } = require('common-tags');
6
+ const cluster = require('cluster');
7
+ const { cpus } = require('os');
8
+ const process = require('process');
5
9
  const npmResolve = require('resolve');
10
+
6
11
  let defaults = require('./defaults.js');
7
- const { stripIndent } = require('common-tags');
8
12
 
9
- // **Awaiting the Apostrophe function is optional**
13
+ // ## Top-level options
14
+ //
15
+ // `cluster`
16
+ //
17
+ // If set to `true`, Apostrophe will spawn as many processes as
18
+ // there are CPU cores on the server, or a minimum of 2, and balance
19
+ // incoming connections among them. This ensures availability while one
20
+ // process is restarting due to a crash and also increases scalability if
21
+ // the server has multiple CPU cores.
22
+ //
23
+ // If set to an object with a `processes` property, that many
24
+ // processes are started. If `processes` is 0 or a negative number,
25
+ // it is added to the number of CPU cores reported by the server.
26
+ // Notably, `-1` can be a good way to reserve one CPU core for MongoDB
27
+ // in a single-server deployment.
28
+ //
29
+ // However when in cluster mode no fewer than 2 processes will be
30
+ // started as there is no availability benefit without at least 2.
31
+ //
32
+ // If a child process exits with a failure status code it will be
33
+ // restarted. However, if it exits in less than 20 seconds after
34
+ // startup there will be a 20 second delay to avoid flooding logs
35
+ // and pinning the CPU.
36
+ //
37
+ // Alternatively the `APOS_CLUSTER_PROCESSES` environment variable
38
+ // can be set to a number, which will effectively set the cluster
39
+ // option to `cluster: { processes: n }`.
40
+ //
41
+ // ## Awaiting the Apostrophe function
10
42
  //
11
43
  // The apos function is async, but in typical cases you do not
12
44
  // need to await it. If you simply call it, Apostrophe will
@@ -21,8 +53,63 @@ const { stripIndent } = require('common-tags');
21
53
  // To avoid exiting on errors, pass the `exit: false` option.
22
54
  // This can option also can be used to allow awaiting a command line
23
55
  // task, as they also normally exit on completion.
56
+ //
57
+ // If `options.cluster` is truthy, the function quickly resolves to
58
+ // `null` in the primary process. In the child process it resolves as
59
+ // documented above.
24
60
 
25
61
  module.exports = async function(options) {
62
+ const guardTime = 20000;
63
+ if (process.env.APOS_CLUSTER_PROCESSES) {
64
+ options.cluster = {
65
+ processes: parseInt(process.env.APOS_CLUSTER_PROCESSES)
66
+ };
67
+ }
68
+ if (options.cluster && (process.env.NODE_ENV !== 'production')) {
69
+ console.log('NODE_ENV is not set to production, disabling cluster mode');
70
+ options.cluster = false;
71
+ }
72
+ if (options.cluster && !argv._.length) {
73
+ // For bc with node 14 and below we need to check both
74
+ if (cluster.isPrimary || cluster.isMaster) {
75
+ let processes = options.cluster.processes || cpus().length;
76
+ if (processes <= 0) {
77
+ processes = cpus().length + processes;
78
+ }
79
+ let capped = '';
80
+ if (processes > cpus().length) {
81
+ processes = cpus().length;
82
+ capped = ' (capped to number of CPU cores)';
83
+ }
84
+ if (processes < 2) {
85
+ processes = 2;
86
+ if (capped) {
87
+ capped = ' (less than 2 cores, capped to minimum of 2)';
88
+ } else {
89
+ capped = ' (using minimum of 2)';
90
+ }
91
+ }
92
+ console.log(`Starting ${processes} cluster child processes${capped}`);
93
+ for (let i = 0; i < processes; i++) {
94
+ clusterFork();
95
+ }
96
+ cluster.on('exit', (worker, code, signal) => {
97
+ if (code !== 0) {
98
+ if ((Date.now() - worker.bornAt) < guardTime) {
99
+ console.error(`Worker process ${worker.process.pid} failed in ${seconds(Date.now() - worker.bornAt)}, waiting ${seconds(guardTime)} before restart`);
100
+ setTimeout(() => {
101
+ respawn(worker);
102
+ }, guardTime);
103
+ } else {
104
+ respawn(worker);
105
+ }
106
+ }
107
+ });
108
+ return null;
109
+ } else {
110
+ console.log(`Cluster worker ${process.pid} started`);
111
+ }
112
+ }
26
113
 
27
114
  // The core is not a true moog object but it must look enough like one
28
115
  // to participate as an async event emitter
@@ -174,7 +261,7 @@ module.exports = async function(options) {
174
261
  function getRoot() {
175
262
  let _module = module;
176
263
  let m = _module;
177
- while (m.parent) {
264
+ while (m.parent && m.parent.filename) {
178
265
  // The test file is the root as far as we are concerned,
179
266
  // not mocha itself
180
267
  if (m.parent.filename.match(/\/node_modules\/mocha\//)) {
@@ -254,6 +341,8 @@ module.exports = async function(options) {
254
341
  // when options.testModule is true. There must be a
255
342
  // test/ or tests/ subdir of the module containing
256
343
  // a test.js file that runs under mocha via devDependencies.
344
+ // If `options.testModule` is a string it will be used as a
345
+ // namespace for the test module.
257
346
 
258
347
  function testModule() {
259
348
  if (!options.testModule) {
@@ -277,9 +366,17 @@ module.exports = async function(options) {
277
366
  if (testDir === moduleDir) {
278
367
  throw new Error('Test file must be in test/ or tests/ subdirectory of module');
279
368
  }
369
+
370
+ const pkgName = require(`${moduleDir}/package.json`).name;
371
+ let pkgNamespace = '';
372
+ if (pkgName.includes('/')) {
373
+ const parts = pkgName.split('/');
374
+ pkgNamespace = '/' + parts.slice(0, parts.length - 1).join('/');
375
+ }
376
+
280
377
  if (!fs.existsSync(testDir + '/node_modules')) {
281
- fs.mkdirSync(testDir + '/node_modules');
282
- fs.symlinkSync(moduleDir, testDir + '/node_modules/' + require('path').basename(moduleDir), 'dir');
378
+ fs.mkdirSync(testDir + '/node_modules' + pkgNamespace, { recursive: true });
379
+ fs.symlinkSync(moduleDir, testDir + '/node_modules/' + pkgName, 'dir');
283
380
  }
284
381
 
285
382
  // Not quite superfluous: it'll return self.root, but
@@ -324,6 +421,10 @@ module.exports = async function(options) {
324
421
  synth.define(name, options);
325
422
  });
326
423
 
424
+ // Apostrophe prefers that any improvements to @apostrophecms/global
425
+ // be applied before any project level version of @apostrophecms/global
426
+ synth.applyImprovementsBeforeProjectLevel();
427
+
327
428
  return synth;
328
429
  }
329
430
 
@@ -349,6 +450,11 @@ module.exports = async function(options) {
349
450
  validSteps.push(step.name);
350
451
  }
351
452
  }
453
+
454
+ if (!fs.existsSync(self.localModules)) {
455
+ return;
456
+ }
457
+
352
458
  const dirs = fs.readdirSync(self.localModules);
353
459
  for (const dir of dirs) {
354
460
  if (dir.match(/^@/)) {
@@ -499,3 +605,17 @@ module.exports.bundle = {
499
605
  modules: abstractClasses.concat(_.keys(defaults.modules)),
500
606
  directory: 'modules'
501
607
  };
608
+
609
+ function seconds(msec) {
610
+ return (Math.round(msec / 100) / 10) + ' seconds';
611
+ }
612
+
613
+ function clusterFork() {
614
+ const worker = cluster.fork();
615
+ worker.bornAt = Date.now();
616
+ }
617
+
618
+ function respawn(worker) {
619
+ console.error(`Respawning worker process ${worker.process.pid}`);
620
+ clusterFork();
621
+ }
@@ -166,10 +166,25 @@ module.exports = function(options) {
166
166
  // Even if the package exists in node_modules it might just be a
167
167
  // sub-dependency due to npm/yarn flattening, which means we could be
168
168
  // confused by an unrelated npm module with the same name as an Apostrophe
169
- // module unless we verify it is a real project-level dependency
169
+ // module unless we verify it is a real project-level dependency. However
170
+ // if no package.json at all exists at project level we do search up the
171
+ // tree until we find one to accommodate patterns like `src/app.js`
170
172
  if (!self.validPackages) {
171
- const info = JSON.parse(fs.readFileSync(`${path.dirname(self.root.filename)}/package.json`, 'utf8'));
172
- self.validPackages = new Set([ ...Object.keys(info.dependencies || {}), ...Object.keys(info.devDependencies || {}) ]);
173
+ const initialFolder = path.dirname(self.root.filename);
174
+ let folder = initialFolder;
175
+ while (true) {
176
+ const file = `${folder}/package.json`;
177
+ if (fs.existsSync(file)) {
178
+ const info = JSON.parse(fs.readFileSync(file, 'utf8'));
179
+ self.validPackages = new Set([ ...Object.keys(info.dependencies || {}), ...Object.keys(info.devDependencies || {}) ]);
180
+ break;
181
+ } else {
182
+ folder = path.dirname(folder);
183
+ if (!folder.length) {
184
+ throw new Error(`package.json was not found in ${initialFolder} or any of its parent folders.`);
185
+ }
186
+ }
187
+ }
173
188
  }
174
189
  if (!self.validPackages.has(type)) {
175
190
  return null;
@@ -187,5 +202,28 @@ module.exports = function(options) {
187
202
  return _.has(self.improvements, name);
188
203
  };
189
204
 
205
+ self.applyImprovementsBeforeProjectLevel = () => {
206
+ for (const [ name, definition ] of Object.entries(self.definitions)) {
207
+ // At this stage the complete definition of a type is a linked list of `extend`
208
+ // properties starting from what should be project level, unless there
209
+ // are improvements. Shuffle project level to be the first in the
210
+ // linked list
211
+ if (definition.__meta.name !== self.originalToMy(name)) {
212
+ let candidate = definition;
213
+ let predecessor = null;
214
+ while (candidate.extend && ((typeof candidate.extend) === 'object')) {
215
+ predecessor = candidate;
216
+ candidate = candidate.extend;
217
+ if (candidate.__meta.name === self.originalToMy(name)) {
218
+ predecessor.extend = candidate.extend;
219
+ candidate.extend = definition;
220
+ self.definitions[name] = candidate;
221
+ break;
222
+ }
223
+ }
224
+ }
225
+ }
226
+ };
227
+
190
228
  return self;
191
229
  };
package/lib/moog.js CHANGED
@@ -142,8 +142,8 @@ module.exports = function(options) {
142
142
 
143
143
  const upgradeHints = {
144
144
  construct: 'in Apostrophe 3.x, "construct" has been replaced with "methods", "routes", "apiRoutes", etc.',
145
- beforeConstruct: 'in Apostrophe 3.x, "beforeConstruct" has been replaced with "beforeSuperClass". It takes (self, options) and should be solely concerned with modifying the options before the base class sees them. It must be synchronous. Check out the new fields section, you might not need beforeSuperClass.',
146
- afterConstruct: 'in Apostrophe 3.x, "afterConstruct" has been replaced with "init". It takes (self, options) and may be an async function.'
145
+ beforeConstruct: 'in Apostrophe 3.x, "beforeConstruct" has been replaced with "beforeSuperClass". It takes (self) and should be solely concerned with modifying the options before the base class sees them. It must be synchronous. Check out the new fields section, you might not need beforeSuperClass.',
146
+ afterConstruct: 'in Apostrophe 3.x, "afterConstruct" has been replaced with "init". It takes (self) and may be an async function.'
147
147
  };
148
148
 
149
149
  for (const step of steps) {
@@ -194,15 +194,27 @@ module.exports = function(options) {
194
194
  const groups = klona(that[`${cascade}Groups`]);
195
195
  for (const value of Object.values(properties.group)) {
196
196
  for (const field of value.fields || []) {
197
- for (const value of Object.values(groups)) {
198
- if (value.fields) {
199
- if (value.fields.includes(field)) {
200
- value.fields = value.fields.filter(_field => _field !== field);
197
+ // Remove fields from existing groups if they're added to a new
198
+ // group.
199
+ for (const val of Object.values(groups)) {
200
+ if (val.fields) {
201
+ if (val.fields.includes(field)) {
202
+ val.fields = val.fields.filter(_field => _field !== field);
201
203
  }
202
204
  }
203
205
  }
204
206
  }
205
207
  }
208
+
209
+ // Combine groups of the same name now that inherited groups are
210
+ // filtered
211
+ for (const [ key, value ] of Object.entries(properties.group)) {
212
+ if (groups[key] && Array.isArray(groups[key].fields)) {
213
+ value.fields = groups[key].fields.concat(value.fields);
214
+ value.label = value.label || groups[key].label;
215
+ }
216
+ }
217
+
206
218
  that[`${cascade}Groups`] = {
207
219
  ...groups,
208
220
  ...klona(properties.group)
@@ -289,14 +301,14 @@ module.exports = function(options) {
289
301
  };
290
302
  }
291
303
  if ((typeof step[keyword]) !== 'function') {
292
- throw stepError(step, `${keyword} must be a function that takes (self, options) and returns an object`);
304
+ throw stepError(step, `${keyword} must be a function that takes (self) and returns an object`);
293
305
  }
294
306
  _.merge(context, step[keyword](that, options));
295
307
  }
296
308
  const extend = getExtendKey(keyword);
297
309
  if (step[extend]) {
298
310
  if ((typeof step[extend]) !== 'function') {
299
- throw stepError(step, `${extend} must be a function that takes (self, options) and returns an object`);
311
+ throw stepError(step, `${extend} must be a function that takes (self) and returns an object`);
300
312
  }
301
313
  const extensions = step[extend](that, options);
302
314
  wrap(context, extensions);
@@ -25,13 +25,18 @@
25
25
  @click="switchLocale(locale)"
26
26
  >
27
27
  <span class="apos-locale">
28
- <CheckIcon
28
+ <AposIndicator
29
29
  v-if="isActive(locale)"
30
+ icon="check-bold-icon"
31
+ fill-color="var(--a-primary)"
30
32
  class="apos-check"
31
- title="$t('apostrophe:currentLocale')"
32
- :size="12"
33
+ :icon-size="12"
34
+ :title="$t('apostrophe:currentLocale')"
33
35
  />
34
36
  {{ locale.label }}
37
+ <span class="apos-locale-name">
38
+ ({{ locale.name }})
39
+ </span>
35
40
  <span
36
41
  class="apos-locale-localized"
37
42
  :class="{ 'apos-state-is-localized': isLocalized(locale) }"
@@ -43,24 +48,23 @@
43
48
  <p class="apos-available-description">
44
49
  {{ $t('apostrophe:documentExistsInLocales') }}
45
50
  </p>
46
- <span
51
+ <AposButton
47
52
  v-for="locale in availableLocales"
48
53
  :key="locale.name"
49
54
  class="apos-available-locale"
50
- >
51
- {{ locale.label }}
52
- </span>
55
+ :label="locale.label"
56
+ type="quiet"
57
+ :modifiers="['no-motion']"
58
+ @click="switchLocale(locale)"
59
+ />
53
60
  </div>
54
61
  </div>
55
62
  </AposContextMenu>
56
63
  </template>
57
64
 
58
65
  <script>
59
- import CheckIcon from 'vue-material-design-icons/Check.vue';
60
-
61
66
  export default {
62
67
  name: 'TheAposAdminBarLocale',
63
- components: { CheckIcon },
64
68
  data() {
65
69
  return {
66
70
  search: '',
@@ -79,11 +83,11 @@ export default {
79
83
  button() {
80
84
  return {
81
85
  label: {
82
- key: window.apos.i18n.locale,
86
+ key: apos.i18n.locale,
83
87
  localize: false
84
88
  },
85
89
  icon: 'chevron-down-icon',
86
- modifiers: [ 'icon-right', 'no-motion' ],
90
+ modifiers: [ 'icon-right', 'no-motion', 'uppercase' ],
87
91
  type: 'quiet'
88
92
  };
89
93
  },
@@ -149,7 +153,7 @@ export default {
149
153
  }
150
154
  } else {
151
155
  const currentLocale = apos.i18n.locales[apos.locale];
152
-
156
+ this.$refs.menu.hide();
153
157
  const toLocalize = await apos.confirm(
154
158
  {
155
159
  icon: false,
@@ -207,6 +211,13 @@ export default {
207
211
  &::after {
208
212
  right: 0;
209
213
  }
214
+
215
+ &::v-deep .apos-button__label {
216
+ @include type-small;
217
+ color: var(--a-primary);
218
+ font-weight: var(--a-weight-bold);
219
+ letter-spacing: 1px;
220
+ }
210
221
  }
211
222
 
212
223
  .apos-locales-picker {
@@ -214,15 +225,14 @@ export default {
214
225
  }
215
226
 
216
227
  .apos-locales-filter {
228
+ @include type-large;
217
229
  box-sizing: border-box;
218
230
  width: 100%;
219
- padding: 25px 45px 20px 20px;
220
- font-size: 14px;
231
+ padding: 20px 45px 20px 20px;
221
232
  border-top: 0;
222
233
  border-right: 0;
223
234
  border-bottom: 1px solid var(--a-base-9);
224
235
  border-left: 0;
225
- color: var(--a-text-primary);
226
236
  border-top-right-radius: var(--a-border-radius);
227
237
  border-top-left-radius: var(--a-border-radius);
228
238
 
@@ -242,8 +252,7 @@ export default {
242
252
  max-height: 350px;
243
253
  overflow-y: scroll;
244
254
  padding-left: 0;
245
- margin-top: 0;
246
- margin-bottom: 0;
255
+ margin: $spacing-base 0;
247
256
  font-weight: var(--a-weight-base);
248
257
  }
249
258
 
@@ -264,7 +273,7 @@ export default {
264
273
  .apos-check {
265
274
  position: absolute;
266
275
  top: 50%;
267
- left: 20px;
276
+ left: 18px;
268
277
  transform: translateY(-50%);
269
278
  color: var(--a-primary);
270
279
  stroke: var(--a-primary);
@@ -279,11 +288,12 @@ export default {
279
288
  .apos-locale-localized {
280
289
  position: relative;
281
290
  top: -1px;
291
+ left: 5px;
282
292
  display: inline-block;
283
- height: 5px;
284
- width: 5px;
293
+ width: 3px;
294
+ height: 3px;
285
295
  border: 1px solid var(--a-base-5);
286
- border-radius: 3px;
296
+ border-radius: 50%;
287
297
 
288
298
  &.apos-state-is-localized {
289
299
  background-color: var(--a-success);
@@ -293,7 +303,7 @@ export default {
293
303
  }
294
304
 
295
305
  .apos-available-locales {
296
- padding: 20px;
306
+ padding: $spacing-double;
297
307
  border-top: 1px solid var(--a-base-9);
298
308
  }
299
309
 
@@ -307,4 +317,13 @@ export default {
307
317
  margin-right: 10px;
308
318
  margin-bottom: 5px;
309
319
  }
320
+
321
+ .apos-available-description {
322
+ margin-top: 0;
323
+ }
324
+
325
+ .apos-locale-name {
326
+ text-transform: uppercase;
327
+ }
328
+
310
329
  </style>
@@ -259,20 +259,32 @@ export default {
259
259
  // So that areas revert to being editable
260
260
  await this.refresh();
261
261
  },
262
- async onContextEditing() {
263
- // Accept a hint that someone is actively typing in a
264
- // rich text editor and a context-edited event is likely
265
- // coming; allows continuity of the "Saving..." indicator
266
- // so it doesn't flicker once a second as you type
267
- this.editing = true;
268
- if (this.editingTimeout) {
269
- clearTimeout(this.editingTimeout);
262
+ // Accept a hint that a user is actively typing and/or manipulating controls
263
+ // and it would best not to enable a save button or a "...Saved" indication yet
264
+ // to avoid a frenetic display and/or a situation where not everything is ready
265
+ // to be saved yet.
266
+ //
267
+ // If the event is emitted with a boolean value of `true`, the emitter takes
268
+ // responsibility for later emitting `false` to indicate active typing/manipulating
269
+ // is no longer in progress. If the event is emitted with no value then there is a
270
+ // 1100-millisecond, debounced timeout.
271
+
272
+ async onContextEditing(state) {
273
+ if ((typeof state) === 'boolean') {
274
+ this.editing = state;
275
+ } else {
276
+ if (!this.editing) {
277
+ this.editing = true;
278
+ }
279
+ if (this.editingTimeout) {
280
+ clearTimeout(this.editingTimeout);
281
+ }
282
+ this.editingTimeout = setTimeout(() => {
283
+ this.editing = false;
284
+ // Wait slightly longer than the rich text editor does
285
+ // before sending us a context-edited event
286
+ }, 1100);
270
287
  }
271
- this.editingTimeout = setTimeout(() => {
272
- this.editing = false;
273
- // Wait slightly longer than the rich text editor does
274
- // before sending us a context-edited event
275
- }, 1100);
276
288
  },
277
289
  async onPublish(e) {
278
290
  if (!this.canPublish) {
@@ -1,5 +1,6 @@
1
1
  const _ = require('lodash');
2
2
  const deep = require('deep-get-set');
3
+ const { stripIndent } = require('common-tags');
3
4
 
4
5
  // An area is a series of zero or more widgets, in which users can add
5
6
  // and remove widgets and drag them to reorder them. This module implements
@@ -131,6 +132,14 @@ module.exports = {
131
132
  const field = self.apos.schema.getFieldById(area._fieldId);
132
133
 
133
134
  const options = field.options;
135
+ if (!options) {
136
+ throw new Error(stripIndent`
137
+ The area field ${field.name} has no options property.
138
+
139
+ You probably forgot to nest the widgets property
140
+ in an options property.
141
+ `);
142
+ }
134
143
  _.each(options.widgets, function (options, name) {
135
144
  const manager = self.widgetManagers[name];
136
145
  if (manager) {