From d8ede82510afe55a06c754710463a45eab6e5d7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?John=20Molakvo=C3=A6=20=28skjnldsv=29?= Date: Mon, 11 May 2020 18:38:15 +0200 Subject: [PATCH 1/9] Add create group entry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: John Molakvoæ (skjnldsv) --- css/ContactsList.scss | 14 ------ src/components/ContactDetails.vue | 26 ++++++----- src/components/ContactsList.vue | 37 +++++++++++++++ src/components/EmptyContent.vue | 77 +++++++++++++++++++++++++++++++ src/store/groups.js | 23 +++++++++ src/views/Contacts.vue | 38 ++++++++++++++- 6 files changed, 188 insertions(+), 27 deletions(-) create mode 100644 src/components/EmptyContent.vue diff --git a/css/ContactsList.scss b/css/ContactsList.scss index 75ef932ae..43e8f17c6 100644 --- a/css/ContactsList.scss +++ b/css/ContactsList.scss @@ -33,17 +33,3 @@ border-radius: 50%; opacity: 1; } - -// Virtual scroller overrides -.vue-recycle-scroller { - position: sticky !important; -} - -.vue-recycle-scroller__item-view { - // TODO: find better solution? - // https://github.com/Akryum/vue-virtual-scroller/issues/70 - // hack to not show the transition - overflow: hidden; - // same as app-content-list-item - height: 68px; -} diff --git a/src/components/ContactDetails.vue b/src/components/ContactDetails.vue index 10c69a04f..da1b7e870 100644 --- a/src/components/ContactDetails.vue +++ b/src/components/ContactDetails.vue @@ -23,17 +23,17 @@ + + \ No newline at end of file diff --git a/src/components/EmptyContent.vue b/src/components/EmptyContent.vue new file mode 100644 index 000000000..5198f8ced --- /dev/null +++ b/src/components/EmptyContent.vue @@ -0,0 +1,77 @@ + + + + + + + diff --git a/src/store/groups.js b/src/store/groups.js index 8df01b19c..981e09328 100644 --- a/src/store/groups.js +++ b/src/store/groups.js @@ -105,6 +105,19 @@ const mutations = { } }) }, + + /** + * Add a group + * + * @param {Object} state the store data + * @param {string} groupName the name of the group + */ + addGroup(state, groupName) { + state.groups.push({ + name: groupName, + contacts: [], + }) + }, } const getters = { @@ -146,6 +159,16 @@ const actions = { removeContactToGroup(context, { groupName, contact }) { context.commit('removeContactToGroup', { groupName, contact }) }, + + /** + * Add a group + * + * @param {Object} context the store mutations + * @param {string} groupName the name of the group + */ + addGroup(context, groupName) { + context.commit('addGroup', groupName) + }, } export default { state, mutations, getters, actions } diff --git a/src/views/Contacts.vue b/src/views/Contacts.vue index 13c5dc167..60784a971 100644 --- a/src/views/Contacts.vue +++ b/src/views/Contacts.vue @@ -52,6 +52,20 @@ {{ item.utils.counter }} + + + + @@ -70,7 +84,9 @@
- @@ -96,6 +112,7 @@ import AppNavigationCounter from '@nextcloud/vue/dist/Components/AppNavigationCo import AppNavigationNew from '@nextcloud/vue/dist/Components/AppNavigationNew' import AppNavigationSettings from '@nextcloud/vue/dist/Components/AppNavigationSettings' import ActionButton from '@nextcloud/vue/dist/Components/ActionButton' +import ActionInput from '@nextcloud/vue/dist/Components/ActionInput' import Content from '@nextcloud/vue/dist/Components/Content' import Modal from '@nextcloud/vue/dist/Components/Modal' import isMobile from '@nextcloud/vue/dist/Mixins/isMobile' @@ -128,6 +145,7 @@ export default { AppNavigationNew, AppNavigationSettings, ActionButton, + ActionInput, ContactDetails, ContactsList, Content, @@ -155,6 +173,8 @@ export default { data() { return { + isNewGroupMenuOpen: false, + isCreatingGroup: false, loading: true, searchQuery: '', } @@ -510,6 +530,22 @@ export default { this.$store.dispatch('changeStage', 'default') } }, + + toggleNewGroupMenu() { + this.isNewGroupMenuOpen = !this.isNewGroupMenuOpen + }, + createNewGroup(e) { + const input = e.target.querySelector('input[type=text]') + const groupName = input.value.trim() + this.$store.dispatch('addGroup', groupName) + this.isNewGroupMenuOpen = false + }, }, } + + From 63b2aff43903d51fc382a7c6cb0019845363c183 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?John=20Molakvo=C3=A6=20=28skjnldsv=29?= Date: Fri, 19 Jun 2020 07:36:52 +0200 Subject: [PATCH 2/9] Temp MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: John Molakvoæ (skjnldsv) --- src/components/ContactDetails.vue | 8 +- src/components/ContactsList.vue | 4 +- src/components/EntityPicker/EntityBubble.vue | 109 ++++++ src/components/EntityPicker/EntityPicker.vue | 334 ++++++++++++++++++ .../EntityPicker/EntitySearchResult.vue | 122 +++++++ src/components/Properties/PropertyGroups.vue | 6 +- src/views/Contacts.vue | 177 ++++++---- webpack.common.js | 3 +- 8 files changed, 689 insertions(+), 74 deletions(-) create mode 100644 src/components/EntityPicker/EntityBubble.vue create mode 100644 src/components/EntityPicker/EntityPicker.vue create mode 100644 src/components/EntityPicker/EntitySearchResult.vue diff --git a/src/components/ContactDetails.vue b/src/components/ContactDetails.vue index da1b7e870..0735a3d0e 100644 --- a/src/components/ContactDetails.vue +++ b/src/components/ContactDetails.vue @@ -105,7 +105,7 @@ show: true, trigger: 'manual', }" - class="header-icon header-icon--pulse icon-history-force-white" + class="header-icon header-icon--pulse icon-history" @click="refreshContact" /> @@ -115,7 +115,7 @@ show: true, trigger: 'manual', }" - class="header-icon header-icon--pulse icon-up-force-white" + class="header-icon header-icon--pulse icon-up" @click="updateContact" /> @@ -316,12 +316,12 @@ export default { warning() { if (!this.contact.dav) { return { - icon: 'icon-error-white header-icon--pulse', + icon: 'icon-error header-icon--pulse', msg: t('contacts', 'This contact is not yet synced. Edit it to save it to the server.'), } } else if (this.isReadOnly) { return { - icon: 'icon-eye-white', + icon: 'icon-eye', msg: t('contacts', 'This contact is in read-only mode. You do not have permission to edit this contact.'), } } diff --git a/src/components/ContactsList.vue b/src/components/ContactsList.vue index 222b4f1ed..1a5f07455 100644 --- a/src/components/ContactsList.vue +++ b/src/components/ContactsList.vue @@ -180,7 +180,7 @@ export default { onAddContactsToGroup() { // TODO: add popup - } + }, }, } @@ -199,4 +199,4 @@ export default { // same as app-content-list-item height: 68px; } - \ No newline at end of file + diff --git a/src/components/EntityPicker/EntityBubble.vue b/src/components/EntityPicker/EntityBubble.vue new file mode 100644 index 000000000..0394d9c29 --- /dev/null +++ b/src/components/EntityPicker/EntityBubble.vue @@ -0,0 +1,109 @@ + + + + + + diff --git a/src/components/EntityPicker/EntityPicker.vue b/src/components/EntityPicker/EntityPicker.vue new file mode 100644 index 000000000..d0421af03 --- /dev/null +++ b/src/components/EntityPicker/EntityPicker.vue @@ -0,0 +1,334 @@ + + + + + + + + + diff --git a/src/components/EntityPicker/EntitySearchResult.vue b/src/components/EntityPicker/EntitySearchResult.vue new file mode 100644 index 000000000..e0a25109c --- /dev/null +++ b/src/components/EntityPicker/EntitySearchResult.vue @@ -0,0 +1,122 @@ + + + + + + + diff --git a/src/components/Properties/PropertyGroups.vue b/src/components/Properties/PropertyGroups.vue index 030269928..63990e082 100644 --- a/src/components/Properties/PropertyGroups.vue +++ b/src/components/Properties/PropertyGroups.vue @@ -22,7 +22,9 @@ @@ -108,6 +153,7 @@ import AppContent from '@nextcloud/vue/dist/Components/AppContent' import AppNavigation from '@nextcloud/vue/dist/Components/AppNavigation' import AppNavigationItem from '@nextcloud/vue/dist/Components/AppNavigationItem' +import AppNavigationSpacer from '@nextcloud/vue/dist/Components/AppNavigationSpacer' import AppNavigationCounter from '@nextcloud/vue/dist/Components/AppNavigationCounter' import AppNavigationNew from '@nextcloud/vue/dist/Components/AppNavigationNew' import AppNavigationSettings from '@nextcloud/vue/dist/Components/AppNavigationSettings' @@ -125,6 +171,7 @@ import SettingsSection from '../components/SettingsSection' import ContactsList from '../components/ContactsList' import ContactDetails from '../components/ContactDetails' import ImportScreen from '../components/ImportScreen' +import EntityPicker from '../components/EntityPicker/EntityPicker' import Contact from '../models/contact' import rfcProps from '../models/rfcProps' @@ -144,12 +191,14 @@ export default { AppNavigationCounter, AppNavigationNew, AppNavigationSettings, + AppNavigationSpacer, ActionButton, ActionInput, ContactDetails, ContactsList, Content, ImportScreen, + EntityPicker, Modal, SettingsSection, }, @@ -173,10 +222,18 @@ export default { data() { return { - isNewGroupMenuOpen: false, + GROUP_ALL_CONTACTS, + GROUP_NO_GROUP_CONTACTS, isCreatingGroup: false, + isNewGroupMenuOpen: false, loading: true, searchQuery: '', + showContactPicker: true, + contactPickerforGroup: null, + pickerTypes: [{ + id: 'contacts', + label: t('contacts', 'contacts'), + }], } }, @@ -242,70 +299,32 @@ export default { // generate groups menu from groups store groupsMenu() { return this.groups.map(group => { - return { + return Object.assign(group, { id: group.name.replace(' ', '_'), key: group.name.replace(' ', '_'), router: { name: 'group', params: { selectedGroup: group.name }, }, - text: group.name, - utils: { - counter: group.contacts.length, - actions: [ - { - icon: 'icon-download', - text: 'Download', - action: () => this.downloadGroup(group), - }, - ], - }, - } + icon: group.name === t('contactsinteraction', 'Recently contacted') + ? 'icon-recent-actors' + : '', + }) }).sort(function(a, b) { - return parseInt(b.utils.counter) - parseInt(a.utils.counter) + return parseInt(b.contacts.length) - parseInt(a.contacts.length) }) }, - // building the main menu - menu() { - return this.groupAllGroup.concat(this.groupNotGrouped.concat(this.groupsMenu)) - }, - - // default group for every contacts - groupAllGroup() { - return [{ - id: 'everyone', - key: 'everyone', - icon: 'icon-contacts-dark', - router: { - name: 'group', - params: { selectedGroup: GROUP_ALL_CONTACTS }, - }, - text: GROUP_ALL_CONTACTS, - utils: { - counter: this.sortedContacts.length, - }, - }] - }, - - // default group for every contacts - groupNotGrouped() { - if (this.ungroupedContacts.length === 0) { - return [] - } - return [{ - id: 'notgrouped', - key: 'notgrouped', - icon: 'icon-user', - router: { - name: 'group', - params: { selectedGroup: GROUP_NO_GROUP_CONTACTS }, - }, - text: GROUP_NO_GROUP_CONTACTS, - utils: { - counter: this.ungroupedContacts.length, - }, - }] + /** + * Contacts formatted for the EntityPicker + * @returns {Array} + */ + pickerData() { + return Object.values(this.contacts).map(contact => ({ + id: contact.key, + label: contact.displayName, + type: 'contact', + })) }, }, @@ -539,7 +558,33 @@ export default { const groupName = input.value.trim() this.$store.dispatch('addGroup', groupName) this.isNewGroupMenuOpen = false + + // Select group + this.$router.push({ + name: 'contact', + params: { + selectedGroup: groupName, + selectedContact: undefined, + }, + }) + }, + + // Bulk contacts group management handlers + addContactsToGroup(group) { + this.showContactPicker = true + this.contactPickerforGroup = group }, + + onContactPickerClose() { + this.showContactPicker = false + }, + + onContactPickerPick(selection) { + const group = this.contactPickerforGroup + console.info('Adding', selection, 'to group', group) + this.contactPickerforGroup = null + }, + }, } diff --git a/webpack.common.js b/webpack.common.js index 8aff48847..94dca3a86 100644 --- a/webpack.common.js +++ b/webpack.common.js @@ -50,6 +50,7 @@ module.exports = { new webpack.DefinePlugin({ appVersion }) ], resolve: { - extensions: ['*', '.js', '.vue'] + extensions: ['*', '.js', '.vue'], + symlinks: false, } } From f44028131344636e45c5158cc13ccbe4edd19097 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?John=20Molakvo=C3=A6=20=28skjnldsv=29?= Date: Thu, 2 Jul 2020 17:49:42 +0200 Subject: [PATCH 3/9] Add PatchPlugin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: John Molakvoæ (skjnldsv) --- appinfo/info.xml | 4 + img/recent-actors.svg | 1 + lib/AppInfo/Application.php | 28 ++- lib/Dav/PatchPlugin.php | 186 +++++++++++++++ package-lock.json | 34 ++- src/components/EntityPicker/EntityBubble.vue | 25 +- src/components/EntityPicker/EntityPicker.vue | 215 ++++++++++++------ .../EntityPicker/EntitySearchResult.vue | 21 +- src/components/ProcessingScreen.vue | 57 +++++ src/services/appendContactToGroup.js | 41 ++++ src/store/contacts.js | 2 +- src/views/Contacts.vue | 119 ++++++++-- 12 files changed, 614 insertions(+), 119 deletions(-) create mode 100644 img/recent-actors.svg create mode 100644 lib/Dav/PatchPlugin.php create mode 100644 src/components/ProcessingScreen.vue create mode 100644 src/services/appendContactToGroup.js diff --git a/appinfo/info.xml b/appinfo/info.xml index 1a943d679..931b4b8a5 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -33,6 +33,10 @@ https://github.com/nextcloud/contacts/issues https://github.com/nextcloud/contacts.git + + + + diff --git a/img/recent-actors.svg b/img/recent-actors.svg new file mode 100644 index 000000000..c47e2a6df --- /dev/null +++ b/img/recent-actors.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 565fe5b75..a2f3a3968 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -22,16 +22,36 @@ */ namespace OCA\Contacts\AppInfo; +use OCA\Contacts\Dav\PatchPlugin; use OCP\AppFramework\App; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\SabrePluginEvent; class Application extends App { public const APP_ID = 'contacts'; - - public function __construct() { - parent::__construct(self::APP_ID); - } public const AVAIL_SETTINGS = [ 'allowSocialSync' => 'yes', ]; + + public function __construct() { + parent::__construct(self::APP_ID); + } + + public function register() { + $server = $this->getContainer()->getServer(); + + /** @var IEventDispatcher $eventDispatcher */ + $eventDispatcher = $server->query(IEventDispatcher::class); + $eventDispatcher->addListener('OCA\DAV\Connector\Sabre::addPlugin', function (SabrePluginEvent $event) { + $server = $event->getServer(); + + if ($server !== null) { + // We have to register the LockPlugin here and not info.xml, + // because info.xml plugins are loaded, after the + // beforeMethod:* hook has already been emitted. + $server->addPlugin($this->getContainer()->query(PatchPlugin::class)); + } + }); + } } diff --git a/lib/Dav/PatchPlugin.php b/lib/Dav/PatchPlugin.php new file mode 100644 index 000000000..61f36b7ab --- /dev/null +++ b/lib/Dav/PatchPlugin.php @@ -0,0 +1,186 @@ + + * + * @author John Molakvoæ (skjnldsv) + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Contacts\Dav; + +use Sabre\CardDAV\Card; +use Sabre\DAV; +use Sabre\DAV\INode; +use Sabre\DAV\PropPatch; +use Sabre\DAV\Server; +use Sabre\DAV\ServerPlugin; +use Sabre\HTTP\RequestInterface; +use Sabre\HTTP\ResponseInterface; +use Sabre\VObject\Component\VCard; +use Sabre\VObject\Reader; + +class PatchPlugin extends ServerPlugin { + public const METHOD_REPLACE = 0; + public const METHOD_APPEND = 1; + + /** @var Server */ + protected $server; + + /** + * Initializes the plugin and registers event handlers + * + * @param Server $server + * @return void + */ + public function initialize(Server $server) { + $this->server = $server; + $server->on('method:PATCH', [$this, 'httpPatch']); + } + + /** + * Use this method to tell the server this plugin defines additional + * HTTP methods. + * + * This method is passed a uri. It should only return HTTP methods that are + * available for the specified uri. + * + * We claim to support PATCH method (partirl update) if and only if + * - the node exist + * - the node implements our partial update interface + * + * @param string $uri + * + * @return array + */ + public function getHTTPMethods($uri) { + $tree = $this->server->tree; + + if ($tree->nodeExists($uri)) { + $node = $tree->getNodeForPath($uri); + if ($node instanceof Card) { + return ['PATCH']; + } + } + + return []; + } + + /** + * Adds all CardDAV-specific properties + * + * @param PropPatch $propPatch + * @param INode $node + * @return void + */ + public function httpPatch(RequestInterface $request, ResponseInterface $response) { + $path = $request->getPath(); + $node = $this->server->tree->getNodeForPath($path); + + if (!($node instanceof Card)) { + return true; + } + + // Checking ACL, if available. + if ($aclPlugin = $this->server->getPlugin('acl')) { + /** @var \Sabre\DAVACL\Plugin $aclPlugin */ + $aclPlugin->checkPrivileges($path, '{DAV:}write'); + } + + // Init property name & value + $propertyName = $request->getHeader('X-Property'); + if (is_null($propertyName)) { + throw new DAV\Exception\BadRequest('No valid "X-Property" found in the headers'); + } + + $propertyData = $request->getHeader('X-Property-Replace'); + $method = self::METHOD_REPLACE; + if (is_null($propertyData)) { + $propertyData = $request->getHeader('X-Property-Append'); + $method = self::METHOD_APPEND; + if (is_null($propertyData)) { + throw new DAV\Exception\BadRequest('No valid "X-Property-Append" or "X-Property-Replace" found in the headers'); + } + } + + // Init contact + $vCard = Reader::read($node->get()); + $properties = $vCard->select($propertyName); + + // We cannot know which one to update in that case + if (count($properties) > 1) { + throw new DAV\Exception\BadRequest('The specified property appear more than once'); + } + + // Init if not in the vcard + if (count($properties) === 0) { + $vCard->add($propertyName, $propertyData); + $properties = $vCard->select($propertyName); + } + + // Replace existing value + if ($method === self::METHOD_REPLACE) { + $properties[0]->setRawMimeDirValue($propertyData); + } + + // Append to existing value + if ($method === self::METHOD_APPEND) { + $oldData = $properties[0]->getValue(); + $properties[0]->setRawMimeDirValue($oldData.$propertyData); + } + + // Validate & write + $vCard->validate(); + $node->put($vCard->serialize()); + $response->setStatus(200); + + return false; + } + + /** + * Returns a plugin name. + * + * Using this name other plugins will be able to access other plugins + * using \Sabre\DAV\Server::getPlugin + * + * @return string + */ + public function getPluginName() { + return 'vcard-patch'; + } + + /** + * Returns a bunch of meta-data about the plugin. + * + * Providing this information is optional, and is mainly displayed by the + * Browser plugin. + * + * The description key in the returned array may contain html and will not + * be sanitized. + * + * @return array + */ + public function getPluginInfo() { + return [ + 'name' => $this->getPluginName(), + 'description' => 'Allow to patch unique properties.' + ]; + } +} diff --git a/package-lock.json b/package-lock.json index 74baae906..90627ef4e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2673,9 +2673,15 @@ } }, "@nextcloud/vue": { +<<<<<<< HEAD "version": "2.3.0", "resolved": "https://registry.npmjs.org/@nextcloud/vue/-/vue-2.3.0.tgz", "integrity": "sha512-6uf7Hu4Obaet7BOs9H/Ng63xAYqks9CL7hsOOHGUzWFYrPPBxgt79iD9OOPpPfJuLQ3Nnuibh942X1QreCBRkw==", +======= + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@nextcloud/vue/-/vue-2.2.0.tgz", + "integrity": "sha512-F7KA39DrBQT/IFY42rqfcA0NvOqQ06PUtI6Htph5quXXgXdvqIqRSb+w2/aWkmprKwHRaBMtCX3Dxrd+uGdqpw==", +>>>>>>> 90efae4b... Add PatchPlugin "requires": { "@nextcloud/auth": "^1.2.3", "@nextcloud/axios": "^1.3.2", @@ -4349,9 +4355,15 @@ } }, "date-fns": { +<<<<<<< HEAD "version": "2.15.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.15.0.tgz", "integrity": "sha512-ZCPzAMJZn3rNUvvQIMlXhDr4A+Ar07eLeGsGREoWU19a3Pqf5oYa+ccd+B3F6XVtQY6HANMFdOQ8A+ipFnvJdQ==" +======= + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.14.0.tgz", + "integrity": "sha512-1zD+68jhFgDIM0rF05rcwYO8cExdNqxjq4xP1QKM60Q45mnO6zaMWB4tOzrIr4M4GSLntsKeE4c9Bdl2jhL/yw==" +>>>>>>> 90efae4b... Add PatchPlugin }, "date-format-parse": { "version": "0.2.5", @@ -6016,6 +6028,11 @@ "brace-expansion": "^1.1.7" } }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + }, "minipass": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", @@ -6042,6 +6059,14 @@ "optional": true, "requires": { "minimist": "^1.2.5" + }, + "dependencies": { + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "optional": true + } } }, "ms": { @@ -7958,7 +7983,8 @@ "minimist": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true }, "minimist-options": { "version": "3.0.2", @@ -11945,9 +11971,15 @@ } }, "webpack-node-externals": { +<<<<<<< HEAD "version": "2.5.1", "resolved": "https://registry.npmjs.org/webpack-node-externals/-/webpack-node-externals-2.5.1.tgz", "integrity": "sha512-RWxKGibUU5kuJT6JDYmXGa3QsZskqIaiBvZ2wBxHlJzWVJPOyBMnroXf23uxEHnj1rYS8jNdyUfrNAXJ2bANNw==", +======= + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/webpack-node-externals/-/webpack-node-externals-2.5.0.tgz", + "integrity": "sha512-g7/Z7Q/gsP8GkJkKZuJggn6RSb5PvxW1YD5vvmRZIxaSxAzkqjfL5n9CslVmNYlSqBVCyiqFgOqVS2IOObCSRg==", +>>>>>>> 90efae4b... Add PatchPlugin "dev": true }, "webpack-sources": { diff --git a/src/components/EntityPicker/EntityBubble.vue b/src/components/EntityPicker/EntityBubble.vue index 0394d9c29..bcc7705f3 100644 --- a/src/components/EntityPicker/EntityBubble.vue +++ b/src/components/EntityPicker/EntityBubble.vue @@ -92,17 +92,22 @@ export default { background-color: var(--color-primary-light); } -.entity-picker__bubble-delete { - display: block; - height: 100%; - // squeeze in the border radius - margin-right: -4px; - opacity: .7; +.entity-picker__bubble { + // Add space between bubbles + margin-right: 4px; - &:hover, - &:active, - &:focus { - opacity: 1; + &-delete { + display: block; + height: 100%; + // squeeze in the border radius + margin-right: -4px; + opacity: .7; + + &:hover, + &:active, + &:focus { + opacity: 1; + } } } diff --git a/src/components/EntityPicker/EntityPicker.vue b/src/components/EntityPicker/EntityPicker.vue index d0421af03..a60ee85ea 100644 --- a/src/components/EntityPicker/EntityPicker.vue +++ b/src/components/EntityPicker/EntityPicker.vue @@ -33,44 +33,49 @@ v-model="searchQuery" class="entity-picker__search-input" type="search" - :placeholder="t('contacts', 'Search contacts')"> + :placeholder="t('contacts', 'Search {types}', {types: searchPlaceholderTypes})" + @change="onSearch">
- -
- - - + + + + + + + {{ t('contacts', 'Loading …') }} + + + +
+ +
+ +

+ {{ t('contacts', 'Add {type}', {type: type.label.toLowerCase()}) }} +

+ + - - - - - - {{ t('contacts', 'No results') }} - - - - {{ t('contacts', 'Loading …') }} - + @click="onToggle(entity)" /> +
+ + {{ t('contacts', 'No results') }} + +