Compare commits

...
67 Commits
Author SHA1 Message Date
0bbb28c432 feat: sort conversation on priority (#6943)
* feat: update seed script to include prioritt

* feat: add sort_handler for conversations

* test: sort on priority order

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2023-04-25 09:47:47 +05:30
Sojan JoseandGitHub cf91e9eb58 chore: Use find_each instead of .all.each (#6975)
- Enable the rubocop Rails/FindEach
- Replace the .all.each with .find_each

This should let us avoid potential memory usage.
Motivation from the speedshop newsletter by Nate Berkopec

ref: https://www.rubyinrails.com/2017/11/16/use-find-each-instead-of-all-each-in-rails/
ref: https://linear.app/chatwoot/issue/CW-1480/chore-run-all-sidekiq-jobs-async
2023-04-25 09:32:35 +05:30
b529baa5eb fix: Pagination bug in chat list (#6899)
* fix: Pagination bug in chat list

* chore: Review fixes

* Improves variable namings

---------

Co-authored-by: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com>
2023-04-25 09:01:54 +05:30
Pranav Raj SandGitHub c071f66cdd fix: Update the icon for low priority (#6978) 2023-04-24 13:52:14 -07:00
92fa9c4fdc feat: Ability to improve drafts in the editor using GPT integration (#6957)
ref: https://github.com/chatwoot/chatwoot/issues/6436
fixes: https://linear.app/chatwoot/issue/CW-1552/ability-to-rephrase-text-in-the-editor-using-gpt-integration

---------

Co-authored-by: Sojan <sojan@pepalo.com>
Co-authored-by: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com>
2023-04-24 23:52:23 +05:30
f6e0453bb2 fix vertical overflow scroll on BackButton (#6914)
* fix: vertical overflow scroll on BackButton

Co-authored-by: raph941 <45232708+raph941@users.noreply.github.com>

* chore: adds suggested improvement to handle RTS view

Co-authored-by: raph941 <45232708+raph941@users.noreply.github.com>

---------

Co-authored-by: raph941 <45232708+raph941@users.noreply.github.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2023-04-24 22:57:58 +05:30
Pranav Raj SandGitHub 1a07828b1b fix: Disable the entire element if priority is missing (#6972) 2023-04-24 10:21:39 -07:00
Shivam MishraandGitHub 54880b2342 feat: Track conversation priority events (#6968) 2023-04-24 20:21:43 +05:30
Muhsin KelothandGitHub e32f3e71e4 feat: Right click context menu action to change the priority (#6947)
* Right click context menu action to change the priority

* Review comments

* Update Index.vue

* Remove selected priority  from menu

* Code cleanup

* Update conversation.json
2023-04-24 20:07:50 +05:30
Shivam MishraandGitHub 0874aeee2d feat: priority UI (#6966) 2023-04-24 19:00:08 +05:30
Muhsin KelothandGitHub f1fc658a0d feat: Add an action in Macro to change the priority (#6940) 2023-04-24 17:14:30 +05:30
Tejaswini ChileandGitHub e3193dcabc feat: Link help center portal to an Inbox (#6903) 2023-04-24 12:49:52 +05:30
Vishnu NarayananandGitHub f825a22997 feat: add Makefile (#6948)
* feat: add  Makefile
* chore: refactor
2023-04-24 11:32:17 +05:30
Muhsin KelothandGitHub 815322b27a feat: Refetch the active conversation messages on action cable reconnect (#6790) 2023-04-24 10:17:12 +05:30
Pranav Raj SandGitHub 474e65f4c8 feat: Save in_reply_to from WhatsApp messages (#6964) 2023-04-23 18:28:14 -07:00
3a35281b3f chore: Update translations (#6941)
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2023-04-21 15:28:56 +05:30
Muhsin KelothandGitHub ac6de50b4d feat: Add a condition for filters based on the priority of automation (#6939) 2023-04-20 18:10:05 +05:30
a34729c153 feat: add activity message for priority change (#6933)
* feat: add priority const

* feat: add toggle priority method

* feat: update controller route and specs

* refactor: status change method

* refactor: abstract label change and mute activity

* feat: add priority change_activity

* fix: interpolation for previous_changes

* refactor: reduce cognitive complexity of priority_change_activity

* refactor: move priority activity message handler to a separate module

* refactor: move typing logic to a service

* refactor: tests to reduce complexity

* fix: typo

* fix: constants

* fix: priority conditions

* fix: add a response

* fix: argument destructuring in I18n.t

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2023-04-20 16:41:53 +05:30
6b2736aa63 fix: inconsistency in report and summary for metric counts (#6817)
* feat: include timezone offset in summary calculation

* fix: exlcude end in date range

* test: explicit end of day

* fix: test for report builder

* fix: reports.spec.js

---------

Co-authored-by: Tejaswini Chile <tejaswini@chatwoot.com>
2023-04-20 12:55:04 +05:30
Muhsin KelothandGitHub d1584eea72 feat: Add an action on automation to change the priority (#6925) 2023-04-20 11:31:33 +05:30
Sivin VargheseandGitHub 527042afd1 fix: Code fix from PR #6823 (#6934) 2023-04-19 23:09:37 +05:30
Nithin David ThomasandGitHub bd1e69e4b4 feat: Adds new inbox selector with more info for new message modal [cw-1358] (#6823) 2023-04-19 23:02:50 +05:30
Vishnu NarayananandGitHub 76d4c22c2d fix: build_id in heroku installations (#6932) 2023-04-19 21:39:10 +05:30
Tejaswini ChileandGitHub 5cdc7d654a feat: Support multiple emails in email transcript automation (#6924) 2023-04-19 16:55:25 +05:30
Tejaswini ChileandGitHub 821d49943a Delete inbox api doc update (#6930) 2023-04-19 16:43:51 +05:30
Shivam MishraandGitHub 9c0259da6b feat: add priority field to conversation (#6921) (#6927)
* feat: add priority

* feat: add indexes
2023-04-19 13:23:14 +05:30
Pranav Raj SandGitHub ea2c442328 chore: Add account_id as custom_attribute for cloud users (#6926) 2023-04-18 22:35:11 -07:00
Volodymyr MakukhandGitHub b93b8cdab6 Fix flaky reporting_event_listener_spec (#6923) 2023-04-19 10:42:45 +05:30
Pranav Raj SandGitHub e6505fc7a4 chore: Cache the dashboard app on the first load (#6774) 2023-04-18 19:44:57 -07:00
Pranav Raj SandGitHub c5c36af529 fix: Remove fallback phone_number search in WhatsApp event processing (#6904) 2023-04-18 18:30:01 -07:00
026e03c307 fix: Handle spaces in CC/BCC email lists (#6788)
When the CC field is generated in the UI, the email values are joined together
with ", " but when they are parsed, we currently split by just ",".

This causes an error on the backend and on the frontend.

It seems reasonable to update the code to allow whitespace in the input and to
split by `\s*,\s` and also to trim leading and trailing whitespace from the CC
list.

---------

Co-authored-by: Sojan <sojan@pepalo.com>
2023-04-18 19:18:23 +05:30
Sojan 37b7098673 Merge branch 'release/2.16.0' into develop 2023-04-18 01:04:27 +05:30
Sojan cb2f86b983 Bump version to 2.16.0 2023-04-18 01:02:19 +05:30
905e77048f chore: Contact import improvements (CW-1362) (#6747)
Fixes: https://linear.app/chatwoot/issue/CW-1362/csv-imports-are-not-working-properly
Fixes: #3462

---------

Co-authored-by: Sojan <sojan@pepalo.com>
2023-04-18 00:40:55 +05:30
Vishnu NarayananandGitHub 4505c5dda3 chore: add build id to settings page (#6873)
- Adds a build Id to the settings page
2023-04-18 00:35:35 +05:30
Sivin VargheseGitHubMuhsin Keloth <muhsinkeramam@gmail.com>, Nithin David
c9ce9e5b8f feat: Improved country code in contact form view. (#6801)
* feat: Improved country code in contact.

* chore: Minor fixes

* chore: Minor fixes

* chore: Adds arrow key navigation and cursor pointer

* chore: Minor fix

* chore: Code clean up

* chore: Handle outside click

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>, Nithin David
2023-04-17 20:32:09 +05:30
9e2f991484 feat: audit logs UI (#6803)
* feat: init auditlogs ui

* chore: add api

* fix: action

* chore: add action,username,time

* feat: add pagination support

* chore: format time

* chore: refactor

* chore: refactor auditlogs api response

* chore: update icon

* chore: rubocop fixes

* Fixes the way meta is handled in store

* Fixes meta not appearing issue

---------

Co-authored-by: Sojan Jose <sojan@pepalo.com>
Co-authored-by: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com>
2023-04-17 19:11:05 +05:30
Sivin VargheseandGitHub 80dcd17f6e fix: Border color for CSAT component in widget (#6915) 2023-04-17 17:50:30 +05:30
Shivam MishraandGitHub a38ecf3dde feat: show webhook verify token for WhatsApp (#6916)
- Display WhatsApp webhook verify token in configuration settings
2023-04-17 17:14:08 +05:30
9090eabb8a chore: Update translations (#6895)
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2023-04-17 14:45:49 +05:30
ca2506a941 feat: allow sorting of articles (#6833)
* feat: sort by position

* chore: whitespace change

* feat: add border bottom color to list item

* feat: allow dragging articles

* feat: add migration to reorder all articles

* feat: add onsort method

* feat: finish UI sorting

* feat: show 50 per page in articles list

* feat: add article sorting methods

* feat: patch up reorder action with the API

* refactor: better naming

* chore: add comments

* feat: attach position to article before create

* feat: move article to end if moved between categories

* chore: add comments

* chore: update version

* fix: don't change position if previous category was nil

* fix: condition to trigger update on category change

* refactor: store new_position

* refactor: use grid instead of table

* feat: add snug spacing

* feat: add grab-icon

* feat: add grab icon to list

* refactor: show draggable only for category page

* feat: add update_positions as a class method

---------

Co-authored-by: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com>
2023-04-17 14:43:10 +05:30
Shivam MishraandGitHub 1886d4ce08 fix: response body in twitter callback (#6907)
* fix: response body

* fix: tests
2023-04-14 16:48:28 +05:30
Shivam MishraandGitHub 4d49b81f1c fix: use response.status instead of success (#6906)
* fix: use response.status instead of success

* refactor: use sentry to capture exception

* refactor: explicitly convert to i
2023-04-14 15:22:08 +05:30
Tejaswini ChileandGitHub 2b736f4698 fix: Update from_email in the name to fix the syntax error (#6900) 2023-04-13 10:42:38 -07:00
Tejaswini ChileandGitHub 9ca21df9fd feat: Route emails based on x-original-to in email channel (#6901)
Fixes: #6608
ref: https://linear.app/chatwoot/issue/CW-30/emails-not-routed-based-on-x-original-to
2023-04-13 14:52:12 +05:30
Tejaswini ChileandGitHub 44837aa657 Fix: save twitter profile for inbox (#6667)
Fixes: #737
2023-04-12 14:16:24 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Sojan Jose
d04344c094 chore(deps): bump commonmarker from 0.23.7 to 0.23.9 (#6892)
Bumps [commonmarker](https://github.com/gjtorikian/commonmarker) from 0.23.7 to 0.23.9.
- [Release notes](https://github.com/gjtorikian/commonmarker/releases)
- [Changelog](https://github.com/gjtorikian/commonmarker/blob/main/CHANGELOG.md)
- [Commits](https://github.com/gjtorikian/commonmarker/compare/v0.23.7...v0.23.9)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2023-04-12 14:08:14 +05:30
d45512df72 feat: Account deletion with deleteObjectJob (#6885)
Fixes: https://linear.app/chatwoot/issue/CW-1365/allow-super-admin-to-delete-an-account

Co-authored-by: Sojan Jose <sojan@pepalo.com>
2023-04-12 13:54:01 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2731c2a5be chore(deps): bump nokogiri from 1.14.2 to 1.14.3 (#6893)
Bumps [nokogiri](https://github.com/sparklemotion/nokogiri) from 1.14.2 to 1.14.3.
- [Release notes](https://github.com/sparklemotion/nokogiri/releases)
- [Changelog](https://github.com/sparklemotion/nokogiri/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sparklemotion/nokogiri/compare/v1.14.2...v1.14.3)

---
updated-dependencies:
- dependency-name: nokogiri
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-12 13:29:57 +05:30
Tejaswini ChileandGitHub 610463c980 fix: flatten template array for whatsapp templates (#6880)
- Refactor the WhatsApp template sync job
- Fix the issue when fetching the next set of templates
2023-04-11 17:48:17 +05:30
09ce85b30d Chore: moves localstorage helper as a shared utility (#6838)
* Chore: moves localstorage helper as a shared utility and refactors constants

* Refactors constants file

* Fixes merge conflicts

* Delete constants.js

---------

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2023-04-11 15:50:46 +05:30
Shivam MishraandGitHub 17ff1f11a7 feat: better download for conversation traffic heatmap (#6755)
* feat: genearte report in a grid

* refactor: update API usage

* refactor: separate generate method

* refactor: abstract transform_data

* feat: annotate with comments

* feat: add explicit timezone

* feat: download data only in user timezone

* fix: dates included in heatmap
2023-04-11 09:40:54 +05:30
Pranav Raj SandGitHub f4e121cc44 fix: Update the profile API URL in the documentation (#6875) 2023-04-10 18:23:20 -07:00
Pranav Raj SandGitHub cf934450ab chore: Remove context menu on links (#6874) 2023-04-10 14:13:08 -07:00
Vishnu NarayananandGitHub ad75a79135 feat: add pagination support for audit logs API (#6843)
Add pagination support for audit logs API
2023-04-10 21:07:01 +05:30
Tejaswini ChileandGitHub d49989ace1 chore: Specs for Whatsapp template pagination (#6870)
Spec for https://github.com/chatwoot/chatwoot/pull/6835
2023-04-10 20:44:43 +05:30
Tejaswini ChileandGitHub e69e0bc984 Add status reopen activity message for api channel (#6839) 2023-04-10 19:12:20 +05:30
e877b01e00 fix: Invalidate cache if any of the related objects change (#6860)
* fix: Invalidate cache if any of the related objects change

* Add specs:

* Update context

---------

Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
2023-04-10 15:47:13 +05:30
da11feb39b feat: one-click codepen for widget configuration (#6865)
* feat: allow codepen in code

* feat: enable codepen in config

* feat: update codepen title

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2023-04-10 15:44:06 +05:30
Tejaswini ChileandGitHub 138afd9af6 Feat: Fetch whatsapp templates till next cursor (#6835) 2023-04-10 14:44:27 +05:30
e753365493 feat: Add the ability to change the agent availability status (#6855)
* Add the option change agent availability

* Remove callout

* Move `AVAILABILITY_STATUS_KEYS` to constants

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

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

---------

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2023-04-10 13:37:12 +05:30
Pranav Raj SandGitHub 91dc7733b0 feat: Use inbox image as avatar for the bot (#6859) 2023-04-07 13:25:18 -07:00
Pranav Raj SandGitHub 463c09184c fix: Disable processing events if account is suspended (#6849) 2023-04-07 12:01:03 -07:00
040e9a732f chore: Update translations (#6854)
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2023-04-07 16:05:33 +05:30
71c5a1e1d4 feat: add lograge to improve logging (#5423)
- Add lograge gem to improve rails logging using `LOGRAGE_ENABLED` env variable
- When enabled Single line log for requests in JSON formatting
- Switch sidekiq also to use JSON formatting

Fixes: chatwoot/product#437
---------

Co-authored-by: Sojan Jose <sojan@pepalo.com>
2023-04-07 13:44:30 +05:30
Muhsin KelothandGitHub a521762dd6 feat: Support after param in messages end point (#6848)
Adds support to `after` param while fetching messages
Fixes: https://linear.app/chatwoot/issue/CW-1475/support-after-param-in-messages-end-point
2023-04-07 13:42:54 +05:30
406e8405eb fix: Specs failing for teams/actions.js (#6845)
* fix: specs failing

* fix: specs for labels and inboxes

* Update reports.js

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2023-04-06 20:48:49 +05:30
571 changed files with 9241 additions and 2251 deletions
-2
View File
@@ -1,8 +1,6 @@
.bundle
.env
.env.*
.git
.gitignore
docker-compose.*
docker/Dockerfile
docker/dockerfiles
+2 -3
View File
@@ -110,6 +110,8 @@ AWS_REGION=
RAILS_LOG_TO_STDOUT=true
LOG_LEVEL=info
LOG_SIZE=500
# Configure this environment variable if you want to use lograge instead of rails logger
#LOGRAGE_ENABLED=true
### This environment variables are only required if you are setting up social media channels
@@ -159,9 +161,6 @@ ANDROID_SHA256_CERT_FINGERPRINT=AC:73:8E:DE:EB:56:EA:CC:10:87:02:A7:65:37:7B:38:
# for mobile apps
# FCM_SERVER_KEY=
## Bot Customizations
USE_INBOX_AVATAR_FOR_BOT=true
### APM and Error Monitoring configurations
## Elastic APM
## https://www.elastic.co/guide/en/apm/agent/ruby/current/getting-started-rails.html
+3 -2
View File
@@ -61,8 +61,9 @@ test/cypress/videos/*
/config/master.key
/config/*.enc
.vscode/settings.json
#ignore files under .vscode directory
.vscode
# yalc for local testing
.yalc
yalc.lock
yalc.lock
+4
View File
@@ -71,6 +71,10 @@ Rails/ApplicationController:
- 'app/controllers/platform_controller.rb'
- 'app/controllers/public_controller.rb'
- 'app/controllers/survey/responses_controller.rb'
Rails/FindEach:
Enabled: true
Include:
- 'app/**/*.rb'
Rails/CompactBlank:
Enabled: false
Rails/EnvironmentVariableAccess:
+18 -12
View File
@@ -87,7 +87,7 @@ gem 'line-bot-api'
gem 'twilio-ruby', '~> 5.66'
# twitty will handle subscription of twitter account events
# gem 'twitty', git: 'https://github.com/chatwoot/twitty'
gem 'twitty'
gem 'twitty', '~> 0.1.5'
# facebook client
gem 'koala'
# slack client
@@ -149,7 +149,23 @@ gem 'net-imap', require: false
gem 'net-pop', require: false
gem 'net-smtp', require: false
group :production, :staging do
# 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'
# need for google auth
gem 'omniauth'
gem 'omniauth-google-oauth2'
gem 'omniauth-rails_csrf_protection', '~> 1.0'
### Gems required only in specific deployment environments ###
##############################################################
group :production do
# we dont want request timing out in development while using byebug
gem 'rack-timeout'
end
@@ -206,13 +222,3 @@ group :development, :test do
gem 'spring'
gem 'spring-watcher-listen'
end
# worked with microsoft refresh token
gem 'omniauth-oauth2'
gem 'audited', '~> 5.2'
# need for google auth
gem 'omniauth'
gem 'omniauth-google-oauth2'
gem 'omniauth-rails_csrf_protection', '~> 1.0'
+15 -7
View File
@@ -137,7 +137,7 @@ GEM
byebug (11.1.3)
climate_control (1.1.1)
coderay (1.1.3)
commonmarker (0.23.7)
commonmarker (0.23.9)
concurrent-ruby (1.2.2)
connection_pool (2.2.5)
crack (0.4.5)
@@ -416,6 +416,11 @@ GEM
llhttp-ffi (0.4.0)
ffi-compiler (~> 1.0)
rake (~> 13.0)
lograge (0.12.0)
actionpack (>= 4)
activesupport (>= 4)
railties (>= 4)
request_store (~> 1.0)
loofah (2.19.1)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
@@ -459,14 +464,14 @@ GEM
sidekiq
newrelic_rpm (8.15.0)
nio4r (2.5.8)
nokogiri (1.14.2)
nokogiri (1.14.3)
mini_portile2 (~> 2.8.0)
racc (~> 1.4)
nokogiri (1.14.2-arm64-darwin)
nokogiri (1.14.3-arm64-darwin)
racc (~> 1.4)
nokogiri (1.14.2-x86_64-darwin)
nokogiri (1.14.3-x86_64-darwin)
racc (~> 1.4)
nokogiri (1.14.2-x86_64-linux)
nokogiri (1.14.3-x86_64-linux)
racc (~> 1.4)
oauth (0.5.10)
oauth2 (2.0.9)
@@ -568,6 +573,8 @@ GEM
declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0)
request_store (1.5.1)
rack (>= 1.4)
responders (3.0.1)
actionpack (>= 5.0)
railties (>= 5.0)
@@ -710,7 +717,7 @@ GEM
faraday (>= 0.9, < 3.0)
jwt (>= 1.5, <= 2.5)
nokogiri (>= 1.6, < 2.0)
twitty (0.1.4)
twitty (0.1.5)
oauth
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
@@ -824,6 +831,7 @@ DEPENDENCIES
line-bot-api
liquid
listen
lograge (~> 0.12.0)
maxminddb
mock_redis
net-imap
@@ -875,7 +883,7 @@ DEPENDENCIES
test-prof
time_diff
twilio-ruby (~> 5.66)
twitty
twitty (~> 0.1.5)
tzinfo-data
uglifier
valid_email2
+38
View File
@@ -0,0 +1,38 @@
# Variables
APP_NAME := chatwoot
RAILS_ENV ?= development
# Targets
setup:
gem install bundler
bundle install
yarn install
db_create:
RAILS_ENV=$(RAILS_ENV) bundle exec rails db:create
db_migrate:
RAILS_ENV=$(RAILS_ENV) bundle exec rails db:migrate
db_seed:
RAILS_ENV=$(RAILS_ENV) bundle exec rails db:seed
db:
RAILS_ENV=$(RAILS_ENV) bundle exec rails db:chatwoot_prepare
console:
RAILS_ENV=$(RAILS_ENV) bundle exec rails console
server:
RAILS_ENV=$(RAILS_ENV) bundle exec rails server -b 0.0.0.0 -p 3000
burn:
bundle && yarn
run:
overmind start -f Procfile.dev
docker:
docker build -t $(APP_NAME) -f ./docker/Dockerfile .
.PHONY: setup db_create db_migrate db_seed db console server burn docker run
+1 -1
View File
@@ -1,3 +1,3 @@
release: POSTGRES_STATEMENT_TIMEOUT=600s bundle exec rails db:chatwoot_prepare
release: POSTGRES_STATEMENT_TIMEOUT=600s bundle exec rails db:chatwoot_prepare && echo $SOURCE_VERSION > .git_sha
web: bundle exec rails ip_lookup:setup && bin/rails server -p $PORT -e $RAILS_ENV
worker: bundle exec rails ip_lookup:setup && bundle exec sidekiq -C config/sidekiq.yml
+2 -2
View File
@@ -49,10 +49,10 @@ class Messages::MessageBuilder
return unless @conversation.inbox&.inbox_type == 'Email'
cc_emails = []
cc_emails = @params[:cc_emails].split(',') if @params[:cc_emails].present?
cc_emails = @params[:cc_emails].gsub(/\s+/, '').split(',') if @params[:cc_emails].present?
bcc_emails = []
bcc_emails = @params[:bcc_emails].split(',') if @params[:bcc_emails].present?
bcc_emails = @params[:bcc_emails].gsub(/\s+/, '').split(',') if @params[:bcc_emails].present?
all_email_addresses = cc_emails + bcc_emails
validate_email_addresses(all_email_addresses)
@@ -1,14 +1,19 @@
class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
before_action :portal
before_action :check_authorization
before_action :fetch_article, except: [:index, :create, :attach_file]
before_action :fetch_article, except: [:index, :create, :attach_file, :reorder]
before_action :set_current_page, only: [:index]
def index
@portal_articles = @portal.articles
@all_articles = @portal_articles.search(list_params)
@articles_count = @all_articles.count
@articles = @all_articles.order_by_updated_at.page(@current_page)
@articles = if list_params[:category_slug].present?
@all_articles.order_by_position.page(@current_page).per(50)
else
@all_articles.order_by_updated_at.page(@current_page)
end
end
def create
@@ -43,6 +48,11 @@ class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
render json: { file_url: url_for(file_blob) }
end
def reorder
Article.update_positions(params[:positions_hash])
head :ok
end
private
def fetch_article
@@ -64,13 +64,14 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
assign_conversation if @conversation.status == 'open' && Current.user.is_a?(User) && Current.user&.agent?
end
def toggle_priority
@conversation.toggle_priority(params[:priority])
head :ok
end
def toggle_typing_status
case params[:typing_status]
when 'on'
trigger_typing_event(CONVERSATION_TYPING_ON, params[:is_private])
when 'off'
trigger_typing_event(CONVERSATION_TYPING_OFF, params[:is_private])
end
typing_status_manager = ::Conversations::TypingStatusManager.new(@conversation, current_user, params)
typing_status_manager.toggle_typing_status
head :ok
end
@@ -111,11 +112,6 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
@conversation.update_assignee(@agent)
end
def trigger_typing_event(event, is_private)
user = current_user.presence || @resource
Rails.configuration.dispatcher.dispatch(event, Time.zone.now, conversation: @conversation, user: user, is_private: is_private)
end
def conversation
@conversation ||= Current.account.conversations.find_by!(display_id: params[:id])
authorize @conversation.inbox, :show?
@@ -114,10 +114,13 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
def inbox_attributes
[:name, :avatar, :greeting_enabled, :greeting_message, :enable_email_collect, :csat_survey_enabled,
:enable_auto_assignment, :working_hours_enabled, :out_of_office_message, :timezone, :allow_messages_after_resolved,
:lock_to_single_conversation]
:lock_to_single_conversation, :portal_id]
end
def permitted_params(channel_attributes = [])
# We will remove this line after fixing https://linear.app/chatwoot/issue/CW-1567/null-value-passed-as-null-string-to-backend
params.each { |k, v| params[k] = params[k] == 'null' ? nil : v }
params.permit(
*inbox_attributes,
channel: [:type, *channel_attributes]
@@ -1,5 +1,5 @@
class Api::V1::Accounts::Integrations::HooksController < Api::V1::Accounts::BaseController
before_action :fetch_hook, only: [:update, :destroy]
before_action :fetch_hook, except: [:create]
before_action :check_authorization
def create
@@ -10,6 +10,10 @@ class Api::V1::Accounts::Integrations::HooksController < Api::V1::Accounts::Base
@hook.update!(permitted_params.slice(:status, :settings))
end
def process_event
render json: { message: @hook.process_event(params[:event]) }
end
def destroy
@hook.destroy!
head :ok
@@ -1,5 +1,7 @@
class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
include Api::V2::Accounts::ReportsHelper
include Api::V2::Accounts::HeatmapHelper
before_action :check_authorization
def index
@@ -34,6 +36,9 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
def conversation_traffic
@report_data = generate_conversations_heatmap_report
timezone_offset = (params[:timezone_offset] || 0).to_f
@timezone = ActiveSupport::TimeZone[timezone_offset]
generate_csv('conversation_traffic_reports', 'api/v2/accounts/reports/conversation_traffic')
end
@@ -67,14 +72,16 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
def current_summary_params
common_params.merge({
since: range[:current][:since],
until: range[:current][:until]
until: range[:current][:until],
timezone_offset: params[:timezone_offset]
})
end
def previous_summary_params
common_params.merge({
since: range[:previous][:since],
until: range[:previous][:until]
until: range[:previous][:until],
timezone_offset: params[:timezone_offset]
})
end
+2 -1
View File
@@ -56,7 +56,8 @@ class DashboardController < ActionController::Base
FB_APP_ID: GlobalConfigService.load('FB_APP_ID', ''),
FACEBOOK_API_VERSION: 'v14.0',
IS_ENTERPRISE: ChatwootApp.enterprise?,
AZURE_APP_ID: ENV.fetch('AZURE_APP_ID', '')
AZURE_APP_ID: ENV.fetch('AZURE_APP_ID', ''),
GIT_SHA: GIT_HASH
}
end
end
@@ -47,4 +47,11 @@ class SuperAdmin::AccountsController < SuperAdmin::ApplicationController
Internal::SeedAccountJob.perform_later(requested_resource)
redirect_back(fallback_location: [namespace, requested_resource], notice: 'Account seeding triggered')
end
def destroy
account = Account.find(params[:id])
DeleteObjectJob.perform_later(account) if account.present?
redirect_back(fallback_location: [namespace, requested_resource], notice: 'Account deletion is in progress.')
end
end
@@ -14,7 +14,7 @@ class Twitter::CallbacksController < Twitter::BaseController
redirect_to app_twitter_inbox_agents_url(account_id: account.id, inbox_id: inbox.id)
end
rescue StandardError => e
Rails.logger.error e
ChatwootExceptionTracker.new(e).capture_exception
redirect_to twitter_app_redirect_url
end
@@ -49,10 +49,22 @@ class Twitter::CallbacksController < Twitter::BaseController
twitter_access_token_secret: parsed_body['oauth_token_secret'],
profile_id: parsed_body['user_id']
)
account.inboxes.create!(
inbox = account.inboxes.create!(
name: parsed_body['screen_name'],
channel: twitter_profile
)
save_profile_image(inbox)
inbox
end
def save_profile_image(inbox)
response = twitter_client.user_show(screen_name: inbox.name)
return unless response.status.to_i == 200
parsed_user_profile = response.body
::Avatar::AvatarFromUrlJob.perform_later(inbox, parsed_user_profile['profile_image_url_https'])
end
def permitted_params
+7 -1
View File
@@ -7,12 +7,18 @@ class EmailChannelFinder
def perform
channel = nil
recipient_mails = @email_object.to.to_a + @email_object.cc.to_a
recipient_mails.each do |email|
normalized_email = normalize_email_with_plus_addressing(email)
channel = Channel::Email.find_by('lower(email) = ? OR lower(forward_to_email) = ?', normalized_email, normalized_email)
break if channel.present?
end
channel
end
def recipient_mails
recipient_addresses = @email_object.to.to_a + @email_object.cc.to_a + @email_object.bcc.to_a + [@email_object['X-Original-To'].try(:value)]
recipient_addresses.flatten.compact
end
end
+21 -3
View File
@@ -22,11 +22,29 @@ class MessageFinder
def current_messages
if @params[:after].present? && @params[:before].present?
messages.reorder('created_at asc').where('id >= ? AND id < ?', @params[:after].to_i, @params[:before].to_i).limit(1000)
messages_between(@params[:after].to_i, @params[:before].to_i)
elsif @params[:before].present?
messages.reorder('created_at desc').where('id < ?', @params[:before].to_i).limit(20).reverse
messages_before(@params[:before].to_i)
elsif @params[:after].present?
messages_after(@params[:after].to_i)
else
messages.reorder('created_at desc').limit(20).reverse
messages_latest
end
end
def messages_after(after_id)
messages.reorder('created_at asc').where('id > ?', after_id).limit(100)
end
def messages_before(before_id)
messages.reorder('created_at desc').where('id < ?', before_id).limit(20).reverse
end
def messages_between(after_id, before_id)
messages.reorder('created_at asc').where('id >= ? AND id < ?', after_id, before_id).limit(1000)
end
def messages_latest
messages.reorder('created_at desc').limit(20).reverse
end
end
@@ -0,0 +1,103 @@
module Api::V2::Accounts::HeatmapHelper
def generate_conversations_heatmap_report
timezone_data = generate_heatmap_data_for_timezone(params[:timezone_offset])
group_traffic_data(timezone_data)
end
private
def group_traffic_data(data)
# start with an empty array
result_arr = []
# pick all the unique dates from the data in ascending order
dates = data.pluck(:date).uniq.sort
# add the dates as the first row, leave an empty cell for the hour column
# e.g. [nil, '2023-01-01', '2023-1-02', '2023-01-03']
result_arr << ([nil] + dates)
# group the data by hour, we do not need to sort it, because the data is already sorted
# given it starts from the beginning of the day
# here each hour is a key, and the value is an array of all the items for that hour at each date
# e.g. hour = 1
# value = [{date: 2023-01-01, value: 1}, {date: 2023-01-02, value: 1}, {date: 2023-01-03, value: 1}, ...]
data.group_by { |d| d[:hour] }.each do |hour, items|
# create a new row for each hour
row = [hour]
# group the items by date, so we can easily access the value for each date
# grouped values will be a hasg with the date as the key, and the value as the value
# e.g. { '2023-01-01' => [{date: 2023-01-01, value: 1}], '2023-01-02' => [{date: 2023-01-02, value: 1}], ... }
grouped_values = items.group_by { |d| d[:date] }
# now for each unique date we have, we can access the value for that date and append it to the array
dates.each do |date|
row << (grouped_values[date][0][:value] if grouped_values[date].is_a?(Array))
end
# row will look like [22, 0, 0, 1, 4, 6, 7, 4]
# add the row to the result array
result_arr << row
end
# return the resultant array
# the result looks like this
# [
# [nil, '2023-01-01', '2023-1-02', '2023-01-03'],
# [0, 0, 0, 0],
# [1, 0, 0, 0],
# [2, 0, 0, 0],
# [3, 0, 0, 0],
# [4, 0, 0, 0],
# ]
result_arr
end
def generate_heatmap_data_for_timezone(offset)
timezone = ActiveSupport::TimeZone[offset]&.name
timezone_today = DateTime.now.in_time_zone(timezone).beginning_of_day
timezone_data_raw = generate_heatmap_data(timezone_today, offset)
transform_data(timezone_data_raw, false)
end
def generate_heatmap_data(date, offset)
report_params = {
type: :account,
group_by: 'hour',
metric: 'conversations_count',
business_hours: false
}
V2::ReportBuilder.new(Current.account, report_params.merge({
since: since_timestamp(date),
until: until_timestamp(date),
timezone_offset: offset
})).build
end
def transform_data(data, zone_transform)
# rubocop:disable Rails/TimeZone
data.map do |d|
date = zone_transform ? Time.zone.at(d[:timestamp]) : Time.at(d[:timestamp])
{
date: date.to_date.to_s,
hour: date.hour,
value: d[:value]
}
end
# rubocop:enable Rails/TimeZone
end
def since_timestamp(date)
(date - 6.days).to_i.to_s
end
def until_timestamp(date)
date.to_i.to_s
end
end
@@ -27,27 +27,6 @@ module Api::V2::Accounts::ReportsHelper
end
end
def generate_conversations_heatmap_report
report_params = {
type: :account,
group_by: 'hour',
since: params[:since],
until: params[:until],
metric: 'conversations_count',
business_hours: false
}
data = V2::ReportBuilder.new(Current.account, report_params).build
# data format is { timestamp: 1231242342, value: 3}
# we need to convert it to { date: "2020-01-01", hour: 12, value: 3}
#
# the generated report is **always** in UTC timezone
data.map do |d|
date = Time.zone.at(d[:timestamp]).to_s
[date, d[:value]]
end
end
def generate_report(report_params)
V2::ReportBuilder.new(
Current.account,
+1 -1
View File
@@ -7,7 +7,7 @@ module DateRangeHelper
def range
return if params[:since].blank? || params[:until].blank?
parse_date_time(params[:since])..parse_date_time(params[:until])
parse_date_time(params[:since])...parse_date_time(params[:until])
end
def parse_date_time(datetime)
+16
View File
@@ -0,0 +1,16 @@
/* global axios */
import ApiClient from './ApiClient';
class AuditLogs extends ApiClient {
constructor() {
super('audit_logs', { accountScoped: true });
}
get({ page }) {
const url = page ? `${this.url}?page=${page}` : this.url;
return axios.get(url);
}
}
export default new AuditLogs();
@@ -60,6 +60,13 @@ class ArticlesAPI extends PortalsAPI {
}
);
}
reorderArticles({ portalSlug, reorderedGroup, categorySlug }) {
return axios.post(`${this.url}/${portalSlug}/articles/reorder`, {
positions_hash: reorderedGroup,
category_slug: categorySlug,
});
}
}
export default new ArticlesAPI();
@@ -52,6 +52,12 @@ class ConversationApi extends ApiClient {
});
}
togglePriority({ conversationId, priority }) {
return axios.post(`${this.url}/${conversationId}/toggle_priority`, {
priority,
});
}
assignAgent({ conversationId, agentId }) {
return axios.post(
`${this.url}/${conversationId}/assignments?assignee_id=${agentId}`,
@@ -0,0 +1,23 @@
/* global axios */
import ApiClient from '../ApiClient';
class OpenAIAPI extends ApiClient {
constructor() {
super('integrations', { accountScoped: true });
}
processEvent({ name = 'rephrase', content, tone, hookId }) {
return axios.post(`${this.url}/hooks/${hookId}/process_event`, {
event: {
name: name,
data: {
tone,
content,
},
},
});
}
}
export default new OpenAIAPI();
+3 -2
View File
@@ -40,6 +40,7 @@ class ReportsAPI extends ApiClient {
id,
group_by: groupBy,
business_hours: businessHours,
timezone_offset: getTimeOffset(),
},
});
}
@@ -59,9 +60,9 @@ class ReportsAPI extends ApiClient {
});
}
getConversationTrafficCSV({ from: since, to: until }) {
getConversationTrafficCSV() {
return axios.get(`${this.url}/conversation_traffic`, {
params: { since, until },
params: { timezone_offset: getTimeOffset() },
});
}
@@ -42,9 +42,13 @@ describe('#Reports API', () => {
'/api/v2/reports/summary',
{
params: {
business_hours: undefined,
group_by: undefined,
id: undefined,
since: 1621103400,
until: 1621621800,
timezone_offset: -0,
type: 'account',
until: 1621621800,
},
}
);
+1 -1
View File
@@ -1,5 +1,5 @@
/* global axios */
import wootConstants from 'dashboard/constants';
import wootConstants from 'dashboard/constants/globals';
export const getTestimonialContent = () => {
return axios.get(wootConstants.TESTIMONIAL_URL);
@@ -85,7 +85,7 @@
}
}
.settings.back-button {
.header-section.back-button {
direction: initial;
margin-left: var(--space-normal);
margin-right: var(--space-smaller);
@@ -21,7 +21,7 @@
.multiselect--active {
>.multiselect__tags {
border-color: $color-woot;
border-color: var(--w-500);
}
}
@@ -75,16 +75,13 @@
}
&.multiselect__option--selected {
background: var(--w-400);
color: var(--white);
background: var(--w-75);
&.multiselect__option--highlight:hover {
background: var(--w-600);
color: var(--white);
background: var(--w-75);
&::after {
background: transparent;
color: var(--white);
}
&::after:hover {
@@ -196,6 +193,7 @@
display: flex;
font-size: var(--font-size-small);
margin: 0;
max-height: 3.8rem;
padding: var(--space-smaller) var(--space-micro);
}
@@ -125,6 +125,7 @@
@update-conversation-status="toggleConversationStatus"
@context-menu-toggle="onContextMenuToggle"
@mark-as-unread="markAsUnread"
@assign-priority="assignPriority"
/>
<div v-if="chatListLoading" class="text-center">
@@ -173,7 +174,7 @@ import ConversationCard from './widgets/conversation/ConversationCard';
import timeMixin from '../mixins/time';
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
import conversationMixin from '../mixins/conversations';
import wootConstants from '../constants';
import wootConstants from 'dashboard/constants/globals';
import advancedFilterTypes from './widgets/conversation/advancedFilterItems';
import filterQueryGenerator from '../helper/filterQueryGenerator.js';
import AddCustomViews from 'dashboard/routes/dashboard/customviews/AddCustomViews';
@@ -191,6 +192,7 @@ import {
isOnMentionsView,
isOnUnattendedView,
} from '../store/modules/conversations/helpers/actionHelpers';
import { CONVERSATION_EVENTS } from '../helper/AnalyticsHelper/events';
export default {
components: {
@@ -248,6 +250,9 @@ export default {
...filter,
attributeName: this.$t(`FILTER.ATTRIBUTES.${filter.attributeI18nKey}`),
})),
// chatsOnView is to store the chats that are currently visible on the screen,
// which mirrors the conversationList.
chatsOnView: [],
foldersQuery: {},
showAddFoldersModal: false,
showDeleteFoldersModal: false,
@@ -347,18 +352,39 @@ export default {
this.currentPageFilterKey
);
},
activeAssigneeTabCount() {
const { activeAssigneeTab } = this;
const count = this.assigneeTabItems.find(
item => item.key === activeAssigneeTab
).count;
return count;
},
conversationFilters() {
return {
inboxId: this.conversationInbox ? this.conversationInbox : undefined,
assigneeType: this.activeAssigneeTab,
status: this.activeStatus,
page: this.currentPage + 1,
page: this.conversationListPagination,
labels: this.label ? [this.label] : undefined,
teamId: this.teamId || undefined,
conversationType: this.conversationType || undefined,
folders: this.hasActiveFolders ? this.savedFoldersValue : undefined,
};
},
conversationListPagination() {
const conversationsPerPage = 25;
const isNoFiltersOrFoldersAndChatListNotEmpty =
!this.hasAppliedFiltersOrActiveFolders && this.chatsOnView !== [];
const isUnderPerPage =
this.chatsOnView.length < conversationsPerPage &&
this.activeAssigneeTabCount < conversationsPerPage &&
this.activeAssigneeTabCount > this.chatsOnView.length;
if (isNoFiltersOrFoldersAndChatListNotEmpty && isUnderPerPage) {
return 1;
}
return this.currentPage + 1;
},
pageTitle() {
if (this.hasAppliedFilters) {
return this.$t('CHAT_LIST.TAB_HEADING');
@@ -400,7 +426,6 @@ export default {
} else {
conversationList = [...this.chatLists];
}
return conversationList;
},
activeFolder() {
@@ -449,6 +474,9 @@ export default {
this.resetAndFetchData();
}
},
chatLists() {
this.chatsOnView = this.conversationList;
},
},
mounted() {
this.$store.dispatch('setChatFilter', this.activeStatus);
@@ -670,6 +698,26 @@ export default {
this.showAlert(this.$t('BULK_ACTION.ASSIGN_FAILED'));
}
},
async assignPriority(priority, conversationId = null) {
this.$store.dispatch('setCurrentChatPriority', {
priority,
conversationId,
});
this.$store
.dispatch('assignPriority', { conversationId, priority })
.then(() => {
this.$track(CONVERSATION_EVENTS.CHANGE_PRIORITY, {
newValue: priority,
from: 'Context menu',
});
this.showAlert(
this.$t('CONVERSATION.PRIORITY.CHANGE_PRIORITY.SUCCESSFUL', {
priority,
conversationId,
})
);
});
},
async markAsUnread(conversationId) {
try {
await this.$store.dispatch('markMessagesUnread', {
+43 -6
View File
@@ -1,8 +1,23 @@
<template>
<div class="code--container">
<button class="button small button--copy-code" @click="onCopy">
{{ $t('COMPONENTS.CODE.BUTTON_TEXT') }}
</button>
<div class="code--action-area">
<form
v-if="enableCodePen"
class="code--codeopen-form"
action="https://codepen.io/pen/define"
method="POST"
target="_blank"
>
<input type="hidden" name="data" :value="codepenScriptValue" />
<button type="submit" class="button secondary tiny">
{{ $t('COMPONENTS.CODE.CODEPEN') }}
</button>
</form>
<button class="button secondary tiny" @click="onCopy">
{{ $t('COMPONENTS.CODE.BUTTON_TEXT') }}
</button>
</div>
<highlightjs v-if="script" :language="lang" :code="script" />
</div>
</template>
@@ -21,6 +36,24 @@ export default {
type: String,
default: 'javascript',
},
enableCodePen: {
type: Boolean,
default: false,
},
codepenTitle: {
type: String,
default: 'Chatwoot Codepen',
},
},
computed: {
codepenScriptValue() {
const lang = this.lang === 'javascript' ? 'js' : this.lang;
return JSON.stringify({
title: this.codepenTitle,
private: true,
[lang]: this.script,
});
},
},
methods: {
async onCopy(e) {
@@ -37,10 +70,14 @@ export default {
position: relative;
text-align: left;
.button--copy-code {
margin-top: 0;
.code--action-area {
top: var(--space-small);
position: absolute;
right: 0;
right: var(--space-small);
}
.code--codeopen-form {
display: inline-block;
}
}
</style>
@@ -12,7 +12,8 @@
</template>
<script>
import Banner from 'dashboard/components/ui/Banner.vue';
import { LocalStorage, LOCAL_STORAGE_KEYS } from '../../helper/localStorage';
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
import { LocalStorage } from 'shared/helpers/localStorage';
import { mapGetters } from 'vuex';
import adminMixin from 'dashboard/mixins/isAdmin';
import { hasAnUpdateAvailable } from './versionCheckHelper';
@@ -126,7 +126,7 @@ import WootDropdownSubMenu from 'shared/components/ui/dropdown/DropdownSubMenu.v
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
import WootDropdownDivider from 'shared/components/ui/dropdown/DropdownDivider';
import wootConstants from '../../constants';
import wootConstants from 'dashboard/constants/globals';
import {
CMD_REOPEN_CONVERSATION,
CMD_RESOLVE_CONVERSATION,
@@ -13,6 +13,7 @@ import DropdownMenu from 'shared/components/ui/dropdown/DropdownMenu';
import FeatureToggle from './widgets/FeatureToggle';
import HorizontalBar from './widgets/chart/HorizontalBarChart';
import Input from './widgets/forms/Input.vue';
import PhoneInput from './widgets/forms/PhoneInput.vue';
import Label from './ui/Label';
import LoadingState from './widgets/LoadingState';
import Modal from './Modal';
@@ -40,6 +41,7 @@ const WootUIKit = {
FeatureToggle,
HorizontalBar,
Input,
PhoneInput,
Label,
LoadingState,
Modal,
@@ -52,8 +52,9 @@ import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu';
import WootDropdownHeader from 'shared/components/ui/dropdown/DropdownHeader';
import WootDropdownDivider from 'shared/components/ui/dropdown/DropdownDivider';
import AvailabilityStatusBadge from '../widgets/conversation/AvailabilityStatusBadge';
import wootConstants from 'dashboard/constants/globals';
const AVAILABILITY_STATUS_KEYS = ['online', 'busy', 'offline'];
const { AVAILABILITY_STATUS_KEYS } = wootConstants;
export default {
components: {
@@ -8,6 +8,7 @@ const settings = accountId => ({
'agent_list',
'attributes_list',
'automation_list',
'auditlogs_list',
'billing_settings_index',
'canned_list',
'general_settings_index',
@@ -150,6 +151,14 @@ const settings = accountId => ({
toStateName: 'billing_settings_index',
showOnlyOnCloud: true,
},
{
icon: 'key',
label: 'AUDIT_LOGS',
hasSubMenu: false,
toState: frontendURL(`accounts/${accountId}/settings/audit-log/list`),
toStateName: 'auditlogs_list',
beta: true,
},
],
});
@@ -41,7 +41,7 @@ import PrimaryNavItem from './PrimaryNavItem';
import OptionsMenu from './OptionsMenu';
import AgentDetails from './AgentDetails';
import NotificationBell from './NotificationBell';
import wootConstants from 'dashboard/constants';
import wootConstants from 'dashboard/constants/globals';
import { frontendURL } from 'dashboard/helper/URLHelper';
export default {
@@ -0,0 +1,171 @@
<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>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import { mixin as clickaway } from 'vue-clickaway';
import OpenAPI from 'dashboard/api/integrations/openapi';
import alertMixin from 'shared/mixins/alertMixin';
export default {
mixins: [alertMixin, clickaway],
props: {
conversationId: {
type: Number,
default: 0,
},
message: {
type: String,
default: '',
},
},
data() {
return {
isGenerating: false,
showDropdown: false,
activeTone: 'professional',
tones: [
{
key: 'professional',
value: this.$t(
'INTEGRATION_SETTINGS.OPEN_AI.TONE.OPTIONS.PROFESSIONAL'
),
},
{
key: 'friendly',
value: this.$t('INTEGRATION_SETTINGS.OPEN_AI.TONE.OPTIONS.FRIENDLY'),
},
],
};
},
computed: {
...mapGetters({ appIntegrations: 'integrations/getAppIntegrations' }),
isAIIntegrationEnabled() {
return this.appIntegrations.find(
integration => integration.id === 'openai' && !!integration.hooks.length
);
},
hookId() {
return this.appIntegrations.find(
integration => integration.id === 'openai' && !!integration.hooks.length
).hooks[0].id;
},
buttonText() {
return this.isGenerating
? this.$t('INTEGRATION_SETTINGS.OPEN_AI.BUTTONS.GENERATING')
: this.$t('INTEGRATION_SETTINGS.OPEN_AI.BUTTONS.GENERATE');
},
},
mounted() {
if (!this.appIntegrations.length) {
this.$store.dispatch('integrations/get');
}
},
methods: {
toggleDropdown() {
this.showDropdown = !this.showDropdown;
},
closeDropdown() {
this.showDropdown = false;
},
async processText() {
this.isGenerating = true;
try {
const result = await OpenAPI.processEvent({
hookId: this.hookId,
type: 'rephrase',
content: this.message,
tone: this.activeTone,
});
const {
data: { message: generatedMessage },
} = result;
this.$emit('replace-text', generatedMessage || this.message);
this.closeDropdown();
} catch (error) {
this.showAlert(this.$t('INTEGRATION_SETTINGS.OPEN_AI.GENERATE_ERROR'));
} finally {
this.isGenerating = false;
}
},
},
};
</script>
<style lang="scss" scoped>
.ai-modal {
width: 400px;
right: 0;
left: 0;
padding: var(--space-normal);
bottom: 34px;
position: absolute;
span {
font-size: var(--font-size-small);
font-weight: var(--font-weight-medium);
}
p {
color: var(--s-600);
}
label {
margin-bottom: var(--space-smaller);
}
.status--filter {
background-color: var(--color-background-light);
border: 1px solid var(--color-border);
font-size: var(--font-size-small);
height: var(--space-large);
padding: 0 var(--space-medium) 0 var(--space-small);
}
.modal-footer {
gap: var(--space-smaller);
}
}
</style>
@@ -1,5 +1,5 @@
<template>
<button class="settings back-button" @click.capture="goBack">
<button class="header-section back-button" @click.capture="goBack">
<fluent-icon icon="chevron-left" />
{{ buttonLabel || $t('GENERAL_SETTINGS.BACK') }}
</button>
@@ -9,7 +9,7 @@
</woot-tabs>
</template>
<script>
import wootConstants from '../../constants';
import wootConstants from 'dashboard/constants/globals';
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
import { hasPressedAltAndNKey } from 'shared/helpers/KeyboardHelpers';
@@ -1,5 +1,5 @@
<template>
<div class="dashboard-app--container">
<div v-if="hasOpenedAtleastOnce" class="dashboard-app--container">
<div
v-for="(configItem, index) in config"
:key="index"
@@ -35,9 +35,14 @@ export default {
type: Object,
default: () => ({}),
},
isVisible: {
type: Boolean,
default: false,
},
},
data() {
return {
hasOpenedAtleastOnce: false,
iframeLoading: true,
};
},
@@ -57,6 +62,13 @@ export default {
return { id, name, email };
},
},
watch: {
isVisible() {
if (this.isVisible) {
this.hasOpenedAtleastOnce = true;
}
},
},
mounted() {
window.onmessage = e => {
@@ -0,0 +1,37 @@
import InboxDropdownItem from './InboxDropdownItem';
export default {
title: 'Components/DropDowns/InboxDropdownItem',
component: InboxDropdownItem,
argTypes: {
name: {
defaultValue: 'My new inbox',
control: {
type: 'text',
},
},
inboxIdentifier: {
defaultValue: 'nithin@mail.com',
control: {
type: 'text',
},
},
channelType: {
defaultValue: 'email',
control: {
type: 'text',
},
},
},
};
const Template = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: { InboxDropdownItem },
template: '<inbox-dropdown-item v-bind="$props" ></inbox-dropdown-item>',
});
export const Banner = Template.bind({});
Banner.args = {};
@@ -0,0 +1,102 @@
<template>
<div class="option-item--inbox">
<span class="badge--icon">
<fluent-icon :icon="computedInboxIcon" size="14" />
</span>
<div class="option__user-data">
<h5 class="option__title">
{{ name }}
</h5>
<p class="option__body text-truncate" :title="inboxIdentifier">
{{ inboxIdentifier || computedInboxType }}
</p>
</div>
</div>
</template>
<script>
import {
getInboxClassByType,
getReadableInboxByType,
} from 'dashboard/helper/inbox';
export default {
components: {},
props: {
name: {
type: String,
default: '',
},
inboxIdentifier: {
type: String,
default: '',
},
channelType: {
type: String,
default: '',
},
},
computed: {
computedInboxIcon() {
if (!this.channelType) return 'chat';
const classByType = getInboxClassByType(
this.channelType,
this.inboxIdentifier
);
return classByType;
},
computedInboxType() {
if (!this.channelType) return 'chat';
const classByType = getReadableInboxByType(
this.channelType,
this.inboxIdentifier
);
return classByType;
},
},
};
</script>
<style lang="scss" scoped>
.option-item--inbox {
display: flex;
align-items: center;
height: 3.8rem;
min-width: 0;
padding: 0 var(--space-smaller);
}
.badge--icon {
display: inline-flex;
border-radius: var(--border-radius-small);
margin-right: var(--space-smaller);
background: var(--s-25);
padding: var(--space-micro);
align-items: center;
flex-shrink: 0;
justify-content: center;
width: var(--space-medium);
height: var(--space-medium);
}
.option__user-data {
display: flex;
flex-direction: column;
width: 100%;
min-width: 0;
margin-left: var(--space-smaller);
margin-right: var(--space-smaller);
}
.option__body {
display: inline-block;
color: var(--s-600);
font-size: var(--font-size-small);
line-height: 1.3;
min-width: 0;
margin: 0;
}
.option__title {
line-height: 1.1;
font-size: var(--font-size-mini);
margin: 0;
}
</style>
@@ -91,6 +91,12 @@
v-if="(isAWebWidgetInbox || isAPIInbox) && !isOnPrivateNote"
:conversation-id="conversationId"
/>
<AIAssistanceButton
v-if="message"
:conversation-id="conversationId"
:message="message"
@replace-text="replaceText"
/>
<transition name="modal-fade">
<div
v-show="$refs.upload && $refs.upload.dropActive"
@@ -129,12 +135,13 @@ import {
ALLOWED_FILE_TYPES_FOR_TWILIO_WHATSAPP,
} from 'shared/constants/messages';
import VideoCallButton from '../VideoCallButton';
import AIAssistanceButton from '../AIAssistanceButton.vue';
import { REPLY_EDITOR_MODES } from './constants';
import { mapGetters } from 'vuex';
export default {
name: 'ReplyBottomPanel',
components: { FileUpload, VideoCallButton },
components: { FileUpload, VideoCallButton, AIAssistanceButton },
mixins: [eventListenerMixins, uiSettingsMixin, inboxMixin],
props: {
mode: {
@@ -217,6 +224,10 @@ export default {
type: Number,
required: true,
},
message: {
type: String,
default: '',
},
},
computed: {
...mapGetters({
@@ -303,6 +314,9 @@ export default {
send_with_signature: !this.sendWithSignature,
});
},
replaceText(text) {
this.$emit('replace-text', text);
},
},
};
</script>
@@ -11,7 +11,7 @@
</template>
<script>
import wootConstants from '../../../constants';
import wootConstants from 'dashboard/constants/globals';
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
import { hasPressedAltAndBKey } from 'shared/helpers/KeyboardHelpers';
@@ -23,7 +23,7 @@
:show-badge="false"
/>
</woot-tabs>
<div v-if="!activeIndex" class="messages-and-sidebar">
<div v-show="!activeIndex" class="messages-and-sidebar">
<messages-view
v-if="currentChat.id"
:inbox-id="inboxId"
@@ -41,9 +41,11 @@
</div>
</div>
<dashboard-app-frame
v-else
:key="currentChat.id + '-' + activeIndex"
:config="dashboardApps[activeIndex - 1].content"
v-for="(dashboardApp, index) in dashboardApps"
v-show="activeIndex - 1 === index"
:key="currentChat.id + '-' + dashboardApp.id"
:is-visible="activeIndex - 1 === index"
:config="dashboardApps[index].content"
:current-chat="currentChat"
/>
</div>
@@ -112,6 +114,7 @@ export default {
},
'currentChat.id'() {
this.fetchLabels();
this.activeIndex = 0;
},
},
mounted() {
@@ -33,13 +33,16 @@
<div class="conversation--details columns">
<div class="conversation--metadata">
<inbox-name v-if="showInboxName" :inbox="inbox" />
<span
v-if="showAssignee && assignee.name"
class="label assignee-label text-truncate"
>
<fluent-icon icon="person" size="12" />
{{ assignee.name }}
</span>
<div class="conversation-metadata-attributes">
<span
v-if="showAssignee && assignee.name"
class="label assignee-label text-truncate"
>
<fluent-icon icon="person" size="12" />
{{ assignee.name }}
</span>
<priority-mark :priority="chat.priority" />
</div>
</div>
<h4 class="conversation--user">
{{ currentContact.name }}
@@ -106,12 +109,14 @@
<conversation-context-menu
:status="chat.status"
:inbox-id="inbox.id"
:priority="chat.priority"
:has-unread-messages="hasUnread"
@update-conversation="onUpdateConversation"
@assign-agent="onAssignAgent"
@assign-label="onAssignLabel"
@assign-team="onAssignTeam"
@mark-as-unread="markAsUnread"
@assign-priority="assignPriority"
/>
</woot-context-menu>
</div>
@@ -131,6 +136,7 @@ import ConversationContextMenu from './contextMenu/Index.vue';
import alertMixin from 'shared/mixins/alertMixin';
import TimeAgo from 'dashboard/components/ui/TimeAgo';
import CardLabels from './conversationCardComponents/CardLabels.vue';
import PriorityMark from './PriorityMark.vue';
const ATTACHMENT_ICONS = {
image: 'image',
audio: 'headphones-sound-wave',
@@ -147,6 +153,7 @@ export default {
Thumbnail,
ConversationContextMenu,
TimeAgo,
PriorityMark,
},
mixins: [
@@ -370,6 +377,10 @@ export default {
this.$emit('mark-as-unread', this.chat.id);
this.closeContextMenu();
},
async assignPriority(priority) {
this.$emit('assign-priority', priority, this.chat.id);
this.closeContextMenu();
},
},
};
</script>
@@ -429,6 +440,11 @@ export default {
padding: var(--space-micro) 0 var(--space-micro) 0;
}
.conversation-metadata-attributes {
display: flex;
gap: var(--space-small);
}
.assignee-label {
display: inline-flex;
margin-left: var(--space-small);
@@ -63,7 +63,7 @@ import inboxMixin from 'shared/mixins/inboxMixin';
import InboxName from '../InboxName';
import MoreActions from './MoreActions';
import Thumbnail from '../Thumbnail';
import wootConstants from '../../../constants';
import wootConstants from 'dashboard/constants/globals';
import { conversationListPageURL } from 'dashboard/helper/URLHelper';
export default {
components: {
@@ -444,9 +444,9 @@ export default {
this.hasImageError = true;
},
openContextMenu(e) {
const shouldSkipContextMenu = e.target?.classList.contains(
'skip-context-menu'
);
const shouldSkipContextMenu =
e.target?.classList.contains('skip-context-menu') ||
e.target?.tagName.toLowerCase() === 'a';
if (shouldSkipContextMenu || getSelection().toString()) {
return;
}
@@ -0,0 +1,62 @@
<template>
<span
v-if="priority"
v-tooltip="{
content: tooltipText,
delay: { show: 1500, hide: 0 },
hideOnClick: true,
}"
class="conversation-priority-mark"
:class="{ urgent: priority === CONVERSATION_PRIORITY.URGENT }"
>
<fluent-icon
:icon="`priority-${priority.toLowerCase()}`"
size="14"
view-box="0 0 14 14"
/>
</span>
</template>
<script>
import { CONVERSATION_PRIORITY } from '../../../../shared/constants/messages';
export default {
name: 'PriorityMark',
props: {
priority: {
type: String,
default: '',
validate: value =>
[...Object.values(CONVERSATION_PRIORITY), ''].includes(value),
},
},
data() {
return {
CONVERSATION_PRIORITY,
};
},
computed: {
tooltipText() {
return this.$t(
`CONVERSATION.PRIORITY.OPTIONS.${this.priority.toUpperCase()}`
);
},
},
};
</script>
<style scoped lang="scss">
.conversation-priority-mark {
align-items: center;
background: var(--s-50);
border-radius: var(--border-radius-small);
color: var(--s-600);
display: inline-flex;
width: var(--space-snug);
&.urgent {
background: var(--r-500);
color: var(--w-25);
}
}
</style>
@@ -120,8 +120,10 @@
:toggle-audio-recorder-play-pause="toggleAudioRecorderPlayPause"
:toggle-audio-recorder="toggleAudioRecorder"
:toggle-emoji-picker="toggleEmojiPicker"
:message="message"
@selectWhatsappTemplate="openWhatsappTemplateModal"
@toggle-editor="toggleRichContentEditor"
@replace-text="replaceText"
/>
<whatsapp-templates
:inbox-id="inbox.id"
@@ -174,9 +176,10 @@ import inboxMixin from 'shared/mixins/inboxMixin';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import { DirectUpload } from 'activestorage';
import { frontendURL } from '../../../helper/URLHelper';
import { LocalStorage, LOCAL_STORAGE_KEYS } from '../../../helper/localStorage';
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
import { LocalStorage } from 'shared/helpers/localStorage';
import { trimContent, debounce } from '@chatwoot/utils';
import wootConstants from 'dashboard/constants';
import wootConstants from 'dashboard/constants/globals';
import { isEditorHotKeyEnabled } from 'dashboard/mixins/uiSettings';
import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
import rtlMixin from 'shared/mixins/rtlMixin';
@@ -23,6 +23,14 @@
@click="snoozeConversation(option.snoozedUntil)"
/>
</menu-item-with-submenu>
<menu-item-with-submenu :option="priorityConfig">
<menu-item
v-for="(option, i) in priorityConfig.options"
:key="i"
:option="option"
@click="assignPriority(option.key)"
/>
</menu-item-with-submenu>
<menu-item-with-submenu
:option="labelMenuConfig"
:sub-menu-available="!!labels.length"
@@ -69,7 +77,7 @@
<script>
import MenuItem from './menuItem.vue';
import MenuItemWithSubmenu from './menuItemWithSubmenu.vue';
import wootConstants from 'dashboard/constants.js';
import wootConstants from 'dashboard/constants/globals';
import snoozeTimesMixin from 'dashboard/mixins/conversation/snoozeTimesMixin';
import { mapGetters } from 'vuex';
import AgentLoadingPlaceholder from './agentLoadingPlaceholder.vue';
@@ -93,6 +101,10 @@ export default {
type: Number,
default: null,
},
priority: {
type: String,
default: null,
},
},
data() {
return {
@@ -140,6 +152,33 @@ export default {
},
],
},
priorityConfig: {
key: 'priority',
label: this.$t('CONVERSATION.PRIORITY.TITLE'),
icon: 'warning',
options: [
{
label: this.$t('CONVERSATION.PRIORITY.OPTIONS.NONE'),
key: null,
},
{
label: this.$t('CONVERSATION.PRIORITY.OPTIONS.URGENT'),
key: 'urgent',
},
{
label: this.$t('CONVERSATION.PRIORITY.OPTIONS.HIGH'),
key: 'high',
},
{
label: this.$t('CONVERSATION.PRIORITY.OPTIONS.MEDIUM'),
key: 'medium',
},
{
label: this.$t('CONVERSATION.PRIORITY.OPTIONS.LOW'),
key: 'low',
},
].filter(item => item.key !== this.priority),
},
labelMenuConfig: {
key: 'label',
icon: 'tag',
@@ -193,6 +232,9 @@ export default {
this.snoozeTimes[snoozedUntil] || null
);
},
assignPriority(priority) {
this.$emit('assign-priority', priority);
},
show(key) {
// If the conversation status is same as the action, then don't display the option
// i.e.: Don't show an option to resolve if the conversation is already resolved.
@@ -2,6 +2,6 @@ import emailValidator from 'vuelidate/lib/validators/email';
export const validEmailsByComma = value => {
if (!value.length) return true;
const emails = value.split(',');
const emails = value.replace(/\s+/g, '').split(',');
return emails.every(email => emailValidator(email));
};
@@ -10,4 +10,7 @@ describe('#validEmailsByComma', () => {
it('returns false when one of the email passed is invalid', () => {
expect(validEmailsByComma('ni@njan.com,pova.da')).toEqual(false);
});
it('strips spaces between emails before validating', () => {
expect(validEmailsByComma('1@test.com , 2@test.com')).toEqual(true);
});
});
@@ -0,0 +1,361 @@
<template>
<div class="phone-input--wrap">
<div class="phone-input" :class="{ 'has-error': error }">
<div class="country-emoji--wrap" @click="toggleCountryDropdown">
<h5 v-if="activeCountry.emoji">{{ activeCountry.emoji }}</h5>
<fluent-icon v-else icon="globe" class="fluent-icon" size="16" />
<fluent-icon icon="chevron-down" class="fluent-icon" size="12" />
</div>
<span v-if="activeDialCode" class="country-dial--code">
{{ activeDialCode }}
</span>
<input
:value="phoneNumber"
type="tel"
class="phone-input--field"
:placeholder="placeholder"
:readonly="readonly"
:style="styles"
@input="onChange"
@blur="onBlur"
/>
</div>
<div v-if="showDropdown" ref="dropdown" class="country-dropdown">
<div class="dropdown-search--wrap">
<input
ref="searchbar"
v-model="searchCountry"
type="text"
placeholder="Search"
class="dropdown-search"
/>
</div>
<div
v-for="(country, index) in filteredCountriesBySearch"
ref="dropdownItem"
:key="index"
class="country-dropdown--item"
:class="{
active: country.id === activeCountryCode,
focus: index === selectedIndex,
}"
@click="onSelectCountry(country)"
>
<span class="country-emoji">{{ country.emoji }}</span>
<span class="country-name">
{{ country.name }}
</span>
<span class="country-dial-code">{{ country.dial_code }}</span>
</div>
<div v-if="filteredCountriesBySearch.length === 0">
<span class="no-results">No results found</span>
</div>
</div>
</div>
</template>
<script>
import countries from 'shared/constants/countries.js';
import parsePhoneNumber from 'libphonenumber-js';
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
import {
hasPressedArrowUpKey,
hasPressedArrowDownKey,
isEnter,
} from 'shared/helpers/KeyboardHelpers';
export default {
mixins: [eventListenerMixins],
props: {
value: {
type: [String, Number],
default: '',
},
placeholder: {
type: String,
default: '',
},
readonly: {
type: Boolean,
default: false,
},
styles: {
type: Object,
default: () => {},
},
error: {
type: Boolean,
default: false,
},
},
data() {
return {
countries: [
{
name: 'Select Country',
dial_code: '',
emoji: '',
id: '',
},
...countries,
],
selectedIndex: -1,
showDropdown: false,
searchCountry: '',
activeCountryCode: '',
activeDialCode: '',
phoneNumber: this.value,
};
},
computed: {
filteredCountriesBySearch() {
return this.countries.filter(country => {
const { name, dial_code, id } = country;
const search = this.searchCountry.toLowerCase();
return (
name.toLowerCase().includes(search) ||
dial_code.toLowerCase().includes(search) ||
id.toLowerCase().includes(search)
);
});
},
activeCountry() {
if (this.activeCountryCode) {
return this.countries.find(
country => country.id === this.activeCountryCode
);
}
return '';
},
},
watch: {
value() {
const number = parsePhoneNumber(this.value);
if (number) {
this.activeCountryCode = number.country;
this.activeDialCode = `+${number.countryCallingCode}`;
this.phoneNumber = this.value.replace(
`+${number.countryCallingCode}`,
''
);
}
},
},
mounted() {
window.addEventListener('mouseup', this.onOutsideClick);
this.setActiveCountry();
},
beforeDestroy() {
window.removeEventListener('mouseup', this.onOutsideClick);
},
methods: {
onOutsideClick(e) {
if (
this.showDropdown &&
e.target !== this.$refs.dropdown &&
!this.$refs.dropdown.contains(e.target)
) {
this.closeDropdown();
}
},
onChange(e) {
this.phoneNumber = e.target.value;
this.$emit('input', e.target.value, this.activeDialCode);
},
onBlur(e) {
this.$emit('blur', e.target.value);
},
dropdownItem() {
return Array.from(
this.$refs.dropdown.querySelectorAll(
'div.country-dropdown div.country-dropdown--item'
)
);
},
focusedItem() {
return Array.from(
this.$refs.dropdown.querySelectorAll('div.country-dropdown div.focus')
);
},
focusedItemIndex() {
return Array.from(this.dropdownItem()).indexOf(this.focusedItem()[0]);
},
onKeyDownHandler(e) {
const { showDropdown, filteredCountriesBySearch, onSelectCountry } = this;
const { selectedIndex } = this;
if (showDropdown) {
if (hasPressedArrowDownKey(e)) {
e.preventDefault();
this.selectedIndex = Math.min(
selectedIndex + 1,
filteredCountriesBySearch.length - 1
);
this.$refs.dropdown.scrollTop = this.focusedItemIndex() * 28;
} else if (hasPressedArrowUpKey(e)) {
e.preventDefault();
this.selectedIndex = Math.max(selectedIndex - 1, 0);
this.$refs.dropdown.scrollTop = this.focusedItemIndex() * 28 - 56;
} else if (isEnter(e)) {
e.preventDefault();
onSelectCountry(filteredCountriesBySearch[selectedIndex]);
}
}
},
onSelectCountry(country) {
this.activeCountryCode = country.id;
this.searchCountry = '';
this.activeDialCode = country.dial_code;
this.$emit('setCode', country.dial_code);
this.closeDropdown();
},
setActiveCountry() {
const { phoneNumber } = this;
if (!phoneNumber) return;
const number = parsePhoneNumber(phoneNumber);
if (number) {
this.activeCountryCode = number.country;
this.activeDialCode = number.countryCallingCode;
}
},
toggleCountryDropdown() {
this.showDropdown = !this.showDropdown;
this.selectedIndex = -1;
if (this.showDropdown) {
this.$nextTick(() => {
this.$refs.searchbar.focus();
});
}
},
closeDropdown() {
this.selectedIndex = -1;
this.showDropdown = false;
},
},
};
</script>
<style scoped lang="scss">
.phone-input--wrap {
position: relative;
.phone-input {
display: flex;
align-items: center;
justify-content: flex-start;
margin-bottom: var(--space-normal);
border: 1px solid var(--s-200);
border-radius: var(--border-radius-normal);
&.has-error {
border: 1px solid var(--r-400);
}
}
.country-emoji--wrap {
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: var(--space-small);
background: var(--s-25);
height: 4rem;
width: 5.2rem;
border-radius: var(--border-radius-normal) 0 0 var(--border-radius-normal);
padding: var(--space-small) var(--space-smaller) var(--space-small)
var(--space-small);
h5 {
margin-bottom: 0;
}
}
.country-dial--code {
display: flex;
color: var(--s-300);
font-size: var(--space-normal);
font-weight: normal;
line-height: 1.5;
padding: var(--space-small) 0 var(--space-small) var(--space-small);
}
.phone-input--field {
margin-bottom: 0;
border: 0;
}
.country-dropdown {
z-index: var(--z-index-low);
position: absolute;
height: var(--space-giga);
width: 20rem;
overflow-y: auto;
top: 4rem;
border-radius: var(--border-radius-default);
padding: 0 0 var(--space-smaller) 0;
background-color: var(--white);
box-shadow: var(--shadow-context-menu);
border-radius: var(--border-radius-normal);
.dropdown-search--wrap {
top: 0;
position: sticky;
background-color: var(--white);
padding: var(--space-smaller);
.dropdown-search {
height: var(--space-large);
margin-bottom: 0;
font-size: var(--font-size-small);
border: 1px solid var(--s-200) !important;
}
}
.country-dropdown--item {
display: flex;
align-items: center;
height: 2.8rem;
padding: 0 var(--space-smaller);
cursor: pointer;
&.active {
background-color: var(--s-50);
}
&.focus {
background-color: var(--s-25);
}
&:hover {
background-color: var(--s-50);
}
.country-emoji {
font-size: var(--font-size-default);
margin-right: var(--space-smaller);
}
.country-name {
max-width: 12rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.country-dial-code {
margin-left: var(--space-smaller);
color: var(--s-300);
font-size: var(--font-size-mini);
}
}
.no-results {
display: flex;
align-items: center;
justify-content: center;
color: var(--s-500);
margin-top: var(--space-normal);
}
}
}
</style>
@@ -24,5 +24,6 @@ export default {
DOCS_URL: '//www.chatwoot.com/docs/product/',
TESTIMONIAL_URL: 'https://testimonials.cdn.chatwoot.com/content.json',
SMALL_SCREEN_BREAKPOINT: 1024,
AVAILABILITY_STATUS_KEYS: ['online', 'busy', 'offline'],
};
export const DEFAULT_REDIRECT_URL = '/app/';
@@ -0,0 +1,5 @@
export const LOCAL_STORAGE_KEYS = {
DISMISSED_UPDATES: 'dismissedUpdates',
WIDGET_BUILDER: 'widgetBubble_',
DRAFT_MESSAGES: 'draftMessages',
};
@@ -7,6 +7,7 @@ export const CONVERSATION_EVENTS = Object.freeze({
USED_MENTIONS: 'Used mentions',
SEARCH_CONVERSATION: 'Searched conversations',
APPLY_FILTER: 'Applied filters in the conversation list',
CHANGE_PRIORITY: 'Assigned priority to a conversation',
});
export const ACCOUNT_EVENTS = Object.freeze({
+1 -1
View File
@@ -1,4 +1,4 @@
import { DEFAULT_REDIRECT_URL } from '../constants';
import { DEFAULT_REDIRECT_URL } from 'dashboard/constants/globals';
export const frontendURL = (path, params) => {
const stringifiedParams = params ? `?${new URLSearchParams(params)}` : '';
@@ -30,6 +30,36 @@ class ActionCableConnector extends BaseActionCableConnector {
};
}
onReconnect = () => {
this.syncActiveConversationMessages();
};
onDisconnected = () => {
this.setActiveConversationLastMessageId();
};
setActiveConversationLastMessageId = () => {
const {
params: { conversation_id },
} = this.app.$route;
if (conversation_id) {
this.app.$store.dispatch('setConversationLastMessageId', {
conversationId: Number(conversation_id),
});
}
};
syncActiveConversationMessages = () => {
const {
params: { conversation_id },
} = this.app.$route;
if (conversation_id) {
this.app.$store.dispatch('syncActiveConversationMessages', {
conversationId: Number(conversation_id),
});
}
};
isAValidEvent = data => {
return this.app.$store.getters.getCurrentAccountId === data.account_id;
};
@@ -16,6 +16,29 @@ const MESSAGE_CONDITION_VALUES = [
},
];
export const PRIORITY_CONDITION_VALUES = [
{
id: 'nil',
name: 'None',
},
{
id: 'low',
name: 'Low',
},
{
id: 'medium',
name: 'Medium',
},
{
id: 'high',
name: 'High',
},
{
id: 'urgent',
name: 'Urgent',
},
];
export const getCustomAttributeInputType = key => {
const customAttributeMap = {
date: 'date',
@@ -103,6 +126,7 @@ export const getActionOptions = ({ agents, teams, labels, type }) => {
assign_team: teams,
send_email_to_team: teams,
add_label: generateConditionOptions(labels, 'title'),
change_priority: PRIORITY_CONDITION_VALUES,
};
return actionsMap[type];
};
@@ -139,6 +163,7 @@ export const getConditionOptions = ({
conversation_language: languages,
country_code: countries,
message_type: MESSAGE_CONDITION_VALUES,
priority: PRIORITY_CONDITION_VALUES,
};
return conditionFilterMaps[type];
+50
View File
@@ -1,5 +1,55 @@
import { INBOX_TYPES } from 'shared/mixins/inboxMixin';
export const getInboxSource = (type, phoneNumber, inbox) => {
switch (type) {
case INBOX_TYPES.WEB:
return inbox.website_url || '';
case INBOX_TYPES.TWILIO:
case INBOX_TYPES.WHATSAPP:
return phoneNumber || '';
case INBOX_TYPES.EMAIL:
return inbox.email || '';
default:
return '';
}
};
export const getReadableInboxByType = (type, phoneNumber) => {
switch (type) {
case INBOX_TYPES.WEB:
return 'livechat';
case INBOX_TYPES.FB:
return 'facebook';
case INBOX_TYPES.TWITTER:
return 'twitter';
case INBOX_TYPES.TWILIO:
return phoneNumber?.startsWith('whatsapp') ? 'whatsapp' : 'sms';
case INBOX_TYPES.WHATSAPP:
return 'whatsapp';
case INBOX_TYPES.API:
return 'api';
case INBOX_TYPES.EMAIL:
return 'email';
case INBOX_TYPES.TELEGRAM:
return 'telegram';
case INBOX_TYPES.LINE:
return 'line';
default:
return 'chat';
}
};
export const getInboxClassByType = (type, phoneNumber) => {
switch (type) {
case INBOX_TYPES.WEB:
@@ -57,6 +57,7 @@ export const initializeChatwootEvents = () => {
window.$chatwoot.setCustomAttributes({
signedUpAt: user.created_at,
cloudCustomer: 'true',
account_id: user.account_id,
});
}
@@ -36,6 +36,7 @@ describe('#resolveActionName', () => {
expect(resolveActionName(MACRO_ACTION_TYPES[1].key)).not.toEqual(
MACRO_ACTION_TYPES[0].label
);
expect(resolveActionName('change_priority')).toEqual('Change Priority');
});
});
@@ -74,6 +74,11 @@
"LABEL": "عنوان البريد الإلكتروني",
"PLACEHOLDER": "الرجاء إدخال عنوان البريد الإلكتروني للموظف"
},
"AGENT_AVAILABILITY": {
"LABEL": "التوفر",
"PLACEHOLDER": "Please select an availability status",
"ERROR": "Availability is required"
},
"SUBMIT": "تعديل حساب الموظف"
},
"BUTTON_TEXT": "تعديل",
@@ -0,0 +1,24 @@
{
"AUDIT_LOGS": {
"HEADER": "Audit Logs",
"HEADER_BTN_TXT": "Add Audit Logs",
"LOADING": "Fetching Audit Logs",
"SEARCH_404": "لا توجد عناصر مطابقة لهذا الاستعلام",
"SIDEBAR_TXT": "<p><b>Audit Logs</b> </p><p> Audit Logs are trails for events and actions in a Chatwoot System. </p>",
"LIST": {
"404": "There are no Audit Logs available in this account.",
"TITLE": "Manage Audit Logs",
"DESC": "Audit Logs are trails for events and actions in a Chatwoot System.",
"TABLE_HEADER": [
"User",
"Action",
"عنوان IP",
"Time"
]
},
"API": {
"SUCCESS_MESSAGE": "AuditLogs retrieved successfully",
"ERROR_MESSAGE": "تعذر الاتصال بالخادم، الرجاء المحاولة مرة أخرى لاحقاً"
}
}
}
@@ -120,8 +120,9 @@
"PHONE_NUMBER": {
"PLACEHOLDER": "أدخل رقم الهاتف الخاص بجهة الاتصال",
"LABEL": "رقم الهاتف",
"HELP": "يجب ان يحتوى رقم الهاتف على كود دولتك تسبقها علامة +\nمثال: +20101243567",
"HELP": "Phone number should be of E.164 format eg: +1415555555 [+][country code][area code][local phone number]. You can select the dial code from the dropdown.",
"ERROR": "يجب ان تكون خانة رقم الهاتف إما فارغة او مكتملة مع رمز الدولة",
"DIAL_CODE_ERROR": "Please select a dial code from the list",
"DUPLICATE": "رقم الهاتف هذا مستخدم لجهة اتصال أخرى."
},
"LOCATION": {
@@ -132,6 +133,17 @@
"PLACEHOLDER": "أدخل اسم الشركة",
"LABEL": "اسم الشركة"
},
"COUNTRY": {
"PLACEHOLDER": "Enter the country name",
"LABEL": "اسم الدولة",
"SELECT_PLACEHOLDER": "اختر",
"REMOVE": "حذف",
"SELECT_COUNTRY": "Select Country"
},
"CITY": {
"PLACEHOLDER": "Enter the city name",
"LABEL": "City Name"
},
"SOCIAL_PROFILES": {
"FACEBOOK": {
"PLACEHOLDER": "أدخل اسم مستخدم فيسبوك",
@@ -170,7 +182,8 @@
"LABEL": "إلى"
},
"INBOX": {
"LABEL": "صندوق الوارد",
"LABEL": "Via Inbox",
"PLACEHOLDER": "Choose source inbox",
"ERROR": "حدد صندوق الوارد"
},
"SUBJECT": {
@@ -439,7 +439,8 @@
"LABEL": "الخصائص",
"DISPLAY_FILE_PICKER": "عرض أداة انتقاء الملفات في الـ widget",
"DISPLAY_EMOJI_PICKER": "عرض منتقي الرموز التعبيرية على الـ widget",
"ALLOW_END_CONVERSATION": "السماح للمستخدمين بإنهاء المحادثة من عنصر واجهة المستخدم"
"ALLOW_END_CONVERSATION": "السماح للمستخدمين بإنهاء المحادثة من عنصر واجهة المستخدم",
"USE_INBOX_AVATAR_FOR_BOT": "Use inbox name and avatar for the bot"
},
"SETTINGS_POPUP": {
"MESSENGER_HEADING": "كود \"الماسنجر\"",
@@ -477,6 +478,8 @@
"WHATSAPP_SECTION_UPDATE_TITLE": "Update API Key",
"WHATSAPP_SECTION_UPDATE_PLACEHOLDER": "Enter the new API Key here",
"WHATSAPP_SECTION_UPDATE_BUTTON": "تحديث",
"WHATSAPP_WEBHOOK_TITLE": "رمز التحقق من Webhook",
"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"
},
"AUTO_ASSIGNMENT": {
@@ -164,6 +164,7 @@
"COMPONENTS": {
"CODE": {
"BUTTON_TEXT": "نسخ",
"CODEPEN": "Open in CodePen",
"COPY_SUCCESSFUL": "تم نسخ الكود إلى الحافظة بنجاح"
},
"SHOW_MORE_BLOCK": {
@@ -202,6 +203,7 @@
"HOME": "الرئيسية",
"AGENTS": "موظف الدعم",
"AGENT_BOTS": "البوتات",
"AUDIT_LOGS": "Audit Logs",
"INBOXES": "قنوات التواصل",
"NOTIFICATIONS": "الإشعارات",
"CANNED_RESPONSES": "الردود السريعة",
@@ -74,6 +74,11 @@
"LABEL": "Имейл адрес",
"PLACEHOLDER": "Моля, въведете имейл адрес на агента"
},
"AGENT_AVAILABILITY": {
"LABEL": "Availability",
"PLACEHOLDER": "Please select an availability status",
"ERROR": "Availability is required"
},
"SUBMIT": "Редактирай агента"
},
"BUTTON_TEXT": "Редактирай",
@@ -0,0 +1,24 @@
{
"AUDIT_LOGS": {
"HEADER": "Audit Logs",
"HEADER_BTN_TXT": "Add Audit Logs",
"LOADING": "Fetching Audit Logs",
"SEARCH_404": "Няма резултати отговарящи на тази заявка",
"SIDEBAR_TXT": "<p><b>Audit Logs</b> </p><p> Audit Logs are trails for events and actions in a Chatwoot System. </p>",
"LIST": {
"404": "There are no Audit Logs available in this account.",
"TITLE": "Manage Audit Logs",
"DESC": "Audit Logs are trails for events and actions in a Chatwoot System.",
"TABLE_HEADER": [
"User",
"Action",
"IP адрес",
"Time"
]
},
"API": {
"SUCCESS_MESSAGE": "AuditLogs retrieved successfully",
"ERROR_MESSAGE": "Не можа да се свърже с Woot сървър. Моля, опитайте отново по-късно"
}
}
}
@@ -120,8 +120,9 @@
"PHONE_NUMBER": {
"PLACEHOLDER": "Добавете телефона на контакта",
"LABEL": "Телефон",
"HELP": "Телефона трябва да е във E.164 формат, например: +35955555555 [+][код на държавата][телефонен номер]",
"HELP": "Phone number should be of E.164 format eg: +1415555555 [+][country code][area code][local phone number]. You can select the dial code from the dropdown.",
"ERROR": "Телефона трябва да е празен или във E.164 формат",
"DIAL_CODE_ERROR": "Please select a dial code from the list",
"DUPLICATE": "Телефона се използва от друг контакт."
},
"LOCATION": {
@@ -132,6 +133,17 @@
"PLACEHOLDER": "Въведете име на фирма",
"LABEL": "Име на фирма"
},
"COUNTRY": {
"PLACEHOLDER": "Enter the country name",
"LABEL": "Име на държавата",
"SELECT_PLACEHOLDER": "Select",
"REMOVE": "Remove",
"SELECT_COUNTRY": "Select Country"
},
"CITY": {
"PLACEHOLDER": "Enter the city name",
"LABEL": "City Name"
},
"SOCIAL_PROFILES": {
"FACEBOOK": {
"PLACEHOLDER": "Въведете Facebook потребителско име",
@@ -170,7 +182,8 @@
"LABEL": "До"
},
"INBOX": {
"LABEL": "Входяща кутия",
"LABEL": "Via Inbox",
"PLACEHOLDER": "Choose source inbox",
"ERROR": "Изберете входяща кутия"
},
"SUBJECT": {
@@ -439,7 +439,8 @@
"LABEL": "Features",
"DISPLAY_FILE_PICKER": "Display file picker on the widget",
"DISPLAY_EMOJI_PICKER": "Display emoji picker on the widget",
"ALLOW_END_CONVERSATION": "Allow users to end conversation from the widget"
"ALLOW_END_CONVERSATION": "Allow users to end conversation from the widget",
"USE_INBOX_AVATAR_FOR_BOT": "Use inbox name and avatar for the bot"
},
"SETTINGS_POPUP": {
"MESSENGER_HEADING": "Messenger Script",
@@ -477,6 +478,8 @@
"WHATSAPP_SECTION_UPDATE_TITLE": "Update API Key",
"WHATSAPP_SECTION_UPDATE_PLACEHOLDER": "Enter the new API Key here",
"WHATSAPP_SECTION_UPDATE_BUTTON": "Обновяване",
"WHATSAPP_WEBHOOK_TITLE": "Webhook Verify Token",
"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"
},
"AUTO_ASSIGNMENT": {
@@ -164,6 +164,7 @@
"COMPONENTS": {
"CODE": {
"BUTTON_TEXT": "Copy",
"CODEPEN": "Open in CodePen",
"COPY_SUCCESSFUL": "Code copied to clipboard successfully"
},
"SHOW_MORE_BLOCK": {
@@ -202,6 +203,7 @@
"HOME": "Home",
"AGENTS": "Агенти",
"AGENT_BOTS": "Bots",
"AUDIT_LOGS": "Audit Logs",
"INBOXES": "Inboxes",
"NOTIFICATIONS": "Notifications",
"CANNED_RESPONSES": "Готови отговори",
@@ -74,6 +74,11 @@
"LABEL": "Adreça de correu electrònic",
"PLACEHOLDER": "Introduïu l'adreça de correu electrònic de l'agent"
},
"AGENT_AVAILABILITY": {
"LABEL": "Disponibilitat",
"PLACEHOLDER": "Please select an availability status",
"ERROR": "Availability is required"
},
"SUBMIT": "Editar l'agent"
},
"BUTTON_TEXT": "Edita",
@@ -0,0 +1,24 @@
{
"AUDIT_LOGS": {
"HEADER": "Audit Logs",
"HEADER_BTN_TXT": "Add Audit Logs",
"LOADING": "Fetching Audit Logs",
"SEARCH_404": "No hi ha cap resposta que coincideixi amb aquesta consulta",
"SIDEBAR_TXT": "<p><b>Audit Logs</b> </p><p> Audit Logs are trails for events and actions in a Chatwoot System. </p>",
"LIST": {
"404": "There are no Audit Logs available in this account.",
"TITLE": "Manage Audit Logs",
"DESC": "Audit Logs are trails for events and actions in a Chatwoot System.",
"TABLE_HEADER": [
"User",
"Action",
"Adreça IP",
"Time"
]
},
"API": {
"SUCCESS_MESSAGE": "AuditLogs retrieved successfully",
"ERROR_MESSAGE": "No s'ha pogut connectar amb el servidor Woot. Torna-ho a provar més endavant"
}
}
}
@@ -120,8 +120,9 @@
"PHONE_NUMBER": {
"PLACEHOLDER": "Introdueix el número de telèfon del contacte",
"LABEL": "Número de telèfon",
"HELP": "Phone number should be of E.164 format eg: +1415555555 [+][country code][area code][local phone number]",
"HELP": "Phone number should be of E.164 format eg: +1415555555 [+][country code][area code][local phone number]. You can select the dial code from the dropdown.",
"ERROR": "Phone number should be either empty or of E.164 format",
"DIAL_CODE_ERROR": "Please select a dial code from the list",
"DUPLICATE": "This phone number is in use for another contact."
},
"LOCATION": {
@@ -132,6 +133,17 @@
"PLACEHOLDER": "Introdueix el nom de la companyia",
"LABEL": "Nom de la companyia"
},
"COUNTRY": {
"PLACEHOLDER": "Enter the country name",
"LABEL": "Nom del país",
"SELECT_PLACEHOLDER": "Select",
"REMOVE": "Suprimeix",
"SELECT_COUNTRY": "Select Country"
},
"CITY": {
"PLACEHOLDER": "Enter the city name",
"LABEL": "City Name"
},
"SOCIAL_PROFILES": {
"FACEBOOK": {
"PLACEHOLDER": "Introduïu el nom d'usuari de Facebook",
@@ -170,7 +182,8 @@
"LABEL": "To"
},
"INBOX": {
"LABEL": "Inbox",
"LABEL": "Via Inbox",
"PLACEHOLDER": "Choose source inbox",
"ERROR": "Select an inbox"
},
"SUBJECT": {
@@ -439,7 +439,8 @@
"LABEL": "Característiques",
"DISPLAY_FILE_PICKER": "Mostra el selector de fitxers al widget",
"DISPLAY_EMOJI_PICKER": "Mostra el selector d'emoji al widget",
"ALLOW_END_CONVERSATION": "Allow users to end conversation from the widget"
"ALLOW_END_CONVERSATION": "Allow users to end conversation from the widget",
"USE_INBOX_AVATAR_FOR_BOT": "Use inbox name and avatar for the bot"
},
"SETTINGS_POPUP": {
"MESSENGER_HEADING": "Script del missatger",
@@ -477,6 +478,8 @@
"WHATSAPP_SECTION_UPDATE_TITLE": "Update API Key",
"WHATSAPP_SECTION_UPDATE_PLACEHOLDER": "Enter the new API Key here",
"WHATSAPP_SECTION_UPDATE_BUTTON": "Actualitza",
"WHATSAPP_WEBHOOK_TITLE": "Webhook Verify Token",
"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"
},
"AUTO_ASSIGNMENT": {
@@ -164,6 +164,7 @@
"COMPONENTS": {
"CODE": {
"BUTTON_TEXT": "Copia",
"CODEPEN": "Open in CodePen",
"COPY_SUCCESSFUL": "El codi s'ha copiat al porta-retalls amb èxit"
},
"SHOW_MORE_BLOCK": {
@@ -202,6 +203,7 @@
"HOME": "Inici",
"AGENTS": "Agents",
"AGENT_BOTS": "Bots",
"AUDIT_LOGS": "Audit Logs",
"INBOXES": "Safates d'entrada",
"NOTIFICATIONS": "Notificacions",
"CANNED_RESPONSES": "Respostes predeterminades",
@@ -74,6 +74,11 @@
"LABEL": "E-mailová adresa",
"PLACEHOLDER": "Zadejte prosím e-mailovou adresu agenta"
},
"AGENT_AVAILABILITY": {
"LABEL": "Dostupnost",
"PLACEHOLDER": "Please select an availability status",
"ERROR": "Availability is required"
},
"SUBMIT": "Upravit agenta"
},
"BUTTON_TEXT": "Upravit",
@@ -0,0 +1,24 @@
{
"AUDIT_LOGS": {
"HEADER": "Audit Logs",
"HEADER_BTN_TXT": "Add Audit Logs",
"LOADING": "Fetching Audit Logs",
"SEARCH_404": "Neexistují žádné položky odpovídající tomuto dotazu",
"SIDEBAR_TXT": "<p><b>Audit Logs</b> </p><p> Audit Logs are trails for events and actions in a Chatwoot System. </p>",
"LIST": {
"404": "There are no Audit Logs available in this account.",
"TITLE": "Manage Audit Logs",
"DESC": "Audit Logs are trails for events and actions in a Chatwoot System.",
"TABLE_HEADER": [
"User",
"Action",
"IP adresa",
"Time"
]
},
"API": {
"SUCCESS_MESSAGE": "AuditLogs retrieved successfully",
"ERROR_MESSAGE": "Nelze se připojit k Woot serveru, opakujte akci později"
}
}
}
@@ -120,8 +120,9 @@
"PHONE_NUMBER": {
"PLACEHOLDER": "Zadejte telefonní číslo kontaktu",
"LABEL": "Telefonní číslo",
"HELP": "Phone number should be of E.164 format eg: +1415555555 [+][country code][area code][local phone number]",
"HELP": "Phone number should be of E.164 format eg: +1415555555 [+][country code][area code][local phone number]. You can select the dial code from the dropdown.",
"ERROR": "Phone number should be either empty or of E.164 format",
"DIAL_CODE_ERROR": "Please select a dial code from the list",
"DUPLICATE": "This phone number is in use for another contact."
},
"LOCATION": {
@@ -132,6 +133,17 @@
"PLACEHOLDER": "Zadejte název společnosti",
"LABEL": "Název společnosti"
},
"COUNTRY": {
"PLACEHOLDER": "Enter the country name",
"LABEL": "Country Name",
"SELECT_PLACEHOLDER": "Select",
"REMOVE": "Odebrat",
"SELECT_COUNTRY": "Select Country"
},
"CITY": {
"PLACEHOLDER": "Enter the city name",
"LABEL": "City Name"
},
"SOCIAL_PROFILES": {
"FACEBOOK": {
"PLACEHOLDER": "Zadejte uživatelské jméno na Facebooku",
@@ -170,7 +182,8 @@
"LABEL": "To"
},
"INBOX": {
"LABEL": "Inbox",
"LABEL": "Via Inbox",
"PLACEHOLDER": "Choose source inbox",
"ERROR": "Select an inbox"
},
"SUBJECT": {
@@ -439,7 +439,8 @@
"LABEL": "Funkce",
"DISPLAY_FILE_PICKER": "Display file picker on the widget",
"DISPLAY_EMOJI_PICKER": "Display emoji picker on the widget",
"ALLOW_END_CONVERSATION": "Allow users to end conversation from the widget"
"ALLOW_END_CONVERSATION": "Allow users to end conversation from the widget",
"USE_INBOX_AVATAR_FOR_BOT": "Use inbox name and avatar for the bot"
},
"SETTINGS_POPUP": {
"MESSENGER_HEADING": "Messenger skript",
@@ -477,6 +478,8 @@
"WHATSAPP_SECTION_UPDATE_TITLE": "Update API Key",
"WHATSAPP_SECTION_UPDATE_PLACEHOLDER": "Enter the new API Key here",
"WHATSAPP_SECTION_UPDATE_BUTTON": "Aktualizovat",
"WHATSAPP_WEBHOOK_TITLE": "Webhook Verify Token",
"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"
},
"AUTO_ASSIGNMENT": {
@@ -164,6 +164,7 @@
"COMPONENTS": {
"CODE": {
"BUTTON_TEXT": "Kopírovat",
"CODEPEN": "Open in CodePen",
"COPY_SUCCESSFUL": "Kód byl úspěšně zkopírován do schránky"
},
"SHOW_MORE_BLOCK": {
@@ -202,6 +203,7 @@
"HOME": "Domů",
"AGENTS": "Agenti",
"AGENT_BOTS": "Bots",
"AUDIT_LOGS": "Audit Logs",
"INBOXES": "Schránky",
"NOTIFICATIONS": "Oznámení",
"CANNED_RESPONSES": "Konzervované odpovědi",
@@ -74,6 +74,11 @@
"LABEL": "E-Mail Adresse",
"PLACEHOLDER": "Indtast venligst en e-mail adresse på agenten"
},
"AGENT_AVAILABILITY": {
"LABEL": "Tilgængelighed",
"PLACEHOLDER": "Please select an availability status",
"ERROR": "Availability is required"
},
"SUBMIT": "Rediger Agent"
},
"BUTTON_TEXT": "Rediger",
@@ -0,0 +1,24 @@
{
"AUDIT_LOGS": {
"HEADER": "Audit Logs",
"HEADER_BTN_TXT": "Add Audit Logs",
"LOADING": "Fetching Audit Logs",
"SEARCH_404": "Der er ingen elementer, der matcher denne forespørgsel",
"SIDEBAR_TXT": "<p><b>Audit Logs</b> </p><p> Audit Logs are trails for events and actions in a Chatwoot System. </p>",
"LIST": {
"404": "There are no Audit Logs available in this account.",
"TITLE": "Manage Audit Logs",
"DESC": "Audit Logs are trails for events and actions in a Chatwoot System.",
"TABLE_HEADER": [
"User",
"Action",
"Ip Adresse",
"Time"
]
},
"API": {
"SUCCESS_MESSAGE": "AuditLogs retrieved successfully",
"ERROR_MESSAGE": "Kunne ikke oprette forbindelse til Woot Server, Prøv igen senere"
}
}
}
@@ -120,8 +120,9 @@
"PHONE_NUMBER": {
"PLACEHOLDER": "Indtast telefonnummeret på kontaktpersonen",
"LABEL": "Telefonnummer",
"HELP": "Telefonnummer skal være af E.164 format fx: +1415555555 [+] [landekode] [områdekode] [lokalt telefonnummer]",
"HELP": "Phone number should be of E.164 format eg: +1415555555 [+][country code][area code][local phone number]. You can select the dial code from the dropdown.",
"ERROR": "Telefonnummer skal være enten tomt eller i E.164-format",
"DIAL_CODE_ERROR": "Please select a dial code from the list",
"DUPLICATE": "Dette telefonnummer er i brug for en anden kontakt."
},
"LOCATION": {
@@ -132,6 +133,17 @@
"PLACEHOLDER": "Indtast virksomhedens navn",
"LABEL": "Virksomhedens Navn"
},
"COUNTRY": {
"PLACEHOLDER": "Enter the country name",
"LABEL": "Land Navn",
"SELECT_PLACEHOLDER": "Vælg",
"REMOVE": "Fjern",
"SELECT_COUNTRY": "Select Country"
},
"CITY": {
"PLACEHOLDER": "Enter the city name",
"LABEL": "City Name"
},
"SOCIAL_PROFILES": {
"FACEBOOK": {
"PLACEHOLDER": "Indtast Facebook brugernavn",
@@ -170,7 +182,8 @@
"LABEL": "Til"
},
"INBOX": {
"LABEL": "Indbakke",
"LABEL": "Via Inbox",
"PLACEHOLDER": "Choose source inbox",
"ERROR": "Vælg en indbakke"
},
"SUBJECT": {
@@ -439,7 +439,8 @@
"LABEL": "Funktioner",
"DISPLAY_FILE_PICKER": "Vis filvælger på widget'en",
"DISPLAY_EMOJI_PICKER": "Vis emoji-vælger på widget'en",
"ALLOW_END_CONVERSATION": "Tillad brugere at afslutte samtale fra widget"
"ALLOW_END_CONVERSATION": "Tillad brugere at afslutte samtale fra widget",
"USE_INBOX_AVATAR_FOR_BOT": "Use inbox name and avatar for the bot"
},
"SETTINGS_POPUP": {
"MESSENGER_HEADING": "Messenger- Script",
@@ -477,6 +478,8 @@
"WHATSAPP_SECTION_UPDATE_TITLE": "Update API Key",
"WHATSAPP_SECTION_UPDATE_PLACEHOLDER": "Enter the new API Key here",
"WHATSAPP_SECTION_UPDATE_BUTTON": "Opdater",
"WHATSAPP_WEBHOOK_TITLE": "Webhook Verificér Token",
"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"
},
"AUTO_ASSIGNMENT": {
@@ -164,6 +164,7 @@
"COMPONENTS": {
"CODE": {
"BUTTON_TEXT": "Kopiér",
"CODEPEN": "Open in CodePen",
"COPY_SUCCESSFUL": "Kode kopieret til udklipsholder med succes"
},
"SHOW_MORE_BLOCK": {
@@ -202,6 +203,7 @@
"HOME": "Hjem",
"AGENTS": "Agenter",
"AGENT_BOTS": "Bots",
"AUDIT_LOGS": "Audit Logs",
"INBOXES": "Indbakker",
"NOTIFICATIONS": "Notifikationer",
"CANNED_RESPONSES": "Standardsvar Svar",
@@ -74,6 +74,11 @@
"LABEL": "E-Mail-Addresse",
"PLACEHOLDER": "Bitte geben Sie eine E-Mail-Adresse des Agenten ein"
},
"AGENT_AVAILABILITY": {
"LABEL": "Verfügbarkeit",
"PLACEHOLDER": "Please select an availability status",
"ERROR": "Availability is required"
},
"SUBMIT": "Agent bearbeiten"
},
"BUTTON_TEXT": "Bearbeiten",
@@ -0,0 +1,24 @@
{
"AUDIT_LOGS": {
"HEADER": "Audit Logs",
"HEADER_BTN_TXT": "Add Audit Logs",
"LOADING": "Fetching Audit Logs",
"SEARCH_404": "Es gibt keine Elemente, die dieser Abfrage entsprechen",
"SIDEBAR_TXT": "<p><b>Audit Logs</b> </p><p> Audit Logs are trails for events and actions in a Chatwoot System. </p>",
"LIST": {
"404": "There are no Audit Logs available in this account.",
"TITLE": "Manage Audit Logs",
"DESC": "Audit Logs are trails for events and actions in a Chatwoot System.",
"TABLE_HEADER": [
"User",
"Action",
"IP-Adresse",
"Time"
]
},
"API": {
"SUCCESS_MESSAGE": "AuditLogs retrieved successfully",
"ERROR_MESSAGE": "Es konnte keine Verbindung zum Woot Server hergestellt werden. Bitte versuchen Sie es später erneut"
}
}
}
@@ -120,8 +120,9 @@
"PHONE_NUMBER": {
"PLACEHOLDER": "Geben Sie die Telefonnummer des Kontakts ein",
"LABEL": "Telefonnummer",
"HELP": "Telefonnummer sollte im E.164-Format sein, z. B.: +1415555555 [+][Landesvorwahl][Landesvorwahl][Ortsvorwahl]",
"HELP": "Phone number should be of E.164 format eg: +1415555555 [+][country code][area code][local phone number]. You can select the dial code from the dropdown.",
"ERROR": "Telefonnummer muss leer sein oder im E.164-Format",
"DIAL_CODE_ERROR": "Please select a dial code from the list",
"DUPLICATE": "Diese Telefonnummer wird für einen anderen Kontakt verwendet."
},
"LOCATION": {
@@ -132,6 +133,17 @@
"PLACEHOLDER": "Firmenname eingeben",
"LABEL": "Firmenname"
},
"COUNTRY": {
"PLACEHOLDER": "Enter the country name",
"LABEL": "Ländername",
"SELECT_PLACEHOLDER": "Auswählen",
"REMOVE": "Entfernen",
"SELECT_COUNTRY": "Select Country"
},
"CITY": {
"PLACEHOLDER": "Enter the city name",
"LABEL": "City Name"
},
"SOCIAL_PROFILES": {
"FACEBOOK": {
"PLACEHOLDER": "Facebook-Benutzername eingeben",
@@ -170,7 +182,8 @@
"LABEL": "An"
},
"INBOX": {
"LABEL": "Posteingang",
"LABEL": "Via Inbox",
"PLACEHOLDER": "Choose source inbox",
"ERROR": "Posteingang auswählen"
},
"SUBJECT": {
@@ -439,7 +439,8 @@
"LABEL": "Funktionen",
"DISPLAY_FILE_PICKER": "Dateiauswahl im Widget anzeigen",
"DISPLAY_EMOJI_PICKER": "Emoji-Auswahl im Widget anzeigen",
"ALLOW_END_CONVERSATION": "Benutzern erlauben, die Konversation vom Widget zu beenden"
"ALLOW_END_CONVERSATION": "Benutzern erlauben, die Konversation vom Widget zu beenden",
"USE_INBOX_AVATAR_FOR_BOT": "Use inbox name and avatar for the bot"
},
"SETTINGS_POPUP": {
"MESSENGER_HEADING": "Messenger-Skript",
@@ -477,6 +478,8 @@
"WHATSAPP_SECTION_UPDATE_TITLE": "Update API Key",
"WHATSAPP_SECTION_UPDATE_PLACEHOLDER": "Enter the new API Key here",
"WHATSAPP_SECTION_UPDATE_BUTTON": "Aktualisieren",
"WHATSAPP_WEBHOOK_TITLE": "Webhook-Verifizierungstoken",
"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"
},
"AUTO_ASSIGNMENT": {
@@ -164,6 +164,7 @@
"COMPONENTS": {
"CODE": {
"BUTTON_TEXT": "Kopieren",
"CODEPEN": "Open in CodePen",
"COPY_SUCCESSFUL": "Code erfolgreich in die Zwischenablage kopiert"
},
"SHOW_MORE_BLOCK": {
@@ -202,6 +203,7 @@
"HOME": "Hauptseite",
"AGENTS": "Agenten",
"AGENT_BOTS": "Bots",
"AUDIT_LOGS": "Audit Logs",
"INBOXES": "Posteingänge",
"NOTIFICATIONS": "Push-Benachrichtigungen",
"CANNED_RESPONSES": "Vorgefertigte Antworten",
@@ -74,6 +74,11 @@
"LABEL": "Διεύθυνση Email",
"PLACEHOLDER": "Παρακαλώ εισάγετε το email του πράκτορα"
},
"AGENT_AVAILABILITY": {
"LABEL": "Διαθεσιμότητα",
"PLACEHOLDER": "Please select an availability status",
"ERROR": "Availability is required"
},
"SUBMIT": "Καταχώρηση"
},
"BUTTON_TEXT": "Επεξεργασία",
@@ -0,0 +1,24 @@
{
"AUDIT_LOGS": {
"HEADER": "Audit Logs",
"HEADER_BTN_TXT": "Add Audit Logs",
"LOADING": "Fetching Audit Logs",
"SEARCH_404": "Δεν υπάρχουν αντικείμενα να ταιριάζουν με αυτό το ερώτημα",
"SIDEBAR_TXT": "<p><b>Audit Logs</b> </p><p> Audit Logs are trails for events and actions in a Chatwoot System. </p>",
"LIST": {
"404": "There are no Audit Logs available in this account.",
"TITLE": "Manage Audit Logs",
"DESC": "Audit Logs are trails for events and actions in a Chatwoot System.",
"TABLE_HEADER": [
"User",
"Action",
"Διεύθυνση IP",
"Time"
]
},
"API": {
"SUCCESS_MESSAGE": "AuditLogs retrieved successfully",
"ERROR_MESSAGE": "Αδυναμία σύνδεσης με τον Woot Server, Παρακαλώ προσπαθήστε αργότερα"
}
}
}
@@ -120,8 +120,9 @@
"PHONE_NUMBER": {
"PLACEHOLDER": "Εισάγετε τον αριθμό τηλεφώνου της επαφής",
"LABEL": "Αριθμός Τηλεφώνου",
"HELP": "Ο αριθμός τηλεφώνου πρέπει να έχει μορφή E.164 π.χ.: +14155555 [+][κωδικός χώρας][κωδικός περιοχής][αριθμός τοπικού τηλεφώνου]",
"HELP": "Phone number should be of E.164 format eg: +1415555555 [+][country code][area code][local phone number]. You can select the dial code from the dropdown.",
"ERROR": "Ο αριθμός τηλεφώνου πρέπει να είναι είτε άδειος είτε με μορφή E.164",
"DIAL_CODE_ERROR": "Please select a dial code from the list",
"DUPLICATE": "Αυτός ο αριθμός τηλεφώνου χρησιμοποιείται σε άλλη επαφή."
},
"LOCATION": {
@@ -132,6 +133,17 @@
"PLACEHOLDER": "Εισάγετε το όνομα της εταιρείας",
"LABEL": "Όνομα Εταιρείας"
},
"COUNTRY": {
"PLACEHOLDER": "Enter the country name",
"LABEL": "Όνομα Χώρας",
"SELECT_PLACEHOLDER": "Επιλογή",
"REMOVE": "Διαγραφή",
"SELECT_COUNTRY": "Select Country"
},
"CITY": {
"PLACEHOLDER": "Enter the city name",
"LABEL": "City Name"
},
"SOCIAL_PROFILES": {
"FACEBOOK": {
"PLACEHOLDER": "Εισάγετε το όνομα χρήστη στο Facebook",
@@ -170,7 +182,8 @@
"LABEL": "Προς"
},
"INBOX": {
"LABEL": "Εισερχόμενα",
"LABEL": "Via Inbox",
"PLACEHOLDER": "Choose source inbox",
"ERROR": "Επιλέξτε ένα κιβώτιο εισερχόμενων"
},
"SUBJECT": {
@@ -439,7 +439,8 @@
"LABEL": "Χαρακτηριστικά",
"DISPLAY_FILE_PICKER": "Να εμφανίζεται η επιλογή αρχείου στο widget",
"DISPLAY_EMOJI_PICKER": "Να εμφανίζεται ο επιλογές emoji στο widget",
"ALLOW_END_CONVERSATION": "Επιτρέψτε στους χρήστες να τερματίσουν τη συνομιλία από το widget"
"ALLOW_END_CONVERSATION": "Επιτρέψτε στους χρήστες να τερματίσουν τη συνομιλία από το widget",
"USE_INBOX_AVATAR_FOR_BOT": "Use inbox name and avatar for the bot"
},
"SETTINGS_POPUP": {
"MESSENGER_HEADING": "Κώδικας (Script)",
@@ -477,6 +478,8 @@
"WHATSAPP_SECTION_UPDATE_TITLE": "Update API Key",
"WHATSAPP_SECTION_UPDATE_PLACEHOLDER": "Enter the new API Key here",
"WHATSAPP_SECTION_UPDATE_BUTTON": "Ενημέρωση",
"WHATSAPP_WEBHOOK_TITLE": "Token Επαλήθευσης Webhook",
"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"
},
"AUTO_ASSIGNMENT": {

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