apostrophe 3.11.0 → 3.14.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 (43) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/index.js +37 -1
  3. package/lib/moog.js +2 -2
  4. package/modules/@apostrophecms/asset/index.js +24 -9
  5. package/modules/@apostrophecms/asset/lib/globalIcons.js +2 -0
  6. package/modules/@apostrophecms/attachment/index.js +1 -1
  7. package/modules/@apostrophecms/doc/index.js +9 -3
  8. package/modules/@apostrophecms/doc-type/index.js +2 -2
  9. package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocContextMenu.vue +0 -7
  10. package/modules/@apostrophecms/express/index.js +50 -38
  11. package/modules/@apostrophecms/http/index.js +0 -20
  12. package/modules/@apostrophecms/i18n/i18n/en.json +1 -0
  13. package/modules/@apostrophecms/i18n/i18n/sk.json +23 -1
  14. package/modules/@apostrophecms/i18n/index.js +62 -13
  15. package/modules/@apostrophecms/login/index.js +282 -42
  16. package/modules/@apostrophecms/login/ui/apos/components/TheAposLogin.vue +242 -77
  17. package/modules/@apostrophecms/login/ui/apos/components/TheAposLoginHeader.vue +108 -0
  18. package/modules/@apostrophecms/module/index.js +12 -2
  19. package/modules/@apostrophecms/page/index.js +98 -82
  20. package/modules/@apostrophecms/permission/index.js +1 -1
  21. package/modules/@apostrophecms/piece-page-type/index.js +1 -1
  22. package/modules/@apostrophecms/piece-type/index.js +86 -73
  23. package/modules/@apostrophecms/schema/index.js +18 -2
  24. package/modules/@apostrophecms/schema/ui/apos/components/AposInputArea.vue +12 -5
  25. package/modules/@apostrophecms/schema/ui/apos/components/AposInputCheckboxes.vue +4 -0
  26. package/modules/@apostrophecms/ui/ui/apos/lib/i18next.js +18 -0
  27. package/modules/@apostrophecms/util/index.js +3 -9
  28. package/modules/@apostrophecms/util/ui/src/http.js +1 -7
  29. package/package.json +2 -1
  30. package/test/express.js +2 -26
  31. package/test/http.js +0 -24
  32. package/test/login-requirements.js +328 -0
  33. package/test/modules/base-type/i18n/custom/en.json +4 -0
  34. package/test/modules/base-type/i18n/en.json +3 -0
  35. package/test/modules/nested-module-subdirs/example1/index.js +5 -0
  36. package/test/modules/nested-module-subdirs/modules.js +7 -0
  37. package/test/modules/subtype/i18n/custom/en.json +4 -0
  38. package/test/modules/subtype/index.js +7 -0
  39. package/test/pages-rest.js +39 -0
  40. package/test/pieces-page-type.js +63 -0
  41. package/test/static-i18n.js +28 -0
  42. package/test/with-nested-module-subdirs.js +32 -0
  43. package/test/without-nested-module-subdirs.js +31 -0
@@ -6,29 +6,32 @@
6
6
  :class="themeClass"
7
7
  >
8
8
  <div class="apos-login__wrapper">
9
- <transition name="fade-body">
10
- <div class="apos-login__upper" v-show="loaded">
11
- <div class="apos-login__header">
12
- <label
13
- class="apos-login__project apos-login__project-env"
14
- :class="[`apos-login__project-env--${context.env}`]"
15
- >
16
- {{ context.env }}
17
- </label>
18
- <label class="apos-login__project apos-login__project-name">
19
- {{ context.name }}
20
- </label>
21
- <label class="apos-login--error">
22
- {{ error }}
23
- </label>
24
- </div>
9
+ <transition name="fade-body" mode="out-in">
10
+ <div
11
+ key="1"
12
+ class="apos-login__upper"
13
+ v-if="loaded && phase === 'beforeSubmit'"
14
+ >
15
+ <TheAposLoginHeader
16
+ :env="context.env"
17
+ :name="context.name"
18
+ :error="$t(error)"
19
+ />
25
20
 
26
- <div class="apos-login__body" v-show="loaded">
21
+ <div class="apos-login__body">
27
22
  <form @submit.prevent="submit">
28
23
  <AposSchema
29
24
  :schema="schema"
30
25
  v-model="doc"
31
26
  />
27
+ <Component
28
+ v-for="requirement in beforeSubmitRequirements"
29
+ :key="requirement.name"
30
+ :is="requirement.component"
31
+ v-bind="getRequirementProps(requirement.name)"
32
+ @done="requirementDone(requirement, $event)"
33
+ @block="requirementBlock(requirement)"
34
+ />
32
35
  <!-- TODO -->
33
36
  <!-- <a href="#" class="apos-login__link">Forgot Password</a> -->
34
37
  <AposButton
@@ -44,6 +47,29 @@
44
47
  </form>
45
48
  </div>
46
49
  </div>
50
+ <div
51
+ key="2"
52
+ class="apos-login__upper"
53
+ v-else-if="activeSoloRequirement"
54
+ >
55
+ <TheAposLoginHeader
56
+ :env="context.env"
57
+ :name="context.name"
58
+ :error="$t(error)"
59
+ :tiny="true"
60
+ />
61
+ <div class="apos-login__body">
62
+ <Component
63
+ v-if="!fetchingRequirementProps"
64
+ v-bind="getRequirementProps(activeSoloRequirement.name)"
65
+ :is="activeSoloRequirement.component"
66
+ :success="activeSoloRequirement.success"
67
+ :error="activeSoloRequirement.error"
68
+ @done="requirementDone(activeSoloRequirement, $event)"
69
+ @confirm="requirementConfirmed(activeSoloRequirement)"
70
+ />
71
+ </div>
72
+ </div>
47
73
  </transition>
48
74
  </div>
49
75
  <transition name="fade-footer">
@@ -66,7 +92,9 @@ export default {
66
92
  mixins: [ AposThemeMixin ],
67
93
  data() {
68
94
  return {
69
- loaded: false,
95
+ phase: 'beforeSubmit',
96
+ mounted: false,
97
+ beforeCreateFinished: false,
70
98
  error: '',
71
99
  busy: false,
72
100
  doc: {
@@ -89,15 +117,64 @@ export default {
89
117
  required: true
90
118
  }
91
119
  ],
92
- context: {}
120
+ requirements: getRequirements(),
121
+ context: {},
122
+ requirementProps: {},
123
+ fetchingRequirementProps: false
93
124
  };
94
125
  },
95
126
  computed: {
96
- disabled: function () {
97
- return this.doc.hasErrors;
127
+ loaded() {
128
+ return this.mounted && this.beforeCreateFinished;
129
+ },
130
+ disabled() {
131
+ return this.doc.hasErrors || !!this.beforeSubmitRequirements.find(requirement => !requirement.done);
132
+ },
133
+ beforeSubmitRequirements() {
134
+ return this.requirements.filter(requirement => requirement.phase === 'beforeSubmit');
135
+ },
136
+ // The currently active requirement expecting a solo presentation.
137
+ // Currently it only concerns `afterPasswordVerified` requirements.
138
+ // beforeSubmit requirements are not presented solo.
139
+ activeSoloRequirement() {
140
+ return (this.phase === 'afterPasswordVerified') &&
141
+ this.requirements.find(requirement =>
142
+ (requirement.phase === 'afterPasswordVerified') && !requirement.done
143
+ );
144
+ }
145
+ },
146
+ watch: {
147
+ async activeSoloRequirement(newVal) {
148
+ if (
149
+ (this.phase === 'afterPasswordVerified') &&
150
+ (newVal?.phase === 'afterPasswordVerified') &&
151
+ newVal.propsRequired &&
152
+ !(newVal.success || newVal.error)
153
+ ) {
154
+ try {
155
+ this.fetchingRequirementProps = true;
156
+ const data = await apos.http.post(`${apos.login.action}/requirement-props`, {
157
+ busy: true,
158
+ body: {
159
+ name: newVal.name,
160
+ incompleteToken: this.incompleteToken
161
+ }
162
+ });
163
+ this.requirementProps = {
164
+ ...this.requirementProps,
165
+ [newVal.name]: data
166
+ };
167
+ } catch (e) {
168
+ this.error = e.message || 'apostrophe:loginErrorGeneric';
169
+ } finally {
170
+ this.fetchingRequirementProps = false;
171
+ }
172
+ } else {
173
+ return null;
174
+ }
98
175
  }
99
176
  },
100
- async beforeCreate () {
177
+ async beforeCreate() {
101
178
  const stateChange = parseInt(window.sessionStorage.getItem('aposStateChange'));
102
179
  const seen = JSON.parse(window.sessionStorage.getItem('aposStateChangeSeen') || '{}');
103
180
  if (!seen[window.location.href]) {
@@ -110,15 +187,18 @@ export default {
110
187
  }
111
188
  }
112
189
  try {
113
- this.context = await apos.http.get(`${apos.login.action}/context`, {
190
+ this.context = await apos.http.post(`${apos.login.action}/context`, {
114
191
  busy: true
115
192
  });
193
+ this.requirementProps = this.context.requirementProps;
116
194
  } catch (e) {
117
- this.error = 'An error occurred. Please try again.';
195
+ this.error = e.message || 'apostrophe:loginErrorGeneric';
196
+ } finally {
197
+ this.beforeCreateFinished = true;
118
198
  }
119
199
  },
120
200
  mounted() {
121
- this.loaded = true;
201
+ this.mounted = true;
122
202
  },
123
203
  methods: {
124
204
  async submit() {
@@ -127,27 +207,148 @@ export default {
127
207
  }
128
208
  this.busy = true;
129
209
  this.error = '';
210
+
211
+ await this.invokeInitialLoginApi();
212
+ },
213
+ async invokeInitialLoginApi() {
214
+ try {
215
+ const response = await apos.http.post(`${apos.login.action}/login`, {
216
+ busy: true,
217
+ body: {
218
+ ...this.doc.data,
219
+ requirements: this.getInitialSubmitRequirementsData(),
220
+ session: true
221
+ }
222
+ });
223
+ if (response && response.incompleteToken) {
224
+ this.incompleteToken = response.incompleteToken;
225
+ this.phase = 'afterPasswordVerified';
226
+ } else {
227
+ this.redirectAfterLogin();
228
+ }
229
+ } catch (e) {
230
+ this.error = e.message || 'An error occurred. Please try again.';
231
+ this.phase = 'beforeSubmit';
232
+ this.requirements = getRequirements();
233
+ } finally {
234
+ this.busy = false;
235
+ }
236
+ },
237
+ getInitialSubmitRequirementsData() {
238
+ return Object.fromEntries(this.requirements.filter(r => r.phase !== 'afterPasswordVerified').map(r => ([
239
+ r.name,
240
+ r.value
241
+ ])));
242
+ },
243
+ async invokeFinalLoginApi() {
130
244
  try {
131
245
  await apos.http.post(`${apos.login.action}/login`, {
132
246
  busy: true,
133
247
  body: {
134
248
  ...this.doc.data,
249
+ incompleteToken: this.incompleteToken,
250
+ requirements: this.getFinalSubmitRequirementsData(),
135
251
  session: true
136
252
  }
137
253
  });
138
- window.sessionStorage.setItem('aposStateChange', Date.now());
139
- window.sessionStorage.setItem('aposStateChangeSeen', '{}');
140
- // TODO handle situation where user should be sent somewhere other than homepage.
141
- // Redisplay homepage with editing interface
142
- location.assign(`${apos.prefix}/`);
254
+ this.redirectAfterLogin();
143
255
  } catch (e) {
144
256
  this.error = e.message || 'An error occurred. Please try again.';
257
+ this.requirements = getRequirements();
258
+ this.phase = 'beforeSubmit';
145
259
  } finally {
146
260
  this.busy = false;
147
261
  }
262
+ },
263
+ getFinalSubmitRequirementsData() {
264
+ return Object.fromEntries(this.requirements.filter(r => r.phase === 'afterPasswordVerified').map(r => ([
265
+ r.name,
266
+ r.value
267
+ ])));
268
+ },
269
+ redirectAfterLogin() {
270
+ window.sessionStorage.setItem('aposStateChange', Date.now());
271
+ window.sessionStorage.setItem('aposStateChangeSeen', '{}');
272
+ // TODO handle situation where user should be sent somewhere other than homepage.
273
+ // Redisplay homepage with editing interface
274
+ location.assign(`${apos.prefix}/`);
275
+ },
276
+ async requirementBlock(requirementBlock) {
277
+ const requirement = this.requirements.find(requirement => requirement.name === requirementBlock.name);
278
+ requirement.done = false;
279
+ requirement.value = undefined;
280
+ },
281
+ async requirementDone(requirementDone, value) {
282
+ const requirement = this.requirements.find(requirement => requirement.name === requirementDone.name);
283
+
284
+ if (requirement.phase === 'beforeSubmit') {
285
+ requirement.done = true;
286
+ requirement.value = value;
287
+ return;
288
+ }
289
+
290
+ requirement.error = null;
291
+
292
+ try {
293
+ await apos.http.post(`${apos.login.action}/requirement-verify`, {
294
+ busy: true,
295
+ body: {
296
+ name: requirement.name,
297
+ value,
298
+ incompleteToken: this.incompleteToken
299
+ }
300
+ });
301
+
302
+ requirement.success = true;
303
+ } catch (err) {
304
+ requirement.error = err;
305
+ }
306
+
307
+ // Avoids the need for a deep watch
308
+ this.requirements = [ ...this.requirements ];
309
+
310
+ if (requirement.success && !requirement.askForConfirmation) {
311
+ requirement.done = true;
312
+
313
+ if (!this.activeSoloRequirement) {
314
+ await this.invokeFinalLoginApi();
315
+ }
316
+ }
317
+ },
318
+
319
+ async requirementConfirmed (requirementConfirmed) {
320
+ const requirement = this.requirements
321
+ .find(requirement => requirement.name === requirementConfirmed.name);
322
+
323
+ requirement.done = true;
324
+
325
+ if (!this.activeSoloRequirement) {
326
+ await this.invokeFinalLoginApi();
327
+ }
328
+ },
329
+ getRequirementProps(name) {
330
+ return this.requirementProps[name] || {};
148
331
  }
149
332
  }
150
333
  };
334
+
335
+ function getRequirements() {
336
+ const requirements = Object.entries(apos.login.requirements).map(([ name, requirement ]) => {
337
+ return {
338
+ name,
339
+ component: requirement.component || name,
340
+ ...requirement,
341
+ done: false,
342
+ value: null,
343
+ success: null,
344
+ error: null
345
+ };
346
+ });
347
+ return [
348
+ ...requirements.filter(r => r.phase === 'beforeSubmit'),
349
+ ...requirements.filter(r => r.phase === 'afterPasswordVerified')
350
+ ];
351
+ }
151
352
  </script>
152
353
 
153
354
  <style lang="scss">
@@ -171,13 +372,15 @@ export default {
171
372
 
172
373
  .fade-stage-enter-to,
173
374
  .fade-body-enter-to,
174
- .fade-footer-enter-to {
375
+ .fade-footer-enter-to,
376
+ .fade-body-leave {
175
377
  opacity: 1;
176
378
  }
177
379
 
178
380
  .fade-stage-enter,
179
381
  .fade-body-enter,
180
- .fade-footer-enter {
382
+ .fade-footer-enter,
383
+ .fade-body-leave-to {
181
384
  opacity: 0;
182
385
  }
183
386
 
@@ -186,11 +389,15 @@ export default {
186
389
  transition-delay: 0.6s;
187
390
  }
188
391
 
189
- .fade-body-enter-to {
392
+ .fade-body-leave-active {
393
+ transition: all 0.25s linear;
394
+ }
395
+
396
+ .fade-body-enter-to, .fade-body-leave {
190
397
  transform: translateY(0);
191
398
  }
192
399
 
193
- .fade-body-enter {
400
+ .fade-body-enter, .fade-body-leave-to {
194
401
  transform: translateY(4px);
195
402
  }
196
403
 
@@ -212,48 +419,6 @@ export default {
212
419
  margin: 0 auto;
213
420
  }
214
421
 
215
- &__header {
216
- z-index: $z-index-manager-display;
217
- display: flex;
218
- flex-direction: column;
219
- justify-content: center;
220
- align-items: start;
221
- width: max-content;
222
- }
223
-
224
- &__project-name {
225
- @include type-display;
226
- margin: 0;
227
- color: var(--a-text-primary);
228
- text-transform: capitalize;
229
- }
230
-
231
- &__project-env {
232
- @include type-base;
233
- text-transform: capitalize;
234
- padding: 6px 12px;
235
- color: var(--a-white);
236
- background: var(--a-success);
237
- border-radius: 5px;
238
- margin-bottom: 15px;
239
-
240
- &--development {
241
- background: var(--a-danger);
242
- }
243
-
244
- &--success {
245
- background: var(--a-warning);
246
- }
247
- }
248
-
249
- &--error {
250
- @include type-help;
251
- color: var(--a-danger);
252
- min-height: 13px;
253
- margin-top: 20px;
254
- margin-bottom: 15px;
255
- }
256
-
257
422
  form {
258
423
  position: relative;
259
424
  display: flex;
@@ -290,7 +455,7 @@ export default {
290
455
  max-width: $login-container;
291
456
  margin: auto;
292
457
  align-items: center;
293
- justify-content: start;
458
+ justify-content: flex-start;
294
459
  }
295
460
 
296
461
  &__project-version {
@@ -0,0 +1,108 @@
1
+ <template>
2
+ <div
3
+ class="apos-login__header"
4
+ :class="{'apos-login__header--tiny': tiny}"
5
+ >
6
+ <label
7
+ class="apos-login__project apos-login__project-env"
8
+ :class="[`apos-login__project-env--${env}`]"
9
+ >
10
+ {{ env }}
11
+ </label>
12
+ <label class="apos-login__project apos-login__project-name">
13
+ {{ name }}
14
+ </label>
15
+ <label class="apos-login--error">
16
+ {{ error }}
17
+ </label>
18
+ </div>
19
+ </template>
20
+
21
+ <script>
22
+
23
+ export default {
24
+ props: {
25
+ env: {
26
+ type: String,
27
+ default: ''
28
+ },
29
+ name: {
30
+ type: String,
31
+ default: ''
32
+ },
33
+ tiny: {
34
+ type: Boolean,
35
+ default: false
36
+ },
37
+ error: {
38
+ type: String,
39
+ default: ''
40
+ }
41
+ }
42
+ };
43
+ </script>
44
+ <style scoped lang='scss'>
45
+
46
+ .apos-login {
47
+
48
+ &__header {
49
+ z-index: $z-index-manager-display;
50
+ display: flex;
51
+ flex-direction: column;
52
+ justify-content: center;
53
+ align-items: flex-start;
54
+ width: max-content;
55
+ }
56
+
57
+ &__project-name {
58
+ @include type-display;
59
+ margin: 0;
60
+ color: var(--a-text-primary);
61
+ text-transform: capitalize;
62
+ }
63
+
64
+ &__project-env {
65
+ @include type-base;
66
+ text-transform: capitalize;
67
+ padding: 6px 12px;
68
+ color: var(--a-white);
69
+ background: var(--a-success);
70
+ border-radius: 5px;
71
+ margin-bottom: 15px;
72
+
73
+ &--development {
74
+ background: var(--a-danger);
75
+ }
76
+
77
+ &--success {
78
+ background: var(--a-warning);
79
+ }
80
+ }
81
+
82
+ &--error {
83
+ @include type-help;
84
+ color: var(--a-danger);
85
+ min-height: 13px;
86
+ margin-top: 20px;
87
+ margin-bottom: 15px;
88
+ }
89
+
90
+ &__header--tiny {
91
+ flex-direction: row;
92
+ color: #F8F9FA;
93
+
94
+ .apos-login__project {
95
+ opacity: 0.7
96
+ }
97
+
98
+ .apos-login__project-name {
99
+ font-size: 21px;
100
+
101
+ }
102
+
103
+ .apos-login__project-env {
104
+ margin-right: 10px;
105
+ }
106
+ }
107
+ }
108
+ </style>
@@ -29,10 +29,20 @@ const _ = require('lodash');
29
29
 
30
30
  module.exports = {
31
31
 
32
- cascades: [ 'csrfExceptions' ],
33
-
34
32
  init(self) {
35
33
  self.apos = self.options.apos;
34
+ const capturedSections = [
35
+ 'queries',
36
+ 'extendQueries',
37
+ 'icons'
38
+ ];
39
+ for (const section of capturedSections) {
40
+ // Unparsed sections are now captured in __meta, promote
41
+ // these to the top level to maintain bc. For new unparsed
42
+ // sections we'll leave them in `__meta` to avoid bc breaks
43
+ // with project-level properties of the module
44
+ self[section] = self.__meta[section];
45
+ }
36
46
  // all apostrophe modules are properties of self.apos.modules.
37
47
  // Those with an alias are also properties of self.apos
38
48
  self.apos.modules[self.__meta.name] = self;