Merge branch 'develop' into chore/chat-list-design

This commit is contained in:
Nithin David Thomas 2022-01-20 10:45:20 +05:30 committed by GitHub
commit 00b9803959
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
51 changed files with 762 additions and 53 deletions

View file

@ -0,0 +1,14 @@
/* global axios */
import ApiClient from './ApiClient';
class CustomViewsAPI extends ApiClient {
constructor() {
super('custom_filters', { accountScoped: true });
}
getCustomViews() {
return axios.get(this.url);
}
}
export default new CustomViewsAPI();

View file

@ -1,26 +1,51 @@
l<template>
<template>
<div class="conversations-list-wrap">
<slot></slot>
<div class="chat-list__top" :class="{ filter__applied: hasAppliedFilters }">
<div
class="chat-list__top"
:class="{ filter__applied: hasAppliedFiltersOrActiveCustomViews }"
>
<h1 class="page-sub-title text-truncate" :title="pageTitle">
{{ pageTitle }}
</h1>
<div class="filter--actions">
<chat-filter
v-if="!hasAppliedFilters"
v-if="!hasAppliedFiltersOrActiveCustomViews"
@statusFilterChange="updateStatusType"
/>
<div v-if="hasAppliedFilters && !hasActiveCustomViews">
<woot-button
v-tooltip.top-end="$t('FILTER.CUSTOM_VIEWS.ADD.SAVE_BUTTON')"
size="tiny"
variant="smooth"
color-scheme="secondary"
icon="save"
@click="onClickOpenAddCustomViewsModal"
/>
<woot-button
v-tooltip.top-end="$t('FILTER.CLEAR_BUTTON_LABEL')"
size="tiny"
variant="smooth"
color-scheme="alert"
icon="dismiss-circle"
@click="resetAndFetchData"
/>
</div>
<div v-if="hasActiveCustomViews">
<woot-button
v-tooltip.top-end="$t('FILTER.CUSTOM_VIEWS.DELETE.DELETE_BUTTON')"
size="tiny"
variant="smooth"
color-scheme="alert"
icon="delete"
class="delete-custom-view__button"
@click="onClickOpenDeleteCustomViewsModal"
/>
</div>
<woot-button
v-else
size="small"
variant="clear"
color-scheme="alert"
@click="resetAndFetchData"
>
{{ $t('FILTER.CLEAR_BUTTON_LABEL') }}
</woot-button>
<woot-button
v-tooltip.top-end="$t('FILTER.TOOLTIP_LABEL')"
variant="clear"
color-scheme="secondary"
@ -33,8 +58,22 @@ l<template>
</div>
</div>
<add-custom-views
v-if="showAddCustomViewsModal"
:custom-views-query="customViewsQuery"
@close="onCloseAddCustomViewsModal"
/>
<delete-custom-views
v-if="showDeleteCustomViewsModal"
:show-delete-popup.sync="showDeleteCustomViewsModal"
:active-custom-view="activeCustomView"
:custom-views-id="customViewsId"
@close="onCloseDeleteCustomViewsModal"
/>
<chat-type-tabs
v-if="!hasAppliedFilters"
v-if="!hasAppliedFiltersOrActiveCustomViews"
:items="assigneeTabItems"
:active-tab="activeAssigneeTab"
@chatTabChange="updateAssigneeTab"
@ -50,6 +89,7 @@ l<template>
:key="chat.id"
:active-label="label"
:team-id="teamId"
:custom-views-id="customViewsId"
:chat="chat"
:conversation-type="conversationType"
:show-assignee="showAssigneeInConversationCard"
@ -107,6 +147,8 @@ import conversationMixin from '../mixins/conversations';
import wootConstants from '../constants';
import advancedFilterTypes from './widgets/conversation/advancedFilterItems';
import filterQueryGenerator from '../helper/filterQueryGenerator.js';
import AddCustomViews from 'dashboard/routes/dashboard/customviews/AddCustomViews';
import DeleteCustomViews from 'dashboard/routes/dashboard/customviews/DeleteCustomViews.vue';
import {
hasPressedAltAndJKey,
@ -115,10 +157,12 @@ import {
export default {
components: {
AddCustomViews,
ChatTypeTabs,
ConversationCard,
ChatFilter,
ConversationAdvancedFilter,
DeleteCustomViews,
},
mixins: [timeMixin, conversationMixin, eventListenerMixins],
props: {
@ -138,6 +182,10 @@ export default {
type: String,
default: '',
},
customViewsId: {
type: [String, Number],
default: 0,
},
},
data() {
return {
@ -148,6 +196,9 @@ export default {
...filter,
attributeName: this.$t(`FILTER.ATTRIBUTES.${filter.attributeI18nKey}`),
})),
customViewsQuery: {},
showAddCustomViewsModal: false,
showDeleteCustomViewsModal: false,
};
},
computed: {
@ -162,10 +213,24 @@ export default {
activeInbox: 'getSelectedInbox',
conversationStats: 'conversationStats/getStats',
appliedFilters: 'getAppliedConversationFilters',
customViews: 'customViews/getCustomViews',
}),
hasAppliedFilters() {
return this.appliedFilters.length;
},
hasActiveCustomViews() {
return this.activeCustomView && this.customViewsId !== 0;
},
hasAppliedFiltersOrActiveCustomViews() {
return this.hasAppliedFilters || this.hasActiveCustomViews;
},
savedCustomViewsValue() {
if (this.hasActiveCustomViews) {
const payload = this.activeCustomView.query;
this.fetchSavedFilteredConversations(payload);
}
return {};
},
assigneeTabItems() {
return this.$t('CHAT_LIST.ASSIGNEE_TYPE_TABS').map(item => {
const count = this.conversationStats[item.COUNT_KEY] || 0;
@ -188,7 +253,9 @@ export default {
);
},
currentPageFilterKey() {
return this.hasAppliedFilters ? 'appliedFilters' : this.activeAssigneeTab;
return this.hasAppliedFiltersOrActiveCustomViews
? 'appliedFilters'
: this.activeAssigneeTab;
},
currentFiltersPage() {
return this.$store.getters['conversationPage/getCurrentPageFilter'](
@ -211,6 +278,9 @@ export default {
conversationType: this.conversationType
? this.conversationType
: undefined,
customViews: this.hasActiveCustomViews
? this.savedCustomViewsValue
: undefined,
};
},
pageTitle() {
@ -226,11 +296,14 @@ export default {
if (this.conversationType === 'mention') {
return this.$t('CHAT_LIST.MENTION_HEADING');
}
if (this.hasActiveCustomViews) {
return this.activeCustomView.name;
}
return this.$t('CHAT_LIST.TAB_HEADING');
},
conversationList() {
let conversationList = [];
if (!this.hasAppliedFilters) {
if (!this.hasAppliedFiltersOrActiveCustomViews) {
const filters = this.conversationFilters;
if (this.activeAssigneeTab === 'me') {
conversationList = [...this.mineChatsList(filters)];
@ -245,6 +318,16 @@ export default {
return conversationList;
},
activeCustomView() {
if (this.customViewsId) {
const activeView = this.customViews.filter(
view => view.id === Number(this.customViewsId)
);
const [firstValue] = activeView;
return firstValue;
}
return undefined;
},
activeTeam() {
if (this.teamId) {
return this.$store.getters['teams/getTeam'](this.teamId);
@ -265,6 +348,9 @@ export default {
conversationType() {
this.resetAndFetchData();
},
activeCustomView() {
this.resetAndFetchData();
},
},
mounted() {
this.$store.dispatch('setChatFilter', this.activeStatus);
@ -279,10 +365,23 @@ export default {
if (this.$route.name !== 'home') {
this.$router.push({ name: 'home' });
}
this.customViewsQuery = { payload: payload };
this.$store.dispatch('conversationPage/reset');
this.$store.dispatch('emptyAllConversations');
this.fetchFilteredConversations(payload);
},
onClickOpenAddCustomViewsModal() {
this.showAddCustomViewsModal = true;
},
onCloseAddCustomViewsModal() {
this.showAddCustomViewsModal = false;
},
onClickOpenDeleteCustomViewsModal() {
this.showDeleteCustomViewsModal = true;
},
onCloseDeleteCustomViewsModal() {
this.showDeleteCustomViewsModal = false;
},
onToggleAdvanceFiltersModal() {
this.showAdvancedFilters = !this.showAdvancedFilters;
},
@ -334,6 +433,13 @@ export default {
this.$store.dispatch('conversationPage/reset');
this.$store.dispatch('emptyAllConversations');
this.$store.dispatch('clearConversationFilters');
if (this.hasActiveCustomViews) {
const payload = this.activeCustomView.query;
this.fetchSavedFilteredConversations(payload);
}
if (this.customViewsId) {
return;
}
this.fetchConversations();
},
fetchConversations() {
@ -342,8 +448,12 @@ export default {
.then(() => this.$emit('conversation-load'));
},
loadMoreConversations() {
if (!this.hasAppliedFilters) {
if (!this.hasAppliedFiltersOrActiveCustomViews) {
this.fetchConversations();
}
if (this.hasActiveCustomViews) {
const payload = this.activeCustomView.query;
this.fetchSavedFilteredConversations(payload);
} else {
this.fetchFilteredConversations(this.appliedFilters);
}
@ -358,6 +468,15 @@ export default {
.then(() => this.$emit('conversation-load'));
this.showAdvancedFilters = false;
},
fetchSavedFilteredConversations(payload) {
let page = this.currentFiltersPage + 1;
this.$store
.dispatch('fetchFilteredConversations', {
queryData: payload,
page,
})
.then(() => this.$emit('conversation-load'));
},
updateAssigneeTab(selectedTab) {
if (this.activeAssigneeTab !== selectedTab) {
bus.$emit('clearSearchInput');
@ -412,11 +531,14 @@ export default {
}
.filter__applied {
padding: var(--space-slab) 0 !important;
padding: 0 0 var(--space-slab) 0 !important;
border-bottom: 1px solid var(--color-border);
}
.page-sub-title {
margin-left: var(--space-normal);
}
.delete-custom-view__button {
margin-right: var(--space-normal);
}
</style>

View file

@ -14,6 +14,7 @@
:inboxes="inboxes"
:labels="labels"
:teams="teams"
:custom-views="customViews"
:menu-config="activeSecondaryMenu"
:current-role="currentRole"
@add-label="showAddLabelPopup"
@ -87,6 +88,7 @@ export default {
currentUser: 'getCurrentUser',
globalConfig: 'globalConfig/get',
inboxes: 'inboxes/getInboxes',
customViews: 'customViews/getCustomViews',
accountId: 'getCurrentAccountId',
currentRole: 'getCurrentRole',
labels: 'labels/getLabelsOnSidebar',
@ -122,6 +124,7 @@ export default {
mounted() {
this.$store.dispatch('labels/get');
this.$store.dispatch('inboxes/get');
this.$store.dispatch('customViews/get');
this.$store.dispatch('notifications/unReadCount');
this.$store.dispatch('teams/get');
this.$store.dispatch('attributes/get');

View file

@ -14,6 +14,8 @@ const conversations = accountId => ({
'conversations_through_team',
'conversation_mentions',
'conversation_through_mentions',
'custom_view_conversations',
'conversations_through_custom_view',
],
menuItems: [
{

View file

@ -40,6 +40,10 @@ export default {
type: Array,
default: () => [],
},
customViews: {
type: Array,
default: () => [],
},
menuConfig: {
type: Object,
default: () => {},
@ -150,11 +154,35 @@ export default {
})),
};
},
customViewsSection() {
return {
icon: 'folder',
label: 'CUSTOM_VIEWS',
hasSubMenu: true,
key: 'custom_view',
children: this.customViews
.filter(view => view.filter_type === 'conversation')
.map(view => ({
id: view.id,
label: view.name,
truncateLabel: true,
toState: frontendURL(
`accounts/${this.accountId}/custom_view/${view.id}`
),
})),
};
},
additionalSecondaryMenuItems() {
let conversationMenuItems = [this.inboxSection, this.labelSection];
if (this.teams.length) {
conversationMenuItems = [this.teamSection, ...conversationMenuItems];
}
if (this.customViews.length) {
conversationMenuItems = [
this.customViewsSection,
...conversationMenuItems,
];
}
return {
conversations: conversationMenuItems,
contacts: [this.contactLabelSection],

View file

@ -7,7 +7,7 @@
v-else
class="secondary-menu--title secondary-menu--link fs-small"
:class="computedClass"
:to="menuItem.toState"
:to="menuItem && menuItem.toState"
>
<fluent-icon
:icon="menuItem.icon"

View file

@ -192,7 +192,6 @@ export default {
'setConversationFilters',
JSON.parse(JSON.stringify(this.appliedFilters))
);
this.appliedFilters[this.appliedFilters.length - 1].query_operator = null;
this.$emit('applyFilter', this.appliedFilters);
},
resetFilter(index, currentFilter) {

View file

@ -128,6 +128,10 @@ export default {
type: [String, Number],
default: 0,
},
customViewsId: {
type: [String, Number],
default: 0,
},
showAssignee: {
type: Boolean,
default: false,
@ -242,6 +246,7 @@ export default {
id: chat.id,
label: this.activeLabel,
teamId: this.teamId,
customViewsId: this.customViewsId,
conversationType: this.conversationType,
});
router.push({ path: frontendURL(path) });

View file

@ -12,6 +12,7 @@ export const conversationUrl = ({
label,
teamId,
conversationType = '',
customViewsId,
}) => {
let url = `accounts/${accountId}/conversations/${id}`;
if (activeInbox) {
@ -20,6 +21,8 @@ export const conversationUrl = ({
url = `accounts/${accountId}/label/${label}/conversations/${id}`;
} else if (teamId) {
url = `accounts/${accountId}/team/${teamId}/conversations/${id}`;
} else if (customViewsId && customViewsId !== 0) {
url = `accounts/${accountId}/custom_view/${customViewsId}/conversations/${id}`;
} else if (conversationType === 'mention') {
url = `accounts/${accountId}/mentions/conversations/${id}`;
}

View file

@ -11,7 +11,10 @@ const generatePayload = data => {
}
return item;
});
// For every query added, the query_operator is set default to and so the
// last query will have an extra query_operator, this would break the api.
// Setting this to null for all query payload
payload[payload.length - 1].query_operator = undefined;
return { payload };
};

View file

@ -31,7 +31,7 @@ const testData = [
attribute_key: 'id',
filter_operator: 'equal_to',
values: 'This is a test',
query_operator: null,
query_operator: 'or',
},
];
@ -53,7 +53,6 @@ const finalResult = {
attribute_key: 'id',
filter_operator: 'equal_to',
values: ['This is a test'],
query_operator: null,
},
],
};

View file

@ -40,7 +40,7 @@
"AUTO_RESOLVE_DURATION": {
"LABEL": "Number of days after a ticket should auto resolve if there is no activity",
"PLACEHOLDER": "30",
"ERROR": "Please enter a valid auto resolve duration (minimum 1 day)"
"ERROR": "Please enter a valid auto resolve duration (minimum 1 day and maximum 999 days)"
},
"FEATURES": {
"INBOUND_EMAIL_ENABLED": "Conversation continuity with emails is enabled for your account.",

View file

@ -32,6 +32,35 @@
"BROWSER_LANGUAGE": "Browser Language",
"COUNTRY_NAME": "Country Name",
"REFERER_LINK": "Referer link"
},
"CUSTOM_VIEWS": {
"ADD": {
"TITLE": "Do you want to save this filter?",
"LABEL": "Name this filter",
"PLACEHOLDER": "Enter a name for this filter",
"ERROR_MESSAGE": "Name is required",
"SAVE_BUTTON": "Save filter",
"CANCEL_BUTTON": "Cancel",
"API": {
"SUCCESS_MESSAGE": "Custom view created successfully",
"ERROR_MESSAGE": "Error while creating custom view"
}
},
"DELETE": {
"DELETE_BUTTON": "Delete filter",
"MODAL": {
"CONFIRM": {
"TITLE": "Confirm Deletion",
"MESSAGE": "Are you sure to delete the filter ",
"YES": "Yes, Delete ",
"NO": "No, Keep "
}
},
"API": {
"SUCCESS_MESSAGE": "Custom view deleted successfully",
"ERROR_MESSAGE": "Error while deleting custom view"
}
}
}
}
}

View file

@ -40,7 +40,7 @@
"AUTO_RESOLVE_DURATION": {
"LABEL": "Number of days after a ticket should auto resolve if there is no activity",
"PLACEHOLDER": "30",
"ERROR": "Please enter a valid auto resolve duration (minimum 1 day)"
"ERROR": "Please enter a valid auto resolve duration (minimum 1 day and maximum 999 days)"
},
"FEATURES": {
"INBOUND_EMAIL_ENABLED": "Conversation continuity with emails is enabled for your account.",

View file

@ -72,11 +72,7 @@
},
"AVAILABILITY": {
"LABEL": "Availability",
"STATUSES_LIST": [
"Online",
"Busy",
"Offline"
]
"STATUSES_LIST": ["Online", "Busy", "Offline"]
},
"EMAIL": {
"LABEL": "Your email address",
@ -153,6 +149,7 @@
"CUSTOM_ATTRIBUTES": "Custom Attributes",
"AUTOMATION": "Automation",
"TEAMS": "Teams",
"CUSTOM_VIEWS": "Folders",
"ALL_CONTACTS": "All Contacts",
"TAGGED_WITH": "Tagged with",
"NEW_LABEL": "New label",

View file

@ -40,7 +40,7 @@
"AUTO_RESOLVE_DURATION": {
"LABEL": "Number of days after a ticket should auto resolve if there is no activity",
"PLACEHOLDER": "30",
"ERROR": "Please enter a valid auto resolve duration (minimum 1 day)"
"ERROR": "Please enter a valid auto resolve duration (minimum 1 day and maximum 999 days)"
},
"FEATURES": {
"INBOUND_EMAIL_ENABLED": "Conversation continuity with emails is enabled for your account.",

View file

@ -40,7 +40,7 @@
"AUTO_RESOLVE_DURATION": {
"LABEL": "Number of days after a ticket should auto resolve if there is no activity",
"PLACEHOLDER": "30",
"ERROR": "Please enter a valid auto resolve duration (minimum 1 day)"
"ERROR": "Please enter a valid auto resolve duration (minimum 1 day and maximum 999 days)"
},
"FEATURES": {
"INBOUND_EMAIL_ENABLED": "Conversation continuity with emails is enabled for your account.",

View file

@ -40,7 +40,7 @@
"AUTO_RESOLVE_DURATION": {
"LABEL": "Number of days after a ticket should auto resolve if there is no activity",
"PLACEHOLDER": "30",
"ERROR": "Please enter a valid auto resolve duration (minimum 1 day)"
"ERROR": "Please enter a valid auto resolve duration (minimum 1 day and maximum 999 days)"
},
"FEATURES": {
"INBOUND_EMAIL_ENABLED": "あなたのアカウントでは、メールでの会話が継続できるようになっています。",

View file

@ -40,7 +40,7 @@
"AUTO_RESOLVE_DURATION": {
"LABEL": "പ്രവർത്തനമൊന്നുമില്ലെങ്കിൽ ടിക്കറ്റിന് ശേഷമുള്ള ദിവസങ്ങളുടെ എണ്ണം യാന്ത്രികമായി പരിഹരിക്കേണ്ടതാണ്",
"PLACEHOLDER": "30",
"ERROR": "Please enter a valid auto resolve duration (minimum 1 day)"
"ERROR": "Please enter a valid auto resolve duration (minimum 1 day and maximum 999 days)"
},
"FEATURES": {
"INBOUND_EMAIL_ENABLED": "നിങ്ങളുടെ അക്കൗണ്ടിനായി ഇമെയിലുകളുമായുള്ള സംഭാഷണ തുടർച്ച പ്രവർത്തനക്ഷമമാക്കി.",

View file

@ -40,7 +40,7 @@
"AUTO_RESOLVE_DURATION": {
"LABEL": "Number of days after a ticket should auto resolve if there is no activity",
"PLACEHOLDER": "30",
"ERROR": "Please enter a valid auto resolve duration (minimum 1 day)"
"ERROR": "Please enter a valid auto resolve duration (minimum 1 day and maximum 999 days)"
},
"FEATURES": {
"INBOUND_EMAIL_ENABLED": "Conversation continuity with emails is enabled for your account.",

View file

@ -40,7 +40,7 @@
"AUTO_RESOLVE_DURATION": {
"LABEL": "Number of days after a ticket should auto resolve if there is no activity",
"PLACEHOLDER": "30",
"ERROR": "Please enter a valid auto resolve duration (minimum 1 day)"
"ERROR": "Please enter a valid auto resolve duration (minimum 1 day and maximum 999 days)"
},
"FEATURES": {
"INBOUND_EMAIL_ENABLED": "Conversation continuity with emails is enabled for your account.",

View file

@ -40,7 +40,7 @@
"AUTO_RESOLVE_DURATION": {
"LABEL": "Number of days after a ticket should auto resolve if there is no activity",
"PLACEHOLDER": "30",
"ERROR": "Please enter a valid auto resolve duration (minimum 1 day)"
"ERROR": "Please enter a valid auto resolve duration (minimum 1 day and maximum 999 days)"
},
"FEATURES": {
"INBOUND_EMAIL_ENABLED": "Conversation continuity with emails is enabled for your account.",

View file

@ -40,7 +40,7 @@
"AUTO_RESOLVE_DURATION": {
"LABEL": "Number of days after a ticket should auto resolve if there is no activity",
"PLACEHOLDER": "30",
"ERROR": "Please enter a valid auto resolve duration (minimum 1 day)"
"ERROR": "Please enter a valid auto resolve duration (minimum 1 day and maximum 999 days)"
},
"FEATURES": {
"INBOUND_EMAIL_ENABLED": "การสนทนาด้วยอีเมล์ถูกเปิดสำหรับบัญชีของคุณ",

View file

@ -40,7 +40,7 @@
"AUTO_RESOLVE_DURATION": {
"LABEL": "Number of days after a ticket should auto resolve if there is no activity",
"PLACEHOLDER": "30",
"ERROR": "Please enter a valid auto resolve duration (minimum 1 day)"
"ERROR": "Please enter a valid auto resolve duration (minimum 1 day and maximum 999 days)"
},
"FEATURES": {
"INBOUND_EMAIL_ENABLED": "Conversation continuity with emails is enabled for your account.",

View file

@ -40,7 +40,7 @@
"AUTO_RESOLVE_DURATION": {
"LABEL": "Number of days after a ticket should auto resolve if there is no activity",
"PLACEHOLDER": "30",
"ERROR": "Please enter a valid auto resolve duration (minimum 1 day)"
"ERROR": "Please enter a valid auto resolve duration (minimum 1 day and maximum 999 days)"
},
"FEATURES": {
"INBOUND_EMAIL_ENABLED": "Tính liên tục của cuộc trò chuyện với email được kích hoạt cho tài khoản của bạn.",

View file

@ -40,7 +40,7 @@
"AUTO_RESOLVE_DURATION": {
"LABEL": "Number of days after a ticket should auto resolve if there is no activity",
"PLACEHOLDER": "30",
"ERROR": "Please enter a valid auto resolve duration (minimum 1 day)"
"ERROR": "Please enter a valid auto resolve duration (minimum 1 day and maximum 999 days)"
},
"FEATURES": {
"INBOUND_EMAIL_ENABLED": "您的帐户启用了与电子邮件的对话连续性。",

View file

@ -40,7 +40,7 @@
"AUTO_RESOLVE_DURATION": {
"LABEL": "Number of days after a ticket should auto resolve if there is no activity",
"PLACEHOLDER": "30",
"ERROR": "Please enter a valid auto resolve duration (minimum 1 day)"
"ERROR": "Please enter a valid auto resolve duration (minimum 1 day and maximum 999 days)"
},
"FEATURES": {
"INBOUND_EMAIL_ENABLED": "您的帳戶啟用了電子信箱與對話的持續性功能。",

View file

@ -159,6 +159,12 @@ export default {
border-right: 1px solid var(--color-border);
}
.list-group {
.list-group-item {
background-color: var(--white);
}
}
.close-button {
position: absolute;
right: var(--space-normal);

View file

@ -160,7 +160,6 @@ export default {
'contacts/setContactFilters',
JSON.parse(JSON.stringify(this.appliedFilters))
);
this.appliedFilters[this.appliedFilters.length - 1].query_operator = null;
this.$emit('applyFilter', this.appliedFilters);
},
resetFilter(index, currentFilter) {

View file

@ -236,6 +236,12 @@ export default {
}
}
.list-group {
.list-group-item {
background-color: var(--white);
}
}
::v-deep {
.contact--profile {
padding-bottom: var(--space-slab);

View file

@ -5,6 +5,7 @@
:label="label"
:team-id="teamId"
:conversation-type="conversationType"
:custom-views-id="customViewsId"
@conversation-load="onConversationLoad"
>
<pop-over-search />
@ -54,6 +55,10 @@ export default {
type: String,
default: '',
},
customViewsId: {
type: [String, Number],
default: 0,
},
},
data() {
return {

View file

@ -83,6 +83,25 @@ export default {
teamId: route.params.teamId,
}),
},
{
path: frontendURL('accounts/:accountId/custom_view/:id'),
name: 'custom_view_conversations',
roles: ['administrator', 'agent'],
component: ConversationView,
props: route => ({ customViewsId: route.params.id }),
},
{
path: frontendURL(
'accounts/:accountId/custom_view/:id/conversations/:conversation_id'
),
name: 'conversations_through_custom_view',
roles: ['administrator', 'agent'],
component: ConversationView,
props: route => ({
conversationId: route.params.conversation_id,
customViewsId: route.params.id,
}),
},
{
path: frontendURL('accounts/:accountId/mentions/conversations'),
name: 'conversation_mentions',

View file

@ -0,0 +1,97 @@
<template>
<woot-modal :show.sync="show" :on-close="onClose">
<woot-modal-header :header-title="$t('FILTER.CUSTOM_VIEWS.ADD.TITLE')" />
<form class="row" @submit.prevent="saveCustomViews">
<div class="medium-12 columns">
<woot-input
v-model="name"
:label="$t('FILTER.CUSTOM_VIEWS.ADD.LABEL')"
type="text"
:error="
$v.name.$error ? $t('FILTER.CUSTOM_VIEWS.ADD.ERROR_MESSAGE') : ''
"
:class="{ error: $v.name.$error }"
:placeholder="$t('FILTER.CUSTOM_VIEWS.ADD.PLACEHOLDER')"
@blur="$v.name.$touch"
/>
<div class="modal-footer">
<woot-button :disabled="isButtonDisabled">
{{ $t('FILTER.CUSTOM_VIEWS.ADD.SAVE_BUTTON') }}
</woot-button>
<woot-button variant="clear" @click.prevent="onClose">
{{ $t('FILTER.CUSTOM_VIEWS.ADD.CANCEL_BUTTON') }}
</woot-button>
</div>
</div>
</form>
</woot-modal>
</template>
<script>
import { required, minLength } from 'vuelidate/lib/validators';
import alertMixin from 'shared/mixins/alertMixin';
export default {
mixins: [alertMixin],
props: {
filterType: {
type: Number,
default: 0,
},
customViewsQuery: {
type: Object,
default: () => {},
},
},
data() {
return {
show: true,
name: '',
};
},
computed: {
isButtonDisabled() {
return this.$v.name.$invalid;
},
},
validations: {
name: {
required,
minLength: minLength(1),
},
},
methods: {
onClose() {
this.$emit('close');
},
async saveCustomViews() {
this.$v.$touch();
if (this.$v.$invalid) {
return;
}
try {
await this.$store.dispatch('customViews/create', {
name: this.name,
filter_type: this.filterType,
query: this.customViewsQuery,
});
this.alertMessage = this.$t(
'FILTER.CUSTOM_VIEWS.ADD.API.SUCCESS_MESSAGE'
);
this.onClose();
} catch (error) {
const errorMessage = error?.message;
this.alertMessage =
errorMessage || this.$t('FILTER.CUSTOM_VIEWS.ADD.API.ERROR_MESSAGE');
} finally {
this.showAlert(this.alertMessage);
}
},
},
};
</script>

View file

@ -0,0 +1,77 @@
<template>
<div>
<woot-delete-modal
v-if="showDeletePopup"
:show.sync="showDeletePopup"
:on-close="closeDeletePopup"
:on-confirm="deleteSavedCustomViews"
:title="$t('FILTER.CUSTOM_VIEWS.DELETE.MODAL.CONFIRM.TITLE')"
:message="deleteMessage"
:confirm-text="deleteConfirmText"
:reject-text="deleteRejectText"
/>
</div>
</template>
<script>
import alertMixin from 'shared/mixins/alertMixin';
export default {
mixins: [alertMixin],
props: {
showDeletePopup: {
type: Boolean,
default: false,
},
activeCustomView: {
type: Object,
default: () => {},
},
customViewsId: {
type: [String, Number],
default: 0,
},
},
computed: {
deleteMessage() {
return `${this.$t(
'FILTER.CUSTOM_VIEWS.DELETE.MODAL.CONFIRM.MESSAGE'
)} ${this.activeCustomView && this.activeCustomView.name} ?`;
},
deleteConfirmText() {
return `${this.$t('FILTER.CUSTOM_VIEWS.DELETE.MODAL.CONFIRM.YES')} ${this
.activeCustomView && this.activeCustomView.name}`;
},
deleteRejectText() {
return `${this.$t('FILTER.CUSTOM_VIEWS.DELETE.MODAL.CONFIRM.NO')} ${this
.activeCustomView && this.activeCustomView.name}`;
},
},
methods: {
async deleteSavedCustomViews() {
try {
await this.$store.dispatch(
'customViews/delete',
Number(this.customViewsId)
);
this.closeDeletePopup();
this.showAlert(
this.$t('FILTER.CUSTOM_VIEWS.DELETE.API.SUCCESS_MESSAGE')
);
} catch (error) {
const errorMessage =
error?.response?.message ||
this.$t('FILTER.CUSTOM_VIEWS.DELETE.API.ERROR_MESSAGE');
this.showAlert(errorMessage);
}
if (this.$route.name !== 'home') {
this.$router.push({ name: 'home' });
}
},
closeDeletePopup() {
this.$emit('close');
},
},
};
</script>

View file

@ -116,7 +116,7 @@
</template>
<script>
import { required, minValue } from 'vuelidate/lib/validators';
import { required, minValue, maxValue } from 'vuelidate/lib/validators';
import { mapGetters } from 'vuex';
import alertMixin from 'shared/mixins/alertMixin';
import configMixin from 'shared/mixins/configMixin';
@ -146,6 +146,7 @@ export default {
},
autoResolveDuration: {
minValue: minValue(1),
maxValue: maxValue(999),
},
},
computed: {

View file

@ -4,7 +4,7 @@
<ve-table
v-else
:columns="columns"
scroll-width="155rem"
scroll-width="190rem"
:table-data="tableData"
:border-around="true"
/>

View file

@ -31,6 +31,7 @@ import teams from './modules/teams';
import userNotificationSettings from './modules/userNotificationSettings';
import webhooks from './modules/webhooks';
import attributes from './modules/attributes';
import customViews from './modules/customViews';
Vue.use(Vuex);
export default new Vuex.Store({
@ -65,5 +66,6 @@ export default new Vuex.Store({
userNotificationSettings,
webhooks,
attributes,
customViews,
},
});

View file

@ -0,0 +1,79 @@
import * as MutationHelpers from 'shared/helpers/vuex/mutationHelpers';
import types from '../mutation-types';
import CustomViewsAPI from '../../api/customViews';
export const state = {
records: [],
uiFlags: {
isFetching: false,
isCreating: false,
isDeleting: false,
},
};
export const getters = {
getUIFlags(_state) {
return _state.uiFlags;
},
getCustomViews(_state) {
return _state.records;
},
};
export const actions = {
get: async function getCustomViews({ commit }) {
commit(types.SET_CUSTOM_VIEW_UI_FLAG, { isFetching: true });
try {
const response = await CustomViewsAPI.getCustomViews();
commit(types.SET_CUSTOM_VIEW, response.data);
} catch (error) {
// Ignore error
} finally {
commit(types.SET_CUSTOM_VIEW_UI_FLAG, { isFetching: false });
}
},
create: async function createCustomViews({ commit }, obj) {
commit(types.SET_CUSTOM_VIEW_UI_FLAG, { isCreating: true });
try {
const response = await CustomViewsAPI.create(obj);
commit(types.ADD_CUSTOM_VIEW, response.data);
} catch (error) {
const errorMessage = error?.response?.data?.message;
throw new Error(errorMessage);
} finally {
commit(types.SET_CUSTOM_VIEW_UI_FLAG, { isCreating: false });
}
},
delete: async ({ commit }, id) => {
commit(types.SET_CUSTOM_VIEW_UI_FLAG, { isDeleting: true });
try {
await CustomViewsAPI.delete(id);
commit(types.DELETE_CUSTOM_VIEW, id);
} catch (error) {
throw new Error(error);
} finally {
commit(types.SET_CUSTOM_VIEW_UI_FLAG, { isDeleting: false });
}
},
};
export const mutations = {
[types.SET_CUSTOM_VIEW_UI_FLAG](_state, data) {
_state.uiFlags = {
..._state.uiFlags,
...data,
};
},
[types.ADD_CUSTOM_VIEW]: MutationHelpers.create,
[types.SET_CUSTOM_VIEW]: MutationHelpers.set,
[types.DELETE_CUSTOM_VIEW]: MutationHelpers.destroy,
};
export default {
namespaced: true,
actions,
state,
getters,
mutations,
};

View file

@ -0,0 +1,72 @@
import axios from 'axios';
import { actions } from '../../customViews';
import * as types from '../../../mutation-types';
import customViewList from './fixtures';
const commit = jest.fn();
global.axios = axios;
jest.mock('axios');
describe('#actions', () => {
describe('#get', () => {
it('sends correct actions if API is success', async () => {
axios.get.mockResolvedValue({ data: customViewList });
await actions.get({ commit });
expect(commit.mock.calls).toEqual([
[types.default.SET_CUSTOM_VIEW_UI_FLAG, { isFetching: true }],
[types.default.SET_CUSTOM_VIEW, customViewList],
[types.default.SET_CUSTOM_VIEW_UI_FLAG, { isFetching: false }],
]);
});
it('sends correct actions if API is error', async () => {
axios.get.mockRejectedValue({ message: 'Incorrect header' });
await actions.get({ commit });
expect(commit.mock.calls).toEqual([
[types.default.SET_CUSTOM_VIEW_UI_FLAG, { isFetching: true }],
[types.default.SET_CUSTOM_VIEW_UI_FLAG, { isFetching: false }],
]);
});
});
describe('#create', () => {
it('sends correct actions if API is success', async () => {
axios.post.mockResolvedValue({ data: customViewList[0] });
await actions.create({ commit }, customViewList[0]);
expect(commit.mock.calls).toEqual([
[types.default.SET_CUSTOM_VIEW_UI_FLAG, { isCreating: true }],
[types.default.ADD_CUSTOM_VIEW, customViewList[0]],
[types.default.SET_CUSTOM_VIEW_UI_FLAG, { isCreating: false }],
]);
});
it('sends correct actions if API is error', async () => {
axios.post.mockRejectedValue({ message: 'Incorrect header' });
await expect(actions.create({ commit })).rejects.toThrow(Error);
expect(commit.mock.calls).toEqual([
[types.default.SET_CUSTOM_VIEW_UI_FLAG, { isCreating: true }],
[types.default.SET_CUSTOM_VIEW_UI_FLAG, { isCreating: false }],
]);
});
});
describe('#delete', () => {
it('sends correct actions if API is success', async () => {
axios.delete.mockResolvedValue({ data: customViewList[0] });
await actions.delete({ commit }, customViewList[0].id);
expect(commit.mock.calls).toEqual([
[types.default.SET_CUSTOM_VIEW_UI_FLAG, { isDeleting: true }],
[types.default.DELETE_CUSTOM_VIEW, customViewList[0].id],
[types.default.SET_CUSTOM_VIEW_UI_FLAG, { isDeleting: false }],
]);
});
it('sends correct actions if API is error', async () => {
axios.delete.mockRejectedValue({ message: 'Incorrect header' });
await expect(
actions.delete({ commit }, customViewList[0].id)
).rejects.toThrow(Error);
expect(commit.mock.calls).toEqual([
[types.default.SET_CUSTOM_VIEW_UI_FLAG, { isDeleting: true }],
[types.default.SET_CUSTOM_VIEW_UI_FLAG, { isDeleting: false }],
]);
});
});
});

View file

@ -0,0 +1,42 @@
export default [
{
name: 'Custom view',
filter_type: 'conversation',
query: {
payload: [
{
attribute_key: 'assignee_id',
filter_operator: 'equal_to',
values: [45],
query_operator: 'and',
},
{
attribute_key: 'inbox_id',
filter_operator: 'equal_to',
values: [144],
query_operator: 'and',
},
],
},
},
{
name: 'Custom view 1',
filter_type: 'conversation',
query: {
payload: [
{
attribute_key: 'browser_language',
filter_operator: 'equal_to',
values: ['eng'],
query_operator: 'or',
},
{
attribute_key: 'campaign_id',
filter_operator: 'equal_to',
values: [15],
query_operator: 'and',
},
],
},
},
];

View file

@ -0,0 +1,65 @@
import { getters } from '../../customViews';
import customViewList from './fixtures';
describe('#getters', () => {
it('getCustomViews', () => {
const state = { records: customViewList };
expect(getters.getCustomViews(state)).toEqual([
{
name: 'Custom view',
filter_type: 'conversation',
query: {
payload: [
{
attribute_key: 'assignee_id',
filter_operator: 'equal_to',
values: [45],
query_operator: 'and',
},
{
attribute_key: 'inbox_id',
filter_operator: 'equal_to',
values: [144],
query_operator: 'and',
},
],
},
},
{
name: 'Custom view 1',
filter_type: 'conversation',
query: {
payload: [
{
attribute_key: 'browser_language',
filter_operator: 'equal_to',
values: ['eng'],
query_operator: 'or',
},
{
attribute_key: 'campaign_id',
filter_operator: 'equal_to',
values: [15],
query_operator: 'and',
},
],
},
},
]);
});
it('getUIFlags', () => {
const state = {
uiFlags: {
isFetching: true,
isCreating: false,
isDeleting: false,
},
};
expect(getters.getUIFlags(state)).toEqual({
isFetching: true,
isCreating: false,
isDeleting: false,
});
});
});

View file

@ -0,0 +1,29 @@
import types from '../../../mutation-types';
import { mutations } from '../../customViews';
import customViewList from './fixtures';
describe('#mutations', () => {
describe('#SET_CUSTOM_VIEW', () => {
it('set custom view records', () => {
const state = { records: [] };
mutations[types.SET_CUSTOM_VIEW](state, customViewList);
expect(state.records).toEqual(customViewList);
});
});
describe('#ADD_CUSTOM_VIEW', () => {
it('push newly created custom views to the store', () => {
const state = { records: [customViewList] };
mutations[types.ADD_CUSTOM_VIEW](state, customViewList[0]);
expect(state.records).toEqual([customViewList, customViewList[0]]);
});
});
describe('#DELETE_CUSTOM_VIEW', () => {
it('delete custom view record', () => {
const state = { records: [customViewList[0]] };
mutations[types.DELETE_CUSTOM_VIEW](state, customViewList[0]);
expect(state.records).toEqual([customViewList[0]]);
});
});
});

View file

@ -188,4 +188,10 @@ export default {
ADD_CUSTOM_ATTRIBUTE: 'ADD_CUSTOM_ATTRIBUTE',
EDIT_CUSTOM_ATTRIBUTE: 'EDIT_CUSTOM_ATTRIBUTE',
DELETE_CUSTOM_ATTRIBUTE: 'DELETE_CUSTOM_ATTRIBUTE',
// Custom Views
SET_CUSTOM_VIEW_UI_FLAG: 'SET_CUSTOM_VIEW_UI_FLAG',
SET_CUSTOM_VIEW: 'SET_CUSTOM_VIEW',
ADD_CUSTOM_VIEW: 'ADD_CUSTOM_VIEW',
DELETE_CUSTOM_VIEW: 'DELETE_CUSTOM_VIEW',
};

View file

@ -71,6 +71,7 @@
"power-outline": "M8.204 4.82a.75.75 0 0 1 .634 1.36A7.51 7.51 0 0 0 4.5 12.991c0 4.148 3.358 7.51 7.499 7.51s7.499-3.362 7.499-7.51a7.51 7.51 0 0 0-4.323-6.804.75.75 0 1 1 .637-1.358 9.01 9.01 0 0 1 5.186 8.162c0 4.976-4.029 9.01-9 9.01C7.029 22 3 17.966 3 12.99a9.01 9.01 0 0 1 5.204-8.17ZM12 2.496a.75.75 0 0 1 .743.648l.007.102v7.5a.75.75 0 0 1-1.493.102l-.007-.102v-7.5a.75.75 0 0 1 .75-.75Z",
"quote-outline": "M7.5 6a2.5 2.5 0 0 1 2.495 2.336l.005.206c-.01 3.555-1.24 6.614-3.705 9.223a.75.75 0 1 1-1.09-1.03c1.64-1.737 2.66-3.674 3.077-5.859A2.5 2.5 0 1 1 7.5 6Zm9 0a2.5 2.5 0 0 1 2.495 2.336l.005.206c-.01 3.56-1.238 6.614-3.705 9.223a.75.75 0 1 1-1.09-1.03c1.643-1.738 2.662-3.672 3.078-5.859A2.5 2.5 0 1 1 16.5 6Zm-9 1.5a1 1 0 1 0 .993 1.117l.007-.124a1 1 0 0 0-1-.993Zm9 0a1 1 0 1 0 .993 1.117l.007-.124a1 1 0 0 0-1-.993Z",
"resize-large-outline": "M6.25 4.5A1.75 1.75 0 0 0 4.5 6.25v1.5a.75.75 0 0 1-1.5 0v-1.5A3.25 3.25 0 0 1 6.25 3h1.5a.75.75 0 0 1 0 1.5h-1.5ZM19.5 6.25a1.75 1.75 0 0 0-1.75-1.75h-1.5a.75.75 0 0 1 0-1.5h1.5A3.25 3.25 0 0 1 21 6.25v1.5a.75.75 0 0 1-1.5 0v-1.5ZM19.5 17.75a1.75 1.75 0 0 1-1.75 1.75h-1.5a.75.75 0 0 0 0 1.5h1.5A3.25 3.25 0 0 0 21 17.75v-1.5a.75.75 0 0 0-1.5 0v1.5ZM4.5 17.75c0 .966.784 1.75 1.75 1.75h1.5a.75.75 0 0 1 0 1.5h-1.5A3.25 3.25 0 0 1 3 17.75v-1.5a.75.75 0 0 1 1.5 0v1.5ZM8.25 6A2.25 2.25 0 0 0 6 8.25v7.5A2.25 2.25 0 0 0 8.25 18h7.5A2.25 2.25 0 0 0 18 15.75v-7.5A2.25 2.25 0 0 0 15.75 6h-7.5ZM7.5 8.25a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 .75.75v7.5a.75.75 0 0 1-.75.75h-7.5a.75.75 0 0 1-.75-.75v-7.5Z",
"save-outline": "M3 5.75A2.75 2.75 0 0 1 5.75 3h9.964a3.25 3.25 0 0 1 2.299.952l2.035 2.035c.61.61.952 1.437.952 2.299v9.964A2.75 2.75 0 0 1 18.25 21H5.75A2.75 2.75 0 0 1 3 18.25V5.75ZM5.75 4.5c-.69 0-1.25.56-1.25 1.25v12.5c0 .69.56 1.25 1.25 1.25H6v-5.25A2.25 2.25 0 0 1 8.25 12h7.5A2.25 2.25 0 0 1 18 14.25v5.25h.25c.69 0 1.25-.56 1.25-1.25V8.286c0-.465-.184-.91-.513-1.238l-2.035-2.035a1.75 1.75 0 0 0-.952-.49V7.25a2.25 2.25 0 0 1-2.25 2.25h-4.5A2.25 2.25 0 0 1 7 7.25V4.5H5.75Zm10.75 15v-5.25a.75.75 0 0 0-.75-.75h-7.5a.75.75 0 0 0-.75.75v5.25h9Zm-8-15v2.75c0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75V4.5h-6Z",
"search-outline": "M10 2.75a7.25 7.25 0 0 1 5.63 11.819l4.9 4.9a.75.75 0 0 1-.976 1.134l-.084-.073-4.901-4.9A7.25 7.25 0 1 1 10 2.75Zm0 1.5a5.75 5.75 0 1 0 0 11.5 5.75 5.75 0 0 0 0-11.5Z",
"send-outline": "M5.694 12 2.299 3.272c-.236-.607.356-1.188.942-.982l.093.04 18 9a.75.75 0 0 1 .097 1.283l-.097.058-18 9c-.583.291-1.217-.244-1.065-.847l.03-.096L5.694 12 2.299 3.272 5.694 12ZM4.402 4.54l2.61 6.71h6.627a.75.75 0 0 1 .743.648l.007.102a.75.75 0 0 1-.649.743l-.101.007H7.01l-2.609 6.71L19.322 12 4.401 4.54Z",
"send-clock-outline": "M5.694 12 2.299 3.272c-.236-.608.356-1.189.942-.982l.093.04 18 9a.752.752 0 0 1 .264 1.124 6.473 6.473 0 0 0-4.272-1.452L4.402 4.54l2.61 6.71h6.627a.75.75 0 0 1 .724.556c-.472.26-.909.578-1.3.944H7.011l-2.609 6.71 6.753-3.377a6.522 6.522 0 0 0-.147 1.75l-7.674 3.838c-.583.291-1.217-.245-1.065-.847l.03-.096L5.694 12ZM23 17.5a5.5 5.5 0 1 0-11 0 5.5 5.5 0 0 0 11 0Zm-5.5 0h2a.5.5 0 1 1 0 1H17a.5.5 0 0 1-.5-.5v-3a.5.5 0 0 1 1 0v2.5Z",

View file

@ -135,9 +135,5 @@ export default {
color: $color-body;
}
}
.close-unread-wrap {
text-align: left;
}
}
</style>

View file

@ -32,7 +32,7 @@ class Account < ApplicationRecord
}.freeze
validates :name, presence: true
validates :auto_resolve_duration, numericality: { greater_than_or_equal_to: 1, allow_nil: true }
validates :auto_resolve_duration, numericality: { greater_than_or_equal_to: 1, less_than_or_equal_to: 999, allow_nil: true }
has_many :account_users, dependent: :destroy_async
has_many :agent_bot_inboxes, dependent: :destroy_async

View file

@ -4,7 +4,7 @@ module AssignmentHandler
included do
before_save :ensure_assignee_is_from_team
after_update :notify_assignment_change, :process_assignment_activities
after_commit :notify_assignment_change, :process_assignment_activities
end
private

View file

@ -71,6 +71,7 @@ class Notification < ApplicationRecord
end
# TODO: move to a data presenter
# rubocop:disable Metrics/CyclomaticComplexity
def push_message_title
case notification_type
when 'conversation_creation'
@ -81,14 +82,15 @@ class Notification < ApplicationRecord
I18n.t(
'notifications.notification_title.assigned_conversation_new_message',
display_id: conversation.display_id,
content: primary_actor.content&.truncate_words(10)
content: primary_actor&.content&.truncate_words(10)
)
when 'conversation_mention'
"[##{conversation.display_id}] #{transform_user_mention_content primary_actor.content}"
"[##{conversation&.display_id}] #{transform_user_mention_content primary_actor&.content}"
else
''
end
end
# rubocop:enable Metrics/CyclomaticComplexity
def conversation
return primary_actor.conversation if %w[assigned_conversation_new_message conversation_mention].include? notification_type

View file

@ -1,5 +1,5 @@
shared: &shared
version: '2.1.0'
version: '2.1.1'
development:
<<: *shared

View file

@ -1,6 +1,6 @@
{
"name": "@chatwoot/chatwoot",
"version": "2.1.0",
"version": "2.1.1",
"license": "MIT",
"scripts": {
"eslint": "eslint app/**/*.{js,vue} --fix",

View file

@ -5,6 +5,7 @@ require 'rails_helper'
RSpec.describe Account do
it { is_expected.to validate_presence_of(:name) }
it { is_expected.to validate_numericality_of(:auto_resolve_duration).is_greater_than_or_equal_to(1) }
it { is_expected.to validate_numericality_of(:auto_resolve_duration).is_less_than_or_equal_to(999) }
it { is_expected.to have_many(:users).through(:account_users) }
it { is_expected.to have_many(:account_users) }