Compare commits

...
Author SHA1 Message Date
Muhsin Keloth 6e90600c03 chore: add packages 2024-04-30 09:13:36 +05:30
Muhsin Keloth 19410dfa3d feat: add linear 2024-04-29 13:27:18 +05:30
Shivam MishraandGitHub 78f94511ff feat: add sitemap for help center (#9280)
* feat: start sitemap

* feat: add base url and last mod to sitemap

* fix: typo

* test: sitemap generation

* test: add draft articles

* fix: escape dots in regex matching

* feat: perpend protocol to the url

* feat: use ChatwootApp.help_center_root

* feat: don't parse the URL

* fix: function declaration
2024-04-26 21:36:39 +05:30
d88d0bdd80 feat: Add user profile avatar (#9298)
* feat: add avatar

* chore: add more colors

* chore: add helpers

* chore: build prettier issues

* chore: refactor shouldShowImage

* chore: code cleanup

* Update app/javascript/v3/components/Form/InitialsAvatar.vue

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

* chore: revire comments

---------

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2024-04-26 16:02:10 +05:30
47f8b2cd0c refactor: handling keyboard shortcuts (#9242)
* fix: Resolve and go next keyboard shortcuts doesn't work

* refactor: use buildHotKeys instead of  hasPressedCommandPlusAltAndEKey

* feat: install tinykeys

* refactor: use tinykeys

* test: update buildKeyEvents

* fix: remove stray import

* feat: handle action list globally

* feat: allow configuring `allowOnFocusedInput`

* chore: Navigate chat list item

* chore: Navigate dashboard

* feat: Navigate editor top panel

* feat: Toggle file upload

* chore: More keyboard shortcuts

* chore: Update mention selection mixin

* chore: Phone input

* chore: Clean up

* chore: Clean up

* chore: Dropdown and editor

* chore: Enter key to send and clean up

* chore: Rename mixin

* chore: Review fixes

* chore: Removed unused shortcut from modal

* fix: Specs

---------

Co-authored-by: iamsivin <iamsivin@gmail.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2024-04-26 15:41:02 +05:30
PranavandGitHub ffd47081bd chore(cleanup): Delete sentiment feature (#9304)
- The feature is unused, removing it for now, will bring it back with better models later.
2024-04-25 22:49:10 -07:00
PranavandGitHub 77db0d0701 feat: Add configurable interval for IMAP sync (#9302) 2024-04-25 18:58:20 -07:00
Shivam MishraandGitHub e757237029 feat: don't show widget on plain layout (#9297) 2024-04-25 15:26:25 +05:30
04ae38076f feat: Add button component (#9276)
* feat: Add button component

* chore: code cleanup

* chore: code cleanup

* feat: update styles

* chore: cleanup button

* chore: review comments

* chore: code cleanup

* chore: review comments

* chore: fix spacing

* chore: remove unused code

* chore: remove custom click event

* feat: update props validation

* chore: review comments

* refactor: use trailingIcon as a boolean prop

* fix: build issues

---------

Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
2024-04-25 14:21:23 +05:30
Sivin VargheseandGitHub 2ddf613c58 fix: Audio attachment issues (#9260)
* fix: Audio attachment issues

* chore: Style fix

* chore: Minor fix

* chore: Minor fix

* chore: Review fixes
2024-04-25 13:42:46 +05:30
Muhsin KelothandGitHub 1bf3ad9a08 fix: Remove unnecessary attributes from notification deleted action cable event payload (#9295)
fix: simplify notification deleted payload event
2024-04-25 11:25:19 +05:30
9086650fe2 fix: heatmap colors for dark mode [CW-3241] (#9278)
* feat: add new heatmap colors

* fix: loader

* fix: move new styles to tailwind

* feat: update tw classes

* refactor: update styles

* feat: add useI18n composable

* feat: use composition api

* fix: empty div

* chore: don't import defineProps

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

---------

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2024-04-25 10:23:15 +05:30
Shivam MishraandGitHub 8c813097db feat: update SLA upgrade banner (#9291)
feat: add cancel anytime notice
2024-04-24 17:19:35 +05:30
12403b75d6 feat: remove log rocket (#9279)
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2024-04-24 12:36:36 +05:30
Shivam MishraandGitHub 480cf4952d fix: set size limit to 300KB (#9289) 2024-04-24 12:09:31 +05:30
Muhsin KelothandGitHub d7e928df26 feat: Add loom video embed support for help center articles (#9288)
* feat: Add loom support for helpcenter articles

* fix: responsiveness

* fix: style issues

* fix: review comments

* Update custom_markdown_renderer.rb
2024-04-24 11:44:10 +05:30
Muhsin KelothandGitHub 8957277e5d feat: Add new colors (#9258) 2024-04-22 14:08:12 +05:30
Sojan JoseandGitHub 60b355b500 fix: Avoid vector search if query is not present in the params (#9265) 2024-04-19 14:50:49 -07:00
Sivin VargheseandGitHub d2e324b5fd fix: Remove duplicate tooltip for failed messages on the conversation UI (#9259) 2024-04-19 09:47:41 -07:00
Sojan JoseandGitHub 15638e9b8b chore: Add validation to prevent message flooding (#9254)
- Add a validation to limit messages created per minute to avoid message flooding cases.
2024-04-18 00:14:59 -07:00
ca2fa5ff06 fix: Clicking on contact attributes doesn't do anything if contact attributes list is empty (#9257)
* fix: Clicking on contact attributes doesn't do anything if contact attributes list is empty

* Update app/javascript/dashboard/i18n/locale/en/conversation.json

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>

* Update app/javascript/dashboard/i18n/locale/en/contact.json

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2024-04-18 12:31:32 +05:30
PranavandGitHub 888d3cb3fa chore: Increase the rate limit for contact search (#9256) 2024-04-17 16:59:38 -07:00
Sojan 8fcf476206 Merge branch 'release/3.8.0' into develop 2024-04-16 17:09:17 -07:00
Sojan 29f07e7e06 Bump version to 3.8.0 2024-04-16 17:08:36 -07:00
2cde42c7ec feat: add upgrade banner for SLA feature (#9240)
- Add an upgrade CTA for the SLA feature

-------------------

Co-authored-by: Sojan Jose <sojan@pepalo.com>
Co-authored-by: Pranav <pranav@chatwoot.com>
2024-04-16 16:59:39 -07:00
Sojan JoseandGitHub d12c38c344 chore: Move embedding generation to sidekiq (#9238)
chore: Move embedding generation to a job
2024-04-16 09:43:16 -07:00
Sivin VargheseandGitHub 5cc41c9ed5 fix: Reports filter style issue (#9241) 2024-04-16 11:12:04 +05:30
c05a79cf61 fix: Add more filters for SLA download reports (#9231)
Co-authored-by: iamsivin <iamsivin@gmail.com>
2024-04-16 09:00:52 +05:30
f71f43e54a chore: Update translations (#9236)
- update translations 

------------------
Co-authored-by: Sojan <sojan@pepalo.com>
2024-04-15 18:49:04 -07:00
Sojan JoseandGitHub db87cde09e chore: Fix typo in help center embedding search helper (#9237)
chore: fix typo in helpcenter embedding search helper
2024-04-15 17:54:52 -07:00
PranavandGitHub 3d31d4dd7f feat: Add widget to help center config (#9235) 2024-04-15 16:46:48 -07:00
Sojan JoseandGitHub 42a457ff5d feat: Use embeddings in help center search (#9227) 2024-04-15 16:35:23 -07:00
ade658ad86 chore: [Snyk] Fix for 1 vulnerabilities (#9229)
fix: Gemfile & Gemfile.lock to reduce vulnerabilities

The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-RUBY-RACK-1061917

Co-authored-by: snyk-bot <snyk-bot@snyk.io>
2024-04-15 14:07:37 -07:00
bce2a07d03 feat: do not send contact details to the widget (#9223)
* refactor: use has_email instead of email

* feat: remove usage of details directly in forms

* test: update payload

* test: fix transcript test

* refactor: use computed hasEmail

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2024-04-12 15:30:07 +05:30
Sivin VargheseandGitHub dc757598f1 fix: Disable clear filter button in add filter dropdown (#9226) 2024-04-12 13:38:51 +05:30
3b6ae772bf feat: SLA report filter (#9218)
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2024-04-12 11:03:18 +05:30
PranavandGitHub e8fe3c7c05 fix: Terminate IMAP connection after the job is complete (#9225) 2024-04-11 15:47:43 -07:00
Sivin VargheseandGitHub 0477b8ca14 fix: Widget iFrame is adding a white background (#9221) 2024-04-11 21:34:31 +05:30
aed156f359 feat: Better SLA missed events popover (#9215)
* feat: SLA events component

* feat: Add SLA event item component

* Update SLAPopoverCard.vue

* Update SLAPopoverCard.vue

* fix: Translation

* Update SLAEventItem.vue

* feat: complete sticky nrt

* chore: code cleanup

* Update SLACardLabel.vue

* chore: code cleanup

* chore: away click fixes

* feat: use tabular nums

---------

Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
2024-04-10 12:49:38 +05:30
RedandGitHub 9013450e00 fix: Fixes 9012 Cancelling Google OAuth signup redirects user to /auth/sign_in (#9022)
Overriding new_user_session action to avoid rendering error directly in webpage
2024-04-10 12:12:48 +05:30
Shivam MishraandGitHub 3b2b1b7b8c feat: remove CODEOWNERS for js & vue files (#9217)
feat: Remove CODEOWNERS for js & vue files
2024-04-10 16:37:38 +10:00
455a05481c fix: Remove background color from table footer (#9213)
* fix: remove bg from footer

* Update SLATable.vue

* Update app/javascript/dashboard/routes/dashboard/settings/reports/components/SLA/SLATable.vue

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

---------

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2024-04-09 15:29:05 +10:00
626f487232 feat: update create SLA modal design (#9182)
---------

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2024-04-08 21:02:01 -07:00
8fe3c91813 feat: Custom attribute sidebar list UX improvements (#9070)
---------

Co-authored-by: Pranav <pranav@chatwoot.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2024-04-08 21:00:49 -07:00
Muhsin KelothandGitHub c4e111b554 feat: Download SLA reports (#9201) 2024-04-08 20:51:34 -07:00
Sivin VargheseandGitHub 12c5739287 fix: Adds background color for skelton loader in dark mode (#9211) 2024-04-08 20:28:56 -07:00
Muhsin KelothandGitHub 78724f7459 feat: Table footer design updates (#9194)
* feat: table footer component cleanup

* Update TableFooter.vue

* feat: Update design

* chore: remove RTL mixin

* chore: Make component in composable format

* chore: review fixes
2024-04-09 10:50:41 +10:00
1038d1500e feat: SLA reports view (#9189)
* feat: SLA report table


* feat: Add SLA popover card

* feat: Update popover position

* feat: Add loader

* Update SLACardLabel.vue

* feat: Update column order

* chore: fix conditions

* Update SLATable.vue

* chore: enable reports in ui

* chore: Revamp report SLA apis

* chore: revert download method

* chore: improve the code

* Update enterprise/app/views/api/v1/accounts/applied_slas/download.csv.erb

Co-authored-by: Vishnu Narayanan <iamwishnu@gmail.com>

* chore: style fixes

* chore: fix specs

* feat: Add number of conversations

* chore: review comments

* fix: translation

* Update app/javascript/dashboard/i18n/locale/en/report.json

Co-authored-by: Vishnu Narayanan <iamwishnu@gmail.com>

* Update app/javascript/dashboard/i18n/locale/en/report.json

Co-authored-by: Vishnu Narayanan <iamwishnu@gmail.com>

* Update app/javascript/dashboard/i18n/locale/en/report.json

Co-authored-by: Vishnu Narayanan <iamwishnu@gmail.com>

* Update SLAReportItem.vue

* Update report.json

* Update package.json

* chore: review comments

* chore: remove unused translation

* feat: Add TableHeaderCell component

* chore: more review fixes

* Update app/javascript/dashboard/components/widgets/TableHeaderCell.vue

Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>

* Update TableHeaderCell.vue

---------

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: Vishnu Narayanan <iamwishnu@gmail.com>
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
2024-04-09 10:14:36 +10:00
Sivin VargheseandGitHub 7ae09ce1bf feat: Reports filter components (#9204)
* feat: Reports filter components

* Update FilterButton.vue

* Update FilterDropdownSearch.vue

* feat: Adds component level search

* Update FilterDropdownSearch.vue

* chore: Set max height

* feat: Add focus

* chore: Uses picoSearch package
2024-04-08 20:46:08 +10:00
Sivin VargheseandGitHub aef4b582db fix: Update the SLA alert styles to match the labels (#9199) 2024-04-05 09:06:52 -07:00
Shivam MishraandGitHub 69ebdc63e3 feat: better empty state for SLA (#9198)
* feat: better empty state

* feat: better create CTA

* feat: use text-sm
2024-04-05 11:45:05 +05:30
Shivam MishraandGitHub 2397f817fb fix: add priority in filter keys [CW-3177] (#9179)
* fix: ConditionValidationService fails on conversation priority

* fix: custom attribute clash in spec
2024-04-05 10:03:48 +05:30
Vishnu NarayananandGitHub 95c83725d7 chore: add db_reset make command (#9197) 2024-04-05 12:11:30 +11:00
fa3bc05d3d chore: fix sla email notifications (#9192)
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2024-04-04 21:16:49 +05:30
Sojan JoseandGitHub 18bfda35b3 chore: Fix agent API returning 500 error (#9193)
- We were using the @user object in the view, which wasn't available when using api_access_token.
- Also replaced the object to @agent and added additional specs to confirm the right object

fixes: https://linear.app/chatwoot/issue/CW-3185/agent-create-api-throwing-500-response
ref: https://discord.com/channels/647412545203994635/1219928138163949568/1219928138163949568
2024-04-04 19:44:47 +05:30
5c9acfb4e9 chore: Handle duplicate Webhooks from slack (#9187)
* chore: Handle duplicate Webhooks from slack

* chore: fixes

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2024-04-04 17:00:45 +05:30
Sivin VargheseandGitHub e49ef773d8 feat: UI to show the SLA threshold in chat screen (#9146)
- UI will show the breach in the conversation list.
- UI will show the breach in the conversation header.

Fixes: https://linear.app/chatwoot/issue/CW-3146/update-the-ui-to-show-the-breach-in-the-conversation-list
Fixes: https://linear.app/chatwoot/issue/CW-3144/ui-update-to-show-the-breachgoing-to-breach
2024-04-04 15:46:46 +05:30
e21d7552d3 feat: extending lock to single conversation to meta inbox (#9104)
This change introduces the ability to lock conversations to a single thread for Instagram and facebook messages within the Meta inbox, mirroring existing functionality in WhatsApp and SMS inboxes.

Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
2024-04-03 21:33:23 +05:30
6b7a707fef chore: Security upgrade omniauth-google-oauth2 from 1.1.1 to 1.1.2 (#9173)
fix: Gemfile & Gemfile.lock to reduce vulnerabilities

The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-RUBY-RACK-1061917

Co-authored-by: snyk-bot <snyk-bot@snyk.io>
2024-04-03 20:22:46 +05:30
Muhsin KelothandGitHub 727fa67735 feat: SLA reports store (#9185)
- Added sla reports actions, getters and mutations.
2024-04-03 12:53:31 +05:30
Muhsin KelothandGitHub fc25f43448 feat: Add SLA reports overview component (#9167) 2024-04-02 15:08:19 -07:00
Muhsin KelothandGitHub 631598b6b6 chore: Fix twilio inbox create transaction rollback (#9181)
chore: Fix twilio create transaction
2024-04-02 15:04:10 +05:30
Sojan JoseandGitHub 4e28481f27 feat: Conversation API to return applied_sla and sla_events (#9174)
* chore: Add sla_events to push_event_data

* chore: Return SLA details in the API

* chore: feature lock sla push event data

* Update _conversation.json.jbuilder

* chore: rubocop fixes
2024-04-01 23:30:07 +05:30
16282f6a66 feat: Add push/email notification support for SLA (#9140)
* feat: update SLA evaluation logic

* Update enterprise/app/services/sla/evaluate_applied_sla_service.rb

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>

* chore: refactor spec to bring down expecations in a single block

* chore: fix process_account_applied_sla spec

* chore: add spec to test multiple nrt misses

* feat: persist sla notifications

* feat: revert persist sla notifications

* feat: add SLA push/email notification support

* chore: refactor sla_status to include active_with_misses

* chore: add support for sla push/email notifications

* chore: refactor

* chore: add liquid templates

* chore: add spec for liquid templates

* chore: add spec for sla email notifications

* chore: add spec for SlaPolicyDrop

* chore: refactor to ee namespace

* chore: set enterprise test type to mailer

* feat: enable sla notification settings only if SLA enabled

* chore: refactor

* chore: fix spec

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2024-03-29 20:27:21 +11:00
6956436a76 feat: refactor SLA evaluation logic (#9133)
* feat: update SLA evaluation logic

* chore: handle nrt

* chore: handle applied_sla status

* chore: refactor spec to bring down expecations in a single block

* chore: fix process_account_applied_sla spec

* chore: add spec to test multiple nrt misses

* feat: persist sla notifications

* feat: revert persist sla notifications

* chore: refactor sla_status to include active_with_misses

* chore: refactor spec

* Update evaluate_applied_sla_service.rb

* minor refactors

* clean up

* move notification related spec

* chore: refactor notifications spec to sla_event model

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2024-03-29 02:01:43 +11:00
9a1c54a82d feat: disable automation rules if condition fails multiple times (#9017)
* feat: add email for disabling automation rule

* feat: disable automation rules and notify admin

* feat: reset error count after update

* feat: trigger invalid_condition_error if rule is invalid

* feat: setup error trackable concern

* refactor: use ErrorTrackable in Reauthorizable

* fix: optional argument

* feat: separate reauthorization_required_key

* test: update case to use ERROR_TRACKABLE_COUNT

* Revert "test: update case to use ERROR_TRACKABLE_COUNT"

This reverts commit f439847147556a02759a7597a7fcf1d66091cafc.

* Revert "feat: separate reauthorization_required_key"

This reverts commit f4514fce217b0a2f2c2bf701a15de0a8b47acbc4.

* Revert "fix: optional argument"

This reverts commit 93b4194ec3f10f67e2402388c966c071c4d3b4fd.

* Revert "refactor: use ErrorTrackable in Reauthorizable"

This reverts commit 513c2a522bc782e73ea4b0f5ae34ce01e70e042c.

* Revert "feat: setup error trackable concern"

This reverts commit 278683060cf422f60af5d5c77100aa5272141141.

* feat: use reauthorizable for automation rule

* feat: remove redis key

* test: fix method names

* chore: refactor

---------

Co-authored-by: Vishnu Narayanan <iamwishnu@gmail.com>
Co-authored-by: Sojan <sojan@pepalo.com>
2024-03-28 13:14:16 +05:30
Sivin VargheseandGitHub 3e07320d22 feat: SLA threshold card component (#9163)
- Component to display SLA timer in the conversation card and header
2024-03-27 13:19:51 +05:30
1253264382 fix: Avoid duplicate invitation emails when adding an agent (#9131)
Co-authored-by: Sojan <sojan@pepalo.com>
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
Co-authored-by: Pranav <pranav@chatwoot.com>
2024-03-26 18:58:45 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2ee911e33a chore(deps): bump express from 4.18.2 to 4.19.2 (#9159)
Bumps [express](https://github.com/expressjs/express) from 4.18.2 to 4.19.2.
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/master/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.18.2...4.19.2)

---
updated-dependencies:
- dependency-name: express
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-26 15:22:06 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
cdcf02c943 chore(deps): bump follow-redirects from 1.15.3 to 1.15.6 (#9119)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.3 to 1.15.6.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.3...v1.15.6)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-26 15:12:39 -07:00
3b7694b163 chore(snyk): Security upgrade markdown-it from 13.0.1 to 13.0.2 (#9153)
fix: package.json & yarn.lock to reduce vulnerabilities

The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-JS-MARKDOWNIT-6483324

Co-authored-by: snyk-bot <snyk-bot@snyk.io>
2024-03-26 15:12:09 -07:00
Shivam MishraandGitHub d1dd319091 feat: API to download breached conversations (#9150)
* feat: add download conversations endpoint

* feat: template for conversation list download

* feat: setup download API and tests

* chore: revert formatting change

* feat: rename download method

* feat: rename template

* feat: include sla_policy table in download query

* refactor: add nil safety to assignee

* chore: Update en.yml

* fix: remove applied_sla relation
2024-03-26 09:22:49 +05:30
Shivam MishraandGitHub 9917cb4273 fix: Convert cached_label_list to text (#9143) 2024-03-25 18:47:08 -07:00
Muhsin KelothandGitHub 48452a42f4 feat: Add SLA metrics API (#9132) 2024-03-25 12:24:43 +05:30
6d4551bca2 feat: Update the design for SLA policy management pages (#9136)
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
Co-authored-by: Pranav <pranav@chatwoot.com>
2024-03-22 14:39:09 -07:00
e94b276f64 feat: Add help URLs for features in features.yml (#9134)
Co-authored-by: Pranav <pranav@chatwoot.com>
2024-03-21 10:30:46 -07:00
c51492c674 feat: SLA List Item component (#9135)
- Base settings list and list item components.
- SLA list item component.

Fixes: https://linear.app/chatwoot/issue/CW-3126/create-a-sla-list-item-component-with-the-new-design


Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
Co-authored-by: Pranav <pranav@chatwoot.com>
2024-03-21 19:30:11 +05:30
Ryan KonandGitHub 762a39330a fix: use safe nav when downcasing email in from_email (#9139)
Use safe nav when downcasing email in from_email
2024-03-21 18:44:04 +05:30
44956176a1 feat: Add SLA header component (#9129)
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
Co-authored-by: Pranav <pranav@chatwoot.com>
2024-03-20 21:04:44 -07:00
Sojan JoseandGitHub 1303469087 feat: Ability filter blocked contacts (#9048)
- This PR introduces the ability to filter blocked contacts from the contacts filter UI
2024-03-20 18:11:50 +05:30
f78f278e2f fix: Update validations for filter service (#8239)
- Refactor filter service for better readability and maintenance
- Add validations for the following:
   - If an invalid attribute is passed, a custom exception InvalidAttribute will be thrown.
   - If an invalid operator is passed, a custom exception InvalidOperator will be thrown.
   - If an invalid value (currently checking only null check), a custom exception InvalidValue will be thrown.

Fixes: https://linear.app/chatwoot/issue/CW-2702/activerecordstatementinvalid-pginvalidtextrepresentation-error-invalid
Fixes: https://linear.app/chatwoot/issue/CW-2703/activerecordstatementinvalid-pginvaliddatetimeformat-error-invalid
Fixes:  https://linear.app/chatwoot/issue/CW-2700/activerecordstatementinvalid-pgsyntaxerror-error-syntax-error-at-or


Co-authored-by: Sojan <sojan@pepalo.com>
2024-03-20 16:29:36 +05:30
Vishnu NarayananandGitHub b017d05ed9 feat: add sla events table (#9126)
* feat: add sla events table

* chore: refactor to EE namespace

* chore: refactor

* chore: fix spec

* chore: add references to account,inbox,sla_policy

* chore: update specs

* chore: update spec to check backfilling id's

* Update spec/enterprise/models/sla_event_spec.rb
2024-03-20 11:59:37 +05:30
bd97226c95 fix: Locale not correct in root url when accessing help center with custom domain (#9110)
- Ensuring that SwitchLocale concern handles the case of custom domain for portals and set locale according to that

Co-authored-by: Sojan Jose <sojan@pepalo.com>
2024-03-19 18:48:59 +05:30
Sojan a8b930e351 Merge branch 'release/3.7.0' into develop 2024-03-18 17:27:23 +05:30
Sojan e1e8857195 Bump version to 3.7.0 2024-03-18 17:26:09 +05:30
Muhsin KelothandGitHub b5c6d90abd chore: Fix CSATMetrics component snap shot spec (#9122)
chore: Fix snapshot specs
2024-03-18 17:02:23 +05:30
Sojan JoseandGitHub fd832d7593 chore: Add verbose errors for IMAP email channels (#9116) 2024-03-15 20:53:23 -07:00
Vishnu NarayananandGitHub 29e44ac6d0 feat: add option to delete and disable edits for SLA (#9108)
* feat: add delete button for SLA

* chore: remove edit SLA

* chore: remove update related texts from i18n
2024-03-15 12:21:32 +05:30
Sivin VargheseandGitHub 58ee2e125a fix: Update the error response during changing the password from profile settings (#9114) 2024-03-14 23:07:19 -07:00
89d0b2cb6e feat: Add the bot performance reports UI (#9036)
Co-authored-by: Pranav <pranav@chatwoot.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2024-03-14 23:04:14 -07:00
476077ab84 fix: Update location component to avoid overflow, handle location title from Telegram payload (#9113)
Co-authored-by: Pranav <pranav@chatwoot.com>
2024-03-14 22:53:43 -07:00
Muhsin KelothandGitHub 586552013e feat: Update the contact_type when creating or updating the contact (#9107)
* feat: Update location and country code when the contact create/update

* feat: Update the location and country_code when creating or updating the contact.

* chore: improve comments

* feat: Update the contact_type when the contact created/updated

* chore: add more specs

* chore: code cleanups

* chore: code cleanups

* Update contact_spec.rb

* Update inbox.rb

* Update sync_attributes_spec.rb

* chore: build fixes

* chore: check visitor type before update

* chore: review fixes
2024-03-15 10:55:40 +05:30
Sojan JoseandGitHub 3dae3ff3ad feat: Conversation update API for sla_policy_id (#8973)
- Add an endpoint for updating conversation attributes (priority / sla_policy_id )
- Swagger spec
- minor chores around the conversation API/spec

Fixes: https://linear.app/chatwoot/issue/CW-2100/feat-backend-api-to-update-the-sla-of-a-conversation
2024-03-14 17:22:32 +05:30
29171565ed feat: Add image preview for inline images in vue-letter (#9099)
Co-authored-by: Pranav <pranav@chatwoot.com>
2024-03-13 18:46:35 -07:00
55fcbe2dde feat: Add the ability to zoom on chat images (#9093)
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: Pranav <pranav@chatwoot.com>
2024-03-13 18:04:42 -07:00
Vishnu NarayananandGitHub 7f4b2d66d4 feat: add activity message for SLA (#9100)
* feat: add activity message for SLA

* chore: refactor to reduce method complexity

* chore: refactor

* feat: add spec for SLAactivity message

* chore: move SLA spec to enterprise folder

* chore: move team activity methods to a separate module

* chore: fix typo

* chore: move sla activity message spec to conversation model
2024-03-13 20:05:34 +05:30
Muhsin KelothandGitHub 804a42c271 feat: Update the location and country_code when creating or updating the contact (#9106)
* feat: Update the location and country_code when creating or updating the contact.

* chore: improve comments
2024-03-13 16:40:21 +05:30
Vishnu NarayananandGitHub 561fafa198 feat: Update icons for the SLA feature in dashboard (#9101) 2024-03-12 12:45:46 -07:00
Vishnu NarayananandGitHub aaf70cf1cf feat: add push notification when SLA missed (#9078)
* feat: add push notification when SLA missed

* chore: sent notification only for inbox members

* feat: add conv particpants+admins to SLA notification list

* chore: add spec to ensure notification is created

* chore: refactor to multiple alerts for SLA conditions

* chore: add sla_policy as secondary_actor in notification
2024-03-11 21:49:41 +05:30
0685e04aae feat: Add inbox view under feature flag on the sidebar (#9049)
Co-authored-by: Pranav <pranav@chatwoot.com>
2024-03-07 18:27:39 -08:00
PranavandGitHub b6bf6bd414 docs: Update README.md, remove broken badge (#9080) 2024-03-07 22:27:29 +05:30
Shivam MishraandGitHub 9f70b93ff3 feat: Add information about email in the transcript (#9020) 2024-03-06 22:28:33 -08:00
PranavandGitHub 505ede2761 fix: Change the column identifier from string to text to avoid overflow (#9073)
Fixes: https://linear.app/chatwoot/issue/CW-3118/cannot-subscribe-to-notifications-on-microsoft-edge

Fix the issue with notifications in Microsoft Edge. The Edge push notification payload identifier has more than 255 characters. The API calls were failing due to this. This PR would fix the issue.
2024-03-07 11:13:01 +05:30
Chatwoot BotandGitHub 56bf543f7e chore: Update translations (#9052) 2024-03-06 23:30:32 +05:30
Muhsin KelothandGitHub 216de4d123 fix: Conversation assignment notification title (#9071) 2024-03-06 23:05:13 +05:30
Muhsin KelothandGitHub ef90b7a3d8 feat: Revamp the notification title and content (#8988) 2024-03-06 20:51:56 +05:30
Muhsin KelothandGitHub 58dd2633ba feat: Add contact helper (#8989) 2024-03-06 17:39:39 +05:30
Shivam MishraandGitHub c62c512ec4 fix: inline attachments not handled if tag was missing (#9068) 2024-03-06 17:15:29 +05:30
Matheus RodriguesandGitHub e086cb0d86 docs: added missing type text for messages (#9067)
- It adds the text type of message which is missing on the documentation
2024-03-06 16:30:51 +05:30
b4d37fa16b fix: Update help center styles, fix routing issues (#9061)
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2024-03-04 09:57:14 -08:00
Sivin VargheseandGitHub a9d42e8c7e fix: Update the broken styles in help center insert modal (#9062) 2024-03-04 08:28:47 -08:00
Sojan JoseandGitHub 81060a72a4 feat: Improve reply suggestions using help center articles (#9026) 2024-03-03 20:25:19 -08:00
881d4bf644 feat: Add backend APIs for the bot metrics (#9031)
Co-authored-by: Pranav <pranav@chatwoot.com>
2024-03-01 08:20:20 -08:00
Sivin VargheseandGitHub 9581264286 fix: Broken styles (#9046) 2024-03-01 13:06:33 +05:30
Shivam MishraandGitHub 7c5c88b17a fix: Update border radius for login/unauthenticated pages (#9042) 2024-02-29 10:34:13 -08:00
Sivin VargheseandGitHub 351cc940ce fix: Fix the button variant success color in dark mode (#9043) 2024-02-29 10:33:31 -08:00
Sivin VargheseandGitHub 9c2147aced fix: UI breakages (#9040)
- Fixes overflow of contact details in the contact side panel.
- Fixes line height issue in article header.
- Fixes overflow of category names in the article list.
- Fixes typing indicator alignment issues
2024-02-29 15:10:29 +05:30
Sojan JoseandGitHub cb8f6f1a39 fix: Update the reply time tooltip to avoid confusion (#9037) 2024-02-28 12:22:09 -08:00
a92639e391 fix: Filter dropdown alignment issue in reports page (#9035)
* fix: Filter dropdown alignment issue in reports page

* fix: add hide class

* chore: Clean up

---------

Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
2024-02-28 20:26:23 +05:30
dafedddc1a feat: Remove Foundation in favor of Tailwind (#8984)
* feat: Remove foundation

* chore: Minor fix

* Minor fix

* Update _forms.scss

* chore: More changes

* chore: Minor fix

* chore: Clean up

* fix: font-weight

* chore: More changes

* chore: Setting page

* chore: Editor fix

* chore: Reports page

* chore: More changes

* chore: Minor changes

* chore: More fixes

* chore: More changes

* chore: More changes

* chore: More changes

* chore: Minor fix

* chore: More changes

* chore: More changes

* chore: More changes

* chore: More changes

* chore: Clean up

* chore: Minor fix

* chore: Clean ups

* chore: Rename basic file

* chore: Remove unused files

* chore: Fix expanded input

* Fix campaign rendering

* chore: Clean up

* chore: More changes

* chore: Remove unused files

* fix: Overflow issue

* chore: Minor fix

* chore: Clean up

* chore: Minor fix

* chore: Remove unused files

* chore: Minor fix

* chore: Minor fix

* fix: autoprefixer start/end value has mixed support

* chore: Minor fix

* chore: Remove unused files

* chore: Minor fix

* chore: Minor fix

* chore: Minor fix

* Add responsive design to label settings

* fix inbox view

* chore: Minor fix

* w-60% to w-2/3

* chore: Fix team

* chore: Fix button

* w-[34%] to w-1/3

* chore: Fix border

* Add support mobile views in team page

* chore: fix snackbar

* chore: clean up

* chore: Clean up

* fix: loading state alignment

* fix: alert styles

* chore: Minor fix

* fix: spacing for agent bot row

* fix: layout

* fix: layout for SLA

* fix: checkbox

* fix: SLA checkbox spacing

* Update inbox settings pages

* fix macros listing page layout

* fix canned responses

* chore: Fix bot page

* chore: fix automation page

* chore: fix agents page

* chore: fix canned response editor

* chore: Fix settings table

* chore: fix settings layout

* chore: Minor fix

* fix: canned response table layou

* fix: layout for table header for webhooks

* fix: webhook row layout

* fix: dashboard app modal layout

* fix: add title to canned response truncated shortcode

* fix: dashboard apps row layuot

* fix: layouts hooks

* fix: body color

* fix: delete action color in portal locales

* fix: text color for campagin title

* fix: success button color

---------

Co-authored-by: Pranav <pranav@chatwoot.com>
Co-authored-by: Vishnu Narayanan <iamwishnu@gmail.com>
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
2024-02-28 13:56:28 +05:30
9f905ce2e6 feat: Update the input for the SLA threshold selection (#8974)
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
Co-authored-by: Pranav <pranav@chatwoot.com>
2024-02-27 23:00:24 -08:00
PranavandGitHub dca14ef82d fix: Downgrade rack-cors to 2.0.0 to fix CVE-2024-27456 (#9032) 2024-02-27 20:20:59 -08:00
ac249c75c4 chore: Update the metric card component to support generic cases (#9030)
Rename the CSAT metric card to a generic name, updated the implementation to use composition API and removed all the custom CSS in the component to conform with TailwindCSS styles
---------
Co-authored-by: Pranav <pranav@chatwoot.com>
2024-02-27 17:51:06 -08:00
Sojan JoseandGitHub 41e269e873 feat(ee): Add reporting events for bots (#9027)
Added a new event conversation_bot_resolved and added a job to auto resolve the bot conversations if there was no activity for the last 1 hour.
2024-02-27 14:53:28 -08:00
b7a83dcbcd feat: Add a placeholder for image attachments in conversations view (#8969)
We analyze an image to get it's height and width. On the frontend, we would show a placeholder with the corresponding width and height until the images are loaded properly.

---------

Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
2024-02-27 14:46:16 -08:00
Danilo Campana FuchsandGitHub fd993feece chore(docs): Update the API documentation to add the filter status=all to conversation list and meta (#8994) 2024-02-26 15:15:55 -08:00
773be6f8ec feat: Interface to validate response_source (#8894)
- This PR adds a UI to validate the response source quality quickly. It also helps to test with sample questions and update responses in the database when missing.

Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2024-02-26 20:20:12 +05:30
Vishnu NarayananandGitHub 77e463990a feat: seed db for heroku review apps (#9019) 2024-02-26 16:41:05 +05:30
Vishnu NarayananandGitHub 8f6ff4bc41 chore: refactor deploy check gh action (#9018)
* chore: refactor deploy check action

    Refactor deployment check gh action to account for the initial setup time required for heroku review apps
    Process /api response via jq only if http.status_code is 200
2024-02-26 16:14:01 +05:30
Sojan JoseandGitHub ab56374b2f chore: Fix Bundle Audit Breakages (#9016)
Name: actionpack
Version: 7.0.8
CVE: CVE-2024-26143
Criticality: Unknown
URL: https://discuss.rubyonrails.org/t/possible-xss-vulnerability-in-action-controller/84947

-----

Name: rack
Version: 2.2.8
CVE: CVE-2024-25126
Criticality: Unknown
URL: https://discuss.rubyonrails.org/t/denial-of-service-vulnerability-in-rack-content-type-parsing/84941
2024-02-26 15:25:21 +05:30
Shivam MishraandGitHub fa305d76e8 fix: Add custom rubocop rules for email fetch (#8987) 2024-02-23 11:58:02 -08:00
Vishnu NarayananandGitHub 1de3ff1a6d chore(infra): Add GH action to ensure deployment success for every PR (#8990) 2024-02-22 15:28:28 -08:00
Shivam MishraandGitHub 106319406b fix: remove rubocop rule (#8985)
* fix: remove rubocop rule

* chore: remove ignore comment

* chore: remove ignore comment
2024-02-22 13:36:15 +05:30
Chatwoot BotandGitHub a5e9cd94e1 chore: Update translations (#8983) 2024-02-21 16:01:14 -08:00
27ac262a26 feat(ee): Add support for SLA in automation rules (#8910)
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2024-02-21 14:45:43 -08:00
ae4c8d818f feat: Ability to block contacts permanently (#8922)
Co-authored-by: Pranav <pranav@chatwoot.com>
2024-02-21 14:18:42 -08:00
Shivam MishraandGitHub c031cb19d2 fix: downcase email before finding (#8921)
* fix: downcase email when finding

* feat: add `from_email` class

* refactor: use `from_email`

* feat: add rule to disallow find_by email directly

* chore:  remove redundant test

Since the previous imlpmentation didn't do a case-insentive search, a new user would be created, and the error would be raised at the DB layer. With the new changes, this test case is redundant

* refactor: use from_email
2024-02-21 18:51:00 +05:30
ebae547a60 feat: Add ability to resolve API channel conversations (#8348)
- Create a new endpoint to fetch a single conversation in client apis
- Create a new endpoint to resolve a single conversation in client apis
- Update swagger API definition to include missing endpoints

Fixes: #6329

Co-authored-by: Cristian Duta <Cristian.Duta@ti8m.ch>
2024-02-21 17:41:20 +05:30
cc47ccaa2c feat(ee): Add SLA management UI (#8777)
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Pranav <pranav@chatwoot.com>
2024-02-20 23:03:22 -08:00
Vishnu NarayananandGitHub d53097f77d fix: Raise error if email to_header is invalid (#8688) 2024-02-20 17:03:39 -08:00
9911c5dc12 chore: Hide banners on onboarding view (#8934)
---------
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2024-02-20 16:12:51 -08:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
19aef3e94b chore(deps): bump ip from 1.1.5 to 1.1.9 (#8976)
Bumps [ip](https://github.com/indutny/node-ip) from 1.1.5 to 1.1.9.
- [Commits](https://github.com/indutny/node-ip/compare/v1.1.5...v1.1.9)

---
updated-dependencies:
- dependency-name: ip
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-20 16:11:02 -08:00
23230e0143 feat: sla-7 ensure applied_sla uniqueness (#8938)
* feat: refactor fetching sla in action service

* chore: modify spec

* chore: ensure applied_sla uniqueness

* chore: review fixes

* feat: add unique index on applied_sla

* chore: fix spec

* chore: add new specs to improve coverage

* chore: improve spec

* Update spec/enterprise/services/enterprise/action_service_spec.rb

---------

Co-authored-by: Sojan Jose <sojan@pepalo.com>
2024-02-20 21:59:49 +05:30
Vishnu NarayananandGitHub f92cea144c feat(perf): sla-9 improve perf of TriggerSlasForAccountsJob (#8953)
* feat: improve perf of TriggerSlasForAccountsJob
2024-02-20 21:54:37 +05:30
Muhsin KelothandGitHub e6cf8c39b7 feat: Update last_snoozed_at after the un-snooze notification (#8943) 2024-02-20 18:55:39 +05:30
Muhsin KelothandGitHub c5c0845151 feat: Add labels, status and priority in notification push_event_data (#8972) 2024-02-20 17:17:25 +05:30
5d9fb55370 feat: Export contact improvements (#8895)
This pull request enhances the export contacts feature by adding a confirmation step before exporting. Previously, clicking the export button would trigger the export action without confirmation.

Additionally, it ensures that only the intended recipient receives the export email, addressing the previous behaviour where all administrators received it.

Fixes: #8504

Co-authored-by: Sojan Jose <sojan@pepalo.com>
2024-02-20 17:11:03 +05:30
721a2f5052 feat: API changes to support multi step user signup (#8933)
-API Changes to support the new onboarding flow 

Co-authored-by: Sojan <sojan@pepalo.com>
2024-02-20 16:48:51 +05:30
7320957405 chore: Add facebook_api_version to global config (#8965)
- Ability to configure facebook_api_version from global config

Co-authored-by: Pranav <pranav@chatwoot.com>
2024-02-20 15:53:29 +05:30
Sojan e4d072c79c Merge branch 'release/3.6.0' into develop 2024-02-19 15:59:26 +05:30
Sojan 71ee10c889 Bump version to 3.6.0 2024-02-19 15:58:21 +05:30
Sojan JoseandGitHub 978a8a4cb2 fix: support_email and inbound_email_domain returning empty string (#8963)
chore: Fix for inbound email domain being nil
2024-02-19 15:43:35 +05:30
cd06b2b337 fix: Inbox view Read/Snoozed display filters (#8907)
* fix: Notification filters

* Update notification_finder.rb

* Update notification_finder.rb

* Update notification_finder.rb

* fix: spec

* fix: specs

* Update notification_finder.rb

* fix: add more fixes

* Update notification_finder.rb

* fix: specs

* chore: better comments

* chore: removed filtering

* chore: refactoring

* fix: review fixes

* fix: API call

* chore: Minor fix

* Rename spec

* Fix params getting undefined

* Fix finder

---------

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
Co-authored-by: Pranav <pranav@chatwoot.com>
2024-02-17 13:59:25 +05:30
6eb06377cc feat: Add textarea component to use in forms (#8932)
Co-authored-by: Pranav <pranav@chatwoot.com>
2024-02-16 12:19:08 -08:00
Sojan JoseandGitHub 94892e7168 chore: Fix flaky spec relying on unguaranteed order (#8956) 2024-02-16 12:11:39 -08:00
Muhsin KelothandGitHub fe744abe26 fix: Mark notification as unread after the un-snooze (#8917) 2024-02-16 17:30:54 +05:30
Nithin David ThomasandGitHub a902b49bc5 feat: Adds bulk_invite api for onboarding view (#8931)
- New API for bulk email invite
2024-02-16 17:01:27 +05:30
Sojan JoseandGitHub 9c07b6dd46 chore: Support for updated_within in ConverationFinder (#8947)
- `updated_within' accepts value in seconds and returns all conversations updated in the given period with out pagination. This API will assist in our refetch logic on socket disconnect

ref: #8888
2024-02-16 16:44:55 +05:30
Muhsin KelothandGitHub 3b7b06dbec feat: Add meta to notification model (#8941)
* feat: Add meta and description to notification model

* chore: remove description
2024-02-16 14:38:02 +05:30
Sivin VargheseandGitHub 597a4164e8 fix: Editor hyperlink popup is hidden under modal (#8950) 2024-02-16 08:19:06 +05:30
Nithin David ThomasandGitHub 678c00f254 fix: Removes default slot for right of label (#8936) 2024-02-14 10:48:54 -08:00
Nithin David ThomasandGitHub 7298002da7 fix: Adds forgot password link for login form (#8927)
- focus ring not appearing issue
- missing forgot password link

Fixes: https://linear.app/chatwoot/issue/CW-3090/fix-fixes-issues-with-login-form
2024-02-14 16:31:32 +05:30
1451 changed files with 35229 additions and 9506 deletions
-5
View File
@@ -180,8 +180,6 @@ ANDROID_SHA256_CERT_FINGERPRINT=AC:73:8E:DE:EB:56:EA:CC:10:87:02:A7:65:37:7B:38:
## Sentry
# SENTRY_DSN=
## LogRocket
# LOG_ROCKET_PROJECT_ID=xxxxx/some-project
# MICROSOFT CLARITY
# MS_CLARITY_TOKEN=xxxxxxxxx
@@ -251,9 +249,6 @@ AZURE_APP_SECRET=
## OpenAI key
# OPENAI_API_KEY=
# Sentiment analysis model file path
SENTIMENT_FILE_PATH=
# Housekeeping/Performance related configurations
# Set to true if you want to remove stale contact inboxes
# contact_inboxes with no conversation older than 90 days will be removed
-5
View File
@@ -1,7 +1,2 @@
## All javascript files should be reviewed by pranav before merging
*.js @pranavrajs
*.vue @pranavrajs
## All enterprise related files should be reviewed by sojan before merging
/enterprise/* @sojan-official
+45
View File
@@ -0,0 +1,45 @@
## github action to check deployment success
## curl the deployment url and check for 200 status
## deployment url will be of the form chatwoot-pr-<pr_number>.herokuapp.com
name: Deploy Check
on:
pull_request:
jobs:
deployment_check:
name: Check Deployment
runs-on: ubuntu-latest
steps:
- name: Install jq
run: sudo apt-get install -y jq
- name: Print Deployment URL
run: echo "https://chatwoot-pr-${{ github.event.pull_request.number }}.herokuapp.com"
- name: Check Deployment Status
run: |
max_attempts=10
attempt=1
status_code=0
echo "Waiting for review app to be deployed/redeployed, trying in 10 minutes..."
sleep 600
while [ $attempt -le $max_attempts ]; do
response=$(curl -s -o /dev/null -w "%{http_code}" https://chatwoot-pr-${{ github.event.pull_request.number }}.herokuapp.com/api)
status_code=$(echo $response | head -n 1)
if [ $status_code -eq 200 ]; then
body=$(curl -s https://chatwoot-pr-${{ github.event.pull_request.number }}.herokuapp.com/api)
if echo "$body" | jq -e '.version and .timestamp and .queue_services == "ok" and .data_services == "ok"' > /dev/null; then
echo "Deployment successful"
exit 0
else
echo "Deployment status unknown, retrying in 3 minutes..."
sleep 180
fi
else
echo "Waiting for review app to be ready, retrying in 3 minutes..."
sleep 180
attempt=$((attempt + 1))
fi
done
echo "Deployment failed after $max_attempts attempts"
exit 1
fi
@@ -73,6 +73,7 @@ jobs:
spec/enterprise/controllers/api/v1/accounts/response_sources_controller_spec.rb \
spec/enterprise/services/enterprise/message_templates/response_bot_service_spec.rb \
spec/enterprise/controllers/enterprise/api/v1/accounts/inboxes_controller_spec.rb:47 \
spec/enterprise/jobs/enterprise/account/conversations_resolution_scheduler_job_spec.rb \
--profile=10 \
--format documentation
+12
View File
@@ -2,6 +2,8 @@ require:
- rubocop-performance
- rubocop-rails
- rubocop-rspec
- ./rubocop/use_from_email.rb
- ./rubocop/custom_cop_location.rb
Layout/LineLength:
Max: 150
@@ -140,6 +142,16 @@ RSpec/MultipleExpectations:
RSpec/MultipleMemoizedHelpers:
Max: 14
# custom rules
UseFromEmail:
Enabled: true
Exclude:
- 'app/models/user.rb'
- 'app/models/contact.rb'
CustomCopLocation:
Enabled: true
AllCops:
NewCops: enable
Exclude:
+5 -8
View File
@@ -3,8 +3,8 @@ source 'https://rubygems.org'
ruby '3.2.2'
##-- base gems for rails --##
gem 'rack-cors', require: 'rack/cors'
gem 'rails', '~> 7.0.8.0'
gem 'rack-cors', '2.0.0', require: 'rack/cors'
gem 'rails', '~> 7.0.8.1'
# Reduces boot times through caching; required in config/boot.rb
gem 'bootsnap', require: false
@@ -69,7 +69,7 @@ gem 'webpacker'
gem 'barnes'
##--- gems for authentication & authorization ---##
gem 'devise', '>= 4.9.3'
gem 'devise', '>= 4.9.4'
gem 'devise-secure_password', git: 'https://github.com/chatwoot/devise-secure_password', branch: 'chatwoot'
gem 'devise_token_auth'
# authorization
@@ -165,7 +165,7 @@ gem 'audited', '~> 5.4', '>= 5.4.1'
# need for google auth
gem 'omniauth', '>= 2.1.2'
gem 'omniauth-google-oauth2'
gem 'omniauth-google-oauth2', '>= 1.1.2'
gem 'omniauth-rails_csrf_protection', '~> 1.0'
## Gems for reponse bot
@@ -175,9 +175,6 @@ gem 'pgvector'
# Convert Website HTML to Markdown
gem 'reverse_markdown'
# Sentiment analysis
gem 'informers'
### Gems required only in specific deployment environments ###
##############################################################
@@ -203,7 +200,7 @@ group :development do
gem 'rack-mini-profiler', '>= 3.2.0', require: false
gem 'stackprof'
# Should install the associated chrome extension to view query logs
gem 'meta_request'
gem 'meta_request', '>= 0.8.0'
end
group :test do
+103 -110
View File
@@ -33,70 +33,70 @@ GIT
GEM
remote: https://rubygems.org/
specs:
actioncable (7.0.8)
actionpack (= 7.0.8)
activesupport (= 7.0.8)
actioncable (7.0.8.1)
actionpack (= 7.0.8.1)
activesupport (= 7.0.8.1)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
actionmailbox (7.0.8)
actionpack (= 7.0.8)
activejob (= 7.0.8)
activerecord (= 7.0.8)
activestorage (= 7.0.8)
activesupport (= 7.0.8)
actionmailbox (7.0.8.1)
actionpack (= 7.0.8.1)
activejob (= 7.0.8.1)
activerecord (= 7.0.8.1)
activestorage (= 7.0.8.1)
activesupport (= 7.0.8.1)
mail (>= 2.7.1)
net-imap
net-pop
net-smtp
actionmailer (7.0.8)
actionpack (= 7.0.8)
actionview (= 7.0.8)
activejob (= 7.0.8)
activesupport (= 7.0.8)
actionmailer (7.0.8.1)
actionpack (= 7.0.8.1)
actionview (= 7.0.8.1)
activejob (= 7.0.8.1)
activesupport (= 7.0.8.1)
mail (~> 2.5, >= 2.5.4)
net-imap
net-pop
net-smtp
rails-dom-testing (~> 2.0)
actionpack (7.0.8)
actionview (= 7.0.8)
activesupport (= 7.0.8)
actionpack (7.0.8.1)
actionview (= 7.0.8.1)
activesupport (= 7.0.8.1)
rack (~> 2.0, >= 2.2.4)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
actiontext (7.0.8)
actionpack (= 7.0.8)
activerecord (= 7.0.8)
activestorage (= 7.0.8)
activesupport (= 7.0.8)
actiontext (7.0.8.1)
actionpack (= 7.0.8.1)
activerecord (= 7.0.8.1)
activestorage (= 7.0.8.1)
activesupport (= 7.0.8.1)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
actionview (7.0.8)
activesupport (= 7.0.8)
actionview (7.0.8.1)
activesupport (= 7.0.8.1)
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 (7.0.8)
activesupport (= 7.0.8)
activejob (7.0.8.1)
activesupport (= 7.0.8.1)
globalid (>= 0.3.6)
activemodel (7.0.8)
activesupport (= 7.0.8)
activerecord (7.0.8)
activemodel (= 7.0.8)
activesupport (= 7.0.8)
activemodel (7.0.8.1)
activesupport (= 7.0.8.1)
activerecord (7.0.8.1)
activemodel (= 7.0.8.1)
activesupport (= 7.0.8.1)
activerecord-import (1.4.1)
activerecord (>= 4.2)
activestorage (7.0.8)
actionpack (= 7.0.8)
activejob (= 7.0.8)
activerecord (= 7.0.8)
activesupport (= 7.0.8)
activestorage (7.0.8.1)
actionpack (= 7.0.8.1)
activejob (= 7.0.8.1)
activerecord (= 7.0.8.1)
activesupport (= 7.0.8.1)
marcel (~> 1.0)
mini_mime (>= 1.1.0)
activesupport (7.0.8)
activesupport (7.0.8.1)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
@@ -149,9 +149,8 @@ GEM
multi_json (~> 1)
statsd-ruby (~> 1.1)
base64 (0.1.1)
bcrypt (3.1.19)
bcrypt (3.1.20)
bindex (0.8.1)
blingfire (0.1.8)
bootsnap (1.16.0)
msgpack (~> 1.2)
brakeman (5.4.1)
@@ -194,7 +193,7 @@ GEM
irb (>= 1.5.0)
reline (>= 0.3.1)
declarative (0.0.20)
devise (4.9.3)
devise (4.9.4)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
railties (>= 4.1.0)
@@ -237,9 +236,8 @@ GEM
railties (>= 5.0.0)
faker (3.2.0)
i18n (>= 1.8.11, < 2)
faraday (2.7.4)
faraday-net_http (>= 2.0, < 3.1)
ruby2_keywords (>= 0.0.4)
faraday (2.9.0)
faraday-net_http (>= 2.0, < 3.2)
faraday-follow_redirects (0.3.0)
faraday (>= 1, < 3)
faraday-mashify (0.1.1)
@@ -247,7 +245,8 @@ GEM
hashie
faraday-multipart (1.0.4)
multipart-post (~> 2)
faraday-net_http (3.0.2)
faraday-net_http (3.1.0)
net-http
faraday-net_http_persistent (2.1.0)
faraday (~> 2.5)
net-http-persistent (~> 4.0)
@@ -316,15 +315,15 @@ GEM
google-cloud-translate-v3 (0.6.0)
gapic-common (>= 0.17.1, < 2.a)
google-cloud-errors (~> 1.0)
google-protobuf (3.25.2)
google-protobuf (3.25.2-arm64-darwin)
google-protobuf (3.25.2-x86_64-darwin)
google-protobuf (3.25.2-x86_64-linux)
google-protobuf (3.25.3)
google-protobuf (3.25.3-arm64-darwin)
google-protobuf (3.25.3-x86_64-darwin)
google-protobuf (3.25.3-x86_64-linux)
googleapis-common-protos (1.4.0)
google-protobuf (~> 3.14)
googleapis-common-protos-types (~> 1.2)
grpc (~> 1.27)
googleapis-common-protos-types (1.11.0)
googleapis-common-protos-types (1.14.0)
google-protobuf (~> 3.18)
googleauth (1.5.2)
faraday (>= 0.17.3, < 3.a)
@@ -335,14 +334,17 @@ GEM
signet (>= 0.16, < 2.a)
groupdate (6.2.1)
activesupport (>= 5.2)
grpc (1.54.3)
google-protobuf (~> 3.21)
grpc (1.62.0)
google-protobuf (~> 3.25)
googleapis-common-protos-types (~> 1.0)
grpc (1.54.3-x86_64-darwin)
google-protobuf (~> 3.21)
grpc (1.62.0-arm64-darwin)
google-protobuf (~> 3.25)
googleapis-common-protos-types (~> 1.0)
grpc (1.54.3-x86_64-linux)
google-protobuf (~> 3.21)
grpc (1.62.0-x86_64-darwin)
google-protobuf (~> 3.25)
googleapis-common-protos-types (~> 1.0)
grpc (1.62.0-x86_64-linux)
google-protobuf (~> 3.25)
googleapis-common-protos-types (~> 1.0)
haikunator (1.1.1)
hairtrigger (1.0.0)
@@ -366,15 +368,11 @@ GEM
mini_mime (>= 1.0.0)
multi_xml (>= 0.5.2)
httpclient (2.8.3)
i18n (1.14.1)
i18n (1.14.4)
concurrent-ruby (~> 1.0)
image_processing (1.12.2)
mini_magick (>= 4.9.5, < 5)
ruby-vips (>= 2.0.17, < 3)
informers (0.2.0)
blingfire (>= 0.1.7)
numo-narray
onnxruntime (>= 0.5.1)
io-console (0.6.0)
irb (1.7.2)
reline (>= 0.3.6)
@@ -394,7 +392,8 @@ GEM
hana (~> 1.3)
regexp_parser (~> 2.0)
uri_template (~> 0.7)
jwt (2.7.0)
jwt (2.8.1)
base64
kaminari (1.2.2)
activesupport (>= 4.1.0)
kaminari-actionview (= 1.2.2)
@@ -451,17 +450,17 @@ GEM
marcel (1.0.2)
maxminddb (0.1.22)
memoist (0.16.2)
meta_request (0.7.4)
meta_request (0.8.2)
rack-contrib (>= 1.1, < 3)
railties (>= 3.0.0, < 7.1)
railties (>= 3.0.0, < 8)
method_source (1.0.0)
mime-types (3.4.1)
mime-types-data (~> 3.2015)
mime-types-data (3.2023.0218.1)
mini_magick (4.12.0)
mini_mime (1.1.5)
mini_portile2 (2.8.5)
minitest (5.21.2)
mini_portile2 (2.8.6)
minitest (5.22.3)
mock_redis (0.36.0)
ruby2_keywords
msgpack (1.7.0)
@@ -470,6 +469,8 @@ GEM
multipart-post (2.3.0)
neighbor (0.2.3)
activerecord (>= 5.2)
net-http (0.4.1)
uri
net-http-persistent (4.0.2)
connection_pool (~> 2.2)
net-imap (0.4.9)
@@ -488,16 +489,15 @@ GEM
newrelic_rpm (9.6.0)
base64
nio4r (2.7.0)
nokogiri (1.16.2)
nokogiri (1.16.4)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
nokogiri (1.16.2-arm64-darwin)
nokogiri (1.16.4-arm64-darwin)
racc (~> 1.4)
nokogiri (1.16.2-x86_64-darwin)
nokogiri (1.16.4-x86_64-darwin)
racc (~> 1.4)
nokogiri (1.16.2-x86_64-linux)
nokogiri (1.16.4-x86_64-linux)
racc (~> 1.4)
numo-narray (0.9.2.1)
oauth (1.1.0)
oauth-tty (~> 1.0, >= 1.0.1)
snaky_hash (~> 2.0)
@@ -515,25 +515,17 @@ GEM
hashie (>= 3.4.6)
rack (>= 2.2.3)
rack-protection
omniauth-google-oauth2 (1.1.1)
omniauth-google-oauth2 (1.1.2)
jwt (>= 2.0)
oauth2 (~> 2.0.6)
oauth2 (~> 2.0)
omniauth (~> 2.0)
omniauth-oauth2 (~> 1.8.0)
omniauth-oauth2 (~> 1.8)
omniauth-oauth2 (1.8.0)
oauth2 (>= 1.4, < 3)
omniauth (~> 2.0)
omniauth-rails_csrf_protection (1.0.1)
actionpack (>= 4.2)
omniauth (~> 2.0)
onnxruntime (0.7.6)
ffi
onnxruntime (0.7.6-arm64-darwin)
ffi
onnxruntime (0.7.6-x86_64-darwin)
ffi
onnxruntime (0.7.6-x86_64-linux)
ffi
openssl (3.1.0)
orm_adapter (0.5.0)
os (1.1.4)
@@ -559,36 +551,37 @@ GEM
activesupport (>= 3.0.0)
raabro (1.4.0)
racc (1.7.3)
rack (2.2.8)
rack (2.2.9)
rack-attack (6.7.0)
rack (>= 1.0, < 4)
rack-contrib (2.4.0)
rack (< 4)
rack-cors (2.0.1)
rack-cors (2.0.0)
rack (>= 2.0.0)
rack-mini-profiler (3.2.0)
rack (>= 1.2.0)
rack-protection (3.1.0)
rack-protection (3.2.0)
base64 (>= 0.1.0)
rack (~> 2.2, >= 2.2.4)
rack-proxy (0.7.6)
rack
rack-test (2.1.0)
rack (>= 1.3)
rack-timeout (0.6.3)
rails (7.0.8)
actioncable (= 7.0.8)
actionmailbox (= 7.0.8)
actionmailer (= 7.0.8)
actionpack (= 7.0.8)
actiontext (= 7.0.8)
actionview (= 7.0.8)
activejob (= 7.0.8)
activemodel (= 7.0.8)
activerecord (= 7.0.8)
activestorage (= 7.0.8)
activesupport (= 7.0.8)
rails (7.0.8.1)
actioncable (= 7.0.8.1)
actionmailbox (= 7.0.8.1)
actionmailer (= 7.0.8.1)
actionpack (= 7.0.8.1)
actiontext (= 7.0.8.1)
actionview (= 7.0.8.1)
activejob (= 7.0.8.1)
activemodel (= 7.0.8.1)
activerecord (= 7.0.8.1)
activestorage (= 7.0.8.1)
activesupport (= 7.0.8.1)
bundler (>= 1.15.0)
railties (= 7.0.8)
railties (= 7.0.8.1)
rails-dom-testing (2.2.0)
activesupport (>= 5.0.0)
minitest
@@ -596,15 +589,15 @@ GEM
rails-html-sanitizer (1.6.0)
loofah (~> 2.21)
nokogiri (~> 1.14)
railties (7.0.8)
actionpack (= 7.0.8)
activesupport (= 7.0.8)
railties (7.0.8.1)
actionpack (= 7.0.8.1)
activesupport (= 7.0.8.1)
method_source
rake (>= 12.2)
thor (~> 1.0)
zeitwerk (~> 2.5)
rainbow (3.1.1)
rake (13.1.0)
rake (13.2.1)
rb-fsevent (0.11.2)
rb-inotify (0.10.1)
ffi (~> 1.0)
@@ -765,7 +758,7 @@ GEM
stripe (8.5.0)
telephone_number (1.4.20)
test-prof (1.2.1)
thor (1.3.0)
thor (1.3.1)
tilt (2.3.0)
time_diff (0.3.0)
activesupport
@@ -790,11 +783,12 @@ GEM
unf_ext (0.0.8.2)
unicode-display_width (2.4.2)
uniform_notifier (1.16.0)
uri (0.13.0)
uri_template (0.7.0)
valid_email2 (4.0.6)
activemodel (>= 3.2)
mail (~> 2.5)
version_gem (1.1.3)
version_gem (1.1.4)
warden (1.2.9)
rack (>= 2.0.9)
web-console (4.2.1)
@@ -823,7 +817,7 @@ GEM
working_hours (1.4.1)
activesupport (>= 3.2)
tzinfo
zeitwerk (2.6.12)
zeitwerk (2.6.13)
PLATFORMS
arm64-darwin-20
@@ -862,7 +856,7 @@ DEPENDENCIES
database_cleaner
ddtrace
debug (~> 1.8)
devise (>= 4.9.3)
devise (>= 4.9.4)
devise-secure_password!
devise_token_auth
dotenv-rails
@@ -887,7 +881,6 @@ DEPENDENCIES
hashie
html2text!
image_processing
informers
jbuilder
json_refs
json_schemer
@@ -900,14 +893,14 @@ DEPENDENCIES
listen
lograge (~> 0.14.0)
maxminddb
meta_request
meta_request (>= 0.8.0)
mock_redis
neighbor
net-smtp (~> 0.3.4)
newrelic-sidekiq-metrics (>= 1.6.2)
newrelic_rpm
omniauth (>= 2.1.2)
omniauth-google-oauth2
omniauth-google-oauth2 (>= 1.1.2)
omniauth-oauth2
omniauth-rails_csrf_protection (~> 1.0)
pg
@@ -918,10 +911,10 @@ DEPENDENCIES
puma
pundit
rack-attack (>= 6.7.0)
rack-cors
rack-cors (= 2.0.0)
rack-mini-profiler (>= 3.2.0)
rack-timeout
rails (~> 7.0.8.0)
rails (~> 7.0.8.1)
redis
redis-namespace
responders (>= 3.1.1)
+4 -1
View File
@@ -17,6 +17,9 @@ db_migrate:
db_seed:
RAILS_ENV=$(RAILS_ENV) bundle exec rails db:seed
db_reset:
RAILS_ENV=$(RAILS_ENV) bundle exec rails db:reset
db:
RAILS_ENV=$(RAILS_ENV) bundle exec rails db:chatwoot_prepare
@@ -49,4 +52,4 @@ debug_worker:
docker:
docker build -t $(APP_NAME) -f ./docker/Dockerfile .
.PHONY: setup db_create db_migrate db_seed db console server burn docker run force_run debug debug_worker
.PHONY: setup db_create db_migrate db_seed db_reset db console server burn docker run force_run debug debug_worker
-1
View File
@@ -23,7 +23,6 @@ Customer engagement suite, an open-source alternative to Intercom, Zendesk, Sale
<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>
<a href="https://huntr.dev/bounties/disclose"><img src="https://cdn.huntr.dev/huntr_security_badge_mono.svg" alt="Huntr"></a>
<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%2Fuptime.json" alt="uptime"></a>
<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>
<a href="https://artifacthub.io/packages/helm/chatwoot/chatwoot"><img src="https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/artifact-hub" alt="Artifact HUB"></a>
+5
View File
@@ -72,6 +72,11 @@
"scripts": {
"test": "bundle exec rake test"
}
},
"review": {
"scripts": {
"postdeploy": "bundle exec rails db:seed"
}
}
}
}
+1 -1
View File
@@ -59,7 +59,7 @@ class ContactIdentifyAction
def existing_email_contact
return if params[:email].blank?
@existing_email_contact ||= account.contacts.find_by(email: params[:email])
@existing_email_contact ||= account.contacts.from_email(params[:email])
end
def existing_phone_number_contact
+1 -7
View File
@@ -16,7 +16,6 @@ class AgentBuilder
def perform
ActiveRecord::Base.transaction do
@user = find_or_create_user
send_confirmation_if_required
create_account_user
end
@user
@@ -27,18 +26,13 @@ class AgentBuilder
# Finds a user by email or creates a new one with a temporary password.
# @return [User] the found or created user.
def find_or_create_user
user = User.find_by(email: email)
user = User.from_email(email)
return user if user
temp_password = "1!aA#{SecureRandom.alphanumeric(12)}"
User.create!(email: email, name: name, password: temp_password, password_confirmation: temp_password)
end
# Sends confirmation instructions if the user is persisted and not confirmed.
def send_confirmation_if_required
@user.send_confirmation_instructions if user_needs_confirmation?
end
# Checks if the user needs confirmation.
# @return [Boolean] true if the user is persisted and not confirmed, false otherwise.
def user_needs_confirmation?
@@ -75,7 +75,7 @@ class ContactInboxWithContactBuilder
def find_contact_by_email(email)
return if email.blank?
account.contacts.find_by(email: email.downcase)
account.contacts.from_email(email)
end
def find_contact_by_phone_number(phone_number)
@@ -53,7 +53,23 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
end
def conversation
@conversation ||= Conversation.find_by(conversation_params) || build_conversation
@conversation ||= set_conversation_based_on_inbox_config
end
def set_conversation_based_on_inbox_config
if @inbox.lock_to_single_conversation
Conversation.where(conversation_params).order(created_at: :desc).first || build_conversation
else
find_or_build_for_multiple_conversations
end
end
def find_or_build_for_multiple_conversations
# If lock to single conversation is disabled, we will create a new conversation if previous conversation is resolved
last_conversation = Conversation.where(conversation_params).where.not(status: :resolved).order(created_at: :desc).first
return build_conversation if last_conversation.nil?
last_conversation
end
def build_conversation
@@ -69,9 +69,28 @@ class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder
end
def conversation
@conversation ||= Conversation.where(conversation_params).find_by(
"additional_attributes ->> 'type' = 'instagram_direct_message'"
) || build_conversation
@conversation ||= set_conversation_based_on_inbox_config
end
def instagram_direct_message_conversation
Conversation.where(conversation_params)
.where("additional_attributes ->> 'type' = 'instagram_direct_message'")
end
def set_conversation_based_on_inbox_config
if @inbox.lock_to_single_conversation
instagram_direct_message_conversation.order(created_at: :desc).first || build_conversation
else
find_or_build_for_multiple_conversations
end
end
def find_or_build_for_multiple_conversations
last_conversation = instagram_direct_message_conversation.where.not(status: :resolved).order(created_at: :desc).first
return build_conversation if last_conversation.nil?
last_conversation
end
def message_content
+10
View File
@@ -54,6 +54,13 @@ class V2::ReportBuilder
}
end
def bot_summary
{
bot_resolutions_count: bot_resolutions.count,
bot_handoffs_count: bot_handoffs.count
}
end
def conversation_metrics
if params[:type].equal?(:account)
live_conversations
@@ -71,6 +78,8 @@ class V2::ReportBuilder
avg_first_response_time
avg_resolution_time reply_time
resolutions_count
bot_resolutions_count
bot_handoffs_count
reply_time].include?(params[:metric])
end
@@ -123,6 +132,7 @@ class V2::ReportBuilder
unattended: @open_conversations.unattended.count
}
metric[:unassigned] = @open_conversations.unassigned.count if params[:type].equal?(:account)
metric[:pending] = @open_conversations.pending.count if params[:type].equal?(:account)
metric
end
end
@@ -0,0 +1,54 @@
class V2::Reports::BotMetricsBuilder
include DateRangeHelper
attr_reader :account, :params
def initialize(account, params)
@account = account
@params = params
end
def metrics
{
conversation_count: bot_conversations.count,
message_count: bot_messages.count,
resolution_rate: bot_resolution_rate.to_i,
handoff_rate: bot_handoff_rate.to_i
}
end
private
def bot_activated_inbox_ids
@bot_activated_inbox_ids ||= account.inboxes.filter(&:active_bot?).map(&:id)
end
def bot_conversations
@bot_conversations ||= account.conversations.where(inbox_id: bot_activated_inbox_ids).where(created_at: range)
end
def bot_messages
@bot_messages ||= account.messages.outgoing.where(conversation_id: bot_conversations.ids).where(created_at: range)
end
def bot_resolutions_count
account.reporting_events.joins(:conversation).select(:conversation_id).where(account_id: account.id, name: :conversation_bot_resolved,
created_at: range).distinct.count
end
def bot_handoffs_count
account.reporting_events.joins(:conversation).select(:conversation_id).where(account_id: account.id, name: :conversation_bot_handoff,
created_at: range).distinct.count
end
def bot_resolution_rate
return 0 if bot_conversations.count.zero?
bot_resolutions_count.to_f / bot_conversations.count * 100
end
def bot_handoff_rate
return 0 if bot_conversations.count.zero?
bot_handoffs_count.to_f / bot_conversations.count * 100
end
end
@@ -19,7 +19,7 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
account: Current.account
)
builder.perform
@agent = builder.perform
end
def update
@@ -49,6 +49,11 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
Rails.logger.info "[Agent#bulk_create] ignoring email #{email}, errors: #{e.record.errors}"
end
end
# This endpoint is used to bulk create agents during onboarding
# onboarding_step key in present in Current account custom attributes, since this is a one time operation
Current.account.custom_attributes.delete('onboarding_step')
Current.account.save!
head :ok
end
@@ -2,13 +2,9 @@ class Api::V1::Accounts::Channels::TwilioChannelsController < Api::V1::Accounts:
before_action :authorize_request
def create
ActiveRecord::Base.transaction do
authenticate_twilio
build_inbox
setup_webhooks if @twilio_channel.sms?
rescue StandardError => e
render_could_not_create_error(e.message)
end
process_create
rescue StandardError => e
render_could_not_create_error(e.message)
end
private
@@ -17,6 +13,14 @@ class Api::V1::Accounts::Channels::TwilioChannelsController < Api::V1::Accounts:
authorize ::Inbox
end
def process_create
ActiveRecord::Base.transaction do
authenticate_twilio
build_inbox
setup_webhooks if @twilio_channel.sms?
end
end
def authenticate_twilio
client = if permitted_params[:api_key_sid].present?
Twilio::REST::Client.new(permitted_params[:api_key_sid], permitted_params[:auth_token], permitted_params[:account_sid])
@@ -46,7 +46,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
def export
column_names = params['column_names']
Account::ContactsExportJob.perform_later(Current.account.id, column_names)
Account::ContactsExportJob.perform_later(Current.account.id, column_names, Current.user.email)
head :ok, message: I18n.t('errors.contacts.export.success')
end
@@ -65,6 +65,10 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
contacts = result[:contacts]
@contacts_count = result[:count]
@contacts = fetch_contacts(contacts)
rescue CustomExceptions::CustomFilter::InvalidAttribute,
CustomExceptions::CustomFilter::InvalidOperator,
CustomExceptions::CustomFilter::InvalidValue => e
render_could_not_create_error(e.message)
end
def contactable_inboxes
@@ -148,7 +152,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
end
def permitted_params
params.permit(:name, :identifier, :email, :phone_number, :avatar, :avatar_url, additional_attributes: {}, custom_attributes: {})
params.permit(:name, :identifier, :email, :phone_number, :avatar, :blocked, :avatar_url, additional_attributes: {}, custom_attributes: {})
end
def contact_custom_attributes
@@ -36,10 +36,18 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
end
end
def update
@conversation.update!(permitted_update_params)
end
def filter
result = ::Conversations::FilterService.new(params.permit!, current_user).perform
@conversations = result[:conversations]
@conversations_count = result[:count]
rescue CustomExceptions::CustomFilter::InvalidAttribute,
CustomExceptions::CustomFilter::InvalidOperator,
CustomExceptions::CustomFilter::InvalidValue => e
render_could_not_create_error(e.message)
end
def mute
@@ -110,6 +118,11 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
private
def permitted_update_params
# TODO: Move the other conversation attributes to this method and remove specific endpoints for each attribute
params.permit(:priority)
end
def update_last_seen_on_conversation(last_seen_at, update_assignee)
# rubocop:disable Rails/SkipsModelValidations
@conversation.update_column(:agent_last_seen_at, last_seen_at)
@@ -176,3 +189,5 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
@conversation.assignee_id? && Current.user == @conversation.assignee
end
end
Api::V1::Accounts::ConversationsController.prepend_mod_with('Api::V1::Accounts::ConversationsController')
@@ -7,8 +7,8 @@ class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseContro
before_action :set_current_page, only: [:index]
def index
@notifications = notification_finder.notifications
@unread_count = notification_finder.unread_count
@notifications = notification_finder.perform
@count = notification_finder.count
end
@@ -54,7 +54,8 @@ class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseContro
end
def snooze
@notification.update(snoozed_until: parse_date_time(params[:snoozed_until].to_s)) if params[:snoozed_until]
updated_meta = (@notification.meta || {}).merge('last_snoozed_at' => nil)
@notification.update(snoozed_until: parse_date_time(params[:snoozed_until].to_s), meta: updated_meta) if params[:snoozed_until]
render json: @notification
end
@@ -46,6 +46,7 @@ class Api::V1::AccountsController < Api::BaseController
def update
@account.assign_attributes(account_params.slice(:name, :locale, :domain, :support_email, :auto_resolve_duration))
@account.custom_attributes.merge!(custom_attributes_params)
@account.custom_attributes['onboarding_step'] = 'invite_team' if @account.custom_attributes['onboarding_step'] == 'account_update'
@account.save!
end
@@ -33,10 +33,10 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
end
def transcript
if permitted_params[:email].present? && conversation.present?
if conversation.present? && conversation.contact.present? && conversation.contact.email.present?
ConversationReplyMailer.with(account: conversation.account).conversation_transcript(
conversation,
permitted_params[:email]
conversation.contact.email
)&.deliver_later
end
head :ok
@@ -14,6 +14,12 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
render json: summary_metrics
end
def bot_summary
summary = V2::ReportBuilder.new(Current.account, current_summary_params).bot_summary
summary[:previous] = V2::ReportBuilder.new(Current.account, previous_summary_params).bot_summary
render json: summary
end
def agents
@report_data = generate_agents_report
generate_csv('agents_report', 'api/v2/accounts/reports/agents')
@@ -48,6 +54,11 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
render json: conversation_metrics
end
def bot_metrics
bot_metrics = V2::Reports::BotMetricsBuilder.new(Current.account, params).metrics
render json: bot_metrics
end
private
def generate_csv(filename, template)
@@ -22,6 +22,7 @@ class Api::V2::AccountsController < Api::BaseController
).perform
fetch_account_and_user_info
update_account_info if @account.present?
if @user
send_auth_headers(@user)
@@ -33,6 +34,18 @@ class Api::V2::AccountsController < Api::BaseController
private
def account_attributes
{
custom_attributes: @account.custom_attributes.merge({ 'onboarding_step' => 'profile_update' })
}
end
def update_account_info
@account.update!(
account_attributes
)
end
def fetch_account_and_user_info; end
def fetch_account
@@ -25,3 +25,4 @@ class ApplicationController < ActionController::Base
}
end
end
ApplicationController.include_mod_with('Concerns::ApplicationControllerConcern')
@@ -1,6 +1,6 @@
module AccessTokenAuthHelper
BOT_ACCESSIBLE_ENDPOINTS = {
'api/v1/accounts/conversations' => %w[toggle_status toggle_priority create],
'api/v1/accounts/conversations' => %w[toggle_status toggle_priority create update],
'api/v1/accounts/conversations/messages' => ['create'],
'api/v1/accounts/conversations/assignments' => ['create']
}.freeze
@@ -0,0 +1,5 @@
module DomainHelper
def self.chatwoot_domain?(domain = request.host)
[URI.parse(ENV.fetch('FRONTEND_URL', '')).host, URI.parse(ENV.fetch('HELPCENTER_URL', '')).host].include?(domain)
end
end
+15
View File
@@ -6,6 +6,7 @@ module SwitchLocale
def switch_locale(&)
# priority is for locale set in query string (mostly for widget/from js sdk)
locale ||= locale_from_params
locale ||= locale_from_custom_domain
# if locale is not set in account, let's use DEFAULT_LOCALE env variable
locale ||= locale_from_env_variable
set_locale(locale, &)
@@ -16,6 +17,20 @@ module SwitchLocale
set_locale(locale, &)
end
# If the request is coming from a custom domain, it should be for a helpcenter portal
# We will use the portal locale in such cases
def locale_from_custom_domain(&)
return if params[:locale]
domain = request.host
return if DomainHelper.chatwoot_domain?(domain)
@portal = Portal.find_by(custom_domain: domain)
return unless @portal
@portal.default_locale
end
def set_locale(locale, &)
# if locale is empty, use default_locale
locale ||= I18n.default_locale
+2 -1
View File
@@ -18,6 +18,7 @@ class DashboardController < ActionController::Base
'LOGO', 'LOGO_DARK', 'LOGO_THUMBNAIL',
'INSTALLATION_NAME',
'WIDGET_BRAND_URL', 'TERMS_URL',
'BRAND_URL', 'BRAND_NAME',
'PRIVACY_URL',
'DISPLAY_MANIFEST',
'CREATE_NEW_ACCOUNT_FROM_DASHBOARD',
@@ -55,7 +56,7 @@ class DashboardController < ActionController::Base
VAPID_PUBLIC_KEY: VapidService.public_key,
ENABLE_ACCOUNT_SIGNUP: GlobalConfigService.load('ENABLE_ACCOUNT_SIGNUP', 'false'),
FB_APP_ID: GlobalConfigService.load('FB_APP_ID', ''),
FACEBOOK_API_VERSION: 'v14.0',
FACEBOOK_API_VERSION: GlobalConfigService.load('FACEBOOK_API_VERSION', 'v17.0'),
IS_ENTERPRISE: ChatwootApp.enterprise?,
AZURE_APP_ID: ENV.fetch('AZURE_APP_ID', ''),
GIT_SHA: GIT_HASH
@@ -5,7 +5,7 @@ class DeviseOverrides::PasswordsController < Devise::PasswordsController
skip_before_action :authenticate_user!, raise: false
def create
@user = User.find_by(email: params[:email])
@user = User.from_email(params[:email])
if @user
@user.send_reset_password_instructions
build_response(I18n.t('messages.reset_password_success'), 200)
@@ -4,6 +4,10 @@ class DeviseOverrides::SessionsController < DeviseTokenAuth::SessionsController
wrap_parameters format: []
before_action :process_sso_auth_token, only: [:create]
def new
redirect_to login_page_url(error: 'access-denied')
end
def create
# Authenticate user via the temporary sso auth token
if params[:sso_auth_token].present? && @resource.present?
@@ -21,6 +25,12 @@ class DeviseOverrides::SessionsController < DeviseTokenAuth::SessionsController
private
def login_page_url(error: nil)
frontend_url = ENV.fetch('FRONTEND_URL', nil)
"#{frontend_url}/app/login?error=#{error}"
end
def authenticate_resource_with_sso_token
@token = @resource.create_token
@resource.save!
@@ -33,7 +43,7 @@ class DeviseOverrides::SessionsController < DeviseTokenAuth::SessionsController
def process_sso_auth_token
return if params[:email].blank?
user = User.find_by(email: params[:email])
user = User.from_email(params[:email])
@resource = user if user&.valid_sso_auth_token?(params[:sso_auth_token])
end
end
@@ -8,7 +8,7 @@ class Platform::Api::V1::UsersController < PlatformController
def show; end
def create
@resource = (User.find_by(email: user_params[:email]) || User.new(user_params))
@resource = (User.from_email(user_params[:email]) || User.new(user_params))
@resource.skip_confirmation!
@resource.save!
@platform_app.platform_app_permissibles.find_or_create_by!(permissible: @resource)
@@ -1,15 +1,31 @@
class Public::Api::V1::Inboxes::ConversationsController < Public::Api::V1::InboxesController
include Events::Types
before_action :set_conversation, only: [:toggle_typing, :update_last_seen]
before_action :set_conversation, only: [:toggle_typing, :update_last_seen, :show, :toggle_status]
def index
@conversations = @contact_inbox.hmac_verified? ? @contact.conversations : @contact_inbox.conversations
end
def show; end
def create
@conversation = create_conversation
end
def toggle_status
# Check if the conversation is already resolved to prevent redundant operations
return if @conversation.resolved?
# Assign the conversation's contact as the resolver
# This step attributes the resolution action to the contact involved in the conversation
# If this assignment is not made, the system implicitly becomes the resolver by default
Current.contact = @conversation.contact
# Update the conversation's status to 'resolved' to reflect its closure
@conversation.status = :resolved
@conversation.save!
end
def toggle_typing
case params[:typing_status]
when 'on'
@@ -30,7 +46,11 @@ class Public::Api::V1::Inboxes::ConversationsController < Public::Api::V1::Inbox
private
def set_conversation
@conversation = @contact_inbox.contact.conversations.find_by!(display_id: params[:id])
@conversation = if @contact_inbox.hmac_verified?
@contact_inbox.contact.conversations.find_by!(display_id: params[:id])
else
@contact_inbox.conversations.find_by!(display_id: params[:id])
end
end
def create_conversation
@@ -7,7 +7,7 @@ class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::B
def index
@articles = @portal.articles
@articles = @articles.search(list_params) if list_params.present?
search_articles
order_by_sort_param
@articles.page(list_params[:page]) if list_params[:page].present?
end
@@ -16,6 +16,10 @@ class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::B
private
def search_articles
@articles = @articles.search(list_params) if list_params.present?
end
def order_by_sort_param
@articles = if list_params[:sort].present? && list_params[:sort] == 'views'
@articles.order_by_views
@@ -51,3 +55,5 @@ class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::B
ChatwootMarkdownRenderer.new(content).render_article
end
end
Public::Api::V1::Portals::ArticlesController.prepend_mod_with('Public::Api::V1::Portals::ArticlesController')
@@ -47,7 +47,7 @@ class Public::Api::V1::Portals::BaseController < PublicController
@locale = if article.category.present?
article.category.locale
else
'en'
article.portal.default_locale
end
I18n.with_locale(@locale, &)
@@ -6,6 +6,12 @@ class Public::Api::V1::PortalsController < Public::Api::V1::Portals::BaseControl
def show; end
def sitemap
@help_center_url = @portal.custom_domain || ChatwootApp.help_center_root
# if help_center_url does not contain a protocol, prepend it with https
@help_center_url = "https://#{@help_center_url}" unless @help_center_url.include?('://')
end
private
def portal
+1 -2
View File
@@ -8,8 +8,7 @@ class PublicController < ActionController::Base
def ensure_custom_domain_request
domain = request.host
return if [URI.parse(ENV.fetch('FRONTEND_URL', '')).host, URI.parse(ENV.fetch('HELPCENTER_URL', '')).host].include?(domain)
return if DomainHelper.chatwoot_domain?(domain)
@portal = ::Portal.find_by(custom_domain: domain)
return if @portal.present?
@@ -34,7 +34,7 @@ class SuperAdmin::AppConfigsController < SuperAdmin::ApplicationController
def allowed_configs
@allowed_configs = case @config
when 'facebook'
%w[FB_APP_ID FB_VERIFY_TOKEN FB_APP_SECRET IG_VERIFY_TOKEN ENABLE_MESSENGER_CHANNEL_HUMAN_AGENT]
%w[FB_APP_ID FB_VERIFY_TOKEN FB_APP_SECRET IG_VERIFY_TOKEN FACEBOOK_API_VERSION ENABLE_MESSENGER_CHANNEL_HUMAN_AGENT]
when 'email'
['MAILER_INBOUND_EMAIL_DOMAIN']
else
@@ -5,6 +5,10 @@
# If you want to add pagination or other controller-level concerns,
# you're free to overwrite the RESTful controller actions.
class SuperAdmin::ApplicationController < Administrate::ApplicationController
include ActionView::Helpers::TagHelper
include ActionView::Context
helper_method :render_vue_component
# authenticiation done via devise : SuperAdmin Model
before_action :authenticate_super_admin!
@@ -23,6 +27,17 @@ class SuperAdmin::ApplicationController < Administrate::ApplicationController
private
def render_vue_component(component_name, props = {})
html_options = {
id: 'app',
data: {
component_name: component_name,
props: props.to_json
}
}
content_tag(:div, '', html_options)
end
def invalid_action_perfomed
# rubocop:disable Rails/I18nLocaleTexts
flash[:error] = 'Invalid action performed'
+14 -3
View File
@@ -163,12 +163,23 @@ class ConversationFinder
params[:page] || 1
end
def conversations
@conversations = @conversations.includes(
def conversations_base_query
@conversations.includes(
:taggings, :inbox, { assignee: { avatar_attachment: [:blob] } }, { contact: { avatar_attachment: [:blob] } }, :team, :contact_inbox
)
end
def conversations
@conversations = conversations_base_query
sort_by, sort_order = SORT_OPTIONS[params[:sort_by]] || SORT_OPTIONS['last_activity_at_desc']
@conversations.send(sort_by, sort_order).page(current_page).per(ENV.fetch('CONVERSATION_RESULTS_PER_PAGE', '25').to_i)
@conversations = @conversations.send(sort_by, sort_order)
if params[:updated_within].present?
@conversations.where('conversations.updated_at > ?', Time.zone.now - params[:updated_within].to_i.seconds)
else
@conversations.page(current_page).per(ENV.fetch('CONVERSATION_RESULTS_PER_PAGE', '25').to_i)
end
end
end
ConversationFinder.prepend_mod_with('ConversationFinder')
+14 -10
View File
@@ -10,8 +10,8 @@ class NotificationFinder
set_up
end
def perform
notifications
def notifications
@notifications.page(current_page).per(RESULTS_PER_PAGE).order(last_activity_at: sort_order)
end
def unread_count
@@ -26,27 +26,31 @@ class NotificationFinder
def set_up
find_all_notifications
filter_by_read_status
filter_by_status
filter_snoozed_notifications
fitler_read_notifications
end
def find_all_notifications
@notifications = current_user.notifications.where(account_id: @current_account.id)
end
def filter_by_status
@notifications = @notifications.where('snoozed_until > ?', DateTime.now.utc) if params[:status] == 'snoozed'
def filter_snoozed_notifications
@notifications = @notifications.where(snoozed_until: nil) unless type_included?('snoozed')
end
def filter_by_read_status
@notifications = @notifications.where.not(read_at: nil) if params[:type] == 'read'
def fitler_read_notifications
@notifications = @notifications.where(read_at: nil) unless type_included?('read')
end
def type_included?(type)
(params[:includes] || []).include?(type)
end
def current_page
params[:page] || 1
end
def notifications
@notifications.page(current_page).per(RESULTS_PER_PAGE).order(last_activity_at: params[:sort_order] || :desc)
def sort_order
params[:sort_order] || :desc
end
end
+7
View File
@@ -2,4 +2,11 @@ module ApplicationHelper
def available_locales_with_name
LANGUAGES_CONFIG.map { |_key, val| val.slice(:name, :iso_639_1_code) }
end
def feature_help_urls
features = YAML.safe_load(Rails.root.join('config/features.yml').read).freeze
features.each_with_object({}) do |feature, hash|
hash[feature['name']] = feature['help_url'] if feature['help_url']
end
end
end
+83
View File
@@ -0,0 +1,83 @@
module ContactHelper
def parse_name(full_name)
# If the input is nil or not a string, return a hash with all values set to nil
return default_name_hash if invalid_name?(full_name)
# If the input is a number, return a hash with the number as the first name
return numeric_name_hash(full_name) if valid_number?(full_name)
full_name = full_name.squish
# If full name consists of only one word, consider it as the first name
return single_word_name_hash(full_name) if single_word?(full_name)
parts = split_name(full_name)
parts = handle_conjunction(parts)
build_name_hash(parts)
end
private
def default_name_hash
{ first_name: nil, last_name: nil, middle_name: nil, prefix: nil, suffix: nil }
end
def invalid_name?(full_name)
!full_name.is_a?(String) || full_name.empty?
end
def numeric_name_hash(full_name)
{ first_name: full_name, last_name: nil, middle_name: nil, prefix: nil, suffix: nil }
end
def valid_number?(full_name)
full_name.gsub(/\s+/, '').match?(/\A\+?\d+\z/)
end
def single_word_name_hash(full_name)
{ first_name: full_name, last_name: nil, middle_name: nil, prefix: nil, suffix: nil }
end
def single_word?(full_name)
full_name.split.size == 1
end
def split_name(full_name)
full_name.split
end
def handle_conjunction(parts)
conjunctions = ['and', '&']
parts.each_index do |i|
next unless conjunctions.include?(parts[i]) && i.positive?
parts[i - 1] = [parts[i - 1], parts[i + 1]].join(' ')
parts.delete_at(i)
parts.delete_at(i)
end
parts
end
def build_name_hash(parts)
suffix = parts.pop if parts.last.match?(/(\w+\.|[IVXLM]+|[A-Z]+)$/)
last_name = parts.pop
prefix = parts.shift if parts.first.match?(/^\w+\./)
first_name = parts.shift
middle_name = parts.join(' ')
hash = {
first_name: first_name,
last_name: last_name,
prefix: prefix,
middle_name: middle_name,
suffix: suffix
}
# Reverse name if "," was used in Last, First notation.
if hash[:first_name] =~ /,$/
hash[:first_name] = hash[:last_name]
hash[:last_name] = Regexp.last_match.pre_match
end
hash
end
end
+84
View File
@@ -0,0 +1,84 @@
module FilterHelper
def build_condition_query(model_filters, query_hash, current_index)
current_filter = model_filters[query_hash['attribute_key']]
# Throw InvalidOperator Error if the attribute is a standard attribute
# and the operator is not allowed in the config
if current_filter.present? && current_filter['filter_operators'].exclude?(query_hash[:filter_operator])
raise CustomExceptions::CustomFilter::InvalidOperator.new(
attribute_name: query_hash['attribute_key'],
allowed_keys: current_filter['filter_operators']
)
end
# Every other filter expects a value to be present
if %w[is_present is_not_present].exclude?(query_hash[:filter_operator]) && query_hash['values'].blank?
raise CustomExceptions::CustomFilter::InvalidValue.new(attribute_name: query_hash['attribute_key'])
end
condition_query = build_condition_query_string(current_filter, query_hash, current_index)
# The query becomes empty only when it doesn't match to any supported
# standard attribute or custom attribute defined in the account.
if condition_query.empty?
raise CustomExceptions::CustomFilter::InvalidAttribute.new(key: query_hash['attribute_key'],
allowed_keys: model_filters.keys)
end
condition_query
end
def build_condition_query_string(current_filter, query_hash, current_index)
filter_operator_value = filter_operation(query_hash, current_index)
return handle_nil_filter(query_hash, current_index) if current_filter.nil?
case current_filter['attribute_type']
when 'additional_attributes'
handle_additional_attributes(query_hash, filter_operator_value, current_filter['data_type'])
else
handle_standard_attributes(current_filter, query_hash, current_index, filter_operator_value)
end
end
def handle_nil_filter(query_hash, current_index)
attribute_type = "#{filter_config[:entity].downcase}_attribute"
custom_attribute_query(query_hash, attribute_type, current_index)
end
def handle_additional_attributes(query_hash, filter_operator_value, data_type)
if data_type == 'text_case_insensitive'
"LOWER(#{filter_config[:table_name]}.additional_attributes ->> '#{query_hash[:attribute_key]}') " \
"#{filter_operator_value} #{query_hash[:query_operator]}"
else
"#{filter_config[:table_name]}.additional_attributes ->> '#{query_hash[:attribute_key]}' " \
"#{filter_operator_value} #{query_hash[:query_operator]} "
end
end
def handle_standard_attributes(current_filter, query_hash, current_index, filter_operator_value)
case current_filter['data_type']
when 'date'
date_filter(current_filter, query_hash, filter_operator_value)
when 'labels'
tag_filter_query(query_hash, current_index)
when 'text_case_insensitive'
text_case_insensitive_filter(query_hash, filter_operator_value)
else
default_filter(query_hash, filter_operator_value)
end
end
def date_filter(current_filter, query_hash, filter_operator_value)
"(#{filter_config[:table_name]}.#{query_hash[:attribute_key]})::#{current_filter['data_type']} " \
"#{filter_operator_value}#{current_filter['data_type']} #{query_hash[:query_operator]}"
end
def text_case_insensitive_filter(query_hash, filter_operator_value)
"LOWER(#{filter_config[:table_name]}.#{query_hash[:attribute_key]}) " \
"#{filter_operator_value} #{query_hash[:query_operator]}"
end
def default_filter(query_hash, filter_operator_value)
"#{filter_config[:table_name]}.#{query_hash[:attribute_key]} #{filter_operator_value} #{query_hash[:query_operator]}"
end
end
+18
View File
@@ -32,6 +32,14 @@ module ReportHelper
(get_grouped_values resolutions).count
end
def bot_resolutions_count
(get_grouped_values bot_resolutions).count
end
def bot_handoffs_count
(get_grouped_values bot_handoffs).count
end
def conversations
scope.conversations.where(account_id: account.id, created_at: range)
end
@@ -49,6 +57,16 @@ module ReportHelper
conversations: { status: :resolved }, created_at: range).distinct
end
def bot_resolutions
scope.reporting_events.joins(:conversation).select(:conversation_id).where(account_id: account.id, name: :conversation_bot_resolved,
conversations: { status: :resolved }, created_at: range).distinct
end
def bot_handoffs
scope.reporting_events.joins(:conversation).select(:conversation_id).where(account_id: account.id, name: :conversation_bot_handoff,
created_at: range).distinct
end
def avg_first_response_time
grouped_reporting_events = (get_grouped_values scope.reporting_events.where(name: 'first_response', account_id: account.id))
return grouped_reporting_events.average(:value_in_business_hours) if params[:business_hours]
+6 -2
View File
@@ -8,8 +8,8 @@
>
<update-banner :latest-chatwoot-version="latestChatwootVersion" />
<template v-if="currentAccountId">
<pending-email-verification-banner />
<payment-pending-banner />
<pending-email-verification-banner v-if="hideOnOnboardingView" />
<payment-pending-banner v-if="hideOnOnboardingView" />
<upgrade-banner />
</template>
<transition name="fade" mode="out-in">
@@ -38,6 +38,7 @@ import vueActionCable from './helper/actionCable';
import WootSnackbarBox from './components/SnackbarContainer.vue';
import rtlMixin from 'shared/mixins/rtlMixin';
import { setColorTheme } from './helper/themeHelper';
import { isOnOnboardingView } from 'v3/helpers/RouteHelper';
import {
registerSubscription,
verifyServiceWorkerExistence,
@@ -79,6 +80,9 @@ export default {
const { accounts = [] } = this.currentUser || {};
return accounts.length > 0;
},
hideOnOnboardingView() {
return !isOnOnboardingView(this.$route);
},
},
watch: {
+8
View File
@@ -1,9 +1,17 @@
/* global axios */
import ApiClient from './ApiClient';
class Agents extends ApiClient {
constructor() {
super('agents', { accountScoped: true });
}
bulkInvite({ emails }) {
return axios.post(`${this.url}/bulk_create`, {
emails,
});
}
}
export default new Agents();
@@ -7,12 +7,13 @@ class NotificationsAPI extends ApiClient {
}
get({ page, status, type, sortOrder }) {
const includesFilter = [status, type].filter(value => !!value);
return axios.get(this.url, {
params: {
page,
status,
type,
sort_order: sortOrder,
includes: includesFilter,
},
});
}
+18
View File
@@ -84,6 +84,24 @@ class ReportsAPI extends ApiClient {
params: { since, until, business_hours: businessHours },
});
}
getBotMetrics({ from, to } = {}) {
return axios.get(`${this.url}/bot_metrics`, {
params: { since: from, until: to },
});
}
getBotSummary({ from, to, groupBy, businessHours } = {}) {
return axios.get(`${this.url}/bot_summary`, {
params: {
since: from,
until: to,
type: 'account',
group_by: groupBy,
business_hours: businessHours,
},
});
}
}
export default new ReportsAPI();
+9
View File
@@ -0,0 +1,9 @@
import ApiClient from './ApiClient';
class SlaAPI extends ApiClient {
constructor() {
super('sla_policies', { accountScoped: true });
}
}
export default new SlaAPI();
@@ -0,0 +1,78 @@
/* global axios */
import ApiClient from './ApiClient';
class SLAReportsAPI extends ApiClient {
constructor() {
super('applied_slas', { accountScoped: true });
}
get({
from,
to,
assigned_agent_id,
inbox_id,
team_id,
sla_policy_id,
label_list,
page,
} = {}) {
return axios.get(this.url, {
params: {
since: from,
until: to,
assigned_agent_id,
inbox_id,
team_id,
sla_policy_id,
label_list,
page,
},
});
}
download({
from,
to,
assigned_agent_id,
inbox_id,
team_id,
sla_policy_id,
label_list,
} = {}) {
return axios.get(`${this.url}/download`, {
params: {
since: from,
until: to,
assigned_agent_id,
inbox_id,
team_id,
label_list,
sla_policy_id,
},
});
}
getMetrics({
from,
to,
assigned_agent_id,
inbox_id,
team_id,
label_list,
sla_policy_id,
} = {}) {
return axios.get(`${this.url}/metrics`, {
params: {
since: from,
until: to,
assigned_agent_id,
inbox_id,
label_list,
team_id,
sla_policy_id,
},
});
}
}
export default new SLAReportsAPI();
@@ -10,4 +10,29 @@ describe('#AgentAPI', () => {
expect(agents).toHaveProperty('update');
expect(agents).toHaveProperty('delete');
});
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
window.axios = axiosMock;
});
afterEach(() => {
window.axios = originalAxios;
});
it('#bulkInvite', () => {
agents.bulkInvite({ emails: ['hello@hi.com'] });
expect(axiosMock.post).toHaveBeenCalledWith(
'/api/v1/agents/bulk_create',
{
emails: ['hello@hi.com'],
}
);
});
});
});
@@ -27,20 +27,36 @@ describe('#NotificationAPI', () => {
window.axios = originalAxios;
});
it('#get', () => {
notificationsAPI.get({
page: 1,
status: 'read',
type: 'Conversation',
sortOrder: 'desc',
});
expect(axiosMock.get).toHaveBeenCalledWith('/api/v1/notifications', {
params: {
describe('#get', () => {
it('generates the API call if both params are available', () => {
notificationsAPI.get({
page: 1,
status: 'read',
type: 'Conversation',
sort_order: 'desc',
},
status: 'snoozed',
type: 'read',
sortOrder: 'desc',
});
expect(axiosMock.get).toHaveBeenCalledWith('/api/v1/notifications', {
params: {
page: 1,
sort_order: 'desc',
includes: ['snoozed', 'read'],
},
});
});
it('generates the API call if one of the params are available', () => {
notificationsAPI.get({
page: 1,
type: 'read',
sortOrder: 'desc',
});
expect(axiosMock.get).toHaveBeenCalledWith('/api/v1/notifications', {
params: {
page: 1,
sort_order: 'desc',
includes: ['read'],
},
});
});
});
@@ -111,6 +111,40 @@ describe('#Reports API', () => {
});
});
it('#getBotMetrics', () => {
reportsAPI.getBotMetrics({ from: 1621103400, to: 1621621800 });
expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v2/reports/bot_metrics',
{
params: {
since: 1621103400,
until: 1621621800,
},
}
);
});
it('#getBotSummary', () => {
reportsAPI.getBotSummary({
from: 1621103400,
to: 1621621800,
groupBy: 'date',
businessHours: true,
});
expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v2/reports/bot_summary',
{
params: {
since: 1621103400,
until: 1621621800,
type: 'account',
group_by: 'date',
business_hours: true,
},
}
);
});
it('#getConversationMetric', () => {
reportsAPI.getConversationMetric('account');
expect(axiosMock.get).toHaveBeenCalledWith(
@@ -0,0 +1,104 @@
import SLAReportsAPI from '../slaReports';
import ApiClient from '../ApiClient';
describe('#SLAReports API', () => {
it('creates correct instance', () => {
expect(SLAReportsAPI).toBeInstanceOf(ApiClient);
expect(SLAReportsAPI.apiVersion).toBe('/api/v1');
expect(SLAReportsAPI).toHaveProperty('get');
expect(SLAReportsAPI).toHaveProperty('getMetrics');
});
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
window.axios = axiosMock;
});
afterEach(() => {
window.axios = originalAxios;
});
it('#get', () => {
SLAReportsAPI.get({
page: 1,
from: 1622485800,
to: 1623695400,
assigned_agent_id: 1,
inbox_id: 1,
team_id: 1,
sla_policy_id: 1,
label_list: ['label1'],
});
expect(axiosMock.get).toHaveBeenCalledWith('/api/v1/applied_slas', {
params: {
page: 1,
since: 1622485800,
until: 1623695400,
assigned_agent_id: 1,
inbox_id: 1,
team_id: 1,
sla_policy_id: 1,
label_list: ['label1'],
},
});
});
it('#getMetrics', () => {
SLAReportsAPI.getMetrics({
from: 1622485800,
to: 1623695400,
assigned_agent_id: 1,
inbox_id: 1,
team_id: 1,
sla_policy_id: 1,
label_list: ['label1'],
});
expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v1/applied_slas/metrics',
{
params: {
since: 1622485800,
until: 1623695400,
assigned_agent_id: 1,
inbox_id: 1,
team_id: 1,
sla_policy_id: 1,
label_list: ['label1'],
},
}
);
});
it('#download', () => {
SLAReportsAPI.download({
from: 1622485800,
to: 1623695400,
assigned_agent_id: 1,
inbox_id: 1,
team_id: 1,
sla_policy_id: 1,
label_list: ['label1'],
});
expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v1/applied_slas/download',
{
params: {
since: 1622485800,
until: 1623695400,
assigned_agent_id: 1,
inbox_id: 1,
team_id: 1,
sla_policy_id: 1,
label_list: ['label1'],
},
}
);
});
});
});
@@ -1,6 +1,13 @@
@import '~vue2-datepicker/scss/index';
.date-picker {
// To be removed one SLA reports date picker is created
&.small {
.mx-input {
@apply h-8 text-sm;
}
}
&.no-margin {
.mx-input {
@apply mb-0;
@@ -1,58 +0,0 @@
.button {
font-family: $body-font-family;
font-weight: $font-weight-medium;
&.round {
border-radius: 1000px;
}
}
select {
height: 2.5rem;
}
.card {
margin-bottom: var(--space-small);
padding: var(--space-normal);
}
code {
border: 0;
font-family: 'ui-monospace', 'SFMono-Regular', 'Menlo', 'Monaco', 'Consolas',
'"Liberation Mono"', '"Courier New"', 'monospace';
font-size: $font-size-mini;
&.hljs {
background: $color-background;
border-radius: var(--border-radius-large);
padding: $space-two;
@apply bg-slate-50 dark:bg-slate-700 text-slate-800 dark:text-slate-100;
}
}
.text-truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.text-capitalize {
text-transform: capitalize;
}
.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);
}
@@ -1,623 +0,0 @@
// Foundation for Sites Settings
// -----------------------------
//
// Table of Contents:
//
// 1. Global
// 2. Breakpoints
// 3. The Grid
// 4. Base Typography
// 5. Typography Helpers
// 6. Abide
// 7. Accordion
// 8. Accordion Menu
// 9. Badge
// 10. Breadcrumbs
// 11. Button
// 12. Button Group
// 13. Callout
// 14. Card
// 15. Close Button
// 16. Drilldown
// 17. Dropdown
// 18. Dropdown Menu
// 19. Forms
// 20. Label
// 21. Media Object
// 22. Menu
// 23. Meter
// 24. Off-canvas
// 25. Orbit
// 26. Pagination
// 27. Progress Bar
// 28. Responsive Embed
// 29. Reveal
// 30. Slider
// 31. Switch
// 32. Table
// 33. Tabs
// 34. Thumbnail
// 35. Title Bar
// 36. Tooltip
// 37. Top Bar
@import '~foundation-sites/scss/util/util';
// 1. Global
// ---------
// Disable contrast warnings in Foundation.
$contrast-warnings: false;
$global-font-size: 16px;
$global-width: 100%;
$global-lineheight: 1.5;
$foundation-palette: (primary: $color-woot,
secondary: #5d7592,
success: #44ce4b,
warning: #ffc532,
alert: #ff382d);
$light-gray: #c0ccda;
$medium-gray: #8492a6;
$dark-gray: $color-gray;
$black: #000;
$white: #fff;
$body-background: $white;
$body-font-color: $color-body;
$body-font-family: 'PlusJakarta',
-apple-system,
system-ui,
BlinkMacSystemFont,
"Segoe UI",
Roboto,
"Helvetica Neue",
Tahoma,
Arial,
sans-serif;
$body-antialiased: true;
$global-margin: $space-small;
$global-padding: $space-small;
$global-weight-normal: normal;
$global-weight-bold: bold;
$global-radius: 0;
$global-text-direction: ltr;
$global-flexbox: false;
$print-transparent-backgrounds: true;
@include add-foundation-colors;
// 2. Breakpoints
// --------------
$breakpoints: (small: 0,
medium: 640px,
large: 1024px,
xlarge: 1200px,
xxlarge: 1400px,
xxxlarge: 1600px,
);
$print-breakpoint: large;
$breakpoint-classes: (small medium large);
// 3. The Grid
// -----------
$grid-row-width: $global-width;
$grid-column-count: 12;
$grid-column-gutter: (small: $zero,
medium: $zero);
$grid-column-align-edge: true;
$block-grid-max: 8;
// 4. Base Typography
// ------------------
$header-font-family: $body-font-family;
$header-font-weight: $font-weight-medium;
$header-font-style: normal;
$font-family-monospace: $body-font-family;
$header-color: $color-heading;
$header-lineheight: 1.4;
$header-margin-bottom: 0.3125rem;
$header-styles: (small: ("h1": ("font-size": 24),
"h2": ("font-size": 20),
"h3": ("font-size": 19),
"h4": ("font-size": 18),
"h5": ("font-size": 17),
"h6": ("font-size": 16)),
medium: ("h1": ("font-size": 48),
"h2": ("font-size": 40),
"h3": ("font-size": 31),
"h4": ("font-size": 25),
"h5": ("font-size": 20),
"h6": ("font-size": 16)));
$header-text-rendering: optimizeLegibility;
$small-font-size: 80%;
$header-small-font-color: $medium-gray;
$paragraph-lineheight: 1.65;
$paragraph-margin-bottom: var(--space-small);
$paragraph-text-rendering: optimizeLegibility;
$code-color: $black;
$code-font-family: $font-family-monospace;
$code-font-weight: $global-weight-normal;
$code-background: $light-gray;
$code-border: 1px solid $medium-gray;
$code-padding: rem-calc(2 5 1);
$anchor-color: $primary-color;
$anchor-color-hover: scale-color($anchor-color, $lightness: -14%);
$anchor-text-decoration: none;
$anchor-text-decoration-hover: none;
$hr-width: $global-width;
$hr-border: 1px solid $medium-gray;
$hr-margin: rem-calc(20) auto;
$list-lineheight: $paragraph-lineheight;
$list-margin-bottom: $paragraph-margin-bottom;
$list-style-type: disc;
$list-style-position: outside;
$list-side-margin: 0.78125rem;
$list-nested-side-margin: 0.78125rem;
$defnlist-margin-bottom: 0.6875rem;
$defnlist-term-weight: $global-weight-bold;
$defnlist-term-margin-bottom: 0.1875rem;
$blockquote-color: $dark-gray;
$blockquote-padding: rem-calc(9 20 0 19);
$blockquote-border: 1px solid $medium-gray;
$cite-font-size: rem-calc(13);
$cite-color: $dark-gray;
$cite-pseudo-content: '\2014 \0020';
$keystroke-font: $font-family-monospace;
$keystroke-color: $black;
$keystroke-background: $light-gray;
$keystroke-padding: rem-calc(2 4 0);
$keystroke-radius: $global-radius;
$abbr-underline: 1px dotted $black;
// 5. Typography Helpers
// ---------------------
$lead-font-size: $global-font-size * 1.25;
$lead-lineheight: 1.6;
$subheader-lineheight: 1.4;
$subheader-color: $dark-gray;
$subheader-font-weight: $global-weight-normal;
$subheader-margin-top: 0.125rem;
$subheader-margin-bottom: 0.3125rem;
$stat-font-size: 1.5625rem;
// 6. Abide
// --------
$abide-inputs: true;
$abide-labels: true;
$input-background-invalid: get-color(alert);
$form-label-color-invalid: get-color(alert);
$input-error-color: get-color(alert);
$input-error-font-size: rem-calc(12);
$input-error-font-weight: $global-weight-bold;
// 7. Accordion
// ------------
$accordion-background: $white;
$accordion-plusminus: true;
$accordion-title-font-size: rem-calc(12);
$accordion-item-color: $primary-color;
$accordion-item-background-hover: $light-gray;
$accordion-item-padding: 0.78125rem 0.625rem;
$accordion-content-background: $white;
$accordion-content-border: 1px solid $light-gray;
$accordion-content-color: $body-font-color;
$accordion-content-padding: 0.625rem;
// 8. Accordion Menu
// -----------------
$accordionmenu-arrows: true;
$accordionmenu-arrow-color: $primary-color;
$accordionmenu-arrow-size: 6px;
// 9. Badge
// --------
$badge-background: $primary-color;
$badge-color: $white;
$badge-color-alt: $black;
$badge-palette: $foundation-palette;
$badge-padding: var(--space-smaller);
$badge-minwidth: 2.1em;
$badge-font-size: var(--font-size-nano);
// 10. Breadcrumbs
// ---------------
$breadcrumbs-margin: 0 0 $global-margin 0;
$breadcrumbs-item-font-size: rem-calc(11);
$breadcrumbs-item-color: $primary-color;
$breadcrumbs-item-color-current: $black;
$breadcrumbs-item-color-disabled: $medium-gray;
$breadcrumbs-item-margin: 0.46875rem;
$breadcrumbs-item-uppercase: true;
$breadcrumbs-item-slash: true;
// 11. Button
// ----------
$button-padding: var(--space-smaller) 1em;
$button-margin: 0 0 $global-margin 0;
$button-fill: solid;
$button-background: $primary-color;
$button-background-hover: scale-color($button-background, $lightness: -15%);
$button-color: $white;
$button-color-alt: $white;
$button-radius: var(--border-radius-normal);
$button-sizes: (tiny: var(--font-size-micro),
small: var(--font-size-mini),
default: var(--font-size-small),
large: var(--font-size-medium));
$button-palette: $foundation-palette;
$button-opacity-disabled: 0.4;
$button-background-hover-lightness: -20%;
$button-hollow-hover-lightness: -50%;
$button-transition: background-color 0.25s ease-out,
color 0.25s ease-out;
// 12. Button Group
// ----------------
$buttongroup-margin: 0;
$buttongroup-spacing: 0;
$buttongroup-child-selector: '.button';
$buttongroup-expand-max: 6;
$buttongroup-radius-on-each: false;
// 13. Callout
// -----------
$callout-background: $white;
$callout-background-fade: 85%;
$callout-border: 1px solid rgba($black, 0.25);
$callout-margin: 0 0 0.625rem 0;
$callout-padding: 0.625rem;
$callout-font-color: $body-font-color;
$callout-font-color-alt: $body-background;
$callout-radius: $global-radius;
$callout-link-tint: 30%;
// 14. Card
// --------
$card-background: $white;
$card-font-color: $body-font-color;
$card-divider-background: $light-gray;
$card-border: 1px solid var(--color-border);
$card-shadow: var(--shadow-small);
$card-border-radius: var(--border-radius-normal);
$card-padding: var(--space-small);
$card-margin: $global-margin;
// 15. Close Button
// ----------------
$closebutton-position: right top;
$closebutton-offset-horizontal: (small: 0.66rem,
medium: 1rem);
$closebutton-offset-vertical: (small: 0.33em,
medium: 0.5rem);
$closebutton-size: (small: 1.5em,
medium: 2em);
$closebutton-lineheight: 1;
$closebutton-color: $dark-gray;
$closebutton-color-hover: $black;
// 16. Drilldown
// -------------
$drilldown-transition: transform 0.15s linear;
$drilldown-arrows: true;
$drilldown-arrow-color: $primary-color;
$drilldown-arrow-size: 6px;
$drilldown-background: $white;
// 17. Dropdown
// ------------
$dropdown-padding: 0.625rem;
$dropdown-background: $body-background;
$dropdown-border: 1px solid $medium-gray;
$dropdown-font-size: 0.625rem;
$dropdown-width: 300px;
$dropdown-radius: $global-radius;
$dropdown-sizes: (tiny: 100px,
small: 200px,
large: 400px);
// 18. Dropdown Menu
// -----------------
$dropdownmenu-arrows: true;
$dropdownmenu-arrow-color: $anchor-color;
$dropdownmenu-arrow-size: 6px;
$dropdownmenu-min-width: 200px;
$dropdownmenu-background: $white;
$dropdownmenu-border: 1px solid $medium-gray;
// 19. Forms
// ---------
$fieldset-border: 1px solid $light-gray;
$fieldset-padding: $space-two;
$fieldset-margin: $space-one $zero;
$legend-padding: rem-calc(0 3);
$form-spacing: $space-normal;
$helptext-color: $color-body;
$helptext-font-size: $font-size-small;
$helptext-font-style: italic;
$input-prefix-color: $color-body;
$input-prefix-background: var(--b-100);
$input-prefix-border: 1px solid $color-border;
$input-prefix-padding: 0.625rem;
$form-label-color: $color-body;
$form-label-font-size: rem-calc(14);
$form-label-font-weight: $font-weight-medium;
$form-label-line-height: 1.8;
$select-background: $white;
$select-triangle-color: $dark-gray;
$select-radius: var(--border-radius-normal);
$input-color: $color-body;
$input-placeholder-color: $light-gray;
$input-font-family: inherit;
$input-font-size: $font-size-default;
$input-font-weight: $global-weight-normal;
$input-background: $white;
$input-background-focus: $white;
$input-background-disabled: $light-gray;
$input-border: 1px solid var(--s-200);
$input-border-focus: 1px solid lighten($primary-color, 15%);
$input-shadow: 0;
$input-shadow-focus: 0;
$input-cursor-disabled: not-allowed;
$input-transition: border-color 0.25s ease-in-out;
$input-number-spinners: true;
$input-radius: var(--border-radius-normal);
$form-button-radius: var(--border-radius-normal);
// 20. Label
// ---------
$label-background: $white;
$label-color: $black;
$label-color-alt: $black;
$label-palette: $foundation-palette;
$label-font-size: $font-size-mini;
$label-padding: $space-smaller $space-small;
$label-radius: var(--border-radius-small);
// 21. Media Object
// ----------------
$mediaobject-margin-bottom: $global-margin;
$mediaobject-section-padding: $global-padding;
$mediaobject-image-width-stacked: 100%;
// 22. Menu
// --------
$menu-margin: 0;
$menu-margin-nested: $space-medium;
$menu-item-padding: $space-slab;
$menu-item-color-active: $white;
$menu-item-background-active: $color-background;
$menu-icon-spacing: 0.15625rem;
$menu-item-background-hover: $light-gray;
$menu-border: $light-gray;
// 23. Meter
// ---------
$meter-height: 0.625rem;
$meter-radius: $global-radius;
$meter-background: $medium-gray;
$meter-fill-good: $success-color;
$meter-fill-medium: $warning-color;
$meter-fill-bad: $alert-color;
// 24. Off-canvas
// --------------
$offcanvas-sizes: (small: 14.375,
medium: 14.375,
);
$offcanvas-vertical-sizes: (small: 14.375,
medium: 14.375,
);
$offcanvas-background: $light-gray;
$offcanvas-shadow: 0 0 10px rgba($black, 0.7);
$offcanvas-push-zindex: 1;
$offcanvas-overlap-zindex: 10;
$offcanvas-reveal-zindex: 1;
$offcanvas-transition-length: 0.5s;
$offcanvas-transition-timing: ease;
$offcanvas-fixed-reveal: true;
$offcanvas-exit-background: rgba($white, 0.25);
$maincontent-class: 'off-canvas-content';
// 25. Orbit
// ---------
$orbit-bullet-background: $medium-gray;
$orbit-bullet-background-active: $dark-gray;
$orbit-bullet-diameter: 0.75rem;
$orbit-bullet-margin: 0.0625rem;
$orbit-bullet-margin-top: 0.5rem;
$orbit-bullet-margin-bottom: 0.5rem;
$orbit-caption-background: rgba($black, 0.5);
$orbit-caption-padding: 0.625rem;
$orbit-control-background-hover: rgba($black, 0.5);
$orbit-control-padding: 0.625rem;
$orbit-control-zindex: 10;
// 26. Pagination
// --------------
$pagination-font-size: rem-calc(14);
$pagination-margin-bottom: $global-margin;
$pagination-item-color: $black;
$pagination-item-padding: rem-calc(3 10);
$pagination-item-spacing: rem-calc(1);
$pagination-radius: $global-radius;
$pagination-item-background-hover: $light-gray;
$pagination-item-background-current: $primary-color;
$pagination-item-color-current: $white;
$pagination-item-color-disabled: $medium-gray;
$pagination-ellipsis-color: $black;
$pagination-mobile-items: false;
$pagination-mobile-current-item: false;
$pagination-arrows: true;
// 27. Progress Bar
// ----------------
$progress-height: 0.625rem;
$progress-background: $medium-gray;
$progress-margin-bottom: $global-margin;
$progress-meter-background: $primary-color;
$progress-radius: $global-radius;
// 28. Responsive Embed
// --------------------
$responsive-embed-margin-bottom: rem-calc(16);
$responsive-embed-ratios: (default: 4 by 3,
widescreen: 16 by 9);
// 29. Reveal
// ----------
$reveal-background: $white;
$reveal-width: 600px;
$reveal-max-width: $global-width;
$reveal-padding: $global-padding;
$reveal-border: 1px solid $medium-gray;
$reveal-radius: $global-radius;
$reveal-zindex: 1005;
$reveal-overlay-background: rgba($black, 0.45);
// 30. Slider
// ----------
$slider-width-vertical: 0.3125rem;
$slider-transition: all 0.2s ease-in-out;
$slider-height: 0.3125rem;
$slider-background: $light-gray;
$slider-fill-background: $medium-gray;
$slider-handle-height: 0.875rem;
$slider-handle-width: 0.875rem;
$slider-handle-background: $primary-color;
$slider-opacity-disabled: 0.25;
$slider-radius: $global-radius;
// 31. Switch
// ----------
$switch-background: $light-gray;
$switch-background-active: $primary-color;
$switch-height: $space-two;
$switch-height-tiny: $space-slab;
$switch-height-small: $space-normal;
$switch-height-large: $space-large;
$switch-radius: $space-large;
$switch-margin: $global-margin;
$switch-paddle-background: $white;
$switch-paddle-offset: $space-micro;
$switch-paddle-radius: $space-large;
$switch-paddle-transition: all 0.15s ease-out;
// 32. Table
// ---------
$table-background: transparent;
$table-color-scale: 5%;
$table-border: 1px solid transparent;
$table-padding: rem-calc(8 10 10);
$table-hover-scale: 2%;
$table-row-hover: darken($table-background, $table-hover-scale);
$table-row-stripe-hover: darken($table-background,
$table-color-scale + $table-hover-scale);
$table-is-striped: false;
$table-striped-background: smart-scale($table-background, $table-color-scale);
$table-stripe: even;
$table-head-background: smart-scale($table-background, $table-color-scale / 2);
$table-head-row-hover: darken($table-head-background, $table-hover-scale);
$table-foot-background: smart-scale($table-background, $table-color-scale);
$table-foot-row-hover: darken($table-foot-background, $table-hover-scale);
$table-head-font-color: $body-font-color;
$table-foot-font-color: $body-font-color;
$show-header-for-stacked: false;
// 33. Tabs
// --------
$tab-margin: 0;
$tab-background: transparent;
$tab-background-active: transparent;
$tab-item-font-size: $font-size-small;
$tab-item-background-hover: transparent;
$tab-item-padding: $space-one $zero;
$tab-color: $primary-color;
$tab-active-color: $primary-color;
$tab-expand-max: 6;
$tab-content-background: transparent;
$tab-content-border: transparent;
$tab-content-color: foreground($tab-background, $primary-color);
$tab-content-padding: 0.625rem;
// 34. Thumbnail
// -------------
$thumbnail-border: solid 4px $white;
$thumbnail-margin-bottom: $global-margin;
$thumbnail-shadow: 0 0 0 1px rgba($black, 0.2);
$thumbnail-shadow-hover: 0 0 6px 1px rgba($primary-color, 0.5);
$thumbnail-transition: box-shadow 200ms ease-out;
$thumbnail-radius: $global-radius;
// 35. Title Bar
// -------------
$titlebar-background: $black;
$titlebar-color: $white;
$titlebar-padding: 0.3125rem;
$titlebar-text-font-weight: bold;
$titlebar-icon-color: $white;
$titlebar-icon-color-hover: $medium-gray;
$titlebar-icon-spacing: 0.15625rem;
// 36. Tooltip
// -----------
$has-tip-font-weight: $global-weight-bold;
$has-tip-border-bottom: dotted 1px $dark-gray;
$tooltip-background-color: $black;
$tooltip-color: $white;
$tooltip-padding: 0.46875rem;
$tooltip-font-size: $font-size-mini;
$tooltip-pip-width: 0.46875rem;
$tooltip-pip-height: $tooltip-pip-width * 0.866;
$tooltip-radius: $global-radius;
// 37. Top Bar
// -----------
$topbar-padding: 0.3125;
$topbar-background: $light-gray;
$topbar-submenu-background: $topbar-background;
$topbar-title-spacing: 0.3125 0.625rem 0.3125 0;
$topbar-input-width: 200px;
$topbar-unstack-breakpoint: medium;
// Internal variable that contains the flex justifying options
$-zf-flex-justify: -zf-flex-justify($global-text-direction);
$menu-items-padding: $space-one;
$xy-grid: false;
@@ -1,64 +1,22 @@
.bg-light {
@apply bg-slate-25 dark:bg-slate-800;
}
.flex-center {
@include flex-align(center, middle);
display: flex;
}
.bottom-space-fix {
margin-bottom: auto;
}
.full-height {
@include full-height();
}
// loader class
.spinner {
@include color-spinner();
display: inline-block;
height: $space-medium;
padding: $zero $space-medium;
position: relative;
vertical-align: middle;
width: $space-medium;
@apply inline-block h-6 py-0 px-6 relative align-middle w-6;
&.message {
@include normal-shadow;
background: $color-white;
border-radius: $space-large;
left: 0;
margin: $space-slab auto;
padding: $space-normal;
top: 0;
@apply bg-white dark:bg-slate-800 rounded-full left-0 my-3 mx-auto p-4 top-0;
&::before {
margin-left: -$space-slab;
margin-top: -$space-slab;
@apply -ml-3 -mt-3;
}
}
&.small {
height: $space-normal;
width: $space-normal;
@apply h-4 w-4;
&::before {
height: $space-normal;
margin-top: -$space-small;
width: $space-normal;
@apply h-4 -mt-2 w-4;
}
}
}
.justify-space-between {
justify-content: space-between;
}
.w-full {
width: 100%;
}
.h-full {
height: 100%;
}
@@ -1,5 +1,19 @@
// scss-lint:disable SpaceAfterPropertyColon
@import 'shared/assets/fonts/inter';
// Inter,
html,
body {
font-family:
'PlusJakarta',
-apple-system,
system-ui,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
'Helvetica Neue',
Tahoma,
Arial,
sans-serif !important;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
height: 100%;
@@ -131,37 +131,12 @@
}
}
.search-header--wrap {
.search--input {
text-align: right;
}
.layout-switch__container {
transform: rotate(180deg);
}
}
// Basic filter dropdown
.basic-filter {
left: 0;
right: unset;
}
// Card label
.label-container {
.label {
margin-left: var(--space-smaller);
margin-right: 0;
}
}
// Secondary sidebar toggle button
.toggle-sidebar {
margin-left: 0;
margin-right: var(--space-minus-small);
transform: rotate(180deg);
}
// Bulk actions
.bulk-action__container {
.triangle {
@@ -202,22 +177,6 @@
}
}
// Notification panel
.notification-wrap {
left: 0;
right: var(--space-jumbo);
.action-button {
margin-left: var(--space-small);
margin-right: 0;
}
.notification-content--wrap {
margin-left: 0;
margin-right: var(--space-small);
}
}
// Help center
.article-container .row--article-block {
td:last-child {
@@ -324,10 +283,6 @@
// Other changes
.account-selector--wrap {
direction: initial;
}
.colorpicker--chrome {
direction: initial;
}
@@ -347,9 +302,4 @@
.contact--form .input-group {
direction: initial;
}
// scss-lint:disable QualifyingElement
.dropdown-menu--header > span.title {
text-align: right;
}
}
@@ -1,33 +0,0 @@
.page-title {
font-size: $font-size-big;
}
.page-sub-title {
font-size: $font-size-large;
word-wrap: break-word;
}
.block-title {
font-size: $font-size-medium;
}
.sub-block-title {
font-size: $font-size-default;
}
.text-block-title {
font-size: $font-size-small;
}
.text-muted {
color: var(--s-300);
}
a {
font-size: $font-size-small;
}
p {
font-size: $font-size-small;
word-spacing: .12em;
}
@@ -1,73 +0,0 @@
.margin-bottom-small {
margin-bottom: var(--space-small);
}
.margin-right-smaller {
margin-right: var(--space-smaller);
}
.margin-left-minus-slab {
margin-left: var(--space-minus-slab);
}
.margin-right-minus-slab {
margin-right: 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 {
@apply border-r border-slate-50 dark:border-slate-700;
}
.border-left {
border-left: 1px solid var(--color-border);
}
.text-ellipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.flex-between {
align-items: center;
display: flex;
justify-content: space-between;
}
.flex-end {
display: flex;
justify-content: end;
}
.flex-align-center {
align-items: center;
display: flex;
}
+349 -40
View File
@@ -15,63 +15,372 @@
@import 'variables';
@import 'mixins';
@import 'foundation-settings';
@import 'helper-classes';
@import 'formulate';
@import 'date-picker';
@import 'foundation-sites/scss/foundation';
@include foundation-everything($flex: true);
@include foundation-prototype-text-utilities;
@include foundation-prototype-text-transformation;
@include foundation-prototype-text-decoration;
@include foundation-prototype-font-styling;
@include foundation-prototype-list-style-type;
@include foundation-prototype-rounded;
@include foundation-prototype-bordered;
@include foundation-prototype-shadow;
@include foundation-prototype-separator;
@include foundation-prototype-overflow;
@include foundation-prototype-display;
@include foundation-prototype-position;
@include foundation-prototype-border-box;
@include foundation-prototype-border-none;
@include foundation-prototype-sizing;
@include foundation-prototype-spacing;
@import 'typography';
@import 'layout';
@import 'animations';
@import 'foundation-custom';
@import 'rtl';
@import 'widgets/base';
@import 'widgets/buttons';
@import 'widgets/conv-header';
@import 'widgets/conversation-card';
@import 'widgets/conversation-view';
@import 'widgets/forms';
@import 'widgets/login';
@import 'widgets/modal';
@import 'widgets/reply-box';
@import 'widgets/report';
@import 'widgets/search-box';
@import 'widgets/sidemenu';
@import 'widgets/snackbar';
@import 'widgets/states';
@import 'widgets/status-bar';
@import 'widgets/tabs';
@import 'widgets/woot-tables';
@import 'views/settings/inbox';
@import 'views/settings/integrations';
@import 'plugins/multiselect';
@import 'plugins/dropdown';
@import '~shared/assets/stylesheets/ionicons';
@import 'utility-helpers';
.tooltip {
@apply bg-slate-900 text-white py-1 px-2 z-40 text-xs rounded-md dark:bg-slate-200 dark:text-slate-900;
}
.hide {
@apply hidden;
}
@layer base {
// scss-lint:disable PropertySortOrder
:root {
--color-amber-25: 254 253 251;
--color-amber-50: 255 249 237;
--color-amber-75: 255 243 208;
--color-amber-100: 255 236 183;
--color-amber-200: 255 224 161;
--color-amber-300: 245 208 140;
--color-amber-400: 228 187 120;
--color-amber-500: 214 163 92;
--color-amber-600: 214 163 92;
--color-amber-700: 255 186 26;
--color-amber-800: 145 89 48;
--color-amber-900: 79 52 34;
--color-ash-100: 235 235 239;
--color-ash-200: 228 228 233;
--color-ash-25: 252 252 253;
--color-ash-300: 221 221 227;
--color-ash-400: 211 212 219;
--color-ash-50: 249 249 251;
--color-ash-500: 185 187 198;
--color-ash-600: 139 141 152;
--color-ash-700: 126 128 138;
--color-ash-75: 242 242 245;
--color-ash-800: 96 100 108;
--color-ash-900: 28 32 36;
--color-primary-25: 251 253 255;
--color-primary-50: 245 249 255;
--color-primary-75: 233 243 255;
--color-primary-100: 218 236 255;
--color-primary-200: 201 226 255;
--color-primary-300: 181 213 255;
--color-primary-400: 155 195 252;
--color-primary-500: 117 171 247;
--color-primary-600: 39 129 246;
--color-primary-700: 16 115 233;
--color-primary-800: 8 109 224;
--color-primary-900: 11 50 101;
--color-ruby-100: 255 220 225;
--color-ruby-200: 255 206 214;
--color-ruby-25: 255 252 253;
--color-ruby-300: 248 191 200;
--color-ruby-400: 239 172 184;
--color-ruby-50: 255 247 248;
--color-ruby-500: 229 146 163;
--color-ruby-600: 229 70 102;
--color-ruby-700: 220 59 93;
--color-ruby-75: 254 234 237;
--color-ruby-800: 202 36 77;
--color-ruby-900: 100 23 43;
--color-teal-100: 224 248 243;
--color-teal-200: 204 243 234;
--color-teal-25: 250 254 253;
--color-teal-300: 184 234 224;
--color-teal-400: 161 222 210;
--color-teal-50: 243 251 249;
--color-teal-500: 83 185 171;
--color-teal-600: 18 165 148;
--color-teal-700: 13 155 138;
--color-teal-75: 236 249 255;
--color-teal-800: 0 133 115;
--color-teal-900: 13 61 56;
--color-green-25: 251 254 252;
--color-green-50: 244 251 246;
--color-green-75: 230 246 235;
--color-green-100: 214 241 223;
--color-green-200: 196 232 209;
--color-green-300: 173 221 192;
--color-green-400: 142 206 170;
--color-green-500: 91 185 139;
--color-green-600: 48 164 108;
--color-green-700: 43 154 102;
--color-green-800: 33 131 88;
--color-green-900: 25 59 45;
--color-mint-25: 249 254 253;
--color-mint-50: 242 251 249;
--color-mint-75: 221 249 242;
--color-mint-100: 200 244 233;
--color-mint-200: 179 236 222;
--color-mint-300: 156 224 208;
--color-mint-400: 126 207 189;
--color-mint-500: 76 187 165;
--color-mint-600: 134 234 212;
--color-mint-700: 125 224 203;
--color-mint-800: 2 120 100;
--color-mint-900: 22 67 60;
--color-sky-25: 249 254 255;
--color-sky-50: 241 250 253;
--color-sky-75: 225 246 253;
--color-sky-100: 209 240 250;
--color-sky-200: 190 231 245;
--color-sky-300: 169 218 237;
--color-sky-400: 141 202 227;
--color-sky-500: 96 179 215;
--color-sky-600: 124 226 254;
--color-sky-700: 116 218 248;
--color-sky-800: 0 116 158;
--color-sky-900: 29 62 86;
--color-indigo-25: 253 253 254;
--color-indigo-50: 247 249 255;
--color-indigo-75: 237 242 254;
--color-indigo-100: 225 233 255;
--color-indigo-200: 210 222 255;
--color-indigo-300: 193 208 255;
--color-indigo-400: 171 189 249;
--color-indigo-500: 141 164 239;
--color-indigo-600: 62 99 221;
--color-indigo-700: 51 88 212;
--color-indigo-800: 58 91 199;
--color-indigo-900: 31 45 92;
--color-iris-25: 253 253 255;
--color-iris-50: 248 248 255;
--color-iris-75: 240 241 254;
--color-iris-100: 230 231 255;
--color-iris-200: 218 220 255;
--color-iris-300: 203 205 255;
--color-iris-400: 184 186 248;
--color-iris-500: 155 158 240;
--color-iris-600: 91 91 214;
--color-iris-700: 81 81 205;
--color-iris-800: 87 83 198;
--color-iris-900: 39 41 98;
--color-violet-25: 253 252 254;
--color-violet-50: 250 248 255;
--color-violet-75: 244 240 254;
--color-violet-100: 235 228 255;
--color-violet-200: 225 217 255;
--color-violet-300: 212 202 254;
--color-violet-400: 194 181 245;
--color-violet-500: 170 153 236;
--color-violet-600: 110 86 207;
--color-violet-700: 101 77 196;
--color-violet-800: 101 80 185;
--color-violet-900: 47 38 95;
--color-pink-25: 255 252 254;
--color-pink-50: 254 247 251;
--color-pink-75: 254 233 245;
--color-pink-100: 251 220 239;
--color-pink-200: 246 206 231;
--color-pink-300: 239 191 221;
--color-pink-400: 231 172 208;
--color-pink-500: 221 147 194;
--color-pink-600: 214 64 159;
--color-pink-700: 207 56 151;
--color-pink-800: 194 41 138;
--color-pink-900: 101 18 73;
--color-orange-25: 254 252 251;
--color-orange-50: 255 247 237;
--color-orange-75: 255 239 214;
--color-orange-100: 255 223 181;
--color-orange-200: 255 209 154;
--color-orange-300: 255 193 130;
--color-orange-400: 245 174 115;
--color-orange-500: 236 148 85;
--color-orange-600: 247 107 21;
--color-orange-700: 239 95 0;
--color-orange-800: 204 78 0;
--color-orange-900: 88 45 29;
}
// scss-lint:disable QualifyingElement
body.dark {
--color-amber-25: 31 19 0;
--color-amber-50: 37 24 4;
--color-amber-75: 48 32 11;
--color-amber-100: 57 39 15;
--color-amber-200: 67 46 18;
--color-amber-300: 83 57 22;
--color-amber-400: 111 77 29;
--color-amber-500: 169 118 42;
--color-amber-600: 169 118 42;
--color-amber-700: 255 203 71;
--color-amber-800: 255 204 77;
--color-amber-900: 255 231 179;
--color-ash-100: 46 48 53;
--color-ash-200: 53 55 60;
--color-ash-25: 24 24 26;
--color-ash-300: 60 63 68;
--color-ash-400: 70 75 80;
--color-ash-50: 27 27 31;
--color-ash-500: 90 97 101;
--color-ash-600: 105 110 119;
--color-ash-700: 120 127 133;
--color-ash-75: 39 40 45;
--color-ash-800: 173 177 184;
--color-ash-900: 237 238 240;
--color-primary-25: 10 17 28;
--color-primary-50: 15 24 38;
--color-primary-75: 15 39 72;
--color-primary-100: 10 49 99;
--color-primary-200: 18 61 117;
--color-primary-300: 29 74 134;
--color-primary-400: 40 89 156;
--color-primary-500: 48 106 186;
--color-primary-600: 39 129 246;
--color-primary-700: 21 116 231;
--color-primary-800: 126 182 255;
--color-primary-900: 205 227 255;
--color-ruby-100: 78 19 37;
--color-ruby-200: 94 26 46;
--color-ruby-25: 25 17 19;
--color-ruby-300: 111 37 57;
--color-ruby-400: 136 52 71;
--color-ruby-50: 30 21 23;
--color-ruby-500: 179 68 90;
--color-ruby-600: 229 70 102;
--color-ruby-700: 236 90 114;
--color-ruby-75: 58 20 30;
--color-ruby-800: 255 148 157;
--color-ruby-900: 254 210 225;
--color-teal-100: 2 59 55;
--color-teal-200: 8 72 67;
--color-teal-25: 13 21 20;
--color-teal-300: 28 105 97;
--color-teal-400: 28 105 97;
--color-teal-50: 17 28 27;
--color-teal-500: 32 126 115;
--color-teal-600: 41 163 131;
--color-teal-700: 14 179 158;
--color-teal-75: 13 45 42;
--color-teal-800: 11 216 182;
--color-teal-900: 173 240 221;
--color-green-25: 14 21 18;
--color-green-50: 18 27 23;
--color-green-75: 19 45 33;
--color-green-100: 17 59 41;
--color-green-200: 23 73 51;
--color-green-300: 32 87 62;
--color-green-400: 40 104 74;
--color-green-500: 47 124 87;
--color-green-600: 48 164 108;
--color-green-700: 51 176 116;
--color-green-800: 61 214 140;
--color-green-900: 177 241 203;
--color-mint-25: 14 21 21;
--color-mint-50: 15 27 27;
--color-mint-75: 9 44 43;
--color-mint-100: 0 58 56;
--color-mint-200: 0 71 68;
--color-mint-300: 16 86 80;
--color-mint-400: 30 104 95;
--color-mint-500: 39 127 112;
--color-mint-600: 134 234 212;
--color-mint-700: 168 245 229;
--color-mint-800: 88 213 186;
--color-mint-900: 196 245 225;
--color-sky-25: 14 21 21;
--color-sky-50: 15 27 27;
--color-sky-75: 9 44 43;
--color-sky-100: 0 58 56;
--color-sky-200: 0 71 68;
--color-sky-300: 16 86 80;
--color-sky-400: 30 104 95;
--color-sky-500: 39 127 112;
--color-sky-600: 134 234 212;
--color-sky-700: 168 245 229;
--color-sky-800: 88 213 186;
--color-sky-900: 196 245 225;
--color-indigo-25: 17 19 31;
--color-indigo-50: 20 23 38;
--color-indigo-75: 24 36 73;
--color-indigo-100: 29 46 98;
--color-indigo-200: 37 57 116;
--color-indigo-300: 48 67 132;
--color-indigo-400: 58 79 151;
--color-indigo-500: 67 93 177;
--color-indigo-600: 62 99 221;
--color-indigo-700: 84 114 228;
--color-indigo-800: 158 177 255;
--color-indigo-900: 214 225 255;
--color-iris-25: 19 19 30;
--color-iris-50: 23 22 37;
--color-iris-75: 32 34 72;
--color-iris-100: 38 42 101;
--color-iris-200: 48 51 116;
--color-iris-300: 61 62 130;
--color-iris-400: 74 74 149;
--color-iris-500: 89 88 177;
--color-iris-600: 91 91 214;
--color-iris-700: 110 106 222;
--color-iris-800: 177 169 255;
--color-iris-900: 224 223 254;
--color-violet-25: 20 18 31;
--color-violet-50: 27 21 37;
--color-violet-75: 41 31 67;
--color-violet-100: 51 37 91;
--color-violet-200: 60 46 105;
--color-violet-300: 71 56 118;
--color-violet-400: 86 70 139;
--color-violet-500: 105 88 173;
--color-violet-600: 110 86 207;
--color-violet-700: 125 102 217;
--color-violet-800: 186 167 255;
--color-violet-900: 226 221 254;
--color-pink-25: 25 17 23;
--color-pink-50: 33 18 29;
--color-pink-75: 55 23 47;
--color-pink-100: 75 20 61;
--color-pink-200: 89 28 71;
--color-pink-300: 105 41 85;
--color-pink-400: 131 56 105;
--color-pink-500: 168 72 133;
--color-pink-600: 214 64 159;
--color-pink-700: 222 81 168;
--color-pink-800: 255 141 204;
--color-pink-900: 253 209 234;
--color-orange-25: 23 18 14;
--color-orange-50: 30 22 15;
--color-orange-75: 51 30 11;
--color-orange-100: 70 33 0;
--color-orange-200: 86 40 0;
--color-orange-300: 102 53 12;
--color-orange-400: 126 69 29;
--color-orange-500: 163 88 41;
--color-orange-600: 247 107 21;
--color-orange-700: 255 128 31;
--color-orange-800: 255 160 87;
--color-orange-900: 255 224 194;
}
}
@@ -1,46 +1,7 @@
.dropdown-pane {
@include elegant-card;
@include border-light;
box-sizing: content-box;
padding: var(--space-small);
width: fit-content;
z-index: var(--z-index-very-high);
@apply border rounded-lg hidden relative invisible shadow-lg border-slate-25 dark:border-slate-700 box-content p-2 w-fit z-[9999];
&.dropdown-pane--open {
@apply bg-white dark:bg-slate-800;
display: block;
visibility: visible;
}
&.dropdowm--bottom {
&::before {
@include arrow(top, var(--color-border-light), 14px);
position: absolute;
right: 6px;
top: -14px;
}
&::after {
@include arrow(top, $color-white, var(--space-slab));
position: absolute;
right: var(--space-small);
top: -12px;
}
}
&.dropdowm--top {
&::before {
@include arrow(bottom, var(--color-border-light), 14px);
bottom: -14px;
position: absolute;
right: 6px;
}
&::after {
@include arrow(bottom, $color-white, var(--space-slab));
bottom: -12px;
position: absolute;
right: var(--space-small);
}
@apply bg-white absolute dark:bg-slate-800 block visible;
}
}
@@ -97,6 +97,10 @@
.multiselect__tags {
@apply bg-white dark:bg-slate-900 border border-solid border-slate-200 dark:border-slate-600 m-0 min-h-[2.875rem] pt-0;
input {
@apply border-0 border-none;
}
}
.multiselect__tags-wrap {
@@ -149,6 +153,28 @@
}
.multiselect-wrap--small {
// To be removed one SLA reports date picker is created
&.tiny {
.multiselect.no-margin {
@apply min-h-[32px];
}
.multiselect__select {
@apply min-h-[32px] h-8;
&::before {
@apply top-[60%];
}
}
.multiselect__tags {
@apply min-h-[32px] max-h-[32px];
.multiselect__single {
@apply pt-1 pb-1;
}
}
}
.multiselect__tags,
.multiselect__input,
@@ -180,7 +206,6 @@
.multiselect--disabled .multiselect__select {
@apply bg-transparent;
}
}
.multiselect-wrap--medium {
@@ -13,20 +13,14 @@
@import '~shared/assets/stylesheets/ionicons';
@import 'mixins';
@import 'foundation-settings';
@import 'helper-classes';
@import 'foundation-sites/scss/foundation';
@include foundation-prototype-spacing;
@include foundation-everything($flex: true);
@import 'typography';
@import 'layout';
@import 'animations';
@import 'foundation-custom';
@import 'widgets/buttons';
@import 'widgets/forms';
@import 'widgets/base';
@import 'plugins/multiselect';
@@ -36,7 +30,6 @@
@import 'tailwindcss/utilities';
@import 'widget/assets/scss/utilities';
html,
body {
font-family: 'PlusJakarta', sans-serif;
@@ -1,112 +1 @@
.settings {
@apply overflow-auto;
}
.wizard-box {
.item {
@apply cursor-pointer py-4 pr-4 pl-6 relative;
&::before,
&::after {
@apply bg-slate-75 dark:bg-slate-600 content-[''] h-full absolute top-5 w-0.5;
}
&::before {
@apply h-4 top-0;
}
&:first-child {
&::before {
@apply h-0;
}
}
&:last-child {
&::after {
@apply h-0;
}
}
&.active {
h3 {
@apply text-woot-500 dark:text-woot-500;
}
.step {
@apply bg-woot-500 dark:bg-woot-500;
}
}
&.over {
&::after {
@apply bg-woot-500 dark:bg-woot-500;
}
.step {
@apply bg-woot-500 dark:bg-woot-500;
}
& + .item {
&::before {
@apply bg-woot-500 dark:bg-woot-500;
}
}
}
h3 {
@apply text-slate-800 dark:text-slate-100 text-base pl-6;
}
.completed {
@apply text-green-500 dark:text-green-500 ml-1;
}
p {
@apply text-slate-600 dark:text-slate-300 text-sm m-0 pl-6;
}
.step {
@apply bg-slate-75 dark:bg-slate-600 rounded-2xl font-medium w-4 left-4 leading-4 z-[999] absolute text-center text-white dark:text-white text-xxs top-5;
i {
@apply text-xxs;
}
}
}
}
.wizard-body {
@apply border border-slate-25 dark:border-slate-800/60 bg-white dark:bg-slate-900 h-full p-6;
&.height-auto {
@apply h-auto;
}
}
.settings--content {
@apply my-2 mx-8;
.title {
@apply font-medium;
}
.code {
@apply bg-slate-50 dark:bg-slate-800 overflow-auto p-2.5 whitespace-nowrap;
code {
@apply bg-transparent border-0;
}
}
}
.login-init {
@apply pt-[30%] text-center;
p {
@apply p-6;
}
> a > img {
@apply w-60;
}
}
// to be removed
@@ -0,0 +1,146 @@
// scss-lint:disable QualifyingElement
// Base typography
h1,
h2,
h3,
h4,
h5,
h6 {
@apply font-medium text-slate-800 dark:text-slate-50;
}
p {
text-rendering: optimizeLegibility;
word-spacing: 0.12em;
@apply mb-2 leading-[1.65] text-sm;
a {
@apply text-woot-500 dark:text-woot-500 cursor-pointer;
}
}
a {
@apply text-sm;
}
hr {
@apply clear-both max-w-full h-0 my-5 mx-0 border-slate-300 dark:border-slate-600;
}
ul,
ol,
dl {
@apply mb-2 list-disc list-outside leading-[1.65];
}
// Form elements
label {
@apply text-slate-800 dark:text-slate-200 block m-0 leading-7 text-sm font-medium;
&.error {
input {
@apply mb-1;
}
}
}
.input-wrap,
.help-text {
@apply text-slate-800 dark:text-slate-100 text-sm font-medium;
.help-text {
@apply font-normal text-slate-600 dark:text-slate-400;
}
}
// Focus outline removal
.button,
textarea,
input:focus {
outline: none;
}
// Inputs
input[type='text']:not(.reset-base),
input[type='number']:not(.reset-base),
input[type='password']:not(.reset-base),
input[type='date']:not(.reset-base),
input[type='email']:not(.reset-base),
input[type='url']:not(.reset-base) {
@apply block box-border w-full transition-colors focus:border-woot-500 dark:focus:border-woot-600 duration-[0.25s] ease-[ease-in-out] h-10 appearance-none mx-0 mt-0 mb-4 p-2 rounded-md text-base font-normal bg-white dark:bg-slate-900 focus:bg-white focus:dark:bg-slate-900 text-slate-900 dark:text-slate-100 border border-solid border-slate-200 dark:border-slate-600;
&[disabled] {
@apply bg-slate-200 dark:bg-slate-700 text-slate-400 dark:text-slate-400 border-slate-200 dark:border-slate-600 cursor-not-allowed;
}
}
input[type='file'] {
@apply bg-white dark:bg-slate-800 leading-[1.15] mb-4;
}
// Select
select {
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' version='1.1' width='32' height='24' viewBox='0 0 32 24'><polygon points='0,0 32,0 16,24' style='fill: rgb%28110, 111, 115%29'></polygon></svg>");
background-position: right -1rem center;
background-size: 9px 6px;
@apply h-10 mx-0 mt-0 mb-4 bg-origin-content focus-visible:outline-none bg-no-repeat py-2 pr-6 pl-2 rounded-md w-full text-base font-normal appearance-none transition-colors focus:border-woot-500 dark:focus:border-woot-600 duration-[0.25s] ease-[ease-in-out] bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-100 border border-solid border-slate-200 dark:border-slate-600;
}
// Textarea
textarea {
@apply block box-border w-full transition-colors focus:border-woot-500 dark:focus:border-woot-600 duration-[0.25s] ease-[ease-in-out] h-16 appearance-none mx-0 mt-0 mb-4 p-2 rounded-md text-base font-normal bg-white dark:bg-slate-900 focus:bg-white focus:dark:bg-slate-900 text-slate-900 dark:text-slate-100 border border-solid border-slate-200 dark:border-slate-600;
&[disabled] {
@apply bg-slate-200 dark:bg-slate-700 text-slate-400 dark:text-slate-400 border-slate-200 dark:border-slate-600 cursor-not-allowed;
}
}
// Error handling
.has-multi-select-error {
div.multiselect {
@apply mb-1;
}
}
.error {
input,
input:not([type]),
textarea,
select,
.multiselect > .multiselect__tags,
.multiselect:not(.no-margin) {
@apply border border-solid border-red-400 dark:border-red-400 mb-1;
}
.message {
@apply text-red-400 dark:text-red-400 block text-sm mb-2.5 w-full;
}
}
.input-group.small {
input {
@apply text-sm h-8;
}
.error {
@apply border-red-400 dark:border-red-400;
}
}
// Code styling
code {
font-family: 'ui-monospace', 'SFMono-Regular', 'Menlo', 'Monaco', 'Consolas',
'"Liberation Mono"', '"Courier New"', 'monospace';
@apply text-xs border-0;
&.hljs {
@apply bg-slate-50 dark:bg-slate-700 text-slate-800 dark:text-slate-50 rounded-lg p-5;
.hljs-number,
.hljs-string {
@apply text-red-800 dark:text-red-400;
}
}
}
@@ -1,8 +1,40 @@
// scss-lint:disable SpaceAfterPropertyColon
// scss-lint:disable MergeableSelector
button {
font-family: inherit;
transition:
background-color 0.25s ease-out,
color 0.25s ease-out;
@apply inline-block items-center mb-0 text-center align-middle cursor-pointer text-sm mt-0 mx-0 py-1 px-2.5 border border-solid border-transparent dark:border-transparent rounded-[0.3125rem];
&:disabled,
&.disabled {
@apply opacity-40 cursor-not-allowed;
}
}
.button-group {
@apply mb-0 flex flex-nowrap items-stretch;
.button {
flex: 0 0 auto;
@apply m-0 text-sm rounded-none first:rounded-tl-[0.3125rem] first:rounded-bl-[0.3125rem] last:rounded-tr-[0.3125rem] last:rounded-br-[0.3125rem] rtl:space-x-reverse;
}
.button--only-icon {
@apply w-10 justify-center pl-0 pr-0;
}
}
.back-button {
@apply m-0;
}
.button {
@apply items-center inline-flex h-10 mb-0 gap-2;
@apply items-center bg-woot-500 dark:bg-woot-500 px-2.5 text-white dark:text-white inline-flex h-10 mb-0 gap-2 font-medium;
.button__content {
@apply w-full;
@apply w-full whitespace-nowrap overflow-hidden text-ellipsis;
img,
svg {
@@ -10,12 +42,61 @@
}
}
&:hover {
@apply bg-woot-600 dark:bg-woot-600;
}
&:disabled,
&.disabled {
@apply opacity-40 cursor-not-allowed;
}
&.success {
@apply bg-[#44ce4b] dark:bg-[#44ce4b] text-white dark:text-white;
}
&.secondary {
@apply bg-slate-700 dark:bg-slate-600 text-white dark:text-white;
}
&.primary {
@apply bg-woot-500 dark:bg-woot-500 text-white dark:text-white;
}
&.clear {
@apply text-woot-500 dark:text-woot-500 bg-transparent dark:bg-transparent;
}
&.alert {
@apply bg-red-500 dark:bg-red-500 text-white dark:text-white;
&.clear {
@apply bg-transparent dark:bg-transparent;
}
}
&.warning {
@apply bg-[#ffc532] dark:bg-[#ffc532] text-white dark:text-white;
&.clear {
@apply bg-transparent dark:bg-transparent;
}
}
&.tiny {
@apply h-6 text-[10px];
}
&.small {
@apply h-8 text-xs;
}
.spinner {
@apply px-2 py-0;
}
// @TODDO - Remove after moving all buttons to woot-button
.icon + .button__content {
.icon+.button__content {
@apply w-auto;
}
@@ -34,10 +115,10 @@
}
&.hollow {
@apply border border-woot-500 dark:border-woot-500 text-woot-500 dark:text-woot-500 hover:bg-woot-50 dark:hover:bg-woot-900;
@apply border border-woot-500 bg-transparent dark:bg-transparent dark:border-woot-500 text-woot-500 dark:text-woot-500 hover:bg-woot-50 dark:hover:bg-woot-900;
&.secondary {
@apply text-slate-700 border-slate-200 dark:border-slate-600 dark:text-slate-100 hover:bg-slate-50 dark:hover:bg-slate-700;
@apply text-slate-700 border-slate-100 dark:border-slate-800 dark:text-slate-100 hover:bg-slate-50 dark:hover:bg-slate-700;
}
&.success {
@@ -1 +0,0 @@
// File to be removed
@@ -1,16 +0,0 @@
@keyframes left-shift-animation {
0%,
100% {
transform: translateX(0);
}
50% {
transform: translateX(1px);
}
}
.conversation {
&.active {
animation: left-shift-animation 0.25s $swift-ease-out-function;
}
}
@@ -79,7 +79,7 @@
@apply rounded-r-lg rounded-l mr-auto break-words;
&:not(.is-unsupported) {
@apply border border-slate-50 dark:border-slate-700 bg-white dark:bg-slate-700 text-black-900 dark:text-slate-50
@apply border border-slate-50 dark:border-slate-700 bg-white dark:bg-slate-700 text-black-900 dark:text-slate-50;
}
&.is-image {
@@ -91,7 +91,7 @@
}
.file {
.text-block-title {
.attachment-name {
@apply text-slate-700 dark:text-woot-300;
}
@@ -222,20 +222,6 @@
@apply flex relative flex-col;
}
.typing-indicator-wrap {
@apply items-center flex h-0 absolute w-full -top-8;
.typing-indicator {
@include elegant-card;
@include round-corner;
@apply py-2 pr-4 pl-5 bg-white dark:bg-slate-700 text-slate-800 dark:text-slate-100 text-xs font-semibold my-2.5 mx-auto;
.gif {
@apply ml-2 w-6;
}
}
}
.left .bubble .text-content {
h1,
h2,
@@ -1,78 +0,0 @@
// scss-lint:disable QualifyingElement
label {
@apply text-slate-800 dark:text-slate-200;
}
textarea {
@apply bg-white dark:bg-slate-900 focus:bg-white focus:dark:bg-slate-900 text-slate-900 dark:text-slate-100 border-slate-200 dark:border-slate-600;
}
input {
@apply bg-white dark:bg-slate-900 focus:bg-white focus:dark:bg-slate-900 text-slate-900 dark:text-slate-100 border-slate-200 dark:border-slate-600;
&[disabled] {
@apply bg-slate-200 dark:bg-slate-700 text-slate-400 dark:text-slate-400 border-slate-200 dark:border-slate-600;
}
}
input[type='file'] {
@apply bg-white dark:bg-slate-800;
}
select {
@apply bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-100 border-slate-200 dark:border-slate-600;
}
.error {
input[type='color'],
input[type='date'],
input[type='datetime'],
input[type='datetime-local'],
input[type='email'],
input[type='month'],
input[type='number'],
input[type='password'],
input[type='search'],
input[type='tel'],
input[type='text'],
input[type='time'],
input[type='url'],
input[type='week'],
input:not([type]),
textarea,
select,
.multiselect > .multiselect__tags {
@apply border border-solid border-red-400 dark:border-red-400;
}
.message {
@apply text-red-400 dark:text-red-400 block text-sm mb-2.5 w-full;
}
}
.button,
textarea,
input {
&:focus {
outline: none;
}
}
.input-wrap {
@apply text-slate-800 dark:text-slate-100 text-sm font-medium;
}
.help-text {
@apply font-normal text-slate-600 dark:text-slate-400;
}
.input-group.small {
input {
@apply text-sm h-8;
}
.error {
@apply border-red-400 dark:border-red-400;
}
}
@@ -1,66 +0,0 @@
.auth-wrap {
width: 100%;
}
// Outside login wrapper
.login {
@include full-height;
overflow-y: auto;
padding-top: $space-larger * 1.2;
.login__hero {
margin-bottom: $space-larger;
.hero__logo {
width: 180px;
}
.hero__title {
font-weight: $font-weight-light;
margin-top: $space-larger;
}
.hero__sub {
color: $medium-gray;
font-size: $font-size-medium;
}
}
// Login box
.login-box {
@include background-white;
@include border-normal;
@include elegant-card;
border-radius: $space-smaller;
padding: $space-large;
label {
color: $color-gray;
font-size: $font-size-default;
input {
font-size: $font-size-default;
height: $space-larger;
padding: $space-slab;
}
.error {
font-size: $font-size-small;
}
}
.button {
height: $space-larger;
}
}
.sigin__footer {
font-size: $font-size-default;
padding: $space-medium;
> a {
font-weight: $font-weight-bold;
}
}
}
@@ -1,75 +0,0 @@
.modal-mask {
// @include flex;
// @include flex-align(center, middle);
@apply flex items-center justify-center bg-modal-backdrop-light dark:bg-modal-backdrop-dark z-[9990] h-full left-0 fixed top-0 w-full;
}
.page-top-bar {
@apply px-8 pt-9 pb-0;
img {
@apply max-h-[3.75rem];
}
}
.modal-container {
@apply shadow-md rounded-sm max-h-full overflow-auto relative w-[37.5rem];
&.medium {
@apply max-w-[80%] w-[56.25rem];
}
.content-box {
@apply h-auto p-0;
}
h2 {
@apply text-slate-800 dark:text-slate-100 text-lg font-semibold;
}
p {
@apply text-sm m-0 p-0 text-slate-600 mt-2 text-sm dark:text-slate-300;
}
.content {
@apply p-8;
}
form,
.modal-content {
@apply pt-4 pb-8 px-8 self-center;
a {
@apply p-4;
}
}
.modal-footer {
// @include flex;
// @include flex-align($x: flex-end, $y: middle);
@apply flex justify-end items-center py-2 px-0 gap-2;
&.justify-content-end {
@apply justify-end;
}
}
.delete-item {
@apply p-8;
button {
@apply m-0;
}
}
}
.modal-enter,
.modal-leave {
@apply opacity-0;
}
.modal-enter .modal-container,
.modal-leave .modal-container {
transform: scale(1.1);
// @apply transform scale-110;
}
@@ -1,60 +0,0 @@
.reply-box {
transition: box-shadow 0.35s $swift-ease-out-function,
height 2s $swift-ease-out-function;
&.is-focused {
box-shadow: var(--shadow);
}
.reply-box__top {
.icon {
color: $medium-gray;
cursor: pointer;
font-size: $font-size-medium;
margin-right: $space-small;
&.active {
color: $color-woot;
}
}
.attachment {
cursor: pointer;
margin-right: $space-one;
padding: 0 $space-small;
}
.video-js {
background: transparent;
// Override min-height : 50px in foundation
//
max-height: $space-mega * 2.4;
min-height: 3rem;
padding: var(--space-normal) 0 0;
resize: none;
}
> textarea {
@include ghost-input();
background: transparent;
margin: 0;
max-height: $space-mega * 2.4;
// Override min-height : 50px in foundation
min-height: 3rem;
padding: var(--space-normal) 0 0;
resize: none;
}
}
&.is-private {
@apply bg-yellow-100 dark:bg-yellow-800;
.reply-box__top {
@apply bg-yellow-100 dark:bg-yellow-800;
> input {
@apply bg-yellow-100 dark:bg-yellow-800;
}
}
}
}
@@ -1,60 +0,0 @@
.report-card {
@include custom-border-top(3px, transparent);
cursor: pointer;
margin: 0;
padding: var(--space-normal);
&.active {
@include custom-border-top(3px, var(--color-woot));
@include background-white;
.heading,
.metric {
color: var(--color-woot);
}
}
.heading {
align-items: center;
color: var(--color-heading);
display: flex;
font-size: var(--font-size-small);
font-weight: var(--font-weight-bold);
margin: 0;
}
.info-icon {
color: var(--b-400);
margin-left: var(--space-micro);
}
.metric-wrap {
align-items: center;
display: flex;
}
.metric {
font-size: var(--font-size-big);
font-weight: var(--font-weight-feather);
margin-top: var(--space-smaller);
}
.metric-trend {
font-size: var(--font-size-small);
margin: 0 var(--space-small);
}
.metric-up {
color: $success-color;
}
.metric-down {
color: $alert-color;
}
.desc {
font-size: var(--font-size-small);
margin: 0;
text-transform: capitalize;
}
}
@@ -1,29 +0,0 @@
.reports-option__rounded--item {
border-radius: 100%;
height: var(--space-two);
width: var(--space-two);
}
.reports-option__item {
flex-shrink: 0;
margin-right: var(--space-small);
}
.reports-option__label--swatch {
border: 1px solid var(--color-border);
}
.reports-option__wrap {
align-items: center;
display: flex;
}
.reports-option__title {
margin: 0 var(--space-small);
}
.switch {
margin-bottom: var(--space-zero);
margin-left: var(--space-small);
}
@@ -1,18 +0,0 @@
.search {
@include flex;
@include flex-align($x: left, $y: middle);
@include flex-shrink;
padding: $space-one $space-normal;
transition: all 0.3s var(--ease-in-out-quad);
> .icon {
color: $medium-gray;
font-size: $font-size-medium;
}
> input {
@include ghost-input();
margin: 0;
}
}
@@ -1,78 +0,0 @@
.side-menu {
i {
margin-right: var(--space-smaller);
min-width: var(--space-two);
}
}
.sidebar {
z-index: 1024 - 1;
//logo
.logo {
img {
max-height: 108px;
padding: $woot-logo-padding;
}
}
.nested {
a {
font-size: var(--font-size-small);
margin-bottom: var(--space-micro);
margin-top: var(--space-micro);
.inbox-icon {
display: inline-block;
margin-right: var(--space-micro);
min-width: var(--space-normal);
text-align: center;
}
}
}
}
// bottom-nav
.bottom-nav {
@include flex;
@include space-between-column;
@include border-normal-top;
flex-direction: column;
padding: var(--space-one) var(--space-normal) var(--space-one)
var(--space-one);
position: relative;
&:hover {
background: var(--color-background-light);
}
.dropdown-pane {
bottom: 3.75rem;
display: block;
visibility: visible;
width: fit-content;
}
.active {
border-bottom: 2px solid $medium-gray;
}
}
.hamburger--menu {
cursor: pointer;
display: block;
margin-right: var(--space-normal);
}
.header--icon {
display: block;
margin: 0 var(--space-small) 0 var(--space-smaller);
@media screen and (max-width: 1200px) {
display: none;
}
}
.header-title {
margin: 0 var(--space-small);
}
@@ -1,45 +0,0 @@
.ui-snackbar-container {
left: 0;
margin: 0 auto;
max-width: 25rem;
overflow: hidden;
position: absolute;
right: 0;
text-align: center;
top: $space-normal;
z-index: 9999;
}
.ui-snackbar {
@include shadow;
background-color: $woot-snackbar-bg;
border-radius: $space-smaller;
display: inline-flex;
margin-bottom: $space-small;
max-width: 25rem;
min-height: 1.875rem;
min-width: 15rem;
padding: $space-slab $space-medium;
text-align: left;
}
.ui-snackbar-text {
color: $color-white;
font-size: $font-size-small;
font-weight: $font-weight-medium;
}
.ui-snackbar-action {
margin-left: auto;
padding-left: 1.875rem;
button {
background: none;
border: 0;
color: $woot-snackbar-button;
font-size: $font-size-small;
margin: 0;
padding: 0;
text-transform: uppercase;
}
}
@@ -1 +0,0 @@
// To be removed
@@ -1,46 +0,0 @@
.status-bar {
@include flex;
@include flex-align($x: center, $y: middle);
background: lighten($warning-color, 36%);
flex-direction: column;
margin: 0;
padding: $space-normal $space-smaller;
.message {
font-weight: $font-weight-medium;
margin-bottom: $zero;
}
.button {
margin: $space-smaller $zero $zero;
padding: $space-small $space-normal;
}
&.danger {
background: lighten($alert-color, 30%);
.button {
// Default and disabled states
&,
&.disabled,
&[disabled],
&.disabled:hover,
&[disabled]:hover,
&.disabled:focus,
&[disabled]:focus {
background-color: $alert-color;
color: $color-white;
}
&:hover,
&:focus {
background-color: darken($alert-color, 7%);
color: $color-white;
}
}
}
&.warning {
background: lighten($warning-color, 36%);
}
}
@@ -7,7 +7,7 @@
}
.tabs {
@apply border-r-0 border-l-0 border-t-0 flex min-w-[6.25rem] py-0 px-4;
@apply border-r-0 border-l-0 border-t-0 flex min-w-[6.25rem] py-0 px-4 list-none mb-0;
}
.tabs--with-scroll {
@@ -31,7 +31,7 @@
}
.tabs-title {
@apply flex-shrink-0 my-0 mx-2 ;
@apply flex-shrink-0 my-0 mx-2;
.badge {
@apply bg-slate-50 dark:bg-slate-800 rounded-md text-slate-600 dark:text-slate-100 h-5 flex items-center justify-center text-xxs font-semibold my-0 mx-1 px-1 py-0;
@@ -53,7 +53,7 @@
}
a {
@apply flex items-center flex-row border-b border-transparent text-slate-500 dark:text-slate-200 text-sm top-[1px] relative;
@apply flex items-center flex-row border-b py-2.5 select-none cursor-pointer border-transparent text-slate-500 dark:text-slate-200 text-sm top-[1px] relative;
transition: border-color 0.15s $swift-ease-out-function;
}
@@ -1,9 +1,9 @@
table {
@apply border-spacing-0 text-sm;
@apply border-spacing-0 text-sm w-full;
thead {
th {
@apply font-semibold tracking-[1px] text-left uppercase text-slate-900 dark:text-slate-200;
@apply font-semibold tracking-[1px] text-left px-2.5 uppercase text-slate-900 dark:text-slate-200;
}
}
@@ -1,7 +1,7 @@
<template>
<div class="-mt-px text-sm">
<button
class="flex items-center select-none w-full bg-slate-50 dark:bg-slate-800 border border-l-0 border-r-0 border-solid m-0 border-slate-100 dark:border-slate-700/50 cursor-grab justify-between py-2 px-4 drag-handle"
class="flex items-center select-none w-full rounded-none bg-slate-50 dark:bg-slate-800 border border-l-0 border-r-0 border-solid m-0 border-slate-100 dark:border-slate-700/50 cursor-grab justify-between py-2 px-4 drag-handle"
@click="$emit('click')"
>
<div class="flex justify-between mb-0.5">
@@ -1,10 +1,12 @@
<template>
<button
class="sm:w-[50%] md:w-[34%] lg:w-[25%] channel"
class="sm:w-[50%] md:w-1/3 lg:w-1/4 bg-white dark:bg-slate-900 cursor-pointer flex flex-col transition-all duration-200 ease-in -m-px py-4 px-0 items-center border border-solid border-slate-25 dark:border-slate-800 hover:border-woot-500 dark:hover:border-woot-500 hover:shadow-md hover:z-50 disabled:opacity-60"
@click="$emit('click')"
>
<img :src="src" :alt="title" />
<h3 class="channel__title">
<img :src="src" :alt="title" class="w-[50%] my-4 mx-auto" />
<h3
class="text-slate-800 dark:text-slate-100 text-base text-center capitalize"
>
{{ title }}
</h3>
</button>
@@ -31,32 +33,8 @@ export default {
filter: grayscale(100%);
}
&.channel:hover {
&:hover {
@apply border-transparent shadow-none cursor-not-allowed;
}
}
.channel {
@apply bg-white dark:bg-slate-900 cursor-pointer flex flex-col transition-all duration-200 ease-in -m-px py-4 px-0 items-center border border-solid border-slate-25 dark:border-slate-800;
&:hover {
@apply border-woot-500 dark:border-woot-500 shadow-md z-50;
}
&.disabled {
@apply opacity-60;
}
img {
@apply w-[50%] my-4 mx-auto;
}
.channel__title {
@apply text-slate-800 dark:text-slate-100 text-base text-center capitalize;
}
p {
@apply text-slate-600 dark:text-slate-300 w-full text-sm;
}
}
</style>
@@ -1,14 +1,14 @@
<template>
<div
class="conversations-list-wrap flex-basis-clamp flex-shrink-0 overflow-hidden flex flex-col border-r rtl:border-r-0 rtl:border-l border-slate-50 dark:border-slate-800/50"
:class="{
hide: !showConversationList,
'list--full-width': isOnExpandedLayout,
}"
class="flex flex-col flex-shrink-0 overflow-hidden border-r conversations-list-wrap rtl:border-r-0 rtl:border-l border-slate-50 dark:border-slate-800/50"
:class="[
{ hidden: !showConversationList },
isOnExpandedLayout ? 'basis-full' : 'flex-basis-clamp',
]"
>
<slot />
<div
class="flex items-center justify-between py-0 px-4"
class="flex items-center justify-between px-4 py-0"
:class="{
'pb-3 border-b border-slate-75 dark:border-slate-700':
hasAppliedFiltersOrActiveFolders,
@@ -16,7 +16,7 @@
>
<div class="flex max-w-[85%] justify-center items-center">
<h1
class="text-xl break-words overflow-hidden whitespace-nowrap text-ellipsis text-black-900 dark:text-slate-100 mb-0"
class="text-xl font-medium break-words truncate text-black-900 dark:text-slate-100"
:title="pageTitle"
>
{{ pageTitle }}
@@ -107,7 +107,7 @@
<p
v-if="!chatListLoading && !conversationList.length"
class="overflow-auto p-4 flex justify-center items-center"
class="flex items-center justify-center p-4 overflow-auto"
>
{{ $t('CHAT_LIST.LIST.404') }}
</p>
@@ -127,7 +127,7 @@
/>
<div
ref="conversationList"
class="conversations-list flex-1"
class="flex-1 conversations-list"
:class="{ 'overflow-hidden': isContextMenuOpen }"
>
<virtual-list
@@ -136,14 +136,17 @@
:data-sources="conversationList"
:data-component="itemComponent"
:extra-props="virtualListExtraProps"
class="w-full overflow-auto h-full"
class="w-full h-full overflow-auto"
footer-tag="div"
>
<template #footer>
<div v-if="chatListLoading" class="text-center">
<span class="spinner mt-4 mb-4" />
<span class="mt-4 mb-4 spinner" />
</div>
<p v-if="showEndOfListMessage" class="text-center text-muted p-4">
<p
v-if="showEndOfListMessage"
class="p-4 text-center text-slate-400 dark:text-slate-300"
>
{{ $t('CHAT_LIST.EOF') }}
</p>
<intersection-observer
@@ -182,7 +185,7 @@ import ConversationBasicFilter from './widgets/conversation/ConversationBasicFil
import ChatTypeTabs from './widgets/ChatTypeTabs.vue';
import ConversationItem from './ConversationItem.vue';
import timeMixin from '../mixins/time';
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
import conversationMixin from '../mixins/conversations';
import wootConstants from 'dashboard/constants/globals';
import advancedFilterTypes from './widgets/conversation/advancedFilterItems';
@@ -196,11 +199,6 @@ import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import languages from 'dashboard/components/widgets/conversation/advancedFilterItems/languages';
import countries from 'shared/constants/countries';
import { generateValuesForEditCustomViews } from 'dashboard/helper/customViewsHelper';
import {
hasPressedAltAndJKey,
hasPressedAltAndKKey,
} from 'shared/helpers/KeyboardHelpers';
import { conversationListPageURL } from '../helper/URLHelper';
import {
isOnMentionsView,
@@ -225,7 +223,7 @@ export default {
mixins: [
timeMixin,
conversationMixin,
eventListenerMixins,
keyboardEventListenerMixins,
alertMixin,
filterMixin,
uiSettingsMixin,
@@ -688,30 +686,40 @@ export default {
lastConversationIndex,
};
},
handleKeyEvents(e) {
if (hasPressedAltAndJKey(e)) {
const { allConversations, activeConversationIndex } =
this.getKeyboardListenerParams();
if (activeConversationIndex === -1) {
allConversations[0].click();
}
if (activeConversationIndex >= 1) {
allConversations[activeConversationIndex - 1].click();
}
handlePreviousConversation() {
const { allConversations, activeConversationIndex } =
this.getKeyboardListenerParams();
if (activeConversationIndex === -1) {
allConversations[0].click();
}
if (hasPressedAltAndKKey(e)) {
const {
allConversations,
activeConversationIndex,
lastConversationIndex,
} = this.getKeyboardListenerParams();
if (activeConversationIndex === -1) {
allConversations[lastConversationIndex].click();
} else if (activeConversationIndex < lastConversationIndex) {
allConversations[activeConversationIndex + 1].click();
}
if (activeConversationIndex >= 1) {
allConversations[activeConversationIndex - 1].click();
}
},
handleNextConversation() {
const {
allConversations,
activeConversationIndex,
lastConversationIndex,
} = this.getKeyboardListenerParams();
if (activeConversationIndex === -1) {
allConversations[lastConversationIndex].click();
} else if (activeConversationIndex < lastConversationIndex) {
allConversations[activeConversationIndex + 1].click();
}
},
getKeyboardEvents() {
return {
'Alt+KeyJ': {
action: () => this.handlePreviousConversation(),
allowOnFocusedInput: true,
},
'Alt+KeyK': {
action: () => this.handleNextConversation(),
allowOnFocusedInput: true,
},
};
},
resetAndFetchData() {
this.appliedFilter = [];
this.resetBulkActions();
@@ -1031,24 +1039,10 @@ export default {
</style>
<style scoped lang="scss">
.conversations-list-wrap {
&.hide {
@apply hidden;
}
&.list--full-width {
@apply basis-full;
}
}
.conversations-list {
@apply overflow-hidden hover:overflow-y-auto;
}
.load-more--button {
@apply text-center rounded-none;
}
.tab--chat-type {
@apply py-0 px-4;
@@ -1,24 +1,28 @@
<template>
<div class="py-3 px-4">
<div class="flex items-center mb-1">
<h4 class="text-block-title flex items-center m-0 w-full error">
<div v-if="isAttributeTypeCheckbox" class="checkbox-wrap">
<h4 class="text-sm flex items-center m-0 w-full error">
<div v-if="isAttributeTypeCheckbox" class="flex items-center">
<input
v-model="editedValue"
class="checkbox"
class="!my-0 mr-2 ml-0"
type="checkbox"
@change="onUpdate"
/>
</div>
<div class="flex items-center justify-between w-full">
<span
class="attribute-name"
:class="{ error: $v.editedValue.$error }"
class="w-full font-medium text-sm mb-0"
:class="
$v.editedValue.$error
? 'text-red-400 dark:text-red-500'
: 'text-slate-800 dark:text-slate-100'
"
>
{{ label }}
</span>
<woot-button
v-if="showActions"
v-if="showCopyAndDeleteButton"
v-tooltip.left="$t('CUSTOM_ATTRIBUTES.ACTIONS.DELETE')"
variant="link"
size="medium"
@@ -31,20 +35,25 @@
</h4>
</div>
<div v-if="notAttributeTypeCheckboxAndList">
<div v-show="isEditing">
<div class="input-group small">
<div v-if="isEditing" v-on-clickaway="onClickAway">
<div class="mb-2 w-full flex items-center">
<input
ref="inputfield"
v-model="editedValue"
:type="inputType"
class="input-group-field"
class="!h-8 ltr:rounded-r-none rtl:rounded-l-none !mb-0 !text-sm"
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>
<woot-button
size="small"
icon="checkmark"
class="rounded-l-none rtl:rounded-r-none"
@click="onUpdate"
/>
</div>
</div>
<span
@@ -56,7 +65,7 @@
</div>
<div
v-show="!isEditing"
class="value--view"
class="flex group"
:class="{ 'is-editable': showActions }"
>
<a
@@ -64,35 +73,35 @@
:href="hrefURL"
target="_blank"
rel="noopener noreferrer"
class="value inline-block rounded-sm mb-0 break-all py-0.5 px-1"
class="group-hover:bg-slate-50 group-hover:dark:bg-slate-700 inline-block rounded-sm mb-0 break-all py-0.5 px-1"
>
{{ urlValue }}
</a>
<p
v-else
class="value inline-block rounded-sm mb-0 break-all py-0.5 px-1"
class="group-hover:bg-slate-50 group-hover:dark:bg-slate-700 inline-block rounded-sm mb-0 break-all py-0.5 px-1"
>
{{ displayValue || '---' }}
</p>
<div class="flex max-w-[2rem] gap-1 ml-1 rtl:mr-1 rtl:ml-0">
<woot-button
v-if="showActions"
v-if="showCopyAndDeleteButton"
v-tooltip="$t('CUSTOM_ATTRIBUTES.ACTIONS.COPY')"
variant="link"
size="small"
color-scheme="secondary"
icon="clipboard"
class-names="edit-button"
class-names="hidden group-hover:flex !w-6 flex-shrink-0"
@click="onCopy"
/>
<woot-button
v-if="showActions"
v-if="showEditButton"
v-tooltip.right="$t('CUSTOM_ATTRIBUTES.ACTIONS.EDIT')"
variant="link"
size="small"
color-scheme="secondary"
icon="edit"
class-names="edit-button"
class-names="hidden group-hover:flex !w-6 flex-shrink-0"
@click="onEdit"
/>
</div>
@@ -121,6 +130,7 @@
</template>
<script>
import { mixin as clickaway } from 'vue-clickaway';
import { format, parseISO } from 'date-fns';
import { required, url } from 'vuelidate/lib/validators';
import { BUS_EVENTS } from 'shared/constants/busEvents';
@@ -133,7 +143,7 @@ export default {
components: {
MultiselectDropdown,
},
mixins: [customAttributeMixin],
mixins: [customAttributeMixin, clickaway],
props: {
label: { type: String, required: true },
values: { type: Array, default: () => [] },
@@ -155,11 +165,18 @@ export default {
editedValue: null,
};
},
computed: {
showCopyAndDeleteButton() {
return this.value && this.showActions;
},
showEditButton() {
return !this.value && this.showActions;
},
displayValue() {
if (this.isAttributeTypeDate) {
return new Date(this.value || new Date()).toLocaleDateString();
return this.value
? new Date(this.value || new Date()).toLocaleDateString()
: '';
}
if (this.isAttributeTypeCheckbox) {
return this.value === 'false' ? false : this.value;
@@ -225,6 +242,10 @@ export default {
this.isEditing = false;
this.editedValue = this.formattedValue;
},
contactId() {
// Fix to solve validation not resetting when contactId changes in contact page
this.$v.$reset();
},
},
validations() {
@@ -263,6 +284,10 @@ export default {
this.$refs.inputfield.focus();
}
},
onClickAway() {
this.$v.$reset();
this.isEditing = false;
},
onEdit() {
this.isEditing = true;
this.$nextTick(() => {
@@ -289,6 +314,7 @@ export default {
},
onDelete() {
this.isEditing = false;
this.$v.$reset();
this.$emit('delete', this.attributeKey);
},
onCopy() {
@@ -299,36 +325,6 @@ export default {
</script>
<style lang="scss" scoped>
.checkbox-wrap {
@apply flex items-center;
}
.checkbox {
@apply my-0 mr-2 ml-0;
}
.attribute-name {
@apply w-full text-slate-800 dark:text-slate-100;
&.error {
@apply text-red-400 dark:text-red-500;
}
}
.edit-button {
@apply hidden;
}
.value--view {
@apply flex;
&.is-editable:hover {
.value {
@apply bg-slate-50 dark:bg-slate-700 mb-0;
}
.edit-button {
@apply block;
}
}
}
::v-deep {
.selector-wrap {
@apply m-0 top-1;
+42 -28
View File
@@ -7,7 +7,13 @@
@mousedown="handleMouseDown"
>
<div
:class="modalContainerClassName"
:class="{
'modal-container rtl:text-right shadow-md max-h-full overflow-auto relative bg-white dark:bg-slate-800 skip-context-menu': true,
'rounded-xl w-[37.5rem]': !fullWidth,
'items-center rounded-none flex h-full justify-center w-full':
fullWidth,
[size]: true,
}"
@mouse.stop
@mousedown="event => event.stopPropagation()"
>
@@ -16,7 +22,7 @@
color-scheme="secondary"
icon="dismiss"
variant="clear"
class="absolute ltr:right-2 rtl:left-2 top-2 z-10"
class="absolute z-10 ltr:right-2 rtl:left-2 top-2"
@click="close"
/>
<slot />
@@ -60,15 +66,6 @@ export default {
};
},
computed: {
modalContainerClassName() {
let className =
'modal-container bg-white dark:bg-slate-800 skip-context-menu';
if (this.fullWidth) {
return `${className} modal-container--full-width`;
}
return `${className} ${this.size}`;
},
modalClassName() {
const modalClassNameMap = {
centered: '',
@@ -109,26 +106,43 @@ export default {
};
</script>
<style scoped lang="scss">
.modal-container--full-width {
align-items: center;
border-radius: 0;
display: flex;
height: 100%;
justify-content: center;
width: 100%;
}
.modal-mask.right-aligned {
justify-content: flex-end;
<style lang="scss">
.modal-mask {
@apply flex items-center justify-center bg-modal-backdrop-light dark:bg-modal-backdrop-dark z-[9990] h-full left-0 fixed top-0 w-full;
.modal-container {
border-radius: 0;
height: 100%;
width: 30rem;
&.medium {
@apply max-w-[80%] w-[56.25rem];
}
// .content-box {
// @apply h-auto p-0;
// }
.content {
@apply p-8;
}
form,
.modal-content {
@apply pt-4 pb-8 px-8 self-center;
a {
@apply p-4;
}
}
}
}
.modal-big {
width: 60%;
@apply w-full;
}
.modal-mask.right-aligned {
@apply justify-end;
.modal-container {
@apply rounded-none h-full w-[30rem];
}
}
.modal-enter,
.modal-leave {
@apply opacity-0;
}
.modal-enter .modal-container,
.modal-leave .modal-container {
transform: scale(1.1);
}
</style>
@@ -1,21 +1,21 @@
<template>
<div class="flex flex-col items-start pt-8 px-8 pb-0">
<div class="flex flex-col items-start px-8 pt-8 pb-0">
<img v-if="headerImage" :src="headerImage" alt="No image" />
<h2
ref="modalHeaderTitle"
class="text-slate-800 text-lg dark:text-slate-100"
class="text-base font-semibold leading-6 text-slate-800 dark:text-slate-50"
>
{{ headerTitle }}
</h2>
<p
v-if="headerContent"
ref="modalHeaderContent"
class="w-full break-words text-slate-600 mt-2 text-sm dark:text-slate-300"
class="w-full mt-2 text-sm leading-5 break-words text-slate-600 dark:text-slate-300"
>
{{ headerContent }}
<span
v-if="headerContentValue"
class="font-semibold text-sm text-slate-600 dark:text-slate-300"
class="text-sm font-semibold text-slate-600 dark:text-slate-300"
>
{{ headerContentValue }}
</span>
@@ -1,19 +1,21 @@
<template>
<div
class="ml-0 mr-0 flex pt-0 pr-4 pb-4 pl-0"
class="ml-0 mr-0 flex py-8 w-full xl:w-3/4 flex-col xl:flex-row"
:class="{
'pt-4 border-b border-solid border-slate-50 dark:border-slate-700/30':
'border-b border-solid border-slate-50 dark:border-slate-700/30':
showBorder,
}"
>
<div class="w-[30%] min-w-0 max-w-[30%] pr-12">
<div class="w-full xl:w-1/4 min-w-0 xl:max-w-[30%] pr-12">
<p
v-if="title"
class="text-base text-woot-500 dark:text-woot-500 mb-0 font-medium"
>
{{ title }}
</p>
<p class="text-sm mb-2">
<p
class="text-sm mb-2 text-slate-700 dark:text-slate-300 leading-5 tracking-normal mt-2"
>
<slot v-if="subTitle" name="subTitle">
{{ subTitle }}
</slot>
@@ -23,7 +25,7 @@
{{ note }}
</p>
</div>
<div class="w-[50%] min-w-0 max-w-[50%]">
<div class="w-full xl:w-1/2 min-w-0 xl:max-w-[50%]">
<slot />
</div>
</div>
@@ -1,11 +1,17 @@
<template>
<div>
<div class="ui-snackbar">
<div class="ui-snackbar-text">
<div
class="shadow-sm bg-slate-800 dark:bg-slate-700 rounded-[4px] items-center gap-3 inline-flex mb-2 max-w-[25rem] min-h-[1.875rem] min-w-[15rem] px-6 py-3 text-left"
>
<div class="text-white dark:text-white text-sm font-medium">
{{ message }}
</div>
<div v-if="action" class="ui-snackbar-action">
<router-link v-if="action.type == 'link'" :to="action.to">
<div v-if="action">
<router-link
v-if="action.type == 'link'"
:to="action.to"
class="text-woot-500 dark:text-woot-500 cursor-pointer font-medium hover:text-woot-600 dark:hover:text-woot-600 select-none"
>
{{ action.message }}
</router-link>
</div>
@@ -1,5 +1,9 @@
<template>
<transition-group name="toast-fade" tag="div" class="ui-snackbar-container">
<transition-group
name="toast-fade"
tag="div"
class="left-0 my-0 mx-auto max-w-[25rem] overflow-hidden absolute right-0 text-center top-4 z-[9999]"
>
<woot-snackbar
v-for="snackMessage in snackMessages"
:key="snackMessage.key"
@@ -48,7 +48,7 @@
v-on-clickaway="closeDropdown"
class="dropdown-pane dropdown-pane--open"
>
<woot-dropdown-menu>
<woot-dropdown-menu class="mb-0">
<woot-dropdown-item v-if="!isPending">
<woot-button
variant="clear"
@@ -91,12 +91,7 @@ import { mapGetters } from 'vuex';
import { mixin as clickaway } from 'vue-clickaway';
import alertMixin from 'shared/mixins/alertMixin';
import CustomSnoozeModal from 'dashboard/components/CustomSnoozeModal.vue';
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
import {
hasPressedAltAndEKey,
hasPressedCommandPlusAltAndEKey,
hasPressedAltAndMKey,
} from 'shared/helpers/KeyboardHelpers';
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
import { findSnoozeTime } from 'dashboard/helper/snoozeHelpers';
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
@@ -114,7 +109,7 @@ export default {
WootDropdownMenu,
CustomSnoozeModal,
},
mixins: [clickaway, alertMixin, eventListenerMixins],
mixins: [clickaway, alertMixin, keyboardEventListenerMixins],
props: { conversationId: { type: [String, Number], required: true } },
data() {
return {
@@ -159,37 +154,52 @@ export default {
bus.$off(CMD_RESOLVE_CONVERSATION, this.onCmdResolveConversation);
},
methods: {
async handleKeyEvents(e) {
getKeyboardEvents() {
return {
'Alt+KeyM': {
action: () => this.$refs.arrowDownButton?.$el.click(),
allowOnFocusedInput: true,
},
'Alt+KeyE': this.resolveOrToast,
'$mod+Alt+KeyE': async event => {
const { all, activeIndex, lastIndex } = this.getConversationParams();
await this.resolveOrToast();
if (activeIndex < lastIndex) {
all[activeIndex + 1].click();
} else if (all.length > 1) {
all[0].click();
document.querySelector('.conversations-list').scrollTop = 0;
}
event.preventDefault();
},
};
},
getConversationParams() {
const allConversations = document.querySelectorAll(
'.conversations-list .conversation'
);
if (hasPressedAltAndMKey(e)) {
if (this.$refs.arrowDownButton) {
this.$refs.arrowDownButton.$el.click();
}
}
if (hasPressedAltAndEKey(e)) {
const activeConversation = document.querySelector(
'div.conversations-list div.conversation.active'
);
const activeConversationIndex = [...allConversations].indexOf(
activeConversation
);
const lastConversationIndex = allConversations.length - 1;
try {
await this.toggleStatus(wootConstants.STATUS_TYPE.RESOLVED);
} catch (error) {
// error
}
if (hasPressedCommandPlusAltAndEKey(e)) {
if (activeConversationIndex < lastConversationIndex) {
allConversations[activeConversationIndex + 1].click();
} else if (allConversations.length > 1) {
allConversations[0].click();
document.querySelector('.conversations-list').scrollTop = 0;
}
e.preventDefault();
}
const activeConversation = document.querySelector(
'div.conversations-list div.conversation.active'
);
const activeConversationIndex = [...allConversations].indexOf(
activeConversation
);
const lastConversationIndex = allConversations.length - 1;
return {
all: allConversations,
activeIndex: activeConversationIndex,
lastIndex: lastConversationIndex,
};
},
async resolveOrToast() {
try {
await this.toggleStatus(wootConstants.STATUS_TYPE.RESOLVED);
} catch (error) {
// error
}
},
onCmdSnoozeConversation(snoozeType) {
@@ -18,7 +18,6 @@ import Label from './ui/Label';
import LoadingState from './widgets/LoadingState';
import Modal from './Modal';
import ModalHeader from './ModalHeader';
import ReportStatsCard from './widgets/ReportStatsCard';
import SidemenuIcon from './SidemenuIcon';
import Spinner from 'shared/components/Spinner';
import SubmitButton from './buttons/FormSubmitButton';
@@ -46,7 +45,6 @@ const WootUIKit = {
LoadingState,
Modal,
ModalHeader,
ReportStatsCard,
SidemenuIcon,
Spinner,
SubmitButton,

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