diff options
author | John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com> | 2018-08-20 17:40:17 +0200 |
---|---|---|
committer | John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com> | 2018-08-21 18:17:45 +0200 |
commit | 0b5f975b5288e1a9548260f9ac8f47453982f9a7 (patch) | |
tree | 41c70a22dc26fe6bf42eb4eb5bee22e94d645e87 /src | |
parent | 48d0f179817b784eb5133b857ad9981d72dd0ac1 (diff) |
Type update, management and auto selection fix
Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
Diffstat (limited to 'src')
-rw-r--r-- | src/components/ContactDetails.vue | 13 | ||||
-rw-r--r-- | src/components/ContactDetails/ContactDetailsProperty.vue | 45 | ||||
-rw-r--r-- | src/components/properties/PropertyDateTime.vue | 17 | ||||
-rw-r--r-- | src/components/properties/PropertyGroups.vue | 45 | ||||
-rw-r--r-- | src/components/properties/PropertyMultipleText.vue | 17 | ||||
-rw-r--r-- | src/components/properties/PropertyText.vue | 19 | ||||
-rw-r--r-- | src/store/addressbooks.js | 2 | ||||
-rw-r--r-- | src/store/groups.js | 68 |
8 files changed, 168 insertions, 58 deletions
diff --git a/src/components/ContactDetails.vue b/src/components/ContactDetails.vue index f732befd..95de110d 100644 --- a/src/components/ContactDetails.vue +++ b/src/components/ContactDetails.vue @@ -81,24 +81,25 @@ <!-- contact details --> <section class="contact-details"> <contact-details-property v-for="(property, index) in sortedProperties" :key="index" :index="index" - :sorted-properties="sortedProperties" :property="property" @updatedcontact="updateContact" /> + :sorted-properties="sortedProperties" :property="property" :contact="contact" + @updatedcontact="updateContact" /> </section> </template> </div> </template> <script> -import popoverMenu from './core/popoverMenu' -import contactDetailsProperty from './ContactDetails/ContactDetailsProperty' - -import Contact from '../models/contact' -import rfcProps from '../models/rfcProps.js' import ICAL from 'ical.js' import ClickOutside from 'vue-click-outside' import Vue from 'vue' import VTooltip from 'v-tooltip' import debounce from 'debounce' +import Contact from '../models/contact' +import rfcProps from '../models/rfcProps.js' + +import popoverMenu from './core/popoverMenu' +import contactDetailsProperty from './ContactDetails/ContactDetailsProperty' Vue.use(VTooltip) diff --git a/src/components/ContactDetails/ContactDetailsProperty.vue b/src/components/ContactDetails/ContactDetailsProperty.vue index 5b7b2e27..7aa6b0c9 100644 --- a/src/components/ContactDetails/ContactDetailsProperty.vue +++ b/src/components/ContactDetails/ContactDetailsProperty.vue @@ -22,14 +22,16 @@ <template> <!-- If not in the rfcProps then we don't want to display it --> - <component v-if="propModel && propType !== 'unknown'" :is="componentInstance" :select-type="selectType" + <component v-if="propModel && propType !== 'unknown'" :is="componentInstance" :select-type.sync="selectType" :prop-model="propModel" :value.sync="value" :is-first-property="isFirstProperty" - :class="{'property--last': isLastProperty}" @delete="deleteProp" /> + :class="{'property--last': isLastProperty}" :contact="contact" @delete="deleteProp" /> </template> <script> import { Property } from 'ical.js' import rfcProps from '../../models/rfcProps.js' +import Contact from '../../models/contact' + import PropertyText from '../properties/PropertyText' import PropertyMultipleText from '../properties/PropertyMultipleText' import PropertyDateTime from '../properties/PropertyDateTime' @@ -53,6 +55,10 @@ export default { index: { type: Number, default: 0 + }, + contact: { + type: Contact, + default: null } }, @@ -122,16 +128,32 @@ export default { // we only use uppercase strings .map(str => str.toUpperCase()) - // Compare array and check if the number of exact matches - // equals the array length to find the exact property - return this.propModel.options.find(option => selectedType.length === option.id.split(',').reduce((matches, type) => { - matches += selectedType.indexOf(type) > -1 ? 1 : 0 - return matches - }, 0)) - } else if (this.type) { + // Compare array and score them by how many matches they have to the selected type + // sorting directly is cleaner but slower + // https://jsperf.com/array-map-and-intersection-perf + let matchingTypes = this.propModel.options.map(type => { + return { + type, + // "WORK,HOME" => ['WORK', 'HOME'] + score: type.id.split(',').filter(value => selectedType.indexOf(value) !== -1).length + } + }) + + // Sort by score, filtering out the null score and selecting the first match + let matchingType = matchingTypes + .sort((a, b) => b.score - a.score) + .filter(type => type.score > 0)[0] + + if (matchingType) { + return matchingType.type + } + } + if (this.type) { + // vcard 3.0 save pref alongside TYPE + let selectedType = this.type.filter(type => type !== 'pref').join(',') return { - id: this.type.join(','), - name: this.type.join(',') + id: selectedType, + name: selectedType } } return false @@ -139,6 +161,7 @@ export default { set(data) { // ical.js take types as arrays this.type = data.id.split(',') + this.$emit('updatedcontact') } }, diff --git a/src/components/properties/PropertyDateTime.vue b/src/components/properties/PropertyDateTime.vue index b8a1c940..2d6205b4 100644 --- a/src/components/properties/PropertyDateTime.vue +++ b/src/components/properties/PropertyDateTime.vue @@ -27,9 +27,10 @@ <div class="property__row"> <!-- type selector --> - <multiselect v-if="propModel.options" v-model="selectType" + <multiselect v-if="propModel.options" v-model="localType" :options="propModel.options" :searchable="false" :placeholder="t('contacts', 'Select type')" - class="multiselect-vue property__label" track-by="id" label="name" /> + class="multiselect-vue property__label" track-by="id" label="name" + @input="updateType" /> <!-- if we do not support any type on our model but one is set anyway --> <div v-else-if="selectType" class="property__label">{{ selectType.name }}</div> @@ -41,7 +42,7 @@ <button :title="t('contacts', 'Delete')" class="property__delete icon-delete" @click="deleteProperty" /> <input v-model.trim="localValue" class="property__value" type="text" - @input="updateProp"> + @input="updateValue"> </div> </div> </template> @@ -85,7 +86,8 @@ export default { data() { return { - localValue: this.value + localValue: this.value, + localType: this.selectType } }, @@ -110,9 +112,14 @@ export default { /** * Debounce and send update event to parent */ - updateProp: debounce(function(e) { + updateValue: debounce(function(e) { // https://vuejs.org/v2/guide/components-custom-events.html#sync-Modifier this.$emit('update:value', this.localValue) + }, 500), + + updateType: debounce(function(e) { + // https://vuejs.org/v2/guide/components-custom-events.html#sync-Modifier + this.$emit('update:selectType', this.localType) }, 500) } } diff --git a/src/components/properties/PropertyGroups.vue b/src/components/properties/PropertyGroups.vue index 0fb0cd2b..01976b4a 100644 --- a/src/components/properties/PropertyGroups.vue +++ b/src/components/properties/PropertyGroups.vue @@ -25,11 +25,13 @@ <div class="property__row"> <div class="property__label">{{ propModel.readableName }}</div> + <!-- multiselect taggable groups with a limit to 3 groups shown --> <multiselect v-model="localValue" :options="groups" :placeholder="t('contacts', 'Add contact in group')" :limit="3" :multiple="true" :taggable="true" :close-on-select="false" tag-placeholder="create" class="multiselect-vue property__value" - @tag="createGroup" @select="addContactToGroup" @remove="removeContactToGroup"> - <span v-tooltip.auto="formatGroupsTitle" slot="limit" class="multiselect__limit">+{{ localValue.length - 2 }}</span> + @tag="validateGroup" @select="addContactToGroup" @remove="removeContactToGroup"> + <!-- show how many groups are hidden and add tooltip --> + <span v-tooltip.auto="formatGroupsTitle" slot="limit" class="multiselect__limit">+{{ localValue.length - 3 }}</span> <span slot="noResult">{{ t('settings', 'No results') }}</span> </multiselect> </div> @@ -88,29 +90,50 @@ export default { /** * Debounce and send update event to parent */ - updateProp: debounce(function(e) { + updateValue: debounce(function(e) { // https://vuejs.org/v2/guide/components-custom-events.html#sync-Modifier this.$emit('update:value', this.localValue) }, 500), - addContactToGroup(group) { - console.log(group) + /** + * Dispatch contact addition to group + * + * @param {String} groupName the group name + */ + addContactToGroup(groupName) { this.$store.dispatch('addContactToGroup', { contact: this.contact, - group + groupName }) }, - removeContactToGroup(group) { - console.log(group) + /** + * Dispatch contact removal from group + * + * @param {String} groupName the group name + */ + removeContactToGroup(groupName) { this.$store.dispatch('removeContactToGroup', { contact: this.contact, - group + groupName }) }, - createGroup(group) { - console.log(group) + /** + * Validate groupname and dispatch creation + * + * @param {String} groupName the group name + * @returns {Boolean} + */ + validateGroup(groupName) { + // Only allow characters without vcard special chars + let groupRegex = /^[^;,:]+$/gmi + if (groupName.match(groupRegex)) { + this.addContactToGroup(groupName) + this.localValue.push(groupName) + return true + } + return false } } } diff --git a/src/components/properties/PropertyMultipleText.vue b/src/components/properties/PropertyMultipleText.vue index 3252d3da..1ac0e8ad 100644 --- a/src/components/properties/PropertyMultipleText.vue +++ b/src/components/properties/PropertyMultipleText.vue @@ -27,9 +27,10 @@ <div class="property__row"> <!-- type selector --> - <multiselect v-if="propModel.options" v-model="selectType" + <multiselect v-if="propModel.options" v-model="localType" :options="propModel.options" :searchable="false" :placeholder="t('contacts', 'Select type')" - class="multiselect-vue property__label" track-by="id" label="name" /> + class="multiselect-vue property__label" track-by="id" label="name" + @input="updateType" /> <!-- if we do not support any type on our model but one is set anyway --> <div v-else-if="selectType" class="property__label">{{ selectType.name }}</div> @@ -44,7 +45,7 @@ <div v-for="index in propModel.displayOrder" :key="index" class="property__row"> <div class="property__label">{{ propModel.readableValues[index] }}</div> <input v-model.trim="localValue[index]" class="property__value" type="text" - @input="updateProp"> + @input="updateValue"> </div> </div> </template> @@ -87,7 +88,8 @@ export default { data() { return { - localValue: this.value + localValue: this.value, + localType: this.selectType } }, @@ -113,9 +115,14 @@ export default { /** * Debounce and send update event to parent */ - updateProp: debounce(function(e) { + updateValue: debounce(function(e) { // https://vuejs.org/v2/guide/components-custom-events.html#sync-Modifier this.$emit('update:value', this.localValue) + }, 500), + + updateType: debounce(function(e) { + // https://vuejs.org/v2/guide/components-custom-events.html#sync-Modifier + this.$emit('update:selectType', this.localType) }, 500) } } diff --git a/src/components/properties/PropertyText.vue b/src/components/properties/PropertyText.vue index cdd613a9..a15caa4d 100644 --- a/src/components/properties/PropertyText.vue +++ b/src/components/properties/PropertyText.vue @@ -27,9 +27,10 @@ <div class="property__row"> <!-- type selector --> - <multiselect v-if="propModel.options" v-model="selectType" + <multiselect v-if="propModel.options" v-model="localType" :options="propModel.options" :searchable="false" :placeholder="t('contacts', 'Select type')" - class="multiselect-vue property__label" track-by="id" label="name" /> + class="multiselect-vue property__label" track-by="id" label="name" + @input="updateType" /> <!-- if we do not support any type on our model but one is set anyway --> <div v-else-if="selectType" class="property__label">{{ selectType.name }}</div> @@ -40,8 +41,8 @@ <!-- delete the prop --> <button :title="t('contacts', 'Delete')" class="property__delete icon-delete" @click="deleteProperty" /> - <input v-model.trim="localValue"class="property__value" type="text" - @input="updateProp"> + <input v-model.trim="localValue" class="property__value" type="text" + @input="updateValue"> </div> </div> </template> @@ -84,7 +85,8 @@ export default { data() { return { - localValue: this.value + localValue: this.value, + localType: this.selectType } }, @@ -108,9 +110,14 @@ export default { /** * Debounce and send update event to parent */ - updateProp: debounce(function(e) { + updateValue: debounce(function(e) { // https://vuejs.org/v2/guide/components-custom-events.html#sync-Modifier this.$emit('update:value', this.localValue) + }, 500), + + updateType: debounce(function(e) { + // https://vuejs.org/v2/guide/components-custom-events.html#sync-Modifier + this.$emit('update:selectType', this.localType) }, 500) } } diff --git a/src/store/addressbooks.js b/src/store/addressbooks.js index 301a3446..2dfe1aa8 100644 --- a/src/store/addressbooks.js +++ b/src/store/addressbooks.js @@ -221,7 +221,7 @@ const actions = { context.commit('appendContactsToAddressbook', { addressbook, contacts }) context.commit('appendContacts', contacts) context.commit('sortContacts') - context.commit('appendGroups', contacts) + context.commit('appendGroupsFromContacts', contacts) }, /** diff --git a/src/store/groups.js b/src/store/groups.js index a557ba76..ca261603 100644 --- a/src/store/groups.js +++ b/src/store/groups.js @@ -31,15 +31,17 @@ const mutations = { * @param {Object} state * @param {Contact[]} contacts */ - appendGroups(state, contacts) { + appendGroupsFromContacts(state, contacts) { // init groups list let groups = Object.values(contacts) .map(contact => contact.groups.map(group => { + // extend group to model return { name: group, contacts: [] } })[0]) + // ensure we only have one group of each state.groups = state.groups.concat(groups) .filter(function(group, index, self) { return group && self.findIndex(search => search && search.name === group.name) === index @@ -48,21 +50,49 @@ const mutations = { Object.values(contacts) .forEach(contact => { if (contact.groups) { - contact.groups.forEach(group => { - state.groups.find(search => search.name === group).contacts.push(contact.key) + contact.groups.forEach(groupName => { + let group = state.groups.find(search => search.name === groupName) + group.contacts.push(contact.key) }) } }) }, /** - * Add group if no exist and append contact to it + * Add contact to group and create groupif not existing * * @param {Object} state - * @param {Contact} contact + * @param {Object} data + * @param {String} data.groupName the name of the group + * @param {Contact} data.contact the contact */ - addContactToGroup(state, { group, contact }) { - state.groups.find(search => search.name === group).contacts.push(contact.key) + addContactToGroup(state, { groupName, contact }) { + let group = state.groups.find(search => search.name === groupName) + // nothing? create a new one + if (!group) { + state.groups.push({ + name: groupName, + contacts: [] + }) + group = state.groups.find(search => search.name === groupName) + } + group.contacts.push(contact.key) + }, + + /** + * Remove contact from group + * + * @param {Object} state + * @param {Object} data + * @param {String} data.groupName the name of the group + * @param {Contact} data.contact the contact + */ + removeContactToGroup(state, { groupName, contact }) { + let contacts = state.groups.find(search => search.name === groupName).contacts + let index = contacts.findIndex(search => search === contact.key) + if (index > -1) { + contacts.splice(index, 1) + } } } @@ -73,15 +103,27 @@ const getters = { const actions = { /** - * Add contact and update groups + * Add contact and to a group + * + * @param {Object} context + * @param {Object} data + * @param {String} data.groupName the name of the group + * @param {Contact} data.contact the contact + */ + addContactToGroup(context, { groupName, contact }) { + context.commit('addContactToGroup', { groupName, contact }) + }, + + /** + * Remove contact from group * * @param {Object} context - * @param {Object} addressbook + * @param {Object} data + * @param {String} data.groupName the name of the group + * @param {Contact} data.contact the contact */ - addContact(context, contact) { - contact.groups.forEach(group => { - context.commit('addContactToGroup', { group, contact }) - }) + removeContactToGroup(context, { groupName, contact }) { + context.commit('removeContactToGroup', { groupName, contact }) } } |