Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ABLABEL and ITEMX.property support #991

Merged
merged 7 commits into from
Mar 29, 2019
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 23 additions & 5 deletions css/Properties/Properties.scss
Original file line number Diff line number Diff line change
@@ -60,13 +60,19 @@ $property-value-max-width: 250px;
min-width: $property-label-min-width !important; // override multiselect
max-width: $property-label-max-width;

opacity: .7;
user-select: none;
background-size: 16px;

&,
.multiselect__input::placeholder {
text-align: right;
.multiselect__input {
&::placeholder {
text-align: right;
}
+ .multiselect__single {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
}

&:not(.multiselect) {
@@ -77,12 +83,15 @@ $property-value-max-width: 250px;
}

// mouse feedback
.multiselect__tags {
opacity: .7;
}
&:hover,
&:focus,
&:active {
opacity: 1;
.multiselect__tags {
border-color: var(--color-border-dark);
opacity: 1;
}
}

@@ -105,8 +114,17 @@ $property-value-max-width: 250px;
background-position: center right 4px;
padding-right: 24px;
}
}
.multiselect__content-wrapper {
min-width: $property-label-max-width; // improve readability on narrow screens
width: auto !important; // grow bigger if content is bigger than the original 100%
right: 0; // align right
}
@media only screen and (max-width: 768px) {
// align left of screen on narrow views
.multiselect__content-wrapper {
min-width: $property-label-max-width; // improve readability on narrow screens
left: 0;
right: auto;
}
}
}
12 changes: 7 additions & 5 deletions src/components/ContactDetails.vue
Original file line number Diff line number Diff line change
@@ -279,11 +279,13 @@ export default {
* @returns {Array}
*/
sortedProperties() {
return this.localContact.properties.slice(0).sort((a, b) => {
return (
rfcProps.fieldOrder.indexOf(a.name) - rfcProps.fieldOrder.indexOf(b.name)
)
})
return this.localContact.properties
.slice(0)
.sort((a, b) => {
const nameA = a.name.split('.').pop()
const nameB = b.name.split('.').pop()
return rfcProps.fieldOrder.indexOf(nameA) - rfcProps.fieldOrder.indexOf(nameB)
})
},

/**
69 changes: 64 additions & 5 deletions src/components/ContactDetails/ContactDetailsProperty.vue
Original file line number Diff line number Diff line change
@@ -27,7 +27,7 @@
:property="property" :is-last-property="isLastProperty" :class="{'property--last': isLastProperty}"
:contact="contact" :prop-name="propName" :prop-type="propType"
:options="sortedModelOptions" :is-read-only="isReadOnly"
@delete="deleteProp" />
@delete="deleteProp" @update="updateProp" />
</template>

<script>
@@ -101,15 +101,15 @@ export default {
// is this the first property of its kind
isFirstProperty() {
if (this.index > 0) {
return this.sortedProperties[this.index - 1].name !== this.propName
return this.sortedProperties[this.index - 1].name.split('.').pop() !== this.propName
}
return true
},
// is this the last property of its kind
isLastProperty() {
// array starts at 0, length starts at 1
if (this.index < this.sortedProperties.length - 1) {
return this.sortedProperties[this.index + 1].name !== this.propName
return this.sortedProperties[this.index + 1].name.split('.').pop() !== this.propName
}
return true
},
@@ -126,6 +126,11 @@ export default {
* @returns {string}
*/
propName() {
// ! is this a ITEMXX.XXX property??
if (this.propGroup[1]) {
return this.propGroup[1]
}

return this.property.name
},
/**
@@ -139,6 +144,7 @@ export default {
if (this.propModel && this.propModel.force) {
return this.propModel.force
}

return this.property.getDefaultType()
},

@@ -171,6 +177,25 @@ export default {
return []
},

/**
* Return the id and type of a property group
* e.g ITEMXX.tel => ['ITEMXX', 'tel']
*
* @returns {Array}
*/
propGroup() {
return this.property.name.split('.')
},

/**
* Return the associated X-ABLABEL if any
*
* @returns {Property}
*/
propLabel() {
return this.contact.vCard.getFirstProperty(`${this.propGroup[0]}.x-ablabel`)
},

/**
* Returns the closest match to the selected type
* or return the default selected as a new object if
@@ -180,6 +205,13 @@ export default {
*/
selectType: {
get() {
// ! if ABLABEL is present, this is a priority
if (this.propLabel) {
return {
id: this.propLabel.name,
name: this.propLabel.getFirstValue()
}
}
if (this.propModel && this.propModel.options && this.type) {

let selectedType = this.type
@@ -219,8 +251,27 @@ export default {
return null
},
set(data) {
// ical.js take types as arrays
this.type = data.id.split(',')
// if a custom label exists and this is the one we selected
if (this.propLabel && data.id === this.propLabel.name) {
this.propLabel.setValue(data.name)
// only one can coexist
this.type = []
} else {
// ical.js take types as arrays
this.type = data.id.split(',')
// only one can coexist
this.contact.vCard.removeProperty(`${this.propGroup[0]}.x-ablabel`)

// checking if there is any other property in this group
const groups = this.contact.jCal[1]
.map(prop => prop[0])
.filter(name => name.startsWith(`${this.propGroup[0]}.`))
if (groups.length === 1) {
// then this prop is the latest of its group
// -> converting back to simple prop
this.property.jCal[0] = this.propGroup[1]
}
}
this.$emit('updatedcontact')
}

@@ -281,8 +332,16 @@ export default {
* Delete this property
*/
deleteProp() {
console.info('removing', this.property, this.propGroup)
this.contact.vCard.removeProperty(this.property)
this.$emit('updatedcontact')
},

/**
* Update this property
*/
updateProp() {
this.$emit('updatedcontact')
}
}
}
7 changes: 4 additions & 3 deletions src/components/Properties/PropertyMultipleText.vue
Original file line number Diff line number Diff line change
@@ -29,9 +29,10 @@
<div class="property__row">
<!-- type selector -->
<multiselect v-if="propModel.options" v-model="localType"
:options="options" :searchable="false" :placeholder="t('contacts', 'Select type')"
:disabled="isReadOnly" class="property__label" track-by="id"
label="name" @input="updateType" />
:options="options" :placeholder="t('contacts', 'Select type')"
:taggable="true" tag-placeholder="create" :disabled="isReadOnly"
class="property__label" track-by="id" label="name"
@tag="createLabel" @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">
7 changes: 4 additions & 3 deletions src/components/Properties/PropertyText.vue
Original file line number Diff line number Diff line change
@@ -29,9 +29,10 @@
<div class="property__row">
<!-- type selector -->
<multiselect v-if="propModel.options" v-model="localType"
:options="options" :searchable="false" :placeholder="t('contacts', 'Select type')"
:disabled="isReadOnly" class="property__label" track-by="id"
label="name" @input="updateType" />
:options="options" :placeholder="t('contacts', 'Select type')"
:taggable="true" tag-placeholder="create" :disabled="isReadOnly"
class="property__label" track-by="id" label="name"
@tag="createLabel" @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">
37 changes: 36 additions & 1 deletion src/mixins/PropertyMixin.js
Original file line number Diff line number Diff line change
@@ -21,6 +21,7 @@
*/
import debounce from 'debounce'
import Contact from 'Models/contact'
import ICAL from 'ical.js'

export default {
props: {
@@ -129,6 +130,40 @@ export default {
updateType: debounce(function(e) {
// https://vuejs.org/v2/guide/components-custom-events.html#sync-Modifier
this.$emit('update:selectType', this.localType)
}, 500)
}, 500),

createLabel(label) {
let propGroup = this.property.name
if (!this.property.name.startsWith('nextcloud')) {
propGroup = `nextcloud${this.getNcGroupCount() + 1}.${this.property.name}`
this.property.jCal[0] = propGroup
}
const group = propGroup.split('.')[0]
const name = propGroup.split('.')[1]

this.contact.vCard.addPropertyWithValue(`${group}.x-ablabel`, label)

// force update the main design sets
if (ICAL.design.vcard.property[name]) {
ICAL.design.vcard.property[propGroup]
= ICAL.design.vcard.property[name]
}
if (ICAL.design.vcard3.property[name]) {
ICAL.design.vcard3.property[propGroup]
= ICAL.design.vcard3.property[name]
}

this.$emit('update')
},

getNcGroupCount() {
const props = this.contact.jCal[1]
.map(prop => prop[0].split('.')[0]) // itemxxx.adr => itemxxx
.filter(name => name.startsWith('nextcloud')) // filter nextcloudxxx.adr
.map(prop => parseInt(prop.split('nextcloud')[1])) // nextcloudxxx => xxx
return props.length > 0
? Math.max.apply(null, props) // get max iteration of nextcloud grouped props
: 0
}
}
}
40 changes: 40 additions & 0 deletions src/models/contact.js
Original file line number Diff line number Diff line change
@@ -35,6 +35,40 @@ const isEmpty = value => {
return (Array.isArray(value) && value.join('') === '') || (!Array.isArray(value) && value === '')
}

/**
* Parse a jCal and update the global designset
* if any grouped property is found
*
* @param {Array} jCal the contact ICAL.js jCal
* @returns {Boolean}
*/
const updateDesignSet = jCal => {
let result = false
jCal[1].forEach(prop => {
const propGroup = prop[0].split('.')

// if this is a grouped property, update the designSet
if (propGroup.length === 2 && (
ICAL.design.vcard.property[propGroup[1]]
|| ICAL.design.vcard3.property[propGroup[1]]
)) {
// force update the main design sets
if (ICAL.design.vcard.property[propGroup[1]]) {
ICAL.design.vcard.property[prop[0]]
= ICAL.design.vcard.property[propGroup[1]]
result = true
}
if (ICAL.design.vcard3.property[propGroup[1]]) {
ICAL.design.vcard3.property[prop[0]]
= ICAL.design.vcard3.property[propGroup[1]]

result = true
}
}
})
return result
}

export default class Contact {

/**
@@ -54,6 +88,12 @@ export default class Contact {
throw new Error('Only one contact is allowed in the vcard data')
}

// add grouped properties to the design set
// if any found, refresh the contact jCal
if (updateDesignSet(jCal)) {
jCal = ICAL.parse(vcard)
}

this.jCal = jCal
this.addressbook = addressbook
this.vCard = new ICAL.Component(this.jCal)