diff options
author | John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com> | 2019-01-30 11:16:29 +0100 |
---|---|---|
committer | John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com> | 2019-01-30 12:17:23 +0100 |
commit | a1561dfe3a728f3b636852df05d36c1947883dd7 (patch) | |
tree | 91e90dae3ec08878ae81a2f68a90fba4050ebfc2 | |
parent | 7af5d37ac8428784843fd3cca531fb30ecf71fe3 (diff) |
Add property-defined actions
Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
-rw-r--r-- | css/Properties/Properties.scss | 41 | ||||
-rw-r--r-- | css/icons.scss | 4 | ||||
-rw-r--r-- | img/up.svg | 3 | ||||
-rw-r--r-- | package-lock.json | 8 | ||||
-rw-r--r-- | src/components/ContactDetails.vue | 4 | ||||
-rw-r--r-- | src/components/ContactDetails/ContactDetailsProperty.vue | 12 | ||||
-rw-r--r-- | src/components/Properties/PropertyDateTime.vue | 5 | ||||
-rw-r--r-- | src/components/Properties/PropertyMultipleText.vue | 5 | ||||
-rw-r--r-- | src/components/Properties/PropertySelect.vue | 5 | ||||
-rw-r--r-- | src/components/Properties/PropertyText.vue | 11 | ||||
-rw-r--r-- | src/main.js | 3 | ||||
-rw-r--r-- | src/mixins/PropertyMixin.js | 11 | ||||
-rw-r--r-- | src/models/rfcProps.js | 23 | ||||
-rw-r--r-- | src/services/checks/missingFN.js | 4 | ||||
-rw-r--r-- | src/store/contacts.js | 4 |
15 files changed, 95 insertions, 48 deletions
diff --git a/css/Properties/Properties.scss b/css/Properties/Properties.scss index 55ad6c99..a777054f 100644 --- a/css/Properties/Properties.scss +++ b/css/Properties/Properties.scss @@ -26,8 +26,8 @@ $property-value-max-width: 250px; .property { @include generate-grid-span(1); position: relative; - padding-right: 44px; // delete button - // we need this to keep the alignment of the ext and delete button + padding-right: 44px; // actions menu / button + // we need this to keep the alignment of the ext and delete/action button // The flex grow will never go over those values. Therefore we can set // the max width and keep the right alignment max-width: $property-label-max-width + $property-value-max-width + 44px; @@ -37,8 +37,8 @@ $property-value-max-width: 250px; &--last { margin-bottom: $grid-height-unit; } - // no delete icon on addressbook selector - &--addressbooks &__delete { + // no delete/action icon on addressbook selector + &--addressbooks &__actions { display: none !important; } @@ -132,17 +132,15 @@ $property-value-max-width: 250px; &:hover, &:focus, &:active { - ~ .property__ext, - ~ .property__delete { + ~ .property__ext { opacity: .5; } } } } - // show ext & delete button on full row hover - &:hover &__ext, - &:hover &__delete { + // show ext button on full row hover + &:hover &__ext{ opacity: .5; } @@ -156,28 +154,31 @@ $property-value-max-width: 250px; &:focus, &:active { opacity: .7; - // still show the delete button for keyboard accessibility - ~ .property__delete { - opacity: .5; - } } } - // Delete property button - &__delete { - position: absolute; + // Delete property button + actions + &__actions { + position: absolute !important; top: 0; left: 100%; - width: $grid-height-unit; - height: $grid-height-unit; margin: 0; + margin-top: -3px; // align with line because of the 44x44px size border: 0; background-color: transparent; - opacity: 0; + z-index: 10; + // opacity applies on the single action OR + &:not(.action-item--multiple), + &.action-item--multiple .icon-more { + opacity: 0.5; + } &:hover, &:active, &:focus { - opacity: .7; + &:not(.action-item--multiple), + &.action-item--multiple .icon-more { + opacity: 0.7; + } } } } diff --git a/css/icons.scss b/css/icons.scss index 5549a162..41cc93b3 100644 --- a/css/icons.scss +++ b/css/icons.scss @@ -35,3 +35,7 @@ .icon-eye-white { @include icon-color('eye', 'contacts', $color-white, 1); } + +.icon-up { + @include icon-color('up', 'contacts', $color-black, 1); +} diff --git a/img/up.svg b/img/up.svg new file mode 100644 index 00000000..fa3ecbb1 --- /dev/null +++ b/img/up.svg @@ -0,0 +1,3 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"> + <path d="M10 15H6V8H1l7-7 7 7h-5z"/> +</svg> diff --git a/package-lock.json b/package-lock.json index 597691c0..b77bc877 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2042,7 +2042,7 @@ }, "cdav-library": { "version": "github:nextcloud/cdav-library#37cfc379b050deff8cb15407d5652098c350bff0", - "from": "github:nextcloud/cdav-library#37cfc379b050deff8cb15407d5652098c350bff0", + "from": "github:nextcloud/cdav-library", "requires": { "@babel/polyfill": "^7.2.5" } @@ -10242,9 +10242,9 @@ "dev": true }, "vue2-datepicker": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/vue2-datepicker/-/vue2-datepicker-2.8.0.tgz", - "integrity": "sha512-IQw/ai04WYGZC4P7toLhryBHhrqc1hCJ3ivgEayXfeDD1EjJnIhUMb4nqo2JvRG72nPqc9HJvHM/66K+AZnjUA==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/vue2-datepicker/-/vue2-datepicker-2.9.0.tgz", + "integrity": "sha512-THS5uCcWAPSU8OAqeie+Jie+7aw23WVbptfR5x1A8YFn4IizgWvttXoLpgwKmwwuJ59O36eoY0aeP/ugsK1RCQ==", "requires": { "fecha": "^2.3.3" } diff --git a/src/components/ContactDetails.vue b/src/components/ContactDetails.vue index ab55c221..1bc9b3cf 100644 --- a/src/components/ContactDetails.vue +++ b/src/components/ContactDetails.vue @@ -95,9 +95,11 @@ <section v-else class="contact-details"> <!-- properties iteration --> <!-- using contact.key in the key and index as key to avoid conflicts between similar data and exact key --> + <!-- passing the debounceUpdateContact so that the contact-property component contains the function + and allow us to use it on the rfcProps since the scope is forwarded to the actions --> <contact-property v-for="(property, index) in sortedProperties" :key="`${index}-${contact.key}-${property.name}`" :index="index" :sorted-properties="sortedProperties" :property="property" :contact="contact" - @updatedcontact="debounceUpdateContact" /> + :update-contact="debounceUpdateContact" @updatedcontact="debounceUpdateContact" /> <!-- addressbook change select - no last property because class is not applied here, empty property because this is a required prop on regular property-select. But since diff --git a/src/components/ContactDetails/ContactDetailsProperty.vue b/src/components/ContactDetails/ContactDetailsProperty.vue index f6bab167..35db5032 100644 --- a/src/components/ContactDetails/ContactDetailsProperty.vue +++ b/src/components/ContactDetails/ContactDetailsProperty.vue @@ -61,6 +61,14 @@ export default { contact: { type: Contact, default: null + }, + /** + * This is needed so that we can update + * the contact within the rfcProps actions + */ + updateContact: { + type: Function, + default: () => {} } }, @@ -139,7 +147,9 @@ export default { * @returns {Object} */ propModel() { - return this.properties[this.propName] + // passing this to properties to allow us to scope the properties object + // this make possible defining actions there + return this.properties(this)[this.propName] }, /** diff --git a/src/components/Properties/PropertyDateTime.vue b/src/components/Properties/PropertyDateTime.vue index 96c72f58..ff2df7e8 100644 --- a/src/components/Properties/PropertyDateTime.vue +++ b/src/components/Properties/PropertyDateTime.vue @@ -43,9 +43,8 @@ {{ propModel.readableName }} </div> - <!-- delete the prop --> - <button v-if="!isReadOnly" :title="t('contacts', 'Delete')" class="property__delete icon-delete" - @click="deleteProperty" /> + <!-- props actions --> + <action :actions="actions" class="property__actions" /> <!-- Real input where the picker shows --> <datetime-picker :value="localValue.toJSDate()" :minute-step="10" :lang="lang" diff --git a/src/components/Properties/PropertyMultipleText.vue b/src/components/Properties/PropertyMultipleText.vue index b2220e03..1408e3ff 100644 --- a/src/components/Properties/PropertyMultipleText.vue +++ b/src/components/Properties/PropertyMultipleText.vue @@ -47,9 +47,8 @@ <input v-if="!property.isStructuredValue" v-model.trim="localValue[0]" :readonly="isReadOnly" class="property__value" type="text" @input="updateValue"> - <!-- delete the prop --> - <button v-if="!isReadOnly" :title="t('contacts', 'Delete')" class="property__delete icon-delete" - @click="deleteProperty" /> + <!-- props actions --> + <action :actions="actions" class="property__actions" /> </div> <!-- force order based on model --> diff --git a/src/components/Properties/PropertySelect.vue b/src/components/Properties/PropertySelect.vue index 51ccd032..69593f8c 100644 --- a/src/components/Properties/PropertySelect.vue +++ b/src/components/Properties/PropertySelect.vue @@ -37,9 +37,8 @@ {{ propModel.readableName }} </div> - <!-- delete the prop --> - <button v-if="!isReadOnly" :title="t('contacts', 'Delete')" class="property__delete icon-delete" - @click="deleteProperty" /> + <!-- props actions --> + <action :actions="actions" class="property__actions" /> <multiselect v-model="matchedOptions" :options="propModel.options" :placeholder="t('contacts', 'Select option')" :disabled="isSingleOption || isReadOnly" class="property__value" track-by="id" diff --git a/src/components/Properties/PropertyText.vue b/src/components/Properties/PropertyText.vue index b61df21d..447775ab 100644 --- a/src/components/Properties/PropertyText.vue +++ b/src/components/Properties/PropertyText.vue @@ -58,9 +58,8 @@ <a v-if="haveExtHandler" :href="externalHandler" class="property__ext icon-external" target="_blank" /> - <!-- delete the prop --> - <button v-if="!isReadOnly" :title="t('contacts', 'Delete')" class="property__delete icon-delete" - @click="deleteProperty" /> + <!-- props actions --> + <action :actions="actions" class="property__actions" /> </div> </div> </template> @@ -157,12 +156,6 @@ export default { }, methods: { - /** - * Delete the property - */ - deleteProperty() { - this.$emit('delete') - }, /** * Watch textarea resize and update the gridSize accordingly diff --git a/src/main.js b/src/main.js index 1cbb209c..e0264e5f 100644 --- a/src/main.js +++ b/src/main.js @@ -27,7 +27,7 @@ import { sync } from 'vuex-router-sync' import { generateFilePath } from 'nextcloud-server/dist/router' /** GLOBAL COMPONENTS AND DIRECTIVE */ -import { AppNavigation, DatetimePicker, Multiselect, PopoverMenu } from 'nextcloud-vue' +import { Action, AppNavigation, DatetimePicker, Multiselect, PopoverMenu } from 'nextcloud-vue' import ClickOutside from 'vue-click-outside' import { VTooltip } from 'v-tooltip' import VueClipboard from 'vue-clipboard2' @@ -43,6 +43,7 @@ __webpack_nonce__ = btoa(OC.requestToken) // eslint-disable-next-line __webpack_public_path__ = generateFilePath('contacts', '', 'js/') +Vue.component('Action', Action) Vue.component('AppNavigation', AppNavigation) Vue.component('DatetimePicker', DatetimePicker) Vue.component('Multiselect', Multiselect) diff --git a/src/mixins/PropertyMixin.js b/src/mixins/PropertyMixin.js index 73b3f21c..cdb166f2 100644 --- a/src/mixins/PropertyMixin.js +++ b/src/mixins/PropertyMixin.js @@ -76,6 +76,17 @@ export default { } }, + computed: { + actions() { + const del = { + text: t('contacts', 'Delete'), + icon: 'icon-delete', + action: this.deleteProperty + } + return [del, ...this.propModel.actions ? this.propModel.actions : []] + } + }, + watch: { /** * Since we're updating a local data based on the value prop, diff --git a/src/models/rfcProps.js b/src/models/rfcProps.js index 122736f2..79c891e6 100644 --- a/src/models/rfcProps.js +++ b/src/models/rfcProps.js @@ -21,7 +21,17 @@ */ import { VCardTime } from 'ical.js' -const properties = { +const copyNtoFN = ({ contact, updateContact }) => () => { + if (contact.vCard.hasProperty('n')) { + // Stevenson;John;Philip,Paul;Dr.;Jr.,M.D.,A.C.P. + // -> John Stevenson + const n = contact.vCard.getFirstPropertyValue('n') + contact.fullName = n.slice(0, 2).reverse().join(' ') + updateContact() + } +} + +const properties = component => ({ nickname: { readableName: t('contacts', 'Nickname'), icon: 'icon-user' @@ -39,7 +49,14 @@ const properties = { defaultValue: { value: ['', '', '', '', ''] }, - icon: 'icon-user' + icon: 'icon-user', + actions: [ + { + text: t('contacts', 'Copy to full name'), + icon: 'icon-up', + action: copyNtoFN(component) + } + ] }, note: { readableName: t('contacts', 'Notes'), @@ -266,7 +283,7 @@ const properties = { { id: 'O', name: t('contacts', 'Other') } ] } -} +}) const fieldOrder = [ 'org', diff --git a/src/services/checks/missingFN.js b/src/services/checks/missingFN.js index 1f17afc8..449f4a43 100644 --- a/src/services/checks/missingFN.js +++ b/src/services/checks/missingFN.js @@ -29,6 +29,10 @@ export default { run: contact => { return !contact.vCard.hasProperty('fn') // No FN || contact.vCard.getFirstPropertyValue('fn') === '' // Empty FN + || ( // we don't want to fix newly created contacts + contact.dav // Existing contact + && contact.vCard.getFirstPropertyValue('fn') === t('contacts', 'New contact') // AND Unchanged FN + ) }, fix: contact => { if (contact.vCard.hasProperty('n')) { diff --git a/src/store/contacts.js b/src/store/contacts.js index 5258d861..d5538b6e 100644 --- a/src/store/contacts.js +++ b/src/store/contacts.js @@ -285,6 +285,10 @@ const actions = { * @returns {Promise} */ async updateContact(context, contact) { + + // Checking contact validity 🙈 + validate(contact) + let vData = ICAL.stringify(contact.vCard.jCal) // if no dav key, contact does not exists on server |