apostrophe 3.3.0 → 3.5.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 (74) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/deploy-test-count +1 -1
  3. package/index.js +76 -11
  4. package/lib/moog-require.js +18 -3
  5. package/lib/moog.js +19 -8
  6. package/modules/@apostrophecms/admin-bar/index.js +1 -1
  7. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposAdminBarLocale.vue +44 -24
  8. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextBar.vue +3 -2
  9. package/modules/@apostrophecms/area/index.js +38 -1
  10. package/modules/@apostrophecms/area/ui/apos/apps/AposAreas.js +3 -0
  11. package/modules/@apostrophecms/area/ui/apos/components/AposAreaWidget.vue +4 -4
  12. package/modules/@apostrophecms/asset/index.js +9 -9
  13. package/modules/@apostrophecms/asset/lib/globalIcons.js +2 -0
  14. package/modules/@apostrophecms/asset/lib/webpack/src/webpack.scss.js +5 -2
  15. package/modules/@apostrophecms/attachment/index.js +63 -1
  16. package/modules/@apostrophecms/db/index.js +0 -44
  17. package/modules/@apostrophecms/doc/index.js +70 -46
  18. package/modules/@apostrophecms/doc-type/index.js +14 -15
  19. package/modules/@apostrophecms/express/index.js +2 -2
  20. package/modules/@apostrophecms/global/index.js +13 -18
  21. package/modules/@apostrophecms/i18n/i18n/en.json +13 -0
  22. package/modules/@apostrophecms/i18n/index.js +20 -3
  23. package/modules/@apostrophecms/i18n/ui/apos/apps/AposI18nCrossDomainSession.js +3 -21
  24. package/modules/@apostrophecms/i18n/ui/apos/components/AposI18nLocalize.vue +153 -121
  25. package/modules/@apostrophecms/image/ui/apos/components/AposMediaManagerEditor.vue +22 -12
  26. package/modules/@apostrophecms/image-widget/views/widget.html +1 -0
  27. package/modules/@apostrophecms/login/index.js +38 -4
  28. package/modules/@apostrophecms/login/ui/apos/components/TheAposLogin.vue +3 -0
  29. package/modules/@apostrophecms/migration/index.js +2 -1
  30. package/modules/@apostrophecms/modal/ui/apos/components/AposModalConfirm.vue +1 -1
  31. package/modules/@apostrophecms/modal/ui/apos/mixins/AposDocsManagerMixin.js +3 -1
  32. package/modules/@apostrophecms/module/index.js +2 -2
  33. package/modules/@apostrophecms/module/lib/events.js +10 -0
  34. package/modules/@apostrophecms/page/index.js +57 -104
  35. package/modules/@apostrophecms/page-type/index.js +53 -15
  36. package/modules/@apostrophecms/permission/ui/apos/components/AposInputRole.vue +10 -14
  37. package/modules/@apostrophecms/piece-type/index.js +21 -31
  38. package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManager.vue +4 -1
  39. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue +14 -6
  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 +5 -7
  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 +26 -28
  46. package/modules/@apostrophecms/schema/lib/joinr.js +1 -3
  47. package/modules/@apostrophecms/schema/ui/apos/components/AposInputRadio.vue +8 -5
  48. package/modules/@apostrophecms/schema/ui/apos/components/AposInputRelationship.vue +9 -1
  49. package/modules/@apostrophecms/schema/ui/apos/components/AposSearchList.vue +53 -11
  50. package/modules/@apostrophecms/search/index.js +3 -3
  51. package/modules/@apostrophecms/submitted-draft/index.js +1 -1
  52. package/modules/@apostrophecms/task/index.js +15 -7
  53. package/modules/@apostrophecms/template/index.js +1 -1
  54. package/modules/@apostrophecms/ui/index.js +6 -2
  55. package/modules/@apostrophecms/ui/ui/apos/components/AposButton.vue +16 -1
  56. package/modules/@apostrophecms/ui/ui/apos/components/AposIndicator.vue +5 -0
  57. package/modules/@apostrophecms/ui/ui/apos/lib/localized-v-tooltip.js +1 -1
  58. package/modules/@apostrophecms/ui/ui/apos/scss/global/_theme.scss +3 -0
  59. package/modules/@apostrophecms/ui/ui/apos/scss/global/_widgets.scss +3 -0
  60. package/modules/@apostrophecms/ui/ui/apos/scss/global/import-all.scss +2 -1
  61. package/modules/@apostrophecms/user/index.js +21 -0
  62. package/modules/@apostrophecms/util/index.js +6 -2
  63. package/modules/@apostrophecms/util/ui/src/util.js +7 -0
  64. package/package.json +4 -2
  65. package/scripts/lint-i18n.js +2 -2
  66. package/test/bootstrapping.js +3 -63
  67. package/test/draft-published.js +4 -6
  68. package/test/events.js +16 -1
  69. package/test/events2.js +1 -1
  70. package/test/login.js +183 -0
  71. package/test/moog.js +47 -0
  72. package/test/pieces.js +51 -2
  73. package/test/subdir-project/app.js +3 -0
  74. package/test/subdir-project.js +26 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,81 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.5.0 - 2021-09-23
4
+
5
+ ### Fixes
6
+
7
+ * Pinned dependency on `vue-material-design-icons` to fix `apos-build.js` build error in production.
8
+ * 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.
9
+ * Fixes moog error messages to reflect the recommended pattern of customization functions only taking `self` as an argument.
10
+ * Rich Text widgets now instantiate with a valid element from the `styles` option rather than always starting with an unclassed `<p>` tag.
11
+ * 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.
12
+ * 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.
13
+ * `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.
14
+ * 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.
15
+ * 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).
16
+ * `options.testModule` works properly when implementing unit tests for an npm module that is namespaced.
17
+
18
+ ### Changes
19
+
20
+ * 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.
21
+
22
+ ### Adds
23
+
24
+ * Rich Text widget's styles support a `def` property for specifying the default style the editor should instantiate with.
25
+ * A more helpful error message if a field of type `area` is missing its `options` property.
26
+
27
+ ## 3.4.1 - 2021-09-13
28
+
29
+ No changes. Publishing to correctly mark the latest 3.x release as "latest" in npm.
30
+
31
+ ## 3.4.0 - 2021-09-13
32
+
33
+ ### Security
34
+
35
+ * 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.
36
+ * 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.
37
+
38
+ ### Fixes
39
+
40
+ * The `apos.attachment.each` method, intended for migrations, now respects its `criteria` argument. This was necessary to the above security fix.
41
+ * Removes a lodash wrapper around `@apostrophecms/express` `bodyParser.json` options that prevented adding custom options to the body parser.
42
+ * Uses `req.clone` consistently when creating a new `req` object with a different mode or locale for localization purposes, etc.
43
+ * Fixes bug in the "select all" relationship chooser UI where it selected unpublished items.
44
+ * Fixes bug in "next" and "previous" query builders.
45
+ * Cutting and pasting widgets now works between locales that do not share a hostname, provided that you switch locales after cutting (it does not work between tabs that are already open on separate hostnames).
46
+ * The `req.session` object now exists in task `req` objects, for better compatibility. It has no actual persistence.
47
+ * Unlocalized piece types, such as users, may now be selected as part of a relationship when browsing.
48
+ * Unpublished localized piece types may not be selected via the autocomplete feature of the relationship input field, which formerly ignored this requirement, although the browse button enforced it.
49
+ * The server-side JavaScript and REST APIs to delete pieces now work properly for pieces that are not subject to either localization or draft/published workflow at all the (`localize: false` option). UI for this is under discussion, this is just a bug fix for the back end feature which already existed.
50
+ * Starting in version 3.3.1, a newly added image widget did not display its image until the page was refreshed. This has been fixed.
51
+ * A bug that prevented Undo operations from working properly and resulted in duplicate widget _id properties has been fixed.
52
+ * A bug that caused problems for Undo operations in nested widgets, i.e. layout or multicolumn widgets, has been fixed.
53
+ * Duplicate widget _id properties within the same document are now prevented on the server side at save time.
54
+ * Existing duplicate widget _id properties are corrected by a one-time migration.
55
+
56
+ ### Adds
57
+
58
+ * Adds a linter to warn in dev mode when a module name include a period.
59
+ * Lints module names for `apostrophe-` prefixes even if they don't have a module directory (e.g., only in `app.js`).
60
+ * Starts all `warnDev` messages with a line break and warning symbol (⚠️) to stand out in the console.
61
+ * `apos.util.onReady` aliases `apos.util.onReadyAndRefresh` for brevity. The `apos.util.onReadyAndRefresh` method name will be deprecated in the next major version.
62
+ * Adds a developer setting that applies a margin between parent and child areas, allowing developers to change the default spacing in nested areas.
63
+
64
+ ### Changes
65
+
66
+ * Removes the temporary `trace` method from the `@apostrophecms/db` module.
67
+ * 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.
68
+ * Only autofocuses rich text editors when they are empty.
69
+ * Nested areas now have a vertical margin applied when editing, allowing easier access to the parent area's controls.
70
+
71
+ ## 3.3.1 - 2021-09-01
72
+
73
+ ### Fixes
74
+
75
+ * In some situations it was possible for a relationship with just one selected document to list that document several times in the returned result, resulting in very large responses.
76
+ * Permissions roles UI localized correctly.
77
+ * Do not crash on startup if users have a relationship to another type. This was caused by the code that checks whether any users exist to present a warning to developers. That code was running too early for relationships to work due to event timing issues.
78
+
3
79
  ## 3.3.0 - 2021-08-30
4
80
 
5
81
  ### Fixes
package/deploy-test-count CHANGED
@@ -1 +1 @@
1
- 2
1
+ 6
package/index.js CHANGED
@@ -4,6 +4,7 @@ const argv = require('boring')({ end: true });
4
4
  const fs = require('fs');
5
5
  const npmResolve = require('resolve');
6
6
  let defaults = require('./defaults.js');
7
+ const { stripIndent } = require('common-tags');
7
8
 
8
9
  // **Awaiting the Apostrophe function is optional**
9
10
  //
@@ -102,13 +103,25 @@ module.exports = async function(options) {
102
103
  // are out there and what icons they need without
103
104
  // actually instantiating them
104
105
  self.modulesToBeInstantiated = modulesToBeInstantiated;
106
+ self.eventAliases = {};
107
+ self.aliasEvent('modulesReady', 'modulesRegistered');
108
+ self.aliasEvent('afterInit', 'ready');
105
109
 
106
110
  defineModules();
107
111
 
108
112
  await instantiateModules();
109
113
  lintModules();
110
- await self.emit('modulesReady');
111
- await self.emit('afterInit');
114
+ await self.emit('modulesRegistered'); // formerly modulesReady
115
+ self.apos.schema.validateAllSchemas();
116
+ self.apos.schema.registerAllSchemas();
117
+ await self.apos.migration.migrate(); // emits before and after events, inside the lock
118
+ await self.apos.global.insertIfMissing();
119
+ await self.apos.page.implementParkAllInDefaultLocale();
120
+ await self.apos.doc.replicate(); // emits beforeReplicate and afterReplicate events
121
+ // Replicate will have created the parked pages across locales if needed, but we may
122
+ // still need to reset parked properties
123
+ await self.apos.page.implementParkAllInOtherLocales();
124
+ await self.emit('ready'); // formerly afterInit
112
125
  if (self.taskRan) {
113
126
  process.exit(0);
114
127
  } else {
@@ -241,6 +254,8 @@ module.exports = async function(options) {
241
254
  // when options.testModule is true. There must be a
242
255
  // test/ or tests/ subdir of the module containing
243
256
  // a test.js file that runs under mocha via devDependencies.
257
+ // If `options.testModule` is a string it will be used as a
258
+ // namespace for the test module.
244
259
 
245
260
  function testModule() {
246
261
  if (!options.testModule) {
@@ -264,9 +279,17 @@ module.exports = async function(options) {
264
279
  if (testDir === moduleDir) {
265
280
  throw new Error('Test file must be in test/ or tests/ subdirectory of module');
266
281
  }
282
+
283
+ const pkgName = require(`${moduleDir}/package.json`).name;
284
+ let pkgNamespace = '';
285
+ if (pkgName.includes('/')) {
286
+ const parts = pkgName.split('/');
287
+ pkgNamespace = '/' + parts.slice(0, parts.length - 1).join('/');
288
+ }
289
+
267
290
  if (!fs.existsSync(testDir + '/node_modules')) {
268
- fs.mkdirSync(testDir + '/node_modules');
269
- fs.symlinkSync(moduleDir, testDir + '/node_modules/' + require('path').basename(moduleDir), 'dir');
291
+ fs.mkdirSync(testDir + '/node_modules' + pkgNamespace, { recursive: true });
292
+ fs.symlinkSync(moduleDir, testDir + '/node_modules/' + pkgName, 'dir');
270
293
  }
271
294
 
272
295
  // Not quite superfluous: it'll return self.root, but
@@ -336,19 +359,24 @@ module.exports = async function(options) {
336
359
  validSteps.push(step.name);
337
360
  }
338
361
  }
362
+
363
+ if (!fs.existsSync(self.localModules)) {
364
+ return;
365
+ }
366
+
339
367
  const dirs = fs.readdirSync(self.localModules);
340
368
  for (const dir of dirs) {
341
369
  if (dir.match(/^@/)) {
342
370
  const nsDirs = fs.readdirSync(`${self.localModules}/${dir}`);
343
371
  for (let nsDir of nsDirs) {
344
372
  nsDir = `${dir}/${nsDir}`;
345
- test(nsDir);
373
+ testDir(nsDir);
346
374
  }
347
375
  } else {
348
- test(dir);
376
+ testDir(dir);
349
377
  }
350
378
  }
351
- function test(name) {
379
+ function testDir(name) {
352
380
  // Projects that have different theme modules activated at different times
353
381
  // are a frequent source of false positives for this warning, so ignore
354
382
  // seemingly unused modules with "theme" in the name
@@ -362,14 +390,25 @@ module.exports = async function(options) {
362
390
  // index.js might not exist, that's fine for our purposes
363
391
  }
364
392
  if (name.match(/^apostrophe-/)) {
365
- warn('namespace-apostrophe-modules', `You have a ${self.localModules}/${name} folder. You are probably trying to configure an official Apostrophe module, but those are namespaced now. Your directory should be renamed ${self.localModules}/${name.replace(/^apostrophe-/, '@apostrophecms/')}\n\nIf you get this warning for your own, original module, do not use the apostrophe- prefix. It is reserved.`);
393
+ warn(
394
+ 'namespace-apostrophe-modules',
395
+ stripIndent`
396
+ You have a ${self.localModules}/${name} folder.
397
+ You are probably trying to configure an official Apostrophe module, but those
398
+ are namespaced now. Your directory should be renamed
399
+ ${self.localModules}/${name.replace(/^apostrophe-/, '@apostrophecms/')}
400
+
401
+ If you get this warning for your own, original module, do not use the
402
+ "apostrophe-" prefix. It is reserved.
403
+ `
404
+ );
366
405
  } else {
367
406
  warn('orphan-modules', `You have a ${self.localModules}/${name} folder, but that module is not activated in app.js and it is not a base class of any other active module. Right now that code doesn't do anything.`);
368
407
  }
369
408
  }
370
409
  function warn(name, message) {
371
- if (self.utils) {
372
- self.utils.warnDevOnce(name, message);
410
+ if (self.util) {
411
+ self.util.warnDevOnce(name, message);
373
412
  } else {
374
413
  // apos.util not in play, this can be the case in our bootstrap tests
375
414
  if (self.argv[`ignore-${name}`]) {
@@ -382,6 +421,30 @@ module.exports = async function(options) {
382
421
  }
383
422
 
384
423
  for (const [ name, module ] of Object.entries(self.modules)) {
424
+ if (name.match(/^apostrophe-/)) {
425
+ self.util.warnDevOnce(
426
+ 'namespace-apostrophe-modules',
427
+ stripIndent`
428
+ You have configured an ${name} module.
429
+ You are probably trying to configure an official Apostrophe module, but those
430
+ are namespaced now. Your module should be renamed ${name.replace(/^apostrophe-/, '@apostrophecms/')}
431
+
432
+ If you get this warning for your own original module, do not use the
433
+ "apostrophe-" prefix. It is reserved.
434
+ `
435
+ );
436
+ }
437
+ const moduleNameRegex = /\./;
438
+ if (name.match(moduleNameRegex)) {
439
+ self.util.warnDevOnce(
440
+ 'module-name-periods',
441
+ stripIndent`
442
+ You have configured a module named ${name}.
443
+ Modules names may not include periods. Please change this to avoid bugs.
444
+ `
445
+ );
446
+ }
447
+
385
448
  if (module.options.extends && ((typeof module.options.extends) === 'string')) {
386
449
  lint(`The module ${name} contains an "extends" option. This is probably a\nmistake. In Apostrophe "extend" is used to extend other modules.`);
387
450
  }
@@ -437,7 +500,9 @@ module.exports = async function(options) {
437
500
  }
438
501
 
439
502
  function lint(s) {
440
- self.util.warnDev('\n⚠️ It looks like you may have made a mistake in your code:\n\n' + s + '\n');
503
+ self.util.warnDev(stripIndent`
504
+ It looks like you may have made a mistake in your code:\n${s}
505
+ `);
441
506
  }
442
507
  }
443
508
 
@@ -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;
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,26 @@ 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
+ }
215
+ }
216
+
206
217
  that[`${cascade}Groups`] = {
207
218
  ...groups,
208
219
  ...klona(properties.group)
@@ -289,14 +300,14 @@ module.exports = function(options) {
289
300
  };
290
301
  }
291
302
  if ((typeof step[keyword]) !== 'function') {
292
- throw stepError(step, `${keyword} must be a function that takes (self, options) and returns an object`);
303
+ throw stepError(step, `${keyword} must be a function that takes (self) and returns an object`);
293
304
  }
294
305
  _.merge(context, step[keyword](that, options));
295
306
  }
296
307
  const extend = getExtendKey(keyword);
297
308
  if (step[extend]) {
298
309
  if ((typeof step[extend]) !== 'function') {
299
- throw stepError(step, `${extend} must be a function that takes (self, options) and returns an object`);
310
+ throw stepError(step, `${extend} must be a function that takes (self) and returns an object`);
300
311
  }
301
312
  const extensions = step[extend](that, options);
302
313
  wrap(context, extensions);
@@ -21,7 +21,7 @@ module.exports = {
21
21
  },
22
22
  handlers(self) {
23
23
  return {
24
- 'apostrophe:afterInit': {
24
+ 'apostrophe:ready': {
25
25
  orderAndGroupItems() {
26
26
  self.orderItems();
27
27
  self.groupItems();
@@ -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
  },
@@ -136,7 +140,8 @@ export default {
136
140
  const result = await apos.http.post(`${apos.i18n.action}/locale`, {
137
141
  body: {
138
142
  contextDocId: apos.adminBar.context && apos.adminBar.context._id,
139
- locale: name
143
+ locale: name,
144
+ clipboard: localStorage.getItem('aposWidgetClipboard')
140
145
  }
141
146
  });
142
147
 
@@ -148,7 +153,7 @@ export default {
148
153
  }
149
154
  } else {
150
155
  const currentLocale = apos.i18n.locales[apos.locale];
151
-
156
+ this.$refs.menu.hide();
152
157
  const toLocalize = await apos.confirm(
153
158
  {
154
159
  icon: false,
@@ -206,6 +211,13 @@ export default {
206
211
  &::after {
207
212
  right: 0;
208
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
+ }
209
221
  }
210
222
 
211
223
  .apos-locales-picker {
@@ -213,15 +225,14 @@ export default {
213
225
  }
214
226
 
215
227
  .apos-locales-filter {
228
+ @include type-large;
216
229
  box-sizing: border-box;
217
230
  width: 100%;
218
- padding: 25px 45px 20px 20px;
219
- font-size: 14px;
231
+ padding: 20px 45px 20px 20px;
220
232
  border-top: 0;
221
233
  border-right: 0;
222
234
  border-bottom: 1px solid var(--a-base-9);
223
235
  border-left: 0;
224
- color: var(--a-text-primary);
225
236
  border-top-right-radius: var(--a-border-radius);
226
237
  border-top-left-radius: var(--a-border-radius);
227
238
 
@@ -241,8 +252,7 @@ export default {
241
252
  max-height: 350px;
242
253
  overflow-y: scroll;
243
254
  padding-left: 0;
244
- margin-top: 0;
245
- margin-bottom: 0;
255
+ margin: $spacing-base 0;
246
256
  font-weight: var(--a-weight-base);
247
257
  }
248
258
 
@@ -263,7 +273,7 @@ export default {
263
273
  .apos-check {
264
274
  position: absolute;
265
275
  top: 50%;
266
- left: 20px;
276
+ left: 18px;
267
277
  transform: translateY(-50%);
268
278
  color: var(--a-primary);
269
279
  stroke: var(--a-primary);
@@ -278,11 +288,12 @@ export default {
278
288
  .apos-locale-localized {
279
289
  position: relative;
280
290
  top: -1px;
291
+ left: 5px;
281
292
  display: inline-block;
282
- height: 5px;
283
- width: 5px;
293
+ width: 3px;
294
+ height: 3px;
284
295
  border: 1px solid var(--a-base-5);
285
- border-radius: 3px;
296
+ border-radius: 50%;
286
297
 
287
298
  &.apos-state-is-localized {
288
299
  background-color: var(--a-success);
@@ -292,7 +303,7 @@ export default {
292
303
  }
293
304
 
294
305
  .apos-available-locales {
295
- padding: 20px;
306
+ padding: $spacing-double;
296
307
  border-top: 1px solid var(--a-base-9);
297
308
  }
298
309
 
@@ -306,4 +317,13 @@ export default {
306
317
  margin-right: 10px;
307
318
  margin-bottom: 5px;
308
319
  }
320
+
321
+ .apos-available-description {
322
+ margin-top: 0;
323
+ }
324
+
325
+ .apos-locale-name {
326
+ text-transform: uppercase;
327
+ }
328
+
309
329
  </style>
@@ -48,7 +48,7 @@ export default {
48
48
  // If the URL references a draft, go into draft mode but then clean up the URL
49
49
  const draftMode = query.aposMode || 'published';
50
50
  if (draftMode === 'draft') {
51
- const newQuery = { ... query };
51
+ const newQuery = { ...query };
52
52
  delete newQuery.aposMode;
53
53
  history.replaceState(null, '', apos.http.addQueryToUrl(location.href, newQuery));
54
54
  }
@@ -442,6 +442,7 @@ export default {
442
442
  this.rememberLastBaseContext();
443
443
  },
444
444
  onContextEdited(patch) {
445
+ patch = klona(patch);
445
446
  this.patchesSinceLoaded.push(patch);
446
447
  this.patchesSinceSave.push(patch);
447
448
  this.undone = [];
@@ -512,7 +513,7 @@ export default {
512
513
 
513
514
  if (refreshable) {
514
515
  refreshable.innerHTML = content;
515
- if (!this.original) {
516
+ if (this.editMode && (!this.original)) {
516
517
  // the first time we enter edit mode on the page, we need to
517
518
  // establish a baseline for undo/redo. Use our
518
519
  // "@ notation" PATCH feature. Sort the areas by DOM depth
@@ -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
@@ -26,6 +27,7 @@ module.exports = {
26
27
  self.richTextWidgetTypes = [];
27
28
  self.widgetManagers = {};
28
29
  self.enableBrowserData();
30
+ self.addDeduplicateWidgetIdsMigration();
29
31
  },
30
32
  apiRoutes(self) {
31
33
  return {
@@ -52,6 +54,9 @@ module.exports = {
52
54
  widget = await sanitize(widget);
53
55
  widget._edit = true;
54
56
  widget._docId = _docId;
57
+ // So that carrying out relationship loading again can yield results
58
+ // (the idsStorage must be populated as if we were saving)
59
+ self.apos.schema.prepareForStorage(req, widget);
55
60
  await load();
56
61
  return render();
57
62
  async function sanitize(widget) {
@@ -71,7 +76,7 @@ module.exports = {
71
76
  },
72
77
  handlers(self) {
73
78
  return {
74
- 'apostrophe:modulesReady': {
79
+ 'apostrophe:modulesRegistered': {
75
80
  getRichTextWidgetTypes() {
76
81
  _.each(self.widgetManagers, function (manager, name) {
77
82
  if (manager.getRichText) {
@@ -127,6 +132,14 @@ module.exports = {
127
132
  const field = self.apos.schema.getFieldById(area._fieldId);
128
133
 
129
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
+ }
130
143
  _.each(options.widgets, function (options, name) {
131
144
  const manager = self.widgetManagers[name];
132
145
  if (manager) {
@@ -564,6 +577,30 @@ module.exports = {
564
577
  widgetManagers,
565
578
  action: self.action
566
579
  };
580
+ },
581
+ async addDeduplicateWidgetIdsMigration() {
582
+ self.apos.migration.add('deduplicate-widget-ids', () => {
583
+ // Make them globally unique because that is easiest to
584
+ // definitely get correct for this one-time migration, although
585
+ // there is no guarantee that widget ids are unique between
586
+ // separate documents going forward. The guarantee is that they
587
+ // will be unique within documents
588
+ const seen = new Set();
589
+ return self.apos.migration.eachWidget({}, async (doc, widget, dotPath) => {
590
+ if ((!widget._id) || seen.has(widget._id)) {
591
+ const _id = self.apos.util.generateId();
592
+ return self.apos.doc.db.updateOne({
593
+ _id: doc._id
594
+ }, {
595
+ $set: {
596
+ [`${dotPath}._id`]: _id
597
+ }
598
+ });
599
+ } else {
600
+ seen.add(widget._id);
601
+ }
602
+ });
603
+ });
567
604
  }
568
605
  };
569
606
  },
@@ -6,6 +6,9 @@ export default function() {
6
6
  createWidgetClipboardApp();
7
7
 
8
8
  prepareAreas();
9
+
10
+ document.documentElement.style.setProperty('--a-widget-margin', apos.ui.widgetMargin);
11
+
9
12
  apos.bus.$on('widget-rendered', function() {
10
13
  prepareAreas();
11
14
  });
@@ -384,9 +384,9 @@ export default {
384
384
  }
385
385
  if (!this.focused) {
386
386
  this.state.labels.show = false;
387
+ this.state.add.top.show = false;
388
+ this.state.add.bottom.show = false;
387
389
  }
388
- this.state.add.top.show = false;
389
- this.state.add.bottom.show = false;
390
390
  },
391
391
 
392
392
  focus(e) {
@@ -396,8 +396,8 @@ export default {
396
396
  this.focused = true;
397
397
  this.state.container.focus = true;
398
398
  this.state.controls.show = true;
399
- this.state.add.top.show = false;
400
- this.state.add.bottom.show = false;
399
+ this.state.add.top.show = true;
400
+ this.state.add.bottom.show = true;
401
401
  this.state.labels.show = true;
402
402
  document.addEventListener('click', this.unfocus);
403
403
  },