diff --git a/.travis.yml b/.travis.yml index c5a9c417f..471d699f7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -38,7 +38,7 @@ matrix: - php: 7.2 env: "DB=mysql CORE_BRANCH=master" - php: 7.3 - env: "DB=mysql CORE_BRANCH=master TEST_JS=TRUE PHP_COVERAGE=TRUE" + env: "DB=mysql CORE_BRANCH=master PHP_COVERAGE=TRUE" - php: 7.2 env: "DB=pgsql CORE_BRANCH=master" - php: 7.3 @@ -96,14 +96,6 @@ script: # Run server's app code checker - php ../../occ app:check-code contacts - # Run JS tests - - if [[ "$TEST_JS" = "TRUE" ]]; - then make test; - fi - - # Test JS compilation - - make build-js-production - # Test php - make test-php - if [[ "$PHP_COVERAGE" = "TRUE" ]]; diff --git a/css/icons.scss b/css/icons.scss index 36202b26f..0d7c99d04 100644 --- a/css/icons.scss +++ b/css/icons.scss @@ -29,6 +29,7 @@ @include icon-black-white('up', 'contacts', 1); @include icon-black-white('no-calendar', 'contacts', 1); @include icon-black-white('language', 'contacts', 2); +@include icon-black-white('clone', 'contacts', 2); .icon-up-force-white { // using #fffffe to trick the accessibility dark theme icon invert @@ -54,3 +55,4 @@ // using #fffffe to trick the accessibility dark theme icon invert @include icon-color('picture', 'places', '#fffffe', 1, true); } + diff --git a/img/clone.svg b/img/clone.svg new file mode 100644 index 000000000..469fd1beb --- /dev/null +++ b/img/clone.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 95fa875ee..31ca67d80 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3482,7 +3482,7 @@ }, "cdav-library": { "version": "github:nextcloud/cdav-library#288562d3569fa0f5c6224cd0b02f450d5185b3dd", - "from": "github:nextcloud/cdav-library#288562d3569fa0f5c6224cd0b02f450d5185b3dd", + "from": "github:nextcloud/cdav-library", "requires": { "@babel/polyfill": "^7.8.3" } @@ -4619,9 +4619,9 @@ } }, "es-abstract": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.0.tgz", - "integrity": "sha512-yYkE07YF+6SIBmg1MsJ9dlub5L48Ek7X0qz+c/CPCHS9EBXfESorzng4cJQjJW5/pB6vDF41u7F8vUhLVDqIug==", + "version": "1.17.4", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.4.tgz", + "integrity": "sha512-Ae3um/gb8F0mui/jPL+QiqmglkUsaQf7FwBEHYIFkztkneosu9imhqHpBzQ3h1vit8t5iQ74t6PEVvphBZiuiQ==", "dev": true, "requires": { "es-to-primitive": "^1.2.1", @@ -4936,15 +4936,6 @@ "requires": { "ms": "2.0.0" } - }, - "resolve": { - "version": "1.14.2", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.14.2.tgz", - "integrity": "sha512-EjlOBLBO1kxsUxsKjLt7TAECyKW6fOh1VRkykQkKGzcBbjjPIxBqGh0jf7GJ3k/f5mxMqW3htMD3WdTUVtW8HQ==", - "dev": true, - "requires": { - "path-parse": "^1.0.6" - } } } }, @@ -5086,9 +5077,9 @@ } }, "eslint-module-utils": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.5.1.tgz", - "integrity": "sha512-GcNwsYv8MfoEBSbAmV+PSVn2RlhpCShbLImtNviAYa/LE0PgNqxH5tLi1Ld9yeFwdjHsarXK+7G9vsyddmB6dw==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.5.2.tgz", + "integrity": "sha512-LGScZ/JSlqGKiT8OC+cYRxseMjyqt6QO54nl281CK93unD89ijSeRV6An8Ci/2nvWVKe8K/Tqdm75RQoIOCr+Q==", "dev": true, "requires": { "debug": "^2.6.9", @@ -5134,9 +5125,9 @@ } }, "eslint-plugin-import": { - "version": "2.20.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.20.0.tgz", - "integrity": "sha512-NK42oA0mUc8Ngn4kONOPsPB1XhbUvNHqF+g307dPV28aknPoiNnKLFd9em4nkswwepdF5ouieqv5Th/63U7YJQ==", + "version": "2.20.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.20.1.tgz", + "integrity": "sha512-qQHgFOTjguR+LnYRoToeZWT62XM55MBVXObHM6SKFd1VzDcX/vqT1kAz8ssqigh5eMj8qXcRoXXGZpPP6RfdCw==", "dev": true, "requires": { "array-includes": "^3.0.3", @@ -5171,15 +5162,6 @@ "esutils": "^2.0.2", "isarray": "^1.0.0" } - }, - "resolve": { - "version": "1.14.2", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.14.2.tgz", - "integrity": "sha512-EjlOBLBO1kxsUxsKjLt7TAECyKW6fOh1VRkykQkKGzcBbjjPIxBqGh0jf7GJ3k/f5mxMqW3htMD3WdTUVtW8HQ==", - "dev": true, - "requires": { - "path-parse": "^1.0.6" - } } } }, diff --git a/src/components/ContactDetails.vue b/src/components/ContactDetails.vue index 5ce82a865..e75b97331 100644 --- a/src/components/ContactDetails.vue +++ b/src/components/ContactDetails.vue @@ -94,7 +94,7 @@ :class="{'icon-loading-small': loadingUpdate, [`${warning.icon}`]: warning}" class="header-icon" - href="#" /> + @click="onWarningClick" />
- + {{ t('contacts', 'Download') }} + + + {{ t('contacts', 'Clone contact') }} + {{ t('contacts', 'Generate QR Code') }} @@ -377,6 +387,7 @@ export default { /** * Store getters filtered and mapped to usable object + * This is the list of addressbooks that are available to write * * @returns {Array} */ @@ -550,6 +561,38 @@ export default { } }, + /** + * Copy contact to the specified addressbook + * + * @param {string} addressbookId the desired addressbook ID + */ + async copyContactToAddressbook(addressbookId) { + const addressbook = this.addressbooks.find(search => search.id === addressbookId) + this.loadingUpdate = true + if (addressbook) { + try { + const contact = await this.$store.dispatch('copyContactToAddressbook', { + // we need to use the store contact, not the local contact + // using this.contact and not this.localContact + contact: this.contact, + addressbook, + }) + // select the contact again + this.$router.push({ + name: 'contact', + params: { + selectedGroup: this.$route.params.selectedGroup, + selectedContact: contact.key, + }, + }) + } catch (error) { + console.error(error) + } finally { + this.loadingUpdate = false + } + } + }, + /** * Refresh the data of a contact */ @@ -589,6 +632,30 @@ export default { this.debounceUpdateContact() } }, + + /** + * Clone the current contact to another addressbook + */ + cloneContact() { + // only one addressbook, let's clone it there + if (this.addressbooksOptions.length === 1) { + this.copyContactToAddressbook(this.addressbooksOptions[0].id) + } + }, + + /** + * The user clicked the warning icon + */ + onWarningClick() { + // if the user clicked the readonly icon, let's focus the clone button + if (this.isReadOnly && this.addressbooksOptions.length > 0) { + this.openedMenu = true + this.$nextTick(() => { + // focus the clone button + this.$refs.actions.onMouseFocusAction({ target: this.$refs.cloneAction.$el }) + }) + } + }, }, } diff --git a/src/components/Properties/PropertyMultipleText.vue b/src/components/Properties/PropertyMultipleText.vue index cd4107e94..896ff6617 100644 --- a/src/components/Properties/PropertyMultipleText.vue +++ b/src/components/Properties/PropertyMultipleText.vue @@ -63,8 +63,8 @@ @input="updateValue"> - diff --git a/src/store/addressbooks.js b/src/store/addressbooks.js index 5871d7912..310df1e6d 100644 --- a/src/store/addressbooks.js +++ b/src/store/addressbooks.js @@ -407,7 +407,7 @@ const actions = { // Get vcard string try { - const vData = ICAL.stringify(contact.vCard.jCal) + const vData = contact.vCard.toString() // push contact to server and use limit requests.push(limit(() => contact.addressbook.dav.createVCard(vData) .then((response) => { @@ -512,6 +512,34 @@ const actions = { await context.commit('addContactToAddressbook', contact) return contact }, + + /** + * Copy a contact to the provided addressbook + * + * @param {Object} context the store mutations + * @param {Object} data destructuring object + * @param {Contact} data.contact the contact to copy + * @param {Object} data.addressbook the addressbook to move the contact to + * @returns {Contact} the new contact object + */ + async copyContactToAddressbook(context, { contact, addressbook }) { + // init new contact & strip old uid + const vData = contact.vCard.toString().replace(/^UID.+/im, '') + const newContact = new Contact(vData, addressbook) + + try { + const response = await contact.dav.copy(addressbook.dav) + // setting the contact dav property + Vue.set(newContact, 'dav', response) + + } catch (error) { + throw error + } + // success, update store + await context.commit('addContact', newContact) + await context.commit('addContactToAddressbook', newContact) + return newContact + }, } export default { state, mutations, getters, actions }