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:
committed by
GitHub
parent
2babfd6148
commit
f214c9c47c
@@ -8,7 +8,6 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/* global bus */
|
||||
import Sidebar from '../../components/layout/Sidebar';
|
||||
|
||||
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>
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
import ContactConversations from './ContactConversations.vue';
|
||||
import ContactDetailsItem from './ContactDetailsItem.vue';
|
||||
import ContactInfo from './contact/ContactInfo';
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import AppContainer from './Dashboard';
|
||||
import settings from './settings/settings.routes';
|
||||
import conversation from './conversation/conversation.routes';
|
||||
import { routes as contactRoutes } from './contacts/routes';
|
||||
import { frontendURL } from '../../helper/URLHelper';
|
||||
|
||||
export default {
|
||||
@@ -8,7 +9,7 @@ export default {
|
||||
{
|
||||
path: frontendURL('accounts/:account_id'),
|
||||
component: AppContainer,
|
||||
children: [...conversation.routes, ...settings.routes],
|
||||
children: [...conversation.routes, ...settings.routes, ...contactRoutes],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user