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:
@@ -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
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -108,8 +108,7 @@
|
|||||||
"Phone Number",
|
"Phone Number",
|
||||||
"Conversations",
|
"Conversations",
|
||||||
"Last Contacted"
|
"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": {
|
"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",
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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?
|
||||||
|
|||||||
Reference in New Issue
Block a user