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:
parent
32fce96503
commit
5c3de5e095
13 changed files with 72 additions and 69 deletions
|
@ -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
|
||||
|
|
|
@ -286,6 +286,7 @@ export default {
|
|||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$store.dispatch('labels/get');
|
||||
this.$store.dispatch('inboxes/get');
|
||||
},
|
||||
methods: {
|
||||
|
|
|
@ -108,8 +108,7 @@
|
|||
"Phone Number",
|
||||
"Conversations",
|
||||
"Last Contacted"
|
||||
],
|
||||
"EDIT_BUTTON": "Edit"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
||||
}
|
||||
}
|
||||
}
|
|
@ -121,7 +121,7 @@
|
|||
"SIDEBAR": {
|
||||
"CONVERSATIONS": "Conversations",
|
||||
"REPORTS": "Reports",
|
||||
"CONTACTS": "Contacts",
|
||||
"CONTACTS": "Contacts (Beta)",
|
||||
"SETTINGS": "Settings",
|
||||
"HOME": "Home",
|
||||
"AGENTS": "Agents",
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
:placeholder="$t('CONTACTS_PAGE.SEARCH_INPUT_PLACEHOLDER')"
|
||||
class="contact-search"
|
||||
:value="searchQuery"
|
||||
@keyup.enter="onSearchSubmit"
|
||||
@input="onInputSearch"
|
||||
/>
|
||||
<woot-submit-button
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -52,6 +52,7 @@
|
|||
{{ $t('EDIT_CONTACT.BUTTON_LABEL') }}
|
||||
</woot-button>
|
||||
<edit-contact
|
||||
v-if="showEditModal"
|
||||
:show="showEditModal"
|
||||
:contact="contact"
|
||||
@cancel="toggleEditModal"
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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?
|
||||
|
|
Loading…
Reference in a new issue