Compare commits

..
Author SHA1 Message Date
Shivam Mishra 99c33f306a feat: use resolved contacts for filtering 2025-03-25 18:53:43 +05:30
Shivam Mishra 400feb4388 feat: move resolved contacts to a scope 2025-03-25 18:53:32 +05:30
Shivam Mishra 340e5d2acc feat: update schema 2025-03-25 18:43:02 +05:30
Shivam Mishra 89deb74c24 feat: add new indexes 2025-03-25 18:34:30 +05:30
Muhsin KelothandGitHub 8826a7066c chore: Remove delete Instagram story implementation (#11097)
Currently, Instagram stories are downloaded and displayed as
attachments. We previously implemented story deletion by checking
individual messages via
https://github.com/chatwoot/chatwoot/pull/5300/files#diff-684a16c1b0b4c099fcdfeed95b1820e11fef629fe332ec7ce6a8c600331dd06dR110,
but this approach proved impractical and was removed. This PR removes
all unused code to avoid confusion. We will revisit the story deletion
feature when we implement `instagram_manage_insights`.
2025-03-25 09:44:26 +05:30
50efd28d16 feat: Add support for frontend filtering of conversations (#11111)
This pull request includes significant changes to the filtering logic
for conversations in the frontend, here's a summary of the changes

This includes adding a `matchesFilters` method that evaluates a
conversation against the applied filters. It does so by first evaluating
all the conditions, and later converting the results into a JSONLogic
object that can be evaluated according to Postgres operator precedence

### Alignment Specs

To ensure the frontend and backend implementations always align, we've
added tests on both sides with same cases, for anyone fixing any
regressions found in the frontend implementation, they need to ensure
the existing tests always pass.

Test Case | JavaScript Spec | Ruby Spec | Match?
-- | -- | -- | --
**A AND B OR C** | Present | Present | Yes
Matches when all conditions are true | Present | Present | Yes
Matches when first condition is false but third is true | Present |
Present | Yes
Matches when first and second conditions are false but third is true |
Present | Present | Yes
Does not match when all conditions are false | Present | Present | Yes
**A OR B AND C** | Present | Present | Yes
Matches when first condition is true | Present | Present | Yes
Matches when second and third conditions are true | Present | Present |
Yes
**A AND B OR C AND D** | Present | Present | Yes
Matches when first two conditions are true | Present | Present | Yes
Matches when last two conditions are true | Present | Present | Yes
**Mixed Operators (A AND (B OR C) AND D)** | Present | Present | Yes
Matches when all conditions in the chain are true | Present | Present |
Yes
Does not match when the last condition is false | Present | Present |
Yes

---------

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Pranav <pranav@chatwoot.com>
2025-03-24 19:39:04 -07:00
41d6f9a200 chore: Add cache to improve widget performance (#11163)
- Add dynamic importing for routes.
- Added caching for `campaign`, `articles` and `inbox_members` API end
points.

---------

Co-authored-by: Pranav <pranavrajs@gmail.com>
2025-03-24 16:04:49 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Shivam Mishra
07d19362d2 chore(deps): Bump nokogiri from 1.18.3 to 1.18.4 (#11153)
Bumps [nokogiri](https://github.com/sparklemotion/nokogiri) from 1.18.3
to 1.18.4.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/sparklemotion/nokogiri/releases">nokogiri's
releases</a>.</em></p>
<blockquote>
<h2>v1.18.4 / 2025-03-14</h2>
<h3>Security</h3>
<ul>
<li>[CRuby] Vendored libxslt is updated to <a
href="https://gitlab.gnome.org/GNOME/libxslt/-/releases/v1.1.43">v1.1.43</a>
to address CVE-2025-24855 and CVE-2024-55549. See <a
href="https://github.com/sparklemotion/nokogiri/security/advisories/GHSA-mrxw-mxhj-p664">GHSA-mrxw-mxhj-p664</a>
for more information.</li>
</ul>
<!-- raw HTML omitted -->

<pre><code>8f2263cef9953ce09bd5293d76c9bbd3013d2f94d1cca67783dfe6635c529deb
nokogiri-1.18.4-aarch64-linux-gnu.gem
4e231f8ba3128cfc2ef0cc0bdc807d7ce71fc62cb6a78216e817be8631fe6a96
nokogiri-1.18.4-aarch64-linux-musl.gem
73902663b23b1123282b9c0b6d9654b1fb286dfee8d65cb1f6029087b7f0d037
nokogiri-1.18.4-arm64-darwin.gem
cc2945e2c19560a61a97737e6bd3b329edb1f82ca204d46a18e5e98ad0a550a6
nokogiri-1.18.4-arm-linux-gnu.gem
4fb7f44de0cd85abfa869e4cfb619410da174ebf9fbe26ae0caa65462b818bcb
nokogiri-1.18.4-arm-linux-musl.gem
bb7820521c1bbae1d3e0092ff03b27a8e700912b37d80f962b7e4567947a64ac
nokogiri-1.18.4.gem
cbc0bab72eb5a9573efa7b98351fdd44c609e8d4585456ca1be18db2b7764b64
nokogiri-1.18.4-java.gem
bd567cb509eb75de8f27ca6ecaf4a38bf0563482188991f9bcccccac9c3b9a2f
nokogiri-1.18.4-x64-mingw-ucrt.gem
e4776f58eea9b94d05caf8bf351e3c6aa1cce01edcc2ed530f3c302c13178965
nokogiri-1.18.4-x86_64-darwin.gem
b1c6407b346b88704e97a342a80acd4755175324e624da34d0c5cfdc8d34191e
nokogiri-1.18.4-x86_64-linux-gnu.gem
ea7c0356a70f3d2d0d76315b533877013d20368d5c9f437c38e0bd462c4844dc
nokogiri-1.18.4-x86_64-linux-musl.gem
</code></pre>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/sparklemotion/nokogiri/blob/main/CHANGELOG.md">nokogiri's
changelog</a>.</em></p>
<blockquote>
<h2>v1.18.4 / 2025-03-14</h2>
<h3>Security</h3>
<ul>
<li>[CRuby] Vendored libxslt is updated to <a
href="https://gitlab.gnome.org/GNOME/libxslt/-/releases/v1.1.43">v1.1.43</a>
to address CVE-2025-24855 and CVE-2024-55549. See <a
href="https://github.com/sparklemotion/nokogiri/security/advisories/GHSA-mrxw-mxhj-p664">GHSA-mrxw-mxhj-p664</a>
for more information.</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/sparklemotion/nokogiri/commit/80edf1c35d79eae3a2fd7315f1fb61831ca3c94a"><code>80edf1c</code></a>
version bump to v1.18.4</li>
<li><a
href="https://github.com/sparklemotion/nokogiri/commit/3c12e357632043725bb7dde0e8a9992d08c66b57"><code>3c12e35</code></a>
dep: update libxslt to 1.1.43 (v1.18.x branch) (<a
href="https://redirect.github.com/sparklemotion/nokogiri/issues/3467">#3467</a>)</li>
<li><a
href="https://github.com/sparklemotion/nokogiri/commit/395c085edd7bd00bfb6b36b23a0ff2f9b8e29d30"><code>395c085</code></a>
dep: update libxslt to 1.1.43</li>
<li><a
href="https://github.com/sparklemotion/nokogiri/commit/41257151137222136b4af1faa75ba9bf2fbd5ad2"><code>4125715</code></a>
ci: tired of waiting for gnome mirrors</li>
<li><a
href="https://github.com/sparklemotion/nokogiri/commit/35faaa3fcb4a726bfb8d5e5475b89247909fa0a2"><code>35faaa3</code></a>
doc: update CHANGELOG with GHSA</li>
<li>See full diff in <a
href="https://github.com/sparklemotion/nokogiri/compare/v1.18.3...v1.18.4">compare
view</a></li>
</ul>
</details>
<br />


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

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

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

---

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

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

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
2025-03-24 13:55:45 +05:30
Sojan abf69c5134 Merge branch 'release/4.0.4' into develop 2025-03-21 18:55:14 -07:00
Sojan c2946415c8 Bump version to 4.0.4 2025-03-21 18:54:44 -07:00
Chatwoot BotandGitHub 3c85b79d2a chore: Update translations (#11141)
- Update translations
2025-03-21 18:50:52 -07:00
PranavandGitHub 6757b025f4 chore: Update readme (#11154)
- changes to the readme
2025-03-21 18:14:51 -07:00
PranavandGitHub d355801555 fix: Do not allow sending messages if merged contact has a duplicate session (#11152)
In this PR https://github.com/chatwoot/chatwoot/pull/11139, if there is
an attempt to create a duplication session for the contact in the same
inbox, we will anonymize the old session.

This PR would prevent sending messages to the older sessions. The
support agents will have to create a new conversation to continue
messages with customer.
2025-03-21 18:04:46 -07:00
PranavandGitHub 950d9f50a5 chore: Update Readme (#11148)
Update readme.
2025-03-21 14:37:38 -07:00
Sivin VargheseandGitHub 3a693947b5 feat: Add RTL Support to Widget (#11022)
This PR adds RTL support to the web widget for improved right-to-left language compatibility, updates colors, and cleans up code.

Fixes https://linear.app/chatwoot/issue/CW-4089/rtl-issues-on-widget

https://github.com/chatwoot/chatwoot/issues/9791

Other PR: https://github.com/chatwoot/chatwoot/pull/11016
2025-03-21 09:39:03 -07:00
Sivin VargheseandGitHub e4ea078e52 chore: Update buttons in conversation screens - 2 (#11134) 2025-03-21 21:54:54 +05:30
280bc58963 chore: Update buttons in conversation screens(#11132)
# Pull Request Template

### Changes includes
* Update send message and add note button
<img width="151" alt="image"
src="https://github.com/user-attachments/assets/646d1d22-07be-4f2f-9090-6642556aa761"
/>
<img width="151" alt="image"
src="https://github.com/user-attachments/assets/b5651420-c48b-4932-aff9-911788b9eabc"
/>
<img width="165" alt="image"
src="https://github.com/user-attachments/assets/f1ace171-2115-4eba-9055-e568d1b73c5e"
/>
<img width="165" alt="image"
src="https://github.com/user-attachments/assets/0c818461-ccdb-46ab-b7d3-3917e4ee4e74"
/>
<img width="165" alt="image"
src="https://github.com/user-attachments/assets/eef1545b-1bed-47a1-8b83-ed5d5da3f24f"
/>
<img width="165" alt="image"
src="https://github.com/user-attachments/assets/429de6e6-1263-4216-9222-4eaece95da81"
/>
<img width="165" alt="image"
src="https://github.com/user-attachments/assets/59a69867-4da1-4695-b88c-329142a693a8"
/>
<img width="165" alt="image"
src="https://github.com/user-attachments/assets/7114745c-836f-4c5a-b5d6-e200e1343a73"
/>

* Remove Unused component `AnnouncementPopup.vue`
* Updated button for custom attributes in conversation sidebar.
<img width="225" alt="image"
src="https://github.com/user-attachments/assets/a71f6c31-aca9-4e1b-bf63-6b9d5ed183c8"
/>
<img width="310" alt="image"
src="https://github.com/user-attachments/assets/8d847e1b-4a13-4108-a487-ce3d36257afa"
/>

* Update button in custom snooze modal buttons
<img width="207" alt="image"
src="https://github.com/user-attachments/assets/78315ce6-9734-467b-a4d3-e753d3eca384"
/>

* Update modal component close button
<img width="80" alt="image"
src="https://github.com/user-attachments/assets/643e9ef0-b781-47ce-a66b-a9ee4760c952"
/>

* Update AI assistant modal and AICTA modal
<img width="319" alt="image"
src="https://github.com/user-attachments/assets/8d0986ec-ec7a-4abb-9327-f73df8b4d942"
/>
<img width="565" alt="image"
src="https://github.com/user-attachments/assets/1e02ddd1-7f51-4d8a-bb57-558b9a50c938"
/>

* Update remove attachment button
<img width="301" alt="image"
src="https://github.com/user-attachments/assets/90c93eee-0b4d-4839-9db5-edc4b023df4b"
/>

* Update the conversation header buttons
<img width="256" alt="image"
src="https://github.com/user-attachments/assets/abac5d7e-dd83-40ae-b548-76bbafaa2231"
/>

* Update the retry button in old message bubbles.

---------

Co-authored-by: Pranav <pranav@chatwoot.com>
2025-03-21 14:47:28 +05:30
Sivin VargheseandGitHub 5745ffbcb0 fix: Theme inconsistency between portal page and widget (#11140) 2025-03-21 13:26:50 +05:30
PranavandGitHub b5deac468e fix: Fix duplicate contact inbox race condition (#11139)
This PR addresses a race condition in the contact inbox model caused by
duplicate `source_id` values linked to different contacts.

The issue typically occurs when an agent updates a contact’s email or
phone number or when two contacts are merged. In these scenarios, the
`source_id`, which is intended to uniquely identify the contact in a
session, may still be associated with the old contact inbox.

To solve this, we check if there’s already a ContactInbox with the same
source_id but linked to another contact. If we find one, we update that
old record by changing its source_id to a random value. This breaks the
wrong connection and prevents issues, while still keeping the old data
safe.

However, this is only a temporary fix. The main issue is with the way
the contact inbox model is designed. Right now, it’s being used to track
sessions, but that may not be necessary for non-live chat channels. In
the long run, we should consider redesigning this part of the system to
avoid such problems.
2025-03-20 18:24:28 -07:00
PranavandGitHub 4bce0a5fae fix: Duplicate action being sent when we click on save contact (#11138)
Fix duplicate save contact action being sent when we click on the
button.
2025-03-20 14:36:31 -07:00
PranavandGitHub 359b992683 chore: Reduce meta conversation API calls (#11116)
This PR would reduce the API calls if there are more than 100 total
conversations, we will throttle requests the number of requests.

Earlier this was 1000 total conversations.
2025-03-20 13:08:59 -07:00
Chatwoot BotandGitHub 41be634db0 chore: Update translations (#11117) 2025-03-20 12:00:36 -07:00
Shivam MishraandGitHub 9cf2bd284b feat: Support card message postback event as widget event (#11133)
Fixes https://github.com/chatwoot/chatwoot/issues/5956
Fixes https://github.com/chatwoot/chatwoot/issues/1895
2025-03-20 12:00:24 -07:00
Sivin VargheseandGitHub 91b7785dfe chore: Update buttons in Inbox and its settings page (#11127) 2025-03-20 11:59:47 -07:00
Sivin VargheseandGitHub 220014c290 chore: Update buttons in teams page (#11128)
# Pull Request Template

<img width="1188" alt="image"
src="https://github.com/user-attachments/assets/604fe991-3896-4e32-b227-7506b9939102"
/>
<img width="181" alt="image"
src="https://github.com/user-attachments/assets/557c01ed-e91b-45ce-a658-008ec27aa5ff"
/>
<img width="175" alt="image"
src="https://github.com/user-attachments/assets/5ecfb742-983a-44f1-8c94-143335318bc8"
/>
<img width="328" alt="image"
src="https://github.com/user-attachments/assets/4a05f942-3afc-45d1-8005-129245b52cef"
/>
<img width="344" alt="image"
src="https://github.com/user-attachments/assets/a3e5c85d-cd42-4368-b7aa-0422cdfe1c75"
/>
<img width="263" alt="image"
src="https://github.com/user-attachments/assets/85821352-b8c2-407a-9808-c426b8dd7bfb"
/>
<img width="263" alt="image"
src="https://github.com/user-attachments/assets/65fc0b98-c8a0-4f27-a648-3d250cd1c66d"
/>
2025-03-20 11:44:31 -07:00
Sivin VargheseandGitHub 48dd487db3 chore: Update buttons in agents page (#11129) 2025-03-20 11:26:56 -07:00
Sivin VargheseandGitHub 0cbcb8b80c chore: Update buttons in labels page (#11126) 2025-03-20 21:19:44 +05:30
Sivin VargheseandGitHub 16e2c33db5 chore: Update buttons in custom attributes page (#11125) 2025-03-20 21:19:28 +05:30
Sivin VargheseandGitHub 3cf3aca760 chore: Update buttons in automation page (#11124) 2025-03-20 21:18:57 +05:30
Sivin VargheseandGitHub 2310d92805 chore: Update buttons in bots page (#11123) 2025-03-20 21:18:16 +05:30
Sivin VargheseandGitHub 3eee499093 chore: Update buttons in macros page (#11122) 2025-03-20 21:16:06 +05:30
Sivin VargheseandGitHub 46289a6491 chore: Update buttons in integrations page (#11120) 2025-03-20 21:15:47 +05:30
Sivin VargheseandGitHub b2debdc912 chore: Update buttons in canned response page (#11121) 2025-03-20 19:03:53 +05:30
Sivin VargheseandGitHub 46297ce85e chore: Update buttons in SLA page (#11118) 2025-03-20 18:22:45 +05:30
Sivin VargheseandGitHub f82dfee0a5 chore: Update buttons in custom role page (#11119) 2025-03-20 18:22:04 +05:30
57a571ea87 fix: Fix the issue with context menu for right click on images and videos (#11114)
This pull request includes changes to various components adding a
`skip-context-menu` class to ensure the system context menu opens
instead of the Chatwoot message one

---------

Co-authored-by: Pranav <pranav@chatwoot.com>
Co-authored-by: Pranav <pranavrajs@gmail.com>
2025-03-19 20:09:44 -07:00
3a4249da11 feat: Add support for multi-language support for Captain (#11068)
This PR implements the following features

- FAQs from conversations will be generated in account language
- Contact notes will be generated in account language
- Copilot chat will respond in user language, unless the agent asks the
question in a different language

## Changes
### Copilot Chat

- Update the prompt to include an instruction for the language, the bot
will reply in asked language, but will default to account language
- Update the `ChatService` class to include pass the language to
`SystemPromptsService`

### FAQ and Contact note generation

- Update contact note generator and conversation generator to include
account locale
- Pass the account locale to `SystemPromptsService`


<details><summary>Screenshots</summary>

#### FAQs being generated in system langauge

![CleanShot 2025-03-12 at 13 32
30@2x](https://github.com/user-attachments/assets/84685bd8-3785-4432-aff3-419f60d96dd3)


#### Copilot responding in system language

![CleanShot 2025-03-12 at 13 47
03@2x](https://github.com/user-attachments/assets/38383293-4228-47bd-b74a-773e9a194f90)


</details>

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Pranav <pranav@chatwoot.com>
2025-03-19 18:25:33 -07:00
441 changed files with 7389 additions and 4405 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 285 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 966 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 934 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

+1
View File
@@ -173,6 +173,7 @@ gem 'pgvector'
# Convert Website HTML to Markdown # Convert Website HTML to Markdown
gem 'reverse_markdown' gem 'reverse_markdown'
gem 'iso-639'
gem 'ruby-openai' gem 'ruby-openai'
gem 'shopify_api' gem 'shopify_api'
+7 -4
View File
@@ -378,6 +378,8 @@ GEM
io-console (0.6.0) io-console (0.6.0)
irb (1.7.2) irb (1.7.2)
reline (>= 0.3.6) reline (>= 0.3.6)
iso-639 (0.3.8)
csv
jbuilder (2.11.5) jbuilder (2.11.5)
actionview (>= 5.0.0) actionview (>= 5.0.0)
activesupport (>= 5.0.0) activesupport (>= 5.0.0)
@@ -499,14 +501,14 @@ GEM
newrelic_rpm (9.6.0) newrelic_rpm (9.6.0)
base64 base64
nio4r (2.7.3) nio4r (2.7.3)
nokogiri (1.18.3) nokogiri (1.18.4)
mini_portile2 (~> 2.8.2) mini_portile2 (~> 2.8.2)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.18.3-arm64-darwin) nokogiri (1.18.4-arm64-darwin)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.18.3-x86_64-darwin) nokogiri (1.18.4-x86_64-darwin)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.18.3-x86_64-linux-gnu) nokogiri (1.18.4-x86_64-linux-gnu)
racc (~> 1.4) racc (~> 1.4)
oauth (1.1.0) oauth (1.1.0)
oauth-tty (~> 1.0, >= 1.0.1) oauth-tty (~> 1.0, >= 1.0.1)
@@ -913,6 +915,7 @@ DEPENDENCIES
hashie hashie
html2text html2text
image_processing image_processing
iso-639
jbuilder jbuilder
json_refs json_refs
json_schemer json_schemer
+63 -46
View File
@@ -1,22 +1,11 @@
## 🚨 Note: This branch is unstable. For the stable branch's source code, please use the branch [3.x](https://github.com/chatwoot/chatwoot/tree/3.x) <img src="./.github/screenshots/header.png#gh-light-mode-only" width="100%" alt="Header light mode"/>
<img src="./.github/screenshots/header-dark.png#gh-dark-mode-only" width="100%" alt="Header dark mode"/>
<img src="https://user-images.githubusercontent.com/2246121/282256557-1570674b-d142-4198-9740-69404cc6a339.png#gh-light-mode-only" width="100%" alt="Chat dashboard dark mode"/>
<img src="https://user-images.githubusercontent.com/2246121/282256632-87f6a01b-6467-4e0e-8a93-7bbf66d03a17.png#gh-dark-mode-only" width="100%" alt="Chat dashboard"/>
___ ___
# Chatwoot # Chatwoot
Customer engagement suite, an open-source alternative to Intercom, Zendesk, Salesforce Service Cloud etc. The modern customer support platform, an open-source alternative to Intercom, Zendesk, Salesforce Service Cloud etc.
<p>
<a href="https://heroku.com/deploy?template=https://github.com/chatwoot/chatwoot/tree/master" alt="Deploy to Heroku">
<img width="150" alt="Deploy" src="https://www.herokucdn.com/deploy/button.svg"/>
</a>
<a href="https://marketplace.digitalocean.com/apps/chatwoot?refcode=f2238426a2a8" alt="Deploy to DigitalOcean">
<img width="200" alt="Deploy to DO" src="https://www.deploytodo.com/do-btn-blue.svg"/>
</a>
</p>
<p> <p>
<a href="https://codeclimate.com/github/chatwoot/chatwoot/maintainability"><img src="https://api.codeclimate.com/v1/badges/e6e3f66332c91e5a4c0c/maintainability" alt="Maintainability"></a> <a href="https://codeclimate.com/github/chatwoot/chatwoot/maintainability"><img src="https://api.codeclimate.com/v1/badges/e6e3f66332c91e5a4c0c/maintainability" alt="Maintainability"></a>
@@ -31,41 +20,71 @@ Customer engagement suite, an open-source alternative to Intercom, Zendesk, Sale
<a href="https://artifacthub.io/packages/helm/chatwoot/chatwoot"><img src="https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/artifact-hub" alt="Artifact HUB"></a> <a href="https://artifacthub.io/packages/helm/chatwoot/chatwoot"><img src="https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/artifact-hub" alt="Artifact HUB"></a>
</p> </p>
<img src="https://user-images.githubusercontent.com/2246121/282255783-ee8a50c9-f42d-4752-8201-2d59965a663d.png#gh-light-mode-only" width="100%" alt="Chat dashboard dark mode"/>
<img src="https://user-images.githubusercontent.com/2246121/282255784-3d1994ec-d895-4ff5-ac68-d819987e1869.png#gh-dark-mode-only" width="100%" alt="Chat dashboard"/>
Chatwoot is an open-source, self-hosted customer engagement suite. Chatwoot lets you view and manage your customer data, communicate with them irrespective of which medium they use, and re-engage them based on their profile. <p>
<a href="https://heroku.com/deploy?template=https://github.com/chatwoot/chatwoot/tree/master" alt="Deploy to Heroku">
<img width="150" alt="Deploy" src="https://www.herokucdn.com/deploy/button.svg"/>
</a>
<a href="https://marketplace.digitalocean.com/apps/chatwoot?refcode=f2238426a2a8" alt="Deploy to DigitalOcean">
<img width="200" alt="Deploy to DO" src="https://www.deploytodo.com/do-btn-blue.svg"/>
</a>
</p>
## Features <img src="./.github/screenshots/dashboard.png#gh-light-mode-only" width="100%" alt="Chat dashboard dark mode"/>
<img src="./.github/screenshots/dashboard-dark.png#gh-dark-mode-only" width="100%" alt="Chat dashboard"/>
Chatwoot supports the following conversation channels: ---
- **Website**: Talk to your customers using our live chat widget and make use of our SDK to identify a user and provide contextual support. Chatwoot is the modern, open-source, and self-hosted customer support platform designed to help businesses deliver exceptional customer support experience. Built for scale and flexibility, Chatwoot gives you full control over your customer data while providing powerful tools to manage conversations across channels.
- **Facebook**: Connect your Facebook pages and start replying to the direct messages to your page.
- **Instagram**: Connect your Instagram profile and start replying to the direct messages.
- **Twitter**: Connect your Twitter profiles and reply to direct messages or the tweets where you are mentioned.
- **Telegram**: Connect your Telegram bot and reply to your customers right from a single dashboard.
- **WhatsApp**: Connect your WhatsApp business account and manage the conversation in Chatwoot.
- **Line**: Connect your Line account and manage the conversations in Chatwoot.
- **SMS**: Connect your Twilio SMS account and reply to the SMS queries in Chatwoot.
- **API Channel**: Build custom communication channels using our API channel.
- **Email**: Forward all your email queries to Chatwoot and view it in our integrated dashboard.
And more. ### ✨ Captain – AI Agent for Support
Other features include: Supercharge your support with Captain, Chatwoot’s AI agent. Captain helps automate responses, handle common queries, and reduce agent workload—ensuring customers get instant, accurate answers. With Captain, your team can focus on complex conversations while routine questions are resolved automatically. Read more about Captain [here](https://chwt.app/captain-docs).
### 💬 Omnichannel Support Desk
Chatwoot centralizes all customer conversations into one powerful inbox, no matter where your customers reach out from. It supports live chat on your website, email, Facebook, Instagram, Twitter, WhatsApp, Telegram, Line, SMS etc.
### 📚 Help center portal
Publish help articles, FAQs, and guides through the built-in Help Center Portal. Enable customers to find answers on their own, reduce repetitive queries, and keep your support team focused on more complex issues.
### 🗂️ Other features
#### Collaboration & Productivity
- Private Notes and @mentions for internal team discussions.
- Labels to organize and categorize conversations.
- Keyboard Shortcuts and a Command Bar for quick navigation.
- Canned Responses to reply faster to frequently asked questions.
- Auto-Assignment to route conversations based on agent availability.
- Multi-lingual Support to serve customers in multiple languages.
- Custom Views and Filters for better inbox organization.
- Business Hours and Auto-Responders to manage response expectations.
- Teams and Automation tools for scaling support workflows.
- Agent Capacity Management to balance workload across the team.
#### Customer Data & Segmentation
- Contact Management with profiles and interaction history.
- Contact Segments and Notes for targeted communication.
- Campaigns to proactively engage customers.
- Custom Attributes for storing additional customer data.
- Pre-Chat Forms to collect user information before starting conversations.
#### Integrations
- Slack Integration to manage conversations directly from Slack.
- Dialogflow Integration for chatbot automation.
- Dashboard Apps to embed internal tools within Chatwoot.
- Shopify Integration to view and manage customer orders right within Chatwoot.
- Use Google Translate to translate messages from your customers in realtime.
- Create and manage Linear tickets within Chatwoot.
#### Reports & Insights
- Live View of ongoing conversations for real-time monitoring.
- Conversation, Agent, Inbox, Label, and Team Reports for operational visibility.
- CSAT Reports to measure customer satisfaction.
- Downloadable Reports for offline analysis and reporting.
- **CRM**: Save all your customer information right inside Chatwoot, use contact notes to log emails, phone calls, or meeting notes.
- **Custom Attributes**: Define custom attribute attributes to store information about a contact or a conversation and extend the product to match your workflow.
- **Shared multi-brand inboxes**: Manage multiple brands or pages using a shared inbox.
- **Private notes**: Use @mentions and private notes to communicate internally about a conversation.
- **Canned responses (Saved replies)**: Improve the response rate by adding saved replies for frequently asked questions.
- **Conversation Labels**: Use conversation labels to create custom workflows.
- **Auto assignment**: Chatwoot intelligently assigns a ticket to the agents who have access to the inbox depending on their availability and load.
- **Conversation continuity**: If the user has provided an email address through the chat widget, Chatwoot will send an email to the customer under the agent name so that the user can continue the conversation over the email.
- **Multi-lingual support**: Chatwoot supports 10+ languages.
- **Powerful API & Webhooks**: Extend the capability of the software using Chatwoot’s webhooks and APIs.
- **Integrations**: Chatwoot natively integrates with Slack right now. Manage your conversations in Slack without logging into the dashboard.
## Documentation ## Documentation
@@ -107,13 +126,11 @@ For other supported options, checkout our [deployment page](https://chatwoot.com
Looking to report a vulnerability? Please refer our [SECURITY.md](./SECURITY.md) file. Looking to report a vulnerability? Please refer our [SECURITY.md](./SECURITY.md) file.
## Community
## Community? Questions? Support ?
If you need help or just want to hang out, come, say hi on our [Discord](https://discord.gg/cJXdrwS) server. If you need help or just want to hang out, come, say hi on our [Discord](https://discord.gg/cJXdrwS) server.
## Contributors
## Contributors ✨
Thanks goes to all these [wonderful people](https://www.chatwoot.com/docs/contributors): Thanks goes to all these [wonderful people](https://www.chatwoot.com/docs/contributors):
+35
View File
@@ -64,5 +64,40 @@ class ContactInboxBuilder
inbox_id: @inbox.id, inbox_id: @inbox.id,
source_id: @source_id source_id: @source_id
) )
rescue ActiveRecord::RecordNotUnique
Rails.logger.info("[ContactInboxBuilder] RecordNotUnique #{@source_id} #{@contact.id} #{@inbox.id}")
update_old_contact_inbox
retry
end
def update_old_contact_inbox
# The race condition occurs when there’s a contact inbox with the
# same source ID but linked to a different contact. This can happen
# if the agent updates the contact’s email or phone number, or
# if the contact is merged with another.
#
# We update the old contact inbox source_id to a random value to
# avoid disrupting the current flow. However, the root cause of
# this issue is a flaw in the contact inbox model design.
# Contact inbox is essentially tracking a session and is not
# needed for non-live chat channels.
raise ActiveRecord::RecordNotUnique unless allowed_channels?
contact_inbox = ::ContactInbox.find_by(inbox_id: @inbox.id, source_id: @source_id)
return if contact_inbox.blank?
contact_inbox.update!(source_id: new_source_id)
end
def new_source_id
if @inbox.whatsapp? || @inbox.sms? || @inbox.twilio?
"whatsapp:#{@source_id}#{rand(100)}"
else
"#{rand(10)}#{@source_id}"
end
end
def allowed_channels?
@inbox.email? || @inbox.sms? || @inbox.twilio? || @inbox.whatsapp?
end end
end end
@@ -1,51 +0,0 @@
module Database::ConnectionDiagnostics
extend ActiveSupport::Concern
include Database::QueryInspection
include Database::QueryLogging
private
def log_connection_pool_stats(connection_pool)
{
pool_size: connection_pool.size,
active_connections: connection_pool.connections.count(&:in_use?),
total_connections: connection_pool.connections.count,
waiting_threads: connection_pool.num_waiting_in_queue,
checkout_timeout: connection_pool.checkout_timeout
}
end
def connection_status_sql
<<~SQL.squish
SELECT count(*) as connection_count, state#{' '}
FROM pg_stat_activity#{' '}
GROUP BY state;
SQL
end
def fetch_connection_diagnostics
{
active_queries: fetch_active_queries,
locked_queries: fetch_locked_queries,
connection_status: ActiveRecord::Base.connection.execute(connection_status_sql).to_a
}
rescue StandardError => e
Rails.logger.error "Error fetching active query data: #{e.message}"
{ active_queries: [], locked_queries: [] }
end
def caller_info(exception)
{
process_id: Process.pid,
thread_id: Thread.current.object_id,
backtrace: exception.backtrace&.first(15) || []
}
end
def log_timeout_error(exception, connection_info)
Rails.logger.error "ActiveRecord::ConnectionTimeoutError: #{exception.message}"
Rails.logger.error "Connection Pool Stats: #{connection_info.except(:active_queries, :locked_queries).inspect}"
log_active_queries(connection_info[:active_queries])
log_locked_queries(connection_info[:locked_queries])
end
end
@@ -1,91 +0,0 @@
module Database::QueryInspection
extend ActiveSupport::Concern
private
def active_queries_sql
<<~SQL.squish
SELECT pid,#{' '}
now() - pg_stat_activity.query_start AS duration,
query,
state,
wait_event_type,
wait_event,
backend_type,
application_name,
client_addr,
usename
FROM pg_stat_activity#{' '}
WHERE state <> 'idle'
AND query NOT ILIKE '%pg_stat_activity%'
ORDER BY duration DESC;
SQL
end
def process_active_query_row(row)
{
pid: row['pid'],
duration: row['duration'].to_s,
state: row['state'],
query: row['query'],
wait_event_type: row['wait_event_type'],
wait_event: row['wait_event'],
backend_type: row['backend_type'],
application_name: row['application_name'],
client_addr: row['client_addr'],
username: row['usename']
}
end
def fetch_active_queries
query_data = ActiveRecord::Base.connection.execute(active_queries_sql)
query_data.map { |row| process_active_query_row(row) }
end
def locked_queries_sql
<<~SQL.squish
SELECT blocked_locks.pid AS blocked_pid,
blocked_activity.usename AS blocked_user,
blocking_locks.pid AS blocking_pid,
blocking_activity.usename AS blocking_user,
blocked_activity.query AS blocked_statement,
blocking_activity.query AS blocking_statement,
now() - blocking_activity.query_start AS blocking_duration
FROM pg_catalog.pg_locks blocked_locks
JOIN pg_catalog.pg_stat_activity blocked_activity ON blocked_activity.pid = blocked_locks.pid
JOIN pg_catalog.pg_locks blocking_locks#{' '}
ON blocking_locks.locktype = blocked_locks.locktype
AND blocking_locks.DATABASE IS NOT DISTINCT FROM blocked_locks.DATABASE
AND blocking_locks.relation IS NOT DISTINCT FROM blocked_locks.relation
AND blocking_locks.page IS NOT DISTINCT FROM blocked_locks.page
AND blocking_locks.tuple IS NOT DISTINCT FROM blocked_locks.tuple
AND blocking_locks.virtualxid IS NOT DISTINCT FROM blocked_locks.virtualxid
AND blocking_locks.transactionid IS NOT DISTINCT FROM blocked_locks.transactionid
AND blocking_locks.classid IS NOT DISTINCT FROM blocked_locks.classid
AND blocking_locks.objid IS NOT DISTINCT FROM blocked_locks.objid
AND blocking_locks.objsubid IS NOT DISTINCT FROM blocked_locks.objsubid
AND blocking_locks.pid != blocked_locks.pid
JOIN pg_catalog.pg_stat_activity blocking_activity ON blocking_activity.pid = blocking_locks.pid
WHERE NOT blocked_locks.granted;
SQL
end
def process_lock_row(row)
{
blocked_pid: row['blocked_pid'],
blocked_user: row['blocked_user'],
blocking_pid: row['blocking_pid'],
blocking_user: row['blocking_user'],
blocked_statement: row['blocked_statement'],
blocking_statement: row['blocking_statement'],
blocking_duration: row['blocking_duration'].to_s
}
end
def fetch_locked_queries
lock_data = ActiveRecord::Base.connection.execute(locked_queries_sql)
return [] if lock_data.count.zero?
lock_data.map { |row| process_lock_row(row) }
end
end
@@ -1,33 +0,0 @@
module Database::QueryLogging
extend ActiveSupport::Concern
private
def log_query_details(query_info, index)
Rails.logger.error "Query ##{index + 1} [PID: #{query_info[:pid]}] [Duration: #{query_info[:duration]}] [State: #{query_info[:state]}]:"
Rails.logger.error "App: #{query_info[:application_name]} User: #{query_info[:username]} Client: #{query_info[:client_addr]}"
Rails.logger.error "Waiting: #{query_info[:wait_event_type]} / #{query_info[:wait_event]}" if query_info[:wait_event_type].present?
Rails.logger.error query_info[:query]
end
def log_active_queries(active_queries)
if active_queries.any?
Rails.logger.error "Active Database Queries (#{active_queries.count}):"
active_queries.each_with_index { |query_info, index| log_query_details(query_info, index) }
else
Rails.logger.error 'No active queries found or unable to retrieve query information'
end
end
def log_locked_queries(locked_queries)
return if locked_queries.blank?
Rails.logger.error "Locked Queries (#{locked_queries.count}):"
locked_queries.each_with_index do |lock_info, index|
Rails.logger.error "Lock ##{index + 1}: PID #{lock_info[:blocked_pid]} blocked by PID #{lock_info[:blocking_pid]}"
Rails.logger.error "Duration: #{lock_info[:blocking_duration]}"
Rails.logger.error "Blocked query: #{lock_info[:blocked_statement]}"
Rails.logger.error "Blocking query: #{lock_info[:blocking_statement]}"
end
end
end
@@ -1,10 +1,8 @@
module RequestExceptionHandler module RequestExceptionHandler
extend ActiveSupport::Concern extend ActiveSupport::Concern
include Database::ConnectionDiagnostics
included do included do
rescue_from ActiveRecord::RecordInvalid, with: :render_record_invalid rescue_from ActiveRecord::RecordInvalid, with: :render_record_invalid
rescue_from ActiveRecord::ConnectionTimeoutError, with: :handle_connection_timeout
end end
private private
@@ -20,36 +18,11 @@ module RequestExceptionHandler
rescue ActionController::ParameterMissing => e rescue ActionController::ParameterMissing => e
log_handled_error(e) log_handled_error(e)
render_could_not_create_error(e.message) render_could_not_create_error(e.message)
rescue ActiveRecord::ConnectionTimeoutError => e
handle_connection_timeout(e)
ensure ensure
# to address the thread variable leak issues in Puma/Thin webserver # to address the thread variable leak issues in Puma/Thin webserver
Current.reset Current.reset
end end
def handle_connection_timeout(exception)
connection_pool = ActiveRecord::Base.connection_pool
connection_info = log_connection_pool_stats(connection_pool)
# Gather diagnostic info
diagnostics = fetch_connection_diagnostics
connection_info.merge!(diagnostics)
connection_info[:caller_info] = caller_info(exception)
# Log error details
log_timeout_error(exception, connection_info)
# Report to exception tracker
ChatwootExceptionTracker.new(
exception,
user: Current.user,
account: Current.account,
additional_context: { connection_info: connection_info }
).capture_exception
render_service_unavailable('Database connection timeout. Please try again later.')
end
def render_unauthorized(message) def render_unauthorized(message)
render json: { error: message }, status: :unauthorized render json: { error: message }, status: :unauthorized
end end
@@ -70,10 +43,6 @@ module RequestExceptionHandler
render json: { error: message }, status: :internal_server_error render json: { error: message }, status: :internal_server_error
end end
def render_service_unavailable(message)
render json: { error: message }, status: :service_unavailable
end
def render_record_invalid(exception) def render_record_invalid(exception)
log_handled_error(exception) log_handled_error(exception)
render json: { render json: {
@@ -4,7 +4,6 @@
@apply inline-block h-6 py-0 px-6 relative align-middle w-6; @apply inline-block h-6 py-0 px-6 relative align-middle w-6;
&.message { &.message {
@include normal-shadow;
@apply bg-white dark:bg-slate-800 rounded-full left-0 my-3 mx-auto p-4 top-0; @apply bg-white dark:bg-slate-800 rounded-full left-0 my-3 mx-auto p-4 top-0;
&::before { &::before {
@@ -1,79 +1,7 @@
@import 'dashboard/assets/scss/variables'; @import 'dashboard/assets/scss/variables';
@import 'widget/assets/scss/mixins';
$spinner-before-border-color: rgba(255, 255, 255, 0.7); $spinner-before-border-color: rgba(255, 255, 255, 0.7);
//borders
@mixin border-nil() {
border-color: transparent;
border: 0;
}
@mixin thin-border($color) {
border: 1px solid $color;
}
@mixin custom-border-bottom($size, $color) {
border-bottom: $size solid $color;
}
@mixin custom-border-top($size, $color) {
border-top: $size solid $color;
}
@mixin border-normal() {
@apply border border-slate-50 dark:border-slate-700;
}
@mixin border-normal-left() {
@apply border-l border-slate-50 dark:border-slate-700;
}
@mixin border-normal-top() {
@apply border-t border-slate-50 dark:border-slate-700;
}
@mixin border-normal-right() {
@apply border-r border-slate-50 dark:border-slate-700;
}
@mixin border-normal-bottom() {
@apply border-b border-slate-50 dark:border-slate-700;
}
@mixin border-light() {
@apply border border-slate-25 dark:border-slate-700;
}
@mixin border-light-left() {
@apply border-l border-slate-25 dark:border-slate-700;
}
@mixin border-light-top() {
@apply border-t border-slate-25 dark:border-slate-700;
}
@mixin border-light-right() {
@apply border-r border-slate-25 dark:border-slate-700;
}
@mixin border-light-bottom() {
@apply border-b border-slate-25 dark:border-slate-700;
}
// background
@mixin background-gray() {
background: $color-background;
}
@mixin background-light() {
@apply bg-slate-50 dark:bg-slate-800;
}
@mixin background-white() {
@apply bg-white dark:bg-slate-900;
}
// input form // input form
@mixin ghost-input() { @mixin ghost-input() {
box-shadow: none; box-shadow: none;
@@ -87,65 +15,6 @@ $spinner-before-border-color: rgba(255, 255, 255, 0.7);
} }
} }
// flex-layout
@mixin space-between() {
display: flex;
justify-content: space-between;
}
@mixin space-between-column() {
@include space-between;
flex-direction: column;
}
@mixin space-between-row() {
@include space-between;
flex-direction: row;
}
@mixin flex-shrink() {
flex: 0 0 auto;
max-width: 100%;
}
@mixin flex-weight($value) {
// Grab flex-grow for older browsers.
$flex-grow: nth($value, 1);
// 2009
@include prefixer(box-flex, $flex-grow, webkit moz spec);
// 2011 (IE 10), 2012
@include prefixer(flex, $value, webkit moz ms spec);
}
// full height
@mixin full-height() {
height: 100%;
}
@mixin round-corner() {
border-radius: 1000px;
}
@mixin scroll-on-hover() {
overflow: hidden;
&:hover {
overflow-y: auto;
}
}
@mixin horizontal-scroll() {
overflow-y: auto;
}
@mixin elegant-card() {
@include normal-shadow;
border-radius: $space-small;
}
@mixin color-spinner() { @mixin color-spinner() {
@keyframes spinner { @keyframes spinner {
to { to {
@@ -230,17 +99,3 @@ $spinner-before-border-color: rgba(255, 255, 255, 0.7);
border-left: $size solid transparent; border-left: $size solid transparent;
} }
} }
@mixin text-ellipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
@mixin three-column-grid($column-one-width: 16rem,
$column-three-width: 16rem) {
width: 100%;
height: 100%;
display: grid;
grid-template-columns: minmax($column-one-width, 6fr) 10fr minmax($column-three-width, 6fr);
}
@@ -55,13 +55,13 @@ defineExpose({ dialogRef, contactsFormRef, onSuccess });
@click="closeDialog" @click="closeDialog"
/> />
<Button <Button
type="submit"
:label=" :label="
t('CONTACTS_LAYOUT.HEADER.ACTIONS.CONTACT_CREATION.SAVE_CONTACT') t('CONTACTS_LAYOUT.HEADER.ACTIONS.CONTACT_CREATION.SAVE_CONTACT')
" "
color="blue" color="blue"
:disabled="contactsFormRef?.isFormInvalid" :disabled="contactsFormRef?.isFormInvalid"
:is-loading="isCreatingContact" :is-loading="isCreatingContact"
@click="handleDialogConfirm"
/> />
</div> </div>
</template> </template>
@@ -190,7 +190,7 @@ const STYLE_CONFIG = {
const variantClasses = computed(() => { const variantClasses = computed(() => {
const variantMap = { const variantMap = {
ghost: `${STYLE_CONFIG.colors[computedColor.value].ghost}`, ghost: `${STYLE_CONFIG.colors[computedColor.value].ghost}`,
link: `${STYLE_CONFIG.colors[computedColor.value].link} p-0 font-medium underline-offset-4`, link: `${STYLE_CONFIG.colors[computedColor.value].link} p-0 font-medium underline-offset-2`,
outline: STYLE_CONFIG.colors[computedColor.value].outline, outline: STYLE_CONFIG.colors[computedColor.value].outline,
faded: STYLE_CONFIG.colors[computedColor.value].faded, faded: STYLE_CONFIG.colors[computedColor.value].faded,
solid: STYLE_CONFIG.colors[computedColor.value].solid, solid: STYLE_CONFIG.colors[computedColor.value].solid,
@@ -13,6 +13,9 @@ const attachment = computed(() => {
<template> <template>
<BaseBubble class="bg-transparent" data-bubble-name="audio"> <BaseBubble class="bg-transparent" data-bubble-name="audio">
<AudioChip :attachment="attachment" class="p-2 text-n-slate-12" /> <AudioChip
:attachment="attachment"
class="p-2 text-n-slate-12 skip-context-menu"
/>
</BaseBubble> </BaseBubble>
</template> </template>
@@ -56,6 +56,7 @@ const downloadAttachment = async () => {
</div> </div>
<div v-else class="relative group rounded-lg overflow-hidden"> <div v-else class="relative group rounded-lg overflow-hidden">
<img <img
class="skip-context-menu"
:src="attachment.dataUrl" :src="attachment.dataUrl"
:width="attachment.width" :width="attachment.width"
:height="attachment.height" :height="attachment.height"
@@ -63,8 +64,9 @@ const downloadAttachment = async () => {
@error="handleError" @error="handleError"
/> />
<div <div
class="inset-0 p-2 absolute bg-gradient-to-tl from-n-slate-12/30 dark:from-n-slate-1/50 via-transparent to-transparent hidden group-hover:flex items-end justify-end gap-1.5" class="inset-0 p-2 pointer-events-none absolute bg-gradient-to-tl from-n-slate-12/30 dark:from-n-slate-1/50 via-transparent to-transparent hidden group-hover:flex"
> />
<div class="absolute right-2 bottom-2 hidden group-hover:flex gap-2">
<Button xs solid slate icon="i-lucide-expand" class="opacity-60" /> <Button xs solid slate icon="i-lucide-expand" class="opacity-60" />
<Button <Button
xs xs
@@ -41,13 +41,13 @@ const onVideoLoadError = () => {
<div v-if="content" v-dompurify-html="formattedContent" class="mb-2" /> <div v-if="content" v-dompurify-html="formattedContent" class="mb-2" />
<img <img
v-if="!hasImgStoryError" v-if="!hasImgStoryError"
class="rounded-lg max-w-80" class="rounded-lg max-w-80 skip-context-menu"
:src="attachment.dataUrl" :src="attachment.dataUrl"
@error="onImageLoadError" @error="onImageLoadError"
/> />
<video <video
v-else-if="!hasVideoStoryError" v-else-if="!hasVideoStoryError"
class="rounded-lg max-w-80" class="rounded-lg max-w-80 skip-context-menu"
controls controls
:src="attachment.dataUrl" :src="attachment.dataUrl"
@error="onVideoLoadError" @error="onVideoLoadError"
@@ -35,13 +35,13 @@ const isReel = computed(() => {
<div class="relative group rounded-lg overflow-hidden"> <div class="relative group rounded-lg overflow-hidden">
<div <div
v-if="isReel" v-if="isReel"
class="absolute p-2 flex items-start justify-end right-0" class="absolute p-2 flex items-start justify-end right-0 pointer-events-none"
> >
<Icon icon="i-lucide-instagram" class="text-white shadow-lg" /> <Icon icon="i-lucide-instagram" class="text-white shadow-lg" />
</div> </div>
<video <video
controls controls
class="rounded-lg" class="rounded-lg skip-context-menu"
:src="attachment.dataUrl" :src="attachment.dataUrl"
:class="{ :class="{
'max-w-48': isReel, 'max-w-48': isReel,
@@ -36,7 +36,7 @@ const handleError = () => {
</div> </div>
<img <img
v-else v-else
class="object-cover w-full h-full" class="object-cover w-full h-full skip-context-menu"
:src="attachment.dataUrl" :src="attachment.dataUrl"
@error="handleError" @error="handleError"
/> />
@@ -61,6 +61,7 @@ import {
getUserPermissions, getUserPermissions,
filterItemsByPermission, filterItemsByPermission,
} from 'dashboard/helper/permissionsHelper.js'; } from 'dashboard/helper/permissionsHelper.js';
import { matchesFilters } from '../store/modules/conversations/helpers/filterHelpers';
import { CONVERSATION_EVENTS } from '../helper/AnalyticsHelper/events'; import { CONVERSATION_EVENTS } from '../helper/AnalyticsHelper/events';
import { ASSIGNEE_TYPE_TAB_PERMISSIONS } from 'dashboard/constants/permissions.js'; import { ASSIGNEE_TYPE_TAB_PERMISSIONS } from 'dashboard/constants/permissions.js';
@@ -105,7 +106,7 @@ const advancedFilterTypes = ref(
); );
const currentUser = useMapGetter('getCurrentUser'); const currentUser = useMapGetter('getCurrentUser');
const chatLists = useMapGetter('getAllConversations'); const chatLists = useMapGetter('getFilteredConversations');
const mineChatsList = useMapGetter('getMineChats'); const mineChatsList = useMapGetter('getMineChats');
const allChatList = useMapGetter('getAllStatusChats'); const allChatList = useMapGetter('getAllStatusChats');
const unAssignedChatsList = useMapGetter('getUnAssignedChats'); const unAssignedChatsList = useMapGetter('getUnAssignedChats');
@@ -324,6 +325,14 @@ const conversationList = computed(() => {
} else { } else {
localConversationList = [...chatLists.value]; localConversationList = [...chatLists.value];
} }
if (activeFolder.value) {
const { payload } = activeFolder.value.query;
localConversationList = localConversationList.filter(conversation => {
return matchesFilters(conversation, payload);
});
}
return localConversationList; return localConversationList;
}); });
@@ -460,6 +469,12 @@ function setParamsForEditFolderModal() {
campaigns: campaigns.value, campaigns: campaigns.value,
languages: languages, languages: languages,
countries: countries, countries: countries,
priority: [
{ id: 'low', name: t('CONVERSATION.PRIORITY.OPTIONS.LOW') },
{ id: 'medium', name: t('CONVERSATION.PRIORITY.OPTIONS.MEDIUM') },
{ id: 'high', name: t('CONVERSATION.PRIORITY.OPTIONS.HIGH') },
{ id: 'urgent', name: t('CONVERSATION.PRIORITY.OPTIONS.URGENT') },
],
filterTypes: advancedFilterTypes.value, filterTypes: advancedFilterTypes.value,
allCustomAttributes: conversationCustomAttributes.value, allCustomAttributes: conversationCustomAttributes.value,
}; };
@@ -9,12 +9,15 @@ import { getRegexp } from 'shared/helpers/Validators';
import { useVuelidate } from '@vuelidate/core'; import { useVuelidate } from '@vuelidate/core';
import { emitter } from 'shared/helpers/mitt'; import { emitter } from 'shared/helpers/mitt';
import NextButton from 'dashboard/components-next/button/Button.vue';
const DATE_FORMAT = 'yyyy-MM-dd'; const DATE_FORMAT = 'yyyy-MM-dd';
export default { export default {
components: { components: {
MultiselectDropdown, MultiselectDropdown,
HelperTextPopup, HelperTextPopup,
NextButton,
}, },
props: { props: {
label: { type: String, required: true }, label: { type: String, required: true },
@@ -219,14 +222,13 @@ export default {
class="mt-0.5" class="mt-0.5"
/> />
</span> </span>
<woot-button <NextButton
v-if="showActions && value" v-if="showActions && value"
v-tooltip.left="$t('CUSTOM_ATTRIBUTES.ACTIONS.DELETE')" v-tooltip.left="$t('CUSTOM_ATTRIBUTES.ACTIONS.DELETE')"
variant="link" slate
size="medium" sm
color-scheme="secondary" link
icon="delete" icon="i-lucide-trash-2"
class-names="flex justify-end !w-fit"
@click="onDelete" @click="onDelete"
/> />
</div> </div>
@@ -246,10 +248,10 @@ export default {
@keyup.enter="onUpdate" @keyup.enter="onUpdate"
/> />
<div> <div>
<woot-button <NextButton
size="small" sm
icon="checkmark" icon="i-lucide-check"
class="ltr:rounded-l-none rtl:rounded-r-none" class="ltr:rounded-l-none rtl:rounded-r-none h-[34px]"
@click="onUpdate" @click="onUpdate"
/> />
</div> </div>
@@ -281,25 +283,27 @@ export default {
> >
{{ displayValue || '---' }} {{ displayValue || '---' }}
</p> </p>
<div class="flex max-w-[2rem] gap-1 ml-1 rtl:mr-1 rtl:ml-0"> <div
<woot-button class="flex items-center max-w-[2rem] gap-1 ml-1 rtl:mr-1 rtl:ml-0"
>
<NextButton
v-if="showActions && value" v-if="showActions && value"
v-tooltip="$t('CUSTOM_ATTRIBUTES.ACTIONS.COPY')" v-tooltip="$t('CUSTOM_ATTRIBUTES.ACTIONS.COPY')"
variant="link" xs
size="small" slate
color-scheme="secondary" ghost
icon="clipboard" icon="i-lucide-clipboard"
class-names="hidden group-hover:flex !w-6 flex-shrink-0" class="hidden group-hover:flex flex-shrink-0"
@click="onCopy" @click="onCopy"
/> />
<woot-button <NextButton
v-if="showActions" v-if="showActions"
v-tooltip.right="$t('CUSTOM_ATTRIBUTES.ACTIONS.EDIT')" v-tooltip.right="$t('CUSTOM_ATTRIBUTES.ACTIONS.EDIT')"
variant="link" xs
size="small" slate
color-scheme="secondary" ghost
icon="edit" icon="i-lucide-pen"
class-names="hidden group-hover:flex !w-6 flex-shrink-0" class="hidden group-hover:flex flex-shrink-0"
@click="onEdit" @click="onEdit"
/> />
</div> </div>
@@ -1,9 +1,11 @@
<script> <script>
import DatePicker from 'vue-datepicker-next'; import DatePicker from 'vue-datepicker-next';
import NextButton from 'dashboard/components-next/button/Button.vue';
export default { export default {
components: { components: {
DatePicker, DatePicker,
NextButton,
}, },
emits: ['close', 'chooseTime'], emits: ['close', 'chooseTime'],
@@ -52,18 +54,23 @@ export default {
v-model:value="snoozeTime" v-model:value="snoozeTime"
type="datetime" type="datetime"
inline inline
input-class="mx-input reset-base" input-class="mx-input "
:lang="lang" :lang="lang"
:disabled-date="disabledDate" :disabled-date="disabledDate"
:disabled-time="disabledTime" :disabled-time="disabledTime"
/> />
<div class="flex flex-row justify-end w-full gap-2 px-0 py-2"> <div class="flex flex-row justify-end w-full gap-2 px-0 py-2">
<woot-button variant="clear" @click.prevent="onClose"> <NextButton
{{ $t('CONVERSATION.CUSTOM_SNOOZE.CANCEL') }} faded
</woot-button> slate
<woot-button> type="reset"
{{ $t('CONVERSATION.CUSTOM_SNOOZE.APPLY') }} :label="$t('CONVERSATION.CUSTOM_SNOOZE.CANCEL')"
</woot-button> @click.prevent="onClose"
/>
<NextButton
type="submit"
:label="$t('CONVERSATION.CUSTOM_SNOOZE.APPLY')"
/>
</div> </div>
</form> </form>
</div> </div>
@@ -2,6 +2,7 @@
// [TODO] Use Teleport to move the modal to the end of the body // [TODO] Use Teleport to move the modal to the end of the body
import { ref, computed, defineEmits, onMounted } from 'vue'; import { ref, computed, defineEmits, onMounted } from 'vue';
import { useEventListener } from '@vueuse/core'; import { useEventListener } from '@vueuse/core';
import Button from 'dashboard/components-next/button/Button.vue';
const { modalType, closeOnBackdropClick, onClose } = defineProps({ const { modalType, closeOnBackdropClick, onClose } = defineProps({
closeOnBackdropClick: { type: Boolean, default: true }, closeOnBackdropClick: { type: Boolean, default: true },
@@ -85,11 +86,11 @@ onMounted(() => {
@mouse.stop @mouse.stop
@mousedown="event => event.stopPropagation()" @mousedown="event => event.stopPropagation()"
> >
<woot-button <Button
v-if="showCloseButton" v-if="showCloseButton"
color-scheme="secondary" ghost
icon="dismiss" slate
variant="clear" icon="i-lucide-x"
class="absolute z-10 ltr:right-2 rtl:left-2 top-2" class="absolute z-10 ltr:right-2 rtl:left-2 top-2"
@click="close" @click="close"
/> />
@@ -11,6 +11,8 @@ import {
} from 'dashboard/helper/routeHelpers'; } from 'dashboard/helper/routeHelpers';
import { useEventListener } from '@vueuse/core'; import { useEventListener } from '@vueuse/core';
import Button from 'dashboard/components-next/button/Button.vue';
const { t } = useI18n(); const { t } = useI18n();
const route = useRoute(); const route = useRoute();
@@ -107,34 +109,31 @@ onBeforeUnmount(() => {
<template> <template>
<transition name="network-notification-fade" tag="div"> <transition name="network-notification-fade" tag="div">
<div v-show="showNotification" class="fixed z-50 top-4 left-2 group"> <div v-show="showNotification" class="fixed z-50 top-2 left-2 group">
<div <div
class="relative flex items-center justify-between w-full px-2 py-1 bg-yellow-200 rounded-lg shadow-lg dark:bg-yellow-700" class="relative flex items-center justify-between w-full px-2 py-1 bg-n-amber-4 dark:bg-n-amber-8 rounded-lg shadow-lg"
> >
<fluent-icon <fluent-icon :icon="iconName" class="text-n-amber-12" size="18" />
:icon="iconName" <span class="px-2 text-xs font-medium tracking-wide text-n-amber-12">
class="text-yellow-700/50 dark:text-yellow-50"
size="18"
/>
<span
class="px-2 text-xs font-medium tracking-wide text-yellow-700/70 dark:text-yellow-50"
>
{{ bannerText }} {{ bannerText }}
</span> </span>
<woot-button <Button
v-if="canRefresh" v-if="canRefresh"
ghost
sm
amber
icon="i-lucide-refresh-ccw"
:title="$t('NETWORK.BUTTON.REFRESH')" :title="$t('NETWORK.BUTTON.REFRESH')"
variant="clear" class="!text-n-amber-12 dark:!text-n-amber-9"
size="small"
color-scheme="warning"
icon="arrow-clockwise"
@click="refreshPage" @click="refreshPage"
/> />
<woot-button
variant="clear" <Button
size="small" ghost
color-scheme="warning" sm
icon="dismiss" amber
icon="i-lucide-x"
class="!text-n-amber-12 dark:!text-n-amber-9"
@click="closeNotification" @click="closeNotification"
/> />
</div> </div>
@@ -1,90 +0,0 @@
<script>
export default {
props: {
popupMessage: {
type: String,
default: '',
},
routeText: {
type: String,
default: '',
},
hasCloseButton: {
type: Boolean,
default: true,
},
closeButtonText: {
type: String,
default: '',
},
},
emits: ['open', 'close'],
methods: {
onClickOpenPath() {
this.$emit('open');
},
onClickClose(e) {
this.$emit('close', e);
},
},
};
</script>
<template>
<div class="announcement-popup">
<span v-if="popupMessage" class="popup-content">
{{ popupMessage }}
<span v-if="routeText" class="route-url" @click="onClickOpenPath">
{{ routeText }}
</span>
</span>
<div v-if="hasCloseButton" class="popup-close">
<woot-button
v-if="hasCloseButton"
color-scheme="primary"
variant="link"
size="small"
@click="onClickClose"
>
{{ closeButtonText }}
</woot-button>
</div>
</div>
</template>
<style lang="scss">
.announcement-popup {
max-width: 15rem;
min-width: 10rem;
display: flex;
position: absolute;
flex-direction: column;
align-items: flex-start;
height: fit-content;
background: var(--white);
padding: 0 var(--space-normal);
z-index: var(--z-index-much-higher);
box-shadow: var(--b-200) 4px 4px 16px 4px;
border-radius: var(--border-radius-normal);
.popup-content {
font-size: var(--font-size-mini);
color: var(--s-700);
padding: var(--space-one) 0;
.route-url {
font-size: var(--font-size-mini);
color: var(--s-600);
cursor: pointer;
text-decoration: underline;
}
}
.popup-close {
width: 100%;
display: flex;
align-items: center;
padding: var(--space-one) 0;
border-top: 1px solid var(--color-border-light);
}
}
</style>
@@ -2,10 +2,12 @@
import { useMessageFormatter } from 'shared/composables/useMessageFormatter'; import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
import { useAI } from 'dashboard/composables/useAI'; import { useAI } from 'dashboard/composables/useAI';
import AILoader from './AILoader.vue'; import AILoader from './AILoader.vue';
import NextButton from 'dashboard/components-next/button/Button.vue';
export default { export default {
components: { components: {
AILoader, AILoader,
NextButton,
}, },
props: { props: {
aiOption: { aiOption: {
@@ -84,16 +86,22 @@ export default {
</div> </div>
<div class="flex flex-row justify-end w-full gap-2 px-0 py-2"> <div class="flex flex-row justify-end w-full gap-2 px-0 py-2">
<woot-button variant="clear" @click.prevent="onClose"> <NextButton
{{ faded
slate
type="reset"
:label="
$t('INTEGRATION_SETTINGS.OPEN_AI.ASSISTANCE_MODAL.BUTTONS.CANCEL') $t('INTEGRATION_SETTINGS.OPEN_AI.ASSISTANCE_MODAL.BUTTONS.CANCEL')
}} "
</woot-button> @click.prevent="onClose"
<woot-button :disabled="!generatedContent"> />
{{ <NextButton
type="submit"
:disabled="!generatedContent"
:label="
$t('INTEGRATION_SETTINGS.OPEN_AI.ASSISTANCE_MODAL.BUTTONS.APPLY') $t('INTEGRATION_SETTINGS.OPEN_AI.ASSISTANCE_MODAL.BUTTONS.APPLY')
}} "
</woot-button> />
</div> </div>
</form> </form>
</div> </div>
@@ -6,7 +6,12 @@ import { useUISettings } from 'dashboard/composables/useUISettings';
import { useAI } from 'dashboard/composables/useAI'; import { useAI } from 'dashboard/composables/useAI';
import { OPEN_AI_EVENTS } from 'dashboard/helper/AnalyticsHelper/events'; import { OPEN_AI_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
import NextButton from 'dashboard/components-next/button/Button.vue';
export default { export default {
components: {
NextButton,
},
emits: ['close'], emits: ['close'],
setup() { setup() {
@@ -94,16 +99,30 @@ export default {
/> />
</div> </div>
<div class="flex flex-row justify-between w-full gap-2 px-0 py-2"> <div class="flex flex-row justify-between w-full gap-2 px-0 py-2">
<woot-button variant="link" @click.prevent="openOpenAIDoc"> <NextButton
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.BUTTONS.NEED_HELP') }} ghost
</woot-button> type="button"
class="!px-3"
:label="
$t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.BUTTONS.NEED_HELP')
"
@click.prevent="openOpenAIDoc"
/>
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<woot-button variant="clear" @click.prevent="onDismiss"> <NextButton
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.BUTTONS.DISMISS') }} faded
</woot-button> slate
<woot-button :is-disabled="v$.value.$invalid"> type="reset"
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.BUTTONS.FINISH') }} :label="
</woot-button> $t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.BUTTONS.DISMISS')
"
@click.prevent="onDismiss"
/>
<NextButton
type="submit"
:disabled="v$.value.$invalid"
:label="$t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.BUTTONS.FINISH')"
/>
</div> </div>
</div> </div>
</form> </form>
@@ -2,6 +2,8 @@
import { computed } from 'vue'; import { computed } from 'vue';
import { formatBytes } from 'shared/helpers/FileHelper'; import { formatBytes } from 'shared/helpers/FileHelper';
import Button from 'dashboard/components-next/button/Button.vue';
const props = defineProps({ const props = defineProps({
attachments: { attachments: {
type: Array, type: Array,
@@ -73,9 +75,11 @@ const fileName = file => {
</span> </span>
</div> </div>
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">
<woot-button <Button
class="!w-6 !h-6 text-sm rounded-md hover:bg-slate-50 dark:hover:bg-slate-800 clear secondary" ghost
icon="dismiss" slate
xs
icon="i-lucide-x"
@click="onRemoveAttachment(index)" @click="onRemoveAttachment(index)"
/> />
</div> </div>
@@ -2,11 +2,14 @@
import AutomationActionTeamMessageInput from './AutomationActionTeamMessageInput.vue'; import AutomationActionTeamMessageInput from './AutomationActionTeamMessageInput.vue';
import AutomationActionFileInput from './AutomationFileInput.vue'; import AutomationActionFileInput from './AutomationFileInput.vue';
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue'; import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
import NextButton from 'dashboard/components-next/button/Button.vue';
export default { export default {
components: { components: {
AutomationActionTeamMessageInput, AutomationActionTeamMessageInput,
AutomationActionFileInput, AutomationActionFileInput,
WootMessageEditor, WootMessageEditor,
NextButton,
}, },
props: { props: {
modelValue: { modelValue: {
@@ -172,11 +175,11 @@ export default {
/> />
</div> </div>
</div> </div>
<woot-button <NextButton
v-if="!isMacro" v-if="!isMacro"
icon="dismiss" icon="i-lucide-x"
variant="clear" slate
color-scheme="secondary" ghost
class="flex-shrink-0" class="flex-shrink-0"
@click="removeAction" @click="removeAction"
/> />
@@ -32,7 +32,7 @@ const buttonStyleClass = props.compact ? 'text-sm' : 'text-base';
:class="buttonStyleClass" :class="buttonStyleClass"
@click.capture="goBack" @click.capture="goBack"
> >
<fluent-icon icon="chevron-left" class="-ml-1" /> <i class="i-lucide-chevron-left -ml-1 text-lg" />
{{ buttonLabel || $t('GENERAL_SETTINGS.BACK') }} {{ buttonLabel || $t('GENERAL_SETTINGS.BACK') }}
</button> </button>
</template> </template>
@@ -1,6 +1,11 @@
<script> <script>
import NextButton from 'dashboard/components-next/button/Button.vue';
export default { export default {
name: 'FilterInput', name: 'FilterInput',
components: {
NextButton,
},
props: { props: {
modelValue: { modelValue: {
type: Object, type: Object,
@@ -242,10 +247,11 @@ export default {
:placeholder="$t('FILTER.INPUT_PLACEHOLDER')" :placeholder="$t('FILTER.INPUT_PLACEHOLDER')"
/> />
</div> </div>
<woot-button <NextButton
icon="dismiss" icon="i-lucide-x"
variant="clear" slate
color-scheme="secondary" ghost
class="flex-shrink-0"
@click="removeFilter" @click="removeFilter"
/> />
</div> </div>
@@ -164,11 +164,6 @@ export default {
'is-note-mode': this.isNote, 'is-note-mode': this.isNote,
}; };
}, },
buttonClass() {
return {
warning: this.isNote,
};
},
showAttachButton() { showAttachButton() {
return this.showFileUpload || this.isNote; return this.showFileUpload || this.isNote;
}, },
@@ -367,14 +362,15 @@ export default {
/> />
</div> </div>
<div class="right-wrap"> <div class="right-wrap">
<woot-button <NextButton
size="small" :label="sendButtonText"
:class-names="buttonClass" type="submit"
:is-disabled="isSendDisabled" sm
:color="isNote ? 'amber' : 'blue'"
:disabled="isSendDisabled"
class="flex-shrink-0"
@click="onSend" @click="onSend"
> />
{{ sendButtonText }}
</woot-button>
</div> </div>
</div> </div>
</template> </template>
@@ -13,6 +13,8 @@ import { snoozedReopenTime } from 'dashboard/helper/snoozeHelpers';
import { FEATURE_FLAGS } from 'dashboard/featureFlags'; import { FEATURE_FLAGS } from 'dashboard/featureFlags';
import Linear from './linear/index.vue'; import Linear from './linear/index.vue';
import NextButton from 'dashboard/components-next/button/Button.vue';
export default { export default {
components: { components: {
BackButton, BackButton,
@@ -21,6 +23,7 @@ export default {
Thumbnail, Thumbnail,
SLACardLabel, SLACardLabel,
Linear, Linear,
NextButton,
}, },
mixins: [inboxMixin], mixins: [inboxMixin],
props: { props: {
@@ -154,16 +157,13 @@ export default {
<div <div
class="flex flex-row items-center max-w-full gap-1 p-0 m-0 w-fit" class="flex flex-row items-center max-w-full gap-1 p-0 m-0 w-fit"
> >
<woot-button <NextButton link slate @click.prevent="$emit('contactPanelToggle')">
variant="link" <span
color-scheme="secondary" class="text-base font-medium truncate leading-tight text-n-slate-12"
class="[&>span]:overflow-hidden [&>span]:whitespace-nowrap [&>span]:text-ellipsis min-w-0" >
@click.prevent="$emit('contactPanelToggle')"
>
<span class="text-base font-medium leading-tight text-n-slate-12">
{{ currentContact.name }} {{ currentContact.name }}
</span> </span>
</woot-button> </NextButton>
<fluent-icon <fluent-icon
v-if="!isHMACVerified" v-if="!isHMACVerified"
v-tooltip="$t('CONVERSATION.UNVERIFIED_SESSION')" v-tooltip="$t('CONVERSATION.UNVERIFIED_SESSION')"
@@ -180,14 +180,13 @@ export default {
<span v-if="isSnoozed" class="font-medium text-n-amber-10"> <span v-if="isSnoozed" class="font-medium text-n-amber-10">
{{ snoozedDisplayText }} {{ snoozedDisplayText }}
</span> </span>
<woot-button <NextButton
class="p-0" link
size="small" xs
variant="link" blue
:label="contactPanelToggleText"
@click="$emit('contactPanelToggle')" @click="$emit('contactPanelToggle')"
> />
{{ contactPanelToggleText }}
</woot-button>
</div> </div>
</div> </div>
</div> </div>
@@ -25,6 +25,8 @@ import * as Sentry from '@sentry/vue';
import { useTrack } from 'dashboard/composables'; import { useTrack } from 'dashboard/composables';
import { emitter } from 'shared/helpers/mitt'; import { emitter } from 'shared/helpers/mitt';
import NextButton from 'dashboard/components-next/button/Button.vue';
export default { export default {
components: { components: {
BubbleActions, BubbleActions,
@@ -40,6 +42,7 @@ export default {
InstagramStory, InstagramStory,
InstagramStoryReply, InstagramStoryReply,
Spinner, Spinner,
NextButton,
}, },
props: { props: {
data: { data: {
@@ -452,12 +455,12 @@ export default {
v-if="isFailed && !hasOneDayPassed && !isAnEmailInbox" v-if="isFailed && !hasOneDayPassed && !isAnEmailInbox"
class="message-failed--alert" class="message-failed--alert"
> >
<woot-button <NextButton
v-tooltip.top-end="$t('CONVERSATION.TRY_AGAIN')" v-tooltip.top-end="$t('CONVERSATION.TRY_AGAIN')"
size="tiny" ghost
color-scheme="alert" xs
variant="clear" ruby
icon="arrow-clockwise" icon="i-lucide-refresh-ccw"
@click="retrySendMessage" @click="retrySendMessage"
/> />
</div> </div>
@@ -660,10 +663,10 @@ export default {
} }
&.is-failed { &.is-failed {
@apply bg-red-200 dark:bg-red-200; @apply bg-n-ruby-4 dark:bg-n-ruby-4 text-n-slate-12;
.message-text--metadata .time { .message-text--metadata .time {
@apply text-red-50 dark:text-red-50; @apply text-n-ruby-12 dark:text-n-ruby-12;
} }
} }
} }
@@ -724,7 +727,7 @@ li.right {
} }
.wrap.is-failed { .wrap.is-failed {
@apply flex items-end ml-auto; @apply flex items-end ltr:ml-auto rtl:mr-auto;
} }
} }
@@ -1,10 +1,11 @@
<script> <script setup>
export default { import { useRouter } from 'vue-router';
methods: { import Button from 'dashboard/components-next/button/Button.vue';
openProfileSettings() {
return this.$router.push({ name: 'profile_settings_index' }); const router = useRouter();
},
}, const openProfileSettings = () => {
return router.push({ name: 'profile_settings_index' });
}; };
</script> </script>
@@ -14,13 +15,12 @@ export default {
> >
<p class="w-fit !m-0"> <p class="w-fit !m-0">
{{ $t('CONVERSATION.FOOTER.MESSAGE_SIGNATURE_NOT_CONFIGURED') }} {{ $t('CONVERSATION.FOOTER.MESSAGE_SIGNATURE_NOT_CONFIGURED') }}
<woot-button
color-scheme="primary" <Button
variant="link" link
:label="$t('CONVERSATION.FOOTER.CLICK_HERE')"
@click="openProfileSettings" @click="openProfileSettings"
> />
{{ $t('CONVERSATION.FOOTER.CLICK_HERE') }}
</woot-button>
</p> </p>
</div> </div>
</template> </template>
@@ -37,6 +37,8 @@ import wootConstants from 'dashboard/constants/globals';
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage'; import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
import { FEATURE_FLAGS } from '../../../featureFlags'; import { FEATURE_FLAGS } from '../../../featureFlags';
import NextButton from 'dashboard/components-next/button/Button.vue';
export default { export default {
components: { components: {
Message, Message,
@@ -44,6 +46,7 @@ export default {
ReplyBox, ReplyBox,
Banner, Banner,
ConversationLabelSuggestion, ConversationLabelSuggestion,
NextButton,
}, },
mixins: [inboxMixin], mixins: [inboxMixin],
props: { props: {
@@ -488,13 +491,15 @@ export default {
:href-link-text="replyWindowLinkText" :href-link-text="replyWindowLinkText"
/> />
<div class="flex justify-end"> <div class="flex justify-end">
<woot-button <NextButton
variant="smooth" faded
size="tiny" xs
color-scheme="secondary" slate
class="box-border fixed z-10 bg-white border border-r-0 border-solid rounded-bl-calc rtl:rotate-180 rounded-tl-calc border-n-weak" class="!rounded-r-none rtl:rotate-180 !rounded-2xl !fixed z-10"
:icon="
isContactPanelOpen ? 'i-ph-caret-right-fill' : 'i-ph-caret-left-fill'
"
:class="isInboxView ? 'top-52 md:top-40' : 'top-32'" :class="isInboxView ? 'top-52 md:top-40' : 'top-32'"
:icon="isRightOrLeftIcon"
@click="onToggleContactPanel" @click="onToggleContactPanel"
/> />
</div> </div>
@@ -614,20 +619,6 @@ export default {
</div> </div>
</template> </template>
<style scoped>
@tailwind components;
@layer components {
.rounded-bl-calc {
border-bottom-left-radius: calc(1.5rem + 1px);
}
.rounded-tl-calc {
border-top-left-radius: calc(1.5rem + 1px);
}
}
</style>
<style scoped lang="scss"> <style scoped lang="scss">
.modal-mask { .modal-mask {
@apply absolute; @apply absolute;
@@ -1,21 +1,20 @@
<script> <script setup>
import MessagePreview from 'dashboard/components/widgets/conversation/MessagePreview.vue'; import MessagePreview from 'dashboard/components/widgets/conversation/MessagePreview.vue';
import Button from 'dashboard/components-next/button/Button.vue';
export default { defineProps({
components: { MessagePreview }, message: {
props: { type: Object,
message: { required: true,
type: Object,
required: true,
},
}, },
emits: ['dismiss'], });
};
const emit = defineEmits(['dismiss']);
</script> </script>
<template> <template>
<div <div
class="reply-editor bg-slate-50 dark:bg-slate-800 rounded-md py-1 pl-2 pr-1 text-xs tracking-wide mt-2 flex items-center gap-1.5 -mx-2" class="reply-editor bg-n-slate-9/10 rounded-md py-1 pl-2 pr-1 text-xs tracking-wide mt-2 flex items-center gap-1.5 -mx-2"
> >
<fluent-icon class="flex-shrink-0 icon" icon="arrow-reply" size="14" /> <fluent-icon class="flex-shrink-0 icon" icon="arrow-reply" size="14" />
<div class="flex-grow gap-1 mt-px text-xs truncate"> <div class="flex-grow gap-1 mt-px text-xs truncate">
@@ -27,14 +26,13 @@ export default {
class="inline" class="inline"
/> />
</div> </div>
<woot-button <Button
v-tooltip="$t('CONVERSATION.REPLYBOX.DISMISS_REPLY')" v-tooltip="$t('CONVERSATION.REPLYBOX.DISMISS_REPLY')"
color-scheme="secondary" ghost
icon="dismiss" xs
variant="clear" slate
size="tiny" icon="i-lucide-x"
class="flex-shrink-0" @click.stop="emit('dismiss')"
@click.stop="$emit('dismiss')"
/> />
</div> </div>
</template> </template>
@@ -12,7 +12,12 @@ import { ref, computed, onMounted } from 'vue';
import { useVuelidate } from '@vuelidate/core'; import { useVuelidate } from '@vuelidate/core';
import { requiredIf } from '@vuelidate/validators'; import { requiredIf } from '@vuelidate/validators';
import NextButton from 'dashboard/components-next/button/Button.vue';
export default { export default {
components: {
NextButton,
},
props: { props: {
template: { template: {
type: Object, type: Object,
@@ -114,78 +119,47 @@ export default {
readonly readonly
class="template-input" class="template-input"
/> />
<div v-if="variables" class="template__variables-container"> <div v-if="variables" class="p-2.5">
<p class="variables-label"> <p class="text-sm font-semibold mb-2.5">
{{ $t('WHATSAPP_TEMPLATES.PARSER.VARIABLES_LABEL') }} {{ $t('WHATSAPP_TEMPLATES.PARSER.VARIABLES_LABEL') }}
</p> </p>
<div <div
v-for="(variable, key) in processedParams" v-for="(variable, key) in processedParams"
:key="key" :key="key"
class="template__variable-item" class="items-center flex mb-2.5"
> >
<span class="variable-label"> <span
class="bg-n-alpha-black2 text-n-slate-12 inline-block rounded-md text-xs py-2.5 px-6"
>
{{ key }} {{ key }}
</span> </span>
<woot-input <woot-input
v-model="processedParams[key]" v-model="processedParams[key]"
type="text" type="text"
class="variable-input" class="flex-1 text-sm ml-2.5"
:styles="{ marginBottom: 0 }" :styles="{ marginBottom: 0 }"
/> />
</div> </div>
<p v-if="v$.$dirty && v$.$invalid" class="error"> <p
v-if="v$.$dirty && v$.$invalid"
class="bg-n-ruby-9/20 rounded-md text-n-ruby-9 p-2.5 text-center"
>
{{ $t('WHATSAPP_TEMPLATES.PARSER.FORM_ERROR_MESSAGE') }} {{ $t('WHATSAPP_TEMPLATES.PARSER.FORM_ERROR_MESSAGE') }}
</p> </p>
</div> </div>
<footer> <footer class="flex justify-end gap-2">
<woot-button variant="smooth" @click="resetTemplate"> <NextButton
{{ $t('WHATSAPP_TEMPLATES.PARSER.GO_BACK_LABEL') }} faded
</woot-button> slate
<woot-button type="button" @click="sendMessage"> type="reset"
{{ $t('WHATSAPP_TEMPLATES.PARSER.SEND_MESSAGE_LABEL') }} :label="$t('WHATSAPP_TEMPLATES.PARSER.GO_BACK_LABEL')"
</woot-button> @click="resetTemplate"
/>
<NextButton
type="button"
:label="$t('WHATSAPP_TEMPLATES.PARSER.SEND_MESSAGE_LABEL')"
@click="sendMessage"
/>
</footer> </footer>
</div> </div>
</template> </template>
<style scoped lang="scss">
.template__variables-container {
@apply p-2.5;
}
.variables-label {
@apply text-sm font-semibold mb-2.5;
}
.template__variable-item {
@apply items-center flex mb-2.5;
.label {
@apply text-xs;
}
.variable-input {
@apply flex-1 text-sm ml-2.5;
}
.variable-label {
@apply bg-slate-75 dark:bg-slate-700 text-slate-700 dark:text-slate-100 inline-block rounded-md text-xs py-2.5 px-6;
}
}
footer {
@apply flex justify-end;
button {
@apply ml-2.5;
}
}
.error {
@apply bg-red-100 dark:bg-red-100 rounded-md text-red-800 dark:text-red-800 p-2.5 text-center;
}
.template-input {
@apply bg-slate-25 dark:bg-slate-900 text-slate-700 dark:text-slate-100;
}
</style>
@@ -43,48 +43,56 @@ export default {
<template> <template>
<div class="w-full"> <div class="w-full">
<div class="gap-1 templates__list-search"> <div
<fluent-icon icon="search" class="search-icon" size="16" /> class="gap-1 bg-n-alpha-black2 items-center flex mb-2.5 py-0 px-2.5 rounded-lg outline outline-1 outline-n-weak hover:outline-n-slate-6 dark:hover:outline-n-slate-6 focus-within:outline-n-brand dark:focus-within:outline-n-brand"
>
<fluent-icon icon="search" class="text-n-slate-12" size="16" />
<input <input
v-model="query" v-model="query"
type="search" type="search"
:placeholder="$t('WHATSAPP_TEMPLATES.PICKER.SEARCH_PLACEHOLDER')" :placeholder="$t('WHATSAPP_TEMPLATES.PICKER.SEARCH_PLACEHOLDER')"
class="templates__search-input" class="reset-base w-full h-9 bg-transparent text-n-slate-12 !text-sm !outline-0"
/> />
</div> </div>
<div class="template__list-container"> <div
class="bg-n-background outline-n-container outline outline-1 rounded-lg max-h-[18.75rem] overflow-y-auto p-2.5"
>
<div v-for="(template, i) in filteredTemplateMessages" :key="template.id"> <div v-for="(template, i) in filteredTemplateMessages" :key="template.id">
<button <button
class="template__list-item" class="rounded-lg cursor-pointer block p-2.5 text-left w-full hover:bg-n-alpha-2 dark:hover:bg-n-solid-2"
@click="$emit('onSelect', template)" @click="$emit('onSelect', template)"
> >
<div> <div>
<div class="flex items-center justify-between mb-2.5"> <div class="flex items-center justify-between mb-2.5">
<p class="label-title"> <p class="text-sm">
{{ template.name }} {{ template.name }}
</p> </p>
<span <span
class="inline-block px-2 py-1 text-xs leading-none bg-white rounded-sm cursor-default dark:bg-slate-700 text-slate-800 dark:text-slate-100" class="inline-block px-2 py-1 text-xs leading-none bg-n-slate-3 rounded-lg cursor-default text-n-slate-12"
> >
{{ $t('WHATSAPP_TEMPLATES.PICKER.LABELS.LANGUAGE') }} : {{ $t('WHATSAPP_TEMPLATES.PICKER.LABELS.LANGUAGE') }} :
{{ template.language }} {{ template.language }}
</span> </span>
</div> </div>
<div> <div>
<p class="strong"> <p class="font-medium">
{{ $t('WHATSAPP_TEMPLATES.PICKER.LABELS.TEMPLATE_BODY') }} {{ $t('WHATSAPP_TEMPLATES.PICKER.LABELS.TEMPLATE_BODY') }}
</p> </p>
<p class="label-body">{{ getTemplatebody(template) }}</p> <p class="label-body">{{ getTemplatebody(template) }}</p>
</div> </div>
<div class="label-category"> <div class="mt-5">
<p class="strong"> <p class="font-medium">
{{ $t('WHATSAPP_TEMPLATES.PICKER.LABELS.CATEGORY') }} {{ $t('WHATSAPP_TEMPLATES.PICKER.LABELS.CATEGORY') }}
</p> </p>
<p>{{ template.category }}</p> <p>{{ template.category }}</p>
</div> </div>
</div> </div>
</button> </button>
<hr v-if="i != filteredTemplateMessages.length - 1" :key="`hr-${i}`" /> <hr
v-if="i != filteredTemplateMessages.length - 1"
:key="`hr-${i}`"
class="border-b border-solid border-n-weak my-2.5 mx-auto max-w-[95%]"
/>
</div> </div>
<div v-if="!filteredTemplateMessages.length"> <div v-if="!filteredTemplateMessages.length">
<p> <p>
@@ -97,46 +105,7 @@ export default {
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
.templates__list-search { .label-body {
@apply items-center flex bg-slate-25 dark:bg-slate-900 mb-2.5 py-0 px-2.5 rounded-md border border-solid border-slate-100 dark:border-slate-700; font-family: monospace;
.search-icon {
@apply text-slate-400 dark:text-slate-300;
}
.templates__search-input {
@apply bg-transparent border-0 text-xs h-9 m-0;
}
}
.template__list-container {
@apply bg-slate-25 dark:bg-slate-900 rounded-md max-h-[18.75rem] overflow-y-auto p-2.5;
.template__list-item {
@apply rounded-lg cursor-pointer block p-2.5 text-left w-full hover:bg-woot-50 dark:hover:bg-slate-800;
.label-title {
@apply text-sm;
}
.label-category {
@apply mt-5;
span {
@apply text-sm font-semibold;
}
}
.label-body {
font-family: monospace;
}
}
}
.strong {
@apply text-xs font-semibold;
}
hr {
@apply border-b border-solid border-slate-100 dark:border-slate-700 my-2.5 mx-auto max-w-[95%];
} }
</style> </style>
@@ -22,6 +22,14 @@ const filterTypes = [
filterOperators: OPERATOR_TYPES_2, filterOperators: OPERATOR_TYPES_2,
attributeModel: 'standard', attributeModel: 'standard',
}, },
{
attributeKey: 'priority',
attributeI18nKey: 'PRIORITY',
inputType: 'multi_select',
dataType: 'text',
filterOperators: OPERATOR_TYPES_1,
attributeModel: 'standard',
},
{ {
attributeKey: 'inbox_id', attributeKey: 'inbox_id',
attributeI18nKey: 'INBOX_NAME', attributeI18nKey: 'INBOX_NAME',
@@ -166,188 +166,190 @@ onMounted(() => {
</script> </script>
<template> <template>
<woot-modal <Teleport to="body">
v-model:show="show" <woot-modal
full-width v-model:show="show"
:show-close-button="false" full-width
:on-close="onClose" :show-close-button="false"
> :on-close="onClose"
<div
class="bg-n-background flex flex-col h-[inherit] w-[inherit] overflow-hidden select-none"
@click="onClose"
> >
<header <div
class="z-10 flex items-center justify-between w-full h-16 px-6 py-2 bg-n-background border-b border-n-weak" class="bg-n-background flex flex-col h-[inherit] w-[inherit] overflow-hidden select-none"
@click.stop @click="onClose"
> >
<div <header
v-if="senderDetails" class="z-10 flex items-center justify-between w-full h-16 px-6 py-2 bg-n-background border-b border-n-weak"
class="flex items-center min-w-[15rem] shrink-0" @click.stop
> >
<Thumbnail
v-if="senderDetails.avatar"
:username="senderDetails.name"
:src="senderDetails.avatar"
class="flex-shrink-0"
/>
<div class="flex flex-col ml-2 rtl:ml-0 rtl:mr-2 overflow-hidden">
<h3 class="text-base leading-5 m-0 font-medium">
<span
class="overflow-hidden text-n-slate-12 whitespace-nowrap text-ellipsis"
>
{{ senderDetails.name }}
</span>
</h3>
<span
class="text-xs text-n-slate-11 whitespace-nowrap text-ellipsis"
>
{{ readableTime }}
</span>
</div>
</div>
<div
class="flex-1 mx-2 px-2 truncate text-sm font-medium text-center text-n-slate-12"
>
<span v-dompurify-html="fileNameFromDataUrl" class="truncate" />
</div>
<div class="flex items-center gap-2 ml-2 shrink-0">
<NextButton
v-if="isImage"
icon="i-lucide-zoom-in"
slate
ghost
@click="onZoom(0.1)"
/>
<NextButton
v-if="isImage"
icon="i-lucide-zoom-out"
slate
ghost
@click="onZoom(-0.1)"
/>
<NextButton
v-if="isImage"
icon="i-lucide-rotate-ccw"
slate
ghost
@click="onRotate('counter-clockwise')"
/>
<NextButton
v-if="isImage"
icon="i-lucide-rotate-cw"
slate
ghost
@click="onRotate('clockwise')"
/>
<NextButton
icon="i-lucide-download"
slate
ghost
:is-loading="isDownloading"
:disabled="isDownloading"
@click="onClickDownload"
/>
<NextButton icon="i-lucide-x" slate ghost @click="onClose" />
</div>
</header>
<main class="flex items-stretch flex-1 h-full overflow-hidden">
<div class="flex items-center justify-center w-16 shrink-0">
<NextButton
v-if="hasMoreThanOneAttachment"
icon="i-lucide-chevron-left"
class="z-10"
blue
faded
lg
:disabled="activeImageIndex === 0"
@click.stop="
onClickChangeAttachment(
allAttachments[activeImageIndex - 1],
activeImageIndex - 1
)
"
/>
</div>
<div class="flex-1 flex items-center justify-center overflow-hidden">
<div <div
v-if="isImage" v-if="senderDetails"
:style="imageWrapperStyle" class="flex items-center min-w-[15rem] shrink-0"
class="flex items-center justify-center origin-center"
:class="{
// Adjust dimensions when rotated 90/270 degrees to maintain visibility
// and prevent image from overflowing container in different aspect ratios
'w-[calc(100dvh-8rem)] h-[calc(100dvw-7rem)]':
activeImageRotation % 180 !== 0,
'size-full': activeImageRotation % 180 === 0,
}"
> >
<img <Thumbnail
ref="imageRef" v-if="senderDetails.avatar"
:key="activeAttachment.message_id" :username="senderDetails.name"
:src="activeAttachment.data_url" :src="senderDetails.avatar"
:style="imageStyle" class="flex-shrink-0"
class="max-h-full max-w-full object-contain duration-100 ease-in-out transform select-none" />
@click.stop <div class="flex flex-col ml-2 rtl:ml-0 rtl:mr-2 overflow-hidden">
@dblclick.stop="onDoubleClickZoomImage" <h3 class="text-base leading-5 m-0 font-medium">
@wheel.prevent.stop="onWheelImageZoom" <span
@mousemove="onMouseMove" class="overflow-hidden text-n-slate-12 whitespace-nowrap text-ellipsis"
@mouseleave="onMouseLeave" >
{{ senderDetails.name }}
</span>
</h3>
<span
class="text-xs text-n-slate-11 whitespace-nowrap text-ellipsis"
>
{{ readableTime }}
</span>
</div>
</div>
<div
class="flex-1 mx-2 px-2 truncate text-sm font-medium text-center text-n-slate-12"
>
<span v-dompurify-html="fileNameFromDataUrl" class="truncate" />
</div>
<div class="flex items-center gap-2 ml-2 shrink-0">
<NextButton
v-if="isImage"
icon="i-lucide-zoom-in"
slate
ghost
@click="onZoom(0.1)"
/>
<NextButton
v-if="isImage"
icon="i-lucide-zoom-out"
slate
ghost
@click="onZoom(-0.1)"
/>
<NextButton
v-if="isImage"
icon="i-lucide-rotate-ccw"
slate
ghost
@click="onRotate('counter-clockwise')"
/>
<NextButton
v-if="isImage"
icon="i-lucide-rotate-cw"
slate
ghost
@click="onRotate('clockwise')"
/>
<NextButton
icon="i-lucide-download"
slate
ghost
:is-loading="isDownloading"
:disabled="isDownloading"
@click="onClickDownload"
/>
<NextButton icon="i-lucide-x" slate ghost @click="onClose" />
</div>
</header>
<main class="flex items-stretch flex-1 h-full overflow-hidden">
<div class="flex items-center justify-center w-16 shrink-0">
<NextButton
v-if="hasMoreThanOneAttachment"
icon="i-lucide-chevron-left"
class="z-10"
blue
faded
lg
:disabled="activeImageIndex === 0"
@click.stop="
onClickChangeAttachment(
allAttachments[activeImageIndex - 1],
activeImageIndex - 1
)
"
/> />
</div> </div>
<video <div class="flex-1 flex items-center justify-center overflow-hidden">
v-if="isVideo" <div
:key="activeAttachment.message_id" v-if="isImage"
:src="activeAttachment.data_url" :style="imageWrapperStyle"
controls class="flex items-center justify-center origin-center"
playsInline :class="{
class="max-h-full max-w-full object-contain" // Adjust dimensions when rotated 90/270 degrees to maintain visibility
@click.stop // and prevent image from overflowing container in different aspect ratios
/> 'w-[calc(100dvh-8rem)] h-[calc(100dvw-7rem)]':
activeImageRotation % 180 !== 0,
'size-full': activeImageRotation % 180 === 0,
}"
>
<img
ref="imageRef"
:key="activeAttachment.message_id"
:src="activeAttachment.data_url"
:style="imageStyle"
class="max-h-full max-w-full object-contain duration-100 ease-in-out transform select-none"
@click.stop
@dblclick.stop="onDoubleClickZoomImage"
@wheel.prevent.stop="onWheelImageZoom"
@mousemove="onMouseMove"
@mouseleave="onMouseLeave"
/>
</div>
<audio <video
v-if="isAudio" v-if="isVideo"
:key="activeAttachment.message_id" :key="activeAttachment.message_id"
controls :src="activeAttachment.data_url"
class="w-full max-w-md" controls
@click.stop playsInline
> class="max-h-full max-w-full object-contain"
<source :src="`${activeAttachment.data_url}?t=${Date.now()}`" /> @click.stop
</audio> />
</div>
<div class="flex items-center justify-center w-16 shrink-0"> <audio
<NextButton v-if="isAudio"
v-if="hasMoreThanOneAttachment" :key="activeAttachment.message_id"
icon="i-lucide-chevron-right" controls
class="z-10" class="w-full max-w-md"
blue @click.stop
faded >
lg <source :src="`${activeAttachment.data_url}?t=${Date.now()}`" />
:disabled="activeImageIndex === allAttachments.length - 1" </audio>
@click.stop=" </div>
onClickChangeAttachment(
allAttachments[activeImageIndex + 1],
activeImageIndex + 1
)
"
/>
</div>
</main>
<footer <div class="flex items-center justify-center w-16 shrink-0">
class="z-10 flex items-center justify-center h-12 border-t border-n-weak" <NextButton
> v-if="hasMoreThanOneAttachment"
<div icon="i-lucide-chevron-right"
class="rounded-md flex items-center justify-center px-3 py-1 bg-n-slate-3 text-n-slate-12 text-sm font-medium" class="z-10"
blue
faded
lg
:disabled="activeImageIndex === allAttachments.length - 1"
@click.stop="
onClickChangeAttachment(
allAttachments[activeImageIndex + 1],
activeImageIndex + 1
)
"
/>
</div>
</main>
<footer
class="z-10 flex items-center justify-center h-12 border-t border-n-weak"
> >
{{ `${activeImageIndex + 1} / ${allAttachments.length}` }} <div
</div> class="rounded-md flex items-center justify-center px-3 py-1 bg-n-slate-3 text-n-slate-12 text-sm font-medium"
</footer> >
</div> {{ `${activeImageIndex + 1} / ${allAttachments.length}` }}
</woot-modal> </div>
</footer>
</div>
</woot-modal>
</Teleport>
</template> </template>
@@ -3,6 +3,7 @@ import { ref, computed } from 'vue';
import wootConstants from 'dashboard/constants/globals'; import wootConstants from 'dashboard/constants/globals';
import SLAEventItem from './SLAEventItem.vue'; import SLAEventItem from './SLAEventItem.vue';
import Button from 'dashboard/components-next/button/Button.vue';
const props = defineProps({ const props = defineProps({
slaMissedEvents: { slaMissedEvents: {
@@ -60,20 +61,19 @@ const toggleShowAllNRT = () => {
v-if="shouldShowMoreNRTButton" v-if="shouldShowMoreNRTButton"
class="flex flex-col items-end w-full" class="flex flex-col items-end w-full"
> >
<woot-button <Button
size="small" link
:icon="!shouldShowAllNrts ? 'plus-sign' : ''" xs
variant="link" slate
color-scheme="secondary" class="hover:!no-underline"
class="hover:!no-underline !gap-1 hover:!bg-transparent dark:hover:!bg-transparent" :icon="!shouldShowAllNrts ? 'i-lucide-plus' : ''"
@click="toggleShowAllNRT" :label="
>
{{
shouldShowAllNrts shouldShowAllNrts
? $t('SLA.EVENTS.HIDE', { count: nrtMisses.length }) ? $t('SLA.EVENTS.HIDE', { count: nrtMisses.length })
: $t('SLA.EVENTS.SHOW_MORE', { count: nrtMisses.length }) : $t('SLA.EVENTS.SHOW_MORE', { count: nrtMisses.length })
}} "
</woot-button> @click="toggleShowAllNRT"
/>
</div> </div>
</template> </template>
</SLAEventItem> </SLAEventItem>
@@ -2,11 +2,13 @@
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue'; import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
import Spinner from 'shared/components/Spinner.vue'; import Spinner from 'shared/components/Spinner.vue';
import NextButton from 'dashboard/components-next/button/Button.vue';
export default { export default {
components: { components: {
Thumbnail, Thumbnail,
Spinner, Spinner,
NextButton,
}, },
props: { props: {
selectedInboxes: { selectedInboxes: {
@@ -94,13 +96,7 @@ export default {
</div> </div>
<div class="flex items-center justify-between header"> <div class="flex items-center justify-between header">
<span>{{ $t('BULK_ACTION.AGENT_SELECT_LABEL') }}</span> <span>{{ $t('BULK_ACTION.AGENT_SELECT_LABEL') }}</span>
<woot-button <NextButton ghost xs slate icon="i-lucide-x" @click="onClose" />
size="tiny"
variant="clear"
color-scheme="secondary"
icon="dismiss"
@click="onClose"
/>
</div> </div>
<div class="container"> <div class="container">
<div <div
@@ -121,7 +117,7 @@ export default {
v-model="query" v-model="query"
type="search" type="search"
:placeholder="$t('BULK_ACTION.SEARCH_INPUT_PLACEHOLDER')" :placeholder="$t('BULK_ACTION.SEARCH_INPUT_PLACEHOLDER')"
class="agent--search_input" class="reset-base !outline-0 !text-sm agent--search_input"
/> />
</div> </div>
</li> </li>
@@ -161,21 +157,21 @@ export default {
}} }}
</p> </p>
<div class="agent-confirmation-actions"> <div class="agent-confirmation-actions">
<woot-button <NextButton
color-scheme="primary" faded
variant="smooth" sm
slate
type="reset"
:label="$t('BULK_ACTION.GO_BACK_LABEL')"
@click="goBack" @click="goBack"
> />
{{ $t('BULK_ACTION.GO_BACK_LABEL') }} <NextButton
</woot-button> sm
<woot-button type="submit"
color-scheme="primary" :label="$t('BULK_ACTION.YES')"
variant="flat"
:is-loading="uiFlags.isUpdating" :is-loading="uiFlags.isUpdating"
@click="submit" @click="submit"
> />
{{ $t('BULK_ACTION.YES') }}
</woot-button>
</div> </div>
</div> </div>
</div> </div>
@@ -1,7 +1,11 @@
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import NextButton from 'dashboard/components-next/button/Button.vue';
export default { export default {
components: {
NextButton,
},
emits: ['update', 'close', 'assign'], emits: ['update', 'close', 'assign'],
data() { data() {
return { return {
@@ -40,13 +44,7 @@ export default {
</div> </div>
<div class="flex items-center justify-between header"> <div class="flex items-center justify-between header">
<span>{{ $t('BULK_ACTION.LABELS.ASSIGN_LABELS') }}</span> <span>{{ $t('BULK_ACTION.LABELS.ASSIGN_LABELS') }}</span>
<woot-button <NextButton ghost xs slate icon="i-lucide-x" @click="onClose" />
size="tiny"
variant="clear"
color-scheme="secondary"
icon="dismiss"
@click="onClose"
/>
</div> </div>
<div class="labels-list"> <div class="labels-list">
<header class="labels-list__header"> <header class="labels-list__header">
@@ -58,7 +56,7 @@ export default {
v-model="query" v-model="query"
type="search" type="search"
:placeholder="$t('BULK_ACTION.SEARCH_INPUT_PLACEHOLDER')" :placeholder="$t('BULK_ACTION.SEARCH_INPUT_PLACEHOLDER')"
class="label--search_input" class="reset-base !outline-0 !text-sm label--search_input"
/> />
</div> </div>
</header> </header>
@@ -91,15 +89,13 @@ export default {
</li> </li>
</ul> </ul>
<footer class="labels-list__footer"> <footer class="labels-list__footer">
<woot-button <NextButton
size="small" sm
is-expanded type="submit"
color-scheme="primary" :label="$t('BULK_ACTION.LABELS.ASSIGN_SELECTED_LABELS')"
:disabled="!selectedLabels.length" :disabled="!selectedLabels.length"
@click="$emit('assign', selectedLabels)" @click="$emit('assign', selectedLabels)"
> />
<span>{{ $t('BULK_ACTION.LABELS.ASSIGN_SELECTED_LABELS') }}</span>
</woot-button>
</footer> </footer>
</div> </div>
</div> </div>
@@ -1,6 +1,11 @@
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import NextButton from 'dashboard/components-next/button/Button.vue';
export default { export default {
components: {
NextButton,
},
emits: ['assignTeam', 'close'], emits: ['assignTeam', 'close'],
data() { data() {
@@ -40,13 +45,7 @@ export default {
</div> </div>
<div class="flex items-center justify-between header"> <div class="flex items-center justify-between header">
<span>{{ $t('BULK_ACTION.TEAMS.TEAM_SELECT_LABEL') }}</span> <span>{{ $t('BULK_ACTION.TEAMS.TEAM_SELECT_LABEL') }}</span>
<woot-button <NextButton ghost xs slate icon="i-lucide-x" @click="onClose" />
size="tiny"
variant="clear"
color-scheme="secondary"
icon="dismiss"
@click="onClose"
/>
</div> </div>
<div class="container"> <div class="container">
<div class="team__list-container"> <div class="team__list-container">
@@ -60,7 +59,7 @@ export default {
v-model="query" v-model="query"
type="search" type="search"
:placeholder="$t('BULK_ACTION.SEARCH_INPUT_PLACEHOLDER')" :placeholder="$t('BULK_ACTION.SEARCH_INPUT_PLACEHOLDER')"
class="agent--search_input" class="reset-base !outline-0 !text-sm agent--search_input"
/> />
</div> </div>
</li> </li>
@@ -4,6 +4,7 @@ import { ref } from 'vue';
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue'; import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue'; import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
import Button from 'dashboard/components-next/button/Button.vue';
const props = defineProps({ const props = defineProps({
showResolve: { showResolve: {
@@ -25,9 +26,9 @@ const emit = defineEmits(['update', 'close']);
const { t } = useI18n(); const { t } = useI18n();
const actions = ref([ const actions = ref([
{ icon: 'checkmark', key: 'resolved' }, { icon: 'i-lucide-check', key: 'resolved' },
{ icon: 'arrow-redo', key: 'open' }, { icon: 'i-lucide-redo', key: 'open' },
{ icon: 'send-clock', key: 'snoozed' }, { icon: 'i-lucide-alarm-clock', key: 'snoozed' },
]); ]);
const updateConversations = key => { const updateConversations = key => {
@@ -85,28 +86,21 @@ const actionLabel = key => {
<span class="text-sm font-medium text-slate-600 dark:text-slate-100"> <span class="text-sm font-medium text-slate-600 dark:text-slate-100">
{{ $t('BULK_ACTION.UPDATE.CHANGE_STATUS') }} {{ $t('BULK_ACTION.UPDATE.CHANGE_STATUS') }}
</span> </span>
<woot-button <Button ghost xs slate icon="i-lucide-x" @click="onClose" />
size="tiny"
variant="clear"
color-scheme="secondary"
icon="dismiss"
@click="onClose"
/>
</div> </div>
<div class="px-2.5 pt-0 pb-2.5"> <div class="px-2.5 pt-0 pb-2.5">
<WootDropdownMenu class="m-0 list-none"> <WootDropdownMenu class="m-0 list-none">
<template v-for="action in actions"> <template v-for="action in actions">
<WootDropdownItem v-if="showAction(action.key)" :key="action.key"> <WootDropdownItem v-if="showAction(action.key)" :key="action.key">
<woot-button <Button
variant="clear" ghost
color-scheme="secondary" sm
size="small" slate
class="!w-full ltr:!justify-start rtl:!justify-end"
:icon="action.icon" :icon="action.icon"
class="hover:!bg-n-slate-3 dark:hover:!bg-n-solid-3" :label="actionLabel(action.key)"
@click="updateConversations(action.key)" @click="updateConversations(action.key)"
> />
{{ actionLabel(action.key) }}
</woot-button>
</WootDropdownItem> </WootDropdownItem>
</template> </template>
</WootDropdownMenu> </WootDropdownMenu>
@@ -9,6 +9,7 @@ import validations from './validations';
import { parseLinearAPIErrorResponse } from 'dashboard/store/utils/api'; import { parseLinearAPIErrorResponse } from 'dashboard/store/utils/api';
import SearchableDropdown from './SearchableDropdown.vue'; import SearchableDropdown from './SearchableDropdown.vue';
import { LINEAR_EVENTS } from 'dashboard/helper/AnalyticsHelper/events'; import { LINEAR_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
import Button from 'dashboard/components-next/button/Button.vue';
const props = defineProps({ const props = defineProps({
conversationId: { conversationId: {
@@ -248,20 +249,20 @@ onMounted(getTeams);
/> />
</div> </div>
<div class="flex items-center justify-end w-full gap-2 mt-8"> <div class="flex items-center justify-end w-full gap-2 mt-8">
<woot-button <Button
class="px-4 rounded-xl button clear outline-woot-200/50 outline" faded
slate
type="reset"
:label="$t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.CANCEL')"
@click.prevent="onClose" @click.prevent="onClose"
> />
{{ $t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.CANCEL') }} <Button
</woot-button> type="submit"
<woot-button :label="$t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.CREATE')"
:is-disabled="isSubmitDisabled" :disabled="isSubmitDisabled"
class="px-4 rounded-xl"
:is-loading="isCreating" :is-loading="isCreating"
@click.prevent="createIssue" @click.prevent="createIssue"
> />
{{ $t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.CREATE') }}
</woot-button>
</div> </div>
</div> </div>
</template> </template>
@@ -1,5 +1,7 @@
<script setup> <script setup>
import { inject } from 'vue'; import { inject } from 'vue';
import Button from 'dashboard/components-next/button/Button.vue';
const props = defineProps({ const props = defineProps({
identifier: { identifier: {
type: String, type: String,
@@ -37,30 +39,24 @@ const openIssue = () => {
/> />
<span class="text-xs font-medium text-ash-900">{{ identifier }}</span> <span class="text-xs font-medium text-ash-900">{{ identifier }}</span>
</div> </div>
<div class="flex items-center gap-0.5"> <div class="flex items-center gap-1">
<woot-button <Button
variant="clear" ghost
color-scheme="secondary" xs
class="h-[24px]" slate
icon="i-lucide-unlink"
class="!transition-none"
:is-loading="isUnlinking" :is-loading="isUnlinking"
@click="unlinkIssue" @click="unlinkIssue"
> />
<fluent-icon <Button
v-if="!isUnlinking" ghost
icon="unlink" xs
size="12" slate
type="outline" class="!transition-none"
icon-lib="lucide" icon="i-lucide-arrow-up-right"
/>
</woot-button>
<woot-button
variant="clear"
class="h-[24px]"
color-scheme="secondary"
@click="openIssue" @click="openIssue"
> />
<fluent-icon icon="arrow-up-right" size="14" />
</woot-button>
</div> </div>
</div> </div>
</template> </template>
@@ -8,6 +8,7 @@ import FilterButton from 'dashboard/components/ui/Dropdown/DropdownButton.vue';
import FilterListDropdown from 'dashboard/components/ui/Dropdown/DropdownList.vue'; import FilterListDropdown from 'dashboard/components/ui/Dropdown/DropdownList.vue';
import { parseLinearAPIErrorResponse } from 'dashboard/store/utils/api'; import { parseLinearAPIErrorResponse } from 'dashboard/store/utils/api';
import { LINEAR_EVENTS } from 'dashboard/helper/AnalyticsHelper/events'; import { LINEAR_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
import Button from 'dashboard/components-next/button/Button.vue';
const props = defineProps({ const props = defineProps({
conversationId: { conversationId: {
@@ -129,20 +130,20 @@ const linkIssue = async () => {
</template> </template>
</FilterButton> </FilterButton>
<div class="flex items-center justify-end w-full gap-2 mt-2"> <div class="flex items-center justify-end w-full gap-2 mt-2">
<woot-button <Button
class="px-4 rounded-xl button clear outline-woot-200/50 outline" faded
slate
type="reset"
:label="$t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.CANCEL')"
@click.prevent="onClose" @click.prevent="onClose"
> />
{{ $t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.CANCEL') }} <Button
</woot-button> type="submit"
<woot-button :label="$t('INTEGRATION_SETTINGS.LINEAR.LINK.TITLE')"
:is-disabled="isSubmitDisabled" :disabled="isSubmitDisabled"
class="px-4 rounded-xl"
:is-loading="isLinking" :is-loading="isLinking"
@click.prevent="linkIssue" @click.prevent="linkIssue"
> />
{{ $t('INTEGRATION_SETTINGS.LINEAR.LINK.TITLE') }}
</woot-button>
</div> </div>
</div> </div>
</template> </template>
@@ -9,6 +9,7 @@ import Issue from './Issue.vue';
import { useTrack } from 'dashboard/composables'; import { useTrack } from 'dashboard/composables';
import { LINEAR_EVENTS } from 'dashboard/helper/AnalyticsHelper/events'; import { LINEAR_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
import { parseLinearAPIErrorResponse } from 'dashboard/store/utils/api'; import { parseLinearAPIErrorResponse } from 'dashboard/store/utils/api';
import Button from 'dashboard/components-next/button/Button.vue';
const props = defineProps({ const props = defineProps({
conversationId: { conversationId: {
@@ -100,23 +101,25 @@ onMounted(() => {
<template> <template>
<div class="relative" :class="{ group: linkedIssue }"> <div class="relative" :class="{ group: linkedIssue }">
<woot-button <Button
v-on-clickaway="closeIssue" v-on-clickaway="closeIssue"
v-tooltip="tooltipText" v-tooltip="tooltipText"
variant="clear" sm
color-scheme="secondary" ghost
slate
class="!gap-1"
@click="openIssue" @click="openIssue"
> >
<fluent-icon <fluent-icon
icon="linear" icon="linear"
size="19" size="19"
class="text-[#5E6AD2]" class="text-[#5E6AD2] flex-shrink-0"
view-box="0 0 19 19" view-box="0 0 19 19"
/> />
<span v-if="linkedIssue" class="text-xs font-medium text-ash-800"> <span v-if="linkedIssue" class="text-xs font-medium text-n-slate-11">
{{ linkedIssue.issue.identifier }} {{ linkedIssue.issue.identifier }}
</span> </span>
</woot-button> </Button>
<Issue <Issue
v-if="linkedIssue" v-if="linkedIssue"
:issue="linkedIssue.issue" :issue="linkedIssue.issue"
@@ -3,9 +3,12 @@ import { required } from '@vuelidate/validators';
import { useVuelidate } from '@vuelidate/core'; import { useVuelidate } from '@vuelidate/core';
import Modal from '../../Modal.vue'; import Modal from '../../Modal.vue';
import NextButton from 'dashboard/components-next/button/Button.vue';
export default { export default {
components: { components: {
Modal, Modal,
NextButton,
}, },
props: { props: {
show: { type: Boolean, default: false }, show: { type: Boolean, default: false },
@@ -66,13 +69,20 @@ export default {
:placeholder="confirmPlaceHolderText" :placeholder="confirmPlaceHolderText"
@blur="v$.value.$touch" @blur="v$.value.$touch"
/> />
<div class="button-wrapper"> <div class="flex items-center justify-end gap-2">
<woot-button color-scheme="alert" :is-disabled="v$.value.$invalid"> <NextButton
{{ confirmText }} faded
</woot-button> slate
<woot-button class="clear" @click.prevent="closeModal"> type="reset"
{{ rejectText }} :label="rejectText"
</woot-button> @click.prevent="closeModal"
/>
<NextButton
ruby
type="submit"
:label="confirmText"
:disabled="v$.value.$invalid"
/>
</div> </div>
</form> </form>
</Modal> </Modal>
@@ -1,9 +1,11 @@
<script> <script>
import Modal from '../../Modal.vue'; import Modal from '../../Modal.vue';
import NextButton from 'dashboard/components-next/button/Button.vue';
export default { export default {
components: { components: {
Modal, Modal,
NextButton,
}, },
props: { props: {
title: { title: {
@@ -55,12 +57,8 @@ export default {
<div class="h-auto overflow-auto flex flex-col"> <div class="h-auto overflow-auto flex flex-col">
<woot-modal-header :header-title="title" :header-content="description" /> <woot-modal-header :header-title="title" :header-content="description" />
<div class="flex flex-row justify-end gap-2 py-4 px-6 w-full"> <div class="flex flex-row justify-end gap-2 py-4 px-6 w-full">
<woot-button variant="clear" @click="cancel"> <NextButton faded type="reset" :label="cancelLabel" @click="cancel" />
{{ cancelLabel }} <NextButton type="submit" :label="confirmLabel" @click="confirm" />
</woot-button>
<woot-button @click="confirm">
{{ confirmLabel }}
</woot-button>
</div> </div>
</div> </div>
</Modal> </Modal>
@@ -1,5 +1,6 @@
<script setup> <script setup>
import Modal from '../../Modal.vue'; import Modal from '../../Modal.vue';
import Button from 'dashboard/components-next/button/Button.vue';
defineProps({ defineProps({
onClose: { type: Function, default: () => {} }, onClose: { type: Function, default: () => {} },
@@ -22,23 +23,8 @@ const show = defineModel('show', { type: Boolean, default: false });
:header-content-value="messageValue" :header-content-value="messageValue"
/> />
<div class="flex items-center justify-end gap-2 p-8"> <div class="flex items-center justify-end gap-2 p-8">
<woot-button variant="clear" class="action-button" @click="onClose"> <Button faded slate type="reset" :label="rejectText" @click="onClose" />
{{ rejectText }} <Button ruby type="submit" :label="confirmText" @click="onConfirm" />
</woot-button>
<woot-button
color-scheme="alert"
class="action-button"
variant="smooth"
@click="onConfirm"
>
{{ confirmText }}
</woot-button>
</div> </div>
</Modal> </Modal>
</template> </template>
<style lang="scss" scoped>
.action-button {
max-width: var(--space-giga);
}
</style>
@@ -21,6 +21,7 @@ export const getAttributeInputType = (key, allCustomAttributes) => {
const customAttribute = allCustomAttributes.find( const customAttribute = allCustomAttributes.find(
attr => attr.attribute_key === key attr => attr.attribute_key === key
); );
const { attribute_display_type } = customAttribute; const { attribute_display_type } = customAttribute;
const filterInputTypes = generateCustomAttributesInputType( const filterInputTypes = generateCustomAttributesInputType(
attribute_display_type attribute_display_type
@@ -68,10 +69,22 @@ const getValuesForCountries = (values, countries) => {
})); }));
}; };
const getValuesForPriority = (values, priority) => {
return priority.filter(option => values.includes(option.id));
};
export const getValuesForFilter = (filter, params) => { export const getValuesForFilter = (filter, params) => {
const { attribute_key, values } = filter; const { attribute_key, values } = filter;
const { languages, countries, agents, inboxes, teams, campaigns, labels } = const {
params; languages,
countries,
agents,
inboxes,
teams,
campaigns,
labels,
priority,
} = params;
switch (attribute_key) { switch (attribute_key) {
case 'status': case 'status':
return getValuesForStatus(values); return getValuesForStatus(values);
@@ -83,15 +96,14 @@ export const getValuesForFilter = (filter, params) => {
return getValuesName(values, teams, 'id', 'name'); return getValuesName(values, teams, 'id', 'name');
case 'campaign_id': case 'campaign_id':
return getValuesName(values, campaigns, 'id', 'title'); return getValuesName(values, campaigns, 'id', 'title');
case 'labels': { case 'labels':
return getValuesForLabels(values, labels); return getValuesForLabels(values, labels);
} case 'priority':
case 'browser_language': { return getValuesForPriority(values, priority);
case 'browser_language':
return getValuesForLanguages(values, languages); return getValuesForLanguages(values, languages);
} case 'country_code':
case 'country_code': {
return getValuesForCountries(values, countries); return getValuesForCountries(values, countries);
}
default: default:
return { id: values[0], name: values[0] }; return { id: values[0], name: values[0] };
} }
@@ -100,9 +112,9 @@ export const getValuesForFilter = (filter, params) => {
export const generateValuesForEditCustomViews = (filter, params) => { export const generateValuesForEditCustomViews = (filter, params) => {
const { attribute_key, filter_operator, values } = filter; const { attribute_key, filter_operator, values } = filter;
const { filterTypes, allCustomAttributes } = params; const { filterTypes, allCustomAttributes } = params;
const inboxType = getInputType(attribute_key, filter_operator, filterTypes); const inputType = getInputType(attribute_key, filter_operator, filterTypes);
if (inboxType === undefined) { if (inputType === undefined) {
const filterInputTypes = getAttributeInputType( const filterInputTypes = getAttributeInputType(
attribute_key, attribute_key,
allCustomAttributes allCustomAttributes
@@ -112,7 +124,7 @@ export const generateValuesForEditCustomViews = (filter, params) => {
: { id: values[0], name: values[0] }; : { id: values[0], name: values[0] };
} }
return inboxType === 'multi_select' || inboxType === 'search_select' return inputType === 'multi_select' || inputType === 'search_select'
? getValuesForFilter(filter, params) ? getValuesForFilter(filter, params)
: values[0].toString(); : values[0].toString();
}; };
@@ -295,7 +295,27 @@
"CONVERSATION_INFO": "Conversation Information", "CONVERSATION_INFO": "Conversation Information",
"CONTACT_ATTRIBUTES": "Contact Attributes", "CONTACT_ATTRIBUTES": "Contact Attributes",
"PREVIOUS_CONVERSATION": "Previous Conversations", "PREVIOUS_CONVERSATION": "Previous Conversations",
"MACROS": "Macros" "MACROS": "Macros",
"SHOPIFY_ORDERS": "Shopify Orders"
},
"SHOPIFY": {
"ORDER_ID": "Order #{id}",
"ERROR": "Error loading orders",
"NO_SHOPIFY_ORDERS": "No orders found",
"FINANCIAL_STATUS": {
"PENDING": "Pending",
"AUTHORIZED": "Authorized",
"PARTIALLY_PAID": "Partially Paid",
"PAID": "Paid",
"PARTIALLY_REFUNDED": "Partially Refunded",
"REFUNDED": "Refunded",
"VOIDED": "Voided"
},
"FULFILLMENT_STATUS": {
"FULFILLED": "Fulfilled",
"PARTIALLY_FULFILLED": "Partially Fulfilled",
"UNFULFILLED": "Unfulfilled"
}
} }
}, },
"CONVERSATION_CUSTOM_ATTRIBUTES": { "CONVERSATION_CUSTOM_ATTRIBUTES": {
@@ -1,5 +1,20 @@
{ {
"INTEGRATION_SETTINGS": { "INTEGRATION_SETTINGS": {
"SHOPIFY": {
"DELETE": {
"TITLE": "Delete Shopify Integration",
"MESSAGE": "Are you sure you want to delete the Shopify integration?"
},
"STORE_URL": {
"TITLE": "Connect Shopify Store",
"LABEL": "Store URL",
"PLACEHOLDER": "your-store.myshopify.com",
"HELP": "Enter your Shopify store's myshopify.com URL",
"CANCEL": "Cancel",
"SUBMIT": "Connect Store"
},
"ERROR": "There was an error connecting to Shopify. Please try again or contact support if the issue persists."
},
"HEADER": "Integrations", "HEADER": "Integrations",
"DESCRIPTION": "Chatwoot integrates with multiple tools and services to improve your team's efficiency. Explore the list below to configure your favorite apps.", "DESCRIPTION": "Chatwoot integrates with multiple tools and services to improve your team's efficiency. Explore the list below to configure your favorite apps.",
"LEARN_MORE": "Learn more about integrations", "LEARN_MORE": "Learn more about integrations",
@@ -119,7 +119,8 @@
"EMPTY_LIST": "No results found" "EMPTY_LIST": "No results found"
}, },
"PAGINATION": { "PAGINATION": {
"RESULTS": "Showing {start} to {end} of {total} results" "RESULTS": "Showing {start} to {end} of {total} results",
"PER_PAGE_TEMPLATE": "{size} / page"
} }
}, },
"AGENT_REPORTS": { "AGENT_REPORTS": {
@@ -295,7 +295,27 @@
"CONVERSATION_INFO": "معلومات المحادثة", "CONVERSATION_INFO": "معلومات المحادثة",
"CONTACT_ATTRIBUTES": "سمات جهة الاتصال", "CONTACT_ATTRIBUTES": "سمات جهة الاتصال",
"PREVIOUS_CONVERSATION": "المحادثات السابقة", "PREVIOUS_CONVERSATION": "المحادثات السابقة",
"MACROS": "ماكروس" "MACROS": "ماكروس",
"SHOPIFY_ORDERS": "Shopify Orders"
},
"SHOPIFY": {
"ORDER_ID": "Order #{id}",
"ERROR": "Error loading orders",
"NO_SHOPIFY_ORDERS": "No orders found",
"FINANCIAL_STATUS": {
"PENDING": "معلق",
"AUTHORIZED": "Authorized",
"PARTIALLY_PAID": "Partially Paid",
"PAID": "Paid",
"PARTIALLY_REFUNDED": "Partially Refunded",
"REFUNDED": "Refunded",
"VOIDED": "Voided"
},
"FULFILLMENT_STATUS": {
"FULFILLED": "Fulfilled",
"PARTIALLY_FULFILLED": "Partially Fulfilled",
"UNFULFILLED": "Unfulfilled"
}
} }
}, },
"CONVERSATION_CUSTOM_ATTRIBUTES": { "CONVERSATION_CUSTOM_ATTRIBUTES": {
@@ -1,5 +1,20 @@
{ {
"INTEGRATION_SETTINGS": { "INTEGRATION_SETTINGS": {
"SHOPIFY": {
"DELETE": {
"TITLE": "Delete Shopify Integration",
"MESSAGE": "Are you sure you want to delete the Shopify integration?"
},
"STORE_URL": {
"TITLE": "Connect Shopify Store",
"LABEL": "Store URL",
"PLACEHOLDER": "your-store.myshopify.com",
"HELP": "Enter your Shopify store's myshopify.com URL",
"CANCEL": "إلغاء",
"SUBMIT": "Connect Store"
},
"ERROR": "There was an error connecting to Shopify. Please try again or contact support if the issue persists."
},
"HEADER": "خيارات الربط", "HEADER": "خيارات الربط",
"DESCRIPTION": "Chatwoot تتكامل مع أدوات وخدمات متعددة لتحسين كفاءة فريقك. استكشف القائمة أدناه لتكوين تطبيقاتك المفضلة.", "DESCRIPTION": "Chatwoot تتكامل مع أدوات وخدمات متعددة لتحسين كفاءة فريقك. استكشف القائمة أدناه لتكوين تطبيقاتك المفضلة.",
"LEARN_MORE": "معرفة المزيد عن التكاملات", "LEARN_MORE": "معرفة المزيد عن التكاملات",
@@ -119,7 +119,8 @@
"EMPTY_LIST": "لم يتم العثور على النتائج" "EMPTY_LIST": "لم يتم العثور على النتائج"
}, },
"PAGINATION": { "PAGINATION": {
"RESULTS": "Showing {start} to {end} of {total} results" "RESULTS": "Showing {start} to {end} of {total} results",
"PER_PAGE_TEMPLATE": "{size} / page"
} }
}, },
"AGENT_REPORTS": { "AGENT_REPORTS": {
@@ -295,7 +295,27 @@
"CONVERSATION_INFO": "Conversation Information", "CONVERSATION_INFO": "Conversation Information",
"CONTACT_ATTRIBUTES": "Contact Attributes", "CONTACT_ATTRIBUTES": "Contact Attributes",
"PREVIOUS_CONVERSATION": "Previous Conversations", "PREVIOUS_CONVERSATION": "Previous Conversations",
"MACROS": "Macros" "MACROS": "Macros",
"SHOPIFY_ORDERS": "Shopify Orders"
},
"SHOPIFY": {
"ORDER_ID": "Order #{id}",
"ERROR": "Error loading orders",
"NO_SHOPIFY_ORDERS": "No orders found",
"FINANCIAL_STATUS": {
"PENDING": "Pending",
"AUTHORIZED": "Authorized",
"PARTIALLY_PAID": "Partially Paid",
"PAID": "Paid",
"PARTIALLY_REFUNDED": "Partially Refunded",
"REFUNDED": "Refunded",
"VOIDED": "Voided"
},
"FULFILLMENT_STATUS": {
"FULFILLED": "Fulfilled",
"PARTIALLY_FULFILLED": "Partially Fulfilled",
"UNFULFILLED": "Unfulfilled"
}
} }
}, },
"CONVERSATION_CUSTOM_ATTRIBUTES": { "CONVERSATION_CUSTOM_ATTRIBUTES": {
@@ -1,5 +1,20 @@
{ {
"INTEGRATION_SETTINGS": { "INTEGRATION_SETTINGS": {
"SHOPIFY": {
"DELETE": {
"TITLE": "Delete Shopify Integration",
"MESSAGE": "Are you sure you want to delete the Shopify integration?"
},
"STORE_URL": {
"TITLE": "Connect Shopify Store",
"LABEL": "Store URL",
"PLACEHOLDER": "your-store.myshopify.com",
"HELP": "Enter your Shopify store's myshopify.com URL",
"CANCEL": "Cancel",
"SUBMIT": "Connect Store"
},
"ERROR": "There was an error connecting to Shopify. Please try again or contact support if the issue persists."
},
"HEADER": "Integrations", "HEADER": "Integrations",
"DESCRIPTION": "Chatwoot integrates with multiple tools and services to improve your team's efficiency. Explore the list below to configure your favorite apps.", "DESCRIPTION": "Chatwoot integrates with multiple tools and services to improve your team's efficiency. Explore the list below to configure your favorite apps.",
"LEARN_MORE": "Learn more about integrations", "LEARN_MORE": "Learn more about integrations",
@@ -119,7 +119,8 @@
"EMPTY_LIST": "No results found" "EMPTY_LIST": "No results found"
}, },
"PAGINATION": { "PAGINATION": {
"RESULTS": "Showing {start} to {end} of {total} results" "RESULTS": "Showing {start} to {end} of {total} results",
"PER_PAGE_TEMPLATE": "{size} / page"
} }
}, },
"AGENT_REPORTS": { "AGENT_REPORTS": {
@@ -295,7 +295,27 @@
"CONVERSATION_INFO": "Conversation Information", "CONVERSATION_INFO": "Conversation Information",
"CONTACT_ATTRIBUTES": "Contact Attributes", "CONTACT_ATTRIBUTES": "Contact Attributes",
"PREVIOUS_CONVERSATION": "Предишни разговори", "PREVIOUS_CONVERSATION": "Предишни разговори",
"MACROS": "Macros" "MACROS": "Macros",
"SHOPIFY_ORDERS": "Shopify Orders"
},
"SHOPIFY": {
"ORDER_ID": "Order #{id}",
"ERROR": "Error loading orders",
"NO_SHOPIFY_ORDERS": "No orders found",
"FINANCIAL_STATUS": {
"PENDING": "Предстоящ",
"AUTHORIZED": "Authorized",
"PARTIALLY_PAID": "Partially Paid",
"PAID": "Paid",
"PARTIALLY_REFUNDED": "Partially Refunded",
"REFUNDED": "Refunded",
"VOIDED": "Voided"
},
"FULFILLMENT_STATUS": {
"FULFILLED": "Fulfilled",
"PARTIALLY_FULFILLED": "Partially Fulfilled",
"UNFULFILLED": "Unfulfilled"
}
} }
}, },
"CONVERSATION_CUSTOM_ATTRIBUTES": { "CONVERSATION_CUSTOM_ATTRIBUTES": {
@@ -1,5 +1,20 @@
{ {
"INTEGRATION_SETTINGS": { "INTEGRATION_SETTINGS": {
"SHOPIFY": {
"DELETE": {
"TITLE": "Delete Shopify Integration",
"MESSAGE": "Are you sure you want to delete the Shopify integration?"
},
"STORE_URL": {
"TITLE": "Connect Shopify Store",
"LABEL": "Store URL",
"PLACEHOLDER": "your-store.myshopify.com",
"HELP": "Enter your Shopify store's myshopify.com URL",
"CANCEL": "Отмени",
"SUBMIT": "Connect Store"
},
"ERROR": "There was an error connecting to Shopify. Please try again or contact support if the issue persists."
},
"HEADER": "Integrations", "HEADER": "Integrations",
"DESCRIPTION": "Chatwoot integrates with multiple tools and services to improve your team's efficiency. Explore the list below to configure your favorite apps.", "DESCRIPTION": "Chatwoot integrates with multiple tools and services to improve your team's efficiency. Explore the list below to configure your favorite apps.",
"LEARN_MORE": "Learn more about integrations", "LEARN_MORE": "Learn more about integrations",
@@ -119,7 +119,8 @@
"EMPTY_LIST": "Няма намерени резултати" "EMPTY_LIST": "Няма намерени резултати"
}, },
"PAGINATION": { "PAGINATION": {
"RESULTS": "Showing {start} to {end} of {total} results" "RESULTS": "Showing {start} to {end} of {total} results",
"PER_PAGE_TEMPLATE": "{size} / page"
} }
}, },
"AGENT_REPORTS": { "AGENT_REPORTS": {
@@ -295,7 +295,27 @@
"CONVERSATION_INFO": "Informació de la conversa", "CONVERSATION_INFO": "Informació de la conversa",
"CONTACT_ATTRIBUTES": "Atributs de contacte", "CONTACT_ATTRIBUTES": "Atributs de contacte",
"PREVIOUS_CONVERSATION": "Converses prèvies", "PREVIOUS_CONVERSATION": "Converses prèvies",
"MACROS": "Macros" "MACROS": "Macros",
"SHOPIFY_ORDERS": "Shopify Orders"
},
"SHOPIFY": {
"ORDER_ID": "Order #{id}",
"ERROR": "Error loading orders",
"NO_SHOPIFY_ORDERS": "No orders found",
"FINANCIAL_STATUS": {
"PENDING": "Pendent",
"AUTHORIZED": "Authorized",
"PARTIALLY_PAID": "Partially Paid",
"PAID": "Paid",
"PARTIALLY_REFUNDED": "Partially Refunded",
"REFUNDED": "Refunded",
"VOIDED": "Voided"
},
"FULFILLMENT_STATUS": {
"FULFILLED": "Fulfilled",
"PARTIALLY_FULFILLED": "Partially Fulfilled",
"UNFULFILLED": "Unfulfilled"
}
} }
}, },
"CONVERSATION_CUSTOM_ATTRIBUTES": { "CONVERSATION_CUSTOM_ATTRIBUTES": {
@@ -1,5 +1,20 @@
{ {
"INTEGRATION_SETTINGS": { "INTEGRATION_SETTINGS": {
"SHOPIFY": {
"DELETE": {
"TITLE": "Delete Shopify Integration",
"MESSAGE": "Are you sure you want to delete the Shopify integration?"
},
"STORE_URL": {
"TITLE": "Connect Shopify Store",
"LABEL": "Store URL",
"PLACEHOLDER": "your-store.myshopify.com",
"HELP": "Enter your Shopify store's myshopify.com URL",
"CANCEL": "Cancel·la",
"SUBMIT": "Connect Store"
},
"ERROR": "There was an error connecting to Shopify. Please try again or contact support if the issue persists."
},
"HEADER": "Integracions", "HEADER": "Integracions",
"DESCRIPTION": "Chatwoot integrates with multiple tools and services to improve your team's efficiency. Explore the list below to configure your favorite apps.", "DESCRIPTION": "Chatwoot integrates with multiple tools and services to improve your team's efficiency. Explore the list below to configure your favorite apps.",
"LEARN_MORE": "Learn more about integrations", "LEARN_MORE": "Learn more about integrations",
@@ -119,7 +119,8 @@
"EMPTY_LIST": "No s'ha trobat agents" "EMPTY_LIST": "No s'ha trobat agents"
}, },
"PAGINATION": { "PAGINATION": {
"RESULTS": "Showing {start} to {end} of {total} results" "RESULTS": "Showing {start} to {end} of {total} results",
"PER_PAGE_TEMPLATE": "{size} / page"
} }
}, },
"AGENT_REPORTS": { "AGENT_REPORTS": {
@@ -295,7 +295,27 @@
"CONVERSATION_INFO": "Informace o konverzaci", "CONVERSATION_INFO": "Informace o konverzaci",
"CONTACT_ATTRIBUTES": "Atributy kontaktu", "CONTACT_ATTRIBUTES": "Atributy kontaktu",
"PREVIOUS_CONVERSATION": "Předchozí konverzace", "PREVIOUS_CONVERSATION": "Předchozí konverzace",
"MACROS": "Macros" "MACROS": "Macros",
"SHOPIFY_ORDERS": "Shopify Orders"
},
"SHOPIFY": {
"ORDER_ID": "Order #{id}",
"ERROR": "Error loading orders",
"NO_SHOPIFY_ORDERS": "No orders found",
"FINANCIAL_STATUS": {
"PENDING": "Čekající",
"AUTHORIZED": "Authorized",
"PARTIALLY_PAID": "Partially Paid",
"PAID": "Paid",
"PARTIALLY_REFUNDED": "Partially Refunded",
"REFUNDED": "Refunded",
"VOIDED": "Voided"
},
"FULFILLMENT_STATUS": {
"FULFILLED": "Fulfilled",
"PARTIALLY_FULFILLED": "Partially Fulfilled",
"UNFULFILLED": "Unfulfilled"
}
} }
}, },
"CONVERSATION_CUSTOM_ATTRIBUTES": { "CONVERSATION_CUSTOM_ATTRIBUTES": {
@@ -1,5 +1,20 @@
{ {
"INTEGRATION_SETTINGS": { "INTEGRATION_SETTINGS": {
"SHOPIFY": {
"DELETE": {
"TITLE": "Delete Shopify Integration",
"MESSAGE": "Are you sure you want to delete the Shopify integration?"
},
"STORE_URL": {
"TITLE": "Connect Shopify Store",
"LABEL": "Store URL",
"PLACEHOLDER": "your-store.myshopify.com",
"HELP": "Enter your Shopify store's myshopify.com URL",
"CANCEL": "Zrušit",
"SUBMIT": "Connect Store"
},
"ERROR": "There was an error connecting to Shopify. Please try again or contact support if the issue persists."
},
"HEADER": "Integrace", "HEADER": "Integrace",
"DESCRIPTION": "Chatwoot integrates with multiple tools and services to improve your team's efficiency. Explore the list below to configure your favorite apps.", "DESCRIPTION": "Chatwoot integrates with multiple tools and services to improve your team's efficiency. Explore the list below to configure your favorite apps.",
"LEARN_MORE": "Learn more about integrations", "LEARN_MORE": "Learn more about integrations",
@@ -119,7 +119,8 @@
"EMPTY_LIST": "Žádné výsledky" "EMPTY_LIST": "Žádné výsledky"
}, },
"PAGINATION": { "PAGINATION": {
"RESULTS": "Showing {start} to {end} of {total} results" "RESULTS": "Showing {start} to {end} of {total} results",
"PER_PAGE_TEMPLATE": "{size} / page"
} }
}, },
"AGENT_REPORTS": { "AGENT_REPORTS": {
@@ -295,7 +295,27 @@
"CONVERSATION_INFO": "Samtale Information", "CONVERSATION_INFO": "Samtale Information",
"CONTACT_ATTRIBUTES": "Kontakt Attributter", "CONTACT_ATTRIBUTES": "Kontakt Attributter",
"PREVIOUS_CONVERSATION": "Tidligere Samtaler", "PREVIOUS_CONVERSATION": "Tidligere Samtaler",
"MACROS": "Macros" "MACROS": "Macros",
"SHOPIFY_ORDERS": "Shopify Orders"
},
"SHOPIFY": {
"ORDER_ID": "Order #{id}",
"ERROR": "Error loading orders",
"NO_SHOPIFY_ORDERS": "No orders found",
"FINANCIAL_STATUS": {
"PENDING": "Afventer",
"AUTHORIZED": "Authorized",
"PARTIALLY_PAID": "Partially Paid",
"PAID": "Paid",
"PARTIALLY_REFUNDED": "Partially Refunded",
"REFUNDED": "Refunded",
"VOIDED": "Voided"
},
"FULFILLMENT_STATUS": {
"FULFILLED": "Fulfilled",
"PARTIALLY_FULFILLED": "Partially Fulfilled",
"UNFULFILLED": "Unfulfilled"
}
} }
}, },
"CONVERSATION_CUSTOM_ATTRIBUTES": { "CONVERSATION_CUSTOM_ATTRIBUTES": {
@@ -1,5 +1,20 @@
{ {
"INTEGRATION_SETTINGS": { "INTEGRATION_SETTINGS": {
"SHOPIFY": {
"DELETE": {
"TITLE": "Delete Shopify Integration",
"MESSAGE": "Are you sure you want to delete the Shopify integration?"
},
"STORE_URL": {
"TITLE": "Connect Shopify Store",
"LABEL": "Store URL",
"PLACEHOLDER": "your-store.myshopify.com",
"HELP": "Enter your Shopify store's myshopify.com URL",
"CANCEL": "Annuller",
"SUBMIT": "Connect Store"
},
"ERROR": "There was an error connecting to Shopify. Please try again or contact support if the issue persists."
},
"HEADER": "Integrationer", "HEADER": "Integrationer",
"DESCRIPTION": "Chatwoot integrates with multiple tools and services to improve your team's efficiency. Explore the list below to configure your favorite apps.", "DESCRIPTION": "Chatwoot integrates with multiple tools and services to improve your team's efficiency. Explore the list below to configure your favorite apps.",
"LEARN_MORE": "Learn more about integrations", "LEARN_MORE": "Learn more about integrations",
@@ -119,7 +119,8 @@
"EMPTY_LIST": "Ingen resultater fundet" "EMPTY_LIST": "Ingen resultater fundet"
}, },
"PAGINATION": { "PAGINATION": {
"RESULTS": "Showing {start} to {end} of {total} results" "RESULTS": "Showing {start} to {end} of {total} results",
"PER_PAGE_TEMPLATE": "{size} / page"
} }
}, },
"AGENT_REPORTS": { "AGENT_REPORTS": {
@@ -295,7 +295,27 @@
"CONVERSATION_INFO": "Konversationsinformationen", "CONVERSATION_INFO": "Konversationsinformationen",
"CONTACT_ATTRIBUTES": "Kontakt-Attribute", "CONTACT_ATTRIBUTES": "Kontakt-Attribute",
"PREVIOUS_CONVERSATION": "Vorherige Konversationen", "PREVIOUS_CONVERSATION": "Vorherige Konversationen",
"MACROS": "Makros" "MACROS": "Makros",
"SHOPIFY_ORDERS": "Shopify Orders"
},
"SHOPIFY": {
"ORDER_ID": "Order #{id}",
"ERROR": "Error loading orders",
"NO_SHOPIFY_ORDERS": "No orders found",
"FINANCIAL_STATUS": {
"PENDING": "Ausstehend",
"AUTHORIZED": "Authorized",
"PARTIALLY_PAID": "Partially Paid",
"PAID": "Paid",
"PARTIALLY_REFUNDED": "Partially Refunded",
"REFUNDED": "Refunded",
"VOIDED": "Voided"
},
"FULFILLMENT_STATUS": {
"FULFILLED": "Fulfilled",
"PARTIALLY_FULFILLED": "Partially Fulfilled",
"UNFULFILLED": "Unfulfilled"
}
} }
}, },
"CONVERSATION_CUSTOM_ATTRIBUTES": { "CONVERSATION_CUSTOM_ATTRIBUTES": {
@@ -1,5 +1,20 @@
{ {
"INTEGRATION_SETTINGS": { "INTEGRATION_SETTINGS": {
"SHOPIFY": {
"DELETE": {
"TITLE": "Delete Shopify Integration",
"MESSAGE": "Are you sure you want to delete the Shopify integration?"
},
"STORE_URL": {
"TITLE": "Connect Shopify Store",
"LABEL": "Store URL",
"PLACEHOLDER": "your-store.myshopify.com",
"HELP": "Enter your Shopify store's myshopify.com URL",
"CANCEL": "Stornieren",
"SUBMIT": "Connect Store"
},
"ERROR": "There was an error connecting to Shopify. Please try again or contact support if the issue persists."
},
"HEADER": "Integrationen", "HEADER": "Integrationen",
"DESCRIPTION": "Chatwoot integriert sich mit mehreren Tools und Diensten, um die Effizienz Ihres Teams zu verbessern. Erkunden Sie die folgende Liste, um Ihre Lieblingsapps zu konfigurieren.", "DESCRIPTION": "Chatwoot integriert sich mit mehreren Tools und Diensten, um die Effizienz Ihres Teams zu verbessern. Erkunden Sie die folgende Liste, um Ihre Lieblingsapps zu konfigurieren.",
"LEARN_MORE": "Mehr über Integrationen erfahren", "LEARN_MORE": "Mehr über Integrationen erfahren",
@@ -119,7 +119,8 @@
"EMPTY_LIST": "Keine Ergebnisse gefunden" "EMPTY_LIST": "Keine Ergebnisse gefunden"
}, },
"PAGINATION": { "PAGINATION": {
"RESULTS": "Showing {start} to {end} of {total} results" "RESULTS": "Showing {start} to {end} of {total} results",
"PER_PAGE_TEMPLATE": "{size} / page"
} }
}, },
"AGENT_REPORTS": { "AGENT_REPORTS": {
@@ -295,7 +295,27 @@
"CONVERSATION_INFO": "Πληροφορίες Συνομιλίας", "CONVERSATION_INFO": "Πληροφορίες Συνομιλίας",
"CONTACT_ATTRIBUTES": "Ιδιότητες Επαφής", "CONTACT_ATTRIBUTES": "Ιδιότητες Επαφής",
"PREVIOUS_CONVERSATION": "Προηγούμενες συνομιλίες", "PREVIOUS_CONVERSATION": "Προηγούμενες συνομιλίες",
"MACROS": "Μακροεντολές" "MACROS": "Μακροεντολές",
"SHOPIFY_ORDERS": "Shopify Orders"
},
"SHOPIFY": {
"ORDER_ID": "Order #{id}",
"ERROR": "Error loading orders",
"NO_SHOPIFY_ORDERS": "No orders found",
"FINANCIAL_STATUS": {
"PENDING": "Εκκρεμεί",
"AUTHORIZED": "Authorized",
"PARTIALLY_PAID": "Partially Paid",
"PAID": "Paid",
"PARTIALLY_REFUNDED": "Partially Refunded",
"REFUNDED": "Refunded",
"VOIDED": "Voided"
},
"FULFILLMENT_STATUS": {
"FULFILLED": "Fulfilled",
"PARTIALLY_FULFILLED": "Partially Fulfilled",
"UNFULFILLED": "Unfulfilled"
}
} }
}, },
"CONVERSATION_CUSTOM_ATTRIBUTES": { "CONVERSATION_CUSTOM_ATTRIBUTES": {
@@ -1,5 +1,20 @@
{ {
"INTEGRATION_SETTINGS": { "INTEGRATION_SETTINGS": {
"SHOPIFY": {
"DELETE": {
"TITLE": "Delete Shopify Integration",
"MESSAGE": "Are you sure you want to delete the Shopify integration?"
},
"STORE_URL": {
"TITLE": "Connect Shopify Store",
"LABEL": "Store URL",
"PLACEHOLDER": "your-store.myshopify.com",
"HELP": "Enter your Shopify store's myshopify.com URL",
"CANCEL": "Άκυρο",
"SUBMIT": "Connect Store"
},
"ERROR": "There was an error connecting to Shopify. Please try again or contact support if the issue persists."
},
"HEADER": "Ενοποιήσεις", "HEADER": "Ενοποιήσεις",
"DESCRIPTION": "Chatwoot integrates with multiple tools and services to improve your team's efficiency. Explore the list below to configure your favorite apps.", "DESCRIPTION": "Chatwoot integrates with multiple tools and services to improve your team's efficiency. Explore the list below to configure your favorite apps.",
"LEARN_MORE": "Learn more about integrations", "LEARN_MORE": "Learn more about integrations",
@@ -119,7 +119,8 @@
"EMPTY_LIST": "Δεν βρέθηκαν αποτελέσματα" "EMPTY_LIST": "Δεν βρέθηκαν αποτελέσματα"
}, },
"PAGINATION": { "PAGINATION": {
"RESULTS": "Showing {start} to {end} of {total} results" "RESULTS": "Showing {start} to {end} of {total} results",
"PER_PAGE_TEMPLATE": "{size} / page"
} }
}, },
"AGENT_REPORTS": { "AGENT_REPORTS": {
@@ -295,7 +295,27 @@
"CONVERSATION_INFO": "Información de la conversación", "CONVERSATION_INFO": "Información de la conversación",
"CONTACT_ATTRIBUTES": "Atributos de contacto", "CONTACT_ATTRIBUTES": "Atributos de contacto",
"PREVIOUS_CONVERSATION": "Conversaciones anteriores", "PREVIOUS_CONVERSATION": "Conversaciones anteriores",
"MACROS": "Macros" "MACROS": "Macros",
"SHOPIFY_ORDERS": "Shopify Orders"
},
"SHOPIFY": {
"ORDER_ID": "Order #{id}",
"ERROR": "Error loading orders",
"NO_SHOPIFY_ORDERS": "No orders found",
"FINANCIAL_STATUS": {
"PENDING": "Pendientes",
"AUTHORIZED": "Authorized",
"PARTIALLY_PAID": "Partially Paid",
"PAID": "Paid",
"PARTIALLY_REFUNDED": "Partially Refunded",
"REFUNDED": "Refunded",
"VOIDED": "Voided"
},
"FULFILLMENT_STATUS": {
"FULFILLED": "Fulfilled",
"PARTIALLY_FULFILLED": "Partially Fulfilled",
"UNFULFILLED": "Unfulfilled"
}
} }
}, },
"CONVERSATION_CUSTOM_ATTRIBUTES": { "CONVERSATION_CUSTOM_ATTRIBUTES": {
@@ -1,5 +1,20 @@
{ {
"INTEGRATION_SETTINGS": { "INTEGRATION_SETTINGS": {
"SHOPIFY": {
"DELETE": {
"TITLE": "Delete Shopify Integration",
"MESSAGE": "Are you sure you want to delete the Shopify integration?"
},
"STORE_URL": {
"TITLE": "Connect Shopify Store",
"LABEL": "Store URL",
"PLACEHOLDER": "your-store.myshopify.com",
"HELP": "Enter your Shopify store's myshopify.com URL",
"CANCEL": "Cancelar",
"SUBMIT": "Connect Store"
},
"ERROR": "There was an error connecting to Shopify. Please try again or contact support if the issue persists."
},
"HEADER": "Integraciones", "HEADER": "Integraciones",
"DESCRIPTION": "Chatwoot se integra con múltiples herramientas y servicios para mejorar la eficiencia de tu equipo. Explora la lista de abajo para configurar tus aplicaciones favoritas.", "DESCRIPTION": "Chatwoot se integra con múltiples herramientas y servicios para mejorar la eficiencia de tu equipo. Explora la lista de abajo para configurar tus aplicaciones favoritas.",
"LEARN_MORE": "Más información acerca de integraciones", "LEARN_MORE": "Más información acerca de integraciones",
@@ -119,7 +119,8 @@
"EMPTY_LIST": "No se encontraron resultados" "EMPTY_LIST": "No se encontraron resultados"
}, },
"PAGINATION": { "PAGINATION": {
"RESULTS": "Mostrando {start} a {end} de {total} resultados" "RESULTS": "Mostrando {start} a {end} de {total} resultados",
"PER_PAGE_TEMPLATE": "{size} / page"
} }
}, },
"AGENT_REPORTS": { "AGENT_REPORTS": {
@@ -295,7 +295,27 @@
"CONVERSATION_INFO": "اطلاعات مکالمه", "CONVERSATION_INFO": "اطلاعات مکالمه",
"CONTACT_ATTRIBUTES": "ویژگی‌های تماس", "CONTACT_ATTRIBUTES": "ویژگی‌های تماس",
"PREVIOUS_CONVERSATION": "گفتگوهای قبلی", "PREVIOUS_CONVERSATION": "گفتگوهای قبلی",
"MACROS": "ماکروها" "MACROS": "ماکروها",
"SHOPIFY_ORDERS": "Shopify Orders"
},
"SHOPIFY": {
"ORDER_ID": "Order #{id}",
"ERROR": "Error loading orders",
"NO_SHOPIFY_ORDERS": "No orders found",
"FINANCIAL_STATUS": {
"PENDING": "در انتظار",
"AUTHORIZED": "Authorized",
"PARTIALLY_PAID": "Partially Paid",
"PAID": "Paid",
"PARTIALLY_REFUNDED": "Partially Refunded",
"REFUNDED": "Refunded",
"VOIDED": "Voided"
},
"FULFILLMENT_STATUS": {
"FULFILLED": "Fulfilled",
"PARTIALLY_FULFILLED": "Partially Fulfilled",
"UNFULFILLED": "Unfulfilled"
}
} }
}, },
"CONVERSATION_CUSTOM_ATTRIBUTES": { "CONVERSATION_CUSTOM_ATTRIBUTES": {
@@ -1,5 +1,20 @@
{ {
"INTEGRATION_SETTINGS": { "INTEGRATION_SETTINGS": {
"SHOPIFY": {
"DELETE": {
"TITLE": "Delete Shopify Integration",
"MESSAGE": "Are you sure you want to delete the Shopify integration?"
},
"STORE_URL": {
"TITLE": "Connect Shopify Store",
"LABEL": "Store URL",
"PLACEHOLDER": "your-store.myshopify.com",
"HELP": "Enter your Shopify store's myshopify.com URL",
"CANCEL": "انصراف",
"SUBMIT": "Connect Store"
},
"ERROR": "There was an error connecting to Shopify. Please try again or contact support if the issue persists."
},
"HEADER": "برنامه‌های تلفیق شده", "HEADER": "برنامه‌های تلفیق شده",
"DESCRIPTION": "Chatwoot integrates with multiple tools and services to improve your team's efficiency. Explore the list below to configure your favorite apps.", "DESCRIPTION": "Chatwoot integrates with multiple tools and services to improve your team's efficiency. Explore the list below to configure your favorite apps.",
"LEARN_MORE": "Learn more about integrations", "LEARN_MORE": "Learn more about integrations",
@@ -119,7 +119,8 @@
"EMPTY_LIST": "نتیجه‌ای یافت نشد" "EMPTY_LIST": "نتیجه‌ای یافت نشد"
}, },
"PAGINATION": { "PAGINATION": {
"RESULTS": "Showing {start} to {end} of {total} results" "RESULTS": "Showing {start} to {end} of {total} results",
"PER_PAGE_TEMPLATE": "{size} / page"
} }
}, },
"AGENT_REPORTS": { "AGENT_REPORTS": {
@@ -295,7 +295,27 @@
"CONVERSATION_INFO": "Keskustelun Tiedot", "CONVERSATION_INFO": "Keskustelun Tiedot",
"CONTACT_ATTRIBUTES": "Yhteystiedon määritteet", "CONTACT_ATTRIBUTES": "Yhteystiedon määritteet",
"PREVIOUS_CONVERSATION": "Edelliset keskustelut", "PREVIOUS_CONVERSATION": "Edelliset keskustelut",
"MACROS": "Macros" "MACROS": "Macros",
"SHOPIFY_ORDERS": "Shopify Orders"
},
"SHOPIFY": {
"ORDER_ID": "Order #{id}",
"ERROR": "Error loading orders",
"NO_SHOPIFY_ORDERS": "No orders found",
"FINANCIAL_STATUS": {
"PENDING": "Odottava",
"AUTHORIZED": "Authorized",
"PARTIALLY_PAID": "Partially Paid",
"PAID": "Paid",
"PARTIALLY_REFUNDED": "Partially Refunded",
"REFUNDED": "Refunded",
"VOIDED": "Voided"
},
"FULFILLMENT_STATUS": {
"FULFILLED": "Fulfilled",
"PARTIALLY_FULFILLED": "Partially Fulfilled",
"UNFULFILLED": "Unfulfilled"
}
} }
}, },
"CONVERSATION_CUSTOM_ATTRIBUTES": { "CONVERSATION_CUSTOM_ATTRIBUTES": {
@@ -1,5 +1,20 @@
{ {
"INTEGRATION_SETTINGS": { "INTEGRATION_SETTINGS": {
"SHOPIFY": {
"DELETE": {
"TITLE": "Delete Shopify Integration",
"MESSAGE": "Are you sure you want to delete the Shopify integration?"
},
"STORE_URL": {
"TITLE": "Connect Shopify Store",
"LABEL": "Store URL",
"PLACEHOLDER": "your-store.myshopify.com",
"HELP": "Enter your Shopify store's myshopify.com URL",
"CANCEL": "Peruuta",
"SUBMIT": "Connect Store"
},
"ERROR": "There was an error connecting to Shopify. Please try again or contact support if the issue persists."
},
"HEADER": "Integraatiot", "HEADER": "Integraatiot",
"DESCRIPTION": "Chatwoot integrates with multiple tools and services to improve your team's efficiency. Explore the list below to configure your favorite apps.", "DESCRIPTION": "Chatwoot integrates with multiple tools and services to improve your team's efficiency. Explore the list below to configure your favorite apps.",
"LEARN_MORE": "Learn more about integrations", "LEARN_MORE": "Learn more about integrations",
@@ -119,7 +119,8 @@
"EMPTY_LIST": "Tuloksia ei löytynyt" "EMPTY_LIST": "Tuloksia ei löytynyt"
}, },
"PAGINATION": { "PAGINATION": {
"RESULTS": "Showing {start} to {end} of {total} results" "RESULTS": "Showing {start} to {end} of {total} results",
"PER_PAGE_TEMPLATE": "{size} / page"
} }
}, },
"AGENT_REPORTS": { "AGENT_REPORTS": {
@@ -295,7 +295,27 @@
"CONVERSATION_INFO": "Informations de la conversation", "CONVERSATION_INFO": "Informations de la conversation",
"CONTACT_ATTRIBUTES": "Attributs du contact", "CONTACT_ATTRIBUTES": "Attributs du contact",
"PREVIOUS_CONVERSATION": "Conversations précédentes", "PREVIOUS_CONVERSATION": "Conversations précédentes",
"MACROS": "Macros" "MACROS": "Macros",
"SHOPIFY_ORDERS": "Shopify Orders"
},
"SHOPIFY": {
"ORDER_ID": "Order #{id}",
"ERROR": "Error loading orders",
"NO_SHOPIFY_ORDERS": "No orders found",
"FINANCIAL_STATUS": {
"PENDING": "En attente",
"AUTHORIZED": "Authorized",
"PARTIALLY_PAID": "Partially Paid",
"PAID": "Paid",
"PARTIALLY_REFUNDED": "Partially Refunded",
"REFUNDED": "Refunded",
"VOIDED": "Voided"
},
"FULFILLMENT_STATUS": {
"FULFILLED": "Fulfilled",
"PARTIALLY_FULFILLED": "Partially Fulfilled",
"UNFULFILLED": "Unfulfilled"
}
} }
}, },
"CONVERSATION_CUSTOM_ATTRIBUTES": { "CONVERSATION_CUSTOM_ATTRIBUTES": {
@@ -1,5 +1,20 @@
{ {
"INTEGRATION_SETTINGS": { "INTEGRATION_SETTINGS": {
"SHOPIFY": {
"DELETE": {
"TITLE": "Delete Shopify Integration",
"MESSAGE": "Are you sure you want to delete the Shopify integration?"
},
"STORE_URL": {
"TITLE": "Connect Shopify Store",
"LABEL": "Store URL",
"PLACEHOLDER": "your-store.myshopify.com",
"HELP": "Enter your Shopify store's myshopify.com URL",
"CANCEL": "Annuler",
"SUBMIT": "Connect Store"
},
"ERROR": "There was an error connecting to Shopify. Please try again or contact support if the issue persists."
},
"HEADER": "Intégrations", "HEADER": "Intégrations",
"DESCRIPTION": "Chatwoot integrates with multiple tools and services to improve your team's efficiency. Explore the list below to configure your favorite apps.", "DESCRIPTION": "Chatwoot integrates with multiple tools and services to improve your team's efficiency. Explore the list below to configure your favorite apps.",
"LEARN_MORE": "Learn more about integrations", "LEARN_MORE": "Learn more about integrations",
@@ -119,7 +119,8 @@
"EMPTY_LIST": "Aucun résultat trouvé" "EMPTY_LIST": "Aucun résultat trouvé"
}, },
"PAGINATION": { "PAGINATION": {
"RESULTS": "Showing {start} to {end} of {total} results" "RESULTS": "Showing {start} to {end} of {total} results",
"PER_PAGE_TEMPLATE": "{size} / page"
} }
}, },
"AGENT_REPORTS": { "AGENT_REPORTS": {
@@ -295,7 +295,27 @@
"CONVERSATION_INFO": "מידע על שיחה", "CONVERSATION_INFO": "מידע על שיחה",
"CONTACT_ATTRIBUTES": "תכונות יצירת קשר", "CONTACT_ATTRIBUTES": "תכונות יצירת קשר",
"PREVIOUS_CONVERSATION": "שיחות קודמות", "PREVIOUS_CONVERSATION": "שיחות קודמות",
"MACROS": "מאקרו" "MACROS": "מאקרו",
"SHOPIFY_ORDERS": "Shopify Orders"
},
"SHOPIFY": {
"ORDER_ID": "Order #{id}",
"ERROR": "Error loading orders",
"NO_SHOPIFY_ORDERS": "No orders found",
"FINANCIAL_STATUS": {
"PENDING": "ממתין ל",
"AUTHORIZED": "Authorized",
"PARTIALLY_PAID": "Partially Paid",
"PAID": "Paid",
"PARTIALLY_REFUNDED": "Partially Refunded",
"REFUNDED": "Refunded",
"VOIDED": "Voided"
},
"FULFILLMENT_STATUS": {
"FULFILLED": "Fulfilled",
"PARTIALLY_FULFILLED": "Partially Fulfilled",
"UNFULFILLED": "Unfulfilled"
}
} }
}, },
"CONVERSATION_CUSTOM_ATTRIBUTES": { "CONVERSATION_CUSTOM_ATTRIBUTES": {
@@ -1,5 +1,20 @@
{ {
"INTEGRATION_SETTINGS": { "INTEGRATION_SETTINGS": {
"SHOPIFY": {
"DELETE": {
"TITLE": "Delete Shopify Integration",
"MESSAGE": "Are you sure you want to delete the Shopify integration?"
},
"STORE_URL": {
"TITLE": "Connect Shopify Store",
"LABEL": "Store URL",
"PLACEHOLDER": "your-store.myshopify.com",
"HELP": "Enter your Shopify store's myshopify.com URL",
"CANCEL": "ביטול",
"SUBMIT": "Connect Store"
},
"ERROR": "There was an error connecting to Shopify. Please try again or contact support if the issue persists."
},
"HEADER": "אינטגרציות", "HEADER": "אינטגרציות",
"DESCRIPTION": "Chatwoot integrates with multiple tools and services to improve your team's efficiency. Explore the list below to configure your favorite apps.", "DESCRIPTION": "Chatwoot integrates with multiple tools and services to improve your team's efficiency. Explore the list below to configure your favorite apps.",
"LEARN_MORE": "Learn more about integrations", "LEARN_MORE": "Learn more about integrations",
@@ -119,7 +119,8 @@
"EMPTY_LIST": "לא נמצאו תוצאות" "EMPTY_LIST": "לא נמצאו תוצאות"
}, },
"PAGINATION": { "PAGINATION": {
"RESULTS": "Showing {start} to {end} of {total} results" "RESULTS": "Showing {start} to {end} of {total} results",
"PER_PAGE_TEMPLATE": "{size} / page"
} }
}, },
"AGENT_REPORTS": { "AGENT_REPORTS": {
@@ -295,7 +295,27 @@
"CONVERSATION_INFO": "Conversation Information", "CONVERSATION_INFO": "Conversation Information",
"CONTACT_ATTRIBUTES": "Contact Attributes", "CONTACT_ATTRIBUTES": "Contact Attributes",
"PREVIOUS_CONVERSATION": "Previous Conversations", "PREVIOUS_CONVERSATION": "Previous Conversations",
"MACROS": "Macros" "MACROS": "Macros",
"SHOPIFY_ORDERS": "Shopify Orders"
},
"SHOPIFY": {
"ORDER_ID": "Order #{id}",
"ERROR": "Error loading orders",
"NO_SHOPIFY_ORDERS": "No orders found",
"FINANCIAL_STATUS": {
"PENDING": "Pending",
"AUTHORIZED": "Authorized",
"PARTIALLY_PAID": "Partially Paid",
"PAID": "Paid",
"PARTIALLY_REFUNDED": "Partially Refunded",
"REFUNDED": "Refunded",
"VOIDED": "Voided"
},
"FULFILLMENT_STATUS": {
"FULFILLED": "Fulfilled",
"PARTIALLY_FULFILLED": "Partially Fulfilled",
"UNFULFILLED": "Unfulfilled"
}
} }
}, },
"CONVERSATION_CUSTOM_ATTRIBUTES": { "CONVERSATION_CUSTOM_ATTRIBUTES": {

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