diff options
author | John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com> | 2020-07-24 10:31:16 +0200 |
---|---|---|
committer | John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com> | 2020-08-21 09:56:08 +0200 |
commit | 68cb27a5b4ef0eecd7a0af597d3050bc709c5754 (patch) | |
tree | cd0354dfcc98d7e7528b7460f10c668bf46100d6 /src | |
parent | 8439848c3da189a0cd3cac7c2aa149d0a207b00b (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.vue | 2 | ||||
-rw-r--r-- | src/components/EntityPicker/EntityPicker.vue | 63 | ||||
-rw-r--r-- | src/components/EntityPicker/EntitySearchResult.vue | 110 | ||||
-rw-r--r-- | src/components/ProcessingScreen.vue | 11 | ||||
-rw-r--r-- | src/views/Contacts.vue | 77 |
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 }, |