feat: Add Contacts page (#1335)
Co-authored-by: Sojan <sojan@pepalo.com> Co-authored-by: Pranav Raj Sreepuram <pranavrajs@gmail.com>
This commit is contained in:
parent
2babfd6148
commit
f214c9c47c
41 changed files with 1163 additions and 179 deletions
|
@ -64,6 +64,6 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def agents
|
def agents
|
||||||
@agents ||= Current.account.users.order_by_full_name
|
@agents ||= Current.account.users.order_by_full_name.includes({ avatar_attachment: [:blob] })
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,17 +1,32 @@
|
||||||
class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
||||||
|
RESULTS_PER_PAGE = 15
|
||||||
protect_from_forgery with: :null_session
|
protect_from_forgery with: :null_session
|
||||||
|
|
||||||
before_action :check_authorization
|
before_action :check_authorization
|
||||||
|
before_action :set_current_page, only: [:index, :active, :search]
|
||||||
before_action :fetch_contact, only: [:show, :update]
|
before_action :fetch_contact, only: [:show, :update]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@contacts = Current.account.contacts
|
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)
|
||||||
|
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_count = contacts.count
|
||||||
|
@contacts = fetch_contact_last_seen_at(contacts)
|
||||||
end
|
end
|
||||||
|
|
||||||
# returns online contacts
|
# returns online contacts
|
||||||
def active
|
def active
|
||||||
@contacts = Current.account.contacts.where(id: ::OnlineStatusTracker
|
contacts = Current.account.contacts.where(id: ::OnlineStatusTracker
|
||||||
.get_available_contact_ids(Current.account.id))
|
.get_available_contact_ids(Current.account.id))
|
||||||
|
@contacts_count = contacts.count
|
||||||
|
@contacts = contacts.page(@current_page)
|
||||||
end
|
end
|
||||||
|
|
||||||
def show; end
|
def show; end
|
||||||
|
@ -36,13 +51,19 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
||||||
}, status: :unprocessable_entity
|
}, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
def search
|
private
|
||||||
render json: { error: 'Specify search string with parameter q' }, status: :unprocessable_entity if params[:q].blank? && return
|
|
||||||
|
|
||||||
@contacts = Current.account.contacts.where('name LIKE :search OR email LIKE :search', search: "%#{params[:q]}%")
|
def set_current_page
|
||||||
|
@current_page = params[:page] || 1
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
def fetch_contact_last_seen_at(contacts)
|
||||||
|
contacts.left_outer_joins(:conversations)
|
||||||
|
.select('contacts.*, COUNT(conversations.id) as conversations_count, MAX(conversations.contact_last_seen_at) as last_seen_at')
|
||||||
|
.group('contacts.id')
|
||||||
|
.includes([{ avatar_attachment: [:blob] }, { contact_inboxes: [:inbox] }])
|
||||||
|
.page(@current_page).per(RESULTS_PER_PAGE)
|
||||||
|
end
|
||||||
|
|
||||||
def build_contact_inbox
|
def build_contact_inbox
|
||||||
return if params[:inbox_id].blank?
|
return if params[:inbox_id].blank?
|
||||||
|
|
|
@ -4,7 +4,7 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
||||||
before_action :check_authorization
|
before_action :check_authorization
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@inboxes = policy_scope(Current.account.inboxes.order_by_name.includes(:channel, :avatar_attachment))
|
@inboxes = policy_scope(Current.account.inboxes.order_by_name.includes(:channel, { avatar_attachment: [:blob] }))
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
|
|
|
@ -62,9 +62,7 @@ class ConversationFinder
|
||||||
end
|
end
|
||||||
|
|
||||||
def find_all_conversations
|
def find_all_conversations
|
||||||
@conversations = current_account.conversations.includes(
|
@conversations = current_account.conversations.where(inbox_id: @inbox_ids)
|
||||||
:assignee, :inbox, :taggings, contact: [:avatar_attachment]
|
|
||||||
).where(inbox_id: @inbox_ids)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def filter_by_assignee_type
|
def filter_by_assignee_type
|
||||||
|
@ -106,6 +104,9 @@ class ConversationFinder
|
||||||
end
|
end
|
||||||
|
|
||||||
def conversations
|
def conversations
|
||||||
|
@conversations = @conversations.includes(
|
||||||
|
:taggings, :inbox, { assignee: { avatar_attachment: [:blob] } }, { contact: { avatar_attachment: [:blob] } }
|
||||||
|
)
|
||||||
current_page ? @conversations.latest.page(current_page) : @conversations.latest
|
current_page ? @conversations.latest.page(current_page) : @conversations.latest
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -11,7 +11,7 @@ class MessageFinder
|
||||||
private
|
private
|
||||||
|
|
||||||
def conversation_messages
|
def conversation_messages
|
||||||
@conversation.messages.includes(:attachments, :sender)
|
@conversation.messages.includes(:attachments, :sender, sender: { avatar_attachment: [:blob] })
|
||||||
end
|
end
|
||||||
|
|
||||||
def messages
|
def messages
|
||||||
|
|
|
@ -6,9 +6,17 @@ class ContactAPI extends ApiClient {
|
||||||
super('contacts', { accountScoped: true });
|
super('contacts', { accountScoped: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get(page) {
|
||||||
|
return axios.get(`${this.url}?page=${page}`);
|
||||||
|
}
|
||||||
|
|
||||||
getConversations(contactId) {
|
getConversations(contactId) {
|
||||||
return axios.get(`${this.url}/${contactId}/conversations`);
|
return axios.get(`${this.url}/${contactId}/conversations`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
search(search = '', page = 1) {
|
||||||
|
return axios.get(`${this.url}/search?q=${search}&page=${page}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new ContactAPI();
|
export default new ContactAPI();
|
||||||
|
|
|
@ -26,3 +26,4 @@
|
||||||
@import 'views/signup';
|
@import 'views/signup';
|
||||||
|
|
||||||
@import 'plugins/multiselect';
|
@import 'plugins/multiselect';
|
||||||
|
@import 'plugins/dropdown';
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
@import 'shared/assets/stylesheets/colors';
|
@import 'shared/assets/stylesheets/colors';
|
||||||
@import 'shared/assets/stylesheets/spacing';
|
@import 'shared/assets/stylesheets/spacing';
|
||||||
@import 'shared/assets/stylesheets/font-size';
|
@import 'shared/assets/stylesheets/font-size';
|
||||||
|
@import 'shared/assets/stylesheets/font-weights';
|
||||||
@import 'variables';
|
@import 'variables';
|
||||||
|
|
||||||
@import '~spinkit/scss/spinners/7-three-bounce';
|
@import '~spinkit/scss/spinners/7-three-bounce';
|
||||||
|
|
27
app/javascript/dashboard/assets/scss/plugins/_dropdown.scss
Normal file
27
app/javascript/dashboard/assets/scss/plugins/_dropdown.scss
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
.dropdown-pane.sleek {
|
||||||
|
@include elegant-card;
|
||||||
|
@include border-light;
|
||||||
|
padding-left: 0;
|
||||||
|
padding-right: 0;
|
||||||
|
right: -12px;
|
||||||
|
top: 48px;
|
||||||
|
width: auto;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
@include arrow(top, var(--color-border-light), 14px);
|
||||||
|
position: absolute;
|
||||||
|
right: 6px;
|
||||||
|
top: -14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
@include arrow(top, $color-white, var(--space-slab));
|
||||||
|
position: absolute;
|
||||||
|
right: var(--space-small);
|
||||||
|
top: -12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown>li>a:hover {
|
||||||
|
background: var(--color-background);
|
||||||
|
}
|
||||||
|
}
|
|
@ -30,7 +30,7 @@
|
||||||
<availability-status />
|
<availability-status />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bottom-nav">
|
<div class="bottom-nav app-context-menu">
|
||||||
<transition name="menu-slide">
|
<transition name="menu-slide">
|
||||||
<div
|
<div
|
||||||
v-if="showOptionsMenu"
|
v-if="showOptionsMenu"
|
||||||
|
@ -405,4 +405,7 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.app-context-menu {
|
||||||
|
height: 6rem;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="row empty-state">
|
<div class="row empty-state">
|
||||||
<h3 class="title">{{title}}</h3>
|
<h3 class="title">{{ title }}</h3>
|
||||||
<p class="message">{{message}}</p>
|
<p class="message">{{ message }}</p>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
title: String,
|
title: String,
|
||||||
|
|
|
@ -7,6 +7,7 @@ export const getSidebarItems = accountId => ({
|
||||||
'inbox_dashboard',
|
'inbox_dashboard',
|
||||||
'inbox_conversation',
|
'inbox_conversation',
|
||||||
'conversation_through_inbox',
|
'conversation_through_inbox',
|
||||||
|
'contacts_dashboard',
|
||||||
'settings_account_reports',
|
'settings_account_reports',
|
||||||
'profile_settings',
|
'profile_settings',
|
||||||
'profile_settings_index',
|
'profile_settings_index',
|
||||||
|
@ -23,6 +24,13 @@ export const getSidebarItems = accountId => ({
|
||||||
toolTip: 'Conversation from all subscribed inboxes',
|
toolTip: 'Conversation from all subscribed inboxes',
|
||||||
toStateName: 'home',
|
toStateName: 'home',
|
||||||
},
|
},
|
||||||
|
contacts: {
|
||||||
|
icon: 'ion-person-stalker',
|
||||||
|
label: 'CONTACTS',
|
||||||
|
hasSubMenu: false,
|
||||||
|
toState: frontendURL(`accounts/${accountId}/contacts`),
|
||||||
|
toStateName: 'contacts_dashboard',
|
||||||
|
},
|
||||||
report: {
|
report: {
|
||||||
icon: 'ion-arrow-graph-up-right',
|
icon: 'ion-arrow-graph-up-right',
|
||||||
label: 'REPORTS',
|
label: 'REPORTS',
|
||||||
|
|
|
@ -95,5 +95,21 @@
|
||||||
"SUCCESS_MESSAGE": "Updated contact successfully",
|
"SUCCESS_MESSAGE": "Updated contact successfully",
|
||||||
"CONTACT_ALREADY_EXIST": "This email address is in use for another contact.",
|
"CONTACT_ALREADY_EXIST": "This email address is in use for another contact.",
|
||||||
"ERROR_MESSAGE": "There was an error updating the contact, please try again"
|
"ERROR_MESSAGE": "There was an error updating the contact, please try again"
|
||||||
|
},
|
||||||
|
"CONTACTS_PAGE": {
|
||||||
|
"HEADER": "Contacts",
|
||||||
|
"SEARCH_BUTTON": "Search",
|
||||||
|
"SEARCH_INPUT_PLACEHOLDER": "Search for contacts",
|
||||||
|
"LIST": {
|
||||||
|
"LOADING_MESSAGE": "Loading contacts...",
|
||||||
|
"404": "No contacts matches your search 🔍",
|
||||||
|
"TABLE_HEADER": [
|
||||||
|
"Name",
|
||||||
|
"Phone Number",
|
||||||
|
"Conversations",
|
||||||
|
"Last Contacted"
|
||||||
|
],
|
||||||
|
"EDIT_BUTTON": "Edit"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
21
app/javascript/dashboard/i18n/locale/en/contactsPage.json
Normal file
21
app/javascript/dashboard/i18n/locale/en/contactsPage.json
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -57,8 +57,7 @@
|
||||||
},
|
},
|
||||||
"AVAILABILITY": {
|
"AVAILABILITY": {
|
||||||
"LABEL": "Availability",
|
"LABEL": "Availability",
|
||||||
"STATUSES_LIST": [
|
"STATUSES_LIST": [{
|
||||||
{
|
|
||||||
"value": "online",
|
"value": "online",
|
||||||
"label": "Online"
|
"label": "Online"
|
||||||
},
|
},
|
||||||
|
@ -122,6 +121,7 @@
|
||||||
"SIDEBAR": {
|
"SIDEBAR": {
|
||||||
"CONVERSATIONS": "Conversations",
|
"CONVERSATIONS": "Conversations",
|
||||||
"REPORTS": "Reports",
|
"REPORTS": "Reports",
|
||||||
|
"CONTACTS": "Contacts",
|
||||||
"SETTINGS": "Settings",
|
"SETTINGS": "Settings",
|
||||||
"HOME": "Home",
|
"HOME": "Home",
|
||||||
"AGENTS": "Agents",
|
"AGENTS": "Agents",
|
||||||
|
|
|
@ -8,7 +8,6 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
/* global bus */
|
|
||||||
import Sidebar from '../../components/layout/Sidebar';
|
import Sidebar from '../../components/layout/Sidebar';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
|
@ -0,0 +1,97 @@
|
||||||
|
<template>
|
||||||
|
<div class="medium-3 bg-white contact--panel">
|
||||||
|
<span class="close-button" @click="onClose">
|
||||||
|
<i class="ion-android-close close-icon" />
|
||||||
|
</span>
|
||||||
|
<contact-info :contact="contact" />
|
||||||
|
<contact-custom-attributes
|
||||||
|
v-if="hasContactAttributes"
|
||||||
|
:custom-attributes="contact.custom_attributes"
|
||||||
|
/>
|
||||||
|
<contact-conversations
|
||||||
|
v-if="contact.id"
|
||||||
|
:contact-id="contact.id"
|
||||||
|
conversation-id=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import ContactConversations from 'dashboard/routes/dashboard/conversation/ContactConversations';
|
||||||
|
import ContactInfo from 'dashboard/routes/dashboard/conversation/contact/ContactInfo';
|
||||||
|
import ContactCustomAttributes from 'dashboard/routes/dashboard/conversation/ContactCustomAttributes';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
ContactCustomAttributes,
|
||||||
|
ContactConversations,
|
||||||
|
ContactInfo,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
contact: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
onClose: {
|
||||||
|
type: Function,
|
||||||
|
default: () => {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
hasContactAttributes() {
|
||||||
|
const { custom_attributes: customAttributes } = this.contact;
|
||||||
|
return customAttributes && Object.keys(customAttributes).length;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import '~dashboard/assets/scss/variables';
|
||||||
|
@import '~dashboard/assets/scss/mixins';
|
||||||
|
|
||||||
|
.contact--panel {
|
||||||
|
@include border-normal-left;
|
||||||
|
|
||||||
|
background: white;
|
||||||
|
font-size: var(--font-size-small);
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow: auto;
|
||||||
|
position: relative;
|
||||||
|
padding: var(--space-one);
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-button {
|
||||||
|
position: absolute;
|
||||||
|
right: var(--space-normal);
|
||||||
|
top: var(--space-slab);
|
||||||
|
font-size: var(--font-size-big);
|
||||||
|
color: var(--color-heading);
|
||||||
|
|
||||||
|
.close-icon {
|
||||||
|
margin-right: var(--space-smaller);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversation--details {
|
||||||
|
border-top: 1px solid $color-border-light;
|
||||||
|
padding: var(--space-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-conversation--panel {
|
||||||
|
border-top: 1px solid $color-border-light;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact--mute {
|
||||||
|
color: var(--r-400);
|
||||||
|
display: block;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact--actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,198 @@
|
||||||
|
<template>
|
||||||
|
<section class="contacts-table-wrap">
|
||||||
|
<table class="woot-table contacts-table">
|
||||||
|
<thead>
|
||||||
|
<th
|
||||||
|
v-for="thHeader in $t('CONTACTS_PAGE.LIST.TABLE_HEADER')"
|
||||||
|
:key="thHeader"
|
||||||
|
>
|
||||||
|
{{ thHeader }}
|
||||||
|
</th>
|
||||||
|
</thead>
|
||||||
|
<tbody v-show="showTableData">
|
||||||
|
<tr
|
||||||
|
v-for="contactItem in contacts"
|
||||||
|
:key="contactItem.id"
|
||||||
|
:class="{ 'is-active': contactItem.id === activeContactId }"
|
||||||
|
@click="() => onClickContact(contactItem.id)"
|
||||||
|
>
|
||||||
|
<td>
|
||||||
|
<div class="row-main-info">
|
||||||
|
<thumbnail
|
||||||
|
:src="contactItem.thumbnail"
|
||||||
|
size="36px"
|
||||||
|
:username="contactItem.name"
|
||||||
|
:status="contactItem.availability_status"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<h4 class="sub-block-title user-name">
|
||||||
|
{{ contactItem.name }}
|
||||||
|
</h4>
|
||||||
|
<p class="user-email">
|
||||||
|
{{ contactItem.email || '--' }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>{{ contactItem.phone_number || '--' }}</td>
|
||||||
|
<td class="conversation-count-item">
|
||||||
|
{{ contactItem.conversations_count }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ contactItem.last_contacted_at || '--' }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<empty-state
|
||||||
|
v-if="showSearchEmptyState"
|
||||||
|
:title="$t('CONTACTS_PAGE.LIST.404')"
|
||||||
|
/>
|
||||||
|
<div v-if="isLoading" class="contacts--loader">
|
||||||
|
<spinner />
|
||||||
|
<span>{{ $t('CONTACTS_PAGE.LIST.LOADING_MESSAGE') }}</span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
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';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Thumbnail,
|
||||||
|
EmptyState,
|
||||||
|
Spinner,
|
||||||
|
},
|
||||||
|
mixins: [clickaway],
|
||||||
|
props: {
|
||||||
|
contacts: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
showSearchEmptyState: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
openEditModal: {
|
||||||
|
type: Function,
|
||||||
|
default: () => {},
|
||||||
|
},
|
||||||
|
onClickContact: {
|
||||||
|
type: Function,
|
||||||
|
default: () => {},
|
||||||
|
},
|
||||||
|
isLoading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
activeContactId: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
currentRoute() {
|
||||||
|
return ' ';
|
||||||
|
},
|
||||||
|
sidebarClassName() {
|
||||||
|
if (this.isOnDesktop) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
if (this.isSidebarOpen) {
|
||||||
|
return 'off-canvas is-open ';
|
||||||
|
}
|
||||||
|
return 'off-canvas position-left is-transition-push is-closed';
|
||||||
|
},
|
||||||
|
contentClassName() {
|
||||||
|
if (this.isOnDesktop) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
if (this.isSidebarOpen) {
|
||||||
|
return 'off-canvas-content is-open-left has-transition-push has-position-left';
|
||||||
|
}
|
||||||
|
return 'off-canvas-content';
|
||||||
|
},
|
||||||
|
showTableData() {
|
||||||
|
return !this.showSearchEmptyState && !this.isLoading;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import '~dashboard/assets/scss/mixins';
|
||||||
|
|
||||||
|
.contacts-table-wrap {
|
||||||
|
@include scroll-on-hover;
|
||||||
|
background: var(--color-background-light);
|
||||||
|
flex: 1 1;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contacts-table {
|
||||||
|
> thead {
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
background: white;
|
||||||
|
|
||||||
|
> th:first-child {
|
||||||
|
padding-left: var(--space-medium);
|
||||||
|
width: 30%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> tbody {
|
||||||
|
> tr {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--b-50);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-active {
|
||||||
|
background: var(--b-100);
|
||||||
|
}
|
||||||
|
|
||||||
|
> td {
|
||||||
|
padding: var(--space-slab);
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
padding-left: var(--space-medium);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.conversation-count-item {
|
||||||
|
padding-left: var(--space-medium);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.row-main-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.user-thumbnail-box {
|
||||||
|
margin-right: var(--space-small);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-name {
|
||||||
|
text-transform: capitalize;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-email {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.contacts--loader {
|
||||||
|
font-size: var(--font-size-default);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: var(--space-big);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,146 @@
|
||||||
|
<template>
|
||||||
|
<div class="contacts-page row">
|
||||||
|
<div class="left-wrap" :class="wrapClas">
|
||||||
|
<contacts-header
|
||||||
|
:search-query="searchQuery"
|
||||||
|
:on-search-submit="onSearchSubmit"
|
||||||
|
: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"
|
||||||
|
/>
|
||||||
|
<contacts-footer
|
||||||
|
:on-page-change="onPageChange"
|
||||||
|
:current-page="Number(meta.currentPage)"
|
||||||
|
:total-count="meta.count"
|
||||||
|
/>
|
||||||
|
<edit-contact
|
||||||
|
:show="showEditModal"
|
||||||
|
:contact="selectedContact"
|
||||||
|
@cancel="closeEditModal"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<contact-info-panel
|
||||||
|
v-if="showContactViewPane"
|
||||||
|
:contact="selectedContact"
|
||||||
|
:on-close="closeContactInfoPanel"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<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';
|
||||||
|
import ContactsFooter from './Footer';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
ContactsHeader,
|
||||||
|
ContactsTable,
|
||||||
|
ContactsFooter,
|
||||||
|
EditContact,
|
||||||
|
ContactInfoPanel,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
searchQuery: '',
|
||||||
|
showEditModal: false,
|
||||||
|
selectedContactId: '',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters({
|
||||||
|
records: 'contacts/getContacts',
|
||||||
|
uiFlags: 'contacts/getUIFlags',
|
||||||
|
meta: 'contacts/getMeta',
|
||||||
|
}),
|
||||||
|
showEmptySearchResult() {
|
||||||
|
const hasEmptyResults = !!this.searchQuery && this.records.length === 0;
|
||||||
|
return hasEmptyResults;
|
||||||
|
},
|
||||||
|
selectedContact() {
|
||||||
|
if (this.selectedContactId) {
|
||||||
|
const contact = this.records.find(
|
||||||
|
item => this.selectedContactId === item.id
|
||||||
|
);
|
||||||
|
return contact;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
showContactViewPane() {
|
||||||
|
return this.selectedContactId !== '';
|
||||||
|
},
|
||||||
|
wrapClas() {
|
||||||
|
return this.showContactViewPane ? 'medium-9' : 'medium-12';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$store.dispatch('contacts/get', { page: 1 });
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onInputSearch(event) {
|
||||||
|
const newQuery = event.target.value;
|
||||||
|
const refetchAllContacts = !!this.searchQuery && newQuery === '';
|
||||||
|
|
||||||
|
if (refetchAllContacts) {
|
||||||
|
this.$store.dispatch('contacts/get', { page: 1 });
|
||||||
|
}
|
||||||
|
this.searchQuery = event.target.value;
|
||||||
|
},
|
||||||
|
onSearchSubmit() {
|
||||||
|
this.$store.dispatch('contacts/search', {
|
||||||
|
search: this.searchQuery,
|
||||||
|
page: 1,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onPageChange(page) {
|
||||||
|
if (this.searchQuery) {
|
||||||
|
this.$store.dispatch('contacts/search', {
|
||||||
|
search: this.searchQuery,
|
||||||
|
page,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.$store.dispatch('contacts/get', { page });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
openContactInfoPanel(contactId) {
|
||||||
|
this.selectedContactId = contactId;
|
||||||
|
this.showContactInfoPanelPane = true;
|
||||||
|
},
|
||||||
|
closeContactInfoPanel() {
|
||||||
|
this.selectedContactId = '';
|
||||||
|
this.showContactInfoPanelPane = false;
|
||||||
|
},
|
||||||
|
openEditModal(contactId) {
|
||||||
|
this.selectedContactId = contactId;
|
||||||
|
this.showEditModal = true;
|
||||||
|
},
|
||||||
|
closeEditModal() {
|
||||||
|
this.selectedContactId = '';
|
||||||
|
this.showEditModal = false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.contacts-page {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.left-wrap {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding-top: var(--space-normal);
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,204 @@
|
||||||
|
<template>
|
||||||
|
<footer class="footer">
|
||||||
|
<div class="left-aligned-wrap">
|
||||||
|
<div class="page-meta">
|
||||||
|
<strong>{{ firstIndex }}</strong>
|
||||||
|
- <strong>{{ lastIndex }}</strong> of
|
||||||
|
<strong>{{ totalCount }}</strong> items
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="right-aligned-wrap">
|
||||||
|
<div
|
||||||
|
v-if="totalCount"
|
||||||
|
class="primary button-group pagination-button-group"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="button small goto-first"
|
||||||
|
:class="firstPageButtonClass"
|
||||||
|
@click="onFirstPage"
|
||||||
|
>
|
||||||
|
<i class="ion-chevron-left" />
|
||||||
|
<i class="ion-chevron-left" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="button small"
|
||||||
|
:class="prevPageButtonClass"
|
||||||
|
@click="onPrevPage"
|
||||||
|
>
|
||||||
|
<i class="ion-chevron-left" />
|
||||||
|
</button>
|
||||||
|
<button class="button" @click.prevent>
|
||||||
|
{{ currentPage }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="button small"
|
||||||
|
:class="nextPageButtonClass"
|
||||||
|
@click="onNextPage"
|
||||||
|
>
|
||||||
|
<i class="ion-chevron-right" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="button small goto-last"
|
||||||
|
:class="lastPageButtonClass"
|
||||||
|
@click="onLastPage"
|
||||||
|
>
|
||||||
|
<i class="ion-chevron-right" />
|
||||||
|
<i class="ion-chevron-right" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
components: {},
|
||||||
|
props: {
|
||||||
|
currentPage: {
|
||||||
|
type: Number,
|
||||||
|
default: 1,
|
||||||
|
},
|
||||||
|
pageSize: {
|
||||||
|
type: Number,
|
||||||
|
default: 25,
|
||||||
|
},
|
||||||
|
totalCount: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
onPageChange: {
|
||||||
|
type: Function,
|
||||||
|
default: () => {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
firstIndex() {
|
||||||
|
const firstIndex = this.pageSize * (this.currentPage - 1) + 1;
|
||||||
|
return firstIndex;
|
||||||
|
},
|
||||||
|
lastIndex() {
|
||||||
|
const index = Math.min(this.totalCount, this.pageSize * this.currentPage);
|
||||||
|
return index;
|
||||||
|
},
|
||||||
|
searchButtonClass() {
|
||||||
|
return this.searchQuery !== '' ? 'show' : '';
|
||||||
|
},
|
||||||
|
hasLastPage() {
|
||||||
|
const isDisabled =
|
||||||
|
this.currentPage === Math.ceil(this.totalCount / this.pageSize);
|
||||||
|
return isDisabled;
|
||||||
|
},
|
||||||
|
lastPageButtonClass() {
|
||||||
|
const className = this.hasLastPage ? 'disabled' : '';
|
||||||
|
return className;
|
||||||
|
},
|
||||||
|
hasFirstPage() {
|
||||||
|
const isDisabled = this.currentPage === 1;
|
||||||
|
return isDisabled;
|
||||||
|
},
|
||||||
|
firstPageButtonClass() {
|
||||||
|
const className = this.hasFirstPage ? 'disabled' : '';
|
||||||
|
return className;
|
||||||
|
},
|
||||||
|
hasNextPage() {
|
||||||
|
const isDisabled =
|
||||||
|
this.currentPage === Math.ceil(this.totalCount / this.pageSize);
|
||||||
|
return isDisabled;
|
||||||
|
},
|
||||||
|
nextPageButtonClass() {
|
||||||
|
const className = this.hasNextPage ? 'disabled' : '';
|
||||||
|
return className;
|
||||||
|
},
|
||||||
|
hasPrevPage() {
|
||||||
|
const isDisabled = this.currentPage === 1;
|
||||||
|
return isDisabled;
|
||||||
|
},
|
||||||
|
prevPageButtonClass() {
|
||||||
|
const className = this.hasPrevPage ? 'disabled' : '';
|
||||||
|
return className;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onNextPage() {
|
||||||
|
if (this.hasNextPage) return;
|
||||||
|
const newPage = this.currentPage + 1;
|
||||||
|
this.onPageChange(newPage);
|
||||||
|
},
|
||||||
|
onPrevPage() {
|
||||||
|
if (this.hasPrevPage) return;
|
||||||
|
|
||||||
|
const newPage = this.currentPage - 1;
|
||||||
|
this.onPageChange(newPage);
|
||||||
|
},
|
||||||
|
onFirstPage() {
|
||||||
|
if (this.hasFirstPage) return;
|
||||||
|
|
||||||
|
const newPage = 1;
|
||||||
|
this.onPageChange(newPage);
|
||||||
|
},
|
||||||
|
onLastPage() {
|
||||||
|
if (this.hasLastPage) return;
|
||||||
|
|
||||||
|
const newPage = Math.ceil(this.totalCount / this.pageSize);
|
||||||
|
this.onPageChange(newPage);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.footer {
|
||||||
|
height: 60px;
|
||||||
|
border-top: 1px solid var(--color-border);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 var(--space-normal);
|
||||||
|
}
|
||||||
|
.page-meta {
|
||||||
|
font-size: var(--font-size-mini);
|
||||||
|
}
|
||||||
|
.pagination-button-group {
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
.button {
|
||||||
|
background: transparent;
|
||||||
|
border-color: var(--b-400);
|
||||||
|
color: var(--color-body);
|
||||||
|
margin-bottom: 0;
|
||||||
|
margin-left: -2px;
|
||||||
|
font-size: var(--font-size-small);
|
||||||
|
padding: var(--space-small) var(--space-normal);
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus,
|
||||||
|
&:active {
|
||||||
|
background: var(--b-400);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
border-top-left-radius: 3px;
|
||||||
|
border-bottom-left-radius: 3px;
|
||||||
|
}
|
||||||
|
&:last-child {
|
||||||
|
border-top-right-radius: 3px;
|
||||||
|
border-bottom-right-radius: 3px;
|
||||||
|
}
|
||||||
|
&.small {
|
||||||
|
font-size: var(--font-size-micro);
|
||||||
|
}
|
||||||
|
&.disabled {
|
||||||
|
background: var(--b-300);
|
||||||
|
color: var(--b-900);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.goto-first,
|
||||||
|
&.goto-last {
|
||||||
|
i:last-child {
|
||||||
|
margin-left: var(--space-minus-smaller);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,114 @@
|
||||||
|
<template>
|
||||||
|
<header class="header">
|
||||||
|
<div class="table-actions-wrap">
|
||||||
|
<div class="left-aligned-wrap">
|
||||||
|
<h1 class="page-title">
|
||||||
|
{{ $t('CONTACTS_PAGE.HEADER') }}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div class="right-aligned-wrap">
|
||||||
|
<div class="search-wrap">
|
||||||
|
<i class="ion-ios-search-strong search-icon" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
:placeholder="$t('CONTACTS_PAGE.SEARCH_INPUT_PLACEHOLDER')"
|
||||||
|
class="contact-search"
|
||||||
|
:value="searchQuery"
|
||||||
|
@input="onInputSearch"
|
||||||
|
/>
|
||||||
|
<woot-submit-button
|
||||||
|
:button-text="$t('CONTACTS_PAGE.SEARCH_BUTTON')"
|
||||||
|
:loading="false"
|
||||||
|
:button-class="searchButtonClass"
|
||||||
|
@click="onSearchSubmit"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
components: {},
|
||||||
|
props: {
|
||||||
|
searchQuery: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
onInputSearch: {
|
||||||
|
type: Function,
|
||||||
|
default: () => {},
|
||||||
|
},
|
||||||
|
onSearchSubmit: {
|
||||||
|
type: Function,
|
||||||
|
default: () => {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
searchButtonClass() {
|
||||||
|
return this.searchQuery !== '' ? 'show' : '';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
/* TODO-REM; Change variables sizing to rem after html font size change from 1.0 t0 1.6 */
|
||||||
|
|
||||||
|
.header {
|
||||||
|
padding: 0 var(--space-medium);
|
||||||
|
}
|
||||||
|
.page-title {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.table-actions-wrap {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: var(--space-slab);
|
||||||
|
}
|
||||||
|
.search-wrap {
|
||||||
|
width: 400px;
|
||||||
|
height: 3.6rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.search-icon {
|
||||||
|
position: absolute;
|
||||||
|
top: 1px;
|
||||||
|
left: var(--space-one);
|
||||||
|
height: 3.6rem;
|
||||||
|
line-height: 3.6rem;
|
||||||
|
font-size: var(--font-size-medium);
|
||||||
|
color: var(--b-700);
|
||||||
|
}
|
||||||
|
.contact-search {
|
||||||
|
margin: 0;
|
||||||
|
height: 3.6rem;
|
||||||
|
width: 100%;
|
||||||
|
padding-left: var(--space-large);
|
||||||
|
padding-right: 6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
margin-left: var(--space-small);
|
||||||
|
height: 3.2rem;
|
||||||
|
top: var(--space-micro);
|
||||||
|
right: var(--space-micro);
|
||||||
|
position: absolute;
|
||||||
|
padding: 0 var(--space-small);
|
||||||
|
transition: transform 100ms linear;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-1px);
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.show {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
12
app/javascript/dashboard/routes/dashboard/contacts/routes.js
Normal file
12
app/javascript/dashboard/routes/dashboard/contacts/routes.js
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
/* eslint arrow-body-style: 0 */
|
||||||
|
import ContactsView from './components/ContactsView';
|
||||||
|
import { frontendURL } from '../../../helper/URLHelper';
|
||||||
|
|
||||||
|
export const routes = [
|
||||||
|
{
|
||||||
|
path: frontendURL('accounts/:accountId/contacts'),
|
||||||
|
name: 'contacts_dashboard',
|
||||||
|
roles: ['administrator', 'agent'],
|
||||||
|
component: ContactsView,
|
||||||
|
},
|
||||||
|
];
|
|
@ -45,6 +45,7 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
|
|
||||||
import ContactConversations from './ContactConversations.vue';
|
import ContactConversations from './ContactConversations.vue';
|
||||||
import ContactDetailsItem from './ContactDetailsItem.vue';
|
import ContactDetailsItem from './ContactDetailsItem.vue';
|
||||||
import ContactInfo from './contact/ContactInfo';
|
import ContactInfo from './contact/ContactInfo';
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import AppContainer from './Dashboard';
|
import AppContainer from './Dashboard';
|
||||||
import settings from './settings/settings.routes';
|
import settings from './settings/settings.routes';
|
||||||
import conversation from './conversation/conversation.routes';
|
import conversation from './conversation/conversation.routes';
|
||||||
|
import { routes as contactRoutes } from './contacts/routes';
|
||||||
import { frontendURL } from '../../helper/URLHelper';
|
import { frontendURL } from '../../helper/URLHelper';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -8,7 +9,7 @@ export default {
|
||||||
{
|
{
|
||||||
path: frontendURL('accounts/:account_id'),
|
path: frontendURL('accounts/:account_id'),
|
||||||
component: AppContainer,
|
component: AppContainer,
|
||||||
children: [...conversation.routes, ...settings.routes],
|
children: [...conversation.routes, ...settings.routes, ...contactRoutes],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,126 +0,0 @@
|
||||||
import { DuplicateContactException } from 'shared/helpers/CustomErrors';
|
|
||||||
import * as types from '../mutation-types';
|
|
||||||
import ContactAPI from '../../api/contacts';
|
|
||||||
import Vue from 'vue';
|
|
||||||
|
|
||||||
const state = {
|
|
||||||
records: {},
|
|
||||||
uiFlags: {
|
|
||||||
isFetching: false,
|
|
||||||
isFetchingItem: false,
|
|
||||||
isUpdating: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getters = {
|
|
||||||
getContacts($state) {
|
|
||||||
return Object.values($state.records);
|
|
||||||
},
|
|
||||||
getUIFlags($state) {
|
|
||||||
return $state.uiFlags;
|
|
||||||
},
|
|
||||||
getContact: $state => id => {
|
|
||||||
const contact = $state.records[id];
|
|
||||||
return contact || {};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const actions = {
|
|
||||||
get: async ({ commit }) => {
|
|
||||||
commit(types.default.SET_CONTACT_UI_FLAG, { isFetching: true });
|
|
||||||
try {
|
|
||||||
const response = await ContactAPI.get();
|
|
||||||
commit(types.default.SET_CONTACTS, response.data.payload);
|
|
||||||
commit(types.default.SET_CONTACT_UI_FLAG, { isFetching: false });
|
|
||||||
} catch (error) {
|
|
||||||
commit(types.default.SET_CONTACT_UI_FLAG, { isFetching: false });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
show: async ({ commit }, { id }) => {
|
|
||||||
commit(types.default.SET_CONTACT_UI_FLAG, { isFetchingItem: true });
|
|
||||||
try {
|
|
||||||
const response = await ContactAPI.show(id);
|
|
||||||
commit(types.default.SET_CONTACT_ITEM, response.data.payload);
|
|
||||||
commit(types.default.SET_CONTACT_UI_FLAG, { isFetchingItem: false });
|
|
||||||
} catch (error) {
|
|
||||||
commit(types.default.SET_CONTACT_UI_FLAG, { isFetchingItem: false });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
update: async ({ commit }, { id, ...updateObj }) => {
|
|
||||||
commit(types.default.SET_CONTACT_UI_FLAG, { isUpdating: true });
|
|
||||||
try {
|
|
||||||
const response = await ContactAPI.update(id, updateObj);
|
|
||||||
commit(types.default.EDIT_CONTACT, response.data.payload);
|
|
||||||
commit(types.default.SET_CONTACT_UI_FLAG, { isUpdating: false });
|
|
||||||
} catch (error) {
|
|
||||||
commit(types.default.SET_CONTACT_UI_FLAG, { isUpdating: false });
|
|
||||||
if (error.response?.data?.contact) {
|
|
||||||
throw new DuplicateContactException(error.response.data.contact);
|
|
||||||
} else {
|
|
||||||
throw new Error(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
updatePresence: ({ commit }, data) => {
|
|
||||||
commit(types.default.UPDATE_CONTACTS_PRESENCE, data);
|
|
||||||
},
|
|
||||||
|
|
||||||
setContact({ commit }, data) {
|
|
||||||
commit(types.default.SET_CONTACT_ITEM, data);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const mutations = {
|
|
||||||
[types.default.SET_CONTACT_UI_FLAG]($state, data) {
|
|
||||||
$state.uiFlags = {
|
|
||||||
...$state.uiFlags,
|
|
||||||
...data,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
[types.default.SET_CONTACTS]: ($state, data) => {
|
|
||||||
data.forEach(contact => {
|
|
||||||
Vue.set($state.records, contact.id, {
|
|
||||||
...($state.records[contact.id] || {}),
|
|
||||||
...contact,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
[types.default.SET_CONTACT_ITEM]: ($state, data) => {
|
|
||||||
Vue.set($state.records, data.id, {
|
|
||||||
...($state.records[data.id] || {}),
|
|
||||||
...data,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
[types.default.EDIT_CONTACT]: ($state, data) => {
|
|
||||||
Vue.set($state.records, data.id, data);
|
|
||||||
},
|
|
||||||
|
|
||||||
[types.default.UPDATE_CONTACTS_PRESENCE]: ($state, data) => {
|
|
||||||
Object.values($state.records).forEach(element => {
|
|
||||||
const availabilityStatus = data[element.id];
|
|
||||||
if (availabilityStatus) {
|
|
||||||
Vue.set(
|
|
||||||
$state.records[element.id],
|
|
||||||
'availability_status',
|
|
||||||
availabilityStatus
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
Vue.delete($state.records[element.id], 'availability_status');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
|
||||||
namespaced: true,
|
|
||||||
state,
|
|
||||||
getters,
|
|
||||||
actions,
|
|
||||||
mutations,
|
|
||||||
};
|
|
74
app/javascript/dashboard/store/modules/contacts/actions.js
Normal file
74
app/javascript/dashboard/store/modules/contacts/actions.js
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
import { DuplicateContactException } from 'shared/helpers/CustomErrors';
|
||||||
|
import types from '../../mutation-types';
|
||||||
|
import ContactAPI from '../../../api/contacts';
|
||||||
|
|
||||||
|
export const actions = {
|
||||||
|
search: async ({ commit }, { search, page }) => {
|
||||||
|
commit(types.SET_CONTACT_UI_FLAG, { isFetching: true });
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
data: { payload, meta },
|
||||||
|
} = await ContactAPI.search(search, page);
|
||||||
|
commit(types.CLEAR_CONTACTS);
|
||||||
|
commit(types.SET_CONTACTS, payload);
|
||||||
|
commit(types.SET_CONTACT_META, meta);
|
||||||
|
commit(types.SET_CONTACT_UI_FLAG, { isFetching: false });
|
||||||
|
} catch (error) {
|
||||||
|
commit(types.SET_CONTACT_UI_FLAG, { isFetching: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
get: async ({ commit }, { page = 1 } = {}) => {
|
||||||
|
commit(types.SET_CONTACT_UI_FLAG, { isFetching: true });
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
data: { payload, meta },
|
||||||
|
} = await ContactAPI.get(page);
|
||||||
|
commit(types.CLEAR_CONTACTS);
|
||||||
|
commit(types.SET_CONTACTS, payload);
|
||||||
|
commit(types.SET_CONTACT_META, meta);
|
||||||
|
commit(types.SET_CONTACT_UI_FLAG, { isFetching: false });
|
||||||
|
} catch (error) {
|
||||||
|
commit(types.SET_CONTACT_UI_FLAG, { isFetching: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
show: async ({ commit }, { id }) => {
|
||||||
|
commit(types.SET_CONTACT_UI_FLAG, { isFetchingItem: true });
|
||||||
|
try {
|
||||||
|
const response = await ContactAPI.show(id);
|
||||||
|
commit(types.SET_CONTACT_ITEM, response.data.payload);
|
||||||
|
commit(types.SET_CONTACT_UI_FLAG, {
|
||||||
|
isFetchingItem: false,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
commit(types.SET_CONTACT_UI_FLAG, {
|
||||||
|
isFetchingItem: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
update: async ({ commit }, { id, ...updateObj }) => {
|
||||||
|
commit(types.SET_CONTACT_UI_FLAG, { isUpdating: true });
|
||||||
|
try {
|
||||||
|
const response = await ContactAPI.update(id, updateObj);
|
||||||
|
commit(types.EDIT_CONTACT, response.data.payload);
|
||||||
|
commit(types.SET_CONTACT_UI_FLAG, { isUpdating: false });
|
||||||
|
} catch (error) {
|
||||||
|
commit(types.SET_CONTACT_UI_FLAG, { isUpdating: false });
|
||||||
|
if (error.response?.data?.contact) {
|
||||||
|
throw new DuplicateContactException(error.response.data.contact);
|
||||||
|
} else {
|
||||||
|
throw new Error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
updatePresence: ({ commit }, data) => {
|
||||||
|
commit(types.UPDATE_CONTACTS_PRESENCE, data);
|
||||||
|
},
|
||||||
|
|
||||||
|
setContact({ commit }, data) {
|
||||||
|
commit(types.SET_CONTACT_ITEM, data);
|
||||||
|
},
|
||||||
|
};
|
15
app/javascript/dashboard/store/modules/contacts/getters.js
Normal file
15
app/javascript/dashboard/store/modules/contacts/getters.js
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
export const getters = {
|
||||||
|
getContacts($state) {
|
||||||
|
return Object.values($state.records);
|
||||||
|
},
|
||||||
|
getUIFlags($state) {
|
||||||
|
return $state.uiFlags;
|
||||||
|
},
|
||||||
|
getContact: $state => id => {
|
||||||
|
const contact = $state.records[id];
|
||||||
|
return contact || {};
|
||||||
|
},
|
||||||
|
getMeta: $state => {
|
||||||
|
return $state.meta;
|
||||||
|
},
|
||||||
|
};
|
24
app/javascript/dashboard/store/modules/contacts/index.js
Normal file
24
app/javascript/dashboard/store/modules/contacts/index.js
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import { getters } from './getters';
|
||||||
|
import { actions } from './actions';
|
||||||
|
import { mutations } from './mutations';
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
meta: {
|
||||||
|
count: 0,
|
||||||
|
currentPage: 1,
|
||||||
|
},
|
||||||
|
records: {},
|
||||||
|
uiFlags: {
|
||||||
|
isFetching: false,
|
||||||
|
isFetchingItem: false,
|
||||||
|
isUpdating: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
namespaced: true,
|
||||||
|
state,
|
||||||
|
getters,
|
||||||
|
actions,
|
||||||
|
mutations,
|
||||||
|
};
|
56
app/javascript/dashboard/store/modules/contacts/mutations.js
Normal file
56
app/javascript/dashboard/store/modules/contacts/mutations.js
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
import Vue from 'vue';
|
||||||
|
import types from '../../mutation-types';
|
||||||
|
|
||||||
|
export const mutations = {
|
||||||
|
[types.SET_CONTACT_UI_FLAG]($state, data) {
|
||||||
|
$state.uiFlags = {
|
||||||
|
...$state.uiFlags,
|
||||||
|
...data,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
[types.CLEAR_CONTACTS]: $state => {
|
||||||
|
Vue.set($state, 'records', {});
|
||||||
|
},
|
||||||
|
|
||||||
|
[types.SET_CONTACT_META]: ($state, data) => {
|
||||||
|
const { count, current_page: currentPage } = data;
|
||||||
|
Vue.set($state.meta, 'count', count);
|
||||||
|
Vue.set($state.meta, 'currentPage', currentPage);
|
||||||
|
},
|
||||||
|
|
||||||
|
[types.SET_CONTACTS]: ($state, data) => {
|
||||||
|
data.forEach(contact => {
|
||||||
|
Vue.set($state.records, contact.id, {
|
||||||
|
...($state.records[contact.id] || {}),
|
||||||
|
...contact,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
[types.SET_CONTACT_ITEM]: ($state, data) => {
|
||||||
|
Vue.set($state.records, data.id, {
|
||||||
|
...($state.records[data.id] || {}),
|
||||||
|
...data,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
[types.EDIT_CONTACT]: ($state, data) => {
|
||||||
|
Vue.set($state.records, data.id, data);
|
||||||
|
},
|
||||||
|
|
||||||
|
[types.UPDATE_CONTACTS_PRESENCE]: ($state, data) => {
|
||||||
|
Object.values($state.records).forEach(element => {
|
||||||
|
const availabilityStatus = data[element.id];
|
||||||
|
if (availabilityStatus) {
|
||||||
|
Vue.set(
|
||||||
|
$state.records[element.id],
|
||||||
|
'availability_status',
|
||||||
|
availabilityStatus
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
Vue.delete($state.records[element.id], 'availability_status');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
|
@ -1,9 +1,11 @@
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { actions } from '../../contacts';
|
import Contacts from '../../contacts';
|
||||||
import * as types from '../../../mutation-types';
|
import types from '../../../mutation-types';
|
||||||
import contactList from './fixtures';
|
import contactList from './fixtures';
|
||||||
import { DuplicateContactException } from '../../../../../shared/helpers/CustomErrors';
|
import { DuplicateContactException } from '../../../../../shared/helpers/CustomErrors';
|
||||||
|
|
||||||
|
const { actions } = Contacts;
|
||||||
|
|
||||||
const commit = jest.fn();
|
const commit = jest.fn();
|
||||||
global.axios = axios;
|
global.axios = axios;
|
||||||
jest.mock('axios');
|
jest.mock('axios');
|
||||||
|
@ -11,20 +13,24 @@ jest.mock('axios');
|
||||||
describe('#actions', () => {
|
describe('#actions', () => {
|
||||||
describe('#get', () => {
|
describe('#get', () => {
|
||||||
it('sends correct mutations if API is success', async () => {
|
it('sends correct mutations if API is success', async () => {
|
||||||
axios.get.mockResolvedValue({ data: { payload: contactList } });
|
axios.get.mockResolvedValue({
|
||||||
|
data: { payload: contactList, meta: { count: 100, current_page: 1 } },
|
||||||
|
});
|
||||||
await actions.get({ commit });
|
await actions.get({ commit });
|
||||||
expect(commit.mock.calls).toEqual([
|
expect(commit.mock.calls).toEqual([
|
||||||
[types.default.SET_CONTACT_UI_FLAG, { isFetching: true }],
|
[types.SET_CONTACT_UI_FLAG, { isFetching: true }],
|
||||||
[types.default.SET_CONTACTS, contactList],
|
[types.CLEAR_CONTACTS],
|
||||||
[types.default.SET_CONTACT_UI_FLAG, { isFetching: false }],
|
[types.SET_CONTACTS, contactList],
|
||||||
|
[types.SET_CONTACT_META, { count: 100, current_page: 1 }],
|
||||||
|
[types.SET_CONTACT_UI_FLAG, { isFetching: false }],
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
it('sends correct mutations if API is error', async () => {
|
it('sends correct mutations if API is error', async () => {
|
||||||
axios.get.mockRejectedValue({ message: 'Incorrect header' });
|
axios.get.mockRejectedValue({ message: 'Incorrect header' });
|
||||||
await actions.get({ commit });
|
await actions.get({ commit });
|
||||||
expect(commit.mock.calls).toEqual([
|
expect(commit.mock.calls).toEqual([
|
||||||
[types.default.SET_CONTACT_UI_FLAG, { isFetching: true }],
|
[types.SET_CONTACT_UI_FLAG, { isFetching: true }],
|
||||||
[types.default.SET_CONTACT_UI_FLAG, { isFetching: false }],
|
[types.SET_CONTACT_UI_FLAG, { isFetching: false }],
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -34,17 +40,17 @@ describe('#actions', () => {
|
||||||
axios.get.mockResolvedValue({ data: { payload: contactList[0] } });
|
axios.get.mockResolvedValue({ data: { payload: contactList[0] } });
|
||||||
await actions.show({ commit }, { id: contactList[0].id });
|
await actions.show({ commit }, { id: contactList[0].id });
|
||||||
expect(commit.mock.calls).toEqual([
|
expect(commit.mock.calls).toEqual([
|
||||||
[types.default.SET_CONTACT_UI_FLAG, { isFetchingItem: true }],
|
[types.SET_CONTACT_UI_FLAG, { isFetchingItem: true }],
|
||||||
[types.default.SET_CONTACT_ITEM, contactList[0]],
|
[types.SET_CONTACT_ITEM, contactList[0]],
|
||||||
[types.default.SET_CONTACT_UI_FLAG, { isFetchingItem: false }],
|
[types.SET_CONTACT_UI_FLAG, { isFetchingItem: false }],
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
it('sends correct mutations if API is error', async () => {
|
it('sends correct mutations if API is error', async () => {
|
||||||
axios.get.mockRejectedValue({ message: 'Incorrect header' });
|
axios.get.mockRejectedValue({ message: 'Incorrect header' });
|
||||||
await actions.show({ commit }, { id: contactList[0].id });
|
await actions.show({ commit }, { id: contactList[0].id });
|
||||||
expect(commit.mock.calls).toEqual([
|
expect(commit.mock.calls).toEqual([
|
||||||
[types.default.SET_CONTACT_UI_FLAG, { isFetchingItem: true }],
|
[types.SET_CONTACT_UI_FLAG, { isFetchingItem: true }],
|
||||||
[types.default.SET_CONTACT_UI_FLAG, { isFetchingItem: false }],
|
[types.SET_CONTACT_UI_FLAG, { isFetchingItem: false }],
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -54,9 +60,9 @@ describe('#actions', () => {
|
||||||
axios.patch.mockResolvedValue({ data: { payload: contactList[0] } });
|
axios.patch.mockResolvedValue({ data: { payload: contactList[0] } });
|
||||||
await actions.update({ commit }, contactList[0]);
|
await actions.update({ commit }, contactList[0]);
|
||||||
expect(commit.mock.calls).toEqual([
|
expect(commit.mock.calls).toEqual([
|
||||||
[types.default.SET_CONTACT_UI_FLAG, { isUpdating: true }],
|
[types.SET_CONTACT_UI_FLAG, { isUpdating: true }],
|
||||||
[types.default.EDIT_CONTACT, contactList[0]],
|
[types.EDIT_CONTACT, contactList[0]],
|
||||||
[types.default.SET_CONTACT_UI_FLAG, { isUpdating: false }],
|
[types.SET_CONTACT_UI_FLAG, { isUpdating: false }],
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
it('sends correct actions if API is error', async () => {
|
it('sends correct actions if API is error', async () => {
|
||||||
|
@ -65,8 +71,8 @@ describe('#actions', () => {
|
||||||
Error
|
Error
|
||||||
);
|
);
|
||||||
expect(commit.mock.calls).toEqual([
|
expect(commit.mock.calls).toEqual([
|
||||||
[types.default.SET_CONTACT_UI_FLAG, { isUpdating: true }],
|
[types.SET_CONTACT_UI_FLAG, { isUpdating: true }],
|
||||||
[types.default.SET_CONTACT_UI_FLAG, { isUpdating: false }],
|
[types.SET_CONTACT_UI_FLAG, { isUpdating: false }],
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -83,8 +89,8 @@ describe('#actions', () => {
|
||||||
DuplicateContactException
|
DuplicateContactException
|
||||||
);
|
);
|
||||||
expect(commit.mock.calls).toEqual([
|
expect(commit.mock.calls).toEqual([
|
||||||
[types.default.SET_CONTACT_UI_FLAG, { isUpdating: true }],
|
[types.SET_CONTACT_UI_FLAG, { isUpdating: true }],
|
||||||
[types.default.SET_CONTACT_UI_FLAG, { isUpdating: false }],
|
[types.SET_CONTACT_UI_FLAG, { isUpdating: false }],
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -93,9 +99,7 @@ describe('#actions', () => {
|
||||||
it('returns correct mutations', () => {
|
it('returns correct mutations', () => {
|
||||||
const data = { id: 1, name: 'john doe', availability_status: 'online' };
|
const data = { id: 1, name: 'john doe', availability_status: 'online' };
|
||||||
actions.setContact({ commit }, data);
|
actions.setContact({ commit }, data);
|
||||||
expect(commit.mock.calls).toEqual([
|
expect(commit.mock.calls).toEqual([[types.SET_CONTACT_ITEM, data]]);
|
||||||
[types.default.SET_CONTACT_ITEM, data],
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import { getters } from '../../contacts';
|
import Contacts from '../../contacts';
|
||||||
import contactList from './fixtures';
|
import contactList from './fixtures';
|
||||||
|
|
||||||
|
const { getters } = Contacts;
|
||||||
|
|
||||||
describe('#getters', () => {
|
describe('#getters', () => {
|
||||||
it('getContacts', () => {
|
it('getContacts', () => {
|
||||||
const state = {
|
const state = {
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
import * as types from '../../../mutation-types';
|
import types from '../../../mutation-types';
|
||||||
import { mutations } from '../../contacts';
|
import Contacts from '../../contacts';
|
||||||
|
const { mutations } = Contacts;
|
||||||
|
|
||||||
describe('#mutations', () => {
|
describe('#mutations', () => {
|
||||||
describe('#SET_CONTACTS', () => {
|
describe('#SET_CONTACTS', () => {
|
||||||
it('set contact records', () => {
|
it('set contact records', () => {
|
||||||
const state = { records: {} };
|
const state = { records: {} };
|
||||||
mutations[types.default.SET_CONTACTS](state, [
|
mutations[types.SET_CONTACTS](state, [
|
||||||
{ id: 1, name: 'contact1', email: 'contact1@chatwoot.com' },
|
{ id: 1, name: 'contact1', email: 'contact1@chatwoot.com' },
|
||||||
]);
|
]);
|
||||||
expect(state.records).toEqual({
|
expect(state.records).toEqual({
|
||||||
|
@ -25,7 +26,7 @@ describe('#mutations', () => {
|
||||||
1: { id: 1, name: 'contact1', email: 'contact1@chatwoot.com' },
|
1: { id: 1, name: 'contact1', email: 'contact1@chatwoot.com' },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
mutations[types.default.SET_CONTACT_ITEM](state, {
|
mutations[types.SET_CONTACT_ITEM](state, {
|
||||||
id: 2,
|
id: 2,
|
||||||
name: 'contact2',
|
name: 'contact2',
|
||||||
email: 'contact2@chatwoot.com',
|
email: 'contact2@chatwoot.com',
|
||||||
|
@ -44,7 +45,7 @@ describe('#mutations', () => {
|
||||||
1: { id: 1, name: 'contact1', email: 'contact1@chatwoot.com' },
|
1: { id: 1, name: 'contact1', email: 'contact1@chatwoot.com' },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
mutations[types.default.EDIT_CONTACT](state, {
|
mutations[types.EDIT_CONTACT](state, {
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'contact2',
|
name: 'contact2',
|
||||||
email: 'contact2@chatwoot.com',
|
email: 'contact2@chatwoot.com',
|
||||||
|
|
|
@ -88,9 +88,11 @@ export default {
|
||||||
DELETE_WEBHOOK: 'DELETE_WEBHOOK',
|
DELETE_WEBHOOK: 'DELETE_WEBHOOK',
|
||||||
|
|
||||||
// Contacts
|
// Contacts
|
||||||
|
SET_CONTACT_META: 'SET_CONTACT_META',
|
||||||
SET_CONTACT_UI_FLAG: 'SET_CONTACT_UI_FLAG',
|
SET_CONTACT_UI_FLAG: 'SET_CONTACT_UI_FLAG',
|
||||||
SET_CONTACT_ITEM: 'SET_CONTACT_ITEM',
|
SET_CONTACT_ITEM: 'SET_CONTACT_ITEM',
|
||||||
SET_CONTACTS: 'SET_CONTACTS',
|
SET_CONTACTS: 'SET_CONTACTS',
|
||||||
|
CLEAR_CONTACTS: 'CLEAR_CONTACTS',
|
||||||
EDIT_CONTACT: 'EDIT_CONTACT',
|
EDIT_CONTACT: 'EDIT_CONTACT',
|
||||||
UPDATE_CONTACTS_PRESENCE: 'UPDATE_CONTACTS_PRESENCE',
|
UPDATE_CONTACTS_PRESENCE: 'UPDATE_CONTACTS_PRESENCE',
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
:root {
|
||||||
|
--font-weight-feather: 100;
|
||||||
|
--font-weight-light: 300;
|
||||||
|
--font-weight-normal: 400;
|
||||||
|
--font-weight-medium: 500;
|
||||||
|
--font-weight-bold: 600;
|
||||||
|
--font-weight-black: 700;
|
||||||
|
}
|
|
@ -13,4 +13,17 @@
|
||||||
--space-larger: 4.8rem;
|
--space-larger: 4.8rem;
|
||||||
--space-jumbo: 6.4rem;
|
--space-jumbo: 6.4rem;
|
||||||
--space-mega: 10.0rem;
|
--space-mega: 10.0rem;
|
||||||
|
|
||||||
|
--space-minus-micro: -0.2rem;
|
||||||
|
--space-minus-smaller: -0.4rem;
|
||||||
|
--space-minus-small: -0.8rem;
|
||||||
|
--space-minus-one: -1rem;
|
||||||
|
--space-minus-slab: -1.2rem;
|
||||||
|
--space-minus-normal: -1.6rem;
|
||||||
|
--space-minus-two: -2.0rem;
|
||||||
|
--space-minus-medium: -2.4rem;
|
||||||
|
--space-minus-large: -3.2rem;
|
||||||
|
--space-minus-larger: -4.8rem;
|
||||||
|
--space-minus-jumbo: -6.4rem;
|
||||||
|
--space-minus-mega: -10.0rem;
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,7 +37,10 @@ export default {
|
||||||
Spinner,
|
Spinner,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
groupedMessages: Array,
|
groupedMessages: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -1,3 +1,8 @@
|
||||||
|
json.meta do
|
||||||
|
json.count @contacts_count
|
||||||
|
json.current_page @current_page
|
||||||
|
end
|
||||||
|
|
||||||
json.payload do
|
json.payload do
|
||||||
json.array! @contacts do |contact|
|
json.array! @contacts do |contact|
|
||||||
json.partial! 'api/v1/models/contact.json.jbuilder', resource: contact, with_contact_inboxes: true
|
json.partial! 'api/v1/models/contact.json.jbuilder', resource: contact, with_contact_inboxes: true
|
||||||
|
|
|
@ -1,3 +1,8 @@
|
||||||
|
json.meta do
|
||||||
|
json.count @contacts_count
|
||||||
|
json.current_page @current_page
|
||||||
|
end
|
||||||
|
|
||||||
json.payload do
|
json.payload do
|
||||||
json.array! @contacts do |contact|
|
json.array! @contacts do |contact|
|
||||||
json.partial! 'api/v1/models/contact.json.jbuilder', resource: contact, with_contact_inboxes: true
|
json.partial! 'api/v1/models/contact.json.jbuilder', resource: contact, with_contact_inboxes: true
|
||||||
|
|
|
@ -1,3 +1,8 @@
|
||||||
|
json.meta do
|
||||||
|
json.count @contacts_count
|
||||||
|
json.current_page @current_page
|
||||||
|
end
|
||||||
|
|
||||||
json.payload do
|
json.payload do
|
||||||
json.array! @contacts do |contact|
|
json.array! @contacts do |contact|
|
||||||
json.partial! 'api/v1/models/contact.json.jbuilder', resource: contact, with_contact_inboxes: true
|
json.partial! 'api/v1/models/contact.json.jbuilder', resource: contact, with_contact_inboxes: true
|
||||||
|
|
|
@ -6,6 +6,8 @@ json.name resource.name
|
||||||
json.phone_number resource.phone_number
|
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.last_seen_at resource.last_seen_at 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?
|
||||||
|
|
|
@ -23,9 +23,22 @@ RSpec.describe 'Contacts API', type: :request do
|
||||||
as: :json
|
as: :json
|
||||||
|
|
||||||
expect(response).to have_http_status(:success)
|
expect(response).to have_http_status(:success)
|
||||||
expect(response.body).to include(contact.email)
|
response_body = JSON.parse(response.body)
|
||||||
expect(response.body).to include(contact_inbox.source_id)
|
expect(response_body['payload'].first['email']).to eq(contact.email)
|
||||||
expect(response.body).to include(contact_inbox.inbox.name)
|
expect(response_body['payload'].first['contact_inboxes'].first['source_id']).to eq(contact_inbox.source_id)
|
||||||
|
expect(response_body['payload'].first['contact_inboxes'].first['inbox']['name']).to eq(contact_inbox.inbox.name)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns includes conversations count and last seen at' do
|
||||||
|
create(:conversation, contact: contact, account: account, inbox: contact_inbox.inbox, contact_last_seen_at: Time.now.utc)
|
||||||
|
get "/api/v1/accounts/#{account.id}/contacts",
|
||||||
|
headers: admin.create_new_auth_token,
|
||||||
|
as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
response_body = JSON.parse(response.body)
|
||||||
|
expect(response_body['payload'].first['conversations_count']).to eq(contact.conversations.count)
|
||||||
|
expect(response_body['payload'].first['last_seen_at']).present?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue