apostrophe 3.31.0 → 3.32.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 (59) hide show
  1. package/.eslintrc +3 -0
  2. package/CHANGELOG.md +21 -1
  3. package/modules/@apostrophecms/asset/index.js +65 -7
  4. package/modules/@apostrophecms/asset/lib/webpack/utils.js +242 -28
  5. package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocEditor.vue +26 -16
  6. package/modules/@apostrophecms/i18n/i18n/en.json +17 -3
  7. package/modules/@apostrophecms/i18n/i18n/es.json +1 -0
  8. package/modules/@apostrophecms/i18n/i18n/fr.json +1 -0
  9. package/modules/@apostrophecms/i18n/i18n/pt-BR.json +2 -1
  10. package/modules/@apostrophecms/i18n/i18n/sk.json +1 -0
  11. package/modules/@apostrophecms/i18n/ui/apos/components/AposI18nLocalize.vue +41 -20
  12. package/modules/@apostrophecms/login/index.js +123 -26
  13. package/modules/@apostrophecms/login/ui/apos/components/AposForgotPasswordForm.vue +124 -0
  14. package/modules/@apostrophecms/login/ui/apos/components/AposLoginForm.vue +339 -0
  15. package/modules/@apostrophecms/login/ui/apos/components/AposResetPasswordForm.vue +163 -0
  16. package/modules/@apostrophecms/login/ui/apos/components/TheAposLogin.vue +135 -293
  17. package/modules/@apostrophecms/login/ui/apos/components/TheAposLoginHeader.vue +65 -14
  18. package/modules/@apostrophecms/login/ui/apos/mixins/AposLoginFormMixin.js +45 -0
  19. package/modules/@apostrophecms/login/views/passwordResetEmail.html +9 -0
  20. package/modules/@apostrophecms/modal/ui/apos/components/AposModal.vue +17 -6
  21. package/modules/@apostrophecms/modal/ui/apos/components/AposModalBody.vue +4 -1
  22. package/modules/@apostrophecms/modal/ui/apos/components/AposModalTabs.vue +7 -1
  23. package/modules/@apostrophecms/piece-type/index.js +1 -1
  24. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue +4 -3
  25. package/modules/@apostrophecms/rich-text-widget/ui/apos/tiptap-extensions/Default.js +11 -5
  26. package/modules/@apostrophecms/rich-text-widget/ui/apos/tiptap-extensions/Heading.js +6 -2
  27. package/modules/@apostrophecms/rich-text-widget/ui/apos/tiptap-extensions/Link.js +7 -4
  28. package/modules/@apostrophecms/schema/lib/addFieldTypes.js +1 -1
  29. package/modules/@apostrophecms/schema/ui/apos/components/AposInputWrapper.vue +4 -1
  30. package/modules/@apostrophecms/schema/ui/apos/components/AposSchema.vue +4 -0
  31. package/modules/@apostrophecms/template/index.js +11 -12
  32. package/modules/@apostrophecms/template/lib/bundlesLoader.js +20 -5
  33. package/modules/@apostrophecms/ui/ui/apos/mixins/AposAdvisoryLockMixin.js +2 -1
  34. package/modules/@apostrophecms/widget-type/index.js +17 -3
  35. package/package.json +1 -1
  36. package/test/assets.js +338 -25
  37. package/test/extra_node_modules/@company/bundle/index.js +8 -0
  38. package/test/extra_node_modules/@company/bundle/ui/src/company.js +3 -0
  39. package/test/extra_node_modules/@company/bundle/ui/src/company.scss +3 -0
  40. package/test/login.js +427 -12
  41. package/test/modules/@company/bundle/index.js +10 -0
  42. package/test/modules/@company/bundle/ui/src/company.js +3 -0
  43. package/test/modules/@company/bundle/ui/src/company.scss +3 -0
  44. package/test/modules/bundle-edge/index.js +10 -0
  45. package/test/modules/bundle-edge/ui/src/edge.js +3 -0
  46. package/test/modules/bundle-edge/ui/src/edge.scss +3 -0
  47. package/test/modules/bundle-page/index.js +2 -1
  48. package/test/modules/bundle-page/ui/src/extra.js +3 -1
  49. package/test/modules/bundle-page/ui/src/extra.scss +3 -0
  50. package/test/modules/bundle-page/ui/src/main.js +3 -0
  51. package/test/modules/bundle-page/ui/src/main.scss +3 -0
  52. package/test/modules/bundle-page-type/index.js +12 -0
  53. package/test/modules/bundle-page-type/ui/src/another.js +3 -0
  54. package/test/modules/bundle-page-type/ui/src/index.js +3 -0
  55. package/test/modules/bundle-page-type/ui/src/index.scss +3 -0
  56. package/test/modules/bundle-page-type/ui/src/main.js +3 -0
  57. package/test/modules/bundle-page-type/ui/src/main.scss +3 -0
  58. package/test/modules/bundle-widget/ui/src/extra2.js +3 -1
  59. package/test-lib/test.js +9 -1
@@ -6,75 +6,46 @@
6
6
  v-show="loaded"
7
7
  :class="themeClass"
8
8
  >
9
+ <transition name="fade-outer">
10
+ <div v-if="showNav" class="apos-login__nav">
11
+ <a
12
+ href="#"
13
+ class="apos-login__link apos-login--arrow-left"
14
+ @click.prevent="setStage('login')"
15
+ >{{ $t('apostrophe:loginBack') }}</a>
16
+ <a
17
+ :href="homeUrl"
18
+ class="apos-login__link apos-login--arrow-right"
19
+ >{{ $t('apostrophe:loginHome') }}</a>
20
+ </div>
21
+ </transition>
9
22
  <div class="apos-login__wrapper">
10
23
  <transition name="fade-body" mode="out-in">
11
- <div
12
- key="1"
13
- class="apos-login__upper"
14
- v-if="loaded && phase === 'beforeSubmit'"
15
- >
16
- <TheAposLoginHeader
17
- :env="context.env"
18
- :name="context.name"
19
- :error="$t(error)"
20
- />
21
-
22
- <div class="apos-login__body">
23
- <form @submit.prevent="submit">
24
- <AposSchema
25
- :schema="schema"
26
- v-model="doc"
27
- />
28
- <Component
29
- v-for="requirement in beforeSubmitRequirements"
30
- :key="requirement.name"
31
- :is="requirement.component"
32
- v-bind="getRequirementProps(requirement.name)"
33
- @done="requirementDone(requirement, $event)"
34
- @block="requirementBlock(requirement)"
35
- />
36
- <!-- TODO -->
37
- <!-- <a href="#" class="apos-login__link">Forgot Password</a> -->
38
- <AposButton
39
- data-apos-test="loginSubmit"
40
- :busy="busy"
41
- :disabled="disabled"
42
- type="primary"
43
- label="apostrophe:login"
44
- button-type="submit"
45
- class="apos-login__submit"
46
- :modifiers="['gradient-on-hover', 'block']"
47
- @click="submit"
48
- />
49
- </form>
50
- </div>
51
- </div>
52
- <div
53
- key="2"
54
- class="apos-login__upper"
55
- v-else-if="activeSoloRequirement"
56
- >
57
- <TheAposLoginHeader
58
- :env="context.env"
59
- :name="context.name"
60
- :error="$t(error)"
61
- :tiny="true"
62
- />
63
- <div class="apos-login__body">
64
- <Component
65
- v-if="!fetchingRequirementProps"
66
- v-bind="getRequirementProps(activeSoloRequirement.name)"
67
- :is="activeSoloRequirement.component"
68
- :success="activeSoloRequirement.success"
69
- :error="activeSoloRequirement.error"
70
- @done="requirementDone(activeSoloRequirement, $event)"
71
- @confirm="requirementConfirmed(activeSoloRequirement)"
72
- />
73
- </div>
74
- </div>
24
+ <AposForgotPasswordForm
25
+ v-if="loaded && stage === 'forgotPassword'"
26
+ :context="context"
27
+ :context-error="error"
28
+ @redirect="onRedirect"
29
+ @set-stage="setStage"
30
+ />
31
+ <AposResetPasswordForm
32
+ v-else-if="loaded && stage === 'resetPassword'"
33
+ :context="context"
34
+ :data="passwordResetData"
35
+ :context-error="error"
36
+ @redirect="onRedirect"
37
+ @set-stage="setStage"
38
+ />
39
+ <AposLoginForm
40
+ v-else-if="loaded"
41
+ :context="context"
42
+ :context-error="error"
43
+ @redirect="onRedirect"
44
+ @set-stage="setStage"
45
+ />
75
46
  </transition>
76
47
  </div>
77
- <transition name="fade-footer">
48
+ <transition name="fade-outer">
78
49
  <div class="apos-login__footer" v-show="loaded">
79
50
  <AposLogo class="apos-login__logo" />
80
51
  <label class="apos-login__project-version">
@@ -89,94 +60,38 @@
89
60
  <script>
90
61
  import AposThemeMixin from 'Modules/@apostrophecms/ui/mixins/AposThemeMixin';
91
62
 
63
+ const STAGES = [
64
+ 'login',
65
+ 'forgotPassword',
66
+ 'resetPassword'
67
+ ];
68
+
92
69
  export default {
93
70
  name: 'TheAposLogin',
94
71
  mixins: [ AposThemeMixin ],
95
72
  data() {
96
73
  return {
97
- phase: 'beforeSubmit',
74
+ stage: STAGES[0],
98
75
  mounted: false,
99
76
  beforeCreateFinished: false,
100
77
  error: '',
101
- busy: false,
102
- doc: {
103
- data: {},
104
- hasErrors: false
105
- },
106
- schema: [
107
- {
108
- name: 'username',
109
- label: 'Username',
110
- placeholder: 'Enter username',
111
- type: 'string',
112
- required: true
113
- },
114
- {
115
- name: 'password',
116
- label: 'Password',
117
- placeholder: 'Enter password',
118
- type: 'password',
119
- required: true
120
- }
121
- ],
122
- requirements: getRequirements(),
123
- context: {},
124
- requirementProps: {},
125
- fetchingRequirementProps: false
78
+ passwordResetData: {},
79
+ context: {}
126
80
  };
127
81
  },
128
82
  computed: {
129
83
  loaded() {
130
84
  return this.mounted && this.beforeCreateFinished;
131
85
  },
132
- disabled() {
133
- return this.doc.hasErrors ||
134
- !!this.beforeSubmitRequirements.find(requirement => !requirement.done);
135
- },
136
- beforeSubmitRequirements() {
137
- return this.requirements.filter(requirement => requirement.phase === 'beforeSubmit');
86
+ showNav() {
87
+ return this.stage !== STAGES[0];
138
88
  },
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
- }
89
+ homeUrl() {
90
+ return `${apos.prefix}/`;
178
91
  }
179
92
  },
93
+ // We need it here and not in the login form because the version used in the footer.
94
+ // The context will be passed to every form, might be a good thing in the future.
180
95
  async beforeCreate() {
181
96
  const stateChange = parseInt(window.sessionStorage.getItem('aposStateChange'));
182
97
  const seen = JSON.parse(window.sessionStorage.getItem('aposStateChangeSeen') || '{}');
@@ -193,167 +108,59 @@ export default {
193
108
  this.context = await apos.http.post(`${apos.login.action}/context`, {
194
109
  busy: true
195
110
  });
196
- this.requirementProps = this.context.requirementProps;
197
111
  } catch (e) {
112
+ this.context = {};
198
113
  this.error = e.message || 'apostrophe:loginErrorGeneric';
199
114
  } finally {
200
115
  this.beforeCreateFinished = true;
201
116
  }
202
117
  },
118
+ created() {
119
+ const url = new URL(document.location);
120
+ const data = {
121
+ email: url.searchParams.get('email'),
122
+ reset: url.searchParams.get('reset')
123
+ };
124
+ if (data.email && data.reset) {
125
+ this.passwordResetData = data;
126
+ this.setStage('resetPassword');
127
+ }
128
+ },
203
129
  mounted() {
204
130
  this.mounted = true;
205
131
  },
206
132
  methods: {
207
- async submit() {
208
- if (this.busy) {
133
+ setStage(name) {
134
+ // 1. Enabled status per stage. A bit cryptic but effective.
135
+ // Search for a method composed of the `name` + `Enabled`
136
+ // (e.g. `forgotPasswordEnabled` and execute it (should return boolean).
137
+ // If no method is found it is enabled. Fallback to the default stage.
138
+ const enabled = this[`${name}Enabled`]?.() ?? true;
139
+ if (!enabled) {
140
+ this.stage = STAGES[0];
209
141
  return;
210
142
  }
211
- this.busy = true;
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
- } finally {
236
- this.busy = false;
143
+ // 2. Set it only if it's a known stage
144
+ if (STAGES.includes(name)) {
145
+ this.stage = name;
146
+ return;
237
147
  }
148
+ // 3. Fallback to the default stage
149
+ this.stage = STAGES[0];
238
150
  },
239
- getInitialSubmitRequirementsData() {
240
- return Object.fromEntries(this.requirements
241
- .filter(r => r.phase !== 'afterPasswordVerified' || !r.done)
242
- .map(r => ([
243
- r.name,
244
- r.value
245
- ])));
246
- },
247
- async invokeFinalLoginApi() {
248
- try {
249
- await apos.http.post(`${apos.login.action}/login`, {
250
- busy: true,
251
- body: {
252
- ...this.doc.data,
253
- incompleteToken: this.incompleteToken,
254
- requirements: this.getFinalSubmitRequirementsData(),
255
- session: true
256
- }
257
- });
258
- this.redirectAfterLogin();
259
- } catch (e) {
260
- this.error = e.message || 'An error occurred. Please try again.';
261
- this.phase = 'beforeSubmit';
262
- } finally {
263
- this.busy = false;
264
- }
151
+ forgotPasswordEnabled() {
152
+ return apos.login.passwordResetEnabled;
265
153
  },
266
- getFinalSubmitRequirementsData() {
267
- return Object.fromEntries(this.requirements.filter(r => r.phase === 'afterPasswordVerified').map(r => ([
268
- r.name,
269
- r.value
270
- ])));
154
+ resetPasswordEnabled() {
155
+ return apos.login.passwordResetEnabled;
271
156
  },
272
- redirectAfterLogin() {
157
+ onRedirect(loc) {
273
158
  window.sessionStorage.setItem('aposStateChange', Date.now());
274
159
  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
281
- .find(requirement => requirement.name === requirementBlock.name);
282
- requirement.done = false;
283
- requirement.value = undefined;
284
- },
285
- async requirementDone(requirementDone, value) {
286
- const requirement = this.requirements
287
- .find(requirement => requirement.name === requirementDone.name);
288
-
289
- if (requirement.phase === 'beforeSubmit') {
290
- requirement.done = true;
291
- requirement.value = value;
292
- return;
293
- }
294
-
295
- requirement.error = null;
296
-
297
- try {
298
- await apos.http.post(`${apos.login.action}/requirement-verify`, {
299
- busy: true,
300
- body: {
301
- name: requirement.name,
302
- value,
303
- incompleteToken: this.incompleteToken
304
- }
305
- });
306
-
307
- requirement.success = true;
308
- } catch (err) {
309
- requirement.error = err;
310
- }
311
-
312
- // Avoids the need for a deep watch
313
- this.requirements = [ ...this.requirements ];
314
-
315
- if (requirement.success && !requirement.askForConfirmation) {
316
- requirement.done = true;
317
-
318
- if (!this.activeSoloRequirement) {
319
- await this.invokeFinalLoginApi();
320
- }
321
- }
322
- },
323
-
324
- async requirementConfirmed (requirementConfirmed) {
325
- const requirement = this.requirements
326
- .find(requirement => requirement.name === requirementConfirmed.name);
327
-
328
- requirement.done = true;
329
-
330
- if (!this.activeSoloRequirement) {
331
- await this.invokeFinalLoginApi();
332
- }
333
- },
334
- getRequirementProps(name) {
335
- return this.requirementProps[name] || {};
160
+ location.assign(loc);
336
161
  }
337
162
  }
338
163
  };
339
-
340
- function getRequirements() {
341
- const requirements = Object.entries(apos.login.requirements).map(([ name, requirement ]) => {
342
- return {
343
- name,
344
- component: requirement.component || name,
345
- ...requirement,
346
- done: false,
347
- value: null,
348
- success: null,
349
- error: null
350
- };
351
- });
352
- return [
353
- ...requirements.filter(r => r.phase === 'beforeSubmit'),
354
- ...requirements.filter(r => r.phase === 'afterPasswordVerified')
355
- ];
356
- }
357
164
  </script>
358
165
 
359
166
  <style lang="scss">
@@ -377,14 +184,14 @@ function getRequirements() {
377
184
 
378
185
  .fade-stage-enter-to,
379
186
  .fade-body-enter-to,
380
- .fade-footer-enter-to,
187
+ .fade-outer-enter-to,
381
188
  .fade-body-leave {
382
189
  opacity: 1;
383
190
  }
384
191
 
385
192
  .fade-stage-enter,
386
193
  .fade-body-enter,
387
- .fade-footer-enter,
194
+ .fade-outer-enter,
388
195
  .fade-body-leave-to {
389
196
  opacity: 0;
390
197
  }
@@ -406,7 +213,7 @@ function getRequirements() {
406
213
  transform: translateY(4px);
407
214
  }
408
215
 
409
- .fade-footer-enter-active {
216
+ .fade-outer-enter-active {
410
217
  transition: opacity 0.4s linear;
411
218
  transition-delay: 1s;
412
219
  }
@@ -418,22 +225,61 @@ function getRequirements() {
418
225
  height: 100vh;
419
226
  background-color: var(--a-background-primary);
420
227
 
421
- &__wrapper {
422
- width: 100%;
423
- max-width: $login-container;
424
- margin: 0 auto;
228
+ &__nav {
229
+ position: absolute;
230
+ top: 0;
231
+ right: 0;
232
+ left: 0;
233
+ display: flex;
234
+ justify-content: space-between;
235
+ align-items: center;
236
+ padding: $spacing-triple;
425
237
  }
426
238
 
427
- form {
428
- position: relative;
429
- display: flex;
430
- flex-direction: column;
239
+ &__link {
240
+ @include type-large;
241
+ display: inline-block;
242
+ text-decoration: underline;
243
+ text-underline-offset: 2px;
431
244
 
432
- button {
433
- margin-top: $spacing-double;
245
+ &:hover,
246
+ &:focus,
247
+ &:active {
248
+ color: var(--a-text-primary);
434
249
  }
435
250
  }
436
251
 
252
+ &--arrow-left,
253
+ &--arrow-right {
254
+ display: flex;
255
+ align-items: center;
256
+ justify-content: center;
257
+ gap: $spacing-half;
258
+ }
259
+
260
+ &--arrow-left::before,
261
+ &--arrow-right::after {
262
+ content: '';
263
+ width: 3px;
264
+ height: 3px;
265
+ border: solid var(--a-text-primary);
266
+ border-width: 3px 3px 0 0;
267
+ }
268
+
269
+ &--arrow-right::after {
270
+ transform: rotate(45deg);
271
+ }
272
+
273
+ &--arrow-left::before {
274
+ transform: rotate(-135deg);
275
+ }
276
+
277
+ &__wrapper {
278
+ width: 100%;
279
+ max-width: $login-container;
280
+ margin: 0 auto;
281
+ }
282
+
437
283
  &__loader {
438
284
  display: flex;
439
285
  flex-direction: column;
@@ -472,8 +318,4 @@ function getRequirements() {
472
318
  margin-left: auto;
473
319
  }
474
320
  }
475
-
476
- .apos-login__submit ::v-deep .apos-button {
477
- height: 47px;
478
- }
479
321
  </style>
@@ -1,16 +1,30 @@
1
1
  <template>
2
2
  <div
3
3
  class="apos-login__header"
4
- :class="{'apos-login__header--tiny': tiny}"
4
+ :class="{'apos-login__header--tiny': tiny, 'apos-login__header--center': !!subtitle}"
5
5
  >
6
+ <div class="apos-login__project-header">
7
+ <label
8
+ class="apos-login__project apos-login__project-env"
9
+ :class="[`apos-login__project-env--${env}`]"
10
+ >
11
+ {{ env }}
12
+ </label>
13
+ <label
14
+ v-if="subtitle"
15
+ class="apos-login__project apos-login__project-subtitle"
16
+ >
17
+ {{ subtitle }}
18
+ </label>
19
+ </div>
20
+ <label class="apos-login__project apos-login__project-name">
21
+ {{ title }}
22
+ </label>
6
23
  <label
7
- class="apos-login__project apos-login__project-env"
8
- :class="[`apos-login__project-env--${env}`]"
24
+ v-if="help"
25
+ class="apos-login--help"
9
26
  >
10
- {{ env }}
11
- </label>
12
- <label class="apos-login__project apos-login__project-name">
13
- {{ name }}
27
+ {{ help }}
14
28
  </label>
15
29
  <label class="apos-login--error">
16
30
  {{ error }}
@@ -26,24 +40,32 @@ export default {
26
40
  type: String,
27
41
  default: ''
28
42
  },
29
- name: {
43
+ title: {
30
44
  type: String,
31
45
  default: ''
32
46
  },
33
- tiny: {
34
- type: Boolean,
35
- default: false
47
+ subtitle: {
48
+ type: String,
49
+ default: ''
50
+ },
51
+ help: {
52
+ type: String,
53
+ default: ''
36
54
  },
37
55
  error: {
38
56
  type: String,
39
57
  default: ''
58
+ },
59
+ tiny: {
60
+ type: Boolean,
61
+ default: false
40
62
  }
41
63
  }
42
64
  };
43
65
  </script>
44
66
  <style scoped lang='scss'>
45
-
46
67
  .apos-login {
68
+ $this: &;
47
69
 
48
70
  &__header {
49
71
  z-index: $z-index-manager-display;
@@ -51,7 +73,22 @@ export default {
51
73
  flex-direction: column;
52
74
  justify-content: center;
53
75
  align-items: flex-start;
54
- width: max-content;
76
+ }
77
+
78
+ &__project-header {
79
+ display: flex;
80
+ gap: $spacing-base;
81
+ flex-wrap: nowrap;
82
+ margin-bottom: 15px;
83
+ }
84
+
85
+ &__project-subtitle {
86
+ @include type-title;
87
+ margin: 0;
88
+ opacity: 0.6;
89
+ line-height: 1;
90
+ text-transform: capitalize;
91
+ white-space: nowrap;
55
92
  }
56
93
 
57
94
  &__project-name {
@@ -68,7 +105,6 @@ export default {
68
105
  color: var(--a-white);
69
106
  background: var(--a-success);
70
107
  border-radius: 5px;
71
- margin-bottom: 15px;
72
108
 
73
109
  &--development {
74
110
  background: var(--a-danger);
@@ -79,6 +115,13 @@ export default {
79
115
  }
80
116
  }
81
117
 
118
+ &--help {
119
+ @include type-label;
120
+ margin-top: $spacing-double;
121
+ text-align: center;
122
+ white-space: pre-line;
123
+ }
124
+
82
125
  &--error {
83
126
  @include type-help;
84
127
  color: var(--a-danger);
@@ -87,6 +130,14 @@ export default {
87
130
  margin-bottom: 15px;
88
131
  }
89
132
 
133
+ &__header--center {
134
+ align-items: center;
135
+
136
+ #{$this}__project-header {
137
+ margin-bottom: $spacing-triple;
138
+ }
139
+ }
140
+
90
141
  &__header--tiny {
91
142
  flex-direction: row;
92
143
  // stylelint-disable-next-line scale-unlimited/declaration-strict-value
@@ -0,0 +1,45 @@
1
+ // Mixin for login form related common behavior.
2
+
3
+ export default {
4
+ props: {
5
+ contextError: {
6
+ type: String,
7
+ default: ''
8
+ },
9
+ context: {
10
+ type: Object,
11
+ default: function() {
12
+ return {};
13
+ }
14
+ }
15
+ },
16
+ data() {
17
+ return {
18
+ error: '',
19
+ doc: {
20
+ data: {},
21
+ hasErrors: false
22
+ }
23
+ };
24
+ },
25
+ computed: {
26
+ passwordResetEnabled() {
27
+ return apos.login.passwordResetEnabled;
28
+ }
29
+ },
30
+ watch: {
31
+ contextError(newVal) {
32
+ // Copy it only once
33
+ if (!this.contextErrorReceived && newVal && !this.error) {
34
+ this.error = newVal;
35
+ this.contextErrorReceived = true;
36
+ }
37
+ }
38
+ },
39
+ mounted() {
40
+ this.error = this.contextError;
41
+ if (this.contextError) {
42
+ this.contextErrorReceived = true;
43
+ }
44
+ }
45
+ };