@globalbrain/sefirot 3.7.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/SAlert.vue +103 -0
- package/lib/components/SErrorBoundary.vue +33 -0
- package/lib/components/SLoginPage.vue +204 -0
- package/lib/components/icon/SIconGbLogoWhite.vue +13 -0
- package/lib/components/icon/SIconGoogle.vue +12 -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 +37 -29
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import IconSuccess from '@iconify-icons/ph/check-bold'
|
|
3
|
+
import IconInfo from '@iconify-icons/ph/info-bold'
|
|
4
|
+
import IconWarning from '@iconify-icons/ph/warning-bold'
|
|
5
|
+
import IconDanger from '@iconify-icons/ph/warning-octagon-bold'
|
|
6
|
+
import SIcon from './SIcon.vue'
|
|
7
|
+
|
|
8
|
+
withDefaults(defineProps<{
|
|
9
|
+
mode?: 'info' | 'success' | 'warning' | 'danger'
|
|
10
|
+
}>(), {
|
|
11
|
+
mode: 'info'
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
const iconDict = {
|
|
15
|
+
info: IconInfo,
|
|
16
|
+
success: IconSuccess,
|
|
17
|
+
warning: IconWarning,
|
|
18
|
+
danger: IconDanger
|
|
19
|
+
} as const
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<template>
|
|
23
|
+
<div class="SAlert" :class="[mode]">
|
|
24
|
+
<div class="icon">
|
|
25
|
+
<SIcon :icon="iconDict[mode]" class="icon-svg" />
|
|
26
|
+
</div>
|
|
27
|
+
<div class="content">
|
|
28
|
+
<slot />
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
</template>
|
|
32
|
+
|
|
33
|
+
<style scoped lang="postcss">
|
|
34
|
+
.SAlert {
|
|
35
|
+
display: grid;
|
|
36
|
+
grid-template-columns: auto minmax(0, 1fr);
|
|
37
|
+
gap: 14px;
|
|
38
|
+
border: 1px solid var(--alert-border-color);
|
|
39
|
+
border-radius: 6px;
|
|
40
|
+
padding: 16px;
|
|
41
|
+
background-color: var(--alert-bg-color);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.icon {
|
|
45
|
+
display: flex;
|
|
46
|
+
justify-content: center;
|
|
47
|
+
align-items: center;
|
|
48
|
+
height: 24px;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.icon-svg {
|
|
52
|
+
width: 20px;
|
|
53
|
+
height: 20px;
|
|
54
|
+
color: var(--alert-icon-color);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.content {
|
|
58
|
+
display: flex;
|
|
59
|
+
flex-direction: column;
|
|
60
|
+
gap: 16px;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.content :slotted(p) {
|
|
64
|
+
margin: 0;
|
|
65
|
+
max-width: 65ch;
|
|
66
|
+
line-height: 24px;
|
|
67
|
+
font-size: 14px;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.content :slotted(a) {
|
|
71
|
+
font-weight: 500;
|
|
72
|
+
text-decoration: underline;
|
|
73
|
+
transition: color 0.25s;
|
|
74
|
+
|
|
75
|
+
&:hover {
|
|
76
|
+
color: var(--c-text-2);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.SAlert.info {
|
|
81
|
+
--alert-border-color: var(--c-border-info-1);
|
|
82
|
+
--alert-bg-color: var(--c-bg-info-dimm-a1);
|
|
83
|
+
--alert-icon-color: var(--c-text-info-1);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.SAlert.success {
|
|
87
|
+
--alert-border-color: var(--c-border-success-1);
|
|
88
|
+
--alert-bg-color: var(--c-bg-success-dimm-a1);
|
|
89
|
+
--alert-icon-color: var(--c-text-success-1);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.SAlert.warning {
|
|
93
|
+
--alert-border-color: var(--c-border-warning-1);
|
|
94
|
+
--alert-bg-color: var(--c-bg-warning-dimm-a1);
|
|
95
|
+
--alert-icon-color: var(--c-text-warning-1);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.SAlert.danger {
|
|
99
|
+
--alert-border-color: var(--c-border-danger-1);
|
|
100
|
+
--alert-bg-color: var(--c-bg-danger-dimm-a1);
|
|
101
|
+
--alert-icon-color: var(--c-text-danger-1);
|
|
102
|
+
}
|
|
103
|
+
</style>
|
|
@@ -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,204 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import IconGoogle from '@iconify-icons/ri/google-fill'
|
|
3
|
+
import { computed } from 'vue'
|
|
4
|
+
import SButton from './SButton.vue'
|
|
5
|
+
import SLink from './SLink.vue'
|
|
6
|
+
import SIconGbLogoWhite from './icon/SIconGbLogoWhite.vue'
|
|
7
|
+
|
|
8
|
+
export interface CoverTitle {
|
|
9
|
+
text: string
|
|
10
|
+
link: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface CoverPhotographer {
|
|
14
|
+
text: string
|
|
15
|
+
link: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface Action {
|
|
19
|
+
type: 'google'
|
|
20
|
+
onClick: () => Promise<void>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const props = defineProps<{
|
|
24
|
+
cover: string
|
|
25
|
+
coverTitle: CoverTitle
|
|
26
|
+
coverPhotographer: CoverPhotographer
|
|
27
|
+
actions: Action[]
|
|
28
|
+
}>()
|
|
29
|
+
|
|
30
|
+
const coverBgImageStyle = computed(() => `url(${props.cover})`)
|
|
31
|
+
|
|
32
|
+
function getActionLabel(type: Action['type']) {
|
|
33
|
+
switch (type) {
|
|
34
|
+
case 'google':
|
|
35
|
+
return 'Sign in via Google'
|
|
36
|
+
default:
|
|
37
|
+
throw new Error('[sefirot] Invalid action type')
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function getIconComponent(type: Action['type']) {
|
|
42
|
+
switch (type) {
|
|
43
|
+
case 'google':
|
|
44
|
+
return IconGoogle
|
|
45
|
+
default:
|
|
46
|
+
throw new Error('[sefirot] Invalid action type')
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
</script>
|
|
50
|
+
|
|
51
|
+
<template>
|
|
52
|
+
<div class="SLoginPage dark">
|
|
53
|
+
<div class="cover">
|
|
54
|
+
<div class="cover-caption">
|
|
55
|
+
<p class="cover-caption-text">
|
|
56
|
+
<SLink class="cover-caption-link" :href="coverTitle.link">
|
|
57
|
+
{{ coverTitle.text }}
|
|
58
|
+
</SLink>
|
|
59
|
+
by
|
|
60
|
+
<SLink class="cover-caption-link" :href="coverPhotographer.link">
|
|
61
|
+
{{ coverPhotographer.text }}
|
|
62
|
+
</SLink>
|
|
63
|
+
</p>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
<div class="form">
|
|
67
|
+
<div class="form-container">
|
|
68
|
+
<div class="form-logo">
|
|
69
|
+
<SIconGbLogoWhite class="form-logo-icon" />
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<div class="form-content">
|
|
73
|
+
<h1 class="form-title">Sign in to account</h1>
|
|
74
|
+
<p class="form-lead">This is a very closed login form meant for specific audiences only. If you can’t login, well, you know who to ask.</p>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<div class="form-actions">
|
|
78
|
+
<SButton
|
|
79
|
+
v-for="action in actions"
|
|
80
|
+
:key="action.type"
|
|
81
|
+
size="large"
|
|
82
|
+
mode="white"
|
|
83
|
+
rounded
|
|
84
|
+
:label="getActionLabel(action.type)"
|
|
85
|
+
:icon="getIconComponent(action.type)"
|
|
86
|
+
@click="action.onClick"
|
|
87
|
+
/>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
</template>
|
|
93
|
+
|
|
94
|
+
<style scoped lang="postcss">
|
|
95
|
+
.SLoginPage {
|
|
96
|
+
position: relative;
|
|
97
|
+
background-color: var(--c-bg-elv-1);
|
|
98
|
+
|
|
99
|
+
@media (min-width: 768px) {
|
|
100
|
+
display: grid;
|
|
101
|
+
grid-template-columns: 1fr 392px;
|
|
102
|
+
gap: 4px;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
@media (min-width: 1024px) {
|
|
106
|
+
grid-template-columns: 1fr 480px;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.cover {
|
|
111
|
+
display: none;
|
|
112
|
+
width: 100%;
|
|
113
|
+
height: 100%;
|
|
114
|
+
background-image: v-bind(coverBgImageStyle);
|
|
115
|
+
background-position: 50% 50%;
|
|
116
|
+
background-size: cover;
|
|
117
|
+
background-repeat: no-repeat;
|
|
118
|
+
|
|
119
|
+
@media (min-width: 768px) {
|
|
120
|
+
display: block;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.cover-caption {
|
|
125
|
+
position: absolute;
|
|
126
|
+
left: 0;
|
|
127
|
+
bottom: 0;
|
|
128
|
+
border-top: 4px solid var(--c-bg-elv-1);
|
|
129
|
+
border-right: 4px solid var(--c-bg-elv-1);
|
|
130
|
+
padding: 16px 24px;
|
|
131
|
+
font-size: 12px;
|
|
132
|
+
background-color: var(--c-bg-elv-2);
|
|
133
|
+
|
|
134
|
+
@media (min-width: 768px) {
|
|
135
|
+
display: block;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.cover-caption-text {
|
|
140
|
+
color: var(--c-text-2);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.cover-caption-link {
|
|
144
|
+
color: var(--c-text-1);
|
|
145
|
+
transition: color 0.25s;
|
|
146
|
+
|
|
147
|
+
&:hover {
|
|
148
|
+
color: var(--c-text-2);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.form {
|
|
153
|
+
padding: 96px 32px 48px;
|
|
154
|
+
min-height: 100vh;
|
|
155
|
+
background-color: var(--c-bg-elv-2);
|
|
156
|
+
|
|
157
|
+
@media (min-width: 768px) {
|
|
158
|
+
display: flex;
|
|
159
|
+
justify-content: center;
|
|
160
|
+
align-items: center;
|
|
161
|
+
padding: 48px;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.form-container {
|
|
166
|
+
@media (min-width: 768px) {
|
|
167
|
+
margin-top: -96px;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.form-logo {
|
|
172
|
+
margin: 0 auto;
|
|
173
|
+
width: 80px;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.form-content {
|
|
177
|
+
padding-top: 64px;
|
|
178
|
+
text-align: center;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.form-title {
|
|
182
|
+
font-size: 20px;
|
|
183
|
+
font-weight: 600;
|
|
184
|
+
color: var(--c-text-1);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.form-lead {
|
|
188
|
+
margin: 0 auto;
|
|
189
|
+
padding: 12px;
|
|
190
|
+
max-width: 336px;
|
|
191
|
+
font-size: 14px;
|
|
192
|
+
color: var(--c-text-2);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.form-actions {
|
|
196
|
+
display: flex;
|
|
197
|
+
flex-direction: column;
|
|
198
|
+
align-items: center;
|
|
199
|
+
gap: 8px;
|
|
200
|
+
padding-top: 24px;
|
|
201
|
+
text-align: center;
|
|
202
|
+
margin: 0 auto;
|
|
203
|
+
}
|
|
204
|
+
</style>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 192">
|
|
3
|
+
<polygon class="line" points="140.49 21.76 140.49 36.09 192 17.32 192 3 140.49 21.76 " />
|
|
4
|
+
<polygon class="line" points="42.93 159.04 0 174.68 0 189 42.93 173.36 42.93 159.04 " />
|
|
5
|
+
<path class="mark" d="M82.7,57v5.63a24.68,24.68,0,0,0-15.42-5.84c-14.66,0-28.11,11.55-28.11,36,0,27.18,12.22,38.32,27.43,38.32,6.33,0,11.49-2.55,16.06-6.22-.48,19.11-6.5,22.06-24.48,28.61v14.32c23.72-8.65,38.23-13.59,38.23-42.56V52ZM53.16,92.37c0-15.08,5.43-22.69,15.48-22.69,5.45,0,10.57,3.08,14.06,6.42V112c-3.57,3.44-8.29,6.21-13.52,6.21C58.86,118.19,53.16,110.31,53.16,92.37Z " />
|
|
6
|
+
<path class="mark" d="M143.47,57.57c-7.32,0-13.17,3.46-18.23,8.13V27.39l-13.58,4.94V129h13.58v-6.53a24.77,24.77,0,0,0,17.57,8c14.51,0,27.55-11.33,27.55-35.31C170.36,68.49,158.52,57.57,143.47,57.57Zm-2,60.22c-6.92,0-13.31-5.06-16.37-9.19V79.15c3.59-4.53,9.31-8.92,15.84-8.92,10.11,0,15.7,7.72,15.7,25.31C156.65,110.33,151.33,117.79,141.48,117.79Z " />
|
|
7
|
+
</svg>
|
|
8
|
+
</template>
|
|
9
|
+
|
|
10
|
+
<style scoped lang="postcss">
|
|
11
|
+
.line { fill: #979fa4; }
|
|
12
|
+
.mark { fill: #ffffff; }
|
|
13
|
+
</style>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<svg
|
|
3
|
+
viewBox="0 0 16 16"
|
|
4
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
5
|
+
fill-rule="evenodd"
|
|
6
|
+
clip-rule="evenodd"
|
|
7
|
+
stroke-linejoin="round"
|
|
8
|
+
stroke-miterlimit="1.414"
|
|
9
|
+
>
|
|
10
|
+
<path d="M8.16 6.857V9.6h4.537c-.183 1.177-1.37 3.45-4.537 3.45-2.73 0-4.96-2.26-4.96-5.05s2.23-5.05 4.96-5.05c1.554 0 2.594.66 3.19 1.233l2.17-2.092C12.126.79 10.32 0 8.16 0c-4.423 0-8 3.577-8 8s3.577 8 8 8c4.617 0 7.68-3.246 7.68-7.817 0-.526-.057-.926-.126-1.326H8.16z" />
|
|
11
|
+
</svg>
|
|
12
|
+
</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",
|
|
@@ -39,66 +39,74 @@
|
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
41
|
"@iconify-icons/ph": "^1.2.5",
|
|
42
|
+
"@iconify-icons/ri": "^1.2.10",
|
|
42
43
|
"@iconify/vue": "^4.1.1",
|
|
43
|
-
"@tanstack/vue-virtual": "3.0.0-beta.62",
|
|
44
44
|
"@types/body-scroll-lock": "^3.1.2",
|
|
45
|
-
"@types/lodash-es": "^4.17.
|
|
46
|
-
"@types/markdown-it": "^13.0.
|
|
45
|
+
"@types/lodash-es": "^4.17.12",
|
|
46
|
+
"@types/markdown-it": "^13.0.7",
|
|
47
47
|
"@vuelidate/core": "^2.0.3",
|
|
48
48
|
"@vuelidate/validators": "^2.0.4",
|
|
49
|
-
"@vueuse/core": "^10.
|
|
49
|
+
"@vueuse/core": "^10.7.0",
|
|
50
50
|
"body-scroll-lock": "4.0.0-beta.0",
|
|
51
51
|
"fuse.js": "^7.0.0",
|
|
52
52
|
"lodash-es": "^4.17.21",
|
|
53
|
-
"markdown-it": "^
|
|
53
|
+
"markdown-it": "^14.0.0",
|
|
54
54
|
"normalize.css": "^8.0.1",
|
|
55
55
|
"pinia": "^2.1.7",
|
|
56
|
-
"postcss": "^8.4.
|
|
56
|
+
"postcss": "^8.4.32",
|
|
57
57
|
"postcss-nested": "^6.0.1",
|
|
58
58
|
"v-calendar": "^3.1.2",
|
|
59
|
-
"vue": "^3.3.
|
|
59
|
+
"vue": "^3.3.10",
|
|
60
60
|
"vue-router": "^4.2.5"
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
|
-
"
|
|
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"
|
|
64
70
|
},
|
|
65
71
|
"devDependencies": {
|
|
66
72
|
"@globalbrain/eslint-config": "^1.5.2",
|
|
67
|
-
"@histoire/plugin-vue": "^0.17.
|
|
73
|
+
"@histoire/plugin-vue": "^0.17.6",
|
|
68
74
|
"@iconify-icons/ph": "^1.2.5",
|
|
75
|
+
"@iconify-icons/ri": "^1.2.10",
|
|
69
76
|
"@iconify/vue": "^4.1.1",
|
|
70
77
|
"@release-it/conventional-changelog": "^8.0.1",
|
|
71
|
-
"@tanstack/vue-virtual": "3.0.0-beta.62",
|
|
72
78
|
"@types/body-scroll-lock": "^3.1.2",
|
|
73
|
-
"@types/
|
|
74
|
-
"@types/
|
|
75
|
-
"@types/
|
|
76
|
-
"@
|
|
77
|
-
"@
|
|
78
|
-
"@
|
|
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",
|
|
79
87
|
"@vuelidate/core": "^2.0.3",
|
|
80
88
|
"@vuelidate/validators": "^2.0.4",
|
|
81
|
-
"@vueuse/core": "^10.
|
|
89
|
+
"@vueuse/core": "^10.7.0",
|
|
82
90
|
"body-scroll-lock": "4.0.0-beta.0",
|
|
83
|
-
"eslint": "^8.
|
|
91
|
+
"eslint": "^8.55.0",
|
|
84
92
|
"fuse.js": "^7.0.0",
|
|
85
93
|
"happy-dom": "^12.10.3",
|
|
86
|
-
"histoire": "^0.17.
|
|
94
|
+
"histoire": "^0.17.6",
|
|
87
95
|
"lodash-es": "^4.17.21",
|
|
88
|
-
"markdown-it": "^
|
|
96
|
+
"markdown-it": "^14.0.0",
|
|
89
97
|
"normalize.css": "^8.0.1",
|
|
90
98
|
"pinia": "^2.1.7",
|
|
91
|
-
"postcss": "^8.4.
|
|
99
|
+
"postcss": "^8.4.32",
|
|
92
100
|
"postcss-nested": "^6.0.1",
|
|
93
101
|
"punycode": "^2.3.1",
|
|
94
102
|
"release-it": "^17.0.0",
|
|
95
|
-
"typescript": "~5.
|
|
103
|
+
"typescript": "~5.3.3",
|
|
96
104
|
"v-calendar": "^3.1.2",
|
|
97
|
-
"vite": "^5.0.
|
|
98
|
-
"vitepress": "1.0.0-rc.
|
|
99
|
-
"vitest": "^1.0.
|
|
100
|
-
"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",
|
|
101
109
|
"vue-router": "^4.2.5",
|
|
102
|
-
"vue-tsc": "^1.8.
|
|
110
|
+
"vue-tsc": "^1.8.25"
|
|
103
111
|
}
|
|
104
112
|
}
|