Compare commits

...
Author SHA1 Message Date
Vishnu Narayanan cd016c5ad4 chore: log webhook response data on error 2023-11-24 14:18:13 +05:30
Sojan dcd18072b4 Merge branch 'release/3.3.1'
Publish Chatwoot CE docker images / build (push) Waiting to run
2023-11-18 10:24:25 -08:00
Sojan 4244945e7b Bump version to 3.3.1 2023-11-18 10:23:18 -08:00
Clairton Rodrigo HeinzenandGitHub 17725e4dd0 fix: Breakage of message pane when Openai integration isn't enabled (#8381)
Error when the Openai integration isn't enabled

fixes: https://github.com/chatwoot/chatwoot/issues/8379
2023-11-18 10:18:55 -08:00
Sojan d55bb63aa0 Merge branch 'release/3.3.0' into develop 2023-11-17 20:37:44 -08:00
Sojan bda2d1d9a4 Merge branch 'release/3.3.0'
Publish Chatwoot CE docker images / build (push) Waiting to run
2023-11-17 20:37:25 -08:00
Sojan 6e22695b85 Bump version to 3.3.0 2023-11-17 20:34:39 -08:00
Pranav Raj SandGitHub 146e46d79f fix: Disable AI Assist CTA if not required (#8375)
- Show CTAs only on production
2023-11-17 20:10:46 -08:00
Nithin David ThomasandGitHub 0af27a2387 feat: Adds support for logo in portal settings page [CW-2585] (#8354) 2023-11-17 19:58:27 -08:00
7380f0e7ce chore: Making OpenAI label suggestions optional (#8374)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2023-11-17 19:54:15 -08:00
Pranav Raj SandGitHub 2c5a0abcce fix: Update the height of the input based on the rows (#8373) 2023-11-17 17:29:44 -08:00
Sojan JoseandGitHub f23e33c1a9 perf: Remove unwanted conversation count query (#8372) 2023-11-17 16:40:40 -08:00
5588d6e344 feat: convert feature_flag to bigint [CW-2767] (#8350)
Co-authored-by: Sojan <sojan@pepalo.com>
2023-11-17 15:50:24 -08:00
59ace66c6f chore: [Snyk] Fix for vulnerabilities (#8340)
Co-authored-by: snyk-bot <snyk-bot@snyk.io>
2023-11-17 15:24:00 -08:00
Sojan JoseandGitHub decef1ad52 chore: Add rails_panel gem for query debugging (#8370) 2023-11-18 02:30:35 +05:30
Sojan JoseandGitHub 0e19a4196b chore: Endpoint to purge portal logo (#8365) 2023-11-18 01:13:22 +05:30
Muhsin KelothandGitHub 04dd65687b feat: Changes to include delivery reports for all channels (#8329) 2023-11-16 19:43:35 +05:30
Muhsin KelothandGitHub 5f503b1a57 feat: Change slack bot and activity message appearance (#8349) 2023-11-16 12:59:52 +05:30
Sojan JoseandGitHub 5f6e974531 chore: Add website_token into request logs (#8362)
- Log website_token into request logs to identify specific widgets
2023-11-15 20:32:21 -08:00
Sojan JoseandGitHub a023eabf59 chore: Add logging for facebook errors (#8360)
- Add logging for Facebook errors
2023-11-15 14:56:21 -08:00
Sivin VargheseandGitHub cb1e25a487 feat: Change the highlight in article page on scrolling (#8330) 2023-11-15 13:51:46 +05:30
Sojan JoseandGitHub bcf7c6c45a chore: Improve account seeding (#8357) 2023-11-15 13:29:00 +05:30
Shivam MishraandGitHub b411b6ca21 feat: remove feature flag checks for message reply to (#8352) 2023-11-15 13:05:58 +05:30
Shivam MishraandGitHub cd217a3f86 feat: allow automation rules to remove labels (#8346) 2023-11-13 13:10:55 +05:30
Pranav Raj SandGitHub 6c79642450 chore: Update dashboard screenshots and the logo (#8342) 2023-11-11 23:08:05 +05:30
Nithin David ThomasandGitHub 201d476720 chore: Add meta viewport tag to public portal (#8335) 2023-11-09 20:30:58 -08:00
Muhsin KelothandGitHub c1a2e67c9e fix: Change the route change logic based on bus events, after creating a new conversation in the widget. (#8328) 2023-11-10 09:44:59 +05:30
1948bef9ac fix: Fix line break issue with link in portal header (#8323)
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2023-11-08 20:38:38 -08:00
Sivin VargheseandGitHub 5486f39f7e feat: Update uncategorized block design (#8324) 2023-11-09 08:47:38 +05:30
f018b0013c feat: Updated the design of the article page (#8166)
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2023-11-08 18:05:13 -08:00
950f085e80 feat: Updated the design of the category page (#8165)
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2023-11-08 18:03:16 -08:00
268e26625b feat: Adds the ability to see the popular articles (#8152)
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2023-11-08 18:01:07 -08:00
7041d86e4d feat: Updated home page design (#8106)
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2023-11-08 17:59:03 -08:00
78398ee800 feat: Updated public portal header design (#8089)
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2023-11-08 17:56:59 -08:00
Sojan JoseandGitHub 7b09b02737 chore: Fix trigger message for response Bot (#8322)
- Fix the bug where wrong messages was used to search response sources
2023-11-08 17:02:48 -08:00
Muhsin KelothandGitHub af7631d9f1 chore: Update MarkMessagesAsReadJob to accept delivered status (#8319) 2023-11-08 13:44:57 -08:00
Sivin VargheseandGitHub e0f33e62af feat: Adds support for image resize in the message bubble (#8182) 2023-11-08 14:11:47 +05:30
5d224f2e04 feat: allow inbox specific flags for signature toggle (#8280)
Co-authored-by: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com>
2023-11-08 10:47:22 +05:30
Gabriel QuinaandGitHub 4cb981d81b fix: Semantics across portuguese translations (#8291) 2023-11-08 10:42:57 +05:30
f891cfbf47 chore: [Snyk] Security upgrade sidekiq-cron from 1.10.1 to 1.11.0 (#8294)
Co-authored-by: snyk-bot <snyk-bot@snyk.io>
2023-11-08 10:32:04 +05:30
Sojan JoseandGitHub e54c8a857d chore: Debug ChatGpt response (#8316)
- Add loggers to debug ChatGPT requests and responses
2023-11-07 20:07:29 -08:00
66229b0d6b chore: [Snyk] Security upgrade audited from 5.4.0 to 5.4.1 (#8305)
Co-authored-by: snyk-bot <snyk-bot@snyk.io>
2023-11-07 17:45:32 -08:00
Muhsin KelothandGitHub 3eb1d49577 fix: Handle all types of errors in API channel webhooks (#8307) 2023-11-07 12:54:49 +05:30
Vishnu NarayananandGitHub 677888bcde feat: Add job to remove stale contact inboxes (#8096) 2023-11-06 20:24:30 -08:00
abbb4180ea chore: Add facebook business_management scope (#8004)
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2023-11-06 18:37:41 -08:00
Pranav Raj SandGitHub d1cbc56d67 chore: Update widget config API to migrate the widget (#8303)
- Update the keys in the widget config API to support loading the widget separately from the Rails App.
2023-11-06 15:27:29 -08:00
EmanuelandGitHub b579ff832e chore: Update the case in the translation files (#8298) 2023-11-06 08:01:54 -08:00
Nithin David ThomasandGitHub 39d0748a5b feat: Lets users insert connected portal article into replies [CW-2282] (#8117)
- Lets users insert connected portal articles into replies

https://linear.app/chatwoot/issue/CW-2282/list-all-the-top-articles-from-the-connected-help-center
https://linear.app/chatwoot/issue/CW-1453/container-view-for-showing-search-input-and-result-items
2023-11-04 02:57:25 -07:00
Muhsin KelothandGitHub b4d20689b7 feat: Mark the messages as failed if the API channel webhooks fail for any reason. (#8277) 2023-11-04 12:26:28 +05:30
3b84b0fc47 feat: Creates 404 page for public portal [CW-2727] (#8293)
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2023-11-04 04:59:44 +05:30
Nithin David ThomasandGitHub 23ea829510 chore: Refactors help center article url helper (#8269) 2023-11-04 04:18:34 +05:30
ebe9daea00 chore: Swagger documentation for contact labels APIs (#8279)
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2023-11-03 15:09:12 +05:30
Shivam MishraandGitHub 36b6c0cb9c feat: support image height in markdown rendering of messages (#8177)
- This PR adds BaseMarkdownRenderer, it takes all the required attributes from the image node, parses the cw_image_height query and renders it.
2023-11-02 13:51:54 -07:00
Muhsin KelothandGitHub 16c36a78f0 chore: Remove sentry exceptions for the message status failed errors (#8285)
- Remove sending exceptions to Sentry after capturing the message failed errors.
2023-11-02 12:00:22 -07:00
Nithin David ThomasandGitHub 4999f7ed23 fix: Fixes wrong page size in category filter in articles page (#8270) 2023-11-02 15:19:19 +05:30
Chatwoot BotandGitHub e8f4ac632f chore: Update translations (#8255) 2023-11-01 15:13:32 -07:00
3c7854ccdf fix: Fixes error in ChatList component (#8266)
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2023-11-02 03:23:29 +05:30
Pranav Raj SandGitHub 1e70223ed7 chore: Upgrade Cypress to 13.4.0 (#8271) 2023-10-31 19:39:34 -07:00
f34cd3ea61 chore: [Snyk] Security upgrade newrelic-sidekiq-metrics from 1.6.1 to 1.6.2 (#8254)
Co-authored-by: snyk-bot <snyk-bot@snyk.io>
2023-10-31 18:24:18 -07:00
Muhsin KelothandGitHub 68b8d721a7 fix: Update the message status to failed if the outgoing Facebook message fails. (#8258) 2023-10-31 17:40:17 -07:00
Nithin David ThomasandGitHub d876419b27 fix: Update text area height while typing [CW-2438] (#8268) 2023-10-31 17:39:25 -07:00
8455186e9f feat: allow instagram reply_to [CW-2609] (#8248)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2023-10-31 17:31:12 -07:00
b6584ec68b feat: toggle reply to on widget based on feature flag (#8261)
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2023-10-31 21:33:14 +05:30
Muhsin KelothandGitHub 29f18c7f18 fix: Handle Twilio::REST::RestError (#8257) 2023-10-31 16:23:34 +05:30
Muhsin KelothandGitHub 11b27f9805 feat: Handle Line send message/attachments errors (#8200) 2023-10-31 07:42:30 +05:30
Pranav Raj SandGitHub 653e0335c0 fix: Handle PermissionDeniedError for Dialogflow processor (#8252) 2023-10-30 13:24:03 -07:00
013dab7da6 fix: Add support for dark mode for the calendars (date and time picker). (#8250)
Co-authored-by: Liam Ashdown <liam.ashdown@debbiesvillas.co.uk>
2023-10-30 11:48:16 -07:00
Pranav Raj SandGitHub 2ba81830f3 fix: Update Instagram story rendering (#8240) 2023-10-30 11:16:14 -07:00
Sivin VargheseandGitHub dfba4770bd feat: Save sort Conversations filter (#8237) 2023-10-30 15:25:21 +05:30
LiamandGitHub 63e702ca12 fix: Fix an issue where Chat Widget Unread Message is not correctly d… (#8245) 2023-10-30 12:52:26 +05:30
Will KramerandGitHub 394fece846 docs: Update .env.example for clearer inbound Postmark instructions (#8235)
Updated the .env.example file to provide clearer instructions for setting up the Postmark inbound webhook. Many users, including myself, found the initial instructions ambiguous, leading to difficulties during setup. This change aims to simplify the process for future users by providing clearer instructions and examples.

Fixes: #8234
2023-10-27 16:06:27 -07:00
Muhsin KelothandGitHub 24fbab94c3 chore: Refactor MarkMessagesAsReadJob based on conversation id and time stamp (#8217)
- Mark all messages as read by providing the conversation ID and timestamp.
- For Instagram, ensure all previous messages that weren't marked as failed are now marked as read. This is because the read events are only triggered for the most recent message and not for any previous ones.
2023-10-27 15:21:39 -07:00
Muhsin KelothandGitHub 61e03fa33a feat: Bandwidth sms channel delivery reports (#8198)
Fixes: https://linear.app/chatwoot/issue/CW-2566/delivery-report-for-bandwidth-sms
2023-10-27 14:07:15 -07:00
Pranav Raj SandGitHub 6c4b92f7f6 fix: Revert the changes for SyncCustomFilterCountJob (#8238) 2023-10-27 10:45:48 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
f023325b0e chore(deps): bump browserify-sign from 4.2.1 to 4.2.2 (#8233)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-27 07:39:48 -07:00
dd05482cb0 chore: update UI copy (#8229)
Co-authored-by: Hricha Shandily <hrichashandily@gmail.com>
2023-10-27 16:05:42 +05:30
d94108bf3f feat: show ReplyTo in widget UI (#8094)
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2023-10-27 13:35:02 +05:30
ab872beb1d feat: Helper to support dynamic system theme in public portal (#8206)
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2023-10-26 12:59:51 +05:30
d02cfff4cc fix: canned responses not working when signature is present (#8176)
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2023-10-26 11:35:46 +05:30
Robert CokerandGitHub 736f269c49 fix: Always reset CC/BCC emails when switching conversations (#8157) 2023-10-25 21:42:36 -07:00
Pranav Raj SandGitHub b067acfd4a fix: Update the alignment of the info icon on contact info panel (#8216) 2023-10-25 21:30:14 -07:00
Sivin VargheseandGitHub 232fa6fd18 fix: Revert missing files for search (#8215) 2023-10-26 09:08:28 +05:30
Chatwoot BotandGitHub 483d55df91 chore: Update translations (#8161) 2023-10-25 19:20:30 -07:00
3e54d3654b feat: Updated the search result fly-out menu design (#8203)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2023-10-25 19:16:49 -07:00
Sojan JoseandGitHub b6831d464e chore: Update API documentation for whatsapp templates (#8211)
- Update API documentation for whatsapp templates
2023-10-25 17:06:58 -07:00
Sojan JoseandGitHub 8c67ea56e7 chore: Campaign conversations should be created in open state (#8209)
- Ensure that conversations originating from campaigns are created in the open state.
2023-10-25 13:03:08 -07:00
Muhsin KelothandGitHub 54bc4c23df fix: Telegram model specs (#8207) 2023-10-26 00:14:09 +05:30
Muhsin KelothandGitHub 58f47eb02c feat: Handle Facebook send message/attachments errors (#8197) 2023-10-25 08:59:25 -07:00
4fc62ed9ae feat: Handle Instagram send message/attachments errors (#8174)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2023-10-25 08:56:09 -07:00
4a89bab23a feat: Handle Telegram send message/attachments errors (#8173)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2023-10-25 08:36:31 -07:00
Muhsin KelothandGitHub 7438f3b157 fix: Rescue slack ChannelNotFound error (#8196) 2023-10-25 13:53:30 +05:30
Shivam MishraandGitHub 9551dbc25f feat: add gtm to vueapp (#8154) 2023-10-24 15:51:21 +05:30
Sivin VargheseandGitHub 2e54f4a67c feat: Adds an event for the Google Translate usage (#8175) 2023-10-24 15:34:51 +05:30
Shivam MishraandGitHub 6d9b2a6b33 refactor: UI flags for Reply To (#8147) 2023-10-20 14:19:19 +05:30
Sivin VargheseandGitHub 35a9acf099 feat: Adds the ability to set an emoji for help center category (#8111) 2023-10-20 13:52:30 +05:30
b9694a0818 feat: support reply to for Telegram (#8105)
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2023-10-20 13:14:20 +05:30
Shivam MishraandGitHub 7416bbb25e feat: support reply to for outgoing message in WhatsApp (#8107)
- This PR enables replies to WhatsApp.
2023-10-19 13:24:46 -07:00
Sojan 83231b9678 Merge branch 'release/3.2.0'
Publish Chatwoot CE docker images / build (push) Waiting to run
2023-10-17 17:55:34 -07:00
407 changed files with 6152 additions and 2541 deletions
+20 -1
View File
@@ -81,7 +81,7 @@ SMTP_OPENSSL_VERIFY_MODE=peer
# Mail Incoming # Mail Incoming
# This is the domain set for the reply emails when conversation continuity is enabled # This is the domain set for the reply emails when conversation continuity is enabled
MAILER_INBOUND_EMAIL_DOMAIN= MAILER_INBOUND_EMAIL_DOMAIN=
# Set this to appropriate ingress channel with regards to incoming emails # Set this to the appropriate ingress channel with regards to incoming emails
# Possible values are : # Possible values are :
# relay for Exim, Postfix, Qmail # relay for Exim, Postfix, Qmail
# mailgun for Mailgun # mailgun for Mailgun
@@ -91,10 +91,20 @@ MAILER_INBOUND_EMAIL_DOMAIN=
RAILS_INBOUND_EMAIL_SERVICE= RAILS_INBOUND_EMAIL_SERVICE=
# Use one of the following based on the email ingress service # Use one of the following based on the email ingress service
# Ref: https://edgeguides.rubyonrails.org/action_mailbox_basics.html # Ref: https://edgeguides.rubyonrails.org/action_mailbox_basics.html
# Set this to a password of your choice and use it in the Inbound webhook
RAILS_INBOUND_EMAIL_PASSWORD= RAILS_INBOUND_EMAIL_PASSWORD=
MAILGUN_INGRESS_SIGNING_KEY= MAILGUN_INGRESS_SIGNING_KEY=
MANDRILL_INGRESS_API_KEY= MANDRILL_INGRESS_API_KEY=
# Creating Your Inbound Webhook Instructions for Postmark and Sendgrid:
# Inbound webhook URL format:
# https://actionmailbox:[YOUR_RAILS_INBOUND_EMAIL_PASSWORD]@[YOUR_CHATWOOT_DOMAIN.COM]/rails/action_mailbox/[RAILS_INBOUND_EMAIL_SERVICE]/inbound_emails
# Note: Replace the values inside the brackets; do not include the brackets themselves.
# Example: https://actionmailbox:mYRandomPassword3@chatwoot.example.com/rails/action_mailbox/postmark/inbound_emails
# For Postmark
# Ensure the 'Include raw email content in JSON payload' checkbox is selected in the inbound webhook section.
# Storage # Storage
ACTIVE_STORAGE_SERVICE=local ACTIVE_STORAGE_SERVICE=local
@@ -176,6 +186,9 @@ ANDROID_SHA256_CERT_FINGERPRINT=AC:73:8E:DE:EB:56:EA:CC:10:87:02:A7:65:37:7B:38:
# MICROSOFT CLARITY # MICROSOFT CLARITY
# MS_CLARITY_TOKEN=xxxxxxxxx # MS_CLARITY_TOKEN=xxxxxxxxx
# GOOGLE_TAG_MANAGER
# GOOGLE_TAG = GTM-XXXXXXX
## Scout ## Scout
## https://scoutapm.com/docs/ruby/configuration ## https://scoutapm.com/docs/ruby/configuration
# SCOUT_KEY=YOURKEY # SCOUT_KEY=YOURKEY
@@ -240,3 +253,9 @@ AZURE_APP_SECRET=
# Sentiment analysis model file path # Sentiment analysis model file path
SENTIMENT_FILE_PATH= SENTIMENT_FILE_PATH=
# Housekeeping/Performance related configurations
# Set to true if you want to remove stale contact inboxes
# contact_inboxes with no conversation older than 90 days will be removed
# REMOVE_STALE_CONTACT_INBOX_JOB_STATUS=false
+1
View File
@@ -4,6 +4,7 @@ module.exports = {
'prettier', 'prettier',
'plugin:vue/recommended', 'plugin:vue/recommended',
'plugin:storybook/recommended', 'plugin:storybook/recommended',
'plugin:cypress/recommended',
], ],
parserOptions: { parserOptions: {
parser: '@babel/eslint-parser', parser: '@babel/eslint-parser',
+7 -5
View File
@@ -107,16 +107,16 @@ gem 'google-cloud-translate-v3'
gem 'ddtrace', require: false gem 'ddtrace', require: false
gem 'elastic-apm', require: false gem 'elastic-apm', require: false
gem 'newrelic_rpm', require: false gem 'newrelic_rpm', require: false
gem 'newrelic-sidekiq-metrics', require: false gem 'newrelic-sidekiq-metrics', '>= 1.6.2', require: false
gem 'scout_apm', require: false gem 'scout_apm', require: false
gem 'sentry-rails', '>= 5.12.0', require: false gem 'sentry-rails', '>= 5.13.0', require: false
gem 'sentry-ruby', require: false gem 'sentry-ruby', require: false
gem 'sentry-sidekiq', '>= 5.12.0', require: false gem 'sentry-sidekiq', '>= 5.13.0', require: false
##-- background job processing --## ##-- background job processing --##
gem 'sidekiq', '>= 7.1.3' gem 'sidekiq', '>= 7.1.3'
# We want cron jobs # We want cron jobs
gem 'sidekiq-cron', '>= 1.10.1' gem 'sidekiq-cron', '>= 1.11.0'
##-- Push notification service --## ##-- Push notification service --##
gem 'fcm' gem 'fcm'
@@ -159,7 +159,7 @@ gem 'lograge', '~> 0.14.0', require: false
# worked with microsoft refresh token # worked with microsoft refresh token
gem 'omniauth-oauth2' gem 'omniauth-oauth2'
gem 'audited', '~> 5.4', '>= 5.4.0' gem 'audited', '~> 5.4', '>= 5.4.1'
# need for google auth # need for google auth
gem 'omniauth' gem 'omniauth'
@@ -200,6 +200,8 @@ group :development do
# profiling # profiling
gem 'rack-mini-profiler', '>= 3.1.1', require: false gem 'rack-mini-profiler', '>= 3.1.1', require: false
gem 'stackprof' gem 'stackprof'
# Should install the associated chrome extension to view query logs
gem 'meta_request'
end end
group :test do group :test do
+33 -25
View File
@@ -126,9 +126,9 @@ GEM
rake (>= 10.4, < 14.0) rake (>= 10.4, < 14.0)
ast (2.4.2) ast (2.4.2)
attr_extras (7.1.0) attr_extras (7.1.0)
audited (5.4.0) audited (5.4.1)
activerecord (>= 5.0, < 7.2) activerecord (>= 5.0, < 7.7)
request_store (~> 1.2) activesupport (>= 5.0, < 7.7)
aws-eventstream (1.2.0) aws-eventstream (1.2.0)
aws-partitions (1.760.0) aws-partitions (1.760.0)
aws-sdk-core (3.171.1) aws-sdk-core (3.171.1)
@@ -148,6 +148,7 @@ GEM
barnes (0.0.9) barnes (0.0.9)
multi_json (~> 1) multi_json (~> 1)
statsd-ruby (~> 1.1) statsd-ruby (~> 1.1)
base64 (0.1.1)
bcrypt (3.1.19) bcrypt (3.1.19)
bindex (0.8.1) bindex (0.8.1)
blingfire (0.1.8) blingfire (0.1.8)
@@ -174,7 +175,7 @@ GEM
rexml rexml
crass (1.0.6) crass (1.0.6)
csv-safe (3.2.1) csv-safe (3.2.1)
cypress-on-rails (1.13.1) cypress-on-rails (1.16.0)
rack rack
database_cleaner (2.0.2) database_cleaner (2.0.2)
database_cleaner-active_record (>= 2, < 3) database_cleaner-active_record (>= 2, < 3)
@@ -261,7 +262,7 @@ GEM
rake rake
flag_shih_tzu (0.3.23) flag_shih_tzu (0.3.23)
foreman (0.87.2) foreman (0.87.2)
fugit (1.8.1) fugit (1.9.0)
et-orbi (~> 1, >= 1.2.7) et-orbi (~> 1, >= 1.2.7)
raabro (~> 1.4) raabro (~> 1.4)
gapic-common (0.18.0) gapic-common (0.18.0)
@@ -450,13 +451,16 @@ GEM
marcel (1.0.2) marcel (1.0.2)
maxminddb (0.1.22) maxminddb (0.1.22)
memoist (0.16.2) memoist (0.16.2)
meta_request (0.7.4)
rack-contrib (>= 1.1, < 3)
railties (>= 3.0.0, < 7.1)
method_source (1.0.0) method_source (1.0.0)
mime-types (3.4.1) mime-types (3.4.1)
mime-types-data (~> 3.2015) mime-types-data (~> 3.2015)
mime-types-data (3.2023.0218.1) mime-types-data (3.2023.0218.1)
mini_magick (4.12.0) mini_magick (4.12.0)
mini_mime (1.1.5) mini_mime (1.1.5)
mini_portile2 (2.8.4) mini_portile2 (2.8.5)
minitest (5.20.0) minitest (5.20.0)
mock_redis (0.36.0) mock_redis (0.36.0)
ruby2_keywords ruby2_keywords
@@ -478,10 +482,11 @@ GEM
net-smtp (0.3.3) net-smtp (0.3.3)
net-protocol net-protocol
netrc (0.11.0) netrc (0.11.0)
newrelic-sidekiq-metrics (1.6.1) newrelic-sidekiq-metrics (1.6.2)
newrelic_rpm (~> 8) newrelic_rpm (>= 8.0.0)
sidekiq sidekiq
newrelic_rpm (8.16.0) newrelic_rpm (9.6.0)
base64
nio4r (2.5.9) nio4r (2.5.9)
nokogiri (1.15.4) nokogiri (1.15.4)
mini_portile2 (~> 2.8.2) mini_portile2 (~> 2.8.2)
@@ -553,10 +558,12 @@ GEM
pundit (2.3.0) pundit (2.3.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
raabro (1.4.0) raabro (1.4.0)
racc (1.7.1) racc (1.7.3)
rack (2.2.8) rack (2.2.8)
rack-attack (6.7.0) rack-attack (6.7.0)
rack (>= 1.0, < 4) rack (>= 1.0, < 4)
rack-contrib (2.4.0)
rack (< 4)
rack-cors (2.0.1) rack-cors (2.0.1)
rack (>= 2.0.0) rack (>= 2.0.0)
rack-mini-profiler (3.1.1) rack-mini-profiler (3.1.1)
@@ -597,13 +604,13 @@ GEM
thor (~> 1.0) thor (~> 1.0)
zeitwerk (~> 2.5) zeitwerk (~> 2.5)
rainbow (3.1.1) rainbow (3.1.1)
rake (13.0.6) rake (13.1.0)
rb-fsevent (0.11.2) rb-fsevent (0.11.2)
rb-inotify (0.10.1) rb-inotify (0.10.1)
ffi (~> 1.0) ffi (~> 1.0)
redis (5.0.6) redis (5.0.6)
redis-client (>= 0.9.0) redis-client (>= 0.9.0)
redis-client (0.17.0) redis-client (0.18.0)
connection_pool connection_pool
redis-namespace (1.10.0) redis-namespace (1.10.0)
redis (>= 4) redis (>= 4)
@@ -702,23 +709,23 @@ GEM
activesupport (>= 4) activesupport (>= 4)
selectize-rails (0.12.6) selectize-rails (0.12.6)
semantic_range (3.0.0) semantic_range (3.0.0)
sentry-rails (5.12.0) sentry-rails (5.13.0)
railties (>= 5.0) railties (>= 5.0)
sentry-ruby (~> 5.12.0) sentry-ruby (~> 5.13.0)
sentry-ruby (5.12.0) sentry-ruby (5.13.0)
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
sentry-sidekiq (5.12.0) sentry-sidekiq (5.13.0)
sentry-ruby (~> 5.12.0) sentry-ruby (~> 5.13.0)
sidekiq (>= 3.0) sidekiq (>= 3.0)
sexp_processor (4.17.0) sexp_processor (4.17.0)
shoulda-matchers (5.3.0) shoulda-matchers (5.3.0)
activesupport (>= 5.2.0) activesupport (>= 5.2.0)
sidekiq (7.1.6) sidekiq (7.2.0)
concurrent-ruby (< 2) concurrent-ruby (< 2)
connection_pool (>= 2.3.0) connection_pool (>= 2.3.0)
rack (>= 2.2.4) rack (>= 2.2.4)
redis-client (>= 0.14.0) redis-client (>= 0.14.0)
sidekiq-cron (1.10.1) sidekiq-cron (1.11.0)
fugit (~> 1.8) fugit (~> 1.8)
globalid (>= 1.0.1) globalid (>= 1.0.1)
sidekiq (>= 6) sidekiq (>= 6)
@@ -758,7 +765,7 @@ GEM
stripe (8.5.0) stripe (8.5.0)
telephone_number (1.4.20) telephone_number (1.4.20)
test-prof (1.2.1) test-prof (1.2.1)
thor (1.2.2) thor (1.3.0)
tilt (2.2.0) tilt (2.2.0)
time_diff (0.3.0) time_diff (0.3.0)
activesupport activesupport
@@ -838,7 +845,7 @@ DEPENDENCIES
administrate-field-belongs_to_search administrate-field-belongs_to_search
annotate annotate
attr_extras attr_extras
audited (~> 5.4, >= 5.4.0) audited (~> 5.4, >= 5.4.1)
aws-sdk-s3 aws-sdk-s3
azure-storage-blob! azure-storage-blob!
barnes barnes
@@ -893,9 +900,10 @@ DEPENDENCIES
listen listen
lograge (~> 0.14.0) lograge (~> 0.14.0)
maxminddb maxminddb
meta_request
mock_redis mock_redis
neighbor neighbor
newrelic-sidekiq-metrics newrelic-sidekiq-metrics (>= 1.6.2)
newrelic_rpm newrelic_rpm
omniauth omniauth
omniauth-google-oauth2 omniauth-google-oauth2
@@ -927,12 +935,12 @@ DEPENDENCIES
scout_apm scout_apm
scss_lint scss_lint
seed_dump seed_dump
sentry-rails (>= 5.12.0) sentry-rails (>= 5.13.0)
sentry-ruby sentry-ruby
sentry-sidekiq (>= 5.12.0) sentry-sidekiq (>= 5.13.0)
shoulda-matchers shoulda-matchers
sidekiq (>= 7.1.3) sidekiq (>= 7.1.3)
sidekiq-cron (>= 1.10.1) sidekiq-cron (>= 1.11.0)
simplecov (= 0.17.1) simplecov (= 0.17.1)
slack-ruby-client (~> 2.2.0) slack-ruby-client (~> 2.2.0)
spring spring
+2 -2
View File
@@ -1,3 +1,3 @@
backend: RAILS_ENV=test bin/rails s -p 5050 backend: RAILS_ENV=test bin/rails s -p 5050
frontend: bin/webpack-dev-server frontend: export NODE_OPTIONS=--openssl-legacy-provider && bin/webpack-dev-server
worker: dotenv RAILS_ENV=test bundle exec sidekiq -C config/sidekiq.yml worker: RAILS_ENV=test dotenv bundle exec sidekiq -C config/sidekiq.yml
+10 -11
View File
@@ -1,10 +1,12 @@
<p align="center"> <img src="https://user-images.githubusercontent.com/2246121/282256557-1570674b-d142-4198-9740-69404cc6a339.png#gh-light-mode-only" width="100%" alt="Chat dashboard dark mode"/>
<img src="https://s3.us-west-2.amazonaws.com/gh-assets.chatwoot.com/brand.svg" alt="Woot-logo" width="240" /> <img src="https://user-images.githubusercontent.com/2246121/282256632-87f6a01b-6467-4e0e-8a93-7bbf66d03a17.png#gh-dark-mode-only" width="100%" alt="Chat dashboard"/>
<p align="center">Customer engagement suite, an open-source alternative to Intercom, Zendesk, Salesforce Service Cloud etc.</p> ___
</p>
<p align="center"> # Chatwoot
Customer engagement suite, an open-source alternative to Intercom, Zendesk, Salesforce Service Cloud etc.
<p>
<a href="https://heroku.com/deploy?template=https://github.com/chatwoot/chatwoot/tree/master" alt="Deploy to Heroku"> <a href="https://heroku.com/deploy?template=https://github.com/chatwoot/chatwoot/tree/master" alt="Deploy to Heroku">
<img width="150" alt="Deploy" src="https://www.herokucdn.com/deploy/button.svg"/> <img width="150" alt="Deploy" src="https://www.herokucdn.com/deploy/button.svg"/>
</a> </a>
@@ -13,9 +15,7 @@
</a> </a>
</p> </p>
___ <p>
<p align="center">
<a href="https://codeclimate.com/github/chatwoot/chatwoot/maintainability"><img src="https://api.codeclimate.com/v1/badges/e6e3f66332c91e5a4c0c/maintainability" alt="Maintainability"></a> <a href="https://codeclimate.com/github/chatwoot/chatwoot/maintainability"><img src="https://api.codeclimate.com/v1/badges/e6e3f66332c91e5a4c0c/maintainability" alt="Maintainability"></a>
<img src="https://img.shields.io/circleci/build/github/chatwoot/chatwoot" alt="CircleCI Badge"> <img src="https://img.shields.io/circleci/build/github/chatwoot/chatwoot" alt="CircleCI Badge">
<a href="https://hub.docker.com/r/chatwoot/chatwoot/"><img src="https://img.shields.io/docker/pulls/chatwoot/chatwoot" alt="Docker Pull Badge"></a> <a href="https://hub.docker.com/r/chatwoot/chatwoot/"><img src="https://img.shields.io/docker/pulls/chatwoot/chatwoot" alt="Docker Pull Badge"></a>
@@ -29,9 +29,8 @@ ___
<a href="https://artifacthub.io/packages/helm/chatwoot/chatwoot"><img src="https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/artifact-hub" alt="Artifact HUB"></a> <a href="https://artifacthub.io/packages/helm/chatwoot/chatwoot"><img src="https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/artifact-hub" alt="Artifact HUB"></a>
</p> </p>
<img src="https://chatwoot-public-assets.s3.amazonaws.com/github/screenshot.png" width="100%" alt="Chat dashboard"/> <img src="https://user-images.githubusercontent.com/2246121/282255783-ee8a50c9-f42d-4752-8201-2d59965a663d.png#gh-light-mode-only" width="100%" alt="Chat dashboard dark mode"/>
<img src="https://user-images.githubusercontent.com/2246121/282255784-3d1994ec-d895-4ff5-ac68-d819987e1869.png#gh-dark-mode-only" width="100%" alt="Chat dashboard"/>
Chatwoot is an open-source, self-hosted customer engagement suite. Chatwoot lets you view and manage your customer data, communicate with them irrespective of which medium they use, and re-engage them based on their profile. Chatwoot is an open-source, self-hosted customer engagement suite. Chatwoot lets you view and manage your customer data, communicate with them irrespective of which medium they use, and re-engage them based on their profile.
@@ -76,6 +76,10 @@ class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder
message[:reply_to][:story] if message[:reply_to].present? && message[:reply_to][:story].present? message[:reply_to][:story] if message[:reply_to].present? && message[:reply_to][:story].present?
end end
def message_reply_attributes
message[:reply_to][:mid] if message[:reply_to].present? && message[:reply_to][:mid].present?
end
def build_message def build_message
return if @outgoing_echo && already_sent_from_chatwoot? return if @outgoing_echo && already_sent_from_chatwoot?
return if message_content.blank? && all_unsupported_files? return if message_content.blank? && all_unsupported_files?
@@ -118,7 +122,10 @@ class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder
message_type: message_type, message_type: message_type,
source_id: message_identifier, source_id: message_identifier,
content: message_content, content: message_content,
sender: @outgoing_echo ? nil : contact sender: @outgoing_echo ? nil : contact,
content_attributes: {
in_reply_to_external_id: message_reply_attributes
}
} }
end end
+35 -3
View File
@@ -9,11 +9,11 @@ class Messages::MessageBuilder
@user = user @user = user
@message_type = params[:message_type] || 'outgoing' @message_type = params[:message_type] || 'outgoing'
@attachments = params[:attachments] @attachments = params[:attachments]
@automation_rule = @params&.dig(:content_attributes, :automation_rule_id) @automation_rule = content_attributes&.dig(:automation_rule_id)
return unless params.instance_of?(ActionController::Parameters) return unless params.instance_of?(ActionController::Parameters)
@in_reply_to = params.to_unsafe_h&.dig(:content_attributes, :in_reply_to) @in_reply_to = content_attributes&.dig(:in_reply_to)
@items = params.to_unsafe_h&.dig(:content_attributes, :items) @items = content_attributes&.dig(:items)
end end
def perform def perform
@@ -26,6 +26,38 @@ class Messages::MessageBuilder
private private
# Extracts content attributes from the given params.
# - Converts ActionController::Parameters to a regular hash if needed.
# - Attempts to parse a JSON string if content is a string.
# - Returns an empty hash if content is not present, if there's a parsing error, or if it's an unexpected type.
def content_attributes
params = convert_to_hash(@params)
content_attributes = params.fetch(:content_attributes, {})
return parse_json(content_attributes) if content_attributes.is_a?(String)
return content_attributes if content_attributes.is_a?(Hash)
{}
end
# Converts the given object to a hash.
# If it's an instance of ActionController::Parameters, converts it to an unsafe hash.
# Otherwise, returns the object as-is.
def convert_to_hash(obj)
return obj.to_unsafe_h if obj.instance_of?(ActionController::Parameters)
obj
end
# Attempts to parse a string as JSON.
# If successful, returns the parsed hash with symbolized names.
# If unsuccessful, returns nil.
def parse_json(content)
JSON.parse(content, symbolize_names: true)
rescue JSON::ParserError
{}
end
def process_attachments def process_attachments
return if @attachments.blank? return if @attachments.blank?
@@ -10,7 +10,7 @@ class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
@articles_count = @all_articles.count @articles_count = @all_articles.count
@articles = if list_params[:category_slug].present? @articles = if list_params[:category_slug].present?
@all_articles.order_by_position.page(@current_page).per(50) @all_articles.order_by_position.page(@current_page)
else else
@all_articles.order_by_updated_at.page(@current_page) @all_articles.order_by_updated_at.page(@current_page)
end end
@@ -48,7 +48,7 @@ class Api::V1::Accounts::CategoriesController < Api::V1::Accounts::BaseControlle
def category_params def category_params
params.require(:category).permit( params.require(:category).permit(
:name, :description, :position, :slug, :locale, :parent_category_id, :associated_category_id :name, :description, :position, :slug, :locale, :icon, :parent_category_id, :associated_category_id
) )
end end
@@ -47,6 +47,11 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
head :ok head :ok
end end
def logo
@portal.logo.purge if @portal.logo.attached?
head :ok
end
def process_attached_logo def process_attached_logo
blob_id = params[:blob_id] blob_id = params[:blob_id]
blob = ActiveStorage::Blob.find_by(id: blob_id) blob = ActiveStorage::Blob.find_by(id: blob_id)
@@ -79,6 +79,9 @@ class Api::V1::Widget::BaseController < ApplicationController
sender: @contact, sender: @contact,
content: permitted_params[:message][:content], content: permitted_params[:message][:content],
inbox_id: conversation.inbox_id, inbox_id: conversation.inbox_id,
content_attributes: {
in_reply_to: permitted_params[:message][:reply_to]
},
echo_id: permitted_params[:message][:echo_id], echo_id: permitted_params[:message][:echo_id],
message_type: :incoming message_type: :incoming
} }
@@ -28,7 +28,7 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
conversation.contact_last_seen_at = DateTime.now.utc conversation.contact_last_seen_at = DateTime.now.utc
conversation.save! conversation.save!
::Conversations::MarkMessagesAsReadJob.perform_later(conversation) ::Conversations::UpdateMessageStatusJob.perform_later(conversation.id, conversation.contact_last_seen_at)
head :ok head :ok
end end
@@ -64,7 +64,7 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
def permitted_params def permitted_params
# timestamp parameter is used in create conversation method # timestamp parameter is used in create conversation method
params.permit(:id, :before, :after, :website_token, contact: [:name, :email], message: [:content, :referer_url, :timestamp, :echo_id]) params.permit(:id, :before, :after, :website_token, contact: [:name, :email], message: [:content, :referer_url, :timestamp, :echo_id, :reply_to])
end end
def set_message def set_message
@@ -23,7 +23,7 @@ class Public::Api::V1::Inboxes::ConversationsController < Public::Api::V1::Inbox
def update_last_seen def update_last_seen
@conversation.contact_last_seen_at = DateTime.now.utc @conversation.contact_last_seen_at = DateTime.now.utc
@conversation.save! @conversation.save!
::Conversations::MarkMessagesAsReadJob.perform_later(@conversation) ::Conversations::UpdateMessageStatusJob.perform_later(@conversation.id, @conversation.contact_last_seen_at)
head :ok head :ok
end end
@@ -39,10 +39,6 @@ class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::B
) )
end end
def portal
@portal ||= Portal.find_by!(slug: permitted_params[:slug], archived: false)
end
def list_params def list_params
params.permit(:query, :locale, :sort, :status) params.permit(:query, :locale, :sort, :status)
end end
@@ -14,10 +14,14 @@ class Public::Api::V1::Portals::BaseController < PublicController
@theme = if %w[dark light].include?(params[:theme]) @theme = if %w[dark light].include?(params[:theme])
params[:theme] params[:theme]
else else
'' 'system'
end end
end end
def portal
@portal ||= Portal.find_by!(slug: params[:slug], archived: false)
end
def set_locale(&) def set_locale(&)
switch_locale_with_portal(&) if params[:locale].present? switch_locale_with_portal(&) if params[:locale].present?
switch_locale_with_article(&) if params[:article_slug].present? switch_locale_with_article(&) if params[:article_slug].present?
@@ -40,6 +44,8 @@ class Public::Api::V1::Portals::BaseController < PublicController
def switch_locale_with_article(&) def switch_locale_with_article(&)
article = Article.find_by(slug: params[:article_slug]) article = Article.find_by(slug: params[:article_slug])
Rails.logger.info "Article: not found for slug: #{params[:article_slug]}"
render_404 && return if article.blank?
@locale = if article.category.present? @locale = if article.category.present?
article.category.locale article.category.locale
@@ -53,4 +59,9 @@ class Public::Api::V1::Portals::BaseController < PublicController
def allow_iframe_requests def allow_iframe_requests
response.headers.delete('X-Frame-Options') if @is_plain_layout_enabled response.headers.delete('X-Frame-Options') if @is_plain_layout_enabled
end end
def render_404
portal
render 'public/api/v1/portals/error/404', status: :not_found
end
end end
@@ -13,10 +13,9 @@ class Public::Api::V1::Portals::CategoriesController < Public::Api::V1::Portals:
private private
def set_category def set_category
@category = @portal.categories.find_by!(locale: params[:locale], slug: params[:category_slug]) @category = @portal.categories.find_by(locale: params[:locale], slug: params[:category_slug])
end
def portal Rails.logger.info "Category: not found for slug: #{params[:category_slug]}"
@portal ||= Portal.find_by!(slug: params[:slug], archived: false) render_404 && return if @category.blank?
end end
end end
+39 -2
View File
@@ -1,11 +1,48 @@
module PortalHelper module PortalHelper
def generate_portal_bg_color(portal_color, theme) def generate_portal_bg_color(portal_color, theme)
base_color = theme == 'dark' ? 'black' : 'white' base_color = theme == 'dark' ? 'black' : 'white'
"color-mix(in srgb, #{portal_color} 10%, #{base_color})" "color-mix(in srgb, #{portal_color} 20%, #{base_color})"
end end
def generate_portal_bg(portal_color, theme) def generate_portal_bg(portal_color, theme)
bg_image = theme == 'dark' ? 'grid_dark.svg' : 'grid.svg' bg_image = theme == 'dark' ? 'hexagon-dark.svg' : 'hexagon-light.svg'
"background: url(/assets/images/hc/#{bg_image}) #{generate_portal_bg_color(portal_color, theme)}" "background: url(/assets/images/hc/#{bg_image}) #{generate_portal_bg_color(portal_color, theme)}"
end end
def language_name(locale)
language_map = YAML.load_file(Rails.root.join('config/languages/language_map.yml'))
language_map[locale] || locale
end
def get_theme_names(theme)
if theme == 'light'
I18n.t('public_portal.header.appearance.light')
elsif theme == 'dark'
I18n.t('public_portal.header.appearance.dark')
else
I18n.t('public_portal.header.appearance.system')
end
end
def get_theme_icon(theme)
if theme == 'light'
'icons/sun'
elsif theme == 'dark'
'icons/moon'
else
'icons/monitor'
end
end
def generate_gradient_to_bottom(theme)
"background-image: linear-gradient(to bottom, transparent, #{theme == 'dark' ? '#151718' : 'white'})"
end
def generate_article_link(portal_slug, article_slug, theme)
"/hc/#{portal_slug}/articles/#{article_slug}#{theme.present? && theme != 'system' ? "?theme=#{theme}" : ''}"
end
def render_category_content(content)
ChatwootMarkdownRenderer.new(content).render_markdown_to_plain_text
end
end end
@@ -1,6 +1,7 @@
/* global axios */ /* global axios */
import PortalsAPI from './portals'; import PortalsAPI from './portals';
import { getArticleSearchURL } from 'dashboard/helper/URLHelper.js';
class ArticlesAPI extends PortalsAPI { class ArticlesAPI extends PortalsAPI {
constructor() { constructor() {
@@ -12,14 +13,31 @@ class ArticlesAPI extends PortalsAPI {
portalSlug, portalSlug,
locale, locale,
status, status,
author_id, authorId,
category_slug, categorySlug,
sort,
}) { }) {
let baseUrl = `${this.url}/${portalSlug}/articles?page=${pageNumber}&locale=${locale}`; const url = getArticleSearchURL({
if (status !== undefined) baseUrl += `&status=${status}`; pageNumber,
if (author_id) baseUrl += `&author_id=${author_id}`; portalSlug,
if (category_slug) baseUrl += `&category_slug=${category_slug}`; locale,
return axios.get(baseUrl); status,
authorId,
categorySlug,
sort,
host: this.url,
});
return axios.get(url);
}
searchArticles({ portalSlug, query }) {
const url = getArticleSearchURL({
portalSlug,
query,
host: this.url,
});
return axios.get(url);
} }
getArticle({ id, portalSlug }) { getArticle({ id, portalSlug }) {
@@ -17,6 +17,10 @@ class PortalsAPI extends ApiClient {
deletePortal(portalSlug) { deletePortal(portalSlug) {
return axios.delete(`${this.url}/${portalSlug}`); return axios.delete(`${this.url}/${portalSlug}`);
} }
deleteLogo(portalSlug) {
return axios.delete(`${this.url}/${portalSlug}/logo`);
}
} }
export default PortalsAPI; export default PortalsAPI;
@@ -26,9 +26,13 @@ export const buildCreatePayload = ({
payload.append('echo_id', echoId); payload.append('echo_id', echoId);
payload.append('cc_emails', ccEmails); payload.append('cc_emails', ccEmails);
payload.append('bcc_emails', bccEmails); payload.append('bcc_emails', bccEmails);
if (toEmails) { if (toEmails) {
payload.append('to_emails', toEmails); payload.append('to_emails', toEmails);
} }
if (contentAttributes) {
payload.append('content_attributes', JSON.stringify(contentAttributes));
}
} else { } else {
payload = { payload = {
content: message, content: message,
@@ -34,7 +34,7 @@ describe('#PortalAPI', () => {
portalSlug: 'room-rental', portalSlug: 'room-rental',
locale: 'en-US', locale: 'en-US',
status: 'published', status: 'published',
author_id: '1', authorId: '1',
}); });
expect(axiosMock.get).toHaveBeenCalledWith( expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v1/portals/room-rental/articles?page=1&locale=en-US&status=published&author_id=1' '/api/v1/portals/room-rental/articles?page=1&locale=en-US&status=published&author_id=1'
@@ -85,6 +85,33 @@ describe('#PortalAPI', () => {
window.axios = originalAxios; window.axios = originalAxios;
}); });
it('#searchArticles', () => {
articlesAPI.searchArticles({
query: 'test',
portalSlug: 'room-rental',
});
expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v1/portals/room-rental/articles?query=test'
);
});
});
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
window.axios = axiosMock;
});
afterEach(() => {
window.axios = originalAxios;
});
it('#updateArticle', () => { it('#updateArticle', () => {
articlesAPI.updateArticle({ articlesAPI.updateArticle({
articleId: 1, articleId: 1,
@@ -50,6 +50,7 @@ describe('#ConversationAPI', () => {
message: 'test content', message: 'test content',
echoId: 12, echoId: 12,
isPrivate: true, isPrivate: true,
contentAttributes: { in_reply_to: 12 },
files: [new Blob(['test-content'], { type: 'application/pdf' })], files: [new Blob(['test-content'], { type: 'application/pdf' })],
}); });
expect(formPayload).toBeInstanceOf(FormData); expect(formPayload).toBeInstanceOf(FormData);
@@ -57,6 +58,10 @@ describe('#ConversationAPI', () => {
expect(formPayload.get('echo_id')).toEqual('12'); expect(formPayload.get('echo_id')).toEqual('12');
expect(formPayload.get('private')).toEqual('true'); expect(formPayload.get('private')).toEqual('true');
expect(formPayload.get('cc_emails')).toEqual(''); expect(formPayload.get('cc_emails')).toEqual('');
expect(formPayload.get('bcc_emails')).toEqual('');
expect(formPayload.get('content_attributes')).toEqual(
'{"in_reply_to":12}'
);
}); });
it('builds object payload if file is not available', () => { it('builds object payload if file is not available', () => {
@@ -1,9 +1,5 @@
@import '~vue2-datepicker/scss/index'; @import '~vue2-datepicker/scss/index';
.mx-datepicker-popup {
@apply z-[99999];
}
.date-picker { .date-picker {
&.no-margin { &.no-margin {
.mx-input { .mx-input {
@@ -29,10 +25,52 @@
.mx-input[readonly] { .mx-input[readonly] {
@apply bg-white dark:bg-slate-900 cursor-pointer; @apply bg-white dark:bg-slate-900 cursor-pointer;
} }
.mx-icon-calendar {
@apply dark:text-slate-500;
}
} }
.mx-calendar-content .cell:hover { .mx-datepicker-main {
@apply bg-slate-75 dark:bg-slate-700 text-slate-900 dark:text-slate-100; @apply border-0 bg-white dark:bg-slate-800;
.cell {
&.disabled {
@apply bg-slate-25 dark:bg-slate-900 text-slate-200 dark:text-slate-300;
}
&:hover,
&.hover-in-range,
&.in-range {
@apply bg-slate-75 dark:bg-slate-700 text-slate-900 dark:text-slate-100;
}
}
.mx-time {
@apply border-0 bg-white dark:bg-slate-800;
.mx-time-header {
@apply border-0;
}
.mx-time-item {
&.disabled {
@apply bg-slate-25 dark:bg-slate-900;
}
&:hover {
@apply bg-slate-75 dark:bg-slate-700;
}
}
}
.today {
@apply font-semibold;
}
}
.mx-datepicker-popup {
@apply z-[99999];
} }
.mx-datepicker-inline { .mx-datepicker-inline {
@@ -41,24 +79,4 @@
.mx-calendar { .mx-calendar {
@apply w-full; @apply w-full;
} }
.cell.disabled {
@apply bg-slate-25 dark:bg-slate-900 text-slate-200 dark:text-slate-300;
}
.mx-time-item.disabled {
@apply bg-slate-25 dark:bg-slate-900;
}
.today {
@apply font-semibold;
}
.mx-datepicker-main {
@apply border-0 bg-white dark:bg-slate-800;
}
.mx-time-header {
@apply border-0;
}
} }
@@ -16,3 +16,23 @@
width: 100%; width: 100%;
} }
} }
.integration-hooks {
.formulate-input[data-type='checkbox'] {
.formulate-input-wrapper {
@apply flex;
.formulate-input-element {
@apply pr-2;
input {
@apply mb-0;
}
}
}
.formulate-input-element-decorator {
@apply hidden;
}
}
}
@@ -207,6 +207,7 @@ import DeleteCustomViews from 'dashboard/routes/dashboard/customviews/DeleteCust
import ConversationBulkActions from './widgets/conversation/conversationBulkActions/Index.vue'; import ConversationBulkActions from './widgets/conversation/conversationBulkActions/Index.vue';
import alertMixin from 'shared/mixins/alertMixin'; import alertMixin from 'shared/mixins/alertMixin';
import filterMixin from 'shared/mixins/filterMixin'; import filterMixin from 'shared/mixins/filterMixin';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import languages from 'dashboard/components/widgets/conversation/advancedFilterItems/languages'; import languages from 'dashboard/components/widgets/conversation/advancedFilterItems/languages';
import countries from 'shared/constants/countries'; import countries from 'shared/constants/countries';
import { generateValuesForEditCustomViews } from 'dashboard/helper/customViewsHelper'; import { generateValuesForEditCustomViews } from 'dashboard/helper/customViewsHelper';
@@ -238,6 +239,7 @@ export default {
eventListenerMixins, eventListenerMixins,
alertMixin, alertMixin,
filterMixin, filterMixin,
uiSettingsMixin,
], ],
props: { props: {
conversationInbox: { conversationInbox: {
@@ -408,8 +410,12 @@ export default {
}, },
conversationListPagination() { conversationListPagination() {
const conversationsPerPage = 25; const conversationsPerPage = 25;
const hasChatsOnView =
this.chatsOnView &&
Array.isArray(this.chatsOnView) &&
!this.chatsOnView.length;
const isNoFiltersOrFoldersAndChatListNotEmpty = const isNoFiltersOrFoldersAndChatListNotEmpty =
!this.hasAppliedFiltersOrActiveFolders && this.chatsOnView !== []; !this.hasAppliedFiltersOrActiveFolders && hasChatsOnView;
const isUnderPerPage = const isUnderPerPage =
this.chatsOnView.length < conversationsPerPage && this.chatsOnView.length < conversationsPerPage &&
this.activeAssigneeTabCount < conversationsPerPage && this.activeAssigneeTabCount < conversationsPerPage &&
@@ -515,6 +521,7 @@ export default {
}, },
}, },
mounted() { mounted() {
this.setFiltersFromUISettings();
this.$store.dispatch('setChatStatusFilter', this.activeStatus); this.$store.dispatch('setChatStatusFilter', this.activeStatus);
this.$store.dispatch('setChatSortFilter', this.activeSortBy); this.$store.dispatch('setChatSortFilter', this.activeSortBy);
this.resetAndFetchData(); this.resetAndFetchData();
@@ -544,6 +551,12 @@ export default {
this.$store.dispatch('customViews/update', payloadData); this.$store.dispatch('customViews/update', payloadData);
this.closeAdvanceFiltersModal(); this.closeAdvanceFiltersModal();
}, },
setFiltersFromUISettings() {
const { conversations_filter_by: filterBy = {} } = this.uiSettings;
const { status, order_by: orderBy } = filterBy;
this.activeStatus = status || wootConstants.STATUS_TYPE.OPEN;
this.activeSortBy = orderBy || wootConstants.SORT_BY_TYPE.LATEST;
},
onClickOpenAddFoldersModal() { onClickOpenAddFoldersModal() {
this.showAddFoldersModal = true; this.showAddFoldersModal = true;
}, },
@@ -136,6 +136,7 @@ export default {
toStateName: 'labels_list', toStateName: 'labels_list',
showModalForNewItem: true, showModalForNewItem: true,
modalName: 'AddLabel', modalName: 'AddLabel',
dataTestid: 'sidebar-new-label-button',
children: this.labels.map(label => ({ children: this.labels.map(label => ({
id: label.id, id: label.id,
label: label.title, label: label.title,
@@ -78,6 +78,7 @@
variant="clear" variant="clear"
color-scheme="secondary" color-scheme="secondary"
icon="add" icon="add"
:data-testid="menuItem.dataTestid"
@click="e => newLinkClick(e, navigate)" @click="e => newLinkClick(e, navigate)"
> >
{{ $t(`SIDEBAR.${menuItem.newLinkTag}`) }} {{ $t(`SIDEBAR.${menuItem.newLinkTag}`) }}
@@ -61,6 +61,7 @@ export default {
computed: { computed: {
...mapGetters({ ...mapGetters({
currentChat: 'getSelectedChat', currentChat: 'getSelectedChat',
isAChatwootInstance: 'globalConfig/isAChatwootInstance',
}), }),
isAICTAModalDismissed() { isAICTAModalDismissed() {
return this.uiSettings.is_open_ai_cta_modal_dismissed; return this.uiSettings.is_open_ai_cta_modal_dismissed;
@@ -70,7 +71,8 @@ export default {
return ( return (
this.isAdmin && this.isAdmin &&
!this.isAIIntegrationEnabled && !this.isAIIntegrationEnabled &&
!this.isAICTAModalDismissed !this.isAICTAModalDismissed &&
this.isAChatwootInstance
); );
}, },
// Display a AI CTA button for agents and other admins who have not yet opened the AI assistance modal. // Display a AI CTA button for agents and other admins who have not yet opened the AI assistance modal.
@@ -135,6 +135,7 @@ export default {
// allowSignature is a kill switch, ensuring no signature methods // allowSignature is a kill switch, ensuring no signature methods
// are triggered except when this flag is true // are triggered except when this flag is true
allowSignature: { type: Boolean, default: false }, allowSignature: { type: Boolean, default: false },
channelType: { type: String, default: '' },
showImageResizeToolbar: { type: Boolean, default: false }, // A kill switch to show or hide the image toolbar showImageResizeToolbar: { type: Boolean, default: false }, // A kill switch to show or hide the image toolbar
}, },
data() { data() {
@@ -266,8 +267,11 @@ export default {
sendWithSignature() { sendWithSignature() {
// this is considered the source of truth, we watch this property // this is considered the source of truth, we watch this property
// on change, we toggle the signature in the editor // on change, we toggle the signature in the editor
const { send_with_signature: isEnabled } = this.uiSettings; if (this.allowSignature && !this.isPrivate && this.channelType) {
return isEnabled && this.allowSignature && !this.isPrivate; return this.fetchSignatureFlagFromUiSettings(this.channelType);
}
return false;
}, },
}, },
watch: { watch: {
@@ -442,7 +446,7 @@ export default {
this.onFocus(); this.onFocus();
}, },
click: () => { click: () => {
// this.isEditorMouseFocusedOnAnImage(); Enable it when the backend supports for message resize is done. this.isEditorMouseFocusedOnAnImage();
}, },
blur: () => { blur: () => {
this.onBlur(); this.onBlur();
@@ -670,7 +674,7 @@ export default {
() => this.resetTyping(), () => this.resetTyping(),
TYPING_INDICATOR_IDLE_TIME TYPING_INDICATOR_IDLE_TIME
); );
// this.updateImgToolbarOnDelete(); Enable it when the backend supports for message resize is done. this.updateImgToolbarOnDelete();
}, },
onKeydown(event) { onKeydown(event) {
if (this.isEnterToSendEnabled()) { if (this.isEnterToSendEnabled()) {
@@ -101,7 +101,7 @@
<transition name="modal-fade"> <transition name="modal-fade">
<div <div
v-show="$refs.upload && $refs.upload.dropActive" v-show="$refs.upload && $refs.upload.dropActive"
class="flex items-center justify-center gap-2 fixed left-0 right-0 top-0 bottom-0 w-full h-full z-20 text-slate-600 dark:text-slate-200 bg-white_transparent dark:bg-black_transparent flex-col" class="fixed top-0 bottom-0 left-0 right-0 z-20 flex flex-col items-center justify-center w-full h-full gap-2 text-slate-600 dark:text-slate-200 bg-white_transparent dark:bg-black_transparent"
> >
<fluent-icon icon="cloud-backup" size="40" /> <fluent-icon icon="cloud-backup" size="40" />
<h4 class="page-sub-title text-slate-600 dark:text-slate-200"> <h4 class="page-sub-title text-slate-600 dark:text-slate-200">
@@ -109,6 +109,16 @@
</h4> </h4>
</div> </div>
</transition> </transition>
<woot-button
v-if="enableInsertArticleInReply"
v-tooltip.top-end="$t('HELP_CENTER.ARTICLE_SEARCH.OPEN_ARTICLE_SEARCH')"
icon="document-text-link"
color-scheme="secondary"
variant="smooth"
size="small"
:title="$t('HELP_CENTER.ARTICLE_SEARCH.OPEN_ARTICLE_SEARCH')"
@click="toggleInsertArticle"
/>
</div> </div>
<div class="right-wrap"> <div class="right-wrap">
<woot-button <woot-button
@@ -233,6 +243,10 @@ export default {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
portalSlug: {
type: String,
required: true,
},
}, },
computed: { computed: {
...mapGetters({ ...mapGetters({
@@ -299,14 +313,21 @@ export default {
return !this.isOnPrivateNote; return !this.isOnPrivateNote;
}, },
sendWithSignature() { sendWithSignature() {
const { send_with_signature: isEnabled } = this.uiSettings; // channelType is sourced from inboxMixin
return isEnabled; return this.fetchSignatureFlagFromUiSettings(this.channelType);
}, },
signatureToggleTooltip() { signatureToggleTooltip() {
return this.sendWithSignature return this.sendWithSignature
? this.$t('CONVERSATION.FOOTER.DISABLE_SIGN_TOOLTIP') ? this.$t('CONVERSATION.FOOTER.DISABLE_SIGN_TOOLTIP')
: this.$t('CONVERSATION.FOOTER.ENABLE_SIGN_TOOLTIP'); : this.$t('CONVERSATION.FOOTER.ENABLE_SIGN_TOOLTIP');
}, },
enableInsertArticleInReply() {
const isFeatEnabled = this.isFeatureEnabledonAccount(
this.accountId,
FEATURE_FLAGS.INSERT_ARTICLE_IN_REPLY
);
return isFeatEnabled && this.portalSlug;
},
}, },
mounted() { mounted() {
ActiveStorage.start(); ActiveStorage.start();
@@ -318,13 +339,14 @@ export default {
} }
}, },
toggleMessageSignature() { toggleMessageSignature() {
this.updateUISettings({ this.setSignatureFlagForInbox(this.channelType, !this.sendWithSignature);
send_with_signature: !this.sendWithSignature,
});
}, },
replaceText(text) { replaceText(text) {
this.$emit('replace-text', text); this.$emit('replace-text', text);
}, },
toggleInsertArticle() {
this.$emit('toggle-insert-article');
},
}, },
}; };
</script> </script>
@@ -32,7 +32,7 @@
}}</span> }}</span>
<filter-item <filter-item
type="sort" type="sort"
:selected-value="chatSortFilter" :selected-value="sortFilter"
:items="chatSortItems" :items="chatSortItems"
path-prefix="CHAT_LIST.CHAT_SORT_FILTER_ITEMS" path-prefix="CHAT_LIST.CHAT_SORT_FILTER_ITEMS"
@onChangeFilter="onChangeFilter" @onChangeFilter="onChangeFilter"
@@ -47,12 +47,13 @@ import wootConstants from 'dashboard/constants/globals';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { mixin as clickaway } from 'vue-clickaway'; import { mixin as clickaway } from 'vue-clickaway';
import FilterItem from './FilterItem.vue'; import FilterItem from './FilterItem.vue';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
export default { export default {
components: { components: {
FilterItem, FilterItem,
}, },
mixins: [clickaway], mixins: [clickaway, uiSettingsMixin],
data() { data() {
return { return {
showActionsDropdown: false, showActionsDropdown: false,
@@ -83,8 +84,17 @@ export default {
closeDropdown() { closeDropdown() {
this.showActionsDropdown = false; this.showActionsDropdown = false;
}, },
onChangeFilter(type, value) { onChangeFilter(value, type) {
this.$emit('changeFilter', type, value); this.$emit('changeFilter', value, type);
this.saveSelectedFilter(type, value);
},
saveSelectedFilter(type, value) {
this.updateUISettings({
conversations_filter_by: {
status: type === 'status' ? value : this.chatStatus,
order_by: type === 'sort' ? value : this.sortFilter,
},
});
}, },
}, },
}; };
@@ -22,22 +22,12 @@
:bcc="emailHeadAttributes.bcc" :bcc="emailHeadAttributes.bcc"
:is-incoming="isIncoming" :is-incoming="isIncoming"
/> />
<blockquote v-if="storyReply" class="story-reply-quote"> <instagram-story-reply v-if="storyUrl" :story-url="storyUrl" />
<span>{{ $t('CONVERSATION.REPLIED_TO_STORY') }}</span>
<bubble-image
v-if="!hasImgStoryError && storyUrl"
:url="storyUrl"
@error="onStoryLoadError"
/>
<bubble-video
v-else-if="hasImgStoryError && storyUrl"
:url="storyUrl"
/>
</blockquote>
<bubble-reply-to <bubble-reply-to
v-if="inReplyToMessageId && inboxSupportsReplyTo" v-if="inReplyToMessageId && inboxSupportsReplyTo.incoming"
:message="inReplyTo" :message="inReplyTo"
:message-type="data.message_type" :message-type="data.message_type"
:parent-has-attachments="hasAttachments"
/> />
<bubble-text <bubble-text
v-if="data.content" v-if="data.content"
@@ -58,10 +48,15 @@
</span> </span>
<div v-if="!isPending && hasAttachments"> <div v-if="!isPending && hasAttachments">
<div v-for="attachment in attachments" :key="attachment.id"> <div v-for="attachment in attachments" :key="attachment.id">
<instagram-story
v-if="isAnInstagramStory"
:story-url="attachment.data_url"
@error="onMediaLoadError"
/>
<bubble-image-audio-video <bubble-image-audio-video
v-if="isAttachmentImageVideoAudio(attachment.file_type)" v-else-if="isAttachmentImageVideoAudio(attachment.file_type)"
:attachment="attachment" :attachment="attachment"
@error="onImageLoadError" @error="onMediaLoadError"
/> />
<bubble-location <bubble-location
v-else-if="attachment.file_type === 'location'" v-else-if="attachment.file_type === 'location'"
@@ -74,9 +69,6 @@
:name="data.content" :name="data.content"
:phone-number="attachment.fallback_title" :phone-number="attachment.fallback_title"
/> />
<instagram-image-error-placeholder
v-else-if="hasImageError && hasInstagramStory"
/>
<bubble-file v-else :url="attachment.data_url" /> <bubble-file v-else :url="attachment.data_url" />
</div> </div>
</div> </div>
@@ -88,7 +80,6 @@
:story-id="`${storyId}`" :story-id="`${storyId}`"
:is-a-tweet="isATweet" :is-a-tweet="isATweet"
:is-a-whatsapp-channel="isAWhatsAppChannel" :is-a-whatsapp-channel="isAWhatsAppChannel"
:has-instagram-story="hasInstagramStory"
:is-email="isEmailContentType" :is-email="isEmailContentType"
:is-private="data.private" :is-private="data.private"
:message-type="data.message_type" :message-type="data.message_type"
@@ -137,19 +128,18 @@
<script> <script>
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin'; import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
import BubbleActions from './bubble/Actions.vue'; import BubbleActions from './bubble/Actions.vue';
import BubbleContact from './bubble/Contact.vue';
import BubbleFile from './bubble/File.vue'; import BubbleFile from './bubble/File.vue';
import BubbleImage from './bubble/Image.vue';
import BubbleVideo from './bubble/Video.vue';
import BubbleImageAudioVideo from './bubble/ImageAudioVideo.vue'; import BubbleImageAudioVideo from './bubble/ImageAudioVideo.vue';
import BubbleIntegration from './bubble/Integration.vue'; import BubbleIntegration from './bubble/Integration.vue';
import BubbleLocation from './bubble/Location.vue'; import BubbleLocation from './bubble/Location.vue';
import BubbleMailHead from './bubble/MailHead.vue'; import BubbleMailHead from './bubble/MailHead.vue';
import BubbleText from './bubble/Text.vue';
import BubbleContact from './bubble/Contact.vue';
import BubbleReplyTo from './bubble/ReplyTo.vue'; import BubbleReplyTo from './bubble/ReplyTo.vue';
import Spinner from 'shared/components/Spinner.vue'; import BubbleText from './bubble/Text.vue';
import ContextMenu from 'dashboard/modules/conversations/components/MessageContextMenu.vue'; import ContextMenu from 'dashboard/modules/conversations/components/MessageContextMenu.vue';
import instagramImageErrorPlaceholder from './instagramImageErrorPlaceholder.vue'; import InstagramStory from './bubble/InstagramStory.vue';
import InstagramStoryReply from './bubble/InstagramStoryReply.vue';
import Spinner from 'shared/components/Spinner.vue';
import alertMixin from 'shared/mixins/alertMixin'; import alertMixin from 'shared/mixins/alertMixin';
import contentTypeMixin from 'shared/mixins/contentTypeMixin'; import contentTypeMixin from 'shared/mixins/contentTypeMixin';
import { MESSAGE_TYPE, MESSAGE_STATUS } from 'shared/constants/messages'; import { MESSAGE_TYPE, MESSAGE_STATUS } from 'shared/constants/messages';
@@ -162,19 +152,18 @@ import { LocalStorage } from 'shared/helpers/localStorage';
export default { export default {
components: { components: {
BubbleActions, BubbleActions,
BubbleContact,
BubbleFile, BubbleFile,
BubbleImage,
BubbleVideo,
BubbleImageAudioVideo, BubbleImageAudioVideo,
BubbleIntegration, BubbleIntegration,
BubbleLocation, BubbleLocation,
BubbleMailHead, BubbleMailHead,
BubbleText,
BubbleContact,
BubbleReplyTo, BubbleReplyTo,
BubbleText,
ContextMenu, ContextMenu,
InstagramStory,
InstagramStoryReply,
Spinner, Spinner,
instagramImageErrorPlaceholder,
}, },
mixins: [alertMixin, messageFormatterMixin, contentTypeMixin], mixins: [alertMixin, messageFormatterMixin, contentTypeMixin],
props: { props: {
@@ -190,17 +179,13 @@ export default {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
hasInstagramStory: {
type: Boolean,
default: false,
},
isWebWidgetInbox: { isWebWidgetInbox: {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
inboxSupportsReplyTo: { inboxSupportsReplyTo: {
type: Boolean, type: Object,
default: false, default: () => ({}),
}, },
inReplyTo: { inReplyTo: {
type: Object, type: Object,
@@ -210,10 +195,9 @@ export default {
data() { data() {
return { return {
showContextMenu: false, showContextMenu: false,
hasImageError: false, hasMediaLoadError: false,
contextMenuPosition: {}, contextMenuPosition: {},
showBackgroundHighlight: false, showBackgroundHighlight: false,
hasImgStoryError: false,
}; };
}, },
computed: { computed: {
@@ -289,19 +273,22 @@ export default {
// since old messages are only loaded when the user scrolls up // since old messages are only loaded when the user scrolls up
return this.data.content_attributes?.in_reply_to; return this.data.content_attributes?.in_reply_to;
}, },
isAnInstagramStory() {
return this.contentAttributes.image_type === 'story_mention';
},
contextMenuEnabledOptions() { contextMenuEnabledOptions() {
return { return {
copy: this.hasText, copy: this.hasText,
delete: this.hasText || this.hasAttachments, delete: this.hasText || this.hasAttachments,
cannedResponse: this.isOutgoing && this.hasText, cannedResponse: this.isOutgoing && this.hasText,
replyTo: !this.data.private && this.inboxSupportsReplyTo, replyTo: !this.data.private && this.inboxSupportsReplyTo.outgoing,
}; };
}, },
contentAttributes() { contentAttributes() {
return this.data.content_attributes || {}; return this.data.content_attributes || {};
}, },
externalError() { externalError() {
return this.contentAttributes.external_error || null; return this.contentAttributes.external_error || '';
}, },
sender() { sender() {
return this.data.sender || {}; return this.data.sender || {};
@@ -318,9 +305,6 @@ export default {
storyUrl() { storyUrl() {
return this.contentAttributes.story_url || null; return this.contentAttributes.story_url || null;
}, },
storyReply() {
return this.storyUrl && this.hasInstagramStory;
},
contentType() { contentType() {
const { const {
data: { content_type: contentType }, data: { content_type: contentType },
@@ -401,7 +385,7 @@ export default {
return false; return false;
} }
if (this.isFailed) { if (this.isFailed) {
return this.externalError ? '' : this.$t(`CONVERSATION.SEND_FAILED`); return this.externalError || this.$t(`CONVERSATION.SEND_FAILED`);
} }
return false; return false;
}, },
@@ -459,13 +443,11 @@ export default {
}, },
watch: { watch: {
data() { data() {
this.hasImageError = false; this.hasMediaLoadError = false;
this.hasImgStoryError = false;
}, },
}, },
mounted() { mounted() {
this.hasImageError = false; this.hasMediaLoadError = false;
this.hasImgStoryError = false;
bus.$on(BUS_EVENTS.ON_MESSAGE_LIST_SCROLL, this.closeContextMenu); bus.$on(BUS_EVENTS.ON_MESSAGE_LIST_SCROLL, this.closeContextMenu);
this.setupHighlightTimer(); this.setupHighlightTimer();
}, },
@@ -475,13 +457,13 @@ export default {
}, },
methods: { methods: {
isAttachmentImageVideoAudio(fileType) { isAttachmentImageVideoAudio(fileType) {
return ['image', 'audio', 'video'].includes(fileType); return ['image', 'audio', 'video', 'story_mention'].includes(fileType);
}, },
hasMediaAttachment(type) { hasMediaAttachment(type) {
if (this.hasAttachments && this.data.attachments.length > 0) { if (this.hasAttachments && this.data.attachments.length > 0) {
const { attachments = [{}] } = this.data; const { attachments = [{}] } = this.data;
const { file_type: fileType } = attachments[0]; const { file_type: fileType } = attachments[0];
return fileType === type && !this.hasImageError; return fileType === type && !this.hasMediaLoadError;
} }
if (this.storyReply) { if (this.storyReply) {
return true; return true;
@@ -494,11 +476,8 @@ export default {
async retrySendMessage() { async retrySendMessage() {
await this.$store.dispatch('sendMessageWithData', this.data); await this.$store.dispatch('sendMessageWithData', this.data);
}, },
onImageLoadError() { onMediaLoadError() {
this.hasImageError = true; this.hasMediaLoadError = true;
},
onStoryLoadError() {
this.hasImgStoryError = true;
}, },
openContextMenu(e) { openContextMenu(e) {
const shouldSkipContextMenu = const shouldSkipContextMenu =
@@ -730,8 +709,4 @@ li.right {
} }
} }
} }
.story-reply-quote {
@apply mt-2 mx-4 mb-0 px-2 pb-0 pt-2 border-l-4 border-solid border-slate-75 dark:border-slate-600 text-slate-600 dark:text-slate-200;
}
</style> </style>
@@ -31,7 +31,6 @@
:data="message" :data="message"
:is-a-tweet="isATweet" :is-a-tweet="isATweet"
:is-a-whatsapp-channel="isAWhatsAppChannel" :is-a-whatsapp-channel="isAWhatsAppChannel"
:has-instagram-story="hasInstagramStory"
:is-web-widget-inbox="isAWebWidgetInbox" :is-web-widget-inbox="isAWebWidgetInbox"
:inbox-supports-reply-to="inboxSupportsReplyTo" :inbox-supports-reply-to="inboxSupportsReplyTo"
:in-reply-to="getInReplyToMessage(message)" :in-reply-to="getInReplyToMessage(message)"
@@ -54,7 +53,6 @@
:data="message" :data="message"
:is-a-tweet="isATweet" :is-a-tweet="isATweet"
:is-a-whatsapp-channel="isAWhatsAppChannel" :is-a-whatsapp-channel="isAWhatsAppChannel"
:has-instagram-story="hasInstagramStory"
:is-web-widget-inbox="isAWebWidgetInbox" :is-web-widget-inbox="isAWebWidgetInbox"
:inbox-supports-reply-to="inboxSupportsReplyTo" :inbox-supports-reply-to="inboxSupportsReplyTo"
:in-reply-to="getInReplyToMessage(message)" :in-reply-to="getInReplyToMessage(message)"
@@ -116,7 +114,6 @@ import { LocalStorage } from 'shared/helpers/localStorage';
// constants // constants
import { BUS_EVENTS } from 'shared/constants/busEvents'; import { BUS_EVENTS } from 'shared/constants/busEvents';
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
import { REPLY_POLICY } from 'shared/constants/links'; import { REPLY_POLICY } from 'shared/constants/links';
import wootConstants from 'dashboard/constants/globals'; import wootConstants from 'dashboard/constants/globals';
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage'; import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
@@ -238,10 +235,6 @@ export default {
isATweet() { isATweet() {
return this.conversationType === 'tweet'; return this.conversationType === 'tweet';
}, },
hasInstagramStory() {
return this.conversationType === 'instagram_direct_message';
},
isRightOrLeftIcon() { isRightOrLeftIcon() {
if (this.isContactPanelOpen) { if (this.isContactPanelOpen) {
return 'arrow-chevron-right'; return 'arrow-chevron-right';
@@ -291,13 +284,10 @@ export default {
return this.currentChat.unread_count || 0; return this.currentChat.unread_count || 0;
}, },
inboxSupportsReplyTo() { inboxSupportsReplyTo() {
return ( return {
this.inboxHasFeature(INBOX_FEATURES.REPLY_TO) && incoming: this.inboxHasFeature(INBOX_FEATURES.REPLY_TO),
this.isFeatureEnabledonAccount( outgoing: this.inboxHasFeature(INBOX_FEATURES.REPLY_TO_OUTGOING),
this.accountId, };
FEATURE_FLAGS.MESSAGE_REPLY_TO
)
);
}, },
}, },
@@ -350,7 +340,7 @@ export default {
// method available in mixin, need to ensure that integrations are present // method available in mixin, need to ensure that integrations are present
await this.fetchIntegrationsIfRequired(); await this.fetchIntegrationsIfRequired();
if (!this.isAIIntegrationEnabled) { if (!this.isLabelSuggestionFeatureEnabled) {
return; return;
} }
@@ -18,6 +18,12 @@
:popout-reply-box="popoutReplyBox" :popout-reply-box="popoutReplyBox"
@click="$emit('click')" @click="$emit('click')"
/> />
<article-search-popover
v-if="showArticleSearchPopover && connectedPortalSlug"
:selected-portal-slug="connectedPortalSlug"
@insert="handleInsert"
@close="onSearchPopoverClose"
/>
<div class="reply-box__top"> <div class="reply-box__top">
<reply-to-message <reply-to-message
v-if="shouldShowReplyToMessage" v-if="shouldShowReplyToMessage"
@@ -35,7 +41,7 @@
v-if="showEmojiPicker" v-if="showEmojiPicker"
v-on-clickaway="hideEmojiPicker" v-on-clickaway="hideEmojiPicker"
:class="emojiDialogClassOnExpandedLayoutAndRTLView" :class="emojiDialogClassOnExpandedLayoutAndRTLView"
:on-click="emojiOnClick" :on-click="addIntoEditor"
/> />
<reply-email-head <reply-email-head
v-if="showReplyHead" v-if="showReplyHead"
@@ -79,6 +85,7 @@
:variables="messageVariables" :variables="messageVariables"
:signature="signatureToApply" :signature="signatureToApply"
:allow-signature="true" :allow-signature="true"
:channel-type="channelType"
@typing-off="onTypingOff" @typing-off="onTypingOff"
@typing-on="onTypingOn" @typing-on="onTypingOn"
@focus="onFocus" @focus="onFocus"
@@ -121,10 +128,12 @@
:toggle-audio-recorder="toggleAudioRecorder" :toggle-audio-recorder="toggleAudioRecorder"
:toggle-emoji-picker="toggleEmojiPicker" :toggle-emoji-picker="toggleEmojiPicker"
:message="message" :message="message"
:portal-slug="connectedPortalSlug"
:new-conversation-modal-active="newConversationModalActive" :new-conversation-modal-active="newConversationModalActive"
@selectWhatsappTemplate="openWhatsappTemplateModal" @selectWhatsappTemplate="openWhatsappTemplateModal"
@toggle-editor="toggleRichContentEditor" @toggle-editor="toggleRichContentEditor"
@replace-text="replaceText" @replace-text="replaceText"
@toggle-insert-article="toggleInsertArticle"
/> />
<whatsapp-templates <whatsapp-templates
:inbox-id="inbox.id" :inbox-id="inbox.id"
@@ -154,6 +163,7 @@ import AttachmentPreview from 'dashboard/components/widgets/AttachmentsPreview.v
import ReplyTopPanel from 'dashboard/components/widgets/WootWriter/ReplyTopPanel.vue'; import ReplyTopPanel from 'dashboard/components/widgets/WootWriter/ReplyTopPanel.vue';
import ReplyEmailHead from './ReplyEmailHead.vue'; import ReplyEmailHead from './ReplyEmailHead.vue';
import ReplyBottomPanel from 'dashboard/components/widgets/WootWriter/ReplyBottomPanel.vue'; import ReplyBottomPanel from 'dashboard/components/widgets/WootWriter/ReplyBottomPanel.vue';
import ArticleSearchPopover from 'dashboard/routes/dashboard/helpcenter/components/ArticleSearch/SearchPopover.vue';
import MessageSignatureMissingAlert from './MessageSignatureMissingAlert'; import MessageSignatureMissingAlert from './MessageSignatureMissingAlert';
import Banner from 'dashboard/components/ui/Banner.vue'; import Banner from 'dashboard/components/ui/Banner.vue';
import { REPLY_EDITOR_MODES } from 'dashboard/components/widgets/WootWriter/constants'; import { REPLY_EDITOR_MODES } from 'dashboard/components/widgets/WootWriter/constants';
@@ -184,7 +194,6 @@ import {
replaceSignature, replaceSignature,
extractTextFromMarkdown, extractTextFromMarkdown,
} from 'dashboard/helper/editorHelper'; } from 'dashboard/helper/editorHelper';
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage'; import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
import { LocalStorage } from 'shared/helpers/localStorage'; import { LocalStorage } from 'shared/helpers/localStorage';
@@ -206,6 +215,7 @@ export default {
Banner, Banner,
WhatsappTemplates, WhatsappTemplates,
MessageSignatureMissingAlert, MessageSignatureMissingAlert,
ArticleSearchPopover,
}, },
mixins: [ mixins: [
clickaway, clickaway,
@@ -248,6 +258,7 @@ export default {
showCannedMenu: false, showCannedMenu: false,
showVariablesMenu: false, showVariablesMenu: false,
newConversationModalActive: false, newConversationModalActive: false,
showArticleSearchPopover: false,
}; };
}, },
computed: { computed: {
@@ -264,11 +275,7 @@ export default {
return ( return (
this.inReplyTo?.id && this.inReplyTo?.id &&
!this.isPrivate && !this.isPrivate &&
this.inboxHasFeature(INBOX_FEATURES.REPLY_TO) && this.inboxHasFeature(INBOX_FEATURES.REPLY_TO)
this.isFeatureEnabledonAccount(
this.accountId,
FEATURE_FLAGS.MESSAGE_REPLY_TO
)
); );
}, },
showRichContentEditor() { showRichContentEditor() {
@@ -466,8 +473,7 @@ export default {
return !!this.signatureToApply; return !!this.signatureToApply;
}, },
sendWithSignature() { sendWithSignature() {
const { send_with_signature: isEnabled } = this.uiSettings; return this.fetchSignatureFlagFromUiSettings(this.channelType);
return isEnabled;
}, },
editorMessageKey() { editorMessageKey() {
const { editor_message_key: isEnabled } = this.uiSettings; const { editor_message_key: isEnabled } = this.uiSettings;
@@ -506,11 +512,18 @@ export default {
? this.messageSignature ? this.messageSignature
: extractTextFromMarkdown(this.messageSignature); : extractTextFromMarkdown(this.messageSignature);
}, },
connectedPortalSlug() {
const { help_center: portal = {} } = this.inbox;
const { slug = '' } = portal;
return slug;
},
}, },
watch: { watch: {
currentChat(conversation) { currentChat(conversation) {
const { can_reply: canReply } = conversation; const { can_reply: canReply } = conversation;
this.setCCAndToEmailsFromLastChat();
if (this.isOnPrivateNote) { if (this.isOnPrivateNote) {
return; return;
} }
@@ -521,7 +534,6 @@ export default {
this.replyType = REPLY_EDITOR_MODES.NOTE; this.replyType = REPLY_EDITOR_MODES.NOTE;
} }
this.setCCAndToEmailsFromLastChat();
this.fetchAndSetReplyTo(); this.fetchAndSetReplyTo();
}, },
conversationIdByRoute(conversationId, oldConversationId) { conversationIdByRoute(conversationId, oldConversationId) {
@@ -531,17 +543,25 @@ export default {
} }
}, },
message(updatedMessage) { message(updatedMessage) {
this.hasSlashCommand = // Check if the message starts with a slash.
updatedMessage[0] === '/' && !this.showRichContentEditor; const bodyWithoutSignature = removeSignature(
const hasNextWord = updatedMessage.includes(' '); updatedMessage,
const isShortCodeActive = this.hasSlashCommand && !hasNextWord; this.signatureToApply
if (isShortCodeActive) { );
this.mentionSearchKey = updatedMessage.substring(1); const startsWithSlash = bodyWithoutSignature.startsWith('/');
this.showMentions = true;
} else { // Determine if the user is potentially typing a slash command.
this.mentionSearchKey = ''; // This is true if the message starts with a slash and the rich content editor is not active.
this.showMentions = false; this.hasSlashCommand = startsWithSlash && !this.showRichContentEditor;
} this.showMentions = this.hasSlashCommand;
// If a slash command is active, extract the command text after the slash.
// If not, reset the mentionSearchKey.
this.mentionSearchKey = this.hasSlashCommand
? bodyWithoutSignature.substring(1)
: '';
// Autosave the current message draft.
this.doAutoSaveDraft(); this.doAutoSaveDraft();
}, },
replyType(updatedReplyType, oldReplyType) { replyType(updatedReplyType, oldReplyType) {
@@ -588,6 +608,23 @@ export default {
); );
}, },
methods: { methods: {
handleInsert(article) {
const { url, title } = article;
if (this.isRichEditorEnabled) {
// Removing empty lines from the title
const lines = title.split('\n');
const nonEmptyLines = lines.filter(line => line.trim() !== '');
const filteredMarkdown = nonEmptyLines.join(' ');
bus.$emit(
BUS_EVENTS.INSERT_INTO_RICH_EDITOR,
`[${filteredMarkdown}](${url})`
);
} else {
this.addIntoEditor(
`${this.$t('CONVERSATION.REPLYBOX.INSERT_READ_MORE')} ${url}`
);
}
},
toggleRichContentEditor() { toggleRichContentEditor() {
this.updateUISettings({ this.updateUISettings({
display_rich_content_editor: !this.showRichContentEditor, display_rich_content_editor: !this.showRichContentEditor,
@@ -819,10 +856,18 @@ export default {
this.hideWhatsappTemplatesModal(); this.hideWhatsappTemplatesModal();
}, },
replaceText(message) { replaceText(message) {
if (this.sendWithSignature && !this.private) {
// if signature is enabled, append it to the message
// appendSignature ensures that the signature is not duplicated
// so we don't need to check if the signature is already present
message = appendSignature(message, this.signatureToApply);
}
const updatedMessage = replaceVariablesInMessage({ const updatedMessage = replaceVariablesInMessage({
message, message,
variables: this.messageVariables, variables: this.messageVariables,
}); });
setTimeout(() => { setTimeout(() => {
this.$track(CONVERSATION_EVENTS.INSERTED_A_CANNED_RESPONSE); this.$track(CONVERSATION_EVENTS.INSERTED_A_CANNED_RESPONSE);
this.message = updatedMessage; this.message = updatedMessage;
@@ -845,22 +890,22 @@ export default {
clearEditorSelection() { clearEditorSelection() {
this.updateEditorSelectionWith = ''; this.updateEditorSelectionWith = '';
}, },
insertEmoji(emoji, selectionStart, selectionEnd) { insertIntoTextEditor(text, selectionStart, selectionEnd) {
const { message } = this; const { message } = this;
const newMessage = const newMessage =
message.slice(0, selectionStart) + message.slice(0, selectionStart) +
emoji + text +
message.slice(selectionEnd, message.length); message.slice(selectionEnd, message.length);
this.message = newMessage; this.message = newMessage;
}, },
emojiOnClick(emoji) { addIntoEditor(content) {
if (this.showRichContentEditor) { if (this.showRichContentEditor) {
this.updateEditorSelectionWith = emoji; this.updateEditorSelectionWith = content;
this.onFocus(); this.onFocus();
} }
if (!this.showRichContentEditor) { if (!this.showRichContentEditor) {
const { selectionStart, selectionEnd } = this.$refs.messageInput.$el; const { selectionStart, selectionEnd } = this.$refs.messageInput.$el;
this.insertEmoji(emoji, selectionStart, selectionEnd); this.insertIntoTextEditor(content, selectionStart, selectionEnd);
} }
}, },
clearMessage() { clearMessage() {
@@ -964,6 +1009,19 @@ export default {
(item, index) => itemIndex !== index (item, index) => itemIndex !== index
); );
}, },
setReplyToInPayload(payload) {
if (this.inReplyTo?.id) {
return {
...payload,
contentAttributes: {
...payload.contentAttributes,
in_reply_to: this.inReplyTo.id,
},
};
}
return payload;
},
getMessagePayloadForWhatsapp(message) { getMessagePayloadForWhatsapp(message) {
const multipleMessagePayload = []; const multipleMessagePayload = [];
@@ -973,41 +1031,41 @@ export default {
const attachedFile = this.globalConfig.directUploadsEnabled const attachedFile = this.globalConfig.directUploadsEnabled
? attachment.blobSignedId ? attachment.blobSignedId
: attachment.resource.file; : attachment.resource.file;
const attachmentPayload = { let attachmentPayload = {
conversationId: this.currentChat.id, conversationId: this.currentChat.id,
files: [attachedFile], files: [attachedFile],
private: false, private: false,
message: caption, message: caption,
sender: this.sender, sender: this.sender,
}; };
attachmentPayload = this.setReplyToInPayload(attachmentPayload);
multipleMessagePayload.push(attachmentPayload); multipleMessagePayload.push(attachmentPayload);
caption = ''; caption = '';
}); });
} else { } else {
const messagePayload = { let messagePayload = {
conversationId: this.currentChat.id, conversationId: this.currentChat.id,
message, message,
private: false, private: false,
sender: this.sender, sender: this.sender,
}; };
messagePayload = this.setReplyToInPayload(messagePayload);
multipleMessagePayload.push(messagePayload); multipleMessagePayload.push(messagePayload);
} }
return multipleMessagePayload; return multipleMessagePayload;
}, },
getMessagePayload(message) { getMessagePayload(message) {
const messagePayload = { let messagePayload = {
conversationId: this.currentChat.id, conversationId: this.currentChat.id,
message, message,
private: this.isPrivate, private: this.isPrivate,
sender: this.sender, sender: this.sender,
}; };
messagePayload = this.setReplyToInPayload(messagePayload);
if (this.inReplyTo?.id) {
messagePayload.contentAttributes = {
in_reply_to: this.inReplyTo.id,
};
}
if (this.attachedFiles && this.attachedFiles.length) { if (this.attachedFiles && this.attachedFiles.length) {
messagePayload.files = []; messagePayload.files = [];
@@ -1106,6 +1164,12 @@ export default {
// When new conversation modal is open // When new conversation modal is open
this.newConversationModalActive = isActive; this.newConversationModalActive = isActive;
}, },
onSearchPopoverClose() {
this.showArticleSearchPopover = false;
},
toggleInsertArticle() {
this.showArticleSearchPopover = !this.showArticleSearchPopover;
},
}, },
}; };
</script> </script>
@@ -1124,7 +1188,7 @@ export default {
} }
.reply-box { .reply-box {
@apply border-t border-slate-50 dark:border-slate-700 bg-white dark:bg-slate-900; @apply relative border-t border-slate-50 dark:border-slate-700 bg-white dark:bg-slate-900;
&.is-private { &.is-private {
@apply bg-yellow-50 dark:bg-yellow-200; @apply bg-yellow-50 dark:bg-yellow-200;
@@ -114,10 +114,6 @@ export default {
type: Boolean, type: Boolean,
default: true, default: true,
}, },
hasInstagramStory: {
type: Boolean,
default: true,
},
messageType: { messageType: {
type: Number, type: Number,
default: 1, default: 1,
@@ -196,7 +192,7 @@ export default {
if (!this.showStatusIndicators) { if (!this.showStatusIndicators) {
return false; return false;
} }
// Messages will be marked as sent for the Email channel if they have a source ID.
if (this.isAnEmailChannel) { if (this.isAnEmailChannel) {
return !!this.sourceId; return !!this.sourceId;
} }
@@ -204,24 +200,38 @@ export default {
if ( if (
this.isAWhatsAppChannel || this.isAWhatsAppChannel ||
this.isATwilioChannel || this.isATwilioChannel ||
this.isAFacebookInbox this.isAFacebookInbox ||
this.isASmsInbox ||
this.isATelegramChannel
) { ) {
return this.sourceId && this.isSent; return this.sourceId && this.isSent;
} }
// All messages will be mark as sent for the Line channel, as there is no source ID.
if (this.isALineChannel) {
return true;
}
return false; return false;
}, },
showDeliveredIndicator() { showDeliveredIndicator() {
if (!this.showStatusIndicators) { if (!this.showStatusIndicators) {
return false; return false;
} }
if (
if (this.isAWhatsAppChannel || this.isATwilioChannel) { this.isAWhatsAppChannel ||
this.isATwilioChannel ||
this.isASmsInbox ||
this.isAFacebookInbox
) {
return this.sourceId && this.isDelivered; return this.sourceId && this.isDelivered;
} }
// We will consider messages as delivered for web widget inbox and API inbox if they are sent // All messages marked as delivered for the web widget inbox and API inbox once they are sent.
if (this.isAWebWidgetInbox || this.isAPIInbox) { if (this.isAWebWidgetInbox || this.isAPIInbox) {
return this.isSent; return this.isSent;
} }
if (this.isALineChannel) {
return this.isDelivered;
}
return false; return false;
}, },
@@ -229,12 +239,6 @@ export default {
if (!this.showStatusIndicators) { if (!this.showStatusIndicators) {
return false; return false;
} }
if (this.isAWebWidgetInbox || this.isAPIInbox) {
const { contact_last_seen_at: contactLastSeenAt } = this.currentChat;
return contactLastSeenAt >= this.createdAt;
}
if ( if (
this.isAWhatsAppChannel || this.isAWhatsAppChannel ||
this.isATwilioChannel || this.isATwilioChannel ||
@@ -243,6 +247,10 @@ export default {
return this.sourceId && this.isRead; return this.sourceId && this.isRead;
} }
if (this.isAWebWidgetInbox || this.isAPIInbox) {
return this.isRead;
}
return false; return false;
}, },
}, },
@@ -1,6 +1,6 @@
<template> <template>
<div class="image message-text__wrap"> <div class="image message-text__wrap">
<img :src="url" @click="onClick" @error="onImgError()" /> <img :src="url" @click="onClick" @error="$emit('error')" />
<woot-modal :full-width="true" :show.sync="show" :on-close="onClose"> <woot-modal :full-width="true" :show.sync="show" :on-close="onClose">
<img :src="url" class="modal-image skip-context-menu" /> <img :src="url" class="modal-image skip-context-menu" />
</woot-modal> </woot-modal>
@@ -28,9 +28,6 @@ export default {
onClick() { onClick() {
this.show = true; this.show = true;
}, },
onImgError() {
this.$emit('error');
},
}, },
}; };
</script> </script>
@@ -4,13 +4,14 @@
v-if="isImage && !isImageError" v-if="isImage && !isImageError"
:src="attachment.data_url" :src="attachment.data_url"
@click="onClick" @click="onClick"
@error="onImgError()" @error="onImgError"
/> />
<video <video
v-if="isVideo" v-if="isVideo"
:src="attachment.data_url" :src="attachment.data_url"
muted muted
playsInline playsInline
@error="onImgError"
@click="onClick" @click="onClick"
/> />
<audio v-else-if="isAudio" controls class="skip-context-menu"> <audio v-else-if="isAudio" controls class="skip-context-menu">
@@ -21,7 +22,7 @@
:show.sync="show" :show.sync="show"
:attachment="attachment" :attachment="attachment"
:all-attachments="filteredCurrentChatAttachments" :all-attachments="filteredCurrentChatAttachments"
@error="onImgError()" @error="onImgError"
@close="onClose" @close="onClose"
/> />
</div> </div>
@@ -0,0 +1,51 @@
<template>
<bubble-image
v-if="!hasImgStoryError"
:url="storyUrl"
@error="onImageLoadError"
/>
<bubble-video
v-else-if="!hasVideoStoryError"
:url="storyUrl"
@error="onVideoLoadError"
/>
<instagram-story-error-place-holder v-else />
</template>
<script>
import BubbleImage from './Image.vue';
import BubbleVideo from './Video.vue';
import InstagramStoryErrorPlaceHolder from './InstagramStoryErrorPlaceHolder.vue';
export default {
components: {
BubbleImage,
BubbleVideo,
InstagramStoryErrorPlaceHolder,
},
props: {
storyUrl: {
type: String,
default: '',
},
},
data() {
return {
hasImgStoryError: false,
hasVideoStoryError: false,
};
},
methods: {
onImageLoadError() {
this.hasImgStoryError = true;
this.emitError();
},
onVideoLoadError() {
this.hasVideoStoryError = true;
this.emitError();
},
emitError() {
this.$emit('error');
},
},
};
</script>
@@ -0,0 +1,14 @@
<template>
<div
class="flex items-center justify-center px-8 h-28 w-full bg-slate-100 text-slate-700 dark:bg-slate-500 dark:text-slate-75"
>
<fluent-icon icon="document-error" size="32" />
<p class="mb-0 text-slate-700 dark:text-slate-75">
{{ $t('COMPONENTS.FILE_BUBBLE.INSTAGRAM_STORY_UNAVAILABLE') }}
</p>
</div>
</template>
<script>
export default {};
</script>
@@ -0,0 +1,35 @@
<template>
<blockquote
class="my-0 px-2 pb-0 pt-0 border-l-4 border-solid border-slate-75 dark:border-slate-600 text-slate-600 dark:text-slate-200"
>
<span>{{ $t('CONVERSATION.REPLIED_TO_STORY') }}</span>
<instagram-story :story-url="storyUrl" class="mt-3 rounded-md" />
</blockquote>
</template>
<script>
import InstagramStory from './InstagramStory.vue';
export default {
components: { InstagramStory },
props: {
storyUrl: {
type: String,
default: '',
},
},
data() {
return {
hasImgStoryError: false,
hasVideoStoryError: false,
};
},
methods: {
onImageLoadError() {
this.hasImgStoryError = true;
},
onVideoLoadError() {
this.hasVideoStoryError = true;
},
},
};
</script>
@@ -1,10 +1,11 @@
<template> <template>
<div <div
class="px-2 py-1.5 -mx-2 rounded-sm min-w-[15rem] mb-2" class="px-2 py-1.5 rounded-sm min-w-[10rem] mb-2"
:class="{ :class="{
'bg-slate-100 dark:bg-slate-600 dark:text-slate-50': 'bg-slate-50 dark:bg-slate-600 dark:text-slate-50':
messageType === MESSAGE_TYPE.INCOMING, messageType === MESSAGE_TYPE.INCOMING,
'bg-woot-600 text-woot-50': messageType === MESSAGE_TYPE.OUTGOING, 'bg-woot-600 text-woot-50': messageType === MESSAGE_TYPE.OUTGOING,
'-mx-2': !parentHasAttachments,
}" }"
> >
<message-preview <message-preview
@@ -33,6 +34,10 @@ export default {
type: Number, type: Number,
required: true, required: true,
}, },
parentHasAttachments: {
type: Boolean,
required: true,
},
}, },
data() { data() {
return { MESSAGE_TYPE }; return { MESSAGE_TYPE };
@@ -1,12 +1,12 @@
<template> <template>
<div class="video message-text__wrap"> <div class="video message-text__wrap">
<video :src="url" muted playsInline @click="onClick" /> <video ref="videoElement" :src="url" muted playsInline @click="onClick" />
<woot-modal :show.sync="show" :on-close="onClose"> <woot-modal :show.sync="show" :on-close="onClose">
<video <video
:src="url" :src="url"
controls controls
playsInline playsInline
class="modal-video skip-context-menu" class="modal-video skip-context-menu mx-auto"
/> />
</woot-modal> </woot-modal>
</div> </div>
@@ -25,6 +25,11 @@ export default {
show: false, show: false,
}; };
}, },
mounted() {
this.$refs.videoElement.onerror = () => {
this.$emit('error');
};
},
methods: { methods: {
onClose() { onClose() {
this.show = false; this.show = false;
@@ -1,26 +0,0 @@
<template>
<div class="image-placeholder">
<fluent-icon icon="document-error" size="32" />
<p>{{ $t('COMPONENTS.FILE_BUBBLE.INSTAGRAM_STORY_UNAVAILABLE') }}</p>
</div>
</template>
<script>
export default {};
</script>
<style scoped>
.image-placeholder {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
padding: var(--space-slab);
height: calc(var(--space-large) * 10);
margin-top: var(--space-one);
width: 100%;
background-color: var(--s-75);
color: var(--s-800);
border-radius: var(--border-radius-normal);
}
</style>
+1 -1
View File
@@ -16,5 +16,5 @@ export const FEATURE_FLAGS = {
TEAM_MANAGEMENT: 'team_management', TEAM_MANAGEMENT: 'team_management',
VOICE_RECORDER: 'voice_recorder', VOICE_RECORDER: 'voice_recorder',
AUDIT_LOGS: 'audit_logs', AUDIT_LOGS: 'audit_logs',
MESSAGE_REPLY_TO: 'message_reply_to', INSERT_ARTICLE_IN_REPLY: 'insert_article_in_reply',
}; };
@@ -3,6 +3,7 @@ export const CONVERSATION_EVENTS = Object.freeze({
SENT_MESSAGE: 'Sent a message', SENT_MESSAGE: 'Sent a message',
SENT_PRIVATE_NOTE: 'Sent a private note', SENT_PRIVATE_NOTE: 'Sent a private note',
INSERTED_A_CANNED_RESPONSE: 'Inserted a canned response', INSERTED_A_CANNED_RESPONSE: 'Inserted a canned response',
TRANSLATE_A_MESSAGE: 'Translated a message',
INSERTED_A_VARIABLE: 'Inserted a variable', INSERTED_A_VARIABLE: 'Inserted a variable',
USED_MENTIONS: 'Used mentions', USED_MENTIONS: 'Used mentions',
SEARCH_CONVERSATION: 'Searched conversations', SEARCH_CONVERSATION: 'Searched conversations',
@@ -64,3 +64,47 @@ export const isValidURL = value => {
/^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$/gm; /^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$/gm;
return URL_REGEX.test(value); return URL_REGEX.test(value);
}; };
export const getArticleSearchURL = ({
host,
portalSlug,
pageNumber,
locale,
status,
authorId,
categorySlug,
sort,
query,
}) => {
const queryParams = new URLSearchParams({});
const params = {
page: pageNumber,
locale,
status,
author_id: authorId,
category_slug: categorySlug,
sort,
query,
};
Object.entries(params).forEach(([key, value]) => {
if (value !== null && value !== undefined) {
queryParams.set(key, value);
}
});
return `${host}/${portalSlug}/articles?${queryParams.toString()}`;
};
export const hasValidAvatarUrl = avatarUrl => {
try {
const { host: avatarUrlHost } = new URL(avatarUrl);
const isFromGravatar = ['www.gravatar.com', 'gravatar'].includes(
avatarUrlHost
);
return avatarUrl && !isFromGravatar;
} catch (error) {
return false;
}
};
@@ -135,6 +135,7 @@ export const getActionOptions = ({ agents, teams, labels, type }) => {
assign_team: teams, assign_team: teams,
send_email_to_team: teams, send_email_to_team: teams,
add_label: generateConditionOptions(labels, 'title'), add_label: generateConditionOptions(labels, 'title'),
remove_label: generateConditionOptions(labels, 'title'),
change_priority: PRIORITY_CONDITION_VALUES, change_priority: PRIORITY_CONDITION_VALUES,
}; };
return actionsMap[type]; return actionsMap[type];
@@ -3,6 +3,8 @@ import {
conversationUrl, conversationUrl,
isValidURL, isValidURL,
conversationListPageURL, conversationListPageURL,
getArticleSearchURL,
hasValidAvatarUrl,
} from '../URLHelper'; } from '../URLHelper';
describe('#URL Helpers', () => { describe('#URL Helpers', () => {
@@ -75,4 +77,117 @@ describe('#URL Helpers', () => {
expect(isValidURL('alert.window')).toBe(false); expect(isValidURL('alert.window')).toBe(false);
}); });
}); });
describe('getArticleSearchURL', () => {
it('should generate a basic URL without optional parameters', () => {
const url = getArticleSearchURL({
portalSlug: 'news',
pageNumber: 1,
locale: 'en',
host: 'myurl.com',
});
expect(url).toBe('myurl.com/news/articles?page=1&locale=en');
});
it('should include status parameter if provided', () => {
const url = getArticleSearchURL({
portalSlug: 'news',
pageNumber: 1,
locale: 'en',
status: 'published',
host: 'myurl.com',
});
expect(url).toBe(
'myurl.com/news/articles?page=1&locale=en&status=published'
);
});
it('should include author_id parameter if provided', () => {
const url = getArticleSearchURL({
portalSlug: 'news',
pageNumber: 1,
locale: 'en',
authorId: 123,
host: 'myurl.com',
});
expect(url).toBe(
'myurl.com/news/articles?page=1&locale=en&author_id=123'
);
});
it('should include category_slug parameter if provided', () => {
const url = getArticleSearchURL({
portalSlug: 'news',
pageNumber: 1,
locale: 'en',
categorySlug: 'technology',
host: 'myurl.com',
});
expect(url).toBe(
'myurl.com/news/articles?page=1&locale=en&category_slug=technology'
);
});
it('should include sort parameter if provided', () => {
const url = getArticleSearchURL({
portalSlug: 'news',
pageNumber: 1,
locale: 'en',
sort: 'views',
host: 'myurl.com',
});
expect(url).toBe('myurl.com/news/articles?page=1&locale=en&sort=views');
});
it('should handle multiple optional parameters', () => {
const url = getArticleSearchURL({
portalSlug: 'news',
pageNumber: 1,
locale: 'en',
status: 'draft',
authorId: 456,
categorySlug: 'science',
sort: 'views',
host: 'myurl.com',
});
expect(url).toBe(
'myurl.com/news/articles?page=1&locale=en&status=draft&author_id=456&category_slug=science&sort=views'
);
});
it('should handle missing optional parameters gracefully', () => {
const url = getArticleSearchURL({
portalSlug: 'news',
pageNumber: 1,
locale: 'en',
host: 'myurl.com',
});
expect(url).toBe('myurl.com/news/articles?page=1&locale=en');
});
});
describe('hasValidAvatarUrl', () => {
test('should return true for valid non-Gravatar URL', () => {
expect(hasValidAvatarUrl('https://chatwoot.com/avatar.jpg')).toBe(true);
});
test('should return false for a Gravatar URL (www.gravatar.com)', () => {
expect(hasValidAvatarUrl('https://www.gravatar.com/avatar.jpg')).toBe(
false
);
});
test('should return false for a Gravatar URL (gravatar)', () => {
expect(hasValidAvatarUrl('https://gravatar/avatar.jpg')).toBe(false);
});
test('should handle invalid URL', () => {
expect(hasValidAvatarUrl('invalid-url')).toBe(false); // or expect an error, depending on function design
});
test('should return false for empty or undefined URL', () => {
expect(hasValidAvatarUrl('')).toBe(false);
expect(hasValidAvatarUrl()).toBe(false);
});
});
}); });
@@ -1,6 +1,7 @@
{ {
"EMOJI": { "EMOJI": {
"PLACEHOLDER": "Search emojis", "PLACEHOLDER": "Search emojis",
"NOT_FOUND": "No emoji match your search" "NOT_FOUND": "No emoji match your search",
"REMOVE": "Remove"
} }
} }
@@ -365,7 +365,7 @@
"NAME": { "NAME": {
"LABEL": "Name", "LABEL": "Name",
"PLACEHOLDER": "Category name", "PLACEHOLDER": "Category name",
"HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.", "HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "Name is required" "ERROR": "Name is required"
}, },
"SLUG": { "SLUG": {
@@ -396,7 +396,7 @@
"NAME": { "NAME": {
"LABEL": "Name", "LABEL": "Name",
"PLACEHOLDER": "Category name", "PLACEHOLDER": "Category name",
"HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.", "HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "Name is required" "ERROR": "Name is required"
}, },
"SLUG": { "SLUG": {
@@ -1,6 +1,7 @@
{ {
"EMOJI": { "EMOJI": {
"PLACEHOLDER": "ابحث في الايموجي", "PLACEHOLDER": "ابحث في الايموجي",
"NOT_FOUND": "لا يوجد إيموجي يطابق بحثك" "NOT_FOUND": "لا يوجد إيموجي يطابق بحثك",
"REMOVE": "حذف"
} }
} }
@@ -365,7 +365,7 @@
"NAME": { "NAME": {
"LABEL": "الاسم", "LABEL": "الاسم",
"PLACEHOLDER": "Category name", "PLACEHOLDER": "Category name",
"HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.", "HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "الاسم مطلوب" "ERROR": "الاسم مطلوب"
}, },
"SLUG": { "SLUG": {
@@ -396,7 +396,7 @@
"NAME": { "NAME": {
"LABEL": "الاسم", "LABEL": "الاسم",
"PLACEHOLDER": "Category name", "PLACEHOLDER": "Category name",
"HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.", "HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "الاسم مطلوب" "ERROR": "الاسم مطلوب"
}, },
"SLUG": { "SLUG": {
@@ -1,6 +1,7 @@
{ {
"EMOJI": { "EMOJI": {
"PLACEHOLDER": "Search emojis", "PLACEHOLDER": "Search emojis",
"NOT_FOUND": "No emoji match your search" "NOT_FOUND": "No emoji match your search",
"REMOVE": "Remove"
} }
} }
@@ -365,7 +365,7 @@
"NAME": { "NAME": {
"LABEL": "Име", "LABEL": "Име",
"PLACEHOLDER": "Category name", "PLACEHOLDER": "Category name",
"HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.", "HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "Name is required" "ERROR": "Name is required"
}, },
"SLUG": { "SLUG": {
@@ -396,7 +396,7 @@
"NAME": { "NAME": {
"LABEL": "Име", "LABEL": "Име",
"PLACEHOLDER": "Category name", "PLACEHOLDER": "Category name",
"HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.", "HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "Name is required" "ERROR": "Name is required"
}, },
"SLUG": { "SLUG": {
@@ -1,6 +1,7 @@
{ {
"EMOJI": { "EMOJI": {
"PLACEHOLDER": "Search emojis", "PLACEHOLDER": "Search emojis",
"NOT_FOUND": "No emoji match your search" "NOT_FOUND": "No emoji match your search",
"REMOVE": "Suprimeix"
} }
} }
@@ -365,7 +365,7 @@
"NAME": { "NAME": {
"LABEL": "Nom", "LABEL": "Nom",
"PLACEHOLDER": "Category name", "PLACEHOLDER": "Category name",
"HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.", "HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "Name is required" "ERROR": "Name is required"
}, },
"SLUG": { "SLUG": {
@@ -396,7 +396,7 @@
"NAME": { "NAME": {
"LABEL": "Nom", "LABEL": "Nom",
"PLACEHOLDER": "Category name", "PLACEHOLDER": "Category name",
"HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.", "HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "Name is required" "ERROR": "Name is required"
}, },
"SLUG": { "SLUG": {
@@ -1,6 +1,7 @@
{ {
"EMOJI": { "EMOJI": {
"PLACEHOLDER": "Search emojis", "PLACEHOLDER": "Search emojis",
"NOT_FOUND": "No emoji match your search" "NOT_FOUND": "No emoji match your search",
"REMOVE": "Odebrat"
} }
} }
@@ -365,7 +365,7 @@
"NAME": { "NAME": {
"LABEL": "Název", "LABEL": "Název",
"PLACEHOLDER": "Category name", "PLACEHOLDER": "Category name",
"HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.", "HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "Name is required" "ERROR": "Name is required"
}, },
"SLUG": { "SLUG": {
@@ -396,7 +396,7 @@
"NAME": { "NAME": {
"LABEL": "Název", "LABEL": "Název",
"PLACEHOLDER": "Category name", "PLACEHOLDER": "Category name",
"HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.", "HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "Name is required" "ERROR": "Name is required"
}, },
"SLUG": { "SLUG": {
@@ -1,6 +1,7 @@
{ {
"EMOJI": { "EMOJI": {
"PLACEHOLDER": "Søg emojis", "PLACEHOLDER": "Søg emojis",
"NOT_FOUND": "Ingen emoji matcher din søgning" "NOT_FOUND": "Ingen emoji matcher din søgning",
"REMOVE": "Fjern"
} }
} }
@@ -365,7 +365,7 @@
"NAME": { "NAME": {
"LABEL": "Navn", "LABEL": "Navn",
"PLACEHOLDER": "Kategori navn", "PLACEHOLDER": "Kategori navn",
"HELP_TEXT": "Kategorinavnet vil blive brugt i den offentlige vendende portal til at kategorisere artikler.", "HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "Navn er påkrævet" "ERROR": "Navn er påkrævet"
}, },
"SLUG": { "SLUG": {
@@ -396,7 +396,7 @@
"NAME": { "NAME": {
"LABEL": "Navn", "LABEL": "Navn",
"PLACEHOLDER": "Kategori navn", "PLACEHOLDER": "Kategori navn",
"HELP_TEXT": "Kategorinavnet vil blive brugt i den offentlige vendende portal til at kategorisere artikler.", "HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "Navn er påkrævet" "ERROR": "Navn er påkrævet"
}, },
"SLUG": { "SLUG": {
@@ -1,6 +1,7 @@
{ {
"EMOJI": { "EMOJI": {
"PLACEHOLDER": "Emojis durchsuchen", "PLACEHOLDER": "Emojis durchsuchen",
"NOT_FOUND": "Kein Emoji entspricht deiner Suche" "NOT_FOUND": "Kein Emoji entspricht deiner Suche",
"REMOVE": "Entfernen"
} }
} }
@@ -365,7 +365,7 @@
"NAME": { "NAME": {
"LABEL": "Name", "LABEL": "Name",
"PLACEHOLDER": "Kategoriename", "PLACEHOLDER": "Kategoriename",
"HELP_TEXT": "Der Kategoriename wird im öffentlich zugänglichen Portal verwendet, um Artikel zu kategorisieren.", "HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "Name wird benötigt" "ERROR": "Name wird benötigt"
}, },
"SLUG": { "SLUG": {
@@ -396,7 +396,7 @@
"NAME": { "NAME": {
"LABEL": "Name", "LABEL": "Name",
"PLACEHOLDER": "Kategoriename", "PLACEHOLDER": "Kategoriename",
"HELP_TEXT": "Der Kategoriename wird im öffentlich zugänglichen Portal verwendet, um Artikel zu kategorisieren.", "HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "Name wird benötigt" "ERROR": "Name wird benötigt"
}, },
"SLUG": { "SLUG": {
@@ -1,6 +1,7 @@
{ {
"EMOJI": { "EMOJI": {
"PLACEHOLDER": "Αναζήτηση emojis", "PLACEHOLDER": "Αναζήτηση emojis",
"NOT_FOUND": "Κανένα emoji δεν ταιριάζει με την αναζήτησή σας" "NOT_FOUND": "Κανένα emoji δεν ταιριάζει με την αναζήτησή σας",
"REMOVE": "Διαγραφή"
} }
} }
@@ -365,7 +365,7 @@
"NAME": { "NAME": {
"LABEL": "Όνομα", "LABEL": "Όνομα",
"PLACEHOLDER": "Όνομα κατηγορίας", "PLACEHOLDER": "Όνομα κατηγορίας",
"HELP_TEXT": "Η κατηγορία θα χρησιμοποιηθεί στην πύλη που βλέπει το κοινό για την κατηγοριοποίηση των άρθρων.", "HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "Απαιτείται όνομα" "ERROR": "Απαιτείται όνομα"
}, },
"SLUG": { "SLUG": {
@@ -396,7 +396,7 @@
"NAME": { "NAME": {
"LABEL": "Όνομα", "LABEL": "Όνομα",
"PLACEHOLDER": "Όνομα κατηγορίας", "PLACEHOLDER": "Όνομα κατηγορίας",
"HELP_TEXT": "Η κατηγορία θα χρησιμοποιηθεί στην πύλη που βλέπει το κοινό για την κατηγοριοποίηση των άρθρων.", "HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "Απαιτείται όνομα" "ERROR": "Απαιτείται όνομα"
}, },
"SLUG": { "SLUG": {
@@ -1,18 +1,18 @@
{ {
"FILTER": { "FILTER": {
"TITLE": "Filter Conversations", "TITLE": "Filter conversations",
"SUBTITLE": "Add filters below and hit 'Apply filters' to filter conversations.", "SUBTITLE": "Add your filters below and hit 'Apply filters' to cut through the chat clutter.",
"EDIT_CUSTOM_FILTER": "Edit Folder", "EDIT_CUSTOM_FILTER": "Edit Folder",
"CUSTOM_VIEWS_SUBTITLE": "Add or remove filters and update your folder.", "CUSTOM_VIEWS_SUBTITLE": "Add or remove filters and update your folder.",
"ADD_NEW_FILTER": "Add Filter", "ADD_NEW_FILTER": "Add filter",
"FILTER_DELETE_ERROR": "You should have atleast one filter to save", "FILTER_DELETE_ERROR": "Oops, looks like we can't save nothing! Please add at least one filter to save it.",
"SUBMIT_BUTTON_LABEL": "Apply filters", "SUBMIT_BUTTON_LABEL": "Apply filters",
"UPDATE_BUTTON_LABEL": "Update folder", "UPDATE_BUTTON_LABEL": "Update folder",
"CANCEL_BUTTON_LABEL": "Cancel", "CANCEL_BUTTON_LABEL": "Cancel",
"CLEAR_BUTTON_LABEL": "Clear Filters", "CLEAR_BUTTON_LABEL": "Clear filters",
"EMPTY_VALUE_ERROR": "Value is required",
"FOLDER_LABEL": "Folder Name", "FOLDER_LABEL": "Folder Name",
"FOLDER_QUERY_LABEL": "Folder Query", "FOLDER_QUERY_LABEL": "Folder Query",
"EMPTY_VALUE_ERROR": "Value is required.",
"TOOLTIP_LABEL": "Filter conversations", "TOOLTIP_LABEL": "Filter conversations",
"QUERY_DROPDOWN_LABELS": { "QUERY_DROPDOWN_LABELS": {
"AND": "AND", "AND": "AND",
@@ -36,44 +36,44 @@
}, },
"ATTRIBUTES": { "ATTRIBUTES": {
"STATUS": "Status", "STATUS": "Status",
"ASSIGNEE_NAME": "Assignee Name", "ASSIGNEE_NAME": "Assignee name",
"INBOX_NAME": "Inbox Name", "INBOX_NAME": "Inbox name",
"TEAM_NAME": "Team Name", "TEAM_NAME": "Team name",
"CONVERSATION_IDENTIFIER": "Conversation Identifier", "CONVERSATION_IDENTIFIER": "Conversation identifier",
"CAMPAIGN_NAME": "Campaign Name", "CAMPAIGN_NAME": "Campaign name",
"LABELS": "Labels", "LABELS": "Labels",
"BROWSER_LANGUAGE": "Browser Language", "BROWSER_LANGUAGE": "Browser language",
"PRIORITY": "Priority", "PRIORITY": "Priority",
"COUNTRY_NAME": "Country Name", "COUNTRY_NAME": "Country name",
"REFERER_LINK": "Referer link", "REFERER_LINK": "Referer link",
"CUSTOM_ATTRIBUTE_LIST": "List", "CUSTOM_ATTRIBUTE_LIST": "List",
"CUSTOM_ATTRIBUTE_TEXT": "Text", "CUSTOM_ATTRIBUTE_TEXT": "Text",
"CUSTOM_ATTRIBUTE_NUMBER": "Number", "CUSTOM_ATTRIBUTE_NUMBER": "Number",
"CUSTOM_ATTRIBUTE_LINK": "Link", "CUSTOM_ATTRIBUTE_LINK": "Link",
"CUSTOM_ATTRIBUTE_CHECKBOX": "Checkbox", "CUSTOM_ATTRIBUTE_CHECKBOX": "Checkbox",
"CREATED_AT": "Created At", "CREATED_AT": "Created at",
"LAST_ACTIVITY": "Last Activity" "LAST_ACTIVITY": "Last activity"
}, },
"GROUPS": { "GROUPS": {
"STANDARD_FILTERS": "Standard Filters", "STANDARD_FILTERS": "Standard filters",
"ADDITIONAL_FILTERS": "Additional Filters", "ADDITIONAL_FILTERS": "Additional filters",
"CUSTOM_ATTRIBUTES": "Custom Attributes" "CUSTOM_ATTRIBUTES": "Custom attributes"
}, },
"CUSTOM_VIEWS": { "CUSTOM_VIEWS": {
"ADD": { "ADD": {
"TITLE": "Do you want to save this filter?", "TITLE": "Do you want to save this filter?",
"LABEL": "Name this filter", "LABEL": "Name this filter",
"PLACEHOLDER": "Enter a name for this filter", "PLACEHOLDER": "Name your filter to refer it later.",
"ERROR_MESSAGE": "Name is required", "ERROR_MESSAGE": "Name is required.",
"SAVE_BUTTON": "Save filter", "SAVE_BUTTON": "Save filter",
"CANCEL_BUTTON": "Cancel", "CANCEL_BUTTON": "Cancel",
"API_FOLDERS": { "API_FOLDERS": {
"SUCCESS_MESSAGE": "Folder created successfully", "SUCCESS_MESSAGE": "Folder created successfully.",
"ERROR_MESSAGE": "Error while creating folder" "ERROR_MESSAGE": "Error while creating folder."
}, },
"API_SEGMENTS": { "API_SEGMENTS": {
"SUCCESS_MESSAGE": "Segment created successfully", "SUCCESS_MESSAGE": "Segment created successfully.",
"ERROR_MESSAGE": "Error while creating segment" "ERROR_MESSAGE": "Error while creating segment."
} }
}, },
"EDIT": { "EDIT": {
@@ -83,19 +83,19 @@
"DELETE_BUTTON": "Delete filter", "DELETE_BUTTON": "Delete filter",
"MODAL": { "MODAL": {
"CONFIRM": { "CONFIRM": {
"TITLE": "Confirm Deletion", "TITLE": "Confirm deletion",
"MESSAGE": "Are you sure to delete the filter ", "MESSAGE": "Are you sure to delete the filter ",
"YES": "Yes, Delete", "YES": "Yes, delete",
"NO": "No, Keep it" "NO": "No, keep it"
} }
}, },
"API_FOLDERS": { "API_FOLDERS": {
"SUCCESS_MESSAGE": "Folder deleted successfully", "SUCCESS_MESSAGE": "Folder deleted successfully.",
"ERROR_MESSAGE": "Error while deleting folder" "ERROR_MESSAGE": "Error while deleting folder."
}, },
"API_SEGMENTS": { "API_SEGMENTS": {
"SUCCESS_MESSAGE": "Segment deleted successfully", "SUCCESS_MESSAGE": "Segment deleted successfully.",
"ERROR_MESSAGE": "Error while deleting segment" "ERROR_MESSAGE": "Error while deleting segment."
} }
} }
} }
@@ -1,73 +1,73 @@
{ {
"AGENT_BOTS": { "AGENT_BOTS": {
"HEADER": "Bots", "HEADER": "Bots",
"LOADING_EDITOR": "Loading Editor...", "LOADING_EDITOR": "Loading editor...",
"HEADER_BTN_TXT": "Add Bot Configuration", "HEADER_BTN_TXT": "Add bot configuration",
"SIDEBAR_TXT": "<p><b>Agent Bots</b> <p>Agent bots allows you to automate the conversations</p>", "SIDEBAR_TXT": "<p><b>Agent Bots</b> <p>Agent Bots are like the most fabulous members of your team. They can handle the small stuff, so you can focus on the stuff that matters. Give them a try.</p> <p> You can manage your bots from this page or create new ones using the 'Add bot configuraton' button.</p> <p> Open the <a href=\"https://www.chatwoot.com/hc/chatwoot-user-guide-cloud-version/articles/1677497472-how-to-use-agent-bots\" target=\"blank\">Agent bots handbook</a> in another tab for a helping hand.</p>",
"CSML_BOT_EDITOR": { "CSML_BOT_EDITOR": {
"NAME": { "NAME": {
"LABEL": "Bot Name", "LABEL": "Bot name",
"PLACEHOLDER": "Give your bot a name", "PLACEHOLDER": "Name your bot.",
"ERROR": "Bot name is required" "ERROR": "Bot name is required."
}, },
"DESCRIPTION": { "DESCRIPTION": {
"LABEL": "Bot Description", "LABEL": "Bot description",
"PLACEHOLDER": "What does this bot do?" "PLACEHOLDER": "What does this bot do?"
}, },
"BOT_CONFIG": { "BOT_CONFIG": {
"ERROR": "Please enter your CSML bot configuration above", "ERROR": "Please enter your CSML bot configuration above.",
"API_ERROR": "Your CSML configuration is invalid, please fix it and try again." "API_ERROR": "Your CSML configuration is invalid. Please fix it and try again."
}, },
"SUBMIT": "Validate and save" "SUBMIT": "Validate and save"
}, },
"BOT_CONFIGURATION": { "BOT_CONFIGURATION": {
"TITLE": "Select an agent bot", "TITLE": "Select an agent bot",
"DESC": "You can set an agent bot from the list to this inbox. The bot can initially handle the conversation and transfer it to an agent when needed.", "DESC": "Assign an Agent Bot to your inbox. They can handle initial conversations and transfer them to a live agent when necessary.",
"SUBMIT": "Update", "SUBMIT": "Update",
"DISCONNECT": "Disconnect Bot", "DISCONNECT": "Disconnect bot",
"SUCCESS_MESSAGE": "Successfully updated the agent bot", "SUCCESS_MESSAGE": "Successfully updated the agent bot.",
"DISCONNECTED_SUCCESS_MESSAGE": "Successfully disconnected the agent bot", "DISCONNECTED_SUCCESS_MESSAGE": "Successfully disconnected the agent bot.",
"ERROR_MESSAGE": "Could not update the agent bot, please try again later", "ERROR_MESSAGE": "Could not update the agent bot. Please try again.",
"DISCONNECTED_ERROR_MESSAGE": "Could not disconnect the agent bot, please try again later", "DISCONNECTED_ERROR_MESSAGE": "Could not disconnect the agent bot. Please try again.",
"SELECT_PLACEHOLDER": "Select Bot" "SELECT_PLACEHOLDER": "Select bot"
}, },
"ADD": { "ADD": {
"TITLE": "Configure new bot", "TITLE": "Configure new bot",
"CANCEL_BUTTON_TEXT": "Cancel", "CANCEL_BUTTON_TEXT": "Cancel",
"API": { "API": {
"SUCCESS_MESSAGE": "Bot added successfully", "SUCCESS_MESSAGE": "Bot added successfully.",
"ERROR_MESSAGE": "Could not add bot, Please try again later" "ERROR_MESSAGE": "Could not add bot. Please try again later."
} }
}, },
"LIST": { "LIST": {
"404": "No Bots found, you can create a bot by clicking the 'Configure new bot' Button ↗", "404": "No bots found. You can create a bot by clicking the 'Configure new bot' button ↗",
"LOADING": "Fetching Bots...", "LOADING": "Fetching bots...",
"TYPE": "Bot Type" "TYPE": "Bot type"
}, },
"DELETE": { "DELETE": {
"BUTTON_TEXT": "Delete", "BUTTON_TEXT": "Delete",
"TITLE": "Delete Bot", "TITLE": "Delete bot",
"SUBMIT": "Delete", "SUBMIT": "Delete",
"CANCEL_BUTTON_TEXT": "Cancel", "CANCEL_BUTTON_TEXT": "Cancel",
"DESCRIPTION": "Are you sure you want to delete this bot? This action is irreversible", "DESCRIPTION": "Are you sure you want to delete this bot? This action is irreversible.",
"API": { "API": {
"SUCCESS_MESSAGE": "Bot deleted successfully", "SUCCESS_MESSAGE": "Bot deleted successfully.",
"ERROR_MESSAGE": "Could not able to delete bot, Please try again later" "ERROR_MESSAGE": "Could not delete bot. Please try again."
} }
}, },
"EDIT": { "EDIT": {
"BUTTON_TEXT": "Edit", "BUTTON_TEXT": "Edit",
"LOADING": "Fetching Bots...", "LOADING": "Fetching bots...",
"TITLE": "Edit Bot", "TITLE": "Edit bot",
"CANCEL_BUTTON_TEXT": "Cancel", "CANCEL_BUTTON_TEXT": "Cancel",
"API": { "API": {
"SUCCESS_MESSAGE": "Bot updated successfully", "SUCCESS_MESSAGE": "Bot updated successfully.",
"ERROR_MESSAGE": "Could not update bot, Please try again later" "ERROR_MESSAGE": "Could not update bot. Please try again."
} }
}, },
"TYPES": { "TYPES": {
"WEBHOOK": "Webhook Bot", "WEBHOOK": "Webhook bot",
"CSML": "CSML Bot" "CSML": "CSML bot"
} }
} }
} }
@@ -1,40 +1,40 @@
{ {
"BULK_ACTION": { "BULK_ACTION": {
"CONVERSATIONS_SELECTED": "%{conversationCount} conversations selected", "CONVERSATIONS_SELECTED": "%{conversationCount} conversations selected",
"AGENT_SELECT_LABEL": "Select Agent", "AGENT_SELECT_LABEL": "Select agent",
"ASSIGN_CONFIRMATION_LABEL": "Are you sure to assign %{conversationCount} %{conversationLabel} to", "ASSIGN_CONFIRMATION_LABEL": "Are you sure to assign %{conversationCount} %{conversationLabel} to",
"UNASSIGN_CONFIRMATION_LABEL": "Are you sure to unassign %{conversationCount} %{conversationLabel}?", "UNASSIGN_CONFIRMATION_LABEL": "Are you sure to unassign %{conversationCount} %{conversationLabel}?",
"GO_BACK_LABEL": "Go back", "GO_BACK_LABEL": "Go back",
"ASSIGN_LABEL": "Assign", "ASSIGN_LABEL": "Assign",
"YES": "Yes", "YES": "Yes",
"ASSIGN_AGENT_TOOLTIP": "Assign Agent", "ASSIGN_AGENT_TOOLTIP": "Assign agent",
"ASSIGN_TEAM_TOOLTIP": "Assign team", "ASSIGN_TEAM_TOOLTIP": "Assign team",
"ASSIGN_SUCCESFUL": "Conversations assigned successfully", "ASSIGN_SUCCESFUL": "Conversations assigned successfully.",
"ASSIGN_FAILED": "Failed to assign conversations, please try again", "ASSIGN_FAILED": "Failed to assign conversations. Please try again.",
"RESOLVE_SUCCESFUL": "Conversations resolved successfully", "RESOLVE_SUCCESFUL": "Conversations resolved successfully.",
"RESOLVE_FAILED": "Failed to resolve conversations, please try again", "RESOLVE_FAILED": "Failed to resolve conversations. Please try again.",
"ALL_CONVERSATIONS_SELECTED_ALERT": "Conversations visible on this page are only selected.", "ALL_CONVERSATIONS_SELECTED_ALERT": "Conversations visible on this page are only selected.",
"AGENT_LIST_LOADING": "Loading Agents", "AGENT_LIST_LOADING": "Loading agents",
"UPDATE": { "UPDATE": {
"CHANGE_STATUS": "Change status", "CHANGE_STATUS": "Change status",
"SNOOZE_UNTIL_NEXT_REPLY": "Snooze until next reply", "SNOOZE_UNTIL_NEXT_REPLY": "Snooze until next reply.",
"UPDATE_SUCCESFUL": "Conversation status updated successfully.", "UPDATE_SUCCESFUL": "Conversation status updated successfully.",
"UPDATE_FAILED": "Failed to update conversations, please try again" "UPDATE_FAILED": "Failed to update conversations. Please try again."
}, },
"LABELS": { "LABELS": {
"ASSIGN_LABELS": "Assign Labels", "ASSIGN_LABELS": "Assign labels",
"NO_LABELS_FOUND": "No labels found for", "NO_LABELS_FOUND": "No labels found for",
"ASSIGN_SELECTED_LABELS": "Assign selected labels", "ASSIGN_SELECTED_LABELS": "Assign selected labels",
"ASSIGN_SUCCESFUL": "Labels assigned successfully", "ASSIGN_SUCCESFUL": "Labels assigned successfully.",
"ASSIGN_FAILED": "Failed to assign labels, please try again" "ASSIGN_FAILED": "Failed to assign labels. Please try again."
}, },
"TEAMS": { "TEAMS": {
"TEAM_SELECT_LABEL": "Select Team", "TEAM_SELECT_LABEL": "Select team",
"NONE": "None", "NONE": "None",
"NO_TEAMS_AVAILABLE": "There are no teams added to this account yet.", "NO_TEAMS_AVAILABLE": "There are no teams added to this account yet.",
"ASSIGN_SELECTED_TEAMS": "Assign selected team", "ASSIGN_SELECTED_TEAMS": "Assign selected team.",
"ASSIGN_SUCCESFUL": "Teams assiged successfully", "ASSIGN_SUCCESFUL": "Teams assiged successfully.",
"ASSIGN_FAILED": "Failed to assign team, please try again" "ASSIGN_FAILED": "Failed to assign team. Please try again."
} }
} }
} }
@@ -1,71 +1,71 @@
{ {
"CANNED_MGMT": { "CANNED_MGMT": {
"HEADER": "Canned Responses", "HEADER": "Canned Responses",
"HEADER_BTN_TXT": "Add Canned Response", "HEADER_BTN_TXT": "Add canned response",
"LOADING": "Fetching Canned Responses", "LOADING": "Fetching canned responses...",
"SEARCH_404": "There are no items matching this query", "SEARCH_404": "There are no items matching this query.",
"SIDEBAR_TXT": "<p><b>Canned Responses</b> </p><p> Canned Responses are saved reply templates which can be used to quickly send out a reply to a conversation. </p><p> For creating a Canned Response, just click on the <b>Add Canned Response</b>. You can also edit or delete an existing Canned Response by clicking on the Edit or Delete button </p><p> Canned responses are used with the help of <b>Short Codes</b>. Agents can access canned responses while on a chat by typing <b>'/'</b> followed by the short code. </p>", "SIDEBAR_TXT": "<p><b>Canned Responses</b> </p><p> Canned Responses are pre-written reply templates that help you quickly respond to a conversation. To insert a canned response during a chat, agents can type a short code preceded by a '/' character. </p><p> You can manage your canned responses from this page or create new ones using the \"Add canned response\" button.</p><p>Open the <a target=\"_blank\" href=\"https://www.chatwoot.com/hc/chatwoot-user-guide-cloud-version/articles/1677501325-how-to-create-saved-reply-templates-with-canned-responses\">Canned Responses handbook</a> in another tab for a helping hand.</p><p>Also, check out the all-new <a href=\"https://www.chatwoot.com/tools/canned-responses-library\" target=\"_blank\">Canned Responses Library</a>.</p>",
"LIST": { "LIST": {
"404": "There are no canned responses available in this account.", "404": "There are no canned responses available in this account.",
"TITLE": "Manage canned responses", "TITLE": "Manage canned responses",
"DESC": "Canned Responses are predefined reply templates which can be used to quickly send out replies to tickets.", "DESC": "Canned Responses are predefined reply templates which can be used to quickly send out replies to conversations.",
"TABLE_HEADER": ["Short Code", "Content", "Actions"] "TABLE_HEADER": ["Short code", "Content", "Actions"]
}, },
"ADD": { "ADD": {
"TITLE": "Add Canned Response", "TITLE": "Add canned response",
"DESC": "Canned Responses are saved reply templates which can be used to quickly send out reply to conversation.", "DESC": "Canned Responses are predefined reply templates which can be used to quickly send out replies to conversations.",
"CANCEL_BUTTON_TEXT": "Cancel", "CANCEL_BUTTON_TEXT": "Cancel",
"FORM": { "FORM": {
"SHORT_CODE": { "SHORT_CODE": {
"LABEL": "Short Code", "LABEL": "Short code",
"PLACEHOLDER": "Please enter a short code", "PLACEHOLDER": "Please enter a short code.",
"ERROR": "Short Code is required" "ERROR": "Short Code is required."
}, },
"CONTENT": { "CONTENT": {
"LABEL": "Content", "LABEL": "Message",
"PLACEHOLDER": "Please enter a content", "PLACEHOLDER": "Please write the message you want to save as a template to use later.",
"ERROR": "Content is required" "ERROR": "Message is required."
}, },
"SUBMIT": "Submit" "SUBMIT": "Submit"
}, },
"API": { "API": {
"SUCCESS_MESSAGE": "Canned Response added successfully", "SUCCESS_MESSAGE": "Canned response added successfully.",
"ERROR_MESSAGE": "Could not create canned response. Please try again later." "ERROR_MESSAGE": "Could not connect to Woot server. Please try again."
} }
}, },
"EDIT": { "EDIT": {
"TITLE": "Edit Canned Response", "TITLE": "Edit canned response",
"CANCEL_BUTTON_TEXT": "Cancel", "CANCEL_BUTTON_TEXT": "Cancel",
"FORM": { "FORM": {
"SHORT_CODE": { "SHORT_CODE": {
"LABEL": "Short Code", "LABEL": "Short code",
"PLACEHOLDER": "Please enter a shortcode", "PLACEHOLDER": "Please enter a shortcode.",
"ERROR": "Short Code is required" "ERROR": "Short code is required."
}, },
"CONTENT": { "CONTENT": {
"LABEL": "Content", "LABEL": "Message",
"PLACEHOLDER": "Please enter a content", "PLACEHOLDER": "Please write the message you want to save as a template to use later.",
"ERROR": "Content is required" "ERROR": "Message is required."
}, },
"SUBMIT": "Submit" "SUBMIT": "Submit"
}, },
"BUTTON_TEXT": "Edit", "BUTTON_TEXT": "Edit",
"API": { "API": {
"SUCCESS_MESSAGE": "Canned Response updated successfully", "SUCCESS_MESSAGE": "Canned response is updated successfully.",
"ERROR_MESSAGE": "Could not update canned response. Please try again later." "ERROR_MESSAGE": "Could not connect to Woot server. Please try again."
} }
}, },
"DELETE": { "DELETE": {
"BUTTON_TEXT": "Delete", "BUTTON_TEXT": "Delete",
"API": { "API": {
"SUCCESS_MESSAGE": "Canned response deleted successfully", "SUCCESS_MESSAGE": "Canned response deleted successfully.",
"ERROR_MESSAGE": "Could not delete canned response. Please try again later." "ERROR_MESSAGE": "Could not connect to Woot server. Please try again."
}, },
"CONFIRM": { "CONFIRM": {
"TITLE": "Confirm Deletion", "TITLE": "Confirm deletion",
"MESSAGE": "Are you sure to delete ", "MESSAGE": "Are you sure to delete ",
"YES": "Yes, Delete ", "YES": "Yes, delete ",
"NO": "No, Keep " "NO": "No, keep "
} }
} }
} }
@@ -139,6 +139,7 @@
"PRIVATE_NOTE": "Private Note", "PRIVATE_NOTE": "Private Note",
"SEND": "Send", "SEND": "Send",
"CREATE": "Add Note", "CREATE": "Add Note",
"INSERT_READ_MORE": "Read more",
"DISMISS_REPLY": "Dismiss reply", "DISMISS_REPLY": "Dismiss reply",
"REPLYING_TO": "Replying to:", "REPLYING_TO": "Replying to:",
"TIP_FORMAT_ICON": "Show rich text editor", "TIP_FORMAT_ICON": "Show rich text editor",
@@ -1,6 +1,7 @@
{ {
"EMOJI": { "EMOJI": {
"PLACEHOLDER": "Search emojis", "PLACEHOLDER": "Search emojis",
"NOT_FOUND": "No emoji match your search" "NOT_FOUND": "No emoji match your search",
"REMOVE": "Remove"
} }
} }
@@ -76,6 +76,9 @@
}, },
"ARTICLE_SEARCH_RESULT": { "ARTICLE_SEARCH_RESULT": {
"UNCATEGORIZED": "Uncategorized", "UNCATEGORIZED": "Uncategorized",
"SEARCH_RESULTS": "Search results for %{query}",
"EMPTY_TEXT": "Search for articles to insert into replies.",
"SEARCH_LOADER": "Searching...",
"INSERT_ARTICLE": "Insert", "INSERT_ARTICLE": "Insert",
"NO_RESULT": "No articles found", "NO_RESULT": "No articles found",
"COPY_LINK": "Copy article link to clipboard", "COPY_LINK": "Copy article link to clipboard",
@@ -220,7 +223,10 @@
"LOGO": { "LOGO": {
"LABEL": "Logo", "LABEL": "Logo",
"UPLOAD_BUTTON": "Upload logo", "UPLOAD_BUTTON": "Upload logo",
"HELP_TEXT": "This logo will be displayed on the portal header." "HELP_TEXT": "This logo will be displayed on the portal header.",
"IMAGE_UPLOAD_SUCCESS": "Logo uploaded successfully",
"IMAGE_UPLOAD_ERROR": "Logo deleted successfully",
"IMAGE_DELETE_ERROR": "Error while deleting logo"
}, },
"NAME": { "NAME": {
"LABEL": "Name", "LABEL": "Name",
@@ -365,7 +371,7 @@
"NAME": { "NAME": {
"LABEL": "Name", "LABEL": "Name",
"PLACEHOLDER": "Category name", "PLACEHOLDER": "Category name",
"HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.", "HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "Name is required" "ERROR": "Name is required"
}, },
"SLUG": { "SLUG": {
@@ -396,7 +402,7 @@
"NAME": { "NAME": {
"LABEL": "Name", "LABEL": "Name",
"PLACEHOLDER": "Category name", "PLACEHOLDER": "Category name",
"HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.", "HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "Name is required" "ERROR": "Name is required"
}, },
"SLUG": { "SLUG": {
@@ -426,6 +432,21 @@
} }
} }
}, },
"ARTICLE_SEARCH": {
"TITLE": "Search articles",
"PLACEHOLDER": "Search articles",
"NO_RESULT": "No articles found",
"SEARCHING": "Searching...",
"SEARCH_BUTTON": "Search",
"INSERT_ARTICLE": "Insert link",
"IFRAME_ERROR": "URL is empty or invalid. Unable to display content.",
"OPEN_ARTICLE_SEARCH": "Insert article from Help Center",
"SUCCESS_ARTICLE_INSERTED": "Article inserted successfully",
"PREVIEW_LINK": "Preview article",
"CANCEL": "Close",
"BACK": "Back",
"BACK_RESULTS": "Back to results"
},
"UPGRADE_PAGE": { "UPGRADE_PAGE": {
"TITLE": "Help Center", "TITLE": "Help Center",
"DESCRIPTION": "Create user-friendly self-service portals. Help your users to access the articles and get support 24/7. Upgrade your subscription to enable this feature.", "DESCRIPTION": "Create user-friendly self-service portals. Help your users to access the articles and get support 24/7. Upgrade your subscription to enable this feature.",
@@ -139,8 +139,8 @@
"PRIVATE_NOTE": "Nota privada", "PRIVATE_NOTE": "Nota privada",
"SEND": "Enviar", "SEND": "Enviar",
"CREATE": "Añadir nota", "CREATE": "Añadir nota",
"DISMISS_REPLY": "Dismiss reply", "DISMISS_REPLY": "Descartar respuesta",
"REPLYING_TO": "Replying to:", "REPLYING_TO": "Respondiendo a:",
"TIP_FORMAT_ICON": "Mostrar editor de textos", "TIP_FORMAT_ICON": "Mostrar editor de textos",
"TIP_EMOJI_ICON": "Mostrar selector de emoji", "TIP_EMOJI_ICON": "Mostrar selector de emoji",
"TIP_ATTACH_ICON": "Adjuntar archivos", "TIP_ATTACH_ICON": "Adjuntar archivos",
@@ -195,7 +195,7 @@
}, },
"CONTEXT_MENU": { "CONTEXT_MENU": {
"COPY": "Copiar", "COPY": "Copiar",
"REPLY_TO": "Reply to this message", "REPLY_TO": "Responder a este mensaje",
"DELETE": "Eliminar", "DELETE": "Eliminar",
"CREATE_A_CANNED_RESPONSE": "Añadir a respuestas predefinidas", "CREATE_A_CANNED_RESPONSE": "Añadir a respuestas predefinidas",
"TRANSLATE": "Traducir", "TRANSLATE": "Traducir",
@@ -1,6 +1,7 @@
{ {
"EMOJI": { "EMOJI": {
"PLACEHOLDER": "Buscar emojis", "PLACEHOLDER": "Buscar emojis",
"NOT_FOUND": "Ningún emoji coincide con tu búsqueda" "NOT_FOUND": "Ningún emoji coincide con tu búsqueda",
"REMOVE": "Eliminar"
} }
} }
@@ -365,7 +365,7 @@
"NAME": { "NAME": {
"LABEL": "Nombre", "LABEL": "Nombre",
"PLACEHOLDER": "Nombre de categoría", "PLACEHOLDER": "Nombre de categoría",
"HELP_TEXT": "El nombre de la categoría se utilizará en el portal público orientado a categorizar los artículos.", "HELP_TEXT": "El nombre de la categoría y el icono se utilizarán en el portal público para categorizar los artículos.",
"ERROR": "El nombre es requerido" "ERROR": "El nombre es requerido"
}, },
"SLUG": { "SLUG": {
@@ -396,7 +396,7 @@
"NAME": { "NAME": {
"LABEL": "Nombre", "LABEL": "Nombre",
"PLACEHOLDER": "Nombre de categoría", "PLACEHOLDER": "Nombre de categoría",
"HELP_TEXT": "El nombre de la categoría se utilizará en el portal público orientado a categorizar los artículos.", "HELP_TEXT": "El nombre de la categoría y el icono se utilizarán en el portal público para categorizar los artículos.",
"ERROR": "El nombre es requerido" "ERROR": "El nombre es requerido"
}, },
"SLUG": { "SLUG": {
@@ -428,28 +428,28 @@
}, },
"UPGRADE_PAGE": { "UPGRADE_PAGE": {
"TITLE": "Centro de ayuda", "TITLE": "Centro de ayuda",
"DESCRIPTION": "Create user-friendly self-service portals. Help your users to access the articles and get support 24/7. Upgrade your subscription to enable this feature.", "DESCRIPTION": "Crear portales de autoservicio amigables. Ayuda a tus usuarios a acceder a los artículos y obtener soporte 24/7. Actualiza tu suscripción para activar esta característica.",
"SELF_HOSTED_DESCRIPTION": "Create user-friendly self-service portals. Help your users to access the articles and get support 24/7. Please contact your administrator to enable this feature.", "SELF_HOSTED_DESCRIPTION": "Crear portales de autoservicio amigables. Ayuda a tus usuarios a acceder a los artículos y obtener soporte 24/7. Póngase en contacto con su administrador para habilitar esta característica.",
"BUTTON": { "BUTTON": {
"LEARN_MORE": "Más información", "LEARN_MORE": "Más información",
"UPGRADE": "Upgrade" "UPGRADE": "Mejorar"
}, },
"FEATURES": { "FEATURES": {
"PORTALS": { "PORTALS": {
"TITLE": "Multiple portals", "TITLE": "Múltiples portales",
"DESCRIPTION": "Create multiple help center portals for different products using the same account." "DESCRIPTION": "Crear múltiples portales de centro de ayuda para diferentes productos usando la misma cuenta."
}, },
"LOCALES": { "LOCALES": {
"TITLE": "Full support for locales", "TITLE": "Soporte completo para locales",
"DESCRIPTION": "Localize the portal in your language. We support all locales and allow translations for every article." "DESCRIPTION": "Localiza el portal en tu idioma. Apoyamos todas las localizaciones y permitimos traducciones para cada artículo."
}, },
"SEO": { "SEO": {
"TITLE": "SEO-friendly design", "TITLE": "Diseño amigable con SEO",
"DESCRIPTION": "Customize your meta tags to improve your visibility on search engines with our SEO-friendly pages." "DESCRIPTION": "Personalice sus meta-etiquetas para mejorar su visibilidad en los motores de búsqueda con nuestras páginas amigables con SEO."
}, },
"API": { "API": {
"TITLE": "Full API support", "TITLE": "Soporte completo de API",
"DESCRIPTION": "Use the portal as a headless CMS with third party front-end frameworks using our APIs." "DESCRIPTION": "Utilice el portal como un CMS sin cabeceras con frameworks front-end de terceros usando nuestras APIs."
} }
} }
} }
@@ -39,7 +39,7 @@
"SORT_TOOLTIP_LABEL": "مرتب‌سازی گفتگوها", "SORT_TOOLTIP_LABEL": "مرتب‌سازی گفتگوها",
"CHAT_SORT": { "CHAT_SORT": {
"STATUS": "وضعیت", "STATUS": "وضعیت",
"ORDER_BY": "سفارش توسط" "ORDER_BY": "مرتب بر اساس"
}, },
"CHAT_TIME_STAMP": { "CHAT_TIME_STAMP": {
"CREATED": { "CREATED": {
@@ -62,7 +62,7 @@
"TEXT": "اولویت" "TEXT": "اولویت"
}, },
"sort_on_waiting_since": { "sort_on_waiting_since": {
"TEXT": "پاسخ در انتظار" "TEXT": "در انتظار پاسخ"
} }
}, },
"ATTACHMENTS": { "ATTACHMENTS": {
@@ -9,7 +9,7 @@
"LOCATION": "مکان", "LOCATION": "مکان",
"BROWSER_LANGUAGE": "مرور زبان", "BROWSER_LANGUAGE": "مرور زبان",
"CONVERSATION_TITLE": "جزئیات مکالمه", "CONVERSATION_TITLE": "جزئیات مکالمه",
"VIEW_PROFILE": "نمایش مشخصات", "VIEW_PROFILE": "نمایش پروفایل",
"BROWSER": "مرورگر", "BROWSER": "مرورگر",
"OS": "سیستم عامل", "OS": "سیستم عامل",
"INITIATED_FROM": "شروع شده از", "INITIATED_FROM": "شروع شده از",
@@ -62,13 +62,13 @@
"DESC": "اطلاعات اولیه درباره مخاطب را اضافه کنید." "DESC": "اطلاعات اولیه درباره مخاطب را اضافه کنید."
}, },
"IMPORT_CONTACTS": { "IMPORT_CONTACTS": {
"BUTTON_LABEL": "وارد كردن", "BUTTON_LABEL": "تزریق کردن",
"TITLE": "وارد کردن مخاطبین", "TITLE": "تزریق مخاطبین",
"DESC": "مخاطبین را از طریق یک فایل CSV وارد کنید.", "DESC": "مخاطبین را از طریق یک فایل CSV وارد کنید.",
"DOWNLOAD_LABEL": "نمونه csv را بارگیری کنید.", "DOWNLOAD_LABEL": "نمونه csv را بارگیری کنید.",
"FORM": { "FORM": {
"LABEL": "پرونده CSV", "LABEL": "پرونده CSV",
"SUBMIT": "وارد كردن", "SUBMIT": "تزریق کردن",
"CANCEL": "انصراف" "CANCEL": "انصراف"
}, },
"SUCCESS_MESSAGE": "You will be notified via email when the import is complete.", "SUCCESS_MESSAGE": "You will be notified via email when the import is complete.",
@@ -1,7 +1,7 @@
{ {
"CONVERSATION": { "CONVERSATION": {
"SELECT_A_CONVERSATION": "لطفا یک گفتگو را از پنجره گفتگوها انتخاب کنید", "SELECT_A_CONVERSATION": "لطفا یک گفتگو را از پنجره گفتگوها انتخاب کنید",
"CSAT_REPLY_MESSAGE": "لطفاً به مکالمه امتیاز دهید", "CSAT_REPLY_MESSAGE": "لطفاً به گفتگو امتیاز دهید",
"404": "با عرض پوزش، ما نمی‌توانیم گفتگو را پیدا کنیم. لطفا دوباره تلاش کنید", "404": "با عرض پوزش، ما نمی‌توانیم گفتگو را پیدا کنیم. لطفا دوباره تلاش کنید",
"SWITCH_VIEW_LAYOUT": "تغییر طرح‌بندی", "SWITCH_VIEW_LAYOUT": "تغییر طرح‌بندی",
"DASHBOARD_APP_TAB_MESSAGES": "پیام‌ها", "DASHBOARD_APP_TAB_MESSAGES": "پیام‌ها",
@@ -37,7 +37,7 @@
"REPLYING_TO": "شما در حال پاسخ دادن به:", "REPLYING_TO": "شما در حال پاسخ دادن به:",
"REMOVE_SELECTION": "حذف انتخاب‌شده‌ها", "REMOVE_SELECTION": "حذف انتخاب‌شده‌ها",
"DOWNLOAD": "دانلود", "DOWNLOAD": "دانلود",
"UNKNOWN_FILE_TYPE": "پرونده ناشناخته", "UNKNOWN_FILE_TYPE": "فایل ناشناخته",
"SAVE_CONTACT": "ذخیره", "SAVE_CONTACT": "ذخیره",
"UPLOADING_ATTACHMENTS": "در حال بارگذاری پیوست‌ها...", "UPLOADING_ATTACHMENTS": "در حال بارگذاری پیوست‌ها...",
"REPLIED_TO_STORY": "Replied to your story", "REPLIED_TO_STORY": "Replied to your story",
@@ -46,7 +46,7 @@
"NO_RESPONSE": "بدون پاسخ", "NO_RESPONSE": "بدون پاسخ",
"RATING_TITLE": "رتبه", "RATING_TITLE": "رتبه",
"FEEDBACK_TITLE": "بازخورد", "FEEDBACK_TITLE": "بازخورد",
"REPLY_MESSAGE_NOT_FOUND": "Message not available", "REPLY_MESSAGE_NOT_FOUND": "پیام در دسترس نیست",
"CARD": { "CARD": {
"SHOW_LABELS": "مشاهده کردن برچسب‌ها", "SHOW_LABELS": "مشاهده کردن برچسب‌ها",
"HIDE_LABELS": "پنهان کردن برچسب‌ها" "HIDE_LABELS": "پنهان کردن برچسب‌ها"
@@ -58,7 +58,7 @@
"OPEN": "بیشتر", "OPEN": "بیشتر",
"CLOSE": "بستن", "CLOSE": "بستن",
"DETAILS": "جزئیات", "DETAILS": "جزئیات",
"SNOOZED_UNTIL": "Snoozed until", "SNOOZED_UNTIL": "به تعویق افتاده تا",
"SNOOZED_UNTIL_TOMORROW": "تا فردا به تعویق افتاد", "SNOOZED_UNTIL_TOMORROW": "تا فردا به تعویق افتاد",
"SNOOZED_UNTIL_NEXT_WEEK": "تا هفته آینده به تعویق افتاد", "SNOOZED_UNTIL_NEXT_WEEK": "تا هفته آینده به تعویق افتاد",
"SNOOZED_UNTIL_NEXT_REPLY": "تا پاسخ بعدی به تعویق افتاد" "SNOOZED_UNTIL_NEXT_REPLY": "تا پاسخ بعدی به تعویق افتاد"
@@ -76,23 +76,23 @@
"CUSTOM_SNOOZE": { "CUSTOM_SNOOZE": {
"TITLE": "به تعویق انداختن تا", "TITLE": "به تعویق انداختن تا",
"APPLY": "به تعویق انداختن", "APPLY": "به تعویق انداختن",
"CANCEL": "انصراف" "CANCEL": "لغو"
}, },
"PRIORITY": { "PRIORITY": {
"TITLE": "اولویت", "TITLE": "اولویت",
"OPTIONS": { "OPTIONS": {
"NONE": "هیچکدام", "NONE": "هیچکدام",
"URGENT": "Urgent", "URGENT": "Urgent",
"HIGH": "High", "HIGH": "بالا",
"MEDIUM": "Medium", "MEDIUM": "متوسط",
"LOW": "Low" "LOW": "پایین"
}, },
"CHANGE_PRIORITY": { "CHANGE_PRIORITY": {
"SELECT_PLACEHOLDER": "هیچکدام", "SELECT_PLACEHOLDER": "هیچکدام",
"INPUT_PLACEHOLDER": "Select priority", "INPUT_PLACEHOLDER": "انتخاب اولویت",
"NO_RESULTS": "نتیجه‌ای یافت نشد", "NO_RESULTS": "نتیجه‌ای یافت نشد",
"SUCCESSFUL": "Changed priority of conversation id %{conversationId} to %{priority}", "SUCCESSFUL": "تغییر اولویت گفتگوی با شناسه %{conversationId} به %{priority}",
"FAILED": "Couldn't change priority. Please try again." "FAILED": "تغییر اولویت گفتگو با موفقیت انجام نشد. لطفا دوباره تلاش نمایید."
} }
}, },
"CARD_CONTEXT_MENU": { "CARD_CONTEXT_MENU": {
@@ -139,8 +139,8 @@
"PRIVATE_NOTE": "یادداشت خصوصی", "PRIVATE_NOTE": "یادداشت خصوصی",
"SEND": "ارسال", "SEND": "ارسال",
"CREATE": "افزودن یادداشت", "CREATE": "افزودن یادداشت",
"DISMISS_REPLY": "Dismiss reply", "DISMISS_REPLY": "رد پاسخ",
"REPLYING_TO": "Replying to:", "REPLYING_TO": "در حال پاسخ دهی به:",
"TIP_FORMAT_ICON": "نمایش ویرایشگر متنی پیشرفته", "TIP_FORMAT_ICON": "نمایش ویرایشگر متنی پیشرفته",
"TIP_EMOJI_ICON": "انتخاب ایموجی", "TIP_EMOJI_ICON": "انتخاب ایموجی",
"TIP_ATTACH_ICON": "ضمیمه فایل", "TIP_ATTACH_ICON": "ضمیمه فایل",
@@ -152,10 +152,10 @@
"STOP_AUDIO_RECORDING": "در حال توقف ضبط صدا", "STOP_AUDIO_RECORDING": "در حال توقف ضبط صدا",
"": "", "": "",
"EMAIL_HEAD": { "EMAIL_HEAD": {
"TO": "TO", "TO": "به",
"ADD_BCC": "افزودن bcc", "ADD_BCC": "افزودن رونوشت",
"CC": { "CC": {
"LABEL": "CC", "LABEL": "رونوشت",
"PLACEHOLDER": "ایمیل‌ها با کاما از هم جدا می‌شوند", "PLACEHOLDER": "ایمیل‌ها با کاما از هم جدا می‌شوند",
"ERROR": "لطفا آدرس‌ ایمیل معتبر وارد کنید" "ERROR": "لطفا آدرس‌ ایمیل معتبر وارد کنید"
}, },
@@ -195,7 +195,7 @@
}, },
"CONTEXT_MENU": { "CONTEXT_MENU": {
"COPY": "کپی", "COPY": "کپی",
"REPLY_TO": "Reply to this message", "REPLY_TO": "پاسخ به این پیام",
"DELETE": "حذف", "DELETE": "حذف",
"CREATE_A_CANNED_RESPONSE": "اضافه کردن به پاسخ‌های آماده", "CREATE_A_CANNED_RESPONSE": "اضافه کردن به پاسخ‌های آماده",
"TRANSLATE": "ترجمه", "TRANSLATE": "ترجمه",
@@ -296,7 +296,7 @@
"SUBJECT": "موضوع" "SUBJECT": "موضوع"
}, },
"CONVERSATION_PARTICIPANTS": { "CONVERSATION_PARTICIPANTS": {
"SIDEBAR_MENU_TITLE": "شرکت کننده", "SIDEBAR_MENU_TITLE": "شرکت کنندگان",
"SIDEBAR_TITLE": "شرکت کنندگان در گفتگو", "SIDEBAR_TITLE": "شرکت کنندگان در گفتگو",
"NO_RECORDS_FOUND": "نتیجه‌ای یافت نشد", "NO_RECORDS_FOUND": "نتیجه‌ای یافت نشد",
"ADD_PARTICIPANTS": "شرکت کنندگان را انتخاب کنید", "ADD_PARTICIPANTS": "شرکت کنندگان را انتخاب کنید",
@@ -1,6 +1,7 @@
{ {
"EMOJI": { "EMOJI": {
"PLACEHOLDER": "جستجوی ایموجی", "PLACEHOLDER": "جستجوی ایموجی",
"NOT_FOUND": "هیچ ایموجی با جستجوی شما مطابقت ندارد" "NOT_FOUND": "هیچ ایموجی با جستجوی شما مطابقت ندارد",
"REMOVE": "حذف"
} }
} }
@@ -3,7 +3,7 @@
"TITLE": "تنظیمات حساب", "TITLE": "تنظیمات حساب",
"SUBMIT": "به‌روزرسانی تنظیمات", "SUBMIT": "به‌روزرسانی تنظیمات",
"BACK": "بازگشت", "BACK": "بازگشت",
"DISMISS": "Dismiss", "DISMISS": "نادیده بگیر",
"UPDATE": { "UPDATE": {
"ERROR": "تنظیمات به‌روزرسانی نشد، دوباره امتحان کنید!", "ERROR": "تنظیمات به‌روزرسانی نشد، دوباره امتحان کنید!",
"SUCCESS": "تنظیمات با موفقیت اعمال شد" "SUCCESS": "تنظیمات با موفقیت اعمال شد"
@@ -92,7 +92,7 @@
}, },
"NETWORK": { "NETWORK": {
"NOTIFICATION": { "NOTIFICATION": {
"TEXT": "قطع کردن Chatwoot" "TEXT": "ارتباط با سیستم چت قطع شده است"
}, },
"BUTTON": { "BUTTON": {
"REFRESH": "تازه کردن" "REFRESH": "تازه کردن"
@@ -112,7 +112,7 @@
"REMOVE_LABEL": "برچسب را از مکالمه حذف کنید", "REMOVE_LABEL": "برچسب را از مکالمه حذف کنید",
"SETTINGS": "تنظیمات", "SETTINGS": "تنظیمات",
"AI_ASSIST": "AI Assist", "AI_ASSIST": "AI Assist",
"APPEARANCE": "Appearance" "APPEARANCE": "ظاهری"
}, },
"COMMANDS": { "COMMANDS": {
"GO_TO_CONVERSATION_DASHBOARD": "به داشبورد مکالمه بروید", "GO_TO_CONVERSATION_DASHBOARD": "به داشبورد مکالمه بروید",
@@ -135,7 +135,7 @@
"ADD_LABELS_TO_CONVERSATION": "برچسب را به مکالمه اضافه کنید", "ADD_LABELS_TO_CONVERSATION": "برچسب را به مکالمه اضافه کنید",
"ASSIGN_AN_AGENT": "یک ایجنت تعیین کنید", "ASSIGN_AN_AGENT": "یک ایجنت تعیین کنید",
"AI_ASSIST": "AI Assist", "AI_ASSIST": "AI Assist",
"ASSIGN_PRIORITY": "Assign priority", "ASSIGN_PRIORITY": "انتساب اولویت",
"ASSIGN_A_TEAM": "یک تیم را تعیین کنید", "ASSIGN_A_TEAM": "یک تیم را تعیین کنید",
"MUTE_CONVERSATION": "صدای گفتگو را قطع کن", "MUTE_CONVERSATION": "صدای گفتگو را قطع کن",
"UNMUTE_CONVERSATION": "مکالمه را باصدا کنید", "UNMUTE_CONVERSATION": "مکالمه را باصدا کنید",
@@ -147,8 +147,8 @@
"UNTIL_NEXT_REPLY": "تا پاسخ بعدی", "UNTIL_NEXT_REPLY": "تا پاسخ بعدی",
"UNTIL_NEXT_WEEK": "تا هفته بعد", "UNTIL_NEXT_WEEK": "تا هفته بعد",
"UNTIL_TOMORROW": "تا فردا", "UNTIL_TOMORROW": "تا فردا",
"UNTIL_NEXT_MONTH": "Until next month", "UNTIL_NEXT_MONTH": "تا ماه آینده",
"AN_HOUR_FROM_NOW": "Until an hour from now", "AN_HOUR_FROM_NOW": "از حالا تا یک ساعت دیگر",
"CUSTOM": "سفارشی...", "CUSTOM": "سفارشی...",
"CHANGE_APPEARANCE": "تغییر ظاهر", "CHANGE_APPEARANCE": "تغییر ظاهر",
"LIGHT_MODE": "روشن", "LIGHT_MODE": "روشن",
@@ -365,7 +365,7 @@
"NAME": { "NAME": {
"LABEL": "نام", "LABEL": "نام",
"PLACEHOLDER": "نام دسته‌بندی", "PLACEHOLDER": "نام دسته‌بندی",
"HELP_TEXT": "از نام دسته در پرتال عمومی برای دسته بندی مقالات استفاده می شود.", "HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "نام الزامی است" "ERROR": "نام الزامی است"
}, },
"SLUG": { "SLUG": {
@@ -396,7 +396,7 @@
"NAME": { "NAME": {
"LABEL": "نام", "LABEL": "نام",
"PLACEHOLDER": "نام دسته‌بندی", "PLACEHOLDER": "نام دسته‌بندی",
"HELP_TEXT": "از نام دسته در پرتال عمومی برای دسته بندی مقالات استفاده می شود.", "HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "نام الزامی است" "ERROR": "نام الزامی است"
}, },
"SLUG": { "SLUG": {
@@ -162,7 +162,7 @@
"PROVIDERS": { "PROVIDERS": {
"LABEL": "ارائه دهنده API", "LABEL": "ارائه دهنده API",
"TWILIO": "Twilio", "TWILIO": "Twilio",
"BANDWIDTH": "Bandwidth" "BANDWIDTH": "پهنای باند"
}, },
"API": { "API": {
"ERROR_MESSAGE": "ما نتوانستیم کانال پیامک را ذخیره کنیم" "ERROR_MESSAGE": "ما نتوانستیم کانال پیامک را ذخیره کنیم"
@@ -405,21 +405,21 @@
"DISABLED": "غیرفعال" "DISABLED": "غیرفعال"
}, },
"SENDER_NAME_SECTION": { "SENDER_NAME_SECTION": {
"TITLE": "Sender name", "TITLE": "نام ارسال کننده",
"SUB_TEXT": "Select the name shown to the your customer when they receive emails from your agents.", "SUB_TEXT": "Select the name shown to the your customer when they receive emails from your agents.",
"FOR_EG": "For eg:", "FOR_EG": "For eg:",
"FRIENDLY": { "FRIENDLY": {
"TITLE": "Friendly", "TITLE": "دوستانه",
"FROM": "از", "FROM": "از",
"SUBTITLE": "Add the name of the agent who sent the reply in the sender name to make it friendly." "SUBTITLE": "Add the name of the agent who sent the reply in the sender name to make it friendly."
}, },
"PROFESSIONAL": { "PROFESSIONAL": {
"TITLE": "Professional", "TITLE": "حرفه ای",
"SUBTITLE": "Use only the configured business name as the sender name in the email header." "SUBTITLE": "Use only the configured business name as the sender name in the email header."
}, },
"BUSINESS_NAME": { "BUSINESS_NAME": {
"BUTTON_TEXT": "+ Configure your business name", "BUTTON_TEXT": "+ Configure your business name",
"PLACEHOLDER": "Enter your business name", "PLACEHOLDER": "نام کسب و کار خود را وارد کنید",
"SAVE_BUTTON_TEXT": "ذخیره" "SAVE_BUTTON_TEXT": "ذخیره"
} }
}, },
@@ -486,7 +486,7 @@
"ENABLE_EMAIL_COLLECT_BOX_SUB_TEXT": "فعال یا غیرفعال کردن فرم دریافت ایمیل از کاربر", "ENABLE_EMAIL_COLLECT_BOX_SUB_TEXT": "فعال یا غیرفعال کردن فرم دریافت ایمیل از کاربر",
"AUTO_ASSIGNMENT": "فعال کردن واگذاری خودکار گفتگو به ایجنت ها", "AUTO_ASSIGNMENT": "فعال کردن واگذاری خودکار گفتگو به ایجنت ها",
"ENABLE_CSAT": "فعال کردن رضایت مشتری", "ENABLE_CSAT": "فعال کردن رضایت مشتری",
"SENDER_NAME_SECTION": "Enable Agent Name in Email", "SENDER_NAME_SECTION": "فعال سازی نام اپراتور در ایمیل",
"ENABLE_CSAT_SUB_TEXT": "پس از پایان گفتگو ، نظرسنجی CSAT (رضایت مشتری) را فعال/غیرفعال کنید", "ENABLE_CSAT_SUB_TEXT": "پس از پایان گفتگو ، نظرسنجی CSAT (رضایت مشتری) را فعال/غیرفعال کنید",
"SENDER_NAME_SECTION_TEXT": "Enable/Disable showing Agent's name in email, if disabled it will show business name", "SENDER_NAME_SECTION_TEXT": "Enable/Disable showing Agent's name in email, if disabled it will show business name",
"ENABLE_CONTINUITY_VIA_EMAIL": "ادامه مکالمه را از طریق ایمیل فعال کنید", "ENABLE_CONTINUITY_VIA_EMAIL": "ادامه مکالمه را از طریق ایمیل فعال کنید",
@@ -498,7 +498,7 @@
"AUTO_ASSIGNMENT_SUB_TEXT": "فعال کردن یا غیرفعال کردن واگذاری خودکار گفتگوها به ایجنت های عضو این صندوق ورودی.", "AUTO_ASSIGNMENT_SUB_TEXT": "فعال کردن یا غیرفعال کردن واگذاری خودکار گفتگوها به ایجنت های عضو این صندوق ورودی.",
"HMAC_VERIFICATION": "اعتبار سنجی کاربر", "HMAC_VERIFICATION": "اعتبار سنجی کاربر",
"HMAC_DESCRIPTION": "به منظور اعتبارسنجی هویت کاربر، می‌توانید یک «شناسه_هش» برای هر کاربر ارسال کنید. می‌توانید با استفاده از «شناسه» با کلید نشان داده شده در اینجا یک هش HMAC sha256 ایجاد کنید.", "HMAC_DESCRIPTION": "به منظور اعتبارسنجی هویت کاربر، می‌توانید یک «شناسه_هش» برای هر کاربر ارسال کنید. می‌توانید با استفاده از «شناسه» با کلید نشان داده شده در اینجا یک هش HMAC sha256 ایجاد کنید.",
"HMAC_LINK_TO_DOCS": "You can read more here.", "HMAC_LINK_TO_DOCS": "از اینجا می توانید بیشتر بخوانید.",
"HMAC_MANDATORY_VERIFICATION": "مجبور کردن تأیید اعتبار هویت کاربر", "HMAC_MANDATORY_VERIFICATION": "مجبور کردن تأیید اعتبار هویت کاربر",
"HMAC_MANDATORY_DESCRIPTION": "اگر فعال باشد، درخواست‌هایی که «identifier_hash» را ندارند رد می‌شوند.", "HMAC_MANDATORY_DESCRIPTION": "اگر فعال باشد، درخواست‌هایی که «identifier_hash» را ندارند رد می‌شوند.",
"INBOX_IDENTIFIER": "شناسه صندوق ورودی", "INBOX_IDENTIFIER": "شناسه صندوق ورودی",
@@ -674,7 +674,7 @@
"PLACE_HOLDER": "دسترسی به ما ساده است. هر سوالی پیش آمد همینجا از ما بپرسید." "PLACE_HOLDER": "دسترسی به ما ساده است. هر سوالی پیش آمد همینجا از ما بپرسید."
}, },
"REPLY_TIME": { "REPLY_TIME": {
"LABEL": "زمان پاسخ", "LABEL": "زمان پاسخ دهی",
"IN_A_FEW_MINUTES": "در چند دقیقه", "IN_A_FEW_MINUTES": "در چند دقیقه",
"IN_A_FEW_HOURS": "در چند ساعت", "IN_A_FEW_HOURS": "در چند ساعت",
"IN_A_DAY": "در یک روز" "IN_A_DAY": "در یک روز"
@@ -712,12 +712,12 @@
"CHAT": "گفتگو" "CHAT": "گفتگو"
}, },
"REPLY_TIME": { "REPLY_TIME": {
"IN_A_FEW_MINUTES": "معمولاً در عرض چند دقیقه پاسخ می دهند", "IN_A_FEW_MINUTES": "معمولاً در عرض چند دقیقه پاسخ دهی انجام می شود",
"IN_A_FEW_HOURS": "معمولاً در عرض چند ساعت پاسخ می دهند", "IN_A_FEW_HOURS": "معمولاً در عرض چند ساعت پاسخ می دهند",
"IN_A_DAY": "به طور معمول در یک روز پاسخ می دهند" "IN_A_DAY": "به طور معمول در یک روز پاسخ دهی انجام می شود"
}, },
"FOOTER": { "FOOTER": {
"START_CONVERSATION_BUTTON_TEXT": "شروع گفتگو", "START_CONVERSATION_BUTTON_TEXT": "گفتگو را شروع کنید",
"CHAT_INPUT_PLACEHOLDER": "پیام خود را وارد کنید" "CHAT_INPUT_PLACEHOLDER": "پیام خود را وارد کنید"
}, },
"BODY": { "BODY": {
@@ -131,7 +131,7 @@
"KEY_PLACEHOLDER": "Enter your OpenAI API key", "KEY_PLACEHOLDER": "Enter your OpenAI API key",
"BUTTONS": { "BUTTONS": {
"NEED_HELP": "به کمک احتياج داری؟", "NEED_HELP": "به کمک احتياج داری؟",
"DISMISS": "Dismiss", "DISMISS": "نادیده بگیر",
"FINISH": "Finish Setup" "FINISH": "Finish Setup"
}, },
"DISMISS_MESSAGE": "You can setup OpenAI integration later Whenever you want.", "DISMISS_MESSAGE": "You can setup OpenAI integration later Whenever you want.",
@@ -46,7 +46,7 @@
"DISMISS": "Dismiss suggestion" "DISMISS": "Dismiss suggestion"
}, },
"POWERED_BY": "Chatwoot AI", "POWERED_BY": "Chatwoot AI",
"DISMISS": "Dismiss", "DISMISS": "نادیده بگیر",
"ADD_SELECTED_LABELS": "Add selected labels", "ADD_SELECTED_LABELS": "Add selected labels",
"ADD_SELECTED_LABEL": "Add selected label", "ADD_SELECTED_LABEL": "Add selected label",
"ADD_ALL_LABELS": "Add all labels" "ADD_ALL_LABELS": "Add all labels"
@@ -34,7 +34,7 @@
"ERROR": "Inbox is required" "ERROR": "Inbox is required"
}, },
"MESSAGE": { "MESSAGE": {
"LABEL": "Message", "LABEL": "Viesti",
"PLACEHOLDER": "Please enter the message of campaign", "PLACEHOLDER": "Please enter the message of campaign",
"ERROR": "Message is required" "ERROR": "Message is required"
}, },
@@ -88,7 +88,7 @@
"404": "There are no campaigns created for this inbox.", "404": "There are no campaigns created for this inbox.",
"TABLE_HEADER": { "TABLE_HEADER": {
"TITLE": "Title", "TITLE": "Title",
"MESSAGE": "Message", "MESSAGE": "Viesti",
"INBOX": "Inbox", "INBOX": "Inbox",
"STATUS": "Tila", "STATUS": "Tila",
"SENDER": "Sender", "SENDER": "Sender",
@@ -1,75 +1,75 @@
{ {
"CANNED_MGMT": { "CANNED_MGMT": {
"HEADER": "Tallennetut vastaukset", "HEADER": "Tallennetut vastaukset",
"HEADER_BTN_TXT": "Lisää tallennettu vastaus", "HEADER_BTN_TXT": "Add canned response",
"LOADING": "Haetaan tallennettuja vastauksia", "LOADING": "Fetching canned responses...",
"SEARCH_404": "Tätä hakua vastaavia kohteita ei löydy", "SEARCH_404": "Tätä hakua vastaavia kohteita ei löydy.",
"SIDEBAR_TXT": "<p><b>Canned Responses</b> </p><p> Canned Responses are saved reply templates which can be used to quickly send out a reply to a conversation. </p><p> For creating a Canned Response, just click on the <b>Add Canned Response</b>. You can also edit or delete an existing Canned Response by clicking on the Edit or Delete button </p><p> Canned responses are used with the help of <b>Short Codes</b>. Agents can access canned responses while on a chat by typing <b>'/'</b> followed by the short code. </p>", "SIDEBAR_TXT": "<p><b>Canned Responses</b> </p><p> Canned Responses are pre-written reply templates that help you quickly respond to a conversation. To insert a canned response during a chat, agents can type a short code preceded by a '/' character. </p><p> You can manage your canned responses from this page or create new ones using the \"Add canned response\" button.</p><p>Open the <a target=\"_blank\" href=\"https://www.chatwoot.com/hc/chatwoot-user-guide-cloud-version/articles/1677501325-how-to-create-saved-reply-templates-with-canned-responses\">Canned Responses handbook</a> in another tab for a helping hand.</p><p>Also, check out the all-new <a href=\"https://www.chatwoot.com/tools/canned-responses-library\" target=\"_blank\">Canned Responses Library</a>.</p>",
"LIST": { "LIST": {
"404": "Tällä tilillä ei ole tallennettuja vastauksia.", "404": "Tällä tilillä ei ole tallennettuja vastauksia.",
"TITLE": "Hallitse tallennettuja vastauksia", "TITLE": "Hallitse tallennettuja vastauksia",
"DESC": "Tallennetut vastaukset ovat ennalta valmisteltuja vastausmalleja, joita voidaan käyttää nopeasti lähettää vastaukset tukipyyntöihin.", "DESC": "Canned Responses are predefined reply templates which can be used to quickly send out replies to conversations.",
"TABLE_HEADER": [ "TABLE_HEADER": [
"Oikotie", "Short code",
"Sisältö", "Sisältö",
"Toiminnot" "Toiminnot"
] ]
}, },
"ADD": { "ADD": {
"TITLE": "Lisää tallennettu vastaus", "TITLE": "Add canned response",
"DESC": "Canned Responses are saved reply templates which can be used to quickly send out reply to conversation.", "DESC": "Canned Responses are predefined reply templates which can be used to quickly send out replies to conversations.",
"CANCEL_BUTTON_TEXT": "Peruuta", "CANCEL_BUTTON_TEXT": "Peruuta",
"FORM": { "FORM": {
"SHORT_CODE": { "SHORT_CODE": {
"LABEL": "Oikotie", "LABEL": "Short code",
"PLACEHOLDER": "Please enter a short code", "PLACEHOLDER": "Please enter a short code.",
"ERROR": "Oikotie on vaadittu" "ERROR": "Short Code is required."
}, },
"CONTENT": { "CONTENT": {
"LABEL": "Sisältö", "LABEL": "Viesti",
"PLACEHOLDER": "Ole hyvä ja syötä sisältö", "PLACEHOLDER": "Please write the message you want to save as a template to use later.",
"ERROR": "Sisältö on vaadittu" "ERROR": "Message is required."
}, },
"SUBMIT": "Lähetä" "SUBMIT": "Lähetä"
}, },
"API": { "API": {
"SUCCESS_MESSAGE": "Tallennettu vastaus lisätty onnistuneesti", "SUCCESS_MESSAGE": "Canned response added successfully.",
"ERROR_MESSAGE": "Yhteyden muodostaminen Woot-palvelimelle ei onnistunut, yritä myöhemmin uudelleen" "ERROR_MESSAGE": "Yhteyden muodostaminen Woot-palvelimelle ei onnistunut, yritä myöhemmin uudelleen"
} }
}, },
"EDIT": { "EDIT": {
"TITLE": "Muokkaa tallennettua vastausta", "TITLE": "Edit canned response",
"CANCEL_BUTTON_TEXT": "Peruuta", "CANCEL_BUTTON_TEXT": "Peruuta",
"FORM": { "FORM": {
"SHORT_CODE": { "SHORT_CODE": {
"LABEL": "Oikotie", "LABEL": "Short code",
"PLACEHOLDER": "Ole hyvä ja syötä oikotie", "PLACEHOLDER": "Please enter a shortcode.",
"ERROR": "Oikotie on vaadittu" "ERROR": "Short code is required."
}, },
"CONTENT": { "CONTENT": {
"LABEL": "Sisältö", "LABEL": "Viesti",
"PLACEHOLDER": "Ole hyvä ja syötä sisältö", "PLACEHOLDER": "Please write the message you want to save as a template to use later.",
"ERROR": "Sisältö on vaadittu" "ERROR": "Message is required."
}, },
"SUBMIT": "Lähetä" "SUBMIT": "Lähetä"
}, },
"BUTTON_TEXT": "Muokkaa", "BUTTON_TEXT": "Muokkaa",
"API": { "API": {
"SUCCESS_MESSAGE": "Tallennettu vastaus päivitetty onnistuneesti", "SUCCESS_MESSAGE": "Canned response is updated successfully.",
"ERROR_MESSAGE": "Yhteyden muodostaminen Woot-palvelimelle ei onnistunut, yritä myöhemmin uudelleen" "ERROR_MESSAGE": "Yhteyden muodostaminen Woot-palvelimelle ei onnistunut, yritä myöhemmin uudelleen"
} }
}, },
"DELETE": { "DELETE": {
"BUTTON_TEXT": "Poista", "BUTTON_TEXT": "Poista",
"API": { "API": {
"SUCCESS_MESSAGE": "Tallennettu vastaus poistettu onnistuneesti", "SUCCESS_MESSAGE": "Canned response deleted successfully.",
"ERROR_MESSAGE": "Yhteyden muodostaminen Woot-palvelimelle ei onnistunut, yritä myöhemmin uudelleen" "ERROR_MESSAGE": "Yhteyden muodostaminen Woot-palvelimelle ei onnistunut, yritä myöhemmin uudelleen"
}, },
"CONFIRM": { "CONFIRM": {
"TITLE": "Vahvista poistaminen", "TITLE": "Confirm deletion",
"MESSAGE": "Oletko varma että haluat poistaa ", "MESSAGE": "Oletko varma että haluat poistaa ",
"YES": "Kyllä, poista ", "YES": "Yes, delete ",
"NO": "Ei, säilytä " "NO": "No, keep "
} }
} }
} }
@@ -200,7 +200,7 @@
"ERROR": "Subject can't be empty" "ERROR": "Subject can't be empty"
}, },
"MESSAGE": { "MESSAGE": {
"LABEL": "Message", "LABEL": "Viesti",
"PLACEHOLDER": "Write your message here", "PLACEHOLDER": "Write your message here",
"ERROR": "Message can't be empty" "ERROR": "Message can't be empty"
}, },
@@ -309,20 +309,20 @@
}, },
"VALUE": { "VALUE": {
"LABEL": "Attribute value", "LABEL": "Attribute value",
"PLACEHOLDER": "Eg: 11901 " "PLACEHOLDER": "Esim: 11901 "
}, },
"ADD": { "ADD": {
"TITLE": "Luo uusi määrite ", "TITLE": "Luo uusi määrite ",
"SUCCESS": "Attribute added successfully", "SUCCESS": "Attribuutin lisäys onnistui",
"ERROR": "Unable to add attribute. Please try again later" "ERROR": "Attribuuttia ei voitu lisätä. Yritä myöhemmin uudelleen"
}, },
"UPDATE": { "UPDATE": {
"SUCCESS": "Attribute updated successfully", "SUCCESS": "Tunniste päivitetty onnistuneesti",
"ERROR": "Unable to update attribute. Please try again later" "ERROR": "Attribuuttia ei voida päivittää. Yritä myöhemmin uudelleen"
}, },
"DELETE": { "DELETE": {
"SUCCESS": "Attribute deleted successfully", "SUCCESS": "Attribuutin poisto onnistui",
"ERROR": "Unable to delete attribute. Please try again later" "ERROR": "Attribuuttia ei voida poistaa. Yritä uudelleen myöhemmin"
}, },
"ATTRIBUTE_SELECT": { "ATTRIBUTE_SELECT": {
"TITLE": "Lisää määritteitä", "TITLE": "Lisää määritteitä",
@@ -331,15 +331,15 @@
}, },
"ATTRIBUTE_TYPE": { "ATTRIBUTE_TYPE": {
"LIST": { "LIST": {
"PLACEHOLDER": "Select value", "PLACEHOLDER": "Valitse arvo",
"SEARCH_INPUT_PLACEHOLDER": "Search value", "SEARCH_INPUT_PLACEHOLDER": "Etsi arvo",
"NO_RESULT": "Tuloksia ei löytynyt" "NO_RESULT": "Tuloksia ei löytynyt"
} }
} }
}, },
"VALIDATIONS": { "VALIDATIONS": {
"REQUIRED": "Valid value is required", "REQUIRED": "Kelvollinen arvo vaaditaan",
"INVALID_URL": "Invalid URL" "INVALID_URL": "Virheellinen URL"
} }
}, },
"MERGE_CONTACTS": { "MERGE_CONTACTS": {
@@ -355,7 +355,7 @@
"HELP_LABEL": "Säilytetään" "HELP_LABEL": "Säilytetään"
}, },
"SUMMARY": { "SUMMARY": {
"TITLE": "Summary", "TITLE": "Yhteenveto",
"DELETE_WARNING": "Contact of <strong>%{primaryContactName}</strong> will be deleted.", "DELETE_WARNING": "Contact of <strong>%{primaryContactName}</strong> will be deleted.",
"ATTRIBUTE_WARNING": "Contact details of <strong>%{primaryContactName}</strong> will be copied to <strong>%{parentContactName}</strong>." "ATTRIBUTE_WARNING": "Contact details of <strong>%{primaryContactName}</strong> will be copied to <strong>%{parentContactName}</strong>."
}, },
@@ -270,17 +270,17 @@
"CONVERSATION_CUSTOM_ATTRIBUTES": { "CONVERSATION_CUSTOM_ATTRIBUTES": {
"ADD_BUTTON_TEXT": "Create attribute", "ADD_BUTTON_TEXT": "Create attribute",
"UPDATE": { "UPDATE": {
"SUCCESS": "Attribute updated successfully", "SUCCESS": "Tunniste päivitetty onnistuneesti",
"ERROR": "Unable to update attribute. Please try again later" "ERROR": "Attribuuttia ei voida päivittää. Yritä myöhemmin uudelleen"
}, },
"ADD": { "ADD": {
"TITLE": "Add", "TITLE": "Add",
"SUCCESS": "Attribute added successfully", "SUCCESS": "Attribuutin lisäys onnistui",
"ERROR": "Unable to add attribute. Please try again later" "ERROR": "Attribuuttia ei voitu lisätä. Yritä myöhemmin uudelleen"
}, },
"DELETE": { "DELETE": {
"SUCCESS": "Attribute deleted successfully", "SUCCESS": "Attribuutin poisto onnistui",
"ERROR": "Unable to delete attribute. Please try again later" "ERROR": "Attribuuttia ei voida poistaa. Yritä uudelleen myöhemmin"
}, },
"ATTRIBUTE_SELECT": { "ATTRIBUTE_SELECT": {
"TITLE": "Lisää määritteitä", "TITLE": "Lisää määritteitä",
@@ -1,6 +1,7 @@
{ {
"EMOJI": { "EMOJI": {
"PLACEHOLDER": "Search emojis", "PLACEHOLDER": "Search emojis",
"NOT_FOUND": "No emoji match your search" "NOT_FOUND": "No emoji match your search",
"REMOVE": "Poista"
} }
} }
@@ -365,7 +365,7 @@
"NAME": { "NAME": {
"LABEL": "Nimi", "LABEL": "Nimi",
"PLACEHOLDER": "Category name", "PLACEHOLDER": "Category name",
"HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.", "HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "Name is required" "ERROR": "Name is required"
}, },
"SLUG": { "SLUG": {
@@ -396,7 +396,7 @@
"NAME": { "NAME": {
"LABEL": "Nimi", "LABEL": "Nimi",
"PLACEHOLDER": "Category name", "PLACEHOLDER": "Category name",
"HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.", "HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "Name is required" "ERROR": "Name is required"
}, },
"SLUG": { "SLUG": {
@@ -410,7 +410,7 @@
"FOR_EG": "For eg:", "FOR_EG": "For eg:",
"FRIENDLY": { "FRIENDLY": {
"TITLE": "Friendly", "TITLE": "Friendly",
"FROM": "from", "FROM": "lähettäjä",
"SUBTITLE": "Add the name of the agent who sent the reply in the sender name to make it friendly." "SUBTITLE": "Add the name of the agent who sent the reply in the sender name to make it friendly."
}, },
"PROFESSIONAL": { "PROFESSIONAL": {
@@ -19,7 +19,7 @@
"BOT_LABEL": "Botti", "BOT_LABEL": "Botti",
"READ_MORE": "Read more", "READ_MORE": "Read more",
"WROTE": "wrote:", "WROTE": "wrote:",
"FROM": "from", "FROM": "lähettäjä",
"EMAIL": "sähköposti" "EMAIL": "sähköposti"
} }
} }
@@ -1,6 +1,7 @@
{ {
"EMOJI": { "EMOJI": {
"PLACEHOLDER": "Rechercher des émojis", "PLACEHOLDER": "Rechercher des émojis",
"NOT_FOUND": "Aucun émoji ne correspond à votre recherche" "NOT_FOUND": "Aucun émoji ne correspond à votre recherche",
"REMOVE": "Supprimer"
} }
} }
@@ -365,7 +365,7 @@
"NAME": { "NAME": {
"LABEL": "Nom", "LABEL": "Nom",
"PLACEHOLDER": "Nom de la catégorie", "PLACEHOLDER": "Nom de la catégorie",
"HELP_TEXT": "Le nom de la catégorie sera utilisé dans le portail public pour classer les articles.", "HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "Le nom est requis" "ERROR": "Le nom est requis"
}, },
"SLUG": { "SLUG": {
@@ -396,7 +396,7 @@
"NAME": { "NAME": {
"LABEL": "Nom", "LABEL": "Nom",
"PLACEHOLDER": "Nom de la catégorie", "PLACEHOLDER": "Nom de la catégorie",
"HELP_TEXT": "Le nom de la catégorie sera utilisé dans le portail public pour classer les articles.", "HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "Le nom est requis" "ERROR": "Le nom est requis"
}, },
"SLUG": { "SLUG": {
@@ -1,6 +1,7 @@
{ {
"EMOJI": { "EMOJI": {
"PLACEHOLDER": "חפש אימוג'י", "PLACEHOLDER": "חפש אימוג'י",
"NOT_FOUND": "אין אמוג'י שתואם את החיפוש שלך" "NOT_FOUND": "אין אמוג'י שתואם את החיפוש שלך",
"REMOVE": "הסר"
} }
} }
@@ -365,7 +365,7 @@
"NAME": { "NAME": {
"LABEL": "שם", "LABEL": "שם",
"PLACEHOLDER": "שם קטגוריה", "PLACEHOLDER": "שם קטגוריה",
"HELP_TEXT": "שם הקטגוריה ישמש בפורטל הפונה לציבור כדי לסווג מאמרים.", "HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "שם שדה חובה" "ERROR": "שם שדה חובה"
}, },
"SLUG": { "SLUG": {
@@ -396,7 +396,7 @@
"NAME": { "NAME": {
"LABEL": "שם", "LABEL": "שם",
"PLACEHOLDER": "שם קטגוריה", "PLACEHOLDER": "שם קטגוריה",
"HELP_TEXT": "שם הקטגוריה ישמש בפורטל הפונה לציבור כדי לסווג מאמרים.", "HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "שם שדה חובה" "ERROR": "שם שדה חובה"
}, },
"SLUG": { "SLUG": {
@@ -1,6 +1,7 @@
{ {
"EMOJI": { "EMOJI": {
"PLACEHOLDER": "Search emojis", "PLACEHOLDER": "Search emojis",
"NOT_FOUND": "No emoji match your search" "NOT_FOUND": "No emoji match your search",
"REMOVE": "Remove"
} }
} }
@@ -365,7 +365,7 @@
"NAME": { "NAME": {
"LABEL": "Name", "LABEL": "Name",
"PLACEHOLDER": "Category name", "PLACEHOLDER": "Category name",
"HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.", "HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "Name is required" "ERROR": "Name is required"
}, },
"SLUG": { "SLUG": {
@@ -396,7 +396,7 @@
"NAME": { "NAME": {
"LABEL": "Name", "LABEL": "Name",
"PLACEHOLDER": "Category name", "PLACEHOLDER": "Category name",
"HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.", "HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "Name is required" "ERROR": "Name is required"
}, },
"SLUG": { "SLUG": {
@@ -1,6 +1,7 @@
{ {
"EMOJI": { "EMOJI": {
"PLACEHOLDER": "Search emojis", "PLACEHOLDER": "Search emojis",
"NOT_FOUND": "No emoji match your search" "NOT_FOUND": "No emoji match your search",
"REMOVE": "Izbaci"
} }
} }

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