summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJohn Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>2020-06-19 07:36:52 +0200
committerJohn Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>2020-08-21 09:56:07 +0200
commit63b2aff43903d51fc382a7c6cb0019845363c183 (patch)
treea88f3787e8bf2f28fe05388b66bdb9f198b83512
parentd8ede82510afe55a06c754710463a45eab6e5d7c (diff)
Temp
Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
-rw-r--r--src/components/ContactDetails.vue8
-rw-r--r--src/components/ContactsList.vue4
-rw-r--r--src/components/EntityPicker/EntityBubble.vue109
-rw-r--r--src/components/EntityPicker/EntityPicker.vue334
-rw-r--r--src/components/EntityPicker/EntitySearchResult.vue122
-rw-r--r--src/components/Properties/PropertyGroups.vue6
-rw-r--r--src/views/Contacts.vue177
-rw-r--r--webpack.common.js3
8 files changed, 689 insertions, 74 deletions
diff --git a/src/components/ContactDetails.vue b/src/components/ContactDetails.vue
index da1b7e87..0735a3d0 100644
--- a/src/components/ContactDetails.vue
+++ b/src/components/ContactDetails.vue
@@ -105,7 +105,7 @@
show: true,
trigger: 'manual',
}"
- class="header-icon header-icon--pulse icon-history-force-white"
+ class="header-icon header-icon--pulse icon-history"
@click="refreshContact" />
<!-- repaired contact message -->
@@ -115,7 +115,7 @@
show: true,
trigger: 'manual',
}"
- class="header-icon header-icon--pulse icon-up-force-white"
+ class="header-icon header-icon--pulse icon-up"
@click="updateContact" />
<!-- menu actions -->
@@ -316,12 +316,12 @@ export default {
warning() {
if (!this.contact.dav) {
return {
- icon: 'icon-error-white header-icon--pulse',
+ icon: 'icon-error header-icon--pulse',
msg: t('contacts', 'This contact is not yet synced. Edit it to save it to the server.'),
}
} else if (this.isReadOnly) {
return {
- icon: 'icon-eye-white',
+ icon: 'icon-eye',
msg: t('contacts', 'This contact is in read-only mode. You do not have permission to edit this contact.'),
}
}
diff --git a/src/components/ContactsList.vue b/src/components/ContactsList.vue
index 222b4f1e..1a5f0745 100644
--- a/src/components/ContactsList.vue
+++ b/src/components/ContactsList.vue
@@ -180,7 +180,7 @@ export default {
onAddContactsToGroup() {
// TODO: add popup
- }
+ },
},
}
</script>
@@ -199,4 +199,4 @@ export default {
// same as app-content-list-item
height: 68px;
}
-</style> \ No newline at end of file
+</style>
diff --git a/src/components/EntityPicker/EntityBubble.vue b/src/components/EntityPicker/EntityBubble.vue
new file mode 100644
index 00000000..0394d9c2
--- /dev/null
+++ b/src/components/EntityPicker/EntityBubble.vue
@@ -0,0 +1,109 @@
+<!--
+ - @copyright Copyright (c) 2018 John Molakvoæ <skjnldsv@protonmail.com>
+ -
+ - @author John Molakvoæ <skjnldsv@protonmail.com>
+ - @author Team Popcorn <teampopcornberlin@gmail.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>
+ <UserBubble
+ class="entity-picker__bubble"
+ :margin="0"
+ :size="22"
+ :display-name="label">
+ <template #title>
+ <a href="#"
+ :title="t('contacts', 'Remove {type}', { type })"
+ class="entity-picker__bubble-delete icon-close"
+ @click="onDelete" />
+ </template>
+ </UserBubble>
+</template>
+
+<script>
+import UserBubble from '@nextcloud/vue/dist/Components/UserBubble'
+
+export default {
+ name: 'EntityBubble',
+
+ components: {
+ UserBubble,
+ },
+
+ props: {
+ /**
+ * Unique id of the entity
+ */
+ id: {
+ type: String,
+ required: true,
+ },
+
+ /**
+ * Label of the entity
+ */
+ label: {
+ type: String,
+ required: true,
+ },
+
+ /**
+ * Type of the entity. e.g user, circle, group...
+ */
+ type: {
+ type: String,
+ required: true,
+ },
+ },
+
+ methods: {
+ onDelete() {
+ // Emit delete. Be aware it might be unique
+ // amongst their types, but not unique amongst
+ // the whole selection all. Make sure to
+ // properly compare all necessary data.
+ this.$emit('delete', {
+ id: this.id,
+ type: this.type,
+ })
+ },
+ },
+}
+</script>
+
+<style lang="scss" scoped>
+// better visual with light default tint
+::v-deep .user-bubble__content {
+ background-color: var(--color-primary-light);
+}
+
+.entity-picker__bubble-delete {
+ display: block;
+ height: 100%;
+ // squeeze in the border radius
+ margin-right: -4px;
+ opacity: .7;
+
+ &:hover,
+ &:active,
+ &:focus {
+ opacity: 1;
+ }
+}
+
+</style>
diff --git a/src/components/EntityPicker/EntityPicker.vue b/src/components/EntityPicker/EntityPicker.vue
new file mode 100644
index 00000000..d0421af0
--- /dev/null
+++ b/src/components/EntityPicker/EntityPicker.vue
@@ -0,0 +1,334 @@
+<!--
+ - @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@pm.me>
+ -
+ - @author Marco Ambrosini <marcoambrosini@pm.me>
+ -
+ - @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>
+ <Modal
+ size="full"
+ @close="onCancel">
+ <!-- Wrapper for content & navigation -->
+ <div
+ class="entity-picker">
+ <!-- Search -->
+ <div class="entity-picker__search">
+ <div class="entity-picker__search-icon icon-search" />
+ <input
+ v-model="searchQuery"
+ class="entity-picker__search-input"
+ type="search"
+ :placeholder="t('contacts', 'Search contacts')">
+ </div>
+
+ <!-- Content -->
+ <div class="entity-picker__content">
+ <!-- Picked entities -->
+ <transition-group
+ v-if="selection.length > 0"
+ name="zoom"
+ tag="ul"
+ class="entity-picker__selection">
+ <EntityBubble
+ v-for="entity in selection"
+ :key="entity.key || `entity-${entity.type}-${entity.id}`"
+ v-bind="entity"
+ @delete="onDelete(entity)" />
+ </transition-group>
+
+ <!-- Searched & picked entities -->
+ <template v-if="searchSet.length > 0 && availableEntities.length > 0">
+ <section v-for="type in availableEntities" :key="type.id" class="entity-picker__options">
+ <h4 v-if="isSingleType" class="entity-picker__options-caption">
+ {{ t('contacts', 'Add {type}', {type: type.label}) }}
+ </h4>
+ <EntitySearchResult v-for="entity in type.dataSet"
+ :key="entity.key || `entity-${entity.type}-${entity.id}`"
+ v-bind="entity" />
+ </section>
+ </template>
+ <EmptyContent v-else-if="searchQuery" icon="icon-search">
+ {{ t('contacts', 'No results') }}
+ </EmptyContent>
+ <!-- TODO: find better wording/icon -->
+ <EmptyContent v-else icon="">
+ {{ t('contacts', 'Loading …') }}
+ </EmptyContent>
+ </div>
+
+ <div class="entity-picker__navigation">
+ <button
+ class="navigation__button-left"
+ @click="onCancel">
+ {{ t('contacts', 'Cancel') }}
+ </button>
+ <button
+ class="navigation__button-right primary"
+ @click="onSubmit">
+ {{ t('contacts', 'Add to group') }}
+ </button>
+ </div>
+ </div>
+ </modal>
+</template>
+
+<script>
+import Modal from '@nextcloud/vue/dist/Components/Modal'
+import EmptyContent from '@nextcloud/vue/dist/Components/EmptyContent'
+import EntityBubble from './EntityBubble'
+import EntitySearchResult from './EntitySearchResult'
+
+export default {
+ name: 'EntityPicker',
+
+ components: {
+ EmptyContent,
+ EntityBubble,
+ EntitySearchResult,
+ Modal,
+ },
+
+ props: {
+ dataSet: {
+ type: Array,
+ required: true,
+ },
+ dataTypes: {
+ type: Array,
+ required: true,
+ },
+ sort: {
+ type: String,
+ default: 'label',
+ },
+ },
+
+ data() {
+ return {
+ searchQuery: '',
+ selection: [
+ { type: 'user', label: 'Test 4 Lorem ipsum is a very long user name', id: 'test4' },
+ { type: 'user', label: 'Test 2', id: 'test2' },
+ { type: 'user', label: 'Test 1 (AVHJ)', id: 'tes2' },
+ { type: 'user', label: 'Test 278 975 869', id: 'tebst2' },
+ { type: 'user', label: 'Administrator', id: 'tespot2' },
+ ],
+ }
+ },
+
+ computed: {
+ /**
+ * Are we handling a single entity type ?
+ * @returns {boolean}
+ */
+ isSingleType() {
+ return !(this.dataTypes.length > 1)
+ },
+
+ /**
+ * Available data based on current search if query
+ * is valid, returns default full data et otherwise
+ * @returns {Object[]}
+ */
+ searchSet() {
+ if (this.searchQuery && this.searchQuery.trim !== '') {
+ return this.dataSet.filter(entity => {
+ return entity.label.indexOf(this.searchQuery) > -1
+ })
+ }
+ return this.dataSet
+ },
+
+ /**
+ * Returns available entities grouped by type(s) if any
+ * @returns {Array[]}
+ */
+ availableEntities() {
+ // If only one type, return the full set directly
+ if (this.isSingleType) {
+ return [{
+ id: this.dataTypes[0].id,
+ label: this.dataTypes[0].label,
+ dataSet: this.searchSet,
+ }]
+ }
+
+ // Else group by types
+ return this.dataTypes.map(type => ({
+ id: type.id,
+ label: type.label,
+ dataSet: this.searchSet.filter(entity => entity.type === type),
+ }))
+ },
+ },
+
+ methods: {
+ onCancel() {
+ this.$emit('close')
+ },
+ onSubmit() {
+ this.$emit('submit', this.selection)
+ },
+ /**
+ * Remove entity from selection
+ * @param {Object} entity the entity to remove
+ */
+ onDelete(entity) {
+ const index = this.selection.findIndex(search => search === entity)
+ this.selection.splice(index, 1)
+ console.debug('Removing entity from selection', entity)
+ },
+ },
+
+}
+
+</script>
+
+<style lang="scss" scoped>
+
+// Dialog variables
+$dialog-margin: 20px;
+$dialog-width: 300px;
+$dialog-height: 480px;
+
+// https://uxplanet.org/7-rules-for-mobile-ui-button-design-e9cf2ea54556
+// recommended is 48px
+// 44px is what we choose and have very good visual-to-usability ratio
+$clickable-area: 44px;
+
+// background icon size
+// also used for the scss icon font
+$icon-size: 16px;
+
+// icon padding for a $clickable-area width and a $icon-size icon
+// ( 44px - 16px ) / 2
+$icon-margin: ($clickable-area - $icon-size) / 2;
+
+.entity-picker {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ /** This next 2 rules are pretty hacky, with the modal component somehow
+ the margin applied to the content is added to the total modal width,
+ so here we subtract it to the width and height of the content.
+ */
+ width: $dialog-width - $dialog-margin * 2;
+ height: $dialog-height - $dialog-margin * 2;
+ margin: $dialog-margin;
+
+ &__search {
+ position: relative;
+ display: flex;
+ align-items: center;
+ &-input {
+ width: 100%;
+ height: $clickable-area !important;
+ padding-left: $clickable-area;
+ font-size: 16px;
+ line-height: $clickable-area;
+ }
+ &-icon {
+ position: absolute;
+ width: $clickable-area;
+ height: $clickable-area;
+ }
+ }
+
+ &__content {
+ height: 100%;
+ }
+
+ &__selection {
+ display: flex;
+ overflow-y: auto;
+ align-content: flex-start;
+ flex: 1 0 auto;
+ flex-wrap: wrap;
+ // half a line height to know there is more lines
+ max-height: 6.5em;
+ padding: 4px 0;
+ border-bottom: 1px solid var(--color-background-darker);
+ }
+
+ &__navigation {
+ z-index: 1;
+ display: flex;
+ // define our base width, no shrinkage
+ flex: 0 0;
+ justify-content: space-between;
+ // Same as above
+ width: $dialog-width - $dialog-margin * 2;
+ box-shadow: 0 -10px 5px var(--color-main-background);
+ &__button-right {
+ margin-left: auto;
+ }
+ }
+}
+
+.entity-picker__options {
+ margin: 4px 0;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ &-caption {
+ color: var(--color-primary);
+ line-height: $clickable-area;
+ list-style-type: none;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ box-shadow: none !important;
+ user-select: none;
+ pointer-events: none;
+ padding-left: 10px;
+
+ &:not(:first-child) {
+ margin-top: $clickable-area / 2;
+ }
+ }
+}
+
+/** Size full in the modal component doesn't have border radius, this adds
+it back */
+::v-deep .modal-container {
+ border-radius: var(--border-radius-large) !important;
+}
+
+</style>
+
+<style lang="scss" scoped>
+.zoom-enter-active {
+ animation: zoom-in var(--animation-quick);
+}
+
+.zoom-leave-active {
+ animation: zoom-in var(--animation-quick) reverse;
+
+ will-change: transform;
+}
+
+@keyframes zoom-in {
+ 0% {
+ transform: scale(0);
+ }
+ 100% {
+ transform: scale(1);
+ }
+}
+
+</style>
diff --git a/src/components/EntityPicker/EntitySearchResult.vue b/src/components/EntityPicker/EntitySearchResult.vue
new file mode 100644
index 00000000..e0a25109
--- /dev/null
+++ b/src/components/EntityPicker/EntitySearchResult.vue
@@ -0,0 +1,122 @@
+<!--
+ - @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>
+ <UserBubble
+ class="entity-picker__bubble"
+ :class="{'entity-picker__bubble--selected': selected}"
+ :display-name="label"
+ :margin="6"
+ :size="44"
+ url="#"
+ @click="onClick">
+ <template #title>
+ <span class="entity-picker__bubble-checkmark icon-checkmark" />
+ </template>
+ </UserBubble>
+</template>
+
+<script>
+import UserBubble from '@nextcloud/vue/dist/Components/UserBubble'
+
+export default {
+ name: 'EntitySearchResult',
+
+ components: {
+ UserBubble,
+ },
+
+ props: {
+ /**
+ * Unique id of the entity
+ */
+ id: {
+ type: String,
+ required: true,
+ },
+
+ /**
+ * Label of the entity
+ */
+ label: {
+ type: String,
+ required: true,
+ },
+
+ /**
+ * Label of the entity
+ */
+ selected: {
+ type: Boolean,
+ default: false,
+ },
+ },
+
+ methods: {
+ onClick(event) {
+ console.info(event)
+ this.$emit('click', event)
+ },
+ },
+}
+
+</script>
+
+<style lang="scss" scoped>
+.entity-picker__bubble {
+ display: flex;
+ margin-bottom: 4px;
+ .entity-picker__bubble-checkmark {
+ display: block;
+ opacity: 0;
+ }
+
+ &--selected,
+ &:hover,
+ &:focus {
+ .entity-picker__bubble-checkmark {
+ opacity: 1;
+ }
+ ::v-deep .user-bubble__content {
+ // better visual with light default tint
+ background-color: var(--color-primary-light);
+ }
+ }
+}
+
+::v-deep .user-bubble__content {
+ // Take full width
+ width: 100%;
+ // Override default styling
+ background: none;
+ .user-bubble__secondary {
+ // Force show checkmark
+ display: inline-flex;
+ margin-right: 4px;
+ margin-left: auto;
+ }
+ &, * {
+ // the whole row is clickable,let's force the proper cursor
+ cursor: pointer;
+ }
+}
+
+</style>
diff --git a/src/components/Properties/PropertyGroups.vue b/src/components/Properties/PropertyGroups.vue
index 03026992..63990e08 100644
--- a/src/components/Properties/PropertyGroups.vue
+++ b/src/components/Properties/PropertyGroups.vue
@@ -22,7 +22,9 @@
<template>
<div v-if="propModel" class="grid-span-2 property property--without-actions">
- <!-- NO title if first element for groups -->
+ <PropertyTitle
+ icon="icon-contacts-dark"
+ :readable-name="t('contacts', 'Groups')" />
<div class="property__row">
<div class="property__label">
@@ -60,11 +62,13 @@
import debounce from 'debounce'
import Multiselect from '@nextcloud/vue/dist/Components/Multiselect'
import Contact from '../../models/contact'
+import PropertyTitle from './PropertyTitle'
export default {
name: 'PropertyGroups',
components: {
+ PropertyTitle,
Multiselect,
},
diff --git a/src/views/Contacts.vue b/src/views/Contacts.vue
index 60784a97..0f4364a8 100644
--- a/src/views/Contacts.vue
+++ b/src/views/Contacts.vue
@@ -35,21 +35,57 @@
<!-- groups list -->
<ul v-if="!loading" id="groups-list">
- <AppNavigationItem v-for="item in menu"
- :key="item.key"
- :to="item.router"
- :title="item.text"
- :icon="item.icon">
+ <!-- All contacts group -->
+ <AppNavigationItem id="everyone"
+ :title="GROUP_ALL_CONTACTS"
+ :to="{
+ name: 'group',
+ params: { selectedGroup: GROUP_ALL_CONTACTS },
+ }"
+ icon="icon-contacts-dark">
+ <AppNavigationCounter slot="counter">
+ {{ sortedContacts.length }}
+ </AppNavigationCounter>
+ </AppNavigationItem>
+
+ <!-- Not grouped group -->
+ <AppNavigationItem
+ v-if="ungroupedContacts.length > 0"
+ id="notgrouped"
+ :title="GROUP_NO_GROUP_CONTACTS"
+ :to="{
+ name: 'group',
+ params: { selectedGroup: GROUP_NO_GROUP_CONTACTS },
+ }"
+ icon="icon-user">
+ <AppNavigationCounter slot="counter">
+ {{ ungroupedContacts.length }}
+ </AppNavigationCounter>
+ </AppNavigationItem>
+
+ <AppNavigationSpacer />
+
+ <!-- Custom groups -->
+ <AppNavigationItem v-for="group in groupsMenu"
+ :key="group.key"
+ :to="group.router"
+ :title="group.name"
+ :icon="group.icon">
<template slot="actions">
- <ActionButton v-for="action in item.utils.actions"
- :key="action.text"
- :icon="action.icon"
- @click="action.action">
- {{ action.text }}
+ <ActionButton
+ icon="icon-add"
+ @click="addContactsToGroup(group)">
+ {{ t('contacts', 'Add contacts to this group') }}
+ </ActionButton>
+ <ActionButton
+ icon="icon-download"
+ @click="downloadGroup(group)">
+ {{ t('contacts', 'Download') }}
</ActionButton>
</template>
+
<AppNavigationCounter slot="counter">
- {{ item.utils.counter }}
+ {{ group.contacts.length }}
</AppNavigationCounter>
</AppNavigationItem>
@@ -95,12 +131,21 @@
<ContactDetails :loading="loading" :contact-key="selectedContact" />
</div>
</AppContent>
+
+ <!-- Import modal -->
<Modal v-if="isImporting"
:clear-view-delay="-1"
:can-close="isImportDone"
@close="closeImport">
<ImportScreen />
</Modal>
+
+ <!-- Select contacts group modal -->
+ <EntityPicker v-if="showContactPicker"
+ :data-types="pickerTypes"
+ :data-set="pickerData"
+ @close="onContactPickerClose"
+ @submit="onContactPickerPick" />
</Content>
</template>
@@ -108,6 +153,7 @@
import AppContent from '@nextcloud/vue/dist/Components/AppContent'
import AppNavigation from '@nextcloud/vue/dist/Components/AppNavigation'
import AppNavigationItem from '@nextcloud/vue/dist/Components/AppNavigationItem'
+import AppNavigationSpacer from '@nextcloud/vue/dist/Components/AppNavigationSpacer'
import AppNavigationCounter from '@nextcloud/vue/dist/Components/AppNavigationCounter'
import AppNavigationNew from '@nextcloud/vue/dist/Components/AppNavigationNew'
import AppNavigationSettings from '@nextcloud/vue/dist/Components/AppNavigationSettings'
@@ -125,6 +171,7 @@ import SettingsSection from '../components/SettingsSection'
import ContactsList from '../components/ContactsList'
import ContactDetails from '../components/ContactDetails'
import ImportScreen from '../components/ImportScreen'
+import EntityPicker from '../components/EntityPicker/EntityPicker'
import Contact from '../models/contact'
import rfcProps from '../models/rfcProps'
@@ -144,12 +191,14 @@ export default {
AppNavigationCounter,
AppNavigationNew,
AppNavigationSettings,
+ AppNavigationSpacer,
ActionButton,
ActionInput,
ContactDetails,
ContactsList,
Content,
ImportScreen,
+ EntityPicker,
Modal,
SettingsSection,
},
@@ -173,10 +222,18 @@ export default {
data() {
return {
- isNewGroupMenuOpen: false,
+ GROUP_ALL_CONTACTS,
+ GROUP_NO_GROUP_CONTACTS,
isCreatingGroup: false,
+ isNewGroupMenuOpen: false,
loading: true,
searchQuery: '',
+ showContactPicker: true,
+ contactPickerforGroup: null,
+ pickerTypes: [{
+ id: 'contacts',
+ label: t('contacts', 'contacts'),
+ }],
}
},
@@ -242,70 +299,32 @@ export default {
// generate groups menu from groups store
groupsMenu() {
return this.groups.map(group => {
- return {
+ return Object.assign(group, {
id: group.name.replace(' ', '_'),
key: group.name.replace(' ', '_'),
router: {
name: 'group',
params: { selectedGroup: group.name },
},
- text: group.name,
- utils: {
- counter: group.contacts.length,
- actions: [
- {
- icon: 'icon-download',
- text: 'Download',
- action: () => this.downloadGroup(group),
- },
- ],
- },
- }
+ icon: group.name === t('contactsinteraction', 'Recently contacted')
+ ? 'icon-recent-actors'
+ : '',
+ })
}).sort(function(a, b) {
- return parseInt(b.utils.counter) - parseInt(a.utils.counter)
+ return parseInt(b.contacts.length) - parseInt(a.contacts.length)
})
},
- // building the main menu
- menu() {
- return this.groupAllGroup.concat(this.groupNotGrouped.concat(this.groupsMenu))
- },
-
- // default group for every contacts
- groupAllGroup() {
- return [{
- id: 'everyone',
- key: 'everyone',
- icon: 'icon-contacts-dark',
- router: {
- name: 'group',
- params: { selectedGroup: GROUP_ALL_CONTACTS },
- },
- text: GROUP_ALL_CONTACTS,
- utils: {
- counter: this.sortedContacts.length,
- },
- }]
- },
-
- // default group for every contacts
- groupNotGrouped() {
- if (this.ungroupedContacts.length === 0) {
- return []
- }
- return [{
- id: 'notgrouped',
- key: 'notgrouped',
- icon: 'icon-user',
- router: {
- name: 'group',
- params: { selectedGroup: GROUP_NO_GROUP_CONTACTS },
- },
- text: GROUP_NO_GROUP_CONTACTS,
- utils: {
- counter: this.ungroupedContacts.length,
- },
- }]
+ /**
+ * Contacts formatted for the EntityPicker
+ * @returns {Array}
+ */
+ pickerData() {
+ return Object.values(this.contacts).map(contact => ({
+ id: contact.key,
+ label: contact.displayName,
+ type: 'contact',
+ }))
},
},
@@ -539,7 +558,33 @@ export default {
const groupName = input.value.trim()
this.$store.dispatch('addGroup', groupName)
this.isNewGroupMenuOpen = false
+
+ // Select group
+ this.$router.push({
+ name: 'contact',
+ params: {
+ selectedGroup: groupName,
+ selectedContact: undefined,
+ },
+ })
+ },
+
+ // Bulk contacts group management handlers
+ addContactsToGroup(group) {
+ this.showContactPicker = true
+ this.contactPickerforGroup = group
},
+
+ onContactPickerClose() {
+ this.showContactPicker = false
+ },
+
+ onContactPickerPick(selection) {
+ const group = this.contactPickerforGroup
+ console.info('Adding', selection, 'to group', group)
+ this.contactPickerforGroup = null
+ },
+
},
}</