summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--css/ContactsList.scss14
-rw-r--r--src/components/ContactDetails.vue26
-rw-r--r--src/components/ContactsList.vue37
-rw-r--r--src/components/EmptyContent.vue77
-rw-r--r--src/store/groups.js23
-rw-r--r--src/views/Contacts.vue38
6 files changed, 188 insertions, 27 deletions
diff --git a/css/ContactsList.scss b/css/ContactsList.scss
index 75ef932a..43e8f17c 100644
--- a/css/ContactsList.scss
+++ b/css/ContactsList.scss
@@ -33,17 +33,3 @@
border-radius: 50%;
opacity: 1;
}
-
-// Virtual scroller overrides
-.vue-recycle-scroller {
- position: sticky !important;
-}
-
-.vue-recycle-scroller__item-view {
- // TODO: find better solution?
- // https://github.com/Akryum/vue-virtual-scroller/issues/70
- // hack to not show the transition
- overflow: hidden;
- // same as app-content-list-item
- height: 68px;
-}
diff --git a/src/components/ContactDetails.vue b/src/components/ContactDetails.vue
index 10c69a04..da1b7e87 100644
--- a/src/components/ContactDetails.vue
+++ b/src/components/ContactDetails.vue
@@ -23,17 +23,17 @@
<template>
<div id="contact-details" class="app-content-details">
<!-- nothing selected or contact not found -->
- <div v-if="!contact && !loading" id="emptycontent">
- <div class="icon-contacts" />
- <h2>{{ t('contacts', 'No contact selected') }}</h2>
- <p>{{ t('contacts', 'Select a contact on the list to begin') }}</p>
- </div>
+ <EmptyContent v-if="!contact && !loading" icon="icon-contacts-dark">
+ {{ t('contacts', 'No contact selected') }}
+ <template #desc>
+ {{ t('contacts', 'Select a contact on the list to begin') }}
+ </template>
+ </EmptyContent>
<!-- loading -->
- <div v-else-if="loading" id="emptycontent">
- <div class="icon-contacts" />
- <h2>{{ t('contacts', 'Loading') }}</h2>
- </div>
+ <EmptyContent v-else-if="loading" icon="icon-contacts-dark">
+ {{ t('contacts', 'Loading contacts …') }}
+ </EmptyContent>
<template v-else>
<!-- contact header -->
@@ -244,6 +244,7 @@ import validate from '../services/validate'
import AddNewProp from './ContactDetails/ContactDetailsAddNewProp'
import ContactAvatar from './ContactDetails/ContactDetailsAvatar'
import ContactProperty from './ContactDetails/ContactDetailsProperty'
+import EmptyContent from './EmptyContent'
import PropertyGroups from './Properties/PropertyGroups'
import PropertyRev from './Properties/PropertyRev'
import PropertySelect from './Properties/PropertySelect'
@@ -254,17 +255,18 @@ export default {
name: 'ContactDetails',
components: {
- Actions,
ActionButton,
ActionLink,
+ Actions,
AddNewProp,
ContactAvatar,
ContactProperty,
+ EmptyContent,
+ Modal,
+ Multiselect,
PropertyGroups,
PropertyRev,
PropertySelect,
- Modal,
- Multiselect,
},
props: {
diff --git a/src/components/ContactsList.vue b/src/components/ContactsList.vue
index f2200693..222b4f1e 100644
--- a/src/components/ContactsList.vue
+++ b/src/components/ContactsList.vue
@@ -24,6 +24,7 @@
<!-- same uid can coexists between different addressbooks
so we need to use the addressbook id as key as well -->
<RecycleScroller
+ v-if="haveContact"
id="contacts-list"
ref="scroller"
:class="{'icon-loading': loading, showdetails: selectedContact}"
@@ -40,10 +41,22 @@
@deleted="selectContact" />
</template>
</RecycleScroller>
+
+ <div v-else class="app-content-list">
+ <EmptyContent>
+ {{ t('contacts', 'No contacts in this group') }}
+ <template #action>
+ <button class="primary" @click="onAddContactsToGroup">
+ {{ t('forms', 'Add some') }}
+ </button>
+ </template>
+ </EmptyContent>
+ </div>
</template>
<script>
import ContactsListItem from './ContactsList/ContactsListItem'
+import EmptyContent from './EmptyContent'
import { RecycleScroller } from 'vue-virtual-scroller/dist/vue-virtual-scroller.umd.js'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
@@ -52,6 +65,7 @@ export default {
components: {
ContactsListItem,
+ EmptyContent,
RecycleScroller,
},
@@ -90,6 +104,9 @@ export default {
filteredList() {
return this.list.filter(contact => this.matchSearch(this.contacts[contact.key]))
},
+ haveContact() {
+ return this.selectedGroup && this.filteredList.length > 0
+ },
},
watch: {
@@ -160,6 +177,26 @@ export default {
}
return true
},
+
+ onAddContactsToGroup() {
+ // TODO: add popup
+ }
},
}
</script>
+
+<style lang="scss" scoped>
+// Virtual scroller overrides
+.vue-recycle-scroller {
+ position: sticky !important;
+}
+
+.vue-recycle-scroller__item-view {
+ // TODO: find better solution?
+ // https://github.com/Akryum/vue-virtual-scroller/issues/70
+ // hack to not show the transition
+ overflow: hidden;
+ // same as app-content-list-item
+ height: 68px;
+}
+</style> \ No newline at end of file
diff --git a/src/components/EmptyContent.vue b/src/components/EmptyContent.vue
new file mode 100644
index 00000000..5198f8ce
--- /dev/null
+++ b/src/components/EmptyContent.vue
@@ -0,0 +1,77 @@
+<!--
+ - @copyright Copyright (c) 2020 John Molakvoæ <skjnldsv@protonmail.com>
+ -
+ - @author John Molakvoæ <skjnldsv@protonmail.com>
+ -
+ - @license GNU AGPL version 3 or any later version
+ -
+ - This program is free software: you can redistribute it and/or modify
+ - it under the terms of the GNU Affero General Public License as
+ - published by the Free Software Foundation, either version 3 of the
+ - License, or (at your option) any later version.
+ -
+ - This program is distributed in the hope that it will be useful,
+ - but WITHOUT ANY WARRANTY; without even the implied warranty of
+ - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ - GNU Affero General Public License for more details.
+ -
+ - You should have received a copy of the GNU Affero General Public License
+ - along with this program. If not, see <http://www.gnu.org/licenses/>.
+ -
+ -->
+
+<template>
+ <div class="empty-content" role="note">
+ <div class="empty-content__icon" :class="icon" role="img" />
+ <h2 class="empty-content__title">
+ <slot />
+ </h2>
+ <p v-show="$slots.desc" class="empty-content__desc">
+ <slot name="desc" />
+ </p>
+ <div v-show="$slots.action" class="empty-content__action">
+ <slot name="action" />
+ </div>
+ </div>
+</template>
+
+<script>
+export default {
+ name: 'EmptyContent',
+
+ props: {
+ icon: {
+ type: String,
+ default: 'icon-forms',
+ },
+ },
+}
+</script>
+
+<style lang="scss">
+.empty-content {
+ margin-top: 20vh;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+
+ &__icon {
+ width: 64px;
+ height: 64px;
+ margin: 0 auto 15px;
+ opacity: .4;
+ background-size: 64px;
+ background-repeat: no-repeat;
+ background-position: center;
+ }
+
+ &__title {
+ margin-bottom: 8px;
+ }
+
+ &__desc {
+ margin-bottom: 16px;
+ }
+}
+
+</style>
diff --git a/src/store/groups.js b/src/store/groups.js
index 8df01b19..981e0932 100644
--- a/src/store/groups.js
+++ b/src/store/groups.js
@@ -105,6 +105,19 @@ const mutations = {
}
})
},
+
+ /**
+ * Add a group
+ *
+ * @param {Object} state the store data
+ * @param {string} groupName the name of the group
+ */
+ addGroup(state, groupName) {
+ state.groups.push({
+ name: groupName,
+ contacts: [],
+ })
+ },
}
const getters = {
@@ -146,6 +159,16 @@ const actions = {
removeContactToGroup(context, { groupName, contact }) {
context.commit('removeContactToGroup', { groupName, contact })
},
+
+ /**
+ * Add a group
+ *
+ * @param {Object} context the store mutations
+ * @param {string} groupName the name of the group
+ */
+ addGroup(context, groupName) {
+ context.commit('addGroup', groupName)
+ },
}
export default { state, mutations, getters, actions }
diff --git a/src/views/Contacts.vue b/src/views/Contacts.vue
index 13c5dc16..60784a97 100644
--- a/src/views/Contacts.vue
+++ b/src/views/Contacts.vue
@@ -52,6 +52,20 @@
{{ item.utils.counter }}
</AppNavigationCounter>
</AppNavigationItem>
+
+ <AppNavigationItem
+ :force-menu="true"
+ :menu-open.sync="isNewGroupMenuOpen"
+ :title="t('contacts', '+ New group')"
+ menu-icon="icon-add"
+ @click.prevent.stop="toggleNewGroupMenu">
+ <template slot="actions">
+ <ActionInput
+ icon="icon-contacts-dark"
+ :placeholder="t('contacts','Group name')"
+ @submit.prevent.stop="createNewGroup" />
+ </template>
+ </AppNavigationItem>
</ul>
<!-- settings -->
@@ -70,7 +84,9 @@
<div id="app-content-wrapper">
<!-- contacts list -->
- <ContactsList :list="contactsList"
+ <ContactsList
+ v-if="!loading"
+ :list="contactsList"
:contacts="contacts"
:loading="loading"
:search-query="searchQuery" />
@@ -96,6 +112,7 @@ import AppNavigationCounter from '@nextcloud/vue/dist/Components/AppNavigationCo
import AppNavigationNew from '@nextcloud/vue/dist/Components/AppNavigationNew'
import AppNavigationSettings from '@nextcloud/vue/dist/Components/AppNavigationSettings'
import ActionButton from '@nextcloud/vue/dist/Components/ActionButton'
+import ActionInput from '@nextcloud/vue/dist/Components/ActionInput'
import Content from '@nextcloud/vue/dist/Components/Content'
import Modal from '@nextcloud/vue/dist/Components/Modal'
import isMobile from '@nextcloud/vue/dist/Mixins/isMobile'
@@ -128,6 +145,7 @@ export default {
AppNavigationNew,
AppNavigationSettings,
ActionButton,
+ ActionInput,
ContactDetails,
ContactsList,
Content,
@@ -155,6 +173,8 @@ export default {
data() {
return {
+ isNewGroupMenuOpen: false,
+ isCreatingGroup: false,
loading: true,
searchQuery: '',
}
@@ -510,6 +530,22 @@ export default {
this.$store.dispatch('changeStage', 'default')
}
},
+
+ toggleNewGroupMenu() {
+ this.isNewGroupMenuOpen = !this.isNewGroupMenuOpen
+ },
+ createNewGroup(e) {
+ const input = e.target.querySelector('input[type=text]')
+ const groupName = input.value.trim()
+ this.$store.dispatch('addGroup', groupName)
+ this.isNewGroupMenuOpen = false
+ },
},
}
</script>
+
+<style lang="scss" scoped>
+#app-content-wrapper {
+ display: flex;
+}
+</style>