Compare commits

...
206 Commits
Author SHA1 Message Date
Sojan 4a902fce6d Merge branch 'release/2.1.0' 2022-01-11 19:28:42 -08:00
Sojan 5475746a87 Bump version to 2.1.0 2022-01-11 19:26:23 -08:00
Vishnu NarayananandGitHub acba07cf6e chore: revert GlobalConfig changes (#3702)
Priority to GlobalConfig changes are reverted and for the time being, env vars will take precedence if set.

Fixes #3699
2022-01-11 19:20:23 -08:00
Sojan JoseandGitHub f44be0b1e6 fix: SuperAdmin Improvements (#3733)
- Fix broken access tokens pages
- Fix broken avatar images in the super admin users tab
- Fix broken pagination links in super admin
- Add agent bot tabs

Fixes: #2021
2022-01-11 19:00:00 -08:00
Prithvi TharunandGitHub bec1266242 fix: Update shortcode -> short code (#3708)
Closes #3672
2022-01-11 15:20:52 -08:00
Sojan JoseandGitHub ebb04487a8 chore: Prevent null in Contact JSONB attributes (#3730) 2022-01-11 15:14:55 -08:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
de37975be4 chore(deps): bump google-protobuf from 3.18.1 to 3.19.2 (#3714)
Bumps [google-protobuf](https://github.com/protocolbuffers/protobuf) from 3.18.1 to 3.19.2.
- [Release notes](https://github.com/protocolbuffers/protobuf/releases)
- [Changelog](https://github.com/protocolbuffers/protobuf/blob/master/generate_changelog.py)
- [Commits](https://github.com/protocolbuffers/protobuf/compare/v3.18.1...v3.19.2)

---
updated-dependencies:
- dependency-name: google-protobuf
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-01-11 13:25:44 -08:00
Axel TietjeandGitHub 8a26d78623 chore: fix german translations (#3724) 2022-01-12 00:36:57 +05:30
Aswin Dev P.SandGitHub 7ee7062843 feat: Toggle to disallow users to send messages after a conversation is resolved (#3605) 2022-01-11 00:32:03 -08:00
Tejaswini ChileandGitHub a0884310f4 feat: Save automation rules (#3359) 2022-01-10 12:41:59 +05:30
Anisha-artandGitHub 9a9462f5cb fix: Removed extra space before "." (#3717)
Fixes Removed extra space before "." as mentioned in the issue: #3669
2022-01-09 22:27:45 -08:00
Prithvi TharunandGitHub 9b6f1df871 fix: Update copy in canned responses (#3716)
Fixes #3669
2022-01-09 22:26:35 -08:00
Anisha-artandGitHub ab4d63a006 Error with plurals in Invite members onboarding guide. Issue #3671 (#3718)
Updated "email address" to "email addresses" in the Invite your team members guide.
2022-01-10 10:29:16 +05:30
Fayaz AhmedandGitHub 0745ceb430 Chore: Update ninja keys to 1.1.9 (#3710) 2022-01-07 13:52:08 -08:00
Pranav Raj SandGitHub f7ed7756d9 chore: Use logo thumbnail instead of logo in the sidebar (#3706) 2022-01-06 15:09:01 -08:00
Muhsin KelothandGitHub e1fe4f09bc chore: Add subscription delete API (#3703) 2022-01-06 21:40:43 +05:30
Nayan PatelandGitHub a61dae0bb2 General updates and add CODEOWNERS (#3685) 2022-01-05 12:37:22 +05:30
Nithin David ThomasandGitHub fade94658a chore: Show scrollbar for sidebar only on hover (#3688) 2022-01-05 09:37:43 +05:30
Vishnu NarayananandGitHub e775d22b98 feat: add new view and controller to display app configs together (#3563)
fixes: #3578
2022-01-04 21:50:16 +05:30
Tejaswini ChileandGitHub e304ad27cb Chore: Custom filter api docs updated (#3668) 2022-01-04 13:10:29 +05:30
Prithvi TharunandGitHub 3e8ae3865f chore: Update the copy for new password in profile settings (#3676)
Fixes #3656
2022-01-02 22:43:50 -08:00
Nithin David ThomasandGitHub 223fb2b7e4 chore: Updates design of resolve context menu (#3667) 2021-12-29 22:24:11 +05:30
Nithin David ThomasandGitHub 666028a443 feat: Adds multiple path support for fluent icons (#3665)
* feat: Adds multiple path support for icons

* Changes dashboard icon
2021-12-29 18:01:49 +05:30
Sojan 8bdd7ba3e4 Merge branch 'release/2.0.2' 2021-12-28 21:56:59 +05:30
Sojan a1c77c8c4c Bump version to 2.0.2 2021-12-28 21:55:57 +05:30
Sojan JoseandGitHub 577e9905c6 chore: Fix issue with conversation assignment notifications (#3661)
- fixes issues with conversation assignment notifications not working in certain cases
2021-12-28 21:53:13 +05:30
Muhsin KelothandGitHub 9606abe251 chore: Fix Eslint warnings (#3654)
* Fix all the eslint warnings

* Revert the schema
2021-12-27 13:49:31 +05:30
Sojan 28da30f7e4 Merge branch 'release/2.0.1' into develop 2021-12-23 01:04:10 +05:30
Sojan 9f663ed4e5 Merge branch 'release/2.0.1' 2021-12-23 01:04:02 +05:30
Sojan acc55ea79c Bump version to 2.0.1 2021-12-23 01:02:56 +05:30
Sojan JoseandGitHub 13d6734977 chore: Minor API cleanups (#3645)
- exception list updated
- revert mail presenter changes
2021-12-23 00:55:49 +05:30
Tejaswini ChileandGitHub c57c975a0d bug: NoMethodError: undefined method `match' for in_reply_to (#3641)
Fixes #3615
2021-12-22 22:33:18 +05:30
Tejaswini ChileandGitHub 44486fc8e1 chore: Improve email parsing using email trimmer gem (#3611)
Email parsing using email_trimmer gem

Fixes: #3539 , #2954, #3572
2021-12-22 18:16:40 +05:30
Sojan JoseandGitHub 009abc1948 chore: API improvements (#3637)
- Unique validations for Inbox members and Team member objects
- Move notification processing to Async
2021-12-21 22:48:01 +05:30
Muhsin KelothandGitHub 262474166f chore: Add display id in all conversation notifications (#3635)
- Added conversation display id in push payload for the notifications conversation_creation and conversation_assignment
2021-12-21 19:59:22 +05:30
Nithin David ThomasandGitHub 307118b235 feat: Shows error message with retry for widget messages (#3594)
- Adds error message retry option for widget bubbles
- Adds a fallback for widget images with file type bubble
2021-12-21 12:02:43 +05:30
Sivin VargheseandGitHub 0130e08016 fix: Remove animated running numbers (#3604) 2021-12-20 18:19:59 -08:00
Nithin David ThomasandGitHub 83655f4ca4 feat: Show errors for message bubbles in dashboard (#3585) 2021-12-21 00:07:33 +05:30
6fe5484119 chore: Allow more filetypes in uploads (#3557)
- Allowing the ability to upload more common file types like zip, Docx etc
- Fallback for image bubble when the image URL isn't available

fixes: #3270

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Nithin David <1277421+nithindavid@users.noreply.github.com>
2021-12-20 23:50:37 +05:30
Tejaswini ChileandGitHub 76e8acd3c6 Fix: Editable attrs presence check for the channel class (#3630) 2021-12-20 19:38:38 +05:30
Sivin VargheseandGitHub ad09c5dc5f bug: Fixes pressing enter key not submitting values in widget. (#3610) 2021-12-20 08:48:09 +05:30
Sojan JoseandGitHub f8cf0cd7c5 chore: Enable Slovak Language (#3614) 2021-12-19 11:38:21 +05:30
Sojan JoseandGitHub 46d2a8f218 chore: New Crowdin updates (#3616) 2021-12-19 11:38:02 +05:30
Amit KumarandGitHub 6069cd9eba chore: Contact builder ignores custom attributes (#3571)
This is incorrect as per the documentation here: https://www.chatwoot.com/developers/api/#operation/create-a-contact
This allows setting the proper attributes when creating a contact
2021-12-19 10:59:03 +05:30
Sojan JoseandGitHub 8d65608666 chore: Clean up migrations (#3613)
Fixes: #3530
2021-12-19 09:47:07 +05:30
Aswin Dev P.SandGitHub 5ee209c079 chore: Fix user email re-confirmation flow (#3581)
Users can change their email from profile settings. They will be logged out immediately. Users can log in again with the updated email without verifying the same. This is a security problem.

So this change enforce the user to reconfirm the email after changing it. Users can log in with the updated email only after the confirmation.

Fixes: https://huntr.dev/bounties/7afd04b4-232e-4907-8a3c-acf8bd4b5b22/
2021-12-16 19:32:49 +05:30
Sivin VargheseandGitHub e0c9687f5e bug: Fixes irregular padding on agent selector in report page (#3593)
* bug: Fixes irregular padding on agent selector in report page

* code climate fixes
2021-12-16 13:20:35 +05:30
Sivin VargheseandGitHub 93f384a3ec enhancement: Changes button design for tables in settings page (#3553)
* enhancement: Changes button design for tables in settings page

* Minor fixes

* Review fixes
2021-12-16 13:00:55 +05:30
Tejaswini ChileandGitHub 9984edd3ef Feat: Manage conversation for tweets based on the tweet flag (#3353)
Add tweet conversation only if tweets are enabled.

Fixes #1961
2021-12-16 00:24:50 +05:30
Sivin VargheseandGitHub e2e459a1ac bug: Fixes space missing issue with message bubbles after sending messages (#3562) 2021-12-15 19:12:11 +05:30
Tejaswini ChileandGitHub b7336eca65 Fix: Add max retries for all jobs (#3582) 2021-12-15 17:31:16 +05:30
Nithin David ThomasandGitHub 6ae06f2c7a fix: Fixes wrong color for note tab in reply box (#3566)
* fix: Fixes wrong color for note tab in reply box

* Changes color for warning button

* Fixes spacing
2021-12-14 20:09:52 +05:30
Vishnu NarayananandGitHub 7986d61b6f feat: allow users to select components to install when using linux script (#3528)
Provide a choice to select the components to install. The script
should be able to just install chatwoot alone. Postgres, redis,
SSL certs via letsencrypt and nginx as a webserver should be
installed after confirmation.

Fixes: #3513
2021-12-14 18:26:57 +05:30
Nithin David ThomasandGitHub c2191e48b1 Chore: Fixes icon size in button (#3555)
* Chore: Fixes icon size in button

* Changes normal button to woot button
2021-12-14 17:37:25 +05:30
Vishnu NarayananandGitHub ac372f188d chore: add line messenger in readme (#3552) 2021-12-13 14:19:12 +05:30
Sivin VargheseandGitHub 178cb34319 bug: Fixes expanded reply box is not closing after the message is sent (#3537) 2021-12-13 12:37:21 +05:30
Tejaswini ChileandGitHub c2519ea1ea Fix: fixing mail to and in_reply_to issues (#3451) 2021-12-10 19:42:26 +05:30
Pranav Raj SandGitHub dcdeaa7954 chore: Remove license tag from Readme 2021-12-09 02:30:33 -08:00
arabakevinandGitHub 0b7eb2e2c1 chore: Fix typo in enterprise license (#3535) 2021-12-09 02:22:37 -08:00
Pranav Raj SandGitHub 2defe4a8fe chore: Fix line breaks in Readme.md 2021-12-09 02:19:16 -08:00
Pranav Raj SandGitHub b0af3f2498 chore: Update Readme.md 2021-12-09 02:16:05 -08:00
Sojan JoseandGitHub 5feb5aba72 Merge tag 'v2.0.0' into develop
v2.0.0
2021-12-09 08:17:34 +00:00
Sojan JoseandGitHub a7623041bf Merge branch 'release/2.0.0' 2021-12-09 08:17:15 +00:00
Sojan JoseandGitHub d268f155e4 Bump version to 2.0.0 2021-12-09 06:59:23 +00:00
b1eea7f7d1 chore: Introduce enterprise edition license (#3209)
- Initialize an "enterprise" folder that is copyrighted.
- You can remove this folder and the system will continue functioning normally, in case you want a purely MIT licensed product.
- Enable limit on the number of user accounts in enterprise code.
- Use enterprise edition injector methods (inspired from Gitlab).
- SaaS software would run enterprise edition software always.

Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2021-12-09 12:07:48 +05:30
Pranav Raj SandGitHub 2f63ebb8a6 fix: Add fixes for sentry errors (#3522)
- Add fixes for sentry errors
2021-12-09 11:50:28 +05:30
Pranav Raj SandGitHub 2be71e73dc feat: Add a view for mentions (#3505)
- Added a new table mentions for saving user mentions
- Added a filter conversation_type in the API
- Added a view to see the mentions
2021-12-09 11:20:14 +05:30
Sojan JoseandGitHub 1db82f235d Chore: Improve confirmation flow for agents (#3519)
- Agents are redirected to the password reset page which confirms the agent as well as sets a new password.
2021-12-09 11:11:46 +05:30
9306b725d8 chore: Fix pre-commit hooks (#3525)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2021-12-08 17:32:00 -08:00
Muhsin KelothandGitHub 3ee6b7d6e9 chore: Add the ability to use same key for conversation and contact (#3526) 2021-12-08 20:15:27 +05:30
Tejaswini ChileandGitHub 35130bc83e Fix: Add foreign key with delete cascade for destroy_async (#3527) 2021-12-08 18:03:27 +05:30
Tejaswini ChileandGitHub f609a28c6b Fix: Added facebook login scope with new advanced access for page read (#3518) 2021-12-08 16:17:49 +05:30
Sivin VargheseandGitHub 714c10dc66 bug: Fixes error with the secondary menu. (#3524) 2021-12-08 14:20:59 +05:30
f5d79ffe58 chore: Updates icon for network disconnect alert (#3514)
* chore: Upadates icon for network disconnect alert

* Changes background color

* Review fixes

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2021-12-07 23:45:36 +05:30
Nithin David ThomasandGitHub 824101bc30 fix: Fixes cc bcc being sent when mail head is empty (#3515)
* fix: Fixes cc bcc being sent wrongly with emails

* fixes values not sycned to parent component

* Update app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue

* Review fixes
2021-12-07 23:44:32 +05:30
Muhsin KelothandGitHub 46afcd9348 fix: Index custom attribute uniqueness on account (#3516)
- Move custom attribute uniqueness validation to account
2021-12-07 18:20:47 +05:30
Nithin David ThomasandGitHub 6b025816f5 fix: Update sidebar toggle icon breaking message view (#3512) 2021-12-06 08:41:34 -08:00
Sivin VargheseandGitHub 3c72a2759f enhancement: Improves sidebar open/close toggle design (#3507) 2021-12-06 18:16:36 +05:30
a98f10130b bug: Fix cannot copy text content from sidebar (#3506)
* bug: Fixes cannot copy text content from sidebar .

* fixes cannot copy issue in sidebar

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2021-12-06 14:38:04 +05:30
Muhsin KelothandGitHub b46ee5e580 Fix: Conversation status event in widget side (#3509) 2021-12-03 22:45:00 +05:30
thedev105andGitHub 6d378eb206 Fix: Update contact validation messages (#3500)
Fixes #3476
2021-12-03 13:19:11 +05:30
d7cfe6858e feat: Add advanced contact filters (#3471)
Co-authored-by: Tejaswini <tejaswini@chatwoot.com>
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2021-12-02 19:12:44 -08:00
Sivin VargheseandGitHub 1c29f5bbe4 bug: Fixes missing inbox icon in Twillio SMS channel. (#3502) 2021-12-02 11:47:49 +05:30
8b4134c790 fix: Update route permissions in the new primary menu (#3499)
* fix: Display rolewise primary sidebar

* Fix issues with roles

* Fix active style

* Fix accessible menu

* Fix key missing

* Changes menu icon size

Co-authored-by: Nithin David <1277421+nithindavid@users.noreply.github.com>
2021-12-02 11:02:43 +05:30
Sojan JoseandGitHub b826319776 chore: create CONTRIBUTING.md 2021-12-01 23:21:02 +05:30
Muhsin KelothandGitHub 025bbf1bfb chore: Add i18n survey transaltion in crowdin (#3496) 2021-12-01 18:25:22 +05:30
Nithin David ThomasandGitHub 2a5932cf1b chore: Fixes error in reply box code (#3494) 2021-12-01 16:04:02 +05:30
0a18993b70 fix: Changes thumbnail colors to new color scheme (#3483)
* Enhancement: Updates sidebar to a new design (#2733)

* feat: Changes primary navbar to new design (#2598)

* feat: updates design for secondary navbar (#2612)

* Changes primary nvbar to new design

* Updates design for contexual sidebar

* Fixes issues with JSON

* Remove duplication of notificatons in Navigation

* Fixes broken tests

* Fixes broken tests

* Update app/javascript/dashboard/components/layout/AvailabilityStatus.vue

* Update app/javascript/dashboard/components/layout/AvailabilityStatus.vue

* Update app/javascript/dashboard/components/layout/SidebarItem.vue

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>

* Update app/javascript/dashboard/components/layout/SidebarItem.vue

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>

* Update app/javascript/dashboard/modules/sidebar/components/Secondary.vue

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>

* Chore: Update design changes to features

* Fixes menu transitions and refactors code

* Refactors sidebar routeing logic

* lint error fixes

* Fixes dropdown menu styles

* Fixes secondary new item links

* Fixes lint scss issues

* fixes linter issues

* Fixes broken test cases

* Update AvailabilityStatus.spec.js

* Review feedbacks

* Fixes add modal for label

* Add tooltip for primary menu item

* Tooltip for notifications

* Adds tooltip for primary menu items

* Review fixes

* Review fixes

* Fix merge issues

* fixes logo size for login pages

* fixes Merge breaks with styles

* fix: Changes thumbnail colors to new color scheme

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2021-12-01 15:18:41 +05:30
b01d032d0d feat: Updates sidebar to accomodate sub menu (#3416)
* Enhancement: Updates sidebar to a new design (#2733)

* feat: Changes primary navbar to new design (#2598)

* feat: updates design for secondary navbar (#2612)

* Changes primary nvbar to new design

* Updates design for contexual sidebar

* Fixes issues with JSON

* Remove duplication of notificatons in Navigation

* Fixes broken tests

* Fixes broken tests

* Update app/javascript/dashboard/components/layout/AvailabilityStatus.vue

* Update app/javascript/dashboard/components/layout/AvailabilityStatus.vue

* Update app/javascript/dashboard/components/layout/SidebarItem.vue

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>

* Update app/javascript/dashboard/components/layout/SidebarItem.vue

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>

* Update app/javascript/dashboard/modules/sidebar/components/Secondary.vue

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>

* Chore: Update design changes to features

* Fixes menu transitions and refactors code

* Refactors sidebar routeing logic

* lint error fixes

* Fixes dropdown menu styles

* Fixes secondary new item links

* Fixes lint scss issues

* fixes linter issues

* Fixes broken test cases

* Update AvailabilityStatus.spec.js

* Review feedbacks

* Fixes add modal for label

* Add tooltip for primary menu item

* Tooltip for notifications

* Adds tooltip for primary menu items

* Review fixes

* Review fixes

* Fix merge issues

* fixes logo size for login pages

* fixes Merge breaks with styles

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2021-12-01 12:45:39 +05:30
Pranav Raj SandGitHub c792cfc0be feat: Use Fluent SVG icons on the dashboard (#3482) 2021-11-30 21:03:18 -08:00
Pranav Raj SandGitHub 98be3b33c1 fix: Update missing method onNewToastMessage (#3481) 2021-11-30 10:57:44 -08:00
Sojan JoseandGitHub 0115b4ddc4 chore: validation spec for schedules (#3480)
We had instances where after copy-pasting the schedule block, the dev forgets to change the schedule key,
This would break some of the scheduled jobs without explicit errors.

this PR add a spec that will prevent it from happening
2021-11-30 21:44:30 +05:30
Sojan JoseandGitHub d5c30760a7 chore: Whatsapp templates fix for 360 dialog (#3479)
Fixes: #3426
2021-11-30 20:50:35 +05:30
Pranav Raj SandGitHub 0899f62912 fix: Use bus.$off to remove listeners on destroy (#3478) 2021-11-30 10:45:36 +05:30
Sojan JoseandGitHub 86dfdfb9ab chore: fix scheduled jobs (#3473)
- fixes the duplicate schedule job key
2021-11-27 17:08:40 +05:30
Shivam ChaharandGitHub bfcde9b022 chore: Conversation history in email notifications (#3414)
Display recent messages in the notification email when a new conversation is created.

Fixes: #2041
2021-11-27 15:38:27 +05:30
Akhil G KrishnanandGitHub 6a98a812e7 Fix: Outgoing email sender header translation issue (#3393)
Outgoing email-sender header translation fix for "from"

Fixes #3380
2021-11-27 09:46:12 +05:30
Shivam ChaharandGitHub a4ee8ee239 Chore: Improve conversation auto resolution (#3405)
Fixes: #1583
2021-11-27 09:26:18 +05:30
SanjuandGitHub 89f9f39e6e chore: add account id in account settings page (#3365)
Fixes: #3317
2021-11-27 09:07:09 +05:30
Santhosh CandGitHub b326da7d94 feat: Ability to remove user profile picture (#3156)
Fixes #3107
2021-11-27 00:56:07 +05:30
Sojan JoseandGitHub add004a56e Chore: API Improvements (#3469) 2021-11-27 00:48:46 +05:30
Sivin VargheseandGitHub 55843c5bad chore: fixes filter input multiselect height issue (#3466) 2021-11-26 13:41:09 +05:30
2c3c1888f1 Enhancement: Show unread marker on widget bubble (#3390)
* add unread marker

* add removeClass in IFramehelper

* Update app/javascript/sdk/sdk.js

* Update app/javascript/sdk/sdk.js

Co-authored-by: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com>
2021-11-25 20:25:14 +05:30
Tejaswini ChileandGitHub b0370ce44d Feat: Fix contact filter pagination (#3463) 2021-11-25 12:01:31 +05:30
Vishnu NarayananandGitHub 3a48e08fe0 feat: migrate facebook env vars to globalConfig (#3369)
Migrate facebook env vars to globalConfig and make it editable from the super admin UI.
2021-11-25 00:55:26 +05:30
Nithin David ThomasandGitHub c23e2c23d4 chore: Update design for filters (#3423) 2021-11-24 14:26:32 +05:30
Tejaswini ChileandGitHub 330a97b5c5 Fix: contact filter values (#3456) 2021-11-24 12:58:00 +05:30
Muhsin KelothandGitHub 00143f7ee5 fix: Render checkbox if string value passed (#3453) 2021-11-24 11:02:45 +05:30
Sivin VargheseandGitHub da8f9d0337 feat: Add support for List and Checkbox in Custom Attributes (#3439) 2021-11-24 09:00:55 +05:30
Pranav Raj SandGitHub 0530e9491c fix: Use role permissions from accounts instead of role attribute (#3445)
Use role permissions from accounts instead of the role attribute

Fixes: #2557
2021-11-23 17:37:01 +05:30
Tejaswini ChileandGitHub 11cd7fd6c5 Feat: Added support changes for contact filters (#3435) 2021-11-23 16:36:44 +05:30
Tejaswini ChileandGitHub 9103fbbdde chore: Update schema files (#3438)
- removes the unintended table from schema files
2021-11-23 16:04:01 +05:30
Sivin VargheseandGitHub e182b59002 bug: fixes link icon doesn't work in the campaign editor (#3447) 2021-11-23 15:26:26 +05:30
Fayaz AhmedandGitHub 7f8204a9da fix: Required check for Conversation filters (#3442) 2021-11-23 09:42:47 +05:30
Sojan 0033a35ab8 Merge branch 'hotfix/1.22.1' into develop
# Conflicts:
#	db/schema.rb
2021-11-22 23:35:44 +05:30
Sojan 5db9d397c5 Merge branch 'hotfix/1.22.1' 2021-11-22 23:34:37 +05:30
791d90c6b7 chore: Migrate PubSub Token to contact inbox (#3434)
At present, the websocket pubsub tokens are present at the contact objects in chatwoot. A better approach would be to have these tokens at the contact_inbox object instead. This helps chatwoot to deliver the websocket events targetted to the specific widget connection, stop contact events from leaking into other chat sessions from the same contact.

Fixes #1682
Fixes #1664

Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2021-11-22 23:32:17 +05:30
Pranav Raj SandGitHub 1a8b8f0900 chore: Move referrerHost to appConfig store (#3433) 2021-11-22 15:05:29 +05:30
Vishnu NarayananandGitHub 1be7c2d72c chore: add DigitialOcean 1-click deployment button (#3421) 2021-11-22 13:20:35 +05:30
Pranav Raj SandGitHub e93e43532a fix: Pass label title instead of id to filter API (#3432) 2021-11-22 12:06:43 +05:30
Sojan JoseandGitHub c332e2df12 chore: Add Code of Conduct 2021-11-20 12:08:45 +05:30
Pranav Raj SandGitHub da163cc1f7 chore: Move unscoped styles to global stylesheet (#3422) 2021-11-19 16:09:20 +05:30
Tejaswini ChileandGitHub 7b44bf5b78 Fix: Filter with label name (#3418) 2021-11-19 13:55:40 +05:30
Aswin Dev P.SandGitHub 24e6a92297 feat: IMAP Email Channel (#3298)
This change allows the user to configure both IMAP and SMTP for an email inbox. IMAP enables the user to see emails in Chatwoot. And user can use SMTP to reply to an email conversation.

Users can use the default settings to send and receive emails for email inboxes if both IMAP and SMTP are disabled.

Fixes #2520
2021-11-19 11:52:27 +05:30
Fayaz AhmedandGitHub 8384d0b38e chore: Update dropdown z-index to be on top of contact details pane (#3411) 2021-11-18 19:55:32 +05:30
c2333214af feat: Add Advanced Conversation Filters (#3239)
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
Co-authored-by: Tejaswini <tejaswini@chatwoot.com>
2021-11-18 19:15:02 +05:30
Muhsin KelothandGitHub bf96e8b68d choore: Change values to attribute_values in custom attributes (#3413) 2021-11-18 18:59:28 +05:30
Pranav Raj SandGitHub a3ba8f9e35 feat: Remove ionicons v2 dependency on widget (#3402) 2021-11-18 15:18:51 +05:30
Muhsin KelothandGitHub c2b4991fd8 feat: Add automation route (#3410) 2021-11-18 14:09:55 +05:30
Akhil G KrishnanandGitHub b81a9f2010 Chore: Replaced dependent destroy with dependent destroy_async in all models (#3249) 2021-11-18 10:32:29 +05:30
VinayakandGitHub 4eeaadbd5b chore: Added SMTP_TLS and SMTP_SSL in .env.example
Added SMTP_TLS and SMTP_SSL in .env.example

Fixes #3396
2021-11-17 22:11:27 +05:30
Jan-DavidandGitHub e500d1216b Fix: Make swagger doc compliant to OpenAPI (#3394)
This fixes issues in the swagger.json file. The motivation to do so is to be able to generate API clients using https://openapi-generator.tech Doing so will require further changes to the api spec, but this seems like a good first step since it is now "valid" according to editor.swagger.io and openapi-generator validate.

Fixes #2806
2021-11-17 18:55:15 +05:30
Pranav Raj SandGitHub d227a56747 fix: Set default value of sortBy to empty string (#3406) 2021-11-17 18:27:14 +05:30
Shivam ChaharandGitHub fec4a29081 Feat: Include previous messages in mention/assign notifications email (#3385) 2021-11-17 18:05:53 +05:30
Muhsin KelothandGitHub 9ea42ebff2 chore: Add the support for list and checkbox in custom attribute model (#3400)
ref: #3399
2021-11-16 23:25:19 +05:30
Sojan e5a80195ab Merge branch 'master' into develop 2021-11-15 21:52:59 +05:30
Sojan 928580dd41 Merge branch 'release/1.22.0' into develop 2021-11-15 21:50:54 +05:30
Sojan 01577acb2e Merge branch 'release/1.22.0' 2021-11-15 21:50:36 +05:30
Sojan 52c850f8b6 Bump version to 1.22.0 2021-11-15 21:49:14 +05:30
Pranav Raj SandGitHub 809509404b fix: Set cc, bcc default to empty string if unavailable (#3387) 2021-11-15 21:05:24 +05:30
Sojan JoseandGitHub acb06e7df6 chore: Prevent notification email loop (#3386)
Configuring an agent email also as a support email inbox leads to conversations getting created in a loop if notifications were also configured to the same email.
2021-11-15 19:15:51 +05:30
e849759e15 feat: Add Command bar for improved productivity (#3352)
Co-authored-by: Fayaz Ahmed <15716057+fayazara@users.noreply.github.com>
2021-11-15 17:22:44 +05:30
1e3f255ece chore: Update translations, fix Crowdin issues
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2021-11-15 17:18:56 +05:30
Muhsin KelothandGitHub a2764e5c1d chore: Update deleteCustomAttribute method in SDK (#3334) 2021-11-15 14:56:35 +05:30
Muhsin KelothandGitHub c2db8a1fd7 chore: Add the option to edit custom attribute display name (#3382) 2021-11-15 12:45:47 +05:30
Muhsin KelothandGitHub bb18516403 chore: Update conversation sidebar items default order (#3381) 2021-11-15 10:34:57 +05:30
Muhsin KelothandGitHub d1a62fe6ab feat: Add the option for changing the contact/conversation sidebar items order (#3362) 2021-11-13 12:32:53 +05:30
Sojan JoseandGitHub f68a4b55bb chore: Update the translations for CHAT_STATUS_FILTER_ITEMS (#3375) 2021-11-13 11:53:59 +05:30
Sojan JoseandGitHub 987c8f5c7c chore: Fix translations breaking the chat filter (#3374)
fixes: #2552
2021-11-13 11:45:18 +05:30
Sojan JoseandGitHub e011b4566b chore: New Translation updates (#3371) 2021-11-12 23:40:08 +05:30
Tejaswini ChileandGitHub 37a36349f2 feat: Update Swagger for the custom filter endpoint (#3367) 2021-11-12 19:28:03 +05:30
cf5f6d5a74 feat: enable Dialogflow integration all inboxes except email (#3285)
Fixes #2461

Co-authored-by: Sojan Jose <sojan@pepalo.com>
2021-11-12 17:53:03 +05:30
Sojan JoseandGitHub d78cb67a2a chore: Fix conversation status in webhooks (#3364)
- fix the wrong conversation status being sent in webhooks
- additional information in websocket events
- refactor activity messaging code
- move activity message generation to background job to stop the callback loop
2021-11-12 16:17:59 +05:30
b119d9e729 fix: Remove * as import from conversation module (#3366)
* fix: Remove * as import from conversation module

* Remove * as import from conversation test spec

Co-authored-by: Fayaz Ahmed <15716057+fayazara@users.noreply.github.com>
2021-11-12 13:41:43 +05:30
Tejaswini ChileandGitHub 564fa5f392 feat: Update custom filter for present/not_present operator (#3361) 2021-11-12 11:30:39 +05:30
Vishnu NarayananandGitHub 58aae7a4a9 fix: make GlobalConfig migrated from .env editable from UI (#3360) 2021-11-11 19:31:25 +05:30
Muhsin KelothandGitHub c6326993df feat: Show pre-chat form before triggering the campaign (#3215) 2021-11-11 19:02:16 +05:30
Muhsin KelothandGitHub 76370267f3 feat: Render contact custom attributes in contact/conversation sidebar (#3310) 2021-11-11 15:23:33 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
e12edb51a2 chore: Bump axios from 0.21.1 to 0.21.2 (#3358)
Bumps [axios](https://github.com/axios/axios) from 0.21.1 to 0.21.2.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/master/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v0.21.1...v0.21.2)

---
updated-dependencies:
- dependency-name: axios
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-11 15:05:59 +05:30
JakobandGitHub 1dfa173b3a fix: Limit rails, postgres and redis container access to localhost (#3354)
This change limits the rails, redis and postgres container on `docker-compose.production.yaml` file to localhost only. 

The default docker-compose configuration will expose redis, postgres and rails directly to the internet when the service is started on a virtual machine.

In most cases that is not what you want, and especially for redis and postgres exposing the services could be a potential security risk. By adding 127.0.0.1 access is limited to localhost and access is only possible after nginx oder another web server is configured as reverse proxy.

Note: Moving forward, anyone using docker-compose.production.yaml need to have something like Nginxto proxy the requests to the container.

If you want to verify whether the installation is working, try curl -I localhost:3000 to see if it returns 200. Also, you could temporarily drop the 127:0.0.1:3000:3000 for rails to 3000:3000 to access your instance at http://:3000. It's recommended to revert this change back and use Nginx in front.

Approved-by: Vishnu Narayanan <vishnu@chatwoot.com>
2021-11-11 14:43:25 +05:30
Sojan JoseandGitHub a4c87f2052 chore: Handle attachments in Whatsapp Channel (#3299)
send and receive attachments in 360Dialog WhatsApp channels
2021-11-11 13:03:48 +05:30
sarzynskiandGitHub b3e313a200 fix: Ensure HMAC mandatory when enabled (#3350)
Add missing condition checking if HMAC is mandatory.

Fixes #3349
2021-11-11 09:48:38 +05:30
Tejaswini ChileandGitHub c0f5a59cf8 Bug: Instagram render template message twice (#3313) 2021-11-10 18:32:46 +05:30
Tejaswini ChileandGitHub 358171062e Feat: Add Null values at the last while sorting (#3292)
* Add Null values at the last while sorting
* Add contacts at last with special character in it
* Optimize SQL order and direction
2021-11-10 14:11:00 +05:30
6cfd7d3836 feat: autogenerate vapid keys for push notifications (#3128)
* feat: Autogenerate push notification keys
* add vapid service class and remove pushkey model
* add spec for vapid service
* unset vapid env keys
* Unset VAPID_PRIVATE_KEY env variable

Co-authored-by: Sojan Jose <sojan@chatwoot.com>
Co-authored-by: Vishnu Narayanan <vishnu@chatwoot.com>
2021-11-09 21:36:32 +05:30
Nithin David 0c9b682329 Revert "feat: Updates widget message state to support multiple conversations"
This reverts commit a34bb565af.
2021-11-09 18:34:55 +05:30
Nithin David a34bb565af feat: Updates widget message state to support multiple conversations 2021-11-09 18:23:20 +05:30
Pranav Raj SandGitHub d00b8df952 fix: Revert translated keys in conversation status (#3337) 2021-11-08 17:03:34 +04:00
Akhil G KrishnanandGitHub 4e01278ea8 TEAM_AVAILABILITY dutch translation issue fixed (#3324) 2021-11-08 17:44:02 +05:30
wsuffandGitHub 26f36548c2 chore: Update message limit to Twilio recommended 320 characters (#3333) 2021-11-08 10:27:00 +04:00
A. VerdeandGitHub 81708f92a8 chore: Fcm notification priority (#3319) 2021-11-08 11:32:07 +05:30
Vishnu NarayananandGitHub 97ee1bfa97 feat: add GlobalConfigService to support env vars migration (#3288) 2021-11-03 21:34:42 +04:00
Sivin VargheseandGitHub 03b1a3d045 feat: Adds support for paste attachment with keys. (#3293)
* copy paste files

* Minor fixes

* Adds support for pasting attachment

* Minor files

* Review fixes
2021-11-02 10:05:49 +05:30
Sivin VargheseandGitHub 0651b11fda bug: Fixes the height issue of attachment image preview. (#3296)
* bug: Fixes the height issue of attachment image preview

* fixes codeclimate issue
2021-11-01 21:00:14 +05:30
Tejaswini ChileandGitHub 372bd75028 feat: Add filter APIs for Contacts and Conversations (#3264) 2021-11-01 12:27:04 +04:00
Hugo SartiandGitHub 68fa694268 fix: Disable typing indicator for user when agent is typing a private note (#3303) 2021-11-01 12:20:07 +04:00
Tejaswini ChileandGitHub 000c7603d6 feat/3247 Escape html tags from the slack messages (#3306) 2021-11-01 12:08:10 +05:30
Muhsin KelothandGitHub a3b6e5655d Disallow editing attribute key and type (#3305) 2021-10-30 12:14:09 +05:30
Muhsin KelothandGitHub ab77e03c92 feat: Render conversation custom attributes (#3065) 2021-10-30 07:09:46 +05:30
Sojan JoseandGitHub 69f55a25b6 chore: Minor API fixes (#3290)
- Log whether web-hook was successful
- Inbox update shouldn't need channel params
2021-10-28 15:26:20 +05:30
Aswin Dev P.SandGitHub 46867e89cb feat: Ability to specify a subject line for outbound emails (#3168) 2021-10-27 13:09:29 +05:30
Sojan JoseandGitHub 2f2d2b4f73 chore: Update translations (#3289) 2021-10-26 23:01:52 +05:30
fcbe68141d fix: Show twilio channel name based on medium (#3284)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2021-10-26 16:00:00 +05:30
Vishnu NarayananandGitHub ac46d0c855 feat: add domain check prompt for linux installation script (#3282)
- move domain check to beginning to have all user input at the start
- notify user to point DNS if they plan to use a domain with Chatwoot
2021-10-25 21:56:26 +05:30
SanjuandGitHub 74db319a7c feat: Add video preview & player in conversation (#3201) 2021-10-25 19:46:37 +05:30
98e9fedfa6 fix: Resolve contacts filter when entered with contry code (#3241)
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2021-10-25 19:26:11 +05:30
f2c6799444 feat: Update translations (#3279)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2021-10-25 19:23:58 +05:30
Jay ClarkandGitHub 5119e7a79e fix: Update z-index for chevron in contact sidepanel (#3268)
Signed-off-by: Jay Clark <jay@jayeclark.dev>
2021-10-25 19:14:13 +05:30
Pranav Raj SandGitHub 8e6ce3a813 feat: Add notes for Contacts (#3273)
Fixes #2275
2021-10-25 18:35:58 +05:30
Viraj BahulkarandGitHub e5e73a08fe fix: Render missing captions for telegram media attachments (#3257) 2021-10-25 15:52:06 +05:30
Akhil G KrishnanandGitHub 84df9c807d chore: Add climate-control gem for handling the test ENV vars (#3267) 2021-10-25 13:13:25 +05:30
Sojan JoseandGitHub 06289b03ea chore: Add Contact Note APIs (#3266) 2021-10-24 12:40:30 +05:30
Sivin VargheseandGitHub 19855a90e2 fix: Remove duplicate attachment downloads (#3262) 2021-10-22 19:23:05 +05:30
Jan-DavidandGitHub 09643b63b0 fix: Use correct German translation of 'disabled' (#3255) 2021-10-22 10:42:13 +05:30
Tejaswini ChileandGitHub 658a511b88 feat: Conversation and contact endpoint (#3198) 2021-10-20 18:14:56 +05:30
Muhsin KelothandGitHub b0041ccf3f fix: Close button click action in contact details sidepanel (#3245) 2021-10-20 09:05:08 +05:30
050df43789 feat: Adds sound alert notification for new messages on the widget (#3181)
* feat: Adds sound alert notification for new messages on the widget

* Review fixes

* uses absolute path

* Minor fixes

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2021-10-18 06:09:04 -07:00
Sojan 3b3ab39ca3 Merge branch 'release/1.21.1' into develop 2021-10-18 14:34:08 +05:30
Sojan 7015609577 Merge branch 'release/1.21.1' 2021-10-18 14:34:01 +05:30
Sojan 58ad0e6fd6 Bump version to 1.21.1 2021-10-18 14:33:46 +05:30
Sojan b8ea8e5b5e Merge branch 'master' into develop 2021-10-18 14:32:33 +05:30
Vishnu NarayananandGitHub eada8c9af2 chore: Fix puma http-parser error (#3236)
https://github.com/puma/puma/releases/tag/v5.5.2
2021-10-18 14:23:28 +05:30
Pranav Raj SandGitHub 6de964a0bd fix: HTTP parser error in linux installations (#3232) 2021-10-18 14:20:48 +05:30
lybrusandGitHub 9f9dd09985 chore: .env.example MAILER_SENDER_EMAIL put value in quotes (#3226)
Signed-off-by: Damir Rashidov <damir@rashidov.eu>
2021-10-18 14:15:04 +05:30
Sojan a9849e402e Merge branch 'master' into develop 2021-10-16 00:18:47 +05:30
Sojan a2ffcc4502 Merge branch 'release/1.21.0' into develop 2021-10-16 00:13:23 +05:30
1267 changed files with 37918 additions and 10076 deletions
+13 -1
View File
@@ -7,7 +7,7 @@ defaults: &defaults
working_directory: ~/build
docker:
# specify the version you desire here
- image: cimg/ruby:3.0.2-node
- image: cimg/ruby:3.0.2-browsers
# Specify service dependencies here if necessary
# CircleCI maintains a library of pre-built images
@@ -77,6 +77,18 @@ jobs:
paths:
- cc-test-reporter
# verify swagger specification
- run:
name: Verify swagger API specification
command: |
bundle exec rake swagger:build
if [[ `git status swagger/swagger.json --porcelain` ]]
then
echo "ERROR: The swagger.json file is not in sync with the yaml specification. Run 'rake swagger:build' and commit 'swagger/swagger.json'."
exit 1
fi
curl -L https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/5.3.0/openapi-generator-cli-5.3.0.jar > ~/tmp/openapi-generator-cli-5.3.0.jar
java -jar ~/tmp/openapi-generator-cli-5.3.0.jar validate -i swagger/swagger.json
# Database setup
- run: yarn install --check-files
- run: bundle exec rake db:create
+27 -19
View File
@@ -1,4 +1,4 @@
version: "2"
version: '2'
plugins:
rubocop:
enabled: false
@@ -17,22 +17,30 @@ checks:
method-count:
enabled: true
config:
threshold: 25
threshold: 32
file-lines:
enabled: true
config:
threshold: 300
exclude_patterns:
- "spec/"
- "**/specs/"
- "db/*"
- "bin/**/*"
- "db/**/*"
- "config/**/*"
- "public/**/*"
- "vendor/**/*"
- "node_modules/**/*"
- "lib/tasks/auto_annotate_models.rake"
- "app/test-matchers.js"
- "docs/*"
- "**/*.md"
- "**/*.yml"
- "app/javascript/dashboard/i18n/locale"
- "**/*.stories.js"
- "stories/"
- 'spec/'
- '**/specs/'
- 'db/*'
- 'bin/**/*'
- 'db/**/*'
- 'config/**/*'
- 'public/**/*'
- 'vendor/**/*'
- 'node_modules/**/*'
- 'lib/tasks/auto_annotate_models.rake'
- 'app/test-matchers.js'
- 'docs/*'
- '**/*.md'
- '**/*.yml'
- 'app/javascript/dashboard/i18n/locale'
- '**/*.stories.js'
- 'stories/'
- 'app/javascript/dashboard/components/widgets/conversation/advancedFilterItems/index.js'
- 'app/javascript/shared/constants/countries.js'
- 'app/javascript/dashboard/components/widgets/conversation/advancedFilterItems/languages.js'
- 'app/javascript/dashboard/routes/dashboard/contacts/contactFilterItems/index.js'
+4 -1
View File
@@ -41,7 +41,7 @@ RAILS_MAX_THREADS=5
# The email from which all outgoing emails are sent
# could user either `email@yourdomain.com` or `BrandName <email@yourdomain.com>`
MAILER_SENDER_EMAIL=Chatwoot <accounts@chatwoot.com>
MAILER_SENDER_EMAIL="Chatwoot <accounts@chatwoot.com>"
#SMTP domain key is set up for HELO checking
@@ -57,6 +57,9 @@ SMTP_AUTHENTICATION=
SMTP_ENABLE_STARTTLS_AUTO=true
# Can be: 'none', 'peer', 'client_once', 'fail_if_no_peer_cert', see http://api.rubyonrails.org/classes/ActionMailer/Base.html
SMTP_OPENSSL_VERIFY_MODE=peer
# Comment out the following environment variables if required by your SMTP server
# SMTP_TLS=
# SMTP_SSL=
# Mail Incoming
# This is the domain set for the reply emails when conversation continuity is enabled
+1
View File
@@ -28,6 +28,7 @@ module.exports = {
}],
'vue/html-self-closing': 'off',
"vue/no-v-html": 'off',
'vue/singleline-html-element-content-newline': 'off',
'import/extensions': ['off']
},
+5
View File
@@ -0,0 +1,5 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npm run eslint
bundle exec rubocop -a
git add
+4
View File
@@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
sh bin/validate_push
+5 -1
View File
@@ -11,9 +11,11 @@ Metrics/ClassLength:
Max: 125
Exclude:
- 'app/models/conversation.rb'
- 'app/models/contact.rb'
- 'app/mailers/conversation_reply_mailer.rb'
- 'app/models/message.rb'
- 'app/builders/messages/facebook/message_builder.rb'
- 'app/controllers/api/v1/accounts/contacts_controller.rb'
RSpec/ExampleLength:
Max: 25
Style/Documentation:
@@ -56,7 +58,7 @@ Metrics/BlockLength:
- db/schema.rb
Metrics/ModuleLength:
Exclude:
- lib/woot_message_seeder.rb
- lib/seeders/message_seeder.rb
Rails/ApplicationController:
Exclude:
- 'app/controllers/api/v1/widget/messages_controller.rb'
@@ -86,6 +88,7 @@ Naming/VariableNumber:
Metrics/MethodLength:
Exclude:
- 'db/migrate/20161123131628_devise_token_auth_create_users.rb'
- 'db/migrate/20211219031453_update_foreign_keys_on_delete.rb'
Rails/CreateTableWithTimestamps:
Exclude:
- 'db/migrate/20170207092002_acts_as_taggable_on_migration.acts_as_taggable_on_engine.rb'
@@ -101,6 +104,7 @@ Metrics/AbcSize:
- 'db/migrate/20190819005836_add_missing_indexes_on_taggings.acts_as_taggable_on_engine.rb'
- 'db/migrate/20161123131628_devise_token_auth_create_users.rb'
- 'app/controllers/api/v1/accounts/inboxes_controller.rb'
- 'db/migrate/20211219031453_update_foreign_keys_on_delete.rb'
Metrics/CyclomaticComplexity:
Max: 7
Exclude:
+128
View File
@@ -0,0 +1,128 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
hello@chatwoot.com.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.
+5
View File
@@ -0,0 +1,5 @@
# Contributing to Chatwoot
Thanks for taking the time to contribute! :tada::+1:
Please refer to our [Contributing Guide](https://www.chatwoot.com/docs/contributing-guide) for detailed instructions on how to contribute.
+7 -1
View File
@@ -121,6 +121,10 @@ gem 'hairtrigger'
gem 'procore-sift'
# parse email
gem 'email_reply_trimmer'
gem 'html2text'
group :production, :staging do
# we dont want request timing out in development while using byebug
gem 'rack-timeout'
@@ -144,6 +148,8 @@ group :test do
gem 'cypress-on-rails', '~> 1.0'
# fast cleaning of database
gem 'database_cleaner'
# mock http calls
gem 'webmock'
end
group :development, :test do
@@ -155,6 +161,7 @@ group :development, :test do
gem 'active_record_query_trace'
gem 'bundle-audit', require: false
gem 'byebug', platform: :mri
gem 'climate_control'
gem 'factory_bot_rails'
gem 'faker'
gem 'listen'
@@ -170,5 +177,4 @@ group :development, :test do
gem 'simplecov', '0.17.1', require: false
gem 'spring'
gem 'spring-watcher-listen'
gem 'webmock'
end
+73 -66
View File
@@ -9,63 +9,63 @@ GIT
GEM
remote: https://rubygems.org/
specs:
actioncable (6.1.4.1)
actionpack (= 6.1.4.1)
activesupport (= 6.1.4.1)
actioncable (6.1.4.3)
actionpack (= 6.1.4.3)
activesupport (= 6.1.4.3)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
actionmailbox (6.1.4.1)
actionpack (= 6.1.4.1)
activejob (= 6.1.4.1)
activerecord (= 6.1.4.1)
activestorage (= 6.1.4.1)
activesupport (= 6.1.4.1)
actionmailbox (6.1.4.3)
actionpack (= 6.1.4.3)
activejob (= 6.1.4.3)
activerecord (= 6.1.4.3)
activestorage (= 6.1.4.3)
activesupport (= 6.1.4.3)
mail (>= 2.7.1)
actionmailer (6.1.4.1)
actionpack (= 6.1.4.1)
actionview (= 6.1.4.1)
activejob (= 6.1.4.1)
activesupport (= 6.1.4.1)
actionmailer (6.1.4.3)
actionpack (= 6.1.4.3)
actionview (= 6.1.4.3)
activejob (= 6.1.4.3)
activesupport (= 6.1.4.3)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
actionpack (6.1.4.1)
actionview (= 6.1.4.1)
activesupport (= 6.1.4.1)
actionpack (6.1.4.3)
actionview (= 6.1.4.3)
activesupport (= 6.1.4.3)
rack (~> 2.0, >= 2.0.9)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
actiontext (6.1.4.1)
actionpack (= 6.1.4.1)
activerecord (= 6.1.4.1)
activestorage (= 6.1.4.1)
activesupport (= 6.1.4.1)
actiontext (6.1.4.3)
actionpack (= 6.1.4.3)
activerecord (= 6.1.4.3)
activestorage (= 6.1.4.3)
activesupport (= 6.1.4.3)
nokogiri (>= 1.8.5)
actionview (6.1.4.1)
activesupport (= 6.1.4.1)
actionview (6.1.4.3)
activesupport (= 6.1.4.3)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.1, >= 1.2.0)
active_record_query_trace (1.8)
activejob (6.1.4.1)
activesupport (= 6.1.4.1)
activejob (6.1.4.3)
activesupport (= 6.1.4.3)
globalid (>= 0.3.6)
activemodel (6.1.4.1)
activesupport (= 6.1.4.1)
activerecord (6.1.4.1)
activemodel (= 6.1.4.1)
activesupport (= 6.1.4.1)
activemodel (6.1.4.3)
activesupport (= 6.1.4.3)
activerecord (6.1.4.3)
activemodel (= 6.1.4.3)
activesupport (= 6.1.4.3)
activerecord-import (1.2.0)
activerecord (>= 3.2)
activestorage (6.1.4.1)
actionpack (= 6.1.4.1)
activejob (= 6.1.4.1)
activerecord (= 6.1.4.1)
activesupport (= 6.1.4.1)
activestorage (6.1.4.3)
actionpack (= 6.1.4.3)
activejob (= 6.1.4.3)
activerecord (= 6.1.4.3)
activesupport (= 6.1.4.3)
marcel (~> 1.0.0)
mini_mime (>= 1.1.0)
activesupport (6.1.4.1)
activesupport (6.1.4.3)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
@@ -133,6 +133,7 @@ GEM
bundler (>= 1.2.0, < 3)
thor (~> 1.0)
byebug (11.1.3)
climate_control (1.0.1)
coderay (1.1.3)
commonmarker (0.23.2)
concurrent-ruby (1.1.9)
@@ -178,6 +179,7 @@ GEM
addressable (~> 2.8)
ecma-re-validator (0.3.0)
regexp_parser (~> 2.0)
email_reply_trimmer (0.1.13)
erubi (1.10.0)
erubis (2.7.0)
et-orbi (1.2.5)
@@ -218,7 +220,7 @@ GEM
grpc (~> 1.25)
geocoder (1.7.0)
gli (2.20.1)
globalid (0.5.2)
globalid (1.0.0)
activesupport (>= 5.0)
google-apis-core (0.4.1)
addressable (~> 2.5, >= 2.5.1)
@@ -253,9 +255,9 @@ GEM
google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0)
google-protobuf (3.18.1)
google-protobuf (3.18.1-universal-darwin)
google-protobuf (3.18.1-x86_64-linux)
google-protobuf (3.19.2)
google-protobuf (3.19.2-x86_64-darwin)
google-protobuf (3.19.2-x86_64-linux)
googleapis-common-protos (1.3.12)
google-protobuf (~> 3.14)
googleapis-common-protos-types (~> 1.2)
@@ -289,6 +291,8 @@ GEM
hashdiff (1.0.1)
hashie (4.1.0)
hkdf (0.3.0)
html2text (0.2.1)
nokogiri (~> 1.6)
http-accept (1.7.0)
http-cookie (1.0.4)
domain_name (~> 0.5)
@@ -296,7 +300,7 @@ GEM
mime-types (~> 3.0)
multi_xml (>= 0.5.2)
httpclient (2.8.3)
i18n (1.8.10)
i18n (1.8.11)
concurrent-ruby (~> 1.0)
image_processing (1.12.1)
mini_magick (>= 4.9.5, < 5)
@@ -342,7 +346,7 @@ GEM
listen (3.7.0)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
loofah (2.12.0)
loofah (2.13.0)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
mail (2.7.1)
@@ -357,7 +361,7 @@ GEM
mini_magick (4.11.0)
mini_mime (1.1.2)
mini_portile2 (2.5.3)
minitest (5.14.4)
minitest (5.15.0)
mock_redis (0.29.0)
ruby2_keywords
momentjs-rails (2.20.1)
@@ -396,12 +400,12 @@ GEM
pry-rails (0.3.9)
pry (>= 0.10.4)
public_suffix (4.0.6)
puma (5.5.1)
puma (5.5.2)
nio4r (~> 2.0)
pundit (2.1.1)
activesupport (>= 3.0.0)
raabro (1.4.0)
racc (1.5.2)
racc (1.6.0)
rack (2.2.3)
rack-attack (6.5.0)
rack (>= 1.0, < 3)
@@ -412,29 +416,29 @@ GEM
rack-test (1.1.0)
rack (>= 1.0, < 3)
rack-timeout (0.6.0)
rails (6.1.4.1)
actioncable (= 6.1.4.1)
actionmailbox (= 6.1.4.1)
actionmailer (= 6.1.4.1)
actionpack (= 6.1.4.1)
actiontext (= 6.1.4.1)
actionview (= 6.1.4.1)
activejob (= 6.1.4.1)
activemodel (= 6.1.4.1)
activerecord (= 6.1.4.1)
activestorage (= 6.1.4.1)
activesupport (= 6.1.4.1)
rails (6.1.4.3)
actioncable (= 6.1.4.3)
actionmailbox (= 6.1.4.3)
actionmailer (= 6.1.4.3)
actionpack (= 6.1.4.3)
actiontext (= 6.1.4.3)
actionview (= 6.1.4.3)
activejob (= 6.1.4.3)
activemodel (= 6.1.4.3)
activerecord (= 6.1.4.3)
activestorage (= 6.1.4.3)
activesupport (= 6.1.4.3)
bundler (>= 1.15.0)
railties (= 6.1.4.1)
railties (= 6.1.4.3)
sprockets-rails (>= 2.0.0)
rails-dom-testing (2.0.3)
activesupport (>= 4.2.0)
nokogiri (>= 1.6)
rails-html-sanitizer (1.4.2)
loofah (~> 2.3)
railties (6.1.4.1)
actionpack (= 6.1.4.1)
activesupport (= 6.1.4.1)
railties (6.1.4.3)
actionpack (= 6.1.4.3)
activesupport (= 6.1.4.3)
method_source
rake (>= 0.13)
thor (~> 1.0)
@@ -572,9 +576,9 @@ GEM
sprockets (4.0.2)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
sprockets-rails (3.2.2)
actionpack (>= 4.0)
activesupport (>= 4.0)
sprockets-rails (3.4.2)
actionpack (>= 5.2)
activesupport (>= 5.2)
sprockets (>= 3.0.0)
squasher (0.6.2)
statsd-ruby (1.5.0)
@@ -631,7 +635,7 @@ GEM
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
wisper (2.0.0)
zeitwerk (2.4.2)
zeitwerk (2.5.1)
PLATFORMS
arm64-darwin-20
@@ -657,6 +661,7 @@ DEPENDENCIES
bullet
bundle-audit
byebug
climate_control
commonmarker
cypress-on-rails (~> 1.0)
database_cleaner
@@ -666,6 +671,7 @@ DEPENDENCIES
devise_token_auth
dotenv-rails
down (~> 5.0)
email_reply_trimmer
facebook-messenger
factory_bot_rails
faker
@@ -680,6 +686,7 @@ DEPENDENCIES
haikunator
hairtrigger
hashie
html2text
image_processing
jbuilder
json_refs
+6 -2
View File
@@ -1,7 +1,11 @@
The MIT License (MIT)
Copyright (c) 2017-2021 Chatwoot Inc.
Portions of this software are licensed as follows:
* All content that resides under the "enterprise/" directory of this repository, if that directory exists, is licensed under the license defined in "enterprise/LICENSE".
* All third party components incorporated into the Chatwoot Software are licensed under the original license provided by the owner of the applicable component.
* Content outside of the above mentioned directories or restrictions above is available under the "MIT Expat" license as defined below.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
+33 -17
View File
@@ -6,7 +6,10 @@
<p align="center">
<a href="https://heroku.com/deploy?template=https://github.com/chatwoot/chatwoot/tree/master" alt="Deploy to Heroku">
<img alt="Deploy" src="https://www.herokucdn.com/deploy/button.svg"/>
<img width="150" alt="Deploy" src="https://www.herokucdn.com/deploy/button.svg"/>
</a>
<a href="https://marketplace.digitalocean.com/apps/chatwoot?refcode=f2238426a2a8" alt="Deploy to DigitalOcean">
<img width="200" alt="Deploy to DO" src="https://www.deploytodo.com/do-btn-blue.svg"/>
</a>
</p>
@@ -17,7 +20,6 @@ ___
<img src="https://img.shields.io/circleci/build/github/chatwoot/chatwoot" alt="CircleCI Badge">
<a href="https://hub.docker.com/r/chatwoot/chatwoot/"><img src="https://img.shields.io/docker/pulls/chatwoot/chatwoot" alt="Docker Pull Badge"></a>
<a href="https://hub.docker.com/r/chatwoot/chatwoot/"><img src="https://img.shields.io/docker/cloud/build/chatwoot/chatwoot" alt="Docker Build Badge"></a>
<img src="https://img.shields.io/github/license/chatwoot/chatwoot" alt="License">
<img src="https://img.shields.io/github/commit-activity/m/chatwoot/chatwoot" alt="Commits-per-month">
<a title="Crowdin" target="_self" href="https://chatwoot.crowdin.com/chatwoot"><img src="https://badges.crowdin.net/e/37ced7eba411064bd792feb3b7a28b16/localized.svg"></a>
<a href="https://discord.gg/cJXdrwS"><img src="https://img.shields.io/discord/647412545203994635" alt="Discord"></a>
@@ -26,34 +28,39 @@ ___
<a href="https://status.chatwoot.com"><img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fchatwoot%2Fstatus%2Fmaster%2Fapi%2Fchatwoot%2Fresponse-time.json" alt="response time"></a>
</p>
<img src="https://s3.us-west-2.amazonaws.com/gh-assets.chatwoot.com/chatwoot-dashboard-assets.png" width="100%" alt="Chat dashboard"/>
<img src="https://chatwoot-public-assets.s3.amazonaws.com/github/screenshot.png" width="100%" alt="Chat dashboard"/>
Chatwoot is an open-source omnichannel customer support software. The development of Chatwoot started in 2016. It failed to succeed as a business and eventually shut up shop in 2017. During 2019 #Hacktoberfest, the maintainers decided to make it open-source, instead of letting the code rust in a private repo. With a pleasant surprise, Chatwoot became a trending project on Hacker News and best of all, got lots of love from the community.
Now, a failed project is back on track and the prospects are looking great. The team is back to working on the project and this time, we are building it in the open. Thanks to the ideas and contributions from the community.
Chatwoot is an open-source, self-hosted customer engagement suite. Chatwoot lets you view and manage your customer data, communicate with them irrespective of which medium they use, and re-engage them based on their profile.
## Features
Chatwoot gives an integrated view of conversations happening in different communication channels.
It supports the following conversation channels:
Chatwoot supports the following conversation channels:
- **Website**: Talk to your customers using our live chat widget and make use of our SDK to identify a user and provide contextual support.
- **Facebook**: Connect your Facebook pages and start replying to the direct messages to your page.
- **Instagram**: Connect your Instagram profile and start replying to the direct messages.
- **Twitter**: Connect your Twitter profiles and reply to direct messages or the tweets where you are mentioned.
- **Whatsapp**: Connect your Whatsapp business account and manage the conversation in Chatwoot
- **SMS**: Connect your Twilio SMS account and reply to the SMS queries in Chatwoot
- **Telegram**: Connect your Telegram bot and reply to your customers right from a single dashboard.
- **WhatsApp**: Connect your WhatsApp business account and manage the conversation in Chatwoot.
- **Line**: Connect your Line account and manage the conversations in Chatwoot.
- **SMS**: Connect your Twilio SMS account and reply to the SMS queries in Chatwoot.
- **API Channel**: Build custom communication channels using our API channel.
- **Email (beta)**: Forward all your email queries to Chatwoot and view it in our integrated dashboard.
- **Email**: Forward all your email queries to Chatwoot and view it in our integrated dashboard.
And more.
Other features include:
- **Multi-brand inboxes**: Manage multiple brands or pages using a single dashboard.
- **Private notes**: Inter team communication is possible using private notes in a conversation.
- **CRM**: Save all your customer information right inside Chatwoot, use contact notes to log emails, phone calls, or meeting notes.
- **Custom Attributes**: Define custom attribute attributes to store information about a contact or a conversation and extend the product to match your workflow.
- **Shared multi-brand inboxes**: Manage multiple brands or pages using a shared inbox.
- **Private notes**: Use @mentions and private notes to communicate internally about a conversation.
- **Canned responses (Saved replies)**: Improve the response rate by adding saved replies for frequently asked questions.
- **Conversation Labels**: Use conversation labelling to create custom workflows.
- **Conversation Labels**: Use conversation labels to create custom workflows.
- **Auto assignment**: Chatwoot intelligently assigns a ticket to the agents who have access to the inbox depending on their availability and load.
- **Conversation continuity**: If the user has provided an email address through the chat widget, Chatwoot would send an email to the customer under the agent name so that the user can continue the conversation over the email.
- **Conversation continuity**: If the user has provided an email address through the chat widget, Chatwoot will send an email to the customer under the agent name so that the user can continue the conversation over the email.
- **Multi-lingual support**: Chatwoot supports 10+ languages.
- **Powerful API & Webhooks**: Extend the capability of the software using Chatwoots webhooks and APIs.
- **Integrations**: Chatwoot natively integrates with Slack right now. Manage your conversations in Slack without logging into the dashboard.
@@ -81,9 +88,18 @@ Deploying Chatwoot to Heroku is a breeze. It's as simple as clicking this button
Follow this [link](https://www.chatwoot.com/docs/environment-variables) to understand setting the correct environment variables for the app to work with all the features. There might be breakages if you do not set the relevant environment variables.
### DigitalOcean 1-Click Kubernetes deployment
Chatwoot now supports 1-Click deployment to DigitalOcean as a kubernetes app.
<a href="https://marketplace.digitalocean.com/apps/chatwoot?refcode=f2238426a2a8" alt="Deploy to DigitalOcean">
<img width="200" alt="Deploy to DO" src="https://www.deploytodo.com/do-btn-blue.svg"/>
</a>
### Other deployment options
Please follow [deployment architecture guide](https://www.chatwoot.com/docs/deployment/architecture) to deploy with Docker or Caprover.
For other supported options, checkout our [deployment page](https://chatwoot.com/deploy).
## Security
@@ -102,4 +118,4 @@ Thanks goes to all these [wonderful people](https://www.chatwoot.com/docs/contri
<a href="https://github.com/chatwoot/chatwoot/graphs/contributors"><img src="https://opencollective.com/chatwoot/contributors.svg?width=890&button=false" /></a>
*Chatwoot* &copy; 2017-2021, Chatwoot Inc - Released under the MIT License.
*Chatwoot* &copy; 2017-2022, Chatwoot Inc - Released under the MIT License.
+2 -3
View File
@@ -48,11 +48,10 @@ class ContactMergeAction
# attributes in base contact are given preference
merged_attributes = mergee_contact_attributes.deep_merge(base_contact_attributes)
# retaining old pubsub token to notify the contacts that are listening
mergee_pubsub_token = mergee_contact.pubsub_token
@mergee_contact.destroy!
Rails.configuration.dispatcher.dispatch(CONTACT_MERGED, Time.zone.now, contact: @base_contact, tokens: [mergee_pubsub_token])
Rails.configuration.dispatcher.dispatch(CONTACT_MERGED, Time.zone.now, contact: @base_contact,
tokens: [@base_contact.contact_inboxes.filter_map(&:pubsub_token)])
@base_contact.update!(merged_attributes)
end
end
+2 -1
View File
@@ -33,7 +33,8 @@ class ContactBuilder
phone_number: contact_attributes[:phone_number],
email: contact_attributes[:email],
identifier: contact_attributes[:identifier],
additional_attributes: contact_attributes[:additional_attributes]
additional_attributes: contact_attributes[:additional_attributes],
custom_attributes: contact_attributes[:custom_attributes]
)
end
+9 -1
View File
@@ -4,7 +4,7 @@ class ContactInboxBuilder
def perform
@contact = Contact.find(contact_id)
@inbox = @contact.account.inboxes.find(inbox_id)
return unless ['Channel::TwilioSms', 'Channel::Email', 'Channel::Api'].include? @inbox.channel_type
return unless ['Channel::TwilioSms', 'Channel::Email', 'Channel::Api', 'Channel::Whatsapp'].include? @inbox.channel_type
source_id = @source_id || generate_source_id
create_contact_inbox(source_id) if source_id.present?
@@ -14,12 +14,20 @@ class ContactInboxBuilder
def generate_source_id
return twilio_source_id if @inbox.channel_type == 'Channel::TwilioSms'
return wa_source_id if @inbox.channel_type == 'Channel::Whatsapp'
return @contact.email if @inbox.channel_type == 'Channel::Email'
return SecureRandom.uuid if @inbox.channel_type == 'Channel::Api'
nil
end
def wa_source_id
return unless @contact.phone_number
# whatsapp doesn't want the + in e164 format
"#{@contact.phone_number}.delete('+')"
end
def twilio_source_id
return unless @contact.phone_number
@@ -38,6 +38,10 @@ class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder
@outgoing_echo ? :outgoing : :incoming
end
def message_identifier
message[:mid]
end
def message_source_id
@outgoing_echo ? recipient_id : sender_id
end
@@ -66,10 +70,6 @@ class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder
@messaging[:message][:text]
end
def content_attributes
{ message_id: @messaging[:message][:mid] }
end
def build_message
return if @outgoing_echo && already_sent_from_chatwoot?
@@ -103,22 +103,17 @@ class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder
account_id: conversation.account_id,
inbox_id: conversation.inbox_id,
message_type: message_type,
source_id: message_source_id,
source_id: message_identifier,
content: message_content,
content_attributes: content_attributes,
sender: @outgoing_echo ? nil : contact
}
end
def already_sent_from_chatwoot?
cw_message = conversation.messages.where(
source_id: nil,
message_type: 'outgoing',
content: message_content,
private: false,
status: :sent
source_id: @messaging[:message][:mid]
).first
cw_message.update(content_attributes: content_attributes) if cw_message.present?
cw_message.present?
end
+3 -1
View File
@@ -8,9 +8,11 @@ class Messages::MessageBuilder
@conversation = conversation
@user = user
@message_type = params[:message_type] || 'outgoing'
@items = params.to_unsafe_h&.dig(:content_attributes, :items)
@attachments = params[:attachments]
return unless params.instance_of?(ActionController::Parameters)
@in_reply_to = params.to_unsafe_h&.dig(:content_attributes, :in_reply_to)
@items = params.to_unsafe_h&.dig(:content_attributes, :items)
end
def perform
+1 -1
View File
@@ -31,7 +31,7 @@ class RoomChannel < ApplicationCable::Channel
def current_user
@current_user ||= if params[:user_id].blank?
Contact.find_by!(pubsub_token: @pubsub_token)
ContactInbox.find_by!(pubsub_token: @pubsub_token).contact
else
User.find_by!(pubsub_token: @pubsub_token, id: params[:user_id])
end
@@ -2,6 +2,7 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
before_action :fetch_agent, except: [:create, :index]
before_action :check_authorization
before_action :find_user, only: [:create]
before_action :validate_limit, only: [:create]
before_action :create_user, only: [:create]
before_action :save_account_user, only: [:create]
@@ -69,4 +70,8 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
def agents
@agents ||= Current.account.users.order_by_full_name.includes({ avatar_attachment: [:blob] })
end
def validate_limit
render_payment_required('Account limit exceeded. Upgrade to a higher plan') if agents.count >= Current.account.usage_limits[:agents]
end
end
@@ -0,0 +1,21 @@
class Api::V1::Accounts::AutomationRulesController < Api::V1::Accounts::BaseController
before_action :check_authorization
def index
@automation_rules = Current.account.automation_rules
end
def create
@automation_rule = Current.account.automation_rules.create(automation_rules_permit)
end
private
def automation_rules_permit
params.permit(
:name, :description, :event_name, :account_id,
conditions: [:attribute_key, :filter_operator, :query_operator, { values: [] }],
actions: [:action_name, { action_params: [:intiated_at] }]
)
end
end
@@ -74,7 +74,7 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController
end
def long_lived_token(omniauth_token)
koala = Koala::Facebook::OAuth.new(ENV['FB_APP_ID'], ENV['FB_APP_SECRET'])
koala = Koala::Facebook::OAuth.new(GlobalConfigService.load('FB_APP_ID', ''), GlobalConfigService.load('FB_APP_SECRET', ''))
koala.exchange_access_token_info(omniauth_token)['access_token']
rescue StandardError => e
Rails.logger.info e
@@ -33,7 +33,7 @@ class Api::V1::Accounts::CannedResponsesController < Api::V1::Accounts::BaseCont
def canned_responses
if params[:search]
Current.account.canned_responses.where('short_code ILIKE ?', "#{params[:search]}%")
Current.account.canned_responses.where('short_code ILIKE :search OR content ILIKE :search', search: "%#{params[:search]}%")
else
Current.account.canned_responses
end
@@ -0,0 +1,9 @@
class Api::V1::Accounts::Contacts::BaseController < Api::V1::Accounts::BaseController
before_action :ensure_contact
private
def ensure_contact
@contact = Current.account.contacts.find(params[:contact_id])
end
end
@@ -1,5 +1,4 @@
class Api::V1::Accounts::Contacts::ContactInboxesController < Api::V1::Accounts::BaseController
before_action :ensure_contact
class Api::V1::Accounts::Contacts::ContactInboxesController < Api::V1::Accounts::Contacts::BaseController
before_action :ensure_inbox, only: [:create]
def create
@@ -13,8 +12,4 @@ class Api::V1::Accounts::Contacts::ContactInboxesController < Api::V1::Accounts:
@inbox = Current.account.inboxes.find(params[:inbox_id])
authorize @inbox, :show?
end
def ensure_contact
@contact = Current.account.contacts.find(params[:contact_id])
end
end
@@ -1,8 +1,8 @@
class Api::V1::Accounts::Contacts::ConversationsController < Api::V1::Accounts::BaseController
class Api::V1::Accounts::Contacts::ConversationsController < Api::V1::Accounts::Contacts::BaseController
def index
@conversations = Current.account.conversations.includes(
:assignee, :contact, :inbox, :taggings
).where(inbox_id: inbox_ids, contact_id: permitted_params[:contact_id])
).where(inbox_id: inbox_ids, contact_id: @contact.id)
end
private
@@ -14,8 +14,4 @@ class Api::V1::Accounts::Contacts::ConversationsController < Api::V1::Accounts::
[]
end
end
def permitted_params
params.permit(:contact_id)
end
end
@@ -1,13 +1,13 @@
class Api::V1::Accounts::Contacts::LabelsController < Api::V1::Accounts::BaseController
class Api::V1::Accounts::Contacts::LabelsController < Api::V1::Accounts::Contacts::BaseController
include LabelConcern
private
def model
@model ||= Current.account.contacts.find(permitted_params[:contact_id])
@model ||= @contact
end
def permitted_params
params.permit(:contact_id, labels: [])
params.permit(labels: [])
end
end
@@ -0,0 +1,32 @@
class Api::V1::Accounts::Contacts::NotesController < Api::V1::Accounts::Contacts::BaseController
before_action :note, except: [:index, :create]
def index
@notes = @contact.notes.latest.includes(:user)
end
def create
@note = @contact.notes.create!(note_params)
end
def destroy
@note.destroy
head :ok
end
def show; end
def update
@note.update(note_params)
end
private
def note
@note ||= @contact.notes.find(params[:id])
end
def note_params
params.require(:note).permit(:content).merge({ contact_id: @contact.id, user_id: Current.user.id })
end
end
@@ -1,17 +1,19 @@
class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
include Sift
sort_on :email, type: :string
sort_on :name, type: :string
sort_on :name, internal_name: :order_on_name, type: :scope, scope_params: [:direction]
sort_on :phone_number, type: :string
sort_on :last_activity_at, type: :datetime
sort_on :last_activity_at, internal_name: :order_on_last_activity_at, type: :scope, scope_params: [:direction]
sort_on :company, internal_name: :order_on_company_name, type: :scope, scope_params: [:direction]
sort_on :city, internal_name: :order_on_city, type: :scope, scope_params: [:direction]
sort_on :country, internal_name: :order_on_country_name, type: :scope, scope_params: [:direction]
RESULTS_PER_PAGE = 15
before_action :check_authorization
before_action :set_current_page, only: [:index, :active, :search]
before_action :fetch_contact, only: [:show, :update, :destroy, :contactable_inboxes]
before_action :set_include_contact_inboxes, only: [:index, :search]
before_action :set_current_page, only: [:index, :active, :search, :filter]
before_action :fetch_contact, only: [:show, :update, :destroy, :contactable_inboxes, :destroy_custom_attributes]
before_action :set_include_contact_inboxes, only: [:index, :search, :filter]
def index
@contacts_count = resolved_contacts.count
@@ -50,11 +52,24 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
def show; end
def filter
result = ::Contacts::FilterService.new(params.permit!, current_user).perform
contacts = result[:contacts]
@contacts_count = result[:count]
@contacts = fetch_contacts_with_conversation_count(contacts)
end
def contactable_inboxes
@all_contactable_inboxes = Contacts::ContactableInboxesService.new(contact: @contact).get
@contactable_inboxes = @all_contactable_inboxes.select { |contactable_inbox| policy(contactable_inbox[:inbox]).show? }
end
# TODO : refactor this method into dedicated contacts/custom_attributes controller class and routes
def destroy_custom_attributes
@contact.custom_attributes = @contact.custom_attributes.excluding(params[:custom_attributes])
@contact.save!
end
def create
ActiveRecord::Base.transaction do
@contact = Current.account.contacts.new(contact_params)
@@ -66,11 +81,6 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
def update
@contact.assign_attributes(contact_update_params)
@contact.save!
rescue ActiveRecord::RecordInvalid => e
render json: {
message: e.record.errors.full_messages.join(', '),
contact: Current.account.contacts.find_by(email: contact_params[:email])
}, status: :unprocessable_entity
end
def destroy
@@ -91,10 +101,8 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
def resolved_contacts
return @resolved_contacts if @resolved_contacts
@resolved_contacts = Current.account.contacts
.where.not(email: [nil, ''])
.or(Current.account.contacts.where.not(phone_number: [nil, '']))
.or(Current.account.contacts.where.not(identifier: [nil, '']))
@resolved_contacts = Current.account.contacts.resolved_contacts
@resolved_contacts = @resolved_contacts.tagged_with(params[:labels], any: true) if params[:labels].present?
@resolved_contacts
end
@@ -2,7 +2,7 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
include Events::Types
include DateRangeHelper
before_action :conversation, except: [:index, :meta, :search, :create]
before_action :conversation, except: [:index, :meta, :search, :create, :filter]
before_action :contact_inbox, only: [:create]
def index
@@ -31,6 +31,12 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
def show; end
def filter
result = ::Conversations::FilterService.new(params.permit!, current_user).perform
@conversations = result[:conversations]
@conversations_count = result[:count]
end
def mute
@conversation.mute!
head :ok
@@ -60,17 +66,18 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
def toggle_typing_status
case params[:typing_status]
when 'on'
trigger_typing_event(CONVERSATION_TYPING_ON)
trigger_typing_event(CONVERSATION_TYPING_ON, params[:is_private])
when 'off'
trigger_typing_event(CONVERSATION_TYPING_OFF)
trigger_typing_event(CONVERSATION_TYPING_OFF, params[:is_private])
end
head :ok
end
def update_last_seen
@conversation.agent_last_seen_at = DateTime.now.utc
@conversation.assignee_last_seen_at = DateTime.now.utc if assignee?
@conversation.save!
# rubocop:disable Rails/SkipsModelValidations
@conversation.update_column(:agent_last_seen_at, DateTime.now.utc)
@conversation.update_column(:assignee_last_seen_at, DateTime.now.utc) if assignee?
# rubocop:enable Rails/SkipsModelValidations
end
def custom_attributes
@@ -86,9 +93,9 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
@conversation.snoozed_until = parse_date_time(params[:snoozed_until].to_s) if params[:snoozed_until]
end
def trigger_typing_event(event)
def trigger_typing_event(event, is_private)
user = current_user.presence || @resource
Rails.configuration.dispatcher.dispatch(event, Time.zone.now, conversation: @conversation, user: user)
Rails.configuration.dispatcher.dispatch(event, Time.zone.now, conversation: @conversation, user: user, is_private: is_private)
end
def conversation
@@ -25,9 +25,7 @@ class Api::V1::Accounts::CustomAttributeDefinitionsController < Api::V1::Account
private
def fetch_custom_attributes_definitions
@custom_attribute_definitions = Current.account.custom_attribute_definitions.where(
attribute_model: permitted_params[:attribute_model] || DEFAULT_ATTRIBUTE_MODEL
)
@custom_attribute_definitions = Current.account.custom_attribute_definitions.with_attribute_model(permitted_params[:attribute_model])
end
def fetch_custom_attribute_definition
@@ -41,7 +39,7 @@ class Api::V1::Accounts::CustomAttributeDefinitionsController < Api::V1::Account
:attribute_display_type,
:attribute_key,
:attribute_model,
:default_value
attribute_values: []
)
end
@@ -1,11 +1,11 @@
class Api::V1::Accounts::InboxMembersController < Api::V1::Accounts::BaseController
before_action :fetch_inbox
before_action :current_agents_ids, only: [:update]
before_action :current_agents_ids, only: [:create, :update]
def create
authorize @inbox, :create?
ActiveRecord::Base.transaction do
params[:user_ids].map { |user_id| @inbox.add_member(user_id) }
agents_to_be_added_ids.map { |user_id| @inbox.add_member(user_id) }
end
fetch_updated_agents
end
@@ -1,4 +1,5 @@
class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
include Api::V1::InboxesHelper
before_action :fetch_inbox, except: [:index, :create]
before_action :fetch_agent_bot, only: [:set_agent_bot]
# we are already handling the authorization in fetch inbox
@@ -41,9 +42,14 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
def update
@inbox.update(permitted_params.except(:channel))
@inbox.update_working_hours(params.permit(working_hours: Inbox::OFFISABLE_ATTRS)[:working_hours]) if params[:working_hours]
channel_attributes = get_channel_attributes(@inbox.channel_type)
@inbox.channel.update!(permitted_params(channel_attributes)[:channel]) if permitted_params(channel_attributes)[:channel].present?
# Inbox update doesn't necessarily need channel attributes
return if permitted_params(channel_attributes)[:channel].blank?
validate_email_channel(channel_attributes) if @inbox.inbox_type == 'Email'
@inbox.channel.update!(permitted_params(channel_attributes)[:channel])
update_channel_feature_flags
end
@@ -112,23 +118,14 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
def permitted_params(channel_attributes = [])
params.permit(
:name, :avatar, :greeting_enabled, :greeting_message, :enable_email_collect, :csat_survey_enabled,
:enable_auto_assignment, :working_hours_enabled, :out_of_office_message, :timezone,
:enable_auto_assignment, :working_hours_enabled, :out_of_office_message, :timezone, :allow_messages_after_resolved,
channel: [:type, *channel_attributes]
)
end
def get_channel_attributes(channel_type)
case channel_type
when 'Channel::WebWidget'
Channel::WebWidget::EDITABLE_ATTRS
when 'Channel::Api'
Channel::Api::EDITABLE_ATTRS
when 'Channel::Email'
Channel::Email::EDITABLE_ATTRS
when 'Channel::Telegram'
Channel::Telegram::EDITABLE_ATTRS
when 'Channel::Line'
Channel::Line::EDITABLE_ATTRS
if channel_type.constantize.const_defined?('EDITABLE_ATTRS')
channel_type.constantize::EDITABLE_ATTRS.presence
else
[]
end
@@ -8,7 +8,7 @@ class Api::V1::Accounts::TeamMembersController < Api::V1::Accounts::BaseControll
def create
ActiveRecord::Base.transaction do
@team_members = params[:user_ids].map { |user_id| @team.add_member(user_id) }
@team_members = members_to_be_added_ids.map { |user_id| @team.add_member(user_id) }
end
end
@@ -55,7 +55,7 @@ class Api::V1::AccountsController < Api::BaseController
end
def check_signup_enabled
raise ActionController::RoutingError, 'Not Found' if ENV.fetch('ENABLE_ACCOUNT_SIGNUP', true) == 'false'
raise ActionController::RoutingError, 'Not Found' if GlobalConfigService.load('ENABLE_ACCOUNT_SIGNUP', 'false') == 'false'
end
def pundit_user
@@ -7,6 +7,12 @@ class Api::V1::NotificationSubscriptionsController < Api::BaseController
render json: notification_subscription
end
def destroy
notification_subscription = NotificationSubscription.where(["subscription_attributes->>'push_token' = ?", params[:push_token]]).first
notification_subscription.destroy
head :ok
end
private
def set_user
@@ -13,6 +13,11 @@ class Api::V1::ProfilesController < Api::BaseController
@user.update!(profile_params)
end
def avatar
@user.avatar.attachment.destroy! if @user.avatar.attached?
head :ok
end
def availability
@user.account_users.find_by!(account_id: availability_params[:account_id]).update!(availability: availability_params[:availability])
end
@@ -1,5 +1,5 @@
class Api::V1::Widget::ContactsController < Api::V1::Widget::BaseController
before_action :process_hmac
before_action :process_hmac, only: [:update]
def show; end
@@ -8,18 +8,35 @@ class Api::V1::Widget::ContactsController < Api::V1::Widget::BaseController
contact: @contact,
params: permitted_params.to_h.deep_symbolize_keys
)
render json: contact_identify_action.perform
@contact = contact_identify_action.perform
end
# TODO : clean up this with proper routes delete contacts/custom_attributes
def destroy_custom_attributes
@contact.custom_attributes = @contact.custom_attributes.excluding(params[:custom_attributes])
@contact.save!
render json: @contact
end
private
def process_hmac
return if params[:identifier_hash].blank?
raise StandardError, 'HMAC failed: Invalid Identifier Hash Provided' unless valid_hmac?
return unless should_verify_hmac?
render json: { error: 'HMAC failed: Invalid Identifier Hash Provided' }, status: :unauthorized unless valid_hmac?
@contact_inbox.update(hmac_verified: true)
end
def should_verify_hmac?
return false if params[:identifier_hash].blank? && !@web_widget.hmac_mandatory
# Taking an extra caution that the hmac is triggered whenever identifier is present
return false if params[:custom_attributes].present? && params[:identifier].blank?
true
end
def valid_hmac?
params[:identifier_hash] == OpenSSL::HMAC.hexdigest(
'sha256',
@@ -31,13 +31,18 @@ module RequestExceptionHandler
render json: { error: message }, status: :unprocessable_entity
end
def render_payment_required(message)
render json: { error: message }, status: :payment_required
end
def render_internal_server_error(message)
render json: { error: message }, status: :internal_server_error
end
def render_record_invalid(exception)
render json: {
message: exception.record.errors.full_messages.join(', ')
message: exception.record.errors.full_messages.join(', '),
attributes: exception.record.errors.attribute_names
}, status: :unprocessable_entity
end
+8 -3
View File
@@ -26,12 +26,17 @@ class DashboardController < ActionController::Base
'API_CHANNEL_THUMBNAIL',
'ANALYTICS_TOKEN',
'ANALYTICS_HOST'
).merge(
APP_VERSION: Chatwoot.config[:version]
)
).merge(app_config)
end
def ensure_installation_onboarding
redirect_to '/installation/onboarding' if ::Redis::Alfred.get(::Redis::Alfred::CHATWOOT_INSTALLATION_ONBOARDING)
end
def app_config
{ APP_VERSION: Chatwoot.config[:version],
VAPID_PUBLIC_KEY: VapidService.public_key,
ENABLE_ACCOUNT_SIGNUP: GlobalConfigService.load('ENABLE_ACCOUNT_SIGNUP', 'false'),
FB_APP_ID: GlobalConfigService.load('FB_APP_ID', '') }
end
end
@@ -28,10 +28,7 @@ class DeviseOverrides::ConfirmationsController < Devise::ConfirmationsController
end
def create_reset_token_link(user)
raw, enc = Devise.token_generator.generate(user.class, :reset_password_token)
user.reset_password_token = enc
user.reset_password_sent_at = Time.now.utc
user.save(validate: false)
"/app/auth/password/edit?config=default&redirect_url=&reset_password_token=#{raw}"
token = user.send(:set_reset_password_token)
"/app/auth/password/edit?config=default&redirect_url=&reset_password_token=#{token}"
end
end
@@ -0,0 +1,21 @@
class SuperAdmin::AppConfigsController < SuperAdmin::ApplicationController
def show
@allowed_configs = %w[FB_APP_ID FB_VERIFY_TOKEN FB_APP_SECRET]
# ref: https://github.com/rubocop/rubocop/issues/7767
# rubocop:disable Style/HashTransformValues
@fb_config = InstallationConfig.where(name: @allowed_configs)
.pluck(:name, :serialized_value)
.map { |name, serialized_value| [name, serialized_value['value']] }
.to_h
# rubocop:enable Style/HashTransformValues
end
def create
params['app_config'].each do |key, value|
i = InstallationConfig.where(name: key).first_or_create(value: value, locked: false)
i.value = value
i.save!
end
redirect_to super_admin_app_config_url
end
end
@@ -25,6 +25,6 @@ class Webhooks::InstagramController < ApplicationController
private
def valid_instagram_token?(token)
token == ENV['IG_VERIFY_TOKEN']
token == GlobalConfigService.load('IG_VERIFY_TOKEN', '')
end
end
+5 -5
View File
@@ -29,21 +29,21 @@ class WidgetsController < ActionController::Base
def set_contact
return if @auth_token_params[:source_id].nil?
contact_inbox = ::ContactInbox.find_by(
@contact_inbox = ::ContactInbox.find_by(
inbox_id: @web_widget.inbox.id,
source_id: @auth_token_params[:source_id]
)
@contact = contact_inbox ? contact_inbox.contact : nil
@contact = @contact_inbox ? @contact_inbox.contact : nil
end
def build_contact
return if @contact.present?
contact_inbox = @web_widget.create_contact_inbox(additional_attributes)
@contact = contact_inbox.contact
@contact_inbox = @web_widget.create_contact_inbox(additional_attributes)
@contact = @contact_inbox.contact
payload = { source_id: contact_inbox.source_id, inbox_id: @web_widget.inbox.id }
payload = { source_id: @contact_inbox.source_id, inbox_id: @web_widget.inbox.id }
@token = ::Widget::TokenService.new(payload: payload).generate_token
end
+4 -1
View File
@@ -12,6 +12,7 @@ class AgentBotDashboard < Administrate::BaseDashboard
avatar_url: AvatarField,
id: Field::Number,
name: Field::String,
account: Field::BelongsTo.with_options(searchable: true, searchable_field: 'name', order: 'id DESC'),
description: Field::String,
outgoing_url: Field::String,
created_at: Field::DateTime,
@@ -26,6 +27,7 @@ class AgentBotDashboard < Administrate::BaseDashboard
COLLECTION_ATTRIBUTES = %i[
id
avatar_url
account
name
outgoing_url
].freeze
@@ -34,7 +36,7 @@ class AgentBotDashboard < Administrate::BaseDashboard
# an array of attributes that will be displayed on the model's show page.
SHOW_PAGE_ATTRIBUTES = %i[
id
avatar_url
account
name
description
outgoing_url
@@ -45,6 +47,7 @@ class AgentBotDashboard < Administrate::BaseDashboard
# on the model's form (`new` and `edit`) pages.
FORM_ATTRIBUTES = %i[
name
account
description
outgoing_url
].freeze
+3 -1
View File
@@ -15,7 +15,9 @@ class AsyncDispatcher < BaseDispatcher
EventListener.instance,
HookListener.instance,
InstallationWebhookListener.instance,
WebhookListener.instance
NotificationListener.instance,
WebhookListener.instance,
AutomationRuleListener.instance
]
end
end
+1 -1
View File
@@ -5,6 +5,6 @@ class SyncDispatcher < BaseDispatcher
end
def listeners
[ActionCableListener.instance, AgentBotListener.instance, NotificationListener.instance]
[ActionCableListener.instance, AgentBotListener.instance]
end
end
+25
View File
@@ -1,5 +1,30 @@
class ConversationDrop < BaseDrop
include MessageFormatHelper
def display_id
@obj.try(:display_id)
end
def contact_name
@obj.try(:contact).name.capitalize || 'Customer'
end
def recent_messages
@obj.try(:recent_messages).map do |message|
{
'sender' => message_sender_name(message.sender),
'content' => render_message_content(transform_user_mention_content(message.content)),
'attachments' => message.attachments.map(&:file_url)
}
end
end
private
def message_sender_name(sender)
return 'Bot' if sender.blank?
return contact_name if sender.instance_of?(Contact)
sender&.available_name || sender&.name
end
end
+3
View File
@@ -1,2 +1,5 @@
class InboxDrop < BaseDrop
def name
@obj.try(:name)
end
end
+2 -2
View File
@@ -6,7 +6,7 @@ class MessageDrop < BaseDrop
end
def text_content
content = @obj.try(:content)
transform_user_mention_content content
content = @obj.try(:content) || ''
render_message_content(transform_user_mention_content(content))
end
end
+1 -1
View File
@@ -2,6 +2,6 @@ require 'administrate/field/base'
class AvatarField < Administrate::Field::Base
def avatar_url
data.presence || '/admin/avatar.png'
data.presence&.gsub('?d=404', '?d=mp')
end
end
+12 -3
View File
@@ -70,7 +70,12 @@ class ConversationFinder
end
def find_all_conversations
@conversations = current_account.conversations.where(inbox_id: @inbox_ids)
if params[:conversation_type] == 'mention'
conversation_ids = current_account.mentions.where(user: current_user).pluck(:conversation_id)
@conversations = current_account.conversations.where(id: conversation_ids)
else
@conversations = current_account.conversations.where(inbox_id: @inbox_ids)
end
end
def filter_by_assignee_type
@@ -121,8 +126,12 @@ class ConversationFinder
def conversations
@conversations = @conversations.includes(
:taggings, :inbox, { assignee: { avatar_attachment: [:blob] } }, { contact: { avatar_attachment: [:blob] } }, :team
:taggings, :inbox, { assignee: { avatar_attachment: [:blob] } }, { contact: { avatar_attachment: [:blob] } }, :team, :contact_inbox
)
@conversations.latest.page(current_page)
if params[:conversation_type] == 'mention'
@conversations.page(current_page)
else
@conversations.latest.page(current_page)
end
end
end
+33
View File
@@ -0,0 +1,33 @@
module Api::V1::InboxesHelper
def validate_email_channel(attributes)
channel_data = permitted_params(attributes)[:channel]
validate_imap(channel_data)
validate_smtp(channel_data)
end
private
def validate_imap(channel_data)
return unless channel_data.key?('imap_enabled') && channel_data[:imap_enabled]
Mail.defaults do
retriever_method :imap, { address: channel_data[:imap_address],
port: channel_data[:imap_port],
user_name: channel_data[:imap_email],
password: channel_data[:imap_password],
enable_ssl: channel_data[:imap_enable_ssl] }
end
Mail.connection do # rubocop:disable:block
end
end
def validate_smtp(channel_data)
return unless channel_data.key?('smtp_enabled') && channel_data[:smtp_enabled]
smtp = Net::SMTP.start(channel_data[:smtp_address], channel_data[:smtp_port], channel_data[:smtp_domain], channel_data[:smtp_email],
channel_data[:smtp_password], :login)
smtp.finish unless smtp&.nil?
end
end
+22 -9
View File
@@ -1,16 +1,29 @@
module FileTypeHelper
# NOTE: video, audio, image, etc are filetypes previewable in frontend
def file_type(content_type)
return :image if [
'image/jpeg',
'image/png',
'image/gif',
'image/tiff',
'image/bmp'
].include?(content_type)
return :video if content_type.include?('video/')
return :image if image_file?(content_type)
return :video if video_file?(content_type)
return :audio if content_type.include?('audio/')
:file
end
def image_file?(content_type)
[
'image/jpeg',
'image/png',
'image/gif',
'image/bmp',
'image/webp'
].include?(content_type)
end
def video_file?(content_type)
[
'video/ogg',
'video/mp4',
'video/webm',
'video/quicktime'
].include?(content_type)
end
end
+7
View File
@@ -1,6 +1,13 @@
module MessageFormatHelper
include RegexHelper
def transform_user_mention_content(message_content)
message_content.gsub(MENTION_REGEX, '\1')
end
def render_message_content(message_content)
# rubocop:disable Rails/OutputSafety
CommonMarker.render_html(message_content).html_safe
# rubocop:enable Rails/OutputSafety
end
end
+2 -2
View File
@@ -6,8 +6,8 @@ class AttributeAPI extends ApiClient {
super('custom_attribute_definitions', { accountScoped: true });
}
getAttributesByModel(modelId) {
return axios.get(`${this.url}?attribute_model=${modelId}`);
getAttributesByModel() {
return axios.get(this.url);
}
}
+4
View File
@@ -166,4 +166,8 @@ export default {
profile: { ...availabilityData },
});
},
deleteAvatar() {
return axios.delete(endPoints('deleteAvatar').url);
},
};
+21 -1
View File
@@ -2,7 +2,27 @@ import ApiClient from './ApiClient';
class ContactNotes extends ApiClient {
constructor() {
super('contact_notes', { accountScoped: true });
super('notes', { accountScoped: true });
this.contactId = null;
}
get url() {
return `${this.baseUrl()}/contacts/${this.contactId}/notes`;
}
get(contactId) {
this.contactId = contactId;
return super.get();
}
create(contactId, content) {
this.contactId = contactId;
return super.create({ content });
}
delete(contactId, id) {
this.contactId = contactId;
return super.delete(id);
}
}
+11
View File
@@ -53,6 +53,11 @@ class ContactAPI extends ApiClient {
return axios.get(requestURL);
}
filter(page = 1, sortAttr = 'name', queryPayload) {
let requestURL = `${this.url}/filter?${buildContactParams(page, sortAttr)}`;
return axios.post(requestURL, queryPayload);
}
importContacts(file) {
const formData = new FormData();
formData.append('import_file', file);
@@ -60,6 +65,12 @@ class ContactAPI extends ApiClient {
headers: { 'Content-Type': 'multipart/form-data' },
});
}
destroyCustomAttributes(contactId, customAttributes) {
return axios.post(`${this.url}/${contactId}/destroy_custom_attributes`, {
custom_attributes: customAttributes,
});
}
}
export default new ContactAPI();
@@ -36,6 +36,10 @@ const endPoints = {
},
params: { omniauth_token: '' },
},
deleteAvatar: {
url: '/api/v1/profile/avatar',
},
};
export default page => {
@@ -6,7 +6,15 @@ class ConversationApi extends ApiClient {
super('conversations', { accountScoped: true });
}
get({ inboxId, status, assigneeType, page, labels, teamId }) {
get({
inboxId,
status,
assigneeType,
page,
labels,
teamId,
conversationType,
}) {
return axios.get(this.url, {
params: {
inbox_id: inboxId,
@@ -15,6 +23,15 @@ class ConversationApi extends ApiClient {
assignee_type: assigneeType,
page,
labels,
conversation_type: conversationType,
},
});
}
filter(payload) {
return axios.post(`${this.url}/filter`, payload.queryData, {
params: {
page: payload.page,
},
});
}
@@ -51,9 +68,10 @@ class ConversationApi extends ApiClient {
return axios.post(`${this.url}/${id}/update_last_seen`);
}
toggleTyping({ conversationId, status }) {
toggleTyping({ conversationId, status, isPrivate }) {
return axios.post(`${this.url}/${conversationId}/toggle_typing_status`, {
typing_status: status,
is_private: isPrivate,
});
}
@@ -65,7 +83,7 @@ class ConversationApi extends ApiClient {
return axios.post(`${this.url}/${conversationId}/unmute`);
}
meta({ inboxId, status, assigneeType, labels, teamId }) {
meta({ inboxId, status, assigneeType, labels, teamId, conversationType }) {
return axios.get(`${this.url}/meta`, {
params: {
inbox_id: inboxId,
@@ -73,6 +91,7 @@ class ConversationApi extends ApiClient {
assignee_type: assigneeType,
labels,
team_id: teamId,
conversation_type: conversationType,
},
});
}
@@ -80,6 +99,12 @@ class ConversationApi extends ApiClient {
sendEmailTranscript({ conversationId, email }) {
return axios.post(`${this.url}/${conversationId}/transcript`, { email });
}
updateCustomAttributes({ conversationId, customAttributes }) {
return axios.post(`${this.url}/${conversationId}/custom_attributes`, {
custom_attributes: customAttributes,
});
}
}
export default new ConversationApi();
@@ -8,8 +8,8 @@ export const buildCreatePayload = ({
contentAttributes,
echoId,
file,
ccEmails,
bccEmails,
ccEmails = '',
bccEmails = '',
}) => {
let payload;
if (file) {
@@ -47,8 +47,8 @@ class MessageApi extends ApiClient {
contentAttributes,
echo_id: echoId,
file,
ccEmails,
bccEmails,
ccEmails = '',
bccEmails = '',
}) {
return axios({
method: 'post',
@@ -11,6 +11,7 @@ describe('#ContactsAPI', () => {
expect(contactAPI).toHaveProperty('update');
expect(contactAPI).toHaveProperty('delete');
expect(contactAPI).toHaveProperty('getConversations');
expect(contactAPI).toHaveProperty('filter');
});
describeWithAPIMock('API calls', context => {
@@ -60,6 +61,16 @@ describe('#ContactsAPI', () => {
);
});
it('#destroyCustomAttributes', () => {
contactAPI.destroyCustomAttributes(1, ['cloudCustomer']);
expect(context.axiosMock.post).toHaveBeenCalledWith(
'/api/v1/contacts/1/destroy_custom_attributes',
{
custom_attributes: ['cloudCustomer'],
}
);
});
it('#importContacts', () => {
const file = 'file';
contactAPI.importContacts(file);
@@ -71,6 +82,24 @@ describe('#ContactsAPI', () => {
}
);
});
it('#filter', () => {
const queryPayload = {
payload: [
{
attribute_key: 'email',
filter_operator: 'contains',
values: ['fayaz'],
query_operator: null,
},
],
};
contactAPI.filter(1, 'name', queryPayload);
expect(context.axiosMock.post).toHaveBeenCalledWith(
'/api/v1/contacts/filter?include_contact_inboxes=false&page=1&sort=name',
queryPayload
);
});
});
});
@@ -19,6 +19,7 @@ describe('#ConversationAPI', () => {
expect(conversationAPI).toHaveProperty('unmute');
expect(conversationAPI).toHaveProperty('meta');
expect(conversationAPI).toHaveProperty('sendEmailTranscript');
expect(conversationAPI).toHaveProperty('filter');
});
describeWithAPIMock('API calls', context => {
@@ -160,5 +161,54 @@ describe('#ConversationAPI', () => {
}
);
});
it('#updateCustomAttributes', () => {
conversationAPI.updateCustomAttributes({
conversationId: 45,
customAttributes: { order_d: '1001' },
});
expect(context.axiosMock.post).toHaveBeenCalledWith(
'/api/v1/conversations/45/custom_attributes',
{
custom_attributes: { order_d: '1001' },
}
);
});
it('#filter', () => {
const payload = {
page: 1,
queryData: {
payload: [
{
attribute_key: 'status',
filter_operator: 'equal_to',
values: ['pending', 'resolved'],
query_operator: 'and',
},
{
attribute_key: 'assignee',
filter_operator: 'equal_to',
values: [3],
query_operator: 'and',
},
{
attribute_key: 'id',
filter_operator: 'equal_to',
values: ['This is a test'],
query_operator: null,
},
],
},
};
conversationAPI.filter(payload);
expect(
context.axiosMock.post
).toHaveBeenCalledWith(
'/api/v1/conversations/filter',
payload.queryData,
{ params: { page: payload.page } }
);
});
});
});
@@ -35,12 +35,14 @@ describe('#ConversationAPI', () => {
message: 'test content',
echoId: 12,
isPrivate: true,
file: new Blob(['test-content'], { type: 'application/pdf' }),
});
expect(formPayload).toBeInstanceOf(FormData);
expect(formPayload.get('content')).toEqual('test content');
expect(formPayload.get('echo_id')).toEqual('12');
expect(formPayload.get('private')).toEqual('true');
expect(formPayload.get('cc_emails')).toEqual('');
});
it('builds object payload if file is not available', () => {
@@ -56,6 +58,8 @@ describe('#ConversationAPI', () => {
private: false,
echo_id: 12,
content_attributes: { in_reply_to: 12 },
bcc_emails: '',
cc_emails: '',
});
});
});
@@ -1,4 +1,3 @@
/* Enter and leave animations can use different */
/* durations and timing functions. */
.slide-fade-enter-active {
@@ -9,7 +8,8 @@
transition: all .3s $ease-out-cubic;
}
.slide-fade-enter, .slide-fade-leave-to {
.slide-fade-enter,
.slide-fade-leave-to {
opacity: 0;
transform: translateX(10px);
}
@@ -22,22 +22,33 @@
transform: translateX($space-medium);
}
.conversations-list-enter-active, .conversations-list-leave-active {
.conversations-list-enter-active,
.conversations-list-leave-active {
transition: all .25s $ease-out-cubic;
}
.conversations-list-enter, .conversations-list-leave-to /* .conversations-list-leave-active for <2.1.8 */ {
.conversations-list-enter,
.conversations-list-leave-to {
opacity: 0;
transform: translateX($space-medium);
}
.menu-list-enter-active, .menu-list-leave-active {
transition: all .2s $ease-out-cubic;
.menu-list-enter-active,
.menu-list-leave-active {
transition: opacity .3s $ease-out-cubic,
transform .2s $ease-out-cubic;
}
.menu-list-enter, .menu-list-leave-to /* .conversations-list-leave-active for <2.1.8 */ {
.menu-list-leave-to {
opacity: 0;
transform: translateX($space-medium);
position: absolute;
transform: translateX($space-small);
}
.menu-list-enter {
opacity: 0;
transform: translateX(-$space-small);
}
.slide-up-enter-active {
@@ -48,8 +59,8 @@
transition: all .3s $ease-out-cubic;
}
.slide-up-enter, .slide-up-leave-to
/* .slide-fade-leave-active for <2.1.8 */ {
.slide-up-enter,
.slide-up-leave-to {
transform: translateY(-$space-medium);
opacity: 0;
}
@@ -60,10 +71,10 @@
transition: transform 0.25s $ease-in-cubic, opacity 0.15s $ease-in-cubic;
}
.menu-slide-enter, .menu-slide-leave-to
/* .slide-fade-leave-active for <2.1.8 */ {
transform: translateY($space-small);
.menu-slide-enter,
.menu-slide-leave-to {
opacity: 0;
transform: translateY($space-small);
}
@@ -75,10 +86,10 @@
transition: all .1s $ease-out-sine;
}
.toast-fade-enter, .toast-fade-leave-to
/* .toast-fade-leave-active for <2.1.8 */ {
transform: translateY(-$space-small);
.toast-fade-enter,
.toast-fade-leave-to {
opacity: 0;
transform: translateY(-$space-small);
}
.modal-fade-enter-active {
@@ -89,8 +100,8 @@
transition: all .1s $ease-out-sine;
}
.modal-fade-enter, .modal-fade-leave-to
/* .slide-fade-leave-active for <2.1.8 */ {
.modal-fade-enter,
.modal-fade-leave-to {
opacity: 0;
}
@@ -9,10 +9,10 @@
.card {
margin-bottom: var(--space-small);
padding: var(--space-small);
padding: var(--space-normal);
}
.button-wrapper .button.link.grey-btn {
.button-wrapper .button.grey-btn {
margin-left: var(--space-normal);
}
@@ -21,7 +21,7 @@
font-size: $font-size-mini;
max-width: 15rem;
padding: $space-smaller $space-small;
z-index: 9999;
z-index: 999;
}
code {
@@ -49,7 +49,21 @@ code {
.cursor-pointer {
cursor: pointer;
}
// remove when grid gutters are fixed
.columns.with-right-space {
padding-right: var(--space-normal);
}
.badge {
border-radius: var(--border-radius-normal);
}
.padding-right-small {
padding-right: var(--space-one);
}
.margin-right-small {
margin-right: var(--space-small);
}
@@ -45,6 +45,9 @@
// 1. Global
// ---------
// Disable contrast warnings in Foundation.
$contrast-warnings: false;
$global-font-size: 10px;
$global-width: 100%;
$global-lineheight: 1.5;
@@ -219,9 +222,9 @@ $badge-background: $primary-color;
$badge-color: $white;
$badge-color-alt: $black;
$badge-palette: $foundation-palette;
$badge-padding: 0.3em;
$badge-padding: var(--space-smaller);
$badge-minwidth: 2.1em;
$badge-font-size: 0.6rem;
$badge-font-size: var(--font-size-nano);
// 10. Breadcrumbs
// ---------------
@@ -400,7 +403,7 @@ $mediaobject-image-width-stacked: 100%;
$menu-margin: 0;
$menu-margin-nested: $space-medium;
$menu-item-padding: $space-one;
$menu-item-padding: $space-slab;
$menu-item-color-active: $white;
$menu-item-background-active: $color-background;
$menu-icon-spacing: 0.25rem;
@@ -55,6 +55,10 @@
justify-content: space-between;
}
.w-100 {
.w-full {
width: 100%;
}
.h-full {
height: 100%;
}
@@ -1,3 +1,56 @@
.margin-right-small {
margin-right: var(--space-small);
}
.margin-right-smaller {
margin-right: var(--space-smaller);
}
.margin-left-minus-slab {
margin-left: var(--space-minus-slab);
}
.fs-small {
font-size: var(--font-size-small);
}
.fs-default {
font-size: var(--font-size-default);
}
.fw-medium {
font-weight: var(--font-weight-medium);
}
.p-normal {
padding: var(--space-normal);
}
.overflow-scroll {
overflow: scroll;
}
.overflow-auto {
overflow: auto;
}
.overflow-hidden {
overflow: hidden;
}
.border-right {
border-right: 1px solid var(--color-border);
}
.border-left {
border-left: 1px solid var(--color-border);
}
.bg-white {
background-color: var(--white);
}
.text-y-800 {
color: var(--y-800);
}
@@ -44,11 +44,14 @@ $woot-logo-padding: $space-large $space-two;
$color-woot: #1f93ff;
$color-gray: #6e6f73;
$color-light-gray: #999a9b;
$color-border: #e0e6ed;
$color-border-light: #f0f4f5;
$color-border-dark: #cad0d4;
$color-background: #f4f6fb;
$color-background-light: #f9fafc;
$color-border: var(--s-75);
$color-border-light: var(--s-50);
$color-border-dark: var(--s-100);
$color-background: var(--s-50);
$color-background-light: var(--s-25);
$color-white: #fff;
$color-body: #3c4858;
$color-heading: #1f2d3d;
@@ -14,7 +14,6 @@
@import 'helper-classes';
@import 'formulate';
@import 'date-picker';
@import 'utility-helpers';
@import 'foundation-sites/scss/foundation';
@import '~bourbon/core/bourbon';
@@ -50,3 +49,4 @@
@import 'plugins/multiselect';
@import 'plugins/dropdown';
@import '~shared/assets/stylesheets/ionicons';
@import 'utility-helpers';
@@ -2,8 +2,9 @@
@include elegant-card;
@include border-light;
box-sizing: content-box;
padding: var(--space-small);
width: fit-content;
z-index: 999;
z-index: var(--z-index-very-high);
&.dropdown-pane--open {
display: block;
@@ -51,6 +51,10 @@
width: 100%;
}
p {
margin-bottom: 0;
}
&.multiselect__option--highlight {
background: var(--white);
color: var(--color-body);
@@ -8,7 +8,7 @@
@include background-white;
@include flex;
@include flex-align($x: justify, $y: middle);
@include border-normal-bottom;
border-bottom: 1px solid var(--s-50);
height: $header-height;
min-height: $header-height;
@@ -25,6 +25,7 @@ $default-button-height: 4.0rem;
// @TODDO - Remove after moving all buttons to woot-button
.icon+.button__content {
padding-left: var(--space-small);
width: auto;
}
&.expanded {
@@ -65,6 +66,32 @@ $default-button-height: 4.0rem;
}
}
&.clear {
&.warning {
color: var(--y-800);
}
&.button--only-icon:hover {
background: var(--w-50);
&.secondary {
background: var(--s-50);
}
&.success {
background: var(--g-50);
}
&.alert {
background: var(--r-50);
}
&.warning {
background: var(--y-100);
}
}
}
// Sizes
&.tiny {
height: var(--space-medium);
@@ -103,7 +130,6 @@ $default-button-height: 4.0rem;
padding: 0;
}
}
@@ -85,11 +85,6 @@
text-overflow: ellipsis;
white-space: nowrap;
width: 27rem;
.small-icon {
font-size: $font-size-mini;
vertical-align: top;
}
}
.conversation--meta {
@@ -17,7 +17,8 @@
}
}
.image {
.image,
.video {
cursor: pointer;
position: relative;
@@ -26,7 +27,13 @@
}
.modal-image {
max-width: 85%;
max-height: 90%;
max-width: 90%;
}
.modal-video {
max-height: 75vh;
max-width: 100%;
}
&::before {
@@ -35,11 +42,21 @@
content: '';
height: 20%;
left: 0;
opacity: .8;
opacity: 0.8;
position: absolute;
width: 100%;
}
}
.video {
.modal-container {
width: auto;
.modal--close {
z-index: var(--z-index-low);
}
}
}
}
.conversations-list-wrap {
@@ -76,7 +93,7 @@
.status--filter {
@include padding($zero null $zero $space-normal);
@include margin($space-smaller $space-slab $zero $zero);
@include margin($zero);
background-color: $color-background-light;
border: 1px solid $color-border;
float: right;
@@ -118,7 +135,7 @@
&.unread--toast {
+.right {
margin-bottom: 0;
margin-bottom: var(--space-micro);
}
+.left {
@@ -1,8 +1,7 @@
.error {
#{$all-text-inputs},
select,
.multiselect>.multiselect__tags {
.multiselect > .multiselect__tags {
@include thin-border(var(--r-400));
}
@@ -40,4 +39,8 @@ input {
font-size: var(--font-size-small);
height: var(--space-large);
}
.error {
border-color: var(--r-400);
}
}
@@ -19,7 +19,7 @@
cursor: pointer;
font-size: $font-size-big;
line-height: $space-normal;
padding: $space-normal $space-two;
padding: $space-normal;
position: absolute;
right: $space-micro;
top: $space-micro;
@@ -29,7 +29,6 @@
}
}
.page-top-bar {
@include padding($space-large $space-large $zero);
@@ -48,13 +47,16 @@
position: relative;
width: 60rem;
&.medium {
max-width: 80%;
width: 90rem;
}
.content-box {
@include padding($zero);
height: auto;
}
h2 {
color: $color-heading;
font-size: $font-size-medium;
@@ -89,15 +91,19 @@
button {
font-size: $font-size-small;
}
&.justify-content-end {
justify-content: end;
}
}
.delete-item {
@include padding($space-large);
button {
@include margin($zero);
}
}
}
.modal-enter,
@@ -21,10 +21,7 @@
border: 1px solid var(--color-border);
}
.margin-right-small {
margin-right: var(--space-small);
}
.display-flex {
.reports-option__wrap {
align-items: center;
display: flex;
}
@@ -6,12 +6,6 @@
}
.sidebar {
@include border-normal-right;
@include background-white;
@include full-height;
@include margin(0);
@include space-between-column;
width: $nav-bar-width;
z-index: 1024 - 1;
//logo
@@ -22,26 +16,6 @@
}
}
.main-nav {
a {
border-radius: $space-smaller;
color: $color-gray;
font-size: $font-size-default;
font-weight: $font-weight-medium;
.wrap,
.child-icon {
&:hover {
color: $color-woot;
}
}
}
.active a .wrap {
color: $color-woot;
}
}
.nested {
a {
font-size: $font-size-small;
@@ -83,34 +57,6 @@
}
}
.main-nav {
@include flex-weight(1);
@include scroll-on-hover;
padding: 0 $space-medium - $space-one;
a {
&::before {
margin-right: $space-slab;
}
}
.menu-title {
color: $color-gray;
font-size: $font-size-medium;
margin-top: $space-medium;
>span {
margin-left: $space-one;
}
}
}
.menu-title+ul>li>a {
@include padding($space-micro null);
color: $medium-gray;
line-height: $global-lineheight;
}
.hamburger--menu {
cursor: pointer;
display: none;
@@ -1,6 +1,6 @@
<template>
<div class="cw-accordion">
<button class="cw-accordion--title" @click="$emit('click')">
<button class="cw-accordion--title drag-handle" @click="$emit('click')">
<div class="cw-accordion--title-wrap">
<emoji-or-icon class="icon-or-emoji" :icon="icon" :emoji="emoji" />
<h5>
@@ -10,12 +10,16 @@
<div class="button-icon--wrap">
<slot name="button" />
<div class="chevron-icon__wrap">
<i v-if="isOpen" class="ion-minus chevron-icon"></i>
<i v-else class="ion-plus chevron-icon"></i>
<fluent-icon v-if="isOpen" size="24" icon="subtract" type="solid" />
<fluent-icon v-else size="24" icon="add" type="solid" />
</div>
</div>
</button>
<div v-if="isOpen" class="cw-accordion--content">
<div
v-if="isOpen"
class="cw-accordion--content"
:class="{ compact: compact }"
>
<slot />
</div>
</div>
@@ -33,6 +37,10 @@ export default {
type: String,
required: true,
},
compact: {
type: Boolean,
default: false,
},
icon: {
type: String,
default: '',
@@ -58,10 +66,10 @@ export default {
}
.cw-accordion--title {
align-items: center;
background: var(--b-50);
border-bottom: 1px solid var(--color-border);
border-top: 1px solid var(--color-border);
cursor: pointer;
background: var(--s-50);
border-bottom: 1px solid var(--s-100);
border-top: 1px solid var(--s-100);
cursor: grab;
display: flex;
justify-content: space-between;
margin: 0;
@@ -106,5 +114,9 @@ export default {
.cw-accordion--content {
padding: var(--space-normal);
&.compact {
padding: 0;
}
}
</style>
+132 -11
View File
@@ -1,14 +1,40 @@
<template>
l<template>
<div class="conversations-list-wrap">
<slot></slot>
<div class="chat-list__top">
<div class="chat-list__top" :class="{ filter__applied: hasAppliedFilters }">
<h1 class="page-title text-truncate" :title="pageTitle">
{{ pageTitle }}
</h1>
<chat-filter @statusFilterChange="updateStatusType" />
<div class="filter--actions">
<chat-filter
v-if="!hasAppliedFilters"
@statusFilterChange="updateStatusType"
/>
<woot-button
v-else
size="small"
variant="clear"
color-scheme="alert"
@click="resetAndFetchData"
>
{{ $t('FILTER.CLEAR_BUTTON_LABEL') }}
</woot-button>
<woot-button
v-tooltip.top-end="$t('FILTER.TOOLTIP_LABEL')"
variant="clear"
color-scheme="secondary"
icon="filter"
size="small"
class="btn-filter"
@click="onToggleAdvanceFiltersModal"
>
</woot-button>
</div>
</div>
<chat-type-tabs
v-if="!hasAppliedFilters"
:items="assigneeTabItems"
:active-tab="activeAssigneeTab"
class="tab--chat-type"
@@ -26,6 +52,7 @@
:active-label="label"
:team-id="teamId"
:chat="chat"
:conversation-type="conversationType"
:show-assignee="showAssigneeInConversationCard"
/>
@@ -37,7 +64,7 @@
v-if="!hasCurrentPageEndReached && !chatListLoading"
variant="clear"
size="expanded"
@click="fetchConversations"
@click="loadMoreConversations"
>
{{ $t('CHAT_LIST.LOAD_MORE_CONVERSATIONS') }}
</woot-button>
@@ -53,6 +80,18 @@
{{ $t('CHAT_LIST.EOF') }}
</p>
</div>
<woot-modal
:show.sync="showAdvancedFilters"
:on-close="onToggleAdvanceFiltersModal"
size="medium"
>
<conversation-advanced-filter
v-if="showAdvancedFilters"
:filter-types="advancedFilterTypes"
:on-close="onToggleAdvanceFiltersModal"
@applyFilter="onApplyFilter"
/>
</woot-modal>
</div>
</template>
@@ -60,12 +99,16 @@
import { mapGetters } from 'vuex';
import ChatFilter from './widgets/conversation/ChatFilter';
import ConversationAdvancedFilter from './widgets/conversation/ConversationAdvancedFilter';
import ChatTypeTabs from './widgets/ChatTypeTabs';
import ConversationCard from './widgets/conversation/ConversationCard';
import timeMixin from '../mixins/time';
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
import conversationMixin from '../mixins/conversations';
import wootConstants from '../constants';
import advancedFilterTypes from './widgets/conversation/advancedFilterItems';
import filterQueryGenerator from '../helper/filterQueryGenerator.js';
import {
hasPressedAltAndJKey,
hasPressedAltAndKKey,
@@ -76,6 +119,7 @@ export default {
ChatTypeTabs,
ConversationCard,
ChatFilter,
ConversationAdvancedFilter,
},
mixins: [timeMixin, conversationMixin, eventListenerMixins],
props: {
@@ -91,11 +135,20 @@ export default {
type: String,
default: '',
},
conversationType: {
type: String,
default: '',
},
},
data() {
return {
activeAssigneeTab: wootConstants.ASSIGNEE_TYPE.ME,
activeStatus: wootConstants.STATUS_TYPE.OPEN,
showAdvancedFilters: false,
advancedFilterTypes: advancedFilterTypes.map(filter => ({
...filter,
attributeName: this.$t(`FILTER.ATTRIBUTES.${filter.attributeI18nKey}`),
})),
};
},
computed: {
@@ -109,7 +162,11 @@ export default {
currentUserID: 'getCurrentUserID',
activeInbox: 'getSelectedInbox',
conversationStats: 'conversationStats/getStats',
appliedFilters: 'getAppliedConversationFilters',
}),
hasAppliedFilters() {
return this.appliedFilters.length;
},
assigneeTabItems() {
return this.$t('CHAT_LIST.ASSIGNEE_TYPE_TABS').map(item => {
const count = this.conversationStats[item.COUNT_KEY] || 0;
@@ -131,9 +188,17 @@ export default {
this.activeAssigneeTab
);
},
currentPageFilterKey() {
return this.hasAppliedFilters ? 'appliedFilters' : this.activeAssigneeTab;
},
currentFiltersPage() {
return this.$store.getters['conversationPage/getCurrentPageFilter'](
this.currentPageFilterKey
);
},
hasCurrentPageEndReached() {
return this.$store.getters['conversationPage/getHasEndReached'](
this.activeAssigneeTab
this.currentPageFilterKey
);
},
conversationFilters() {
@@ -144,6 +209,9 @@ export default {
page: this.currentPage + 1,
labels: this.label ? [this.label] : undefined,
teamId: this.teamId ? this.teamId : undefined,
conversationType: this.conversationType
? this.conversationType
: undefined,
};
},
pageTitle() {
@@ -156,17 +224,24 @@ export default {
if (this.label) {
return `#${this.label}`;
}
if (this.conversationType === 'mention') {
return this.$t('CHAT_LIST.MENTION_HEADING');
}
return this.$t('CHAT_LIST.TAB_HEADING');
},
conversationList() {
let conversationList = [];
const filters = this.conversationFilters;
if (this.activeAssigneeTab === 'me') {
conversationList = [...this.mineChatsList(filters)];
} else if (this.activeAssigneeTab === 'unassigned') {
conversationList = [...this.unAssignedChatsList(filters)];
if (!this.hasAppliedFilters) {
const filters = this.conversationFilters;
if (this.activeAssigneeTab === 'me') {
conversationList = [...this.mineChatsList(filters)];
} else if (this.activeAssigneeTab === 'unassigned') {
conversationList = [...this.unAssignedChatsList(filters)];
} else {
conversationList = [...this.allChatList(filters)];
}
} else {
conversationList = [...this.allChatList(filters)];
conversationList = [...this.chatLists];
}
return conversationList;
@@ -188,6 +263,9 @@ export default {
label() {
this.resetAndFetchData();
},
conversationType() {
this.resetAndFetchData();
},
},
mounted() {
this.$store.dispatch('setChatFilter', this.activeStatus);
@@ -198,6 +276,17 @@ export default {
});
},
methods: {
onApplyFilter(payload) {
if (this.$route.name !== 'home') {
this.$router.push({ name: 'home' });
}
this.$store.dispatch('conversationPage/reset');
this.$store.dispatch('emptyAllConversations');
this.fetchFilteredConversations(payload);
},
onToggleAdvanceFiltersModal() {
this.showAdvancedFilters = !this.showAdvancedFilters;
},
getKeyboardListenerParams() {
const allConversations = this.$refs.activeConversation.querySelectorAll(
'div.conversations-list div.conversation'
@@ -245,6 +334,7 @@ export default {
resetAndFetchData() {
this.$store.dispatch('conversationPage/reset');
this.$store.dispatch('emptyAllConversations');
this.$store.dispatch('clearConversationFilters');
this.fetchConversations();
},
fetchConversations() {
@@ -252,6 +342,23 @@ export default {
.dispatch('fetchAllConversations', this.conversationFilters)
.then(() => this.$emit('conversation-load'));
},
loadMoreConversations() {
if (!this.hasAppliedFilters) {
this.fetchConversations();
} else {
this.fetchFilteredConversations(this.appliedFilters);
}
},
fetchFilteredConversations(payload) {
let page = this.currentFiltersPage + 1;
this.$store
.dispatch('fetchFilteredConversations', {
queryData: filterQueryGenerator(payload),
page,
})
.then(() => this.$emit('conversation-load'));
this.showAdvancedFilters = false;
},
updateAssigneeTab(selectedTab) {
if (this.activeAssigneeTab !== selectedTab) {
bus.$emit('clearSearchInput');
@@ -273,6 +380,7 @@ export default {
<style scoped lang="scss">
@import '~dashboard/assets/scss/woot';
.spinner {
margin-top: var(--space-normal);
margin-bottom: var(--space-normal);
@@ -295,4 +403,17 @@ export default {
flex-basis: 46rem;
}
}
.filter--actions {
display: flex;
align-items: center;
}
.btn-filter {
margin: 0 var(--space-smaller);
}
.filter__applied {
padding: var(--space-slab) 0 !important;
border-bottom: 1px solid var(--color-border);
}
</style>
@@ -0,0 +1,354 @@
<template>
<div class="custom-attribute">
<div class="title-wrap">
<h4 class="text-block-title title error">
<div v-if="isAttributeTypeCheckbox" class="checkbox-wrap">
<input
v-model="editedValue"
class="checkbox"
type="checkbox"
@change="onUpdate"
/>
</div>
<div class="name-button__wrap">
<span
class="attribute-name"
:class="{ error: $v.editedValue.$error }"
>
{{ label }}
</span>
<woot-button
v-if="showActions"
v-tooltip.left="$t('CUSTOM_ATTRIBUTES.ACTIONS.DELETE')"
variant="link"
size="medium"
color-scheme="secondary"
icon="delete"
class-names="delete-button"
@click="onDelete"
/>
</div>
</h4>
</div>
<div v-if="notAttributeTypeCheckboxAndList">
<div v-show="isEditing">
<div class="input-group small">
<input
ref="inputfield"
v-model="editedValue"
:type="inputType"
class="input-group-field"
autofocus="true"
:class="{ error: $v.editedValue.$error }"
@blur="$v.editedValue.$touch"
@keyup.enter="onUpdate"
/>
<div class="input-group-button">
<woot-button size="small" icon="checkmark" @click="onUpdate" />
</div>
</div>
<span v-if="shouldShowErrorMessage" class="error-message">
{{ errorMessage }}
</span>
</div>
<div
v-show="!isEditing"
class="value--view"
:class="{ 'is-editable': showActions }"
>
<a
v-if="isAttributeTypeLink"
:href="value"
target="_blank"
rel="noopener noreferrer"
class="value"
>
{{ value || '---' }}
</a>
<p v-else class="value">
{{ displayValue || '---' }}
</p>
<div class="action-buttons__wrap">
<woot-button
v-if="showActions"
v-tooltip="$t('CUSTOM_ATTRIBUTES.ACTIONS.COPY')"
variant="link"
size="small"
color-scheme="secondary"
icon="clipboard"
class-names="edit-button"
@click="onCopy"
/>
<woot-button
v-if="showActions"
v-tooltip.right="$t('CUSTOM_ATTRIBUTES.ACTIONS.EDIT')"
variant="link"
size="small"
color-scheme="secondary"
icon="edit"
class-names="edit-button"
@click="onEdit"
/>
</div>
</div>
</div>
<div v-if="isAttributeTypeList">
<multiselect-dropdown
:options="listOptions"
:selected-item="selectedItem"
:has-thumbnail="false"
:multiselector-placeholder="
$t('CUSTOM_ATTRIBUTES.FORM.ATTRIBUTE_TYPE.LIST.PLACEHOLDER')
"
:no-search-result="
$t('CUSTOM_ATTRIBUTES.FORM.ATTRIBUTE_TYPE.LIST.NO_RESULT')
"
:input-placeholder="
$t(
'CUSTOM_ATTRIBUTES.FORM.ATTRIBUTE_TYPE.LIST.SEARCH_INPUT_PLACEHOLDER'
)
"
@click="onUpdateListValue"
/>
</div>
</div>
</template>
<script>
import format from 'date-fns/format';
import { required, url } from 'vuelidate/lib/validators';
import { BUS_EVENTS } from 'shared/constants/busEvents';
import MultiselectDropdown from 'shared/components/ui/MultiselectDropdown.vue';
const DATE_FORMAT = 'yyyy-MM-dd';
export default {
components: {
MultiselectDropdown,
},
props: {
label: { type: String, required: true },
values: { type: Array, default: () => [] },
value: { type: [String, Number, Boolean], default: '' },
showActions: { type: Boolean, default: false },
attributeType: { type: String, default: 'text' },
attributeKey: { type: String, required: true },
contactId: { type: Number, default: null },
},
data() {
return {
isEditing: false,
editedValue: null,
};
},
validations() {
if (this.isAttributeTypeLink) {
return {
editedValue: { required, url },
};
}
return {
editedValue: { required },
};
},
computed: {
formattedValue() {
if (this.isAttributeTypeDate) {
return format(new Date(this.value || new Date()), DATE_FORMAT);
}
if (this.isAttributeTypeCheckbox) {
return this.value === 'false' ? false : this.value;
}
return this.value;
},
listOptions() {
return this.values.map((value, index) => ({
id: index + 1,
name: value,
}));
},
selectedItem() {
const id = this.values.indexOf(this.editedValue) + 1;
return { id, name: this.editedValue };
},
isAttributeTypeCheckbox() {
return this.attributeType === 'checkbox';
},
isAttributeTypeList() {
return this.attributeType === 'list';
},
isAttributeTypeLink() {
return this.attributeType === 'link';
},
isAttributeTypeDate() {
return this.attributeType === 'date';
},
notAttributeTypeCheckboxAndList() {
return !this.isAttributeTypeCheckbox && !this.isAttributeTypeList;
},
inputType() {
return this.isAttributeTypeLink ? 'url' : this.attributeType;
},
shouldShowErrorMessage() {
return this.$v.editedValue.$error;
},
errorMessage() {
if (this.$v.editedValue.url) {
return this.$t('CUSTOM_ATTRIBUTES.VALIDATIONS.INVALID_URL');
}
return this.$t('CUSTOM_ATTRIBUTES.VALIDATIONS.REQUIRED');
},
displayValue() {
if (this.attributeType === 'date') {
return format(new Date(this.editedValue), 'dd-MM-yyyy');
}
return this.editedValue;
},
},
mounted() {
this.editedValue = this.formattedValue;
bus.$on(BUS_EVENTS.FOCUS_CUSTOM_ATTRIBUTE, focusAttributeKey => {
if (this.attributeKey === focusAttributeKey) {
this.onEdit();
}
});
},
methods: {
focusInput() {
if (this.$refs.inputfield) {
this.$refs.inputfield.focus();
}
},
onEdit() {
this.isEditing = true;
this.$nextTick(() => {
this.focusInput();
});
},
onUpdateListValue(value) {
if (value) {
this.editedValue = value.name;
this.onUpdate();
}
},
onUpdate() {
const updatedValue =
this.attributeType === 'date'
? format(new Date(this.editedValue), DATE_FORMAT)
: this.editedValue;
this.$v.$touch();
if (this.$v.$invalid) {
return;
}
this.isEditing = false;
this.$emit('update', this.attributeKey, updatedValue);
},
onDelete() {
this.isEditing = false;
this.$emit('delete', this.attributeKey);
},
onCopy() {
this.$emit('copy', this.value);
},
},
};
</script>
<style lang="scss" scoped>
.custom-attribute {
padding: var(--space-slab) var(--space-normal);
}
.title-wrap {
display: flex;
align-items: center;
margin-bottom: var(--space-mini);
}
.title {
display: flex;
align-items: center;
margin: 0;
width: 100%;
}
.checkbox-wrap {
display: flex;
align-items: center;
}
.checkbox {
margin: 0 var(--space-small) 0 0;
}
.name-button__wrap {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}
.attribute-name {
width: 100%;
&.error {
color: var(--r-400);
}
}
.title--icon {
width: var(--space-two);
}
.edit-button {
display: none;
}
.delete-button {
display: flex;
justify-content: flex-end;
width: var(--space-normal);
}
.value--view {
display: flex;
&.is-editable:hover {
.value {
background: var(--color-background);
margin-bottom: 0;
}
.edit-button {
display: block;
}
}
.action-buttons__wrap {
display: flex;
max-width: var(--space-larger);
}
}
.value {
display: inline-block;
min-width: var(--space-mega);
border-radius: var(--border-radius-small);
margin-bottom: 0;
word-break: break-all;
padding: var(--space-micro) var(--space-smaller);
}
.error-message {
color: var(--r-400);
display: block;
font-size: 1.4rem;
font-size: var(--font-size-small);
font-weight: 400;
margin-bottom: 1rem;
margin-top: -1.6rem;
width: 100%;
}
::v-deep {
.selector-wrap {
margin: 0;
top: var(--space-smaller);
.selector-name {
margin-left: 0;
}
}
.name {
margin-left: 0;
}
}
</style>
@@ -7,7 +7,9 @@
@click="onBackDropClick"
>
<div :class="modalContainerClassName" @click.stop>
<i class="ion-android-close modal--close" @click="close"></i>
<button class="modal--close" @click="close">
<fluent-icon icon="dismiss" />
</button>
<slot />
</div>
</div>
@@ -34,6 +36,10 @@ export default {
type: String,
default: 'centered',
},
size: {
type: String,
default: '',
},
},
computed: {
modalContainerClassName() {
@@ -41,7 +47,8 @@ export default {
if (this.fullWidth) {
return `${className} modal-container--full-width`;
}
return className;
return `${className} ${this.size}`;
},
modalClassName() {
const modalClassNameMap = {
@@ -2,29 +2,21 @@
<transition name="network-notification-fade" tag="div">
<div v-show="showNotification" class="ui-notification-container">
<div class="ui-notification">
<svg
class="ui-notification-icon"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M18.364 5.636a9 9 0 010 12.728m0 0l-2.829-2.829m2.829 2.829L21 21M15.536 8.464a5 5 0 010 7.072m0 0l-2.829-2.829m-4.243 2.829a4.978 4.978 0 01-1.414-2.83m-1.414 5.658a9 9 0 01-2.167-9.238m7.824 2.167a1 1 0 111.414 1.414m-1.414-1.414L3 3m8.293 8.293l1.414 1.414"
/>
</svg>
<fluent-icon icon="wifi-off" />
<p class="ui-notification-text">
{{ $t('NETWORK.NOTIFICATION.TEXT') }}
</p>
<button class="ui-refresh-button" @click="refreshPage">
<woot-button variant="clear" size="small" @click="refreshPage">
{{ $t('NETWORK.BUTTON.REFRESH') }}
</button>
<button class="ui-close-button" @click="closeNotification">
<i class="ui-close-icon icon ion-ios-close-outline" />
</button>
</woot-button>
<woot-button
variant="clear"
size="small"
color-scheme="secondary"
icon="dismiss-circle"
@click="closeNotification"
>
</woot-button>
</div>
</div>
</transition>
@@ -72,52 +64,24 @@ export default {
position: absolute;
right: var(--space-normal);
top: var(--space-normal);
width: 100%;
z-index: 9999;
z-index: var(--z-index-very-high);
}
.ui-notification {
@include shadow;
align-items: center;
background-color: var(--white);
border: 1px solid var(--color-border);
border-radius: var(--space-one);
display: flex;
justify-content: space-between;
max-width: 40rem;
min-height: 3rem;
align-items: center;
width: 100%;
background-color: var(--y-100);
border-radius: var(--border-radius-medium);
box-shadow: var(--shadow-large);
min-width: 24rem;
padding: var(--space-normal) var(--space-two);
text-align: left;
padding: var(--space-normal);
}
.ui-notification-text {
margin: 0;
}
.ui-refresh-button {
color: var(--color-woot);
font-size: var(--font-size-small);
font-weight: bold;
&:hover {
cursor: pointer;
}
}
.ui-notification-icon {
color: var(--b-600);
width: var(--font-size-mega);
}
.ui-close-icon {
color: var(--b-600);
font-size: var(--font-size-large);
}
.ui-close-button {
&:hover {
cursor: pointer;
}
margin: 0 var(--space-small);
}
</style>
@@ -1,18 +1,28 @@
<template>
<i class="ion-android-menu hamburger--menu" @click="onMenuItemClick" />
<button @click="onMenuItemClick">
<fluent-icon class="hamburger--menu" icon="list" />
</button>
</template>
<script>
import { BUS_EVENTS } from 'shared/constants/busEvents';
export default {
methods: {
onMenuItemClick() {
bus.$emit('sidemenu_icon_click');
bus.$emit(BUS_EVENTS.TOGGLE_SIDEMENU);
},
},
};
</script>
<style lang="scss" scoped>
.ion-android-menu {
font-size: var(--font-size-big);
<style scoped lang="scss">
.hamburger--menu {
cursor: pointer;
display: none;
margin-right: var(--space-normal);
@media screen and (max-width: 1200px) {
display: block;
}
}
</style>
@@ -1,16 +1,17 @@
<template>
<div>
<div class="ui-snackbar">
<div class="ui-snackbar-text">{{ message }}</div>
<div class="ui-snackbar-text">
{{ message }}
</div>
</div>
</div>
</template>
<script>
export default {
props: {
message: String,
message: { type: String, default: '' },
showButton: Boolean,
duration: {
type: [String, Number],
@@ -22,9 +23,7 @@ export default {
toggleAfterTimeout: false,
};
},
methods: {
},
mounted() {
},
mounted() {},
methods: {},
};
</script>
@@ -29,12 +29,18 @@ export default {
},
mounted() {
bus.$on('newToastMessage', message => {
bus.$on('newToastMessage', this.onNewToastMessage);
},
beforeDestroy() {
bus.$off('newToastMessage', this.onNewToastMessage);
},
methods: {
onNewToastMessage(message) {
this.snackMessages.push({ key: new Date().getTime(), message });
window.setTimeout(() => {
this.snackMessages.splice(0, 1);
}, this.duration);
});
},
},
};
</script>
@@ -1,9 +1,10 @@
<template>
<button :type="type" class="button nice" :class="variant" @click="onClick">
<i
<fluent-icon
v-if="!isLoading && icon"
class="icon"
:class="buttonIconClass + ' ' + icon"
:class="buttonIconClass"
:icon="icon"
/>
<spinner v-if="isLoading" />
<slot></slot>
@@ -6,7 +6,7 @@
:class="computedClass"
@click="onClick"
>
<i v-if="!!iconClass" :class="iconClass" class="icon" />
<fluent-icon v-if="!!iconClass" :icon="iconClass" class="icon" />
<span>{{ buttonText }}</span>
<spinner v-if="loading" />
</button>
@@ -5,10 +5,10 @@
v-if="isOpen"
class-names="resolve"
color-scheme="success"
icon="ion-checkmark"
icon="checkmark"
emoji="✅"
:is-loading="isLoading"
@click="() => toggleStatus(STATUS_TYPE.RESOLVED)"
@click="onCmdResolveConversation"
>
{{ this.$t('CONVERSATION.HEADER.RESOLVE_ACTION') }}
</woot-button>
@@ -16,10 +16,10 @@
v-else-if="isResolved"
class-names="resolve"
color-scheme="warning"
icon="ion-refresh"
icon="arrow-redo"
emoji="👀"
:is-loading="isLoading"
@click="() => toggleStatus(STATUS_TYPE.OPEN)"
@click="onCmdOpenConversation"
>
{{ this.$t('CONVERSATION.HEADER.REOPEN_ACTION') }}
</woot-button>
@@ -27,9 +27,9 @@
v-else-if="showOpenButton"
class-names="resolve"
color-scheme="primary"
icon="ion-person"
icon="person"
:is-loading="isLoading"
@click="() => toggleStatus(STATUS_TYPE.OPEN)"
@click="onCmdOpenConversation"
>
{{ this.$t('CONVERSATION.HEADER.OPEN_ACTION') }}
</woot-button>
@@ -38,7 +38,7 @@
ref="arrowDownButton"
:color-scheme="buttonClass"
:disabled="isLoading"
icon="ion-arrow-down-b"
icon="chevron-down"
emoji="🔽"
@click="openDropdown"
/>
@@ -52,12 +52,16 @@
<woot-dropdown-item v-if="!isPending">
<woot-button
variant="clear"
color-scheme="secondary"
size="small"
icon="book-clock"
@click="() => toggleStatus(STATUS_TYPE.PENDING)"
>
{{ this.$t('CONVERSATION.RESOLVE_DROPDOWN.MARK_PENDING') }}
</woot-button>
</woot-dropdown-item>
<woot-dropdown-divider v-if="isOpen" />
<woot-dropdown-sub-menu
v-if="isOpen"
:title="this.$t('CONVERSATION.RESOLVE_DROPDOWN.SNOOZE.TITLE')"
@@ -65,6 +69,9 @@
<woot-dropdown-item>
<woot-button
variant="clear"
color-scheme="secondary"
size="small"
icon="send-clock"
@click="() => toggleStatus(STATUS_TYPE.SNOOZED, null)"
>
{{ this.$t('CONVERSATION.RESOLVE_DROPDOWN.SNOOZE.NEXT_REPLY') }}
@@ -73,6 +80,9 @@
<woot-dropdown-item>
<woot-button
variant="clear"
color-scheme="secondary"
size="small"
icon="dual-screen-clock"
@click="
() => toggleStatus(STATUS_TYPE.SNOOZED, snoozeTimes.tomorrow)
"
@@ -83,6 +93,9 @@
<woot-dropdown-item>
<woot-button
variant="clear"
color-scheme="secondary"
size="small"
icon="calendar-clock"
@click="
() => toggleStatus(STATUS_TYPE.SNOOZED, snoozeTimes.nextWeek)
"
@@ -110,6 +123,8 @@ import {
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
import WootDropdownSubMenu from 'shared/components/ui/dropdown/DropdownSubMenu.vue';
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
import WootDropdownDivider from 'shared/components/ui/dropdown/DropdownDivider';
import wootConstants from '../../constants';
import {
getUnixTime,
@@ -118,12 +133,18 @@ import {
startOfTomorrow,
startOfWeek,
} from 'date-fns';
import {
CMD_REOPEN_CONVERSATION,
CMD_RESOLVE_CONVERSATION,
CMD_SNOOZE_CONVERSATION,
} from '../../routes/dashboard/commands/commandBarBusEvents';
export default {
components: {
WootDropdownItem,
WootDropdownMenu,
WootDropdownSubMenu,
WootDropdownDivider,
},
mixins: [clickaway, alertMixin, eventListenerMixins],
props: { conversationId: { type: [String, Number], required: true } },
@@ -135,9 +156,7 @@ export default {
};
},
computed: {
...mapGetters({
currentChat: 'getSelectedChat',
}),
...mapGetters({ currentChat: 'getSelectedChat' }),
isOpen() {
return this.currentChat.status === wootConstants.STATUS_TYPE.OPEN;
},
@@ -170,6 +189,16 @@ export default {
};
},
},
mounted() {
bus.$on(CMD_SNOOZE_CONVERSATION, this.onCmdSnoozeConversation);
bus.$on(CMD_REOPEN_CONVERSATION, this.onCmdOpenConversation);
bus.$on(CMD_RESOLVE_CONVERSATION, this.onCmdResolveConversation);
},
destroyed() {
bus.$off(CMD_SNOOZE_CONVERSATION, this.onCmdSnoozeConversation);
bus.$off(CMD_REOPEN_CONVERSATION, this.onCmdOpenConversation);
bus.$off(CMD_RESOLVE_CONVERSATION, this.onCmdResolveConversation);
},
methods: {
async handleKeyEvents(e) {
const allConversations = document.querySelectorAll(
@@ -204,6 +233,18 @@ export default {
}
}
},
onCmdSnoozeConversation(snoozeType) {
this.toggleStatus(
this.STATUS_TYPE.SNOOZED,
this.snoozeTimes[snoozeType] || null
);
},
onCmdOpenConversation() {
this.toggleStatus(this.STATUS_TYPE.OPEN);
},
onCmdResolveConversation() {
this.toggleStatus(this.STATUS_TYPE.RESOLVED);
},
showOpenButton() {
return this.isResolved || this.isSnoozed;
},
@@ -244,5 +285,6 @@ export default {
margin-top: var(--space-micro);
right: 0;
max-width: 20rem;
min-width: 15.6rem;
}
</style>
@@ -1,66 +1,41 @@
<template>
<div class="status">
<div class="status-view">
<availability-status-badge :status="currentUserAvailability" />
<div class="status-view--title">
{{ availabilityDisplayLabel }}
</div>
</div>
<div class="status-change">
<transition name="menu-slide">
<div
v-if="isStatusMenuOpened"
v-on-clickaway="closeStatusMenu"
class="dropdown-pane dropdowm--top"
>
<woot-dropdown-menu>
<woot-dropdown-item
v-for="status in availabilityStatuses"
:key="status.value"
class="status-items"
>
<woot-button
variant="clear"
size="small"
color-scheme="secondary"
class-names="status-change--dropdown-button"
:is-disabled="status.disabled"
@click="
changeAvailabilityStatus(status.value, currentAccountId)
"
>
<availability-status-badge :status="status.value" />
{{ status.label }}
</woot-button>
</woot-dropdown-item>
</woot-dropdown-menu>
</div>
</transition>
<woot-dropdown-menu>
<woot-dropdown-header :title="$t('SIDEBAR.SET_AVAILABILITY_TITLE')" />
<woot-dropdown-item
v-for="status in availabilityStatuses"
:key="status.value"
class="status-items"
>
<woot-button
variant="clear"
color-scheme="secondary"
class-names="status-change--change-button link"
@click="openStatusMenu"
size="small"
:color-scheme="status.disabled ? '' : 'secondary'"
:variant="status.disabled ? 'smooth' : 'clear'"
class-names="status-change--dropdown-button"
@click="changeAvailabilityStatus(status.value)"
>
{{ $t('SIDEBAR_ITEMS.CHANGE_AVAILABILITY_STATUS') }}
<availability-status-badge :status="status.value" />
{{ status.label }}
</woot-button>
</div>
</div>
</woot-dropdown-item>
<woot-dropdown-divider />
</woot-dropdown-menu>
</template>
<script>
import { mapGetters } from 'vuex';
import { mixin as clickaway } from 'vue-clickaway';
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
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 AvailabilityStatusBadge from '../widgets/conversation/AvailabilityStatusBadge';
const AVAILABILITY_STATUS_KEYS = ['online', 'busy', 'offline'];
export default {
components: {
WootDropdownHeader,
WootDropdownDivider,
WootDropdownMenu,
WootDropdownItem,
AvailabilityStatusBadge,
@@ -100,8 +75,7 @@ export default {
label: statusLabel,
value: AVAILABILITY_STATUS_KEYS[index],
disabled:
this.currentUserAvailability ===
AVAILABILITY_STATUS_KEYS[index],
this.currentUserAvailability === AVAILABILITY_STATUS_KEYS[index],
})
);
},
@@ -114,7 +88,8 @@ export default {
closeStatusMenu() {
this.isStatusMenuOpened = false;
},
changeAvailabilityStatus(availability, accountId) {
changeAvailabilityStatus(availability) {
const accountId = this.currentAccountId;
if (this.isUpdating) {
return;
}
@@ -1,77 +1,37 @@
<template>
<aside class="sidebar animated shrink columns">
<div class="logo">
<router-link :to="dashboardPath" replace>
<img :src="globalConfig.logo" :alt="globalConfig.installationName" />
</router-link>
</div>
<div class="main-nav">
<transition-group name="menu-list" tag="ul" class="menu vertical">
<sidebar-item
v-for="item in accessibleMenuItems"
:key="item.toState"
:menu-item="item"
/>
<sidebar-item
v-if="shouldShowTeams"
:key="teamSection.toState"
:menu-item="teamSection"
/>
<sidebar-item
v-if="shouldShowSidebarItem"
:key="inboxSection.toState"
:menu-item="inboxSection"
/>
<sidebar-item
v-if="shouldShowSidebarItem"
:key="labelSection.toState"
:menu-item="labelSection"
@add-label="showAddLabelPopup"
/>
<sidebar-item
v-if="showShowContactSideMenu"
:key="contactLabelSection.key"
:menu-item="contactLabelSection"
@add-label="showAddLabelPopup"
/>
</transition-group>
</div>
<div class="bottom-nav">
<availability-status />
</div>
<div class="bottom-nav app-context-menu" @click="toggleOptions">
<agent-details @show-options="toggleOptions" />
<notification-bell />
<span class="current-user--options icon ion-android-more-vertical" />
<options-menu
:show="showOptionsMenu"
@toggle-accounts="toggleAccountModal"
@show-support-chat-window="toggleSupportChatWindow"
@key-shortcut-modal="toggleKeyShortcutModal"
@close="toggleOptions"
/>
</div>
<aside class="woot-sidebar">
<primary-sidebar
:logo-source="globalConfig.logoThumbnail"
:installation-name="globalConfig.installationName"
:account-id="accountId"
:menu-items="primaryMenuItems"
:active-menu-item="activePrimaryMenu.key"
@toggle-accounts="toggleAccountModal"
@key-shortcut-modal="toggleKeyShortcutModal"
/>
<secondary-sidebar
:account-id="accountId"
:inboxes="inboxes"
:labels="labels"
:teams="teams"
:menu-config="activeSecondaryMenu"
:current-role="currentRole"
@add-label="showAddLabelPopup"
/>
<woot-key-shortcut-modal
v-if="showShortcutModal"
@close="closeKeyShortcutModal"
@clickaway="closeKeyShortcutModal"
/>
<account-selector
:show-account-modal="showAccountModal"
@close-account-modal="toggleAccountModal"
@show-create-account-modal="openCreateAccountModal"
/>
<add-account-modal
:show="showCreateAccountModal"
@close-account-create-modal="closeCreateAccountModal"
/>
<woot-modal :show.sync="showAddLabelModal" :on-close="hideAddLabelPopup">
<add-label-modal @close="hideAddLabelPopup" />
</woot-modal>
@@ -82,17 +42,14 @@
import { mapGetters } from 'vuex';
import adminMixin from '../../mixins/isAdmin';
import SidebarItem from './SidebarItem';
import AvailabilityStatus from './AvailabilityStatus';
import { frontendURL } from '../../helper/URLHelper';
import { getSidebarItems } from '../../i18n/default-sidebar';
import { getSidebarItems } from './config/default-sidebar';
import alertMixin from 'shared/mixins/alertMixin';
import NotificationBell from './sidebarComponents/NotificationBell';
import AgentDetails from './sidebarComponents/AgentDetails.vue';
import OptionsMenu from './sidebarComponents/OptionsMenu.vue';
import AccountSelector from './sidebarComponents/AccountSelector.vue';
import AddAccountModal from './sidebarComponents/AddAccountModal.vue';
import AddLabelModal from '../../routes/dashboard/settings/labels/AddLabel';
import PrimarySidebar from './sidebarComponents/Primary';
import SecondarySidebar from './sidebarComponents/Secondary';
import WootKeyShortcutModal from 'components/widgets/modal/WootKeyShortcutModal';
import {
hasPressedAltAndCKey,
@@ -107,14 +64,11 @@ import router from '../../routes';
export default {
components: {
AgentDetails,
SidebarItem,
AvailabilityStatus,
NotificationBell,
OptionsMenu,
AccountSelector,
AddAccountModal,
AddLabelModal,
PrimarySidebar,
SecondarySidebar,
WootKeyShortcutModal,
},
mixins: [adminMixin, alertMixin, eventListenerMixins],
@@ -135,129 +89,34 @@ export default {
inboxes: 'inboxes/getInboxes',
accountId: 'getCurrentAccountId',
currentRole: 'getCurrentRole',
accountLabels: 'labels/getLabelsOnSidebar',
labels: 'labels/getLabelsOnSidebar',
teams: 'teams/getMyTeams',
}),
sidemenuItems() {
sideMenuConfig() {
return getSidebarItems(this.accountId);
},
accessibleMenuItems() {
// get all keys in menuGroup
const groupKey = Object.keys(this.sidemenuItems);
primaryMenuItems() {
const menuItems = this.sideMenuConfig.primaryMenu;
return menuItems.filter(menuItem =>
menuItem.roles.includes(this.currentRole)
);
},
activeSecondaryMenu() {
const { secondaryMenu } = this.sideMenuConfig;
const { name: currentRoute } = this.$route;
let menuItems = [];
// Iterate over menuGroup to find the correct group
for (let i = 0; i < groupKey.length; i += 1) {
const groupItem = this.sidemenuItems[groupKey[i]];
// Check if current route is included
const isRouteIncluded = groupItem.routes.includes(this.currentRoute);
if (isRouteIncluded) {
menuItems = Object.values(groupItem.menuItems);
}
}
return this.filterMenuItemsByRole(menuItems);
const activeSecondaryMenu =
secondaryMenu.find(menuItem =>
menuItem.routes.includes(currentRoute)
) || {};
return activeSecondaryMenu;
},
currentRoute() {
return this.$store.state.route.name;
},
shouldShowSidebarItem() {
return this.sidemenuItems.common.routes.includes(this.currentRoute);
},
showShowContactSideMenu() {
return this.sidemenuItems.contacts.routes.includes(this.currentRoute);
},
shouldShowTeams() {
return this.shouldShowSidebarItem && this.teams.length;
},
inboxSection() {
return {
icon: 'ion-folder',
label: 'INBOXES',
hasSubMenu: true,
newLink: true,
key: 'inbox',
cssClass: 'menu-title align-justify',
toState: frontendURL(`accounts/${this.accountId}/settings/inboxes`),
toStateName: 'settings_inbox_list',
newLinkRouteName: 'settings_inbox_new',
children: this.inboxes.map(inbox => ({
id: inbox.id,
label: inbox.name,
toState: frontendURL(`accounts/${this.accountId}/inbox/${inbox.id}`),
type: inbox.channel_type,
phoneNumber: inbox.phone_number,
})),
};
},
labelSection() {
return {
icon: 'ion-pound',
label: 'LABELS',
hasSubMenu: true,
newLink: true,
key: 'label',
cssClass: 'menu-title align-justify',
toState: frontendURL(`accounts/${this.accountId}/settings/labels`),
toStateName: 'labels_list',
showModalForNewItem: true,
modalName: 'AddLabel',
children: this.accountLabels.map(label => ({
id: label.id,
label: label.title,
color: label.color,
truncateLabel: true,
toState: frontendURL(
`accounts/${this.accountId}/label/${label.title}`
),
})),
};
},
contactLabelSection() {
return {
icon: 'ion-pound',
label: 'TAGGED_WITH',
hasSubMenu: true,
key: 'label',
newLink: false,
cssClass: 'menu-title align-justify',
toState: frontendURL(`accounts/${this.accountId}/settings/labels`),
toStateName: 'labels_list',
showModalForNewItem: true,
modalName: 'AddLabel',
children: this.accountLabels.map(label => ({
id: label.id,
label: label.title,
color: label.color,
truncateLabel: true,
toState: frontendURL(
`accounts/${this.accountId}/labels/${label.title}/contacts`
),
})),
};
},
teamSection() {
return {
icon: 'ion-ios-people',
label: 'TEAMS',
hasSubMenu: true,
newLink: true,
key: 'team',
cssClass: 'menu-title align-justify teams-sidebar-menu',
toState: frontendURL(`accounts/${this.accountId}/settings/teams`),
toStateName: 'teams_list',
newLinkRouteName: 'settings_teams_new',
children: this.teams.map(team => ({
id: team.id,
label: team.name,
truncateLabel: true,
toState: frontendURL(`accounts/${this.accountId}/team/${team.id}`),
})),
};
},
dashboardPath() {
return frontendURL(`accounts/${this.accountId}/dashboard`);
activePrimaryMenu() {
const activePrimaryMenu =
this.primaryMenuItems.find(
menuItem => menuItem.key === this.activeSecondaryMenu.parentNav
) || {};
return activePrimaryMenu;
},
},
mounted() {
@@ -265,6 +124,7 @@ export default {
this.$store.dispatch('inboxes/get');
this.$store.dispatch('notifications/unReadCount');
this.$store.dispatch('teams/get');
this.$store.dispatch('attributes/get');
},
methods: {
@@ -301,25 +161,11 @@ export default {
}
},
isCurrentRouteSameAsNavigation(routeName) {
return router.currentRoute && router.currentRoute.name === routeName;
return this.$route.name === routeName;
},
toggleSupportChatWindow() {
window.$chatwoot.toggle();
},
filterMenuItemsByRole(menuItems) {
if (!this.currentRole) {
return [];
}
return menuItems.filter(
menuItem =>
window.roleWiseRoutes[this.currentRole].indexOf(
menuItem.toStateName
) > -1
);
},
toggleOptions() {
this.showOptionsMenu = !this.showOptionsMenu;
},
toggleAccountModal() {
this.showAccountModal = !this.showAccountModal;
},
@@ -340,6 +186,13 @@ export default {
};
</script>
<style lang="scss" scoped>
.woot-sidebar {
background: var(--white);
display: flex;
}
</style>
<style lang="scss">
@import '~dashboard/assets/scss/variables';
@@ -353,11 +206,11 @@ export default {
cursor: pointer;
padding: $space-small $space-large;
.ion-ios-checkmark {
font-size: $font-size-big;
.selected--account {
margin-top: -$space-smaller;
& + .account--details {
padding-left: $space-normal;
padding-left: $space-normal - $space-micro;
}
}
@@ -404,7 +257,7 @@ export default {
margin-top: auto;
}
.teams-sidebar-menu + .nested.vertical.menu {
padding-left: calc(var(--space-medium) - var(--space-one));
.secondary-menu .nested.vertical.menu {
margin-left: var(--space-small);
}
</style>
@@ -1,154 +0,0 @@
<template>
<router-link
:to="menuItem.toState"
tag="li"
active-class="active"
:class="computedClass"
>
<a
class="sub-menu-title"
:class="getMenuItemClass"
data-tooltip
aria-haspopup="true"
:title="menuItem.toolTip"
>
<div class="wrap">
<i :class="menuItem.icon" />
{{ $t(`SIDEBAR.${menuItem.label}`) }}
</div>
<span
v-if="showItem(menuItem)"
class="child-icon ion-android-add-circle"
@click.prevent="newLinkClick(menuItem)"
/>
</a>
<ul v-if="menuItem.hasSubMenu" class="nested vertical menu">
<router-link
v-for="child in menuItem.children"
:key="child.id"
active-class="active flex-container"
tag="li"
:to="child.toState"
>
<a href="#" :class="computedChildClass(child)">
<div class="wrap">
<i
v-if="menuItem.key === 'inbox'"
class="inbox-icon"
:class="computedInboxClass(child)"
/>
<span
v-if="child.color"
class="label-color--display"
:style="{ backgroundColor: child.color }"
/>
<div
:title="computedChildTitle(child)"
:class="computedChildClass(child)"
>
{{ child.label }}
</div>
</div>
</a>
</router-link>
</ul>
</router-link>
</template>
<script>
import { mapGetters } from 'vuex';
import router from '../../routes';
import adminMixin from '../../mixins/isAdmin';
import { getInboxClassByType } from 'dashboard/helper/inbox';
export default {
mixins: [adminMixin],
props: {
menuItem: {
type: Object,
default() {
return {};
},
},
},
computed: {
...mapGetters({
activeInbox: 'getSelectedInbox',
}),
getMenuItemClass() {
return this.menuItem.cssClass
? `side-menu ${this.menuItem.cssClass}`
: 'side-menu';
},
computedClass() {
// If active Inbox is present
// donot highlight conversations
if (this.activeInbox) return ' ';
if (
this.$store.state.route.name === 'inbox_conversation' &&
this.menuItem.toStateName === 'home'
) {
return 'active';
}
return ' ';
},
},
methods: {
computedInboxClass(child) {
const { type, phoneNumber } = child;
const classByType = getInboxClassByType(type, phoneNumber);
return classByType;
},
computedChildClass(child) {
if (!child.truncateLabel) return '';
return 'text-truncate';
},
computedChildTitle(child) {
if (!child.truncateLabel) return false;
return child.label;
},
newLinkClick(item) {
if (item.newLinkRouteName) {
router.push({ name: item.newLinkRouteName, params: { page: 'new' } });
} else if (item.showModalForNewItem) {
if (item.modalName === 'AddLabel') {
this.$emit('add-label');
}
}
},
showItem(item) {
return this.isAdmin && item.newLink !== undefined;
},
},
};
</script>
<style lang="scss" scoped>
@import '~dashboard/assets/scss/variables';
.sub-menu-title {
display: flex;
justify-content: space-between;
}
.wrap {
display: flex;
align-items: center;
}
.label-color--display {
border-radius: $space-smaller;
height: $space-normal;
margin-right: $space-small;
min-width: $space-normal;
width: $space-normal;
}
.inbox-icon {
position: relative;
top: -1px;
&.ion-ios-email {
font-size: var(--font-size-medium);
}
}
</style>
@@ -0,0 +1,19 @@
import conversations from './sidebarItems/conversations';
import contacts from './sidebarItems/contacts';
import reports from './sidebarItems/reports';
import campaigns from './sidebarItems/campaigns';
import settings from './sidebarItems/settings';
import notifications from './sidebarItems/notifications';
import primaryMenu from './sidebarItems/primaryMenu';
export const getSidebarItems = accountId => ({
primaryMenu: primaryMenu(accountId),
secondaryMenu: [
conversations(accountId),
contacts(accountId),
reports(accountId),
campaigns(accountId),
settings(accountId),
notifications(accountId),
],
});
@@ -1,30 +1,26 @@
import { frontendURL } from '../../helper/URLHelper';
import { frontendURL } from '../../../../helper/URLHelper';
const campaigns = accountId => ({
parentNav: 'campaigns',
routes: ['settings_account_campaigns', 'one_off'],
menuItems: {
back: {
icon: 'ion-ios-arrow-back',
label: 'HOME',
hasSubMenu: false,
toStateName: 'home',
toState: frontendURL(`accounts/${accountId}/dashboard`),
},
ongoingCampaigns: {
icon: 'ion-arrow-swap',
menuItems: [
{
icon: 'arrow-swap',
label: 'ONGOING',
key: 'ongoingCampaigns',
hasSubMenu: false,
toState: frontendURL(`accounts/${accountId}/campaigns/ongoing`),
toStateName: 'settings_account_campaigns',
},
onOffCampaigns: {
icon: 'ion-radio-waves',
{
key: 'oneOffCampaigns',
icon: 'sound-source',
label: 'ONE_OFF',
hasSubMenu: false,
toState: frontendURL(`accounts/${accountId}/campaigns/one_off`),
toStateName: 'one_off',
},
},
],
});
export default campaigns;
@@ -0,0 +1,21 @@
import { frontendURL } from '../../../../helper/URLHelper';
const contacts = accountId => ({
parentNav: 'contacts',
routes: [
'contacts_dashboard',
'contact_profile_dashboard',
'contacts_labels_dashboard',
],
menuItems: [
{
icon: 'contact-card-group',
label: 'ALL_CONTACTS',
hasSubMenu: false,
toState: frontendURL(`accounts/${accountId}/contacts`),
toStateName: 'contacts_dashboard',
},
],
});
export default contacts;
@@ -0,0 +1,37 @@
import { frontendURL } from '../../../../helper/URLHelper';
const conversations = accountId => ({
parentNav: 'conversations',
routes: [
'home',
'inbox_dashboard',
'inbox_conversation',
'conversation_through_inbox',
'notifications_dashboard',
'label_conversations',
'conversations_through_label',
'team_conversations',
'conversations_through_team',
'conversation_mentions',
'conversation_through_mentions',
],
menuItems: [
{
icon: 'chat',
label: 'ALL_CONVERSATIONS',
key: 'conversations',
toState: frontendURL(`accounts/${accountId}/dashboard`),
toolTip: 'Conversation from all subscribed inboxes',
toStateName: 'home',
},
{
icon: 'mention',
label: 'MENTIONED_CONVERSATIONS',
key: 'conversation_mentions',
toState: frontendURL(`accounts/${accountId}/mentions/conversations`),
toStateName: 'conversation_mentions',
},
],
});
export default conversations;

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