Skip to content

Commit

Permalink
Merge pull request #991 from nextcloud/enhancement/ablabel
Browse files Browse the repository at this point in the history
Add ABLABEL and ITEMX.property support
  • Loading branch information
skjnldsv authored Mar 29, 2019
2 parents f91cb46 + 1fb881b commit 0444744
Show file tree
Hide file tree
Showing 7 changed files with 178 additions and 22 deletions.
28 changes: 23 additions & 5 deletions css/Properties/Properties.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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;
}
}

Expand All @@ -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;
}
}
}
Expand Down
12 changes: 7 additions & 5 deletions src/components/ContactDetails.vue
Original file line number Diff line number Diff line change
Expand Up @@ -280,11 +280,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)
})
},

/**
Expand Down
69 changes: 64 additions & 5 deletions src/components/ContactDetails/ContactDetailsProperty.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -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
},
Expand All @@ -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
},
/**
Expand All @@ -139,6 +144,7 @@ export default {
if (this.propModel && this.propModel.force) {
return this.propModel.force
}

return this.property.getDefaultType()
},

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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')
}

Expand Down Expand Up @@ -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')
}
}
}
Expand Down
7 changes: 4 additions & 3 deletions src/components/Properties/PropertyMultipleText.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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">
Expand Down
7 changes: 4 additions & 3 deletions src/components/Properties/PropertyText.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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">
Expand Down
37 changes: 36 additions & 1 deletion src/mixins/PropertyMixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
*/
import debounce from 'debounce'
import Contact from 'Models/contact'
import ICAL from 'ical.js'

export default {
props: {
Expand Down Expand Up @@ -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
Expand Up @@ -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 {

/**
Expand All @@ -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)
Expand Down

0 comments on commit 0444744

Please sign in to comment.