fix: Enhance CRM UI (#1397)

* feat: Sort by name
* feat: Fetch labels from sidebar
* Remove unused language file
* Add beta tag to contacts
* Add timeMixin, reduce font-size
* Remove unused methods
* Remove unused prop
* Disabled footer if no contacts or invalid page
* Add keyup for input
* Fix conversation not loading if there are no active conversations
* return last_seen_at as unix time
* Fix contact edit modal
* Add loader for edit contact button
* Fix review comments
This commit is contained in:
Pranav Raj S
2020-11-11 16:02:14 +05:30
committed by GitHub
parent 32fce96503
commit 5c3de5e095
13 changed files with 72 additions and 69 deletions

View File

@@ -7,16 +7,14 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
before_action :fetch_contact, only: [:show, :update] before_action :fetch_contact, only: [:show, :update]
def index def index
contacts = Current.account.contacts.where.not(email: [nil, '']).or(Current.account.contacts.where.not(phone_number: [nil, ''])) @contacts_count = resolved_contacts.count
@contacts_count = contacts.count @contacts = fetch_contact_last_seen_at(resolved_contacts)
@contacts = fetch_contact_last_seen_at(contacts)
end end
def search def search
render json: { error: 'Specify search string with parameter q' }, status: :unprocessable_entity if params[:q].blank? && return render json: { error: 'Specify search string with parameter q' }, status: :unprocessable_entity if params[:q].blank? && return
contacts = Current.account.contacts.where.not(email: [nil, '']).or(Current.account.contacts.where.not(phone_number: [nil, ''])) contacts = resolved_contacts.where('name LIKE :search OR email LIKE :search', search: "%#{params[:q]}%")
.where('name LIKE :search OR email LIKE :search', search: "%#{params[:q]}%")
@contacts_count = contacts.count @contacts_count = contacts.count
@contacts = fetch_contact_last_seen_at(contacts) @contacts = fetch_contact_last_seen_at(contacts)
end end
@@ -53,6 +51,13 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
private private
def resolved_contacts
@resolved_contacts ||= Current.account.contacts
.where.not(email: [nil, ''])
.or(Current.account.contacts.where.not(phone_number: [nil, '']))
.order('LOWER(name)')
end
def set_current_page def set_current_page
@current_page = params[:page] || 1 @current_page = params[:page] || 1
end end

View File

@@ -286,6 +286,7 @@ export default {
}, },
}, },
mounted() { mounted() {
this.$store.dispatch('labels/get');
this.$store.dispatch('inboxes/get'); this.$store.dispatch('inboxes/get');
}, },
methods: { methods: {

View File

@@ -108,8 +108,7 @@
"Phone Number", "Phone Number",
"Conversations", "Conversations",
"Last Contacted" "Last Contacted"
], ]
"EDIT_BUTTON": "Edit"
} }
} }
} }

View File

@@ -1,21 +0,0 @@
{
"CONTACTS_PAGE": {
"HEADER": "Contacts",
"SEARCH_BUTTON": "Search",
"SEARCH_INPUT_PLACEHOLDER": "Search for contacts",
"LIST": {
"404": "There are no canned responses available in this account.",
"TITLE": "Manage canned responses",
"DESC": "Canned Responses are predefined reply templates which can be used to quickly send out replies to tickets.",
"TABLE_HEADER": [
"Name",
"Phone Number",
"Conversations",
"Last Contacted"
],
"EDIT_BUTTON": "Edit",
"VIEW_BUTTON": "View"
}
}
}

View File

@@ -121,7 +121,7 @@
"SIDEBAR": { "SIDEBAR": {
"CONVERSATIONS": "Conversations", "CONVERSATIONS": "Conversations",
"REPORTS": "Reports", "REPORTS": "Reports",
"CONTACTS": "Contacts", "CONTACTS": "Contacts (Beta)",
"SETTINGS": "Settings", "SETTINGS": "Settings",
"HOME": "Home", "HOME": "Home",
"AGENTS": "Agents", "AGENTS": "Agents",

View File

@@ -29,17 +29,21 @@
{{ contactItem.name }} {{ contactItem.name }}
</h4> </h4>
<p class="user-email"> <p class="user-email">
{{ contactItem.email || '--' }} {{ contactItem.email || '---' }}
</p> </p>
</div> </div>
</div> </div>
</td> </td>
<td>{{ contactItem.phone_number || '--' }}</td> <td>{{ contactItem.phone_number || '---' }}</td>
<td class="conversation-count-item"> <td class="conversation-count-item">
{{ contactItem.conversations_count }} {{ contactItem.conversations_count }}
</td> </td>
<td> <td>
{{ contactItem.last_contacted_at || '--' }} {{
contactItem.last_seen_at
? dynamicTime(contactItem.last_seen_at)
: '---'
}}
</td> </td>
</tr> </tr>
</tbody> </tbody>
@@ -60,6 +64,7 @@ import { mixin as clickaway } from 'vue-clickaway';
import Spinner from 'shared/components/Spinner.vue'; import Spinner from 'shared/components/Spinner.vue';
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue'; import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
import EmptyState from 'dashboard/components/widgets/EmptyState.vue'; import EmptyState from 'dashboard/components/widgets/EmptyState.vue';
import timeMixin from 'dashboard/mixins/time';
export default { export default {
components: { components: {
@@ -67,7 +72,7 @@ export default {
EmptyState, EmptyState,
Spinner, Spinner,
}, },
mixins: [clickaway], mixins: [clickaway, timeMixin],
props: { props: {
contacts: { contacts: {
type: Array, type: Array,
@@ -77,10 +82,6 @@ export default {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
openEditModal: {
type: Function,
default: () => {},
},
onClickContact: { onClickContact: {
type: Function, type: Function,
default: () => {}, default: () => {},
@@ -90,7 +91,7 @@ export default {
default: false, default: false,
}, },
activeContactId: { activeContactId: {
type: String, type: [String, Number],
default: '', default: '',
}, },
}, },
@@ -134,6 +135,8 @@ export default {
} }
.contacts-table { .contacts-table {
margin-top: -1px;
> thead { > thead {
border-bottom: 1px solid var(--color-border); border-bottom: 1px solid var(--color-border);
background: white; background: white;
@@ -178,8 +181,9 @@ export default {
} }
.user-name { .user-name {
text-transform: capitalize; font-size: var(--font-size-small);
margin: 0; margin: 0;
text-transform: capitalize;
} }
.user-email { .user-email {

View File

@@ -4,12 +4,12 @@
<contacts-header <contacts-header
:search-query="searchQuery" :search-query="searchQuery"
:on-search-submit="onSearchSubmit" :on-search-submit="onSearchSubmit"
this-selected-contact-id=""
:on-input-search="onInputSearch" :on-input-search="onInputSearch"
/> />
<contacts-table <contacts-table
:contacts="records" :contacts="records"
:show-search-empty-state="showEmptySearchResult" :show-search-empty-state="showEmptySearchResult"
:open-edit-modal="openEditModal"
:is-loading="uiFlags.isFetching" :is-loading="uiFlags.isFetching"
:on-click-contact="openContactInfoPanel" :on-click-contact="openContactInfoPanel"
:active-contact-id="selectedContactId" :active-contact-id="selectedContactId"
@@ -19,11 +19,6 @@
:current-page="Number(meta.currentPage)" :current-page="Number(meta.currentPage)"
:total-count="meta.count" :total-count="meta.count"
/> />
<edit-contact
:show="showEditModal"
:contact="selectedContact"
@cancel="closeEditModal"
/>
</div> </div>
<contact-info-panel <contact-info-panel
v-if="showContactViewPane" v-if="showContactViewPane"
@@ -36,8 +31,6 @@
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import EditContact from 'dashboard/routes/dashboard/conversation/contact/EditContact';
import ContactsHeader from './Header'; import ContactsHeader from './Header';
import ContactsTable from './ContactsTable'; import ContactsTable from './ContactsTable';
import ContactInfoPanel from './ContactInfoPanel'; import ContactInfoPanel from './ContactInfoPanel';
@@ -48,7 +41,6 @@ export default {
ContactsHeader, ContactsHeader,
ContactsTable, ContactsTable,
ContactsFooter, ContactsFooter,
EditContact,
ContactInfoPanel, ContactInfoPanel,
}, },
data() { data() {
@@ -83,9 +75,15 @@ export default {
wrapClas() { wrapClas() {
return this.showContactViewPane ? 'medium-9' : 'medium-12'; return this.showContactViewPane ? 'medium-9' : 'medium-12';
}, },
pageParameter() {
const selectedPageNumber = Number(this.$route.query?.page);
return !Number.isNaN(selectedPageNumber) && selectedPageNumber >= 1
? selectedPageNumber
: 1;
},
}, },
mounted() { mounted() {
this.$store.dispatch('contacts/get', { page: 1 }); this.$store.dispatch('contacts/get', { page: this.pageParameter });
}, },
methods: { methods: {
onInputSearch(event) { onInputSearch(event) {
@@ -98,12 +96,15 @@ export default {
this.searchQuery = event.target.value; this.searchQuery = event.target.value;
}, },
onSearchSubmit() { onSearchSubmit() {
this.selectedContactId = '';
this.$store.dispatch('contacts/search', { this.$store.dispatch('contacts/search', {
search: this.searchQuery, search: this.searchQuery,
page: 1, page: 1,
}); });
}, },
onPageChange(page) { onPageChange(page) {
this.selectedContactId = '';
window.history.pushState({}, null, `${this.$route.path}?page=${page}`);
if (this.searchQuery) { if (this.searchQuery) {
this.$store.dispatch('contacts/search', { this.$store.dispatch('contacts/search', {
search: this.searchQuery, search: this.searchQuery,
@@ -121,14 +122,6 @@ export default {
this.selectedContactId = ''; this.selectedContactId = '';
this.showContactInfoPanelPane = false; this.showContactInfoPanelPane = false;
}, },
openEditModal(contactId) {
this.selectedContactId = contactId;
this.showEditModal = true;
},
closeEditModal() {
this.selectedContactId = '';
this.showEditModal = false;
},
}, },
}; };
</script> </script>

View File

@@ -1,5 +1,5 @@
<template> <template>
<footer class="footer"> <footer v-if="isFooterVisible" class="footer">
<div class="left-aligned-wrap"> <div class="left-aligned-wrap">
<div class="page-meta"> <div class="page-meta">
<strong>{{ firstIndex }}</strong> <strong>{{ firstIndex }}</strong>
@@ -60,7 +60,7 @@ export default {
}, },
pageSize: { pageSize: {
type: Number, type: Number,
default: 25, default: 15,
}, },
totalCount: { totalCount: {
type: Number, type: Number,
@@ -72,6 +72,9 @@ export default {
}, },
}, },
computed: { computed: {
isFooterVisible() {
return this.totalCount && !(this.firstIndex > this.totalCount);
},
firstIndex() { firstIndex() {
const firstIndex = this.pageSize * (this.currentPage - 1) + 1; const firstIndex = this.pageSize * (this.currentPage - 1) + 1;
return firstIndex; return firstIndex;
@@ -163,7 +166,7 @@ export default {
.button { .button {
background: transparent; background: transparent;
border-color: var(--b-400); border-color: var(--color-border);
color: var(--color-body); color: var(--color-body);
margin-bottom: 0; margin-bottom: 0;
margin-left: -2px; margin-left: -2px;
@@ -173,23 +176,27 @@ export default {
&:hover, &:hover,
&:focus, &:focus,
&:active { &:active {
background: var(--b-400); background: var(--s-200);
color: white; color: white;
} }
&:first-child { &:first-child {
border-top-left-radius: 3px; border-top-left-radius: var(--space-smaller);
border-bottom-left-radius: 3px; border-bottom-left-radius: var(--space-smaller);
} }
&:last-child { &:last-child {
border-top-right-radius: 3px; border-top-right-radius: var(--space-smaller);
border-bottom-right-radius: 3px; border-bottom-right-radius: var(--space-smaller);
} }
&.small { &.small {
font-size: var(--font-size-micro); font-size: var(--font-size-micro);
} }
&.disabled { &.disabled {
background: var(--b-300); background: var(--s-200);
border-color: var(--s-200);
color: var(--b-900); color: var(--b-900);
} }

View File

@@ -14,6 +14,7 @@
:placeholder="$t('CONTACTS_PAGE.SEARCH_INPUT_PLACEHOLDER')" :placeholder="$t('CONTACTS_PAGE.SEARCH_INPUT_PLACEHOLDER')"
class="contact-search" class="contact-search"
:value="searchQuery" :value="searchQuery"
@keyup.enter="onSearchSubmit"
@input="onInputSearch" @input="onInputSearch"
/> />
<woot-submit-button <woot-submit-button

View File

@@ -82,10 +82,11 @@ export default {
}, },
mounted() { mounted() {
this.$store.dispatch('labels/get');
this.$store.dispatch('agents/get'); this.$store.dispatch('agents/get');
this.initialize(); this.initialize();
this.fetchConversation();
this.$watch('$store.state.route', () => this.initialize()); this.$watch('$store.state.route', () => this.initialize());
this.$watch('chatList.length', () => { this.$watch('chatList.length', () => {
this.fetchConversation(); this.fetchConversation();

View File

@@ -52,6 +52,7 @@
{{ $t('EDIT_CONTACT.BUTTON_LABEL') }} {{ $t('EDIT_CONTACT.BUTTON_LABEL') }}
</woot-button> </woot-button>
<edit-contact <edit-contact
v-if="showEditModal"
:show="showEditModal" :show="showEditModal"
:contact="contact" :contact="contact"
@cancel="toggleEditModal" @cancel="toggleEditModal"

View File

@@ -82,7 +82,10 @@
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<div class="medium-12 columns"> <div class="medium-12 columns">
<woot-submit-button :button-text="$t('EDIT_CONTACT.FORM.SUBMIT')" /> <woot-submit-button
:loading="uiFlags.isUpdating"
:button-text="$t('EDIT_CONTACT.FORM.SUBMIT')"
/>
<button class="button clear" @click.prevent="onCancel"> <button class="button clear" @click.prevent="onCancel">
{{ $t('EDIT_CONTACT.FORM.CANCEL') }} {{ $t('EDIT_CONTACT.FORM.CANCEL') }}
</button> </button>
@@ -97,6 +100,7 @@
import alertMixin from 'shared/mixins/alertMixin'; import alertMixin from 'shared/mixins/alertMixin';
import { DuplicateContactException } from 'shared/helpers/CustomErrors'; import { DuplicateContactException } from 'shared/helpers/CustomErrors';
import { required } from 'vuelidate/lib/validators'; import { required } from 'vuelidate/lib/validators';
import { mapGetters } from 'vuex';
export default { export default {
mixins: [alertMixin], mixins: [alertMixin],
@@ -143,11 +147,19 @@ export default {
location: {}, location: {},
bio: {}, bio: {},
}, },
computed: {
...mapGetters({
uiFlags: 'contacts/getUIFlags',
}),
},
watch: { watch: {
contact() { contact() {
this.setContactObject(); this.setContactObject();
}, },
}, },
mounted() {
this.setContactObject();
},
methods: { methods: {
onCancel() { onCancel() {
this.$emit('cancel'); this.$emit('cancel');

View File

@@ -7,7 +7,7 @@ json.phone_number resource.phone_number
json.thumbnail resource.avatar_url json.thumbnail resource.avatar_url
json.custom_attributes resource.custom_attributes json.custom_attributes resource.custom_attributes
json.conversations_count resource.conversations_count if resource[:conversations_count].present? json.conversations_count resource.conversations_count if resource[:conversations_count].present?
json.last_seen_at resource.last_seen_at if resource[:last_seen_at].present? json.last_seen_at resource.last_seen_at.to_i if resource[:last_seen_at].present?
# we only want to output contact inbox when its /contacts endpoints # we only want to output contact inbox when its /contacts endpoints
if defined?(with_contact_inboxes) && with_contact_inboxes.present? if defined?(with_contact_inboxes) && with_contact_inboxes.present?