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 JoseandGitHub 6ab964b161 chore: fix flaky spec (#7686)
- fix flaky reporting specs
- fix flaky export spec
2023-08-25 00:11:41 -07:00
Shivam MishraandGitHub 5598b4b27e feat: implement mutex for SlackSendJob (#7783) 2023-08-25 11:58:29 +07:00
Sojan JoseandGitHub 64ae9f625a chore: Persist emojis in Contact Import (#7803)
The previous fix would remove emojis in contact data. This change ensures they are persisited

ref: #7787
2023-08-24 14:04:52 -07:00
Pranav Raj SandGitHub 3dd3b7b3aa fix: Rescue InvalidAuth and mark integration as inactive (#7797) 2023-08-24 17:02:24 +05:30
Pranav Raj SandGitHub f451e59aec chore: Update translations (#7796) 2023-08-24 17:02:11 +05:30
paulocrandGitHub c04f67e8a5 fix: Update translation in Spanish (#7770) 2023-08-24 16:24:48 +05:30
Pranav Raj SandGitHub 6ddc99d066 fix: Update the in_reply_to to logic to use processed_mail (#7793) 2023-08-24 16:19:17 +05:30
Sojan JoseandGitHub 4cbb3208e8 chore: Revert gem update (#7792)
- updating redis client caused deployment issues in Heroku, hence reverting to prev version until its resolved
2023-08-24 01:08:34 -07:00
Pranav Raj SandGitHub 5f2a720920 chore: Update translations (#7766) 2023-08-24 13:26:39 +05:30
Sojan JoseandGitHub acb7debd3f chore: Contact import improvements (#7787)
- Ensure existing contact information is updated on data import
- Refactor the existing job to make it more readable
- Fixes issues with import files in the wrong encoding
fixes: #7307
2023-08-23 23:24:47 -07:00
Shivam MishraandGitHub 44f6a9ec3c feat: update lock timeout and retry configuration (#7785) 2023-08-24 12:59:47 +07:00
Shivam MishraandGitHub 24468d71fb feat: update rails (#7786)
Bump up rails
2023-08-23 13:45:55 -07:00
Shivam MishraandGitHub 26ef21a243 feat: locking and retry in FB message parsing (#7701) 2023-08-23 09:48:17 +07:00
Sivin VargheseandGitHub 18235d3fb5 fix: Draft, Articles, Archived are counted wrong in help-center (#7761) 2023-08-21 12:42:10 +05:30
Sivin VargheseandGitHub 55972cc17a fix: Dashboard overflow issue if there is banner (#7748) 2023-08-21 12:22:50 +05:30
2daf1ae827 feat: Event to capture the message signature feature (#7760)
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2023-08-21 13:30:21 +07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Shivam Mishra
22421b5ac8 chore(deps): bump puma from 6.2.2 to 6.3.1 (#7765)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
2023-08-21 13:09:20 +07:00
Muhsin KelothandGitHub 0d41853b0b fix: OpenAI CE specs (#7710) 2023-08-21 12:54:35 +07:00
LiamandGitHub 355926897a fix: Fixes error while updating widget builder title (#7756) 2023-08-19 10:57:44 +05:30
Pranav Raj SandGitHub 4101a7b566 chore: Update translations from Crowdin (#7750) 2023-08-18 12:48:50 -07:00
2d31535620 fix: Improve the contrast for the code snippet on the Widget Builder page. (#7758)
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2023-08-18 12:44:30 -07:00
LiamandGitHub c54b469dcb fix: Update the style of the 'Go To' input in the conversations table (#7753) 2023-08-17 13:50:11 -07:00
Pranav Raj SandGitHub 29e8e41443 fix: Delete agent bots without deleting the messages (#7754) 2023-08-17 13:48:49 -07:00
178bc80b25 fix: Update email alerts for Slack integration (#7739)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2023-08-17 11:46:37 -07:00
Sivin VargheseandGitHub ab039e14fd chore: Improve article list pagination logic (#7737) 2023-08-17 09:32:29 -07:00
LiamandGitHub 961c911c26 fix: Fix the issue where tag agent option is appearing above the profile menu (#7743) 2023-08-17 09:23:26 -07:00
Muhsin KelothandGitHub 12ae49089b feat: Add analytics events for notification feature (#7749) 2023-08-17 09:18:31 -07:00
Jordan BroughandGitHub 996f842882 feat: Allow Twilio::REST::TwilioError errors to raise and prevent Inbox creation (#7379)
This update will mean that errors will roll back the current transaction and the error will be sent back to the frontend and the user will know that the Inbox did not finish setting up successfully.
2023-08-16 15:00:02 -07:00
Sivin VargheseandGitHub 616f702871 fix: Agent name not visible in team page in Dark mode (#7730)
Fixes: https://linear.app/chatwoot/issue/CW-2399/agent-name-not-visible-in-team-page
2023-08-16 14:39:14 -07:00
Pranav Raj SandGitHub 8015490aba chore: Update translations (#7731) 2023-08-16 11:18:14 -07:00
1026 changed files with 18343 additions and 6196 deletions
+7 -8
View File
@@ -15,9 +15,9 @@ defaults: &defaults
- image: cimg/postgres:15.3
- image: cimg/redis:6.2.6
environment:
- RAILS_LOG_TO_STDOUT: false
- COVERAGE: true
- LOG_LEVEL: warn
- RAILS_LOG_TO_STDOUT: false
- COVERAGE: true
- LOG_LEVEL: warn
parallelism: 4
resource_class: large
@@ -46,7 +46,7 @@ jobs:
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"
nvm install v16
nvm install v20
echo 'export NVM_DIR="$HOME/.nvm"' >> $BASH_ENV
echo '[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"' >> $BASH_ENV
@@ -64,7 +64,6 @@ jobs:
- ~/.bundle
key: chatwoot-bundle-{{ .Environment.CACHE_VERSION }}-v20220524-{{ checksum "Gemfile.lock" }}
# Only necessary if app uses webpacker or yarn in some other way
- restore_cache:
keys:
@@ -82,7 +81,7 @@ jobs:
- ~/.cache/yarn
- run:
name: Download cc-test-reporter
name: Download cc-test-reporter
command: |
mkdir -p ~/tmp
curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ~/tmp/cc-test-reporter
@@ -182,7 +181,7 @@ jobs:
- attach_workspace:
at: ~/build
- run:
name: Download cc-test-reporter
name: Download cc-test-reporter
command: |
mkdir -p ~/tmp
curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ~/tmp/cc-test-reporter
@@ -204,4 +203,4 @@ workflows:
- build
- upload-coverage:
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
## To prevent and throttle abusive requests
# ENABLE_RACK_ATTACK=true
# RACK_ATTACK_LIMIT=300
# ENABLE_RACK_ATTACK_WIDGET_API=true
## Running chatwoot as an API only server
## setting this value to true will disable the frontend dashboard endpoints
+7 -3
View File
@@ -6,7 +6,7 @@ module.exports = {
'plugin:storybook/recommended',
],
parserOptions: {
parser: 'babel-eslint',
parser: '@babel/eslint-parser',
ecmaVersion: 2020,
sourceType: 'module',
},
@@ -24,13 +24,16 @@ module.exports = {
'jsx-a11y/label-has-for': 'off',
'jsx-a11y/anchor-is-valid': 'off',
'import/no-unresolved': 'off',
'vue/html-indent': 'off',
'vue/multi-word-component-names': 'off',
'vue/max-attributes-per-line': [
'error',
{
singleline: 20,
singleline: {
max: 20,
},
multiline: {
max: 1,
allowFirstLine: false,
},
},
],
@@ -47,6 +50,7 @@ module.exports = {
},
],
'vue/no-v-html': 'off',
'vue/component-definition-name-casing': 'off',
'vue/singleline-html-element-content-newline': 'off',
'import/extensions': ['off'],
'no-console': 'error',
+4 -1
View File
@@ -52,7 +52,8 @@ jobs:
- uses: actions/setup-node@v3
with:
node-version: 16
node-version: 20
cache: yarn
- name: yarn
run: yarn install
@@ -75,6 +76,8 @@ jobs:
- name: Run backend tests
run: |
bundle exec rspec --profile=10 --format documentation
env:
NODE_OPTIONS: --openssl-legacy-provider
- name: Upload rails log folder
uses: actions/upload-artifact@v3
+8 -2
View File
@@ -51,7 +51,8 @@ jobs:
- uses: actions/setup-node@v3
with:
node-version: 16
node-version: 20
cache: yarn
- name: yarn
run: yarn install
@@ -68,7 +69,12 @@ jobs:
# Run Response Bot specs
- name: Run backend tests
run: |
bundle exec rspec spec/enterprise/controllers/api/v1/accounts/response_sources_controller_spec.rb spec/enterprise/controllers/enterprise/api/v1/accounts/inboxes_controller_spec.rb:47 --profile=10 --format documentation
bundle exec rspec \
spec/enterprise/controllers/api/v1/accounts/response_sources_controller_spec.rb \
spec/enterprise/services/enterprise/message_templates/response_bot_service_spec.rb \
spec/enterprise/controllers/enterprise/api/v1/accounts/inboxes_controller_spec.rb:47 \
--profile=10 \
--format documentation
- name: Upload rails log folder
uses: actions/upload-artifact@v3
+5 -2
View File
@@ -21,7 +21,8 @@ jobs:
- uses: actions/setup-node@v3
with:
node-version: 16
node-version: 20
cache: 'yarn'
- name: yarn
run: yarn install
@@ -30,9 +31,11 @@ jobs:
run: |
rm -rf enterprise
rm -rf spec/enterprise
- name: Run asset compile
run: bundle exec rake assets:precompile
env:
NODE_OPTIONS: --openssl-legacy-provider
- name: Size Check
run: yarn run size
+1 -1
View File
@@ -1 +1 @@
16.20.1
20.5.1
+5 -9
View File
@@ -8,15 +8,11 @@ Layout/LineLength:
Max: 150
Metrics/ClassLength:
Max: 125
Max: 175
Exclude:
- 'app/models/conversation.rb'
- 'app/models/contact.rb'
- 'app/mailers/conversation_reply_mailer.rb'
- 'app/models/message.rb'
- 'app/builders/messages/facebook/message_builder.rb'
- 'app/controllers/api/v1/accounts/contacts_controller.rb'
- 'app/listeners/action_cable_listener.rb'
- 'app/models/conversation.rb'
RSpec/ExampleLength:
Max: 25
Style/Documentation:
@@ -62,6 +58,7 @@ Metrics/BlockLength:
Metrics/ModuleLength:
Exclude:
- lib/seeders/message_seeder.rb
- spec/support/slack_stubs.rb
Rails/ApplicationController:
Exclude:
- 'app/controllers/api/v1/widget/messages_controller.rb'
@@ -73,7 +70,7 @@ Rails/ApplicationController:
- 'app/controllers/survey/responses_controller.rb'
Rails/FindEach:
Enabled: true
Include:
Include:
- 'app/**/*.rb'
Rails/CompactBlank:
Enabled: false
@@ -189,7 +186,6 @@ RSpec/IndexedLet:
RSpec/NamedSubject:
Enabled: false
# we should bring this down
RSpec/MultipleMemoizedHelpers:
Max: 14
+8 -7
View File
@@ -4,7 +4,7 @@ ruby '3.2.2'
##-- base gems for rails --##
gem 'rack-cors', require: 'rack/cors'
gem 'rails', '~> 7.0.5.1'
gem 'rails', '~> 7.0.8.0'
# Reduces boot times through caching; required in config/boot.rb
gem 'bootsnap', require: false
@@ -76,6 +76,7 @@ gem 'pundit'
# super admin
gem 'administrate', '>= 0.19.0'
gem 'administrate-field-active_storage'
gem 'administrate-field-belongs_to_search'
##--- gems for pubsub service ---##
# https://karolgalanciak.com/blog/2019/11/30/from-activerecord-callbacks-to-publish-slash-subscribe-pattern-and-event-driven-design/
@@ -108,14 +109,14 @@ gem 'elastic-apm', require: false
gem 'newrelic_rpm', require: false
gem 'newrelic-sidekiq-metrics', 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-sidekiq', '>= 5.10.0', require: false
gem 'sentry-sidekiq', '>= 5.11.0', require: false
##-- background job processing --##
gem 'sidekiq'
gem 'sidekiq', '>= 7.1.3'
# We want cron jobs
gem 'sidekiq-cron'
gem 'sidekiq-cron', '>= 1.10.1'
##-- Push notification service --##
gem 'fcm'
@@ -158,7 +159,7 @@ gem 'lograge', '~> 0.13.0', require: false
# worked with microsoft refresh token
gem 'omniauth-oauth2'
gem 'audited', '~> 5.3'
gem 'audited', '~> 5.4', '>= 5.4.0'
# need for google auth
gem 'omniauth'
@@ -188,7 +189,7 @@ group :development do
gem 'bullet'
gem 'letter_opener'
gem 'scss_lint', require: false
gem 'web-console'
gem 'web-console', '>= 4.2.1'
# used in swagger build
gem 'json_refs'
+90 -84
View File
@@ -33,70 +33,70 @@ GIT
GEM
remote: https://rubygems.org/
specs:
actioncable (7.0.5.1)
actionpack (= 7.0.5.1)
activesupport (= 7.0.5.1)
actioncable (7.0.8)
actionpack (= 7.0.8)
activesupport (= 7.0.8)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
actionmailbox (7.0.5.1)
actionpack (= 7.0.5.1)
activejob (= 7.0.5.1)
activerecord (= 7.0.5.1)
activestorage (= 7.0.5.1)
activesupport (= 7.0.5.1)
actionmailbox (7.0.8)
actionpack (= 7.0.8)
activejob (= 7.0.8)
activerecord (= 7.0.8)
activestorage (= 7.0.8)
activesupport (= 7.0.8)
mail (>= 2.7.1)
net-imap
net-pop
net-smtp
actionmailer (7.0.5.1)
actionpack (= 7.0.5.1)
actionview (= 7.0.5.1)
activejob (= 7.0.5.1)
activesupport (= 7.0.5.1)
actionmailer (7.0.8)
actionpack (= 7.0.8)
actionview (= 7.0.8)
activejob (= 7.0.8)
activesupport (= 7.0.8)
mail (~> 2.5, >= 2.5.4)
net-imap
net-pop
net-smtp
rails-dom-testing (~> 2.0)
actionpack (7.0.5.1)
actionview (= 7.0.5.1)
activesupport (= 7.0.5.1)
actionpack (7.0.8)
actionview (= 7.0.8)
activesupport (= 7.0.8)
rack (~> 2.0, >= 2.2.4)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
actiontext (7.0.5.1)
actionpack (= 7.0.5.1)
activerecord (= 7.0.5.1)
activestorage (= 7.0.5.1)
activesupport (= 7.0.5.1)
actiontext (7.0.8)
actionpack (= 7.0.8)
activerecord (= 7.0.8)
activestorage (= 7.0.8)
activesupport (= 7.0.8)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
actionview (7.0.5.1)
activesupport (= 7.0.5.1)
actionview (7.0.8)
activesupport (= 7.0.8)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.1, >= 1.2.0)
active_record_query_trace (1.8)
activejob (7.0.5.1)
activesupport (= 7.0.5.1)
activejob (7.0.8)
activesupport (= 7.0.8)
globalid (>= 0.3.6)
activemodel (7.0.5.1)
activesupport (= 7.0.5.1)
activerecord (7.0.5.1)
activemodel (= 7.0.5.1)
activesupport (= 7.0.5.1)
activemodel (7.0.8)
activesupport (= 7.0.8)
activerecord (7.0.8)
activemodel (= 7.0.8)
activesupport (= 7.0.8)
activerecord-import (1.4.1)
activerecord (>= 4.2)
activestorage (7.0.5.1)
actionpack (= 7.0.5.1)
activejob (= 7.0.5.1)
activerecord (= 7.0.5.1)
activesupport (= 7.0.5.1)
activestorage (7.0.8)
actionpack (= 7.0.8)
activejob (= 7.0.8)
activerecord (= 7.0.8)
activesupport (= 7.0.8)
marcel (~> 1.0)
mini_mime (>= 1.1.0)
activesupport (7.0.5.1)
activesupport (7.0.8)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
@@ -116,13 +116,18 @@ GEM
administrate-field-active_storage (0.4.2)
administrate (>= 0.2.2)
rails (>= 7.0)
administrate-field-belongs_to_search (0.8.0)
administrate (>= 0.3, < 1.0)
jbuilder (~> 2)
rails (>= 4.2, < 7.1)
selectize-rails (~> 0.6)
annotate (3.2.0)
activerecord (>= 3.2, < 8.0)
rake (>= 10.4, < 14.0)
ast (2.4.2)
attr_extras (7.1.0)
audited (5.3.3)
activerecord (>= 5.0, < 7.1)
audited (5.4.0)
activerecord (>= 5.0, < 7.2)
request_store (~> 1.2)
aws-eventstream (1.2.0)
aws-partitions (1.760.0)
@@ -269,8 +274,8 @@ GEM
grpc (~> 1.36)
geocoder (1.8.1)
gli (2.21.0)
globalid (1.1.0)
activesupport (>= 5.0)
globalid (1.2.1)
activesupport (>= 6.1)
gmail_xoauth (0.4.2)
oauth (>= 0.3.6)
google-apis-core (0.11.0)
@@ -450,9 +455,9 @@ GEM
mime-types-data (~> 3.2015)
mime-types-data (3.2023.0218.1)
mini_magick (4.12.0)
mini_mime (1.1.2)
mini_mime (1.1.5)
mini_portile2 (2.8.4)
minitest (5.19.0)
minitest (5.20.0)
mock_redis (0.36.0)
ruby2_keywords
msgpack (1.7.0)
@@ -463,7 +468,7 @@ GEM
activerecord (>= 5.2)
net-http-persistent (4.0.2)
connection_pool (~> 2.2)
net-imap (0.3.6)
net-imap (0.3.7)
date
net-protocol
net-pop (0.1.2)
@@ -478,14 +483,14 @@ GEM
sidekiq
newrelic_rpm (8.16.0)
nio4r (2.5.9)
nokogiri (1.15.3)
nokogiri (1.15.4)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
nokogiri (1.15.3-arm64-darwin)
nokogiri (1.15.4-arm64-darwin)
racc (~> 1.4)
nokogiri (1.15.3-x86_64-darwin)
nokogiri (1.15.4-x86_64-darwin)
racc (~> 1.4)
nokogiri (1.15.3-x86_64-linux)
nokogiri (1.15.4-x86_64-linux)
racc (~> 1.4)
numo-narray (0.9.2.1)
oauth (1.1.0)
@@ -543,7 +548,7 @@ GEM
pry-rails (0.3.9)
pry (>= 0.10.4)
public_suffix (5.0.1)
puma (6.2.2)
puma (6.3.1)
nio4r (~> 2.0)
pundit (2.3.0)
activesupport (>= 3.0.0)
@@ -563,30 +568,30 @@ GEM
rack-test (2.1.0)
rack (>= 1.3)
rack-timeout (0.6.3)
rails (7.0.5.1)
actioncable (= 7.0.5.1)
actionmailbox (= 7.0.5.1)
actionmailer (= 7.0.5.1)
actionpack (= 7.0.5.1)
actiontext (= 7.0.5.1)
actionview (= 7.0.5.1)
activejob (= 7.0.5.1)
activemodel (= 7.0.5.1)
activerecord (= 7.0.5.1)
activestorage (= 7.0.5.1)
activesupport (= 7.0.5.1)
rails (7.0.8)
actioncable (= 7.0.8)
actionmailbox (= 7.0.8)
actionmailer (= 7.0.8)
actionpack (= 7.0.8)
actiontext (= 7.0.8)
actionview (= 7.0.8)
activejob (= 7.0.8)
activemodel (= 7.0.8)
activerecord (= 7.0.8)
activestorage (= 7.0.8)
activesupport (= 7.0.8)
bundler (>= 1.15.0)
railties (= 7.0.5.1)
rails-dom-testing (2.1.1)
railties (= 7.0.8)
rails-dom-testing (2.2.0)
activesupport (>= 5.0.0)
minitest
nokogiri (>= 1.6)
rails-html-sanitizer (1.6.0)
loofah (~> 2.21)
nokogiri (~> 1.14)
railties (7.0.5.1)
actionpack (= 7.0.5.1)
activesupport (= 7.0.5.1)
railties (7.0.8)
actionpack (= 7.0.8)
activesupport (= 7.0.8)
method_source
rake (>= 12.2)
thor (~> 1.0)
@@ -598,7 +603,7 @@ GEM
ffi (~> 1.0)
redis (5.0.6)
redis-client (>= 0.9.0)
redis-client (0.14.1)
redis-client (0.17.0)
connection_pool
redis-namespace (1.10.0)
redis (>= 4)
@@ -697,23 +702,23 @@ GEM
activesupport (>= 4)
selectize-rails (0.12.6)
semantic_range (3.0.0)
sentry-rails (5.10.0)
sentry-rails (5.11.0)
railties (>= 5.0)
sentry-ruby (~> 5.10.0)
sentry-ruby (5.10.0)
sentry-ruby (~> 5.11.0)
sentry-ruby (5.11.0)
concurrent-ruby (~> 1.0, >= 1.0.2)
sentry-sidekiq (5.10.0)
sentry-ruby (~> 5.10.0)
sentry-sidekiq (5.11.0)
sentry-ruby (~> 5.11.0)
sidekiq (>= 3.0)
sexp_processor (4.17.0)
shoulda-matchers (5.3.0)
activesupport (>= 5.2.0)
sidekiq (7.1.2)
sidekiq (7.1.3)
concurrent-ruby (< 2)
connection_pool (>= 2.3.0)
rack (>= 2.2.4)
redis-client (>= 0.14.0)
sidekiq-cron (1.10.0)
sidekiq-cron (1.10.1)
fugit (~> 1.8)
globalid (>= 1.0.1)
sidekiq (>= 6)
@@ -786,7 +791,7 @@ GEM
version_gem (1.1.2)
warden (1.2.9)
rack (>= 2.0.9)
web-console (4.2.0)
web-console (4.2.1)
actionview (>= 6.0.0)
activemodel (>= 6.0.0)
bindex (>= 0.4.0)
@@ -805,14 +810,14 @@ GEM
railties (>= 5.2)
semantic_range (>= 2.3.0)
webrick (1.8.1)
websocket-driver (0.7.5)
websocket-driver (0.7.6)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
wisper (2.0.0)
working_hours (1.4.1)
activesupport (>= 3.2)
tzinfo
zeitwerk (2.6.9)
zeitwerk (2.6.11)
PLATFORMS
arm64-darwin-20
@@ -831,9 +836,10 @@ DEPENDENCIES
acts-as-taggable-on
administrate (>= 0.19.0)
administrate-field-active_storage
administrate-field-belongs_to_search
annotate
attr_extras
audited (~> 5.3)
audited (~> 5.4, >= 5.4.0)
aws-sdk-s3
azure-storage-blob!
barnes
@@ -907,7 +913,7 @@ DEPENDENCIES
rack-cors
rack-mini-profiler (>= 3.1.1)
rack-timeout
rails (~> 7.0.5.1)
rails (~> 7.0.8.0)
redis
redis-namespace
responders
@@ -922,12 +928,12 @@ DEPENDENCIES
scout_apm
scss_lint
seed_dump
sentry-rails (>= 5.10.0)
sentry-rails (>= 5.11.0)
sentry-ruby
sentry-sidekiq (>= 5.10.0)
sentry-sidekiq (>= 5.11.0)
shoulda-matchers
sidekiq
sidekiq-cron
sidekiq (>= 7.1.3)
sidekiq-cron (>= 1.10.1)
simplecov (= 0.17.1)
slack-ruby-client (~> 2.0.0)
spring
@@ -943,7 +949,7 @@ DEPENDENCIES
tzinfo-data
uglifier
valid_email2
web-console
web-console (>= 4.2.1)
web-push
webmock
webpacker
+1 -1
View File
@@ -1,4 +1,4 @@
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
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",
"buildpacks": [
{
"url": "heroku/nodejs"
},
{
"url": "heroku/ruby"
}
@@ -29,3 +29,5 @@
@import 'components/pagination';
@import 'components/search';
@import 'components/reports';
@import 'custom_styles';
@@ -24,14 +24,6 @@ select {
font-size: $base-font-size;
}
input,
select,
textarea {
display: block;
font-family: $base-font-family;
font-size: 16px;
}
[type="color"],
[type="date"],
[type="datetime-local"],
@@ -51,6 +43,7 @@ textarea {
background-color: $white;
border: $base-border;
border-radius: $base-border-radius;
font-family: $base-font-family;
padding: 0.5em;
transition: border-color $base-duration $base-timing;
width: 100%;
@@ -4,8 +4,8 @@
float: left;
margin-bottom: $base-spacing;
margin-top: 0.25em;
text-align: right;
width: calc(15% - 1rem);
text-align: left;
width: calc(16% - 1rem);
}
.preserve-whitespace {
@@ -17,7 +17,7 @@
float: left;
margin-bottom: $base-spacing;
margin-left: 2rem;
width: calc(85% - 1rem);
width: calc(84% - 1rem);
}
.attribute--nested {
@@ -10,7 +10,7 @@ input[type="submit"],
color: $white;
cursor: pointer;
display: inline-block;
font-size: $font-size-default;
font-size: $font-size-small;
-webkit-font-smoothing: antialiased;
font-weight: $font-weight-medium;
line-height: 1;
@@ -1,13 +1,18 @@
.main-content {
font-size: $font-size-default;
left: 23rem;
left: 21rem;
position: absolute;
right: 0;
top: 0;
}
.main-content__body {
font-size: $font-size-small;
padding: $space-two;
table {
font-size: $font-size-small;
}
}
.main-content__header {
@@ -20,7 +25,7 @@
}
.main-content__page-title {
font-size: $font-size-large;
font-size: $font-size-medium;
font-weight: $font-weight-medium;
margin-right: auto;
}
@@ -1,7 +1,12 @@
.logo-brand {
margin-bottom: $space-normal;
padding: $space-normal $space-smaller $space-small;
text-align: center;
text-align: left;
img {
margin-bottom: $space-smaller;
max-height: 3rem;
}
}
.navigation {
@@ -19,12 +24,13 @@
padding: $space-normal;
position: fixed;
top: 0;
width: 23rem;
width: 21rem;
z-index: 1023;
li {
align-items: center;
display: flex;
font-size: $font-size-small;
a {
color: $color-gray;
@@ -35,6 +41,10 @@
min-width: $space-medium;
}
}
hr {
margin: $space-slab;
}
}
.navigation__link {
@@ -43,7 +53,7 @@
display: block;
line-height: 1;
margin-bottom: $space-smaller;
padding: $space-one;
padding: $space-small;
&:hover {
color: $blue;
@@ -0,0 +1,24 @@
// custom styles for the dashboard
.feature-cell {
background: $color-extra-light-blue;
border-radius: 10px;
float: left;
margin-left: 8px;
margin-top: 6px;
padding: 4px 12px;
.icon-container {
margin-right: 4px;
}
.value-container {
margin-left: 6px;
}
}
.feature-container {
max-width: 100rem;
}
@@ -4,7 +4,7 @@ $base-font-family: PlusJakarta, Inter, -apple-system, BlinkMacSystemFont, "Segoe
sans-serif !default;
$heading-font-family: $base-font-family !default;
$base-font-size: 16px !default;
$base-font-size: 14px !default;
$base-line-height: 1.5 !default;
$heading-line-height: 1.2 !default;
@@ -78,11 +78,6 @@ $conv-header-height: 4rem;
$inbox-thumb-size: 4.8rem;
// Spinner
$spinkit-spinner-color: $color-white !default;
$spinkit-spinner-margin: 0 0 0 1.6rem !default;
$spinkit-size: 1.6rem !default;
// Snackbar default
$woot-snackbar-bg: #323232;
$woot-snackbar-button: #ffeb3b;
@@ -1,7 +1,7 @@
class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
before_action :portal
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]
def index
@@ -36,17 +36,6 @@ class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
head :ok
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
Article.update_positions(params[:positions_hash])
head :ok
@@ -20,16 +20,6 @@ class Api::V1::Accounts::AutomationRulesController < Api::V1::Accounts::BaseCont
@automation_rule
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
ActiveRecord::Base.transaction do
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
@contacts_count = resolved_contacts.count
@contacts = fetch_contacts_with_conversation_count(resolved_contacts)
@contacts = fetch_contacts(resolved_contacts)
end
def search
render json: { error: 'Specify search string with parameter q' }, status: :unprocessable_entity if params[:q].blank? && return
contacts = resolved_contacts.where(
'name ILIKE :search OR email ILIKE :search OR phone_number ILIKE :search OR contacts.identifier LIKE :search',
'name ILIKE :search OR email ILIKE :search OR phone_number ILIKE :search OR contacts.identifier LIKE :search
OR contacts.additional_attributes->>\'company_name\' ILIKE :search',
search: "%#{params[:q].strip}%"
)
@contacts_count = contacts.count
@contacts = fetch_contacts_with_conversation_count(contacts)
@contacts = fetch_contacts(contacts)
end
def import
@@ -63,7 +64,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
result = ::Contacts::FilterService.new(params.permit!, current_user).perform
contacts = result[:contacts]
@contacts_count = result[:count]
@contacts = fetch_contacts_with_conversation_count(contacts)
@contacts = fetch_contacts(contacts)
end
def contactable_inboxes
@@ -125,17 +126,14 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
@current_page = params[:page] || 1
end
def fetch_contacts_with_conversation_count(contacts)
conversation_count_sub_query = 'SELECT COUNT(*) FROM "conversations" WHERE "conversations"."contact_id" = "contacts"."id"'
contacts_with_conversation_count = filtrate(contacts)
.select("contacts.*, (#{conversation_count_sub_query}) as conversations_count")
.group('contacts.id')
.includes([{ avatar_attachment: [:blob] }])
.page(@current_page).per(RESULTS_PER_PAGE)
def fetch_contacts(contacts)
contacts_with_avatar = filtrate(contacts)
.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
def build_contact_inbox
@@ -39,16 +39,6 @@ class Api::V1::Accounts::MacrosController < Api::V1::Accounts::BaseController
head :ok
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
::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
include ::FileTypeHelper
before_action :fetch_portal, except: [:index, :create, :attach_file]
before_action :fetch_portal, except: [:index, :create]
before_action :check_authorization
before_action :set_current_page, only: [:index]
@@ -53,16 +53,6 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
@portal.logo.attach(blob)
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
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
def create
builder = Integrations::Slack::IncomingMessageBuilder.new(params)
builder = Integrations::Slack::IncomingMessageBuilder.new(permitted_params)
response = builder.perform
render json: response
end
private
# TODO: This is a temporary solution to permit all params for slack unfurling job.
# We should only permit the params that we use. Handle all the params based on events and send it to the respective services.
def permitted_params
params.permit!
end
end
@@ -8,7 +8,7 @@ module EnsureCurrentAccountHelper
def ensure_current_account
account = Account.find(params[:account_id])
ensure_account_is_active?(account)
render_unauthorized('Account is suspended') and return unless account.active?
if current_user
account_accessible_for_user?(account)
@@ -27,8 +27,4 @@ module EnsureCurrentAccountHelper
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)
end
def ensure_account_is_active?(account)
render_unauthorized('Account is suspended') unless account.active?
end
end
@@ -9,11 +9,14 @@ module RequestExceptionHandler
def handle_with_exception
yield
rescue ActiveRecord::RecordNotFound
rescue ActiveRecord::RecordNotFound => e
log_handled_error(e)
render_not_found_error('Resource could not be found')
rescue Pundit::NotAuthorizedError
rescue Pundit::NotAuthorizedError => e
log_handled_error(e)
render_unauthorized('You are not authorized to do this action')
rescue ActionController::ParameterMissing => e
log_handled_error(e)
render_could_not_create_error(e.message)
ensure
# to address the thread variable leak issues in Puma/Thin webserver
@@ -41,6 +44,7 @@ module RequestExceptionHandler
end
def render_record_invalid(exception)
log_handled_error(exception)
render json: {
message: exception.record.errors.full_messages.join(', '),
attributes: exception.record.errors.attribute_names
@@ -48,6 +52,11 @@ module RequestExceptionHandler
end
def render_error_response(exception)
log_handled_error(exception)
render json: exception.to_hash, status: exception.http_status
end
def log_handled_error(exception)
logger.info("Handled error: #{exception.inspect}")
end
end
@@ -33,7 +33,7 @@ class Public::Api::V1::Inboxes::ConversationsController < Public::Api::V1::Inbox
end
def create_conversation
::Conversation.create!(conversation_params)
ConversationBuilder.new(params: conversation_params, contact_inbox: @contact_inbox).perform
end
def trigger_typing_event(event)
@@ -41,11 +41,6 @@ class Public::Api::V1::Inboxes::ConversationsController < Public::Api::V1::Inbox
end
def conversation_params
{
account_id: @contact_inbox.contact.account_id,
inbox_id: @contact_inbox.inbox_id,
contact_id: @contact_inbox.contact_id,
contact_inbox_id: @contact_inbox.id
}
params.permit(custom_attributes: {})
end
end
@@ -44,7 +44,7 @@ class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::B
end
def list_params
params.permit(:query, :locale, :sort)
params.permit(:query, :locale, :sort, :status)
end
def permitted_params
@@ -1,6 +1,8 @@
class Public::Api::V1::Portals::BaseController < PublicController
before_action :show_plain_layout
before_action :set_color_scheme
around_action :set_locale
after_action :allow_iframe_requests
private
@@ -8,6 +10,14 @@ class Public::Api::V1::Portals::BaseController < PublicController
@is_plain_layout_enabled = params[:show_plain_layout] == 'true'
end
def set_color_scheme
@theme = if %w[dark light].include?(params[:theme])
params[:theme]
else
''
end
end
def set_locale(&)
switch_locale_with_portal(&) if params[:locale].present?
switch_locale_with_article(&) if params[:article_slug].present?
@@ -39,4 +49,8 @@ class Public::Api::V1::Portals::BaseController < PublicController
I18n.with_locale(@locale, &)
end
def allow_iframe_requests
response.headers.delete('X-Frame-Options') if @is_plain_layout_enabled
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
redis_metrics
chatwoot_edition
instance_meta
end
def chatwoot_edition
@@ -18,6 +19,10 @@ class SuperAdmin::InstanceStatusesController < SuperAdmin::ApplicationController
end
end
def instance_meta
@metrics['Database Migrations'] = ActiveRecord::Base.connection.migration_context.needs_migration? ? 'pending' : 'completed'
end
def chatwoot_version
@metrics['Chatwoot version'] = Chatwoot.config[:version]
end
+2 -2
View File
@@ -18,8 +18,8 @@ class AccountDashboard < Administrate::BaseDashboard
end
ATTRIBUTE_TYPES = {
id: Field::Number,
name: Field::String,
id: Field::Number.with_options(searchable: true),
name: Field::String.with_options(searchable: true),
created_at: Field::DateTime,
updated_at: Field::DateTime,
users: CountField,
+3 -3
View File
@@ -8,9 +8,9 @@ class AccountUserDashboard < Administrate::BaseDashboard
# which determines how the attribute is displayed
# on pages throughout the dashboard.
ATTRIBUTE_TYPES = {
account: Field::BelongsTo.with_options(searchable: true, searchable_field: 'name', order: 'id DESC'),
user: Field::BelongsTo.with_options(searchable: true, searchable_field: 'name', order: 'id DESC'),
inviter: Field::BelongsTo.with_options(class_name: 'User', searchable: true, searchable_field: 'name'),
account: Field::BelongsToSearch.with_options(class_name: 'Account', searchable: true, searchable_field: [:name, :id], order: 'id DESC'),
user: Field::BelongsToSearch.with_options(class_name: 'User', searchable: true, searchable_field: [:name, :email, :id], order: 'id DESC'),
inviter: Field::BelongsToSearch.with_options(class_name: 'User', searchable: true, searchable_field: [:name, :email, :id], order: 'id DESC'),
id: Field::Number,
role: Field::Select.with_options(collection: AccountUser.roles.keys),
created_at: Field::DateTime,
+3 -3
View File
@@ -9,7 +9,7 @@ class UserDashboard < Administrate::BaseDashboard
# on pages throughout the dashboard.
ATTRIBUTE_TYPES = {
account_users: Field::HasMany,
id: Field::Number,
id: Field::Number.with_options(searchable: true),
avatar_url: AvatarField,
avatar: Field::ActiveStorage.with_options(
destroy_url: proc do |_namespace, _resource, attachment|
@@ -28,9 +28,9 @@ class UserDashboard < Administrate::BaseDashboard
confirmed_at: Field::DateTime,
confirmation_sent_at: Field::DateTime,
unconfirmed_email: Field::String,
name: Field::String,
name: Field::String.with_options(searchable: true),
display_name: Field::String,
email: Field::String,
email: Field::String.with_options(searchable: true),
tokens: Field::String.with_options(searchable: false),
created_at: Field::DateTime,
updated_at: Field::DateTime,
+11
View File
@@ -0,0 +1,11 @@
module PortalHelper
def generate_portal_bg_color(portal_color, theme)
base_color = theme == 'dark' ? 'black' : 'white'
"color-mix(in srgb, #{portal_color} 10%, #{base_color})"
end
def generate_portal_bg(portal_color, theme)
bg_image = theme == 'dark' ? 'grid_dark.svg' : 'grid.svg'
"background: url(/assets/images/hc/#{bg_image}) #{generate_portal_bg_color(portal_color, theme)}"
end
end
+6 -8
View File
@@ -2,7 +2,7 @@
<div
v-if="!authUIFlags.isFetching"
id="app"
class="app-wrapper app-root"
class="app-wrapper h-full flex-grow-0 min-h-0 w-full"
:class="{ 'app-rtl--wrapper': isRTLView }"
:dir="isRTLView ? 'rtl' : 'ltr'"
>
@@ -26,14 +26,14 @@
<script>
import { mapGetters } from 'vuex';
import AddAccountModal from '../dashboard/components/layout/sidebarComponents/AddAccountModal';
import AddAccountModal from '../dashboard/components/layout/sidebarComponents/AddAccountModal.vue';
import LoadingState from './components/widgets/LoadingState.vue';
import NetworkNotification from './components/NetworkNotification';
import NetworkNotification from './components/NetworkNotification.vue';
import UpdateBanner from './components/app/UpdateBanner.vue';
import UpgradeBanner from './components/app/UpgradeBanner.vue';
import PaymentPendingBanner from './components/app/PaymentPendingBanner.vue';
import vueActionCable from './helper/actionCable';
import WootSnackbarBox from './components/SnackbarContainer';
import WootSnackbarBox from './components/SnackbarContainer.vue';
import rtlMixin from 'shared/mixins/rtlMixin';
import { setColorTheme } from './helper/themeHelper';
import {
@@ -111,10 +111,8 @@ export default {
this.$store.dispatch('setActiveAccount', {
accountId: this.currentAccountId,
});
const {
locale,
latest_chatwoot_version: latestChatwootVersion,
} = this.getAccount(this.currentAccountId);
const { locale, latest_chatwoot_version: latestChatwootVersion } =
this.getAccount(this.currentAccountId);
const { pubsub_token: pubsubToken } = this.currentUser || {};
this.setLocale(locale);
this.updateRTLDirectionView(locale);
+2 -3
View File
@@ -15,9 +15,8 @@ class ApiClient {
// eslint-disable-next-line class-methods-use-this
get accountIdFromRoute() {
const isInsideAccountScopedURLs = window.location.pathname.includes(
'/app/accounts'
);
const isInsideAccountScopedURLs =
window.location.pathname.includes('/app/accounts');
if (isInsideAccountScopedURLs) {
return window.location.pathname.split('/')[3];
@@ -9,14 +9,6 @@ class AutomationsAPI extends ApiClient {
clone(automationId) {
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();
+1
View File
@@ -53,6 +53,7 @@ class ContactAPI extends ApiClient {
return axios.get(requestURL);
}
// eslint-disable-next-line default-param-last
filter(page = 1, sortAttr = 'name', queryPayload) {
let requestURL = `${this.url}/filter?${buildContactParams(page, sortAttr)}`;
return axios.post(requestURL, queryPayload);
@@ -1,6 +1,5 @@
import accountAPI from '../account';
import ApiClient from '../../ApiClient';
import describeWithAPIMock from '../../specs/apiSpecHelper';
describe('#enterpriseAccountAPI', () => {
it('creates correct instance', () => {
@@ -13,17 +12,33 @@ describe('#enterpriseAccountAPI', () => {
expect(accountAPI).toHaveProperty('checkout');
});
describeWithAPIMock('API calls', context => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
window.axios = axiosMock;
});
afterEach(() => {
window.axios = originalAxios;
});
it('#checkout', () => {
accountAPI.checkout();
expect(context.axiosMock.post).toHaveBeenCalledWith(
expect(axiosMock.post).toHaveBeenCalledWith(
'/enterprise/api/v1/checkout'
);
});
it('#subscription', () => {
accountAPI.subscription();
expect(context.axiosMock.post).toHaveBeenCalledWith(
expect(axiosMock.post).toHaveBeenCalledWith(
'/enterprise/api/v1/subscription'
);
});
@@ -47,20 +47,6 @@ class ArticlesAPI extends PortalsAPI {
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 }) {
return axios.post(`${this.url}/${portalSlug}/articles/reorder`, {
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) {
return axios.get(`${this.url}/summary`, {
params: {
@@ -1,6 +1,5 @@
import accountAPI from '../account';
import ApiClient from '../ApiClient';
import describeWithAPIMock from './apiSpecHelper';
describe('#accountAPI', () => {
it('creates correct instance', () => {
@@ -13,12 +12,28 @@ describe('#accountAPI', () => {
expect(accountAPI).toHaveProperty('createAccount');
});
describeWithAPIMock('API calls', context => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
window.axios = axiosMock;
});
afterEach(() => {
window.axios = originalAxios;
});
it('#createAccount', () => {
accountAPI.createAccount({
name: 'Chatwoot',
});
expect(context.axiosMock.post).toHaveBeenCalledWith('/api/v1/accounts', {
expect(axiosMock.post).toHaveBeenCalledWith('/api/v1/accounts', {
name: 'Chatwoot',
});
});
@@ -1,6 +1,5 @@
import accountActionsAPI from '../accountActions';
import ApiClient from '../ApiClient';
import describeWithAPIMock from './apiSpecHelper';
describe('#ContactsAPI', () => {
it('creates correct instance', () => {
@@ -8,10 +7,26 @@ describe('#ContactsAPI', () => {
expect(accountActionsAPI).toHaveProperty('merge');
});
describeWithAPIMock('API calls', context => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
window.axios = axiosMock;
});
afterEach(() => {
window.axios = originalAxios;
});
it('#merge', () => {
accountActionsAPI.merge(1, 2);
expect(context.axiosMock.post).toHaveBeenCalledWith(
expect(axiosMock.post).toHaveBeenCalledWith(
'/api/v1/actions/contact_merge',
{
base_contact_id: 1,
@@ -1,27 +0,0 @@
function apiSpecHelper() {
beforeEach(() => {
this.originalAxios = window.axios;
this.axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
window.axios = this.axiosMock;
});
afterEach(() => {
window.axios = this.originalAxios;
});
}
// https://stackoverflow.com/a/59344023/3901856
const sharedWrapper = describe('sharedWrapper', () => {});
export default function describeWithAPIMock(skillName, testFn) {
return describe(skillName, function configureContext() {
function Context() {}
Context.prototype = sharedWrapper.ctx;
this.ctx = new Context();
apiSpecHelper.call(this);
testFn.call(this, this);
});
}
@@ -1,6 +1,5 @@
import articlesAPI from '../helpCenter/articles';
import ApiClient from 'dashboard/api/helpCenter/portals';
import describeWithAPIMock from './apiSpecHelper';
describe('#PortalAPI', () => {
it('creates correct instance', () => {
@@ -12,7 +11,23 @@ describe('#PortalAPI', () => {
expect(articlesAPI).toHaveProperty('delete');
expect(articlesAPI).toHaveProperty('getArticles');
});
describeWithAPIMock('API calls', context => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
window.axios = axiosMock;
});
afterEach(() => {
window.axios = originalAxios;
});
it('#getArticles', () => {
articlesAPI.getArticles({
pageNumber: 1,
@@ -21,30 +36,62 @@ describe('#PortalAPI', () => {
status: 'published',
author_id: '1',
});
expect(context.axiosMock.get).toHaveBeenCalledWith(
expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v1/portals/room-rental/articles?page=1&locale=en-US&status=published&author_id=1'
);
});
});
describeWithAPIMock('API calls', context => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
window.axios = axiosMock;
});
afterEach(() => {
window.axios = originalAxios;
});
it('#getArticle', () => {
articlesAPI.getArticle({
id: 1,
portalSlug: 'room-rental',
});
expect(context.axiosMock.get).toHaveBeenCalledWith(
expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v1/portals/room-rental/articles/1'
);
});
});
describeWithAPIMock('API calls', context => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
window.axios = axiosMock;
});
afterEach(() => {
window.axios = originalAxios;
});
it('#updateArticle', () => {
articlesAPI.updateArticle({
articleId: 1,
portalSlug: 'room-rental',
articleObj: { title: 'Update shipping address' },
});
expect(context.axiosMock.patch).toHaveBeenCalledWith(
expect(axiosMock.patch).toHaveBeenCalledWith(
'/api/v1/portals/room-rental/articles/1',
{
title: 'Update shipping address',
@@ -52,13 +99,29 @@ describe('#PortalAPI', () => {
);
});
});
describeWithAPIMock('API calls', context => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
window.axios = axiosMock;
});
afterEach(() => {
window.axios = originalAxios;
});
it('#deleteArticle', () => {
articlesAPI.deleteArticle({
articleId: 1,
portalSlug: 'room-rental',
});
expect(context.axiosMock.delete).toHaveBeenCalledWith(
expect(axiosMock.delete).toHaveBeenCalledWith(
'/api/v1/portals/room-rental/articles/1'
);
});
@@ -1,18 +1,30 @@
import assignableAgentsAPI from '../assignableAgents';
import describeWithAPIMock from './apiSpecHelper';
describe('#AssignableAgentsAPI', () => {
describeWithAPIMock('API calls', context => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
window.axios = axiosMock;
});
afterEach(() => {
window.axios = originalAxios;
});
it('#getAssignableAgents', () => {
assignableAgentsAPI.get([1]);
expect(context.axiosMock.get).toHaveBeenCalledWith(
'/api/v1/assignable_agents',
{
params: {
inbox_ids: [1],
},
}
);
expect(axiosMock.get).toHaveBeenCalledWith('/api/v1/assignable_agents', {
params: {
inbox_ids: [1],
},
});
});
});
});
@@ -1,6 +1,5 @@
import fbChannel from '../../channel/fbChannel';
import ApiClient from '../../ApiClient';
import describeWithAPIMock from '../apiSpecHelper';
describe('#FBChannel', () => {
it('creates correct instance', () => {
@@ -11,10 +10,26 @@ describe('#FBChannel', () => {
expect(fbChannel).toHaveProperty('update');
expect(fbChannel).toHaveProperty('delete');
});
describeWithAPIMock('API calls', context => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
window.axios = axiosMock;
});
afterEach(() => {
window.axios = originalAxios;
});
it('#create', () => {
fbChannel.create({ omniauthToken: 'ASFM131CSF@#@$', appId: 'chatwoot' });
expect(context.axiosMock.post).toHaveBeenCalledWith(
expect(axiosMock.post).toHaveBeenCalledWith(
'/api/v1/callbacks/register_facebook_page',
{
omniauthToken: 'ASFM131CSF@#@$',
@@ -27,7 +42,7 @@ describe('#FBChannel', () => {
omniauthToken: 'ASFM131CSF@#@$',
inboxId: 1,
});
expect(context.axiosMock.post).toHaveBeenCalledWith(
expect(axiosMock.post).toHaveBeenCalledWith(
'/api/v1/callbacks/reauthorize_page',
{
omniauth_token: 'ASFM131CSF@#@$',
@@ -1,6 +1,5 @@
import contactAPI, { buildContactParams } from '../contacts';
import ApiClient from '../ApiClient';
import describeWithAPIMock from './apiSpecHelper';
describe('#ContactsAPI', () => {
it('creates correct instance', () => {
@@ -15,56 +14,67 @@ describe('#ContactsAPI', () => {
expect(contactAPI).toHaveProperty('destroyAvatar');
});
describeWithAPIMock('API calls', context => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
window.axios = axiosMock;
});
afterEach(() => {
window.axios = originalAxios;
});
it('#get', () => {
contactAPI.get(1, 'name', 'customer-support');
expect(context.axiosMock.get).toHaveBeenCalledWith(
expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v1/contacts?include_contact_inboxes=false&page=1&sort=name&labels[]=customer-support'
);
});
it('#getConversations', () => {
contactAPI.getConversations(1);
expect(context.axiosMock.get).toHaveBeenCalledWith(
expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v1/contacts/1/conversations'
);
});
it('#getContactableInboxes', () => {
contactAPI.getContactableInboxes(1);
expect(context.axiosMock.get).toHaveBeenCalledWith(
expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v1/contacts/1/contactable_inboxes'
);
});
it('#getContactLabels', () => {
contactAPI.getContactLabels(1);
expect(context.axiosMock.get).toHaveBeenCalledWith(
'/api/v1/contacts/1/labels'
);
expect(axiosMock.get).toHaveBeenCalledWith('/api/v1/contacts/1/labels');
});
it('#updateContactLabels', () => {
const labels = ['support-query'];
contactAPI.updateContactLabels(1, labels);
expect(context.axiosMock.post).toHaveBeenCalledWith(
'/api/v1/contacts/1/labels',
{
labels,
}
);
expect(axiosMock.post).toHaveBeenCalledWith('/api/v1/contacts/1/labels', {
labels,
});
});
it('#search', () => {
contactAPI.search('leads', 1, 'date', 'customer-support');
expect(context.axiosMock.get).toHaveBeenCalledWith(
expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v1/contacts/search?include_contact_inboxes=false&page=1&sort=date&q=leads&labels[]=customer-support'
);
});
it('#destroyCustomAttributes', () => {
contactAPI.destroyCustomAttributes(1, ['cloudCustomer']);
expect(context.axiosMock.post).toHaveBeenCalledWith(
expect(axiosMock.post).toHaveBeenCalledWith(
'/api/v1/contacts/1/destroy_custom_attributes',
{
custom_attributes: ['cloudCustomer'],
@@ -75,7 +85,7 @@ describe('#ContactsAPI', () => {
it('#importContacts', () => {
const file = 'file';
contactAPI.importContacts(file);
expect(context.axiosMock.post).toHaveBeenCalledWith(
expect(axiosMock.post).toHaveBeenCalledWith(
'/api/v1/contacts/import',
expect.any(FormData),
{
@@ -96,7 +106,7 @@ describe('#ContactsAPI', () => {
],
};
contactAPI.filter(1, 'name', queryPayload);
expect(context.axiosMock.post).toHaveBeenCalledWith(
expect(axiosMock.post).toHaveBeenCalledWith(
'/api/v1/contacts/filter?include_contact_inboxes=false&page=1&sort=name',
queryPayload
);
@@ -104,7 +114,7 @@ describe('#ContactsAPI', () => {
it('#destroyAvatar', () => {
contactAPI.destroyAvatar(1);
expect(context.axiosMock.delete).toHaveBeenCalledWith(
expect(axiosMock.delete).toHaveBeenCalledWith(
'/api/v1/contacts/1/avatar'
);
});
@@ -1,6 +1,5 @@
import conversationsAPI from '../conversations';
import ApiClient from '../ApiClient';
import describeWithAPIMock from './apiSpecHelper';
describe('#ConversationApi', () => {
it('creates correct instance', () => {
@@ -14,10 +13,26 @@ describe('#ConversationApi', () => {
expect(conversationsAPI).toHaveProperty('updateLabels');
});
describeWithAPIMock('API calls', context => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
window.axios = axiosMock;
});
afterEach(() => {
window.axios = originalAxios;
});
it('#getLabels', () => {
conversationsAPI.getLabels(1);
expect(context.axiosMock.get).toHaveBeenCalledWith(
expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v1/conversations/1/labels'
);
});
@@ -25,7 +40,7 @@ describe('#ConversationApi', () => {
it('#updateLabels', () => {
const labels = ['support-query'];
conversationsAPI.updateLabels(1, labels);
expect(context.axiosMock.post).toHaveBeenCalledWith(
expect(axiosMock.post).toHaveBeenCalledWith(
'/api/v1/conversations/1/labels',
{
labels,
@@ -1,6 +1,5 @@
import csatReportsAPI from '../csatReports';
import ApiClient from '../ApiClient';
import describeWithAPIMock from './apiSpecHelper';
describe('#Reports API', () => {
it('creates correct instance', () => {
@@ -9,10 +8,26 @@ describe('#Reports API', () => {
expect(csatReportsAPI).toHaveProperty('get');
expect(csatReportsAPI).toHaveProperty('getMetrics');
});
describeWithAPIMock('API calls', context => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
window.axios = axiosMock;
});
afterEach(() => {
window.axios = originalAxios;
});
it('#get', () => {
csatReportsAPI.get({ page: 1, from: 1622485800, to: 1623695400 });
expect(context.axiosMock.get).toHaveBeenCalledWith(
expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v1/csat_survey_responses',
{
params: {
@@ -26,7 +41,7 @@ describe('#Reports API', () => {
});
it('#getMetrics', () => {
csatReportsAPI.getMetrics({ from: 1622485800, to: 1623695400 });
expect(context.axiosMock.get).toHaveBeenCalledWith(
expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v1/csat_survey_responses/metrics',
{
params: { since: 1622485800, until: 1623695400 },
@@ -39,7 +54,7 @@ describe('#Reports API', () => {
to: 1623695400,
user_ids: 1,
});
expect(context.axiosMock.get).toHaveBeenCalledWith(
expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v1/csat_survey_responses/download',
{
params: {
@@ -1,6 +1,5 @@
import conversationAPI from '../../inbox/conversation';
import ApiClient from '../../ApiClient';
import describeWithAPIMock from '../apiSpecHelper';
describe('#ConversationAPI', () => {
it('creates correct instance', () => {
@@ -22,7 +21,23 @@ describe('#ConversationAPI', () => {
expect(conversationAPI).toHaveProperty('filter');
});
describeWithAPIMock('API calls', context => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
window.axios = axiosMock;
});
afterEach(() => {
window.axios = originalAxios;
});
it('#get conversations', () => {
conversationAPI.get({
inboxId: 1,
@@ -32,19 +47,16 @@ describe('#ConversationAPI', () => {
labels: [],
teamId: 1,
});
expect(context.axiosMock.get).toHaveBeenCalledWith(
'/api/v1/conversations',
{
params: {
inbox_id: 1,
team_id: 1,
status: 'open',
assignee_type: 'me',
page: 1,
labels: [],
},
}
);
expect(axiosMock.get).toHaveBeenCalledWith('/api/v1/conversations', {
params: {
inbox_id: 1,
team_id: 1,
status: 'open',
assignee_type: 'me',
page: 1,
labels: [],
},
});
});
it('#search', () => {
@@ -53,7 +65,7 @@ describe('#ConversationAPI', () => {
page: 1,
});
expect(context.axiosMock.get).toHaveBeenCalledWith(
expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v1/conversations/search',
{
params: {
@@ -66,7 +78,7 @@ describe('#ConversationAPI', () => {
it('#toggleStatus', () => {
conversationAPI.toggleStatus({ conversationId: 12, status: 'online' });
expect(context.axiosMock.post).toHaveBeenCalledWith(
expect(axiosMock.post).toHaveBeenCalledWith(
`/api/v1/conversations/12/toggle_status`,
{
status: 'online',
@@ -77,7 +89,7 @@ describe('#ConversationAPI', () => {
it('#assignAgent', () => {
conversationAPI.assignAgent({ conversationId: 12, agentId: 34 });
expect(context.axiosMock.post).toHaveBeenCalledWith(
expect(axiosMock.post).toHaveBeenCalledWith(
`/api/v1/conversations/12/assignments?assignee_id=34`,
{}
);
@@ -85,7 +97,7 @@ describe('#ConversationAPI', () => {
it('#assignTeam', () => {
conversationAPI.assignTeam({ conversationId: 12, teamId: 1 });
expect(context.axiosMock.post).toHaveBeenCalledWith(
expect(axiosMock.post).toHaveBeenCalledWith(
`/api/v1/conversations/12/assignments`,
{
team_id: 1,
@@ -95,7 +107,7 @@ describe('#ConversationAPI', () => {
it('#markMessageRead', () => {
conversationAPI.markMessageRead({ id: 12 });
expect(context.axiosMock.post).toHaveBeenCalledWith(
expect(axiosMock.post).toHaveBeenCalledWith(
`/api/v1/conversations/12/update_last_seen`
);
});
@@ -105,7 +117,7 @@ describe('#ConversationAPI', () => {
conversationId: 12,
status: 'typing_on',
});
expect(context.axiosMock.post).toHaveBeenCalledWith(
expect(axiosMock.post).toHaveBeenCalledWith(
`/api/v1/conversations/12/toggle_typing_status`,
{
typing_status: 'typing_on',
@@ -115,14 +127,14 @@ describe('#ConversationAPI', () => {
it('#mute', () => {
conversationAPI.mute(45);
expect(context.axiosMock.post).toHaveBeenCalledWith(
expect(axiosMock.post).toHaveBeenCalledWith(
'/api/v1/conversations/45/mute'
);
});
it('#unmute', () => {
conversationAPI.unmute(45);
expect(context.axiosMock.post).toHaveBeenCalledWith(
expect(axiosMock.post).toHaveBeenCalledWith(
'/api/v1/conversations/45/unmute'
);
});
@@ -135,18 +147,15 @@ describe('#ConversationAPI', () => {
labels: [],
teamId: 1,
});
expect(context.axiosMock.get).toHaveBeenCalledWith(
'/api/v1/conversations/meta',
{
params: {
inbox_id: 1,
team_id: 1,
status: 'open',
assignee_type: 'me',
labels: [],
},
}
);
expect(axiosMock.get).toHaveBeenCalledWith('/api/v1/conversations/meta', {
params: {
inbox_id: 1,
team_id: 1,
status: 'open',
assignee_type: 'me',
labels: [],
},
});
});
it('#sendEmailTranscript', () => {
@@ -154,7 +163,7 @@ describe('#ConversationAPI', () => {
conversationId: 45,
email: 'john@acme.inc',
});
expect(context.axiosMock.post).toHaveBeenCalledWith(
expect(axiosMock.post).toHaveBeenCalledWith(
'/api/v1/conversations/45/transcript',
{
email: 'john@acme.inc',
@@ -167,7 +176,7 @@ describe('#ConversationAPI', () => {
conversationId: 45,
customAttributes: { order_d: '1001' },
});
expect(context.axiosMock.post).toHaveBeenCalledWith(
expect(axiosMock.post).toHaveBeenCalledWith(
'/api/v1/conversations/45/custom_attributes',
{
custom_attributes: { order_d: '1001' },
@@ -202,9 +211,7 @@ describe('#ConversationAPI', () => {
},
};
conversationAPI.filter(payload);
expect(
context.axiosMock.post
).toHaveBeenCalledWith(
expect(axiosMock.post).toHaveBeenCalledWith(
'/api/v1/conversations/filter',
payload.queryData,
{ params: { page: payload.page } }
@@ -213,7 +220,7 @@ describe('#ConversationAPI', () => {
it('#getAllAttachments', () => {
conversationAPI.getAllAttachments(1);
expect(context.axiosMock.get).toHaveBeenCalledWith(
expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v1/conversations/1/attachments'
);
});
@@ -1,6 +1,5 @@
import messageAPI, { buildCreatePayload } from '../../inbox/message';
import ApiClient from '../../ApiClient';
import describeWithAPIMock from '../apiSpecHelper';
describe('#ConversationAPI', () => {
it('creates correct instance', () => {
@@ -13,13 +12,29 @@ describe('#ConversationAPI', () => {
expect(messageAPI).toHaveProperty('getPreviousMessages');
});
describeWithAPIMock('API calls', context => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
window.axios = axiosMock;
});
afterEach(() => {
window.axios = originalAxios;
});
it('#getPreviousMessages', () => {
messageAPI.getPreviousMessages({
conversationId: 12,
before: 4573,
});
expect(context.axiosMock.get).toHaveBeenCalledWith(
expect(axiosMock.get).toHaveBeenCalledWith(
`/api/v1/conversations/12/messages`,
{
params: {
@@ -35,7 +50,6 @@ describe('#ConversationAPI', () => {
message: 'test content',
echoId: 12,
isPrivate: true,
files: [new Blob(['test-content'], { type: 'application/pdf' })],
});
expect(formPayload).toBeInstanceOf(FormData);
@@ -58,8 +72,10 @@ describe('#ConversationAPI', () => {
private: false,
echo_id: 12,
content_attributes: { in_reply_to: 12 },
bcc_emails: '',
cc_emails: '',
bcc_emails: '',
to_emails: '',
template_params: undefined,
});
});
});
@@ -1,6 +1,5 @@
import inboxesAPI from '../inboxes';
import ApiClient from '../ApiClient';
import describeWithAPIMock from './apiSpecHelper';
describe('#InboxesAPI', () => {
it('creates correct instance', () => {
@@ -14,19 +13,32 @@ describe('#InboxesAPI', () => {
expect(inboxesAPI).toHaveProperty('getAgentBot');
expect(inboxesAPI).toHaveProperty('setAgentBot');
});
describeWithAPIMock('API calls', context => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
window.axios = axiosMock;
});
afterEach(() => {
window.axios = originalAxios;
});
it('#getCampaigns', () => {
inboxesAPI.getCampaigns(2);
expect(context.axiosMock.get).toHaveBeenCalledWith(
'/api/v1/inboxes/2/campaigns'
);
expect(axiosMock.get).toHaveBeenCalledWith('/api/v1/inboxes/2/campaigns');
});
it('#deleteInboxAvatar', () => {
inboxesAPI.deleteInboxAvatar(2);
expect(context.axiosMock.delete).toHaveBeenCalledWith(
'/api/v1/inboxes/2/avatar'
);
expect(axiosMock.delete).toHaveBeenCalledWith('/api/v1/inboxes/2/avatar');
});
});
});
@@ -1,6 +1,5 @@
import integrationAPI from '../integrations';
import ApiClient from '../ApiClient';
import describeWithAPIMock from './apiSpecHelper';
describe('#integrationAPI', () => {
it('creates correct instance', () => {
@@ -16,11 +15,27 @@ describe('#integrationAPI', () => {
expect(integrationAPI).toHaveProperty('listAllSlackChannels');
expect(integrationAPI).toHaveProperty('deleteHook');
});
describeWithAPIMock('API calls', context => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
window.axios = axiosMock;
});
afterEach(() => {
window.axios = originalAxios;
});
it('#connectSlack', () => {
const code = 'SDNFJNSDFNDSJN';
integrationAPI.connectSlack(code);
expect(context.axiosMock.post).toHaveBeenCalledWith(
expect(axiosMock.post).toHaveBeenCalledWith(
'/api/v1/integrations/slack',
{
code,
@@ -31,7 +46,7 @@ describe('#integrationAPI', () => {
it('#updateSlack', () => {
const updateObj = { referenceId: 'SDFSDGSVE' };
integrationAPI.updateSlack(updateObj);
expect(context.axiosMock.patch).toHaveBeenCalledWith(
expect(axiosMock.patch).toHaveBeenCalledWith(
'/api/v1/integrations/slack',
{
reference_id: updateObj.referenceId,
@@ -41,16 +56,14 @@ describe('#integrationAPI', () => {
it('#listAllSlackChannels', () => {
integrationAPI.listAllSlackChannels();
expect(context.axiosMock.get).toHaveBeenCalledWith(
expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v1/integrations/slack/list_all_channels'
);
});
it('#delete', () => {
integrationAPI.delete(2);
expect(context.axiosMock.delete).toHaveBeenCalledWith(
'/api/v1/integrations/2'
);
expect(axiosMock.delete).toHaveBeenCalledWith('/api/v1/integrations/2');
});
it('#createHook', () => {
@@ -59,7 +72,7 @@ describe('#integrationAPI', () => {
settings: { api_key: 'SDFSDGSVE' },
};
integrationAPI.createHook(hookData);
expect(context.axiosMock.post).toHaveBeenCalledWith(
expect(axiosMock.post).toHaveBeenCalledWith(
'/api/v1/integrations/hooks',
hookData
);
@@ -67,7 +80,7 @@ describe('#integrationAPI', () => {
it('#deleteHook', () => {
integrationAPI.deleteHook(2);
expect(context.axiosMock.delete).toHaveBeenCalledWith(
expect(axiosMock.delete).toHaveBeenCalledWith(
'/api/v1/integrations/hooks/2'
);
});
@@ -1,6 +1,5 @@
import DyteAPIClient from '../../integrations/dyte';
import ApiClient from '../../ApiClient';
import describeWithAPIMock from '../apiSpecHelper';
describe('#accountAPI', () => {
it('creates correct instance', () => {
@@ -9,10 +8,26 @@ describe('#accountAPI', () => {
expect(DyteAPIClient).toHaveProperty('addParticipantToMeeting');
});
describeWithAPIMock('createAMeeting', context => {
describe('createAMeeting', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
window.axios = axiosMock;
});
afterEach(() => {
window.axios = originalAxios;
});
it('creates a valid request', () => {
DyteAPIClient.createAMeeting(1);
expect(context.axiosMock.post).toHaveBeenCalledWith(
expect(axiosMock.post).toHaveBeenCalledWith(
'/api/v1/integrations/dyte/create_a_meeting',
{
conversation_id: 1,
@@ -21,10 +36,26 @@ describe('#accountAPI', () => {
});
});
describeWithAPIMock('addParticipantToMeeting', context => {
describe('addParticipantToMeeting', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
window.axios = axiosMock;
});
afterEach(() => {
window.axios = originalAxios;
});
it('creates a valid request', () => {
DyteAPIClient.addParticipantToMeeting(1);
expect(context.axiosMock.post).toHaveBeenCalledWith(
expect(axiosMock.post).toHaveBeenCalledWith(
'/api/v1/integrations/dyte/add_participant_to_meeting',
{
message_id: 1,
@@ -1,6 +1,5 @@
import notificationsAPI from '../notifications';
import ApiClient from '../ApiClient';
import describeWithAPIMock from './apiSpecHelper';
describe('#NotificationAPI', () => {
it('creates correct instance', () => {
@@ -11,31 +10,47 @@ describe('#NotificationAPI', () => {
expect(notificationsAPI).toHaveProperty('read');
expect(notificationsAPI).toHaveProperty('readAll');
});
describeWithAPIMock('API calls', context => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
window.axios = axiosMock;
});
afterEach(() => {
window.axios = originalAxios;
});
it('#get', () => {
notificationsAPI.get(1);
expect(context.axiosMock.get).toHaveBeenCalledWith(
expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v1/notifications?page=1'
);
});
it('#getNotifications', () => {
notificationsAPI.getNotifications(1);
expect(context.axiosMock.get).toHaveBeenCalledWith(
expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v1/notifications/1/notifications'
);
});
it('#getUnreadCount', () => {
notificationsAPI.getUnreadCount();
expect(context.axiosMock.get).toHaveBeenCalledWith(
expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v1/notifications/unread_count'
);
});
it('#read', () => {
notificationsAPI.read(48670, 'Conversation');
expect(context.axiosMock.post).toHaveBeenCalledWith(
expect(axiosMock.post).toHaveBeenCalledWith(
'/api/v1/notifications/read_all',
{
primary_actor_id: 'Conversation',
@@ -46,7 +61,7 @@ describe('#NotificationAPI', () => {
it('#readAll', () => {
notificationsAPI.readAll();
expect(context.axiosMock.post).toHaveBeenCalledWith(
expect(axiosMock.post).toHaveBeenCalledWith(
'/api/v1/notifications/read_all'
);
});
@@ -1,6 +1,5 @@
import reportsAPI from '../reports';
import ApiClient from '../ApiClient';
import describeWithAPIMock from './apiSpecHelper';
describe('#Reports API', () => {
it('creates correct instance', () => {
@@ -18,14 +17,30 @@ describe('#Reports API', () => {
expect(reportsAPI).toHaveProperty('getInboxReports');
expect(reportsAPI).toHaveProperty('getTeamReports');
});
describeWithAPIMock('API calls', context => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
window.axios = axiosMock;
});
afterEach(() => {
window.axios = originalAxios;
});
it('#getAccountReports', () => {
reportsAPI.getReports({
metric: 'conversations_count',
from: 1621103400,
to: 1621621800,
});
expect(context.axiosMock.get).toHaveBeenCalledWith('/api/v2/reports', {
expect(axiosMock.get).toHaveBeenCalledWith('/api/v2/reports', {
params: {
metric: 'conversations_count',
since: 1621103400,
@@ -38,20 +53,17 @@ describe('#Reports API', () => {
it('#getAccountSummary', () => {
reportsAPI.getSummary(1621103400, 1621621800);
expect(context.axiosMock.get).toHaveBeenCalledWith(
'/api/v2/reports/summary',
{
params: {
business_hours: undefined,
group_by: undefined,
id: undefined,
since: 1621103400,
timezone_offset: -0,
type: 'account',
until: 1621621800,
},
}
);
expect(axiosMock.get).toHaveBeenCalledWith('/api/v2/reports/summary', {
params: {
business_hours: undefined,
group_by: undefined,
id: undefined,
since: 1621103400,
timezone_offset: -0,
type: 'account',
until: 1621621800,
},
});
});
it('#getAgentReports', () => {
@@ -60,60 +72,48 @@ describe('#Reports API', () => {
to: 1621621800,
businessHours: true,
});
expect(context.axiosMock.get).toHaveBeenCalledWith(
'/api/v2/reports/agents',
{
params: {
since: 1621103400,
until: 1621621800,
business_hours: true,
},
}
);
expect(axiosMock.get).toHaveBeenCalledWith('/api/v2/reports/agents', {
params: {
since: 1621103400,
until: 1621621800,
business_hours: true,
},
});
});
it('#getLabelReports', () => {
reportsAPI.getLabelReports({ from: 1621103400, to: 1621621800 });
expect(context.axiosMock.get).toHaveBeenCalledWith(
'/api/v2/reports/labels',
{
params: {
since: 1621103400,
until: 1621621800,
},
}
);
expect(axiosMock.get).toHaveBeenCalledWith('/api/v2/reports/labels', {
params: {
since: 1621103400,
until: 1621621800,
},
});
});
it('#getInboxReports', () => {
reportsAPI.getInboxReports({ from: 1621103400, to: 1621621800 });
expect(context.axiosMock.get).toHaveBeenCalledWith(
'/api/v2/reports/inboxes',
{
params: {
since: 1621103400,
until: 1621621800,
},
}
);
expect(axiosMock.get).toHaveBeenCalledWith('/api/v2/reports/inboxes', {
params: {
since: 1621103400,
until: 1621621800,
},
});
});
it('#getTeamReports', () => {
reportsAPI.getTeamReports({ from: 1621103400, to: 1621621800 });
expect(context.axiosMock.get).toHaveBeenCalledWith(
'/api/v2/reports/teams',
{
params: {
since: 1621103400,
until: 1621621800,
},
}
);
expect(axiosMock.get).toHaveBeenCalledWith('/api/v2/reports/teams', {
params: {
since: 1621103400,
until: 1621621800,
},
});
});
it('#getConversationMetric', () => {
reportsAPI.getConversationMetric('account');
expect(context.axiosMock.get).toHaveBeenCalledWith(
expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v2/reports/conversations',
{
params: {
@@ -1,6 +1,5 @@
import teamsAPI from '../teams';
import ApiClient from '../ApiClient';
import describeWithAPIMock from './apiSpecHelper';
describe('#TeamsAPI', () => {
it('creates correct instance', () => {
@@ -14,17 +13,33 @@ describe('#TeamsAPI', () => {
expect(teamsAPI).toHaveProperty('addAgents');
expect(teamsAPI).toHaveProperty('updateAgents');
});
describeWithAPIMock('API calls', context => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
window.axios = axiosMock;
});
afterEach(() => {
window.axios = originalAxios;
});
it('#getAgents', () => {
teamsAPI.getAgents({ teamId: 1 });
expect(context.axiosMock.get).toHaveBeenCalledWith(
expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v1/teams/1/team_members'
);
});
it('#addAgents', () => {
teamsAPI.addAgents({ teamId: 1, agentsList: { user_ids: [1, 10, 21] } });
expect(context.axiosMock.post).toHaveBeenCalledWith(
expect(axiosMock.post).toHaveBeenCalledWith(
'/api/v1/teams/1/team_members',
{
user_ids: { user_ids: [1, 10, 21] },
@@ -38,7 +53,7 @@ describe('#TeamsAPI', () => {
teamId: 1,
agentsList,
});
expect(context.axiosMock.patch).toHaveBeenCalledWith(
expect(axiosMock.patch).toHaveBeenCalledWith(
'/api/v1/teams/1/team_members',
{
user_ids: agentsList,
@@ -26,7 +26,7 @@ code {
background: $color-background;
border-radius: var(--border-radius-large);
padding: $space-two;
@apply bg-slate-50 dark:bg-slate-600 text-slate-800 dark:text-slate-100;
@apply bg-slate-50 dark:bg-slate-700 text-slate-800 dark:text-slate-100;
}
}
@@ -9,87 +9,25 @@ body {
}
.app-wrapper {
@include full-height;
flex-grow: 0;
min-height: 0;
width: 100%;
@apply h-full flex-grow-0 min-h-0 w-full;
.button--fixed-top {
@apply fixed ltr:right-2 rtl:left-2 top-2 flex flex-row;
}
}
.banner + .app-wrapper {
// Reduce the height of the dashboard to make room for the banner.
// And causing the top right green-action button to be pushed down when scrolling.
@apply h-[calc(100%-48px)];
.button--fixed-top {
top: 5.6 * $space-one;
@apply top-14;
}
.off-canvas-content {
.button--fixed-top {
top: $space-small;
@apply top-2;
}
}
}
is-closed .app-root {
@include flex;
flex-direction: column;
}
.app-content {
@include flex;
@include full-height;
min-height: 0;
overflow: hidden;
}
.view-box {
@include full-height;
@include space-between-column;
height: 100vh;
margin: 0;
}
.view-panel {
flex-direction: column;
margin: 0;
overflow-y: auto;
padding: $space-normal;
}
.content-box {
overflow: auto;
padding: $space-normal;
}
.back-button {
@include flex;
align-items: center;
color: $color-woot;
cursor: pointer;
font-size: $font-size-default;
font-weight: $font-weight-normal;
margin-right: $space-normal;
&::before {
font-size: $font-size-large;
margin-right: $space-small;
vertical-align: text-bottom;
}
}
.button-spinner {
float: right;
}
.no-items-error-message {
@include flex;
@include full-height;
align-items: center;
flex-direction: column;
justify-content: center;
img {
max-width: $space-mega;
padding: $space-one;
}
}
@@ -1,20 +1,6 @@
.app-rtl--wrapper {
direction: rtl;
.header-section.back-button {
direction: initial;
margin-left: var(--space-normal);
margin-right: var(--space-smaller);
}
// Settings header action button
.button--fixed-top {
left: $space-small;
position: fixed;
right: unset;
top: $space-small;
}
// Woot Tabs
.tabs-title {
&:first-child {
@@ -71,9 +71,3 @@
align-items: center;
display: flex;
}
.button--fixed-top {
position: fixed;
right: var(--space-small);
top: var(--space-small);
}
@@ -81,11 +81,6 @@ $conv-header-height: 2.5rem;
$inbox-thumb-size: 3rem;
// Spinner
$spinkit-spinner-color: $color-white !default;
$spinkit-spinner-margin: 0 0 0 1rem !default;
$spinkit-size: 1rem !default;
// Snackbar default
$woot-snackbar-bg: #323232;
$woot-snackbar-button: #ffeb3b;
@@ -149,15 +149,11 @@
}
.multiselect-wrap--small {
.multiselect__tags,
.multiselect__input {
@apply items-center flex;
}
.multiselect__tags,
.multiselect__input,
.multiselect {
@apply bg-white dark:bg-slate-900 text-slate-800 dark:text-slate-100 rounded-[5px] text-sm h-10 min-h-[2.5rem];
@apply bg-white dark:bg-slate-900 text-slate-800 dark:text-slate-100 rounded-[5px] text-sm min-h-[2.5rem];
}
.multiselect__input {
@@ -165,11 +161,15 @@
}
.multiselect__single {
@apply items-center flex m-0 text-sm max-h-[2.375rem] text-slate-800 dark:text-slate-100 bg-white dark:bg-slate-900 py-1 px-0.5;
@apply items-center flex m-0 text-sm max-h-[2.375rem] text-slate-800 dark:text-slate-100 bg-white dark:bg-slate-900 py-3 px-0.5;
}
.multiselect__placeholder {
@apply m-0 py-1 px-0.5;
@apply m-0 py-2 px-0.5;
}
.multiselect__tag {
@apply py-[6px] my-[1px];
}
.multiselect__select {
@@ -181,9 +181,6 @@
@apply bg-transparent;
}
.multiselect__tags-wrap {
@apply flex-shrink-0;
}
}
.multiselect-wrap--medium {
@@ -9,7 +9,6 @@
@import 'shared/assets/stylesheets/border-radius';
@import 'variables';
@import '~spinkit/scss/spinners/7-three-bounce';
@import 'vue-multiselect/dist/vue-multiselect.min.css';
@import '~shared/assets/stylesheets/ionicons';
@@ -1,5 +1,5 @@
.button {
@apply items-center inline-flex h-10 mb-0;
@apply items-center inline-flex h-10 mb-0 gap-2;
.button__content {
@apply w-full;
@@ -14,17 +14,9 @@
@apply px-2 py-0;
}
.icon--emoji + .button__content {
@apply pl-2 rtl:pr-2 rtl:pl-0;
}
.icon--font + .button__content {
@apply pl-2 rtl:pr-2 rtl:pl-0;
}
// @TODDO - Remove after moving all buttons to woot-button
.icon + .button__content {
@apply pl-2 w-auto rtl:pr-2 rtl:pl-0;
@apply w-auto;
}
&.expanded {
@@ -157,18 +149,10 @@
// Sizes
&.tiny {
@apply h-6;
.icon + .button__content {
@apply pl-1 rtl:pr-1 rtl:pl-0;
}
}
&.small {
@apply h-8 pb-1 pt-1;
.icon + .button__content {
@apply pl-1 rtl:pr-1 rtl:pl-0;
}
}
&.large {
@@ -2,10 +2,6 @@
transition: box-shadow 0.35s $swift-ease-out-function,
height 2s $swift-ease-out-function;
&.is-focused {
box-shadow: var(--shadow);
}
.reply-box__top {
.icon {
color: $medium-gray;
@@ -47,8 +43,6 @@
}
&.is-private {
@apply bg-yellow-100 dark:bg-yellow-800;
.reply-box__top {
@apply bg-yellow-100 dark:bg-yellow-800;
@@ -76,13 +76,13 @@ table {
.ve-pagination-goto {
@apply text-slate-600 dark:text-slate-200;
.ve-pagination-goto-input {
@apply bg-white dark:bg-slate-900 text-slate-600 dark:text-slate-200;
}
}
.ve-pagination-li {
@apply bg-white dark:bg-slate-900 text-slate-600 dark:text-slate-200 border-slate-75 dark:border-slate-700;
}
.ve-pagination-goto-input {
@apply bg-white dark:bg-slate-900 text-slate-600 dark:text-slate-200;
}
}
@@ -31,7 +31,7 @@
</template>
<script>
import EmojiOrIcon from 'shared/components/EmojiOrIcon';
import EmojiOrIcon from 'shared/components/EmojiOrIcon.vue';
export default {
components: {
@@ -10,7 +10,8 @@
<div
class="flex items-center justify-between py-0 px-4"
:class="{
'pb-3 border-b border-slate-75 dark:border-slate-700': hasAppliedFiltersOrActiveFolders,
'pb-3 border-b border-slate-75 dark:border-slate-700':
hasAppliedFiltersOrActiveFolders,
}"
>
<div class="flex max-w-[85%] justify-center items-center">
@@ -24,9 +25,7 @@
v-if="!hasAppliedFiltersOrActiveFolders"
class="p-1 my-0.5 mx-1 rounded-md capitalize bg-slate-50 dark:bg-slate-800 text-xxs text-slate-600 dark:text-slate-300"
>
{{
this.$t(`CHAT_LIST.CHAT_STATUS_FILTER_ITEMS.${activeStatus}.TEXT`)
}}
{{ $t(`CHAT_LIST.CHAT_STATUS_FILTER_ITEMS.${activeStatus}.TEXT`) }}
</span>
</div>
<div class="flex items-center gap-1">
@@ -193,17 +192,17 @@
<script>
import { mapGetters } from 'vuex';
import ConversationAdvancedFilter from './widgets/conversation/ConversationAdvancedFilter';
import ConversationBasicFilter from './widgets/conversation/ConversationBasicFilter';
import ChatTypeTabs from './widgets/ChatTypeTabs';
import ConversationCard from './widgets/conversation/ConversationCard';
import ConversationAdvancedFilter from './widgets/conversation/ConversationAdvancedFilter.vue';
import ConversationBasicFilter from './widgets/conversation/ConversationBasicFilter.vue';
import ChatTypeTabs from './widgets/ChatTypeTabs.vue';
import ConversationCard from './widgets/conversation/ConversationCard.vue';
import timeMixin from '../mixins/time';
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
import conversationMixin from '../mixins/conversations';
import wootConstants from 'dashboard/constants/globals';
import advancedFilterTypes from './widgets/conversation/advancedFilterItems';
import filterQueryGenerator from '../helper/filterQueryGenerator.js';
import AddCustomViews from 'dashboard/routes/dashboard/customviews/AddCustomViews';
import AddCustomViews from 'dashboard/routes/dashboard/customviews/AddCustomViews.vue';
import DeleteCustomViews from 'dashboard/routes/dashboard/customviews/DeleteCustomViews.vue';
import ConversationBulkActions from './widgets/conversation/conversationBulkActions/Index.vue';
import alertMixin from 'shared/mixins/alertMixin';
@@ -642,10 +641,8 @@ export default {
},
handleKeyEvents(e) {
if (hasPressedAltAndJKey(e)) {
const {
allConversations,
activeConversationIndex,
} = this.getKeyboardListenerParams();
const { allConversations, activeConversationIndex } =
this.getKeyboardListenerParams();
if (activeConversationIndex === -1) {
allConversations[0].click();
}
@@ -61,7 +61,7 @@
>
<a
v-if="isAttributeTypeLink"
:href="value"
:href="hrefURL"
target="_blank"
rel="noopener noreferrer"
class="value inline-block rounded-sm mb-0 break-all py-0.5 px-1"
@@ -188,6 +188,9 @@ export default {
urlValue() {
return isValidURL(this.value) ? this.value : '---';
},
hrefURL() {
return isValidURL(this.value) ? this.value : '';
},
notAttributeTypeCheckboxAndList() {
return !this.isAttributeTypeCheckbox && !this.isAttributeTypeList;
},
@@ -13,10 +13,10 @@
/>
<div class="flex flex-row justify-end gap-2 py-2 px-0 w-full">
<woot-button variant="clear" @click.prevent="onClose">
{{ this.$t('CONVERSATION.CUSTOM_SNOOZE.CANCEL') }}
{{ $t('CONVERSATION.CUSTOM_SNOOZE.CANCEL') }}
</woot-button>
<woot-button>
{{ this.$t('CONVERSATION.CUSTOM_SNOOZE.APPLY') }}
{{ $t('CONVERSATION.CUSTOM_SNOOZE.APPLY') }}
</woot-button>
</div>
</form>
@@ -1,6 +1,6 @@
<template>
<div class="text--container">
<woot-button size="small" class=" button--text" @click="onCopy">
<woot-button size="small" class="button--text" @click="onCopy">
{{ $t('COMPONENTS.CODE.BUTTON_TEXT') }}
</woot-button>
<woot-button
@@ -66,9 +66,9 @@ export default {
'right-aligned': 'right-aligned',
};
return `modal-mask skip-context-menu ${modalClassNameMap[
this.modalType
] || ''}`;
return `modal-mask skip-context-menu ${
modalClassNameMap[this.modalType] || ''
}`;
},
},
mounted() {
@@ -2,7 +2,8 @@
<div
class="ml-0 mr-0 flex pt-0 pr-4 pb-4 pl-0"
:class="{
'pt-4 border-b border-solid border-slate-50 dark:border-slate-700/30': showBorder,
'pt-4 border-b border-solid border-slate-50 dark:border-slate-700/30':
showBorder,
}"
>
<div class="w-[30%] min-w-0 max-w-[30%] pr-12">
@@ -10,7 +10,7 @@
</template>
<script>
import WootSnackbar from './Snackbar';
import WootSnackbar from './Snackbar.vue';
export default {
components: {
@@ -72,10 +72,8 @@ export default {
const { custom_attributes: subscription } = account;
if (!subscription) return EMPTY_SUBSCRIPTION_INFO;
const {
subscription_status: status,
subscription_ends_on: endsOn,
} = subscription;
const { subscription_status: status, subscription_ends_on: endsOn } =
subscription;
return { status, endsOn: new Date(endsOn) };
},
@@ -1,4 +1,4 @@
const semver = require('semver');
import semver from 'semver';
export const hasAnUpdateAvailable = (latestVersion, currentVersion) => {
if (!semver.valid(latestVersion)) {
@@ -13,7 +13,7 @@
</template>
<script>
import Spinner from 'shared/components/Spinner';
import Spinner from 'shared/components/Spinner.vue';
export default {
components: {
@@ -10,7 +10,7 @@
:is-loading="isLoading"
@click="onCmdResolveConversation"
>
{{ this.$t('CONVERSATION.HEADER.RESOLVE_ACTION') }}
{{ $t('CONVERSATION.HEADER.RESOLVE_ACTION') }}
</woot-button>
<woot-button
v-else-if="isResolved"
@@ -21,7 +21,7 @@
:is-loading="isLoading"
@click="onCmdOpenConversation"
>
{{ this.$t('CONVERSATION.HEADER.REOPEN_ACTION') }}
{{ $t('CONVERSATION.HEADER.REOPEN_ACTION') }}
</woot-button>
<woot-button
v-else-if="showOpenButton"
@@ -31,7 +31,7 @@
:is-loading="isLoading"
@click="onCmdOpenConversation"
>
{{ this.$t('CONVERSATION.HEADER.OPEN_ACTION') }}
{{ $t('CONVERSATION.HEADER.OPEN_ACTION') }}
</woot-button>
<woot-button
v-if="showAdditionalActions"
@@ -57,7 +57,7 @@
icon="snooze"
@click="() => openSnoozeModal()"
>
{{ this.$t('CONVERSATION.RESOLVE_DROPDOWN.SNOOZE_UNTIL') }}
{{ $t('CONVERSATION.RESOLVE_DROPDOWN.SNOOZE_UNTIL') }}
</woot-button>
</woot-dropdown-item>
<woot-dropdown-item v-if="!isPending">
@@ -68,7 +68,7 @@
icon="book-clock"
@click="() => toggleStatus(STATUS_TYPE.PENDING)"
>
{{ this.$t('CONVERSATION.RESOLVE_DROPDOWN.MARK_PENDING') }}
{{ $t('CONVERSATION.RESOLVE_DROPDOWN.MARK_PENDING') }}
</woot-button>
</woot-dropdown-item>
</woot-dropdown-menu>
@@ -90,7 +90,7 @@ import { getUnixTime } from 'date-fns';
import { mapGetters } from 'vuex';
import { mixin as clickaway } from 'vue-clickaway';
import alertMixin from 'shared/mixins/alertMixin';
import CustomSnoozeModal from 'dashboard/components/CustomSnoozeModal';
import CustomSnoozeModal from 'dashboard/components/CustomSnoozeModal.vue';
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
import {
hasPressedAltAndEKey,
@@ -49,11 +49,11 @@
import { mapGetters } from 'vuex';
import { mixin as clickaway } from 'vue-clickaway';
import alertMixin from 'shared/mixins/alertMixin';
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem';
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu';
import WootDropdownHeader from 'shared/components/ui/dropdown/DropdownHeader';
import WootDropdownDivider from 'shared/components/ui/dropdown/DropdownDivider';
import AvailabilityStatusBadge from '../widgets/conversation/AvailabilityStatusBadge';
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
import WootDropdownHeader from 'shared/components/ui/dropdown/DropdownHeader.vue';
import WootDropdownDivider from 'shared/components/ui/dropdown/DropdownDivider.vue';
import AvailabilityStatusBadge from '../widgets/conversation/AvailabilityStatusBadge.vue';
import wootConstants from 'dashboard/constants/globals';
const { AVAILABILITY_STATUS_KEYS } = wootConstants;
@@ -35,8 +35,8 @@ import adminMixin from '../../mixins/isAdmin';
import { getSidebarItems } from './config/default-sidebar';
import alertMixin from 'shared/mixins/alertMixin';
import PrimarySidebar from './sidebarComponents/Primary';
import SecondarySidebar from './sidebarComponents/Secondary';
import PrimarySidebar from './sidebarComponents/Primary.vue';
import SecondarySidebar from './sidebarComponents/Secondary.vue';
import {
hasPressedAltAndCKey,
hasPressedAltAndRKey,
@@ -34,8 +34,8 @@
<woot-submit-button
:disabled="
$v.accountName.$invalid ||
$v.accountName.$invalid ||
uiFlags.isCreating
$v.accountName.$invalid ||
uiFlags.isCreating
"
:button-text="$t('CREATE_ACCOUNT.FORM.SUBMIT')"
:loading="uiFlags.isCreating"
@@ -16,7 +16,7 @@
</template>
<script>
import { mapGetters } from 'vuex';
import Thumbnail from '../../widgets/Thumbnail';
import Thumbnail from '../../widgets/Thumbnail.vue';
export default {
components: {
@@ -3,7 +3,8 @@
<button
class="text-slate-600 dark:text-slate-100 w-10 h-10 my-2 flex items-center justify-center rounded-lg hover:bg-slate-25 dark:hover:bg-slate-700 dark:hover:text-slate-100 hover:text-slate-600 relative"
:class="{
'bg-woot-50 dark:bg-slate-800 text-woot-500 hover:bg-woot-50': isNotificationPanelActive,
'bg-woot-50 dark:bg-slate-800 text-woot-500 hover:bg-woot-50':
isNotificationPanelActive,
}"
@click="openNotificationPanel"
>
@@ -3,7 +3,7 @@
<div
v-if="show"
v-on-clickaway="onClickAway"
class="left-3 rtl:left-auto rtl:right-3 bottom-16 w-64 absolute z-20 rounded-md shadow-xl bg-white dark:bg-slate-800 py-2 px-2 border border-slate-25 dark:border-slate-700"
class="left-3 rtl:left-auto rtl:right-3 bottom-16 w-64 absolute z-30 rounded-md shadow-xl bg-white dark:bg-slate-800 py-2 px-2 border border-slate-25 dark:border-slate-700"
:class="{ 'block visible': show }"
>
<availability-status />
@@ -109,9 +109,9 @@
import { mixin as clickaway } from 'vue-clickaway';
import { mapGetters } from 'vuex';
import Auth from '../../../api/auth';
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem';
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu';
import AvailabilityStatus from 'dashboard/components/layout/AvailabilityStatus';
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
import AvailabilityStatus from 'dashboard/components/layout/AvailabilityStatus.vue';
export default {
components: {
@@ -39,13 +39,14 @@
</div>
</template>
<script>
import Logo from './Logo';
import PrimaryNavItem from './PrimaryNavItem';
import OptionsMenu from './OptionsMenu';
import AgentDetails from './AgentDetails';
import NotificationBell from './NotificationBell';
import Logo from './Logo.vue';
import PrimaryNavItem from './PrimaryNavItem.vue';
import OptionsMenu from './OptionsMenu.vue';
import AgentDetails from './AgentDetails.vue';
import NotificationBell from './NotificationBell.vue';
import wootConstants from 'dashboard/constants/globals';
import { frontendURL } from 'dashboard/helper/URLHelper';
import { ACCOUNT_EVENTS } from '../../../helper/AnalyticsHelper/events';
export default {
components: {
@@ -99,6 +100,7 @@ export default {
window.$chatwoot.toggle();
},
openNotificationPanel() {
this.$track(ACCOUNT_EVENTS.OPENED_NOTIFICATIONS);
this.$emit('open-notification-panel');
},
},
@@ -9,7 +9,8 @@
class="font-medium h-7 my-1 hover:bg-slate-25 hover:text-bg-50 flex items-center px-2 rounded-md dark:hover:bg-slate-800"
:class="{
'bg-woot-25 dark:bg-slate-800': isActive,
'text-ellipsis overflow-hidden whitespace-nowrap max-w-full': shouldTruncate,
'text-ellipsis overflow-hidden whitespace-nowrap max-w-full':
shouldTruncate,
}"
@click="navigate"
>
@@ -44,7 +45,8 @@
class="text-sm text-slate-700 dark:text-slate-100"
:class="{
'text-woot-500 dark:text-woot-500': isActive,
'text-ellipsis overflow-hidden whitespace-nowrap max-w-full': shouldTruncate,
'text-ellipsis overflow-hidden whitespace-nowrap max-w-full':
shouldTruncate,
}"
>
{{ label }}
@@ -35,7 +35,8 @@
:class="{
'text-slate-300 dark:text-slate-600': isCountZero && !isActiveView,
'text-slate-600 dark:text-slate-50': !isCountZero && !isActiveView,
'bg-woot-75 dark:bg-woot-200 text-woot-600 dark:text-woot-600': isActiveView,
'bg-woot-75 dark:bg-woot-200 text-woot-600 dark:text-woot-600':
isActiveView,
'bg-slate-50 dark:bg-slate-700': !isActiveView,
}"
>
@@ -97,7 +98,7 @@ import {
getInboxWarningIconClass,
} from 'dashboard/helper/inbox';
import SecondaryChildNavItem from './SecondaryChildNavItem';
import SecondaryChildNavItem from './SecondaryChildNavItem.vue';
import {
isOnMentionsView,
isOnUnattendedView,
@@ -1,5 +1,8 @@
<template>
<div class="banner" :class="bannerClasses">
<div
class="banner flex items-center h-12 gap-4 text-white dark:text-white text-xs py-3 px-4 justify-center"
:class="bannerClasses"
>
<span class="banner-message">
{{ bannerMessage }}
<a
@@ -96,8 +99,6 @@ export default {
<style lang="scss" scoped>
.banner {
@apply flex items-center gap-4 text-white dark:text-white text-xs py-3 px-4 justify-center;
&.primary {
@apply bg-woot-500 dark:bg-woot-500;
.banner-action__button {
@@ -53,16 +53,24 @@ export default {
const createdTimeDiff = Date.now() - this.createdAtTimestamp * 1000;
const isBeforeAMonth = createdTimeDiff > DAY_IN_MILLI_SECONDS * 30;
return !isBeforeAMonth
? `Created ${this.createdAtTimeAgo}`
: `Created at: ${this.dateFormat(this.createdAtTimestamp)}`;
? `${this.$t('CHAT_LIST.CHAT_TIME_STAMP.CREATED.LATEST')} ${
this.createdAtTimeAgo
}`
: `${this.$t(
'CHAT_LIST.CHAT_TIME_STAMP.CREATED.OLDEST'
)} ${this.dateFormat(this.createdAtTimestamp)}`;
},
lastActivity() {
const lastActivityTimeDiff =
Date.now() - this.lastActivityTimestamp * 1000;
const isNotActive = lastActivityTimeDiff > DAY_IN_MILLI_SECONDS * 30;
return !isNotActive
? `Last activity ${this.lastActivityAtTimeAgo}`
: `Last activity: ${this.dateFormat(this.lastActivityTimestamp)}`;
? `${this.$t('CHAT_LIST.CHAT_TIME_STAMP.LAST_ACTIVITY.ACTIVE')} ${
this.lastActivityAtTimeAgo
}`
: `${this.$t(
'CHAT_LIST.CHAT_TIME_STAMP.LAST_ACTIVITY.NOT_ACTIVE'
)} ${this.dateFormat(this.lastActivityTimestamp)}`;
},
tooltipText() {
return `${this.createdAt}
@@ -28,8 +28,8 @@
</button>
</template>
<script>
import Spinner from 'shared/components/Spinner';
import EmojiOrIcon from 'shared/components/EmojiOrIcon';
import Spinner from 'shared/components/Spinner.vue';
import EmojiOrIcon from 'shared/components/EmojiOrIcon.vue';
export default {
name: 'WootButton',

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