feat: Add notes for Contacts (#3273)

Fixes #2275
This commit is contained in:
Pranav Raj S
2021-10-25 18:35:58 +05:30
committed by GitHub
parent e5e73a08fe
commit 8e6ce3a813
29 changed files with 416 additions and 278 deletions

View File

@@ -1,9 +1,17 @@
<template>
<div class="medium-3 bg-white contact--panel">
<span class="close-button" @click="onClose">
<div
class="small-12 medium-3 bg-white contact--panel"
:class="{ 'border-left': showAvatar }"
>
<span v-if="showAvatar" class="close-button" @click="onClose">
<i class="ion-android-close close-icon" />
</span>
<contact-info show-new-message :contact="contact" @panel-close="onClose" />
<contact-info
:show-avatar="showAvatar"
show-new-message
:contact="contact"
@panel-close="onClose"
/>
<accordion-item
:title="$t('CONTACT_PANEL.SIDEBAR_SECTIONS.CUSTOM_ATTRIBUTES')"
:is-open="isContactSidebarItemOpen('is_ct_custom_attr_open')"
@@ -61,6 +69,10 @@ export default {
type: Function,
default: () => {},
},
showAvatar: {
type: Boolean,
default: true,
},
},
computed: {
hasContactAttributes() {
@@ -85,7 +97,7 @@ export default {
overflow-y: auto;
overflow: auto;
position: relative;
border-left: 1px solid var(--color-border);
border-right: 1px solid var(--color-border);
}
.close-button {

View File

@@ -28,6 +28,7 @@
<script>
import { mixin as clickaway } from 'vue-clickaway';
import { VeTable } from 'vue-easytable';
import flag from 'country-code-emoji';
import Spinner from 'shared/components/Spinner.vue';
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
@@ -94,10 +95,10 @@ export default {
...item,
phone_number: item.phone_number || '---',
company: additional.company_name || '---',
location: additional.location || '---',
profiles: additional.social_profiles || {},
city: additional.city || '---',
country: additional.country || '---',
country: additional.country,
countryCode: additional.country_code,
conversationsCount: item.conversations_count || '---',
last_activity_at: lastActivityAt
? this.dynamicTime(lastActivityAt)
@@ -128,12 +129,17 @@ export default {
status={row.availability_status}
/>
<div class="user-block">
<h6 class="sub-block-title user-name text-truncate">
{row.name}
<h6 class="sub-block-title text-truncate">
<router-link
to={`/app/accounts/${this.$route.params.accountId}/contacts/${row.id}`}
class="user-name"
>
{row.name}
</router-link>
</h6>
<span class="button clear small link">
<button class="button clear small link view-details--button">
{this.$t('CONTACTS_PAGE.LIST.VIEW_DETAILS')}
</span>
</button>
</div>
</div>
</woot-button>
@@ -186,6 +192,16 @@ export default {
key: 'country',
title: this.$t('CONTACTS_PAGE.LIST.TABLE_HEADER.COUNTRY'),
align: 'left',
renderBodyCell: ({ row }) => {
if (row.country) {
return (
<div class="text-truncate">
{`${flag(row.countryCode)} ${row.country}`}
</div>
);
}
return '---';
},
},
{
field: 'profiles',
@@ -281,10 +297,15 @@ export default {
.user-name {
font-size: var(--font-size-small);
font-weight: var(--font-weight-medium);
margin: 0;
text-transform: capitalize;
}
.view-details--button {
color: var(--color-body);
}
.user-email {
margin: 0;
}

View File

@@ -1,81 +1,136 @@
<template>
<div class="contact-manage-view">
<contacts-header
:search-query="searchQuery"
:on-search-submit="onSearchSubmit"
:on-input-search="onInputSearch"
:on-toggle-create="onToggleCreate"
/>
<manage-layout :contact-id="contactId" />
<div class="view-box columns bg-white">
<settings-header
button-route="new"
:header-title="contact.name"
show-back-button
:back-button-label="$t('CONTACT_PROFILE.BACK_BUTTON')"
:back-url="backUrl"
:show-new-button="false"
>
<thumbnail
v-if="contact.thumbnail"
:src="contact.thumbnail"
:username="contact.name"
size="32px"
class="margin-right-small"
/>
</settings-header>
<create-contact :show="showCreateModal" @cancel="onToggleCreate" />
<div
v-if="uiFlags.isFetchingItem"
class="text-center p-normal fs-default h-full"
>
<spinner size="" />
<span>{{ $t('CONTACT_PROFILE.LOADING') }}</span>
</div>
<div
v-else-if="contact.id"
class="overflow-hidden column contact--dashboard-content"
>
<div class="row h-full">
<contact-info-panel :show-avatar="false" :contact="contact" />
<div class="small-12 medium-9 h-full">
<woot-tabs :index="selectedTabIndex" @change="onClickTabChange">
<woot-tabs-item
v-for="tab in tabs"
:key="tab.key"
:name="tab.name"
:show-badge="false"
/>
</woot-tabs>
<div class="tab-content overflow-auto">
<contact-notes
v-if="selectedTabIndex === 0"
:contact-id="Number(contactId)"
/>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import ContactsHeader from '../components/Header';
import ManageLayout from 'dashboard/modules/contact/components/ManageLayout';
import CreateContact from 'dashboard/routes/dashboard/conversation/contact/CreateContact';
import ContactInfoPanel from '../components/ContactInfoPanel.vue';
import ContactNotes from 'dashboard/modules/notes/NotesOnContactPage';
import SettingsHeader from '../../settings/SettingsHeader.vue';
import Spinner from 'shared/components/Spinner';
import Thumbnail from 'dashboard/components/widgets/Thumbnail';
export default {
components: {
ContactsHeader,
CreateContact,
ManageLayout,
ContactInfoPanel,
ContactNotes,
SettingsHeader,
Spinner,
Thumbnail,
},
props: {
contactId: {
type: [String, Number],
default: 0,
required: true,
},
},
data() {
return {
searchQuery: '',
showCreateModal: false,
selectedTabIndex: 0,
};
},
computed: {
...mapGetters({
uiFlags: 'contacts/getUIFlags',
}),
tabs() {
return [
{
key: 0,
name: this.$t('NOTES.HEADER.TITLE'),
},
];
},
showEmptySearchResult() {
const hasEmptyResults = !!this.searchQuery && this.records.length === 0;
return hasEmptyResults;
},
contact() {
return this.$store.getters['contacts/getContact'](this.contactId);
},
backUrl() {
return `/app/accounts/${this.$route.params.accountId}/contacts`;
},
},
mounted() {
this.fetchContactDetails();
},
mounted() {},
methods: {
onInputSearch(event) {
const newQuery = event.target.value;
const refetchAllContacts = !!this.searchQuery && newQuery === '';
if (refetchAllContacts) {
this.$store.dispatch('contacts/get', { page: 1 });
}
this.searchQuery = newQuery;
onClickTabChange(index) {
this.selectedTabIndex = index;
},
onSearchSubmit() {
this.selectedContactId = '';
if (this.searchQuery) {
this.$store.dispatch('contacts/search', {
search: this.searchQuery,
page: 1,
});
}
},
onToggleCreate() {
this.showCreateModal = !this.showCreateModal;
fetchContactDetails() {
const { contactId: id } = this;
this.$store.dispatch('contacts/show', { id });
},
},
};
</script>
<style lang="scss" scoped>
.contact-manage-view {
display: flex;
flex-direction: column;
width: 100%;
flex: 1 1 0;
@import '~dashboard/assets/scss/mixins';
.left {
border-right: 1px solid var(--color-border);
overflow: auto;
}
.right {
padding: var(--space-normal);
}
.tab-content {
background: var(--color-background-light);
height: calc(100% - 40px);
padding: var(--space-normal);
}
</style>

View File

@@ -21,7 +21,7 @@ export const routes = [
},
{
path: frontendURL('accounts/:accountId/contacts/:contactId'),
name: 'contacts_dashboard_manage',
name: 'contact_profile_dashboard',
roles: ['administrator', 'agent'],
component: ContactManageView,
props: route => {

View File

@@ -222,9 +222,8 @@ export default {
return this.additionalAttributes.initiated_at;
},
browserName() {
return `${this.browser.browser_name || ''} ${
this.browser.browser_version || ''
}`;
return `${this.browser.browser_name || ''} ${this.browser
.browser_version || ''}`;
},
contactAdditionalAttributes() {
return this.contact.additional_attributes || {};
@@ -248,8 +247,10 @@ export default {
return `${cityAndCountry} ${countryFlag}`;
},
platformName() {
const { platform_name: platformName, platform_version: platformVersion } =
this.browser;
const {
platform_name: platformName,
platform_version: platformVersion,
} = this.browser;
return `${platformName || ''} ${platformVersion || ''}`;
},
channelType() {
@@ -403,7 +404,7 @@ export default {
::v-deep {
.contact--profile {
padding-bottom: var(--space-slab);
border-bottom: 1px solid var(--color-border-light);
border-bottom: 1px solid var(--color-border);
}
.conversation--actions .multiselect-wrap--small {
.multiselect {

View File

@@ -2,6 +2,7 @@
<div class="contact--profile">
<div class="contact--info">
<thumbnail
v-if="showAvatar"
:src="contact.thumbnail"
size="56px"
:username="contact.name"
@@ -9,8 +10,16 @@
/>
<div class="contact--details">
<h3 class="sub-block-title contact--name">
{{ contact.name }}
<h3 v-if="showAvatar" class="sub-block-title contact--name">
<a
:href="contactProfileLink"
class="fs-default"
target="_blank"
rel="noopener nofollow noreferrer"
>
{{ contact.name }}
<i class="ion-android-open open-link--icon" />
</a>
</h3>
<p v-if="additionalAttributes.description" class="contact--bio">
{{ additionalAttributes.description }}
@@ -25,7 +34,6 @@
:title="$t('CONTACT_PANEL.EMAIL_ADDRESS')"
show-copy
/>
<contact-info-row
:href="contact.phone_number ? `tel:${contact.phone_number}` : ''"
:value="contact.phone_number"
@@ -161,6 +169,10 @@ export default {
type: Boolean,
default: false,
},
showAvatar: {
type: Boolean,
default: true,
},
},
data() {
return {
@@ -172,6 +184,9 @@ export default {
},
computed: {
...mapGetters({ uiFlags: 'contacts/getUIFlags' }),
contactProfileLink() {
return `/app/accounts/${this.$route.params.accountId}/contacts/${this.contact.id}`;
},
additionalAttributes() {
return this.contact.additional_attributes || {};
},
@@ -266,6 +281,16 @@ export default {
.contact--name {
text-transform: capitalize;
white-space: normal;
a {
color: var(--color-body);
}
.open-link--icon {
color: var(--color-body);
font-size: var(--font-size-small);
margin-left: var(--space-smaller);
}
}
.contact--metadata {

View File

@@ -2,8 +2,13 @@
<div class="settings-header">
<h1 class="page-title">
<woot-sidemenu-icon></woot-sidemenu-icon>
<back-button v-if="showBackButton" :back-url="backUrl"></back-button>
<i :class="iconClass"></i>
<back-button
v-if="showBackButton"
:button-label="backButtonLabel"
:back-url="backUrl"
/>
<i v-if="icon" :class="iconClass"></i>
<slot></slot>
<span>{{ headerTitle }}</span>
</h1>
<router-link
@@ -51,6 +56,10 @@ export default {
type: [String, Object],
default: '',
},
backButtonLabel: {
type: String,
default: '',
},
},
computed: {
...mapGetters({