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 1539 additions and 543 deletions
+1 -1
View File
@@ -16,7 +16,7 @@ on:
jobs:
nightly:
runs-on: ubuntu-20.04
runs-on: ubuntu-24.04
steps:
- name: get installer
+2 -2
View File
@@ -561,7 +561,7 @@ GEM
activesupport (>= 3.0.0)
raabro (1.4.0)
racc (1.8.1)
rack (2.2.11)
rack (2.2.12)
rack-attack (6.7.0)
rack (>= 1.0, < 4)
rack-contrib (2.5.0)
@@ -799,7 +799,7 @@ GEM
unf_ext (0.0.8.2)
unicode-display_width (2.4.2)
uniform_notifier (1.16.0)
uri (0.13.0)
uri (1.0.3)
uri_template (0.7.0)
valid_email2 (5.2.6)
activemodel (>= 3.2)
@@ -1,8 +1,8 @@
button,
input[type="button"],
input[type="reset"],
input[type="submit"],
.button {
button:not(.reset-base),
input[type='button']:not(.reset-base),
input[type='reset']:not(.reset-base),
input[type='submit']:not(.reset-base),
.button:not(.reset-base) {
appearance: none;
background-color: $color-woot;
border: 0;
@@ -10,7 +10,6 @@
.icon-container {
margin-right: 2px;
}
.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 :inbox, :contact, :contact_inbox, only: [:create]
ATTACHMENT_RESULTS_PER_PAGE = 100
def index
result = conversation_finder.perform
@conversations = result[:conversations]
@@ -24,7 +26,12 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
end
def attachments
@attachments_count = @conversation.attachments.count
@attachments = @conversation.attachments
.includes(:message)
.order(created_at: :desc)
.page(attachment_params[:page])
.per(ATTACHMENT_RESULTS_PER_PAGE)
end
def show; end
@@ -124,6 +131,10 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
params.permit(:priority)
end
def attachment_params
params.permit(:page)
end
def update_last_seen_on_conversation(last_seen_at, update_assignee)
# rubocop:disable Rails/SkipsModelValidations
@conversation.update_column(:agent_last_seen_at, last_seen_at)
+19 -14
View File
@@ -5,10 +5,11 @@ module SwitchLocale
def switch_locale(&)
# 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
# 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, &)
end
@@ -32,26 +33,30 @@ module SwitchLocale
end
def set_locale(locale, &)
# if locale is empty, use default_locale
locale ||= I18n.default_locale
safe_locale = validate_and_get_locale(locale)
# Ensure locale won't bleed into other requests
# https://guides.rubyonrails.org/i18n.html#managing-the-locale-across-requests
I18n.with_locale(locale, &)
I18n.with_locale(safe_locale, &)
end
def locale_from_params
I18n.available_locales.map(&:to_s).include?(params[:locale]) ? params[:locale] : nil
def validate_and_get_locale(locale)
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
def locale_from_account(account)
return unless account
I18n.available_locales.map(&:to_s).include?(account.locale) ? account.locale : nil
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
account.locale
end
end
@@ -1,4 +1,6 @@
class Public::Api::V1::Portals::BaseController < PublicController
include SwitchLocale
before_action :show_plain_layout
before_action :set_color_scheme
before_action :set_global_config
@@ -27,14 +29,7 @@ class Public::Api::V1::Portals::BaseController < PublicController
end
def switch_locale_with_portal(&)
locale_without_variant = params[:locale].split('_')[0]
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
@locale = validate_and_get_locale(params[:locale])
I18n.with_locale(@locale, &)
end
@@ -44,12 +39,12 @@ class Public::Api::V1::Portals::BaseController < PublicController
Rails.logger.info "Article: not found for slug: #{params[:article_slug]}"
render_404 && return if article.blank?
@locale = if article.category.present?
article.category.locale
else
article.portal.default_locale
end
article_locale = if article.category.present?
article.category.locale
else
article.portal.default_locale
end
@locale = validate_and_get_locale(article_locale)
I18n.with_locale(@locale, &)
end
+5 -1
View File
@@ -78,7 +78,11 @@ class AccountDashboard < Administrate::BaseDashboard
# COLLECTION_FILTERS = {
# open: ->(resources) { resources.where(open: true) }
# }.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
# across all pages of the admin dashboard.
+6 -1
View File
@@ -94,7 +94,12 @@ class UserDashboard < Administrate::BaseDashboard
# COLLECTION_FILTERS = {
# open: ->(resources) { resources.where(open: true) }
# }.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
# 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)
current_filter = model_filters[query_hash['attribute_key']]
@@ -89,4 +89,18 @@ module FilterHelper
operator = condition['query_operator'].upcase
raise CustomExceptions::CustomFilter::InvalidQueryOperator.new({}) unless %w[AND OR].include?(operator)
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
@@ -146,17 +146,19 @@
--solid-2: 255 255 255;
--solid-3: 255 255 255;
--solid-active: 255 255 255;
--solid-amber: 252 232 193;
--solid-blue: 218 236 255;
--solid-amber: 255 228 181;
--solid-blue: 182 216 255;
--solid-iris: 230 231 255;
--solid-purple: 202 202 255;
--solid-red: 255 212 219;
--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;
--black-alpha-1: 0, 0, 0, 0.12;
--black-alpha-1: 0, 0, 0, 0.08;
--black-alpha-2: 0, 0, 0, 0.04;
--border-blue: 39, 129, 246, 0.5;
--white-alpha: 255, 255, 255, 0.8;
--white-alpha: 255, 255, 255, 0.84;
}
.dark {
@@ -240,16 +242,18 @@
--gray-12: 238 238 238;
--background-color: 18 18 19;
--text-blue: 126 182 255;
--border-strong: 52 52 52;
--border-weak: 38 38 42;
--solid-1: 23 23 26;
--solid-2: 29 30 36;
--solid-3: 44 45 54;
--solid-active: 53 57 66;
--solid-amber: 42 37 30;
--solid-blue: 16 49 91;
--solid-amber: 56 50 41;
--solid-blue: 4 51 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-2: 139, 147, 182, 0.15;
@@ -50,16 +50,16 @@ const AVATAR_COLORS = {
dark: [
['#4B143D', '#FF8DCC'],
['#3F220D', '#FFA366'],
['#2A2A2A', '#ADB1B8'],
['#023B37', '#0BD8B6'],
['#2A2A2A', '#ADB1B8'],
['#27264D', '#A19EFF'],
['#1D2E62', '#9EB1FF'],
],
light: [
['#FBDCEF', '#C2298A'],
['#FFE0BB', '#99543A'],
['#E8E8E8', '#60646C'],
['#CCF3EA', '#008573'],
['#E8E8E8', '#60646C'],
['#EBEBFE', '#4747C2'],
['#E1E9FF', '#3A5BC7'],
],
@@ -2,7 +2,10 @@ import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useOperators } from './operators';
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';
/**
@@ -59,7 +62,11 @@ export function useContactFilterContext() {
* @type {import('vue').ComputedRef<FilterType[]>}
*/
const customFilterTypes = computed(() =>
buildAttributesFilterTypes(contactAttributes.value, getOperatorTypes)
buildAttributesFilterTypes(
contactAttributes.value,
getOperatorTypes,
'contact'
)
);
/**
@@ -67,8 +74,8 @@ export function useContactFilterContext() {
*/
const filterTypes = computed(() => [
{
attributeKey: 'name',
value: 'name',
attributeKey: CONTACT_ATTRIBUTES.NAME,
value: CONTACT_ATTRIBUTES.NAME,
attributeName: t('CONTACTS_LAYOUT.FILTER.NAME'),
label: t('CONTACTS_LAYOUT.FILTER.NAME'),
inputType: 'plainText',
@@ -77,8 +84,8 @@ export function useContactFilterContext() {
attributeModel: 'standard',
},
{
attributeKey: 'email',
value: 'email',
attributeKey: CONTACT_ATTRIBUTES.EMAIL,
value: CONTACT_ATTRIBUTES.EMAIL,
attributeName: t('CONTACTS_LAYOUT.FILTER.EMAIL'),
label: t('CONTACTS_LAYOUT.FILTER.EMAIL'),
inputType: 'plainText',
@@ -87,8 +94,8 @@ export function useContactFilterContext() {
attributeModel: 'standard',
},
{
attributeKey: 'phone_number',
value: 'phone_number',
attributeKey: CONTACT_ATTRIBUTES.PHONE_NUMBER,
value: CONTACT_ATTRIBUTES.PHONE_NUMBER,
attributeName: t('CONTACTS_LAYOUT.FILTER.PHONE_NUMBER'),
label: t('CONTACTS_LAYOUT.FILTER.PHONE_NUMBER'),
inputType: 'plainText',
@@ -97,8 +104,8 @@ export function useContactFilterContext() {
attributeModel: 'standard',
},
{
attributeKey: 'identifier',
value: 'identifier',
attributeKey: CONTACT_ATTRIBUTES.IDENTIFIER,
value: CONTACT_ATTRIBUTES.IDENTIFIER,
attributeName: t('CONTACTS_LAYOUT.FILTER.IDENTIFIER'),
label: t('CONTACTS_LAYOUT.FILTER.IDENTIFIER'),
inputType: 'plainText',
@@ -107,8 +114,8 @@ export function useContactFilterContext() {
attributeModel: 'standard',
},
{
attributeKey: 'country_code',
value: 'country_code',
attributeKey: CONTACT_ATTRIBUTES.COUNTRY_CODE,
value: CONTACT_ATTRIBUTES.COUNTRY_CODE,
attributeName: t('FILTER.ATTRIBUTES.COUNTRY_NAME'),
label: t('FILTER.ATTRIBUTES.COUNTRY_NAME'),
inputType: 'searchSelect',
@@ -118,8 +125,8 @@ export function useContactFilterContext() {
attributeModel: 'additional',
},
{
attributeKey: 'city',
value: 'city',
attributeKey: CONTACT_ATTRIBUTES.CITY,
value: CONTACT_ATTRIBUTES.CITY,
attributeName: t('CONTACTS_LAYOUT.FILTER.CITY'),
label: t('CONTACTS_LAYOUT.FILTER.CITY'),
inputType: 'plainText',
@@ -128,8 +135,8 @@ export function useContactFilterContext() {
attributeModel: 'standard',
},
{
attributeKey: 'created_at',
value: 'created_at',
attributeKey: CONTACT_ATTRIBUTES.CREATED_AT,
value: CONTACT_ATTRIBUTES.CREATED_AT,
attributeName: t('CONTACTS_LAYOUT.FILTER.CREATED_AT'),
label: t('CONTACTS_LAYOUT.FILTER.CREATED_AT'),
inputType: 'date',
@@ -138,8 +145,8 @@ export function useContactFilterContext() {
attributeModel: 'standard',
},
{
attributeKey: 'last_activity_at',
value: 'last_activity_at',
attributeKey: CONTACT_ATTRIBUTES.LAST_ACTIVITY_AT,
value: CONTACT_ATTRIBUTES.LAST_ACTIVITY_AT,
attributeName: t('CONTACTS_LAYOUT.FILTER.LAST_ACTIVITY'),
label: t('CONTACTS_LAYOUT.FILTER.LAST_ACTIVITY'),
inputType: 'date',
@@ -148,8 +155,8 @@ export function useContactFilterContext() {
attributeModel: 'standard',
},
{
attributeKey: 'referer',
value: 'referer',
attributeKey: CONTACT_ATTRIBUTES.REFERER,
value: CONTACT_ATTRIBUTES.REFERER,
attributeName: t('CONTACTS_LAYOUT.FILTER.REFERER_LINK'),
label: t('CONTACTS_LAYOUT.FILTER.REFERER_LINK'),
inputType: 'plainText',
@@ -158,8 +165,8 @@ export function useContactFilterContext() {
attributeModel: 'standard',
},
{
attributeKey: 'blocked',
value: 'blocked',
attributeKey: CONTACT_ATTRIBUTES.BLOCKED,
value: CONTACT_ATTRIBUTES.BLOCKED,
attributeName: t('CONTACTS_LAYOUT.FILTER.BLOCKED'),
label: t('CONTACTS_LAYOUT.FILTER.BLOCKED'),
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
* @param {string} key - The attribute display type key
@@ -20,24 +52,37 @@ export const getCustomAttributeInputType = key => {
/**
* Builds filter types for custom attributes
* This also removes any conflicting attributes
* @param {Array} attributes - The attributes array
* @param {Function} getOperatorTypes - Function to get operator types
* @returns {Array} Array of filter types
*/
export const buildAttributesFilterTypes = (attributes, getOperatorTypes) => {
return attributes.map(attr => ({
attributeKey: attr.attributeKey,
value: attr.attributeKey,
attributeName: attr.attributeDisplayName,
label: attr.attributeDisplayName,
inputType: getCustomAttributeInputType(attr.attributeDisplayType),
filterOperators: getOperatorTypes(attr.attributeDisplayType),
options:
attr.attributeDisplayType === 'list'
? attr.attributeValues.map(item => ({ id: item, name: item }))
: [],
attributeModel: 'customAttributes',
}));
export const buildAttributesFilterTypes = (
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,
value: attr.attributeKey,
attributeName: attr.attributeDisplayName,
label: attr.attributeDisplayName,
inputType: getCustomAttributeInputType(attr.attributeDisplayType),
filterOperators: getOperatorTypes(attr.attributeDisplayType),
options:
attr.attributeDisplayType === 'list'
? attr.attributeValues.map(item => ({ id: item, name: item }))
: [],
attributeModel: 'customAttributes',
}));
};
/**
@@ -3,7 +3,10 @@ import { useI18n } from 'vue-i18n';
import { useOperators } from './operators';
import { useMapGetter } from 'dashboard/composables/store.js';
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 languages from 'dashboard/components/widgets/conversation/advancedFilterItems/languages.js';
@@ -70,7 +73,11 @@ export function useConversationFilterContext() {
* @type {import('vue').ComputedRef<FilterType[]>}
*/
const customFilterTypes = computed(() =>
buildAttributesFilterTypes(conversationAttributes.value, getOperatorTypes)
buildAttributesFilterTypes(
conversationAttributes.value,
getOperatorTypes,
'conversation'
)
);
/**
@@ -78,8 +85,8 @@ export function useConversationFilterContext() {
*/
const filterTypes = computed(() => [
{
attributeKey: 'status',
value: 'status',
attributeKey: CONVERSATION_ATTRIBUTES.STATUS,
value: CONVERSATION_ATTRIBUTES.STATUS,
attributeName: t('FILTER.ATTRIBUTES.STATUS'),
label: t('FILTER.ATTRIBUTES.STATUS'),
inputType: 'multiSelect',
@@ -94,8 +101,24 @@ export function useConversationFilterContext() {
attributeModel: 'standard',
},
{
attributeKey: 'assignee_id',
value: 'assignee_id',
attributeKey: CONVERSATION_ATTRIBUTES.PRIORITY,
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'),
label: t('FILTER.ATTRIBUTES.ASSIGNEE_NAME'),
inputType: 'searchSelect',
@@ -110,8 +133,8 @@ export function useConversationFilterContext() {
attributeModel: 'standard',
},
{
attributeKey: 'inbox_id',
value: 'inbox_id',
attributeKey: CONVERSATION_ATTRIBUTES.INBOX_ID,
value: CONVERSATION_ATTRIBUTES.INBOX_ID,
attributeName: t('FILTER.ATTRIBUTES.INBOX_NAME'),
label: t('FILTER.ATTRIBUTES.INBOX_NAME'),
inputType: 'searchSelect',
@@ -126,8 +149,8 @@ export function useConversationFilterContext() {
attributeModel: 'standard',
},
{
attributeKey: 'team_id',
value: 'team_id',
attributeKey: CONVERSATION_ATTRIBUTES.TEAM_ID,
value: CONVERSATION_ATTRIBUTES.TEAM_ID,
attributeName: t('FILTER.ATTRIBUTES.TEAM_NAME'),
label: t('FILTER.ATTRIBUTES.TEAM_NAME'),
inputType: 'searchSelect',
@@ -137,8 +160,8 @@ export function useConversationFilterContext() {
attributeModel: 'standard',
},
{
attributeKey: 'display_id',
value: 'display_id',
attributeKey: CONVERSATION_ATTRIBUTES.DISPLAY_ID,
value: CONVERSATION_ATTRIBUTES.DISPLAY_ID,
attributeName: t('FILTER.ATTRIBUTES.CONVERSATION_IDENTIFIER'),
label: t('FILTER.ATTRIBUTES.CONVERSATION_IDENTIFIER'),
inputType: 'plainText',
@@ -147,8 +170,8 @@ export function useConversationFilterContext() {
attributeModel: 'standard',
},
{
attributeKey: 'campaign_id',
value: 'campaign_id',
attributeKey: CONVERSATION_ATTRIBUTES.CAMPAIGN_ID,
value: CONVERSATION_ATTRIBUTES.CAMPAIGN_ID,
attributeName: t('FILTER.ATTRIBUTES.CAMPAIGN_NAME'),
label: t('FILTER.ATTRIBUTES.CAMPAIGN_NAME'),
inputType: 'searchSelect',
@@ -161,8 +184,8 @@ export function useConversationFilterContext() {
attributeModel: 'standard',
},
{
attributeKey: 'labels',
value: 'labels',
attributeKey: CONVERSATION_ATTRIBUTES.LABELS,
value: CONVERSATION_ATTRIBUTES.LABELS,
attributeName: t('FILTER.ATTRIBUTES.LABELS'),
label: t('FILTER.ATTRIBUTES.LABELS'),
inputType: 'multiSelect',
@@ -185,8 +208,8 @@ export function useConversationFilterContext() {
attributeModel: 'standard',
},
{
attributeKey: 'browser_language',
value: 'browser_language',
attributeKey: CONVERSATION_ATTRIBUTES.BROWSER_LANGUAGE,
value: CONVERSATION_ATTRIBUTES.BROWSER_LANGUAGE,
attributeName: t('FILTER.ATTRIBUTES.BROWSER_LANGUAGE'),
label: t('FILTER.ATTRIBUTES.BROWSER_LANGUAGE'),
inputType: 'searchSelect',
@@ -196,8 +219,8 @@ export function useConversationFilterContext() {
attributeModel: 'additional',
},
{
attributeKey: 'country_code',
value: 'country_code',
attributeKey: CONVERSATION_ATTRIBUTES.COUNTRY_CODE,
value: CONVERSATION_ATTRIBUTES.COUNTRY_CODE,
attributeName: t('FILTER.ATTRIBUTES.COUNTRY_NAME'),
label: t('FILTER.ATTRIBUTES.COUNTRY_NAME'),
inputType: 'searchSelect',
@@ -207,8 +230,8 @@ export function useConversationFilterContext() {
attributeModel: 'additional',
},
{
attributeKey: 'referer',
value: 'referer',
attributeKey: CONVERSATION_ATTRIBUTES.REFERER,
value: CONVERSATION_ATTRIBUTES.REFERER,
attributeName: t('FILTER.ATTRIBUTES.REFERER_LINK'),
label: t('FILTER.ATTRIBUTES.REFERER_LINK'),
inputType: 'plainText',
@@ -217,8 +240,8 @@ export function useConversationFilterContext() {
attributeModel: 'additional',
},
{
attributeKey: 'created_at',
value: 'created_at',
attributeKey: CONVERSATION_ATTRIBUTES.CREATED_AT,
value: CONVERSATION_ATTRIBUTES.CREATED_AT,
attributeName: t('FILTER.ATTRIBUTES.CREATED_AT'),
label: t('FILTER.ATTRIBUTES.CREATED_AT'),
inputType: 'date',
@@ -227,8 +250,8 @@ export function useConversationFilterContext() {
attributeModel: 'standard',
},
{
attributeKey: 'last_activity_at',
value: 'last_activity_at',
attributeKey: CONVERSATION_ATTRIBUTES.LAST_ACTIVITY_AT,
value: CONVERSATION_ATTRIBUTES.LAST_ACTIVITY_AT,
attributeName: t('FILTER.ATTRIBUTES.LAST_ACTIVITY'),
label: t('FILTER.ATTRIBUTES.LAST_ACTIVITY'),
inputType: 'date',
@@ -117,7 +117,7 @@ const props = defineProps({
},
conversationId: { type: Number, required: true },
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 },
inboxId: { type: Number, default: null }, // eslint-disable-line vue/no-unused-properties
inboxSupportsReplyTo: { type: Object, default: () => ({}) },
@@ -173,7 +173,11 @@ const variant = computed(() => {
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
// message sent by the current user
if (
@@ -182,17 +186,15 @@ const isMyMessage = computed(() => {
) {
return true;
}
const senderId = props.senderId ?? props.sender?.id;
const senderType = props.senderType ?? props.sender?.type;
if (!senderType || !senderId) {
return false;
return true;
}
return (
senderType.toLowerCase() === SENDER_TYPES.USER.toLowerCase() &&
props.currentUserId === senderId
);
return senderType.toLowerCase() === SENDER_TYPES.USER.toLowerCase();
});
/**
@@ -200,7 +202,7 @@ const isMyMessage = computed(() => {
* @returns {import('vue').ComputedRef<'left'|'right'|'center'>} The computed orientation
*/
const orientation = computed(() => {
if (isMyMessage.value) {
if (isBotOrAgentMessage.value) {
return ORIENTATION.RIGHT;
}
@@ -221,8 +223,8 @@ const flexOrientationClass = computed(() => {
const gridClass = computed(() => {
const map = {
[ORIENTATION.LEFT]: 'grid grid-cols-[24px_1fr]',
[ORIENTATION.RIGHT]: 'grid grid-cols-1fr',
[ORIENTATION.LEFT]: 'grid grid-cols-1fr',
[ORIENTATION.RIGHT]: 'grid grid-cols-[1fr_24px]',
};
return map[orientation.value];
@@ -231,13 +233,13 @@ const gridClass = computed(() => {
const gridTemplate = computed(() => {
const map = {
[ORIENTATION.LEFT]: `
"avatar bubble"
"spacer meta"
`,
[ORIENTATION.RIGHT]: `
"bubble"
"meta"
`,
[ORIENTATION.RIGHT]: `
"bubble avatar"
"meta spacer"
`,
};
return map[orientation.value];
@@ -251,7 +253,7 @@ const shouldGroupWithNext = computed(() => {
const shouldShowAvatar = computed(() => {
if (props.messageType === MESSAGE_TYPES.ACTIVITY) return false;
if (orientation.value === ORIENTATION.RIGHT) return false;
if (orientation.value === ORIENTATION.LEFT) return false;
return true;
});
@@ -438,7 +440,7 @@ provideMessageContext({
isPrivate: computed(() => props.private),
variant,
orientation,
isMyMessage,
isBotOrAgentMessage,
shouldGroupWithNext,
});
</script>
@@ -448,7 +450,7 @@ provideMessageContext({
<div
v-if="shouldRenderMessage"
: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"
:class="[
flexOrientationClass,
@@ -470,22 +472,28 @@ provideMessageContext({
'w-full': variant === MESSAGE_VARIANTS.EMAIL,
},
]"
class="gap-x-3"
class="gap-x-2"
:style="{
gridTemplateAreas: gridTemplate,
}"
>
<div
v-if="!shouldGroupWithNext && shouldShowAvatar"
v-tooltip.right-end="avatarTooltip"
v-tooltip.left-end="avatarTooltip"
class="[grid-area:avatar] flex items-end"
>
<Avatar v-bind="avatarInfo" :size="24" />
<Avatar v-bind="avatarInfo" :size="20" />
</div>
<div
class="[grid-area:bubble] flex"
: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,
}"
@contextmenu="openContextMenu($event)"
@@ -26,7 +26,7 @@ const { t } = useI18n();
/>
</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="{
'ltr:left-0 rtl:right-0': orientation === ORIENTATION.LEFT,
'ltr:right-0 rtl:left-0': orientation === ORIENTATION.RIGHT,
@@ -66,7 +66,11 @@ const shouldGroupWithNext = (index, searchList) => {
nextMessageType === 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;
@@ -95,6 +99,17 @@ const getInReplyToMessage = parentMessage => {
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>
<template>
@@ -112,6 +127,7 @@ const getInReplyToMessage = parentMessage => {
:group-with-next="shouldGroupWithNext(index, allMessages)"
:inbox-supports-reply-to="inboxSupportsReplyTo"
:current-user-id="currentUserId"
:class="getMessageSpacingClass(message, allMessages, index)"
data-clarity-mask="True"
/>
</template>
@@ -102,6 +102,7 @@ const statusToShow = computed(() => {
if (isRead.value) return MESSAGE_STATUS.READ;
if (isDelivered.value) return MESSAGE_STATUS.DELIVERED;
if (isSent.value) return MESSAGE_STATUS.SENT;
if (status.value === MESSAGE_STATUS.FAILED) return MESSAGE_STATUS.FAILED;
return MESSAGE_STATUS.PROGRESS;
});
@@ -14,7 +14,7 @@ const readableTime = computed(() =>
<template>
<BaseBubble
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"
>
<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',
[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.BOT]: 'bg-n-solid-iris text-n-slate-12',
[MESSAGE_VARIANTS.TEMPLATE]: 'bg-n-solid-iris text-n-slate-12',
[MESSAGE_VARIANTS.ERROR]: 'bg-n-ruby-4 text-n-ruby-12',
[MESSAGE_VARIANTS.BOT]: 'bg-n-solid-purple text-n-slate-12',
[MESSAGE_VARIANTS.TEMPLATE]: 'bg-n-solid-purple text-n-slate-12',
[MESSAGE_VARIANTS.ERROR]: 'bg-n-solid-red text-n-ruby-12',
[MESSAGE_VARIANTS.EMAIL]: 'w-full',
[MESSAGE_VARIANTS.UNSUPPORTED]:
'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"
rel="noreferrer noopener nofollow"
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 }}
</a>
<button
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"
>
{{ action.label }}
@@ -62,7 +62,7 @@ const hasQuotedMessage = computed(() => {
<EmailMeta
class="p-3"
:class="{
'border-b border-n-strong': isIncoming,
'border-b border-n-slate-6': isIncoming,
'border-b border-n-slate-8/20': isOutgoing,
}"
/>
@@ -75,7 +75,7 @@ const hasQuotedMessage = computed(() => {
>
<div
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
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"
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>
</BaseBubble>
</template>
@@ -20,12 +20,15 @@ const isEmpty = computed(() => {
<template>
<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">
{{ $t('CONVERSATION.NO_CONTENT') }}
</span>
<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">
<div
v-if="contentAttributes.submittedEmail"
@@ -1,8 +1,7 @@
<script setup>
import { computed, defineOptions, useAttrs } from 'vue';
import ImageChip from 'next/message/chips/Image.vue';
import VideoChip from 'next/message/chips/Video.vue';
import ImageVideoChip from 'next/message/chips/AttachmentGrid.vue'; // Image and Video Grids are the same component
import AudioChip from 'next/message/chips/Audio.vue';
import FileChip from 'next/message/chips/File.vue';
import { useMessageContext } from '../provider.js';
@@ -37,7 +36,7 @@ const attrs = useAttrs();
const { orientation } = useMessageContext();
const classToApply = computed(() => {
const baseClasses = [attrs.class, 'flex', 'flex-wrap'];
const baseClasses = [attrs.class, 'flex', 'flex-wrap', 'gap-2'];
if (orientation.value === 'right') {
baseClasses.push('justify-end');
@@ -77,30 +76,25 @@ const files = computed(() => {
<template>
<div v-if="mediaAttachments.length" :class="classToApply">
<template v-for="attachment in mediaAttachments" :key="attachment.id">
<ImageChip
v-if="attachment.fileType === ATTACHMENT_TYPES.IMAGE"
:attachment="attachment"
/>
<VideoChip
v-else-if="attachment.fileType === ATTACHMENT_TYPES.VIDEO"
:attachment="attachment"
/>
</template>
<ImageVideoChip :attachments="mediaAttachments" />
</div>
<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
class="bg-n-alpha-3 dark:bg-n-alpha-2 text-n-slate-12"
:attachment="attachment"
/>
</div>
</div>
<div v-if="files.length" :class="classToApply">
<FileChip
v-for="attachment in files"
:key="attachment.id"
:attachment="attachment"
/>
<div className="grid grid-cols-1 2xl:grid-cols-2 gap-3">
<FileChip
v-for="attachment in files"
:key="attachment.id"
:attachment="attachment"
/>
</div>
</div>
</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,
required: true,
},
remainingCount: {
type: Number,
required: true,
},
shouldShowOverlay: {
type: Boolean,
required: true,
},
});
const hasError = ref(false);
const showGallery = ref(false);
@@ -20,12 +29,20 @@ const { filteredCurrentChatAttachments } = useMessageContext();
const handleError = () => {
hasError.value = true;
};
const handleGalleryClick = () => {
showGallery.value = true;
};
const handleGalleryClose = () => {
showGallery.value = false;
};
</script>
<template>
<div
class="size-[72px] overflow-hidden contain-content rounded-xl cursor-pointer"
@click="showGallery = true"
class="rounded-lg overflow-hidden contain-content cursor-pointer size-full"
@click="handleGalleryClick"
>
<div
v-if="hasError"
@@ -34,6 +51,7 @@ const handleError = () => {
<Icon icon="i-lucide-circle-off" class="text-n-slate-11" />
{{ $t('COMPONENTS.MEDIA.LOADING_FAILED') }}
</div>
<img
v-else
class="object-cover w-full h-full"
@@ -41,12 +59,23 @@ const handleError = () => {
@error="handleError"
/>
</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
v-if="showGallery"
v-model:show="showGallery"
:attachment="useSnakeCase(attachment)"
:all-attachments="filteredCurrentChatAttachments"
@error="handleError"
@close="() => (showGallery = false)"
@close="handleGalleryClose"
/>
</template>
@@ -10,17 +10,33 @@ defineProps({
type: Object,
required: true,
},
remainingCount: {
type: Number,
required: true,
},
shouldShowOverlay: {
type: Boolean,
required: true,
},
});
const showGallery = ref(false);
const { filteredCurrentChatAttachments } = useMessageContext();
const handleGalleryClick = () => {
showGallery.value = true;
};
const handleGalleryClose = () => {
showGallery.value = false;
};
</script>
<template>
<div
class="size-[72px] overflow-hidden contain-content rounded-xl cursor-pointer relative group"
@click="showGallery = true"
class="rounded-lg overflow-hidden contain-content cursor-pointer size-full"
@click="handleGalleryClick"
>
<video
:src="attachment.dataUrl"
@@ -29,6 +45,7 @@ const { filteredCurrentChatAttachments } = useMessageContext();
playsInline
/>
<div
v-if="!shouldShowOverlay"
class="absolute w-full h-full inset-0 p-1 flex items-center justify-center"
>
<div
@@ -41,12 +58,21 @@ const { filteredCurrentChatAttachments } = useMessageContext();
</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
v-if="showGallery"
v-model:show="showGallery"
:attachment="useSnakeCase(attachment)"
:all-attachments="filteredCurrentChatAttachments"
@error="onError"
@close="() => (showGallery = false)"
@close="handleGalleryClose"
/>
</template>
@@ -98,7 +98,7 @@ const MessageControl = Symbol('MessageControl');
* @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<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>} 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."
},
"FORM": {
"GO_TO_CONVERSATION": "Aussicht",
"GO_TO_CONVERSATION": "Anzeigen",
"SUCCESS_MESSAGE": "Die Nachricht wurde erfolgreich versendet!",
"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.",
@@ -751,7 +751,7 @@
"WHATSAPP": "WhatsApp",
"SMS": "SMS",
"EMAIL": "E-Mail",
"TELEGRAM": "Telegram",
"TELEGRAM": "Telegramm",
"LINE": "Line",
"API": "API-Kanal"
}
@@ -12,7 +12,7 @@
},
"WEBHOOK": {
"SUBSCRIBED_EVENTS": "Abonnierte Events",
"LEARN_MORE": "Learn more about webhooks",
"LEARN_MORE": "Mehr über Webhooks erfahren",
"FORM": {
"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.",
@@ -31,7 +31,7 @@
},
"END_POINT": {
"LABEL": "Webhook-URL",
"PLACEHOLDER": "Example: {webhookExampleURL}",
"PLACEHOLDER": "Beispiel: {webhookExampleURL}",
"ERROR": "Bitte geben Sie eine gültige URL ein"
},
"EDIT_SUBMIT": "Webhook aktualisieren",
@@ -114,7 +114,7 @@
},
"OPEN_AI": {
"AI_ASSIST": "AI-Assistent",
"WITH_AI": " {option} with AI ",
"WITH_AI": " {option} mit KI ",
"OPTIONS": {
"REPLY_SUGGESTION": "Antwortvorschlag",
"SUMMARIZE": "Zusammenfassen",
@@ -235,7 +235,7 @@
"ERROR": "Beim Abrufen der linearen Probleme ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut",
"LINK_SUCCESS": "Problem erfolgreich verknüpft",
"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": {
"TITLE": "Lineares Problem erstellen/verknüpfen",
@@ -294,7 +294,7 @@
"PRIORITY": "Priorität",
"ASSIGNEE": "Zugewiesener",
"LABELS": "Labels",
"CREATED_AT": "Created at {createdAt}"
"CREATED_AT": "Erstellt am {createdAt}"
},
"UNLINK": {
"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"
},
"DELETE": {
"TITLE": "Are you sure you want to delete the integration?",
"MESSAGE": "Are you sure you want to delete the integration?",
"TITLE": "Sind Sie sicher, dass Sie die Integration löschen möchten?",
"MESSAGE": "Sind Sie sicher, dass Sie die Integration löschen möchten?",
"CONFIRM": "Ja, löschen",
"CANCEL": "Stornieren"
}
@@ -313,27 +313,27 @@
"NAME": "Kapitän",
"COPILOT": {
"SEND_MESSAGE": "Nachricht senden...",
"LOADER": "Captain is thinking",
"LOADER": "Captain denkt nach",
"YOU": "Sie",
"USE": "Use this",
"RESET": "Reset",
"SELECT_ASSISTANT": "Select Assistant"
"USE": "Verwenden",
"RESET": "Zurücksetzen",
"SELECT_ASSISTANT": "Assistent auswählen"
},
"PAYWALL": {
"TITLE": "Upgrade to use Captain AI",
"TITLE": "Upgrade auf Captain AI",
"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_NOW": "Upgrade now",
"CANCEL_ANYTIME": "You can change or cancel your plan anytime"
"UPGRADE_PROMPT": "Tarif upgraden, um Zugang zu unseren Assistenten, Copilot und mehr zu erhalten.",
"UPGRADE_NOW": "Jetzt upgraden",
"CANCEL_ANYTIME": "Sie können Ihr Paket jederzeit ändern oder kündigen"
},
"ENTERPRISE_PAYWALL": {
"AVAILABLE_ON": "Captain AI feature is only available in a paid plan.",
"UPGRADE_PROMPT": "Upgrade your plan to get access to our assistants, copilot and more.",
"ASK_ADMIN": "Please reach out to your administrator for the upgrade."
"AVAILABLE_ON": "Captain AI Funktion ist nur mit einem kostenpflichtigen Tarif verfügbar.",
"UPGRADE_PROMPT": "Tarif upgraden, um Zugang zu unseren Assistenten, Copilot und mehr zu erhalten.",
"ASK_ADMIN": "Bitte kontaktieren Sie Ihren Administrator für das Upgrade."
},
"BANNER": {
"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": {
"CANCEL": "Stornieren",
@@ -100,7 +100,7 @@
"NO_RESULTS": "Nenhum resultado encontrado."
},
"MULTI_SELECTOR": {
"PLACEHOLDER": "Nenhuma",
"PLACEHOLDER": "Nenhum",
"TITLE": {
"AGENT": "Selecionar agente",
"TEAM": "Selecionar time"
@@ -1,16 +1,16 @@
{
"AUDIT_LOGS": {
"HEADER": "Registros de Auditoria",
"HEADER_BTN_TXT": "Adicionar Registros de Auditoria",
"HEADER": "Auditoria",
"HEADER_BTN_TXT": "Adicionar 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.",
"LEARN_MORE": "Saiba mais sobre os logs de auditoria",
"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": {
"404": "Não há Registros de Auditoria disponíveis nesta conta.",
"TITLE": "Gerenciar Registros de Auditoria",
"DESC": "Registros de Auditoria são trilhas para eventos e ações em um Sistema Chatwoot.",
"404": "Não há Logs de Auditoria disponíveis nesta conta.",
"TITLE": "Gerenciar Logs de Auditoria",
"DESC": "Logs de auditoria são rastros para eventos e ações em um Sistema de Chatwoot.",
"TABLE_HEADER": {
"ACTIVITY": "Usuário",
"TIME": "Ação",
@@ -27,12 +27,12 @@
"ERROR": "Falha ao atualizar etiquetas"
},
"CONVERSATION": {
"TITLE": "Marcador da conversa",
"ADD_BUTTON": "Adicionar marcador"
"TITLE": "Etiquetas da conversa",
"ADD_BUTTON": "Adicionar etiquetas"
},
"LABEL_SELECT": {
"TITLE": "Adicionar marcador",
"PLACEHOLDER": "Pesquisar marcador ",
"TITLE": "Adicionar etiquetas",
"PLACEHOLDER": "Pesquisar etiquetas",
"NO_RESULT": "Nenhuma etiqueta encontrada",
"CREATE_LABEL": "Criar etiqueta"
}
@@ -65,7 +65,7 @@
"HEADER": {
"RESOLVE_ACTION": "Resolver",
"REOPEN_ACTION": "Reabrir",
"OPEN_ACTION": "Abertas",
"OPEN_ACTION": "Abrir",
"OPEN": "Mais",
"CLOSE": "Fechar",
"DETAILS": "detalhes",
@@ -148,7 +148,7 @@
"MESSAGE_SIGN_TOOLTIP": "Assinatura de mensagem",
"ENABLE_SIGN_TOOLTIP": "Ativar 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",
"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",
@@ -15,7 +15,7 @@
},
"ENTERPRISE_PAYWALL": {
"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."
},
"LIST": {
@@ -1,6 +1,6 @@
{
"GENERAL_SETTINGS": {
"TITLE": "Configurações da conta",
"TITLE": "Conta",
"SUBMIT": "Atualizar configurações",
"BACK": "Anterior",
"DISMISS": "Recusar",
@@ -131,10 +131,10 @@
"GO_TO_INBOX_REPORTS": "Ir para Relatórios da Caixa de Entrada",
"GO_TO_TEAM_REPORTS": "Ir para Relatórios da Equipe",
"GO_TO_SETTINGS_AGENTS": "Ir para Configurações de Agente",
"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_TEAMS": "Ir para as Configurações de Equipe",
"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_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_ACCOUNT": "Ir para as Configurações da Conta",
"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.",
"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",
"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",
"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",
@@ -695,7 +695,7 @@
"PLACE_HOLDER": "Fale conosco no chat"
},
"UPDATE": {
"BUTTON_TEXT": "Atualizar configurações do widget",
"BUTTON_TEXT": "Atualizar Configurações do Widget",
"API": {
"SUCCESS_MESSAGE": "Configurações do widget atualizadas com sucesso",
"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"
},
"DELETE": {
"TITLE": "Are you sure you want to delete the integration?",
"MESSAGE": "Are you sure you want to delete the integration?",
"TITLE": "Tem certeza que deseja excluir esta integração?",
"MESSAGE": "Tem certeza que deseja excluir esta integração?",
"CONFIRM": "Sim, excluir",
"CANCEL": "Cancelar"
}
@@ -317,7 +317,7 @@
"YOU": "Você",
"USE": "Use isto",
"RESET": "Reiniciar",
"SELECT_ASSISTANT": "Select Assistant"
"SELECT_ASSISTANT": "Selecione o Assistente"
},
"PAYWALL": {
"TITLE": "Atualize para usar o Capitão IA",
@@ -440,19 +440,19 @@
"DOCUMENTABLE": {
"CONVERSATION": "Conversação #{id}"
},
"SELECTED": "{count} selected",
"BULK_APPROVE_BUTTON": "Approve",
"SELECTED": "{count} selecionado",
"BULK_APPROVE_BUTTON": "Aprovar",
"BULK_DELETE_BUTTON": "Excluir",
"BULK_APPROVE": {
"SUCCESS_MESSAGE": "FAQs approved successfully",
"ERROR_MESSAGE": "There was an error approving the FAQs, please try again."
"SUCCESS_MESSAGE": "Perguntas Frequentes aprovadas com sucesso",
"ERROR_MESSAGE": "Ocorreu um erro ao aprovar as Perguntas Frequentes. Tente novamente."
},
"BULK_DELETE": {
"TITLE": "Delete FAQs?",
"DESCRIPTION": "Are you sure you want to delete the selected FAQs? This action cannot be undone.",
"CONFIRM": "Yes, delete all",
"SUCCESS_MESSAGE": "FAQs deleted successfully",
"ERROR_MESSAGE": "There was an error deleting the FAQs, please try again."
"TITLE": "Excluir as Perguntas Frequentes?",
"DESCRIPTION": "Tem certeza que deseja excluir as Perguntas Frequentes selecionadas? Esta ação não pode ser desfeita.",
"CONFIRM": "Sim, excluir todas",
"SUCCESS_MESSAGE": "Perguntas Frequentes excluídas com sucesso/",
"ERROR_MESSAGE": "Ocorreu um erro ao excluir as Perguntas Frequentes, por favor tente novamente."
},
"DELETE": {
"TITLE": "Tem certeza que deseja excluir o FAQ?",
@@ -1,7 +1,7 @@
{
"LABEL_MGMT": {
"HEADER": "Etiquetas",
"HEADER_BTN_TXT": "Adicionar marcador",
"HEADER_BTN_TXT": "Adicionar etiqueta",
"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.",
"LEARN_MORE": "Aprenda mais sobre etiquetas",
@@ -18,21 +18,21 @@
},
"FORM": {
"NAME": {
"LABEL": "Nome do marcador",
"PLACEHOLDER": "Nome do marcador",
"REQUIRED_ERROR": "O nome do marcador é obrigatório",
"LABEL": "Nome da Etiqueta",
"PLACEHOLDER": "Nome da etiqueta",
"REQUIRED_ERROR": "O nome da etiqueta é obrigatório",
"MINIMUM_LENGTH_ERROR": "Tamanho mínimo 2 é necessário",
"VALID_ERROR": "Somente Letras, Números, Hífen e Sublinhado são permitidos"
},
"DESCRIPTION": {
"LABEL": "Descrição",
"PLACEHOLDER": "Descrição do marcador"
"PLACEHOLDER": "Descrição da etiqueta"
},
"COLOR": {
"LABEL": "Cor"
},
"SHOW_ON_SIDEBAR": {
"LABEL": "Exibir marcador na barra lateral"
"LABEL": "Exibir etiqueta na barra lateral"
},
"EDIT": "Alterar",
"CREATE": "Criar",
@@ -54,10 +54,10 @@
"SUGGESTED_LABELS": "Etiquetas sugeridos"
},
"ADD": {
"TITLE": "Adicionar marcador",
"TITLE": "Adicionar etiqueta",
"DESC": "Etiquetas permitem agrupar as conversas.",
"API": {
"SUCCESS_MESSAGE": "Marcador adicionado com sucesso",
"SUCCESS_MESSAGE": "Etiqueta adicionada com sucesso",
"ERROR_MESSAGE": "Ocorreu um erro, por favor tente novamente"
}
},
@@ -71,7 +71,7 @@
"DELETE": {
"BUTTON_TEXT": "Excluir",
"API": {
"SUCCESS_MESSAGE": "Marcador excluído com sucesso",
"SUCCESS_MESSAGE": "Etiqueta excluída com sucesso",
"ERROR_MESSAGE": "Ocorreu um erro, por favor tente novamente"
},
"CONFIRM": {
@@ -471,7 +471,7 @@
"NO_AGENTS": "Não há conversas por agentes",
"TABLE_HEADER": {
"AGENT": "Agente",
"OPEN": "Abertas",
"OPEN": "Abrir",
"UNATTENDED": "Não atendidas",
"STATUS": "Situação"
}
@@ -509,7 +509,7 @@
"SLA": "Nome do SLA",
"AGENTS": "Nome do Agente",
"INBOXES": "Nome da Caixa de Entrada",
"LABELS": "Nome do marcador",
"LABELS": "Nome da etiqueta",
"TEAMS": "Nome da equipe"
},
"SLA": "Política SLA",
@@ -26,7 +26,7 @@
"UPDATE_SUCCESS": "Suas configurações foram atualizadas com sucesso",
"CARD": {
"ENTER_KEY": {
"HEADING": "Inserir (↵)",
"HEADING": "Enter (↵)",
"CONTENT": "Enviar mensagens pressionando a tecla Enter em vez de clicar no botão de enviar."
},
"CMD_ENTER_KEY": {
@@ -37,19 +37,19 @@
},
"INTERFACE_SECTION": {
"TITLE": "Interface",
"NOTE": "Customize the look and feel of your Chatwoot dashboard.",
"NOTE": "Personalize a aparência do seu painel do Chatwoot.",
"FONT_SIZE": {
"TITLE": "Font size",
"NOTE": "Adjust the text size across the dashboard based on your preference.",
"UPDATE_SUCCESS": "Your font settings have been updated successfully",
"UPDATE_ERROR": "There is an error while updating the font settings, please try again",
"TITLE": "Tamanho da fonte",
"NOTE": "Ajuste o tamanho do texto do painel com base na sua preferência.",
"UPDATE_SUCCESS": "As configurações de sua fonte foram atualizadas com sucesso",
"UPDATE_ERROR": "Ocorreu um erro ao atualizar as configurações de fonte, por favor, tente novamente",
"OPTIONS": {
"SMALLER": "Smaller",
"SMALL": "Small",
"SMALLER": "Menor",
"SMALL": "Pequeno",
"DEFAULT": "Padrão",
"LARGE": "Large",
"LARGER": "Larger",
"EXTRA_LARGE": "Extra Large"
"LARGE": "Grande",
"LARGER": "Maior",
"EXTRA_LARGE": "Muito Grande"
}
}
},
@@ -287,13 +287,13 @@
"HOME": "Principal",
"AGENTS": "Agentes",
"AGENT_BOTS": "Robôs",
"AUDIT_LOGS": "Registros de Auditoria",
"AUDIT_LOGS": "Auditoria",
"INBOXES": "Caixas de Entrada",
"NOTIFICATIONS": "Notificações",
"CANNED_RESPONSES": "Respostas Prontas",
"INTEGRATIONS": "Integrações",
"PROFILE_SETTINGS": "Configurações do Perfil",
"ACCOUNT_SETTINGS": "Configurações da conta",
"ACCOUNT_SETTINGS": "Conta",
"APPLICATIONS": "Aplicações",
"LABELS": "Etiquetas",
"CUSTOM_ATTRIBUTES": "Atributos Personalizados",
@@ -322,7 +322,7 @@
"REPORTS_INBOX": "Caixa de Entrada",
"REPORTS_TEAM": "Equipe",
"SET_AVAILABILITY_TITLE": "Defina como",
"SET_YOUR_AVAILABILITY": "Definir a sua disponibilidade",
"SET_YOUR_AVAILABILITY": "Disponibilidade",
"SLA": "SLA",
"CUSTOM_ROLES": "Regras Personalizadas",
"BETA": "Beta",
@@ -1,9 +1,9 @@
{
"SLA": {
"HEADER": "Termos do Serviço",
"HEADER": "Acordo de Nível de Serviço",
"ADD_ACTION": "Adicionar 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",
"LOADING": "Buscando SLAs",
"PAYWALL": {
@@ -15,7 +15,7 @@
},
"ENTERPRISE_PAYWALL": {
"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."
},
"LIST": {
@@ -266,7 +266,7 @@
"ATTRIBUTE_WARNING": "Контактная информация <strong>{primaryContactName}</strong> будет скопирована в <strong>{parentContactName}</strong>."
},
"SEARCH": {
"ERROR_MESSAGE": "Something went wrong. Please try again later."
"ERROR_MESSAGE": "Что-то пошло не так. Пожалуйста, попробуйте позже."
},
"FORM": {
"SUBMIT": " Объединить контакты",
@@ -563,7 +563,7 @@
"GO_TO_CONVERSATION": "Просмотр",
"SUCCESS_MESSAGE": "Сообщение успешно отправлено!",
"ERROR_MESSAGE": "Произошла ошибка при создании диалога. Пожалуйста, повторите попытку позже.",
"NO_INBOX_ALERT": "Нет доступных \"входящих\" для начала разговора с этим контактом.",
"NO_INBOX_ALERT": "Нет доступных входящих для начала разговора с этим контактом.",
"CONTACT_SELECTOR": {
"LABEL": "Кому:",
"TAG_INPUT_PLACEHOLDER": "Поиск контакта с именем, email или номером телефона",
@@ -31,7 +31,7 @@
},
"PERMISSIONS": {
"CONVERSATION_MANAGE": "Управление всеми диалогами",
"CONVERSATION_UNASSIGNED_MANAGE": "Управление неназначенными беседами и теми, которые назначены им",
"CONVERSATION_UNASSIGNED_MANAGE": "Управление неназначенными беседами и теми, которые им назначены",
"CONVERSATION_PARTICIPATING_MANAGE": "Управление текущими беседами и теми, которые им назначены",
"CONTACT_MANAGE": "Управление контактами",
"REPORT_MANAGE": "Управление отчетами",
@@ -114,7 +114,7 @@
"CHANGE_PRIORITY": "Изменить приоритет",
"CHANGE_TEAM": "Изменить команду",
"SNOOZE_CONVERSATION": "Включить звук диалога",
"ADD_LABEL": "Добавить метку в разговор",
"ADD_LABEL": "Добавить метку в диалог",
"REMOVE_LABEL": "Удалить метку из диалога",
"SETTINGS": "Настройки",
"AI_ASSIST": "Помощь ИИ",
@@ -128,11 +128,11 @@
"GO_TO_CONVERSATION_REPORTS": "Перейти к отчетам по диалогам",
"GO_TO_AGENT_REPORTS": "Перейти к отчетам по сотрудникам",
"GO_TO_LABEL_REPORTS": "Перейти к отчетам по меткам",
"GO_TO_INBOX_REPORTS": "Перейти к отчётам по \"Входящим\"",
"GO_TO_INBOX_REPORTS": "Перейти к отчётам по «Входящим»",
"GO_TO_TEAM_REPORTS": "Перейти к отчетам по командам",
"GO_TO_SETTINGS_AGENTS": "Перейти к настройкам сотрудников",
"GO_TO_SETTINGS_TEAMS": "Перейти к настройкам команд",
"GO_TO_SETTINGS_INBOXES": "Перейти к настройкам \"Входящих\"",
"GO_TO_SETTINGS_INBOXES": "Перейти к настройкам «Входящих»",
"GO_TO_SETTINGS_LABELS": "Перейти к настройкам меток",
"GO_TO_SETTINGS_CANNED_RESPONSES": "Перейти к настройкам шаблонных ответов",
"GO_TO_SETTINGS_APPLICATIONS": "Перейти в настройки приложения",
@@ -2,7 +2,7 @@
"INBOX_MGMT": {
"HEADER": "Источники",
"DESCRIPTION": "Канал — это способ коммуникации, который ваш клиент выбирает для связи с вами. Входящие — это место, где вы управляете диалогами для определенного канала. Он может включать диалоги из различных источников, таких как электронная почта, онлайн чат или социальные сети.",
"LEARN_MORE": "Узнать больше о \"Входящих\"",
"LEARN_MORE": "Узнать больше о «Входящих»",
"RECONNECTION_REQUIRED": "Входящие сообщения отключены. Вы не будете получать новые сообщения пока не пройдете авторизацию повторно.",
"CLICK_TO_RECONNECT": "Нажмите здесь для повторного подключения.",
"LIST": {
@@ -208,8 +208,8 @@
}
},
"WHATSAPP": {
"TITLE": "WhatsApp канал",
"DESC": "Начните поддерживать ваших клиентов через WhatsApp.",
"TITLE": "Канал WhatsApp",
"DESC": "Начните поддерживать своих клиентов через WhatsApp.",
"PROVIDERS": {
"LABEL": "Поставщик API",
"TWILIO": "Twilio",
@@ -302,8 +302,8 @@
"ERROR": "Произошла ошибка при отвязке задачи, пожалуйста, попробуйте ещё раз"
},
"DELETE": {
"TITLE": "Are you sure you want to delete the integration?",
"MESSAGE": "Are you sure you want to delete the integration?",
"TITLE": "Вы уверены, что хотите удалить интеграцию?",
"MESSAGE": "Вы уверены, что хотите удалить интеграцию?",
"CONFIRM": "Да, удалить",
"CANCEL": "Отменить"
}
@@ -317,18 +317,18 @@
"YOU": "Вы",
"USE": "Использовать это",
"RESET": "Сброс",
"SELECT_ASSISTANT": "Select Assistant"
"SELECT_ASSISTANT": "Выбрать ассистента"
},
"PAYWALL": {
"TITLE": "Обновите тарифный план, чтобы использовать Captain AI",
"AVAILABLE_ON": "Капитан недоступен на бесплатном тарифном плане.",
"UPGRADE_PROMPT": "Обновите тарифный план, чтобы получить доступ к помощникам, Captain AI и другому.",
"UPGRADE_PROMPT": "Обновите тарифный план, чтобы получить доступ к нашим ассистентам, copilot и другим функциям.",
"UPGRADE_NOW": "Обновить сейчас",
"CANCEL_ANYTIME": "Вы можете изменить или отменить ваш тарифный план в любое время"
},
"ENTERPRISE_PAYWALL": {
"AVAILABLE_ON": "Функция Captain AI доступна только в платном плане.",
"UPGRADE_PROMPT": "Обновите тарифный план, чтобы получить доступ к помощникам, Captain AI и другому.",
"UPGRADE_PROMPT": "Обновите тарифный план, чтобы получить доступ к нашим ассистентам, copilot и другим функциям.",
"ASK_ADMIN": "Пожалуйста, обратитесь к вашему администратору для обновления."
},
"BANNER": {
@@ -341,35 +341,35 @@
"EDIT": "Обновить"
},
"ASSISTANTS": {
"HEADER": "Помощники",
"ADD_NEW": "Создать нового помощника",
"HEADER": "Ассистенты",
"ADD_NEW": "Создать нового ассистента",
"DELETE": {
"TITLE": "Вы уверены, что хотите удалить помощника?",
"DESCRIPTION": "Это действие необратимо. Удаление этого помощника удалит его из всех подключенных источников и навсегда удалит все сгенерированные знания.",
"TITLE": "Вы уверены, что хотите удалить ассистента?",
"DESCRIPTION": "Это действие необратимо. Удаление этого ассистента удалит его из всех подключенных источников и навсегда удалит все сгенерированные знания.",
"CONFIRM": "Да, удалить",
"SUCCESS_MESSAGE": "Помощник успешно удален",
"ERROR_MESSAGE": "При удалении помощника произошла ошибка. Пожалуйста, попробуйте еще раз."
"SUCCESS_MESSAGE": "Ассистент успешно удален",
"ERROR_MESSAGE": "При удалении ассистента произошла ошибка. Пожалуйста, попробуйте еще раз."
},
"FORM_DESCRIPTION": "Заполните приведенные ниже сведения, чтобы назвать своего помощника, описать его цель и указать поддерживаемый продукт.",
"FORM_DESCRIPTION": "Заполните приведенные ниже сведения, чтобы назвать своего ассистента, описать его цель и указать поддерживаемый продукт.",
"CREATE": {
"TITLE": "Создать помощника",
"SUCCESS_MESSAGE": "Помощник успешно создан",
"ERROR_MESSAGE": "При создании помощника произошла ошибка, пожалуйста, попробуйте еще раз."
"TITLE": "Создать ассистента",
"SUCCESS_MESSAGE": "Ассистент успешно создан",
"ERROR_MESSAGE": "При создании ассистента произошла ошибка, пожалуйста, попробуйте еще раз."
},
"FORM": {
"NAME": {
"LABEL": "Имя помощника",
"PLACEHOLDER": "Введите имя помощника",
"ERROR": "Пожалуйста, укажите имя помощника"
"LABEL": "Имя ассистента",
"PLACEHOLDER": "Введите имя ассистента",
"ERROR": "Пожалуйста, укажите имя ассистента"
},
"DESCRIPTION": {
"LABEL": "Описание помощника",
"PLACEHOLDER": "Опишите, как и где будет использоваться этот помощник",
"LABEL": "Описание ассистента",
"PLACEHOLDER": "Опишите, как и где будет использоваться этот ассистент",
"ERROR": "Необходимо описание"
},
"PRODUCT_NAME": {
"LABEL": "Название продукта",
"PLACEHOLDER": "Введите название продукта, для которого предназначен этот помощник",
"PLACEHOLDER": "Введите название продукта, для которого предназначен этот ассистент",
"ERROR": "Требуется название продукта"
},
"FEATURES": {
@@ -379,18 +379,18 @@
}
},
"EDIT": {
"TITLE": "Обновить помощника",
"SUCCESS_MESSAGE": "Помощник успешно обновлен",
"ERROR_MESSAGE": "При обновлении помощника произошла ошибка, пожалуйста, попробуйте еще раз."
"TITLE": "Обновить ассистента",
"SUCCESS_MESSAGE": "Ассистент успешно обновлен",
"ERROR_MESSAGE": "При обновлении ассистента произошла ошибка, пожалуйста, попробуйте еще раз."
},
"OPTIONS": {
"EDIT_ASSISTANT": "Редактировать помощника",
"DELETE_ASSISTANT": "Удалить помощника",
"EDIT_ASSISTANT": "Редактировать ассистента",
"DELETE_ASSISTANT": "Удалить ассистента",
"VIEW_CONNECTED_INBOXES": "Просмотр подключенных источников"
},
"EMPTY_STATE": {
"TITLE": "Нет доступных помощников",
"SUBTITLE": "Создайте помощника, чтобы дать быстрые и точные ответы пользователям. Он может учиться на ваших статьях из центра поддержки и прошлых диалогах."
"TITLE": "Нет доступных ассистентов",
"SUBTITLE": "Создайте ассистента, чтобы дать быстрые и точные ответы пользователям. Он может учиться на ваших статьях из центра поддержки и прошлых диалогах."
}
},
"DOCUMENTS": {
@@ -413,9 +413,9 @@
"ERROR": "Пожалуйста, укажите корректный URL для документа"
},
"ASSISTANT": {
"LABEL": "Помощник",
"PLACEHOLDER": "Выберите помощника",
"ERROR": "Необходимо заполнить поле помощника"
"LABEL": "Ассистент",
"PLACEHOLDER": "Выберите ассистента",
"ERROR": "Необходимо заполнить поле ассистента"
}
},
"DELETE": {
@@ -440,19 +440,19 @@
"DOCUMENTABLE": {
"CONVERSATION": "Диалог #{id}"
},
"SELECTED": "{count} selected",
"BULK_APPROVE_BUTTON": "Approve",
"SELECTED": "Выбрано {count}",
"BULK_APPROVE_BUTTON": "Одобрить",
"BULK_DELETE_BUTTON": "Удалить",
"BULK_APPROVE": {
"SUCCESS_MESSAGE": "FAQs approved successfully",
"ERROR_MESSAGE": "There was an error approving the FAQs, please try again."
"SUCCESS_MESSAGE": "FAQ успешно одобрены",
"ERROR_MESSAGE": "Произошла ошибка при одобрении FAQ, пожалуйста, попробуйте ещё раз."
},
"BULK_DELETE": {
"TITLE": "Delete FAQs?",
"DESCRIPTION": "Are you sure you want to delete the selected FAQs? This action cannot be undone.",
"CONFIRM": "Yes, delete all",
"SUCCESS_MESSAGE": "FAQs deleted successfully",
"ERROR_MESSAGE": "There was an error deleting the FAQs, please try again."
"TITLE": "Удалить FAQ?",
"DESCRIPTION": "Вы уверены, что хотите удалить выбранные FAQ? Это действие невозможно отменить.",
"CONFIRM": "Да, удалить всё",
"SUCCESS_MESSAGE": "FAQ успешно удалены",
"ERROR_MESSAGE": "Произошла ошибка при удалении FAQ, пожалуйста, попробуйте ещё раз."
},
"DELETE": {
"TITLE": "Вы действительно хотите удалить FAQ?",
@@ -462,7 +462,7 @@
"ERROR_MESSAGE": "Произошла ошибка при удалении FAQ, попробуйте еще раз."
},
"FILTER": {
"ASSISTANT": "Помощник: {selected}",
"ASSISTANT": "Ассистент: {selected}",
"STATUS": "Статус: {selected}",
"ALL_ASSISTANTS": "Все"
},
@@ -472,7 +472,7 @@
"APPROVED": "Одобрено",
"ALL": "Все"
},
"FORM_DESCRIPTION": "Добавьте вопрос и соответствующий ему ответ в базу знаний и выберите помощника, с которым он должен связаться.",
"FORM_DESCRIPTION": "Добавьте вопрос и соответствующий ему ответ в базу знаний и выберите ассистента, с которым он должен связаться.",
"CREATE": {
"TITLE": "Добавить FAQ",
"SUCCESS_MESSAGE": "Ответ успешно добавлен.",
@@ -487,59 +487,59 @@
"ANSWER": {
"LABEL": "Ответ",
"PLACEHOLDER": "Введите ответ",
"ERROR": "Please provide a valid answer."
"ERROR": "Пожалуйста, введите корректный ответ."
},
"ASSISTANT": {
"LABEL": "Помощник",
"PLACEHOLDER": "Select an assistant",
"ERROR": "Please select an assistant."
"LABEL": "Ассистент",
"PLACEHOLDER": "Выбрать ассистента",
"ERROR": "Выбрать ассистента"
}
},
"EDIT": {
"TITLE": "Update the FAQ",
"SUCCESS_MESSAGE": "The FAQ has been successfully updated",
"ERROR_MESSAGE": "There was an error updating the FAQ, please try again",
"APPROVE_SUCCESS_MESSAGE": "The FAQ was marked as approved"
"TITLE": "Обновить FAQ",
"SUCCESS_MESSAGE": "FAQ успешно обновлён",
"ERROR_MESSAGE": "Произошла ошибка при обновлении FAQ, пожалуйста, попробуйте ещё раз",
"APPROVE_SUCCESS_MESSAGE": "FAQ был отмечен как одобренный"
},
"OPTIONS": {
"APPROVE": "Mark as approved",
"EDIT_RESPONSE": "Edit FAQ",
"DELETE_RESPONSE": "Delete FAQ"
"APPROVE": "Отметить как одобренный",
"EDIT_RESPONSE": "FAQ отмечен как одобренный",
"DELETE_RESPONSE": "Удалить FAQ"
},
"EMPTY_STATE": {
"TITLE": "No FAQs Found",
"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."
"TITLE": "FAQ не найдены",
"SUBTITLE": "FAQ помогают вашему ассистенту быстро и точно отвечать на вопросы клиентов. Их можно генерировать автоматически из вашего контента или добавлять вручную."
}
},
"INBOXES": {
"HEADER": "Connected Inboxes",
"ADD_NEW": "Connect a new inbox",
"HEADER": "Подключённые источники входящих",
"ADD_NEW": "Подключить новый источник входящих",
"OPTIONS": {
"DISCONNECT": "Отключиться"
},
"DELETE": {
"TITLE": "Are you sure to disconnect the inbox?",
"TITLE": "Вы уверены, что хотите отключить этот источник входящих?",
"DESCRIPTION": "",
"CONFIRM": "Да, удалить",
"SUCCESS_MESSAGE": "The inbox was successfully disconnected.",
"ERROR_MESSAGE": "There was an error disconnecting the inbox, please try again."
"SUCCESS_MESSAGE": "Источник входящих успешно отключён.",
"ERROR_MESSAGE": "Произошла ошибка при отключении источника входящих, пожалуйста, попробуйте ещё раз."
},
"FORM_DESCRIPTION": "Choose an inbox to connect with the assistant.",
"FORM_DESCRIPTION": "Выберите источник входящих для подключения к ассистенту.",
"CREATE": {
"TITLE": "Connect an Inbox",
"SUCCESS_MESSAGE": "The inbox was successfully connected.",
"ERROR_MESSAGE": "An error occurred while connecting the inbox. Please try again."
"TITLE": "Подключить источник входящих",
"SUCCESS_MESSAGE": "Источник входящих успешно подключён.",
"ERROR_MESSAGE": "Произошла ошибка при подключении источника входящих. Пожалуйста, попробуйте ещё раз."
},
"FORM": {
"INBOX": {
"LABEL": "Электронная почта",
"PLACEHOLDER": "Choose the inbox to deploy the assistant.",
"ERROR": "An inbox selection is required."
"PLACEHOLDER": "Выберите источник входящих для развертывания ассистента.",
"ERROR": "Необходимо выбрать источник входящих."
}
},
"EMPTY_STATE": {
"TITLE": "No Connected Inboxes",
"SUBTITLE": "Connecting an inbox allows the assistant to handle initial questions from your customers before transferring them to you."
"TITLE": "Нет подключённых источников входящих",
"SUBTITLE": "Подключение источника входящих позволяет ассистенту обрабатывать первые вопросы ваших клиентов до их передачи вам."
}
}
}
@@ -124,7 +124,7 @@
},
"AGENT_REPORTS": {
"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": "Загрузка данных графика...",
"NO_ENOUGH_DATA": "Недостаточно данных для создания отчета, пожалуйста, повторите попытку позже.",
"DOWNLOAD_AGENT_REPORTS": "Сказать отчёт по агентам",
@@ -259,7 +259,7 @@
},
"INBOX_REPORTS": {
"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": "Загрузка данных графика...",
"NO_ENOUGH_DATA": "Недостаточно данных для создания отчета, пожалуйста, повторите попытку позже.",
"DOWNLOAD_INBOX_REPORTS": "Скачать отчет по входящим",
@@ -327,7 +327,7 @@
},
"TEAM_REPORTS": {
"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": "Загрузка данных графика...",
"NO_ENOUGH_DATA": "Недостаточно данных для создания отчета, пожалуйста, повторите попытку позже.",
"DOWNLOAD_TEAM_REPORTS": "Скачать отчет по команде",
@@ -546,9 +546,9 @@
"INBOX": "Электронная почта",
"AGENT": "Оператор",
"TEAM": "Команда",
"AVG_RESOLUTION_TIME": "Avg. Resolution Time",
"AVG_FIRST_RESPONSE_TIME": "Avg. First Response Time",
"AVG_REPLY_TIME": "Avg. Customer Waiting Time",
"AVG_RESOLUTION_TIME": "Среднее время решения",
"AVG_FIRST_RESPONSE_TIME": "Среднее время первого ответа",
"AVG_REPLY_TIME": "Среднее время ожидания клиента",
"RESOLUTION_COUNT": "Количество завершенных",
"CONVERSATIONS": "Количество диалогов"
}
@@ -11,8 +11,8 @@
"CONVERSATIONS": "Диалоги",
"MESSAGES": "Сообщения"
},
"VIEW_MORE": "View more",
"LOAD_MORE": "Load more",
"VIEW_MORE": "Посмотреть больше",
"LOAD_MORE": "Загрузить ещё",
"SEARCHING_DATA": "Идёт поиск",
"LOADING_DATA": "Загрузка",
"EMPTY_STATE": "Не найдено {item} для запроса '{query}'",
@@ -36,20 +36,20 @@
}
},
"INTERFACE_SECTION": {
"TITLE": "Interface",
"NOTE": "Customize the look and feel of your Chatwoot dashboard.",
"TITLE": "Интерфейс",
"NOTE": "Настройте внешний вид и оформление вашей панели Chatwoot.",
"FONT_SIZE": {
"TITLE": "Font size",
"NOTE": "Adjust the text size across the dashboard based on your preference.",
"UPDATE_SUCCESS": "Your font settings have been updated successfully",
"UPDATE_ERROR": "There is an error while updating the font settings, please try again",
"TITLE": "Размер шрифта",
"NOTE": "Измените размер текста на всей панели в соответствии с вашими предпочтениями.",
"UPDATE_SUCCESS": "Настройки шрифта успешно обновлены",
"UPDATE_ERROR": "Произошла ошибка при обновлении настроек шрифта, пожалуйста, попробуйте ещё раз",
"OPTIONS": {
"SMALLER": "Smaller",
"SMALL": "Small",
"SMALLER": "Меньше",
"SMALL": "Маленький",
"DEFAULT": "По умолчанию",
"LARGE": "Large",
"LARGER": "Larger",
"EXTRA_LARGE": "Extra Large"
"LARGE": "Большой",
"LARGER": "Крупнее",
"EXTRA_LARGE": "Очень большой"
}
}
},
@@ -162,7 +162,7 @@
"REQUEST_PUSH": "Включить push-уведомления",
"SLA_MISSED_FIRST_RESPONSE": "Отправить push-уведомление, если диалог не соответствует SLA первого ответа",
"SLA_MISSED_NEXT_RESPONSE": "Отправить push-уведомление, если диалог не успел получить следующий ответ SLA",
"SLA_MISSED_RESOLUTION": "Отправить push-уведомление, если диалог не соответствует уровню разрешения SLA"
"SLA_MISSED_RESOLUTION": "Отправить push-уведомление, если диалог не соответствует SLA"
},
"PROFILE_IMAGE": {
"LABEL": "Изображение профиля"
@@ -281,9 +281,9 @@
"SETTINGS": "Настройки",
"CONTACTS": "Контакты",
"CAPTAIN": "Капитан",
"CAPTAIN_ASSISTANTS": "Помощники",
"CAPTAIN_ASSISTANTS": "Ассистенты",
"CAPTAIN_DOCUMENTS": "Документы",
"CAPTAIN_RESPONSES": "FAQs",
"CAPTAIN_RESPONSES": "FAQ",
"HOME": "Главная",
"AGENTS": "Операторы",
"AGENT_BOTS": "Боты",
@@ -345,14 +345,14 @@
},
"BILLING_SETTINGS": {
"TITLE": "Платёж",
"DESCRIPTION": "Manage your subscription here, upgrade your plan and get more for your team.",
"DESCRIPTION": "Управляйте подпиской здесь, обновите тарифный план и получите больше возможностей для вашей команды.",
"CURRENT_PLAN": {
"TITLE": "Текущий план",
"PLAN_NOTE": "На данный момент вы подписаны на ** план{plan}** с **{quantity}** лицензиями",
"SEAT_COUNT": "Number of seats",
"RENEWS_ON": "Renews on"
"SEAT_COUNT": "Количество мест",
"RENEWS_ON": "Продление"
},
"VIEW_PRICING": "View Pricing",
"VIEW_PRICING": "Посмотреть цены",
"MANAGE_SUBSCRIPTION": {
"TITLE": "Управление подпиской",
"DESCRIPTION": "Просматривайте ваши предыдущие счета, редактируйте платежные реквизиты или отмените подписку.",
@@ -360,11 +360,11 @@
},
"CAPTAIN": {
"TITLE": "Капитан",
"DESCRIPTION": "Manage usage and credits for Captain AI.",
"BUTTON_TXT": "Buy more credits",
"DESCRIPTION": "Управляйте использованием и кредитами для Captain AI.",
"BUTTON_TXT": "Купить дополнительные кредиты",
"DOCUMENTS": "Документы",
"RESPONSES": "Responses",
"UPGRADE": "Captain is not available on the free plan, upgrade now to get access to assistants, copilot and more."
"RESPONSES": "Ответы",
"UPGRADE": "Captain недоступен в бесплатном тарифном плане, обновите его, чтобы получить доступ к ассистентам, copilot и другим функциям."
},
"CHAT_WITH_US": {
"TITLE": "Нужна помощь?",
@@ -3,7 +3,6 @@ import { mapGetters } from 'vuex';
import { defineAsyncComponent } from '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 AddAccountModal from 'dashboard/components/layout/sidebarComponents/AddAccountModal.vue';
import AccountSelector from 'dashboard/components/layout/sidebarComponents/AccountSelector.vue';
@@ -20,6 +19,10 @@ import { FEATURE_FLAGS } from 'dashboard/featureFlags';
const CommandBar = defineAsyncComponent(
() => import('./commands/commandbar.vue')
);
const Sidebar = defineAsyncComponent(
() => import('../../components/layout/Sidebar.vue')
);
import { emitter } from 'shared/helpers/mitt';
export default {
@@ -121,8 +121,6 @@ export default {
this.isALineChannel ||
this.isAPIInbox ||
(this.isAnEmailChannel && !this.inbox.provider) ||
this.isAMicrosoftInbox ||
this.isAGoogleInbox ||
this.isAWhatsAppChannel ||
this.isAWebWidgetInbox
) {
@@ -39,13 +39,14 @@ export default {
},
mixins: [globalConfigMixin],
setup() {
const { isEditorHotKeyEnabled } = useUISettings();
const { isEditorHotKeyEnabled, updateUISettings } = useUISettings();
const { currentFontSize, updateFontSize } = useFontSize();
return {
currentFontSize,
updateFontSize,
isEditorHotKeyEnabled,
updateUISettings,
};
},
data() {
@@ -2,8 +2,15 @@ class Inboxes::FetchImapEmailInboxesJob < ApplicationJob
queue_as :scheduled_jobs
def perform
Inbox.where(channel_type: 'Channel::Email').all.find_each(batch_size: 100) do |inbox|
::Inboxes::FetchImapEmailsJob.perform_later(inbox.channel) if inbox.channel.imap_enabled
email_inboxes = Inbox.where(channel_type: 'Channel::Email')
email_inboxes.find_each(batch_size: 100) do |inbox|
::Inboxes::FetchImapEmailsJob.perform_later(inbox.channel) if should_fetch_emails?(inbox)
end
end
private
def should_fetch_emails?(inbox)
inbox.channel.imap_enabled && !inbox.account.suspended?
end
end
+18 -3
View File
@@ -37,7 +37,7 @@ class Channel::Sms < ApplicationRecord
body = message_body(contact_number, message.content)
body['media'] = message.attachments.map(&:download_url) if message.attachments.present?
send_to_bandwidth(body)
send_to_bandwidth(body, message)
end
def send_text_message(contact_number, message_content)
@@ -56,7 +56,7 @@ class Channel::Sms < ApplicationRecord
}
end
def send_to_bandwidth(body)
def send_to_bandwidth(body, message = nil)
response = HTTParty.post(
"#{api_base_path}/users/#{provider_config['account_id']}/messages",
basic_auth: bandwidth_auth,
@@ -64,7 +64,22 @@ class Channel::Sms < ApplicationRecord
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
def bandwidth_auth
+14
View File
@@ -22,6 +22,12 @@
# index_custom_attribute_definitions_on_account_id (account_id)
#
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) }
validates :attribute_display_name, presence: true
@@ -31,6 +37,7 @@ class CustomAttributeDefinition < ApplicationRecord
validates :attribute_display_type, 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_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
::Inboxes::UpdateWidgetPreChatCustomFieldsJob.perform_later(account, self)
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
@@ -20,15 +20,30 @@ class Facebook::SendOnFacebookService < Base::SendOnChannelService
end
def send_message_to_facebook(delivery_params)
result = Facebook::Messenger::Bot.deliver(delivery_params, page_id: channel.page_id)
parsed_result = JSON.parse(result)
parsed_result = deliver_message(delivery_params)
return if parsed_result.nil?
if parsed_result['error'].present?
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
message.update!(source_id: parsed_result['message_id']) if parsed_result['message_id'].present?
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
{
recipient: { id: contact.get_source_id(inbox.id) },
+9 -12
View File
@@ -1,7 +1,7 @@
require 'json'
class FilterService
include FilterHelper
include Filters::FilterHelper
include CustomExceptions::CustomFilter
ATTRIBUTE_MODEL = 'conversation_attribute'.freeze
@@ -43,18 +43,15 @@ class FilterService
end
def filter_values(query_hash)
case query_hash['attribute_key']
when 'status'
return Conversation.statuses.values if query_hash['values'].include?('all')
attribute_key = query_hash['attribute_key']
values = query_hash['values']
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)
end
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'
case_insensitive_values(query_hash)
end
def downcase_array_values(values)
@@ -70,22 +70,28 @@ class Instagram::SendOnInstagramService < Base::SendOnChannelService
query: query
)
if response[:error].present?
Rails.logger.error("Instagram response: #{response['error']} : #{message_content}")
message.status = :failed
message.external_error = external_error(response)
handle_response(response, message_content)
end
def handle_response(response, message_content)
parsed_response = response.parsed_response
if response.success? && parsed_response['error'].blank?
message.update!(source_id: parsed_response['message_id'])
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
message.source_id = response['message_id'] if response['message_id'].present?
message.save!
response
end
def external_error(response)
# https://developers.facebook.com/docs/instagram-api/reference/error-codes/
error_message = response[:error][:message]
error_code = response[:error][:code]
error_message = response.dig('error', 'message')
error_code = response.dig('error', 'code')
"#{error_code} - #{error_message}"
end
@@ -27,6 +27,33 @@ class Whatsapp::Providers::BaseService
raise 'Overwrite this method in child class'
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)
buttons = []
items.each do |item|
@@ -1,5 +1,6 @@
class Whatsapp::Providers::Whatsapp360DialogService < Whatsapp::Providers::BaseService
def send_message(phone_number, message)
@message = message
if message.attachments.present?
send_attachment_message(phone_number, message)
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['filename'] = attachment.file.filename if type == 'document'
response = HTTParty.post(
"#{api_base_path}/messages",
headers: api_headers,
@@ -91,13 +93,9 @@ class Whatsapp::Providers::Whatsapp360DialogService < Whatsapp::Providers::BaseS
process_response(response)
end
def process_response(response)
if response.success?
response['messages'].first['id']
else
Rails.logger.error response.body
nil
end
def error_message(response)
# {"meta": {"success": false, "http_code": 400, "developer_message": "errro-message", "360dialog_trace_id": "someid"}}
response.parsed_response.dig('meta', 'developer_message')
end
def template_body_parameters(template_info)
@@ -1,5 +1,7 @@
class Whatsapp::Providers::WhatsappCloudService < Whatsapp::Providers::BaseService
def send_message(phone_number, message)
@message = message
if message.attachments.present?
send_attachment_message(phone_number, message)
elsif message.content_type == 'input_select'
@@ -111,13 +113,9 @@ class Whatsapp::Providers::WhatsappCloudService < Whatsapp::Providers::BaseServi
process_response(response)
end
def process_response(response)
if response.success?
response['messages'].first['id']
else
Rails.logger.error response.body
nil
end
def error_message(response)
# https://developers.facebook.com/docs/whatsapp/cloud-api/support/error-codes/#sample-response
response.parsed_response&.dig('error', 'message')
end
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.message_id attachment.push_event_data[:message_id]
json.thumb_url attachment.push_event_data[:thumb_url]
@@ -1,11 +1,19 @@
<% content_for(:title) do %>
Configure Settings - <%= @config.titleize %>
<% end %>
<header class="main-content__header" role="banner">
<h1 class="main-content__page-title" id="page-title">
<%= content_for(:title) %>
</h1>
</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">
<%= form_with url: super_admin_app_config_url(config: @config) , method: :post do |form| %>
<% @allowed_configs.each do |key| %>
@@ -15,18 +23,36 @@
</div>
<div class="-mt-2 field-unit__field ">
<% if @installation_configs[key]&.dig('type') == 'boolean' %>
<%= form.select "app_config[#{key}]",
[["True", true], ["False", false]],
<%= form.select "app_config[#{key}]",
[["True", true], ["False", false]],
{ selected: ActiveModel::Type::Boolean.new.cast(@app_config[key]) },
class: "mt-2 border border-slate-100 p-1 rounded-md"
class: "mt-2 border border-slate-100 p-1 rounded-md"
%>
<% elsif @installation_configs[key]&.dig('type') == 'code' %>
<%= form.text_area "app_config[#{key}]",
value: @app_config[key],
<%= form.text_area "app_config[#{key}]",
value: @app_config[key],
rows: 12,
wrap: 'off',
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 %>
<%= form.text_field "app_config[#{key}]", value: @app_config[key] %>
<% end %>
@@ -43,3 +69,26 @@
</div>
<% end %>
</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,27 +28,36 @@ It renders the `_table` partial to display details about the resources.
<% end %>
<header class="main-content__header" role="banner">
<h1 class="main-content__page-title" id="page-title">
<%= content_for(:title) %>
</h1>
<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) %>
</h1>
<% if show_search_bar %>
<%= render(
"search",
search_term: search_term,
resource_name: display_resource_name(page.resource_name)
) %>
<% end %>
<div class="flex items-center">
<% if show_search_bar %>
<div class="flex items-center">
<%= render("filters", page: page) %>
<div class="ml-3">
<%= render(
"search",
search_term: search_term,
resource_name: display_resource_name(page.resource_name)
) %>
</div>
</div>
<% end %>
<div>
<%= link_to(
t(
"administrate.actions.new_resource",
name: page.resource_name.titleize.downcase
),
[:new, namespace, page.resource_path.to_sym],
class: "button",
) if accessible_action?(new_resource, :new) %>
<div class="whitespace-nowrap ml-4">
<%= link_to(
t(
"administrate.actions.new_resource",
name: page.resource_name.titleize.downcase
),
[:new, namespace, page.resource_path.to_sym],
class: "button",
) if accessible_action?(new_resource, :new) %>
</div>
</div>
</div>
</header>
+14
View File
@@ -103,13 +103,16 @@
display_title: 'Facebook Verify Token'
description: 'The verify token used for Facebook Messenger Webhook'
locked: false
type: secret
- name: FB_APP_SECRET
display_title: 'Facebook App Secret'
locked: false
type: secret
- name: IG_VERIFY_TOKEN
display_title: 'Instagram Verify Token'
description: 'The verify token used for Instagram Webhook'
locked: false
type: secret
- name: 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`'
@@ -131,6 +134,7 @@
- name: AZURE_APP_SECRET
display_title: 'Azure App Secret'
locked: false
type: secret
# End of Microsoft Email Channel Config
# MARK: Captain Config
@@ -138,6 +142,7 @@
display_title: 'OpenAI API Key'
description: 'The API key used to authenticate requests to OpenAI services for Captain AI.'
locked: false
type: secret
- name: CAPTAIN_OPEN_AI_MODEL
display_title: 'OpenAI Model'
description: 'The OpenAI model configured for use in Captain AI. Default: gpt-4o-mini'
@@ -146,6 +151,7 @@
display_title: 'FireCrawl API Key (optional)'
description: 'The FireCrawl API key for the Captain AI service'
locked: false
type: secret
- name: CAPTAIN_CLOUD_PLAN_LIMITS
display_title: 'Captain Cloud Plan Limits'
description: 'The limits for the Captain AI service for different plans'
@@ -160,11 +166,13 @@
display_title: 'Inbox Token'
description: 'The Chatwoot Inbox Token for Contact Support in Cloud'
locked: false
type: secret
- name: CHATWOOT_INBOX_HMAC_KEY
value:
display_title: 'Inbox HMAC Key'
description: 'The Chatwoot Inbox HMAC Key for Contact Support in Cloud'
locked: false
type: secret
- name: CHATWOOT_CLOUD_PLANS
display_title: 'Cloud Plans'
value:
@@ -180,10 +188,12 @@
value:
display_title: 'Analytics Token'
description: 'The June.so analytics token for Chatwoot cloud'
type: secret
- name: CLEARBIT_API_KEY
value:
display_title: 'Clearbit API Key'
description: 'This API key is used for onboarding the users, to pre-fill account data.'
type: secret
- name: DASHBOARD_SCRIPTS
value:
display_title: 'Dashboard Scripts'
@@ -206,12 +216,14 @@
- name: CHATWOOT_SUPPORT_WEBSITE_TOKEN
value:
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
value:
description: 'The Chatwoot script base URL, to display the "Contact Support" option on the billing page'
- name: CHATWOOT_SUPPORT_IDENTIFIER_HASH
value:
description: 'The Chatwoot identifier hash, to validate the contact in the live chat window.'
type: secret
- name: ACCOUNT_SECURITY_NOTIFICATION_WEBHOOK_URL
display_title: Webhook URL to post security analysis
value:
@@ -245,6 +257,7 @@
display_title: 'Firebase Credentials'
value:
locked: false
type: secret
description: 'Contents on your firebase credentials json file'
## ------ End of Configs added for FCM v1 notifications ------ ##
@@ -259,4 +272,5 @@
value:
locked: false
description: 'Linear client secret'
type: secret
## ------ 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_query_operator: Query operator must be either "AND" or "OR".
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:
period: Reporting period %{since} to %{until}
utc_warning: The report generated is in UTC timezone
+2
View File
@@ -68,6 +68,8 @@ ar:
invalid_operator: مشغل غير صالح. المشغل المسموح به لـ %{attribute_name} هو [%{allowed_keys}].
invalid_query_operator: Query operator must be either "AND" or "OR".
invalid_value: قيمة غير صالحة. القيم المقدمة ل %{attribute_name} غير صالحة
custom_attribute_definition:
key_conflict: The provided key is not allowed as it might conflict with default attributes.
reports:
period: فترة التبليغ %{since} إلى %{until}
utc_warning: التقرير الذي تم إنشاؤه في التوقيت العالمي الموحّد
+2
View File
@@ -68,6 +68,8 @@ az:
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_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:
period: Reporting period %{since} to %{until}
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_query_operator: Query operator must be either "AND" or "OR".
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:
period: Reporting period %{since} to %{until}
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_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
custom_attribute_definition:
key_conflict: The provided key is not allowed as it might conflict with default attributes.
reports:
period: Període d'informes %{since} a %{until}
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_query_operator: Query operator must be either "AND" or "OR".
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:
period: Reporting period %{since} to %{until}
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_query_operator: Query operator must be either "AND" or "OR".
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:
period: Rapporteringsperiode %{since} til %{until}
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_query_operator: Query operator must be either "AND" or "OR".
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:
period: Berichtszeitraum von %{since} bis %{until}
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_query_operator: Query operator must be either "AND" or "OR".
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:
period: Περίοδος αναφοράς %{since} έως %{until}
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_query_operator: Query operator must be either "AND" or "OR".
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:
period: Reporting period %{since} to %{until}
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_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
custom_attribute_definition:
key_conflict: The provided key is not allowed as it might conflict with default attributes.
reports:
period: Reportando el periodo desde %{since} hasta %{until}
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_query_operator: Query operator must be either "AND" or "OR".
invalid_value: مقدار معتبر نیست. مقادیر ارائه شده برای %{attribute_name} معتبر نیست
custom_attribute_definition:
key_conflict: The provided key is not allowed as it might conflict with default attributes.
reports:
period: زمان گزارش از %{since} تا %{until}
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_query_operator: Query operator must be either "AND" or "OR".
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:
period: Raportointijakso %{since} %{until}
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_query_operator: Query operator must be either "AND" or "OR".
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:
period: Période de rapport %{since} à %{until}
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_query_operator: Query operator must be either "AND" or "OR".
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:
period: Reporting period %{since} to %{until}
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_query_operator: Query operator must be either "AND" or "OR".
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:
period: Reporting period %{since} to %{until}
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_query_operator: Query operator must be either "AND" or "OR".
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:
period: Reporting period %{since} to %{until}
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_query_operator: Query operator must be either "AND" or "OR".
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:
period: Jelentési időszak %{since}-tól %{until}-ig
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_query_operator: Query operator must be either "AND" or "OR".
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:
period: Reporting period %{since} to %{until}
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_query_operator: Query operator must be either "AND" or "OR".
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:
period: Periode pelaporan %{since} hingga %{until}
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_query_operator: Query operator must be either "AND" or "OR".
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:
period: Reporting period %{since} to %{until}
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_query_operator: Query operator must be either "AND" or "OR".
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:
period: Periodo di segnalazione da %{since} a %{until}
utc_warning: The report generated is in UTC timezone
+2
View File
@@ -68,6 +68,8 @@ ja:
invalid_operator: 無効な演算子です。%{attribute_name} に許可されている演算子は [%{allowed_keys}] です。
invalid_query_operator: クエリ演算子は "AND" または "OR" でなければなりません。
invalid_value: 無効な値です。%{attribute_name} に提供された値は無効です。
custom_attribute_definition:
key_conflict: The provided key is not allowed as it might conflict with default attributes.
reports:
period: レポート期間 %{since} から %{until} まで
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_query_operator: Query operator must be either "AND" or "OR".
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:
period: Reporting period %{since} to %{until}
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_query_operator: Query operator must be either "AND" or "OR".
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:
period: 보고 기간 %{since} - %{until}
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_query_operator: Query operator must be either "AND" or "OR".
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:
period: Ataskaitinis laikotarpis nuo %{since} iki %{until}
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_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
custom_attribute_definition:
key_conflict: The provided key is not allowed as it might conflict with default attributes.
reports:
period: Ziņošanas periods %{since} līdz %{until}
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_query_operator: Query operator must be either "AND" or "OR".
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:
period: Reporting period %{since} to %{until}
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_query_operator: Query operator must be either "AND" or "OR".
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:
period: Reporting period %{since} to %{until}
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_query_operator: Query operator must be either "AND" or "OR".
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:
period: Reporting period %{since} to %{until}
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