@userfrosting/sprinkle-core 6.0.0-alpha.5 → 6.0.0-alpha.6

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.
@@ -1,2 +1,3 @@
1
- export { useSprunjer } from './sprunjer'
1
+ export { useSprunjer } from './useSprunjer'
2
2
  export { useCsrf } from './useCsrf'
3
+ export { useAxiosInterceptor } from './useAxiosInterceptor'
@@ -0,0 +1,30 @@
1
+ import axios from 'axios'
2
+ import { useAlertsStore } from '../stores/useAlertsStore'
3
+ import { Severity } from '../interfaces'
4
+
5
+ /**
6
+ * Axios Error Handler
7
+ *
8
+ * This composable sets up an Axios interceptor to handle errors globally using
9
+ * the Alerts store. It sends the error to the Alerts store, unless it's a 401
10
+ * error.
11
+ *
12
+ * @see https://axios-http.com/docs/interceptors
13
+ */
14
+ export const useAxiosInterceptor = () => {
15
+ axios.interceptors.response.use(
16
+ (response) => response,
17
+ (error) => {
18
+ if (error.response.status !== 401) {
19
+ const alertStore = useAlertsStore()
20
+ alertStore.push({
21
+ title: error.response.data.title ?? error.response?.statusText,
22
+ description: error.response?.data?.description ?? error.message,
23
+ style: Severity.Danger
24
+ })
25
+ }
26
+
27
+ return Promise.reject(error)
28
+ }
29
+ )
30
+ }
@@ -34,6 +34,14 @@ export const useCsrf = () => {
34
34
  axios.defaults.headers.patch[key_value.value] = token.value
35
35
  }
36
36
 
37
+ /**
38
+ * Fetch the CSRF token from the server
39
+ */
40
+ async function fetchCsrfToken() {
41
+ const response = await axios.get('/api/csrf')
42
+ updateFromHeaders(response.headers)
43
+ }
44
+
37
45
  /**
38
46
  * Get the CSRF token name and value keys from config.
39
47
  */
@@ -115,6 +123,7 @@ export const useCsrf = () => {
115
123
  name,
116
124
  token,
117
125
  isEnabled,
118
- updateFromHeaders
126
+ updateFromHeaders,
127
+ fetchCsrfToken
119
128
  }
120
129
  }
@@ -33,7 +33,13 @@
33
33
  */
34
34
  import { ref, toValue, watchEffect, computed } from 'vue'
35
35
  import axios from 'axios'
36
- import type { AssociativeArray, Sprunjer, SprunjerData, SprunjerResponse } from '../interfaces'
36
+ import type {
37
+ ApiErrorResponse,
38
+ AssociativeArray,
39
+ Sprunjer,
40
+ SprunjerData,
41
+ SprunjerResponse
42
+ } from '../interfaces'
37
43
 
38
44
  export const useSprunjer = (
39
45
  dataUrl: string | (() => string),
@@ -60,6 +66,7 @@ export const useSprunjer = (
60
66
 
61
67
  // State
62
68
  const loading = ref<boolean>(false)
69
+ const error = ref<ApiErrorResponse | null>(null)
63
70
 
64
71
  /**
65
72
  * Api fetch function
@@ -87,8 +94,7 @@ export const useSprunjer = (
87
94
  data.value.filterable = response.data.filterable ?? []
88
95
  })
89
96
  .catch((err) => {
90
- // TODO : User toast alert, or export alert
91
- console.error(err)
97
+ error.value = err.response.data as ApiErrorResponse
92
98
  })
93
99
  .finally(() => {
94
100
  loading.value = false
@@ -169,6 +175,7 @@ export const useSprunjer = (
169
175
  data,
170
176
  fetch,
171
177
  loading,
178
+ error,
172
179
  downloadCsv,
173
180
  totalPages,
174
181
  countFiltered,
@@ -1,6 +1,7 @@
1
1
  import type { App } from 'vue'
2
2
  import { useConfigStore, useTranslator } from './stores'
3
3
  import { useCsrf } from './composables/useCsrf'
4
+ import { useAxiosInterceptor } from './composables'
4
5
 
5
6
  /**
6
7
  * Core Sprinkle initialization recipe.
@@ -11,6 +12,13 @@ import { useCsrf } from './composables/useCsrf'
11
12
  */
12
13
  export default {
13
14
  install: (app: App) => {
15
+ /**
16
+ * Add Axios error handler.
17
+ * Load first to ensure that all axios requests are intercepted.
18
+ * This is important for the config loading and translator loading.
19
+ */
20
+ useAxiosInterceptor()
21
+
14
22
  /**
15
23
  * Load configuration
16
24
  */
@@ -4,5 +4,12 @@
4
4
  * Generic API Response interface.
5
5
  */
6
6
  export interface ApiResponse {
7
- message: string
7
+ title: string
8
+ description?: string
9
+ }
10
+
11
+ export interface ApiErrorResponse {
12
+ title: string
13
+ description: string
14
+ status: number
8
15
  }
@@ -27,4 +27,4 @@ export type { SprunjerRequest, SprunjerResponse } from './sprunjerApi'
27
27
  export type { DictionaryResponse, DictionaryEntries, DictionaryConfig } from './DictionaryApi'
28
28
 
29
29
  // Misc
30
- export type { ApiResponse } from './ApiResponse'
30
+ export type { ApiResponse, ApiErrorResponse } from './ApiResponse'
@@ -4,7 +4,7 @@
4
4
  * Represents the interface for the Sprunjer composable.
5
5
  */
6
6
  import type { Ref, ComputedRef } from 'vue'
7
- import type { AssociativeArray } from '.'
7
+ import type { ApiErrorResponse, AssociativeArray } from '.'
8
8
 
9
9
  export interface Sprunjer {
10
10
  dataUrl: string | (() => string)
@@ -15,6 +15,7 @@ export interface Sprunjer {
15
15
  data: Ref<SprunjerData>
16
16
  fetch: () => void
17
17
  loading: Ref<boolean>
18
+ error: Ref<ApiErrorResponse | null>
18
19
  totalPages: ComputedRef<number>
19
20
  downloadCsv: () => void
20
21
  countFiltered: ComputedRef<number>
@@ -12,7 +12,7 @@ export default [
12
12
  title: 'ERROR.404.TITLE',
13
13
  description: 'ERROR.404.DESCRIPTION'
14
14
  },
15
- component: () => import('../views/404NotFound.vue')
15
+ component: () => import('../views/Page404NotFound.vue')
16
16
  },
17
17
  {
18
18
  path: '/:pathMatch(.*)*',
@@ -21,7 +21,7 @@ export default [
21
21
  title: 'ERROR.401.TITLE',
22
22
  description: 'ERROR.401.DESCRIPTION'
23
23
  },
24
- component: () => import('../views/401Unauthorized.vue')
24
+ component: () => import('../views/Page401Unauthorized.vue')
25
25
  },
26
26
  {
27
27
  path: '/:pathMatch(.*)*',
@@ -30,7 +30,7 @@ export default [
30
30
  title: 'ERROR.403.TITLE',
31
31
  description: 'ERROR.403.DESCRIPTION'
32
32
  },
33
- component: () => import('../views/403Forbidden.vue')
33
+ component: () => import('../views/Page403Forbidden.vue')
34
34
  },
35
35
  {
36
36
  path: '/:pathMatch(.*)*',
@@ -39,6 +39,6 @@ export default [
39
39
  title: 'ERROR.TITLE',
40
40
  description: 'ERROR.DESCRIPTION'
41
41
  },
42
- component: () => import('../views/ErrorPage.vue')
42
+ component: () => import('../views/PageError.vue')
43
43
  }
44
44
  ]
@@ -1,3 +1,4 @@
1
- export { useConfigStore } from './config'
1
+ export { useConfigStore } from './useConfigStore'
2
2
  export { usePageMeta } from './usePageMeta'
3
3
  export { useTranslator } from './useTranslator'
4
+ export { useAlertsStore } from './useAlertsStore'
@@ -0,0 +1,30 @@
1
+ import { defineStore } from 'pinia'
2
+ import type { AlertInterface } from '../interfaces'
3
+
4
+ /**
5
+ * Alerts Store
6
+ *
7
+ * Manages application alerts in a reactive store. Templates can use this
8
+ * store to display alerts to users, including errors, warnings,
9
+ * informational, or success messages. When an alert is added, templates
10
+ * should automatically update the interface to reflect the change.
11
+ */
12
+ export const useAlertsStore = defineStore('alerts', {
13
+ state: () => ({
14
+ alerts: [] as AlertInterface[]
15
+ }),
16
+ actions: {
17
+ push(alert: AlertInterface) {
18
+ this.alerts.push(alert)
19
+ },
20
+ pop() {
21
+ return this.alerts.pop()
22
+ },
23
+ shift() {
24
+ return this.alerts.shift()
25
+ },
26
+ clear() {
27
+ this.alerts = []
28
+ }
29
+ }
30
+ })
@@ -1,11 +1,11 @@
1
1
  import { describe, expect, beforeEach, afterEach, test, vi } from 'vitest'
2
2
  import axios from 'axios'
3
- import { useConfigStore } from '../../stores/config'
3
+ import { useConfigStore } from '../../stores/useConfigStore'
4
4
  import { useCsrf } from '../../composables/useCsrf'
5
5
  import { nextTick } from 'vue'
6
6
 
7
7
  // Mock the config store
8
- vi.mock('../../stores/config')
8
+ vi.mock('../../stores/useConfigStore')
9
9
  const mockUseConfigStore = {
10
10
  get: vi.fn()
11
11
  }
@@ -1,8 +1,8 @@
1
1
  import { describe, expect, test, vi } from 'vitest'
2
2
  import { createApp } from 'vue'
3
- import { useConfigStore } from '../stores/config'
3
+ import { useConfigStore } from '../stores/useConfigStore'
4
4
  import plugin from '..'
5
- import * as Config from '../stores/config'
5
+ import * as Config from '../stores/useConfigStore'
6
6
  import * as Translator from '../stores/useTranslator'
7
7
 
8
8
  const mockConfigStore = {
@@ -1,7 +1,7 @@
1
1
  import { describe, expect, beforeEach, test, vi } from 'vitest'
2
2
  import { setActivePinia, createPinia } from 'pinia'
3
3
  import axios from 'axios'
4
- import { useConfigStore } from '../../stores/config'
4
+ import { useConfigStore } from '../../stores/useConfigStore'
5
5
 
6
6
  const testConfig = {
7
7
  name: 'Test Config',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@userfrosting/sprinkle-core",
3
- "version": "6.0.0-alpha.5",
3
+ "version": "6.0.0-alpha.6",
4
4
  "type": "module",
5
5
  "description": "Core Sprinkle for UserFrosting",
6
6
  "funding": "https://opencollective.com/userfrosting",
@@ -59,7 +59,7 @@
59
59
  "happy-dom": "^15.11.6",
60
60
  "less": "^4.2.0",
61
61
  "npm-run-all2": "^6.1.2",
62
- "prettier": "^3.2.5",
62
+ "prettier": "^3.6.2",
63
63
  "vite": "^6.2",
64
64
  "vite-plugin-dts": "^4.0.0",
65
65
  "vitest": "^3.1.1",