Replace local GroupSwitch & Searchbox components with 2fauth/ui ones

This commit is contained in:
Bubka
2025-06-25 17:47:04 +02:00
parent f5646500da
commit a5f7ec20a3
8 changed files with 115 additions and 231 deletions

View File

@ -1,10 +1,10 @@
<script setup>
import SearchBox from '@/components/SearchBox.vue'
import userService from '@/services/userService'
import Spinner from '@/components/Spinner.vue'
import { FontAwesomeLayers } from '@fortawesome/vue-fontawesome'
import { UseColorMode } from '@vueuse/components'
import { useErrorHandler } from '@2fauth/stores'
import { SearchBox } from '@2fauth/ui'
const errorHandler = useErrorHandler()
const $2fauth = inject('2fauth')

View File

@ -1,53 +0,0 @@
<script setup>
import userService from '@/services/userService'
import { useUserStore } from '@/stores/user'
import { UseColorMode } from '@vueuse/components'
const user = useUserStore()
const props = defineProps({
showGroupSwitch: Boolean,
groups: Array
})
const emit = defineEmits(['update:showGroupSwitch'])
/**
* Sets the selected group as the active group
*/
function setActiveGroup(id) {
user.preferences.activeGroup = id
if( user.preferences.rememberActiveGroup ) {
userService.updatePreference('activeGroup', id)
}
emit('update:showGroupSwitch', false)
}
</script>
<template>
<div id="groupSwitch" class="container groups">
<div class="columns is-centered">
<div class="column is-one-third-tablet is-one-quarter-desktop is-one-quarter-widescreen is-one-quarter-fullhd">
<div class="columns is-multiline">
<div class="column is-full" v-for="group in groups" :key="group.id">
<UseColorMode v-slot="{ mode }">
<button type="button" class="button is-fullwidth" :class="{'is-dark has-text-light is-outlined': mode == 'dark'}" @click="setActiveGroup(group.id)">{{ group.name }}</button>
</UseColorMode>
</div>
</div>
<div class="columns is-centered">
<div class="column has-text-centered">
<RouterLink :to="{ name: 'groups' }" >{{ $t('message.groups.manage_groups') }}</RouterLink>
</div>
</div>
</div>
</div>
<VueFooter>
<template #default>
<NavigationButton action="close" :useLinkTag="false" @closed="$emit('update:showGroupSwitch', false)" />
</template>
</VueFooter>
</div>
</template>

View File

@ -1,78 +0,0 @@
<script setup>
const keyword = defineModel('keyword')
const props = defineProps({
hasNoBackground: {
type: Boolean,
default: false
},
placeholder: String,
})
const searchInput = ref(null)
onMounted(() => {
document.addEventListener('keydown', ctrlFHandler)
document.addEventListener('keypress', anyPrintableKeyHandler)
})
onUnmounted(() => {
document.removeEventListener('keydown', ctrlFHandler)
document.removeEventListener('keypress', anyPrintableKeyHandler)
})
/**
* Attach an event listen for ctrl+F
*/
function ctrlFHandler(e) {
if (e.key === "f" && (e.ctrlKey || e.metaKey)) {
e.preventDefault()
searchInput.value?.focus()
}
}
/**
* Clear the search field
*/
function clearSearch() {
keyword.value = ''
}
/**
* Attach an event listen for any key press to automatically focus on search
* without having to ctrl+F
*/
function anyPrintableKeyHandler(e) {
if (e.key === 'Enter') {
return
}
keyword.value = e.key
searchInput.value?.setSelectionRange(1, 1)
searchInput.value?.focus()
e.preventDefault()
}
</script>
<template>
<div role="search" class="field">
<div class="control has-icons-right">
<input
v-model="keyword"
@keyup.esc.prevent="(event) => { clearSearch(); event.target.blur() }"
@keyup.enter.prevent="(event) => event.target.blur()"
@keypress.stop
ref="searchInput"
id="txtSearch"
type="search"
tabindex="1"
:aria-label="$t('message.search')"
:title="$t('message.search')"
:placeholder="placeholder"
class="input is-rounded is-search"
:class="{ 'has-no-background': hasNoBackground }">
<span class="icon is-small is-right">
<button type="button" v-if="keyword != ''" id="btnClearSearch" tabindex="1" :title="$t('message.clear_search')" class="clear-selection delete" @click="clearSearch"></button>
<FontAwesomeIcon v-else :icon="['fas', 'search']" />
</span>
</div>
</div>
</template>

View File

@ -3,91 +3,78 @@ import { useUserStore } from '@/stores/user'
import { useNotify } from '@2fauth/ui'
import groupService from '@/services/groupService'
export const useGroups = defineStore({
id: 'groups',
export const useGroups = defineStore('groups', () => {
state: () => {
return {
items: [],
fetchedOn: null,
const user = useUserStore()
const notify = useNotify()
// STATE
const items = ref([])
const fetchedOn = ref(null)
// GETTERS
const current = computed(() => {
const group = items.value.find(item => item.id === parseInt(user.preferences.activeGroup))
return group && user.preferences.activeGroup != 0 ? group.name : null
})
const withoutTheAllGroup = computed(() => items.value.filter(item => item.id > 0))
const theAllGroup = computed(() => items.value.find(item => item.id == 0))
const isEmpty = computed(() => withoutTheAllGroup.value.length == 0)
const count = computed(() => withoutTheAllGroup.value.length)
// ACTIONS
function $reset() {
items.value = [];
fetchedOn.value = null;
}
},
getters: {
current(state) {
const group = state.items.find(item => item.id === parseInt(useUserStore().preferences.activeGroup))
// TODO : restore translated prompt
return group ? group.name : 'message.all'
},
withoutTheAllGroup(state) {
return state.items.filter(item => item.id > 0)
},
theAllGroup(state) {
return state.items.find(item => item.id == 0)
},
isEmpty() {
return this.withoutTheAllGroup.length == 0
},
count() {
return this.withoutTheAllGroup.length
},
},
actions: {
/**
* Adds or edits a group
* @param {object} group
*/
addOrEdit(group) {
const index = this.items.findIndex(g => g.id === parseInt(group.id))
function addOrEdit(group) {
const index = items.value.findIndex(g => g.id === parseInt(group.id))
if (index > -1) {
this.items[index] = group
// TODO : restore translated message
useNotify().success({ text: 'message.groups.group_name_saved' })
items.value[index] = group
notify.success({ text: this.$i18n.global.t('message.groups.group_name_saved') })
}
else {
this.items.push(group)
// TODO : restore translated message
useNotify().success({ text: 'message.groups.group_successfully_created' })
items.value.push(group)
notify.success({ text: this.$i18n.global.t('message.groups.group_successfully_created') })
}
}
},
/**
* Fetches the groups collection from the backend
*/
async fetch() {
async function fetch() {
// We do not want to fetch fresh data multiple times in the same 2s timespan
const age = Math.floor(Date.now() - this.fetchedOn)
const age = Math.floor(Date.now() - fetchedOn.value)
const isNotFresh = age > 2000
if (isNotFresh) {
this.fetchedOn = Date.now()
fetchedOn.value = Date.now()
await groupService.getAll().then(response => {
this.items = response.data
items.value = response.data
})
}
},
}
/**
* Deletes a group
*/
async delete(id) {
const user = useUserStore()
// TODO : restore translated message
// if (confirm(t('message.groups.confirm.delete'))) {
if (confirm('message.groups.confirm.delete')) {
async function remove(id) {
if (confirm(this.$i18n.global.t('message.groups.confirm.delete'))) {
await groupService.delete(id).then(response => {
this.items = this.items.filter(a => a.id !== id)
// TODO : restore translated message
useNotify().success({ text: 'message.groups.group_successfully_deleted' })
items.value = items.value.filter(a => a.id !== id)
notify.success({ text: this.$i18n.global.t('message.groups.group_successfully_deleted') })
// Reset group filter to 'All' (groupId=0) since the backend has already made
// the change automatically. This prevents a new request.
@ -96,7 +83,24 @@ export const useGroups = defineStore({
}
})
}
},
}
},
return {
// STATE
items,
fetchedOn,
// GETTERS
current,
withoutTheAllGroup,
theAllGroup,
isEmpty,
count,
// ACTIONS
$reset,
addOrEdit,
fetch,
remove,
}
})

View File

@ -1,10 +1,9 @@
<script setup>
import tabs from './tabs'
import userService from '@/services/userService'
import { useNotify, TabBar } from '@2fauth/ui'
import { useNotify, TabBar, SearchBox } from '@2fauth/ui'
import { UseColorMode } from '@vueuse/components'
import Spinner from '@/components/Spinner.vue'
import SearchBox from '@/components/SearchBox.vue'
import { useErrorHandler } from '@2fauth/stores'
const errorHandler = useErrorHandler()

View File

@ -65,10 +65,10 @@
router.push({ name: 'accounts' })
})
.catch(error => {
if( error.response.status === 401 ) {
if( error.response?.status === 401 ) {
notify.alert({text: t('message.auth.forms.authentication_failed'), duration: 10000 })
}
else if( error.response.status !== 422 ) {
else if( error.response?.status !== 422 ) {
errorHandler.parse(error)
router.push({ name: 'genericError' })
}

View File

@ -49,7 +49,7 @@
{{ group.name }}
<!-- delete icon -->
<UseColorMode v-slot="{ mode }">
<button type="button" class="button tag is-pulled-right" :class="mode == 'dark' ? 'is-dark' : 'is-white'" @click="groups.delete(group.id)" :title="$t('message.delete')">
<button type="button" class="button tag is-pulled-right" :class="mode == 'dark' ? 'is-dark' : 'is-white'" @click="groups.remove(group.id)" :title="$t('message.delete')">
{{ $t('message.delete') }}
</button>
</UseColorMode>

View File

@ -1,9 +1,7 @@
<script setup>
import twofaccountService from '@/services/twofaccountService'
import TotpLooper from '@/components/TotpLooper.vue'
import GroupSwitch from '@/components/GroupSwitch.vue'
import DestinationGroupSelector from '@/components/DestinationGroupSelector.vue'
import SearchBox from '@/components/SearchBox.vue'
import Toolbar from '@/components/Toolbar.vue'
import OtpDisplay from '@/components/OtpDisplay.vue'
import ActionButtons from '@/components/ActionButtons.vue'
@ -11,7 +9,7 @@
import Dots from '@/components/Dots.vue'
import { UseColorMode } from '@vueuse/components'
import { useUserStore } from '@/stores/user'
import { useNotify } from '@2fauth/ui'
import { useNotify, SearchBox, GroupSwitch } from '@2fauth/ui'
import { useBusStore } from '@/stores/bus'
import { useTwofaccounts } from '@/stores/twofaccounts'
import { useGroups } from '@/stores/groups'
@ -331,14 +329,28 @@
twofaccounts.selectNone()
}
/**
* Saves the active group to the backends
*/
function saveActiveGroup() {
if( user.preferences.rememberActiveGroup ) {
userService.updatePreference('activeGroup', user.preferences.activeGroup)
}
}
</script>
<template>
<UseColorMode v-slot="{ mode }">
<div>
<!-- TODO: Persist the new active group by listening to the active-group-changed event -->
<!-- TODO: Add the link to the group management view in the GroupSwitch slot -->
<GroupSwitch v-if="showGroupSwitch" v-model:showGroupSwitch="showGroupSwitch" v-model:groups="groups.items" />
<GroupSwitch
v-if="showGroupSwitch"
v-model:is-visible="showGroupSwitch"
v-model:active-group="user.preferences.activeGroup"
:groups="groups.items"
@active-group-changed="saveActiveGroup">
<RouterLink :to="{ name: 'groups' }" >{{ $t('message.groups.manage_groups') }}</RouterLink>
</GroupSwitch>
<DestinationGroupSelector
v-if="showDestinationGroupSelector"
v-model:showDestinationGroupSelector="showDestinationGroupSelector"
@ -370,7 +382,7 @@
</div>
<div class="column" v-else>
<button type="button" id="btnShowGroupSwitch" :title="$t('message.groups.show_group_selector')" tabindex="1" class="button is-text is-like-text" :class="{'has-text-grey' : mode != 'dark'}" @click.stop="showGroupSwitch = !showGroupSwitch">
{{ groups.current }} ({{ twofaccounts.filteredCount }})&nbsp;
{{ groups.current ? groups.current : $t('message.all') }} ({{ twofaccounts.filteredCount }})&nbsp;
<FontAwesomeIcon :icon="['fas', 'caret-down']" />
</button>
</div>