mirror of
https://github.com/Bubka/2FAuth.git
synced 2025-08-12 15:07:04 +02:00
Replace local GroupSwitch & Searchbox components with 2fauth/ui ones
This commit is contained in:
@ -1,10 +1,10 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import SearchBox from '@/components/SearchBox.vue'
|
|
||||||
import userService from '@/services/userService'
|
import userService from '@/services/userService'
|
||||||
import Spinner from '@/components/Spinner.vue'
|
import Spinner from '@/components/Spinner.vue'
|
||||||
import { FontAwesomeLayers } from '@fortawesome/vue-fontawesome'
|
import { FontAwesomeLayers } from '@fortawesome/vue-fontawesome'
|
||||||
import { UseColorMode } from '@vueuse/components'
|
import { UseColorMode } from '@vueuse/components'
|
||||||
import { useErrorHandler } from '@2fauth/stores'
|
import { useErrorHandler } from '@2fauth/stores'
|
||||||
|
import { SearchBox } from '@2fauth/ui'
|
||||||
|
|
||||||
const errorHandler = useErrorHandler()
|
const errorHandler = useErrorHandler()
|
||||||
const $2fauth = inject('2fauth')
|
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 { useNotify } from '@2fauth/ui'
|
||||||
import groupService from '@/services/groupService'
|
import groupService from '@/services/groupService'
|
||||||
|
|
||||||
export const useGroups = defineStore({
|
export const useGroups = defineStore('groups', () => {
|
||||||
id: 'groups',
|
|
||||||
|
|
||||||
state: () => {
|
const user = useUserStore()
|
||||||
return {
|
const notify = useNotify()
|
||||||
items: [],
|
|
||||||
fetchedOn: null,
|
// 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) {
|
* Fetches the groups collection from the backend
|
||||||
const group = state.items.find(item => item.id === parseInt(useUserStore().preferences.activeGroup))
|
*/
|
||||||
|
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
|
if (isNotFresh) {
|
||||||
return group ? group.name : 'message.all'
|
fetchedOn.value = Date.now()
|
||||||
},
|
|
||||||
|
|
||||||
withoutTheAllGroup(state) {
|
await groupService.getAll().then(response => {
|
||||||
return state.items.filter(item => item.id > 0)
|
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) {
|
// Reset group filter to 'All' (groupId=0) since the backend has already made
|
||||||
return state.items.find(item => item.id == 0)
|
// the change automatically. This prevents a new request.
|
||||||
},
|
if( parseInt(user.preferences.activeGroup) === id ) {
|
||||||
|
user.preferences.activeGroup = 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
isEmpty() {
|
return {
|
||||||
return this.withoutTheAllGroup.length == 0
|
// STATE
|
||||||
},
|
items,
|
||||||
|
fetchedOn,
|
||||||
|
|
||||||
count() {
|
// GETTERS
|
||||||
return this.withoutTheAllGroup.length
|
current,
|
||||||
},
|
withoutTheAllGroup,
|
||||||
},
|
theAllGroup,
|
||||||
|
isEmpty,
|
||||||
|
count,
|
||||||
|
|
||||||
actions: {
|
// ACTIONS
|
||||||
/**
|
$reset,
|
||||||
* Adds or edits a group
|
addOrEdit,
|
||||||
* @param {object} group
|
fetch,
|
||||||
*/
|
remove,
|
||||||
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
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import tabs from './tabs'
|
import tabs from './tabs'
|
||||||
import userService from '@/services/userService'
|
import userService from '@/services/userService'
|
||||||
import { useNotify, TabBar } from '@2fauth/ui'
|
import { useNotify, TabBar, SearchBox } from '@2fauth/ui'
|
||||||
import { UseColorMode } from '@vueuse/components'
|
import { UseColorMode } from '@vueuse/components'
|
||||||
import Spinner from '@/components/Spinner.vue'
|
import Spinner from '@/components/Spinner.vue'
|
||||||
import SearchBox from '@/components/SearchBox.vue'
|
|
||||||
import { useErrorHandler } from '@2fauth/stores'
|
import { useErrorHandler } from '@2fauth/stores'
|
||||||
|
|
||||||
const errorHandler = useErrorHandler()
|
const errorHandler = useErrorHandler()
|
||||||
|
@ -65,10 +65,10 @@
|
|||||||
router.push({ name: 'accounts' })
|
router.push({ name: 'accounts' })
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
if( error.response.status === 401 ) {
|
if( error.response?.status === 401 ) {
|
||||||
notify.alert({text: t('message.auth.forms.authentication_failed'), duration: 10000 })
|
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)
|
errorHandler.parse(error)
|
||||||
router.push({ name: 'genericError' })
|
router.push({ name: 'genericError' })
|
||||||
}
|
}
|
||||||
|
@ -49,7 +49,7 @@
|
|||||||
{{ group.name }}
|
{{ group.name }}
|
||||||
<!-- delete icon -->
|
<!-- delete icon -->
|
||||||
<UseColorMode v-slot="{ mode }">
|
<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') }}
|
{{ $t('message.delete') }}
|
||||||
</button>
|
</button>
|
||||||
</UseColorMode>
|
</UseColorMode>
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import twofaccountService from '@/services/twofaccountService'
|
import twofaccountService from '@/services/twofaccountService'
|
||||||
import TotpLooper from '@/components/TotpLooper.vue'
|
import TotpLooper from '@/components/TotpLooper.vue'
|
||||||
import GroupSwitch from '@/components/GroupSwitch.vue'
|
|
||||||
import DestinationGroupSelector from '@/components/DestinationGroupSelector.vue'
|
import DestinationGroupSelector from '@/components/DestinationGroupSelector.vue'
|
||||||
import SearchBox from '@/components/SearchBox.vue'
|
|
||||||
import Toolbar from '@/components/Toolbar.vue'
|
import Toolbar from '@/components/Toolbar.vue'
|
||||||
import OtpDisplay from '@/components/OtpDisplay.vue'
|
import OtpDisplay from '@/components/OtpDisplay.vue'
|
||||||
import ActionButtons from '@/components/ActionButtons.vue'
|
import ActionButtons from '@/components/ActionButtons.vue'
|
||||||
@ -11,7 +9,7 @@
|
|||||||
import Dots from '@/components/Dots.vue'
|
import Dots from '@/components/Dots.vue'
|
||||||
import { UseColorMode } from '@vueuse/components'
|
import { UseColorMode } from '@vueuse/components'
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
import { useNotify } from '@2fauth/ui'
|
import { useNotify, SearchBox, GroupSwitch } from '@2fauth/ui'
|
||||||
import { useBusStore } from '@/stores/bus'
|
import { useBusStore } from '@/stores/bus'
|
||||||
import { useTwofaccounts } from '@/stores/twofaccounts'
|
import { useTwofaccounts } from '@/stores/twofaccounts'
|
||||||
import { useGroups } from '@/stores/groups'
|
import { useGroups } from '@/stores/groups'
|
||||||
@ -331,14 +329,28 @@
|
|||||||
twofaccounts.selectNone()
|
twofaccounts.selectNone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves the active group to the backends
|
||||||
|
*/
|
||||||
|
function saveActiveGroup() {
|
||||||
|
if( user.preferences.rememberActiveGroup ) {
|
||||||
|
userService.updatePreference('activeGroup', user.preferences.activeGroup)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<UseColorMode v-slot="{ mode }">
|
<UseColorMode v-slot="{ mode }">
|
||||||
<div>
|
<div>
|
||||||
<!-- TODO: Persist the new active group by listening to the active-group-changed event -->
|
<GroupSwitch
|
||||||
<!-- TODO: Add the link to the group management view in the GroupSwitch slot -->
|
v-if="showGroupSwitch"
|
||||||
<GroupSwitch v-if="showGroupSwitch" v-model:showGroupSwitch="showGroupSwitch" v-model:groups="groups.items" />
|
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
|
<DestinationGroupSelector
|
||||||
v-if="showDestinationGroupSelector"
|
v-if="showDestinationGroupSelector"
|
||||||
v-model:showDestinationGroupSelector="showDestinationGroupSelector"
|
v-model:showDestinationGroupSelector="showDestinationGroupSelector"
|
||||||
@ -370,7 +382,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="column" v-else>
|
<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">
|
<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']" />
|
<FontAwesomeIcon :icon="['fas', 'caret-down']" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
Reference in New Issue
Block a user