Compare commits

...
Author SHA1 Message Date
Sojan fd01a5056a Merge branch 'release/3.13.0'
Publish Chatwoot CE docker images / build (push) Waiting to run
2024-09-17 16:47:37 -07:00
Sojan 377fb5c3b9 Bump version to 3.13.0 2024-09-17 16:46:02 -07:00
58e78621ba chore: Custom Roles to manage permissions [ UI ] (#9865)
In admin settings, this Pr will add the UI for managing custom roles (
ref: https://github.com/chatwoot/chatwoot/pull/9995 ). It also handles
the routing logic changes to accommodate fine-tuned permissions.

---------

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

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

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

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

- `Gemfile`



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

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

</details>



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

|  | Issue | Score | 

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




---

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

---

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

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

---

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

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

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

---------

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


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

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

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

---

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

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

</details>

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

The UI now shows emoji names instead of slugs, and the search logic has been updated to generate `searchString` without whitespaces, allowing users to search for emojis like 'face_with' using 'facewith'
2024-09-13 10:54:18 +05:30
Sojan d70ba8ff40 Merge branch 'release/3.12.0'
Publish Chatwoot CE docker images / build (push) Waiting to run
2024-08-19 15:54:36 -07:00
100 changed files with 2707 additions and 663 deletions
+1 -1
View File
@@ -228,7 +228,7 @@ group :development, :test do
gem 'mock_redis'
gem 'pry-rails'
gem 'rspec_junit_formatter'
gem 'rspec-rails', '>= 6.1.4'
gem 'rspec-rails', '>= 6.1.5'
gem 'rubocop', require: false
gem 'rubocop-performance', require: false
gem 'rubocop-rails', require: false
+6 -6
View File
@@ -638,16 +638,16 @@ GEM
strscan
rspec-core (3.13.0)
rspec-support (~> 3.13.0)
rspec-expectations (3.13.1)
rspec-expectations (3.13.2)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-mocks (3.13.1)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-rails (6.1.4)
actionpack (>= 6.1)
activesupport (>= 6.1)
railties (>= 6.1)
rspec-rails (7.0.1)
actionpack (>= 7.0)
activesupport (>= 7.0)
railties (>= 7.0)
rspec-core (~> 3.13)
rspec-expectations (~> 3.13)
rspec-mocks (~> 3.13)
@@ -930,7 +930,7 @@ DEPENDENCIES
responders (>= 3.1.1)
rest-client
reverse_markdown
rspec-rails (>= 6.1.4)
rspec-rails (>= 6.1.5)
rspec_junit_formatter
rubocop
rubocop-performance
@@ -13,7 +13,7 @@ class Api::V1::Accounts::Conversations::MessagesController < Api::V1::Accounts::
def destroy
ActiveRecord::Base.transaction do
message.update!(content: I18n.t('conversations.messages.deleted'), content_attributes: { deleted: true })
message.update!(content: I18n.t('conversations.messages.deleted'), content_type: :text, content_attributes: { deleted: true })
message.attachments.destroy_all
end
end
@@ -11,6 +11,8 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
process_update_contact
@conversation = create_conversation
conversation.messages.create!(message_params)
# TODO: Temporary fix for message type cast issue, since message_type is returning as string instead of integer
conversation.reload
end
end
@@ -66,7 +66,9 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
end
def check_authorization
raise Pundit::NotAuthorizedError unless Current.account_user.administrator?
return if Current.account_user.administrator?
raise Pundit::NotAuthorizedError
end
def common_params
@@ -135,3 +137,5 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
V2::ReportBuilder.new(Current.account, conversation_params).conversation_metrics
end
end
Api::V2::Accounts::ReportsController.prepend_mod_with('Api::V2::Accounts::ReportsController')
@@ -6,7 +6,7 @@ class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::B
layout 'portal'
def index
@articles = @portal.articles
@articles = @portal.articles.published
search_articles
order_by_sort_param
@articles.page(list_params[:page]) if list_params[:page].present?
@@ -0,0 +1,9 @@
import ApiClient from './ApiClient';
class CustomRole extends ApiClient {
constructor() {
super('custom_roles', { accountScoped: true });
}
}
export default new CustomRole();
@@ -27,6 +27,11 @@ import {
} from '../store/modules/conversations/helpers/actionHelpers';
import { CONVERSATION_EVENTS } from '../helper/AnalyticsHelper/events';
import IntersectionObserver from './IntersectionObserver.vue';
import {
getUserPermissions,
filterItemsByPermission,
} from 'dashboard/helper/permissionsHelper.js';
import { ASSIGNEE_TYPE_TAB_PERMISSIONS } from 'dashboard/constants/permissions.js';
export default {
components: {
@@ -204,6 +209,7 @@ export default {
computed: {
...mapGetters({
currentUser: 'getCurrentUser',
currentAccountId: 'getCurrentAccountId',
chatLists: 'getAllConversations',
mineChatsList: 'getMineChats',
allChatList: 'getAllStatusChats',
@@ -243,20 +249,19 @@ export default {
name,
};
},
userPermissions() {
return getUserPermissions(this.currentUser, this.currentAccountId);
},
assigneeTabItems() {
const ASSIGNEE_TYPE_TAB_KEYS = {
me: 'mineCount',
unassigned: 'unAssignedCount',
all: 'allCount',
};
return Object.keys(ASSIGNEE_TYPE_TAB_KEYS).map(key => {
const count = this.conversationStats[ASSIGNEE_TYPE_TAB_KEYS[key]] || 0;
return {
key,
name: this.$t(`CHAT_LIST.ASSIGNEE_TYPE_TABS.${key}`),
count,
};
});
return filterItemsByPermission(
ASSIGNEE_TYPE_TAB_PERMISSIONS,
this.userPermissions,
item => item.permissions
).map(({ key, count: countKey }) => ({
key,
name: this.$t(`CHAT_LIST.ASSIGNEE_TYPE_TABS.${key}`),
count: this.conversationStats[countKey] || 0,
}));
},
showAssigneeInConversationCard() {
return (
@@ -39,6 +39,7 @@ const settings = accountId => ({
'settings_teams_list',
'settings_teams_new',
'sla_list',
'custom_roles_list',
],
menuItems: [
{
@@ -178,6 +179,18 @@ const settings = accountId => ({
isEnterpriseOnly: true,
featureFlag: FEATURE_FLAGS.AUDIT_LOGS,
},
{
icon: 'scan-person',
label: 'CUSTOM_ROLES',
hasSubMenu: false,
meta: {
permissions: ['administrator'],
},
toState: frontendURL(`accounts/${accountId}/settings/custom-roles/list`),
toStateName: 'custom_roles_list',
isEnterpriseOnly: true,
beta: true,
},
{
icon: 'document-list-clock',
label: 'SLA',
@@ -52,9 +52,13 @@ export default {
{{ account.name }}
</div>
<div
class="text-xs font-medium text-slate-500 dark:text-slate-500 hover:underline-offset-4"
class="text-xs font-medium lowercase text-slate-500 dark:text-slate-500 hover:underline-offset-4"
>
{{ account.role }}
{{
account.custom_role_id
? account.custom_role.name
: account.role
}}
</div>
</label>
</span>
@@ -21,8 +21,11 @@ const activeTabIndex = computed(() => {
});
const onTabChange = selectedTabIndex => {
if (props.items[selectedTabIndex].key !== props.activeTab) {
emit('chatTabChange', props.items[selectedTabIndex].key);
if (selectedTabIndex >= 0 && selectedTabIndex < props.items.length) {
const selectedItem = props.items[selectedTabIndex];
if (selectedItem.key !== props.activeTab) {
emit('chatTabChange', selectedItem.key);
}
}
};
@@ -32,7 +35,8 @@ const keyboardEvents = {
if (props.activeTab === wootConstants.ASSIGNEE_TYPE.ALL) {
onTabChange(0);
} else {
onTabChange(activeTabIndex.value + 1);
const nextIndex = (activeTabIndex.value + 1) % props.items.length;
onTabChange(nextIndex);
}
},
},
@@ -97,6 +97,7 @@ export default {
allowSignature: { type: Boolean, default: false },
channelType: { type: String, default: '' },
showImageResizeToolbar: { type: Boolean, default: false }, // A kill switch to show or hide the image toolbar
focusOnMount: { type: Boolean, default: true },
},
setup() {
const {
@@ -247,7 +248,7 @@ export default {
},
}),
suggestionsPlugin({
matcher: triggerCharacters(':', 1), // Trigger after ':' and at least 1 characters
matcher: triggerCharacters(':', 2), // Trigger after ':' and at least 2 characters
suggestionClass: '',
onEnter: args => {
this.showEmojiMenu = true;
@@ -346,7 +347,9 @@ export default {
mounted() {
this.createEditorView();
this.editorView.updateState(this.state);
this.focusEditorInputField();
if (this.focusOnMount) {
this.focusEditorInputField();
}
// BUS Event to insert text or markdown into the editor at the
// current cursor position.
@@ -383,7 +386,7 @@ export default {
// these drafts can also have a signature, so we need to check if the body is empty
// and handle things accordingly
this.handleEmptyBodyWithSignature();
} else {
} else if (this.focusOnMount) {
// this is in the else block, handleEmptyBodyWithSignature also has a call to the focus method
// the position is set to start, because the signature is added at the end of the body
this.focusEditorInputField('end');
@@ -220,11 +220,7 @@ export default {
: this.$t('CONVERSATION.FOOTER.ENABLE_SIGN_TOOLTIP');
},
enableInsertArticleInReply() {
const isFeatEnabled = this.isFeatureEnabledonAccount(
this.accountId,
FEATURE_FLAGS.INSERT_ARTICLE_IN_REPLY
);
return isFeatEnabled && this.portalSlug;
return this.portalSlug;
},
isFetchingAppIntegrations() {
return this.uiFlags.isFetching;
@@ -403,6 +399,7 @@ export default {
label {
@apply cursor-pointer;
}
&:hover button {
@apply dark:bg-slate-800 bg-slate-100;
}
@@ -1,6 +1,6 @@
<script setup>
import { shallowRef, computed, onMounted } from 'vue';
import emojis from 'shared/components/emoji/emojisGroup.json';
import emojiGroups from 'shared/components/emoji/emojisGroup.json';
import MentionBox from '../mentions/MentionBox.vue';
const props = defineProps({
@@ -23,10 +23,12 @@ const items = computed(() => {
});
function loadEmojis() {
allEmojis.value = emojis.flatMap(group =>
group.emojis.map(emoji => ({
...emoji,
searchString: `${emoji.slug} ${emoji.name}`.toLowerCase(),
allEmojis.value = emojiGroups.flatMap(({ emojis }) =>
emojis.map(({ name, slug, ...rest }) => ({
...rest,
name,
slug,
searchString: `${name.replace(/\s+/g, '')} ${slug}`.toLowerCase(), // Remove all whitespace and convert to lowercase
}))
);
}
@@ -60,7 +62,7 @@ onMounted(() => {
'font-normal': !selected,
}"
>
:{{ item.slug }}
:{{ item.name }}
</p>
</span>
</template>
@@ -0,0 +1,53 @@
export const AVAILABLE_CUSTOM_ROLE_PERMISSIONS = [
'conversation_manage',
'conversation_unassigned_manage',
'conversation_participating_manage',
'contact_manage',
'report_manage',
'knowledge_base_manage',
];
export const ROLES = ['agent', 'administrator'];
export const CONVERSATION_PERMISSIONS = [
'conversation_manage',
'conversation_unassigned_manage',
'conversation_participating_manage',
];
export const MANAGE_ALL_CONVERSATION_PERMISSIONS = 'conversation_manage';
export const CONVERSATION_UNASSIGNED_PERMISSIONS =
'conversation_unassigned_manage';
export const CONVERSATION_PARTICIPATING_PERMISSIONS =
'conversation_participating_manage';
export const CONTACT_PERMISSIONS = 'contact_manage';
export const REPORTS_PERMISSIONS = 'report_manage';
export const PORTAL_PERMISSIONS = 'knowledge_base_manage';
export const ASSIGNEE_TYPE_TAB_PERMISSIONS = {
me: {
count: 'mineCount',
permissions: [...ROLES, ...CONVERSATION_PERMISSIONS],
},
unassigned: {
count: 'unAssignedCount',
permissions: [
...ROLES,
MANAGE_ALL_CONVERSATION_PERMISSIONS,
CONVERSATION_UNASSIGNED_PERMISSIONS,
],
},
all: {
count: 'allCount',
permissions: [
...ROLES,
MANAGE_ALL_CONVERSATION_PERMISSIONS,
CONVERSATION_PARTICIPATING_PERMISSIONS,
],
},
};
+1 -1
View File
@@ -16,7 +16,6 @@ export const FEATURE_FLAGS = {
TEAM_MANAGEMENT: 'team_management',
VOICE_RECORDER: 'voice_recorder',
AUDIT_LOGS: 'audit_logs',
INSERT_ARTICLE_IN_REPLY: 'insert_article_in_reply',
INBOX_VIEW: 'inbox_view',
SLA: 'sla',
RESPONSE_BOT: 'response_bot',
@@ -32,4 +31,5 @@ export const FEATURE_FLAGS = {
IP_LOOKUP: 'ip_lookup',
LINEAR: 'linear_integration',
CAPTAIN: 'captain_integration',
CUSTOM_ROLES: 'custom_roles',
};
@@ -5,6 +5,11 @@ import {
getAlertAudio,
initOnEvents,
} from 'shared/helpers/AudioNotificationHelper';
import {
ROLES,
CONVERSATION_PERMISSIONS,
} from 'dashboard/constants/permissions.js';
import { getUserPermissions } from 'dashboard/helper/permissionsHelper.js';
const NOTIFICATION_TIME = 30000;
@@ -14,12 +19,13 @@ class DashboardAudioNotificationHelper {
this.audioAlertType = 'none';
this.playAlertOnlyWhenHidden = true;
this.alertIfUnreadConversationExist = false;
this.currentUser = null;
this.currentUserId = null;
this.audioAlertTone = 'ding';
}
setInstanceValues = ({
currentUserId,
currentUser,
alwaysPlayAudioAlert,
alertIfUnreadConversationExist,
audioAlertType,
@@ -28,7 +34,8 @@ class DashboardAudioNotificationHelper {
this.audioAlertType = audioAlertType;
this.playAlertOnlyWhenHidden = !alwaysPlayAudioAlert;
this.alertIfUnreadConversationExist = alertIfUnreadConversationExist;
this.currentUserId = currentUserId;
this.currentUser = currentUser;
this.currentUserId = currentUser.id;
this.audioAlertTone = audioAlertTone;
initOnEvents.forEach(e => {
document.addEventListener(e, this.onAudioListenEvent, false);
@@ -112,6 +119,20 @@ class DashboardAudioNotificationHelper {
return message?.sender_id === this.currentUserId;
};
isUserHasConversationPermission = () => {
const currentAccountId = window.WOOT.$store.getters.getCurrentAccountId;
// Get the user permissions for the current account
const userPermissions = getUserPermissions(
this.currentUser,
currentAccountId
);
// Check if the user has the required permissions
const hasRequiredPermission = [...ROLES, ...CONVERSATION_PERMISSIONS].some(
permission => userPermissions.includes(permission)
);
return hasRequiredPermission;
};
shouldNotifyOnMessage = message => {
if (this.audioAlertType === 'mine') {
return this.isConversationAssignedToCurrentUser(message);
@@ -120,6 +141,11 @@ class DashboardAudioNotificationHelper {
};
onNewMessage = message => {
// If the user does not have the permission to view the conversation, then dismiss the alert
if (!this.isUserHasConversationPermission()) {
return;
}
// If the message is sent by the current user or the
// correct notification is not enabled, then dismiss the alert
if (
@@ -41,3 +41,32 @@ export const buildPermissionsFromRouter = (routes = []) =>
return acc;
}, {});
/**
* Filters and transforms items based on user permissions.
*
* @param {Object} items - An object containing items to be filtered.
* @param {Array} userPermissions - Array of permissions the user has.
* @param {Function} getPermissions - Function to extract required permissions from an item.
* @param {Function} [transformItem] - Optional function to transform each item after filtering.
* @returns {Array} Filtered and transformed items.
*/
export const filterItemsByPermission = (
items,
userPermissions,
getPermissions,
transformItem = (key, item) => ({ key, ...item })
) => {
// Helper function to check if an item has the required permissions
const hasRequiredPermissions = item => {
const requiredPermissions = getPermissions(item);
return (
requiredPermissions.length === 0 ||
hasPermissions(requiredPermissions, userPermissions)
);
};
return Object.entries(items)
.filter(([, item]) => hasRequiredPermissions(item)) // Keep only items with required permissions
.map(([key, item]) => transformItem(key, item)); // Transform each remaining item
};
@@ -4,11 +4,39 @@ import {
getCurrentAccount,
} from './permissionsHelper';
import {
ROLES,
CONVERSATION_PERMISSIONS,
CONTACT_PERMISSIONS,
REPORTS_PERMISSIONS,
PORTAL_PERMISSIONS,
} from 'dashboard/constants/permissions.js';
export const routeIsAccessibleFor = (route, userPermissions = []) => {
const { meta: { permissions: routePermissions = [] } = {} } = route;
return hasPermissions(routePermissions, userPermissions);
};
export const defaultRedirectPage = (to, permissions) => {
const { accountId } = to.params;
const permissionRoutes = [
{
permissions: [...ROLES, ...CONVERSATION_PERMISSIONS],
path: 'dashboard',
},
{ permissions: [CONTACT_PERMISSIONS], path: 'contacts' },
{ permissions: [REPORTS_PERMISSIONS], path: 'reports/overview' },
{ permissions: [PORTAL_PERMISSIONS], path: 'portals' },
];
const route = permissionRoutes.find(({ permissions: routePermissions }) =>
hasPermissions(routePermissions, permissions)
);
return `accounts/${accountId}/${route ? route.path : 'dashboard'}`;
};
const validateActiveAccountRoutes = (to, user) => {
// If the current account is active, then check for the route permissions
const accountDashboardURL = `accounts/${to.params.accountId}/dashboard`;
@@ -22,7 +50,7 @@ const validateActiveAccountRoutes = (to, user) => {
const isAccessible = routeIsAccessibleFor(to, userPermissions);
// If the route is not accessible for the user, return to dashboard screen
return isAccessible ? null : accountDashboardURL;
return isAccessible ? null : defaultRedirectPage(to, userPermissions);
};
export const validateLoggedInRoutes = (to, user) => {
@@ -26,7 +26,7 @@ const initializeAudioAlerts = user => {
} = uiSettings || {};
DashboardAudioNotificationHelper.setInstanceValues({
currentUserId: user.id,
currentUser: user,
audioAlertType: audioAlertType || 'none',
audioAlertTone: audioAlertTone || 'ding',
alwaysPlayAudioAlert: alwaysPlayAudioAlert || false,
@@ -3,6 +3,7 @@ import {
getCurrentAccount,
getUserPermissions,
hasPermissions,
filterItemsByPermission,
} from '../permissionsHelper';
describe('#getCurrentAccount', () => {
@@ -105,3 +106,113 @@ describe('buildPermissionsFromRouter', () => {
}).toThrow("The route doesn't have the required permissions defined");
});
});
describe('filterItemsByPermission', () => {
const items = {
item1: { name: 'Item 1', permissions: ['agent', 'administrator'] },
item2: {
name: 'Item 2',
permissions: [
'conversation_manage',
'conversation_unassigned_manage',
'conversation_participating_manage',
],
},
item3: { name: 'Item 3', permissions: ['contact_manage'] },
item4: { name: 'Item 4', permissions: ['report_manage'] },
item5: { name: 'Item 5', permissions: ['knowledge_base_manage'] },
item6: {
name: 'Item 6',
permissions: [
'agent',
'administrator',
'conversation_manage',
'conversation_unassigned_manage',
'conversation_participating_manage',
'contact_manage',
'report_manage',
'knowledge_base_manage',
],
},
item7: { name: 'Item 7', permissions: [] },
};
const getPermissions = item => item.permissions;
it('filters items based on user permissions', () => {
const userPermissions = ['agent', 'contact_manage', 'report_manage'];
const result = filterItemsByPermission(
items,
userPermissions,
getPermissions
);
expect(result).toHaveLength(5);
expect(result).toContainEqual(
expect.objectContaining({ key: 'item1', name: 'Item 1' })
);
expect(result).toContainEqual(
expect.objectContaining({ key: 'item3', name: 'Item 3' })
);
expect(result).toContainEqual(
expect.objectContaining({ key: 'item4', name: 'Item 4' })
);
expect(result).toContainEqual(
expect.objectContaining({ key: 'item6', name: 'Item 6' })
);
});
it('includes items with empty permissions', () => {
const userPermissions = [];
const result = filterItemsByPermission(
items,
userPermissions,
getPermissions
);
expect(result).toHaveLength(1);
expect(result).toContainEqual(
expect.objectContaining({ key: 'item7', name: 'Item 7' })
);
});
it('uses custom transform function when provided', () => {
const userPermissions = ['agent', 'contact_manage'];
const customTransform = (key, item) => ({ id: key, title: item.name });
const result = filterItemsByPermission(
items,
userPermissions,
getPermissions,
customTransform
);
expect(result).toHaveLength(4);
expect(result).toContainEqual({ id: 'item1', title: 'Item 1' });
expect(result).toContainEqual({ id: 'item3', title: 'Item 3' });
expect(result).toContainEqual({ id: 'item6', title: 'Item 6' });
});
it('handles empty items object', () => {
const result = filterItemsByPermission({}, ['agent'], getPermissions);
expect(result).toHaveLength(0);
});
it('handles custom getPermissions function', () => {
const customItems = {
item1: { name: 'Item 1', requiredPerms: ['agent', 'administrator'] },
item2: { name: 'Item 2', requiredPerms: ['contact_manage'] },
};
const customGetPermissions = item => item.requiredPerms;
const result = filterItemsByPermission(
customItems,
['agent'],
customGetPermissions
);
expect(result).toHaveLength(1);
expect(result).toContainEqual(
expect.objectContaining({ key: 'item1', name: 'Item 1' })
);
});
});
@@ -1,6 +1,7 @@
import {
getConversationDashboardRoute,
isAConversationRoute,
defaultRedirectPage,
routeIsAccessibleFor,
validateLoggedInRoutes,
isAInboxViewRoute,
@@ -14,6 +15,57 @@ describe('#routeIsAccessibleFor', () => {
});
});
describe('#defaultRedirectPage', () => {
const to = {
params: { accountId: '2' },
fullPath: '/app/accounts/2/dashboard',
name: 'home',
};
it('should return dashboard route for users with conversation permissions', () => {
const permissions = ['conversation_manage', 'agent'];
expect(defaultRedirectPage(to, permissions)).toBe('accounts/2/dashboard');
});
it('should return contacts route for users with contact permissions', () => {
const permissions = ['contact_manage'];
expect(defaultRedirectPage(to, permissions)).toBe('accounts/2/contacts');
});
it('should return reports route for users with report permissions', () => {
const permissions = ['report_manage'];
expect(defaultRedirectPage(to, permissions)).toBe(
'accounts/2/reports/overview'
);
});
it('should return portals route for users with portal permissions', () => {
const permissions = ['knowledge_base_manage'];
expect(defaultRedirectPage(to, permissions)).toBe('accounts/2/portals');
});
it('should return dashboard route as default for users with custom roles', () => {
const permissions = ['custom_role'];
expect(defaultRedirectPage(to, permissions)).toBe('accounts/2/dashboard');
});
it('should return dashboard route for users with administrator role', () => {
const permissions = ['administrator'];
expect(defaultRedirectPage(to, permissions)).toBe('accounts/2/dashboard');
});
it('should return dashboard route for users with multiple permissions', () => {
const permissions = [
'contact_manage',
'custom_role',
'conversation_manage',
'agent',
'administrator',
];
expect(defaultRedirectPage(to, permissions)).toBe('accounts/2/dashboard');
});
});
describe('#validateLoggedInRoutes', () => {
describe('when account access is missing', () => {
it('should return the login route', () => {
@@ -18,7 +18,8 @@
"STATUS": "Status",
"ACTIONS": "Actions",
"VERIFIED": "Verified",
"VERIFICATION_PENDING": "Verification Pending"
"VERIFICATION_PENDING": "Verification Pending",
"AVAILABLE_CUSTOM_ROLE": "Available custom role permissions"
},
"ADD": {
"TITLE": "Add agent to your team",
@@ -0,0 +1,86 @@
{
"CUSTOM_ROLE": {
"HEADER": "Custom Roles",
"LEARN_MORE": "Learn more about custom roles",
"DESCRIPTION": "Custom roles are roles that are created by the account owner or admin. These roles can be assigned to agents to define their access and permissions within the account. Custom roles can be created with specific permissions and access levels to suit the requirements of the organization.",
"HEADER_BTN_TXT": "Add custom role",
"LOADING": "Fetching custom roles...",
"SEARCH_404": "There are no items matching this query.",
"PAYWALL": {
"TITLE": "Upgrade to create custom roles",
"AVAILABLE_ON": "The custom role feature is only available in the Business and Enterprise plans.",
"UPGRADE_PROMPT": "Upgrade your plan to get access to advanced features like team management, automations, custom attributes, and more.",
"UPGRADE_NOW": "Upgrade now",
"CANCEL_ANYTIME": "You can change or cancel your plan anytime"
},
"ENTERPRISE_PAYWALL": {
"AVAILABLE_ON": "The custom role feature is only available in the paid plans.",
"UPGRADE_PROMPT": "Upgrade to a paid plan to access advanced features like audit logs, agent capacity, and more.",
"ASK_ADMIN": "Please reach out to your administrator for the upgrade."
},
"LIST": {
"404": "There are no custom roles available in this account.",
"TITLE": "Manage custom roles",
"DESC": "Custom roles are roles that are created by the account owner or admin. These roles can be assigned to agents to define their access and permissions within the account. Custom roles can be created with specific permissions and access levels to suit the requirements of the organization.",
"TABLE_HEADER": ["Name", "Description", "Permissions", "Actions"]
},
"PERMISSIONS": {
"CONVERSATION_MANAGE": "Manage all conversations",
"CONVERSATION_UNASSIGNED_MANAGE": "Manage unassigned conversations and those assigned to them",
"CONVERSATION_PARTICIPATING_MANAGE": "Manage participating conversations and those assigned to them",
"CONTACT_MANAGE": "Manage contacts",
"REPORT_MANAGE": "Manage reports",
"KNOWLEDGE_BASE_MANAGE": "Manage knowledge base"
},
"FORM": {
"NAME": {
"LABEL": "Name",
"PLACEHOLDER": "Please enter a name.",
"ERROR": "Name is required."
},
"DESCRIPTION": {
"LABEL": "Description",
"PLACEHOLDER": "Please enter a description.",
"ERROR": "Description is required."
},
"PERMISSIONS": {
"LABEL": "Permissions",
"ERROR": "Permissions are required."
},
"CANCEL_BUTTON_TEXT": "Cancel",
"API": {
"ERROR_MESSAGE": "Could not connect to Woot server. Please try again."
}
},
"ADD": {
"TITLE": "Add custom role",
"DESC": " Custom roles allows you to create roles with specific permissions and access levels to suit the requirements of the organization.",
"SUBMIT": "Submit",
"API": {
"SUCCESS_MESSAGE": "Custom role added successfully."
}
},
"EDIT": {
"BUTTON_TEXT": "Edit",
"TITLE": "Edit custom role",
"DESC": " Custom roles allows you to create roles with specific permissions and access levels to suit the requirements of the organization.",
"SUBMIT": "Update",
"API": {
"SUCCESS_MESSAGE": "Custom role updated successfully."
}
},
"DELETE": {
"BUTTON_TEXT": "Delete",
"API": {
"SUCCESS_MESSAGE": "Custom role deleted successfully.",
"ERROR_MESSAGE": "Could not connect to Woot server. Please try again."
},
"CONFIRM": {
"TITLE": "Confirm deletion",
"MESSAGE": "Are you sure to delete ",
"YES": "Yes, delete ",
"NO": "No, keep "
}
}
}
}
@@ -33,6 +33,7 @@ import sla from './sla.json';
import inbox from './inbox.json';
import general from './general.json';
import datePicker from './datePicker.json';
import customRole from './customRole.json';
export default {
...advancedFilters,
@@ -70,4 +71,5 @@ export default {
...inbox,
...general,
...datePicker,
...customRole,
};
@@ -145,11 +145,7 @@
},
"AVAILABILITY": {
"LABEL": "Availability",
"STATUSES_LIST": [
"Online",
"Busy",
"Offline"
],
"STATUSES_LIST": ["Online", "Busy", "Offline"],
"SET_AVAILABILITY_SUCCESS": "Availability has been set successfully",
"SET_AVAILABILITY_ERROR": "Couldn't set availability, please try again"
},
@@ -277,6 +273,7 @@
"REPORTS_TEAM": "Team",
"SET_AVAILABILITY_TITLE": "Set yourself as",
"SLA": "SLA",
"CUSTOM_ROLES": "Custom Roles",
"BETA": "Beta",
"REPORTS_OVERVIEW": "Overview",
"REAUTHORIZE": "Your inbox connection has expired, please reconnect\n to continue receiving and sending messages",
@@ -4,6 +4,16 @@ import SearchTabs from './SearchTabs.vue';
import SearchResultConversationsList from './SearchResultConversationsList.vue';
import SearchResultMessagesList from './SearchResultMessagesList.vue';
import SearchResultContactsList from './SearchResultContactsList.vue';
import Policy from 'dashboard/components/policy.vue';
import {
ROLES,
CONVERSATION_PERMISSIONS,
CONTACT_PERMISSIONS,
} from 'dashboard/constants/permissions.js';
import {
getUserPermissions,
filterItemsByPermission,
} from 'dashboard/helper/permissionsHelper.js';
import { mapGetters } from 'vuex';
import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
@@ -14,16 +24,22 @@ export default {
SearchResultContactsList,
SearchResultConversationsList,
SearchResultMessagesList,
Policy,
},
data() {
return {
selectedTab: 'all',
query: '',
contactPermissions: CONTACT_PERMISSIONS,
conversationPermissions: CONVERSATION_PERMISSIONS,
rolePermissions: ROLES,
};
},
computed: {
...mapGetters({
currentUser: 'getCurrentUser',
currentAccountId: 'getCurrentAccountId',
contactRecords: 'conversationSearch/getContactRecords',
conversationRecords: 'conversationSearch/getConversationRecords',
messageRecords: 'conversationSearch/getMessageRecords',
@@ -59,34 +75,76 @@ export default {
filterMessages() {
return this.selectedTab === 'messages' || this.isSelectedTabAll;
},
userPermissions() {
return getUserPermissions(this.currentUser, this.currentAccountId);
},
totalSearchResultsCount() {
return (
this.contacts.length + this.conversations.length + this.messages.length
const permissionCounts = {
contacts: {
permissions: [...this.rolePermissions, this.contactPermissions],
count: () => this.contacts.length,
},
conversations: {
permissions: [
...this.rolePermissions,
...this.conversationPermissions,
],
count: () => this.conversations.length + this.messages.length,
},
};
const filteredCounts = filterItemsByPermission(
permissionCounts,
this.userPermissions,
item => item.permissions,
(_, item) => item.count
);
return filteredCounts.reduce((total, count) => total + count(), 0);
},
tabs() {
return [
{
const allTabsConfig = {
all: {
key: 'all',
name: this.$t('SEARCH.TABS.ALL'),
count: this.totalSearchResultsCount,
permissions: [
this.contactPermissions,
...this.rolePermissions,
...this.conversationPermissions,
],
},
{
contacts: {
key: 'contacts',
name: this.$t('SEARCH.TABS.CONTACTS'),
count: this.contacts.length,
permissions: [...this.rolePermissions, this.contactPermissions],
},
{
conversations: {
key: 'conversations',
name: this.$t('SEARCH.TABS.CONVERSATIONS'),
count: this.conversations.length,
permissions: [
...this.rolePermissions,
...this.conversationPermissions,
],
},
{
messages: {
key: 'messages',
name: this.$t('SEARCH.TABS.MESSAGES'),
count: this.messages.length,
permissions: [
...this.rolePermissions,
...this.conversationPermissions,
],
},
];
};
return filterItemsByPermission(
allTabsConfig,
this.userPermissions,
item => item.permissions
);
},
activeTabIndex() {
const index = this.tabs.findIndex(tab => tab.key === this.selectedTab);
@@ -165,29 +223,39 @@ export default {
</header>
<div class="search-results">
<div v-if="showResultsSection">
<SearchResultContactsList
v-if="filterContacts"
:is-fetching="uiFlags.contact.isFetching"
:contacts="contacts"
:query="query"
:show-title="isSelectedTabAll"
/>
<Policy :permissions="[...rolePermissions, contactPermissions]">
<SearchResultContactsList
v-if="filterContacts"
:is-fetching="uiFlags.contact.isFetching"
:contacts="contacts"
:query="query"
:show-title="isSelectedTabAll"
/>
</Policy>
<SearchResultMessagesList
v-if="filterMessages"
:is-fetching="uiFlags.message.isFetching"
:messages="messages"
:query="query"
:show-title="isSelectedTabAll"
/>
<Policy
:permissions="[...rolePermissions, ...conversationPermissions]"
>
<SearchResultMessagesList
v-if="filterMessages"
:is-fetching="uiFlags.message.isFetching"
:messages="messages"
:query="query"
:show-title="isSelectedTabAll"
/>
</Policy>
<SearchResultConversationsList
v-if="filterConversations"
:is-fetching="uiFlags.conversation.isFetching"
:conversations="conversations"
:query="query"
:show-title="isSelectedTabAll"
/>
<Policy
:permissions="[...rolePermissions, ...conversationPermissions]"
>
<SearchResultConversationsList
v-if="filterConversations"
:is-fetching="uiFlags.conversation.isFetching"
:conversations="conversations"
:query="query"
:show-title="isSelectedTabAll"
/>
</Policy>
</div>
<div v-else-if="showEmptySearchResults" class="empty">
<fluent-icon icon="info" size="16px" class="icon" />
@@ -1,5 +1,10 @@
/* eslint-disable storybook/default-exports */
import { frontendURL } from '../../helper/URLHelper';
import {
ROLES,
CONVERSATION_PERMISSIONS,
CONTACT_PERMISSIONS,
} from 'dashboard/constants/permissions.js';
const SearchView = () => import('./components/SearchView.vue');
@@ -8,7 +13,7 @@ export const routes = [
path: frontendURL('accounts/:accountId/search'),
name: 'search',
meta: {
permissions: ['administrator', 'agent'],
permissions: [...ROLES, ...CONVERSATION_PERMISSIONS, CONTACT_PERMISSIONS],
},
component: SearchView,
},
@@ -8,7 +8,7 @@ export const routes = [
path: frontendURL('accounts/:accountId/contacts'),
name: 'contacts_dashboard',
meta: {
permissions: ['administrator', 'agent'],
permissions: ['administrator', 'agent', 'contact_manage'],
},
component: ContactsView,
},
@@ -16,7 +16,7 @@ export const routes = [
path: frontendURL('accounts/:accountId/contacts/custom_view/:id'),
name: 'contacts_segments_dashboard',
meta: {
permissions: ['administrator', 'agent'],
permissions: ['administrator', 'agent', 'contact_manage'],
},
component: ContactsView,
props: route => {
@@ -27,7 +27,7 @@ export const routes = [
path: frontendURL('accounts/:accountId/labels/:label/contacts'),
name: 'contacts_labels_dashboard',
meta: {
permissions: ['administrator', 'agent'],
permissions: ['administrator', 'agent', 'contact_manage'],
},
component: ContactsView,
props: route => {
@@ -38,7 +38,7 @@ export const routes = [
path: frontendURL('accounts/:accountId/contacts/:contactId'),
name: 'contact_profile_dashboard',
meta: {
permissions: ['administrator', 'agent'],
permissions: ['administrator', 'agent', 'contact_manage'],
},
component: ContactManageView,
props: route => {
@@ -15,6 +15,14 @@ export default {
PopOverSearch,
CmdBarConversationSnooze,
},
beforeRouteLeave(to, from, next) {
// Clear selected state if navigating away from a conversation to a route without a conversationId to prevent stale data issues
// and resolves timing issues during navigation with conversation view and other screens
if (this.conversationId) {
this.$store.dispatch('clearSelectedState');
}
next(); // Continue with navigation
},
props: {
inboxId: {
type: [String, Number],
@@ -2,13 +2,21 @@
import { frontendURL } from '../../../helper/URLHelper';
const ConversationView = () => import('./ConversationView.vue');
const CONVERSATION_PERMISSIONS = [
'administrator',
'agent',
'conversation_manage',
'conversation_unassigned_manage',
'conversation_participating_manage',
];
export default {
routes: [
{
path: frontendURL('accounts/:accountId/dashboard'),
name: 'home',
meta: {
permissions: ['administrator', 'agent'],
permissions: CONVERSATION_PERMISSIONS,
},
component: ConversationView,
props: () => {
@@ -19,7 +27,7 @@ export default {
path: frontendURL('accounts/:accountId/conversations/:conversation_id'),
name: 'inbox_conversation',
meta: {
permissions: ['administrator', 'agent'],
permissions: CONVERSATION_PERMISSIONS,
},
component: ConversationView,
props: route => {
@@ -30,7 +38,7 @@ export default {
path: frontendURL('accounts/:accountId/inbox/:inbox_id'),
name: 'inbox_dashboard',
meta: {
permissions: ['administrator', 'agent'],
permissions: CONVERSATION_PERMISSIONS,
},
component: ConversationView,
props: route => {
@@ -43,7 +51,7 @@ export default {
),
name: 'conversation_through_inbox',
meta: {
permissions: ['administrator', 'agent'],
permissions: CONVERSATION_PERMISSIONS,
},
component: ConversationView,
props: route => {
@@ -57,7 +65,7 @@ export default {
path: frontendURL('accounts/:accountId/label/:label'),
name: 'label_conversations',
meta: {
permissions: ['administrator', 'agent'],
permissions: CONVERSATION_PERMISSIONS,
},
component: ConversationView,
props: route => ({ label: route.params.label }),
@@ -68,7 +76,7 @@ export default {
),
name: 'conversations_through_label',
meta: {
permissions: ['administrator', 'agent'],
permissions: CONVERSATION_PERMISSIONS,
},
component: ConversationView,
props: route => ({
@@ -80,7 +88,7 @@ export default {
path: frontendURL('accounts/:accountId/team/:teamId'),
name: 'team_conversations',
meta: {
permissions: ['administrator', 'agent'],
permissions: CONVERSATION_PERMISSIONS,
},
component: ConversationView,
props: route => ({ teamId: route.params.teamId }),
@@ -91,7 +99,7 @@ export default {
),
name: 'conversations_through_team',
meta: {
permissions: ['administrator', 'agent'],
permissions: CONVERSATION_PERMISSIONS,
},
component: ConversationView,
props: route => ({
@@ -103,7 +111,7 @@ export default {
path: frontendURL('accounts/:accountId/custom_view/:id'),
name: 'folder_conversations',
meta: {
permissions: ['administrator', 'agent'],
permissions: CONVERSATION_PERMISSIONS,
},
component: ConversationView,
props: route => ({ foldersId: route.params.id }),
@@ -114,7 +122,7 @@ export default {
),
name: 'conversations_through_folders',
meta: {
permissions: ['administrator', 'agent'],
permissions: CONVERSATION_PERMISSIONS,
},
component: ConversationView,
props: route => ({
@@ -126,7 +134,7 @@ export default {
path: frontendURL('accounts/:accountId/mentions/conversations'),
name: 'conversation_mentions',
meta: {
permissions: ['administrator', 'agent'],
permissions: CONVERSATION_PERMISSIONS,
},
component: ConversationView,
props: () => ({ conversationType: 'mention' }),
@@ -137,7 +145,7 @@ export default {
),
name: 'conversation_through_mentions',
meta: {
permissions: ['administrator', 'agent'],
permissions: CONVERSATION_PERMISSIONS,
},
component: ConversationView,
props: route => ({
@@ -149,7 +157,7 @@ export default {
path: frontendURL('accounts/:accountId/unattended/conversations'),
name: 'conversation_unattended',
meta: {
permissions: ['administrator', 'agent'],
permissions: CONVERSATION_PERMISSIONS,
},
component: ConversationView,
props: () => ({ conversationType: 'unattended' }),
@@ -160,7 +168,7 @@ export default {
),
name: 'conversation_through_unattended',
meta: {
permissions: ['administrator', 'agent'],
permissions: CONVERSATION_PERMISSIONS,
},
component: ConversationView,
props: route => ({
@@ -172,7 +180,7 @@ export default {
path: frontendURL('accounts/:accountId/participating/conversations'),
name: 'conversation_participating',
meta: {
permissions: ['administrator', 'agent'],
permissions: CONVERSATION_PERMISSIONS,
},
component: ConversationView,
props: () => ({ conversationType: 'participating' }),
@@ -183,7 +191,7 @@ export default {
),
name: 'conversation_through_participating',
meta: {
permissions: ['administrator', 'agent'],
permissions: CONVERSATION_PERMISSIONS,
},
component: ConversationView,
props: route => ({
@@ -38,7 +38,7 @@ export default {
path: frontendURL('accounts/:accountId/suspended'),
name: 'account_suspended',
meta: {
permissions: ['administrator', 'agent'],
permissions: ['administrator', 'agent', 'custom_role'],
},
component: Suspended,
},
@@ -33,7 +33,7 @@ const portalRoutes = [
path: getPortalRoute(''),
name: 'default_portal_articles',
meta: {
permissions: ['administrator'],
permissions: ['administrator', 'knowledge_base_manage'],
},
component: DefaultPortalArticles,
},
@@ -41,7 +41,7 @@ const portalRoutes = [
path: getPortalRoute('all'),
name: 'list_all_portals',
meta: {
permissions: ['administrator', 'agent'],
permissions: ['administrator', 'agent', 'knowledge_base_manage'],
},
component: ListAllPortals,
},
@@ -54,7 +54,7 @@ const portalRoutes = [
name: 'new_portal_information',
component: PortalDetails,
meta: {
permissions: ['administrator'],
permissions: ['administrator', 'knowledge_base_manage'],
},
},
{
@@ -62,7 +62,7 @@ const portalRoutes = [
name: 'portal_customization',
component: PortalCustomization,
meta: {
permissions: ['administrator'],
permissions: ['administrator', 'knowledge_base_manage'],
},
},
{
@@ -70,7 +70,7 @@ const portalRoutes = [
name: 'portal_finish',
component: PortalSettingsFinish,
meta: {
permissions: ['administrator'],
permissions: ['administrator', 'knowledge_base_manage'],
},
},
],
@@ -79,14 +79,14 @@ const portalRoutes = [
path: getPortalRoute(':portalSlug'),
name: 'portalSlug',
meta: {
permissions: ['administrator', 'agent'],
permissions: ['administrator', 'agent', 'knowledge_base_manage'],
},
component: ShowPortal,
},
{
path: getPortalRoute(':portalSlug/edit'),
meta: {
permissions: ['administrator', 'agent'],
permissions: ['administrator', 'agent', 'knowledge_base_manage'],
},
component: EditPortal,
children: [
@@ -95,7 +95,7 @@ const portalRoutes = [
name: 'edit_portal_information',
component: EditPortalBasic,
meta: {
permissions: ['administrator'],
permissions: ['administrator', 'knowledge_base_manage'],
},
},
{
@@ -103,7 +103,7 @@ const portalRoutes = [
name: 'edit_portal_customization',
component: EditPortalCustomization,
meta: {
permissions: ['administrator'],
permissions: ['administrator', 'knowledge_base_manage'],
},
},
{
@@ -111,14 +111,14 @@ const portalRoutes = [
name: 'edit_portal_locales',
component: EditPortalLocales,
meta: {
permissions: ['administrator'],
permissions: ['administrator', 'knowledge_base_manage'],
},
},
{
path: 'categories',
name: 'list_all_locale_categories',
meta: {
permissions: ['administrator', 'agent'],
permissions: ['administrator', 'agent', 'knowledge_base_manage'],
},
component: ListAllCategories,
},
@@ -131,7 +131,7 @@ const articleRoutes = [
path: getPortalRoute(':portalSlug/:locale/articles'),
name: 'list_all_locale_articles',
meta: {
permissions: ['administrator', 'agent'],
permissions: ['administrator', 'agent', 'knowledge_base_manage'],
},
component: ListAllArticles,
},
@@ -139,7 +139,7 @@ const articleRoutes = [
path: getPortalRoute(':portalSlug/:locale/articles/new'),
name: 'new_article',
meta: {
permissions: ['administrator', 'agent'],
permissions: ['administrator', 'agent', 'knowledge_base_manage'],
},
component: NewArticle,
},
@@ -147,7 +147,7 @@ const articleRoutes = [
path: getPortalRoute(':portalSlug/:locale/articles/mine'),
name: 'list_mine_articles',
meta: {
permissions: ['administrator', 'agent'],
permissions: ['administrator', 'agent', 'knowledge_base_manage'],
},
component: ListAllArticles,
},
@@ -155,7 +155,7 @@ const articleRoutes = [
path: getPortalRoute(':portalSlug/:locale/articles/archived'),
name: 'list_archived_articles',
meta: {
permissions: ['administrator', 'agent'],
permissions: ['administrator', 'agent', 'knowledge_base_manage'],
},
component: ListAllArticles,
},
@@ -164,7 +164,7 @@ const articleRoutes = [
path: getPortalRoute(':portalSlug/:locale/articles/draft'),
name: 'list_draft_articles',
meta: {
permissions: ['administrator', 'agent'],
permissions: ['administrator', 'agent', 'knowledge_base_manage'],
},
component: ListAllArticles,
},
@@ -173,7 +173,7 @@ const articleRoutes = [
path: getPortalRoute(':portalSlug/:locale/articles/:articleSlug'),
name: 'edit_article',
meta: {
permissions: ['administrator', 'agent'],
permissions: ['administrator', 'agent', 'knowledge_base_manage'],
},
component: EditArticle,
},
@@ -184,7 +184,7 @@ const categoryRoutes = [
path: getPortalRoute(':portalSlug/:locale/categories'),
name: 'all_locale_categories',
meta: {
permissions: ['administrator', 'agent'],
permissions: ['administrator', 'agent', 'knowledge_base_manage'],
},
component: ListAllCategories,
},
@@ -192,7 +192,7 @@ const categoryRoutes = [
path: getPortalRoute(':portalSlug/:locale/categories/new'),
name: 'new_category_in_locale',
meta: {
permissions: ['administrator', 'agent'],
permissions: ['administrator', 'agent', 'knowledge_base_manage'],
},
component: NewCategory,
},
@@ -200,7 +200,7 @@ const categoryRoutes = [
path: getPortalRoute(':portalSlug/:locale/categories/:categorySlug'),
name: 'show_category',
meta: {
permissions: ['administrator', 'agent'],
permissions: ['administrator', 'agent', 'knowledge_base_manage'],
},
component: ListAllArticles,
},
@@ -210,7 +210,7 @@ const categoryRoutes = [
),
name: 'show_category_articles',
meta: {
permissions: ['administrator', 'agent'],
permissions: ['administrator', 'agent', 'knowledge_base_manage'],
},
component: ListCategoryArticles,
},
@@ -218,7 +218,7 @@ const categoryRoutes = [
path: getPortalRoute(':portalSlug/:locale/categories/:categorySlug'),
name: 'edit_category',
meta: {
permissions: ['administrator', 'agent'],
permissions: ['administrator', 'agent', 'knowledge_base_manage'],
},
component: EditCategory,
},
@@ -2,6 +2,10 @@ import { frontendURL } from 'dashboard/helper/URLHelper';
const InboxListView = () => import('./InboxList.vue');
const InboxDetailView = () => import('./InboxView.vue');
const InboxEmptyStateView = () => import('./InboxEmptyState.vue');
import {
ROLES,
CONVERSATION_PERMISSIONS,
} from 'dashboard/constants/permissions.js';
export const routes = [
{
@@ -13,7 +17,7 @@ export const routes = [
name: 'inbox_view',
component: InboxEmptyStateView,
meta: {
permissions: ['administrator', 'agent'],
permissions: [...ROLES, ...CONVERSATION_PERMISSIONS],
},
},
{
@@ -21,7 +25,7 @@ export const routes = [
name: 'inbox_view_conversation',
component: InboxDetailView,
meta: {
permissions: ['administrator', 'agent'],
permissions: [...ROLES, ...CONVERSATION_PERMISSIONS],
},
},
],
@@ -19,7 +19,7 @@ export const routes = [
name: 'notifications_index',
component: NotificationsView,
meta: {
permissions: ['administrator', 'agent'],
permissions: ['administrator', 'agent', 'custom_role'],
},
},
],
@@ -1,156 +1,164 @@
<script>
import { useVuelidate } from '@vuelidate/core';
import { required, minLength, email } from '@vuelidate/validators';
import { mapGetters } from 'vuex';
<script setup>
import { ref, computed } from 'vue';
import { useStore, useMapGetter } from 'dashboard/composables/store';
import { useI18n } from 'dashboard/composables/useI18n';
import { useAlert } from 'dashboard/composables';
import { useVuelidate } from '@vuelidate/core';
import { required, email } from '@vuelidate/validators';
import WootSubmitButton from 'dashboard/components/buttons/FormSubmitButton.vue';
export default {
props: {
onClose: {
type: Function,
default: () => {},
const emit = defineEmits(['close']);
const store = useStore();
const { t } = useI18n();
const agentName = ref('');
const agentEmail = ref('');
const selectedRoleId = ref('agent');
const rules = {
agentName: { required },
agentEmail: { required, email },
selectedRoleId: { required },
};
const v$ = useVuelidate(rules, {
agentName,
agentEmail,
selectedRoleId,
});
const uiFlags = useMapGetter('agents/getUIFlags');
const getCustomRoles = useMapGetter('customRole/getCustomRoles');
const roles = computed(() => {
const defaultRoles = [
{
id: 'administrator',
name: 'administrator',
label: t('AGENT_MGMT.AGENT_TYPES.ADMINISTRATOR'),
},
},
setup() {
return { v$: useVuelidate() };
},
data() {
return {
agentName: '',
agentEmail: '',
agentType: 'agent',
vertical: 'bottom',
horizontal: 'center',
roles: [
{
name: 'administrator',
label: this.$t('AGENT_MGMT.AGENT_TYPES.ADMINISTRATOR'),
},
{
name: 'agent',
label: this.$t('AGENT_MGMT.AGENT_TYPES.AGENT'),
},
],
show: true,
{
id: 'agent',
name: 'agent',
label: t('AGENT_MGMT.AGENT_TYPES.AGENT'),
},
];
const customRoles = getCustomRoles.value.map(role => ({
id: role.id,
name: `custom_${role.id}`,
label: role.name,
}));
return [...defaultRoles, ...customRoles];
});
const selectedRole = computed(() =>
roles.value.find(
role =>
role.id === selectedRoleId.value || role.name === selectedRoleId.value
)
);
const addAgent = async () => {
v$.value.$touch();
if (v$.value.$invalid) return;
try {
const payload = {
name: agentName.value,
email: agentEmail.value,
};
},
computed: {
...mapGetters({
uiFlags: 'agents/getUIFlags',
}),
},
validations: {
agentName: {
required,
minLength: minLength(1),
},
agentEmail: {
required,
email,
},
agentType: {
required,
},
},
methods: {
async addAgent() {
try {
await this.$store.dispatch('agents/create', {
name: this.agentName,
email: this.agentEmail,
role: this.agentType,
});
useAlert(this.$t('AGENT_MGMT.ADD.API.SUCCESS_MESSAGE'));
this.onClose();
} catch (error) {
const {
response: {
data: {
error: errorResponse = '',
attributes: attributes = [],
message: attrError = '',
} = {},
} = {},
} = error;
if (selectedRole.value.name.startsWith('custom_')) {
payload.custom_role_id = selectedRole.value.id;
} else {
payload.role = selectedRole.value.name;
}
let errorMessage = '';
if (error?.response?.status === 422 && !attributes.includes('base')) {
errorMessage = this.$t('AGENT_MGMT.ADD.API.EXIST_MESSAGE');
} else {
errorMessage = this.$t('AGENT_MGMT.ADD.API.ERROR_MESSAGE');
}
useAlert(errorResponse || attrError || errorMessage);
}
},
},
await store.dispatch('agents/create', payload);
useAlert(t('AGENT_MGMT.ADD.API.SUCCESS_MESSAGE'));
emit('close');
} catch (error) {
const {
response: {
data: {
error: errorResponse = '',
attributes: attributes = [],
message: attrError = '',
} = {},
} = {},
} = error;
let errorMessage = '';
if (error?.response?.status === 422 && !attributes.includes('base')) {
errorMessage = t('AGENT_MGMT.ADD.API.EXIST_MESSAGE');
} else {
errorMessage = t('AGENT_MGMT.ADD.API.ERROR_MESSAGE');
}
useAlert(errorResponse || attrError || errorMessage);
}
};
</script>
<template>
<woot-modal :show.sync="show" :on-close="onClose">
<div class="flex flex-col h-auto overflow-auto">
<woot-modal-header
:header-title="$t('AGENT_MGMT.ADD.TITLE')"
:header-content="$t('AGENT_MGMT.ADD.DESC')"
/>
<div class="flex flex-col h-auto overflow-auto">
<woot-modal-header
:header-title="$t('AGENT_MGMT.ADD.TITLE')"
:header-content="$t('AGENT_MGMT.ADD.DESC')"
/>
<form class="flex flex-col items-start w-full" @submit.prevent="addAgent">
<div class="w-full">
<label :class="{ error: v$.agentName.$error }">
{{ $t('AGENT_MGMT.ADD.FORM.NAME.LABEL') }}
<input
v-model.trim="agentName"
type="text"
:placeholder="$t('AGENT_MGMT.ADD.FORM.NAME.PLACEHOLDER')"
@input="v$.agentName.$touch"
/>
</label>
</div>
<form
class="flex flex-col items-start w-full"
@submit.prevent="addAgent()"
>
<div class="w-full">
<label :class="{ error: v$.selectedRoleId.$error }">
{{ $t('AGENT_MGMT.ADD.FORM.AGENT_TYPE.LABEL') }}
<select v-model="selectedRoleId" @change="v$.selectedRoleId.$touch">
<option v-for="role in roles" :key="role.id" :value="role.id">
{{ role.label }}
</option>
</select>
<span v-if="v$.selectedRoleId.$error" class="message">
{{ $t('AGENT_MGMT.ADD.FORM.AGENT_TYPE.ERROR') }}
</span>
</label>
</div>
<div class="w-full">
<label :class="{ error: v$.agentEmail.$error }">
{{ $t('AGENT_MGMT.ADD.FORM.EMAIL.LABEL') }}
<input
v-model.trim="agentEmail"
type="email"
:placeholder="$t('AGENT_MGMT.ADD.FORM.EMAIL.PLACEHOLDER')"
@input="v$.agentEmail.$touch"
/>
</label>
</div>
<div class="flex flex-row justify-end w-full gap-2 px-0 py-2">
<div class="w-full">
<label :class="{ error: v$.agentName.$error }">
{{ $t('AGENT_MGMT.ADD.FORM.NAME.LABEL') }}
<input
v-model.trim="agentName"
type="text"
:placeholder="$t('AGENT_MGMT.ADD.FORM.NAME.PLACEHOLDER')"
@input="v$.agentName.$touch"
/>
</label>
<WootSubmitButton
:disabled="v$.$invalid || uiFlags.isCreating"
:button-text="$t('AGENT_MGMT.ADD.FORM.SUBMIT')"
:loading="uiFlags.isCreating"
/>
<button class="button clear" @click.prevent="emit('close')">
{{ $t('AGENT_MGMT.ADD.CANCEL_BUTTON_TEXT') }}
</button>
</div>
<div class="w-full">
<label :class="{ error: v$.agentType.$error }">
{{ $t('AGENT_MGMT.ADD.FORM.AGENT_TYPE.LABEL') }}
<select v-model="agentType">
<option v-for="role in roles" :key="role.name" :value="role.name">
{{ role.label }}
</option>
</select>
<span v-if="v$.agentType.$error" class="message">
{{ $t('AGENT_MGMT.ADD.FORM.AGENT_TYPE.ERROR') }}
</span>
</label>
</div>
<div class="w-full">
<label :class="{ error: v$.agentEmail.$error }">
{{ $t('AGENT_MGMT.ADD.FORM.EMAIL.LABEL') }}
<input
v-model.trim="agentEmail"
type="text"
:placeholder="$t('AGENT_MGMT.ADD.FORM.EMAIL.PLACEHOLDER')"
@input="v$.agentEmail.$touch"
/>
</label>
</div>
<div class="flex flex-row justify-end w-full gap-2 px-0 py-2">
<div class="w-full">
<woot-submit-button
:disabled="
v$.agentEmail.$invalid ||
v$.agentName.$invalid ||
uiFlags.isCreating
"
:button-text="$t('AGENT_MGMT.ADD.FORM.SUBMIT')"
:loading="uiFlags.isCreating"
/>
<button class="button clear" @click.prevent="onClose">
{{ $t('AGENT_MGMT.ADD.CANCEL_BUTTON_TEXT') }}
</button>
</div>
</div>
</form>
</div>
</woot-modal>
</div>
</form>
</div>
</template>
@@ -1,203 +1,220 @@
<script>
<script setup>
import { ref, computed } from 'vue';
import { useVuelidate } from '@vuelidate/core';
import { required, minLength } from '@vuelidate/validators';
import { mapGetters } from 'vuex';
import { useStore, useMapGetter } from 'dashboard/composables/store';
import { useI18n } from 'dashboard/composables/useI18n';
import { useAlert } from 'dashboard/composables';
import WootSubmitButton from '../../../../components/buttons/FormSubmitButton.vue';
import Modal from '../../../../components/Modal.vue';
import WootSubmitButton from 'dashboard/components/buttons/FormSubmitButton.vue';
import Auth from '../../../../api/auth';
import wootConstants from 'dashboard/constants/globals';
const props = defineProps({
id: {
type: Number,
required: true,
},
name: {
type: String,
required: true,
},
email: {
type: String,
default: '',
},
type: {
type: String,
default: '',
},
availability: {
type: String,
default: '',
},
customRoleId: {
type: Number,
default: null,
},
});
const emit = defineEmits(['close']);
const { AVAILABILITY_STATUS_KEYS } = wootConstants;
export default {
components: {
WootSubmitButton,
Modal,
},
props: {
id: {
type: Number,
required: true,
const store = useStore();
const { t } = useI18n();
const agentName = ref(props.name);
const agentAvailability = ref(props.availability);
const selectedRoleId = ref(props.customRoleId || props.type);
const agentCredentials = ref({ email: props.email });
const rules = {
agentName: { required, minLength: minLength(1) },
selectedRoleId: { required },
agentAvailability: { required },
};
const v$ = useVuelidate(rules, {
agentName,
selectedRoleId,
agentAvailability,
});
const pageTitle = computed(
() => `${t('AGENT_MGMT.EDIT.TITLE')} - ${props.name}`
);
const uiFlags = useMapGetter('agents/getUIFlags');
const getCustomRoles = useMapGetter('customRole/getCustomRoles');
const roles = computed(() => {
const defaultRoles = [
{
id: 'administrator',
name: 'administrator',
label: t('AGENT_MGMT.AGENT_TYPES.ADMINISTRATOR'),
},
name: {
type: String,
required: true,
{
id: 'agent',
name: 'agent',
label: t('AGENT_MGMT.AGENT_TYPES.AGENT'),
},
email: {
type: String,
default: '',
},
type: {
type: String,
default: '',
},
availability: {
type: String,
default: '',
},
onClose: {
type: Function,
required: true,
},
},
setup() {
return { v$: useVuelidate() };
},
data() {
return {
roles: [
{
name: 'administrator',
label: this.$t('AGENT_MGMT.AGENT_TYPES.ADMINISTRATOR'),
},
{
name: 'agent',
label: this.$t('AGENT_MGMT.AGENT_TYPES.AGENT'),
},
],
agentName: this.name,
agentAvailability: this.availability,
agentType: this.type,
agentCredentials: {
email: this.email,
},
show: true,
];
const customRoles = getCustomRoles.value.map(role => ({
id: role.id,
name: `custom_${role.id}`,
label: role.name,
}));
return [...defaultRoles, ...customRoles];
});
const selectedRole = computed(() =>
roles.value.find(
role =>
role.id === selectedRoleId.value || role.name === selectedRoleId.value
)
);
const availabilityStatuses = computed(() =>
t('PROFILE_SETTINGS.FORM.AVAILABILITY.STATUSES_LIST').map(
(statusLabel, index) => ({
label: statusLabel,
value: AVAILABILITY_STATUS_KEYS[index],
disabled: props.availability === AVAILABILITY_STATUS_KEYS[index],
})
)
);
const editAgent = async () => {
v$.value.$touch();
if (v$.value.$invalid) return;
try {
const payload = {
id: props.id,
name: agentName.value,
availability: agentAvailability.value,
};
},
validations: {
agentName: {
required,
minLength: minLength(1),
},
agentType: {
required,
},
agentAvailability: {
required,
},
},
computed: {
pageTitle() {
return `${this.$t('AGENT_MGMT.EDIT.TITLE')} - ${this.name}`;
},
...mapGetters({
uiFlags: 'agents/getUIFlags',
}),
availabilityStatuses() {
return this.$t('PROFILE_SETTINGS.FORM.AVAILABILITY.STATUSES_LIST').map(
(statusLabel, index) => ({
label: statusLabel,
value: AVAILABILITY_STATUS_KEYS[index],
disabled:
this.currentUserAvailability === AVAILABILITY_STATUS_KEYS[index],
})
);
},
},
methods: {
async editAgent() {
try {
await this.$store.dispatch('agents/update', {
id: this.id,
name: this.agentName,
role: this.agentType,
availability: this.agentAvailability,
});
useAlert(this.$t('AGENT_MGMT.EDIT.API.SUCCESS_MESSAGE'));
this.onClose();
} catch (error) {
useAlert(this.$t('AGENT_MGMT.EDIT.API.ERROR_MESSAGE'));
}
},
async resetPassword() {
try {
await Auth.resetPassword(this.agentCredentials);
useAlert(
this.$t('AGENT_MGMT.EDIT.PASSWORD_RESET.ADMIN_SUCCESS_MESSAGE')
);
} catch (error) {
useAlert(this.$t('AGENT_MGMT.EDIT.PASSWORD_RESET.ERROR_MESSAGE'));
}
},
},
if (selectedRole.value.name.startsWith('custom_')) {
payload.custom_role_id = selectedRole.value.id;
} else {
payload.role = selectedRole.value.name;
payload.custom_role_id = null;
}
await store.dispatch('agents/update', payload);
useAlert(t('AGENT_MGMT.EDIT.API.SUCCESS_MESSAGE'));
emit('close');
} catch (error) {
useAlert(t('AGENT_MGMT.EDIT.API.ERROR_MESSAGE'));
}
};
const resetPassword = async () => {
try {
await Auth.resetPassword(agentCredentials.value);
useAlert(t('AGENT_MGMT.EDIT.PASSWORD_RESET.ADMIN_SUCCESS_MESSAGE'));
} catch (error) {
useAlert(t('AGENT_MGMT.EDIT.PASSWORD_RESET.ERROR_MESSAGE'));
}
};
</script>
<template>
<Modal :show.sync="show" :on-close="onClose">
<div class="flex flex-col h-auto overflow-auto">
<woot-modal-header :header-title="pageTitle" />
<form class="w-full" @submit.prevent="editAgent()">
<div class="w-full">
<label :class="{ error: v$.agentName.$error }">
{{ $t('AGENT_MGMT.EDIT.FORM.NAME.LABEL') }}
<input
v-model.trim="agentName"
type="text"
:placeholder="$t('AGENT_MGMT.EDIT.FORM.NAME.PLACEHOLDER')"
@input="v$.agentName.$touch"
/>
</label>
</div>
<div class="flex flex-col h-auto overflow-auto">
<woot-modal-header :header-title="pageTitle" />
<form class="w-full" @submit.prevent="editAgent">
<div class="w-full">
<label :class="{ error: v$.agentName.$error }">
{{ $t('AGENT_MGMT.EDIT.FORM.NAME.LABEL') }}
<input
v-model.trim="agentName"
type="text"
:placeholder="$t('AGENT_MGMT.EDIT.FORM.NAME.PLACEHOLDER')"
@input="v$.agentName.$touch"
/>
</label>
</div>
<div class="w-full">
<label :class="{ error: v$.agentType.$error }">
{{ $t('AGENT_MGMT.EDIT.FORM.AGENT_TYPE.LABEL') }}
<select v-model="agentType">
<option v-for="role in roles" :key="role.name" :value="role.name">
{{ role.label }}
</option>
</select>
<span v-if="v$.agentType.$error" class="message">
{{ $t('AGENT_MGMT.EDIT.FORM.AGENT_TYPE.ERROR') }}
</span>
</label>
</div>
<div class="w-full">
<label :class="{ error: v$.selectedRoleId.$error }">
{{ $t('AGENT_MGMT.EDIT.FORM.AGENT_TYPE.LABEL') }}
<select v-model="selectedRoleId" @change="v$.selectedRoleId.$touch">
<option v-for="role in roles" :key="role.id" :value="role.id">
{{ role.label }}
</option>
</select>
<span v-if="v$.selectedRoleId.$error" class="message">
{{ $t('AGENT_MGMT.EDIT.FORM.AGENT_TYPE.ERROR') }}
</span>
</label>
</div>
<div class="w-full">
<label :class="{ error: v$.agentAvailability.$error }">
{{ $t('PROFILE_SETTINGS.FORM.AVAILABILITY.LABEL') }}
<select v-model="agentAvailability">
<option
v-for="role in availabilityStatuses"
:key="role.value"
:value="role.value"
>
{{ role.label }}
</option>
</select>
<span v-if="v$.agentAvailability.$error" class="message">
{{ $t('AGENT_MGMT.EDIT.FORM.AGENT_AVAILABILITY.ERROR') }}
</span>
</label>
</div>
<div class="flex flex-row justify-end w-full gap-2 px-0 py-2">
<div class="w-[50%]">
<WootSubmitButton
:disabled="
v$.agentType.$invalid ||
v$.agentName.$invalid ||
uiFlags.isUpdating
"
:button-text="$t('AGENT_MGMT.EDIT.FORM.SUBMIT')"
:loading="uiFlags.isUpdating"
/>
<button class="button clear" @click.prevent="onClose">
{{ $t('AGENT_MGMT.EDIT.CANCEL_BUTTON_TEXT') }}
</button>
</div>
<div class="w-[50%] text-right">
<woot-button
icon="lock-closed"
variant="clear"
@click.prevent="resetPassword"
<div class="w-full">
<label :class="{ error: v$.agentAvailability.$error }">
{{ $t('PROFILE_SETTINGS.FORM.AVAILABILITY.LABEL') }}
<select
v-model="agentAvailability"
@change="v$.agentAvailability.$touch"
>
<option
v-for="status in availabilityStatuses"
:key="status.value"
:value="status.value"
>
{{ $t('AGENT_MGMT.EDIT.PASSWORD_RESET.ADMIN_RESET_BUTTON') }}
</woot-button>
</div>
{{ status.label }}
</option>
</select>
<span v-if="v$.agentAvailability.$error" class="message">
{{ $t('AGENT_MGMT.EDIT.FORM.AGENT_AVAILABILITY.ERROR') }}
</span>
</label>
</div>
<div class="flex flex-row justify-end w-full gap-2 px-0 py-2">
<div class="w-[50%]">
<WootSubmitButton
:disabled="v$.$invalid || uiFlags.isUpdating"
:button-text="$t('AGENT_MGMT.EDIT.FORM.SUBMIT')"
:loading="uiFlags.isUpdating"
/>
<button class="button clear" @click.prevent="emit('close')">
{{ $t('AGENT_MGMT.EDIT.CANCEL_BUTTON_TEXT') }}
</button>
</div>
</form>
</div>
</Modal>
<div class="w-[50%] text-right">
<woot-button
icon="lock-closed"
variant="clear"
@click.prevent="resetPassword"
>
{{ $t('AGENT_MGMT.EDIT.PASSWORD_RESET.ADMIN_RESET_BUTTON') }}
</woot-button>
</div>
</div>
</form>
</div>
</template>
@@ -3,7 +3,11 @@ import { useAlert } from 'dashboard/composables';
import { computed, onMounted, ref } from 'vue';
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
import { useI18n } from 'dashboard/composables/useI18n';
import { useStoreGetters, useStore } from 'dashboard/composables/store';
import {
useStoreGetters,
useStore,
useMapGetter,
} from 'dashboard/composables/store';
import AddAgent from './AddAgent.vue';
import EditAgent from './EditAgent.vue';
@@ -34,11 +38,32 @@ const deleteMessage = computed(() => {
const agentList = computed(() => getters['agents/getAgents'].value);
const uiFlags = computed(() => getters['agents/getUIFlags'].value);
const currentUserId = computed(() => getters.getCurrentUserID.value);
const customRoles = useMapGetter('customRole/getCustomRoles');
onMounted(() => {
store.dispatch('agents/get');
store.dispatch('customRole/getCustomRole');
});
const findCustomRole = agent =>
customRoles.value.find(role => role.id === agent.custom_role_id);
const getAgentRoleName = agent => {
if (!agent.custom_role_id) {
return t(`AGENT_MGMT.AGENT_TYPES.${agent.role.toUpperCase()}`);
}
const customRole = findCustomRole(agent);
return customRole ? customRole.name : '';
};
const getAgentRolePermissions = agent => {
if (!agent.custom_role_id) {
return [];
}
const customRole = findCustomRole(agent);
return customRole?.permissions || [];
};
const verifiedAdministrators = computed(() => {
return agentList.value.filter(
agent => agent.role === 'administrator' && agent.confirmed
@@ -63,6 +88,7 @@ const showDeleteAction = agent => {
}
return true;
};
const showAlertMessage = message => {
loading.value[currentAgent.value.id] = false;
currentAgent.value = {};
@@ -124,7 +150,7 @@ const confirmDeletion = () => {
>
<template #actions>
<woot-button
class="button nice rounded-md"
class="rounded-md button nice"
icon="add-circle"
@click="openAddPopup"
>
@@ -140,7 +166,7 @@ const confirmDeletion = () => {
>
<tr v-for="(agent, index) in agentList" :key="agent.email">
<td class="py-4 ltr:pr-4 rtl:pl-4">
<div class="flex items-center flex-row gap-4">
<div class="flex flex-row items-center gap-4">
<Thumbnail
:src="agent.thumbnail"
:username="agent.name"
@@ -156,9 +182,39 @@ const confirmDeletion = () => {
</div>
</td>
<td class="py-4 ltr:pr-4 rtl:pl-4">
<span class="block font-medium capitalize">
{{ $t(`AGENT_MGMT.AGENT_TYPES.${agent.role.toUpperCase()}`) }}
<td class="relative py-4 ltr:pr-4 rtl:pl-4">
<span
class="block font-medium w-fit"
:class="{
'hover:text-gray-900 group cursor-pointer':
agent.custom_role_id,
}"
>
{{ getAgentRoleName(agent) }}
<div
class="absolute left-0 z-10 hidden max-w-[300px] w-auto bg-white rounded-xl border border-slate-50 shadow-lg top-14 md:top-12 dark:bg-slate-800 dark:border-slate-700"
:class="{ 'group-hover:block': agent.custom_role_id }"
>
<div class="flex flex-col gap-1 p-4">
<span class="font-semibold">
{{ $t('AGENT_MGMT.LIST.AVAILABLE_CUSTOM_ROLE') }}
</span>
<ul class="pl-4 mb-0 list-disc">
<li
v-for="permission in getAgentRolePermissions(agent)"
:key="permission"
class="font-normal"
>
{{
$t(
`CUSTOM_ROLE.PERMISSIONS.${permission.toUpperCase()}`
)
}}
</li>
</ul>
</div>
</div>
</span>
</td>
<td class="py-4 ltr:pr-4 rtl:pl-4">
@@ -200,7 +256,7 @@ const confirmDeletion = () => {
</template>
<woot-modal :show.sync="showAddPopup" :on-close="hideAddPopup">
<AddAgent :on-close="hideAddPopup" />
<AddAgent @close="hideAddPopup" />
</woot-modal>
<woot-modal :show.sync="showEditPopup" :on-close="hideEditPopup">
@@ -211,7 +267,8 @@ const confirmDeletion = () => {
:type="currentAgent.role"
:email="currentAgent.email"
:availability="currentAgent.availability_status"
:on-close="hideEditPopup"
:custom-role-id="currentAgent.custom_role_id"
@close="hideEditPopup"
/>
</woot-modal>
@@ -1,5 +1,8 @@
import { frontendURL } from '../../../../helper/URLHelper';
import {
ROLES,
CONVERSATION_PERMISSIONS,
} from 'dashboard/constants/permissions.js';
const SettingsWrapper = () => import('../SettingsWrapper.vue');
const CannedHome = () => import('./Index.vue');
@@ -17,7 +20,7 @@ export default {
path: 'list',
name: 'canned_list',
meta: {
permissions: ['administrator', 'agent'],
permissions: [...ROLES, ...CONVERSATION_PERMISSIONS],
},
component: CannedHome,
},
@@ -0,0 +1,79 @@
<script setup>
defineProps({
featurePrefix: {
type: String,
required: true,
},
i18nKey: {
type: String,
required: true,
},
isOnChatwootCloud: {
type: Boolean,
default: false,
},
isSuperAdmin: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(['click']);
</script>
<template>
<div
class="flex flex-col max-w-md px-6 py-6 bg-white border shadow dark:bg-slate-800 rounded-xl border-slate-100 dark:border-slate-900"
>
<div class="flex items-center w-full gap-2 mb-4">
<span
class="flex items-center justify-center w-6 h-6 rounded-full bg-woot-75/70 dark:bg-woot-800/40"
>
<fluent-icon
size="14"
class="flex-shrink-0 text-woot-500 dark:text-woot-500"
icon="lock-closed"
/>
</span>
<span class="text-base font-medium text-slate-900 dark:text-white">
{{ $t(`${featurePrefix}.PAYWALL.TITLE`) }}
</span>
</div>
<p
class="text-sm font-normal"
v-html="$t(`${featurePrefix}.${i18nKey}.AVAILABLE_ON`)"
/>
<p class="text-sm font-normal">
{{ $t(`${featurePrefix}.${i18nKey}.UPGRADE_PROMPT`) }}
<span v-if="!isOnChatwootCloud && !isSuperAdmin">
{{ $t(`${featurePrefix}.ENTERPRISE_PAYWALL.ASK_ADMIN`) }}
</span>
</p>
<template v-if="isOnChatwootCloud || true">
<woot-button
color-scheme="primary"
class="w-full mt-2 text-center rounded-xl"
size="expanded"
is-expanded
@click="emit('click')"
>
{{ $t(`${featurePrefix}.PAYWALL.UPGRADE_NOW`) }}
</woot-button>
<span class="mt-2 text-xs tracking-tight text-center">
{{ $t(`${featurePrefix}.PAYWALL.CANCEL_ANYTIME`) }}
</span>
</template>
<template v-else-if="isSuperAdmin">
<a href="/super_admin" class="block w-full">
<woot-button
color-scheme="primary"
class="w-full mt-2 text-center rounded-xl"
size="expanded"
is-expanded
>
{{ $t(`${featurePrefix}.PAYWALL.UPGRADE_NOW`) }}
</woot-button>
</a>
</template>
</div>
</template>
@@ -0,0 +1,189 @@
<script setup>
import { useAlert } from 'dashboard/composables';
import SettingsLayout from '../SettingsLayout.vue';
import BaseSettingsHeader from '../components/BaseSettingsHeader.vue';
import CustomRoleModal from './component/CustomRoleModal.vue';
import CustomRoleTableBody from './component/CustomRoleTableBody.vue';
import CustomRolePaywall from './component/CustomRolePaywall.vue';
import { computed, onMounted, ref } from 'vue';
import { useI18n } from 'dashboard/composables/useI18n';
import { useStore, useMapGetter } from 'dashboard/composables/store';
const store = useStore();
const { t } = useI18n();
const showCustomRoleModal = ref(false);
const customRoleModalMode = ref('add');
const selectedRole = ref(null);
const loading = ref({});
const showDeleteConfirmationPopup = ref(false);
const activeResponse = ref({});
const records = useMapGetter('customRole/getCustomRoles');
const uiFlags = useMapGetter('customRole/getUIFlags');
const deleteConfirmText = computed(
() => `${t('CUSTOM_ROLE.DELETE.CONFIRM.YES')} ${activeResponse.value.name}`
);
const deleteRejectText = computed(
() => `${t('CUSTOM_ROLE.DELETE.CONFIRM.NO')} ${activeResponse.value.name}`
);
const deleteMessage = computed(() => {
return ` ${activeResponse.value.name} ? `;
});
const isFeatureEnabledOnAccount = useMapGetter(
'accounts/isFeatureEnabledonAccount'
);
const currentAccountId = useMapGetter('getCurrentAccountId');
const isBehindAPaywall = computed(() => {
return !isFeatureEnabledOnAccount.value(
currentAccountId.value,
'custom_roles'
);
});
const fetchCustomRoles = async () => {
try {
await store.dispatch('customRole/getCustomRole');
} catch (error) {
// Ignore Error
}
};
onMounted(() => {
fetchCustomRoles();
});
const showAlertMessage = message => {
loading.value[activeResponse.value.id] = false;
activeResponse.value = {};
useAlert(message);
};
const openAddModal = () => {
if (isBehindAPaywall.value) return;
customRoleModalMode.value = 'add';
selectedRole.value = null;
showCustomRoleModal.value = true;
};
const openEditModal = role => {
customRoleModalMode.value = 'edit';
selectedRole.value = role;
showCustomRoleModal.value = true;
};
const hideCustomRoleModal = () => {
selectedRole.value = null;
showCustomRoleModal.value = false;
};
const openDeletePopup = response => {
showDeleteConfirmationPopup.value = true;
activeResponse.value = response;
};
const closeDeletePopup = () => {
showDeleteConfirmationPopup.value = false;
};
const deleteCustomRole = async id => {
try {
await store.dispatch('customRole/deleteCustomRole', id);
showAlertMessage(t('CUSTOM_ROLE.DELETE.API.SUCCESS_MESSAGE'));
} catch (error) {
const errorMessage =
error?.message || t('CUSTOM_ROLE.DELETE.API.ERROR_MESSAGE');
showAlertMessage(errorMessage);
}
};
const confirmDeletion = () => {
loading[activeResponse.value.id] = true;
closeDeletePopup();
deleteCustomRole(activeResponse.value.id);
};
</script>
<template>
<SettingsLayout
:is-loading="uiFlags.fetchingList"
:loading-message="$t('CUSTOM_ROLE.LOADING')"
:no-records-found="!records.length && !isBehindAPaywall"
:no-records-message="$t('CUSTOM_ROLE.LIST.404')"
>
<template #header>
<BaseSettingsHeader
:title="$t('CUSTOM_ROLE.HEADER')"
:description="$t('CUSTOM_ROLE.DESCRIPTION')"
:link-text="$t('CUSTOM_ROLE.LEARN_MORE')"
feature-name="canned_responses"
>
<template #actions>
<woot-button
class="rounded-md button nice"
icon="add-circle"
:disabled="isBehindAPaywall"
@click="openAddModal"
>
{{ $t('CUSTOM_ROLE.HEADER_BTN_TXT') }}
</woot-button>
</template>
</BaseSettingsHeader>
</template>
<template #body>
<CustomRolePaywall v-if="isBehindAPaywall" />
<table
v-else
class="min-w-full overflow-x-auto divide-y divide-slate-75 dark:divide-slate-700"
>
<thead>
<th
v-for="thHeader in $t('CUSTOM_ROLE.LIST.TABLE_HEADER')"
:key="thHeader"
class="py-4 pr-4 font-semibold text-left text-slate-700 dark:text-slate-300"
>
<span class="mb-0">
{{ thHeader }}
</span>
</th>
</thead>
<CustomRoleTableBody
:roles="records"
:loading="loading"
@edit="openEditModal"
@delete="openDeletePopup"
/>
</table>
</template>
<woot-modal
:show.sync="showCustomRoleModal"
:on-close="hideCustomRoleModal"
>
<CustomRoleModal
:mode="customRoleModalMode"
:selected-role="selectedRole"
@close="hideCustomRoleModal"
/>
</woot-modal>
<woot-delete-modal
:show.sync="showDeleteConfirmationPopup"
:on-close="closeDeletePopup"
:on-confirm="confirmDeletion"
:title="$t('CUSTOM_ROLE.DELETE.CONFIRM.TITLE')"
:message="$t('CUSTOM_ROLE.DELETE.CONFIRM.MESSAGE')"
:message-value="deleteMessage"
:confirm-text="deleteConfirmText"
:reject-text="deleteRejectText"
/>
</SettingsLayout>
</template>
@@ -0,0 +1,248 @@
<script setup>
import { ref, reactive, computed, onMounted, watch } from 'vue';
import { useStore } from 'dashboard/composables/store';
import { useI18n } from 'dashboard/composables/useI18n';
import { useVuelidate } from '@vuelidate/core';
import { required, minLength } from '@vuelidate/validators';
import { useAlert } from 'dashboard/composables';
import {
AVAILABLE_CUSTOM_ROLE_PERMISSIONS,
MANAGE_ALL_CONVERSATION_PERMISSIONS,
CONVERSATION_UNASSIGNED_PERMISSIONS,
CONVERSATION_PARTICIPATING_PERMISSIONS,
} from 'dashboard/constants/permissions.js';
import WootSubmitButton from 'dashboard/components/buttons/FormSubmitButton.vue';
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
const props = defineProps({
mode: {
type: String,
default: 'add',
validator: value => ['add', 'edit'].includes(value),
},
selectedRole: {
type: Object,
default: () => ({}),
},
});
const emit = defineEmits(['close']);
const store = useStore();
const { t } = useI18n();
const name = ref('');
const description = ref('');
const selectedPermissions = ref([]);
const nameInput = ref(null);
const addCustomRole = reactive({
showLoading: false,
message: '',
});
const rules = computed(() => ({
name: { required, minLength: minLength(2) },
description: { required },
selectedPermissions: { required, minLength: minLength(1) },
}));
const v$ = useVuelidate(rules, { name, description, selectedPermissions });
const resetForm = () => {
name.value = '';
description.value = '';
selectedPermissions.value = [];
v$.value.$reset();
};
const populateEditForm = () => {
name.value = props.selectedRole.name || '';
description.value = props.selectedRole.description || '';
selectedPermissions.value = props.selectedRole.permissions || [];
};
watch(
selectedPermissions,
(newValue, oldValue) => {
// Check if manage all conversation permission is added or removed
const hasAddedManageAllConversation =
newValue.includes(MANAGE_ALL_CONVERSATION_PERMISSIONS) &&
!oldValue.includes(MANAGE_ALL_CONVERSATION_PERMISSIONS);
const hasRemovedManageAllConversation =
oldValue.includes(MANAGE_ALL_CONVERSATION_PERMISSIONS) &&
!newValue.includes(MANAGE_ALL_CONVERSATION_PERMISSIONS);
if (hasAddedManageAllConversation) {
// If manage all conversation permission is added,
// then add unassigned and participating permissions automatically
selectedPermissions.value = [
...new Set([
...selectedPermissions.value,
CONVERSATION_UNASSIGNED_PERMISSIONS,
CONVERSATION_PARTICIPATING_PERMISSIONS,
]),
];
} else if (hasRemovedManageAllConversation) {
// If manage all conversation permission is removed,
// then only remove manage all conversation permission
selectedPermissions.value = selectedPermissions.value.filter(
p => p !== MANAGE_ALL_CONVERSATION_PERMISSIONS
);
}
},
{ deep: true }
);
onMounted(() => {
if (props.mode === 'edit') {
populateEditForm();
}
// Focus the name input when mounted
nameInput.value?.focus();
});
const getTranslationKey = base => {
return props.mode === 'edit'
? `CUSTOM_ROLE.EDIT.${base}`
: `CUSTOM_ROLE.ADD.${base}`;
};
const modalTitle = computed(() => t(getTranslationKey('TITLE')));
const modalDescription = computed(() => t(getTranslationKey('DESC')));
const submitButtonText = computed(() => t(getTranslationKey('SUBMIT')));
const handleCustomRole = async () => {
v$.value.$touch();
if (v$.value.$invalid) return;
addCustomRole.showLoading = true;
try {
const roleData = {
name: name.value,
description: description.value,
permissions: selectedPermissions.value,
};
if (props.mode === 'edit') {
await store.dispatch('customRole/updateCustomRole', {
id: props.selectedRole.id,
...roleData,
});
useAlert(t('CUSTOM_ROLE.EDIT.API.SUCCESS_MESSAGE'));
} else {
await store.dispatch('customRole/createCustomRole', roleData);
useAlert(t('CUSTOM_ROLE.ADD.API.SUCCESS_MESSAGE'));
}
resetForm();
emit('close');
} catch (error) {
const errorMessage =
error?.message || t(`CUSTOM_ROLE.FORM.API.ERROR_MESSAGE`);
useAlert(errorMessage);
} finally {
addCustomRole.showLoading = false;
}
};
const isSubmitDisabled = computed(
() => v$.value.$invalid || addCustomRole.showLoading
);
</script>
<template>
<div class="flex flex-col h-auto overflow-auto">
<woot-modal-header
:header-title="modalTitle"
:header-content="modalDescription"
/>
<form class="flex flex-col w-full" @submit.prevent="handleCustomRole">
<div class="w-full">
<label :class="{ 'text-red-500': v$.name.$error }">
{{ $t('CUSTOM_ROLE.FORM.NAME.LABEL') }}
<input
ref="nameInput"
v-model.trim="name"
type="text"
:class="{ '!border-red-500': v$.name.$error }"
:placeholder="$t('CUSTOM_ROLE.FORM.NAME.PLACEHOLDER')"
@blur="v$.name.$touch"
/>
</label>
</div>
<div class="w-full">
<label :class="{ 'text-red-500': v$.description.$error }">
{{ $t('CUSTOM_ROLE.FORM.DESCRIPTION.LABEL') }}
</label>
<div class="editor-wrap">
<WootMessageEditor
v-model="description"
class="message-editor [&>div]:px-1 h-28"
:class="{ editor_warning: v$.description.$error }"
enable-variables
:focus-on-mount="false"
:enable-canned-responses="false"
:placeholder="$t('CUSTOM_ROLE.FORM.DESCRIPTION.PLACEHOLDER')"
@blur="v$.description.$touch"
/>
</div>
</div>
<div class="w-full">
<label :class="{ 'text-red-500': v$.selectedPermissions.$error }">
{{ $t('CUSTOM_ROLE.FORM.PERMISSIONS.LABEL') }}
</label>
<div class="flex flex-col gap-2.5 mb-4">
<div
v-for="permission in AVAILABLE_CUSTOM_ROLE_PERMISSIONS"
:key="permission"
class="flex items-center"
>
<input
:id="permission"
v-model="selectedPermissions"
type="checkbox"
:value="permission"
name="permissions"
class="ltr:mr-2 rtl:ml-2"
/>
<label :for="permission" class="text-sm">
{{ $t(`CUSTOM_ROLE.PERMISSIONS.${permission.toUpperCase()}`) }}
</label>
</div>
</div>
</div>
<div class="flex flex-row justify-end w-full gap-2 px-0 py-2">
<WootSubmitButton
:disabled="isSubmitDisabled"
:button-text="submitButtonText"
:loading="addCustomRole.showLoading"
/>
<button class="button clear" @click.prevent="emit('close')">
{{ $t('CUSTOM_ROLE.FORM.CANCEL_BUTTON_TEXT') }}
</button>
</div>
</form>
</div>
</template>
<style scoped lang="scss">
::v-deep {
.ProseMirror-menubar {
@apply hidden;
}
.ProseMirror-woot-style {
@apply max-h-[110px];
p {
@apply text-base;
}
}
}
</style>
@@ -0,0 +1,96 @@
<script setup>
import { computed } from 'vue';
import { useMapGetter } from 'dashboard/composables/store';
import { useRouter } from 'dashboard/composables/route';
import BasePaywallModal from 'dashboard/routes/dashboard/settings/components/BasePaywallModal.vue';
import CustomRoleListItem from './CustomRoleTableBody.vue';
const dummyCustomRolesData = [
{
name: 'All Permissions',
description: 'All permissions',
permissions: [
'conversation_manage',
'conversation_participating_manage',
'conversation_unassigned_manage',
'contact_manage',
'report_manage',
'knowledge_base_manage',
],
},
{
name: 'Conversation Permissions',
description: 'Conversation permissions',
permissions: [
'conversation_manage',
'conversation_participating_manage',
'conversation_unassigned_manage',
],
},
{
name: 'Contact Permissions',
description: 'Contact permissions',
permissions: ['contact_manage'],
},
{
name: 'Report Permissions',
description: 'Report permissions',
permissions: ['report_manage'],
},
];
const router = useRouter();
const isOnChatwootCloud = useMapGetter('globalConfig/isOnChatwootCloud');
const currentUser = useMapGetter('getCurrentUser');
const currentAccountId = useMapGetter('getCurrentAccountId');
const isSuperAdmin = computed(() => {
return currentUser.value.type === 'SuperAdmin';
});
const i18nKey = computed(() =>
isOnChatwootCloud.value ? 'PAYWALL' : 'ENTERPRISE_PAYWALL'
);
const goToBillingSettings = () => {
router.push({
name: 'billing_settings_index',
params: { accountId: currentAccountId.value },
});
};
</script>
<template>
<div class="w-full min-h-[12rem] relative">
<div class="w-full space-y-3 text-sm">
<thead class="opacity-30 dark:opacity-30">
<th
v-for="thHeader in $t('CUSTOM_ROLE.LIST.TABLE_HEADER')"
:key="thHeader"
class="py-4 pr-4 font-semibold text-left text-slate-700 dark:text-slate-300"
>
<span class="mb-0">
{{ thHeader }}
</span>
</th>
</thead>
<CustomRoleListItem
class="opacity-25 dark:opacity-20"
:roles="dummyCustomRolesData"
:loading="{}"
/>
</div>
<div
class="absolute inset-0 flex flex-col items-center justify-center w-full h-full bg-gradient-to-t from-white dark:from-slate-900 to-transparent"
>
<BasePaywallModal
feature-prefix="CUSTOM_ROLE"
:i18n-key="i18nKey"
:is-on-chatwoot-cloud="isOnChatwootCloud"
:is-super-admin="isSuperAdmin"
@click="goToBillingSettings"
/>
</div>
</div>
</template>
@@ -0,0 +1,67 @@
<script setup>
import { useI18n } from 'dashboard/composables/useI18n';
import { getI18nKey } from 'dashboard/routes/dashboard/settings/helper/settingsHelper';
defineProps({
roles: {
type: Array,
required: true,
},
loading: {
type: Object,
default: () => ({}),
},
});
const emit = defineEmits(['edit', 'delete']);
const { t } = useI18n();
const getFormattedPermissions = role => {
return role.permissions
.map(event => t(getI18nKey('CUSTOM_ROLE.PERMISSIONS', event)))
.join(', ');
};
</script>
<template>
<tbody
class="divide-y divide-slate-50 dark:divide-slate-800 text-slate-700 dark:text-slate-300"
>
<tr v-for="(customRole, index) in roles" :key="index">
<td
class="max-w-xs py-4 pr-4 font-medium truncate align-baseline"
:title="customRole.name"
>
{{ customRole.name }}
</td>
<td class="py-4 pr-4 whitespace-normal align-baseline md:break-words">
{{ customRole.description }}
</td>
<td class="py-4 pr-4 whitespace-normal align-baseline md:break-words">
{{ getFormattedPermissions(customRole) }}
</td>
<td class="flex justify-end gap-1 py-4">
<woot-button
v-tooltip.top="$t('CUSTOM_ROLE.EDIT.BUTTON_TEXT')"
variant="smooth"
size="tiny"
color-scheme="secondary"
class-names="grey-btn"
icon="edit"
@click="emit('edit', customRole)"
/>
<woot-button
v-tooltip.top="$t('CUSTOM_ROLE.DELETE.BUTTON_TEXT')"
variant="smooth"
color-scheme="alert"
size="tiny"
icon="dismiss-circle"
class-names="grey-btn"
:is-loading="loading[customRole.id]"
@click="emit('delete', customRole)"
/>
</td>
</tr>
</tbody>
</template>
@@ -0,0 +1,27 @@
import { frontendURL } from 'dashboard/helper/URLHelper';
const SettingsWrapper = () => import('../SettingsWrapper.vue');
const CustomRolesHome = () => import('./Index.vue');
export default {
routes: [
{
path: frontendURL('accounts/:accountId/settings/custom-roles'),
component: SettingsWrapper,
children: [
{
path: '',
redirect: 'list',
},
{
path: 'list',
name: 'custom_roles_list',
meta: {
permissions: ['administrator'],
},
component: CustomRolesHome,
},
],
},
],
};
@@ -0,0 +1,4 @@
export const getI18nKey = (prefix, event) => {
const eventName = event.toUpperCase();
return `${prefix}.${eventName}`;
};
@@ -0,0 +1,30 @@
import { getI18nKey } from '../settingsHelper';
describe('settingsHelper', () => {
describe('getI18nKey', () => {
it('should return the correct i18n key', () => {
const prefix = 'CUSTOM_ROLE.PERMISSIONS';
const event = 'conversation_manage';
const expectedKey = 'CUSTOM_ROLE.PERMISSIONS.CONVERSATION_MANAGE';
expect(getI18nKey(prefix, event)).toBe(expectedKey);
});
it('should handle different prefixes', () => {
const prefix = 'INTEGRATION_SETTINGS.WEBHOOK.FORM.SUBSCRIPTIONS.EVENTS';
const event = 'message_created';
const expectedKey =
'INTEGRATION_SETTINGS.WEBHOOK.FORM.SUBSCRIPTIONS.EVENTS.MESSAGE_CREATED';
expect(getI18nKey(prefix, event)).toBe(expectedKey);
});
it('should convert event to uppercase', () => {
const prefix = 'TEST_PREFIX';
const event = 'lowercaseEvent';
const expectedKey = 'TEST_PREFIX.LOWERCASEEVENT';
expect(getI18nKey(prefix, event)).toBe(expectedKey);
});
});
});
@@ -2,7 +2,7 @@
import { useVuelidate } from '@vuelidate/core';
import { required, url, minLength } from '@vuelidate/validators';
import wootConstants from 'dashboard/constants/globals';
import { getEventNamei18n } from './webhookHelper';
import { getI18nKey } from 'dashboard/routes/dashboard/settings/helper/settingsHelper';
const { EXAMPLE_WEBHOOK_URL } = wootConstants;
@@ -69,7 +69,7 @@ export default {
subscriptions: this.subscriptions,
});
},
getEventNamei18n,
getI18nKey,
},
};
</script>
@@ -108,13 +108,20 @@ export default {
class="mr-2"
/>
<label :for="event" class="text-sm">
{{ `${$t(getEventNamei18n(event))} (${event})` }}
{{
`${$t(
getI18nKey(
'INTEGRATION_SETTINGS.WEBHOOK.FORM.SUBSCRIPTIONS.EVENTS',
event
)
)} (${event})`
}}
</label>
</div>
</div>
</div>
<div class="flex flex-row justify-end gap-2 py-2 px-0 w-full">
<div class="flex flex-row justify-end w-full gap-2 px-0 py-2">
<div class="w-full">
<woot-button
:disabled="v$.$invalid || isSubmitting"
@@ -1,6 +1,6 @@
<script setup>
import { computed } from 'vue';
import { getEventNamei18n } from './webhookHelper';
import { getI18nKey } from 'dashboard/routes/dashboard/settings/helper/settingsHelper';
import ShowMore from 'dashboard/components/widgets/ShowMore.vue';
import { useI18n } from 'dashboard/composables/useI18n';
@@ -17,7 +17,16 @@ const props = defineProps({
const { t } = useI18n();
const subscribedEvents = computed(() => {
const { subscriptions } = props.webhook;
return subscriptions.map(event => t(getEventNamei18n(event))).join(', ');
return subscriptions
.map(event =>
t(
getI18nKey(
'INTEGRATION_SETTINGS.WEBHOOK.FORM.SUBSCRIPTIONS.EVENTS',
event
)
)
)
.join(', ');
});
</script>
@@ -27,7 +36,7 @@ const subscribedEvents = computed(() => {
<div class="font-medium break-words text-slate-700 dark:text-slate-100">
{{ webhook.url }}
</div>
<div class="text-sm text-slate-500 dark:text-slate-400 block mt-1">
<div class="block mt-1 text-sm text-slate-500 dark:text-slate-400">
<span class="font-medium">
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.SUBSCRIBED_EVENTS') }}:
</span>
@@ -35,7 +44,7 @@ const subscribedEvents = computed(() => {
</div>
</td>
<td class="py-4 min-w-xs">
<div class="flex gap-1 justify-end">
<div class="flex justify-end gap-1">
<woot-button
v-tooltip.top="$t('INTEGRATION_SETTINGS.WEBHOOK.EDIT.BUTTON_TEXT')"
variant="smooth"
@@ -1,9 +0,0 @@
import { getEventNamei18n } from '../webhookHelper';
describe('#getEventNamei18n', () => {
it('returns correct i18n translation text', () => {
expect(getEventNamei18n('message_created')).toEqual(
`INTEGRATION_SETTINGS.WEBHOOK.FORM.SUBSCRIPTIONS.EVENTS.MESSAGE_CREATED`
);
});
});
@@ -1,4 +0,0 @@
export const getEventNamei18n = event => {
const eventName = event.toUpperCase();
return `INTEGRATION_SETTINGS.WEBHOOK.FORM.SUBSCRIPTIONS.EVENTS.${eventName}`;
};
@@ -1,5 +1,9 @@
import { frontendURL } from 'dashboard/helper/URLHelper';
import {
ROLES,
CONVERSATION_PERMISSIONS,
} from 'dashboard/constants/permissions.js';
const SettingsContent = () => import('../Wrapper.vue');
const SettingsWrapper = () => import('../SettingsWrapper.vue');
const Macros = () => import('./Index.vue');
@@ -16,7 +20,7 @@ export default {
name: 'macros_wrapper',
component: Macros,
meta: {
permissions: ['administrator', 'agent'],
permissions: [...ROLES, ...CONVERSATION_PERMISSIONS],
},
},
],
@@ -37,7 +41,7 @@ export default {
name: 'macros_edit',
component: MacroEditor,
meta: {
permissions: ['administrator', 'agent'],
permissions: [...ROLES, ...CONVERSATION_PERMISSIONS],
},
},
{
@@ -45,7 +49,7 @@ export default {
name: 'macros_new',
component: MacroEditor,
meta: {
permissions: ['administrator', 'agent'],
permissions: [...ROLES, ...CONVERSATION_PERMISSIONS],
},
},
],
@@ -14,12 +14,18 @@ import NotificationPreferences from './NotificationPreferences.vue';
import AudioNotifications from './AudioNotifications.vue';
import FormSection from 'dashboard/components/FormSection.vue';
import AccessToken from './AccessToken.vue';
import Policy from 'dashboard/components/policy.vue';
import {
ROLES,
CONVERSATION_PERMISSIONS,
} from 'dashboard/constants/permissions.js';
export default {
components: {
MessageSignature,
FormSection,
UserProfilePicture,
Policy,
UserBasicDetails,
HotKeyCard,
ChangePassword,
@@ -71,6 +77,8 @@ export default {
'/assets/images/dashboard/profile/hot-key-ctrl-enter-dark.svg',
},
],
notificationPermissions: [...ROLES, ...CONVERSATION_PERMISSIONS],
audioNotificationPermissions: [...ROLES, ...CONVERSATION_PERMISSIONS],
};
},
computed: {
@@ -235,17 +243,21 @@ export default {
>
<ChangePassword />
</FormSection>
<FormSection
:title="$t('PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.TITLE')"
:description="
$t('PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.NOTE')
"
>
<AudioNotifications />
</FormSection>
<FormSection :title="$t('PROFILE_SETTINGS.FORM.NOTIFICATIONS.TITLE')">
<NotificationPreferences />
</FormSection>
<Policy :permissions="audioNotificationPermissions">
<FormSection
:title="$t('PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.TITLE')"
:description="
$t('PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.NOTE')
"
>
<AudioNotifications />
</FormSection>
</Policy>
<Policy :permissions="notificationPermissions">
<FormSection :title="$t('PROFILE_SETTINGS.FORM.NOTIFICATIONS.TITLE')">
<NotificationPreferences />
</FormSection>
</Policy>
<FormSection
:title="$t('PROFILE_SETTINGS.FORM.ACCESS_TOKEN.TITLE')"
:description="
@@ -9,7 +9,7 @@ export default {
path: frontendURL('accounts/:accountId/profile'),
name: 'profile_settings',
meta: {
permissions: ['administrator', 'agent'],
permissions: ['administrator', 'agent', 'custom_role'],
},
component: SettingsContent,
children: [
@@ -18,7 +18,7 @@ export default {
name: 'profile_settings_index',
component: Index,
meta: {
permissions: ['administrator', 'agent'],
permissions: ['administrator', 'agent', 'custom_role'],
},
},
],
@@ -30,7 +30,7 @@ export default {
path: 'overview',
name: 'account_overview_reports',
meta: {
permissions: ['administrator'],
permissions: ['administrator', 'report_manage'],
},
component: LiveReports,
},
@@ -49,7 +49,7 @@ export default {
path: 'conversation',
name: 'conversation_reports',
meta: {
permissions: ['administrator'],
permissions: ['administrator', 'report_manage'],
},
component: Index,
},
@@ -68,7 +68,7 @@ export default {
path: 'csat',
name: 'csat_reports',
meta: {
permissions: ['administrator'],
permissions: ['administrator', 'report_manage'],
},
component: CsatResponses,
},
@@ -87,7 +87,7 @@ export default {
path: 'bot',
name: 'bot_reports',
meta: {
permissions: ['administrator'],
permissions: ['administrator', 'report_manage'],
},
component: BotReports,
},
@@ -106,7 +106,7 @@ export default {
path: 'agent',
name: 'agent_reports',
meta: {
permissions: ['administrator'],
permissions: ['administrator', 'report_manage'],
},
component: AgentReports,
},
@@ -125,7 +125,7 @@ export default {
path: 'label',
name: 'label_reports',
meta: {
permissions: ['administrator'],
permissions: ['administrator', 'report_manage'],
},
component: LabelReports,
},
@@ -144,7 +144,7 @@ export default {
path: 'inboxes',
name: 'inbox_reports',
meta: {
permissions: ['administrator'],
permissions: ['administrator', 'report_manage'],
},
component: InboxReports,
},
@@ -162,7 +162,7 @@ export default {
path: 'teams',
name: 'team_reports',
meta: {
permissions: ['administrator'],
permissions: ['administrator', 'report_manage'],
},
component: TeamReports,
},
@@ -181,7 +181,7 @@ export default {
path: 'sla',
name: 'sla_reports',
meta: {
permissions: ['administrator'],
permissions: ['administrator', 'report_manage'],
},
component: SLAReports,
},
@@ -1,4 +1,9 @@
import { frontendURL } from '../../../helper/URLHelper';
import {
ROLES,
CONVERSATION_PERMISSIONS,
} from 'dashboard/constants/permissions.js';
import account from './account/account.routes';
import agent from './agents/agent.routes';
import agentBot from './agentBots/agentBot.routes';
@@ -16,6 +21,7 @@ import reports from './reports/reports.routes';
import store from '../../../store';
import sla from './sla/sla.routes';
import teams from './teams/teams.routes';
import customRoles from './customRoles/customRole.routes';
import profile from './profile/profile.routes';
export default {
@@ -24,10 +30,13 @@ export default {
path: frontendURL('accounts/:accountId/settings'),
name: 'settings_home',
meta: {
permissions: ['administrator', 'agent'],
permissions: [...ROLES, ...CONVERSATION_PERMISSIONS],
},
redirect: () => {
if (store.getters.getCurrentRole === 'administrator') {
if (
store.getters.getCurrentRole === 'administrator' &&
store.getters.getCurrentCustomRoleId === null
) {
return frontendURL('accounts/:accountId/settings/general');
}
return frontendURL('accounts/:accountId/settings/canned-response');
@@ -49,6 +58,7 @@ export default {
...reports.routes,
...sla.routes,
...teams.routes,
...customRoles.routes,
...profile.routes,
],
};
@@ -1,5 +1,6 @@
<script setup>
import BaseEmptyState from './BaseEmptyState.vue';
import BasePaywallModal from 'dashboard/routes/dashboard/settings/components/BasePaywallModal.vue';
const props = defineProps({
isSuperAdmin: {
@@ -18,59 +19,12 @@ const i18nKey = props.isOnChatwootCloud ? 'PAYWALL' : 'ENTERPRISE_PAYWALL';
<template>
<BaseEmptyState>
<div
class="flex flex-col max-w-md px-6 py-6 bg-white border shadow dark:bg-slate-800 rounded-xl border-slate-100 dark:border-slate-900"
>
<div class="flex items-center w-full gap-2 mb-4">
<span
class="flex items-center justify-center w-6 h-6 rounded-full bg-woot-75/70 dark:bg-woot-800/40"
>
<fluent-icon
size="14"
class="flex-shrink-0 text-woot-500 dark:text-woot-500"
icon="lock-closed"
/>
</span>
<span class="text-base font-medium text-slate-900 dark:text-white">
{{ $t('SLA.PAYWALL.TITLE') }}
</span>
</div>
<p
class="text-sm font-normal"
v-html="$t(`SLA.${i18nKey}.AVAILABLE_ON`)"
/>
<p class="text-sm font-normal">
{{ $t(`SLA.${i18nKey}.UPGRADE_PROMPT`) }}
<span v-if="!isOnChatwootCloud && !isSuperAdmin">
{{ $t('SLA.ENTERPRISE_PAYWALL.ASK_ADMIN') }}
</span>
</p>
<template v-if="isOnChatwootCloud || true">
<woot-button
color-scheme="primary"
class="w-full mt-2 text-center rounded-xl"
size="expanded"
is-expanded
@click="emit('click')"
>
{{ $t('SLA.PAYWALL.UPGRADE_NOW') }}
</woot-button>
<span class="mt-2 text-xs tracking-tight text-center">
{{ $t('SLA.PAYWALL.CANCEL_ANYTIME') }}
</span>
</template>
<template v-else-if="isSuperAdmin">
<a href="/super_admin" class="block w-full">
<woot-button
color-scheme="primary"
class="w-full mt-2 text-center rounded-xl"
size="expanded"
is-expanded
>
{{ $t('SLA.PAYWALL.UPGRADE_NOW') }}
</woot-button>
</a>
</template>
</div>
<BasePaywallModal
feature-prefix="SLA"
:i18n-key="i18nKey"
:is-on-chatwoot-cloud="isOnChatwootCloud"
:is-super-admin="isSuperAdmin"
@click="emit('click')"
/>
</BaseEmptyState>
</template>
+2
View File
@@ -27,6 +27,7 @@ import conversationTypingStatus from './modules/conversationTypingStatus';
import conversationWatchers from './modules/conversationWatchers';
import csat from './modules/csat';
import customViews from './modules/customViews';
import customRole from './modules/customRole';
import dashboardApps from './modules/dashboardApps';
import globalConfig from 'shared/store/globalConfig';
import inboxAssignableAgents from './modules/inboxAssignableAgents';
@@ -77,6 +78,7 @@ export default new Vuex.Store({
conversationWatchers,
csat,
customViews,
customRole,
dashboardApps,
globalConfig,
inboxAssignableAgents,
@@ -66,6 +66,14 @@ export const getters = {
return currentAccount.role;
},
getCurrentCustomRoleId($state, $getters) {
const { accounts = [] } = $state.currentUser;
const [currentAccount = {}] = accounts.filter(
account => account.id === $getters.getCurrentAccountId
);
return currentAccount.custom_role_id;
},
getCurrentUser($state) {
return $state.currentUser;
},
@@ -0,0 +1,100 @@
import { throwErrorMessage } from 'dashboard/store/utils/api';
import * as MutationHelpers from 'shared/helpers/vuex/mutationHelpers';
import * as types from '../mutation-types';
import CustomRoleAPI from '../../api/customRole';
export const state = {
records: [],
uiFlags: {
fetchingList: false,
creatingItem: false,
updatingItem: false,
deletingItem: false,
},
};
export const getters = {
getCustomRoles($state) {
return $state.records;
},
getUIFlags($state) {
return $state.uiFlags;
},
};
export const actions = {
getCustomRole: async function getCustomRole({ commit }) {
commit(types.default.SET_CUSTOM_ROLE_UI_FLAG, { fetchingList: true });
try {
const response = await CustomRoleAPI.get();
commit(types.default.SET_CUSTOM_ROLE, response.data);
commit(types.default.SET_CUSTOM_ROLE_UI_FLAG, { fetchingList: false });
} catch (error) {
commit(types.default.SET_CUSTOM_ROLE_UI_FLAG, { fetchingList: false });
}
},
createCustomRole: async function createCustomRole({ commit }, customRoleObj) {
commit(types.default.SET_CUSTOM_ROLE_UI_FLAG, { creatingItem: true });
try {
const response = await CustomRoleAPI.create(customRoleObj);
commit(types.default.ADD_CUSTOM_ROLE, response.data);
commit(types.default.SET_CUSTOM_ROLE_UI_FLAG, { creatingItem: false });
return response.data;
} catch (error) {
commit(types.default.SET_CUSTOM_ROLE_UI_FLAG, { creatingItem: false });
return throwErrorMessage(error);
}
},
updateCustomRole: async function updateCustomRole(
{ commit },
{ id, ...updateObj }
) {
commit(types.default.SET_CUSTOM_ROLE_UI_FLAG, { updatingItem: true });
try {
const response = await CustomRoleAPI.update(id, updateObj);
commit(types.default.EDIT_CUSTOM_ROLE, response.data);
commit(types.default.SET_CUSTOM_ROLE_UI_FLAG, { updatingItem: false });
return response.data;
} catch (error) {
commit(types.default.SET_CUSTOM_ROLE_UI_FLAG, { updatingItem: false });
return throwErrorMessage(error);
}
},
deleteCustomRole: async function deleteCustomRole({ commit }, id) {
commit(types.default.SET_CUSTOM_ROLE_UI_FLAG, { deletingItem: true });
try {
await CustomRoleAPI.delete(id);
commit(types.default.DELETE_CUSTOM_ROLE, id);
commit(types.default.SET_CUSTOM_ROLE_UI_FLAG, { deletingItem: true });
return id;
} catch (error) {
commit(types.default.SET_CUSTOM_ROLE_UI_FLAG, { deletingItem: true });
return throwErrorMessage(error);
}
},
};
export const mutations = {
[types.default.SET_CUSTOM_ROLE_UI_FLAG](_state, data) {
_state.uiFlags = {
..._state.uiFlags,
...data,
};
},
[types.default.SET_CUSTOM_ROLE]: MutationHelpers.set,
[types.default.ADD_CUSTOM_ROLE]: MutationHelpers.create,
[types.default.EDIT_CUSTOM_ROLE]: MutationHelpers.update,
[types.default.DELETE_CUSTOM_ROLE]: MutationHelpers.destroy,
};
export default {
namespaced: true,
state,
getters,
actions,
mutations,
};
@@ -42,6 +42,26 @@ describe('#getters', () => {
});
});
describe('#getCurrentCustomRoleId', () => {
it('returns current custom role id', () => {
expect(
getters.getCurrentCustomRoleId(
{ currentUser: { accounts: [{ id: 1, custom_role_id: 1 }] } },
{ getCurrentAccountId: 1 }
)
).toEqual(1);
});
it('returns undefined if account is not available', () => {
expect(
getters.getCurrentCustomRoleId(
{ currentUser: { accounts: [{ id: 1, custom_role_id: 1 }] } },
{ getCurrentAccountId: 2 }
)
).toEqual(undefined);
});
});
describe('#getCurrentUserAvailability', () => {
it('returns correct availability status', () => {
expect(
@@ -0,0 +1,97 @@
import axios from 'axios';
import { actions } from '../../customRole';
import * as types from '../../../mutation-types';
import { customRoleList } from './fixtures';
const commit = vi.fn();
global.axios = axios;
vi.mock('axios');
describe('#actions', () => {
describe('#getCustomRole', () => {
it('sends correct actions if API is success', async () => {
axios.get.mockResolvedValue({ data: customRoleList });
await actions.getCustomRole({ commit });
expect(commit.mock.calls).toEqual([
[types.default.SET_CUSTOM_ROLE_UI_FLAG, { fetchingList: true }],
[types.default.SET_CUSTOM_ROLE, customRoleList],
[types.default.SET_CUSTOM_ROLE_UI_FLAG, { fetchingList: false }],
]);
});
it('sends correct actions if API is error', async () => {
axios.get.mockRejectedValue({ message: 'Incorrect header' });
await actions.getCustomRole({ commit });
expect(commit.mock.calls).toEqual([
[types.default.SET_CUSTOM_ROLE_UI_FLAG, { fetchingList: true }],
[types.default.SET_CUSTOM_ROLE_UI_FLAG, { fetchingList: false }],
]);
});
});
describe('#createCustomRole', () => {
it('sends correct actions if API is success', async () => {
axios.post.mockResolvedValue({ data: customRoleList[0] });
await actions.createCustomRole({ commit }, customRoleList[0]);
expect(commit.mock.calls).toEqual([
[types.default.SET_CUSTOM_ROLE_UI_FLAG, { creatingItem: true }],
[types.default.ADD_CUSTOM_ROLE, customRoleList[0]],
[types.default.SET_CUSTOM_ROLE_UI_FLAG, { creatingItem: false }],
]);
});
it('sends correct actions if API is error', async () => {
axios.post.mockRejectedValue({ message: 'Incorrect header' });
await expect(actions.createCustomRole({ commit })).rejects.toThrow(Error);
expect(commit.mock.calls).toEqual([
[types.default.SET_CUSTOM_ROLE_UI_FLAG, { creatingItem: true }],
[types.default.SET_CUSTOM_ROLE_UI_FLAG, { creatingItem: false }],
]);
});
});
describe('#updateCustomRole', () => {
it('sends correct actions if API is success', async () => {
axios.patch.mockResolvedValue({ data: customRoleList[0] });
await actions.updateCustomRole(
{ commit },
{ id: 1, ...customRoleList[0] }
);
expect(commit.mock.calls).toEqual([
[types.default.SET_CUSTOM_ROLE_UI_FLAG, { updatingItem: true }],
[types.default.EDIT_CUSTOM_ROLE, customRoleList[0]],
[types.default.SET_CUSTOM_ROLE_UI_FLAG, { updatingItem: false }],
]);
});
it('sends correct actions if API is error', async () => {
axios.patch.mockRejectedValue({ message: 'Incorrect header' });
await expect(
actions.updateCustomRole({ commit }, { id: 1 })
).rejects.toThrow(Error);
expect(commit.mock.calls).toEqual([
[types.default.SET_CUSTOM_ROLE_UI_FLAG, { updatingItem: true }],
[types.default.SET_CUSTOM_ROLE_UI_FLAG, { updatingItem: false }],
]);
});
});
describe('#deleteCustomRole', () => {
it('sends correct actions if API is success', async () => {
axios.delete.mockResolvedValue({ data: customRoleList[0] });
await actions.deleteCustomRole({ commit }, 1);
expect(commit.mock.calls).toEqual([
[types.default.SET_CUSTOM_ROLE_UI_FLAG, { deletingItem: true }],
[types.default.DELETE_CUSTOM_ROLE, 1],
[types.default.SET_CUSTOM_ROLE_UI_FLAG, { deletingItem: true }],
]);
});
it('sends correct actions if API is error', async () => {
axios.delete.mockRejectedValue({ message: 'Incorrect header' });
await expect(actions.deleteCustomRole({ commit }, 1)).rejects.toThrow(
Error
);
expect(commit.mock.calls).toEqual([
[types.default.SET_CUSTOM_ROLE_UI_FLAG, { deletingItem: true }],
[types.default.SET_CUSTOM_ROLE_UI_FLAG, { deletingItem: true }],
]);
});
});
});
@@ -0,0 +1,77 @@
export const customRoleList = [
{
id: 1,
name: 'Super Custom Role',
description: 'Role with all available custom role permissions',
permissions: [
'conversation_participating_manage',
'conversation_unassigned_manage',
'conversation_manage',
'contact_manage',
'report_manage',
'knowledge_base_manage',
],
created_at: '2024-09-04T05:30:22.282Z',
updated_at: '2024-09-05T09:21:02.844Z',
},
{
id: 2,
name: 'Conversation Manager Role',
description: 'Role for managing all aspects of conversations',
permissions: [
'conversation_unassigned_manage',
'conversation_participating_manage',
'conversation_manage',
],
created_at: '2024-09-05T09:21:38.692Z',
updated_at: '2024-09-05T09:21:38.692Z',
},
{
id: 3,
name: 'Participating Agent Role',
description: 'Role for agents participating in conversations',
permissions: ['conversation_participating_manage'],
created_at: '2024-09-06T08:03:14.550Z',
updated_at: '2024-09-06T08:03:14.550Z',
},
{
id: 4,
name: 'Contact Manager Role',
description: 'Role for managing contacts only',
permissions: ['contact_manage'],
created_at: '2024-09-06T08:15:56.877Z',
updated_at: '2024-09-06T09:53:28.103Z',
},
{
id: 5,
name: 'Report Analyst Role',
description: 'Role for accessing and managing reports',
permissions: ['report_manage'],
created_at: '2024-09-06T09:53:58.277Z',
updated_at: '2024-09-06T09:53:58.277Z',
},
{
id: 6,
name: 'Knowledge Base Editor Role',
description: 'Role for managing the knowledge base',
permissions: ['knowledge_base_manage'],
created_at: '2024-09-06T09:54:27.649Z',
updated_at: '2024-09-06T09:54:27.649Z',
},
{
id: 7,
name: 'Unassigned Queue Manager Role',
description: 'Role for managing unassigned conversations',
permissions: ['conversation_unassigned_manage'],
created_at: '2024-09-06T09:55:00.503Z',
updated_at: '2024-09-06T09:55:00.503Z',
},
{
id: 8,
name: 'Basic Conversation Handler Role',
description: 'Role for basic conversation management',
permissions: ['conversation_manage'],
created_at: '2024-09-06T09:55:19.519Z',
updated_at: '2024-09-06T09:55:19.519Z',
},
];
@@ -0,0 +1,26 @@
import { getters } from '../../customRole';
import { customRoleList } from './fixtures';
describe('#getters', () => {
it('getCustomRoles', () => {
const state = { records: customRoleList };
expect(getters.getCustomRoles(state)).toEqual(customRoleList);
});
it('getUIFlags', () => {
const state = {
uiFlags: {
fetchingList: true,
creatingItem: false,
updatingItem: false,
deletingItem: false,
},
};
expect(getters.getUIFlags(state)).toEqual({
fetchingList: true,
creatingItem: false,
updatingItem: false,
deletingItem: false,
});
});
});
@@ -0,0 +1,48 @@
import types from '../../../mutation-types';
import { mutations } from '../../customRole';
import { customRoleList } from './fixtures';
describe('#mutations', () => {
describe('#SET_CUSTOM_ROLE', () => {
it('set custom role records', () => {
const state = { records: [] };
mutations[types.SET_CUSTOM_ROLE](state, customRoleList);
expect(state.records).toEqual(customRoleList);
});
});
describe('#ADD_CUSTOM_ROLE', () => {
it('push newly created custom role to the store', () => {
const state = { records: [customRoleList[0]] };
mutations[types.ADD_CUSTOM_ROLE](state, customRoleList[1]);
expect(state.records).toEqual([customRoleList[0], customRoleList[1]]);
});
});
describe('#EDIT_CUSTOM_ROLE', () => {
it('update custom role record', () => {
const state = { records: [customRoleList[0]] };
const updatedRole = { ...customRoleList[0], name: 'Updated Role' };
mutations[types.EDIT_CUSTOM_ROLE](state, updatedRole);
expect(state.records).toEqual([updatedRole]);
});
});
describe('#DELETE_CUSTOM_ROLE', () => {
it('delete custom role record', () => {
const state = { records: [customRoleList[0], customRoleList[1]] };
mutations[types.DELETE_CUSTOM_ROLE](state, customRoleList[0].id);
expect(state.records).toEqual([customRoleList[1]]);
});
});
describe('#SET_CUSTOM_ROLE_UI_FLAG', () => {
it('set custom role UI flags', () => {
const state = { uiFlags: {} };
mutations[types.SET_CUSTOM_ROLE_UI_FLAG](state, {
fetchingList: true,
});
expect(state.uiFlags).toEqual({ fetchingList: true });
});
});
});
@@ -98,6 +98,13 @@ export default {
EDIT_CANNED: 'EDIT_CANNED',
DELETE_CANNED: 'DELETE_CANNED',
// Custom Role
SET_CUSTOM_ROLE_UI_FLAG: 'SET_CUSTOM_ROLE_UI_FLAG',
SET_CUSTOM_ROLE: 'SET_CUSTOM_ROLE',
ADD_CUSTOM_ROLE: 'ADD_CUSTOM_ROLE',
EDIT_CUSTOM_ROLE: 'EDIT_CUSTOM_ROLE',
DELETE_CUSTOM_ROLE: 'DELETE_CUSTOM_ROLE',
// Labels
SET_LABEL_UI_FLAG: 'SET_LABEL_UI_FLAG',
SET_LABELS: 'SET_LABELS',
@@ -271,15 +271,9 @@
"M11.1667 17.8333C14.8486 17.8333 17.8333 14.8486 17.8333 11.1667C17.8333 7.48477 14.8486 4.5 11.1667 4.5C7.48477 4.5 4.5 7.48477 4.5 11.1667C4.5 14.8486 7.48477 17.8333 11.1667 17.8333Z",
"M19.5001 19.5001L15.9167 15.9167"
],
"chevrons-left-outline": [
"m11 17-5-5 5-5",
"m18 17-5-5 5-5"
],
"chevrons-left-outline": ["m11 17-5-5 5-5", "m18 17-5-5 5-5"],
"chevron-left-single-outline": "m15 18-6-6 6-6",
"chevrons-right-outline": [
"m6 17 5-5-5-5",
"m13 17 5-5-5-5"
],
"chevrons-right-outline": ["m6 17 5-5-5-5", "m13 17 5-5-5-5"],
"chevron-right-single-outline": "m9 18 6-6-6-6",
"avatar-upload-outline": "M19.754 11a.75.75 0 0 1 .743.648l.007.102v7a3.25 3.25 0 0 1-3.065 3.246l-.185.005h-11a3.25 3.25 0 0 1-3.244-3.066l-.006-.184V11.75a.75.75 0 0 1 1.494-.102l.006.102v7a1.75 1.75 0 0 0 1.607 1.745l.143.006h11A1.75 1.75 0 0 0 19 18.894l.005-.143V11.75a.75.75 0 0 1 .75-.75ZM6.22 7.216l4.996-4.996a.75.75 0 0 1 .976-.073l.084.072l5.005 4.997a.75.75 0 0 1-.976 1.134l-.084-.073l-3.723-3.716l.001 11.694a.75.75 0 0 1-.648.743l-.102.007a.75.75 0 0 1-.743-.648L11 16.255V4.558L7.28 8.277a.75.75 0 0 1-.976.073l-.084-.073a.75.75 0 0 1-.073-.977l.073-.084l4.996-4.996L6.22 7.216Z",
"text-copy-outline": "M5.503 4.627L5.5 6.75v10.504a3.25 3.25 0 0 0 3.25 3.25h8.616a2.25 2.25 0 0 1-2.122 1.5H8.75A4.75 4.75 0 0 1 4 17.254V6.75c0-.98.627-1.815 1.503-2.123M17.75 2A2.25 2.25 0 0 1 20 4.25v13a2.25 2.25 0 0 1-2.25 2.25h-9a2.25 2.25 0 0 1-2.25-2.25v-13A2.25 2.25 0 0 1 8.75 2zm0 1.5h-9a.75.75 0 0 0-.75.75v13c0 .414.336.75.75.75h9a.75.75 0 0 0 .75-.75v-13a.75.75 0 0 0-.75-.75",
@@ -290,5 +284,6 @@
"M6.49573 9.20645C6.49573 8.67008 6.93058 8.23523 7.46695 8.23523C8.00337 8.23523 8.43817 8.67008 8.43817 9.20645V11.4511C8.43817 11.9875 8.00337 12.4223 7.46695 12.4223C6.93058 12.4223 6.49573 11.9875 6.49573 11.4511V9.20645Z",
"M9.60364 9.20645C9.60364 8.67008 10.0385 8.23523 10.5749 8.23523C11.1113 8.23523 11.5461 8.67008 11.5461 9.20645V11.4511C11.5461 11.9875 11.1113 12.4223 10.5749 12.4223C10.0385 12.4223 9.60364 11.9875 9.60364 11.4511V9.20645Z",
"M17.1442 5.57049C13.5275 5.06019 10.5793 5.04007 6.88135 5.56825C5.9466 5.70176 5.32812 5.79197 4.85654 5.92976C4.41928 6.05757 4.17061 6.20994 3.96492 6.43984C3.539 6.91583 3.48286 7.45419 3.4248 9.33184C3.36775 11.1772 3.48076 12.831 3.69481 14.6918C3.80887 15.6834 3.88736 16.3526 4.01268 16.8613C4.13155 17.3439 4.27532 17.6034 4.47513 17.802C4.67654 18.0023 4.93467 18.1435 5.40841 18.2581C5.90952 18.3793 6.56702 18.4526 7.5442 18.5592C10.7045 18.904 13.0702 18.9022 16.2423 18.561C17.2313 18.4546 17.8995 18.3813 18.4081 18.2609C18.8913 18.1465 19.1511 18.0063 19.3497 17.8118C19.5442 17.6213 19.6928 17.3587 19.8217 16.852C19.9561 16.3234 20.0476 15.624 20.18 14.5966C20.4162 12.7633 20.5863 11.1533 20.5929 9.3896C20.5999 7.50391 20.5613 6.96737 20.1306 6.46971C19.9226 6.22932 19.6696 6.0713 19.2224 5.93968C18.7395 5.79754 18.1042 5.70594 17.1442 5.57049ZM6.65555 3.98715C10.5078 3.43695 13.6072 3.45849 17.3674 3.98902L17.4224 3.99678C18.3127 4.12235 19.0648 4.22844 19.6733 4.40753C20.33 4.60078 20.8792 4.89417 21.3382 5.4245C22.2041 6.42482 22.1984 7.6117 22.1909 9.18858C22.1905 9.25686 22.1902 9.32584 22.19 9.3956C22.183 11.2604 22.0026 12.949 21.764 14.8006L21.7577 14.8496C21.6332 15.8159 21.5307 16.6121 21.3695 17.2458C21.2 17.9121 20.9467 18.4833 20.4672 18.9529C19.9919 19.4183 19.4302 19.6602 18.776 19.8151C18.1582 19.9613 17.3895 20.044 16.4629 20.1436L16.4131 20.149C13.1283 20.5023 10.6472 20.5043 7.37097 20.1469L7.32043 20.1414C6.40679 20.0417 5.64604 19.9587 5.03292 19.8104C4.38112 19.6527 3.82317 19.406 3.34911 18.9347C2.87346 18.4618 2.62363 17.8999 2.46191 17.2433C2.30938 16.6241 2.22071 15.8531 2.11393 14.9246L2.10815 14.8743C1.88863 12.9659 1.76823 11.23 1.82845 9.28246C1.83063 9.2118 1.83272 9.14191 1.83479 9.07281C1.8816 7.50776 1.91671 6.33374 2.7747 5.37486C3.22992 4.86612 3.76798 4.58399 4.40853 4.39678C5.00257 4.22316 5.73505 4.11858 6.60207 3.99479C6.61981 3.99225 6.63764 3.9897 6.65555 3.98715Z"
]
],
"scan-person-outline": "M5.25 3.5A1.75 1.75 0 0 0 3.5 5.25v3a.75.75 0 0 1-1.5 0v-3A3.25 3.25 0 0 1 5.25 2h3a.75.75 0 0 1 0 1.5zm0 17a1.75 1.75 0 0 1-1.75-1.75v-3a.75.75 0 0 0-1.5 0v3A3.25 3.25 0 0 0 5.25 22h3a.75.75 0 0 0 .707-1l-.005-.015a.75.75 0 0 0-.702-.485zM20.5 5.25a1.75 1.75 0 0 0-1.75-1.75h-3a.75.75 0 0 1 0-1.5h3A3.25 3.25 0 0 1 22 5.25v3a.75.75 0 0 1-1.5 0zM18.75 20.5a1.75 1.75 0 0 0 1.75-1.75v-3a.75.75 0 0 1 1.5 0v3A3.25 3.25 0 0 1 18.75 22h-3a.75.75 0 0 1 0-1.5zM6.5 18.616q0 .465.258.884H5.25a1 1 0 0 1-.129-.011A3.1 3.1 0 0 1 5 18.616v-.366A2.25 2.25 0 0 1 7.25 16h9.5A2.25 2.25 0 0 1 19 18.25v.366c0 .31-.047.601-.132.875a1 1 0 0 1-.118.009h-1.543a1.56 1.56 0 0 0 .293-.884v-.366a.75.75 0 0 0-.75-.75h-9.5a.75.75 0 0 0-.75.75zm8.25-8.866a2.75 2.75 0 1 0-5.5 0a2.75 2.75 0 0 0 5.5 0m1.5 0a4.25 4.25 0 1 1-8.5 0a4.25 4.25 0 0 1 8.5 0"
}
+2
View File
@@ -33,3 +33,5 @@ class ArticlePolicy < ApplicationPolicy
@record.first.portal.members.include?(@user)
end
end
ArticlePolicy.prepend_mod_with('Enterprise::ArticlePolicy')
+2
View File
@@ -29,3 +29,5 @@ class CategoryPolicy < ApplicationPolicy
@record.first.portal.members.include?(@user)
end
end
CategoryPolicy.prepend_mod_with('Enterprise::CategoryPolicy')
+2
View File
@@ -37,3 +37,5 @@ class PortalPolicy < ApplicationPolicy
@record.first.members.include?(@user)
end
end
PortalPolicy.prepend_mod_with('Enterprise::PortalPolicy')
+2
View File
@@ -3,3 +3,5 @@ class ReportPolicy < ApplicationPolicy
@account_user.administrator?
end
end
ReportPolicy.prepend_mod_with('Enterprise::ReportPolicy')
@@ -30,5 +30,6 @@ json.accounts do
# availability derived from presence
json.availability_status account_user.availability_status
json.auto_offline account_user.auto_offline
json.partial! 'api/v1/models/account_user', account_user: account_user if ChatwootApp.enterprise?
end
end
+1 -1
View File
@@ -1,5 +1,5 @@
shared: &shared
version: '3.12.0'
version: '3.13.0'
development:
<<: *shared
+3
View File
@@ -89,3 +89,6 @@
enabled: false
- name: captain_integration
enabled: false
- name: custom_roles
enabled: false
premium: true
@@ -1,9 +1,17 @@
module Enterprise::Api::V1::Accounts::AgentsController
def account_user_attributes
super + [:custom_role_id]
def create
super
associate_agent_with_custom_role
end
def allowed_agent_params
super + [:custom_role_id]
def update
super
associate_agent_with_custom_role
end
private
def associate_agent_with_custom_role
@agent.current_account_user.update!(custom_role_id: params[:custom_role_id])
end
end
@@ -0,0 +1,7 @@
module Enterprise::Api::V2::Accounts::ReportsController
def check_authorization
return if Current.account_user.custom_role&.permissions&.include?('report_manage')
super
end
end
@@ -0,0 +1,29 @@
module Enterprise::ArticlePolicy
def index?
@account_user.custom_role&.permissions&.include?('knowledge_base_manage') || super
end
def update?
@account_user.custom_role&.permissions&.include?('knowledge_base_manage') || super
end
def show?
@account_user.custom_role&.permissions&.include?('knowledge_base_manage') || super
end
def edit?
@account_user.custom_role&.permissions&.include?('knowledge_base_manage') || super
end
def create?
@account_user.custom_role&.permissions&.include?('knowledge_base_manage') || super
end
def destroy?
@account_user.custom_role&.permissions&.include?('knowledge_base_manage') || super
end
def reorder?
@account_user.custom_role&.permissions&.include?('knowledge_base_manage') || super
end
end
@@ -0,0 +1,25 @@
module Enterprise::CategoryPolicy
def index?
@account_user.custom_role&.permissions&.include?('knowledge_base_manage') || super
end
def update?
@account_user.custom_role&.permissions&.include?('knowledge_base_manage') || super
end
def show?
@account_user.custom_role&.permissions&.include?('knowledge_base_manage') || super
end
def edit?
@account_user.custom_role&.permissions&.include?('knowledge_base_manage') || super
end
def create?
@account_user.custom_role&.permissions&.include?('knowledge_base_manage') || super
end
def destroy?
@account_user.custom_role&.permissions&.include?('knowledge_base_manage') || super
end
end
@@ -0,0 +1,33 @@
module Enterprise::PortalPolicy
def index?
@account_user.custom_role&.permissions&.include?('knowledge_base_manage') || super
end
def update?
@account_user.custom_role&.permissions&.include?('knowledge_base_manage') || super
end
def show?
@account_user.custom_role&.permissions&.include?('knowledge_base_manage') || super
end
def edit?
@account_user.custom_role&.permissions&.include?('knowledge_base_manage') || super
end
def create?
@account_user.custom_role&.permissions&.include?('knowledge_base_manage') || super
end
def destroy?
@account_user.custom_role&.permissions&.include?('knowledge_base_manage') || super
end
def add_members?
@account_user.custom_role&.permissions&.include?('knowledge_base_manage') || super
end
def logo?
@account_user.custom_role&.permissions&.include?('knowledge_base_manage') || super
end
end
@@ -0,0 +1,5 @@
module Enterprise::ReportPolicy
def view?
@account_user.custom_role&.permissions&.include?('report_manage') || super
end
end
@@ -0,0 +1,2 @@
json.custom_role_id account_user&.custom_role_id
json.custom_role account_user&.custom_role&.as_json(only: [:id, :name, :description, :permissions])
+1
View File
@@ -3,3 +3,4 @@
- audit_logs
- response_bot
- sla
- custom_roles
+5 -4
View File
@@ -1,10 +1,11 @@
class Integrations::OpenaiBaseService
# 3.5 support 16,385 tokens
# gpt-4o-mini supports 128,000 tokens
# 1 token is approx 4 characters
# 16385 * 4 = 65540 characters, sticking to 50,000 to be safe
TOKEN_LIMIT = 50_000
# sticking with 120000 to be safe
# 120000 * 4 = 480,000 characters (rounding off downwards to 400,000 to be safe)
TOKEN_LIMIT = 400_000
API_URL = 'https://api.openai.com/v1/chat/completions'.freeze
GPT_MODEL = 'gpt-3.5-turbo'.freeze
GPT_MODEL = ENV.fetch('OPENAI_GPT_MODEL', 'gpt-4o-mini').freeze
ALLOWED_EVENT_NAMES = %w[rephrase summarize reply_suggestion fix_spelling_grammar shorten expand make_friendly make_formal simplify].freeze
CACHEABLE_EVENTS = %w[].freeze
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@chatwoot/chatwoot",
"version": "3.12.0",
"version": "3.13.0",
"license": "MIT",
"scripts": {
"eslint": "eslint app/**/*.{js,vue}",
@@ -216,6 +216,21 @@ RSpec.describe 'Conversation Messages API', type: :request do
expect(message.reload.deleted).to be true
expect(message.reload.content_attributes['bcc_emails']).to be_nil
end
it 'deletes interactive messages' do
interactive_message = create(
:message, message_type: :outgoing, content: 'test', content_type: 'input_select',
content_attributes: { 'items' => [{ 'title' => 'test', 'value' => 'test' }] },
conversation: conversation
)
delete "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/messages/#{interactive_message.id}",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(interactive_message.reload.deleted).to be true
end
end
context 'when the message id is invalid' do
@@ -13,6 +13,21 @@ RSpec.describe '/api/v1/widget/conversations/toggle_typing', type: :request do
Widget::TokenService.new(payload: { source_id: second_session.source_id, inbox_id: web_widget.inbox.id }).generate_token
end
def conversation_params
{
website_token: web_widget.website_token,
contact: {
name: 'contact-name',
email: 'contact-email@chatwoot.com',
phone_number: '+919745313456'
},
message: {
content: 'This is a test message'
},
custom_attributes: { order_id: '12345' }
}
end
describe 'GET /api/v1/widget/conversations' do
context 'with a conversation' do
it 'returns the correct conversation params' do
@@ -47,21 +62,10 @@ RSpec.describe '/api/v1/widget/conversations/toggle_typing', type: :request do
end
describe 'POST /api/v1/widget/conversations' do
it 'creates a conversation' do
it 'creates a conversation with correct details' do
post '/api/v1/widget/conversations',
headers: { 'X-Auth-Token' => token },
params: {
website_token: web_widget.website_token,
contact: {
name: 'contact-name',
email: 'contact-email@chatwoot.com',
phone_number: '+919745313456'
},
message: {
content: 'This is a test message'
},
custom_attributes: { order_id: '12345' }
},
params: conversation_params,
as: :json
expect(response).to have_http_status(:success)
@@ -70,8 +74,19 @@ RSpec.describe '/api/v1/widget/conversations/toggle_typing', type: :request do
expect(json_response['contact']['email']).to eq 'contact-email@chatwoot.com'
expect(json_response['contact']['phone_number']).to eq '+919745313456'
expect(json_response['contact']['name']).to eq 'contact-name'
end
it 'creates a conversation with correct message and custom attributes' do
post '/api/v1/widget/conversations',
headers: { 'X-Auth-Token' => token },
params: conversation_params,
as: :json
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['custom_attributes']['order_id']).to eq '12345'
expect(json_response['messages'][0]['content']).to eq 'This is a test message'
expect(json_response['messages'][0]['message_type']).to eq 0
end
it 'create a conversation with a name and without an email' do
@@ -94,13 +94,28 @@ RSpec.describe 'Platform Users API', type: :request do
context 'when it is an authenticated platform app' do
let(:platform_app) { create(:platform_app) }
it 'creates a new user and permissible for the user' do
expect do
post '/platform/api/v1/users/', params: { name: 'test', display_name: 'displaytest',
email: 'test@test.com', password: 'Password1!',
custom_attributes: { test: 'test_create' } },
headers: { api_access_token: platform_app.access_token.token }, as: :json
end.not_to enqueue_mail
it 'creates a new user and permissible for the user without sending an email' do
# TODO: enqueued mail check failes because of : https://github.com/rspec/rspec-rails/pull/2793
# revert to this block when the issue is fixed
# expect do
# post '/platform/api/v1/users/', params: { name: 'test', display_name: 'displaytest',
# email: 'test@test.com', password: 'Password1!',
# custom_attributes: { test: 'test_create' } },
# headers: { api_access_token: platform_app.access_token.token }, as: :json
# byebug
# end.not_to have_enqueued_mail
##------ revert this block when the issue is fixed
post '/platform/api/v1/users/', params: { name: 'test', display_name: 'displaytest',
email: 'test@test.com', password: 'Password1!',
custom_attributes: { test: 'test_create' } },
headers: { api_access_token: platform_app.access_token.token }, as: :json
mail_jobs = ActiveJob::Base.queue_adapter.enqueued_jobs.select do |job|
job[:job] == 'ActionMailer::MailDeliveryJob'
end
expect(mail_jobs.count).to eq(0)
##------ revert this block when the issue is fixed
expect(response).to have_http_status(:success)
data = response.parsed_body
@@ -17,15 +17,22 @@ RSpec.describe 'Public Articles API', type: :request do
create(:article, category: category, portal: portal, account_id: account.id, author_id: agent.id, associated_article_id: article.id, views: 1)
create(:article, category: category_2, portal: portal, account_id: account.id, author_id: agent.id, associated_article_id: article.id, views: 5)
create(:article, category: category_2, portal: portal, account_id: account.id, author_id: agent.id, views: 4)
create(:article, category: category_2, portal: portal, account_id: account.id, author_id: agent.id, status: :draft)
end
describe 'GET /public/api/v1/portals/:slug/articles' do
it 'Fetch all articles in the portal' do
get "/hc/#{portal.slug}/#{category.locale}/categories/#{category.slug}/articles"
expect(response).to have_http_status(:success)
end
it 'Fetches only the published articles in the portal' do
get "/hc/#{portal.slug}/#{category_2.locale}/categories/#{category.slug}/articles.json"
expect(response).to have_http_status(:success)
response_data = JSON.parse(response.body, symbolize_names: true)[:payload]
expect(response_data.length).to eq(2)
end
it 'get all articles with searched text query' do
article2 = create(:article,
account_id: account.id,
@@ -0,0 +1,28 @@
require 'rails_helper'
RSpec.describe 'Profile API', type: :request do
describe 'GET /api/v1/profile' do
let(:account) { create(:account) }
let!(:custom_role_account) { create(:account, name: 'Custom Role Account') }
let!(:custom_role) { create(:custom_role, name: 'Custom Role', account: custom_role_account) }
let!(:agent) { create(:user, account: account, custom_attributes: { test: 'test' }, role: :agent) }
before do
create(:account_user, account: custom_role_account, user: agent, custom_role: custom_role)
end
context 'when it is an authenticated user' do
it 'returns user custom role information' do
get '/api/v1/profile',
headers: agent.create_new_auth_token,
as: :json
parsed_response = response.parsed_body
# map accounts object and make sure custom role id and name are present
role_account = parsed_response['accounts'].find { |account| account['id'] == custom_role_account.id }
expect(role_account['custom_role']['id']).to eq(custom_role.id)
expect(role_account['custom_role']['name']).to eq(custom_role.name)
end
end
end
end
@@ -3,10 +3,25 @@ require 'rails_helper'
RSpec.describe 'Enterprise Agents API', type: :request do
let(:account) { create(:account) }
let(:admin) { create(:user, account: account, role: :administrator) }
let!(:custom_role) { create(:custom_role, account: account) }
describe 'POST /api/v1/accounts/{account.id}/agents' do
let(:params) { { email: 'test@example.com', name: 'Test User', role: 'agent', custom_role_id: custom_role.id } }
context 'when it is an authenticated administrator' do
it 'creates an agent with the specified custom role' do
post "/api/v1/accounts/#{account.id}/agents", headers: admin.create_new_auth_token, params: params, as: :json
expect(response).to have_http_status(:success)
agent = account.agents.last
expect(agent.account_users.first.custom_role_id).to eq(custom_role.id)
expect(JSON.parse(response.body)['custom_role_id']).to eq(custom_role.id)
end
end
end
describe 'PUT /api/v1/accounts/{account.id}/agents/:id' do
let(:other_agent) { create(:user, account: account, role: :agent) }
let!(:custom_role) { create(:custom_role, account: account) }
context 'when it is an authenticated administrator' do
it 'modified the custom role of the agent' do
@@ -10,8 +10,9 @@ RSpec.describe Sla::ProcessAccountAppliedSlasJob do
let!(:active_with_misses_applied_sla) { create(:applied_sla, account: account, sla_policy: sla_policy, sla_status: 'active_with_misses') }
it 'enqueues the job' do
expect { described_class.perform_later }.to have_enqueued_job(described_class)
expect { described_class.perform_later(account) }.to have_enqueued_job(described_class)
.on_queue('medium')
.with(account)
end
it 'calls the ProcessAppliedSlaJob for both active and active_with_misses' do
@@ -3,14 +3,15 @@ require 'rails_helper'
RSpec.describe Sla::ProcessAppliedSlaJob do
context 'when perform is called' do
let(:account) { create(:account) }
let(:applied_sla) { create(:applied_sla, account: account) }
it 'enqueues the job' do
expect { described_class.perform_later }.to have_enqueued_job(described_class)
expect { described_class.perform_later(applied_sla) }.to have_enqueued_job(described_class)
.with(applied_sla)
.on_queue('medium')
end
it 'calls the EvaluateAppliedSlaService' do
applied_sla = create(:applied_sla)
expect(Sla::EvaluateAppliedSlaService).to receive(:new).with(applied_sla: applied_sla).and_call_original
described_class.perform_now(applied_sla)
end
@@ -1,7 +1,7 @@
require 'rails_helper'
RSpec.describe Account::ContactsExportJob do
subject(:job) { described_class.perform_later }
subject(:job) { described_class.perform_later(account.id, user.id, [], {}) }
let(:account) { create(:account) }
let(:user) { create(:user, account: account, email: 'account-user-test@test.com') }
@@ -1,14 +1,14 @@
require 'rails_helper'
RSpec.describe Conversations::ResolutionJob do
subject(:job) { described_class.perform_later(account) }
subject(:job) { described_class.perform_later(account: account) }
let!(:account) { create(:account) }
let!(:conversation) { create(:conversation, account: account) }
it 'enqueues the job' do
expect { job }.to have_enqueued_job(described_class)
.with(account)
.with(account: account)
.on_queue('low')
end
@@ -1,7 +1,7 @@
require 'rails_helper'
RSpec.describe Conversations::UpdateMessageStatusJob do
subject(:job) { described_class.perform_later(account) }
subject(:job) { described_class.perform_later(conversation.id, conversation.contact_last_seen_at, :read) }
let!(:account) { create(:account) }
let!(:conversation) { create(:conversation, account: account, contact_last_seen_at: DateTime.now.utc) }
@@ -9,7 +9,7 @@ RSpec.describe Conversations::UpdateMessageStatusJob do
it 'enqueues the job' do
expect { job }.to have_enqueued_job(described_class)
.with(account)
.with(conversation.id, conversation.contact_last_seen_at, :read)
.on_queue('low')
end
@@ -12,7 +12,7 @@ RSpec.describe Inboxes::FetchImapEmailsJob do
describe '#perform' do
it 'enqueues the job' do
expect do
described_class.perform_later
described_class.perform_later(imap_email_channel, 1)
end.to have_enqueued_job(described_class).on_queue('scheduled_jobs')
end
@@ -1,7 +1,7 @@
require 'rails_helper'
RSpec.describe Migration::ConversationsFirstReplySchedulerJob do
subject(:job) { described_class.perform_later }
subject(:job) { described_class.perform_later(account) }
let!(:account) { create(:account) }
let!(:inbox) { create(:inbox, account: account) }
@@ -10,6 +10,7 @@ RSpec.describe Migration::ConversationsFirstReplySchedulerJob do
it 'enqueues the job' do
expect { job }.to have_enqueued_job(described_class)
.on_queue('scheduled_jobs')
.with(account)
end
context 'when there is an outgoing message in conversation' do
@@ -5,9 +5,8 @@ RSpec.describe Notification::RemoveOldNotificationJob do
let(:conversation) { create(:conversation) }
it 'enqueues the job' do
notification = create(:notification, user: user, notification_type: 'conversation_creation', primary_actor: conversation)
expect do
described_class.perform_later(notification)
described_class.perform_later
end.to have_enqueued_job(described_class)
.on_queue('low')
end
@@ -35,7 +35,7 @@ RSpec.describe Integrations::Openai::ProcessorService do
it 'returns the rephrased message using the tone in data' do
request_body = {
'model' => 'gpt-3.5-turbo',
'model' => 'gpt-4o-mini',
'messages' => [
{
'role' => 'system',
@@ -61,7 +61,7 @@ RSpec.describe Integrations::Openai::ProcessorService do
it 'returns the suggested reply' do
request_body = {
'model' => 'gpt-3.5-turbo',
'model' => 'gpt-4o-mini',
'messages' => [
{ role: 'system',
content: Rails.root.join('lib/integrations/openai/openai_prompts/reply.txt').read },
@@ -88,7 +88,7 @@ RSpec.describe Integrations::Openai::ProcessorService do
it 'returns the summarized message' do
request_body = {
'model' => 'gpt-3.5-turbo',
'model' => 'gpt-4o-mini',
'messages' => [
{ 'role' => 'system',
'content' => summary_prompt },
@@ -127,7 +127,7 @@ RSpec.describe Integrations::Openai::ProcessorService do
it 'returns the corrected text' do
request_body = {
'model' => 'gpt-3.5-turbo',
'model' => 'gpt-4o-mini',
'messages' => [
{ 'role' => 'system', 'content' => 'You are a helpful support agent. Please fix the spelling and grammar of the following response. ' \
'Ensure that the reply should be in user language.' },
@@ -149,7 +149,7 @@ RSpec.describe Integrations::Openai::ProcessorService do
it 'returns the shortened text' do
request_body = {
'model' => 'gpt-3.5-turbo',
'model' => 'gpt-4o-mini',
'messages' => [
{ 'role' => 'system', 'content' => 'You are a helpful support agent. Please shorten the following response. ' \
'Ensure that the reply should be in user language.' },
@@ -171,7 +171,7 @@ RSpec.describe Integrations::Openai::ProcessorService do
it 'returns the expanded text' do
request_body = {
'model' => 'gpt-3.5-turbo',
'model' => 'gpt-4o-mini',
'messages' => [
{ 'role' => 'system', 'content' => 'You are a helpful support agent. Please expand the following response. ' \
'Ensure that the reply should be in user language.' },
@@ -193,7 +193,7 @@ RSpec.describe Integrations::Openai::ProcessorService do
it 'returns the friendly text' do
request_body = {
'model' => 'gpt-3.5-turbo',
'model' => 'gpt-4o-mini',
'messages' => [
{ 'role' => 'system', 'content' => 'You are a helpful support agent. Please make the following response more friendly. ' \
'Ensure that the reply should be in user language.' },
@@ -215,7 +215,7 @@ RSpec.describe Integrations::Openai::ProcessorService do
it 'returns the formal text' do
request_body = {
'model' => 'gpt-3.5-turbo',
'model' => 'gpt-4o-mini',
'messages' => [
{ 'role' => 'system', 'content' => 'You are a helpful support agent. Please make the following response more formal. ' \
'Ensure that the reply should be in user language.' },
@@ -237,7 +237,7 @@ RSpec.describe Integrations::Openai::ProcessorService do
it 'returns the simplified text' do
request_body = {
'model' => 'gpt-3.5-turbo',
'model' => 'gpt-4o-mini',
'messages' => [
{ 'role' => 'system', 'content' => 'You are a helpful support agent. Please simplify the following response. ' \
'Ensure that the reply should be in user language.' },
+150 -35
View File
@@ -7845,10 +7845,10 @@ bn.js@^5.2.1:
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.1.tgz#0bc527a6a0d18d0aa8d5b0538ce4a77dccfa7b70"
integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==
body-parser@1.20.2:
version "1.20.2"
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd"
integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==
body-parser@1.20.3:
version "1.20.3"
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.3.tgz#1953431221c6fb5cd63c4b36d53fab0928e548c6"
integrity sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==
dependencies:
bytes "3.1.2"
content-type "~1.0.5"
@@ -7858,7 +7858,7 @@ body-parser@1.20.2:
http-errors "2.0.0"
iconv-lite "0.4.24"
on-finished "2.4.1"
qs "6.11.0"
qs "6.13.0"
raw-body "2.5.2"
type-is "~1.6.18"
unpipe "1.0.0"
@@ -8211,6 +8211,17 @@ call-bind@^1.0.0, call-bind@^1.0.2:
function-bind "^1.1.1"
get-intrinsic "^1.0.2"
call-bind@^1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9"
integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==
dependencies:
es-define-property "^1.0.0"
es-errors "^1.3.0"
function-bind "^1.1.2"
get-intrinsic "^1.2.4"
set-function-length "^1.2.1"
call-me-maybe@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.2.tgz#03f964f19522ba643b1b0693acb9152fe2074baa"
@@ -9513,6 +9524,15 @@ define-data-property@^1.0.1:
gopd "^1.0.1"
has-property-descriptors "^1.0.0"
define-data-property@^1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e"
integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==
dependencies:
es-define-property "^1.0.0"
es-errors "^1.3.0"
gopd "^1.0.1"
define-lazy-prop@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f"
@@ -9874,15 +9894,10 @@ dotenv@^8.0.0:
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.6.0.tgz#061af664d19f7f4d8fc6e4ff9b584ce237adcb8b"
integrity sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==
dset@^3.1.1:
version "3.1.2"
resolved "https://registry.yarnpkg.com/dset/-/dset-3.1.2.tgz#89c436ca6450398396dc6538ea00abc0c54cd45a"
integrity sha512-g/M9sqy3oHe477Ar4voQxWtaPIFw1jTdKZuomOjhCcBx9nHUNn0pu6NopuFFrTh/TRZIKEj+76vLWFu9BNKk+Q==
dset@^3.1.2:
version "3.1.3"
resolved "https://registry.yarnpkg.com/dset/-/dset-3.1.3.tgz#c194147f159841148e8e34ca41f638556d9542d2"
integrity sha512-20TuZZHCEZ2O71q9/+8BwKwZ0QtD9D8ObhrihJPr+vLLYlSuAU3/zL4cSlgbfeoGHTjCSJBa7NGcrF9/Bx/WJQ==
dset@^3.1.1, dset@^3.1.2:
version "3.1.4"
resolved "https://registry.yarnpkg.com/dset/-/dset-3.1.4.tgz#f8eaf5f023f068a036d08cd07dc9ffb7d0065248"
integrity sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==
duplexify@^3.4.2, duplexify@^3.6.0:
version "3.7.1"
@@ -9985,6 +10000,11 @@ encodeurl@~1.0.2:
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==
encodeurl@~2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58"
integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==
end-of-stream@^1.0.0, end-of-stream@^1.1.0:
version "1.4.4"
resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
@@ -10172,6 +10192,18 @@ es-array-method-boxes-properly@^1.0.0:
resolved "https://registry.yarnpkg.com/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz#873f3e84418de4ee19c5be752990b2e44718d09e"
integrity sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==
es-define-property@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845"
integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==
dependencies:
get-intrinsic "^1.2.4"
es-errors@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f"
integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==
es-get-iterator@^1.0.2:
version "1.1.3"
resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.3.tgz#3ef87523c5d464d41084b2c3c9c214f1199763d6"
@@ -10725,36 +10757,36 @@ expand-tilde@^2.0.0, expand-tilde@^2.0.2:
homedir-polyfill "^1.0.1"
express@^4.17.1:
version "4.19.2"
resolved "https://registry.yarnpkg.com/express/-/express-4.19.2.tgz#e25437827a3aa7f2a827bc8171bbbb664a356465"
integrity sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==
version "4.20.0"
resolved "https://registry.yarnpkg.com/express/-/express-4.20.0.tgz#f1d08e591fcec770c07be4767af8eb9bcfd67c48"
integrity sha512-pLdae7I6QqShF5PnNTCVn4hI91Dx0Grkn2+IAsMTgMIKuQVte2dN9PeGSSAME2FR8anOhVA62QDIUaWVfEXVLw==
dependencies:
accepts "~1.3.8"
array-flatten "1.1.1"
body-parser "1.20.2"
body-parser "1.20.3"
content-disposition "0.5.4"
content-type "~1.0.4"
cookie "0.6.0"
cookie-signature "1.0.6"
debug "2.6.9"
depd "2.0.0"
encodeurl "~1.0.2"
encodeurl "~2.0.0"
escape-html "~1.0.3"
etag "~1.8.1"
finalhandler "1.2.0"
fresh "0.5.2"
http-errors "2.0.0"
merge-descriptors "1.0.1"
merge-descriptors "1.0.3"
methods "~1.1.2"
on-finished "2.4.1"
parseurl "~1.3.3"
path-to-regexp "0.1.7"
path-to-regexp "0.1.10"
proxy-addr "~2.0.7"
qs "6.11.0"
range-parser "~1.2.1"
safe-buffer "5.2.1"
send "0.18.0"
serve-static "1.15.0"
send "0.19.0"
serve-static "1.16.0"
setprototypeof "1.2.0"
statuses "2.0.1"
type-is "~1.6.18"
@@ -11268,6 +11300,11 @@ function-bind@^1.1.1:
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
function-bind@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c"
integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
function.prototype.name@^1.1.0, function.prototype.name@^1.1.5:
version "1.1.5"
resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.5.tgz#cce0505fe1ffb80503e6f9e46cc64e46a12a9621"
@@ -11351,6 +11388,17 @@ get-intrinsic@^1.1.3, get-intrinsic@^1.2.0, get-intrinsic@^1.2.1:
has-proto "^1.0.1"
has-symbols "^1.0.3"
get-intrinsic@^1.2.4:
version "1.2.4"
resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd"
integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==
dependencies:
es-errors "^1.3.0"
function-bind "^1.1.2"
has-proto "^1.0.1"
has-symbols "^1.0.3"
hasown "^2.0.0"
get-package-type@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a"
@@ -11686,6 +11734,13 @@ has-property-descriptors@^1.0.0:
dependencies:
get-intrinsic "^1.1.1"
has-property-descriptors@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854"
integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==
dependencies:
es-define-property "^1.0.0"
has-proto@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.1.tgz#1885c1305538958aff469fef37937c22795408e0"
@@ -11778,6 +11833,13 @@ hash.js@^1.0.0, hash.js@^1.0.3:
inherits "^2.0.3"
minimalistic-assert "^1.0.1"
hasown@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003"
integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==
dependencies:
function-bind "^1.1.2"
hast-to-hyperscript@^9.0.0:
version "9.0.1"
resolved "https://registry.yarnpkg.com/hast-to-hyperscript/-/hast-to-hyperscript-9.0.1.tgz#9b67fd188e4c81e8ad66f803855334173920218d"
@@ -13984,10 +14046,10 @@ meow@^3.1.0:
redent "^1.0.0"
trim-newlines "^1.0.0"
merge-descriptors@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==
merge-descriptors@1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz#d80319a65f3c7935351e5cfdac8f9318504dbed5"
integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==
merge-source-map@^1.1.0:
version "1.1.0"
@@ -14610,6 +14672,11 @@ object-inspect@^1.12.3:
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9"
integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==
object-inspect@^1.13.1:
version "1.13.2"
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.2.tgz#dea0088467fb991e67af4058147a24824a3043ff"
integrity sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==
object-inspect@^1.9.0:
version "1.12.2"
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.2.tgz#c0641f26394532f28ab8d796ab954e43c009a8ea"
@@ -15162,10 +15229,10 @@ path-scurry@^1.11.1:
lru-cache "^10.2.0"
minipass "^5.0.0 || ^6.0.2 || ^7.0.0"
path-to-regexp@0.1.7:
version "0.1.7"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==
path-to-regexp@0.1.10:
version "0.1.10"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.10.tgz#67e9108c5c0551b9e5326064387de4763c4d5f8b"
integrity sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==
path-type@^1.0.0:
version "1.1.0"
@@ -16867,6 +16934,13 @@ qs@6.11.0:
dependencies:
side-channel "^1.0.4"
qs@6.13.0:
version "6.13.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906"
integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==
dependencies:
side-channel "^1.0.6"
qs@^6.10.0:
version "6.11.2"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.2.tgz#64bea51f12c1f5da1bc01496f48ffcff7c69d7d9"
@@ -17877,6 +17951,25 @@ send@0.18.0:
range-parser "~1.2.1"
statuses "2.0.1"
send@0.19.0:
version "0.19.0"
resolved "https://registry.yarnpkg.com/send/-/send-0.19.0.tgz#bbc5a388c8ea6c048967049dbeac0e4a3f09d7f8"
integrity sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==
dependencies:
debug "2.6.9"
depd "2.0.0"
destroy "1.2.0"
encodeurl "~1.0.2"
escape-html "~1.0.3"
etag "~1.8.1"
fresh "0.5.2"
http-errors "2.0.0"
mime "1.6.0"
ms "2.1.3"
on-finished "2.4.1"
range-parser "~1.2.1"
statuses "2.0.1"
serialize-javascript@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa"
@@ -17922,10 +18015,10 @@ serve-index@^1.9.1:
mime-types "~2.1.17"
parseurl "~1.3.2"
serve-static@1.15.0:
version "1.15.0"
resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540"
integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==
serve-static@1.16.0:
version "1.16.0"
resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.16.0.tgz#2bf4ed49f8af311b519c46f272bf6ac3baf38a92"
integrity sha512-pDLK8zwl2eKaYrs8mrPZBJua4hMplRWJ1tIFksVC3FtBEBnl8dxgeHtsaMS8DhS9i4fLObaon6ABoc4/hQGdPA==
dependencies:
encodeurl "~1.0.2"
escape-html "~1.0.3"
@@ -17937,6 +18030,18 @@ set-blocking@^2.0.0:
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc=
set-function-length@^1.2.1:
version "1.2.2"
resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449"
integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==
dependencies:
define-data-property "^1.1.4"
es-errors "^1.3.0"
function-bind "^1.1.2"
get-intrinsic "^1.2.4"
gopd "^1.0.1"
has-property-descriptors "^1.0.2"
set-function-name@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/set-function-name/-/set-function-name-2.0.1.tgz#12ce38b7954310b9f61faa12701620a0c882793a"
@@ -18019,6 +18124,16 @@ side-channel@^1.0.4:
get-intrinsic "^1.0.2"
object-inspect "^1.9.0"
side-channel@^1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2"
integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==
dependencies:
call-bind "^1.0.7"
es-errors "^1.3.0"
get-intrinsic "^1.2.4"
object-inspect "^1.13.1"
siginfo@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/siginfo/-/siginfo-2.0.0.tgz#32e76c70b79724e3bb567cb9d543eb858ccfaf30"