feat: Update design of Contacts table (#1753)

Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
This commit is contained in:
Nithin David Thomas 2021-03-01 22:28:15 +05:30 committed by GitHub
parent 03bc4bf224
commit 89e5f18dfb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 243 additions and 126 deletions

View file

@ -44,3 +44,7 @@ code {
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
.cursor-pointer {
cursor: pointer;
}

View file

@ -111,12 +111,18 @@
"LIST": { "LIST": {
"LOADING_MESSAGE": "Loading contacts...", "LOADING_MESSAGE": "Loading contacts...",
"404": "No contacts matches your search 🔍", "404": "No contacts matches your search 🔍",
"TABLE_HEADER": [ "TABLE_HEADER": {
"Name", "NAME": "Name",
"Phone Number", "PHONE_NUMBER": "Phone Number",
"Conversations", "CONVERSATIONS": "Conversations",
"Last Contacted" "LAST_ACTIVITY": "Last Activity",
] "COUNTRY": "Country",
"CITY": "City",
"SOCIAL_PROFILES": "Social Profiles",
"COMPANY": "Company",
"EMAIL_ADDRESS": "Email Address"
},
"VIEW_DETAILS": "View details"
} }
} }
} }

View file

@ -1,53 +1,14 @@
<template> <template>
<section class="contacts-table-wrap"> <section class="contacts-table-wrap">
<table class="woot-table contacts-table"> <ve-table
<thead> :fixed-header="true"
<th max-height="calc(100vh - 11.4rem)"
v-for="thHeader in $t('CONTACTS_PAGE.LIST.TABLE_HEADER')" scroll-width="187rem"
:key="thHeader" :columns="columns"
> :table-data="tableData"
{{ thHeader }} :border-around="false"
</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_seen_at
? dynamicTime(contactItem.last_seen_at)
: '---'
}}
</td>
</tr>
</tbody>
</table>
<empty-state <empty-state
v-if="showSearchEmptyState" v-if="showSearchEmptyState"
:title="$t('CONTACTS_PAGE.LIST.404')" :title="$t('CONTACTS_PAGE.LIST.404')"
@ -61,6 +22,8 @@
<script> <script>
import { mixin as clickaway } from 'vue-clickaway'; import { mixin as clickaway } from 'vue-clickaway';
import { VeTable } from 'vue-easytable';
import Spinner from 'shared/components/Spinner.vue'; import Spinner from 'shared/components/Spinner.vue';
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue'; import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
import EmptyState from 'dashboard/components/widgets/EmptyState.vue'; import EmptyState from 'dashboard/components/widgets/EmptyState.vue';
@ -68,9 +31,9 @@ import timeMixin from 'dashboard/mixins/time';
export default { export default {
components: { components: {
Thumbnail,
EmptyState, EmptyState,
Spinner, Spinner,
VeTable,
}, },
mixins: [clickaway, timeMixin], mixins: [clickaway, timeMixin],
props: { props: {
@ -95,30 +58,150 @@ export default {
default: '', default: '',
}, },
}, },
data() {
return {
columns: [
{
field: 'name',
key: 'name',
title: this.$t('CONTACTS_PAGE.LIST.TABLE_HEADER.NAME'),
fixed: 'left',
align: 'left',
width: 300,
renderBodyCell: ({ row }) => (
<button
class="row--user-block cursor-pointer"
onClick={() => this.onClickContact(row.id)}
>
<Thumbnail
src={row.thumbnail}
size="36px"
username={row.name}
status={row.availability_status}
/>
<div>
<h6 class="sub-block-title user-name text-truncate">
{row.name}
</h6>
<button class="button clear small">
{this.$t('CONTACTS_PAGE.LIST.VIEW_DETAILS')}
</button>
</div>
</button>
),
},
{
field: 'email',
key: 'email',
title: this.$t('CONTACTS_PAGE.LIST.TABLE_HEADER.EMAIL_ADDRESS'),
align: 'left',
width: 240,
renderBodyCell: ({ row }) => {
if (row.email)
return (
<div class="text-truncate">
<a
target="_blank"
rel="noopener noreferrer nofollow"
href={`mailto:${row.email}`}
>
{row.email}
</a>
</div>
);
return '---';
},
},
{
field: 'phone',
key: 'phone',
title: this.$t('CONTACTS_PAGE.LIST.TABLE_HEADER.PHONE_NUMBER'),
align: 'left',
},
{
field: 'company',
key: 'company',
title: this.$t('CONTACTS_PAGE.LIST.TABLE_HEADER.COMPANY'),
align: 'left',
},
{
field: 'city',
key: 'city',
title: this.$t('CONTACTS_PAGE.LIST.TABLE_HEADER.CITY'),
align: 'left',
},
{
field: 'country',
key: 'country',
title: this.$t('CONTACTS_PAGE.LIST.TABLE_HEADER.COUNTRY'),
align: 'left',
},
{
field: 'profiles',
key: 'profiles',
title: this.$t('CONTACTS_PAGE.LIST.TABLE_HEADER.SOCIAL_PROFILES'),
align: 'left',
renderBodyCell: ({ row }) => {
const { profiles } = row;
const items = Object.keys(profiles);
if (!items.length) return '---';
return (
<div class="cell--social-profiles">
{items.map(
profile =>
profiles[profile] && (
<a
target="_blank"
rel="noopener noreferrer nofollow"
href={`https://${profile}.com/${profiles[profile]}`}
>
<i class={`ion-social-${profile}`} />
</a>
)
)}
</div>
);
},
},
{
field: 'lastSeen',
key: 'lastSeen',
title: this.$t('CONTACTS_PAGE.LIST.TABLE_HEADER.LAST_ACTIVITY'),
align: 'left',
},
{
field: 'conversationsCount',
key: 'conversationsCount',
title: this.$t('CONTACTS_PAGE.LIST.TABLE_HEADER.CONVERSATIONS'),
width: 150,
align: 'left',
},
],
};
},
computed: { computed: {
currentRoute() { tableData() {
return ' '; if (this.isLoading) {
}, return [];
sidebarClassName() {
if (this.isOnDesktop) {
return '';
} }
if (this.isSidebarOpen) { return this.contacts.map(item => {
return 'off-canvas is-open '; const additional = item.additional_attributes || {};
} const { last_seen_at: lastSeenAt } = item;
return 'off-canvas position-left is-transition-push is-closed'; return {
}, ...item,
contentClassName() { phone: item.phone_number || '---',
if (this.isOnDesktop) { company: additional.company_name || '---',
return ''; location: additional.location || '---',
} profiles: additional.social_profiles || {},
if (this.isSidebarOpen) { city: additional.city || '---',
return 'off-canvas-content is-open-left has-transition-push has-position-left'; country: additional.country || '---',
} conversationsCount: item.conversations_count || '---',
return 'off-canvas-content'; lastSeen: lastSeenAt ? this.dynamicTime(lastSeenAt) : '---',
}, };
showTableData() { });
return !this.showSearchEmptyState && !this.isLoading;
}, },
}, },
}; };
@ -128,52 +211,19 @@ export default {
@import '~dashboard/assets/scss/mixins'; @import '~dashboard/assets/scss/mixins';
.contacts-table-wrap { .contacts-table-wrap {
@include scroll-on-hover;
flex: 1 1; flex: 1 1;
height: 100%; height: 100%;
overflow: hidden;
} }
.contacts-table { .contacts-table-wrap::v-deep {
margin-top: -1px; .ve-table {
padding-bottom: var(--space-large);
> thead {
border-bottom: 1px solid var(--color-border);
background: white;
> th:first-child {
padding-left: var(--space-medium);
width: 30%;
}
} }
.row--user-block {
> 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; align-items: center;
display: flex;
text-align: left;
.user-thumbnail-box { .user-thumbnail-box {
margin-right: var(--space-small); margin-right: var(--space-small);
@ -189,13 +239,35 @@ export default {
margin: 0; margin: 0;
} }
} }
.ve-table-header-th {
padding: var(--space-small) var(--space-two) !important;
}
.ve-table-body-td {
padding: var(--space-slab) var(--space-two) !important;
}
.ve-table-header-th {
font-size: var(--font-size-mini) !important;
}
} }
.contacts--loader { .contacts--loader {
font-size: var(--font-size-default);
display: flex;
align-items: center; align-items: center;
display: flex;
font-size: var(--font-size-default);
justify-content: center; justify-content: center;
padding: var(--space-big); padding: var(--space-big);
} }
.cell--social-profiles {
a {
color: var(--s-300);
display: inline-block;
font-size: var(--font-size-medium);
min-width: var(--space-large);
text-align: center;
}
}
</style> </style>

View file

@ -141,7 +141,6 @@ export default {
.left-wrap { .left-wrap {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding-top: var(--space-normal);
height: 100%; height: 100%;
} }
</style> </style>

View file

@ -69,11 +69,6 @@ export default {
</script> </script>
<style lang="scss" scoped> <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 { .page-title {
margin: 0; margin: 0;
} }
@ -81,7 +76,14 @@ export default {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
width: 100%; width: 100%;
margin-bottom: var(--space-slab); padding: var(--space-small) var(--space-small) var(--space-small)
var(--space-normal);
}
.left-aligned-wrap {
display: flex;
align-items: center;
justify-content: center;
} }
.right-aligned-wrap { .right-aligned-wrap {

View file

@ -29,6 +29,7 @@ import {
registerSubscription, registerSubscription,
} from '../dashboard/helper/pushHelper'; } from '../dashboard/helper/pushHelper';
import * as Sentry from '@sentry/vue'; import * as Sentry from '@sentry/vue';
import 'vue-easytable/libs/theme-default/index.css';
Vue.config.env = process.env; Vue.config.env = process.env;

View file

@ -8,6 +8,10 @@ const resolve = {
widget: path.resolve('./app/javascript/widget'), widget: path.resolve('./app/javascript/widget'),
assets: path.resolve('./app/javascript/dashboard/assets'), assets: path.resolve('./app/javascript/dashboard/assets'),
components: path.resolve('./app/javascript/dashboard/components'), components: path.resolve('./app/javascript/dashboard/components'),
'./iconfont.eot': 'vue-easytable/libs/font/iconfont.eot',
'./iconfont.woff': 'vue-easytable/libs/font/iconfont.woff',
'./iconfont.ttf': 'vue-easytable/libs/font/iconfont.ttf',
'./iconfont.svg': 'vue-easytable/libs/font/iconfont.svg',
}, },
}; };

View file

@ -49,6 +49,7 @@
"vue-chartjs": "^3.4.2", "vue-chartjs": "^3.4.2",
"vue-clickaway": "~2.1.0", "vue-clickaway": "~2.1.0",
"vue-color": "^2.7.1", "vue-color": "^2.7.1",
"vue-easytable": "^2.1.2",
"vue-i18n": "^8.22.1", "vue-i18n": "^8.22.1",
"vue-loader": "^15.7.0", "vue-loader": "^15.7.0",
"vue-multiselect": "~2.1.6", "vue-multiselect": "~2.1.6",

View file

@ -9584,6 +9584,11 @@ requires-port@^1.0.0:
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=
resize-observer-polyfill@^1.5.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==
resolve-cwd@^2.0.0: resolve-cwd@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a"
@ -11224,6 +11229,16 @@ vue-color@^2.7.1:
material-colors "^1.0.0" material-colors "^1.0.0"
tinycolor2 "^1.1.2" tinycolor2 "^1.1.2"
vue-easytable@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/vue-easytable/-/vue-easytable-2.1.2.tgz#6cb22138b588195ee25ad37bfd660b48ce996a3a"
integrity sha512-x/88KdYABxbuJJlw6rAz8SAEJwoicOwUCgwubN/ibTuKT4+dDrFJnqW5LJHEhMWa+VIvTZnmBXZTobkyuak7hw==
dependencies:
lodash "^4.17.20"
resize-observer-polyfill "^1.5.1"
vue "^2.6.12"
vue-template-compiler "^2.6.11"
vue-eslint-parser@^7.0.0: vue-eslint-parser@^7.0.0:
version "7.0.0" version "7.0.0"
resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-7.0.0.tgz#a4ed2669f87179dedd06afdd8736acbb3a3864d6" resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-7.0.0.tgz#a4ed2669f87179dedd06afdd8736acbb3a3864d6"
@ -11304,6 +11319,14 @@ vue-template-compiler@^2.6.10:
de-indent "^1.0.2" de-indent "^1.0.2"
he "^1.1.0" he "^1.1.0"
vue-template-compiler@^2.6.11:
version "2.6.12"
resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.6.12.tgz#947ed7196744c8a5285ebe1233fe960437fcc57e"
integrity sha512-OzzZ52zS41YUbkCBfdXShQTe69j1gQDZ9HIX8miuC9C3rBCk9wIRjLiZZLrmX9V+Ftq/YEyv1JaVr5Y/hNtByg==
dependencies:
de-indent "^1.0.2"
he "^1.1.0"
vue-template-es2015-compiler@^1.6.0, vue-template-es2015-compiler@^1.9.0: vue-template-es2015-compiler@^1.6.0, vue-template-es2015-compiler@^1.9.0:
version "1.9.1" version "1.9.1"
resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825" resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825"
@ -11319,6 +11342,11 @@ vue@^2.6.0:
resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.11.tgz#76594d877d4b12234406e84e35275c6d514125c5" resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.11.tgz#76594d877d4b12234406e84e35275c6d514125c5"
integrity sha512-VfPwgcGABbGAue9+sfrD4PuwFar7gPb1yl1UK1MwXoQPAw0BKSqWfoYCT/ThFrdEVWoI51dBuyCoiNU9bZDZxQ== integrity sha512-VfPwgcGABbGAue9+sfrD4PuwFar7gPb1yl1UK1MwXoQPAw0BKSqWfoYCT/ThFrdEVWoI51dBuyCoiNU9bZDZxQ==
vue@^2.6.12:
version "2.6.12"
resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.12.tgz#f5ebd4fa6bd2869403e29a896aed4904456c9123"
integrity sha512-uhmLFETqPPNyuLLbsKz6ioJ4q7AZHzD8ZVFNATNyICSZouqP2Sz0rotWQC8UNBF6VGSCs5abnKJoStA6JbCbfg==
vuelidate@~0.7.5: vuelidate@~0.7.5:
version "0.7.5" version "0.7.5"
resolved "https://registry.yarnpkg.com/vuelidate/-/vuelidate-0.7.5.tgz#ff48c75ae9d24ea24c24e9ea08065eda0a0cba0a" resolved "https://registry.yarnpkg.com/vuelidate/-/vuelidate-0.7.5.tgz#ff48c75ae9d24ea24c24e9ea08065eda0a0cba0a"