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 +29 -1
- package/README.md +1 -1
- package/index.js +104 -3
- package/modules/@apostrophecms/doc/index.js +2 -0
- package/modules/@apostrophecms/doc-type/index.js +1 -1
- package/modules/@apostrophecms/i18n/index.js +10 -1
- package/modules/@apostrophecms/login/index.js +0 -15
- package/modules/@apostrophecms/migration/index.js +1 -1
- package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManagerDisplay.vue +1 -1
- package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue +29 -5
- package/modules/@apostrophecms/schema/index.js +81 -7
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputSelect.vue +24 -6
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputSlug.vue +0 -4
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputString.vue +0 -7
- package/modules/@apostrophecms/ui/ui/apos/components/AposCellContextMenu.vue +1 -1
- package/modules/@apostrophecms/ui/ui/apos/lib/i18next.js +16 -2
- package/modules/@apostrophecms/ui/ui/apos/scss/global/_tables.scss +4 -3
- package/modules/@apostrophecms/widget-type/index.js +1 -1
- package/package.json +1 -1
- package/.scratch.md +0 -2
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 [
|
|
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 |
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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 {
|
package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue
CHANGED
|
@@ -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="
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
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
|
|
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 && !
|
|
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(
|
|
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.
|
|
67
|
-
this.next = this.
|
|
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.
|
|
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 {
|
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
package/.scratch.md
DELETED