@globalbrain/sefirot 3.8.0 → 3.9.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.
- package/lib/components/SErrorBoundary.vue +33 -0
- package/lib/composables/Api.ts +71 -0
- package/lib/composables/Data.ts +1 -1
- package/lib/composables/Url.ts +96 -0
- package/lib/http/Http.ts +137 -0
- package/package.json +35 -29
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { onErrorCaptured, ref } from 'vue'
|
|
3
|
+
import { useRouter } from 'vue-router'
|
|
4
|
+
|
|
5
|
+
const emit = defineEmits<{
|
|
6
|
+
(e: 'error', value: any): void
|
|
7
|
+
}>()
|
|
8
|
+
|
|
9
|
+
const error = ref<Error | null>(null)
|
|
10
|
+
|
|
11
|
+
onErrorCaptured((e) => {
|
|
12
|
+
if (import.meta.env.DEV) {
|
|
13
|
+
console.error(e)
|
|
14
|
+
}
|
|
15
|
+
if (!import.meta.env.SSR) {
|
|
16
|
+
error.value = e
|
|
17
|
+
emit('error', e)
|
|
18
|
+
return false
|
|
19
|
+
}
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
async function clearError(options: { redirect?: string } = {}) {
|
|
23
|
+
if (options.redirect) {
|
|
24
|
+
await useRouter().replace(options.redirect)
|
|
25
|
+
}
|
|
26
|
+
error.value = null
|
|
27
|
+
}
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<template>
|
|
31
|
+
<slot v-if="error != null" name="error" :error="error" :clear-error="clearError" />
|
|
32
|
+
<slot v-else />
|
|
33
|
+
</template>
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { type Ref, ref } from 'vue'
|
|
2
|
+
import { Http } from '../http/Http'
|
|
3
|
+
|
|
4
|
+
export interface Query<Data = any> {
|
|
5
|
+
loading: Ref<boolean>
|
|
6
|
+
data: Ref<Data | undefined>
|
|
7
|
+
execute(): Promise<Data>
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface UseQueryOptions {
|
|
11
|
+
/**
|
|
12
|
+
* controls whether the query should execute immediately
|
|
13
|
+
* @default true
|
|
14
|
+
*/
|
|
15
|
+
immediate?: boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface Mutation<Data = any, Args extends any[] = any[]> {
|
|
19
|
+
loading: Ref<boolean>
|
|
20
|
+
data: Ref<Data | undefined>
|
|
21
|
+
execute(...args: Args): Promise<Data>
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type Get<Data = any, Args extends any[] = any[]> = Mutation<Data, Args>
|
|
25
|
+
|
|
26
|
+
export function useQuery<Data = any>(
|
|
27
|
+
req: (http: Http) => Promise<Data>,
|
|
28
|
+
options: UseQueryOptions = {}
|
|
29
|
+
): Query<Data> {
|
|
30
|
+
const loading = ref(false)
|
|
31
|
+
const data = ref<Data | undefined>()
|
|
32
|
+
|
|
33
|
+
if (options.immediate !== false) {
|
|
34
|
+
execute()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function execute(): Promise<Data> {
|
|
38
|
+
loading.value = true
|
|
39
|
+
|
|
40
|
+
const res: Data = await req(new Http())
|
|
41
|
+
data.value = res
|
|
42
|
+
|
|
43
|
+
loading.value = false
|
|
44
|
+
return res
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return { loading, data, execute }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function useMutation<Data = any, Args extends any[] = any[]>(
|
|
51
|
+
req: (http: Http, ...args: Args) => Promise<Data>
|
|
52
|
+
): Mutation<Data, Args> {
|
|
53
|
+
const loading = ref(false)
|
|
54
|
+
const data = ref<Data | undefined>()
|
|
55
|
+
|
|
56
|
+
async function execute(...args: Args): Promise<Data> {
|
|
57
|
+
loading.value = true
|
|
58
|
+
|
|
59
|
+
const res: Data = await req(new Http(), ...args)
|
|
60
|
+
data.value = res
|
|
61
|
+
|
|
62
|
+
loading.value = false
|
|
63
|
+
return res
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return { loading, data, execute }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const useGet: <Data = any, Args extends any[] = any[]>(
|
|
70
|
+
req: (http: Http, ...args: Args) => Promise<Data>
|
|
71
|
+
) => Get<Data, Args> = useMutation
|
package/lib/composables/Data.ts
CHANGED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import isPlainObject from 'lodash-es/isPlainObject'
|
|
2
|
+
import { watch } from 'vue'
|
|
3
|
+
import { useRoute, useRouter } from 'vue-router'
|
|
4
|
+
|
|
5
|
+
export interface UseUrlQuerySyncOptions {
|
|
6
|
+
casts?: Record<string, (value: string) => any>
|
|
7
|
+
exclude?: string[]
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function useUrlQuerySync(
|
|
11
|
+
state: Record<string, any>,
|
|
12
|
+
{ casts = {}, exclude }: UseUrlQuerySyncOptions = {}
|
|
13
|
+
): void {
|
|
14
|
+
const router = useRouter()
|
|
15
|
+
const route = useRoute()
|
|
16
|
+
|
|
17
|
+
const flattenInitialState = flattenObject(JSON.parse(JSON.stringify(state)))
|
|
18
|
+
|
|
19
|
+
setStateFromQuery()
|
|
20
|
+
|
|
21
|
+
watch(() => state, setQueryFromState, { deep: true, immediate: true })
|
|
22
|
+
|
|
23
|
+
function setStateFromQuery() {
|
|
24
|
+
const flattenState = flattenObject(state)
|
|
25
|
+
const flattenQuery = flattenObject(route.query)
|
|
26
|
+
|
|
27
|
+
Object.keys(flattenQuery).forEach((key) => {
|
|
28
|
+
if (exclude?.includes(key)) {
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const value = flattenQuery[key]
|
|
33
|
+
if (value === undefined) {
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const cast = casts[key]
|
|
38
|
+
flattenState[key] = cast ? cast(value) : value
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
Object.assign(state, unflattenObject(flattenState))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function setQueryFromState() {
|
|
45
|
+
const flattenState = flattenObject(state)
|
|
46
|
+
const flattenQuery = flattenObject(route.query)
|
|
47
|
+
|
|
48
|
+
Object.keys(flattenState).forEach((key) => {
|
|
49
|
+
if (exclude?.includes(key)) {
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const value = flattenState[key]
|
|
54
|
+
const initialValue = flattenInitialState[key]
|
|
55
|
+
|
|
56
|
+
if (value === initialValue) {
|
|
57
|
+
delete flattenQuery[key]
|
|
58
|
+
} else {
|
|
59
|
+
flattenQuery[key] = value
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (flattenQuery[key] === undefined) {
|
|
63
|
+
delete flattenQuery[key]
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
await router.replace({ query: unflattenObject(flattenQuery) })
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function flattenObject(obj: Record<string, any>, prefix = '') {
|
|
72
|
+
return Object.keys(obj).reduce((acc, k) => {
|
|
73
|
+
const pre = prefix.length ? `${prefix}.` : ''
|
|
74
|
+
if (isPlainObject(obj[k])) {
|
|
75
|
+
Object.assign(acc, flattenObject(obj[k], pre + k))
|
|
76
|
+
} else {
|
|
77
|
+
acc[pre + k] = obj[k]
|
|
78
|
+
}
|
|
79
|
+
return acc
|
|
80
|
+
}, {} as Record<string, any>)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function unflattenObject(obj: Record<string, any>) {
|
|
84
|
+
return Object.keys(obj).reduce((acc, k) => {
|
|
85
|
+
const keys = k.split('.')
|
|
86
|
+
keys.reduce((a, c, i) => {
|
|
87
|
+
if (i === keys.length - 1) {
|
|
88
|
+
a[c] = obj[k]
|
|
89
|
+
} else {
|
|
90
|
+
a[c] = a[c] || {}
|
|
91
|
+
}
|
|
92
|
+
return a[c]
|
|
93
|
+
}, acc)
|
|
94
|
+
return acc
|
|
95
|
+
}, {} as Record<string, any>)
|
|
96
|
+
}
|
package/lib/http/Http.ts
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { parse as parseContentDisposition } from '@tinyhttp/content-disposition'
|
|
2
|
+
import { parse as parseCookie } from '@tinyhttp/cookie'
|
|
3
|
+
import FileSaver from 'file-saver'
|
|
4
|
+
import { $fetch, type FetchOptions } from 'ofetch'
|
|
5
|
+
import { stringify } from 'qs'
|
|
6
|
+
|
|
7
|
+
export class Http {
|
|
8
|
+
static xsrfUrl = '/api/csrf-cookie'
|
|
9
|
+
|
|
10
|
+
private async ensureXsrfToken(): Promise<string | undefined> {
|
|
11
|
+
let xsrfToken = parseCookie(document.cookie)['XSRF-TOKEN']
|
|
12
|
+
|
|
13
|
+
if (!xsrfToken) {
|
|
14
|
+
await this.head(Http.xsrfUrl)
|
|
15
|
+
xsrfToken = parseCookie(document.cookie)['XSRF-TOKEN']
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return xsrfToken
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
private async buildRequest(
|
|
22
|
+
url: string,
|
|
23
|
+
_options: FetchOptions = {}
|
|
24
|
+
): Promise<[string, FetchOptions]> {
|
|
25
|
+
const { method, params, query, ...options } = _options
|
|
26
|
+
|
|
27
|
+
const xsrfToken
|
|
28
|
+
= ['POST', 'PUT', 'PATCH', 'DELETE'].includes(method || '') && (await this.ensureXsrfToken())
|
|
29
|
+
|
|
30
|
+
const queryString = stringify(
|
|
31
|
+
{ ...params, ...query },
|
|
32
|
+
{ arrayFormat: 'brackets', encodeValuesOnly: true }
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
return [
|
|
36
|
+
`${url}${queryString ? `?${queryString}` : ''}`,
|
|
37
|
+
{
|
|
38
|
+
method,
|
|
39
|
+
credentials: 'include',
|
|
40
|
+
...options,
|
|
41
|
+
headers: {
|
|
42
|
+
Accept: 'application/json',
|
|
43
|
+
...(xsrfToken && { 'X-XSRF-TOKEN': xsrfToken }),
|
|
44
|
+
...options.headers
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
]
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private async performRequest<T>(url: string, options: FetchOptions = {}) {
|
|
51
|
+
return $fetch<T, any>(...(await this.buildRequest(url, options)))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private async performRequestRaw<T>(url: string, options: FetchOptions = {}) {
|
|
55
|
+
return $fetch.raw<T, any>(...(await this.buildRequest(url, options)))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private objectToFormData(obj: any, form?: FormData, namespace?: string) {
|
|
59
|
+
const fd = form || new FormData()
|
|
60
|
+
let formKey: string
|
|
61
|
+
|
|
62
|
+
for (const property in obj) {
|
|
63
|
+
if (Reflect.has(obj, property)) {
|
|
64
|
+
if (namespace) {
|
|
65
|
+
formKey = `${namespace}[${property}]`
|
|
66
|
+
} else {
|
|
67
|
+
formKey = property
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (obj[property] === undefined) {
|
|
71
|
+
continue
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (typeof obj[property] === 'object' && !(obj[property] instanceof Blob)) {
|
|
75
|
+
this.objectToFormData(obj[property], fd, property)
|
|
76
|
+
} else {
|
|
77
|
+
fd.append(formKey, obj[property])
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return fd
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async get<T = any>(url: string, options?: FetchOptions): Promise<T> {
|
|
86
|
+
return this.performRequest<T>(url, { method: 'GET', ...options })
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async head<T = any>(url: string, options?: FetchOptions): Promise<T> {
|
|
90
|
+
return this.performRequest<T>(url, { method: 'HEAD', ...options })
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async post<T = any>(url: string, body?: any, options?: FetchOptions): Promise<T> {
|
|
94
|
+
return this.performRequest<T>(url, { method: 'POST', body, ...options })
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async put<T = any>(url: string, body?: any, options?: FetchOptions): Promise<T> {
|
|
98
|
+
return this.performRequest<T>(url, { method: 'PUT', body, ...options })
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async patch<T = any>(url: string, body?: any, options?: FetchOptions): Promise<T> {
|
|
102
|
+
return this.performRequest<T>(url, { method: 'PATCH', body, ...options })
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async delete<T = any>(url: string, options?: FetchOptions): Promise<T> {
|
|
106
|
+
return this.performRequest<T>(url, { method: 'DELETE', ...options })
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async upload<T = any>(url: string, body?: any, options?: FetchOptions): Promise<T> {
|
|
110
|
+
const formData = this.objectToFormData(body)
|
|
111
|
+
|
|
112
|
+
return this.performRequest<T>(url, {
|
|
113
|
+
method: 'POST',
|
|
114
|
+
body: formData,
|
|
115
|
+
...options
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async download(url: string, options?: FetchOptions): Promise<void> {
|
|
120
|
+
const { _data: blob, headers } = await this.performRequestRaw<Blob>(url, {
|
|
121
|
+
method: 'GET',
|
|
122
|
+
responseType: 'blob',
|
|
123
|
+
...options
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
if (!blob) {
|
|
127
|
+
throw new Error('No blob')
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const { filename = 'download' }
|
|
131
|
+
= parseContentDisposition(headers.get('Content-Disposition') || '')?.parameters || {}
|
|
132
|
+
|
|
133
|
+
FileSaver.saveAs(blob, filename as string)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export type { FetchOptions }
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@globalbrain/sefirot",
|
|
3
|
-
"version": "3.
|
|
4
|
-
"packageManager": "pnpm@8.
|
|
3
|
+
"version": "3.9.0",
|
|
4
|
+
"packageManager": "pnpm@8.11.0",
|
|
5
5
|
"description": "Vue Components for Global Brain Design System.",
|
|
6
6
|
"author": "Kia Ishii <ka.ishii@globalbrains.com>",
|
|
7
7
|
"license": "MIT",
|
|
@@ -41,66 +41,72 @@
|
|
|
41
41
|
"@iconify-icons/ph": "^1.2.5",
|
|
42
42
|
"@iconify-icons/ri": "^1.2.10",
|
|
43
43
|
"@iconify/vue": "^4.1.1",
|
|
44
|
-
"@tanstack/vue-virtual": "3.0.0-beta.62",
|
|
45
44
|
"@types/body-scroll-lock": "^3.1.2",
|
|
46
|
-
"@types/lodash-es": "^4.17.
|
|
47
|
-
"@types/markdown-it": "^13.0.
|
|
45
|
+
"@types/lodash-es": "^4.17.12",
|
|
46
|
+
"@types/markdown-it": "^13.0.7",
|
|
48
47
|
"@vuelidate/core": "^2.0.3",
|
|
49
48
|
"@vuelidate/validators": "^2.0.4",
|
|
50
|
-
"@vueuse/core": "^10.
|
|
49
|
+
"@vueuse/core": "^10.7.0",
|
|
51
50
|
"body-scroll-lock": "4.0.0-beta.0",
|
|
52
51
|
"fuse.js": "^7.0.0",
|
|
53
52
|
"lodash-es": "^4.17.21",
|
|
54
|
-
"markdown-it": "^
|
|
53
|
+
"markdown-it": "^14.0.0",
|
|
55
54
|
"normalize.css": "^8.0.1",
|
|
56
55
|
"pinia": "^2.1.7",
|
|
57
|
-
"postcss": "^8.4.
|
|
56
|
+
"postcss": "^8.4.32",
|
|
58
57
|
"postcss-nested": "^6.0.1",
|
|
59
58
|
"v-calendar": "^3.1.2",
|
|
60
|
-
"vue": "^3.3.
|
|
59
|
+
"vue": "^3.3.10",
|
|
61
60
|
"vue-router": "^4.2.5"
|
|
62
61
|
},
|
|
63
62
|
"dependencies": {
|
|
64
|
-
"
|
|
63
|
+
"@tanstack/vue-virtual": "3.0.0-beta.62",
|
|
64
|
+
"@tinyhttp/content-disposition": "^2.2.0",
|
|
65
|
+
"@tinyhttp/cookie": "^2.1.0",
|
|
66
|
+
"dayjs": "^1.11.10",
|
|
67
|
+
"file-saver": "^2.0.5",
|
|
68
|
+
"ofetch": "^1.3.3",
|
|
69
|
+
"qs": "^6.11.2"
|
|
65
70
|
},
|
|
66
71
|
"devDependencies": {
|
|
67
72
|
"@globalbrain/eslint-config": "^1.5.2",
|
|
68
|
-
"@histoire/plugin-vue": "^0.17.
|
|
73
|
+
"@histoire/plugin-vue": "^0.17.6",
|
|
69
74
|
"@iconify-icons/ph": "^1.2.5",
|
|
70
75
|
"@iconify-icons/ri": "^1.2.10",
|
|
71
76
|
"@iconify/vue": "^4.1.1",
|
|
72
77
|
"@release-it/conventional-changelog": "^8.0.1",
|
|
73
|
-
"@tanstack/vue-virtual": "3.0.0-beta.62",
|
|
74
78
|
"@types/body-scroll-lock": "^3.1.2",
|
|
75
|
-
"@types/
|
|
76
|
-
"@types/
|
|
77
|
-
"@types/
|
|
78
|
-
"@
|
|
79
|
-
"@
|
|
80
|
-
"@
|
|
79
|
+
"@types/file-saver": "^2.0.7",
|
|
80
|
+
"@types/lodash-es": "^4.17.12",
|
|
81
|
+
"@types/markdown-it": "^13.0.7",
|
|
82
|
+
"@types/node": "^20.10.4",
|
|
83
|
+
"@types/qs": "^6.9.10",
|
|
84
|
+
"@vitejs/plugin-vue": "^4.5.2",
|
|
85
|
+
"@vitest/coverage-v8": "^1.0.2",
|
|
86
|
+
"@vue/test-utils": "^2.4.3",
|
|
81
87
|
"@vuelidate/core": "^2.0.3",
|
|
82
88
|
"@vuelidate/validators": "^2.0.4",
|
|
83
|
-
"@vueuse/core": "^10.
|
|
89
|
+
"@vueuse/core": "^10.7.0",
|
|
84
90
|
"body-scroll-lock": "4.0.0-beta.0",
|
|
85
|
-
"eslint": "^8.
|
|
91
|
+
"eslint": "^8.55.0",
|
|
86
92
|
"fuse.js": "^7.0.0",
|
|
87
93
|
"happy-dom": "^12.10.3",
|
|
88
|
-
"histoire": "^0.17.
|
|
94
|
+
"histoire": "^0.17.6",
|
|
89
95
|
"lodash-es": "^4.17.21",
|
|
90
|
-
"markdown-it": "^
|
|
96
|
+
"markdown-it": "^14.0.0",
|
|
91
97
|
"normalize.css": "^8.0.1",
|
|
92
98
|
"pinia": "^2.1.7",
|
|
93
|
-
"postcss": "^8.4.
|
|
99
|
+
"postcss": "^8.4.32",
|
|
94
100
|
"postcss-nested": "^6.0.1",
|
|
95
101
|
"punycode": "^2.3.1",
|
|
96
102
|
"release-it": "^17.0.0",
|
|
97
|
-
"typescript": "~5.
|
|
103
|
+
"typescript": "~5.3.3",
|
|
98
104
|
"v-calendar": "^3.1.2",
|
|
99
|
-
"vite": "^5.0.
|
|
100
|
-
"vitepress": "1.0.0-rc.
|
|
101
|
-
"vitest": "^1.0.
|
|
102
|
-
"vue": "^3.3.
|
|
105
|
+
"vite": "^5.0.6",
|
|
106
|
+
"vitepress": "1.0.0-rc.31",
|
|
107
|
+
"vitest": "^1.0.2",
|
|
108
|
+
"vue": "^3.3.10",
|
|
103
109
|
"vue-router": "^4.2.5",
|
|
104
|
-
"vue-tsc": "^1.8.
|
|
110
|
+
"vue-tsc": "^1.8.25"
|
|
105
111
|
}
|
|
106
112
|
}
|