Compare commits

..
Author SHA1 Message Date
Sivin VargheseandGitHub 4ed150fd82 Merge branch 'develop' into fix/next-message-bubbles 2025-03-10 21:52:34 +05:30
PranavandGitHub 66fbe1bde7 fix: Hide Configuration page for Microsoft & Gmail channels (#11045)
The Microsoft and Gmail channels don’t function with custom SMTP or IMAP
configurations or forwarding option. Displaying the configuration tab
creates confusion among users.

This PR would eliminate the option for these channels.
2025-03-07 16:02:40 -08:00
a543cc92df chore: Update translations (#11043)
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2025-03-07 14:07:28 -08:00
PranavandGitHub 22331fb626 fix: Paginate attachments API (#11044)
There are attachments with over 1000 attachments (unusual) in
production, and some of them timeout. This PR would limit the number of
attachments to 100 (which is sufficient for viewing the files in the
gallery, pagination on the UI can be added later).
2025-03-07 13:56:10 -08:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
08f0ea2c98 chore(deps): Bump vue-i18n from 9.14.2 to 9.14.3 (#11041)
Bumps
[vue-i18n](https://github.com/intlify/vue-i18n/tree/HEAD/packages/vue-i18n)
from 9.14.2 to 9.14.3.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/intlify/vue-i18n/releases">vue-i18n's
releases</a>.</em></p>
<blockquote>
<h2>v9.14.3</h2>
<!-- raw HTML omitted -->
<h2>What's Changed</h2>
<h3>🔒 Security Fixes</h3>
<ul>
<li>fix: prototype pollution in <code>handleFlatJson</code>, about
details see <a
href="https://github.com/intlify/vue-i18n/security/advisories/GHSA-p2ph-7g93-hw3m">https://github.com/intlify/vue-i18n/security/advisories/GHSA-p2ph-7g93-hw3m</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/intlify/vue-i18n/compare/v9.14.2...v9.14.3">https://github.com/intlify/vue-i18n/compare/v9.14.2...v9.14.3</a></p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/intlify/vue-i18n/blob/v9.14.3/CHANGELOG.md">vue-i18n's
changelog</a>.</em></p>
<blockquote>
<h1>v9.14.3 (2025-03-07T03:21:38Z)</h1>
<p>This changelog is generated by <a
href="https://github.com/intlify/vue-i18n/releases/tag/v9.14.3">GitHub
Releases</a></p>
<!-- raw HTML omitted -->
<p><strong>Full Changelog</strong>: <a
href="https://github.com/intlify/vue-i18n/compare/v9.14.2...v9.14.3">https://github.com/intlify/vue-i18n/compare/v9.14.2...v9.14.3</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/intlify/vue-i18n/commit/2e255c59ded7caf7c35f67190a336ce4cec82816"><code>2e255c5</code></a>
release: v9.14.3</li>
<li><a
href="https://github.com/intlify/vue-i18n/commit/6695eec30737265032dca70690df00ad8d3aa713"><code>6695eec</code></a>
fix: update package name for npm provenance</li>
<li><a
href="https://github.com/intlify/vue-i18n/commit/2b5149d42afe9b00a514e6b1ee2021212e47edf5"><code>2b5149d</code></a>
Revert &quot;release: v9.14.3&quot;</li>
<li><a
href="https://github.com/intlify/vue-i18n/commit/26449f9d74165736cd1d5ff768ff3c7d12d8a8d5"><code>26449f9</code></a>
release: v9.14.3</li>
<li>See full diff in <a
href="https://github.com/intlify/vue-i18n/commits/v9.14.3/packages/vue-i18n">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=vue-i18n&package-manager=npm_and_yarn&previous-version=9.14.2&new-version=9.14.3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/chatwoot/chatwoot/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-07 11:49:36 -08:00
PranavandGitHub 8fefdea4e8 chore: Dynamically load OldSidebar if needed (#11038)
Move old sidebar loading to async
2025-03-06 19:12:54 -08:00
PranavandGitHub a513f152ed fix: Extend the locale without variant check for article locales as well (#11021)
We allow users to select locale variants when creating the help center
(e.g., pt_BR or en_UK). However, the selected variant may not always be
available for translation in the app. In such cases, we need to fall
back to either the base language or the default locale.

While this fallback logic was implemented for the portal locale, it was
missing for article locales.

This PR fixes that issue.
2025-03-06 18:24:46 -08:00
a0cf25ccde chore: Rescue slack link unfurling errors. (#11033)
Fixes
https://linear.app/chatwoot/issue/CW-4122/slackwebapierrorsmissingscope-missing-scope

This PR adds the ability to handle errors when scopes are missing during
link unfurling. Since link unfurling is just a nice-to-have feature that
doesn't affect core functionality, we will silently ignore these errors.

---------

Co-authored-by: Sojan <sojan@pepalo.com>
2025-03-06 15:57:16 -08:00
8d85a02ca9 feat: handle Channel errors (#11015)
This PR adds missing error handlers for the following channels and cases

1. WhatsApp - Generic Handlers for both Cloud and 360Dialog (Deprecated)
2. Instagram - Handler for a case where there is an HTTP error instead
of an `:error` in the 200 response
3. Facebook - Errors from the two sentry issues
([Net::OpenTimeout](https://chatwoot-p3.sentry.io/issues/6164805227) &
[JSON::ParserError](https://chatwoot-p3.sentry.io/issues/5903200786))
4. SMS: Generic handlers for Bandwidth SMS

#### Checklist

- [x] Bandwidth SMS
- [x] Whatsapp Cloud + 360 Dialog
- [x] Twilio SMS
- [x] Line
- [x] Telegram
- [x] Instagram
- [x] Facebook
- [x] GMail
- [x] 365 Mail
- [x] SMTP Mail

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2025-03-06 20:09:47 +05:30
Sivin VargheseandGitHub 7e1458fd32 fix: Issue when saving hotkeys (#11026)
# Pull Request Template

## Description

This PR fixes an issue when saving send message button hotkeys, where a
TypeError occurs: `this.updateUISettings is not a function`.

This issue arises after merging this PR
https://github.com/chatwoot/chatwoot/pull/10974

Fixes
https://chatwoot-p3.sentry.io/issues/6339976939/events/ca9946f92cb74428a72f1f74976a56a3/

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
2025-03-06 10:53:02 +05:30
PranavandGitHub d017156f32 fix: Disable syncing IMAP if the account is suspended (#11031)
This PR disables the IMAP syncing if the account is suspended.
2025-03-05 17:10:24 -08:00
Sojan JoseandGitHub c1f6d9f76f feat: Add the ability to filter items in Super Admin panel (#11020) 2025-03-05 16:32:54 -08:00
ed562832a6 feat: add ui element for secrets in superadmin (#11000)
- add secret type for installation_config in the super admin console
- hide tokens by default on the UI

Before
----
<img width="891" alt="image"
src="https://github.com/user-attachments/assets/1c51b3bb-47f9-49bf-bb0d-75b84d8d2eeb"
/>


After
----
<img width="958" alt="image"
src="https://github.com/user-attachments/assets/b7eca1ff-1cc0-4259-9977-6109eb38ad64"
/>

---------

Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
2025-03-04 16:51:40 +05:30
Vishnu NarayananandGitHub 12d7be62d3 chore: fix nightly linux installer github action (#11009)
- chore: fix nightly linux installer github action
2025-03-03 17:45:57 +05:30
6040e50265 chore: Ability to filter conversations with priority (#10967)
- Ability to filter conversation with priority

---------

Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2025-03-03 16:38:22 +05:30
Sivin VargheseandGitHub 019e2de124 Merge branch 'develop' into fix/next-message-bubbles 2025-02-26 14:52:32 +05:30
iamsivin eaf7f8d4a7 chore: Minor fix 2025-02-21 12:10:22 +05:30
Sivin VargheseandGitHub 390f8bf9ac Merge branch 'develop' into fix/next-message-bubbles 2025-02-21 12:08:03 +05:30
Sivin VargheseandGitHub 89b8276946 Merge branch 'develop' into fix/next-message-bubbles 2025-02-20 23:36:23 +05:30
Pranav 27946f5af8 Adjust the size of the avatar 2025-02-19 18:53:37 -08:00
PranavandGitHub 898020e7c3 Merge branch 'develop' into fix/next-message-bubbles 2025-02-19 16:19:29 -08:00
Sivin VargheseandGitHub daf3ef2496 Merge branch 'develop' into fix/next-message-bubbles 2025-02-19 20:28:55 +05:30
PranavandGitHub 707ec5dfde Merge branch 'develop' into fix/next-message-bubbles 2025-02-12 16:43:00 -08:00
Sivin VargheseandGitHub fd115c8ab1 Merge branch 'develop' into fix/next-message-bubbles 2025-02-11 22:52:44 +05:30
Sivin VargheseandGitHub 45869c4bfa Merge branch 'develop' into fix/next-message-bubbles 2025-02-11 09:20:45 +05:30
iamsivin 30ebf7724e chore: Minor fix 2025-02-07 23:29:02 +05:30
Sivin VargheseandGitHub 8733c0c64a Merge branch 'develop' into fix/next-message-bubbles 2025-02-07 23:26:24 +05:30
Sivin VargheseandGitHub b378cb5f82 Merge branch 'develop' into fix/next-message-bubbles 2025-02-04 08:59:42 +05:30
Sivin VargheseandGitHub 5fffa86dc2 Merge branch 'develop' into fix/next-message-bubbles 2025-02-03 21:21:17 +05:30
Sivin VargheseandGitHub 05d29fb046 Merge branch 'develop' into fix/next-message-bubbles 2025-02-03 15:21:15 +05:30
Sivin VargheseandGitHub dff4a2208d Merge branch 'develop' into fix/next-message-bubbles 2025-02-03 12:55:01 +05:30
iamsivin 4f48d52eba chore: Minor fix 2025-01-31 21:56:36 +05:30
Sivin VargheseandGitHub b1ffce88e2 Merge branch 'develop' into fix/next-message-bubbles 2025-01-31 21:51:33 +05:30
Sivin VargheseandGitHub b5994e4b70 Merge branch 'develop' into fix/next-message-bubbles 2025-01-31 12:46:30 +05:30
Sivin VargheseandGitHub 1c2f2ecad1 Merge branch 'develop' into fix/next-message-bubbles 2025-01-30 15:27:25 +05:30
Sivin VargheseandGitHub d9ce71d21d Merge branch 'develop' into fix/next-message-bubbles 2025-01-30 09:38:16 +05:30
iamsivin e94a58d976 chore: Fix email overlay bg 2025-01-29 22:24:40 +05:30
Sivin VargheseandGitHub c41f7507bf Merge branch 'develop' into fix/next-message-bubbles 2025-01-29 19:36:26 +05:30
iamsivin ad9a34949b chore: Improvements 2025-01-29 16:29:49 +05:30
Sivin VargheseandGitHub 182cddd799 Merge branch 'develop' into fix/next-message-bubbles 2025-01-29 15:35:04 +05:30
iamsivin 5c55584038 chore: Fix bullets color 2025-01-29 15:24:02 +05:30
Shivam MishraandGitHub 0a6df5b67a Merge branch 'develop' into fix/next-message-bubbles 2025-01-29 13:59:33 +05:30
iamsivin d0ca703da2 chore: Review fix 2025-01-28 23:31:13 +05:30
Sivin VargheseandGitHub 0cac9e1a6d Merge branch 'develop' into fix/next-message-bubbles 2025-01-28 19:12:05 +05:30
iamsivin 7bdc4d78b2 chore: Adds padding for activity message bubble 2025-01-28 11:34:45 +05:30
iamsivin 2500341278 chore: Update border-b for email bubble 2025-01-27 20:05:02 +05:30
iamsivin d09701db37 chore: Update colors 2025-01-27 19:59:46 +05:30
Sivin VargheseandGitHub 8ac679e042 Merge branch 'develop' into fix/next-message-bubbles 2025-01-27 19:34:42 +05:30
iamsivin cc4817b233 chore: Minor fix 2025-01-27 19:19:15 +05:30
iamsivin 3c227f8048 chore: Minor fix 2025-01-27 18:55:51 +05:30
Sivin VargheseandGitHub 79545ba2b8 Merge branch 'develop' into fix/next-message-bubbles 2025-01-27 13:20:06 +05:30
iamsivin 1dd9e867cd chore: Minor fix 2025-01-24 23:52:33 +05:30
iamsivin 123f0cb61f chore: Update file chip 2025-01-24 23:42:06 +05:30
iamsivin 78220565a9 chore: Minor fix 2025-01-24 23:15:19 +05:30
Sivin VargheseandGitHub cbcb32acf5 Merge branch 'develop' into fix/next-message-bubbles 2025-01-24 23:08:32 +05:30
iamsivin d451329a81 chore: Minor fix 2025-01-24 18:59:34 +05:30
iamsivin 5f083b5c17 chore: Minor fix 2025-01-24 18:56:55 +05:30
iamsivin a296847dd4 chore: Adds playback speed for audio bubble 2025-01-24 18:38:37 +05:30
iamsivin 0a0d1e07f4 chore: Fis eslint 2025-01-24 18:04:55 +05:30
iamsivin 077a8fd498 chore: Update attachment chip preview 2025-01-24 17:56:53 +05:30
iamsivin 11e77e9349 fix: Message grouping, orientation, spacing, and tooltip 2025-01-24 14:25:29 +05:30
137 changed files with 1542 additions and 546 deletions
+1 -1
View File
@@ -16,7 +16,7 @@ on:
jobs: jobs:
nightly: nightly:
runs-on: ubuntu-20.04 runs-on: ubuntu-24.04
steps: steps:
- name: get installer - name: get installer
+2 -2
View File
@@ -561,7 +561,7 @@ GEM
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
raabro (1.4.0) raabro (1.4.0)
racc (1.8.1) racc (1.8.1)
rack (2.2.11) rack (2.2.12)
rack-attack (6.7.0) rack-attack (6.7.0)
rack (>= 1.0, < 4) rack (>= 1.0, < 4)
rack-contrib (2.5.0) rack-contrib (2.5.0)
@@ -799,7 +799,7 @@ GEM
unf_ext (0.0.8.2) unf_ext (0.0.8.2)
unicode-display_width (2.4.2) unicode-display_width (2.4.2)
uniform_notifier (1.16.0) uniform_notifier (1.16.0)
uri (0.13.0) uri (1.0.3)
uri_template (0.7.0) uri_template (0.7.0)
valid_email2 (5.2.6) valid_email2 (5.2.6)
activemodel (>= 3.2) activemodel (>= 3.2)
@@ -1,8 +1,8 @@
button, button:not(.reset-base),
input[type="button"], input[type='button']:not(.reset-base),
input[type="reset"], input[type='reset']:not(.reset-base),
input[type="submit"], input[type='submit']:not(.reset-base),
.button { .button:not(.reset-base) {
appearance: none; appearance: none;
background-color: $color-woot; background-color: $color-woot;
border: 0; border: 0;
@@ -10,7 +10,6 @@
.icon-container { .icon-container {
margin-right: 2px; margin-right: 2px;
} }
.value-container { .value-container {
@@ -6,6 +6,8 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
before_action :conversation, except: [:index, :meta, :search, :create, :filter] before_action :conversation, except: [:index, :meta, :search, :create, :filter]
before_action :inbox, :contact, :contact_inbox, only: [:create] before_action :inbox, :contact, :contact_inbox, only: [:create]
ATTACHMENT_RESULTS_PER_PAGE = 100
def index def index
result = conversation_finder.perform result = conversation_finder.perform
@conversations = result[:conversations] @conversations = result[:conversations]
@@ -24,7 +26,12 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
end end
def attachments def attachments
@attachments_count = @conversation.attachments.count
@attachments = @conversation.attachments @attachments = @conversation.attachments
.includes(:message)
.order(created_at: :desc)
.page(attachment_params[:page])
.per(ATTACHMENT_RESULTS_PER_PAGE)
end end
def show; end def show; end
@@ -124,6 +131,10 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
params.permit(:priority) params.permit(:priority)
end end
def attachment_params
params.permit(:page)
end
def update_last_seen_on_conversation(last_seen_at, update_assignee) def update_last_seen_on_conversation(last_seen_at, update_assignee)
# rubocop:disable Rails/SkipsModelValidations # rubocop:disable Rails/SkipsModelValidations
@conversation.update_column(:agent_last_seen_at, last_seen_at) @conversation.update_column(:agent_last_seen_at, last_seen_at)
+19 -14
View File
@@ -5,10 +5,11 @@ module SwitchLocale
def switch_locale(&) def switch_locale(&)
# priority is for locale set in query string (mostly for widget/from js sdk) # priority is for locale set in query string (mostly for widget/from js sdk)
locale ||= locale_from_params locale ||= params[:locale]
locale ||= locale_from_custom_domain locale ||= locale_from_custom_domain
# if locale is not set in account, let's use DEFAULT_LOCALE env variable # if locale is not set in account, let's use DEFAULT_LOCALE env variable
locale ||= locale_from_env_variable locale ||= ENV.fetch('DEFAULT_LOCALE', nil)
set_locale(locale, &) set_locale(locale, &)
end end
@@ -32,26 +33,30 @@ module SwitchLocale
end end
def set_locale(locale, &) def set_locale(locale, &)
# if locale is empty, use default_locale safe_locale = validate_and_get_locale(locale)
locale ||= I18n.default_locale
# Ensure locale won't bleed into other requests # Ensure locale won't bleed into other requests
# https://guides.rubyonrails.org/i18n.html#managing-the-locale-across-requests # https://guides.rubyonrails.org/i18n.html#managing-the-locale-across-requests
I18n.with_locale(locale, &) I18n.with_locale(safe_locale, &)
end end
def locale_from_params def validate_and_get_locale(locale)
I18n.available_locales.map(&:to_s).include?(params[:locale]) ? params[:locale] : nil return I18n.default_locale.to_s if locale.blank?
available_locales = I18n.available_locales.map(&:to_s)
locale_without_variant = locale.split('_')[0]
if available_locales.include?(locale)
locale
elsif available_locales.include?(locale_without_variant)
locale_without_variant
else
I18n.default_locale.to_s
end
end end
def locale_from_account(account) def locale_from_account(account)
return unless account return unless account
I18n.available_locales.map(&:to_s).include?(account.locale) ? account.locale : nil account.locale
end
def locale_from_env_variable
return unless ENV.fetch('DEFAULT_LOCALE', nil)
I18n.available_locales.map(&:to_s).include?(ENV.fetch('DEFAULT_LOCALE')) ? ENV.fetch('DEFAULT_LOCALE') : nil
end end
end end
@@ -1,4 +1,6 @@
class Public::Api::V1::Portals::BaseController < PublicController class Public::Api::V1::Portals::BaseController < PublicController
include SwitchLocale
before_action :show_plain_layout before_action :show_plain_layout
before_action :set_color_scheme before_action :set_color_scheme
before_action :set_global_config before_action :set_global_config
@@ -27,14 +29,7 @@ class Public::Api::V1::Portals::BaseController < PublicController
end end
def switch_locale_with_portal(&) def switch_locale_with_portal(&)
locale_without_variant = params[:locale].split('_')[0] @locale = validate_and_get_locale(params[:locale])
is_locale_available = I18n.available_locales.map(&:to_s).include?(params[:locale])
is_locale_variant_available = I18n.available_locales.map(&:to_s).include?(locale_without_variant)
if is_locale_available
@locale = params[:locale]
elsif is_locale_variant_available
@locale = locale_without_variant
end
I18n.with_locale(@locale, &) I18n.with_locale(@locale, &)
end end
@@ -44,12 +39,12 @@ class Public::Api::V1::Portals::BaseController < PublicController
Rails.logger.info "Article: not found for slug: #{params[:article_slug]}" Rails.logger.info "Article: not found for slug: #{params[:article_slug]}"
render_404 && return if article.blank? render_404 && return if article.blank?
@locale = if article.category.present? article_locale = if article.category.present?
article.category.locale article.category.locale
else else
article.portal.default_locale article.portal.default_locale
end end
@locale = validate_and_get_locale(article_locale)
I18n.with_locale(@locale, &) I18n.with_locale(@locale, &)
end end
+5 -1
View File
@@ -78,7 +78,11 @@ class AccountDashboard < Administrate::BaseDashboard
# COLLECTION_FILTERS = { # COLLECTION_FILTERS = {
# open: ->(resources) { resources.where(open: true) } # open: ->(resources) { resources.where(open: true) }
# }.freeze # }.freeze
COLLECTION_FILTERS = {}.freeze COLLECTION_FILTERS = {
active: ->(resources) { resources.where(status: :active) },
suspended: ->(resources) { resources.where(status: :suspended) },
recent: ->(resources) { resources.where('created_at > ?', 30.days.ago) }
}.freeze
# Overwrite this method to customize how accounts are displayed # Overwrite this method to customize how accounts are displayed
# across all pages of the admin dashboard. # across all pages of the admin dashboard.
+6 -1
View File
@@ -94,7 +94,12 @@ class UserDashboard < Administrate::BaseDashboard
# COLLECTION_FILTERS = { # COLLECTION_FILTERS = {
# open: ->(resources) { resources.where(open: true) } # open: ->(resources) { resources.where(open: true) }
# }.freeze # }.freeze
COLLECTION_FILTERS = {}.freeze COLLECTION_FILTERS = {
super_admin: ->(resources) { resources.where(type: 'SuperAdmin') },
confirmed: ->(resources) { resources.where.not(confirmed_at: nil) },
unconfirmed: ->(resources) { resources.where(confirmed_at: nil) },
recent: ->(resources) { resources.where('created_at > ?', 30.days.ago) }
}.freeze
# Overwrite this method to customize how users are displayed # Overwrite this method to customize how users are displayed
# across all pages of the admin dashboard. # across all pages of the admin dashboard.
@@ -1,4 +1,4 @@
module FilterHelper module Filters::FilterHelper
def build_condition_query(model_filters, query_hash, current_index) def build_condition_query(model_filters, query_hash, current_index)
current_filter = model_filters[query_hash['attribute_key']] current_filter = model_filters[query_hash['attribute_key']]
@@ -89,4 +89,18 @@ module FilterHelper
operator = condition['query_operator'].upcase operator = condition['query_operator'].upcase
raise CustomExceptions::CustomFilter::InvalidQueryOperator.new({}) unless %w[AND OR].include?(operator) raise CustomExceptions::CustomFilter::InvalidQueryOperator.new({}) unless %w[AND OR].include?(operator)
end end
def conversation_status_values(values)
return Conversation.statuses.values if values.include?('all')
values.map { |x| Conversation.statuses[x.to_sym] }
end
def conversation_priority_values(values)
values.map { |x| Conversation.priorities[x.to_sym] }
end
def message_type_values(values)
values.map { |x| Message.message_types[x.to_sym] }
end
end end
@@ -146,17 +146,19 @@
--solid-2: 255 255 255; --solid-2: 255 255 255;
--solid-3: 255 255 255; --solid-3: 255 255 255;
--solid-active: 255 255 255; --solid-active: 255 255 255;
--solid-amber: 252 232 193; --solid-amber: 255 228 181;
--solid-blue: 218 236 255; --solid-blue: 182 216 255;
--solid-iris: 230 231 255; --solid-iris: 230 231 255;
--solid-purple: 202 202 255;
--solid-red: 255 212 219;
--alpha-1: 67, 67, 67, 0.06; --alpha-1: 67, 67, 67, 0.06;
--alpha-2: 201, 202, 207, 0.15; --alpha-2: 196, 197, 198, 0.22;
--alpha-3: 255, 255, 255, 0.96; --alpha-3: 255, 255, 255, 0.96;
--black-alpha-1: 0, 0, 0, 0.12; --black-alpha-1: 0, 0, 0, 0.08;
--black-alpha-2: 0, 0, 0, 0.04; --black-alpha-2: 0, 0, 0, 0.04;
--border-blue: 39, 129, 246, 0.5; --border-blue: 39, 129, 246, 0.5;
--white-alpha: 255, 255, 255, 0.8; --white-alpha: 255, 255, 255, 0.84;
} }
.dark { .dark {
@@ -240,16 +242,18 @@
--gray-12: 238 238 238; --gray-12: 238 238 238;
--background-color: 18 18 19; --background-color: 18 18 19;
--text-blue: 126 182 255;
--border-strong: 52 52 52; --border-strong: 52 52 52;
--border-weak: 38 38 42; --border-weak: 38 38 42;
--solid-1: 23 23 26; --solid-1: 23 23 26;
--solid-2: 29 30 36; --solid-2: 29 30 36;
--solid-3: 44 45 54; --solid-3: 44 45 54;
--solid-active: 53 57 66; --solid-active: 53 57 66;
--solid-amber: 42 37 30; --solid-amber: 56 50 41;
--solid-blue: 16 49 91; --solid-blue: 4 51 101;
--solid-iris: 38 42 101; --solid-iris: 38 42 101;
--text-blue: 126 182 255; --solid-purple: 51 51 107;
--solid-red: 78 46 55;
--alpha-1: 36, 36, 36, 0.8; --alpha-1: 36, 36, 36, 0.8;
--alpha-2: 139, 147, 182, 0.15; --alpha-2: 139, 147, 182, 0.15;
@@ -50,16 +50,16 @@ const AVATAR_COLORS = {
dark: [ dark: [
['#4B143D', '#FF8DCC'], ['#4B143D', '#FF8DCC'],
['#3F220D', '#FFA366'], ['#3F220D', '#FFA366'],
['#2A2A2A', '#ADB1B8'],
['#023B37', '#0BD8B6'], ['#023B37', '#0BD8B6'],
['#2A2A2A', '#ADB1B8'],
['#27264D', '#A19EFF'], ['#27264D', '#A19EFF'],
['#1D2E62', '#9EB1FF'], ['#1D2E62', '#9EB1FF'],
], ],
light: [ light: [
['#FBDCEF', '#C2298A'], ['#FBDCEF', '#C2298A'],
['#FFE0BB', '#99543A'], ['#FFE0BB', '#99543A'],
['#E8E8E8', '#60646C'],
['#CCF3EA', '#008573'], ['#CCF3EA', '#008573'],
['#E8E8E8', '#60646C'],
['#EBEBFE', '#4747C2'], ['#EBEBFE', '#4747C2'],
['#E1E9FF', '#3A5BC7'], ['#E1E9FF', '#3A5BC7'],
], ],
@@ -2,7 +2,10 @@ import { computed } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useOperators } from './operators'; import { useOperators } from './operators';
import { useMapGetter } from 'dashboard/composables/store.js'; import { useMapGetter } from 'dashboard/composables/store.js';
import { buildAttributesFilterTypes } from './helper/filterHelper.js'; import {
buildAttributesFilterTypes,
CONTACT_ATTRIBUTES,
} from './helper/filterHelper.js';
import countries from 'shared/constants/countries.js'; import countries from 'shared/constants/countries.js';
/** /**
@@ -59,7 +62,11 @@ export function useContactFilterContext() {
* @type {import('vue').ComputedRef<FilterType[]>} * @type {import('vue').ComputedRef<FilterType[]>}
*/ */
const customFilterTypes = computed(() => const customFilterTypes = computed(() =>
buildAttributesFilterTypes(contactAttributes.value, getOperatorTypes) buildAttributesFilterTypes(
contactAttributes.value,
getOperatorTypes,
'contact'
)
); );
/** /**
@@ -67,8 +74,8 @@ export function useContactFilterContext() {
*/ */
const filterTypes = computed(() => [ const filterTypes = computed(() => [
{ {
attributeKey: 'name', attributeKey: CONTACT_ATTRIBUTES.NAME,
value: 'name', value: CONTACT_ATTRIBUTES.NAME,
attributeName: t('CONTACTS_LAYOUT.FILTER.NAME'), attributeName: t('CONTACTS_LAYOUT.FILTER.NAME'),
label: t('CONTACTS_LAYOUT.FILTER.NAME'), label: t('CONTACTS_LAYOUT.FILTER.NAME'),
inputType: 'plainText', inputType: 'plainText',
@@ -77,8 +84,8 @@ export function useContactFilterContext() {
attributeModel: 'standard', attributeModel: 'standard',
}, },
{ {
attributeKey: 'email', attributeKey: CONTACT_ATTRIBUTES.EMAIL,
value: 'email', value: CONTACT_ATTRIBUTES.EMAIL,
attributeName: t('CONTACTS_LAYOUT.FILTER.EMAIL'), attributeName: t('CONTACTS_LAYOUT.FILTER.EMAIL'),
label: t('CONTACTS_LAYOUT.FILTER.EMAIL'), label: t('CONTACTS_LAYOUT.FILTER.EMAIL'),
inputType: 'plainText', inputType: 'plainText',
@@ -87,8 +94,8 @@ export function useContactFilterContext() {
attributeModel: 'standard', attributeModel: 'standard',
}, },
{ {
attributeKey: 'phone_number', attributeKey: CONTACT_ATTRIBUTES.PHONE_NUMBER,
value: 'phone_number', value: CONTACT_ATTRIBUTES.PHONE_NUMBER,
attributeName: t('CONTACTS_LAYOUT.FILTER.PHONE_NUMBER'), attributeName: t('CONTACTS_LAYOUT.FILTER.PHONE_NUMBER'),
label: t('CONTACTS_LAYOUT.FILTER.PHONE_NUMBER'), label: t('CONTACTS_LAYOUT.FILTER.PHONE_NUMBER'),
inputType: 'plainText', inputType: 'plainText',
@@ -97,8 +104,8 @@ export function useContactFilterContext() {
attributeModel: 'standard', attributeModel: 'standard',
}, },
{ {
attributeKey: 'identifier', attributeKey: CONTACT_ATTRIBUTES.IDENTIFIER,
value: 'identifier', value: CONTACT_ATTRIBUTES.IDENTIFIER,
attributeName: t('CONTACTS_LAYOUT.FILTER.IDENTIFIER'), attributeName: t('CONTACTS_LAYOUT.FILTER.IDENTIFIER'),
label: t('CONTACTS_LAYOUT.FILTER.IDENTIFIER'), label: t('CONTACTS_LAYOUT.FILTER.IDENTIFIER'),
inputType: 'plainText', inputType: 'plainText',
@@ -107,8 +114,8 @@ export function useContactFilterContext() {
attributeModel: 'standard', attributeModel: 'standard',
}, },
{ {
attributeKey: 'country_code', attributeKey: CONTACT_ATTRIBUTES.COUNTRY_CODE,
value: 'country_code', value: CONTACT_ATTRIBUTES.COUNTRY_CODE,
attributeName: t('FILTER.ATTRIBUTES.COUNTRY_NAME'), attributeName: t('FILTER.ATTRIBUTES.COUNTRY_NAME'),
label: t('FILTER.ATTRIBUTES.COUNTRY_NAME'), label: t('FILTER.ATTRIBUTES.COUNTRY_NAME'),
inputType: 'searchSelect', inputType: 'searchSelect',
@@ -118,8 +125,8 @@ export function useContactFilterContext() {
attributeModel: 'additional', attributeModel: 'additional',
}, },
{ {
attributeKey: 'city', attributeKey: CONTACT_ATTRIBUTES.CITY,
value: 'city', value: CONTACT_ATTRIBUTES.CITY,
attributeName: t('CONTACTS_LAYOUT.FILTER.CITY'), attributeName: t('CONTACTS_LAYOUT.FILTER.CITY'),
label: t('CONTACTS_LAYOUT.FILTER.CITY'), label: t('CONTACTS_LAYOUT.FILTER.CITY'),
inputType: 'plainText', inputType: 'plainText',
@@ -128,8 +135,8 @@ export function useContactFilterContext() {
attributeModel: 'standard', attributeModel: 'standard',
}, },
{ {
attributeKey: 'created_at', attributeKey: CONTACT_ATTRIBUTES.CREATED_AT,
value: 'created_at', value: CONTACT_ATTRIBUTES.CREATED_AT,
attributeName: t('CONTACTS_LAYOUT.FILTER.CREATED_AT'), attributeName: t('CONTACTS_LAYOUT.FILTER.CREATED_AT'),
label: t('CONTACTS_LAYOUT.FILTER.CREATED_AT'), label: t('CONTACTS_LAYOUT.FILTER.CREATED_AT'),
inputType: 'date', inputType: 'date',
@@ -138,8 +145,8 @@ export function useContactFilterContext() {
attributeModel: 'standard', attributeModel: 'standard',
}, },
{ {
attributeKey: 'last_activity_at', attributeKey: CONTACT_ATTRIBUTES.LAST_ACTIVITY_AT,
value: 'last_activity_at', value: CONTACT_ATTRIBUTES.LAST_ACTIVITY_AT,
attributeName: t('CONTACTS_LAYOUT.FILTER.LAST_ACTIVITY'), attributeName: t('CONTACTS_LAYOUT.FILTER.LAST_ACTIVITY'),
label: t('CONTACTS_LAYOUT.FILTER.LAST_ACTIVITY'), label: t('CONTACTS_LAYOUT.FILTER.LAST_ACTIVITY'),
inputType: 'date', inputType: 'date',
@@ -148,8 +155,8 @@ export function useContactFilterContext() {
attributeModel: 'standard', attributeModel: 'standard',
}, },
{ {
attributeKey: 'referer', attributeKey: CONTACT_ATTRIBUTES.REFERER,
value: 'referer', value: CONTACT_ATTRIBUTES.REFERER,
attributeName: t('CONTACTS_LAYOUT.FILTER.REFERER_LINK'), attributeName: t('CONTACTS_LAYOUT.FILTER.REFERER_LINK'),
label: t('CONTACTS_LAYOUT.FILTER.REFERER_LINK'), label: t('CONTACTS_LAYOUT.FILTER.REFERER_LINK'),
inputType: 'plainText', inputType: 'plainText',
@@ -158,8 +165,8 @@ export function useContactFilterContext() {
attributeModel: 'standard', attributeModel: 'standard',
}, },
{ {
attributeKey: 'blocked', attributeKey: CONTACT_ATTRIBUTES.BLOCKED,
value: 'blocked', value: CONTACT_ATTRIBUTES.BLOCKED,
attributeName: t('CONTACTS_LAYOUT.FILTER.BLOCKED'), attributeName: t('CONTACTS_LAYOUT.FILTER.BLOCKED'),
label: t('CONTACTS_LAYOUT.FILTER.BLOCKED'), label: t('CONTACTS_LAYOUT.FILTER.BLOCKED'),
inputType: 'searchSelect', inputType: 'searchSelect',
@@ -1,3 +1,35 @@
/**
* Standard attributes of the conversation model
*/
export const CONVERSATION_ATTRIBUTES = {
STATUS: 'status',
PRIORITY: 'priority',
ASSIGNEE_ID: 'assignee_id',
INBOX_ID: 'inbox_id',
TEAM_ID: 'team_id',
DISPLAY_ID: 'display_id',
CAMPAIGN_ID: 'campaign_id',
LABELS: 'labels',
BROWSER_LANGUAGE: 'browser_language',
COUNTRY_CODE: 'country_code',
REFERER: 'referer',
CREATED_AT: 'created_at',
LAST_ACTIVITY_AT: 'last_activity_at',
};
export const CONTACT_ATTRIBUTES = {
NAME: 'name',
EMAIL: 'email',
PHONE_NUMBER: 'phone_number',
IDENTIFIER: 'identifier',
COUNTRY_CODE: 'country_code',
CITY: 'city',
CREATED_AT: 'created_at',
LAST_ACTIVITY_AT: 'last_activity_at',
REFERER: 'referer',
BLOCKED: 'blocked',
};
/** /**
* Determines the input type for a custom attribute based on its key * Determines the input type for a custom attribute based on its key
* @param {string} key - The attribute display type key * @param {string} key - The attribute display type key
@@ -20,12 +52,25 @@ export const getCustomAttributeInputType = key => {
/** /**
* Builds filter types for custom attributes * Builds filter types for custom attributes
* This also removes any conflicting attributes
* @param {Array} attributes - The attributes array * @param {Array} attributes - The attributes array
* @param {Function} getOperatorTypes - Function to get operator types * @param {Function} getOperatorTypes - Function to get operator types
* @returns {Array} Array of filter types * @returns {Array} Array of filter types
*/ */
export const buildAttributesFilterTypes = (attributes, getOperatorTypes) => { export const buildAttributesFilterTypes = (
return attributes.map(attr => ({ attributes,
getOperatorTypes,
filterModel = 'conversation'
) => {
const standardAttributes = Object.values(
filterModel === 'conversation'
? CONVERSATION_ATTRIBUTES
: CONTACT_ATTRIBUTES
);
return attributes
.filter(attr => !standardAttributes.includes(attr.attributeKey))
.map(attr => ({
attributeKey: attr.attributeKey, attributeKey: attr.attributeKey,
value: attr.attributeKey, value: attr.attributeKey,
attributeName: attr.attributeDisplayName, attributeName: attr.attributeDisplayName,
@@ -3,7 +3,10 @@ import { useI18n } from 'vue-i18n';
import { useOperators } from './operators'; import { useOperators } from './operators';
import { useMapGetter } from 'dashboard/composables/store.js'; import { useMapGetter } from 'dashboard/composables/store.js';
import { useChannelIcon } from 'next/icon/provider'; import { useChannelIcon } from 'next/icon/provider';
import { buildAttributesFilterTypes } from './helper/filterHelper'; import {
buildAttributesFilterTypes,
CONVERSATION_ATTRIBUTES,
} from './helper/filterHelper';
import countries from 'shared/constants/countries.js'; import countries from 'shared/constants/countries.js';
import languages from 'dashboard/components/widgets/conversation/advancedFilterItems/languages.js'; import languages from 'dashboard/components/widgets/conversation/advancedFilterItems/languages.js';
@@ -70,7 +73,11 @@ export function useConversationFilterContext() {
* @type {import('vue').ComputedRef<FilterType[]>} * @type {import('vue').ComputedRef<FilterType[]>}
*/ */
const customFilterTypes = computed(() => const customFilterTypes = computed(() =>
buildAttributesFilterTypes(conversationAttributes.value, getOperatorTypes) buildAttributesFilterTypes(
conversationAttributes.value,
getOperatorTypes,
'conversation'
)
); );
/** /**
@@ -78,8 +85,8 @@ export function useConversationFilterContext() {
*/ */
const filterTypes = computed(() => [ const filterTypes = computed(() => [
{ {
attributeKey: 'status', attributeKey: CONVERSATION_ATTRIBUTES.STATUS,
value: 'status', value: CONVERSATION_ATTRIBUTES.STATUS,
attributeName: t('FILTER.ATTRIBUTES.STATUS'), attributeName: t('FILTER.ATTRIBUTES.STATUS'),
label: t('FILTER.ATTRIBUTES.STATUS'), label: t('FILTER.ATTRIBUTES.STATUS'),
inputType: 'multiSelect', inputType: 'multiSelect',
@@ -94,8 +101,24 @@ export function useConversationFilterContext() {
attributeModel: 'standard', attributeModel: 'standard',
}, },
{ {
attributeKey: 'assignee_id', attributeKey: CONVERSATION_ATTRIBUTES.PRIORITY,
value: 'assignee_id', value: CONVERSATION_ATTRIBUTES.PRIORITY,
attributeName: t('FILTER.ATTRIBUTES.PRIORITY'),
label: t('FILTER.ATTRIBUTES.PRIORITY'),
inputType: 'multiSelect',
options: ['low', 'medium', 'high', 'urgent'].map(id => {
return {
id,
name: t(`CONVERSATION.PRIORITY.OPTIONS.${id.toUpperCase()}`),
};
}),
dataType: 'text',
filterOperators: equalityOperators.value,
attributeModel: 'standard',
},
{
attributeKey: CONVERSATION_ATTRIBUTES.ASSIGNEE_ID,
value: CONVERSATION_ATTRIBUTES.ASSIGNEE_ID,
attributeName: t('FILTER.ATTRIBUTES.ASSIGNEE_NAME'), attributeName: t('FILTER.ATTRIBUTES.ASSIGNEE_NAME'),
label: t('FILTER.ATTRIBUTES.ASSIGNEE_NAME'), label: t('FILTER.ATTRIBUTES.ASSIGNEE_NAME'),
inputType: 'searchSelect', inputType: 'searchSelect',
@@ -110,8 +133,8 @@ export function useConversationFilterContext() {
attributeModel: 'standard', attributeModel: 'standard',
}, },
{ {
attributeKey: 'inbox_id', attributeKey: CONVERSATION_ATTRIBUTES.INBOX_ID,
value: 'inbox_id', value: CONVERSATION_ATTRIBUTES.INBOX_ID,
attributeName: t('FILTER.ATTRIBUTES.INBOX_NAME'), attributeName: t('FILTER.ATTRIBUTES.INBOX_NAME'),
label: t('FILTER.ATTRIBUTES.INBOX_NAME'), label: t('FILTER.ATTRIBUTES.INBOX_NAME'),
inputType: 'searchSelect', inputType: 'searchSelect',
@@ -126,8 +149,8 @@ export function useConversationFilterContext() {
attributeModel: 'standard', attributeModel: 'standard',
}, },
{ {
attributeKey: 'team_id', attributeKey: CONVERSATION_ATTRIBUTES.TEAM_ID,
value: 'team_id', value: CONVERSATION_ATTRIBUTES.TEAM_ID,
attributeName: t('FILTER.ATTRIBUTES.TEAM_NAME'), attributeName: t('FILTER.ATTRIBUTES.TEAM_NAME'),
label: t('FILTER.ATTRIBUTES.TEAM_NAME'), label: t('FILTER.ATTRIBUTES.TEAM_NAME'),
inputType: 'searchSelect', inputType: 'searchSelect',
@@ -137,8 +160,8 @@ export function useConversationFilterContext() {
attributeModel: 'standard', attributeModel: 'standard',
}, },
{ {
attributeKey: 'display_id', attributeKey: CONVERSATION_ATTRIBUTES.DISPLAY_ID,
value: 'display_id', value: CONVERSATION_ATTRIBUTES.DISPLAY_ID,
attributeName: t('FILTER.ATTRIBUTES.CONVERSATION_IDENTIFIER'), attributeName: t('FILTER.ATTRIBUTES.CONVERSATION_IDENTIFIER'),
label: t('FILTER.ATTRIBUTES.CONVERSATION_IDENTIFIER'), label: t('FILTER.ATTRIBUTES.CONVERSATION_IDENTIFIER'),
inputType: 'plainText', inputType: 'plainText',
@@ -147,8 +170,8 @@ export function useConversationFilterContext() {
attributeModel: 'standard', attributeModel: 'standard',
}, },
{ {
attributeKey: 'campaign_id', attributeKey: CONVERSATION_ATTRIBUTES.CAMPAIGN_ID,
value: 'campaign_id', value: CONVERSATION_ATTRIBUTES.CAMPAIGN_ID,
attributeName: t('FILTER.ATTRIBUTES.CAMPAIGN_NAME'), attributeName: t('FILTER.ATTRIBUTES.CAMPAIGN_NAME'),
label: t('FILTER.ATTRIBUTES.CAMPAIGN_NAME'), label: t('FILTER.ATTRIBUTES.CAMPAIGN_NAME'),
inputType: 'searchSelect', inputType: 'searchSelect',
@@ -161,8 +184,8 @@ export function useConversationFilterContext() {
attributeModel: 'standard', attributeModel: 'standard',
}, },
{ {
attributeKey: 'labels', attributeKey: CONVERSATION_ATTRIBUTES.LABELS,
value: 'labels', value: CONVERSATION_ATTRIBUTES.LABELS,
attributeName: t('FILTER.ATTRIBUTES.LABELS'), attributeName: t('FILTER.ATTRIBUTES.LABELS'),
label: t('FILTER.ATTRIBUTES.LABELS'), label: t('FILTER.ATTRIBUTES.LABELS'),
inputType: 'multiSelect', inputType: 'multiSelect',
@@ -185,8 +208,8 @@ export function useConversationFilterContext() {
attributeModel: 'standard', attributeModel: 'standard',
}, },
{ {
attributeKey: 'browser_language', attributeKey: CONVERSATION_ATTRIBUTES.BROWSER_LANGUAGE,
value: 'browser_language', value: CONVERSATION_ATTRIBUTES.BROWSER_LANGUAGE,
attributeName: t('FILTER.ATTRIBUTES.BROWSER_LANGUAGE'), attributeName: t('FILTER.ATTRIBUTES.BROWSER_LANGUAGE'),
label: t('FILTER.ATTRIBUTES.BROWSER_LANGUAGE'), label: t('FILTER.ATTRIBUTES.BROWSER_LANGUAGE'),
inputType: 'searchSelect', inputType: 'searchSelect',
@@ -196,8 +219,8 @@ export function useConversationFilterContext() {
attributeModel: 'additional', attributeModel: 'additional',
}, },
{ {
attributeKey: 'country_code', attributeKey: CONVERSATION_ATTRIBUTES.COUNTRY_CODE,
value: 'country_code', value: CONVERSATION_ATTRIBUTES.COUNTRY_CODE,
attributeName: t('FILTER.ATTRIBUTES.COUNTRY_NAME'), attributeName: t('FILTER.ATTRIBUTES.COUNTRY_NAME'),
label: t('FILTER.ATTRIBUTES.COUNTRY_NAME'), label: t('FILTER.ATTRIBUTES.COUNTRY_NAME'),
inputType: 'searchSelect', inputType: 'searchSelect',
@@ -207,8 +230,8 @@ export function useConversationFilterContext() {
attributeModel: 'additional', attributeModel: 'additional',
}, },
{ {
attributeKey: 'referer', attributeKey: CONVERSATION_ATTRIBUTES.REFERER,
value: 'referer', value: CONVERSATION_ATTRIBUTES.REFERER,
attributeName: t('FILTER.ATTRIBUTES.REFERER_LINK'), attributeName: t('FILTER.ATTRIBUTES.REFERER_LINK'),
label: t('FILTER.ATTRIBUTES.REFERER_LINK'), label: t('FILTER.ATTRIBUTES.REFERER_LINK'),
inputType: 'plainText', inputType: 'plainText',
@@ -217,8 +240,8 @@ export function useConversationFilterContext() {
attributeModel: 'additional', attributeModel: 'additional',
}, },
{ {
attributeKey: 'created_at', attributeKey: CONVERSATION_ATTRIBUTES.CREATED_AT,
value: 'created_at', value: CONVERSATION_ATTRIBUTES.CREATED_AT,
attributeName: t('FILTER.ATTRIBUTES.CREATED_AT'), attributeName: t('FILTER.ATTRIBUTES.CREATED_AT'),
label: t('FILTER.ATTRIBUTES.CREATED_AT'), label: t('FILTER.ATTRIBUTES.CREATED_AT'),
inputType: 'date', inputType: 'date',
@@ -227,8 +250,8 @@ export function useConversationFilterContext() {
attributeModel: 'standard', attributeModel: 'standard',
}, },
{ {
attributeKey: 'last_activity_at', attributeKey: CONVERSATION_ATTRIBUTES.LAST_ACTIVITY_AT,
value: 'last_activity_at', value: CONVERSATION_ATTRIBUTES.LAST_ACTIVITY_AT,
attributeName: t('FILTER.ATTRIBUTES.LAST_ACTIVITY'), attributeName: t('FILTER.ATTRIBUTES.LAST_ACTIVITY'),
label: t('FILTER.ATTRIBUTES.LAST_ACTIVITY'), label: t('FILTER.ATTRIBUTES.LAST_ACTIVITY'),
inputType: 'date', inputType: 'date',
@@ -117,7 +117,7 @@ const props = defineProps({
}, },
conversationId: { type: Number, required: true }, conversationId: { type: Number, required: true },
createdAt: { type: Number, required: true }, // eslint-disable-line vue/no-unused-properties createdAt: { type: Number, required: true }, // eslint-disable-line vue/no-unused-properties
currentUserId: { type: Number, required: true }, currentUserId: { type: Number, required: true }, // eslint-disable-line vue/no-unused-properties
groupWithNext: { type: Boolean, default: false }, groupWithNext: { type: Boolean, default: false },
inboxId: { type: Number, default: null }, // eslint-disable-line vue/no-unused-properties inboxId: { type: Number, default: null }, // eslint-disable-line vue/no-unused-properties
inboxSupportsReplyTo: { type: Object, default: () => ({}) }, inboxSupportsReplyTo: { type: Object, default: () => ({}) },
@@ -173,7 +173,11 @@ const variant = computed(() => {
return variants[props.messageType] || MESSAGE_VARIANTS.USER; return variants[props.messageType] || MESSAGE_VARIANTS.USER;
}); });
const isMyMessage = computed(() => { const isBotOrAgentMessage = computed(() => {
if (props.messageType === MESSAGE_TYPES.ACTIVITY) {
return false;
}
// if an outgoing message is still processing, then it's definitely a // if an outgoing message is still processing, then it's definitely a
// message sent by the current user // message sent by the current user
if ( if (
@@ -182,17 +186,15 @@ const isMyMessage = computed(() => {
) { ) {
return true; return true;
} }
const senderId = props.senderId ?? props.sender?.id; const senderId = props.senderId ?? props.sender?.id;
const senderType = props.senderType ?? props.sender?.type; const senderType = props.senderType ?? props.sender?.type;
if (!senderType || !senderId) { if (!senderType || !senderId) {
return false; return true;
} }
return ( return senderType.toLowerCase() === SENDER_TYPES.USER.toLowerCase();
senderType.toLowerCase() === SENDER_TYPES.USER.toLowerCase() &&
props.currentUserId === senderId
);
}); });
/** /**
@@ -200,7 +202,7 @@ const isMyMessage = computed(() => {
* @returns {import('vue').ComputedRef<'left'|'right'|'center'>} The computed orientation * @returns {import('vue').ComputedRef<'left'|'right'|'center'>} The computed orientation
*/ */
const orientation = computed(() => { const orientation = computed(() => {
if (isMyMessage.value) { if (isBotOrAgentMessage.value) {
return ORIENTATION.RIGHT; return ORIENTATION.RIGHT;
} }
@@ -221,8 +223,8 @@ const flexOrientationClass = computed(() => {
const gridClass = computed(() => { const gridClass = computed(() => {
const map = { const map = {
[ORIENTATION.LEFT]: 'grid grid-cols-[24px_1fr]', [ORIENTATION.LEFT]: 'grid grid-cols-1fr',
[ORIENTATION.RIGHT]: 'grid grid-cols-1fr', [ORIENTATION.RIGHT]: 'grid grid-cols-[1fr_24px]',
}; };
return map[orientation.value]; return map[orientation.value];
@@ -231,13 +233,13 @@ const gridClass = computed(() => {
const gridTemplate = computed(() => { const gridTemplate = computed(() => {
const map = { const map = {
[ORIENTATION.LEFT]: ` [ORIENTATION.LEFT]: `
"avatar bubble"
"spacer meta"
`,
[ORIENTATION.RIGHT]: `
"bubble" "bubble"
"meta" "meta"
`, `,
[ORIENTATION.RIGHT]: `
"bubble avatar"
"meta spacer"
`,
}; };
return map[orientation.value]; return map[orientation.value];
@@ -251,7 +253,7 @@ const shouldGroupWithNext = computed(() => {
const shouldShowAvatar = computed(() => { const shouldShowAvatar = computed(() => {
if (props.messageType === MESSAGE_TYPES.ACTIVITY) return false; if (props.messageType === MESSAGE_TYPES.ACTIVITY) return false;
if (orientation.value === ORIENTATION.RIGHT) return false; if (orientation.value === ORIENTATION.LEFT) return false;
return true; return true;
}); });
@@ -438,7 +440,7 @@ provideMessageContext({
isPrivate: computed(() => props.private), isPrivate: computed(() => props.private),
variant, variant,
orientation, orientation,
isMyMessage, isBotOrAgentMessage,
shouldGroupWithNext, shouldGroupWithNext,
}); });
</script> </script>
@@ -448,7 +450,7 @@ provideMessageContext({
<div <div
v-if="shouldRenderMessage" v-if="shouldRenderMessage"
:id="`message${props.id}`" :id="`message${props.id}`"
class="flex w-full message-bubble-container mb-2" class="flex w-full message-bubble-container"
:data-message-id="props.id" :data-message-id="props.id"
:class="[ :class="[
flexOrientationClass, flexOrientationClass,
@@ -470,22 +472,28 @@ provideMessageContext({
'w-full': variant === MESSAGE_VARIANTS.EMAIL, 'w-full': variant === MESSAGE_VARIANTS.EMAIL,
}, },
]" ]"
class="gap-x-3" class="gap-x-2"
:style="{ :style="{
gridTemplateAreas: gridTemplate, gridTemplateAreas: gridTemplate,
}" }"
> >
<div <div
v-if="!shouldGroupWithNext && shouldShowAvatar" v-if="!shouldGroupWithNext && shouldShowAvatar"
v-tooltip.right-end="avatarTooltip" v-tooltip.left-end="avatarTooltip"
class="[grid-area:avatar] flex items-end" class="[grid-area:avatar] flex items-end"
> >
<Avatar v-bind="avatarInfo" :size="24" /> <Avatar v-bind="avatarInfo" :size="20" />
</div> </div>
<div <div
class="[grid-area:bubble] flex" class="[grid-area:bubble] flex"
:class="{ :class="{
'ltr:pl-9 rtl:pl-0 justify-end': orientation === ORIENTATION.RIGHT, 'justify-end': orientation === ORIENTATION.RIGHT,
'ltr:pl-12 rtl:pr-12 2xl:ltr:pl-0 2xl:rtl:pr-0':
orientation === ORIENTATION.RIGHT &&
variant !== MESSAGE_VARIANTS.EMAIL,
'ltr:pr-12 rtl:pl-12 2xl:ltr:pr-0 2xl:rtl:pl-0':
orientation === ORIENTATION.LEFT &&
variant !== MESSAGE_VARIANTS.EMAIL,
'min-w-0': variant === MESSAGE_VARIANTS.EMAIL, 'min-w-0': variant === MESSAGE_VARIANTS.EMAIL,
}" }"
@contextmenu="openContextMenu($event)" @contextmenu="openContextMenu($event)"
@@ -26,7 +26,7 @@ const { t } = useI18n();
/> />
</div> </div>
<div <div
class="absolute bg-n-alpha-3 px-4 py-3 border rounded-xl border-n-strong text-n-slate-12 bottom-6 w-52 text-xs backdrop-blur-[100px] shadow-[0px_0px_24px_0px_rgba(0,0,0,0.12)] opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all" class="absolute bg-n-alpha-3 px-4 py-3 border rounded-xl border-n-strong text-n-slate-12 bottom-6 w-52 text-xs backdrop-blur-[100px] shadow-[0px_0px_24px_0px_rgba(0,0,0,0.12)] opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all break-all"
:class="{ :class="{
'ltr:left-0 rtl:right-0': orientation === ORIENTATION.LEFT, 'ltr:left-0 rtl:right-0': orientation === ORIENTATION.LEFT,
'ltr:right-0 rtl:left-0': orientation === ORIENTATION.RIGHT, 'ltr:right-0 rtl:left-0': orientation === ORIENTATION.RIGHT,
@@ -66,7 +66,11 @@ const shouldGroupWithNext = (index, searchList) => {
nextMessageType === MESSAGE_TYPES.TEMPLATE && nextMessageType === MESSAGE_TYPES.TEMPLATE &&
currentMessageType === MESSAGE_TYPES.TEMPLATE; currentMessageType === MESSAGE_TYPES.TEMPLATE;
if (!hasSameSender || areBothTemplates) return false; const areBothActivity =
nextMessageType === MESSAGE_TYPES.ACTIVITY &&
currentMessageType === MESSAGE_TYPES.ACTIVITY;
if (!hasSameSender || areBothTemplates || areBothActivity) return false;
if (currentMessageType !== nextMessageType) return false; if (currentMessageType !== nextMessageType) return false;
@@ -95,6 +99,17 @@ const getInReplyToMessage = parentMessage => {
return replyMessage ? useCamelCase(replyMessage) : null; return replyMessage ? useCamelCase(replyMessage) : null;
}; };
const getMessageSpacingClass = (message, messages, index) => {
// For non-activity messages, use groupWithNext logic
if (message.messageType !== MESSAGE_TYPES.ACTIVITY) {
return shouldGroupWithNext(index, messages) ? 'mb-1' : 'mb-6';
}
// For activity messages, check next message exists and is also an activity
const nextMessage = messages[index + 1];
return nextMessage?.messageType === MESSAGE_TYPES.ACTIVITY ? 'mb-2' : 'mb-6';
};
</script> </script>
<template> <template>
@@ -112,6 +127,7 @@ const getInReplyToMessage = parentMessage => {
:group-with-next="shouldGroupWithNext(index, allMessages)" :group-with-next="shouldGroupWithNext(index, allMessages)"
:inbox-supports-reply-to="inboxSupportsReplyTo" :inbox-supports-reply-to="inboxSupportsReplyTo"
:current-user-id="currentUserId" :current-user-id="currentUserId"
:class="getMessageSpacingClass(message, allMessages, index)"
data-clarity-mask="True" data-clarity-mask="True"
/> />
</template> </template>
@@ -49,7 +49,7 @@ const isSent = computed(() => {
isASmsInbox.value || isASmsInbox.value ||
isATelegramChannel.value isATelegramChannel.value
) { ) {
return status.value === MESSAGE_STATUS.SENT; return sourceId.value && status.value === MESSAGE_STATUS.SENT;
} }
// All messages will be mark as sent for the Line channel, as there is no source ID. // All messages will be mark as sent for the Line channel, as there is no source ID.
@@ -67,7 +67,7 @@ const isDelivered = computed(() => {
isASmsInbox.value || isASmsInbox.value ||
isAFacebookInbox.value isAFacebookInbox.value
) { ) {
return status.value === MESSAGE_STATUS.DELIVERED; return sourceId.value && status.value === MESSAGE_STATUS.DELIVERED;
} }
// All messages marked as delivered for the web widget inbox and API inbox once they are sent. // All messages marked as delivered for the web widget inbox and API inbox once they are sent.
if (isAWebWidgetInbox.value || isAPIInbox.value) { if (isAWebWidgetInbox.value || isAPIInbox.value) {
@@ -88,7 +88,7 @@ const isRead = computed(() => {
isATwilioChannel.value || isATwilioChannel.value ||
isAFacebookInbox.value isAFacebookInbox.value
) { ) {
return status.value === MESSAGE_STATUS.READ; return sourceId.value && status.value === MESSAGE_STATUS.READ;
} }
if (isAWebWidgetInbox.value || isAPIInbox.value) { if (isAWebWidgetInbox.value || isAPIInbox.value) {
@@ -102,6 +102,7 @@ const statusToShow = computed(() => {
if (isRead.value) return MESSAGE_STATUS.READ; if (isRead.value) return MESSAGE_STATUS.READ;
if (isDelivered.value) return MESSAGE_STATUS.DELIVERED; if (isDelivered.value) return MESSAGE_STATUS.DELIVERED;
if (isSent.value) return MESSAGE_STATUS.SENT; if (isSent.value) return MESSAGE_STATUS.SENT;
if (status.value === MESSAGE_STATUS.FAILED) return MESSAGE_STATUS.FAILED;
return MESSAGE_STATUS.PROGRESS; return MESSAGE_STATUS.PROGRESS;
}); });
@@ -14,7 +14,7 @@ const readableTime = computed(() =>
<template> <template>
<BaseBubble <BaseBubble
v-tooltip.top="readableTime" v-tooltip.top="readableTime"
class="px-2 py-0.5 !rounded-full flex min-w-0 items-center gap-2" class="px-3 py-1 !rounded-full flex min-w-0 items-center gap-2"
data-bubble-name="activity" data-bubble-name="activity"
> >
<span v-dompurify-html="content" :title="content" /> <span v-dompurify-html="content" :title="content" />
@@ -20,9 +20,9 @@ const varaintBaseMap = {
'bg-n-solid-amber text-n-amber-12 [&_.prosemirror-mention-node]:font-semibold', 'bg-n-solid-amber text-n-amber-12 [&_.prosemirror-mention-node]:font-semibold',
[MESSAGE_VARIANTS.USER]: 'bg-n-slate-4 text-n-slate-12', [MESSAGE_VARIANTS.USER]: 'bg-n-slate-4 text-n-slate-12',
[MESSAGE_VARIANTS.ACTIVITY]: 'bg-n-alpha-1 text-n-slate-11 text-sm', [MESSAGE_VARIANTS.ACTIVITY]: 'bg-n-alpha-1 text-n-slate-11 text-sm',
[MESSAGE_VARIANTS.BOT]: 'bg-n-solid-iris text-n-slate-12', [MESSAGE_VARIANTS.BOT]: 'bg-n-solid-purple text-n-slate-12',
[MESSAGE_VARIANTS.TEMPLATE]: 'bg-n-solid-iris text-n-slate-12', [MESSAGE_VARIANTS.TEMPLATE]: 'bg-n-solid-purple text-n-slate-12',
[MESSAGE_VARIANTS.ERROR]: 'bg-n-ruby-4 text-n-ruby-12', [MESSAGE_VARIANTS.ERROR]: 'bg-n-solid-red text-n-ruby-12',
[MESSAGE_VARIANTS.EMAIL]: 'w-full', [MESSAGE_VARIANTS.EMAIL]: 'w-full',
[MESSAGE_VARIANTS.UNSUPPORTED]: [MESSAGE_VARIANTS.UNSUPPORTED]:
'bg-n-solid-amber/70 border border-dashed border-n-amber-12 text-n-amber-12', 'bg-n-solid-amber/70 border border-dashed border-n-amber-12 text-n-amber-12',
@@ -64,13 +64,13 @@ const senderName = computed(() => {
:href="action.href" :href="action.href"
rel="noreferrer noopener nofollow" rel="noreferrer noopener nofollow"
target="_blank" target="_blank"
class="w-full block bg-n-solid-3 px-4 py-2 rounded-lg text-sm text-center border border-n-container" class="w-full block bg-n-alpha-white px-4 py-2 rounded-lg text-sm text-center border border-n-container"
> >
{{ action.label }} {{ action.label }}
</a> </a>
<button <button
v-else v-else
class="w-full bg-n-solid-3 px-4 py-2 rounded-lg text-sm text-center border border-n-container" class="w-full bg-n-alpha-white px-4 py-2 rounded-lg text-sm text-center border border-n-container"
@click="action.onClick" @click="action.onClick"
> >
{{ action.label }} {{ action.label }}
@@ -62,7 +62,7 @@ const hasQuotedMessage = computed(() => {
<EmailMeta <EmailMeta
class="p-3" class="p-3"
:class="{ :class="{
'border-b border-n-strong': isIncoming, 'border-b border-n-slate-6': isIncoming,
'border-b border-n-slate-8/20': isOutgoing, 'border-b border-n-slate-8/20': isOutgoing,
}" }"
/> />
@@ -75,7 +75,7 @@ const hasQuotedMessage = computed(() => {
> >
<div <div
v-if="isExpandable && !isExpanded" v-if="isExpandable && !isExpanded"
class="absolute left-0 right-0 bottom-0 h-40 px-8 flex items-end bg-gradient-to-t from-n-gray-3 via-n-gray-3 via-20% to-transparent" class="absolute left-0 right-0 bottom-0 h-40 px-8 flex items-end bg-gradient-to-t from-n-slate-4 via-n-strong/50 to-transparent dark:from-n-slate-5 dark:via-n-slate-4/50"
> >
<button <button
class="text-n-slate-12 py-2 px-8 mx-auto text-center flex items-center gap-2" class="text-n-slate-12 py-2 px-8 mx-auto text-center flex items-center gap-2"
@@ -139,7 +139,10 @@ const hasQuotedMessage = computed(() => {
v-if="Array.isArray(attachments) && attachments.length" v-if="Array.isArray(attachments) && attachments.length"
class="px-4 pb-4 space-y-2" class="px-4 pb-4 space-y-2"
> >
<AttachmentChips :attachments="attachments" class="gap-1" /> <AttachmentChips
:attachments="attachments"
class="flex flex-col gap-2.5"
/>
</section> </section>
</BaseBubble> </BaseBubble>
</template> </template>
@@ -20,12 +20,15 @@ const isEmpty = computed(() => {
<template> <template>
<BaseBubble class="px-4 py-3" data-bubble-name="text"> <BaseBubble class="px-4 py-3" data-bubble-name="text">
<div class="gap-3 flex flex-col"> <div class="gap-4 flex flex-col">
<span v-if="isEmpty" class="text-n-slate-11"> <span v-if="isEmpty" class="text-n-slate-11">
{{ $t('CONVERSATION.NO_CONTENT') }} {{ $t('CONVERSATION.NO_CONTENT') }}
</span> </span>
<FormattedContent v-if="content" :content="content" /> <FormattedContent v-if="content" :content="content" />
<AttachmentChips :attachments="attachments" class="gap-2" /> <AttachmentChips
:attachments="attachments"
class="flex flex-col gap-2.5"
/>
<template v-if="isTemplate"> <template v-if="isTemplate">
<div <div
v-if="contentAttributes.submittedEmail" v-if="contentAttributes.submittedEmail"
@@ -1,8 +1,7 @@
<script setup> <script setup>
import { computed, defineOptions, useAttrs } from 'vue'; import { computed, defineOptions, useAttrs } from 'vue';
import ImageChip from 'next/message/chips/Image.vue'; import ImageVideoChip from 'next/message/chips/AttachmentGrid.vue'; // Image and Video Grids are the same component
import VideoChip from 'next/message/chips/Video.vue';
import AudioChip from 'next/message/chips/Audio.vue'; import AudioChip from 'next/message/chips/Audio.vue';
import FileChip from 'next/message/chips/File.vue'; import FileChip from 'next/message/chips/File.vue';
import { useMessageContext } from '../provider.js'; import { useMessageContext } from '../provider.js';
@@ -37,7 +36,7 @@ const attrs = useAttrs();
const { orientation } = useMessageContext(); const { orientation } = useMessageContext();
const classToApply = computed(() => { const classToApply = computed(() => {
const baseClasses = [attrs.class, 'flex', 'flex-wrap']; const baseClasses = [attrs.class, 'flex', 'flex-wrap', 'gap-2'];
if (orientation.value === 'right') { if (orientation.value === 'right') {
baseClasses.push('justify-end'); baseClasses.push('justify-end');
@@ -77,30 +76,25 @@ const files = computed(() => {
<template> <template>
<div v-if="mediaAttachments.length" :class="classToApply"> <div v-if="mediaAttachments.length" :class="classToApply">
<template v-for="attachment in mediaAttachments" :key="attachment.id"> <ImageVideoChip :attachments="mediaAttachments" />
<ImageChip
v-if="attachment.fileType === ATTACHMENT_TYPES.IMAGE"
:attachment="attachment"
/>
<VideoChip
v-else-if="attachment.fileType === ATTACHMENT_TYPES.VIDEO"
:attachment="attachment"
/>
</template>
</div> </div>
<div v-if="recordings.length" :class="classToApply"> <div v-if="recordings.length" :class="classToApply">
<div v-for="attachment in recordings" :key="attachment.id"> <div v-for="attachment in recordings" :key="attachment.id" class="w-full">
<AudioChip <AudioChip
class="bg-n-alpha-3 dark:bg-n-alpha-2 text-n-slate-12" class="bg-n-alpha-3 dark:bg-n-alpha-2 text-n-slate-12"
:attachment="attachment" :attachment="attachment"
/> />
</div> </div>
</div> </div>
<div v-if="files.length" :class="classToApply"> <div v-if="files.length" :class="classToApply">
<div className="grid grid-cols-1 2xl:grid-cols-2 gap-3">
<FileChip <FileChip
v-for="attachment in files" v-for="attachment in files"
:key="attachment.id" :key="attachment.id"
:attachment="attachment" :attachment="attachment"
/> />
</div> </div>
</div>
</template> </template>
@@ -0,0 +1,82 @@
<script setup>
import { computed } from 'vue';
import ImageChip from './Image.vue';
import VideoChip from './Video.vue';
import { ATTACHMENT_TYPES } from '../constants';
const props = defineProps({
attachments: {
type: Array,
required: true,
validator: value => Array.isArray(value) && value.length > 0,
},
});
const MAX_DISPLAYED = 5;
const visibleAttachments = computed(() =>
props.attachments.slice(0, MAX_DISPLAYED)
);
const remainingCount = computed(() =>
Math.max(0, props.attachments.length - MAX_DISPLAYED)
);
const gridClass = computed(() => {
const count = props.attachments.length;
const base = 'grid gap-2 w-full';
if (count === 1) return `${base} grid-cols-1`;
const classes = {
2: `${base} grid-cols-2 max-h-[500px]`,
3: `${base} grid-cols-2 max-h-[500px]`,
4: `${base} grid-cols-2 max-h-[500px]`,
5: `${base} grid-cols-3 max-h-[500px]`,
};
return classes[count] || classes[5];
});
const itemClass = computed(() => index => {
const count = props.attachments.length;
const base = 'relative overflow-hidden rounded-lg';
if (count === 1) return `${base} w-full h-auto`;
if (count === 3 && index === 0) return `${base} row-span-2 h-[492px]`;
if (count >= 5 && index === 0) return `${base} col-span-2 h-[242px]`;
return count === 2 ? `${base} h-[500px]` : `${base} h-[242px]`;
});
const shouldShowOverlay = computed(
() => index => remainingCount.value > 0 && index === MAX_DISPLAYED - 1
);
const componentMap = {
[ATTACHMENT_TYPES.IMAGE]: ImageChip,
[ATTACHMENT_TYPES.VIDEO]: VideoChip,
};
const getComponent = fileType =>
componentMap[fileType?.toLowerCase()] || componentMap[ATTACHMENT_TYPES.IMAGE];
</script>
<template>
<div :class="gridClass">
<div
v-for="(attachment, index) in visibleAttachments"
:key="attachment.id"
:class="itemClass(index)"
>
<component
:is="getComponent(attachment.fileType)"
:attachment="attachment"
:remaining-count="remainingCount"
:should-show-overlay="shouldShowOverlay(index)"
/>
</div>
</div>
</template>
@@ -11,7 +11,16 @@ defineProps({
type: Object, type: Object,
required: true, required: true,
}, },
remainingCount: {
type: Number,
required: true,
},
shouldShowOverlay: {
type: Boolean,
required: true,
},
}); });
const hasError = ref(false); const hasError = ref(false);
const showGallery = ref(false); const showGallery = ref(false);
@@ -20,12 +29,20 @@ const { filteredCurrentChatAttachments } = useMessageContext();
const handleError = () => { const handleError = () => {
hasError.value = true; hasError.value = true;
}; };
const handleGalleryClick = () => {
showGallery.value = true;
};
const handleGalleryClose = () => {
showGallery.value = false;
};
</script> </script>
<template> <template>
<div <div
class="size-[72px] overflow-hidden contain-content rounded-xl cursor-pointer" class="rounded-lg overflow-hidden contain-content cursor-pointer size-full"
@click="showGallery = true" @click="handleGalleryClick"
> >
<div <div
v-if="hasError" v-if="hasError"
@@ -34,6 +51,7 @@ const handleError = () => {
<Icon icon="i-lucide-circle-off" class="text-n-slate-11" /> <Icon icon="i-lucide-circle-off" class="text-n-slate-11" />
{{ $t('COMPONENTS.MEDIA.LOADING_FAILED') }} {{ $t('COMPONENTS.MEDIA.LOADING_FAILED') }}
</div> </div>
<img <img
v-else v-else
class="object-cover w-full h-full" class="object-cover w-full h-full"
@@ -41,12 +59,23 @@ const handleError = () => {
@error="handleError" @error="handleError"
/> />
</div> </div>
<div
v-if="shouldShowOverlay"
class="absolute inset-0 flex items-center cursor-pointer justify-center bg-n-black/25 dark:bg-n-alpha-1 rounded-lg"
@click="handleGalleryClick"
>
<span class="text-white text-2xl font-semibold">
+{{ remainingCount }}
</span>
</div>
<GalleryView <GalleryView
v-if="showGallery" v-if="showGallery"
v-model:show="showGallery" v-model:show="showGallery"
:attachment="useSnakeCase(attachment)" :attachment="useSnakeCase(attachment)"
:all-attachments="filteredCurrentChatAttachments" :all-attachments="filteredCurrentChatAttachments"
@error="handleError" @error="handleError"
@close="() => (showGallery = false)" @close="handleGalleryClose"
/> />
</template> </template>
@@ -10,17 +10,33 @@ defineProps({
type: Object, type: Object,
required: true, required: true,
}, },
remainingCount: {
type: Number,
required: true,
},
shouldShowOverlay: {
type: Boolean,
required: true,
},
}); });
const showGallery = ref(false); const showGallery = ref(false);
const { filteredCurrentChatAttachments } = useMessageContext(); const { filteredCurrentChatAttachments } = useMessageContext();
const handleGalleryClick = () => {
showGallery.value = true;
};
const handleGalleryClose = () => {
showGallery.value = false;
};
</script> </script>
<template> <template>
<div <div
class="size-[72px] overflow-hidden contain-content rounded-xl cursor-pointer relative group" class="rounded-lg overflow-hidden contain-content cursor-pointer size-full"
@click="showGallery = true" @click="handleGalleryClick"
> >
<video <video
:src="attachment.dataUrl" :src="attachment.dataUrl"
@@ -29,6 +45,7 @@ const { filteredCurrentChatAttachments } = useMessageContext();
playsInline playsInline
/> />
<div <div
v-if="!shouldShowOverlay"
class="absolute w-full h-full inset-0 p-1 flex items-center justify-center" class="absolute w-full h-full inset-0 p-1 flex items-center justify-center"
> >
<div <div
@@ -41,12 +58,21 @@ const { filteredCurrentChatAttachments } = useMessageContext();
</div> </div>
</div> </div>
</div> </div>
<div
v-if="shouldShowOverlay"
class="absolute inset-0 flex items-center cursor-pointer justify-center bg-n-black/25 dark:bg-n-alpha-1 rounded-lg"
@click="handleGalleryClick"
>
<span class="text-white text-2xl font-semibold">
+{{ remainingCount }}
</span>
</div>
<GalleryView <GalleryView
v-if="showGallery" v-if="showGallery"
v-model:show="showGallery" v-model:show="showGallery"
:attachment="useSnakeCase(attachment)" :attachment="useSnakeCase(attachment)"
:all-attachments="filteredCurrentChatAttachments" :all-attachments="filteredCurrentChatAttachments"
@error="onError" @close="handleGalleryClose"
@close="() => (showGallery = false)"
/> />
</template> </template>
@@ -98,7 +98,7 @@ const MessageControl = Symbol('MessageControl');
* @property {import('vue').Ref<Sender|null>} [sender=null] - The sender information * @property {import('vue').Ref<Sender|null>} [sender=null] - The sender information
* @property {import('vue').ComputedRef<MessageOrientation>} orientation - The visual variant of the message * @property {import('vue').ComputedRef<MessageOrientation>} orientation - The visual variant of the message
* @property {import('vue').ComputedRef<MessageVariant>} variant - The visual variant of the message * @property {import('vue').ComputedRef<MessageVariant>} variant - The visual variant of the message
* @property {import('vue').ComputedRef<boolean>} isMyMessage - Does the message belong to the current user * @property {import('vue').ComputedRef<boolean>} isBotOrAgentMessage - Does the message belong to the bot or agents
* @property {import('vue').ComputedRef<boolean>} isPrivate - Proxy computed value for private * @property {import('vue').ComputedRef<boolean>} isPrivate - Proxy computed value for private
* @property {import('vue').ComputedRef<boolean>} shouldGroupWithNext - Should group with the next message or not, it is differnt from groupWithNext, this has a bypass for a failed message * @property {import('vue').ComputedRef<boolean>} shouldGroupWithNext - Should group with the next message or not, it is differnt from groupWithNext, this has a bypass for a failed message
*/ */
@@ -560,7 +560,7 @@
"ERROR_MESSAGE": "Wir konnten die Suche nicht abschließen. Bitte versuch es erneut." "ERROR_MESSAGE": "Wir konnten die Suche nicht abschließen. Bitte versuch es erneut."
}, },
"FORM": { "FORM": {
"GO_TO_CONVERSATION": "Aussicht", "GO_TO_CONVERSATION": "Anzeigen",
"SUCCESS_MESSAGE": "Die Nachricht wurde erfolgreich versendet!", "SUCCESS_MESSAGE": "Die Nachricht wurde erfolgreich versendet!",
"ERROR_MESSAGE": "Beim Erstellen der Unterhaltung ist ein Fehler aufgetreten. Bitte versuchen Sie es später erneut.", "ERROR_MESSAGE": "Beim Erstellen der Unterhaltung ist ein Fehler aufgetreten. Bitte versuchen Sie es später erneut.",
"NO_INBOX_ALERT": "Es sind keine Posteingänge vorhanden, um eine Unterhaltung mit diesem Kontakt zu starten.", "NO_INBOX_ALERT": "Es sind keine Posteingänge vorhanden, um eine Unterhaltung mit diesem Kontakt zu starten.",
@@ -751,7 +751,7 @@
"WHATSAPP": "WhatsApp", "WHATSAPP": "WhatsApp",
"SMS": "SMS", "SMS": "SMS",
"EMAIL": "E-Mail", "EMAIL": "E-Mail",
"TELEGRAM": "Telegram", "TELEGRAM": "Telegramm",
"LINE": "Line", "LINE": "Line",
"API": "API-Kanal" "API": "API-Kanal"
} }
@@ -12,7 +12,7 @@
}, },
"WEBHOOK": { "WEBHOOK": {
"SUBSCRIBED_EVENTS": "Abonnierte Events", "SUBSCRIBED_EVENTS": "Abonnierte Events",
"LEARN_MORE": "Learn more about webhooks", "LEARN_MORE": "Mehr über Webhooks erfahren",
"FORM": { "FORM": {
"CANCEL": "Stornieren", "CANCEL": "Stornieren",
"DESC": "Webhook-Ereignisse bieten Ihnen Echtzeitinformationen darüber, was in Ihrem Chatwoot-Konto passiert. Bitte geben Sie eine gültige URL ein, um einen Rückruf zu konfigurieren.", "DESC": "Webhook-Ereignisse bieten Ihnen Echtzeitinformationen darüber, was in Ihrem Chatwoot-Konto passiert. Bitte geben Sie eine gültige URL ein, um einen Rückruf zu konfigurieren.",
@@ -31,7 +31,7 @@
}, },
"END_POINT": { "END_POINT": {
"LABEL": "Webhook-URL", "LABEL": "Webhook-URL",
"PLACEHOLDER": "Example: {webhookExampleURL}", "PLACEHOLDER": "Beispiel: {webhookExampleURL}",
"ERROR": "Bitte geben Sie eine gültige URL ein" "ERROR": "Bitte geben Sie eine gültige URL ein"
}, },
"EDIT_SUBMIT": "Webhook aktualisieren", "EDIT_SUBMIT": "Webhook aktualisieren",
@@ -114,7 +114,7 @@
}, },
"OPEN_AI": { "OPEN_AI": {
"AI_ASSIST": "AI-Assistent", "AI_ASSIST": "AI-Assistent",
"WITH_AI": " {option} with AI ", "WITH_AI": " {option} mit KI ",
"OPTIONS": { "OPTIONS": {
"REPLY_SUGGESTION": "Antwortvorschlag", "REPLY_SUGGESTION": "Antwortvorschlag",
"SUMMARIZE": "Zusammenfassen", "SUMMARIZE": "Zusammenfassen",
@@ -235,7 +235,7 @@
"ERROR": "Beim Abrufen der linearen Probleme ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut", "ERROR": "Beim Abrufen der linearen Probleme ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut",
"LINK_SUCCESS": "Problem erfolgreich verknüpft", "LINK_SUCCESS": "Problem erfolgreich verknüpft",
"LINK_ERROR": "Beim Verknüpfen des Problems ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut", "LINK_ERROR": "Beim Verknüpfen des Problems ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut",
"LINK_TITLE": "Conversation (#{conversationId}) with {name}" "LINK_TITLE": "Unterhaltung (#{conversationId}) mit {name}"
}, },
"ADD_OR_LINK": { "ADD_OR_LINK": {
"TITLE": "Lineares Problem erstellen/verknüpfen", "TITLE": "Lineares Problem erstellen/verknüpfen",
@@ -294,7 +294,7 @@
"PRIORITY": "Priorität", "PRIORITY": "Priorität",
"ASSIGNEE": "Zugewiesener", "ASSIGNEE": "Zugewiesener",
"LABELS": "Labels", "LABELS": "Labels",
"CREATED_AT": "Created at {createdAt}" "CREATED_AT": "Erstellt am {createdAt}"
}, },
"UNLINK": { "UNLINK": {
"TITLE": "Verknüpfung aufheben", "TITLE": "Verknüpfung aufheben",
@@ -302,8 +302,8 @@
"ERROR": "Beim Aufheben der Verknüpfung des Problems ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut" "ERROR": "Beim Aufheben der Verknüpfung des Problems ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut"
}, },
"DELETE": { "DELETE": {
"TITLE": "Are you sure you want to delete the integration?", "TITLE": "Sind Sie sicher, dass Sie die Integration löschen möchten?",
"MESSAGE": "Are you sure you want to delete the integration?", "MESSAGE": "Sind Sie sicher, dass Sie die Integration löschen möchten?",
"CONFIRM": "Ja, löschen", "CONFIRM": "Ja, löschen",
"CANCEL": "Stornieren" "CANCEL": "Stornieren"
} }
@@ -313,27 +313,27 @@
"NAME": "Kapitän", "NAME": "Kapitän",
"COPILOT": { "COPILOT": {
"SEND_MESSAGE": "Nachricht senden...", "SEND_MESSAGE": "Nachricht senden...",
"LOADER": "Captain is thinking", "LOADER": "Captain denkt nach",
"YOU": "Sie", "YOU": "Sie",
"USE": "Use this", "USE": "Verwenden",
"RESET": "Reset", "RESET": "Zurücksetzen",
"SELECT_ASSISTANT": "Select Assistant" "SELECT_ASSISTANT": "Assistent auswählen"
}, },
"PAYWALL": { "PAYWALL": {
"TITLE": "Upgrade to use Captain AI", "TITLE": "Upgrade auf Captain AI",
"AVAILABLE_ON": "Captain is not available on the free plan.", "AVAILABLE_ON": "Captain is not available on the free plan.",
"UPGRADE_PROMPT": "Upgrade your plan to get access to our assistants, copilot and more.", "UPGRADE_PROMPT": "Tarif upgraden, um Zugang zu unseren Assistenten, Copilot und mehr zu erhalten.",
"UPGRADE_NOW": "Upgrade now", "UPGRADE_NOW": "Jetzt upgraden",
"CANCEL_ANYTIME": "You can change or cancel your plan anytime" "CANCEL_ANYTIME": "Sie können Ihr Paket jederzeit ändern oder kündigen"
}, },
"ENTERPRISE_PAYWALL": { "ENTERPRISE_PAYWALL": {
"AVAILABLE_ON": "Captain AI feature is only available in a paid plan.", "AVAILABLE_ON": "Captain AI Funktion ist nur mit einem kostenpflichtigen Tarif verfügbar.",
"UPGRADE_PROMPT": "Upgrade your plan to get access to our assistants, copilot and more.", "UPGRADE_PROMPT": "Tarif upgraden, um Zugang zu unseren Assistenten, Copilot und mehr zu erhalten.",
"ASK_ADMIN": "Please reach out to your administrator for the upgrade." "ASK_ADMIN": "Bitte kontaktieren Sie Ihren Administrator für das Upgrade."
}, },
"BANNER": { "BANNER": {
"RESPONSES": "You've used over 80% of your response limit. To continue using Captain AI, please upgrade.", "RESPONSES": "You've used over 80% of your response limit. To continue using Captain AI, please upgrade.",
"DOCUMENTS": "Document limit reached. Upgrade to continue using Captain AI." "DOCUMENTS": "Dokumentenlimit erreicht. Upgraden um Cpatain AI weiter zu verwenden."
}, },
"FORM": { "FORM": {
"CANCEL": "Stornieren", "CANCEL": "Stornieren",
@@ -100,7 +100,7 @@
"NO_RESULTS": "Nenhum resultado encontrado." "NO_RESULTS": "Nenhum resultado encontrado."
}, },
"MULTI_SELECTOR": { "MULTI_SELECTOR": {
"PLACEHOLDER": "Nenhuma", "PLACEHOLDER": "Nenhum",
"TITLE": { "TITLE": {
"AGENT": "Selecionar agente", "AGENT": "Selecionar agente",
"TEAM": "Selecionar time" "TEAM": "Selecionar time"
@@ -1,16 +1,16 @@
{ {
"AUDIT_LOGS": { "AUDIT_LOGS": {
"HEADER": "Registros de Auditoria", "HEADER": "Auditoria",
"HEADER_BTN_TXT": "Adicionar Registros de Auditoria", "HEADER_BTN_TXT": "Adicionar Logs de Auditoria",
"LOADING": "Buscando Logs de Auditoria", "LOADING": "Buscando Logs de Auditoria",
"DESCRIPTION": "Logs de Auditoria mantêm um registro de atividades em sua conta, permitindo que você acompanhe e auditoria de sua conta, equipe ou serviços.", "DESCRIPTION": "Logs de Auditoria mantêm um registro de atividades em sua conta, permitindo que você acompanhe e auditoria de sua conta, equipe ou serviços.",
"LEARN_MORE": "Saiba mais sobre os logs de auditoria", "LEARN_MORE": "Saiba mais sobre os logs de auditoria",
"SEARCH_404": "Não existem itens correspondentes a esta consulta", "SEARCH_404": "Não existem itens correspondentes a esta consulta",
"SIDEBAR_TXT": "<p><b>Registros de Auditoria</b> </p><p> Registros de Auditoria são trilhas para eventos e ações em um Sistema Chatwoot. </p>", "SIDEBAR_TXT": "<p><b>Logs de Auditoria</b> </p><p> Os Logs de Auditoria são rastros para eventos e ações em um Sistema Chatwoot. </p>",
"LIST": { "LIST": {
"404": "Não há Registros de Auditoria disponíveis nesta conta.", "404": "Não há Logs de Auditoria disponíveis nesta conta.",
"TITLE": "Gerenciar Registros de Auditoria", "TITLE": "Gerenciar Logs de Auditoria",
"DESC": "Registros de Auditoria são trilhas para eventos e ações em um Sistema Chatwoot.", "DESC": "Logs de auditoria são rastros para eventos e ações em um Sistema de Chatwoot.",
"TABLE_HEADER": { "TABLE_HEADER": {
"ACTIVITY": "Usuário", "ACTIVITY": "Usuário",
"TIME": "Ação", "TIME": "Ação",
@@ -27,12 +27,12 @@
"ERROR": "Falha ao atualizar etiquetas" "ERROR": "Falha ao atualizar etiquetas"
}, },
"CONVERSATION": { "CONVERSATION": {
"TITLE": "Marcador da conversa", "TITLE": "Etiquetas da conversa",
"ADD_BUTTON": "Adicionar marcador" "ADD_BUTTON": "Adicionar etiquetas"
}, },
"LABEL_SELECT": { "LABEL_SELECT": {
"TITLE": "Adicionar marcador", "TITLE": "Adicionar etiquetas",
"PLACEHOLDER": "Pesquisar marcador ", "PLACEHOLDER": "Pesquisar etiquetas",
"NO_RESULT": "Nenhuma etiqueta encontrada", "NO_RESULT": "Nenhuma etiqueta encontrada",
"CREATE_LABEL": "Criar etiqueta" "CREATE_LABEL": "Criar etiqueta"
} }
@@ -65,7 +65,7 @@
"HEADER": { "HEADER": {
"RESOLVE_ACTION": "Resolver", "RESOLVE_ACTION": "Resolver",
"REOPEN_ACTION": "Reabrir", "REOPEN_ACTION": "Reabrir",
"OPEN_ACTION": "Abertas", "OPEN_ACTION": "Abrir",
"OPEN": "Mais", "OPEN": "Mais",
"CLOSE": "Fechar", "CLOSE": "Fechar",
"DETAILS": "detalhes", "DETAILS": "detalhes",
@@ -148,7 +148,7 @@
"MESSAGE_SIGN_TOOLTIP": "Assinatura de mensagem", "MESSAGE_SIGN_TOOLTIP": "Assinatura de mensagem",
"ENABLE_SIGN_TOOLTIP": "Ativar assinatura", "ENABLE_SIGN_TOOLTIP": "Ativar assinatura",
"DISABLE_SIGN_TOOLTIP": "Desativar assinatura", "DISABLE_SIGN_TOOLTIP": "Desativar assinatura",
"MSG_INPUT": "Shift + enter para nova linha. Digite '/' para atalhos.", "MSG_INPUT": "Shift + enter para nova linha. Digite '/' para selecionar uma Resposta Pronta.",
"PRIVATE_MSG_INPUT": "A mensagem será visível apenas para agentes", "PRIVATE_MSG_INPUT": "A mensagem será visível apenas para agentes",
"MESSAGE_SIGNATURE_NOT_CONFIGURED": "A assinatura da mensagem não está configurada. Por favor, configure-a nas configurações do perfil.", "MESSAGE_SIGNATURE_NOT_CONFIGURED": "A assinatura da mensagem não está configurada. Por favor, configure-a nas configurações do perfil.",
"CLICK_HERE": "Clique aqui para atualizar", "CLICK_HERE": "Clique aqui para atualizar",
@@ -15,7 +15,7 @@
}, },
"ENTERPRISE_PAYWALL": { "ENTERPRISE_PAYWALL": {
"AVAILABLE_ON": "O recurso de função personalizada está disponível apenas nos planos pagos.", "AVAILABLE_ON": "O recurso de função personalizada está disponível apenas nos planos pagos.",
"UPGRADE_PROMPT": "Atualize para um plano pago para acessar recursos avançados como registros de auditoria, capacidade do agente e muito mais.", "UPGRADE_PROMPT": "Atualize para um plano pago para acessar recursos avançados como logs de auditoria, capacidade do agente e muito mais.",
"ASK_ADMIN": "Entre em contato com seu administrador para fazer a atualização." "ASK_ADMIN": "Entre em contato com seu administrador para fazer a atualização."
}, },
"LIST": { "LIST": {
@@ -1,6 +1,6 @@
{ {
"GENERAL_SETTINGS": { "GENERAL_SETTINGS": {
"TITLE": "Configurações da conta", "TITLE": "Conta",
"SUBMIT": "Atualizar configurações", "SUBMIT": "Atualizar configurações",
"BACK": "Anterior", "BACK": "Anterior",
"DISMISS": "Recusar", "DISMISS": "Recusar",
@@ -131,10 +131,10 @@
"GO_TO_INBOX_REPORTS": "Ir para Relatórios da Caixa de Entrada", "GO_TO_INBOX_REPORTS": "Ir para Relatórios da Caixa de Entrada",
"GO_TO_TEAM_REPORTS": "Ir para Relatórios da Equipe", "GO_TO_TEAM_REPORTS": "Ir para Relatórios da Equipe",
"GO_TO_SETTINGS_AGENTS": "Ir para Configurações de Agente", "GO_TO_SETTINGS_AGENTS": "Ir para Configurações de Agente",
"GO_TO_SETTINGS_TEAMS": "Ir para as configurações de equipe", "GO_TO_SETTINGS_TEAMS": "Ir para as Configurações de Equipe",
"GO_TO_SETTINGS_INBOXES": "Ir para as configurações da Caixa de Entrada", "GO_TO_SETTINGS_INBOXES": "Ir para as Configurações da Caixa de Entrada",
"GO_TO_SETTINGS_LABELS": "Ir para as Configurações de Etiqueta", "GO_TO_SETTINGS_LABELS": "Ir para as Configurações de Etiqueta",
"GO_TO_SETTINGS_CANNED_RESPONSES": "Ir para as configurações de respostas prontas", "GO_TO_SETTINGS_CANNED_RESPONSES": "Ir para as Configurações de Respostas Prontas",
"GO_TO_SETTINGS_APPLICATIONS": "Vá para Configurações do Aplicativo", "GO_TO_SETTINGS_APPLICATIONS": "Vá para Configurações do Aplicativo",
"GO_TO_SETTINGS_ACCOUNT": "Ir para as Configurações da Conta", "GO_TO_SETTINGS_ACCOUNT": "Ir para as Configurações da Conta",
"GO_TO_SETTINGS_PROFILE": "Ir para as Configurações do Perfil", "GO_TO_SETTINGS_PROFILE": "Ir para as Configurações do Perfil",
@@ -500,7 +500,7 @@
"ENABLE_CONTINUITY_VIA_EMAIL_SUB_TEXT": "As conversas continuarão sobre o e-mail se o endereço de e-mail de contato estiver disponível.", "ENABLE_CONTINUITY_VIA_EMAIL_SUB_TEXT": "As conversas continuarão sobre o e-mail se o endereço de e-mail de contato estiver disponível.",
"LOCK_TO_SINGLE_CONVERSATION": "Bloquear para conversa única", "LOCK_TO_SINGLE_CONVERSATION": "Bloquear para conversa única",
"LOCK_TO_SINGLE_CONVERSATION_SUB_TEXT": "Ativar ou desativar várias conversas para o mesmo contato nesta caixa de entrada", "LOCK_TO_SINGLE_CONVERSATION_SUB_TEXT": "Ativar ou desativar várias conversas para o mesmo contato nesta caixa de entrada",
"INBOX_UPDATE_TITLE": "Configurações da Caixa de entrada", "INBOX_UPDATE_TITLE": "Configurações da Caixa de Entrada",
"INBOX_UPDATE_SUB_TEXT": "Atualize suas configurações de caixa de entrada", "INBOX_UPDATE_SUB_TEXT": "Atualize suas configurações de caixa de entrada",
"AUTO_ASSIGNMENT_SUB_TEXT": "Ativar ou desativar a atribuição automática de novas conversas aos agentes adicionados a essa caixa de entrada.", "AUTO_ASSIGNMENT_SUB_TEXT": "Ativar ou desativar a atribuição automática de novas conversas aos agentes adicionados a essa caixa de entrada.",
"HMAC_VERIFICATION": "Validação de Identidade do Usuário", "HMAC_VERIFICATION": "Validação de Identidade do Usuário",
@@ -695,7 +695,7 @@
"PLACE_HOLDER": "Fale conosco no chat" "PLACE_HOLDER": "Fale conosco no chat"
}, },
"UPDATE": { "UPDATE": {
"BUTTON_TEXT": "Atualizar configurações do widget", "BUTTON_TEXT": "Atualizar Configurações do Widget",
"API": { "API": {
"SUCCESS_MESSAGE": "Configurações do widget atualizadas com sucesso", "SUCCESS_MESSAGE": "Configurações do widget atualizadas com sucesso",
"ERROR_MESSAGE": "Não é possível atualizar as configurações do widget" "ERROR_MESSAGE": "Não é possível atualizar as configurações do widget"
@@ -302,8 +302,8 @@
"ERROR": "Houve um erro ao desvincular o atributo, por favor, tente novamente" "ERROR": "Houve um erro ao desvincular o atributo, por favor, tente novamente"
}, },
"DELETE": { "DELETE": {
"TITLE": "Are you sure you want to delete the integration?", "TITLE": "Tem certeza que deseja excluir esta integração?",
"MESSAGE": "Are you sure you want to delete the integration?", "MESSAGE": "Tem certeza que deseja excluir esta integração?",
"CONFIRM": "Sim, excluir", "CONFIRM": "Sim, excluir",
"CANCEL": "Cancelar" "CANCEL": "Cancelar"
} }
@@ -317,7 +317,7 @@
"YOU": "Você", "YOU": "Você",
"USE": "Use isto", "USE": "Use isto",
"RESET": "Reiniciar", "RESET": "Reiniciar",
"SELECT_ASSISTANT": "Select Assistant" "SELECT_ASSISTANT": "Selecione o Assistente"
}, },
"PAYWALL": { "PAYWALL": {
"TITLE": "Atualize para usar o Capitão IA", "TITLE": "Atualize para usar o Capitão IA",
@@ -440,19 +440,19 @@
"DOCUMENTABLE": { "DOCUMENTABLE": {
"CONVERSATION": "Conversação #{id}" "CONVERSATION": "Conversação #{id}"
}, },
"SELECTED": "{count} selected", "SELECTED": "{count} selecionado",
"BULK_APPROVE_BUTTON": "Approve", "BULK_APPROVE_BUTTON": "Aprovar",
"BULK_DELETE_BUTTON": "Excluir", "BULK_DELETE_BUTTON": "Excluir",
"BULK_APPROVE": { "BULK_APPROVE": {
"SUCCESS_MESSAGE": "FAQs approved successfully", "SUCCESS_MESSAGE": "Perguntas Frequentes aprovadas com sucesso",
"ERROR_MESSAGE": "There was an error approving the FAQs, please try again." "ERROR_MESSAGE": "Ocorreu um erro ao aprovar as Perguntas Frequentes. Tente novamente."
}, },
"BULK_DELETE": { "BULK_DELETE": {
"TITLE": "Delete FAQs?", "TITLE": "Excluir as Perguntas Frequentes?",
"DESCRIPTION": "Are you sure you want to delete the selected FAQs? This action cannot be undone.", "DESCRIPTION": "Tem certeza que deseja excluir as Perguntas Frequentes selecionadas? Esta ação não pode ser desfeita.",
"CONFIRM": "Yes, delete all", "CONFIRM": "Sim, excluir todas",
"SUCCESS_MESSAGE": "FAQs deleted successfully", "SUCCESS_MESSAGE": "Perguntas Frequentes excluídas com sucesso/",
"ERROR_MESSAGE": "There was an error deleting the FAQs, please try again." "ERROR_MESSAGE": "Ocorreu um erro ao excluir as Perguntas Frequentes, por favor tente novamente."
}, },
"DELETE": { "DELETE": {
"TITLE": "Tem certeza que deseja excluir o FAQ?", "TITLE": "Tem certeza que deseja excluir o FAQ?",
@@ -1,7 +1,7 @@
{ {
"LABEL_MGMT": { "LABEL_MGMT": {
"HEADER": "Etiquetas", "HEADER": "Etiquetas",
"HEADER_BTN_TXT": "Adicionar marcador", "HEADER_BTN_TXT": "Adicionar etiqueta",
"LOADING": "Buscando etiquetas", "LOADING": "Buscando etiquetas",
"DESCRIPTION": "As etiquetas ajudam você a categorizar e priorizar conversas e leads. Você pode atribuir uma etiqueta a uma conversa ou contato usando o painel lateral.", "DESCRIPTION": "As etiquetas ajudam você a categorizar e priorizar conversas e leads. Você pode atribuir uma etiqueta a uma conversa ou contato usando o painel lateral.",
"LEARN_MORE": "Aprenda mais sobre etiquetas", "LEARN_MORE": "Aprenda mais sobre etiquetas",
@@ -18,21 +18,21 @@
}, },
"FORM": { "FORM": {
"NAME": { "NAME": {
"LABEL": "Nome do marcador", "LABEL": "Nome da Etiqueta",
"PLACEHOLDER": "Nome do marcador", "PLACEHOLDER": "Nome da etiqueta",
"REQUIRED_ERROR": "O nome do marcador é obrigatório", "REQUIRED_ERROR": "O nome da etiqueta é obrigatório",
"MINIMUM_LENGTH_ERROR": "Tamanho mínimo 2 é necessário", "MINIMUM_LENGTH_ERROR": "Tamanho mínimo 2 é necessário",
"VALID_ERROR": "Somente Letras, Números, Hífen e Sublinhado são permitidos" "VALID_ERROR": "Somente Letras, Números, Hífen e Sublinhado são permitidos"
}, },
"DESCRIPTION": { "DESCRIPTION": {
"LABEL": "Descrição", "LABEL": "Descrição",
"PLACEHOLDER": "Descrição do marcador" "PLACEHOLDER": "Descrição da etiqueta"
}, },
"COLOR": { "COLOR": {
"LABEL": "Cor" "LABEL": "Cor"
}, },
"SHOW_ON_SIDEBAR": { "SHOW_ON_SIDEBAR": {
"LABEL": "Exibir marcador na barra lateral" "LABEL": "Exibir etiqueta na barra lateral"
}, },
"EDIT": "Alterar", "EDIT": "Alterar",
"CREATE": "Criar", "CREATE": "Criar",
@@ -54,10 +54,10 @@
"SUGGESTED_LABELS": "Etiquetas sugeridos" "SUGGESTED_LABELS": "Etiquetas sugeridos"
}, },
"ADD": { "ADD": {
"TITLE": "Adicionar marcador", "TITLE": "Adicionar etiqueta",
"DESC": "Etiquetas permitem agrupar as conversas.", "DESC": "Etiquetas permitem agrupar as conversas.",
"API": { "API": {
"SUCCESS_MESSAGE": "Marcador adicionado com sucesso", "SUCCESS_MESSAGE": "Etiqueta adicionada com sucesso",
"ERROR_MESSAGE": "Ocorreu um erro, por favor tente novamente" "ERROR_MESSAGE": "Ocorreu um erro, por favor tente novamente"
} }
}, },
@@ -71,7 +71,7 @@
"DELETE": { "DELETE": {
"BUTTON_TEXT": "Excluir", "BUTTON_TEXT": "Excluir",
"API": { "API": {
"SUCCESS_MESSAGE": "Marcador excluído com sucesso", "SUCCESS_MESSAGE": "Etiqueta excluída com sucesso",
"ERROR_MESSAGE": "Ocorreu um erro, por favor tente novamente" "ERROR_MESSAGE": "Ocorreu um erro, por favor tente novamente"
}, },
"CONFIRM": { "CONFIRM": {
@@ -471,7 +471,7 @@
"NO_AGENTS": "Não há conversas por agentes", "NO_AGENTS": "Não há conversas por agentes",
"TABLE_HEADER": { "TABLE_HEADER": {
"AGENT": "Agente", "AGENT": "Agente",
"OPEN": "Abertas", "OPEN": "Abrir",
"UNATTENDED": "Não atendidas", "UNATTENDED": "Não atendidas",
"STATUS": "Situação" "STATUS": "Situação"
} }
@@ -509,7 +509,7 @@
"SLA": "Nome do SLA", "SLA": "Nome do SLA",
"AGENTS": "Nome do Agente", "AGENTS": "Nome do Agente",
"INBOXES": "Nome da Caixa de Entrada", "INBOXES": "Nome da Caixa de Entrada",
"LABELS": "Nome do marcador", "LABELS": "Nome da etiqueta",
"TEAMS": "Nome da equipe" "TEAMS": "Nome da equipe"
}, },
"SLA": "Política SLA", "SLA": "Política SLA",
@@ -26,7 +26,7 @@
"UPDATE_SUCCESS": "Suas configurações foram atualizadas com sucesso", "UPDATE_SUCCESS": "Suas configurações foram atualizadas com sucesso",
"CARD": { "CARD": {
"ENTER_KEY": { "ENTER_KEY": {
"HEADING": "Inserir (↵)", "HEADING": "Enter (↵)",
"CONTENT": "Enviar mensagens pressionando a tecla Enter em vez de clicar no botão de enviar." "CONTENT": "Enviar mensagens pressionando a tecla Enter em vez de clicar no botão de enviar."
}, },
"CMD_ENTER_KEY": { "CMD_ENTER_KEY": {
@@ -37,19 +37,19 @@
}, },
"INTERFACE_SECTION": { "INTERFACE_SECTION": {
"TITLE": "Interface", "TITLE": "Interface",
"NOTE": "Customize the look and feel of your Chatwoot dashboard.", "NOTE": "Personalize a aparência do seu painel do Chatwoot.",
"FONT_SIZE": { "FONT_SIZE": {
"TITLE": "Font size", "TITLE": "Tamanho da fonte",
"NOTE": "Adjust the text size across the dashboard based on your preference.", "NOTE": "Ajuste o tamanho do texto do painel com base na sua preferência.",
"UPDATE_SUCCESS": "Your font settings have been updated successfully", "UPDATE_SUCCESS": "As configurações de sua fonte foram atualizadas com sucesso",
"UPDATE_ERROR": "There is an error while updating the font settings, please try again", "UPDATE_ERROR": "Ocorreu um erro ao atualizar as configurações de fonte, por favor, tente novamente",
"OPTIONS": { "OPTIONS": {
"SMALLER": "Smaller", "SMALLER": "Menor",
"SMALL": "Small", "SMALL": "Pequeno",
"DEFAULT": "Padrão", "DEFAULT": "Padrão",
"LARGE": "Large", "LARGE": "Grande",
"LARGER": "Larger", "LARGER": "Maior",
"EXTRA_LARGE": "Extra Large" "EXTRA_LARGE": "Muito Grande"
} }
} }
}, },
@@ -287,13 +287,13 @@
"HOME": "Principal", "HOME": "Principal",
"AGENTS": "Agentes", "AGENTS": "Agentes",
"AGENT_BOTS": "Robôs", "AGENT_BOTS": "Robôs",
"AUDIT_LOGS": "Registros de Auditoria", "AUDIT_LOGS": "Auditoria",
"INBOXES": "Caixas de Entrada", "INBOXES": "Caixas de Entrada",
"NOTIFICATIONS": "Notificações", "NOTIFICATIONS": "Notificações",
"CANNED_RESPONSES": "Respostas Prontas", "CANNED_RESPONSES": "Respostas Prontas",
"INTEGRATIONS": "Integrações", "INTEGRATIONS": "Integrações",
"PROFILE_SETTINGS": "Configurações do Perfil", "PROFILE_SETTINGS": "Configurações do Perfil",
"ACCOUNT_SETTINGS": "Configurações da conta", "ACCOUNT_SETTINGS": "Conta",
"APPLICATIONS": "Aplicações", "APPLICATIONS": "Aplicações",
"LABELS": "Etiquetas", "LABELS": "Etiquetas",
"CUSTOM_ATTRIBUTES": "Atributos Personalizados", "CUSTOM_ATTRIBUTES": "Atributos Personalizados",
@@ -322,7 +322,7 @@
"REPORTS_INBOX": "Caixa de Entrada", "REPORTS_INBOX": "Caixa de Entrada",
"REPORTS_TEAM": "Equipe", "REPORTS_TEAM": "Equipe",
"SET_AVAILABILITY_TITLE": "Defina como", "SET_AVAILABILITY_TITLE": "Defina como",
"SET_YOUR_AVAILABILITY": "Definir a sua disponibilidade", "SET_YOUR_AVAILABILITY": "Disponibilidade",
"SLA": "SLA", "SLA": "SLA",
"CUSTOM_ROLES": "Regras Personalizadas", "CUSTOM_ROLES": "Regras Personalizadas",
"BETA": "Beta", "BETA": "Beta",
@@ -1,9 +1,9 @@
{ {
"SLA": { "SLA": {
"HEADER": "Termos do Serviço", "HEADER": "Acordo de Nível de Serviço",
"ADD_ACTION": "Adicionar SLA", "ADD_ACTION": "Adicionar SLA",
"ADD_ACTION_LONG": "Criar uma nova Política de SLA", "ADD_ACTION_LONG": "Criar uma nova Política de SLA",
"DESCRIPTION": "Contratos de Nível de Serviço (SLAs em inglês) são contratos que definem expectativas claras entre sua equipe e os clientes. Estabelecem normas para tempos de resposta e de resolução, criando um quadro de responsabilização e garantindo uma experiência coerente e de qualidade.", "DESCRIPTION": "Acordo de Nível de Serviço (SLAs em inglês) são acordos que definem expectativas claras entre sua equipe e os clientes. Estabelecem normas para tempos de resposta e de resolução, criando um quadro de responsabilização e garantindo uma experiência coerente e de qualidade.",
"LEARN_MORE": "Saiba mais sobre SLA", "LEARN_MORE": "Saiba mais sobre SLA",
"LOADING": "Buscando SLAs", "LOADING": "Buscando SLAs",
"PAYWALL": { "PAYWALL": {
@@ -15,7 +15,7 @@
}, },
"ENTERPRISE_PAYWALL": { "ENTERPRISE_PAYWALL": {
"AVAILABLE_ON": "O recurso SLA está disponível apenas nos planos pagos.", "AVAILABLE_ON": "O recurso SLA está disponível apenas nos planos pagos.",
"UPGRADE_PROMPT": "Atualize para um plano pago para acessar recursos avançados como registros de auditoria, capacidade do agente e muito mais.", "UPGRADE_PROMPT": "Atualize para um plano pago para acessar recursos avançados como logs de auditoria, capacidade do agente e muito mais.",
"ASK_ADMIN": "Entre em contato com seu administrador para fazer a atualização." "ASK_ADMIN": "Entre em contato com seu administrador para fazer a atualização."
}, },
"LIST": { "LIST": {
@@ -266,7 +266,7 @@
"ATTRIBUTE_WARNING": "Контактная информация <strong>{primaryContactName}</strong> будет скопирована в <strong>{parentContactName}</strong>." "ATTRIBUTE_WARNING": "Контактная информация <strong>{primaryContactName}</strong> будет скопирована в <strong>{parentContactName}</strong>."
}, },
"SEARCH": { "SEARCH": {
"ERROR_MESSAGE": "Something went wrong. Please try again later." "ERROR_MESSAGE": "Что-то пошло не так. Пожалуйста, попробуйте позже."
}, },
"FORM": { "FORM": {
"SUBMIT": " Объединить контакты", "SUBMIT": " Объединить контакты",
@@ -563,7 +563,7 @@
"GO_TO_CONVERSATION": "Просмотр", "GO_TO_CONVERSATION": "Просмотр",
"SUCCESS_MESSAGE": "Сообщение успешно отправлено!", "SUCCESS_MESSAGE": "Сообщение успешно отправлено!",
"ERROR_MESSAGE": "Произошла ошибка при создании диалога. Пожалуйста, повторите попытку позже.", "ERROR_MESSAGE": "Произошла ошибка при создании диалога. Пожалуйста, повторите попытку позже.",
"NO_INBOX_ALERT": "Нет доступных \"входящих\" для начала разговора с этим контактом.", "NO_INBOX_ALERT": "Нет доступных входящих для начала разговора с этим контактом.",
"CONTACT_SELECTOR": { "CONTACT_SELECTOR": {
"LABEL": "Кому:", "LABEL": "Кому:",
"TAG_INPUT_PLACEHOLDER": "Поиск контакта с именем, email или номером телефона", "TAG_INPUT_PLACEHOLDER": "Поиск контакта с именем, email или номером телефона",
@@ -31,7 +31,7 @@
}, },
"PERMISSIONS": { "PERMISSIONS": {
"CONVERSATION_MANAGE": "Управление всеми диалогами", "CONVERSATION_MANAGE": "Управление всеми диалогами",
"CONVERSATION_UNASSIGNED_MANAGE": "Управление неназначенными беседами и теми, которые назначены им", "CONVERSATION_UNASSIGNED_MANAGE": "Управление неназначенными беседами и теми, которые им назначены",
"CONVERSATION_PARTICIPATING_MANAGE": "Управление текущими беседами и теми, которые им назначены", "CONVERSATION_PARTICIPATING_MANAGE": "Управление текущими беседами и теми, которые им назначены",
"CONTACT_MANAGE": "Управление контактами", "CONTACT_MANAGE": "Управление контактами",
"REPORT_MANAGE": "Управление отчетами", "REPORT_MANAGE": "Управление отчетами",
@@ -114,7 +114,7 @@
"CHANGE_PRIORITY": "Изменить приоритет", "CHANGE_PRIORITY": "Изменить приоритет",
"CHANGE_TEAM": "Изменить команду", "CHANGE_TEAM": "Изменить команду",
"SNOOZE_CONVERSATION": "Включить звук диалога", "SNOOZE_CONVERSATION": "Включить звук диалога",
"ADD_LABEL": "Добавить метку в разговор", "ADD_LABEL": "Добавить метку в диалог",
"REMOVE_LABEL": "Удалить метку из диалога", "REMOVE_LABEL": "Удалить метку из диалога",
"SETTINGS": "Настройки", "SETTINGS": "Настройки",
"AI_ASSIST": "Помощь ИИ", "AI_ASSIST": "Помощь ИИ",
@@ -128,11 +128,11 @@
"GO_TO_CONVERSATION_REPORTS": "Перейти к отчетам по диалогам", "GO_TO_CONVERSATION_REPORTS": "Перейти к отчетам по диалогам",
"GO_TO_AGENT_REPORTS": "Перейти к отчетам по сотрудникам", "GO_TO_AGENT_REPORTS": "Перейти к отчетам по сотрудникам",
"GO_TO_LABEL_REPORTS": "Перейти к отчетам по меткам", "GO_TO_LABEL_REPORTS": "Перейти к отчетам по меткам",
"GO_TO_INBOX_REPORTS": "Перейти к отчётам по \"Входящим\"", "GO_TO_INBOX_REPORTS": "Перейти к отчётам по «Входящим»",
"GO_TO_TEAM_REPORTS": "Перейти к отчетам по командам", "GO_TO_TEAM_REPORTS": "Перейти к отчетам по командам",
"GO_TO_SETTINGS_AGENTS": "Перейти к настройкам сотрудников", "GO_TO_SETTINGS_AGENTS": "Перейти к настройкам сотрудников",
"GO_TO_SETTINGS_TEAMS": "Перейти к настройкам команд", "GO_TO_SETTINGS_TEAMS": "Перейти к настройкам команд",
"GO_TO_SETTINGS_INBOXES": "Перейти к настройкам \"Входящих\"", "GO_TO_SETTINGS_INBOXES": "Перейти к настройкам «Входящих»",
"GO_TO_SETTINGS_LABELS": "Перейти к настройкам меток", "GO_TO_SETTINGS_LABELS": "Перейти к настройкам меток",
"GO_TO_SETTINGS_CANNED_RESPONSES": "Перейти к настройкам шаблонных ответов", "GO_TO_SETTINGS_CANNED_RESPONSES": "Перейти к настройкам шаблонных ответов",
"GO_TO_SETTINGS_APPLICATIONS": "Перейти в настройки приложения", "GO_TO_SETTINGS_APPLICATIONS": "Перейти в настройки приложения",
@@ -2,7 +2,7 @@
"INBOX_MGMT": { "INBOX_MGMT": {
"HEADER": "Источники", "HEADER": "Источники",
"DESCRIPTION": "Канал — это способ коммуникации, который ваш клиент выбирает для связи с вами. Входящие — это место, где вы управляете диалогами для определенного канала. Он может включать диалоги из различных источников, таких как электронная почта, онлайн чат или социальные сети.", "DESCRIPTION": "Канал — это способ коммуникации, который ваш клиент выбирает для связи с вами. Входящие — это место, где вы управляете диалогами для определенного канала. Он может включать диалоги из различных источников, таких как электронная почта, онлайн чат или социальные сети.",
"LEARN_MORE": "Узнать больше о \"Входящих\"", "LEARN_MORE": "Узнать больше о «Входящих»",
"RECONNECTION_REQUIRED": "Входящие сообщения отключены. Вы не будете получать новые сообщения пока не пройдете авторизацию повторно.", "RECONNECTION_REQUIRED": "Входящие сообщения отключены. Вы не будете получать новые сообщения пока не пройдете авторизацию повторно.",
"CLICK_TO_RECONNECT": "Нажмите здесь для повторного подключения.", "CLICK_TO_RECONNECT": "Нажмите здесь для повторного подключения.",
"LIST": { "LIST": {
@@ -208,8 +208,8 @@
} }
}, },
"WHATSAPP": { "WHATSAPP": {
"TITLE": "WhatsApp канал", "TITLE": "Канал WhatsApp",
"DESC": "Начните поддерживать ваших клиентов через WhatsApp.", "DESC": "Начните поддерживать своих клиентов через WhatsApp.",
"PROVIDERS": { "PROVIDERS": {
"LABEL": "Поставщик API", "LABEL": "Поставщик API",
"TWILIO": "Twilio", "TWILIO": "Twilio",
@@ -302,8 +302,8 @@
"ERROR": "Произошла ошибка при отвязке задачи, пожалуйста, попробуйте ещё раз" "ERROR": "Произошла ошибка при отвязке задачи, пожалуйста, попробуйте ещё раз"
}, },
"DELETE": { "DELETE": {
"TITLE": "Are you sure you want to delete the integration?", "TITLE": "Вы уверены, что хотите удалить интеграцию?",
"MESSAGE": "Are you sure you want to delete the integration?", "MESSAGE": "Вы уверены, что хотите удалить интеграцию?",
"CONFIRM": "Да, удалить", "CONFIRM": "Да, удалить",
"CANCEL": "Отменить" "CANCEL": "Отменить"
} }
@@ -317,18 +317,18 @@
"YOU": "Вы", "YOU": "Вы",
"USE": "Использовать это", "USE": "Использовать это",
"RESET": "Сброс", "RESET": "Сброс",
"SELECT_ASSISTANT": "Select Assistant" "SELECT_ASSISTANT": "Выбрать ассистента"
}, },
"PAYWALL": { "PAYWALL": {
"TITLE": "Обновите тарифный план, чтобы использовать Captain AI", "TITLE": "Обновите тарифный план, чтобы использовать Captain AI",
"AVAILABLE_ON": "Капитан недоступен на бесплатном тарифном плане.", "AVAILABLE_ON": "Капитан недоступен на бесплатном тарифном плане.",
"UPGRADE_PROMPT": "Обновите тарифный план, чтобы получить доступ к помощникам, Captain AI и другому.", "UPGRADE_PROMPT": "Обновите тарифный план, чтобы получить доступ к нашим ассистентам, copilot и другим функциям.",
"UPGRADE_NOW": "Обновить сейчас", "UPGRADE_NOW": "Обновить сейчас",
"CANCEL_ANYTIME": "Вы можете изменить или отменить ваш тарифный план в любое время" "CANCEL_ANYTIME": "Вы можете изменить или отменить ваш тарифный план в любое время"
}, },
"ENTERPRISE_PAYWALL": { "ENTERPRISE_PAYWALL": {
"AVAILABLE_ON": "Функция Captain AI доступна только в платном плане.", "AVAILABLE_ON": "Функция Captain AI доступна только в платном плане.",
"UPGRADE_PROMPT": "Обновите тарифный план, чтобы получить доступ к помощникам, Captain AI и другому.", "UPGRADE_PROMPT": "Обновите тарифный план, чтобы получить доступ к нашим ассистентам, copilot и другим функциям.",
"ASK_ADMIN": "Пожалуйста, обратитесь к вашему администратору для обновления." "ASK_ADMIN": "Пожалуйста, обратитесь к вашему администратору для обновления."
}, },
"BANNER": { "BANNER": {
@@ -341,35 +341,35 @@
"EDIT": "Обновить" "EDIT": "Обновить"
}, },
"ASSISTANTS": { "ASSISTANTS": {
"HEADER": "Помощники", "HEADER": "Ассистенты",
"ADD_NEW": "Создать нового помощника", "ADD_NEW": "Создать нового ассистента",
"DELETE": { "DELETE": {
"TITLE": "Вы уверены, что хотите удалить помощника?", "TITLE": "Вы уверены, что хотите удалить ассистента?",
"DESCRIPTION": "Это действие необратимо. Удаление этого помощника удалит его из всех подключенных источников и навсегда удалит все сгенерированные знания.", "DESCRIPTION": "Это действие необратимо. Удаление этого ассистента удалит его из всех подключенных источников и навсегда удалит все сгенерированные знания.",
"CONFIRM": "Да, удалить", "CONFIRM": "Да, удалить",
"SUCCESS_MESSAGE": "Помощник успешно удален", "SUCCESS_MESSAGE": "Ассистент успешно удален",
"ERROR_MESSAGE": "При удалении помощника произошла ошибка. Пожалуйста, попробуйте еще раз." "ERROR_MESSAGE": "При удалении ассистента произошла ошибка. Пожалуйста, попробуйте еще раз."
}, },
"FORM_DESCRIPTION": "Заполните приведенные ниже сведения, чтобы назвать своего помощника, описать его цель и указать поддерживаемый продукт.", "FORM_DESCRIPTION": "Заполните приведенные ниже сведения, чтобы назвать своего ассистента, описать его цель и указать поддерживаемый продукт.",
"CREATE": { "CREATE": {
"TITLE": "Создать помощника", "TITLE": "Создать ассистента",
"SUCCESS_MESSAGE": "Помощник успешно создан", "SUCCESS_MESSAGE": "Ассистент успешно создан",
"ERROR_MESSAGE": "При создании помощника произошла ошибка, пожалуйста, попробуйте еще раз." "ERROR_MESSAGE": "При создании ассистента произошла ошибка, пожалуйста, попробуйте еще раз."
}, },
"FORM": { "FORM": {
"NAME": { "NAME": {
"LABEL": "Имя помощника", "LABEL": "Имя ассистента",
"PLACEHOLDER": "Введите имя помощника", "PLACEHOLDER": "Введите имя ассистента",
"ERROR": "Пожалуйста, укажите имя помощника" "ERROR": "Пожалуйста, укажите имя ассистента"
}, },
"DESCRIPTION": { "DESCRIPTION": {
"LABEL": "Описание помощника", "LABEL": "Описание ассистента",
"PLACEHOLDER": "Опишите, как и где будет использоваться этот помощник", "PLACEHOLDER": "Опишите, как и где будет использоваться этот ассистент",
"ERROR": "Необходимо описание" "ERROR": "Необходимо описание"
}, },
"PRODUCT_NAME": { "PRODUCT_NAME": {
"LABEL": "Название продукта", "LABEL": "Название продукта",
"PLACEHOLDER": "Введите название продукта, для которого предназначен этот помощник", "PLACEHOLDER": "Введите название продукта, для которого предназначен этот ассистент",
"ERROR": "Требуется название продукта" "ERROR": "Требуется название продукта"
}, },
"FEATURES": { "FEATURES": {
@@ -379,18 +379,18 @@
} }
}, },
"EDIT": { "EDIT": {
"TITLE": "Обновить помощника", "TITLE": "Обновить ассистента",
"SUCCESS_MESSAGE": "Помощник успешно обновлен", "SUCCESS_MESSAGE": "Ассистент успешно обновлен",
"ERROR_MESSAGE": "При обновлении помощника произошла ошибка, пожалуйста, попробуйте еще раз." "ERROR_MESSAGE": "При обновлении ассистента произошла ошибка, пожалуйста, попробуйте еще раз."
}, },
"OPTIONS": { "OPTIONS": {
"EDIT_ASSISTANT": "Редактировать помощника", "EDIT_ASSISTANT": "Редактировать ассистента",
"DELETE_ASSISTANT": "Удалить помощника", "DELETE_ASSISTANT": "Удалить ассистента",
"VIEW_CONNECTED_INBOXES": "Просмотр подключенных источников" "VIEW_CONNECTED_INBOXES": "Просмотр подключенных источников"
}, },
"EMPTY_STATE": { "EMPTY_STATE": {
"TITLE": "Нет доступных помощников", "TITLE": "Нет доступных ассистентов",
"SUBTITLE": "Создайте помощника, чтобы дать быстрые и точные ответы пользователям. Он может учиться на ваших статьях из центра поддержки и прошлых диалогах." "SUBTITLE": "Создайте ассистента, чтобы дать быстрые и точные ответы пользователям. Он может учиться на ваших статьях из центра поддержки и прошлых диалогах."
} }
}, },
"DOCUMENTS": { "DOCUMENTS": {
@@ -413,9 +413,9 @@
"ERROR": "Пожалуйста, укажите корректный URL для документа" "ERROR": "Пожалуйста, укажите корректный URL для документа"
}, },
"ASSISTANT": { "ASSISTANT": {
"LABEL": "Помощник", "LABEL": "Ассистент",
"PLACEHOLDER": "Выберите помощника", "PLACEHOLDER": "Выберите ассистента",
"ERROR": "Необходимо заполнить поле помощника" "ERROR": "Необходимо заполнить поле ассистента"
} }
}, },
"DELETE": { "DELETE": {
@@ -440,19 +440,19 @@
"DOCUMENTABLE": { "DOCUMENTABLE": {
"CONVERSATION": "Диалог #{id}" "CONVERSATION": "Диалог #{id}"
}, },
"SELECTED": "{count} selected", "SELECTED": "Выбрано {count}",
"BULK_APPROVE_BUTTON": "Approve", "BULK_APPROVE_BUTTON": "Одобрить",
"BULK_DELETE_BUTTON": "Удалить", "BULK_DELETE_BUTTON": "Удалить",
"BULK_APPROVE": { "BULK_APPROVE": {
"SUCCESS_MESSAGE": "FAQs approved successfully", "SUCCESS_MESSAGE": "FAQ успешно одобрены",
"ERROR_MESSAGE": "There was an error approving the FAQs, please try again." "ERROR_MESSAGE": "Произошла ошибка при одобрении FAQ, пожалуйста, попробуйте ещё раз."
}, },
"BULK_DELETE": { "BULK_DELETE": {
"TITLE": "Delete FAQs?", "TITLE": "Удалить FAQ?",
"DESCRIPTION": "Are you sure you want to delete the selected FAQs? This action cannot be undone.", "DESCRIPTION": "Вы уверены, что хотите удалить выбранные FAQ? Это действие невозможно отменить.",
"CONFIRM": "Yes, delete all", "CONFIRM": "Да, удалить всё",
"SUCCESS_MESSAGE": "FAQs deleted successfully", "SUCCESS_MESSAGE": "FAQ успешно удалены",
"ERROR_MESSAGE": "There was an error deleting the FAQs, please try again." "ERROR_MESSAGE": "Произошла ошибка при удалении FAQ, пожалуйста, попробуйте ещё раз."
}, },
"DELETE": { "DELETE": {
"TITLE": "Вы действительно хотите удалить FAQ?", "TITLE": "Вы действительно хотите удалить FAQ?",
@@ -462,7 +462,7 @@
"ERROR_MESSAGE": "Произошла ошибка при удалении FAQ, попробуйте еще раз." "ERROR_MESSAGE": "Произошла ошибка при удалении FAQ, попробуйте еще раз."
}, },
"FILTER": { "FILTER": {
"ASSISTANT": "Помощник: {selected}", "ASSISTANT": "Ассистент: {selected}",
"STATUS": "Статус: {selected}", "STATUS": "Статус: {selected}",
"ALL_ASSISTANTS": "Все" "ALL_ASSISTANTS": "Все"
}, },
@@ -472,7 +472,7 @@
"APPROVED": "Одобрено", "APPROVED": "Одобрено",
"ALL": "Все" "ALL": "Все"
}, },
"FORM_DESCRIPTION": "Добавьте вопрос и соответствующий ему ответ в базу знаний и выберите помощника, с которым он должен связаться.", "FORM_DESCRIPTION": "Добавьте вопрос и соответствующий ему ответ в базу знаний и выберите ассистента, с которым он должен связаться.",
"CREATE": { "CREATE": {
"TITLE": "Добавить FAQ", "TITLE": "Добавить FAQ",
"SUCCESS_MESSAGE": "Ответ успешно добавлен.", "SUCCESS_MESSAGE": "Ответ успешно добавлен.",
@@ -487,59 +487,59 @@
"ANSWER": { "ANSWER": {
"LABEL": "Ответ", "LABEL": "Ответ",
"PLACEHOLDER": "Введите ответ", "PLACEHOLDER": "Введите ответ",
"ERROR": "Please provide a valid answer." "ERROR": "Пожалуйста, введите корректный ответ."
}, },
"ASSISTANT": { "ASSISTANT": {
"LABEL": "Помощник", "LABEL": "Ассистент",
"PLACEHOLDER": "Select an assistant", "PLACEHOLDER": "Выбрать ассистента",
"ERROR": "Please select an assistant." "ERROR": "Выбрать ассистента"
} }
}, },
"EDIT": { "EDIT": {
"TITLE": "Update the FAQ", "TITLE": "Обновить FAQ",
"SUCCESS_MESSAGE": "The FAQ has been successfully updated", "SUCCESS_MESSAGE": "FAQ успешно обновлён",
"ERROR_MESSAGE": "There was an error updating the FAQ, please try again", "ERROR_MESSAGE": "Произошла ошибка при обновлении FAQ, пожалуйста, попробуйте ещё раз",
"APPROVE_SUCCESS_MESSAGE": "The FAQ was marked as approved" "APPROVE_SUCCESS_MESSAGE": "FAQ был отмечен как одобренный"
}, },
"OPTIONS": { "OPTIONS": {
"APPROVE": "Mark as approved", "APPROVE": "Отметить как одобренный",
"EDIT_RESPONSE": "Edit FAQ", "EDIT_RESPONSE": "FAQ отмечен как одобренный",
"DELETE_RESPONSE": "Delete FAQ" "DELETE_RESPONSE": "Удалить FAQ"
}, },
"EMPTY_STATE": { "EMPTY_STATE": {
"TITLE": "No FAQs Found", "TITLE": "FAQ не найдены",
"SUBTITLE": "FAQs help your assistant provide quick and accurate answers to questions from your customers. They can be generated automatically from your content or can be added manually." "SUBTITLE": "FAQ помогают вашему ассистенту быстро и точно отвечать на вопросы клиентов. Их можно генерировать автоматически из вашего контента или добавлять вручную."
} }
}, },
"INBOXES": { "INBOXES": {
"HEADER": "Connected Inboxes", "HEADER": "Подключённые источники входящих",
"ADD_NEW": "Connect a new inbox", "ADD_NEW": "Подключить новый источник входящих",
"OPTIONS": { "OPTIONS": {
"DISCONNECT": "Отключиться" "DISCONNECT": "Отключиться"
}, },
"DELETE": { "DELETE": {
"TITLE": "Are you sure to disconnect the inbox?", "TITLE": "Вы уверены, что хотите отключить этот источник входящих?",
"DESCRIPTION": "", "DESCRIPTION": "",
"CONFIRM": "Да, удалить", "CONFIRM": "Да, удалить",
"SUCCESS_MESSAGE": "The inbox was successfully disconnected.", "SUCCESS_MESSAGE": "Источник входящих успешно отключён.",
"ERROR_MESSAGE": "There was an error disconnecting the inbox, please try again." "ERROR_MESSAGE": "Произошла ошибка при отключении источника входящих, пожалуйста, попробуйте ещё раз."
}, },
"FORM_DESCRIPTION": "Choose an inbox to connect with the assistant.", "FORM_DESCRIPTION": "Выберите источник входящих для подключения к ассистенту.",
"CREATE": { "CREATE": {
"TITLE": "Connect an Inbox", "TITLE": "Подключить источник входящих",
"SUCCESS_MESSAGE": "The inbox was successfully connected.", "SUCCESS_MESSAGE": "Источник входящих успешно подключён.",
"ERROR_MESSAGE": "An error occurred while connecting the inbox. Please try again." "ERROR_MESSAGE": "Произошла ошибка при подключении источника входящих. Пожалуйста, попробуйте ещё раз."
}, },
"FORM": { "FORM": {
"INBOX": { "INBOX": {
"LABEL": "Электронная почта", "LABEL": "Электронная почта",
"PLACEHOLDER": "Choose the inbox to deploy the assistant.", "PLACEHOLDER": "Выберите источник входящих для развертывания ассистента.",
"ERROR": "An inbox selection is required." "ERROR": "Необходимо выбрать источник входящих."
} }
}, },
"EMPTY_STATE": { "EMPTY_STATE": {
"TITLE": "No Connected Inboxes", "TITLE": "Нет подключённых источников входящих",
"SUBTITLE": "Connecting an inbox allows the assistant to handle initial questions from your customers before transferring them to you." "SUBTITLE": "Подключение источника входящих позволяет ассистенту обрабатывать первые вопросы ваших клиентов до их передачи вам."
} }
} }
} }
@@ -124,7 +124,7 @@
}, },
"AGENT_REPORTS": { "AGENT_REPORTS": {
"HEADER": "Обзор агентов", "HEADER": "Обзор агентов",
"DESCRIPTION": "Easily track agent performance with key metrics such as conversations, response times, resolution times, and resolved cases. Click an agents name to learn more.", "DESCRIPTION": "Легко отслеживайте эффективность работы агентов с помощью ключевых метрик, таких как количество бесед, время ответа, время решения проблем и количество решённых случаев. Нажмите на имя агента, чтобы узнать подробности.",
"LOADING_CHART": "Загрузка данных графика...", "LOADING_CHART": "Загрузка данных графика...",
"NO_ENOUGH_DATA": "Недостаточно данных для создания отчета, пожалуйста, повторите попытку позже.", "NO_ENOUGH_DATA": "Недостаточно данных для создания отчета, пожалуйста, повторите попытку позже.",
"DOWNLOAD_AGENT_REPORTS": "Сказать отчёт по агентам", "DOWNLOAD_AGENT_REPORTS": "Сказать отчёт по агентам",
@@ -259,7 +259,7 @@
}, },
"INBOX_REPORTS": { "INBOX_REPORTS": {
"HEADER": "Обзор входящих", "HEADER": "Обзор входящих",
"DESCRIPTION": "Quickly view your inbox performance with key metrics like conversations, response times, resolution times, and resolved cases—all in one place. Click an inbox name for more details.", "DESCRIPTION": "Быстро просматривайте эффективность источника входящих с основными метриками — количеством бесед, временем ответа, временем решения проблем и количеством решённых случаев — всё в одном месте. Нажмите на имя источника для получения подробной информации.",
"LOADING_CHART": "Загрузка данных графика...", "LOADING_CHART": "Загрузка данных графика...",
"NO_ENOUGH_DATA": "Недостаточно данных для создания отчета, пожалуйста, повторите попытку позже.", "NO_ENOUGH_DATA": "Недостаточно данных для создания отчета, пожалуйста, повторите попытку позже.",
"DOWNLOAD_INBOX_REPORTS": "Скачать отчет по входящим", "DOWNLOAD_INBOX_REPORTS": "Скачать отчет по входящим",
@@ -327,7 +327,7 @@
}, },
"TEAM_REPORTS": { "TEAM_REPORTS": {
"HEADER": "Обзор команды", "HEADER": "Обзор команды",
"DESCRIPTION": "Get a snapshot of your teams performance with essential metrics, including conversations, response times, resolution times, and resolved cases. Click a team name for more details.", "DESCRIPTION": "Получите обзор работы вашей команды с основными метриками, такими как количество бесед, время ответа, время решения проблем и количество решённых случаев. Нажмите на имя команды для подробностей.",
"LOADING_CHART": "Загрузка данных графика...", "LOADING_CHART": "Загрузка данных графика...",
"NO_ENOUGH_DATA": "Недостаточно данных для создания отчета, пожалуйста, повторите попытку позже.", "NO_ENOUGH_DATA": "Недостаточно данных для создания отчета, пожалуйста, повторите попытку позже.",
"DOWNLOAD_TEAM_REPORTS": "Скачать отчет по команде", "DOWNLOAD_TEAM_REPORTS": "Скачать отчет по команде",
@@ -546,9 +546,9 @@
"INBOX": "Электронная почта", "INBOX": "Электронная почта",
"AGENT": "Оператор", "AGENT": "Оператор",
"TEAM": "Команда", "TEAM": "Команда",
"AVG_RESOLUTION_TIME": "Avg. Resolution Time", "AVG_RESOLUTION_TIME": "Среднее время решения",
"AVG_FIRST_RESPONSE_TIME": "Avg. First Response Time", "AVG_FIRST_RESPONSE_TIME": "Среднее время первого ответа",
"AVG_REPLY_TIME": "Avg. Customer Waiting Time", "AVG_REPLY_TIME": "Среднее время ожидания клиента",
"RESOLUTION_COUNT": "Количество завершенных", "RESOLUTION_COUNT": "Количество завершенных",
"CONVERSATIONS": "Количество диалогов" "CONVERSATIONS": "Количество диалогов"
} }
@@ -11,8 +11,8 @@
"CONVERSATIONS": "Диалоги", "CONVERSATIONS": "Диалоги",
"MESSAGES": "Сообщения" "MESSAGES": "Сообщения"
}, },
"VIEW_MORE": "View more", "VIEW_MORE": "Посмотреть больше",
"LOAD_MORE": "Load more", "LOAD_MORE": "Загрузить ещё",
"SEARCHING_DATA": "Идёт поиск", "SEARCHING_DATA": "Идёт поиск",
"LOADING_DATA": "Загрузка", "LOADING_DATA": "Загрузка",
"EMPTY_STATE": "Не найдено {item} для запроса '{query}'", "EMPTY_STATE": "Не найдено {item} для запроса '{query}'",
@@ -36,20 +36,20 @@
} }
}, },
"INTERFACE_SECTION": { "INTERFACE_SECTION": {
"TITLE": "Interface", "TITLE": "Интерфейс",
"NOTE": "Customize the look and feel of your Chatwoot dashboard.", "NOTE": "Настройте внешний вид и оформление вашей панели Chatwoot.",
"FONT_SIZE": { "FONT_SIZE": {
"TITLE": "Font size", "TITLE": "Размер шрифта",
"NOTE": "Adjust the text size across the dashboard based on your preference.", "NOTE": "Измените размер текста на всей панели в соответствии с вашими предпочтениями.",
"UPDATE_SUCCESS": "Your font settings have been updated successfully", "UPDATE_SUCCESS": "Настройки шрифта успешно обновлены",
"UPDATE_ERROR": "There is an error while updating the font settings, please try again", "UPDATE_ERROR": "Произошла ошибка при обновлении настроек шрифта, пожалуйста, попробуйте ещё раз",
"OPTIONS": { "OPTIONS": {
"SMALLER": "Smaller", "SMALLER": "Меньше",
"SMALL": "Small", "SMALL": "Маленький",
"DEFAULT": "По умолчанию", "DEFAULT": "По умолчанию",
"LARGE": "Large", "LARGE": "Большой",
"LARGER": "Larger", "LARGER": "Крупнее",
"EXTRA_LARGE": "Extra Large" "EXTRA_LARGE": "Очень большой"
} }
} }
}, },
@@ -162,7 +162,7 @@
"REQUEST_PUSH": "Включить push-уведомления", "REQUEST_PUSH": "Включить push-уведомления",
"SLA_MISSED_FIRST_RESPONSE": "Отправить push-уведомление, если диалог не соответствует SLA первого ответа", "SLA_MISSED_FIRST_RESPONSE": "Отправить push-уведомление, если диалог не соответствует SLA первого ответа",
"SLA_MISSED_NEXT_RESPONSE": "Отправить push-уведомление, если диалог не успел получить следующий ответ SLA", "SLA_MISSED_NEXT_RESPONSE": "Отправить push-уведомление, если диалог не успел получить следующий ответ SLA",
"SLA_MISSED_RESOLUTION": "Отправить push-уведомление, если диалог не соответствует уровню разрешения SLA" "SLA_MISSED_RESOLUTION": "Отправить push-уведомление, если диалог не соответствует SLA"
}, },
"PROFILE_IMAGE": { "PROFILE_IMAGE": {
"LABEL": "Изображение профиля" "LABEL": "Изображение профиля"
@@ -281,9 +281,9 @@
"SETTINGS": "Настройки", "SETTINGS": "Настройки",
"CONTACTS": "Контакты", "CONTACTS": "Контакты",
"CAPTAIN": "Капитан", "CAPTAIN": "Капитан",
"CAPTAIN_ASSISTANTS": "Помощники", "CAPTAIN_ASSISTANTS": "Ассистенты",
"CAPTAIN_DOCUMENTS": "Документы", "CAPTAIN_DOCUMENTS": "Документы",
"CAPTAIN_RESPONSES": "FAQs", "CAPTAIN_RESPONSES": "FAQ",
"HOME": "Главная", "HOME": "Главная",
"AGENTS": "Операторы", "AGENTS": "Операторы",
"AGENT_BOTS": "Боты", "AGENT_BOTS": "Боты",
@@ -345,14 +345,14 @@
}, },
"BILLING_SETTINGS": { "BILLING_SETTINGS": {
"TITLE": "Платёж", "TITLE": "Платёж",
"DESCRIPTION": "Manage your subscription here, upgrade your plan and get more for your team.", "DESCRIPTION": "Управляйте подпиской здесь, обновите тарифный план и получите больше возможностей для вашей команды.",
"CURRENT_PLAN": { "CURRENT_PLAN": {
"TITLE": "Текущий план", "TITLE": "Текущий план",
"PLAN_NOTE": "На данный момент вы подписаны на ** план{plan}** с **{quantity}** лицензиями", "PLAN_NOTE": "На данный момент вы подписаны на ** план{plan}** с **{quantity}** лицензиями",
"SEAT_COUNT": "Number of seats", "SEAT_COUNT": "Количество мест",
"RENEWS_ON": "Renews on" "RENEWS_ON": "Продление"
}, },
"VIEW_PRICING": "View Pricing", "VIEW_PRICING": "Посмотреть цены",
"MANAGE_SUBSCRIPTION": { "MANAGE_SUBSCRIPTION": {
"TITLE": "Управление подпиской", "TITLE": "Управление подпиской",
"DESCRIPTION": "Просматривайте ваши предыдущие счета, редактируйте платежные реквизиты или отмените подписку.", "DESCRIPTION": "Просматривайте ваши предыдущие счета, редактируйте платежные реквизиты или отмените подписку.",
@@ -360,11 +360,11 @@
}, },
"CAPTAIN": { "CAPTAIN": {
"TITLE": "Капитан", "TITLE": "Капитан",
"DESCRIPTION": "Manage usage and credits for Captain AI.", "DESCRIPTION": "Управляйте использованием и кредитами для Captain AI.",
"BUTTON_TXT": "Buy more credits", "BUTTON_TXT": "Купить дополнительные кредиты",
"DOCUMENTS": "Документы", "DOCUMENTS": "Документы",
"RESPONSES": "Responses", "RESPONSES": "Ответы",
"UPGRADE": "Captain is not available on the free plan, upgrade now to get access to assistants, copilot and more." "UPGRADE": "Captain недоступен в бесплатном тарифном плане, обновите его, чтобы получить доступ к ассистентам, copilot и другим функциям."
}, },
"CHAT_WITH_US": { "CHAT_WITH_US": {
"TITLE": "Нужна помощь?", "TITLE": "Нужна помощь?",
@@ -3,7 +3,6 @@ import { mapGetters } from 'vuex';
import { defineAsyncComponent } from 'vue'; import { defineAsyncComponent } from 'vue';
import NextSidebar from 'next/sidebar/Sidebar.vue'; import NextSidebar from 'next/sidebar/Sidebar.vue';
import Sidebar from '../../components/layout/Sidebar.vue';
import WootKeyShortcutModal from 'dashboard/components/widgets/modal/WootKeyShortcutModal.vue'; import WootKeyShortcutModal from 'dashboard/components/widgets/modal/WootKeyShortcutModal.vue';
import AddAccountModal from 'dashboard/components/layout/sidebarComponents/AddAccountModal.vue'; import AddAccountModal from 'dashboard/components/layout/sidebarComponents/AddAccountModal.vue';
import AccountSelector from 'dashboard/components/layout/sidebarComponents/AccountSelector.vue'; import AccountSelector from 'dashboard/components/layout/sidebarComponents/AccountSelector.vue';
@@ -20,6 +19,10 @@ import { FEATURE_FLAGS } from 'dashboard/featureFlags';
const CommandBar = defineAsyncComponent( const CommandBar = defineAsyncComponent(
() => import('./commands/commandbar.vue') () => import('./commands/commandbar.vue')
); );
const Sidebar = defineAsyncComponent(
() => import('../../components/layout/Sidebar.vue')
);
import { emitter } from 'shared/helpers/mitt'; import { emitter } from 'shared/helpers/mitt';
export default { export default {
@@ -121,8 +121,6 @@ export default {
this.isALineChannel || this.isALineChannel ||
this.isAPIInbox || this.isAPIInbox ||
(this.isAnEmailChannel && !this.inbox.provider) || (this.isAnEmailChannel && !this.inbox.provider) ||
this.isAMicrosoftInbox ||
this.isAGoogleInbox ||
this.isAWhatsAppChannel || this.isAWhatsAppChannel ||
this.isAWebWidgetInbox this.isAWebWidgetInbox
) { ) {
@@ -39,13 +39,14 @@ export default {
}, },
mixins: [globalConfigMixin], mixins: [globalConfigMixin],
setup() { setup() {
const { isEditorHotKeyEnabled } = useUISettings(); const { isEditorHotKeyEnabled, updateUISettings } = useUISettings();
const { currentFontSize, updateFontSize } = useFontSize(); const { currentFontSize, updateFontSize } = useFontSize();
return { return {
currentFontSize, currentFontSize,
updateFontSize, updateFontSize,
isEditorHotKeyEnabled, isEditorHotKeyEnabled,
updateUISettings,
}; };
}, },
data() { data() {
@@ -2,8 +2,15 @@ class Inboxes::FetchImapEmailInboxesJob < ApplicationJob
queue_as :scheduled_jobs queue_as :scheduled_jobs
def perform def perform
Inbox.where(channel_type: 'Channel::Email').all.find_each(batch_size: 100) do |inbox| email_inboxes = Inbox.where(channel_type: 'Channel::Email')
::Inboxes::FetchImapEmailsJob.perform_later(inbox.channel) if inbox.channel.imap_enabled email_inboxes.find_each(batch_size: 100) do |inbox|
::Inboxes::FetchImapEmailsJob.perform_later(inbox.channel) if should_fetch_emails?(inbox)
end end
end end
private
def should_fetch_emails?(inbox)
inbox.channel.imap_enabled && !inbox.account.suspended?
end
end end
+18 -3
View File
@@ -37,7 +37,7 @@ class Channel::Sms < ApplicationRecord
body = message_body(contact_number, message.content) body = message_body(contact_number, message.content)
body['media'] = message.attachments.map(&:download_url) if message.attachments.present? body['media'] = message.attachments.map(&:download_url) if message.attachments.present?
send_to_bandwidth(body) send_to_bandwidth(body, message)
end end
def send_text_message(contact_number, message_content) def send_text_message(contact_number, message_content)
@@ -56,7 +56,7 @@ class Channel::Sms < ApplicationRecord
} }
end end
def send_to_bandwidth(body) def send_to_bandwidth(body, message = nil)
response = HTTParty.post( response = HTTParty.post(
"#{api_base_path}/users/#{provider_config['account_id']}/messages", "#{api_base_path}/users/#{provider_config['account_id']}/messages",
basic_auth: bandwidth_auth, basic_auth: bandwidth_auth,
@@ -64,7 +64,22 @@ class Channel::Sms < ApplicationRecord
body: body.to_json body: body.to_json
) )
response.success? ? response.parsed_response['id'] : nil if response.success?
response.parsed_response['id']
else
handle_error(response, message)
nil
end
end
def handle_error(response, message)
Rails.logger.error("[#{account_id}] Error sending SMS: #{response.parsed_response['description']}")
return if message.blank?
# https://dev.bandwidth.com/apis/messaging-apis/messaging/#tag/Messages/operation/createMessage
message.external_error = response.parsed_response['description']
message.status = :failed
message.save!
end end
def bandwidth_auth def bandwidth_auth
+14
View File
@@ -22,6 +22,12 @@
# index_custom_attribute_definitions_on_account_id (account_id) # index_custom_attribute_definitions_on_account_id (account_id)
# #
class CustomAttributeDefinition < ApplicationRecord class CustomAttributeDefinition < ApplicationRecord
STANDARD_ATTRIBUTES = {
:conversation => %w[status priority assignee_id inbox_id team_id display_id campaign_id labels browser_language country_code referer created_at
last_activity_at],
:contact => %w[name email phone_number identifier country_code city created_at last_activity_at referer blocked]
}.freeze
scope :with_attribute_model, ->(attribute_model) { attribute_model.presence && where(attribute_model: attribute_model) } scope :with_attribute_model, ->(attribute_model) { attribute_model.presence && where(attribute_model: attribute_model) }
validates :attribute_display_name, presence: true validates :attribute_display_name, presence: true
@@ -31,6 +37,7 @@ class CustomAttributeDefinition < ApplicationRecord
validates :attribute_display_type, presence: true validates :attribute_display_type, presence: true
validates :attribute_model, presence: true validates :attribute_model, presence: true
validate :attribute_must_not_conflict, on: :create
enum attribute_model: { conversation_attribute: 0, contact_attribute: 1 } enum attribute_model: { conversation_attribute: 0, contact_attribute: 1 }
enum attribute_display_type: { text: 0, number: 1, currency: 2, percent: 3, link: 4, date: 5, list: 6, checkbox: 7 } enum attribute_display_type: { text: 0, number: 1, currency: 2, percent: 3, link: 4, date: 5, list: 6, checkbox: 7 }
@@ -48,4 +55,11 @@ class CustomAttributeDefinition < ApplicationRecord
def update_widget_pre_chat_custom_fields def update_widget_pre_chat_custom_fields
::Inboxes::UpdateWidgetPreChatCustomFieldsJob.perform_later(account, self) ::Inboxes::UpdateWidgetPreChatCustomFieldsJob.perform_later(account, self)
end end
def attribute_must_not_conflict
model_keys = attribute_model.to_sym == :conversation_attribute ? :conversation : :contact
return unless attribute_key.in?(STANDARD_ATTRIBUTES[model_keys])
errors.add(:attribute_key, I18n.t('errors.custom_attribute_definition.key_conflict'))
end
end end
@@ -20,15 +20,30 @@ class Facebook::SendOnFacebookService < Base::SendOnChannelService
end end
def send_message_to_facebook(delivery_params) def send_message_to_facebook(delivery_params)
result = Facebook::Messenger::Bot.deliver(delivery_params, page_id: channel.page_id) parsed_result = deliver_message(delivery_params)
parsed_result = JSON.parse(result) return if parsed_result.nil?
if parsed_result['error'].present? if parsed_result['error'].present?
message.update!(status: :failed, external_error: external_error(parsed_result)) message.update!(status: :failed, external_error: external_error(parsed_result))
Rails.logger.info "Facebook::SendOnFacebookService: Error sending message to Facebook : Page - #{channel.page_id} : #{result}" Rails.logger.info "Facebook::SendOnFacebookService: Error sending message to Facebook : Page - #{channel.page_id} : #{parsed_result}"
end end
message.update!(source_id: parsed_result['message_id']) if parsed_result['message_id'].present? message.update!(source_id: parsed_result['message_id']) if parsed_result['message_id'].present?
end end
def deliver_message(delivery_params)
result = Facebook::Messenger::Bot.deliver(delivery_params, page_id: channel.page_id)
JSON.parse(result)
rescue JSON::ParserError
message.update!(status: :failed, external_error: 'Facebook was unable to process this request')
Rails.logger.error "Facebook::SendOnFacebookService: Error parsing JSON response from Facebook : Page - #{channel.page_id} : #{result}"
nil
rescue Net::OpenTimeout
message.update!(status: :failed, external_error: 'Request timed out, please try again later')
Rails.logger.error "Facebook::SendOnFacebookService: Timeout error sending message to Facebook : Page - #{channel.page_id}"
nil
end
def fb_text_message_params def fb_text_message_params
{ {
recipient: { id: contact.get_source_id(inbox.id) }, recipient: { id: contact.get_source_id(inbox.id) },
+8 -11
View File
@@ -1,7 +1,7 @@
require 'json' require 'json'
class FilterService class FilterService
include FilterHelper include Filters::FilterHelper
include CustomExceptions::CustomFilter include CustomExceptions::CustomFilter
ATTRIBUTE_MODEL = 'conversation_attribute'.freeze ATTRIBUTE_MODEL = 'conversation_attribute'.freeze
@@ -43,19 +43,16 @@ class FilterService
end end
def filter_values(query_hash) def filter_values(query_hash)
case query_hash['attribute_key'] attribute_key = query_hash['attribute_key']
when 'status' values = query_hash['values']
return Conversation.statuses.values if query_hash['values'].include?('all')
return conversation_status_values(values) if attribute_key == 'status'
return conversation_priority_values(values) if attribute_key == 'priority'
return message_type_values(values) if attribute_key == 'message_type'
return downcase_array_values(values) if attribute_key == 'content'
query_hash['values'].map { |x| Conversation.statuses[x.to_sym] }
when 'message_type'
query_hash['values'].map { |x| Message.message_types[x.to_sym] }
when 'content'
downcase_array_values(query_hash['values'])
else
case_insensitive_values(query_hash) case_insensitive_values(query_hash)
end end
end
def downcase_array_values(values) def downcase_array_values(values)
values.map(&:downcase) values.map(&:downcase)
@@ -70,22 +70,28 @@ class Instagram::SendOnInstagramService < Base::SendOnChannelService
query: query query: query
) )
if response[:error].present? handle_response(response, message_content)
Rails.logger.error("Instagram response: #{response['error']} : #{message_content}")
message.status = :failed
message.external_error = external_error(response)
end end
message.source_id = response['message_id'] if response['message_id'].present? def handle_response(response, message_content)
message.save! parsed_response = response.parsed_response
if response.success? && parsed_response['error'].blank?
message.update!(source_id: parsed_response['message_id'])
response parsed_response
else
external_error = external_error(parsed_response)
Rails.logger.error("Instagram response: #{external_error} : #{message_content}")
message.update!(status: :failed, external_error: external_error)
nil
end
end end
def external_error(response) def external_error(response)
# https://developers.facebook.com/docs/instagram-api/reference/error-codes/ # https://developers.facebook.com/docs/instagram-api/reference/error-codes/
error_message = response[:error][:message] error_message = response.dig('error', 'message')
error_code = response[:error][:code] error_code = response.dig('error', 'code')
"#{error_code} - #{error_message}" "#{error_code} - #{error_message}"
end end
@@ -27,6 +27,33 @@ class Whatsapp::Providers::BaseService
raise 'Overwrite this method in child class' raise 'Overwrite this method in child class'
end end
def error_message
raise 'Overwrite this method in child class'
end
def process_response(response)
parsed_response = response.parsed_response
if response.success? && parsed_response['error'].blank?
parsed_response['messages'].first['id']
else
handle_error(response)
nil
end
end
def handle_error(response)
Rails.logger.error response.body
return if @message.blank?
# https://developers.facebook.com/docs/whatsapp/cloud-api/support/error-codes/#sample-response
error_message = error_message(response)
return if error_message.blank?
@message.external_error = error_message
@message.status = :failed
@message.save!
end
def create_buttons(items) def create_buttons(items)
buttons = [] buttons = []
items.each do |item| items.each do |item|
@@ -1,5 +1,6 @@
class Whatsapp::Providers::Whatsapp360DialogService < Whatsapp::Providers::BaseService class Whatsapp::Providers::Whatsapp360DialogService < Whatsapp::Providers::BaseService
def send_message(phone_number, message) def send_message(phone_number, message)
@message = message
if message.attachments.present? if message.attachments.present?
send_attachment_message(phone_number, message) send_attachment_message(phone_number, message)
elsif message.content_type == 'input_select' elsif message.content_type == 'input_select'
@@ -78,6 +79,7 @@ class Whatsapp::Providers::Whatsapp360DialogService < Whatsapp::Providers::BaseS
} }
type_content['caption'] = message.content unless %w[audio sticker].include?(type) type_content['caption'] = message.content unless %w[audio sticker].include?(type)
type_content['filename'] = attachment.file.filename if type == 'document' type_content['filename'] = attachment.file.filename if type == 'document'
response = HTTParty.post( response = HTTParty.post(
"#{api_base_path}/messages", "#{api_base_path}/messages",
headers: api_headers, headers: api_headers,
@@ -91,13 +93,9 @@ class Whatsapp::Providers::Whatsapp360DialogService < Whatsapp::Providers::BaseS
process_response(response) process_response(response)
end end
def process_response(response) def error_message(response)
if response.success? # {"meta": {"success": false, "http_code": 400, "developer_message": "errro-message", "360dialog_trace_id": "someid"}}
response['messages'].first['id'] response.parsed_response.dig('meta', 'developer_message')
else
Rails.logger.error response.body
nil
end
end end
def template_body_parameters(template_info) def template_body_parameters(template_info)
@@ -1,5 +1,7 @@
class Whatsapp::Providers::WhatsappCloudService < Whatsapp::Providers::BaseService class Whatsapp::Providers::WhatsappCloudService < Whatsapp::Providers::BaseService
def send_message(phone_number, message) def send_message(phone_number, message)
@message = message
if message.attachments.present? if message.attachments.present?
send_attachment_message(phone_number, message) send_attachment_message(phone_number, message)
elsif message.content_type == 'input_select' elsif message.content_type == 'input_select'
@@ -111,13 +113,9 @@ class Whatsapp::Providers::WhatsappCloudService < Whatsapp::Providers::BaseServi
process_response(response) process_response(response)
end end
def process_response(response) def error_message(response)
if response.success? # https://developers.facebook.com/docs/whatsapp/cloud-api/support/error-codes/#sample-response
response['messages'].first['id'] response.parsed_response&.dig('error', 'message')
else
Rails.logger.error response.body
nil
end
end end
def template_body_parameters(template_info) def template_body_parameters(template_info)
@@ -1,3 +1,7 @@
json.meta do
json.total_count @attachments_count
end
json.payload @attachments do |attachment| json.payload @attachments do |attachment|
json.message_id attachment.push_event_data[:message_id] json.message_id attachment.push_event_data[:message_id]
json.thumb_url attachment.push_event_data[:thumb_url] json.thumb_url attachment.push_event_data[:thumb_url]
@@ -1,11 +1,19 @@
<% content_for(:title) do %> <% content_for(:title) do %>
Configure Settings - <%= @config.titleize %> Configure Settings - <%= @config.titleize %>
<% end %> <% end %>
<header class="main-content__header" role="banner"> <header class="main-content__header" role="banner">
<h1 class="main-content__page-title" id="page-title"> <h1 class="main-content__page-title" id="page-title">
<%= content_for(:title) %> <%= content_for(:title) %>
</h1> </h1>
</header> </header>
<style>
.eye-icon.eye-hide path {
d: path('M3 3l18 18M10.5 10.677a2 2 0 002.823 2.823M7.362 7.561C5.68 8.74 4.279 10.42 3 12c1.889 2.991 5.282 6 9 6 1.55 0 3.043-.523 4.395-1.35M12 6c4.008 0 6.701 3.009 9 6a15.66 15.66 0 01-1.078 1.5');
}
</style>
<section class="main-content__body"> <section class="main-content__body">
<%= form_with url: super_admin_app_config_url(config: @config) , method: :post do |form| %> <%= form_with url: super_admin_app_config_url(config: @config) , method: :post do |form| %>
<% @allowed_configs.each do |key| %> <% @allowed_configs.each do |key| %>
@@ -27,6 +35,24 @@
wrap: 'off', wrap: 'off',
class: "mt-2 border font-mono text-xs border-slate-100 p-1 rounded-md overflow-scroll" class: "mt-2 border font-mono text-xs border-slate-100 p-1 rounded-md overflow-scroll"
%> %>
<% elsif @installation_configs[key]&.dig('type') == 'secret' %>
<div class="relative">
<%= form.password_field "app_config[#{key}]",
id: "app_config_#{key}",
value: @app_config[key],
class: "mt-2 border border-slate-100 p-1.5 pr-8 rounded-md w-full"
%>
<button
type="button"
class="absolute reset-base !bg-white top-1/2 !outline-0 !text-n-slate-11 -translate-y-1/2 right-2 p-1 hover:!bg-n-slate-5 rounded-sm toggle-password"
data-target="app_config_<%= key %>"
>
<svg class="eye-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 5C5.63636 5 2 12 2 12C2 12 5.63636 19 12 19C18.3636 19 22 12 22 12C22 12 18.3636 5 12 5Z"/>
<path d="M12 15C13.6569 15 15 13.6569 15 12C15 10.3431 13.6569 9 12 9C10.3431 9 9 10.3431 9 12C9 13.6569 10.3431 15 12 15Z"/>
</svg>
</button>
</div>
<% else %> <% else %>
<%= form.text_field "app_config[#{key}]", value: @app_config[key] %> <%= form.text_field "app_config[#{key}]", value: @app_config[key] %>
<% end %> <% end %>
@@ -43,3 +69,26 @@
</div> </div>
<% end %> <% end %>
</section> </section>
<% content_for :javascript do %>
<script>
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.toggle-password').forEach(button => {
button.addEventListener('click', () => {
const targetId = button.dataset.target;
const input = document.getElementById(targetId);
const type = input.type === 'password' ? 'text' : 'password';
input.type = type;
// Toggle icon
const svg = button.querySelector('.eye-icon');
if (type === 'password') {
svg.classList.remove('eye-hide')
} else {
svg.classList.add('eye-hide')
}
});
});
});
</script>
<% end %>
@@ -0,0 +1,63 @@
<%#
# Filters
This partial is used on the `index` page to display available filters
for a collection of resources.
## Local variables:
- `page`:
An instance of [Administrate::Page::Collection][1].
Contains helper methods to help display a table,
and knows which attributes should be displayed in the resource's table.
[1]: http://www.rubydoc.info/gems/administrate/Administrate/Page/Collection
%>
<%
# Get the dashboard class name from the resource name
resource_name = page.resource_name.classify
dashboard_class_name = "#{resource_name}Dashboard"
dashboard_class = dashboard_class_name.constantize
# Get the current filter if any
current_filter = nil
if params[:search] && params[:search].include?(':')
current_filter = params[:search].split(':').first
end
%>
<% if dashboard_class.const_defined?(:COLLECTION_FILTERS) && !dashboard_class::COLLECTION_FILTERS.empty? %>
<div class="flex items-center bg-gray-100 border-0 rounded-md shadow-none relative w-[260px]">
<div class="flex items-center h-10 px-2 w-full">
<div class="flex items-center justify-center flex-shrink-0 mr-2 text-gray-500" title="Filter by">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"></polygon>
</svg>
</div>
<div class="flex-1 h-full min-w-0 relative">
<select id="filter-select" class="appearance-none bg-gray-100 border-0 text-gray-700 cursor-pointer text-sm h-full overflow-hidden truncate whitespace-nowrap w-full pr-7 pl-0 py-2 focus:outline-none bg-[url('data:image/svg+xml,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 width=%2712%27 height=%2712%27 viewBox=%270 0 12 12%27%3E%3Cpath fill=%27%23293f54%27 d=%27M6 9L1 4h10z%27/%3E%3C/svg%3E')] bg-[right_0.25rem_center] bg-no-repeat bg-[length:0.75rem]" onchange="applyFilter(this.value)">
<option value="">All records</option>
<% dashboard_class::COLLECTION_FILTERS.each do |filter_name, _| %>
<option value="<%= filter_name %>" <%= 'selected' if filter_name.to_s == current_filter %>>
<%= filter_name.to_s.titleize %>
</option>
<% end %>
</select>
<% if current_filter %>
<a href="?" class="flex items-center justify-center rounded-full text-gray-500 text-xl font-bold h-[18px] w-[18px] leading-none absolute right-5 top-1/2 -translate-y-1/2 no-underline z-2 hover:text-gray-900" title="Clear filter">×</a>
<% end %>
</div>
</div>
</div>
<script>
function applyFilter(filterName) {
if (filterName) {
window.location.href = "?search=" + encodeURIComponent(filterName) + "%3A";
} else {
window.location.href = "?";
}
}
</script>
<% end %>
@@ -0,0 +1,24 @@
<form class="search" role="search">
<label class="search__label" for="search">
<svg class="search__eyeglass-icon" role="img">
<title>
<%= t("administrate.search.label", resource: resource_name) %>
</title>
<use xlink:href="#icon-eyeglass" />
</svg>
</label>
<input class="search__input"
id="search"
type="search"
name="search"
placeholder="<%= t("administrate.search.label", resource: resource_name) %>"
value="<%= search_term %>">
<%= link_to clear_search_params, class: "search__clear-link" do %>
<svg class="search__clear-icon" role="img">
<title><%= t("administrate.search.clear") %></title>
<use xlink:href="#icon-cancel" />
</svg>
<% end %>
</form>
@@ -28,19 +28,26 @@ It renders the `_table` partial to display details about the resources.
<% end %> <% end %>
<header class="main-content__header" role="banner"> <header class="main-content__header" role="banner">
<h1 class="main-content__page-title" id="page-title"> <div class="flex items-center justify-between w-full">
<h1 class="main-content__page-title m-0 mr-6" id="page-title">
<%= content_for(:title) %> <%= content_for(:title) %>
</h1> </h1>
<div class="flex items-center">
<% if show_search_bar %> <% if show_search_bar %>
<div class="flex items-center">
<%= render("filters", page: page) %>
<div class="ml-3">
<%= render( <%= render(
"search", "search",
search_term: search_term, search_term: search_term,
resource_name: display_resource_name(page.resource_name) resource_name: display_resource_name(page.resource_name)
) %> ) %>
</div>
</div>
<% end %> <% end %>
<div> <div class="whitespace-nowrap ml-4">
<%= link_to( <%= link_to(
t( t(
"administrate.actions.new_resource", "administrate.actions.new_resource",
@@ -50,6 +57,8 @@ It renders the `_table` partial to display details about the resources.
class: "button", class: "button",
) if accessible_action?(new_resource, :new) %> ) if accessible_action?(new_resource, :new) %>
</div> </div>
</div>
</div>
</header> </header>
<section class="main-content__body main-content__body--flush"> <section class="main-content__body main-content__body--flush">
+14
View File
@@ -103,13 +103,16 @@
display_title: 'Facebook Verify Token' display_title: 'Facebook Verify Token'
description: 'The verify token used for Facebook Messenger Webhook' description: 'The verify token used for Facebook Messenger Webhook'
locked: false locked: false
type: secret
- name: FB_APP_SECRET - name: FB_APP_SECRET
display_title: 'Facebook App Secret' display_title: 'Facebook App Secret'
locked: false locked: false
type: secret
- name: IG_VERIFY_TOKEN - name: IG_VERIFY_TOKEN
display_title: 'Instagram Verify Token' display_title: 'Instagram Verify Token'
description: 'The verify token used for Instagram Webhook' description: 'The verify token used for Instagram Webhook'
locked: false locked: false
type: secret
- name: FACEBOOK_API_VERSION - name: FACEBOOK_API_VERSION
display_title: 'Facebook API Version' display_title: 'Facebook API Version'
description: 'Configure this if you want to use a different Facebook API version. Make sure its prefixed with `v`' description: 'Configure this if you want to use a different Facebook API version. Make sure its prefixed with `v`'
@@ -131,6 +134,7 @@
- name: AZURE_APP_SECRET - name: AZURE_APP_SECRET
display_title: 'Azure App Secret' display_title: 'Azure App Secret'
locked: false locked: false
type: secret
# End of Microsoft Email Channel Config # End of Microsoft Email Channel Config
# MARK: Captain Config # MARK: Captain Config
@@ -138,6 +142,7 @@
display_title: 'OpenAI API Key' display_title: 'OpenAI API Key'
description: 'The API key used to authenticate requests to OpenAI services for Captain AI.' description: 'The API key used to authenticate requests to OpenAI services for Captain AI.'
locked: false locked: false
type: secret
- name: CAPTAIN_OPEN_AI_MODEL - name: CAPTAIN_OPEN_AI_MODEL
display_title: 'OpenAI Model' display_title: 'OpenAI Model'
description: 'The OpenAI model configured for use in Captain AI. Default: gpt-4o-mini' description: 'The OpenAI model configured for use in Captain AI. Default: gpt-4o-mini'
@@ -146,6 +151,7 @@
display_title: 'FireCrawl API Key (optional)' display_title: 'FireCrawl API Key (optional)'
description: 'The FireCrawl API key for the Captain AI service' description: 'The FireCrawl API key for the Captain AI service'
locked: false locked: false
type: secret
- name: CAPTAIN_CLOUD_PLAN_LIMITS - name: CAPTAIN_CLOUD_PLAN_LIMITS
display_title: 'Captain Cloud Plan Limits' display_title: 'Captain Cloud Plan Limits'
description: 'The limits for the Captain AI service for different plans' description: 'The limits for the Captain AI service for different plans'
@@ -160,11 +166,13 @@
display_title: 'Inbox Token' display_title: 'Inbox Token'
description: 'The Chatwoot Inbox Token for Contact Support in Cloud' description: 'The Chatwoot Inbox Token for Contact Support in Cloud'
locked: false locked: false
type: secret
- name: CHATWOOT_INBOX_HMAC_KEY - name: CHATWOOT_INBOX_HMAC_KEY
value: value:
display_title: 'Inbox HMAC Key' display_title: 'Inbox HMAC Key'
description: 'The Chatwoot Inbox HMAC Key for Contact Support in Cloud' description: 'The Chatwoot Inbox HMAC Key for Contact Support in Cloud'
locked: false locked: false
type: secret
- name: CHATWOOT_CLOUD_PLANS - name: CHATWOOT_CLOUD_PLANS
display_title: 'Cloud Plans' display_title: 'Cloud Plans'
value: value:
@@ -180,10 +188,12 @@
value: value:
display_title: 'Analytics Token' display_title: 'Analytics Token'
description: 'The June.so analytics token for Chatwoot cloud' description: 'The June.so analytics token for Chatwoot cloud'
type: secret
- name: CLEARBIT_API_KEY - name: CLEARBIT_API_KEY
value: value:
display_title: 'Clearbit API Key' display_title: 'Clearbit API Key'
description: 'This API key is used for onboarding the users, to pre-fill account data.' description: 'This API key is used for onboarding the users, to pre-fill account data.'
type: secret
- name: DASHBOARD_SCRIPTS - name: DASHBOARD_SCRIPTS
value: value:
display_title: 'Dashboard Scripts' display_title: 'Dashboard Scripts'
@@ -206,12 +216,14 @@
- name: CHATWOOT_SUPPORT_WEBSITE_TOKEN - name: CHATWOOT_SUPPORT_WEBSITE_TOKEN
value: value:
description: 'The Chatwoot website token, used to identify the Chatwoot inbox and display the "Contact Support" option on the billing page' description: 'The Chatwoot website token, used to identify the Chatwoot inbox and display the "Contact Support" option on the billing page'
type: secret
- name: CHATWOOT_SUPPORT_SCRIPT_URL - name: CHATWOOT_SUPPORT_SCRIPT_URL
value: value:
description: 'The Chatwoot script base URL, to display the "Contact Support" option on the billing page' description: 'The Chatwoot script base URL, to display the "Contact Support" option on the billing page'
- name: CHATWOOT_SUPPORT_IDENTIFIER_HASH - name: CHATWOOT_SUPPORT_IDENTIFIER_HASH
value: value:
description: 'The Chatwoot identifier hash, to validate the contact in the live chat window.' description: 'The Chatwoot identifier hash, to validate the contact in the live chat window.'
type: secret
- name: ACCOUNT_SECURITY_NOTIFICATION_WEBHOOK_URL - name: ACCOUNT_SECURITY_NOTIFICATION_WEBHOOK_URL
display_title: Webhook URL to post security analysis display_title: Webhook URL to post security analysis
value: value:
@@ -245,6 +257,7 @@
display_title: 'Firebase Credentials' display_title: 'Firebase Credentials'
value: value:
locked: false locked: false
type: secret
description: 'Contents on your firebase credentials json file' description: 'Contents on your firebase credentials json file'
## ------ End of Configs added for FCM v1 notifications ------ ## ## ------ End of Configs added for FCM v1 notifications ------ ##
@@ -259,4 +272,5 @@
value: value:
locked: false locked: false
description: 'Linear client secret' description: 'Linear client secret'
type: secret
## ------ End of Configs added for Linear ------ ## ## ------ End of Configs added for Linear ------ ##
+2
View File
@@ -68,6 +68,8 @@ am:
invalid_operator: Invalid operator. The allowed operators for %{attribute_name} are [%{allowed_keys}]. invalid_operator: Invalid operator. The allowed operators for %{attribute_name} are [%{allowed_keys}].
invalid_query_operator: Query operator must be either "AND" or "OR". invalid_query_operator: Query operator must be either "AND" or "OR".
invalid_value: Invalid value. The values provided for %{attribute_name} are invalid invalid_value: Invalid value. The values provided for %{attribute_name} are invalid
custom_attribute_definition:
key_conflict: The provided key is not allowed as it might conflict with default attributes.
reports: reports:
period: Reporting period %{since} to %{until} period: Reporting period %{since} to %{until}
utc_warning: The report generated is in UTC timezone utc_warning: The report generated is in UTC timezone
+2
View File
@@ -68,6 +68,8 @@ ar:
invalid_operator: مشغل غير صالح. المشغل المسموح به لـ %{attribute_name} هو [%{allowed_keys}]. invalid_operator: مشغل غير صالح. المشغل المسموح به لـ %{attribute_name} هو [%{allowed_keys}].
invalid_query_operator: Query operator must be either "AND" or "OR". invalid_query_operator: Query operator must be either "AND" or "OR".
invalid_value: قيمة غير صالحة. القيم المقدمة ل %{attribute_name} غير صالحة invalid_value: قيمة غير صالحة. القيم المقدمة ل %{attribute_name} غير صالحة
custom_attribute_definition:
key_conflict: The provided key is not allowed as it might conflict with default attributes.
reports: reports:
period: فترة التبليغ %{since} إلى %{until} period: فترة التبليغ %{since} إلى %{until}
utc_warning: التقرير الذي تم إنشاؤه في التوقيت العالمي الموحّد utc_warning: التقرير الذي تم إنشاؤه في التوقيت العالمي الموحّد
+2
View File
@@ -68,6 +68,8 @@ az:
invalid_operator: Invalid operator. The allowed operators for %{attribute_name} are [%{allowed_keys}]. invalid_operator: Invalid operator. The allowed operators for %{attribute_name} are [%{allowed_keys}].
invalid_query_operator: Query operator must be either "AND" or "OR". invalid_query_operator: Query operator must be either "AND" or "OR".
invalid_value: Invalid value. The values provided for %{attribute_name} are invalid invalid_value: Invalid value. The values provided for %{attribute_name} are invalid
custom_attribute_definition:
key_conflict: The provided key is not allowed as it might conflict with default attributes.
reports: reports:
period: Reporting period %{since} to %{until} period: Reporting period %{since} to %{until}
utc_warning: The report generated is in UTC timezone utc_warning: The report generated is in UTC timezone
+2
View File
@@ -68,6 +68,8 @@ bg:
invalid_operator: Invalid operator. The allowed operators for %{attribute_name} are [%{allowed_keys}]. invalid_operator: Invalid operator. The allowed operators for %{attribute_name} are [%{allowed_keys}].
invalid_query_operator: Query operator must be either "AND" or "OR". invalid_query_operator: Query operator must be either "AND" or "OR".
invalid_value: Invalid value. The values provided for %{attribute_name} are invalid invalid_value: Invalid value. The values provided for %{attribute_name} are invalid
custom_attribute_definition:
key_conflict: The provided key is not allowed as it might conflict with default attributes.
reports: reports:
period: Reporting period %{since} to %{until} period: Reporting period %{since} to %{until}
utc_warning: The report generated is in UTC timezone utc_warning: The report generated is in UTC timezone
+2
View File
@@ -68,6 +68,8 @@ ca:
invalid_operator: Operador no vàlid. Els operadors permesos per a %{attribute_name} son [%{allowed_keys}]. invalid_operator: Operador no vàlid. Els operadors permesos per a %{attribute_name} son [%{allowed_keys}].
invalid_query_operator: Query operator must be either "AND" or "OR". invalid_query_operator: Query operator must be either "AND" or "OR".
invalid_value: Valor no vàlid. Els valors proporcionats per a %{attribute_name} no són vàlids invalid_value: Valor no vàlid. Els valors proporcionats per a %{attribute_name} no són vàlids
custom_attribute_definition:
key_conflict: The provided key is not allowed as it might conflict with default attributes.
reports: reports:
period: Període d'informes %{since} a %{until} period: Període d'informes %{since} a %{until}
utc_warning: L'informe generat es troba a la zona horària UTC utc_warning: L'informe generat es troba a la zona horària UTC
+2
View File
@@ -68,6 +68,8 @@ cs:
invalid_operator: Invalid operator. The allowed operators for %{attribute_name} are [%{allowed_keys}]. invalid_operator: Invalid operator. The allowed operators for %{attribute_name} are [%{allowed_keys}].
invalid_query_operator: Query operator must be either "AND" or "OR". invalid_query_operator: Query operator must be either "AND" or "OR".
invalid_value: Invalid value. The values provided for %{attribute_name} are invalid invalid_value: Invalid value. The values provided for %{attribute_name} are invalid
custom_attribute_definition:
key_conflict: The provided key is not allowed as it might conflict with default attributes.
reports: reports:
period: Reporting period %{since} to %{until} period: Reporting period %{since} to %{until}
utc_warning: The report generated is in UTC timezone utc_warning: The report generated is in UTC timezone
+2
View File
@@ -68,6 +68,8 @@ da:
invalid_operator: Invalid operator. The allowed operators for %{attribute_name} are [%{allowed_keys}]. invalid_operator: Invalid operator. The allowed operators for %{attribute_name} are [%{allowed_keys}].
invalid_query_operator: Query operator must be either "AND" or "OR". invalid_query_operator: Query operator must be either "AND" or "OR".
invalid_value: Invalid value. The values provided for %{attribute_name} are invalid invalid_value: Invalid value. The values provided for %{attribute_name} are invalid
custom_attribute_definition:
key_conflict: The provided key is not allowed as it might conflict with default attributes.
reports: reports:
period: Rapporteringsperiode %{since} til %{until} period: Rapporteringsperiode %{since} til %{until}
utc_warning: The report generated is in UTC timezone utc_warning: The report generated is in UTC timezone
+2
View File
@@ -68,6 +68,8 @@ de:
invalid_operator: Ungültiger Operator. Die erlaubten Operatoren für %{attribute_name} sind [%{allowed_keys}]. invalid_operator: Ungültiger Operator. Die erlaubten Operatoren für %{attribute_name} sind [%{allowed_keys}].
invalid_query_operator: Query operator must be either "AND" or "OR". invalid_query_operator: Query operator must be either "AND" or "OR".
invalid_value: Ungültiger Wert. Die Werte für %{attribute_name} sind ungültig invalid_value: Ungültiger Wert. Die Werte für %{attribute_name} sind ungültig
custom_attribute_definition:
key_conflict: The provided key is not allowed as it might conflict with default attributes.
reports: reports:
period: Berichtszeitraum von %{since} bis %{until} period: Berichtszeitraum von %{since} bis %{until}
utc_warning: Der generierte Bericht ist in UTC-Zeitzone utc_warning: Der generierte Bericht ist in UTC-Zeitzone
+2
View File
@@ -68,6 +68,8 @@ el:
invalid_operator: Invalid operator. The allowed operators for %{attribute_name} are [%{allowed_keys}]. invalid_operator: Invalid operator. The allowed operators for %{attribute_name} are [%{allowed_keys}].
invalid_query_operator: Query operator must be either "AND" or "OR". invalid_query_operator: Query operator must be either "AND" or "OR".
invalid_value: Invalid value. The values provided for %{attribute_name} are invalid invalid_value: Invalid value. The values provided for %{attribute_name} are invalid
custom_attribute_definition:
key_conflict: The provided key is not allowed as it might conflict with default attributes.
reports: reports:
period: Περίοδος αναφοράς %{since} έως %{until} period: Περίοδος αναφοράς %{since} έως %{until}
utc_warning: The report generated is in UTC timezone utc_warning: The report generated is in UTC timezone
+2
View File
@@ -82,6 +82,8 @@ en:
invalid_operator: Invalid operator. The allowed operators for %{attribute_name} are [%{allowed_keys}]. invalid_operator: Invalid operator. The allowed operators for %{attribute_name} are [%{allowed_keys}].
invalid_query_operator: Query operator must be either "AND" or "OR". invalid_query_operator: Query operator must be either "AND" or "OR".
invalid_value: Invalid value. The values provided for %{attribute_name} are invalid invalid_value: Invalid value. The values provided for %{attribute_name} are invalid
custom_attribute_definition:
key_conflict: The provided key is not allowed as it might conflict with default attributes.
reports: reports:
period: Reporting period %{since} to %{until} period: Reporting period %{since} to %{until}
utc_warning: The report generated is in UTC timezone utc_warning: The report generated is in UTC timezone
+2
View File
@@ -68,6 +68,8 @@ es:
invalid_operator: Operador no válido. Los operadores permitidos para %{attribute_name} son [%{allowed_keys}]. invalid_operator: Operador no válido. Los operadores permitidos para %{attribute_name} son [%{allowed_keys}].
invalid_query_operator: El operador de consulta debe ser "Y" o "O". invalid_query_operator: El operador de consulta debe ser "Y" o "O".
invalid_value: Valor no válido. Los valores proporcionados para %{attribute_name} no son válidos invalid_value: Valor no válido. Los valores proporcionados para %{attribute_name} no son válidos
custom_attribute_definition:
key_conflict: The provided key is not allowed as it might conflict with default attributes.
reports: reports:
period: Reportando el periodo desde %{since} hasta %{until} period: Reportando el periodo desde %{since} hasta %{until}
utc_warning: El informe generado está en zona horaria UTC utc_warning: El informe generado está en zona horaria UTC
+2
View File
@@ -68,6 +68,8 @@ fa:
invalid_operator: این عملیات مجاز نیست. عملیات های مجاز برای %{attribute_name} شامل %{allowed_keys} می باشد. invalid_operator: این عملیات مجاز نیست. عملیات های مجاز برای %{attribute_name} شامل %{allowed_keys} می باشد.
invalid_query_operator: Query operator must be either "AND" or "OR". invalid_query_operator: Query operator must be either "AND" or "OR".
invalid_value: مقدار معتبر نیست. مقادیر ارائه شده برای %{attribute_name} معتبر نیست invalid_value: مقدار معتبر نیست. مقادیر ارائه شده برای %{attribute_name} معتبر نیست
custom_attribute_definition:
key_conflict: The provided key is not allowed as it might conflict with default attributes.
reports: reports:
period: زمان گزارش از %{since} تا %{until} period: زمان گزارش از %{since} تا %{until}
utc_warning: گزارش تولید شده در منطقه زمانی UTC است utc_warning: گزارش تولید شده در منطقه زمانی UTC است
+2
View File
@@ -68,6 +68,8 @@ fi:
invalid_operator: Invalid operator. The allowed operators for %{attribute_name} are [%{allowed_keys}]. invalid_operator: Invalid operator. The allowed operators for %{attribute_name} are [%{allowed_keys}].
invalid_query_operator: Query operator must be either "AND" or "OR". invalid_query_operator: Query operator must be either "AND" or "OR".
invalid_value: Invalid value. The values provided for %{attribute_name} are invalid invalid_value: Invalid value. The values provided for %{attribute_name} are invalid
custom_attribute_definition:
key_conflict: The provided key is not allowed as it might conflict with default attributes.
reports: reports:
period: Raportointijakso %{since} %{until} period: Raportointijakso %{since} %{until}
utc_warning: The report generated is in UTC timezone utc_warning: The report generated is in UTC timezone
+2
View File
@@ -68,6 +68,8 @@ fr:
invalid_operator: Invalid operator. The allowed operators for %{attribute_name} are [%{allowed_keys}]. invalid_operator: Invalid operator. The allowed operators for %{attribute_name} are [%{allowed_keys}].
invalid_query_operator: Query operator must be either "AND" or "OR". invalid_query_operator: Query operator must be either "AND" or "OR".
invalid_value: Invalid value. The values provided for %{attribute_name} are invalid invalid_value: Invalid value. The values provided for %{attribute_name} are invalid
custom_attribute_definition:
key_conflict: The provided key is not allowed as it might conflict with default attributes.
reports: reports:
period: Période de rapport %{since} à %{until} period: Période de rapport %{since} à %{until}
utc_warning: Le rapport généré est dans le fuseau horaire UTC utc_warning: Le rapport généré est dans le fuseau horaire UTC
+2
View File
@@ -68,6 +68,8 @@ he:
invalid_operator: Invalid operator. The allowed operators for %{attribute_name} are [%{allowed_keys}]. invalid_operator: Invalid operator. The allowed operators for %{attribute_name} are [%{allowed_keys}].
invalid_query_operator: Query operator must be either "AND" or "OR". invalid_query_operator: Query operator must be either "AND" or "OR".
invalid_value: Invalid value. The values provided for %{attribute_name} are invalid invalid_value: Invalid value. The values provided for %{attribute_name} are invalid
custom_attribute_definition:
key_conflict: The provided key is not allowed as it might conflict with default attributes.
reports: reports:
period: Reporting period %{since} to %{until} period: Reporting period %{since} to %{until}
utc_warning: The report generated is in UTC timezone utc_warning: The report generated is in UTC timezone
+2
View File
@@ -68,6 +68,8 @@ hi:
invalid_operator: Invalid operator. The allowed operators for %{attribute_name} are [%{allowed_keys}]. invalid_operator: Invalid operator. The allowed operators for %{attribute_name} are [%{allowed_keys}].
invalid_query_operator: Query operator must be either "AND" or "OR". invalid_query_operator: Query operator must be either "AND" or "OR".
invalid_value: Invalid value. The values provided for %{attribute_name} are invalid invalid_value: Invalid value. The values provided for %{attribute_name} are invalid
custom_attribute_definition:
key_conflict: The provided key is not allowed as it might conflict with default attributes.
reports: reports:
period: Reporting period %{since} to %{until} period: Reporting period %{since} to %{until}
utc_warning: The report generated is in UTC timezone utc_warning: The report generated is in UTC timezone
+2
View File
@@ -68,6 +68,8 @@ hr:
invalid_operator: Invalid operator. The allowed operators for %{attribute_name} are [%{allowed_keys}]. invalid_operator: Invalid operator. The allowed operators for %{attribute_name} are [%{allowed_keys}].
invalid_query_operator: Query operator must be either "AND" or "OR". invalid_query_operator: Query operator must be either "AND" or "OR".
invalid_value: Invalid value. The values provided for %{attribute_name} are invalid invalid_value: Invalid value. The values provided for %{attribute_name} are invalid
custom_attribute_definition:
key_conflict: The provided key is not allowed as it might conflict with default attributes.
reports: reports:
period: Reporting period %{since} to %{until} period: Reporting period %{since} to %{until}
utc_warning: The report generated is in UTC timezone utc_warning: The report generated is in UTC timezone
+2
View File
@@ -68,6 +68,8 @@ hu:
invalid_operator: Invalid operator. The allowed operators for %{attribute_name} are [%{allowed_keys}]. invalid_operator: Invalid operator. The allowed operators for %{attribute_name} are [%{allowed_keys}].
invalid_query_operator: Query operator must be either "AND" or "OR". invalid_query_operator: Query operator must be either "AND" or "OR".
invalid_value: Invalid value. The values provided for %{attribute_name} are invalid invalid_value: Invalid value. The values provided for %{attribute_name} are invalid
custom_attribute_definition:
key_conflict: The provided key is not allowed as it might conflict with default attributes.
reports: reports:
period: Jelentési időszak %{since}-tól %{until}-ig period: Jelentési időszak %{since}-tól %{until}-ig
utc_warning: A generált riport UTC időzónát használ utc_warning: A generált riport UTC időzónát használ
+2
View File
@@ -68,6 +68,8 @@ hy:
invalid_operator: Invalid operator. The allowed operators for %{attribute_name} are [%{allowed_keys}]. invalid_operator: Invalid operator. The allowed operators for %{attribute_name} are [%{allowed_keys}].
invalid_query_operator: Query operator must be either "AND" or "OR". invalid_query_operator: Query operator must be either "AND" or "OR".
invalid_value: Invalid value. The values provided for %{attribute_name} are invalid invalid_value: Invalid value. The values provided for %{attribute_name} are invalid
custom_attribute_definition:
key_conflict: The provided key is not allowed as it might conflict with default attributes.
reports: reports:
period: Reporting period %{since} to %{until} period: Reporting period %{since} to %{until}
utc_warning: The report generated is in UTC timezone utc_warning: The report generated is in UTC timezone
+2
View File
@@ -68,6 +68,8 @@ id:
invalid_operator: Invalid operator. The allowed operators for %{attribute_name} are [%{allowed_keys}]. invalid_operator: Invalid operator. The allowed operators for %{attribute_name} are [%{allowed_keys}].
invalid_query_operator: Query operator must be either "AND" or "OR". invalid_query_operator: Query operator must be either "AND" or "OR".
invalid_value: Invalid value. The values provided for %{attribute_name} are invalid invalid_value: Invalid value. The values provided for %{attribute_name} are invalid
custom_attribute_definition:
key_conflict: The provided key is not allowed as it might conflict with default attributes.
reports: reports:
period: Periode pelaporan %{since} hingga %{until} period: Periode pelaporan %{since} hingga %{until}
utc_warning: Laporan yang dihasilkan berada dalam zona waktu UTC utc_warning: Laporan yang dihasilkan berada dalam zona waktu UTC
+2
View File
@@ -68,6 +68,8 @@ is:
invalid_operator: Invalid operator. The allowed operators for %{attribute_name} are [%{allowed_keys}]. invalid_operator: Invalid operator. The allowed operators for %{attribute_name} are [%{allowed_keys}].
invalid_query_operator: Query operator must be either "AND" or "OR". invalid_query_operator: Query operator must be either "AND" or "OR".
invalid_value: Invalid value. The values provided for %{attribute_name} are invalid invalid_value: Invalid value. The values provided for %{attribute_name} are invalid
custom_attribute_definition:
key_conflict: The provided key is not allowed as it might conflict with default attributes.
reports: reports:
period: Reporting period %{since} to %{until} period: Reporting period %{since} to %{until}
utc_warning: The report generated is in UTC timezone utc_warning: The report generated is in UTC timezone
+2
View File
@@ -68,6 +68,8 @@ it:
invalid_operator: Invalid operator. The allowed operators for %{attribute_name} are [%{allowed_keys}]. invalid_operator: Invalid operator. The allowed operators for %{attribute_name} are [%{allowed_keys}].
invalid_query_operator: Query operator must be either "AND" or "OR". invalid_query_operator: Query operator must be either "AND" or "OR".
invalid_value: Invalid value. The values provided for %{attribute_name} are invalid invalid_value: Invalid value. The values provided for %{attribute_name} are invalid
custom_attribute_definition:
key_conflict: The provided key is not allowed as it might conflict with default attributes.
reports: reports:
period: Periodo di segnalazione da %{since} a %{until} period: Periodo di segnalazione da %{since} a %{until}
utc_warning: The report generated is in UTC timezone utc_warning: The report generated is in UTC timezone
+2
View File
@@ -68,6 +68,8 @@ ja:
invalid_operator: 無効な演算子です。%{attribute_name} に許可されている演算子は [%{allowed_keys}] です。 invalid_operator: 無効な演算子です。%{attribute_name} に許可されている演算子は [%{allowed_keys}] です。
invalid_query_operator: クエリ演算子は "AND" または "OR" でなければなりません。 invalid_query_operator: クエリ演算子は "AND" または "OR" でなければなりません。
invalid_value: 無効な値です。%{attribute_name} に提供された値は無効です。 invalid_value: 無効な値です。%{attribute_name} に提供された値は無効です。
custom_attribute_definition:
key_conflict: The provided key is not allowed as it might conflict with default attributes.
reports: reports:
period: レポート期間 %{since} から %{until} まで period: レポート期間 %{since} から %{until} まで
utc_warning: 生成されたレポートはUTCタイムゾーンです utc_warning: 生成されたレポートはUTCタイムゾーンです
+2
View File
@@ -68,6 +68,8 @@ ka:
invalid_operator: Invalid operator. The allowed operators for %{attribute_name} are [%{allowed_keys}]. invalid_operator: Invalid operator. The allowed operators for %{attribute_name} are [%{allowed_keys}].
invalid_query_operator: Query operator must be either "AND" or "OR". invalid_query_operator: Query operator must be either "AND" or "OR".
invalid_value: Invalid value. The values provided for %{attribute_name} are invalid invalid_value: Invalid value. The values provided for %{attribute_name} are invalid
custom_attribute_definition:
key_conflict: The provided key is not allowed as it might conflict with default attributes.
reports: reports:
period: Reporting period %{since} to %{until} period: Reporting period %{since} to %{until}
utc_warning: The report generated is in UTC timezone utc_warning: The report generated is in UTC timezone
+2
View File
@@ -68,6 +68,8 @@ ko:
invalid_operator: Invalid operator. The allowed operators for %{attribute_name} are [%{allowed_keys}]. invalid_operator: Invalid operator. The allowed operators for %{attribute_name} are [%{allowed_keys}].
invalid_query_operator: Query operator must be either "AND" or "OR". invalid_query_operator: Query operator must be either "AND" or "OR".
invalid_value: Invalid value. The values provided for %{attribute_name} are invalid invalid_value: Invalid value. The values provided for %{attribute_name} are invalid
custom_attribute_definition:
key_conflict: The provided key is not allowed as it might conflict with default attributes.
reports: reports:
period: 보고 기간 %{since} - %{until} period: 보고 기간 %{since} - %{until}
utc_warning: The report generated is in UTC timezone utc_warning: The report generated is in UTC timezone
+2
View File
@@ -68,6 +68,8 @@ lt:
invalid_operator: Invalid operator. The allowed operators for %{attribute_name} are [%{allowed_keys}]. invalid_operator: Invalid operator. The allowed operators for %{attribute_name} are [%{allowed_keys}].
invalid_query_operator: Query operator must be either "AND" or "OR". invalid_query_operator: Query operator must be either "AND" or "OR".
invalid_value: Invalid value. The values provided for %{attribute_name} are invalid invalid_value: Invalid value. The values provided for %{attribute_name} are invalid
custom_attribute_definition:
key_conflict: The provided key is not allowed as it might conflict with default attributes.
reports: reports:
period: Ataskaitinis laikotarpis nuo %{since} iki %{until} period: Ataskaitinis laikotarpis nuo %{since} iki %{until}
utc_warning: Sugeneruota ataskaita yra UTC laiko juostoje utc_warning: Sugeneruota ataskaita yra UTC laiko juostoje
+2
View File
@@ -68,6 +68,8 @@ lv:
invalid_operator: Nederīgs operators. Atļautie operatori priekš %{attribute_name} ir [%{allowed_keys}]. invalid_operator: Nederīgs operators. Atļautie operatori priekš %{attribute_name} ir [%{allowed_keys}].
invalid_query_operator: Vaicājuma operatoram ir jābūt "UN" vai "VAI". invalid_query_operator: Vaicājuma operatoram ir jābūt "UN" vai "VAI".
invalid_value: Nederīga vērtība. Norādītās vērtības priekš %{attribute_name} nav derīgas invalid_value: Nederīga vērtība. Norādītās vērtības priekš %{attribute_name} nav derīgas
custom_attribute_definition:
key_conflict: The provided key is not allowed as it might conflict with default attributes.
reports: reports:
period: Ziņošanas periods %{since} līdz %{until} period: Ziņošanas periods %{since} līdz %{until}
utc_warning: Izveidotais pārskats atbilst UTC laika joslai utc_warning: Izveidotais pārskats atbilst UTC laika joslai
+2
View File
@@ -68,6 +68,8 @@ ml:
invalid_operator: Invalid operator. The allowed operators for %{attribute_name} are [%{allowed_keys}]. invalid_operator: Invalid operator. The allowed operators for %{attribute_name} are [%{allowed_keys}].
invalid_query_operator: Query operator must be either "AND" or "OR". invalid_query_operator: Query operator must be either "AND" or "OR".
invalid_value: Invalid value. The values provided for %{attribute_name} are invalid invalid_value: Invalid value. The values provided for %{attribute_name} are invalid
custom_attribute_definition:
key_conflict: The provided key is not allowed as it might conflict with default attributes.
reports: reports:
period: Reporting period %{since} to %{until} period: Reporting period %{since} to %{until}
utc_warning: The report generated is in UTC timezone utc_warning: The report generated is in UTC timezone
+2
View File
@@ -68,6 +68,8 @@ ms:
invalid_operator: Invalid operator. The allowed operators for %{attribute_name} are [%{allowed_keys}]. invalid_operator: Invalid operator. The allowed operators for %{attribute_name} are [%{allowed_keys}].
invalid_query_operator: Query operator must be either "AND" or "OR". invalid_query_operator: Query operator must be either "AND" or "OR".
invalid_value: Invalid value. The values provided for %{attribute_name} are invalid invalid_value: Invalid value. The values provided for %{attribute_name} are invalid
custom_attribute_definition:
key_conflict: The provided key is not allowed as it might conflict with default attributes.
reports: reports:
period: Reporting period %{since} to %{until} period: Reporting period %{since} to %{until}
utc_warning: The report generated is in UTC timezone utc_warning: The report generated is in UTC timezone
+2
View File
@@ -68,6 +68,8 @@ ne:
invalid_operator: Invalid operator. The allowed operators for %{attribute_name} are [%{allowed_keys}]. invalid_operator: Invalid operator. The allowed operators for %{attribute_name} are [%{allowed_keys}].
invalid_query_operator: Query operator must be either "AND" or "OR". invalid_query_operator: Query operator must be either "AND" or "OR".
invalid_value: Invalid value. The values provided for %{attribute_name} are invalid invalid_value: Invalid value. The values provided for %{attribute_name} are invalid
custom_attribute_definition:
key_conflict: The provided key is not allowed as it might conflict with default attributes.
reports: reports:
period: Reporting period %{since} to %{until} period: Reporting period %{since} to %{until}
utc_warning: The report generated is in UTC timezone utc_warning: The report generated is in UTC timezone

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