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 JoseandGitHub e9950afb1a refactor: Optimise article query in chat widget (#7919)
- optimise article query in chatwidget
2023-09-14 17:19:42 -07:00
Pranav Raj SandGitHub f21298aeb9 chore: Increase the click area for the article list item (#7918) 2023-09-14 20:44:08 +05:30
Vishnu NarayananandGitHub 7b6a56714f fix: double rendor error on message create api for suspended accounts (#7917) 2023-09-14 20:06:48 +05:30
94a20af9db fix: Render articles in widget if it is available (#7654)
Co-authored-by: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
Co-authored-by: Sojan <sojan@pepalo.com>
2023-09-14 19:56:17 +05:30
Sojan JoseandGitHub 616371adbb chore: Improve the behavior of lock to single conversation (#7899) 2023-09-14 12:32:57 +05:30
Muhsin KelothandGitHub 2d4ef0c328 feat: Add None option in automation assign agent actions (#7900) 2023-09-14 09:01:58 +05:30
5e6e234afe feat (perf): Remove conversation count in contacts list view (#7915)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2023-09-14 08:49:37 +05:30
Muhsin KelothandGitHub 18e9e771b5 fix: Check body exists before deleting null bytes in Twilio (#7910) 2023-09-13 13:40:26 +05:30
0245e01763 fix: Enable serbian translation (#7905)
Enable language: Serbian.

Co-authored-by: BikashSah999 <51731962+BikashSah999@users.noreply.github.com>
2023-09-13 00:36:34 -07:00
fcd40e9636 chore: Update translations (#7887)
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2023-09-12 18:09:47 -07:00
Jordan BroughandGitHub 4f5c5e9f85 fix: Erase null bytes from incoming Twilio messages (#7901)
We've had some messages come in from a few different phone numbers that had null bytes in them. I don't know how this happens. They don't seem to be malicious.

They currently cause the Postgres gem to raise an error when Chatwoot attempts to save the message body to the database:

ArgumentError (string contains null byte)

Related Rails GitHub issue: rails/rails#26891
2023-09-12 18:07:18 -07:00
cb07ac16d1 chore: [Snyk] Fix for 1 vulnerabilities (#7906)
Co-authored-by: snyk-bot <snyk-bot@snyk.io>
2023-09-12 18:03:58 -07:00
Muhsin KelothandGitHub 4fa299ff15 chore: Disable archived channels from the slack integration (#7904) 2023-09-12 22:21:50 +05:30
Pranav Raj SandGitHub 71e9566854 Revert "feat: Ability to send attachment in new conversation (#7698)" (#7903) 2023-09-12 20:57:49 +05:30
272f920811 chore: [Snyk] Security upgrade rails from 7.0.7.2 to 7.0.8 (#7888)
Co-authored-by: snyk-bot <snyk-bot@snyk.io>
2023-09-11 19:12:38 -07:00
e39d19b1e8 feat: Adds image support for message signature (#7827)
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
2023-09-11 10:23:45 +05:30
Nithin David ThomasandGitHub 6c39aed882 fix: Refactor components in widget to show articles (#7874) 2023-09-11 09:31:50 +05:30
Nithin David ThomasandGitHub 8d43101892 chore: Improvements to dark mode usage in widget (#7873) 2023-09-11 09:31:11 +05:30
Pranav Raj SandGitHub f05535c5ad Revert "chore: Improve the behaviour of lock to single conversation (… (#7886) 2023-09-09 09:59:21 +05:30
Pranav Raj SandGitHub 4503ba018d chore: Increase the limit of greeting message (#7883) 2023-09-08 15:33:09 +05:30
8bc2bc6d40 feat: Ability to send attachment in new conversation (#7698)
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2023-09-08 12:12:24 +05:30
Sojan JoseandGitHub bfd192ebb2 chore: Improve the behaviour of lock to single conversation (#7863) 2023-09-08 11:16:24 +05:30
Sojan JoseandGitHub 6e13d22ff7 chore: Updated prompt for Article Bot (#7872) 2023-09-08 11:11:07 +05:30
Muhsin KelothandGitHub 3e8b6e1aee fix: Disable AI assist if the AI integration not enabled (#7876) 2023-09-07 19:17:10 +05:30
Sivin VargheseandGitHub 3038aaef67 fix: Editor menu option is not showing (#7879) 2023-09-07 17:15:19 +05:30
Muhsin KelothandGitHub 57a68666cb fix: Integration page responsiveness issues (#7875) 2023-09-07 14:16:44 +05:30
4acb824857 chore: [Snyk] Fix for 1 vulnerabilities (#7870)
Co-authored-by: snyk-bot <snyk-bot@snyk.io>
2023-09-06 18:36:32 -07:00
5c74674c2b feat: Update rack attack IP limit (#7866)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2023-09-06 18:56:42 +05:30
Nithin David ThomasandGitHub 52e49d5f9d feat: Creates component to show article in an iFrame (#7857) 2023-09-06 18:55:39 +05:30
Shivam MishraandGitHub e529e1206e fix: Update inline image processing logic to fix missing images when multiple inline images present (#7861) 2023-09-06 14:35:19 +05:30
Nithin David ThomasandGitHub a8009c7b39 fix: Allow Iframes requests on portal controller (#7858) 2023-09-06 14:25:03 +05:30
99bb074656 chore: Security upgrade web-console from 4.2.0 to 4.2.1 (#7864)
Co-authored-by: snyk-bot <snyk-bot@snyk.io>
2023-09-06 11:31:05 +05:30
Nithin David ThomasandGitHub 9322112481 chore: Improvements to utils to load article in widget (#7859) 2023-09-05 21:37:51 +05:30
Muhsin KelothandGitHub ce4cfee8bd fix: Attachment sent from slack doesn't reach to chatwoot (#7852) 2023-09-05 17:23:58 +05:30
Sivin VargheseandGitHub f31fc2b375 fix: Avoid XSS in custom attributes (#7800) 2023-09-05 09:49:54 +05:30
e5f7807833 fix: Redirect after contact delete [CW-2397] (#7740)
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2023-09-04 18:48:30 +05:30
Onur YAŞARandGitHub 2483d99d3c fix: Update Portal Turkish Translations (#7833) 2023-09-04 18:37:34 +05:30
Shivam MishraandGitHub 336584c95a feat: mutex for InstagramEventsJob [CW-2447] (#7828) 2023-09-04 15:02:13 +05:30
Shivam MishraandGitHub 9ebabb9832 feat: common attachment endpoint follow-up changes (#7826) 2023-09-01 15:18:48 +07:00
Shivam MishraandGitHub 2acf09b3eb refactor: use conversation_id instead of sender_id (#7831) 2023-08-31 16:10:37 +07:00
Pranav Raj SandGitHub d18d3e9e2e fix: Re-arrange plugins to fix canned-responses/mentions not working (#7830) 2023-08-31 15:42:40 +07:00
deec1d213b feat: add a common upload endpoint (#7806)
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2023-08-31 10:36:02 +07:00
Shivam MishraandGitHub 25c3fb3c36 feat: Track the usage of query operator (#7821) 2023-08-30 08:28:05 +05:30
e2a6dc3e04 chore: Upgrade Node.js to v20 (#7759)
Co-authored-by: Vishnu Narayanan <vishnu@chatwoot.com>
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2023-08-28 19:20:35 +05:30
57feedbf25 fix: Fixes extra backslash while breaking paragraphs [CW-1505] (#7457)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2023-08-28 15:46:10 +05:30
Pranav Raj SandGitHub 71310c21ad chore: Update translations (#7807) 2023-08-28 09:02:22 +05:30
623cd72897 fix: Update the order of display of the CSAT ratings (#7805)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2023-08-27 07:39:06 +05:30
04aa13e8aa feat: Disable options from the message signature editor (#7777)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2023-08-26 08:51:39 +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
816 changed files with 16169 additions and 5335 deletions
+7 -8
View File
@@ -15,9 +15,9 @@ defaults: &defaults
- image: cimg/postgres:15.3 - image: cimg/postgres:15.3
- image: cimg/redis:6.2.6 - image: cimg/redis:6.2.6
environment: environment:
- RAILS_LOG_TO_STDOUT: false - RAILS_LOG_TO_STDOUT: false
- COVERAGE: true - COVERAGE: true
- LOG_LEVEL: warn - LOG_LEVEL: warn
parallelism: 4 parallelism: 4
resource_class: large resource_class: large
@@ -46,7 +46,7 @@ jobs:
export NVM_DIR="$HOME/.nvm" export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"
nvm install v16 nvm install v20
echo 'export NVM_DIR="$HOME/.nvm"' >> $BASH_ENV echo 'export NVM_DIR="$HOME/.nvm"' >> $BASH_ENV
echo '[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"' >> $BASH_ENV echo '[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"' >> $BASH_ENV
@@ -64,7 +64,6 @@ jobs:
- ~/.bundle - ~/.bundle
key: chatwoot-bundle-{{ .Environment.CACHE_VERSION }}-v20220524-{{ checksum "Gemfile.lock" }} key: chatwoot-bundle-{{ .Environment.CACHE_VERSION }}-v20220524-{{ checksum "Gemfile.lock" }}
# Only necessary if app uses webpacker or yarn in some other way # Only necessary if app uses webpacker or yarn in some other way
- restore_cache: - restore_cache:
keys: keys:
@@ -82,7 +81,7 @@ jobs:
- ~/.cache/yarn - ~/.cache/yarn
- run: - run:
name: Download cc-test-reporter name: Download cc-test-reporter
command: | command: |
mkdir -p ~/tmp mkdir -p ~/tmp
curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ~/tmp/cc-test-reporter curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ~/tmp/cc-test-reporter
@@ -182,7 +181,7 @@ jobs:
- attach_workspace: - attach_workspace:
at: ~/build at: ~/build
- run: - run:
name: Download cc-test-reporter name: Download cc-test-reporter
command: | command: |
mkdir -p ~/tmp mkdir -p ~/tmp
curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ~/tmp/cc-test-reporter curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ~/tmp/cc-test-reporter
@@ -204,4 +203,4 @@ workflows:
- build - build
- upload-coverage: - upload-coverage:
requires: requires:
- build - build
+2
View File
@@ -199,6 +199,8 @@ ANDROID_SHA256_CERT_FINGERPRINT=AC:73:8E:DE:EB:56:EA:CC:10:87:02:A7:65:37:7B:38:
## Rack Attack configuration ## Rack Attack configuration
## To prevent and throttle abusive requests ## To prevent and throttle abusive requests
# ENABLE_RACK_ATTACK=true # ENABLE_RACK_ATTACK=true
# RACK_ATTACK_LIMIT=300
# ENABLE_RACK_ATTACK_WIDGET_API=true
## Running chatwoot as an API only server ## Running chatwoot as an API only server
## setting this value to true will disable the frontend dashboard endpoints ## setting this value to true will disable the frontend dashboard endpoints
+7 -3
View File
@@ -6,7 +6,7 @@ module.exports = {
'plugin:storybook/recommended', 'plugin:storybook/recommended',
], ],
parserOptions: { parserOptions: {
parser: 'babel-eslint', parser: '@babel/eslint-parser',
ecmaVersion: 2020, ecmaVersion: 2020,
sourceType: 'module', sourceType: 'module',
}, },
@@ -24,13 +24,16 @@ module.exports = {
'jsx-a11y/label-has-for': 'off', 'jsx-a11y/label-has-for': 'off',
'jsx-a11y/anchor-is-valid': 'off', 'jsx-a11y/anchor-is-valid': 'off',
'import/no-unresolved': 'off', 'import/no-unresolved': 'off',
'vue/html-indent': 'off',
'vue/multi-word-component-names': 'off',
'vue/max-attributes-per-line': [ 'vue/max-attributes-per-line': [
'error', 'error',
{ {
singleline: 20, singleline: {
max: 20,
},
multiline: { multiline: {
max: 1, max: 1,
allowFirstLine: false,
}, },
}, },
], ],
@@ -47,6 +50,7 @@ module.exports = {
}, },
], ],
'vue/no-v-html': 'off', 'vue/no-v-html': 'off',
'vue/component-definition-name-casing': 'off',
'vue/singleline-html-element-content-newline': 'off', 'vue/singleline-html-element-content-newline': 'off',
'import/extensions': ['off'], 'import/extensions': ['off'],
'no-console': 'error', 'no-console': 'error',
+4 -1
View File
@@ -52,7 +52,8 @@ jobs:
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:
node-version: 16 node-version: 20
cache: yarn
- name: yarn - name: yarn
run: yarn install run: yarn install
@@ -75,6 +76,8 @@ jobs:
- name: Run backend tests - name: Run backend tests
run: | run: |
bundle exec rspec --profile=10 --format documentation bundle exec rspec --profile=10 --format documentation
env:
NODE_OPTIONS: --openssl-legacy-provider
- name: Upload rails log folder - name: Upload rails log folder
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
+8 -2
View File
@@ -51,7 +51,8 @@ jobs:
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:
node-version: 16 node-version: 20
cache: yarn
- name: yarn - name: yarn
run: yarn install run: yarn install
@@ -68,7 +69,12 @@ jobs:
# Run Response Bot specs # Run Response Bot specs
- name: Run backend tests - name: Run backend tests
run: | 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 - name: Upload rails log folder
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
+4 -1
View File
@@ -21,7 +21,8 @@ jobs:
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:
node-version: 16 node-version: 20
cache: 'yarn'
- name: yarn - name: yarn
run: yarn install run: yarn install
@@ -33,6 +34,8 @@ jobs:
- name: Run asset compile - name: Run asset compile
run: bundle exec rake assets:precompile run: bundle exec rake assets:precompile
env:
NODE_OPTIONS: --openssl-legacy-provider
- name: Size Check - name: Size Check
run: yarn run size run: yarn run size
+1 -1
View File
@@ -1 +1 @@
16.20.1 20.5.1
+4 -7
View File
@@ -8,15 +8,11 @@ Layout/LineLength:
Max: 150 Max: 150
Metrics/ClassLength: Metrics/ClassLength:
Max: 125 Max: 175
Exclude: Exclude:
- 'app/models/conversation.rb'
- 'app/models/contact.rb'
- 'app/mailers/conversation_reply_mailer.rb'
- 'app/models/message.rb' - 'app/models/message.rb'
- 'app/builders/messages/facebook/message_builder.rb' - 'app/models/conversation.rb'
- 'app/controllers/api/v1/accounts/contacts_controller.rb'
- 'app/listeners/action_cable_listener.rb'
RSpec/ExampleLength: RSpec/ExampleLength:
Max: 25 Max: 25
Style/Documentation: Style/Documentation:
@@ -62,6 +58,7 @@ Metrics/BlockLength:
Metrics/ModuleLength: Metrics/ModuleLength:
Exclude: Exclude:
- lib/seeders/message_seeder.rb - lib/seeders/message_seeder.rb
- spec/support/slack_stubs.rb
Rails/ApplicationController: Rails/ApplicationController:
Exclude: Exclude:
- 'app/controllers/api/v1/widget/messages_controller.rb' - 'app/controllers/api/v1/widget/messages_controller.rb'
+8 -7
View File
@@ -4,7 +4,7 @@ ruby '3.2.2'
##-- base gems for rails --## ##-- base gems for rails --##
gem 'rack-cors', require: 'rack/cors' gem 'rack-cors', require: 'rack/cors'
gem 'rails', '~> 7.0.7.2' gem 'rails', '~> 7.0.8.0'
# Reduces boot times through caching; required in config/boot.rb # Reduces boot times through caching; required in config/boot.rb
gem 'bootsnap', require: false gem 'bootsnap', require: false
@@ -76,6 +76,7 @@ gem 'pundit'
# super admin # super admin
gem 'administrate', '>= 0.19.0' gem 'administrate', '>= 0.19.0'
gem 'administrate-field-active_storage' gem 'administrate-field-active_storage'
gem 'administrate-field-belongs_to_search'
##--- gems for pubsub service ---## ##--- gems for pubsub service ---##
# https://karolgalanciak.com/blog/2019/11/30/from-activerecord-callbacks-to-publish-slash-subscribe-pattern-and-event-driven-design/ # https://karolgalanciak.com/blog/2019/11/30/from-activerecord-callbacks-to-publish-slash-subscribe-pattern-and-event-driven-design/
@@ -108,14 +109,14 @@ gem 'elastic-apm', require: false
gem 'newrelic_rpm', require: false gem 'newrelic_rpm', require: false
gem 'newrelic-sidekiq-metrics', require: false gem 'newrelic-sidekiq-metrics', require: false
gem 'scout_apm', require: false gem 'scout_apm', require: false
gem 'sentry-rails', '>= 5.10.0', require: false gem 'sentry-rails', '>= 5.11.0', require: false
gem 'sentry-ruby', require: false gem 'sentry-ruby', require: false
gem 'sentry-sidekiq', '>= 5.10.0', require: false gem 'sentry-sidekiq', '>= 5.11.0', require: false
##-- background job processing --## ##-- background job processing --##
gem 'sidekiq' gem 'sidekiq', '>= 7.1.3'
# We want cron jobs # We want cron jobs
gem 'sidekiq-cron' gem 'sidekiq-cron', '>= 1.10.1'
##-- Push notification service --## ##-- Push notification service --##
gem 'fcm' gem 'fcm'
@@ -158,7 +159,7 @@ gem 'lograge', '~> 0.13.0', require: false
# worked with microsoft refresh token # worked with microsoft refresh token
gem 'omniauth-oauth2' gem 'omniauth-oauth2'
gem 'audited', '~> 5.3' gem 'audited', '~> 5.4', '>= 5.4.0'
# need for google auth # need for google auth
gem 'omniauth' gem 'omniauth'
@@ -188,7 +189,7 @@ group :development do
gem 'bullet' gem 'bullet'
gem 'letter_opener' gem 'letter_opener'
gem 'scss_lint', require: false gem 'scss_lint', require: false
gem 'web-console' gem 'web-console', '>= 4.2.1'
# used in swagger build # used in swagger build
gem 'json_refs' gem 'json_refs'
+89 -83
View File
@@ -33,70 +33,70 @@ GIT
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
actioncable (7.0.7.2) actioncable (7.0.8)
actionpack (= 7.0.7.2) actionpack (= 7.0.8)
activesupport (= 7.0.7.2) activesupport (= 7.0.8)
nio4r (~> 2.0) nio4r (~> 2.0)
websocket-driver (>= 0.6.1) websocket-driver (>= 0.6.1)
actionmailbox (7.0.7.2) actionmailbox (7.0.8)
actionpack (= 7.0.7.2) actionpack (= 7.0.8)
activejob (= 7.0.7.2) activejob (= 7.0.8)
activerecord (= 7.0.7.2) activerecord (= 7.0.8)
activestorage (= 7.0.7.2) activestorage (= 7.0.8)
activesupport (= 7.0.7.2) activesupport (= 7.0.8)
mail (>= 2.7.1) mail (>= 2.7.1)
net-imap net-imap
net-pop net-pop
net-smtp net-smtp
actionmailer (7.0.7.2) actionmailer (7.0.8)
actionpack (= 7.0.7.2) actionpack (= 7.0.8)
actionview (= 7.0.7.2) actionview (= 7.0.8)
activejob (= 7.0.7.2) activejob (= 7.0.8)
activesupport (= 7.0.7.2) activesupport (= 7.0.8)
mail (~> 2.5, >= 2.5.4) mail (~> 2.5, >= 2.5.4)
net-imap net-imap
net-pop net-pop
net-smtp net-smtp
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
actionpack (7.0.7.2) actionpack (7.0.8)
actionview (= 7.0.7.2) actionview (= 7.0.8)
activesupport (= 7.0.7.2) activesupport (= 7.0.8)
rack (~> 2.0, >= 2.2.4) rack (~> 2.0, >= 2.2.4)
rack-test (>= 0.6.3) rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0)
actiontext (7.0.7.2) actiontext (7.0.8)
actionpack (= 7.0.7.2) actionpack (= 7.0.8)
activerecord (= 7.0.7.2) activerecord (= 7.0.8)
activestorage (= 7.0.7.2) activestorage (= 7.0.8)
activesupport (= 7.0.7.2) activesupport (= 7.0.8)
globalid (>= 0.6.0) globalid (>= 0.6.0)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
actionview (7.0.7.2) actionview (7.0.8)
activesupport (= 7.0.7.2) activesupport (= 7.0.8)
builder (~> 3.1) builder (~> 3.1)
erubi (~> 1.4) erubi (~> 1.4)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.1, >= 1.2.0) rails-html-sanitizer (~> 1.1, >= 1.2.0)
active_record_query_trace (1.8) active_record_query_trace (1.8)
activejob (7.0.7.2) activejob (7.0.8)
activesupport (= 7.0.7.2) activesupport (= 7.0.8)
globalid (>= 0.3.6) globalid (>= 0.3.6)
activemodel (7.0.7.2) activemodel (7.0.8)
activesupport (= 7.0.7.2) activesupport (= 7.0.8)
activerecord (7.0.7.2) activerecord (7.0.8)
activemodel (= 7.0.7.2) activemodel (= 7.0.8)
activesupport (= 7.0.7.2) activesupport (= 7.0.8)
activerecord-import (1.4.1) activerecord-import (1.4.1)
activerecord (>= 4.2) activerecord (>= 4.2)
activestorage (7.0.7.2) activestorage (7.0.8)
actionpack (= 7.0.7.2) actionpack (= 7.0.8)
activejob (= 7.0.7.2) activejob (= 7.0.8)
activerecord (= 7.0.7.2) activerecord (= 7.0.8)
activesupport (= 7.0.7.2) activesupport (= 7.0.8)
marcel (~> 1.0) marcel (~> 1.0)
mini_mime (>= 1.1.0) mini_mime (>= 1.1.0)
activesupport (7.0.7.2) activesupport (7.0.8)
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2) i18n (>= 1.6, < 2)
minitest (>= 5.1) minitest (>= 5.1)
@@ -116,13 +116,18 @@ GEM
administrate-field-active_storage (0.4.2) administrate-field-active_storage (0.4.2)
administrate (>= 0.2.2) administrate (>= 0.2.2)
rails (>= 7.0) 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) annotate (3.2.0)
activerecord (>= 3.2, < 8.0) activerecord (>= 3.2, < 8.0)
rake (>= 10.4, < 14.0) rake (>= 10.4, < 14.0)
ast (2.4.2) ast (2.4.2)
attr_extras (7.1.0) attr_extras (7.1.0)
audited (5.3.3) audited (5.4.0)
activerecord (>= 5.0, < 7.1) activerecord (>= 5.0, < 7.2)
request_store (~> 1.2) request_store (~> 1.2)
aws-eventstream (1.2.0) aws-eventstream (1.2.0)
aws-partitions (1.760.0) aws-partitions (1.760.0)
@@ -269,8 +274,8 @@ GEM
grpc (~> 1.36) grpc (~> 1.36)
geocoder (1.8.1) geocoder (1.8.1)
gli (2.21.0) gli (2.21.0)
globalid (1.1.0) globalid (1.2.1)
activesupport (>= 5.0) activesupport (>= 6.1)
gmail_xoauth (0.4.2) gmail_xoauth (0.4.2)
oauth (>= 0.3.6) oauth (>= 0.3.6)
google-apis-core (0.11.0) google-apis-core (0.11.0)
@@ -450,9 +455,9 @@ GEM
mime-types-data (~> 3.2015) mime-types-data (~> 3.2015)
mime-types-data (3.2023.0218.1) mime-types-data (3.2023.0218.1)
mini_magick (4.12.0) mini_magick (4.12.0)
mini_mime (1.1.2) mini_mime (1.1.5)
mini_portile2 (2.8.4) mini_portile2 (2.8.4)
minitest (5.19.0) minitest (5.20.0)
mock_redis (0.36.0) mock_redis (0.36.0)
ruby2_keywords ruby2_keywords
msgpack (1.7.0) msgpack (1.7.0)
@@ -463,7 +468,7 @@ GEM
activerecord (>= 5.2) activerecord (>= 5.2)
net-http-persistent (4.0.2) net-http-persistent (4.0.2)
connection_pool (~> 2.2) connection_pool (~> 2.2)
net-imap (0.3.6) net-imap (0.3.7)
date date
net-protocol net-protocol
net-pop (0.1.2) net-pop (0.1.2)
@@ -478,14 +483,14 @@ GEM
sidekiq sidekiq
newrelic_rpm (8.16.0) newrelic_rpm (8.16.0)
nio4r (2.5.9) nio4r (2.5.9)
nokogiri (1.15.3) nokogiri (1.15.4)
mini_portile2 (~> 2.8.2) mini_portile2 (~> 2.8.2)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.15.3-arm64-darwin) nokogiri (1.15.4-arm64-darwin)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.15.3-x86_64-darwin) nokogiri (1.15.4-x86_64-darwin)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.15.3-x86_64-linux) nokogiri (1.15.4-x86_64-linux)
racc (~> 1.4) racc (~> 1.4)
numo-narray (0.9.2.1) numo-narray (0.9.2.1)
oauth (1.1.0) oauth (1.1.0)
@@ -563,30 +568,30 @@ GEM
rack-test (2.1.0) rack-test (2.1.0)
rack (>= 1.3) rack (>= 1.3)
rack-timeout (0.6.3) rack-timeout (0.6.3)
rails (7.0.7.2) rails (7.0.8)
actioncable (= 7.0.7.2) actioncable (= 7.0.8)
actionmailbox (= 7.0.7.2) actionmailbox (= 7.0.8)
actionmailer (= 7.0.7.2) actionmailer (= 7.0.8)
actionpack (= 7.0.7.2) actionpack (= 7.0.8)
actiontext (= 7.0.7.2) actiontext (= 7.0.8)
actionview (= 7.0.7.2) actionview (= 7.0.8)
activejob (= 7.0.7.2) activejob (= 7.0.8)
activemodel (= 7.0.7.2) activemodel (= 7.0.8)
activerecord (= 7.0.7.2) activerecord (= 7.0.8)
activestorage (= 7.0.7.2) activestorage (= 7.0.8)
activesupport (= 7.0.7.2) activesupport (= 7.0.8)
bundler (>= 1.15.0) bundler (>= 1.15.0)
railties (= 7.0.7.2) railties (= 7.0.8)
rails-dom-testing (2.1.1) rails-dom-testing (2.2.0)
activesupport (>= 5.0.0) activesupport (>= 5.0.0)
minitest minitest
nokogiri (>= 1.6) nokogiri (>= 1.6)
rails-html-sanitizer (1.6.0) rails-html-sanitizer (1.6.0)
loofah (~> 2.21) loofah (~> 2.21)
nokogiri (~> 1.14) nokogiri (~> 1.14)
railties (7.0.7.2) railties (7.0.8)
actionpack (= 7.0.7.2) actionpack (= 7.0.8)
activesupport (= 7.0.7.2) activesupport (= 7.0.8)
method_source method_source
rake (>= 12.2) rake (>= 12.2)
thor (~> 1.0) thor (~> 1.0)
@@ -598,7 +603,7 @@ GEM
ffi (~> 1.0) ffi (~> 1.0)
redis (5.0.6) redis (5.0.6)
redis-client (>= 0.9.0) redis-client (>= 0.9.0)
redis-client (0.14.1) redis-client (0.17.0)
connection_pool connection_pool
redis-namespace (1.10.0) redis-namespace (1.10.0)
redis (>= 4) redis (>= 4)
@@ -697,23 +702,23 @@ GEM
activesupport (>= 4) activesupport (>= 4)
selectize-rails (0.12.6) selectize-rails (0.12.6)
semantic_range (3.0.0) semantic_range (3.0.0)
sentry-rails (5.10.0) sentry-rails (5.11.0)
railties (>= 5.0) railties (>= 5.0)
sentry-ruby (~> 5.10.0) sentry-ruby (~> 5.11.0)
sentry-ruby (5.10.0) sentry-ruby (5.11.0)
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
sentry-sidekiq (5.10.0) sentry-sidekiq (5.11.0)
sentry-ruby (~> 5.10.0) sentry-ruby (~> 5.11.0)
sidekiq (>= 3.0) sidekiq (>= 3.0)
sexp_processor (4.17.0) sexp_processor (4.17.0)
shoulda-matchers (5.3.0) shoulda-matchers (5.3.0)
activesupport (>= 5.2.0) activesupport (>= 5.2.0)
sidekiq (7.1.2) sidekiq (7.1.3)
concurrent-ruby (< 2) concurrent-ruby (< 2)
connection_pool (>= 2.3.0) connection_pool (>= 2.3.0)
rack (>= 2.2.4) rack (>= 2.2.4)
redis-client (>= 0.14.0) redis-client (>= 0.14.0)
sidekiq-cron (1.10.0) sidekiq-cron (1.10.1)
fugit (~> 1.8) fugit (~> 1.8)
globalid (>= 1.0.1) globalid (>= 1.0.1)
sidekiq (>= 6) sidekiq (>= 6)
@@ -786,7 +791,7 @@ GEM
version_gem (1.1.2) version_gem (1.1.2)
warden (1.2.9) warden (1.2.9)
rack (>= 2.0.9) rack (>= 2.0.9)
web-console (4.2.0) web-console (4.2.1)
actionview (>= 6.0.0) actionview (>= 6.0.0)
activemodel (>= 6.0.0) activemodel (>= 6.0.0)
bindex (>= 0.4.0) bindex (>= 0.4.0)
@@ -805,14 +810,14 @@ GEM
railties (>= 5.2) railties (>= 5.2)
semantic_range (>= 2.3.0) semantic_range (>= 2.3.0)
webrick (1.8.1) webrick (1.8.1)
websocket-driver (0.7.5) websocket-driver (0.7.6)
websocket-extensions (>= 0.1.0) websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5) websocket-extensions (0.1.5)
wisper (2.0.0) wisper (2.0.0)
working_hours (1.4.1) working_hours (1.4.1)
activesupport (>= 3.2) activesupport (>= 3.2)
tzinfo tzinfo
zeitwerk (2.6.9) zeitwerk (2.6.11)
PLATFORMS PLATFORMS
arm64-darwin-20 arm64-darwin-20
@@ -831,9 +836,10 @@ DEPENDENCIES
acts-as-taggable-on acts-as-taggable-on
administrate (>= 0.19.0) administrate (>= 0.19.0)
administrate-field-active_storage administrate-field-active_storage
administrate-field-belongs_to_search
annotate annotate
attr_extras attr_extras
audited (~> 5.3) audited (~> 5.4, >= 5.4.0)
aws-sdk-s3 aws-sdk-s3
azure-storage-blob! azure-storage-blob!
barnes barnes
@@ -907,7 +913,7 @@ DEPENDENCIES
rack-cors rack-cors
rack-mini-profiler (>= 3.1.1) rack-mini-profiler (>= 3.1.1)
rack-timeout rack-timeout
rails (~> 7.0.7.2) rails (~> 7.0.8.0)
redis redis
redis-namespace redis-namespace
responders responders
@@ -922,12 +928,12 @@ DEPENDENCIES
scout_apm scout_apm
scss_lint scss_lint
seed_dump seed_dump
sentry-rails (>= 5.10.0) sentry-rails (>= 5.11.0)
sentry-ruby sentry-ruby
sentry-sidekiq (>= 5.10.0) sentry-sidekiq (>= 5.11.0)
shoulda-matchers shoulda-matchers
sidekiq sidekiq (>= 7.1.3)
sidekiq-cron sidekiq-cron (>= 1.10.1)
simplecov (= 0.17.1) simplecov (= 0.17.1)
slack-ruby-client (~> 2.0.0) slack-ruby-client (~> 2.0.0)
spring spring
@@ -943,7 +949,7 @@ DEPENDENCIES
tzinfo-data tzinfo-data
uglifier uglifier
valid_email2 valid_email2
web-console web-console (>= 4.2.1)
web-push web-push
webmock webmock
webpacker webpacker
+1 -1
View File
@@ -1,4 +1,4 @@
backend: bin/rails s -p 3000 backend: bin/rails s -p 3000
frontend: bin/webpack-dev-server frontend: export NODE_OPTIONS=--openssl-legacy-provider && bin/webpack-dev-server
# https://github.com/mperham/sidekiq/issues/3090#issuecomment-389748695 # https://github.com/mperham/sidekiq/issues/3090#issuecomment-389748695
worker: dotenv bundle exec sidekiq -C config/sidekiq.yml worker: dotenv bundle exec sidekiq -C config/sidekiq.yml
+1 -1
View File
@@ -1 +1 @@
2.17.0 3.1.0
+1 -1
View File
@@ -1 +1 @@
2.3.0 2.4.0
+3
View File
@@ -60,6 +60,9 @@
], ],
"stack": "heroku-20", "stack": "heroku-20",
"buildpacks": [ "buildpacks": [
{
"url": "heroku/nodejs"
},
{ {
"url": "heroku/ruby" "url": "heroku/ruby"
} }
@@ -29,3 +29,5 @@
@import 'components/pagination'; @import 'components/pagination';
@import 'components/search'; @import 'components/search';
@import 'components/reports'; @import 'components/reports';
@import 'custom_styles';
@@ -24,14 +24,6 @@ select {
font-size: $base-font-size; font-size: $base-font-size;
} }
input,
select,
textarea {
display: block;
font-family: $base-font-family;
font-size: 16px;
}
[type="color"], [type="color"],
[type="date"], [type="date"],
[type="datetime-local"], [type="datetime-local"],
@@ -51,6 +43,7 @@ textarea {
background-color: $white; background-color: $white;
border: $base-border; border: $base-border;
border-radius: $base-border-radius; border-radius: $base-border-radius;
font-family: $base-font-family;
padding: 0.5em; padding: 0.5em;
transition: border-color $base-duration $base-timing; transition: border-color $base-duration $base-timing;
width: 100%; width: 100%;
@@ -4,8 +4,8 @@
float: left; float: left;
margin-bottom: $base-spacing; margin-bottom: $base-spacing;
margin-top: 0.25em; margin-top: 0.25em;
text-align: right; text-align: left;
width: calc(15% - 1rem); width: calc(16% - 1rem);
} }
.preserve-whitespace { .preserve-whitespace {
@@ -17,7 +17,7 @@
float: left; float: left;
margin-bottom: $base-spacing; margin-bottom: $base-spacing;
margin-left: 2rem; margin-left: 2rem;
width: calc(85% - 1rem); width: calc(84% - 1rem);
} }
.attribute--nested { .attribute--nested {
@@ -10,7 +10,7 @@ input[type="submit"],
color: $white; color: $white;
cursor: pointer; cursor: pointer;
display: inline-block; display: inline-block;
font-size: $font-size-default; font-size: $font-size-small;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
font-weight: $font-weight-medium; font-weight: $font-weight-medium;
line-height: 1; line-height: 1;
@@ -1,13 +1,18 @@
.main-content { .main-content {
font-size: $font-size-default; font-size: $font-size-default;
left: 23rem; left: 21rem;
position: absolute; position: absolute;
right: 0; right: 0;
top: 0; top: 0;
} }
.main-content__body { .main-content__body {
font-size: $font-size-small;
padding: $space-two; padding: $space-two;
table {
font-size: $font-size-small;
}
} }
.main-content__header { .main-content__header {
@@ -20,7 +25,7 @@
} }
.main-content__page-title { .main-content__page-title {
font-size: $font-size-large; font-size: $font-size-medium;
font-weight: $font-weight-medium; font-weight: $font-weight-medium;
margin-right: auto; margin-right: auto;
} }
@@ -1,7 +1,12 @@
.logo-brand { .logo-brand {
margin-bottom: $space-normal; margin-bottom: $space-normal;
padding: $space-normal $space-smaller $space-small; padding: $space-normal $space-smaller $space-small;
text-align: center; text-align: left;
img {
margin-bottom: $space-smaller;
max-height: 3rem;
}
} }
.navigation { .navigation {
@@ -19,12 +24,13 @@
padding: $space-normal; padding: $space-normal;
position: fixed; position: fixed;
top: 0; top: 0;
width: 23rem; width: 21rem;
z-index: 1023; z-index: 1023;
li { li {
align-items: center; align-items: center;
display: flex; display: flex;
font-size: $font-size-small;
a { a {
color: $color-gray; color: $color-gray;
@@ -35,6 +41,10 @@
min-width: $space-medium; min-width: $space-medium;
} }
} }
hr {
margin: $space-slab;
}
} }
.navigation__link { .navigation__link {
@@ -43,7 +53,7 @@
display: block; display: block;
line-height: 1; line-height: 1;
margin-bottom: $space-smaller; margin-bottom: $space-smaller;
padding: $space-one; padding: $space-small;
&:hover { &:hover {
color: $blue; 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; sans-serif !default;
$heading-font-family: $base-font-family !default; $heading-font-family: $base-font-family !default;
$base-font-size: 16px !default; $base-font-size: 14px !default;
$base-line-height: 1.5 !default; $base-line-height: 1.5 !default;
$heading-line-height: 1.2 !default; $heading-line-height: 1.2 !default;
@@ -78,11 +78,6 @@ $conv-header-height: 4rem;
$inbox-thumb-size: 4.8rem; $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 // Snackbar default
$woot-snackbar-bg: #323232; $woot-snackbar-bg: #323232;
$woot-snackbar-button: #ffeb3b; $woot-snackbar-button: #ffeb3b;
@@ -1,7 +1,7 @@
class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
before_action :portal before_action :portal
before_action :check_authorization before_action :check_authorization
before_action :fetch_article, except: [:index, :create, :attach_file, :reorder] before_action :fetch_article, except: [:index, :create, :reorder]
before_action :set_current_page, only: [:index] before_action :set_current_page, only: [:index]
def index def index
@@ -36,17 +36,6 @@ class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
head :ok head :ok
end end
def attach_file
file_blob = ActiveStorage::Blob.create_and_upload!(
key: nil,
io: params[:background_image].tempfile,
filename: params[:background_image].original_filename,
content_type: params[:background_image].content_type
)
file_blob.save!
render json: { file_url: url_for(file_blob) }
end
def reorder def reorder
Article.update_positions(params[:positions_hash]) Article.update_positions(params[:positions_hash])
head :ok head :ok
@@ -20,16 +20,6 @@ class Api::V1::Accounts::AutomationRulesController < Api::V1::Accounts::BaseCont
@automation_rule @automation_rule
end end
def attach_file
file_blob = ActiveStorage::Blob.create_and_upload!(
key: nil,
io: params[:attachment].tempfile,
filename: params[:attachment].original_filename,
content_type: params[:attachment].content_type
)
render json: { blob_key: file_blob.key, blob_id: file_blob.id }
end
def update def update
ActiveRecord::Base.transaction do ActiveRecord::Base.transaction do
automation_rule_update automation_rule_update
@@ -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
@@ -18,18 +18,19 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
def index def index
@contacts_count = resolved_contacts.count @contacts_count = resolved_contacts.count
@contacts = fetch_contacts_with_conversation_count(resolved_contacts) @contacts = fetch_contacts(resolved_contacts)
end end
def search def search
render json: { error: 'Specify search string with parameter q' }, status: :unprocessable_entity if params[:q].blank? && return render json: { error: 'Specify search string with parameter q' }, status: :unprocessable_entity if params[:q].blank? && return
contacts = resolved_contacts.where( 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}%" search: "%#{params[:q].strip}%"
) )
@contacts_count = contacts.count @contacts_count = contacts.count
@contacts = fetch_contacts_with_conversation_count(contacts) @contacts = fetch_contacts(contacts)
end end
def import def import
@@ -63,7 +64,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
result = ::Contacts::FilterService.new(params.permit!, current_user).perform result = ::Contacts::FilterService.new(params.permit!, current_user).perform
contacts = result[:contacts] contacts = result[:contacts]
@contacts_count = result[:count] @contacts_count = result[:count]
@contacts = fetch_contacts_with_conversation_count(contacts) @contacts = fetch_contacts(contacts)
end end
def contactable_inboxes def contactable_inboxes
@@ -125,17 +126,14 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
@current_page = params[:page] || 1 @current_page = params[:page] || 1
end end
def fetch_contacts_with_conversation_count(contacts) def fetch_contacts(contacts)
conversation_count_sub_query = 'SELECT COUNT(*) FROM "conversations" WHERE "conversations"."contact_id" = "contacts"."id"' contacts_with_avatar = filtrate(contacts)
contacts_with_conversation_count = filtrate(contacts) .includes([{ avatar_attachment: [:blob] }])
.select("contacts.*, (#{conversation_count_sub_query}) as conversations_count") .page(@current_page).per(RESULTS_PER_PAGE)
.group('contacts.id')
.includes([{ avatar_attachment: [:blob] }])
.page(@current_page).per(RESULTS_PER_PAGE)
return contacts_with_conversation_count.includes([{ contact_inboxes: [:inbox] }]) if @include_contact_inboxes return contacts_with_avatar.includes([{ contact_inboxes: [:inbox] }]) if @include_contact_inboxes
contacts_with_conversation_count contacts_with_avatar
end end
def build_contact_inbox def build_contact_inbox
@@ -39,16 +39,6 @@ class Api::V1::Accounts::MacrosController < Api::V1::Accounts::BaseController
head :ok head :ok
end end
def attach_file
file_blob = ActiveStorage::Blob.create_and_upload!(
key: nil,
io: params[:attachment].tempfile,
filename: params[:attachment].original_filename,
content_type: params[:attachment].content_type
)
render json: { blob_key: file_blob.key, blob_id: file_blob.id }
end
def execute def execute
::MacrosExecutionJob.perform_later(@macro, conversation_ids: params[:conversation_ids], user: Current.user) ::MacrosExecutionJob.perform_later(@macro, conversation_ids: params[:conversation_ids], user: Current.user)
@@ -1,7 +1,7 @@
class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
include ::FileTypeHelper include ::FileTypeHelper
before_action :fetch_portal, except: [:index, :create, :attach_file] before_action :fetch_portal, except: [:index, :create]
before_action :check_authorization before_action :check_authorization
before_action :set_current_page, only: [:index] before_action :set_current_page, only: [:index]
@@ -53,16 +53,6 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
@portal.logo.attach(blob) @portal.logo.attach(blob)
end end
def attach_file
file_blob = ActiveStorage::Blob.create_and_upload!(
key: nil,
io: params[:logo].tempfile,
filename: params[:logo].original_filename,
content_type: params[:logo].content_type
)
render json: { blob_key: file_blob.key, blob_id: file_blob.id }
end
private private
def fetch_portal def fetch_portal
@@ -0,0 +1,13 @@
class Api::V1::Accounts::UploadController < Api::V1::Accounts::BaseController
def create
file_blob = ActiveStorage::Blob.create_and_upload!(
key: nil,
io: params[:attachment].tempfile,
filename: params[:attachment].original_filename,
content_type: params[:attachment].content_type
)
file_blob.save!
render json: { file_url: url_for(file_blob), blob_key: file_blob.key, blob_id: file_blob.id }
end
end
@@ -1,7 +1,15 @@
class Api::V1::Integrations::WebhooksController < ApplicationController class Api::V1::Integrations::WebhooksController < ApplicationController
def create def create
builder = Integrations::Slack::IncomingMessageBuilder.new(params) builder = Integrations::Slack::IncomingMessageBuilder.new(permitted_params)
response = builder.perform response = builder.perform
render json: response render json: response
end 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 end
@@ -8,7 +8,7 @@ module EnsureCurrentAccountHelper
def ensure_current_account def ensure_current_account
account = Account.find(params[:account_id]) account = Account.find(params[:account_id])
ensure_account_is_active?(account) render_unauthorized('Account is suspended') and return unless account.active?
if current_user if current_user
account_accessible_for_user?(account) account_accessible_for_user?(account)
@@ -27,8 +27,4 @@ module EnsureCurrentAccountHelper
def account_accessible_for_bot?(account) def account_accessible_for_bot?(account)
render_unauthorized('You are not authorized to access this account') unless @resource.agent_bot_inboxes.find_by(account_id: account.id) render_unauthorized('You are not authorized to access this account') unless @resource.agent_bot_inboxes.find_by(account_id: account.id)
end end
def ensure_account_is_active?(account)
render_unauthorized('Account is suspended') unless account.active?
end
end end
@@ -9,11 +9,14 @@ module RequestExceptionHandler
def handle_with_exception def handle_with_exception
yield yield
rescue ActiveRecord::RecordNotFound rescue ActiveRecord::RecordNotFound => e
log_handled_error(e)
render_not_found_error('Resource could not be found') 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') render_unauthorized('You are not authorized to do this action')
rescue ActionController::ParameterMissing => e rescue ActionController::ParameterMissing => e
log_handled_error(e)
render_could_not_create_error(e.message) render_could_not_create_error(e.message)
ensure ensure
# to address the thread variable leak issues in Puma/Thin webserver # to address the thread variable leak issues in Puma/Thin webserver
@@ -41,6 +44,7 @@ module RequestExceptionHandler
end end
def render_record_invalid(exception) def render_record_invalid(exception)
log_handled_error(exception)
render json: { render json: {
message: exception.record.errors.full_messages.join(', '), message: exception.record.errors.full_messages.join(', '),
attributes: exception.record.errors.attribute_names attributes: exception.record.errors.attribute_names
@@ -48,6 +52,11 @@ module RequestExceptionHandler
end end
def render_error_response(exception) def render_error_response(exception)
log_handled_error(exception)
render json: exception.to_hash, status: exception.http_status render json: exception.to_hash, status: exception.http_status
end end
def log_handled_error(exception)
logger.info("Handled error: #{exception.inspect}")
end
end end
@@ -33,7 +33,7 @@ class Public::Api::V1::Inboxes::ConversationsController < Public::Api::V1::Inbox
end end
def create_conversation def create_conversation
::Conversation.create!(conversation_params) ConversationBuilder.new(params: conversation_params, contact_inbox: @contact_inbox).perform
end end
def trigger_typing_event(event) def trigger_typing_event(event)
@@ -41,11 +41,6 @@ class Public::Api::V1::Inboxes::ConversationsController < Public::Api::V1::Inbox
end end
def conversation_params def conversation_params
{ params.permit(custom_attributes: {})
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
}
end end
end end
@@ -44,7 +44,7 @@ class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::B
end end
def list_params def list_params
params.permit(:query, :locale, :sort) params.permit(:query, :locale, :sort, :status)
end end
def permitted_params def permitted_params
@@ -1,6 +1,8 @@
class Public::Api::V1::Portals::BaseController < PublicController class Public::Api::V1::Portals::BaseController < PublicController
before_action :show_plain_layout before_action :show_plain_layout
before_action :set_color_scheme
around_action :set_locale around_action :set_locale
after_action :allow_iframe_requests
private private
@@ -8,6 +10,14 @@ class Public::Api::V1::Portals::BaseController < PublicController
@is_plain_layout_enabled = params[:show_plain_layout] == 'true' @is_plain_layout_enabled = params[:show_plain_layout] == 'true'
end end
def set_color_scheme
@theme = if %w[dark light].include?(params[:theme])
params[:theme]
else
''
end
end
def set_locale(&) def set_locale(&)
switch_locale_with_portal(&) if params[:locale].present? switch_locale_with_portal(&) if params[:locale].present?
switch_locale_with_article(&) if params[:article_slug].present? switch_locale_with_article(&) if params[:article_slug].present?
@@ -39,4 +49,8 @@ class Public::Api::V1::Portals::BaseController < PublicController
I18n.with_locale(@locale, &) I18n.with_locale(@locale, &)
end end
def allow_iframe_requests
response.headers.delete('X-Frame-Options') if @is_plain_layout_enabled
end
end end
@@ -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 postgres_status
redis_metrics redis_metrics
chatwoot_edition chatwoot_edition
instance_meta
end end
def chatwoot_edition def chatwoot_edition
@@ -18,6 +19,10 @@ class SuperAdmin::InstanceStatusesController < SuperAdmin::ApplicationController
end end
end end
def instance_meta
@metrics['Database Migrations'] = ActiveRecord::Base.connection.migration_context.needs_migration? ? 'pending' : 'completed'
end
def chatwoot_version def chatwoot_version
@metrics['Chatwoot version'] = Chatwoot.config[:version] @metrics['Chatwoot version'] = Chatwoot.config[:version]
end end
+2 -2
View File
@@ -18,8 +18,8 @@ class AccountDashboard < Administrate::BaseDashboard
end end
ATTRIBUTE_TYPES = { ATTRIBUTE_TYPES = {
id: Field::Number, id: Field::Number.with_options(searchable: true),
name: Field::String, name: Field::String.with_options(searchable: true),
created_at: Field::DateTime, created_at: Field::DateTime,
updated_at: Field::DateTime, updated_at: Field::DateTime,
users: CountField, users: CountField,
+3 -3
View File
@@ -8,9 +8,9 @@ class AccountUserDashboard < Administrate::BaseDashboard
# which determines how the attribute is displayed # which determines how the attribute is displayed
# on pages throughout the dashboard. # on pages throughout the dashboard.
ATTRIBUTE_TYPES = { ATTRIBUTE_TYPES = {
account: Field::BelongsTo.with_options(searchable: true, searchable_field: 'name', order: 'id DESC'), account: Field::BelongsToSearch.with_options(class_name: 'Account', searchable: true, searchable_field: [:name, :id], order: 'id DESC'),
user: Field::BelongsTo.with_options(searchable: true, searchable_field: 'name', order: 'id DESC'), user: Field::BelongsToSearch.with_options(class_name: 'User', searchable: true, searchable_field: [:name, :email, :id], order: 'id DESC'),
inviter: Field::BelongsTo.with_options(class_name: 'User', searchable: true, searchable_field: 'name'), inviter: Field::BelongsToSearch.with_options(class_name: 'User', searchable: true, searchable_field: [:name, :email, :id], order: 'id DESC'),
id: Field::Number, id: Field::Number,
role: Field::Select.with_options(collection: AccountUser.roles.keys), role: Field::Select.with_options(collection: AccountUser.roles.keys),
created_at: Field::DateTime, created_at: Field::DateTime,
+3 -3
View File
@@ -9,7 +9,7 @@ class UserDashboard < Administrate::BaseDashboard
# on pages throughout the dashboard. # on pages throughout the dashboard.
ATTRIBUTE_TYPES = { ATTRIBUTE_TYPES = {
account_users: Field::HasMany, account_users: Field::HasMany,
id: Field::Number, id: Field::Number.with_options(searchable: true),
avatar_url: AvatarField, avatar_url: AvatarField,
avatar: Field::ActiveStorage.with_options( avatar: Field::ActiveStorage.with_options(
destroy_url: proc do |_namespace, _resource, attachment| destroy_url: proc do |_namespace, _resource, attachment|
@@ -28,9 +28,9 @@ class UserDashboard < Administrate::BaseDashboard
confirmed_at: Field::DateTime, confirmed_at: Field::DateTime,
confirmation_sent_at: Field::DateTime, confirmation_sent_at: Field::DateTime,
unconfirmed_email: Field::String, unconfirmed_email: Field::String,
name: Field::String, name: Field::String.with_options(searchable: true),
display_name: Field::String, display_name: Field::String,
email: Field::String, email: Field::String.with_options(searchable: true),
tokens: Field::String.with_options(searchable: false), tokens: Field::String.with_options(searchable: false),
created_at: Field::DateTime, created_at: Field::DateTime,
updated_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> <script>
import { mapGetters } from 'vuex'; 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 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 UpdateBanner from './components/app/UpdateBanner.vue';
import UpgradeBanner from './components/app/UpgradeBanner.vue'; import UpgradeBanner from './components/app/UpgradeBanner.vue';
import PaymentPendingBanner from './components/app/PaymentPendingBanner.vue'; import PaymentPendingBanner from './components/app/PaymentPendingBanner.vue';
import vueActionCable from './helper/actionCable'; import vueActionCable from './helper/actionCable';
import WootSnackbarBox from './components/SnackbarContainer'; import WootSnackbarBox from './components/SnackbarContainer.vue';
import rtlMixin from 'shared/mixins/rtlMixin'; import rtlMixin from 'shared/mixins/rtlMixin';
import { setColorTheme } from './helper/themeHelper'; import { setColorTheme } from './helper/themeHelper';
import { import {
@@ -111,10 +111,8 @@ export default {
this.$store.dispatch('setActiveAccount', { this.$store.dispatch('setActiveAccount', {
accountId: this.currentAccountId, accountId: this.currentAccountId,
}); });
const { const { locale, latest_chatwoot_version: latestChatwootVersion } =
locale, this.getAccount(this.currentAccountId);
latest_chatwoot_version: latestChatwootVersion,
} = this.getAccount(this.currentAccountId);
const { pubsub_token: pubsubToken } = this.currentUser || {}; const { pubsub_token: pubsubToken } = this.currentUser || {};
this.setLocale(locale); this.setLocale(locale);
this.updateRTLDirectionView(locale); this.updateRTLDirectionView(locale);
+2 -3
View File
@@ -15,9 +15,8 @@ class ApiClient {
// eslint-disable-next-line class-methods-use-this // eslint-disable-next-line class-methods-use-this
get accountIdFromRoute() { get accountIdFromRoute() {
const isInsideAccountScopedURLs = window.location.pathname.includes( const isInsideAccountScopedURLs =
'/app/accounts' window.location.pathname.includes('/app/accounts');
);
if (isInsideAccountScopedURLs) { if (isInsideAccountScopedURLs) {
return window.location.pathname.split('/')[3]; return window.location.pathname.split('/')[3];
@@ -9,14 +9,6 @@ class AutomationsAPI extends ApiClient {
clone(automationId) { clone(automationId) {
return axios.post(`${this.url}/${automationId}/clone`); return axios.post(`${this.url}/${automationId}/clone`);
} }
attachment(file) {
return axios.post(`${this.url}/attach_file`, file, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
}
} }
export default new AutomationsAPI(); export default new AutomationsAPI();
+1
View File
@@ -53,6 +53,7 @@ class ContactAPI extends ApiClient {
return axios.get(requestURL); return axios.get(requestURL);
} }
// eslint-disable-next-line default-param-last
filter(page = 1, sortAttr = 'name', queryPayload) { filter(page = 1, sortAttr = 'name', queryPayload) {
let requestURL = `${this.url}/filter?${buildContactParams(page, sortAttr)}`; let requestURL = `${this.url}/filter?${buildContactParams(page, sortAttr)}`;
return axios.post(requestURL, queryPayload); return axios.post(requestURL, queryPayload);
@@ -1,6 +1,5 @@
import accountAPI from '../account'; import accountAPI from '../account';
import ApiClient from '../../ApiClient'; import ApiClient from '../../ApiClient';
import describeWithAPIMock from '../../specs/apiSpecHelper';
describe('#enterpriseAccountAPI', () => { describe('#enterpriseAccountAPI', () => {
it('creates correct instance', () => { it('creates correct instance', () => {
@@ -13,17 +12,33 @@ describe('#enterpriseAccountAPI', () => {
expect(accountAPI).toHaveProperty('checkout'); 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', () => { it('#checkout', () => {
accountAPI.checkout(); accountAPI.checkout();
expect(context.axiosMock.post).toHaveBeenCalledWith( expect(axiosMock.post).toHaveBeenCalledWith(
'/enterprise/api/v1/checkout' '/enterprise/api/v1/checkout'
); );
}); });
it('#subscription', () => { it('#subscription', () => {
accountAPI.subscription(); accountAPI.subscription();
expect(context.axiosMock.post).toHaveBeenCalledWith( expect(axiosMock.post).toHaveBeenCalledWith(
'/enterprise/api/v1/subscription' '/enterprise/api/v1/subscription'
); );
}); });
@@ -47,20 +47,6 @@ class ArticlesAPI extends PortalsAPI {
return axios.delete(`${this.url}/${portalSlug}/articles/${articleId}`); return axios.delete(`${this.url}/${portalSlug}/articles/${articleId}`);
} }
uploadImage({ portalSlug, file }) {
let formData = new FormData();
formData.append('background_image', file);
return axios.post(
`${this.url}/${portalSlug}/articles/attach_file`,
formData,
{
headers: {
'Content-Type': 'multipart/form-data',
},
}
);
}
reorderArticles({ portalSlug, reorderedGroup, categorySlug }) { reorderArticles({ portalSlug, reorderedGroup, categorySlug }) {
return axios.post(`${this.url}/${portalSlug}/articles/reorder`, { return axios.post(`${this.url}/${portalSlug}/articles/reorder`, {
positions_hash: reorderedGroup, positions_hash: reorderedGroup,
+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) { getSummary(since, until, type = 'account', id, groupBy, businessHours) {
return axios.get(`${this.url}/summary`, { return axios.get(`${this.url}/summary`, {
params: { params: {
@@ -1,6 +1,5 @@
import accountAPI from '../account'; import accountAPI from '../account';
import ApiClient from '../ApiClient'; import ApiClient from '../ApiClient';
import describeWithAPIMock from './apiSpecHelper';
describe('#accountAPI', () => { describe('#accountAPI', () => {
it('creates correct instance', () => { it('creates correct instance', () => {
@@ -13,12 +12,28 @@ describe('#accountAPI', () => {
expect(accountAPI).toHaveProperty('createAccount'); 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', () => { it('#createAccount', () => {
accountAPI.createAccount({ accountAPI.createAccount({
name: 'Chatwoot', name: 'Chatwoot',
}); });
expect(context.axiosMock.post).toHaveBeenCalledWith('/api/v1/accounts', { expect(axiosMock.post).toHaveBeenCalledWith('/api/v1/accounts', {
name: 'Chatwoot', name: 'Chatwoot',
}); });
}); });
@@ -1,6 +1,5 @@
import accountActionsAPI from '../accountActions'; import accountActionsAPI from '../accountActions';
import ApiClient from '../ApiClient'; import ApiClient from '../ApiClient';
import describeWithAPIMock from './apiSpecHelper';
describe('#ContactsAPI', () => { describe('#ContactsAPI', () => {
it('creates correct instance', () => { it('creates correct instance', () => {
@@ -8,10 +7,26 @@ describe('#ContactsAPI', () => {
expect(accountActionsAPI).toHaveProperty('merge'); 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', () => { it('#merge', () => {
accountActionsAPI.merge(1, 2); accountActionsAPI.merge(1, 2);
expect(context.axiosMock.post).toHaveBeenCalledWith( expect(axiosMock.post).toHaveBeenCalledWith(
'/api/v1/actions/contact_merge', '/api/v1/actions/contact_merge',
{ {
base_contact_id: 1, 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 articlesAPI from '../helpCenter/articles';
import ApiClient from 'dashboard/api/helpCenter/portals'; import ApiClient from 'dashboard/api/helpCenter/portals';
import describeWithAPIMock from './apiSpecHelper';
describe('#PortalAPI', () => { describe('#PortalAPI', () => {
it('creates correct instance', () => { it('creates correct instance', () => {
@@ -12,7 +11,23 @@ describe('#PortalAPI', () => {
expect(articlesAPI).toHaveProperty('delete'); expect(articlesAPI).toHaveProperty('delete');
expect(articlesAPI).toHaveProperty('getArticles'); 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', () => { it('#getArticles', () => {
articlesAPI.getArticles({ articlesAPI.getArticles({
pageNumber: 1, pageNumber: 1,
@@ -21,30 +36,62 @@ describe('#PortalAPI', () => {
status: 'published', status: 'published',
author_id: '1', 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' '/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', () => { it('#getArticle', () => {
articlesAPI.getArticle({ articlesAPI.getArticle({
id: 1, id: 1,
portalSlug: 'room-rental', portalSlug: 'room-rental',
}); });
expect(context.axiosMock.get).toHaveBeenCalledWith( expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v1/portals/room-rental/articles/1' '/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', () => { it('#updateArticle', () => {
articlesAPI.updateArticle({ articlesAPI.updateArticle({
articleId: 1, articleId: 1,
portalSlug: 'room-rental', portalSlug: 'room-rental',
articleObj: { title: 'Update shipping address' }, articleObj: { title: 'Update shipping address' },
}); });
expect(context.axiosMock.patch).toHaveBeenCalledWith( expect(axiosMock.patch).toHaveBeenCalledWith(
'/api/v1/portals/room-rental/articles/1', '/api/v1/portals/room-rental/articles/1',
{ {
title: 'Update shipping address', 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', () => { it('#deleteArticle', () => {
articlesAPI.deleteArticle({ articlesAPI.deleteArticle({
articleId: 1, articleId: 1,
portalSlug: 'room-rental', portalSlug: 'room-rental',
}); });
expect(context.axiosMock.delete).toHaveBeenCalledWith( expect(axiosMock.delete).toHaveBeenCalledWith(
'/api/v1/portals/room-rental/articles/1' '/api/v1/portals/room-rental/articles/1'
); );
}); });
@@ -1,18 +1,30 @@
import assignableAgentsAPI from '../assignableAgents'; import assignableAgentsAPI from '../assignableAgents';
import describeWithAPIMock from './apiSpecHelper';
describe('#AssignableAgentsAPI', () => { 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', () => { it('#getAssignableAgents', () => {
assignableAgentsAPI.get([1]); assignableAgentsAPI.get([1]);
expect(context.axiosMock.get).toHaveBeenCalledWith( expect(axiosMock.get).toHaveBeenCalledWith('/api/v1/assignable_agents', {
'/api/v1/assignable_agents', params: {
{ inbox_ids: [1],
params: { },
inbox_ids: [1], });
},
}
);
}); });
}); });
}); });
@@ -1,6 +1,5 @@
import fbChannel from '../../channel/fbChannel'; import fbChannel from '../../channel/fbChannel';
import ApiClient from '../../ApiClient'; import ApiClient from '../../ApiClient';
import describeWithAPIMock from '../apiSpecHelper';
describe('#FBChannel', () => { describe('#FBChannel', () => {
it('creates correct instance', () => { it('creates correct instance', () => {
@@ -11,10 +10,26 @@ describe('#FBChannel', () => {
expect(fbChannel).toHaveProperty('update'); expect(fbChannel).toHaveProperty('update');
expect(fbChannel).toHaveProperty('delete'); 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', () => { it('#create', () => {
fbChannel.create({ omniauthToken: 'ASFM131CSF@#@$', appId: 'chatwoot' }); fbChannel.create({ omniauthToken: 'ASFM131CSF@#@$', appId: 'chatwoot' });
expect(context.axiosMock.post).toHaveBeenCalledWith( expect(axiosMock.post).toHaveBeenCalledWith(
'/api/v1/callbacks/register_facebook_page', '/api/v1/callbacks/register_facebook_page',
{ {
omniauthToken: 'ASFM131CSF@#@$', omniauthToken: 'ASFM131CSF@#@$',
@@ -27,7 +42,7 @@ describe('#FBChannel', () => {
omniauthToken: 'ASFM131CSF@#@$', omniauthToken: 'ASFM131CSF@#@$',
inboxId: 1, inboxId: 1,
}); });
expect(context.axiosMock.post).toHaveBeenCalledWith( expect(axiosMock.post).toHaveBeenCalledWith(
'/api/v1/callbacks/reauthorize_page', '/api/v1/callbacks/reauthorize_page',
{ {
omniauth_token: 'ASFM131CSF@#@$', omniauth_token: 'ASFM131CSF@#@$',
@@ -1,6 +1,5 @@
import contactAPI, { buildContactParams } from '../contacts'; import contactAPI, { buildContactParams } from '../contacts';
import ApiClient from '../ApiClient'; import ApiClient from '../ApiClient';
import describeWithAPIMock from './apiSpecHelper';
describe('#ContactsAPI', () => { describe('#ContactsAPI', () => {
it('creates correct instance', () => { it('creates correct instance', () => {
@@ -15,56 +14,67 @@ describe('#ContactsAPI', () => {
expect(contactAPI).toHaveProperty('destroyAvatar'); 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', () => { it('#get', () => {
contactAPI.get(1, 'name', 'customer-support'); 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' '/api/v1/contacts?include_contact_inboxes=false&page=1&sort=name&labels[]=customer-support'
); );
}); });
it('#getConversations', () => { it('#getConversations', () => {
contactAPI.getConversations(1); contactAPI.getConversations(1);
expect(context.axiosMock.get).toHaveBeenCalledWith( expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v1/contacts/1/conversations' '/api/v1/contacts/1/conversations'
); );
}); });
it('#getContactableInboxes', () => { it('#getContactableInboxes', () => {
contactAPI.getContactableInboxes(1); contactAPI.getContactableInboxes(1);
expect(context.axiosMock.get).toHaveBeenCalledWith( expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v1/contacts/1/contactable_inboxes' '/api/v1/contacts/1/contactable_inboxes'
); );
}); });
it('#getContactLabels', () => { it('#getContactLabels', () => {
contactAPI.getContactLabels(1); contactAPI.getContactLabels(1);
expect(context.axiosMock.get).toHaveBeenCalledWith( expect(axiosMock.get).toHaveBeenCalledWith('/api/v1/contacts/1/labels');
'/api/v1/contacts/1/labels'
);
}); });
it('#updateContactLabels', () => { it('#updateContactLabels', () => {
const labels = ['support-query']; const labels = ['support-query'];
contactAPI.updateContactLabels(1, labels); contactAPI.updateContactLabels(1, labels);
expect(context.axiosMock.post).toHaveBeenCalledWith( expect(axiosMock.post).toHaveBeenCalledWith('/api/v1/contacts/1/labels', {
'/api/v1/contacts/1/labels', labels,
{ });
labels,
}
);
}); });
it('#search', () => { it('#search', () => {
contactAPI.search('leads', 1, 'date', 'customer-support'); 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' '/api/v1/contacts/search?include_contact_inboxes=false&page=1&sort=date&q=leads&labels[]=customer-support'
); );
}); });
it('#destroyCustomAttributes', () => { it('#destroyCustomAttributes', () => {
contactAPI.destroyCustomAttributes(1, ['cloudCustomer']); contactAPI.destroyCustomAttributes(1, ['cloudCustomer']);
expect(context.axiosMock.post).toHaveBeenCalledWith( expect(axiosMock.post).toHaveBeenCalledWith(
'/api/v1/contacts/1/destroy_custom_attributes', '/api/v1/contacts/1/destroy_custom_attributes',
{ {
custom_attributes: ['cloudCustomer'], custom_attributes: ['cloudCustomer'],
@@ -75,7 +85,7 @@ describe('#ContactsAPI', () => {
it('#importContacts', () => { it('#importContacts', () => {
const file = 'file'; const file = 'file';
contactAPI.importContacts(file); contactAPI.importContacts(file);
expect(context.axiosMock.post).toHaveBeenCalledWith( expect(axiosMock.post).toHaveBeenCalledWith(
'/api/v1/contacts/import', '/api/v1/contacts/import',
expect.any(FormData), expect.any(FormData),
{ {
@@ -96,7 +106,7 @@ describe('#ContactsAPI', () => {
], ],
}; };
contactAPI.filter(1, 'name', queryPayload); 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', '/api/v1/contacts/filter?include_contact_inboxes=false&page=1&sort=name',
queryPayload queryPayload
); );
@@ -104,7 +114,7 @@ describe('#ContactsAPI', () => {
it('#destroyAvatar', () => { it('#destroyAvatar', () => {
contactAPI.destroyAvatar(1); contactAPI.destroyAvatar(1);
expect(context.axiosMock.delete).toHaveBeenCalledWith( expect(axiosMock.delete).toHaveBeenCalledWith(
'/api/v1/contacts/1/avatar' '/api/v1/contacts/1/avatar'
); );
}); });
@@ -1,6 +1,5 @@
import conversationsAPI from '../conversations'; import conversationsAPI from '../conversations';
import ApiClient from '../ApiClient'; import ApiClient from '../ApiClient';
import describeWithAPIMock from './apiSpecHelper';
describe('#ConversationApi', () => { describe('#ConversationApi', () => {
it('creates correct instance', () => { it('creates correct instance', () => {
@@ -14,10 +13,26 @@ describe('#ConversationApi', () => {
expect(conversationsAPI).toHaveProperty('updateLabels'); 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', () => { it('#getLabels', () => {
conversationsAPI.getLabels(1); conversationsAPI.getLabels(1);
expect(context.axiosMock.get).toHaveBeenCalledWith( expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v1/conversations/1/labels' '/api/v1/conversations/1/labels'
); );
}); });
@@ -25,7 +40,7 @@ describe('#ConversationApi', () => {
it('#updateLabels', () => { it('#updateLabels', () => {
const labels = ['support-query']; const labels = ['support-query'];
conversationsAPI.updateLabels(1, labels); conversationsAPI.updateLabels(1, labels);
expect(context.axiosMock.post).toHaveBeenCalledWith( expect(axiosMock.post).toHaveBeenCalledWith(
'/api/v1/conversations/1/labels', '/api/v1/conversations/1/labels',
{ {
labels, labels,
@@ -1,6 +1,5 @@
import csatReportsAPI from '../csatReports'; import csatReportsAPI from '../csatReports';
import ApiClient from '../ApiClient'; import ApiClient from '../ApiClient';
import describeWithAPIMock from './apiSpecHelper';
describe('#Reports API', () => { describe('#Reports API', () => {
it('creates correct instance', () => { it('creates correct instance', () => {
@@ -9,10 +8,26 @@ describe('#Reports API', () => {
expect(csatReportsAPI).toHaveProperty('get'); expect(csatReportsAPI).toHaveProperty('get');
expect(csatReportsAPI).toHaveProperty('getMetrics'); 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', () => { it('#get', () => {
csatReportsAPI.get({ page: 1, from: 1622485800, to: 1623695400 }); csatReportsAPI.get({ page: 1, from: 1622485800, to: 1623695400 });
expect(context.axiosMock.get).toHaveBeenCalledWith( expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v1/csat_survey_responses', '/api/v1/csat_survey_responses',
{ {
params: { params: {
@@ -26,7 +41,7 @@ describe('#Reports API', () => {
}); });
it('#getMetrics', () => { it('#getMetrics', () => {
csatReportsAPI.getMetrics({ from: 1622485800, to: 1623695400 }); csatReportsAPI.getMetrics({ from: 1622485800, to: 1623695400 });
expect(context.axiosMock.get).toHaveBeenCalledWith( expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v1/csat_survey_responses/metrics', '/api/v1/csat_survey_responses/metrics',
{ {
params: { since: 1622485800, until: 1623695400 }, params: { since: 1622485800, until: 1623695400 },
@@ -39,7 +54,7 @@ describe('#Reports API', () => {
to: 1623695400, to: 1623695400,
user_ids: 1, user_ids: 1,
}); });
expect(context.axiosMock.get).toHaveBeenCalledWith( expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v1/csat_survey_responses/download', '/api/v1/csat_survey_responses/download',
{ {
params: { params: {
@@ -1,6 +1,5 @@
import conversationAPI from '../../inbox/conversation'; import conversationAPI from '../../inbox/conversation';
import ApiClient from '../../ApiClient'; import ApiClient from '../../ApiClient';
import describeWithAPIMock from '../apiSpecHelper';
describe('#ConversationAPI', () => { describe('#ConversationAPI', () => {
it('creates correct instance', () => { it('creates correct instance', () => {
@@ -22,7 +21,23 @@ describe('#ConversationAPI', () => {
expect(conversationAPI).toHaveProperty('filter'); 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', () => { it('#get conversations', () => {
conversationAPI.get({ conversationAPI.get({
inboxId: 1, inboxId: 1,
@@ -32,19 +47,16 @@ describe('#ConversationAPI', () => {
labels: [], labels: [],
teamId: 1, teamId: 1,
}); });
expect(context.axiosMock.get).toHaveBeenCalledWith( expect(axiosMock.get).toHaveBeenCalledWith('/api/v1/conversations', {
'/api/v1/conversations', params: {
{ inbox_id: 1,
params: { team_id: 1,
inbox_id: 1, status: 'open',
team_id: 1, assignee_type: 'me',
status: 'open', page: 1,
assignee_type: 'me', labels: [],
page: 1, },
labels: [], });
},
}
);
}); });
it('#search', () => { it('#search', () => {
@@ -53,7 +65,7 @@ describe('#ConversationAPI', () => {
page: 1, page: 1,
}); });
expect(context.axiosMock.get).toHaveBeenCalledWith( expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v1/conversations/search', '/api/v1/conversations/search',
{ {
params: { params: {
@@ -66,7 +78,7 @@ describe('#ConversationAPI', () => {
it('#toggleStatus', () => { it('#toggleStatus', () => {
conversationAPI.toggleStatus({ conversationId: 12, status: 'online' }); conversationAPI.toggleStatus({ conversationId: 12, status: 'online' });
expect(context.axiosMock.post).toHaveBeenCalledWith( expect(axiosMock.post).toHaveBeenCalledWith(
`/api/v1/conversations/12/toggle_status`, `/api/v1/conversations/12/toggle_status`,
{ {
status: 'online', status: 'online',
@@ -77,7 +89,7 @@ describe('#ConversationAPI', () => {
it('#assignAgent', () => { it('#assignAgent', () => {
conversationAPI.assignAgent({ conversationId: 12, agentId: 34 }); conversationAPI.assignAgent({ conversationId: 12, agentId: 34 });
expect(context.axiosMock.post).toHaveBeenCalledWith( expect(axiosMock.post).toHaveBeenCalledWith(
`/api/v1/conversations/12/assignments?assignee_id=34`, `/api/v1/conversations/12/assignments?assignee_id=34`,
{} {}
); );
@@ -85,7 +97,7 @@ describe('#ConversationAPI', () => {
it('#assignTeam', () => { it('#assignTeam', () => {
conversationAPI.assignTeam({ conversationId: 12, teamId: 1 }); conversationAPI.assignTeam({ conversationId: 12, teamId: 1 });
expect(context.axiosMock.post).toHaveBeenCalledWith( expect(axiosMock.post).toHaveBeenCalledWith(
`/api/v1/conversations/12/assignments`, `/api/v1/conversations/12/assignments`,
{ {
team_id: 1, team_id: 1,
@@ -95,7 +107,7 @@ describe('#ConversationAPI', () => {
it('#markMessageRead', () => { it('#markMessageRead', () => {
conversationAPI.markMessageRead({ id: 12 }); conversationAPI.markMessageRead({ id: 12 });
expect(context.axiosMock.post).toHaveBeenCalledWith( expect(axiosMock.post).toHaveBeenCalledWith(
`/api/v1/conversations/12/update_last_seen` `/api/v1/conversations/12/update_last_seen`
); );
}); });
@@ -105,7 +117,7 @@ describe('#ConversationAPI', () => {
conversationId: 12, conversationId: 12,
status: 'typing_on', status: 'typing_on',
}); });
expect(context.axiosMock.post).toHaveBeenCalledWith( expect(axiosMock.post).toHaveBeenCalledWith(
`/api/v1/conversations/12/toggle_typing_status`, `/api/v1/conversations/12/toggle_typing_status`,
{ {
typing_status: 'typing_on', typing_status: 'typing_on',
@@ -115,14 +127,14 @@ describe('#ConversationAPI', () => {
it('#mute', () => { it('#mute', () => {
conversationAPI.mute(45); conversationAPI.mute(45);
expect(context.axiosMock.post).toHaveBeenCalledWith( expect(axiosMock.post).toHaveBeenCalledWith(
'/api/v1/conversations/45/mute' '/api/v1/conversations/45/mute'
); );
}); });
it('#unmute', () => { it('#unmute', () => {
conversationAPI.unmute(45); conversationAPI.unmute(45);
expect(context.axiosMock.post).toHaveBeenCalledWith( expect(axiosMock.post).toHaveBeenCalledWith(
'/api/v1/conversations/45/unmute' '/api/v1/conversations/45/unmute'
); );
}); });
@@ -135,18 +147,15 @@ describe('#ConversationAPI', () => {
labels: [], labels: [],
teamId: 1, teamId: 1,
}); });
expect(context.axiosMock.get).toHaveBeenCalledWith( expect(axiosMock.get).toHaveBeenCalledWith('/api/v1/conversations/meta', {
'/api/v1/conversations/meta', params: {
{ inbox_id: 1,
params: { team_id: 1,
inbox_id: 1, status: 'open',
team_id: 1, assignee_type: 'me',
status: 'open', labels: [],
assignee_type: 'me', },
labels: [], });
},
}
);
}); });
it('#sendEmailTranscript', () => { it('#sendEmailTranscript', () => {
@@ -154,7 +163,7 @@ describe('#ConversationAPI', () => {
conversationId: 45, conversationId: 45,
email: 'john@acme.inc', email: 'john@acme.inc',
}); });
expect(context.axiosMock.post).toHaveBeenCalledWith( expect(axiosMock.post).toHaveBeenCalledWith(
'/api/v1/conversations/45/transcript', '/api/v1/conversations/45/transcript',
{ {
email: 'john@acme.inc', email: 'john@acme.inc',
@@ -167,7 +176,7 @@ describe('#ConversationAPI', () => {
conversationId: 45, conversationId: 45,
customAttributes: { order_d: '1001' }, customAttributes: { order_d: '1001' },
}); });
expect(context.axiosMock.post).toHaveBeenCalledWith( expect(axiosMock.post).toHaveBeenCalledWith(
'/api/v1/conversations/45/custom_attributes', '/api/v1/conversations/45/custom_attributes',
{ {
custom_attributes: { order_d: '1001' }, custom_attributes: { order_d: '1001' },
@@ -202,9 +211,7 @@ describe('#ConversationAPI', () => {
}, },
}; };
conversationAPI.filter(payload); conversationAPI.filter(payload);
expect( expect(axiosMock.post).toHaveBeenCalledWith(
context.axiosMock.post
).toHaveBeenCalledWith(
'/api/v1/conversations/filter', '/api/v1/conversations/filter',
payload.queryData, payload.queryData,
{ params: { page: payload.page } } { params: { page: payload.page } }
@@ -213,7 +220,7 @@ describe('#ConversationAPI', () => {
it('#getAllAttachments', () => { it('#getAllAttachments', () => {
conversationAPI.getAllAttachments(1); conversationAPI.getAllAttachments(1);
expect(context.axiosMock.get).toHaveBeenCalledWith( expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v1/conversations/1/attachments' '/api/v1/conversations/1/attachments'
); );
}); });
@@ -1,6 +1,5 @@
import messageAPI, { buildCreatePayload } from '../../inbox/message'; import messageAPI, { buildCreatePayload } from '../../inbox/message';
import ApiClient from '../../ApiClient'; import ApiClient from '../../ApiClient';
import describeWithAPIMock from '../apiSpecHelper';
describe('#ConversationAPI', () => { describe('#ConversationAPI', () => {
it('creates correct instance', () => { it('creates correct instance', () => {
@@ -13,13 +12,29 @@ describe('#ConversationAPI', () => {
expect(messageAPI).toHaveProperty('getPreviousMessages'); 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', () => { it('#getPreviousMessages', () => {
messageAPI.getPreviousMessages({ messageAPI.getPreviousMessages({
conversationId: 12, conversationId: 12,
before: 4573, before: 4573,
}); });
expect(context.axiosMock.get).toHaveBeenCalledWith( expect(axiosMock.get).toHaveBeenCalledWith(
`/api/v1/conversations/12/messages`, `/api/v1/conversations/12/messages`,
{ {
params: { params: {
@@ -35,7 +50,6 @@ describe('#ConversationAPI', () => {
message: 'test content', message: 'test content',
echoId: 12, echoId: 12,
isPrivate: true, isPrivate: true,
files: [new Blob(['test-content'], { type: 'application/pdf' })], files: [new Blob(['test-content'], { type: 'application/pdf' })],
}); });
expect(formPayload).toBeInstanceOf(FormData); expect(formPayload).toBeInstanceOf(FormData);
@@ -58,8 +72,10 @@ describe('#ConversationAPI', () => {
private: false, private: false,
echo_id: 12, echo_id: 12,
content_attributes: { in_reply_to: 12 }, content_attributes: { in_reply_to: 12 },
bcc_emails: '',
cc_emails: '', cc_emails: '',
bcc_emails: '',
to_emails: '',
template_params: undefined,
}); });
}); });
}); });
@@ -1,6 +1,5 @@
import inboxesAPI from '../inboxes'; import inboxesAPI from '../inboxes';
import ApiClient from '../ApiClient'; import ApiClient from '../ApiClient';
import describeWithAPIMock from './apiSpecHelper';
describe('#InboxesAPI', () => { describe('#InboxesAPI', () => {
it('creates correct instance', () => { it('creates correct instance', () => {
@@ -14,19 +13,32 @@ describe('#InboxesAPI', () => {
expect(inboxesAPI).toHaveProperty('getAgentBot'); expect(inboxesAPI).toHaveProperty('getAgentBot');
expect(inboxesAPI).toHaveProperty('setAgentBot'); 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', () => { it('#getCampaigns', () => {
inboxesAPI.getCampaigns(2); inboxesAPI.getCampaigns(2);
expect(context.axiosMock.get).toHaveBeenCalledWith( expect(axiosMock.get).toHaveBeenCalledWith('/api/v1/inboxes/2/campaigns');
'/api/v1/inboxes/2/campaigns'
);
}); });
it('#deleteInboxAvatar', () => { it('#deleteInboxAvatar', () => {
inboxesAPI.deleteInboxAvatar(2); inboxesAPI.deleteInboxAvatar(2);
expect(context.axiosMock.delete).toHaveBeenCalledWith( expect(axiosMock.delete).toHaveBeenCalledWith('/api/v1/inboxes/2/avatar');
'/api/v1/inboxes/2/avatar'
);
}); });
}); });
}); });
@@ -1,6 +1,5 @@
import integrationAPI from '../integrations'; import integrationAPI from '../integrations';
import ApiClient from '../ApiClient'; import ApiClient from '../ApiClient';
import describeWithAPIMock from './apiSpecHelper';
describe('#integrationAPI', () => { describe('#integrationAPI', () => {
it('creates correct instance', () => { it('creates correct instance', () => {
@@ -16,11 +15,27 @@ describe('#integrationAPI', () => {
expect(integrationAPI).toHaveProperty('listAllSlackChannels'); expect(integrationAPI).toHaveProperty('listAllSlackChannels');
expect(integrationAPI).toHaveProperty('deleteHook'); 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', () => { it('#connectSlack', () => {
const code = 'SDNFJNSDFNDSJN'; const code = 'SDNFJNSDFNDSJN';
integrationAPI.connectSlack(code); integrationAPI.connectSlack(code);
expect(context.axiosMock.post).toHaveBeenCalledWith( expect(axiosMock.post).toHaveBeenCalledWith(
'/api/v1/integrations/slack', '/api/v1/integrations/slack',
{ {
code, code,
@@ -31,7 +46,7 @@ describe('#integrationAPI', () => {
it('#updateSlack', () => { it('#updateSlack', () => {
const updateObj = { referenceId: 'SDFSDGSVE' }; const updateObj = { referenceId: 'SDFSDGSVE' };
integrationAPI.updateSlack(updateObj); integrationAPI.updateSlack(updateObj);
expect(context.axiosMock.patch).toHaveBeenCalledWith( expect(axiosMock.patch).toHaveBeenCalledWith(
'/api/v1/integrations/slack', '/api/v1/integrations/slack',
{ {
reference_id: updateObj.referenceId, reference_id: updateObj.referenceId,
@@ -41,16 +56,14 @@ describe('#integrationAPI', () => {
it('#listAllSlackChannels', () => { it('#listAllSlackChannels', () => {
integrationAPI.listAllSlackChannels(); integrationAPI.listAllSlackChannels();
expect(context.axiosMock.get).toHaveBeenCalledWith( expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v1/integrations/slack/list_all_channels' '/api/v1/integrations/slack/list_all_channels'
); );
}); });
it('#delete', () => { it('#delete', () => {
integrationAPI.delete(2); integrationAPI.delete(2);
expect(context.axiosMock.delete).toHaveBeenCalledWith( expect(axiosMock.delete).toHaveBeenCalledWith('/api/v1/integrations/2');
'/api/v1/integrations/2'
);
}); });
it('#createHook', () => { it('#createHook', () => {
@@ -59,7 +72,7 @@ describe('#integrationAPI', () => {
settings: { api_key: 'SDFSDGSVE' }, settings: { api_key: 'SDFSDGSVE' },
}; };
integrationAPI.createHook(hookData); integrationAPI.createHook(hookData);
expect(context.axiosMock.post).toHaveBeenCalledWith( expect(axiosMock.post).toHaveBeenCalledWith(
'/api/v1/integrations/hooks', '/api/v1/integrations/hooks',
hookData hookData
); );
@@ -67,7 +80,7 @@ describe('#integrationAPI', () => {
it('#deleteHook', () => { it('#deleteHook', () => {
integrationAPI.deleteHook(2); integrationAPI.deleteHook(2);
expect(context.axiosMock.delete).toHaveBeenCalledWith( expect(axiosMock.delete).toHaveBeenCalledWith(
'/api/v1/integrations/hooks/2' '/api/v1/integrations/hooks/2'
); );
}); });
@@ -1,6 +1,5 @@
import DyteAPIClient from '../../integrations/dyte'; import DyteAPIClient from '../../integrations/dyte';
import ApiClient from '../../ApiClient'; import ApiClient from '../../ApiClient';
import describeWithAPIMock from '../apiSpecHelper';
describe('#accountAPI', () => { describe('#accountAPI', () => {
it('creates correct instance', () => { it('creates correct instance', () => {
@@ -9,10 +8,26 @@ describe('#accountAPI', () => {
expect(DyteAPIClient).toHaveProperty('addParticipantToMeeting'); 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', () => { it('creates a valid request', () => {
DyteAPIClient.createAMeeting(1); DyteAPIClient.createAMeeting(1);
expect(context.axiosMock.post).toHaveBeenCalledWith( expect(axiosMock.post).toHaveBeenCalledWith(
'/api/v1/integrations/dyte/create_a_meeting', '/api/v1/integrations/dyte/create_a_meeting',
{ {
conversation_id: 1, 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', () => { it('creates a valid request', () => {
DyteAPIClient.addParticipantToMeeting(1); DyteAPIClient.addParticipantToMeeting(1);
expect(context.axiosMock.post).toHaveBeenCalledWith( expect(axiosMock.post).toHaveBeenCalledWith(
'/api/v1/integrations/dyte/add_participant_to_meeting', '/api/v1/integrations/dyte/add_participant_to_meeting',
{ {
message_id: 1, message_id: 1,
@@ -1,6 +1,5 @@
import notificationsAPI from '../notifications'; import notificationsAPI from '../notifications';
import ApiClient from '../ApiClient'; import ApiClient from '../ApiClient';
import describeWithAPIMock from './apiSpecHelper';
describe('#NotificationAPI', () => { describe('#NotificationAPI', () => {
it('creates correct instance', () => { it('creates correct instance', () => {
@@ -11,31 +10,47 @@ describe('#NotificationAPI', () => {
expect(notificationsAPI).toHaveProperty('read'); expect(notificationsAPI).toHaveProperty('read');
expect(notificationsAPI).toHaveProperty('readAll'); 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', () => { it('#get', () => {
notificationsAPI.get(1); notificationsAPI.get(1);
expect(context.axiosMock.get).toHaveBeenCalledWith( expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v1/notifications?page=1' '/api/v1/notifications?page=1'
); );
}); });
it('#getNotifications', () => { it('#getNotifications', () => {
notificationsAPI.getNotifications(1); notificationsAPI.getNotifications(1);
expect(context.axiosMock.get).toHaveBeenCalledWith( expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v1/notifications/1/notifications' '/api/v1/notifications/1/notifications'
); );
}); });
it('#getUnreadCount', () => { it('#getUnreadCount', () => {
notificationsAPI.getUnreadCount(); notificationsAPI.getUnreadCount();
expect(context.axiosMock.get).toHaveBeenCalledWith( expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v1/notifications/unread_count' '/api/v1/notifications/unread_count'
); );
}); });
it('#read', () => { it('#read', () => {
notificationsAPI.read(48670, 'Conversation'); notificationsAPI.read(48670, 'Conversation');
expect(context.axiosMock.post).toHaveBeenCalledWith( expect(axiosMock.post).toHaveBeenCalledWith(
'/api/v1/notifications/read_all', '/api/v1/notifications/read_all',
{ {
primary_actor_id: 'Conversation', primary_actor_id: 'Conversation',
@@ -46,7 +61,7 @@ describe('#NotificationAPI', () => {
it('#readAll', () => { it('#readAll', () => {
notificationsAPI.readAll(); notificationsAPI.readAll();
expect(context.axiosMock.post).toHaveBeenCalledWith( expect(axiosMock.post).toHaveBeenCalledWith(
'/api/v1/notifications/read_all' '/api/v1/notifications/read_all'
); );
}); });
@@ -1,6 +1,5 @@
import reportsAPI from '../reports'; import reportsAPI from '../reports';
import ApiClient from '../ApiClient'; import ApiClient from '../ApiClient';
import describeWithAPIMock from './apiSpecHelper';
describe('#Reports API', () => { describe('#Reports API', () => {
it('creates correct instance', () => { it('creates correct instance', () => {
@@ -18,14 +17,30 @@ describe('#Reports API', () => {
expect(reportsAPI).toHaveProperty('getInboxReports'); expect(reportsAPI).toHaveProperty('getInboxReports');
expect(reportsAPI).toHaveProperty('getTeamReports'); 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', () => { it('#getAccountReports', () => {
reportsAPI.getReports({ reportsAPI.getReports({
metric: 'conversations_count', metric: 'conversations_count',
from: 1621103400, from: 1621103400,
to: 1621621800, to: 1621621800,
}); });
expect(context.axiosMock.get).toHaveBeenCalledWith('/api/v2/reports', { expect(axiosMock.get).toHaveBeenCalledWith('/api/v2/reports', {
params: { params: {
metric: 'conversations_count', metric: 'conversations_count',
since: 1621103400, since: 1621103400,
@@ -38,20 +53,17 @@ describe('#Reports API', () => {
it('#getAccountSummary', () => { it('#getAccountSummary', () => {
reportsAPI.getSummary(1621103400, 1621621800); reportsAPI.getSummary(1621103400, 1621621800);
expect(context.axiosMock.get).toHaveBeenCalledWith( expect(axiosMock.get).toHaveBeenCalledWith('/api/v2/reports/summary', {
'/api/v2/reports/summary', params: {
{ business_hours: undefined,
params: { group_by: undefined,
business_hours: undefined, id: undefined,
group_by: undefined, since: 1621103400,
id: undefined, timezone_offset: -0,
since: 1621103400, type: 'account',
timezone_offset: -0, until: 1621621800,
type: 'account', },
until: 1621621800, });
},
}
);
}); });
it('#getAgentReports', () => { it('#getAgentReports', () => {
@@ -60,60 +72,48 @@ describe('#Reports API', () => {
to: 1621621800, to: 1621621800,
businessHours: true, businessHours: true,
}); });
expect(context.axiosMock.get).toHaveBeenCalledWith( expect(axiosMock.get).toHaveBeenCalledWith('/api/v2/reports/agents', {
'/api/v2/reports/agents', params: {
{ since: 1621103400,
params: { until: 1621621800,
since: 1621103400, business_hours: true,
until: 1621621800, },
business_hours: true, });
},
}
);
}); });
it('#getLabelReports', () => { it('#getLabelReports', () => {
reportsAPI.getLabelReports({ from: 1621103400, to: 1621621800 }); reportsAPI.getLabelReports({ from: 1621103400, to: 1621621800 });
expect(context.axiosMock.get).toHaveBeenCalledWith( expect(axiosMock.get).toHaveBeenCalledWith('/api/v2/reports/labels', {
'/api/v2/reports/labels', params: {
{ since: 1621103400,
params: { until: 1621621800,
since: 1621103400, },
until: 1621621800, });
},
}
);
}); });
it('#getInboxReports', () => { it('#getInboxReports', () => {
reportsAPI.getInboxReports({ from: 1621103400, to: 1621621800 }); reportsAPI.getInboxReports({ from: 1621103400, to: 1621621800 });
expect(context.axiosMock.get).toHaveBeenCalledWith( expect(axiosMock.get).toHaveBeenCalledWith('/api/v2/reports/inboxes', {
'/api/v2/reports/inboxes', params: {
{ since: 1621103400,
params: { until: 1621621800,
since: 1621103400, },
until: 1621621800, });
},
}
);
}); });
it('#getTeamReports', () => { it('#getTeamReports', () => {
reportsAPI.getTeamReports({ from: 1621103400, to: 1621621800 }); reportsAPI.getTeamReports({ from: 1621103400, to: 1621621800 });
expect(context.axiosMock.get).toHaveBeenCalledWith( expect(axiosMock.get).toHaveBeenCalledWith('/api/v2/reports/teams', {
'/api/v2/reports/teams', params: {
{ since: 1621103400,
params: { until: 1621621800,
since: 1621103400, },
until: 1621621800, });
},
}
);
}); });
it('#getConversationMetric', () => { it('#getConversationMetric', () => {
reportsAPI.getConversationMetric('account'); reportsAPI.getConversationMetric('account');
expect(context.axiosMock.get).toHaveBeenCalledWith( expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v2/reports/conversations', '/api/v2/reports/conversations',
{ {
params: { params: {
@@ -1,6 +1,5 @@
import teamsAPI from '../teams'; import teamsAPI from '../teams';
import ApiClient from '../ApiClient'; import ApiClient from '../ApiClient';
import describeWithAPIMock from './apiSpecHelper';
describe('#TeamsAPI', () => { describe('#TeamsAPI', () => {
it('creates correct instance', () => { it('creates correct instance', () => {
@@ -14,17 +13,33 @@ describe('#TeamsAPI', () => {
expect(teamsAPI).toHaveProperty('addAgents'); expect(teamsAPI).toHaveProperty('addAgents');
expect(teamsAPI).toHaveProperty('updateAgents'); 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', () => { it('#getAgents', () => {
teamsAPI.getAgents({ teamId: 1 }); teamsAPI.getAgents({ teamId: 1 });
expect(context.axiosMock.get).toHaveBeenCalledWith( expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v1/teams/1/team_members' '/api/v1/teams/1/team_members'
); );
}); });
it('#addAgents', () => { it('#addAgents', () => {
teamsAPI.addAgents({ teamId: 1, agentsList: { user_ids: [1, 10, 21] } }); 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', '/api/v1/teams/1/team_members',
{ {
user_ids: { user_ids: [1, 10, 21] }, user_ids: { user_ids: [1, 10, 21] },
@@ -38,7 +53,7 @@ describe('#TeamsAPI', () => {
teamId: 1, teamId: 1,
agentsList, agentsList,
}); });
expect(context.axiosMock.patch).toHaveBeenCalledWith( expect(axiosMock.patch).toHaveBeenCalledWith(
'/api/v1/teams/1/team_members', '/api/v1/teams/1/team_members',
{ {
user_ids: agentsList, user_ids: agentsList,
@@ -81,11 +81,6 @@ $conv-header-height: 2.5rem;
$inbox-thumb-size: 3rem; $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 // Snackbar default
$woot-snackbar-bg: #323232; $woot-snackbar-bg: #323232;
$woot-snackbar-button: #ffeb3b; $woot-snackbar-button: #ffeb3b;
@@ -149,15 +149,11 @@
} }
.multiselect-wrap--small { .multiselect-wrap--small {
.multiselect__tags,
.multiselect__input {
@apply items-center flex;
}
.multiselect__tags, .multiselect__tags,
.multiselect__input, .multiselect__input,
.multiselect { .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 { .multiselect__input {
@@ -165,11 +161,15 @@
} }
.multiselect__single { .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 { .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 { .multiselect__select {
@@ -181,9 +181,6 @@
@apply bg-transparent; @apply bg-transparent;
} }
.multiselect__tags-wrap {
@apply flex-shrink-0;
}
} }
.multiselect-wrap--medium { .multiselect-wrap--medium {
@@ -9,7 +9,6 @@
@import 'shared/assets/stylesheets/border-radius'; @import 'shared/assets/stylesheets/border-radius';
@import 'variables'; @import 'variables';
@import '~spinkit/scss/spinners/7-three-bounce';
@import 'vue-multiselect/dist/vue-multiselect.min.css'; @import 'vue-multiselect/dist/vue-multiselect.min.css';
@import '~shared/assets/stylesheets/ionicons'; @import '~shared/assets/stylesheets/ionicons';
@@ -2,10 +2,6 @@
transition: box-shadow 0.35s $swift-ease-out-function, transition: box-shadow 0.35s $swift-ease-out-function,
height 2s $swift-ease-out-function; height 2s $swift-ease-out-function;
&.is-focused {
box-shadow: var(--shadow);
}
.reply-box__top { .reply-box__top {
.icon { .icon {
color: $medium-gray; color: $medium-gray;
@@ -47,8 +43,6 @@
} }
&.is-private { &.is-private {
@apply bg-yellow-100 dark:bg-yellow-800;
.reply-box__top { .reply-box__top {
@apply bg-yellow-100 dark:bg-yellow-800; @apply bg-yellow-100 dark:bg-yellow-800;
@@ -31,7 +31,7 @@
</template> </template>
<script> <script>
import EmojiOrIcon from 'shared/components/EmojiOrIcon'; import EmojiOrIcon from 'shared/components/EmojiOrIcon.vue';
export default { export default {
components: { components: {
@@ -10,7 +10,8 @@
<div <div
class="flex items-center justify-between py-0 px-4" class="flex items-center justify-between py-0 px-4"
:class="{ :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"> <div class="flex max-w-[85%] justify-center items-center">
@@ -24,9 +25,7 @@
v-if="!hasAppliedFiltersOrActiveFolders" 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" 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"
> >
{{ {{ $t(`CHAT_LIST.CHAT_STATUS_FILTER_ITEMS.${activeStatus}.TEXT`) }}
this.$t(`CHAT_LIST.CHAT_STATUS_FILTER_ITEMS.${activeStatus}.TEXT`)
}}
</span> </span>
</div> </div>
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
@@ -193,17 +192,17 @@
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import ConversationAdvancedFilter from './widgets/conversation/ConversationAdvancedFilter'; import ConversationAdvancedFilter from './widgets/conversation/ConversationAdvancedFilter.vue';
import ConversationBasicFilter from './widgets/conversation/ConversationBasicFilter'; import ConversationBasicFilter from './widgets/conversation/ConversationBasicFilter.vue';
import ChatTypeTabs from './widgets/ChatTypeTabs'; import ChatTypeTabs from './widgets/ChatTypeTabs.vue';
import ConversationCard from './widgets/conversation/ConversationCard'; import ConversationCard from './widgets/conversation/ConversationCard.vue';
import timeMixin from '../mixins/time'; import timeMixin from '../mixins/time';
import eventListenerMixins from 'shared/mixins/eventListenerMixins'; import eventListenerMixins from 'shared/mixins/eventListenerMixins';
import conversationMixin from '../mixins/conversations'; import conversationMixin from '../mixins/conversations';
import wootConstants from 'dashboard/constants/globals'; import wootConstants from 'dashboard/constants/globals';
import advancedFilterTypes from './widgets/conversation/advancedFilterItems'; import advancedFilterTypes from './widgets/conversation/advancedFilterItems';
import filterQueryGenerator from '../helper/filterQueryGenerator.js'; import filterQueryGenerator from '../helper/filterQueryGenerator.js';
import AddCustomViews from 'dashboard/routes/dashboard/customviews/AddCustomViews'; import AddCustomViews from 'dashboard/routes/dashboard/customviews/AddCustomViews.vue';
import DeleteCustomViews from 'dashboard/routes/dashboard/customviews/DeleteCustomViews.vue'; import DeleteCustomViews from 'dashboard/routes/dashboard/customviews/DeleteCustomViews.vue';
import ConversationBulkActions from './widgets/conversation/conversationBulkActions/Index.vue'; import ConversationBulkActions from './widgets/conversation/conversationBulkActions/Index.vue';
import alertMixin from 'shared/mixins/alertMixin'; import alertMixin from 'shared/mixins/alertMixin';
@@ -642,10 +641,8 @@ export default {
}, },
handleKeyEvents(e) { handleKeyEvents(e) {
if (hasPressedAltAndJKey(e)) { if (hasPressedAltAndJKey(e)) {
const { const { allConversations, activeConversationIndex } =
allConversations, this.getKeyboardListenerParams();
activeConversationIndex,
} = this.getKeyboardListenerParams();
if (activeConversationIndex === -1) { if (activeConversationIndex === -1) {
allConversations[0].click(); allConversations[0].click();
} }
@@ -61,7 +61,7 @@
> >
<a <a
v-if="isAttributeTypeLink" v-if="isAttributeTypeLink"
:href="value" :href="hrefURL"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
class="value inline-block rounded-sm mb-0 break-all py-0.5 px-1" class="value inline-block rounded-sm mb-0 break-all py-0.5 px-1"
@@ -188,6 +188,9 @@ export default {
urlValue() { urlValue() {
return isValidURL(this.value) ? this.value : '---'; return isValidURL(this.value) ? this.value : '---';
}, },
hrefURL() {
return isValidURL(this.value) ? this.value : '';
},
notAttributeTypeCheckboxAndList() { notAttributeTypeCheckboxAndList() {
return !this.isAttributeTypeCheckbox && !this.isAttributeTypeList; return !this.isAttributeTypeCheckbox && !this.isAttributeTypeList;
}, },
@@ -13,10 +13,10 @@
/> />
<div class="flex flex-row justify-end gap-2 py-2 px-0 w-full"> <div class="flex flex-row justify-end gap-2 py-2 px-0 w-full">
<woot-button variant="clear" @click.prevent="onClose"> <woot-button variant="clear" @click.prevent="onClose">
{{ this.$t('CONVERSATION.CUSTOM_SNOOZE.CANCEL') }} {{ $t('CONVERSATION.CUSTOM_SNOOZE.CANCEL') }}
</woot-button> </woot-button>
<woot-button> <woot-button>
{{ this.$t('CONVERSATION.CUSTOM_SNOOZE.APPLY') }} {{ $t('CONVERSATION.CUSTOM_SNOOZE.APPLY') }}
</woot-button> </woot-button>
</div> </div>
</form> </form>
@@ -1,6 +1,6 @@
<template> <template>
<div class="text--container"> <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') }} {{ $t('COMPONENTS.CODE.BUTTON_TEXT') }}
</woot-button> </woot-button>
<woot-button <woot-button
@@ -66,9 +66,9 @@ export default {
'right-aligned': 'right-aligned', 'right-aligned': 'right-aligned',
}; };
return `modal-mask skip-context-menu ${modalClassNameMap[ return `modal-mask skip-context-menu ${
this.modalType modalClassNameMap[this.modalType] || ''
] || ''}`; }`;
}, },
}, },
mounted() { mounted() {
@@ -2,7 +2,8 @@
<div <div
class="ml-0 mr-0 flex pt-0 pr-4 pb-4 pl-0" class="ml-0 mr-0 flex pt-0 pr-4 pb-4 pl-0"
:class="{ :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"> <div class="w-[30%] min-w-0 max-w-[30%] pr-12">
@@ -10,7 +10,7 @@
</template> </template>
<script> <script>
import WootSnackbar from './Snackbar'; import WootSnackbar from './Snackbar.vue';
export default { export default {
components: { components: {
@@ -72,10 +72,8 @@ export default {
const { custom_attributes: subscription } = account; const { custom_attributes: subscription } = account;
if (!subscription) return EMPTY_SUBSCRIPTION_INFO; if (!subscription) return EMPTY_SUBSCRIPTION_INFO;
const { const { subscription_status: status, subscription_ends_on: endsOn } =
subscription_status: status, subscription;
subscription_ends_on: endsOn,
} = subscription;
return { status, endsOn: new Date(endsOn) }; return { status, endsOn: new Date(endsOn) };
}, },
@@ -1,4 +1,4 @@
const semver = require('semver'); import semver from 'semver';
export const hasAnUpdateAvailable = (latestVersion, currentVersion) => { export const hasAnUpdateAvailable = (latestVersion, currentVersion) => {
if (!semver.valid(latestVersion)) { if (!semver.valid(latestVersion)) {
@@ -13,7 +13,7 @@
</template> </template>
<script> <script>
import Spinner from 'shared/components/Spinner'; import Spinner from 'shared/components/Spinner.vue';
export default { export default {
components: { components: {
@@ -10,7 +10,7 @@
:is-loading="isLoading" :is-loading="isLoading"
@click="onCmdResolveConversation" @click="onCmdResolveConversation"
> >
{{ this.$t('CONVERSATION.HEADER.RESOLVE_ACTION') }} {{ $t('CONVERSATION.HEADER.RESOLVE_ACTION') }}
</woot-button> </woot-button>
<woot-button <woot-button
v-else-if="isResolved" v-else-if="isResolved"
@@ -21,7 +21,7 @@
:is-loading="isLoading" :is-loading="isLoading"
@click="onCmdOpenConversation" @click="onCmdOpenConversation"
> >
{{ this.$t('CONVERSATION.HEADER.REOPEN_ACTION') }} {{ $t('CONVERSATION.HEADER.REOPEN_ACTION') }}
</woot-button> </woot-button>
<woot-button <woot-button
v-else-if="showOpenButton" v-else-if="showOpenButton"
@@ -31,7 +31,7 @@
:is-loading="isLoading" :is-loading="isLoading"
@click="onCmdOpenConversation" @click="onCmdOpenConversation"
> >
{{ this.$t('CONVERSATION.HEADER.OPEN_ACTION') }} {{ $t('CONVERSATION.HEADER.OPEN_ACTION') }}
</woot-button> </woot-button>
<woot-button <woot-button
v-if="showAdditionalActions" v-if="showAdditionalActions"
@@ -57,7 +57,7 @@
icon="snooze" icon="snooze"
@click="() => openSnoozeModal()" @click="() => openSnoozeModal()"
> >
{{ this.$t('CONVERSATION.RESOLVE_DROPDOWN.SNOOZE_UNTIL') }} {{ $t('CONVERSATION.RESOLVE_DROPDOWN.SNOOZE_UNTIL') }}
</woot-button> </woot-button>
</woot-dropdown-item> </woot-dropdown-item>
<woot-dropdown-item v-if="!isPending"> <woot-dropdown-item v-if="!isPending">
@@ -68,7 +68,7 @@
icon="book-clock" icon="book-clock"
@click="() => toggleStatus(STATUS_TYPE.PENDING)" @click="() => toggleStatus(STATUS_TYPE.PENDING)"
> >
{{ this.$t('CONVERSATION.RESOLVE_DROPDOWN.MARK_PENDING') }} {{ $t('CONVERSATION.RESOLVE_DROPDOWN.MARK_PENDING') }}
</woot-button> </woot-button>
</woot-dropdown-item> </woot-dropdown-item>
</woot-dropdown-menu> </woot-dropdown-menu>
@@ -90,7 +90,7 @@ import { getUnixTime } from 'date-fns';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { mixin as clickaway } from 'vue-clickaway'; import { mixin as clickaway } from 'vue-clickaway';
import alertMixin from 'shared/mixins/alertMixin'; 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 eventListenerMixins from 'shared/mixins/eventListenerMixins';
import { import {
hasPressedAltAndEKey, hasPressedAltAndEKey,
@@ -49,11 +49,11 @@
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { mixin as clickaway } from 'vue-clickaway'; import { mixin as clickaway } from 'vue-clickaway';
import alertMixin from 'shared/mixins/alertMixin'; import alertMixin from 'shared/mixins/alertMixin';
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem'; import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu'; import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
import WootDropdownHeader from 'shared/components/ui/dropdown/DropdownHeader'; import WootDropdownHeader from 'shared/components/ui/dropdown/DropdownHeader.vue';
import WootDropdownDivider from 'shared/components/ui/dropdown/DropdownDivider'; import WootDropdownDivider from 'shared/components/ui/dropdown/DropdownDivider.vue';
import AvailabilityStatusBadge from '../widgets/conversation/AvailabilityStatusBadge'; import AvailabilityStatusBadge from '../widgets/conversation/AvailabilityStatusBadge.vue';
import wootConstants from 'dashboard/constants/globals'; import wootConstants from 'dashboard/constants/globals';
const { AVAILABILITY_STATUS_KEYS } = wootConstants; const { AVAILABILITY_STATUS_KEYS } = wootConstants;
@@ -35,8 +35,8 @@ import adminMixin from '../../mixins/isAdmin';
import { getSidebarItems } from './config/default-sidebar'; import { getSidebarItems } from './config/default-sidebar';
import alertMixin from 'shared/mixins/alertMixin'; import alertMixin from 'shared/mixins/alertMixin';
import PrimarySidebar from './sidebarComponents/Primary'; import PrimarySidebar from './sidebarComponents/Primary.vue';
import SecondarySidebar from './sidebarComponents/Secondary'; import SecondarySidebar from './sidebarComponents/Secondary.vue';
import { import {
hasPressedAltAndCKey, hasPressedAltAndCKey,
hasPressedAltAndRKey, hasPressedAltAndRKey,
@@ -34,8 +34,8 @@
<woot-submit-button <woot-submit-button
:disabled=" :disabled="
$v.accountName.$invalid || $v.accountName.$invalid ||
$v.accountName.$invalid || $v.accountName.$invalid ||
uiFlags.isCreating uiFlags.isCreating
" "
:button-text="$t('CREATE_ACCOUNT.FORM.SUBMIT')" :button-text="$t('CREATE_ACCOUNT.FORM.SUBMIT')"
:loading="uiFlags.isCreating" :loading="uiFlags.isCreating"
@@ -16,7 +16,7 @@
</template> </template>
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import Thumbnail from '../../widgets/Thumbnail'; import Thumbnail from '../../widgets/Thumbnail.vue';
export default { export default {
components: { components: {
@@ -3,7 +3,8 @@
<button <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="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="{ :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" @click="openNotificationPanel"
> >
@@ -109,9 +109,9 @@
import { mixin as clickaway } from 'vue-clickaway'; import { mixin as clickaway } from 'vue-clickaway';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import Auth from '../../../api/auth'; import Auth from '../../../api/auth';
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem'; import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu'; import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
import AvailabilityStatus from 'dashboard/components/layout/AvailabilityStatus'; import AvailabilityStatus from 'dashboard/components/layout/AvailabilityStatus.vue';
export default { export default {
components: { components: {
@@ -39,11 +39,11 @@
</div> </div>
</template> </template>
<script> <script>
import Logo from './Logo'; import Logo from './Logo.vue';
import PrimaryNavItem from './PrimaryNavItem'; import PrimaryNavItem from './PrimaryNavItem.vue';
import OptionsMenu from './OptionsMenu'; import OptionsMenu from './OptionsMenu.vue';
import AgentDetails from './AgentDetails'; import AgentDetails from './AgentDetails.vue';
import NotificationBell from './NotificationBell'; import NotificationBell from './NotificationBell.vue';
import wootConstants from 'dashboard/constants/globals'; import wootConstants from 'dashboard/constants/globals';
import { frontendURL } from 'dashboard/helper/URLHelper'; import { frontendURL } from 'dashboard/helper/URLHelper';
import { ACCOUNT_EVENTS } from '../../../helper/AnalyticsHelper/events'; 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="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="{ :class="{
'bg-woot-25 dark:bg-slate-800': isActive, '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" @click="navigate"
> >
@@ -44,7 +45,8 @@
class="text-sm text-slate-700 dark:text-slate-100" class="text-sm text-slate-700 dark:text-slate-100"
:class="{ :class="{
'text-woot-500 dark:text-woot-500': isActive, '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 }} {{ label }}
@@ -35,7 +35,8 @@
:class="{ :class="{
'text-slate-300 dark:text-slate-600': isCountZero && !isActiveView, 'text-slate-300 dark:text-slate-600': isCountZero && !isActiveView,
'text-slate-600 dark:text-slate-50': !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, 'bg-slate-50 dark:bg-slate-700': !isActiveView,
}" }"
> >
@@ -97,7 +98,7 @@ import {
getInboxWarningIconClass, getInboxWarningIconClass,
} from 'dashboard/helper/inbox'; } from 'dashboard/helper/inbox';
import SecondaryChildNavItem from './SecondaryChildNavItem'; import SecondaryChildNavItem from './SecondaryChildNavItem.vue';
import { import {
isOnMentionsView, isOnMentionsView,
isOnUnattendedView, isOnUnattendedView,
@@ -53,16 +53,24 @@ export default {
const createdTimeDiff = Date.now() - this.createdAtTimestamp * 1000; const createdTimeDiff = Date.now() - this.createdAtTimestamp * 1000;
const isBeforeAMonth = createdTimeDiff > DAY_IN_MILLI_SECONDS * 30; const isBeforeAMonth = createdTimeDiff > DAY_IN_MILLI_SECONDS * 30;
return !isBeforeAMonth return !isBeforeAMonth
? `Created ${this.createdAtTimeAgo}` ? `${this.$t('CHAT_LIST.CHAT_TIME_STAMP.CREATED.LATEST')} ${
: `Created at: ${this.dateFormat(this.createdAtTimestamp)}`; this.createdAtTimeAgo
}`
: `${this.$t(
'CHAT_LIST.CHAT_TIME_STAMP.CREATED.OLDEST'
)} ${this.dateFormat(this.createdAtTimestamp)}`;
}, },
lastActivity() { lastActivity() {
const lastActivityTimeDiff = const lastActivityTimeDiff =
Date.now() - this.lastActivityTimestamp * 1000; Date.now() - this.lastActivityTimestamp * 1000;
const isNotActive = lastActivityTimeDiff > DAY_IN_MILLI_SECONDS * 30; const isNotActive = lastActivityTimeDiff > DAY_IN_MILLI_SECONDS * 30;
return !isNotActive return !isNotActive
? `Last activity ${this.lastActivityAtTimeAgo}` ? `${this.$t('CHAT_LIST.CHAT_TIME_STAMP.LAST_ACTIVITY.ACTIVE')} ${
: `Last activity: ${this.dateFormat(this.lastActivityTimestamp)}`; this.lastActivityAtTimeAgo
}`
: `${this.$t(
'CHAT_LIST.CHAT_TIME_STAMP.LAST_ACTIVITY.NOT_ACTIVE'
)} ${this.dateFormat(this.lastActivityTimestamp)}`;
}, },
tooltipText() { tooltipText() {
return `${this.createdAt} return `${this.createdAt}
@@ -28,8 +28,8 @@
</button> </button>
</template> </template>
<script> <script>
import Spinner from 'shared/components/Spinner'; import Spinner from 'shared/components/Spinner.vue';
import EmojiOrIcon from 'shared/components/EmojiOrIcon'; import EmojiOrIcon from 'shared/components/EmojiOrIcon.vue';
export default { export default {
name: 'WootButton', name: 'WootButton',
@@ -1,6 +1,6 @@
<template> <template>
<div class="animation-container margin-top-1"> <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" /> <fluent-icon icon="wand" size="14" class="ai-typing--icon" />
<label> <label>
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.ASSISTANCE_MODAL.AI_WRITING') }} {{ $t('INTEGRATION_SETTINGS.OPEN_AI.ASSISTANCE_MODAL.AI_WRITING') }}
@@ -108,7 +108,7 @@
<script> <script>
import AutomationActionTeamMessageInput from './AutomationActionTeamMessageInput.vue'; import AutomationActionTeamMessageInput from './AutomationActionTeamMessageInput.vue';
import AutomationActionFileInput from './AutomationFileInput.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 { export default {
components: { components: {
AutomationActionTeamMessageInput, AutomationActionTeamMessageInput,
@@ -26,7 +26,7 @@
</template> </template>
<script> <script>
import Spinner from 'shared/components/Spinner'; import Spinner from 'shared/components/Spinner.vue';
import alertMixin from 'shared/mixins/alertMixin'; import alertMixin from 'shared/mixins/alertMixin';
export default { export default {
components: { components: {
@@ -61,11 +61,9 @@ export default {
this.label = this.$t('AUTOMATION.ATTACHMENT.LABEL_UPLOADING'); this.label = this.$t('AUTOMATION.ATTACHMENT.LABEL_UPLOADING');
try { try {
const file = event.target.files[0]; const file = event.target.files[0];
const formData = new FormData();
formData.append('attachment', file, file.name);
const id = await this.$store.dispatch( const id = await this.$store.dispatch(
'automations/uploadAttachment', 'automations/uploadAttachment',
formData file
); );
this.$emit('input', [id]); this.$emit('input', [id]);
this.uploadState = 'uploaded'; this.uploadState = 'uploaded';
@@ -7,7 +7,7 @@
/> />
</template> </template>
<script> <script>
import ChannelSelector from '../ChannelSelector'; import ChannelSelector from '../ChannelSelector.vue';
export default { export default {
components: { ChannelSelector }, components: { ChannelSelector },
props: { props: {
@@ -21,7 +21,7 @@
</template> </template>
<script> <script>
import LoadingState from 'dashboard/components/widgets/LoadingState'; import LoadingState from 'dashboard/components/widgets/LoadingState.vue';
export default { export default {
components: { components: {
LoadingState, LoadingState,
@@ -1,3 +1,4 @@
<!-- eslint-disable vue/no-mutating-props -->
<template> <template>
<div> <div>
<div <div
@@ -255,7 +256,9 @@ export default {
value === 'conversation_attribute' || value === 'conversation_attribute' ||
value === 'contact_attribute' value === 'contact_attribute'
) { ) {
// eslint-disable-next-line vue/no-mutating-props
this.value.custom_attribute_type = this.customAttributeType; this.value.custom_attribute_type = this.customAttributeType;
// eslint-disable-next-line vue/no-mutating-props
} else this.value.custom_attribute_type = ''; } else this.value.custom_attribute_type = '';
}, },
immediate: true, immediate: true,
@@ -30,9 +30,9 @@
</template> </template>
<script> <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 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 { mixin as clickaway } from 'vue-clickaway';
import adminMixin from 'dashboard/mixins/isAdmin'; import adminMixin from 'dashboard/mixins/isAdmin';
import { import {

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