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
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
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]
def index
contacts = Current.account.contacts.where.not(email: [nil, '']).or(Current.account.contacts.where.not(phone_number: [nil, '']))
@contacts_count = contacts.count
@contacts = fetch_contact_last_seen_at(contacts)
@contacts_count = resolved_contacts.count
@contacts = fetch_contact_last_seen_at(resolved_contacts)
end
def search
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, '']))
.where('name LIKE :search OR email LIKE :search', search: "%#{params[:q]}%")
contacts = resolved_contacts.where('name LIKE :search OR email LIKE :search', search: "%#{params[:q]}%")
@contacts_count = contacts.count
@contacts = fetch_contact_last_seen_at(contacts)
end
@ -53,6 +51,13 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
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
@current_page = params[:page] || 1
end

View file

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

View file

@ -108,8 +108,7 @@
"Phone Number",
"Conversations",
"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": {
"CONVERSATIONS": "Conversations",
"REPORTS": "Reports",
"CONTACTS": "Contacts",
"CONTACTS": "Contacts (Beta)",
"SETTINGS": "Settings",
"HOME": "Home",
"AGENTS": "Agents",

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -7,7 +7,7 @@ json.phone_number resource.phone_number
json.thumbnail resource.avatar_url
json.custom_attributes resource.custom_attributes
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
if defined?(with_contact_inboxes) && with_contact_inboxes.present?