diff options
author | John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com> | 2020-09-03 11:06:32 +0200 |
---|---|---|
committer | John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com> | 2020-09-03 15:00:23 +0200 |
commit | c7c23b936e35f54f6a750cedc4afe138685a6d3a (patch) | |
tree | 8ac636373d0962a9ef7b0bb9bb65f8f55483feb5 | |
parent | 791f215a25bac11bbd145467d5e9eb1777249282 (diff) |
Fix virtual group
Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
-rw-r--r-- | lib/AppInfo/Application.php | 2 | ||||
-rw-r--r-- | src/components/ContactDetails.vue | 11 | ||||
-rw-r--r-- | src/components/ContactsList.vue | 27 | ||||
-rw-r--r-- | src/components/EntityPicker/EntityBubble.vue | 1 | ||||
-rw-r--r-- | src/components/EntityPicker/EntityPicker.vue | 18 | ||||
-rw-r--r-- | src/components/ProcessingScreen.vue | 37 | ||||
-rw-r--r-- | src/components/Properties/PropertyGroups.vue | 2 | ||||
-rw-r--r-- | src/views/Contacts.vue | 168 |
8 files changed, 166 insertions, 100 deletions
diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 67ef5121..e949cb69 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -49,7 +49,7 @@ class Application extends App { $server = $event->getServer(); if ($server !== null) { - // We have to register the LockPlugin here and not info.xml, + // We have to register the PatchPlugin 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/src/components/ContactDetails.vue b/src/components/ContactDetails.vue index 81806cc3..7073a78e 100644 --- a/src/components/ContactDetails.vue +++ b/src/components/ContactDetails.vue @@ -23,18 +23,13 @@ <template> <AppContentDetails> <!-- nothing selected or contact not found --> - <EmptyContent v-if="!contact && !loading" icon="icon-contacts"> + <EmptyContent v-if="!contact" icon="icon-contacts"> {{ t('contacts', 'No contact selected') }} <template #desc> {{ t('contacts', 'Select a contact on the list to begin') }} </template> </EmptyContent> - <!-- loading --> - <EmptyContent v-else-if="loading" icon="icon-contacts"> - {{ t('contacts', 'Loading contacts …') }} - </EmptyContent> - <template v-else> <!-- contact header --> <header class="contact-header"> @@ -287,10 +282,6 @@ export default { }, props: { - loading: { - type: Boolean, - default: true, - }, contactKey: { type: String, default: undefined, diff --git a/src/components/ContactsList.vue b/src/components/ContactsList.vue index 743dc77c..2c81b271 100644 --- a/src/components/ContactsList.vue +++ b/src/components/ContactsList.vue @@ -24,10 +24,9 @@ <!-- same uid can coexists between different addressbooks so we need to use the addressbook id as key as well --> <RecycleScroller - v-if="haveContact" id="contacts-list" ref="scroller" - :class="{'icon-loading': loading, showdetails: selectedContact}" + :class="{ showdetails: selectedContact }" class="app-content-list" :items="filteredList" :item-size="itemHeight" @@ -41,22 +40,10 @@ @deleted="selectContact" /> </template> </RecycleScroller> - - <div v-else class="app-content-list"> - <EmptyContent> - {{ t('contacts', 'No contacts in this group') }} - <template #action> - <button class="primary" @click="onAddContactsToGroup"> - {{ t('forms', 'Add some') }} - </button> - </template> - </EmptyContent> - </div> </template> <script> import ContactsListItem from './ContactsList/ContactsListItem' -import EmptyContent from './EmptyContent' import { RecycleScroller } from 'vue-virtual-scroller/dist/vue-virtual-scroller.umd.js' import 'vue-virtual-scroller/dist/vue-virtual-scroller.css' @@ -65,7 +52,6 @@ export default { components: { ContactsListItem, - EmptyContent, RecycleScroller, }, @@ -78,10 +64,6 @@ export default { type: Object, required: true, }, - loading: { - type: Boolean, - default: true, - }, searchQuery: { type: String, default: '', @@ -104,9 +86,6 @@ export default { filteredList() { return this.list.filter(contact => this.matchSearch(this.contacts[contact.key])) }, - haveContact() { - return this.selectedGroup && this.filteredList.length > 0 - }, }, watch: { @@ -177,10 +156,6 @@ export default { } return true }, - - onAddContactsToGroup() { - this.$emit('onAddContactsToGroup') - }, }, } </script> diff --git a/src/components/EntityPicker/EntityBubble.vue b/src/components/EntityPicker/EntityBubble.vue index bcc7705f..5388429d 100644 --- a/src/components/EntityPicker/EntityBubble.vue +++ b/src/components/EntityPicker/EntityBubble.vue @@ -95,6 +95,7 @@ export default { .entity-picker__bubble { // Add space between bubbles margin-right: 4px; + display: flex; &-delete { display: block; diff --git a/src/components/EntityPicker/EntityPicker.vue b/src/components/EntityPicker/EntityPicker.vue index a35da7c1..b9c8d7cf 100644 --- a/src/components/EntityPicker/EntityPicker.vue +++ b/src/components/EntityPicker/EntityPicker.vue @@ -30,10 +30,11 @@ <div class="entity-picker__search"> <div class="entity-picker__search-icon icon-search" /> <input + ref="input" v-model="searchQuery" + :placeholder="t('contacts', 'Search {types}', {types: searchPlaceholderTypes})" class="entity-picker__search-input" type="search" - :placeholder="t('contacts', 'Search {types}', {types: searchPlaceholderTypes})" @change="onSearch"> </div> @@ -51,10 +52,15 @@ </transition-group> <!-- TODO: find better wording/icon --> - <EmptyContent v-if="loading" icon=""> + <EmptyContent v-if="loading" icon="icon-loading"> {{ t('contacts', 'Loading …') }} </EmptyContent> + <!-- TODO: find better wording/icon --> + <EmptyContent v-else-if="dataSet.length === 0" icon=""> + {{ t('contacts', 'List is empty') }} + </EmptyContent> + <!-- Searched & picked entities --> <VirtualList v-else-if="searchSet.length > 0 && availableEntities.length > 0" class="entity-picker__options" @@ -216,6 +222,13 @@ export default { }, }, + mounted() { + this.$nextTick(() => { + this.$refs.input.focus() + this.$refs.input.select() + }) + }, + methods: { onCancel() { /** @@ -344,6 +357,7 @@ $icon-margin: ($clickable-area - $icon-size) / 2; &__options { margin: $entity-spacing 0; overflow-y: auto; + flex: 1 1 100%; } &__navigation { diff --git a/src/components/ProcessingScreen.vue b/src/components/ProcessingScreen.vue index 9a666732..8a7269e3 100644 --- a/src/components/ProcessingScreen.vue +++ b/src/components/ProcessingScreen.vue @@ -5,7 +5,9 @@ <div class="processing-screen__progress"> <progress :max="total" :value="progress" /> </div> - <slot name="desc" /> + <div class="processing-screen__desc"> + <slot name="desc" /> + </div> </template> </EmptyContent> </template> @@ -42,25 +44,38 @@ export default { min-width: 30vw; margin: 50px; - // Progress wrapper + // Progress &desc wrapper &::v-deep > p { display: flex; + align-items: center; + flex-direction: column; width: 80%; margin: auto; } - - button { - padding: 10px; - height: 44px; - align-self: flex-end; - margin-top: 22px; - min-width: 100px; - } } &__progress { - width: 100%; display: flex; + width: 100%; + } + + &__desc { + display: inline-flex; + align-items: center; + margin-top: 22px; + color: var(--color-text-maxcontrast); + + &::v-deep button { + min-width: 100px; + height: 44px; + padding: 10px 20px; + + // Put buttons at the end + &:first-of-type { + margin-left: auto; + } + } } } + </style> diff --git a/src/components/Properties/PropertyGroups.vue b/src/components/Properties/PropertyGroups.vue index 170565e1..ad0fca20 100644 --- a/src/components/Properties/PropertyGroups.vue +++ b/src/components/Properties/PropertyGroups.vue @@ -51,7 +51,7 @@ +{{ localValue.length - 3 }} </span> <span slot="noResult"> - {{ t('settings', 'No results') }} + {{ t('contacts', 'No results') }} </span> </Multiselect> </div> diff --git a/src/views/Contacts.vue b/src/views/Contacts.vue index 3fb2e903..aedc3b3c 100644 --- a/src/views/Contacts.vue +++ b/src/views/Contacts.vue @@ -22,7 +22,7 @@ --> <template> - <Content app-name="contacts" :class="{'icon-loading': loading}"> + <Content app-name="contacts"> <!-- new-contact-button + navigation + settings --> <AppNavigation> <!-- new-contact-button --> @@ -43,7 +43,7 @@ params: { selectedGroup: GROUP_ALL_CONTACTS }, }" icon="icon-contacts-dark"> - <AppNavigationCounter slot="counter"> + <AppNavigationCounter v-if="sortedContacts.length" slot="counter"> {{ sortedContacts.length }} </AppNavigationCounter> </AppNavigationItem> @@ -58,7 +58,7 @@ params: { selectedGroup: GROUP_NO_GROUP_CONTACTS }, }" icon="icon-user"> - <AppNavigationCounter slot="counter"> + <AppNavigationCounter v-if="ungroupedContacts.length" slot="counter"> {{ ungroupedContacts.length }} </AppNavigationCounter> </AppNavigationItem> @@ -67,13 +67,13 @@ <AppNavigationItem v-if="isContactsInteractionEnabled && recentlyContactedContacts && recentlyContactedContacts.contacts.length > 0" id="recentlycontacted" - :title="t('contactsinteraction', 'Recently contacted')" + :title="GROUP_RECENTLY_CONTACTED" :to="{ name: 'group', - params: { selectedGroup: t('contactsinteraction', 'Recently contacted') }, + params: { selectedGroup: GROUP_RECENTLY_CONTACTED }, }" icon="icon-recent-actors"> - <AppNavigationCounter slot="counter"> + <AppNavigationCounter v-if="recentlyContactedContacts.contacts.length" slot="counter"> {{ recentlyContactedContacts.contacts.length }} </AppNavigationCounter> </AppNavigationItem> @@ -99,12 +99,13 @@ </ActionButton> </template> - <AppNavigationCounter slot="counter"> + <AppNavigationCounter v-if="group.contacts.length > 0" slot="counter"> {{ group.contacts.length }} </AppNavigationCounter> </AppNavigationItem> <AppNavigationItem + id="newgroup" :force-menu="true" :menu-open.sync="isNewGroupMenuOpen" :title="t('contacts', '+ New group')" @@ -131,25 +132,47 @@ </AppNavigation> <AppContent> - <!-- go back to list when in details mode --> - <div v-if="selectedContact && isMobile" - id="app-details-toggle" - class="icon-confirm" - tabindex="0" - @click="showList" /> - - <div id="app-content-wrapper"> + <div v-if="loading"> + <EmptyContent icon="icon-loading"> + {{ t('contacts', 'Loading contacts …') }} + </EmptyContent> + </div> + + <div v-else-if="isEmptyGroup && !isRealGroup"> + <EmptyContent icon="icon-contacts-dark"> + {{ t('contacts', `You don't have any contacts yet`) }} + <template #desc> + <button class="primary" @click="newContact"> + {{ t('contacts', 'Create contact') }} + </button> + </template> + </EmptyContent> + </div> + + <div v-else-if="isEmptyGroup && isRealGroup"> + <EmptyContent icon="icon-contacts-dark"> + {{ t('contacts', 'There are no contacts in this group') }} + <template #desc> + <button v-if="contacts.length === 0" class="primary" @click="addContactsToGroup(selectedGroup)"> + {{ t('contacts', 'Create contacts') }} + </button> + <button v-else class="primary" @click="addContactsToGroup(selectedGroup)"> + {{ t('contacts', 'Add contacts') }} + </button> + </template> + </EmptyContent> + </div> + + <div v-else id="app-content-wrapper"> <!-- contacts list --> <ContactsList v-if="!loading" :list="contactsList" :contacts="contacts" - :loading="loading" - :search-query="searchQuery" - @onAddContactsToGroup="addContactsToGroup(selectedGroup)" /> + :search-query="searchQuery" /> <!-- main contacts details --> - <ContactDetails :loading="loading" :contact-key="selectedContact" /> + <ContactDetails :contact-key="selectedContact" /> </div> </AppContent> @@ -176,19 +199,27 @@ <ProcessingScreen v-bind="processStatus"> {{ processStatus.total === processStatus.progress ? n('contacts', - '{total} contact added to {name}', - '{total} contacts added to {name}', - processStatus.total, + '{success} contact added to {name}', + '{success} contacts added to {name}', + processStatus.success, processStatus ) : n('contacts', - 'Adding {total} contact to {name}', - 'Adding {total} contacts to {name}', - processStatus.total, + 'Adding {success} contact to {name}', + 'Adding {success} contacts to {name}', + processStatus.success, processStatus ) }} <template #desc> - <button v-if="processStatus.total === processStatus.progress" class="primary processing-screen__button" @click="closeProcess"> + <span v-if="processStatus.error > 0"> + {{ n('contacts', + '{count} error', + '{count} errors', + processStatus.error, + {count: processStatus.error} + ) }} + </span> + <button v-if="processStatus.total === processStatus.progress" class="primary" @click="closeProcess"> {{ t('contacts', 'Close') }} </button> </template> @@ -209,6 +240,7 @@ import AppNavigationNew from '@nextcloud/vue/dist/Components/AppNavigationNew' import AppNavigationSettings from '@nextcloud/vue/dist/Components/AppNavigationSettings' import AppNavigationSpacer from '@nextcloud/vue/dist/Components/AppNavigationSpacer' import Content from '@nextcloud/vue/dist/Components/Content' +import EmptyContent from '@nextcloud/vue/dist/Components/EmptyContent' import isMobile from '@nextcloud/vue/dist/Mixins/isMobile' import Modal from '@nextcloud/vue/dist/Components/Modal' @@ -234,6 +266,7 @@ import isContactsInteractionEnabled from '../services/isContactsInteractionEnabl const GROUP_ALL_CONTACTS = t('contacts', 'All contacts') const GROUP_NO_GROUP_CONTACTS = t('contacts', 'Not grouped') +const GROUP_RECENTLY_CONTACTED = t('contactsinteraction', 'Recently contacted') export default { name: 'Contacts', @@ -252,6 +285,7 @@ export default { ContactDetails, ContactsList, Content, + EmptyContent, EntityPicker, ImportScreen, Modal, @@ -279,6 +313,7 @@ export default { return { GROUP_ALL_CONTACTS, GROUP_NO_GROUP_CONTACTS, + GROUP_RECENTLY_CONTACTED, isContactsInteractionEnabled, loading: true, @@ -300,8 +335,10 @@ export default { isProcessing: false, isProcessDone: false, processStatus: { - total: 0, + error: 0, progress: 0, + success: 0, + total: 0, name: '', }, } @@ -328,10 +365,17 @@ export default { return this.$store.getters.getImportState }, - // importing states + /** + * Are we importing contacts ? + * @returns {boolean} + */ isImporting() { return this.importState.stage !== 'default' }, + /** + * Are we done importing contacts ? + * @returns {boolean} + */ isImportDone() { return this.importState.stage === 'done' }, @@ -342,6 +386,30 @@ export default { }, /** + * Is this a real group ? + * Aka not a dynamically generated one like `All contacts` + * @returns {boolean} + */ + isRealGroup() { + return this.groups.findIndex(group => group.name === this.selectedGroup) > -1 + }, + /** + * Is this a real group and is this empty + * @returns {boolean} + */ + isEmptyRealGroup() { + return this.contactsList.length === 0 + && this.isRealGroup + }, + /** + * Is the current group empty + * @returns {boolean} + */ + isEmptyGroup() { + return this.contactsList.length === 0 + }, + + /** * Contacts list based on the selected group. * Those filters are pretty fast, so let's only * intersect the groups contacts and the full @@ -382,7 +450,7 @@ export default { menu.sort() // Find the Recently Contacted group, delete it from array - const recentlyIndex = menu.findIndex(group => group.name === t('contactsinteraction', 'Recently contacted')) + const recentlyIndex = menu.findIndex(group => group.name === GROUP_RECENTLY_CONTACTED) if (recentlyIndex >= 0) { menu.splice(recentlyIndex, 1) } @@ -392,7 +460,7 @@ export default { // Recently contacted data recentlyContactedContacts() { - return this.groups.find(group => group.name === t('contactsinteraction', 'Recently contacted')) + return this.groups.find(group => group.name === GROUP_RECENTLY_CONTACTED) }, }, @@ -619,27 +687,10 @@ export default { }, /** - * Show the list and deselect contact - */ - showList() { - // Reset the selected contact - this.$router.push({ - name: 'contact', - params: { - selectedGroup: this.selectedGroup, - selectedContact: undefined, - }, - }) - }, - - /** * Done importing, the user closed the import status screen */ closeImport() { - // TODO: remove after https://github.com/nextcloud/nextcloud-vue/pull/323 - if (this.isImportDone) { - this.$store.dispatch('changeStage', 'default') - } + this.$store.dispatch('changeStage', 'default') }, toggleNewGroupMenu() { @@ -663,7 +714,7 @@ export default { // Select group this.$router.push({ - name: 'contact', + name: 'group', params: { selectedGroup: groupName, }, @@ -715,6 +766,8 @@ export default { this.processStatus.total = selection.length this.processStatus.name = this.contactPickerforGroup.name + this.processStatus.progress = 0 + this.processStatus.error = 0 // max simultaneous requests const limit = pLimit(3) @@ -731,9 +784,11 @@ export default { .then((response) => { this.$store.dispatch('addContactToGroup', { contact, groupName }) this.processStatus.progress++ + this.processStatus.success++ }) .catch((error) => { this.processStatus.progress++ + this.processStatus.error++ console.error(error) }) )) @@ -745,6 +800,11 @@ export default { Promise.all(requests).then(() => { this.isProcessDone = true this.showContactPicker = false + + // Auto close after 3 seconds if no errors + if (this.processStatus.error === 0) { + setTimeout(this.closeProcess, 3000) + } }) }, @@ -752,6 +812,12 @@ export default { this.contactPickerforGroup = null this.isProcessing = false this.isProcessDone = false + + // Reset + this.processStatus.error = 0 + this.processStatus.progress = 0 + this.processStatus.success = 0 + this.processStatus.total = 0 }, }, @@ -759,6 +825,10 @@ export default { </script> <style lang="scss" scoped> +#newgroup a { + color: var(--color-text-maxcontrast); +} + #app-content-wrapper { display: flex; } |