Compare commits

..
Author SHA1 Message Date
Sojan JoseandGitHub e0bf2bd9d4 fix: Lograge issue on non api pages (#10193)
This PR addresses several issues related to logging:

- Enabling Lograge broke certain non-API URLs, such as password reset.
This occurred due to the user ID tagging we had in Lograge, which has
now been limited to API pages only.
- Disabled the start and done logs in Sidekiq.
- Investigated why Sidekiq logs weren’t being output as JSON. This is
due to the use of ActiveJob instead of Sidekiq for the job base classes.

**Potential Options for Converting ActiveJob Logs to JSON:**
- https://glozer.rocks/ojb
- https://learnedreverie.medium.com/activejob-logs-as-json-6912403d8c81
- https://github.com/roidrage/lograge/pull/226
2024-10-01 16:53:27 -07:00
Shivam MishraandGitHub a0dddae289 feat: let users re auth legacy google inboxes (#10179)
This PR allows migration of legacy GMail inbox users to new OAuth based
inbox

## How to test?

1. Create an inbox from the seed data and set it's IMAP address to
`imap.gmail.com` from the UI
2. Open `rails console` and run the following

   ```
   inbox = Inbox.find(100) # use your inbox id here
   channel = inbox.channel
   channel.update(email: 'hello@chatwoot.com')
   channel.prompt_reauthorization!
   ```
3. This will show the prompt on the UI. Once you click on Reauthorize,
it should open Google Auth. Reauthroize with the same email address as
used in the inbox and it should start working as usual

### Setting up ENV

```sh
GOOGLE_OAUTH_CLIENT_ID=<some-hash>.apps.googleusercontent.com
GOOGLE_OAUTH_CLIENT_SECRET=<client-secret>
GOOGLE_OAUTH_CALLBACK_URL="http://localhost:3000/omniauth/google_oauth2/callback"
```
2024-09-30 21:48:52 +05:30
Sivin VargheseandGitHub f4f2d678cf fix: Show error messages from response (#10173)
This PR fixes the issue where proper error messages from the backend
were not displayed when an email already exists in the system during a
profile update, or when a phone number is already taken for Twilio
during the creation of a new account.

Fixes:
https://linear.app/chatwoot/issue/CW-3560/prod-customer-facing-issue-updating-email
2024-09-26 20:16:39 -07:00
Sojan JoseandGitHub 4a7a0427e9 feat: Provision captain accounts automatically (#10168)
- Provision accounts on Chatwoot cloud automatically if the feature is enabled
2024-09-26 19:21:29 -07:00
Sojan JoseandGitHub d107d0adec fix: Twilio channel attachment issues (#10167)
We received customer reports that attachments in Twilio messages
required page reloads to appear. This issue occurred because in the old
Twilio builder, we saved the message and attachment in two stages. The
new builders follow a streamlined approach, where both are saved in a
single transaction. This update aligns the Twilio channel with the new
builder format and resolves the issue.

### Testing:

Tests cover the attachment cases, ensuring that all original tests pass
with these changes.
2024-09-25 18:03:28 -07:00
d79d9e8b46 fix: Include uncategorized articles in the all article section to allow edit/delete (#10153)
Fixes https://github.com/chatwoot/chatwoot/issues/9935
Fixes https://github.com/chatwoot/chatwoot/issues/8213

The articles were grouped by category, with locale being a derived
attribute from the category. If a category was deleted, the article
wouldn't appear on the dashboard. However, due to a bug, it would show
up in the uncategorized section on the public portal, leaving agents
unable to edit or update the article.

To address this issue, I've added a locale attribute directly to the
article. This attribute is automatically set from the category or the
portal's default locale if not supplied. The API parameters now use this
attribute to filter articles. As a result, the dashboard will display
articles even if they're not associated with a category, improving the
overall workflow.

**Main updates:** 
- Add locale attribute to the Article model. Add db migration to back
fill the data based on the above logic.
- Add a new scope search_by_locale and use it instead of
search_by_category_locale.
- Update the ERB template to include the locale filter.
- Move from `joins` to `left_outer_joins` to include the articles with
no categories.

---------

Co-authored-by: Sojan <sojan@pepalo.com>
2024-09-23 23:39:03 -07:00
PranavandGitHub b524ceeca1 feat: Auto-populate the telephone code based on the browser timezone (#10146)
Fixes https://github.com/chatwoot/chatwoot/issues/6228


There is a country code selector for the phone input field. This is
often a point of frustration. See the response below.

> We are using the phone number field however this can be frustrating
for customers, especially mobile users, to select the +1 US country
code. Our users are typically local businesses and being able to default
to +1 country code in the phone number field or the account would
improve the interaction they have with customers.


Most people who run local businesses don't need a country selector.
However, to preserve the quality of the data we store, we need a country
code.

A balance between these two issues can be found with an auto-populating
country code field based on the browser's timezone. This is what I did
in this PR. Based on the browser timezone, we will resolve it to the
closest country code.
2024-09-23 23:25:38 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
e68dac8c45 chore(deps): bump puma from 6.4.2 to 6.4.3 (#10137)
Bumps [puma](https://github.com/puma/puma) from 6.4.2 to 6.4.3.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/puma/puma/releases">puma's
releases</a>.</em></p>
<blockquote>
<h2>6.4.3</h2>
<ul>
<li>Security
<ul>
<li>Discards any headers using underscores if the non-underscore version
also exists. Without this, an attacker could overwrite values set by
intermediate proxies (e.g. X-Forwarded-For). (<a
href="https://github.com/puma/puma/security/advisories/GHSA-9hf4-67fc-4vf4">CVE-2024-45614</a>/GHSA-9hf4-67fc-4vf4)</li>
</ul>
</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/puma/puma/blob/master/History.md">puma's
changelog</a>.</em></p>
<blockquote>
<h2>6.4.3 / 2024-09-19</h2>
<ul>
<li>Security
<ul>
<li>Discards any headers using underscores if the non-underscore version
also exists. Without this, an attacker could overwrite values set by
intermediate proxies (e.g. X-Forwarded-For). (<a
href="https://github.com/puma/puma/security/advisories/GHSA-9hf4-67fc-4vf4">CVE-2024-45614</a>/GHSA-9hf4-67fc-4vf4)</li>
</ul>
</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/puma/puma/commit/e867e53aa4e7df91fa97d8995a80fc2f9466a080"><code>e867e53</code></a>
6.4.3</li>
<li><a
href="https://github.com/puma/puma/commit/63a27b5b5bc333bb123d34359fb0a10488bf7392"><code>63a27b5</code></a>
5.6.9 release note [ci skip]</li>
<li><a
href="https://github.com/puma/puma/commit/cac3fd18cf29ed43719ff5d52d9cfec215f0a043"><code>cac3fd1</code></a>
Merge commit from fork</li>
<li>See full diff in <a
href="https://github.com/puma/puma/compare/v6.4.2...v6.4.3">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=puma&package-manager=bundler&previous-version=6.4.2&new-version=6.4.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>
2024-09-23 11:40:47 -07:00
Chatwoot BotandGitHub f0d00b6965 chore: Update translations (#10124)
- update translations
2024-09-19 16:30:30 -07:00
dependabot[bot]andGitHub f15a4f8aa4 chore(deps): bump google-protobuf from 3.25.3 to 3.25.5 (#10133)
- Bumps google-protobuf from 3.25.3 to 3.25.5.
2024-09-19 16:28:29 -07:00
Sojan c0d5b24881 Merge branch 'release/3.13.0' into develop 2024-09-17 16:47:56 -07:00
Sojan 377fb5c3b9 Bump version to 3.13.0 2024-09-17 16:46:02 -07:00
58e78621ba chore: Custom Roles to manage permissions [ UI ] (#9865)
In admin settings, this Pr will add the UI for managing custom roles (
ref: https://github.com/chatwoot/chatwoot/pull/9995 ). It also handles
the routing logic changes to accommodate fine-tuned permissions.

---------

Co-authored-by: Pranav <pranavrajs@gmail.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2024-09-17 11:40:11 -07:00
Sojan JoseandGitHub fba73c7186 chore: Fix issue with deleting interactive messages (#10123)
- fix issue with deleting interactive messages
2024-09-16 21:05:39 -07:00
Muhsin KelothandGitHub 467700499b fix: message_type in widget conversation create end point (#10120)
The `before_type_cast` method sometimes returns a string for
`message_type`, creating inconsistencies in conversation create API
response.
2024-09-16 22:34:09 +05:30
Muhsin KelothandGitHub 5d52e4e0a6 revert: "fix: message_type inconsistency across message end points" (#10119)
Reverts chatwoot/chatwoot#10108
2024-09-16 20:00:11 +05:30
Muhsin KelothandGitHub 05b8486538 fix: message_type inconsistency across message end points (#10108)
The `before_type_cast` method sometimes returns a string for
`message_type`, creating inconsistencies in different payloads. This
pull request will remove all `before_type_cast` usage and replace it
with `to_i` methods.
2024-09-16 16:14:35 +05:30
PranavandGitHub b9ff164041 fix: Remove draft articles from the help center search (#10116)
Limit the API to return only the published articles in public help
center API.

Fixes https://github.com/chatwoot/chatwoot/issues/10026
2024-09-16 15:47:44 +05:30
PranavandGitHub 73d448a203 fix: Remove feature flag for insert article into editor (#10114)
Fixes #10107
2024-09-16 12:25:49 +05:30
8d8ec23aa1 chore: [Snyk] Security upgrade rspec-rails from 6.1.4 to 6.1.5 (#10066)
![snyk-top-banner](https://github.com/andygongea/OWASP-Benchmark/assets/818805/c518c423-16fe-447e-b67f-ad5a49b5d123)

### Snyk has created this PR to fix 1 vulnerabilities in the rubygems
dependencies of this project.

#### Snyk changed the following file(s):

- `Gemfile`



<details>
<summary>⚠️ <b>Warning</b></summary>

```
Failed to update the Gemfile.lock, please update manually before merging.
```

</details>



#### Vulnerabilities that will be fixed with an upgrade:

|  | Issue | Score | 

:-------------------------:|:-------------------------|:-------------------------
![medium
severity](https://res.cloudinary.com/snyk/image/upload/w_20,h_20/v1561977819/icon/m.png
'medium severity') | Web Cache Poisoning
<br/>[SNYK-RUBY-RACK-1061917](https://snyk.io/vuln/SNYK-RUBY-RACK-1061917)
| &nbsp;&nbsp;**616**&nbsp;&nbsp;




---

> [!IMPORTANT]
>
> - Check the changes in this PR to ensure they won't cause issues with
your project.
> - Max score is 1000. Note that the real score may have changed since
the PR was raised.
> - This PR was automatically created by Snyk using the credentials of a
real user.

---

**Note:** _You are seeing this because you or someone else with access
to this repository has authorized Snyk to open fix PRs._

For more information: <img
src="https://api.segment.io/v1/pixel/track?data=eyJ3cml0ZUtleSI6InJyWmxZcEdHY2RyTHZsb0lYd0dUcVg4WkFRTnNCOUEwIiwiYW5vbnltb3VzSWQiOiJhMWE2MzkzZS03ODdhLTRmYWItOGY1MS0zZjdmN2YzNzVlZDYiLCJldmVudCI6IlBSIHZpZXdlZCIsInByb3BlcnRpZXMiOnsicHJJZCI6ImExYTYzOTNlLTc4N2EtNGZhYi04ZjUxLTNmN2Y3ZjM3NWVkNiJ9fQ=="
width="0" height="0"/>
🧐 [View latest project
report](https://app.snyk.io/org/chatwoot/project/b7197bbd-6200-4f23-931d-c39928584360?utm_source&#x3D;github&amp;utm_medium&#x3D;referral&amp;page&#x3D;fix-pr)
📜 [Customise PR
templates](https://docs.snyk.io/scan-using-snyk/pull-requests/snyk-fix-pull-or-merge-requests/customize-pr-templates)
🛠 [Adjust project
settings](https://app.snyk.io/org/chatwoot/project/b7197bbd-6200-4f23-931d-c39928584360?utm_source&#x3D;github&amp;utm_medium&#x3D;referral&amp;page&#x3D;fix-pr/settings)
📚 [Read about Snyk's upgrade
logic](https://support.snyk.io/hc/en-us/articles/360003891078-Snyk-patches-to-fix-vulnerabilities)

---

**Learn how to fix vulnerabilities with free interactive lessons:**

🦉 [Learn about vulnerability in an interactive lesson of Snyk
Learn.](https://learn.snyk.io/?loc&#x3D;fix-pr)

[//]: #
'snyk:metadata:{"customTemplate":{"variablesUsed":[],"fieldsUsed":[]},"dependencies":[{"name":"rspec-rails","from":"6.1.4","to":"6.1.5"}],"env":"prod","issuesToFix":[{"exploit_maturity":"Proof
of
Concept","id":"SNYK-RUBY-RACK-1061917","priority_score":616,"priority_score_factors":[{"type":"exploit","label":"Proof
of
Concept","score":107},{"type":"fixability","label":true,"score":214},{"type":"cvssScore","label":"5.9","score":295},{"type":"scoreVersion","label":"v1","score":1}],"severity":"medium","title":"Web
Cache Poisoning"},{"exploit_maturity":"Proof of
Concept","id":"SNYK-RUBY-RACK-1061917","priority_score":616,"priority_score_factors":[{"type":"exploit","label":"Proof
of
Concept","score":107},{"type":"fixability","label":true,"score":214},{"type":"cvssScore","label":"5.9","score":295},{"type":"scoreVersion","label":"v1","score":1}],"severity":"medium","title":"Web
Cache Poisoning"},{"exploit_maturity":"Proof of
Concept","id":"SNYK-RUBY-RACK-1061917","priority_score":616,"priority_score_factors":[{"type":"exploit","label":"Proof
of
Concept","score":107},{"type":"fixability","label":true,"score":214},{"type":"cvssScore","label":"5.9","score":295},{"type":"scoreVersion","label":"v1","score":1}],"severity":"medium","title":"Web
Cache Poisoning"},{"exploit_maturity":"Proof of
Concept","id":"SNYK-RUBY-RACK-1061917","priority_score":616,"priority_score_factors":[{"type":"exploit","label":"Proof
of
Concept","score":107},{"type":"fixability","label":true,"score":214},{"type":"cvssScore","label":"5.9","score":295},{"type":"scoreVersion","label":"v1","score":1}],"severity":"medium","title":"Web
Cache
Poisoning"}],"prId":"a1a6393e-787a-4fab-8f51-3f7f7f375ed6","prPublicId":"a1a6393e-787a-4fab-8f51-3f7f7f375ed6","packageManager":"rubygems","priorityScoreList":[616],"projectPublicId":"b7197bbd-6200-4f23-931d-c39928584360","projectUrl":"https://app.snyk.io/org/chatwoot/project/b7197bbd-6200-4f23-931d-c39928584360?utm_source=github&utm_medium=referral&page=fix-pr","prType":"fix","templateFieldSources":{"branchName":"default","commitMessage":"default","description":"default","title":"default"},"templateVariants":["updated-fix-title","pr-warning-shown","priorityScore"],"type":"auto","upgrade":["SNYK-RUBY-RACK-1061917"],"vulns":["SNYK-RUBY-RACK-1061917"],"patch":[],"isBreakingChange":false,"remediationStrategy":"vuln"}'

---------

Co-authored-by: snyk-bot <snyk-bot@snyk.io>
2024-09-16 09:44:07 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
add6403aa6 chore(deps): bump express from 4.19.2 to 4.20.0 (#10093)
Bumps [express](https://github.com/expressjs/express) from 4.19.2 to
4.20.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/expressjs/express/releases">express's
releases</a>.</em></p>
<blockquote>
<h2>4.20.0</h2>
<h2>What's Changed</h2>
<h3>Important</h3>
<ul>
<li>IMPORTANT: The default <code>depth</code> level for parsing
URL-encoded data is now <code>32</code> (previously was
<code>Infinity</code>)</li>
<li>Remove link renderization in html while using
<code>res.redirect</code></li>
</ul>
<h3>Other Changes</h3>
<ul>
<li>4.19.2 Staging by <a
href="https://github.com/wesleytodd"><code>@​wesleytodd</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/5561">expressjs/express#5561</a></li>
<li>remove duplicate location test for data uri by <a
href="https://github.com/wesleytodd"><code>@​wesleytodd</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/5562">expressjs/express#5562</a></li>
<li>feat: document beta releases expectations by <a
href="https://github.com/marco-ippolito"><code>@​marco-ippolito</code></a>
in <a
href="https://redirect.github.com/expressjs/express/pull/5565">expressjs/express#5565</a></li>
<li>Cut down on duplicated CI runs by <a
href="https://github.com/jonchurch"><code>@​jonchurch</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/5564">expressjs/express#5564</a></li>
<li>Add a Threat Model by <a
href="https://github.com/UlisesGascon"><code>@​UlisesGascon</code></a>
in <a
href="https://redirect.github.com/expressjs/express/pull/5526">expressjs/express#5526</a></li>
<li>Assign captain of encodeurl by <a
href="https://github.com/blakeembrey"><code>@​blakeembrey</code></a> in
<a
href="https://redirect.github.com/expressjs/express/pull/5579">expressjs/express#5579</a></li>
<li>Nominate jonchurch as repo captain for <code>http-errors</code>,
<code>expressjs.com</code>, <code>morgan</code>, <code>cors</code>,
<code>body-parser</code> by <a
href="https://github.com/jonchurch"><code>@​jonchurch</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/5587">expressjs/express#5587</a></li>
<li>docs: update Security.md by <a
href="https://github.com/inigomarquinez"><code>@​inigomarquinez</code></a>
in <a
href="https://redirect.github.com/expressjs/express/pull/5590">expressjs/express#5590</a></li>
<li>docs: update triage nomination policy by <a
href="https://github.com/UlisesGascon"><code>@​UlisesGascon</code></a>
in <a
href="https://redirect.github.com/expressjs/express/pull/5600">expressjs/express#5600</a></li>
<li>Add CodeQL (SAST) by <a
href="https://github.com/UlisesGascon"><code>@​UlisesGascon</code></a>
in <a
href="https://redirect.github.com/expressjs/express/pull/5433">expressjs/express#5433</a></li>
<li>docs: add UlisesGascon as triage initiative captain by <a
href="https://github.com/UlisesGascon"><code>@​UlisesGascon</code></a>
in <a
href="https://redirect.github.com/expressjs/express/pull/5605">expressjs/express#5605</a></li>
<li>deps: encodeurl@~2.0.0 by <a
href="https://github.com/blakeembrey"><code>@​blakeembrey</code></a> in
<a
href="https://redirect.github.com/expressjs/express/pull/5569">expressjs/express#5569</a></li>
<li>skip QUERY method test by <a
href="https://github.com/jonchurch"><code>@​jonchurch</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/5628">expressjs/express#5628</a></li>
<li>ignore ETAG query test on 21 and 22, reuse skip util by <a
href="https://github.com/jonchurch"><code>@​jonchurch</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/5639">expressjs/express#5639</a></li>
<li>add support Node.js@22 in the CI by <a
href="https://github.com/mertcanaltin"><code>@​mertcanaltin</code></a>
in <a
href="https://redirect.github.com/expressjs/express/pull/5627">expressjs/express#5627</a></li>
<li>doc: add table of contents, tc/triager lists to readme by <a
href="https://github.com/mertcanaltin"><code>@​mertcanaltin</code></a>
in <a
href="https://redirect.github.com/expressjs/express/pull/5619">expressjs/express#5619</a></li>
<li>List and sort all projects, add captains by <a
href="https://github.com/blakeembrey"><code>@​blakeembrey</code></a> in
<a
href="https://redirect.github.com/expressjs/express/pull/5653">expressjs/express#5653</a></li>
<li>docs: add <a
href="https://github.com/UlisesGascon"><code>@​UlisesGascon</code></a>
as captain for cookie-parser by <a
href="https://github.com/UlisesGascon"><code>@​UlisesGascon</code></a>
in <a
href="https://redirect.github.com/expressjs/express/pull/5666">expressjs/express#5666</a></li>
<li> bring back query tests for node 21 by <a
href="https://github.com/ctcpip"><code>@​ctcpip</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/5690">expressjs/express#5690</a></li>
<li>[v4] Deprecate <code>res.clearCookie</code> accepting
<code>options.maxAge</code> and <code>options.expires</code> by <a
href="https://github.com/jonchurch"><code>@​jonchurch</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/5672">expressjs/express#5672</a></li>
<li>skip QUERY tests for Node 21 only, still not supported by <a
href="https://github.com/jonchurch"><code>@​jonchurch</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/5695">expressjs/express#5695</a></li>
<li>📝 update people, add ctcpip to TC by <a
href="https://github.com/ctcpip"><code>@​ctcpip</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/5683">expressjs/express#5683</a></li>
<li>remove minor version pinning from ci by <a
href="https://github.com/jonchurch"><code>@​jonchurch</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/5722">expressjs/express#5722</a></li>
<li>Fix link variable use in attribution section of CODE OF CONDUCT by
<a href="https://github.com/IamLizu"><code>@​IamLizu</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/5762">expressjs/express#5762</a></li>
<li>Replace Appveyor windows testing with GHA by <a
href="https://github.com/jonchurch"><code>@​jonchurch</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/5599">expressjs/express#5599</a></li>
<li>Add OSSF Scorecard badge by <a
href="https://github.com/UlisesGascon"><code>@​UlisesGascon</code></a>
in <a
href="https://redirect.github.com/expressjs/express/pull/5436">expressjs/express#5436</a></li>
<li>update scorecard link by <a
href="https://github.com/bjohansebas"><code>@​bjohansebas</code></a> in
<a
href="https://redirect.github.com/expressjs/express/pull/5814">expressjs/express#5814</a></li>
<li>Nominate <a
href="https://github.com/IamLizu"><code>@​IamLizu</code></a> to the
triage team by <a
href="https://github.com/UlisesGascon"><code>@​UlisesGascon</code></a>
in <a
href="https://redirect.github.com/expressjs/express/pull/5836">expressjs/express#5836</a></li>
<li>deps: path-to-regexp@0.1.8 by <a
href="https://github.com/blakeembrey"><code>@​blakeembrey</code></a> in
<a
href="https://redirect.github.com/expressjs/express/pull/5603">expressjs/express#5603</a></li>
<li>docs: specify new instructions for <code>question</code> and
<code>discuss</code> by <a
href="https://github.com/IamLizu"><code>@​IamLizu</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/5835">expressjs/express#5835</a></li>
<li>4.x: Upgrade <code>merge-descriptors</code> dependency by <a
href="https://github.com/RobinTail"><code>@​RobinTail</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/5781">expressjs/express#5781</a></li>
<li>path-to-regexp@0.1.10 by <a
href="https://github.com/blakeembrey"><code>@​blakeembrey</code></a> in
<a
href="https://redirect.github.com/expressjs/express/pull/5902">expressjs/express#5902</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/marco-ippolito"><code>@​marco-ippolito</code></a>
made their first contribution in <a
href="https://redirect.github.com/expressjs/express/pull/5565">expressjs/express#5565</a></li>
<li><a
href="https://github.com/inigomarquinez"><code>@​inigomarquinez</code></a>
made their first contribution in <a
href="https://redirect.github.com/expressjs/express/pull/5590">expressjs/express#5590</a></li>
<li><a
href="https://github.com/mertcanaltin"><code>@​mertcanaltin</code></a>
made their first contribution in <a
href="https://redirect.github.com/expressjs/express/pull/5627">expressjs/express#5627</a></li>
<li><a href="https://github.com/ctcpip"><code>@​ctcpip</code></a> made
their first contribution in <a
href="https://redirect.github.com/expressjs/express/pull/5690">expressjs/express#5690</a></li>
<li><a
href="https://github.com/bjohansebas"><code>@​bjohansebas</code></a>
made their first contribution in <a
href="https://redirect.github.com/expressjs/express/pull/5814">expressjs/express#5814</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/expressjs/express/compare/4.19.1...4.20.0">https://github.com/expressjs/express/compare/4.19.1...4.20.0</a></p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/expressjs/express/blob/master/History.md">express's
changelog</a>.</em></p>
<blockquote>
<h1>4.20.0 / 2024-09-10</h1>
<ul>
<li>deps: serve-static@0.16.0
<ul>
<li>Remove link renderization in html while redirecting</li>
</ul>
</li>
<li>deps: send@0.19.0
<ul>
<li>Remove link renderization in html while redirecting</li>
</ul>
</li>
<li>deps: body-parser@0.6.0
<ul>
<li>add <code>depth</code> option to customize the depth level in the
parser</li>
<li>IMPORTANT: The default <code>depth</code> level for parsing
URL-encoded data is now <code>32</code> (previously was
<code>Infinity</code>)</li>
</ul>
</li>
<li>Remove link renderization in html while using
<code>res.redirect</code></li>
<li>deps: path-to-regexp@0.1.10
<ul>
<li>Adds support for named matching groups in the routes using a
regex</li>
<li>Adds backtracking protection to parameters without regexes
defined</li>
</ul>
</li>
<li>deps: encodeurl@~2.0.0
<ul>
<li>Removes encoding of <code>\</code>, <code>|</code>, and
<code>^</code> to align better with URL spec</li>
</ul>
</li>
<li>Deprecate passing <code>options.maxAge</code> and
<code>options.expires</code> to <code>res.clearCookie</code>
<ul>
<li>Will be ignored in v5, clearCookie will set a cookie with an expires
in the past to instruct clients to delete the cookie</li>
</ul>
</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/expressjs/express/commit/21df421ebc7a5249bb31101da666bbf22adc3f18"><code>21df421</code></a>
4.20.0</li>
<li><a
href="https://github.com/expressjs/express/commit/4c9ddc1c47bf579e55c2fe837d76a952e9fd8959"><code>4c9ddc1</code></a>
feat: upgrade to serve-static@0.16.0</li>
<li><a
href="https://github.com/expressjs/express/commit/9ebe5d500d22cbb2b8aaa73446866b084c747971"><code>9ebe5d5</code></a>
feat: upgrade to send@0.19.0 (<a
href="https://redirect.github.com/expressjs/express/issues/5928">#5928</a>)</li>
<li><a
href="https://github.com/expressjs/express/commit/ec4a01b6b8814d7b007f36a3023f4dbafdbc3d09"><code>ec4a01b</code></a>
feat: upgrade to body-parser@1.20.3 (<a
href="https://redirect.github.com/expressjs/express/issues/5926">#5926</a>)</li>
<li><a
href="https://github.com/expressjs/express/commit/54271f69b511fea198471e6ff3400ab805d6b553"><code>54271f6</code></a>
fix: don't render redirect values in anchor href</li>
<li><a
href="https://github.com/expressjs/express/commit/125bb742a38cd97938a3932b47cc301e41c31f5d"><code>125bb74</code></a>
path-to-regexp@0.1.10 (<a
href="https://redirect.github.com/expressjs/express/issues/5902">#5902</a>)</li>
<li><a
href="https://github.com/expressjs/express/commit/2a980ad16052e53b398c9953fea50e3daa0b495c"><code>2a980ad</code></a>
merge-descriptors@1.0.3 (<a
href="https://redirect.github.com/expressjs/express/issues/5781">#5781</a>)</li>
<li><a
href="https://github.com/expressjs/express/commit/a3e7e05e0a435b7b4be25bd38d8d0ca19a773ca9"><code>a3e7e05</code></a>
docs: specify new instructions for <code>question</code> and
<code>discuss</code></li>
<li><a
href="https://github.com/expressjs/express/commit/c5addb9a17c5b4c9fccdd2c04153a30595e03385"><code>c5addb9</code></a>
deps: path-to-regexp@0.1.8 (<a
href="https://redirect.github.com/expressjs/express/issues/5603">#5603</a>)</li>
<li><a
href="https://github.com/expressjs/express/commit/e35380a39d94937e3d0f7119e0efbc7cd69d003f"><code>e35380a</code></a>
docs: add <a
href="https://github.com/IamLizu"><code>@​IamLizu</code></a> to the
triage team (<a
href="https://redirect.github.com/expressjs/express/issues/5836">#5836</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/expressjs/express/compare/4.19.2...4.20.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=express&package-manager=npm_and_yarn&previous-version=4.19.2&new-version=4.20.0)](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>
2024-09-16 09:42:56 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
4b779dbe0f chore(deps): bump dset from 3.1.2 to 3.1.4 (#10097)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-16 09:42:14 +05:30
Sojan JoseandGitHub ab86f62fcc chore: Update GPT Model (#10111)
Update the open AI model, as 3.5 is being deprecated. Provide as option to swap out models via environment variables.
2024-09-16 09:41:20 +05:30
aaab2ac788 fix: Cannot open conversation from contact sidebar. (#10102)
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2024-09-13 17:58:23 +05:30
Sivin VargheseandGitHub aa4edaa1e6 feat: Display emoji names and improve search (#10104)
This PR enhances the emoji search functionality in the editor's emoji selector by improving how emoji names are displayed and searched.

The UI now shows emoji names instead of slugs, and the search logic has been updated to generate `searchString` without whitespaces, allowing users to search for emojis like 'face_with' using 'facewith'
2024-09-13 10:54:18 +05:30
a76cd7684a chore: Replace darkmode mixin with useDarkMode composable [CW-3474] (#9949)
# Pull Request Template

## Description

Replaces darkModeMixin with the new useDarkMode composable and replaces
wll usages of mixin the the composable in components and pages

Fixes
https://linear.app/chatwoot/issue/CW-3474/rewrite-darkmodemixin-mixin-to-a-composable

## Type of change

Please delete options that are not relevant.

- [x] New feature (non-breaking change which adds functionality)

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2024-09-12 00:29:41 +05:30
Muhsin KelothandGitHub 2c17c95eab feat: Add the ability to paste images to editor (#10072) 2024-09-11 09:44:13 +05:30
Sojan JoseandGitHub bb74c621b5 chore: Bump up character limit for email channel (#10082) 2024-09-07 08:41:18 +05:30
Muhsin KelothandGitHub 185a122ec7 fix: Contact filter attribute groups (#10080) 2024-09-06 13:41:48 +05:30
Sivin VargheseandGitHub 05dece826d fix: Editor controls not working after newline (Shift+Enter) (#10079) 2024-09-06 12:38:45 +05:30
Muhsin KelothandGitHub 528b984a8d feat: Extend upload API end point to support external images (#10062) 2024-09-05 10:42:54 +05:30
Sojan JoseandGitHub db0e654c03 chore: [Snyk] Security upgrade meta_request from 0.8.2 to 0.8.3 (#10061)
snyk security updates
2024-09-04 17:04:07 -07:00
e99e6a8443 chore: [Snyk] Security upgrade omniauth-google-oauth2 from 1.1.2 to 1.1.3 (#10058)
- Security updates from snyk 

Co-authored-by: snyk-bot <snyk-bot@snyk.io>
2024-09-04 16:14:50 -07:00
Sojan JoseandGitHub d6fd3ce048 chore: Fix flaky permissions test (#10074)
- the permissions tests were failing when the random number turns out to
be 0, fixing this case and ensuring that its always between, 1-4.
2024-09-04 16:13:29 -07:00
Sivin VargheseandGitHub 8a2f652b94 fix: TypeError cannot read properties of undefined (reading 'click') (#10067)
Fixes https://linear.app/chatwoot/issue/CW-3535/typeerror-cannot-read-properties-of-undefined-reading-click
2024-09-04 11:34:08 +05:30
Sivin VargheseandGitHub a3732c8f51 feat: Adds support for selecting emojis using the keyboard (#10055) 2024-09-04 11:32:54 +05:30
3a0e68030a chore: [Snyk] Fix for 1 vulnerabilities (#10038)
fix for vulnerabilties

------
Co-authored-by: snyk-bot <snyk-bot@snyk.io>
2024-08-29 18:10:13 -07:00
PranavandGitHub f087461abc feat: Update the design for the webhook management page (#10050) 2024-08-29 20:29:27 +05:30
3a47b7e3d1 feat(design): Update the design for the custom attribute console (#10049)
This PR continues the design update series, updates the design for the custom attributes management page. This PR improves the interaction in the Add Custom Attribute feature. Now, the attribute model in the add attribute form will default to the currently selected tab.


---------

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2024-08-29 19:06:11 +05:30
Muhsin KelothandGitHub 6dda1e8c8f feat: Added support for copying and pasting images in article editor (#10044) 2024-08-29 18:50:52 +05:30
098825c149 fix: default return in blocked domains (#10046)
Co-authored-by: Vishnu Narayanan <iamwishnu@gmail.com>
2024-08-29 15:36:57 +05:30
31e7663258 feat: Update the design for the Inbox management console (#10043)
This is the continuation of the design update PR. This changes the design for the inbox pages.
---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2024-08-29 11:19:32 +05:30
933 changed files with 13048 additions and 3477 deletions
+5 -5
View File
@@ -96,12 +96,12 @@ gem 'koala'
# slack client
gem 'slack-ruby-client', '~> 2.2.0'
# for dialogflow integrations
gem 'google-cloud-dialogflow-v2'
gem 'google-cloud-dialogflow-v2', '>= 0.24.0'
gem 'grpc'
# Translate integrations
# 'google-cloud-translate' gem depends on faraday 2.0 version
# this dependency breaks the slack-ruby-client gem
gem 'google-cloud-translate-v3'
gem 'google-cloud-translate-v3', '>= 0.7.0'
##-- apm and error monitoring ---#
# loaded only when environment variables are set.
@@ -165,7 +165,7 @@ gem 'audited', '~> 5.4', '>= 5.4.1'
# need for google auth
gem 'omniauth', '>= 2.1.2'
gem 'omniauth-google-oauth2', '>= 1.1.2'
gem 'omniauth-google-oauth2', '>= 1.1.3'
gem 'omniauth-rails_csrf_protection', '~> 1.0', '>= 1.0.2'
## Gems for reponse bot
@@ -200,7 +200,7 @@ group :development do
gem 'rack-mini-profiler', '>= 3.2.0', require: false
gem 'stackprof'
# Should install the associated chrome extension to view query logs
gem 'meta_request', '>= 0.8.0'
gem 'meta_request', '>= 0.8.3'
end
group :test do
@@ -228,7 +228,7 @@ group :development, :test do
gem 'mock_redis'
gem 'pry-rails'
gem 'rspec_junit_formatter'
gem 'rspec-rails', '>= 6.1.4'
gem 'rspec-rails', '>= 6.1.5'
gem 'rubocop', require: false
gem 'rubocop-performance', require: false
gem 'rubocop-rails', require: false
+30 -30
View File
@@ -257,7 +257,7 @@ GEM
faraday-net_http_persistent (2.1.0)
faraday (~> 2.5)
net-http-persistent (~> 4.0)
faraday-retry (2.1.0)
faraday-retry (2.2.1)
faraday (~> 2.0)
fcm (1.0.8)
faraday (>= 1.0.0, < 3.0)
@@ -271,7 +271,7 @@ GEM
fugit (1.11.1)
et-orbi (~> 1, >= 1.2.11)
raabro (~> 1.4)
gapic-common (0.18.0)
gapic-common (0.20.0)
faraday (>= 1.9, < 3.a)
faraday-retry (>= 1.0, < 3.a)
google-protobuf (~> 3.14)
@@ -301,15 +301,15 @@ GEM
google-cloud-core (1.6.0)
google-cloud-env (~> 1.0)
google-cloud-errors (~> 1.0)
google-cloud-dialogflow-v2 (0.23.0)
gapic-common (>= 0.18.0, < 2.a)
google-cloud-dialogflow-v2 (0.31.0)
gapic-common (>= 0.20.0, < 2.a)
google-cloud-errors (~> 1.0)
google-cloud-location (>= 0.4, < 2.a)
google-cloud-env (1.6.0)
faraday (>= 0.17.3, < 3.0)
google-cloud-errors (1.3.1)
google-cloud-location (0.4.0)
gapic-common (>= 0.17.1, < 2.a)
google-cloud-location (0.6.0)
gapic-common (>= 0.20.0, < 2.a)
google-cloud-errors (~> 1.0)
google-cloud-storage (1.44.0)
addressable (~> 2.8)
@@ -319,17 +319,17 @@ GEM
google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0)
google-cloud-translate-v3 (0.6.0)
gapic-common (>= 0.17.1, < 2.a)
google-cloud-translate-v3 (0.10.0)
gapic-common (>= 0.20.0, < 2.a)
google-cloud-errors (~> 1.0)
google-protobuf (3.25.3)
google-protobuf (3.25.3-arm64-darwin)
google-protobuf (3.25.3-x86_64-darwin)
google-protobuf (3.25.3-x86_64-linux)
googleapis-common-protos (1.4.0)
google-protobuf (~> 3.14)
googleapis-common-protos-types (~> 1.2)
grpc (~> 1.27)
google-protobuf (3.25.5)
google-protobuf (3.25.5-arm64-darwin)
google-protobuf (3.25.5-x86_64-darwin)
google-protobuf (3.25.5-x86_64-linux)
googleapis-common-protos (1.6.0)
google-protobuf (>= 3.18, < 5.a)
googleapis-common-protos-types (~> 1.7)
grpc (~> 1.41)
googleapis-common-protos-types (1.14.0)
google-protobuf (~> 3.18)
googleauth (1.5.2)
@@ -457,7 +457,7 @@ GEM
marcel (1.0.4)
maxminddb (0.1.22)
memoist (0.16.2)
meta_request (0.8.2)
meta_request (0.8.3)
rack-contrib (>= 1.1, < 3)
railties (>= 3.0.0, < 8)
method_source (1.1.0)
@@ -522,7 +522,7 @@ GEM
hashie (>= 3.4.6)
rack (>= 2.2.3)
rack-protection
omniauth-google-oauth2 (1.1.2)
omniauth-google-oauth2 (1.1.3)
jwt (>= 2.0)
oauth2 (~> 2.0)
omniauth (~> 2.0)
@@ -552,7 +552,7 @@ GEM
pry-rails (0.3.9)
pry (>= 0.10.4)
public_suffix (6.0.0)
puma (6.4.2)
puma (6.4.3)
nio4r (~> 2.0)
pundit (2.3.0)
activesupport (>= 3.0.0)
@@ -638,16 +638,16 @@ GEM
strscan
rspec-core (3.13.0)
rspec-support (~> 3.13.0)
rspec-expectations (3.13.1)
rspec-expectations (3.13.2)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-mocks (3.13.1)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-rails (6.1.4)
actionpack (>= 6.1)
activesupport (>= 6.1)
railties (>= 6.1)
rspec-rails (7.0.1)
actionpack (>= 7.0)
activesupport (>= 7.0)
railties (>= 7.0)
rspec-core (~> 3.13)
rspec-expectations (~> 3.13)
rspec-mocks (~> 3.13)
@@ -819,7 +819,7 @@ GEM
rack-proxy (>= 0.6.1)
railties (>= 5.2)
semantic_range (>= 2.3.0)
webrick (1.8.1)
webrick (1.8.2)
websocket-driver (0.7.6)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
@@ -881,9 +881,9 @@ DEPENDENCIES
foreman
geocoder
gmail_xoauth
google-cloud-dialogflow-v2
google-cloud-dialogflow-v2 (>= 0.24.0)
google-cloud-storage
google-cloud-translate-v3
google-cloud-translate-v3 (>= 0.7.0)
groupdate
grpc
haikunator
@@ -903,14 +903,14 @@ DEPENDENCIES
listen
lograge (~> 0.14.0)
maxminddb
meta_request (>= 0.8.0)
meta_request (>= 0.8.3)
mock_redis
neighbor
net-smtp (~> 0.3.4)
newrelic-sidekiq-metrics (>= 1.6.2)
newrelic_rpm
omniauth (>= 2.1.2)
omniauth-google-oauth2 (>= 1.1.2)
omniauth-google-oauth2 (>= 1.1.3)
omniauth-oauth2
omniauth-rails_csrf_protection (~> 1.0, >= 1.0.2)
pg
@@ -930,7 +930,7 @@ DEPENDENCIES
responders (>= 3.1.1)
rest-client
reverse_markdown
rspec-rails (>= 6.1.4)
rspec-rails (>= 6.1.5)
rspec_junit_formatter
rubocop
rubocop-performance
+2 -2
View File
@@ -94,8 +94,8 @@ class AccountBuilder
def blocked_domains
domains = GlobalConfigService.load('BLOCKED_EMAIL_DOMAINS', '')
domains.split("\n").map(&:strip) if domains.present?
return [] if domains.blank?
[]
domains.split("\n").map(&:strip)
end
end
@@ -13,7 +13,7 @@ class Api::V1::Accounts::Conversations::MessagesController < Api::V1::Accounts::
def destroy
ActiveRecord::Base.transaction do
message.update!(content: I18n.t('conversations.messages.deleted'), content_attributes: { deleted: true })
message.update!(content: I18n.t('conversations.messages.deleted'), content_type: :text, content_attributes: { deleted: true })
message.attachments.destroy_all
end
end
@@ -1,13 +1,68 @@
class Api::V1::Accounts::UploadController < Api::V1::Accounts::BaseController
def create
file_blob = ActiveStorage::Blob.create_and_upload!(
key: nil,
io: params[:attachment].tempfile,
filename: params[:attachment].original_filename,
content_type: params[:attachment].content_type
)
file_blob.save!
result = if params[:attachment].present?
create_from_file
elsif params[:external_url].present?
create_from_url
else
render_error('No file or URL provided', :unprocessable_entity)
end
render_success(result) if result.is_a?(ActiveStorage::Blob)
end
private
def create_from_file
attachment = params[:attachment]
create_and_save_blob(attachment.tempfile, attachment.original_filename, attachment.content_type)
end
def create_from_url
uri = parse_uri(params[:external_url])
return if performed?
fetch_and_process_file_from_uri(uri)
end
def parse_uri(url)
uri = URI.parse(url)
validate_uri(uri)
uri
rescue URI::InvalidURIError, SocketError
render_error('Invalid URL provided', :unprocessable_entity)
nil
end
def validate_uri(uri)
raise URI::InvalidURIError unless uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
end
def fetch_and_process_file_from_uri(uri)
uri.open do |file|
create_and_save_blob(file, File.basename(uri.path), file.content_type)
end
rescue OpenURI::HTTPError => e
render_error("Failed to fetch file from URL: #{e.message}", :unprocessable_entity)
rescue SocketError
render_error('Invalid URL provided', :unprocessable_entity)
rescue StandardError
render_error('An unexpected error occurred', :internal_server_error)
end
def create_and_save_blob(io, filename, content_type)
ActiveStorage::Blob.create_and_upload!(
io: io,
filename: filename,
content_type: content_type
)
end
def render_success(file_blob)
render json: { file_url: url_for(file_blob), blob_key: file_blob.key, blob_id: file_blob.id }
end
def render_error(message, status)
render json: { error: message }, status: status
end
end
@@ -11,6 +11,8 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
process_update_contact
@conversation = create_conversation
conversation.messages.create!(message_params)
# TODO: Temporary fix for message type cast issue, since message_type is returning as string instead of integer
conversation.reload
end
end
@@ -66,7 +66,9 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
end
def check_authorization
raise Pundit::NotAuthorizedError unless Current.account_user.administrator?
return if Current.account_user.administrator?
raise Pundit::NotAuthorizedError
end
def common_params
@@ -135,3 +137,5 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
V2::ReportBuilder.new(Current.account, conversation_params).conversation_metrics
end
end
Api::V2::Accounts::ReportsController.prepend_mod_with('Api::V2::Accounts::ReportsController')
@@ -6,7 +6,7 @@ class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::B
layout 'portal'
def index
@articles = @portal.articles
@articles = @portal.articles.published
search_articles
order_by_sort_param
@articles.page(list_params[:page]) if list_params[:page].present?
@@ -0,0 +1,9 @@
import ApiClient from './ApiClient';
class CustomRole extends ApiClient {
constructor() {
super('custom_roles', { accountScoped: true });
}
}
export default new CustomRole();
@@ -27,6 +27,11 @@ import {
} from '../store/modules/conversations/helpers/actionHelpers';
import { CONVERSATION_EVENTS } from '../helper/AnalyticsHelper/events';
import IntersectionObserver from './IntersectionObserver.vue';
import {
getUserPermissions,
filterItemsByPermission,
} from 'dashboard/helper/permissionsHelper.js';
import { ASSIGNEE_TYPE_TAB_PERMISSIONS } from 'dashboard/constants/permissions.js';
export default {
components: {
@@ -117,35 +122,41 @@ export default {
lastConversationIndex,
};
};
const handlePreviousConversation = () => {
const { allConversations, activeConversationIndex } =
getKeyboardListenerParams();
if (activeConversationIndex === -1) {
allConversations[0].click();
}
if (activeConversationIndex >= 1) {
allConversations[activeConversationIndex - 1].click();
}
};
const handleNextConversation = () => {
const handleConversationNavigation = direction => {
const {
allConversations,
activeConversationIndex,
lastConversationIndex,
} = getKeyboardListenerParams();
if (activeConversationIndex === -1) {
allConversations[lastConversationIndex].click();
} else if (activeConversationIndex < lastConversationIndex) {
allConversations[activeConversationIndex + 1].click();
// Determine the new index based on the direction
const newIndex =
direction === 'previous'
? activeConversationIndex - 1
: activeConversationIndex + 1;
// Check if the new index is within the valid range
if (
allConversations.length > 0 &&
newIndex >= 0 &&
newIndex <= lastConversationIndex
) {
// Click the conversation at the new index
allConversations[newIndex].click();
} else if (allConversations.length > 0) {
// If the new index is out of range, click the first or last conversation based on the direction
const fallbackIndex =
direction === 'previous' ? 0 : lastConversationIndex;
allConversations[fallbackIndex].click();
}
};
const keyboardEvents = {
'Alt+KeyJ': {
action: () => handlePreviousConversation(),
action: () => handleConversationNavigation('previous'),
allowOnFocusedInput: true,
},
'Alt+KeyK': {
action: () => handleNextConversation(),
action: () => handleConversationNavigation('next'),
allowOnFocusedInput: true,
},
};
@@ -198,6 +209,7 @@ export default {
computed: {
...mapGetters({
currentUser: 'getCurrentUser',
currentAccountId: 'getCurrentAccountId',
chatLists: 'getAllConversations',
mineChatsList: 'getMineChats',
allChatList: 'getAllStatusChats',
@@ -237,20 +249,19 @@ export default {
name,
};
},
userPermissions() {
return getUserPermissions(this.currentUser, this.currentAccountId);
},
assigneeTabItems() {
const ASSIGNEE_TYPE_TAB_KEYS = {
me: 'mineCount',
unassigned: 'unAssignedCount',
all: 'allCount',
};
return Object.keys(ASSIGNEE_TYPE_TAB_KEYS).map(key => {
const count = this.conversationStats[ASSIGNEE_TYPE_TAB_KEYS[key]] || 0;
return {
key,
name: this.$t(`CHAT_LIST.ASSIGNEE_TYPE_TABS.${key}`),
count,
};
});
return filterItemsByPermission(
ASSIGNEE_TYPE_TAB_PERMISSIONS,
this.userPermissions,
item => item.permissions
).map(({ key, count: countKey }) => ({
key,
name: this.$t(`CHAT_LIST.ASSIGNEE_TYPE_TABS.${key}`),
count: this.conversationStats[countKey] || 0,
}));
},
showAssigneeInConversationCard() {
return (
@@ -331,10 +331,12 @@ export default {
::v-deep {
.selector-wrap {
@apply m-0 top-1;
.selector-name {
@apply ml-0;
}
}
.name {
@apply ml-0;
}
@@ -39,6 +39,7 @@ const settings = accountId => ({
'settings_teams_list',
'settings_teams_new',
'sla_list',
'custom_roles_list',
],
menuItems: [
{
@@ -178,6 +179,18 @@ const settings = accountId => ({
isEnterpriseOnly: true,
featureFlag: FEATURE_FLAGS.AUDIT_LOGS,
},
{
icon: 'scan-person',
label: 'CUSTOM_ROLES',
hasSubMenu: false,
meta: {
permissions: ['administrator'],
},
toState: frontendURL(`accounts/${accountId}/settings/custom-roles/list`),
toStateName: 'custom_roles_list',
isEnterpriseOnly: true,
beta: true,
},
{
icon: 'document-list-clock',
label: 'SLA',
@@ -52,9 +52,13 @@ export default {
{{ account.name }}
</div>
<div
class="text-xs font-medium text-slate-500 dark:text-slate-500 hover:underline-offset-4"
class="text-xs font-medium lowercase text-slate-500 dark:text-slate-500 hover:underline-offset-4"
>
{{ account.role }}
{{
account.custom_role_id
? account.custom_role.name
: account.role
}}
</div>
</label>
</span>
@@ -21,8 +21,11 @@ const activeTabIndex = computed(() => {
});
const onTabChange = selectedTabIndex => {
if (props.items[selectedTabIndex].key !== props.activeTab) {
emit('chatTabChange', props.items[selectedTabIndex].key);
if (selectedTabIndex >= 0 && selectedTabIndex < props.items.length) {
const selectedItem = props.items[selectedTabIndex];
if (selectedItem.key !== props.activeTab) {
emit('chatTabChange', selectedItem.key);
}
}
};
@@ -32,7 +35,8 @@ const keyboardEvents = {
if (props.activeTab === wootConstants.ASSIGNEE_TYPE.ALL) {
onTabChange(0);
} else {
onTabChange(activeTabIndex.value + 1);
const nextIndex = (activeTabIndex.value + 1) % props.items.length;
onTabChange(nextIndex);
}
},
},
@@ -1,38 +1,34 @@
<script>
export default {
props: {
text: {
type: String,
default: '',
},
limit: {
type: Number,
default: 120,
},
},
data() {
return {
showMore: false,
};
},
computed: {
textToBeDisplayed() {
if (this.showMore || this.text.length <= this.limit) {
return this.text;
}
<script setup>
import { computed, ref } from 'vue';
import { useI18n } from 'dashboard/composables/useI18n';
return this.text.slice(0, this.limit) + '...';
},
buttonLabel() {
const i18nKey = !this.showMore ? 'SHOW_MORE' : 'SHOW_LESS';
return this.$t(`COMPONENTS.SHOW_MORE_BLOCK.${i18nKey}`);
},
const props = defineProps({
text: {
type: String,
default: '',
},
methods: {
toggleShowMore() {
this.showMore = !this.showMore;
},
limit: {
type: Number,
default: 120,
},
});
const { t } = useI18n();
const showMore = ref(false);
const textToBeDisplayed = computed(() => {
if (showMore.value || props.text.length <= props.limit) {
return props.text;
}
return props.text.slice(0, props.limit) + '...';
});
const buttonLabel = computed(() => {
const i18nKey = !showMore.value ? 'SHOW_MORE' : 'SHOW_LESS';
return t(`COMPONENTS.SHOW_MORE_BLOCK.${i18nKey}`);
});
const toggleShowMore = () => {
showMore.value = !showMore.value;
};
</script>
@@ -41,16 +37,10 @@ export default {
{{ textToBeDisplayed }}
<button
v-if="text.length > limit"
class="show-more--button"
class="text-woot-500 !p-0 !border-0 align-top"
@click="toggleShowMore"
>
{{ buttonLabel }}
</button>
</span>
</template>
<style scoped>
.show-more--button {
color: var(--w-500);
}
</style>
@@ -17,6 +17,8 @@ import { BUS_EVENTS } from 'shared/constants/busEvents';
import TagAgents from '../conversation/TagAgents.vue';
import CannedResponse from '../conversation/CannedResponse.vue';
import VariableList from '../conversation/VariableList.vue';
import KeyboardEmojiSelector from './keyboardEmojiSelector.vue';
import {
appendSignature,
removeSignature,
@@ -24,6 +26,7 @@ import {
scrollCursorIntoView,
findNodeToInsertImage,
setURLWithQueryAndSize,
getContentNode,
} from 'dashboard/helper/editorHelper';
const TYPING_INDICATOR_IDLE_TIME = 4000;
@@ -35,10 +38,8 @@ import {
} from 'shared/helpers/KeyboardHelpers';
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
import { useUISettings } from 'dashboard/composables/useUISettings';
import {
replaceVariablesInMessage,
createTypingIndicator,
} from '@chatwoot/utils';
import { createTypingIndicator } from '@chatwoot/utils';
import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
import { uploadFile } from 'dashboard/helper/uploadHelper';
@@ -71,7 +72,12 @@ const createState = (
export default {
name: 'WootMessageEditor',
components: { TagAgents, CannedResponse, VariableList },
components: {
TagAgents,
CannedResponse,
VariableList,
KeyboardEmojiSelector,
},
mixins: [keyboardEventListenerMixins],
props: {
value: { type: String, default: '' },
@@ -91,6 +97,7 @@ export default {
allowSignature: { type: Boolean, default: false },
channelType: { type: String, default: '' },
showImageResizeToolbar: { type: Boolean, default: false }, // A kill switch to show or hide the image toolbar
focusOnMount: { type: Boolean, default: true },
},
setup() {
const {
@@ -119,9 +126,11 @@ export default {
showUserMentions: false,
showCannedMenu: false,
showVariables: false,
showEmojiMenu: false,
mentionSearchKey: '',
cannedSearchTerm: '',
variableSearchTerm: '',
emojiSearchTerm: '',
editorView: null,
range: null,
state: undefined,
@@ -169,7 +178,7 @@ export default {
this.editorView = args.view;
this.range = args.range;
this.mentionSearchKey = args.text.replace('@', '');
this.mentionSearchKey = args.text;
return false;
},
@@ -198,7 +207,7 @@ export default {
this.editorView = args.view;
this.range = args.range;
this.cannedSearchTerm = args.text.replace('/', '');
this.cannedSearchTerm = args.text;
return false;
},
onExit: () => {
@@ -226,7 +235,7 @@ export default {
this.editorView = args.view;
this.range = args.range;
this.variableSearchTerm = args.text.replace('{{', '');
this.variableSearchTerm = args.text;
return false;
},
onExit: () => {
@@ -238,6 +247,31 @@ export default {
return event.keyCode === 13 && this.showVariables;
},
}),
suggestionsPlugin({
matcher: triggerCharacters(':', 2), // Trigger after ':' and at least 2 characters
suggestionClass: '',
onEnter: args => {
this.showEmojiMenu = true;
this.emojiSearchTerm = args.text || '';
this.range = args.range;
this.editorView = args.view;
return false;
},
onChange: args => {
this.editorView = args.view;
this.range = args.range;
this.emojiSearchTerm = args.text;
return false;
},
onExit: () => {
this.emojiSearchTerm = '';
this.showEmojiMenu = false;
return false;
},
onKeyDown: ({ event }) => {
return event.keyCode === 13 && this.showEmojiMenu;
},
}),
];
},
sendWithSignature() {
@@ -267,6 +301,8 @@ export default {
},
editorId() {
this.showCannedMenu = false;
this.showEmojiMenu = false;
this.showVariables = false;
this.cannedSearchTerm = '';
this.reloadState(this.value);
},
@@ -311,7 +347,9 @@ export default {
mounted() {
this.createEditorView();
this.editorView.updateState(this.state);
this.focusEditorInputField();
if (this.focusOnMount) {
this.focusEditorInputField();
}
// BUS Event to insert text or markdown into the editor at the
// current cursor position.
@@ -348,7 +386,7 @@ export default {
// these drafts can also have a signature, so we need to check if the body is empty
// and handle things accordingly
this.handleEmptyBodyWithSignature();
} else {
} else if (this.focusOnMount) {
// this is in the else block, handleEmptyBodyWithSignature also has a call to the focus method
// the position is set to start, because the signature is added at the end of the body
this.focusEditorInputField('end');
@@ -517,57 +555,36 @@ export default {
this.editorView.dispatch(tr.setSelection(selection));
this.editorView.focus();
},
insertMentionNode(mentionItem) {
/**
* Inserts special content (mention, canned response, variable, emoji) into the editor.
* @param {string} type - The type of special content to insert. Possible values: 'mention', 'canned_response', 'variable', 'emoji'.
* @param {Object|string} content - The content to insert, depending on the type.
*/
insertSpecialContent(type, content) {
if (!this.editorView) {
return null;
}
const node = this.editorView.state.schema.nodes.mention.create({
userId: mentionItem.id,
userFullName: mentionItem.name,
});
this.insertNodeIntoEditor(node, this.range.from, this.range.to);
this.$track(CONVERSATION_EVENTS.USED_MENTIONS);
return false;
},
insertCannedResponse(cannedItem) {
const updatedMessage = replaceVariablesInMessage({
message: cannedItem,
variables: this.variables,
});
if (!this.editorView) {
return null;
return;
}
let node = new MessageMarkdownTransformer(messageSchema).parse(
updatedMessage
let { node, from, to } = getContentNode(
this.editorView,
type,
content,
this.range,
this.variables
);
const from =
node.textContent === updatedMessage
? this.range.from
: this.range.from - 1;
this.insertNodeIntoEditor(node, from, this.range.to);
this.$track(CONVERSATION_EVENTS.INSERTED_A_CANNED_RESPONSE);
return false;
},
insertVariable(variable) {
if (!this.editorView) {
return null;
}
const content = `{{${variable}}}`;
let node = this.editorView.state.schema.text(content);
const { from, to } = this.range;
if (!node) return;
this.insertNodeIntoEditor(node, from, to);
this.showVariables = false;
this.$track(CONVERSATION_EVENTS.INSERTED_A_VARIABLE);
return false;
const event_map = {
mention: CONVERSATION_EVENTS.USED_MENTIONS,
cannedResponse: CONVERSATION_EVENTS.INSERTED_A_CANNED_RESPONSE,
variable: CONVERSATION_EVENTS.INSERTED_A_VARIABLE,
emoji: CONVERSATION_EVENTS.INSERTED_AN_EMOJI,
};
this.$track(event_map[type]);
},
openFileBrowser() {
this.$refs.imageUpload.click();
@@ -687,17 +704,22 @@ export default {
<TagAgents
v-if="showUserMentions && isPrivate"
:search-key="mentionSearchKey"
@click="insertMentionNode"
@click="content => insertSpecialContent('mention', content)"
/>
<CannedResponse
v-if="shouldShowCannedResponses"
:search-key="cannedSearchTerm"
@click="insertCannedResponse"
@click="content => insertSpecialContent('cannedResponse', content)"
/>
<VariableList
v-if="shouldShowVariables"
:search-key="variableSearchTerm"
@click="insertVariable"
@click="content => insertSpecialContent('variable', content)"
/>
<KeyboardEmojiSelector
v-if="showEmojiMenu"
:search-key="emojiSearchTerm"
@click="emoji => insertSpecialContent('emoji', emoji)"
/>
<input
ref="imageUpload"
@@ -8,6 +8,7 @@ import {
EditorState,
Selection,
} from '@chatwoot/prosemirror-schema';
import imagePastePlugin from '@chatwoot/prosemirror-schema/src/plugins/image';
import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
import { useAlert } from 'dashboard/composables';
import { useUISettings } from 'dashboard/composables/useUISettings';
@@ -55,7 +56,7 @@ export default {
return {
editorView: null,
state: undefined,
plugins: [],
plugins: [imagePastePlugin(this.handleImageUpload)],
};
},
computed: {
@@ -76,6 +77,7 @@ export default {
this.reloadState();
},
},
created() {
this.state = createState(
this.value,
@@ -95,6 +97,24 @@ export default {
openFileBrowser() {
this.$refs.imageUploadInput.click();
},
async handleImageUpload(url) {
try {
const fileUrl = await this.$store.dispatch(
'articles/uploadExternalImage',
{
portalSlug: this.$route.params.portalSlug,
url,
}
);
return fileUrl;
} catch (error) {
useAlert(
this.$t('HELP_CENTER.ARTICLE_EDITOR.IMAGE_UPLOAD.UN_AUTHORIZED_ERROR')
);
return '';
}
},
onFileChange() {
const file = this.$refs.imageUploadInput.files[0];
@@ -120,7 +140,6 @@ export default {
if (fileUrl) {
this.onImageUploadStart(fileUrl);
}
useAlert(this.$t('HELP_CENTER.ARTICLE_EDITOR.IMAGE_UPLOAD.SUCCESS'));
} catch (error) {
useAlert(this.$t('HELP_CENTER.ARTICLE_EDITOR.IMAGE_UPLOAD.ERROR'));
}
@@ -173,6 +192,18 @@ export default {
blur: () => {
this.onBlur();
},
paste: (view, event) => {
const data = event.clipboardData.files;
if (data.length > 0) {
data.forEach(file => {
// Check if the file is an image
if (file.type.includes('image')) {
this.uploadImageToStorage(file);
}
});
event.preventDefault();
}
},
},
});
},
@@ -220,11 +220,7 @@ export default {
: this.$t('CONVERSATION.FOOTER.ENABLE_SIGN_TOOLTIP');
},
enableInsertArticleInReply() {
const isFeatEnabled = this.isFeatureEnabledonAccount(
this.accountId,
FEATURE_FLAGS.INSERT_ARTICLE_IN_REPLY
);
return isFeatEnabled && this.portalSlug;
return this.portalSlug;
},
isFetchingAppIntegrations() {
return this.uiFlags.isFetching;
@@ -403,6 +399,7 @@ export default {
label {
@apply cursor-pointer;
}
&:hover button {
@apply dark:bg-slate-800 bg-slate-100;
}
@@ -0,0 +1,70 @@
<script setup>
import { shallowRef, computed, onMounted } from 'vue';
import emojiGroups from 'shared/components/emoji/emojisGroup.json';
import MentionBox from '../mentions/MentionBox.vue';
const props = defineProps({
searchKey: {
type: String,
default: '',
},
});
const emit = defineEmits(['click']);
const allEmojis = shallowRef([]);
const items = computed(() => {
if (!props.searchKey) return [];
const searchTerm = props.searchKey.toLowerCase();
return allEmojis.value.filter(emoji =>
emoji.searchString.includes(searchTerm)
);
});
function loadEmojis() {
allEmojis.value = emojiGroups.flatMap(({ emojis }) =>
emojis.map(({ name, slug, ...rest }) => ({
...rest,
name,
slug,
searchString: `${name.replace(/\s+/g, '')} ${slug}`.toLowerCase(), // Remove all whitespace and convert to lowercase
}))
);
}
function handleMentionClick(item = {}) {
emit('click', item.emoji);
}
onMounted(() => {
loadEmojis();
});
</script>
<!-- eslint-disable-next-line vue/no-root-v-if -->
<template>
<MentionBox
v-if="items.length"
type="emoji"
:items="items"
@mentionSelect="handleMentionClick"
>
<template #default="{ item, selected }">
<span
class="max-w-full inline-flex items-center gap-0.5 min-w-0 mb-0 text-sm font-medium text-slate-900 dark:text-slate-100 group-hover:text-woot-500 dark:group-hover:text-woot-500 truncate"
>
{{ item.emoji }}
<p
class="relative mb-0 truncate bottom-px"
:class="{
'text-woot-500 dark:text-woot-500': selected,
'font-normal': !selected,
}"
>
:{{ item.name }}
</p>
</span>
</template>
</MentionBox>
</template>
@@ -41,14 +41,11 @@ export default {
};
</script>
<!-- eslint-disable-next-line vue/no-root-v-if -->
<template>
<MentionBox
v-if="items.length"
:items="items"
@mentionSelect="handleMentionClick"
>
<template slot-scope="{ item }">
<strong>{{ item.label }}</strong> - {{ item.description }}
</template>
</MentionBox>
/>
</template>
@@ -338,7 +338,11 @@ export default {
:show-query-operator="i !== appliedFilters.length - 1"
:show-user-input="showUserInput(appliedFilters[i].filter_operator)"
grouped-filters
:error-message="validationErrors[`filter_${i}`]"
:error-message="
validationErrors[`filter_${i}`]
? $t(`CONTACTS_FILTER.ERRORS.VALUE_REQUIRED`)
: ''
"
@resetFilter="resetFilter(i, appliedFilters[i])"
@removeFilter="removeFilter(i)"
/>
@@ -237,6 +237,9 @@ export default {
if (this.isASmsInbox) {
return MESSAGE_MAX_LENGTH.TWILIO_SMS;
}
if (this.isAnEmailChannel) {
return MESSAGE_MAX_LENGTH.EMAIL;
}
return MESSAGE_MAX_LENGTH.GENERAL;
},
showFileUpload() {
@@ -56,20 +56,14 @@ export default {
};
</script>
<!-- eslint-disable-next-line vue/no-root-v-if -->
<template>
<MentionBox
v-if="items.length"
type="variable"
:items="items"
@mentionSelect="handleVariableClick"
>
<template slot-scope="{ item }">
<span class="text-capitalize variable--list-label">
{{ item.description }}
</span>
({{ item.label }})
</template>
</MentionBox>
/>
</template>
<style scoped>
@@ -1,6 +1,10 @@
<script>
import countries from 'shared/constants/countries.js';
import parsePhoneNumber from 'libphonenumber-js';
import {
getActiveCountryCode,
getActiveDialCode,
} from 'shared/components/PhoneInput/helper';
export default {
props: {
@@ -30,8 +34,8 @@ export default {
selectedIndex: -1,
showDropdown: false,
searchCountry: '',
activeCountryCode: '',
activeDialCode: '',
activeCountryCode: getActiveCountryCode(),
activeDialCode: getActiveDialCode(),
phoneNumber: this.value,
};
},
@@ -90,22 +90,24 @@ const variableKey = (item = {}) => {
}"
@click="onListItemSelection(index)"
>
<p
class="max-w-full min-w-0 mb-0 overflow-hidden text-sm font-medium text-slate-900 dark:text-slate-100 group-hover:text-woot-500 dark:group-hover:text-woot-500 text-ellipsis whitespace-nowrap"
:class="{
'text-woot-500 dark:text-woot-500': index === selectedIndex,
}"
>
{{ item.description }}
</p>
<p
class="max-w-full min-w-0 mb-0 overflow-hidden text-xs text-slate-500 dark:text-slate-300 group-hover:text-woot-500 dark:group-hover:text-woot-500 text-ellipsis whitespace-nowrap"
:class="{
'text-woot-500 dark:text-woot-500': index === selectedIndex,
}"
>
{{ variableKey(item) }}
</p>
<slot :item="item" :index="index" :selected="index === selectedIndex">
<p
class="max-w-full min-w-0 mb-0 overflow-hidden text-sm font-medium text-slate-900 dark:text-slate-100 group-hover:text-woot-500 dark:group-hover:text-woot-500 text-ellipsis whitespace-nowrap"
:class="{
'text-woot-500 dark:text-woot-500': index === selectedIndex,
}"
>
{{ item.description }}
</p>
<p
class="max-w-full min-w-0 mb-0 overflow-hidden text-xs text-slate-500 dark:text-slate-300 group-hover:text-woot-500 dark:group-hover:text-woot-500 text-ellipsis whitespace-nowrap"
:class="{
'text-woot-500 dark:text-woot-500': index === selectedIndex,
}"
>
{{ variableKey(item) }}
</p>
</slot>
</button>
</woot-dropdown-item>
</ul>
@@ -0,0 +1,53 @@
export const AVAILABLE_CUSTOM_ROLE_PERMISSIONS = [
'conversation_manage',
'conversation_unassigned_manage',
'conversation_participating_manage',
'contact_manage',
'report_manage',
'knowledge_base_manage',
];
export const ROLES = ['agent', 'administrator'];
export const CONVERSATION_PERMISSIONS = [
'conversation_manage',
'conversation_unassigned_manage',
'conversation_participating_manage',
];
export const MANAGE_ALL_CONVERSATION_PERMISSIONS = 'conversation_manage';
export const CONVERSATION_UNASSIGNED_PERMISSIONS =
'conversation_unassigned_manage';
export const CONVERSATION_PARTICIPATING_PERMISSIONS =
'conversation_participating_manage';
export const CONTACT_PERMISSIONS = 'contact_manage';
export const REPORTS_PERMISSIONS = 'report_manage';
export const PORTAL_PERMISSIONS = 'knowledge_base_manage';
export const ASSIGNEE_TYPE_TAB_PERMISSIONS = {
me: {
count: 'mineCount',
permissions: [...ROLES, ...CONVERSATION_PERMISSIONS],
},
unassigned: {
count: 'unAssignedCount',
permissions: [
...ROLES,
MANAGE_ALL_CONVERSATION_PERMISSIONS,
CONVERSATION_UNASSIGNED_PERMISSIONS,
],
},
all: {
count: 'allCount',
permissions: [
...ROLES,
MANAGE_ALL_CONVERSATION_PERMISSIONS,
CONVERSATION_PARTICIPATING_PERMISSIONS,
],
},
};
+1 -1
View File
@@ -16,7 +16,6 @@ export const FEATURE_FLAGS = {
TEAM_MANAGEMENT: 'team_management',
VOICE_RECORDER: 'voice_recorder',
AUDIT_LOGS: 'audit_logs',
INSERT_ARTICLE_IN_REPLY: 'insert_article_in_reply',
INBOX_VIEW: 'inbox_view',
SLA: 'sla',
RESPONSE_BOT: 'response_bot',
@@ -32,4 +31,5 @@ export const FEATURE_FLAGS = {
IP_LOOKUP: 'ip_lookup',
LINEAR: 'linear_integration',
CAPTAIN: 'captain_integration',
CUSTOM_ROLES: 'custom_roles',
};
@@ -5,6 +5,7 @@ export const CONVERSATION_EVENTS = Object.freeze({
INSERTED_A_CANNED_RESPONSE: 'Inserted a canned response',
TRANSLATE_A_MESSAGE: 'Translated a message',
INSERTED_A_VARIABLE: 'Inserted a variable',
INSERTED_AN_EMOJI: 'Inserted an emoji',
USED_MENTIONS: 'Used mentions',
SEARCH_CONVERSATION: 'Searched conversations',
APPLY_FILTER: 'Applied filters in the conversation list',
@@ -5,6 +5,11 @@ import {
getAlertAudio,
initOnEvents,
} from 'shared/helpers/AudioNotificationHelper';
import {
ROLES,
CONVERSATION_PERMISSIONS,
} from 'dashboard/constants/permissions.js';
import { getUserPermissions } from 'dashboard/helper/permissionsHelper.js';
const NOTIFICATION_TIME = 30000;
@@ -14,12 +19,13 @@ class DashboardAudioNotificationHelper {
this.audioAlertType = 'none';
this.playAlertOnlyWhenHidden = true;
this.alertIfUnreadConversationExist = false;
this.currentUser = null;
this.currentUserId = null;
this.audioAlertTone = 'ding';
}
setInstanceValues = ({
currentUserId,
currentUser,
alwaysPlayAudioAlert,
alertIfUnreadConversationExist,
audioAlertType,
@@ -28,7 +34,8 @@ class DashboardAudioNotificationHelper {
this.audioAlertType = audioAlertType;
this.playAlertOnlyWhenHidden = !alwaysPlayAudioAlert;
this.alertIfUnreadConversationExist = alertIfUnreadConversationExist;
this.currentUserId = currentUserId;
this.currentUser = currentUser;
this.currentUserId = currentUser.id;
this.audioAlertTone = audioAlertTone;
initOnEvents.forEach(e => {
document.addEventListener(e, this.onAudioListenEvent, false);
@@ -112,6 +119,20 @@ class DashboardAudioNotificationHelper {
return message?.sender_id === this.currentUserId;
};
isUserHasConversationPermission = () => {
const currentAccountId = window.WOOT.$store.getters.getCurrentAccountId;
// Get the user permissions for the current account
const userPermissions = getUserPermissions(
this.currentUser,
currentAccountId
);
// Check if the user has the required permissions
const hasRequiredPermission = [...ROLES, ...CONVERSATION_PERMISSIONS].some(
permission => userPermissions.includes(permission)
);
return hasRequiredPermission;
};
shouldNotifyOnMessage = message => {
if (this.audioAlertType === 'mine') {
return this.isConversationAssignedToCurrentUser(message);
@@ -120,6 +141,11 @@ class DashboardAudioNotificationHelper {
};
onNewMessage = message => {
// If the user does not have the permission to view the conversation, then dismiss the alert
if (!this.isUserHasConversationPermission()) {
return;
}
// If the message is sent by the current user or the
// correct notification is not enabled, then dismiss the alert
if (
@@ -3,6 +3,8 @@ import {
MessageMarkdownTransformer,
MessageMarkdownSerializer,
} from '@chatwoot/prosemirror-schema';
import { replaceVariablesInMessage } from '@chatwoot/utils';
import * as Sentry from '@sentry/browser';
/**
@@ -281,3 +283,92 @@ export function setURLWithQueryAndSize(selectedImageNode, size, editorView) {
}
}
}
/**
* Content Node Creation Helper Functions for
* - mention
* - canned response
* - variable
* - emoji
*/
/**
* Centralized node creation function that handles the creation of different types of nodes based on the specified type.
* @param {Object} editorView - The editor view instance.
* @param {string} nodeType - The type of node to create ('mention', 'cannedResponse', 'variable', 'emoji').
* @param {Object|string} content - The content needed to create the node, which varies based on node type.
* @returns {Object|null} - The created ProseMirror node or null if the type is not supported.
*/
const createNode = (editorView, nodeType, content) => {
const { state } = editorView;
switch (nodeType) {
case 'mention':
return state.schema.nodes.mention.create({
userId: content.id,
userFullName: content.name,
});
case 'cannedResponse':
return new MessageMarkdownTransformer(messageSchema).parse(content);
case 'variable':
return state.schema.text(`{{${content}}}`);
case 'emoji':
return state.schema.text(content);
default:
return null;
}
};
/**
* Object mapping types to their respective node creation functions.
*/
const nodeCreators = {
mention: (editorView, content, from, to) => ({
node: createNode(editorView, 'mention', content),
from,
to,
}),
cannedResponse: (editorView, content, from, to, variables) => {
const updatedMessage = replaceVariablesInMessage({
message: content,
variables,
});
const node = createNode(editorView, 'cannedResponse', updatedMessage);
return {
node,
from: node.textContent === updatedMessage ? from : from - 1,
to,
};
},
variable: (editorView, content, from, to) => ({
node: createNode(editorView, 'variable', content),
from,
to,
}),
emoji: (editorView, content, from, to) => ({
node: createNode(editorView, 'emoji', content),
from,
to,
}),
};
/**
* Retrieves a content node based on the specified type and content, using a functional approach to select the appropriate node creation function.
* @param {Object} editorView - The editor view instance.
* @param {string} type - The type of content node to create ('mention', 'cannedResponse', 'variable', 'emoji').
* @param {string|Object} content - The content to be transformed into a node.
* @param {Object} range - An object containing 'from' and 'to' properties indicating the range in the document where the node should be placed.
* @param {Object} variables - Optional. Variables to replace in the content, used for 'cannedResponse' type.
* @returns {Object} - An object containing the created node and the updated 'from' and 'to' positions.
*/
export const getContentNode = (
editorView,
type,
content,
{ from, to },
variables
) => {
const creator = nodeCreators[type];
return creator
? creator(editorView, content, from, to, variables)
: { node: null, from, to };
};
@@ -9,6 +9,7 @@ const FEATURE_HELP_URLS = {
custom_attributes: 'https://chwt.app/hc/custom-attributes',
dashboard_apps: 'https://chwt.app/hc/dashboard-apps',
help_center: 'https://chwt.app/hc/help-center',
inboxes: 'https://chwt.app/hc/inboxes',
integrations: 'https://chwt.app/hc/integrations',
labels: 'https://chwt.app/hc/labels',
macros: 'https://chwt.app/hc/macros',
@@ -16,6 +17,7 @@ const FEATURE_HELP_URLS = {
reports: 'https://chwt.app/hc/reports',
sla: 'https://chwt.app/hc/sla',
team_management: 'https://chwt.app/hc/teams',
webhook: 'https://chwt.app/hc/webhooks',
};
export function getHelpUrlForFeature(featureName) {
@@ -41,3 +41,32 @@ export const buildPermissionsFromRouter = (routes = []) =>
return acc;
}, {});
/**
* Filters and transforms items based on user permissions.
*
* @param {Object} items - An object containing items to be filtered.
* @param {Array} userPermissions - Array of permissions the user has.
* @param {Function} getPermissions - Function to extract required permissions from an item.
* @param {Function} [transformItem] - Optional function to transform each item after filtering.
* @returns {Array} Filtered and transformed items.
*/
export const filterItemsByPermission = (
items,
userPermissions,
getPermissions,
transformItem = (key, item) => ({ key, ...item })
) => {
// Helper function to check if an item has the required permissions
const hasRequiredPermissions = item => {
const requiredPermissions = getPermissions(item);
return (
requiredPermissions.length === 0 ||
hasPermissions(requiredPermissions, userPermissions)
);
};
return Object.entries(items)
.filter(([, item]) => hasRequiredPermissions(item)) // Keep only items with required permissions
.map(([key, item]) => transformItem(key, item)); // Transform each remaining item
};
@@ -4,11 +4,39 @@ import {
getCurrentAccount,
} from './permissionsHelper';
import {
ROLES,
CONVERSATION_PERMISSIONS,
CONTACT_PERMISSIONS,
REPORTS_PERMISSIONS,
PORTAL_PERMISSIONS,
} from 'dashboard/constants/permissions.js';
export const routeIsAccessibleFor = (route, userPermissions = []) => {
const { meta: { permissions: routePermissions = [] } = {} } = route;
return hasPermissions(routePermissions, userPermissions);
};
export const defaultRedirectPage = (to, permissions) => {
const { accountId } = to.params;
const permissionRoutes = [
{
permissions: [...ROLES, ...CONVERSATION_PERMISSIONS],
path: 'dashboard',
},
{ permissions: [CONTACT_PERMISSIONS], path: 'contacts' },
{ permissions: [REPORTS_PERMISSIONS], path: 'reports/overview' },
{ permissions: [PORTAL_PERMISSIONS], path: 'portals' },
];
const route = permissionRoutes.find(({ permissions: routePermissions }) =>
hasPermissions(routePermissions, permissions)
);
return `accounts/${accountId}/${route ? route.path : 'dashboard'}`;
};
const validateActiveAccountRoutes = (to, user) => {
// If the current account is active, then check for the route permissions
const accountDashboardURL = `accounts/${to.params.accountId}/dashboard`;
@@ -22,7 +50,7 @@ const validateActiveAccountRoutes = (to, user) => {
const isAccessible = routeIsAccessibleFor(to, userPermissions);
// If the route is not accessible for the user, return to dashboard screen
return isAccessible ? null : accountDashboardURL;
return isAccessible ? null : defaultRedirectPage(to, userPermissions);
};
export const validateLoggedInRoutes = (to, user) => {
@@ -26,7 +26,7 @@ const initializeAudioAlerts = user => {
} = uiSettings || {};
DashboardAudioNotificationHelper.setInstanceValues({
currentUserId: user.id,
currentUser: user,
audioAlertType: audioAlertType || 'none',
audioAlertTone: audioAlertTone || 'ding',
alwaysPlayAudioAlert: alwaysPlayAudioAlert || false,
@@ -0,0 +1,127 @@
// Moved from editorHelper.spec.js to editorContentHelper.spec.js
// the mock of chatwoot/prosemirror-schema is getting conflicted with other specs
import { getContentNode } from '../editorHelper';
import {
MessageMarkdownTransformer,
messageSchema,
} from '@chatwoot/prosemirror-schema';
import { replaceVariablesInMessage } from '@chatwoot/utils';
vi.mock('@chatwoot/prosemirror-schema', () => ({
MessageMarkdownTransformer: vi.fn(),
messageSchema: {},
}));
vi.mock('@chatwoot/utils', () => ({
replaceVariablesInMessage: vi.fn(),
}));
describe('getContentNode', () => {
let editorView;
beforeEach(() => {
editorView = {
state: {
schema: {
nodes: {
mention: {
create: vi.fn(),
},
},
text: vi.fn(),
},
},
};
});
describe('getMentionNode', () => {
it('should create a mention node', () => {
const content = { id: 1, name: 'John Doe' };
const from = 0;
const to = 10;
getContentNode(editorView, 'mention', content, {
from,
to,
});
expect(editorView.state.schema.nodes.mention.create).toHaveBeenCalledWith(
{
userId: content.id,
userFullName: content.name,
}
);
});
});
describe('getCannedResponseNode', () => {
it('should create a canned response node', () => {
const content = 'Hello {{name}}';
const variables = { name: 'John' };
const from = 0;
const to = 10;
const updatedMessage = 'Hello John';
replaceVariablesInMessage.mockReturnValue(updatedMessage);
MessageMarkdownTransformer.mockImplementation(() => ({
parse: vi.fn().mockReturnValue({ textContent: updatedMessage }),
}));
const { node } = getContentNode(
editorView,
'cannedResponse',
content,
{ from, to },
variables
);
expect(replaceVariablesInMessage).toHaveBeenCalledWith({
message: content,
variables,
});
expect(MessageMarkdownTransformer).toHaveBeenCalledWith(messageSchema);
expect(node.textContent).toBe(updatedMessage);
});
});
describe('getVariableNode', () => {
it('should create a variable node', () => {
const content = 'name';
const from = 0;
const to = 10;
getContentNode(editorView, 'variable', content, {
from,
to,
});
expect(editorView.state.schema.text).toHaveBeenCalledWith('{{name}}');
});
});
describe('getEmojiNode', () => {
it('should create an emoji node', () => {
const content = '😊';
const from = 0;
const to = 2;
getContentNode(editorView, 'emoji', content, {
from,
to,
});
expect(editorView.state.schema.text).toHaveBeenCalledWith('😊');
});
});
describe('getContentNode', () => {
it('should return null for invalid type', () => {
const content = 'invalid';
const from = 0;
const to = 10;
const { node } = getContentNode(editorView, 'invalid', content, {
from,
to,
});
expect(node).toBeNull();
});
});
});
@@ -3,6 +3,7 @@ import {
getCurrentAccount,
getUserPermissions,
hasPermissions,
filterItemsByPermission,
} from '../permissionsHelper';
describe('#getCurrentAccount', () => {
@@ -105,3 +106,113 @@ describe('buildPermissionsFromRouter', () => {
}).toThrow("The route doesn't have the required permissions defined");
});
});
describe('filterItemsByPermission', () => {
const items = {
item1: { name: 'Item 1', permissions: ['agent', 'administrator'] },
item2: {
name: 'Item 2',
permissions: [
'conversation_manage',
'conversation_unassigned_manage',
'conversation_participating_manage',
],
},
item3: { name: 'Item 3', permissions: ['contact_manage'] },
item4: { name: 'Item 4', permissions: ['report_manage'] },
item5: { name: 'Item 5', permissions: ['knowledge_base_manage'] },
item6: {
name: 'Item 6',
permissions: [
'agent',
'administrator',
'conversation_manage',
'conversation_unassigned_manage',
'conversation_participating_manage',
'contact_manage',
'report_manage',
'knowledge_base_manage',
],
},
item7: { name: 'Item 7', permissions: [] },
};
const getPermissions = item => item.permissions;
it('filters items based on user permissions', () => {
const userPermissions = ['agent', 'contact_manage', 'report_manage'];
const result = filterItemsByPermission(
items,
userPermissions,
getPermissions
);
expect(result).toHaveLength(5);
expect(result).toContainEqual(
expect.objectContaining({ key: 'item1', name: 'Item 1' })
);
expect(result).toContainEqual(
expect.objectContaining({ key: 'item3', name: 'Item 3' })
);
expect(result).toContainEqual(
expect.objectContaining({ key: 'item4', name: 'Item 4' })
);
expect(result).toContainEqual(
expect.objectContaining({ key: 'item6', name: 'Item 6' })
);
});
it('includes items with empty permissions', () => {
const userPermissions = [];
const result = filterItemsByPermission(
items,
userPermissions,
getPermissions
);
expect(result).toHaveLength(1);
expect(result).toContainEqual(
expect.objectContaining({ key: 'item7', name: 'Item 7' })
);
});
it('uses custom transform function when provided', () => {
const userPermissions = ['agent', 'contact_manage'];
const customTransform = (key, item) => ({ id: key, title: item.name });
const result = filterItemsByPermission(
items,
userPermissions,
getPermissions,
customTransform
);
expect(result).toHaveLength(4);
expect(result).toContainEqual({ id: 'item1', title: 'Item 1' });
expect(result).toContainEqual({ id: 'item3', title: 'Item 3' });
expect(result).toContainEqual({ id: 'item6', title: 'Item 6' });
});
it('handles empty items object', () => {
const result = filterItemsByPermission({}, ['agent'], getPermissions);
expect(result).toHaveLength(0);
});
it('handles custom getPermissions function', () => {
const customItems = {
item1: { name: 'Item 1', requiredPerms: ['agent', 'administrator'] },
item2: { name: 'Item 2', requiredPerms: ['contact_manage'] },
};
const customGetPermissions = item => item.requiredPerms;
const result = filterItemsByPermission(
customItems,
['agent'],
customGetPermissions
);
expect(result).toHaveLength(1);
expect(result).toContainEqual(
expect.objectContaining({ key: 'item1', name: 'Item 1' })
);
});
});
@@ -1,6 +1,7 @@
import {
getConversationDashboardRoute,
isAConversationRoute,
defaultRedirectPage,
routeIsAccessibleFor,
validateLoggedInRoutes,
isAInboxViewRoute,
@@ -14,6 +15,57 @@ describe('#routeIsAccessibleFor', () => {
});
});
describe('#defaultRedirectPage', () => {
const to = {
params: { accountId: '2' },
fullPath: '/app/accounts/2/dashboard',
name: 'home',
};
it('should return dashboard route for users with conversation permissions', () => {
const permissions = ['conversation_manage', 'agent'];
expect(defaultRedirectPage(to, permissions)).toBe('accounts/2/dashboard');
});
it('should return contacts route for users with contact permissions', () => {
const permissions = ['contact_manage'];
expect(defaultRedirectPage(to, permissions)).toBe('accounts/2/contacts');
});
it('should return reports route for users with report permissions', () => {
const permissions = ['report_manage'];
expect(defaultRedirectPage(to, permissions)).toBe(
'accounts/2/reports/overview'
);
});
it('should return portals route for users with portal permissions', () => {
const permissions = ['knowledge_base_manage'];
expect(defaultRedirectPage(to, permissions)).toBe('accounts/2/portals');
});
it('should return dashboard route as default for users with custom roles', () => {
const permissions = ['custom_role'];
expect(defaultRedirectPage(to, permissions)).toBe('accounts/2/dashboard');
});
it('should return dashboard route for users with administrator role', () => {
const permissions = ['administrator'];
expect(defaultRedirectPage(to, permissions)).toBe('accounts/2/dashboard');
});
it('should return dashboard route for users with multiple permissions', () => {
const permissions = [
'contact_manage',
'custom_role',
'conversation_manage',
'agent',
'administrator',
];
expect(defaultRedirectPage(to, permissions)).toBe('accounts/2/dashboard');
});
});
describe('#validateLoggedInRoutes', () => {
describe('when account access is missing', () => {
it('should return the login route', () => {
@@ -1,52 +1,93 @@
import { uploadFile } from '../uploadHelper';
import axios from 'axios';
import { uploadExternalImage, uploadFile } from '../uploadHelper';
global.axios = axios;
vi.mock('axios');
describe('#Upload Helpers', () => {
describe('Upload Helpers', () => {
afterEach(() => {
// Cleaning up the mock after each test
axios.post.mockReset();
});
it('should send a POST request with correct data', async () => {
const mockFile = new File(['dummy content'], 'example.png', {
type: 'image/png',
describe('uploadFile', () => {
it('should send a POST request with correct data', async () => {
const mockFile = new File(['dummy content'], 'example.png', {
type: 'image/png',
});
const mockResponse = {
data: {
file_url: 'https://example.com/fileUrl',
blob_key: 'blobKey123',
blob_id: 'blobId456',
},
};
axios.post.mockResolvedValueOnce(mockResponse);
const result = await uploadFile(mockFile, '1602');
expect(axios.post).toHaveBeenCalledWith(
'/api/v1/accounts/1602/upload',
expect.any(FormData),
{ headers: { 'Content-Type': 'multipart/form-data' } }
);
expect(result).toEqual({
fileUrl: 'https://example.com/fileUrl',
blobKey: 'blobKey123',
blobId: 'blobId456',
});
});
const mockResponse = {
data: {
file_url: 'https://example.com/fileUrl',
blob_key: 'blobKey123',
blob_id: 'blobId456',
},
};
axios.post.mockResolvedValueOnce(mockResponse);
it('should handle errors', async () => {
const mockFile = new File(['dummy content'], 'example.png', {
type: 'image/png',
});
const mockError = new Error('Failed to upload');
const result = await uploadFile(mockFile, '1602');
axios.post.mockRejectedValueOnce(mockError);
expect(axios.post).toHaveBeenCalledWith(
'/api/v1/accounts/1602/upload',
expect.any(FormData),
{ headers: { 'Content-Type': 'multipart/form-data' } }
);
expect(result).toEqual({
fileUrl: 'https://example.com/fileUrl',
blobKey: 'blobKey123',
blobId: 'blobId456',
await expect(uploadFile(mockFile)).rejects.toThrow('Failed to upload');
});
});
it('should handle errors', async () => {
const mockFile = new File(['dummy content'], 'example.png', {
type: 'image/png',
describe('uploadExternalImage', () => {
it('should send a POST request with correct data', async () => {
const mockUrl = 'https://example.com/image.jpg';
const mockResponse = {
data: {
file_url: 'https://example.com/fileUrl',
blob_key: 'blobKey123',
blob_id: 'blobId456',
},
};
axios.post.mockResolvedValueOnce(mockResponse);
const result = await uploadExternalImage(mockUrl, '1602');
expect(axios.post).toHaveBeenCalledWith(
'/api/v1/accounts/1602/upload',
{ external_url: mockUrl },
{ headers: { 'Content-Type': 'application/json' } }
);
expect(result).toEqual({
fileUrl: 'https://example.com/fileUrl',
blobKey: 'blobKey123',
blobId: 'blobId456',
});
});
const mockError = new Error('Failed to upload');
axios.post.mockRejectedValueOnce(mockError);
it('should handle errors', async () => {
const mockUrl = 'https://example.com/image.jpg';
const mockError = new Error('Failed to upload');
await expect(uploadFile(mockFile)).rejects.toThrow('Failed to upload');
axios.post.mockRejectedValueOnce(mockError);
await expect(uploadExternalImage(mockUrl)).rejects.toThrow(
'Failed to upload'
);
});
});
});
@@ -19,26 +19,47 @@ const HEADERS = {
* The function uses FormData to wrap the file and axios to send the request.
*
* @param {File} file - The file to be uploaded. It should be a File object (typically coming from a file input element).
* @param {string} accountId - The account ID.
* @returns {Promise} A promise that resolves with the server's response when the upload is successful, or rejects if there's an error.
*/
export async function uploadFile(file, accountId) {
// Create a new FormData instance.
let formData = new FormData();
if (!accountId) {
accountId = window.location.pathname.split('/')[3];
}
// Append the file to the FormData instance under the key 'attachment'.
let formData = new FormData();
formData.append('attachment', file);
// Use axios to send a POST request to the upload endpoint.
const { data } = await axios.post(
`/api/${API_VERSION}/accounts/${accountId}/upload`,
formData,
{
headers: HEADERS,
}
{ headers: HEADERS }
);
return {
fileUrl: data.file_url,
blobKey: data.blob_key,
blobId: data.blob_id,
};
}
/**
* Uploads an image from an external URL.
*
* @param {string} url - The external URL of the image.
* @param {string} accountId - The account ID.
* @returns {Promise} A promise that resolves with the server's response.
*/
export async function uploadExternalImage(url, accountId) {
if (!accountId) {
accountId = window.location.pathname.split('/')[3];
}
const { data } = await axios.post(
`/api/${API_VERSION}/accounts/${accountId}/upload`,
{ external_url: url },
{ headers: { 'Content-Type': 'application/json' } }
);
return {
@@ -55,6 +55,9 @@
"CREATED_AT": "Created at",
"LAST_ACTIVITY": "Last activity"
},
"ERRORS": {
"VALUE_REQUIRED": "Value is required"
},
"GROUPS": {
"STANDARD_FILTERS": "Standard filters",
"ADDITIONAL_FILTERS": "Additional filters",
@@ -18,7 +18,8 @@
"STATUS": "Status",
"ACTIONS": "Actions",
"VERIFIED": "Verified",
"VERIFICATION_PENDING": "Verification Pending"
"VERIFICATION_PENDING": "Verification Pending",
"AVAILABLE_CUSTOM_ROLE": "Available custom role permissions"
},
"ADD": {
"TITLE": "Add agent to your team",
@@ -3,7 +3,8 @@
"HEADER": "Custom Attributes",
"HEADER_BTN_TXT": "Add Custom Attribute",
"LOADING": "Fetching custom attributes",
"SIDEBAR_TXT": "<p><b>Custom Attributes</b> <p>A custom attribute tracks facts about your contacts/conversation — like the subscription plan, or when they ordered the first item etc. <br /><br />For creating a Custom Attribute, just click on the <b>Add Custom Attribute.</b> You can also edit or delete an existing Custom Attribute by clicking on the Edit or Delete button.</p>",
"DESCRIPTION": "A custom attribute tracks additional details about your contacts or conversations—such as the subscription plan or the date of their first purchase. You can add different types of custom attributes, such as text, lists, or numbers, to capture the specific information you need.",
"LEARN_MORE": "Learn more about custom attributes",
"ADD": {
"TITLE": "Add Custom Attribute",
"SUBMIT": "Create",
@@ -1,9 +1,10 @@
{
"AUTOMATION": {
"HEADER": "Automations",
"HEADER": "Automation",
"DESCRIPTION": "Automation can replace and streamline existing processes that require manual effort, such as adding labels and assigning conversations to the most suitable agent. This allows the team to focus on their strengths while reducing time spent on routine tasks.",
"LEARN_MORE": "Learn more about automation",
"HEADER_BTN_TXT": "Add Automation Rule",
"LOADING": "Fetching automation rules",
"SIDEBAR_TXT": "<p><b>Automation Rules</b> <p>Automation can replace and automate existing processes that require manual effort. You can do many things with automation, including adding labels and assigning conversation to the best agent. So the team focuses on what they do best and spends more little time on manual tasks.</p>",
"ADD": {
"TITLE": "Add Automation Rule",
"SUBMIT": "Create",
@@ -30,6 +30,9 @@
"is_lesser_than": "Is lesser than",
"days_before": "Is x days before"
},
"ERRORS": {
"VALUE_REQUIRED": "Value is required"
},
"ATTRIBUTES": {
"NAME": "Name",
"EMAIL": "Email",
@@ -0,0 +1,91 @@
{
"CUSTOM_ROLE": {
"HEADER": "Custom Roles",
"LEARN_MORE": "Learn more about custom roles",
"DESCRIPTION": "Custom roles are roles that are created by the account owner or admin. These roles can be assigned to agents to define their access and permissions within the account. Custom roles can be created with specific permissions and access levels to suit the requirements of the organization.",
"HEADER_BTN_TXT": "Add custom role",
"LOADING": "Fetching custom roles...",
"SEARCH_404": "There are no items matching this query.",
"PAYWALL": {
"TITLE": "Upgrade to create custom roles",
"AVAILABLE_ON": "The custom role feature is only available in the Business and Enterprise plans.",
"UPGRADE_PROMPT": "Upgrade your plan to get access to advanced features like team management, automations, custom attributes, and more.",
"UPGRADE_NOW": "Upgrade now",
"CANCEL_ANYTIME": "You can change or cancel your plan anytime"
},
"ENTERPRISE_PAYWALL": {
"AVAILABLE_ON": "The custom role feature is only available in the paid plans.",
"UPGRADE_PROMPT": "Upgrade to a paid plan to access advanced features like audit logs, agent capacity, and more.",
"ASK_ADMIN": "Please reach out to your administrator for the upgrade."
},
"LIST": {
"404": "There are no custom roles available in this account.",
"TITLE": "Manage custom roles",
"DESC": "Custom roles are roles that are created by the account owner or admin. These roles can be assigned to agents to define their access and permissions within the account. Custom roles can be created with specific permissions and access levels to suit the requirements of the organization.",
"TABLE_HEADER": [
"Name",
"Description",
"Permissions",
"Actions"
]
},
"PERMISSIONS": {
"CONVERSATION_MANAGE": "Manage all conversations",
"CONVERSATION_UNASSIGNED_MANAGE": "Manage unassigned conversations and those assigned to them",
"CONVERSATION_PARTICIPATING_MANAGE": "Manage participating conversations and those assigned to them",
"CONTACT_MANAGE": "Manage contacts",
"REPORT_MANAGE": "Manage reports",
"KNOWLEDGE_BASE_MANAGE": "Manage knowledge base"
},
"FORM": {
"NAME": {
"LABEL": "Name",
"PLACEHOLDER": "Please enter a name.",
"ERROR": "Name is required."
},
"DESCRIPTION": {
"LABEL": "Description",
"PLACEHOLDER": "Please enter a description.",
"ERROR": "Description is required."
},
"PERMISSIONS": {
"LABEL": "Permissions",
"ERROR": "Permissions are required."
},
"CANCEL_BUTTON_TEXT": "Cancel",
"API": {
"ERROR_MESSAGE": "Could not connect to Woot server. Please try again."
}
},
"ADD": {
"TITLE": "Add custom role",
"DESC": " Custom roles allows you to create roles with specific permissions and access levels to suit the requirements of the organization.",
"SUBMIT": "Submit",
"API": {
"SUCCESS_MESSAGE": "Custom role added successfully."
}
},
"EDIT": {
"BUTTON_TEXT": "Edit",
"TITLE": "Edit custom role",
"DESC": " Custom roles allows you to create roles with specific permissions and access levels to suit the requirements of the organization.",
"SUBMIT": "Update",
"API": {
"SUCCESS_MESSAGE": "Custom role updated successfully."
}
},
"DELETE": {
"BUTTON_TEXT": "Delete",
"API": {
"SUCCESS_MESSAGE": "Custom role deleted successfully.",
"ERROR_MESSAGE": "Could not connect to Woot server. Please try again."
},
"CONFIRM": {
"TITLE": "Confirm deletion",
"MESSAGE": "Are you sure to delete ",
"YES": "Yes, delete ",
"NO": "No, keep "
}
}
}
}
@@ -41,6 +41,7 @@
"UPLOADING": "Uploading...",
"SUCCESS": "Image uploaded successfully",
"ERROR": "Error while uploading image",
"UN_AUTHORIZED_ERROR": "You are not authorized to upload images",
"ERROR_FILE_SIZE": "Image size should be less than {size}MB",
"ERROR_FILE_FORMAT": "Image format should be jpg, jpeg or png",
"ERROR_FILE_DIMENSIONS": "Image dimensions should be less than 2000 x 2000"
@@ -1,7 +1,8 @@
{
"INBOX_MGMT": {
"HEADER": "Inboxes",
"SIDEBAR_TXT": "<p><b>Inbox</b></p> <p> When you connect a website or a facebook Page to Chatwoot, it is called an <b>Inbox</b>. You can have unlimited inboxes in your Chatwoot account. </p><p> Click on <b>Add Inbox</b> to connect a website or a Facebook Page. </p><p> In the Dashboard, you can see all the conversations from all your inboxes in a single place and respond to them under the `Conversations` tab. </p><p> You can also see conversations specific to an inbox by clicking on the inbox name on the left pane of the dashboard. </p>",
"DESCRIPTION": "A channel is the mode of communication your customer chooses to interact with you. An inbox is where you manage interactions for a specific channel. It can include communications from various sources such as email, live chat, and social media.",
"LEARN_MORE": "Learn more about inboxes",
"RECONNECTION_REQUIRED": "Your inbox is disconnected. You won't receive new messages until you reauthorize it.",
"CLICK_TO_RECONNECT": "Click here to reconnect.",
"LIST": {
@@ -367,12 +368,19 @@
"DESCRIPTION": "Click on the Sign in with Microsoft button to get started. You will redirected to the email sign in page. Once you accept the requested permissions, you would be redirected back to the inbox creation step.",
"EMAIL_PLACEHOLDER": "Enter email address",
"SIGN_IN": "Sign in with Microsoft",
"HELP": "To add your Microsoft account as a channel, you need to authenticate your Microsoft account by clicking on 'Sign in with Microsoft' ",
"ERROR_MESSAGE": "There was an error connecting to Microsoft, please try again"
},
"GOOGLE": {
"TITLE": "Google Email",
"DESCRIPTION": "Click on the Sign in with Google button to get started. You will redirected to the email sign in page. Once you accept the requested permissions, you would be redirected back to the inbox creation step.",
"SIGN_IN": "Sign in with Google",
"EMAIL_PLACEHOLDER": "Enter email address",
"ERROR_MESSAGE": "There was an error connecting to Google, please try again"
}
},
"DETAILS": {
"LOADING_FB": "Authenticating you with Facebook...",
"ERROR_FB_LOADING": "Error loading Facebook SDK. Please disable any ad-blockers and try again from a different browser.",
"ERROR_FB_AUTH": "Something went wrong, Please refresh page...",
"ERROR_FB_UNAUTHORIZED": "You're not authorized to perform this action. ",
"ERROR_FB_UNAUTHORIZED_HELP": "Please ensure you have access to the Facebook page with full control. You can read more about Facebook roles <a href=\" https://www.facebook.com/help/187316341316631\">here</a>.",
@@ -736,7 +744,20 @@
},
"EMAIL_PROVIDERS": {
"MICROSOFT": "Microsoft",
"GOOGLE": "Google",
"OTHER_PROVIDERS": "Other Providers"
},
"CHANNELS": {
"MESSENGER": "Messenger",
"WEB_WIDGET": "Website",
"TWITTER_PROFILE": "Twitter",
"TWILIO_SMS": "Twilio SMS",
"WHATSAPP": "WhatsApp",
"SMS": "SMS",
"EMAIL": "Email",
"TELEGRAM": "Telegram",
"LINE": "Line",
"API": "API Channel"
}
}
}
@@ -12,6 +12,7 @@
},
"WEBHOOK": {
"SUBSCRIBED_EVENTS": "Subscribed Events",
"LEARN_MORE": "Learn more about webhooks",
"FORM": {
"CANCEL": "Cancel",
"DESC": "Webhook events provide you the realtime information about what's happening in your Chatwoot account. Please enter a valid URL to configure a callback.",
@@ -1,10 +1,11 @@
{
"MACROS": {
"HEADER": "Macros",
"DESCRIPTION": "A macro is a set of saved actions that help customer service agents easily complete tasks. The agents can define a set of actions like tagging a conversation with a label, sending an email transcript, updating a custom attribute, etc., and they can run these actions in a single click.",
"LEARN_MORE": "Learn more about macros",
"HEADER_BTN_TXT": "Add a new macro",
"HEADER_BTN_TXT_SAVE": "Save macro",
"LOADING": "Fetching macros",
"SIDEBAR_TXT": "<p><b>Macros</b><p>A macro is a set of saved actions that help customer service agents easily complete tasks. The agents can define a set of actions like tagging a conversation with a label, sending an email transcript, updating a custom attribute, etc., and they can run these actions in a single click. When the agents run the macro, the actions would be performed sequentially in the order they are defined. Macros improve productivity and increase consistency in actions. </p><p>A macro can be helpful in 2 ways. </p><p><b>As an agent assist:</b> If an agent performs a set of actions multiple times, they can save it as a macro and execute all the actions together using a single click.</p><p><b>As an option to onboard a team member:</b> Every agent has to perform many different checks/actions during each conversation. Onboarding a new support team member will be easy if pre-defined macros are available on the account. Instead of describing each step in detail, the manager/team lead can point to the macros used in different scenarios.</p>",
"ERROR": "Something went wrong. Please try again",
"ORDER_INFO": "Macros will run in the order you add your actions. You can rearrange them by dragging them by the handle beside each node.",
"ADD": {
@@ -277,6 +277,7 @@
"REPORTS_TEAM": "Team",
"SET_AVAILABILITY_TITLE": "Set yourself as",
"SLA": "SLA",
"CUSTOM_ROLES": "Custom Roles",
"BETA": "Beta",
"REPORTS_OVERVIEW": "Overview",
"REAUTHORIZE": "Your inbox connection has expired, please reconnect\n to continue receiving and sending messages",
@@ -1,6 +1,6 @@
{
"SLA": {
"HEADER": "SLA",
"HEADER": "Service Level Agreements",
"ADD_ACTION": "Add SLA",
"ADD_ACTION_LONG": "Create a new SLA Policy",
"DESCRIPTION": "Service Level Agreements (SLAs) are contracts that define clear expectations between your team and customers. They establish standards for response and resolution times, creating a framework for accountability and ensures a consistent, high-quality experience.",
@@ -105,4 +105,4 @@
"HIDE": "Hide {count} rows"
}
}
}
}
@@ -18,7 +18,7 @@
"AND": "و",
"OR": "أو"
},
"INPUT_PLACEHOLDER": "Enter value",
"INPUT_PLACEHOLDER": "أدخل القيمة",
"OPERATOR_LABELS": {
"equal_to": "يساوي",
"not_equal_to": "لا يساوي",
@@ -55,6 +55,9 @@
"CREATED_AT": "تم إنشاؤها في",
"LAST_ACTIVITY": "آخر نشاط"
},
"ERRORS": {
"VALUE_REQUIRED": "القيمة مطلوبة"
},
"GROUPS": {
"STANDARD_FILTERS": "التصفيات القياسية",
"ADDITIONAL_FILTERS": "التصفيات الإضافية",
@@ -3,8 +3,8 @@
"HEADER": "الوكلاء",
"HEADER_BTN_TXT": "إضافة وكيل",
"LOADING": "جار جلب قائمة الوكلاء",
"DESCRIPTION": "An agent is a member of your customer support team who can view and respond to user messages. The list below shows all the agents in your account.",
"LEARN_MORE": "Learn about user roles",
"DESCRIPTION": "الوكيل هو عضو في فريق دعم العملاء الذي يمكنه عرض رسائل المستخدم والرد عليها. القائمة أدناه تظهر جميع الوكلاء في حسابك.",
"LEARN_MORE": "تعرف على أدوار المستخدم",
"AGENT_TYPES": {
"ADMINISTRATOR": "المدير",
"AGENT": "وكيل الدعم"
@@ -18,7 +18,8 @@
"STATUS": "الحالة",
"ACTIONS": "الإجراءات",
"VERIFIED": "تم التحقق",
"VERIFICATION_PENDING": "بانتظار التحقق"
"VERIFICATION_PENDING": "بانتظار التحقق",
"AVAILABLE_CUSTOM_ROLE": "Available custom role permissions"
},
"ADD": {
"TITLE": "إضافة وكيل دعم فني إلى فريقك",
@@ -3,7 +3,8 @@
"HEADER": "صفات مخصصة",
"HEADER_BTN_TXT": "إضافة صفة خاصة",
"LOADING": "جلب الصفات المخصصة",
"SIDEBAR_TXT": "<p><b>الصفات المخصصة</b> <p>صفة مخصصة تتبع الحقائق حول جهات الاتصال/المحادثة الخاصة بك - مثل خُطَّة الاشتراك. أو عندما يطلبون العنصر الأول وما إلى ذلك. <br /><br />لإنشاء صفة مخصصة، فقط انقر فوق<b>أضف صفة مخصصة.</b> يمكنك أيضا تعديل أو حذف صفة مخصصة موجودة بالنقر على زر التحرير أو الحذف.</p>",
"DESCRIPTION": "A custom attribute tracks additional details about your contacts or conversations—such as the subscription plan or the date of their first purchase. You can add different types of custom attributes, such as text, lists, or numbers, to capture the specific information you need.",
"LEARN_MORE": "Learn more about custom attributes",
"ADD": {
"TITLE": "إضافة صفة خاصة",
"SUBMIT": "إنشاء",
@@ -3,8 +3,8 @@
"HEADER": "سجلات التدقيق",
"HEADER_BTN_TXT": "إضافة سجلات التدقيق",
"LOADING": "جارٍ جلب سجلات التدقيق",
"DESCRIPTION": "Audit Logs maintain a record of activities in your account, allowing you to track and audit your account, team, or services.",
"LEARN_MORE": "Learn more about audit logs",
"DESCRIPTION": "سجلات مراجعة الحسابات تحتفظ بسجل للأنشطة في حسابك، مما يسمح لك بتتبع ومراجعة حسابك أو فريقك أو خدماتك.",
"LEARN_MORE": "معرفة المزيد عن سجلات المراجعة",
"SEARCH_404": "لا توجد عناصر مطابقة لهذا الاستعلام",
"SIDEBAR_TXT": "<p><b>سجلات التدقيق</b> </p><p> سجلات التدقيق هي مسارات للأحداث والإجراءات في نظام Chatwoot. </p>",
"LIST": {
@@ -32,7 +32,7 @@
"EDIT": {
"SELF": "%{agentName} غير %{attributes} الخاصة به إلى %{values}",
"OTHER": "%{agentName} غير %{attributes} لـ %{user} إلى %{values}",
"DELETED": "%{agentName} changed %{attributes} of a deleted user to %{values}"
"DELETED": "%{agentName} غير %{attributes} لـ %{user} إلى %{values}"
}
},
"INBOX": {
@@ -1,9 +1,10 @@
{
"AUTOMATION": {
"HEADER": "الأتمتة",
"DESCRIPTION": "Automation can replace and streamline existing processes that require manual effort, such as adding labels and assigning conversations to the most suitable agent. This allows the team to focus on their strengths while reducing time spent on routine tasks.",
"LEARN_MORE": "Learn more about automation",
"HEADER_BTN_TXT": "إضافة قاعدة أتمتة",
"LOADING": "جلب قواعد الأتمتة",
"SIDEBAR_TXT": "<p><b>قواعد الأتمتة الآليه</b> <p>يمكن للأتمتة استبدال وأتمتة العمليات القائمة التي تتطلب جهداً يدوياً. يمكنك القيام بالعديد من الأشياء مع التشغيل الآلي، بما في ذلك إضافة تسميات وتعيين المحادثة لأفضل وكيل. لذا يركز الفريق على ما يفعلونه على أفضل وجه ويقضي وقتاً قليلاً على المهام اليدوية.</p>",
"ADD": {
"TITLE": "إضافة قاعدة أتمتة",
"SUBMIT": "إنشاء",
@@ -94,8 +95,8 @@
"DELETE_MESSAGE": "يجب أن يكون لديك على الأقل شرط واحد للحفظ",
"TEAM_MESSAGE_INPUT_PLACEHOLDER": "اكتب رسالتك هنا",
"TEAM_DROPDOWN_PLACEHOLDER": "اختيار فريق",
"EMAIL_INPUT_PLACEHOLDER": "Enter email",
"URL_INPUT_PLACEHOLDER": "Enter URL"
"EMAIL_INPUT_PLACEHOLDER": "أدخل البريد الإلكتروني",
"URL_INPUT_PLACEHOLDER": "أدخل رابط"
},
"TOGGLE": {
"ACTIVATION_TITLE": "تفعيل قاعدة الأتمتة",
@@ -1,8 +1,8 @@
{
"CANNED_MGMT": {
"HEADER": "الردود الجاهزة",
"LEARN_MORE": "Learn more about canned responses",
"DESCRIPTION": "Canned Responses are pre-written reply templates that help you quickly respond to a conversation. Agents can type the '/' character followed by the shortcode to insert a canned response during a conversation. ",
"LEARN_MORE": "معرفة المزيد عن الاستجابات المعلبة",
"DESCRIPTION": "الردود المسبقة هي قوالب رد مكتوبة مسبقاً تساعدك على الرد بسرعة على محادثة. يمكن للوكلاء كتابة حرف '/' يتبعه الرمز المختصر لإدراج رد مسبق أثناء محادثة. ",
"HEADER_BTN_TXT": "إضافة رد جاهز",
"LOADING": "جاري جلب الردود الجاهزة...",
"SEARCH_404": "لا توجد عناصر مطابقة لهذا الاستعلام.",
@@ -30,6 +30,9 @@
"is_lesser_than": "هو أقل من",
"days_before": "قبل x أيام"
},
"ERRORS": {
"VALUE_REQUIRED": "القيمة مطلوبة"
},
"ATTRIBUTES": {
"NAME": "الاسم",
"EMAIL": "البريد الإلكتروني",
@@ -143,7 +143,7 @@
"PRIVATE_MSG_INPUT": "زر Shift + Enter لإضافة سطر جديد. سيكون هذا مرئياً للوكلاء فقط",
"MESSAGE_SIGNATURE_NOT_CONFIGURED": "لم يتم تكوين توقيع الرسالة، الرجاء تكوينه في إعدادات الملف الشخصي.",
"CLICK_HERE": "انقر هنا للتحديث",
"WHATSAPP_TEMPLATES": "قوالب Whatsapp"
"WHATSAPP_TEMPLATES": "قوالب الواتساب"
},
"REPLYBOX": {
"REPLY": "إضافة رد",
@@ -241,9 +241,9 @@
"ONBOARDING": {
"TITLE": "اهلاً 👋، مرحباً بك في %{installationName}!",
"DESCRIPTION": "شكرا للتسجيل. نريدك أن تحصل على أقصى استفادة من %{installationName}. إليك بعض الأشياء التي يمكنك القيام بها في %{installationName} لجعل التجربة رائعة.",
"GREETING_MORNING": "👋 Good morning, %{name}. Welcome to %{installationName}.",
"GREETING_AFTERNOON": "👋 Good afternoon, %{name}. Welcome to %{installationName}.",
"GREETING_EVENING": "👋 Good evening, %{name}. Welcome to %{installationName}.",
"GREETING_MORNING": "👋 صباح الخير، %{name}. مرحبا بك في %{installationName}.",
"GREETING_AFTERNOON": "👋 مساء الخير، %{name}. مرحبا بك في %{installationName}.",
"GREETING_EVENING": "👋 مساء الخير، %{name}. مرحبا بك في %{installationName}.",
"READ_LATEST_UPDATES": "اطلع على آخر التحديثات",
"ALL_CONVERSATION": {
"TITLE": "جميع محادثاتك في مكان واحد",
@@ -261,14 +261,14 @@
"NEW_LINK": "انقر هنا لإنشاء وسوم"
},
"CANNED_RESPONSES": {
"TITLE": "Create canned responses",
"DESCRIPTION": "Pre-written quick reply templates help you quickly respond to a conversation. Agents can type the '/' character followed by the shortcode to insert a response.",
"NEW_LINK": "Click here to create a canned response"
"TITLE": "إدارة الردود الجاهزة",
"DESCRIPTION": "الردود المسبقة هي قوالب رد مكتوبة مسبقاً تساعدك على الرد بسرعة على محادثة. يمكن للوكلاء كتابة حرف '/' يتبعه الرمز المختصر لإدراج رد مسبق أثناء محادثة.",
"NEW_LINK": "انقر هنا لإنشاء استجابة مسبقة"
}
},
"CONVERSATION_SIDEBAR": {
"ASSIGNEE_LABEL": "الوكيل المكلف",
"SELF_ASSIGN": "إسناد لي",
"SELF_ASSIGN": "إسناد إلي",
"TEAM_LABEL": "الفريق المكلف",
"SELECT": {
"PLACEHOLDER": "لا شيء"
@@ -0,0 +1,91 @@
{
"CUSTOM_ROLE": {
"HEADER": "Custom Roles",
"LEARN_MORE": "Learn more about custom roles",
"DESCRIPTION": "Custom roles are roles that are created by the account owner or admin. These roles can be assigned to agents to define their access and permissions within the account. Custom roles can be created with specific permissions and access levels to suit the requirements of the organization.",
"HEADER_BTN_TXT": "Add custom role",
"LOADING": "Fetching custom roles...",
"SEARCH_404": "لا توجد عناصر مطابقة لهذا الاستعلام.",
"PAYWALL": {
"TITLE": "Upgrade to create custom roles",
"AVAILABLE_ON": "The custom role feature is only available in the Business and Enterprise plans.",
"UPGRADE_PROMPT": "Upgrade your plan to get access to advanced features like team management, automations, custom attributes, and more.",
"UPGRADE_NOW": "Upgrade now",
"CANCEL_ANYTIME": "You can change or cancel your plan anytime"
},
"ENTERPRISE_PAYWALL": {
"AVAILABLE_ON": "The custom role feature is only available in the paid plans.",
"UPGRADE_PROMPT": "Upgrade to a paid plan to access advanced features like audit logs, agent capacity, and more.",
"ASK_ADMIN": "Please reach out to your administrator for the upgrade."
},
"LIST": {
"404": "There are no custom roles available in this account.",
"TITLE": "Manage custom roles",
"DESC": "Custom roles are roles that are created by the account owner or admin. These roles can be assigned to agents to define their access and permissions within the account. Custom roles can be created with specific permissions and access levels to suit the requirements of the organization.",
"TABLE_HEADER": [
"الاسم",
"الوصف",
"Permissions",
"الإجراءات"
]
},
"PERMISSIONS": {
"CONVERSATION_MANAGE": "Manage all conversations",
"CONVERSATION_UNASSIGNED_MANAGE": "Manage unassigned conversations and those assigned to them",
"CONVERSATION_PARTICIPATING_MANAGE": "Manage participating conversations and those assigned to them",
"CONTACT_MANAGE": "Manage contacts",
"REPORT_MANAGE": "Manage reports",
"KNOWLEDGE_BASE_MANAGE": "Manage knowledge base"
},
"FORM": {
"NAME": {
"LABEL": "الاسم",
"PLACEHOLDER": "Please enter a name.",
"ERROR": "الاسم مطلوب."
},
"DESCRIPTION": {
"LABEL": "الوصف",
"PLACEHOLDER": "Please enter a description.",
"ERROR": "الوصف مطلوب."
},
"PERMISSIONS": {
"LABEL": "Permissions",
"ERROR": "Permissions are required."
},
"CANCEL_BUTTON_TEXT": "إلغاء",
"API": {
"ERROR_MESSAGE": "تعذر الاتصال بالخادم، الرجاء المحاولة مرة أخرى لاحقاً"
}
},
"ADD": {
"TITLE": "Add custom role",
"DESC": " Custom roles allows you to create roles with specific permissions and access levels to suit the requirements of the organization.",
"SUBMIT": "إرسال",
"API": {
"SUCCESS_MESSAGE": "Custom role added successfully."
}
},
"EDIT": {
"BUTTON_TEXT": "تعديل",
"TITLE": "Edit custom role",
"DESC": " Custom roles allows you to create roles with specific permissions and access levels to suit the requirements of the organization.",
"SUBMIT": "تحديث",
"API": {
"SUCCESS_MESSAGE": "Custom role updated successfully."
}
},
"DELETE": {
"BUTTON_TEXT": "حذف",
"API": {
"SUCCESS_MESSAGE": "Custom role deleted successfully.",
"ERROR_MESSAGE": "تعذر الاتصال بالخادم، الرجاء المحاولة مرة أخرى لاحقاً"
},
"CONFIRM": {
"TITLE": "تأكيد الحذف",
"MESSAGE": "هل أنت متأكد من الحذف ",
"YES": "نعم، احذف ",
"NO": "لا، احتفظ به "
}
}
}
}
@@ -41,6 +41,7 @@
"UPLOADING": "جاري الرفع...",
"SUCCESS": "تم رفع الصورة بنجاح",
"ERROR": "حدث خطأ أثناء رفع الصورة",
"UN_AUTHORIZED_ERROR": "You are not authorized to upload images",
"ERROR_FILE_SIZE": "حجم الصورة يجب أن يكون أقل من {size}MB",
"ERROR_FILE_FORMAT": "تنسيق الصورة يجب أن يكون jpg أو jpeg أو png",
"ERROR_FILE_DIMENSIONS": "ينبغي أن تكون أبعاد الصورة أقل من 2000 × 2000"
@@ -482,6 +483,6 @@
}
}
},
"LOADING": "Loading..."
"LOADING": "جار التحميل..."
}
}
@@ -1,7 +1,8 @@
{
"INBOX_MGMT": {
"HEADER": "قنوات التواصل",
"SIDEBAR_TXT": "<p><b>قنوات التواصل</b></p> <p> عند ربطك لموقع ويب أو صفحة فيسبوك إلى Chatwoot، يتم تسميتها <b>قناة تواصل</b>. يمكنك إنشاء قنوات تواصل غير محدودة من مختلف الأنواع في حساب Chatwoot الخاص بك. </p><p> انقر فوق <b>إضافة قناة تواصل</b> لربط موقع الويب أو صفحة فيسبوك الخاصة بك. </p><p> من لوحة الإدارة، يمكنك رؤية جميع المحادثات من جميع صناديق الوارد الخاصة بك والرد عليها من مكان موّحد عبر الضغط على علامة التبويب \"المحادثات\". </p><p> يمكنك أيضًا مشاهدة المحادثات الخاصة بصندوق وارد معين بالنقر على اسم صندوق الوارد على الجزء الجانبي من لوحة الإدارة. </p>",
"DESCRIPTION": "القناة هي وضع الاتصال الذي يختاره العميل للتفاعل معك. صندوق الوارد هو المكان الذي تدير فيه التفاعلات لقناة معينة. ويمكن أن تشمل الاتصالات من مصادر مختلفة مثل البريد الإلكتروني، والمحادثة الحية، ووسائط الإعلام الاجتماعية.",
"LEARN_MORE": "تعلم المزيد عن صناديق البريد",
"RECONNECTION_REQUIRED": "Your inbox is disconnected. You won't receive new messages until you reauthorize it.",
"CLICK_TO_RECONNECT": "Click here to reconnect.",
"LIST": {
@@ -367,12 +368,19 @@
"DESCRIPTION": "Click on the Sign in with Microsoft button to get started. You will redirected to the email sign in page. Once you accept the requested permissions, you would be redirected back to the inbox creation step.",
"EMAIL_PLACEHOLDER": "Enter email address",
"SIGN_IN": "تسجيل الدخول باستخدام Microsoft",
"HELP": "To add your Microsoft account as a channel, you need to authenticate your Microsoft account by clicking on 'Sign in with Microsoft' ",
"ERROR_MESSAGE": "There was an error connecting to Microsoft, please try again"
},
"GOOGLE": {
"TITLE": "البريد الإلكتروني لجوجل",
"DESCRIPTION": "انقر على تسجيل الدخول باستخدام زر جوجل للبدء. سيتم إعادة توجيهك إلى صفحة تسجيل الدخول بالبريد الإلكتروني. بمجرد قبول الأذونات المطلوبة، سيتم إعادة توجيهك إلى خطوة إنشاء علبة الوارد.",
"SIGN_IN": "تسجيل الدخول باستخدام Google",
"EMAIL_PLACEHOLDER": "Enter email address",
"ERROR_MESSAGE": "حدث خطأ أثناء الاتصال بجوجل، الرجاء المحاولة مرة أخرى"
}
},
"DETAILS": {
"LOADING_FB": "جار المصادقة والربط مع الفيسبوك...",
"ERROR_FB_LOADING": "Error loading Facebook SDK. Please disable any ad-blockers and try again from a different browser.",
"ERROR_FB_AUTH": "حدث خطأ ما، الرجاء تحديث الصفحة...",
"ERROR_FB_UNAUTHORIZED": "You're not authorized to perform this action. ",
"ERROR_FB_UNAUTHORIZED_HELP": "Please ensure you have access to the Facebook page with full control. You can read more about Facebook roles <a href=\" https://www.facebook.com/help/187316341316631\">here</a>.",
@@ -736,7 +744,20 @@
},
"EMAIL_PROVIDERS": {
"MICROSOFT": "Microsoft",
"GOOGLE": "Google",
"OTHER_PROVIDERS": "Other Providers"
},
"CHANNELS": {
"MESSENGER": "Messenger",
"WEB_WIDGET": "الموقع الإلكتروني",
"TWITTER_PROFILE": "تويتر",
"TWILIO_SMS": "Twilio SMS",
"WHATSAPP": "واتساب",
"SMS": "SMS",
"EMAIL": "البريد الإلكتروني",
"TELEGRAM": "تيليجرام",
"LINE": "Line",
"API": "قناة API"
}
}
}
@@ -12,6 +12,7 @@
},
"WEBHOOK": {
"SUBSCRIBED_EVENTS": "الأحداث المشتركة",
"LEARN_MORE": "Learn more about webhooks",
"FORM": {
"CANCEL": "إلغاء",
"DESC": "أحداث Webhook توفر لك معلومات في الوقت الحقيقي حول ما يحدث في حساب Chatwoot الخاص بك. الرجاء إدخال عنوان URL صالح لتكوين callback.",
@@ -4,7 +4,7 @@
"HEADER_BTN_TXT": "إضافة وسم جديد",
"LOADING": "جار جلب الوسوم",
"DESCRIPTION": "Labels help you categorize and prioritize conversations and leads. You can assign a label to a conversation or contact using the side panel.",
"LEARN_MORE": "Learn more about labels",
"LEARN_MORE": "معرفة المزيد حول التسميات",
"SEARCH_404": "لا توجد عناصر مطابقة لهذا الاستعلام",
"LIST": {
"404": "لا توجد وسوم متوفرة في هذا الحساب.",
@@ -51,7 +51,7 @@
"ADD_SELECTED_LABELS": "إضافة الوسوم المحددة",
"ADD_SELECTED_LABEL": "إضافة الوسوم المحددة",
"ADD_ALL_LABELS": "إضافة جميع الأوسمة",
"SUGGESTED_LABELS": "Suggested labels"
"SUGGESTED_LABELS": "التسميات المقترحة"
},
"ADD": {
"TITLE": "إضافة وسم جديد",
@@ -1,10 +1,11 @@
{
"MACROS": {
"HEADER": "ماكروس",
"DESCRIPTION": "الماكرو هو مجموعة من الإجراءات المحفوظة التي تساعد وكلاء خدمة العملاء على إكمال المهام بسهولة. يمكن للوكلاء تحديد مجموعة من الإجراءات مثل وضع علامة على محادثة مع تسمية، وإرسال نص بريد إلكتروني، وتحديث سمة مخصصة، إلخ. ويمكنهم تنفيذ هذه الإجراءات بنقرة واحدة.",
"LEARN_MORE": "تعلم المزيد حول الماكرو",
"HEADER_BTN_TXT": "إضافة ماكرو جديد",
"HEADER_BTN_TXT_SAVE": "حفظ الماكرو",
"LOADING": "جاري جلب الماكروس",
"SIDEBAR_TXT": "<p><b>الماكروس</b><p>الماكرو هو مجموعة من الإجراءات المحفوظة التي تساعد وكلاء خدمة العملاء على إكمال المهام بسهولة. يمكن للوكلاء تحديد مجموعة من الإجراءات مثل وضع علامة على محادثة مع تسمية، وإرسال نص بريد إلكتروني، وتحديث سمة مخصصة، إلخ. ويمكنهم تنفيذ هذه الإجراءات بنقرة واحدة. وعندما يدير الوكلاء الكلية، يتم تنفيذ الإجراءات بالتسلسل حسب الترتيب الذي تحدده. يحسن الماكرو الإنتاجية ويزيد الاتساق في الإجراءات. </p><p>يمكن أن يكون الماكرو مفيداً بطريقتين. </p><p><b>كوكيل يساعد:</b> إذا قام وكيل بمجموعة من الإجراءات عدة مرات، يمكنهم حفظه كماكلي وتنفيذ جميع الإجراءات معاً باستخدام نقرة واحدة.</p><p><b>كخيار للدخول إلى عضوية الفريق:</b> يجب على كل وكيل إجراء العديد من الشيكات والإجراءات المختلفة خلال كل محادثة. سيكون عضو فريق الدعم الجديد من أونبواردينغ سهلا إذا كان الماكرو المحدد مسبقا متاحا على الحساب. وبدلا من وصف كل خطوة بالتفصيل، يمكن للمدير/الفريق أن يشير إلى الكتلة الكلية المستخدمة في سيناريوهات مختلفة.</p>",
"ERROR": "حدث خطأ ما. الرجاء المحاولة مرة أخرى",
"ORDER_INFO": "سيتم تشغيل الماكرو بالترتيب الذي تضيفه إجراءاتك. يمكنك إعادة ترتيبهم بسحبهم بواسطة المعالج بجانب كل عقدة.",
"ADD": {
@@ -277,6 +277,7 @@
"REPORTS_TEAM": "الفريق",
"SET_AVAILABILITY_TITLE": "تعيين نفسك كـ",
"SLA": "SLA",
"CUSTOM_ROLES": "Custom Roles",
"BETA": "تجريبي",
"REPORTS_OVERVIEW": "نظرة عامة",
"REAUTHORIZE": "انتهت صلاحية اتصال صندوق الوارد الخاص بك، يرجى إعادة الاتصال\n لمواصلة تلقي وإرسال الرسائل",
@@ -1,6 +1,6 @@
{
"SLA": {
"HEADER": "SLA",
"HEADER": "اتفاقات مستوى الخدمة",
"ADD_ACTION": "Add SLA",
"ADD_ACTION_LONG": "Create a new SLA Policy",
"DESCRIPTION": "Service Level Agreements (SLAs) are contracts that define clear expectations between your team and customers. They establish standards for response and resolution times, creating a framework for accountability and ensures a consistent, high-quality experience.",
@@ -105,4 +105,4 @@
"HIDE": "Hide {count} rows"
}
}
}
}
@@ -1,7 +1,7 @@
{
"WHATSAPP_TEMPLATES": {
"MODAL": {
"TITLE": "قوالب Whatsapp",
"TITLE": "قوالب الواتساب",
"SUBTITLE": "حدد القالب الذي تريد إرساله",
"TEMPLATE_SELECTED_SUBTITLE": "معالجة %{templateName}"
},
@@ -55,6 +55,9 @@
"CREATED_AT": "Създаден в",
"LAST_ACTIVITY": "Last activity"
},
"ERRORS": {
"VALUE_REQUIRED": "Изисква се стойност"
},
"GROUPS": {
"STANDARD_FILTERS": "Standard filters",
"ADDITIONAL_FILTERS": "Additional filters",
@@ -18,7 +18,8 @@
"STATUS": "Статус",
"ACTIONS": "Действия",
"VERIFIED": "Проверен",
"VERIFICATION_PENDING": "Предстои проверка"
"VERIFICATION_PENDING": "Предстои проверка",
"AVAILABLE_CUSTOM_ROLE": "Available custom role permissions"
},
"ADD": {
"TITLE": "Добави агент в екипа си",
@@ -3,7 +3,8 @@
"HEADER": "Персонализирани атрибути",
"HEADER_BTN_TXT": "Add Custom Attribute",
"LOADING": "Fetching custom attributes",
"SIDEBAR_TXT": "<p><b>Custom Attributes</b> <p>A custom attribute tracks facts about your contacts/conversation — like the subscription plan, or when they ordered the first item etc. <br /><br />For creating a Custom Attribute, just click on the <b>Add Custom Attribute.</b> You can also edit or delete an existing Custom Attribute by clicking on the Edit or Delete button.</p>",
"DESCRIPTION": "A custom attribute tracks additional details about your contacts or conversations—such as the subscription plan or the date of their first purchase. You can add different types of custom attributes, such as text, lists, or numbers, to capture the specific information you need.",
"LEARN_MORE": "Learn more about custom attributes",
"ADD": {
"TITLE": "Add Custom Attribute",
"SUBMIT": "Създаване",
@@ -1,9 +1,10 @@
{
"AUTOMATION": {
"HEADER": "Automations",
"HEADER": "Автоматизация",
"DESCRIPTION": "Automation can replace and streamline existing processes that require manual effort, such as adding labels and assigning conversations to the most suitable agent. This allows the team to focus on their strengths while reducing time spent on routine tasks.",
"LEARN_MORE": "Learn more about automation",
"HEADER_BTN_TXT": "Добавяне правило за автоматизация",
"LOADING": "Fetching automation rules",
"SIDEBAR_TXT": "<p><b>Automation Rules</b> <p>Automation can replace and automate existing processes that require manual effort. You can do many things with automation, including adding labels and assigning conversation to the best agent. So the team focuses on what they do best and spends more little time on manual tasks.</p>",
"ADD": {
"TITLE": "Добавяне правило за автоматизация",
"SUBMIT": "Създаване",
@@ -30,6 +30,9 @@
"is_lesser_than": "Is lesser than",
"days_before": "Is x days before"
},
"ERRORS": {
"VALUE_REQUIRED": "Изисква се стойност"
},
"ATTRIBUTES": {
"NAME": "Име",
"EMAIL": "Имейл",
@@ -0,0 +1,91 @@
{
"CUSTOM_ROLE": {
"HEADER": "Custom Roles",
"LEARN_MORE": "Learn more about custom roles",
"DESCRIPTION": "Custom roles are roles that are created by the account owner or admin. These roles can be assigned to agents to define their access and permissions within the account. Custom roles can be created with specific permissions and access levels to suit the requirements of the organization.",
"HEADER_BTN_TXT": "Add custom role",
"LOADING": "Fetching custom roles...",
"SEARCH_404": "Няма резултати отговарящи на тази заявка.",
"PAYWALL": {
"TITLE": "Upgrade to create custom roles",
"AVAILABLE_ON": "The custom role feature is only available in the Business and Enterprise plans.",
"UPGRADE_PROMPT": "Upgrade your plan to get access to advanced features like team management, automations, custom attributes, and more.",
"UPGRADE_NOW": "Upgrade now",
"CANCEL_ANYTIME": "You can change or cancel your plan anytime"
},
"ENTERPRISE_PAYWALL": {
"AVAILABLE_ON": "The custom role feature is only available in the paid plans.",
"UPGRADE_PROMPT": "Upgrade to a paid plan to access advanced features like audit logs, agent capacity, and more.",
"ASK_ADMIN": "Please reach out to your administrator for the upgrade."
},
"LIST": {
"404": "There are no custom roles available in this account.",
"TITLE": "Manage custom roles",
"DESC": "Custom roles are roles that are created by the account owner or admin. These roles can be assigned to agents to define their access and permissions within the account. Custom roles can be created with specific permissions and access levels to suit the requirements of the organization.",
"TABLE_HEADER": [
"Име",
"Описание",
"Permissions",
"Действия"
]
},
"PERMISSIONS": {
"CONVERSATION_MANAGE": "Manage all conversations",
"CONVERSATION_UNASSIGNED_MANAGE": "Manage unassigned conversations and those assigned to them",
"CONVERSATION_PARTICIPATING_MANAGE": "Manage participating conversations and those assigned to them",
"CONTACT_MANAGE": "Manage contacts",
"REPORT_MANAGE": "Manage reports",
"KNOWLEDGE_BASE_MANAGE": "Manage knowledge base"
},
"FORM": {
"NAME": {
"LABEL": "Име",
"PLACEHOLDER": "Please enter a name.",
"ERROR": "Name is required."
},
"DESCRIPTION": {
"LABEL": "Описание",
"PLACEHOLDER": "Please enter a description.",
"ERROR": "Description is required."
},
"PERMISSIONS": {
"LABEL": "Permissions",
"ERROR": "Permissions are required."
},
"CANCEL_BUTTON_TEXT": "Отмени",
"API": {
"ERROR_MESSAGE": "Не можа да се свърже с Woot сървър. Моля, опитайте отново по-късно"
}
},
"ADD": {
"TITLE": "Add custom role",
"DESC": " Custom roles allows you to create roles with specific permissions and access levels to suit the requirements of the organization.",
"SUBMIT": "Изпращане",
"API": {
"SUCCESS_MESSAGE": "Custom role added successfully."
}
},
"EDIT": {
"BUTTON_TEXT": "Редактирай",
"TITLE": "Edit custom role",
"DESC": " Custom roles allows you to create roles with specific permissions and access levels to suit the requirements of the organization.",
"SUBMIT": "Обновяване",
"API": {
"SUCCESS_MESSAGE": "Custom role updated successfully."
}
},
"DELETE": {
"BUTTON_TEXT": "Изтрий",
"API": {
"SUCCESS_MESSAGE": "Custom role deleted successfully.",
"ERROR_MESSAGE": "Не можа да се свърже с Woot сървър. Моля, опитайте отново по-късно"
},
"CONFIRM": {
"TITLE": "Потвърди изтриването",
"MESSAGE": "Сигурни ли сте за изтриването ",
"YES": "Yes, delete ",
"NO": "No, keep "
}
}
}
}
@@ -41,6 +41,7 @@
"UPLOADING": "Качване...",
"SUCCESS": "Image uploaded successfully",
"ERROR": "Error while uploading image",
"UN_AUTHORIZED_ERROR": "You are not authorized to upload images",
"ERROR_FILE_SIZE": "Image size should be less than {size}MB",
"ERROR_FILE_FORMAT": "Image format should be jpg, jpeg or png",
"ERROR_FILE_DIMENSIONS": "Image dimensions should be less than 2000 x 2000"
@@ -1,7 +1,8 @@
{
"INBOX_MGMT": {
"HEADER": "Inboxes",
"SIDEBAR_TXT": "<p><b>Inbox</b></p> <p> When you connect a website or a facebook Page to Chatwoot, it is called an <b>Inbox</b>. You can have unlimited inboxes in your Chatwoot account. </p><p> Click on <b>Add Inbox</b> to connect a website or a Facebook Page. </p><p> In the Dashboard, you can see all the conversations from all your inboxes in a single place and respond to them under the `Conversations` tab. </p><p> You can also see conversations specific to an inbox by clicking on the inbox name on the left pane of the dashboard. </p>",
"DESCRIPTION": "A channel is the mode of communication your customer chooses to interact with you. An inbox is where you manage interactions for a specific channel. It can include communications from various sources such as email, live chat, and social media.",
"LEARN_MORE": "Learn more about inboxes",
"RECONNECTION_REQUIRED": "Your inbox is disconnected. You won't receive new messages until you reauthorize it.",
"CLICK_TO_RECONNECT": "Click here to reconnect.",
"LIST": {
@@ -367,12 +368,19 @@
"DESCRIPTION": "Click on the Sign in with Microsoft button to get started. You will redirected to the email sign in page. Once you accept the requested permissions, you would be redirected back to the inbox creation step.",
"EMAIL_PLACEHOLDER": "Enter email address",
"SIGN_IN": "Sign in with Microsoft",
"HELP": "To add your Microsoft account as a channel, you need to authenticate your Microsoft account by clicking on 'Sign in with Microsoft' ",
"ERROR_MESSAGE": "There was an error connecting to Microsoft, please try again"
},
"GOOGLE": {
"TITLE": "Google Email",
"DESCRIPTION": "Click on the Sign in with Google button to get started. You will redirected to the email sign in page. Once you accept the requested permissions, you would be redirected back to the inbox creation step.",
"SIGN_IN": "Sign in with Google",
"EMAIL_PLACEHOLDER": "Enter email address",
"ERROR_MESSAGE": "There was an error connecting to Google, please try again"
}
},
"DETAILS": {
"LOADING_FB": "Authenticating you with Facebook...",
"ERROR_FB_LOADING": "Error loading Facebook SDK. Please disable any ad-blockers and try again from a different browser.",
"ERROR_FB_AUTH": "Something went wrong, Please refresh page...",
"ERROR_FB_UNAUTHORIZED": "You're not authorized to perform this action. ",
"ERROR_FB_UNAUTHORIZED_HELP": "Please ensure you have access to the Facebook page with full control. You can read more about Facebook roles <a href=\" https://www.facebook.com/help/187316341316631\">here</a>.",
@@ -736,7 +744,20 @@
},
"EMAIL_PROVIDERS": {
"MICROSOFT": "Microsoft",
"GOOGLE": "Google",
"OTHER_PROVIDERS": "Other Providers"
},
"CHANNELS": {
"MESSENGER": "Messenger",
"WEB_WIDGET": "Website",
"TWITTER_PROFILE": "Twitter",
"TWILIO_SMS": "Twilio SMS",
"WHATSAPP": "WhatsApp",
"SMS": "SMS",
"EMAIL": "Имейл",
"TELEGRAM": "Telegram",
"LINE": "Line",
"API": "API Channel"
}
}
}
@@ -12,6 +12,7 @@
},
"WEBHOOK": {
"SUBSCRIBED_EVENTS": "Subscribed Events",
"LEARN_MORE": "Learn more about webhooks",
"FORM": {
"CANCEL": "Отмени",
"DESC": "Webhook events provide you the realtime information about what's happening in your Chatwoot account. Please enter a valid URL to configure a callback.",
@@ -1,10 +1,11 @@
{
"MACROS": {
"HEADER": "Macros",
"DESCRIPTION": "A macro is a set of saved actions that help customer service agents easily complete tasks. The agents can define a set of actions like tagging a conversation with a label, sending an email transcript, updating a custom attribute, etc., and they can run these actions in a single click.",
"LEARN_MORE": "Learn more about macros",
"HEADER_BTN_TXT": "Add a new macro",
"HEADER_BTN_TXT_SAVE": "Save macro",
"LOADING": "Fetching macros",
"SIDEBAR_TXT": "<p><b>Macros</b><p>A macro is a set of saved actions that help customer service agents easily complete tasks. The agents can define a set of actions like tagging a conversation with a label, sending an email transcript, updating a custom attribute, etc., and they can run these actions in a single click. When the agents run the macro, the actions would be performed sequentially in the order they are defined. Macros improve productivity and increase consistency in actions. </p><p>A macro can be helpful in 2 ways. </p><p><b>As an agent assist:</b> If an agent performs a set of actions multiple times, they can save it as a macro and execute all the actions together using a single click.</p><p><b>As an option to onboard a team member:</b> Every agent has to perform many different checks/actions during each conversation. Onboarding a new support team member will be easy if pre-defined macros are available on the account. Instead of describing each step in detail, the manager/team lead can point to the macros used in different scenarios.</p>",
"ERROR": "Something went wrong. Please try again",
"ORDER_INFO": "Macros will run in the order you add your actions. You can rearrange them by dragging them by the handle beside each node.",
"ADD": {
@@ -277,6 +277,7 @@
"REPORTS_TEAM": "Team",
"SET_AVAILABILITY_TITLE": "Set yourself as",
"SLA": "SLA",
"CUSTOM_ROLES": "Custom Roles",
"BETA": "Beta",
"REPORTS_OVERVIEW": "Overview",
"REAUTHORIZE": "Your inbox connection has expired, please reconnect\n to continue receiving and sending messages",
@@ -1,6 +1,6 @@
{
"SLA": {
"HEADER": "SLA",
"HEADER": "Service Level Agreements",
"ADD_ACTION": "Add SLA",
"ADD_ACTION_LONG": "Create a new SLA Policy",
"DESCRIPTION": "Service Level Agreements (SLAs) are contracts that define clear expectations between your team and customers. They establish standards for response and resolution times, creating a framework for accountability and ensures a consistent, high-quality experience.",
@@ -105,4 +105,4 @@
"HIDE": "Hide {count} rows"
}
}
}
}
@@ -55,6 +55,9 @@
"CREATED_AT": "Creat a",
"LAST_ACTIVITY": "Última activitat"
},
"ERRORS": {
"VALUE_REQUIRED": "El valor és necessari"
},
"GROUPS": {
"STANDARD_FILTERS": "Filtres estàndard",
"ADDITIONAL_FILTERS": "Filtres addicionals",
@@ -18,7 +18,8 @@
"STATUS": "Estat",
"ACTIONS": "Accions",
"VERIFIED": "Verificat",
"VERIFICATION_PENDING": "Verificació pendent"
"VERIFICATION_PENDING": "Verificació pendent",
"AVAILABLE_CUSTOM_ROLE": "Available custom role permissions"
},
"ADD": {
"TITLE": "Afegir agent al teu equip",
@@ -3,7 +3,8 @@
"HEADER": "Atributs personalitzats",
"HEADER_BTN_TXT": "Afegir atribut personalitzat",
"LOADING": "S'estan recollint atributs personalitzats",
"SIDEBAR_TXT": "<p><b>Atributs personalitzats</b> <p>Un atribut personalitzat fa un seguiment dels fets sobre els vostres contactes/converses, com ara el pla de subscripció, o quan van demanar el primer element, etc. <br /><br />Per crear un atribut personalitzat, només cal que feu clic a <b>Afegeix un atribut personalitzat.</b> També podeu editar o suprimir un atribut personalitzat existent fent clic al botó Edita o Suprimeix.</p>",
"DESCRIPTION": "A custom attribute tracks additional details about your contacts or conversations—such as the subscription plan or the date of their first purchase. You can add different types of custom attributes, such as text, lists, or numbers, to capture the specific information you need.",
"LEARN_MORE": "Learn more about custom attributes",
"ADD": {
"TITLE": "Afegir atribut personalitzat",
"SUBMIT": "Crear",
@@ -1,9 +1,10 @@
{
"AUTOMATION": {
"HEADER": "Automatització",
"DESCRIPTION": "Automation can replace and streamline existing processes that require manual effort, such as adding labels and assigning conversations to the most suitable agent. This allows the team to focus on their strengths while reducing time spent on routine tasks.",
"LEARN_MORE": "Learn more about automation",
"HEADER_BTN_TXT": "Afegeix una regla d'automatització",
"LOADING": "S'estan recollint regles d'automatització",
"SIDEBAR_TXT": "<p><b>Regles d'automatització</b> <p>L'automatització pot substituir i automatitzar processos existents que requereixen un esforç manual. Pots fer moltes coses amb l'automatització, com ara afegir etiquetes i assignar converses al millor agent. Així, l'equip se centra en allò que millor fa i dedica més poc temps a les tasques manuals.</p>",
"ADD": {
"TITLE": "Afegeix una regla d'automatització",
"SUBMIT": "Crear",
@@ -30,6 +30,9 @@
"is_lesser_than": "És més petit que",
"days_before": "És x dies abans"
},
"ERRORS": {
"VALUE_REQUIRED": "El valor és necessari"
},
"ATTRIBUTES": {
"NAME": "Nom",
"EMAIL": "Correu electrònic",
@@ -0,0 +1,91 @@
{
"CUSTOM_ROLE": {
"HEADER": "Custom Roles",
"LEARN_MORE": "Learn more about custom roles",
"DESCRIPTION": "Custom roles are roles that are created by the account owner or admin. These roles can be assigned to agents to define their access and permissions within the account. Custom roles can be created with specific permissions and access levels to suit the requirements of the organization.",
"HEADER_BTN_TXT": "Add custom role",
"LOADING": "Fetching custom roles...",
"SEARCH_404": "No hi ha cap resposta que coincideixi amb aquesta consulta.",
"PAYWALL": {
"TITLE": "Upgrade to create custom roles",
"AVAILABLE_ON": "The custom role feature is only available in the Business and Enterprise plans.",
"UPGRADE_PROMPT": "Actualitza el teu pla per accedir a funcions avançades com ara gestió d'equips, automatitzacions, atributs personalitzats i molt més.",
"UPGRADE_NOW": "Actualitza ara",
"CANCEL_ANYTIME": "Pots canviar o cancel·lar el teu pla en qualsevol moment"
},
"ENTERPRISE_PAYWALL": {
"AVAILABLE_ON": "The custom role feature is only available in the paid plans.",
"UPGRADE_PROMPT": "Actualitza a un pla de pagament per accedir a funcions avançades com els registres d'auditoria, la capacitat de l'agent i molt més.",
"ASK_ADMIN": "Posa't en contacte amb el vostre administrador per obtenir l'actualització."
},
"LIST": {
"404": "There are no custom roles available in this account.",
"TITLE": "Manage custom roles",
"DESC": "Custom roles are roles that are created by the account owner or admin. These roles can be assigned to agents to define their access and permissions within the account. Custom roles can be created with specific permissions and access levels to suit the requirements of the organization.",
"TABLE_HEADER": [
"Nom",
"Descripció",
"Permissions",
"Accions"
]
},
"PERMISSIONS": {
"CONVERSATION_MANAGE": "Manage all conversations",
"CONVERSATION_UNASSIGNED_MANAGE": "Manage unassigned conversations and those assigned to them",
"CONVERSATION_PARTICIPATING_MANAGE": "Manage participating conversations and those assigned to them",
"CONTACT_MANAGE": "Manage contacts",
"REPORT_MANAGE": "Manage reports",
"KNOWLEDGE_BASE_MANAGE": "Manage knowledge base"
},
"FORM": {
"NAME": {
"LABEL": "Nom",
"PLACEHOLDER": "Please enter a name.",
"ERROR": "El nom és obligatori."
},
"DESCRIPTION": {
"LABEL": "Descripció",
"PLACEHOLDER": "Please enter a description.",
"ERROR": "La descripció és necessària."
},
"PERMISSIONS": {
"LABEL": "Permissions",
"ERROR": "Permissions are required."
},
"CANCEL_BUTTON_TEXT": "Cancel·la",
"API": {
"ERROR_MESSAGE": "No s'ha pogut connectar amb el servidor Woot. Torna-ho a provar més endavant"
}
},
"ADD": {
"TITLE": "Add custom role",
"DESC": " Custom roles allows you to create roles with specific permissions and access levels to suit the requirements of the organization.",
"SUBMIT": "Envia",
"API": {
"SUCCESS_MESSAGE": "Custom role added successfully."
}
},
"EDIT": {
"BUTTON_TEXT": "Edita",
"TITLE": "Edit custom role",
"DESC": " Custom roles allows you to create roles with specific permissions and access levels to suit the requirements of the organization.",
"SUBMIT": "Actualitza",
"API": {
"SUCCESS_MESSAGE": "Custom role updated successfully."
}
},
"DELETE": {
"BUTTON_TEXT": "Esborrar",
"API": {
"SUCCESS_MESSAGE": "Custom role deleted successfully.",
"ERROR_MESSAGE": "No s'ha pogut connectar amb el servidor Woot. Torna-ho a provar més endavant"
},
"CONFIRM": {
"TITLE": "Confirma la supressió",
"MESSAGE": "N'estas segur? ",
"YES": "Sí, esborra ",
"NO": "No, mantén-la "
}
}
}
}
@@ -41,6 +41,7 @@
"UPLOADING": "S'està carregant...",
"SUCCESS": "La imatge s'ha carregat correctament",
"ERROR": "S'ha produït un error en carregar la imatge",
"UN_AUTHORIZED_ERROR": "You are not authorized to upload images",
"ERROR_FILE_SIZE": "La mida de la imatge ha de ser inferior a {size}MB",
"ERROR_FILE_FORMAT": "El format de la imatge ha de ser JPG, JPEG o PNG",
"ERROR_FILE_DIMENSIONS": "Les dimensions de la imatge han de ser inferiors a 2000 x 2000"
@@ -1,7 +1,8 @@
{
"INBOX_MGMT": {
"HEADER": "Safates d'entrada",
"SIDEBAR_TXT": "<p><b>Safata dentrada</b></p> <p> Quan connecteu un lloc web o una pàgina de Facebook a Chatwoot, sanomena<b>Safata dentrada</b>. Podeu tenir bústies dentrada il·limitades al vostre compte de Chatwoot. </p><p> Fer clic a <b>Add Safata dentrada</b> per connectar un lloc web o una pàgina de Facebook.</p><p> Al tauler, pots veure totes les converses de totes les teves safates d'entrada en un sol lloc i respondre-hi a la pestanya `Converses`.</p><p> També pots veure converses específiques duna safata dentrada fent clic al nom de la safata dentrada a l'esquerre del tauler. </p>",
"DESCRIPTION": "A channel is the mode of communication your customer chooses to interact with you. An inbox is where you manage interactions for a specific channel. It can include communications from various sources such as email, live chat, and social media.",
"LEARN_MORE": "Learn more about inboxes",
"RECONNECTION_REQUIRED": "La teva safata d'entrada està desconnectada. No rebràs missatges nous fins que no els tornis a autoritzar.",
"CLICK_TO_RECONNECT": "Fes clic aquí per tornar a connectar-te.",
"LIST": {
@@ -367,12 +368,19 @@
"DESCRIPTION": "Fes clic al botó Inicia sessió amb Microsoft per començar. Se't redirigirà a la pàgina d'inici de sessió del correu electrònic. Un cop acceptis els permisos sol·licitats, se't redirigirà al pas de creació de la safata d'entrada.",
"EMAIL_PLACEHOLDER": "Introdueix una adreça de correu electrònic",
"SIGN_IN": "Inicia la sessió amb Microsoft",
"HELP": "Per afegir el teu compte de Microsoft com a canal, has d'autenticar el teu compte de Microsoft fent clic a \"Inicia la sessió amb Microsoft\" ",
"ERROR_MESSAGE": "S'ha produït un error en connectar amb Microsoft, torna-ho a provar"
},
"GOOGLE": {
"TITLE": "Google Email",
"DESCRIPTION": "Click on the Sign in with Google button to get started. You will redirected to the email sign in page. Once you accept the requested permissions, you would be redirected back to the inbox creation step.",
"SIGN_IN": "Sign in with Google",
"EMAIL_PLACEHOLDER": "Introdueix una adreça de correu electrònic",
"ERROR_MESSAGE": "There was an error connecting to Google, please try again"
}
},
"DETAILS": {
"LOADING_FB": "S'està autenticant amb Facebook...",
"ERROR_FB_LOADING": "Error loading Facebook SDK. Please disable any ad-blockers and try again from a different browser.",
"ERROR_FB_AUTH": "Alguna cosa ha anat malament, actualitza la pàgina...",
"ERROR_FB_UNAUTHORIZED": "No estàs autoritzat per dur a terme aquesta acció. ",
"ERROR_FB_UNAUTHORIZED_HELP": "Assegura't que tens accés a la pàgina de Facebook amb control total. Pots obtenir més informació sobre les funcions de Facebook <a href=\" https://www.facebook.com/help/187316341316631\">aquí</a>.",
@@ -736,7 +744,20 @@
},
"EMAIL_PROVIDERS": {
"MICROSOFT": "Microsoft",
"GOOGLE": "Google",
"OTHER_PROVIDERS": "Altres proveïdors"
},
"CHANNELS": {
"MESSENGER": "Messenger",
"WEB_WIDGET": "Lloc web",
"TWITTER_PROFILE": "Twitter",
"TWILIO_SMS": "Twilio SMS",
"WHATSAPP": "WhatsApp",
"SMS": "SMS",
"EMAIL": "Correu electrònic",
"TELEGRAM": "Telegram",
"LINE": "Line",
"API": "Canal de l'API"
}
}
}
@@ -12,6 +12,7 @@
},
"WEBHOOK": {
"SUBSCRIBED_EVENTS": "Esdeveniments subscrits",
"LEARN_MORE": "Learn more about webhooks",
"FORM": {
"CANCEL": "Cancel·la",
"DESC": "Els esdeveniments de Webhook us proporcionen informació en temps real sobre el que passa al vostre compte de Chatwoot. Introduïu una URL vàlid per configurar un callback.",
@@ -1,10 +1,11 @@
{
"MACROS": {
"HEADER": "Macros",
"DESCRIPTION": "A macro is a set of saved actions that help customer service agents easily complete tasks. The agents can define a set of actions like tagging a conversation with a label, sending an email transcript, updating a custom attribute, etc., and they can run these actions in a single click.",
"LEARN_MORE": "Learn more about macros",
"HEADER_BTN_TXT": "Afegeix una nova macro",
"HEADER_BTN_TXT_SAVE": "Desa la macro",
"LOADING": "Obtenció de macros",
"SIDEBAR_TXT": "<p><b>Macros</b><p>Una macro és un conjunt d'accions desades que ajuden els agents d'atenció al client a completar tasques fàcilment. Els agents poden definir un conjunt d'accions com etiquetar una conversa amb una etiqueta, enviar una transcripció de correu electrònic, actualitzar un atribut personalitzat, etc., i poden executar aquestes accions amb un sol clic. Quan els agents executen la macro, les accions es realitzaran seqüencialment en l'ordre en què es defineixen. Les macros milloren la productivitat i augmenten la coherència en les accions. </p><p>Una macro pot ser útil de dues maneres. </p><p><b>Com a ajuda d'un agent:</b> si un agent realitza un conjunt d'accions diverses vegades, pot desar-lo com a macro i executar totes les accions junts amb un sol clic.</ p><p><b>Com a opció per incorporar un membre de l'equip:</b> Cada agent ha de realitzar moltes comprovacions/accions diferents durant cada conversa. La incorporació d'un nou membre de l'equip d'assistència serà fàcil si hi ha macros predefinides disponibles al compte. En lloc de descriure cada pas en detall, el responsable o el responsable de l'equip pot indicar les macros utilitzades en diferents escenaris.</p>",
"ERROR": "Alguna cosa ha anat malament, torna-ho a provar",
"ORDER_INFO": "Les macros s'executaran en l'ordre en què afegeixis les teves accions. Pots reorganitzar-los arrossegant-los per l'identificador al costat de cada node.",
"ADD": {
@@ -277,6 +277,7 @@
"REPORTS_TEAM": "Equip",
"SET_AVAILABILITY_TITLE": "Posa't com",
"SLA": "SLA",
"CUSTOM_ROLES": "Custom Roles",
"BETA": "Beta",
"REPORTS_OVERVIEW": "Resum",
"REAUTHORIZE": "La vostra connexió a la safata d'entrada ha caducat, torna a connectar\n per continuar rebent i enviant missatges",
@@ -1,6 +1,6 @@
{
"SLA": {
"HEADER": "SLA",
"HEADER": "Service Level Agreements",
"ADD_ACTION": "Afegeix SLA",
"ADD_ACTION_LONG": "Crea una nova política de SLA",
"DESCRIPTION": "Els acords de nivell de servei (SLA) són contractes que defineixen expectatives clares entre el vostre equip i els clients. Estableixen estàndards de temps de resposta i resolució, creen un marc de responsabilitat i garanteixen una experiència coherent i d'alta qualitat.",
@@ -105,4 +105,4 @@
"HIDE": "Amaga {count} files"
}
}
}
}
@@ -55,6 +55,9 @@
"CREATED_AT": "Created at",
"LAST_ACTIVITY": "Last activity"
},
"ERRORS": {
"VALUE_REQUIRED": "Hodnota je povinná"
},
"GROUPS": {
"STANDARD_FILTERS": "Standard filters",
"ADDITIONAL_FILTERS": "Additional filters",
@@ -18,7 +18,8 @@
"STATUS": "Stav",
"ACTIONS": "Akce",
"VERIFIED": "Ověřeno",
"VERIFICATION_PENDING": "Probíhá ověření"
"VERIFICATION_PENDING": "Probíhá ověření",
"AVAILABLE_CUSTOM_ROLE": "Available custom role permissions"
},
"ADD": {
"TITLE": "Přidat agenta do vašeho týmu",
@@ -3,7 +3,8 @@
"HEADER": "Vlastní atributy",
"HEADER_BTN_TXT": "Add Custom Attribute",
"LOADING": "Fetching custom attributes",
"SIDEBAR_TXT": "<p><b>Custom Attributes</b> <p>A custom attribute tracks facts about your contacts/conversation — like the subscription plan, or when they ordered the first item etc. <br /><br />For creating a Custom Attribute, just click on the <b>Add Custom Attribute.</b> You can also edit or delete an existing Custom Attribute by clicking on the Edit or Delete button.</p>",
"DESCRIPTION": "A custom attribute tracks additional details about your contacts or conversations—such as the subscription plan or the date of their first purchase. You can add different types of custom attributes, such as text, lists, or numbers, to capture the specific information you need.",
"LEARN_MORE": "Learn more about custom attributes",
"ADD": {
"TITLE": "Add Custom Attribute",
"SUBMIT": "Create",
@@ -1,9 +1,10 @@
{
"AUTOMATION": {
"HEADER": "Automations",
"HEADER": "Automation",
"DESCRIPTION": "Automation can replace and streamline existing processes that require manual effort, such as adding labels and assigning conversations to the most suitable agent. This allows the team to focus on their strengths while reducing time spent on routine tasks.",
"LEARN_MORE": "Learn more about automation",
"HEADER_BTN_TXT": "Add Automation Rule",
"LOADING": "Fetching automation rules",
"SIDEBAR_TXT": "<p><b>Automation Rules</b> <p>Automation can replace and automate existing processes that require manual effort. You can do many things with automation, including adding labels and assigning conversation to the best agent. So the team focuses on what they do best and spends more little time on manual tasks.</p>",
"ADD": {
"TITLE": "Add Automation Rule",
"SUBMIT": "Create",
@@ -30,6 +30,9 @@
"is_lesser_than": "Je menší než",
"days_before": "Je o x dnů dříve"
},
"ERRORS": {
"VALUE_REQUIRED": "Hodnota je povinná"
},
"ATTRIBUTES": {
"NAME": "Název",
"EMAIL": "E-mailová adresa",
@@ -0,0 +1,91 @@
{
"CUSTOM_ROLE": {
"HEADER": "Custom Roles",
"LEARN_MORE": "Learn more about custom roles",
"DESCRIPTION": "Custom roles are roles that are created by the account owner or admin. These roles can be assigned to agents to define their access and permissions within the account. Custom roles can be created with specific permissions and access levels to suit the requirements of the organization.",
"HEADER_BTN_TXT": "Add custom role",
"LOADING": "Fetching custom roles...",
"SEARCH_404": "Neexistují žádné položky odpovídající tomuto dotazu.",
"PAYWALL": {
"TITLE": "Upgrade to create custom roles",
"AVAILABLE_ON": "The custom role feature is only available in the Business and Enterprise plans.",
"UPGRADE_PROMPT": "Upgrade your plan to get access to advanced features like team management, automations, custom attributes, and more.",
"UPGRADE_NOW": "Upgrade now",
"CANCEL_ANYTIME": "You can change or cancel your plan anytime"
},
"ENTERPRISE_PAYWALL": {
"AVAILABLE_ON": "The custom role feature is only available in the paid plans.",
"UPGRADE_PROMPT": "Upgrade to a paid plan to access advanced features like audit logs, agent capacity, and more.",
"ASK_ADMIN": "Please reach out to your administrator for the upgrade."
},
"LIST": {
"404": "There are no custom roles available in this account.",
"TITLE": "Manage custom roles",
"DESC": "Custom roles are roles that are created by the account owner or admin. These roles can be assigned to agents to define their access and permissions within the account. Custom roles can be created with specific permissions and access levels to suit the requirements of the organization.",
"TABLE_HEADER": [
"Název",
"Description",
"Permissions",
"Akce"
]
},
"PERMISSIONS": {
"CONVERSATION_MANAGE": "Manage all conversations",
"CONVERSATION_UNASSIGNED_MANAGE": "Manage unassigned conversations and those assigned to them",
"CONVERSATION_PARTICIPATING_MANAGE": "Manage participating conversations and those assigned to them",
"CONTACT_MANAGE": "Manage contacts",
"REPORT_MANAGE": "Manage reports",
"KNOWLEDGE_BASE_MANAGE": "Manage knowledge base"
},
"FORM": {
"NAME": {
"LABEL": "Název",
"PLACEHOLDER": "Please enter a name.",
"ERROR": "Name is required."
},
"DESCRIPTION": {
"LABEL": "Description",
"PLACEHOLDER": "Please enter a description.",
"ERROR": "Description is required."
},
"PERMISSIONS": {
"LABEL": "Permissions",
"ERROR": "Permissions are required."
},
"CANCEL_BUTTON_TEXT": "Zrušit",
"API": {
"ERROR_MESSAGE": "Nelze se připojit k Woot serveru, opakujte akci později"
}
},
"ADD": {
"TITLE": "Add custom role",
"DESC": " Custom roles allows you to create roles with specific permissions and access levels to suit the requirements of the organization.",
"SUBMIT": "Odeslat",
"API": {
"SUCCESS_MESSAGE": "Custom role added successfully."
}
},
"EDIT": {
"BUTTON_TEXT": "Upravit",
"TITLE": "Edit custom role",
"DESC": " Custom roles allows you to create roles with specific permissions and access levels to suit the requirements of the organization.",
"SUBMIT": "Aktualizovat",
"API": {
"SUCCESS_MESSAGE": "Custom role updated successfully."
}
},
"DELETE": {
"BUTTON_TEXT": "Vymazat",
"API": {
"SUCCESS_MESSAGE": "Custom role deleted successfully.",
"ERROR_MESSAGE": "Nelze se připojit k Woot serveru, opakujte akci později"
},
"CONFIRM": {
"TITLE": "Confirm deletion",
"MESSAGE": "Opravdu chcete odstranit ",
"YES": "Yes, delete ",
"NO": "No, keep "
}
}
}
}
@@ -41,6 +41,7 @@
"UPLOADING": "Nahrávání...",
"SUCCESS": "Image uploaded successfully",
"ERROR": "Error while uploading image",
"UN_AUTHORIZED_ERROR": "You are not authorized to upload images",
"ERROR_FILE_SIZE": "Image size should be less than {size}MB",
"ERROR_FILE_FORMAT": "Image format should be jpg, jpeg or png",
"ERROR_FILE_DIMENSIONS": "Image dimensions should be less than 2000 x 2000"
@@ -1,7 +1,8 @@
{
"INBOX_MGMT": {
"HEADER": "Schránky",
"SIDEBAR_TXT": "<p><b>Inbox</b></p> <p> When you connect a website or a facebook Page to Chatwoot, it is called an <b>Inbox</b>. You can have unlimited inboxes in your Chatwoot account. </p><p> Click on <b>Add Inbox</b> to connect a website or a Facebook Page. </p><p> In the Dashboard, you can see all the conversations from all your inboxes in a single place and respond to them under the `Conversations` tab. </p><p> You can also see conversations specific to an inbox by clicking on the inbox name on the left pane of the dashboard. </p>",
"DESCRIPTION": "A channel is the mode of communication your customer chooses to interact with you. An inbox is where you manage interactions for a specific channel. It can include communications from various sources such as email, live chat, and social media.",
"LEARN_MORE": "Learn more about inboxes",
"RECONNECTION_REQUIRED": "Your inbox is disconnected. You won't receive new messages until you reauthorize it.",
"CLICK_TO_RECONNECT": "Click here to reconnect.",
"LIST": {
@@ -367,12 +368,19 @@
"DESCRIPTION": "Click on the Sign in with Microsoft button to get started. You will redirected to the email sign in page. Once you accept the requested permissions, you would be redirected back to the inbox creation step.",
"EMAIL_PLACEHOLDER": "Enter email address",
"SIGN_IN": "Sign in with Microsoft",
"HELP": "To add your Microsoft account as a channel, you need to authenticate your Microsoft account by clicking on 'Sign in with Microsoft' ",
"ERROR_MESSAGE": "There was an error connecting to Microsoft, please try again"
},
"GOOGLE": {
"TITLE": "Google Email",
"DESCRIPTION": "Click on the Sign in with Google button to get started. You will redirected to the email sign in page. Once you accept the requested permissions, you would be redirected back to the inbox creation step.",
"SIGN_IN": "Sign in with Google",
"EMAIL_PLACEHOLDER": "Enter email address",
"ERROR_MESSAGE": "There was an error connecting to Google, please try again"
}
},
"DETAILS": {
"LOADING_FB": "Ověřování pomocí Facebooku...",
"ERROR_FB_LOADING": "Error loading Facebook SDK. Please disable any ad-blockers and try again from a different browser.",
"ERROR_FB_AUTH": "Něco se pokazilo, prosím obnovte stránku...",
"ERROR_FB_UNAUTHORIZED": "You're not authorized to perform this action. ",
"ERROR_FB_UNAUTHORIZED_HELP": "Please ensure you have access to the Facebook page with full control. You can read more about Facebook roles <a href=\" https://www.facebook.com/help/187316341316631\">here</a>.",
@@ -736,7 +744,20 @@
},
"EMAIL_PROVIDERS": {
"MICROSOFT": "Microsoft",
"GOOGLE": "Google",
"OTHER_PROVIDERS": "Other Providers"
},
"CHANNELS": {
"MESSENGER": "Messenger",
"WEB_WIDGET": "Website",
"TWITTER_PROFILE": "Twitter",
"TWILIO_SMS": "Twilio SMS",
"WHATSAPP": "WhatsApp",
"SMS": "SMS",
"EMAIL": "E-mailová adresa",
"TELEGRAM": "Telegram",
"LINE": "Line",
"API": "API Channel"
}
}
}

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