mirror of https://github.com/nextcloud/contacts
428 lines
11 KiB
Vue
428 lines
11 KiB
Vue
<!--
|
|
- @copyright Copyright (c) 2018 John Molakvoæ <skjnldsv@protonmail.com>
|
|
-
|
|
- @author John Molakvoæ <skjnldsv@protonmail.com>
|
|
- @author Charismatic Claire <charismatic.claire@noservice.noreply>
|
|
-
|
|
- @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>
|
|
<Content app-name="contacts">
|
|
<!-- go back to list when in details mode -->
|
|
<a v-if="selectedContact && isMobile"
|
|
class="app-details-toggle icon-confirm"
|
|
href="#"
|
|
@click.stop.prevent="showList" />
|
|
|
|
<!-- new-contact-button + navigation + settings -->
|
|
<RootNavigation
|
|
:contacts-list="contactsList"
|
|
:loading="loadingContacts || loadingCircles"
|
|
:selected-group="selectedGroup"
|
|
:selected-contact="selectedContact">
|
|
<!-- new-contact-button -->
|
|
<AppNavigationNew v-if="!loadingContacts"
|
|
button-id="new-contact-button"
|
|
:text="t('contacts','New contact')"
|
|
button-class="icon-add"
|
|
:disabled="!defaultAddressbook"
|
|
@click="newContact" />
|
|
</RootNavigation>
|
|
|
|
<!-- Main content: circle or contacts -->
|
|
<CircleContent v-if="selectedCircle"
|
|
:loading="loadingCircles" />
|
|
<ContactsContent v-else
|
|
:contacts-list="contactsList"
|
|
:loading="loadingContacts"
|
|
@newContact="newContact" />
|
|
|
|
<!-- Import modal -->
|
|
<Modal v-if="isImporting"
|
|
:clear-view-delay="-1"
|
|
:can-close="isImportDone"
|
|
@close="closeImport">
|
|
<ImportView @close="closeImport" />
|
|
</Modal>
|
|
|
|
<!-- Select contacts group modal -->
|
|
<ContactsPicker />
|
|
</Content>
|
|
</template>
|
|
|
|
<script>
|
|
import { GROUP_ALL_CONTACTS, GROUP_NO_GROUP_CONTACTS, ROUTE_CIRCLE } from '../models/constants.ts'
|
|
|
|
import AppNavigationNew from '@nextcloud/vue/dist/Components/AppNavigationNew'
|
|
import Content from '@nextcloud/vue/dist/Components/Content'
|
|
import isMobile from '@nextcloud/vue/dist/Mixins/isMobile'
|
|
import Modal from '@nextcloud/vue/dist/Components/Modal'
|
|
|
|
import { showError } from '@nextcloud/dialogs'
|
|
import { VCardTime } from 'ical.js'
|
|
|
|
import CircleContent from '../components/AppContent/CircleContent'
|
|
import ContactsContent from '../components/AppContent/ContactsContent'
|
|
import ContactsPicker from '../components/EntityPicker/ContactsPicker'
|
|
import ImportView from './Processing/ImportView'
|
|
import RootNavigation from '../components/AppNavigation/RootNavigation'
|
|
|
|
import Contact from '../models/contact'
|
|
import rfcProps from '../models/rfcProps'
|
|
|
|
import client from '../services/cdav'
|
|
|
|
export default {
|
|
name: 'Contacts',
|
|
|
|
components: {
|
|
AppNavigationNew,
|
|
CircleContent,
|
|
ContactsContent,
|
|
ContactsPicker,
|
|
Content,
|
|
ImportView,
|
|
Modal,
|
|
RootNavigation,
|
|
},
|
|
|
|
mixins: [
|
|
isMobile,
|
|
],
|
|
|
|
// passed by the router
|
|
props: {
|
|
selectedCircle: {
|
|
type: String,
|
|
default: undefined,
|
|
},
|
|
selectedGroup: {
|
|
type: String,
|
|
default: undefined,
|
|
},
|
|
selectedContact: {
|
|
type: String,
|
|
default: undefined,
|
|
},
|
|
},
|
|
|
|
data() {
|
|
return {
|
|
loadingCircles: true,
|
|
loadingContacts: true,
|
|
}
|
|
},
|
|
|
|
computed: {
|
|
// store getters
|
|
addressbooks() {
|
|
return this.$store.getters.getAddressbooks
|
|
},
|
|
contacts() {
|
|
return this.$store.getters.getContacts
|
|
},
|
|
sortedContacts() {
|
|
return this.$store.getters.getSortedContacts
|
|
},
|
|
groups() {
|
|
return this.$store.getters.getGroups
|
|
},
|
|
orderKey() {
|
|
return this.$store.getters.getOrderKey
|
|
},
|
|
importState() {
|
|
return this.$store.getters.getImportState
|
|
},
|
|
|
|
/**
|
|
* Are we importing contacts ?
|
|
* @returns {boolean}
|
|
*/
|
|
isImporting() {
|
|
return this.importState.stage !== 'default'
|
|
},
|
|
/**
|
|
* Are we done importing contacts ?
|
|
* @returns {boolean}
|
|
*/
|
|
isImportDone() {
|
|
return this.importState.stage === 'done'
|
|
},
|
|
|
|
// first enabled addressbook of the list
|
|
defaultAddressbook() {
|
|
return this.addressbooks.find(addressbook => !addressbook.readOnly && addressbook.enabled)
|
|
},
|
|
|
|
/**
|
|
* Contacts list based on the selected group.
|
|
* Those filters are pretty fast, so let's only
|
|
* intersect the groups contacts and the full
|
|
* sorted contacts List.
|
|
*
|
|
* @returns {Array}
|
|
*/
|
|
contactsList() {
|
|
if (this.selectedGroup === GROUP_ALL_CONTACTS) {
|
|
return this.sortedContacts
|
|
} else if (this.selectedGroup === GROUP_NO_GROUP_CONTACTS) {
|
|
return this.ungroupedContacts.map(contact => this.sortedContacts.find(item => item.key === contact.key))
|
|
}
|
|
const group = this.groups.filter(group => group.name === this.selectedGroup)[0]
|
|
if (group) {
|
|
return this.sortedContacts.filter(contact => group.contacts.indexOf(contact.key) >= 0)
|
|
}
|
|
return []
|
|
},
|
|
|
|
ungroupedContacts() {
|
|
return this.sortedContacts.filter(contact => this.contacts[contact.key].groups && this.contacts[contact.key].groups.length === 0)
|
|
},
|
|
},
|
|
|
|
watch: {
|
|
// watch url change and group select
|
|
selectedGroup() {
|
|
if (!this.isMobile) {
|
|
this.selectFirstContactIfNone()
|
|
}
|
|
},
|
|
// watch url change and contact select
|
|
selectedContact() {
|
|
if (!this.isMobile) {
|
|
this.selectFirstContactIfNone()
|
|
}
|
|
},
|
|
},
|
|
|
|
mounted() {
|
|
// Register search
|
|
this.search = new OCA.Search(this.search, this.resetSearch)
|
|
},
|
|
|
|
async beforeMount() {
|
|
// get addressbooks then get contacts
|
|
client.connect({ enableCardDAV: true }).then(() => {
|
|
console.debug('Connected to dav!', client)
|
|
this.$store.dispatch('getAddressbooks')
|
|
.then((addressbooks) => {
|
|
const writeableAddressBooks = addressbooks.filter(addressbook => !addressbook.readOnly)
|
|
|
|
// No writeable addressbooks? Create a new one!
|
|
if (writeableAddressBooks.length === 0) {
|
|
this.$store.dispatch('appendAddressbook', { displayName: t('contacts', 'Contacts') })
|
|
.then(() => {
|
|
this.fetchContacts()
|
|
})
|
|
// else, let's get those contacts!
|
|
} else {
|
|
this.fetchContacts()
|
|
}
|
|
})
|
|
// check local storage for orderKey
|
|
if (localStorage.getItem('orderKey')) {
|
|
// run setOrder mutation with local storage key
|
|
this.$store.commit('setOrder', localStorage.getItem('orderKey'))
|
|
}
|
|
})
|
|
|
|
// Get circles
|
|
this.$store.dispatch('getCircles').then(() => {
|
|
this.loadingCircles = false
|
|
})
|
|
},
|
|
|
|
methods: {
|
|
async newContact() {
|
|
const rev = new VCardTime()
|
|
const contact = new Contact(`
|
|
BEGIN:VCARD
|
|
VERSION:4.0
|
|
PRODID:-//Nextcloud Contacts v${appVersion}
|
|
END:VCARD
|
|
`.trim().replace(/\t/gm, ''),
|
|
this.defaultAddressbook)
|
|
|
|
contact.fullName = t('contacts', 'New contact')
|
|
rev.fromUnixTime(Date.now() / 1000)
|
|
contact.rev = rev
|
|
|
|
// itterate over all properties (filter is not usable on objects and we need the key of the property)
|
|
const properties = rfcProps.properties
|
|
for (const name in properties) {
|
|
if (properties[name].default) {
|
|
const defaultData = properties[name].defaultValue
|
|
// add default field
|
|
const property = contact.vCard.addPropertyWithValue(name, defaultData.value)
|
|
// add default type
|
|
if (defaultData.type) {
|
|
property.setParameter('type', defaultData.type)
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
// set group if it's selected already
|
|
// BUT NOT if it's the _fake_ groups like all contacts and not grouped
|
|
if ([GROUP_ALL_CONTACTS, GROUP_NO_GROUP_CONTACTS].indexOf(this.selectedGroup) === -1) {
|
|
contact.groups = [this.selectedGroup]
|
|
}
|
|
try {
|
|
// this will trigger the proper commits to groups, contacts and addressbook
|
|
await this.$store.dispatch('addContact', contact)
|
|
await this.$router.push({
|
|
name: 'contact',
|
|
params: {
|
|
selectedGroup: this.selectedGroup,
|
|
selectedContact: contact.key,
|
|
},
|
|
})
|
|
} catch (error) {
|
|
showError(t('contacts', 'Unable to create the contact.'))
|
|
console.error(error)
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Dispatch sorting update request to the store
|
|
*
|
|
* @param {string} orderKey the object key to order by
|
|
*/
|
|
updateSorting(orderKey = 'displayName') {
|
|
this.$store.commit('setOrder', orderKey)
|
|
this.$store.commit('sortContacts')
|
|
},
|
|
|
|
/**
|
|
* Fetch the contacts of each addressbooks
|
|
*/
|
|
fetchContacts() {
|
|
// wait for all addressbooks to have fetch their contacts
|
|
Promise.all(this.addressbooks
|
|
.filter(addressbook => addressbook.enabled)
|
|
.map(addressbook => {
|
|
return this.$store.dispatch('getContactsFromAddressBook', { addressbook })
|
|
})
|
|
).then(results => {
|
|
this.loadingContacts = false
|
|
if (!this.isMobile) {
|
|
this.selectFirstContactIfNone()
|
|
}
|
|
})
|
|
},
|
|
|
|
/**
|
|
* Select the first contact of the list
|
|
* if none are selected already
|
|
*/
|
|
selectFirstContactIfNone() {
|
|
// Do not redirect if pending import
|
|
if (this.$route.name === 'import') {
|
|
return
|
|
}
|
|
|
|
const inList = this.contactsList.findIndex(contact => contact.key === this.selectedContact) > -1
|
|
if (this.selectedContact === undefined || !inList) {
|
|
// Unknown contact
|
|
if (this.selectedContact && !inList) {
|
|
showError(t('contacts', 'Contact not found'))
|
|
this.$router.push({
|
|
name: 'group',
|
|
params: {
|
|
selectedGroup: this.selectedGroup,
|
|
},
|
|
})
|
|
}
|
|
|
|
// Unknown group
|
|
if (!this.selectedCircle
|
|
&& !this.groups.find(group => group.name === this.selectedGroup)
|
|
&& GROUP_ALL_CONTACTS !== this.selectedGroup
|
|
&& GROUP_NO_GROUP_CONTACTS !== this.selectedGroup
|
|
&& ROUTE_CIRCLE !== this.selectedGroup) {
|
|
showError(t('contacts', 'Group {group} not found', { group: this.selectedGroup }))
|
|
console.error('Group not found', this.selectedGroup)
|
|
|
|
this.$router.push({
|
|
name: 'root',
|
|
})
|
|
return
|
|
}
|
|
|
|
if (Object.keys(this.contactsList).length) {
|
|
this.$router.push({
|
|
name: 'contact',
|
|
params: {
|
|
selectedGroup: this.selectedGroup,
|
|
selectedContact: Object.values(this.contactsList)[0].key,
|
|
},
|
|
})
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Show the list and deselect contact
|
|
*/
|
|
showList() {
|
|
// Reset the selected contact
|
|
this.$router.push({
|
|
name: 'contact',
|
|
params: {
|
|
selectedGroup: this.selectedGroup,
|
|
selectedContact: undefined,
|
|
},
|
|
})
|
|
},
|
|
|
|
/**
|
|
* Done importing, the user closed the import status screen
|
|
*/
|
|
closeImport() {
|
|
this.$store.dispatch('changeStage', 'default')
|
|
},
|
|
},
|
|
}
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.app-details-toggle {
|
|
position: absolute;
|
|
width: 44px;
|
|
height: 44px;
|
|
padding: 14px;
|
|
cursor: pointer;
|
|
opacity: .6;
|
|
font-size: 16px;
|
|
line-height: 17px;
|
|
transform: rotate(180deg);
|
|
background-color: var(--color-main-background);
|
|
z-index: 2000;
|
|
&:active,
|
|
&:hover,
|
|
&:focus {
|
|
opacity: 1;
|
|
}
|
|
|
|
// Hide app-navigation toggle if shown
|
|
&::v-deep + .app-navigation .app-navigation-toggle {
|
|
display: none;
|
|
}
|
|
}
|
|
</style>
|