summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorJohn Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>2020-07-24 10:31:16 +0200
committerJohn Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>2020-08-21 09:56:08 +0200
commit68cb27a5b4ef0eecd7a0af597d3050bc709c5754 (patch)
treecd0354dfcc98d7e7528b7460f10c668bf46100d6 /src
parent8439848c3da189a0cd3cac7c2aa149d0a207b00b (diff)
Fix performances and add some button + end of process screen feedback
Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
Diffstat (limited to 'src')
-rw-r--r--src/components/ContactsList.vue2
-rw-r--r--src/components/EntityPicker/EntityPicker.vue63
-rw-r--r--src/components/EntityPicker/EntitySearchResult.vue110
-rw-r--r--src/components/ProcessingScreen.vue11
-rw-r--r--src/views/Contacts.vue77
5 files changed, 164 insertions, 99 deletions
diff --git a/src/components/ContactsList.vue b/src/components/ContactsList.vue
index 1a5f0745..d89d493c 100644
--- a/src/components/ContactsList.vue
+++ b/src/components/ContactsList.vue
@@ -179,7 +179,7 @@ export default {
},
onAddContactsToGroup() {
- // TODO: add popup
+ this.$emit('onAddContactsToGroup')
},
},
}
diff --git a/src/components/EntityPicker/EntityPicker.vue b/src/components/EntityPicker/EntityPicker.vue
index a60ee85e..f4f11e5e 100644
--- a/src/components/EntityPicker/EntityPicker.vue
+++ b/src/components/EntityPicker/EntityPicker.vue
@@ -56,21 +56,13 @@
</EmptyContent>
<!-- Searched & picked entities -->
- <div v-else-if="searchSet.length > 0 && availableEntities.length > 0" class="entity-picker__options">
- <!-- For each type we show title + list -->
- <div v-for="type in availableEntities" :key="type.id" class="entity-picker__option">
- <!-- Show content if we have something to show -->
- <h4 v-if="!isSingleType && type.dataSet.length > 0" class="entity-picker__option-caption">
- {{ t('contacts', 'Add {type}', {type: type.label.toLowerCase()}) }}
- </h4>
-
- <EntitySearchResult v-for="entity in type.dataSet"
- :key="entity.key || `entity-${entity.type}-${entity.id}`"
- :selection="selection"
- v-bind="entity"
- @click="onToggle(entity)" />
- </div>
- </div>
+ <VirtualList v-else-if="searchSet.length > 0 && availableEntities.length > 0"
+ class="entity-picker__options"
+ data-key="id"
+ :data-sources="availableEntities"
+ :data-component="EntitySearchResult"
+ :estimate-size="44"
+ :extra-props="{selection, onClick: onPick}" />
<EmptyContent v-else-if="searchQuery" icon="icon-search">
{{ t('contacts', 'No results') }}
@@ -95,6 +87,8 @@
<script>
import Modal from '@nextcloud/vue/dist/Components/Modal'
import EmptyContent from '@nextcloud/vue/dist/Components/EmptyContent'
+import VirtualList from 'vue-virtual-scroll-list'
+
import EntityBubble from './EntityBubble'
import EntitySearchResult from './EntitySearchResult'
@@ -104,8 +98,8 @@ export default {
components: {
EmptyContent,
EntityBubble,
- EntitySearchResult,
Modal,
+ VirtualList,
},
props: {
@@ -152,6 +146,7 @@ export default {
return {
searchQuery: '',
selection: {},
+ EntitySearchResult,
}
},
@@ -192,19 +187,18 @@ export default {
availableEntities() {
// If only one type, return the full set directly
if (this.isSingleType) {
- return [{
- id: this.dataTypes[0].id,
- label: this.dataTypes[0].label,
- dataSet: this.searchSet,
- }]
+ return this.searchSet
}
// Else group by types
- return this.dataTypes.map(type => ({
- id: type.id,
- label: type.label,
- dataSet: this.searchSet.filter(entity => entity.type === type.id),
- }))
+ return this.dataTypes.map(type => [
+ {
+ id: type.id,
+ label: type.label,
+ heading: true,
+ },
+ ...this.searchSet.filter(entity => entity.type === type.id),
+ ]).flat()
},
},
@@ -337,23 +331,6 @@ $icon-margin: ($clickable-area - $icon-size) / 2;
margin: $entity-spacing 0;
overflow-y: auto;
}
- &__option {
- &-caption {
- padding-left: 10px;
- list-style-type: none;
- user-select: none;
- white-space: nowrap;
- text-overflow: ellipsis;
- pointer-events: none;
- color: var(--color-primary);
- box-shadow: none !important;
- line-height: $clickable-area;
-
- &:not(:first-child) {
- margin-top: $clickable-area / 2;
- }
- }
- }
&__navigation {
z-index: 1;
diff --git a/src/components/EntityPicker/EntitySearchResult.vue b/src/components/EntityPicker/EntitySearchResult.vue
index 9962541a..56693d82 100644
--- a/src/components/EntityPicker/EntitySearchResult.vue
+++ b/src/components/EntityPicker/EntitySearchResult.vue
@@ -20,14 +20,19 @@
-->
<template>
+ <h4 v-if="source.heading" :key="source.id" class="entity-picker__option-caption">
+ {{ t('contacts', 'Add {type}', {type: source.label.toLowerCase()}) }}
+ </h4>
+
<UserBubble
+ v-else
class="entity-picker__bubble"
:class="{'entity-picker__bubble--selected': isSelected}"
- :display-name="label"
+ :display-name="source.label"
:margin="6"
:size="44"
url="#"
- @click.stop.prevent="onClick">
+ @click.stop.prevent="onClick(source)">
<template #title>
<span class="entity-picker__bubble-checkmark icon-checkmark" />
</template>
@@ -45,44 +50,25 @@ export default {
},
props: {
- /**
- * Unique id of the entity
- */
- id: {
- type: String,
- required: true,
+ source: {
+ type: Object,
+ default() {
+ return {}
+ },
},
-
- /**
- * Label of the entity
- */
- label: {
- type: String,
- required: true,
+ onClick: {
+ type: Function,
+ default() {},
},
-
- /**
- * Label of the entity
- */
selection: {
type: Object,
- required: true,
+ default: () => ([]),
},
},
computed: {
isSelected() {
- return this.id in this.selection
- },
- },
-
- methods: {
- /**
- * Forward click to parent
- * @param {Event} event the click event
- */
- onClick(event) {
- this.$emit('click', event)
+ return this.source.id in this.selection
},
},
}
@@ -90,23 +76,57 @@ export default {
</script>
<style lang="scss" scoped>
-.entity-picker__bubble {
- display: flex;
- margin-bottom: 4px;
- .entity-picker__bubble-checkmark {
- display: block;
- opacity: 0;
+// https://uxplanet.org/7-rules-for-mobile-ui-button-design-e9cf2ea54556
+// recommended is 48px
+// 44px is what we choose and have very good visual-to-usability ratio
+$clickable-area: 44px;
+
+// background icon size
+// also used for the scss icon font
+$icon-size: 16px;
+
+// icon padding for a $clickable-area width and a $icon-size icon
+// ( 44px - 16px ) / 2
+$icon-margin: ($clickable-area - $icon-size) / 2;
+
+.entity-picker {
+ &__option {
+ &-caption {
+ padding-left: 10px;
+ list-style-type: none;
+ user-select: none;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ pointer-events: none;
+ color: var(--color-primary);
+ box-shadow: none !important;
+ line-height: $clickable-area;
+
+ &:not(:first-child) {
+ margin-top: $clickable-area / 2;
+ }
+ }
}
- &--selected,
- &:hover,
- &:focus {
- .entity-picker__bubble-checkmark {
- opacity: 1;
+ &__bubble {
+ display: flex;
+ margin-bottom: 4px;
+
+ &-checkmark {
+ display: block;
+ opacity: 0;
}
- ::v-deep .user-bubble__content {
- // better visual with light default tint
- background-color: var(--color-primary-light);
+
+ &--selected,
+ &:hover,
+ &:focus {
+ .entity-picker__bubble-checkmark {
+ opacity: 1;
+ }
+ ::v-deep .user-bubble__content {
+ // better visual with light default tint
+ background-color: var(--color-primary-light);
+ }
}
}
}
diff --git a/src/components/ProcessingScreen.vue b/src/components/ProcessingScreen.vue
index e362187d..ab1d0844 100644
--- a/src/components/ProcessingScreen.vue
+++ b/src/components/ProcessingScreen.vue
@@ -5,6 +5,7 @@
<div class="processing-screen__progress">
<progress :max="total" :value="progress" />
</div>
+ <slot name="desc" />
</template>
</EmptyContent>
</template>
@@ -43,10 +44,18 @@ export default {
// Progress wrapper
&::v-deep > p {
- display: block;
+ display: flex;
width: 80%;
margin: auto;
}
+
+ button {
+ padding: 10px;
+ height: 44px;
+ align-self: flex-end;
+ margin-top: 22px;
+ min-width: 100px;
+ }
}
&__progress {
diff --git a/src/views/Contacts.vue b/src/views/Contacts.vue
index 3bcdb91a..5e1db112 100644
--- a/src/views/Contacts.vue
+++ b/src/views/Contacts.vue
@@ -111,8 +111,11 @@
menu-icon="icon-add"
@click.prevent.stop="toggleNewGroupMenu">
<template slot="actions">
+ <ActionText :icon="createGroupError ? 'icon-error' : 'icon-contacts-dark'">
+ {{ createGroupError ? createGroupError : t('contacts', 'Create a new group') }}
+ </ActionText>
<ActionInput
- icon="icon-contacts-dark"
+ icon=""
:placeholder="t('contacts','Group name')"
@submit.prevent.stop="createNewGroup" />
</template>
@@ -142,7 +145,8 @@
:list="contactsList"
:contacts="contacts"
:loading="loading"
- :search-query="searchQuery" />
+ :search-query="searchQuery"
+ @onAddContactsToGroup="addContactsToGroup(selectedGroup)" />
<!-- main contacts details -->
<ContactDetails :loading="loading" :contact-key="selectedContact" />
@@ -170,12 +174,24 @@
:can-close="isProcessDone"
@close="closeProcess">
<ProcessingScreen v-bind="processStatus">
- {{ n('contacts',
- 'Adding {total} contact to {name}',
- 'Adding {total} contacts to {name}',
- processStatus.total,
- processStatus
- ) }}
+ {{ processStatus.total === processStatus.progress
+ ? n('contacts',
+ '{total} contact added to {name}',
+ '{total} contacts added to {name}',
+ processStatus.total,
+ processStatus
+ )
+ : n('contacts',
+ 'Adding {total} contact to {name}',
+ 'Adding {total} contacts to {name}',
+ processStatus.total,
+ processStatus
+ ) }}
+ <template #desc>
+ <button v-if="processStatus.total === processStatus.progress" class="primary processing-screen__button" @click="closeProcess">
+ {{ t('contacts', 'Close') }}
+ </button>
+ </template>
</ProcessingScreen>
</Modal>
</Content>
@@ -184,6 +200,7 @@
<script>
import ActionButton from '@nextcloud/vue/dist/Components/ActionButton'
import ActionInput from '@nextcloud/vue/dist/Components/ActionInput'
+import ActionText from '@nextcloud/vue/dist/Components/ActionText'
import AppContent from '@nextcloud/vue/dist/Components/AppContent'
import AppNavigation from '@nextcloud/vue/dist/Components/AppNavigation'
import AppNavigationCounter from '@nextcloud/vue/dist/Components/AppNavigationCounter'
@@ -223,6 +240,7 @@ export default {
components: {
ActionButton,
ActionInput,
+ ActionText,
AppContent,
AppNavigation,
AppNavigationCounter,
@@ -262,9 +280,13 @@ export default {
GROUP_ALL_CONTACTS,
GROUP_NO_GROUP_CONTACTS,
isContactsInteractionEnabled,
+ loading: true,
+
+ // Create group
isCreatingGroup: false,
isNewGroupMenuOpen: false,
loading: true,
+ createGroupError: null,
// Add to group picker
searchQuery: '',
@@ -520,9 +542,28 @@ export default {
selectFirstContactIfNone() {
const inList = this.contactsList.findIndex(contact => contact.key === this.selectedContact) > -1
if (this.selectedContact === undefined || !inList) {
+ // Unknown contact
if (this.selectedContact && !inList) {
OC.Notification.showTemporary(t('contacts', 'Contact not found'))
+ this.$router.push({
+ name: 'group',
+ params: {
+ selectedGroup: this.selectedGroup,
+ },
+ })
}
+
+ // Unknown group
+ if (!this.groups.find(group => group.name === this.selectedGroup)
+ && this.GROUP_ALL_CONTACTS !== this.selectedGroup
+ && this.GROUP_NO_GROUP_CONTACTS !== this.selectedGroup) {
+ OC.Notification.showTemporary(t('contacts', 'Group not found'))
+ this.$router.push({
+ name: 'root',
+ })
+ return
+ }
+
if (Object.keys(this.contactsList).length) {
this.$router.push({
name: 'contact',
@@ -615,6 +656,16 @@ export default {
createNewGroup(e) {
const input = e.target.querySelector('input[type=text]')
const groupName = input.value.trim()
+
+ // Check if already exists
+ if (this.groups.find(group => group.name === groupName)) {
+ this.createGroupError = t('contacts', 'This group already exists')
+ return
+ }
+
+ this.createGroupError = null
+
+ console.debug('Created new local group', groupName)
this.$store.dispatch('addGroup', groupName)
this.isNewGroupMenuOpen = false
@@ -623,13 +674,21 @@ export default {
name: 'contact',
params: {
selectedGroup: groupName,
- selectedContact: undefined,
},
})
},
// Bulk contacts group management handlers
addContactsToGroup(group) {
+ // Get the full group if we provided the group name only
+ if (typeof group === 'string') {
+ group = this.groups.find(a => a.name === group)
+ if (!group) {
+ console.error('Cannot add contact to an undefined group', group)
+ return
+ }
+ }
+
this.showContactPicker = true
this.contactPickerforGroup = group
},