Compare commits

...
Author SHA1 Message Date
Muhsin KelothandGitHub 3b723ecccc Merge branch 'develop' into refactor/structured-clone 2024-07-24 19:18:38 +05:30
Sivin VargheseandGitHub 71073a06ff fix: Broken connect to facebook page layout (#9830)
# Pull Request Template

## Description

This PR will fix the layout issue in connecting to the Facebook page
layout.

Fixes
https://linear.app/chatwoot/issue/CW-3489/connect-facebook-page-layout-is-broken

## Type of change

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

## How Has This Been Tested?

**Before**
<img width="1264" alt="image"
src="https://github.com/user-attachments/assets/2b9e412b-f172-4184-b45b-8c97333c3e51">


**After**
<img width="1264" alt="image"
src="https://github.com/user-attachments/assets/035f3468-f611-43ce-a058-fb8eafa1a45d">




## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
2024-07-24 16:45:17 +05:30
Muhsin KelothandGitHub 168938500f Merge branch 'develop' into refactor/structured-clone 2024-07-24 12:19:15 +05:30
PranavandGitHub 39d20b197d chore: Update the integration icons, logic for enabled/active attributes for the integration (#9828)
This PR would update the logos for the integrations (also add the dark mode variants to be used in the future) and updates the logic for enabled / active attributes in the apps.
2024-07-23 17:45:53 -07:00
Sivin VargheseandGitHub fb99ba7b40 feat: Rewrite uiSettings mixin to a composable (#9819) 2024-07-23 21:27:22 +05:30
79aa5a5d7f feat: Replace alertMixin usage with useAlert (#9793)
# Pull Request Template

## Description

This PR will replace the usage of `alertMixin` from the code base with
the `useAlert` composable.

Fixes
https://linear.app/chatwoot/issue/CW-3462/replace-alertmixin-usage-with-usealert

## Type of change

- [x] Breaking change (fix or feature that would cause existing
functionality not to work as expected)

## How Has This Been Tested?

Please refer this issue description

https://linear.app/chatwoot/issue/CW-3462/replace-alertmixin-usage-with-usealert


## Checklist:

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

---------

Co-authored-by: Sojan Jose <sojan@pepalo.com>
2024-07-23 16:41:11 +05:30
Muhsin KelothandGitHub 2bb7d1e4b2 Merge branch 'develop' into refactor/structured-clone 2024-07-23 13:12:45 +05:30
Sivin VargheseandGitHub 10ee773aac feat: Rewrite labels/validationMixin mixin to a helper (#9818) 2024-07-22 21:41:05 +05:30
Shivam Mishra c5c3b74728 refactor: use structuredClone instead of JSON stringify and parse 2024-07-22 16:09:05 +05:30
Sivin VargheseandGitHub 81d7c51a84 feat: Replace contentTypeMixin within the component (#9802)
# Pull Request Template

## Description

This PR will completely remove the `contentTypeMixin` and be used in the
component as it is.

Fixes
https://linear.app/chatwoot/issue/CW-3465/rewrite-contenttypemixin-mixin-as-a-helper

## Type of change

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

## How Has This Been Tested?

Take a look at this component
`app/javascript/dashboard/components/widgets/conversation/Message.vue`


## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
2024-07-22 13:08:04 +05:30
79381b08cc feat: Move timeMixin to a helper (#9799)
# Pull Request Template

## Description

This PR will replace the usage of `timeMixin` with `timeHelper`

Fixes
https://linear.app/chatwoot/issue/CW-3451/move-time-mixin-to-a-helper

## Type of change

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

## How Has This Been Tested?

Please refer to this issue description.
https://linear.app/chatwoot/issue/CW-3451/move-time-mixin-to-a-helper

## Checklist:

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

---------

Co-authored-by: Sojan Jose <sojan@pepalo.com>
2024-07-22 13:07:29 +05:30
Sivin VargheseandGitHub 84c380c8c4 feat: Replace SLA validationMixin within the component (#9804)
# Pull Request Template

## Description

This PR will completely remove the SLA `validationMixin` and be used in
the component as it is with a name change.

Fixes
https://linear.app/chatwoot/issue/CW-3456/rewrite-sla-validationmixin-mixin-to-a-composable

## Type of change

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

## How Has This Been Tested?

Take a look at these components
1. `dashboard/routes/dashboard/settings/sla/AddSLA.vue` **(Not used)**
2. `dashboard/routes/dashboard/settings/sla/SlaForm.vue`
3. `dashboard/routes/dashboard/settings/sla/SlaTimeInput.vue`


## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
2024-07-22 13:05:55 +05:30
cb0642564c feat: add promise based loader for FB script (#9780)
![CleanShot 2024-07-16 at 11 10
40@2x](https://github.com/user-attachments/assets/8b938968-5f80-4a19-95fb-e00e1dbd7526)

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2024-07-22 11:32:05 +05:30
23e30fcb1a feat: Delete bulkActionsMixin (#9800)
# Pull Request Template

## Description

This PR will remove the `bulkActionsMixin` usage. Seems like it is not
used anywhere.

Fixes https://linear.app/chatwoot/issue/CW-3453/delete-bulkactionsmixin

## Type of change

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


## Checklist:

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

Co-authored-by: Sojan Jose <sojan@pepalo.com>
2024-07-19 11:14:56 +05:30
Sojan JoseandGitHub ae8619142f chore: Update dependencies to fix security issues (#9801)
- update dependencies to fix security issues
2024-07-18 20:08:26 -07:00
Vishnu NarayananandGitHub 499408ea6f fix: add restart policy for rails/sidekiq containers (#9797)
- Add `restart:always` policy for rails and sidekiq containers in the
production compose file

Fixes #9501
Fixes
https://linear.app/chatwoot/issue/PR-1099/missing-restart-always-at-docker-compose-file
2024-07-18 18:33:03 +05:30
Sojan dc245814fe Merge branch 'release/3.11.0' into develop 2024-07-16 19:23:13 -07:00
Sojan 6c52a0638d Bump version to 3.11.0 2024-07-16 19:22:11 -07:00
64a9a46b28 fix: Docker compose build of unable to select packages (#9783)
Fixes #9782

Co-authored-by: grape <grape@auro.gg>
2024-07-16 09:51:56 +05:30
Sivin VargheseandGitHub 30d4f6c2f5 fix: TypeError cannot read properties of undefined (reading 'status') (#9774)
# Pull Request Template

## Description

**Issue**
The error `Cannot read properties of undefined (reading 'status')`
occurs because the `error.response.status` is accessed without checking
if `error.response` is present.

I can't able to reproduce this issue.

**Solution**
To resolve this issue, I added a check to ensure that this
`error.response` is defined before accessing the `error.response.status`

Fixes
https://linear.app/chatwoot/issue/CW-3322/typeerror-cannot-read-properties-of-undefined-reading-status

## Type of change

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

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
2024-07-16 07:02:02 +05:30
Sivin VargheseandGitHub 5909c0f8b7 fix: TypeError cannot read properties of null (reading 'file_type') (#9778)
# Pull Request Template

## Description

**Cases**

## Type of change


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



## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
2024-07-16 07:01:48 +05:30
Sivin VargheseandGitHub 554388bff3 fix: TypeError cannot read property 'focus' of undefined (#9765) 2024-07-15 19:33:16 +05:30
Sivin VargheseandGitHub ebf3b46f0d fix: TypeError cannot read properties of undefined (reading '$store') (#9777)
This issue is an extremely narrow edge case that does not affect the app functionality. It occurs here

1. `executeRecurringNotification` is the method that sets a timeout of 30 seconds and pings as required
2. This method is triggered when one of the following events happen `click`, `touchstart`, `keypress`, `keydown`
3. The class in question is initalized before the Vue app is mounted in `application.js`, the vue app is mounted on `window.onload` event
4. In a case where the Vue app is not created (i.e. `window.onload` event is not fired) within 30 seconds of a user input, this error occurs.
2024-07-15 18:50:43 +05:30
Shivam MishraandGitHub 5cfac31f93 fix: return if user is not present (#9768)
Fixes
https://chatwoot-p3.sentry.io/share/issue/aa6a3c0accb345e984ed80eaddc3efd5/

In the case where the user is redirected to the page from a redirect
like google login, the analytics helper breaks. This PR fixes it by
checking if the user is present.

This does not break the user email tracking since the user is redirected
anyway and the `AnalyticsHelper` is reinitialised
2024-07-15 17:42:35 +05:30
Sivin VargheseandGitHub 787ac3134d fix: TypeError cannot read properties of undefined (reading 'data') (#9766)
# Pull Request Template

## Description

**Issue**
The error `Cannot read properties of undefined (reading 'data')` occurs
because the `error.response.data` is accessed without checking if
`error.response` is present.

I can able to reproduce this issue.

**Solution**
To resolve this issue, I can add a check to ensure that this
`error.response` is defined before accessing the `error.response.data`

Fixes
https://linear.app/chatwoot/issue/CW-3406/typeerror-cannot-read-properties-of-undefined-reading-data

## Type of change

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


## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
2024-07-12 09:21:40 +05:30
Sivin VargheseandGitHub 2a06c85f1a fix: Message editor fails to render markdown headings (#9748) 2024-07-11 12:23:27 +05:30
acefd82378 fix: Add a DISCONNECT_DELAY_THRESHOLD while fetching the conversations (#9757)
The disconnect threshold is added to account for delays in identifying disconnections (for example, the websocket disconnection takes up to 3 seconds) while fetching the latest updated conversations or messages.

In this case, the cable disconnection event takes about 3 seconds to fire. If there was a conversation which was created in this 3 second, it would not be displayed in the UI until the refresh.

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2024-07-10 20:28:30 -07:00
Sivin VargheseandGitHub abf376940d fix: Avoid overflow caused by the empty span element (#9753)
This PR addresses an issue with the `createNotificationBubble` function, which generates an empty `span` element with the class `woot--notification`. This `span` element is currently not utilized anywhere in the code and lacks associated CSS, leading to an overflow issue, specifically in the Firefox browser. It solves the website overflow issue by removing this unused `span` element. This change is unrelated to the unread notification dot, which is working as before.

Fixes https://linear.app/chatwoot/issue/PR-1098/missing-css-for-woot-notification-div
2024-07-10 19:56:46 -07:00
9de8c27368 feat: Use vitest instead of jest, run all the specs anywhere in app/ folder in the CI (#9722)
Due to the pattern `**/specs/*.spec.js` defined in CircleCI, none of the
frontend spec in the folders such as
`specs/<domain-name>/getters.spec.js` were not executed in Circle CI.

This PR fixes the issue, along with the following changes: 
- Use vitest instead of jest
- Remove jest dependancies
- Update tests to work with vitest

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2024-07-10 08:32:16 -07:00
wudiandGitHub 9498d1f003 fix: Localize 'Social Profiles' text in ContactForm (#9745) 2024-07-09 22:16:45 -07:00
PranavandGitHub a2cb932d54 chore: Upgrade csv-safe to the latest version (#9739)
The following error starting is shown on the console after the ruby
upgrade.

csv.rb was loaded from the standard library, but will no longer be part
of the default gems since Ruby 3.4.0. Add csv to your Gemfile or
gemspec. Also contact author of csv-safe-3.2.1 to add csv into its
gemspec.


Csv-safe has already added a patch via
https://github.com/zvory/csv-safe/pull/20.

This PR updates the version to the latest version of csv-safe (3.3.1)
2024-07-09 12:03:05 -04:00
fe246698b6 chore: [Snyk] Fix for 1 vulnerabilities (#9720)
- updates for security vulnerabilities 

Co-authored-by: snyk-bot <snyk-bot@snyk.io>
2024-07-08 14:00:54 -04:00
31bcdaa3dd chore: Upgrade ruby to 3.3.3 (#9664)
- Upgrade the ruby version to 3.3.3

---------

Co-authored-by: Vishnu Narayanan <iamwishnu@gmail.com>
2024-07-05 12:20:07 +05:30
Sivin VargheseandGitHub 6ae606c981 fix: Custom snooze is not working in mobile view (#9717)
# Pull Request Template

## Description

Currently, when a user navigates to a chat and attempts to access the
custom snooze modal, it is not visible, making it unable to set custom
snooze options. With this fix, the custom snooze modal will correctly
display even when a chat is open in mobile view.

**Cause of this issue**
The `<custom-snooze-modal/>` component is added to the `<chat-list/>`
component. To accommodate small screen views, we are using the expanded
view. However, if we open a chat and select the custom snooze option
from the chat header in the message view, the `<custom-snooze-modal/>`
component is hidden in the `<chat-list/>` component.

**Solution**
So, I moved the `<custom-snooze-modal/>` to the wrapper component
`<conversation-view/>` so we can use in all cases like,
1. Right-click to custom snooze
2. CMD bar custom snooze
3. Small screen custom snooze
2024-07-04 13:13:03 +05:30
aaf47b4c1f chore: [Snyk] Security upgrade sidekiq from 7.2.4 to 7.3.0 (#9710)
Upgrade gems to mitigate vulnerabilities.

Co-authored-by: snyk-bot <snyk-bot@snyk.io>
2024-07-03 15:48:57 -07:00
cc4851b19d chore: Move frontend authorization to permission based system (#9709)
We previously relied on user roles to determine whether to render
specific routes in our frontend components. A permissions-based model is replacing this approach.


Follow up: #9695

Co-authored-by: Pranav <pranavrajs@gmail.com>
2024-07-03 15:13:16 -07:00
Shivam MishraandGitHub 5520bf68f3 feat: disable scripts on password reset page (#9693) 2024-07-01 11:11:57 +05:30
Sojan JoseandGitHub 46621b0983 chore: Add permissions to auth data (#9695)
This API change sets the foundation for an upcoming frontend update, transitioning from a role-based model to a permission-based model. This new approach will determine eligibility for various actions and UI elements based on specific permissions rather than roles, enhancing flexibility and security in user access management.
2024-06-28 12:52:48 -07:00
Sivin VargheseandGitHub 2c94c89077 feat: Add video message viewer in agent widget bubble (#9691)
Fixes https://linear.app/chatwoot/issue/CW-3384/video-message-display-issue
2024-06-28 12:51:27 -07:00
Sojan 97de283103 Merge branch 'release/3.10.2' into develop 2024-06-26 17:09:16 -07:00
Sojan 7ed7c1b618 Bump version to 3.10.2 2024-06-26 17:04:56 -07:00
Sivin VargheseandGitHub 8f3234cf4b feat: Add video message viewing in to the user bubble in widget (#9642) 2024-06-26 13:21:41 -07:00
What are you searching?andGitHub b2de6843f6 chore()Update Translate vi.yml (#9656) 2024-06-26 12:54:57 -07:00
73c4180e64 chore(snyk): Upgrade sentry-rails & sentry-sidekiq to 5.18.0 (#9686)
<p>This PR was automatically created by Snyk using the credentials of a
real user.</p><br
/>![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`
- `Gemfile.lock`




#### 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=eyJ3cml0ZUtleSI6InJyWmxZcEdHY2RyTHZsb0lYd0dUcVg4WkFRTnNCOUEwIiwiYW5vbnltb3VzSWQiOiJhM2ZjZWMzOC1mZjk5LTRkNjQtYWU2OS01NDVhZDA2N2FmZjUiLCJldmVudCI6IlBSIHZpZXdlZCIsInByb3BlcnRpZXMiOnsicHJJZCI6ImEzZmNlYzM4LWZmOTktNGQ2NC1hZTY5LTU0NWFkMDY3YWZmNSJ9fQ=="
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":"sentry-rails","from":"5.17.3","to":"5.18.0"},{"name":"sentry-sidekiq","from":"5.17.3","to":"5.18.0"}],"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"}],"prId":"a3fcec38-ff99-4d64-ae69-545ad067aff5","prPublicId":"a3fcec38-ff99-4d64-ae69-545ad067aff5","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":["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>
Co-authored-by: Pranav <pranav@chatwoot.com>
2024-06-26 12:46:21 -07:00
96f4f50d2d feat: Add the ability to un-assign teams using automation (#9668)
Co-authored-by: Pranav <pranavrajs@gmail.com>
2024-06-26 12:40:36 -07:00
PranavandGitHub f7f687ce53 fix: Update notification payload (#9666)
- Fix notification payload to avoid argument error.
2024-06-21 17:28:48 -07:00
Sojan 5716bb29b8 Merge branch 'release/3.10.1' into develop 2024-06-21 15:07:09 -07:00
439 changed files with 5134 additions and 5406 deletions
+2 -5
View File
@@ -7,7 +7,7 @@ defaults: &defaults
working_directory: ~/build
docker:
# specify the version you desire here
- image: cimg/ruby:3.2.2-browsers
- image: cimg/ruby:3.3.3-browsers
# Specify service dependencies here if necessary
# CircleCI maintains a library of pre-built images
@@ -130,10 +130,7 @@ jobs:
command: |
mkdir -p ~/tmp/test-results/frontend_specs
~/tmp/cc-test-reporter before-build
TESTFILES=$(circleci tests glob **/specs/*.spec.js | circleci tests split --split-by=timings)
yarn test:coverage --profile 10 \
--out ~/tmp/test-results/yarn.xml \
-- ${TESTFILES}
yarn test:coverage
- run:
name: Code Climate Test Coverage
command: |
+2 -2
View File
@@ -12,7 +12,7 @@ services:
args:
VARIANT: "ubuntu-22.04"
NODE_VERSION: "20.9.0"
RUBY_VERSION: "3.2.2"
RUBY_VERSION: "3.3.3"
# On Linux, you may need to update USER_UID and USER_GID below if not your local UID is not 1000.
USER_UID: "1000"
USER_GID: "1000"
@@ -25,7 +25,7 @@ services:
args:
VARIANT: "ubuntu-22.04"
NODE_VERSION: "20.9.0"
RUBY_VERSION: "3.2.2"
RUBY_VERSION: "3.3.3"
# On Linux, you may need to update USER_UID and USER_GID below if not your local UID is not 1000.
USER_UID: "1000"
USER_GID: "1000"
+1 -1
View File
@@ -65,10 +65,10 @@ module.exports = {
},
env: {
browser: true,
jest: true,
node: true,
},
globals: {
bus: true,
vi: true,
},
};
+1 -1
View File
@@ -1 +1 @@
3.2.2
3.3.3
+4 -4
View File
@@ -1,6 +1,6 @@
source 'https://rubygems.org'
ruby '3.2.2'
ruby '3.3.3'
##-- base gems for rails --##
gem 'rack-cors', '2.0.0', require: 'rack/cors'
@@ -111,12 +111,12 @@ gem 'elastic-apm', require: false
gem 'newrelic_rpm', require: false
gem 'newrelic-sidekiq-metrics', '>= 1.6.2', require: false
gem 'scout_apm', require: false
gem 'sentry-rails', '>= 5.14.0', require: false
gem 'sentry-rails', '>= 5.18.1', require: false
gem 'sentry-ruby', require: false
gem 'sentry-sidekiq', '>= 5.15.0', require: false
gem 'sentry-sidekiq', '>= 5.18.1', require: false
##-- background job processing --##
gem 'sidekiq', '>= 7.2.4'
gem 'sidekiq', '>= 7.3.0'
# We want cron jobs
gem 'sidekiq-cron', '>= 1.12.0'
+44 -36
View File
@@ -103,8 +103,8 @@ GEM
tzinfo (~> 2.0)
acts-as-taggable-on (9.0.1)
activerecord (>= 6.0, < 7.1)
addressable (2.8.4)
public_suffix (>= 2.0.2, < 6.0)
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
administrate (0.20.1)
actionpack (>= 6.0, < 8.0)
actionview (>= 6.0, < 8.0)
@@ -150,7 +150,7 @@ GEM
statsd-ruby (~> 1.1)
base64 (0.2.0)
bcrypt (3.1.20)
bigdecimal (3.1.7)
bigdecimal (3.1.8)
bindex (0.8.1)
bootsnap (1.16.0)
msgpack (~> 1.2)
@@ -171,10 +171,13 @@ GEM
commonmarker (0.23.10)
concurrent-ruby (1.3.3)
connection_pool (2.4.1)
crack (0.4.5)
crack (1.0.0)
bigdecimal
rexml
crass (1.0.6)
csv-safe (3.2.1)
csv (3.3.0)
csv-safe (3.3.1)
csv (~> 3.0)
cypress-on-rails (1.16.0)
rack
database_cleaner (2.0.2)
@@ -183,13 +186,16 @@ GEM
activerecord (>= 5.a)
database_cleaner-core (~> 2.0.0)
database_cleaner-core (2.0.1)
date (3.3.4)
ddtrace (1.11.1)
debase-ruby_core_source (>= 0.10.16, <= 3.2.0)
libdatadog (~> 2.0.0.1.0)
libddwaf (~> 1.8.2.0.0)
datadog-ci (0.8.3)
msgpack
debase-ruby_core_source (3.2.0)
date (3.3.4)
ddtrace (1.23.2)
datadog-ci (~> 0.8.1)
debase-ruby_core_source (= 3.3.1)
libdatadog (~> 7.0.0.1.0)
libddwaf (~> 1.14.0.0.0)
msgpack
debase-ruby_core_source (3.3.1)
debug (1.8.0)
irb (>= 1.5.0)
reline (>= 0.3.1)
@@ -353,7 +359,7 @@ GEM
ruby2ruby (~> 2.4)
ruby_parser (~> 3.10)
hana (1.3.7)
hashdiff (1.0.1)
hashdiff (1.1.0)
hashie (5.0.0)
http (5.1.1)
addressable (~> 2.8)
@@ -416,15 +422,15 @@ GEM
addressable (~> 2.8)
letter_opener (1.8.1)
launchy (>= 2.2, < 3)
libdatadog (2.0.0.1.0)
libdatadog (2.0.0.1.0-x86_64-linux)
libddwaf (1.8.2.0.0)
libdatadog (7.0.0.1.0)
libdatadog (7.0.0.1.0-x86_64-linux)
libddwaf (1.14.0.0.0)
ffi (~> 1.0)
libddwaf (1.8.2.0.0-arm64-darwin)
libddwaf (1.14.0.0.0-arm64-darwin)
ffi (~> 1.0)
libddwaf (1.8.2.0.0-x86_64-darwin)
libddwaf (1.14.0.0.0-x86_64-darwin)
ffi (~> 1.0)
libddwaf (1.8.2.0.0-x86_64-linux)
libddwaf (1.14.0.0.0-x86_64-linux)
ffi (~> 1.0)
line-bot-api (1.28.0)
liquid (5.4.0)
@@ -434,6 +440,7 @@ GEM
llhttp-ffi (0.4.0)
ffi-compiler (~> 1.0)
rake (~> 13.0)
logger (1.6.0)
lograge (0.14.0)
actionpack (>= 4)
activesupport (>= 4)
@@ -460,7 +467,7 @@ GEM
mini_magick (4.12.0)
mini_mime (1.1.5)
mini_portile2 (2.8.7)
minitest (5.24.0)
minitest (5.24.1)
mock_redis (0.36.0)
ruby2_keywords
msgpack (1.7.0)
@@ -544,7 +551,7 @@ GEM
method_source (~> 1.0)
pry-rails (0.3.9)
pry (>= 0.10.4)
public_suffix (5.0.1)
public_suffix (6.0.0)
puma (6.4.2)
nio4r (~> 2.0)
pundit (2.3.0)
@@ -603,7 +610,7 @@ GEM
ffi (~> 1.0)
redis (5.0.6)
redis-client (>= 0.9.0)
redis-client (0.22.1)
redis-client (0.22.2)
connection_pool
redis-namespace (1.10.0)
redis (>= 4)
@@ -627,8 +634,8 @@ GEM
retriable (3.1.2)
reverse_markdown (2.1.1)
nokogiri
rexml (3.2.8)
strscan (>= 3.0.9)
rexml (3.3.2)
strscan
rspec-core (3.13.0)
rspec-support (~> 3.13.0)
rspec-expectations (3.13.1)
@@ -703,23 +710,24 @@ GEM
activesupport (>= 4)
selectize-rails (0.12.6)
semantic_range (3.0.0)
sentry-rails (5.17.3)
sentry-rails (5.18.1)
railties (>= 5.0)
sentry-ruby (~> 5.17.3)
sentry-ruby (5.17.3)
sentry-ruby (~> 5.18.1)
sentry-ruby (5.18.1)
bigdecimal
concurrent-ruby (~> 1.0, >= 1.0.2)
sentry-sidekiq (5.17.3)
sentry-ruby (~> 5.17.3)
sentry-sidekiq (5.18.1)
sentry-ruby (~> 5.18.1)
sidekiq (>= 3.0)
sexp_processor (4.17.0)
shoulda-matchers (5.3.0)
activesupport (>= 5.2.0)
sidekiq (7.2.4)
sidekiq (7.3.0)
concurrent-ruby (< 2)
connection_pool (>= 2.3.0)
logger
rack (>= 2.2.4)
redis-client (>= 0.19.0)
redis-client (>= 0.22.2)
sidekiq-cron (1.12.0)
fugit (~> 1.8)
globalid (>= 1.0.1)
@@ -802,7 +810,7 @@ GEM
web-push (3.0.1)
jwt (~> 2.0)
openssl (~> 3.0)
webmock (3.18.1)
webmock (3.23.1)
addressable (>= 2.8.0)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
@@ -931,11 +939,11 @@ DEPENDENCIES
scout_apm
scss_lint
seed_dump
sentry-rails (>= 5.14.0)
sentry-rails (>= 5.18.1)
sentry-ruby
sentry-sidekiq (>= 5.15.0)
sentry-sidekiq (>= 5.18.1)
shoulda-matchers
sidekiq (>= 7.2.4)
sidekiq (>= 7.3.0)
sidekiq-cron (>= 1.12.0)
simplecov (= 0.17.1)
slack-ruby-client (~> 2.2.0)
@@ -960,7 +968,7 @@ DEPENDENCIES
working_hours
RUBY VERSION
ruby 3.2.2p185
ruby 3.3.3p89
BUNDLED WITH
2.4.6
2.5.16
+11 -1
View File
@@ -37,7 +37,7 @@ class DashboardController < ActionController::Base
end
def set_dashboard_scripts
@dashboard_scripts = GlobalConfig.get_value('DASHBOARD_SCRIPTS')
@dashboard_scripts = sensitive_path? ? nil : GlobalConfig.get_value('DASHBOARD_SCRIPTS')
end
def ensure_installation_onboarding
@@ -75,4 +75,14 @@ class DashboardController < ActionController::Base
'application'
end
end
def sensitive_path?
# dont load dashboard scripts on sensitive paths like password reset
sensitive_paths = [edit_user_password_path].freeze
# remove app prefix
current_path = request.path.gsub(%r{^/app}, '')
sensitive_paths.include?(current_path)
end
end
-11
View File
@@ -1,11 +0,0 @@
import App from './App';
import '../../test-matchers';
describe(`App component`, () => {
it(`should be a component`, () => {
// Arrange
// Act
expect(App).toBeVueComponent('App');
// Assert
});
});
@@ -15,10 +15,10 @@ describe('#enterpriseAccountAPI', () => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
post: vi.fn(() => Promise.resolve()),
get: vi.fn(() => Promise.resolve()),
patch: vi.fn(() => Promise.resolve()),
delete: vi.fn(() => Promise.resolve()),
};
beforeEach(() => {
@@ -15,10 +15,10 @@ describe('#accountAPI', () => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
post: vi.fn(() => Promise.resolve()),
get: vi.fn(() => Promise.resolve()),
patch: vi.fn(() => Promise.resolve()),
delete: vi.fn(() => Promise.resolve()),
};
beforeEach(() => {
@@ -10,10 +10,10 @@ describe('#ContactsAPI', () => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
post: vi.fn(() => Promise.resolve()),
get: vi.fn(() => Promise.resolve()),
patch: vi.fn(() => Promise.resolve()),
delete: vi.fn(() => Promise.resolve()),
};
beforeEach(() => {
@@ -14,7 +14,7 @@ describe('#AgentAPI', () => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
post: vi.fn(() => Promise.resolve()),
};
beforeEach(() => {
@@ -14,10 +14,10 @@ describe('#PortalAPI', () => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
post: vi.fn(() => Promise.resolve()),
get: vi.fn(() => Promise.resolve()),
patch: vi.fn(() => Promise.resolve()),
delete: vi.fn(() => Promise.resolve()),
};
beforeEach(() => {
@@ -44,10 +44,10 @@ describe('#PortalAPI', () => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
post: vi.fn(() => Promise.resolve()),
get: vi.fn(() => Promise.resolve()),
patch: vi.fn(() => Promise.resolve()),
delete: vi.fn(() => Promise.resolve()),
};
beforeEach(() => {
@@ -71,10 +71,10 @@ describe('#PortalAPI', () => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
post: vi.fn(() => Promise.resolve()),
get: vi.fn(() => Promise.resolve()),
patch: vi.fn(() => Promise.resolve()),
delete: vi.fn(() => Promise.resolve()),
};
beforeEach(() => {
@@ -98,10 +98,10 @@ describe('#PortalAPI', () => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
post: vi.fn(() => Promise.resolve()),
get: vi.fn(() => Promise.resolve()),
patch: vi.fn(() => Promise.resolve()),
delete: vi.fn(() => Promise.resolve()),
};
beforeEach(() => {
@@ -129,10 +129,10 @@ describe('#PortalAPI', () => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
post: vi.fn(() => Promise.resolve()),
get: vi.fn(() => Promise.resolve()),
patch: vi.fn(() => Promise.resolve()),
delete: vi.fn(() => Promise.resolve()),
};
beforeEach(() => {
@@ -4,10 +4,10 @@ describe('#AssignableAgentsAPI', () => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
post: vi.fn(() => Promise.resolve()),
get: vi.fn(() => Promise.resolve()),
patch: vi.fn(() => Promise.resolve()),
delete: vi.fn(() => Promise.resolve()),
};
beforeEach(() => {
@@ -13,10 +13,10 @@ describe('#FBChannel', () => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
post: vi.fn(() => Promise.resolve()),
get: vi.fn(() => Promise.resolve()),
patch: vi.fn(() => Promise.resolve()),
delete: vi.fn(() => Promise.resolve()),
};
beforeEach(() => {
@@ -17,10 +17,10 @@ describe('#ContactsAPI', () => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
post: vi.fn(() => Promise.resolve()),
get: vi.fn(() => Promise.resolve()),
patch: vi.fn(() => Promise.resolve()),
delete: vi.fn(() => Promise.resolve()),
};
beforeEach(() => {
@@ -16,10 +16,10 @@ describe('#ConversationApi', () => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
post: vi.fn(() => Promise.resolve()),
get: vi.fn(() => Promise.resolve()),
patch: vi.fn(() => Promise.resolve()),
delete: vi.fn(() => Promise.resolve()),
};
beforeEach(() => {
@@ -11,10 +11,10 @@ describe('#Reports API', () => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
post: vi.fn(() => Promise.resolve()),
get: vi.fn(() => Promise.resolve()),
patch: vi.fn(() => Promise.resolve()),
delete: vi.fn(() => Promise.resolve()),
};
beforeEach(() => {
@@ -24,10 +24,10 @@ describe('#ConversationAPI', () => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
post: vi.fn(() => Promise.resolve()),
get: vi.fn(() => Promise.resolve()),
patch: vi.fn(() => Promise.resolve()),
delete: vi.fn(() => Promise.resolve()),
};
beforeEach(() => {
@@ -15,10 +15,10 @@ describe('#ConversationAPI', () => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
post: vi.fn(() => Promise.resolve()),
get: vi.fn(() => Promise.resolve()),
patch: vi.fn(() => Promise.resolve()),
delete: vi.fn(() => Promise.resolve()),
};
beforeEach(() => {
@@ -17,10 +17,10 @@ describe('#InboxesAPI', () => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
post: vi.fn(() => Promise.resolve()),
get: vi.fn(() => Promise.resolve()),
patch: vi.fn(() => Promise.resolve()),
delete: vi.fn(() => Promise.resolve()),
};
beforeEach(() => {
@@ -18,10 +18,10 @@ describe('#integrationAPI', () => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
post: vi.fn(() => Promise.resolve()),
get: vi.fn(() => Promise.resolve()),
patch: vi.fn(() => Promise.resolve()),
delete: vi.fn(() => Promise.resolve()),
};
beforeEach(() => {
@@ -11,10 +11,10 @@ describe('#accountAPI', () => {
describe('createAMeeting', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
post: vi.fn(() => Promise.resolve()),
get: vi.fn(() => Promise.resolve()),
patch: vi.fn(() => Promise.resolve()),
delete: vi.fn(() => Promise.resolve()),
};
beforeEach(() => {
@@ -39,10 +39,10 @@ describe('#accountAPI', () => {
describe('addParticipantToMeeting', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
post: vi.fn(() => Promise.resolve()),
get: vi.fn(() => Promise.resolve()),
patch: vi.fn(() => Promise.resolve()),
delete: vi.fn(() => Promise.resolve()),
};
beforeEach(() => {
@@ -16,10 +16,10 @@ describe('#linearAPI', () => {
describe('getTeams', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
post: vi.fn(() => Promise.resolve()),
get: vi.fn(() => Promise.resolve()),
patch: vi.fn(() => Promise.resolve()),
delete: vi.fn(() => Promise.resolve()),
};
beforeEach(() => {
@@ -41,10 +41,10 @@ describe('#linearAPI', () => {
describe('getTeamEntities', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
post: vi.fn(() => Promise.resolve()),
get: vi.fn(() => Promise.resolve()),
patch: vi.fn(() => Promise.resolve()),
delete: vi.fn(() => Promise.resolve()),
};
beforeEach(() => {
@@ -66,10 +66,10 @@ describe('#linearAPI', () => {
describe('createIssue', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
post: vi.fn(() => Promise.resolve()),
get: vi.fn(() => Promise.resolve()),
patch: vi.fn(() => Promise.resolve()),
delete: vi.fn(() => Promise.resolve()),
};
beforeEach(() => {
@@ -96,10 +96,10 @@ describe('#linearAPI', () => {
describe('link_issue', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
post: vi.fn(() => Promise.resolve()),
get: vi.fn(() => Promise.resolve()),
patch: vi.fn(() => Promise.resolve()),
delete: vi.fn(() => Promise.resolve()),
};
beforeEach(() => {
@@ -125,10 +125,10 @@ describe('#linearAPI', () => {
describe('getLinkedIssue', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
post: vi.fn(() => Promise.resolve()),
get: vi.fn(() => Promise.resolve()),
patch: vi.fn(() => Promise.resolve()),
delete: vi.fn(() => Promise.resolve()),
};
beforeEach(() => {
@@ -150,10 +150,10 @@ describe('#linearAPI', () => {
describe('unlinkIssue', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
post: vi.fn(() => Promise.resolve()),
get: vi.fn(() => Promise.resolve()),
patch: vi.fn(() => Promise.resolve()),
delete: vi.fn(() => Promise.resolve()),
};
beforeEach(() => {
@@ -178,10 +178,10 @@ describe('#linearAPI', () => {
describe('searchIssues', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
post: vi.fn(() => Promise.resolve()),
get: vi.fn(() => Promise.resolve()),
patch: vi.fn(() => Promise.resolve()),
delete: vi.fn(() => Promise.resolve()),
};
beforeEach(() => {
@@ -13,10 +13,10 @@ describe('#NotificationAPI', () => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
post: vi.fn(() => Promise.resolve()),
get: vi.fn(() => Promise.resolve()),
patch: vi.fn(() => Promise.resolve()),
delete: vi.fn(() => Promise.resolve()),
};
beforeEach(() => {
@@ -20,10 +20,10 @@ describe('#Reports API', () => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
post: vi.fn(() => Promise.resolve()),
get: vi.fn(() => Promise.resolve()),
patch: vi.fn(() => Promise.resolve()),
delete: vi.fn(() => Promise.resolve()),
};
beforeEach(() => {
@@ -12,10 +12,10 @@ describe('#SLAReports API', () => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
post: vi.fn(() => Promise.resolve()),
get: vi.fn(() => Promise.resolve()),
patch: vi.fn(() => Promise.resolve()),
delete: vi.fn(() => Promise.resolve()),
};
beforeEach(() => {
@@ -16,10 +16,10 @@ describe('#TeamsAPI', () => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
post: vi.fn(() => Promise.resolve()),
get: vi.fn(() => Promise.resolve()),
patch: vi.fn(() => Promise.resolve()),
delete: vi.fn(() => Promise.resolve()),
};
beforeEach(() => {
@@ -111,27 +111,19 @@
@updateFolder="onUpdateSavedFilter"
/>
</woot-modal>
<woot-modal
:show.sync="showCustomSnoozeModal"
:on-close="hideCustomSnoozeModal"
>
<custom-snooze-modal
@close="hideCustomSnoozeModal"
@choose-time="chooseSnoozeTime"
/>
</woot-modal>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import { useUISettings } from 'dashboard/composables/useUISettings';
import { useAlert } from 'dashboard/composables';
import VirtualList from 'vue-virtual-scroll-list';
import ChatListHeader from './ChatListHeader.vue';
import ConversationAdvancedFilter from './widgets/conversation/ConversationAdvancedFilter.vue';
import ChatTypeTabs from './widgets/ChatTypeTabs.vue';
import ConversationItem from './ConversationItem.vue';
import timeMixin from '../mixins/time';
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
import conversationMixin from '../mixins/conversations';
import wootConstants from 'dashboard/constants/globals';
@@ -140,9 +132,7 @@ import filterQueryGenerator from '../helper/filterQueryGenerator.js';
import AddCustomViews from 'dashboard/routes/dashboard/customviews/AddCustomViews.vue';
import DeleteCustomViews from 'dashboard/routes/dashboard/customviews/DeleteCustomViews.vue';
import ConversationBulkActions from './widgets/conversation/conversationBulkActions/Index.vue';
import alertMixin from 'shared/mixins/alertMixin';
import filterMixin from 'shared/mixins/filterMixin';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import languages from 'dashboard/components/widgets/conversation/advancedFilterItems/languages';
import countries from 'shared/constants/countries';
import { generateValuesForEditCustomViews } from 'dashboard/helper/customViewsHelper';
@@ -152,10 +142,6 @@ import {
isOnUnattendedView,
} from '../store/modules/conversations/helpers/actionHelpers';
import { CONVERSATION_EVENTS } from '../helper/AnalyticsHelper/events';
import { CMD_SNOOZE_CONVERSATION } from 'dashboard/routes/dashboard/commands/commandBarBusEvents';
import { findSnoozeTime } from 'dashboard/helper/snoozeHelpers';
import { getUnixTime } from 'date-fns';
import CustomSnoozeModal from 'dashboard/components/CustomSnoozeModal.vue';
import IntersectionObserver from './IntersectionObserver.vue';
export default {
@@ -170,16 +156,8 @@ export default {
ConversationBulkActions,
IntersectionObserver,
VirtualList,
CustomSnoozeModal,
},
mixins: [
timeMixin,
conversationMixin,
keyboardEventListenerMixins,
alertMixin,
filterMixin,
uiSettingsMixin,
],
mixins: [conversationMixin, keyboardEventListenerMixins, filterMixin],
provide() {
return {
// Actions to be performed on virtual list item and context menu.
@@ -224,6 +202,13 @@ export default {
type: Boolean,
},
},
setup() {
const { uiSettings } = useUISettings();
return {
uiSettings,
};
},
data() {
return {
activeAssigneeTab: wootConstants.ASSIGNEE_TYPE.ME,
@@ -247,7 +232,6 @@ export default {
root: this.$refs.conversationList,
rootMargin: '100px 0px 100px 0px',
},
showCustomSnoozeModal: false,
itemComponent: ConversationItem,
// virtualListExtraProps is to pass the props to the conversationItem component.
@@ -283,7 +267,6 @@ export default {
campaigns: 'campaigns/getAllCampaigns',
labels: 'labels/getLabels',
selectedConversations: 'bulkActions/getSelectedConversationIds',
contextMenuChatId: 'getContextMenuChatId',
}),
hasAppliedFilters() {
return this.appliedFilters.length !== 0;
@@ -517,11 +500,6 @@ export default {
this.$emitter.on('fetch_conversation_stats', () => {
this.$store.dispatch('conversationStats/get', this.conversationFilters);
});
this.$emitter.on(CMD_SNOOZE_CONVERSATION, this.onCmdSnoozeConversation);
},
beforeDestroy() {
this.$emitter.off(CMD_SNOOZE_CONVERSATION, this.onCmdSnoozeConversation);
},
methods: {
updateVirtualListProps(key, value) {
@@ -833,7 +811,7 @@ export default {
});
this.$store.dispatch('bulkActions/clearSelectedConversationIds');
if (conversationId) {
this.showAlert(
useAlert(
this.$t(
'CONVERSATION.CARD_CONTEXT_MENU.API.AGENT_ASSIGNMENT.SUCCESFUL',
{
@@ -843,10 +821,10 @@ export default {
)
);
} else {
this.showAlert(this.$t('BULK_ACTION.ASSIGN_SUCCESFUL'));
useAlert(this.$t('BULK_ACTION.ASSIGN_SUCCESFUL'));
}
} catch (err) {
this.showAlert(this.$t('BULK_ACTION.ASSIGN_FAILED'));
useAlert(this.$t('BULK_ACTION.ASSIGN_FAILED'));
}
},
async assignPriority(priority, conversationId = null) {
@@ -861,7 +839,7 @@ export default {
newValue: priority,
from: 'Context menu',
});
this.showAlert(
useAlert(
this.$t('CONVERSATION.PRIORITY.CHANGE_PRIORITY.SUCCESSFUL', {
priority,
conversationId,
@@ -904,7 +882,7 @@ export default {
conversationId,
teamId: team.id,
});
this.showAlert(
useAlert(
this.$t(
'CONVERSATION.CARD_CONTEXT_MENU.API.TEAM_ASSIGNMENT.SUCCESFUL',
{
@@ -914,7 +892,7 @@ export default {
)
);
} catch (error) {
this.showAlert(
useAlert(
this.$t('CONVERSATION.CARD_CONTEXT_MENU.API.TEAM_ASSIGNMENT.FAILED')
);
}
@@ -931,7 +909,7 @@ export default {
});
this.$store.dispatch('bulkActions/clearSelectedConversationIds');
if (conversationId) {
this.showAlert(
useAlert(
this.$t(
'CONVERSATION.CARD_CONTEXT_MENU.API.LABEL_ASSIGNMENT.SUCCESFUL',
{
@@ -941,10 +919,10 @@ export default {
)
);
} else {
this.showAlert(this.$t('BULK_ACTION.LABELS.ASSIGN_SUCCESFUL'));
useAlert(this.$t('BULK_ACTION.LABELS.ASSIGN_SUCCESFUL'));
}
} catch (err) {
this.showAlert(this.$t('BULK_ACTION.LABELS.ASSIGN_FAILED'));
useAlert(this.$t('BULK_ACTION.LABELS.ASSIGN_FAILED'));
}
},
async onAssignTeamsForBulk(team) {
@@ -957,9 +935,9 @@ export default {
},
});
this.$store.dispatch('bulkActions/clearSelectedConversationIds');
this.showAlert(this.$t('BULK_ACTION.TEAMS.ASSIGN_SUCCESFUL'));
useAlert(this.$t('BULK_ACTION.TEAMS.ASSIGN_SUCCESFUL'));
} catch (err) {
this.showAlert(this.$t('BULK_ACTION.TEAMS.ASSIGN_FAILED'));
useAlert(this.$t('BULK_ACTION.TEAMS.ASSIGN_FAILED'));
}
},
async onUpdateConversations(status, snoozedUntil) {
@@ -973,9 +951,9 @@ export default {
snoozed_until: snoozedUntil,
});
this.$store.dispatch('bulkActions/clearSelectedConversationIds');
this.showAlert(this.$t('BULK_ACTION.UPDATE.UPDATE_SUCCESFUL'));
useAlert(this.$t('BULK_ACTION.UPDATE.UPDATE_SUCCESFUL'));
} catch (err) {
this.showAlert(this.$t('BULK_ACTION.UPDATE.UPDATE_FAILED'));
useAlert(this.$t('BULK_ACTION.UPDATE.UPDATE_FAILED'));
}
},
toggleConversationStatus(conversationId, status, snoozedUntil) {
@@ -986,7 +964,7 @@ export default {
snoozedUntil,
})
.then(() => {
this.showAlert(this.$t('CONVERSATION.CHANGE_STATUS'));
useAlert(this.$t('CONVERSATION.CHANGE_STATUS'));
this.isLoading = false;
});
},
@@ -999,43 +977,6 @@ export default {
onContextMenuToggle(state) {
this.isContextMenuOpen = state;
},
onCmdSnoozeConversation(snoozeType) {
if (snoozeType === wootConstants.SNOOZE_OPTIONS.UNTIL_CUSTOM_TIME) {
this.showCustomSnoozeModal = true;
} else {
this.toggleStatus(
wootConstants.STATUS_TYPE.SNOOZED,
findSnoozeTime(snoozeType) || null
);
}
},
chooseSnoozeTime(customSnoozeTime) {
this.showCustomSnoozeModal = false;
if (customSnoozeTime) {
this.toggleStatus(
wootConstants.STATUS_TYPE.SNOOZED,
getUnixTime(customSnoozeTime)
);
}
},
toggleStatus(status, snoozedUntil) {
this.$store
.dispatch('toggleStatus', {
conversationId: this.currentChat?.id || this.contextMenuChatId,
status,
snoozedUntil,
})
.then(() => {
this.$store.dispatch('setContextMenuChatId', null);
this.showAlert(this.$t('CONVERSATION.CHANGE_STATUS'));
});
},
hideCustomSnoozeModal() {
// if we select custom snooze and then the custom snooze modal is open
// Then if the custom snooze modal is closed and set the context menu chat id to null
this.$store.dispatch('setContextMenuChatId', null);
this.showCustomSnoozeModal = false;
},
},
};
</script>
+2 -3
View File
@@ -25,10 +25,9 @@
<script>
import 'highlight.js/styles/default.css';
import { copyTextToClipboard } from 'shared/helpers/clipboard';
import alertMixin from 'shared/mixins/alertMixin';
import { useAlert } from 'dashboard/composables';
export default {
mixins: [alertMixin],
props: {
script: {
type: String,
@@ -61,7 +60,7 @@ export default {
async onCopy(e) {
e.preventDefault();
await copyTextToClipboard(this.script);
this.showAlert(this.$t('COMPONENTS.CODE.COPY_SUCCESSFUL'));
useAlert(this.$t('COMPONENTS.CODE.COPY_SUCCESSFUL'));
},
},
};
@@ -18,10 +18,9 @@
<script>
import 'highlight.js/styles/default.css';
import { copyTextToClipboard } from 'shared/helpers/clipboard';
import alertMixin from 'shared/mixins/alertMixin';
import { useAlert } from 'dashboard/composables';
export default {
mixins: [alertMixin],
props: {
value: {
type: String,
@@ -37,7 +36,7 @@ export default {
async onCopy(e) {
e.preventDefault();
await copyTextToClipboard(this.value);
this.showAlert(this.$t('COMPONENTS.CODE.COPY_SUCCESSFUL'));
useAlert(this.$t('COMPONENTS.CODE.COPY_SUCCESSFUL'));
},
toggleMasked() {
this.masked = !this.masked;
@@ -15,13 +15,11 @@
<script>
import WootSnackbar from './Snackbar.vue';
import alertMixin from 'shared/mixins/alertMixin';
export default {
components: {
WootSnackbar,
},
mixins: [alertMixin],
props: {
duration: {
type: Number,
@@ -42,7 +40,7 @@ export default {
this.$emitter.off('newToastMessage', this.onNewToastMessage);
},
methods: {
onNewToastMessage(message, action) {
onNewToastMessage({ message, action }) {
this.snackMessages.push({
key: new Date().getTime(),
message,
@@ -10,9 +10,9 @@
</template>
<script>
import Banner from 'dashboard/components/ui/Banner.vue';
import { mapGetters } from 'vuex';
import adminMixin from 'dashboard/mixins/isAdmin';
import { useAdmin } from 'dashboard/composables/useAdmin';
import Banner from 'dashboard/components/ui/Banner.vue';
import accountMixin from 'dashboard/mixins/account';
const EMPTY_SUBSCRIPTION_INFO = {
@@ -22,7 +22,13 @@ const EMPTY_SUBSCRIPTION_INFO = {
export default {
components: { Banner },
mixins: [adminMixin, accountMixin],
mixins: [accountMixin],
setup() {
const { isAdmin } = useAdmin();
return {
isAdmin,
};
},
computed: {
...mapGetters({
isOnChatwootCloud: 'globalConfig/isOnChatwootCloud',
@@ -14,11 +14,11 @@
import Banner from 'dashboard/components/ui/Banner.vue';
import { mapGetters } from 'vuex';
import accountMixin from 'dashboard/mixins/account';
import alertMixin from 'shared/mixins/alertMixin';
import { useAlert } from 'dashboard/composables';
export default {
components: { Banner },
mixins: [accountMixin, alertMixin],
mixins: [accountMixin],
computed: {
...mapGetters({
currentUser: 'getCurrentUser',
@@ -36,7 +36,7 @@ export default {
methods: {
resendVerificationEmail() {
this.$store.dispatch('resendConfirmation');
this.showAlert(this.$t('APP_GLOBAL.EMAIL_VERIFICATION_SENT'));
useAlert(this.$t('APP_GLOBAL.EMAIL_VERIFICATION_SENT'));
},
},
};
@@ -14,15 +14,20 @@ import Banner from 'dashboard/components/ui/Banner.vue';
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
import { LocalStorage } from 'shared/helpers/localStorage';
import { mapGetters } from 'vuex';
import adminMixin from 'dashboard/mixins/isAdmin';
import { useAdmin } from 'dashboard/composables/useAdmin';
import { hasAnUpdateAvailable } from './versionCheckHelper';
export default {
components: { Banner },
mixins: [adminMixin],
props: {
latestChatwootVersion: { type: String, default: '' },
},
setup() {
const { isAdmin } = useAdmin();
return {
isAdmin,
};
},
data() {
return { userDismissedBanner: false };
},
@@ -12,13 +12,12 @@
<script>
import Banner from 'dashboard/components/ui/Banner.vue';
import { mapGetters } from 'vuex';
import adminMixin from 'dashboard/mixins/isAdmin';
import accountMixin from 'dashboard/mixins/account';
import { differenceInDays } from 'date-fns';
export default {
components: { Banner },
mixins: [adminMixin, accountMixin],
mixins: [accountMixin],
data() {
return { conversationMeta: {} };
},
@@ -78,7 +78,7 @@
<script>
import { mapGetters } from 'vuex';
import alertMixin from 'shared/mixins/alertMixin';
import { useAlert } from 'dashboard/composables';
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
@@ -94,7 +94,7 @@ export default {
WootDropdownItem,
WootDropdownMenu,
},
mixins: [alertMixin, keyboardEventListenerMixins],
mixins: [keyboardEventListenerMixins],
props: { conversationId: { type: [String, Number], required: true } },
data() {
return {
@@ -209,7 +209,7 @@ export default {
snoozedUntil,
})
.then(() => {
this.showAlert(this.$t('CONVERSATION.CHANGE_STATUS'));
useAlert(this.$t('CONVERSATION.CHANGE_STATUS'));
this.isLoading = false;
});
},
@@ -18,7 +18,7 @@
</woot-button>
</woot-dropdown-item>
<woot-dropdown-divider />
<woot-dropdown-item class="m-0 flex items-center justify-between p-2">
<woot-dropdown-item class="flex items-center justify-between p-2 m-0">
<div class="flex items-center">
<fluent-icon
v-tooltip.right-start="$t('SIDEBAR.SET_AUTO_OFFLINE.INFO_TEXT')"
@@ -28,7 +28,7 @@
/>
<span
class="my-0 mx-1 text-xs font-medium text-slate-600 dark:text-slate-100"
class="mx-1 my-0 text-xs font-medium text-slate-600 dark:text-slate-100"
>
{{ $t('SIDEBAR.SET_AUTO_OFFLINE.TEXT') }}
</span>
@@ -36,7 +36,7 @@
<woot-switch
size="small"
class="mt-px mx-1 mb-0"
class="mx-1 mt-px mb-0"
:value="currentUserAutoOffline"
@input="updateAutoOffline"
/>
@@ -47,7 +47,7 @@
<script>
import { mapGetters } from 'vuex';
import alertMixin from 'shared/mixins/alertMixin';
import { useAlert } from 'dashboard/composables';
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
import WootDropdownHeader from 'shared/components/ui/dropdown/DropdownHeader.vue';
@@ -65,9 +65,6 @@ export default {
WootDropdownItem,
AvailabilityStatusBadge,
},
mixins: [alertMixin],
data() {
return {
isStatusMenuOpened: false,
@@ -129,7 +126,7 @@ export default {
account_id: this.currentAccountId,
});
} catch (error) {
this.showAlert(
useAlert(
this.$t('PROFILE_SETTINGS.FORM.AVAILABILITY.SET_AVAILABILITY_ERROR')
);
} finally {
@@ -20,7 +20,7 @@
:teams="teams"
:custom-views="customViews"
:menu-config="activeSecondaryMenu"
:current-role="currentRole"
:current-user="currentUser"
:is-on-chatwoot-cloud="isOnChatwootCloud"
@add-label="showAddLabelPopup"
@toggle-accounts="toggleAccountModal"
@@ -30,21 +30,20 @@
<script>
import { mapGetters } from 'vuex';
import adminMixin from '../../mixins/isAdmin';
import { getSidebarItems } from './config/default-sidebar';
import alertMixin from 'shared/mixins/alertMixin';
import PrimarySidebar from './sidebarComponents/Primary.vue';
import SecondarySidebar from './sidebarComponents/Secondary.vue';
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
import router from '../../routes';
import router, { routesWithPermissions } from '../../routes';
import { hasPermissions } from '../../helper/permissionsHelper';
export default {
components: {
PrimarySidebar,
SecondarySidebar,
},
mixins: [adminMixin, alertMixin, keyboardEventListenerMixins],
mixins: [keyboardEventListenerMixins],
props: {
showSecondarySidebar: {
type: Boolean,
@@ -98,9 +97,13 @@ export default {
return getSidebarItems(this.accountId);
},
primaryMenuItems() {
const userPermissions = this.currentUser.permissions;
const menuItems = this.sideMenuConfig.primaryMenu;
return menuItems.filter(menuItem => {
const isAvailableForTheUser = menuItem.roles.includes(this.currentRole);
const isAvailableForTheUser = hasPermissions(
routesWithPermissions[menuItem.toStateName],
userPermissions
);
if (!isAvailableForTheUser) {
return false;
@@ -9,7 +9,6 @@ const primaryMenuItems = accountId => [
featureFlag: FEATURE_FLAGS.INBOX_VIEW,
toState: frontendURL(`accounts/${accountId}/inbox-view`),
toStateName: 'inbox_view',
roles: ['administrator', 'agent'],
},
{
icon: 'chat',
@@ -17,7 +16,6 @@ const primaryMenuItems = accountId => [
label: 'CONVERSATIONS',
toState: frontendURL(`accounts/${accountId}/dashboard`),
toStateName: 'home',
roles: ['administrator', 'agent'],
},
{
icon: 'book-contacts',
@@ -26,7 +24,6 @@ const primaryMenuItems = accountId => [
featureFlag: FEATURE_FLAGS.CRM,
toState: frontendURL(`accounts/${accountId}/contacts`),
toStateName: 'contacts_dashboard',
roles: ['administrator', 'agent'],
},
{
icon: 'arrow-trending-lines',
@@ -34,8 +31,7 @@ const primaryMenuItems = accountId => [
label: 'REPORTS',
featureFlag: FEATURE_FLAGS.REPORTS,
toState: frontendURL(`accounts/${accountId}/reports`),
toStateName: 'settings_account_reports',
roles: ['administrator'],
toStateName: 'account_overview_reports',
},
{
icon: 'megaphone',
@@ -44,7 +40,6 @@ const primaryMenuItems = accountId => [
featureFlag: FEATURE_FLAGS.CAMPAIGNS,
toState: frontendURL(`accounts/${accountId}/campaigns`),
toStateName: 'ongoing_campaigns',
roles: ['administrator'],
},
{
icon: 'library',
@@ -54,7 +49,6 @@ const primaryMenuItems = accountId => [
alwaysVisibleOnChatwootInstances: true,
toState: frontendURL(`accounts/${accountId}/portals`),
toStateName: 'default_portal_articles',
roles: ['administrator'],
},
{
icon: 'settings',
@@ -62,7 +56,6 @@ const primaryMenuItems = accountId => [
label: 'SETTINGS',
toState: frontendURL(`accounts/${accountId}/settings`),
toStateName: 'settings_home',
roles: ['administrator', 'agent'],
},
];
@@ -24,7 +24,6 @@ const settings = accountId => ({
'settings_inbox_list',
'settings_inbox_new',
'settings_inbox_show',
'settings_inbox',
'settings_inboxes_add_agents',
'settings_inboxes_page_channel',
'settings_integrations_dashboard_apps',
@@ -46,6 +45,9 @@ const settings = accountId => ({
icon: 'briefcase',
label: 'ACCOUNT_SETTINGS',
hasSubMenu: false,
meta: {
permissions: ['administrator'],
},
toState: frontendURL(`accounts/${accountId}/settings/general`),
toStateName: 'general_settings_index',
},
@@ -53,6 +55,9 @@ const settings = accountId => ({
icon: 'people',
label: 'AGENTS',
hasSubMenu: false,
meta: {
permissions: ['administrator'],
},
toState: frontendURL(`accounts/${accountId}/settings/agents/list`),
toStateName: 'agent_list',
featureFlag: FEATURE_FLAGS.AGENT_MANAGEMENT,
@@ -61,6 +66,9 @@ const settings = accountId => ({
icon: 'people-team',
label: 'TEAMS',
hasSubMenu: false,
meta: {
permissions: ['administrator'],
},
toState: frontendURL(`accounts/${accountId}/settings/teams/list`),
toStateName: 'settings_teams_list',
featureFlag: FEATURE_FLAGS.TEAM_MANAGEMENT,
@@ -69,6 +77,9 @@ const settings = accountId => ({
icon: 'mail-inbox-all',
label: 'INBOXES',
hasSubMenu: false,
meta: {
permissions: ['administrator'],
},
toState: frontendURL(`accounts/${accountId}/settings/inboxes/list`),
toStateName: 'settings_inbox_list',
featureFlag: FEATURE_FLAGS.INBOX_MANAGEMENT,
@@ -77,6 +88,9 @@ const settings = accountId => ({
icon: 'tag',
label: 'LABELS',
hasSubMenu: false,
meta: {
permissions: ['administrator'],
},
toState: frontendURL(`accounts/${accountId}/settings/labels/list`),
toStateName: 'labels_list',
featureFlag: FEATURE_FLAGS.LABELS,
@@ -85,6 +99,9 @@ const settings = accountId => ({
icon: 'code',
label: 'CUSTOM_ATTRIBUTES',
hasSubMenu: false,
meta: {
permissions: ['administrator'],
},
toState: frontendURL(
`accounts/${accountId}/settings/custom-attributes/list`
),
@@ -95,6 +112,9 @@ const settings = accountId => ({
icon: 'automation',
label: 'AUTOMATION',
hasSubMenu: false,
meta: {
permissions: ['administrator'],
},
toState: frontendURL(`accounts/${accountId}/settings/automation/list`),
toStateName: 'automation_list',
featureFlag: FEATURE_FLAGS.AUTOMATIONS,
@@ -103,6 +123,9 @@ const settings = accountId => ({
icon: 'bot',
label: 'AGENT_BOTS',
hasSubMenu: false,
meta: {
permissions: ['administrator'],
},
globalConfigFlag: 'csmlEditorHost',
toState: frontendURL(`accounts/${accountId}/settings/agent-bots`),
toStateName: 'agent_bots',
@@ -112,6 +135,9 @@ const settings = accountId => ({
icon: 'flash-settings',
label: 'MACROS',
hasSubMenu: false,
meta: {
permissions: ['administrator', 'agent'],
},
toState: frontendURL(`accounts/${accountId}/settings/macros`),
toStateName: 'macros_wrapper',
featureFlag: FEATURE_FLAGS.MACROS,
@@ -120,6 +146,9 @@ const settings = accountId => ({
icon: 'chat-multiple',
label: 'CANNED_RESPONSES',
hasSubMenu: false,
meta: {
permissions: ['administrator', 'agent'],
},
toState: frontendURL(
`accounts/${accountId}/settings/canned-response/list`
),
@@ -130,6 +159,9 @@ const settings = accountId => ({
icon: 'flash-on',
label: 'INTEGRATIONS',
hasSubMenu: false,
meta: {
permissions: ['administrator'],
},
toState: frontendURL(`accounts/${accountId}/settings/integrations`),
toStateName: 'settings_integrations',
featureFlag: FEATURE_FLAGS.INTEGRATIONS,
@@ -138,6 +170,9 @@ const settings = accountId => ({
icon: 'star-emphasis',
label: 'APPLICATIONS',
hasSubMenu: false,
meta: {
permissions: ['administrator'],
},
toState: frontendURL(`accounts/${accountId}/settings/applications`),
toStateName: 'settings_applications',
featureFlag: FEATURE_FLAGS.INTEGRATIONS,
@@ -146,6 +181,9 @@ const settings = accountId => ({
icon: 'key',
label: 'AUDIT_LOGS',
hasSubMenu: false,
meta: {
permissions: ['administrator'],
},
toState: frontendURL(`accounts/${accountId}/settings/audit-log/list`),
toStateName: 'auditlogs_list',
isEnterpriseOnly: true,
@@ -156,6 +194,9 @@ const settings = accountId => ({
icon: 'document-list-clock',
label: 'SLA',
hasSubMenu: false,
meta: {
permissions: ['administrator'],
},
toState: frontendURL(`accounts/${accountId}/settings/sla/list`),
toStateName: 'sla_list',
isEnterpriseOnly: true,
@@ -166,6 +207,9 @@ const settings = accountId => ({
icon: 'credit-card-person',
label: 'BILLING',
hasSubMenu: false,
meta: {
permissions: ['administrator'],
},
toState: frontendURL(`accounts/${accountId}/settings/billing`),
toStateName: 'billing_settings_index',
showOnlyOnCloud: true,
@@ -51,10 +51,9 @@
<script>
import { required, minLength } from 'vuelidate/lib/validators';
import { mapGetters } from 'vuex';
import alertMixin from 'shared/mixins/alertMixin';
import { useAlert } from 'dashboard/composables';
export default {
mixins: [alertMixin],
props: {
show: {
type: Boolean,
@@ -88,13 +87,13 @@ export default {
account_name: this.accountName,
});
this.$emit('close-account-create-modal');
this.showAlert(this.$t('CREATE_ACCOUNT.API.SUCCESS_MESSAGE'));
useAlert(this.$t('CREATE_ACCOUNT.API.SUCCESS_MESSAGE'));
window.location = `/app/accounts/${account_id}/dashboard`;
} catch (error) {
if (error.response.status === 422) {
this.showAlert(this.$t('CREATE_ACCOUNT.API.EXIST_MESSAGE'));
useAlert(this.$t('CREATE_ACCOUNT.API.EXIST_MESSAGE'));
} else {
this.showAlert(this.$t('CREATE_ACCOUNT.API.ERROR_MESSAGE'));
useAlert(this.$t('CREATE_ACCOUNT.API.ERROR_MESSAGE'));
}
}
},
@@ -29,6 +29,8 @@ import SecondaryNavItem from './SecondaryNavItem.vue';
import AccountContext from './AccountContext.vue';
import { mapGetters } from 'vuex';
import { FEATURE_FLAGS } from '../../../featureFlags';
import { hasPermissions } from '../../../helper/permissionsHelper';
import { routesWithPermissions } from '../../../routes';
export default {
components: {
@@ -60,9 +62,9 @@ export default {
type: Object,
default: () => {},
},
currentRole: {
type: String,
default: '',
currentUser: {
type: Object,
default: () => {},
},
isOnChatwootCloud: {
type: Boolean,
@@ -80,16 +82,16 @@ export default {
return this.customViews.filter(view => view.filter_type === 'contact');
},
accessibleMenuItems() {
if (!this.currentRole) {
return [];
}
const menuItemsFilteredByRole = this.menuConfig.menuItems.filter(
menuItem =>
window.roleWiseRoutes[this.currentRole].indexOf(
menuItem.toStateName
) > -1
const menuItemsFilteredByPermissions = this.menuConfig.menuItems.filter(
menuItem => {
const { permissions: userPermissions = [] } = this.currentUser;
return hasPermissions(
routesWithPermissions[menuItem.toStateName],
userPermissions
);
}
);
return menuItemsFilteredByRole.filter(item => {
return menuItemsFilteredByPermissions.filter(item => {
if (item.showOnlyOnCloud) {
return this.isOnChatwootCloud;
}
@@ -65,35 +65,36 @@
:show-child-count="showChildCount(child.count)"
:child-item-count="child.count"
/>
<router-link
v-if="showItem(menuItem)"
v-slot="{ href, navigate }"
:to="menuItem.toState"
custom
>
<li class="pl-1">
<a :href="href">
<woot-button
size="tiny"
variant="clear"
color-scheme="secondary"
icon="add"
:data-testid="menuItem.dataTestid"
@click="e => newLinkClick(e, navigate)"
>
{{ $t(`SIDEBAR.${menuItem.newLinkTag}`) }}
</woot-button>
</a>
</li>
</router-link>
<Policy :permissions="['administrator']">
<router-link
v-if="menuItem.newLink"
v-slot="{ href, navigate }"
:to="menuItem.toState"
custom
>
<li class="pl-1">
<a :href="href">
<woot-button
size="tiny"
variant="clear"
color-scheme="secondary"
icon="add"
:data-testid="menuItem.dataTestid"
@click="e => newLinkClick(e, navigate)"
>
{{ $t(`SIDEBAR.${menuItem.newLinkTag}`) }}
</woot-button>
</a>
</li>
</router-link>
</Policy>
</ul>
</li>
</template>
<script>
import { mapGetters } from 'vuex';
import adminMixin from '../../../mixins/isAdmin';
import { useAdmin } from 'dashboard/composables/useAdmin';
import configMixin from 'shared/mixins/configMixin';
import {
getInboxClassByType,
@@ -105,16 +106,23 @@ import {
isOnMentionsView,
isOnUnattendedView,
} from '../../../store/modules/conversations/helpers/actionHelpers';
import Policy from '../../policy.vue';
export default {
components: { SecondaryChildNavItem },
mixins: [adminMixin, configMixin],
components: { SecondaryChildNavItem, Policy },
mixins: [configMixin],
props: {
menuItem: {
type: Object,
default: () => ({}),
},
},
setup() {
const { isAdmin } = useAdmin();
return {
isAdmin,
};
},
computed: {
...mapGetters({
activeInbox: 'getSelectedInbox',
@@ -1,12 +1,12 @@
import AccountSelector from '../AccountSelector';
import AccountSelector from '../AccountSelector.vue';
import { createLocalVue, mount } from '@vue/test-utils';
import Vuex from 'vuex';
import VueI18n from 'vue-i18n';
import i18n from 'dashboard/i18n';
import WootModal from 'dashboard/components/Modal';
import WootModalHeader from 'dashboard/components/ModalHeader';
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon';
import WootModal from 'dashboard/components/Modal.vue';
import WootModalHeader from 'dashboard/components/ModalHeader.vue';
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue';
const localVue = createLocalVue();
localVue.component('woot-modal', WootModal);
@@ -1,12 +1,12 @@
import AgentDetails from '../AgentDetails';
import AgentDetails from '../AgentDetails.vue';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
import VueI18n from 'vue-i18n';
import VTooltip from 'v-tooltip';
import i18n from 'dashboard/i18n';
import Thumbnail from 'dashboard/components/widgets/Thumbnail';
import WootButton from 'dashboard/components/ui/WootButton';
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
import WootButton from 'dashboard/components/ui/WootButton.vue';
const localVue = createLocalVue();
localVue.use(Vuex);
localVue.use(VueI18n);
@@ -1,13 +1,15 @@
import NotificationBell from '../NotificationBell';
import NotificationBell from '../NotificationBell.vue';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
import VueI18n from 'vue-i18n';
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue';
import i18n from 'dashboard/i18n';
const localVue = createLocalVue();
localVue.use(Vuex);
localVue.use(VueI18n);
localVue.component('fluent-icon', FluentIcon);
const i18nConfig = new VueI18n({
locale: 'en',
@@ -27,7 +29,7 @@ describe('notificationBell', () => {
beforeEach(() => {
actions = {
showNotification: jest.fn(),
showNotification: vi.fn(),
};
modules = {
auth: {
@@ -2,15 +2,21 @@ import AvailabilityStatus from '../AvailabilityStatus.vue';
import { createLocalVue, mount } from '@vue/test-utils';
import Vuex from 'vuex';
import VueI18n from 'vue-i18n';
import VTooltip from 'v-tooltip';
import WootButton from 'dashboard/components/ui/WootButton.vue';
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
import WootDropdownHeader from 'shared/components/ui/dropdown/DropdownHeader.vue';
import WootDropdownDivider from 'shared/components/ui/dropdown/DropdownDivider.vue';
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue';
import WootButton from 'dashboard/components/ui/WootButton';
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem';
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu';
import WootDropdownHeader from 'shared/components/ui/dropdown/DropdownHeader';
import WootDropdownDivider from 'shared/components/ui/dropdown/DropdownDivider';
import i18n from 'dashboard/i18n';
const localVue = createLocalVue();
localVue.use(VTooltip, {
defaultHtml: false,
});
localVue.use(Vuex);
localVue.use(VueI18n);
localVue.component('woot-button', WootButton);
@@ -18,12 +24,14 @@ localVue.component('woot-dropdown-header', WootDropdownHeader);
localVue.component('woot-dropdown-menu', WootDropdownMenu);
localVue.component('woot-dropdown-divider', WootDropdownDivider);
localVue.component('woot-dropdown-item', WootDropdownItem);
localVue.component('fluent-icon', FluentIcon);
const i18nConfig = new VueI18n({ locale: 'en', messages: i18n });
describe('AvailabilityStatus', () => {
const currentAvailability = 'online';
const currentAccountId = '1';
const currentUserAutoOffline = false;
let store = null;
let actions = null;
let modules = null;
@@ -31,7 +39,7 @@ describe('AvailabilityStatus', () => {
beforeEach(() => {
actions = {
updateAvailability: jest.fn(() => {
updateAvailability: vi.fn(() => {
return Promise.resolve();
}),
};
@@ -41,6 +49,7 @@ describe('AvailabilityStatus', () => {
getters: {
getCurrentUserAvailability: () => currentAvailability,
getCurrentAccountId: () => currentAccountId,
getCurrentUserAutoOffline: () => currentUserAutoOffline,
},
},
};
@@ -0,0 +1,23 @@
<script setup>
import { useStoreGetters } from 'dashboard/composables/store';
import { computed } from 'vue';
import { hasPermissions } from '../helper/permissionsHelper';
const props = defineProps({
permissions: {
type: Array,
required: true,
},
});
const getters = useStoreGetters();
const user = getters.getCurrentUser.value;
const hasPermission = computed(() =>
hasPermissions(props.permissions, user.permissions)
);
</script>
<template>
<div v-if="hasPermission">
<slot />
</div>
</template>
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
import SidemenuIcon from '../SidemenuIcon';
import SidemenuIcon from '../SidemenuIcon.vue';
describe('SidemenuIcon', () => {
test('matches snapshot', () => {
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`SidemenuIcon matches snapshot 1`] = `
exports[`SidemenuIcon > matches snapshot 1`] = `
<button
class="-ml-3 text-black-900 dark:text-slate-300"
color-scheme="secondary"
@@ -18,11 +18,11 @@ describe('Date formatting functions', () => {
const testDate = new Date(2020, 4, 15); // May 15, 2020
beforeEach(() => {
jest.spyOn(navigator, 'language', 'get').mockReturnValue('en-US');
vi.spyOn(navigator, 'language', 'get').mockReturnValue('en-US');
});
afterEach(() => {
jest.restoreAllMocks();
vi.restoreAllMocks();
});
it('returns the correct month name from a date', () => {
@@ -39,7 +39,7 @@ describe('Date formatting functions', () => {
});
it('returns the correct date format for the current locale en-IN', () => {
jest.spyOn(navigator, 'language', 'get').mockReturnValue('en-IN');
vi.spyOn(navigator, 'language', 'get').mockReturnValue('en-IN');
const expected = 'dd/MM/yyyy';
expect(getIntlDateFormatForLocale()).toBe(expected);
});
@@ -231,13 +231,13 @@ describe('isHoveringNextDayInRange', () => {
describe('getActiveDateRange', () => {
const currentDate = new Date(2020, 5, 15, 12, 0); // May 15, 2020, at noon
beforeAll(() => {
beforeEach(() => {
// Mocking the current date to ensure consistency in tests
jest.useFakeTimers().setSystemTime(currentDate.getTime());
vi.useFakeTimers().setSystemTime(currentDate.getTime());
});
afterAll(() => {
jest.useRealTimers();
afterEach(() => {
vi.useRealTimers();
});
it('returns the correct range for "last7days"', () => {
@@ -5,7 +5,7 @@
delay: { show: 1500, hide: 0 },
hideOnClick: true,
}"
class="text-xxs text-slate-500 dark:text-slate-500 leading-4 ml-auto hover:text-slate-900 dark:hover:text-slate-100"
class="ml-auto leading-4 text-xxs text-slate-500 dark:text-slate-500 hover:text-slate-900 dark:hover:text-slate-100"
>
<span>{{ `${createdAtTime}${lastActivityTime}` }}</span>
</div>
@@ -16,11 +16,14 @@ const MINUTE_IN_MILLI_SECONDS = 60000;
const HOUR_IN_MILLI_SECONDS = MINUTE_IN_MILLI_SECONDS * 60;
const DAY_IN_MILLI_SECONDS = HOUR_IN_MILLI_SECONDS * 24;
import timeMixin from 'dashboard/mixins/time';
import {
dynamicTime,
dateFormat,
shortTimestamp,
} from 'shared/helpers/timeHelper';
export default {
name: 'TimeAgo',
mixins: [timeMixin],
props: {
isAutoRefreshEnabled: {
type: Boolean,
@@ -37,17 +40,17 @@ export default {
},
data() {
return {
lastActivityAtTimeAgo: this.dynamicTime(this.lastActivityTimestamp),
createdAtTimeAgo: this.dynamicTime(this.createdAtTimestamp),
lastActivityAtTimeAgo: dynamicTime(this.lastActivityTimestamp),
createdAtTimeAgo: dynamicTime(this.createdAtTimestamp),
timer: null,
};
},
computed: {
lastActivityTime() {
return this.shortTimestamp(this.lastActivityAtTimeAgo);
return shortTimestamp(this.lastActivityAtTimeAgo);
},
createdAtTime() {
return this.shortTimestamp(this.createdAtTimeAgo);
return shortTimestamp(this.createdAtTimeAgo);
},
createdAt() {
const createdTimeDiff = Date.now() - this.createdAtTimestamp * 1000;
@@ -56,9 +59,9 @@ export default {
? `${this.$t('CHAT_LIST.CHAT_TIME_STAMP.CREATED.LATEST')} ${
this.createdAtTimeAgo
}`
: `${this.$t(
'CHAT_LIST.CHAT_TIME_STAMP.CREATED.OLDEST'
)} ${this.dateFormat(this.createdAtTimestamp)}`;
: `${this.$t('CHAT_LIST.CHAT_TIME_STAMP.CREATED.OLDEST')} ${dateFormat(
this.createdAtTimestamp
)}`;
},
lastActivity() {
const lastActivityTimeDiff =
@@ -70,7 +73,7 @@ export default {
}`
: `${this.$t(
'CHAT_LIST.CHAT_TIME_STAMP.LAST_ACTIVITY.NOT_ACTIVE'
)} ${this.dateFormat(this.lastActivityTimestamp)}`;
)} ${dateFormat(this.lastActivityTimestamp)}`;
},
tooltipText() {
return `${this.createdAt}
@@ -79,10 +82,10 @@ export default {
},
watch: {
lastActivityTimestamp() {
this.lastActivityAtTimeAgo = this.dynamicTime(this.lastActivityTimestamp);
this.lastActivityAtTimeAgo = dynamicTime(this.lastActivityTimestamp);
},
createdAtTimestamp() {
this.createdAtTimeAgo = this.dynamicTime(this.createdAtTimestamp);
this.createdAtTimeAgo = dynamicTime(this.createdAtTimestamp);
},
},
mounted() {
@@ -96,10 +99,8 @@ export default {
methods: {
createTimer() {
this.timer = setTimeout(() => {
this.lastActivityAtTimeAgo = this.dynamicTime(
this.lastActivityTimestamp
);
this.createdAtTimeAgo = this.dynamicTime(this.createdAtTimestamp);
this.lastActivityAtTimeAgo = dynamicTime(this.lastActivityTimestamp);
this.createdAtTimeAgo = dynamicTime(this.createdAtTimestamp);
this.createTimer();
}, this.refreshTime());
},
@@ -35,13 +35,13 @@
</template>
<script>
import { mapGetters } from 'vuex';
import { useAdmin } from 'dashboard/composables/useAdmin';
import { useUISettings } from 'dashboard/composables/useUISettings';
import AICTAModal from './AICTAModal.vue';
import AIAssistanceModal from './AIAssistanceModal.vue';
import adminMixin from 'dashboard/mixins/aiMixin';
import aiMixin from 'dashboard/mixins/isAdmin';
import aiMixin from 'dashboard/mixins/aiMixin';
import { CMD_AI_ASSIST } from 'dashboard/routes/dashboard/commands/commandBarBusEvents';
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import AIAssistanceCTAButton from './AIAssistanceCTAButton.vue';
export default {
@@ -50,7 +50,17 @@ export default {
AICTAModal,
AIAssistanceCTAButton,
},
mixins: [aiMixin, keyboardEventListenerMixins, adminMixin, uiSettingsMixin],
mixins: [aiMixin, keyboardEventListenerMixins],
setup() {
const { uiSettings, updateUISettings } = useUISettings();
const { isAdmin } = useAdmin();
return {
uiSettings,
updateUISettings,
isAdmin,
};
},
data: () => ({
showAIAssistanceModal: false,
showAICtaModal: false,
@@ -1,14 +1,14 @@
<template>
<div class="px-0 min-w-0 flex-1">
<div class="flex-1 min-w-0 px-0">
<woot-modal-header
:header-title="$t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.TITLE')"
:header-content="$t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.DESC')"
/>
<form
class="flex flex-wrap flex-col modal-content"
class="flex flex-col flex-wrap modal-content"
@submit.prevent="finishOpenAI"
>
<div class="mt-2 w-full">
<div class="w-full mt-2">
<woot-input
v-model="value"
type="text"
@@ -19,7 +19,7 @@
@blur="$v.value.$touch"
/>
</div>
<div class="flex flex-row justify-between gap-2 py-2 px-0 w-full">
<div class="flex flex-row justify-between w-full gap-2 px-0 py-2">
<woot-button variant="link" @click.prevent="openOpenAIDoc">
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.BUTTONS.NEED_HELP') }}
</woot-button>
@@ -39,13 +39,20 @@
<script>
import { required } from 'vuelidate/lib/validators';
import { mapGetters } from 'vuex';
import { useAlert } from 'dashboard/composables';
import { useUISettings } from 'dashboard/composables/useUISettings';
import aiMixin from 'dashboard/mixins/aiMixin';
import alertMixin from 'shared/mixins/alertMixin';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import { OPEN_AI_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
export default {
mixins: [aiMixin, alertMixin, uiSettingsMixin],
mixins: [aiMixin],
setup() {
const { updateUISettings } = useUISettings();
return {
updateUISettings,
};
},
data() {
return {
value: '',
@@ -67,7 +74,7 @@ export default {
},
onDismiss() {
this.showAlert(
useAlert(
this.$t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.DISMISS_MESSAGE')
);
this.updateUISettings({
@@ -97,7 +104,7 @@ export default {
this.alertMessage =
errorMessage || this.$t('INTEGRATION_APPS.ADD.API.ERROR_MESSAGE');
} finally {
this.showAlert(this.alertMessage);
useAlert(this.alertMessage);
}
},
openOpenAIDoc() {
@@ -26,13 +26,12 @@
</template>
<script>
import { useAlert } from 'dashboard/composables';
import Spinner from 'shared/components/Spinner.vue';
import alertMixin from 'shared/mixins/alertMixin';
export default {
components: {
Spinner,
},
mixins: [alertMixin],
props: {
value: {
type: Array,
@@ -71,7 +70,7 @@ export default {
} catch (error) {
this.uploadState = 'failed';
this.label = this.$t('AUTOMATION.ATTACHMENT.LABEL_UPLOAD_FAILED');
this.showAlert(this.$t('AUTOMATION.ATTACHMENT.UPLOAD_ERROR'));
useAlert(this.$t('AUTOMATION.ATTACHMENT.UPLOAD_ERROR'));
}
},
},
@@ -33,7 +33,7 @@
import AddLabel from 'shared/components/ui/dropdown/AddLabel.vue';
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
import LabelDropdown from 'shared/components/ui/label/LabelDropdown.vue';
import adminMixin from 'dashboard/mixins/isAdmin';
import { useAdmin } from 'dashboard/composables/useAdmin';
export default {
components: {
@@ -41,7 +41,7 @@ export default {
LabelDropdown,
},
mixins: [adminMixin, keyboardEventListenerMixins],
mixins: [keyboardEventListenerMixins],
props: {
allLabels: {
@@ -54,6 +54,13 @@ export default {
},
},
setup() {
const { isAdmin } = useAdmin();
return {
isAdmin,
};
},
data() {
return {
showSearchDropdownLabel: false,
@@ -15,10 +15,9 @@
<script>
import { mapGetters } from 'vuex';
import DyteAPI from 'dashboard/api/integrations/dyte';
import alertMixin from 'shared/mixins/alertMixin';
import { useAlert } from 'dashboard/composables';
export default {
mixins: [alertMixin],
props: {
conversationId: {
type: Number,
@@ -47,7 +46,7 @@ export default {
try {
await DyteAPI.createAMeeting(this.conversationId);
} catch (error) {
this.showAlert(this.$t('INTEGRATION_SETTINGS.DYTE.CREATE_ERROR'));
useAlert(this.$t('INTEGRATION_SETTINGS.DYTE.CREATE_ERROR'));
} finally {
this.isLoading = false;
}
@@ -11,7 +11,7 @@ import 'videojs-record/dist/css/videojs.record.css';
import videojs from 'video.js';
import alertMixin from '../../../../shared/mixins/alertMixin';
import { useAlert } from 'dashboard/composables';
import Recorder from 'opus-recorder';
@@ -50,7 +50,6 @@ const RECORDER_CONFIG = {
export default {
name: 'WootAudioRecorder',
mixins: [alertMixin],
props: {
audioRecordFormat: {
type: String,
@@ -188,14 +187,10 @@ export default {
deviceErrorName?.includes('notallowederror') ||
deviceErrorName?.includes('permissiondeniederror')
) {
this.showAlert(
this.$t('CONVERSATION.REPLYBOX.TIP_AUDIORECORDER_PERMISSION')
);
useAlert(this.$t('CONVERSATION.REPLYBOX.TIP_AUDIORECORDER_PERMISSION'));
this.fireStateRecorderChanged('notallowederror');
} else {
this.showAlert(
this.$t('CONVERSATION.REPLYBOX.TIP_AUDIORECORDER_ERROR')
);
useAlert(this.$t('CONVERSATION.REPLYBOX.TIP_AUDIORECORDER_ERROR'));
}
},
formatTimeProgress() {
@@ -80,8 +80,7 @@ import {
hasPressedCommandAndEnter,
} from 'shared/helpers/KeyboardHelpers';
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import { isEditorHotKeyEnabled } from 'dashboard/mixins/uiSettings';
import { useUISettings } from 'dashboard/composables/useUISettings';
import {
replaceVariablesInMessage,
createTypingIndicator,
@@ -89,7 +88,7 @@ import {
import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
import { uploadFile } from 'dashboard/helper/uploadHelper';
import alertMixin from 'shared/mixins/alertMixin';
import { useAlert } from 'dashboard/composables';
import {
MESSAGE_EDITOR_MENU_OPTIONS,
MESSAGE_EDITOR_IMAGE_RESIZES,
@@ -119,7 +118,7 @@ const createState = (
export default {
name: 'WootMessageEditor',
components: { TagAgents, CannedResponse, VariableList },
mixins: [keyboardEventListenerMixins, uiSettingsMixin, alertMixin],
mixins: [keyboardEventListenerMixins],
props: {
value: { type: String, default: '' },
editorId: { type: String, default: '' },
@@ -139,6 +138,19 @@ export default {
channelType: { type: String, default: '' },
showImageResizeToolbar: { type: Boolean, default: false }, // A kill switch to show or hide the image toolbar
},
setup() {
const {
uiSettings,
isEditorHotKeyEnabled,
fetchSignatureFlagFromUISettings,
} = useUISettings();
return {
uiSettings,
isEditorHotKeyEnabled,
fetchSignatureFlagFromUISettings,
};
},
data() {
return {
typingIndicator: createTypingIndicator(
@@ -278,7 +290,7 @@ export default {
// this is considered the source of truth, we watch this property
// on change, we toggle the signature in the editor
if (this.allowSignature && !this.isPrivate && this.channelType) {
return this.fetchSignatureFlagFromUiSettings(this.channelType);
return this.fetchSignatureFlagFromUISettings(this.channelType);
}
return false;
@@ -521,10 +533,10 @@ export default {
}
},
isEnterToSendEnabled() {
return isEditorHotKeyEnabled(this.uiSettings, 'enter');
return this.isEditorHotKeyEnabled('enter');
},
isCmdPlusEnterToSendEnabled() {
return isEditorHotKeyEnabled(this.uiSettings, 'cmd_enter');
return this.isEditorHotKeyEnabled('cmd_enter');
},
getKeyboardEvents() {
return {
@@ -611,7 +623,7 @@ export default {
if (checkFileSizeLimit(file, MAXIMUM_FILE_UPLOAD_SIZE)) {
this.uploadImageToStorage(file);
} else {
this.showAlert(
useAlert(
this.$t(
'PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.IMAGE_UPLOAD_SIZE_ERROR',
{
@@ -629,13 +641,13 @@ export default {
if (fileUrl) {
this.onImageInsertInEditor(fileUrl);
}
this.showAlert(
useAlert(
this.$t(
'PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.IMAGE_UPLOAD_SUCCESS'
)
);
} catch (error) {
this.showAlert(
useAlert(
this.$t(
'PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.IMAGE_UPLOAD_ERROR'
)
@@ -24,9 +24,9 @@ import {
Selection,
} from '@chatwoot/prosemirror-schema';
import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
import alertMixin from 'shared/mixins/alertMixin';
import { useAlert } from 'dashboard/composables';
import { useUISettings } from 'dashboard/composables/useUISettings';
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
const MAXIMUM_FILE_UPLOAD_SIZE = 4; // in MB
const createState = (
@@ -51,13 +51,21 @@ const createState = (
};
export default {
mixins: [keyboardEventListenerMixins, uiSettingsMixin, alertMixin],
mixins: [keyboardEventListenerMixins],
props: {
value: { type: String, default: '' },
editorId: { type: String, default: '' },
placeholder: { type: String, default: '' },
enabledMenuOptions: { type: Array, default: () => [] },
},
setup() {
const { uiSettings, updateUISettings } = useUISettings();
return {
uiSettings,
updateUISettings,
};
},
data() {
return {
editorView: null,
@@ -108,7 +116,7 @@ export default {
if (checkFileSizeLimit(file, MAXIMUM_FILE_UPLOAD_SIZE)) {
this.uploadImageToStorage(file);
} else {
this.showAlert(
useAlert(
this.$t('HELP_CENTER.ARTICLE_EDITOR.IMAGE_UPLOAD.ERROR_FILE_SIZE', {
size: MAXIMUM_FILE_UPLOAD_SIZE,
})
@@ -127,13 +135,9 @@ export default {
if (fileUrl) {
this.onImageUploadStart(fileUrl);
}
this.showAlert(
this.$t('HELP_CENTER.ARTICLE_EDITOR.IMAGE_UPLOAD.SUCCESS')
);
useAlert(this.$t('HELP_CENTER.ARTICLE_EDITOR.IMAGE_UPLOAD.SUCCESS'));
} catch (error) {
this.showAlert(
this.$t('HELP_CENTER.ARTICLE_EDITOR.IMAGE_UPLOAD.ERROR')
);
useAlert(this.$t('HELP_CENTER.ARTICLE_EDITOR.IMAGE_UPLOAD.ERROR'));
}
},
onImageUploadStart(fileUrl) {
@@ -134,10 +134,10 @@
</template>
<script>
import { useUISettings } from 'dashboard/composables/useUISettings';
import FileUpload from 'vue-upload-component';
import * as ActiveStorage from 'activestorage';
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import inboxMixin from 'shared/mixins/inboxMixin';
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
import {
@@ -153,7 +153,7 @@ import { mapGetters } from 'vuex';
export default {
name: 'ReplyBottomPanel',
components: { FileUpload, VideoCallButton, AIAssistanceButton },
mixins: [keyboardEventListenerMixins, uiSettingsMixin, inboxMixin],
mixins: [keyboardEventListenerMixins, inboxMixin],
props: {
mode: {
type: String,
@@ -248,6 +248,15 @@ export default {
required: true,
},
},
setup() {
const { setSignatureFlagForInbox, fetchSignatureFlagFromUISettings } =
useUISettings();
return {
setSignatureFlagForInbox,
fetchSignatureFlagFromUISettings,
};
},
computed: {
...mapGetters({
accountId: 'getCurrentAccountId',
@@ -320,7 +329,7 @@ export default {
},
sendWithSignature() {
// channelType is sourced from inboxMixin
return this.fetchSignatureFlagFromUiSettings(this.channelType);
return this.fetchSignatureFlagFromUISettings(this.channelType);
},
signatureToggleTooltip() {
return this.sendWithSignature
@@ -79,7 +79,7 @@
</template>
<script>
import alertMixin from 'shared/mixins/alertMixin';
import { useAlert } from 'dashboard/composables';
import { required, requiredIf } from 'vuelidate/lib/validators';
import FilterInputBox from '../FilterInput/Index.vue';
import languages from './advancedFilterItems/languages';
@@ -89,12 +89,13 @@ import { filterAttributeGroups } from './advancedFilterItems';
import filterMixin from 'shared/mixins/filterMixin';
import * as OPERATORS from 'dashboard/components/widgets/FilterInput/FilterOperatorTypes.js';
import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
import cloneObject from 'dashboard/helpers/clone';
export default {
components: {
FilterInputBox,
},
mixins: [alertMixin, filterMixin],
mixins: [filterMixin],
props: {
onClose: {
type: Function,
@@ -341,7 +342,7 @@ export default {
},
removeFilter(index) {
if (this.appliedFilters.length <= 1) {
this.showAlert(this.$t('FILTER.FILTER_DELETE_ERROR'));
useAlert(this.$t('FILTER.FILTER_DELETE_ERROR'));
} else {
this.appliedFilters.splice(index, 1);
}
@@ -351,7 +352,7 @@ export default {
if (this.$v.$invalid) return;
this.$store.dispatch(
'setConversationFilters',
JSON.parse(JSON.stringify(this.appliedFilters))
cloneObject(this.appliedFilters)
);
this.$emit('applyFilter', this.appliedFilters);
this.$track(CONVERSATION_EVENTS.APPLY_FILTER, {
@@ -12,10 +12,10 @@
<div
v-if="showActionsDropdown"
v-on-clickaway="closeDropdown"
class="dropdown-pane dropdown-pane--open mt-1 right-0 basic-filter"
class="right-0 mt-1 dropdown-pane dropdown-pane--open basic-filter"
>
<div class="items-center flex justify-between last:mt-4">
<span class="text-slate-800 dark:text-slate-100 text-xs font-medium">{{
<div class="flex items-center justify-between last:mt-4">
<span class="text-xs font-medium text-slate-800 dark:text-slate-100">{{
$t('CHAT_LIST.CHAT_SORT.STATUS')
}}</span>
<filter-item
@@ -26,8 +26,8 @@
@onChangeFilter="onChangeFilter"
/>
</div>
<div class="items-center flex justify-between last:mt-4">
<span class="text-slate-800 dark:text-slate-100 text-xs font-medium">{{
<div class="flex items-center justify-between last:mt-4">
<span class="text-xs font-medium text-slate-800 dark:text-slate-100">{{
$t('CHAT_LIST.CHAT_SORT.ORDER_BY')
}}</span>
<filter-item
@@ -46,13 +46,19 @@
import wootConstants from 'dashboard/constants/globals';
import { mapGetters } from 'vuex';
import FilterItem from './FilterItem.vue';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import { useUISettings } from 'dashboard/composables/useUISettings';
export default {
components: {
FilterItem,
},
mixins: [uiSettingsMixin],
setup() {
const { updateUISettings } = useUISettings();
return {
updateUISettings,
};
},
data() {
return {
showActionsDropdown: false,
@@ -120,13 +120,11 @@ import { mapGetters } from 'vuex';
import Thumbnail from '../Thumbnail.vue';
import MessagePreview from './MessagePreview.vue';
import conversationMixin from '../../../mixins/conversations';
import timeMixin from '../../../mixins/time';
import router from '../../../routes';
import { frontendURL, conversationUrl } from '../../../helper/URLHelper';
import InboxName from '../InboxName.vue';
import inboxMixin from 'shared/mixins/inboxMixin';
import ConversationContextMenu from './contextMenu/Index.vue';
import alertMixin from 'shared/mixins/alertMixin';
import TimeAgo from 'dashboard/components/ui/TimeAgo.vue';
import CardLabels from './conversationCardComponents/CardLabels.vue';
import PriorityMark from './PriorityMark.vue';
@@ -143,8 +141,7 @@ export default {
PriorityMark,
SLACardLabel,
},
mixins: [inboxMixin, timeMixin, conversationMixin, alertMixin],
mixins: [inboxMixin, conversationMixin],
props: {
activeLabel: {
type: String,
@@ -1,7 +1,7 @@
<!-- eslint-disable vue/no-mutating-props -->
<template>
<woot-modal :show.sync="show" :on-close="onCancel">
<div class="h-auto overflow-auto flex flex-col">
<div class="flex flex-col h-auto overflow-auto">
<woot-modal-header
:header-title="$t('EMAIL_TRANSCRIPT.TITLE')"
:header-content="$t('EMAIL_TRANSCRIPT.DESC')"
@@ -61,7 +61,7 @@
</label>
</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">
<woot-submit-button
:button-text="$t('EMAIL_TRANSCRIPT.SUBMIT')"
:disabled="!isFormValid"
@@ -77,9 +77,8 @@
<script>
import { required, minLength, email } from 'vuelidate/lib/validators';
import alertMixin from 'shared/mixins/alertMixin';
import { useAlert } from 'dashboard/composables';
export default {
mixins: [alertMixin],
props: {
show: {
type: Boolean,
@@ -142,10 +141,10 @@ export default {
email: this.selectedEmailAddress,
conversationId: this.currentChat.id,
});
this.showAlert(this.$t('EMAIL_TRANSCRIPT.SEND_EMAIL_SUCCESS'));
useAlert(this.$t('EMAIL_TRANSCRIPT.SEND_EMAIL_SUCCESS'));
this.onCancel();
} catch (error) {
this.showAlert(this.$t('EMAIL_TRANSCRIPT.SEND_EMAIL_ERROR'));
useAlert(this.$t('EMAIL_TRANSCRIPT.SEND_EMAIL_ERROR'));
} finally {
this.isSubmitting = false;
}
@@ -35,7 +35,7 @@
</template>
<script>
import { mapGetters } from 'vuex';
import adminMixin from 'dashboard/mixins/isAdmin';
import { useAdmin } from 'dashboard/composables/useAdmin';
import accountMixin from 'dashboard/mixins/account';
import OnboardingView from '../OnboardingView.vue';
import EmptyStateMessage from './EmptyStateMessage.vue';
@@ -45,13 +45,19 @@ export default {
OnboardingView,
EmptyStateMessage,
},
mixins: [accountMixin, adminMixin],
mixins: [accountMixin],
props: {
isOnExpandedLayout: {
type: Boolean,
default: false,
},
},
setup() {
const { isAdmin } = useAdmin();
return {
isAdmin,
};
},
computed: {
...mapGetters({
currentChat: 'getSelectedChat',
@@ -127,7 +127,7 @@
</div>
<div
v-if="shouldShowContextMenu"
class="context-menu-wrap invisible group-hover:visible"
class="invisible context-menu-wrap group-hover:visible"
>
<context-menu
v-if="isBubble && !isMessageDeleted"
@@ -157,8 +157,7 @@ import ContextMenu from 'dashboard/modules/conversations/components/MessageConte
import InstagramStory from './bubble/InstagramStory.vue';
import InstagramStoryReply from './bubble/InstagramStoryReply.vue';
import Spinner from 'shared/components/Spinner.vue';
import alertMixin from 'shared/mixins/alertMixin';
import contentTypeMixin from 'shared/mixins/contentTypeMixin';
import { CONTENT_TYPES } from 'shared/constants/contentType';
import { MESSAGE_TYPE, MESSAGE_STATUS } from 'shared/constants/messages';
import { generateBotMessageContent } from './helpers/botMessageContentHelper';
import { BUS_EVENTS } from 'shared/constants/busEvents';
@@ -166,6 +165,7 @@ import { ACCOUNT_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
import { LocalStorage } from 'shared/helpers/localStorage';
import { getDayDifferenceFromNow } from 'shared/helpers/DateHelper';
import * as Sentry from '@sentry/browser';
export default {
components: {
@@ -183,7 +183,7 @@ export default {
InstagramStoryReply,
Spinner,
},
mixins: [alertMixin, messageFormatterMixin, contentTypeMixin],
mixins: [messageFormatterMixin],
props: {
data: {
type: Object,
@@ -481,6 +481,9 @@ export default {
}
return '';
},
isEmailContentType() {
return this.contentType === CONTENT_TYPES.INCOMING_EMAIL;
},
},
watch: {
data() {
@@ -502,15 +505,29 @@ export default {
},
hasMediaAttachment(type) {
if (this.hasAttachments && this.data.attachments.length > 0) {
const { attachments = [{}] } = this.data;
const { file_type: fileType } = attachments[0];
return fileType === type && !this.hasMediaLoadError;
return this.compareMessageFileType(this.data, type);
}
if (this.storyReply) {
return true;
}
return false;
},
compareMessageFileType(messageData, type) {
try {
const { attachments = [{}] } = messageData;
const { file_type: fileType } = attachments[0];
return fileType === type && !this.hasMediaLoadError;
} catch (err) {
Sentry.setContext('attachment-parsing-error', {
messageData,
type,
hasMediaLoadError: this.hasMediaLoadError,
});
Sentry.captureException(err);
return false;
}
},
handleContextMenuClick() {
this.showContextMenu = !this.showContextMenu;
},
@@ -1,5 +1,5 @@
<template>
<div class="flex actions--container relative items-center gap-2">
<div class="relative flex items-center gap-2 actions--container">
<woot-button
v-if="!currentChat.muted"
v-tooltip="$t('CONTACT_PANEL.MUTE_CONTACT')"
@@ -37,7 +37,7 @@
</template>
<script>
import { mapGetters } from 'vuex';
import alertMixin from 'shared/mixins/alertMixin';
import { useAlert } from 'dashboard/composables';
import EmailTranscriptModal from './EmailTranscriptModal.vue';
import ResolveAction from '../../buttons/ResolveAction.vue';
import {
@@ -51,7 +51,6 @@ export default {
EmailTranscriptModal,
ResolveAction,
},
mixins: [alertMixin],
data() {
return {
showEmailActionsModal: false,
@@ -73,11 +72,11 @@ export default {
methods: {
mute() {
this.$store.dispatch('muteConversation', this.currentChat.id);
this.showAlert(this.$t('CONTACT_PANEL.MUTED_SUCCESS'));
useAlert(this.$t('CONTACT_PANEL.MUTED_SUCCESS'));
},
unmute() {
this.$store.dispatch('unmuteConversation', this.currentChat.id);
this.showAlert(this.$t('CONTACT_PANEL.UNMUTED_SUCCESS'));
useAlert(this.$t('CONTACT_PANEL.UNMUTED_SUCCESS'));
},
toggleEmailActionsModal() {
this.showEmailActionsModal = !this.showEmailActionsModal;
@@ -153,7 +153,8 @@
<script>
import { mapGetters } from 'vuex';
import alertMixin from 'shared/mixins/alertMixin';
import { useAlert } from 'dashboard/composables';
import { useUISettings } from 'dashboard/composables/useUISettings';
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
import CannedResponse from './CannedResponse.vue';
@@ -180,10 +181,8 @@ import {
import WhatsappTemplates from './WhatsappTemplates/Modal.vue';
import { MESSAGE_MAX_LENGTH } from 'shared/helpers/MessageTypeHelper';
import inboxMixin, { INBOX_FEATURES } from 'shared/mixins/inboxMixin';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import { trimContent, debounce } from '@chatwoot/utils';
import wootConstants from 'dashboard/constants/globals';
import { isEditorHotKeyEnabled } from 'dashboard/mixins/uiSettings';
import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
import rtlMixin from 'shared/mixins/rtlMixin';
import fileUploadMixin from 'dashboard/mixins/fileUploadMixin';
@@ -197,7 +196,7 @@ import {
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
import { LocalStorage } from 'shared/helpers/localStorage';
const EmojiInput = () => import('shared/components/emoji/EmojiInput');
const EmojiInput = () => import('shared/components/emoji/EmojiInput.vue');
export default {
components: {
@@ -218,8 +217,6 @@ export default {
},
mixins: [
inboxMixin,
uiSettingsMixin,
alertMixin,
messageFormatterMixin,
rtlMixin,
fileUploadMixin,
@@ -231,6 +228,21 @@ export default {
default: false,
},
},
setup() {
const {
uiSettings,
updateUISettings,
isEditorHotKeyEnabled,
fetchSignatureFlagFromUISettings,
} = useUISettings();
return {
uiSettings,
updateUISettings,
isEditorHotKeyEnabled,
fetchSignatureFlagFromUISettings,
};
},
data() {
return {
message: '',
@@ -310,7 +322,7 @@ export default {
agentId,
})
.then(() => {
this.showAlert(this.$t('CONVERSATION.CHANGE_AGENT'));
useAlert(this.$t('CONVERSATION.CHANGE_AGENT'));
});
},
},
@@ -406,7 +418,7 @@ export default {
if (this.isPrivate) {
sendMessageText = this.$t('CONVERSATION.REPLYBOX.CREATE');
}
const keyLabel = isEditorHotKeyEnabled(this.uiSettings, 'cmd_enter')
const keyLabel = this.isEditorHotKeyEnabled('cmd_enter')
? '(⌘ + ↵)'
: '(↵)';
return `${sendMessageText} ${keyLabel}`;
@@ -479,7 +491,7 @@ export default {
return !!this.signatureToApply;
},
sendWithSignature() {
return this.fetchSignatureFlagFromUiSettings(this.channelType);
return this.fetchSignatureFlagFromUISettings(this.channelType);
},
editorMessageKey() {
const { editor_message_key: isEnabled } = this.uiSettings;
@@ -748,7 +760,7 @@ export default {
!this.showCannedMenu &&
!this.showVariablesMenu &&
this.isFocused &&
isEditorHotKeyEnabled(this.uiSettings, selectedKey)
this.isEditorHotKeyEnabled(selectedKey)
);
},
onPaste(e) {
@@ -880,7 +892,7 @@ export default {
} catch (error) {
const errorMessage =
error?.response?.data?.error || this.$t('CONVERSATION.MESSAGE_ERROR');
this.showAlert(errorMessage);
useAlert(errorMessage);
}
},
async onSendWhatsAppReply(messagePayload) {
@@ -77,10 +77,10 @@
import { MESSAGE_TYPE, MESSAGE_STATUS } from 'shared/constants/messages';
import inboxMixin from 'shared/mixins/inboxMixin';
import { mapGetters } from 'vuex';
import timeMixin from '../../../../mixins/time';
import { messageTimestamp } from 'shared/helpers/timeHelper';
export default {
mixins: [inboxMixin, timeMixin],
mixins: [inboxMixin],
props: {
sender: {
type: Object,
@@ -159,7 +159,7 @@ export default {
return MESSAGE_STATUS.SENT === this.messageStatus;
},
readableTime() {
return this.messageTimestamp(this.createdAt, 'LLL d, h:mm a');
return messageTimestamp(this.createdAt, 'LLL d, h:mm a');
},
screenName() {
const { additional_attributes: additionalAttributes = {} } =
@@ -17,14 +17,13 @@
</template>
<script>
import { useAlert } from 'dashboard/composables';
import {
DuplicateContactException,
ExceptionWithMessage,
} from 'shared/helpers/CustomErrors';
import alertMixin from 'shared/mixins/alertMixin';
export default {
mixins: [alertMixin],
props: {
name: {
type: String,
@@ -52,18 +51,18 @@ export default {
'contacts/create',
this.getContactObject()
);
this.showAlert(this.$t('CONTACT_FORM.SUCCESS_MESSAGE'));
useAlert(this.$t('CONTACT_FORM.SUCCESS_MESSAGE'));
}
this.openContactNewTab(contact.id);
} catch (error) {
if (error instanceof DuplicateContactException) {
if (error.data.includes('phone_number')) {
this.showAlert(this.$t('CONTACT_FORM.FORM.PHONE_NUMBER.DUPLICATE'));
useAlert(this.$t('CONTACT_FORM.FORM.PHONE_NUMBER.DUPLICATE'));
}
} else if (error instanceof ExceptionWithMessage) {
this.showAlert(error.data);
useAlert(error.data);
} else {
this.showAlert(this.$t('CONTACT_FORM.ERROR_MESSAGE'));
useAlert(this.$t('CONTACT_FORM.ERROR_MESSAGE'));
}
}
},
@@ -31,10 +31,9 @@
<script>
import DyteAPI from 'dashboard/api/integrations/dyte';
import { buildDyteURL } from 'shared/helpers/IntegrationHelper';
import alertMixin from 'shared/mixins/alertMixin';
import { useAlert } from 'dashboard/composables';
export default {
mixins: [alertMixin],
props: {
messageId: {
type: Number,
@@ -61,7 +60,7 @@ export default {
await DyteAPI.addParticipantToMeeting(this.messageId);
this.dyteAuthToken = authToken;
} catch (err) {
this.showAlert(this.$t('INTEGRATION_SETTINGS.DYTE.JOIN_ERROR'));
useAlert(this.$t('INTEGRATION_SETTINGS.DYTE.JOIN_ERROR'));
} finally {
this.isLoading = false;
}
@@ -12,7 +12,7 @@
@click="onClose"
>
<div
class="bg-white dark:bg-slate-900 z-10 flex items-center justify-between w-full h-16 px-6 py-2"
class="z-10 flex items-center justify-between w-full h-16 px-6 py-2 bg-white dark:bg-slate-900"
@click.stop
>
<div
@@ -167,7 +167,7 @@
/>
</div>
</div>
<div class="flex items-center justify-center w-full h-16 px-6 py-2 z-10">
<div class="z-10 flex items-center justify-center w-full h-16 px-6 py-2">
<div
class="items-center rounded-sm flex font-semibold justify-center min-w-[5rem] p-1 bg-slate-25 dark:bg-slate-800 text-slate-600 dark:text-slate-200 text-sm"
>
@@ -182,7 +182,7 @@
<script>
import { mapGetters } from 'vuex';
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
import timeMixin from 'dashboard/mixins/time';
import { messageTimestamp } from 'shared/helpers/timeHelper';
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
@@ -199,7 +199,7 @@ export default {
components: {
Thumbnail,
},
mixins: [keyboardEventListenerMixins, timeMixin],
mixins: [keyboardEventListenerMixins],
props: {
show: {
type: Boolean,
@@ -236,7 +236,7 @@ export default {
readableTime() {
const { created_at: createdAt } = this.activeAttachment;
if (!createdAt) return '';
return this.messageTimestamp(createdAt, 'LLL d yyyy, h:mm a') || '';
return messageTimestamp(createdAt, 'LLL d yyyy, h:mm a') || '';
},
isImage() {
return this.activeFileType === ALLOWED_FILE_TYPES.IMAGE;
@@ -2,16 +2,16 @@ import { createLocalVue, mount } from '@vue/test-utils';
import Vuex from 'vuex';
import VueI18n from 'vue-i18n';
import VTooltip from 'v-tooltip';
import Button from 'dashboard/components/buttons/Button';
import Button from 'dashboard/components/buttons/Button.vue';
import i18n from 'dashboard/i18n';
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon';
import MoreActions from '../MoreActions';
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue';
import MoreActions from '../MoreActions.vue';
jest.mock('shared/helpers/mitt', () => ({
vi.mock('shared/helpers/mitt', () => ({
emitter: {
emit: jest.fn(),
on: jest.fn(),
off: jest.fn(),
emit: vi.fn(),
on: vi.fn(),
off: vi.fn(),
},
}));
@@ -26,9 +26,9 @@ localVue.component('fluent-icon', FluentIcon);
localVue.component('woot-button', Button);
localVue.prototype.$emitter = {
emit: jest.fn(),
on: jest.fn(),
off: jest.fn(),
emit: vi.fn(),
on: vi.fn(),
off: vi.fn(),
};
const i18nConfig = new VueI18n({ locale: 'en', messages: i18n });
@@ -49,8 +49,8 @@ describe('MoveActions', () => {
currentChat,
};
muteConversation = jest.fn(() => Promise.resolve());
unmuteConversation = jest.fn(() => Promise.resolve());
muteConversation = vi.fn(() => Promise.resolve());
unmuteConversation = vi.fn(() => Promise.resolve());
modules = {
conversations: { actions: { muteConversation, unmuteConversation } },
@@ -85,11 +85,11 @@ describe('MoveActions', () => {
it('shows alert', async () => {
await moreActions.find('button:first-child').trigger('click');
expect(emitter.emit).toBeCalledWith(
'newToastMessage',
'This contact is blocked successfully. You will not be notified of any future conversations.',
undefined
);
expect(emitter.emit).toBeCalledWith('newToastMessage', {
message:
'This contact is blocked successfully. You will not be notified of any future conversations.',
action: null,
});
});
});
@@ -111,11 +111,10 @@ describe('MoveActions', () => {
it('shows alert', async () => {
await moreActions.find('button:first-child').trigger('click');
expect(emitter.emit).toBeCalledWith(
'newToastMessage',
'This contact is unblocked successfully.',
undefined
);
expect(emitter.emit).toBeCalledWith('newToastMessage', {
message: 'This contact is unblocked successfully.',
action: null,
});
});
});
});
@@ -8,7 +8,7 @@ const buildComponent = ({ data = {}, methods = {} }) => ({
data() {
return { ...data, selectedIndex: 0, items: [1, 2, 3] };
},
methods: { ...methods, onSelect: jest.fn(), adjustScroll: jest.fn() },
methods: { ...methods, onSelect: vi.fn(), adjustScroll: vi.fn() },
mixins: [keyboardEventListenerMixins],
});
@@ -21,7 +21,7 @@ describe('mentionSelectionKeyboardMixin', () => {
});
it('ArrowUp and Control+KeyP update selectedIndex correctly', () => {
const preventDefault = jest.fn();
const preventDefault = vi.fn();
const keyboardEvents = wrapper.vm.getKeyboardEvents();
if (keyboardEvents && keyboardEvents.ArrowUp) {
@@ -39,7 +39,7 @@ describe('mentionSelectionKeyboardMixin', () => {
});
it('ArrowDown and Control+KeyN update selectedIndex correctly', () => {
const preventDefault = jest.fn();
const preventDefault = vi.fn();
const keyboardEvents = wrapper.vm.getKeyboardEvents();
if (keyboardEvents && keyboardEvents.ArrowDown) {
@@ -57,7 +57,7 @@ describe('mentionSelectionKeyboardMixin', () => {
});
it('Enter key triggers onSelect method', () => {
const preventDefault = jest.fn();
const preventDefault = vi.fn();
const keyboardEvents = wrapper.vm.getKeyboardEvents();
if (keyboardEvents && keyboardEvents.Enter) {
+12 -3
View File
@@ -1,6 +1,10 @@
import { getCurrentInstance } from 'vue';
import { emitter } from 'shared/helpers/mitt';
/**
* Custom hook to track events
* @returns {Function} The track function
*/
export const useTrack = () => {
const vm = getCurrentInstance();
if (!vm) throw new Error('must be called in setup');
@@ -8,6 +12,11 @@ export const useTrack = () => {
return vm.proxy.$track;
};
export function useAlert(message, action) {
emitter.emit('newToastMessage', message, action);
}
/**
* Emits a toast message event using a global emitter.
* @param {string} message - The message to be displayed in the toast.
* @param {Object|null} action - Optional callback function or object to execute.
*/
export const useAlert = (message, action = null) => {
emitter.emit('newToastMessage', { message, action });
};
@@ -2,17 +2,17 @@ import { shallowMount } from '@vue/test-utils';
import { emitter } from 'shared/helpers/mitt';
import { useEmitter } from '../emitter';
jest.mock('shared/helpers/mitt', () => ({
vi.mock('shared/helpers/mitt', () => ({
emitter: {
on: jest.fn(),
off: jest.fn(),
on: vi.fn(),
off: vi.fn(),
},
}));
describe('useEmitter', () => {
let wrapper;
const eventName = 'my-event';
const callback = jest.fn();
const callback = vi.fn();
beforeEach(() => {
wrapper = shallowMount({
@@ -29,10 +29,6 @@ describe('useEmitter', () => {
});
});
afterEach(() => {
jest.resetAllMocks();
});
it('should add an event listener on mount', () => {
expect(emitter.on).toHaveBeenCalledWith(eventName, callback);
});
@@ -0,0 +1,51 @@
import { getCurrentInstance } from 'vue';
import { emitter } from 'shared/helpers/mitt';
import { useTrack, useAlert } from '../index';
vi.mock('vue', () => ({
getCurrentInstance: vi.fn(),
}));
vi.mock('shared/helpers/mitt', () => ({
emitter: {
emit: vi.fn(),
},
}));
describe('useTrack', () => {
it('should return $track from the current instance proxy', () => {
const mockProxy = { $track: vi.fn() };
getCurrentInstance.mockReturnValue({ proxy: mockProxy });
const track = useTrack();
expect(track).toBe(mockProxy.$track);
});
it('should throw an error if called outside of setup', () => {
getCurrentInstance.mockReturnValue(null);
expect(useTrack).toThrowError('must be called in setup');
});
});
describe('useAlert', () => {
it('should emit a newToastMessage event with the provided message and action', () => {
const message = 'Toast message';
const action = {
type: 'link',
to: '/app/accounts/1/conversations/1',
message: 'Navigate',
};
useAlert(message, action);
expect(emitter.emit).toHaveBeenCalledWith('newToastMessage', {
message,
action,
});
});
it('should emit a newToastMessage event with the provided message and no action if action is null', () => {
const message = 'Toast message';
useAlert(message);
expect(emitter.emit).toHaveBeenCalledWith('newToastMessage', {
message,
action: null,
});
});
});
@@ -0,0 +1,52 @@
import { ref } from 'vue';
import { useAdmin } from '../useAdmin';
import { useStoreGetters } from 'dashboard/composables/store';
vi.mock('dashboard/composables/store');
describe('useAdmin', () => {
it('returns true if the current user is an administrator', () => {
useStoreGetters.mockReturnValue({
getCurrentRole: ref('administrator'),
});
const { isAdmin } = useAdmin();
expect(isAdmin.value).toBe(true);
});
it('returns false if the current user is not an administrator', () => {
useStoreGetters.mockReturnValue({
getCurrentRole: ref('user'),
});
const { isAdmin } = useAdmin();
expect(isAdmin.value).toBe(false);
});
it('returns false if the current user role is null', () => {
useStoreGetters.mockReturnValue({
getCurrentRole: ref(null),
});
const { isAdmin } = useAdmin();
expect(isAdmin.value).toBe(false);
});
it('returns false if the current user role is undefined', () => {
useStoreGetters.mockReturnValue({
getCurrentRole: ref(undefined),
});
const { isAdmin } = useAdmin();
expect(isAdmin.value).toBe(false);
});
it('returns false if the current user role is an empty string', () => {
useStoreGetters.mockReturnValue({
getCurrentRole: ref(''),
});
const { isAdmin } = useAdmin();
expect(isAdmin.value).toBe(false);
});
});
@@ -0,0 +1,138 @@
import { ref } from 'vue';
import {
useUISettings,
DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER,
DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER,
} from 'dashboard/composables/useUISettings';
// Mocking the store composables
const mockDispatch = vi.fn();
const getUISettingsMock = ref({
is_ct_labels_open: true,
conversation_sidebar_items_order: DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER,
contact_sidebar_items_order: DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER,
editor_message_key: 'enter',
});
vi.mock('dashboard/composables/store', () => ({
useStoreGetters: () => ({
getUISettings: getUISettingsMock,
}),
useStore: () => ({
dispatch: mockDispatch,
}),
}));
describe('useUISettings', () => {
beforeEach(() => {
mockDispatch.mockClear();
});
it('returns uiSettings', () => {
const { uiSettings } = useUISettings();
expect(uiSettings.value).toEqual({
is_ct_labels_open: true,
conversation_sidebar_items_order:
DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER,
contact_sidebar_items_order: DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER,
editor_message_key: 'enter',
});
});
it('updates UI settings correctly', () => {
const { updateUISettings } = useUISettings();
updateUISettings({ enter_to_send_enabled: true });
expect(mockDispatch).toHaveBeenCalledWith('updateUISettings', {
uiSettings: {
enter_to_send_enabled: true,
is_ct_labels_open: true,
conversation_sidebar_items_order:
DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER,
contact_sidebar_items_order: DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER,
editor_message_key: 'enter',
},
});
});
it('toggles sidebar UI state correctly', () => {
const { toggleSidebarUIState } = useUISettings();
toggleSidebarUIState('is_ct_labels_open');
expect(mockDispatch).toHaveBeenCalledWith('updateUISettings', {
uiSettings: {
is_ct_labels_open: false,
conversation_sidebar_items_order:
DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER,
contact_sidebar_items_order: DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER,
editor_message_key: 'enter',
},
});
});
it('returns correct conversation sidebar items order', () => {
const { conversationSidebarItemsOrder } = useUISettings();
expect(conversationSidebarItemsOrder.value).toEqual(
DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER
);
});
it('returns correct contact sidebar items order', () => {
const { contactSidebarItemsOrder } = useUISettings();
expect(contactSidebarItemsOrder.value).toEqual(
DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER
);
});
it('returns correct value for isContactSidebarItemOpen', () => {
const { isContactSidebarItemOpen } = useUISettings();
expect(isContactSidebarItemOpen('is_ct_labels_open')).toBe(true);
expect(isContactSidebarItemOpen('non_existent_key')).toBe(false);
});
it('sets signature flag for inbox correctly', () => {
const { setSignatureFlagForInbox } = useUISettings();
setSignatureFlagForInbox('email', true);
expect(mockDispatch).toHaveBeenCalledWith('updateUISettings', {
uiSettings: {
is_ct_labels_open: true,
conversation_sidebar_items_order:
DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER,
contact_sidebar_items_order: DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER,
email_signature_enabled: true,
editor_message_key: 'enter',
},
});
});
it('fetches signature flag from UI settings correctly', () => {
const { fetchSignatureFlagFromUISettings } = useUISettings();
expect(fetchSignatureFlagFromUISettings('email')).toBe(undefined);
});
it('returns correct value for isEditorHotKeyEnabled when editor_message_key is configured', () => {
getUISettingsMock.value.enter_to_send_enabled = false;
const { isEditorHotKeyEnabled } = useUISettings();
expect(isEditorHotKeyEnabled('enter')).toBe(true);
expect(isEditorHotKeyEnabled('cmd_enter')).toBe(false);
});
it('returns correct value for isEditorHotKeyEnabled when editor_message_key is not configured', () => {
getUISettingsMock.value.editor_message_key = undefined;
const { isEditorHotKeyEnabled } = useUISettings();
expect(isEditorHotKeyEnabled('enter')).toBe(false);
expect(isEditorHotKeyEnabled('cmd_enter')).toBe(true);
});
it('handles non-existent keys', () => {
const {
isContactSidebarItemOpen,
fetchSignatureFlagFromUISettings,
isEditorHotKeyEnabled,
} = useUISettings();
expect(isContactSidebarItemOpen('non_existent_key')).toBe(false);
expect(fetchSignatureFlagFromUISettings('non_existent_key')).toBe(
undefined
);
expect(isEditorHotKeyEnabled('non_existent_key')).toBe(false);
});
});
@@ -0,0 +1,17 @@
import { computed } from 'vue';
import { useStoreGetters } from 'dashboard/composables/store';
/**
* Composable to determine if the current user is an administrator.
* @returns {Boolean} - True if the current user is an administrator, false otherwise.
*/
export function useAdmin() {
const getters = useStoreGetters();
const currentUserRole = computed(() => getters.getCurrentRole.value);
const isAdmin = computed(() => currentUserRole.value === 'administrator');
return {
isAdmin,
};
}
@@ -0,0 +1,149 @@
import { computed } from 'vue';
import { useStore, useStoreGetters } from 'dashboard/composables/store';
export const DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER = Object.freeze([
{ name: 'conversation_actions' },
{ name: 'macros' },
{ name: 'conversation_info' },
{ name: 'contact_attributes' },
{ name: 'previous_conversation' },
{ name: 'conversation_participants' },
]);
export const DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER = Object.freeze([
{ name: 'contact_attributes' },
{ name: 'contact_labels' },
{ name: 'previous_conversation' },
]);
/**
* Slugifies the channel name.
* Replaces spaces, hyphens, and double colons with underscores.
* @param {string} name - The channel name to slugify.
* @returns {string} The slugified channel name.
*/
const slugifyChannel = name =>
name?.toLowerCase().replace(' ', '_').replace('-', '_').replace('::', '_');
/**
* Computes the order of items in the conversation sidebar, using defaults if not present.
* @param {Object} uiSettings - Reactive UI settings object.
* @returns {Array} Ordered list of sidebar items.
*/
const useConversationSidebarItemsOrder = uiSettings => {
return computed(() => {
const { conversation_sidebar_items_order: itemsOrder } = uiSettings.value;
// If the sidebar order is not set, use the default order.
if (!itemsOrder) {
return DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER;
}
// Create a copy of itemsOrder to avoid mutating the original store object.
const itemsOrderCopy = [...itemsOrder];
// If the sidebar order doesn't have the new elements, then add them to the list.
DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER.forEach(item => {
if (!itemsOrderCopy.find(i => i.name === item.name)) {
itemsOrderCopy.push(item);
}
});
return itemsOrderCopy;
});
};
/**
* Computes the order of items in the contact sidebar,using defaults if not present.
* @param {Object} uiSettings - Reactive UI settings object.
* @returns {Array} Ordered list of sidebar items.
*/
const useContactSidebarItemsOrder = uiSettings => {
return computed(() => {
const { contact_sidebar_items_order: itemsOrder } = uiSettings.value;
return itemsOrder || DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER;
});
};
/**
* Toggles the open state of a sidebar item.
* @param {string} key - The key of the sidebar item to toggle.
* @param {Object} uiSettings - Reactive UI settings object.
* @param {Function} updateUISettings - Function to update UI settings.
*/
const toggleSidebarUIState = (key, uiSettings, updateUISettings) => {
updateUISettings({ [key]: !uiSettings.value[key] });
};
/**
* Sets the signature flag for a specific channel type in the inbox settings.
* @param {string} channelType - The type of the channel.
* @param {boolean} value - The value to set for the signature enabled flag.
* @param {Function} updateUISettings - Function to update UI settings.
*/
const setSignatureFlagForInbox = (channelType, value, updateUISettings) => {
if (!channelType) return;
const slugifiedChannel = slugifyChannel(channelType);
updateUISettings({ [`${slugifiedChannel}_signature_enabled`]: value });
};
/**
* Fetches the signature flag for a specific channel type from UI settings.
* @param {string} channelType - The type of the channel.
* @param {Object} uiSettings - Reactive UI settings object.
* @returns {boolean} The value of the signature enabled flag.
*/
const fetchSignatureFlagFromUISettings = (channelType, uiSettings) => {
if (!channelType) return false;
const slugifiedChannel = slugifyChannel(channelType);
return uiSettings.value[`${slugifiedChannel}_signature_enabled`];
};
/**
* Checks if a specific editor hotkey is enabled.
* @param {string} key - The key to check.
* @param {Object} uiSettings - Reactive UI settings object.
* @returns {boolean} True if the hotkey is enabled, otherwise false.
*/
const isEditorHotKeyEnabled = (key, uiSettings) => {
const {
editor_message_key: editorMessageKey,
enter_to_send_enabled: enterToSendEnabled,
} = uiSettings.value || {};
if (!editorMessageKey) {
return key === (enterToSendEnabled ? 'enter' : 'cmd_enter');
}
return editorMessageKey === key;
};
/**
* Main composable function for managing UI settings.
* @returns {Object} An object containing reactive properties and methods for UI settings management.
*/
export function useUISettings() {
const getters = useStoreGetters();
const store = useStore();
const uiSettings = computed(() => getters.getUISettings.value);
const updateUISettings = (settings = {}) => {
store.dispatch('updateUISettings', {
uiSettings: {
...uiSettings.value,
...settings,
},
});
};
return {
uiSettings,
updateUISettings,
conversationSidebarItemsOrder: useConversationSidebarItemsOrder(uiSettings),
contactSidebarItemsOrder: useContactSidebarItemsOrder(uiSettings),
isContactSidebarItemOpen: key => !!uiSettings.value[key],
toggleSidebarUIState: key =>
toggleSidebarUIState(key, uiSettings, updateUISettings),
setSignatureFlagForInbox: (channelType, value) =>
setSignatureFlagForInbox(channelType, value, updateUISettings),
fetchSignatureFlagFromUISettings: channelType =>
fetchSignatureFlagFromUISettings(channelType, uiSettings),
isEditorHotKeyEnabled: key => isEditorHotKeyEnabled(key, uiSettings),
};
}
@@ -38,9 +38,10 @@ export class AnalyticsHelper {
* @param {Object} user - User object
*/
identify(user) {
if (!this.analytics) {
if (!this.analytics || !user) {
return;
}
this.user = user;
this.analytics.identify(this.user.email, {
userId: this.user.id,
@@ -1,13 +1,13 @@
import helperObject, { AnalyticsHelper } from '../';
jest.mock('@june-so/analytics-next', () => ({
vi.mock('@june-so/analytics-next', () => ({
AnalyticsBrowser: {
load: () => [
{
identify: jest.fn(),
track: jest.fn(),
page: jest.fn(),
group: jest.fn(),
identify: vi.fn(),
track: vi.fn(),
page: vi.fn(),
group: vi.fn(),
},
],
},
@@ -40,7 +40,7 @@ describe('AnalyticsHelper', () => {
describe('identify', () => {
beforeEach(() => {
analyticsHelper.analytics = { identify: jest.fn(), group: jest.fn() };
analyticsHelper.analytics = { identify: vi.fn(), group: vi.fn() };
});
it('should call identify on analytics browser with correct arguments', () => {
@@ -87,7 +87,7 @@ describe('AnalyticsHelper', () => {
describe('track', () => {
beforeEach(() => {
analyticsHelper.analytics = { track: jest.fn() };
analyticsHelper.analytics = { track: vi.fn() };
analyticsHelper.user = { id: '123' };
});
@@ -118,7 +118,7 @@ describe('AnalyticsHelper', () => {
describe('page', () => {
beforeEach(() => {
analyticsHelper.analytics = { page: jest.fn() };
analyticsHelper.analytics = { page: vi.fn() };
});
it('should call the analytics.page method with the correct arguments', () => {
@@ -2,34 +2,35 @@ import Vue from 'vue';
import plugin from '../plugin';
import analyticsHelper from '../index';
vi.spyOn(analyticsHelper, 'init');
vi.spyOn(analyticsHelper, 'track');
describe('Vue Analytics Plugin', () => {
beforeEach(() => {
jest.spyOn(analyticsHelper, 'init');
jest.spyOn(analyticsHelper, 'track');
Vue.use(plugin);
});
afterEach(() => {
jest.resetModules();
jest.clearAllMocks();
it('should call the init method on analyticsHelper once during plugin installation', () => {
expect(analyticsHelper.init).toHaveBeenCalledTimes(1);
});
it('should call the init method on the analyticsHelper', () => {
expect(analyticsHelper.init).toHaveBeenCalled();
});
it('should add the analyticsHelper to the Vue prototype', () => {
it('should add the analyticsHelper to the Vue prototype as $analytics', () => {
expect(Vue.prototype.$analytics).toBe(analyticsHelper);
});
it('should add the track method to the Vue prototype', () => {
it('should add a track method to the Vue prototype as $track', () => {
expect(typeof Vue.prototype.$track).toBe('function');
Vue.prototype.$track('eventName');
expect(analyticsHelper.track).toHaveBeenCalledWith('eventName');
expect(analyticsHelper.track)
.toHaveBeenCalledTimes(1)
.toHaveBeenCalledWith('eventName');
});
it('should call the track method on the analyticsHelper when $track is called', () => {
Vue.prototype.$track('eventName');
expect(analyticsHelper.track).toHaveBeenCalledWith('eventName');
it('should call the track method on analyticsHelper with the correct event name when $track is called', () => {
const eventName = 'testEvent';
Vue.prototype.$track(eventName);
expect(analyticsHelper.track)
.toHaveBeenCalledTimes(1)
.toHaveBeenCalledWith(eventName);
});
});
@@ -52,6 +52,10 @@ class DashboardAudioNotificationHelper {
};
executeRecurringNotification = () => {
if (!window.WOOT || !window.WOOT.$store) {
this.clearSetTimeout();
return;
}
const mineConversation = window.WOOT.$store.getters.getMineChats({
assigneeType: 'me',
status: 'open',
@@ -0,0 +1,124 @@
const SCRIPT_TYPE = 'text/javascript';
const DATA_LOADED_ATTR = 'data-loaded';
const SCRIPT_PROPERTIES = [
'defer',
'crossOrigin',
'noModule',
'referrerPolicy',
'id',
];
/**
* Custom error class for script loading failures.
* @extends Error
*/
class ScriptLoaderError extends Error {
/**
* Creates a new ScriptLoaderError.
* @param {string} src - The source URL of the script that failed to load.
* @param {string} message - The error message.
*/
constructor(src, message = 'Failed to load script') {
super(message);
this.name = 'ScriptLoaderError';
this.src = src;
}
/**
* Gets detailed error information.
* @returns {string} A string containing the error details.
*/
getErrorDetails() {
return `Failed to load script from source: ${this.src}`;
}
}
/**
* Creates a new script element with the specified attributes.
* @param {string} src - The source URL of the script.
* @param {Object} options - Options for configuring the script element.
* @param {string} [options.type='text/javascript'] - The type of the script.
* @param {boolean} [options.async=true] - Whether the script should load asynchronously.
* @param {boolean} [options.defer] - Whether the script execution should be deferred.
* @param {string} [options.crossOrigin] - The CORS setting for the script.
* @param {boolean} [options.noModule] - Whether the script should not be treated as a JavaScript module.
* @param {string} [options.referrerPolicy] - The referrer policy for the script.
* @param {string} [options.id] - The id attribute for the script element.
* @param {Object} [options.attrs] - Additional attributes to set on the script element.
* @returns {HTMLScriptElement} The created script element.
*/
const createScriptElement = (src, options) => {
const el = document.createElement('script');
el.type = options.type || SCRIPT_TYPE;
el.async = options.async !== false;
el.src = src;
SCRIPT_PROPERTIES.forEach(property => {
if (property in options) {
el[property] = options[property];
}
});
Object.entries(options.attrs || {}).forEach(([name, value]) =>
el.setAttribute(name, value)
);
return el;
};
/**
* Finds an existing script element with the specified source URL.
* @param {string} src - The source URL to search for.
* @returns {HTMLScriptElement|null} The found script element, or null if not found.
*/
const findExistingScript = src => {
return document.querySelector(`script[src="${src}"]`);
};
/**
* Loads a script asynchronously and returns a promise.
* @param {string} src - The source URL of the script to load.
* @param {Object} options - Options for configuring the script element.
* @param {string} [options.type='text/javascript'] - The type of the script.
* @param {boolean} [options.async=true] - Whether the script should load asynchronously.
* @param {boolean} [options.defer] - Whether the script execution should be deferred.
* @param {string} [options.crossOrigin] - The CORS setting for the script.
* @param {boolean} [options.noModule] - Whether the script should not be treated as a JavaScript module.
* @param {string} [options.referrerPolicy] - The referrer policy for the script.
* @param {string} [options.id] - The id attribute for the script element.
* @param {Object} [options.attrs] - Additional attributes to set on the script element.
* @returns {Promise<HTMLScriptElement|boolean>} A promise that resolves with the loaded script element,
* or false if the script couldn't be loaded.
* @throws {ScriptLoaderError} If the script fails to load.
*/
export async function loadScript(src, options) {
if (typeof window === 'undefined' || !window.document) {
return Promise.resolve(false);
}
return new Promise((resolve, reject) => {
if (typeof src !== 'string' || src.trim() === '') {
reject(new Error('Invalid source URL provided'));
return;
}
let el = findExistingScript(src);
if (!el) {
el = createScriptElement(src, options);
document.head.appendChild(el);
} else if (el.hasAttribute(DATA_LOADED_ATTR)) {
resolve(el);
return;
}
const handleError = () => reject(new ScriptLoaderError(src));
el.addEventListener('error', handleError);
el.addEventListener('abort', handleError);
el.addEventListener('load', () => {
el.setAttribute(DATA_LOADED_ATTR, 'true');
resolve(el);
});
});
}
@@ -9,6 +9,11 @@ import {
const MAX_DISCONNECT_SECONDS = 10800;
// The disconnect delay threshold is added to account for delays in identifying
// disconnections (for example, the websocket disconnection takes up to 3 seconds)
// while fetching the latest updated conversations or messages.
const DISCONNECT_DELAY_THRESHOLD = 15;
class ReconnectService {
constructor(store, router) {
this.store = store;
@@ -47,7 +52,8 @@ class ReconnectService {
fetchConversations = async () => {
await this.store.dispatch('updateChatListFilters', {
page: null,
updatedWithin: this.getSecondsSinceDisconnect(),
updatedWithin:
this.getSecondsSinceDisconnect() + DISCONNECT_DELAY_THRESHOLD,
});
await this.store.dispatch('fetchAllConversations');
// Reset the updatedWithin in the store chat list filter after fetching conversations when the user is reconnected
@@ -1,3 +1,5 @@
import cloneObject from 'dashboard/helpers/clone';
const allElementsString = arr => {
return arr.every(elem => typeof elem === 'string');
};
@@ -27,7 +29,7 @@ const generatePayloadForObject = item => {
};
const generatePayload = data => {
const actions = JSON.parse(JSON.stringify(data));
const actions = cloneObject(data);
let payload = actions.map(item => {
if (Array.isArray(item.action_params)) {
item.action_params = formatArray(item.action_params);
@@ -5,6 +5,8 @@ import {
} from 'dashboard/routes/dashboard/settings/automation/operators';
import filterQueryGenerator from './filterQueryGenerator';
import actionQueryGenerator from './actionQueryGenerator';
import cloneObject from 'dashboard/helpers/clone';
const MESSAGE_CONDITION_VALUES = [
{
id: 'incoming',
@@ -121,7 +123,7 @@ export const generateConditionOptions = (options, key = 'id') => {
};
// Add the "None" option to the agent list
export const agentList = agents => [
export const addNoneToList = agents => [
{
id: 'nil',
name: 'None',
@@ -137,8 +139,8 @@ export const getActionOptions = ({
type,
}) => {
const actionsMap = {
assign_agent: agentList(agents),
assign_team: teams,
assign_agent: addNoneToList(agents),
assign_team: addNoneToList(teams),
send_email_to_team: teams,
add_label: generateConditionOptions(labels, 'title'),
remove_label: generateConditionOptions(labels, 'title'),
@@ -255,7 +257,7 @@ export const getStandardAttributeInputType = (automationTypes, event, key) => {
};
export const generateAutomationPayload = payload => {
const automation = JSON.parse(JSON.stringify(payload));
const automation = cloneObject(payload);
automation.conditions[automation.conditions.length - 1].query_operator = null;
automation.conditions = filterQueryGenerator(automation.conditions).payload;
automation.actions = actionQueryGenerator(automation.actions);
+33
View File
@@ -0,0 +1,33 @@
/**
* Creates a deep clone of the provided object.
*
* This function attempts to use the `structuredClone` method if available.
* If `structuredClone` is not supported, it falls back to using
* `JSON.parse(JSON.stringify())`.
*
* @param {*} obj - The object to be cloned. Can be of any type.
* @returns {*} A deep clone of the input object.
*
* @throws {TypeError} If the object contains values that JSON cannot serialize
* (e.g., functions, undefined) when falling back to
* JSON methods.
*
* @example
* const original = { a: 1, b: { c: 2 } };
* const clone = cloneObject(original);
* console.log(clone); // { a: 1, b: { c: 2 } }
* console.log(original === clone); // false
*/
export function cloneObject(obj) {
if (typeof structuredClone === 'function') {
return structuredClone(obj);
}
// The JSON method doesn't handle all JavaScript types correctly and may cause unexpected behavior.
// At the moment structuredClone has good adoption across browsers https://caniuse.com/mdn-api_structuredclone
// and is the preferred method for cloning objects.
//
// We can consider implementing a more robust fallback method in the future if we find users are running into issues.
// Ref: https://github.com/lukeed/klona
return JSON.parse(JSON.stringify(obj));
}
@@ -1,3 +1,5 @@
import cloneObject from 'dashboard/helpers/clone';
const setArrayValues = item => {
return item.values[0]?.id ? item.values.map(val => val.id) : item.values;
};
@@ -21,7 +23,7 @@ const generateValues = item => {
const generatePayload = data => {
// Make a copy of data to avoid vue data reactivity issues
const filters = JSON.parse(JSON.stringify(data));
const filters = cloneObject(data);
let payload = filters.map(item => {
// If item key is content, we will split it using comma and return as array
// FIX ME: Make this generic option instead of using the key directly here
@@ -0,0 +1,34 @@
export const hasPermissions = (
requiredPermissions = [],
availablePermissions = []
) => {
return requiredPermissions.some(permission =>
availablePermissions.includes(permission)
);
};
const isPermissionsPresentInRoute = route =>
route.meta && route.meta.permissions;
export const buildPermissionsFromRouter = (routes = []) =>
routes.reduce((acc, route) => {
if (route.name) {
if (!isPermissionsPresentInRoute(route)) {
// eslint-disable-next-line
console.error(route);
throw new Error(
"The route doesn't have the required permissions defined"
);
}
acc[route.name] = route.meta.permissions;
}
if (route.children) {
acc = {
...acc,
...buildPermissionsFromRouter(route.children),
};
}
return acc;
}, {});
@@ -1,7 +1,7 @@
/* eslint-disable no-console */
import NotificationSubscriptions from '../api/notificationSubscription';
import auth from '../api/auth';
import { emitter } from 'shared/helpers/mitt';
import { useAlert } from 'dashboard/composables';
export const verifyServiceWorkerExistence = (callback = () => {}) => {
if (!('serviceWorker' in navigator)) {
@@ -69,19 +69,13 @@ export const registerSubscription = (onSuccess = () => {}) => {
onSuccess();
})
.catch(() => {
emitter.emit(
'newToastMessage',
'This browser does not support desktop notification'
);
useAlert('This browser does not support desktop notification');
});
};
export const requestPushPermissions = ({ onSuccess }) => {
if (!('Notification' in window)) {
emitter.emit(
'newToastMessage',
'This browser does not support desktop notification'
);
useAlert('This browser does not support desktop notification');
} else if (Notification.permission === 'granted') {
registerSubscription(onSuccess);
} else if (Notification.permission !== 'denied') {
@@ -1,19 +1,16 @@
import { hasPermissions } from './permissionsHelper';
// eslint-disable-next-line default-param-last
export const getCurrentAccount = ({ accounts } = {}, accountId) => {
return accounts.find(account => account.id === accountId);
};
// eslint-disable-next-line default-param-last
export const getUserRole = ({ accounts } = {}, accountId) => {
const currentAccount = getCurrentAccount({ accounts }, accountId) || {};
return currentAccount.role || null;
export const routeIsAccessibleFor = (route, userPermissions = []) => {
const { meta: { permissions: routePermissions = [] } = {} } = route;
return hasPermissions(routePermissions, userPermissions);
};
export const routeIsAccessibleFor = (route, role, roleWiseRoutes) => {
return roleWiseRoutes[role].includes(route);
};
const validateActiveAccountRoutes = (to, user, roleWiseRoutes) => {
const validateActiveAccountRoutes = (to, user) => {
// If the current account is active, then check for the route permissions
const accountDashboardURL = `accounts/${to.params.accountId}/dashboard`;
@@ -22,15 +19,13 @@ const validateActiveAccountRoutes = (to, user, roleWiseRoutes) => {
return accountDashboardURL;
}
const userRole = getUserRole(user, Number(to.params.accountId));
const isAccessible = routeIsAccessibleFor(to.name, userRole, roleWiseRoutes);
const isAccessible = routeIsAccessibleFor(to, user.permissions);
// If the route is not accessible for the user, return to dashboard screen
return isAccessible ? null : accountDashboardURL;
};
export const validateLoggedInRoutes = (to, user, roleWiseRoutes) => {
export const validateLoggedInRoutes = (to, user) => {
const currentAccount = getCurrentAccount(user, Number(to.params.accountId));
// If current account is missing, either user does not have
// access to the account or the account is deleted, return to login screen
if (!currentAccount) {
@@ -40,7 +35,7 @@ export const validateLoggedInRoutes = (to, user, roleWiseRoutes) => {
const isCurrentAccountActive = currentAccount.status === 'active';
if (isCurrentAccountActive) {
return validateActiveAccountRoutes(to, user, roleWiseRoutes);
return validateActiveAccountRoutes(to, user);
}
// If the current account is not active, then redirect the user to the suspended screen
@@ -4,7 +4,7 @@ describe('DataManager', () => {
const accountId = 'test-account';
let dataManager;
beforeAll(async () => {
beforeEach(async () => {
dataManager = new DataManager(accountId);
await dataManager.initDb();
});
@@ -0,0 +1,95 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { loadScript } from '../DOMHelpers';
import { JSDOM } from 'jsdom';
describe('loadScript', () => {
let dom;
let window;
let document;
beforeEach(() => {
dom = new JSDOM('<!DOCTYPE html><html><head></head><body></body></html>', {
url: 'http://localhost',
});
window = dom.window;
document = window.document;
global.document = document;
});
afterEach(() => {
vi.restoreAllMocks();
delete global.document;
});
it('should load a script successfully', async () => {
const src = 'https://example.com/script.js';
const loadPromise = loadScript(src, {});
// Simulate successful script load
setTimeout(() => {
const script = document.querySelector(`script[src="${src}"]`);
if (script) {
script.dispatchEvent(new window.Event('load'));
}
}, 0);
const script = await loadPromise;
expect(script).toBeTruthy();
expect(script.getAttribute('src')).toBe(src);
expect(script.getAttribute('data-loaded')).toBe('true');
});
it('should not load a script if document is not available', async () => {
delete global.document;
const result = await loadScript('https://example.com/script.js', {});
expect(result).toBe(false);
});
it('should use an existing script if already present', async () => {
const src = 'https://example.com/existing-script.js';
const existingScript = document.createElement('script');
existingScript.src = src;
existingScript.setAttribute('data-loaded', 'true');
document.head.appendChild(existingScript);
const script = await loadScript(src, {});
expect(script).toBe(existingScript);
});
it('should set custom attributes on the script element', async () => {
const src = 'https://example.com/custom-script.js';
const options = {
type: 'module',
async: false,
defer: true,
crossOrigin: 'anonymous',
noModule: true,
referrerPolicy: 'origin',
id: 'custom-script',
attrs: { 'data-custom': 'value' },
};
const loadPromise = loadScript(src, options);
// Simulate successful script load
setTimeout(() => {
const script = document.querySelector(`script[src="${src}"]`);
if (script) {
script.dispatchEvent(new window.Event('load'));
}
}, 0);
const script = await loadPromise;
expect(script.type).toBe('module');
expect(script.async).toBe(false);
expect(script.defer).toBe(true);
expect(script.crossOrigin).toBe('anonymous');
expect(script.noModule).toBe(true);
expect(script.referrerPolicy).toBe('origin');
expect(script.id).toBe('custom-script');
expect(script.getAttribute('data-custom')).toBe('value');
});
});
@@ -8,26 +8,26 @@ import {
} from 'dashboard/helper/routeHelpers';
import ReconnectService from 'dashboard/helper/ReconnectService';
jest.mock('shared/helpers/mitt', () => ({
vi.mock('shared/helpers/mitt', () => ({
emitter: {
on: jest.fn(),
off: jest.fn(),
emit: jest.fn(),
on: vi.fn(),
off: vi.fn(),
emit: vi.fn(),
},
}));
jest.mock('date-fns', () => ({
differenceInSeconds: jest.fn(),
vi.mock('date-fns', () => ({
differenceInSeconds: vi.fn(),
}));
jest.mock('dashboard/helper/routeHelpers', () => ({
isAConversationRoute: jest.fn(),
isAInboxViewRoute: jest.fn(),
isNotificationRoute: jest.fn(),
vi.mock('dashboard/helper/routeHelpers', () => ({
isAConversationRoute: vi.fn(),
isAInboxViewRoute: vi.fn(),
isNotificationRoute: vi.fn(),
}));
const storeMock = {
dispatch: jest.fn(),
dispatch: vi.fn(),
getters: {
getAppliedConversationFiltersQuery: [],
'customViews/getActiveConversationFolder': { query: {} },
@@ -46,17 +46,17 @@ describe('ReconnectService', () => {
let reconnectService;
beforeEach(() => {
window.addEventListener = jest.fn();
window.removeEventListener = jest.fn();
window.addEventListener = vi.fn();
window.removeEventListener = vi.fn();
Object.defineProperty(window, 'location', {
configurable: true,
value: { reload: jest.fn() },
value: { reload: vi.fn() },
});
reconnectService = new ReconnectService(storeMock, routerMock);
});
afterEach(() => {
jest.clearAllMocks();
vi.clearAllMocks();
});
describe('constructor', () => {
@@ -102,7 +102,7 @@ describe('ReconnectService', () => {
expect(reconnectService.getSecondsSinceDisconnect()).toBe(0);
});
it('should return the number of seconds since disconnect', () => {
it('should return the number of seconds + threshold since disconnect', () => {
reconnectService.disconnectTime = new Date();
differenceInSeconds.mockReturnValue(100);
expect(reconnectService.getSecondsSinceDisconnect()).toBe(100);
@@ -111,7 +111,7 @@ describe('ReconnectService', () => {
describe('handleOnlineEvent', () => {
it('should reload the page if disconnected for more than 3 hours', () => {
reconnectService.getSecondsSinceDisconnect = jest
reconnectService.getSecondsSinceDisconnect = vi
.fn()
.mockReturnValue(10801);
reconnectService.handleOnlineEvent();
@@ -119,7 +119,7 @@ describe('ReconnectService', () => {
});
it('should not reload the page if disconnected for less than 3 hours', () => {
reconnectService.getSecondsSinceDisconnect = jest
reconnectService.getSecondsSinceDisconnect = vi
.fn()
.mockReturnValue(10799);
reconnectService.handleOnlineEvent();
@@ -128,22 +128,27 @@ describe('ReconnectService', () => {
});
describe('fetchConversations', () => {
it('should dispatch updateChatListFilters and fetchAllConversations', async () => {
reconnectService.getSecondsSinceDisconnect = jest
.fn()
.mockReturnValue(100);
it('should update the filters with disconnected time and the threshold', async () => {
reconnectService.getSecondsSinceDisconnect = vi.fn().mockReturnValue(100);
await reconnectService.fetchConversations();
expect(storeMock.dispatch).toHaveBeenCalledWith('updateChatListFilters', {
page: null,
updatedWithin: 100,
updatedWithin: 115,
});
});
it('should dispatch updateChatListFilters and fetchAllConversations', async () => {
reconnectService.getSecondsSinceDisconnect = vi.fn().mockReturnValue(100);
await reconnectService.fetchConversations();
expect(storeMock.dispatch).toHaveBeenCalledWith('updateChatListFilters', {
page: null,
updatedWithin: 115,
});
expect(storeMock.dispatch).toHaveBeenCalledWith('fetchAllConversations');
});
it('should dispatch updateChatListFilters and reset updatedWithin', async () => {
reconnectService.getSecondsSinceDisconnect = jest
.fn()
.mockReturnValue(100);
reconnectService.getSecondsSinceDisconnect = vi.fn().mockReturnValue(100);
await reconnectService.fetchConversations();
expect(storeMock.dispatch).toHaveBeenCalledWith('updateChatListFilters', {
updatedWithin: null,
@@ -173,7 +178,7 @@ describe('ReconnectService', () => {
},
],
};
const spy = jest.spyOn(
const spy = vi.spyOn(
reconnectService,
'fetchFilteredOrSavedConversations'
);
@@ -191,7 +196,7 @@ describe('ReconnectService', () => {
query: null,
};
const spy = jest.spyOn(reconnectService, 'fetchConversations');
const spy = vi.spyOn(reconnectService, 'fetchConversations');
await reconnectService.fetchConversationsOnReconnect();
@@ -204,7 +209,7 @@ describe('ReconnectService', () => {
query: { test: 'activeFolderQuery' },
};
const spy = jest.spyOn(
const spy = vi.spyOn(
reconnectService,
'fetchFilteredOrSavedConversations'
);
@@ -270,11 +275,11 @@ describe('ReconnectService', () => {
describe('handleRouteSpecificFetch', () => {
it('should fetch conversations and messages if current route is a conversation route', async () => {
isAConversationRoute.mockReturnValue(true);
const spyConversations = jest.spyOn(
const spyConversations = vi.spyOn(
reconnectService,
'fetchConversationsOnReconnect'
);
const spyMessages = jest.spyOn(
const spyMessages = vi.spyOn(
reconnectService,
'fetchConversationMessagesOnReconnect'
);
@@ -285,14 +290,14 @@ describe('ReconnectService', () => {
it('should fetch notifications if current route is an inbox view route', async () => {
isAInboxViewRoute.mockReturnValue(true);
const spy = jest.spyOn(reconnectService, 'fetchNotificationsOnReconnect');
const spy = vi.spyOn(reconnectService, 'fetchNotificationsOnReconnect');
await reconnectService.handleRouteSpecificFetch();
expect(spy).toHaveBeenCalled();
});
it('should fetch notifications if current route is a notification route', async () => {
isNotificationRoute.mockReturnValue(true);
const spy = jest.spyOn(reconnectService, 'fetchNotificationsOnReconnect');
const spy = vi.spyOn(reconnectService, 'fetchNotificationsOnReconnect');
await reconnectService.handleRouteSpecificFetch();
expect(spy).toHaveBeenCalled();
});
@@ -320,7 +325,7 @@ describe('ReconnectService', () => {
describe('onDisconnect', () => {
it('should set disconnectTime and call setConversationLastMessageId', () => {
reconnectService.setConversationLastMessageId = jest.fn();
reconnectService.setConversationLastMessageId = vi.fn();
reconnectService.onDisconnect();
expect(reconnectService.disconnectTime).toBeInstanceOf(Date);
expect(reconnectService.setConversationLastMessageId).toHaveBeenCalled();
@@ -329,8 +334,8 @@ describe('ReconnectService', () => {
describe('onReconnect', () => {
it('should handle route-specific fetch, revalidate caches, and emit WEBSOCKET_RECONNECT_COMPLETED event', async () => {
reconnectService.handleRouteSpecificFetch = jest.fn();
reconnectService.revalidateCaches = jest.fn();
reconnectService.handleRouteSpecificFetch = vi.fn();
reconnectService.revalidateCaches = vi.fn();
await reconnectService.onReconnect();
expect(reconnectService.handleRouteSpecificFetch).toHaveBeenCalled();
expect(reconnectService.revalidateCaches).toHaveBeenCalled();
@@ -0,0 +1,61 @@
import { describe, it, expect, vi } from 'vitest';
import { cloneObject } from '../clone'; // Update this path
describe('cloneObject', () => {
it('should clone a simple object', () => {
const original = { a: 1, b: 'string', c: true };
const cloned = cloneObject(original);
expect(cloned).toEqual(original);
expect(cloned).not.toBe(original);
});
it('should clone a nested object', () => {
const original = { a: 1, b: { c: 2, d: { e: 3 } } };
const cloned = cloneObject(original);
expect(cloned).toEqual(original);
expect(cloned.b).not.toBe(original.b);
expect(cloned.b.d).not.toBe(original.b.d);
});
it('should clone an array', () => {
const original = [1, 2, [3, 4]];
const cloned = cloneObject(original);
expect(cloned).toEqual(original);
expect(cloned).not.toBe(original);
expect(cloned[2]).not.toBe(original[2]);
});
it('should clone a Date object', () => {
const original = new Date();
const cloned = cloneObject(original);
expect(cloned).toEqual(original);
expect(cloned).not.toBe(original);
});
it('should use structuredClone when available', () => {
const structuredCloneSpy = vi.fn(x => x);
global.structuredClone = structuredCloneSpy;
const obj = { a: 1 };
cloneObject(obj);
expect(structuredCloneSpy).toHaveBeenCalledWith(obj);
delete global.structuredClone;
});
it('should fall back to JSON methods when structuredClone is not available', () => {
const jsonParseSpy = vi.spyOn(JSON, 'parse');
const jsonStringifySpy = vi.spyOn(JSON, 'stringify');
const original = { a: 1 };
global.structuredClone = undefined;
cloneObject(original);
expect(jsonStringifySpy).toHaveBeenCalledWith(original);
expect(jsonParseSpy).toHaveBeenCalled();
jsonParseSpy.mockRestore();
jsonStringifySpy.mockRestore();
});
});

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