apostrophe 3.52.0 → 3.53.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 (52) hide show
  1. package/CHANGELOG.md +60 -2
  2. package/defaults.js +1 -0
  3. package/index.js +3 -2
  4. package/lib/check-if-conditions.js +44 -0
  5. package/lib/moog-require.js +23 -1
  6. package/modules/@apostrophecms/admin-bar/index.js +30 -1
  7. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextBar.vue +4 -1
  8. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextTitle.vue +14 -8
  9. package/modules/@apostrophecms/area/ui/apos/components/AposAreaExpandedMenu.vue +8 -2
  10. package/modules/@apostrophecms/area/ui/apos/components/AposAreaWidget.vue +4 -0
  11. package/modules/@apostrophecms/command-menu/ui/apos/components/AposCommandMenuShortcut.vue +1 -0
  12. package/modules/@apostrophecms/doc/index.js +13 -7
  13. package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocContextMenu.vue +36 -22
  14. package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocEditor.vue +35 -27
  15. package/modules/@apostrophecms/i18n/i18n/en.json +8 -0
  16. package/modules/@apostrophecms/i18n/index.js +49 -2
  17. package/modules/@apostrophecms/image/ui/apos/components/AposMediaUploader.vue +16 -1
  18. package/modules/@apostrophecms/login/index.js +5 -1
  19. package/modules/@apostrophecms/modal/ui/apos/components/AposDocsManagerToolbar.vue +2 -0
  20. package/modules/@apostrophecms/modal/ui/apos/components/AposModal.vue +37 -40
  21. package/modules/@apostrophecms/modal/ui/apos/components/AposModalConfirm.vue +1 -2
  22. package/modules/@apostrophecms/modal/ui/apos/components/AposModalShareDraft.vue +3 -2
  23. package/modules/@apostrophecms/modal/ui/apos/components/AposModalTabs.vue +4 -5
  24. package/modules/@apostrophecms/modal/ui/apos/mixins/AposFocusMixin.js +91 -0
  25. package/modules/@apostrophecms/modal/ui/apos/mixins/AposModalTabsMixin.js +16 -4
  26. package/modules/@apostrophecms/page/ui/apos/components/AposPagesManager.vue +9 -3
  27. package/modules/@apostrophecms/piece-type/index.js +1 -1
  28. package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManager.vue +2 -0
  29. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue +1 -1
  30. package/modules/@apostrophecms/schema/index.js +13 -0
  31. package/modules/@apostrophecms/schema/lib/addFieldTypes.js +3 -10
  32. package/modules/@apostrophecms/schema/ui/apos/components/AposArrayEditor.vue +0 -1
  33. package/modules/@apostrophecms/schema/ui/apos/components/AposInputSlug.vue +1 -15
  34. package/modules/@apostrophecms/schema/ui/apos/components/AposSchema.vue +20 -13
  35. package/modules/@apostrophecms/schema/ui/apos/components/AposSubform.vue +164 -0
  36. package/modules/@apostrophecms/schema/ui/apos/logic/AposSubform.js +141 -0
  37. package/modules/@apostrophecms/settings/index.js +627 -0
  38. package/modules/@apostrophecms/settings/ui/apos/apps/TheAposSettings.js +8 -0
  39. package/modules/@apostrophecms/settings/ui/apos/components/AposSettingsManager.vue +162 -0
  40. package/modules/@apostrophecms/settings/ui/apos/logic/AposSettingsManager.js +169 -0
  41. package/modules/@apostrophecms/ui/ui/apos/components/AposButton.vue +10 -0
  42. package/modules/@apostrophecms/ui/ui/apos/components/AposButtonSplit.vue +23 -6
  43. package/modules/@apostrophecms/ui/ui/apos/components/AposCellLabels.vue +1 -7
  44. package/modules/@apostrophecms/ui/ui/apos/components/AposSubformPreview.vue +136 -0
  45. package/modules/@apostrophecms/ui/ui/apos/lib/i18next.js +6 -6
  46. package/modules/@apostrophecms/ui/ui/apos/mixins/AposCellMixin.js +9 -0
  47. package/modules/@apostrophecms/ui/ui/apos/scss/global/_admin.scss +9 -0
  48. package/modules/@apostrophecms/ui/ui/apos/scss/global/_widgets.scss +5 -1
  49. package/modules/@apostrophecms/user/index.js +30 -3
  50. package/package.json +1 -1
  51. package/test/i18n.js +168 -0
  52. package/test/settings.js +544 -0
@@ -0,0 +1,544 @@
1
+ const t = require('../test-lib/test.js');
2
+ const assert = require('assert/strict');
3
+
4
+ describe('user settings', function () {
5
+ this.timeout(t.timeout);
6
+
7
+ let apos;
8
+
9
+ beforeEach(async function() {
10
+ await t.destroy(apos);
11
+ apos = null;
12
+ });
13
+
14
+ after(async function() {
15
+ return t.destroy(apos);
16
+ });
17
+
18
+ it('should have settings module', async function() {
19
+ apos = await t.create({
20
+ root: module
21
+ });
22
+ assert(apos.modules['@apostrophecms/settings'], 'module not found');
23
+ assert(apos.settings, 'module alias not found');
24
+ assert.deepEqual(apos.settings.options.subforms, {});
25
+ assert.deepEqual(apos.settings.options.groups, {});
26
+ assert.deepEqual(apos.settings.subforms, []);
27
+ assert.deepEqual(apos.settings.userSchema, []);
28
+ assert.equal(apos.settings.hasSchema(), false);
29
+ });
30
+
31
+ it('should panic on non-existing or forbidden user field in subforms', async function () {
32
+ apos = await t.create({
33
+ root: module
34
+ });
35
+
36
+ apos.settings.options.subforms = {
37
+ test: {
38
+ fields: [ 'nonExistingField' ]
39
+ }
40
+ };
41
+ assert.throws(
42
+ apos.settings.initSubforms,
43
+ {
44
+ message: '[@apostrophecms/settings] The field "nonExistingField" is not a valid user field.'
45
+ }
46
+ );
47
+
48
+ const testFields = [ ...apos.settings.systemForbiddenFields ];
49
+
50
+ for (const field of testFields) {
51
+ apos.settings.options.subforms = {
52
+ test: {
53
+ fields: [ field ]
54
+ }
55
+ };
56
+ assert.throws(
57
+ apos.settings.initSubforms,
58
+ {
59
+ message: `[@apostrophecms/settings] The field "${field}" is forbidden.`
60
+ }
61
+ );
62
+ }
63
+ });
64
+
65
+ // It should process settings schema and register the subset of the user schema that
66
+ // is relevant to the configured subforms.
67
+ it('should init subforms', async function () {
68
+ apos = await t.create({
69
+ root: module,
70
+ modules: {
71
+ '@apostrophecms/i18n': {
72
+ options: {
73
+ adminLocales: [
74
+ {
75
+ label: 'English',
76
+ value: 'en'
77
+ },
78
+ {
79
+ label: 'French',
80
+ value: 'fr'
81
+ }
82
+ ]
83
+ }
84
+ },
85
+ '@apostrophecms/user': {
86
+ fields: {
87
+ add: {
88
+ firstName: {
89
+ type: 'string',
90
+ label: 'First Name'
91
+ },
92
+ lastName: {
93
+ type: 'string',
94
+ label: 'Last Name'
95
+ }
96
+ }
97
+ }
98
+ },
99
+ '@apostrophecms/settings': {
100
+ options: {
101
+ subforms: {
102
+ name: {
103
+ label: 'Name',
104
+ fields: [ 'firstName', 'lastName' ],
105
+ preview: '{{ firstName }} {{ lastName }}'
106
+ },
107
+ adminLocale: {
108
+ fields: [ 'adminLocale' ]
109
+ }
110
+ }
111
+ // No groups configured
112
+ }
113
+ }
114
+ }
115
+ });
116
+
117
+ // Schema is processed
118
+ const schema = apos.settings.userSchema;
119
+ assert.equal(apos.settings.hasSchema(), true);
120
+ assert.equal(schema.length, 3);
121
+ assert(schema.some(field => field.name === 'firstName'));
122
+ assert(schema.some(field => field.name === 'lastName'));
123
+ assert(schema.some(field => field.name === 'adminLocale'));
124
+
125
+ const nameSchema = apos.settings.getSubformSchema('name');
126
+ assert.equal(nameSchema.length, 2);
127
+ assert(nameSchema.some(field => field.name === 'firstName'));
128
+ assert(nameSchema.some(field => field.name === 'lastName'));
129
+
130
+ const adminLocaleSchema = apos.settings.getSubformSchema('adminLocale');
131
+ assert.equal(adminLocaleSchema.length, 1);
132
+ assert(adminLocaleSchema.some(field => field.name === 'adminLocale'));
133
+
134
+ // Subforms are initialized
135
+ const subforms = apos.settings.subforms;
136
+ assert.equal(subforms.length, 2);
137
+
138
+ const nameSubform = subforms.find(subform => subform.name === 'name');
139
+ assert.equal(nameSubform.label, 'Name');
140
+ assert.equal(nameSubform.preview, '{{ firstName }} {{ lastName }}');
141
+ assert.deepEqual(nameSubform.schema, nameSchema);
142
+ assert.deepEqual(nameSubform.group, {
143
+ name: 'ungrouped',
144
+ label: 'apostrophe:ungrouped'
145
+ });
146
+
147
+ const adminLocaleSubform = subforms.find(subform => subform.name === 'adminLocale');
148
+ assert.equal(adminLocaleSubform.label, undefined);
149
+ assert.equal(adminLocaleSubform.preview, undefined);
150
+ assert.deepEqual(adminLocaleSubform.schema, adminLocaleSchema);
151
+ assert.deepEqual(adminLocaleSubform.group, {
152
+ name: 'ungrouped',
153
+ label: 'apostrophe:ungrouped'
154
+ });
155
+
156
+ // Appropriate browser data is sent
157
+ const browserData = apos.settings.getBrowserData(apos.task.getReq({
158
+ session: {}
159
+ }));
160
+ assert.deepEqual(browserData.subforms, subforms);
161
+ assert.equal(browserData.action, '/api/v1/@apostrophecms/settings');
162
+ });
163
+
164
+ it('should init groups', async function () {
165
+ apos = await createCommonInstance();
166
+ const [ first, second, third, fourth ] = apos.settings.subforms;
167
+ assert.equal(apos.settings.subforms.length, 4);
168
+
169
+ assert.equal(first.name, 'name');
170
+ assert.deepEqual(first.group, {
171
+ name: 'account',
172
+ label: 'Account'
173
+ });
174
+ assert.equal(second.name, 'password');
175
+ assert.deepEqual(second.group, {
176
+ name: 'account',
177
+ label: 'Account'
178
+ });
179
+ assert.equal(third.name, 'adminLocale');
180
+ assert.deepEqual(third.group, {
181
+ name: 'preferences',
182
+ label: 'Preferences'
183
+ });
184
+ assert.equal(fourth.name, 'display');
185
+ assert.deepEqual(fourth.group, {
186
+ name: 'ungrouped',
187
+ label: 'apostrophe:ungrouped'
188
+ });
189
+ });
190
+
191
+ it('should handle protected subforms', async function () {
192
+ apos = await createCommonInstance();
193
+ const [ first, second, third, fourth ] = apos.settings.subforms;
194
+ assert.equal(apos.settings.subforms.length, 4);
195
+
196
+ assert.equal(first.name, 'name');
197
+ assert.equal(first.protection, 'password');
198
+ // verify the explicitly set by the config private flag is removed
199
+ assert.equal(typeof first._passwordChangeForm, 'undefined');
200
+ assert.deepEqual(first.fields, [ 'firstName', 'lastName' ]);
201
+ assert.equal(first.schema.length, 3);
202
+ // last field is the current password field
203
+ {
204
+ const pwdField = first.schema[first.schema.length - 1];
205
+ assert.equal(pwdField.type, 'password');
206
+ assert.equal(pwdField.name, 'passwordCurrent');
207
+ assert.equal(pwdField.required, true);
208
+ }
209
+
210
+ assert.equal(second.name, 'password');
211
+ assert.equal(second.protection, 'password');
212
+ assert.equal(second._passwordChangeForm, true);
213
+ // displayName is removed
214
+ assert.deepEqual(second.fields, [ 'password' ]);
215
+ assert(second.help);
216
+ assert.equal(second.schema[0].name, 'password');
217
+ assert.equal(second.schema[0].type, 'password');
218
+ assert.equal(second.schema[1].name, 'passwordRepeat');
219
+ assert.equal(second.schema[1].type, 'password');
220
+ assert.equal(second.schema[2].name, 'passwordCurrent');
221
+ assert.equal(second.schema[2].type, 'password');
222
+
223
+ assert.equal(third.name, 'adminLocale');
224
+ assert.equal(!!third.protection, false);
225
+ assert.equal(third.schema.length, 1);
226
+
227
+ assert.equal(fourth.name, 'display');
228
+ assert.equal(fourth.protection, 'password');
229
+ assert.deepEqual(fourth.fields, [ 'displayName' ]);
230
+ assert.equal(fourth.schema.length, 2);
231
+ // last field is the current password field
232
+ {
233
+ const pwdField = fourth.schema[fourth.schema.length - 1];
234
+ assert.equal(pwdField.type, 'password');
235
+ assert.equal(pwdField.name, 'passwordCurrent');
236
+ assert.equal(pwdField.required, true);
237
+ }
238
+ });
239
+
240
+ it('should return 404 when settings user data and no configuration', async function () {
241
+ apos = await t.create({
242
+ root: module
243
+ });
244
+ const { jar } = await login(apos);
245
+
246
+ await assert.rejects(
247
+ apos.http.get('/api/v1/@apostrophecms/settings', { jar }),
248
+ {
249
+ status: 404
250
+ }
251
+ );
252
+
253
+ });
254
+
255
+ it('should load settings user data', async function () {
256
+ apos = await createCommonInstance();
257
+ const { jar, user } = await login(apos);
258
+ const result = await apos.http.get('/api/v1/@apostrophecms/settings', {
259
+ jar
260
+ });
261
+
262
+ assert(result._id);
263
+ assert.deepEqual(result, {
264
+ _id: user._id,
265
+ adminLocale: '',
266
+ displayName: '',
267
+ firstName: '',
268
+ lastName: ''
269
+ });
270
+ });
271
+
272
+ it('should validate when updating settings user data and no configuration', async function () {
273
+ apos = await t.create({
274
+ root: module
275
+ });
276
+
277
+ {
278
+ const { jar } = await login(apos);
279
+
280
+ await assert.rejects(
281
+ apos.http.patch('/api/v1/@apostrophecms/settings/name', {
282
+ jar
283
+ }),
284
+ {
285
+ status: 404
286
+ }
287
+ );
288
+ await t.destroy(apos);
289
+ }
290
+
291
+ apos = await createCommonInstance();
292
+ {
293
+ const { jar } = await login(apos);
294
+ await assert.rejects(
295
+ apos.http.patch('/api/v1/@apostrophecms/settings/non-existing', {
296
+ jar
297
+ }),
298
+ {
299
+ status: 404
300
+ }
301
+ );
302
+ }
303
+ });
304
+
305
+ it('should update settings', async function () {
306
+ apos = await createCommonInstance();
307
+ const { jar, user } = await login(apos);
308
+ const result = await apos.http.patch('/api/v1/@apostrophecms/settings/adminLocale', {
309
+ body: {
310
+ adminLocale: 'fr'
311
+ },
312
+ jar
313
+ });
314
+
315
+ assert(result._id);
316
+ assert.deepEqual(result, {
317
+ _id: user._id,
318
+ adminLocale: 'fr'
319
+ });
320
+ const _user = await apos.user.find(apos.task.getReq(), { _id: user._id }).toObject();
321
+ assert.equal(_user.adminLocale, 'fr');
322
+ });
323
+
324
+ it('should change password', async function () {
325
+ apos = await createCommonInstance();
326
+ const { jar } = await login(apos);
327
+
328
+ // Passwords do not match
329
+ await assert.rejects(
330
+ apos.http.patch('/api/v1/@apostrophecms/settings/password', {
331
+ body: {
332
+ password: 'newpassword',
333
+ passwordRepeat: 'doesNotMatch',
334
+ passwordCurrent: 'invalid'
335
+ },
336
+ jar
337
+ }),
338
+ function (err) {
339
+ assert.equal(err.status, 400);
340
+ assert.equal(err.body.data.errors[0].path, 'passwordRepeat');
341
+ assert.equal(err.body.data.errors[0].message, 'invalid');
342
+ return true;
343
+ }
344
+ );
345
+
346
+ // Current password is invalid
347
+ await assert.rejects(
348
+ apos.http.patch('/api/v1/@apostrophecms/settings/password', {
349
+ body: {
350
+ password: 'newpassword',
351
+ passwordRepeat: 'newpassword',
352
+ passwordCurrent: 'invalid'
353
+ },
354
+ jar
355
+ }),
356
+ function (err) {
357
+ assert.equal(err.status, 403);
358
+ assert.equal(err.body.data.path, 'passwordCurrent');
359
+ assert.equal(err.body.name, 'forbidden');
360
+ return true;
361
+ }
362
+ );
363
+
364
+ await apos.http.patch('/api/v1/@apostrophecms/settings/password', {
365
+ body: {
366
+ password: 'newpassword',
367
+ passwordRepeat: 'newpassword',
368
+ passwordCurrent: 'editor'
369
+ },
370
+ jar
371
+ });
372
+
373
+ await assert.rejects(t.loginAs(apos, 'editor', 'editor'));
374
+ await logout(apos, 'editor', 'editor', jar);
375
+ await t.loginAs(apos, 'editor', 'newpassword');
376
+ });
377
+
378
+ it('should password protect subforms', async function () {
379
+ apos = await createCommonInstance();
380
+ const { jar, user } = await login(apos);
381
+ assert(!user.displayName);
382
+
383
+ // No current password
384
+ await assert.rejects(
385
+ apos.http.patch('/api/v1/@apostrophecms/settings/display', {
386
+ body: {
387
+ displayName: 'Hacker'
388
+ },
389
+ jar
390
+ }),
391
+ function (err) {
392
+ assert.equal(err.status, 403);
393
+ assert.equal(err.body.data.path, 'passwordCurrent');
394
+ assert.equal(err.body.name, 'forbidden');
395
+ return true;
396
+ }
397
+ );
398
+
399
+ // Current password is invalid
400
+ await assert.rejects(
401
+ apos.http.patch('/api/v1/@apostrophecms/settings/display', {
402
+ body: {
403
+ displayName: 'Editor',
404
+ passwordCurrent: 'invalid'
405
+ },
406
+ jar
407
+ }),
408
+ function (err) {
409
+ assert.equal(err.status, 403);
410
+ assert.equal(err.body.data.path, 'passwordCurrent');
411
+ assert.equal(err.body.name, 'forbidden');
412
+ return true;
413
+ }
414
+ );
415
+
416
+ await apos.http.patch('/api/v1/@apostrophecms/settings/display', {
417
+ body: {
418
+ displayName: 'Editor',
419
+ passwordCurrent: 'editor'
420
+ },
421
+ jar
422
+ });
423
+
424
+ const validateUser = await apos.user
425
+ .find(apos.task.getReq(), { _id: user._id })
426
+ .toObject();
427
+
428
+ assert.equal(validateUser.displayName, 'Editor');
429
+
430
+ });
431
+ });
432
+
433
+ async function createCommonInstance() {
434
+ return t.create({
435
+ root: module,
436
+ modules: {
437
+ '@apostrophecms/i18n': {
438
+ options: {
439
+ adminLocales: [
440
+ {
441
+ label: 'English',
442
+ value: 'en'
443
+ },
444
+ {
445
+ label: 'French',
446
+ value: 'fr'
447
+ }
448
+ ]
449
+ }
450
+ },
451
+ '@apostrophecms/user': {
452
+ fields: {
453
+ add: {
454
+ firstName: {
455
+ type: 'string',
456
+ label: 'First Name'
457
+ },
458
+ lastName: {
459
+ type: 'string',
460
+ label: 'Last Name'
461
+ },
462
+ displayName: {
463
+ type: 'string',
464
+ label: 'Display Name'
465
+ }
466
+ }
467
+ }
468
+ },
469
+ '@apostrophecms/settings': {
470
+ options: {
471
+ subforms: {
472
+ display: {
473
+ fields: [ 'displayName' ],
474
+ // same as `protection: 'password'`
475
+ protection: true,
476
+ // validate it can't happen
477
+ _passwordChangeForm: true
478
+ },
479
+ adminLocale: {
480
+ fields: [ 'adminLocale' ]
481
+ },
482
+ name: {
483
+ label: 'Name',
484
+ fields: [ 'firstName', 'lastName' ],
485
+ preview: '{{ firstName }} {{ lastName }}',
486
+ protection: 'password'
487
+ },
488
+ password: {
489
+ // Ensure that only `password` will be used
490
+ fields: [ 'password', 'displayName' ],
491
+ // Test system protected fields
492
+ protection: false
493
+ }
494
+ },
495
+ groups: {
496
+ account: {
497
+ label: 'Account',
498
+ subforms: [ 'name', 'nonExisting', 'password' ]
499
+ },
500
+ preferences: {
501
+ label: 'Preferences',
502
+ subforms: [ 'adminLocale' ]
503
+ }
504
+ }
505
+ }
506
+ }
507
+ }
508
+ });
509
+ }
510
+
511
+ async function login(apos) {
512
+ const user = await t.createUser(apos, 'editor', {
513
+ username: 'editor',
514
+ password: 'editor'
515
+ });
516
+ const jar = await t.getUserJar(apos, user);
517
+ await apos.http.get('/', { jar });
518
+
519
+ return {
520
+ apos,
521
+ user,
522
+ jar
523
+ };
524
+ }
525
+
526
+ async function logout(apos, username, password, jar) {
527
+ await apos.http.post(
528
+ '/api/v1/@apostrophecms/login/logout',
529
+ {
530
+ body: {
531
+ username,
532
+ password,
533
+ session: true
534
+ },
535
+ jar
536
+ }
537
+ );
538
+ await apos.http.get(
539
+ '/',
540
+ {
541
+ jar
542
+ }
543
+ );
544
+ }