@ditojs/admin 3.2.5 → 3.2.7

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ditojs/admin",
3
- "version": "3.2.5",
3
+ "version": "3.2.7",
4
4
  "type": "module",
5
5
  "description": "Dito.js Admin is a schema based admin interface for Dito.js Server, featuring auto-generated views and forms and built with Vue.js",
6
6
  "repository": "https://github.com/ditojs/dito/tree/main/packages/admin",
@@ -41,8 +41,8 @@
41
41
  "not ie_mob > 0"
42
42
  ],
43
43
  "dependencies": {
44
- "@ditojs/ui": "^3.2.0",
45
- "@ditojs/utils": "^3.2.0"
44
+ "@ditojs/ui": "^3.2.6",
45
+ "@ditojs/utils": "^3.2.6"
46
46
  },
47
47
  "peerDependencies": {
48
48
  "@kyvg/vue3-notification": "^3.4.2",
@@ -87,7 +87,7 @@
87
87
  "vue-upload-component": "^3.2.2"
88
88
  },
89
89
  "devDependencies": {
90
- "@ditojs/build": "^3.2.0",
90
+ "@ditojs/build": "^3.2.6",
91
91
  "@kyvg/vue3-notification": "^3.4.2",
92
92
  "@tiptap/core": "^3.31.4",
93
93
  "@tiptap/extension-blockquote": "^3.31.4",
@@ -131,5 +131,5 @@
131
131
  "vue-router": "^5.3.1",
132
132
  "vue-upload-component": "^3.2.2"
133
133
  },
134
- "gitHead": "f149113bc95317c74b17f4af56ccfebed789c47b"
134
+ "gitHead": "88a3df9bbd036594e886ea6169cef3dfd4b10fe7"
135
135
  }
package/src/DitoAdmin.js CHANGED
@@ -150,9 +150,10 @@ export default class DitoAdmin {
150
150
  components: {
151
151
  DitoRoot,
152
152
  VueNotifications,
153
- // This may only be needed to avoid tree-shacking of these components,
154
- // since they actually handle registry internally already.
155
- // TODO: Remove this once we have a better solution.
153
+ // Registered globally too, so that the custom components of apps can
154
+ // use them in their templates: Unlike the admin's own components, they
155
+ // don't extend `DitoComponent`, which shares its registry. This also
156
+ // keeps the modules from being tree-shaken.
156
157
  ...components,
157
158
  ...types
158
159
  },
@@ -5,7 +5,7 @@
5
5
  template(
6
6
  v-for="(buttonSchema, buttonDataPath) in buttonSchemas"
7
7
  )
8
- DitoContainer(
8
+ DitoContainer.dito-buttons__container(
9
9
  v-if="shouldRenderSchema(buttonSchema)"
10
10
  :key="buttonDataPath"
11
11
  :schema="buttonSchema"
@@ -21,7 +21,7 @@
21
21
  )
22
22
  //- Render each node in the default slot through `dito-vnode`,
23
23
  //- so it can be wrapped in a `.dito-container` class.
24
- .dito-container(
24
+ .dito-container.dito-buttons__container(
25
25
  v-if="hasVNodeContent(vnode)"
26
26
  )
27
27
  DitoVnode(:vnode="vnode")
@@ -86,8 +86,7 @@ export default DitoComponent.component('DitoButtons', {
86
86
 
87
87
  <style lang="scss">
88
88
  .dito-buttons {
89
- > .dito-container {
90
- padding: 0;
91
- }
89
+ // The containers of the buttons don't add padding around them.
90
+ --container-padding: 0px;
92
91
  }
93
92
  </style>
@@ -159,6 +159,7 @@ export default DitoComponent.component('DitoCreateButton', {
159
159
  .dito-pulldown {
160
160
  right: 0;
161
161
 
162
+ // TODO: BEM: Use a modifier of this block for sticky button groups.
162
163
  .dito-buttons--sticky & {
163
164
  top: unset;
164
165
  right: unset;
@@ -8,7 +8,7 @@
8
8
  @mouseup="onMouseUp"
9
9
  )
10
10
  UseFocusTrap.dito-dialog__focus-trap(:options="focusTrapOptions")
11
- form.dito-scroll-parent(
11
+ form.dito-dialog__form.dito-scroll-parent(
12
12
  @submit.prevent="submit"
13
13
  @keydown.enter="onEnter"
14
14
  )
@@ -209,6 +209,8 @@ export default DitoComponent.component('DitoDialog', {
209
209
  @import '../styles/_imports';
210
210
 
211
211
  .dito-dialog {
212
+ $self: &;
213
+
212
214
  position: fixed;
213
215
  display: flex;
214
216
  inset: 0;
@@ -231,8 +233,7 @@ export default DitoComponent.component('DitoDialog', {
231
233
  order: -1;
232
234
  }
233
235
 
234
- // TODO: `&__inner`
235
- form {
236
+ &__form {
236
237
  position: relative;
237
238
  display: flex;
238
239
  box-sizing: border-box;
@@ -242,23 +243,23 @@ export default DitoComponent.component('DitoDialog', {
242
243
  max-height: 100%;
243
244
  box-shadow: 0 20px 60px -2px rgb(27, 33, 58, 0.4);
244
245
  }
245
- }
246
246
 
247
- .dito-dialog-enter-active,
248
- .dito-dialog-leave-active {
249
- transition: opacity 0.15s;
247
+ &-enter-active,
248
+ &-leave-active {
249
+ transition: opacity 0.15s;
250
250
 
251
- form {
252
- transition: transform 0.25s;
251
+ #{$self}__form {
252
+ transition: transform 0.25s;
253
+ }
253
254
  }
254
- }
255
255
 
256
- .dito-dialog-enter-from,
257
- .dito-dialog-leave-to {
258
- opacity: 0;
256
+ &-enter-from,
257
+ &-leave-to {
258
+ opacity: 0;
259
259
 
260
- form {
261
- transform: translateY(-20px);
260
+ #{$self}__form {
261
+ transform: translateY(-20px);
262
+ }
262
263
  }
263
264
  }
264
265
  </style>
@@ -311,8 +311,6 @@ export default DitoComponent.component('DitoForm', {
311
311
  if (
312
312
  param &&
313
313
  this.providesData &&
314
- // TODO: See if we can remove this due to `flush: 'post'`.
315
- from.matched[0].path === to.matched[0].path && // Staying on same form
316
314
  from.params[param] !== 'create' && // But haven't been creating
317
315
  to.params[param] !== from.params[param] // Going to a different entity
318
316
  ) {
@@ -152,6 +152,7 @@ export default DitoComponent.component('DitoLabel', {
152
152
  transform: translateY(-4px);
153
153
  }
154
154
 
155
+ // TODO: BEM: Style an element class of this block, e.g. `&__buttons`.
155
156
  .dito-buttons {
156
157
  // Move the label padding inside .dito-buttons, so that it captures all
157
158
  // near mouse events:
@@ -349,6 +349,7 @@ export default DitoComponent.component('DitoPane', {
349
349
  }
350
350
  }
351
351
 
352
+ // TODO: BEM: Style an element class of this block, e.g. `&__buttons`.
352
353
  &__main + .dito-buttons--main {
353
354
  // Needed forms with sticky main buttons.
354
355
  margin: $content-padding;
@@ -244,6 +244,7 @@ export default DitoComponent.component('DitoPanel', {
244
244
  }
245
245
  }
246
246
 
247
+ // TODO: BEM: Style an element class of this block, e.g. `&__buttons`.
247
248
  .dito-buttons {
248
249
  position: absolute;
249
250
  right: $input-padding-ver;
@@ -266,6 +267,7 @@ export default DitoComponent.component('DitoPanel', {
266
267
  padding: 0;
267
268
  }
268
269
 
270
+ // TODO: BEM: Style an element class of this block, e.g. `&__buttons`.
269
271
  > .dito-buttons {
270
272
  --button-margin: #{$form-spacing};
271
273
 
@@ -975,6 +975,7 @@ function compareByDocumentPosition(component1, component2) {
975
975
  margin-bottom: 0;
976
976
  }
977
977
 
978
+ // TODO: BEM: Style an element class of this block, e.g. `&__buttons`.
978
979
  > .dito-buttons {
979
980
  margin-left: var(--button-margin, 0);
980
981
  }
@@ -97,6 +97,7 @@
97
97
  :draggable="childrenDraggable"
98
98
  :label="getItemLabel(childrenSchema, item.data, { index })"
99
99
  :level="level + 1"
100
+ @delete="deleteChild(index)"
100
101
  )
101
102
  //- TODO: Convert dito-tree-item to use dito-label internally, and then
102
103
  //- pass `asObject: true` in the `getItemLabel()` call above.
@@ -115,7 +116,7 @@ import { stripHtml } from '@ditojs/utils'
115
116
  // @vue/component
116
117
  export default DitoComponent.component('DitoTreeItem', {
117
118
  mixins: [ItemMixin, SortableMixin],
118
- emits: ['update:data'],
119
+ emits: ['update:data', 'delete'],
119
120
  inject: ['container'],
120
121
 
121
122
  props: {
@@ -302,8 +303,56 @@ export default DitoComponent.component('DitoTreeItem', {
302
303
  this.editPath(this.path)
303
304
  },
304
305
 
306
+ // Items are removed from the children of their parent, see
307
+ // `deleteChild()`.
305
308
  onDelete() {
306
- // TODO: Implement!
309
+ this.$emit('delete')
310
+ },
311
+
312
+ // Removes the child after confirming it, like inlined lists do. Open forms
313
+ // of the child or of its later siblings are closed, as the removal shifts
314
+ // the indices in their paths. The removal is transient, as the tree is
315
+ // part of its form's data.
316
+ deleteChild(index) {
317
+ const { childrenSchema, container } = this
318
+ const item = this.childrenList[index]
319
+ const label = this.getItemLabel(childrenSchema, item, {
320
+ index,
321
+ extended: true
322
+ })
323
+ if (
324
+ window.confirm(`Do you really want to ${this.verbs.delete} ${label}?`)
325
+ ) {
326
+ if (this.isEditingChildFrom(index)) {
327
+ this.$router.push({
328
+ path: container.path,
329
+ query: this.$route.query
330
+ })
331
+ }
332
+ this.childrenList = this.childrenList.toSpliced(index, 1)
333
+ this.onChange()
334
+ this.notify({
335
+ type: 'info',
336
+ title: 'Successfully Removed',
337
+ text: [`${label} was ${this.verbs.deleted}.`, container.transientNote]
338
+ })
339
+ }
340
+ },
341
+
342
+ // Returns whether the form of a child at the index or after it is open,
343
+ // or the form of one of their own children.
344
+ isEditingChildFrom(index) {
345
+ const childrenPath = this.childrenSchema.path
346
+ if (!childrenPath) {
347
+ return false
348
+ }
349
+ const prefix = `${this.path}/${childrenPath}/`
350
+ const { editPath } = this.container
351
+ if (!editPath.startsWith(prefix)) {
352
+ return false
353
+ }
354
+ const [childIndex] = editPath.slice(prefix.length).split('/')
355
+ return Number(childIndex) >= index
307
356
  },
308
357
 
309
358
  onChange() {
@@ -391,6 +440,7 @@ export default DitoComponent.component('DitoTreeItem', {
391
440
  color: rgba($color-black, 0.2);
392
441
  }
393
442
 
443
+ // TODO: BEM: Style an element class of this block, e.g. `&__buttons`.
394
444
  .dito-buttons {
395
445
  flex: auto;
396
446
  display: flex;
@@ -5,6 +5,7 @@ import { getMultipleValue } from '../utils/schema/data.js'
5
5
  import { getSchemaAccessor } from '../utils/accessor.js'
6
6
  import { setTemporaryId, isReference } from '../utils/data.js'
7
7
  import {
8
+ asArray,
8
9
  isObject,
9
10
  isArray,
10
11
  isString,
@@ -46,7 +47,12 @@ export default {
46
47
  if (!this.relate) {
47
48
  return value
48
49
  }
49
- const option = this.getOptionForValue(value)
50
+ // Keep related objects without options, see
51
+ // `DitoTypeMultiselect.getFallbackOption()`.
52
+ const option = (
53
+ this.getOptionForValue(value) ??
54
+ this.getRelatedObject(value)
55
+ )
50
56
  if (this.shouldSetTemporaryId(option)) {
51
57
  // Options without ids, e.g. new items of the edited data, get
52
58
  // temporary ids when selected, so that the relation can reference
@@ -325,6 +331,14 @@ export default {
325
331
  : value
326
332
  },
327
333
 
334
+ // Returns the related object in the value with the given value of its
335
+ // option, e.g. its id.
336
+ getRelatedObject(value) {
337
+ return asArray(this.value).find(
338
+ object => this.getValueForOption(object) === value
339
+ )
340
+ },
341
+
328
342
  getValueForOption(option) {
329
343
  if (this.shouldSetTemporaryId(option)) {
330
344
  // Until they're selected and get temporary ids, options without ids
@@ -26,6 +26,12 @@ export default {
26
26
  },
27
27
 
28
28
  computed: {
29
+ // Whether the component is part of a form, e.g. a list that loads its own
30
+ // data, unlike forms and the lists of views.
31
+ isInsideForm() {
32
+ return !this.isForm && !!this.formComponent
33
+ },
34
+
29
35
  resourceComponent() {
30
36
  return this
31
37
  },
@@ -215,9 +221,15 @@ export default {
215
221
  this.onFilterErrors?.(data.errors)
216
222
  ) {
217
223
  return true
218
- } else if (this.isUnauthorizedError(response)) {
219
- // TODO: Can we really swallow these errors?
220
- // Is calling `ensureUser()` in `onBeforeRequest()` enough?
224
+ } else if (
225
+ this.isUnauthorizedError(response) &&
226
+ this.isInsideForm
227
+ ) {
228
+ // The user is logged in, see `sendRequest()`, but may not access
229
+ // the resource of a component in a form, e.g. a list that loads
230
+ // its own data. Show it on the component, as several components
231
+ // of the form may be affected. Others notify like for errors.
232
+ this.addUnauthorizedError()
221
233
  return true
222
234
  }
223
235
  }
@@ -242,6 +254,13 @@ export default {
242
254
  return response?.status === 401
243
255
  },
244
256
 
257
+ addUnauthorizedError() {
258
+ const error = `You don't have permission to load this data.`
259
+ if (!this.errors?.includes(error)) {
260
+ this.addError(error)
261
+ }
262
+ },
263
+
245
264
  async handleRequest(
246
265
  {
247
266
  method,
@@ -109,10 +109,53 @@
109
109
  cursor: grabbing;
110
110
  }
111
111
  }
112
+
113
+ .dito-buttons--large & {
114
+ border-radius: 2em;
115
+ padding: $button-round-padding;
116
+ }
117
+
118
+ // Buttons in round and small button groups show icons instead of text.
119
+ // For now, nothing for create: &--create:empty::before
120
+ .dito-buttons--round &,
121
+ .dito-buttons--small & {
122
+ &--add:empty::before {
123
+ @extend %icon-add;
124
+ }
125
+
126
+ &--upload:empty::before {
127
+ @extend %icon-upload;
128
+ }
129
+
130
+ &--delete:empty::before,
131
+ &--remove:empty::before {
132
+ @extend %icon-remove;
133
+ }
134
+
135
+ &--edit:empty::before {
136
+ @extend %icon-edit;
137
+ }
138
+
139
+ &--clear:empty::before {
140
+ @extend %icon-clear;
141
+ }
142
+
143
+ &--drag:empty::before {
144
+ @extend %icon-drag;
145
+ }
146
+
147
+ &--copy:empty::before {
148
+ @extend %icon-copy;
149
+ }
150
+
151
+ &--paste:empty::before {
152
+ @extend %icon-paste;
153
+ }
154
+ }
112
155
  }
113
156
 
114
157
  .dito-buttons {
115
- // TODO: BEM
158
+ $self: &;
116
159
 
117
160
  &--large {
118
161
  --button-margin: 3px;
@@ -122,15 +165,10 @@
122
165
  justify-content: center;
123
166
  padding-bottom: $content-padding;
124
167
 
125
- .dito-container {
168
+ #{$self}__container {
126
169
  // Do not specify this on .dito-buttons directly as it would break borders
127
170
  margin-top: $content-padding;
128
171
  }
129
-
130
- .dito-button {
131
- border-radius: 2em;
132
- padding: $button-round-padding;
133
- }
134
172
  }
135
173
 
136
174
  &--main {
@@ -152,43 +190,4 @@
152
190
  display: none;
153
191
  }
154
192
  }
155
-
156
- &--round,
157
- &--small {
158
- // For now, nothing for create:
159
- // .dito-button--create:empty::before
160
-
161
- .dito-button--add:empty::before {
162
- @extend %icon-add;
163
- }
164
-
165
- .dito-button--upload:empty::before {
166
- @extend %icon-upload;
167
- }
168
-
169
- .dito-button--delete:empty::before,
170
- .dito-button--remove:empty::before {
171
- @extend %icon-remove;
172
- }
173
-
174
- .dito-button--edit:empty::before {
175
- @extend %icon-edit;
176
- }
177
-
178
- .dito-button--clear:empty::before {
179
- @extend %icon-clear;
180
- }
181
-
182
- .dito-button--drag:empty::before {
183
- @extend %icon-drag;
184
- }
185
-
186
- .dito-button--copy:empty::before {
187
- @extend %icon-copy;
188
- }
189
-
190
- .dito-button--paste:empty::before {
191
- @extend %icon-paste;
192
- }
193
- }
194
193
  }
@@ -47,10 +47,10 @@ export default DitoTypeComponent.register('code', {
47
47
  default: false
48
48
  }),
49
49
 
50
+ // The height of the lines is set through `--lines` and the styles, which
51
+ // add the padding, unless the editor was resized to a height of its own.
50
52
  style() {
51
- return {
52
- height: this.height || `calc(${this.lines}em * var(--line-height))`
53
- }
53
+ return this.height ? { height: this.height } : { '--lines': this.lines }
54
54
  }
55
55
  },
56
56
 
@@ -175,6 +175,9 @@ export default DitoTypeComponent.register('code', {
175
175
  // For proper sizing of content along with :style="style" setting above,
176
176
  // for proper line-height calculation.
177
177
  padding: $input-padding;
178
+ height: calc(
179
+ var(--lines) * 1em * var(--line-height) + 2 * #{$input-padding-ver}
180
+ );
178
181
  min-height: calc(1em * var(--line-height) + 2 * $input-padding-ver);
179
182
 
180
183
  .codeflask {
@@ -613,6 +613,7 @@ const LinkWithTitle = Link.extend({
613
613
  padding-right: $input-padding-hor;
614
614
  }
615
615
 
616
+ // TODO: BEM: Style an element class of this block, e.g. `&__buttons`.
616
617
  .dito-buttons--toolbar {
617
618
  margin: 0;
618
619
  }
@@ -37,6 +37,21 @@
37
37
  @tag="onAddTag"
38
38
  @search-change="onSearchChange"
39
39
  )
40
+ //- The default tag, which marks values without options.
41
+ template(#tag="{ option, remove }")
42
+ span.multiselect__tag.dito-multiselect__tag(
43
+ :class=`{
44
+ 'dito-multiselect__tag--unavailable': isUnavailableOption(option)
45
+ }`
46
+ :title="isUnavailableOption(option) ? 'Not among the options' : null"
47
+ @mousedown.prevent
48
+ )
49
+ span {{ getLabelForOption(option) }}
50
+ i.multiselect__tag-icon(
51
+ tabindex="1"
52
+ @keydown.enter.prevent="remove(option)"
53
+ @mousedown.prevent="remove(option)"
54
+ )
40
55
  DitoAffixes(
41
56
  :items="schema.suffix"
42
57
  position="suffix"
@@ -70,7 +85,7 @@ import OptionsMixin from '../mixins/OptionsMixin.js'
70
85
  import DitoAffixes from '../components/DitoAffixes.vue'
71
86
  import VueMultiselect from 'vue-multiselect'
72
87
  import { getSchemaAccessor } from '../utils/accessor.js'
73
- import { isBoolean } from '@ditojs/utils'
88
+ import { isArray, isBoolean, isObject, isString } from '@ditojs/utils'
74
89
 
75
90
  // @vue/component
76
91
  export default DitoTypeComponent.register('multiselect', {
@@ -90,18 +105,22 @@ export default DitoTypeComponent.register('multiselect', {
90
105
  computed: {
91
106
  selectedOptions: {
92
107
  get() {
108
+ // Values without options stay visible once the options are loaded,
109
+ // and are kept when the selection changes, see `getFallbackOption()`,
110
+ // unless the multiselect is taggable and can add them as options.
93
111
  return this.multiple
94
- ? (this.selectedValue || [])
95
- .map(
96
- // If an option cannot be found, we may be in taggable mode and
97
- // can add it.
98
- value => (
99
- this.getOptionForValue(value) || this.addTagOption(value)
112
+ ? this.hasOptions && isArray(this.value)
113
+ ? this.value.map(value => {
114
+ const selectedValue = this.relate
115
+ ? this.getValueForOption(value)
116
+ : value
117
+ return (
118
+ this.getOptionForValue(selectedValue) ||
119
+ this.addTagOption(selectedValue) ||
120
+ this.getFallbackOption(value)
100
121
  )
101
- )
102
- // Filter out options that we couldn't match.
103
- // TODO: Should we display an error instead?
104
- .filter(Boolean)
122
+ })
123
+ : []
105
124
  : this.selectedOption
106
125
  },
107
126
 
@@ -196,6 +215,27 @@ export default DitoTypeComponent.register('multiselect', {
196
215
  }
197
216
  },
198
217
 
218
+ // Returns whether the option is a fallback option for a value without
219
+ // one, see `getFallbackOption()`. Plain options are their own values.
220
+ isUnavailableOption(option) {
221
+ return this.optionValue
222
+ ? !this.getOptionForValue(this.getValueForOption(option))
223
+ : !this.options.includes(option)
224
+ },
225
+
226
+ // Returns an option for a value without one, e.g. of options that only
227
+ // hold the results of searches: Related objects are their own options,
228
+ // other values get an option that holds them as value and label.
229
+ getFallbackOption(value) {
230
+ const { optionValue, optionLabel } = this
231
+ return isObject(value) || !isString(optionValue)
232
+ ? value
233
+ : {
234
+ [optionValue]: value,
235
+ ...(isString(optionLabel) && { [optionLabel]: `${value}` })
236
+ }
237
+ },
238
+
199
239
  focusElement() {
200
240
  this.$refs.element.activate()
201
241
  },
@@ -289,6 +329,16 @@ $tag-line-height: 1em;
289
329
  display: inline-flex;
290
330
  position: relative;
291
331
 
332
+ &__tag {
333
+ color: $color-text-inverted;
334
+ background: $color-active;
335
+
336
+ // Values without options, see `getFallbackOption()`.
337
+ &--unavailable {
338
+ background: $color-grey;
339
+ }
340
+ }
341
+
292
342
  &__inner {
293
343
  flex: 1;
294
344
  position: relative;
@@ -463,11 +513,6 @@ $tag-line-height: 1em;
463
513
  }
464
514
  }
465
515
 
466
- &__tag {
467
- color: $color-text-inverted;
468
- background: $color-active;
469
- }
470
-
471
516
  &__tag-icon {
472
517
  background: none;
473
518
  border-radius: 1em;
@@ -6,7 +6,9 @@
6
6
  :scopes="scopes"
7
7
  )
8
8
  .dito-tree-panel
9
+ //- Objects that are `null` have no tree, as tree items can't create them.
9
10
  DitoTreeItem(
11
+ v-if="treeData"
10
12
  :schema="treeSchema"
11
13
  :dataPath="treeDataPath"
12
14
  :data="treeData"
@@ -17,7 +17,7 @@ import {
17
17
  // e.g. `data: ({ item: { id } }) => async () => load(id)`, so that loading
18
18
  // only starts again when these dependencies change.
19
19
  // - `dataPath` is resolved relative to the data path of the context, in its
20
- // root data, e.g. `'../fontFamily/fontShopSets'`.
20
+ // root data, e.g. `'../author/books'`.
21
21
  // - Data schemas that aren't objects are values, e.g. `options: ['A', 'B']`.
22
22
  //
23
23
  // While a value is loading, `value` is `undefined` and `isLoading` is `true`.
@@ -44,8 +44,8 @@ const ditoOptionKeys = [
44
44
  'processSchema',
45
45
  'getPanelSchema',
46
46
  'getFormSchemasForProcessing',
47
- // Vue 3 / Vue-router 4 forgets these.
48
- // TODO: Create bug-report?
47
+ // vue-router reads route guards from the raw component options, without
48
+ // their mixins, see `extractComponentsGuards()` in its `navigationGuards.ts`.
49
49
  'beforeRouteUpdate',
50
50
  'beforeRouteLeave'
51
51
  ]