apostrophe 3.48.0 → 3.49.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 (45) hide show
  1. package/CHANGELOG.md +43 -2
  2. package/index.js +20 -2
  3. package/lib/locales.js +1 -1
  4. package/lib/moog-require.js +3 -0
  5. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextBar.vue +12 -2
  6. package/modules/@apostrophecms/area/ui/apos/components/AposAreaEditor.vue +2 -0
  7. package/modules/@apostrophecms/area/ui/apos/components/AposAreaWidget.vue +7 -24
  8. package/modules/@apostrophecms/asset/index.js +27 -2
  9. package/modules/@apostrophecms/asset/lib/webpack/apos/webpack.config.js +23 -2
  10. package/modules/@apostrophecms/asset/lib/webpack/src/webpack.config.js +26 -2
  11. package/modules/@apostrophecms/doc/index.js +149 -0
  12. package/modules/@apostrophecms/doc-type/index.js +9 -1
  13. package/modules/@apostrophecms/global/index.js +4 -15
  14. package/modules/@apostrophecms/i18n/i18n/en.json +3 -2
  15. package/modules/@apostrophecms/i18n/index.js +76 -61
  16. package/modules/@apostrophecms/image/ui/apos/components/AposMediaManagerDisplay.vue +14 -1
  17. package/modules/@apostrophecms/login/ui/apos/components/AposForgotPasswordForm.vue +3 -60
  18. package/modules/@apostrophecms/login/ui/apos/components/AposLoginForm.vue +3 -231
  19. package/modules/@apostrophecms/login/ui/apos/components/AposResetPasswordForm.vue +3 -96
  20. package/modules/@apostrophecms/login/ui/apos/components/TheAposLogin.vue +2 -99
  21. package/modules/@apostrophecms/login/ui/apos/logic/AposForgotPasswordForm.js +68 -0
  22. package/modules/@apostrophecms/login/ui/apos/logic/AposLoginForm.js +239 -0
  23. package/modules/@apostrophecms/login/ui/apos/logic/AposResetPasswordForm.js +105 -0
  24. package/modules/@apostrophecms/login/ui/apos/logic/TheAposLogin.js +107 -0
  25. package/modules/@apostrophecms/modal/ui/apos/components/AposDocsManagerToolbar.vue +9 -3
  26. package/modules/@apostrophecms/modal/ui/apos/components/AposModalToolbar.vue +1 -0
  27. package/modules/@apostrophecms/page/index.js +63 -1
  28. package/modules/@apostrophecms/piece-type/index.js +57 -9
  29. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposImageControlDialog.vue +11 -8
  30. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue +226 -72
  31. package/modules/@apostrophecms/schema/index.js +0 -1
  32. package/modules/@apostrophecms/schema/lib/addFieldTypes.js +35 -7
  33. package/modules/@apostrophecms/schema/ui/apos/components/AposInputSlug.vue +21 -1
  34. package/modules/@apostrophecms/schema/ui/apos/components/AposInputString.vue +12 -7
  35. package/modules/@apostrophecms/schema/ui/apos/components/AposSchema.vue +1 -0
  36. package/modules/@apostrophecms/ui/ui/apos/components/AposCombo.vue +178 -20
  37. package/modules/@apostrophecms/ui/ui/apos/components/AposFilterMenu.vue +1 -1
  38. package/modules/@apostrophecms/ui/ui/apos/components/AposPager.vue +4 -6
  39. package/modules/@apostrophecms/ui/ui/apos/scss/mixins/_theme_mixins.scss +1 -0
  40. package/modules/@apostrophecms/util/index.js +5 -6
  41. package/modules/@apostrophecms/util/ui/src/http.js +6 -3
  42. package/package.json +20 -3
  43. package/test/change-doc-ids.js +134 -0
  44. package/test/i18n.js +310 -0
  45. package/test/static-i18n.js +0 -105
package/CHANGELOG.md CHANGED
@@ -1,5 +1,46 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.49.0 (2023-06-08)
4
+
5
+ ### Changes
6
+ * Updates area UX to not display Add Content controls when a widget is focused.
7
+ * Updates area UX to unfocus widget on esc key.
8
+ * Updates widget UI to use dashed outlines instead of borders to indicate bounds.
9
+ * Updates UI for Insert Menu.
10
+ * Updates Insert Menu UX to allow mid-node insertion.
11
+ * Rich Text Widget's Insert components are now expected to emit `done` and `cancel` for proper RT cleanup. `close` still supported for BC, acts as `done`.
12
+ * Migrated the business logic of the login-related Vue components to external mixins, so that the templates and styles can be overridden by
13
+ copying the component `.vue` file to project level without copying all of the business logic. If you have already copied the components to style them,
14
+ we encourage you to consider replacing your `script` tag with the new version, which just imports the mixin, so that fixes we make there will be
15
+ available in your project.
16
+
17
+ ### Adds
18
+ * Adds keyboard accessibility to Insert menu.
19
+ * Adds regex pattern feature for string fields.
20
+ * Adds `pnpm` support. Introduces new optional Apostrophe root configuration `pnpm` to force opt-in/out when auto detection fails. See the [documentation](https://v3.docs.apostrophecms.org/guide/using-pnpm.html) for more details.
21
+ * Adds a warning if database queries involving relationships
22
+ are made before the last `apostrophe:modulesRegistered` handler has fired.
23
+ If you need to call Apostrophe's `find()` methods at startup,
24
+ it is best to wait for the `@apostrophecms/doc:beforeReplicate` event.
25
+ * Allow `@` when a piece is a template and `/@` for page templates (doc-template-library module).
26
+ * Adds a `prefix` option to the http frontend util module.
27
+ If explicitly set to `false`, prevents the prefix from being automatically added to the URL,
28
+ when making calls with already-prefixed URLs for instance.
29
+ * Adds the `redirectToFirstLocale` option to the `i18n` module to prevent users from reaching a version of their site that would not match any locale when requesting the site without a locale prefix in the URL.
30
+ * If just one instance of a piece type should always exist (per locale if localized), the
31
+ `singletonAuto` option may now be set to `true` or to an object with a `slug` option in
32
+ order to guarantee it. This implicitly sets `singleton: true` as well. This is now used
33
+ internally by `@apostrophecms/global` as well as the optional `@apostrophecms-pro/palette` module.
34
+
35
+ ### Fixes
36
+ * Fix 404 error when viewing/editing a doc which draft has a different version of the slug than the published one.
37
+ * Fixed a bug where multiple home pages can potentially be inserted into the database if the
38
+ default locale is renamed. Introduced the `async apos.doc.bestAposDocId(criteria)` method to
39
+ help identify the right `aposDocId` when inserting a document that might exist in
40
+ other locales.
41
+ * Fixed a bug where singletons like the global doc might not be inserted at all if they
42
+ exist under the former name of the default locale and there are no other locales.
43
+
3
44
  ## 3.48.0 (2023-05-26)
4
45
 
5
46
  ### Adds
@@ -105,10 +146,10 @@ shouldn't close the link dialog etc.
105
146
 
106
147
  ### Fixes
107
148
 
108
- * Fix various issues on conditional fields that were occurring when adding new widgets with default values or selecting a falsy value in a field that has a conditional field relying on it.
149
+ * Fix various issues on conditional fields that were occurring when adding new widgets with default values or selecting a falsy value in a field that has a conditional field relying on it.
109
150
  Populate new or existing doc instances with default values and add an empty `null` choice to select fields that do not have a default value (required or not) and to the ones configured with dynamic choices.
110
151
  * Rich text widgets save more reliably when many actions are taken quickly just before save.
111
- * Fix an issue in the `oembed` field where the value was kept in memory after cancelling the widget editor, which resulted in saving the value if the widget was nested and the parent widget was saved.
152
+ * Fix an issue in the `oembed` field where the value was kept in memory after cancelling the widget editor, which resulted in saving the value if the widget was nested and the parent widget was saved.
112
153
  Also improve the `oembed` field UX by setting the input as `readonly` rather than `disabled` when fetching the video metadata, in order to avoid losing its focus when typing.
113
154
 
114
155
  ## 3.44.0 (2023-04-13)
package/index.js CHANGED
@@ -55,6 +55,13 @@ let defaults = require('./defaults.js');
55
55
  // If set, Apostrophe will invoke it (await) before invoking process.exit.
56
56
  // `beforeExit` may be an async function, will be awaited, and takes no arguments.
57
57
  //
58
+ // `pnpm`
59
+ // A boolean to force on or off the pnpm related build routines. If not set,
60
+ // an automated check will be performed to determine if pnpm is in use. We offer
61
+ // an option, because automated check is not 100% reliable. Monorepo tools are
62
+ // often hiding package management specifics (lock files, node_module structure, etc.)
63
+ // in a centralized store.
64
+ //
58
65
  // ## Awaiting the Apostrophe function
59
66
  //
60
67
  // The apos function is async, but in typical cases you do not
@@ -222,6 +229,11 @@ async function apostrophe(options, telemetry, rootSpan) {
222
229
  self.root = options.root || getRoot();
223
230
  self.rootDir = options.rootDir || path.dirname(self.root.filename);
224
231
  self.npmRootDir = options.npmRootDir || self.rootDir;
232
+ self.selfDir = __dirname;
233
+ // Signals to various (build related) places that we are running a pnpm installation.
234
+ // The relevant option, if set, has a higher precedence over the automated check.
235
+ self.isPnpm = options.pnpm ??
236
+ fs.existsSync(path.join(self.npmRootDir, 'pnpm-lock.yaml'));
225
237
 
226
238
  testModule();
227
239
 
@@ -290,7 +302,13 @@ async function apostrophe(options, telemetry, rootSpan) {
290
302
  self.apos.schema.registerAllSchemas();
291
303
  await self.apos.lock.withLock('@apostrophecms/migration:migrate', async () => {
292
304
  await self.apos.migration.migrate(); // emits before and after events, inside the lock
293
- await self.apos.global.insertIfMissing();
305
+ // Inserts the global doc in the default locale if it does not exist; same for other
306
+ // singleton piece types registered by other modules
307
+ for (const module of Object.values(self.modules)) {
308
+ if (self.instanceOf(module, '@apostrophecms/piece-type') && module.options.singletonAuto) {
309
+ await module.insertIfMissing();
310
+ }
311
+ }
294
312
  await self.apos.page.implementParkAllInDefaultLocale();
295
313
  await self.apos.doc.replicate(); // emits beforeReplicate and afterReplicate events
296
314
  // Replicate will have created the parked pages across locales if needed, but we may
@@ -632,7 +650,7 @@ async function apostrophe(options, telemetry, rootSpan) {
632
650
  `
633
651
  );
634
652
  } else {
635
- 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.`);
653
+ warn('orphan-modules', `You have a ${self.localModules}/${name} folder, but that module is not activated in app.js\nand it is not a base class of any other active module. Right now that code doesn't do anything.`);
636
654
  }
637
655
  }
638
656
  function warn(name, message) {
package/lib/locales.js CHANGED
@@ -32,7 +32,7 @@ module.exports = {
32
32
  ) {
33
33
  throw new Error(stripIndent`
34
34
  If some of your locales have hostnames, then they all must have
35
- hostnames, or your top-level baseUrl option must be set.
35
+ hostnames, and your top-level baseUrl option must be set.
36
36
 
37
37
  In development, you can set baseUrl to http://localhost:3000
38
38
  for testing purposes. In production it should always be set
@@ -95,6 +95,7 @@ module.exports = function(options) {
95
95
  npmDefinition = importFresh(npmPath);
96
96
  npmDefinition.__meta = {
97
97
  npm: true,
98
+ bundled: _.has(self.bundled, type),
98
99
  dirname: path.dirname(npmPath),
99
100
  filename: npmPath,
100
101
  name: type
@@ -165,6 +166,8 @@ module.exports = function(options) {
165
166
  // multiple references to my-foo which is ambiguous
166
167
  result.__meta.name = self.originalToMy(originalType);
167
168
  }
169
+ // Mark "my" modules as such
170
+ result.__meta.my = self.isMy(result.__meta.name);
168
171
  return result;
169
172
  };
170
173
 
@@ -533,14 +533,24 @@ export default {
533
533
  aposEdit: '1'
534
534
  } : {})
535
535
  };
536
- const url = apos.http.addQueryToUrl(window.location.href, qs);
536
+
537
+ const { action } = window.apos.modules[this.context.type];
538
+ const doc = await apos.http.get(`${action}/${this.context.aposDocId}`, {
539
+ qs: {
540
+ aposMode: this.draftMode,
541
+ project: { _url: 1 }
542
+ }
543
+ });
544
+
545
+ const url = apos.http.addQueryToUrl(doc._url, qs);
537
546
  const content = await apos.http.get(url, {
538
547
  qs,
539
548
  headers: {
540
549
  'Cache-Control': 'no-cache'
541
550
  },
542
551
  draft: true,
543
- busy: true
552
+ busy: true,
553
+ prefix: false
544
554
  });
545
555
 
546
556
  refreshable.innerHTML = content;
@@ -249,6 +249,8 @@ export default {
249
249
  },
250
250
  updateWidgetFocused(widgetId) {
251
251
  this.focusedWidget = widgetId;
252
+ // Attached to window so that modals can see the area is active
253
+ window.apos.focusedWidget = widgetId;
252
254
  },
253
255
  async up(i) {
254
256
  if (this.docId === window.apos.adminBar.contextId) {
@@ -530,33 +530,16 @@ export default {
530
530
  .apos-area-widget-inner {
531
531
  position: relative;
532
532
  min-height: 50px;
533
- &:before, &:after {
534
- content: '';
535
- position: absolute;
536
- left: 0;
537
- width: 100%;
538
- height: 1px;
539
- border-top: 1px dashed var(--a-primary);
540
- opacity: 0;
541
- transition: opacity 0.2s ease;
542
- pointer-events: none;
543
- }
544
-
545
- &:before {
546
- top: 0;
547
- }
548
- &:after {
549
- bottom: 0;
550
- }
533
+ border-radius: var(--a-border-radius);
534
+ outline: 1px solid transparent;
535
+ transition: outline 0.2s ease;
551
536
  &.apos-is-highlighted {
552
- &:before, &:after {
553
- opacity: 0.4;
554
- }
537
+ outline: 1px dashed var(--a-primary-transparent-50);
555
538
  }
556
539
  &.apos-is-focused {
557
- &:before, &:after {
558
- opacity: 1;
559
- border-top: 1px solid var(--a-primary);
540
+ outline: 1px dashed var(--a-primary);
541
+ &::v-deep .apos-rich-text-editor__editor.apos-is-visually-empty {
542
+ box-shadow: none;
560
543
  }
561
544
  }
562
545
 
@@ -260,11 +260,12 @@ module.exports = {
260
260
  });
261
261
  }
262
262
 
263
- async function moduleOverrides(modulesDir, source) {
263
+ async function moduleOverrides(modulesDir, source, pnpmPaths) {
264
264
  await fs.remove(modulesDir);
265
265
  await fs.mkdirp(modulesDir);
266
266
  let names = {};
267
267
  const directories = {};
268
+ const pnpmOnly = {};
268
269
  // Most other modules are not actually instantiated yet, but
269
270
  // we can access their metadata, which is sufficient
270
271
  for (const name of modulesToInstantiate) {
@@ -273,6 +274,9 @@ module.exports = {
273
274
  for (const entry of metadata.__meta.chain) {
274
275
  const effectiveName = entry.name.replace(/^my-/, '');
275
276
  names[effectiveName] = true;
277
+ if (entry.npm && !entry.bundled && !entry.my) {
278
+ pnpmOnly[entry.dirname] = true;
279
+ }
276
280
  ancestorDirectories.push(entry.dirname);
277
281
  directories[effectiveName] = directories[effectiveName] || [];
278
282
  for (const dir of ancestorDirectories) {
@@ -288,6 +292,21 @@ module.exports = {
288
292
  for (const dir of directories[name]) {
289
293
  const srcDir = `${dir}/${source}`;
290
294
  if (fs.existsSync(srcDir)) {
295
+ if (
296
+ // is pnpm installation
297
+ self.apos.isPnpm &&
298
+ // is npm module and not bundled
299
+ pnpmOnly[dir] &&
300
+ // isn't apos core module
301
+ !dir.startsWith(path.join(self.apos.npmRootDir, 'node_modules/apostrophe/'))
302
+ ) {
303
+ // Ignore further attempts to register this path (performance)
304
+ pnpmOnly[dir] = false;
305
+ // resolve symlinked pnpm path
306
+ const resolved = fs.realpathSync(dir);
307
+ // go up to the pnpm node_modules directory
308
+ pnpmPaths.add(resolved.split(name)[0]);
309
+ }
291
310
  await fs.copy(srcDir, moduleDir);
292
311
  }
293
312
  }
@@ -302,7 +321,9 @@ module.exports = {
302
321
  }));
303
322
  const modulesDir = `${buildDir}/${name}/modules`;
304
323
  const source = options.source || name;
305
- await moduleOverrides(modulesDir, `ui/${source}`);
324
+ // Gather pnpm modules that are used in the build to be added as resolve paths
325
+ const pnpmModules = new Set();
326
+ await moduleOverrides(modulesDir, `ui/${source}`, pnpmModules);
306
327
 
307
328
  let iconImports, componentImports, tiptapExtensionImports, appImports, indexJsImports, indexSassImports;
308
329
  if (options.apos) {
@@ -382,6 +403,7 @@ module.exports = {
382
403
  outputPath: bundleDir,
383
404
  outputFilename,
384
405
  bundles: webpackExtraBundles,
406
+ pnpmModulesResolvePaths: pnpmModules,
385
407
  // Added on the fly by the
386
408
  // @apostrophecms/asset-es5 module,
387
409
  // if it is present
@@ -774,10 +796,13 @@ module.exports = {
774
796
  async function findPackageLock() {
775
797
  const packageLockPath = path.join(self.apos.npmRootDir, 'package-lock.json');
776
798
  const yarnPath = path.join(self.apos.npmRootDir, 'yarn.lock');
799
+ const pnpmPath = path.join(self.apos.npmRootDir, 'pnpm-lock.yaml');
777
800
  if (await fs.pathExists(packageLockPath)) {
778
801
  return packageLockPath;
779
802
  } else if (await fs.pathExists(yarnPath)) {
780
803
  return yarnPath;
804
+ } else if (await fs.pathExists(pnpmPath)) {
805
+ return pnpmPath;
781
806
  } else {
782
807
  return false;
783
808
  }
@@ -11,7 +11,12 @@ if (process.env.APOS_BUNDLE_ANALYZER) {
11
11
  }
12
12
 
13
13
  module.exports = ({
14
- importFile, modulesDir, outputPath, outputFilename
14
+ importFile,
15
+ modulesDir,
16
+ outputPath,
17
+ outputFilename,
18
+ // it's a Set, not an array
19
+ pnpmModulesResolvePaths
15
20
  }, apos) => {
16
21
  const tasks = [ scss, vue, js ].map(task =>
17
22
  task(
@@ -23,6 +28,7 @@ module.exports = ({
23
28
  )
24
29
  );
25
30
 
31
+ const pnpmModulePath = apos.isPnpm ? [ path.join(apos.selfDir, '../') ] : [];
26
32
  const config = {
27
33
  entry: importFile,
28
34
  // Ensure that the correct version of vue-loader is found
@@ -47,7 +53,16 @@ module.exports = ({
47
53
  // at a later date if needed
48
54
  resolveLoader: {
49
55
  extensions: [ '*', '.js', '.vue', '.json' ],
50
- modules: [ 'node_modules/apostrophe/node_modules', 'node_modules' ]
56
+ modules: [
57
+ // 1. Allow webpack to find loaders from core dependencies (pnpm), empty if not pnpm
58
+ ...pnpmModulePath,
59
+ // 2. Allow webpack to find loaders from dependencies of any project level packages (pnpm),
60
+ // empty if not pnpm
61
+ ...[ ...pnpmModulesResolvePaths ],
62
+ // 3. npm related paths
63
+ 'node_modules/apostrophe/node_modules',
64
+ 'node_modules'
65
+ ]
51
66
  },
52
67
  resolve: {
53
68
  extensions: [ '*', '.js', '.vue', '.json' ],
@@ -58,6 +73,12 @@ module.exports = ({
58
73
  },
59
74
  modules: [
60
75
  'node_modules',
76
+ // 1. Allow webpack to find imports from core dependencies (pnpm), empty if not pnpm
77
+ ...pnpmModulePath,
78
+ // 2. Allow webpack to find imports from dependencies of any project level packages (pnpm),
79
+ // empty if not pnpm
80
+ ...[ ...pnpmModulesResolvePaths ],
81
+ // 3. npm related paths
61
82
  `${apos.npmRootDir}/node_modules/apostrophe/node_modules`,
62
83
  `${apos.npmRootDir}/node_modules`
63
84
  ],
@@ -10,7 +10,15 @@ if (process.env.APOS_BUNDLE_ANALYZER) {
10
10
  }
11
11
 
12
12
  module.exports = ({
13
- importFile, modulesDir, outputPath, outputFilename, bundles = {}, es5, es5TaskFn
13
+ importFile,
14
+ modulesDir,
15
+ outputPath,
16
+ outputFilename,
17
+ // it's a Set, not an array
18
+ pnpmModulesResolvePaths,
19
+ bundles = {},
20
+ es5,
21
+ es5TaskFn
14
22
  }, apos) => {
15
23
  const mainBundleName = outputFilename.replace('.js', '');
16
24
  const taskFns = [ scssTask, ...(es5 ? [ es5TaskFn ] : []) ];
@@ -27,6 +35,7 @@ module.exports = ({
27
35
  );
28
36
 
29
37
  const moduleName = es5 ? 'nomodule' : 'module';
38
+ const pnpmModulePath = apos.isPnpm ? [ path.join(apos.selfDir, '../') ] : [];
30
39
  const config = {
31
40
  entry: {
32
41
  [mainBundleName]: importFile,
@@ -57,7 +66,16 @@ module.exports = ({
57
66
  extensions: [ '*', '.js' ],
58
67
  // Make sure css-loader and postcss-loader can always be found, even
59
68
  // if npm didn't hoist them
60
- modules: [ 'node_modules', 'node_modules/apostrophe/node_modules' ]
69
+ modules: [
70
+ 'node_modules',
71
+ // 1. Allow webpack to find loaders from dependencies of any project level packages (pnpm),
72
+ // empty if not pnpm
73
+ ...[ ...pnpmModulesResolvePaths ],
74
+ // 2. Allow webpack to find loaders from core dependencies (pnpm), empty if not pnpm
75
+ ...pnpmModulePath,
76
+ // 3. npm related paths
77
+ 'node_modules/apostrophe/node_modules'
78
+ ]
61
79
  },
62
80
  resolve: {
63
81
  extensions: [ '*', '.js' ],
@@ -67,6 +85,12 @@ module.exports = ({
67
85
  },
68
86
  modules: [
69
87
  'node_modules',
88
+ // 1. Allow webpack to find imports from dependencies of any project level packages (pnpm),
89
+ // empty if not pnpm
90
+ ...[ ...pnpmModulesResolvePaths ],
91
+ // 2. Allow webpack to find imports from core dependencies (pnpm), empty if not pnpm
92
+ ...pnpmModulePath,
93
+ // 3. npm related paths
70
94
  `${apos.npmRootDir}/node_modules`,
71
95
  // Make sure core-js and regenerator-runtime can always be found, even
72
96
  // if npm didn't hoist them
@@ -1,6 +1,7 @@
1
1
  const _ = require('lodash');
2
2
  const cuid = require('cuid');
3
3
  const { SemanticAttributes } = require('@opentelemetry/semantic-conventions');
4
+ const { klona } = require('klona');
4
5
 
5
6
  // This module is responsible for managing all of the documents (apostrophe "docs")
6
7
  // in the `aposDocs` mongodb collection.
@@ -296,6 +297,148 @@ module.exports = {
296
297
  },
297
298
  methods(self) {
298
299
  return {
300
+ // `pairs` is an array of arrays, each containing an old _id
301
+ // and a new _id that should replace it.
302
+ //
303
+ // `aposDocId` is implicitly updated, `path` is updated if a page,
304
+ // and all references found in relationships are updated via reverse
305
+ // relationship id lookups, after which attachment references are updated.
306
+ // This is a slow operation, which is why this method should be called only
307
+ // by migrations and tasks that remedy an unexpected situation. _id is
308
+ // meant to be an immutable property, this method is a workaround
309
+ // for situations like a renamed locale or a replication bug fix.
310
+ //
311
+ // If `keep` is set to `'old'` the old document's content wins
312
+ // in the event of a conflict. If `keep` is set to `'new'` the
313
+ // new document's content wins in the event of a conflict.
314
+ // If `keep` is not set, a `conflict` error is thrown in the
315
+ // event of a conflict.
316
+
317
+ async changeDocIds(pairs, { keep } = {}) {
318
+ let renamed = 0;
319
+ let kept = 0;
320
+ // Get page paths up front so we can avoid multiple queries when working on path changes
321
+ const pages = await self.apos.doc.db.find({
322
+ path: { $exists: 1 },
323
+ slug: /^\//
324
+ }).project({
325
+ path: 1,
326
+ aposLastTargetId: 1
327
+ }).toArray();
328
+ for (const pair of pairs) {
329
+ const [ from, to ] = pair;
330
+ const existing = await self.apos.doc.db.findOne({ _id: from });
331
+ if (!existing) {
332
+ throw self.apos.error('notfound');
333
+ }
334
+ const replacement = klona(existing);
335
+ await self.apos.doc.db.removeOne({ _id: from });
336
+ const oldAposDocId = existing.aposDocId;
337
+ replacement._id = to;
338
+ const parts = to.split(':');
339
+ replacement.aposDocId = parts[0];
340
+ // Watch out for nonlocalized types, don't set aposLocale for them
341
+ if (parts.length > 1) {
342
+ replacement.aposLocale = parts.slice(1).join(':');
343
+ }
344
+ const isPage = self.apos.page.isPage(existing);
345
+ if (isPage) {
346
+ replacement.path = existing.path.replace(existing.aposDocId, replacement.aposDocId);
347
+ }
348
+ try {
349
+ await self.apos.doc.db.insertOne(replacement);
350
+ renamed++;
351
+ } catch (e) {
352
+ // First reinsert old doc to prevent content loss on new doc insert failure
353
+ await self.apos.doc.db.insertOne(existing);
354
+ if (!self.apos.doc.isUniqueError(e)) {
355
+ // We cannot fix this error
356
+ throw e;
357
+ }
358
+ const existingReplacement = await self.apos.doc.db.findOne({ _id: replacement._id });
359
+ if (!existingReplacement) {
360
+ // We don't know the cause of this error
361
+ throw e;
362
+ }
363
+ if (keep === 'new') {
364
+ // New content already exists in new locale, delete old locale
365
+ // and keep new
366
+ await self.apos.doc.db.removeOne({ _id: existing._id });
367
+ kept++;
368
+ } else if (keep === 'old') {
369
+ // We want to keep the old content, but with the new
370
+ // identifiers. Once again we need to remove the old doc first
371
+ // to cut down on conflicts
372
+ try {
373
+ await self.apos.doc.db.deleteOne({ _id: existing._id });
374
+ await self.apos.doc.db.deleteOne({ _id: replacement._id });
375
+ await self.apos.doc.db.insertOne(replacement);
376
+ renamed++;
377
+ } catch (e) {
378
+ // Reinsert old doc to prevent content loss on new doc insert failure
379
+ await self.apos.doc.db.insertOne(existing);
380
+ throw e;
381
+ }
382
+ kept++;
383
+ } else {
384
+ throw self.apos.error('conflict');
385
+ }
386
+ }
387
+ if (isPage) {
388
+ for (const page of pages) {
389
+ if (page.aposLastTargetId === from) {
390
+ await self.apos.doc.db.updateOne({
391
+ _id: page._id
392
+ }, {
393
+ $set: {
394
+ aposLastTargetId: to
395
+ }
396
+ });
397
+ }
398
+ if (page.path.includes(oldAposDocId)) {
399
+ await self.apos.doc.db.updateOne({
400
+ _id: page._id
401
+ }, {
402
+ $set: {
403
+ path: page.path.replace(oldAposDocId, replacement.aposDocId)
404
+ }
405
+ });
406
+ }
407
+ }
408
+ }
409
+ if (existing.relatedReverseIds?.length) {
410
+ const relatedDocs = await self.apos.doc.db.find({
411
+ aposDocId: { $in: existing.relatedReverseIds }
412
+ }).toArray();
413
+ for (const doc of relatedDocs) {
414
+ replaceId(doc, oldAposDocId, replacement.aposDocId);
415
+ await self.apos.doc.db.replaceOne({
416
+ _id: doc._id
417
+ }, doc);
418
+ }
419
+ }
420
+ }
421
+ await self.apos.attachment.recomputeAllDocReferences();
422
+ return {
423
+ renamed,
424
+ kept
425
+ };
426
+ function replaceId(obj, oldId, newId) {
427
+ if (obj == null) {
428
+ return;
429
+ }
430
+ if ((typeof obj) !== 'object') {
431
+ return;
432
+ }
433
+ for (const key of Object.keys(obj)) {
434
+ if (obj[key] === oldId) {
435
+ obj[key] = newId;
436
+ } else {
437
+ replaceId(obj[key], oldId, newId);
438
+ }
439
+ }
440
+ }
441
+ },
299
442
  async enableCollection() {
300
443
  self.db = await self.apos.db.collection('aposDocs');
301
444
  },
@@ -1142,6 +1285,7 @@ module.exports = {
1142
1285
  type: module.name
1143
1286
  });
1144
1287
  }
1288
+ self.replicateReached = true;
1145
1289
  // Include the criteria array in the event so that more entries can be pushed to it
1146
1290
  await self.emit('beforeReplicate', criteria);
1147
1291
  // We can skip the core work of this method if there is only one locale,
@@ -1303,6 +1447,11 @@ module.exports = {
1303
1447
  });
1304
1448
  },
1305
1449
 
1450
+ async bestAposDocId(criteria) {
1451
+ const existing = await self.apos.doc.db.findOne(criteria, { projection: { aposDocId: 1 } });
1452
+ return existing?.aposDocId || self.apos.util.generateId();
1453
+ },
1454
+
1306
1455
  ...require('./lib/legacy-migrations')(self)
1307
1456
  };
1308
1457
  }
@@ -193,6 +193,12 @@ module.exports = {
193
193
  if (!self.options.name) {
194
194
  self.options.name = self.__meta.name;
195
195
  }
196
+ if (self.options.singletonAuto) {
197
+ self.options.singleton = true;
198
+ }
199
+ if (self.options.replicate === undefined) {
200
+ self.options.replicate = self.options.localized && self.options.singletonAuto;
201
+ }
196
202
  self.name = self.options.name;
197
203
  // Each doc-type has an array of fields which will be updated
198
204
  // if the document is moved to the archive. In most cases 'slug'
@@ -1473,7 +1479,9 @@ module.exports = {
1473
1479
  browserOptions.schema = self.allowedSchema(req);
1474
1480
  browserOptions.localized = self.isLocalized();
1475
1481
  browserOptions.autopublish = self.options.autopublish;
1476
- browserOptions.previewDraft = self.isLocalized() && !browserOptions.autopublish && self.options.previewDraft;
1482
+ browserOptions.previewDraft = self.isLocalized() &&
1483
+ !browserOptions.autopublish &&
1484
+ self.options.previewDraft;
1477
1485
 
1478
1486
  return browserOptions;
1479
1487
  }
@@ -42,7 +42,9 @@ module.exports = {
42
42
  // Intentionally the same
43
43
  pluralLabel: 'apostrophe:globalDocLabel',
44
44
  searchable: false,
45
- singleton: true,
45
+ singletonAuto: {
46
+ slug: 'global'
47
+ },
46
48
  showPermissions: true,
47
49
  replicate: true
48
50
  },
@@ -77,23 +79,10 @@ module.exports = {
77
79
  },
78
80
  methods(self) {
79
81
  return {
80
- async insertIfMissing() {
81
- // Insert at startup
82
- const req = self.apos.task.getReq();
83
- const existing = await self.apos.doc.db.findOne({ slug: self.slug });
84
- if (!existing) {
85
- const _new = self.newInstance();
86
- Object.assign(_new, {
87
- slug: self.slug,
88
- type: self.name
89
- });
90
- await self.insert(req, _new);
91
- }
92
- },
93
82
  // Fetch and return the `global` doc object. You probably don't need to call this,
94
83
  // because middleware has already populated `req.data.global` for you.
95
84
  async findGlobal(req) {
96
- return self.find(req, { slug: self.slug }).permission(false).toObject();
85
+ return self.find(req, { type: self.name }).permission(false).toObject();
97
86
  },
98
87
  // Fetch the global doc and add it to `req.data` as `req.data.global`, if it
99
88
  // is not already present. If it is already present, skip the
@@ -376,13 +376,13 @@
376
376
  "richTextH4": "Heading 4 (H4)",
377
377
  "richTextHighlight": "Mark",
378
378
  "richTextHorizontalRule": "Horizontal Rule",
379
- "richTextInsertMenuHeading": "Insert...",
379
+ "richTextInsertMenuHeading": "Insert element...",
380
380
  "richTextItalic": "Italic",
381
381
  "richTextLink": "Link",
382
382
  "richTextOrderedList": "Ordered List",
383
383
  "richTextParagraph": "Paragraph (P)",
384
384
  "richTextPlaceholder": "Start Typing Here...",
385
- "richTextPlaceholderWithInsertMenu": "Start Typing Here or Press /...",
385
+ "richTextPlaceholderWithInsertMenu": "Start typing or press '/' for commands...",
386
386
  "richTextRedo": "Redo",
387
387
  "richTextStrikethrough": "Strike",
388
388
  "richTextStyleConfigWarning": "Misconfigured rich text style: label: {{ label }}, {{ tag }}",
@@ -399,6 +399,7 @@
399
399
  "saveDraftDescription": "Save as a draft to publish later.",
400
400
  "saveType": "Save {{ type }}",
401
401
  "savingDocument": "Saving document...",
402
+ "search": "Search",
402
403
  "searchDocType": "Search {{ type }}",
403
404
  "searchLabel": "Search Page",
404
405
  "searchLocales": "Search Locales",