Merge branch 'develop' into feat/custom-attrs-automations

This commit is contained in:
Fayaz Ahmed 2022-06-03 11:31:10 +05:30 committed by GitHub
commit a9ca6bbacb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 718 additions and 27 deletions

View file

@ -0,0 +1,9 @@
import ApiClient from './ApiClient';
class BulkActionsAPI extends ApiClient {
constructor() {
super('bulk_actions', { accountScoped: true });
}
}
export default new BulkActionsAPI();

View file

@ -0,0 +1,9 @@
import bulkActions from '../bulkActions';
import ApiClient from '../ApiClient';
describe('#BulkActionsAPI', () => {
it('creates correct instance', () => {
expect(bulkActions).toBeInstanceOf(ApiClient);
expect(bulkActions).toHaveProperty('create');
});
});

View file

@ -96,6 +96,9 @@
:chat="chat"
:conversation-type="conversationType"
:show-assignee="showAssigneeInConversationCard"
:selected="isConversationSelected(chat.id)"
@select-conversation="selectConversation"
@de-select-conversation="deSelectConversation"
/>
<div v-if="chatListLoading" class="text-center">
@ -134,6 +137,16 @@
@applyFilter="onApplyFilter"
/>
</woot-modal>
<conversation-bulk-actions
v-if="selectedConversations.length"
:conversations="selectedConversations"
:all-conversations-selected="allConversationsSelected"
:selected-inboxes="uniqueInboxes"
@select-all-conversations="selectAllConversations"
@assign-agent="onAssignAgent"
@resolve-conversations="onResolveConversations"
/>
</div>
</template>
@ -152,6 +165,8 @@ 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 ConversationBulkActions from './widgets/conversation/conversationBulkActions/Actions.vue';
import alertMixin from 'shared/mixins/alertMixin';
import {
hasPressedAltAndJKey,
@ -166,8 +181,9 @@ export default {
ChatFilter,
ConversationAdvancedFilter,
DeleteCustomViews,
ConversationBulkActions,
},
mixins: [timeMixin, conversationMixin, eventListenerMixins],
mixins: [timeMixin, conversationMixin, eventListenerMixins, alertMixin],
props: {
conversationInbox: {
type: [String, Number],
@ -202,6 +218,8 @@ export default {
foldersQuery: {},
showAddFoldersModal: false,
showDeleteFoldersModal: false,
selectedConversations: [],
selectedInboxes: [],
};
},
computed: {
@ -217,6 +235,7 @@ export default {
conversationStats: 'conversationStats/getStats',
appliedFilters: 'getAppliedConversationFilters',
folders: 'customViews/getCustomViews',
inboxes: 'inboxes/getInboxes',
}),
hasAppliedFilters() {
return this.appliedFilters.length !== 0;
@ -343,6 +362,15 @@ export default {
}
return {};
},
allConversationsSelected() {
return (
JSON.stringify(this.selectedConversations) ===
JSON.stringify(this.conversationList.map(item => item.id))
);
},
uniqueInboxes() {
return [...new Set(this.selectedInboxes)];
},
},
watch: {
activeTeam() {
@ -376,6 +404,7 @@ export default {
if (this.$route.name !== 'home') {
this.$router.push({ name: 'home' });
}
this.resetBulkActions();
this.foldersQuery = filterQueryGenerator(payload);
this.$store.dispatch('conversationPage/reset');
this.$store.dispatch('emptyAllConversations');
@ -441,6 +470,7 @@ export default {
}
},
resetAndFetchData() {
this.resetBulkActions();
this.$store.dispatch('conversationPage/reset');
this.$store.dispatch('emptyAllConversations');
this.$store.dispatch('clearConversationFilters');
@ -491,6 +521,7 @@ export default {
},
updateAssigneeTab(selectedTab) {
if (this.activeAssigneeTab !== selectedTab) {
this.resetBulkActions();
bus.$emit('clearSearchInput');
this.activeAssigneeTab = selectedTab;
if (!this.currentPage) {
@ -498,6 +529,10 @@ export default {
}
}
},
resetBulkActions() {
this.selectedConversations = [];
this.selectedInboxes = [];
},
updateStatusType(index) {
if (this.activeStatus !== index) {
this.activeStatus = index;
@ -520,6 +555,59 @@ export default {
this.fetchConversations();
}
},
isConversationSelected(id) {
return this.selectedConversations.includes(id);
},
selectConversation(conversationId, inboxId) {
this.selectedConversations.push(conversationId);
this.selectedInboxes.push(inboxId);
},
deSelectConversation(conversationId, inboxId) {
this.selectedConversations = this.selectedConversations.filter(
item => item !== conversationId
);
this.selectedInboxes = this.selectedInboxes.filter(
item => item !== inboxId
);
},
selectAllConversations(check) {
if (check) {
this.selectedConversations = this.conversationList.map(item => item.id);
this.selectedInboxes = this.conversationList.map(item => item.inbox_id);
} else {
this.resetBulkActions();
}
},
async onAssignAgent(agent) {
try {
await this.$store.dispatch('bulkActions/process', {
type: 'Conversation',
ids: this.selectedConversations,
fields: {
assignee_id: agent.id,
},
});
this.selectedConversations = [];
this.showAlert(this.$t('BULK_ACTION.ASSIGN_SUCCESFUL'));
} catch (err) {
this.showAlert(this.$t('BULK_ACTION.ASSIGN_FAILED'));
}
},
async onResolveConversations() {
try {
await this.$store.dispatch('bulkActions/process', {
type: 'Conversation',
ids: this.selectedConversations,
fields: {
status: 'resolved',
},
});
this.selectedConversations = [];
this.showAlert(this.$t('BULK_ACTION.RESOLVE_SUCCESFUL'));
} catch (error) {
this.showAlert(this.$t('BULK_ACTION.RESOLVE_FAILED'));
}
},
},
};
</script>
@ -535,7 +623,7 @@ export default {
.conversations-list-wrap {
flex-shrink: 0;
width: 34rem;
overflow: hidden;
@include breakpoint(large up) {
width: 36rem;
}

View file

@ -99,7 +99,7 @@ export default {
watch: {
'currentChat.inbox_id'(inboxId) {
if (inboxId) {
this.$store.dispatch('inboxAssignableAgents/fetch', { inboxId });
this.$store.dispatch('inboxAssignableAgents/fetch', [inboxId]);
}
},
'currentChat.id'() {

View file

@ -5,11 +5,24 @@
active: isActiveChat,
'unread-chat': hasUnread,
'has-inbox-name': showInboxName,
'conversation-selected': selected,
}"
@mouseenter="onCardHover"
@mouseleave="onCardLeave"
@click="cardClick(chat)"
>
<label v-if="hovered || selected" class="checkbox-wrapper">
<input
:value="selected"
:checked="selected"
class="checkbox"
type="checkbox"
@change="onSelectConversation($event.target.checked)"
@click.stop
/>
</label>
<thumbnail
v-if="!hideThumbnail"
v-if="bulkActionCheck"
:src="currentContact.thumbnail"
:badge="inboxBadge"
class="columns"
@ -142,8 +155,16 @@ export default {
type: String,
default: '',
},
selected: {
type: Boolean,
default: false,
},
},
data() {
return {
hovered: false,
};
},
computed: {
...mapGetters({
currentChat: 'getSelectedChat',
@ -152,7 +173,9 @@ export default {
currentUser: 'getCurrentUser',
accountId: 'getCurrentAccountId',
}),
bulkActionCheck() {
return !this.hideThumbnail && !this.hovered && !this.selected;
},
chatMetadata() {
return this.chat.meta || {};
},
@ -260,6 +283,16 @@ export default {
}
router.push({ path: frontendURL(path) });
},
onCardHover() {
this.hovered = !this.hideThumbnail;
},
onCardLeave() {
this.hovered = false;
},
onSelectConversation(checked) {
const action = checked ? 'select-conversation' : 'de-select-conversation';
this.$emit(action, this.chat.id, this.inbox.id);
},
},
};
</script>
@ -272,6 +305,10 @@ export default {
}
}
.conversation-selected {
background: var(--color-background-light);
}
.has-inbox-name {
&::v-deep .user-thumbnail-box {
margin-top: var(--space-normal);
@ -320,4 +357,22 @@ export default {
margin-top: var(--space-minus-micro);
vertical-align: middle;
}
.checkbox-wrapper {
height: 40px;
width: 40px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 100%;
margin-top: var(--space-normal);
cursor: pointer;
&:hover {
background-color: var(--w-100);
}
input[type='checkbox'] {
margin: var(--space-zero);
cursor: pointer;
}
}
</style>

View file

@ -0,0 +1,135 @@
<template>
<div class="bulk-action__container">
<div class="flex-between">
<label class="bulk-action__panel flex-between">
<input
ref="selectAllCheck"
type="checkbox"
class="checkbox"
:checked="allConversationsSelected"
@change="selectAll($event)"
/>
<span>
{{
$t('BULK_ACTION.CONVERSATIONS_SELECTED', {
conversationCount: conversations.length,
})
}}
</span>
</label>
<div class="bulk-action__actions flex-between">
<woot-button
v-tooltip="$t('BULK_ACTION.RESOLVE_TOOLTIP')"
size="tiny"
variant="flat"
color-scheme="success"
icon="checkmark"
class="margin-right-smaller"
@click="resolveConversations"
/>
<woot-button
v-tooltip="$t('BULK_ACTION.ASSIGN_AGENT_TOOLTIP')"
size="tiny"
variant="flat"
color-scheme="secondary"
icon="person-assign"
@click="showAgentsList = true"
/>
</div>
<transition name="menu-slide">
<agent-selector
v-if="showAgentsList"
:selected-inboxes="selectedInboxes"
:conversation-count="conversations.length"
@select="submit"
@close="showAgentsList = false"
/>
</transition>
</div>
<div v-if="allConversationsSelected" class="bulk-action__alert">
{{ $t('BULK_ACTION.ALL_CONVERSATIONS_SELECTED_ALERT') }}
</div>
</div>
</template>
<script>
import AgentSelector from './AgentSelector.vue';
export default {
components: {
AgentSelector,
},
props: {
conversations: {
type: Array,
default: () => [],
},
allConversationsSelected: {
type: Boolean,
default: false,
},
selectedInboxes: {
type: Array,
default: () => [],
},
},
data() {
return {
showAgentsList: false,
};
},
mounted() {
this.$refs.selectAllCheck.indeterminate = true;
},
methods: {
selectAll(e) {
this.$emit('select-all-conversations', e.target.checked);
},
submit(agent) {
this.$emit('assign-agent', agent);
},
resolveConversations() {
this.$emit('resolve-conversations');
},
},
};
</script>
<style scoped lang="scss">
.flex-between {
align-items: center;
display: flex;
justify-content: space-between;
}
.bulk-action__container {
background-color: var(--s-50);
border-top: 1px solid var(--s-100);
box-shadow: var(--shadow-bulk-action-container);
padding: var(--space-normal) var(--space-one);
position: relative;
}
.bulk-action__panel {
cursor: pointer;
span {
font-size: var(--font-size-mini);
margin-left: var(--space-smaller);
}
input[type='checkbox'] {
cursor: pointer;
margin: var(--space-zero);
}
}
.bulk-action__alert {
background-color: var(--y-50);
border-radius: var(--border-radius-small);
border: 1px solid var(--y-300);
color: var(--y-700);
font-size: var(--font-size-mini);
margin-top: var(--space-small);
padding: var(--space-half) var(--space-one);
}
</style>

View file

@ -0,0 +1,249 @@
<template>
<div class="bulk-action__agents">
<div class="header flex-between">
<span>{{ $t('BULK_ACTION.AGENT_SELECT_LABEL') }}</span>
<woot-button
size="tiny"
variant="clear"
color-scheme="secondary"
icon="dismiss"
@click="onClose"
/>
</div>
<div class="container">
<div v-if="uiFlags.isUpdating" class="agent__list-loading">
<spinner />
<p>{{ $t('BULK_ACTION.AGENT_LIST_LOADING') }}</p>
</div>
<div v-else class="agent__list-container">
<ul v-if="!selectedAgent">
<li class="search-container">
<div class="agent-list-search flex-between">
<fluent-icon icon="search" class="search-icon" size="16" />
<input
ref="search"
v-model="query"
type="search"
placeholder="Search"
class="agent--search_input"
/>
</div>
</li>
<li v-for="agent in filteredAgents" :key="agent.id">
<div class="agent-list-item" @click="assignAgent(agent)">
<thumbnail
src="agent.thumbnail"
:username="agent.name"
size="22px"
class="margin-right-small"
/>
<span class="reports-option__title">{{ agent.name }}</span>
</div>
</li>
</ul>
<div v-else class="agent-confirmation-container">
<p>
{{
$t('BULK_ACTION.ASSIGN_CONFIRMATION_LABEL', {
conversationCount,
conversationLabel,
})
}}
<strong>
{{ selectedAgent.name }}
</strong>
</p>
<div class="agent-confirmation-actions">
<woot-button
color-scheme="primary"
variant="smooth"
@click="goBack"
>
{{ $t('BULK_ACTION.GO_BACK_LABEL') }}
</woot-button>
<woot-button
color-scheme="primary"
variant="flat"
:is-loading="uiFlags.isUpdating"
@click="submit"
>
{{ $t('BULK_ACTION.ASSIGN_LABEL') }}
</woot-button>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
import Spinner from 'shared/components/Spinner';
import { mixin as clickaway } from 'vue-clickaway';
export default {
components: {
Thumbnail,
Spinner,
},
mixins: [clickaway],
props: {
selectedInboxes: {
type: Array,
default: () => [],
},
conversationCount: {
type: Number,
default: 0,
},
},
data() {
return {
query: '',
selectedAgent: null,
};
},
computed: {
...mapGetters({
uiFlags: 'bulkActions/getUIFlags',
inboxes: 'inboxes/getInboxes',
assignableAgentsUiFlags: 'inboxAssignableAgents/getUIFlags',
}),
filteredAgents() {
if (this.query) {
return this.assignableAgents.filter(agent =>
agent.name.toLowerCase().includes(this.query.toLowerCase())
);
}
return this.assignableAgents;
},
assignableAgents() {
return this.$store.getters['inboxAssignableAgents/getAssignableAgents'](
this.selectedInboxes.join(',')
);
},
conversationLabel() {
return this.conversationCount > 1 ? 'conversations' : 'conversation';
},
},
mounted() {
this.$store.dispatch('inboxAssignableAgents/fetch', this.selectedInboxes);
},
methods: {
submit() {
this.$emit('select', this.selectedAgent);
},
goBack() {
this.selectedAgent = null;
},
assignAgent(agent) {
this.selectedAgent = agent;
},
onClose() {
this.$emit('close');
},
},
};
</script>
<style scoped lang="scss">
.flex-between {
display: flex;
align-items: center;
justify-content: space-between;
}
.bulk-action__agents {
position: absolute;
bottom: 40px;
right: var(--space-small);
width: 100%;
box-shadow: var(--shadow-dropdown-pane);
border-radius: var(--border-radius-large);
border: 1px solid var(--s-50);
background-color: var(--white);
width: 75%;
.header {
padding: var(--space-one);
span {
font-size: var(--font-size-default);
font-weight: var(--font-weight-medium);
}
}
.container {
height: 240px;
overflow-y: auto;
.agent__list-container {
height: 100%;
}
.agent-list-search {
padding: 0 var(--space-one);
border: 1px solid var(--s-100);
border-radius: var(--border-radius-medium);
background-color: var(--s-50);
.search-icon {
color: var(--s-400);
}
.agent--search_input {
border: 0;
font-size: var(--font-size-mini);
margin: 0;
background-color: transparent;
height: unset;
}
}
}
}
ul {
margin: 0;
list-style: none;
}
.agent-list-item {
display: flex;
align-items: center;
padding: var(--space-one);
cursor: pointer;
&:hover {
background-color: var(--s-50);
}
span {
font-size: var(--font-size-small);
}
}
.agent-confirmation-container {
display: flex;
flex-direction: column;
height: 100%;
padding: var(--space-one);
p {
flex-grow: 1;
}
.agent-confirmation-actions {
width: 100%;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: var(--space-one);
}
}
.search-container {
padding: 0 var(--space-one);
position: sticky;
top: 0;
z-index: var(--z-index-twenty);
background-color: var(--white);
}
.agent__list-loading {
height: calc(95% - var(--space-one));
margin: var(--space-one);
border-radius: var(--border-radius-medium);
background-color: var(--s-50);
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
}
</style>

View file

@ -0,0 +1,17 @@
{
"BULK_ACTION": {
"CONVERSATIONS_SELECTED": "%{conversationCount} conversations selected",
"AGENT_SELECT_LABEL": "Select Agent",
"ASSIGN_CONFIRMATION_LABEL": "Are you sure you want to assign %{conversationCount} %{conversationLabel} to",
"GO_BACK_LABEL": "Go back",
"ASSIGN_LABEL": "Assign",
"ASSIGN_AGENT_TOOLTIP": "Assign Agent",
"RESOLVE_TOOLTIP": "Resolve",
"ASSIGN_SUCCESFUL": "Conversations assigned successfully",
"ASSIGN_FAILED": "Failed to assign conversations, please try again",
"RESOLVE_SUCCESFUL": "Conversations resolved successfully",
"RESOLVE_FAILED": "Failed to resolve conversations, please try again",
"ALL_CONVERSATIONS_SELECTED_ALERT": "Conversations visible on this page are only selected.",
"AGENT_LIST_LOADING": "Loading Agents"
}
}

View file

@ -21,6 +21,7 @@ import { default as _setNewPassword } from './setNewPassword.json';
import { default as _settings } from './settings.json';
import { default as _signup } from './signup.json';
import { default as _teamsSettings } from './teamsSettings.json';
import { default as _bulkActions } from './bulkActions.json';
export default {
..._advancedFilters,
@ -46,4 +47,5 @@ export default {
..._settings,
..._signup,
..._teamsSettings,
..._bulkActions,
};

View file

@ -21,7 +21,7 @@
},
"MESSAGE_SIGNATURE_SECTION": {
"TITLE": "Personal message signature",
"NOTE": "Create a personal message signature that would be added to all the messages you send from the platform. Use the rich content editor to create a highly personalised signature.",
"NOTE": "Create a personal message signature that would be added to all the messages you send from your email inbox. Use the rich content editor to create a highly personalised signature.",
"BTN_TEXT": "Save message signature",
"API_ERROR": "Couldn't save signature! Try again",
"API_SUCCESS": "Signature saved successfully"

View file

@ -6,6 +6,7 @@ import agents from './modules/agents';
import attributes from './modules/attributes';
import auth from './modules/auth';
import automations from './modules/automations';
import bulkActions from './modules/bulkActions';
import campaigns from './modules/campaigns';
import cannedResponse from './modules/cannedResponse';
import contactConversations from './modules/contactConversations';
@ -43,6 +44,7 @@ export default new Vuex.Store({
attributes,
auth,
automations,
bulkActions,
campaigns,
cannedResponse,
contactConversations,

View file

@ -0,0 +1,44 @@
import types from '../mutation-types';
import BulkActionsAPI from '../../api/bulkActions';
export const state = {
uiFlags: {
isUpdating: false,
},
};
export const getters = {
getUIFlags(_state) {
return _state.uiFlags;
},
};
export const actions = {
process: async function processAction({ commit }, payload) {
commit(types.SET_BULK_ACTIONS_FLAG, { isUpdating: true });
try {
await BulkActionsAPI.create(payload);
} catch (error) {
throw new Error(error);
} finally {
commit(types.SET_BULK_ACTIONS_FLAG, { isUpdating: false });
}
},
};
export const mutations = {
[types.SET_BULK_ACTIONS_FLAG](_state, data) {
_state.uiFlags = {
..._state.uiFlags,
...data,
};
},
};
export default {
namespaced: true,
actions,
state,
getters,
mutations,
};

View file

@ -26,13 +26,16 @@ export const getters = {
};
export const actions = {
async fetch({ commit }, { inboxId }) {
async fetch({ commit }, inboxIds) {
commit(types.SET_INBOX_ASSIGNABLE_AGENTS_UI_FLAG, { isFetching: true });
try {
const {
data: { payload },
} = await AssignableAgentsAPI.get([inboxId]);
commit(types.SET_INBOX_ASSIGNABLE_AGENTS, { inboxId, members: payload });
} = await AssignableAgentsAPI.get(inboxIds);
commit(types.SET_INBOX_ASSIGNABLE_AGENTS, {
inboxId: inboxIds.join(','),
members: payload,
});
} catch (error) {
throw new Error(error);
} finally {

View file

@ -0,0 +1,28 @@
import axios from 'axios';
import { actions } from '../../bulkActions';
import * as types from '../../../mutation-types';
import payload from './fixtures';
const commit = jest.fn();
global.axios = axios;
jest.mock('axios');
describe('#actions', () => {
describe('#create', () => {
it('sends correct actions if API is success', async () => {
axios.post.mockResolvedValue({ data: payload });
await actions.process({ commit }, payload);
expect(commit.mock.calls).toEqual([
[types.default.SET_BULK_ACTIONS_FLAG, { isUpdating: true }],
[types.default.SET_BULK_ACTIONS_FLAG, { isUpdating: false }],
]);
});
it('sends correct actions if API is error', async () => {
axios.post.mockRejectedValue({ message: 'Incorrect header' });
await expect(actions.process({ commit })).rejects.toThrow(Error);
expect(commit.mock.calls).toEqual([
[types.default.SET_BULK_ACTIONS_FLAG, { isUpdating: true }],
[types.default.SET_BULK_ACTIONS_FLAG, { isUpdating: false }],
]);
});
});
});

View file

@ -0,0 +1,5 @@
export default {
type: 'Conversation',
ids: [64, 39],
fields: { assignee_id: 6 },
};

View file

@ -0,0 +1,14 @@
import { getters } from '../../bulkActions';
describe('#getters', () => {
it('getUIFlags', () => {
const state = {
uiFlags: {
isUpdating: false,
},
};
expect(getters.getUIFlags(state)).toEqual({
isUpdating: false,
});
});
});

View file

@ -0,0 +1,12 @@
import types from '../../../mutation-types';
import { mutations } from '../../bulkActions';
describe('#mutations', () => {
describe('#toggleUiFlag', () => {
it('set update flags', () => {
const state = { uiFlags: { isUpdating: false } };
mutations[types.SET_BULK_ACTIONS_FLAG](state, { isUpdating: true });
expect(state.uiFlags.isUpdating).toEqual(true);
});
});
});

View file

@ -12,12 +12,12 @@ describe('#actions', () => {
axios.get.mockResolvedValue({
data: { payload: agentsData },
});
await actions.fetch({ commit }, { inboxId: 1 });
await actions.fetch({ commit }, [1]);
expect(commit.mock.calls).toEqual([
[types.SET_INBOX_ASSIGNABLE_AGENTS_UI_FLAG, { isFetching: true }],
[
types.SET_INBOX_ASSIGNABLE_AGENTS,
{ inboxId: 1, members: agentsData },
{ inboxId: '1', members: agentsData },
],
[types.SET_INBOX_ASSIGNABLE_AGENTS_UI_FLAG, { isFetching: false }],
]);

View file

@ -211,6 +211,9 @@ export default {
ADD_CUSTOM_VIEW: 'ADD_CUSTOM_VIEW',
DELETE_CUSTOM_VIEW: 'DELETE_CUSTOM_VIEW',
// Bulk Actions
SET_BULK_ACTIONS_FLAG: 'SET_BULK_ACTIONS_FLAG',
// Dashboard Apps
SET_DASHBOARD_APPS_UI_FLAG: 'SET_DASHBOARD_APPS_UI_FLAG',
SET_DASHBOARD_APPS: 'SET_DASHBOARD_APPS',

View file

@ -7,7 +7,7 @@
--w-75: #D6EBFF;
--w-100: #C2E1FF;
--w-200: #99CEFF;
--w-300: ##70BAFF;
--w-300: #70BAFF;
--w-400: #47A6FF;
--w-500: #1F93FF;
--w-600: #1976CC;

View file

@ -8,4 +8,9 @@
0 4px 6px -2px rgba(0, 0, 0, 0.05);
--shadow-larger: 0 20px 25px -5px rgba(0, 0, 0, 0.1),
0 10px 10px -5px rgba(0, 0, 0, 0.04);
--shadow-dropdown-pane:
0 0.8rem 1.6rem rgb(50 50 93 / 8%),
0 0.4rem 1.2rem rgb(0 0 0 / 7%);
--shadow-bulk-action-container:
6px 3px 22px 9px rgb(181 181 181 / 25%);
}

View file

@ -98,6 +98,7 @@
"people-outline": "M4 13.999 13 14a2 2 0 0 1 1.995 1.85L15 16v1.5C14.999 21 11.284 22 8.5 22c-2.722 0-6.335-.956-6.495-4.27L2 17.5v-1.501c0-1.054.816-1.918 1.85-1.995L4 14ZM15.22 14H20c1.054 0 1.918.816 1.994 1.85L22 16v1c-.001 3.062-2.858 4-5 4a7.16 7.16 0 0 1-2.14-.322c.336-.386.607-.827.802-1.327A6.19 6.19 0 0 0 17 19.5l.267-.006c.985-.043 3.086-.363 3.226-2.289L20.5 17v-1a.501.501 0 0 0-.41-.492L20 15.5h-4.051a2.957 2.957 0 0 0-.595-1.34L15.22 14H20h-4.78ZM4 15.499l-.1.01a.51.51 0 0 0-.254.136.506.506 0 0 0-.136.253l-.01.101V17.5c0 1.009.45 1.722 1.417 2.242.826.445 2.003.714 3.266.753l.317.005.317-.005c1.263-.039 2.439-.308 3.266-.753.906-.488 1.359-1.145 1.412-2.057l.005-.186V16a.501.501 0 0 0-.41-.492L13 15.5l-9-.001ZM8.5 3a4.5 4.5 0 1 1 0 9 4.5 4.5 0 0 1 0-9Zm9 2a3.5 3.5 0 1 1 0 7 3.5 3.5 0 0 1 0-7Zm-9-.5c-1.654 0-3 1.346-3 3s1.346 3 3 3 3-1.346 3-3-1.346-3-3-3Zm9 2c-1.103 0-2 .897-2 2s.897 2 2 2 2-.897 2-2-.897-2-2-2Z",
"people-team-outline": "M14.754 10c.966 0 1.75.784 1.75 1.75v4.749a4.501 4.501 0 0 1-9.002 0V11.75c0-.966.783-1.75 1.75-1.75h5.502Zm0 1.5H9.252a.25.25 0 0 0-.25.25v4.749a3.001 3.001 0 0 0 6.002 0V11.75a.25.25 0 0 0-.25-.25ZM3.75 10h3.381a2.738 2.738 0 0 0-.618 1.5H3.75a.25.25 0 0 0-.25.25v3.249a2.501 2.501 0 0 0 3.082 2.433c.085.504.24.985.453 1.432A4.001 4.001 0 0 1 2 14.999V11.75c0-.966.784-1.75 1.75-1.75Zm13.125 0h3.375c.966 0 1.75.784 1.75 1.75V15a4 4 0 0 1-5.03 3.866c.214-.448.369-.929.455-1.433A2.5 2.5 0 0 0 20.5 15v-3.25a.25.25 0 0 0-.25-.25h-2.757a2.738 2.738 0 0 0-.618-1.5ZM12 3a3 3 0 1 1 0 6 3 3 0 0 1 0-6Zm6.5 1a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5Zm-13 0a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5Zm6.5.5a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3Zm6.5 1a1 1 0 1 0 0 2 1 1 0 0 0 0-2Zm-13 0a1 1 0 1 0 0 2 1 1 0 0 0 0-2Z",
"person-add-outline": "M17.5 12a5.5 5.5 0 1 1 0 11a5.5 5.5 0 0 1 0-11zm-5.477 2a6.47 6.47 0 0 0-.709 1.5H4.253a.749.749 0 0 0-.75.75v.577c0 .535.192 1.053.54 1.46c1.253 1.469 3.22 2.214 5.957 2.214c.597 0 1.157-.035 1.68-.106c.246.495.553.954.912 1.367c-.795.16-1.66.24-2.592.24c-3.146 0-5.532-.906-7.098-2.74a3.75 3.75 0 0 1-.898-2.435v-.578A2.249 2.249 0 0 1 4.253 14h7.77zm5.477 0l-.09.008a.5.5 0 0 0-.402.402L17 14.5V17h-2.496l-.09.008a.5.5 0 0 0-.402.402l-.008.09l.008.09a.5.5 0 0 0 .402.402l.09.008H17L17 20.5l.008.09a.5.5 0 0 0 .402.402l.09.008l.09-.008a.5.5 0 0 0 .402-.402L18 20.5V18h2.504l.09-.008a.5.5 0 0 0 .402-.402l.008-.09l-.008-.09a.5.5 0 0 0-.402-.402l-.09-.008H18L18 14.5l-.008-.09a.5.5 0 0 0-.402-.402L17.5 14zM10 2.005a5 5 0 1 1 0 10a5 5 0 0 1 0-10zm0 1.5a3.5 3.5 0 1 0 0 7a3.5 3.5 0 0 0 0-7z",
"person-assign-outline": "M11.313 15.5a6.471 6.471 0 0 1 .709-1.5h-7.77a2.249 2.249 0 0 0-2.249 2.25v.577c0 .892.319 1.756.899 2.435c1.566 1.834 3.952 2.74 7.098 2.74c.931 0 1.796-.08 2.592-.24a6.51 6.51 0 0 1-.913-1.366c-.524.07-1.083.105-1.68.105c-2.737 0-4.703-.745-5.957-2.213a2.25 2.25 0 0 1-.539-1.461v-.578a.75.75 0 0 1 .75-.749h7.06ZM10 2.005a5 5 0 1 1 0 10a5 5 0 0 1 0-10Zm0 1.5a3.5 3.5 0 1 0 0 7a3.5 3.5 0 0 0 0-7ZM23 17.5a5.5 5.5 0 1 1-11 0a5.5 5.5 0 0 1 11 0Zm-4.647-2.853a.5.5 0 0 0-.707.707L19.293 17H15a.5.5 0 1 0 0 1h4.293l-1.647 1.647a.5.5 0 0 0 .707.707l2.5-2.5a.497.497 0 0 0 .147-.345V17.5a.498.498 0 0 0-.15-.357l-2.497-2.496Z",
"person-outline": "M17.754 14a2.249 2.249 0 0 1 2.25 2.249v.575c0 .894-.32 1.76-.902 2.438-1.57 1.834-3.957 2.739-7.102 2.739-3.146 0-5.532-.905-7.098-2.74a3.75 3.75 0 0 1-.898-2.435v-.577a2.249 2.249 0 0 1 2.249-2.25h11.501Zm0 1.5H6.253a.749.749 0 0 0-.75.749v.577c0 .536.192 1.054.54 1.461 1.253 1.468 3.219 2.214 5.957 2.214s4.706-.746 5.962-2.214a2.25 2.25 0 0 0 .541-1.463v-.575a.749.749 0 0 0-.749-.75ZM12 2.004a5 5 0 1 1 0 10 5 5 0 0 1 0-10Zm0 1.5a3.5 3.5 0 1 0 0 7 3.5 3.5 0 0 0 0-7Z",
"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",

View file

@ -6,13 +6,12 @@ class Inboxes::FetchImapEmailsJob < ApplicationJob
def perform(channel)
return unless should_fetch_email?(channel)
process_mail_for_channel(channel)
fetch_mail_for_channel(channel)
# clearing old failures like timeouts since the mail is now successfully processed
channel.reauthorized!
rescue Errno::ECONNREFUSED, Net::OpenTimeout, Net::IMAP::NoResponseError
channel.authorization_error!
rescue StandardError => e
channel.authorization_error!
ChatwootExceptionTracker.new(e, account: channel.account).capture_exception
end
@ -22,7 +21,7 @@ class Inboxes::FetchImapEmailsJob < ApplicationJob
channel.imap_enabled? && !channel.reauthorization_required?
end
def process_mail_for_channel(channel)
def fetch_mail_for_channel(channel)
# TODO: rather than setting this as default method for all mail objects, lets if can do new mail object
# using Mail.retriever_method.new(params)
Mail.defaults do
@ -36,12 +35,18 @@ class Inboxes::FetchImapEmailsJob < ApplicationJob
new_mails = false
Mail.find(what: :last, count: 10, order: :desc).each do |inbound_mail|
if inbound_mail.date.utc >= channel.imap_inbox_synced_at
Imap::ImapMailbox.new.process(inbound_mail, channel)
new_mails = true
end
next unless inbound_mail.date.utc >= channel.imap_inbox_synced_at
process_mail(inbound_mail, channel)
new_mails = true
end
channel.update(imap_inbox_synced_at: Time.now.utc) if new_mails
end
def process_mail(inbound_mail, channel)
Imap::ImapMailbox.new.process(inbound_mail, channel)
rescue StandardError => e
ChatwootExceptionTracker.new(e, account: channel.account).capture_exception
end
end

View file

@ -179,7 +179,8 @@ function main() {
Chatwoot Installation (latest)
***************************************************************************
For more verbose logs, open up a second terminal and follow along using, `tail -f /var/log/chatwoot`.
For more verbose logs, open up a second terminal and follow along using,
'tail -f /var/log/chatwoot'.
EOF
@ -199,7 +200,7 @@ EOF
echo "***** Skipping Postgres and Redis installation. ****"
fi
echo -en "\n➥ 1/9 Installing dependencies. This takes a while."
echo -en "\n➥ 1/9 Installing dependencies. This takes a while.\n"
install_dependencies &>> "${LOG_FILE}"
if [ "$install_pg_redis" != "no" ]
@ -273,7 +274,7 @@ The database migrations had not run as Postgres and Redis were not installed
as part of the installation process. After modifying the environment
variables (in the .env file) with your external database credentials, run
the database migrations using the below command.
`RAILS_ENV=production bundle exec rails db:chatwoot_prepare`.
'RAILS_ENV=production bundle exec rails db:chatwoot_prepare'.
***************************************************************************
EOF
fi

View file

@ -12,8 +12,11 @@ class ChatwootExceptionTracker
end
def capture_exception
capture_exception_with_sentry if ENV['SENTRY_DSN'].present?
# Implement other providers like honeybadger, rollbar etc in future
if ENV['SENTRY_DSN'].present?
capture_exception_with_sentry
else
Rails.logger.error @exception
end
end
private

View file

@ -1,8 +1,9 @@
require 'rails_helper'
describe ChatwootExceptionTracker do
it 'returns nil if no tracker is configured' do
expect(described_class.new('random').capture_exception).to eq(nil)
it 'use rails logger if no tracker is configured' do
expect(Rails.logger).to receive(:error).with('random')
described_class.new('random').capture_exception
end
context 'with sentry DSN' do