@saasmakers/ui 2.0.52 → 2.0.54
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/app/components/auth/AuthEmailConnect.vue +22 -4
- package/app/components/bases/BaseAction.vue +1 -1
- package/app/components/bases/BaseTag.vue +11 -186
- package/app/components/bases/BaseTags.vue +14 -268
- package/app/types/bases.d.ts +10 -30
- package/app/types/global.d.ts +2 -0
- package/app/utils/auth.ts +20 -0
- package/package.json +1 -1
|
@@ -20,6 +20,17 @@ const turnstile = ref<{
|
|
|
20
20
|
reset: () => void
|
|
21
21
|
}>()
|
|
22
22
|
|
|
23
|
+
const turnstileConfig = {
|
|
24
|
+
// Cloudflare Turnstile keeps internal requests in-flight (e.g. clearance redemption). When the
|
|
25
|
+
// widget errors we swallow it here so it does not surface as an unhandled rejection.
|
|
26
|
+
options: { 'error-callback': () => true },
|
|
27
|
+
|
|
28
|
+
// The wrapper's default 250s auto-reset re-runs the challenge while the user is idle, which aborts
|
|
29
|
+
// in-flight requests (surfaces as "The operation was cancelled." on WebKit). Disable it and rely
|
|
30
|
+
// on Turnstile's own refresh-expired handling instead.
|
|
31
|
+
resetInterval: 1000 * 60 * 60 * 24,
|
|
32
|
+
}
|
|
33
|
+
|
|
23
34
|
const { t } = useI18n()
|
|
24
35
|
|
|
25
36
|
const form = reactive({
|
|
@@ -87,6 +98,7 @@ async function onLogin() {
|
|
|
87
98
|
|
|
88
99
|
if (result.isClosed) {
|
|
89
100
|
emit('accountClosed', result.email ?? '')
|
|
101
|
+
resetTurnstile()
|
|
90
102
|
}
|
|
91
103
|
else {
|
|
92
104
|
v$.value.$reset()
|
|
@@ -95,12 +107,10 @@ async function onLogin() {
|
|
|
95
107
|
}
|
|
96
108
|
catch (error) {
|
|
97
109
|
console.error(error)
|
|
110
|
+
|
|
111
|
+
resetTurnstile()
|
|
98
112
|
}
|
|
99
113
|
finally {
|
|
100
|
-
form.captchaToken = ''
|
|
101
|
-
|
|
102
|
-
turnstile.value?.reset()
|
|
103
|
-
|
|
104
114
|
loggingWithEmail.value = false
|
|
105
115
|
}
|
|
106
116
|
}
|
|
@@ -110,6 +120,12 @@ function onUpdateCurrentWindow(newCurrentWindow: AuthEmailConnectCurrentWindow)
|
|
|
110
120
|
|
|
111
121
|
v$.value.$reset()
|
|
112
122
|
}
|
|
123
|
+
|
|
124
|
+
function resetTurnstile() {
|
|
125
|
+
form.captchaToken = ''
|
|
126
|
+
|
|
127
|
+
turnstile.value?.reset()
|
|
128
|
+
}
|
|
113
129
|
</script>
|
|
114
130
|
|
|
115
131
|
<template>
|
|
@@ -165,6 +181,8 @@ function onUpdateCurrentWindow(newCurrentWindow: AuthEmailConnectCurrentWindow)
|
|
|
165
181
|
<NuxtTurnstile
|
|
166
182
|
ref="turnstile"
|
|
167
183
|
v-model="form.captchaToken"
|
|
184
|
+
:options="turnstileConfig.options"
|
|
185
|
+
:reset-interval="turnstileConfig.resetInterval"
|
|
168
186
|
/>
|
|
169
187
|
</div>
|
|
170
188
|
|
|
@@ -44,7 +44,7 @@ function onClick(event: MouseEvent) {
|
|
|
44
44
|
|
|
45
45
|
<template>
|
|
46
46
|
<button
|
|
47
|
-
class="relative flex flex-col items-center rounded-lg text-center transition-colors"
|
|
47
|
+
class="relative flex select-none flex-col items-center rounded-lg text-center transition-colors"
|
|
48
48
|
:aria-busy="loading"
|
|
49
49
|
:class="{
|
|
50
50
|
'cursor-not-allowed opacity-50': disabled,
|
|
@@ -1,199 +1,24 @@
|
|
|
1
1
|
<script lang="ts" setup>
|
|
2
|
-
import { NuxtLinkLocale } from '#components'
|
|
3
2
|
import type { BaseTag } from '../../types/bases'
|
|
4
3
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
avatar: '',
|
|
8
|
-
circle: '',
|
|
9
|
-
clickable: true,
|
|
10
|
-
color: 'black',
|
|
11
|
-
draggable: false,
|
|
12
|
-
editable: false,
|
|
13
|
-
icon: undefined,
|
|
14
|
-
iconSize: undefined,
|
|
15
|
-
id: undefined,
|
|
16
|
-
isCreation: false,
|
|
17
|
-
light: true,
|
|
18
|
-
removable: false,
|
|
19
|
-
rounded: false,
|
|
4
|
+
withDefaults(defineProps<BaseTag>(), {
|
|
5
|
+
color: 'gray',
|
|
20
6
|
size: 'base',
|
|
21
7
|
text: '',
|
|
22
|
-
to: undefined,
|
|
23
|
-
truncate: false,
|
|
24
8
|
})
|
|
25
|
-
|
|
26
|
-
const emit = defineEmits<{
|
|
27
|
-
click: [event: MouseEvent, id?: number | string]
|
|
28
|
-
close: [event: MouseEvent, id?: number | string]
|
|
29
|
-
inputBlur: [event: FocusEvent, name: string, id?: number | string]
|
|
30
|
-
inputSubmit: [event: KeyboardEvent, name: string, id?: number | string]
|
|
31
|
-
remove: [event: MouseEvent, id?: number | string]
|
|
32
|
-
}>()
|
|
33
|
-
|
|
34
|
-
defineSlots<{
|
|
35
|
-
default?: () => VNode[]
|
|
36
|
-
}>()
|
|
37
|
-
|
|
38
|
-
const hovered = ref(false)
|
|
39
|
-
const form = reactive({ name: '' })
|
|
40
|
-
|
|
41
|
-
onBeforeMount(() => {
|
|
42
|
-
if (!props.isCreation) {
|
|
43
|
-
initializeFormForExistingTag()
|
|
44
|
-
}
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
function initializeFormForExistingTag() {
|
|
48
|
-
form.name = String(props.text)?.slice(1)
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function onBlur() {
|
|
52
|
-
hovered.value = false
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function onClick(event: MouseEvent) {
|
|
56
|
-
if (!props.removable) {
|
|
57
|
-
emit('click', event, props.id)
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function onFocus() {
|
|
62
|
-
hovered.value = true
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function onInputBlur(event: FocusEvent) {
|
|
66
|
-
emit('inputBlur', event, form.name.trim(), props.id)
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function onInputSubmit(event: KeyboardEvent) {
|
|
70
|
-
emit('inputSubmit', event, form.name.trim(), props.id)
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function onKeyDown(event: KeyboardEvent) {
|
|
74
|
-
if (props.clickable) {
|
|
75
|
-
(event.currentTarget as HTMLElement).click()
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function onMouseEnter() {
|
|
80
|
-
hovered.value = true
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function onMouseLeave() {
|
|
84
|
-
hovered.value = false
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function onRemove(event: MouseEvent) {
|
|
88
|
-
emit('remove', event, props.id)
|
|
89
|
-
}
|
|
90
9
|
</script>
|
|
91
10
|
|
|
92
11
|
<template>
|
|
93
12
|
<span
|
|
94
|
-
class="flex
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
@mouseleave="onMouseLeave"
|
|
103
|
-
@focusin="onFocus"
|
|
104
|
-
@focusout="onBlur"
|
|
13
|
+
class="pointer-events-none inline-flex rounded font-medium tracking-tight"
|
|
14
|
+
:class="{
|
|
15
|
+
'px-0.5 py-px text-3xs': size === 'xs',
|
|
16
|
+
'px-1 py-px text-3xs': size === 'sm',
|
|
17
|
+
'px-1.5 py-0.5 text-2xs': size === 'base',
|
|
18
|
+
'bg-orange-100 text-orange-700 dark:bg-orange-950 dark:text-orange-300': color === 'orange',
|
|
19
|
+
'bg-gray-200 text-gray-600 dark:bg-gray-800 dark:text-gray-400': color === 'gray',
|
|
20
|
+
}"
|
|
105
21
|
>
|
|
106
|
-
<
|
|
107
|
-
:is="to ? NuxtLinkLocale : 'span'"
|
|
108
|
-
class="inline-flex items-center border font-medium"
|
|
109
|
-
:class="{
|
|
110
|
-
'cursor-pointer': clickable,
|
|
111
|
-
'rounded-md': !rounded,
|
|
112
|
-
'rounded-full': rounded,
|
|
113
|
-
'border-gray-200 dark:border-gray-800 bg-gray-200 dark:bg-gray-800 text-gray-800 dark:text-gray-200': (color === 'black' && light && !active) || removable,
|
|
114
|
-
'border-gray-900 dark:border-gray-100 bg-gray-900 dark:bg-gray-100 text-white dark:text-black': color === 'black' && (!light || active),
|
|
115
|
-
'border-gray-700 dark:border-gray-300 bg-gray-100 dark:bg-gray-900 text-gray-700 dark:text-gray-300': color === 'gray' && light && !active,
|
|
116
|
-
'border-gray-800 dark:border-gray-200 bg-gray-800 dark:bg-gray-200 text-white dark:text-black': color === 'gray' && (!light || active),
|
|
117
|
-
'border-green-700 dark:border-green-300 bg-green-100 dark:bg-green-900 text-green-700 dark:text-green-300': color === 'green' && light && !active,
|
|
118
|
-
'border-green-800 dark:border-green-200 bg-green-800 dark:bg-green-200 text-white dark:text-black': color === 'green' && (!light || active),
|
|
119
|
-
'border-indigo-700 dark:border-indigo-300 bg-indigo-100 dark:bg-indigo-900 text-indigo-700 dark:text-indigo-300': color === 'indigo' && light && !active,
|
|
120
|
-
'border-indigo-800 dark:border-indigo-200 bg-indigo-800 dark:bg-indigo-200 text-white dark:text-black': color === 'indigo' && (!light || active),
|
|
121
|
-
'border-orange-700 dark:border-orange-300 bg-white dark:bg-gray-900 text-orange-700 dark:text-orange-300': color === 'orange' && light && !active,
|
|
122
|
-
'border-orange-800 dark:border-orange-200 bg-orange-800 dark:bg-orange-200 text-white dark:text-black': color === 'orange' && (!light || active),
|
|
123
|
-
'border-red-700 dark:border-red-300 bg-white dark:bg-gray-900 text-red-700 dark:text-red-300': color === 'red' && light && !active,
|
|
124
|
-
'border-red-800 dark:border-red-200 bg-red-800 dark:bg-red-200 text-white dark:text-black': color === 'red' && (!light || active),
|
|
125
|
-
'border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 text-gray-800 dark:text-gray-200': color === 'white' && light && !active,
|
|
126
|
-
'border-white dark:border-gray-900 text-white dark:text-black': color === 'white' && (!light || active),
|
|
127
|
-
'border-dashed border-gray-400 dark:border-gray-600 bg-white dark:bg-gray-900 text-gray-800 dark:text-gray-200': color === 'white' && light && !active,
|
|
128
|
-
'py-0.5 px-1.5': size === 'xs',
|
|
129
|
-
'py-1.5 px-2.5': size === 'sm',
|
|
130
|
-
'py-2 px-3': size === 'base',
|
|
131
|
-
}"
|
|
132
|
-
:to="to"
|
|
133
|
-
>
|
|
134
|
-
<BaseIcon
|
|
135
|
-
v-if="draggable"
|
|
136
|
-
class="js-drag-handle mr-2 cursor-move"
|
|
137
|
-
clickable
|
|
138
|
-
color="gray"
|
|
139
|
-
:icon="getLayerIcon('drag')"
|
|
140
|
-
/>
|
|
141
|
-
|
|
142
|
-
<span
|
|
143
|
-
v-if="avatar || circle || icon"
|
|
144
|
-
class="mr-1 flex items-center text-center"
|
|
145
|
-
:class="{
|
|
146
|
-
'h-2.5 w-2.5': size === 'xs' || size === 'sm' || iconSize === 2.5,
|
|
147
|
-
'h-3 w-3': size === 'base' || iconSize === 3,
|
|
148
|
-
'h-3.5 w-3.5': iconSize === 3.5,
|
|
149
|
-
'h-4 w-4': iconSize === 4,
|
|
150
|
-
}"
|
|
151
|
-
>
|
|
152
|
-
<BaseAvatar
|
|
153
|
-
v-if="avatar"
|
|
154
|
-
class="h-full w-full"
|
|
155
|
-
:shadow="false"
|
|
156
|
-
:src="avatar"
|
|
157
|
-
/>
|
|
158
|
-
|
|
159
|
-
<BaseIcon
|
|
160
|
-
v-else-if="icon"
|
|
161
|
-
class="h-full w-full"
|
|
162
|
-
:icon="icon"
|
|
163
|
-
/>
|
|
164
|
-
</span>
|
|
165
|
-
|
|
166
|
-
<span
|
|
167
|
-
v-if="$slots.default"
|
|
168
|
-
class="mr-1"
|
|
169
|
-
>
|
|
170
|
-
<slot />
|
|
171
|
-
</span>
|
|
172
|
-
|
|
173
|
-
<BaseText
|
|
174
|
-
v-if="!editable"
|
|
175
|
-
class="whitespace-nowrap outline-none"
|
|
176
|
-
:class="{ truncate }"
|
|
177
|
-
:size="size === 'xs' ? '3xs' : size === 'sm' ? '2xs' : 'xs'"
|
|
178
|
-
:text="text"
|
|
179
|
-
/>
|
|
180
|
-
|
|
181
|
-
<FieldInput
|
|
182
|
-
v-else
|
|
183
|
-
v-model="form.name"
|
|
184
|
-
border="none"
|
|
185
|
-
class="w-16"
|
|
186
|
-
size="sm"
|
|
187
|
-
@blur="onInputBlur"
|
|
188
|
-
@submit="onInputSubmit"
|
|
189
|
-
/>
|
|
190
|
-
|
|
191
|
-
<BaseIcon
|
|
192
|
-
v-if="removable"
|
|
193
|
-
class="ml-1.5 text-red-700 dark:text-red-300 hover:text-black dark:hover:text-white"
|
|
194
|
-
:icon="getLayerIcon('closeCircle')"
|
|
195
|
-
@click.prevent.stop="onRemove"
|
|
196
|
-
/>
|
|
197
|
-
</component>
|
|
22
|
+
<slot>{{ text }}</slot>
|
|
198
23
|
</span>
|
|
199
24
|
</template>
|
|
@@ -1,289 +1,35 @@
|
|
|
1
1
|
<script lang="ts" setup>
|
|
2
2
|
import type { BaseTags } from '../../types/bases'
|
|
3
3
|
|
|
4
|
-
const props = withDefaults(defineProps<
|
|
5
|
-
active: false,
|
|
6
|
-
clickable: true,
|
|
7
|
-
draggable: false,
|
|
8
|
-
editable: false,
|
|
9
|
-
hasBack: false,
|
|
10
|
-
hasCreation: false,
|
|
11
|
-
maxTags: Number.POSITIVE_INFINITY,
|
|
12
|
-
removable: false,
|
|
13
|
-
selectable: false,
|
|
14
|
-
selectableUnique: false,
|
|
4
|
+
const props = withDefaults(defineProps<BaseTags>(), {
|
|
15
5
|
size: 'base',
|
|
16
6
|
tags: () => [],
|
|
17
7
|
wrap: false,
|
|
18
8
|
})
|
|
19
9
|
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
create: [event: FocusEvent | KeyboardEvent, name: string]
|
|
25
|
-
detach: [event: MouseEvent, tagId?: number | string]
|
|
26
|
-
remove: [event: MouseEvent, tagId?: number | string]
|
|
27
|
-
update: [event: FocusEvent | KeyboardEvent, name: string, tagId?: number | string]
|
|
28
|
-
}>()
|
|
29
|
-
|
|
30
|
-
const modelValue = defineModel<BaseTags['modelValue']>({ default: () => [] })
|
|
31
|
-
const keyForTagCreation = ref(Date.now())
|
|
32
|
-
const keyForTagUpdate = ref(Date.now())
|
|
33
|
-
const root = ref<HTMLDivElement>()
|
|
34
|
-
const showingAllTags = ref(false)
|
|
35
|
-
const showingTagCreationField = ref(false)
|
|
36
|
-
const { t } = useI18n()
|
|
37
|
-
|
|
38
|
-
const sortedTags = computed({
|
|
39
|
-
get() {
|
|
40
|
-
return [...props.tags]
|
|
41
|
-
.filter((_tag, tagIndex) => tagIndex < props.maxTags || showingAllTags.value)
|
|
42
|
-
.toSorted((tagA, tagB) => {
|
|
43
|
-
return (tagA.order ?? 0) < (tagB.order ?? 0) ? -1 : 0
|
|
44
|
-
})
|
|
45
|
-
},
|
|
46
|
-
|
|
47
|
-
set(tags) {
|
|
48
|
-
const suiteForTags = tags
|
|
49
|
-
.map((tag, order) => ({
|
|
50
|
-
id: tag.id,
|
|
51
|
-
order,
|
|
52
|
-
}))
|
|
53
|
-
.map(tag => ({
|
|
54
|
-
id: tag.id,
|
|
55
|
-
order: tag.order,
|
|
56
|
-
}))
|
|
57
|
-
|
|
58
|
-
return suiteForTags
|
|
59
|
-
},
|
|
10
|
+
const sortedTags = computed(() => {
|
|
11
|
+
return [...props.tags].toSorted((tagA, tagB) => {
|
|
12
|
+
return (tagA.order ?? 0) - (tagB.order ?? 0)
|
|
13
|
+
})
|
|
60
14
|
})
|
|
61
|
-
|
|
62
|
-
function onCreateTag(event: FocusEvent | KeyboardEvent, name: string) {
|
|
63
|
-
showingTagCreationField.value = false
|
|
64
|
-
keyForTagCreation.value = Date.now()
|
|
65
|
-
|
|
66
|
-
emit('create', event, name)
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function onGoBack(event: MouseEvent) {
|
|
70
|
-
emit('back', event)
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function onRemoveTag(event: MouseEvent, tagId?: number | string) {
|
|
74
|
-
emit('remove', event, tagId)
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function onShowAllTags() {
|
|
78
|
-
showingAllTags.value = true
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function onShowTagField() {
|
|
82
|
-
if (!showingTagCreationField.value) {
|
|
83
|
-
showingTagCreationField.value = true
|
|
84
|
-
|
|
85
|
-
nextTick(() => {
|
|
86
|
-
const element = root.value?.querySelector<HTMLInputElement>('input[type="text"]')
|
|
87
|
-
|
|
88
|
-
element?.focus()
|
|
89
|
-
})
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function onTagClick(event: MouseEvent, tagId?: number | string) {
|
|
94
|
-
if (props.selectable && tagId) {
|
|
95
|
-
let value = [...modelValue.value]
|
|
96
|
-
|
|
97
|
-
if (value.includes(tagId)) {
|
|
98
|
-
value.splice(value.indexOf(tagId), 1)
|
|
99
|
-
emit('detach', event, tagId)
|
|
100
|
-
}
|
|
101
|
-
else if (props.selectableUnique) {
|
|
102
|
-
value = [tagId]
|
|
103
|
-
|
|
104
|
-
emit('attach', event, tagId)
|
|
105
|
-
}
|
|
106
|
-
else {
|
|
107
|
-
value.push(tagId)
|
|
108
|
-
emit('attach', event, tagId)
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
modelValue.value = value
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function onUpdateTag(event: FocusEvent | KeyboardEvent, name: string, tagId?: number | string) {
|
|
116
|
-
if (name) {
|
|
117
|
-
emit('update', event, name, tagId)
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
else {
|
|
121
|
-
keyForTagUpdate.value = Date.now()
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
15
|
</script>
|
|
125
16
|
|
|
126
17
|
<template>
|
|
127
18
|
<div
|
|
128
|
-
ref="root"
|
|
129
19
|
class="flex items-center"
|
|
130
20
|
:class="{
|
|
131
|
-
'flex-wrap': wrap,
|
|
132
|
-
'
|
|
133
|
-
'-
|
|
134
|
-
'-
|
|
21
|
+
'flex-wrap gap-1': wrap && size !== 'base',
|
|
22
|
+
'flex-wrap gap-1.5': wrap && size === 'base',
|
|
23
|
+
'w-full gap-1 overflow-x-auto': !wrap && size !== 'base',
|
|
24
|
+
'w-full gap-1.5 overflow-x-auto': !wrap && size === 'base',
|
|
135
25
|
}"
|
|
136
26
|
>
|
|
137
27
|
<BaseTag
|
|
138
|
-
v-
|
|
139
|
-
key="
|
|
140
|
-
:
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}"
|
|
144
|
-
color="indigo"
|
|
145
|
-
:icon="getLayerIcon('back')"
|
|
146
|
-
:size="size"
|
|
147
|
-
:text="t('cancel')"
|
|
148
|
-
@click="onGoBack"
|
|
149
|
-
/>
|
|
150
|
-
|
|
151
|
-
<BaseTag
|
|
152
|
-
v-if="hasCreation"
|
|
153
|
-
:key="keyForTagCreation"
|
|
154
|
-
:class="{
|
|
155
|
-
'm-0.75': size === 'sm',
|
|
156
|
-
'm-1': size === 'base',
|
|
157
|
-
}"
|
|
158
|
-
color="indigo"
|
|
159
|
-
:editable="showingTagCreationField"
|
|
160
|
-
:icon="showingTagCreationField ? getLayerIcon('tags') : getLayerIcon('plus')"
|
|
161
|
-
is-creation
|
|
162
|
-
:light="false"
|
|
163
|
-
:size="size"
|
|
164
|
-
:text="showingTagCreationField ? '' : t('newTag')"
|
|
165
|
-
@click="onShowTagField"
|
|
166
|
-
@input-blur="onCreateTag"
|
|
167
|
-
@input-submit="onCreateTag"
|
|
168
|
-
/>
|
|
169
|
-
|
|
170
|
-
<div class="flex items-center">
|
|
171
|
-
<span
|
|
172
|
-
v-for="(tag, tagIndex) in sortedTags"
|
|
173
|
-
:key="tag.id ? `${tag.id}_${tag.order}_${keyForTagUpdate}` : tagIndex"
|
|
174
|
-
class="flex flex-wrap items-center"
|
|
175
|
-
:class="{
|
|
176
|
-
'm-0.75': size === 'sm',
|
|
177
|
-
'm-1': size === 'base',
|
|
178
|
-
}"
|
|
179
|
-
>
|
|
180
|
-
<template v-if="tag">
|
|
181
|
-
<BaseTag
|
|
182
|
-
:id="tag.id || tagIndex"
|
|
183
|
-
:key="`${tag.id}_${tagIndex}`"
|
|
184
|
-
:active="tag.id && modelValue.includes(tag.id) || tag.active"
|
|
185
|
-
:avatar="tag.avatar"
|
|
186
|
-
:circle="tag.circle"
|
|
187
|
-
:clickable="clickable"
|
|
188
|
-
:color="tag.color"
|
|
189
|
-
:draggable="draggable"
|
|
190
|
-
:editable="editable"
|
|
191
|
-
:icon="tag.icon"
|
|
192
|
-
:icon-size="tag.iconSize"
|
|
193
|
-
:light="tag.light"
|
|
194
|
-
:removable="removable"
|
|
195
|
-
:size="size"
|
|
196
|
-
:text="tag.text"
|
|
197
|
-
:to="tag.to"
|
|
198
|
-
@click="onTagClick"
|
|
199
|
-
@input-blur="onUpdateTag"
|
|
200
|
-
@input-submit="onUpdateTag"
|
|
201
|
-
@remove="onRemoveTag"
|
|
202
|
-
/>
|
|
203
|
-
</template>
|
|
204
|
-
</span>
|
|
205
|
-
</div>
|
|
206
|
-
|
|
207
|
-
<BaseTag
|
|
208
|
-
v-if="tags.length > maxTags && !showingAllTags"
|
|
209
|
-
key="showMore"
|
|
210
|
-
:class="{
|
|
211
|
-
'm-0.75': size === 'sm',
|
|
212
|
-
'm-1': size === 'base',
|
|
213
|
-
}"
|
|
214
|
-
:size="size"
|
|
215
|
-
:text="t('showMore', { value: tags.length - maxTags })"
|
|
216
|
-
@click="onShowAllTags"
|
|
28
|
+
v-for="(tag, tagIndex) in sortedTags"
|
|
29
|
+
:key="tag.id ?? tagIndex"
|
|
30
|
+
:color="tag.color"
|
|
31
|
+
:size="tag.size ?? size"
|
|
32
|
+
:text="tag.text"
|
|
217
33
|
/>
|
|
218
34
|
</div>
|
|
219
35
|
</template>
|
|
220
|
-
|
|
221
|
-
<i18n lang="json">
|
|
222
|
-
{
|
|
223
|
-
"de": {
|
|
224
|
-
"cancel": "Abbrechen",
|
|
225
|
-
"newTag": "Neues Tag",
|
|
226
|
-
"showMore": "& {value} weiteres | & {value} weitere"
|
|
227
|
-
},
|
|
228
|
-
"en": {
|
|
229
|
-
"cancel": "Cancel",
|
|
230
|
-
"newTag": "New tag",
|
|
231
|
-
"showMore": "& {value} other | & {value} others"
|
|
232
|
-
},
|
|
233
|
-
"es": {
|
|
234
|
-
"cancel": "Cancelar",
|
|
235
|
-
"newTag": "Nueva etiqueta",
|
|
236
|
-
"showMore": "& {value} otro | & {value} otros"
|
|
237
|
-
},
|
|
238
|
-
"fr": {
|
|
239
|
-
"cancel": "Annuler",
|
|
240
|
-
"newTag": "Nouveau tag",
|
|
241
|
-
"showMore": "& {value} autre | & {value} autres"
|
|
242
|
-
},
|
|
243
|
-
"id": {
|
|
244
|
-
"cancel": "Batal",
|
|
245
|
-
"newTag": "Tag baru",
|
|
246
|
-
"showMore": "& {value} lainnya | & {value} lainnya"
|
|
247
|
-
},
|
|
248
|
-
"it": {
|
|
249
|
-
"cancel": "Annulla",
|
|
250
|
-
"newTag": "Nuovo tag",
|
|
251
|
-
"showMore": "& {value} altro | & {value} altri"
|
|
252
|
-
},
|
|
253
|
-
"ja": {
|
|
254
|
-
"cancel": "キャンセル",
|
|
255
|
-
"newTag": "新しいタグ",
|
|
256
|
-
"showMore": "& {value} その他 | & {value} その他"
|
|
257
|
-
},
|
|
258
|
-
"ko": {
|
|
259
|
-
"cancel": "취소",
|
|
260
|
-
"newTag": "새 태그",
|
|
261
|
-
"showMore": "& {value}개 더 | & {value}개 더"
|
|
262
|
-
},
|
|
263
|
-
"nl": {
|
|
264
|
-
"cancel": "Annuleren",
|
|
265
|
-
"newTag": "Nieuwe tag",
|
|
266
|
-
"showMore": "& {value} andere | & {value} anderen"
|
|
267
|
-
},
|
|
268
|
-
"pl": {
|
|
269
|
-
"cancel": "Anuluj",
|
|
270
|
-
"newTag": "Nowy tag",
|
|
271
|
-
"showMore": "& {value} inny | & {value} innych"
|
|
272
|
-
},
|
|
273
|
-
"pt": {
|
|
274
|
-
"cancel": "Cancelar",
|
|
275
|
-
"newTag": "Nova etiqueta",
|
|
276
|
-
"showMore": "& {value} outro | & {value} outros"
|
|
277
|
-
},
|
|
278
|
-
"pt-BR": {
|
|
279
|
-
"cancel": "Cancelar",
|
|
280
|
-
"newTag": "Nova tag",
|
|
281
|
-
"showMore": "& {value} outro | & {value} outros"
|
|
282
|
-
},
|
|
283
|
-
"vi": {
|
|
284
|
-
"cancel": "Hủy",
|
|
285
|
-
"newTag": "Thẻ mới",
|
|
286
|
-
"showMore": "& {value} mục khác | & {value} mục khác"
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
</i18n>
|
package/app/types/bases.d.ts
CHANGED
|
@@ -328,41 +328,21 @@ export interface BaseSpinner {
|
|
|
328
328
|
export type BaseStatus = | 'error' | 'info' | 'success' | 'warning'
|
|
329
329
|
|
|
330
330
|
export interface BaseTag {
|
|
331
|
-
|
|
332
|
-
avatar?: string
|
|
333
|
-
circle?: string
|
|
334
|
-
clickable?: boolean
|
|
335
|
-
color?: BaseColor
|
|
336
|
-
draggable?: boolean
|
|
337
|
-
editable?: boolean
|
|
338
|
-
icon?: string
|
|
339
|
-
iconSize?: number
|
|
340
|
-
id?: number
|
|
341
|
-
isCreation?: boolean
|
|
342
|
-
light?: boolean
|
|
343
|
-
order?: number
|
|
344
|
-
removable?: boolean
|
|
345
|
-
rounded?: boolean
|
|
331
|
+
color?: BaseTagColor
|
|
346
332
|
size?: BaseTagSize
|
|
347
|
-
text
|
|
348
|
-
|
|
349
|
-
|
|
333
|
+
text?: BaseTextText
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export type BaseTagColor = 'gray' | 'orange'
|
|
337
|
+
|
|
338
|
+
export interface BaseTagItem extends BaseTag {
|
|
339
|
+
id?: number | string
|
|
340
|
+
order?: number
|
|
350
341
|
}
|
|
351
342
|
|
|
352
343
|
export interface BaseTags {
|
|
353
|
-
active?: boolean
|
|
354
|
-
clickable?: boolean
|
|
355
|
-
draggable?: boolean
|
|
356
|
-
editable?: boolean
|
|
357
|
-
hasBack?: boolean
|
|
358
|
-
hasCreation?: boolean
|
|
359
|
-
maxTags?: number
|
|
360
|
-
modelValue: (number | string)[]
|
|
361
|
-
removable?: boolean
|
|
362
|
-
selectable?: boolean
|
|
363
|
-
selectableUnique?: boolean
|
|
364
344
|
size?: BaseTagSize
|
|
365
|
-
tags
|
|
345
|
+
tags?: BaseTagItem[]
|
|
366
346
|
wrap?: boolean
|
|
367
347
|
}
|
|
368
348
|
|
package/app/types/global.d.ts
CHANGED
|
@@ -69,6 +69,8 @@ declare global {
|
|
|
69
69
|
type BaseSpinner = import('./bases').BaseSpinner
|
|
70
70
|
type BaseStatus = import('./bases').BaseStatus
|
|
71
71
|
type BaseTag = import('./bases').BaseTag
|
|
72
|
+
type BaseTagColor = import('./bases').BaseTagColor
|
|
73
|
+
type BaseTagItem = import('./bases').BaseTagItem
|
|
72
74
|
type BaseTags = import('./bases').BaseTags
|
|
73
75
|
type BaseTagSize = import('./bases').BaseTagSize
|
|
74
76
|
type BaseText = import('./bases').BaseText
|
package/app/utils/auth.ts
CHANGED
|
@@ -1,3 +1,23 @@
|
|
|
1
|
+
export function isRequestCancellation(error: unknown): boolean {
|
|
2
|
+
if (!error || typeof error !== 'object') {
|
|
3
|
+
return false
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const candidate = error as {
|
|
7
|
+
code?: number
|
|
8
|
+
message?: string
|
|
9
|
+
name?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// WebKit (Safari/iOS/WKWebView) rejects aborted requests with NSURLErrorCancelled (-999) and this
|
|
13
|
+
// exact message when Turnstile's in-flight request is torn down on navigate/unmount after login.
|
|
14
|
+
return (
|
|
15
|
+
candidate.name === 'AbortError'
|
|
16
|
+
|| candidate.code === -999
|
|
17
|
+
|| candidate.message === 'The operation was cancelled.'
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
|
|
1
21
|
export function isSocialLoginCancellation(error: unknown): boolean {
|
|
2
22
|
if (!error || typeof error !== 'object') {
|
|
3
23
|
return false
|