mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-02 03:57:52 +00:00
Merge branch 'develop' into chore/update-rails
This commit is contained in:
78
.github/ISSUE_TEMPLATE/bug_report.md
vendored
78
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,78 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: 'Bug'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See the error
|
||||
|
||||
**Expected behavior**
|
||||
|
||||
Share a clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Browser logs**
|
||||
|
||||
Share the browser logs to debug the issue further.
|
||||
|
||||
**Server logs**
|
||||
|
||||
Share the server logs to debug the issue further.
|
||||
|
||||
**Environment**
|
||||
|
||||
Describe whether you are using Chatwoot Cloud (app.chatwoot.com) or a self-hosted installation of Chatwoot. If you are using a self-hosted installation of Chatwoot, describe the type of deployment (Docker/Linux VM installation/Heroku/Kubernetes/Other).
|
||||
|
||||
- [ ] app.chatwoot.com (Chatwoot Cloud)
|
||||
- [ ] Self-hosted
|
||||
- - [ ] Linux VM
|
||||
- - [ ] Docker
|
||||
- - [ ] Kubernetes
|
||||
- - [ ] Heroku
|
||||
- - [ ] Other (Please specify)
|
||||
|
||||
|
||||
**Desktop (please complete the following information)** (If applicable)
|
||||
- OS: [e.g. Linux, Windows, MacOS]
|
||||
- Browser [e.g. chrome, firefox, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Smartphone (please complete the following information)** (If applicable)
|
||||
- Device: [e.g. iPhone6, Pixel7]
|
||||
- OS: [e.g. iOS8.1]
|
||||
- Browser [e.g. stock browser, firefox, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Docker** (If applicable)
|
||||
|
||||
Please share the output of the following.
|
||||
- `docker version`
|
||||
- `docker info`
|
||||
- `docker-compose version`
|
||||
|
||||
**Cloud Provider** (If applicable)
|
||||
- [ ] AWS
|
||||
- [ ] GCP
|
||||
- [ ] Azure
|
||||
- [ ] DigitalOcean
|
||||
- [ ] Others
|
||||
|
||||
**Additional context**
|
||||
|
||||
Add any other context about the problem here.
|
||||
78
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
78
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,78 @@
|
||||
name: 🐞 Bug report
|
||||
description: Create a report to help us improve
|
||||
labels: 'Bug'
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the bug
|
||||
description: A concise description of what you expected to happen along with screenshots if applicable.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: To Reproduce
|
||||
description: Steps to reproduce the behavior.
|
||||
placeholder: |
|
||||
1. In this environment...
|
||||
2. With this config...
|
||||
3. Run '...'
|
||||
4. See error...
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Expected behavior
|
||||
description: A concise description of what you expected to happen.
|
||||
- type: dropdown
|
||||
id: environment
|
||||
attributes:
|
||||
label: Environment
|
||||
description: Describe whether you are using Chatwoot Cloud (app.chatwoot.com) or a self-hosted installation of Chatwoot. If you are using a self-hosted installation of Chatwoot, describe the type of deployment (Docker/Linux VM installation/Heroku/Kubernetes/Other).
|
||||
options:
|
||||
- app.chatwoot.com
|
||||
- Linux VM
|
||||
- Docker
|
||||
- Kubernetes
|
||||
- Heroku
|
||||
- Other [please specify in the description]
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: provider
|
||||
attributes:
|
||||
label: Cloud Provider
|
||||
description:
|
||||
options:
|
||||
- AWS
|
||||
- GCP
|
||||
- Azure
|
||||
- DigitalOcean
|
||||
- Other [please specify in the description]
|
||||
- type: dropdown
|
||||
id: platform
|
||||
attributes:
|
||||
label: Platform
|
||||
description: Describe the platform you are using
|
||||
options:
|
||||
- Browser
|
||||
- Mobile
|
||||
- type: input
|
||||
attributes:
|
||||
label: Operating system
|
||||
description: The operating system and the version you are using.
|
||||
- type: input
|
||||
attributes:
|
||||
label: Browser and version
|
||||
description: The name of the browser and version you are using.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Docker (if applicable)
|
||||
description: |
|
||||
Please share the output of the following.
|
||||
- `docker version`
|
||||
- `docker info`
|
||||
- `docker-compose version`
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add any other context about the problem here.
|
||||
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Report a security issue
|
||||
url: https://www.chatwoot.com/docs/contributing-guide/security-reports/
|
||||
about: Guidelines and steps to report a security vulnerability. Please report security vulnerabilities here.
|
||||
- name: Product Documentation
|
||||
url: https://www.chatwoot.com/help-center
|
||||
about: If you have questions, are confused, or just want to understand our product better, please check out our documentation.
|
||||
20
.github/ISSUE_TEMPLATE/enhancement_request.md
vendored
20
.github/ISSUE_TEMPLATE/enhancement_request.md
vendored
@@ -1,20 +0,0 @@
|
||||
---
|
||||
name: Enhancement request
|
||||
about: Suggest any enhancements for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your enhancement request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the enhancement request here.
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,20 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
28
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
28
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
name: 🧙 Feature request
|
||||
description: Suggest an idea for this project
|
||||
labels: 'feature-request'
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Is your feature or enhancement related to a problem? Please describe.
|
||||
description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the solution you'd like
|
||||
description: A clear and concise description of what you want to happen.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe alternatives you've considered
|
||||
description: A clear and concise description of any alternative solutions or features you've considered.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add any other context or screenshots about the feature request here.
|
||||
validations:
|
||||
required: false
|
||||
@@ -1,2 +1,5 @@
|
||||
class AccountDrop < BaseDrop
|
||||
def name
|
||||
@obj.try(:name)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -10,4 +10,26 @@ module EmailHelper
|
||||
def normalize_email_with_plus_addressing(email)
|
||||
"#{email.split('@').first.split('+').first}@#{email.split('@').last}".downcase
|
||||
end
|
||||
|
||||
def parse_email_variables(conversation, email)
|
||||
case email
|
||||
when modified_liquid_content(email)
|
||||
template = Liquid::Template.parse(modified_liquid_content(email))
|
||||
template.render(message_drops(conversation))
|
||||
when URI::MailTo::EMAIL_REGEXP
|
||||
email
|
||||
end
|
||||
end
|
||||
|
||||
def modified_liquid_content(email)
|
||||
# This regex is used to match the code blocks in the content
|
||||
# We don't want to process liquid in code blocks
|
||||
email.gsub(/`(.*?)`/m, '{% raw %}`\\1`{% endraw %}')
|
||||
end
|
||||
|
||||
def message_drops(conversation)
|
||||
{
|
||||
'contact' => ContactDrop.new(conversation.contact)
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -443,11 +443,11 @@ export default {
|
||||
.conversation-metadata-attributes {
|
||||
display: flex;
|
||||
gap: var(--space-small);
|
||||
margin-left: var(--space-small);
|
||||
}
|
||||
|
||||
.assignee-label {
|
||||
display: inline-flex;
|
||||
margin-left: var(--space-small);
|
||||
max-width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,7 +93,9 @@ import { mapGetters } from 'vuex';
|
||||
|
||||
import ReplyBox from './ReplyBox';
|
||||
import Message from './Message';
|
||||
import conversationMixin from '../../../mixins/conversations';
|
||||
import conversationMixin, {
|
||||
filterDuplicateSourceMessages,
|
||||
} from '../../../mixins/conversations';
|
||||
import Banner from 'dashboard/components/ui/Banner.vue';
|
||||
import { getTypingUsersText } from '../../../helper/commons';
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
@@ -171,24 +173,28 @@ export default {
|
||||
|
||||
return '';
|
||||
},
|
||||
|
||||
getMessages() {
|
||||
const [chat] = this.allConversations.filter(
|
||||
c => c.id === this.currentChat.id
|
||||
);
|
||||
return chat;
|
||||
const messages = this.currentChat.messages || [];
|
||||
if (this.isAWhatsAppChannel) {
|
||||
return filterDuplicateSourceMessages(messages);
|
||||
}
|
||||
return messages;
|
||||
},
|
||||
getReadMessages() {
|
||||
const chat = this.getMessages;
|
||||
return chat === undefined ? null : this.readMessages(chat);
|
||||
return this.readMessages(
|
||||
this.getMessages,
|
||||
this.currentChat.agent_last_seen_at
|
||||
);
|
||||
},
|
||||
getUnReadMessages() {
|
||||
const chat = this.getMessages;
|
||||
return chat === undefined ? null : this.unReadMessages(chat);
|
||||
return this.unReadMessages(
|
||||
this.getMessages,
|
||||
this.currentChat.agent_last_seen_at
|
||||
);
|
||||
},
|
||||
shouldShowSpinner() {
|
||||
return (
|
||||
(this.getMessages && this.getMessages.dataFetched === undefined) ||
|
||||
(this.currentChat && this.currentChat.dataFetched === undefined) ||
|
||||
(!this.listLoadingStatus && this.isLoadingPrevious)
|
||||
);
|
||||
},
|
||||
@@ -208,7 +214,7 @@ export default {
|
||||
|
||||
selectedTweet() {
|
||||
if (this.selectedTweetId) {
|
||||
const { messages = [] } = this.getMessages;
|
||||
const { messages = [] } = this.currentChat;
|
||||
const [selectedMessage] = messages.filter(
|
||||
message => message.id === this.selectedTweetId
|
||||
);
|
||||
@@ -360,7 +366,7 @@ export default {
|
||||
async fetchPreviousMessages(scrollTop = 0) {
|
||||
this.setScrollParams();
|
||||
const shouldLoadMoreMessages =
|
||||
this.getMessages.dataFetched === true &&
|
||||
this.currentChat.dataFetched === true &&
|
||||
!this.listLoadingStatus &&
|
||||
!this.isLoadingPrevious;
|
||||
|
||||
@@ -373,7 +379,7 @@ export default {
|
||||
try {
|
||||
await this.$store.dispatch('fetchPreviousMessages', {
|
||||
conversationId: this.currentChat.id,
|
||||
before: this.getMessages.messages[0].id,
|
||||
before: this.currentChat.messages[0].id,
|
||||
});
|
||||
const heightDifference =
|
||||
this.conversationPanel.scrollHeight - this.heightBeforeLoad;
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
"EMPTY_STATE": "No %{item} found for query '%{query}'",
|
||||
"EMPTY_STATE_FULL": "No results found for query '%{query}'",
|
||||
"PLACEHOLDER_KEYBINDING": "/ to focus",
|
||||
"INPUT_PLACEHOLDER": "Search messages, contacts or conversations",
|
||||
"EMPTY_STATE_DEFAULT": "Search by conversation id, email, phone number, messages for better search results.",
|
||||
"INPUT_PLACEHOLDER": "Type 3 or more characters to search",
|
||||
"EMPTY_STATE_DEFAULT": "Search by conversation id, email, phone number, messages for better search results. ",
|
||||
"BOT_LABEL": "Bot",
|
||||
"READ_MORE": "Read more",
|
||||
"WROTE": "wrote:"
|
||||
|
||||
@@ -12,6 +12,26 @@ const getLastNonActivityMessage = (messageInStore, messageFromAPI) => {
|
||||
return messageInStore || messageFromAPI;
|
||||
};
|
||||
|
||||
export const filterDuplicateSourceMessages = (messages = []) => {
|
||||
const messagesWithoutDuplicates = [];
|
||||
// We cannot use Map or any short hand method as it returns the last message with the duplicate ID
|
||||
// We should return the message with smaller id when there is a duplicate
|
||||
messages.forEach(m1 => {
|
||||
if (m1.source_id) {
|
||||
if (
|
||||
messagesWithoutDuplicates.findIndex(
|
||||
m2 => m1.source_id === m2.source_id
|
||||
) < 0
|
||||
) {
|
||||
messagesWithoutDuplicates.push(m1);
|
||||
}
|
||||
} else {
|
||||
messagesWithoutDuplicates.push(m1);
|
||||
}
|
||||
});
|
||||
return messagesWithoutDuplicates;
|
||||
};
|
||||
|
||||
export default {
|
||||
methods: {
|
||||
lastMessage(m) {
|
||||
@@ -34,14 +54,14 @@ export default {
|
||||
lastNonActivityMessageFromAPI
|
||||
);
|
||||
},
|
||||
readMessages(m) {
|
||||
return m.messages.filter(
|
||||
chat => chat.created_at * 1000 <= m.agent_last_seen_at * 1000
|
||||
readMessages(messages, agentLastSeenAt) {
|
||||
return messages.filter(
|
||||
message => message.created_at * 1000 <= agentLastSeenAt * 1000
|
||||
);
|
||||
},
|
||||
unReadMessages(m) {
|
||||
return m.messages.filter(
|
||||
chat => chat.created_at * 1000 > m.agent_last_seen_at * 1000
|
||||
unReadMessages(messages, agentLastSeenAt) {
|
||||
return messages.filter(
|
||||
message => message.created_at * 1000 > agentLastSeenAt * 1000
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,17 +1,50 @@
|
||||
import conversationMixin from '../conversations';
|
||||
import conversationMixin, {
|
||||
filterDuplicateSourceMessages,
|
||||
} from '../conversations';
|
||||
import conversationFixture from './conversationFixtures';
|
||||
import commonHelpers from '../../helper/commons';
|
||||
commonHelpers();
|
||||
|
||||
describe('#filterDuplicateSourceMessages', () => {
|
||||
it('returns messages without duplicate source_id and all messages without source_id', () => {
|
||||
expect(
|
||||
filterDuplicateSourceMessages([
|
||||
{ source_id: null, id: 1 },
|
||||
{ source_id: '', id: 2 },
|
||||
{ id: 3 },
|
||||
{ source_id: 'wa_1', id: 4 },
|
||||
{ source_id: 'wa_1', id: 5 },
|
||||
{ source_id: 'wa_1', id: 6 },
|
||||
{ source_id: 'wa_2', id: 7 },
|
||||
{ source_id: 'wa_2', id: 8 },
|
||||
{ source_id: 'wa_3', id: 9 },
|
||||
])
|
||||
).toEqual([
|
||||
{ source_id: null, id: 1 },
|
||||
{ source_id: '', id: 2 },
|
||||
{ id: 3 },
|
||||
{ source_id: 'wa_1', id: 4 },
|
||||
{ source_id: 'wa_2', id: 7 },
|
||||
{ source_id: 'wa_3', id: 9 },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#conversationMixin', () => {
|
||||
it('should return read messages if conversation is passed', () => {
|
||||
expect(
|
||||
conversationMixin.methods.readMessages(conversationFixture.conversation)
|
||||
conversationMixin.methods.readMessages(
|
||||
conversationFixture.conversation.messages,
|
||||
conversationFixture.conversation.agent_last_seen_at
|
||||
)
|
||||
).toEqual(conversationFixture.readMessages);
|
||||
});
|
||||
it('should return read messages if conversation is passed', () => {
|
||||
expect(
|
||||
conversationMixin.methods.unReadMessages(conversationFixture.conversation)
|
||||
conversationMixin.methods.unReadMessages(
|
||||
conversationFixture.conversation.messages,
|
||||
conversationFixture.conversation.agent_last_seen_at
|
||||
)
|
||||
).toEqual(conversationFixture.unReadMessages);
|
||||
});
|
||||
|
||||
|
||||
@@ -18,12 +18,23 @@ export default {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
selectedTab: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
activeTab: 0,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
selectedTab(value, oldValue) {
|
||||
if (value !== oldValue) {
|
||||
this.activeTab = this.selectedTab;
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onTabChange(index) {
|
||||
this.activeTab = index;
|
||||
|
||||
@@ -17,11 +17,12 @@
|
||||
<search-tabs
|
||||
v-if="query"
|
||||
:tabs="tabs"
|
||||
:selected-tab="activeTabIndex"
|
||||
@tab-change="tab => (selectedTab = tab)"
|
||||
/>
|
||||
</header>
|
||||
<div class="search-results">
|
||||
<div v-if="all.length">
|
||||
<div v-if="showResultsSection">
|
||||
<search-result-contacts-list
|
||||
v-if="filterContacts"
|
||||
:is-fetching="uiFlags.contact.isFetching"
|
||||
@@ -46,7 +47,7 @@
|
||||
:show-title="isSelectedTabAll"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="showEmptySearchResults && !all.length" class="empty">
|
||||
<div v-else-if="showEmptySearchResults" class="empty">
|
||||
<fluent-icon icon="info" size="16px" class="icon" />
|
||||
<p class="empty-state__text">
|
||||
{{ $t('SEARCH.EMPTY_STATE_FULL', { query }) }}
|
||||
@@ -157,9 +158,23 @@ export default {
|
||||
},
|
||||
];
|
||||
},
|
||||
activeTabIndex() {
|
||||
const index = this.tabs.findIndex(tab => tab.key === this.selectedTab);
|
||||
return index >= 0 ? index : 0;
|
||||
},
|
||||
showEmptySearchResults() {
|
||||
return (
|
||||
this.totalSearchResultsCount === 0 && this.uiFlags.isSearchCompleted
|
||||
this.totalSearchResultsCount === 0 &&
|
||||
this.uiFlags.isSearchCompleted &&
|
||||
!this.uiFlags.isFetching &&
|
||||
this.query
|
||||
);
|
||||
},
|
||||
showResultsSection() {
|
||||
return (
|
||||
(this.uiFlags.isSearchCompleted &&
|
||||
this.totalSearchResultsCount !== 0) ||
|
||||
this.uiFlags.isFetching
|
||||
);
|
||||
},
|
||||
isSelectedTabAll() {
|
||||
@@ -175,6 +190,7 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
onSearch(q) {
|
||||
this.selectedTab = 'all';
|
||||
this.query = q;
|
||||
if (!q) {
|
||||
this.$store.dispatch('conversationSearch/clearSearchResults');
|
||||
|
||||
@@ -17,7 +17,16 @@
|
||||
<div v-if="isBusinessHoursEnabled" class="business-hours-wrap">
|
||||
<label class="unavailable-input-wrap">
|
||||
{{ $t('INBOX_MGMT.BUSINESS_HOURS.UNAVAILABLE_MESSAGE_LABEL') }}
|
||||
<textarea v-model="unavailableMessage" type="text" />
|
||||
<label v-if="isRichEditorEnabled" class="richtext">
|
||||
<woot-message-editor
|
||||
v-model="unavailableMessage"
|
||||
:enable-variables="true"
|
||||
:is-format-mode="true"
|
||||
class="input"
|
||||
:min-height="4"
|
||||
/>
|
||||
</label>
|
||||
<textarea v-else v-model="unavailableMessage" type="text" />
|
||||
</label>
|
||||
<div class="timezone-input-wrap">
|
||||
<label>
|
||||
@@ -61,7 +70,9 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import inboxMixin from 'shared/mixins/inboxMixin';
|
||||
import SettingsSection from 'dashboard/components/SettingsSection';
|
||||
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor';
|
||||
import BusinessDay from './BusinessDay';
|
||||
import {
|
||||
timeSlotParse,
|
||||
@@ -79,8 +90,9 @@ export default {
|
||||
components: {
|
||||
SettingsSection,
|
||||
BusinessDay,
|
||||
WootMessageEditor,
|
||||
},
|
||||
mixins: [alertMixin],
|
||||
mixins: [alertMixin, inboxMixin],
|
||||
props: {
|
||||
inbox: {
|
||||
type: Object,
|
||||
@@ -115,6 +127,15 @@ export default {
|
||||
timeZones() {
|
||||
return [...timeZoneOptions()];
|
||||
},
|
||||
isRichEditorEnabled() {
|
||||
if (
|
||||
this.isATwilioChannel ||
|
||||
this.isATwitterInbox ||
|
||||
this.isAFacebookInbox
|
||||
)
|
||||
return false;
|
||||
return true;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
inbox() {
|
||||
@@ -190,4 +211,11 @@ export default {
|
||||
.business-hours-wrap {
|
||||
margin-bottom: var(--space-medium);
|
||||
}
|
||||
|
||||
.richtext {
|
||||
padding: 0 var(--space-normal);
|
||||
border-radius: var(--border-radius-normal);
|
||||
border: 1px solid var(--color-border);
|
||||
margin: 0 0 var(--space-normal);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -61,9 +61,11 @@ export const actions = {
|
||||
isSearchCompleted: false,
|
||||
});
|
||||
try {
|
||||
dispatch('contactSearch', { q });
|
||||
dispatch('conversationSearch', { q });
|
||||
dispatch('messageSearch', { q });
|
||||
await Promise.all([
|
||||
dispatch('contactSearch', { q }),
|
||||
dispatch('conversationSearch', { q }),
|
||||
dispatch('messageSearch', { q }),
|
||||
]);
|
||||
} catch (error) {
|
||||
// Ignore error
|
||||
} finally {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<woot-message-editor
|
||||
v-model="greetingsMessage"
|
||||
:is-format-mode="true"
|
||||
:enable-variables="true"
|
||||
class="input"
|
||||
:placeholder="placeholder"
|
||||
:min-height="4"
|
||||
|
||||
@@ -13,12 +13,13 @@ module Liquidable
|
||||
'contact' => ContactDrop.new(conversation.contact),
|
||||
'agent' => UserDrop.new(sender),
|
||||
'conversation' => ConversationDrop.new(conversation),
|
||||
'inbox' => InboxDrop.new(inbox)
|
||||
'inbox' => InboxDrop.new(inbox),
|
||||
'account' => AccountDrop.new(conversation.account)
|
||||
}
|
||||
end
|
||||
|
||||
def liquid_processable_message?
|
||||
content.present? && message_type == 'outgoing'
|
||||
content.present? && (message_type == 'outgoing' || message_type == 'template')
|
||||
end
|
||||
|
||||
def process_liquid_in_content
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
class ActionService
|
||||
include EmailHelper
|
||||
|
||||
def initialize(conversation)
|
||||
@conversation = conversation.reload
|
||||
end
|
||||
@@ -59,6 +61,7 @@ class ActionService
|
||||
emails = emails[0].gsub(/\s+/, '').split(',')
|
||||
|
||||
emails.each do |email|
|
||||
email = parse_email_variables(@conversation, email)
|
||||
ConversationReplyMailer.with(account: @conversation.account).conversation_transcript(@conversation, email)&.deliver_later
|
||||
end
|
||||
end
|
||||
|
||||
@@ -199,24 +199,33 @@ contacts:
|
||||
conversations:
|
||||
- channel: Channel::WebWidget
|
||||
messages:
|
||||
- message_type: incoming
|
||||
content: hello world
|
||||
- message_type: incoming
|
||||
content: Hi, I'm having trouble logging in to my account.
|
||||
- message_type: outgoing
|
||||
sender: michael_scott@paperlayer.test
|
||||
content: Hi! Sorry to hear that. Can you please provide me with your username and email address so I can look into it for you?
|
||||
- name: "Tiffanie Cloughton"
|
||||
email: "tcloughton1@newyorker.test"
|
||||
gender: 'female'
|
||||
conversations:
|
||||
- channel: Channel::FacebookPage
|
||||
messages:
|
||||
- message_type: incoming
|
||||
content: hello world
|
||||
- message_type: incoming
|
||||
content: Hi, I need some help with my billing statement.
|
||||
- message_type: outgoing
|
||||
sender: michael_scott@paperlayer.test
|
||||
content: Hello! I'd be happy to assist you with that. Can you please tell me which billing statement you're referring to?
|
||||
- name: "Melonie Keatch"
|
||||
email: "mkeatch2@reuters.test"
|
||||
gender: 'female'
|
||||
conversations:
|
||||
- channel: Channel::TwitterProfile
|
||||
messages:
|
||||
- message_type: incoming
|
||||
content: hello world
|
||||
- message_type: incoming
|
||||
content: Hi, I think I accidentally deleted some important files. Can you help me recover them?
|
||||
- message_type: outgoing
|
||||
sender: michael_scott@paperlayer.test
|
||||
content: Of course! Can you please tell me what type of files they were and where they were located on your device?
|
||||
- name: "Olin Canniffe"
|
||||
email: "ocanniffe3@feedburner.test"
|
||||
gender: 'male'
|
||||
@@ -224,8 +233,11 @@ contacts:
|
||||
- channel: Channel::Whatsapp
|
||||
source_id: "123456723"
|
||||
messages:
|
||||
- message_type: incoming
|
||||
content: hello world
|
||||
- message_type: incoming
|
||||
content: Hi, I'm having trouble connecting to the internet.
|
||||
- message_type: outgoing
|
||||
sender: michael_scott@paperlayer.test
|
||||
content: I'm sorry to hear that. Have you tried restarting your modem/router? If that doesn't work, please let me know and I can provide further assistance.
|
||||
- name: "Viviene Corp"
|
||||
email: "vcorp4@instagram.test"
|
||||
gender: 'female'
|
||||
@@ -233,32 +245,44 @@ contacts:
|
||||
- channel: Channel::Sms
|
||||
source_id: "+1234567"
|
||||
messages:
|
||||
- message_type: incoming
|
||||
content: hello world
|
||||
- message_type: incoming
|
||||
content: Hi, I'm having trouble with the mobile app. It keeps crashing.
|
||||
- message_type: outgoing
|
||||
sender: michael_scott@paperlayer.test
|
||||
content: I'm sorry to hear that. Can you please try uninstalling and reinstalling the app and see if that helps? If not, please let me know and I can look into it further.
|
||||
- name: "Drake Pittway"
|
||||
email: "dpittway5@chron.test"
|
||||
gender: 'male'
|
||||
conversations:
|
||||
- channel: Channel::Line
|
||||
messages:
|
||||
- message_type: incoming
|
||||
content: hello world
|
||||
- message_type: incoming
|
||||
content: Hi, I'm trying to update my account information but it won't save.
|
||||
- message_type: outgoing
|
||||
sender: michael_scott@paperlayer.test
|
||||
content: Sorry for the inconvenience. Can you please provide me with the specific information you're trying to update and the error message you're receiving?
|
||||
- name: "Klaus Crawley"
|
||||
email: "kcrawley6@narod.ru"
|
||||
gender: 'male'
|
||||
conversations:
|
||||
- channel: Channel::WebWidget
|
||||
messages:
|
||||
- message_type: incoming
|
||||
content: hello world
|
||||
- message_type: incoming
|
||||
content: Hi, I need some help setting up my new device.
|
||||
- message_type: outgoing
|
||||
sender: michael_scott@paperlayer.test
|
||||
content: No problem! Can you please tell me the make and model of your device and what specifically you need help with?
|
||||
- name: "Bing Cusworth"
|
||||
email: "bcusworth7@arstechnica.test"
|
||||
gender: 'male'
|
||||
conversations:
|
||||
- channel: Channel::TwitterProfile
|
||||
messages:
|
||||
- message_type: incoming
|
||||
content: hello world
|
||||
- message_type: incoming
|
||||
content: Hi, I accidentally placed an order for the wrong item. Can I cancel it?
|
||||
- message_type: outgoing
|
||||
sender: michael_scott@paperlayer.test
|
||||
content: I'm sorry to hear that. Can you please provide me with your order number and I'll see if I can cancel it for you?
|
||||
- name: "Claus Jira"
|
||||
email: "cjira8@comcast.net"
|
||||
gender: 'male'
|
||||
@@ -266,8 +290,11 @@ contacts:
|
||||
- channel: Channel::Whatsapp
|
||||
source_id: "12323432"
|
||||
messages:
|
||||
- message_type: incoming
|
||||
content: hello world
|
||||
- message_type: incoming
|
||||
content: Hi, I'm having trouble with my email. I can't seem to send or receive any messages.
|
||||
- message_type: outgoing
|
||||
sender: michael_scott@paperlayer.test
|
||||
content: I'm sorry to hear that. Can you please tell me what email client you're using and if you're receiving any error messages?
|
||||
- name: "Quent Dalliston"
|
||||
email: "qdalliston9@zimbio.test"
|
||||
gender: 'male'
|
||||
@@ -275,24 +302,33 @@ contacts:
|
||||
- channel: Channel::Whatsapp
|
||||
source_id: "12342234324"
|
||||
messages:
|
||||
- message_type: incoming
|
||||
content: hello world
|
||||
- message_type: incoming
|
||||
content: Hi, I need some help resetting my password.
|
||||
- message_type: outgoing
|
||||
sender: michael_scott@paperlayer.test
|
||||
content: Sure! Can you please provide me with your username or email address and I'll send you a password reset link?
|
||||
- name: "Coreen Mewett"
|
||||
email: "cmewetta@home.pl"
|
||||
gender: 'female'
|
||||
conversations:
|
||||
- channel: Channel::FacebookPage
|
||||
messages:
|
||||
- message_type: incoming
|
||||
content: hello world
|
||||
- message_type: incoming
|
||||
content: Hi, I think someone may have hacked into my account. What should I do?
|
||||
- message_type: outgoing
|
||||
sender: michael_scott@paperlayer.test
|
||||
content: I'm sorry to hear that. Please change your password immediately and enable two-factor authentication if you haven't already done so. I can also assist you in reviewing your account activity if needed.
|
||||
- name: "Benyamin Janeway"
|
||||
email: "bjanewayb@ustream.tv"
|
||||
gender: 'male'
|
||||
conversations:
|
||||
- channel: Channel::Line
|
||||
messages:
|
||||
- message_type: incoming
|
||||
content: hello world
|
||||
- message_type: incoming
|
||||
content: Hi, I have a question about your product features.
|
||||
- message_type: outgoing
|
||||
sender: michael_scott@paperlayer.test
|
||||
content: Sure thing! What specific feature are you interested in learning more about?
|
||||
- name: "Cordell Dalinder"
|
||||
email: "cdalinderc@msn.test"
|
||||
gender: 'male'
|
||||
@@ -300,8 +336,11 @@ contacts:
|
||||
- channel: Channel::Email
|
||||
source_id: "cdalinderc@msn.test"
|
||||
messages:
|
||||
- message_type: incoming
|
||||
content: hello world
|
||||
- message_type: incoming
|
||||
content: Hi, I need help setting up my new printer.
|
||||
- message_type: outgoing
|
||||
sender: michael_scott@paperlayer.test
|
||||
content: No problem! Can you please provide me with the make and model of your printer and what type of device you'll be connecting it to?
|
||||
- name: "Merrile Petruk"
|
||||
email: "mpetrukd@wunderground.test"
|
||||
gender: 'female'
|
||||
@@ -310,8 +349,11 @@ contacts:
|
||||
source_id: "mpetrukd@wunderground.test"
|
||||
priority: urgent
|
||||
messages:
|
||||
- message_type: incoming
|
||||
content: hello world
|
||||
- message_type: incoming
|
||||
content: Hi, I'm having trouble accessing a file that I shared with someone.
|
||||
- message_type: outgoing
|
||||
sender: michael_scott@paperlayer.test
|
||||
content: I'm sorry to hear that. Can you please tell me which file you're having trouble accessing and who you shared it with? I'll do my best to help you regain access.
|
||||
- name: "Nathaniel Vannuchi"
|
||||
email: "nvannuchie@photobucket.test"
|
||||
gender: 'male'
|
||||
@@ -387,7 +429,9 @@ contacts:
|
||||
- message_type: incoming
|
||||
content: "Hey, \n I'm looking for some help to figure out if it is the right product for me."
|
||||
- message_type: outgoing
|
||||
sender: michael_scott@paperlayer.test
|
||||
content: Welcome to PaperLayer. Our Team will be getting back you shortly.
|
||||
- message_type: outgoing
|
||||
sender: michael_scott@paperlayer.test
|
||||
content: How may i help you ?
|
||||
sender: michael_scott@paperlayer.test
|
||||
|
||||
@@ -91,6 +91,7 @@ RSpec.describe AutomationRules::ActionService do
|
||||
describe '#perform with send_email_transcript action' do
|
||||
before do
|
||||
rule.actions << { action_name: 'send_email_transcript', action_params: ['contact@example.com, agent@example.com,agent1@example.com'] }
|
||||
rule.save
|
||||
end
|
||||
|
||||
it 'will send email to transcript to action params emails' do
|
||||
@@ -102,6 +103,17 @@ RSpec.describe AutomationRules::ActionService do
|
||||
|
||||
described_class.new(rule, account, conversation).perform
|
||||
end
|
||||
|
||||
it 'will send email to transcript to contacts' do
|
||||
rule.actions = [{ action_name: 'send_email_transcript', action_params: ['{{contact.email}}'] }]
|
||||
rule.save
|
||||
|
||||
mailer = double
|
||||
allow(ConversationReplyMailer).to receive(:with).and_return(mailer)
|
||||
allow(mailer).to receive(:conversation_transcript).with(conversation, conversation.contact.email)
|
||||
|
||||
described_class.new(rule.reload, account, conversation).perform
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -8,5 +8,26 @@ describe ::MessageTemplates::Template::Greeting do
|
||||
described_class.new(conversation: conversation).perform
|
||||
expect(conversation.messages.count).to eq(1)
|
||||
end
|
||||
|
||||
it 'creates the greeting messages with template variable' do
|
||||
conversation.inbox.update!(greeting_message: 'Hey, {{contact.name}} welcome to our board.')
|
||||
described_class.new(conversation: conversation).perform
|
||||
expect(conversation.messages.count).to eq(1)
|
||||
expect(conversation.messages.last.content).to eq("Hey, #{conversation.contact.name} welcome to our board.")
|
||||
end
|
||||
|
||||
it 'creates the greeting messages with more than one variable strings' do
|
||||
conversation.inbox.update!(greeting_message: 'Hey, {{contact.name}} welcome to our board. - from {{account.name}}')
|
||||
described_class.new(conversation: conversation).perform
|
||||
expect(conversation.messages.count).to eq(1)
|
||||
expect(conversation.messages.last.content).to eq("Hey, #{conversation.contact.name} welcome to our board. - from #{conversation.account.name}")
|
||||
end
|
||||
|
||||
it 'creates the greeting messages' do
|
||||
conversation.inbox.update!(greeting_message: 'Hello welcome to our board.')
|
||||
described_class.new(conversation: conversation).perform
|
||||
expect(conversation.messages.count).to eq(1)
|
||||
expect(conversation.messages.last.content).to eq('Hello welcome to our board.')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -9,5 +9,22 @@ describe ::MessageTemplates::Template::OutOfOffice do
|
||||
expect(conversation.messages.template.count).to eq(1)
|
||||
expect(conversation.messages.template.first.content).to eq(conversation.inbox.out_of_office_message)
|
||||
end
|
||||
|
||||
it 'creates the out of office messages with template variable' do
|
||||
conversation.inbox.update!(out_of_office_message: 'Hey, {{contact.name}} we are unavailable at the moment.')
|
||||
described_class.new(conversation: conversation).perform
|
||||
expect(conversation.messages.count).to eq(1)
|
||||
expect(conversation.messages.last.content).to eq("Hey, #{conversation.contact.name} we are unavailable at the moment.")
|
||||
end
|
||||
|
||||
it 'creates the out of office messages with more than one variable strings' do
|
||||
conversation.inbox.update!(out_of_office_message:
|
||||
'Hey, {{contact.name}} we are unavailable at the moment. - from {{account.name}}')
|
||||
described_class.new(conversation: conversation).perform
|
||||
expect(conversation.messages.count).to eq(1)
|
||||
expect(conversation.messages.last.content).to eq(
|
||||
"Hey, #{conversation.contact.name} we are unavailable at the moment. - from #{conversation.account.name}"
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user