summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJohn Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>2020-09-03 11:06:32 +0200
committerJohn Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>2020-09-03 15:00:23 +0200
commitc7c23b936e35f54f6a750cedc4afe138685a6d3a (patch)
tree8ac636373d0962a9ef7b0bb9bb65f8f55483feb5
parent791f215a25bac11bbd145467d5e9eb1777249282 (diff)
Fix virtual group
Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
-rw-r--r--lib/AppInfo/Application.php2
-rw-r--r--src/components/ContactDetails.vue11
-rw-r--r--src/components/ContactsList.vue27
-rw-r--r--src/components/EntityPicker/EntityBubble.vue1
-rw-r--r--src/components/EntityPicker/EntityPicker.vue18
-rw-r--r--src/components/ProcessingScreen.vue37
-rw-r--r--src/components/Properties/PropertyGroups.vue2
-rw-r--r--src/views/Contacts.vue168
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;
}