Add Groups to vue front-end

This commit is contained in:
Bubka 2020-10-25 23:59:03 +01:00
parent 9b29c4d294
commit b1c2a56c2a
7 changed files with 387 additions and 10 deletions

View File

@ -5,6 +5,7 @@ import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import { import {
faPlus, faPlus,
faPlusCircle,
faQrcode, faQrcode,
faImage, faImage,
faTrash, faTrash,
@ -16,7 +17,11 @@ import {
faEllipsisH, faEllipsisH,
faBars, faBars,
faSpinner, faSpinner,
faChevronLeft faChevronLeft,
faCaretUp,
faCaretDown,
faLayerGroup,
faMinusCircle,
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
import { import {
@ -25,6 +30,7 @@ import {
library.add( library.add(
faPlus, faPlus,
faPlusCircle,
faQrcode, faQrcode,
faImage, faImage,
faTrash, faTrash,
@ -37,7 +43,11 @@ library.add(
faBars, faBars,
faSpinner, faSpinner,
faGithubAlt, faGithubAlt,
faChevronLeft faChevronLeft,
faCaretUp,
faCaretDown,
faLayerGroup,
faMinusCircle,
); );
Vue.component('font-awesome-icon', FontAwesomeIcon) Vue.component('font-awesome-icon', FontAwesomeIcon)

View File

@ -1,5 +1,22 @@
<template> <template>
<div> <div>
<!-- Group selector -->
<div class="container groups" v-if="showGroupSelector">
<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" v-if="group.count > 0" :key="group.id">
<button :disabled="group.id == $root.appSettings.activeGroup" class="button is-fullwidth is-dark has-text-light is-outlined" @click="setActiveGroup(group.id)">{{ group.name }}</button>
</div>
</div>
<div class="columns is-centered">
<div class="column has-text-centered">
<router-link :to="{ name: 'groups' }" >{{ $t('groups.manage_groups') }}</router-link>
</div>
</div>
</div>
</div>
</div>
<!-- show accounts list --> <!-- show accounts list -->
<div class="container" v-if="this.showAccounts"> <div class="container" v-if="this.showAccounts">
<!-- accounts --> <!-- accounts -->
@ -47,13 +64,16 @@
<!-- </vue-pull-refresh> --> <!-- </vue-pull-refresh> -->
</div> </div>
<!-- header --> <!-- header -->
<div class="header has-background-black-ter" v-if="this.showAccounts"> <div class="header has-background-black-ter" v-if="this.showAccounts || this.showGroupSelector">
<div class="columns is-gapless is-mobile is-centered"> <div class="columns is-gapless is-mobile is-centered">
<div class="column is-three-quarters-mobile is-one-third-tablet is-one-quarter-desktop is-one-quarter-widescreen is-one-quarter-fullhd"> <div class="column is-three-quarters-mobile is-one-third-tablet is-one-quarter-desktop is-one-quarter-widescreen is-one-quarter-fullhd">
<!-- toolbar --> <!-- toolbar -->
<div class="toolbar has-text-centered" v-if="editMode"> <div class="toolbar has-text-centered" v-if="editMode">
<div class="manage-buttons tags has-addons are-medium"> <div class="manage-buttons tags has-addons are-medium">
<span class="tag is-dark">{{ selectedAccounts.length }}&nbsp;{{ $t('commons.selected') }}</span> <span class="tag is-dark">{{ selectedAccounts.length }}&nbsp;{{ $t('commons.selected') }}</span>
<a class="tag is-link" v-if="selectedAccounts.length > 0" @click="moveAccounts">
{{ $t('commons.move') }}&nbsp;<font-awesome-icon :icon="['fas', 'layer-group']" />
</a>
<a class="tag is-danger" v-if="selectedAccounts.length > 0" @click="destroyAccounts"> <a class="tag is-danger" v-if="selectedAccounts.length > 0" @click="destroyAccounts">
{{ $t('commons.delete') }}&nbsp;<font-awesome-icon :icon="['fas', 'trash']" /> {{ $t('commons.delete') }}&nbsp;<font-awesome-icon :icon="['fas', 'trash']" />
</a> </a>
@ -69,6 +89,18 @@
</span> </span>
</div> </div>
</div> </div>
<!-- group selector -->
<div class="group-selector has-text-centered" v-if="!editMode">
<div class="columns" @click="toggleGroupSelector">
<div class="column" v-if="!showGroupSelector">
{{ this.activeGroupName }} ({{ this.accounts.length }})
<font-awesome-icon :icon="['fas', 'caret-down']" />
</div>
<div class="column" v-else>
{{ $t('groups.select_accounts_to_show') }}
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -79,11 +111,11 @@
<twofaccount-show ref="TwofaccountShow" ></twofaccount-show> <twofaccount-show ref="TwofaccountShow" ></twofaccount-show>
</modal> </modal>
<!-- footer --> <!-- footer -->
<vue-footer v-if="showFooter" :showButtons="accounts.length > 0"> <vue-footer v-if="showFooter && !showGroupSelector" :showButtons="accounts.length > 0">
<!-- New item buttons --> <!-- New item buttons -->
<p class="control" v-if="!showUploader && !editMode"> <p class="control" v-if="!showUploader && !editMode">
<a class="button is-link is-rounded is-focus" @click="showUploader = true"> <a class="button is-link is-rounded is-focus" @click="showUploader = true">
<span>{{ $t('twofaccounts.new') }}</span> <span>{{ $t('commons.new') }}</span>
<span class="icon is-small"> <span class="icon is-small">
<font-awesome-icon :icon="['fas', 'qrcode']" /> <font-awesome-icon :icon="['fas', 'qrcode']" />
</span> </span>
@ -91,12 +123,12 @@
</p> </p>
<!-- Manage button --> <!-- Manage button -->
<p class="control" v-if="!showUploader && !editMode"> <p class="control" v-if="!showUploader && !editMode">
<a class="button is-dark is-rounded" @click="setEditModeTo(true)">{{ $t('twofaccounts.manage') }}</a> <a class="button is-dark is-rounded" @click="setEditModeTo(true)">{{ $t('commons.manage') }}</a>
</p> </p>
<!-- Done button --> <!-- Done button -->
<p class="control" v-if="!showUploader && editMode"> <p class="control" v-if="!showUploader && editMode">
<a class="button is-success is-rounded" @click="setEditModeTo(false)"> <a class="button is-success is-rounded" @click="setEditModeTo(false)">
<span>{{ $t('twofaccounts.done') }}</span> <span>{{ $t('commons.done') }}</span>
<span class="icon is-small"> <span class="icon is-small">
<font-awesome-icon :icon="['fas', 'check']" /> <font-awesome-icon :icon="['fas', 'check']" />
</span> </span>
@ -109,6 +141,12 @@
</a> </a>
</p> </p>
</vue-footer> </vue-footer>
<vue-footer v-if="showFooter && showGroupSelector" :showButtons="true">
<!-- Close Group selector button -->
<p class="control">
<a class="button is-dark is-rounded" @click="closeGroupSelector()">{{ $t('commons.close') }}</a>
</p>
</vue-footer>
</div> </div>
</template> </template>
@ -120,18 +158,24 @@
import QuickUploader from './../components/QuickUploader' import QuickUploader from './../components/QuickUploader'
// import vuePullRefresh from 'vue-pull-refresh'; // import vuePullRefresh from 'vue-pull-refresh';
import draggable from 'vuedraggable' import draggable from 'vuedraggable'
import Form from './../components/Form'
export default { export default {
data(){ data(){
return { return {
accounts : [], accounts : [],
groups : [],
selectedAccounts: [], selectedAccounts: [],
form: new Form({
activeGroup: this.$root.appSettings.activeGroup,
}),
showTwofaccountInModal : false, showTwofaccountInModal : false,
search: '', search: '',
editMode: this.InitialEditMode, editMode: this.InitialEditMode,
showUploader: false, showUploader: false,
showFooter: true, showFooter: true,
showGroupSelector: false,
drag: false, drag: false,
} }
}, },
@ -151,9 +195,20 @@
}, },
showAccounts() { showAccounts() {
return this.accounts.length > 0 && !this.showUploader ? true : false return this.accounts.length > 0 && !this.showUploader && !this.showGroupSelector ? true : false
}, },
activeGroupName() {
let g = this.groups.find(el => el.id === parseInt(this.$root.appSettings.activeGroup))
if(g) {
return g.name
}
else {
return this.$t('commons.all')
}
}
}, },
props: ['InitialEditMode'], props: ['InitialEditMode'],
@ -161,6 +216,7 @@
mounted() { mounted() {
this.fetchAccounts() this.fetchAccounts()
this.fetchGroups()
// stop OTP generation on modal close // stop OTP generation on modal close
this.$on('modalClose', function() { this.$on('modalClose', function() {
@ -261,6 +317,72 @@
} }
}, },
async moveAccounts() {
let accountsIds = []
this.selectedAccounts.forEach(id => accountsIds.push(id))
// Backend will associate all accounts with the selected group in the same move
await this.axios.patch('/api/group/accounts', {accountsIds: accountsIds, groupId: '3'} )
// we fetch the accounts again to prevent the js collection being
// desynchronize from the backend php collection
this.fetchAccounts()
},
fetchGroups() {
this.groups = []
this.axios.get('api/groups').then(response => {
response.data.forEach((data) => {
this.groups.push({
id : data.id,
name : data.name,
isActive: data.isActive,
count: data.twofaccounts_count
})
})
})
},
async setActiveGroup(id) {
this.form.activeGroup = id
await this.form.post('/api/settings/options', {returnError: true})
.then(response => {
this.$root.appSettings.activeGroup = response.data.settings.activeGroup
this.closeGroupSelector()
})
.catch(error => {
this.$router.push({ name: 'genericError', params: { err: error.response } })
});
this.fetchAccounts()
},
toggleGroupSelector: function(event) {
if (event) {
this.showGroupSelector ? this.closeGroupSelector() : this.openGroupSelector()
}
},
openGroupSelector: function(event) {
this.showGroupSelector = true
},
closeGroupSelector: function(event) {
this.showGroupSelector = false
},
setEditModeTo(state) { setEditModeTo(state) {
if( state === false ) { if( state === false ) {
this.selectedAccounts = [] this.selectedAccounts = []

View File

@ -0,0 +1,81 @@
<template>
<div class="columns is-centered">
<div class="form-column column is-two-thirds-tablet is-half-desktop is-one-third-widescreen is-one-quarter-fullhd">
<h1 class="title">
{{ $t('groups.groups') }}
</h1>
<p class="is-size-7-mobile">
{{ $t('groups.manage_groups_legend')}}
</p>
<router-link class="is-link" :to="{ name: 'createGroup' }">
<font-awesome-icon :icon="['fas', 'plus-circle']" /> Create new group
</router-link>
<div v-for="group in groups" :key="group.id" class="group-item has-text-light is-size-5 is-size-6-mobile">
{{ group.name }}
<a class="has-text-grey is-pulled-right" @click="deleteGroup(group.id)">
<font-awesome-icon :icon="['fas', 'trash']" />
</a>
<router-link :to="{ name: 'editGroup', params: { groupId: group.id }}" class="tag is-dark">
{{ $t('commons.rename') }}
</router-link>
<span class="is-family-primary is-size-6 is-size-7-mobile has-text-grey">{{ group.count }} {{ $t('twofaccounts.accounts') }}</span>
</div>
<p class="is-size-7 is-pulled-right">
{{ $t('groups.deleting_group_does_not_delete_accounts')}}
</p>
<!-- footer -->
<vue-footer :showButtons="true">
<!-- close button -->
<p class="control">
<router-link :to="{ name: 'accounts' }" class="button is-dark is-rounded" @click="">{{ $t('commons.close') }}</router-link>
</p>
</vue-footer>
</div>
</div>
</template>
<script>
export default {
data() {
return {
groups : [],
}
},
mounted() {
this.fetchGroups()
},
methods: {
async fetchGroups() {
await this.axios.get('api/groups').then(response => {
response.data.forEach((data) => {
this.groups.push({
id : data.id,
name : data.name,
count: data.twofaccounts_count
})
})
})
// Remove the pseudo 'All' group
this.groups.shift()
},
deleteGroup(id) {
if(confirm(this.$t('groups.confirm.delete'))) {
this.axios.delete('/api/groups/' + id)
// Remove the deleted group from the collection
this.groups = this.groups.filter(a => a.id !== id)
}
}
},
}
</script>

View File

@ -0,0 +1,50 @@
<template>
<form-wrapper :title="$t('groups.forms.new_group')">
<form @submit.prevent="createGroup" @keydown="form.onKeydown($event)">
<form-field :form="form" fieldName="name" inputType="text" :label="$t('commons.name')" autofocus />
<div class="field is-grouped">
<div class="control">
<v-button>{{ $t('commons.create') }}</v-button>
</div>
<div class="control">
<button type="button" class="button is-text" @click="cancelCreation">{{ $t('commons.cancel') }}</button>
</div>
</div>
</form>
</form-wrapper>
</template>
<script>
import Form from './../../components/Form'
export default {
data() {
return {
form: new Form({
name: '',
})
}
},
methods: {
async createGroup() {
await this.form.post('/api/groups')
if( this.form.errors.any() === false ) {
this.$router.push({name: 'groups', params: { InitialEditMode: false }});
}
},
cancelCreation: function() {
this.$router.push({name: 'groups', params: { InitialEditMode: false }});
},
},
}
</script>

View File

@ -0,0 +1,63 @@
<template>
<form-wrapper :title="$t('groups.forms.rename_group')">
<form @submit.prevent="updateGroup" @keydown="form.onKeydown($event)">
<form-field :form="form" fieldName="name" inputType="text" :label="$t('groups.forms.new_name')" autofocus />
<div class="field is-grouped">
<div class="control">
<v-button :isLoading="form.isBusy">{{ $t('commons.save') }}</v-button>
</div>
<div class="control">
<button type="button" class="button is-text" @click="cancelCreation">{{ $t('commons.cancel') }}</button>
</div>
</div>
</form>
</form-wrapper>
</template>
<script>
import Form from './../../components/Form'
export default {
data() {
return {
groupExists: false,
form: new Form({
name: '',
})
}
},
created: function() {
this.getGroup();
},
methods: {
async getGroup () {
const { data } = await this.axios.get('/api/groups/' + this.$route.params.groupId)
this.form.fill(data)
this.groupExists = true
},
async updateGroup() {
await this.form.put('/api/groups/' + this.$route.params.groupId)
if( this.form.errors.any() === false ) {
this.$router.push({name: 'groups', params: { InitialEditMode: true }})
}
},
cancelCreation: function() {
this.$router.push({name: 'groups', params: { InitialEditMode: true }});
},
},
}
</script>

View File

@ -0,0 +1,30 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Groups Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are used during authentication for various
| messages that we need to display to the user. You are free to modify
| these language lines according to your application's requirements.
|
*/
'groups' => 'Groups',
'select_accounts_to_show' => 'Select accounts to show',
'manage_groups' => 'Manage groups',
'manage_groups_legend' => 'You can create groups to manage your accounts the way you want. All your accounts remain visible in the pseudo group named \'All\', regardless of the group they belong to.',
'deleting_group_does_not_delete_accounts' => 'Deleting a group does not delete accounts',
'forms' => [
'new_group' => 'New group',
'new_name' => 'New name',
'rename_group' => 'Rename group',
],
'confirm' => [
'delete' => 'Are you sure you want to delete this group?',
],
];

View File

@ -51,13 +51,34 @@ a:hover {
} }
} }
.group-selector {
cursor: pointer;
}
.group-item {
border-bottom: 1px solid hsl(0, 0%, 21%);
padding: 0.75rem;
}
.group-item:first-of-type {
margin-top: 2.5rem;
}
.group-item span {
display: block;
}
.accounts { .accounts {
margin-top: 40px; margin-top: 64px;
}
.groups {
margin-top: 110px;
} }
@media screen and (min-width: 769px) { @media screen and (min-width: 769px) {
.accounts { .accounts {
margin-top: 60px; margin-top: 84px;
} }
} }