apostrophe 3.4.1 → 3.8.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 (111) hide show
  1. package/.eslintrc +4 -0
  2. package/.scratch.md +2 -0
  3. package/CHANGELOG.md +114 -2
  4. package/README.md +1 -1
  5. package/deploy-test-count +1 -1
  6. package/index.js +125 -5
  7. package/lib/moog-require.js +41 -3
  8. package/lib/moog.js +20 -8
  9. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposAdminBarLocale.vue +42 -23
  10. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextBar.vue +30 -14
  11. package/modules/@apostrophecms/area/index.js +9 -0
  12. package/modules/@apostrophecms/area/lib/custom-tags/area.js +1 -1
  13. package/modules/@apostrophecms/area/lib/custom-tags/widget.js +1 -1
  14. package/modules/@apostrophecms/area/ui/apos/apps/AposAreas.js +3 -0
  15. package/modules/@apostrophecms/area/ui/apos/components/AposAreaWidget.vue +6 -6
  16. package/modules/@apostrophecms/asset/index.js +85 -21
  17. package/modules/@apostrophecms/asset/lib/globalIcons.js +2 -0
  18. package/modules/@apostrophecms/asset/lib/webpack/src/webpack.scss.js +5 -2
  19. package/modules/@apostrophecms/attachment/index.js +1 -0
  20. package/modules/@apostrophecms/db/index.js +5 -6
  21. package/modules/@apostrophecms/doc/index.js +13 -3
  22. package/modules/@apostrophecms/doc-type/index.js +24 -4
  23. package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocContextMenu.vue +13 -1
  24. package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocEditor.vue +3 -0
  25. package/modules/@apostrophecms/i18n/i18n/en.json +26 -6
  26. package/modules/@apostrophecms/i18n/i18n/es.json +382 -0
  27. package/modules/@apostrophecms/i18n/i18n/pt-BR.json +379 -0
  28. package/modules/@apostrophecms/i18n/i18n/sk.json +380 -0
  29. package/modules/@apostrophecms/i18n/index.js +10 -1
  30. package/modules/@apostrophecms/i18n/ui/apos/components/AposI18nLocalize.vue +153 -121
  31. package/modules/@apostrophecms/image/index.js +2 -1
  32. package/modules/@apostrophecms/image/ui/apos/components/AposMediaManager.vue +6 -3
  33. package/modules/@apostrophecms/image/ui/apos/components/AposMediaManagerEditor.vue +24 -13
  34. package/modules/@apostrophecms/image-widget/index.js +2 -1
  35. package/modules/@apostrophecms/image-widget/views/widget.html +12 -2
  36. package/modules/@apostrophecms/job/index.js +164 -212
  37. package/modules/@apostrophecms/login/index.js +36 -17
  38. package/modules/@apostrophecms/login/ui/apos/components/TheAposLogin.vue +8 -0
  39. package/modules/@apostrophecms/migration/index.js +1 -1
  40. package/modules/@apostrophecms/modal/ui/apos/components/AposDocsManagerToolbar.vue +151 -61
  41. package/modules/@apostrophecms/modal/ui/apos/components/AposModal.vue +6 -2
  42. package/modules/@apostrophecms/modal/ui/apos/components/AposModalConfirm.vue +9 -7
  43. package/modules/@apostrophecms/modal/ui/apos/mixins/AposDocsManagerMixin.js +12 -15
  44. package/modules/@apostrophecms/modal/ui/apos/mixins/AposEditorMixin.js +6 -0
  45. package/modules/@apostrophecms/module/index.js +1 -1
  46. package/modules/@apostrophecms/notification/index.js +116 -8
  47. package/modules/@apostrophecms/notification/ui/apos/components/AposNotification.vue +89 -11
  48. package/modules/@apostrophecms/notification/ui/apos/components/TheAposNotifications.vue +1 -1
  49. package/modules/@apostrophecms/page/index.js +37 -30
  50. package/modules/@apostrophecms/permission/index.js +1 -1
  51. package/modules/@apostrophecms/permission/ui/apos/components/AposInputRole.vue +4 -2
  52. package/modules/@apostrophecms/piece-type/index.js +178 -61
  53. package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManager.vue +179 -47
  54. package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManagerDisplay.vue +1 -3
  55. package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManagerSelectBox.vue +138 -0
  56. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue +42 -10
  57. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapStyles.vue +3 -0
  58. package/modules/@apostrophecms/rich-text-widget/ui/apos/tiptap-extensions/Classes.js +6 -10
  59. package/modules/@apostrophecms/rich-text-widget/ui/apos/tiptap-extensions/Default.js +64 -0
  60. package/modules/@apostrophecms/rich-text-widget/ui/apos/tiptap-extensions/Document.js +15 -0
  61. package/modules/@apostrophecms/rich-text-widget/ui/apos/tiptap-extensions/Heading.js +23 -0
  62. package/modules/@apostrophecms/schema/index.js +97 -20
  63. package/modules/@apostrophecms/schema/ui/apos/components/AposArrayEditor.vue +1 -0
  64. package/modules/@apostrophecms/schema/ui/apos/components/AposInputArray.vue +4 -1
  65. package/modules/@apostrophecms/schema/ui/apos/components/AposInputAttachment.vue +11 -160
  66. package/modules/@apostrophecms/schema/ui/apos/components/AposInputRadio.vue +8 -5
  67. package/modules/@apostrophecms/schema/ui/apos/components/AposInputRelationship.vue +24 -2
  68. package/modules/@apostrophecms/schema/ui/apos/components/AposInputSelect.vue +24 -6
  69. package/modules/@apostrophecms/schema/ui/apos/components/AposInputSlug.vue +0 -4
  70. package/modules/@apostrophecms/schema/ui/apos/components/AposInputString.vue +0 -7
  71. package/modules/@apostrophecms/schema/ui/apos/components/AposSchema.vue +25 -3
  72. package/modules/@apostrophecms/schema/ui/apos/mixins/AposInputMixin.js +10 -2
  73. package/modules/@apostrophecms/task/index.js +2 -2
  74. package/modules/@apostrophecms/template/index.js +63 -36
  75. package/modules/@apostrophecms/template/lib/custom-tags/component.js +1 -1
  76. package/modules/@apostrophecms/template/lib/custom-tags/render.js +6 -2
  77. package/modules/@apostrophecms/ui/index.js +6 -2
  78. package/modules/@apostrophecms/ui/ui/apos/components/AposButton.vue +21 -3
  79. package/modules/@apostrophecms/ui/ui/apos/components/AposCellContextMenu.vue +1 -1
  80. package/modules/@apostrophecms/ui/ui/apos/components/AposFile.vue +205 -0
  81. package/modules/@apostrophecms/ui/ui/apos/components/AposIndicator.vue +5 -0
  82. package/modules/@apostrophecms/ui/ui/apos/lib/i18next.js +16 -2
  83. package/modules/@apostrophecms/ui/ui/apos/scss/global/_tables.scss +4 -3
  84. package/modules/@apostrophecms/ui/ui/apos/scss/global/_theme.scss +3 -0
  85. package/modules/@apostrophecms/ui/ui/apos/scss/global/_widgets.scss +3 -0
  86. package/modules/@apostrophecms/ui/ui/apos/scss/global/import-all.scss +2 -1
  87. package/modules/@apostrophecms/user/index.js +21 -0
  88. package/modules/@apostrophecms/util/index.js +2 -2
  89. package/modules/@apostrophecms/util/ui/src/http.js +12 -8
  90. package/modules/@apostrophecms/util/ui/src/util.js +15 -0
  91. package/modules/@apostrophecms/widget-type/index.js +1 -1
  92. package/modules/@apostrophecms/widget-type/ui/apos/components/AposWidgetEditor.vue +1 -0
  93. package/modules/@apostrophecms/widget-type/ui/apos/mixins/AposWidgetMixin.js +15 -7
  94. package/package.json +4 -4
  95. package/test/extra_node_modules/improve-global/index.js +7 -0
  96. package/test/extra_node_modules/improve-piece-type/index.js +7 -0
  97. package/test/improve-overrides.js +30 -0
  98. package/test/job.js +224 -0
  99. package/test/login.js +183 -0
  100. package/test/modules/@apostrophecms/global/index.js +8 -0
  101. package/test/modules/fragment-all/views/aux-test.html +7 -0
  102. package/test/modules/fragment-all/views/fragment.html +5 -0
  103. package/test/moog.js +47 -0
  104. package/test/package.json +5 -4
  105. package/test/pieces.js +17 -0
  106. package/test/reverse-relationship.js +170 -0
  107. package/test/subdir-project/app.js +3 -0
  108. package/test/subdir-project.js +26 -0
  109. package/test/templates.js +7 -1
  110. package/test-lib/test.js +23 -12
  111. package/test-lib/util.js +33 -0
@@ -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) {
@@ -451,7 +463,11 @@ export default {
451
463
  }
452
464
  },
453
465
  async onContentChanged(e) {
454
- if (e.doc && (e.doc._id === this.context._id)) {
466
+
467
+ if (
468
+ (e.doc && (e.doc._id === this.context._id)) ||
469
+ (e.docIds && e.docIds.includes(this.context._id))
470
+ ) {
455
471
  if (e.action === 'delete') {
456
472
  if (!this.contextStack.length) {
457
473
  // With the current page gone, we need to move to safe ground
@@ -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) {
@@ -46,7 +46,7 @@ module.exports = function(self) {
46
46
  return { args };
47
47
  },
48
48
  async run(context, doc, name, _with) {
49
- const req = context.env.opts.req;
49
+ const req = context.ctx.__req;
50
50
  let area;
51
51
  if ((!doc) || ((typeof doc) !== 'object')) {
52
52
  throw usage('You must pass an existing doc or widget as the first argument.');
@@ -40,7 +40,7 @@ module.exports = function(self) {
40
40
  return { args };
41
41
  },
42
42
  async run(context, item, options, _with) {
43
- const req = context.env.opts.req;
43
+ const req = context.ctx.__req;
44
44
  if (!item) {
45
45
  self.apos.util.warn('a null widget was encountered.');
46
46
  return '';
@@ -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
  },
@@ -615,8 +615,8 @@ export default {
615
615
  }
616
616
 
617
617
  .apos-area-widget-wrapper--foreign .apos-area-widget-inner .apos-area-widget__breadcrumbs {
618
- background-color: var(--a-primary);
619
- & ::v-deep .apos-button {
618
+ background-color: var(--a-background-inverted);
619
+ & ::v-deep .apos-button__content {
620
620
  color: var(--a-text-inverted);
621
621
  }
622
622
  }
@@ -73,6 +73,12 @@ module.exports = {
73
73
  await fs.remove(buildDir);
74
74
  await fs.mkdirp(buildDir);
75
75
 
76
+ // Static asset files in `public` subdirs of each module are copied
77
+ // to the same relative path `/public/apos-frontend/namespace/modules/modulename`.
78
+ // Inherited files are also copied, with the deepest subclass overriding in the
79
+ // event of a conflict
80
+ await moduleOverrides(`${bundleDir}/modules`, 'public');
81
+
76
82
  for (const [ name, options ] of Object.entries(self.builds)) {
77
83
  // If the option is not present always rebuild everything
78
84
  let rebuild = argv && !argv['check-apos-build'];
@@ -122,12 +128,17 @@ module.exports = {
122
128
  // just `public` and `apos`) by examining those specified as
123
129
  // targets for the various builds
124
130
  const scenes = [ ...new Set(Object.values(self.builds).map(options => options.scenes).flat()) ];
125
- let bundles = [];
131
+ let deployFiles = [];
126
132
  for (const scene of scenes) {
127
- bundles = [ ...bundles, ...merge(scene) ];
133
+ deployFiles = [ ...deployFiles, ...merge(scene) ];
128
134
  }
129
-
130
- await deploy(bundles);
135
+ // enumerate public assets and include them in deployment if appropriate
136
+ const publicAssets = glob.sync('modules/**/*', {
137
+ cwd: bundleDir,
138
+ mark: true
139
+ }).filter(match => !match.endsWith('/'));
140
+ deployFiles = [ ...deployFiles, ...publicAssets ];
141
+ await deploy(deployFiles);
131
142
 
132
143
  if (process.env.APOS_BUNDLE_ANALYZER) {
133
144
  return new Promise((resolve, reject) => {
@@ -162,7 +173,7 @@ module.exports = {
162
173
  for (const name of names) {
163
174
  const moduleDir = `${modulesDir}/${name}`;
164
175
  for (const dir of directories[name]) {
165
- const srcDir = `${dir}/ui/${source}`;
176
+ const srcDir = `${dir}/${source}`;
166
177
  if (fs.existsSync(srcDir)) {
167
178
  await fs.copy(srcDir, moduleDir);
168
179
  }
@@ -176,7 +187,7 @@ module.exports = {
176
187
  }));
177
188
  const modulesDir = `${buildDir}/${name}/modules`;
178
189
  const source = options.source || name;
179
- await moduleOverrides(modulesDir, source);
190
+ await moduleOverrides(modulesDir, `ui/${source}`);
180
191
 
181
192
  let iconImports, componentImports, tiptapExtensionImports, appImports, indexJsImports, indexSassImports;
182
193
  if (options.apos) {
@@ -233,7 +244,8 @@ module.exports = {
233
244
  // Remove previous build artifacts, as some pipelines won't build all artifacts
234
245
  // if there is no input, and we don't want stale output in the bundle
235
246
  fs.removeSync(`${bundleDir}/${outputFilename}`);
236
- fs.removeSync(`${bundleDir}/${outputFilename}`.replace(/\.js$/, '.css'));
247
+ const cssPath = `${bundleDir}/${outputFilename}`.replace(/\.js$/, '.css');
248
+ fs.removeSync(cssPath);
237
249
  await Promise.promisify(webpackModule)(require(`./lib/webpack/${name}/webpack.config`)(
238
250
  {
239
251
  importFile,
@@ -243,6 +255,11 @@ module.exports = {
243
255
  },
244
256
  self.apos
245
257
  ));
258
+ if (fs.existsSync(cssPath)) {
259
+ fs.writeFileSync(cssPath, self.filterCss(fs.readFileSync(cssPath, 'utf8'), {
260
+ modulesPrefix: `${self.getAssetBaseUrl()}/modules`
261
+ }));
262
+ }
246
263
  if (options.apos) {
247
264
  const now = Date.now().toString();
248
265
  fs.writeFileSync(`${bundleDir}/${name}-build-timestamp.txt`, now);
@@ -269,7 +286,9 @@ module.exports = {
269
286
  const publicImports = getImports(name, '*.css', { });
270
287
  fs.writeFileSync(`${bundleDir}/${name}-build.css`,
271
288
  publicImports.paths.map(path => {
272
- return fs.readFileSync(path, 'utf8');
289
+ return self.filterCss(fs.readFileSync(path, 'utf8'), {
290
+ modulesPrefix: `${self.getAssetBaseUrl()}/modules`
291
+ });
273
292
  }).join('\n')
274
293
  );
275
294
  }
@@ -358,7 +377,19 @@ module.exports = {
358
377
  return [ jsModules, jsNoModules, css ];
359
378
  }
360
379
 
361
- async function deploy(bundles) {
380
+ // If NODE_ENV is production, this function will copy the given
381
+ // array of asset files from `${bundleDir}/${file}` to
382
+ // the same relative location in the appropriate release subdirectory in
383
+ // `/public/apos-frontend/releases`, or in `/apos-frontend/releases` in
384
+ // uploadfs if `APOS_UPLOADFS_ASSETS` is present.
385
+ //
386
+ // If NODE_ENV is not production this function does nothing and
387
+ // the assets are served directly from `/public/apos-frontend/${file}`.
388
+ //
389
+ // The namespace (e.g. default) should be part of each filename given.
390
+ // A leading slash should NOT be passed.
391
+
392
+ async function deploy(files) {
362
393
  if (process.env.NODE_ENV !== 'production') {
363
394
  return;
364
395
  }
@@ -373,17 +404,23 @@ module.exports = {
373
404
  } else {
374
405
  // The right choice with Docker if uploadfs is just the local filesystem
375
406
  // mapped to a volume (a Docker build step can't access that)
376
- copyIn = fs.copyFile;
407
+ copyIn = fsCopyIn;
377
408
  releaseDir = `${self.apos.rootDir}/public/apos-frontend/releases/${releaseId}/${namespace}`;
378
409
  await fs.mkdirp(releaseDir);
379
410
  }
380
- for (const bundle of bundles) {
381
- const src = `${bundleDir}/${bundle}`;
382
- await copyIn(src, `${releaseDir}/${bundle}`);
411
+ for (const file of files) {
412
+ const src = `${bundleDir}/${file}`;
413
+ await copyIn(src, `${releaseDir}/${file}`);
383
414
  await fs.remove(src);
384
415
  }
385
416
  }
386
417
 
418
+ async function fsCopyIn(from, to) {
419
+ const base = path.dirname(to);
420
+ await fs.mkdirp(base);
421
+ return fs.copyFile(from, to);
422
+ }
423
+
387
424
  function getImports(folder, pattern, options) {
388
425
  let components = [];
389
426
  const seen = {};
@@ -580,7 +617,14 @@ module.exports = {
580
617
  if (data) {
581
618
  document.body.removeAttribute('data-apos');
582
619
  }
583
- })();
620
+ if (window.apos.modules) {
621
+ for (const module of Object.values(window.apos.modules)) {
622
+ if (module.alias) {
623
+ window.apos[module.alias] = module;
624
+ }
625
+ }
626
+ }
627
+ })();
584
628
  `;
585
629
  self.builds = {
586
630
  src: {
@@ -632,13 +676,6 @@ module.exports = {
632
676
  prologue: stripIndent`
633
677
  import 'Modules/@apostrophecms/ui/scss/global/import-all.scss';
634
678
  import Vue from 'Modules/@apostrophecms/ui/lib/vue';
635
- if (window.apos.modules) {
636
- for (const module of Object.values(window.apos.modules)) {
637
- if (module.alias) {
638
- window.apos[module.alias] = module;
639
- }
640
- }
641
- }
642
679
  window.apos.bus = new Vue();
643
680
  `,
644
681
  // Load only in browsers that support ES6 modules
@@ -650,6 +687,30 @@ module.exports = {
650
687
  if (!self.options.es5) {
651
688
  delete self.builds['src-es5'];
652
689
  }
690
+ },
691
+ // Filter the given css performing any necessary transformations,
692
+ // such as support for the /modules path regardless of where
693
+ // static assets are actually deployed
694
+ filterCss(css, { modulesPrefix }) {
695
+ return self.filterCssUrls(css, url => {
696
+ if (url.startsWith('/modules')) {
697
+ return url.replace('/modules', modulesPrefix);
698
+ }
699
+ return url;
700
+ });
701
+ },
702
+ // Run all URLs in CSS through a filter function
703
+ filterCssUrls(css, filter) {
704
+ css = css.replace(/url\(([^'"].*?)\)/g, function(s, url) {
705
+ return 'url(' + filter(url) + ')';
706
+ });
707
+ css = css.replace(/url\("([^"]+?)"\)/g, function(s, url) {
708
+ return 'url("' + filter(url) + '")';
709
+ });
710
+ css = css.replace(/url\('([^']+?)'\)/g, function(s, url) {
711
+ return 'url(\'' + filter(url) + '\')';
712
+ });
713
+ return css;
653
714
  }
654
715
  };
655
716
  },
@@ -667,6 +728,9 @@ module.exports = {
667
728
  }
668
729
  const script = fs.readFileSync(path.join(__dirname, '/lib/refresh-on-restart.js'), 'utf8');
669
730
  return self.apos.template.safe(`<script data-apos-refresh-on-restart="${self.action}/restart-id">\n${script}</script>`);
731
+ },
732
+ url(path) {
733
+ return `${self.getAssetBaseUrl()}${path}`;
670
734
  }
671
735
  };
672
736
  },
@@ -56,9 +56,11 @@ module.exports = {
56
56
  'play-box-icon': 'PlayBox',
57
57
  'instagram-icon': 'Instagram',
58
58
  'label-icon': 'Label',
59
+ 'lightbulb-on-icon': 'LightbulbOn',
59
60
  'link-icon': 'Link',
60
61
  'list-status-icon': 'ListStatus',
61
62
  'lock-icon': 'Lock',
63
+ 'map-marker-icon': 'MapMarker',
62
64
  'magnify-icon': 'Magnify',
63
65
  'menu-down-icon': 'MenuDown',
64
66
  'minus-icon': 'Minus',
@@ -9,8 +9,11 @@ module.exports = (options, apos) => {
9
9
  use: [
10
10
  // Instead of style-loader, to avoid FOUC
11
11
  MiniCssExtractPlugin.loader,
12
- // Parses CSS imports
13
- 'css-loader',
12
+ // Parses CSS imports and make css-loader ignore urls. Urls will still be handled by webpack
13
+ {
14
+ loader: 'css-loader',
15
+ options: { url: false }
16
+ },
14
17
  // Provides autoprefixing
15
18
  {
16
19
  loader: 'postcss-loader',
@@ -1038,6 +1038,7 @@ module.exports = {
1038
1038
  action: self.action,
1039
1039
  fileGroups: self.fileGroups,
1040
1040
  name: self.name,
1041
+ // for bc
1041
1042
  uploadsUrl: self.uploadfs.getUrl(),
1042
1043
  croppable: self.croppable,
1043
1044
  sized: self.sized
@@ -1,4 +1,4 @@
1
- // This module establishes `apos.db`, the mongodb driver connection object.
1
+ // This module establishes `apos.db`, the MongoDB database object.
2
2
  //
3
3
  // ## Options
4
4
  //
@@ -20,8 +20,7 @@
20
20
  //
21
21
  // ### `client`
22
22
  //
23
- // An existing MongoDB connection (MongoClient) object. If present, a new
24
- // connection instance is created that reuses the same sockets,
23
+ // An existing MongoDB connection (MongoClient) object. If present, it is used
25
24
  // and `uri`, `host`, `connect`, etc. are ignored.
26
25
  //
27
26
  // ### `versionCheck`
@@ -40,7 +39,7 @@
40
39
  // allow other modules to drop related non-MongoDB resources at the
41
40
  // same time, if desired.
42
41
  //
43
- // Note that `apos.db` is the mongodb database object, not this module.
42
+ // Note that `apos.db` is the MongoDB database object, not this module.
44
43
  // You shouldn't need to talk to this module after startup, but you can
45
44
  // access it as `apos.modules['@apostrophecms/db']` if you wish. You can
46
45
  // also access `apos.dbClient` if you need the MongoClient object.
@@ -81,8 +80,8 @@ module.exports = {
81
80
  },
82
81
  methods(self) {
83
82
  return {
84
- // Open the database connection. Always use MongoClient with its
85
- // sensible defaults. Build a URI if we need to, so we can call it
83
+ // Open the database connection. Always uses MongoClient with its
84
+ // sensible defaults. Builds a URI if necessary, so we can call it
86
85
  // in a consistent way.
87
86
  //
88
87
  // One default we override: if the connection is lost, we keep
@@ -38,11 +38,19 @@ module.exports = {
38
38
  restApiRoutes(self) {
39
39
  return {
40
40
  // GET /api/v1/@apostrophecms/doc/_id supports only the universal query
41
- // features, but works for any document type, Simplifies browser-side
42
- // logic for redirects to foreign documents; the frontend only has to
41
+ // features, but works for any document type. Simplifies browser-side
42
+ // logic for redirects to foreign documents. The frontend only has to
43
43
  // know the doc _id.
44
+ //
45
+ // Since this API is solely for editing purposes you will receive
46
+ // a 404 if you request a document you cannot edit.
44
47
  async getOne(req, _id) {
45
- return self.find(req, { _id }).toObject();
48
+ _id = self.apos.i18n.inferIdLocaleAndMode(req, _id);
49
+ const doc = await self.find(req, { _id }).permission('edit').toObject();
50
+ if (!doc) {
51
+ throw self.apos.error('notfound');
52
+ }
53
+ return doc;
46
54
  }
47
55
  };
48
56
  },
@@ -431,6 +439,7 @@ module.exports = {
431
439
  await self.insertBody(req, doc, options);
432
440
  await m.emit('afterInsert', req, doc, options);
433
441
  await m.emit('afterSave', req, doc, options);
442
+ // TODO: Remove `afterLoad` in next major version. Deprecated.
434
443
  await m.emit('afterLoad', req, [ doc ]);
435
444
  return doc;
436
445
  },
@@ -466,6 +475,7 @@ module.exports = {
466
475
  await self.updateBody(req, doc, options);
467
476
  await m.emit('afterUpdate', req, doc, options);
468
477
  await m.emit('afterSave', req, doc, options);
478
+ // TODO: Remove `afterLoad` in next major version. Deprecated.
469
479
  await m.emit('afterLoad', req, [ doc ]);
470
480
  return doc;
471
481
  },
@@ -389,7 +389,7 @@ module.exports = {
389
389
  self.schema = self.apos.schema.compose({
390
390
  addFields: self.apos.schema.fieldsToArray(`Module ${self.__meta.name}`, self.fields),
391
391
  arrangeFields: self.apos.schema.groupsToArray(self.fieldsGroups)
392
- });
392
+ }, self);
393
393
  if (self.options.slugPrefix) {
394
394
  if (self.options.slugPrefix === 'deduplicate-') {
395
395
  const req = self.apos.task.getReq();
@@ -738,7 +738,7 @@ module.exports = {
738
738
  },
739
739
  // Localize (export) the given draft to another locale, creating the document in the
740
740
  // other locale if necessary. By default, if the document already exists in the
741
- // other locale, it is not ovewritten. Use the `update: true` option to change that.
741
+ // other locale, it is not overwritten. Use the `update: true` option to change that.
742
742
  // You can localize starting from either draft or published content. Either way what
743
743
  // gets created or updated in the other locale is a draft.
744
744
  async localize(req, draft, toLocale, options = { update: false }) {
@@ -1217,6 +1217,21 @@ module.exports = {
1217
1217
  // cursors.
1218
1218
 
1219
1219
  project: {
1220
+ launder (p) {
1221
+ // check that project is an object
1222
+ if (!p || typeof p !== 'object' || Array.isArray(p)) {
1223
+ return {};
1224
+ }
1225
+
1226
+ const projection = Object.entries(p).reduce((acc, [ key, val ]) => {
1227
+ return {
1228
+ ...acc,
1229
+ [key]: self.apos.launder.boolean(val)
1230
+ };
1231
+ }, {});
1232
+
1233
+ return projection;
1234
+ },
1220
1235
  finalize() {
1221
1236
  let projection = query.get('project') || {};
1222
1237
  // Keys beginning with `_` are computed values
@@ -1442,9 +1457,14 @@ module.exports = {
1442
1457
  attachments: {
1443
1458
  def: false,
1444
1459
  after(results) {
1445
- for (const doc of results) {
1446
- self.apos.attachment.all(doc, { annotate: true });
1460
+ const attachments = query.get('attachments');
1461
+
1462
+ if (attachments) {
1463
+ self.apos.attachment.all(results, { annotate: true });
1447
1464
  }
1465
+ },
1466
+ launder(b) {
1467
+ return self.apos.launder.boolean(b);
1448
1468
  }
1449
1469
  },
1450
1470
 
@@ -286,9 +286,21 @@ export default {
286
286
  apos.bus.$off('content-changed', this.onContentChanged);
287
287
  },
288
288
  methods: {
289
- onContentChanged(e) {
289
+ async onContentChanged(e) {
290
290
  if (e.doc && (e.doc._id === this.context._id)) {
291
291
  this.context = e.doc;
292
+ } else if (e.docIds && e.docIds.includes(this.context._id)) {
293
+ try {
294
+ this.context = await apos.http.get(`${this.moduleOptions.action}/${this.context._id}`, {
295
+ busy: true
296
+ });
297
+ } catch (error) {
298
+ // If not found it is likely that there was an archiving or restoring
299
+ // batch operation.
300
+ if (error.name !== 'notfound') {
301
+ console.error(error);
302
+ }
303
+ }
292
304
  }
293
305
  },
294
306
  menuHandler(action) {
@@ -68,6 +68,7 @@
68
68
  :doc-id="docId"
69
69
  :value="docFields"
70
70
  @input="updateDocFields"
71
+ @validate="triggerValidate"
71
72
  :server-errors="serverErrors"
72
73
  :ref="tab.name"
73
74
  />
@@ -90,6 +91,7 @@
90
91
  :doc-id="docId"
91
92
  :value="docFields"
92
93
  @input="updateDocFields"
94
+ @validate="triggerValidate"
93
95
  :modifiers="['small', 'inverted']"
94
96
  ref="utilitySchema"
95
97
  :server-errors="serverErrors"
@@ -444,6 +446,7 @@ export default {
444
446
  if (!this.errorCount) {
445
447
  this[action]();
446
448
  } else {
449
+ this.triggerValidation = false;
447
450
  await apos.notify('apostrophe:resolveErrorsBeforeSaving', {
448
451
  type: 'warning',
449
452
  icon: 'alert-circle-icon',