mirror of
https://github.com/Bubka/2FAuth.git
synced 2025-08-13 15:37:08 +02:00
Replace local GroupSwitch & Searchbox components with 2fauth/ui ones
This commit is contained in:
@ -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')
|
||||
|
@ -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>
|
@ -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>
|
178
resources/js/stores/groups.js
vendored
178
resources/js/stores/groups.js
vendored
@ -3,100 +3,104 @@ 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds or edits a group
|
||||
* @param {object} group
|
||||
*/
|
||||
function addOrEdit(group) {
|
||||
const index = items.value.findIndex(g => g.id === parseInt(group.id))
|
||||
|
||||
if (index > -1) {
|
||||
items.value[index] = group
|
||||
notify.success({ text: this.$i18n.global.t('message.groups.group_name_saved') })
|
||||
}
|
||||
},
|
||||
else {
|
||||
items.value.push(group)
|
||||
notify.success({ text: this.$i18n.global.t('message.groups.group_successfully_created') })
|
||||
}
|
||||
}
|
||||
|
||||
getters: {
|
||||
current(state) {
|
||||
const group = state.items.find(item => item.id === parseInt(useUserStore().preferences.activeGroup))
|
||||
/**
|
||||
* Fetches the groups collection from the backend
|
||||
*/
|
||||
async function fetch() {
|
||||
// We do not want to fetch fresh data multiple times in the same 2s timespan
|
||||
const age = Math.floor(Date.now() - fetchedOn.value)
|
||||
const isNotFresh = age > 2000
|
||||
|
||||
// TODO : restore translated prompt
|
||||
return group ? group.name : 'message.all'
|
||||
},
|
||||
if (isNotFresh) {
|
||||
fetchedOn.value = Date.now()
|
||||
|
||||
withoutTheAllGroup(state) {
|
||||
return state.items.filter(item => item.id > 0)
|
||||
},
|
||||
await groupService.getAll().then(response => {
|
||||
items.value = response.data
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a group
|
||||
*/
|
||||
async function remove(id) {
|
||||
if (confirm(this.$i18n.global.t('message.groups.confirm.delete'))) {
|
||||
await groupService.delete(id).then(response => {
|
||||
items.value = items.value.filter(a => a.id !== id)
|
||||
notify.success({ text: this.$i18n.global.t('message.groups.group_successfully_deleted') })
|
||||
|
||||
theAllGroup(state) {
|
||||
return state.items.find(item => item.id == 0)
|
||||
},
|
||||
// Reset group filter to 'All' (groupId=0) since the backend has already made
|
||||
// the change automatically. This prevents a new request.
|
||||
if( parseInt(user.preferences.activeGroup) === id ) {
|
||||
user.preferences.activeGroup = 0
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
isEmpty() {
|
||||
return this.withoutTheAllGroup.length == 0
|
||||
},
|
||||
return {
|
||||
// STATE
|
||||
items,
|
||||
fetchedOn,
|
||||
|
||||
count() {
|
||||
return this.withoutTheAllGroup.length
|
||||
},
|
||||
},
|
||||
// GETTERS
|
||||
current,
|
||||
withoutTheAllGroup,
|
||||
theAllGroup,
|
||||
isEmpty,
|
||||
count,
|
||||
|
||||
actions: {
|
||||
/**
|
||||
* Adds or edits a group
|
||||
* @param {object} group
|
||||
*/
|
||||
addOrEdit(group) {
|
||||
const index = this.items.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' })
|
||||
}
|
||||
else {
|
||||
this.items.push(group)
|
||||
// TODO : restore translated message
|
||||
useNotify().success({ text: 'message.groups.group_successfully_created' })
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetches the groups collection from the backend
|
||||
*/
|
||||
async 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 isNotFresh = age > 2000
|
||||
|
||||
if (isNotFresh) {
|
||||
this.fetchedOn = Date.now()
|
||||
|
||||
await groupService.getAll().then(response => {
|
||||
this.items = 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')) {
|
||||
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' })
|
||||
|
||||
// Reset group filter to 'All' (groupId=0) since the backend has already made
|
||||
// the change automatically. This prevents a new request.
|
||||
if( parseInt(user.preferences.activeGroup) === id ) {
|
||||
user.preferences.activeGroup = 0
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
},
|
||||
// ACTIONS
|
||||
$reset,
|
||||
addOrEdit,
|
||||
fetch,
|
||||
remove,
|
||||
}
|
||||
})
|
||||
|
@ -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()
|
||||
|
@ -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' })
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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 }})
|
||||
{{ groups.current ? groups.current : $t('message.all') }} ({{ twofaccounts.filteredCount }})
|
||||
<FontAwesomeIcon :icon="['fas', 'caret-down']" />
|
||||
</button>
|
||||
</div>
|
||||
|
Reference in New Issue
Block a user