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
Sojan 3eefa4daee Merge branch 'release/3.1.1'
Publish Chatwoot CE docker images / build (push) Waiting to run
2023-09-22 19:54:43 -07:00
Sojan ddce999385 Bump version to 3.1.1 2023-09-22 19:54:20 -07:00
ff915dd2ea fix: Avoid joining tables to fix the distinct value query (#7965)
DISTINCT query with custom attributes return an error. To avoid the error, this PR refactors the query to include tags only when it is required.

Fixes #7931
Fixes #7836

Co-authored-by: Sojan Jose <sojan@pepalo.com>
2023-09-22 19:48:56 -07:00
dd0930d75e chore: Improve search in super admin panel (#7952)
Co-authored-by: Vishnu Narayanan <vishnu@chatwoot.com>
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2023-09-22 19:41:13 +05:30
Shivam MishraandGitHub 3f0d96c24d chore: Add better error messages for Facebook unauthorized (#7936) 2023-09-22 16:41:04 +05:30
Pranav Raj SandGitHub c16b801562 chore: Update the filter for articles in the widget (#7961) 2023-09-21 19:40:46 +05:30
018c468303 chore: Refactors widget to use i18n locale for articles (#7958)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2023-09-21 18:24:03 +05:30
48bf8d08e5 feat: Update dependencies and fix import syntax for Vite migration (#7959)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2023-09-21 17:55:54 +05:30
Nithin David ThomasandGitHub fabb3c8da4 fix: Help Center articles are not available on the widget [CW-2534] (#7954) 2023-09-21 17:06:15 +05:30
bdeb2f9812 feat: Searching contacts by company name (#7867)
Implement the functionality of being able to search the contacts page by company name.
Fixes: #7818

Co-authored-by: Sojan Jose <sojan@pepalo.com>
2023-09-21 00:53:12 -07:00
53dc38e650 fix: Renders youtube and vimeo links within blank lines as embeds (#7422)
Within the article we are now rendering every link for youtube and vimeo as embeds. This isn't a good solution, as users might need to have plain links as well. This fix will render only links within two blank lines as embeds.

Co-authored-by: Sojan Jose <sojan@pepalo.com>
2023-09-21 00:16:03 -07:00
27fc24375d fix: Handling markdown before replacing or appending signature [CW-2532] (#7944)
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2023-09-21 11:28:29 +05:30
f999777a2d chore: Update message signature description (#7943)
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2023-09-20 21:09:25 -07:00
Pranav Raj SandGitHub 71d1e98765 chore: Remove spinkit from dependencies (#7948) 2023-09-20 18:18:55 +05:30
Sivin VargheseandGitHub 2c93d563c1 fix: Update colors to fix dark mode in the editor add-link modal header (#7947) 2023-09-20 17:29:27 +05:30
Pranav Raj SandGitHub f1b556d4a3 fix: Downcase content filter values to fix the query (#7942) 2023-09-20 13:14:00 +05:30
Chatwoot BotandGitHub 73c119cd3d chore: Update translations (#7937) 2023-09-19 21:19:46 -07:00
Smit BosamiyaandGitHub 0ad2f94151 fix: Error when attempting to Delete User-Associated Accounts via Super Admin Console (#7810)
This pull request addresses the issue where an error is encountered while attempting to delete user-associated accounts through the Super Admin Console. The problem arises from an error that occurs when the "destroy" action is triggered on an associated account.

Fixes: #7809
2023-09-19 21:16:11 -07:00
f8cb806548 fix: remove trailing spaces before pushing signature (#7935)
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2023-09-19 13:29:44 +05:30
9ba5adfd60 chore: Support multiple values for automation message content (#7871)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2023-09-19 13:04:58 +05:30
Muhsin KelothandGitHub 0dd5104acb chore: Move slack message concern to the helper (#7912) 2023-09-19 11:09:23 +05:30
Sojan JoseandGitHub 0968aead6c chore: Migration status in super admin (#7938) 2023-09-19 10:31:41 +05:30
Shivam MishraandGitHub 53d530b815 feat: Add upload under account scope (#7914) 2023-09-19 09:51:54 +05:30
2429daa45c feat: cwctl upgrade node for existing installations (#7772)
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2023-09-18 23:49:29 +05:30
Sojan 86894b27b5 Merge branch 'release/3.1.0' into develop 2023-09-15 16:46:47 -07:00
Sojan 05df4d4212 Merge branch 'release/3.1.0'
Publish Chatwoot CE docker images / build (push) Waiting to run
2023-09-15 16:46:37 -07:00
Sojan 47fd0ea2b2 Bump version to 3.1.0 2023-09-15 16:45:59 -07:00
Sojan JoseandGitHub 30b8a365dc chore: Improve the feature management styles in super admin (#7920) 2023-09-15 15:55:15 -07:00
29110ffd6b feat: Allow signature in the editor directly (#7881)
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2023-09-15 18:46:40 +05:30
Sojan fe8a67b76a Merge branch 'release/3.0.0'
Publish Chatwoot CE docker images / build (push) Waiting to run
2023-08-15 22:41:04 -07:00
Sojan e204fe1c1f Merge branch 'release/2.18.0'
Publish Chatwoot CE docker images / build (push) Waiting to run
2023-06-15 18:44:51 +05:30
Sojan 08fa370261 Merge branch 'hotfix/2.17.1'
Publish Chatwoot CE docker images / build (push) Waiting to run
2023-05-19 20:56:06 +05:30
697 changed files with 13749 additions and 4185 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'
+2 -1
View File
@@ -76,6 +76,7 @@ gem 'pundit'
# super admin
gem 'administrate', '>= 0.19.0'
gem 'administrate-field-active_storage'
gem 'administrate-field-belongs_to_search'
##--- gems for pubsub service ---##
# https://karolgalanciak.com/blog/2019/11/30/from-activerecord-callbacks-to-publish-slash-subscribe-pattern-and-event-driven-design/
@@ -158,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'
+9 -3
View File
@@ -116,13 +116,18 @@ GEM
administrate-field-active_storage (0.4.2)
administrate (>= 0.2.2)
rails (>= 7.0)
administrate-field-belongs_to_search (0.8.0)
administrate (>= 0.3, < 1.0)
jbuilder (~> 2)
rails (>= 4.2, < 7.1)
selectize-rails (~> 0.6)
annotate (3.2.0)
activerecord (>= 3.2, < 8.0)
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)
@@ -831,9 +836,10 @@ DEPENDENCIES
acts-as-taggable-on
administrate (>= 0.19.0)
administrate-field-active_storage
administrate-field-belongs_to_search
annotate
attr_extras
audited (~> 5.3)
audited (~> 5.4, >= 5.4.0)
aws-sdk-s3
azure-storage-blob!
barnes
+1 -1
View File
@@ -1 +1 @@
2.17.0
3.1.0
+1 -1
View File
@@ -1 +1 @@
2.3.0
2.4.0
@@ -29,3 +29,5 @@
@import 'components/pagination';
@import 'components/search';
@import 'components/reports';
@import 'custom_styles';
@@ -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;
@@ -0,0 +1,24 @@
// custom styles for the dashboard
.feature-cell {
background: $color-extra-light-blue;
border-radius: 10px;
float: left;
margin-left: 8px;
margin-top: 6px;
padding: 4px 12px;
.icon-container {
margin-right: 4px;
}
.value-container {
margin-left: 6px;
}
}
.feature-container {
max-width: 100rem;
}
@@ -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;
@@ -78,11 +78,6 @@ $conv-header-height: 4rem;
$inbox-thumb-size: 4.8rem;
// Spinner
$spinkit-spinner-color: $color-white !default;
$spinkit-spinner-margin: 0 0 0 1.6rem !default;
$spinkit-size: 1.6rem !default;
// Snackbar default
$woot-snackbar-bg: #323232;
$woot-snackbar-button: #ffeb3b;
@@ -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
@@ -25,7 +25,8 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
render json: { error: 'Specify search string with parameter q' }, status: :unprocessable_entity if params[:q].blank? && return
contacts = resolved_contacts.where(
'name ILIKE :search OR email ILIKE :search OR phone_number ILIKE :search OR contacts.identifier LIKE :search',
'name ILIKE :search OR email ILIKE :search OR phone_number ILIKE :search OR contacts.identifier LIKE :search
OR contacts.additional_attributes->>\'company_name\' ILIKE :search',
search: "%#{params[:q].strip}%"
)
@contacts_count = contacts.count
@@ -1,4 +1,4 @@
class Api::V1::UploadController < Api::BaseController
class Api::V1::Accounts::UploadController < Api::V1::Accounts::BaseController
def create
file_blob = ActiveStorage::Blob.create_and_upload!(
key: nil,
@@ -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
+1 -1
View File
@@ -55,7 +55,7 @@ class DashboardController < ActionController::Base
VAPID_PUBLIC_KEY: VapidService.public_key,
ENABLE_ACCOUNT_SIGNUP: GlobalConfigService.load('ENABLE_ACCOUNT_SIGNUP', 'false'),
FB_APP_ID: GlobalConfigService.load('FB_APP_ID', ''),
FACEBOOK_API_VERSION: 'v17.0',
FACEBOOK_API_VERSION: 'v14.0',
IS_ENTERPRISE: ChatwootApp.enterprise?,
AZURE_APP_ID: ENV.fetch('AZURE_APP_ID', ''),
GIT_SHA: GIT_HASH
@@ -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
@@ -44,7 +44,7 @@ class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::B
end
def list_params
params.permit(:query, :locale, :sort)
params.permit(:query, :locale, :sort, :status)
end
def permitted_params
@@ -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
@@ -6,6 +6,7 @@ class SuperAdmin::InstanceStatusesController < SuperAdmin::ApplicationController
postgres_status
redis_metrics
chatwoot_edition
instance_meta
end
def chatwoot_edition
@@ -18,6 +19,10 @@ class SuperAdmin::InstanceStatusesController < SuperAdmin::ApplicationController
end
end
def instance_meta
@metrics['Database Migrations'] = ActiveRecord::Base.connection.migration_context.needs_migration? ? 'pending' : 'completed'
end
def chatwoot_version
@metrics['Chatwoot version'] = Chatwoot.config[:version]
end
+2 -2
View File
@@ -18,8 +18,8 @@ class AccountDashboard < Administrate::BaseDashboard
end
ATTRIBUTE_TYPES = {
id: Field::Number,
name: Field::String,
id: Field::Number.with_options(searchable: true),
name: Field::String.with_options(searchable: true),
created_at: Field::DateTime,
updated_at: Field::DateTime,
users: CountField,
+3 -3
View File
@@ -8,9 +8,9 @@ class AccountUserDashboard < Administrate::BaseDashboard
# which determines how the attribute is displayed
# on pages throughout the dashboard.
ATTRIBUTE_TYPES = {
account: Field::BelongsTo.with_options(searchable: true, searchable_field: 'name', order: 'id DESC'),
user: Field::BelongsTo.with_options(searchable: true, searchable_field: 'name', order: 'id DESC'),
inviter: Field::BelongsTo.with_options(class_name: 'User', searchable: true, searchable_field: 'name'),
account: Field::BelongsToSearch.with_options(class_name: 'Account', searchable: true, searchable_field: [:name, :id], order: 'id DESC'),
user: Field::BelongsToSearch.with_options(class_name: 'User', searchable: true, searchable_field: [:name, :email, :id], order: 'id DESC'),
inviter: Field::BelongsToSearch.with_options(class_name: 'User', searchable: true, searchable_field: [:name, :email, :id], order: 'id DESC'),
id: Field::Number,
role: Field::Select.with_options(collection: AccountUser.roles.keys),
created_at: Field::DateTime,
+3 -3
View File
@@ -9,7 +9,7 @@ class UserDashboard < Administrate::BaseDashboard
# on pages throughout the dashboard.
ATTRIBUTE_TYPES = {
account_users: Field::HasMany,
id: Field::Number,
id: Field::Number.with_options(searchable: true),
avatar_url: AvatarField,
avatar: Field::ActiveStorage.with_options(
destroy_url: proc do |_namespace, _resource, attachment|
@@ -28,9 +28,9 @@ class UserDashboard < Administrate::BaseDashboard
confirmed_at: Field::DateTime,
confirmation_sent_at: Field::DateTime,
unconfirmed_email: Field::String,
name: Field::String,
name: Field::String.with_options(searchable: true),
display_name: Field::String,
email: Field::String,
email: Field::String.with_options(searchable: true),
tokens: Field::String.with_options(searchable: false),
created_at: Field::DateTime,
updated_at: Field::DateTime,
+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
+5 -7
View File
@@ -26,14 +26,14 @@
<script>
import { mapGetters } from 'vuex';
import AddAccountModal from '../dashboard/components/layout/sidebarComponents/AddAccountModal';
import AddAccountModal from '../dashboard/components/layout/sidebarComponents/AddAccountModal.vue';
import LoadingState from './components/widgets/LoadingState.vue';
import NetworkNotification from './components/NetworkNotification';
import NetworkNotification from './components/NetworkNotification.vue';
import UpdateBanner from './components/app/UpdateBanner.vue';
import UpgradeBanner from './components/app/UpgradeBanner.vue';
import PaymentPendingBanner from './components/app/PaymentPendingBanner.vue';
import vueActionCable from './helper/actionCable';
import WootSnackbarBox from './components/SnackbarContainer';
import WootSnackbarBox from './components/SnackbarContainer.vue';
import rtlMixin from 'shared/mixins/rtlMixin';
import { setColorTheme } from './helper/themeHelper';
import {
@@ -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,
@@ -81,11 +81,6 @@ $conv-header-height: 2.5rem;
$inbox-thumb-size: 3rem;
// Spinner
$spinkit-spinner-color: $color-white !default;
$spinkit-spinner-margin: 0 0 0 1rem !default;
$spinkit-size: 1rem !default;
// Snackbar default
$woot-snackbar-bg: #323232;
$woot-snackbar-button: #ffeb3b;
@@ -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 {
@@ -9,7 +9,6 @@
@import 'shared/assets/stylesheets/border-radius';
@import 'variables';
@import '~spinkit/scss/spinners/7-three-bounce';
@import 'vue-multiselect/dist/vue-multiselect.min.css';
@import '~shared/assets/stylesheets/ionicons';
@@ -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;
@@ -31,7 +31,7 @@
</template>
<script>
import EmojiOrIcon from 'shared/components/EmojiOrIcon';
import EmojiOrIcon from 'shared/components/EmojiOrIcon.vue';
export default {
components: {
@@ -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">
@@ -193,17 +192,17 @@
<script>
import { mapGetters } from 'vuex';
import ConversationAdvancedFilter from './widgets/conversation/ConversationAdvancedFilter';
import ConversationBasicFilter from './widgets/conversation/ConversationBasicFilter';
import ChatTypeTabs from './widgets/ChatTypeTabs';
import ConversationCard from './widgets/conversation/ConversationCard';
import ConversationAdvancedFilter from './widgets/conversation/ConversationAdvancedFilter.vue';
import ConversationBasicFilter from './widgets/conversation/ConversationBasicFilter.vue';
import ChatTypeTabs from './widgets/ChatTypeTabs.vue';
import ConversationCard from './widgets/conversation/ConversationCard.vue';
import timeMixin from '../mixins/time';
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
import conversationMixin from '../mixins/conversations';
import wootConstants from 'dashboard/constants/globals';
import advancedFilterTypes from './widgets/conversation/advancedFilterItems';
import filterQueryGenerator from '../helper/filterQueryGenerator.js';
import AddCustomViews from 'dashboard/routes/dashboard/customviews/AddCustomViews';
import AddCustomViews from 'dashboard/routes/dashboard/customviews/AddCustomViews.vue';
import DeleteCustomViews from 'dashboard/routes/dashboard/customviews/DeleteCustomViews.vue';
import ConversationBulkActions from './widgets/conversation/conversationBulkActions/Index.vue';
import alertMixin from 'shared/mixins/alertMixin';
@@ -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">
@@ -10,7 +10,7 @@
</template>
<script>
import WootSnackbar from './Snackbar';
import WootSnackbar from './Snackbar.vue';
export default {
components: {
@@ -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)) {
@@ -13,7 +13,7 @@
</template>
<script>
import Spinner from 'shared/components/Spinner';
import Spinner from 'shared/components/Spinner.vue';
export default {
components: {
@@ -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>
@@ -90,7 +90,7 @@ import { getUnixTime } from 'date-fns';
import { mapGetters } from 'vuex';
import { mixin as clickaway } from 'vue-clickaway';
import alertMixin from 'shared/mixins/alertMixin';
import CustomSnoozeModal from 'dashboard/components/CustomSnoozeModal';
import CustomSnoozeModal from 'dashboard/components/CustomSnoozeModal.vue';
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
import {
hasPressedAltAndEKey,
@@ -49,11 +49,11 @@
import { mapGetters } from 'vuex';
import { mixin as clickaway } from 'vue-clickaway';
import alertMixin from 'shared/mixins/alertMixin';
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem';
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu';
import WootDropdownHeader from 'shared/components/ui/dropdown/DropdownHeader';
import WootDropdownDivider from 'shared/components/ui/dropdown/DropdownDivider';
import AvailabilityStatusBadge from '../widgets/conversation/AvailabilityStatusBadge';
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
import WootDropdownHeader from 'shared/components/ui/dropdown/DropdownHeader.vue';
import WootDropdownDivider from 'shared/components/ui/dropdown/DropdownDivider.vue';
import AvailabilityStatusBadge from '../widgets/conversation/AvailabilityStatusBadge.vue';
import wootConstants from 'dashboard/constants/globals';
const { AVAILABILITY_STATUS_KEYS } = wootConstants;
@@ -35,8 +35,8 @@ import adminMixin from '../../mixins/isAdmin';
import { getSidebarItems } from './config/default-sidebar';
import alertMixin from 'shared/mixins/alertMixin';
import PrimarySidebar from './sidebarComponents/Primary';
import SecondarySidebar from './sidebarComponents/Secondary';
import PrimarySidebar from './sidebarComponents/Primary.vue';
import SecondarySidebar from './sidebarComponents/Secondary.vue';
import {
hasPressedAltAndCKey,
hasPressedAltAndRKey,
@@ -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"
@@ -16,7 +16,7 @@
</template>
<script>
import { mapGetters } from 'vuex';
import Thumbnail from '../../widgets/Thumbnail';
import Thumbnail from '../../widgets/Thumbnail.vue';
export default {
components: {
@@ -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"
>
@@ -109,9 +109,9 @@
import { mixin as clickaway } from 'vue-clickaway';
import { mapGetters } from 'vuex';
import Auth from '../../../api/auth';
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem';
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu';
import AvailabilityStatus from 'dashboard/components/layout/AvailabilityStatus';
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
import AvailabilityStatus from 'dashboard/components/layout/AvailabilityStatus.vue';
export default {
components: {
@@ -39,11 +39,11 @@
</div>
</template>
<script>
import Logo from './Logo';
import PrimaryNavItem from './PrimaryNavItem';
import OptionsMenu from './OptionsMenu';
import AgentDetails from './AgentDetails';
import NotificationBell from './NotificationBell';
import Logo from './Logo.vue';
import PrimaryNavItem from './PrimaryNavItem.vue';
import OptionsMenu from './OptionsMenu.vue';
import AgentDetails from './AgentDetails.vue';
import NotificationBell from './NotificationBell.vue';
import wootConstants from 'dashboard/constants/globals';
import { frontendURL } from 'dashboard/helper/URLHelper';
import { ACCOUNT_EVENTS } from '../../../helper/AnalyticsHelper/events';
@@ -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,
}"
>
@@ -97,7 +98,7 @@ import {
getInboxWarningIconClass,
} from 'dashboard/helper/inbox';
import SecondaryChildNavItem from './SecondaryChildNavItem';
import SecondaryChildNavItem from './SecondaryChildNavItem.vue';
import {
isOnMentionsView,
isOnUnattendedView,
@@ -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}
@@ -28,8 +28,8 @@
</button>
</template>
<script>
import Spinner from 'shared/components/Spinner';
import EmojiOrIcon from 'shared/components/EmojiOrIcon';
import Spinner from 'shared/components/Spinner.vue';
import EmojiOrIcon from 'shared/components/EmojiOrIcon.vue';
export default {
name: 'WootButton',
@@ -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') }}
@@ -108,7 +108,7 @@
<script>
import AutomationActionTeamMessageInput from './AutomationActionTeamMessageInput.vue';
import AutomationActionFileInput from './AutomationFileInput.vue';
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor';
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
export default {
components: {
AutomationActionTeamMessageInput,
@@ -26,7 +26,7 @@
</template>
<script>
import Spinner from 'shared/components/Spinner';
import Spinner from 'shared/components/Spinner.vue';
import alertMixin from 'shared/mixins/alertMixin';
export default {
components: {
@@ -7,7 +7,7 @@
/>
</template>
<script>
import ChannelSelector from '../ChannelSelector';
import ChannelSelector from '../ChannelSelector.vue';
export default {
components: { ChannelSelector },
props: {
@@ -21,7 +21,7 @@
</template>
<script>
import LoadingState from 'dashboard/components/widgets/LoadingState';
import LoadingState from 'dashboard/components/widgets/LoadingState.vue';
export default {
components: {
LoadingState,
@@ -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,
@@ -30,9 +30,9 @@
</template>
<script>
import AddLabel from 'shared/components/ui/dropdown/AddLabel';
import AddLabel from 'shared/components/ui/dropdown/AddLabel.vue';
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
import LabelDropdown from 'shared/components/ui/label/LabelDropdown';
import LabelDropdown from 'shared/components/ui/label/LabelDropdown.vue';
import { mixin as clickaway } from 'vue-clickaway';
import adminMixin from 'dashboard/mixins/isAdmin';
import {
@@ -42,7 +42,7 @@
* Badge - Chat source indication { fb / telegram }
* Username - Username for avatar
*/
import Avatar from './Avatar';
import Avatar from './Avatar.vue';
import { removeEmoji } from 'shared/helpers/emoji';
export default {
@@ -17,7 +17,7 @@
</div>
</template>
<script>
import Thumbnail from './Thumbnail';
import Thumbnail from './Thumbnail.vue';
export default {
components: {
@@ -23,6 +23,7 @@
@change="onFileChange"
/>
<div ref="editor" />
<slot name="footer" />
</div>
</template>
@@ -41,9 +42,13 @@ import {
triggerCharacters,
} from '@chatwoot/prosemirror-schema/src/mentions/plugin';
import TagAgents from '../conversation/TagAgents';
import CannedResponse from '../conversation/CannedResponse';
import VariableList from '../conversation/VariableList';
import TagAgents from '../conversation/TagAgents.vue';
import CannedResponse from '../conversation/CannedResponse.vue';
import VariableList from '../conversation/VariableList.vue';
import {
appendSignature,
removeSignature,
} from 'dashboard/helper/editorHelper';
const TYPING_INDICATOR_IDLE_TIME = 4000;
const MAXIMUM_FILE_UPLOAD_SIZE = 4; // in MB
@@ -68,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
) => {
@@ -100,6 +107,10 @@ export default {
enableCannedResponses: { type: Boolean, default: true },
variables: { type: Object, default: () => ({}) },
enabledMenuOptions: { type: Array, default: () => [] },
signature: { type: String, default: '' },
// allowSignature is a kill switch, ensuring no signature methods
// are triggered except when this flag is true
allowSignature: { type: Boolean, default: false },
},
data() {
return {
@@ -220,6 +231,12 @@ export default {
}),
];
},
sendWithSignature() {
// this is considered the source of truth, we watch this property
// on change, we toggle the signature in the editor
const { send_with_signature: isEnabled } = this.uiSettings;
return isEnabled && this.allowSignature && !this.isPrivate;
},
},
watch: {
showUserMentions(updatedValue) {
@@ -244,7 +261,6 @@ export default {
isPrivate() {
this.reloadState(this.value);
},
updateSelectionWith(newValue, oldValue) {
if (!this.editorView) {
return null;
@@ -263,6 +279,12 @@ export default {
}
return null;
},
sendWithSignature(newValue) {
// see if the allowSignature flag is true
if (this.allowSignature) {
this.toggleSignatureInEditor(newValue);
}
},
},
created() {
this.state = createState(
@@ -276,7 +298,7 @@ export default {
mounted() {
this.createEditorView();
this.editorView.updateState(this.state);
this.focusEditorInputField();
this.focusEditor(this.value);
},
methods: {
reloadState(content = this.value) {
@@ -288,7 +310,75 @@ export default {
this.editorMenuOptions
);
this.editorView.updateState(this.state);
this.focusEditorInputField();
this.focusEditor(content);
},
focusEditor(content) {
if (this.isBodyEmpty(content) && this.sendWithSignature) {
// reload state can be called when switching between conversations, or when drafts is loaded
// these drafts can also have a signature, so we need to check if the body is empty
// and handle things accordingly
this.handleEmptyBodyWithSignature();
} else {
// this is in the else block, handleEmptyBodyWithSignature also has a call to the focus method
// the position is set to start, because the signature is added at the end of the body
this.focusEditorInputField('end');
}
},
toggleSignatureInEditor(signatureEnabled) {
// The toggleSignatureInEditor gets the new value from the
// watcher, this means that if the value is true, the signature
// is supposed to be added, else we remove it.
if (signatureEnabled) {
this.addSignature();
} else {
this.removeSignature();
}
},
addSignature() {
let content = this.value;
// see if the content is empty, if it is before appending the signature
// we need to add a paragraph node and move the cursor at the start of the editor
const contentWasEmpty = this.isBodyEmpty(content);
content = appendSignature(content, this.signature);
// need to reload first, ensuring that the editorView is updated
this.reloadState(content);
if (contentWasEmpty) {
this.handleEmptyBodyWithSignature();
}
},
removeSignature() {
if (!this.signature) return;
let content = this.value;
content = removeSignature(content, this.signature);
// reload the state, ensuring that the editorView is updated
this.reloadState(content);
},
isBodyEmpty(content) {
// if content is undefined, we assume that the body is empty
if (!content) return true;
// if the signature is present, we need to remove it before checking
// note that we don't update the editorView, so this is safe
const bodyWithoutSignature = this.signature
? removeSignature(content, this.signature)
: content;
// trimming should remove all the whitespaces, so we can check the length
return bodyWithoutSignature.trim().length === 0;
},
handleEmptyBodyWithSignature() {
const { schema, tr } = this.state;
// create a paragraph node and
// start a transaction to append it at the end
const paragraph = schema.nodes.paragraph.create();
const paragraphTransaction = tr.insert(0, paragraph);
this.editorView.dispatch(paragraphTransaction);
// Set the focus at the start of the input field
this.focusEditorInputField('start');
},
createEditorView() {
this.editorView = new EditorView(this.$refs.editor, {
@@ -333,9 +423,11 @@ export default {
this.focusEditorInputField();
}
},
focusEditorInputField() {
focusEditorInputField(pos = 'end') {
const { tr } = this.editorView.state;
const selection = Selection.atEnd(tr.doc);
const selection =
pos === 'end' ? Selection.atEnd(tr.doc) : Selection.atStart(tr.doc);
this.editorView.dispatch(tr.setSelection(selection));
this.editorView.focus();
@@ -581,6 +673,10 @@ export default {
.ProseMirror-prompt {
@apply z-50 bg-slate-25 dark:bg-slate-700 rounded-md border border-solid border-slate-75 dark:border-slate-800;
h5 {
@apply dark:text-slate-25 text-slate-800;
}
}
.is-private {
@@ -616,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
) => {
@@ -134,7 +134,7 @@ import {
ALLOWED_FILE_TYPES,
ALLOWED_FILE_TYPES_FOR_TWILIO_WHATSAPP,
} from 'shared/constants/messages';
import VideoCallButton from '../VideoCallButton';
import VideoCallButton from '../VideoCallButton.vue';
import AIAssistanceButton from '../AIAssistanceButton.vue';
import { REPLY_EDITOR_MODES } from './constants';
import { mapGetters } from 'vuex';
@@ -288,7 +288,7 @@ export default {
}
},
showMessageSignatureButton() {
return !this.isOnPrivateNote && this.isAnEmailChannel;
return !this.isOnPrivateNote;
},
sendWithSignature() {
const { send_with_signature: isEnabled } = this.uiSettings;
@@ -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"
@@ -46,7 +46,7 @@
import wootConstants from 'dashboard/constants/globals';
import { mapGetters } from 'vuex';
import { mixin as clickaway } from 'vue-clickaway';
import FilterItem from './FilterItem';
import FilterItem from './FilterItem.vue';
export default {
components: {
@@ -58,11 +58,11 @@
</template>
<script>
import { mapGetters } from 'vuex';
import ContactPanel from 'dashboard/routes/dashboard/conversation/ContactPanel';
import ConversationHeader from './ConversationHeader';
import ContactPanel from 'dashboard/routes/dashboard/conversation/ContactPanel.vue';
import ConversationHeader from './ConversationHeader.vue';
import DashboardAppFrame from '../DashboardApp/Frame.vue';
import EmptyState from './EmptyState/EmptyState';
import MessagesView from './MessagesView';
import EmptyState from './EmptyState/EmptyState.vue';
import MessagesView from './MessagesView.vue';
export default {
components: {
@@ -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">
@@ -147,16 +147,16 @@
import { mapGetters } from 'vuex';
import { MESSAGE_TYPE } from 'widget/helpers/constants';
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
import Thumbnail from '../Thumbnail';
import Thumbnail from '../Thumbnail.vue';
import conversationMixin from '../../../mixins/conversations';
import timeMixin from '../../../mixins/time';
import router from '../../../routes';
import { frontendURL, conversationUrl } from '../../../helper/URLHelper';
import InboxName from '../InboxName';
import InboxName from '../InboxName.vue';
import inboxMixin from 'shared/mixins/inboxMixin';
import ConversationContextMenu from './contextMenu/Index.vue';
import alertMixin from 'shared/mixins/alertMixin';
import TimeAgo from 'dashboard/components/ui/TimeAgo';
import TimeAgo from 'dashboard/components/ui/TimeAgo.vue';
import CardLabels from './conversationCardComponents/CardLabels.vue';
import PriorityMark from './PriorityMark.vue';
const ATTACHMENT_ICONS = {
@@ -73,12 +73,12 @@
import { hasPressedAltAndOKey } from 'shared/helpers/KeyboardHelpers';
import { mapGetters } from 'vuex';
import agentMixin from '../../../mixins/agentMixin.js';
import BackButton from '../BackButton';
import BackButton from '../BackButton.vue';
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
import inboxMixin from 'shared/mixins/inboxMixin';
import InboxName from '../InboxName';
import MoreActions from './MoreActions';
import Thumbnail from '../Thumbnail';
import InboxName from '../InboxName.vue';
import MoreActions from './MoreActions.vue';
import Thumbnail from '../Thumbnail.vue';
import wootConstants from 'dashboard/constants/globals';
import { conversationListPageURL } from 'dashboard/helper/URLHelper';
import { conversationReopenTime } from 'dashboard/helper/snoozeHelpers';
@@ -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">
@@ -37,8 +37,8 @@
import { mapGetters } from 'vuex';
import adminMixin from 'dashboard/mixins/isAdmin';
import accountMixin from 'dashboard/mixins/account';
import OnboardingView from '../OnboardingView';
import EmptyStateMessage from './EmptyStateMessage';
import OnboardingView from '../OnboardingView.vue';
import EmptyStateMessage from './EmptyStateMessage.vue';
export default {
components: {
@@ -27,7 +27,7 @@
</template>
<script>
import Hotkey from 'dashboard/components/base/Hotkey';
import Hotkey from 'dashboard/components/base/Hotkey.vue';
export default {
components: {

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