apostrophe 3.12.0 → 3.13.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.
@@ -7,28 +7,36 @@
7
7
  >
8
8
  <div class="apos-login__wrapper">
9
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>
10
+ <div
11
+ class="apos-login__upper"
12
+ v-if="loaded && phase === 'beforeSubmit'"
13
+ >
14
+ <TheAposLoginHeader
15
+ :env="context.env"
16
+ :name="context.name"
17
+ :error="$t(error)"
18
+ />
25
19
 
26
- <div class="apos-login__body" v-show="loaded">
27
- <form @submit.prevent="submit">
20
+ <div class="apos-login__body">
21
+ <form
22
+ @submit.prevent="submit"
23
+ >
28
24
  <AposSchema
29
25
  :schema="schema"
30
26
  v-model="doc"
31
27
  />
28
+ <!-- Do not ask these components to render without their props,
29
+ v-show is not enough -->
30
+ <template v-if="loaded">
31
+ <Component
32
+ v-for="requirement in beforeSubmitRequirements"
33
+ :key="requirement.name"
34
+ :is="requirement.component"
35
+ v-bind="getRequirementProps(requirement.name)"
36
+ @done="requirementDone(requirement, $event)"
37
+ @block="requirementBlock(requirement)"
38
+ />
39
+ </template>
32
40
  <!-- TODO -->
33
41
  <!-- <a href="#" class="apos-login__link">Forgot Password</a> -->
34
42
  <AposButton
@@ -44,6 +52,27 @@
44
52
  </form>
45
53
  </div>
46
54
  </div>
55
+ <div
56
+ class="apos-login__upper"
57
+ v-else-if="activeSoloRequirement && !fetchingRequirementProps"
58
+ >
59
+ <TheAposLoginHeader
60
+ :env="context.env"
61
+ :name="context.name"
62
+ :error="$t(error)"
63
+ :tiny="true"
64
+ />
65
+ <div class="apos-login__body">
66
+ <Component
67
+ v-bind="getRequirementProps(activeSoloRequirement.name)"
68
+ :is="activeSoloRequirement.component"
69
+ :success="activeSoloRequirement.success"
70
+ :error="activeSoloRequirement.error"
71
+ @done="requirementDone(activeSoloRequirement, $event)"
72
+ @confirm="requirementConfirmed(activeSoloRequirement)"
73
+ />
74
+ </div>
75
+ </div>
47
76
  </transition>
48
77
  </div>
49
78
  <transition name="fade-footer">
@@ -66,7 +95,9 @@ export default {
66
95
  mixins: [ AposThemeMixin ],
67
96
  data() {
68
97
  return {
69
- loaded: false,
98
+ phase: 'beforeSubmit',
99
+ mounted: false,
100
+ beforeCreateFinished: false,
70
101
  error: '',
71
102
  busy: false,
72
103
  doc: {
@@ -89,15 +120,64 @@ export default {
89
120
  required: true
90
121
  }
91
122
  ],
92
- context: {}
123
+ requirements: getRequirements(),
124
+ context: {},
125
+ requirementProps: {},
126
+ fetchingRequirementProps: false
93
127
  };
94
128
  },
95
129
  computed: {
96
- disabled: function () {
97
- return this.doc.hasErrors;
130
+ loaded() {
131
+ return this.mounted && this.beforeCreateFinished;
132
+ },
133
+ disabled() {
134
+ return this.doc.hasErrors || !!this.beforeSubmitRequirements.find(requirement => !requirement.done);
135
+ },
136
+ beforeSubmitRequirements() {
137
+ return this.requirements.filter(requirement => requirement.phase === 'beforeSubmit');
138
+ },
139
+ // The currently active requirement expecting a solo presentation.
140
+ // Currently it only concerns `afterPasswordVerified` requirements.
141
+ // beforeSubmit requirements are not presented solo.
142
+ activeSoloRequirement() {
143
+ return (this.phase === 'afterPasswordVerified') &&
144
+ this.requirements.find(requirement =>
145
+ (requirement.phase === 'afterPasswordVerified') && !requirement.done
146
+ );
147
+ }
148
+ },
149
+ watch: {
150
+ async activeSoloRequirement(newVal) {
151
+ if (
152
+ (this.phase === 'afterPasswordVerified') &&
153
+ (newVal?.phase === 'afterPasswordVerified') &&
154
+ newVal.propsRequired &&
155
+ !(newVal.success || newVal.error)
156
+ ) {
157
+ try {
158
+ this.fetchingRequirementProps = true;
159
+ const data = await apos.http.post(`${apos.login.action}/requirement-props`, {
160
+ busy: true,
161
+ body: {
162
+ name: newVal.name,
163
+ incompleteToken: this.incompleteToken
164
+ }
165
+ });
166
+ this.requirementProps = {
167
+ ...this.requirementProps,
168
+ [newVal.name]: data
169
+ };
170
+ } catch (e) {
171
+ this.error = e.message || 'apostrophe:loginErrorGeneric';
172
+ } finally {
173
+ this.fetchingRequirementProps = false;
174
+ }
175
+ } else {
176
+ return null;
177
+ }
98
178
  }
99
179
  },
100
- async beforeCreate () {
180
+ async beforeCreate() {
101
181
  const stateChange = parseInt(window.sessionStorage.getItem('aposStateChange'));
102
182
  const seen = JSON.parse(window.sessionStorage.getItem('aposStateChangeSeen') || '{}');
103
183
  if (!seen[window.location.href]) {
@@ -110,15 +190,18 @@ export default {
110
190
  }
111
191
  }
112
192
  try {
113
- this.context = await apos.http.get(`${apos.login.action}/context`, {
193
+ this.context = await apos.http.post(`${apos.login.action}/context`, {
114
194
  busy: true
115
195
  });
196
+ this.requirementProps = this.context.requirementProps;
116
197
  } catch (e) {
117
- this.error = 'An error occurred. Please try again.';
198
+ this.error = e.message || 'apostrophe:loginErrorGeneric';
199
+ } finally {
200
+ this.beforeCreateFinished = true;
118
201
  }
119
202
  },
120
203
  mounted() {
121
- this.loaded = true;
204
+ this.mounted = true;
122
205
  },
123
206
  methods: {
124
207
  async submit() {
@@ -127,27 +210,148 @@ export default {
127
210
  }
128
211
  this.busy = true;
129
212
  this.error = '';
213
+
214
+ await this.invokeInitialLoginApi();
215
+ },
216
+ async invokeInitialLoginApi() {
217
+ try {
218
+ const response = await apos.http.post(`${apos.login.action}/login`, {
219
+ busy: true,
220
+ body: {
221
+ ...this.doc.data,
222
+ requirements: this.getInitialSubmitRequirementsData(),
223
+ session: true
224
+ }
225
+ });
226
+ if (response && response.incompleteToken) {
227
+ this.incompleteToken = response.incompleteToken;
228
+ this.phase = 'afterPasswordVerified';
229
+ } else {
230
+ this.redirectAfterLogin();
231
+ }
232
+ } catch (e) {
233
+ this.error = e.message || 'An error occurred. Please try again.';
234
+ this.phase = 'beforeSubmit';
235
+ this.requirements = getRequirements();
236
+ } finally {
237
+ this.busy = false;
238
+ }
239
+ },
240
+ getInitialSubmitRequirementsData() {
241
+ return Object.fromEntries(this.requirements.filter(r => r.phase !== 'afterPasswordVerified').map(r => ([
242
+ r.name,
243
+ r.value
244
+ ])));
245
+ },
246
+ async invokeFinalLoginApi() {
130
247
  try {
131
248
  await apos.http.post(`${apos.login.action}/login`, {
132
249
  busy: true,
133
250
  body: {
134
251
  ...this.doc.data,
252
+ incompleteToken: this.incompleteToken,
253
+ requirements: this.getFinalSubmitRequirementsData(),
135
254
  session: true
136
255
  }
137
256
  });
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}/`);
257
+ this.redirectAfterLogin();
143
258
  } catch (e) {
144
259
  this.error = e.message || 'An error occurred. Please try again.';
260
+ this.requirements = getRequirements();
261
+ this.phase = 'beforeSubmit';
145
262
  } finally {
146
263
  this.busy = false;
147
264
  }
265
+ },
266
+ getFinalSubmitRequirementsData() {
267
+ return Object.fromEntries(this.requirements.filter(r => r.phase === 'afterPasswordVerified').map(r => ([
268
+ r.name,
269
+ r.value
270
+ ])));
271
+ },
272
+ redirectAfterLogin() {
273
+ window.sessionStorage.setItem('aposStateChange', Date.now());
274
+ window.sessionStorage.setItem('aposStateChangeSeen', '{}');
275
+ // TODO handle situation where user should be sent somewhere other than homepage.
276
+ // Redisplay homepage with editing interface
277
+ location.assign(`${apos.prefix}/`);
278
+ },
279
+ async requirementBlock(requirementBlock) {
280
+ const requirement = this.requirements.find(requirement => requirement.name === requirementBlock.name);
281
+ requirement.done = false;
282
+ requirement.value = undefined;
283
+ },
284
+ async requirementDone(requirementDone, value) {
285
+ const requirement = this.requirements.find(requirement => requirement.name === requirementDone.name);
286
+
287
+ if (requirement.phase === 'beforeSubmit') {
288
+ requirement.done = true;
289
+ requirement.value = value;
290
+ return;
291
+ }
292
+
293
+ requirement.error = null;
294
+
295
+ try {
296
+ await apos.http.post(`${apos.login.action}/requirement-verify`, {
297
+ busy: true,
298
+ body: {
299
+ name: requirement.name,
300
+ value,
301
+ incompleteToken: this.incompleteToken
302
+ }
303
+ });
304
+
305
+ requirement.success = true;
306
+ } catch (err) {
307
+ requirement.error = err;
308
+ }
309
+
310
+ // Avoids the need for a deep watch
311
+ this.requirements = [ ...this.requirements ];
312
+
313
+ if (requirement.success && !requirement.askForConfirmation) {
314
+ requirement.done = true;
315
+
316
+ if (!this.activeSoloRequirement) {
317
+ await this.invokeFinalLoginApi();
318
+ }
319
+ }
320
+ },
321
+
322
+ async requirementConfirmed (requirementConfirmed) {
323
+ const requirement = this.requirements
324
+ .find(requirement => requirement.name === requirementConfirmed.name);
325
+
326
+ requirement.done = true;
327
+
328
+ if (!this.activeSoloRequirement) {
329
+ await this.invokeFinalLoginApi();
330
+ }
331
+ },
332
+ getRequirementProps(name) {
333
+ return this.requirementProps[name] || {};
148
334
  }
149
335
  }
150
336
  };
337
+
338
+ function getRequirements() {
339
+ const requirements = Object.entries(apos.login.requirements).map(([ name, requirement ]) => {
340
+ return {
341
+ name,
342
+ component: requirement.component || name,
343
+ ...requirement,
344
+ done: false,
345
+ value: null,
346
+ success: null,
347
+ error: null
348
+ };
349
+ });
350
+ return [
351
+ ...requirements.filter(r => r.phase === 'beforeSubmit'),
352
+ ...requirements.filter(r => r.phase === 'afterPasswordVerified')
353
+ ];
354
+ }
151
355
  </script>
152
356
 
153
357
  <style lang="scss">
@@ -171,13 +375,15 @@ export default {
171
375
 
172
376
  .fade-stage-enter-to,
173
377
  .fade-body-enter-to,
174
- .fade-footer-enter-to {
378
+ .fade-footer-enter-to,
379
+ .fade-body-leave {
175
380
  opacity: 1;
176
381
  }
177
382
 
178
383
  .fade-stage-enter,
179
384
  .fade-body-enter,
180
- .fade-footer-enter {
385
+ .fade-footer-enter,
386
+ .fade-body-leave-to {
181
387
  opacity: 0;
182
388
  }
183
389
 
@@ -186,11 +392,16 @@ export default {
186
392
  transition-delay: 0.6s;
187
393
  }
188
394
 
189
- .fade-body-enter-to {
395
+ .fade-leave-active {
396
+ transition: all 0.25s linear;
397
+ transition-delay: 0;
398
+ }
399
+
400
+ .fade-body-enter-to,.fade-body-leave {
190
401
  transform: translateY(0);
191
402
  }
192
403
 
193
- .fade-body-enter {
404
+ .fade-body-enter, .fade-body-leave-to {
194
405
  transform: translateY(4px);
195
406
  }
196
407
 
@@ -212,48 +423,6 @@ export default {
212
423
  margin: 0 auto;
213
424
  }
214
425
 
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
426
  form {
258
427
  position: relative;
259
428
  display: flex;
@@ -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: 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>
@@ -1430,8 +1430,8 @@ database.`);
1430
1430
  },
1431
1431
  // If the page will 404 according to the `isFound` method, this
1432
1432
  // method will emit a `notFound` event giving all modules a chance
1433
- // to intercept the request. If none intercept it, the standard 404 behavior
1434
- // is set up.
1433
+ // to intercept the request. If none intercept it, the standard 404
1434
+ // behavior is set up.
1435
1435
  async serveNotFound(req) {
1436
1436
  if (self.isFound(req)) {
1437
1437
  return;
@@ -1446,7 +1446,7 @@ database.`);
1446
1446
  // Simulate what this looks like when the serve page route starts.
1447
1447
  // This is an object, not an array
1448
1448
  params: {
1449
- 0: req.path
1449
+ 0: decodeURIComponent(req.path)
1450
1450
  },
1451
1451
  query: req.query,
1452
1452
  mode: 'draft',
@@ -25,13 +25,14 @@ export default {
25
25
  escapeValue: false
26
26
  },
27
27
  appendNamespaceToMissingKey: true,
28
+ defaultNS: [ apos.i18n.defaultNamespace ],
28
29
  parseMissingKeyHandler (key) {
29
30
  // We include namespaces with unrecognized l10n keys using
30
31
  // `appendNamespaceToMissingKey: true`. This passes strings containing
31
32
  // colons that were never meant to be localized through to the UI.
32
33
  //
33
34
  // Strings that do not include colons ("Content area") are given the
34
- // default namespace by i18next ("translation," by default). Here we
35
+ // default namespace by i18next ("default," in Apostrophe). Here we
35
36
  // check if the key starts with that default namespace, meaning it
36
37
  // belongs to no other registered namespace, then remove that default
37
38
  // namespace before passing this through to be processed and displayed.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apostrophe",
3
- "version": "3.12.0",
3
+ "version": "3.13.0",
4
4
  "description": "The Apostrophe Content Management System.",
5
5
  "main": "index.js",
6
6
  "scripts": {