@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.
@@ -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
@@ -44,7 +44,7 @@ export function useData<T extends Record<string, any>>(
44
44
  }
45
45
 
46
46
  return {
47
- state: reactiveState,
47
+ state: reactiveState as T,
48
48
  init
49
49
  }
50
50
  }
@@ -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
+ }
@@ -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.8.0",
4
- "packageManager": "pnpm@8.10.5",
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.11",
47
- "@types/markdown-it": "^13.0.6",
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.6.1",
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": "^13.0.2",
53
+ "markdown-it": "^14.0.0",
55
54
  "normalize.css": "^8.0.1",
56
55
  "pinia": "^2.1.7",
57
- "postcss": "^8.4.31",
56
+ "postcss": "^8.4.32",
58
57
  "postcss-nested": "^6.0.1",
59
58
  "v-calendar": "^3.1.2",
60
- "vue": "^3.3.8",
59
+ "vue": "^3.3.10",
61
60
  "vue-router": "^4.2.5"
62
61
  },
63
62
  "dependencies": {
64
- "dayjs": "^1.11.10"
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.5",
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/lodash-es": "^4.17.11",
76
- "@types/markdown-it": "^13.0.6",
77
- "@types/node": "^20.9.2",
78
- "@vitejs/plugin-vue": "^4.5.0",
79
- "@vitest/coverage-v8": "^1.0.0-beta.5",
80
- "@vue/test-utils": "^2.4.2",
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.6.1",
89
+ "@vueuse/core": "^10.7.0",
84
90
  "body-scroll-lock": "4.0.0-beta.0",
85
- "eslint": "^8.54.0",
91
+ "eslint": "^8.55.0",
86
92
  "fuse.js": "^7.0.0",
87
93
  "happy-dom": "^12.10.3",
88
- "histoire": "^0.17.5",
94
+ "histoire": "^0.17.6",
89
95
  "lodash-es": "^4.17.21",
90
- "markdown-it": "^13.0.2",
96
+ "markdown-it": "^14.0.0",
91
97
  "normalize.css": "^8.0.1",
92
98
  "pinia": "^2.1.7",
93
- "postcss": "^8.4.31",
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.2.2",
103
+ "typescript": "~5.3.3",
98
104
  "v-calendar": "^3.1.2",
99
- "vite": "^5.0.0",
100
- "vitepress": "1.0.0-rc.29",
101
- "vitest": "^1.0.0-beta.5",
102
- "vue": "^3.3.8",
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.22"
110
+ "vue-tsc": "^1.8.25"
105
111
  }
106
112
  }