@fishawack/lab-velocity 2.0.0-beta.5 → 2.0.0-beta.50

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 (111) hide show
  1. package/README.md +467 -36
  2. package/_Build/js/libs/build-id.js +14 -0
  3. package/_Build/js/libs/filters.js +36 -0
  4. package/_Build/js/libs/globals.js +7 -0
  5. package/_Build/js/libs/router.js +22 -0
  6. package/_Build/js/libs/routes.js +29 -0
  7. package/_Build/js/libs/store.js +21 -0
  8. package/_Build/js/libs/utility.js +161 -0
  9. package/_Build/vue/components/basic/Button.vue +1 -1
  10. package/_Build/vue/components/form/Avatar.vue +90 -0
  11. package/_Build/vue/components/form/Checkbox.vue +10 -0
  12. package/_Build/vue/components/form/InputNumber.vue +1 -1
  13. package/_Build/vue/components/form/Select.vue +223 -33
  14. package/_Build/vue/components/form/Spinner.vue +5 -0
  15. package/_Build/vue/components/layout/Alert.vue +5 -5
  16. package/_Build/vue/components/layout/Audit.vue +143 -0
  17. package/_Build/vue/{modules/AuthModule/components/VBreadcrumbs.vue → components/layout/Breadcrumbs.vue} +4 -4
  18. package/_Build/vue/{modules/AuthModule/components → components/layout}/Chips.vue +2 -2
  19. package/_Build/vue/components/layout/Footer.vue +11 -10
  20. package/_Build/vue/{modules/AuthModule/components/VFormFooter.vue → components/layout/FormFooter.vue} +13 -7
  21. package/_Build/vue/{modules/AuthModule/components → components/layout}/FormRole.vue +10 -8
  22. package/_Build/vue/components/layout/Layout.vue +94 -0
  23. package/_Build/vue/components/layout/Navigation.vue +77 -0
  24. package/_Build/vue/{modules/AuthModule/components/VPageHeader.vue → components/layout/PageHeader.vue} +14 -8
  25. package/_Build/vue/components/layout/SideBar.vue +26 -0
  26. package/_Build/vue/{modules/AuthModule/components/VTable.vue → components/layout/Table.vue} +37 -16
  27. package/_Build/vue/{modules/AuthModule/components/VTableSorter.vue → components/layout/TableSorter.vue} +108 -52
  28. package/_Build/vue/components/layout/TokenDisplay.vue +52 -0
  29. package/_Build/vue/components/layout/pageTitle.vue +1 -1
  30. package/_Build/vue/components/navigation/MenuItem.vue +7 -2
  31. package/_Build/vue/components/navigation/MenuItemGroup.vue +7 -2
  32. package/_Build/vue/modules/AuthModule/js/axios.js +21 -1
  33. package/_Build/vue/modules/AuthModule/js/guest-request.js +32 -0
  34. package/_Build/vue/modules/AuthModule/js/impersonation-banner.js +102 -0
  35. package/_Build/vue/modules/AuthModule/js/router.js +91 -114
  36. package/_Build/vue/modules/AuthModule/js/store.js +23 -6
  37. package/_Build/vue/modules/AuthModule/routes/PCompanies/columns.js +268 -0
  38. package/_Build/vue/modules/AuthModule/routes/PCompanies/resource.js +213 -0
  39. package/_Build/vue/modules/AuthModule/routes/PIntegrations/columns.js +58 -0
  40. package/_Build/vue/modules/AuthModule/routes/PIntegrations/resource.js +79 -0
  41. package/_Build/vue/modules/AuthModule/routes/PTeams/columns.js +78 -0
  42. package/_Build/vue/modules/AuthModule/routes/PTeams/resource.js +251 -0
  43. package/_Build/vue/modules/AuthModule/routes/PUsers/SetPasswordAction.vue +51 -0
  44. package/_Build/vue/modules/AuthModule/routes/PUsers/SetPasswordDialog.vue +138 -0
  45. package/_Build/vue/modules/AuthModule/routes/PUsers/columns.js +349 -0
  46. package/_Build/vue/modules/AuthModule/routes/PUsers/resource.js +239 -0
  47. package/_Build/vue/modules/AuthModule/routes/account-exists.vue +2 -2
  48. package/_Build/vue/modules/AuthModule/routes/change-password.vue +28 -32
  49. package/_Build/vue/modules/AuthModule/routes/container.vue +2 -11
  50. package/_Build/vue/modules/AuthModule/routes/expired-reset.vue +4 -4
  51. package/_Build/vue/modules/AuthModule/routes/expired-verification.vue +10 -9
  52. package/_Build/vue/modules/AuthModule/routes/force-reset.vue +44 -58
  53. package/_Build/vue/modules/AuthModule/routes/forgot.vue +10 -5
  54. package/_Build/vue/modules/AuthModule/routes/login.vue +12 -19
  55. package/_Build/vue/modules/AuthModule/routes/logincallback.vue +1 -3
  56. package/_Build/vue/modules/AuthModule/routes/loginsso.vue +14 -10
  57. package/_Build/vue/modules/AuthModule/routes/logout.vue +17 -5
  58. package/_Build/vue/modules/AuthModule/routes/logoutheadless.vue +1 -3
  59. package/_Build/vue/modules/AuthModule/routes/register.vue +24 -28
  60. package/_Build/vue/modules/AuthModule/routes/reset.vue +20 -14
  61. package/_Build/vue/modules/AuthModule/routes/success-forgot.vue +14 -8
  62. package/_Build/vue/modules/AuthModule/routes/success-reset.vue +2 -2
  63. package/_Build/vue/modules/AuthModule/routes/success-verify.vue +1 -3
  64. package/_Build/vue/modules/AuthModule/routes/verify.vue +11 -14
  65. package/_Build/vue/modules/resource/Children/create.vue +81 -0
  66. package/_Build/vue/modules/resource/Children/edit.vue +106 -0
  67. package/_Build/vue/modules/resource/Children/index.vue +42 -0
  68. package/_Build/vue/modules/resource/Children/partials/form.vue +111 -0
  69. package/_Build/vue/modules/resource/Children/show.vue +166 -0
  70. package/_Build/vue/modules/resource/index.js +561 -0
  71. package/_Build/vue/modules/resource/parent.vue +63 -0
  72. package/_Build/vue/modules/resource/trashable.js +104 -0
  73. package/_base.scss +0 -1
  74. package/_defaults.scss +2 -13
  75. package/_variables.scss +9 -4
  76. package/{modules/_AuthModule.scss → components/_auth.scss} +19 -68
  77. package/components/_datepicker.scss +1 -0
  78. package/components/_descriptions.scss +2 -0
  79. package/components/_footer.scss +1 -0
  80. package/components/_form.scss +18 -0
  81. package/components/_header.scss +3 -27
  82. package/components/_layout.scss +56 -0
  83. package/components/_menu.scss +0 -5
  84. package/components/_sidebar.scss +12 -27
  85. package/components/_table.scss +3 -0
  86. package/components/_token-display.scss +41 -0
  87. package/general.scss +1 -0
  88. package/index.js +31 -1
  89. package/package.json +7 -4
  90. package/vendor.scss +0 -1
  91. package/_Build/vue/components/layout/sideBar.vue +0 -25
  92. package/_Build/vue/modules/AuthModule/adminRoutes/PCompanies/Children/Upload/upload.vue +0 -251
  93. package/_Build/vue/modules/AuthModule/adminRoutes/PCompanies/Children/create.vue +0 -62
  94. package/_Build/vue/modules/AuthModule/adminRoutes/PCompanies/Children/edit.vue +0 -98
  95. package/_Build/vue/modules/AuthModule/adminRoutes/PCompanies/Children/index.vue +0 -90
  96. package/_Build/vue/modules/AuthModule/adminRoutes/PCompanies/Children/partials/form.vue +0 -173
  97. package/_Build/vue/modules/AuthModule/adminRoutes/PCompanies/Children/show.vue +0 -262
  98. package/_Build/vue/modules/AuthModule/adminRoutes/PCompanies/parent.vue +0 -36
  99. package/_Build/vue/modules/AuthModule/adminRoutes/PUsers/Children/create.vue +0 -112
  100. package/_Build/vue/modules/AuthModule/adminRoutes/PUsers/Children/edit.vue +0 -103
  101. package/_Build/vue/modules/AuthModule/adminRoutes/PUsers/Children/index.vue +0 -112
  102. package/_Build/vue/modules/AuthModule/adminRoutes/PUsers/Children/partials/form.vue +0 -169
  103. package/_Build/vue/modules/AuthModule/adminRoutes/PUsers/Children/show.vue +0 -120
  104. package/_Build/vue/modules/AuthModule/adminRoutes/PUsers/parent.vue +0 -36
  105. package/components/_input.scss +0 -0
  106. package/modules/_AuthVariables.scss +0 -7
  107. /package/_Build/vue/{modules/AuthModule/components → components/layout}/AuthModal.vue +0 -0
  108. /package/_Build/vue/{modules/AuthModule/components → components/layout}/Chip.vue +0 -0
  109. /package/_Build/vue/{modules/AuthModule/components/VPasswordValidation.vue → components/layout/PasswordValidation.vue} +0 -0
  110. /package/_Build/vue/{modules/AuthModule/components/VRoleLegend.vue → components/layout/RoleLegend.vue} +0 -0
  111. /package/{modules → components}/_modal.scss +0 -0
@@ -1,7 +1,8 @@
1
1
  <template>
2
2
  <el-table
3
3
  :data="$props.data"
4
- :height="fixedHeight ? 762 : false"
4
+ :height="fixedHeight ? 762 : undefined"
5
+ :row-class-name="rowClassName"
5
6
  style="width: 100%"
6
7
  @sort-change="handleSort"
7
8
  >
@@ -13,24 +14,25 @@
13
14
  <el-table-column
14
15
  :width="item.width ?? 'auto'"
15
16
  :sortable="!!item.sortable ? 'custom' : false"
16
- :label="item.label"
17
+ :label="
18
+ item.label ||
19
+ item.key[0].toUpperCase() + item.key.slice(1)
20
+ "
17
21
  :prop="item.key"
18
22
  :show-overflow-tooltip="true"
19
23
  >
20
24
  <!-- Support a custom render function -->
21
25
  <template v-if="item.render" #default="scope">
22
- {{ item.render(scope.row) }}
23
- </template>
24
-
25
- <!-- Support module passed (as a raw component or :is property) -->
26
- <template v-else-if="item.component" #default="scope">
27
26
  <component
28
27
  :is="
29
- item.component.is
30
- ? item.component.is(scope.row)
31
- : item.component.module(scope.row)
28
+ item.render(
29
+ {
30
+ model: scope.row,
31
+ ...additionalInfo,
32
+ },
33
+ this,
34
+ )
32
35
  "
33
- v-bind="item.component.props(scope.row)"
34
36
  />
35
37
  </template>
36
38
  </el-table-column>
@@ -47,18 +49,18 @@
47
49
  v-if="$props.displayShowAction"
48
50
  :to="$props.targetAction($props.data[scope.$index])"
49
51
  >
50
- <el-button size="small" type="primary">
52
+ <VelButton size="small" type="primary">
51
53
  {{ `View` }}
52
- </el-button>
54
+ </VelButton>
53
55
  </router-link>
54
56
  <router-link
55
57
  v-if="$props.displayEditAction"
56
58
  class="ml"
57
59
  :to="$props.editAction($props.data[scope.$index])"
58
60
  >
59
- <el-button size="small">
61
+ <VelButton size="small">
60
62
  {{ `Edit` }}
61
- </el-button>
63
+ </VelButton>
62
64
  </router-link>
63
65
  </template>
64
66
  </el-table-column>
@@ -67,9 +69,24 @@
67
69
  </template>
68
70
 
69
71
  <script>
72
+ import { ElTable, ElTableColumn } from "element-plus";
73
+ import VelButton from "../basic/Button.vue";
74
+
70
75
  export default {
71
76
  name: "VTable",
72
77
 
78
+ components: {
79
+ ElTable,
80
+ ElTableColumn,
81
+ VelButton,
82
+ },
83
+
84
+ inject: {
85
+ additionalInfo: {
86
+ default: () => ({}),
87
+ },
88
+ },
89
+
73
90
  props: {
74
91
  data: {
75
92
  type: [Array, Object],
@@ -115,8 +132,12 @@ export default {
115
132
  type: Boolean,
116
133
  default: true,
117
134
  },
135
+ rowClassName: {
136
+ type: [Function, String],
137
+ default: undefined,
138
+ },
118
139
  },
119
- emits: ["sort"],
140
+ emits: ["sort", "reload"],
120
141
 
121
142
  methods: {
122
143
  handleSort(data) {
@@ -13,7 +13,7 @@
13
13
  </p>
14
14
  </div>
15
15
  <div class="flex justify-end items-end grid__2/3">
16
- <el-basic
16
+ <VelBasic
17
17
  v-if="jsonData.searchable"
18
18
  ref="search"
19
19
  v-model="search"
@@ -25,19 +25,23 @@
25
25
  @update:model-value="handleSearch"
26
26
  >
27
27
  <template #prepend
28
- ><GIcon embed asis name="icon-search"
28
+ ><GIcon
29
+ embed
30
+ asis
31
+ name="icon-search"
32
+ class="icon--0.5"
29
33
  /></template>
30
- </el-basic>
34
+ </VelBasic>
31
35
  <slot v-if="displayActions" name="table-action">
32
- <el-button
33
- v-if="displayEditAction"
36
+ <VelButton
37
+ v-if="displayCreateAction"
34
38
  size="large"
35
39
  type="primary"
36
40
  tag="a"
37
41
  class="ml-2"
38
42
  @click="
39
43
  $router.push({
40
- name: `${jsonData.pageLink}.create`,
44
+ name: `${jsonData.slug}.create`,
41
45
  })
42
46
  "
43
47
  >
@@ -45,40 +49,42 @@
45
49
  name="icon-plus"
46
50
  embed
47
51
  asis
48
- class="fill-0 mr-0.5"
52
+ class="fill-0 icon--0.5 mr-0.5"
49
53
  />
50
54
  Create new {{ jsonData.label }}
51
- </el-button>
55
+ </VelButton>
52
56
  </slot>
53
57
  </div>
54
58
  </slot>
55
59
  </div>
56
60
  <div class="bg-0 p-0.5 box-shadow-1 border-r-4">
57
- <VTable
61
+ <VelTable
58
62
  :data="table_data"
59
63
  :structure="jsonData.tableStructure"
60
64
  :label="jsonData.label"
61
65
  :over-write-id="jsonData.overWriteId"
62
66
  :fixed-height="fixedHeight"
67
+ :row-class-name="jsonData.rowClassName"
63
68
  :target-action="
64
69
  (item) => ({
65
- name: `${jsonData.pageLink}.show`,
66
- params: { id: item.id },
70
+ name: `${jsonData.slug}.show`,
71
+ params: { [idKey]: item.id },
67
72
  })
68
73
  "
69
74
  :edit-action="
70
75
  (item) => ({
71
- name: `${jsonData.pageLink}.edit`,
72
- params: { id: item.id },
76
+ name: `${jsonData.slug}.edit`,
77
+ params: { [idKey]: item.id },
73
78
  })
74
79
  "
75
80
  :display-actions="displayActions"
76
81
  :display-show-action="displayShowAction"
77
82
  :display-edit-action="displayEditAction"
78
83
  @sort="handleSort"
84
+ @reload="reload"
79
85
  >
80
86
  <slot name="table-content"></slot>
81
- </VTable>
87
+ </VelTable>
82
88
 
83
89
  <div class="flex justify-center items-center py-2">
84
90
  <el-pagination
@@ -93,36 +99,42 @@
93
99
  </div>
94
100
  </div>
95
101
  <div v-else class="absolute transform-center text-center">
96
- <GSpinner class="fill-5" />
97
- <p v-text="`Loading...`" />
102
+ <VelSpinner />
98
103
  </div>
99
104
  </template>
100
105
 
101
106
  <script>
107
+ import axios from "axios";
108
+ import { debounce } from "lodash";
109
+ import { ElPagination } from "element-plus";
110
+ import VelButton from "../basic/Button.vue";
111
+ import VelBasic from "../form/basic.vue";
112
+ import VelSpinner from "../form/Spinner.vue";
113
+
102
114
  export default {
103
- name: "VTableSorter",
115
+ name: "TableSorter",
104
116
 
105
117
  components: {
106
- VTable: require("./VTable.vue").default,
118
+ VelTable: require("./Table.vue").default,
119
+ VelButton,
120
+ VelBasic,
121
+ ElPagination,
122
+ VelSpinner,
107
123
  },
108
124
 
109
125
  props: {
110
126
  jsonData: {
111
127
  required: true,
112
- type: Array,
128
+ type: Object,
113
129
  },
114
130
  fixedHeight: {
115
131
  default: true,
116
132
  type: Boolean,
117
133
  },
118
- api: {
119
- required: true,
120
- type: String,
121
- },
122
- defaults: {
134
+ apiParams: {
123
135
  required: false,
124
- type: String,
125
- default: "",
136
+ type: Object,
137
+ default: () => ({}),
126
138
  },
127
139
  displayActions: {
128
140
  type: Boolean,
@@ -132,44 +144,79 @@ export default {
132
144
  type: Boolean,
133
145
  default: true,
134
146
  },
147
+ displayCreateAction: {
148
+ type: Boolean,
149
+ default: true,
150
+ },
135
151
  displayEditAction: {
136
152
  type: Boolean,
137
153
  default: true,
138
154
  },
155
+ idKey: {
156
+ type: String,
157
+ default: "id",
158
+ },
139
159
  },
140
160
 
141
161
  data() {
142
162
  return {
143
163
  search: null,
144
164
  sort: "-id",
145
- query: [],
165
+ query: {},
146
166
  table_data: [],
147
167
  table_meta: null,
168
+ _abortController: null,
148
169
  };
149
170
  },
150
171
 
172
+ created() {
173
+ this.debouncedSearch = debounce(this._executeSearch, 300);
174
+ },
175
+
176
+ beforeUnmount() {
177
+ this.debouncedSearch.cancel();
178
+ this._abortController?.abort();
179
+ },
180
+
151
181
  mounted() {
152
182
  this.sort = this.jsonData.defaultSort || "-id";
153
183
  this.fetchData({ page: 1 }).then((data) => {
154
- this.table_data = data.data;
155
- this.table_meta = data.meta;
184
+ if (data) {
185
+ this.table_data = data.data;
186
+ this.table_meta = data.meta;
156
187
 
157
- this.table_curr_page = this.table_meta.current_page;
188
+ this.table_curr_page = this.table_meta.current_page;
189
+ }
158
190
  });
159
191
  },
160
192
 
161
193
  methods: {
194
+ reload() {
195
+ this.fetchData({ page: this.table_meta.current_page }).then(
196
+ (data) => {
197
+ if (data) {
198
+ this.table_data = data.data;
199
+ this.table_meta = data.meta;
200
+ }
201
+ },
202
+ );
203
+ },
162
204
  handleSearch(data) {
163
205
  if (data === null || data.length < 3) {
164
- this.query = [];
206
+ this.query = {};
165
207
  } else {
166
- this.query = [
167
- `filter[${this.$refs.search.$el.dataset.key}]=${data}`,
168
- ];
208
+ this.query = {
209
+ [`filter[${this.$refs.search.$el.dataset.key}]`]: data,
210
+ };
169
211
  }
212
+ this.debouncedSearch();
213
+ },
214
+ _executeSearch() {
170
215
  this.fetchData({}).then((data) => {
171
- this.table_data = data.data;
172
- this.table_meta = data.meta;
216
+ if (data) {
217
+ this.table_data = data.data;
218
+ this.table_meta = data.meta;
219
+ }
173
220
  });
174
221
  },
175
222
  handleSort(data) {
@@ -180,24 +227,31 @@ export default {
180
227
  data.order === "ascending" ? data.prop : "-" + data.prop;
181
228
  }
182
229
  this.fetchData({}).then((data) => {
183
- this.table_data = data.data;
184
- this.table_meta = data.meta;
230
+ if (data) {
231
+ this.table_data = data.data;
232
+ this.table_meta = data.meta;
233
+ }
185
234
  });
186
235
  },
187
236
 
188
237
  fetchData: function ({ page = "1" }) {
189
- const mergedQuery = [
190
- ...this.query,
191
- this.defaults,
192
- `sort=${this.sort}`,
193
- `page=${page}`,
194
- ];
195
-
196
- return this.$store.dispatch("getEntity", {
197
- entity: this.$props.api,
198
- query: mergedQuery.join("&"),
199
- errors: this.$root.errors,
200
- });
238
+ this._abortController?.abort();
239
+ this._abortController = new AbortController();
240
+
241
+ return axios
242
+ .get(`${this.$props.jsonData.api}`, {
243
+ params: {
244
+ sort: this.sort,
245
+ page: page,
246
+ ...this.query,
247
+ ...this.apiParams,
248
+ },
249
+ signal: this._abortController.signal,
250
+ })
251
+ .then((res) => res.data)
252
+ .catch((err) => {
253
+ if (!axios.isCancel(err)) console.log(err);
254
+ });
201
255
  },
202
256
 
203
257
  getStatusLabel(status) {
@@ -213,8 +267,10 @@ export default {
213
267
 
214
268
  handleCurrentPageChange(val) {
215
269
  this.fetchData({ page: val }).then((data) => {
216
- this.table_data = data.data;
217
- this.table_meta = data.meta;
270
+ if (data) {
271
+ this.table_data = data.data;
272
+ this.table_meta = data.meta;
273
+ }
218
274
  });
219
275
  },
220
276
 
@@ -0,0 +1,52 @@
1
+ <template>
2
+ <el-dialog
3
+ :model-value="true"
4
+ title="Token Created"
5
+ width="560px"
6
+ class="token-display"
7
+ :close-on-click-modal="false"
8
+ :close-on-press-escape="false"
9
+ :show-close="false"
10
+ >
11
+ <p class="token-display__warning">
12
+ <strong>The token below will not be shown again.</strong> Ensure
13
+ you've taken a copy before closing this window.
14
+ </p>
15
+
16
+ <div class="token-display__block">
17
+ <div class="token-display__header">
18
+ <span class="token-display__label">Bearer Token</span>
19
+ <el-button size="small" plain @click="copy">
20
+ {{ copied ? "Copied ✓" : "Copy" }}
21
+ </el-button>
22
+ </div>
23
+ <pre class="token-display__value">{{ token }}</pre>
24
+ </div>
25
+
26
+ <template #footer>
27
+ <el-button type="primary" @click="$emit('close')">Done</el-button>
28
+ </template>
29
+ </el-dialog>
30
+ </template>
31
+
32
+ <script setup>
33
+ import { ref } from "vue";
34
+ import { ElDialog, ElButton } from "element-plus";
35
+
36
+ const props = defineProps({
37
+ token: {
38
+ type: String,
39
+ required: true,
40
+ },
41
+ });
42
+
43
+ defineEmits(["close"]);
44
+
45
+ const copied = ref(false);
46
+
47
+ async function copy() {
48
+ await navigator.clipboard.writeText(props.token);
49
+ copied.value = true;
50
+ setTimeout(() => (copied.value = false), 2000);
51
+ }
52
+ </script>
@@ -6,7 +6,7 @@
6
6
 
7
7
  <script>
8
8
  export default {
9
- name: "VPageTitle",
9
+ name: "PageTitle",
10
10
 
11
11
  props: {
12
12
  title: {
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <el-menu-item :index="index" class="vel-menu-item" v-bind="$props">
2
+ <el-menu-item class="vel-menu-item" v-bind="$props" :index="`${index}`">
3
3
  <template #title>
4
4
  <slot name="title" />
5
5
  </template>
@@ -15,6 +15,11 @@ export default {
15
15
  components: {
16
16
  ElMenuItem,
17
17
  },
18
- props: ["index"],
18
+ props: {
19
+ index: {
20
+ type: [String, Number],
21
+ required: true,
22
+ },
23
+ },
19
24
  };
20
25
  </script>
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <el-sub-menu :index="index" v-bind="$props">
2
+ <el-sub-menu v-bind="$props" :index="`${index}`">
3
3
  <template #title>
4
4
  <slot name="title" />
5
5
  </template>
@@ -15,6 +15,11 @@ export default {
15
15
  components: {
16
16
  ElSubMenu,
17
17
  },
18
- props: ["index"],
18
+ props: {
19
+ index: {
20
+ type: [String, Number],
21
+ required: true,
22
+ },
23
+ },
19
24
  };
20
25
  </script>
@@ -1,4 +1,21 @@
1
1
  import axios from "axios";
2
+ import debounce from "lodash/debounce";
3
+
4
+ import { ElNotification } from "element-plus";
5
+
6
+ const displayErrorNotification = debounce(async function errors(e) {
7
+ if (e.response && !e.response.data.errors) {
8
+ ElNotification.error({
9
+ title: "Error",
10
+ message:
11
+ e.response.data.message ||
12
+ `${e.response.status}: ${
13
+ e.response.statusText || "Please try again later!"
14
+ }`,
15
+ duration: 10000,
16
+ });
17
+ }
18
+ }, 250);
2
19
 
3
20
  function setAxiosDefaults(baseUrl, router) {
4
21
  axios.defaults.baseURL = baseUrl;
@@ -12,7 +29,8 @@ function setAxiosDefaults(baseUrl, router) {
12
29
  axios.interceptors.response.use(null, (error) => {
13
30
  if (error.response) {
14
31
  if (
15
- error.response.status === 401 &&
32
+ (error.response.status === 401 ||
33
+ error.response.status === 419) &&
16
34
  router.currentRoute.value.name !== "auth.logout"
17
35
  ) {
18
36
  router.push({
@@ -25,6 +43,8 @@ function setAxiosDefaults(baseUrl, router) {
25
43
  }
26
44
  }
27
45
 
46
+ displayErrorNotification(error);
47
+
28
48
  return Promise.reject(error);
29
49
  });
30
50
 
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ import axios from "axios";
3
+
4
+ /**
5
+ * Wraps a form POST to a guest-only route with automatic logged-in detection.
6
+ *
7
+ * If the server redirects to /hydrate/logged-in (because a stale session
8
+ * exists), the helper dispatches a logout, fetches a fresh CSRF cookie,
9
+ * and retries the original request once.
10
+ *
11
+ * @param {object} options
12
+ * @param {object} options.form - form-backend-validation Form instance
13
+ * @param {string} options.url - endpoint to POST to
14
+ * @param {string} [options.method] - HTTP method (default: "post")
15
+ * @param {import('vuex').Store} options.store - Vuex store (for dispatch("logout"))
16
+ * @returns {Promise<object>} resolved response data
17
+ */
18
+ export async function guestRequest({ form, url, method = "post", store }) {
19
+ const res = await form[method](url);
20
+
21
+ if (res && res["logged-in"]) {
22
+ try {
23
+ await store.dispatch("logout");
24
+ } catch (_) {}
25
+
26
+ await axios.get("/sanctum/csrf-cookie");
27
+
28
+ return await form[method](url);
29
+ }
30
+
31
+ return res;
32
+ }
@@ -0,0 +1,102 @@
1
+ "use strict";
2
+
3
+ import { createApp, h, ref } from "vue";
4
+
5
+ let bannerApp = null;
6
+ let bannerContainer = null;
7
+ let storeRef = null;
8
+ const currentUser = ref(null);
9
+
10
+ function mountBanner() {
11
+ if (bannerApp) return;
12
+
13
+ bannerContainer = document.createElement("div");
14
+ document.body.insertBefore(bannerContainer, document.body.firstChild);
15
+
16
+ bannerApp = createApp({
17
+ setup() {
18
+ function stop() {
19
+ storeRef.dispatch("stopImpersonating");
20
+ }
21
+
22
+ return () =>
23
+ h(
24
+ "div",
25
+ {
26
+ style: {
27
+ background: "#c0392b",
28
+ color: "#fff",
29
+ display: "flex",
30
+ alignItems: "center",
31
+ justifyContent: "center",
32
+ gap: "16px",
33
+ padding: "12px 16px",
34
+ fontSize: "14px",
35
+ fontFamily: "sans-serif",
36
+ },
37
+ },
38
+ [
39
+ h(
40
+ "span",
41
+ `Impersonating: ${currentUser.value?.name} (${currentUser.value?.email})`,
42
+ ),
43
+ h(
44
+ "button",
45
+ {
46
+ onClick: stop,
47
+ style: {
48
+ background: "#fff",
49
+ color: "#c0392b",
50
+ border: "none",
51
+ padding: "4px 12px",
52
+ borderRadius: "4px",
53
+ cursor: "pointer",
54
+ fontWeight: "bold",
55
+ },
56
+ },
57
+ "Stop impersonating",
58
+ ),
59
+ ],
60
+ );
61
+ },
62
+ });
63
+
64
+ bannerApp.mount(bannerContainer);
65
+ }
66
+
67
+ function unmountBanner() {
68
+ if (!bannerApp) return;
69
+
70
+ bannerApp.unmount();
71
+ bannerContainer?.remove();
72
+ bannerApp = null;
73
+ bannerContainer = null;
74
+ }
75
+
76
+ export function syncImpersonationBanner(user) {
77
+ currentUser.value = user;
78
+
79
+ if (user?.impersonating_as) {
80
+ mountBanner();
81
+ } else {
82
+ unmountBanner();
83
+ }
84
+ }
85
+
86
+ export function setImpersonationStore(store) {
87
+ storeRef = store;
88
+ }
89
+
90
+ export function ImpersonationPlugin(store) {
91
+ setImpersonationStore(store);
92
+
93
+ // Handle persisted state on load (vuex-persistedstate bypasses mutations)
94
+ syncImpersonationBanner(store.state.auth?.user ?? store.state.user);
95
+
96
+ // Handle all future setUser mutations
97
+ store.subscribe((mutation) => {
98
+ if (mutation.type === "auth/setUser" || mutation.type === "setUser") {
99
+ syncImpersonationBanner(mutation.payload);
100
+ }
101
+ });
102
+ }