apostrophe 3.6.0 → 3.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.7.0 - 2021-10-28
4
+
5
+ ### Adds
6
+
7
+ * Schema select field choices can now be populated by a server side function, like an API call. Set the `choices` property to a method name of the calling module. That function should take a single argument of `req`, and return an array of objects with `label` and `value` properties. The function can be async and will be awaited.
8
+
9
+ * Apostrophe now has built-in support for the Node.js cluster module. If the `APOS_CLUSTER_PROCESSES` environment variable is set to a number, that number of child processes are forked, sharing the same listening port. If the variable is set to `0`, one process is forked for each CPU core, with a minimum of `2` to provide availability during restarts. If the variable is set to a negative number, that number is added to the number of CPU cores, e.g. `-1` is a good way to reserve one core for MongoDB if it is running on the same server. This is for production use only (`NODE_ENV=production`). If a child process fails it is restarted automatically.
10
+
11
+ ### Fixes
12
+
13
+ * Prevents double-escaping interpolated localization strings in the UI.
14
+ * Rich text editor style labels are now run through a localization method to get the translated strings from their l10n keys.
15
+ * Fixes README Node version requirement (Node 12+).
16
+ * The text alignment buttons now work immediately in a new rich text widget. Previously they worked only after manually setting a style or refreshing the page. Thanks to Michelin for their support of this fix.
17
+ * Users can now activate the built-in date and time editing popups of modern browsers when using the `date` and `time` schema field types.
18
+ * Developers can now `require` their project `app.js` file in the Node.js REPL for debugging and inspection. Thanks to [Matthew Francis Brunetti](https://github.com/zenflow).
19
+ * If a static text phrase is unavailable in both the current locale and the default locale, Apostrophe will always fall back to the `en` locale as a last resort, which ensures the admin UI works if it has not been translated.
20
+ * Developers can now `require` their project `app.js` in the Node.js REPL for debugging and inspection
21
+ * Ensure array field items have valid _id prop before storing. Thanks to Thanks to [Matthew Francis Brunetti](https://github.com/zenflow).
22
+
23
+ ### Changes
24
+
25
+ * In 3.x, `relationship` fields have an optional `builders` property, which replaces `filters` from 2.x, and within that an optional `project` property, which replaces `projection` from 2.x (to match MongoDB's `cursor.project`). Prior to this release leaving the old syntax in place could lead to severe performance problems due to a lack of projections. Starting with this release the 2.x syntax results in an error at startup to help the developer correct their code.
26
+ * The `className` option from the widget options in a rich text area field is now also applied to the rich text editor itself, for a consistently WYSIWYG appearance when editing and when viewing. Thanks to [Max Mulatz](https://github.com/klappradla) for this contribution.
27
+ * Adds deprecation notes to doc module `afterLoad` events, which are deprecated.
28
+ * Removes unused `afterLogin` method in the login module.
29
+
3
30
  ## 3.6.0 - 2021-10-13
4
31
 
5
32
  ### Adds
@@ -8,7 +35,8 @@
8
35
  * Adds 'no-search' modifier to relationship fields as a UI simplification option.
9
36
  * Fields can now have their own `modifiers` array. This is combined with the schema modifiers, allowing for finer grained control of field rendering.
10
37
  * Adds a Slovak localization file. Activate the `sk` locale to use this. Many thanks to [Michael Huna](https://github.com/Miselrkba) for the contribution.
11
- * Adds a Spanish localization file. Activate the `es` locale to use this. Many thanks to [egonzalezg9](https://github.com/egonzalezg9) for the contribution.
38
+ * Adds a Spanish localization file. Activate the `es` locale to use this. Many thanks to [Eugenio Gonzalez](https://github.com/egonzalezg9) for the contribution.
39
+ * Adds a Brazilian Portuguese localization file. Activate the `pt-BR` locale to use this. Many thanks to [Pietro Rutzen](https://github.com/pietro-rutzen) for the contribution.
12
40
 
13
41
  ### Fixes
14
42
 
package/README.md CHANGED
@@ -43,7 +43,7 @@ We recommend installing the following with [Homebrew](https://brew.sh/) on macOS
43
43
 
44
44
  | Software | Minimum Version | Notes
45
45
  | ------------- | ------------- | -----
46
- | Node.js | 10.x | Or better
46
+ | Node.js | 12.x | Or better
47
47
  | npm | 6.x | Or better
48
48
  | MongoDB | 3.6 | Or better
49
49
  | Imagemagick | Any | Faster image uploads, GIF support (optional)
package/index.js CHANGED
@@ -2,11 +2,43 @@ const path = require('path');
2
2
  const _ = require('lodash');
3
3
  const argv = require('boring')({ end: true });
4
4
  const fs = require('fs');
5
+ const { stripIndent } = require('common-tags');
6
+ const cluster = require('cluster');
7
+ const { cpus } = require('os');
8
+ const process = require('process');
5
9
  const npmResolve = require('resolve');
10
+
6
11
  let defaults = require('./defaults.js');
7
- const { stripIndent } = require('common-tags');
8
12
 
9
- // **Awaiting the Apostrophe function is optional**
13
+ // ## Top-level options
14
+ //
15
+ // `cluster`
16
+ //
17
+ // If set to `true`, Apostrophe will spawn as many processes as
18
+ // there are CPU cores on the server, or a minimum of 2, and balance
19
+ // incoming connections among them. This ensures availability while one
20
+ // process is restarting due to a crash and also increases scalability if
21
+ // the server has multiple CPU cores.
22
+ //
23
+ // If set to an object with a `processes` property, that many
24
+ // processes are started. If `processes` is 0 or a negative number,
25
+ // it is added to the number of CPU cores reported by the server.
26
+ // Notably, `-1` can be a good way to reserve one CPU core for MongoDB
27
+ // in a single-server deployment.
28
+ //
29
+ // However when in cluster mode no fewer than 2 processes will be
30
+ // started as there is no availability benefit without at least 2.
31
+ //
32
+ // If a child process exits with a failure status code it will be
33
+ // restarted. However, if it exits in less than 20 seconds after
34
+ // startup there will be a 20 second delay to avoid flooding logs
35
+ // and pinning the CPU.
36
+ //
37
+ // Alternatively the `APOS_CLUSTER_PROCESSES` environment variable
38
+ // can be set to a number, which will effectively set the cluster
39
+ // option to `cluster: { processes: n }`.
40
+ //
41
+ // ## Awaiting the Apostrophe function
10
42
  //
11
43
  // The apos function is async, but in typical cases you do not
12
44
  // need to await it. If you simply call it, Apostrophe will
@@ -21,8 +53,63 @@ const { stripIndent } = require('common-tags');
21
53
  // To avoid exiting on errors, pass the `exit: false` option.
22
54
  // This can option also can be used to allow awaiting a command line
23
55
  // task, as they also normally exit on completion.
56
+ //
57
+ // If `options.cluster` is truthy, the function quickly resolves to
58
+ // `null` in the primary process. In the child process it resolves as
59
+ // documented above.
24
60
 
25
61
  module.exports = async function(options) {
62
+ const guardTime = 20000;
63
+ if (process.env.APOS_CLUSTER_PROCESSES) {
64
+ options.cluster = {
65
+ processes: parseInt(process.env.APOS_CLUSTER_PROCESSES)
66
+ };
67
+ }
68
+ if (options.cluster && (process.env.NODE_ENV !== 'production')) {
69
+ console.log('NODE_ENV is not set to production, disabling cluster mode');
70
+ options.cluster = false;
71
+ }
72
+ if (options.cluster && !argv._.length) {
73
+ // For bc with node 14 and below we need to check both
74
+ if (cluster.isPrimary || cluster.isMaster) {
75
+ let processes = options.cluster.processes || cpus().length;
76
+ if (processes <= 0) {
77
+ processes = cpus().length + processes;
78
+ }
79
+ let capped = '';
80
+ if (processes > cpus().length) {
81
+ processes = cpus().length;
82
+ capped = ' (capped to number of CPU cores)';
83
+ }
84
+ if (processes < 2) {
85
+ processes = 2;
86
+ if (capped) {
87
+ capped = ' (less than 2 cores, capped to minimum of 2)';
88
+ } else {
89
+ capped = ' (using minimum of 2)';
90
+ }
91
+ }
92
+ console.log(`Starting ${processes} cluster child processes${capped}`);
93
+ for (let i = 0; i < processes; i++) {
94
+ clusterFork();
95
+ }
96
+ cluster.on('exit', (worker, code, signal) => {
97
+ if (code !== 0) {
98
+ if ((Date.now() - worker.bornAt) < guardTime) {
99
+ console.error(`Worker process ${worker.process.pid} failed in ${seconds(Date.now() - worker.bornAt)}, waiting ${seconds(guardTime)} before restart`);
100
+ setTimeout(() => {
101
+ respawn(worker);
102
+ }, guardTime);
103
+ } else {
104
+ respawn(worker);
105
+ }
106
+ }
107
+ });
108
+ return null;
109
+ } else {
110
+ console.log(`Cluster worker ${process.pid} started`);
111
+ }
112
+ }
26
113
 
27
114
  // The core is not a true moog object but it must look enough like one
28
115
  // to participate as an async event emitter
@@ -174,7 +261,7 @@ module.exports = async function(options) {
174
261
  function getRoot() {
175
262
  let _module = module;
176
263
  let m = _module;
177
- while (m.parent) {
264
+ while (m.parent && m.parent.filename) {
178
265
  // The test file is the root as far as we are concerned,
179
266
  // not mocha itself
180
267
  if (m.parent.filename.match(/\/node_modules\/mocha\//)) {
@@ -518,3 +605,17 @@ module.exports.bundle = {
518
605
  modules: abstractClasses.concat(_.keys(defaults.modules)),
519
606
  directory: 'modules'
520
607
  };
608
+
609
+ function seconds(msec) {
610
+ return (Math.round(msec / 100) / 10) + ' seconds';
611
+ }
612
+
613
+ function clusterFork() {
614
+ const worker = cluster.fork();
615
+ worker.bornAt = Date.now();
616
+ }
617
+
618
+ function respawn(worker) {
619
+ console.error(`Respawning worker process ${worker.process.pid}`);
620
+ clusterFork();
621
+ }
@@ -439,6 +439,7 @@ module.exports = {
439
439
  await self.insertBody(req, doc, options);
440
440
  await m.emit('afterInsert', req, doc, options);
441
441
  await m.emit('afterSave', req, doc, options);
442
+ // TODO: Remove `afterLoad` in next major version. Deprecated.
442
443
  await m.emit('afterLoad', req, [ doc ]);
443
444
  return doc;
444
445
  },
@@ -474,6 +475,7 @@ module.exports = {
474
475
  await self.updateBody(req, doc, options);
475
476
  await m.emit('afterUpdate', req, doc, options);
476
477
  await m.emit('afterSave', req, doc, options);
478
+ // TODO: Remove `afterLoad` in next major version. Deprecated.
477
479
  await m.emit('afterLoad', req, [ doc ]);
478
480
  return doc;
479
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();
@@ -51,9 +51,14 @@ module.exports = {
51
51
  throw self.apos.error('invalid', `Locale prefixes must not contain more than one forward slash ("/").\nUse hyphens as separators. Check locale "${key}".`);
52
52
  }
53
53
  }
54
+ const fallbackLng = [ self.defaultLocale ];
55
+ // In case the default locale also has inadequate admin UI phrases
56
+ if (fallbackLng[0] !== 'en') {
57
+ fallbackLng.push('en');
58
+ }
54
59
  // Make sure we have our own instance to avoid conflicts with other apos objects
55
60
  self.i18next = i18next.createInstance({
56
- fallbackLng: self.defaultLocale,
61
+ fallbackLng,
57
62
  // Required to prevent the debugger from complaining
58
63
  languages: Object.keys(self.locales),
59
64
  // Added later, but required here
@@ -459,6 +464,10 @@ module.exports = {
459
464
  if (req.locale !== self.defaultLocale) {
460
465
  i18n[self.defaultLocale] = self.getBrowserBundles(self.defaultLocale);
461
466
  }
467
+ // In case the default locale also has inadequate admin UI phrases
468
+ if (!i18n.en) {
469
+ i18n.en = self.getBrowserBundles('en');
470
+ }
462
471
  const result = {
463
472
  i18n,
464
473
  locale: req.locale,
@@ -365,21 +365,6 @@ module.exports = {
365
365
  return 1000 * 60 * 60 * (self.options.passwordResetHours || 48);
366
366
  },
367
367
 
368
- // Invoked by passport after an authentication strategy succeeds
369
- // and the user has been logged in. Invokes `loginAfterLogin` on
370
- // any modules that have one and redirects to `req.redirect` or,
371
- // if it is not set, to `/`.
372
-
373
- async afterLogin(req, res) {
374
- try {
375
- await self.emit('after', req);
376
- } catch (e) {
377
- self.apos.util.error(e);
378
- return res.redirect('/');
379
- }
380
- return res.redirect(req.redirect || '/');
381
- },
382
-
383
368
  getBrowserData(req) {
384
369
  return {
385
370
  action: self.action,
@@ -260,7 +260,7 @@ module.exports = {
260
260
  // Intentionally emitted regardless of whether the site is new or not.
261
261
  //
262
262
  // This is the right time to park pages, for instance, because the
263
- // database is guaranteed to be in a sane state, whether because the
263
+ // database is guaranteed to be in a stable state, whether because the
264
264
  // site is new or because migrations ran successfully.
265
265
  await self.emit('after');
266
266
  } finally {
@@ -26,7 +26,7 @@
26
26
  <th class="apos-table__header" key="contextMenu">
27
27
  <component
28
28
  :is="getEl({})"
29
- class="apos-table__header-label is-hidden"
29
+ class="apos-table__header-label apos-is-hidden"
30
30
  >
31
31
  {{ $t('apostrophe:moreOperations') }}
32
32
  </component>
@@ -26,7 +26,7 @@
26
26
  </AposContextMenuDialog>
27
27
  </bubble-menu>
28
28
  <div class="apos-rich-text-editor__editor" :class="editorModifiers">
29
- <editor-content :editor="editor" :class="moduleOptions.className" />
29
+ <editor-content :editor="editor" :class="editorOptions.className" />
30
30
  </div>
31
31
  <div class="apos-rich-text-editor__editor_after" :class="editorModifiers">
32
32
  {{ $t('apostrophe:emptyRichTextWidget') }}
@@ -102,11 +102,27 @@ export default {
102
102
 
103
103
  activeOptions.styles = this.enhanceStyles(activeOptions.styles || this.defaultOptions.styles);
104
104
 
105
+ activeOptions.className = (activeOptions.className !== undefined)
106
+ ? activeOptions.className : this.moduleOptions.className;
107
+
105
108
  return activeOptions;
106
109
  },
107
-
110
+ autofocus() {
111
+ // Only true for a new rich text widget
112
+ return !this.stripPlaceholderBrs(this.value.content).length;
113
+ },
108
114
  initialContent() {
109
- return this.stripPlaceholderBrs(this.value.content);
115
+ const content = this.stripPlaceholderBrs(this.value.content);
116
+ if (!content.length) {
117
+ // If we don't supply a valid instance of the first style, then
118
+ // the text align control will not work until the user manually
119
+ // applies a style or refreshes the page
120
+ const defaultStyle = this.editorOptions.styles.find(style => style.def);
121
+ const _class = defaultStyle.class ? ` class="${defaultStyle.class}"` : '';
122
+ return `<${defaultStyle.tag}${_class}></${defaultStyle.tag}>`;
123
+ } else {
124
+ return content;
125
+ }
110
126
  },
111
127
  toolbar() {
112
128
  return this.editorOptions.toolbar;
@@ -135,7 +151,7 @@ export default {
135
151
  aposTiptapExtensions() {
136
152
  return (apos.tiptapExtensions || [])
137
153
  .map(extension => extension({
138
- styles: this.editorOptions.styles,
154
+ styles: this.editorOptions.styles.map(this.localizeStyle),
139
155
  types: this.tiptapTypes
140
156
  }));
141
157
  }
@@ -152,7 +168,7 @@ export default {
152
168
  mounted() {
153
169
  this.editor = new Editor({
154
170
  content: this.initialContent,
155
- autofocus: !this.initialContent,
171
+ autofocus: this.autofocus,
156
172
  onUpdate: this.editorUpdate,
157
173
  extensions: [
158
174
  StarterKit,
@@ -265,6 +281,14 @@ export default {
265
281
  }
266
282
  }
267
283
  return styles;
284
+ },
285
+ localizeStyle(style) {
286
+ style.label = this.$t(style.label);
287
+
288
+ return {
289
+ ...style,
290
+ label: this.$t(style.label)
291
+ };
268
292
  }
269
293
  }
270
294
  };
@@ -18,6 +18,7 @@ const _ = require('lodash');
18
18
  const dayjs = require('dayjs');
19
19
  const tinycolor = require('tinycolor2');
20
20
  const { klona } = require('klona');
21
+ const { stripIndent } = require('common-tags');
21
22
 
22
23
  module.exports = {
23
24
  options: {
@@ -323,7 +324,13 @@ module.exports = {
323
324
  self.addFieldType({
324
325
  name: 'select',
325
326
  async convert(req, field, data, destination) {
326
- destination[field.name] = self.apos.launder.select(data[field.name], field.choices, field.def);
327
+ let choices;
328
+ if ((typeof field.choices) === 'string') {
329
+ choices = await self.apos.modules[field.moduleName][field.choices](req);
330
+ } else {
331
+ choices = field.choices;
332
+ }
333
+ destination[field.name] = self.apos.launder.select(data[field.name], choices, field.def);
327
334
  },
328
335
  index: function (value, field, texts) {
329
336
  const silent = field.silent === undefined ? true : field.silent;
@@ -354,7 +361,9 @@ module.exports = {
354
361
  return self.apos.launder.select(v, field.choices, null);
355
362
  });
356
363
  } else {
357
- value = self.apos.launder.select(value, field.choices, null);
364
+ value = (typeof field.choices) === 'string'
365
+ ? self.apos.launder.string(value)
366
+ : self.apos.launder.select(value, field.choices, null);
358
367
  if (value === null) {
359
368
  return null;
360
369
  }
@@ -362,9 +371,16 @@ module.exports = {
362
371
  }
363
372
  },
364
373
  choices: async function () {
374
+ let allChoices;
365
375
  const values = await query.toDistinct(field.name);
376
+ if ((typeof field.choices) === 'string') {
377
+ const req = self.apos.task.getReq();
378
+ allChoices = await self.apos.modules[field.moduleName][field.choices](req);
379
+ } else {
380
+ allChoices = field.choices;
381
+ }
366
382
  const choices = _.map(values, function (value) {
367
- const choice = _.find(field.choices, { value: value });
383
+ const choice = _.find(allChoices, { value: value });
368
384
  return {
369
385
  value: value,
370
386
  label: choice && (choice.label || value)
@@ -980,6 +996,12 @@ module.exports = {
980
996
  if (field.schema && !Array.isArray(field.schema)) {
981
997
  fail('schema property should be an array if present at this stage');
982
998
  }
999
+ if (field.filters) {
1000
+ fail('"filters" property should be changed to "builders" for 3.x');
1001
+ }
1002
+ if (field.builders && field.builders.projection) {
1003
+ fail('"projection" sub-property should be changed to "project" for 3.x');
1004
+ }
983
1005
  function lintType(type) {
984
1006
  type = self.apos.doc.normalizeType(type);
985
1007
  if (!_.find(self.apos.doc.managers, { name: type })) {
@@ -1102,7 +1124,7 @@ module.exports = {
1102
1124
  // alterFields option should be avoided if your needs can be met
1103
1125
  // via another option.
1104
1126
 
1105
- compose(options) {
1127
+ compose(options, module) {
1106
1128
  let schema = [];
1107
1129
 
1108
1130
  // Useful for finding good unit test cases
@@ -1292,9 +1314,29 @@ module.exports = {
1292
1314
  // like workflow to patch schema fields of various modules
1293
1315
  // without inadvertently impacting other apos instances
1294
1316
  // when running with @apostrophecms/multisite
1295
- return _.map(schema, function (field) {
1317
+ schema = _.map(schema, function (field) {
1296
1318
  return _.clone(field);
1297
1319
  });
1320
+
1321
+ _.each(schema, function(field) {
1322
+ // For use in resolving options like "choices" when they
1323
+ // contain a method name. For bc don't mess with possible
1324
+ // existing usages in custom schema field types predating
1325
+ // this feature
1326
+ self.setModuleName(field, module);
1327
+ });
1328
+ return schema;
1329
+ },
1330
+
1331
+ // Recursively set moduleName property of the field and any subfields,
1332
+ // as might be found in array or object fields. `module` is an actual module
1333
+ setModuleName(field, module) {
1334
+ field.moduleName = field.moduleName || (module && module.__meta.name);
1335
+ if ((field.type === 'array') || (field.type === 'object')) {
1336
+ _.each(field.schema || [], function(subfield) {
1337
+ self.setModuleName(subfield, module);
1338
+ });
1339
+ }
1298
1340
  },
1299
1341
 
1300
1342
  // refine is like compose, but it starts with an existing schema array
@@ -1930,6 +1972,7 @@ module.exports = {
1930
1972
  } else if (field.type === 'array') {
1931
1973
  if (doc[field.name]) {
1932
1974
  doc[field.name].forEach(item => {
1975
+ item._id = item._id || self.apos.util.generateId();
1933
1976
  item.metaType = 'arrayItem';
1934
1977
  item.scopedArrayName = field.scopedArrayName;
1935
1978
  forSchema(field.schema, item);
@@ -2216,7 +2259,12 @@ module.exports = {
2216
2259
  self.apos.util.error(format(s));
2217
2260
  }
2218
2261
  function format(s) {
2219
- return '\n' + options.type + ' ' + options.subtype + ', field name ' + field.name + ':\n\n' + s + '\n';
2262
+ return stripIndent`
2263
+ ${options.type} ${options.subtype}, ${field.type} field "${field.name}":
2264
+
2265
+ ${s}
2266
+
2267
+ `;
2220
2268
  }
2221
2269
  },
2222
2270
 
@@ -2529,6 +2577,32 @@ module.exports = {
2529
2577
 
2530
2578
  };
2531
2579
  },
2580
+ apiRoutes(self) {
2581
+ return {
2582
+ get: {
2583
+ async choices(req) {
2584
+ const id = self.apos.launder.string(req.query.fieldId);
2585
+ const field = self.getFieldById(id);
2586
+ let choices = [];
2587
+ if (
2588
+ !field ||
2589
+ field.type !== 'select' ||
2590
+ !(field.choices && typeof field.choices === 'string')
2591
+ ) {
2592
+ throw self.apos.error('invalid');
2593
+ }
2594
+ choices = await self.apos.modules[field.moduleName][field.choices](req);
2595
+ if (Array.isArray(choices)) {
2596
+ return {
2597
+ choices
2598
+ };
2599
+ } else {
2600
+ throw self.apos.error('invalid', `The method ${field.choices} from the module ${field.moduleName} did not return an array`);
2601
+ }
2602
+ }
2603
+ }
2604
+ };
2605
+ },
2532
2606
  extendMethods(self) {
2533
2607
  return {
2534
2608
  getBrowserData(_super, req) {
@@ -2543,7 +2617,7 @@ module.exports = {
2543
2617
  }
2544
2618
  fields[name] = component;
2545
2619
  }
2546
-
2620
+ browserOptions.action = self.action;
2547
2621
  browserOptions.components = { fields: fields };
2548
2622
  return browserOptions;
2549
2623
  }
@@ -49,9 +49,27 @@ export default {
49
49
  choices: []
50
50
  };
51
51
  },
52
- mounted() {
52
+ async mounted() {
53
+ let choices;
54
+ if (typeof this.field.choices === 'string') {
55
+ const action = this.options.action;
56
+ const response = await apos.http.get(
57
+ `${action}/choices`,
58
+ {
59
+ qs: {
60
+ fieldId: this.field._id
61
+ },
62
+ busy: true
63
+ }
64
+ );
65
+ if (response.choices) {
66
+ choices = response.choices;
67
+ }
68
+ } else {
69
+ choices = this.field.choices;
70
+ }
53
71
  // Add an null option if there isn't one already
54
- if (!this.field.required && !this.field.choices.find(choice => {
72
+ if (!this.field.required && !choices.find(choice => {
55
73
  return choice.value === null;
56
74
  })) {
57
75
  this.choices.push({
@@ -59,12 +77,12 @@ export default {
59
77
  value: null
60
78
  });
61
79
  }
62
- this.choices = this.choices.concat(this.field.choices);
80
+ this.choices = this.choices.concat(choices);
63
81
  this.$nextTick(() => {
64
82
  // this has to happen on nextTick to avoid emitting before schemaReady is
65
83
  // set in AposSchema
66
- if (this.field.required && (this.next == null) && (this.field.choices[0] != null)) {
67
- this.next = this.field.choices[0].value;
84
+ if (this.field.required && (this.next == null) && (this.choices[0] != null)) {
85
+ this.next = this.choices[0].value;
68
86
  }
69
87
  });
70
88
  },
@@ -74,7 +92,7 @@ export default {
74
92
  return 'required';
75
93
  }
76
94
 
77
- if (value && !this.field.choices.find(choice => choice.value === value)) {
95
+ if (value && !this.choices.find(choice => choice.value === value)) {
78
96
  return 'invalid';
79
97
  }
80
98
 
@@ -73,10 +73,6 @@ export default {
73
73
  icon () {
74
74
  if (this.error) {
75
75
  return 'circle-medium-icon';
76
- } else if (this.field.type === 'date') {
77
- return 'calendar-icon';
78
- } else if (this.field.type === 'time') {
79
- return 'clock-icon';
80
76
  } else if (this.field.icon) {
81
77
  return this.field.icon;
82
78
  } else {
@@ -71,10 +71,6 @@ export default {
71
71
  icon () {
72
72
  if (this.error) {
73
73
  return 'circle-medium-icon';
74
- } else if (this.field.type === 'date') {
75
- return 'calendar-icon';
76
- } else if (this.field.type === 'time') {
77
- return 'clock-icon';
78
74
  } else if (this.field.icon) {
79
75
  return this.field.icon;
80
76
  } else {
@@ -207,9 +203,6 @@ export default {
207
203
  // height of date/time input is slightly larger than others due to the browser spinner ui
208
204
  height: 46px;
209
205
  padding-right: 40px;
210
- &::-webkit-calendar-picker-indicator {
211
- background: none;
212
- }
213
206
  }
214
207
  .apos-input--date {
215
208
  &::-webkit-clear-button {
@@ -64,7 +64,7 @@ export default {
64
64
  .apos-table__cell-field--context-menu__content {
65
65
  @include apos-transition();
66
66
  display: inline-block;
67
- opacity: 0;
67
+ opacity: 0.3;
68
68
  &.apos-is-visible {
69
69
  opacity: 1;
70
70
  }
@@ -10,11 +10,20 @@ export default {
10
10
  install(Vue, options) {
11
11
  const i18n = options.i18n;
12
12
 
13
+ const fallbackLng = [ i18n.defaultLocale ];
14
+ // In case the default locale also has inadequate admin UI phrases
15
+ if (fallbackLng[0] !== 'en') {
16
+ fallbackLng.push('en');
17
+ }
18
+
13
19
  i18next.init({
14
20
  lng: i18n.locale,
15
- fallbackLng: i18n.defaultLocale,
21
+ fallbackLng,
16
22
  resources: {},
17
- debug: i18n.debug
23
+ debug: i18n.debug,
24
+ interpolation: {
25
+ escapeValue: false
26
+ }
18
27
  });
19
28
 
20
29
  for (const [ ns, phrases ] of Object.entries(i18n.i18n[i18n.locale])) {
@@ -25,6 +34,11 @@ export default {
25
34
  i18next.addResourceBundle(i18n.defaultLocale, ns, phrases, true, true);
26
35
  }
27
36
  }
37
+ if ((i18n.locale !== 'en') && (i18n.defaultLocale !== 'en')) {
38
+ for (const [ ns, phrases ] of Object.entries(i18n.i18n.en)) {
39
+ i18next.addResourceBundle('en', ns, phrases, true, true);
40
+ }
41
+ }
28
42
 
29
43
  // Like standard i18next $t, but also with support
30
44
  // for just one object argument with at least a `key`
@@ -5,7 +5,7 @@
5
5
 
6
6
  .apos-table__header {
7
7
  margin-bottom: $spacing-base;
8
- padding: 12.5px 4.5px;
8
+ padding: 12.5px 15px;
9
9
  border-bottom: 1px solid var(--a-base-8);
10
10
  color: var(--a-base-3);
11
11
  text-align: left;
@@ -52,12 +52,13 @@ span.apos-table__header-label:hover {
52
52
  @include apos-transition(all, 0.05s);
53
53
  }
54
54
  .apos-table__cell {
55
- padding: 5px;
55
+ padding: 5px 15px;
56
56
  border-bottom: 1px solid var(--a-base-10);
57
57
  }
58
58
 
59
59
  .apos-table__cell--context-menu {
60
- width: 40px;
60
+ padding-right: 0;
61
+ padding-left: 0;
61
62
  }
62
63
 
63
64
  .apos-table__cell-field--context-menu {
@@ -156,7 +156,7 @@ module.exports = {
156
156
  self.schema = self.apos.schema.compose({
157
157
  addFields: self.apos.schema.fieldsToArray(`Module ${self.__meta.name}`, self.fields),
158
158
  arrangeFields: self.apos.schema.groupsToArray(self.fieldsGroups)
159
- });
159
+ }, self);
160
160
  const forbiddenFields = [
161
161
  '_id',
162
162
  'type'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apostrophe",
3
- "version": "3.6.0",
3
+ "version": "3.7.0",
4
4
  "description": "The Apostrophe Content Management System.",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/.scratch.md DELETED
@@ -1,2 +0,0 @@
1
- - `/deep/ .apos-button` to `:deep(.apos-button)`
2
- - Remove v-popover, v-tooltip, VueClickOutsideElement