Compare commits

..
Author SHA1 Message Date
iamsivin 8fb043de54 chore: spec fixes 2023-05-19 18:55:09 +05:30
iamsivin 6d07a7d92e fix: Problem when using Brazilian Portuguese 2023-05-19 15:51:41 +05:30
Sojan JoseandGitHub 7b9a9831c1 chore: Fix rubococop failures (#7130)
- fixes rubocop failures on develop
2023-05-19 15:32:25 +05:30
Sojan JoseandGitHub 7ab7bac6bf chore: Enable the new Rubocop rules (#7122)
fixes: https://linear.app/chatwoot/issue/CW-1574/renable-the-disabled-rubocop-rules
2023-05-19 14:37:10 +05:30
Shivam MishraandGitHub b988a01df3 fix: exclude private and activity messages from public API (#7123)
- Exclude private messages
2023-05-19 14:24:49 +05:30
105f9a27d2 feat: more CSAT filters (#7038)
* refactor: use grid instead of flex

* refactor: let the parent layout decide the spacing

* feat: add a separate date-range component

* refactor: use new date-range component

* fix: destructure all options

* refactor: separate group by component

* refactor: better handle group by data

* fix: defaul group by

* refactor: variable naming

* refactor: use DATE_RANGE_OPTIONS directly

* chore: update platform in gemfile.lock

* refactor: trigger fetch on filter change

* refactor: remove redundant method

* refactor: simplify methods and emitting

* refactor: simplify filter logic

* refactor: simplify fetching

* refactor: imports

* refactor: prop name

* refactor: CSAT response to use new APIs

* refactor: use common filter event

* refactor: use computed value for validGroupBy

* refactor: better function names

* refactor: rename prop

* refactor: remove redundant props

* refactor: separate agents filter component

* feat: add labels filter

* feat: add inboxes filter

* fix: event

* refactor: send label and inbox along with request payload

* feat: add inbox filter

* feat: add inbox to download

* refactor: use request payload from computed property

* refactor: params

* feat: add team to csat filters

* feat: add team to csat filters

* feat: add filter for rating

* feat: reverse options

* feat: add labels for ratings and translations

* feat: update translation

* fix: margin and spacing

* fix: trailing whitespace

* feat: add tests for filters

* chore: move files

* feat: add try catch with alerts

* feat: update import

* fix: imports

* Updates broken imports

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com>
2023-05-18 22:50:46 +05:30
132cf802d5 fix: Open AI Integration should respond in conversation language (#7092)
Appended "Reply in the user's language." to obtain the answer in the user's language.
Fixes: https://linear.app/chatwoot/issue/CW-1735/chore-use-account-language-for-the-prompts


Co-authored-by: Sojan <sojan@pepalo.com>
2023-05-18 18:50:48 +05:30
Shivam MishraandGitHub f2f33038f2 fix: Make category non-mandatory (#7117)
Creating articles in a new help center without categories throws an error. This PR fixes that.
2023-05-18 18:42:28 +05:30
Vishnu NarayananandGitHub 590ce788b9 fix: update linux script(cwctl) to account for rails7 upgrade (#7106)
* chore: modify cwctl to fetch latest redis version on new installations

* fix: add libvips for activestorage imageprocessing support

* chore: update cwctl version

* feat: upgrade redis and install libvps for existing installations
2023-05-18 16:53:47 +05:30
Clairton Rodrigo HeinzenandGitHub a25179c342 fix: Instagram events job NoMethodError: undefined method for nil:NilClass (#7105)
- Sometimes tag entry does not have messaging or standby and cause a error
2023-05-18 15:22:01 +05:30
Sivin VargheseandGitHub aea9470b6a fix: RTL issue for basic filter dropdown (#7118) 2023-05-18 15:06:18 +05:30
Muhsin KelothandGitHub 6382707146 chore: Check assignee exists or not before rendering the assignee avatar (#7104) 2023-05-17 15:41:23 +05:30
Pranav Raj SandGitHub 49ef4e54ca feat: Add cmd/ctrl click open on the conversation cards (#7100) 2023-05-16 16:57:25 -07:00
Sojan db0d32f01d Merge branch 'release/2.17.0' into develop 2023-05-16 12:34:00 +05:30
Sojan 7b8bcaba11 Bump version to 2.17.0 2023-05-16 12:33:16 +05:30
Pranav Raj SandGitHub 3f39b7ad1f fix: Avoid styles getting purged on prod build (#7086) 2023-05-15 19:05:07 -07:00
Pranav Raj SandGitHub a3547c5a1f feat: Show Table of Contents in the article sidebar (#7085) 2023-05-15 18:43:16 -07:00
Tejaswini ChileandGitHub 0f776a173c Fix: Mail ate compare with nil date (#7084)
fixes: https://chatwoot-p3.sentry.io/issues/4185144468/?project=6382945
2023-05-15 21:42:22 +05:30
Sojan JoseandGitHub d320605556 chore: Fix Activerecord querry cancelled when fetching email (#7083)
limiting the query to 2 days to ensure that it runs timebound.

fixes: https://linear.app/chatwoot/issue/CW-1747/activerecordquerycanceled-pgquerycanceled-error-canceling-statement
2023-05-15 21:23:17 +05:30
Sojan JoseandGitHub ca85d4e1c6 fix: External url validation in attachment (#7082)
fixes: https://linear.app/chatwoot/issue/CW-1746/activerecordrecordinvalid-validation-failed-external-url-is-too-long
2023-05-15 20:33:58 +05:30
Tejaswini ChileandGitHub b994706265 fix: Email date attribute not being set (#7081)
Fixes: https://linear.app/chatwoot/issue/CW-1738/typeerror-no-implicit-conversion-of-nil-into-string-typeerror
2023-05-15 20:19:03 +05:30
d9f2cd0a6e chore: Enable Hebrew language for widget (#7079)
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2023-05-15 16:56:04 +05:30
Sojan JoseandGitHub f2a34ad8aa Chore: Remove deprecation warning post rails upgrade (#7080)
Fixes: https://linear.app/chatwoot/issue/CW-1575/fix-deprecations-post-upgrade-to-rails-7
2023-05-15 16:32:27 +05:30
Tejaswini ChileandGitHub 65e91f6a6f fix: Duplicate instagram conversations for echo messages (#7078) 2023-05-15 16:25:15 +05:30
Nithin David ThomasandGitHub 7dd1562b40 fix: Welcome email copy changes and canned response API error handling [cw-1290] (#7054)
Fixes: https://linear.app/chatwoot/issue/CW-1290/minor-ui-issues
2023-05-15 16:17:55 +05:30
Shivam MishraandGitHub c70367bbd7 feat: add migration to fix image signed keys (#7076) 2023-05-15 15:13:26 +05:30
Nithin David ThomasandGitHub 5a8eff35b5 chore: Enable support for hebrew in dashboard [CW-1736] (#7077) 2023-05-15 14:03:29 +05:30
Muhsin KelothandGitHub 1936369b12 feat: Add analytics event for OpenAI (#7074)
* Add analytics event for OpenAI

* Add more variations
2023-05-15 11:57:56 +05:30
Tejaswini ChileandGitHub 354010a6e1 chore: fetch mails with multiple attachments (#7030) 2023-05-14 10:02:36 +05:30
Sojan JoseandGitHub 385eab6b96 chore: Add max length validation to text fields (#7073)
Introduces a default max length validation for all string and text fields to prevent processing large payloads.
2023-05-12 22:12:21 +05:30
198cd9b28d feat: Show next available day/hour and minutes on widget (#6902)
* feat: Show next available hour and minutes on widget

* chore: Adds spec

* chore: Show days

* chore: Code clean up

* chore: Review fixes

* chore: Minor fixes

* chore: Review suggestion fixes

* chore: Minor fixes

* Added timezone to widget payload

* chore: Adds time zone

* chore: Review fixes

* chore: Adds comments

* chore: Rounded up min with nearest multiple of 5

* chore: Review fixes

* chore: Review fixes

* chore: Review fixes

* chore: Review fixes

* chore: Fix specs

* chore: Review fixes

* chore: Fix specs

* chore: Review fixes

* chore: Moved day names to i18n

* chore: Review fixes

* chore: Fix specs

---------

Co-authored-by: Tejaswini Chile <tejaswini@chatwoot.com>
Co-authored-by: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com>
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
2023-05-12 19:25:51 +05:30
Nithin David ThomasandGitHub abdf00d2cf fixes: Editor adding slash char while creating block quotes [cw-1505] (#7069)
* fixes: Editor adding slash char while creating block quotes [cw-1505]

* Update with latest package
2023-05-12 16:39:18 +05:30
Muhsin KelothandGitHub 2c3160cfee feat: API to list all attachments for a conversation (#7059)
Fixes: https://linear.app/chatwoot/issue/CW-1678/api-to-list-all-attachments-for-a-conversation
2023-05-12 15:48:06 +05:30
Muhsin KelothandGitHub 708bddf4db feat: Refetch the latest messages on action cable reconnect in widget (#6996) 2023-05-12 14:05:22 +05:30
Pranav Raj SandGitHub 020dcc4dc7 fix: Avoid audio URLs getting cached at the frontend (#7062) 2023-05-11 12:14:56 -07:00
271263bcc2 feat: SLA CRUD APIs (EE) (#7027)
Fixes: https://linear.app/chatwoot/issue/CW-1613/sla-api

Co-authored-by: Sojan Jose <sojan@pepalo.com>
2023-05-11 22:42:56 +05:30
Muhsin KelothandGitHub c97d6021e0 chore: Add migration to set the default empty string value for contact name (#7052)
Fixes: https://linear.app/chatwoot/issue/CW-1650/issue-with-the-contact-name
2023-05-11 17:35:19 +05:30
d99997d17d feat: Add ability to filter Conversations by underlying source_id (#6979)
This change adds the ability to include a `source_id` param when querying the `/api/v1/accounts/{account_id}/conversations/search` endpoint. It restricts to results to only conversations related to a contact_inbox with the provided parameter. My motivation for adding this feature was to allow an external API to communicate with a specific conversation with only an awareness of the conversation `source_id` from the client.


Co-authored-by: Sojan Jose <sojan@pepalo.com>
2023-05-11 17:32:29 +05:30
Sivin VargheseandGitHub 9c5d062efc fix: Fix greeting message label in settings (#7056) 2023-05-11 17:30:07 +05:30
Muhsin KelothandGitHub 87f758ee1f feat: Order conversations by priority (#7053) 2023-05-11 12:56:43 +05:30
Shivam MishraandGitHub d0a1ad746a feat: add index migration (#7050) 2023-05-10 17:55:14 +05:30
86384bf876 feat: Reply suggestion and summary generation using OpenAI (#7029)
Add `reply_suggestions` and `summary_generation` options for OpenAI integration

Fixes: https://linear.app/chatwoot/issue/CW-1595/frontend-for-generating-conversation-summary

Co-authored-by: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com>
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2023-05-10 17:50:37 +05:30
Muhsin KelothandGitHub 55e5fa2205 chore: Add the specs for sort conversation (#7047) 2023-05-10 17:49:17 +05:30
Sojan 65bad90b69 Revert "fix: Welcome email copy changes and canned response API error handling [cw-1290] (#6905)"
This reverts commit 07aaa046c1.
2023-05-10 16:43:18 +05:30
07aaa046c1 fix: Welcome email copy changes and canned response API error handling [cw-1290] (#6905)
* fix: Welcome email copy changes and canned response API error handling

* Review fixes

* Uses mixin for alerts in canned page

* Typo fixes

* Copy changes

* Fixes broken tests

* Fixes review comments

* Fixes typo errors with mail template

* Removes unwanted case

* Fixes repetitive texts

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2023-05-10 15:55:48 +05:30
giquieuandGitHub 520bdabefe fix: Record audio in wav format for web/facebook (#7046) 2023-05-10 12:29:56 +05:30
Shivam MishraandGitHub 662967b5d3 feat(perf): add index to messages created at (#7044)
* feat: add index to messages created at

* feat: run migration
2023-05-10 11:34:49 +05:30
6137a45214 chore: Update link in the reply summary email (#7024)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2023-05-09 17:39:09 -07:00
Pranav Raj SandGitHub 5e0ce7793c fix: Re-order gem list to fix rubocop errors (#7043) 2023-05-08 16:15:27 -07:00
Pranav Raj SandGitHub ce3e38df0f chore: Update the design of the help center portal (#6775) 2023-05-08 15:31:38 -07:00
2b88ecdfc4 feat: Add a feature flag for Audit Log UI (#7035)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2023-05-08 12:44:15 -07:00
Pranav Raj SandGitHub 683f259771 fix: Fix rendering issues in Help Center (#7042) 2023-05-08 12:23:26 -07:00
Vishnu NarayananandGitHub 51fb3b7e8e fix: enable lograge in superadmin path (#7026)
* fix: lograge superadmin path
* chore: add spec for superadmin devise session controller
* chore: address review comment
2023-05-08 13:43:55 +05:30
3dedfee350 chore: Update translations (#7033)
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2023-05-06 15:10:50 +05:30
Sojan JoseandGitHub 022383d942 chore: Upgrade to Rails 7 (#6719)
fixes: #6736
2023-05-06 10:44:52 +05:30
59433d9d3c feat: Adds the ability to sort conversations (#6853)
* Add sort filter

* Change UI

* Change filter

* Complete sort by filters

* Style fixes

* Fix default sort

* Update app/javascript/dashboard/components/widgets/conversation/ConversationBasicFilter.vue

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>

* Update app/javascript/dashboard/components/widgets/conversation/ConversationBasicFilter.vue

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>

* Update app/javascript/dashboard/components/ChatList.vue

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>

* Added translation

* Added review fixes

* Add more updates

* Code cleanups

* Update last_activity_at on message received event

* Cleans up the design for chatlist and icons

* Fix sort

* Remove inline styles

* Add tag along with the title

---------

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
Co-authored-by: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com>
2023-05-05 17:08:32 -07:00
Sojan JoseandGitHub 85e57c2e94 chore: Reorganize Sidekiq Queues (#6976)
- Rearrange and reprioritize current sidekiq queues
- Trim the unnecessary queues

ref: https://linear.app/chatwoot/issue/CW-1480/chore-run-all-sidekiq-jobs-async
2023-05-04 15:44:16 +05:30
Tejaswini ChileandGitHub b081fe08b8 feat: whatsapp duplicate message (#7004) 2023-05-03 15:18:20 +05:30
Muhsin KelothandGitHub 0d014d5d4b chore: Bump utils from 0.0.15 to 0.0.16 (#7022) 2023-05-03 14:36:55 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
937338e3ea chore(deps): bump audited from 5.2.0 to 5.3.3 (#7016)
Bumps [audited](https://github.com/collectiveidea/audited) from 5.2.0 to 5.3.3.
- [Release notes](https://github.com/collectiveidea/audited/releases)
- [Changelog](https://github.com/collectiveidea/audited/blob/main/CHANGELOG.md)
- [Commits](https://github.com/collectiveidea/audited/compare/v5.2.0...v5.3.3)

---
updated-dependencies:
- dependency-name: audited
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-03 14:13:04 +05:30
Pranav Raj S 6e506b070e Merge branch 'hotfix/2.16.1' into develop 2023-05-02 15:54:49 -07:00
Pranav Raj S 257a294166 Bump the version to 2.16.1 2023-05-02 15:52:50 -07:00
Pranav Raj S 12f121f0d8 fix: Provide a default to name if name is not present 2023-05-02 15:52:33 -07:00
Tejaswini ChileandGitHub 847d7ea082 feat: Add support to uncategorized articles (#6912) 2023-05-02 15:35:26 +05:30
c8041392dc feat: Enable features on successful subscription (#6953)
* feat: Enable features on successful subscription

* fix: Add specs

* disable features for default plan

* Remove mark as cloud customers

---------

Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2023-05-01 14:58:39 -07:00
be3c75e858 chore: Update translations (#6999)
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2023-04-28 20:28:03 +05:30
Sojan JoseandGitHub 02c467b6db chore: GPT Reply suggestion & summarize endpoints (#7011)
Adds additional endpoints for OpenAI integration which will allow
- Reply Suggestions
- Summarization

ref: #6436
fixes: https://linear.app/chatwoot/issue/CW-1596/backend-for-generating-conversation-summary
2023-04-28 19:57:25 +05:30
Muhsin KelothandGitHub 13fe439d9f Move the reconnect logic from the update presence (#6992) 2023-04-28 14:02:30 +05:30
Shivam MishraandGitHub 2313edd244 feat: update identity validation docs link (#6994)
* feat: update identity validation docs link

* fix: copy

* fix: grammar
2023-04-28 13:36:58 +05:30
Muhsin KelothandGitHub 09c9300c21 chore: Use canned response variable helpers from utils (#7007) 2023-04-27 17:01:34 +05:30
c0e905b5d8 fix: Search improvements and bug fixes [CW-1604] (#6985)
* fix: Search improvements and bug fixes

* Resets tab to all on search

* Fix index bug with tabs

---------

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2023-04-27 13:20:29 +05:30
Pranav Raj SandGitHub 04da8aa8dc fix: Remove duplicates message in WhatsApp Channel (#7003) 2023-04-26 19:57:06 -07:00
Shivam MishraandGitHub 32f7342cd6 chore: Add issue forms for GitHub (#6982) 2023-04-26 11:25:02 -07:00
74afb785c2 fix: Update spacing for the priority icon in the inbox views (#6993)
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2023-04-26 10:21:30 -07:00
Shivam MishraandGitHub cae1e30160 feat: Update demo data to include better messages (#6995) 2023-04-26 10:20:56 -07:00
Tejaswini ChileandGitHub 99dfe1d5af feat: Enable template variables in channel greeting messages (#6971) 2023-04-26 20:23:46 +05:30
Sojan JoseandGitHub 3fa654f5c6 chore: Add Index for widget contact lookup (#6998)
- Adds an index to improve the contact lookup performance while calling setUser from widget
2023-04-26 19:22:16 +05:30
shaheer-haiderandGitHub 37fde64c67 fix: installation of gem bundles (lograge, rack-mini-profiler, stackprof, nokogiri) in docker (#6897) 2023-04-26 17:04:17 +05:30
Tejaswini ChileandGitHub 3c2d6faf68 feat: Instagram story replies will display the original story link (#6846) 2023-04-26 15:27:07 +05:30
Muhsin KelothandGitHub 5d30dabf97 feat: Command bar action for priority (#6989)
* Add command action for priority assignment

* Fix icon path

* Update conversationHotKeys.js
2023-04-26 13:29:01 +05:30
cef1f97d18 feat: update product copy [CWM-45] (#6372)
* chore: Update non-logged-in-page copy

* Update app/javascript/dashboard/i18n/locale/en/login.json

Co-authored-by: Pranav Raj S <pranav@chatwoot.com>

* Update app/javascript/dashboard/i18n/locale/en/signup.json

* Update app/javascript/dashboard/i18n/locale/en/signup.json

---------

Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2023-04-25 22:02:15 -07:00
Pranav Raj SandGitHub 92705723f1 fix: Add /api/v1 to the team_members API documentation (#6987) 2023-04-25 20:17:15 -07:00
Shivam MishraandGitHub af971c9b18 fix: whatsapp template params validation (#6986) 2023-04-26 08:46:30 +05:30
2f2cdd7e7c feat: Hides dismissed campaigns while browsing [cw-33] (#6842)
* Chore: moves localstorage helper as a shared utility and refactors constants

* Refactors constants file

* feat: Hides dismissed campaigns while browsing

* Snoozes all campaigns for an hour after dismissing

* Fixes error with date parsing

* Snooze ongoing campaigns

* Review fixes

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2023-04-25 22:38:36 +05:30
Shivam MishraandGitHub d95283f5c2 feat: improvements to priority (#6981)
* fix: colors for urgent icon

* feat: trigger updated event on priority change

* fix: specs
2023-04-25 22:28:19 +05:30
402428fb4d feat: Splits search api by resources to improve query time [cw-47] (#6942)
* feat: Splits search api by resources to improve query time

* Review fixes

* Spacing fixes

* Update app/javascript/dashboard/modules/search/components/SearchView.vue

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>

* Review fixes

* Refactor searchview

---------

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2023-04-25 17:59:38 +05:30
Shivam MishraandGitHub 5600b518ac fix: validate template_params for WhatsApp (#6881)
- Add JsonSchemaValidator, which takes a declarative schema and validates it for a given property.
- Add specs for JsonSchemaValidator
- Enable the validator for template_params
2023-04-25 16:50:36 +05:30
Sojan 7bd400772d Merge branch 'release/2.16.0'
Publish Chatwoot CE docker images / build (push) Waiting to run
2023-04-18 01:04:19 +05:30
Sojan 3e6d23e071 Merge branch 'release/2.15.0'
Publish Chatwoot CE docker images / build (push) Waiting to run
2023-03-15 19:58:19 +05:30
Sojan 0e3eb51d7a Merge branch 'release/2.14.0'
Publish Chatwoot CE docker images / build (push) Waiting to run
2023-02-16 13:42:21 +05:30
Vishnu Narayanan 4ffe73a1ce Merge branch 'hotfix/2.13.1'
Publish Chatwoot CE docker images / build (push) Waiting to run
2023-02-03 18:47:31 +05:30
1164 changed files with 15648 additions and 5450 deletions
+1 -1
View File
@@ -7,7 +7,7 @@ defaults: &defaults
working_directory: ~/build
docker:
# specify the version you desire here
- image: cimg/ruby:3.1.3-browsers
- image: cimg/ruby:3.2.2-browsers
# Specify service dependencies here if necessary
# CircleCI maintains a library of pre-built images
+1 -1
View File
@@ -30,7 +30,7 @@ RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
npm
# Install rbenv and ruby
ARG RUBY_VERSION="3.1.3"
ARG RUBY_VERSION="3.2.2"
RUN git clone https://github.com/rbenv/rbenv.git ~/.rbenv \
&& echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc \
&& echo 'eval "$(rbenv init -)"' >> ~/.bashrc
-78
View File
@@ -1,78 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: 'Bug'
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See the error
**Expected behavior**
Share a clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Browser logs**
Share the browser logs to debug the issue further.
**Server logs**
Share the server logs to debug the issue further.
**Environment**
Describe whether you are using Chatwoot Cloud (app.chatwoot.com) or a self-hosted installation of Chatwoot. If you are using a self-hosted installation of Chatwoot, describe the type of deployment (Docker/Linux VM installation/Heroku/Kubernetes/Other).
- [ ] app.chatwoot.com (Chatwoot Cloud)
- [ ] Self-hosted
- - [ ] Linux VM
- - [ ] Docker
- - [ ] Kubernetes
- - [ ] Heroku
- - [ ] Other (Please specify)
**Desktop (please complete the following information)** (If applicable)
- OS: [e.g. Linux, Windows, MacOS]
- Browser [e.g. chrome, firefox, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information)** (If applicable)
- Device: [e.g. iPhone6, Pixel7]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, firefox, safari]
- Version [e.g. 22]
**Docker** (If applicable)
Please share the output of the following.
- `docker version`
- `docker info`
- `docker-compose version`
**Cloud Provider** (If applicable)
- [ ] AWS
- [ ] GCP
- [ ] Azure
- [ ] DigitalOcean
- [ ] Others
**Additional context**
Add any other context about the problem here.
+78
View File
@@ -0,0 +1,78 @@
name: 🐞 Bug report
description: Create a report to help us improve
labels: 'Bug'
body:
- type: textarea
attributes:
label: Describe the bug
description: A concise description of what you expected to happen along with screenshots if applicable.
validations:
required: true
- type: textarea
attributes:
label: To Reproduce
description: Steps to reproduce the behavior.
placeholder: |
1. In this environment...
2. With this config...
3. Run '...'
4. See error...
validations:
required: true
- type: textarea
attributes:
label: Expected behavior
description: A concise description of what you expected to happen.
- type: dropdown
id: environment
attributes:
label: Environment
description: Describe whether you are using Chatwoot Cloud (app.chatwoot.com) or a self-hosted installation of Chatwoot. If you are using a self-hosted installation of Chatwoot, describe the type of deployment (Docker/Linux VM installation/Heroku/Kubernetes/Other).
options:
- app.chatwoot.com
- Linux VM
- Docker
- Kubernetes
- Heroku
- Other [please specify in the description]
validations:
required: true
- type: dropdown
id: provider
attributes:
label: Cloud Provider
description:
options:
- AWS
- GCP
- Azure
- DigitalOcean
- Other [please specify in the description]
- type: dropdown
id: platform
attributes:
label: Platform
description: Describe the platform you are using
options:
- Browser
- Mobile
- type: input
attributes:
label: Operating system
description: The operating system and the version you are using.
- type: input
attributes:
label: Browser and version
description: The name of the browser and version you are using.
- type: textarea
attributes:
label: Docker (if applicable)
description: |
Please share the output of the following.
- `docker version`
- `docker info`
- `docker-compose version`
- type: textarea
attributes:
label: Additional context
description: Add any other context about the problem here.
+8
View File
@@ -0,0 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: Report a security issue
url: https://www.chatwoot.com/docs/contributing-guide/security-reports/
about: Guidelines and steps to report a security vulnerability. Please report security vulnerabilities here.
- name: Product Documentation
url: https://www.chatwoot.com/help-center
about: If you have questions, are confused, or just want to understand our product better, please check out our documentation.
@@ -1,20 +0,0 @@
---
name: Enhancement request
about: Suggest any enhancements for this project
title: ''
labels: ''
assignees: ''
---
**Is your enhancement request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions you've considered.
**Additional context**
Add any other context or screenshots about the enhancement request here.
-20
View File
@@ -1,20 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
@@ -0,0 +1,28 @@
name: 🧙 Feature request
description: Suggest an idea for this project
labels: 'feature-request'
body:
- type: textarea
attributes:
label: Is your feature or enhancement related to a problem? Please describe.
description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
validations:
required: true
- type: textarea
attributes:
label: Describe the solution you'd like
description: A clear and concise description of what you want to happen.
validations:
required: true
- type: textarea
attributes:
label: Describe alternatives you've considered
description: A clear and concise description of any alternative solutions or features you've considered.
validations:
required: false
- type: textarea
attributes:
label: Additional context
description: Add any other context or screenshots about the feature request here.
validations:
required: false
+7
View File
@@ -67,3 +67,10 @@ test/cypress/videos/*
# yalc for local testing
.yalc
yalc.lock
/public/packs
/public/packs-test
/node_modules
/yarn-error.log
yarn-debug.log*
.yarn-integrity
+25
View File
@@ -162,8 +162,33 @@ Rails/RenderInline:
Performance/CollectionLiteralInLoop:
Exclude:
- 'db/migrate/20210315101919_enable_email_channel.rb'
Rails/ThreeStateBooleanColumn:
Exclude:
- 'db/migrate/20200509044639_add_hide_input_flag_to_bot_config.rb'
- 'db/migrate/20200605130625_agent_away_message_to_auto_reply.rb'
- 'db/migrate/20200606132552_create_labels.rb'
- 'db/migrate/20201027135006_create_working_hours.rb'
- 'db/migrate/20210112174124_add_hmac_token_to_inbox.rb'
- 'db/migrate/20210114202310_create_teams.rb'
- 'db/migrate/20210212154240_add_request_for_email_on_channel_web_widget.rb'
- 'db/migrate/20210428135041_add_campaigns.rb'
- 'db/migrate/20210602182058_add_hmac_to_api_channel.rb'
- 'db/migrate/20210609133433_add_email_collect_to_inboxes.rb'
- 'db/migrate/20210618095823_add_csat_toggle_for_inbox.rb'
- 'db/migrate/20210927062350_add_trigger_only_during_business_hours_collect_to_campaigns.rb'
- 'db/migrate/20211027073553_add_imap_smtp_config_to_channel_email.rb'
- 'db/migrate/20211109143122_add_tweet_enabled_flag_to_twitter_channel.rb'
- 'db/migrate/20211216110209_add_allow_messages_after_resolved_to_inbox.rb'
- 'db/migrate/20220116103902_add_open_ssl_verify_mode_to_channel_email.rb'
- 'db/migrate/20220216151613_add_open_all_day_to_working_hour.rb'
- 'db/migrate/20220511072655_add_archive_column_to_portal.rb'
- 'db/migrate/20230503101201_create_sla_policies.rb'
RSpec/IndexedLet:
Enabled: false
RSpec/NamedSubject:
Enabled: false
# we should bring this down
RSpec/MultipleMemoizedHelpers:
Max: 14
+1 -1
View File
@@ -1 +1 @@
3.1.3
3.2.2
+1
View File
@@ -0,0 +1 @@
/spec
+26 -23
View File
@@ -1,10 +1,10 @@
source 'https://rubygems.org'
ruby '3.1.3'
ruby '3.2.2'
##-- base gems for rails --##
gem 'rack-cors', require: 'rack/cors'
gem 'rails', '~> 6.1', '>= 6.1.7.3'
gem 'rails', '~> 7'
# Reduces boot times through caching; required in config/boot.rb
gem 'bootsnap', require: false
@@ -36,19 +36,19 @@ gem 'json_schemer'
# Rack middleware for blocking & throttling abusive requests
gem 'rack-attack'
# a utility tool for streaming, flexible and safe downloading of remote files
gem 'down', '~> 5.0'
gem 'down'
# authentication type to fetch and send mail over oauth2.0
gem 'gmail_xoauth'
# Prevent CSV injection
gem 'csv-safe'
# Support message translation
gem 'google-cloud-translate'
##-- for active storage --##
gem 'aws-sdk-s3', require: false
gem 'azure-storage-blob', require: false
# original gem isn't maintained actively
# we wanted updated version of faraday which is a dependency for slack-ruby-client
gem 'azure-storage-blob', git: 'https://github.com/chatwoot/azure-storage-ruby', branch: 'chatwoot', require: false
gem 'google-cloud-storage', require: false
gem 'image_processing', '~> 1.12.2'
gem 'image_processing'
##-- gems for database --#
gem 'groupdate'
@@ -62,13 +62,13 @@ gem 'activerecord-import'
gem 'dotenv-rails'
gem 'foreman'
gem 'puma'
gem 'webpacker', '~> 5.4', '>= 5.4.3'
gem 'webpacker'
# metrics on heroku
gem 'barnes'
##--- gems for authentication & authorization ---##
gem 'devise'
gem 'devise-secure_password', '~> 2.0', git: 'https://github.com/chatwoot/devise-secure_password'
gem 'devise-secure_password', git: 'https://github.com/chatwoot/devise-secure_password', branch: 'chatwoot'
gem 'devise_token_auth'
# authorization
gem 'jwt'
@@ -81,7 +81,6 @@ gem 'administrate'
gem 'wisper', '2.0.0'
##--- gems for channels ---##
# TODO: bump up gem to 2.0
gem 'facebook-messenger'
gem 'line-bot-api'
gem 'twilio-ruby', '~> 5.66'
@@ -91,9 +90,14 @@ gem 'twitty', '~> 0.1.5'
# facebook client
gem 'koala'
# slack client
gem 'slack-ruby-client'
gem 'slack-ruby-client', '~> 2.0.0'
# for dialogflow integrations
gem 'google-cloud-dialogflow'
gem 'google-cloud-dialogflow-v2'
gem 'grpc'
# Translate integrations
# 'google-cloud-translate' gem depends on faraday 2.0 version
# this dependency breaks the slack-ruby-client gem
gem 'google-cloud-translate-v3'
##-- apm and error monitoring ---#
# loaded only when environment variables are set.
@@ -108,9 +112,9 @@ gem 'sentry-ruby', require: false
gem 'sentry-sidekiq', require: false
##-- background job processing --##
gem 'sidekiq', '~> 6.4.2'
gem 'sidekiq'
# We want cron jobs
gem 'sidekiq-cron', '~> 1.6', '>= 1.6.0'
gem 'sidekiq-cron'
##-- Push notification service --##
gem 'fcm'
@@ -129,7 +133,10 @@ gem 'procore-sift'
# parse email
gem 'email_reply_trimmer'
gem 'html2text'
# TODO: we might have to fork this gem since 0.3.1 has hard depency on nokogir 1.10.
# and this gem hasn't been updated for a while.
gem 'html2text', git: 'https://github.com/chatwoot/html2text_ruby', branch: 'chatwoot'
# to calculate working hours
gem 'working_hours'
@@ -144,18 +151,13 @@ gem 'stripe'
## to populate db with sample data
gem 'faker'
# Can remove this in rails 7
gem 'net-imap', require: false
gem 'net-pop', require: false
gem 'net-smtp', require: false
# Include logrange conditionally in intializer using env variable
gem 'lograge', '~> 0.12.0', require: false
# worked with microsoft refresh token
gem 'omniauth-oauth2'
gem 'audited', '~> 5.2'
gem 'audited', '~> 5.3'
# need for google auth
gem 'omniauth'
@@ -174,6 +176,7 @@ group :development do
gem 'annotate'
gem 'bullet'
gem 'letter_opener'
gem 'scss_lint', require: false
gem 'web-console'
# used in swagger build
@@ -189,7 +192,7 @@ end
group :test do
# Cypress in rails.
gem 'cypress-on-rails', '~> 1.13', '>= 1.13.1'
gem 'cypress-on-rails'
# fast cleaning of database
gem 'database_cleaner'
# mock http calls
@@ -211,7 +214,7 @@ group :development, :test do
gem 'mock_redis'
gem 'pry-rails'
gem 'rspec_junit_formatter'
gem 'rspec-rails', '~> 5.0.3'
gem 'rspec-rails'
gem 'rubocop', require: false
gem 'rubocop-performance', require: false
gem 'rubocop-rails', require: false
+341 -323
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1 +1 @@
2.2.0
2.17.0
+1 -1
View File
@@ -1 +1 @@
2.1.0
2.3.0
@@ -63,28 +63,42 @@ class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder
end
def conversation
@conversation ||= Conversation.find_by(conversation_params) || build_conversation
@conversation ||= Conversation.where(
"additional_attributes ->> 'type' = 'instagram_direct_message'"
).find_by(conversation_params) || build_conversation
end
def message_content
@messaging[:message][:text]
end
def story_reply_attributes
message[:reply_to][:story] if message[:reply_to].present? && message[:reply_to][:story].present?
end
def build_message
return if @outgoing_echo && already_sent_from_chatwoot?
return if message_content.blank? && all_unsupported_files?
@message = conversation.messages.create!(message_params)
save_story_id
attachments.each do |attachment|
process_attachment(attachment)
end
end
def save_story_id
return if story_reply_attributes.blank?
@message.save_story_info(story_reply_attributes)
end
def build_conversation
@contact_inbox ||= contact.contact_inboxes.find_by!(source_id: message_source_id)
Conversation.create!(conversation_params.merge(
contact_inbox_id: @contact_inbox.id
contact_inbox_id: @contact_inbox.id,
additional_attributes: { type: 'instagram_direct_message' }
))
end
@@ -92,10 +106,7 @@ class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder
{
account_id: @inbox.account_id,
inbox_id: @inbox.id,
contact_id: contact.id,
additional_attributes: {
type: 'instagram_direct_message'
}
contact_id: contact.id
}
end
@@ -16,6 +16,9 @@ class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
end
end
def show; end
def edit; end
def create
@article = @portal.articles.create!(article_params)
@article.associate_root_article(article_params[:associated_article_id])
@@ -23,10 +26,6 @@ class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
render json: { error: @article.errors.messages }, status: :unprocessable_entity and return unless @article.valid?
end
def edit; end
def show; end
def update
@article.update!(article_params) if params[:article].present?
render json: { error: @article.errors.messages }, status: :unprocessable_entity and return unless @article.valid?
@@ -6,6 +6,8 @@ class Api::V1::Accounts::AutomationRulesController < Api::V1::Accounts::BaseCont
@automation_rules = Current.account.automation_rules
end
def show; end
def create
@automation_rule = Current.account.automation_rules.new(automation_rules_permit)
@automation_rule.actions = params[:actions]
@@ -28,8 +30,6 @@ class Api::V1::Accounts::AutomationRulesController < Api::V1::Accounts::BaseCont
render json: { blob_key: file_blob.key, blob_id: file_blob.id }
end
def show; end
def update
ActiveRecord::Base.transaction do
automation_rule_update
@@ -6,21 +6,21 @@ class Api::V1::Accounts::CampaignsController < Api::V1::Accounts::BaseController
@campaigns = Current.account.campaigns
end
def show; end
def create
@campaign = Current.account.campaigns.create!(campaign_params)
end
def update
@campaign.update!(campaign_params)
end
def destroy
@campaign.destroy!
head :ok
end
def show; end
def update
@campaign.update!(campaign_params)
end
private
def campaign
@@ -9,6 +9,8 @@ class Api::V1::Accounts::CategoriesController < Api::V1::Accounts::BaseControlle
@categories = @portal.categories.search(params)
end
def show; end
def create
@category = @portal.categories.create!(category_params)
@category.related_categories << related_categories_records
@@ -17,8 +19,6 @@ class Api::V1::Accounts::CategoriesController < Api::V1::Accounts::BaseControlle
@category.save!
end
def show; end
def update
@category.update!(category_params)
@category.related_categories = related_categories_records if related_categories_records.any?
@@ -5,21 +5,21 @@ class Api::V1::Accounts::Contacts::NotesController < Api::V1::Accounts::Contacts
@notes = @contact.notes.latest.includes(:user)
end
def show; end
def create
@note = @contact.notes.create!(note_params)
end
def update
@note.update(note_params)
end
def destroy
@note.destroy!
head :ok
end
def show; end
def update
@note.update(note_params)
end
private
def note
@@ -22,6 +22,12 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
@conversations_count = result[:count]
end
def attachments
@attachments = @conversation.attachments
end
def show; end
def create
ActiveRecord::Base.transaction do
@conversation = ConversationBuilder.new(params: params, contact_inbox: @contact_inbox).perform
@@ -29,8 +35,6 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
end
end
def show; end
def filter
result = ::Conversations::FilterService.new(params.permit!, current_user).perform
@conversations = result[:conversations]
@@ -34,9 +34,12 @@ class Api::V1::Accounts::CsatSurveyResponsesController < Api::V1::Accounts::Base
end
def set_csat_survey_responses
@csat_survey_responses = filtrate(
Current.account.csat_survey_responses.includes([:conversation, :assigned_agent, :contact])
).filter_by_created_at(range).filter_by_assigned_agent_id(params[:user_ids])
base_query = Current.account.csat_survey_responses.includes([:conversation, :assigned_agent, :contact])
@csat_survey_responses = filtrate(base_query).filter_by_created_at(range)
.filter_by_assigned_agent_id(params[:user_ids])
.filter_by_inbox_id(params[:inbox_id])
.filter_by_team_id(params[:team_id])
.filter_by_rating(params[:rating])
end
def set_current_page_surveys
@@ -2,6 +2,11 @@ class Api::V1::Accounts::InboxMembersController < Api::V1::Accounts::BaseControl
before_action :fetch_inbox
before_action :current_agents_ids, only: [:create, :update]
def show
authorize @inbox, :show?
fetch_updated_agents
end
def create
authorize @inbox, :create?
ActiveRecord::Base.transaction do
@@ -10,11 +15,6 @@ class Api::V1::Accounts::InboxMembersController < Api::V1::Accounts::BaseControl
fetch_updated_agents
end
def show
authorize @inbox, :show?
fetch_updated_agents
end
def update
authorize @inbox, :update?
update_agents_list
@@ -6,6 +6,10 @@ class Api::V1::Accounts::MacrosController < Api::V1::Accounts::BaseController
@macros = Macro.with_visibility(current_user, params)
end
def show
head :not_found if @macro.nil?
end
def create
@macro = Current.account.macros.new(macros_with_user.merge(created_by_id: current_user.id))
@macro.set_visibility(current_user, permitted_params)
@@ -18,8 +22,16 @@ class Api::V1::Accounts::MacrosController < Api::V1::Accounts::BaseController
@macro
end
def show
head :not_found if @macro.nil?
def update
ActiveRecord::Base.transaction do
@macro.update!(macros_with_user)
@macro.set_visibility(current_user, permitted_params)
process_attachments
@macro.save!
rescue StandardError => e
Rails.logger.error e
render json: { error: @macro.errors.messages }.to_json, status: :unprocessable_entity
end
end
def destroy
@@ -37,18 +49,6 @@ class Api::V1::Accounts::MacrosController < Api::V1::Accounts::BaseController
render json: { blob_key: file_blob.key, blob_id: file_blob.id }
end
def update
ActiveRecord::Base.transaction do
@macro.update!(macros_with_user)
@macro.set_visibility(current_user, permitted_params)
process_attachments
@macro.save!
rescue StandardError => e
Rails.logger.error e
render json: { error: @macro.errors.messages }.to_json, status: :unprocessable_entity
end
end
def execute
::MacrosExecutionJob.perform_later(@macro, conversation_ids: params[:conversation_ids], user: Current.user)
@@ -30,7 +30,7 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
ActiveRecord::Base.transaction do
@portal.update!(portal_params) if params[:portal].present?
# @portal.custom_domain = parsed_custom_domain
process_attached_logo
process_attached_logo if params[:blob_id].present?
rescue StandardError => e
Rails.logger.error e
render json: { error: @portal.errors.messages }.to_json, status: :unprocessable_entity
@@ -14,6 +14,11 @@ class Api::V1::AccountsController < Api::BaseController
CustomExceptions::Account::UserErrors,
with: :render_error_response
def show
@latest_chatwoot_version = ::Redis::Alfred.get(::Redis::Alfred::LATEST_CHATWOOT_VERSION)
render 'api/v1/accounts/show', format: :json
end
def create
@user, @account = AccountBuilder.new(
account_name: account_params[:account_name],
@@ -35,11 +40,6 @@ class Api::V1::AccountsController < Api::BaseController
render json: { cache_keys: get_cache_keys }, status: :ok
end
def show
@latest_chatwoot_version = ::Redis::Alfred.get(::Redis::Alfred::LATEST_CHATWOOT_VERSION)
render 'api/v1/accounts/show', format: :json
end
def update
@account.update!(account_params.slice(:name, :locale, :domain, :support_email, :auto_resolve_duration))
end
@@ -48,7 +48,8 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
def message_finder_params
{
filter_internal_messages: true,
before: permitted_params[:before]
before: permitted_params[:before],
after: permitted_params[:after]
}
end
@@ -62,7 +63,7 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
def permitted_params
# timestamp parameter is used in create conversation method
params.permit(:id, :before, :website_token, contact: [:name, :email], message: [:content, :referer_url, :timestamp, :echo_id])
params.permit(:id, :before, :after, :website_token, contact: [:name, :email], message: [:content, :referer_url, :timestamp, :echo_id])
end
def set_message
+1 -1
View File
@@ -3,7 +3,7 @@ class ApiController < ApplicationController
def index
render json: { version: Chatwoot.config[:version],
timestamp: Time.now.utc.to_formatted_s(:db),
timestamp: Time.now.utc.to_fs(:db),
queue_services: redis_status,
data_services: postgres_status }
end
@@ -4,6 +4,16 @@ class DeviseOverrides::PasswordsController < Devise::PasswordsController
skip_before_action :require_no_authentication, raise: false
skip_before_action :authenticate_user!, raise: false
def create
@user = User.find_by(email: params[:email])
if @user
@user.send_reset_password_instructions
build_response(I18n.t('messages.reset_password_success'), 200)
else
build_response(I18n.t('messages.reset_password_failure'), 404)
end
end
def update
# params: reset_password_token, password, password_confirmation
original_token = params[:reset_password_token]
@@ -17,16 +27,6 @@ class DeviseOverrides::PasswordsController < Devise::PasswordsController
end
end
def create
@user = User.find_by(email: params[:email])
if @user
@user.send_reset_password_instructions
build_response(I18n.t('messages.reset_password_success'), 200)
else
build_response(I18n.t('messages.reset_password_failure'), 404)
end
end
private
def reset_password_and_confirmation(recoverable)
@@ -1,4 +1,4 @@
class DeviseOverrides::SessionsController < ::DeviseTokenAuth::SessionsController
class DeviseOverrides::SessionsController < DeviseTokenAuth::SessionsController
# Prevent session parameter from being passed
# Unpermitted parameter: session
wrap_parameters format: []
@@ -1,4 +1,4 @@
class DeviseOverrides::TokenValidationsController < ::DeviseTokenAuth::TokenValidationsController
class DeviseOverrides::TokenValidationsController < DeviseTokenAuth::TokenValidationsController
def validate_token
# @resource will have been set by set_user_by_token concern
if @resource
@@ -1,12 +1,12 @@
class Platform::Api::V1::AccountsController < PlatformController
def show; end
def create
@resource = Account.create!(account_params)
update_resource_features
@platform_app.platform_app_permissibles.find_or_create_by(permissible: @resource)
end
def show; end
def update
@resource.assign_attributes(account_params)
update_resource_features
@@ -6,14 +6,14 @@ class Platform::Api::V1::AgentBotsController < PlatformController
@resources = @platform_app.platform_app_permissibles.where(permissible_type: 'AgentBot').all
end
def show; end
def create
@resource = AgentBot.new(agent_bot_params)
@resource.save!
@platform_app.platform_app_permissibles.find_or_create_by(permissible: @resource)
end
def show; end
def update
@resource.update!(agent_bot_params)
end
@@ -5,6 +5,8 @@ class Platform::Api::V1::UsersController < PlatformController
before_action(only: [:login]) { set_resource }
before_action(only: [:login]) { validate_platform_app_permissible }
def show; end
def create
@resource = (User.find_by(email: user_params[:email]) || User.new(user_params))
@resource.skip_confirmation!
@@ -16,8 +18,6 @@ class Platform::Api::V1::UsersController < PlatformController
render json: { url: @resource.generate_sso_link }
end
def show; end
def update
@resource.assign_attributes(user_update_params)
@@ -2,6 +2,8 @@ class Public::Api::V1::Inboxes::ContactsController < Public::Api::V1::InboxesCon
before_action :contact_inbox, except: [:create]
before_action :process_hmac
def show; end
def create
source_id = params[:source_id] || SecureRandom.uuid
@contact_inbox = ::ContactInboxWithContactBuilder.new(
@@ -11,8 +13,6 @@ class Public::Api::V1::Inboxes::ContactsController < Public::Api::V1::InboxesCon
).perform
end
def show; end
def update
contact_identify_action = ContactIdentifyAction.new(
contact: @contact_inbox.contact,
@@ -1,7 +1,7 @@
class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::BaseController
before_action :ensure_custom_domain_request, only: [:show, :index]
before_action :portal
before_action :set_category, except: [:index]
before_action :set_category, except: [:index, :show]
before_action :set_article, only: [:show]
layout 'portal'
@@ -16,7 +16,7 @@ class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::B
private
def set_article
@article = @category.articles.find(permitted_params[:id])
@article = @portal.articles.find_by(slug: permitted_params[:article_slug])
@article.increment_view_count
@parsed_content = render_article_content(@article.content)
end
@@ -39,7 +39,7 @@ class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::B
end
def permitted_params
params.permit(:slug, :category_slug, :locale, :id)
params.permit(:slug, :category_slug, :locale, :id, :article_slug)
end
def render_article_content(content)
@@ -5,6 +5,7 @@ class Public::Api::V1::Portals::BaseController < PublicController
def set_locale(&)
switch_locale_with_portal(&) if params[:locale].present?
switch_locale_with_article(&) if params[:article_slug].present?
end
def switch_locale_with_portal(&)
@@ -19,4 +20,16 @@ class Public::Api::V1::Portals::BaseController < PublicController
I18n.with_locale(@locale, &)
end
def switch_locale_with_article(&)
article = Article.find_by(slug: params[:article_slug])
@locale = if article.category.present?
article.category.locale
else
'en'
end
I18n.with_locale(@locale, &)
end
end
@@ -45,13 +45,17 @@ class SuperAdmin::AccountsController < SuperAdmin::ApplicationController
def seed
Internal::SeedAccountJob.perform_later(requested_resource)
# rubocop:disable Rails/I18nLocaleTexts
redirect_back(fallback_location: [namespace, requested_resource], notice: 'Account seeding triggered')
# rubocop:enable Rails/I18nLocaleTexts
end
def destroy
account = Account.find(params[:id])
DeleteObjectJob.perform_later(account) if account.present?
# rubocop:disable Rails/I18nLocaleTexts
redirect_back(fallback_location: [namespace, requested_resource], notice: 'Account deletion is in progress.')
# rubocop:enable Rails/I18nLocaleTexts
end
end
+1 -1
View File
@@ -1,7 +1,7 @@
class SwaggerController < ApplicationController
def respond
if Rails.env.development? || Rails.env.test?
render inline: File.read(Rails.root.join('swagger', derived_path))
render inline: Rails.root.join('swagger', derived_path).read
else
head :not_found
end
+1 -1
View File
@@ -41,7 +41,7 @@ class WidgetsController < ActionController::Base
source_id: @auth_token_params[:source_id]
)
@contact = @contact_inbox ? @contact_inbox.contact : nil
@contact = @contact_inbox&.contact
end
def build_contact
+3
View File
@@ -1,2 +1,5 @@
class AccountDrop < BaseDrop
def name
@obj.try(:name)
end
end
+19 -4
View File
@@ -5,7 +5,8 @@ class ConversationFinder
SORT_OPTIONS = {
latest: 'latest',
sort_on_created_at: 'sort_on_created_at',
last_user_message_at: 'last_user_message_at'
last_user_message_at: 'last_user_message_at',
sort_on_priority: 'sort_on_priority'
}.with_indifferent_access
# assumptions
@@ -53,9 +54,10 @@ class ConversationFinder
find_all_conversations
filter_by_status unless params[:q]
filter_by_team if @team
filter_by_labels if params[:labels]
filter_by_query if params[:q]
filter_by_team
filter_by_labels
filter_by_query
filter_by_source_id
end
def set_inboxes
@@ -106,6 +108,8 @@ class ConversationFinder
end
def filter_by_query
return unless params[:q]
allowed_message_types = [Message.message_types[:incoming], Message.message_types[:outgoing]]
@conversations = conversations.joins(:messages).where('messages.content ILIKE :search', search: "%#{params[:q]}%")
.where(messages: { message_type: allowed_message_types }).includes(:messages)
@@ -120,13 +124,24 @@ class ConversationFinder
end
def filter_by_team
return unless @team
@conversations = @conversations.where(team: @team)
end
def filter_by_labels
return unless params[:labels]
@conversations = @conversations.tagged_with(params[:labels], any: true)
end
def filter_by_source_id
return unless params[:source_id]
@conversations = @conversations.joins(:contact_inbox)
@conversations = @conversations.where(contact_inboxes: { source_id: params[:source_id] })
end
def set_count_for_all_conversations
[
@conversations.assigned_to(current_user).count,
+22
View File
@@ -10,4 +10,26 @@ module EmailHelper
def normalize_email_with_plus_addressing(email)
"#{email.split('@').first.split('+').first}@#{email.split('@').last}".downcase
end
def parse_email_variables(conversation, email)
case email
when modified_liquid_content(email)
template = Liquid::Template.parse(modified_liquid_content(email))
template.render(message_drops(conversation))
when URI::MailTo::EMAIL_REGEXP
email
end
end
def modified_liquid_content(email)
# This regex is used to match the code blocks in the content
# We don't want to process liquid in code blocks
email.gsub(/`(.*?)`/m, '{% raw %}`\\1`{% endraw %}')
end
def message_drops(conversation)
{
'contact' => ContactDrop.new(conversation.contact)
}
end
end
+11 -4
View File
@@ -6,7 +6,7 @@ class CSATReportsAPI extends ApiClient {
super('csat_survey_responses', { accountScoped: true });
}
get({ page, from, to, user_ids } = {}) {
get({ page, from, to, user_ids, inbox_id, team_id, rating } = {}) {
return axios.get(this.url, {
params: {
page,
@@ -14,24 +14,31 @@ class CSATReportsAPI extends ApiClient {
until: to,
sort: '-created_at',
user_ids,
inbox_id,
team_id,
rating,
},
});
}
download({ from, to, user_ids } = {}) {
download({ from, to, user_ids, inbox_id, team_id, rating } = {}) {
return axios.get(`${this.url}/download`, {
params: {
since: from,
until: to,
sort: '-created_at',
user_ids,
inbox_id,
team_id,
rating,
},
});
}
getMetrics({ from, to, user_ids } = {}) {
getMetrics({ from, to, user_ids, inbox_id, team_id } = {}) {
// no ratings for metrics
return axios.get(`${this.url}/metrics`, {
params: { since: from, until: to, user_ids },
params: { since: from, until: to, user_ids, inbox_id, team_id },
});
}
}
@@ -14,6 +14,7 @@ class ConversationApi extends ApiClient {
labels,
teamId,
conversationType,
sortBy,
}) {
return axios.get(this.url, {
params: {
@@ -24,6 +25,7 @@ class ConversationApi extends ApiClient {
page,
labels,
conversation_type: conversationType,
sort_by: sortBy,
},
});
}
@@ -7,14 +7,22 @@ class OpenAIAPI extends ApiClient {
super('integrations', { accountScoped: true });
}
processEvent({ name = 'rephrase', content, tone, hookId }) {
processEvent({ type = 'rephrase', content, tone, conversationId, hookId }) {
let data = {
tone,
content,
};
if (type === 'reply_suggestion' || type === 'summarize') {
data = {
conversation_display_id: conversationId,
};
}
return axios.post(`${this.url}/hooks/${hookId}/process_event`, {
event: {
name: name,
data: {
tone,
content,
},
name: type,
data,
},
});
}
+24
View File
@@ -13,6 +13,30 @@ class SearchAPI extends ApiClient {
},
});
}
contacts({ q }) {
return axios.get(`${this.url}/contacts`, {
params: {
q,
},
});
}
conversations({ q }) {
return axios.get(`${this.url}/conversations`, {
params: {
q,
},
});
}
messages({ q }) {
return axios.get(`${this.url}/messages`, {
params: {
q,
},
});
}
}
export default new SearchAPI();
@@ -5,12 +5,20 @@
}
.date-picker {
.mx-datepicker {
width: 100%;
&.no-margin {
.mx-input {
margin-bottom: 0;
}
}
.mx-datepicker-range {
width: 320px;
&:not(.auto-width) {
.mx-datepicker-range {
width: 320px;
}
}
.mx-datepicker {
width: 100%;
}
.mx-input {
@@ -254,7 +254,7 @@ $button-sizes: (tiny: var(--font-size-micro),
default: var(--font-size-small),
large: var(--font-size-medium));
$button-palette: $foundation-palette;
$button-opacity-disabled: 0.25;
$button-opacity-disabled: 0.4;
$button-background-hover-lightness: -20%;
$button-hollow-hover-lightness: -50%;
$button-transition: background-color 0.25s ease-out,
@@ -261,6 +261,12 @@
}
}
// Basic filter dropdown
.basic-filter {
left: 0;
right: unset;
}
// Card label
.label-container {
.label {
@@ -13,7 +13,9 @@
}
.multiselect {
margin-bottom: var(--space-normal);
&:not(.no-margin) {
margin-bottom: var(--space-normal);
}
&.multiselect--disabled {
opacity: 0.8;
@@ -95,10 +95,6 @@
align-items: center;
justify-content: space-between;
padding: 0 var(--space-normal);
.page-title {
margin-bottom: 0;
}
}
.content-box {
@@ -11,15 +11,23 @@
class="chat-list__top"
:class="{ filter__applied: hasAppliedFiltersOrActiveFolders }"
>
<h1 class="page-title text-truncate" :title="pageTitle">
{{ pageTitle }}
</h1>
<div class="filter--actions">
<chat-filter
<div class="flex-center chat-list__title">
<h1
class="page-sub-title text-truncate margin-bottom-0"
:title="pageTitle"
>
{{ pageTitle }}
</h1>
<span
v-if="!hasAppliedFiltersOrActiveFolders"
@statusFilterChange="updateStatusType"
/>
class="conversation--status-pill"
>
{{
this.$t(`CHAT_LIST.CHAT_STATUS_FILTER_ITEMS.${activeStatus}.TEXT`)
}}
</span>
</div>
<div class="filter--actions">
<div v-if="hasAppliedFilters && !hasActiveFolders">
<woot-button
v-tooltip.top-end="$t('FILTER.CUSTOM_VIEWS.ADD.SAVE_BUTTON')"
@@ -48,7 +56,6 @@
@click="onClickOpenDeleteFoldersModal"
/>
</div>
<woot-button
v-else
v-tooltip.right="$t('FILTER.TOOLTIP_LABEL')"
@@ -58,6 +65,10 @@
size="tiny"
@click="onToggleAdvanceFiltersModal"
/>
<conversation-basic-filter
v-if="!hasAppliedFiltersOrActiveFolders"
@changeFilter="onBasicFilterChange"
/>
</div>
</div>
@@ -167,8 +178,8 @@
<script>
import { mapGetters } from 'vuex';
import ChatFilter from './widgets/conversation/ChatFilter';
import ConversationAdvancedFilter from './widgets/conversation/ConversationAdvancedFilter';
import ConversationBasicFilter from './widgets/conversation/ConversationBasicFilter';
import ChatTypeTabs from './widgets/ChatTypeTabs';
import ConversationCard from './widgets/conversation/ConversationCard';
import timeMixin from '../mixins/time';
@@ -199,10 +210,10 @@ export default {
AddCustomViews,
ChatTypeTabs,
ConversationCard,
ChatFilter,
ConversationAdvancedFilter,
DeleteCustomViews,
ConversationBulkActions,
ConversationBasicFilter,
},
mixins: [
timeMixin,
@@ -245,6 +256,7 @@ export default {
return {
activeAssigneeTab: wootConstants.ASSIGNEE_TYPE.ME,
activeStatus: wootConstants.STATUS_TYPE.OPEN,
activeSortBy: wootConstants.SORT_BY_TYPE.LATEST,
showAdvancedFilters: false,
advancedFilterTypes: advancedFilterTypes.map(filter => ({
...filter,
@@ -364,6 +376,7 @@ export default {
inboxId: this.conversationInbox ? this.conversationInbox : undefined,
assigneeType: this.activeAssigneeTab,
status: this.activeStatus,
sortBy: this.activeSortBy,
page: this.conversationListPagination,
labels: this.label ? [this.label] : undefined,
teamId: this.teamId || undefined,
@@ -479,7 +492,8 @@ export default {
},
},
mounted() {
this.$store.dispatch('setChatFilter', this.activeStatus);
this.$store.dispatch('setChatStatusFilter', this.activeStatus);
this.$store.dispatch('setChatSortFilter', this.activeSortBy);
this.resetAndFetchData();
bus.$on('fetch_conversation_stats', () => {
@@ -625,11 +639,13 @@ export default {
this.selectedConversations = [];
this.selectedInboxes = [];
},
updateStatusType(index) {
if (this.activeStatus !== index) {
this.activeStatus = index;
this.resetAndFetchData();
onBasicFilterChange(value, type) {
if (type === 'status') {
this.activeStatus = value;
} else {
this.activeSortBy = value;
}
this.resetAndFetchData();
},
openLastSavedItemInFolder() {
const lastItemOfFolder = this.folders[this.folders.length - 1];
@@ -878,11 +894,15 @@ export default {
&.list--full-width {
flex-basis: 100%;
}
.page-sub-title {
font-size: var(--font-size-two);
}
}
.filter--actions {
display: flex;
align-items: center;
gap: var(--space-micro);
gap: var(--space-smaller);
}
.filter__applied {
@@ -899,4 +919,19 @@ export default {
}
}
}
.conversation--status-pill {
background: var(--color-background);
border-radius: var(--border-radius-small);
color: var(--color-medium-gray);
font-size: var(--font-size-micro);
font-weight: var(--font-weight-medium);
margin: var(--space-micro) var(--space-small) 0;
padding: var(--space-smaller);
text-transform: capitalize;
}
.chat-list__title {
max-width: 85%;
}
</style>
@@ -5,7 +5,9 @@
{{ title }}
</p>
<p class="sub-head">
{{ subTitle }}
<slot name="subTitle">
{{ subTitle }}
</slot>
</p>
<p v-if="note">
<span class="note">{{ $t('INBOX_MGMT.NOTE') }}</span>
@@ -157,6 +157,7 @@ const settings = accountId => ({
hasSubMenu: false,
toState: frontendURL(`accounts/${accountId}/settings/audit-log/list`),
toStateName: 'auditlogs_list',
featureFlag: FEATURE_FLAGS.AUDIT_LOGS,
beta: true,
},
],
@@ -6,7 +6,11 @@
:disabled="isDisabled || isLoading"
@click="handleClick"
>
<spinner v-if="isLoading" size="small" />
<spinner
v-if="isLoading"
size="small"
:color-scheme="showDarkSpinner ? 'dark' : ''"
/>
<emoji-or-icon
v-else-if="icon || emoji"
class="icon"
@@ -108,6 +112,14 @@ export default {
return 16;
}
},
showDarkSpinner() {
return (
this.colorScheme === 'secondary' ||
this.variant === 'clear' ||
this.variant === 'link' ||
this.variant === 'hollow'
);
},
},
methods: {
handleClick(evt) {
@@ -1,45 +1,70 @@
<template>
<div v-if="isAIIntegrationEnabled" class="position-relative">
<woot-button
v-tooltip.top-end="$t('INTEGRATION_SETTINGS.OPEN_AI.TITLE')"
icon="wand"
color-scheme="secondary"
variant="smooth"
size="small"
@click="toggleDropdown"
/>
<div
v-if="showDropdown"
v-on-clickaway="closeDropdown"
class="dropdown-pane dropdown-pane--open ai-modal"
>
<h4 class="sub-block-title margin-top-1">
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.TITLE') }}
</h4>
<p>
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.SUBTITLE') }}
</p>
<label>
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.TONE.TITLE') }}
</label>
<div class="tone__item">
<select v-model="activeTone" class="status--filter small">
<option v-for="tone in tones" :key="tone.key" :value="tone.key">
{{ tone.value }}
</option>
</select>
</div>
<div class="modal-footer flex-container align-right">
<woot-button variant="clear" size="small" @click="closeDropdown">
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.BUTTONS.CANCEL') }}
</woot-button>
<woot-button
:is-loading="isGenerating"
size="small"
@click="processText"
>
{{ buttonText }}
</woot-button>
<div v-if="!message">
<woot-button
v-if="isPrivateNote"
v-tooltip.top-end="$t('INTEGRATION_SETTINGS.OPEN_AI.SUMMARY_TITLE')"
icon="book-pulse"
color-scheme="secondary"
variant="smooth"
size="small"
:is-loading="uiFlags.summarize"
@click="processEvent('summarize')"
/>
<woot-button
v-else
v-tooltip.top-end="$t('INTEGRATION_SETTINGS.OPEN_AI.REPLY_TITLE')"
icon="wand"
color-scheme="secondary"
variant="smooth"
size="small"
:is-loading="uiFlags.reply_suggestion"
@click="processEvent('reply_suggestion')"
/>
</div>
<div v-else>
<woot-button
v-tooltip.top-end="$t('INTEGRATION_SETTINGS.OPEN_AI.TITLE')"
icon="text-grammar-wand"
color-scheme="secondary"
variant="smooth"
size="small"
@click="toggleDropdown"
/>
<div
v-if="showDropdown"
v-on-clickaway="closeDropdown"
class="dropdown-pane dropdown-pane--open ai-modal"
>
<h4 class="sub-block-title margin-top-1">
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.TITLE') }}
</h4>
<p>
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.SUBTITLE') }}
</p>
<label>
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.TONE.TITLE') }}
</label>
<div class="tone__item">
<select v-model="activeTone" class="status--filter small">
<option v-for="tone in tones" :key="tone.key" :value="tone.key">
{{ tone.value }}
</option>
</select>
</div>
<div class="modal-footer flex-container align-right">
<woot-button variant="clear" size="small" @click="closeDropdown">
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.BUTTONS.CANCEL') }}
</woot-button>
<woot-button
:is-loading="uiFlags.rephrase"
size="small"
@click="processEvent('rephrase')"
>
{{ buttonText }}
</woot-button>
</div>
</div>
</div>
</div>
@@ -49,6 +74,7 @@ import { mapGetters } from 'vuex';
import { mixin as clickaway } from 'vue-clickaway';
import OpenAPI from 'dashboard/api/integrations/openapi';
import alertMixin from 'shared/mixins/alertMixin';
import { OPEN_AI_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
export default {
mixins: [alertMixin, clickaway],
@@ -61,10 +87,18 @@ export default {
type: String,
default: '',
},
isPrivateNote: {
type: Boolean,
default: false,
},
},
data() {
return {
isGenerating: false,
uiFlags: {
rephrase: false,
reply_suggestion: false,
summarize: false,
},
showDropdown: false,
activeTone: 'professional',
tones: [
@@ -94,7 +128,7 @@ export default {
).hooks[0].id;
},
buttonText() {
return this.isGenerating
return this.uiFlags.isRephrasing
? this.$t('INTEGRATION_SETTINGS.OPEN_AI.BUTTONS.GENERATING')
: this.$t('INTEGRATION_SETTINGS.OPEN_AI.BUTTONS.GENERATE');
},
@@ -111,24 +145,35 @@ export default {
closeDropdown() {
this.showDropdown = false;
},
async processText() {
this.isGenerating = true;
async recordAnalytics({ type, tone }) {
const event = OPEN_AI_EVENTS[type.toUpperCase()];
if (event) {
this.$track(event, {
type,
tone,
});
}
},
async processEvent(type = 'rephrase') {
this.uiFlags[type] = true;
try {
const result = await OpenAPI.processEvent({
hookId: this.hookId,
type: 'rephrase',
type,
content: this.message,
tone: this.activeTone,
conversationId: this.conversationId,
});
const {
data: { message: generatedMessage },
} = result;
this.$emit('replace-text', generatedMessage || this.message);
this.closeDropdown();
this.recordAnalytics({ type, tone: this.activeTone });
} catch (error) {
this.showAlert(this.$t('INTEGRATION_SETTINGS.OPEN_AI.GENERATE_ERROR'));
} finally {
this.isGenerating = false;
this.uiFlags[type] = false;
}
},
},
@@ -13,15 +13,19 @@ import videojs from 'video.js';
import alertMixin from '../../../../shared/mixins/alertMixin';
import Recorder from 'opus-recorder';
// Workers to record Audio .ogg and .wav
import encoderWorker from 'opus-recorder/dist/encoderWorker.min';
import waveWorker from 'opus-recorder/dist/waveWorker.min';
import WaveSurfer from 'wavesurfer.js';
import MicrophonePlugin from 'wavesurfer.js/dist/plugin/wavesurfer.microphone.js';
import 'videojs-wavesurfer/dist/videojs.wavesurfer.js';
import 'videojs-wavesurfer/dist/videojs.wavesurfer.js';
import 'videojs-record/dist/videojs.record.js';
import 'videojs-record/dist/plugins/videojs.record.opus-recorder.js';
import OpusRecorderEngine from 'videojs-record/dist/plugins/videojs.record.opus-recorder.js';
import { format, addSeconds } from 'date-fns';
import { AUDIO_FORMATS } from 'shared/constants/messages';
@@ -33,7 +37,7 @@ export default {
props: {
audioRecordFormat: {
type: String,
default: AUDIO_FORMATS.WEBM,
default: AUDIO_FORMATS.WAV,
},
},
data() {
@@ -79,24 +83,18 @@ export default {
maxLength: 900,
timeSlice: 1000,
maxFileSize: 15 * 1024 * 1024,
...(this.audioRecordFormat === AUDIO_FORMATS.WEBM && {
monitorGain: 0,
recordingGain: 1,
numberOfChannels: 1,
encoderSampleRate: 16000,
originalSampleRateOverride: 16000,
streamPages: true,
maxFramesPerPage: 1,
encoderFrameSize: 1,
encoderPath: waveWorker,
displayMilliseconds: false,
audioChannels: 1,
audioSampleRate: 48000,
audioBitRate: 128,
audioEngine: 'opus-recorder',
...(this.audioRecordFormat === AUDIO_FORMATS.WAV && {
audioMimeType: 'audio/wav',
audioWorkerURL: waveWorker,
}),
...(this.audioRecordFormat === AUDIO_FORMATS.OGG && {
displayMilliseconds: false,
audioEngine: 'opus-recorder',
audioMimeType: 'audio/ogg',
audioWorkerURL: encoderWorker,
audioChannels: 1,
audioSampleRate: 48000,
audioBitRate: 128,
}),
},
},
@@ -134,6 +132,11 @@ export default {
},
methods: {
deviceReady() {
if (this.player.record().engine instanceof OpusRecorderEngine) {
if (this.audioRecordFormat === AUDIO_FORMATS.WAV) {
this.player.record().engine.audioType = 'audio/wav';
}
}
this.player.record().start();
},
startRecord() {
@@ -49,7 +49,7 @@ import {
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import { isEditorHotKeyEnabled } from 'dashboard/mixins/uiSettings';
import { replaceVariablesInMessage } from 'dashboard/helper/messageHelper';
import { replaceVariablesInMessage } from '@chatwoot/utils';
import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
const createState = (content, placeholder, plugins = []) => {
@@ -38,12 +38,12 @@
</file-upload>
<woot-button
v-if="showAudioRecorderButton"
v-tooltip.top-end="$t('CONVERSATION.REPLYBOX.TIP_AUDIORECORDER_ICON')"
:icon="!isRecordingAudio ? 'microphone' : 'microphone-off'"
emoji="🎤"
:color-scheme="!isRecordingAudio ? 'secondary' : 'alert'"
variant="smooth"
size="small"
:title="$t('CONVERSATION.REPLYBOX.TIP_AUDIORECORDER_ICON')"
@click="toggleAudioRecorder"
/>
<woot-button
@@ -92,8 +92,8 @@
:conversation-id="conversationId"
/>
<AIAssistanceButton
v-if="message"
:conversation-id="conversationId"
:is-private-note="isOnPrivateNote"
:message="message"
@replace-text="replaceText"
/>
@@ -38,8 +38,8 @@ export default {
this.onTabChange();
},
onTabChange() {
this.$store.dispatch('setChatFilter', this.activeStatus);
this.$emit('statusFilterChange', this.activeStatus);
this.$store.dispatch('setChatStatusFilter', this.activeStatus);
this.$emit('onChangeFilter', this.activeStatus);
},
},
};
@@ -0,0 +1,122 @@
<template>
<div class="position-relative">
<woot-button
v-tooltip.right="$t('CHAT_LIST.SORT_TOOLTIP_LABEL')"
variant="smooth"
size="tiny"
color-scheme="secondary"
class="selector-button"
icon="sort-icon"
@click="toggleDropdown"
/>
<div
v-if="showActionsDropdown"
v-on-clickaway="closeDropdown"
class="dropdown-pane dropdown-pane--open basic-filter"
>
<div class="filter__item">
<span>{{ this.$t('CHAT_LIST.CHAT_SORT.STATUS') }}</span>
<filter-item
type="status"
:selected-value="chatStatus"
:items="chatStatusItems"
path-prefix="CHAT_LIST.CHAT_STATUS_FILTER_ITEMS"
@onChangeFilter="onChangeFilter"
/>
</div>
<div class="filter__item">
<span>{{ this.$t('CHAT_LIST.CHAT_SORT.ORDER_BY') }}</span>
<filter-item
type="sort"
:selected-value="chatSortFilter"
:items="chatSortItems"
path-prefix="CHAT_LIST.CHAT_SORT_FILTER_ITEMS"
@onChangeFilter="onChangeFilter"
/>
</div>
</div>
</div>
</template>
<script>
import wootConstants from 'dashboard/constants/globals';
import { mapGetters } from 'vuex';
import { mixin as clickaway } from 'vue-clickaway';
import FilterItem from './FilterItem';
export default {
components: {
FilterItem,
},
mixins: [clickaway],
data() {
return {
showActionsDropdown: false,
chatStatusItems: this.$t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS'),
chatSortItems: this.$t('CHAT_LIST.CHAT_SORT_FILTER_ITEMS'),
};
},
computed: {
...mapGetters({
chatStatusFilter: 'getChatStatusFilter',
chatSortFilter: 'getChatSortFilter',
}),
chatStatus() {
return this.chatStatusFilter || wootConstants.STATUS_TYPE.OPEN;
},
sortFilter() {
return this.chatSortFilter || wootConstants.SORT_BY_TYPE.LATEST;
},
},
methods: {
onTabChange(value) {
this.$emit('changeFilter', value);
this.closeDropdown();
},
toggleDropdown() {
this.showActionsDropdown = !this.showActionsDropdown;
},
closeDropdown() {
this.showActionsDropdown = false;
},
onChangeFilter(type, value) {
this.$emit('changeFilter', type, value);
},
},
};
</script>
<style lang="scss" scoped>
.basic-filter {
margin-top: var(--space-smaller);
padding: var(--space-normal);
right: 0;
width: 21rem;
span {
font-size: var(--font-size-small);
font-weight: var(--font-weight-medium);
}
.filter__item {
align-items: center;
display: flex;
justify-content: space-between;
&:last-child {
margin-top: var(--space-normal);
}
span {
font-size: var(--font-size-mini);
}
}
}
.icon {
margin-right: var(--space-smaller);
}
.dropdown-icon {
margin-left: var(--space-smaller);
}
</style>
@@ -9,7 +9,7 @@
}"
@mouseenter="onCardHover"
@mouseleave="onCardLeave"
@click="cardClick(chat)"
@click="onCardClick"
@contextmenu="openContextMenu($event)"
>
<label v-if="hovered || selected" class="checkbox-wrapper" @click.stop>
@@ -313,21 +313,33 @@ export default {
},
},
methods: {
cardClick(chat) {
const { activeInbox } = this;
const path = conversationUrl({
accountId: this.accountId,
activeInbox,
id: chat.id,
label: this.activeLabel,
teamId: this.teamId,
foldersId: this.foldersId,
conversationType: this.conversationType,
});
onCardClick(e) {
const { activeInbox, chat } = this;
const path = frontendURL(
conversationUrl({
accountId: this.accountId,
activeInbox,
id: chat.id,
label: this.activeLabel,
teamId: this.teamId,
foldersId: this.foldersId,
conversationType: this.conversationType,
})
);
if (e.metaKey || e.ctrlKey) {
window.open(
window.chatwootConfig.hostURL + path,
'_blank',
'noopener noreferrer nofollow'
);
return;
}
if (this.isActiveChat) {
return;
}
router.push({ path: frontendURL(path) });
router.push({ path });
},
onCardHover() {
this.hovered = !this.hideThumbnail;
@@ -443,11 +455,11 @@ export default {
.conversation-metadata-attributes {
display: flex;
gap: var(--space-small);
margin-left: var(--space-small);
}
.assignee-label {
display: inline-flex;
margin-left: var(--space-small);
max-width: 50%;
}
}
@@ -0,0 +1,55 @@
<template>
<select v-model="activeValue" class="status--filter" @change="onTabChange()">
<option v-for="(value, status) in items" :key="status" :value="status">
{{ $t(`${pathPrefix}.${status}.TEXT`) }}
</option>
</select>
</template>
<script>
export default {
props: {
selectedValue: {
type: String,
required: true,
},
items: {
type: Object,
required: true,
},
type: {
type: String,
required: true,
},
pathPrefix: {
type: String,
required: true,
},
},
data() {
return {
activeValue: this.selectedValue,
};
},
methods: {
onTabChange() {
if (this.type === 'status') {
this.$store.dispatch('setChatStatusFilter', this.activeValue);
} else {
this.$store.dispatch('setChatSortFilter', this.activeValue);
}
this.$emit('onChangeFilter', this.activeValue, this.type);
},
},
};
</script>
<style lang="scss" scoped>
.status--filter {
background-color: var(--color-background-light);
border: 1px solid var(--color-border);
font-size: var(--font-size-mini);
height: var(--space-medium);
margin: 0 var(--space-smaller);
padding: 0 var(--space-medium) 0 var(--space-small);
width: 126px;
}
</style>
@@ -22,6 +22,14 @@
:bcc="emailHeadAttributes.bcc"
:is-incoming="isIncoming"
/>
<blockquote v-if="storyReply" class="story-reply-quote">
<span>{{ $t('CONVERSATION.REPLIED_TO_STORY') }}</span>
<bubble-image
v-if="!hasStoryError"
:url="storyUrl"
@error="onStoryLoadError"
/>
</blockquote>
<bubble-text
v-if="data.content"
:message="message"
@@ -51,7 +59,7 @@
controls
class="skip-context-menu"
>
<source :src="attachment.data_url" />
<source :src="`${attachment.data_url}?t=${Date.now()}`" />
</audio>
<bubble-video
v-else-if="attachment.file_type === 'video'"
@@ -79,7 +87,7 @@
:sender="data.sender"
:story-sender="storySender"
:external-error="externalError"
:story-id="storyId"
:story-id="`${storyId}`"
:is-a-tweet="isATweet"
:is-a-whatsapp-channel="isAWhatsAppChannel"
:has-instagram-story="hasInstagramStory"
@@ -192,6 +200,7 @@ export default {
hasImageError: false,
contextMenuPosition: {},
showBackgroundHighlight: false,
hasStoryError: false,
};
},
computed: {
@@ -277,6 +286,12 @@ export default {
storyId() {
return this.contentAttributes.story_id || null;
},
storyUrl() {
return this.contentAttributes.story_url || null;
},
storyReply() {
return this.storyUrl && this.hasInstagramStory;
},
contentType() {
const {
data: { content_type: contentType },
@@ -414,10 +429,12 @@ export default {
watch: {
data() {
this.hasImageError = false;
this.hasStoryError = false;
},
},
mounted() {
this.hasImageError = false;
this.hasStoryError = false;
bus.$on(BUS_EVENTS.ON_MESSAGE_LIST_SCROLL, this.closeContextMenu);
this.setupHighlightTimer();
},
@@ -432,6 +449,9 @@ export default {
const { file_type: fileType } = attachments[0];
return fileType === type && !this.hasImageError;
}
if (this.storyReply) {
return true;
}
return false;
},
handleContextMenuClick() {
@@ -443,6 +463,9 @@ export default {
onImageLoadError() {
this.hasImageError = true;
},
onStoryLoadError() {
this.hasStoryError = true;
},
openContextMenu(e) {
const shouldSkipContextMenu =
e.target?.classList.contains('skip-context-menu') ||
@@ -672,7 +695,6 @@ li.right {
blockquote {
border-left: var(--space-micro) solid var(--s-75);
color: var(--s-800);
padding: var(--space-smaller) var(--space-small);
margin: var(--space-smaller) 0;
padding: var(--space-small) var(--space-small) 0 var(--space-normal);
}
@@ -704,4 +726,11 @@ li.right {
}
}
}
.story-reply-quote {
border-left: var(--space-micro) solid var(--s-75);
color: var(--s-600);
margin: var(--space-small) var(--space-normal) 0;
padding: var(--space-small) var(--space-small) 0 var(--space-small);
}
</style>
@@ -93,7 +93,9 @@ import { mapGetters } from 'vuex';
import ReplyBox from './ReplyBox';
import Message from './Message';
import conversationMixin from '../../../mixins/conversations';
import conversationMixin, {
filterDuplicateSourceMessages,
} from '../../../mixins/conversations';
import Banner from 'dashboard/components/ui/Banner.vue';
import { getTypingUsersText } from '../../../helper/commons';
import { BUS_EVENTS } from 'shared/constants/busEvents';
@@ -171,24 +173,28 @@ export default {
return '';
},
getMessages() {
const [chat] = this.allConversations.filter(
c => c.id === this.currentChat.id
);
return chat;
const messages = this.currentChat.messages || [];
if (this.isAWhatsAppChannel) {
return filterDuplicateSourceMessages(messages);
}
return messages;
},
getReadMessages() {
const chat = this.getMessages;
return chat === undefined ? null : this.readMessages(chat);
return this.readMessages(
this.getMessages,
this.currentChat.agent_last_seen_at
);
},
getUnReadMessages() {
const chat = this.getMessages;
return chat === undefined ? null : this.unReadMessages(chat);
return this.unReadMessages(
this.getMessages,
this.currentChat.agent_last_seen_at
);
},
shouldShowSpinner() {
return (
(this.getMessages && this.getMessages.dataFetched === undefined) ||
(this.currentChat && this.currentChat.dataFetched === undefined) ||
(!this.listLoadingStatus && this.isLoadingPrevious)
);
},
@@ -208,7 +214,7 @@ export default {
selectedTweet() {
if (this.selectedTweetId) {
const { messages = [] } = this.getMessages;
const { messages = [] } = this.currentChat;
const [selectedMessage] = messages.filter(
message => message.id === this.selectedTweetId
);
@@ -360,7 +366,7 @@ export default {
async fetchPreviousMessages(scrollTop = 0) {
this.setScrollParams();
const shouldLoadMoreMessages =
this.getMessages.dataFetched === true &&
this.currentChat.dataFetched === true &&
!this.listLoadingStatus &&
!this.isLoadingPrevious;
@@ -373,7 +379,7 @@ export default {
try {
await this.$store.dispatch('fetchPreviousMessages', {
conversationId: this.currentChat.id,
before: this.getMessages.messages[0].id,
before: this.currentChat.messages[0].id,
});
const heightDifference =
this.conversationPanel.scrollHeight - this.heightBeforeLoad;
@@ -48,15 +48,17 @@ export default {
<style scoped lang="scss">
.conversation-priority-mark {
align-items: center;
flex-shrink: 0;
background: var(--s-50);
border-radius: var(--border-radius-small);
color: var(--s-600);
display: inline-flex;
width: var(--space-snug);
height: var(--space-snug);
&.urgent {
background: var(--r-500);
color: var(--w-25);
background: var(--r-50);
color: var(--r-500);
}
}
</style>
@@ -164,11 +164,11 @@ import {
AUDIO_FORMATS,
} from 'shared/constants/messages';
import { BUS_EVENTS } from 'shared/constants/busEvents';
import { replaceVariablesInMessage } from 'dashboard/helper/messageHelper';
import {
getMessageVariables,
getUndefinedVariablesInMessage,
} from 'dashboard/helper/messageHelper';
replaceVariablesInMessage,
} from '@chatwoot/utils';
import WhatsappTemplates from './WhatsappTemplates/Modal.vue';
import { buildHotKeys } from 'shared/helpers/KeyboardHelpers';
import { MESSAGE_MAX_LENGTH } from 'shared/helpers/MessageTypeHelper';
@@ -501,10 +501,10 @@ export default {
return `draft-${this.conversationIdByRoute}-${this.replyType}`;
},
audioRecordFormat() {
if (this.isAWebWidgetInbox) {
return AUDIO_FORMATS.WEBM;
if (this.isAWhatsAppChannel) {
return AUDIO_FORMATS.OGG;
}
return AUDIO_FORMATS.OGG;
return AUDIO_FORMATS.WAV;
},
messageVariables() {
const variables = getMessageVariables({
@@ -195,7 +195,7 @@ export default {
return '';
}
const { storySender, storyId } = this;
return `https://www.instagram.com/stories/${storySender}/${storyId}`;
return `https://www.instagram.com/stories/direct/${storySender}_${storyId}`;
},
showStatusIndicators() {
if ((this.isOutgoing || this.isTemplate) && !this.isPrivate) {
@@ -1,6 +1,6 @@
<template>
<div v-on-clickaway="onCloseAgentList" class="bulk-action__agents">
<div class="triangle" :style="cssVars">
<div class="triangle">
<svg height="12" viewBox="0 0 24 12" width="24">
<path
d="M20 12l-8-8-12 12"
@@ -104,14 +104,13 @@ import { mapGetters } from 'vuex';
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
import Spinner from 'shared/components/Spinner';
import { mixin as clickaway } from 'vue-clickaway';
import bulkActionsMixin from 'dashboard/mixins/bulkActionsMixin.js';
export default {
components: {
Thumbnail,
Spinner,
},
mixins: [clickaway, bulkActionsMixin],
mixins: [clickaway],
props: {
selectedInboxes: {
type: Array,
@@ -240,7 +239,7 @@ export default {
display: block;
z-index: var(--z-index-one);
position: absolute;
top: calc(var(--space-slab) * -1);
top: var(--space-minus-slab);
right: var(--triangle-position);
text-align: left;
}
@@ -58,7 +58,7 @@
<transition name="popover-animation">
<label-actions
v-if="showLabelActions"
triangle-position="8.5"
class="label-actions-box"
@assign="assignLabels"
@close="showLabelActions = false"
/>
@@ -66,12 +66,12 @@
<transition name="popover-animation">
<update-actions
v-if="showUpdateActions"
class="update-actions-box"
:selected-inboxes="selectedInboxes"
:conversation-count="conversations.length"
:show-resolve="!showResolvedAction"
:show-reopen="!showOpenAction"
:show-snooze="!showSnoozedAction"
triangle-position="5.6"
@update="updateConversations"
@close="showUpdateActions = false"
/>
@@ -79,9 +79,9 @@
<transition name="popover-animation">
<agent-selector
v-if="showAgentsList"
class="agent-actions-box"
:selected-inboxes="selectedInboxes"
:conversation-count="conversations.length"
triangle-position="2.8"
@select="submit"
@close="showAgentsList = false"
/>
@@ -89,7 +89,7 @@
<transition name="popover-animation">
<team-actions
v-if="showTeamsList"
triangle-position="0.2"
class="team-actions-box"
@assign-team="assignTeam"
@close="showTeamsList = false"
/>
@@ -247,4 +247,17 @@ export default {
opacity: 0;
transform: scale(0.95);
}
.label-actions-box {
--triangle-position: 8.5rem;
}
.update-actions-box {
--triangle-position: 5.6rem;
}
.agent-actions-box {
--triangle-position: 2.8rem;
}
.team-actions-box {
--triangle-position: 0.2rem;
}
</style>
@@ -1,6 +1,6 @@
<template>
<div v-on-clickaway="onClose" class="labels-container">
<div class="triangle" :style="cssVars">
<div class="triangle">
<svg height="12" viewBox="0 0 24 12" width="24">
<path
d="M20 12l-8-8-12 12"
@@ -75,10 +75,9 @@
<script>
import { mixin as clickaway } from 'vue-clickaway';
import { mapGetters } from 'vuex';
import bulkActionsMixin from 'dashboard/mixins/bulkActionsMixin.js';
export default {
mixins: [clickaway, bulkActionsMixin],
mixins: [clickaway],
data() {
return {
query: '',
@@ -207,7 +206,7 @@ export default {
position: absolute;
right: var(--triangle-position);
text-align: left;
top: calc(var(--space-slab) * -1);
top: var(--space-minus-slab);
z-index: var(--z-index-one);
}
}
@@ -1,6 +1,6 @@
<template>
<div v-on-clickaway="onClose" class="bulk-action__teams">
<div class="triangle" :style="cssVars">
<div class="triangle">
<svg height="12" viewBox="0 0 24 12" width="24">
<path
d="M20 12l-8-8-12 12"
@@ -59,9 +59,8 @@
<script>
import { mixin as clickaway } from 'vue-clickaway';
import { mapGetters } from 'vuex';
import bulkActionsMixin from 'dashboard/mixins/bulkActionsMixin.js';
export default {
mixins: [clickaway, bulkActionsMixin],
mixins: [clickaway],
data() {
return {
query: '',
@@ -141,7 +140,7 @@ export default {
display: block;
z-index: var(--z-index-one);
position: absolute;
top: calc(var(--space-slab) * -1);
top: var(--space-minus-slab);
right: var(--triangle-position);
text-align: left;
}
@@ -1,6 +1,6 @@
<template>
<div v-on-clickaway="onClose" class="actions-container">
<div class="triangle" :style="cssVars">
<div class="triangle">
<svg height="12" viewBox="0 0 24 12" width="24">
<path
d="M20 12l-8-8-12 12"
@@ -45,14 +45,13 @@
import { mixin as clickaway } from 'vue-clickaway';
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
import bulkActionsMixin from 'dashboard/mixins/bulkActionsMixin.js';
export default {
components: {
WootDropdownItem,
WootDropdownMenu,
},
mixins: [clickaway, bulkActionsMixin],
mixins: [clickaway],
props: {
selectedInboxes: {
type: Array,
@@ -156,7 +155,7 @@ export default {
position: absolute;
right: var(--triangle-position);
text-align: left;
top: calc(var(--space-slab) * -1);
top: var(--space-minus-slab);
z-index: var(--z-index-one);
}
}
@@ -12,6 +12,11 @@ export default {
SNOOZED: 'snoozed',
ALL: 'all',
},
SORT_BY_TYPE: {
LATEST: 'latest',
CREATED_AT: 'sort_on_created_at',
PRIORITY: 'sort_on_priority',
},
ARTICLE_STATUS_TYPES: {
DRAFT: 0,
PUBLISH: 1,
+1
View File
@@ -15,4 +15,5 @@ export const FEATURE_FLAGS = {
REPORTS: 'reports',
TEAM_MANAGEMENT: 'team_management',
VOICE_RECORDER: 'voice_recorder',
AUDIT_LOGS: 'audit_logs',
};
@@ -76,3 +76,9 @@ export const PORTALS_EVENTS = Object.freeze({
DELETE_ARTICLE: 'Deleted an article',
PREVIEW_ARTICLE: 'Previewed article',
});
export const OPEN_AI_EVENTS = Object.freeze({
SUMMARIZE: 'OpenAI: Used summarize',
REPLY_SUGGESTION: 'OpenAI: Used reply suggestion',
REPHRASE: 'OpenAI: Used rephrase',
});
@@ -105,8 +105,16 @@ class ActionCableConnector extends BaseActionCableConnector {
onLogout = () => AuthAPI.logout();
onMessageCreated = data => {
const {
conversation: { last_activity_at: lastActivityAt },
conversation_id: conversationId,
} = data;
DashboardAudioNotificationHelper.onNewMessage(data);
this.app.$store.dispatch('addMessage', data);
this.app.$store.dispatch('updateConversationLastActivity', {
lastActivityAt,
conversationId,
});
};
onReload = () => window.location.reload();
@@ -1,66 +0,0 @@
const MESSAGE_VARIABLES_REGEX = /{{(.*?)}}/g;
export const replaceVariablesInMessage = ({ message, variables }) => {
return message.replace(MESSAGE_VARIABLES_REGEX, (match, replace) => {
return variables[replace.trim()]
? variables[replace.trim().toLowerCase()]
: '';
});
};
export const capitalizeName = name => {
return name.replace(/\b(\w)/g, s => s.toUpperCase());
};
const skipCodeBlocks = str => str.replace(/```(?:.|\n)+?```/g, '');
export const getFirstName = ({ user }) => {
const firstName = user?.name ? user.name.split(' ').shift() : '';
return capitalizeName(firstName);
};
export const getLastName = ({ user }) => {
if (user && user.name) {
const lastName =
user.name.split(' ').length > 1 ? user.name.split(' ').pop() : '';
return capitalizeName(lastName);
}
return '';
};
export const getMessageVariables = ({ conversation }) => {
const {
meta: { assignee = {}, sender = {} },
id,
} = conversation;
return {
'contact.name': capitalizeName(sender?.name),
'contact.first_name': getFirstName({ user: sender }),
'contact.last_name': getLastName({ user: sender }),
'contact.email': sender?.email,
'contact.phone': sender?.phone_number,
'contact.id': sender?.id,
'conversation.id': id,
'agent.name': capitalizeName(assignee?.name || ''),
'agent.first_name': getFirstName({ user: assignee }),
'agent.last_name': getLastName({ user: assignee }),
'agent.email': assignee?.email ?? '',
};
};
export const getUndefinedVariablesInMessage = ({ message, variables }) => {
const messageWithOutCodeBlocks = skipCodeBlocks(message);
const matches = messageWithOutCodeBlocks.match(MESSAGE_VARIABLES_REGEX);
if (!matches) return [];
return matches
.map(match => {
return match
.replace('{{', '')
.replace('}}', '')
.trim();
})
.filter(variable => {
return !variables[variable];
});
};
@@ -8,8 +8,8 @@ export const buildPortalArticleURL = (
portalSlug,
categorySlug,
locale,
articleId
articleSlug
) => {
const portalURL = buildPortalURL(portalSlug);
return `${portalURL}/${locale}/${categorySlug}/${articleId}`;
return `${portalURL}/articles/${articleSlug}`;
};
@@ -1,166 +0,0 @@
import {
replaceVariablesInMessage,
getFirstName,
getLastName,
getMessageVariables,
getUndefinedVariablesInMessage,
capitalizeName,
} from '../messageHelper';
const variables = {
'contact.name': 'John Doe',
'contact.first_name': 'John',
'contact.last_name': 'Doe',
'contact.email': 'john.p@example.com',
'contact.phone': '1234567890',
'conversation.id': 1,
'agent.first_name': 'Samuel',
'agent.last_name': 'Smith',
'agent.email': 'samuel@gmail.com',
};
describe('#replaceVariablesInMessage', () => {
it('returns the message with variable name', () => {
const message =
'No issues. Hey {{contact.first_name}}, we will send the reset instructions to your email {{ contact.email}}. The {{ agent.first_name }} {{ agent.last_name }} will take care of everything. Your conversation id is {{ conversation.id }}.';
expect(replaceVariablesInMessage({ message, variables })).toBe(
'No issues. Hey John, we will send the reset instructions to your email john.p@example.com. The Samuel Smith will take care of everything. Your conversation id is 1.'
);
});
it('returns the message with variable name having white space', () => {
const message = 'hey {{contact.name}} how may I help you?';
expect(replaceVariablesInMessage({ message, variables })).toBe(
'hey John Doe how may I help you?'
);
});
it('returns the message with variable email', () => {
const message =
'No issues. We will send the reset instructions to your email at {{contact.email}}';
expect(replaceVariablesInMessage({ message, variables })).toBe(
'No issues. We will send the reset instructions to your email at john.p@example.com'
);
});
it('returns the message with multiple variables', () => {
const message =
'hey {{ contact.name }}, no issues. We will send the reset instructions to your email at {{contact.email}}';
expect(replaceVariablesInMessage({ message, variables })).toBe(
'hey John Doe, no issues. We will send the reset instructions to your email at john.p@example.com'
);
});
it('returns the message if the variable is not present in variables', () => {
const message = 'Please dm me at {{contact.twitter}}';
expect(replaceVariablesInMessage({ message, variables })).toBe(
'Please dm me at '
);
});
});
describe('#getFirstName', () => {
it('returns the first name of the contact', () => {
const assignee = { name: 'John Doe' };
expect(getFirstName({ user: assignee })).toBe('John');
});
it('returns the first name of the contact with multiple names', () => {
const assignee = { name: 'John Doe Smith' };
expect(getFirstName({ user: assignee })).toBe('John');
});
});
describe('#getLastName', () => {
it('returns the last name of the contact', () => {
const assignee = { name: 'John Doe' };
expect(getLastName({ user: assignee })).toBe('Doe');
});
it('returns the last name of the contact with multiple names', () => {
const assignee = { name: 'John Doe Smith' };
expect(getLastName({ user: assignee })).toBe('Smith');
});
});
describe('#getMessageVariables', () => {
it('returns the variables', () => {
const conversation = {
meta: {
assignee: {
name: 'samuel Smith',
email: 'samuel@example.com',
},
sender: {
name: 'john Doe',
email: 'john.doe@gmail.com',
phone_number: '1234567890',
},
},
id: 1,
};
expect(getMessageVariables({ conversation })).toEqual({
'contact.name': 'John Doe',
'contact.first_name': 'John',
'contact.last_name': 'Doe',
'contact.email': 'john.doe@gmail.com',
'contact.phone': '1234567890',
'conversation.id': 1,
'agent.name': 'Samuel Smith',
'agent.first_name': 'Samuel',
'agent.last_name': 'Smith',
'agent.email': 'samuel@example.com',
});
});
});
describe('#getUndefinedVariablesInMessage', () => {
it('returns the undefined variables', () => {
const message = 'Please dm me at {{contact.twitter}}';
expect(
getUndefinedVariablesInMessage({ message, variables }).length
).toEqual(1);
expect(getUndefinedVariablesInMessage({ message, variables })).toEqual(
expect.arrayContaining(['contact.twitter'])
);
});
it('skip variables in string with code blocks', () => {
const message =
'hey {{contact_name}} how are you? ``` code: {{contact_name}} ```';
const undefinedVariables = getUndefinedVariablesInMessage({
message,
variables,
});
expect(undefinedVariables.length).toEqual(1);
expect(undefinedVariables).toEqual(
expect.arrayContaining(['contact_name'])
);
});
});
describe('#capitalizeName', () => {
it('capitalize name if name is passed', () => {
const string = 'john peter';
expect(capitalizeName(string)).toBe('John Peter');
});
it('capitalize first name if full name is passed', () => {
const string = 'john Doe';
expect(capitalizeName(string)).toBe('John Doe');
});
it('returns empty string if the string is empty', () => {
const string = '';
expect(capitalizeName(string)).toBe('');
});
it('capitalize last name if last name is passed', () => {
const string = 'john doe';
expect(capitalizeName(string)).toBe('John Doe');
});
it('capitalize first name if first name is passed', () => {
const string = 'john';
expect(capitalizeName(string)).toBe('John');
});
it('capitalize last name if last name is passed', () => {
const string = 'doe';
expect(capitalizeName(string)).toBe('Doe');
});
});
@@ -20,9 +20,9 @@ describe('PortalHelper', () => {
hostURL: 'https://app.chatwoot.com',
helpCenterURL: 'https://help.chatwoot.com',
};
expect(buildPortalArticleURL('handbook', 'culture', 'fr', 1)).toEqual(
'https://help.chatwoot.com/hc/handbook/fr/culture/1'
);
expect(
buildPortalArticleURL('handbook', 'culture', 'fr', 'article-slug')
).toEqual('https://help.chatwoot.com/hc/handbook/articles/article-slug');
window.chatwootConfig = {};
});
});
+2
View File
@@ -9,6 +9,7 @@ import es from './locale/es';
import fa from './locale/fa';
import fi from './locale/fi';
import fr from './locale/fr';
import he from './locale/he';
import hi from './locale/hi';
import hu from './locale/hu';
import id from './locale/id';
@@ -47,6 +48,7 @@ export default {
fa,
fi,
fr,
he,
hi,
hu,
id,
@@ -38,6 +38,7 @@
"CAMPAIGN_NAME": "اسم الحملة",
"LABELS": "الوسوم",
"BROWSER_LANGUAGE": "لغة المتصفح",
"PRIORITY": "Priority",
"COUNTRY_NAME": "اسم الدولة",
"REFERER_LINK": "رابط المرجع",
"CUSTOM_ATTRIBUTE_LIST": "القائمة",
@@ -110,7 +110,8 @@
},
"PLACEHOLDER": {
"AGENT": "البحث عن وكلاء",
"TEAM": "البحث عن فريق"
"TEAM": "البحث عن فريق",
"INPUT": "Search for agents"
}
}
}
@@ -35,6 +35,20 @@
"TEXT": "الكل"
}
},
"VIEW_FILTER": "عرض",
"SORT_TOOLTIP_LABEL": "Sort conversations",
"CHAT_SORT": {
"STATUS": "الحالة",
"ORDER_BY": "Order by"
},
"CHAT_SORT_FILTER_ITEMS": {
"latest": {
"TEXT": "Last activity"
},
"sort_on_created_at": {
"TEXT": "تم إنشاؤها في"
}
},
"ATTACHMENTS": {
"image": {
"CONTENT": "رسالة صورة"
@@ -55,6 +69,24 @@
"CONTENT": "قام بمشاركة رابط"
}
},
"CHAT_SORT_BY_FILTER": {
"TITLE": "Sort conversation",
"DROPDOWN_TITLE": "ترتيب حسب",
"ITEMS": {
"LATEST": {
"NAME": "Last activity at",
"LABEL": "Last activity"
},
"CREATED_AT": {
"NAME": "تم إنشاؤها في",
"LABEL": "تم إنشاؤها في"
},
"LAST_USER_MESSAGE_AT": {
"NAME": "Last user message at",
"LABEL": "Last message"
}
}
},
"RECEIVED_VIA_EMAIL": "تم تلقيه عبر البريد الإلكتروني",
"VIEW_TWEET_IN_TWITTER": "عرض التغريدة في تويتر",
"REPLY_TO_TWEET": "الرد على هذه التغريدة",
@@ -37,6 +37,7 @@
"UNKNOWN_FILE_TYPE": "ملف غير معروف",
"SAVE_CONTACT": "حفظ",
"UPLOADING_ATTACHMENTS": "جاري تحميل المرفقات...",
"REPLIED_TO_STORY": "Replied to your story",
"SUCCESS_DELETE_MESSAGE": "تم حذف الرسالة بنجاح",
"FAIL_DELETE_MESSSAGE": "تعذر حذف الرسالة! حاول مرة أخرى",
"NO_RESPONSE": "لا توجد استجابة",
@@ -66,6 +67,23 @@
"NEXT_WEEK": "الأسبوع المقبل"
}
},
"PRIORITY": {
"TITLE": "Priority",
"OPTIONS": {
"NONE": "لا شيء",
"URGENT": "Urgent",
"HIGH": "High",
"MEDIUM": "Medium",
"LOW": "Low"
},
"CHANGE_PRIORITY": {
"SELECT_PLACEHOLDER": "لا شيء",
"INPUT_PLACEHOLDER": "Select priority",
"NO_RESULTS": "لم يتم العثور على النتائج",
"SUCCESSFUL": "Changed priority of conversation id %{conversationId} to %{priority}",
"FAILED": "Couldn't change priority. Please try again."
}
},
"CARD_CONTEXT_MENU": {
"PENDING": "تحديد كمعلق",
"RESOLVED": "تحديد كمحلولة",
@@ -101,6 +101,7 @@
"REPORTS": "التقارير",
"CONVERSATION": "المحادثات",
"CHANGE_ASSIGNEE": "تغيير المحال إليه",
"CHANGE_PRIORITY": "Change Priority",
"CHANGE_TEAM": "تغيير الفريق",
"ADD_LABEL": "إضافة تسمية إلى المحادثة",
"REMOVE_LABEL": "إزالة التسمية من المحادثة",
@@ -126,6 +127,7 @@
"GO_TO_NOTIFICATIONS": "الذهاب إلى الإشعارات",
"ADD_LABELS_TO_CONVERSATION": "إضافة تسمية إلى المحادثة",
"ASSIGN_AN_AGENT": "تعيين وكيل",
"ASSIGN_PRIORITY": "Assign priority",
"ASSIGN_A_TEAM": "تعيين فريق",
"MUTE_CONVERSATION": "كتم المحادثة",
"UNMUTE_CONVERSATION": "إلغاء كتم المحادثة",
@@ -464,6 +464,7 @@
"AUTO_ASSIGNMENT_SUB_TEXT": "تمكين أو تعطيل الإسناد التلقائي للمحادثات الجديدة إلى الموظفين المضافين إلى قناة التواصل هذه.",
"HMAC_VERIFICATION": "التحقق من هوية المستخدم",
"HMAC_DESCRIPTION": "من أجل التحقق من هوية المستخدم، يمكنك تمرير 'IDer_hash` لكل مستخدم. يمكنك إنشاء تجزئة HMAC sha256 باستخدام \"المعرف\" مع المفتاح المعروض هنا.",
"HMAC_LINK_TO_DOCS": "You can read more here.",
"HMAC_MANDATORY_VERIFICATION": "فرض التحقق من هوية المستخدم",
"HMAC_MANDATORY_DESCRIPTION": "في حالة التمكين، سيتم رفض الطلبات المفقودة 'IDer_hash'.",
"INBOX_IDENTIFIER": "معرف صندوق الوارد",
@@ -482,6 +483,13 @@
"WHATSAPP_WEBHOOK_SUBHEADER": "This token is used to verify the authenticity of the webhook endpoint.",
"UPDATE_PRE_CHAT_FORM_SETTINGS": "Update Pre Chat Form Settings"
},
"HELP_CENTER": {
"LABEL": "Help Center",
"PLACEHOLDER": "Select Help Center",
"SELECT_PLACEHOLDER": "Select Help Center",
"REMOVE": "Remove Help Center",
"SUB_TEXT": "Attach a Help Center with the inbox"
},
"AUTO_ASSIGNMENT": {
"MAX_ASSIGNMENT_LIMIT": "حد الإسناد التلقائي",
"MAX_ASSIGNMENT_LIMIT_RANGE_ERROR": "الرجاء إدخال قيمة أكبر من 0",
@@ -85,6 +85,23 @@
"JOIN_ERROR": "There was an error joining the call, please try again",
"CREATE_ERROR": "There was an error creating a meeting link, please try again"
},
"OPEN_AI": {
"TITLE": "Improve With AI",
"SUBTITLE": "An improved reply will be generated using AI, based on your current draft.",
"TONE": {
"TITLE": "Tone",
"OPTIONS": {
"PROFESSIONAL": "Professional",
"FRIENDLY": "Friendly"
}
},
"BUTTONS": {
"GENERATE": "Generate",
"GENERATING": "Generating...",
"CANCEL": "إلغاء"
},
"GENERATE_ERROR": "There was an error processing the content, please try again"
},
"DELETE": {
"BUTTON_TEXT": "حذف",
"API": {
@@ -3,16 +3,16 @@
"TITLE": "تسجيل الدخول إلى Chatwoot",
"EMAIL": {
"LABEL": "البريد الإلكتروني",
"PLACEHOLDER": "مثال: someone@example.com"
"PLACEHOLDER": "example@companyname.com"
},
"PASSWORD": {
"LABEL": "كلمة المرور",
"PLACEHOLDER": "كلمة المرور"
},
"API": {
"SUCCESS_MESSAGE": "تم تسجيل الدخول بنجاح",
"ERROR_MESSAGE": "تعذر الاتصال بالخادم، الرجاء المحاولة مرة أخرى لاحقاً",
"UNAUTH": "اسم المستخدم / كلمة المرور غير صحيحة. الرجاء المحاولة مرة أخرى"
"SUCCESS_MESSAGE": "Login successful",
"ERROR_MESSAGE": "Could not connect to Woot server. Please try again.",
"UNAUTH": "Username or password is incorrect. Please try again."
},
"OAUTH": {
"GOOGLE_LOGIN": "Login with Google",
@@ -3,12 +3,12 @@
"TITLE": "إعادة تعيين كلمة المرور",
"EMAIL": {
"LABEL": "البريد الإلكتروني",
"PLACEHOLDER": "الرجاء إدخال بريدك الإلكتروني",
"ERROR": "الرجاء إدخال بريد إلكتروني صالح"
"PLACEHOLDER": "الرجاء إدخال بريدك الإلكتروني.",
"ERROR": "الرجاء إدخال بريد إلكتروني صالح."
},
"API": {
"SUCCESS_MESSAGE": "تم إرسال رابط إعادة تعيين كلمة المرور إلى بريدك الإلكتروني",
"ERROR_MESSAGE": "تعذر الاتصال بالخادم، الرجاء المحاولة مرة أخرى لاحقاً"
"SUCCESS_MESSAGE": "تم إرسال رابط إعادة تعيين كلمة المرور إلى بريدك الإلكتروني.",
"ERROR_MESSAGE": "Could not connect to Woot server. Please try again."
},
"SUBMIT": "إرسال"
}
@@ -14,8 +14,8 @@
"EMPTY_STATE": "No %{item} found for query '%{query}'",
"EMPTY_STATE_FULL": "No results found for query '%{query}'",
"PLACEHOLDER_KEYBINDING": "/ to focus",
"INPUT_PLACEHOLDER": "Search messages, contacts or conversations",
"EMPTY_STATE_DEFAULT": "Search by conversation id, email, phone number, messages for better search results.",
"INPUT_PLACEHOLDER": "Type 3 or more characters to search",
"EMPTY_STATE_DEFAULT": "Search by conversation id, email, phone number, messages for better search results. ",
"BOT_LABEL": "رد آلي",
"READ_MORE": "Read more",
"WROTE": "wrote:"
@@ -1,19 +1,19 @@
{
"SET_NEW_PASSWORD": {
"TITLE": "تعيين كلمة مرور جديدة",
"TITLE": "Set new password",
"PASSWORD": {
"LABEL": "كلمة المرور",
"PLACEHOLDER": "كلمة المرور",
"ERROR": "كلمة المرور قصيرة جداً"
"ERROR": "Password is too short."
},
"CONFIRM_PASSWORD": {
"LABEL": "تأكيد كلمة المرور",
"LABEL": "Confirm password",
"PLACEHOLDER": "تأكيد كلمة المرور",
"ERROR": "كلمة المرور غير متطابقة"
"ERROR": "كلمة المرور غير متطابقة."
},
"API": {
"SUCCESS_MESSAGE": "تم تغيير كلمة المرور بنجاح",
"ERROR_MESSAGE": "تعذر الاتصال بالخادم، الرجاء المحاولة مرة أخرى لاحقاً"
"SUCCESS_MESSAGE": "تم تغيير كلمة المرور بنجاح.",
"ERROR_MESSAGE": "Could not connect to Woot server. Please try again."
},
"CAPTCHA": {
"ERROR": "انتهت صلاحية التحقق. الرجاء حل كلمة التحقق مرة أخرى."
@@ -10,33 +10,33 @@
},
"COMPANY_NAME": {
"LABEL": "Company name",
"PLACEHOLDER": "Enter your company name. eg: Wayne Enterprises",
"ERROR": "Company name is too short"
"PLACEHOLDER": "Enter your company name. E.g., Wayne Enterprises",
"ERROR": "Company name is too short."
},
"FULL_NAME": {
"LABEL": "الاسم الكامل",
"PLACEHOLDER": "أدخل اسمك الكامل. مثال: بروس وين",
"ERROR": "الاسم الكامل قصير جداً"
"PLACEHOLDER": "Enter your full name. E.g., Bruce Wayne",
"ERROR": "Full name is too short."
},
"EMAIL": {
"LABEL": "البريد الإلكتروني للعمل",
"PLACEHOLDER": "أدخل عنوان بريدك الإلكتروني للعمل. مثال: bruce@wayne.enterprises",
"ERROR": "Please enter a valid work email address"
"PLACEHOLDER": "Enter your work email address. E.g., bruce@wayne.enterprises",
"ERROR": "Please enter a valid work email address."
},
"PASSWORD": {
"LABEL": "كلمة المرور",
"PLACEHOLDER": "كلمة المرور",
"ERROR": "كلمة المرور قصيرة جداً",
"IS_INVALID_PASSWORD": "يجب أن تحتوي كلمة المرور على الأقل على حرف كبير واحد وحرف صغير واحد ورقم واحد وحرف خاص واحد"
"ERROR": "Password is too short.",
"IS_INVALID_PASSWORD": "يجب أن تحتوي كلمة المرور على الأقل على حرف كبير واحد وحرف صغير واحد ورقم واحد وحرف خاص واحد."
},
"CONFIRM_PASSWORD": {
"LABEL": "تأكيد كلمة المرور",
"PLACEHOLDER": "تأكيد كلمة المرور",
"ERROR": "كلمة المرور غير متطابقة"
"LABEL": "Confirm password",
"PLACEHOLDER": "Confirm password",
"ERROR": "كلمة المرور غير متطابقة."
},
"API": {
"SUCCESS_MESSAGE": "تم التسجيل بنجاح",
"ERROR_MESSAGE": "تعذر الاتصال بالخادم، الرجاء المحاولة مرة أخرى لاحقاً"
"ERROR_MESSAGE": "Could not connect to Woot server. Please try again."
},
"SUBMIT": "إرسال",
"HAVE_AN_ACCOUNT": "هل لديك حساب مسبق؟"
@@ -38,6 +38,7 @@
"CAMPAIGN_NAME": "Име на кампания",
"LABELS": "Етикети",
"BROWSER_LANGUAGE": "Език на браузъра",
"PRIORITY": "Priority",
"COUNTRY_NAME": "Име на държавата",
"REFERER_LINK": "Референтна връзка",
"CUSTOM_ATTRIBUTE_LIST": "List",
@@ -110,7 +110,8 @@
},
"PLACEHOLDER": {
"AGENT": "Търсете агенти",
"TEAM": "Търсете екипи"
"TEAM": "Търсете екипи",
"INPUT": "Search for agents"
}
}
}
@@ -35,6 +35,20 @@
"TEXT": "Всички"
}
},
"VIEW_FILTER": "View",
"SORT_TOOLTIP_LABEL": "Sort conversations",
"CHAT_SORT": {
"STATUS": "Статус",
"ORDER_BY": "Order by"
},
"CHAT_SORT_FILTER_ITEMS": {
"latest": {
"TEXT": "Last activity"
},
"sort_on_created_at": {
"TEXT": "Създаден в"
}
},
"ATTACHMENTS": {
"image": {
"CONTENT": "Съобщение със снимка"
@@ -55,6 +69,24 @@
"CONTENT": "сподели линк"
}
},
"CHAT_SORT_BY_FILTER": {
"TITLE": "Sort conversation",
"DROPDOWN_TITLE": "Sort by",
"ITEMS": {
"LATEST": {
"NAME": "Last activity at",
"LABEL": "Last activity"
},
"CREATED_AT": {
"NAME": "Създаден в",
"LABEL": "Създаден в"
},
"LAST_USER_MESSAGE_AT": {
"NAME": "Last user message at",
"LABEL": "Last message"
}
}
},
"RECEIVED_VIA_EMAIL": "Получено чрез имейл",
"VIEW_TWEET_IN_TWITTER": "Виж туита в Twitter",
"REPLY_TO_TWEET": "Отговори на този туит",

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