Merge branch 'develop' into feat/new-auth-screens

This commit is contained in:
Sivin Varghese 2022-06-07 11:28:17 +05:30 committed by GitHub
commit cf719282aa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 698 additions and 34 deletions

View file

@ -156,11 +156,6 @@ group :test do
end end
group :development, :test do group :development, :test do
# TODO: is this needed ?
# errors thrown by devise password gem
gem 'flay'
gem 'rspec'
# for error thrown by devise password gem
gem 'active_record_query_trace' gem 'active_record_query_trace'
gem 'bundle-audit', require: false gem 'bundle-audit', require: false
gem 'byebug', platform: :mri gem 'byebug', platform: :mri

View file

@ -1,6 +1,6 @@
GIT GIT
remote: https://github.com/chatwoot/devise-secure_password remote: https://github.com/chatwoot/devise-secure_password
revision: de11e8765654b8242d42101ee9c8ffc8126f7975 revision: d777b04f12652d576b1272b8f39857e3e0b3fc26
specs: specs:
devise-secure_password (2.0.1) devise-secure_password (2.0.1)
devise (>= 4.0.0, < 5.0.0) devise (>= 4.0.0, < 5.0.0)
@ -182,7 +182,6 @@ GEM
regexp_parser (~> 2.2) regexp_parser (~> 2.2)
email_reply_trimmer (0.1.13) email_reply_trimmer (0.1.13)
erubi (1.10.0) erubi (1.10.0)
erubis (2.7.0)
et-orbi (1.2.7) et-orbi (1.2.7)
tzinfo tzinfo
execjs (2.8.1) execjs (2.8.1)
@ -204,11 +203,6 @@ GEM
faraday (~> 1) faraday (~> 1)
ffi (1.15.5) ffi (1.15.5)
flag_shih_tzu (0.3.23) flag_shih_tzu (0.3.23)
flay (2.12.1)
erubis (~> 2.7.0)
path_expander (~> 1.0)
ruby_parser (~> 3.0)
sexp_processor (~> 4.0)
foreman (0.87.2) foreman (0.87.2)
fugit (1.5.3) fugit (1.5.3)
et-orbi (~> 1, >= 1.2.7) et-orbi (~> 1, >= 1.2.7)
@ -393,7 +387,6 @@ GEM
parallel (1.21.0) parallel (1.21.0)
parser (3.1.1.0) parser (3.1.1.0)
ast (~> 2.4.1) ast (~> 2.4.1)
path_expander (1.1.0)
pg (1.3.2) pg (1.3.2)
procore-sift (0.16.0) procore-sift (0.16.0)
rails (> 4.2.0) rails (> 4.2.0)
@ -468,10 +461,6 @@ GEM
netrc (~> 0.8) netrc (~> 0.8)
retriable (3.1.2) retriable (3.1.2)
rexml (3.2.5) rexml (3.2.5)
rspec (3.11.0)
rspec-core (~> 3.11.0)
rspec-expectations (~> 3.11.0)
rspec-mocks (~> 3.11.0)
rspec-core (3.11.0) rspec-core (3.11.0)
rspec-support (~> 3.11.0) rspec-support (~> 3.11.0)
rspec-expectations (3.11.0) rspec-expectations (3.11.0)
@ -681,7 +670,6 @@ DEPENDENCIES
faker faker
fcm fcm
flag_shih_tzu flag_shih_tzu
flay
foreman foreman
geocoder geocoder
google-cloud-dialogflow google-cloud-dialogflow
@ -718,7 +706,6 @@ DEPENDENCIES
redis-namespace redis-namespace
responders responders
rest-client rest-client
rspec
rspec-rails (~> 5.0.0) rspec-rails (~> 5.0.0)
rubocop rubocop
rubocop-performance rubocop-performance
@ -755,4 +742,4 @@ RUBY VERSION
ruby 3.0.4p208 ruby 3.0.4p208
BUNDLED WITH BUNDLED WITH
2.3.10 2.3.14

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" :chat="chat"
:conversation-type="conversationType" :conversation-type="conversationType"
:show-assignee="showAssigneeInConversationCard" :show-assignee="showAssigneeInConversationCard"
:selected="isConversationSelected(chat.id)"
@select-conversation="selectConversation"
@de-select-conversation="deSelectConversation"
/> />
<div v-if="chatListLoading" class="text-center"> <div v-if="chatListLoading" class="text-center">
@ -134,6 +137,16 @@
@applyFilter="onApplyFilter" @applyFilter="onApplyFilter"
/> />
</woot-modal> </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> </div>
</template> </template>
@ -152,6 +165,8 @@ import advancedFilterTypes from './widgets/conversation/advancedFilterItems';
import filterQueryGenerator from '../helper/filterQueryGenerator.js'; import filterQueryGenerator from '../helper/filterQueryGenerator.js';
import AddCustomViews from 'dashboard/routes/dashboard/customviews/AddCustomViews'; import AddCustomViews from 'dashboard/routes/dashboard/customviews/AddCustomViews';
import DeleteCustomViews from 'dashboard/routes/dashboard/customviews/DeleteCustomViews.vue'; import DeleteCustomViews from 'dashboard/routes/dashboard/customviews/DeleteCustomViews.vue';
import ConversationBulkActions from './widgets/conversation/conversationBulkActions/Actions.vue';
import alertMixin from 'shared/mixins/alertMixin';
import { import {
hasPressedAltAndJKey, hasPressedAltAndJKey,
@ -166,8 +181,9 @@ export default {
ChatFilter, ChatFilter,
ConversationAdvancedFilter, ConversationAdvancedFilter,
DeleteCustomViews, DeleteCustomViews,
ConversationBulkActions,
}, },
mixins: [timeMixin, conversationMixin, eventListenerMixins], mixins: [timeMixin, conversationMixin, eventListenerMixins, alertMixin],
props: { props: {
conversationInbox: { conversationInbox: {
type: [String, Number], type: [String, Number],
@ -202,6 +218,8 @@ export default {
foldersQuery: {}, foldersQuery: {},
showAddFoldersModal: false, showAddFoldersModal: false,
showDeleteFoldersModal: false, showDeleteFoldersModal: false,
selectedConversations: [],
selectedInboxes: [],
}; };
}, },
computed: { computed: {
@ -217,6 +235,7 @@ export default {
conversationStats: 'conversationStats/getStats', conversationStats: 'conversationStats/getStats',
appliedFilters: 'getAppliedConversationFilters', appliedFilters: 'getAppliedConversationFilters',
folders: 'customViews/getCustomViews', folders: 'customViews/getCustomViews',
inboxes: 'inboxes/getInboxes',
}), }),
hasAppliedFilters() { hasAppliedFilters() {
return this.appliedFilters.length !== 0; return this.appliedFilters.length !== 0;
@ -343,6 +362,15 @@ export default {
} }
return {}; return {};
}, },
allConversationsSelected() {
return (
JSON.stringify(this.selectedConversations) ===
JSON.stringify(this.conversationList.map(item => item.id))
);
},
uniqueInboxes() {
return [...new Set(this.selectedInboxes)];
},
}, },
watch: { watch: {
activeTeam() { activeTeam() {
@ -376,6 +404,7 @@ export default {
if (this.$route.name !== 'home') { if (this.$route.name !== 'home') {
this.$router.push({ name: 'home' }); this.$router.push({ name: 'home' });
} }
this.resetBulkActions();
this.foldersQuery = filterQueryGenerator(payload); this.foldersQuery = filterQueryGenerator(payload);
this.$store.dispatch('conversationPage/reset'); this.$store.dispatch('conversationPage/reset');
this.$store.dispatch('emptyAllConversations'); this.$store.dispatch('emptyAllConversations');
@ -441,6 +470,7 @@ export default {
} }
}, },
resetAndFetchData() { resetAndFetchData() {
this.resetBulkActions();
this.$store.dispatch('conversationPage/reset'); this.$store.dispatch('conversationPage/reset');
this.$store.dispatch('emptyAllConversations'); this.$store.dispatch('emptyAllConversations');
this.$store.dispatch('clearConversationFilters'); this.$store.dispatch('clearConversationFilters');
@ -491,6 +521,7 @@ export default {
}, },
updateAssigneeTab(selectedTab) { updateAssigneeTab(selectedTab) {
if (this.activeAssigneeTab !== selectedTab) { if (this.activeAssigneeTab !== selectedTab) {
this.resetBulkActions();
bus.$emit('clearSearchInput'); bus.$emit('clearSearchInput');
this.activeAssigneeTab = selectedTab; this.activeAssigneeTab = selectedTab;
if (!this.currentPage) { if (!this.currentPage) {
@ -498,6 +529,10 @@ export default {
} }
} }
}, },
resetBulkActions() {
this.selectedConversations = [];
this.selectedInboxes = [];
},
updateStatusType(index) { updateStatusType(index) {
if (this.activeStatus !== index) { if (this.activeStatus !== index) {
this.activeStatus = index; this.activeStatus = index;
@ -520,6 +555,59 @@ export default {
this.fetchConversations(); 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> </script>
@ -535,7 +623,7 @@ export default {
.conversations-list-wrap { .conversations-list-wrap {
flex-shrink: 0; flex-shrink: 0;
width: 34rem; width: 34rem;
overflow: hidden;
@include breakpoint(large up) { @include breakpoint(large up) {
width: 36rem; width: 36rem;
} }

View file

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

View file

@ -5,11 +5,24 @@
active: isActiveChat, active: isActiveChat,
'unread-chat': hasUnread, 'unread-chat': hasUnread,
'has-inbox-name': showInboxName, 'has-inbox-name': showInboxName,
'conversation-selected': selected,
}" }"
@mouseenter="onCardHover"
@mouseleave="onCardLeave"
@click="cardClick(chat)" @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 <thumbnail
v-if="!hideThumbnail" v-if="bulkActionCheck"
:src="currentContact.thumbnail" :src="currentContact.thumbnail"
:badge="inboxBadge" :badge="inboxBadge"
class="columns" class="columns"
@ -142,8 +155,16 @@ export default {
type: String, type: String,
default: '', default: '',
}, },
selected: {
type: Boolean,
default: false,
},
},
data() {
return {
hovered: false,
};
}, },
computed: { computed: {
...mapGetters({ ...mapGetters({
currentChat: 'getSelectedChat', currentChat: 'getSelectedChat',
@ -152,7 +173,9 @@ export default {
currentUser: 'getCurrentUser', currentUser: 'getCurrentUser',
accountId: 'getCurrentAccountId', accountId: 'getCurrentAccountId',
}), }),
bulkActionCheck() {
return !this.hideThumbnail && !this.hovered && !this.selected;
},
chatMetadata() { chatMetadata() {
return this.chat.meta || {}; return this.chat.meta || {};
}, },
@ -260,6 +283,16 @@ export default {
} }
router.push({ path: frontendURL(path) }); 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> </script>
@ -272,6 +305,10 @@ export default {
} }
} }
.conversation-selected {
background: var(--color-background-light);
}
.has-inbox-name { .has-inbox-name {
&::v-deep .user-thumbnail-box { &::v-deep .user-thumbnail-box {
margin-top: var(--space-normal); margin-top: var(--space-normal);
@ -320,4 +357,22 @@ export default {
margin-top: var(--space-minus-micro); margin-top: var(--space-minus-micro);
vertical-align: middle; 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> </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 _settings } from './settings.json';
import { default as _signup } from './signup.json'; import { default as _signup } from './signup.json';
import { default as _teamsSettings } from './teamsSettings.json'; import { default as _teamsSettings } from './teamsSettings.json';
import { default as _bulkActions } from './bulkActions.json';
export default { export default {
..._advancedFilters, ..._advancedFilters,
@ -46,4 +47,5 @@ export default {
..._settings, ..._settings,
..._signup, ..._signup,
..._teamsSettings, ..._teamsSettings,
..._bulkActions,
}; };

View file

@ -6,6 +6,7 @@ import agents from './modules/agents';
import attributes from './modules/attributes'; import attributes from './modules/attributes';
import auth from './modules/auth'; import auth from './modules/auth';
import automations from './modules/automations'; import automations from './modules/automations';
import bulkActions from './modules/bulkActions';
import campaigns from './modules/campaigns'; import campaigns from './modules/campaigns';
import cannedResponse from './modules/cannedResponse'; import cannedResponse from './modules/cannedResponse';
import contactConversations from './modules/contactConversations'; import contactConversations from './modules/contactConversations';
@ -43,6 +44,7 @@ export default new Vuex.Store({
attributes, attributes,
auth, auth,
automations, automations,
bulkActions,
campaigns, campaigns,
cannedResponse, cannedResponse,
contactConversations, 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 = { export const actions = {
async fetch({ commit }, { inboxId }) { async fetch({ commit }, inboxIds) {
commit(types.SET_INBOX_ASSIGNABLE_AGENTS_UI_FLAG, { isFetching: true }); commit(types.SET_INBOX_ASSIGNABLE_AGENTS_UI_FLAG, { isFetching: true });
try { try {
const { const {
data: { payload }, data: { payload },
} = await AssignableAgentsAPI.get([inboxId]); } = await AssignableAgentsAPI.get(inboxIds);
commit(types.SET_INBOX_ASSIGNABLE_AGENTS, { inboxId, members: payload }); commit(types.SET_INBOX_ASSIGNABLE_AGENTS, {
inboxId: inboxIds.join(','),
members: payload,
});
} catch (error) { } catch (error) {
throw new Error(error); throw new Error(error);
} finally { } 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({ axios.get.mockResolvedValue({
data: { payload: agentsData }, data: { payload: agentsData },
}); });
await actions.fetch({ commit }, { inboxId: 1 }); await actions.fetch({ commit }, [1]);
expect(commit.mock.calls).toEqual([ expect(commit.mock.calls).toEqual([
[types.SET_INBOX_ASSIGNABLE_AGENTS_UI_FLAG, { isFetching: true }], [types.SET_INBOX_ASSIGNABLE_AGENTS_UI_FLAG, { isFetching: true }],
[ [
types.SET_INBOX_ASSIGNABLE_AGENTS, types.SET_INBOX_ASSIGNABLE_AGENTS,
{ inboxId: 1, members: agentsData }, { inboxId: '1', members: agentsData },
], ],
[types.SET_INBOX_ASSIGNABLE_AGENTS_UI_FLAG, { isFetching: false }], [types.SET_INBOX_ASSIGNABLE_AGENTS_UI_FLAG, { isFetching: false }],
]); ]);

View file

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

View file

@ -8,4 +8,9 @@
0 4px 6px -2px rgba(0, 0, 0, 0.05); 0 4px 6px -2px rgba(0, 0, 0, 0.05);
--shadow-larger: 0 20px 25px -5px rgba(0, 0, 0, 0.1), --shadow-larger: 0 20px 25px -5px rgba(0, 0, 0, 0.1),
0 10px 10px -5px rgba(0, 0, 0, 0.04); 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

@ -99,6 +99,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-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", "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-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", "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",
"person-account-outline": "M11 15c0-.35.06-.687.17-1H4.253a2.249 2.249 0 0 0-2.249 2.249v.578c0 .892.319 1.756.899 2.435 1.566 1.834 3.952 2.74 7.098 2.74.397 0 .783-.015 1.156-.044A2.998 2.998 0 0 1 11 21v-.535c-.321.024-.655.036-1 .036-2.738 0-4.704-.746-5.958-2.213a2.25 2.25 0 0 1-.539-1.462v-.577c0-.414.336-.75.75-.75H11V15ZM10 2.005a5 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-7ZM12 15a2 2 0 0 1 2-2h7a2 2 0 0 1 2 2v6a2 2 0 0 1-2 2h-7a2 2 0 0 1-2-2v-6Zm2.5 1a.5.5 0 0 0 0 1h6a.5.5 0 0 0 0-1h-6Zm0 3a.5.5 0 0 0 0 1h6a.5.5 0 0 0 0-1h-6Z", "person-account-outline": "M11 15c0-.35.06-.687.17-1H4.253a2.249 2.249 0 0 0-2.249 2.249v.578c0 .892.319 1.756.899 2.435 1.566 1.834 3.952 2.74 7.098 2.74.397 0 .783-.015 1.156-.044A2.998 2.998 0 0 1 11 21v-.535c-.321.024-.655.036-1 .036-2.738 0-4.704-.746-5.958-2.213a2.25 2.25 0 0 1-.539-1.462v-.577c0-.414.336-.75.75-.75H11V15ZM10 2.005a5 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-7ZM12 15a2 2 0 0 1 2-2h7a2 2 0 0 1 2 2v6a2 2 0 0 1-2 2h-7a2 2 0 0 1-2-2v-6Zm2.5 1a.5.5 0 0 0 0 1h6a.5.5 0 0 0 0-1h-6Zm0 3a.5.5 0 0 0 0 1h6a.5.5 0 0 0 0-1h-6Z",
"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", "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",

View file

@ -179,7 +179,8 @@ function main() {
Chatwoot Installation (latest) 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 EOF
@ -199,7 +200,7 @@ EOF
echo "***** Skipping Postgres and Redis installation. ****" echo "***** Skipping Postgres and Redis installation. ****"
fi 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}" install_dependencies &>> "${LOG_FILE}"
if [ "$install_pg_redis" != "no" ] 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 as part of the installation process. After modifying the environment
variables (in the .env file) with your external database credentials, run variables (in the .env file) with your external database credentials, run
the database migrations using the below command. 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 EOF
fi fi