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,627 @@
1
+ // Enable users to manage their personal settings (user record).
2
+ //
3
+ // ## Options
4
+ //
5
+ // `subforms`
6
+ //
7
+ // An object with subform configurations. The key is the subform name, the value
8
+ // is the subform configuration described below. Subforms rendered on the client
9
+ // side have two modes - preview and edit. The initial mode is preview. The configuration
10
+ // provides the necessary information for both modes.
11
+ //
12
+ // ```js
13
+ // subforms: {
14
+ // // The subform name
15
+ // name: {
16
+ // // Required subform fields, shown in the order specified. The fields should
17
+ // // exist in the user schema. The information is used in edit mode only.
18
+ // // Currently supported system user fields are 'adminLocale' and 'password'.
19
+ // // Keep in mind 'adminLocale' is available only if the `apostrophecms/i18n` module
20
+ // // has the appropriate configuration.
21
+ // fields: [ 'firstName', 'lastName' ],
22
+ // // Optional subform label. Used in both preview and edit mode.
23
+ // label: 'Profile',
24
+ // // Optional subform help text. It is rendered instead
25
+ // // of the subform preview value in preview mode only.
26
+ // help: 'Your full name',
27
+ // // The subform value rendered in preview mode, but only if `help` option is not provided.
28
+ // // A string or i18n key / template can be specified.
29
+ // // If not specified, the UI will attempt to generate a
30
+ // // preview value based on the subform schema and field values (space separated).
31
+ // preview: '{{ firstName }} {{ lastName }}',
32
+ // // In effect ONLY if `preview` and `help` options are not present. Provide a custom,
33
+ // // already registered (admin UI) component to render the subform preview value.
34
+ // // The subform config object and current field values will be passed as props.
35
+ // previewComponent: 'MyComponent',
36
+ // // Optional protection type. Currently allowed values are `password`
37
+ // // and `true` (alias of `password`). If specified, the subform will be
38
+ // // protected by the user current password.
39
+ // protection: true,
40
+ // // Optional flag to indicate that the subform should be reloaded after save.
41
+ // reload: true
42
+ // }
43
+ // }
44
+ // ```
45
+ //
46
+ // `groups`
47
+ //
48
+ // An object with group configurations. The key is the group name, the value
49
+ // is the group configuration, described below. Groups are used to organize
50
+ // subforms in the settings modal (tabs). If no groups are configured, a single group
51
+ // named "ungrouped" will be created. The order of the groups is respected.
52
+ //
53
+ // ```js
54
+ // groups: {
55
+ // // The group name
56
+ // account: {
57
+ // // The group label.
58
+ // label: 'Account',
59
+ // // The subforms that belong to the group. The order is respected.
60
+ // subforms: [ 'name', 'password' ]
61
+ // }
62
+ // }
63
+ // ```
64
+ //
65
+ // ## API
66
+ //
67
+ // Add a protected field to the system protected fields list. This will ensure
68
+ // that any subform containing that field will be ALWAYS protected by
69
+ // the user's current password. It is recommended to use this method in the
70
+ // `apostrophe:modulesRegistered` event handler.
71
+ // `self.apos.settings.addProtectedField(fieldName, protectionType)`
72
+ //
73
+ // Add a forbidden field to the forbidden fields list. This will ensure that
74
+ // the field will not be allowed in any subform. It is recommended to use this
75
+ // method in the `apostrophe:modulesRegistered` event handler.
76
+ // `self.apos.settings.addForbiddenField(fieldName)`
77
+ //
78
+ // Add a field to the reload after save fields list. This will ensure that
79
+ // the page will be reloaded after subform containing the field is saved.
80
+ // It is recommended to use this method in the `apostrophe:modulesRegistered`
81
+ // event handler.
82
+ // `self.apos.settings.addReloadAfterSaveField(fieldName)`
83
+ //
84
+ // ## UI
85
+ //
86
+ // An example of a custom `previewComponent` with the core components explained
87
+ // can be found in the relevant PR:
88
+ // https://github.com/apostrophecms/apostrophe/pull/4236
89
+ const { klona } = require('klona');
90
+
91
+ module.exports = {
92
+ options: {
93
+ alias: 'settings',
94
+ subforms: {},
95
+ groups: {}
96
+ },
97
+
98
+ init(self) {
99
+ // List of all allowed protection types and their aliases (`subform.protection: type`).
100
+ // The key is the type or alias, the value is the actual type (always a string).
101
+ // All subforms `protection` prop will be converted to the actual type.
102
+ // Invalid protection type will panic.
103
+ self.protectionTypes = {
104
+ // Protection type to be used if protected is simply set to `true`.
105
+ true: 'password',
106
+ password: 'password'
107
+ // TODO phase 3
108
+ // email: 'email'
109
+ };
110
+ // Collection of fieldName: protectionType objects for system forced protected fields.
111
+ // The order is important, the first match is used (first have higher priority).
112
+ // If there are multiple fields in the subform, having a system protected field,
113
+ // the first match from this list wins. If there is specifically `password` field
114
+ // in the subform, the schema will be completely replaced with the auto-generated
115
+ // password schema.
116
+ // Do not modify this object directly, use
117
+ // `self.apos.settings.addProtectedField(fieldName, protectionType)` instead.
118
+ self.systemProtectedFields = {
119
+ password: self.protectionTypes.password
120
+ // TODO phase 3
121
+ // username: self.protectionTypes.password,
122
+ // email: self.protectionTypes.email
123
+ };
124
+ // Completely forbidden fields, they are not allowed in the subforms.
125
+ // Do not modify this array directly, use
126
+ // `self.apos.settings.addForbiddenField(fieldName)` instead.
127
+ self.systemForbiddenFields = [
128
+ 'role',
129
+ 'disabled',
130
+ // TODO remove in phase 3
131
+ 'username',
132
+ 'email'
133
+ ];
134
+ // Fields that should trigger reload after saving.
135
+ // Do not modify this array directly, use
136
+ // `self.apos.settings.addReloadAfterSaveField(fieldName)` instead.
137
+ self.systemReloadAfterSaveFields = [
138
+ 'adminLocale'
139
+ ];
140
+ self.userSchema = [];
141
+ self.subforms = [];
142
+ self.initSubforms();
143
+ self.enableBrowserData();
144
+ self.addToAdminBar();
145
+ },
146
+
147
+ handlers(self) {
148
+ return {
149
+ 'apostrophe:modulesRegistered': {
150
+ addModal() {
151
+ self.addSettingsModal();
152
+ }
153
+ }
154
+ };
155
+ },
156
+
157
+ methods(self) {
158
+ return {
159
+ // Public API method.
160
+ // Add a protected field to the system protected fields list.
161
+ // Modules can add their own protected fields here
162
+ // via 'apostrophe:modulesRegistered' event handler:
163
+ // ```js
164
+ // self.apos.settings.addProtectedField('myField', true);
165
+ // self.apos.settings.addProtectedField('myField', 'password');
166
+ // self.apos.settings.addProtectedField('myField', 'email');
167
+ // ```
168
+ addProtectedField(fieldName, protectionType) {
169
+ if (!self.protectionTypes[protectionType]) {
170
+ throw new Error(
171
+ `[@apostrophecms/settings] Attempt to add a protected field "${fieldName}" with invalid protection type "${protectionType}".`
172
+ );
173
+ }
174
+ if (!self.systemProtectedFields[fieldName]) {
175
+ self.systemProtectedFields[fieldName] = self.protectionTypes[protectionType];
176
+ }
177
+ },
178
+
179
+ // Public API method.
180
+ // Add a forbidden field to the forbidden fields list.
181
+ // Modules can add their own forbidden fields here
182
+ // via 'apostrophe:modulesRegistered' event handler:
183
+ // `self.apos.settings.addForbiddenField('myField');`
184
+ addForbiddenField(fieldName) {
185
+ if (!self.systemForbiddenFields.includes(fieldName)) {
186
+ self.systemForbiddenFields.push(fieldName);
187
+ }
188
+ },
189
+
190
+ // Public API method.
191
+ // Add a field to the reload after save fields list.
192
+ // Modules can add their own reload after save fields here
193
+ // via 'apostrophe:modulesRegistered' event handler:
194
+ // `self.apos.settings.addReloadAfterSaveField('myField');`
195
+ addReloadAfterSaveField(fieldName) {
196
+ if (!self.systemReloadAfterSaveFields.includes(fieldName)) {
197
+ self.systemReloadAfterSaveFields.push(fieldName);
198
+ }
199
+ },
200
+
201
+ hasSchema() {
202
+ return self.userSchema.length > 0;
203
+ },
204
+
205
+ // Initialize the subforms configuration.
206
+ initSubforms() {
207
+ self.userSchema = self.inferUserSchema();
208
+
209
+ for (const [ name, config ] of Object.entries(self.options.subforms)) {
210
+ // Don't allow malformed subform.fields, the only required prop.
211
+ if (!Array.isArray(config.fields) || config.fields.length === 0) {
212
+ throw new Error(`[@apostrophecms/settings] The subform "${name}" must have at least one field.`);
213
+ }
214
+ // Don't allow malformed subform.protection.
215
+ if (config.protection && !self.protectionTypes[config.protection]) {
216
+ throw new Error(`[@apostrophecms/settings] The protection type "${config.protection}" is not valid.`);
217
+ }
218
+ if (config.protection) {
219
+ config.protection = self.protectionTypes[config.protection];
220
+ }
221
+ // Auto reload after save.
222
+ config.reload = config.reload || self.systemReloadAfterSaveFields
223
+ .some(field => config.fields.includes(field));
224
+ // No one is allowed to set the flag but us.
225
+ delete config._passwordChangeForm;
226
+ const schema = self.getSubformSchema(name);
227
+
228
+ self.subforms.push({
229
+ ...config,
230
+ name,
231
+ schema,
232
+ // constrain the fields to the ones that are actually in the user
233
+ fields: schema.map(field => field.name)
234
+ });
235
+ }
236
+
237
+ this.initGroups();
238
+ this.enhanceSubforms();
239
+ },
240
+
241
+ // Initialize groups based on the configuration given. Fallback to
242
+ // a single group (Other) if none is configured. Move fields that
243
+ // are not in a group to the "Ungrouped" group.
244
+ // This method requires initialized self.subforms.
245
+ initGroups() {
246
+ if (!self.hasSchema()) {
247
+ return;
248
+ }
249
+ // Contains properly sorted fields by groups
250
+ const newSubforms = [];
251
+ // Transformed to array groups
252
+ const groups = [];
253
+ const subforms = self.subforms;
254
+ const otherGroup = {
255
+ name: 'ungrouped',
256
+ label: 'apostrophe:ungrouped'
257
+ };
258
+
259
+ // Transform to array groups
260
+ for (const [ name, group ] of Object.entries(self.options.groups || {})) {
261
+ groups.push({
262
+ name,
263
+ label: group.label || name[0].toUpperCase() + name.slice(1),
264
+ subforms: group.subforms || []
265
+ });
266
+ }
267
+ // Push and Sort subfields to the newSubforms, add group to every subform.
268
+ for (const group of groups) {
269
+ if (!group.subforms.length) {
270
+ continue;
271
+ }
272
+ group.subforms.forEach(name => {
273
+ const subform = subforms.find(subform => subform.name === name);
274
+ if (subform) {
275
+ newSubforms.push({
276
+ ...subform,
277
+ group: {
278
+ name: group.name,
279
+ label: group.label
280
+ }
281
+ });
282
+ }
283
+ });
284
+ }
285
+ // Push the leftover to ungrouped. It shouldn't be possible though.
286
+ const leftover = subforms
287
+ .filter(subform =>
288
+ !newSubforms.some(newSubform => newSubform.name === subform.name)
289
+ );
290
+ for (const subform of leftover) {
291
+ newSubforms.push({
292
+ ...subform,
293
+ group: otherGroup
294
+ });
295
+ }
296
+ self.subforms = newSubforms;
297
+ },
298
+
299
+ // Get the subset of the user schema that is relevant to the configured
300
+ // subforms.
301
+ inferUserSchema() {
302
+ const subforms = self.options.subforms;
303
+ const userSchema = self.apos.user.schema;
304
+ const allSettingsFields = [
305
+ ...new Set(
306
+ Object.keys(subforms)
307
+ .reduce((acc, subform) => {
308
+ // Do not allow password field alongside other fields in a subform
309
+ if (subforms[subform].fields.includes('password')) {
310
+ subforms[subform].fields = [ 'password' ];
311
+ }
312
+ return acc.concat(subforms[subform].fields || []);
313
+ }, [])
314
+ )
315
+ ];
316
+ self.validateSettingsSchema(allSettingsFields, userSchema);
317
+
318
+ return allSettingsFields
319
+ .filter(field => {
320
+ return userSchema.some(userField => userField.name === field);
321
+ })
322
+ // extra safety
323
+ .filter(Boolean)
324
+ .map(field => {
325
+ return klona(userSchema.find(userField => userField.name === field));
326
+ });
327
+ },
328
+
329
+ // Validate that the fields configured in the settings module exist in the
330
+ // user schema and are not forbidden.
331
+ validateSettingsSchema(settingsFieldNames, userSchema) {
332
+ for (const name of settingsFieldNames) {
333
+ if (!userSchema.some(field => field.name === name)) {
334
+ throw new Error(`[@apostrophecms/settings] The field "${name}" is not a valid user field.`);
335
+ }
336
+ if (self.systemForbiddenFields.includes(name)) {
337
+ throw new Error(`[@apostrophecms/settings] The field "${name}" is forbidden.`);
338
+ }
339
+ }
340
+ },
341
+
342
+ // Enhance the subforms - `protection` security.
343
+ // This method requires initialized self.subforms.
344
+ enhanceSubforms() {
345
+ // 1. Add protection flag to subforms for system protected fields.
346
+ for (const [ fieldName, protectionType ] of Object.entries(self.systemProtectedFields)) {
347
+ self.subforms = self.subforms.map(subform => {
348
+ if (subform.fields.includes(fieldName)) {
349
+ subform.protection = protectionType || true;
350
+ }
351
+ return subform;
352
+ });
353
+ }
354
+
355
+ // 2. Ehhance the protected forms schema.
356
+ self.subforms = self.subforms.map(subform => {
357
+ if (!subform.protection) {
358
+ return subform;
359
+ }
360
+
361
+ // 2.1. Special case for the change password subform
362
+ const passwordField = subform.schema.find(field => field.name === 'password');
363
+ if (passwordField) {
364
+ self.enhancePasswordSubform(passwordField, subform);
365
+ return subform;
366
+ }
367
+
368
+ // 2.2. General case for all other protected subforms
369
+ self.enhanceProtectedSubform(subform);
370
+ return subform;
371
+ });
372
+ },
373
+
374
+ // Auto-generate and replace the subform schema for the "password change"
375
+ // scenario.
376
+ enhancePasswordSubform(passwordField, subform) {
377
+ const templateField = self.getPasswordTemplateField();
378
+ subform.help = subform.help || 'apostrophe:passwordChangeHelp';
379
+ if (!subform.label) {
380
+ subform.label = 'apostrophe:password';
381
+ }
382
+ subform.schema = [];
383
+ // Indicates the edge case of password change form
384
+ subform._passwordChangeForm = true;
385
+ subform.schema.push({
386
+ ...passwordField,
387
+ label: 'apostrophe:passwordNew',
388
+ required: true
389
+ });
390
+ subform.schema.push({
391
+ ...templateField,
392
+ label: 'apostrophe:passwordRepeat',
393
+ name: 'passwordRepeat',
394
+ required: true
395
+ });
396
+ subform.schema.push({
397
+ ...templateField,
398
+ label: 'apostrophe:passwordCurrent',
399
+ name: 'passwordCurrent',
400
+ required: true
401
+ });
402
+ },
403
+
404
+ // Enhance the protected subform schema based on the protection type.
405
+ enhanceProtectedSubform(subform) {
406
+ switch (subform.protection) {
407
+ case self.protectionTypes.password: {
408
+ // Last field so that it doesn't mess up with the "first field label"
409
+ // detection on the client side (when form label is not specified).
410
+ subform.schema.push({
411
+ ...self.getPasswordTemplateField(),
412
+ label: 'apostrophe:passwordCurrent',
413
+ name: 'passwordCurrent',
414
+ required: true
415
+ });
416
+ break;
417
+ }
418
+ // TODO `self.protectionTypes.email' in phase 3
419
+
420
+ default: {
421
+ throw new Error(`[@apostrophecms/settings] Not supported protection type "${subform.protection}".`);
422
+ }
423
+ }
424
+ },
425
+
426
+ // Clone the password field from the user schema to be used as a template
427
+ // for auto generated subform schema.
428
+ getPasswordTemplateField() {
429
+ const templateField = klona(self.apos.user.schema.find(field => field.name === 'password'));
430
+ delete templateField.moduleName;
431
+ delete templateField.group;
432
+ delete templateField.name;
433
+ delete templateField.label;
434
+ return templateField;
435
+ },
436
+
437
+ // Get subform fields by subform name.
438
+ // This method requires initialized self.subforms.
439
+ getSubformSchema(name) {
440
+ const subform = self.options.subforms[name];
441
+ if (!subform) {
442
+ throw new Error('notfound', `[@apostrophecms/settings] Subform "${name}" not found.`);
443
+ }
444
+ return subform.fields.map(fieldName => {
445
+ return self.userSchema.find(field => field.name === fieldName);
446
+ });
447
+ },
448
+
449
+ getSubform(name) {
450
+ return self.subforms.find(subform => subform.name === name);
451
+ },
452
+
453
+ // Detect protected subforms and handle them.
454
+ handleProtectedSubform(req, subform, payload) {
455
+ if (!subform.protection) {
456
+ return;
457
+ }
458
+ if (subform._passwordChangeForm) {
459
+ return self.handlePasswordChangeSubform(req, subform, payload);
460
+ }
461
+ switch (subform.protection) {
462
+ case self.protectionTypes.password: {
463
+ return self.handlePasswordProtectedSubform(req, subform, payload);
464
+ }
465
+ // TODO `self.protectionTypes.email' in phase 3
466
+
467
+ // Should not happen as we validate the protected type in the init phase.
468
+ default: {
469
+ throw self.apos.error('invalid', `Not supported protected type "${subform.protection}".`);
470
+ }
471
+ }
472
+ },
473
+
474
+ // Handle the password change subform.
475
+ handlePasswordChangeSubform(req, subform, payload) {
476
+ const { password, passwordRepeat } = payload;
477
+ if (!password || passwordRepeat !== password) {
478
+ const invalid = self.apos.error('invalid', {
479
+ errors: 'invalid'
480
+ });
481
+ invalid.path = 'passwordRepeat';
482
+ throw [ invalid ];
483
+ }
484
+
485
+ return self.handlePasswordProtectedSubform(req, subform, payload);
486
+ },
487
+
488
+ // Handle the password protected subform.
489
+ async handlePasswordProtectedSubform(req, subform, payload) {
490
+ try {
491
+ await self.apos.user.verifyPassword(req.user, payload.passwordCurrent);
492
+ } catch (e) {
493
+ throw self.apos.error(
494
+ 'forbidden',
495
+ 'apostrophe:passwordCurrentError',
496
+ {
497
+ path: 'passwordCurrent'
498
+ }
499
+ );
500
+ }
501
+ return subform;
502
+ },
503
+
504
+ // Handle the after save logic. If the saved subform requires reload
505
+ // after save, we will add session indicator that will allow the client
506
+ // to restore its state. The client is responsible for the actual reload.
507
+ // The session value contains the current subform name. The value is sent
508
+ // once via the `getBrowserData` method and then removed from the session.
509
+ handleAfterSave(req, subform) {
510
+ if (!subform.reload) {
511
+ return;
512
+ }
513
+ req.session.aposSettingsReload = subform.name;
514
+ // TODO email(s) in phase 3
515
+ },
516
+
517
+ addToAdminBar() {
518
+ if (!self.hasSchema()) {
519
+ return;
520
+ }
521
+ self.apos.adminBar.add(
522
+ `${self.__meta.name}:settings`,
523
+ 'apostrophe:settings',
524
+ false,
525
+ {
526
+ user: true
527
+ }
528
+ );
529
+ },
530
+
531
+ addSettingsModal() {
532
+ if (!self.hasSchema()) {
533
+ return;
534
+ }
535
+ self.apos.modal.add(
536
+ `${self.__meta.name}:settings`,
537
+ self.getComponentName('settingsModal', 'AposSettingsManager'),
538
+ { moduleName: self.__meta.name }
539
+ );
540
+ },
541
+
542
+ getBrowserData(req) {
543
+ const restore = req.session.aposSettingsReload;
544
+ delete req.session.aposSettingsReload;
545
+
546
+ return {
547
+ subforms: self.subforms,
548
+ action: self.action,
549
+ restore
550
+ };
551
+ }
552
+ };
553
+ },
554
+
555
+ restApiRoutes(self) {
556
+ return {
557
+ async getAll(req) {
558
+ if (!self.hasSchema() || !req.user) {
559
+ throw self.apos.error('notfound');
560
+ }
561
+ const user = await self.apos.user
562
+ .find(req, { _id: req.user._id })
563
+ .permission(false)
564
+ .toObject();
565
+
566
+ if (!user) {
567
+ throw self.apos.error('notfound');
568
+ }
569
+
570
+ const values = {
571
+ _id: user._id
572
+ };
573
+ for (const field of self.userSchema) {
574
+ values[field.name] = user[field.name];
575
+ }
576
+ return values;
577
+ }
578
+ };
579
+ },
580
+
581
+ apiRoutes(self) {
582
+ return {
583
+ patch: {
584
+ ':subform': async (req) => {
585
+ if (!self.hasSchema() || !req.user) {
586
+ throw self.apos.error('notfound');
587
+ }
588
+ let subform = self.getSubform(
589
+ self.apos.launder.string(req.params.subform)
590
+ );
591
+
592
+ if (!subform || !subform.schema.length) {
593
+ throw self.apos.error('notfound');
594
+ }
595
+
596
+ await self.handleProtectedSubform(req, subform, req.body);
597
+ // Remove the auto-generated fields from the schema
598
+ subform = klona(subform);
599
+ subform.schema = subform.schema
600
+ .filter(field => self.userSchema.some(userField => userField.name === field.name));
601
+
602
+ const user = await self.apos.user
603
+ .find(req, { _id: req.user._id })
604
+ .permission(false)
605
+ .toObject();
606
+
607
+ if (!user) {
608
+ throw self.apos.error('notfound');
609
+ }
610
+
611
+ await self.apos.schema.convert(req, subform.schema, req.body, user);
612
+ await self.apos.user.update(req, user, { permissions: false });
613
+
614
+ await self.handleAfterSave(req, subform, user);
615
+
616
+ const values = {
617
+ _id: user._id
618
+ };
619
+ for (const field of subform.schema) {
620
+ values[field.name] = user[field.name];
621
+ }
622
+ return values;
623
+ }
624
+ }
625
+ };
626
+ }
627
+ };
@@ -0,0 +1,8 @@
1
+ export default function() {
2
+ if (apos.settings.restore) {
3
+ apos.modal.execute('AposSettingsManager', {
4
+ restore: apos.settings.restore
5
+ });
6
+ apos.settings.restore = null;
7
+ }
8
+ }