Compare commits

..
25 Commits
Author SHA1 Message Date
Sojan Jose 88ffa329eb Merge branch 'release/4.13.0'
Publish Chatwoot EE docker images / build (linux/amd64, ubuntu-latest) (push) Waiting to run
Publish Chatwoot EE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Waiting to run
Publish Chatwoot EE docker images / merge (push) Blocked by required conditions
Publish Chatwoot CE docker images / build (linux/amd64, ubuntu-latest) (push) Waiting to run
Publish Chatwoot CE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Waiting to run
Publish Chatwoot CE docker images / merge (push) Blocked by required conditions
2026-04-16 19:02:52 +05:30
Sojan Jose e123a4e500 Bump version to 4.13.0 2026-04-16 19:02:23 +05:30
Sojan JoseandGitHub 135be52431 feat: Introduce last responding agent option to automation assign agent (#12326)
Introduce a `Last Responding Agent` options to assign_agents action in
automations to cover the following use cases.

- Assign conversations to first responding agent : ( automation message
created at , if assignee is nil, assign last responding agent )
- Ensure conversations are not resolved with out an assignee : (
automation conversation resolved at : if assignee is nil, assign last
responding agent )

and potential other cases.

fixes: #1592
2026-04-16 18:54:35 +05:30
03c10ba147 chore: Update translations (#14080)
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-04-16 18:12:33 +05:30
aa2e8f99e4 fix(i18n): correct zh/zh_CN conversation assignment message translations (#14033)
## Summary

The `assignee_name` and `user_name` variables are swapped in the Chinese
(zh/zh_CN) locale files for conversation assignment activity messages,
causing the rendered text to show the wrong person as the assignee.

### Before (incorrect)

| Template | English (correct) | Chinese (incorrect) |
|---|---|---|
| `assignee.assigned` | Assigned to **AgentA** by **Admin** | 由
**AgentA** 分配给 **Admin** |
| `team.assigned` | Assigned to **TeamX** by **Admin** | 由 **TeamX** 分配给
**Admin** |
| `team.assigned_with_assignee` | Assigned to **AgentA** via **TeamX**
by **Admin** | 由 **AgentA** 分配给 **TeamX** 团队的 **Admin** |

The Chinese text reads as if the conversation was assigned **to Admin**
(the API caller), when it was actually assigned **to AgentA**.

### After (correct)

| Template | Chinese (fixed) |
|---|---|
| `assignee.assigned` | 由 **Admin** 分配给 **AgentA** |
| `team.assigned` | 由 **Admin** 分配给 **TeamX** |
| `team.assigned_with_assignee` | 由 **Admin** 通过 **TeamX** 团队分配给
**AgentA** |

Now correctly matches the English template semantics.

## Files Changed

- `config/locales/zh_CN.yml` — 3 lines
- `config/locales/zh.yml` — 3 lines

## How to Verify

1. Set locale to `zh_CN`
2. Have an admin assign a conversation to an agent
3. Check the activity message in the conversation — the assignee name
should appear after "分配给", not before it

Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-04-16 16:34:20 +05:30
Sojan JoseandGitHub aee979ee0b fix: add explicit remove assignment actions to macros and automations (#12172)
This updates macros and automations so agents can explicitly remove
assigned agents or teams, while keeping the existing `Assign -> None`
flow working for backward compatibility.

Fixes: #7551
Closes: #7551

## Why
The original macro change exposed unassignment only through `Assign ->
None`, which made macros behave differently from automations and left
the explicit remove actions inconsistent across the product. This keeps
the lower-risk compatibility path and adds the explicit remove actions
requested in review.

## What this change does
- Adds `Remove Assigned Agent` and `Remove Assigned Team` as explicit
actions in macros.
- Adds the same explicit remove actions in automations.
- Keeps `Assign Agent -> None` and `Assign Team -> None` working for
existing behavior and stored payloads.
- Preserves backward compatibility for existing macro and automation
execution payloads.
- Downmerges the latest `develop` and resolves the conflicts while
keeping both the new remove actions and current `develop` behavior.

## Validation
- Verified both remove actions are available and selectable in the macro
editor.
- Verified both remove actions are available and selectable in the
automation builder.
- Applied a disposable macro with `Remove Assigned Agent` and `Remove
Assigned Team` on a real conversation and confirmed both fields were
cleared.
- Applied a disposable macro with `Assign Agent -> None` and `Assign
Team -> None` on a real conversation and confirmed both fields were
still cleared.
2026-04-16 15:57:41 +05:30
Vishnu NarayananandGitHub 72b8a31f2d fix: handle users being stuck on is_creating billing flow (#12750)
Fixes https://linear.app/chatwoot/issue/CW-5880/handle-customers-being-stuck-on-biling-page
2026-04-16 13:22:31 +05:30
Sivin VargheseandGitHub 48533e2a5d fix: strip markdown hard-break backslashes from webhook payloads (#13950) 2026-04-16 13:19:35 +05:30
b5264a2560 feat: Adds the ability to resize the editor (#13916)
# Pull Request Template

## Description

This PR adds support for resizing the reply editor up to nearly half the
screen height. It also deprecates the old modal-based pop-out reply box,
clicking the same button now expands the editor inline. Users can adjust
the height using the slider or the expand button.


## Type of change

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

## How Has This Been Tested?

### Loom video
https://www.loom.com/share/be27e1c06d19475ab404289710b3b0da


## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules

---------

Co-authored-by: Pranav <pranav@chatwoot.com>
2026-04-16 12:37:56 +05:30
98cf1ce9f6 fix(bulk-select): limit select-all to visible items; add secondary slot (#12891)
Update BulkSelectBar to compute selection state (indeterminate/all) from
visible item IDs and only toggle selection for visible items. Preserve
existing selection for off-screen items when toggling, and guard against
empty visibility. Add detection/rendering for an optional
secondary-actions slot and adjust layout/divider. Also fix
ContactsBulkActionBar selection logic to determine "all selected" by
verifying every visible ID is in the selection. These changes ensure
correct select-all behavior with filtered/visible lists and support
additional UI actions.



https://github.com/user-attachments/assets/d06b78d1-a64a-4c0c-a82a-f870140236c7


# Pull Request Template

## Description

Please include a summary of the change and issue(s) fixed. Also, mention
relevant motivation, context, and any dependencies that this change
requires.
Fixes # (issue)

## Type of change

Please delete options that are not relevant.

- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality not to work as expected)
- [ ] This change requires a documentation update

## How Has This Been Tested?

Please describe the tests that you ran to verify your changes. Provide
instructions so we can reproduce. Please also list any relevant details
for your test configuration.


## Checklist:

- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules

---------

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
2026-04-16 12:22:53 +05:30
Sivin VargheseandGitHub 5eee331da3 feat: add slash command menu to article editor (#14035) 2026-04-16 11:27:59 +05:30
Sivin VargheseandGitHub edd0fc98db feat: Table support in article editor (#13974) 2026-04-16 11:23:10 +05:30
Gabriel JablonskiandGitHub cc008951db fix(sidebar): improve active child route matching logic (#13121) 2026-04-16 10:57:16 +05:30
97dae52841 fix: use committed model registry for RubyLLM (#14067)
RubyLLM bundles a static models.json that doesn't know about models
released after the gem was published. Self-hosted users configuring
newer models hit ModelNotFoundError.

Added a rake task that refreshes the registry from models.dev and saves
to disk. ~~Called during Docker image build so every deploy gets fresh
model data. Falls back silently to the bundled registry if models.dev is
unreachable.~~

Commit the models.json file to code so it is available across
deployments.

---------

Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-04-16 10:28:38 +05:30
Aakash BakhleandGitHub 5264de24b0 feat: migrations for document auto-sync [AI-141] (#14041)
# Pull Request Template

## Description

Add migrations for document auto-sync

Fixes # (issue)

## Type of change

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

## How Has This Been Tested?
locally

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged and published in downstream
modules
2026-04-15 17:56:10 +05:30
b96bf41234 chore: Enable Participating tab for conversations (#11714)
## Summary

This PR enables the **Participating** conversation view in the main
sidebar and keeps the behavior aligned with existing conversation views.

## What changed

- Added **Participating** under Conversations in the new sidebar.
- Added a guard in conversation realtime `addConversation` flow so
generic `conversation.created` events are not injected while the user is
on Participating view.
- Added participating route mapping in conversation-list redirect helper
so list redirects resolve correctly to `/participating/conversations`.

## Scope notes

- Kept changes minimal and consistent with current `develop` behavior.
- No additional update-event filtering was added beyond what existing
views already do.

---------


Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
2026-04-15 17:03:39 +05:30
3f9f054c43 fix: drop WhatsApp incoming messages from blocked contacts (#14061)
## Linear ticket

https://linear.app/chatwoot/issue/CW-6839/blocked-contact-can-still-send-messages-to-whatsapp-inbox

## Description

Drop WhatsApp incoming messages  from blocked contacts

## Type of change

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

## How Has This Been Tested?

- Incoming messages for blocked contacts 

## Checklist:

- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2026-04-15 13:42:48 +07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Sivin Varghese
8e5d4f4d23 chore(deps): bump axios from 1.13.6 to 1.15.0 (#14051)
Bumps [axios](https://github.com/axios/axios) from 1.13.6 to 1.15.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/axios/axios/releases">axios's
releases</a>.</em></p>
<blockquote>
<h2>v1.15.0</h2>
<p>This release delivers two critical security patches, adds runtime
support for Deno and Bun, and includes significant CI hardening,
documentation improvements, and routine dependency updates.</p>
<h2>⚠️ Important Changes</h2>
<ul>
<li><strong>Deprecation:</strong> <code>url.parse()</code> usage has
been replaced to address Node.js deprecation warnings. If you are on a
recent version of Node.js, this resolves console warnings you may have
been seeing. (<strong><a
href="https://redirect.github.com/axios/axios/issues/10625">#10625</a></strong>)</li>
</ul>
<h2>🔒 Security Fixes</h2>
<ul>
<li><strong>Proxy Handling:</strong> Fixed a <code>no_proxy</code>
hostname normalisation bypass that could lead to Server-Side Request
Forgery (SSRF). (<strong><a
href="https://redirect.github.com/axios/axios/issues/10661">#10661</a></strong>)</li>
<li><strong>Header Injection:</strong> Fixed an unrestricted cloud
metadata exfiltration vulnerability via a header injection chain.
(<strong><a
href="https://redirect.github.com/axios/axios/issues/10660">#10660</a></strong>)</li>
</ul>
<h2>🚀 New Features</h2>
<ul>
<li><strong>Runtime Support:</strong> Added compatibility checks and
documentation for Deno and Bun environments. (<strong><a
href="https://redirect.github.com/axios/axios/issues/10652">#10652</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10653">#10653</a></strong>)</li>
</ul>
<h2>🔧 Maintenance &amp; Chores</h2>
<ul>
<li><strong>CI Security:</strong> Hardened workflow permissions to least
privilege, added the <code>zizmor</code> security scanner, pinned action
versions, and gated npm publishing with OIDC and environment protection.
(<strong><a
href="https://redirect.github.com/axios/axios/issues/10618">#10618</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10619">#10619</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10627">#10627</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10637">#10637</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10666">#10666</a></strong>)</li>
<li><strong>Dependencies:</strong> Bumped
<code>serialize-javascript</code>, <code>handlebars</code>,
<code>picomatch</code>, <code>vite</code>, and
<code>denoland/setup-deno</code> to latest versions. Added a 7-day
Dependabot cooldown period. (<strong><a
href="https://redirect.github.com/axios/axios/issues/10574">#10574</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10572">#10572</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10568">#10568</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10663">#10663</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10664">#10664</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10665">#10665</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10669">#10669</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10670">#10670</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10616">#10616</a></strong>)</li>
<li><strong>Documentation:</strong> Unified docs, improved
<code>beforeRedirect</code> credential leakage example, clarified
<code>withCredentials</code>/<code>withXSRFToken</code> behaviour,
HTTP/2 support notes, async/await timeout error handling, header case
preservation, and various typo fixes. (<strong><a
href="https://redirect.github.com/axios/axios/issues/10649">#10649</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10624">#10624</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/7452">#7452</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/7471">#7471</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10654">#10654</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10644">#10644</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10589">#10589</a></strong>)</li>
<li><strong>Housekeeping:</strong> Removed stale files, regenerated
lockfile, and updated sponsor scripts and blocks. (<strong><a
href="https://redirect.github.com/axios/axios/issues/10584">#10584</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10650">#10650</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10582">#10582</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10640">#10640</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10659">#10659</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10668">#10668</a></strong>)</li>
<li><strong>Tests:</strong> Added regression coverage for urlencoded
<code>Content-Type</code> casing. (<strong><a
href="https://redirect.github.com/axios/axios/issues/10573">#10573</a></strong>)</li>
</ul>
<h2>🌟 New Contributors</h2>
<p>We are thrilled to welcome our new contributors. Thank you for
helping improve Axios:</p>
<ul>
<li><strong><a
href="https://github.com/raashish1601"><code>@​raashish1601</code></a></strong>
(<strong><a
href="https://redirect.github.com/axios/axios/issues/10573">#10573</a></strong>)</li>
<li><strong><a
href="https://github.com/Kilros0817"><code>@​Kilros0817</code></a></strong>
(<strong><a
href="https://redirect.github.com/axios/axios/issues/10625">#10625</a></strong>)</li>
<li><strong><a
href="https://github.com/ashstrc"><code>@​ashstrc</code></a></strong>
(<strong><a
href="https://redirect.github.com/axios/axios/issues/10624">#10624</a></strong>)</li>
<li><strong><a
href="https://github.com/Abhi3975"><code>@​Abhi3975</code></a></strong>
(<strong><a
href="https://redirect.github.com/axios/axios/issues/10589">#10589</a></strong>)</li>
<li><strong><a
href="https://github.com/theamodhshetty"><code>@​theamodhshetty</code></a></strong>
(<strong><a
href="https://redirect.github.com/axios/axios/issues/7452">#7452</a></strong>)</li>
</ul>
<h2>v1.14.0</h2>
<p>This release focuses on compatibility fixes, adapter stability
improvements, and test/tooling modernisation.</p>
<h2>⚠️ Important Changes</h2>
<ul>
<li><strong>Breaking Changes:</strong> None identified in this
release.</li>
<li><strong>Action Required:</strong> If you rely on env-based proxy
behaviour or CJS resolution edge-cases, validate your integration after
upgrade (notably <code>proxy-from-env</code> v2 alignment and
<code>main</code> entry compatibility fix).</li>
</ul>
<h2>🚀 New Features</h2>
<ul>
<li><strong>Runtime Features:</strong> No new end-user features were
introduced in this release.</li>
<li><strong>Test Coverage Expansion:</strong> Added broader smoke/module
test coverage for CJS and ESM package usage. (<a
href="https://redirect.github.com/axios/axios/pull/7510">#7510</a>)</li>
</ul>
<h2>🐛 Bug Fixes</h2>
<ul>
<li><strong>Headers:</strong> Trim trailing CRLF in normalised header
values. (<a
href="https://redirect.github.com/axios/axios/pull/7456">#7456</a>)</li>
<li><strong>HTTP/2:</strong> Close detached HTTP/2 sessions on timeout
to avoid lingering sessions. (<a
href="https://redirect.github.com/axios/axios/pull/7457">#7457</a>)</li>
<li><strong>Fetch Adapter:</strong> Cancel <code>ReadableStream</code>
created during request-stream capability probing to prevent async
resource leaks. (<a
href="https://redirect.github.com/axios/axios/pull/7515">#7515</a>)</li>
<li><strong>Proxy Handling:</strong> Fixed env proxy behavior with
<code>proxy-from-env</code> v2 usage. (<a
href="https://redirect.github.com/axios/axios/pull/7499">#7499</a>)</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/axios/axios/blob/v1.x/CHANGELOG.md">axios's
changelog</a>.</em></p>
<blockquote>
<h1>Changelog</h1>
<h2><a
href="https://github.com/axios/axios/compare/v1.13.2...v1.13.3">1.13.3</a>
(2026-01-20)</h2>
<h3>Bug Fixes</h3>
<ul>
<li><strong>http2:</strong> Use port 443 for HTTPS connections by
default. (<a
href="https://redirect.github.com/axios/axios/issues/7256">#7256</a>)
(<a
href="https://github.com/axios/axios/commit/d7e60653460480ffacecf85383012ca1baa6263e">d7e6065</a>)</li>
<li><strong>interceptor:</strong> handle the error in the same
interceptor (<a
href="https://redirect.github.com/axios/axios/issues/6269">#6269</a>)
(<a
href="https://github.com/axios/axios/commit/5945e40bb171d4ac4fc195df276cf952244f0f89">5945e40</a>)</li>
<li>main field in package.json should correspond to cjs artifacts (<a
href="https://redirect.github.com/axios/axios/issues/5756">#5756</a>)
(<a
href="https://github.com/axios/axios/commit/7373fbff24cd92ce650d99ff6f7fe08c2e2a0a04">7373fbf</a>)</li>
<li><strong>package.json:</strong> add 'bun' package.json 'exports'
condition. Load the Node.js build in Bun instead of the browser build
(<a
href="https://redirect.github.com/axios/axios/issues/5754">#5754</a>)
(<a
href="https://github.com/axios/axios/commit/b89217e3e91de17a3d55e2b8f39ceb0e9d8aeda8">b89217e</a>)</li>
<li>silentJSONParsing=false should throw on invalid JSON (<a
href="https://redirect.github.com/axios/axios/issues/7253">#7253</a>)
(<a
href="https://redirect.github.com/axios/axios/issues/7257">#7257</a>)
(<a
href="https://github.com/axios/axios/commit/7d19335e43d6754a1a9a66e424f7f7da259895bf">7d19335</a>)</li>
<li>turn AxiosError into a native error (<a
href="https://redirect.github.com/axios/axios/issues/5394">#5394</a>)
(<a
href="https://redirect.github.com/axios/axios/issues/5558">#5558</a>)
(<a
href="https://github.com/axios/axios/commit/1c6a86dd2c0623ee1af043a8491dbc96d40e883b">1c6a86d</a>)</li>
<li><strong>types:</strong> add handlers to AxiosInterceptorManager
interface (<a
href="https://redirect.github.com/axios/axios/issues/5551">#5551</a>)
(<a
href="https://github.com/axios/axios/commit/8d1271b49fc226ed7defd07cd577bd69a55bb13a">8d1271b</a>)</li>
<li><strong>types:</strong> restore AxiosError.cause type from unknown
to Error (<a
href="https://redirect.github.com/axios/axios/issues/7327">#7327</a>)
(<a
href="https://github.com/axios/axios/commit/d8233d9e8e9a64bfba9bbe01d475ba417510b82b">d8233d9</a>)</li>
<li>unclear error message is thrown when specifying an empty proxy
authorization (<a
href="https://redirect.github.com/axios/axios/issues/6314">#6314</a>)
(<a
href="https://github.com/axios/axios/commit/6ef867e684adf7fb2343e3b29a79078a3c76dc29">6ef867e</a>)</li>
</ul>
<h3>Features</h3>
<ul>
<li>add <code>undefined</code> as a value in AxiosRequestConfig (<a
href="https://redirect.github.com/axios/axios/issues/5560">#5560</a>)
(<a
href="https://github.com/axios/axios/commit/095033c626895ecdcda2288050b63dcf948db3bd">095033c</a>)</li>
<li>add automatic minor and patch upgrades to dependabot (<a
href="https://redirect.github.com/axios/axios/issues/6053">#6053</a>)
(<a
href="https://github.com/axios/axios/commit/65a7584eda6164980ddb8cf5372f0afa2a04c1ed">65a7584</a>)</li>
<li>add Node.js coverage script using c8 (closes <a
href="https://redirect.github.com/axios/axios/issues/7289">#7289</a>)
(<a
href="https://redirect.github.com/axios/axios/issues/7294">#7294</a>)
(<a
href="https://github.com/axios/axios/commit/ec9d94e9f88da13e9219acadf65061fb38ce080a">ec9d94e</a>)</li>
<li>added copilot instructions (<a
href="https://github.com/axios/axios/commit/3f83143bfe617eec17f9d7dcf8bafafeeae74c26">3f83143</a>)</li>
<li>compatibility with frozen prototypes (<a
href="https://redirect.github.com/axios/axios/issues/6265">#6265</a>)
(<a
href="https://github.com/axios/axios/commit/860e03396a536e9b926dacb6570732489c9d7012">860e033</a>)</li>
<li>enhance pipeFileToResponse with error handling (<a
href="https://redirect.github.com/axios/axios/issues/7169">#7169</a>)
(<a
href="https://github.com/axios/axios/commit/88d78842541610692a04282233933d078a8a2552">88d7884</a>)</li>
<li><strong>types:</strong> Intellisense for string literals in a
widened union (<a
href="https://redirect.github.com/axios/axios/issues/6134">#6134</a>)
(<a
href="https://github.com/axios/axios/commit/f73474d02c5aa957b2daeecee65508557fd3c6e5">f73474d</a>),
closes <a
href="https://redirect.github.com//redirect.github.com/microsoft/TypeScript/issues/33471/issues/issuecomment-1376364329">microsoft/TypeScript#33471</a></li>
</ul>
<h3>Reverts</h3>
<ul>
<li>Revert &quot;fix: silentJSONParsing=false should throw on invalid
JSON (<a
href="https://redirect.github.com/axios/axios/issues/7253">#7253</a>)
(<a
href="https://redirect.github.com/axios/axios/issues/7">#7</a>…&quot;
(<a
href="https://redirect.github.com/axios/axios/issues/7298">#7298</a>)
(<a
href="https://github.com/axios/axios/commit/a4230f5581b3f58b6ff531b6dbac377a4fd7942a">a4230f5</a>),
closes <a
href="https://redirect.github.com/axios/axios/issues/7253">#7253</a> <a
href="https://redirect.github.com/axios/axios/issues/7">#7</a> <a
href="https://redirect.github.com/axios/axios/issues/7298">#7298</a></li>
<li><strong>deps:</strong> bump peter-evans/create-pull-request from 7
to 8 in the github-actions group (<a
href="https://redirect.github.com/axios/axios/issues/7334">#7334</a>)
(<a
href="https://github.com/axios/axios/commit/2d6ad5e48bd29b0b2b5e7e95fb473df98301543a">2d6ad5e</a>)</li>
</ul>
<h3>Contributors to this release</h3>
<ul>
<li><!-- raw HTML omitted --> <a href="https://github.com/ashvin2005"
title="+1752/-4 ([#7218](https://github.com/axios/axios/issues/7218)
[#7218](https://github.com/axios/axios/issues/7218) )">Ashvin
Tiwari</a></li>
<li><!-- raw HTML omitted --> <a href="https://github.com/mochinikunj"
title="+940/-12 ([#7294](https://github.com/axios/axios/issues/7294)
[#7294](https://github.com/axios/axios/issues/7294) )">Nikunj
Mochi</a></li>
<li><!-- raw HTML omitted --> <a href="https://github.com/imanchalsingh"
title="+544/-102 ([#7169](https://github.com/axios/axios/issues/7169)
[#7185](https://github.com/axios/axios/issues/7185) )">Anchal
Singh</a></li>
<li><!-- raw HTML omitted --> <a href="https://github.com/jasonsaayman"
title="+317/-73 ([#7334](https://github.com/axios/axios/issues/7334)
[#7298](https://github.com/axios/axios/issues/7298)
)">jasonsaayman</a></li>
<li><!-- raw HTML omitted --> <a href="https://github.com/brodo"
title="+99/-120 ([#5558](https://github.com/axios/axios/issues/5558)
)">Julian Dax</a></li>
<li><!-- raw HTML omitted --> <a
href="https://github.com/AKASHDHARDUBEY" title="+167/-0
([#7287](https://github.com/axios/axios/issues/7287)
[#7288](https://github.com/axios/axios/issues/7288) )">Akash Dhar
Dubey</a></li>
<li><!-- raw HTML omitted --> <a href="https://github.com/madhumitaaa"
title="+20/-68 ([#7198](https://github.com/axios/axios/issues/7198)
)">Madhumita</a></li>
<li><!-- raw HTML omitted --> <a href="https://github.com/Tackoil"
title="+80/-2 ([#6269](https://github.com/axios/axios/issues/6269)
)">Tackoil</a></li>
<li><!-- raw HTML omitted --> <a href="https://github.com/justindhillon"
title="+41/-41 ([#6324](https://github.com/axios/axios/issues/6324)
[#6315](https://github.com/axios/axios/issues/6315) )">Justin
Dhillon</a></li>
<li><!-- raw HTML omitted --> <a href="https://github.com/Rudrxxx"
title="+71/-2 ([#7257](https://github.com/axios/axios/issues/7257)
)">Rudransh</a></li>
<li><!-- raw HTML omitted --> <a href="https://github.com/WuMingDao"
title="+36/-36 ([#7215](https://github.com/axios/axios/issues/7215)
)">WuMingDao</a></li>
<li><!-- raw HTML omitted --> <a href="https://github.com/codenomnom"
title="+70/-0 ([#7201](https://github.com/axios/axios/issues/7201)
[#7201](https://github.com/axios/axios/issues/7201)
)">codenomnom</a></li>
<li><!-- raw HTML omitted --> <a href="https://github.com/Nandann018-ux"
title="+60/-10 ([#7272](https://github.com/axios/axios/issues/7272)
)">Nandan Acharya</a></li>
<li><!-- raw HTML omitted --> <a href="https://github.com/KernelDeimos"
title="+22/-40 ([#7042](https://github.com/axios/axios/issues/7042)
)">Eric Dubé</a></li>
<li><!-- raw HTML omitted --> <a href="https://github.com/tiborpilz"
title="+40/-4 ([#5551](https://github.com/axios/axios/issues/5551)
)">Tibor Pilz</a></li>
<li><!-- raw HTML omitted --> <a href="https://github.com/joaoGabriel55"
title="+31/-4 ([#6314](https://github.com/axios/axios/issues/6314)
)">Gabriel Quaresma</a></li>
<li><!-- raw HTML omitted --> <a href="https://github.com/turadg"
title="+23/-6 ([#6265](https://github.com/axios/axios/issues/6265)
)">Turadg Aleahmad</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/axios/axios/commit/772a4e54ecc4cc2421e2b746daff0aca10f359d7"><code>772a4e5</code></a>
chore(release): prepare release 1.15.0 (<a
href="https://redirect.github.com/axios/axios/issues/10671">#10671</a>)</li>
<li><a
href="https://github.com/axios/axios/commit/4b071371be2f810b4bc7797a13838e0f806ebb22"><code>4b07137</code></a>
chore(deps-dev): bump vite from 8.0.0 to 8.0.5 in /tests/smoke/esm (<a
href="https://redirect.github.com/axios/axios/issues/10663">#10663</a>)</li>
<li><a
href="https://github.com/axios/axios/commit/51e57b39db251bfe3d34af5c943dfea18e06c8b6"><code>51e57b3</code></a>
chore(deps-dev): bump vite from 8.0.2 to 8.0.5 (<a
href="https://redirect.github.com/axios/axios/issues/10664">#10664</a>)</li>
<li><a
href="https://github.com/axios/axios/commit/fba1a77930f0c459677b729161627234b88c90aa"><code>fba1a77</code></a>
chore(deps-dev): bump vite from 8.0.2 to 8.0.5 in /tests/module/esm (<a
href="https://redirect.github.com/axios/axios/issues/10665">#10665</a>)</li>
<li><a
href="https://github.com/axios/axios/commit/0bf6e28eac86e87da2b60bbf5ea4237910e1a08e"><code>0bf6e28</code></a>
chore(deps): bump denoland/setup-deno in the github-actions group (<a
href="https://redirect.github.com/axios/axios/issues/10669">#10669</a>)</li>
<li><a
href="https://github.com/axios/axios/commit/8107157c572ee4a54cb28c01ab7f7f3d895ba661"><code>8107157</code></a>
chore(deps-dev): bump the development_dependencies group with 4 updates
(<a
href="https://redirect.github.com/axios/axios/issues/10670">#10670</a>)</li>
<li><a
href="https://github.com/axios/axios/commit/e66530e3302d56176befd0778155dafea2487542"><code>e66530e</code></a>
ci: require npm-publish environment for releases (<a
href="https://redirect.github.com/axios/axios/issues/10666">#10666</a>)</li>
<li><a
href="https://github.com/axios/axios/commit/49f23cbfe4d308a075281c5f798d4c68f648cbe2"><code>49f23cb</code></a>
chore(sponsor): update sponsor block (<a
href="https://redirect.github.com/axios/axios/issues/10668">#10668</a>)</li>
<li><a
href="https://github.com/axios/axios/commit/363185461b90b1b78845dc8a99a1f103d9b122a1"><code>3631854</code></a>
fix: unrestricted cloud metadata exfiltration via header injection chain
(<a
href="https://redirect.github.com/axios/axios/issues/10">#10</a>...</li>
<li><a
href="https://github.com/axios/axios/commit/fb3befb6daac6cad26b2e54094d0f2d9e47f24df"><code>fb3befb</code></a>
fix: no_proxy hostname normalization bypass leads to ssrf (<a
href="https://redirect.github.com/axios/axios/issues/10661">#10661</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/axios/axios/compare/v1.13.6...v1.15.0">compare
view</a></li>
</ul>
</details>
<details>
<summary>Install script changes</summary>
<p>This version modifies <code>prepare</code> script that runs during
installation. Review the package contents before updating.</p>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=axios&package-manager=npm_and_yarn&previous-version=1.13.6&new-version=1.15.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 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>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2026-04-15 00:44:54 +05:30
64f6bfc811 feat: Inline edit support for contact info (#13976)
# Pull Request Template

## Description

This PR adds inline editing support for contact name, phone number,
email, and company fields in the conversation contact sidebar

## Type of change

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

## How Has This Been Tested?

**Screencast**


https://github.com/user-attachments/assets/e9f8e37d-145b-4736-b27a-eb9ea66847bd



## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules

---------

Co-authored-by: Pranav <pranav@chatwoot.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2026-04-14 16:53:40 +04:00
72c9e1775b fix: Prevent article editor from resetting content while typing (#14014)
# Pull Request Template

## Description


### Description

This PR fixes an issue where the editor would reset content and move the
cursor while typing. The issue was caused by a dual debounce setup
(400ms + 2500ms) that saved content and then overwrote local state with
stale API responses while the user was still typing.

### What changed

* Editor now uses local state (`localTitle`, `localContent`) as the
source of truth while editing
* Vuex store is only used on initial load or navigation
* Replaced dual debounce with a single 500ms debounce (fewer API calls)
* `UPDATE_ARTICLE` now merges updates instead of replacing the article
  * Prevents status changes from wiping unsaved content
* Removed `updateAsync` for a simpler update flow

### How it works

User types
→ local ref updates immediately (editor reads from this)
→ 500ms debounce triggers
→ dispatches `articles/update`
→ API persists the change
→ on success: store merges the response (used by other components)
→ editor remains unaffected (continues using local state)



Fixes
https://linear.app/chatwoot/issue/CW-6727/better-syncing-of-content-the-editor-randomly-updates-the-content

## Type of change

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

## How Has This Been Tested?

1. Open any Help Center article for editing
2. Type continuously for a few seconds — content should not reset or
jump
3. Change article status (publish/archive/draft) while editing — content
should remain intact
4. Test on a slow network (use DevTools throttling) — typing should
remain smooth


## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2026-04-14 16:48:38 +04:00
b7b6e67df7 fix(captain): localize AI summary to account language (#13790)
AI-generated summaries now respect the account's language setting.
Previously, summaries were always returned in English regardless of the
user's configured language, making section headings like "Customer
Intent" and "Action Items" appear in English even for non-English
accounts.

Previous behavior:
<img width="1336" height="790" alt="image"
src="https://github.com/user-attachments/assets/5df8b78b-1218-438d-9578-a806b5cb94ac"
/>


Current Behavior: 
<img width="1253" height="372" alt="image"
src="https://github.com/user-attachments/assets/ae932c97-06da-4baf-9f77-9719bc9162e8"
/>


## What changed
- Added explicit account locale to the AI system prompt in
`Captain::SummaryService`
- Updated the summary prompt template to instruct the model to translate
section headings

## How to test
1. Configure an account with a non-English language (e.g., Portuguese)
2. Open a conversation with messages
3. Use the Copilot "Summarize" feature
4. Verify that section headings ("Customer Intent", "Conversation
Summary", etc.) appear in the account's language

---------

Co-authored-by: Aakash Bakhle <48802744+aakashb95@users.noreply.github.com>
2026-04-14 17:36:10 +05:30
Sivin VargheseandGitHub 288c1cb757 fix: Respect app direction for incoming email content (#14011) 2026-04-14 13:45:34 +05:30
Sojan Jose 899fce1c92 Merge branch 'release/4.12.1'
Publish Chatwoot EE docker images / build (linux/amd64, ubuntu-latest) (push) Waiting to run
Publish Chatwoot EE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Waiting to run
Publish Chatwoot EE docker images / merge (push) Blocked by required conditions
Publish Chatwoot CE docker images / build (linux/amd64, ubuntu-latest) (push) Waiting to run
Publish Chatwoot CE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Waiting to run
Publish Chatwoot CE docker images / merge (push) Blocked by required conditions
2026-03-25 16:21:28 -07:00
Sojan Jose b6b856260f Merge branch 'release/4.12.0'
Publish Chatwoot EE docker images / build (linux/amd64, ubuntu-latest) (push) Waiting to run
Publish Chatwoot EE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Waiting to run
Publish Chatwoot EE docker images / merge (push) Blocked by required conditions
Publish Chatwoot CE docker images / build (linux/amd64, ubuntu-latest) (push) Waiting to run
Publish Chatwoot CE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Waiting to run
Publish Chatwoot CE docker images / merge (push) Blocked by required conditions
2026-03-17 16:23:05 -07:00
Shivam Mishra 79b18e7009 Merge branch 'hotfix/4.11.2' 2026-03-09 21:19:50 +05:30
658 changed files with 29509 additions and 489 deletions
+1 -1
View File
@@ -1 +1 @@
4.12.1
4.13.0
@@ -103,6 +103,7 @@ const showPagination = computed(() => {
<ContactsActiveFiltersPreview
v-if="showActiveFiltersPreview"
:active-segment="activeSegment"
class="mb-1"
@clear-filters="emit('clearFilters')"
@open-filter="openFilter"
/>
@@ -57,7 +57,7 @@ useKeyboardEvents(keyboardEvents);
<template>
<ButtonGroup
class="flex flex-col justify-center items-center absolute top-36 xl:top-24 ltr:right-2 rtl:left-2 bg-n-solid-2/90 backdrop-blur-lg border border-n-weak/50 rounded-full gap-1.5 p-1.5 shadow-sm transition-shadow duration-200 hover:shadow"
class="flex flex-col justify-center items-center absolute top-36 xl:top-24 ltr:right-2 rtl:left-2 bg-n-solid-2/90 backdrop-blur-lg border border-n-weak/50 rounded-full gap-1.5 p-1.5 shadow-sm transition-shadow duration-200 hover:shadow !z-20"
>
<Button
v-tooltip.top="$t('CONVERSATION.SIDEBAR.CONTACT')"
@@ -1,5 +1,5 @@
<script setup>
import { computed, ref } from 'vue';
import { ref, computed, watch } from 'vue';
import { debounce } from '@chatwoot/utils';
import { useI18n } from 'vue-i18n';
import { ARTICLE_EDITOR_MENU_OPTIONS } from 'dashboard/constants/editor';
@@ -27,7 +27,6 @@ const props = defineProps({
const emit = defineEmits([
'saveArticle',
'saveArticleAsync',
'goBack',
'setAuthor',
'setCategory',
@@ -39,42 +38,37 @@ const { t } = useI18n();
const isNewArticle = computed(() => !props.article?.id);
const saveAndSync = value => {
emit('saveArticle', value);
};
const localTitle = ref(props.article?.title ?? '');
const localContent = ref(props.article?.content ?? '');
// this will only send the data to the backend
// but will not update the local state preventing unnecessary re-renders
// since the data is already saved and we keep the editor text as the source of truth
const quickSave = debounce(
value => emit('saveArticleAsync', value),
400,
false
// Sync local state when navigating to a different article or on initial fetch
watch(
() => props.article?.id,
newId => {
if (newId) {
localTitle.value = props.article?.title ?? '';
localContent.value = props.article?.content ?? '';
}
}
);
// 2.5 seconds is enough to know that the user has stopped typing and is taking a pause
// so we can save the data to the backend and retrieve the updated data
// this will update the local state with response data
// Only use to save for existing articles
const saveAndSyncDebounced = debounce(saveAndSync, 2500, false);
const debouncedSave = debounce(value => emit('saveArticle', value), 500, false);
const handleSave = value => {
if (isNewArticle.value) return;
quickSave(value);
saveAndSyncDebounced(value);
debouncedSave(value);
};
const articleTitle = computed({
get: () => props.article.title,
get: () => localTitle.value,
set: value => {
localTitle.value = value;
handleSave({ title: value });
},
});
const localContent = ref(props.article.content || '');
const articleContent = computed({
get: () => props.article.content,
get: () => localContent.value,
set: content => {
localContent.value = content;
handleSave({ content });
@@ -132,7 +126,7 @@ const handleCreateArticle = event => {
/>
<ArticleEditorControls
:article="article"
@save-article="saveAndSync"
@save-article="values => emit('saveArticle', values)"
@set-author="setAuthorId"
@set-category="setCategoryId"
/>
@@ -167,8 +161,12 @@ const handleCreateArticle = event => {
}
.editor-root .has-selection {
.ProseMirror-menubar:not(:has(*)) {
display: none !important;
}
.ProseMirror-menubar {
@apply h-8 rounded-lg !px-2 z-50 bg-n-solid-3 items-center gap-4 ml-0 mb-0 shadow-md outline outline-1 outline-n-weak;
@apply rounded-lg !px-3 !py-1.5 z-50 bg-n-background items-center gap-4 ml-0 mb-0 shadow-md outline outline-1 outline-n-weak;
display: flex;
top: var(--selection-top, auto) !important;
left: var(--selection-left, 0) !important;
@@ -176,15 +174,10 @@ const handleCreateArticle = event => {
position: absolute !important;
.ProseMirror-menuitem {
@apply mr-0;
@apply ltr:mr-0 rtl:ml-0 size-4 flex items-center;
.ProseMirror-icon {
@apply p-0 mt-0 !mr-0;
svg {
width: 20px !important;
height: 20px !important;
}
@apply p-0.5 flex-shrink-0 ltr:mr-2 rtl:ml-2;
}
}
@@ -1,5 +1,5 @@
<script setup>
import { computed } from 'vue';
import { computed, useSlots } from 'vue';
import Checkbox from 'dashboard/components-next/checkbox/Checkbox.vue';
import Button from 'dashboard/components-next/button/Button.vue';
@@ -30,23 +30,40 @@ const modelValue = defineModel({
});
const selectedCount = computed(() => modelValue.value.size);
const totalCount = computed(() => props.allItems.length);
const visibleItemIds = computed(() => props.allItems.map(item => item.id));
const visibleItemCount = computed(() => visibleItemIds.value.length);
const selectedVisibleCount = computed(
() => visibleItemIds.value.filter(id => modelValue.value.has(id)).length
);
const hasSelected = computed(() => selectedCount.value > 0);
const isIndeterminate = computed(
() => hasSelected.value && selectedCount.value < totalCount.value
() =>
selectedVisibleCount.value > 0 &&
selectedVisibleCount.value < visibleItemCount.value
);
const allSelected = computed(
() => totalCount.value > 0 && selectedCount.value === totalCount.value
() =>
visibleItemCount.value > 0 &&
selectedVisibleCount.value === visibleItemCount.value
);
const slots = useSlots();
const hasSecondaryActions = computed(() => Boolean(slots['secondary-actions']));
const bulkCheckboxState = computed({
get: () => allSelected.value,
set: shouldSelectAll => {
const newSelectedIds = shouldSelectAll
? new Set(props.allItems.map(item => item.id))
: new Set();
modelValue.value = newSelectedIds;
if (!visibleItemCount.value) {
return;
}
const updatedSelection = new Set(modelValue.value);
if (shouldSelectAll) {
visibleItemIds.value.forEach(id => updatedSelection.add(id));
} else {
visibleItemIds.value.forEach(id => updatedSelection.delete(id));
}
modelValue.value = updatedSelection;
},
});
</script>
@@ -63,7 +80,7 @@ const bulkCheckboxState = computed({
v-if="hasSelected"
class="flex items-center gap-3 py-1 ltr:pl-3 rtl:pr-3 ltr:pr-4 rtl:pl-4 rounded-lg bg-n-solid-2 outline outline-1 outline-n-container shadow"
>
<div class="flex items-center gap-3">
<div class="flex items-center gap-3 min-w-0">
<div class="flex items-center gap-1.5 min-w-0">
<Checkbox
v-model="bulkCheckboxState"
@@ -78,21 +95,23 @@ const bulkCheckboxState = computed({
<span class="text-sm text-n-slate-10 truncate tabular-nums">
{{ selectedCountLabel }}
</span>
<div class="h-4 w-px bg-n-strong" />
<slot name="secondary-actions" />
</div>
<div class="flex items-center gap-3">
<slot name="actions" :selected-count="selectedCount">
<Button
:label="deleteLabel"
sm
ruby
ghost
class="!px-1.5"
icon="i-lucide-trash"
@click="emit('bulkDelete')"
/>
</slot>
<slot v-if="hasSecondaryActions" name="secondary-actions" />
<div v-if="hasSecondaryActions" class="h-4 w-px bg-n-strong" />
<div class="flex items-center gap-3">
<slot name="actions" :selected-count="selectedCount">
<Button
:label="deleteLabel"
sm
ruby
ghost
class="!px-1.5"
icon="i-lucide-trash"
@click="emit('bulkDelete')"
/>
</slot>
</div>
</div>
</div>
<div v-else class="flex items-center gap-3">
@@ -226,4 +226,10 @@ const handleSeeOriginal = () => {
}
}
}
// Email clients (Gmail, Outlook) hardcode dir="ltr" on wrapper elements.
// In RTL apps this forces email content LTR regardless of actual text.
[dir='rtl'] .letter-render [dir='ltr'] {
direction: inherit;
}
</style>
@@ -250,6 +250,12 @@ const menuItems = computed(() => {
activeOn: ['conversation_through_mentions'],
to: accountScopedRoute('conversation_mentions'),
},
{
name: 'Participating',
label: t('SIDEBAR.PARTICIPATING_CONVERSATIONS'),
activeOn: ['conversation_through_participating'],
to: accountScopedRoute('conversation_participating'),
},
{
name: 'Unattended',
activeOn: ['conversation_through_unattended'],
@@ -158,9 +158,11 @@ const activeChild = computed(() => {
return rankedPage ?? activeOnPages[0];
}
return navigableChildren.value.find(
child => child.to && route.path.startsWith(resolvePath(child.to))
);
return navigableChildren.value.find(child => {
if (!child.to) return false;
const childPath = resolvePath(child.to);
return route.path === childPath || route.path.startsWith(`${childPath}/`);
});
});
const hasActiveChild = computed(() => {
@@ -56,6 +56,7 @@ import { generateValuesForEditCustomViews } from 'dashboard/helper/customViewsHe
import { conversationListPageURL } from '../helper/URLHelper';
import {
isOnMentionsView,
isOnParticipatingView,
isOnUnattendedView,
} from '../store/modules/conversations/helpers/actionHelpers';
import {
@@ -113,6 +114,7 @@ const chatLists = useMapGetter('getFilteredConversations');
const mineChatsList = useMapGetter('getMineChats');
const allChatList = useMapGetter('getAllStatusChats');
const unAssignedChatsList = useMapGetter('getUnAssignedChats');
const participatingChatsList = useMapGetter('getParticipatingChats');
const chatListLoading = useMapGetter('getChatListLoadingStatus');
const activeInbox = useMapGetter('getSelectedInbox');
const conversationStats = useMapGetter('conversationStats/getStats');
@@ -296,13 +298,15 @@ const pageTitle = computed(() => {
if (props.label) {
return `#${props.label}`;
}
if (props.conversationType === 'mention') {
if (props.conversationType === wootConstants.CONVERSATION_TYPE.MENTION) {
return t('CHAT_LIST.MENTION_HEADING');
}
if (props.conversationType === 'participating') {
if (
props.conversationType === wootConstants.CONVERSATION_TYPE.PARTICIPATING
) {
return t('CONVERSATION_PARTICIPANTS.SIDEBAR_MENU_TITLE');
}
if (props.conversationType === 'unattended') {
if (props.conversationType === wootConstants.CONVERSATION_TYPE.UNATTENDED) {
return t('CHAT_LIST.UNATTENDED_HEADING');
}
if (hasActiveFolders.value) {
@@ -311,12 +315,30 @@ const pageTitle = computed(() => {
return t('CHAT_LIST.TAB_HEADING');
});
function filterByAssigneeTab(conversations) {
if (activeAssigneeTab.value === wootConstants.ASSIGNEE_TYPE.ME) {
return conversations.filter(
c => c.meta?.assignee?.id === currentUser.value?.id
);
}
if (activeAssigneeTab.value === wootConstants.ASSIGNEE_TYPE.UNASSIGNED) {
return conversations.filter(c => !c.meta?.assignee);
}
return [...conversations];
}
const conversationList = computed(() => {
let localConversationList = [];
if (!hasAppliedFiltersOrActiveFolders.value) {
const filters = conversationFilters.value;
if (activeAssigneeTab.value === 'me') {
if (
props.conversationType === wootConstants.CONVERSATION_TYPE.PARTICIPATING
) {
localConversationList = filterByAssigneeTab(
participatingChatsList.value(filters)
);
} else if (activeAssigneeTab.value === 'me') {
localConversationList = [...mineChatsList.value(filters)];
} else if (activeAssigneeTab.value === 'unassigned') {
localConversationList = [...unAssignedChatsList.value(filters)];
@@ -637,9 +659,11 @@ function redirectToConversationList() {
let conversationType = '';
if (isOnMentionsView({ route: { name } })) {
conversationType = 'mention';
conversationType = wootConstants.CONVERSATION_TYPE.MENTION;
} else if (isOnParticipatingView({ route: { name } })) {
conversationType = wootConstants.CONVERSATION_TYPE.PARTICIPATING;
} else if (isOnUnattendedView({ route: { name } })) {
conversationType = 'unattended';
conversationType = wootConstants.CONVERSATION_TYPE.UNATTENDED;
}
router.push(
conversationListPageURL({
@@ -27,10 +27,6 @@ const props = defineProps({
type: Boolean,
default: true,
},
isPopout: {
type: Boolean,
default: false,
},
});
const emit = defineEmits([
@@ -208,10 +204,7 @@ onMounted(() => {
<template>
<div class="space-y-2 mb-4">
<div
class="overflow-y-auto"
:class="{ 'max-h-96': isPopout, 'max-h-56': !isPopout }"
>
<div class="overflow-y-auto max-h-56">
<p
v-dompurify-html="formatMessage(generatedContent, false)"
class="text-n-iris-12 text-sm prose-sm font-normal !mb-4"
@@ -984,7 +984,32 @@ useEmitter(BUS_EVENTS.INSERT_INTO_RICH_EDITOR, insertContentIntoEditor);
}
.ProseMirror-woot-style {
@apply overflow-auto min-h-[5rem] max-h-[7.5rem];
@apply overflow-auto;
}
.ProseMirror-woot-style:not(
:where(.resizable-editor-wrapper .ProseMirror-woot-style)
) {
@apply min-h-[5rem] max-h-[7.5rem];
}
// Resizable editor wrapper styles
.resizable-editor-wrapper {
.ProseMirror-woot-style {
min-height: clamp(
var(--editor-min-allowed, var(--editor-min-height, 5rem)),
var(--editor-height, var(--editor-min-height, 5rem)),
var(--editor-max-allowed, var(--editor-max-height, 7.5rem))
);
max-height: clamp(
var(--editor-min-allowed, var(--editor-min-height, 5rem)),
var(--editor-height, var(--editor-min-height, 5rem)),
var(--editor-max-allowed, var(--editor-max-height, 7.5rem))
);
transition:
min-height var(--editor-height-transition, 180ms ease),
max-height var(--editor-height-transition, 180ms ease);
}
}
.ProseMirror-prompt-backdrop::backdrop {
@@ -8,13 +8,22 @@ import {
EditorState,
Selection,
} from '@chatwoot/prosemirror-schema';
import {
suggestionsPlugin,
triggerCharacters,
} from '@chatwoot/prosemirror-schema/src/mentions/plugin';
import imagePastePlugin from '@chatwoot/prosemirror-schema/src/plugins/image';
import { toggleMark } from 'prosemirror-commands';
import { wrapInList } from 'prosemirror-schema-list';
import { toggleBlockType } from '@chatwoot/prosemirror-schema/src/menu/common';
import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
import { useAlert } from 'dashboard/composables';
import { useUISettings } from 'dashboard/composables/useUISettings';
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
import SlashCommandMenu from './SlashCommandMenu.vue';
const MAXIMUM_FILE_UPLOAD_SIZE = 4; // in MB
const SLASH_MENU_OFFSET = 4;
const createState = (
content,
placeholder,
@@ -40,6 +49,7 @@ let editorView = null;
let state;
export default {
components: { SlashCommandMenu },
mixins: [keyboardEventListenerMixins],
props: {
modelValue: { type: String, default: '' },
@@ -62,8 +72,15 @@ export default {
},
data() {
return {
plugins: [imagePastePlugin(this.handleImageUpload)],
plugins: [
imagePastePlugin(this.handleImageUpload),
this.createSlashPlugin(),
],
isTextSelected: false, // Tracks text selection and prevents unnecessary re-renders on mouse selection
showSlashMenu: false,
slashSearchTerm: '',
slashRange: null,
slashMenuPosition: null,
};
},
watch: {
@@ -95,6 +112,126 @@ export default {
}
},
methods: {
createSlashPlugin() {
return suggestionsPlugin({
matcher: triggerCharacters('/', 0),
suggestionClass: '',
onEnter: args => {
this.showSlashMenu = true;
this.slashRange = args.range;
this.slashSearchTerm = args.text || '';
this.updateSlashMenuPosition(args.range.from);
return false;
},
onChange: args => {
this.slashRange = args.range;
this.slashSearchTerm = args.text;
return false;
},
onExit: () => {
this.slashSearchTerm = '';
this.showSlashMenu = false;
this.slashMenuPosition = null;
return false;
},
onKeyDown: ({ event }) => {
return (
event.keyCode === 13 &&
this.showSlashMenu &&
this.$refs.slashMenu?.hasItems
);
},
});
},
updateSlashMenuPosition(pos) {
if (!editorView) return;
const coords = editorView.coordsAtPos(pos);
const editorRect = this.$refs.editor.getBoundingClientRect();
const isRtl = getComputedStyle(this.$refs.editor).direction === 'rtl';
this.slashMenuPosition = {
top: coords.bottom - editorRect.top + SLASH_MENU_OFFSET,
...(isRtl
? { right: editorRect.right - coords.right }
: { left: coords.left - editorRect.left }),
};
},
removeSlashTriggerText() {
if (!editorView || !this.slashRange) return;
const { from, to } = this.slashRange;
editorView.dispatch(editorView.state.tr.delete(from, to));
state = editorView.state;
},
executeSlashCommand(actionKey) {
if (!editorView) return;
this.removeSlashTriggerText();
const { schema } = editorView.state;
const commandMap = {
strong: () =>
toggleMark(schema.marks.strong)(
editorView.state,
editorView.dispatch
),
em: () =>
toggleMark(schema.marks.em)(editorView.state, editorView.dispatch),
strike: () =>
toggleMark(schema.marks.strike)(
editorView.state,
editorView.dispatch
),
code: () =>
toggleMark(schema.marks.code)(editorView.state, editorView.dispatch),
h1: () =>
toggleBlockType(schema.nodes.heading, { level: 1 })(
editorView.state,
editorView.dispatch
),
h2: () =>
toggleBlockType(schema.nodes.heading, { level: 2 })(
editorView.state,
editorView.dispatch
),
h3: () =>
toggleBlockType(schema.nodes.heading, { level: 3 })(
editorView.state,
editorView.dispatch
),
bulletList: () =>
wrapInList(schema.nodes.bullet_list)(
editorView.state,
editorView.dispatch
),
orderedList: () =>
wrapInList(schema.nodes.ordered_list)(
editorView.state,
editorView.dispatch
),
insertTable: () => {
const { table, table_row, table_header, table_cell, paragraph } =
schema.nodes;
const headerCells = [0, 1, 2].map(() =>
table_header.createAndFill(null, paragraph.create())
);
const dataCells = [0, 1, 2].map(() =>
table_cell.createAndFill(null, paragraph.create())
);
const headerRow = table_row.create(null, headerCells);
const dataRow = table_row.create(null, dataCells);
const tableNode = table.create(null, [headerRow, dataRow]);
const tr = editorView.state.tr.replaceSelectionWith(tableNode);
editorView.dispatch(tr.scrollIntoView());
},
};
const command = commandMap[actionKey];
if (command) {
command();
state = editorView.state;
this.emitOnChange();
editorView.focus();
}
},
contentFromEditor() {
if (editorView) {
return ArticleMarkdownSerializer.serialize(editorView.state.doc);
@@ -262,7 +399,8 @@ export default {
// Get the editor's width
const editorWidth = editor.offsetWidth;
const menubarWidth = 480; // Menubar width (adjust as needed (px))
const menubar = editor.querySelector('.ProseMirror-menubar');
const menubarWidth = menubar ? menubar.scrollWidth : 480;
// Get the end position of the selection
const { bottom: endBottom, right: endRight } = editorView.coordsAtPos(to);
@@ -290,7 +428,15 @@ export default {
<template>
<div>
<div class="editor-root editor--article">
<div class="editor-root editor--article relative">
<SlashCommandMenu
v-if="showSlashMenu"
ref="slashMenu"
:search-key="slashSearchTerm"
:enabled-menu-options="enabledMenuOptions"
:position="slashMenuPosition"
@select-action="executeSlashCommand"
/>
<input
ref="imageUploadInput"
type="file"
@@ -54,7 +54,7 @@ export default {
default: undefined,
},
},
emits: ['setReplyMode', 'togglePopout', 'executeCopilotAction'],
emits: ['setReplyMode', 'toggleEditorSize', 'executeCopilotAction'],
setup(props, { emit }) {
const setReplyMode = mode => {
emit('setReplyMode', mode);
@@ -189,7 +189,7 @@ export default {
class="text-n-slate-11"
sm
icon="i-lucide-maximize-2"
@click="$emit('togglePopout')"
@click="$emit('toggleEditorSize')"
/>
</div>
</div>
@@ -0,0 +1,180 @@
<script setup>
import { ref, computed, watch, nextTick } from 'vue';
import { useI18n } from 'vue-i18n';
import { useKeyboardNavigableList } from 'dashboard/composables/useKeyboardNavigableList';
import Icon from 'dashboard/components-next/icon/Icon.vue';
const props = defineProps({
searchKey: {
type: String,
default: '',
},
enabledMenuOptions: {
type: Array,
default: () => [],
},
position: {
type: Object,
default: null,
},
});
const emit = defineEmits(['selectAction']);
const { t } = useI18n();
const EDITOR_ACTIONS = [
{
value: 'h1',
labelKey: 'SLASH_COMMANDS.HEADING_1',
icon: 'i-lucide-heading-1',
menuKey: 'h1',
},
{
value: 'h2',
labelKey: 'SLASH_COMMANDS.HEADING_2',
icon: 'i-lucide-heading-2',
menuKey: 'h2',
},
{
value: 'h3',
labelKey: 'SLASH_COMMANDS.HEADING_3',
icon: 'i-lucide-heading-3',
menuKey: 'h3',
},
{
value: 'strong',
labelKey: 'SLASH_COMMANDS.BOLD',
icon: 'i-lucide-bold',
menuKey: 'strong',
},
{
value: 'em',
labelKey: 'SLASH_COMMANDS.ITALIC',
icon: 'i-lucide-italic',
menuKey: 'em',
},
{
value: 'insertTable',
labelKey: 'SLASH_COMMANDS.TABLE',
icon: 'i-lucide-table',
menuKey: 'insertTable',
},
{
value: 'strike',
labelKey: 'SLASH_COMMANDS.STRIKETHROUGH',
icon: 'i-lucide-strikethrough',
menuKey: 'strike',
},
{
value: 'code',
labelKey: 'SLASH_COMMANDS.CODE',
icon: 'i-lucide-code',
menuKey: 'code',
},
{
value: 'bulletList',
labelKey: 'SLASH_COMMANDS.BULLET_LIST',
icon: 'i-lucide-list',
menuKey: 'bulletList',
},
{
value: 'orderedList',
labelKey: 'SLASH_COMMANDS.ORDERED_LIST',
icon: 'i-lucide-list-ordered',
menuKey: 'orderedList',
},
];
const listContainerRef = ref(null);
const selectedIndex = ref(0);
const items = computed(() => {
const search = props.searchKey.toLowerCase();
return EDITOR_ACTIONS.filter(action => {
if (!props.enabledMenuOptions.includes(action.menuKey)) return false;
if (!search) return true;
return t(action.labelKey).toLowerCase().includes(search);
});
});
const hasItems = computed(() => items.value.length > 0);
const menuStyle = computed(() => {
if (!props.position) return {};
const style = { top: `${props.position.top}px` };
if (props.position.right != null) {
style.right = `${props.position.right}px`;
} else {
style.left = `${props.position.left}px`;
}
return style;
});
const adjustScroll = () => {
nextTick(() => {
const container = listContainerRef.value;
if (!container) return;
const el = container.querySelector(`#slash-item-${selectedIndex.value}`);
if (el) {
el.scrollIntoView({ block: 'nearest', behavior: 'auto' });
}
});
};
const onSelect = () => {
const item = items.value[selectedIndex.value];
if (item) emit('selectAction', item.value);
};
useKeyboardNavigableList({
items,
onSelect,
adjustScroll,
selectedIndex,
});
// Reset selection when filtered items change
watch(items, () => {
selectedIndex.value = 0;
});
const onHover = index => {
selectedIndex.value = index;
};
const onItemClick = index => {
selectedIndex.value = index;
onSelect();
};
defineExpose({ hasItems });
</script>
<!-- eslint-disable-next-line vue/no-root-v-if -->
<template>
<div
v-if="hasItems"
ref="listContainerRef"
class="bg-n-alpha-3 backdrop-blur-[100px] outline outline-1 outline-n-container absolute rounded-xl z-50 flex flex-col min-w-[10rem] shadow-lg p-2 overflow-auto max-h-[15rem]"
:style="menuStyle"
>
<button
v-for="(item, index) in items"
:id="`slash-item-${index}`"
:key="item.value"
type="button"
class="inline-flex items-center justify-start w-full h-8 min-w-0 gap-2 px-2 py-1.5 border-0 rounded-lg text-n-slate-12 hover:bg-n-alpha-1 dark:hover:bg-n-alpha-2"
:class="{
'bg-n-alpha-1 dark:bg-n-alpha-2': index === selectedIndex,
}"
@mouseover="onHover(index)"
@click="onItemClick(index)"
>
<Icon :icon="item.icon" class="flex-shrink-0 size-3.5" />
<span class="min-w-0 text-sm truncate">
{{ t(item.labelKey) }}
</span>
</button>
</div>
</template>
@@ -45,6 +45,7 @@ const backButtonUrl = computed(() => {
const conversationTypeMap = {
conversation_through_mentions: 'mention',
conversation_through_participating: 'participating',
conversation_through_unattended: 'unattended',
};
return conversationListPageURL({
@@ -16,10 +16,6 @@ defineProps({
type: String,
default: '',
},
isPopout: {
type: Boolean,
default: false,
},
});
const emit = defineEmits([
@@ -69,7 +65,6 @@ const onSend = () => {
:generated-content="generatedContent"
:min-height="4"
:enabled-menu-options="[]"
:is-popout="isPopout"
@focus="onFocus"
@blur="onBlur"
@clear-selection="clearEditorSelection"
@@ -1,7 +1,7 @@
<script>
import { ref, provide } from 'vue';
import { ref, provide, useTemplateRef } from 'vue';
import { useElementSize } from '@vueuse/core';
// composable
import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
import { useLabelSuggestions } from 'dashboard/composables/useLabelSuggestions';
import { useSnakeCase } from 'dashboard/composables/useTransformKeys';
@@ -11,6 +11,7 @@ import MessageList from 'next/message/MessageList.vue';
import ConversationLabelSuggestion from './conversation/LabelSuggestion.vue';
import Banner from 'dashboard/components/ui/Banner.vue';
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
import ResizableEditorWrapper from './ResizableEditorWrapper.vue';
// stores and apis
import { mapGetters } from 'vuex';
@@ -43,21 +44,16 @@ export default {
Banner,
ConversationLabelSuggestion,
Spinner,
ResizableEditorWrapper,
},
mixins: [inboxMixin],
setup() {
const isPopOutReplyBox = ref(false);
const conversationPanelRef = ref(null);
const keyboardEvents = {
Escape: {
action: () => {
isPopOutReplyBox.value = false;
},
},
};
useKeyboardEvents(keyboardEvents);
const resizableEditorWrapperRef = ref(null);
const messagesViewRef = useTemplateRef('messagesViewRef');
const topBannerRef = useTemplateRef('topBannerRef');
const { height: containerHeight } = useElementSize(messagesViewRef);
const { height: topBannerHeight } = useElementSize(topBannerRef);
const {
captainTasksEnabled,
@@ -68,11 +64,15 @@ export default {
provide('contextMenuElementTarget', conversationPanelRef);
return {
isPopOutReplyBox,
captainTasksEnabled,
getLabelSuggestions,
isLabelSuggestionFeatureEnabled,
conversationPanelRef,
resizableEditorWrapperRef,
messagesViewRef,
topBannerRef,
containerHeight,
topBannerHeight,
};
},
data() {
@@ -254,6 +254,7 @@ export default {
this.fetchAllAttachmentsFromCurrentChat();
this.fetchSuggestions();
this.messageSentSinceOpened = false;
this.resetReplyEditorHeight();
},
},
@@ -437,26 +438,37 @@ export default {
const payload = useSnakeCase(message);
await this.$store.dispatch('sendMessageWithData', payload);
},
toggleReplyEditorSize() {
this.resizableEditorWrapperRef?.toggleEditorExpand?.();
},
resetReplyEditorHeight() {
this.resizableEditorWrapperRef?.resetEditorHeight?.();
},
},
};
</script>
<template>
<div class="flex flex-col justify-between flex-grow h-full min-w-0 m-0">
<Banner
v-if="!currentChat.can_reply"
color-scheme="alert"
class="mx-2 mt-2 overflow-hidden rounded-lg"
:banner-message="replyWindowBannerMessage"
:href-link="replyWindowLink"
:href-link-text="replyWindowLinkText"
/>
<Banner
v-else-if="hasDuplicateInstagramInbox"
color-scheme="alert"
class="mx-2 mt-2 overflow-hidden rounded-lg"
:banner-message="$t('CONVERSATION.OLD_INSTAGRAM_INBOX_REPLY_BANNER')"
/>
<div
ref="messagesViewRef"
class="flex flex-col justify-between flex-grow h-full min-w-0 m-0"
>
<div ref="topBannerRef">
<Banner
v-if="!currentChat.can_reply"
color-scheme="alert"
class="mx-2 mt-2 overflow-hidden rounded-lg"
:banner-message="replyWindowBannerMessage"
:href-link="replyWindowLink"
:href-link-text="replyWindowLinkText"
/>
<Banner
v-else-if="hasDuplicateInstagramInbox"
color-scheme="alert"
class="mx-2 mt-2 overflow-hidden rounded-lg"
:banner-message="$t('CONVERSATION.OLD_INSTAGRAM_INBOX_REPLY_BANNER')"
/>
</div>
<MessageList
ref="conversationPanelRef"
class="conversation-panel flex-shrink flex-grow basis-px flex flex-col overflow-y-auto relative h-full m-0 pb-4"
@@ -498,13 +510,7 @@ export default {
/>
</template>
</MessageList>
<div
class="flex relative flex-col"
:class="{
'modal-mask': isPopOutReplyBox,
'bg-n-surface-1': !isPopOutReplyBox,
}"
>
<div class="flex relative flex-col bg-n-surface-1">
<div
v-if="isAnyoneTyping"
class="absolute flex items-center w-full h-0 -top-7"
@@ -520,42 +526,12 @@ export default {
/>
</div>
</div>
<ReplyBox
:pop-out-reply-box="isPopOutReplyBox"
@update:pop-out-reply-box="isPopOutReplyBox = $event"
/>
<ResizableEditorWrapper
ref="resizableEditorWrapperRef"
:container-height="Math.max(0, containerHeight - topBannerHeight)"
>
<ReplyBox @toggle-editor-size="toggleReplyEditorSize" />
</ResizableEditorWrapper>
</div>
</div>
</template>
<style scoped lang="scss">
.modal-mask {
@apply fixed;
&::v-deep {
.ProseMirror-woot-style {
@apply max-h-[25rem];
}
.reply-box {
@apply border border-n-weak max-w-[75rem] w-[70%];
&.is-private {
@apply dark:border-n-amber-3/30 border-n-amber-12/5;
}
}
.reply-box .reply-box__top {
@apply relative min-h-[27.5rem];
}
.reply-box__top .input {
@apply min-h-[27.5rem];
}
.emoji-dialog {
@apply absolute ltr:left-auto rtl:right-auto bottom-1;
}
}
}
</style>
@@ -81,13 +81,7 @@ export default {
CopilotReplyBottomPanel,
},
mixins: [inboxMixin, fileUploadMixin, keyboardEventListenerMixins],
props: {
popOutReplyBox: {
type: Boolean,
default: false,
},
},
emits: ['update:popOutReplyBox'],
emits: ['toggleEditorSize'],
setup() {
const {
uiSettings,
@@ -797,7 +791,6 @@ export default {
this.clearMessage();
this.hideEmojiPicker();
this.$emit('update:popOutReplyBox', false);
}
},
sendMessageAsMultipleMessages(message, copilotAcceptedMessage = '') {
@@ -1217,8 +1210,9 @@ export default {
file => !file?.isRecordedAudio
);
},
togglePopout() {
this.$emit('update:popOutReplyBox', !this.popOutReplyBox);
toggleEditorSize() {
this.$emit('toggleEditorSize');
this.$nextTick(() => this.messageEditor?.focusEditorInputField());
},
onSubmitCopilotReply() {
const acceptedMessage = this.copilot.accept();
@@ -1244,9 +1238,8 @@ export default {
:is-message-length-reaching-threshold="isMessageLengthReachingThreshold"
:characters-remaining="charactersRemaining"
:editor-content="message"
:popout-reply-box="popOutReplyBox"
@set-reply-mode="setReplyMode"
@toggle-popout="togglePopout"
@toggle-editor-size="toggleEditorSize"
@toggle-copilot="copilot.toggleEditor"
@execute-copilot-action="executeCopilotAction"
/>
@@ -1275,7 +1268,7 @@ export default {
v-if="showEmojiPicker"
v-on-clickaway="hideEmojiPicker"
:class="{
'emoji-dialog--expanded': isOnExpandedLayout || popOutReplyBox,
'emoji-dialog--expanded': isOnExpandedLayout,
}"
:on-click="addIntoEditor"
/>
@@ -1299,7 +1292,6 @@ export default {
:show-copilot-editor="copilot.showEditor.value"
:is-generating-content="copilot.isGenerating.value"
:generated-content="copilot.generatedContent.value"
:is-popout="popOutReplyBox"
:placeholder="$t('CONVERSATION.FOOTER.COPILOT_MSG_INPUT')"
@focus="onFocus"
@blur="onBlur"
@@ -0,0 +1,154 @@
<script setup>
import { ref, computed, onMounted, onBeforeUnmount, useTemplateRef } from 'vue';
import { useEventListener } from '@vueuse/core';
import { emitter } from 'shared/helpers/mitt';
import { BUS_EVENTS } from 'shared/constants/busEvents';
const props = defineProps({
containerHeight: { type: Number, default: 0 },
});
const DEFAULT_HEIGHT = 120;
const MIN_HEIGHT = 80;
const MIN_MESSAGES_HEIGHT = 200;
const EXPAND_RATIO = 0.5;
const RESET_DELAY_MS = 120;
const wrapperRef = useTemplateRef('wrapperRef');
const surroundingHeight = ref(0);
const editorHeight = ref(DEFAULT_HEIGHT);
const isResizing = ref(false);
const startY = ref(0);
const startHeight = ref(0);
let resetTimeoutId = null;
const clamp = (val, min, max) => Math.min(Math.max(val, min), max);
// Measure height of elements surrounding the editor (top panel, email fields, bottom panel)
const measureSurroundingHeight = () => {
if (wrapperRef.value) {
surroundingHeight.value = Math.max(
0,
wrapperRef.value.offsetHeight - editorHeight.value
);
}
};
const isContainerReady = computed(() => props.containerHeight > 0);
const sizeBounds = computed(() => {
const h = props.containerHeight;
const s = surroundingHeight.value;
const max = Math.max(MIN_HEIGHT, h - MIN_MESSAGES_HEIGHT - s);
const expanded = clamp(Math.floor(h * EXPAND_RATIO - s / 2), MIN_HEIGHT, max);
return {
min: MIN_HEIGHT,
max: isContainerReady.value ? max : DEFAULT_HEIGHT,
expanded,
default: clamp(DEFAULT_HEIGHT, MIN_HEIGHT, max),
};
});
const clampToBounds = val =>
clamp(val, sizeBounds.value.min, sizeBounds.value.max);
const clearDragStyles = () => {
Object.assign(document.body.style, { cursor: '', userSelect: '' });
};
const getClientY = e => (e.touches ? e.touches[0].clientY : e.clientY);
const onResizeStart = event => {
editorHeight.value = clampToBounds(editorHeight.value);
measureSurroundingHeight();
isResizing.value = true;
startY.value = getClientY(event);
startHeight.value = clampToBounds(editorHeight.value);
editorHeight.value = startHeight.value;
Object.assign(document.body.style, {
cursor: 'row-resize',
userSelect: 'none',
});
};
const onResizeMove = event => {
if (!isResizing.value) return;
if (event.touches) event.preventDefault();
editorHeight.value = clampToBounds(
startHeight.value + startY.value - getClientY(event)
);
};
const onResizeEnd = () => {
if (!isResizing.value) return;
isResizing.value = false;
clearDragStyles();
};
const resetEditorHeight = () => {
editorHeight.value = sizeBounds.value.default;
};
const toggleEditorExpand = () => {
editorHeight.value = clampToBounds(editorHeight.value);
measureSurroundingHeight();
const { expanded, max, default: defaultHeight } = sizeBounds.value;
const isExpanded = editorHeight.value > defaultHeight;
// If expanded is too close to default, use max so the toggle is always noticeable
const target = expanded - defaultHeight < 100 ? max : expanded;
editorHeight.value = isExpanded ? defaultHeight : target;
};
const handleMessageSent = () => {
clearTimeout(resetTimeoutId);
resetTimeoutId = setTimeout(resetEditorHeight, RESET_DELAY_MS);
};
onMounted(() => {
emitter.on(BUS_EVENTS.MESSAGE_SENT, handleMessageSent);
});
onBeforeUnmount(() => {
emitter.off(BUS_EVENTS.MESSAGE_SENT, handleMessageSent);
clearTimeout(resetTimeoutId);
if (isResizing.value) {
isResizing.value = false;
clearDragStyles();
}
});
useEventListener(document, 'mousemove', onResizeMove);
useEventListener(document, 'mouseup', onResizeEnd);
useEventListener(document, 'touchmove', onResizeMove, { passive: false });
useEventListener(document, 'touchend', onResizeEnd);
useEventListener(document, 'touchcancel', onResizeEnd);
useEventListener(window, 'blur', onResizeEnd);
defineExpose({ toggleEditorExpand, resetEditorHeight });
</script>
<template>
<div
ref="wrapperRef"
class="relative resizable-editor-wrapper"
:style="{
'--editor-height': editorHeight + 'px',
'--editor-min-allowed': sizeBounds.min + 'px',
'--editor-max-allowed': sizeBounds.max + 'px',
'--editor-height-transition': isResizing ? 'none' : '180ms ease',
}"
>
<div
class="group absolute inset-x-0 -top-4 z-10 flex h-4 cursor-row-resize select-none items-center justify-center bg-gradient-to-b from-transparent from-10% dark:to-n-surface-1/80 to-n-surface-1/90 backdrop-blur-[0.01875rem]"
@mousedown="onResizeStart"
@touchstart.prevent="onResizeStart"
@dblclick="resetEditorHeight"
>
<div
class="w-8 h-0.5 mt-1 rounded-full bg-n-slate-6 group-hover:bg-n-slate-8 transition-all duration-200 motion-safe:group-hover:animate-bounce"
:class="{ 'bg-n-slate-8 animate-bounce': isResizing }"
/>
</div>
<slot />
</div>
</template>
@@ -92,7 +92,9 @@ describe('useAutomation', () => {
case 'assign_team':
return teams;
case 'assign_agent':
return agents;
return options.addNoneToListFn
? options.addNoneToListFn(options.agents)
: options.agents;
case 'send_email_to_team':
return teams;
case 'send_message':
@@ -240,7 +242,11 @@ describe('useAutomation', () => {
expect(getActionDropdownValues('add_label')).toEqual(labels);
expect(getActionDropdownValues('assign_team')).toEqual(teams);
expect(getActionDropdownValues('assign_agent')).toEqual(agents);
expect(getActionDropdownValues('assign_agent')).toEqual([
{ id: 'nil', name: 'AUTOMATION.NONE_OPTION' },
{ id: 'last_responding_agent', name: 'AUTOMATION.LAST_RESPONDING_AGENT' },
...agents,
]);
expect(getActionDropdownValues('send_email_to_team')).toEqual(teams);
expect(getActionDropdownValues('send_message')).toEqual([]);
expect(getActionDropdownValues('add_sla')).toEqual(slaPolicies);
@@ -16,7 +16,20 @@ describe('useEditableAutomation', () => {
return [];
}),
getActionDropdownValues: vi.fn(),
getActionDropdownValues: vi.fn(actionName => {
if (actionName === 'assign_agent') {
return [
{ id: 'nil', name: 'None' },
{
id: 'last_responding_agent',
name: 'Last Responding Agent',
},
{ id: 1, name: 'Agent 1' },
];
}
return [];
}),
});
});
@@ -51,4 +64,35 @@ describe('useEditableAutomation', () => {
},
]);
});
it('rehydrates last responding agent as a selected action option', () => {
const automation = {
event_name: 'conversation_created',
conditions: [],
actions: [
{
action_name: 'assign_agent',
action_params: ['last_responding_agent'],
},
],
};
const automationActionTypes = [
{ key: 'assign_agent', inputType: 'search_select' },
];
const { formatAutomation } = useEditableAutomation();
const result = formatAutomation(automation, [], {}, automationActionTypes);
expect(result.actions).toEqual([
{
action_name: 'assign_agent',
action_params: [
{
id: 'last_responding_agent',
name: 'Last Responding Agent',
},
],
},
]);
});
});
@@ -119,24 +119,30 @@ describe('useMacros', () => {
const { getMacroDropdownValues } = useMacros();
expect(getMacroDropdownValues('add_label')).toHaveLength(mockLabels.length);
expect(getMacroDropdownValues('assign_team')).toHaveLength(
mockTeams.length
);
mockTeams.length + 1
); // +1 for "None"
expect(getMacroDropdownValues('assign_agent')).toHaveLength(
mockAgents.length + 1
); // +1 for "Self"
mockAgents.length + 2
); // +2 for "None" and "Self"
});
it('returns teams for assign_team and send_email_to_team types', () => {
it('returns teams with "None" option for assign_team and teams only for send_email_to_team', () => {
const { getMacroDropdownValues } = useMacros();
expect(getMacroDropdownValues('assign_team')).toEqual(mockTeams);
const assignTeamResult = getMacroDropdownValues('assign_team');
expect(assignTeamResult[0]).toEqual({
id: 'nil',
name: 'AUTOMATION.NONE_OPTION',
});
expect(assignTeamResult.slice(1)).toEqual(mockTeams);
expect(getMacroDropdownValues('send_email_to_team')).toEqual(mockTeams);
});
it('returns agents with "Self" option for assign_agent type', () => {
it('returns agents with "None" and "Self" options for assign_agent type', () => {
const { getMacroDropdownValues } = useMacros();
const result = getMacroDropdownValues('assign_agent');
expect(result[0]).toEqual({ id: 'self', name: 'Self' });
expect(result.slice(1)).toEqual(mockAgents);
expect(result[0]).toEqual({ id: 'nil', name: 'AUTOMATION.NONE_OPTION' });
expect(result[1]).toEqual({ id: 'self', name: 'Self' });
expect(result.slice(2)).toEqual(mockAgents);
});
it('returns formatted labels for add_label and remove_label types', () => {
@@ -172,8 +178,11 @@ describe('useMacros', () => {
const { getMacroDropdownValues } = useMacros();
expect(getMacroDropdownValues('add_label')).toEqual([]);
expect(getMacroDropdownValues('assign_team')).toEqual([]);
expect(getMacroDropdownValues('assign_team')).toEqual([
{ id: 'nil', name: 'AUTOMATION.NONE_OPTION' },
]);
expect(getMacroDropdownValues('assign_agent')).toEqual([
{ id: 'nil', name: 'AUTOMATION.NONE_OPTION' },
{ id: 'self', name: 'Self' },
]);
});
@@ -121,8 +121,19 @@ export default function useAutomationValues() {
* @returns {Array} An array of action dropdown values.
*/
const getActionDropdownValues = type => {
let agentsList = agents.value;
if (type === 'assign_agent') {
agentsList = [
{
id: 'last_responding_agent',
name: t('AUTOMATION.LAST_RESPONDING_AGENT'),
},
...agentsList,
];
}
return getActionOptions({
agents: agents.value,
agents: agentsList,
labels: labels.value,
teams: teams.value,
slaPolicies: slaPolicies.value,
@@ -12,11 +12,14 @@ import { useKeyboardEvents } from './useKeyboardEvents';
/**
* Wrap the action in a function that calls the action and prevents the default event behavior.
* Only prevents default when items are available to navigate.
* @param {Function} action - The action to be called.
* @param {import('vue').Ref<Array>} items - A ref to the array of selectable items.
* @returns {{action: Function, allowOnFocusedInput: boolean}} An object containing the action and a flag to allow the event on focused input.
*/
const createAction = action => ({
const createAction = (action, items) => ({
action: e => {
if (!items.value?.length) return;
action();
e.preventDefault();
},
@@ -38,15 +41,14 @@ const createKeyboardEvents = (
items
) => {
const events = {
ArrowUp: createAction(moveSelectionUp),
'Control+KeyP': createAction(moveSelectionUp),
ArrowDown: createAction(moveSelectionDown),
'Control+KeyN': createAction(moveSelectionDown),
ArrowUp: createAction(moveSelectionUp, items),
'Control+KeyP': createAction(moveSelectionUp, items),
ArrowDown: createAction(moveSelectionDown, items),
'Control+KeyN': createAction(moveSelectionDown, items),
};
// Adds an event handler for the Enter key if the onSelect function is provided.
if (typeof onSelect === 'function') {
events.Enter = createAction(() => items.value?.length > 0 && onSelect());
events.Enter = createAction(onSelect, items);
}
return events;
@@ -15,6 +15,11 @@ export const useMacros = () => {
const teams = computed(() => getters['teams/getTeams'].value);
const agents = computed(() => getters['agents/getVerifiedAgents'].value);
const withNoneOption = options => [
{ id: 'nil', name: t('AUTOMATION.NONE_OPTION') },
...(options || []),
];
/**
* Get dropdown values based on the specified type
* @param {string} type - The type of dropdown values to retrieve
@@ -23,10 +28,15 @@ export const useMacros = () => {
const getMacroDropdownValues = type => {
switch (type) {
case 'assign_team':
return withNoneOption(teams.value);
case 'send_email_to_team':
return teams.value;
case 'assign_agent':
return [{ id: 'self', name: 'Self' }, ...agents.value];
return [
...withNoneOption(),
{ id: 'self', name: 'Self' },
...agents.value,
];
case 'add_label':
case 'remove_label':
return labels.value.map(i => ({
@@ -172,6 +172,7 @@ export const FORMATTING = {
export const ARTICLE_EDITOR_MENU_OPTIONS = [
'strong',
'em',
'strike',
'link',
'undo',
'redo',
@@ -182,6 +183,7 @@ export const ARTICLE_EDITOR_MENU_OPTIONS = [
'h3',
'imageUpload',
'code',
'insertTable',
];
/**
@@ -12,6 +12,11 @@ export default {
SNOOZED: 'snoozed',
ALL: 'all',
},
CONVERSATION_TYPE: {
MENTION: 'mention',
PARTICIPATING: 'participating',
UNATTENDED: 'unattended',
},
SORT_BY_TYPE: {
LAST_ACTIVITY_AT_ASC: 'last_activity_at_asc',
LAST_ACTIVITY_AT_DESC: 'last_activity_at_desc',
@@ -51,6 +51,7 @@ export const conversationListPageURL = ({
} else if (conversationType) {
const urlMap = {
mention: 'mentions/conversations',
participating: 'participating/conversations',
unattended: 'unattended/conversations',
};
url = `accounts/${accountId}/${urlMap[conversationType]}`;
@@ -40,6 +40,15 @@ describe('#URL Helpers', () => {
'/app/accounts/1/custom_view/1'
);
});
it('should return url to participating conversations', () => {
expect(
conversationListPageURL({
accountId: 1,
conversationType: 'participating',
})
).toBe('/app/accounts/1/participating/conversations');
});
});
describe('conversationUrl', () => {
it('should return direct conversation URL if activeInbox is nil', () => {
@@ -45,6 +45,10 @@ describe('#resolveTeamIds', () => {
const resolvedTeams = '⚙️ sales team, 🤷‍♂️ fayaz';
expect(resolveTeamIds(teams, [1, 2])).toEqual(resolvedTeams);
});
it('resolves nil as None', () => {
expect(resolveTeamIds(teams, ['nil'])).toEqual('None');
});
});
describe('#resolveLabels', () => {
@@ -59,6 +63,10 @@ describe('#resolveAgents', () => {
const resolvedAgents = 'John Doe';
expect(resolveAgents(agents, [1])).toEqual(resolvedAgents);
});
it('resolves nil and self values', () => {
expect(resolveAgents(agents, ['nil', 'self'])).toEqual('None, Self');
});
});
describe('#getFileName', () => {
@@ -125,6 +125,7 @@ const validateSingleAction = action => {
'mute_conversation',
'snooze_conversation',
'resolve_conversation',
'remove_assigned_agent',
'remove_assigned_team',
'open_conversation',
'pending_conversation',
@@ -63,6 +63,16 @@
"ERROR_MESSAGE": "Could not update bot. Please try again."
}
},
"SECRET": {
"LABEL": "Webhook Secret",
"COPY": "ምስጢሩን ወደ ክሊፕቦርድ ቅዳ",
"COPY_SUCCESS": "ምስጢሩ ወደ ክሊፕቦርድ ተቀድሷል",
"TOGGLE": "የምስጢሩን ማየት አሳይ/ደብቅ",
"CREATED_DESC": "Use the secret below to verify webhook signatures. Please copy it now, you can also find it later in the bot settings.",
"DONE": "ተጠናቀቀ",
"RESET_SUCCESS": "Webhook secret regenerated successfully",
"RESET_ERROR": "Unable to regenerate webhook secret. Please try again"
},
"ACCESS_TOKEN": {
"TITLE": "Access Token",
"DESCRIPTION": "Copy the access token and save it securely",
@@ -140,6 +140,8 @@
"ACTIONS": {
"ASSIGN_AGENT": "Assign to Agent",
"ASSIGN_TEAM": "Assign a Team",
"REMOVE_ASSIGNED_AGENT": "Remove Assigned Agent",
"REMOVE_ASSIGNED_TEAM": "Remove Assigned Team",
"ADD_LABEL": "Add a Label",
"REMOVE_LABEL": "Remove a Label",
"SEND_EMAIL_TO_TEAM": "Send an Email to Team",
@@ -169,6 +171,7 @@
},
"ATTRIBUTES": {
"MESSAGE_TYPE": "Message Type",
"PRIVATE_NOTE": "የግል ማስታወሻ",
"MESSAGE_CONTAINS": "Message Contains",
"EMAIL": "Email",
"INBOX": "Inbox",
@@ -52,5 +52,17 @@
},
"CHANNEL_SELECTOR": {
"COMING_SOON": "Coming Soon!"
},
"SLASH_COMMANDS": {
"HEADING_1": "Heading 1",
"HEADING_2": "Heading 2",
"HEADING_3": "Heading 3",
"BOLD": "Bold",
"ITALIC": "Italic",
"STRIKETHROUGH": "Strikethrough",
"CODE": "Code",
"BULLET_LIST": "Bullet List",
"ORDERED_LIST": "Ordered List",
"TABLE": "Table"
}
}
@@ -20,6 +20,7 @@
"CALL": "ደውል",
"CALL_INITIATED": "ለእውነተኛው እውቀት መደወል እየተከናወነ ነው…",
"CALL_FAILED": "ጥሪውን መጀመር አልቻልንም። እባክዎ ደግመው ይሞክሩ።.",
"CLICK_TO_EDIT": "Click to edit",
"VOICE_INBOX_PICKER": {
"TITLE": "የድምፅ ኢንቦክስ ይምረጡ"
},
@@ -86,6 +86,14 @@
"PLACEHOLDER": "እባክዎ የWebhook URLዎን ያስገቡ",
"ERROR": "እባክዎ ትክክለኛ አድራሻ ያስገቡ"
},
"CHANNEL_WEBHOOK_SECRET": {
"LABEL": "Webhook Secret",
"COPY": "ምስጢሩን ወደ ክሊፕቦርድ ቅዳ",
"COPY_SUCCESS": "ምስጢሩ ወደ ክሊፕቦርድ ተቀድሷል",
"TOGGLE": "የምስጢሩን ማየት አሳይ/ደብቅ",
"RESET_SUCCESS": "Webhook secret regenerated successfully",
"RESET_ERROR": "Unable to regenerate webhook secret. Please try again"
},
"CHANNEL_DOMAIN": {
"LABEL": "የድር ጣቢያ ዶሜን",
"PLACEHOLDER": "የድር ጣቢያዎን ዶሜን ያስገቡ (ለምሳሌ፡ acme.com)"
@@ -837,6 +837,18 @@
"SUCCESS_MESSAGE": "ብልጽግና ተጠቃሚ መሣሪያ ተሰርዟል",
"ERROR_MESSAGE": "ብልጽግና መሣሪያውን ማስወገድ አልተሳካም"
},
"PAYWALL": {
"TITLE": "Upgrade to use tools with Captain",
"AVAILABLE_ON": "Captain Tools are only available in Business and Enterprise plans. Please upgrade to Business plan to use the feature.",
"UPGRADE_PROMPT": "",
"UPGRADE_NOW": "Open billing",
"CANCEL_ANYTIME": ""
},
"ENTERPRISE_PAYWALL": {
"AVAILABLE_ON": "Captain Tools are only available in the paid plans.",
"UPGRADE_PROMPT": "Please upgrade to a paid plan to use this feature.",
"ASK_ADMIN": "Please reach out to your administrator for the upgrade."
},
"TEST": {
"BUTTON": "Test connection",
"SUCCESS": "Endpoint returned HTTP {status}",
@@ -93,6 +93,7 @@
"ASSIGN_AGENT": "Assign an Agent",
"ADD_LABEL": "Add a Label",
"REMOVE_LABEL": "Remove a Label",
"REMOVE_ASSIGNED_AGENT": "Remove Assigned Agent",
"REMOVE_ASSIGNED_TEAM": "Remove Assigned Team",
"SEND_EMAIL_TRANSCRIPT": "Send an Email Transcript",
"MUTE_CONVERSATION": "Mute Conversation",
@@ -68,7 +68,8 @@
"API_SUCCESS": "ፊርማው በተሳካ ሁኔታ ተቀምጧል",
"IMAGE_UPLOAD_ERROR": "ምስሉን ማስገባት አልተቻለም! እባክዎ እንደገና ይሞክሩ",
"IMAGE_UPLOAD_SUCCESS": "ምስሉ በተሳካ ሁኔታ ታክሏል። እባክዎ ማስቀመጫ ለማስቀመጥ ላይ ይጫኑ",
"IMAGE_UPLOAD_SIZE_ERROR": "የምስል መጠን ከ{size}MB በታች መሆን አለበት"
"IMAGE_UPLOAD_SIZE_ERROR": "የምስል መጠን ከ{size}MB በታች መሆን አለበት",
"INLINE_IMAGE_WARNING": "Pasted inline images were removed. Please use the image upload button to add images to your signature."
},
"MESSAGE_SIGNATURE": {
"LABEL": "የመልእክት ፊርማ",
@@ -45,6 +45,13 @@
"ERROR_MESSAGE": "Could not connect to Woot server. Please try again."
},
"SUBMIT": "Create account",
"HAVE_AN_ACCOUNT": "Already have an account?"
"HAVE_AN_ACCOUNT": "Already have an account?",
"VERIFY_EMAIL": {
"TITLE": "Check your inbox",
"DESCRIPTION": "We sent a verification link to {email}. Click the link to verify your email and get started.",
"RESEND": "የማረጋገጫ ኢሜል እንደገና ላክ",
"RESEND_SUCCESS": "Verification email sent. Please check your inbox.",
"RESEND_ERROR": "Could not send verification email. Please try again."
}
}
}
@@ -63,6 +63,16 @@
"ERROR_MESSAGE": "تعذر تحديث الروبوت. يرجى المحاولة مرة أخرى."
}
},
"SECRET": {
"LABEL": "Webhook Secret",
"COPY": "Copy secret to clipboard",
"COPY_SUCCESS": "Secret copied to clipboard",
"TOGGLE": "Toggle secret visibility",
"CREATED_DESC": "Use the secret below to verify webhook signatures. Please copy it now, you can also find it later in the bot settings.",
"DONE": "Done",
"RESET_SUCCESS": "Webhook secret regenerated successfully",
"RESET_ERROR": "Unable to regenerate webhook secret. Please try again"
},
"ACCESS_TOKEN": {
"TITLE": "رمز المصادقة",
"DESCRIPTION": "Copy the access token and save it securely",
@@ -140,6 +140,8 @@
"ACTIONS": {
"ASSIGN_AGENT": "Assign to Agent",
"ASSIGN_TEAM": "Assign a Team",
"REMOVE_ASSIGNED_AGENT": "Remove Assigned Agent",
"REMOVE_ASSIGNED_TEAM": "Remove Assigned Team",
"ADD_LABEL": "Add a Label",
"REMOVE_LABEL": "Remove a Label",
"SEND_EMAIL_TO_TEAM": "Send an Email to Team",
@@ -169,6 +171,7 @@
},
"ATTRIBUTES": {
"MESSAGE_TYPE": "Message Type",
"PRIVATE_NOTE": "إضافة ملاحظة خاصة",
"MESSAGE_CONTAINS": "Message Contains",
"EMAIL": "البريد الإلكتروني",
"INBOX": "صندوق الوارد",
@@ -52,5 +52,17 @@
},
"CHANNEL_SELECTOR": {
"COMING_SOON": "Coming Soon!"
},
"SLASH_COMMANDS": {
"HEADING_1": "Heading 1",
"HEADING_2": "Heading 2",
"HEADING_3": "Heading 3",
"BOLD": "Bold",
"ITALIC": "Italic",
"STRIKETHROUGH": "Strikethrough",
"CODE": "Code",
"BULLET_LIST": "Bullet List",
"ORDERED_LIST": "Ordered List",
"TABLE": "Table"
}
}
@@ -20,6 +20,7 @@
"CALL": "Call",
"CALL_INITIATED": "جار الاتصال بجهة الاتصال…",
"CALL_FAILED": "تعذر بدء المكالمة. الرجاء المحاولة مرة أخرى.",
"CLICK_TO_EDIT": "Click to edit",
"VOICE_INBOX_PICKER": {
"TITLE": "Choose a voice inbox"
},
@@ -86,6 +86,14 @@
"PLACEHOLDER": "Please enter your Webhook URL",
"ERROR": "الرجاء إدخال عنوان URL صالح"
},
"CHANNEL_WEBHOOK_SECRET": {
"LABEL": "Webhook Secret",
"COPY": "Copy secret to clipboard",
"COPY_SUCCESS": "Secret copied to clipboard",
"TOGGLE": "Toggle secret visibility",
"RESET_SUCCESS": "Webhook secret regenerated successfully",
"RESET_ERROR": "Unable to regenerate webhook secret. Please try again"
},
"CHANNEL_DOMAIN": {
"LABEL": "نطاق الموقع",
"PLACEHOLDER": "أدخل نطاق موقعك الإلكتروني (مثال: acme.com)"
@@ -837,6 +837,18 @@
"SUCCESS_MESSAGE": "Custom tool deleted successfully",
"ERROR_MESSAGE": "Failed to delete custom tool"
},
"PAYWALL": {
"TITLE": "Upgrade to use tools with Captain",
"AVAILABLE_ON": "Captain Tools are only available in Business and Enterprise plans. Please upgrade to Business plan to use the feature.",
"UPGRADE_PROMPT": "",
"UPGRADE_NOW": "فتح الفواتير",
"CANCEL_ANYTIME": ""
},
"ENTERPRISE_PAYWALL": {
"AVAILABLE_ON": "Captain Tools are only available in the paid plans.",
"UPGRADE_PROMPT": "Please upgrade to a paid plan to use this feature.",
"ASK_ADMIN": "Please reach out to your administrator for the upgrade."
},
"TEST": {
"BUTTON": "Test connection",
"SUCCESS": "Endpoint returned HTTP {status}",
@@ -93,6 +93,7 @@
"ASSIGN_AGENT": "Assign an Agent",
"ADD_LABEL": "Add a Label",
"REMOVE_LABEL": "Remove a Label",
"REMOVE_ASSIGNED_AGENT": "Remove Assigned Agent",
"REMOVE_ASSIGNED_TEAM": "Remove Assigned Team",
"SEND_EMAIL_TRANSCRIPT": "Send an Email Transcript",
"MUTE_CONVERSATION": "كتم المحادثة",
@@ -68,7 +68,8 @@
"API_SUCCESS": "تم حفظ التوقيع بنجاح",
"IMAGE_UPLOAD_ERROR": "تعذر رفع الصورة! حاول مرة أخرى",
"IMAGE_UPLOAD_SUCCESS": "تم إضافة الصورة بنجاح. الرجاء النقر على الحفظ لحفظ التوقيع",
"IMAGE_UPLOAD_SIZE_ERROR": "حجم الصورة يجب أن يكون أقل من {size}MB"
"IMAGE_UPLOAD_SIZE_ERROR": "حجم الصورة يجب أن يكون أقل من {size}MB",
"INLINE_IMAGE_WARNING": "Pasted inline images were removed. Please use the image upload button to add images to your signature."
},
"MESSAGE_SIGNATURE": {
"LABEL": "توقيع الرسالة",
@@ -45,6 +45,13 @@
"ERROR_MESSAGE": "تعذر الاتصال بالخادم، الرجاء المحاولة مرة أخرى لاحقاً"
},
"SUBMIT": "إرسال",
"HAVE_AN_ACCOUNT": "هل لديك حساب مسبق؟"
"HAVE_AN_ACCOUNT": "هل لديك حساب مسبق؟",
"VERIFY_EMAIL": {
"TITLE": "Check your inbox",
"DESCRIPTION": "We sent a verification link to {email}. Click the link to verify your email and get started.",
"RESEND": "إعادة إرسال رسالة التحقق",
"RESEND_SUCCESS": "Verification email sent. Please check your inbox.",
"RESEND_ERROR": "Could not send verification email. Please try again."
}
}
}
@@ -63,6 +63,16 @@
"ERROR_MESSAGE": "Could not update bot. Please try again."
}
},
"SECRET": {
"LABEL": "Webhook Secret",
"COPY": "Copy secret to clipboard",
"COPY_SUCCESS": "Secret copied to clipboard",
"TOGGLE": "Toggle secret visibility",
"CREATED_DESC": "Use the secret below to verify webhook signatures. Please copy it now, you can also find it later in the bot settings.",
"DONE": "Hazır",
"RESET_SUCCESS": "Webhook secret regenerated successfully",
"RESET_ERROR": "Unable to regenerate webhook secret. Please try again"
},
"ACCESS_TOKEN": {
"TITLE": "Access Token",
"DESCRIPTION": "Copy the access token and save it securely",
@@ -140,6 +140,8 @@
"ACTIONS": {
"ASSIGN_AGENT": "Assign to Agent",
"ASSIGN_TEAM": "Assign a Team",
"REMOVE_ASSIGNED_AGENT": "Remove Assigned Agent",
"REMOVE_ASSIGNED_TEAM": "Remove Assigned Team",
"ADD_LABEL": "Add a Label",
"REMOVE_LABEL": "Remove a Label",
"SEND_EMAIL_TO_TEAM": "Send an Email to Team",
@@ -169,6 +171,7 @@
},
"ATTRIBUTES": {
"MESSAGE_TYPE": "Message Type",
"PRIVATE_NOTE": "Şəxsi Qeyd",
"MESSAGE_CONTAINS": "Message Contains",
"EMAIL": "Email",
"INBOX": "Inbox",
@@ -52,5 +52,17 @@
},
"CHANNEL_SELECTOR": {
"COMING_SOON": "Coming Soon!"
},
"SLASH_COMMANDS": {
"HEADING_1": "Heading 1",
"HEADING_2": "Heading 2",
"HEADING_3": "Heading 3",
"BOLD": "Bold",
"ITALIC": "Italic",
"STRIKETHROUGH": "Strikethrough",
"CODE": "Code",
"BULLET_LIST": "Bullet List",
"ORDERED_LIST": "Ordered List",
"TABLE": "Table"
}
}
@@ -20,6 +20,7 @@
"CALL": "Zəng et",
"CALL_INITIATED": "Calling the contact…",
"CALL_FAILED": "Unable to start the call. Please try again.",
"CLICK_TO_EDIT": "Click to edit",
"VOICE_INBOX_PICKER": {
"TITLE": "Choose a voice inbox"
},
@@ -698,7 +698,7 @@
"EDIT_ARTICLE": {
"MORE_PROPERTIES": "More properties",
"UNCATEGORIZED": "Uncategorized",
"EDITOR_PLACEHOLDER": "Write something..."
"EDITOR_PLACEHOLDER": "Write your content here. Type '/' for formatting options."
},
"ARTICLE_PROPERTIES": {
"ARTICLE_PROPERTIES": "Article properties",
@@ -86,6 +86,14 @@
"PLACEHOLDER": "Zəhmət olmasa Webhook URL-ni daxil edin",
"ERROR": "Zəhmət olmasa düzgün URL daxil edin"
},
"CHANNEL_WEBHOOK_SECRET": {
"LABEL": "Webhook Secret",
"COPY": "Copy secret to clipboard",
"COPY_SUCCESS": "Secret copied to clipboard",
"TOGGLE": "Toggle secret visibility",
"RESET_SUCCESS": "Webhook secret regenerated successfully",
"RESET_ERROR": "Unable to regenerate webhook secret. Please try again"
},
"CHANNEL_DOMAIN": {
"LABEL": "Veb sayt Domeni",
"PLACEHOLDER": "Veb sayt domeninizi daxil edin (məsələn: acme.com)"
@@ -837,6 +837,18 @@
"SUCCESS_MESSAGE": "Xüsusi alət uğurla silindi",
"ERROR_MESSAGE": "Xüsusi alət silinmədi"
},
"PAYWALL": {
"TITLE": "Upgrade to use tools with Captain",
"AVAILABLE_ON": "Captain Tools are only available in Business and Enterprise plans. Please upgrade to Business plan to use the feature.",
"UPGRADE_PROMPT": "",
"UPGRADE_NOW": "Open billing",
"CANCEL_ANYTIME": ""
},
"ENTERPRISE_PAYWALL": {
"AVAILABLE_ON": "Captain Tools are only available in the paid plans.",
"UPGRADE_PROMPT": "Please upgrade to a paid plan to use this feature.",
"ASK_ADMIN": "Please reach out to your administrator for the upgrade."
},
"TEST": {
"BUTTON": "Bağlantını yoxla",
"SUCCESS": "Endpoint HTTP {status} qaytardı",
@@ -93,6 +93,7 @@
"ASSIGN_AGENT": "Assign an Agent",
"ADD_LABEL": "Add a Label",
"REMOVE_LABEL": "Remove a Label",
"REMOVE_ASSIGNED_AGENT": "Remove Assigned Agent",
"REMOVE_ASSIGNED_TEAM": "Remove Assigned Team",
"SEND_EMAIL_TRANSCRIPT": "Send an Email Transcript",
"MUTE_CONVERSATION": "Mute Conversation",
@@ -68,7 +68,8 @@
"API_SUCCESS": "İmza uğurla yadda saxlanıldı",
"IMAGE_UPLOAD_ERROR": "Şəkil yüklənmədi! Yenidən cəhd edin",
"IMAGE_UPLOAD_SUCCESS": "Şəkil uğurla əlavə edildi. İmzanı saxlamaq üçün zəhmət olmasa Saxla düyməsini basın",
"IMAGE_UPLOAD_SIZE_ERROR": "Şəkil ölçüsü {size}MB-dan az olmalıdır"
"IMAGE_UPLOAD_SIZE_ERROR": "Şəkil ölçüsü {size}MB-dan az olmalıdır",
"INLINE_IMAGE_WARNING": "Pasted inline images were removed. Please use the image upload button to add images to your signature."
},
"MESSAGE_SIGNATURE": {
"LABEL": "Mesaj imzası",
@@ -45,6 +45,13 @@
"ERROR_MESSAGE": "Could not connect to Woot server. Please try again."
},
"SUBMIT": "Create account",
"HAVE_AN_ACCOUNT": "Already have an account?"
"HAVE_AN_ACCOUNT": "Already have an account?",
"VERIFY_EMAIL": {
"TITLE": "Check your inbox",
"DESCRIPTION": "We sent a verification link to {email}. Click the link to verify your email and get started.",
"RESEND": "Təsdiq e-poçtunu yenidən göndər",
"RESEND_SUCCESS": "Verification email sent. Please check your inbox.",
"RESEND_ERROR": "Could not send verification email. Please try again."
}
}
}
@@ -63,6 +63,16 @@
"ERROR_MESSAGE": "Could not update bot. Please try again."
}
},
"SECRET": {
"LABEL": "Webhook Secret",
"COPY": "Copy secret to clipboard",
"COPY_SUCCESS": "Secret copied to clipboard",
"TOGGLE": "Toggle secret visibility",
"CREATED_DESC": "Use the secret below to verify webhook signatures. Please copy it now, you can also find it later in the bot settings.",
"DONE": "Done",
"RESET_SUCCESS": "Webhook secret regenerated successfully",
"RESET_ERROR": "Unable to regenerate webhook secret. Please try again"
},
"ACCESS_TOKEN": {
"TITLE": "Access Token",
"DESCRIPTION": "Copy the access token and save it securely",
@@ -140,6 +140,8 @@
"ACTIONS": {
"ASSIGN_AGENT": "Assign to Agent",
"ASSIGN_TEAM": "Assign a Team",
"REMOVE_ASSIGNED_AGENT": "Remove Assigned Agent",
"REMOVE_ASSIGNED_TEAM": "Remove Assigned Team",
"ADD_LABEL": "Add a Label",
"REMOVE_LABEL": "Remove a Label",
"SEND_EMAIL_TO_TEAM": "Send an Email to Team",
@@ -169,6 +171,7 @@
},
"ATTRIBUTES": {
"MESSAGE_TYPE": "Message Type",
"PRIVATE_NOTE": "Private Note",
"MESSAGE_CONTAINS": "Message Contains",
"EMAIL": "Email",
"INBOX": "Входяща кутия",
@@ -52,5 +52,17 @@
},
"CHANNEL_SELECTOR": {
"COMING_SOON": "Coming Soon!"
},
"SLASH_COMMANDS": {
"HEADING_1": "Heading 1",
"HEADING_2": "Heading 2",
"HEADING_3": "Heading 3",
"BOLD": "Bold",
"ITALIC": "Italic",
"STRIKETHROUGH": "Strikethrough",
"CODE": "Code",
"BULLET_LIST": "Bullet List",
"ORDERED_LIST": "Ordered List",
"TABLE": "Table"
}
}
@@ -20,6 +20,7 @@
"CALL": "Call",
"CALL_INITIATED": "Calling the contact…",
"CALL_FAILED": "Unable to start the call. Please try again.",
"CLICK_TO_EDIT": "Click to edit",
"VOICE_INBOX_PICKER": {
"TITLE": "Choose a voice inbox"
},
@@ -86,6 +86,14 @@
"PLACEHOLDER": "Please enter your Webhook URL",
"ERROR": "Please enter a valid URL"
},
"CHANNEL_WEBHOOK_SECRET": {
"LABEL": "Webhook Secret",
"COPY": "Copy secret to clipboard",
"COPY_SUCCESS": "Secret copied to clipboard",
"TOGGLE": "Toggle secret visibility",
"RESET_SUCCESS": "Webhook secret regenerated successfully",
"RESET_ERROR": "Unable to regenerate webhook secret. Please try again"
},
"CHANNEL_DOMAIN": {
"LABEL": "Website Domain",
"PLACEHOLDER": "Enter your website domain (eg: acme.com)"
@@ -837,6 +837,18 @@
"SUCCESS_MESSAGE": "Custom tool deleted successfully",
"ERROR_MESSAGE": "Failed to delete custom tool"
},
"PAYWALL": {
"TITLE": "Upgrade to use tools with Captain",
"AVAILABLE_ON": "Captain Tools are only available in Business and Enterprise plans. Please upgrade to Business plan to use the feature.",
"UPGRADE_PROMPT": "",
"UPGRADE_NOW": "Open billing",
"CANCEL_ANYTIME": ""
},
"ENTERPRISE_PAYWALL": {
"AVAILABLE_ON": "Captain Tools are only available in the paid plans.",
"UPGRADE_PROMPT": "Please upgrade to a paid plan to use this feature.",
"ASK_ADMIN": "Please reach out to your administrator for the upgrade."
},
"TEST": {
"BUTTON": "Test connection",
"SUCCESS": "Endpoint returned HTTP {status}",
@@ -93,6 +93,7 @@
"ASSIGN_AGENT": "Assign an Agent",
"ADD_LABEL": "Add a Label",
"REMOVE_LABEL": "Remove a Label",
"REMOVE_ASSIGNED_AGENT": "Remove Assigned Agent",
"REMOVE_ASSIGNED_TEAM": "Remove Assigned Team",
"SEND_EMAIL_TRANSCRIPT": "Send an Email Transcript",
"MUTE_CONVERSATION": "Заглушаване на разговора",
@@ -68,7 +68,8 @@
"API_SUCCESS": "Signature saved successfully",
"IMAGE_UPLOAD_ERROR": "Couldn't upload image! Try again",
"IMAGE_UPLOAD_SUCCESS": "Image added successfully. Please click on save to save the signature",
"IMAGE_UPLOAD_SIZE_ERROR": "Image size should be less than {size}MB"
"IMAGE_UPLOAD_SIZE_ERROR": "Image size should be less than {size}MB",
"INLINE_IMAGE_WARNING": "Pasted inline images were removed. Please use the image upload button to add images to your signature."
},
"MESSAGE_SIGNATURE": {
"LABEL": "Message Signature",
@@ -45,6 +45,13 @@
"ERROR_MESSAGE": "Не можа да се свърже с Woot сървър. Моля, опитайте отново по-късно"
},
"SUBMIT": "Create account",
"HAVE_AN_ACCOUNT": "Already have an account?"
"HAVE_AN_ACCOUNT": "Already have an account?",
"VERIFY_EMAIL": {
"TITLE": "Check your inbox",
"DESCRIPTION": "We sent a verification link to {email}. Click the link to verify your email and get started.",
"RESEND": "Resend verification email",
"RESEND_SUCCESS": "Verification email sent. Please check your inbox.",
"RESEND_ERROR": "Could not send verification email. Please try again."
}
}
}
@@ -63,6 +63,16 @@
"ERROR_MESSAGE": "Could not update bot. Please try again."
}
},
"SECRET": {
"LABEL": "Webhook Secret",
"COPY": "গোপন কোড ক্লিপবোর্ডে কপি করুন",
"COPY_SUCCESS": "গোপন কোড ক্লিপবোর্ডে কপি হয়েছে",
"TOGGLE": "গোপন কোডের দৃশ্যমানতা টগল করুন",
"CREATED_DESC": "Use the secret below to verify webhook signatures. Please copy it now, you can also find it later in the bot settings.",
"DONE": "সম্পন্ন",
"RESET_SUCCESS": "Webhook secret regenerated successfully",
"RESET_ERROR": "Unable to regenerate webhook secret. Please try again"
},
"ACCESS_TOKEN": {
"TITLE": "Access Token",
"DESCRIPTION": "Copy the access token and save it securely",
@@ -140,6 +140,8 @@
"ACTIONS": {
"ASSIGN_AGENT": "Assign to Agent",
"ASSIGN_TEAM": "Assign a Team",
"REMOVE_ASSIGNED_AGENT": "Remove Assigned Agent",
"REMOVE_ASSIGNED_TEAM": "Remove Assigned Team",
"ADD_LABEL": "Add a Label",
"REMOVE_LABEL": "Remove a Label",
"SEND_EMAIL_TO_TEAM": "Send an Email to Team",
@@ -169,6 +171,7 @@
},
"ATTRIBUTES": {
"MESSAGE_TYPE": "Message Type",
"PRIVATE_NOTE": "ব্যক্তিগত নোট",
"MESSAGE_CONTAINS": "Message Contains",
"EMAIL": "Email",
"INBOX": "Inbox",
@@ -52,5 +52,17 @@
},
"CHANNEL_SELECTOR": {
"COMING_SOON": "Coming Soon!"
},
"SLASH_COMMANDS": {
"HEADING_1": "Heading 1",
"HEADING_2": "Heading 2",
"HEADING_3": "Heading 3",
"BOLD": "Bold",
"ITALIC": "Italic",
"STRIKETHROUGH": "Strikethrough",
"CODE": "Code",
"BULLET_LIST": "Bullet List",
"ORDERED_LIST": "Ordered List",
"TABLE": "Table"
}
}
@@ -20,6 +20,7 @@
"CALL": "Call",
"CALL_INITIATED": "Calling the contact…",
"CALL_FAILED": "Unable to start the call. Please try again.",
"CLICK_TO_EDIT": "Click to edit",
"VOICE_INBOX_PICKER": {
"TITLE": "Choose a voice inbox"
},
@@ -86,6 +86,14 @@
"PLACEHOLDER": "অনুগ্রহ করে আপনার Webhook URL লিখুন",
"ERROR": "অনুগ্রহ করে একটি বৈধ URL দিন"
},
"CHANNEL_WEBHOOK_SECRET": {
"LABEL": "Webhook Secret",
"COPY": "গোপন কোড ক্লিপবোর্ডে কপি করুন",
"COPY_SUCCESS": "গোপন কোড ক্লিপবোর্ডে কপি হয়েছে",
"TOGGLE": "গোপন কোডের দৃশ্যমানতা টগল করুন",
"RESET_SUCCESS": "Webhook secret regenerated successfully",
"RESET_ERROR": "Unable to regenerate webhook secret. Please try again"
},
"CHANNEL_DOMAIN": {
"LABEL": "ওয়েবসাইট ডোমেইন",
"PLACEHOLDER": "আপনার ওয়েবসাইট ডোমেইন লিখুন (যেমন: acme.com)"
@@ -837,6 +837,18 @@
"SUCCESS_MESSAGE": "কাস্টম টুল সফলভাবে মুছে ফেলা হয়েছে",
"ERROR_MESSAGE": "কাস্টম টুল মুছে ফেলা যায়নি"
},
"PAYWALL": {
"TITLE": "Upgrade to use tools with Captain",
"AVAILABLE_ON": "Captain Tools are only available in Business and Enterprise plans. Please upgrade to Business plan to use the feature.",
"UPGRADE_PROMPT": "",
"UPGRADE_NOW": "Open billing",
"CANCEL_ANYTIME": ""
},
"ENTERPRISE_PAYWALL": {
"AVAILABLE_ON": "Captain Tools are only available in the paid plans.",
"UPGRADE_PROMPT": "Please upgrade to a paid plan to use this feature.",
"ASK_ADMIN": "Please reach out to your administrator for the upgrade."
},
"TEST": {
"BUTTON": "Test connection",
"SUCCESS": "Endpoint returned HTTP {status}",
@@ -93,6 +93,7 @@
"ASSIGN_AGENT": "Assign an Agent",
"ADD_LABEL": "Add a Label",
"REMOVE_LABEL": "Remove a Label",
"REMOVE_ASSIGNED_AGENT": "Remove Assigned Agent",
"REMOVE_ASSIGNED_TEAM": "Remove Assigned Team",
"SEND_EMAIL_TRANSCRIPT": "Send an Email Transcript",
"MUTE_CONVERSATION": "Mute Conversation",
@@ -68,7 +68,8 @@
"API_SUCCESS": "স্বাক্ষর সফলভাবে সংরক্ষণ হয়েছে",
"IMAGE_UPLOAD_ERROR": "ছবি আপলোড করা সম্ভব হয়নি! আবার চেষ্টা করুন",
"IMAGE_UPLOAD_SUCCESS": "ছবি সফলভাবে যোগ হয়েছে। স্বাক্ষর সংরক্ষণ করতে অনুগ্রহ করে সেভ-এ ক্লিক করুন।",
"IMAGE_UPLOAD_SIZE_ERROR": "ছবির আকার {size}MB-এর কম হতে হবে"
"IMAGE_UPLOAD_SIZE_ERROR": "ছবির আকার {size}MB-এর কম হতে হবে",
"INLINE_IMAGE_WARNING": "Pasted inline images were removed. Please use the image upload button to add images to your signature."
},
"MESSAGE_SIGNATURE": {
"LABEL": "বার্তার স্বাক্ষর",
@@ -45,6 +45,13 @@
"ERROR_MESSAGE": "Could not connect to Woot server. Please try again."
},
"SUBMIT": "Create account",
"HAVE_AN_ACCOUNT": "Already have an account?"
"HAVE_AN_ACCOUNT": "Already have an account?",
"VERIFY_EMAIL": {
"TITLE": "Check your inbox",
"DESCRIPTION": "We sent a verification link to {email}. Click the link to verify your email and get started.",
"RESEND": "যাচাইকরণ ইমেল পুনরায় পাঠান",
"RESEND_SUCCESS": "Verification email sent. Please check your inbox.",
"RESEND_ERROR": "Could not send verification email. Please try again."
}
}
}
@@ -63,6 +63,16 @@
"ERROR_MESSAGE": "No s'ha pogut actualitzar el bot. Torneu-ho a provar."
}
},
"SECRET": {
"LABEL": "Webhook Secret",
"COPY": "Copy secret to clipboard",
"COPY_SUCCESS": "Secret copied to clipboard",
"TOGGLE": "Toggle secret visibility",
"CREATED_DESC": "Use the secret below to verify webhook signatures. Please copy it now, you can also find it later in the bot settings.",
"DONE": "Done",
"RESET_SUCCESS": "Webhook secret regenerated successfully",
"RESET_ERROR": "Unable to regenerate webhook secret. Please try again"
},
"ACCESS_TOKEN": {
"TITLE": "Token d'accés",
"DESCRIPTION": "Copy the access token and save it securely",
@@ -140,6 +140,8 @@
"ACTIONS": {
"ASSIGN_AGENT": "Assign to Agent",
"ASSIGN_TEAM": "Assign a Team",
"REMOVE_ASSIGNED_AGENT": "Remove Assigned Agent",
"REMOVE_ASSIGNED_TEAM": "Remove Assigned Team",
"ADD_LABEL": "Add a Label",
"REMOVE_LABEL": "Remove a Label",
"SEND_EMAIL_TO_TEAM": "Send an Email to Team",
@@ -169,6 +171,7 @@
},
"ATTRIBUTES": {
"MESSAGE_TYPE": "Message Type",
"PRIVATE_NOTE": "Nota privada",
"MESSAGE_CONTAINS": "Message Contains",
"EMAIL": "Correu electrònic",
"INBOX": "Safata d'entrada",
@@ -52,5 +52,17 @@
},
"CHANNEL_SELECTOR": {
"COMING_SOON": "Coming Soon!"
},
"SLASH_COMMANDS": {
"HEADING_1": "Heading 1",
"HEADING_2": "Heading 2",
"HEADING_3": "Heading 3",
"BOLD": "Bold",
"ITALIC": "Italic",
"STRIKETHROUGH": "Strikethrough",
"CODE": "Code",
"BULLET_LIST": "Bullet List",
"ORDERED_LIST": "Ordered List",
"TABLE": "Table"
}
}
@@ -20,6 +20,7 @@
"CALL": "Call",
"CALL_INITIATED": "Calling the contact…",
"CALL_FAILED": "Unable to start the call. Please try again.",
"CLICK_TO_EDIT": "Click to edit",
"VOICE_INBOX_PICKER": {
"TITLE": "Choose a voice inbox"
},
@@ -86,6 +86,14 @@
"PLACEHOLDER": "Introdueix l'URL del teu webhook",
"ERROR": "Introduïu una URL vàlid"
},
"CHANNEL_WEBHOOK_SECRET": {
"LABEL": "Webhook Secret",
"COPY": "Copy secret to clipboard",
"COPY_SUCCESS": "Secret copied to clipboard",
"TOGGLE": "Toggle secret visibility",
"RESET_SUCCESS": "Webhook secret regenerated successfully",
"RESET_ERROR": "Unable to regenerate webhook secret. Please try again"
},
"CHANNEL_DOMAIN": {
"LABEL": "Domini del lloc web",
"PLACEHOLDER": "Introduïu el vostre domini de lloc web (pe: acme.com)"
@@ -837,6 +837,18 @@
"SUCCESS_MESSAGE": "Custom tool deleted successfully",
"ERROR_MESSAGE": "Failed to delete custom tool"
},
"PAYWALL": {
"TITLE": "Upgrade to use tools with Captain",
"AVAILABLE_ON": "Captain Tools are only available in Business and Enterprise plans. Please upgrade to Business plan to use the feature.",
"UPGRADE_PROMPT": "",
"UPGRADE_NOW": "Obrir facturació",
"CANCEL_ANYTIME": ""
},
"ENTERPRISE_PAYWALL": {
"AVAILABLE_ON": "Captain Tools are only available in the paid plans.",
"UPGRADE_PROMPT": "Please upgrade to a paid plan to use this feature.",
"ASK_ADMIN": "Posa't en contacte amb el vostre administrador per obtenir l'actualització."
},
"TEST": {
"BUTTON": "Test connection",
"SUCCESS": "Endpoint returned HTTP {status}",
@@ -93,6 +93,7 @@
"ASSIGN_AGENT": "Assign an Agent",
"ADD_LABEL": "Add a Label",
"REMOVE_LABEL": "Remove a Label",
"REMOVE_ASSIGNED_AGENT": "Remove Assigned Agent",
"REMOVE_ASSIGNED_TEAM": "Remove Assigned Team",
"SEND_EMAIL_TRANSCRIPT": "Send an Email Transcript",
"MUTE_CONVERSATION": "Silencia la conversa",
@@ -68,7 +68,8 @@
"API_SUCCESS": "La signatura s'ha desat correctament",
"IMAGE_UPLOAD_ERROR": "No s'ha pogut carregar la imatge! Torna-ho a provar",
"IMAGE_UPLOAD_SUCCESS": "La imatge s'ha afegit correctament. Fes clic a desa per desar la signatura",
"IMAGE_UPLOAD_SIZE_ERROR": "La mida de la imatge ha de ser inferior a {size}MB"
"IMAGE_UPLOAD_SIZE_ERROR": "La mida de la imatge ha de ser inferior a {size}MB",
"INLINE_IMAGE_WARNING": "Pasted inline images were removed. Please use the image upload button to add images to your signature."
},
"MESSAGE_SIGNATURE": {
"LABEL": "Signatura del missatge",
@@ -45,6 +45,13 @@
"ERROR_MESSAGE": "No s'ha pogut connectar amb el servidor Woot. Torna-ho a provar més endavant"
},
"SUBMIT": "Crear un compte",
"HAVE_AN_ACCOUNT": "Ja tens un compte?"
"HAVE_AN_ACCOUNT": "Ja tens un compte?",
"VERIFY_EMAIL": {
"TITLE": "Check your inbox",
"DESCRIPTION": "We sent a verification link to {email}. Click the link to verify your email and get started.",
"RESEND": "Reenviar correu electrònic de verificació",
"RESEND_SUCCESS": "Verification email sent. Please check your inbox.",
"RESEND_ERROR": "Could not send verification email. Please try again."
}
}
}
@@ -63,6 +63,16 @@
"ERROR_MESSAGE": "Could not update bot. Please try again."
}
},
"SECRET": {
"LABEL": "Webhook Secret",
"COPY": "Copy secret to clipboard",
"COPY_SUCCESS": "Secret copied to clipboard",
"TOGGLE": "Toggle secret visibility",
"CREATED_DESC": "Use the secret below to verify webhook signatures. Please copy it now, you can also find it later in the bot settings.",
"DONE": "Done",
"RESET_SUCCESS": "Webhook secret regenerated successfully",
"RESET_ERROR": "Unable to regenerate webhook secret. Please try again"
},
"ACCESS_TOKEN": {
"TITLE": "Přístupový token",
"DESCRIPTION": "Copy the access token and save it securely",
@@ -140,6 +140,8 @@
"ACTIONS": {
"ASSIGN_AGENT": "Přiřadit agentovi",
"ASSIGN_TEAM": "Přiřadit tým",
"REMOVE_ASSIGNED_AGENT": "Remove Assigned Agent",
"REMOVE_ASSIGNED_TEAM": "Remove Assigned Team",
"ADD_LABEL": "Přidat štítek",
"REMOVE_LABEL": "Odebrat štítek",
"SEND_EMAIL_TO_TEAM": "Poslat e-mail týmu",
@@ -169,6 +171,7 @@
},
"ATTRIBUTES": {
"MESSAGE_TYPE": "Typ zprávy",
"PRIVATE_NOTE": "Soukromá poznámka",
"MESSAGE_CONTAINS": "Zpráva obsahuje",
"EMAIL": "E-mailová adresa",
"INBOX": "Inbox",
@@ -52,5 +52,17 @@
},
"CHANNEL_SELECTOR": {
"COMING_SOON": "Coming Soon!"
},
"SLASH_COMMANDS": {
"HEADING_1": "Heading 1",
"HEADING_2": "Heading 2",
"HEADING_3": "Heading 3",
"BOLD": "Bold",
"ITALIC": "Italic",
"STRIKETHROUGH": "Strikethrough",
"CODE": "Code",
"BULLET_LIST": "Bullet List",
"ORDERED_LIST": "Ordered List",
"TABLE": "Table"
}
}
@@ -20,6 +20,7 @@
"CALL": "Call",
"CALL_INITIATED": "Calling the contact…",
"CALL_FAILED": "Unable to start the call. Please try again.",
"CLICK_TO_EDIT": "Click to edit",
"VOICE_INBOX_PICKER": {
"TITLE": "Choose a voice inbox"
},
@@ -86,6 +86,14 @@
"PLACEHOLDER": "Please enter your Webhook URL",
"ERROR": "Zadejte prosím platnou URL"
},
"CHANNEL_WEBHOOK_SECRET": {
"LABEL": "Webhook Secret",
"COPY": "Copy secret to clipboard",
"COPY_SUCCESS": "Secret copied to clipboard",
"TOGGLE": "Toggle secret visibility",
"RESET_SUCCESS": "Webhook secret regenerated successfully",
"RESET_ERROR": "Unable to regenerate webhook secret. Please try again"
},
"CHANNEL_DOMAIN": {
"LABEL": "Doména webových stránek",
"PLACEHOLDER": "Zadejte doménu webu (např. acme.com)"
@@ -837,6 +837,18 @@
"SUCCESS_MESSAGE": "Custom tool deleted successfully",
"ERROR_MESSAGE": "Failed to delete custom tool"
},
"PAYWALL": {
"TITLE": "Upgrade to use tools with Captain",
"AVAILABLE_ON": "Captain Tools are only available in Business and Enterprise plans. Please upgrade to Business plan to use the feature.",
"UPGRADE_PROMPT": "",
"UPGRADE_NOW": "Open billing",
"CANCEL_ANYTIME": ""
},
"ENTERPRISE_PAYWALL": {
"AVAILABLE_ON": "Captain Tools are only available in the paid plans.",
"UPGRADE_PROMPT": "Please upgrade to a paid plan to use this feature.",
"ASK_ADMIN": "Please reach out to your administrator for the upgrade."
},
"TEST": {
"BUTTON": "Test connection",
"SUCCESS": "Endpoint returned HTTP {status}",
@@ -93,6 +93,7 @@
"ASSIGN_AGENT": "Přiřadit agenta",
"ADD_LABEL": "Přidat štítek",
"REMOVE_LABEL": "Odebrat štítek",
"REMOVE_ASSIGNED_AGENT": "Remove Assigned Agent",
"REMOVE_ASSIGNED_TEAM": "Remove Assigned Team",
"SEND_EMAIL_TRANSCRIPT": "Send an Email Transcript",
"MUTE_CONVERSATION": "Ztlumit konverzaci",
@@ -68,7 +68,8 @@
"API_SUCCESS": "Signature saved successfully",
"IMAGE_UPLOAD_ERROR": "Couldn't upload image! Try again",
"IMAGE_UPLOAD_SUCCESS": "Image added successfully. Please click on save to save the signature",
"IMAGE_UPLOAD_SIZE_ERROR": "Image size should be less than {size}MB"
"IMAGE_UPLOAD_SIZE_ERROR": "Image size should be less than {size}MB",
"INLINE_IMAGE_WARNING": "Pasted inline images were removed. Please use the image upload button to add images to your signature."
},
"MESSAGE_SIGNATURE": {
"LABEL": "Message Signature",
@@ -45,6 +45,13 @@
"ERROR_MESSAGE": "Nelze se připojit k Woot serveru, opakujte akci později"
},
"SUBMIT": "Create account",
"HAVE_AN_ACCOUNT": "Máte již účet?"
"HAVE_AN_ACCOUNT": "Máte již účet?",
"VERIFY_EMAIL": {
"TITLE": "Check your inbox",
"DESCRIPTION": "We sent a verification link to {email}. Click the link to verify your email and get started.",
"RESEND": "Resend verification email",
"RESEND_SUCCESS": "Verification email sent. Please check your inbox.",
"RESEND_ERROR": "Could not send verification email. Please try again."
}
}
}
@@ -63,6 +63,16 @@
"ERROR_MESSAGE": "Could not update bot. Please try again."
}
},
"SECRET": {
"LABEL": "Webhook Secret",
"COPY": "Copy secret to clipboard",
"COPY_SUCCESS": "Secret copied to clipboard",
"TOGGLE": "Toggle secret visibility",
"CREATED_DESC": "Use the secret below to verify webhook signatures. Please copy it now, you can also find it later in the bot settings.",
"DONE": "Done",
"RESET_SUCCESS": "Webhook secret regenerated successfully",
"RESET_ERROR": "Unable to regenerate webhook secret. Please try again"
},
"ACCESS_TOKEN": {
"TITLE": "Adgangs Token",
"DESCRIPTION": "Copy the access token and save it securely",
@@ -140,6 +140,8 @@
"ACTIONS": {
"ASSIGN_AGENT": "Assign to Agent",
"ASSIGN_TEAM": "Assign a Team",
"REMOVE_ASSIGNED_AGENT": "Remove Assigned Agent",
"REMOVE_ASSIGNED_TEAM": "Remove Assigned Team",
"ADD_LABEL": "Add a Label",
"REMOVE_LABEL": "Remove a Label",
"SEND_EMAIL_TO_TEAM": "Send an Email to Team",
@@ -169,6 +171,7 @@
},
"ATTRIBUTES": {
"MESSAGE_TYPE": "Message Type",
"PRIVATE_NOTE": "Privat Note",
"MESSAGE_CONTAINS": "Message Contains",
"EMAIL": "E-mail",
"INBOX": "Indbakke",
@@ -52,5 +52,17 @@
},
"CHANNEL_SELECTOR": {
"COMING_SOON": "Coming Soon!"
},
"SLASH_COMMANDS": {
"HEADING_1": "Heading 1",
"HEADING_2": "Heading 2",
"HEADING_3": "Heading 3",
"BOLD": "Bold",
"ITALIC": "Italic",
"STRIKETHROUGH": "Strikethrough",
"CODE": "Code",
"BULLET_LIST": "Bullet List",
"ORDERED_LIST": "Ordered List",
"TABLE": "Table"
}
}
@@ -20,6 +20,7 @@
"CALL": "Call",
"CALL_INITIATED": "Calling the contact…",
"CALL_FAILED": "Unable to start the call. Please try again.",
"CLICK_TO_EDIT": "Click to edit",
"VOICE_INBOX_PICKER": {
"TITLE": "Choose a voice inbox"
},
@@ -86,6 +86,14 @@
"PLACEHOLDER": "Please enter your Webhook URL",
"ERROR": "Angiv en gyldig URL"
},
"CHANNEL_WEBHOOK_SECRET": {
"LABEL": "Webhook Secret",
"COPY": "Copy secret to clipboard",
"COPY_SUCCESS": "Secret copied to clipboard",
"TOGGLE": "Toggle secret visibility",
"RESET_SUCCESS": "Webhook secret regenerated successfully",
"RESET_ERROR": "Unable to regenerate webhook secret. Please try again"
},
"CHANNEL_DOMAIN": {
"LABEL": "Hjemmeside Domæne",
"PLACEHOLDER": "Indtast dit website domæne (fx: ditfirma.dk)"

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