Compare commits

..
Author SHA1 Message Date
Shivam Mishra ae075afb67 test: add more cases 2023-09-20 18:09:19 +05:30
Shivam Mishra e7e34f34b0 test: add more signature cases 2023-09-20 18:08:21 +05:30
Shivam Mishra 9da0258abc fix: test 2023-09-20 18:06:18 +05:30
Shivam Mishra 5ca28f0916 chore: update test data 2023-09-20 18:04:12 +05:30
Shivam Mishra 1ed60066fb fix: importing article styles 2023-09-20 18:03:13 +05:30
Shivam Mishra d18f3e283a feat: remove mock 2023-09-20 18:03:12 +05:30
Shivam Mishra d489f7ceb0 feat: get prosemirror-schema from npm 2023-09-20 18:03:12 +05:30
Sivin VargheseandGitHub 2d46aa0a0a Merge branch 'develop' into fix/signature-bullet-formatting 2023-09-20 17:29:45 +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
Shivam Mishra 695bcc32c1 test: mock prosemirror exports 2023-09-20 16:17:37 +05:30
Sivin VargheseandGitHub 4cc0d89cbd Merge branch 'develop' into fix/signature-bullet-formatting 2023-09-20 15:14:22 +05:30
Shivam Mishra 54769850e8 refactor: remove manual clean methods 2023-09-20 13:54:24 +05:30
Pranav Raj SandGitHub f1b556d4a3 fix: Downcase content filter values to fix the query (#7942) 2023-09-20 13:14:00 +05:30
Shivam MishraandGitHub 986e89b7c0 Merge branch 'develop' into fix/signature-bullet-formatting 2023-09-20 13:09:17 +05:30
Shivam Mishra 607e58465a fix: safer access to textarea ref 2023-09-20 12:51:50 +05:30
Shivam Mishra 47921bca3d fix: safer access to textarea 2023-09-20 12:51:00 +05:30
Shivam Mishra adf67b79ce feat: transform markdown to common mark before appending 2023-09-20 12:50:28 +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 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
Muhsin KelothandGitHub b89c917198 feat: Add CTAs for AI features (#7538) 2023-08-16 08:39:41 +05:30
7b8a3fcae0 feat: auditlog for team and inbox member updates (#7516)
- adds an audit log when an agent is added or removed from a team
- adds an audit log when an agent is added or removed from an inbox

Co-authored-by: Sojan Jose <sojan@pepalo.com>
2023-08-15 19:55:19 -07:00
Sojan JoseandGitHub 2df83276e0 chore: Ability to disable rack attack on widget endpoints (#7729) 2023-08-15 17:44:25 -07:00
7b68a7639f fix: Set the default value to false for the checkbox custom attribute (#7561)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2023-08-15 16:32:06 -07:00
e3b8c1fbb5 fix: Include waiting on agent conversations to unattended view (#7667)
Updating the `unattended` tab to include conversations where the customer responded and is awaiting an agent's response.

Previously it showed only the conversations where the first response was pending.

Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2023-08-15 15:09:10 -07:00
Pranav Raj SandGitHub 310c127693 chore: Update translations (#7728) 2023-08-15 13:47:38 -07:00
Pranav Raj SandGitHub 37c0f1306c fix: Fix breaking storybook build (#7723) 2023-08-14 20:19:02 -07:00
396ef02b0c feat: Improve the initial screen placeholder (#7708)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2023-08-14 17:50:34 -07:00
Pranav Raj SandGitHub ebefb2e201 fix: Consider the emails where in-reply-to header has multiple values (#7715)
- Return the first message id for now to avoid the errors and subsequently missing the message.
- Use .last instead of .first to avoid expensive query.
- Fix array response in response bot.

Fixes:  https://linear.app/chatwoot/issue/CW-2358/activerecordstatementinvalid-pgdatatypemismatch-error-argument-of-and
2023-08-11 17:53:57 -07:00
Pranav Raj SandGitHub 6f09f20991 feat: Update bot typing indicator based on the conversation status (#7714)
- Show the indicator if the last message is incoming and the conversation is in pending status.
- Remove list of articles displayed in the bot response.
2023-08-11 16:41:01 -07:00
cfe86d9c06 fix: Create new slack thread if the thread identifier changes (#7702)
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
2023-08-11 14:19:49 +05:30
Shivam MishraandGitHub 84240e197a fix: Summary prompt test (#7709) 2023-08-11 13:42:27 +05:30
Pranav Raj SandGitHub bafff90f18 chore: Add account_id scope for conversation.messages (#7707) 2023-08-10 23:02:53 -07:00
Pranav Raj SandGitHub 961d810645 Revert "feat: Show popular articles on widget home" (#7706) 2023-08-10 22:27:51 -07:00
Pranav Raj SandGitHub 1a06bfd69c fix: Force account_id in message filters (#7705) 2023-08-10 21:58:25 -07:00
Pranav Raj SandGitHub 1cac2e86c3 fix: Limit count query to return only 10 values (#7704) 2023-08-10 19:30:02 -07:00
Pranav Raj SandGitHub 04287a0924 fix: Rename channel_list to channels to avoid "no implicit conversion of nil" exception (#7700) 2023-08-09 16:11:51 -07:00
c1c3a62412 fix: Improve reply prompt (#7588)
Co-authored-by: Sojan Jose <sojan@chatwoot.com>
2023-08-09 10:25:37 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
b5dec067a1 chore(deps): bump commonmarker from 0.23.9 to 0.23.10 (#7695)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-08 14:25:42 -07:00
Sivin VargheseandGitHub f96f166196 fix: Default timezone is not working (#7692) 2023-08-08 21:30:29 +05:30
Sivin VargheseandGitHub 9ed701065e fix: Dark mode color fixes (#7693) 2023-08-08 21:07:15 +05:30
Vishnu NarayananandGitHub 180befbd42 feat: add gh action to check logging percentage (#7688) 2023-08-08 15:51:46 +05:30
Pranav Raj SandGitHub 4baa4363f4 fix: Update unread_count being 0 if agent has not seen the conversation (#7690) 2023-08-07 18:50:31 -07:00
Pranav Raj SandGitHub 1c8f41f387 fix: Rename logo-dark -> logo_dark to match the installation config (#7689)
Fix: #7676
2023-08-07 14:30:16 -07:00
7f0ca8b15d fix: Security upgrade rack-mini-profiler from 3.1.0 to 3.1.1 [Snyk] (#7657)
Co-authored-by: snyk-bot <snyk-bot@snyk.io>
2023-08-07 14:09:28 -07:00
Sojan JoseandGitHub 4c75bb68f4 chore: Fix OpenAI sentry errors (#7687)
- add check for nil condition
- add logs for openAI
2023-08-07 14:08:26 -07:00
Vishnu NarayananandGitHub ed82eb2932 refactor: remove beta label from automation, bots and macros (#7683)
Fixes: https://linear.app/chatwoot/issue/CW-2334/remove-beta-label-from-everything
2023-08-07 11:15:24 -07:00
Sivin VargheseandGitHub b62166a18a fix: Cannot read properties of undefined (#7682) 2023-08-07 20:49:13 +05:30
bf2b75b281 feat: Add message timestamp to the conversation transcript emails (#7648)
Co-authored-by: Cristian Duta <Cristian.Duta@ti8m.ch>
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2023-08-04 17:21:14 -07:00
c084ad5a68 [Snyk] Security upgrade lograge from 0.12.0 to 0.13.0 (#7635)
Co-authored-by: snyk-bot <snyk-bot@snyk.io>
2023-08-04 16:56:11 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Sojan Jose
dd3e658f19 chore(deps): bump semver from 7.3.5 to 7.5.2 (#7383)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2023-08-04 16:53:30 -07:00
453d7ad9fc fix: Update options interactive messages formatting in web widget (#7437)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2023-08-04 16:52:32 -07:00
Pranav Raj SandGitHub 47b6967dad fix: Update account data seeder to avoid invalid information (#7673)
- The message sender was incorrect. Incoming messages were created under the contact's name, and outgoing messages were created under the user's name instead of the reverse.
- The seed user's email address was incorrect in the message data.
- The Sendmail configuration overrode the Letter Opener config which made it difficult to test the email.
- This PR also fixes an ESLint lint issue on develop.
2023-08-04 16:32:54 -07:00
Sojan JoseandGitHub d06cf27800 chore: Specs for pageCrawler Service (#7632) 2023-08-04 15:44:51 -07:00
Sivin VargheseandGitHub 7f6a3b781f fix: Links are not readable in private notes dark mode (#7671) 2023-08-05 00:10:10 +05:30
fde2b180fe feat: Update design for campaign (#7668)
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2023-08-04 11:39:59 -07:00
Sivin VargheseandGitHub 2a1662c781 fix: Edit custom views folder (#7670) 2023-08-04 13:54:21 +05:30
10d6e9551d feat: Add the option to toggle the dark/light color-scheme (#7662)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2023-08-03 12:21:45 -07:00
Sivin VargheseandGitHub 69d46f278a feat: Dark mode improvements (#7658) 2023-08-03 12:05:22 -07:00
Muhsin KelothandGitHub 0921a7c817 fix: Enable cmd bar actions for all conversation routes (#7664) 2023-08-03 11:40:03 -07:00
Muhsin KelothandGitHub 2a73f124ca feat: Add analytics event for command bar usage (#7659) 2023-08-02 13:45:26 +05:30
Sojan JoseandGitHub ef33c60652 chore: Bump up node version (#7653)
Bump node version to 16.20.1.
 
ref: #6629
2023-08-01 12:48:02 -07:00
Pranav Raj SandGitHub 3a547de909 fix: Update the default status of the hooks to enabled (#7652)
In the recent Slack integration update, we made changes to how hooks are handled. Now, hooks require an additional check to be enabled. By default, new hooks are created in a disabled state, which might lead to issues with the hooks not being executed as expected.

This migration updates the status attribute of hooks to have 'enabled' as the default value, ensuring that new hooks are enabled by default and can function properly.
2023-08-01 12:14:47 -07:00
Shivam MishraandGitHub 62e9fc1bc5 feat: update color palette [CW-2293] (#7617) 2023-08-01 21:34:10 +05:30
Nithin David ThomasandGitHub e052a061f4 feat: Show popular articles on widget home (#7604) 2023-08-01 21:32:44 +05:30
LiamandGitHub 9efadf8804 fix: Update permitted_attributes to support the latest version of administrate (#7644)
This fixes an issue where permitted_attributes throws an error of ArgumentError (wrong number of arguments (given 1, expected 0)):

This is because we now bring in the latest version of administrate and administrate changes the way permitted_attributes works; it now passes an action parameter to permitted_attributes. This means we need to update permitted_attributes to accept that new parameter.

Fixes: #7592
2023-07-31 11:53:44 -07:00
Muhsin KelothandGitHub c8a40932cc chore: Add June event for AI reply suggest dismissal (#7601) 2023-07-31 13:20:47 +05:30
Pranav Raj SandGitHub 9ddd428935 feat: Update the slack integration-flow to allow users to select the channel (#7637) 2023-07-28 14:50:30 -07:00
Sojan JoseandGitHub 4d8ba0148c fix: Use Display name for campaigns (#7631)
fixes: #4299
2023-07-27 17:58:19 +03:00
65c4ad70e1 refactor: improve logging for channel callback controllers (#7555)
Co-authored-by: Tejaswini Chile <tejaswini@chatwoot.com>
2023-07-27 17:10:39 +05:30
9636478c2a chore: Improve logging for ip look up and audit logs [CW-2145] (#7512)
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2023-07-27 16:48:30 +05:30
Nic HippenmeyerandGitHub 65190422c4 fix: Rename unread_since scope (#7267)
- This renames the unread_since scope to created_since, which more accurately describes what the scope returns.
- The EXTRACT(EPOCH FROM created_at) > (?) clause was also simplified and rewritten as created_at > ?, which is equivalent:
2023-07-27 12:29:21 +03:00
Jordan BroughandGitHub a7bc855486 refactor: Remove deprecated "belongs_to" relationships in Message (#7434)
- Remove the deprecated columns in message model
2023-07-27 11:41:34 +03:00
Shivam MishraandGitHub ff97536095 feat(perf): contact page loading speed (#7629) 2023-07-27 13:32:22 +05:30
Sivin VargheseandGitHub fcf0ba12d0 feat: Dark Mode (PR -12) (#7590) 2023-07-27 12:28:06 +05:30
Shivam MishraandGitHub 2cc82af344 fix: apply event name (#7628) 2023-07-27 12:11:57 +05:30
c00855fce4 fix: Skip sentiment analysis for blank text (#7580)
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2023-07-27 12:11:48 +05:30
2d07577731 fix: [Snyk] Security upgrade rack-attack from 6.6.1 to 6.7.0 (#7625)
Co-authored-by: snyk-bot <snyk-bot@snyk.io>
2023-07-27 09:06:56 +03:00
BernieandGitHub 4c98006837 fix: Fix spelling mistakes in the copy (#7627) 2023-07-26 21:48:28 -07:00
Nithin David ThomasandGitHub 89e09857af feat: Add store and API to support articles in widget (#7616) 2023-07-26 14:08:27 -07:00
Shivam MishraandGitHub 12c338364e fix: sentry issues [CW-2222][CW-2232] (#7591) 2023-07-26 23:37:37 +05:30
Sojan JoseandGitHub 846f520ad2 feat: toggle typing and update last seen endpoints for client apis (#7621)
- Add toggle_typing endpoint for client APIs
- Adds update last seen endpoint for client APIs 

Fixes: #7581
2023-07-26 20:40:48 +03:00
Sojan JoseandGitHub 6cbe1ed911 chore: Disable message hooks for conversations without incoming message (#7620)
When using client APIs to create conversations and auto-assignment is turned on, welcome messages were getting triggered. This PR disable the behaviour and ensure template hooks are triggered only if there are incoming messages present. 

Fixes: https://linear.app/chatwoot/issue/CW-2187
2023-07-26 20:38:49 +03:00
LiamandGitHub 6c1ee4d965 fix: Contacts page sort by created_at [CW-2262] (#7584)
Fixes the contacts page sort by created_at.
fixes: #7577
2023-07-26 18:37:30 +03:00
OMAR.AandGitHub 1d8341504a fix: contacts count on filter (#7446)
- Fixes the wrong count shown during the contact filter when contact has multiple labels associated.
2023-07-26 17:31:04 +03:00
Shivam MishraandGitHub f8ae6cd95c fix: no method error when conversation is nil (#7566) 2023-07-26 17:40:41 +05:30
LiamandGitHub 63dd2c5a93 fix: Avoid profile options menu overlapping with message tabs (#7615) 2023-07-25 21:55:57 -07:00
Shivam MishraandSojan 1c47478104 fix: undefined method update_cache_key for nil:NilClass (#7568) 2023-07-25 13:00:34 +03:00
Shivam MishraandSojan 39f14bebbd fix: undefined method first for nil:NilClass (#7567)
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2023-07-25 13:00:21 +03:00
LiamandGitHub 4b27fdf4db fix: Increase the out of office message length to 10_000 (#7583) 2023-07-24 20:29:04 -07:00
Pranav Raj SandGitHub a6a0e78bbe feat: Sort articles based on views (#7599) 2023-07-24 20:27:43 -07:00
703e19304d feat: Components to render articles in widget home (#7596)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2023-07-24 16:45:55 -07:00
Pranav Raj SandGitHub fa7bbdb0b3 feat: Add support for plain design help center layout (#7598) 2023-07-25 05:13:52 +05:30
Nithin David ThomasandGitHub d837065545 chore: Update storybook settings to widget stories (#7595) 2023-07-24 16:28:20 -07:00
2a6f3356c9 chore: Show error message if the AI process APIs fails (#7560)
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
2023-07-24 19:15:34 +05:30
c83105ce4f feat: dismiss label suggestions only for 24 hours (#7579)
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2023-07-24 17:51:09 +05:30
1220 changed files with 31364 additions and 9307 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_IP_LIMIT=3000
# ENABLE_RACK_ATTACK_WIDGET_API=true
## Running chatwoot as an API only server
## setting this value to true will disable the frontend dashboard endpoints
@@ -0,0 +1,55 @@
name: Log Lines Percentage Check
on:
pull_request:
branches:
- develop
jobs:
log_lines_check:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Check for log lines and calculate percentage
run: |
# Define the log line pattern
LOG_LINE_PATTERN="Rails\.logger"
# Get the list of changed files in the pull request
CHANGED_FILES=$(git diff --name-only)
# Initialize a flag to track if any files have insufficient log lines
INSUFFICIENT_LOGS=0
for file in $CHANGED_FILES; do
if [[ $file =~ \.rb$ && ! $file =~ _spec\.rb$ ]]; then
# Count the total number of lines in the file
total_lines=$(wc -l < "$file")
# Count the number of log lines in the file
log_lines=$(grep -c "$LOG_LINE_PATTERN" "$file")
# Calculate the percentage of log lines
if [ "$total_lines" -gt 0 ]; then
percentage=$(awk "BEGIN { pc=100*${log_lines}/${total_lines}; i=int(pc); print (pc-i<0.5)?i:i+1 }")
else
percentage=0
fi
# Check if the percentage is less than 5%
if [ "$percentage" -lt 5 ]; then
echo "Error: Log lines percentage is less than 5% ($percentage%) in $file. Please add more log lines using Rails.logger statements."
INSUFFICIENT_LOGS=1
else
echo "Log lines percentage is $percentage% in $file. Code looks good!"
fi
fi
done
# If any files have insufficient log lines, fail the action
if [ "$INSUFFICIENT_LOGS" -eq 1 ]; then
exit 1
fi
+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
+2 -1
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
+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 @@
14.17.4
20.5.1
+4 -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:
@@ -73,7 +69,7 @@ Rails/ApplicationController:
- 'app/controllers/survey/responses_controller.rb'
Rails/FindEach:
Enabled: true
Include:
Include:
- 'app/**/*.rb'
Rails/CompactBlank:
Enabled: false
@@ -189,7 +185,6 @@ RSpec/IndexedLet:
RSpec/NamedSubject:
Enabled: false
# we should bring this down
RSpec/MultipleMemoizedHelpers:
Max: 14
+13 -1
View File
@@ -23,6 +23,18 @@ module.exports = {
},
'@storybook/addon-links',
'@storybook/addon-essentials',
{
/**
* Fix Storybook issue with PostCSS@8
* @see https://github.com/storybookjs/storybook/issues/12668#issuecomment-773958085
*/
name: '@storybook/addon-postcss',
options: {
postcssLoaderOptions: {
implementation: require('postcss'),
},
},
},
],
webpackFinal: config => {
const newConfig = {
@@ -35,7 +47,7 @@ module.exports = {
newConfig.module.rules.push({
test: /\.scss$/,
use: ['style-loader', 'css-loader', 'sass-loader'],
use: ['style-loader', 'css-loader', 'postcss-loader', 'sass-loader'],
include: path.resolve(__dirname, '../app/javascript'),
});
+4
View File
@@ -4,10 +4,12 @@ import Vuex from 'vuex';
import VueI18n from 'vue-i18n';
import Vuelidate from 'vuelidate';
import Multiselect from 'vue-multiselect';
import VueDOMPurifyHTML from 'vue-dompurify-html';
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon';
import WootUiKit from '../app/javascript/dashboard/components';
import i18n from '../app/javascript/dashboard/i18n';
import { domPurifyConfig } from 'shared/helpers/HTMLSanitizer';
import '../app/javascript/dashboard/assets/scss/storybook.scss';
@@ -15,6 +17,8 @@ Vue.use(VueI18n);
Vue.use(Vuelidate);
Vue.use(WootUiKit);
Vue.use(Vuex);
Vue.use(VueDOMPurifyHTML, domPurifyConfig);
Vue.component('multiselect', Multiselect);
Vue.component('fluent-icon', FluentIcon);
+9 -9
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
@@ -34,7 +34,7 @@ gem 'commonmarker'
# Validate Data against JSON Schema
gem 'json_schemer'
# Rack middleware for blocking & throttling abusive requests
gem 'rack-attack'
gem 'rack-attack', '>= 6.7.0'
# a utility tool for streaming, flexible and safe downloading of remote files
gem 'down'
# authentication type to fetch and send mail over oauth2.0
@@ -108,14 +108,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'
@@ -153,7 +153,7 @@ gem 'stripe'
gem 'faker'
# Include logrange conditionally in intializer using env variable
gem 'lograge', '~> 0.12.0', require: false
gem 'lograge', '~> 0.13.0', require: false
# worked with microsoft refresh token
gem 'omniauth-oauth2'
@@ -188,7 +188,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'
@@ -197,7 +197,7 @@ group :development do
gem 'squasher'
# profiling
gem 'rack-mini-profiler', require: false
gem 'rack-mini-profiler', '>= 3.1.1', require: false
gem 'stackprof'
end
+90 -90
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)
@@ -162,7 +162,7 @@ GEM
byebug (11.1.3)
climate_control (1.2.0)
coderay (1.1.3)
commonmarker (0.23.9)
commonmarker (0.23.10)
concurrent-ruby (1.2.2)
connection_pool (2.4.1)
crack (0.4.5)
@@ -269,8 +269,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)
@@ -429,7 +429,7 @@ GEM
llhttp-ffi (0.4.0)
ffi-compiler (~> 1.0)
rake (~> 13.0)
lograge (0.12.0)
lograge (0.13.0)
actionpack (>= 4)
activesupport (>= 4)
railties (>= 4)
@@ -450,9 +450,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.18.1)
minitest (5.20.0)
mock_redis (0.36.0)
ruby2_keywords
msgpack (1.7.0)
@@ -463,7 +463,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 +478,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,18 +543,18 @@ 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)
raabro (1.4.0)
racc (1.7.1)
rack (2.2.7)
rack-attack (6.6.1)
rack (>= 1.0, < 3)
rack (2.2.8)
rack-attack (6.7.0)
rack (>= 1.0, < 4)
rack-cors (2.0.1)
rack (>= 2.0.0)
rack-mini-profiler (3.1.0)
rack-mini-profiler (3.1.1)
rack (>= 1.2.0)
rack-protection (3.0.6)
rack
@@ -563,30 +563,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 +598,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 +697,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 +786,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 +805,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.8)
zeitwerk (2.6.11)
PLATFORMS
arm64-darwin-20
@@ -886,7 +886,7 @@ DEPENDENCIES
line-bot-api
liquid
listen
lograge (~> 0.12.0)
lograge (~> 0.13.0)
maxminddb
mock_redis
neighbor
@@ -903,11 +903,11 @@ DEPENDENCIES
pry-rails
puma
pundit
rack-attack
rack-attack (>= 6.7.0)
rack-cors
rack-mini-profiler
rack-mini-profiler (>= 3.1.1)
rack-timeout
rails (~> 7.0.5.1)
rails (~> 7.0.8.0)
redis
redis-namespace
responders
@@ -922,12 +922,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 +943,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';
@@ -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;
}
@@ -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
@@ -14,8 +14,18 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController
@facebook_inbox = Current.account.inboxes.create!(name: inbox_name, channel: facebook_channel)
set_instagram_id(page_access_token, facebook_channel)
set_avatar(@facebook_inbox, page_id)
rescue StandardError => e
ChatwootExceptionTracker.new(e).capture_exception
end
rescue StandardError => e
ChatwootExceptionTracker.new(e).capture_exception
Rails.logger.error "Error in register_facebook_page: #{e.message}"
# Additional log statements
log_additional_info
end
def log_additional_info
Rails.logger.debug do
"user_access_token: #{params[:user_access_token]} , page_access_token: #{params[:page_access_token]} ,
page_id: #{params[:page_id]}, inbox_name: #{params[:inbox_name]}"
end
end
@@ -30,6 +40,8 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController
instagram_id = response['instagram_business_account']['id']
facebook_channel.update(instagram_id: instagram_id)
rescue StandardError => e
Rails.logger.error "Error in set_instagram_id: #{e.message}"
end
# get params[:inbox_id], current_account. params[:omniauth_token]
@@ -61,6 +73,7 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController
fb_page&.reauthorized!
rescue StandardError => e
ChatwootExceptionTracker.new(e).capture_exception
Rails.logger.error "Error in update_fb_page: #{e.message}"
end
end
@@ -77,7 +90,7 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController
koala = Koala::Facebook::OAuth.new(GlobalConfigService.load('FB_APP_ID', ''), GlobalConfigService.load('FB_APP_SECRET', ''))
koala.exchange_access_token_info(omniauth_token)['access_token']
rescue StandardError => e
Rails.logger.error e
Rails.logger.error "Error in long_lived_token: #{e.message}"
end
def mark_already_existing_facebook_pages(data)
@@ -4,6 +4,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
sort_on :name, internal_name: :order_on_name, type: :scope, scope_params: [:direction]
sort_on :phone_number, type: :string
sort_on :last_activity_at, internal_name: :order_on_last_activity_at, type: :scope, scope_params: [:direction]
sort_on :created_at, internal_name: :order_on_created_at, type: :scope, scope_params: [:direction]
sort_on :company, internal_name: :order_on_company_name, type: :scope, scope_params: [:direction]
sort_on :city, internal_name: :order_on_city, type: :scope, scope_params: [:direction]
sort_on :country, internal_name: :order_on_country_name, type: :scope, scope_params: [:direction]
@@ -17,7 +18,7 @@ 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
@@ -28,7 +29,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
search: "%#{params[:q].strip}%"
)
@contacts_count = contacts.count
@contacts = fetch_contacts_with_conversation_count(contacts)
@contacts = fetch_contacts(contacts)
end
def import
@@ -62,7 +63,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
@@ -124,17 +125,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
@@ -1,27 +1,27 @@
class Api::V1::Accounts::Integrations::SlackController < Api::V1::Accounts::BaseController
before_action :check_admin_authorization?
before_action :fetch_hook, only: [:update, :destroy]
before_action :fetch_hook, only: [:update, :destroy, :list_all_channels]
def list_all_channels
@channels = channel_builder.fetch_channels
end
def create
ActiveRecord::Base.transaction do
builder = Integrations::Slack::HookBuilder.new(
account: Current.account,
code: params[:code],
inbox_id: params[:inbox_id]
)
@hook = builder.perform
create_chatwoot_slack_channel
end
hook_builder = Integrations::Slack::HookBuilder.new(
account: Current.account,
code: params[:code],
inbox_id: params[:inbox_id]
)
@hook = hook_builder.perform
end
def update
create_chatwoot_slack_channel
render json: @hook
@hook = channel_builder.update(permitted_params[:reference_id])
render json: { error: I18n.t('errors.slack.invalid_channel_id') }, status: :unprocessable_entity if @hook.blank?
end
def destroy
@hook.destroy!
head :ok
end
@@ -31,11 +31,11 @@ class Api::V1::Accounts::Integrations::SlackController < Api::V1::Accounts::Base
@hook = Integrations::Hook.where(account: Current.account).find_by(app_id: 'slack')
end
def create_chatwoot_slack_channel
channel = params[:channel] || 'customer-conversations'
builder = Integrations::Slack::ChannelBuilder.new(
hook: @hook, channel: channel
)
builder.perform
def channel_builder
Integrations::Slack::ChannelBuilder.new(hook: @hook)
end
def permitted_params
params.permit(:reference_id)
end
end
@@ -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
@@ -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
@@ -1,4 +1,7 @@
class Public::Api::V1::Inboxes::ConversationsController < Public::Api::V1::InboxesController
include Events::Types
before_action :set_conversation, only: [:toggle_typing, :update_last_seen]
def index
@conversations = @contact_inbox.hmac_verified? ? @contact.conversations : @contact_inbox.conversations
end
@@ -7,12 +10,36 @@ class Public::Api::V1::Inboxes::ConversationsController < Public::Api::V1::Inbox
@conversation = create_conversation
end
def toggle_typing
case params[:typing_status]
when 'on'
trigger_typing_event(CONVERSATION_TYPING_ON)
when 'off'
trigger_typing_event(CONVERSATION_TYPING_OFF)
end
head :ok
end
def update_last_seen
@conversation.contact_last_seen_at = DateTime.now.utc
@conversation.save!
head :ok
end
private
def set_conversation
@conversation = @contact_inbox.contact.conversations.find_by!(display_id: params[:id])
end
def create_conversation
::Conversation.create!(conversation_params)
end
def trigger_typing_event(event)
Rails.configuration.dispatcher.dispatch(event, Time.zone.now, conversation: @conversation, user: @conversation.contact)
end
def conversation_params
{
account_id: @contact_inbox.contact.account_id,
@@ -8,13 +8,22 @@ class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::B
def index
@articles = @portal.articles
@articles = @articles.search(list_params) if list_params.present?
@articles.order(position: :asc)
order_by_sort_param
@articles.page(list_params[:page]) if list_params[:page].present?
end
def show; end
private
def order_by_sort_param
@articles = if list_params[:sort].present? && list_params[:sort] == 'views'
@articles.order_by_views
else
@articles.order_by_position
end
end
def set_article
@article = @portal.articles.find_by(slug: permitted_params[:article_slug])
@article.increment_view_count
@@ -35,7 +44,7 @@ class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::B
end
def list_params
params.permit(:query, :locale)
params.permit(:query, :locale, :sort)
end
def permitted_params
@@ -1,8 +1,14 @@
class Public::Api::V1::Portals::BaseController < PublicController
before_action :show_plain_layout
around_action :set_locale
after_action :allow_iframe_requests
private
def show_plain_layout
@is_plain_layout_enabled = params[:show_plain_layout] == 'true'
end
def set_locale(&)
switch_locale_with_portal(&) if params[:locale].present?
switch_locale_with_article(&) if params[:article_slug].present?
@@ -34,4 +40,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
@@ -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
+4 -1
View File
@@ -86,7 +86,10 @@ class AccountDashboard < Administrate::BaseDashboard
"##{account.id} #{account.name}"
end
def permitted_attributes
# We do not use the action parameter but we still need to define it
# to prevent an error from being raised (wrong number of arguments)
# Reference: https://github.com/thoughtbot/administrate/pull/2356/files#diff-4e220b661b88f9a19ac527c50d6f1577ef6ab7b0bed2bfdf048e22e6bfa74a05R204
def permitted_attributes(action)
super + [limits: {}]
end
end
+5 -23
View File
@@ -2,8 +2,8 @@
<div
v-if="!authUIFlags.isFetching"
id="app"
class="app-wrapper app-root"
:class="{ 'app-rtl--wrapper': isRTLView, dark: theme === 'dark' }"
class="app-wrapper h-full flex-grow-0 min-h-0 w-full"
:class="{ 'app-rtl--wrapper': isRTLView }"
:dir="isRTLView ? 'rtl' : 'ltr'"
>
<update-banner :latest-chatwoot-version="latestChatwootVersion" />
@@ -35,12 +35,11 @@ import PaymentPendingBanner from './components/app/PaymentPendingBanner.vue';
import vueActionCable from './helper/actionCable';
import WootSnackbarBox from './components/SnackbarContainer';
import rtlMixin from 'shared/mixins/rtlMixin';
import { LocalStorage } from 'shared/helpers/localStorage';
import { setColorTheme } from './helper/themeHelper';
import {
registerSubscription,
verifyServiceWorkerExistence,
} from './helper/pushHelper';
import { LOCAL_STORAGE_KEYS } from './constants/localStorage';
export default {
name: 'App',
@@ -61,7 +60,6 @@ export default {
return {
showAddAccountModal: false,
latestChatwootVersion: null,
theme: 'light',
};
},
@@ -99,27 +97,11 @@ export default {
},
methods: {
initializeColorTheme() {
this.setColorTheme(
window.matchMedia('(prefers-color-scheme: dark)').matches
);
},
setColorTheme(isOSOnDarkMode) {
const selectedColorScheme =
LocalStorage.get(LOCAL_STORAGE_KEYS.COLOR_SCHEME) || 'light';
if (
(selectedColorScheme === 'auto' && isOSOnDarkMode) ||
selectedColorScheme === 'dark'
) {
this.theme = 'dark';
document.body.classList.add('dark');
} else {
this.theme = 'light ';
document.body.classList.remove('dark');
}
setColorTheme(window.matchMedia('(prefers-color-scheme: dark)').matches);
},
listenToThemeChanges() {
const mql = window.matchMedia('(prefers-color-scheme: dark)');
mql.onchange = e => this.setColorTheme(e.matches);
mql.onchange = e => setColorTheme(e.matches);
},
setLocale(locale) {
this.$root.$i18n.locale = locale;
@@ -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();
@@ -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,
+10 -2
View File
@@ -8,11 +8,19 @@ class IntegrationsAPI extends ApiClient {
}
connectSlack(code) {
return axios.post(`${this.baseUrl()}/integrations/slack`, {
code: code,
return axios.post(`${this.baseUrl()}/integrations/slack`, { code });
}
updateSlack({ referenceId }) {
return axios.patch(`${this.baseUrl()}/integrations/slack`, {
reference_id: referenceId,
});
}
listAllSlackChannels() {
return axios.get(`${this.baseUrl()}/integrations/slack/list_all_channels`);
}
delete(integrationId) {
return axios.delete(`${this.baseUrl()}/integrations/${integrationId}`);
}
@@ -11,7 +11,9 @@ describe('#integrationAPI', () => {
expect(integrationAPI).toHaveProperty('update');
expect(integrationAPI).toHaveProperty('delete');
expect(integrationAPI).toHaveProperty('connectSlack');
expect(integrationAPI).toHaveProperty('createHook');
expect(integrationAPI).toHaveProperty('updateSlack');
expect(integrationAPI).toHaveProperty('updateSlack');
expect(integrationAPI).toHaveProperty('listAllSlackChannels');
expect(integrationAPI).toHaveProperty('deleteHook');
});
describeWithAPIMock('API calls', context => {
@@ -26,6 +28,24 @@ describe('#integrationAPI', () => {
);
});
it('#updateSlack', () => {
const updateObj = { referenceId: 'SDFSDGSVE' };
integrationAPI.updateSlack(updateObj);
expect(context.axiosMock.patch).toHaveBeenCalledWith(
'/api/v1/integrations/slack',
{
reference_id: updateObj.referenceId,
}
);
});
it('#listAllSlackChannels', () => {
integrationAPI.listAllSlackChannels();
expect(context.axiosMock.get).toHaveBeenCalledWith(
'/api/v1/integrations/slack/list_all_channels'
);
});
it('#delete', () => {
integrationAPI.delete(2);
expect(context.axiosMock.delete).toHaveBeenCalledWith(
@@ -0,0 +1,34 @@
<svg width="495" height="384" viewBox="0 0 495 384" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="No Chat History">
<path id="Fill 1" fill-rule="evenodd" clip-rule="evenodd" d="M266.903 0C221.687 0 180.128 15.7196 147.313 42.0198H94.2771C87.3866 42.0198 81.749 47.6925 81.749 54.6257C81.749 61.559 87.3866 67.2317 94.2771 67.2317H121.268H129.178C136.068 67.2317 141.706 72.9043 141.706 79.8376C141.706 83.3 140.294 86.4599 138.023 88.7374C135.759 91.0233 132.619 92.4435 129.178 92.4435H112.303H102.986H57.5281C50.6376 92.4435 45 98.1162 45 105.049C45 111.983 50.6376 117.655 57.5281 117.655H90.071C80.2473 140.886 74.8059 166.446 74.8059 193.291C74.8059 203.031 75.5242 212.603 76.9023 221.957C77.4786 225.865 78.1718 229.739 78.982 233.571C102.618 229.932 127.691 227.008 153.866 224.873L155.837 226.68L157.692 224.571C189.137 222.108 222.144 220.797 256.187 220.797C328.516 220.797 396.201 226.714 454.072 237C456.302 227.268 457.806 217.259 458.516 207.023C458.841 202.485 459 197.905 459 193.291C459 86.5356 372.999 0 266.903 0Z" fill="#43484C"/>
<path id="Fill 3" fill-rule="evenodd" clip-rule="evenodd" d="M20.2465 91.4282H12.6678C5.7005 91.4282 0 97.1882 0 104.228C0 111.268 5.7005 117.028 12.6678 117.028H20.2465C27.2137 117.028 32.9142 111.268 32.9142 104.228C32.9142 97.1882 27.2137 91.4282 20.2465 91.4282Z" fill="#43484C"/>
<g id="Group 7">
<mask id="mask0_201_141" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="78" y="219" width="417" height="165">
<path id="Clip 6" fill-rule="evenodd" clip-rule="evenodd" d="M495 219V384H286.5H78V219H495Z" fill="white"/>
</mask>
<g mask="url(#mask0_201_141)">
<path id="Fill 5" fill-rule="evenodd" clip-rule="evenodd" d="M495 279.775C495 286.675 489.336 292.321 482.413 292.321H402.016C398.559 292.321 395.405 293.734 393.131 296.009C390.849 298.276 389.43 301.421 389.43 304.867C389.43 311.768 395.094 317.413 402.016 317.413H416.954C420.41 317.413 423.566 318.827 425.84 321.102C428.122 323.369 429.54 326.514 429.54 329.96C429.54 336.86 423.876 342.506 416.954 342.506H382.608C350.344 366.662 310.251 380.981 266.802 380.981C174.004 380.981 96.5026 315.699 78 228.695C101.747 225.073 126.937 222.162 153.235 220.038L155.215 221.836L157.079 219.736C188.671 217.286 221.833 215.981 256.036 215.981C328.703 215.981 396.705 221.869 454.847 232.107C452.02 244.302 448.026 256.045 442.991 267.228H482.413C485.879 267.228 489.017 268.642 491.3 270.917C493.582 273.184 495 276.328 495 279.775Z" fill="#34393D"/>
</g>
</g>
<path id="Fill 8" fill-rule="evenodd" clip-rule="evenodd" d="M459 206.12C458.285 216.665 456.771 226.975 454.525 237C396.235 226.404 328.06 220.31 255.208 220.31C220.919 220.31 187.673 221.66 156 224.196L177.965 198.615L212.134 221.028L258.388 208.19L337.959 220.31L372.503 208.19L381.252 220.31L402.126 204.736L421.632 222.707L452.396 198L459 206.12Z" fill="#55575E"/>
<path id="Fill 10" fill-rule="evenodd" clip-rule="evenodd" d="M344.905 216.502C353.565 203.915 358.526 188.987 358.397 173.012C358.043 128.578 318.46 92.8734 269.986 93.2602C221.512 93.6478 182.505 129.983 182.86 174.416C183.214 218.849 222.798 254.555 271.271 254.168C288.59 254.03 304.7 249.3 318.238 241.253L354.454 249.78L344.905 216.502Z" fill="#686B73"/>
<path id="Fill 12" fill-rule="evenodd" clip-rule="evenodd" d="M245.029 171.813C245.069 176.862 241.008 180.988 235.959 181.028C230.909 181.069 226.783 177.008 226.743 171.959C226.703 166.909 230.763 162.784 235.813 162.743C240.861 162.703 244.987 166.763 245.029 171.813Z" fill="#BEC0D6"/>
<path id="Fill 14" fill-rule="evenodd" clip-rule="evenodd" d="M277.942 171.812C277.982 176.862 273.922 180.988 268.872 181.028C263.822 181.068 259.696 177.008 259.657 171.959C259.616 166.909 263.677 162.783 268.726 162.743C273.775 162.703 277.901 166.763 277.942 171.812Z" fill="#BEC0D6"/>
<path id="Fill 16" fill-rule="evenodd" clip-rule="evenodd" d="M310.856 171.812C310.897 176.861 306.836 180.987 301.786 181.028C296.737 181.068 292.611 177.008 292.571 171.958C292.53 166.908 296.591 162.783 301.641 162.743C306.689 162.702 310.815 166.763 310.856 171.812Z" fill="#BEC0D6"/>
<path id="Fill 20" fill-rule="evenodd" clip-rule="evenodd" d="M173.714 235.887C173.714 241.693 175.571 247.099 178.763 251.649L175.376 263.789L188.564 260.581C193.523 263.465 199.402 265.143 205.714 265.143C223.388 265.143 237.714 252.044 237.714 235.887C237.714 219.728 223.388 206.629 205.714 206.629C188.041 206.629 173.714 219.728 173.714 235.887Z" fill="url(#paint0_linear_201_141)"/>
<g id="Frame 148">
<path id="Fill 26" fill-rule="evenodd" clip-rule="evenodd" d="M189 234.658C189 236.677 190.637 238.314 192.657 238.314C194.676 238.314 196.314 236.677 196.314 234.658C196.314 232.638 194.676 231 192.657 231C190.637 231 189 232.638 189 234.658Z" fill="#0D0D0D"/>
<path id="Fill 24" fill-rule="evenodd" clip-rule="evenodd" d="M202.314 234.658C202.314 236.677 203.951 238.314 205.971 238.314C207.99 238.314 209.628 236.677 209.628 234.658C209.628 232.638 207.99 231 205.971 231C203.951 231 202.314 232.638 202.314 234.658Z" fill="#0D0D0D"/>
<path id="Fill 22" fill-rule="evenodd" clip-rule="evenodd" d="M215.629 234.658C215.629 236.677 217.265 238.314 219.285 238.314C221.305 238.314 222.943 236.677 222.943 234.658C222.943 232.638 221.305 231 219.285 231C217.265 231 215.629 232.638 215.629 234.658Z" fill="#0D0D0D"/>
</g>
<path id="Fill 28" fill-rule="evenodd" clip-rule="evenodd" d="M153.6 223.557C127.481 225.685 102.461 228.601 78.8755 232.228C78.0671 228.408 77.3754 224.546 76.8003 220.65L117.229 190.171L153.6 223.557Z" fill="#55575E"/>
<path id="Fill 30" fill-rule="evenodd" clip-rule="evenodd" d="M336.457 286.171C336.457 289.706 304.529 292.571 265.143 292.571C225.757 292.571 193.829 289.706 193.829 286.171C193.829 282.638 225.757 279.771 265.143 279.771C304.529 279.771 336.457 282.638 336.457 286.171Z" fill="#686B73"/>
<path id="Fill 32" fill-rule="evenodd" clip-rule="evenodd" d="M453.486 345.599C446.446 345.599 440.686 339.839 440.686 332.799C440.686 325.76 446.446 320 453.486 320C460.526 320 466.286 325.76 466.286 332.799C466.286 339.839 460.526 345.599 453.486 345.599Z" fill="#34393D"/>
</g>
<defs>
<linearGradient id="paint0_linear_201_141" x1="205.714" y1="177.372" x2="147.434" y2="241.115" gradientUnits="userSpaceOnUse">
<stop stop-color="#9DC9FB"/>
<stop offset="1" stop-color="#9DC9FB"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 6.2 KiB

@@ -0,0 +1,33 @@
<svg width="495" height="387" viewBox="0 0 495 387" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="No Chat History">
<path id="Fill 1" fill-rule="evenodd" clip-rule="evenodd" d="M267.927 1.5C222.792 1.5 181.307 17.1457 148.551 43.3222H95.611C88.7329 43.3222 83.1054 48.9682 83.1054 55.8688C83.1054 62.7695 88.7329 68.4155 95.611 68.4155H122.554H130.449C137.327 68.4155 142.955 74.0615 142.955 80.9621C142.955 84.4083 141.546 87.5533 139.278 89.8201C137.019 92.0952 133.884 93.5088 130.449 93.5088H113.605H104.304H58.9279C52.0499 93.5088 46.4224 99.1548 46.4224 106.055C46.4224 112.956 52.0499 118.602 58.9279 118.602H91.4124C81.6064 141.723 76.1748 167.164 76.1748 193.882C76.1748 203.576 76.8918 213.103 78.2674 222.413C78.8426 226.303 79.5346 230.159 80.3433 233.973C103.937 230.351 128.965 227.44 155.093 225.316L157.061 227.114L158.912 225.014C190.301 222.564 223.249 221.259 257.23 221.259C329.429 221.259 396.993 227.147 454.76 237.385C456.986 227.699 458.487 217.737 459.196 207.549C459.521 203.033 459.679 198.474 459.679 193.882C459.679 87.6286 373.832 1.5 267.927 1.5Z" fill="#F1F2F7"/>
<path id="Fill 3" fill-rule="evenodd" clip-rule="evenodd" d="M20.954 92.9282H13.3753C6.40802 92.9282 0.70752 98.6882 0.70752 105.728C0.70752 112.768 6.40802 118.528 13.3753 118.528H20.954C27.9213 118.528 33.6218 112.768 33.6218 105.728C33.6218 98.6882 27.9213 92.9282 20.954 92.9282Z" fill="#F1F2F7"/>
<g id="Group 7">
<mask id="mask0_156_62" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="80" y="221" width="415" height="165">
<path id="Clip 6" fill-rule="evenodd" clip-rule="evenodd" d="M494.293 221.536V385.5H287.188H80.083V221.536H494.293Z" fill="white"/>
</mask>
<g mask="url(#mask0_156_62)">
<path id="Fill 5" fill-rule="evenodd" clip-rule="evenodd" d="M494.293 284.929C494.293 291.786 488.667 297.396 481.79 297.396H401.932C398.498 297.396 395.364 298.801 393.106 301.062C390.838 303.314 389.429 306.439 389.429 309.864C389.429 316.721 395.055 322.331 401.932 322.331H416.769C420.202 322.331 423.337 323.736 425.596 325.997C427.863 328.249 429.271 331.374 429.271 334.799C429.271 341.656 423.645 347.266 416.769 347.266H382.653C350.604 371.271 310.78 385.5 267.622 385.5C175.444 385.5 98.4618 320.627 80.083 234.169C103.671 230.57 128.693 227.678 154.815 225.567L156.781 227.354L158.632 225.268C190.013 222.832 222.954 221.536 256.928 221.536C329.108 221.536 396.656 227.387 454.409 237.561C451.601 249.679 447.633 261.349 442.632 272.461H481.79C485.233 272.461 488.35 273.866 490.617 276.127C492.884 278.379 494.293 281.504 494.293 284.929Z" fill="#E8EBF2"/>
</g>
</g>
<path id="Fill 8" fill-rule="evenodd" clip-rule="evenodd" d="M457.85 206.981C457.142 217.363 455.643 227.515 453.42 237.386C395.73 226.953 328.256 220.953 256.153 220.953C222.216 220.953 189.312 222.282 157.964 224.779L179.704 199.591L213.521 221.659L259.3 209.019L338.053 220.953L372.242 209.019L380.901 220.953L401.56 205.618L420.866 223.313L451.314 198.986L457.85 206.981Z" fill="#E0E2EE"/>
<path id="Fill 10" fill-rule="evenodd" clip-rule="evenodd" d="M345.614 218.002C354.273 205.415 359.234 190.487 359.106 174.512C358.752 130.078 319.168 94.3734 270.694 94.7602C222.22 95.1478 183.213 131.483 183.568 175.916C183.923 220.349 223.506 256.055 271.979 255.668C289.298 255.53 305.408 250.8 318.946 242.753L355.162 251.28L345.614 218.002Z" fill="#D8DBEA"/>
<path id="Fill 12" fill-rule="evenodd" clip-rule="evenodd" d="M245.736 173.313C245.776 178.362 241.716 182.488 236.667 182.528C231.616 182.569 227.49 178.508 227.451 173.459C227.411 168.409 231.471 164.284 236.52 164.243C241.569 164.203 245.695 168.263 245.736 173.313Z" fill="#BEC0D6"/>
<path id="Fill 14" fill-rule="evenodd" clip-rule="evenodd" d="M278.65 173.312C278.69 178.362 274.629 182.488 269.58 182.528C264.53 182.568 260.404 178.508 260.365 173.459C260.323 168.409 264.384 164.283 269.434 164.243C274.483 164.203 278.609 168.263 278.65 173.312Z" fill="#BEC0D6"/>
<path id="Fill 16" fill-rule="evenodd" clip-rule="evenodd" d="M311.565 173.312C311.605 178.361 307.544 182.487 302.495 182.528C297.445 182.568 293.319 178.508 293.28 173.458C293.239 168.408 297.299 164.283 302.349 164.243C307.398 164.202 311.524 168.263 311.565 173.312Z" fill="#BEC0D6"/>
<path id="Fill 18" fill-rule="evenodd" clip-rule="evenodd" d="M157.965 224.585L156.192 226.414L154.308 224.848C155.521 224.753 156.744 224.666 157.965 224.585Z" fill="#F1F2F7"/>
<path id="Fill 20" fill-rule="evenodd" clip-rule="evenodd" d="M174.422 237.387C174.422 243.193 176.279 248.599 179.47 253.149L176.083 265.289L189.272 262.081C194.23 264.965 200.11 266.643 206.422 266.643C224.095 266.643 238.422 253.544 238.422 237.387C238.422 221.228 224.095 208.129 206.422 208.129C188.748 208.129 174.422 221.228 174.422 237.387Z" fill="url(#paint0_linear_156_62)"/>
<path id="Fill 22" fill-rule="evenodd" clip-rule="evenodd" d="M214.651 237.386C214.651 239.405 216.287 241.043 218.308 241.043C220.327 241.043 221.965 239.405 221.965 237.386C221.965 235.366 220.327 233.728 218.308 233.728C216.287 233.728 214.651 235.366 214.651 237.386Z" fill="white"/>
<path id="Fill 24" fill-rule="evenodd" clip-rule="evenodd" d="M203.679 237.386C203.679 239.405 205.316 241.043 207.336 241.043C209.355 241.043 210.993 239.405 210.993 237.386C210.993 235.366 209.355 233.728 207.336 233.728C205.316 233.728 203.679 235.366 203.679 237.386Z" fill="white"/>
<path id="Fill 26" fill-rule="evenodd" clip-rule="evenodd" d="M190.879 237.386C190.879 239.405 192.516 241.043 194.537 241.043C196.556 241.043 198.194 239.405 198.194 237.386C198.194 235.366 196.556 233.728 194.537 233.728C192.516 233.728 190.879 235.366 190.879 237.386Z" fill="white"/>
<path id="Fill 28" fill-rule="evenodd" clip-rule="evenodd" d="M154.308 225.057C128.188 227.185 103.169 230.101 79.583 233.728C78.7746 229.908 78.0829 226.046 77.5078 222.15L117.936 191.671L154.308 225.057Z" fill="#E0E2EE"/>
<path id="Fill 30" fill-rule="evenodd" clip-rule="evenodd" d="M337.164 287.671C337.164 291.206 305.235 294.071 265.85 294.071C226.464 294.071 194.536 291.206 194.536 287.671C194.536 284.138 226.464 281.271 265.85 281.271C305.235 281.271 337.164 284.138 337.164 287.671Z" fill="#D8DBEA"/>
<path id="Fill 32" fill-rule="evenodd" clip-rule="evenodd" d="M454.193 347.099C447.153 347.099 441.393 341.339 441.393 334.299C441.393 327.26 447.153 321.5 454.193 321.5C461.233 321.5 466.993 327.26 466.993 334.299C466.993 341.339 461.233 347.099 454.193 347.099Z" fill="#E1E3EF"/>
</g>
<defs>
<linearGradient id="paint0_linear_156_62" x1="206.422" y1="178.872" x2="148.142" y2="242.615" gradientUnits="userSpaceOnUse">
<stop stop-color="#9DC9FB"/>
<stop offset="1" stop-color="#9DC9FB"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 6.5 KiB

@@ -26,6 +26,7 @@ code {
background: $color-background;
border-radius: var(--border-radius-large);
padding: $space-two;
@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);
}
@@ -72,7 +72,6 @@
@import '~shared/assets/stylesheets/ionicons';
@import 'utility-helpers';
.tooltip {
@apply bg-slate-900 text-white py-1 px-2 z-40 text-xs rounded-md dark:bg-slate-200 dark:text-slate-900;
}
@@ -18,7 +18,15 @@
}
&.multiselect--disabled {
@apply opacity-80;
@apply opacity-50 border border-slate-200 dark:border-slate-600 rounded-md cursor-not-allowed;
.multiselect__select {
@apply cursor-not-allowed bg-white dark:bg-slate-900 rounded-md;
}
.multiselect__tags {
@apply border-0;
}
}
.multiselect--active {
@@ -36,7 +44,7 @@
}
.multiselect__content-wrapper {
@apply bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-600 text-slate-800 dark:text-slate-100;
@apply bg-white dark:bg-slate-900 border border-solid border-slate-200 dark:border-slate-600 text-slate-800 dark:text-slate-100;
}
.multiselect__content {
@@ -10,7 +10,7 @@
@import 'variables';
@import '~spinkit/scss/spinners/7-three-bounce';
@import '~vue-multiselect/dist/vue-multiselect.min.css';
@import 'vue-multiselect/dist/vue-multiselect.min.css';
@import '~shared/assets/stylesheets/ionicons';
@import 'mixins';
@@ -30,3 +30,18 @@
@import 'widgets/forms';
@import 'plugins/multiselect';
@import 'widget/assets/scss/reset';
@import 'tailwindcss/base';
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';
@import 'widget/assets/scss/utilities';
html,
body {
font-family: 'PlusJakarta', sans-serif;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
height: 100%;
}
@@ -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 {
@@ -64,19 +56,19 @@
@apply bg-slate-75 dark:bg-slate-900 border-slate-100 dark:border-slate-700;
&.secondary {
@apply border-slate-100 dark:border-slate-700;
@apply border-slate-100 dark:border-slate-700 text-slate-800 dark:text-slate-100;
}
&.success {
@apply border-slate-100 dark:border-slate-700;
@apply border-slate-100 dark:border-slate-700 text-green-800 dark:text-green-100;
}
&.alert {
@apply border-slate-100 dark:border-slate-700;
@apply border-slate-100 dark:border-slate-700 text-red-700 dark:text-red-100;
}
&.warning {
@apply border-slate-100 dark:border-slate-700;
@apply border-slate-100 dark:border-slate-700 text-yellow-700 dark:text-yellow-700;
}
}
}
@@ -90,7 +82,7 @@
}
&.success {
@apply bg-green-50 dark:bg-green-700 text-green-700 dark:text-green-100 hover:bg-green-100 dark:hover:bg-green-800;
@apply bg-green-50 dark:bg-green-700 text-green-700 dark:text-green-100 hover:bg-green-100 dark:hover:bg-green-800 hover:text-green-800 dark:hover:text-green-100;
}
&.alert {
@@ -118,26 +110,26 @@
}
&.warning {
@apply text-yellow-700 dark:text-yellow-700;
@apply text-yellow-700 dark:text-yellow-600;
}
&:hover {
@apply hover:bg-woot-50 dark:hover:bg-woot-900/50 hover:text-woot-500 dark:hover:text-woot-100;
&.secondary {
@apply hover:bg-slate-50 dark:hover:bg-slate-700;
@apply hover:bg-slate-50 dark:hover:bg-slate-700 hover:text-slate-800 dark:hover:text-slate-100;
}
&.success {
@apply hover:bg-green-50 dark:hover:bg-green-800;
@apply hover:bg-green-50 dark:hover:bg-green-800 hover:text-green-800 dark:hover:text-green-100;
}
&.alert {
@apply hover:bg-red-50 dark:hover:bg-red-800;
@apply hover:bg-red-50 dark:hover:bg-red-800 hover:text-red-700 dark:hover:text-red-100;
}
&.warning {
@apply hover:bg-yellow-50 dark:hover:bg-yellow-100;
@apply hover:bg-yellow-100 dark:hover:bg-yellow-800 hover:text-yellow-700 dark:hover:text-yellow-600;
}
}
@@ -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 {
@@ -1,34 +1,45 @@
// scss-lint:disable MergeableSelector
@mixin bubble-with-types {
@apply py-2 text-sm font-normal bg-woot-500 dark:bg-woot-500 relative px-4 m-0 text-white dark:text-white;
.message-text__wrap {
@apply relative;
.link {
@apply text-white dark:text-white underline;
}
@tailwind utilities;
@layer utilities {
.custom-gradient {
background-image: linear-gradient(
-180deg,
transparent 3%,
rgb(76 81 85) 130%
);
}
.image,
.video {
@apply cursor-pointer relative;
.bubble-with-types {
@apply py-2 text-sm font-normal bg-woot-500 dark:bg-woot-500 relative px-4 m-0 text-white dark:text-white;
.modal-container {
@apply text-center;
.message-text__wrap {
@apply relative;
.link {
@apply text-white dark:text-white underline;
}
}
.modal-image {
@apply max-h-[76vh] max-w-[76vw];
}
.image,
.video {
@apply cursor-pointer relative;
.modal-video {
@apply max-h-[76vh] max-w-[76vw];
}
.modal-container {
@apply text-center;
}
&::before {
@apply bg-gradient-to-t from-transparent via-transparent to-slate-800 bg-repeat bg-clip-border bottom-0 h-[20%] content-[''] left-0 absolute w-full opacity-80;
.modal-image {
@apply max-h-[76vh] max-w-[76vw];
}
.modal-video {
@apply max-h-[76vh] max-w-[76vw];
}
&::before {
@apply custom-gradient bottom-0 h-[20%] content-[''] left-0 absolute w-full opacity-80;
}
}
}
}
@@ -55,8 +66,7 @@
}
.bubble {
@include bubble-with-types;
@apply text-left break-words;
@apply bubble-with-types text-left break-words;
.aplayer {
@apply shadow-none;
@@ -129,7 +139,15 @@
@apply ml-auto break-words rounded-l-lg rounded-r;
&.is-private {
@apply text-black-900 dark:text-black-900 relative border border-solid bg-[#fff3d5] border-[#ffd97a] dark:bg-[#fff3d5] dark:border-[#ffd97a];
@apply text-black-900 dark:text-white relative border border-solid bg-yellow-100 dark:bg-yellow-700 border-yellow-200 dark:border-yellow-600/25;
blockquote {
@apply border-slate-400 dark:border-slate-400 text-slate-800 dark:text-slate-300;
p {
@apply text-slate-600 dark:text-slate-300;
}
}
}
&.is-image {
@@ -1,6 +1,6 @@
.reply-box {
transition: box-shadow 0.35s $swift-ease-out-function,
height 2s $swift-ease-out-function;
height 2s $swift-ease-out-function;
&.is-focused {
box-shadow: var(--shadow);
@@ -47,13 +47,13 @@
}
&.is-private {
background: var(--y-50);
@apply bg-yellow-100 dark:bg-yellow-800;
.reply-box__top {
background: var(--y-50);
@apply bg-yellow-100 dark:bg-yellow-800;
> input {
background: var(--y-50);
@apply bg-yellow-100 dark:bg-yellow-800;
}
}
}
@@ -31,7 +31,7 @@
}
.tabs-title {
@apply flex-shrink-0 my-0 mx-2;
@apply flex-shrink-0 my-0 mx-2 ;
.badge {
@apply bg-slate-50 dark:bg-slate-800 rounded-md text-slate-600 dark:text-slate-100 h-5 flex items-center justify-center text-xxs font-semibold my-0 mx-1 px-1 py-0;
@@ -53,7 +53,7 @@
}
a {
@apply flex items-center flex-row border-b border-transparent text-slate-600 dark:text-slate-200 text-sm top-[1px] relative;
@apply flex items-center flex-row border-b border-transparent text-slate-500 dark:text-slate-200 text-sm top-[1px] relative;
transition: border-color 0.15s $swift-ease-out-function;
}
@@ -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;
}
}
@@ -1,6 +1,6 @@
<template>
<div
class="conversations-list-wrap flex-shrink-0 flex-basis-custom overflow-hidden flex flex-col border-r rtl:border-r-0 rtl:border-l border-slate-50 dark:border-slate-800/50"
class="conversations-list-wrap flex-basis-clamp flex-shrink-0 flex-basis-custom overflow-hidden flex flex-col border-r rtl:border-r-0 rtl:border-l border-slate-50 dark:border-slate-800/50"
:class="{
hide: !showConversationList,
'list--full-width': isOnExpandedLayout,
@@ -15,7 +15,7 @@
>
<div class="flex max-w-[85%] justify-center items-center">
<h1
class="text-xl break-words overflow-hidden whitespace-nowrap text-ellipsis text-black-800 dark:text-slate-100 overflow-hidden whitespace-nowrap text-ellipsis mb-0"
class="text-xl break-words overflow-hidden whitespace-nowrap text-ellipsis text-black-900 dark:text-slate-100 mb-0"
:title="pageTitle"
>
{{ pageTitle }}
@@ -969,8 +969,6 @@ export default {
<style scoped lang="scss">
.conversations-list-wrap {
@apply flex-basis-clamp;
&.hide {
@apply hidden;
}
@@ -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;
},
@@ -2,7 +2,7 @@
<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-600': 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">
@@ -1,11 +1,21 @@
<template>
<kbd class="hotkey">
<kbd class="hotkey p-0.5 min-w-[1rem] uppercase" :class="customClass">
<slot />
</kbd>
</template>
<script>
export default {
props: {
customClass: {
type: String,
default: '',
},
},
};
</script>
<style lang="scss">
kbd.hotkey {
@apply inline-flex p-0.5 leading-[0.625rem] min-w-[1rem] tracking-wide uppercase text-slate-800 dark:text-slate-100 bg-slate-50 dark:bg-slate-600 text-xxs border border-solid border-slate-75 dark:border-slate-600 rounded flex-shrink-0 items-center select-none justify-center;
@apply inline-flex leading-[0.625rem] rounded tracking-wide flex-shrink-0 items-center select-none justify-center;
}
</style>
@@ -8,7 +8,7 @@
>
<fluent-icon v-if="!!iconClass" :icon="iconClass" class="icon" />
<span>{{ buttonText }}</span>
<spinner v-if="loading" />
<spinner v-if="loading" class="ml-2" :color-scheme="spinnerClass" />
</button>
</template>
@@ -40,6 +40,10 @@ export default {
type: String,
default: '',
},
spinnerClass: {
type: String,
default: '',
},
type: {
type: String,
default: 'submit',
@@ -47,7 +51,7 @@ export default {
},
computed: {
computedClass() {
return `button nice ${this.buttonClass || ' '}`;
return `button nice gap-2 ${this.buttonClass || ' '}`;
},
},
methods: {
@@ -59,11 +63,9 @@ export default {
</script>
<style lang="scss" scoped>
button:disabled {
opacity: 1;
background-color: var(--w-100);
@apply bg-woot-100 dark:bg-woot-500/25 dark:text-slate-500 opacity-100;
&:hover {
background-color: var(--w-100);
@apply bg-woot-100 dark:bg-woot-500/25;
}
}
</style>
@@ -29,6 +29,7 @@ const settings = accountId => ({
'settings_inboxes_page_channel',
'settings_integrations_dashboard_apps',
'settings_integrations_integration',
'settings_integrations_slack',
'settings_integrations_webhook',
'settings_integrations',
'settings_teams_add_agents',
@@ -92,7 +93,6 @@ const settings = accountId => ({
{
icon: 'automation',
label: 'AUTOMATION',
beta: true,
hasSubMenu: false,
toState: frontendURL(`accounts/${accountId}/settings/automation/list`),
toStateName: 'automation_list',
@@ -101,7 +101,6 @@ const settings = accountId => ({
{
icon: 'bot',
label: 'AGENT_BOTS',
beta: true,
hasSubMenu: false,
globalConfigFlag: 'csmlEditorHost',
toState: frontendURL(`accounts/${accountId}/settings/agent-bots`),
@@ -114,7 +113,6 @@ const settings = accountId => ({
hasSubMenu: false,
toState: frontendURL(`accounts/${accountId}/settings/macros`),
toStateName: 'macros_wrapper',
beta: true,
featureFlag: FEATURE_FLAGS.MACROS,
},
{
@@ -1,18 +1,20 @@
<template>
<div
v-if="showShowCurrentAccountContext"
class="rounded-md text-xs py-2 px-2 mt-2 relative border border-slate-50 dark:border-slate-800/50 hover:bg-slate-50 dark:hover:bg-slate-700 cursor-pointer"
class="text-slate-700 dark:text-slate-200 rounded-md text-xs py-2 px-2 mt-2 relative border border-slate-50 dark:border-slate-800/50 hover:bg-slate-50 dark:hover:bg-slate-800 cursor-pointer"
@mouseover="setShowSwitch"
@mouseleave="resetShowSwitch"
>
{{ $t('SIDEBAR.CURRENTLY_VIEWING_ACCOUNT') }}
<p class="text-ellipsis font-medium mb-0">
<p
class="text-ellipsis overflow-hidden whitespace-nowrap font-medium mb-0 text-slate-800 dark:text-slate-100"
>
{{ account.name }}
</p>
<transition name="fade">
<div
v-if="showSwitchButton"
class="bg-gradient-to-r rtl:bg-gradient-to-l from-transparent via-white dark:via-slate-700 to-white dark:to-slate-700 flex items-center h-full justify-end absolute top-0 right-0 w-full"
class="ltr:overlay-shadow ltr:dark:overlay-shadow-dark rtl:rtl-overlay-shadow rtl:dark:rtl-overlay-shadow-dark flex items-center h-full rounded-md justify-end absolute top-0 right-0 w-full"
>
<div class="my-0 mx-2">
<woot-button
@@ -54,7 +56,41 @@ export default {
},
};
</script>
<style scoped lang="scss">
<style scoped>
@tailwind components;
@layer components {
.overlay-shadow {
background-image: linear-gradient(
to right,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 1) 50%
);
}
.overlay-shadow-dark {
background-image: linear-gradient(
to right,
rgba(0, 0, 0, 0) 0%,
rgb(21, 23, 24) 50%
);
}
.rtl-overlay-shadow {
background-image: linear-gradient(
to left,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 1) 50%
);
}
.rtl-overlay-shadow-dark {
background-image: linear-gradient(
to left,
rgba(0, 0, 0, 0) 0%,
rgb(21, 23, 24) 50%
);
}
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 300ms ease;
@@ -62,6 +98,6 @@ export default {
.fade-enter,
.fade-leave-to {
opacity: 0;
@apply opacity-0;
}
</style>
@@ -1,7 +1,7 @@
<template>
<div class="mb-4">
<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-800 dark:hover:text-slate-100 hover:text-slate-600 relative"
class="text-slate-600 dark:text-slate-100 w-10 h-10 my-2 flex items-center justify-center rounded-lg hover:bg-slate-25 dark:hover:bg-slate-700 dark:hover:text-slate-100 hover:text-slate-600 relative"
:class="{
'bg-woot-50 dark:bg-slate-800 text-woot-500 hover:bg-woot-50': isNotificationPanelActive,
}"
@@ -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-10 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 />
@@ -60,6 +60,17 @@
</a>
</router-link>
</woot-dropdown-item>
<woot-dropdown-item>
<woot-button
variant="clear"
color-scheme="secondary"
size="small"
icon="appearance"
@click="openAppearanceOptions"
>
{{ $t('SIDEBAR_ITEMS.APPEARANCE') }}
</woot-button>
</woot-dropdown-item>
<woot-dropdown-item v-if="currentUser.type === 'SuperAdmin'">
<a
href="/super_admin"
@@ -145,6 +156,10 @@ export default {
onClickAway() {
if (this.show) this.$emit('close');
},
openAppearanceOptions() {
const ninja = document.querySelector('ninja-keys');
ninja.open({ parent: 'appearance_settings' });
},
},
};
</script>
@@ -46,6 +46,7 @@ import AgentDetails from './AgentDetails';
import NotificationBell from './NotificationBell';
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');
},
},
@@ -3,7 +3,7 @@
<a
v-tooltip.right="$t(`SIDEBAR.${name}`)"
:href="href"
class="text-slate-600 dark:text-slate-100 w-10 h-10 my-2 flex items-center justify-center rounded-lg hover:bg-slate-25 dark:hover:bg-slate-700 dark:hover:text-slate-100 hover:text-slate-600 relative"
class="text-slate-700 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':
isActive || isChildMenuActive,
@@ -22,7 +22,7 @@
class="inline-flex items-center justify-center w-4 rounded-sm bg-slate-100 dark:bg-slate-700 p-0.5 mr-1.5 rtl:mr-0 rtl:ml-1.5"
>
<fluent-icon
class="text-xxs text-slate-600 dark:text-slate-200"
class="text-xxs text-slate-700 dark:text-slate-200"
:class="{
'text-woot-500 dark:text-woot-500': isActive,
}"
@@ -41,7 +41,7 @@
>
<span
:title="menuTitle"
class="text-sm text-slate-600 dark:text-slate-100"
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,
@@ -54,8 +54,8 @@
class="bg-slate-50 dark:bg-slate-700 rounded text-xxs font-medium mx-1 py-0 px-1"
:class="
isCountZero
? `text-slate-300 dark:text-slate-600`
: `text-slate-600 dark:text-slate-50`
? `text-slate-300 dark:text-slate-700`
: `text-slate-700 dark:text-slate-50`
"
>
{{ childItemCount }}
@@ -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,87 +99,65 @@ export default {
<style lang="scss" scoped>
.banner {
--x-padding: var(--space-normal);
--y-padding: var(--space-slab);
display: flex;
gap: var(--x-padding);
color: var(--white);
font-size: var(--font-size-mini);
padding: var(--y-padding) var(--x-padding);
justify-content: center;
&.primary {
background: var(--w-500);
@apply bg-woot-500 dark:bg-woot-500;
.banner-action__button {
background: var(--w-600);
border: none;
color: var(--white);
@apply bg-woot-600 dark:bg-woot-600 border-none text-white;
&:hover {
background: var(--w-800);
@apply bg-woot-700 dark:bg-woot-700;
}
}
}
&.secondary {
background: var(--s-200);
color: var(--s-800);
@apply bg-slate-200 dark:bg-slate-300 text-slate-800 dark:text-slate-800;
a {
color: var(--s-800);
@apply text-slate-800 dark:text-slate-800;
}
}
&.alert {
background: var(--r-500);
@apply bg-red-500 dark:bg-red-500;
.banner-action__button {
background: var(--r-700);
border: none;
color: var(--white);
@apply bg-red-700 dark:bg-red-700 border-none text-white dark:text-white;
&:hover {
background: var(--r-800);
@apply bg-red-800 dark:bg-red-800;
}
}
}
&.warning {
background: var(--y-600);
color: var(--y-500);
@apply bg-yellow-500 dark:bg-yellow-500 text-yellow-500 dark:text-yellow-500;
a {
color: var(--y-500);
@apply text-yellow-500 dark:text-yellow-500;
}
}
&.gray {
background: var(--b-500);
@apply text-black-500 dark:text-black-500;
.banner-action__button {
color: var(--white);
@apply text-white dark:text-white;
}
}
a {
margin-left: var(--space-smaller);
text-decoration: underline;
color: var(--white);
font-size: var(--font-size-mini);
@apply ml-1 underline text-white dark:text-white text-xs;
}
.banner-action__button {
::v-deep .button__content {
white-space: nowrap;
@apply whitespace-nowrap;
}
}
.banner-message {
display: flex;
align-items: center;
@apply flex items-center;
}
.actions {
display: flex;
gap: var(--space-smaller);
right: var(--y-padding);
@apply flex gap-1 right-3;
}
}
</style>
@@ -1,41 +1,60 @@
<template>
<div v-if="isAIIntegrationEnabled" class="position-relative">
<woot-button
v-tooltip.top-end="$t('INTEGRATION_SETTINGS.OPEN_AI.AI_ASSIST')"
icon="wand"
color-scheme="secondary"
variant="smooth"
size="small"
@click="openAIAssist"
/>
<woot-modal
:show.sync="showAIAssistanceModal"
:on-close="hideAIAssistanceModal"
>
<AIAssistanceModal
:ai-option="aiOption"
@apply-text="insertText"
@close="hideAIAssistanceModal"
<div v-if="!isFetchingAppIntegrations">
<div v-if="isAIIntegrationEnabled" class="relative">
<AIAssistanceCTAButton
v-if="shouldShowAIAssistCTAButton"
@click="openAIAssist"
/>
</woot-modal>
<woot-button
v-else
v-tooltip.top-end="$t('INTEGRATION_SETTINGS.OPEN_AI.AI_ASSIST')"
icon="wand"
color-scheme="secondary"
variant="smooth"
size="small"
@click="openAIAssist"
/>
<woot-modal
:show.sync="showAIAssistanceModal"
:on-close="hideAIAssistanceModal"
>
<AIAssistanceModal
:ai-option="aiOption"
@apply-text="insertText"
@close="hideAIAssistanceModal"
/>
</woot-modal>
</div>
<div v-else-if="shouldShowAIAssistCTAButtonForAdmin" class="relative">
<AIAssistanceCTAButton @click="openAICta" />
<woot-modal :show.sync="showAICtaModal" :on-close="hideAICtaModal">
<AICTAModal @close="hideAICtaModal" />
</woot-modal>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import AICTAModal from './AICTAModal.vue';
import AIAssistanceModal from './AIAssistanceModal.vue';
import aiMixin from 'dashboard/mixins/aiMixin';
import adminMixin from 'dashboard/mixins/aiMixin';
import aiMixin from 'dashboard/mixins/isAdmin';
import { CMD_AI_ASSIST } from 'dashboard/routes/dashboard/commands/commandBarBusEvents';
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
import { buildHotKeys } from 'shared/helpers/KeyboardHelpers';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import AIAssistanceCTAButton from './AIAssistanceCTAButton.vue';
export default {
components: {
AIAssistanceModal,
AICTAModal,
AIAssistanceCTAButton,
},
mixins: [aiMixin, eventListenerMixins],
mixins: [aiMixin, eventListenerMixins, adminMixin, uiSettingsMixin],
data: () => ({
showAIAssistanceModal: false,
showAICtaModal: false,
aiOption: '',
initialMessage: '',
}),
@@ -43,6 +62,21 @@ export default {
...mapGetters({
currentChat: 'getSelectedChat',
}),
isAICTAModalDismissed() {
return this.uiSettings.is_open_ai_cta_modal_dismissed;
},
// Display a AI CTA button for admins if the AI integration has not been added yet and the AI assistance modal has not been dismissed.
shouldShowAIAssistCTAButtonForAdmin() {
return (
this.isAdmin &&
!this.isAIIntegrationEnabled &&
!this.isAICTAModalDismissed
);
},
// Display a AI CTA button for agents and other admins who have not yet opened the AI assistance modal.
shouldShowAIAssistCTAButton() {
return this.isAIIntegrationEnabled && !this.isAICTAModalDismissed;
},
},
mounted() {
@@ -61,13 +95,28 @@ export default {
}
},
hideAIAssistanceModal() {
this.recordAnalytics('DISMISS_AI_SUGGESTION', {
aiOption: this.aiOption,
});
this.showAIAssistanceModal = false;
},
openAIAssist() {
// Dismiss the CTA modal if it is not dismissed
if (!this.isAICTAModalDismissed) {
this.updateUISettings({
is_open_ai_cta_modal_dismissed: true,
});
}
this.initialMessage = this.draftMessage;
const ninja = document.querySelector('ninja-keys');
ninja.open({ parent: 'ai_assist' });
},
hideAICtaModal() {
this.showAICtaModal = false;
},
openAICta() {
this.showAICtaModal = true;
},
onAIAssist(option) {
this.aiOption = option;
this.showAIAssistanceModal = true;
@@ -0,0 +1,99 @@
<template>
<div class="relative">
<woot-button
icon="wand"
color-scheme="secondary"
variant="smooth"
size="small"
class-names="cta-btn cta-btn-light dark:cta-btn-dark hover:cta-btn-light-hover dark:hover:cta-btn-dark-hover"
@click="onClick"
>
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.AI_ASSIST') }}
</woot-button>
<div
class="radar-ping-animation absolute top-0 right-0 -mt-1 -mr-1 rounded-full w-3 h-3 bg-woot-500 dark:bg-woot-500"
/>
<div
class="absolute top-0 right-0 -mt-1 -mr-1 rounded-full w-3 h-3 bg-woot-500 dark:bg-woot-500 opacity-50"
/>
</div>
</template>
<script>
export default {
methods: {
onClick() {
this.$emit('click');
},
},
};
</script>
<style scoped>
@tailwind components;
@layer components {
/* Gradient animation */
@keyframes gradient {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
.cta-btn {
animation: gradient 5s ease infinite;
border: 0;
}
.cta-btn-light {
background: linear-gradient(
255.98deg,
rgba(161, 87, 246, 0.2) 15.83%,
rgba(71, 145, 247, 0.2) 81.39%
),
linear-gradient(0deg, #f2f5f8, #f2f5f8);
}
.cta-btn-dark {
background: linear-gradient(
255.98deg,
rgba(161, 87, 246, 0.2) 15.83%,
rgba(71, 145, 247, 0.2) 81.39%
),
linear-gradient(0deg, #313538, #313538);
}
.cta-btn-light-hover {
background: linear-gradient(
255.98deg,
rgba(161, 87, 246, 0.2) 15.83%,
rgba(71, 145, 247, 0.2) 81.39%
),
linear-gradient(0deg, #e3e5e7, #e3e5e7);
}
.cta-btn-dark-hover {
background: linear-gradient(
255.98deg,
rgba(161, 87, 246, 0.2) 15.83%,
rgba(71, 145, 247, 0.2) 81.39%
),
linear-gradient(0deg, #202425, #202425);
}
/* Radar ping animation */
@keyframes ping {
75%,
100% {
transform: scale(2);
opacity: 0;
}
}
.radar-ping-animation {
animation: ping 1s ease infinite;
}
}
</style>
@@ -0,0 +1,108 @@
<template>
<div class="px-0 min-w-0 flex-1">
<woot-modal-header
:header-title="$t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.TITLE')"
:header-content="$t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.DESC')"
/>
<form
class="flex flex-wrap flex-col modal-content"
@submit.prevent="finishOpenAI"
>
<div class="mt-2 w-full">
<woot-input
v-model="value"
type="text"
:class="{ error: $v.value.$error }"
:placeholder="
$t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.KEY_PLACEHOLDER')
"
@blur="$v.value.$touch"
/>
</div>
<div class="flex flex-row justify-between gap-2 py-2 px-0 w-full">
<woot-button variant="link" @click.prevent="openOpenAIDoc">
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.BUTTONS.NEED_HELP') }}
</woot-button>
<div class="flex items-center gap-1">
<woot-button variant="clear" @click.prevent="onDismiss">
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.BUTTONS.DISMISS') }}
</woot-button>
<woot-button :is-disabled="$v.value.$invalid">
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.BUTTONS.FINISH') }}
</woot-button>
</div>
</div>
</form>
</div>
</template>
<script>
import { required } from 'vuelidate/lib/validators';
import { mapGetters } from 'vuex';
import aiMixin from 'dashboard/mixins/aiMixin';
import alertMixin from 'shared/mixins/alertMixin';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import { OPEN_AI_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
export default {
mixins: [aiMixin, alertMixin, uiSettingsMixin],
data() {
return {
value: '',
};
},
validations: {
value: {
required,
},
},
computed: {
...mapGetters({
appIntegrations: 'integrations/getAppIntegrations',
}),
},
methods: {
onClose() {
this.$emit('close');
},
onDismiss() {
this.showAlert(
this.$t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.DISMISS_MESSAGE')
);
this.updateUISettings({
is_open_ai_cta_modal_dismissed: true,
});
this.onClose();
},
async finishOpenAI() {
const payload = {
app_id: 'openai',
settings: {
api_key: this.value,
},
};
try {
await this.$store.dispatch('integrations/createHook', payload);
this.alertMessage = this.$t(
'INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.SUCCESS_MESSAGE'
);
this.recordAnalytics(
OPEN_AI_EVENTS.ADDED_AI_INTEGRATION_VIA_CTA_BUTTON
);
this.onClose();
} catch (error) {
const errorMessage = error?.response?.data?.message;
this.alertMessage =
errorMessage || this.$t('INTEGRATION_APPS.ADD.API.ERROR_MESSAGE');
} finally {
this.showAlert(this.alertMessage);
}
},
openOpenAIDoc() {
window.open('https://www.chatwoot.com/blog/v2-17', '_blank');
},
},
};
</script>
@@ -213,7 +213,7 @@ export default {
}
.filter.has-error {
@apply bg-red-50 dark:bg-red-100;
@apply bg-red-50 dark:bg-red-800/50 border-red-100 dark:border-red-700/50;
}
.filter-inputs {
@@ -221,7 +221,7 @@ export default {
}
.filter-error {
@apply text-red-500 dark:text-red-600 block my-1 mx-0;
@apply text-red-500 dark:text-red-200 block my-1 mx-0;
}
.action__question,
@@ -61,11 +61,9 @@ export default {
this.label = this.$t('AUTOMATION.ATTACHMENT.LABEL_UPLOADING');
try {
const file = event.target.files[0];
const formData = new FormData();
formData.append('attachment', file, file.name);
const id = await this.$store.dispatch(
'automations/uploadAttachment',
formData
file
);
this.$emit('input', [id]);
this.uploadState = 'uploaded';
@@ -1,5 +1,8 @@
<template>
<button class="header-section back-button" @click.capture="goBack">
<button
class="header-section flex items-center text-base font-normal mr-4 ml-2 cursor-pointer text-woot-500 dark:text-woot-500"
@click.capture="goBack"
>
<fluent-icon icon="chevron-left" />
{{ buttonLabel || $t('GENERAL_SETTINGS.BACK') }}
</button>
@@ -61,21 +61,16 @@ export default {
}
.colorpicker--selected {
border: 1px solid var(--color-border-light);
border-radius: $space-smaller;
cursor: pointer;
height: $space-large;
margin-bottom: $space-normal;
width: $space-large;
@apply border border-solid border-slate-50 dark:border-slate-600 rounded cursor-pointer h-8 w-8 mb-4;
}
.colorpicker--chrome.vc-chrome {
@include elegant-card;
@apply shadow-lg -mt-2.5 absolute z-[9999] border border-solid border-slate-75 dark:border-slate-600 rounded;
border: 1px solid $color-border;
border-radius: $space-smaller;
margin-top: -$space-one;
position: absolute;
z-index: 9999;
::v-deep {
input {
@apply bg-white dark:bg-white;
}
}
}
</style>
@@ -114,10 +114,7 @@
@click="removeFilter"
/>
</div>
<p
v-if="v.values.$dirty && v.values.$error"
class="text-red-500 dark:text-red-600 block my-1 mx-0"
>
<p v-if="v.values.$dirty && v.values.$error" class="filter-error">
{{ $t('FILTER.EMPTY_VALUE_ERROR') }}
</p>
</div>
@@ -127,7 +124,7 @@
class="flex items-center justify-center relative my-2.5 mx-0"
>
<hr
class="w-full absolute border-b border-solid border-slate-75 dark:border-slate-600"
class="w-full absolute border-b border-solid border-slate-75 dark:border-slate-800"
/>
<select
v-model="query_operator"
@@ -273,8 +270,8 @@ export default {
},
getInputErrorClass(isDirty, hasError) {
return isDirty && hasError
? 'bg-red-50 dark:bg-red-200 border-red-100 dark:border-red-300 '
: 'bg-slate-50 dark:bg-slate-800 border-slate-75 dark:border-slate-600 ';
? 'bg-red-50 dark:bg-red-800/50 border-red-100 dark:border-red-700/50'
: 'bg-slate-50 dark:bg-slate-800 border-slate-75 dark:border-slate-700/50';
},
},
};
@@ -286,6 +283,10 @@ export default {
}
}
.filter-error {
@apply text-red-500 dark:text-red-200 block my-1 mx-0;
}
.multiselect {
@apply mb-0;
}
@@ -1,5 +1,5 @@
<template>
<div class="row--user-block">
<div class="flex items-center gap-1.5 text-left">
<thumbnail
:src="user.thumbnail"
:size="size"
@@ -7,7 +7,8 @@
:status="user.availability_status"
/>
<h6
class="text-block-title user-name overflow-hidden whitespace-nowrap text-ellipsis text-capitalize"
class="my-0 dark:text-slate-100 overflow-hidden whitespace-nowrap text-ellipsis text-capitalize"
:class="textClass"
>
{{ user.name }}
</h6>
@@ -29,16 +30,10 @@ export default {
type: String,
default: '20px',
},
textClass: {
type: String,
default: 'text-xs text-slate-600',
},
},
};
</script>
<style scoped lang="scss">
.row--user-block {
@apply items-center flex text-left;
.user-name {
@apply my-0 mx-2 text-slate-700 dark:text-slate-100;
}
}
</style>
@@ -15,6 +15,13 @@
:search-key="variableSearchTerm"
@click="insertVariable"
/>
<input
ref="imageUpload"
type="file"
accept="image/png, image/jpeg, image/jpg, image/gif, image/webp"
hidden
@change="onFileChange"
/>
<div ref="editor" />
</div>
</template>
@@ -22,7 +29,7 @@
<script>
import {
messageSchema,
wootMessageWriterSetup,
buildEditor,
EditorView,
MessageMarkdownTransformer,
MessageMarkdownSerializer,
@@ -37,8 +44,13 @@ import {
import TagAgents from '../conversation/TagAgents';
import CannedResponse from '../conversation/CannedResponse';
import VariableList from '../conversation/VariableList';
import {
appendSignature,
removeSignature,
} from 'dashboard/helper/editorHelper';
const TYPING_INDICATOR_IDLE_TIME = 4000;
const MAXIMUM_FILE_UPLOAD_SIZE = 4; // in MB
import {
hasPressedEnterAndNotCmdOrShift,
@@ -51,14 +63,27 @@ import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import { isEditorHotKeyEnabled } from 'dashboard/mixins/uiSettings';
import { replaceVariablesInMessage } from '@chatwoot/utils';
import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
import { uploadFile } from 'dashboard/helper/uploadHelper';
import alertMixin from 'shared/mixins/alertMixin';
import { findNodeToInsertImage } from 'dashboard/helper/messageEditorHelper';
import { MESSAGE_EDITOR_MENU_OPTIONS } from 'dashboard/constants/editor';
const createState = (content, placeholder, plugins = []) => {
const createState = (
content,
placeholder,
plugins = [],
methods = {},
enabledMenuOptions
) => {
return EditorState.create({
doc: new MessageMarkdownTransformer(messageSchema).parse(content),
plugins: wootMessageWriterSetup({
plugins: buildEditor({
schema: messageSchema,
placeholder,
methods,
plugins,
enabledMenuOptions,
}),
});
};
@@ -66,7 +91,7 @@ const createState = (content, placeholder, plugins = []) => {
export default {
name: 'WootMessageEditor',
components: { TagAgents, CannedResponse, VariableList },
mixins: [eventListenerMixins, uiSettingsMixin],
mixins: [eventListenerMixins, uiSettingsMixin, alertMixin],
props: {
value: { type: String, default: '' },
editorId: { type: String, default: '' },
@@ -78,6 +103,11 @@ export default {
enableVariables: { type: Boolean, default: false },
enableCannedResponses: { type: Boolean, default: true },
variables: { type: Object, default: () => ({}) },
enabledMenuOptions: { type: Array, default: () => [] },
signature: { type: String, default: '' },
// allowSignature is a kill switch, ensuring no signature methods
// are triggered except when this flag is true
allowSignature: { type: Boolean, default: false },
},
data() {
return {
@@ -104,6 +134,11 @@ export default {
this.enableCannedResponses && this.showCannedMenu && !this.isPrivate
);
},
editorMenuOptions() {
return this.enabledMenuOptions.length
? this.enabledMenuOptions
: MESSAGE_EDITOR_MENU_OPTIONS;
},
plugins() {
if (!this.enableSuggestions) {
return [];
@@ -193,6 +228,12 @@ export default {
}),
];
},
sendWithSignature() {
// this is considered the source of truth, we watch this property
// on change, we toggle the signature in the editor
const { send_with_signature: isEnabled } = this.uiSettings;
return isEnabled && this.allowSignature && !this.isPrivate;
},
},
watch: {
showUserMentions(updatedValue) {
@@ -204,20 +245,19 @@ export default {
showVariables(updatedValue) {
this.$emit('toggle-variables-menu', !this.isPrivate && updatedValue);
},
value(newValue = '') {
if (newValue !== this.contentFromEditor) {
this.reloadState();
value(newVal = '') {
if (newVal !== this.contentFromEditor) {
this.reloadState(newVal);
}
},
editorId() {
this.showCannedMenu = false;
this.cannedSearchTerm = '';
this.reloadState();
this.reloadState(this.value);
},
isPrivate() {
this.reloadState();
this.reloadState(this.value);
},
updateSelectionWith(newValue, oldValue) {
if (!this.editorView) {
return null;
@@ -236,20 +276,106 @@ export default {
}
return null;
},
sendWithSignature(newValue) {
// see if the allowSignature flag is true
if (this.allowSignature) {
this.toggleSignatureInEditor(newValue);
}
},
},
created() {
this.state = createState(this.value, this.placeholder, this.plugins);
this.state = createState(
this.value,
this.placeholder,
this.plugins,
{ onImageUpload: this.openFileBrowser },
this.editorMenuOptions
);
},
mounted() {
this.createEditorView();
this.editorView.updateState(this.state);
this.focusEditorInputField();
this.focusEditor(this.value);
},
methods: {
reloadState() {
this.state = createState(this.value, this.placeholder, this.plugins);
reloadState(content = this.value) {
this.state = createState(
content,
this.placeholder,
this.plugins,
{ onImageUpload: this.openFileBrowser },
this.editorMenuOptions
);
this.editorView.updateState(this.state);
this.focusEditorInputField();
this.focusEditor(content);
},
focusEditor(content) {
if (this.isBodyEmpty(content) && this.sendWithSignature) {
// reload state can be called when switching between conversations, or when drafts is loaded
// these drafts can also have a signature, so we need to check if the body is empty
// and handle things accordingly
this.handleEmptyBodyWithSignature();
} else {
// this is in the else block, handleEmptyBodyWithSignature also has a call to the focus method
// the position is set to start, because the signature is added at the end of the body
this.focusEditorInputField('end');
}
},
toggleSignatureInEditor(signatureEnabled) {
// The toggleSignatureInEditor gets the new value from the
// watcher, this means that if the value is true, the signature
// is supposed to be added, else we remove it.
if (signatureEnabled) {
this.addSignature();
} else {
this.removeSignature();
}
},
addSignature() {
let content = this.value;
// see if the content is empty, if it is before appending the signature
// we need to add a paragraph node and move the cursor at the start of the editor
const contentWasEmpty = this.isBodyEmpty(content);
content = appendSignature(content, this.signature);
// need to reload first, ensuring that the editorView is updated
this.reloadState(content);
if (contentWasEmpty) {
this.handleEmptyBodyWithSignature();
}
},
removeSignature() {
if (!this.signature) return;
let content = this.value;
content = removeSignature(content, this.signature);
// reload the state, ensuring that the editorView is updated
this.reloadState(content);
},
isBodyEmpty(content) {
// if content is undefined, we assume that the body is empty
if (!content) return true;
// if the signature is present, we need to remove it before checking
// note that we don't update the editorView, so this is safe
const bodyWithoutSignature = this.signature
? removeSignature(content, this.signature)
: content;
// trimming should remove all the whitespaces, so we can check the length
return bodyWithoutSignature.trim().length === 0;
},
handleEmptyBodyWithSignature() {
const { schema, tr } = this.state;
// create a paragraph node and
// start a transaction to append it at the end
const paragraph = schema.nodes.paragraph.create();
const paragraphTransaction = tr.insert(0, paragraph);
this.editorView.dispatch(paragraphTransaction);
// Set the focus at the start of the input field
this.focusEditorInputField('start');
},
createEditorView() {
this.editorView = new EditorView(this.$refs.editor, {
@@ -294,9 +420,11 @@ export default {
this.focusEditorInputField();
}
},
focusEditorInputField() {
focusEditorInputField(pos = 'end') {
const { tr } = this.editorView.state;
const selection = Selection.atEnd(tr.doc);
const selection =
pos === 'end' ? Selection.atEnd(tr.doc) : Selection.atStart(tr.doc);
this.editorView.dispatch(tr.setSelection(selection));
this.editorView.focus();
@@ -374,6 +502,57 @@ export default {
tr.scrollIntoView();
return false;
},
openFileBrowser() {
this.$refs.imageUpload.click();
},
onFileChange() {
const file = this.$refs.imageUpload.files[0];
if (checkFileSizeLimit(file, MAXIMUM_FILE_UPLOAD_SIZE)) {
this.uploadImageToStorage(file);
} else {
this.showAlert(
this.$t(
'PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.IMAGE_UPLOAD_SIZE_ERROR',
{
size: MAXIMUM_FILE_UPLOAD_SIZE,
}
)
);
}
this.$refs.imageUpload.value = '';
},
async uploadImageToStorage(file) {
try {
const { fileUrl } = await uploadFile(file);
if (fileUrl) {
this.onImageInsertInEditor(fileUrl);
}
this.showAlert(
this.$t(
'PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.IMAGE_UPLOAD_SUCCESS'
)
);
} catch (error) {
this.showAlert(
this.$t(
'PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.IMAGE_UPLOAD_ERROR'
)
);
}
},
onImageInsertInEditor(fileUrl) {
const { tr } = this.editorView.state;
const insertData = findNodeToInsertImage(this.editorView.state, fileUrl);
if (insertData) {
this.editorView.dispatch(
tr.insert(insertData.pos, insertData.node).scrollIntoView()
);
this.focusEditorInputField();
}
},
emitOnChange() {
this.editorView.updateState(this.state);
@@ -456,6 +635,28 @@ export default {
}
> .ProseMirror {
@apply p-0 break-words text-slate-800 dark:text-slate-100;
h1,
h2,
h3,
h4,
h5,
h6,
p {
@apply text-slate-800 dark:text-slate-100;
}
blockquote {
@apply border-slate-400 dark:border-slate-500;
p {
@apply text-slate-600 dark:text-slate-400;
}
}
ol li {
@apply list-item list-decimal;
}
}
}
@@ -469,20 +670,28 @@ export default {
.ProseMirror-prompt {
@apply z-50 bg-slate-25 dark:bg-slate-700 rounded-md border border-solid border-slate-75 dark:border-slate-800;
h5 {
@apply dark:text-slate-25 text-slate-800;
}
}
.is-private {
.prosemirror-mention-node {
@apply font-medium bg-slate-50 dark:bg-slate-200 text-slate-900 dark:text-slate-900 py-0 px-1;
@apply font-medium bg-yellow-100 dark:bg-yellow-800 text-slate-900 dark:text-slate-25 py-0 px-1;
}
.ProseMirror-menubar-wrapper {
.ProseMirror-menubar {
@apply bg-yellow-50 dark:bg-yellow-50 text-slate-700 dark:text-slate-700;
@apply bg-yellow-100 dark:bg-yellow-800 text-slate-700 dark:text-slate-25;
}
> .ProseMirror {
@apply text-slate-800 dark:text-slate-800;
@apply text-slate-800 dark:text-slate-25;
p {
@apply text-slate-800 dark:text-slate-25;
}
}
}
}
@@ -16,7 +16,7 @@
<script>
import {
fullSchema,
wootArticleWriterSetup,
buildEditor,
EditorView,
ArticleMarkdownSerializer,
ArticleMarkdownTransformer,
@@ -33,15 +33,17 @@ const createState = (
content,
placeholder,
plugins = [],
onImageUpload = () => {}
methods = {},
enabledMenuOptions
) => {
return EditorState.create({
doc: new ArticleMarkdownTransformer(fullSchema).parse(content),
plugins: wootArticleWriterSetup({
plugins: buildEditor({
schema: fullSchema,
placeholder,
methods,
plugins,
onImageUpload,
enabledMenuOptions,
}),
});
};
@@ -52,6 +54,7 @@ export default {
value: { type: String, default: '' },
editorId: { type: String, default: '' },
placeholder: { type: String, default: '' },
enabledMenuOptions: { type: Array, default: () => [] },
},
data() {
return {
@@ -83,7 +86,8 @@ export default {
this.value,
this.placeholder,
this.plugins,
this.openFileBrowser
{ onImageUpload: this.openFileBrowser },
this.enabledMenuOptions
);
},
mounted() {
@@ -152,7 +156,8 @@ export default {
this.value,
this.placeholder,
this.plugins,
this.openFileBrowser
{ onImageUpload: this.openFileBrowser },
this.enabledMenuOptions
);
this.editorView.updateState(this.state);
this.focusEditorInputField();
@@ -213,7 +218,7 @@ export default {
</script>
<style lang="scss">
@import '~@chatwoot/prosemirror-schema/src/styles/article.scss';
@import '@chatwoot/prosemirror-schema/src/styles/article.scss';
.ProseMirror-menubar-wrapper {
display: flex;
@@ -288,7 +288,7 @@ export default {
}
},
showMessageSignatureButton() {
return !this.isOnPrivateNote && this.isAnEmailChannel;
return !this.isOnPrivateNote;
},
sendWithSignature() {
const { send_with_signature: isEnabled } = this.uiSettings;
@@ -326,7 +326,7 @@ export default {
@apply flex justify-between py-3 px-4;
&.is-note-mode {
@apply bg-yellow-50 dark:bg-yellow-50;
@apply bg-yellow-100 dark:bg-yellow-800;
}
}
@@ -342,7 +342,7 @@ export default {
label {
@apply cursor-pointer;
}
&:hover .button {
&:hover button {
@apply dark:bg-slate-800 bg-slate-100;
}
}
@@ -137,7 +137,7 @@ export default {
.button--note {
@apply border-l-0 rounded-none;
&.is-active {
@apply border-r border-b-0 bg-yellow-50 dark:bg-yellow-50 border-t-0 border-slate-50 dark:border-slate-700;
@apply border-r border-b-0 bg-yellow-100 dark:bg-yellow-800 border-t-0 border-slate-50 dark:border-slate-700;
}
&:hover,
&:active {
@@ -23,7 +23,7 @@
</label>
</div>
<div
class="p-4 rounded-lg bg-slate-25 dark:bg-slate-700 border border-solid border-slate-50 dark:border-slate-700 mb-4"
class="p-4 rounded-lg bg-slate-25 dark:bg-slate-900 border border-solid border-slate-50 dark:border-slate-700/50 mb-4"
>
<filter-input-box
v-for="(filter, i) in appliedFilters"
@@ -358,6 +358,7 @@ export default {
applied_filters: this.appliedFilters.map(filter => ({
key: filter.attribute_key,
operator: filter.filter_operator,
query_operator: filter.query_operator,
})),
});
},
@@ -61,7 +61,7 @@ import { mapGetters } from 'vuex';
import ContactPanel from 'dashboard/routes/dashboard/conversation/ContactPanel';
import ConversationHeader from './ConversationHeader';
import DashboardAppFrame from '../DashboardApp/Frame.vue';
import EmptyState from './EmptyState';
import EmptyState from './EmptyState/EmptyState';
import MessagesView from './MessagesView';
export default {
@@ -1,8 +1,8 @@
<template>
<div
class="conversation flex flex-shrink-0 flex-grow-0 w-auto max-w-full cursor-pointer relative py-0 px-4 border-transparent border-l-2 border-t-0 border-b-0 border-r-0 border-solid items-start hover:bg-slate-50 dark:hover:bg-slate-800 group"
class="conversation flex flex-shrink-0 flex-grow-0 w-auto max-w-full cursor-pointer relative py-0 px-4 border-transparent border-l-2 border-t-0 border-b-0 border-r-0 border-solid items-start hover:bg-slate-25 dark:hover:bg-slate-800 group"
:class="{
'active bg-slate-50 dark:bg-slate-800 border-woot-500': isActiveChat,
'active bg-slate-25 dark:bg-slate-800 border-woot-500': isActiveChat,
'unread-chat': hasUnread,
'has-inbox-name': showInboxName,
'conversation-selected': selected,
@@ -84,7 +84,7 @@
<fluent-icon
v-if="attachmentIcon"
size="16"
class="-mt-0.5 align-middle inline-block"
class="-mt-0.5 align-middle inline-block text-slate-600 dark:text-slate-300"
:icon="attachmentIcon"
/>
{{ this.$t(`${attachmentMessageContent}`) }}
@@ -99,8 +99,8 @@
>
<fluent-icon
size="16"
class="-mt-0.5 align-middle"
icon="info inline-block"
class="-mt-0.5 align-middle inline-block text-slate-600 dark:text-slate-300"
icon="info"
/>
<span>
{{ this.$t(`CHAT_LIST.NO_MESSAGES`) }}
@@ -8,7 +8,11 @@
<div
class="flex justify-center items-center mr-4 rtl:mr-0 rtl:ml-4 min-w-0"
>
<back-button v-if="showBackButton" :back-url="backButtonUrl" />
<back-button
v-if="showBackButton"
:back-url="backButtonUrl"
class="ltr:ml-0 rtl:mr-0 rtl:ml-4"
/>
<Thumbnail
:src="currentContact.thumbnail"
:badge="inboxBadge"
@@ -10,45 +10,40 @@
class="clearfix"
>
<onboarding-view v-if="isAdmin" />
<div v-else class="current-chat">
<div>
<img src="~dashboard/assets/images/inboxes.svg" alt="No Inboxes" />
<span>
{{ $t('CONVERSATION.NO_INBOX_AGENT') }}
</span>
</div>
</div>
<empty-state-message
v-else
:message="$t('CONVERSATION.NO_INBOX_AGENT')"
/>
</div>
<!-- Show empty state images if not loading -->
<div
v-else-if="!uiFlags.isFetching && !loadingChatList"
class="current-chat"
class="flex flex-col items-center justify-center h-full"
>
<!-- No conversations available -->
<div v-if="!allConversations.length">
<img src="~dashboard/assets/images/chat.svg" alt="No Chat" />
<span>
{{ $t('CONVERSATION.NO_MESSAGE_1') }}
<br />
</span>
</div>
<!-- No conversation selected -->
<div v-else-if="allConversations.length && !currentChat.id">
<img src="~dashboard/assets/images/chat.svg" alt="No Chat" />
<span>{{ conversationMissingMessage }}</span>
</div>
<empty-state-message
v-if="!allConversations.length"
:message="$t('CONVERSATION.NO_MESSAGE_1')"
/>
<empty-state-message
v-else-if="allConversations.length && !currentChat.id"
:message="conversationMissingMessage"
/>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import adminMixin from '../../../mixins/isAdmin';
import accountMixin from '../../../mixins/account';
import OnboardingView from './OnboardingView';
import adminMixin from 'dashboard/mixins/isAdmin';
import accountMixin from 'dashboard/mixins/account';
import OnboardingView from '../OnboardingView';
import EmptyStateMessage from './EmptyStateMessage';
export default {
components: {
OnboardingView,
EmptyStateMessage,
},
mixins: [accountMixin, adminMixin],
props: {
@@ -87,51 +82,10 @@ export default {
!this.loadingChatList &&
this.isAdmin
) {
return 'inbox-empty-state';
return 'h-full overflow-auto';
}
return 'columns conv-empty-state';
return 'flex-1 min-w-0 px-0 flex flex-col items-center justify-center h-full';
},
},
};
</script>
<style lang="scss" scoped>
.inbox-empty-state {
height: 100%;
overflow: auto;
}
.current-chat {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
div {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
img {
margin: var(--space-normal);
width: 6.25rem;
}
span {
font-size: var(--font-size-small);
font-weight: var(--font-weight-medium);
text-align: center;
}
}
}
.conv-empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
}
</style>
@@ -0,0 +1,35 @@
<template>
<div class="flex flex-col items-center justify-center h-full">
<img
class="m-4 w-32 hidden dark:block"
src="~dashboard/assets/images/no-chat-dark.svg"
alt="No Chat dark"
/>
<img
class="m-4 w-32 block dark:hidden"
src="~dashboard/assets/images/no-chat.svg"
alt="No Chat"
/>
<span
class="text-sm text-slate-800 dark:text-slate-200 font-medium text-center"
>
{{ message }}
<br />
</span>
<!-- Cmd bar, keyboard shortcuts placeholder -->
<feature-placeholder />
</div>
</template>
<script>
import FeaturePlaceholder from './FeaturePlaceholder.vue';
export default {
components: { FeaturePlaceholder },
props: {
message: {
type: String,
required: true,
},
},
};
</script>
@@ -0,0 +1,51 @@
<template>
<div class="flex flex-col gap-2 mt-9">
<div
v-for="keyShortcut in keyShortcuts"
:key="keyShortcut.key"
class="flex gap-2 items-center"
>
<div class="flex gap-2 items-center">
<hotkey
custom-class="h-6 w-8 text-sm font-medium text-slate-700 dark:text-slate-100 bg-slate-100 dark:bg-slate-700 border-b-2 border-slate-300 dark:border-slate-500"
>
</hotkey>
<hotkey
custom-class="h-6 w-8 text-sm font-medium text-slate-700 dark:text-slate-100 bg-slate-100 dark:bg-slate-700 border-b-2 border-slate-300 dark:border-slate-500"
>
{{ keyShortcut.key }}
</hotkey>
</div>
<span
class="text-sm text-slate-700 dark:text-slate-300 font-medium text-center"
>
{{ keyShortcut.description }}
</span>
</div>
</div>
</template>
<script>
import Hotkey from 'dashboard/components/base/Hotkey';
export default {
components: {
Hotkey,
},
data() {
return {
keyShortcuts: [
{
key: 'K',
description: this.$t('CONVERSATION.EMPTY_STATE.CMD_BAR'),
},
{
key: '/',
description: this.$t('CONVERSATION.EMPTY_STATE.KEYBOARD_SHORTCUTS'),
},
],
};
},
};
</script>
@@ -552,10 +552,10 @@ export default {
}
&.is-private.is-text > .message-text__wrap .link {
@apply text-woot-700 dark:text-woot-700;
@apply text-woot-600 dark:text-woot-200;
}
&.is-private.is-text > .message-text__wrap .prosemirror-mention-node {
@apply font-bold bg-none rounded-sm p-0 text-slate-700 dark:text-slate-700 underline;
@apply font-bold bg-none rounded-sm p-0 bg-yellow-100 dark:bg-yellow-700 text-slate-700 dark:text-slate-25 underline;
}
&.is-from-bot {
@@ -651,6 +651,10 @@ li.right {
@apply bg-slate-75 dark:bg-slate-700 inline-block leading-none rounded-sm p-1;
}
ol li {
@apply list-item list-decimal;
}
pre {
@apply bg-slate-75 dark:bg-slate-700 block border-slate-75 dark:border-slate-700 text-slate-800 dark:text-slate-100 rounded-md p-2 mt-1 mb-2 leading-relaxed whitespace-pre-wrap;
@@ -660,7 +664,11 @@ li.right {
}
blockquote {
@apply border-l-4 mx-0 my-1 pt-2 pr-2 pb-0 pl-4 border-slate-75 border-solid dark:border-slate-700 text-slate-800 dark:text-slate-100;
@apply border-l-4 mx-0 my-1 pt-2 pr-2 pb-0 pl-4 border-slate-75 border-solid dark:border-slate-600 text-slate-800 dark:text-slate-100;
p {
@apply text-slate-800 dark:text-slate-300;
}
}
}
@@ -45,7 +45,7 @@
/>
<li v-show="unreadMessageCount != 0" class="unread--toast">
<span>
{{ unreadMessageCount }}
{{ unreadMessageCount > 9 ? '9+' : unreadMessageCount }}
{{
unreadMessageCount > 1
? $t('CONVERSATION.UNREAD_MESSAGES')
@@ -119,11 +119,13 @@ import aiMixin from 'dashboard/mixins/aiMixin';
import { getTypingUsersText } from '../../../helper/commons';
import { calculateScrollTop } from './helpers/scrollTopCalculationHelper';
import { isEscape } from 'shared/helpers/KeyboardHelpers';
import { LocalStorage } from 'shared/helpers/localStorage';
// constants
import { BUS_EVENTS } from 'shared/constants/busEvents';
import { REPLY_POLICY } from 'shared/constants/links';
import wootConstants from 'dashboard/constants/globals';
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
export default {
components: {
@@ -312,7 +314,7 @@ export default {
return '';
},
unreadMessageCount() {
return this.currentChat.unread_count;
return this.currentChat.unread_count || 0;
},
},
@@ -392,8 +394,11 @@ export default {
});
},
isLabelSuggestionDismissed() {
const dismissed = this.getDismissedConversations(this.currentAccountId);
return dismissed[this.currentAccountId].includes(this.conversationId);
return LocalStorage.getFlag(
LOCAL_STORAGE_KEYS.DISMISSED_LABEL_SUGGESTIONS,
this.currentAccountId,
this.currentChat.id
);
},
fetchAllAttachmentsFromCurrentChat() {
this.$store.dispatch('fetchAllAttachments', this.currentChat.id);
@@ -2,8 +2,9 @@
<div class="reply-box" :class="replyBoxClass">
<banner
v-if="showSelfAssignBanner"
action-button-variant="link"
action-button-variant="clear"
color-scheme="secondary"
class="banner--self-assign"
:banner-message="$t('CONVERSATION.NOT_ASSIGNED_TO_YOU')"
:has-action-button="true"
:action-button-label="$t('CONVERSATION.ASSIGN_TO_ME')"
@@ -52,6 +53,9 @@
class="input"
:placeholder="messagePlaceHolder"
:min-height="4"
:signature="signatureToApply"
:allow-signature="true"
:send-with-signature="sendWithSignature"
@typing-off="onTypingOff"
@typing-on="onTypingOn"
@focus="onFocus"
@@ -68,6 +72,8 @@
:min-height="4"
:enable-variables="true"
:variables="messageVariables"
:signature="signatureToApply"
:allow-signature="true"
@typing-off="onTypingOff"
@typing-on="onTypingOn"
@focus="onFocus"
@@ -85,16 +91,10 @@
/>
</div>
<div
v-if="isSignatureEnabledForInbox"
v-tooltip="$t('CONVERSATION.FOOTER.MESSAGE_SIGN_TOOLTIP')"
v-if="isSignatureEnabledForInbox && !isSignatureAvailable"
class="message-signature-wrap"
>
<p
v-if="isSignatureAvailable"
v-dompurify-html="formatMessage(messageSignature)"
class="message-signature"
/>
<p v-else class="message-signature">
<p class="message-signature">
{{ $t('CONVERSATION.FOOTER.MESSAGE_SIGNATURE_NOT_CONFIGURED') }}
<router-link :to="profilePath">
{{ $t('CONVERSATION.FOOTER.CLICK_HERE') }}
@@ -183,6 +183,12 @@ import wootConstants from 'dashboard/constants/globals';
import { isEditorHotKeyEnabled } from 'dashboard/mixins/uiSettings';
import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
import rtlMixin from 'shared/mixins/rtlMixin';
import {
appendSignature,
removeSignature,
replaceSignature,
extractTextFromMarkdown,
} from 'dashboard/helper/editorHelper';
const EmojiInput = () => import('shared/components/emoji/EmojiInput');
@@ -470,10 +476,10 @@ export default {
);
},
isSignatureEnabledForInbox() {
return !this.isPrivate && this.isAnEmailChannel && this.sendWithSignature;
return !this.isPrivate && this.sendWithSignature;
},
isSignatureAvailable() {
return !!this.messageSignature;
return !!this.signatureToApply;
},
sendWithSignature() {
const { send_with_signature: isEnabled } = this.uiSettings;
@@ -513,6 +519,12 @@ export default {
});
return variables;
},
// ensure that the signature is plain text depending on `showRichContentEditor`
signatureToApply() {
return this.showRichContentEditor
? this.messageSignature
: extractTextFromMarkdown(this.messageSignature);
},
},
watch: {
currentChat(conversation) {
@@ -580,6 +592,23 @@ export default {
this.updateUISettings({
display_rich_content_editor: !this.showRichContentEditor,
});
const plainTextSignature = extractTextFromMarkdown(this.messageSignature);
if (!this.showRichContentEditor && this.messageSignature) {
// remove the old signature -> extract text from markdown -> attach new signature
let message = removeSignature(this.message, this.messageSignature);
message = extractTextFromMarkdown(message);
message = appendSignature(message, plainTextSignature);
this.message = message;
} else {
this.message = replaceSignature(
this.message,
plainTextSignature,
this.messageSignature
);
}
},
saveDraft(conversationId, replyType) {
if (this.message || this.message === '') {
@@ -599,9 +628,22 @@ export default {
getFromDraft() {
if (this.conversationIdByRoute) {
const key = `draft-${this.conversationIdByRoute}-${this.replyType}`;
this.message = this.$store.getters['draftMessages/get'](key) || '';
const messageFromStore =
this.$store.getters['draftMessages/get'](key) || '';
// ensure that the message has signature set based on the ui setting
this.message = this.toggleSignatureForDraft(messageFromStore);
}
},
toggleSignatureForDraft(message) {
if (this.isPrivate) {
return message;
}
return this.sendWithSignature
? appendSignature(message, this.signatureToApply)
: removeSignature(message, this.signatureToApply);
},
removeFromDraft() {
if (this.conversationIdByRoute) {
const key = `draft-${this.conversationIdByRoute}-${this.replyType}`;
@@ -693,19 +735,14 @@ export default {
return;
}
if (!this.showMentions) {
let newMessage = this.message;
if (this.isSignatureEnabledForInbox && this.messageSignature) {
newMessage += '\n\n' + this.messageSignature;
}
const isOnWhatsApp =
this.isATwilioWhatsAppChannel ||
this.isAWhatsAppCloudChannel ||
this.is360DialogWhatsAppChannel;
if (isOnWhatsApp && !this.isPrivate) {
this.sendMessageAsMultipleMessages(newMessage);
this.sendMessageAsMultipleMessages(this.message);
} else {
const messagePayload = this.getMessagePayload(newMessage);
const messagePayload = this.getMessagePayload(this.message);
this.sendMessage(messagePayload);
}
@@ -724,6 +761,15 @@ export default {
this.sendMessage(messagePayload);
});
},
sendMessageAnalyticsData(isPrivate) {
// Analytics data for message signature is enabled or not in channels
return isPrivate
? this.$track(CONVERSATION_EVENTS.SENT_PRIVATE_NOTE)
: this.$track(CONVERSATION_EVENTS.SENT_MESSAGE, {
channelType: this.channelType,
signatureEnabled: this.sendWithSignature,
});
},
async onSendReply() {
const undefinedVariables = getUndefinedVariablesInMessage({
message: this.message,
@@ -757,6 +803,7 @@ export default {
bus.$emit(BUS_EVENTS.SCROLL_TO_MESSAGE);
bus.$emit(BUS_EVENTS.MESSAGE_SENT);
this.removeFromDraft();
this.sendMessageAnalyticsData(messagePayload.private);
} catch (error) {
const errorMessage =
error?.response?.data?.error || this.$t('CONVERSATION.MESSAGE_ERROR');
@@ -817,6 +864,10 @@ export default {
},
clearMessage() {
this.message = '';
if (this.sendWithSignature && !this.isPrivate) {
// if signature is enabled, append it to the message
this.message = appendSignature(this.message, this.signatureToApply);
}
this.attachedFiles = [];
this.isRecordingAudio = false;
},
@@ -1094,6 +1145,10 @@ export default {
@apply mb-0;
}
.banner--self-assign {
@apply py-2;
}
.message-signature-wrap {
@apply my-0 mx-4 px-1 flex max-h-[8vh] items-baseline justify-between hover:bg-slate-25 dark:hover:bg-slate-800 border border-dashed border-slate-100 dark:border-slate-700 rounded-sm overflow-auto;
}
@@ -1110,7 +1165,7 @@ export default {
@apply border-t border-slate-50 dark:border-slate-700 bg-white dark:bg-slate-900;
&.is-private {
@apply bg-yellow-50 dark:bg-yellow-50;
@apply bg-yellow-50 dark:bg-yellow-200;
}
}
.send-button {
@@ -1122,41 +1177,29 @@ export default {
}
.emoji-dialog {
top: unset;
bottom: -40px;
left: -320px;
right: unset;
@apply top-[unset] -bottom-10 -left-80 right-[unset];
&::before {
right: var(--space-minus-normal);
bottom: var(--space-small);
transform: rotate(270deg);
filter: drop-shadow(0px 4px 4px rgba(0, 0, 0, 0.08));
@apply -right-4 bottom-2 rtl:right-0 rtl:-left-4;
}
}
.emoji-dialog--rtl {
left: unset;
right: -320px;
@apply left-[unset] -right-80;
&::before {
left: var(--space-minus-normal);
transform: rotate(90deg);
right: 0;
bottom: var(--space-small);
filter: drop-shadow(0px 4px 4px rgba(0, 0, 0, 0.08));
}
}
.emoji-dialog--expanded {
left: unset;
bottom: var(--space-jumbo);
position: absolute;
z-index: var(--z-index-normal);
@apply left-[unset] bottom-0 absolute z-[100];
&::before {
transform: rotate(0deg);
left: var(--space-smaller);
bottom: var(--space-minus-small);
@apply left-1 -bottom-2;
}
}
.message-signature {
@@ -95,7 +95,7 @@ export default {
<style scoped lang="scss">
.mention--box {
@apply bg-white text-sm dark:bg-slate-700 rounded-md overflow-auto absolute w-full z-[100] pt-2 px-2 pb-0 shadow-md left-0 leading-[1.2] bottom-full max-h-[12.5rem] border-t border-solid border-slate-75 dark:border-slate-800;
@apply bg-white text-sm dark:bg-slate-700 rounded-md overflow-auto absolute w-full z-20 pt-2 px-2 pb-0 shadow-md left-0 leading-[1.2] bottom-full max-h-[12.5rem] border-t border-solid border-slate-75 dark:border-slate-800;
&.with-bottom-border {
@apply border-b-[0.5rem] border-solid border-white dark:border-slate-600;
@@ -21,7 +21,9 @@
<p class="label-title">
{{ template.name }}
</p>
<span class="label-lang label">
<span
class="inline-block py-1 px-2 rounded-sm text-xs leading-none cursor-default bg-white dark:bg-slate-700 text-slate-800 dark:text-slate-100"
>
{{ $t('WHATSAPP_TEMPLATES.PICKER.LABELS.LANGUAGE') }} :
{{ template.language }}
</span>
@@ -114,7 +116,7 @@ export default {
@apply bg-slate-25 dark:bg-slate-900 rounded-md max-h-[18.75rem] overflow-y-auto p-2.5;
.template__list-item {
@apply rounded-lg cursor-pointer block p-2.5 text-left w-full hover:bg-woot-50 dark:hover:bg-slate-600;
@apply rounded-lg cursor-pointer block p-2.5 text-left w-full hover:bg-woot-50 dark:hover:bg-slate-800;
.label-title {
@apply text-sm;
@@ -233,7 +233,7 @@ export default {
return false;
}
if (this.isAWebWidgetInbox) {
if (this.isAWebWidgetInbox || this.isAPIInbox) {
const { contact_last_seen_at: contactLastSeenAt } = this.currentChat;
return contactLastSeenAt >= this.createdAt;
}
@@ -8,7 +8,7 @@
<div class="wrap">
<div class="label-suggestion--container">
<h6 class="label-suggestion--title">Suggested labels</h6>
<div v-if="!fetchingSuggestions" class="label-suggestion--options">
<div class="label-suggestion--options">
<button
v-for="label in preparedLabels"
:key="label.title"
@@ -123,7 +123,6 @@ export default {
data() {
return {
isDismissed: false,
fetchingSuggestions: false,
isHovered: false,
selectedLabels: [],
};
@@ -160,11 +159,7 @@ export default {
if (this.isDismissed) return false;
if (!this.isAIIntegrationEnabled) return false;
return (
!this.fetchingSuggestions &&
this.preparedLabels.length &&
this.chatLabels.length === 0
);
return this.preparedLabels.length && this.chatLabels.length === 0;
},
},
watch: {
@@ -190,12 +185,10 @@ export default {
}
},
dismissSuggestions() {
const dismissed = this.getDismissedConversations(this.currentAccountId);
dismissed[this.currentAccountId].push(this.conversationId);
LocalStorage.set(
LocalStorage.setFlag(
LOCAL_STORAGE_KEYS.DISMISSED_LABEL_SUGGESTIONS,
dismissed
this.currentAccountId,
this.conversationId
);
// dismiss this once the values are set
@@ -203,8 +196,11 @@ export default {
this.trackLabelEvent(OPEN_AI_EVENTS.DISMISS_LABEL_SUGGESTION);
},
isConversationDismissed() {
const dismissed = this.getDismissedConversations(this.currentAccountId);
return dismissed[this.currentAccountId].includes(this.conversationId);
return LocalStorage.getFlag(
LOCAL_STORAGE_KEYS.DISMISSED_LABEL_SUGGESTIONS,
this.currentAccountId,
this.conversationId
);
},
addAllLabels() {
let labelsToAdd = this.selectedLabels;
@@ -215,7 +211,7 @@ export default {
conversationId: this.conversationId,
labels: labelsToAdd,
});
this.trackLabelEvent(OPEN_AI_EVENTS.LABEL_SUGGESTION_APPLIED);
this.trackLabelEvent(OPEN_AI_EVENTS.APPLY_LABEL_SUGGESTION);
},
trackLabelEvent(event) {
const payload = {
@@ -2,7 +2,7 @@
<div class="phone-input--wrap relative">
<div class="phone-input" :class="{ 'has-error': error }">
<div
class="cursor-pointer py-2 pr-1 pl-2 rounded-tl-md rounded-bl-md flex items-center justify-center gap-2 bg-slate-25 dark:bg-slate-700 h-10 w-[3.25rem]"
class="cursor-pointer py-2 pr-1.5 pl-2 rounded-tl-md rounded-bl-md flex items-center justify-center gap-1.5 bg-slate-25 dark:bg-slate-700 h-10 w-14"
@click="toggleCountryDropdown"
>
<h5 v-if="activeCountry.emoji" class="mb-0">
@@ -79,7 +79,7 @@ export default {
<style scoped lang="scss">
.mention--box {
@apply bg-white dark:bg-slate-700 rounded-md overflow-auto absolute w-full z-[100] pt-2 px-2 pb-0 shadow-md left-0 bottom-full max-h-[9.75rem] border-t border-solid border-slate-75 dark:border-slate-800;
@apply bg-white dark:bg-slate-700 rounded-md overflow-auto absolute w-full z-20 pt-2 px-2 pb-0 shadow-md left-0 bottom-full max-h-[9.75rem] border-t border-solid border-slate-75 dark:border-slate-800;
.dropdown-menu__item:last-child {
@apply pb-1;
@@ -4,22 +4,18 @@
<woot-modal-header
:header-title="$t('SIDEBAR_ITEMS.KEYBOARD_SHORTCUTS')"
/>
<div class="grid grid-cols-2 gap-x-5 gap-y-3 pt-0 px-8 pb-8 mt-6">
<div class="grid grid-cols-2 gap-x-5 gap-y-3 pt-0 px-8 pb-4 mt-6">
<div class="flex justify-between items-center min-w-[25rem]">
<h5 class="text-sm text-slate-800 dark:text-slate-100">
{{ $t('KEYBOARD_SHORTCUTS.TOGGLE_MODAL') }}
</h5>
<div class="flex items-center mb-1 ml-2">
<p
class="shortcut-key items-center rounded-md mr-2 py-2 px-2.5 bg-white dark:bg-slate-900 text-slate-800 dark:text-slate-100 font-semibold text-xs"
>
<div class="flex items-center mb-1 ml-2 gap-2">
<hotkey custom-class="min-h-[28px] min-w-[60px] normal-case key">
{{ $t('KEYBOARD_SHORTCUTS.KEYS.WINDOWS_KEY_AND_COMMAND_KEY') }}
</p>
<p
class="shortcut-key items-center rounded-md mr-2 py-2 px-2.5 bg-white dark:bg-slate-900 text-slate-800 dark:text-slate-100 font-semibold text-xs flex justify-center min-w-[2rem] rtl:mr-0 rtl:ml-2"
>
</hotkey>
<hotkey custom-class="min-h-[28px] min-w-[36px] key">
{{ $t('KEYBOARD_SHORTCUTS.KEYS.FORWARD_SLASH_KEY') }}
</p>
</hotkey>
</div>
</div>
</div>
@@ -29,34 +25,26 @@
<h5 class="text-sm text-slate-800 dark:text-slate-100">
{{ $t('KEYBOARD_SHORTCUTS.TITLE.OPEN_CONVERSATION') }}
</h5>
<div class="flex items-center mb-1 ml-2">
<div class="flex mr-2">
<span
class="items-center rounded-md mr-2 py-2 px-2.5 bg-white dark:bg-slate-900 text-slate-800 dark:text-slate-100 font-semibold text-xs"
>
<div class="flex items-center mb-1 ml-2 gap-2">
<div class="flex gap-2">
<hotkey custom-class="min-h-[28px] min-w-[60px] normal-case key">
{{ $t('KEYBOARD_SHORTCUTS.KEYS.ALT_OR_OPTION_KEY') }}
</span>
<span
class="items-center rounded-md mr-2 py-2 px-2.5 bg-white dark:bg-slate-900 text-slate-800 dark:text-slate-100 font-semibold text-xs"
>
</hotkey>
<hotkey custom-class="min-h-[28px] w-9 key">
J
</span>
</hotkey>
<span
class="flex items-center font-semibold text-sm text-slate-800 dark:text-slate-100"
>
{{ $t('KEYBOARD_SHORTCUTS.KEYS.FORWARD_SLASH_KEY') }}
</span>
</div>
<span
class="items-center rounded-md mr-2 py-2 px-2.5 bg-white dark:bg-slate-900 text-slate-800 dark:text-slate-100 font-semibold text-xs"
>
<hotkey custom-class="min-h-[28px] min-w-[60px] normal-case key">
{{ $t('KEYBOARD_SHORTCUTS.KEYS.ALT_OR_OPTION_KEY') }}
</span>
<span
class="items-center rounded-md mr-2 py-2 px-2.5 bg-white dark:bg-slate-900 text-slate-800 dark:text-slate-100 font-semibold text-xs flex justify-center min-w-[2rem] rtl:mr-0 rtl:ml-2"
>
</hotkey>
<hotkey custom-class="w-9 key">
K
</span>
</hotkey>
</div>
</div>
@@ -64,22 +52,16 @@
<h5 class="text-sm text-slate-800 dark:text-slate-100">
{{ $t('KEYBOARD_SHORTCUTS.TITLE.RESOLVE_AND_NEXT') }}
</h5>
<div class="flex items-center mb-1 ml-2">
<span
class="items-center rounded-md mr-2 py-2 px-2.5 bg-white dark:bg-slate-900 text-slate-800 dark:text-slate-100 font-semibold text-xs"
>
<div class="flex items-center mb-1 ml-2 gap-2">
<hotkey custom-class="min-h-[28px] min-w-[60px] normal-case key">
{{ $t('KEYBOARD_SHORTCUTS.KEYS.WINDOWS_KEY_AND_COMMAND_KEY') }}
</span>
<span
class="items-center rounded-md mr-2 py-2 px-2.5 bg-white dark:bg-slate-900 text-slate-800 dark:text-slate-100 font-semibold text-xs"
>
</hotkey>
<hotkey custom-class="min-h-[28px] min-w-[60px] normal-case key">
{{ $t('KEYBOARD_SHORTCUTS.KEYS.ALT_OR_OPTION_KEY') }}
</span>
<span
class="items-center rounded-md mr-2 py-2 px-2.5 bg-white dark:bg-slate-900 text-slate-800 dark:text-slate-100 font-semibold text-xs flex justify-center min-w-[2rem] rtl:mr-0 rtl:ml-2"
>
</hotkey>
<hotkey custom-class="w-9 key">
E
</span>
</hotkey>
</div>
</div>
<div
@@ -87,20 +69,22 @@
:key="shortcutKey.id"
class="flex justify-between items-center min-w-[25rem]"
>
<h5 class="text-sm text-slate-800 dark:text-slate-100">
<h5 class="text-sm text-slate-800 min-w-[36px] dark:text-slate-100">
{{ title(shortcutKey) }}
</h5>
<div class="flex items-center mb-1 ml-2">
<span
class="items-center rounded-md mr-2 py-2 px-2.5 bg-white dark:bg-slate-900 text-slate-800 dark:text-slate-100 font-semibold text-xs"
<div class="flex items-center mb-1 ml-2 gap-2">
<hotkey
:class="{ 'min-w-[60px]': shortcutKey.firstKey !== 'Up' }"
custom-class="min-h-[28px] normal-case key"
>
{{ shortcutKey.firstkey }}
</span>
<span
class="items-center rounded-md mr-2 py-2 px-2.5 bg-white dark:bg-slate-900 text-slate-800 dark:text-slate-100 font-semibold text-xs flex justify-center min-w-[2rem] rtl:mr-0 rtl:ml-2"
{{ shortcutKey.firstKey }}
</hotkey>
<hotkey
:class="{ 'normal-case': shortcutKey.secondKey === 'Down' }"
custom-class="min-h-[28px] min-w-[36px] key"
>
{{ shortcutKey.secondKey }}
</span>
</hotkey>
</div>
</div>
</div>
@@ -111,8 +95,12 @@
<script>
import { mixin as clickaway } from 'vue-clickaway';
import { SHORTCUT_KEYS } from './constants';
import Hotkey from 'dashboard/components/base/Hotkey';
export default {
components: {
Hotkey,
},
mixins: [clickaway],
props: {
show: {
@@ -132,8 +120,8 @@ export default {
},
};
</script>
<style lang="scss" scoped>
.shortcut-key {
@apply py-2 px-2.5;
<style scoped>
.key {
@apply py-2 px-2.5 font-semibold text-xs text-slate-700 dark:text-slate-100 bg-slate-75 dark:bg-slate-900 shadow border-b-2 rtl:border-l-2 ltr:border-r-2 border-slate-200 dark:border-slate-700;
}
</style>
@@ -2,79 +2,79 @@ export const SHORTCUT_KEYS = [
{
id: 1,
label: 'NAVIGATE_DROPDOWN',
firstkey: 'Up',
firstKey: 'Up',
secondKey: 'Down',
},
{
id: 2,
label: 'RESOLVE_CONVERSATION',
firstkey: 'Alt / ⌥',
firstKey: 'Alt / ⌥',
secondKey: 'E',
},
{
id: 3,
label: 'GO_TO_CONVERSATION_DASHBOARD',
firstkey: 'Alt / ⌥',
firstKey: 'Alt / ⌥',
secondKey: 'C',
},
{
id: 4,
label: 'ADD_ATTACHMENT',
firstkey: 'Alt / ⌥',
firstKey: 'Alt / ⌥',
secondKey: 'A',
},
{
id: 5,
label: 'GO_TO_CONTACTS_DASHBOARD',
firstkey: 'Alt / ⌥',
firstKey: 'Alt / ⌥',
secondKey: 'V',
},
{
id: 6,
label: 'TOGGLE_SIDEBAR',
firstkey: 'Alt / ⌥',
firstKey: 'Alt / ⌥',
secondKey: 'O',
},
{
id: 7,
label: 'GO_TO_REPORTS_SIDEBAR',
firstkey: 'Alt / ⌥',
firstKey: 'Alt / ⌥',
secondKey: 'R',
},
{
id: 8,
label: 'MOVE_TO_NEXT_TAB',
firstkey: 'Alt / ⌥',
firstKey: 'Alt / ⌥',
secondKey: 'N',
},
{
id: 9,
label: 'GO_TO_SETTINGS',
firstkey: 'Alt / ⌥',
firstKey: 'Alt / ⌥',
secondKey: 'S',
},
{
id: 10,
label: 'SWITCH_CONVERSATION_STATUS',
firstkey: 'Alt / ⌥',
firstKey: 'Alt / ⌥',
secondKey: 'B',
},
{
id: 11,
label: 'SWITCH_TO_PRIVATE_NOTE',
firstkey: 'Alt / ⌥',
firstKey: 'Alt / ⌥',
secondKey: 'P',
},
{
id: 13,
label: 'SWITCH_TO_REPLY',
firstkey: 'Alt / ⌥',
firstKey: 'Alt / ⌥',
secondKey: 'L',
},
{
id: 14,
label: 'TOGGLE_SNOOZE_DROPDOWN',
firstkey: 'Alt / ⌥',
firstKey: 'Alt / ⌥',
secondKey: 'M',
},
];
@@ -0,0 +1,34 @@
export const MESSAGE_EDITOR_MENU_OPTIONS = [
'strong',
'em',
'link',
'undo',
'redo',
'bulletList',
'orderedList',
'code',
];
export const MESSAGE_SIGNATURE_EDITOR_MENU_OPTIONS = [
'strong',
'em',
'link',
'undo',
'redo',
'imageUpload',
];
export const ARTICLE_EDITOR_MENU_OPTIONS = [
'strong',
'em',
'link',
'undo',
'redo',
'bulletList',
'orderedList',
'h1',
'h2',
'h3',
'imageUpload',
'code',
];
@@ -3,5 +3,5 @@ export const LOCAL_STORAGE_KEYS = {
WIDGET_BUILDER: 'widgetBubble_',
DRAFT_MESSAGES: 'draftMessages',
COLOR_SCHEME: 'color_scheme',
DISMISSED_LABEL_SUGGESTIONS: 'dismissedLabelSuggestions',
DISMISSED_LABEL_SUGGESTIONS: 'labelSuggestionsDismissed',
};
@@ -15,6 +15,9 @@ export const ACCOUNT_EVENTS = Object.freeze({
ADDED_A_CUSTOM_ATTRIBUTE: 'Added a custom attribute',
ADDED_AN_INBOX: 'Added an inbox',
OPEN_MESSAGE_CONTEXT_MENU: 'Opened message context menu',
OPENED_NOTIFICATIONS: 'Opened notifications',
MARK_AS_READ_NOTIFICATIONS: 'Marked notifications as read',
OPEN_CONVERSATION_VIA_NOTIFICATION: 'Opened conversation via notification',
});
export const LABEL_EVENTS = Object.freeze({
@@ -89,4 +92,11 @@ export const OPEN_AI_EVENTS = Object.freeze({
SIMPLIFY: 'OpenAI: Used simplify',
APPLY_LABEL_SUGGESTION: 'OpenAI: Apply label from suggestion',
DISMISS_LABEL_SUGGESTION: 'OpenAI: Dismiss label suggestions',
ADDED_AI_INTEGRATION_VIA_CTA_BUTTON:
'OpenAI: Added AI integration via CTA button',
DISMISS_AI_SUGGESTION: 'OpenAI: Dismiss AI suggestions',
});
export const GENERAL_EVENTS = Object.freeze({
COMMAND_BAR: 'Used commandbar',
});
@@ -30,6 +30,10 @@ const translationKeys = {
'accountuser:create': `AUDIT_LOGS.ACCOUNT_USER.ADD`,
'accountuser:update:self': `AUDIT_LOGS.ACCOUNT_USER.EDIT.SELF`,
'accountuser:update:other': `AUDIT_LOGS.ACCOUNT_USER.EDIT.OTHER`,
'inboxmember:create': `AUDIT_LOGS.INBOX_MEMBER.ADD`,
'inboxmember:destroy': `AUDIT_LOGS.INBOX_MEMBER.REMOVE`,
'teammember:create': `AUDIT_LOGS.TEAM_MEMBER.ADD`,
'teammember:destroy': `AUDIT_LOGS.TEAM_MEMBER.REMOVE`,
'account:update': `AUDIT_LOGS.ACCOUNT.EDIT`,
};
@@ -102,6 +106,58 @@ function handleAccountUserUpdate(auditLogItem, translationPayload, agentList) {
return translationPayload;
}
function setUserInPayload(auditLogItem, translationPayload, agentList) {
const userIdChange = auditLogItem.audited_changes.user_id;
if (userIdChange && userIdChange !== undefined) {
translationPayload.user = getAgentName(userIdChange, agentList);
}
return translationPayload;
}
function setTeamIdInPayload(auditLogItem, translationPayload) {
if (auditLogItem.audited_changes.team_id) {
translationPayload.team_id = auditLogItem.audited_changes.team_id;
}
return translationPayload;
}
function setInboxIdInPayload(auditLogItem, translationPayload) {
if (auditLogItem.audited_changes.inbox_id) {
translationPayload.inbox_id = auditLogItem.audited_changes.inbox_id;
}
return translationPayload;
}
function handleInboxTeamMember(auditLogItem, translationPayload, agentList) {
if (auditLogItem.audited_changes) {
translationPayload = setUserInPayload(
auditLogItem,
translationPayload,
agentList
);
translationPayload = setTeamIdInPayload(auditLogItem, translationPayload);
translationPayload = setInboxIdInPayload(auditLogItem, translationPayload);
}
return translationPayload;
}
function handleAccountUser(
auditLogItem,
translationPayload,
agentList,
action
) {
if (action === 'create') {
return handleAccountUserCreate(auditLogItem, translationPayload, agentList);
}
if (action === 'update') {
return handleAccountUserUpdate(auditLogItem, translationPayload, agentList);
}
return translationPayload;
}
export function generateTranslationPayload(auditLogItem, agentList) {
let translationPayload = {
agentName: getAgentName(auditLogItem.user_id, agentList),
@@ -112,21 +168,20 @@ export function generateTranslationPayload(auditLogItem, agentList) {
const action = auditLogItem.action.toLowerCase();
if (auditableType === 'accountuser') {
if (action === 'create') {
translationPayload = handleAccountUserCreate(
auditLogItem,
translationPayload,
agentList
);
}
translationPayload = handleAccountUser(
auditLogItem,
translationPayload,
agentList,
action
);
}
if (action === 'update') {
translationPayload = handleAccountUserUpdate(
auditLogItem,
translationPayload,
agentList
);
}
if (auditableType === 'inboxmember' || auditableType === 'teammember') {
translationPayload = handleInboxTeamMember(
auditLogItem,
translationPayload,
agentList
);
}
return translationPayload;
@@ -120,9 +120,18 @@ export const generateConditionOptions = (options, key = 'id') => {
});
};
// Add the "None" option to the agent list
export const agentList = agents => [
{
id: 'nil',
name: 'None',
},
...(agents || []),
];
export const getActionOptions = ({ agents, teams, labels, type }) => {
const actionsMap = {
assign_agent: agents,
assign_agent: agentList(agents),
assign_team: teams,
send_email_to_team: teams,
add_label: generateConditionOptions(labels, 'title'),

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