Compare commits

...
Author SHA1 Message Date
Shivam Mishra 6e3375fb84 refactor: move attachment methods to a mixin 2023-10-04 19:33:28 +05:30
Shivam Mishra 58ce9bfc19 refactor: move methods out to mixin 2023-10-04 19:26:18 +05:30
Shivam Mishra 63a071f0d3 style: sort and group imports 2023-10-04 19:26:13 +05:30
Shivam Mishra f0d3ae09d9 refactor: move top level styles to tailwind 2023-10-04 19:26:07 +05:30
Mazen KhalilandGitHub 5c9ab21617 fix: Application not loading after setting RACK_ATTACK_LIMIT ENV variable [CW-2587] (#8044) 2023-10-04 15:56:20 +05:30
b71a580573 feat: Adds dark theme support for public portal [CW-2525] (#7979)
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2023-10-04 11:56:00 +05:30
Sojan JoseandGitHub 6a73953003 chore: Add delay before running dataimport job (#8039)
- We have observed some failures for data import jobs in the cloud due to race conditions with job executions and active storage file uploading. This PR adds delays and retries to accommodate that.
2023-10-03 22:18:57 -07:00
Sojan JoseandGitHub 336af1ac9a chore: Create client API conversations with custom attributes (#8040)
- Update client API create conversations endpoint to accept custom attributes as well.
2023-10-03 22:18:35 -07:00
e4de366b8d feat: Add an option to listen to the start conversation click events (#8038)
This PR will add a new event chatwoot:on-start-conversation to the chat widget, which you can listen to and trigger custom interactions on your end.

Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2023-10-03 18:01:40 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
373c3dcd6c chore(deps): bump postcss from 8.4.30 to 8.4.31 (#8034)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-03 10:08:18 -07:00
Shivam MishraandGitHub 4b12dd89b0 fix: videojs-record version (#8029) 2023-10-03 20:57:23 +05:30
c19cfeaa81 feat: Adds message signature for new email conversations (#7946)
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
2023-10-03 13:45:28 +05:30
fa118ad18c fix: Issue on the status filter field (#7949)
Co-authored-by: AqidaHaidari <42426077+AqidaHaidari@users.noreply.github.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2023-10-03 09:20:46 +05:30
249bfe44bb fix: issue of translation for TimeAgo.vue (#7951)
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
2023-10-03 08:41:30 +05:30
Sojan JoseandGitHub 826d9ec5a7 chore: Refactor Response Bot Data Schema (#8011)
This PR refactors the schema we introduced in #7518 based on the feedback from production tests. Here is the change log

- Decouple Inbox association to a new table inbox_response_sources -> this lets us share the same response source between multiple inboxes
- Add a status field to responses. This ensures that, by default, responses are created in pending status. You can do quality assurance before making them active. In future, this status can be leveraged by the bot to auto-generate response questions from conversations which require a handoff
- Add response_source association to responses and remove hard dependency from response_documents. This lets users write free-form question answers based on conversations, which doesn't necessarily need a response source.
2023-10-01 19:31:38 -07:00
d8b53f5d2f chore: [Snyk] Security upgrade audited from 5.3.3 to 5.4.0 (#8023)
Co-authored-by: snyk-bot <snyk-bot@snyk.io>
2023-09-30 19:49:03 -07:00
Jordan BroughandGitHub 08ced452e1 chore: Log errors handled by RequestExceptionHandler (#8013)
- Logging this info is invaluable when using logs to track down the reason why a particular request failed.
2023-09-30 19:46:58 -07:00
24fe3805d8 feat: Slack link unfurling (#7940)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2023-09-29 19:35:56 +05:30
Sivin VargheseandGitHub 845b0c0a68 fix: Editor file upload browser showing bug (#8019) 2023-09-29 17:53:54 +05:30
Chatwoot BotandGitHub 8ce7539c5e chore: Update translations (#8016) 2023-09-29 15:46:29 +05:30
Shivam MishraandGitHub 5ee980465f fix: Token type hr not supported by Markdown parser (#8003) 2023-09-29 12:05:32 +05:30
Pranav Raj SandGitHub fd633e1613 feat: Add inbox webhook events (#8006)
- Add webhook events for inbox creation/updation.
- Right now, the feature is added under a feature_flag. It is not available by default on all installations.
2023-09-28 15:28:10 -07:00
Pranav Raj SandGitHub 12a64f1b10 chore: Add an API to find the contacts using contact inbox sourceId (#8012)
Fixes: https://linear.app/chatwoot/issue/CW-2578/search-by-facebook-id
2023-09-28 15:26:28 -07:00
Pranav Raj SandGitHub ffc2d98fb8 fix: Use installation locale if configured (#8002) 2023-09-27 19:06:36 +05:30
f7f04f788e feat: Add an option to view the generated articles by Robin AI (#7995)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2023-09-27 18:16:36 +05:30
a88d155dd7 feat: update tool-chain to latest (#7975)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2023-09-27 14:02:34 +05:30
Chatwoot BotandGitHub e8b7e791a5 chore: Update translations (#7969) 2023-09-27 12:47:03 +05:30
b18cac77fb chore: Moved file upload methods to mixin (#7987)
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2023-09-27 11:05:19 +05:30
Sojan JoseandGitHub 647161121e chore: pass to agent if there is error parsing json (#7990)
- GPT bot should pass the conversation to the agent if AI returns invalid json or any other error
2023-09-26 21:05:21 -07:00
Muhsin KelothandGitHub cbbe939662 fix: Set avatar for users(agent/contact) in slack channels (#7960) 2023-09-26 09:38:14 +05:30
Sojan 30cb141e5e Merge branch 'master' into develop 2023-09-22 19:55:11 -07:00
Sojan b0d5c438ac Merge branch 'release/3.1.1' into develop 2023-09-22 19:54:48 -07:00
442 changed files with 10880 additions and 3287 deletions
+1 -1
View File
@@ -199,7 +199,7 @@ ANDROID_SHA256_CERT_FINGERPRINT=AC:73:8E:DE:EB:56:EA:CC:10:87:02:A7:65:37:7B:38:
## Rack Attack configuration
## To prevent and throttle abusive requests
# ENABLE_RACK_ATTACK=true
# RACK_ATTACK_IP_LIMIT=3000
# RACK_ATTACK_LIMIT=300
# ENABLE_RACK_ATTACK_WIDGET_API=true
## Running chatwoot as an API only server
+7 -3
View File
@@ -6,7 +6,7 @@ module.exports = {
'plugin:storybook/recommended',
],
parserOptions: {
parser: 'babel-eslint',
parser: '@babel/eslint-parser',
ecmaVersion: 2020,
sourceType: 'module',
},
@@ -24,13 +24,16 @@ module.exports = {
'jsx-a11y/label-has-for': 'off',
'jsx-a11y/anchor-is-valid': 'off',
'import/no-unresolved': 'off',
'vue/html-indent': 'off',
'vue/multi-word-component-names': 'off',
'vue/max-attributes-per-line': [
'error',
{
singleline: 20,
singleline: {
max: 20,
},
multiline: {
max: 1,
allowFirstLine: false,
},
},
],
@@ -47,6 +50,7 @@ module.exports = {
},
],
'vue/no-v-html': 'off',
'vue/component-definition-name-casing': 'off',
'vue/singleline-html-element-content-newline': 'off',
'import/extensions': ['off'],
'no-console': 'error',
+6 -1
View File
@@ -69,7 +69,12 @@ jobs:
# Run Response Bot specs
- name: Run backend tests
run: |
bundle exec rspec spec/enterprise/controllers/api/v1/accounts/response_sources_controller_spec.rb spec/enterprise/controllers/enterprise/api/v1/accounts/inboxes_controller_spec.rb:47 --profile=10 --format documentation
bundle exec rspec \
spec/enterprise/controllers/api/v1/accounts/response_sources_controller_spec.rb \
spec/enterprise/services/enterprise/message_templates/response_bot_service_spec.rb \
spec/enterprise/controllers/enterprise/api/v1/accounts/inboxes_controller_spec.rb:47 \
--profile=10 \
--format documentation
- name: Upload rails log folder
uses: actions/upload-artifact@v3
+1
View File
@@ -58,6 +58,7 @@ Metrics/BlockLength:
Metrics/ModuleLength:
Exclude:
- lib/seeders/message_seeder.rb
- spec/support/slack_stubs.rb
Rails/ApplicationController:
Exclude:
- 'app/controllers/api/v1/widget/messages_controller.rb'
+1 -1
View File
@@ -159,7 +159,7 @@ gem 'lograge', '~> 0.13.0', require: false
# worked with microsoft refresh token
gem 'omniauth-oauth2'
gem 'audited', '~> 5.3'
gem 'audited', '~> 5.4', '>= 5.4.0'
# need for google auth
gem 'omniauth'
+3 -3
View File
@@ -126,8 +126,8 @@ GEM
rake (>= 10.4, < 14.0)
ast (2.4.2)
attr_extras (7.1.0)
audited (5.3.3)
activerecord (>= 5.0, < 7.1)
audited (5.4.0)
activerecord (>= 5.0, < 7.2)
request_store (~> 1.2)
aws-eventstream (1.2.0)
aws-partitions (1.760.0)
@@ -839,7 +839,7 @@ DEPENDENCIES
administrate-field-belongs_to_search
annotate
attr_extras
audited (~> 5.3)
audited (~> 5.4, >= 5.4.0)
aws-sdk-s3
azure-storage-blob!
barnes
@@ -24,14 +24,6 @@ select {
font-size: $base-font-size;
}
input,
select,
textarea {
display: block;
font-family: $base-font-family;
font-size: 16px;
}
[type="color"],
[type="date"],
[type="datetime-local"],
@@ -51,6 +43,7 @@ textarea {
background-color: $white;
border: $base-border;
border-radius: $base-border-radius;
font-family: $base-font-family;
padding: 0.5em;
transition: border-color $base-duration $base-timing;
width: 100%;
@@ -4,8 +4,8 @@
float: left;
margin-bottom: $base-spacing;
margin-top: 0.25em;
text-align: right;
width: calc(15% - 1rem);
text-align: left;
width: calc(16% - 1rem);
}
.preserve-whitespace {
@@ -17,7 +17,7 @@
float: left;
margin-bottom: $base-spacing;
margin-left: 2rem;
width: calc(85% - 1rem);
width: calc(84% - 1rem);
}
.attribute--nested {
@@ -10,7 +10,7 @@ input[type="submit"],
color: $white;
cursor: pointer;
display: inline-block;
font-size: $font-size-default;
font-size: $font-size-small;
-webkit-font-smoothing: antialiased;
font-weight: $font-weight-medium;
line-height: 1;
@@ -1,13 +1,18 @@
.main-content {
font-size: $font-size-default;
left: 23rem;
left: 21rem;
position: absolute;
right: 0;
top: 0;
}
.main-content__body {
font-size: $font-size-small;
padding: $space-two;
table {
font-size: $font-size-small;
}
}
.main-content__header {
@@ -20,7 +25,7 @@
}
.main-content__page-title {
font-size: $font-size-large;
font-size: $font-size-medium;
font-weight: $font-weight-medium;
margin-right: auto;
}
@@ -1,7 +1,12 @@
.logo-brand {
margin-bottom: $space-normal;
padding: $space-normal $space-smaller $space-small;
text-align: center;
text-align: left;
img {
margin-bottom: $space-smaller;
max-height: 3rem;
}
}
.navigation {
@@ -19,12 +24,13 @@
padding: $space-normal;
position: fixed;
top: 0;
width: 23rem;
width: 21rem;
z-index: 1023;
li {
align-items: center;
display: flex;
font-size: $font-size-small;
a {
color: $color-gray;
@@ -35,6 +41,10 @@
min-width: $space-medium;
}
}
hr {
margin: $space-slab;
}
}
.navigation__link {
@@ -43,7 +53,7 @@
display: block;
line-height: 1;
margin-bottom: $space-smaller;
padding: $space-one;
padding: $space-small;
&:hover {
color: $blue;
@@ -4,7 +4,7 @@ $base-font-family: PlusJakarta, Inter, -apple-system, BlinkMacSystemFont, "Segoe
sans-serif !default;
$heading-font-family: $base-font-family !default;
$base-font-size: 16px !default;
$base-font-size: 14px !default;
$base-line-height: 1.5 !default;
$heading-line-height: 1.2 !default;
@@ -0,0 +1,21 @@
class Api::V1::Accounts::ContactInboxesController < Api::V1::Accounts::BaseController
before_action :ensure_inbox
def filter
contact_inbox = @inbox.contact_inboxes.where(inbox_id: permitted_params[:inbox_id], source_id: permitted_params[:source_id])
return head :not_found if contact_inbox.empty?
@contact = contact_inbox.first.contact
end
private
def ensure_inbox
@inbox = Current.account.inboxes.find(permitted_params[:inbox_id])
authorize @inbox, :show?
end
def permitted_params
params.permit(:inbox_id, :source_id)
end
end
@@ -1,7 +1,15 @@
class Api::V1::Integrations::WebhooksController < ApplicationController
def create
builder = Integrations::Slack::IncomingMessageBuilder.new(params)
builder = Integrations::Slack::IncomingMessageBuilder.new(permitted_params)
response = builder.perform
render json: response
end
private
# TODO: This is a temporary solution to permit all params for slack unfurling job.
# We should only permit the params that we use. Handle all the params based on events and send it to the respective services.
def permitted_params
params.permit!
end
end
@@ -9,11 +9,14 @@ module RequestExceptionHandler
def handle_with_exception
yield
rescue ActiveRecord::RecordNotFound
rescue ActiveRecord::RecordNotFound => e
log_handled_error(e)
render_not_found_error('Resource could not be found')
rescue Pundit::NotAuthorizedError
rescue Pundit::NotAuthorizedError => e
log_handled_error(e)
render_unauthorized('You are not authorized to do this action')
rescue ActionController::ParameterMissing => e
log_handled_error(e)
render_could_not_create_error(e.message)
ensure
# to address the thread variable leak issues in Puma/Thin webserver
@@ -41,6 +44,7 @@ module RequestExceptionHandler
end
def render_record_invalid(exception)
log_handled_error(exception)
render json: {
message: exception.record.errors.full_messages.join(', '),
attributes: exception.record.errors.attribute_names
@@ -48,6 +52,11 @@ module RequestExceptionHandler
end
def render_error_response(exception)
log_handled_error(exception)
render json: exception.to_hash, status: exception.http_status
end
def log_handled_error(exception)
logger.info("Handled error: #{exception.inspect}")
end
end
@@ -33,7 +33,7 @@ class Public::Api::V1::Inboxes::ConversationsController < Public::Api::V1::Inbox
end
def create_conversation
::Conversation.create!(conversation_params)
ConversationBuilder.new(params: conversation_params, contact_inbox: @contact_inbox).perform
end
def trigger_typing_event(event)
@@ -41,11 +41,6 @@ class Public::Api::V1::Inboxes::ConversationsController < Public::Api::V1::Inbox
end
def conversation_params
{
account_id: @contact_inbox.contact.account_id,
inbox_id: @contact_inbox.inbox_id,
contact_id: @contact_inbox.contact_id,
contact_inbox_id: @contact_inbox.id
}
params.permit(custom_attributes: {})
end
end
@@ -1,5 +1,6 @@
class Public::Api::V1::Portals::BaseController < PublicController
before_action :show_plain_layout
before_action :set_color_scheme
around_action :set_locale
after_action :allow_iframe_requests
@@ -9,6 +10,14 @@ class Public::Api::V1::Portals::BaseController < PublicController
@is_plain_layout_enabled = params[:show_plain_layout] == 'true'
end
def set_color_scheme
@theme = if %w[dark light].include?(params[:theme])
params[:theme]
else
''
end
end
def set_locale(&)
switch_locale_with_portal(&) if params[:locale].present?
switch_locale_with_article(&) if params[:article_slug].present?
@@ -0,0 +1,27 @@
class SlackUploadsController < ApplicationController
include Rails.application.routes.url_helpers
before_action :set_blob, only: [:show]
def show
if @blob
redirect_to blob_url
else
redirect_to avatar_url
end
end
private
def set_blob
@blob = ActiveStorage::Blob.find_by(key: params[:blob_key])
end
def blob_url
url_for(@blob.representation(resize_to_fill: [250, nil]))
end
def avatar_url
base_url = ENV.fetch('FRONTEND_URL', nil)
"#{base_url}/integrations/slack/#{params[:sender_type]}.png"
end
end
+11
View File
@@ -0,0 +1,11 @@
module PortalHelper
def generate_portal_bg_color(portal_color, theme)
base_color = theme == 'dark' ? 'black' : 'white'
"color-mix(in srgb, #{portal_color} 10%, #{base_color})"
end
def generate_portal_bg(portal_color, theme)
bg_image = theme == 'dark' ? 'grid_dark.svg' : 'grid.svg'
"background: url(/assets/images/hc/#{bg_image}) #{generate_portal_bg_color(portal_color, theme)}"
end
end
+2 -4
View File
@@ -111,10 +111,8 @@ export default {
this.$store.dispatch('setActiveAccount', {
accountId: this.currentAccountId,
});
const {
locale,
latest_chatwoot_version: latestChatwootVersion,
} = this.getAccount(this.currentAccountId);
const { locale, latest_chatwoot_version: latestChatwootVersion } =
this.getAccount(this.currentAccountId);
const { pubsub_token: pubsubToken } = this.currentUser || {};
this.setLocale(locale);
this.updateRTLDirectionView(locale);
+2 -3
View File
@@ -15,9 +15,8 @@ class ApiClient {
// eslint-disable-next-line class-methods-use-this
get accountIdFromRoute() {
const isInsideAccountScopedURLs = window.location.pathname.includes(
'/app/accounts'
);
const isInsideAccountScopedURLs =
window.location.pathname.includes('/app/accounts');
if (isInsideAccountScopedURLs) {
return window.location.pathname.split('/')[3];
+1
View File
@@ -53,6 +53,7 @@ class ContactAPI extends ApiClient {
return axios.get(requestURL);
}
// eslint-disable-next-line default-param-last
filter(page = 1, sortAttr = 'name', queryPayload) {
let requestURL = `${this.url}/filter?${buildContactParams(page, sortAttr)}`;
return axios.post(requestURL, queryPayload);
@@ -1,6 +1,5 @@
import accountAPI from '../account';
import ApiClient from '../../ApiClient';
import describeWithAPIMock from '../../specs/apiSpecHelper';
describe('#enterpriseAccountAPI', () => {
it('creates correct instance', () => {
@@ -13,17 +12,33 @@ describe('#enterpriseAccountAPI', () => {
expect(accountAPI).toHaveProperty('checkout');
});
describeWithAPIMock('API calls', context => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
window.axios = axiosMock;
});
afterEach(() => {
window.axios = originalAxios;
});
it('#checkout', () => {
accountAPI.checkout();
expect(context.axiosMock.post).toHaveBeenCalledWith(
expect(axiosMock.post).toHaveBeenCalledWith(
'/enterprise/api/v1/checkout'
);
});
it('#subscription', () => {
accountAPI.subscription();
expect(context.axiosMock.post).toHaveBeenCalledWith(
expect(axiosMock.post).toHaveBeenCalledWith(
'/enterprise/api/v1/subscription'
);
});
+1
View File
@@ -31,6 +31,7 @@ class ReportsAPI extends ApiClient {
});
}
// eslint-disable-next-line default-param-last
getSummary(since, until, type = 'account', id, groupBy, businessHours) {
return axios.get(`${this.url}/summary`, {
params: {
@@ -1,6 +1,5 @@
import accountAPI from '../account';
import ApiClient from '../ApiClient';
import describeWithAPIMock from './apiSpecHelper';
describe('#accountAPI', () => {
it('creates correct instance', () => {
@@ -13,12 +12,28 @@ describe('#accountAPI', () => {
expect(accountAPI).toHaveProperty('createAccount');
});
describeWithAPIMock('API calls', context => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
window.axios = axiosMock;
});
afterEach(() => {
window.axios = originalAxios;
});
it('#createAccount', () => {
accountAPI.createAccount({
name: 'Chatwoot',
});
expect(context.axiosMock.post).toHaveBeenCalledWith('/api/v1/accounts', {
expect(axiosMock.post).toHaveBeenCalledWith('/api/v1/accounts', {
name: 'Chatwoot',
});
});
@@ -1,6 +1,5 @@
import accountActionsAPI from '../accountActions';
import ApiClient from '../ApiClient';
import describeWithAPIMock from './apiSpecHelper';
describe('#ContactsAPI', () => {
it('creates correct instance', () => {
@@ -8,10 +7,26 @@ describe('#ContactsAPI', () => {
expect(accountActionsAPI).toHaveProperty('merge');
});
describeWithAPIMock('API calls', context => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
window.axios = axiosMock;
});
afterEach(() => {
window.axios = originalAxios;
});
it('#merge', () => {
accountActionsAPI.merge(1, 2);
expect(context.axiosMock.post).toHaveBeenCalledWith(
expect(axiosMock.post).toHaveBeenCalledWith(
'/api/v1/actions/contact_merge',
{
base_contact_id: 1,
@@ -1,27 +0,0 @@
function apiSpecHelper() {
beforeEach(() => {
this.originalAxios = window.axios;
this.axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
window.axios = this.axiosMock;
});
afterEach(() => {
window.axios = this.originalAxios;
});
}
// https://stackoverflow.com/a/59344023/3901856
const sharedWrapper = describe('sharedWrapper', () => {});
export default function describeWithAPIMock(skillName, testFn) {
return describe(skillName, function configureContext() {
function Context() {}
Context.prototype = sharedWrapper.ctx;
this.ctx = new Context();
apiSpecHelper.call(this);
testFn.call(this, this);
});
}
@@ -1,6 +1,5 @@
import articlesAPI from '../helpCenter/articles';
import ApiClient from 'dashboard/api/helpCenter/portals';
import describeWithAPIMock from './apiSpecHelper';
describe('#PortalAPI', () => {
it('creates correct instance', () => {
@@ -12,7 +11,23 @@ describe('#PortalAPI', () => {
expect(articlesAPI).toHaveProperty('delete');
expect(articlesAPI).toHaveProperty('getArticles');
});
describeWithAPIMock('API calls', context => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
window.axios = axiosMock;
});
afterEach(() => {
window.axios = originalAxios;
});
it('#getArticles', () => {
articlesAPI.getArticles({
pageNumber: 1,
@@ -21,30 +36,62 @@ describe('#PortalAPI', () => {
status: 'published',
author_id: '1',
});
expect(context.axiosMock.get).toHaveBeenCalledWith(
expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v1/portals/room-rental/articles?page=1&locale=en-US&status=published&author_id=1'
);
});
});
describeWithAPIMock('API calls', context => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
window.axios = axiosMock;
});
afterEach(() => {
window.axios = originalAxios;
});
it('#getArticle', () => {
articlesAPI.getArticle({
id: 1,
portalSlug: 'room-rental',
});
expect(context.axiosMock.get).toHaveBeenCalledWith(
expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v1/portals/room-rental/articles/1'
);
});
});
describeWithAPIMock('API calls', context => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
window.axios = axiosMock;
});
afterEach(() => {
window.axios = originalAxios;
});
it('#updateArticle', () => {
articlesAPI.updateArticle({
articleId: 1,
portalSlug: 'room-rental',
articleObj: { title: 'Update shipping address' },
});
expect(context.axiosMock.patch).toHaveBeenCalledWith(
expect(axiosMock.patch).toHaveBeenCalledWith(
'/api/v1/portals/room-rental/articles/1',
{
title: 'Update shipping address',
@@ -52,13 +99,29 @@ describe('#PortalAPI', () => {
);
});
});
describeWithAPIMock('API calls', context => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
window.axios = axiosMock;
});
afterEach(() => {
window.axios = originalAxios;
});
it('#deleteArticle', () => {
articlesAPI.deleteArticle({
articleId: 1,
portalSlug: 'room-rental',
});
expect(context.axiosMock.delete).toHaveBeenCalledWith(
expect(axiosMock.delete).toHaveBeenCalledWith(
'/api/v1/portals/room-rental/articles/1'
);
});
@@ -1,18 +1,30 @@
import assignableAgentsAPI from '../assignableAgents';
import describeWithAPIMock from './apiSpecHelper';
describe('#AssignableAgentsAPI', () => {
describeWithAPIMock('API calls', context => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
window.axios = axiosMock;
});
afterEach(() => {
window.axios = originalAxios;
});
it('#getAssignableAgents', () => {
assignableAgentsAPI.get([1]);
expect(context.axiosMock.get).toHaveBeenCalledWith(
'/api/v1/assignable_agents',
{
params: {
inbox_ids: [1],
},
}
);
expect(axiosMock.get).toHaveBeenCalledWith('/api/v1/assignable_agents', {
params: {
inbox_ids: [1],
},
});
});
});
});
@@ -1,6 +1,5 @@
import fbChannel from '../../channel/fbChannel';
import ApiClient from '../../ApiClient';
import describeWithAPIMock from '../apiSpecHelper';
describe('#FBChannel', () => {
it('creates correct instance', () => {
@@ -11,10 +10,26 @@ describe('#FBChannel', () => {
expect(fbChannel).toHaveProperty('update');
expect(fbChannel).toHaveProperty('delete');
});
describeWithAPIMock('API calls', context => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
window.axios = axiosMock;
});
afterEach(() => {
window.axios = originalAxios;
});
it('#create', () => {
fbChannel.create({ omniauthToken: 'ASFM131CSF@#@$', appId: 'chatwoot' });
expect(context.axiosMock.post).toHaveBeenCalledWith(
expect(axiosMock.post).toHaveBeenCalledWith(
'/api/v1/callbacks/register_facebook_page',
{
omniauthToken: 'ASFM131CSF@#@$',
@@ -27,7 +42,7 @@ describe('#FBChannel', () => {
omniauthToken: 'ASFM131CSF@#@$',
inboxId: 1,
});
expect(context.axiosMock.post).toHaveBeenCalledWith(
expect(axiosMock.post).toHaveBeenCalledWith(
'/api/v1/callbacks/reauthorize_page',
{
omniauth_token: 'ASFM131CSF@#@$',
@@ -1,6 +1,5 @@
import contactAPI, { buildContactParams } from '../contacts';
import ApiClient from '../ApiClient';
import describeWithAPIMock from './apiSpecHelper';
describe('#ContactsAPI', () => {
it('creates correct instance', () => {
@@ -15,56 +14,67 @@ describe('#ContactsAPI', () => {
expect(contactAPI).toHaveProperty('destroyAvatar');
});
describeWithAPIMock('API calls', context => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
window.axios = axiosMock;
});
afterEach(() => {
window.axios = originalAxios;
});
it('#get', () => {
contactAPI.get(1, 'name', 'customer-support');
expect(context.axiosMock.get).toHaveBeenCalledWith(
expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v1/contacts?include_contact_inboxes=false&page=1&sort=name&labels[]=customer-support'
);
});
it('#getConversations', () => {
contactAPI.getConversations(1);
expect(context.axiosMock.get).toHaveBeenCalledWith(
expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v1/contacts/1/conversations'
);
});
it('#getContactableInboxes', () => {
contactAPI.getContactableInboxes(1);
expect(context.axiosMock.get).toHaveBeenCalledWith(
expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v1/contacts/1/contactable_inboxes'
);
});
it('#getContactLabels', () => {
contactAPI.getContactLabels(1);
expect(context.axiosMock.get).toHaveBeenCalledWith(
'/api/v1/contacts/1/labels'
);
expect(axiosMock.get).toHaveBeenCalledWith('/api/v1/contacts/1/labels');
});
it('#updateContactLabels', () => {
const labels = ['support-query'];
contactAPI.updateContactLabels(1, labels);
expect(context.axiosMock.post).toHaveBeenCalledWith(
'/api/v1/contacts/1/labels',
{
labels,
}
);
expect(axiosMock.post).toHaveBeenCalledWith('/api/v1/contacts/1/labels', {
labels,
});
});
it('#search', () => {
contactAPI.search('leads', 1, 'date', 'customer-support');
expect(context.axiosMock.get).toHaveBeenCalledWith(
expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v1/contacts/search?include_contact_inboxes=false&page=1&sort=date&q=leads&labels[]=customer-support'
);
});
it('#destroyCustomAttributes', () => {
contactAPI.destroyCustomAttributes(1, ['cloudCustomer']);
expect(context.axiosMock.post).toHaveBeenCalledWith(
expect(axiosMock.post).toHaveBeenCalledWith(
'/api/v1/contacts/1/destroy_custom_attributes',
{
custom_attributes: ['cloudCustomer'],
@@ -75,7 +85,7 @@ describe('#ContactsAPI', () => {
it('#importContacts', () => {
const file = 'file';
contactAPI.importContacts(file);
expect(context.axiosMock.post).toHaveBeenCalledWith(
expect(axiosMock.post).toHaveBeenCalledWith(
'/api/v1/contacts/import',
expect.any(FormData),
{
@@ -96,7 +106,7 @@ describe('#ContactsAPI', () => {
],
};
contactAPI.filter(1, 'name', queryPayload);
expect(context.axiosMock.post).toHaveBeenCalledWith(
expect(axiosMock.post).toHaveBeenCalledWith(
'/api/v1/contacts/filter?include_contact_inboxes=false&page=1&sort=name',
queryPayload
);
@@ -104,7 +114,7 @@ describe('#ContactsAPI', () => {
it('#destroyAvatar', () => {
contactAPI.destroyAvatar(1);
expect(context.axiosMock.delete).toHaveBeenCalledWith(
expect(axiosMock.delete).toHaveBeenCalledWith(
'/api/v1/contacts/1/avatar'
);
});
@@ -1,6 +1,5 @@
import conversationsAPI from '../conversations';
import ApiClient from '../ApiClient';
import describeWithAPIMock from './apiSpecHelper';
describe('#ConversationApi', () => {
it('creates correct instance', () => {
@@ -14,10 +13,26 @@ describe('#ConversationApi', () => {
expect(conversationsAPI).toHaveProperty('updateLabels');
});
describeWithAPIMock('API calls', context => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
window.axios = axiosMock;
});
afterEach(() => {
window.axios = originalAxios;
});
it('#getLabels', () => {
conversationsAPI.getLabels(1);
expect(context.axiosMock.get).toHaveBeenCalledWith(
expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v1/conversations/1/labels'
);
});
@@ -25,7 +40,7 @@ describe('#ConversationApi', () => {
it('#updateLabels', () => {
const labels = ['support-query'];
conversationsAPI.updateLabels(1, labels);
expect(context.axiosMock.post).toHaveBeenCalledWith(
expect(axiosMock.post).toHaveBeenCalledWith(
'/api/v1/conversations/1/labels',
{
labels,
@@ -1,6 +1,5 @@
import csatReportsAPI from '../csatReports';
import ApiClient from '../ApiClient';
import describeWithAPIMock from './apiSpecHelper';
describe('#Reports API', () => {
it('creates correct instance', () => {
@@ -9,10 +8,26 @@ describe('#Reports API', () => {
expect(csatReportsAPI).toHaveProperty('get');
expect(csatReportsAPI).toHaveProperty('getMetrics');
});
describeWithAPIMock('API calls', context => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
window.axios = axiosMock;
});
afterEach(() => {
window.axios = originalAxios;
});
it('#get', () => {
csatReportsAPI.get({ page: 1, from: 1622485800, to: 1623695400 });
expect(context.axiosMock.get).toHaveBeenCalledWith(
expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v1/csat_survey_responses',
{
params: {
@@ -26,7 +41,7 @@ describe('#Reports API', () => {
});
it('#getMetrics', () => {
csatReportsAPI.getMetrics({ from: 1622485800, to: 1623695400 });
expect(context.axiosMock.get).toHaveBeenCalledWith(
expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v1/csat_survey_responses/metrics',
{
params: { since: 1622485800, until: 1623695400 },
@@ -39,7 +54,7 @@ describe('#Reports API', () => {
to: 1623695400,
user_ids: 1,
});
expect(context.axiosMock.get).toHaveBeenCalledWith(
expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v1/csat_survey_responses/download',
{
params: {
@@ -1,6 +1,5 @@
import conversationAPI from '../../inbox/conversation';
import ApiClient from '../../ApiClient';
import describeWithAPIMock from '../apiSpecHelper';
describe('#ConversationAPI', () => {
it('creates correct instance', () => {
@@ -22,7 +21,23 @@ describe('#ConversationAPI', () => {
expect(conversationAPI).toHaveProperty('filter');
});
describeWithAPIMock('API calls', context => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
window.axios = axiosMock;
});
afterEach(() => {
window.axios = originalAxios;
});
it('#get conversations', () => {
conversationAPI.get({
inboxId: 1,
@@ -32,19 +47,16 @@ describe('#ConversationAPI', () => {
labels: [],
teamId: 1,
});
expect(context.axiosMock.get).toHaveBeenCalledWith(
'/api/v1/conversations',
{
params: {
inbox_id: 1,
team_id: 1,
status: 'open',
assignee_type: 'me',
page: 1,
labels: [],
},
}
);
expect(axiosMock.get).toHaveBeenCalledWith('/api/v1/conversations', {
params: {
inbox_id: 1,
team_id: 1,
status: 'open',
assignee_type: 'me',
page: 1,
labels: [],
},
});
});
it('#search', () => {
@@ -53,7 +65,7 @@ describe('#ConversationAPI', () => {
page: 1,
});
expect(context.axiosMock.get).toHaveBeenCalledWith(
expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v1/conversations/search',
{
params: {
@@ -66,7 +78,7 @@ describe('#ConversationAPI', () => {
it('#toggleStatus', () => {
conversationAPI.toggleStatus({ conversationId: 12, status: 'online' });
expect(context.axiosMock.post).toHaveBeenCalledWith(
expect(axiosMock.post).toHaveBeenCalledWith(
`/api/v1/conversations/12/toggle_status`,
{
status: 'online',
@@ -77,7 +89,7 @@ describe('#ConversationAPI', () => {
it('#assignAgent', () => {
conversationAPI.assignAgent({ conversationId: 12, agentId: 34 });
expect(context.axiosMock.post).toHaveBeenCalledWith(
expect(axiosMock.post).toHaveBeenCalledWith(
`/api/v1/conversations/12/assignments?assignee_id=34`,
{}
);
@@ -85,7 +97,7 @@ describe('#ConversationAPI', () => {
it('#assignTeam', () => {
conversationAPI.assignTeam({ conversationId: 12, teamId: 1 });
expect(context.axiosMock.post).toHaveBeenCalledWith(
expect(axiosMock.post).toHaveBeenCalledWith(
`/api/v1/conversations/12/assignments`,
{
team_id: 1,
@@ -95,7 +107,7 @@ describe('#ConversationAPI', () => {
it('#markMessageRead', () => {
conversationAPI.markMessageRead({ id: 12 });
expect(context.axiosMock.post).toHaveBeenCalledWith(
expect(axiosMock.post).toHaveBeenCalledWith(
`/api/v1/conversations/12/update_last_seen`
);
});
@@ -105,7 +117,7 @@ describe('#ConversationAPI', () => {
conversationId: 12,
status: 'typing_on',
});
expect(context.axiosMock.post).toHaveBeenCalledWith(
expect(axiosMock.post).toHaveBeenCalledWith(
`/api/v1/conversations/12/toggle_typing_status`,
{
typing_status: 'typing_on',
@@ -115,14 +127,14 @@ describe('#ConversationAPI', () => {
it('#mute', () => {
conversationAPI.mute(45);
expect(context.axiosMock.post).toHaveBeenCalledWith(
expect(axiosMock.post).toHaveBeenCalledWith(
'/api/v1/conversations/45/mute'
);
});
it('#unmute', () => {
conversationAPI.unmute(45);
expect(context.axiosMock.post).toHaveBeenCalledWith(
expect(axiosMock.post).toHaveBeenCalledWith(
'/api/v1/conversations/45/unmute'
);
});
@@ -135,18 +147,15 @@ describe('#ConversationAPI', () => {
labels: [],
teamId: 1,
});
expect(context.axiosMock.get).toHaveBeenCalledWith(
'/api/v1/conversations/meta',
{
params: {
inbox_id: 1,
team_id: 1,
status: 'open',
assignee_type: 'me',
labels: [],
},
}
);
expect(axiosMock.get).toHaveBeenCalledWith('/api/v1/conversations/meta', {
params: {
inbox_id: 1,
team_id: 1,
status: 'open',
assignee_type: 'me',
labels: [],
},
});
});
it('#sendEmailTranscript', () => {
@@ -154,7 +163,7 @@ describe('#ConversationAPI', () => {
conversationId: 45,
email: 'john@acme.inc',
});
expect(context.axiosMock.post).toHaveBeenCalledWith(
expect(axiosMock.post).toHaveBeenCalledWith(
'/api/v1/conversations/45/transcript',
{
email: 'john@acme.inc',
@@ -167,7 +176,7 @@ describe('#ConversationAPI', () => {
conversationId: 45,
customAttributes: { order_d: '1001' },
});
expect(context.axiosMock.post).toHaveBeenCalledWith(
expect(axiosMock.post).toHaveBeenCalledWith(
'/api/v1/conversations/45/custom_attributes',
{
custom_attributes: { order_d: '1001' },
@@ -202,9 +211,7 @@ describe('#ConversationAPI', () => {
},
};
conversationAPI.filter(payload);
expect(
context.axiosMock.post
).toHaveBeenCalledWith(
expect(axiosMock.post).toHaveBeenCalledWith(
'/api/v1/conversations/filter',
payload.queryData,
{ params: { page: payload.page } }
@@ -213,7 +220,7 @@ describe('#ConversationAPI', () => {
it('#getAllAttachments', () => {
conversationAPI.getAllAttachments(1);
expect(context.axiosMock.get).toHaveBeenCalledWith(
expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v1/conversations/1/attachments'
);
});
@@ -1,6 +1,5 @@
import messageAPI, { buildCreatePayload } from '../../inbox/message';
import ApiClient from '../../ApiClient';
import describeWithAPIMock from '../apiSpecHelper';
describe('#ConversationAPI', () => {
it('creates correct instance', () => {
@@ -13,13 +12,29 @@ describe('#ConversationAPI', () => {
expect(messageAPI).toHaveProperty('getPreviousMessages');
});
describeWithAPIMock('API calls', context => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
window.axios = axiosMock;
});
afterEach(() => {
window.axios = originalAxios;
});
it('#getPreviousMessages', () => {
messageAPI.getPreviousMessages({
conversationId: 12,
before: 4573,
});
expect(context.axiosMock.get).toHaveBeenCalledWith(
expect(axiosMock.get).toHaveBeenCalledWith(
`/api/v1/conversations/12/messages`,
{
params: {
@@ -35,7 +50,6 @@ describe('#ConversationAPI', () => {
message: 'test content',
echoId: 12,
isPrivate: true,
files: [new Blob(['test-content'], { type: 'application/pdf' })],
});
expect(formPayload).toBeInstanceOf(FormData);
@@ -58,8 +72,10 @@ describe('#ConversationAPI', () => {
private: false,
echo_id: 12,
content_attributes: { in_reply_to: 12 },
bcc_emails: '',
cc_emails: '',
bcc_emails: '',
to_emails: '',
template_params: undefined,
});
});
});
@@ -1,6 +1,5 @@
import inboxesAPI from '../inboxes';
import ApiClient from '../ApiClient';
import describeWithAPIMock from './apiSpecHelper';
describe('#InboxesAPI', () => {
it('creates correct instance', () => {
@@ -14,19 +13,32 @@ describe('#InboxesAPI', () => {
expect(inboxesAPI).toHaveProperty('getAgentBot');
expect(inboxesAPI).toHaveProperty('setAgentBot');
});
describeWithAPIMock('API calls', context => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
window.axios = axiosMock;
});
afterEach(() => {
window.axios = originalAxios;
});
it('#getCampaigns', () => {
inboxesAPI.getCampaigns(2);
expect(context.axiosMock.get).toHaveBeenCalledWith(
'/api/v1/inboxes/2/campaigns'
);
expect(axiosMock.get).toHaveBeenCalledWith('/api/v1/inboxes/2/campaigns');
});
it('#deleteInboxAvatar', () => {
inboxesAPI.deleteInboxAvatar(2);
expect(context.axiosMock.delete).toHaveBeenCalledWith(
'/api/v1/inboxes/2/avatar'
);
expect(axiosMock.delete).toHaveBeenCalledWith('/api/v1/inboxes/2/avatar');
});
});
});
@@ -1,6 +1,5 @@
import integrationAPI from '../integrations';
import ApiClient from '../ApiClient';
import describeWithAPIMock from './apiSpecHelper';
describe('#integrationAPI', () => {
it('creates correct instance', () => {
@@ -16,11 +15,27 @@ describe('#integrationAPI', () => {
expect(integrationAPI).toHaveProperty('listAllSlackChannels');
expect(integrationAPI).toHaveProperty('deleteHook');
});
describeWithAPIMock('API calls', context => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
window.axios = axiosMock;
});
afterEach(() => {
window.axios = originalAxios;
});
it('#connectSlack', () => {
const code = 'SDNFJNSDFNDSJN';
integrationAPI.connectSlack(code);
expect(context.axiosMock.post).toHaveBeenCalledWith(
expect(axiosMock.post).toHaveBeenCalledWith(
'/api/v1/integrations/slack',
{
code,
@@ -31,7 +46,7 @@ describe('#integrationAPI', () => {
it('#updateSlack', () => {
const updateObj = { referenceId: 'SDFSDGSVE' };
integrationAPI.updateSlack(updateObj);
expect(context.axiosMock.patch).toHaveBeenCalledWith(
expect(axiosMock.patch).toHaveBeenCalledWith(
'/api/v1/integrations/slack',
{
reference_id: updateObj.referenceId,
@@ -41,16 +56,14 @@ describe('#integrationAPI', () => {
it('#listAllSlackChannels', () => {
integrationAPI.listAllSlackChannels();
expect(context.axiosMock.get).toHaveBeenCalledWith(
expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v1/integrations/slack/list_all_channels'
);
});
it('#delete', () => {
integrationAPI.delete(2);
expect(context.axiosMock.delete).toHaveBeenCalledWith(
'/api/v1/integrations/2'
);
expect(axiosMock.delete).toHaveBeenCalledWith('/api/v1/integrations/2');
});
it('#createHook', () => {
@@ -59,7 +72,7 @@ describe('#integrationAPI', () => {
settings: { api_key: 'SDFSDGSVE' },
};
integrationAPI.createHook(hookData);
expect(context.axiosMock.post).toHaveBeenCalledWith(
expect(axiosMock.post).toHaveBeenCalledWith(
'/api/v1/integrations/hooks',
hookData
);
@@ -67,7 +80,7 @@ describe('#integrationAPI', () => {
it('#deleteHook', () => {
integrationAPI.deleteHook(2);
expect(context.axiosMock.delete).toHaveBeenCalledWith(
expect(axiosMock.delete).toHaveBeenCalledWith(
'/api/v1/integrations/hooks/2'
);
});
@@ -1,6 +1,5 @@
import DyteAPIClient from '../../integrations/dyte';
import ApiClient from '../../ApiClient';
import describeWithAPIMock from '../apiSpecHelper';
describe('#accountAPI', () => {
it('creates correct instance', () => {
@@ -9,10 +8,26 @@ describe('#accountAPI', () => {
expect(DyteAPIClient).toHaveProperty('addParticipantToMeeting');
});
describeWithAPIMock('createAMeeting', context => {
describe('createAMeeting', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
window.axios = axiosMock;
});
afterEach(() => {
window.axios = originalAxios;
});
it('creates a valid request', () => {
DyteAPIClient.createAMeeting(1);
expect(context.axiosMock.post).toHaveBeenCalledWith(
expect(axiosMock.post).toHaveBeenCalledWith(
'/api/v1/integrations/dyte/create_a_meeting',
{
conversation_id: 1,
@@ -21,10 +36,26 @@ describe('#accountAPI', () => {
});
});
describeWithAPIMock('addParticipantToMeeting', context => {
describe('addParticipantToMeeting', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
window.axios = axiosMock;
});
afterEach(() => {
window.axios = originalAxios;
});
it('creates a valid request', () => {
DyteAPIClient.addParticipantToMeeting(1);
expect(context.axiosMock.post).toHaveBeenCalledWith(
expect(axiosMock.post).toHaveBeenCalledWith(
'/api/v1/integrations/dyte/add_participant_to_meeting',
{
message_id: 1,
@@ -1,6 +1,5 @@
import notificationsAPI from '../notifications';
import ApiClient from '../ApiClient';
import describeWithAPIMock from './apiSpecHelper';
describe('#NotificationAPI', () => {
it('creates correct instance', () => {
@@ -11,31 +10,47 @@ describe('#NotificationAPI', () => {
expect(notificationsAPI).toHaveProperty('read');
expect(notificationsAPI).toHaveProperty('readAll');
});
describeWithAPIMock('API calls', context => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
window.axios = axiosMock;
});
afterEach(() => {
window.axios = originalAxios;
});
it('#get', () => {
notificationsAPI.get(1);
expect(context.axiosMock.get).toHaveBeenCalledWith(
expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v1/notifications?page=1'
);
});
it('#getNotifications', () => {
notificationsAPI.getNotifications(1);
expect(context.axiosMock.get).toHaveBeenCalledWith(
expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v1/notifications/1/notifications'
);
});
it('#getUnreadCount', () => {
notificationsAPI.getUnreadCount();
expect(context.axiosMock.get).toHaveBeenCalledWith(
expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v1/notifications/unread_count'
);
});
it('#read', () => {
notificationsAPI.read(48670, 'Conversation');
expect(context.axiosMock.post).toHaveBeenCalledWith(
expect(axiosMock.post).toHaveBeenCalledWith(
'/api/v1/notifications/read_all',
{
primary_actor_id: 'Conversation',
@@ -46,7 +61,7 @@ describe('#NotificationAPI', () => {
it('#readAll', () => {
notificationsAPI.readAll();
expect(context.axiosMock.post).toHaveBeenCalledWith(
expect(axiosMock.post).toHaveBeenCalledWith(
'/api/v1/notifications/read_all'
);
});
@@ -1,6 +1,5 @@
import reportsAPI from '../reports';
import ApiClient from '../ApiClient';
import describeWithAPIMock from './apiSpecHelper';
describe('#Reports API', () => {
it('creates correct instance', () => {
@@ -18,14 +17,30 @@ describe('#Reports API', () => {
expect(reportsAPI).toHaveProperty('getInboxReports');
expect(reportsAPI).toHaveProperty('getTeamReports');
});
describeWithAPIMock('API calls', context => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
window.axios = axiosMock;
});
afterEach(() => {
window.axios = originalAxios;
});
it('#getAccountReports', () => {
reportsAPI.getReports({
metric: 'conversations_count',
from: 1621103400,
to: 1621621800,
});
expect(context.axiosMock.get).toHaveBeenCalledWith('/api/v2/reports', {
expect(axiosMock.get).toHaveBeenCalledWith('/api/v2/reports', {
params: {
metric: 'conversations_count',
since: 1621103400,
@@ -38,20 +53,17 @@ describe('#Reports API', () => {
it('#getAccountSummary', () => {
reportsAPI.getSummary(1621103400, 1621621800);
expect(context.axiosMock.get).toHaveBeenCalledWith(
'/api/v2/reports/summary',
{
params: {
business_hours: undefined,
group_by: undefined,
id: undefined,
since: 1621103400,
timezone_offset: -0,
type: 'account',
until: 1621621800,
},
}
);
expect(axiosMock.get).toHaveBeenCalledWith('/api/v2/reports/summary', {
params: {
business_hours: undefined,
group_by: undefined,
id: undefined,
since: 1621103400,
timezone_offset: -0,
type: 'account',
until: 1621621800,
},
});
});
it('#getAgentReports', () => {
@@ -60,60 +72,48 @@ describe('#Reports API', () => {
to: 1621621800,
businessHours: true,
});
expect(context.axiosMock.get).toHaveBeenCalledWith(
'/api/v2/reports/agents',
{
params: {
since: 1621103400,
until: 1621621800,
business_hours: true,
},
}
);
expect(axiosMock.get).toHaveBeenCalledWith('/api/v2/reports/agents', {
params: {
since: 1621103400,
until: 1621621800,
business_hours: true,
},
});
});
it('#getLabelReports', () => {
reportsAPI.getLabelReports({ from: 1621103400, to: 1621621800 });
expect(context.axiosMock.get).toHaveBeenCalledWith(
'/api/v2/reports/labels',
{
params: {
since: 1621103400,
until: 1621621800,
},
}
);
expect(axiosMock.get).toHaveBeenCalledWith('/api/v2/reports/labels', {
params: {
since: 1621103400,
until: 1621621800,
},
});
});
it('#getInboxReports', () => {
reportsAPI.getInboxReports({ from: 1621103400, to: 1621621800 });
expect(context.axiosMock.get).toHaveBeenCalledWith(
'/api/v2/reports/inboxes',
{
params: {
since: 1621103400,
until: 1621621800,
},
}
);
expect(axiosMock.get).toHaveBeenCalledWith('/api/v2/reports/inboxes', {
params: {
since: 1621103400,
until: 1621621800,
},
});
});
it('#getTeamReports', () => {
reportsAPI.getTeamReports({ from: 1621103400, to: 1621621800 });
expect(context.axiosMock.get).toHaveBeenCalledWith(
'/api/v2/reports/teams',
{
params: {
since: 1621103400,
until: 1621621800,
},
}
);
expect(axiosMock.get).toHaveBeenCalledWith('/api/v2/reports/teams', {
params: {
since: 1621103400,
until: 1621621800,
},
});
});
it('#getConversationMetric', () => {
reportsAPI.getConversationMetric('account');
expect(context.axiosMock.get).toHaveBeenCalledWith(
expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v2/reports/conversations',
{
params: {
@@ -1,6 +1,5 @@
import teamsAPI from '../teams';
import ApiClient from '../ApiClient';
import describeWithAPIMock from './apiSpecHelper';
describe('#TeamsAPI', () => {
it('creates correct instance', () => {
@@ -14,17 +13,33 @@ describe('#TeamsAPI', () => {
expect(teamsAPI).toHaveProperty('addAgents');
expect(teamsAPI).toHaveProperty('updateAgents');
});
describeWithAPIMock('API calls', context => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
window.axios = axiosMock;
});
afterEach(() => {
window.axios = originalAxios;
});
it('#getAgents', () => {
teamsAPI.getAgents({ teamId: 1 });
expect(context.axiosMock.get).toHaveBeenCalledWith(
expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v1/teams/1/team_members'
);
});
it('#addAgents', () => {
teamsAPI.addAgents({ teamId: 1, agentsList: { user_ids: [1, 10, 21] } });
expect(context.axiosMock.post).toHaveBeenCalledWith(
expect(axiosMock.post).toHaveBeenCalledWith(
'/api/v1/teams/1/team_members',
{
user_ids: { user_ids: [1, 10, 21] },
@@ -38,7 +53,7 @@ describe('#TeamsAPI', () => {
teamId: 1,
agentsList,
});
expect(context.axiosMock.patch).toHaveBeenCalledWith(
expect(axiosMock.patch).toHaveBeenCalledWith(
'/api/v1/teams/1/team_members',
{
user_ids: agentsList,
@@ -149,15 +149,11 @@
}
.multiselect-wrap--small {
.multiselect__tags,
.multiselect__input {
@apply items-center flex;
}
.multiselect__tags,
.multiselect__input,
.multiselect {
@apply bg-white dark:bg-slate-900 text-slate-800 dark:text-slate-100 rounded-[5px] text-sm h-10 min-h-[2.5rem];
@apply bg-white dark:bg-slate-900 text-slate-800 dark:text-slate-100 rounded-[5px] text-sm min-h-[2.5rem];
}
.multiselect__input {
@@ -165,11 +161,15 @@
}
.multiselect__single {
@apply items-center flex m-0 text-sm max-h-[2.375rem] text-slate-800 dark:text-slate-100 bg-white dark:bg-slate-900 py-1 px-0.5;
@apply items-center flex m-0 text-sm max-h-[2.375rem] text-slate-800 dark:text-slate-100 bg-white dark:bg-slate-900 py-3 px-0.5;
}
.multiselect__placeholder {
@apply m-0 py-1 px-0.5;
@apply m-0 py-2 px-0.5;
}
.multiselect__tag {
@apply py-[6px] my-[1px];
}
.multiselect__select {
@@ -181,9 +181,6 @@
@apply bg-transparent;
}
.multiselect__tags-wrap {
@apply flex-shrink-0;
}
}
.multiselect-wrap--medium {
@@ -2,10 +2,6 @@
transition: box-shadow 0.35s $swift-ease-out-function,
height 2s $swift-ease-out-function;
&.is-focused {
box-shadow: var(--shadow);
}
.reply-box__top {
.icon {
color: $medium-gray;
@@ -47,8 +43,6 @@
}
&.is-private {
@apply bg-yellow-100 dark:bg-yellow-800;
.reply-box__top {
@apply bg-yellow-100 dark:bg-yellow-800;
@@ -10,7 +10,8 @@
<div
class="flex items-center justify-between py-0 px-4"
:class="{
'pb-3 border-b border-slate-75 dark:border-slate-700': hasAppliedFiltersOrActiveFolders,
'pb-3 border-b border-slate-75 dark:border-slate-700':
hasAppliedFiltersOrActiveFolders,
}"
>
<div class="flex max-w-[85%] justify-center items-center">
@@ -24,9 +25,7 @@
v-if="!hasAppliedFiltersOrActiveFolders"
class="p-1 my-0.5 mx-1 rounded-md capitalize bg-slate-50 dark:bg-slate-800 text-xxs text-slate-600 dark:text-slate-300"
>
{{
this.$t(`CHAT_LIST.CHAT_STATUS_FILTER_ITEMS.${activeStatus}.TEXT`)
}}
{{ $t(`CHAT_LIST.CHAT_STATUS_FILTER_ITEMS.${activeStatus}.TEXT`) }}
</span>
</div>
<div class="flex items-center gap-1">
@@ -642,10 +641,8 @@ export default {
},
handleKeyEvents(e) {
if (hasPressedAltAndJKey(e)) {
const {
allConversations,
activeConversationIndex,
} = this.getKeyboardListenerParams();
const { allConversations, activeConversationIndex } =
this.getKeyboardListenerParams();
if (activeConversationIndex === -1) {
allConversations[0].click();
}
@@ -13,10 +13,10 @@
/>
<div class="flex flex-row justify-end gap-2 py-2 px-0 w-full">
<woot-button variant="clear" @click.prevent="onClose">
{{ this.$t('CONVERSATION.CUSTOM_SNOOZE.CANCEL') }}
{{ $t('CONVERSATION.CUSTOM_SNOOZE.CANCEL') }}
</woot-button>
<woot-button>
{{ this.$t('CONVERSATION.CUSTOM_SNOOZE.APPLY') }}
{{ $t('CONVERSATION.CUSTOM_SNOOZE.APPLY') }}
</woot-button>
</div>
</form>
@@ -1,6 +1,6 @@
<template>
<div class="text--container">
<woot-button size="small" class=" button--text" @click="onCopy">
<woot-button size="small" class="button--text" @click="onCopy">
{{ $t('COMPONENTS.CODE.BUTTON_TEXT') }}
</woot-button>
<woot-button
@@ -66,9 +66,9 @@ export default {
'right-aligned': 'right-aligned',
};
return `modal-mask skip-context-menu ${modalClassNameMap[
this.modalType
] || ''}`;
return `modal-mask skip-context-menu ${
modalClassNameMap[this.modalType] || ''
}`;
},
},
mounted() {
@@ -2,7 +2,8 @@
<div
class="ml-0 mr-0 flex pt-0 pr-4 pb-4 pl-0"
:class="{
'pt-4 border-b border-solid border-slate-50 dark:border-slate-700/30': showBorder,
'pt-4 border-b border-solid border-slate-50 dark:border-slate-700/30':
showBorder,
}"
>
<div class="w-[30%] min-w-0 max-w-[30%] pr-12">
@@ -72,10 +72,8 @@ export default {
const { custom_attributes: subscription } = account;
if (!subscription) return EMPTY_SUBSCRIPTION_INFO;
const {
subscription_status: status,
subscription_ends_on: endsOn,
} = subscription;
const { subscription_status: status, subscription_ends_on: endsOn } =
subscription;
return { status, endsOn: new Date(endsOn) };
},
@@ -1,4 +1,4 @@
const semver = require('semver');
import semver from 'semver';
export const hasAnUpdateAvailable = (latestVersion, currentVersion) => {
if (!semver.valid(latestVersion)) {
@@ -10,7 +10,7 @@
:is-loading="isLoading"
@click="onCmdResolveConversation"
>
{{ this.$t('CONVERSATION.HEADER.RESOLVE_ACTION') }}
{{ $t('CONVERSATION.HEADER.RESOLVE_ACTION') }}
</woot-button>
<woot-button
v-else-if="isResolved"
@@ -21,7 +21,7 @@
:is-loading="isLoading"
@click="onCmdOpenConversation"
>
{{ this.$t('CONVERSATION.HEADER.REOPEN_ACTION') }}
{{ $t('CONVERSATION.HEADER.REOPEN_ACTION') }}
</woot-button>
<woot-button
v-else-if="showOpenButton"
@@ -31,7 +31,7 @@
:is-loading="isLoading"
@click="onCmdOpenConversation"
>
{{ this.$t('CONVERSATION.HEADER.OPEN_ACTION') }}
{{ $t('CONVERSATION.HEADER.OPEN_ACTION') }}
</woot-button>
<woot-button
v-if="showAdditionalActions"
@@ -57,7 +57,7 @@
icon="snooze"
@click="() => openSnoozeModal()"
>
{{ this.$t('CONVERSATION.RESOLVE_DROPDOWN.SNOOZE_UNTIL') }}
{{ $t('CONVERSATION.RESOLVE_DROPDOWN.SNOOZE_UNTIL') }}
</woot-button>
</woot-dropdown-item>
<woot-dropdown-item v-if="!isPending">
@@ -68,7 +68,7 @@
icon="book-clock"
@click="() => toggleStatus(STATUS_TYPE.PENDING)"
>
{{ this.$t('CONVERSATION.RESOLVE_DROPDOWN.MARK_PENDING') }}
{{ $t('CONVERSATION.RESOLVE_DROPDOWN.MARK_PENDING') }}
</woot-button>
</woot-dropdown-item>
</woot-dropdown-menu>
@@ -34,8 +34,8 @@
<woot-submit-button
:disabled="
$v.accountName.$invalid ||
$v.accountName.$invalid ||
uiFlags.isCreating
$v.accountName.$invalid ||
uiFlags.isCreating
"
:button-text="$t('CREATE_ACCOUNT.FORM.SUBMIT')"
:loading="uiFlags.isCreating"
@@ -3,7 +3,8 @@
<button
class="text-slate-600 dark:text-slate-100 w-10 h-10 my-2 flex items-center justify-center rounded-lg hover:bg-slate-25 dark:hover:bg-slate-700 dark:hover:text-slate-100 hover:text-slate-600 relative"
:class="{
'bg-woot-50 dark:bg-slate-800 text-woot-500 hover:bg-woot-50': isNotificationPanelActive,
'bg-woot-50 dark:bg-slate-800 text-woot-500 hover:bg-woot-50':
isNotificationPanelActive,
}"
@click="openNotificationPanel"
>
@@ -9,7 +9,8 @@
class="font-medium h-7 my-1 hover:bg-slate-25 hover:text-bg-50 flex items-center px-2 rounded-md dark:hover:bg-slate-800"
:class="{
'bg-woot-25 dark:bg-slate-800': isActive,
'text-ellipsis overflow-hidden whitespace-nowrap max-w-full': shouldTruncate,
'text-ellipsis overflow-hidden whitespace-nowrap max-w-full':
shouldTruncate,
}"
@click="navigate"
>
@@ -44,7 +45,8 @@
class="text-sm text-slate-700 dark:text-slate-100"
:class="{
'text-woot-500 dark:text-woot-500': isActive,
'text-ellipsis overflow-hidden whitespace-nowrap max-w-full': shouldTruncate,
'text-ellipsis overflow-hidden whitespace-nowrap max-w-full':
shouldTruncate,
}"
>
{{ label }}
@@ -35,7 +35,8 @@
:class="{
'text-slate-300 dark:text-slate-600': isCountZero && !isActiveView,
'text-slate-600 dark:text-slate-50': !isCountZero && !isActiveView,
'bg-woot-75 dark:bg-woot-200 text-woot-600 dark:text-woot-600': isActiveView,
'bg-woot-75 dark:bg-woot-200 text-woot-600 dark:text-woot-600':
isActiveView,
'bg-slate-50 dark:bg-slate-700': !isActiveView,
}"
>
@@ -53,16 +53,24 @@ export default {
const createdTimeDiff = Date.now() - this.createdAtTimestamp * 1000;
const isBeforeAMonth = createdTimeDiff > DAY_IN_MILLI_SECONDS * 30;
return !isBeforeAMonth
? `Created ${this.createdAtTimeAgo}`
: `Created at: ${this.dateFormat(this.createdAtTimestamp)}`;
? `${this.$t('CHAT_LIST.CHAT_TIME_STAMP.CREATED.LATEST')} ${
this.createdAtTimeAgo
}`
: `${this.$t(
'CHAT_LIST.CHAT_TIME_STAMP.CREATED.OLDEST'
)} ${this.dateFormat(this.createdAtTimestamp)}`;
},
lastActivity() {
const lastActivityTimeDiff =
Date.now() - this.lastActivityTimestamp * 1000;
const isNotActive = lastActivityTimeDiff > DAY_IN_MILLI_SECONDS * 30;
return !isNotActive
? `Last activity ${this.lastActivityAtTimeAgo}`
: `Last activity: ${this.dateFormat(this.lastActivityTimestamp)}`;
? `${this.$t('CHAT_LIST.CHAT_TIME_STAMP.LAST_ACTIVITY.ACTIVE')} ${
this.lastActivityAtTimeAgo
}`
: `${this.$t(
'CHAT_LIST.CHAT_TIME_STAMP.LAST_ACTIVITY.NOT_ACTIVE'
)} ${this.dateFormat(this.lastActivityTimestamp)}`;
},
tooltipText() {
return `${this.createdAt}
@@ -1,6 +1,6 @@
<template>
<div class="animation-container margin-top-1">
<div class="ai-typing--wrap ">
<div class="ai-typing--wrap">
<fluent-icon icon="wand" size="14" class="ai-typing--icon" />
<label>
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.ASSISTANCE_MODAL.AI_WRITING') }}
@@ -1,3 +1,4 @@
<!-- eslint-disable vue/no-mutating-props -->
<template>
<div>
<div
@@ -255,7 +256,9 @@ export default {
value === 'conversation_attribute' ||
value === 'contact_attribute'
) {
// eslint-disable-next-line vue/no-mutating-props
this.value.custom_attribute_type = this.customAttributeType;
// eslint-disable-next-line vue/no-mutating-props
} else this.value.custom_attribute_type = '';
},
immediate: true,
@@ -23,6 +23,7 @@
@change="onFileChange"
/>
<div ref="editor" />
<slot name="footer" />
</div>
</template>
@@ -72,7 +73,9 @@ import { MESSAGE_EDITOR_MENU_OPTIONS } from 'dashboard/constants/editor';
const createState = (
content,
placeholder,
// eslint-disable-next-line default-param-last
plugins = [],
// eslint-disable-next-line default-param-last
methods = {},
enabledMenuOptions
) => {
@@ -709,6 +712,6 @@ export default {
}
.editor-warning__message {
@apply text-red-400 dark:text-red-400 font-normal pt-1 pb-0 px-0;
@apply text-red-400 dark:text-red-400 text-sm font-normal pt-1 pb-0 px-0;
}
</style>
@@ -32,7 +32,9 @@ const MAXIMUM_FILE_UPLOAD_SIZE = 4; // in MB
const createState = (
content,
placeholder,
// eslint-disable-next-line default-param-last
plugins = [],
// eslint-disable-next-line default-param-last
methods = {},
enabledMenuOptions
) => {
@@ -1,8 +1,6 @@
<template>
<div
:class="
`status-badge status-badge__${status} rounded-full w-2.5 h-2.5 mr-0.5 rtl:mr-0 rtl:ml-0.5 inline-flex`
"
:class="`status-badge status-badge__${status} rounded-full w-2.5 h-2.5 mr-0.5 rtl:mr-0 rtl:ml-0.5 inline-flex`"
/>
</template>
<script>
@@ -16,7 +16,7 @@
>
<div class="items-center flex justify-between last:mt-4">
<span class="text-slate-800 dark:text-slate-100 text-xs font-medium">{{
this.$t('CHAT_LIST.CHAT_SORT.STATUS')
$t('CHAT_LIST.CHAT_SORT.STATUS')
}}</span>
<filter-item
type="status"
@@ -28,7 +28,7 @@
</div>
<div class="items-center flex justify-between last:mt-4">
<span class="text-slate-800 dark:text-slate-100 text-xs font-medium">{{
this.$t('CHAT_LIST.CHAT_SORT.ORDER_BY')
$t('CHAT_LIST.CHAT_SORT.ORDER_BY')
}}</span>
<filter-item
type="sort"
@@ -87,7 +87,7 @@
class="-mt-0.5 align-middle inline-block text-slate-600 dark:text-slate-300"
:icon="attachmentIcon"
/>
{{ this.$t(`${attachmentMessageContent}`) }}
{{ $t(`${attachmentMessageContent}`) }}
</span>
<span v-else>
{{ $t('CHAT_LIST.NO_CONTENT') }}
@@ -103,7 +103,7 @@
icon="info"
/>
<span>
{{ this.$t(`CHAT_LIST.NO_MESSAGES`) }}
{{ $t(`CHAT_LIST.NO_MESSAGES`) }}
</span>
</p>
<div class="conversation--meta flex flex-col absolute right-4 top-4">
@@ -1,3 +1,4 @@
<!-- eslint-disable vue/no-mutating-props -->
<template>
<woot-modal :show.sync="show" :on-close="onCancel">
<div class="h-auto overflow-auto flex flex-col">
@@ -0,0 +1,28 @@
<template>
<div
class="my-0 mx-4 px-1 flex max-h-[8vh] items-baseline justify-between hover:bg-slate-25 dark:hover:bg-slate-800 border border-dashed border-slate-100 dark:border-slate-700 rounded-sm overflow-auto"
>
<p class="w-fit !m-0">
{{ $t('CONVERSATION.FOOTER.MESSAGE_SIGNATURE_NOT_CONFIGURED') }}
<woot-button
color-scheme="primary"
variant="link"
@click="openProfileSettings"
>
{{ $t('CONVERSATION.FOOTER.CLICK_HERE') }}
</woot-button>
</p>
</div>
</template>
<script>
export default {
methods: {
openProfileSettings() {
return this.$router.push({ name: 'profile_settings_index' });
},
},
};
</script>
<style></style>
@@ -450,16 +450,14 @@ export default {
// label suggestions are not part of the messages list
// so we need to handle them separately
let labelSuggestions = this.conversationPanel.querySelector(
'.label-suggestion'
);
let labelSuggestions =
this.conversationPanel.querySelector('.label-suggestion');
// if there are unread messages, scroll to the first unread message
if (this.unreadMessageCount > 0) {
// capturing only the unread messages
relevantMessages = this.conversationPanel.querySelectorAll(
'.message--unread'
);
relevantMessages =
this.conversationPanel.querySelectorAll('.message--unread');
} else if (labelSuggestions) {
// when scrolling to the bottom, the label suggestions is below the last message
// so we scroll there if there are no unread messages
@@ -8,8 +8,10 @@
}"
class="shrink-0 rounded-sm inline-flex w-3.5 h-3.5"
:class="{
'bg-red-50 dark:bg-red-700 dark:bg-opacity-30 text-red-500 dark:text-red-600': isUrgent,
'bg-slate-50 dark:bg-slate-700 text-slate-600 dark:text-slate-200': !isUrgent,
'bg-red-50 dark:bg-red-700 dark:bg-opacity-30 text-red-500 dark:text-red-600':
isUrgent,
'bg-slate-50 dark:bg-slate-700 text-slate-600 dark:text-slate-200':
!isUrgent,
}"
>
<fluent-icon
@@ -1,5 +1,11 @@
<template>
<div class="reply-box" :class="replyBoxClass">
<div
class="border-t border-slate-50 dark:border-slate-700 bg-white dark:bg-slate-900 reply-box"
:class="{
'bg-yellow-100 dark:bg-yellow-800 is-private': isPrivate,
'shadow-sm': isFocused || hasAttachments,
}"
>
<banner
v-if="showSelfAssignBanner"
action-button-variant="clear"
@@ -90,17 +96,9 @@
:remove-attachment="removeAttachment"
/>
</div>
<div
<message-signature-missing-alert
v-if="isSignatureEnabledForInbox && !isSignatureAvailable"
class="message-signature-wrap"
>
<p class="message-signature">
{{ $t('CONVERSATION.FOOTER.MESSAGE_SIGNATURE_NOT_CONFIGURED') }}
<router-link :to="profilePath">
{{ $t('CONVERSATION.FOOTER.CLICK_HERE') }}
</router-link>
</p>
</div>
/>
<reply-bottom-panel
:conversation-id="conversationId"
:enable-multiple-file-upload="enableMultipleFileUpload"
@@ -144,53 +142,58 @@
</template>
<script>
// Vuex
import { mapGetters } from 'vuex';
// Mixins
import { mixin as clickaway } from 'vue-clickaway';
import alertMixin from 'shared/mixins/alertMixin';
import CannedResponse from './CannedResponse.vue';
import ResizableTextArea from 'shared/components/ResizableTextArea.vue';
import AttachmentPreview from 'dashboard/components/widgets/AttachmentsPreview.vue';
import ReplyTopPanel from 'dashboard/components/widgets/WootWriter/ReplyTopPanel.vue';
import ReplyEmailHead from './ReplyEmailHead.vue';
import ReplyBottomPanel from 'dashboard/components/widgets/WootWriter/ReplyBottomPanel.vue';
import Banner from 'dashboard/components/ui/Banner.vue';
import { REPLY_EDITOR_MODES } from 'dashboard/components/widgets/WootWriter/constants';
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
import WootAudioRecorder from 'dashboard/components/widgets/WootWriter/AudioRecorder.vue';
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
import {
MAXIMUM_FILE_UPLOAD_SIZE,
MAXIMUM_FILE_UPLOAD_SIZE_TWILIO_SMS_CHANNEL,
AUDIO_FORMATS,
} from 'shared/constants/messages';
import { BUS_EVENTS } from 'shared/constants/busEvents';
import {
getMessageVariables,
getUndefinedVariablesInMessage,
replaceVariablesInMessage,
} from '@chatwoot/utils';
import WhatsappTemplates from './WhatsappTemplates/Modal.vue';
import { buildHotKeys } from 'shared/helpers/KeyboardHelpers';
import { MESSAGE_MAX_LENGTH } from 'shared/helpers/MessageTypeHelper';
import inboxMixin from 'shared/mixins/inboxMixin';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import { DirectUpload } from 'activestorage';
import { frontendURL } from '../../../helper/URLHelper';
import { trimContent, debounce } from '@chatwoot/utils';
import wootConstants from 'dashboard/constants/globals';
import { isEditorHotKeyEnabled } from 'dashboard/mixins/uiSettings';
import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
import rtlMixin from 'shared/mixins/rtlMixin';
import fileUploadMixin from 'dashboard/mixins/fileUploadMixin';
import replyDraftMixin from 'dashboard/mixins/replyDraftMixin';
import audioRecordingMixin from 'dashboard/mixins/audioRecordingMixin';
import emailEditorMixin from 'dashboard/mixins/emailEditorMixin';
import attachmentHandlerMixin from 'dashboard/mixins/attachmentHandlerMixin';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
// constants
import { REPLY_EDITOR_MODES } from 'dashboard/components/widgets/WootWriter/constants';
import { BUS_EVENTS } from 'shared/constants/busEvents';
import { MESSAGE_MAX_LENGTH } from 'shared/helpers/MessageTypeHelper';
import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
import wootConstants from 'dashboard/constants/globals';
// components
import AttachmentPreview from 'dashboard/components/widgets/AttachmentsPreview.vue';
import Banner from 'dashboard/components/ui/Banner.vue';
import CannedResponse from './CannedResponse.vue';
import EmojiInput from 'shared/components/emoji/EmojiInput.vue';
import MessageSignatureMissingAlert from './MessageSignatureMissingAlert.vue';
import ReplyBottomPanel from 'dashboard/components/widgets/WootWriter/ReplyBottomPanel.vue';
import ReplyEmailHead from './ReplyEmailHead.vue';
import ReplyTopPanel from 'dashboard/components/widgets/WootWriter/ReplyTopPanel.vue';
import ResizableTextArea from 'shared/components/ResizableTextArea.vue';
import WhatsappTemplates from './WhatsappTemplates/Modal.vue';
import WootAudioRecorder from 'dashboard/components/widgets/WootWriter/AudioRecorder.vue';
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
// Helpers
import { buildHotKeys } from 'shared/helpers/KeyboardHelpers';
import { isEditorHotKeyEnabled } from 'dashboard/mixins/uiSettings';
import {
appendSignature,
removeSignature,
replaceSignature,
extractTextFromMarkdown,
} from 'dashboard/helper/editorHelper';
const EmojiInput = () => import('shared/components/emoji/EmojiInput');
import {
getMessageVariables,
getUndefinedVariablesInMessage,
replaceVariablesInMessage,
debounce,
} from '@chatwoot/utils';
export default {
components: {
@@ -205,6 +208,7 @@ export default {
WootAudioRecorder,
Banner,
WhatsappTemplates,
MessageSignatureMissingAlert,
},
mixins: [
clickaway,
@@ -213,6 +217,11 @@ export default {
alertMixin,
messageFormatterMixin,
rtlMixin,
fileUploadMixin,
replyDraftMixin,
audioRecordingMixin,
emailEditorMixin,
attachmentHandlerMixin,
],
props: {
selectedTweet: {
@@ -233,17 +242,10 @@ export default {
message: '',
isFocused: false,
showEmojiPicker: false,
attachedFiles: [],
isRecordingAudio: false,
recordingAudioState: '',
recordingAudioDurationText: '',
isUploading: false,
replyType: REPLY_EDITOR_MODES.REPLY,
mentionSearchKey: '',
hasSlashCommand: false,
bccEmails: '',
ccEmails: '',
toEmails: '',
doAutoSaveDraft: () => {},
showWhatsAppTemplatesModal: false,
updateEditorSelectionWith: '',
@@ -400,15 +402,6 @@ export default {
: '(↵)';
return `${sendMessageText} ${keyLabel}`;
},
replyBoxClass() {
return {
'is-private': this.isPrivate,
'is-focused': this.isFocused || this.hasAttachments,
};
},
hasAttachments() {
return this.attachedFiles.length;
},
hasRecordedAudio() {
return (
this.$refs.audioRecorderInput &&
@@ -418,12 +411,6 @@ export default {
isRichEditorEnabled() {
return this.isAWebWidgetInbox || this.isAnEmailChannel;
},
showAudioRecorder() {
return !this.isOnPrivateNote && this.showFileUpload;
},
showAudioRecorderEditor() {
return this.showAudioRecorder && this.isRecordingAudio;
},
isOnPrivateNote() {
return this.replyType === REPLY_EDITOR_MODES.NOTE;
},
@@ -435,9 +422,8 @@ export default {
const {
LAYOUT_TYPES: { CONDENSED },
} = wootConstants;
const {
conversation_display_type: conversationDisplayType = CONDENSED,
} = this.uiSettings;
const { conversation_display_type: conversationDisplayType = CONDENSED } =
this.uiSettings;
return conversationDisplayType !== CONDENSED;
},
emojiDialogClassOnExpandedLayoutAndRTLView() {
@@ -485,9 +471,6 @@ export default {
const { send_with_signature: isEnabled } = this.uiSettings;
return isEnabled;
},
profilePath() {
return frontendURL(`accounts/${this.accountId}/profile/settings`);
},
editorMessageKey() {
const { editor_message_key: isEnabled } = this.uiSettings;
return isEnabled;
@@ -507,12 +490,6 @@ export default {
editorStateId() {
return `draft-${this.conversationIdByRoute}-${this.replyType}`;
},
audioRecordFormat() {
if (this.isAWhatsAppChannel || this.isAPIInbox) {
return AUDIO_FORMATS.OGG;
}
return AUDIO_FORMATS.WAV;
},
messageVariables() {
const variables = getMessageVariables({
conversation: this.currentChat,
@@ -570,7 +547,7 @@ export default {
mounted() {
this.getFromDraft();
// Donot use the keyboard listener mixin here as the events here are supposed to be
// Don't use the keyboard listener mixin here as the events here are supposed to be
// working even if input/textarea is focussed.
document.addEventListener('paste', this.onPaste);
document.addEventListener('keydown', this.handleKeyEvents);
@@ -610,31 +587,6 @@ export default {
);
}
},
saveDraft(conversationId, replyType) {
if (this.message || this.message === '') {
const key = `draft-${conversationId}-${replyType}`;
const draftToSave = trimContent(this.message || '');
this.$store.dispatch('draftMessages/set', {
key,
message: draftToSave,
});
}
},
setToDraft(conversationId, replyType) {
this.saveDraft(conversationId, replyType);
this.message = '';
},
getFromDraft() {
if (this.conversationIdByRoute) {
const key = `draft-${this.conversationIdByRoute}-${this.replyType}`;
const messageFromStore =
this.$store.getters['draftMessages/get'](key) || '';
// ensure that the message has signature set based on the ui setting
this.message = this.toggleSignatureForDraft(messageFromStore);
}
},
toggleSignatureForDraft(message) {
if (this.isPrivate) {
return message;
@@ -644,12 +596,6 @@ export default {
? appendSignature(message, this.signatureToApply)
: removeSignature(message, this.signatureToApply);
},
removeFromDraft() {
if (this.conversationIdByRoute) {
const key = `draft-${this.conversationIdByRoute}-${this.replyType}`;
this.$store.dispatch('draftMessages/delete', { key });
}
},
handleKeyEvents(e) {
const keyCode = buildHotKeys(e);
if (keyCode === 'escape') {
@@ -871,32 +817,9 @@ export default {
this.attachedFiles = [];
this.isRecordingAudio = false;
},
clearEmailField() {
this.ccEmails = '';
this.bccEmails = '';
this.toEmails = '';
},
toggleEmojiPicker() {
this.showEmojiPicker = !this.showEmojiPicker;
},
toggleAudioRecorder() {
this.isRecordingAudio = !this.isRecordingAudio;
this.isRecorderAudioStopped = !this.isRecordingAudio;
if (!this.isRecordingAudio) {
this.clearMessage();
this.clearEmailField();
}
},
toggleAudioRecorderPlayPause() {
if (this.isRecordingAudio) {
if (!this.isRecorderAudioStopped) {
this.isRecorderAudioStopped = true;
this.$refs.audioRecorderInput.stopAudioRecording();
} else if (this.isRecorderAudioStopped) {
this.$refs.audioRecorderInput.playPause();
}
}
},
hideEmojiPicker() {
if (this.showEmojiPicker) {
this.toggleEmojiPicker();
@@ -918,18 +841,6 @@ export default {
onFocus() {
this.isFocused = true;
},
onStateProgressRecorderChanged(duration) {
this.recordingAudioDurationText = duration;
},
onStateRecorderChanged(state) {
this.recordingAudioState = state;
if (state && 'notallowederror'.includes(state)) {
this.toggleAudioRecorder();
}
},
onFinishRecorder(file) {
return file && this.onFileUpload(file);
},
toggleTyping(status) {
const conversationId = this.currentChat.id;
const isPrivate = this.isPrivate;
@@ -944,85 +855,6 @@ export default {
isPrivate,
});
},
onFileUpload(file) {
if (this.globalConfig.directUploadsEnabled) {
this.onDirectFileUpload(file);
} else {
this.onIndirectFileUpload(file);
}
},
onDirectFileUpload(file) {
const MAXIMUM_SUPPORTED_FILE_UPLOAD_SIZE = this.isATwilioSMSChannel
? MAXIMUM_FILE_UPLOAD_SIZE_TWILIO_SMS_CHANNEL
: MAXIMUM_FILE_UPLOAD_SIZE;
if (!file) {
return;
}
if (checkFileSizeLimit(file, MAXIMUM_SUPPORTED_FILE_UPLOAD_SIZE)) {
const upload = new DirectUpload(
file.file,
`/api/v1/accounts/${this.accountId}/conversations/${this.currentChat.id}/direct_uploads`,
{
directUploadWillCreateBlobWithXHR: xhr => {
xhr.setRequestHeader(
'api_access_token',
this.currentUser.access_token
);
},
}
);
upload.create((error, blob) => {
if (error) {
this.showAlert(error);
} else {
this.attachFile({ file, blob });
}
});
} else {
this.showAlert(
this.$t('CONVERSATION.FILE_SIZE_LIMIT', {
MAXIMUM_SUPPORTED_FILE_UPLOAD_SIZE,
})
);
}
},
onIndirectFileUpload(file) {
const MAXIMUM_SUPPORTED_FILE_UPLOAD_SIZE = this.isATwilioSMSChannel
? MAXIMUM_FILE_UPLOAD_SIZE_TWILIO_SMS_CHANNEL
: MAXIMUM_FILE_UPLOAD_SIZE;
if (!file) {
return;
}
if (checkFileSizeLimit(file, MAXIMUM_SUPPORTED_FILE_UPLOAD_SIZE)) {
this.attachFile({ file });
} else {
this.showAlert(
this.$t('CONVERSATION.FILE_SIZE_LIMIT', {
MAXIMUM_SUPPORTED_FILE_UPLOAD_SIZE,
})
);
}
},
attachFile({ blob, file }) {
const reader = new FileReader();
reader.readAsDataURL(file.file);
reader.onloadend = () => {
this.attachedFiles.push({
currentChatId: this.currentChat.id,
resource: blob || file,
isPrivate: this.isPrivate,
thumb: reader.result,
blobSignedId: blob ? blob.signed_id : undefined,
});
};
},
removeAttachment(itemIndex) {
this.attachedFiles = this.attachedFiles.filter(
(item, index) => itemIndex !== index
);
},
getMessagePayloadForWhatsapp(message) {
const multipleMessagePayload = [];
@@ -1091,51 +923,6 @@ export default {
return messagePayload;
},
setCcEmails(value) {
this.bccEmails = value.bccEmails;
this.ccEmails = value.ccEmails;
},
setCCAndToEmailsFromLastChat() {
if (!this.lastEmail) return;
const {
content_attributes: { email: emailAttributes = {} },
} = this.lastEmail;
// Retrieve the email of the current conversation's sender
const conversationContact = this.currentChat?.meta?.sender?.email || '';
let cc = emailAttributes.cc ? [...emailAttributes.cc] : [];
let to = [];
// there might be a situation where the current conversation will include a message from a third person,
// and the current conversation contact is in CC.
// This is an edge-case, reported here: CW-1511 [ONLY FOR INTERNAL REFERENCE]
// So we remove the current conversation contact's email from the CC list if present
if (cc.includes(conversationContact)) {
cc = cc.filter(email => email !== conversationContact);
}
// If the last incoming message sender is different from the conversation contact, add them to the "to"
// and add the conversation contact to the CC
if (!emailAttributes.from.includes(conversationContact)) {
to.push(...emailAttributes.from);
cc.push(conversationContact);
}
// Remove the conversation contact's email from the BCC list if present
let bcc = (emailAttributes.bcc || []).filter(
email => email !== conversationContact
);
// Ensure only unique email addresses are in the CC list
bcc = [...new Set(bcc)];
cc = [...new Set(cc)];
to = [...new Set(to)];
this.ccEmails = cc.join(', ');
this.bccEmails = bcc.join(', ');
this.toEmails = to.join(', ');
},
},
};
</script>
@@ -1149,25 +936,10 @@ export default {
@apply py-2;
}
.message-signature-wrap {
@apply my-0 mx-4 px-1 flex max-h-[8vh] items-baseline justify-between hover:bg-slate-25 dark:hover:bg-slate-800 border border-dashed border-slate-100 dark:border-slate-700 rounded-sm overflow-auto;
}
.message-signature {
@apply w-fit m-0;
}
.attachment-preview-box {
@apply bg-transparent py-0 px-4;
}
.reply-box {
@apply border-t border-slate-50 dark:border-slate-700 bg-white dark:bg-slate-900;
&.is-private {
@apply bg-yellow-50 dark:bg-yellow-200;
}
}
.send-button {
@apply mb-0;
}
@@ -1202,13 +974,6 @@ export default {
@apply left-1 -bottom-2;
}
}
.message-signature {
@apply mb-0;
::v-deep p:last-child {
@apply mb-0;
}
}
.normal-editor__canned-box {
width: calc(100% - 2 * var(--space-normal));
@@ -1,3 +1,4 @@
<!-- eslint-disable vue/no-mutating-props -->
<template>
<woot-modal :show.sync="show" :on-close="onClose" size="modal-big">
<woot-modal-header
@@ -187,8 +187,9 @@ export default {
return '';
}
const { screenName, sourceId } = this;
return `https://twitter.com/${screenName ||
this.inbox.name}/status/${sourceId}`;
return `https://twitter.com/${
screenName || this.inbox.name
}/status/${sourceId}`;
},
linkToStory() {
if (!this.storyId || !this.storySender) {
@@ -57,9 +57,8 @@ export default {
async joinTheCall() {
this.isLoading = true;
try {
const {
data: { authResponse: { authToken } = {} } = {},
} = await DyteAPI.addParticipantToMeeting(this.messageId);
const { data: { authResponse: { authToken } = {} } = {} } =
await DyteAPI.addParticipantToMeeting(this.messageId);
this.dyteAuthToken = authToken;
} catch (err) {
this.showAlert(this.$t('INTEGRATION_SETTINGS.DYTE.JOIN_ERROR'));
@@ -1,3 +1,4 @@
<!-- eslint-disable vue/no-mutating-props -->
<template>
<woot-modal
full-width
@@ -212,8 +213,13 @@ export default {
return this.activeFileType === ALLOWED_FILE_TYPES.AUDIO;
},
senderDetails() {
const { name, available_name: availableName, avatar_url, thumbnail, id } =
this.activeAttachment?.sender || this.attachment?.sender;
const {
name,
available_name: availableName,
avatar_url,
thumbnail,
id,
} = this.activeAttachment?.sender || this.attachment?.sender || {};
const currentUserID = this.currentUser?.id;
return {
name: currentUserID === id ? 'You' : name || availableName || '',
@@ -34,15 +34,13 @@
:option="labelMenuConfig"
:sub-menu-available="!!labels.length"
>
<template>
<menu-item
v-for="label in labels"
:key="label.id"
:option="generateMenuLabelConfig(label, 'label')"
variant="label"
@click="$emit('assign-label', label)"
/>
</template>
<menu-item
v-for="label in labels"
:key="label.id"
:option="generateMenuLabelConfig(label, 'label')"
variant="label"
@click="$emit('assign-label', label)"
/>
</menu-item-with-submenu>
<menu-item-with-submenu
:option="agentMenuConfig"
@@ -17,7 +17,7 @@
type="button"
@click="onAvatarDelete"
>
{{ this.$t('INBOX_MGMT.DELETE.AVATAR_DELETE_BUTTON_TEXT') }}
{{ $t('INBOX_MGMT.DELETE.AVATAR_DELETE_BUTTON_TEXT') }}
</woot-button>
</div>
<label>
@@ -63,8 +63,9 @@
<div v-if="filteredCountriesBySearch.length === 0">
<span
class="flex items-center justify-center text-sm text-slate-500 dark:text-slate-300 mt-4"
>No results found</span
>
No results found
</span>
</div>
</div>
</div>
@@ -1,3 +1,4 @@
<!-- eslint-disable vue/no-mutating-props -->
<template>
<modal :show.sync="show" :on-close="closeModal">
<woot-modal-header :header-title="title" :header-content="message" />
@@ -1,3 +1,4 @@
<!-- eslint-disable vue/no-mutating-props -->
<template>
<modal :show.sync="show" :on-close="onClose">
<woot-modal-header
@@ -30,9 +30,7 @@
<hotkey custom-class="min-h-[28px] min-w-[60px] normal-case key">
{{ $t('KEYBOARD_SHORTCUTS.KEYS.ALT_OR_OPTION_KEY') }}
</hotkey>
<hotkey custom-class="min-h-[28px] w-9 key">
J
</hotkey>
<hotkey custom-class="min-h-[28px] w-9 key"> J </hotkey>
<span
class="flex items-center font-semibold text-sm text-slate-800 dark:text-slate-100"
>
@@ -42,9 +40,7 @@
<hotkey custom-class="min-h-[28px] min-w-[60px] normal-case key">
{{ $t('KEYBOARD_SHORTCUTS.KEYS.ALT_OR_OPTION_KEY') }}
</hotkey>
<hotkey custom-class="w-9 key">
K
</hotkey>
<hotkey custom-class="w-9 key"> K </hotkey>
</div>
</div>
@@ -59,9 +55,7 @@
<hotkey custom-class="min-h-[28px] min-w-[60px] normal-case key">
{{ $t('KEYBOARD_SHORTCUTS.KEYS.ALT_OR_OPTION_KEY') }}
</hotkey>
<hotkey custom-class="w-9 key">
E
</hotkey>
<hotkey custom-class="w-9 key"> E </hotkey>
</div>
</div>
<div
@@ -97,6 +97,7 @@ class DashboardAudioNotificationHelper {
return conversationAssigneeId === this.currentUserId;
};
// eslint-disable-next-line class-methods-use-this
isMessageFromCurrentConversation = message => {
return (
window.WOOT.$store.getters.getSelectedChat?.id === message.conversation_id
+2 -1
View File
@@ -60,6 +60,7 @@ export const conversationListPageURL = ({
export const isValidURL = value => {
/* eslint-disable no-useless-escape */
const URL_REGEX = /^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$/gm;
const URL_REGEX =
/^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$/gm;
return URL_REGEX.test(value);
};
@@ -102,6 +102,7 @@ class ActionCableConnector extends BaseActionCableConnector {
this.app.$store.dispatch('updateConversation', data);
};
// eslint-disable-next-line class-methods-use-this
onLogout = () => AuthAPI.logout();
onMessageCreated = data => {
@@ -117,6 +118,7 @@ class ActionCableConnector extends BaseActionCableConnector {
});
};
// eslint-disable-next-line class-methods-use-this
onReload = () => window.location.reload();
onStatusChange = data => {
@@ -171,6 +173,7 @@ class ActionCableConnector extends BaseActionCableConnector {
}, 30000);
};
// eslint-disable-next-line class-methods-use-this
fetchConversationStats = () => {
bus.$emit('fetch_conversation_stats');
bus.$emit('fetch_overview_reports');
@@ -192,6 +195,7 @@ class ActionCableConnector extends BaseActionCableConnector {
this.app.$store.dispatch('notifications/addNotification', data);
};
// eslint-disable-next-line class-methods-use-this
onFirstReplyCreated = () => {
bus.$emit('fetch_overview_reports');
};
@@ -259,7 +259,9 @@ export const isCustomAttribute = (attrs, key) => {
};
export const generateCustomAttributes = (
// eslint-disable-next-line default-param-last
conversationAttributes = [],
// eslint-disable-next-line default-param-last
contactAttributes = [],
conversationlabel,
contactlabel
@@ -70,15 +70,8 @@ const getValuesForCountries = (values, countries) => {
export const getValuesForFilter = (filter, params) => {
const { attribute_key, values } = filter;
const {
languages,
countries,
agents,
inboxes,
teams,
campaigns,
labels,
} = params;
const { languages, countries, agents, inboxes, teams, campaigns, labels } =
params;
switch (attribute_key) {
case 'status':
return getValuesForStatus(values);
@@ -3,6 +3,7 @@ import {
MessageMarkdownTransformer,
MessageMarkdownSerializer,
} from '@chatwoot/prosemirror-schema';
import * as Sentry from '@sentry/browser';
/**
* The delimiter used to separate the signature from the rest of the body.
@@ -14,9 +15,25 @@ export const SIGNATURE_DELIMITER = '--';
* Parse and Serialize the markdown text to remove any extra spaces or new lines
*/
export function cleanSignature(signature) {
// convert from markdown to common mark format
const nodes = new MessageMarkdownTransformer(messageSchema).parse(signature);
return MessageMarkdownSerializer.serialize(nodes);
try {
// remove any horizontal rule tokens
signature = signature
.replace(/^( *\* *){3,} *$/gm, '')
.replace(/^( *- *){3,} *$/gm, '')
.replace(/^( *_ *){3,} *$/gm, '');
const nodes = new MessageMarkdownTransformer(messageSchema).parse(
signature
);
return MessageMarkdownSerializer.serialize(nodes);
} catch (e) {
// eslint-disable-next-line no-console
console.warn(e);
Sentry.captureException(e);
// The parser can break on some cases
// for example, Token type `hr` not supported by Markdown parser
return signature;
}
}
/**
@@ -1,7 +1,9 @@
// eslint-disable-next-line default-param-last
export const getCurrentAccount = ({ accounts } = {}, accountId) => {
return accounts.find(account => account.id === accountId);
};
// eslint-disable-next-line default-param-last
export const getUserRole = ({ accounts } = {}, accountId) => {
const currentAccount = getCurrentAccount({ accounts }, accountId) || {};
return currentAccount.role || null;
@@ -16,10 +16,8 @@ describe('Helper functions', () => {
const changes = {
role: [0, 1],
};
const {
changes: extractedChanges,
values,
} = extractChangedAccountUserValues(changes);
const { changes: extractedChanges, values } =
extractChangedAccountUserValues(changes);
expect(extractedChanges).toEqual(['role']);
expect(values).toEqual(['administrator']);
});
@@ -28,10 +26,8 @@ describe('Helper functions', () => {
const changes = {
availability: [0, 2],
};
const {
changes: extractedChanges,
values,
} = extractChangedAccountUserValues(changes);
const { changes: extractedChanges, values } =
extractChangedAccountUserValues(changes);
expect(extractedChanges).toEqual(['availability']);
expect(values).toEqual(['busy']);
});
@@ -41,10 +37,8 @@ describe('Helper functions', () => {
role: [1, 0],
availability: [1, 2],
};
const {
changes: extractedChanges,
values,
} = extractChangedAccountUserValues(changes);
const { changes: extractedChanges, values } =
extractChangedAccountUserValues(changes);
expect(extractedChanges).toEqual(['role', 'availability']);
expect(values).toEqual(['agent', 'busy']);
});
@@ -85,6 +85,37 @@ describe('appendSignature', () => {
});
});
describe('cleanSignature', () => {
it('removes any instance of horizontal rule', () => {
const options = [
'---',
'***',
'___',
'- - -',
'* * *',
'_ _ _',
' ---',
'--- ',
' --- ',
'-----',
'*****',
'_____',
'- - - -',
'* * * * *',
'_ _ _ _ _ _',
' - - - - ',
' * * * * * ',
' _ _ _ _ _ _',
'- - - - -',
'* * * * * *',
'_ _ _ _ _ _ _',
];
options.forEach(option => {
expect(cleanSignature(option)).toBe('');
});
});
});
describe('removeSignature', () => {
it('does not remove signature if not present', () => {
Object.keys(DOES_NOT_HAVE_SIGNATURE).forEach(key => {
@@ -97,9 +128,8 @@ describe('removeSignature', () => {
expect(removeSignature(body, signature)).toBe('This is a test\n\n');
});
it('removes signature if present with spaces and new lines', () => {
const { body, signature } = HAS_SIGNATURE[
'signature at end with spaces and new lines'
];
const { body, signature } =
HAS_SIGNATURE['signature at end with spaces and new lines'];
expect(removeSignature(body, signature)).toBe('This is a test\n\n');
});
it('removes signature if present without text before it', () => {
@@ -129,9 +159,8 @@ describe('replaceSignature', () => {
);
});
it('removes signature if present with spaces and new lines', () => {
const { body, signature } = HAS_SIGNATURE[
'signature at end with spaces and new lines'
];
const { body, signature } =
HAS_SIGNATURE['signature at end with spaces and new lines'];
expect(replaceSignature(body, signature, NEW_SIGNATURE)).toBe(
`This is a test\n\n--\n\n${NEW_SIGNATURE}`
);
@@ -371,6 +371,8 @@
"DETAILS": {
"LOADING_FB": "Authenticating you with Facebook...",
"ERROR_FB_AUTH": "Something went wrong, Please refresh page...",
"ERROR_FB_UNAUTHORIZED": "You're not authorized to perform this action. ",
"ERROR_FB_UNAUTHORIZED_HELP": "Please ensure you have access to the Facebook page with full control. You can read more about Facebook roles <a href=\" https://www.facebook.com/help/187316341316631\">here</a>.",
"CREATING_CHANNEL": "Creating your Inbox...",
"TITLE": "Configure Inbox Details",
"DESC": ""
@@ -36,7 +36,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 your email inbox. Use the rich content editor to create a highly personalised signature.",
"NOTE": "Create a unique message signature to appear at the end of every message you send from any inbox. You can also include an inline image, which is supported in live-chat, email, and API inboxes.",
"BTN_TEXT": "Save message signature",
"API_ERROR": "Couldn't save signature! Try again",
"API_SUCCESS": "Signature saved successfully",
@@ -371,6 +371,8 @@
"DETAILS": {
"LOADING_FB": "جار المصادقة والربط مع الفيسبوك...",
"ERROR_FB_AUTH": "حدث خطأ ما، الرجاء تحديث الصفحة...",
"ERROR_FB_UNAUTHORIZED": "You're not authorized to perform this action. ",
"ERROR_FB_UNAUTHORIZED_HELP": "Please ensure you have access to the Facebook page with full control. You can read more about Facebook roles <a href=\" https://www.facebook.com/help/187316341316631\">here</a>.",
"CREATING_CHANNEL": "جار إنشاء قناة التواصل...",
"TITLE": "تهيئة إعدادات قناة التواصل",
"DESC": ""
@@ -36,7 +36,7 @@
},
"MESSAGE_SIGNATURE_SECTION": {
"TITLE": "توقيع الرسالة الشخصية",
"NOTE": "إنشاء توقيع رسالة شخصية يتم إضافتها إلى جميع الرسائل التي ترسلها من المنصة. استخدم محرر المحتوى الغني لإنشاء توقيع شديد التخصيص.",
"NOTE": "Create a unique message signature to appear at the end of every message you send from any inbox. You can also include an inline image, which is supported in live-chat, email, and API inboxes.",
"BTN_TEXT": "حفظ توقيع الرسالة",
"API_ERROR": "تعذر إرسال الرسالة! حاول مرة أخرى",
"API_SUCCESS": "تم حفظ التوقيع بنجاح",
@@ -371,6 +371,8 @@
"DETAILS": {
"LOADING_FB": "Authenticating you with Facebook...",
"ERROR_FB_AUTH": "Something went wrong, Please refresh page...",
"ERROR_FB_UNAUTHORIZED": "You're not authorized to perform this action. ",
"ERROR_FB_UNAUTHORIZED_HELP": "Please ensure you have access to the Facebook page with full control. You can read more about Facebook roles <a href=\" https://www.facebook.com/help/187316341316631\">here</a>.",
"CREATING_CHANNEL": "Creating your Inbox...",
"TITLE": "Configure Inbox Details",
"DESC": ""
@@ -36,7 +36,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 your email inbox. Use the rich content editor to create a highly personalised signature.",
"NOTE": "Create a unique message signature to appear at the end of every message you send from any inbox. You can also include an inline image, which is supported in live-chat, email, and API inboxes.",
"BTN_TEXT": "Save message signature",
"API_ERROR": "Couldn't save signature! Try again",
"API_SUCCESS": "Signature saved successfully",
@@ -371,6 +371,8 @@
"DETAILS": {
"LOADING_FB": "S'està autenticant amb Facebook...",
"ERROR_FB_AUTH": "Alguna cosa ha anat malament, actualitza la pàgina...",
"ERROR_FB_UNAUTHORIZED": "You're not authorized to perform this action. ",
"ERROR_FB_UNAUTHORIZED_HELP": "Please ensure you have access to the Facebook page with full control. You can read more about Facebook roles <a href=\" https://www.facebook.com/help/187316341316631\">here</a>.",
"CREATING_CHANNEL": "S'està creant la safata d'entrada...",
"TITLE": "Configura els detalls de la safata d'entrada",
"DESC": ""
@@ -36,7 +36,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 your email inbox. Use the rich content editor to create a highly personalised signature.",
"NOTE": "Create a unique message signature to appear at the end of every message you send from any inbox. You can also include an inline image, which is supported in live-chat, email, and API inboxes.",
"BTN_TEXT": "Save message signature",
"API_ERROR": "Couldn't save signature! Try again",
"API_SUCCESS": "Signature saved successfully",
@@ -371,6 +371,8 @@
"DETAILS": {
"LOADING_FB": "Ověřování pomocí Facebooku...",
"ERROR_FB_AUTH": "Něco se pokazilo, prosím obnovte stránku...",
"ERROR_FB_UNAUTHORIZED": "You're not authorized to perform this action. ",
"ERROR_FB_UNAUTHORIZED_HELP": "Please ensure you have access to the Facebook page with full control. You can read more about Facebook roles <a href=\" https://www.facebook.com/help/187316341316631\">here</a>.",
"CREATING_CHANNEL": "Vytvářím vaši doručenou poštu...",
"TITLE": "Konfigurace detailů doručené pošty",
"DESC": ""
@@ -36,7 +36,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 your email inbox. Use the rich content editor to create a highly personalised signature.",
"NOTE": "Create a unique message signature to appear at the end of every message you send from any inbox. You can also include an inline image, which is supported in live-chat, email, and API inboxes.",
"BTN_TEXT": "Save message signature",
"API_ERROR": "Couldn't save signature! Try again",
"API_SUCCESS": "Signature saved successfully",
@@ -371,6 +371,8 @@
"DETAILS": {
"LOADING_FB": "Autentificerer dig med Facebook...",
"ERROR_FB_AUTH": "Noget gik galt, Opdatér siden...",
"ERROR_FB_UNAUTHORIZED": "You're not authorized to perform this action. ",
"ERROR_FB_UNAUTHORIZED_HELP": "Please ensure you have access to the Facebook page with full control. You can read more about Facebook roles <a href=\" https://www.facebook.com/help/187316341316631\">here</a>.",
"CREATING_CHANNEL": "Opretter din indbakke...",
"TITLE": "Indstil Indbakkedetaljer",
"DESC": ""
@@ -36,7 +36,7 @@
},
"MESSAGE_SIGNATURE_SECTION": {
"TITLE": "Personlig beskedsignatur",
"NOTE": "Opret en personlig besked signatur, der vil blive føjet til alle de meddelelser, du sender fra din e-mail indbakke. Brug den rige content editor til at oprette en meget personlig signatur.",
"NOTE": "Create a unique message signature to appear at the end of every message you send from any inbox. You can also include an inline image, which is supported in live-chat, email, and API inboxes.",
"BTN_TEXT": "Gem beskedsignatur",
"API_ERROR": "Kunne ikke gemme signatur! Prøv igen",
"API_SUCCESS": "Signatur gemt",
@@ -371,6 +371,8 @@
"DETAILS": {
"LOADING_FB": "Authentifizierung mit Facebook ...",
"ERROR_FB_AUTH": "Es ist ein Fehler aufgetreten. Bitte Seite aktualisieren ...",
"ERROR_FB_UNAUTHORIZED": "You're not authorized to perform this action. ",
"ERROR_FB_UNAUTHORIZED_HELP": "Please ensure you have access to the Facebook page with full control. You can read more about Facebook roles <a href=\" https://www.facebook.com/help/187316341316631\">here</a>.",
"CREATING_CHANNEL": "Erstellen Sie Ihren Posteingang ...",
"TITLE": "Posteingangsdetails konfigurieren",
"DESC": ""

Some files were not shown because too many files have changed in this diff Show More