Compare commits

...
Author SHA1 Message Date
Sivin VargheseandGitHub 72a18a98bf Merge branch 'develop' into feat/CW-2065 2023-07-05 00:24:39 +05:30
Tejaswini ChileandGitHub 71837bedf9 feat: Ability to customise the email sender name [CW-1629] (#7345) 2023-07-04 20:46:01 +05:30
3c4514c9f7 chore: Update translations (#7415)
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2023-07-04 14:23:56 +05:30
Nithin David ThomasandGitHub 4c7c1cd78a chore: Change order of resolve action dropdown buttons (#7456) 2023-07-03 19:39:32 -07:00
985b3f3bfc chore: Reset the base font-size to 16px instead of 10px (#7455)
Co-authored-by: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com>
2023-07-04 03:23:00 +05:30
Clairton Rodrigo HeinzenandGitHub 986abde531 fix: Cannot use 'in' operator to search for 'last_activity_at' (#7447) 2023-07-03 14:12:47 -07:00
Sivin VargheseandGitHub cbae8358be Merge branch 'develop' into feat/CW-2065 2023-07-03 12:00:53 +05:30
b57063a8b8 feat: Support dark mode in login pages (#7420)
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2023-07-01 07:49:52 +05:30
iamsivin 3b4d03d155 chore: Review fixes 2023-06-30 13:27:53 +05:30
iamsivin c19b34eaaa feat: Show counts for custom views in sidebar 2023-06-30 12:47:14 +05:30
Pranav Raj SandGitHub 022f4f899f Revert "feat: Support Azure single-tenant application using the Graph… (#7436) 2023-06-29 16:50:18 -07:00
Sivin VargheseandGitHub 191b8a64fe chore: Remove URLs from translations (#7414) 2023-06-29 15:09:02 +05:30
ea78020f2f fix: Translation issues (#7416)
Co-authored-by: Hricha Shandily <103104754+Hricha-Shandily@users.noreply.github.com>
2023-06-29 14:41:23 +05:30
Muhsin KelothandGitHub dc1a41b136 fix: Set date hours to 9 in snooze time (#7426) 2023-06-29 14:38:03 +05:30
Shivam MishraandGitHub 28e7a5d228 fix: CC conditions when the last email is from someone else (#7010) 2023-06-28 20:28:44 -07:00
40830046e8 feat: Audit Logs for Account User Changes (#7405)
- Audit log for user invitations: https://linear.app/chatwoot/issue/CW-1768/invited-a-user-to-the-account
- Audit log for change role: https://linear.app/chatwoot/issue/CW-1767/name-or-email-changed-the-role-of-the-user-email-to-agent-or-admin
- Audit log for status change: https://linear.app/chatwoot/issue/CW-1766/availability-status-as-events-for-audit-logs


Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
2023-06-28 22:42:06 +05:30
Tejaswini ChileandGitHub d05c953eef feat: Support Azure single-tenant application using the Graph API (#6728) (#6878) 2023-06-28 08:13:08 +05:30
4a770fdea7 feat: Add Plus Jakarta Font to dashboard (#7421)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2023-06-27 17:39:37 -07:00
Tejaswini ChileandGitHub 239b1e2bf1 feat: Include email in the searched result (#7397) 2023-06-27 21:05:36 +05:30
f963e00731 fix: capture user and ip details on Inbox delete (#7395)
Fixes: https://linear.app/chatwoot/issue/CW-1772/ip-address-and-user-details-are-missing-in-some-of-the-logs

Co-authored-by: Sojan <sojan@pepalo.com>
2023-06-27 19:49:24 +05:30
TOMMYandGitHub 155a5b9947 fix: Remove free space (#7411) 2023-06-27 19:28:38 +05:30
TOMMYandGitHub 26a75b250d Merge pull request from GHSA-2472-ggjh-43h2 2023-06-27 17:22:54 +05:30
Pranav Raj SandGitHub 20b84ee6a1 chore: Fix darkMode styles in widget (#7408) 2023-06-26 14:43:22 -07:00
Pranav Raj SandGitHub f1f14312fe chore: Upgrade Tailwind CSS to 3.3.2 (#7380) 2023-06-26 11:27:16 -07:00
Tejaswini ChileandGitHub 212d7caab1 fix: log User is authenticated but not connected (#7394) 2023-06-26 16:07:40 +05:30
9cda16d0e8 fix: Remove overflow string in label delete confirm button (#7386)
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2023-06-26 13:26:47 +05:30
Sivin VargheseandGitHub a8acbb7427 fix: Issue with i18n in update banner (#7392) 2023-06-26 11:32:40 +05:30
1176e5eb8a feat: Custom phone input in pre-chat form (#7275)
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2023-06-26 10:26:06 +05:30
996325f35b feat: multiple UX improvements to labels (#7358)
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com>
2023-06-25 18:49:49 +05:30
Pranav Raj SandGitHub 4f8ce7b597 chore: Add support for dark mode logo (#7378) 2023-06-23 12:06:01 -07:00
Sojan JoseandGitHub f8e631a00c chore: Update Crowdin Config (#7375)
- add pull request titles
- skip ci for crowdin commits
2023-06-23 23:02:55 +05:30
54bf33083e chore: Update translations (#7372)
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2023-06-23 16:01:14 +05:30
533f9f7fe4 fix: Email collect overriding existing contact name (#7355)
Currently, if Enable email collect box is enabled, and the user starting the chat already has an account with the same email, it will override the current account with {name} from the email name@email.com , this fixes it.

Co-authored-by: Sojan <sojan@pepalo.com>
2023-06-22 19:09:12 +05:30
82e5fc413e fix: Label Duplication in Bulk Actions (#7341)
When looping the conversations in bulk action to assign them to new labels, the existing labels in other conversations were also getting duplicated across all conversations. This PR fixes the issue. 

> In the previous implementation, new_labels is appended to the existing labels using the << operator. This operator modifies the original array instead of creating a new one, causing unwanted side effects. More specifically, new_labels is a reference to the original argument array of the method.

Co-authored-by: Sojan <sojan@pepalo.com>
Co-authored-by: Peter Salib <74493166+Peteraymansalib@users.noreply.github.com>
2023-06-22 18:36:58 +05:30
Pranav Raj SandGitHub 93daaea19b feat: Add a sort option for conversations waiting for a reply from an agent (#7364) 2023-06-21 13:20:39 -07:00
e6a49b5800 feat: Shows Youtube and Vimeo links as embeds [cw-1393] (#7330)
This change will render the youbtube, vimeo and .mp4 urls as embedded in the article page in the help centre.

Fixes: https://linear.app/chatwoot/issue/CW-1393/help-center-support-video-upload-in-articles

Co-authored-by: Sojan <sojan@pepalo.com>
2023-06-21 20:28:28 +05:30
Sojan JoseandGitHub 595e6e79f0 chore: Add sla policy association to conversation (#7360)
Adds the sla policy association to the conversation

Fixes: https://linear.app/chatwoot/issue/CW-1615/applying-an-sla-to-the-conversation
2023-06-21 14:48:50 +05:30
Sivin VargheseandGitHub 93d8157a55 feat: Adds Lithuanian language selectable as site language (#7354) 2023-06-20 17:29:05 +05:30
Vishnu NarayananandGitHub 6bea2cbc4a feat: add audit trail for macros (#7352) 2023-06-20 13:04:59 +05:30
Tejaswini ChileandGitHub afb7e67795 fix: empty account variable for custom_attrbute_definition running in background job (#7351) 2023-06-20 12:15:36 +05:30
bc813b71dd feat: refresh the audit page on navigation without manual reload (#7292)
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
2023-06-20 09:36:16 +05:30
9ac4a1eb30 refactor: multiple fixes to contact header [CW-2081] (#7331)
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2023-06-19 21:58:33 +05:30
Muhsin KelothandGitHub 9bb455ec2d feat: Ability to choose the custom time for snooze conversation (#7257) 2023-06-19 18:23:35 +05:30
c410fe333a chore: Update translations (#7347)
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2023-06-19 17:27:43 +05:30
Jordan BroughandGitHub 5e807c5875 chore: Remove unneeded include EnsureCurrentAccountHelper (#7339)
This class inherits from Api::V1::Accounts::BaseController, which already includes EnsureCurrentAccountHelper and also sets up before_action :current_account.
2023-06-19 17:18:07 +05:30
Muhsin KelothandGitHub 9bed57c7d1 feat: Add more snooze options (#7344) 2023-06-19 17:05:16 +05:30
Sojan JoseandGitHub 5c5381c0a9 chore(ci): Ensure PR titles are in semantic format (#7346) 2023-06-19 16:48:45 +05:30
Jordan BroughandGitHub 138630b4ae chore: Update method to access "secret_key_base" (#7337)
This will look at environment variables and Rails Credentials, and Rails Secrets.
See documentation here: https://api.rubyonrails.org/v7.0.5/classes/Rails/Application.html#method-i-secret_key_base
2023-06-19 16:11:19 +05:30
Tejaswini ChileandGitHub 9d0de04f7c fix: set custom filter count in redis (#7164) 2023-06-19 16:10:03 +05:30
Muhsin KelothandGitHub 2c3337b117 feat: Move snooze actions to command bar (#7343) 2023-06-19 13:57:59 +05:30
Shivam MishraandGitHub 237358af24 refactor: use as_json for formatting the string (#7332) 2023-06-19 13:33:48 +05:30
Sivin VargheseandGitHub 35dfff0a5b feat: Attachment view improvements (#7314) 2023-06-19 11:16:28 +05:30
86b2896333 feat: Show alerts when the limit is reached in accounts (#7323)
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
2023-06-15 20:47:11 -07:00
Shivam MishraandGitHub e8a27bea4b feat: Add APIs for limit check in accounts (#7242) 2023-06-15 20:11:40 -07:00
Sojan 0d465362ac Merge branch 'release/2.18.0' into develop 2023-06-15 18:45:01 +05:30
Sojan e383e9259b Bump version to 2.18.0 2023-06-15 18:44:13 +05:30
Muhsin KelothandGitHub 2c34dca347 fix: NS_ERROR_STORAGE_BUSY warnings in firefox (#7319)
Fixes: https://linear.app/chatwoot/issue/CW-2075/ns-error-storage-busy-no-error-message
2023-06-15 17:53:26 +05:30
Pranav Raj SandGitHub 69b9123963 fix: Use available_name only (#7312)
We have display_name and the actual name for an agent. display_name is used in all public facing parts. The https://github.com/chatwoot/chatwoot/pull/7232/files PR missed this and used actual name instead.

This PR fixes it.
2023-06-15 17:52:53 +05:30
Shivam MishraandGitHub cac9fe1880 Revert "feat(perf): update query to reduce N+1 impact [CW-1926] (#7228)" (#7271)
This reverts commit f28533bc47.
2023-06-15 16:23:56 +05:30
Nithin David ThomasandGitHub 06ac7c829b fix: Fixes wrong variable usage in transcript file (#7316) 2023-06-15 16:00:21 +05:30
Shivam MishraandGitHub 9985f1c143 refactor: remove redundant taggable declaration (#7310) 2023-06-15 12:57:56 +05:30
Tejaswini ChileandGitHub 1ee6a8fe90 chore: Add validation for processed content field (#7306)
* logging the messages id for message validation exception

* Update the processed_message_content validation over length

* codeclimate

* specs failing for contacts
2023-06-14 18:28:42 +05:30
Muhsin KelothandGitHub 6eb4fa41ff feat: Add sender details and message create date in the attachment API (#7308) 2023-06-14 17:29:13 +05:30
8b91f0ffac fix: Gemfile.lock to reduce vulnerabilities (#7280)
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-RUBY-RACK-1061917

Co-authored-by: Sojan Jose <sojan@pepalo.com>
2023-06-14 15:40:24 +05:30
d2aa19579e feat: Adds support for superscript in help center articles (#7279)
- Adds support for superscript when rendering article markdown
- Chatwoot Markdown Render to render markdown everywhere

Co-authored-by: Sojan <sojan@pepalo.com>
2023-06-14 15:39:00 +05:30
Shivam MishraandGitHub 2e79a32db7 fix: calculation for resolution count (#7293)
* fix: calculation for resolution count

* test: resolution count bug fix

- ensure enqueued jobs are run
- fix the dates check, conversations resolved today should show up in today

* feat: ensure conversations are resolved

* test: do not count extra events if the conversation is not resolved currently

* fix: typo
2023-06-14 13:50:10 +05:30
Shivam MishraandGitHub a86e236d19 feat: update cache headers for cache_keys (#7283)
Update the cache headers for cache_keys to max-age=10, private, stale-while-revalidate=300

1. The cache will be fresh for 10 seconds (max-age=10). During this time, the browser will use the cached version without checking with the server.
2. After the initial 10 seconds, the browser can continue to serve the stale cache for up to 5 minutes (stale-while-revalidate=300). During this period, it will also try to revalidate and update the cache in the background.
3. After 310 seconds in total (10 seconds fresh, 300 seconds stale), if the browser has not been able to revalidate the cache, it will attempt to fetch the fresh resource directly from the server for subsequent requests, causing potential latency equivalent to a network request.

This means that the data will be directly revalidated only every 5 mins. Other times, it will stay fresh for 10 seconds and revalidate in the background. In most cases, we won't have to rely on a cache validation check because there is a WebSocket event for revalidation, so we know if something changes.

Right now the stale-while-revalidate is 5 minutes, we can then move it to 15 minutes.

> The stale-while-revalidate header is not supported in Safari, for Safari the cache keys will only stay in memory for 10 seconds before being marked stale
2023-06-14 13:21:51 +05:30
c755978352 feat: update banner design [CW-1491] (#7296)
* refactor: banner styling

* refactor: update button style for reply assign banner

* feat: remove update-banner classes

---------

Co-authored-by: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com>
2023-06-14 12:56:42 +05:30
Tejaswini ChileandGitHub 23ca6d56f9 feat: Contact Exports (#7258) 2023-06-13 09:18:43 +05:30
Shivam MishraandGitHub 429ec7194f feat: add index to reporting events [CW-1960] (#7294)
* fix: annotation

* feat: add better index for reporting_events
2023-06-12 18:23:42 +05:30
Tejaswini ChileandGitHub c99d9f9557 fix: search for nil in-reply-to messages (#7286) 2023-06-12 16:01:56 +05:30
Vishnu NarayananandGitHub 9ff5978d6e feat: Add audit trail events for team (#7285) 2023-06-09 17:30:36 -07:00
Muhsin KelothandGitHub cd28f401ba fix: Custom attribute date is not working in some time zone (#7224) 2023-06-09 16:55:03 -07:00
Tejaswini ChileandGitHub 879a244f93 fix: Automations condition based quoted text (#7272) 2023-06-09 17:00:05 +05:30
Tejaswini ChileandGitHub f64f2138db fix: Update mail check for html_part (#7273) 2023-06-09 16:08:40 +05:30
Sojan JoseandGitHub 48f2e58e59 feat: Ability to update avatars from super admin (#7264)
- Ability to update user avatars from super admin
- Ability to update bot avatars from super admin

fixes: #7060
2023-06-09 15:32:24 +05:30
c715e396f0 feat: added input_select type message support for whatsapp (#6886)
- Added input_select message type support for Whatsapp Cloud API and Whatsapp 360dialog.

Co-authored-by: Sojan Jose <sojan@pepalo.com>
2023-06-09 15:20:45 +05:30
Pranav Raj SandGitHub c8fac08e0b fix: Cannot read properties of undefined (reading 'some') (#7278)
Fixes https://linear.app/chatwoot/issue/CW-2041/typeerror-cannot-read-properties-of-undefined-reading-some
Fixes https://linear.app/chatwoot/issue/CW-2042/typeerror-cannot-read-properties-of-undefined-reading-filter
2023-06-08 20:12:14 -07:00
7fd220c177 feat: Adds the ability to edit saved segments (#7254)
* feat: Ability to edit saved filters

* chore: Adds edit contact segment

* chore: Minor fixes

* fix: code climate

* chore: Minor fixes

* chore: Adds ability to custom view name

* chore: Minor fixes

* chore: Adds spec for helper

* chore: Revert contact filter to split to new PR

* Delete editSegmentMixin.js

* chore: Revert fixes

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

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

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

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

* chore: Moved from mixin to helper for reusing in segments

* Delete editFolderMixin.js

* chore: Fix specs and added new specs

* chore: review comment fixes

* chore: Minor fixes

* fix: Not resetting applied filter

* feat: Adds the ability to edit saved segments

* feat: Adds specs for API part

---------

Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
2023-06-08 21:25:51 +05:30
Pranav Raj SandGitHub b3b76d00ff fix: Do not use the default value if the out of office message is empty (#7268) 2023-06-08 08:22:10 -07:00
Shivam MishraandGitHub 2f2ae88cba fix: unattended count mismatch in report and list (#7263) 2023-06-08 17:58:13 +05:30
Tejaswini ChileandGitHub 2b7989f52b fix: reply mailer with sender name (#7232) 2023-06-08 17:07:08 +05:30
d7314079c9 feat: Ability to edit saved folders (#7236)
* feat: Ability to edit saved filters

Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
2023-06-08 14:58:57 +05:30
1017903ee1 fix: Adds domain option to user cookies set by SDK [CW-352] (#7070)
* fix: Adds domain option to user cookies set by SDK

* Adds domain to init event from chatwootSettings variable

* Testing multiple domains on heroku

* Updates with sdk from staging

* Removes sdk init code

* Testing why cookie is not getting set

* Cleans up testing code

* Refactors code to fix codeclimate issues

* Update app/javascript/sdk/cookieHelpers.js

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

* Adds test cases for setCookieWithDomain

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
2023-06-08 14:27:49 +05:30
Tejaswini ChileandGitHub d25e7fd54e fix: add email logging for source-id email-id and message-id (#7262)
* fix: mail logger for source-id email-id and message-id

* added mail provided to the tracking

* change the logger verb from info to error

* app/jobs/inboxes/fetch_imap_emails_job.rb

* codeclimate fix

* remove extra filter

* fixes: specs
2023-06-07 18:24:37 +05:30
Sojan JoseandGitHub 7e89e3a34a chore: Ensure template_sync timestamp is updated (#7265)
Fix for the cases where there are multiple channels with invalid provider config, which results in the templates sync scheduler failing to schedule jobs for valid channels.

fixes: https://linear.app/chatwoot/issue/CW-2032/bug-whatsapp-template-sync-failing-in-cloud
2023-06-07 17:18:24 +05:30
Pranav Raj SandGitHub 39eaed7766 fix: Cannot read properties of undefined (reading 'always_play_audio_alert') (#7260) 2023-06-06 15:16:25 -07:00
Pranav Raj SandGitHub b1a4907ede fix: around_action not yielding is params are present (#7259) 2023-06-06 14:55:08 -07:00
a01d81a7e1 feat: Update display text for auditlog entries (#7226)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2023-06-05 12:51:45 -07:00
Sojan JoseandGitHub 117d5301b4 chore: API documentation updates for CSAT surveys (#7255)
- Expose conversation uuid in APIs
- swagger documentation for cast survey update via public/message/update endpoints
- swagger documentation for survey/responses/conversation_uuid endpoint

Fixes: #6328
2023-06-05 21:14:01 +05:30
Tejaswini ChileandGitHub aae6081d73 fix: check content disposition, for inline messages in mail (#7231) 2023-06-05 20:28:32 +05:30
Sojan JoseandGitHub d03924b846 feat: Enable sending template messages in webhooks (#7252)
fixes: https://github.com/chatwoot/chatwoot/issues/5291
2023-06-05 20:27:28 +05:30
b333d0c986 feat: Attachments view (#7156)
* feat: Attachments view with key shortcuts and dynamically updates when user delete or sent new attachments

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2023-06-05 19:21:47 +05:30
Shivam MishraandGitHub 9f3d155822 feat: Add size limit action (#7253)
* chore: update browserlist

* feat: add size limit action

* feat: set limits

* feat: add size limit in CI

* chore: remove railsenv

* chore: update limits
2023-06-05 18:56:15 +05:30
TOMMYandGitHub 9203b097d2 chore: Show chatwoot_edition in Instance details (#7200)
- Displays the chatwoot edition in the instance details
2023-06-05 17:32:21 +05:30
Marcel StörandGitHub 12cb97db5f fix: Quote database env variables (#7216)
Database credentials, and passwords, in particular, can easily contain characters that are reserved in YAML and must be quoted. 
Example: a case with a password starting with ']'.
2023-06-05 14:19:35 +05:30
Tejaswini ChileandGitHub 09971fd613 fix: Wrap references string into array (#7243) 2023-06-03 07:33:36 +05:30
Pranav Raj SandGitHub ca9d108741 fix: Update documentation of filter APIs (#7245) 2023-06-02 15:36:41 -07:00
Shivam MishraandGitHub f28533bc47 feat(perf): update query to reduce N+1 impact [CW-1926] (#7228)
* feat: update query to reduce N+1 impact

* feat: remove channel eager loading

fixes the error: ActiveRecord::EagerLoadPolymorphicError Exception: Cannot eagerly load the polymorphic association :channel

* test: fix search test

* chore: revert change
2023-06-02 11:11:55 +05:30
Pranav Raj SandGitHub 68f82c11a5 Revert "fix: Updates editor to fix the extra slash issue [cw-1505][cw-1526] (#7091)" (#7238)
This reverts commit 1d0930ef63.
2023-06-01 13:26:02 -07:00
Tejaswini ChileandGitHub abc27fa791 fix: find mail message by references (#7220) 2023-05-31 19:23:29 +05:30
Sojan JoseandGitHub d93a8d05bc chore: Increase character limit for external url fields (#7230)
- Increase the external url field validation to 2048 characters

fixes: https://github.com/chatwoot/chatwoot/issues/7098
2023-05-31 19:17:24 +05:30
Sojan JoseandGitHub 373f5f5b64 chore: Disable coverage reports for .vue files (#7214)
- Disable coverage reporting for .vue files 

fixes: https://linear.app/chatwoot/issue/CW-1331
2023-05-30 19:18:19 +05:30
Sivin VargheseandGitHub ae7df60a75 fix: Special characters not being decoded (#7218) 2023-05-30 18:54:51 +05:30
412d750b6a feat: auditlogs design refactor cw1764 (#7181)
* chore: refactor auditlogs design

* chore: refactor aduit log text

* chore: fix 60% width for activity column

* chore: improve log text formatting

* Apply suggestions from code review

Co-authored-by: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com>

* feat: show agent names if available in auditlogs

* chore: add sign_out

* Apply suggestions from code review

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

* chore: handle custom user actions

---------

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com>
2023-05-30 14:41:29 +05:30
f1a77ba934 feat: allow superadmins to reset cache keys for IndexedDB (#7180)
Allows super admins to reset the cache for an account. This will force the front end to fetch the data again on the next load.

fixes: https://linear.app/chatwoot/issue/CW-1817

Co-authored-by: Sojan <sojan@pepalo.com>
2023-05-29 23:05:17 +05:30
Sojan JoseandGitHub ffc6364690 chore: Improve Whatsapp Templates Sync (#7210)
- update the templates updated at, even if the API request fails ( to prevent jobs from stacking up in case of API failures upstream )
- sequence the job in batches of 25 requests per minutes schedule ( in case API response time is high, also not to send too many requests in a single batch )
- move the sync job re-rerun to 3 hours ( since we are updating the updated at even in case of failures )(prev 15 minutes )

Fixes: https://linear.app/chatwoot/issue/CW-1590
2023-05-29 22:23:22 +05:30
Sojan JoseandGitHub 3a7633b564 feat: API to create HMAC verified conversations (#7209)
Fixes: #6744
2023-05-29 21:57:24 +05:30
amplitudesandGitHub 91d1d44aa9 fix: profile picture distortion (#7203)
By applying pr-1 on a container div instead of directly on the image (which distorts it), it creates the intended padding effect without any image distortion.
2023-05-29 15:00:14 +05:30
Sojan JoseandGitHub afc97faa8b chore: Rotate pubsub token on password change (#7194)
Fixes: https://linear.app/chatwoot/issue/CW-1350/
2023-05-27 13:24:31 +05:30
1d0930ef63 fix: Updates editor to fix the extra slash issue [cw-1505][cw-1526] (#7091)
* fix: Updates editor to fix the extra slash issue

* Updates yarn package

* Updates yarn package for editor schema

---------

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2023-05-26 00:54:37 +05:30
Tejaswini ChileandGitHub 776ea6d39a fix: Help center api doc (#7189) 2023-05-25 18:50:00 +05:30
Muhsin KelothandGitHub 2c85098698 fix: Mentions in assigned/participation notification(#7185) 2023-05-25 18:18:56 +05:30
Sivin VargheseandGitHub 6bd0e074dc feat: Sort agents on availability status (#7174) 2023-05-25 14:49:56 +05:30
Vishnu NarayananandGitHub 123fc73394 feat: add audit trail for sign_in and sign_out (#7158)
* feat: add audit_trail for sign_in event

* chore: ignore unrelated User model columns for auditing

* chore: fix prepend call for webhook/automation rule

* chore: add spec for sign_in event

* chore: refactor sign_in auditlog method to enterprise namespace

* feat: add sign_out audit trail

* feat: review comments
2023-05-25 14:27:30 +05:30
88cef88d80 fix: Instagram story with video not rendering (#7184)
* fix: instagram story with video not rendering

* chore: Minor fixes

---------

Co-authored-by: Tejaswini Chile <tejaswini@chatwoot.com>
2023-05-25 12:20:42 +05:30
Tejaswini ChileandGitHub 8e79cf72e1 fix: issue with slack job (#7179) 2023-05-24 20:22:34 +05:30
Sojan JoseandGitHub bfaca851f1 chore: Throttle conversation transcript endpoints (#7155)
- Throttle conversation transcripts to 20 per hour via rack attack

Fixes: https://linear.app/chatwoot/issue/CW-1630/throttle-transcript-endpoint
2023-05-24 19:56:23 +05:30
Muhsin KelothandGitHub 5618b5ebd3 fix: Enable ogg format for audio recording in API channel (#7178) 2023-05-24 15:05:31 +05:30
Muhsin KelothandGitHub 4807052c37 doc: Swagger for conversation priority API (#7172) 2023-05-23 20:39:15 +05:30
Shivam MishraandGitHub 44b1047b90 feat(perf): contacts query performance (#7175)
* feat: use more indexable where condition

* feat: add index concurrently

* chore: update schema

* refactor: update index name
2023-05-23 17:22:47 +05:30
Shivam MishraandGitHub 9c6c19c3e5 fix: CSAT filter metrics rendering & conversation reports not working [CW-1840, CW-1818] (#7170)
* fix: emoji rendering for CSAT

* feat: add tests for CSAT Metrics

* fix: allow rating in metrics

* refactor: hide satisfaction score & total response chart if rating filter is enabled

* refactor: optional chaining in group by

* fix: spacing using autofill

* test: update csat metrics tests

* test: CSAT metric card
2023-05-23 16:47:04 +05:30
Nithin David ThomasandGitHub 2764338453 fix: Fixes wrong copy being sent to users via email invite [cw-1290] (#7159)
* fix: Fixes invite email copy

* fix: Fixes wrong copy being sent to users via email invite
2023-05-23 09:37:40 +05:30
Shivam MishraandGitHub 4f0fcfcbcd feat: allow Microsoft Clarity from CDN (#7163)
* feat: add microsoft clarity to main app

* feat: mask messages

* fix: spacing
2023-05-23 08:00:01 +05:30
Pranav Raj SandGitHub fdbaf98c3b fix: Update breaking specs (#7169) 2023-05-22 17:01:27 -07:00
03bbd048a7 chore: Add action to set dark mode from the react-native-widget (#7167)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2023-05-22 15:34:09 -07:00
Tejaswini ChileandGitHub d481b9fbcf feat: Limit the number of custom filters per user (#7101) 2023-05-22 18:03:15 +05:30
Vishnu NarayananandGitHub 68dedc37ba chore: update auditlog timestamp format (#7128) 2023-05-22 17:08:50 +05:30
Shivam MishraandGitHub 958c0d7946 feat(perf): improve performance of conversation filter API [CW-1605] (#7120)
* feat(perf): don't use count

* feat(perf): include messages in the conversation query

* feat: include contact inboxes

* refactor: use blank?
2023-05-22 16:03:11 +05:30
Tejaswini ChileandGitHub 9553329092 chore: find instagram_direct_message query (#7157) 2023-05-22 16:01:31 +05:30
Nithin David ThomasandGitHub d01f2063d0 feat: Creates component to show articles search results [CW-1451] (#7126)
* feat: Creates component to show articles search results

* Adds story for the component
2023-05-22 15:15:19 +05:30
Tejaswini ChileandGitHub 590e4e9c1c Fix: prevent IMAPBadResponse exception from sending the authorization mail (#7154) 2023-05-22 13:51:56 +05:30
Tejaswini ChileandGitHub 0e903d2365 chore: Add delay for slack job when message has attachments (#7107) 2023-05-22 13:51:14 +05:30
Shivam MishraandGitHub d6ce1ceeeb fix: CSAT chart showing incorrect data (#7152)
* fix: computation of data for csat chart

* fix: make horizontal chart reactive
2023-05-22 11:01:01 +05:30
Sivin VargheseandGitHub e8f56d0e56 chore: Update greeting message label in settings (#7088) 2023-05-21 21:07:37 -07:00
Pranav Raj SandGitHub b8dae07c0f feat: Add an option to use darkMode only in the widget (#7151) 2023-05-22 09:05:05 +05:30
Sojan JoseandGitHub e3f4be97c0 fix: Reauthorize when channel settings updated (#7134)
Fixes: https://linear.app/chatwoot/issue/CW-1803/bug-inbox-doesnt-update-reauthorisation-required-automattically
2023-05-19 22:04:34 +05:30
Sojan 1f4d096804 Merge branch 'hotfix/2.17.1' into develop 2023-05-19 20:56:16 +05:30
Sivin VargheseandGitHub 1bcc438b21 fix: Problem when using Brazilian Portuguese (#7135)
Fixes:  #7127
2023-05-19 20:55:01 +05:30
Vishnu NarayananandGitHub 8b3d13a920 fix: add libvips to Dockerfile for rails7 (#7133)
Fixes: #7124
Fixes: https://linear.app/chatwoot/issue/CW-1805/investigate-217-upgrade-issues-for-dockerk8s-installations
2023-05-19 17:59:33 +05:30
Sojan dcdd58bae5 Bump version to 2.17.1 2023-05-19 16:00:27 +05:30
Sojan de1fb6ba4e Merge branch 'release/2.17.0'
Publish Chatwoot CE docker images / build (push) Waiting to run
2023-05-16 12:33:47 +05:30
Pranav Raj S 18c5c47836 Merge branch 'hotfix/2.16.1' 2023-05-02 15:53:33 -07:00
1306 changed files with 31798 additions and 9020 deletions
+1 -1
View File
@@ -157,7 +157,7 @@ jobs:
- run:
name: Code Climate Test Coverage
command: |
~/tmp/cc-test-reporter format-coverage -t lcov -o coverage/codeclimate.frontend_$CIRCLE_NODE_INDEX.json buildreports/lcov.info
~/tmp/cc-test-reporter format-coverage -t lcov -o "coverage/codeclimate.frontend_$CIRCLE_NODE_INDEX.json"
- persist_to_workspace:
root: coverage
+3
View File
@@ -173,6 +173,9 @@ ANDROID_SHA256_CERT_FINGERPRINT=AC:73:8E:DE:EB:56:EA:CC:10:87:02:A7:65:37:7B:38:
## LogRocket
# LOG_ROCKET_PROJECT_ID=xxxxx/some-project
# MICROSOFT CLARITY
# MS_CLARITY_TOKEN=xxxxxxxxx
## Scout
## https://scoutapm.com/docs/ruby/configuration
# SCOUT_KEY=YOURKEY
+23
View File
@@ -0,0 +1,23 @@
# ref: https://github.com/amannn/action-semantic-pull-request
# ensure PR title is in semantic format
name: "Lint PR"
on:
pull_request_target:
types:
- opened
- edited
- synchronize
permissions:
pull-requests: read
jobs:
main:
name: Validate PR title
runs-on: ubuntu-latest
steps:
- uses: amannn/action-semantic-pull-request@v5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+38
View File
@@ -0,0 +1,38 @@
name: Run Size Limit Check
on:
pull_request:
branches:
- develop
jobs:
test:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.ref }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
- uses: ruby/setup-ruby@v1
with:
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
- uses: actions/setup-node@v3
with:
node-version: 16
- name: yarn
run: yarn install
- name: Strip enterprise code
run: |
rm -rf enterprise
rm -rf spec/enterprise
- name: Run asset compile
run: bundle exec rake assets:precompile
- name: Size Check
run: yarn run size
+2
View File
@@ -74,3 +74,5 @@ yalc.lock
/yarn-error.log
yarn-debug.log*
.yarn-integrity
/storybook-static
+2 -1
View File
@@ -4,7 +4,7 @@ ruby '3.2.2'
##-- base gems for rails --##
gem 'rack-cors', require: 'rack/cors'
gem 'rails', '~> 7'
gem 'rails', '~> 7.0.5.1'
# Reduces boot times through caching; required in config/boot.rb
gem 'bootsnap', require: false
@@ -75,6 +75,7 @@ gem 'jwt'
gem 'pundit'
# super admin
gem 'administrate'
gem 'administrate-field-active_storage'
##--- gems for pubsub service ---##
# https://karolgalanciak.com/blog/2019/11/30/from-activerecord-callbacks-to-publish-slash-subscribe-pattern-and-event-driven-design/
+78 -73
View File
@@ -33,70 +33,70 @@ GIT
GEM
remote: https://rubygems.org/
specs:
actioncable (7.0.4.3)
actionpack (= 7.0.4.3)
activesupport (= 7.0.4.3)
actioncable (7.0.5.1)
actionpack (= 7.0.5.1)
activesupport (= 7.0.5.1)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
actionmailbox (7.0.4.3)
actionpack (= 7.0.4.3)
activejob (= 7.0.4.3)
activerecord (= 7.0.4.3)
activestorage (= 7.0.4.3)
activesupport (= 7.0.4.3)
actionmailbox (7.0.5.1)
actionpack (= 7.0.5.1)
activejob (= 7.0.5.1)
activerecord (= 7.0.5.1)
activestorage (= 7.0.5.1)
activesupport (= 7.0.5.1)
mail (>= 2.7.1)
net-imap
net-pop
net-smtp
actionmailer (7.0.4.3)
actionpack (= 7.0.4.3)
actionview (= 7.0.4.3)
activejob (= 7.0.4.3)
activesupport (= 7.0.4.3)
actionmailer (7.0.5.1)
actionpack (= 7.0.5.1)
actionview (= 7.0.5.1)
activejob (= 7.0.5.1)
activesupport (= 7.0.5.1)
mail (~> 2.5, >= 2.5.4)
net-imap
net-pop
net-smtp
rails-dom-testing (~> 2.0)
actionpack (7.0.4.3)
actionview (= 7.0.4.3)
activesupport (= 7.0.4.3)
rack (~> 2.0, >= 2.2.0)
actionpack (7.0.5.1)
actionview (= 7.0.5.1)
activesupport (= 7.0.5.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.4.3)
actionpack (= 7.0.4.3)
activerecord (= 7.0.4.3)
activestorage (= 7.0.4.3)
activesupport (= 7.0.4.3)
actiontext (7.0.5.1)
actionpack (= 7.0.5.1)
activerecord (= 7.0.5.1)
activestorage (= 7.0.5.1)
activesupport (= 7.0.5.1)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
actionview (7.0.4.3)
activesupport (= 7.0.4.3)
actionview (7.0.5.1)
activesupport (= 7.0.5.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.4.3)
activesupport (= 7.0.4.3)
activejob (7.0.5.1)
activesupport (= 7.0.5.1)
globalid (>= 0.3.6)
activemodel (7.0.4.3)
activesupport (= 7.0.4.3)
activerecord (7.0.4.3)
activemodel (= 7.0.4.3)
activesupport (= 7.0.4.3)
activemodel (7.0.5.1)
activesupport (= 7.0.5.1)
activerecord (7.0.5.1)
activemodel (= 7.0.5.1)
activesupport (= 7.0.5.1)
activerecord-import (1.4.1)
activerecord (>= 4.2)
activestorage (7.0.4.3)
actionpack (= 7.0.4.3)
activejob (= 7.0.4.3)
activerecord (= 7.0.4.3)
activesupport (= 7.0.4.3)
activestorage (7.0.5.1)
actionpack (= 7.0.5.1)
activejob (= 7.0.5.1)
activerecord (= 7.0.5.1)
activesupport (= 7.0.5.1)
marcel (~> 1.0)
mini_mime (>= 1.1.0)
activesupport (7.0.4.3)
activesupport (7.0.5.1)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
@@ -113,6 +113,9 @@ GEM
kaminari (>= 1.0)
sassc-rails (~> 2.1)
selectize-rails (~> 0.6)
administrate-field-active_storage (0.4.2)
administrate (>= 0.2.2)
rails (>= 7.0)
annotate (3.2.0)
activerecord (>= 3.2, < 8.0)
rake (>= 10.4, < 14.0)
@@ -353,7 +356,7 @@ GEM
mini_mime (>= 1.0.0)
multi_xml (>= 0.5.2)
httpclient (2.8.3)
i18n (1.13.0)
i18n (1.14.1)
concurrent-ruby (~> 1.0)
image_processing (1.12.2)
mini_magick (>= 4.9.5, < 5)
@@ -420,9 +423,9 @@ GEM
activesupport (>= 4)
railties (>= 4)
request_store (~> 1.0)
loofah (2.20.0)
loofah (2.21.3)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
nokogiri (>= 1.12.0)
mail (2.8.1)
mini_mime (>= 0.1.1)
net-imap
@@ -438,7 +441,7 @@ GEM
mini_magick (4.12.0)
mini_mime (1.1.2)
mini_portile2 (2.8.2)
minitest (5.18.0)
minitest (5.18.1)
mock_redis (0.36.0)
ruby2_keywords
msgpack (1.7.0)
@@ -447,7 +450,7 @@ GEM
multipart-post (2.3.0)
net-http-persistent (4.0.2)
connection_pool (~> 2.2)
net-imap (0.3.4)
net-imap (0.3.6)
date
net-protocol
net-pop (0.1.2)
@@ -462,14 +465,14 @@ GEM
sidekiq
newrelic_rpm (8.16.0)
nio4r (2.5.9)
nokogiri (1.14.3)
mini_portile2 (~> 2.8.0)
nokogiri (1.15.2)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
nokogiri (1.14.3-arm64-darwin)
nokogiri (1.15.2-arm64-darwin)
racc (~> 1.4)
nokogiri (1.14.3-x86_64-darwin)
nokogiri (1.15.2-x86_64-darwin)
racc (~> 1.4)
nokogiri (1.14.3-x86_64-linux)
nokogiri (1.15.2-x86_64-linux)
racc (~> 1.4)
oauth (1.1.0)
oauth-tty (~> 1.0, >= 1.0.1)
@@ -522,7 +525,7 @@ GEM
pundit (2.3.0)
activesupport (>= 3.0.0)
raabro (1.4.0)
racc (1.6.2)
racc (1.7.1)
rack (2.2.7)
rack-attack (6.6.1)
rack (>= 1.0, < 3)
@@ -537,28 +540,29 @@ GEM
rack-test (2.1.0)
rack (>= 1.3)
rack-timeout (0.6.3)
rails (7.0.4.3)
actioncable (= 7.0.4.3)
actionmailbox (= 7.0.4.3)
actionmailer (= 7.0.4.3)
actionpack (= 7.0.4.3)
actiontext (= 7.0.4.3)
actionview (= 7.0.4.3)
activejob (= 7.0.4.3)
activemodel (= 7.0.4.3)
activerecord (= 7.0.4.3)
activestorage (= 7.0.4.3)
activesupport (= 7.0.4.3)
rails (7.0.5.1)
actioncable (= 7.0.5.1)
actionmailbox (= 7.0.5.1)
actionmailer (= 7.0.5.1)
actionpack (= 7.0.5.1)
actiontext (= 7.0.5.1)
actionview (= 7.0.5.1)
activejob (= 7.0.5.1)
activemodel (= 7.0.5.1)
activerecord (= 7.0.5.1)
activestorage (= 7.0.5.1)
activesupport (= 7.0.5.1)
bundler (>= 1.15.0)
railties (= 7.0.4.3)
railties (= 7.0.5.1)
rails-dom-testing (2.0.3)
activesupport (>= 4.2.0)
nokogiri (>= 1.6)
rails-html-sanitizer (1.5.0)
loofah (~> 2.19, >= 2.19.1)
railties (7.0.4.3)
actionpack (= 7.0.4.3)
activesupport (= 7.0.4.3)
rails-html-sanitizer (1.6.0)
loofah (~> 2.21)
nokogiri (~> 1.14)
railties (7.0.5.1)
actionpack (= 7.0.5.1)
activesupport (= 7.0.5.1)
method_source
rake (>= 12.2)
thor (~> 1.0)
@@ -722,12 +726,12 @@ GEM
stripe (8.5.0)
telephone_number (1.4.20)
test-prof (1.2.1)
thor (1.2.1)
tilt (2.1.0)
thor (1.2.2)
tilt (2.2.0)
time_diff (0.3.0)
activesupport
i18n
timeout (0.3.2)
timeout (0.4.0)
trailblazer-option (0.1.2)
twilio-ruby (5.77.0)
faraday (>= 0.9, < 3.0)
@@ -784,8 +788,8 @@ GEM
PLATFORMS
arm64-darwin-20
arm64-darwin-22
arm64-darwin-21
arm64-darwin-22
ruby
x86_64-darwin-18
x86_64-darwin-20
@@ -798,6 +802,7 @@ DEPENDENCIES
activerecord-import
acts-as-taggable-on
administrate
administrate-field-active_storage
annotate
attr_extras
audited (~> 5.3)
@@ -870,7 +875,7 @@ DEPENDENCIES
rack-cors
rack-mini-profiler
rack-timeout
rails (~> 7)
rails (~> 7.0.5.1)
redis
redis-namespace
responders
@@ -916,4 +921,4 @@ RUBY VERSION
ruby 3.2.2p185
BUNDLED WITH
2.4.10
2.4.6
+1
View File
@@ -1,5 +1,6 @@
//= link_tree ../images
//= link administrate/application.css
//= link administrate/application.js
//= link administrate-field-active_storage/application.css
//= link dashboardChart.js
//= link secretField.js
+1 -1
View File
@@ -11,7 +11,7 @@ function prepareData(data) {
function getChartOptions() {
var fontFamily =
'Inter,-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif';
'PlusJakarta,-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif';
return {
responsive: true,
legend: { labels: { fontFamily } },
@@ -1,10 +1,10 @@
// Typography
$base-font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
$base-font-family: PlusJakarta, Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif !default;
$heading-font-family: $base-font-family !default;
$base-font-size: 10px !default;
$base-font-size: 16px !default;
$base-line-height: 1.5 !default;
$heading-line-height: 1.2 !default;
@@ -63,9 +63,9 @@ class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder
end
def conversation
@conversation ||= Conversation.where(
@conversation ||= Conversation.where(conversation_params).find_by(
"additional_attributes ->> 'type' = 'instagram_direct_message'"
).find_by(conversation_params) || build_conversation
) || build_conversation
end
def message_content
@@ -96,6 +96,7 @@ class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder
def build_conversation
@contact_inbox ||= contact.contact_inboxes.find_by!(source_id: message_source_id)
Conversation.create!(conversation_params.merge(
contact_inbox_id: @contact_inbox.id,
additional_attributes: { type: 'instagram_direct_message' }
+11 -6
View File
@@ -48,17 +48,22 @@ class Messages::MessageBuilder
def process_emails
return unless @conversation.inbox&.inbox_type == 'Email'
cc_emails = []
cc_emails = @params[:cc_emails].gsub(/\s+/, '').split(',') if @params[:cc_emails].present?
cc_emails = process_email_string(@params[:cc_emails])
bcc_emails = process_email_string(@params[:bcc_emails])
to_emails = process_email_string(@params[:to_emails])
bcc_emails = []
bcc_emails = @params[:bcc_emails].gsub(/\s+/, '').split(',') if @params[:bcc_emails].present?
all_email_addresses = cc_emails + bcc_emails
all_email_addresses = cc_emails + bcc_emails + to_emails
validate_email_addresses(all_email_addresses)
@message.content_attributes[:cc_emails] = cc_emails
@message.content_attributes[:bcc_emails] = bcc_emails
@message.content_attributes[:to_emails] = to_emails
end
def process_email_string(email_string)
return [] if email_string.blank?
email_string.gsub(/\s+/, '').split(',')
end
def validate_email_addresses(all_emails)
+1 -2
View File
@@ -96,10 +96,9 @@ class V2::ReportBuilder
def conversations
@open_conversations = scope.conversations.where(account_id: @account.id).open
first_response_count = @account.reporting_events.where(name: 'first_response', conversation_id: @open_conversations.pluck('id')).count
metric = {
open: @open_conversations.count,
unattended: @open_conversations.count - first_response_count
unattended: @open_conversations.unattended.count
}
metric[:unassigned] = @open_conversations.unassigned.count if params[:type].equal?(:account)
metric
@@ -1,11 +1,13 @@
class Api::V1::Accounts::Contacts::ContactInboxesController < Api::V1::Accounts::Contacts::BaseController
include HmacConcern
before_action :ensure_inbox, only: [:create]
def create
@contact_inbox = ContactInboxBuilder.new(
contact: @contact,
inbox: @inbox,
source_id: params[:source_id]
source_id: params[:source_id],
hmac_verified: hmac_verified?
).perform
end
@@ -42,6 +42,12 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
head :ok
end
def export
column_names = params['column_names']
Account::ContactsExportJob.perform_later(Current.account.id, column_names)
head :ok, message: I18n.t('errors.contacts.export.success')
end
# returns online contacts
def active
contacts = Current.account.contacts.where(id: ::OnlineStatusTracker
@@ -1,5 +1,4 @@
class Api::V1::Accounts::Conversations::BaseController < Api::V1::Accounts::BaseController
include EnsureCurrentAccountHelper
before_action :conversation
private
@@ -1,6 +1,7 @@
class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseController
include Events::Types
include DateRangeHelper
include HmacConcern
before_action :conversation, except: [:index, :meta, :search, :create, :filter]
before_action :inbox, :contact, :contact_inbox, only: [:create]
@@ -104,9 +105,6 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
end
def set_conversation_status
# TODO: temporary fallback for the old bot status in conversation, we will remove after couple of releases
# commenting this out to see if there are any errors, if not we can remove this in subsequent releases
# status = params[:status] == 'bot' ? 'pending' : params[:status]
@conversation.status = params[:status]
@conversation.snoozed_until = parse_date_time(params[:snoozed_until].to_s) if params[:snoozed_until]
end
@@ -152,7 +150,8 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
ContactInboxBuilder.new(
contact: @contact,
inbox: @inbox,
source_id: params[:source_id]
source_id: params[:source_id],
hmac_verified: hmac_verified?
).perform
end
@@ -1,4 +1,5 @@
class Api::V1::Accounts::CustomFiltersController < Api::V1::Accounts::BaseController
before_action :check_authorization
before_action :fetch_custom_filters, except: [:create]
before_action :fetch_custom_filter, only: [:show, :update, :destroy]
DEFAULT_FILTER_TYPE = 'conversation'.freeze
@@ -11,6 +12,7 @@ class Api::V1::Accounts::CustomFiltersController < Api::V1::Accounts::BaseContro
@custom_filter = current_user.custom_filters.create!(
permitted_payload.merge(account_id: Current.account.id)
)
render json: { error: @custom_filter.errors.messages }, status: :unprocessable_entity and return unless @custom_filter.valid?
end
def update
@@ -44,26 +44,7 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
def update
@inbox.update!(permitted_params.except(:channel))
update_inbox_working_hours
channel_attributes = get_channel_attributes(@inbox.channel_type)
# Inbox update doesn't necessarily need channel attributes
return if permitted_params(channel_attributes)[:channel].blank?
if @inbox.inbox_type == 'Email'
begin
validate_email_channel(channel_attributes)
rescue StandardError => e
render json: { message: e }, status: :unprocessable_entity and return
end
@inbox.channel.reauthorized!
end
@inbox.channel.update!(permitted_params(channel_attributes)[:channel])
update_channel_feature_flags
end
def update_inbox_working_hours
@inbox.update_working_hours(params.permit(working_hours: Inbox::OFFISABLE_ATTRS)[:working_hours]) if params[:working_hours]
update_channel if channel_update_required?
end
def agent_bot
@@ -82,7 +63,7 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
end
def destroy
::DeleteObjectJob.perform_later(@inbox) if @inbox.present?
::DeleteObjectJob.perform_later(@inbox, Current.user, request.ip) if @inbox.present?
render status: :ok, json: { message: I18n.t('messages.inbox_deletetion_response') }
end
@@ -103,6 +84,35 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
account_channels_method.create!(permitted_params(channel_type_from_params::EDITABLE_ATTRS)[:channel].except(:type))
end
def update_inbox_working_hours
@inbox.update_working_hours(params.permit(working_hours: Inbox::OFFISABLE_ATTRS)[:working_hours]) if params[:working_hours]
end
def update_channel
channel_attributes = get_channel_attributes(@inbox.channel_type)
return if permitted_params(channel_attributes)[:channel].blank?
validate_and_update_email_channel(channel_attributes) if @inbox.inbox_type == 'Email'
reauthorize_and_update_channel(channel_attributes)
update_channel_feature_flags
end
def channel_update_required?
permitted_params(get_channel_attributes(@inbox.channel_type))[:channel].present?
end
def validate_and_update_email_channel(channel_attributes)
validate_email_channel(channel_attributes)
rescue StandardError => e
render json: { message: e }, status: :unprocessable_entity and return
end
def reauthorize_and_update_channel(channel_attributes)
@inbox.channel.reauthorized! if @inbox.channel.respond_to?(:reauthorized!)
@inbox.channel.update!(permitted_params(channel_attributes)[:channel])
end
def update_channel_feature_flags
return unless @inbox.web_widget?
return unless permitted_params(Channel::WebWidget::EDITABLE_ATTRS)[:channel].key? :selected_feature_flags
@@ -114,7 +124,7 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
def inbox_attributes
[:name, :avatar, :greeting_enabled, :greeting_message, :enable_email_collect, :csat_survey_enabled,
:enable_auto_assignment, :working_hours_enabled, :out_of_office_message, :timezone, :allow_messages_after_resolved,
:lock_to_single_conversation, :portal_id]
:lock_to_single_conversation, :portal_id, :sender_name_type, :business_name]
end
def permitted_params(channel_attributes = [])
@@ -37,6 +37,7 @@ class Api::V1::AccountsController < Api::BaseController
end
def cache_keys
expires_in 10.seconds, public: false, stale_while_revalidate: 5.minutes
render json: { cache_keys: get_cache_keys }, status: :ok
end
@@ -17,7 +17,8 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
@message.update!(submitted_email: contact_email)
ContactIdentifyAction.new(
contact: @contact,
params: { email: contact_email, name: contact_name }
params: { email: contact_email, name: contact_name },
retain_original_contact_name: true
).perform
else
@message.update!(message_update_params[:message])
+5
View File
@@ -0,0 +1,5 @@
module HmacConcern
def hmac_verified?
ActiveModel::Type::Boolean.new.cast(params[:hmac_verified]).present?
end
end
+10 -1
View File
@@ -1,6 +1,7 @@
class DashboardController < ActionController::Base
include SwitchLocale
before_action :set_application_pack
before_action :set_global_config
around_action :switch_locale
before_action :ensure_installation_onboarding, only: [:index]
@@ -14,7 +15,7 @@ class DashboardController < ActionController::Base
def set_global_config
@global_config = GlobalConfig.get(
'LOGO', 'LOGO_THUMBNAIL',
'LOGO', 'LOGO_DARK', 'LOGO_THUMBNAIL',
'INSTALLATION_NAME',
'WIDGET_BRAND_URL', 'TERMS_URL',
'PRIVACY_URL',
@@ -60,4 +61,12 @@ class DashboardController < ActionController::Base
GIT_SHA: GIT_HASH
}
end
def set_application_pack
@application_pack = if request.path.include?('/auth') || request.path.include?('/login')
'v3app'
else
'application'
end
end
end
@@ -37,3 +37,5 @@ class DeviseOverrides::SessionsController < DeviseTokenAuth::SessionsController
@resource = user if user&.valid_sso_auth_token?(params[:sso_auth_token])
end
end
DeviseOverrides::SessionsController.prepend_mod_with('DeviseOverrides::SessionsController')
@@ -43,8 +43,6 @@ class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::B
end
def render_article_content(content)
# rubocop:disable Rails/OutputSafety
CommonMarker.render_html(content).html_safe
# rubocop:enable Rails/OutputSafety
ChatwootMarkdownRenderer.new(content).render_article
end
end
@@ -6,6 +6,8 @@ class Public::Api::V1::Portals::BaseController < PublicController
def set_locale(&)
switch_locale_with_portal(&) if params[:locale].present?
switch_locale_with_article(&) if params[:article_slug].present?
yield
end
def switch_locale_with_portal(&)
@@ -50,6 +50,13 @@ class SuperAdmin::AccountsController < SuperAdmin::ApplicationController
# rubocop:enable Rails/I18nLocaleTexts
end
def reset_cache
requested_resource.reset_cache_keys
# rubocop:disable Rails/I18nLocaleTexts
redirect_back(fallback_location: [namespace, requested_resource], notice: 'Cache keys cleared')
# rubocop:enable Rails/I18nLocaleTexts
end
def destroy
account = Account.find(params[:id])
@@ -41,4 +41,14 @@ class SuperAdmin::AgentBotsController < SuperAdmin::ApplicationController
# See https://administrate-prototype.herokuapp.com/customizing_controller_actions
# for more information
def destroy_avatar
avatar = requested_resource.avatar
avatar.purge
redirect_back(fallback_location: super_admin_agent_bots_path)
end
def scoped_resource
resource_class.with_attached_avatar
end
end
@@ -5,6 +5,17 @@ class SuperAdmin::InstanceStatusesController < SuperAdmin::ApplicationController
sha
postgres_status
redis_metrics
chatwoot_edition
end
def chatwoot_edition
@metrics['Chatwoot edition'] = if ChatwootApp.enterprise?
'Enterprise'
elsif ChatwootApp.custom?
'Custom'
else
'Community'
end
end
def chatwoot_version
@@ -45,6 +45,17 @@ class SuperAdmin::UsersController < SuperAdmin::ApplicationController
# empty values into nil values. It uses other APIs such as `resource_class`
# and `dashboard`:
#
def destroy_avatar
avatar = requested_resource.avatar
avatar.purge
redirect_back(fallback_location: super_admin_users_path)
end
def scoped_resource
resource_class.with_attached_avatar
end
def resource_params
permitted_params = super
permitted_params.delete(:password) if permitted_params[:password].blank?
+7
View File
@@ -10,6 +10,11 @@ class AgentBotDashboard < Administrate::BaseDashboard
ATTRIBUTE_TYPES = {
access_token: Field::HasOne,
avatar_url: AvatarField,
avatar: Field::ActiveStorage.with_options(
destroy_url: proc do |_namespace, _resource, attachment|
[:avatar_super_admin_agent_bot, { attachment_id: attachment.id }]
end
),
id: Field::Number,
name: Field::String,
account: Field::BelongsTo.with_options(searchable: true, searchable_field: 'name', order: 'id DESC'),
@@ -36,6 +41,7 @@ class AgentBotDashboard < Administrate::BaseDashboard
# an array of attributes that will be displayed on the model's show page.
SHOW_PAGE_ATTRIBUTES = %i[
id
avatar_url
account
name
description
@@ -47,6 +53,7 @@ class AgentBotDashboard < Administrate::BaseDashboard
# on the model's form (`new` and `edit`) pages.
FORM_ATTRIBUTES = %i[
name
avatar
account
description
outgoing_url
+6
View File
@@ -11,6 +11,11 @@ class UserDashboard < Administrate::BaseDashboard
account_users: Field::HasMany,
id: Field::Number,
avatar_url: AvatarField,
avatar: Field::ActiveStorage.with_options(
destroy_url: proc do |_namespace, _resource, attachment|
[:avatar_super_admin_user, { attachment_id: attachment.id }]
end
),
provider: Field::String,
uid: Field::String,
password: Field::Password,
@@ -69,6 +74,7 @@ class UserDashboard < Administrate::BaseDashboard
# on the model's form (`new` and `edit`) pages.
FORM_ATTRIBUTES = %i[
name
avatar
display_name
email
password
+3 -2
View File
@@ -6,7 +6,8 @@ class ConversationFinder
latest: 'latest',
sort_on_created_at: 'sort_on_created_at',
last_user_message_at: 'last_user_message_at',
sort_on_priority: 'sort_on_priority'
sort_on_priority: 'sort_on_priority',
sort_on_waiting_since: 'sort_on_waiting_since'
}.with_indifferent_access
# assumptions
@@ -102,7 +103,7 @@ class ConversationFinder
when 'participating'
@conversations = current_user.participating_conversations.where(account_id: current_account.id)
when 'unattended'
@conversations = @conversations.where(first_reply_created_at: nil)
@conversations = @conversations.unattended
end
@conversations
end
+21
View File
@@ -0,0 +1,21 @@
module BillingHelper
private
def default_plan?(account)
installation_config = InstallationConfig.find_by(name: 'CHATWOOT_CLOUD_PLANS')
default_plan = installation_config&.value&.first
# Return false if not plans are configured, so that no checks are enforced
return false if default_plan.blank?
account.custom_attributes['plan_name'].nil? || account.custom_attributes['plan_name'] == default_plan['name']
end
def conversations_this_month(account)
account.conversations.where('created_at > ?', 30.days.ago).count
end
def non_web_inboxes(account)
account.inboxes.where.not(channel_type: Channel::WebWidget.to_s).count
end
end
+1 -3
View File
@@ -7,8 +7,6 @@ module MessageFormatHelper
end
def render_message_content(message_content)
# rubocop:disable Rails/OutputSafety
CommonMarker.render_html(message_content).html_safe
# rubocop:enable Rails/OutputSafety
ChatwootMarkdownRenderer.new(message_content).render_message
end
end
+3 -1
View File
@@ -29,7 +29,9 @@ module ReportHelper
end
def resolutions_count
(get_grouped_values scope.conversations.where(account_id: account.id).resolved).count
object_scope = scope.reporting_events.joins(:conversation).select(:conversation_id).where(account_id: account.id, name: :conversation_resolved,
conversations: { status: :resolved }).distinct
(get_grouped_values object_scope).count
end
def avg_first_response_time
+9 -5
View File
@@ -6,6 +6,10 @@
:class="{ 'app-rtl--wrapper': isRTLView }"
>
<update-banner :latest-chatwoot-version="latestChatwootVersion" />
<template v-if="!accountUIFlags.isFetchingItem && currentAccountId">
<payment-pending-banner />
<upgrade-banner />
</template>
<transition name="fade" mode="out-in">
<router-view />
</transition>
@@ -25,6 +29,8 @@ import AddAccountModal from '../dashboard/components/layout/sidebarComponents/Ad
import LoadingState from './components/widgets/LoadingState.vue';
import NetworkNotification from './components/NetworkNotification';
import UpdateBanner from './components/app/UpdateBanner.vue';
import UpgradeBanner from './components/app/UpgradeBanner.vue';
import PaymentPendingBanner from './components/app/PaymentPendingBanner.vue';
import vueActionCable from './helper/actionCable';
import WootSnackbarBox from './components/SnackbarContainer';
import rtlMixin from 'shared/mixins/rtlMixin';
@@ -41,7 +47,9 @@ export default {
LoadingState,
NetworkNotification,
UpdateBanner,
PaymentPendingBanner,
WootSnackbarBox,
UpgradeBanner,
},
mixins: [rtlMixin],
@@ -59,6 +67,7 @@ export default {
currentUser: 'getCurrentUser',
globalConfig: 'globalConfig/get',
authUIFlags: 'getAuthUIFlags',
accountUIFlags: 'accounts/getUIFlags',
currentAccountId: 'getCurrentAccountId',
}),
hasAccounts() {
@@ -115,11 +124,6 @@ export default {
<style lang="scss">
@import './assets/scss/app';
.update-banner {
height: var(--space-larger);
align-items: center;
font-size: var(--font-size-small) !important;
}
</style>
<style src="vue-multiselect/dist/vue-multiselect.min.css"></style>
-75
View File
@@ -3,47 +3,11 @@
import Cookies from 'js-cookie';
import endPoints from './endPoints';
import {
setAuthCredentials,
clearCookiesOnLogout,
deleteIndexedDBOnLogout,
} from '../store/utils/api';
export default {
login(creds) {
return new Promise((resolve, reject) => {
axios
.post('auth/sign_in', creds)
.then(response => {
setAuthCredentials(response);
resolve(response.data);
})
.catch(error => {
reject(error.response);
});
});
},
register(creds) {
const urlData = endPoints('register');
const fetchPromise = new Promise((resolve, reject) => {
axios
.post(urlData.url, {
account_name: creds.accountName.trim(),
user_full_name: creds.fullName.trim(),
email: creds.email,
password: creds.password,
h_captcha_client_response: creds.hCaptchaClientResponse,
})
.then(response => {
setAuthCredentials(response);
resolve(response);
})
.catch(error => {
reject(error);
});
});
return fetchPromise;
},
validityCheck() {
const urlData = endPoints('validityCheck');
return axios.get(urlData.url);
@@ -73,45 +37,6 @@ export default {
}
return false;
},
verifyPasswordToken({ confirmationToken }) {
return new Promise((resolve, reject) => {
axios
.post('auth/confirmation', {
confirmation_token: confirmationToken,
})
.then(response => {
setAuthCredentials(response);
resolve(response);
})
.catch(error => {
reject(error.response);
});
});
},
setNewPassword({ resetPasswordToken, password, confirmPassword }) {
return new Promise((resolve, reject) => {
axios
.put('auth/password', {
reset_password_token: resetPasswordToken,
password_confirmation: confirmPassword,
password,
})
.then(response => {
setAuthCredentials(response);
resolve(response);
})
.catch(error => {
reject(error.response);
});
});
},
resetPassword({ email }) {
const urlData = endPoints('resetPassword');
return axios.post(urlData.url, { email });
},
profileUpdate({
password,
password_confirmation,
+4
View File
@@ -75,6 +75,10 @@ class ContactAPI extends ApiClient {
destroyAvatar(contactId) {
return axios.delete(`${this.url}/${contactId}/avatar`);
}
exportContacts() {
return axios.get(`${this.url}/export`);
}
}
export default new ContactAPI();
+2 -2
View File
@@ -35,10 +35,10 @@ class CSATReportsAPI extends ApiClient {
});
}
getMetrics({ from, to, user_ids, inbox_id, team_id } = {}) {
getMetrics({ from, to, user_ids, inbox_id, team_id, rating } = {}) {
// no ratings for metrics
return axios.get(`${this.url}/metrics`, {
params: { since: from, until: to, user_ids, inbox_id, team_id },
params: { since: from, until: to, user_ids, inbox_id, team_id, rating },
});
}
}
@@ -13,6 +13,10 @@ class EnterpriseAccountAPI extends ApiClient {
subscription() {
return axios.post(`${this.url}subscription`);
}
getLimits() {
return axios.get(`${this.url}limits`);
}
}
export default new EnterpriseAccountAPI();
@@ -127,6 +127,10 @@ class ConversationApi extends ApiClient {
user_ids: userIds,
});
}
getAllAttachments(conversationId) {
return axios.get(`${this.url}/${conversationId}/attachments`);
}
}
export default new ConversationApi();
@@ -10,6 +10,7 @@ export const buildCreatePayload = ({
files,
ccEmails = '',
bccEmails = '',
toEmails = '',
templateParams,
}) => {
let payload;
@@ -25,6 +26,9 @@ export const buildCreatePayload = ({
payload.append('echo_id', echoId);
payload.append('cc_emails', ccEmails);
payload.append('bcc_emails', bccEmails);
if (toEmails) {
payload.append('to_emails', toEmails);
}
} else {
payload = {
content: message,
@@ -33,6 +37,7 @@ export const buildCreatePayload = ({
content_attributes: contentAttributes,
cc_emails: ccEmails,
bcc_emails: bccEmails,
to_emails: toEmails,
template_params: templateParams,
};
}
@@ -53,6 +58,7 @@ class MessageApi extends ApiClient {
files,
ccEmails = '',
bccEmails = '',
toEmails = '',
templateParams,
}) {
return axios({
@@ -66,6 +72,7 @@ class MessageApi extends ApiClient {
files,
ccEmails,
bccEmails,
toEmails,
templateParams,
}),
});
@@ -210,5 +210,12 @@ describe('#ConversationAPI', () => {
{ params: { page: payload.page } }
);
});
it('#getAllAttachments', () => {
conversationAPI.getAllAttachments(1);
expect(context.axiosMock.get).toHaveBeenCalledWith(
'/api/v1/conversations/1/attachments'
);
});
});
});
@@ -26,7 +26,7 @@
border-radius: var(--border-radius-normal);
box-shadow: none;
display: flex;
height: 4.0rem;
height: 2.5rem;
}
.mx-input:disabled,
@@ -35,3 +35,32 @@
cursor: pointer;
}
}
.mx-datepicker-inline {
width: 100%;
.mx-calendar {
width: 100%;
}
.cell.disabled {
background-color: var(--s-25);
color: var(--s-200);
}
.mx-time-item.disabled {
background-color: var(--s-25);
}
.today {
font-weight: var(--font-weight-bold);
}
.mx-datepicker-main {
border: 0;
}
.mx-time-header {
border: 0;
}
}
@@ -8,7 +8,7 @@
}
select {
height: 4.0rem;
height: 2.5rem;
}
.card {
@@ -48,7 +48,7 @@
// Disable contrast warnings in Foundation.
$contrast-warnings: false;
$global-font-size: 10px;
$global-font-size: 16px;
$global-width: 100%;
$global-lineheight: 1.5;
$foundation-palette: (primary: $color-woot,
@@ -63,7 +63,7 @@ $black: #000;
$white: #fff;
$body-background: $white;
$body-font-color: $color-body;
$body-font-family: 'Inter',
$body-font-family: 'PlusJakarta',
-apple-system,
system-ui,
BlinkMacSystemFont,
@@ -117,7 +117,7 @@ $header-font-style: normal;
$font-family-monospace: $body-font-family;
$header-color: $color-heading;
$header-lineheight: 1.4;
$header-margin-bottom: 0.5rem;
$header-margin-bottom: 0.3125rem;
$header-styles: (small: ("h1": ("font-size": 24),
"h2": ("font-size": 20),
"h3": ("font-size": 19),
@@ -133,7 +133,7 @@ $header-styles: (small: ("h1": ("font-size": 24),
$header-text-rendering: optimizeLegibility;
$small-font-size: 80%;
$header-small-font-color: $medium-gray;
$paragraph-lineheight: 1.45;
$paragraph-lineheight: 1.65;
$paragraph-margin-bottom: var(--space-small);
$paragraph-text-rendering: optimizeLegibility;
$code-color: $black;
@@ -153,11 +153,11 @@ $list-lineheight: $paragraph-lineheight;
$list-margin-bottom: $paragraph-margin-bottom;
$list-style-type: disc;
$list-style-position: outside;
$list-side-margin: 1.25rem;
$list-nested-side-margin: 1.25rem;
$defnlist-margin-bottom: 1rem;
$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.3rem;
$defnlist-term-margin-bottom: 0.1875rem;
$blockquote-color: $dark-gray;
$blockquote-padding: rem-calc(9 20 0 19);
$blockquote-border: 1px solid $medium-gray;
@@ -179,9 +179,9 @@ $lead-lineheight: 1.6;
$subheader-lineheight: 1.4;
$subheader-color: $dark-gray;
$subheader-font-weight: $global-weight-normal;
$subheader-margin-top: 0.2rem;
$subheader-margin-bottom: 0.5rem;
$stat-font-size: 2.5rem;
$subheader-margin-top: 0.125rem;
$subheader-margin-bottom: 0.3125rem;
$stat-font-size: 1.5625rem;
// 6. Abide
// --------
@@ -202,11 +202,11 @@ $accordion-plusminus: true;
$accordion-title-font-size: rem-calc(12);
$accordion-item-color: $primary-color;
$accordion-item-background-hover: $light-gray;
$accordion-item-padding: 1.25rem 1rem;
$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: 1rem;
$accordion-content-padding: 0.625rem;
// 8. Accordion Menu
// -----------------
@@ -234,7 +234,7 @@ $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.75rem;
$breadcrumbs-item-margin: 0.46875rem;
$breadcrumbs-item-uppercase: true;
$breadcrumbs-item-slash: true;
@@ -275,8 +275,8 @@ $buttongroup-radius-on-each: false;
$callout-background: $white;
$callout-background-fade: 85%;
$callout-border: 1px solid rgba($black, 0.25);
$callout-margin: 0 0 1rem 0;
$callout-padding: 1rem;
$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;
@@ -320,10 +320,10 @@ $drilldown-background: $white;
// 17. Dropdown
// ------------
$dropdown-padding: 1rem;
$dropdown-padding: 0.625rem;
$dropdown-background: $body-background;
$dropdown-border: 1px solid $medium-gray;
$dropdown-font-size: 1rem;
$dropdown-font-size: 0.625rem;
$dropdown-width: 300px;
$dropdown-radius: $global-radius;
$dropdown-sizes: (tiny: 100px,
@@ -354,7 +354,7 @@ $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: 1rem;
$input-prefix-padding: 0.625rem;
$form-label-color: $color-body;
$form-label-font-size: rem-calc(14);
$form-label-font-weight: $font-weight-medium;
@@ -406,14 +406,14 @@ $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.25rem;
$menu-icon-spacing: 0.15625rem;
$menu-item-background-hover: $light-gray;
$menu-border: $light-gray;
// 23. Meter
// ---------
$meter-height: 1rem;
$meter-height: 0.625rem;
$meter-radius: $global-radius;
$meter-background: $medium-gray;
$meter-fill-good: $success-color;
@@ -423,11 +423,11 @@ $meter-fill-bad: $alert-color;
// 24. Off-canvas
// --------------
$offcanvas-sizes: (small: 23rem,
medium: 23rem,
$offcanvas-sizes: (small: 14.375,
medium: 14.375,
);
$offcanvas-vertical-sizes: (small: 23rem,
medium: 23rem,
$offcanvas-vertical-sizes: (small: 14.375,
medium: 14.375,
);
$offcanvas-background: $light-gray;
$offcanvas-shadow: 0 0 10px rgba($black, 0.7);
@@ -445,14 +445,14 @@ $maincontent-class: 'off-canvas-content';
$orbit-bullet-background: $medium-gray;
$orbit-bullet-background-active: $dark-gray;
$orbit-bullet-diameter: 1.2rem;
$orbit-bullet-margin: 0.1rem;
$orbit-bullet-margin-top: 0.8rem;
$orbit-bullet-margin-bottom: 0.8rem;
$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: 1rem;
$orbit-caption-padding: 0.625rem;
$orbit-control-background-hover: rgba($black, 0.5);
$orbit-control-padding: 1rem;
$orbit-control-padding: 0.625rem;
$orbit-control-zindex: 10;
// 26. Pagination
@@ -476,7 +476,7 @@ $pagination-arrows: true;
// 27. Progress Bar
// ----------------
$progress-height: 1rem;
$progress-height: 0.625rem;
$progress-background: $medium-gray;
$progress-margin-bottom: $global-margin;
$progress-meter-background: $primary-color;
@@ -504,13 +504,13 @@ $reveal-overlay-background: rgba($black, 0.45);
// 30. Slider
// ----------
$slider-width-vertical: 0.5rem;
$slider-width-vertical: 0.3125rem;
$slider-transition: all 0.2s ease-in-out;
$slider-height: 0.5rem;
$slider-height: 0.3125rem;
$slider-background: $light-gray;
$slider-fill-background: $medium-gray;
$slider-handle-height: 1.4rem;
$slider-handle-width: 1.4rem;
$slider-handle-height: 0.875rem;
$slider-handle-width: 0.875rem;
$slider-handle-background: $primary-color;
$slider-opacity-disabled: 0.25;
$slider-radius: $global-radius;
@@ -569,7 +569,7 @@ $tab-expand-max: 6;
$tab-content-background: transparent;
$tab-content-border: transparent;
$tab-content-color: foreground($tab-background, $primary-color);
$tab-content-padding: 1rem;
$tab-content-padding: 0.625rem;
// 34. Thumbnail
// -------------
@@ -586,11 +586,11 @@ $thumbnail-radius: $global-radius;
$titlebar-background: $black;
$titlebar-color: $white;
$titlebar-padding: 0.5rem;
$titlebar-padding: 0.3125rem;
$titlebar-text-font-weight: bold;
$titlebar-icon-color: $white;
$titlebar-icon-color-hover: $medium-gray;
$titlebar-icon-spacing: 0.25rem;
$titlebar-icon-spacing: 0.15625rem;
// 36. Tooltip
// -----------
@@ -599,19 +599,19 @@ $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.75rem;
$tooltip-padding: 0.46875rem;
$tooltip-font-size: $font-size-mini;
$tooltip-pip-width: 0.75rem;
$tooltip-pip-width: 0.46875rem;
$tooltip-pip-height: $tooltip-pip-width * 0.866;
$tooltip-radius: $global-radius;
// 37. Top Bar
// -----------
$topbar-padding: 0.5rem;
$topbar-padding: 0.3125;
$topbar-background: $light-gray;
$topbar-submenu-background: $topbar-background;
$topbar-title-spacing: 0.5rem 1rem 0.5rem 0;
$topbar-title-spacing: 0.3125 0.625rem 0.3125 0;
$topbar-input-width: 200px;
$topbar-unstack-breakpoint: medium;
@@ -237,8 +237,8 @@ $spinner-before-border-color: rgba(255, 255, 255, 0.7);
white-space: nowrap;
}
@mixin three-column-grid($column-one-width: 25.6rem,
$column-three-width: 25.6rem) {
@mixin three-column-grid($column-one-width: 16rem,
$column-three-width: 16rem) {
width: 100%;
height: 100%;
display: grid;
@@ -439,7 +439,7 @@
}
span {
--minus-space-one-point-five: -1.5rem;
--minus-space-one-point-five: -0.9375rem;
&.active {
transform: translate(
@@ -29,4 +29,5 @@ a {
p {
font-size: $font-size-small;
word-spacing: .12em;
}
@@ -1,30 +1,30 @@
// Font sizes
$font-size-nano: 0.8rem;
$font-size-micro: 1.0rem;
$font-size-mini: 1.2rem;
$font-size-small: 1.4rem;
$font-size-default: 1.6rem;
$font-size-medium: 1.8rem;
$font-size-large: 2.2rem;
$font-size-big: 2.4rem;
$font-size-bigger: 3.0rem;
$font-size-mega: 3.4rem;
$font-size-giga: 4.0rem;
$font-size-nano: 0.5rem;
$font-size-micro: 0.675rem;
$font-size-mini: 0.75rem;
$font-size-small: 0.875rem;
$font-size-default: 1rem;
$font-size-medium: 1.125rem;
$font-size-large: 1.375rem;
$font-size-big: 1.5rem;
$font-size-bigger: 1.75rem;
$font-size-mega: 2.125rem;
$font-size-giga: 2.5rem;
// spaces
$zero: 0;
$space-micro: 0.2rem;
$space-smaller: 0.4rem;
$space-small: 0.8rem;
$space-one: 1rem;
$space-slab: 1.2rem;
$space-normal: 1.6rem;
$space-two: 2.0rem;
$space-medium: 2.4rem;
$space-large: 3.2rem;
$space-larger: 4.8rem;
$space-jumbo: 6.4rem;
$space-mega: 10.0rem;
$space-micro: 0.125rem;
$space-smaller: 0.25rem;
$space-small: 0.5rem;
$space-one: 0.675rem;
$space-slab: 0.75rem;
$space-normal: 1rem;
$space-two: 1.25rem;
$space-medium: 1.5rem;
$space-large: 2rem;
$space-larger: 3rem;
$space-jumbo: 4rem;
$space-mega: 6.25rem;
// font-weight
$font-weight-feather: 100;
@@ -35,8 +35,8 @@ $font-weight-bold: 600;
$font-weight-black: 700;
//Navbar
$nav-bar-width: 23rem;
$header-height: 5.6rem;
$nav-bar-width: 14.375rem;
$header-height: 3.5rem;
$woot-logo-padding: $space-large $space-two;
@@ -71,20 +71,20 @@ $color-primary-light: #c7e3ff;
$color-primary-dark: darken($color-woot, 20%);
// Thumbnail
$thumbnail-radius: 4rem;
$thumbnail-radius: 2.5rem;
// chat-header
$conv-header-height: 4rem;
$conv-header-height: 2.5rem;
// Inbox List
$inbox-thumb-size: 4.8rem;
$inbox-thumb-size: 3rem;
// Spinner
$spinkit-spinner-color: $color-white !default;
$spinkit-spinner-margin: 0 0 0 1.6rem !default;
$spinkit-size: 1.6rem !default;
$spinkit-spinner-margin: 0 0 0 1rem !default;
$spinkit-size: 1rem !default;
// Snackbar default
$woot-snackbar-bg: #323232;
@@ -101,5 +101,5 @@ $ionicons-font-path: '~ionicons/fonts';
$transition-ease-in: all 0.250s ease-in;
:root {
--dashboard-app-tabs-height: 3.9rem;
--dashboard-app-tabs-height: 2.4375rem;
}
@@ -1,4 +1,4 @@
@import 'shared/assets/fonts/inter';
@import 'shared/assets/fonts/plus-jakarta';
@import 'shared/assets/stylesheets/animations';
@import 'shared/assets/stylesheets/colors';
@import 'shared/assets/stylesheets/spacing';
@@ -28,7 +28,7 @@
}
.multiselect__select {
min-height: 4.6rem;
min-height: 2.875rem;
padding: 0;
right: 0;
top: 0;
@@ -98,7 +98,7 @@
border: 1px solid var(--s-200);
border-color: var(--s-200);
margin: 0;
min-height: 4.4rem;
min-height: 2.75rem;
padding-top: $zero;
}
@@ -130,7 +130,7 @@
.multiselect__input {
@include ghost-input;
font-size: $font-size-small;
height: 4.4rem;
height: 2.75rem;
margin-bottom: $zero;
padding: 0;
}
@@ -168,7 +168,7 @@
}
.multiselect-wrap--small {
$multiselect-height: 4.0rem;
$multiselect-height: 2.5rem;
.multiselect__tags,
.multiselect__input {
@@ -195,7 +195,7 @@
display: flex;
font-size: var(--font-size-small);
margin: 0;
max-height: 3.8rem;
max-height: 2.375rem;
padding: var(--space-smaller) var(--space-micro);
}
@@ -219,7 +219,7 @@
}
.multiselect-wrap--medium {
$multiselect-height: 4.8rem;
$multiselect-height: 3rem;
.multiselect__tags,
.multiselect__input {
@@ -1,4 +1,5 @@
@import 'shared/assets/fonts/inter';
@import 'shared/assets/fonts/plus-jakarta';
@import 'shared/assets/stylesheets/animations';
@import 'shared/assets/stylesheets/colors';
@import 'shared/assets/stylesheets/spacing';
@@ -1,3 +1,3 @@
@import 'shared/assets/fonts/inter';
@import 'shared/assets/fonts/plus-jakarta';
@import '../variables';
@import '~shared/assets/stylesheets/ionicons';
@@ -8,8 +8,8 @@
.integration--image {
display: flex;
height: 10rem;
width: 10rem;
height: 6.25rem;
width: 6.25rem;
img {
max-width: 100%;
@@ -1,4 +1,4 @@
$default-button-height: 4.0rem;
$default-button-height: 2.5rem;
.button {
align-items: center;
@@ -1,4 +1,4 @@
$resolve-button-width: 13.2rem;
$resolve-button-width: 8.25rem;
// Conversation header - Light BG
.conv-header {
@@ -14,12 +14,12 @@ $resolve-button-width: 13.2rem;
border: 1px solid var(--color-border);
border-radius: var(--space-smaller);
margin-right: var(--space-small);
width: 21.6rem;
width: 13.5rem;
.icon {
color: $medium-gray;
font-size: $font-size-default;
line-height: 3.8rem;
line-height: 2.375rem;
padding-left: $space-slab;
padding-right: $space-smaller;
}
@@ -81,7 +81,7 @@
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 27rem;
width: 16.875rem;
}
.conversation--meta {
@@ -29,13 +29,13 @@
}
.modal-image {
max-height: 90%;
max-width: 90%;
max-height: 76vh;
max-width: 76vw;
}
.modal-video {
max-height: 75vh;
max-width: 100%;
max-height: 76vh;
max-width: 76vw;
}
&::before {
@@ -53,16 +53,6 @@
width: 100%;
}
}
.video {
.modal-container {
width: auto;
.modal--close {
z-index: var(--z-index-low);
}
}
}
}
.conversations-list-wrap {
@@ -283,7 +273,7 @@
}
.wrap {
--bubble-max-width: 49.6rem;
--bubble-max-width: 31rem;
margin: $zero $space-normal;
max-width: Min(var(--bubble-max-width), 84%);
@@ -400,4 +390,3 @@
margin-bottom: 0;
}
}
@@ -27,7 +27,7 @@
padding: $space-large $space-large $zero;
img {
max-height: 6rem;
max-height: 3.75rem;
}
}
@@ -39,11 +39,11 @@
max-height: 100%;
overflow: auto;
position: relative;
width: 60rem;
width: 37.5rem;
&.medium {
max-width: 80%;
width: 90rem;
width: 56.25rem;
}
.content-box {
@@ -29,7 +29,7 @@
// Override min-height : 50px in foundation
//
max-height: $space-mega * 2.4;
min-height: 4.8rem;
min-height: 3rem;
padding: var(--space-normal) 0 0;
resize: none;
}
@@ -40,7 +40,7 @@
margin: 0;
max-height: $space-mega * 2.4;
// Override min-height : 50px in foundation
min-height: 4.8rem;
min-height: 3rem;
padding: var(--space-normal) 0 0;
resize: none;
}
@@ -47,7 +47,7 @@
}
.dropdown-pane {
bottom: 6rem;
bottom: 3.75rem;
display: block;
visibility: visible;
width: fit-content;
@@ -1,7 +1,7 @@
.ui-snackbar-container {
left: 0;
margin: 0 auto;
max-width: 40rem;
max-width: 25rem;
overflow: hidden;
position: absolute;
right: 0;
@@ -16,9 +16,9 @@
border-radius: $space-smaller;
display: inline-flex;
margin-bottom: $space-small;
max-width: 40rem;
min-height: 3rem;
min-width: 24rem;
max-width: 25rem;
min-height: 1.875rem;
min-width: 15rem;
padding: $space-slab $space-medium;
text-align: left;
}
@@ -31,7 +31,7 @@
.ui-snackbar-action {
margin-left: auto;
padding-left: 3rem;
padding-left: 1.875rem;
button {
background: none;
@@ -43,15 +43,15 @@ table {
.woot-thumbnail {
border-radius: 50%;
height: 5rem;
width: 5rem;
height: 3.125rem;
width: 3.125rem;
}
.button-wrapper {
@include flex-align(left, null);
@include flex;
flex-direction: row;
min-width: 20rem;
min-width: 12.5rem;
}
.button {
@@ -47,6 +47,14 @@
/>
</div>
<div v-if="hasActiveFolders">
<woot-button
v-tooltip.top-end="$t('FILTER.CUSTOM_VIEWS.EDIT.EDIT_BUTTON')"
size="tiny"
variant="smooth"
color-scheme="secondary"
icon="edit"
@click="onToggleAdvanceFiltersModal"
/>
<woot-button
v-tooltip.top-end="$t('FILTER.CUSTOM_VIEWS.DELETE.DELETE_BUTTON')"
size="tiny"
@@ -168,8 +176,11 @@
v-if="showAdvancedFilters"
:initial-filter-types="advancedFilterTypes"
:initial-applied-filters="appliedFilter"
:active-folder-name="activeFolderName"
:on-close="closeAdvanceFiltersModal"
:is-folder-view="hasActiveFolders"
@applyFilter="onApplyFilter"
@updateFolder="onUpdateSavedFilter"
/>
</woot-modal>
</div>
@@ -193,6 +204,9 @@ import DeleteCustomViews from 'dashboard/routes/dashboard/customviews/DeleteCust
import ConversationBulkActions from './widgets/conversation/conversationBulkActions/Index.vue';
import alertMixin from 'shared/mixins/alertMixin';
import filterMixin from 'shared/mixins/filterMixin';
import languages from 'dashboard/components/widgets/conversation/advancedFilterItems/languages';
import countries from 'shared/constants/countries';
import { generateValuesForEditCustomViews } from 'dashboard/helper/customViewsHelper';
import {
hasPressedAltAndJKey,
@@ -289,6 +303,11 @@ export default {
appliedFilters: 'getAppliedConversationFilters',
folders: 'customViews/getCustomViews',
inboxes: 'inboxes/getInboxes',
agentList: 'agents/getAgents',
teamsList: 'teams/getTeams',
inboxesList: 'inboxes/getInboxes',
campaigns: 'campaigns/getAllCampaigns',
labels: 'labels/getLabels',
}),
hasAppliedFilters() {
return this.appliedFilters.length !== 0;
@@ -451,6 +470,9 @@ export default {
}
return undefined;
},
activeFolderName() {
return this.activeFolder?.name;
},
activeTeam() {
if (this.teamId) {
return this.$store.getters['teams/getTeam'](this.teamId);
@@ -483,9 +505,7 @@ export default {
this.resetAndFetchData();
},
activeFolder() {
if (!this.hasAppliedFilters) {
this.resetAndFetchData();
}
this.resetAndFetchData();
},
chatLists() {
this.chatsOnView = this.conversationList;
@@ -496,6 +516,10 @@ export default {
this.$store.dispatch('setChatSortFilter', this.activeSortBy);
this.resetAndFetchData();
if (this.hasActiveFolders) {
this.$store.dispatch('campaigns/get');
}
bus.$on('fetch_conversation_stats', () => {
this.$store.dispatch('conversationStats/get', this.conversationFilters);
});
@@ -508,6 +532,15 @@ export default {
this.$store.dispatch('emptyAllConversations');
this.fetchFilteredConversations(payload);
},
onUpdateSavedFilter(payload, folderName) {
const payloadData = {
...this.activeFolder,
name: folderName,
query: filterQueryGenerator(payload),
};
this.$store.dispatch('customViews/update', payloadData);
this.closeAdvanceFiltersModal();
},
onClickOpenAddFoldersModal() {
this.showAddFoldersModal = true;
},
@@ -521,15 +554,70 @@ export default {
this.showDeleteFoldersModal = false;
},
onToggleAdvanceFiltersModal() {
if (!this.hasAppliedFilters) {
if (!this.hasAppliedFilters && !this.hasActiveFolders) {
this.initializeExistingFilterToModal();
}
if (this.hasActiveFolders) {
this.initializeFolderToFilterModal(this.activeFolder);
}
this.showAdvancedFilters = true;
},
closeAdvanceFiltersModal() {
this.showAdvancedFilters = false;
this.appliedFilter = [];
},
setParamsForEditFolderModal() {
// Here we are setting the params for edit folder modal to show the existing values.
// For agent, team, inboxes,and campaigns we get only the id's from the query.
// So we are mapping the id's to the actual values.
// For labels we get the name of the label from the query.
// If we delete the label from the label list then we will not be able to show the label name.
// For custom attributes we get only attribute key.
// So we are mapping it to find the input type of the attribute to show in the edit folder modal.
const params = {
agents: this.agentList,
teams: this.teamsList,
inboxes: this.inboxesList,
labels: this.labels,
campaigns: this.campaigns,
languages: languages,
countries: countries,
filterTypes: advancedFilterTypes,
allCustomAttributes: this.$store.getters[
'attributes/getAttributesByModel'
]('conversation_attribute'),
};
return params;
},
initializeFolderToFilterModal(activeFolder) {
// Here we are setting the params for edit folder modal.
// To show the existing values. when we click on edit folder button.
// Here we get the query from the active folder.
// And we are mapping the query to the actual values.
// To show in the edit folder modal by the help of generateValuesForEditCustomViews helper.
const query = activeFolder?.query?.payload;
if (!Array.isArray(query)) return;
this.appliedFilter.push(
...query.map(filter => ({
attribute_key: filter.attribute_key,
attribute_model: filter.attribute_model,
filter_operator: filter.filter_operator,
values: Array.isArray(filter.values)
? generateValuesForEditCustomViews(
filter,
this.setParamsForEditFolderModal()
)
: [],
query_operator: filter.query_operator,
custom_attribute_type: filter.custom_attribute_type,
}))
);
},
getKeyboardListenerParams() {
const allConversations = this.$refs.activeConversation.querySelectorAll(
'div.conversations-list div.conversation'
@@ -575,6 +663,7 @@ export default {
}
},
resetAndFetchData() {
this.appliedFilter = [];
this.resetBulkActions();
this.$store.dispatch('conversationPage/reset');
this.$store.dispatch('emptyAllConversations');
@@ -587,7 +676,6 @@ export default {
return;
}
this.fetchConversations();
this.appliedFilter = [];
},
fetchConversations() {
this.$store
@@ -884,7 +972,7 @@ export default {
.conversations-list-wrap {
flex-shrink: 0;
flex-basis: clamp(32rem, 4vw + 34rem, 44rem);
flex-basis: clamp(20rem, 4vw + 21.25rem, 27.5rem);
overflow: hidden;
&.hide {
@@ -115,7 +115,7 @@
</template>
<script>
import format from 'date-fns/format';
import { format, parseISO } from 'date-fns';
import { required, url } from 'vuelidate/lib/validators';
import { BUS_EVENTS } from 'shared/constants/busEvents';
import MultiselectDropdown from 'shared/components/ui/MultiselectDropdown.vue';
@@ -143,15 +143,20 @@ export default {
},
computed: {
formattedValue() {
displayValue() {
if (this.isAttributeTypeDate) {
return format(new Date(this.value || new Date()), DATE_FORMAT);
return new Date(this.value || new Date()).toLocaleDateString();
}
if (this.isAttributeTypeCheckbox) {
return this.value === 'false' ? false : this.value;
}
return this.value;
},
formattedValue() {
return this.isAttributeTypeDate
? format(this.value ? new Date(this.value) : new Date(), DATE_FORMAT)
: this.value;
},
listOptions() {
return this.values.map((value, index) => ({
id: index + 1,
@@ -192,17 +197,11 @@ export default {
}
return this.$t('CUSTOM_ATTRIBUTES.VALIDATIONS.REQUIRED');
},
displayValue() {
if (this.attributeType === 'date') {
return format(new Date(this.editedValue), 'dd-MM-yyyy');
}
return this.editedValue;
},
},
watch: {
value() {
this.isEditing = false;
this.editedValue = this.value;
this.editedValue = this.formattedValue;
},
},
@@ -249,9 +248,8 @@ export default {
onUpdate() {
const updatedValue =
this.attributeType === 'date'
? format(new Date(this.editedValue), DATE_FORMAT)
? parseISO(this.editedValue)
: this.editedValue;
this.$v.$touch();
if (this.$v.$invalid) {
return;
@@ -345,11 +343,11 @@ export default {
.error-message {
color: var(--r-400);
display: block;
font-size: 1.4rem;
font-size: 0.875rem;
font-size: var(--font-size-small);
font-weight: 400;
margin-bottom: 1rem;
margin-top: -1.6rem;
margin-bottom: 0.625rem;
margin-top: -1rem;
width: 100%;
}
@@ -0,0 +1,75 @@
<template>
<div class="column">
<woot-modal-header :header-title="$t('CONVERSATION.CUSTOM_SNOOZE.TITLE')" />
<form class="row modal-content" @submit.prevent="chooseTime">
<date-picker
v-model="snoozeTime"
type="datetime"
inline
:lang="lang"
:disabled-date="disabledDate"
:disabled-time="disabledTime"
:popup-style="{ width: '100%' }"
/>
<div class="modal-footer justify-content-end w-full">
<woot-button variant="clear" @click.prevent="onClose">
{{ this.$t('CONVERSATION.CUSTOM_SNOOZE.CANCEL') }}
</woot-button>
<woot-button>
{{ this.$t('CONVERSATION.CUSTOM_SNOOZE.APPLY') }}
</woot-button>
</div>
</form>
</div>
</template>
<script>
import DatePicker from 'vue2-datepicker';
export default {
components: {
DatePicker,
},
data() {
return {
snoozeTime: null,
lang: {
days: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
yearFormat: 'YYYY',
monthFormat: 'MMMM',
},
};
},
methods: {
onClose() {
this.$emit('close');
},
chooseTime() {
this.$emit('choose-time', this.snoozeTime);
},
disabledDate(date) {
// Disable all the previous dates
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
return date < yesterday;
},
disabledTime(date) {
// Allow only time after 1 hour
const now = new Date();
now.setHours(now.getHours() + 1);
return date < now;
},
},
};
</script>
<style lang="scss" scoped>
.modal-footer {
padding: var(--space-two);
}
.modal-content {
padding: var(--space-small) var(--space-two) var(--space-zero);
}
</style>
@@ -8,6 +8,7 @@
>
<div :class="modalContainerClassName" @click.stop>
<woot-button
v-if="showCloseButton"
color-scheme="secondary"
icon="dismiss"
variant="clear"
@@ -28,6 +29,10 @@ export default {
default: true,
},
show: Boolean,
showCloseButton: {
type: Boolean,
default: true,
},
onClose: {
type: Function,
required: true,
@@ -101,7 +106,7 @@ export default {
.modal-container {
border-radius: 0;
height: 100%;
width: 48rem;
width: 30rem;
}
}
.modal-big {
@@ -77,7 +77,7 @@ export default {
@import '~dashboard/assets/scss/mixins';
.ui-notification-container {
max-width: 40rem;
max-width: 25rem;
position: absolute;
right: var(--space-normal);
top: var(--space-normal);
@@ -94,7 +94,7 @@ export default {
border-radius: var(--border-radius-medium);
box-shadow: var(--shadow-large);
min-width: 24rem;
min-width: 15rem;
padding: var(--space-normal);
}
@@ -1,11 +1,11 @@
<template>
<div class="row settings--section">
<div class="row settings--section" :class="{ border: showBorder }">
<div class="medium-4 small-12 title--section">
<p class="sub-block-title">
<p v-if="title" class="sub-block-title">
{{ title }}
</p>
<p class="sub-head">
<slot name="subTitle">
<slot v-if="subTitle" name="subTitle">
{{ subTitle }}
</slot>
</p>
@@ -25,11 +25,15 @@ export default {
props: {
title: {
type: String,
required: true,
default: '',
},
subTitle: {
type: String,
required: true,
default: '',
},
showBorder: {
type: Boolean,
default: true,
},
note: {
type: String,
@@ -43,9 +47,13 @@ export default {
@import '~dashboard/assets/scss/variables';
.settings--section {
border-bottom: 1px solid $color-border;
display: flex;
padding: $space-normal $space-normal $space-normal 0;
padding: 0 $space-normal $space-normal 0;
&.border {
padding-top: $space-normal;
border-bottom: 1px solid $color-border;
}
.sub-block-title {
color: $color-woot;
@@ -0,0 +1,84 @@
<template>
<banner
v-if="shouldShowBanner"
color-scheme="alert"
:banner-message="bannerMessage"
:action-button-label="actionButtonMessage"
has-action-button
@click="routeToBilling"
/>
</template>
<script>
import Banner from 'dashboard/components/ui/Banner.vue';
import { mapGetters } from 'vuex';
import adminMixin from 'dashboard/mixins/isAdmin';
import accountMixin from 'dashboard/mixins/account';
const EMPTY_SUBSCRIPTION_INFO = {
status: null,
endsOn: null,
};
export default {
components: { Banner },
mixins: [adminMixin, accountMixin],
computed: {
...mapGetters({
isOnChatwootCloud: 'globalConfig/isOnChatwootCloud',
getAccount: 'accounts/getAccount',
}),
bannerMessage() {
return this.$t('GENERAL_SETTINGS.PAYMENT_PENDING');
},
actionButtonMessage() {
return this.$t('GENERAL_SETTINGS.OPEN_BILLING');
},
shouldShowBanner() {
if (!this.isOnChatwootCloud) {
return false;
}
if (!this.isAdmin) {
return false;
}
return this.isPaymentPending();
},
},
methods: {
routeToBilling() {
this.$router.push({
name: 'billing_settings_index',
params: { accountId: this.accountId },
});
},
isPaymentPending() {
const { status, endsOn } = this.getSubscriptionInfo();
if (status && endsOn) {
const now = new Date();
if (status === 'past_due' && endsOn < now) {
return true;
}
}
return false;
},
getSubscriptionInfo() {
const account = this.getAccount(this.accountId);
if (!account) return EMPTY_SUBSCRIPTION_INFO;
const { custom_attributes: subscription } = account;
if (!subscription) return EMPTY_SUBSCRIPTION_INFO;
const {
subscription_status: status,
subscription_ends_on: endsOn,
} = subscription;
return { status, endsOn: new Date(endsOn) };
},
},
};
</script>
@@ -1,7 +1,6 @@
<template>
<banner
v-if="shouldShowBanner"
class="update-banner"
color-scheme="primary"
:banner-message="bannerMessage"
href-link="https://github.com/chatwoot/chatwoot/releases"
@@ -0,0 +1,89 @@
<template>
<banner
v-if="shouldShowBanner"
color-scheme="alert"
:banner-message="bannerMessage"
:action-button-label="actionButtonMessage"
has-action-button
@click="routeToBilling"
/>
</template>
<script>
import Banner from 'dashboard/components/ui/Banner.vue';
import { mapGetters } from 'vuex';
import adminMixin from 'dashboard/mixins/isAdmin';
import accountMixin from 'dashboard/mixins/account';
import { differenceInDays } from 'date-fns';
export default {
components: { Banner },
mixins: [adminMixin, accountMixin],
data() {
return { conversationMeta: {} };
},
computed: {
...mapGetters({
isOnChatwootCloud: 'globalConfig/isOnChatwootCloud',
getAccount: 'accounts/getAccount',
}),
bannerMessage() {
return this.$t('GENERAL_SETTINGS.LIMITS_UPGRADE');
},
actionButtonMessage() {
return this.$t('GENERAL_SETTINGS.OPEN_BILLING');
},
shouldShowBanner() {
if (!this.isOnChatwootCloud) {
return false;
}
if (this.isTrialAccount()) {
return false;
}
return this.isLimitExceeded();
},
},
mounted() {
if (this.isOnChatwootCloud) {
this.fetchLimits();
}
},
methods: {
fetchLimits() {
this.$store.dispatch('accounts/limits');
},
routeToBilling() {
this.$router.push({
name: 'billing_settings_index',
params: { accountId: this.accountId },
});
},
isTrialAccount() {
// check if account is less than 15 days old
const account = this.getAccount(this.accountId);
if (!account) return false;
const createdAt = new Date(account.created_at);
const diffDays = differenceInDays(new Date(), createdAt);
return diffDays <= 15;
},
isLimitExceeded() {
const account = this.getAccount(this.accountId);
if (!account) return false;
const { limits } = account;
if (!limits) return false;
const { conversation, non_web_inboxes: nonWebInboxes } = limits;
return this.testLimit(conversation) || this.testLimit(nonWebInboxes);
},
testLimit({ allowed, consumed }) {
return consumed > allowed;
},
},
};
</script>
@@ -0,0 +1,25 @@
<template>
<kbd class="hotkey">
<slot />
</kbd>
</template>
<style lang="scss">
kbd.hotkey {
display: inline-flex;
flex-shrink: 0;
align-items: center;
user-select: none;
justify-content: center;
padding: var(--space-micro);
min-width: var(--space-normal);
font-size: var(--font-size-micro);
border-radius: var(--space-smaller);
background-color: var(--color-background);
border: 1px solid var(--color-border);
text-transform: uppercase;
color: var(--color-body);
letter-spacing: var(--space-micro);
line-height: var(--font-size-micro);
}
</style>
@@ -49,6 +49,17 @@
class="dropdown-pane dropdown-pane--open"
>
<woot-dropdown-menu>
<woot-dropdown-item v-if="!isPending">
<woot-button
variant="clear"
color-scheme="secondary"
size="small"
icon="snooze"
@click="() => openSnoozeModal()"
>
{{ this.$t('CONVERSATION.RESOLVE_DROPDOWN.SNOOZE_UNTIL') }}
</woot-button>
</woot-dropdown-item>
<woot-dropdown-item v-if="!isPending">
<woot-button
variant="clear"
@@ -60,71 +71,35 @@
{{ this.$t('CONVERSATION.RESOLVE_DROPDOWN.MARK_PENDING') }}
</woot-button>
</woot-dropdown-item>
<woot-dropdown-divider v-if="isOpen" />
<woot-dropdown-sub-menu
v-if="isOpen"
:title="this.$t('CONVERSATION.RESOLVE_DROPDOWN.SNOOZE.TITLE')"
>
<woot-dropdown-item>
<woot-button
variant="clear"
color-scheme="secondary"
size="small"
icon="send-clock"
@click="() => toggleStatus(STATUS_TYPE.SNOOZED, null)"
>
{{ this.$t('CONVERSATION.RESOLVE_DROPDOWN.SNOOZE.NEXT_REPLY') }}
</woot-button>
</woot-dropdown-item>
<woot-dropdown-item>
<woot-button
variant="clear"
color-scheme="secondary"
size="small"
icon="dual-screen-clock"
@click="
() => toggleStatus(STATUS_TYPE.SNOOZED, snoozeTimes.tomorrow)
"
>
{{ this.$t('CONVERSATION.RESOLVE_DROPDOWN.SNOOZE.TOMORROW') }}
</woot-button>
</woot-dropdown-item>
<woot-dropdown-item>
<woot-button
variant="clear"
color-scheme="secondary"
size="small"
icon="calendar-clock"
@click="
() => toggleStatus(STATUS_TYPE.SNOOZED, snoozeTimes.nextWeek)
"
>
{{ this.$t('CONVERSATION.RESOLVE_DROPDOWN.SNOOZE.NEXT_WEEK') }}
</woot-button>
</woot-dropdown-item>
</woot-dropdown-sub-menu>
</woot-dropdown-menu>
</div>
<woot-modal
:show.sync="showCustomSnoozeModal"
:on-close="hideCustomSnoozeModal"
>
<custom-snooze-modal
@close="hideCustomSnoozeModal"
@choose-time="chooseSnoozeTime"
/>
</woot-modal>
</div>
</template>
<script>
import { getUnixTime } from 'date-fns';
import { mapGetters } from 'vuex';
import { mixin as clickaway } from 'vue-clickaway';
import alertMixin from 'shared/mixins/alertMixin';
import snoozeTimesMixin from 'dashboard/mixins/conversation/snoozeTimesMixin.js';
import CustomSnoozeModal from 'dashboard/components/CustomSnoozeModal';
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
import {
hasPressedAltAndEKey,
hasPressedCommandPlusAltAndEKey,
hasPressedAltAndMKey,
} from 'shared/helpers/KeyboardHelpers';
import { findSnoozeTime } from 'dashboard/helper/snoozeHelpers';
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
import WootDropdownSubMenu from 'shared/components/ui/dropdown/DropdownSubMenu.vue';
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
import WootDropdownDivider from 'shared/components/ui/dropdown/DropdownDivider';
import wootConstants from 'dashboard/constants/globals';
import {
@@ -137,16 +112,16 @@ export default {
components: {
WootDropdownItem,
WootDropdownMenu,
WootDropdownSubMenu,
WootDropdownDivider,
CustomSnoozeModal,
},
mixins: [clickaway, alertMixin, eventListenerMixins, snoozeTimesMixin],
mixins: [clickaway, alertMixin, eventListenerMixins],
props: { conversationId: { type: [String, Number], required: true } },
data() {
return {
isLoading: false,
showActionsDropdown: false,
STATUS_TYPE: wootConstants.STATUS_TYPE,
showCustomSnoozeModal: false,
};
},
computed: {
@@ -218,10 +193,26 @@ export default {
}
},
onCmdSnoozeConversation(snoozeType) {
this.toggleStatus(
this.STATUS_TYPE.SNOOZED,
this.snoozeTimes[snoozeType] || null
);
if (snoozeType === wootConstants.SNOOZE_OPTIONS.UNTIL_CUSTOM_TIME) {
this.showCustomSnoozeModal = true;
} else {
this.toggleStatus(
this.STATUS_TYPE.SNOOZED,
findSnoozeTime(snoozeType) || null
);
}
},
chooseSnoozeTime(customSnoozeTime) {
this.showCustomSnoozeModal = false;
if (customSnoozeTime) {
this.toggleStatus(
this.STATUS_TYPE.SNOOZED,
getUnixTime(customSnoozeTime)
);
}
},
hideCustomSnoozeModal() {
this.showCustomSnoozeModal = false;
},
onCmdOpenConversation() {
this.toggleStatus(this.STATUS_TYPE.OPEN);
@@ -252,6 +243,10 @@ export default {
this.isLoading = false;
});
},
openSnoozeModal() {
const ninja = document.querySelector('ninja-keys');
ninja.open({ parent: 'snooze_conversation' });
},
},
};
</script>
@@ -265,10 +260,14 @@ export default {
.dropdown-pane {
left: unset;
top: 4.2rem;
top: 2.625rem;
margin-top: var(--space-micro);
right: 0;
max-width: 20rem;
min-width: 15.6rem;
max-width: 12.5rem;
min-width: 9.75rem;
.dropdown-menu__item {
margin-bottom: 0;
}
}
</style>
@@ -230,7 +230,7 @@ export default {
.account-selector--modal {
.modal-container {
width: 40rem;
width: 25rem;
}
}
@@ -284,7 +284,7 @@ export default {
cursor: pointer;
display: flex;
flex-direction: row;
height: 6rem;
height: 3.75rem;
}
.current-user--options {
@@ -50,7 +50,7 @@
>
<a
:href="href"
class="button small clear secondary"
class="button small clear secondary custom-sidebar--button"
:class="{ 'is-active': isActive }"
@click="e => handleProfileSettingClick(e, navigate)"
>
@@ -64,7 +64,7 @@
<woot-dropdown-item v-if="currentUser.type === 'SuperAdmin'">
<a
href="/super_admin"
class="button small clear secondary"
class="button small clear secondary custom-sidebar--button"
target="_blank"
rel="noopener nofollow noreferrer"
@click="$emit('close')"
@@ -157,4 +157,9 @@ export default {
top: unset;
z-index: var(--z-index-low);
}
.custom-sidebar--button {
height: var(--space-large) !important;
padding: var(--space-smaller) 0.675rem !important;
}
</style>
@@ -193,6 +193,7 @@ export default {
.map(view => ({
id: view.id,
label: view.name,
count: view.count,
truncateLabel: true,
toState: frontendURL(
`accounts/${this.accountId}/custom_view/${view.id}`
@@ -211,6 +212,7 @@ export default {
.map(view => ({
id: view.id,
label: view.name,
count: view.count,
truncateLabel: true,
toState: frontendURL(
`accounts/${this.accountId}/contacts/custom_view/${view.id}`
@@ -258,7 +260,7 @@ export default {
background: var(--white);
border-right: 1px solid var(--s-50);
height: 100%;
width: 20rem;
width: 12.5rem;
flex-shrink: 0;
overflow-y: hidden;
position: unset;
@@ -20,19 +20,22 @@
class="badge--label"
:style="{ backgroundColor: labelColor }"
/>
<span
:title="menuTitle"
class="menu-label button__content"
:class="{ 'text-truncate': shouldTruncate }"
>
{{ label }}
<span v-if="showChildCount" class="count-view">
<div class="menu-label--wrapper">
<span
:title="menuTitle"
class="menu-label button__content"
:class="{ 'text-truncate': shouldTruncate }"
>
{{ label }}
</span>
<span
v-if="showChildCount"
class="count-view"
:class="isCountZero ? 'text-muted' : 'text-color'"
>
{{ childItemCount }}
</span>
</span>
<span v-if="count" class="badge" :class="{ secondary: !isActive }">
{{ count }}
</span>
</div>
<span v-if="warningIcon" class="badge--icon">
<fluent-icon
v-tooltip.top-end="$t('SIDEBAR.FACEBOOK_REAUTHORIZE')"
@@ -72,10 +75,6 @@ export default {
type: String,
default: '',
},
count: {
type: String,
default: '',
},
showChildCount: {
type: Boolean,
default: false,
@@ -89,6 +88,9 @@ export default {
showIcon() {
return { 'text-truncate': this.shouldTruncate };
},
isCountZero() {
return this.childItemCount === 0;
},
menuTitle() {
return this.shouldTruncate ? this.label : '';
},
@@ -134,6 +136,15 @@ $label-badge-size: var(--space-slab);
}
}
.menu-label--wrapper {
align-items: center;
display: flex;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 100%;
}
.menu-label {
flex-grow: 1;
}
@@ -174,11 +185,14 @@ $label-badge-size: var(--space-slab);
.count-view {
background: var(--s-50);
border-radius: var(--border-radius-normal);
color: var(--s-600);
font-size: var(--font-size-micro);
font-weight: var(--font-weight-bold);
margin-left: var(--space-smaller);
padding: var(--space-zero) var(--space-smaller);
line-height: var(--font-size-small);
&.text-color {
color: var(--s-600);
}
}
</style>
@@ -52,8 +52,8 @@ export default {
<style lang="scss">
.announcement-popup {
max-width: 24rem;
min-width: 16rem;
max-width: 15rem;
min-width: 10rem;
display: flex;
position: absolute;
flex-direction: column;
@@ -1,96 +0,0 @@
<template>
<div>
<div v-if="showSeparator" class="separator">
OR
</div>
<a :href="getGoogleAuthUrl()">
<button
class="button expanded button__google_login"
:class="{
// Explicit checking to ensure no other value is used
large: buttonSize === 'large',
small: buttonSize === 'small',
tiny: buttonSize === 'tiny',
}"
>
<img
src="/assets/images/auth/google.svg"
alt="Google Logo"
class="icon"
/>
<slot>{{ $t('LOGIN.OAUTH.GOOGLE_LOGIN') }}</slot>
</button>
</a>
</div>
</template>
<script>
const validButtonSizes = ['small', 'tiny', 'large'];
export default {
props: {
showSeparator: {
type: Boolean,
default: true,
},
buttonSize: {
type: String,
default: undefined,
validator: value =>
validButtonSizes.includes(value) || value === undefined,
},
},
methods: {
getGoogleAuthUrl() {
// Ideally a request to /auth/google_oauth2 should be made
// Creating the URL manually because the devise-token-auth with
// omniauth has a standing issue on redirecting the post request
// https://github.com/lynndylanhurley/devise_token_auth/issues/1466
const baseUrl =
'https://accounts.google.com/o/oauth2/auth/oauthchooseaccount';
const clientId = window.chatwootConfig.googleOAuthClientId;
const redirectUri = window.chatwootConfig.googleOAuthCallbackUrl;
const responseType = 'code';
const scope = 'email profile';
// Build the query string
const queryString = new URLSearchParams({
client_id: clientId,
redirect_uri: redirectUri,
response_type: responseType,
scope: scope,
}).toString();
// Construct the full URL
return `${baseUrl}?${queryString}`;
},
},
};
</script>
<style lang="scss" scoped>
.separator {
display: flex;
align-items: center;
margin: var(--space-two) var(--space-zero);
gap: var(--space-one);
color: var(--s-300);
font-size: var(--font-size-small);
&::before,
&::after {
content: '';
flex: 1;
height: 1px;
background: var(--s-100);
}
}
.button__google_login {
background: var(--white);
display: flex;
align-items: center;
justify-content: center;
gap: var(--space-one);
border: 1px solid var(--s-100);
color: var(--b-800);
}
</style>
@@ -11,26 +11,29 @@
{{ hrefLinkText }}
</a>
</span>
<woot-button
v-if="hasActionButton"
size="small"
variant="link"
icon="arrow-right"
color-scheme="primary"
class-names="banner-action__button"
@click="onClick"
>
{{ actionButtonLabel }}
</woot-button>
<woot-button
v-if="hasCloseButton"
size="small"
variant="link"
color-scheme="secondary"
icon="dismiss-circle"
class-names="banner-action__button"
@click="onClickClose"
/>
<div class="actions">
<woot-button
v-if="hasActionButton"
size="tiny"
icon="arrow-right"
:variant="actionButtonVariant"
color-scheme="primary"
class-names="banner-action__button"
@click="onClick"
>
{{ actionButtonLabel }}
</woot-button>
<woot-button
v-if="hasCloseButton"
size="tiny"
:color-scheme="colorScheme"
icon="dismiss-circle"
class-names="banner-action__button"
@click="onClickClose"
>
{{ $t('GENERAL_SETTINGS.DISMISS') }}
</woot-button>
</div>
</div>
</template>
@@ -53,6 +56,10 @@ export default {
type: Boolean,
default: false,
},
actionButtonVariant: {
type: String,
default: '',
},
actionButtonLabel: {
type: String,
default: '',
@@ -68,7 +75,12 @@ export default {
},
computed: {
bannerClasses() {
return [this.colorScheme];
const classList = [this.colorScheme];
if (this.hasActionButton || this.hasCloseButton) {
classList.push('has-button');
}
return classList;
},
},
methods: {
@@ -84,17 +96,26 @@ export default {
<style lang="scss" scoped>
.banner {
--x-padding: var(--space-normal);
--y-padding: var(--space-slab);
display: flex;
gap: var(--x-padding);
color: var(--white);
font-size: var(--font-size-mini);
padding: var(--space-slab) var(--space-normal);
padding: var(--y-padding) var(--x-padding);
justify-content: center;
position: sticky;
&.primary {
background: var(--w-500);
.banner-action__button {
background: var(--w-600);
border: none;
color: var(--white);
&:hover {
background: var(--w-800);
}
}
}
@@ -107,7 +128,16 @@ export default {
}
&.alert {
background: var(--r-400);
background: var(--r-500);
.banner-action__button {
background: var(--r-700);
border: none;
color: var(--white);
&:hover {
background: var(--r-800);
}
}
}
&.warning {
@@ -133,8 +163,6 @@ export default {
}
.banner-action__button {
margin: 0 var(--space-smaller);
::v-deep .button__content {
white-space: nowrap;
}
@@ -144,5 +172,11 @@ export default {
display: flex;
align-items: center;
}
.actions {
display: flex;
gap: var(--space-smaller);
right: var(--y-padding);
}
}
</style>
@@ -13,9 +13,10 @@
<div class="content-wrap">
{{ content }}
</div>
<div class="image-wrap">
<div v-if="src" class="image-wrap">
<img :src="src" class="image" :class="{ activeimage: active }" />
</div>
<slot v-else />
</div>
</template>
@@ -52,9 +53,9 @@ export default {
border: 1px solid var(--color-border);
display: flex;
flex-direction: column;
max-height: 34rem;
max-width: 38rem;
min-width: 24rem;
max-height: 21.25rem;
max-width: 23.75rem;
min-width: 15rem;
.header--wrap {
background: var(--s-50);
@@ -62,7 +63,7 @@ export default {
border-top-left-radius: var(--border-radius-normal);
border-top-right-radius: var(--border-radius-normal);
display: flex;
height: 4rem;
height: 2.5rem;
justify-content: space-between;
padding: var(--space-small);
width: 100%;
@@ -61,7 +61,7 @@ export default {
}
span {
--space-one-point-five: 1.5rem;
--space-one-point-five: 0.9375rem;
background-color: var(--white);
border-radius: 100%;
box-shadow: var(--toggle-button-box-shadow);
@@ -75,7 +75,7 @@ export default {
flex-direction: column;
overflow: auto;
margin-top: var(--space-normal);
max-height: 20rem;
max-height: 12.5rem;
}
.preview-item {
@@ -84,7 +84,7 @@ export default {
background: var(--color-background-light);
background: var(--b-50);
border-radius: var(--border-radius-normal);
width: 24rem;
width: 15rem;
padding: var(--space-smaller);
margin-bottom: var(--space-one);
}
@@ -61,7 +61,7 @@ export default {
.option-item--inbox {
display: flex;
align-items: center;
height: 3.8rem;
height: 2.375rem;
min-width: 0;
padding: 0 var(--space-smaller);
}
@@ -20,6 +20,7 @@
v-if="showSearchDropdownLabel"
:account-labels="allLabels"
:selected-labels="selectedLabels"
:allow-creation="isAdmin"
@add="addItem"
@remove="removeItem"
/>
@@ -30,8 +31,15 @@
<script>
import AddLabel from 'shared/components/ui/dropdown/AddLabel';
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
import LabelDropdown from 'shared/components/ui/label/LabelDropdown';
import { mixin as clickaway } from 'vue-clickaway';
import adminMixin from 'dashboard/mixins/isAdmin';
import {
buildHotKeys,
isEscape,
isActiveElementTypeable,
} from 'shared/helpers/KeyboardHelpers';
export default {
components: {
@@ -39,7 +47,7 @@ export default {
LabelDropdown,
},
mixins: [clickaway],
mixins: [clickaway, adminMixin, eventListenerMixins],
props: {
allLabels: {
@@ -80,6 +88,18 @@ export default {
closeDropdownLabel() {
this.showSearchDropdownLabel = false;
},
handleKeyEvents(e) {
const keyPattern = buildHotKeys(e);
if (keyPattern === 'l' && !isActiveElementTypeable(e)) {
this.toggleLabels();
e.preventDefault();
} else if (isEscape(e) && this.showSearchDropdownLabel) {
this.closeDropdownLabel();
e.preventDefault();
}
},
},
};
</script>
@@ -225,8 +225,8 @@ export default {
<style lang="scss">
.audio-wave-wrapper {
min-height: 8rem;
height: 8rem;
min-height: 5rem;
height: 5rem;
}
.video-js .vjs-control-bar {
background-color: transparent;
@@ -468,8 +468,8 @@ export default {
}
.ProseMirror-woot-style {
min-height: 8rem;
max-height: 12rem;
min-height: 5rem;
max-height: 7.5rem;
overflow: auto;
}
@@ -230,8 +230,8 @@ export default {
}
.ProseMirror-woot-style {
min-height: 8rem;
max-height: 12rem;
min-height: 5rem;
max-height: 7.5rem;
overflow: auto;
}
@@ -241,6 +241,6 @@ export default {
box-shadow: var(--shadow-large);
border-radius: var(--border-radius-normal);
border: 1px solid var(--color-border);
min-width: 40rem;
min-width: 25rem;
}
</style>
@@ -365,6 +365,6 @@ export default {
}
.icon {
font-size: 8rem;
font-size: 5rem;
}
</style>
@@ -45,6 +45,14 @@ export default {
default: () => {},
},
},
watch: {
collection() {
this.renderChart(this.collection, {
...chartOptions,
...this.chartOptions,
});
},
},
mounted() {
this.renderChart(this.collection, {
...chartOptions,
@@ -1,9 +1,21 @@
<template>
<div class="column">
<woot-modal-header :header-title="$t('FILTER.TITLE')">
<p>{{ $t('FILTER.SUBTITLE') }}</p>
<woot-modal-header :header-title="filterModalHeaderTitle">
<p>{{ filterModalSubTitle }}</p>
</woot-modal-header>
<div class="row modal-content">
<div class="column modal-content">
<div v-if="isFolderView" class="columns">
<label class="input-label" :class="{ error: !activeFolderNewName }">
{{ $t('FILTER.FOLDER_LABEL') }}
<input v-model="activeFolderNewName" type="text" class="name-input" />
<span v-if="!activeFolderNewName" class="message">
{{ $t('FILTER.EMPTY_VALUE_ERROR') }}
</span>
</label>
<label class="input-label">
{{ $t('FILTER.FOLDER_QUERY_LABEL') }}
</label>
</div>
<div class="medium-12 columns filters-wrap">
<filter-input-box
v-for="(filter, i) in appliedFilters"
@@ -42,7 +54,14 @@
<woot-button class="button clear" @click.prevent="onClose">
{{ $t('FILTER.CANCEL_BUTTON_LABEL') }}
</woot-button>
<woot-button @click="submitFilterQuery">
<woot-button
v-if="isFolderView"
:disabled="!activeFolderNewName"
@click="updateSavedCustomViews"
>
{{ $t('FILTER.UPDATE_BUTTON_LABEL') }}
</woot-button>
<woot-button v-else @click="submitFilterQuery">
{{ $t('FILTER.SUBMIT_BUTTON_LABEL') }}
</woot-button>
</div>
@@ -81,6 +100,14 @@ export default {
type: Array,
default: () => [],
},
activeFolderName: {
type: String,
default: '',
},
isFolderView: {
type: Boolean,
default: false,
},
},
validations: {
appliedFilters: {
@@ -107,6 +134,7 @@ export default {
return {
show: true,
appliedFilters: this.initialAppliedFilters,
activeFolderNewName: this.activeFolderName,
filterTypes: this.initialFilterTypes,
filterAttributeGroups,
filterGroups: [],
@@ -119,6 +147,16 @@ export default {
...mapGetters({
getAppliedConversationFilters: 'getAppliedConversationFilters',
}),
filterModalHeaderTitle() {
return !this.isFolderView
? this.$t('FILTER.TITLE')
: this.$t('FILTER.EDIT_CUSTOM_FILTER');
},
filterModalSubTitle() {
return !this.isFolderView
? this.$t('FILTER.SUBTITLE')
: this.$t('FILTER.CUSTOM_VIEWS_SUBTITLE');
},
},
mounted() {
this.setFilterAttributes();
@@ -126,7 +164,7 @@ export default {
if (this.getAppliedConversationFilters.length) {
this.appliedFilters = [];
this.appliedFilters = [...this.getAppliedConversationFilters];
} else {
} else if (!this.isFolderView) {
this.appliedFilters.push({
attribute_key: 'status',
filter_operator: 'equal_to',
@@ -177,11 +215,11 @@ export default {
if (key === 'created_at' || key === 'last_activity_at')
if (operator === 'days_before') return 'plain_text';
const type = this.filterTypes.find(filter => filter.attributeKey === key);
return type.inputType;
return type?.inputType;
},
getOperators(key) {
const type = this.filterTypes.find(filter => filter.attributeKey === key);
return type.filterOperators;
return type?.filterOperators;
},
getDropdownValues(type) {
const statusFilters = this.$t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS');
@@ -267,11 +305,30 @@ export default {
}
},
appendNewFilter() {
this.appliedFilters.push({
attribute_key: 'status',
filter_operator: 'equal_to',
values: '',
if (this.isFolderView) {
this.setQueryOperatorOnLastQuery();
} else {
this.appliedFilters.push({
attribute_key: 'status',
filter_operator: 'equal_to',
values: '',
query_operator: 'and',
});
}
},
setQueryOperatorOnLastQuery() {
const lastItemIndex = this.appliedFilters.length - 1;
this.appliedFilters[lastItemIndex] = {
...this.appliedFilters[lastItemIndex],
query_operator: 'and',
};
this.$nextTick(() => {
this.appliedFilters.push({
attribute_key: 'status',
filter_operator: 'equal_to',
values: '',
query_operator: 'and',
});
});
},
removeFilter(index) {
@@ -296,6 +353,9 @@ export default {
})),
});
},
updateSavedCustomViews() {
this.$emit('updateFolder', this.appliedFilters, this.activeFolderNewName);
},
resetFilter(index, currentFilter) {
this.appliedFilters[index].filter_operator = this.filterTypes.find(
filter => filter.attributeKey === currentFilter.attribute_key
@@ -322,4 +382,12 @@ export default {
.filter-actions {
margin-top: var(--space-normal);
}
.input-label {
margin-bottom: var(--space-smaller);
.name-input {
width: 50%;
}
}
</style>
@@ -90,7 +90,7 @@ export default {
margin-top: var(--space-smaller);
padding: var(--space-normal);
right: 0;
width: 21rem;
width: 13.125rem;
span {
font-size: var(--font-size-small);
@@ -176,23 +176,23 @@ export default {
flex-basis: 100%;
@include breakpoint(medium up) {
flex-basis: 28rem;
flex-basis: 17.5rem;
}
@include breakpoint(large up) {
flex-basis: 30em;
flex-basis: 18.75rem;
}
@include breakpoint(xlarge up) {
flex-basis: 31em;
flex-basis: 19.375rem;
}
@include breakpoint(xxlarge up) {
flex-basis: 33rem;
flex-basis: 20.625rem;
}
@include breakpoint(xxxlarge up) {
flex-basis: 40rem;
flex-basis: 25rem;
}
&::v-deep .contact--panel {
@@ -57,7 +57,6 @@ import { hasPressedAltAndOKey } from 'shared/helpers/KeyboardHelpers';
import { mapGetters } from 'vuex';
import agentMixin from '../../../mixins/agentMixin.js';
import BackButton from '../BackButton';
import differenceInHours from 'date-fns/differenceInHours';
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
import inboxMixin from 'shared/mixins/inboxMixin';
import InboxName from '../InboxName';
@@ -65,6 +64,8 @@ import MoreActions from './MoreActions';
import Thumbnail from '../Thumbnail';
import wootConstants from 'dashboard/constants/globals';
import { conversationListPageURL } from 'dashboard/helper/URLHelper';
import { conversationReopenTime } from 'dashboard/helper/snoozeHelpers';
export default {
components: {
BackButton,
@@ -125,17 +126,9 @@ export default {
snoozedDisplayText() {
const { snoozed_until: snoozedUntil } = this.currentChat;
if (snoozedUntil) {
// When the snooze is applied, it schedules the unsnooze event to next day/week 9AM.
// By that logic if the time difference is less than or equal to 24 + 9 hours we can consider it tomorrow.
const MAX_TIME_DIFFERENCE = 33;
const isSnoozedUntilTomorrow =
differenceInHours(new Date(snoozedUntil), new Date()) <=
MAX_TIME_DIFFERENCE;
return this.$t(
isSnoozedUntilTomorrow
? 'CONVERSATION.HEADER.SNOOZED_UNTIL_TOMORROW'
: 'CONVERSATION.HEADER.SNOOZED_UNTIL_NEXT_WEEK'
);
return `${this.$t(
'CONVERSATION.HEADER.SNOOZED_UNTIL'
)} ${conversationReopenTime(snoozedUntil)}`;
}
return this.$t('CONVERSATION.HEADER.SNOOZED_UNTIL_NEXT_REPLY');
},
@@ -116,7 +116,7 @@ export default {
img {
margin: var(--space-normal);
width: 10rem;
width: 6.25rem;
}
span {
@@ -25,10 +25,14 @@
<blockquote v-if="storyReply" class="story-reply-quote">
<span>{{ $t('CONVERSATION.REPLIED_TO_STORY') }}</span>
<bubble-image
v-if="!hasStoryError"
v-if="!hasImgStoryError && storyUrl"
:url="storyUrl"
@error="onStoryLoadError"
/>
<bubble-video
v-else-if="hasImgStoryError && storyUrl"
:url="storyUrl"
/>
</blockquote>
<bubble-text
v-if="data.content"
@@ -48,23 +52,12 @@
{{ $t('CONVERSATION.UPLOADING_ATTACHMENTS') }}
</span>
<div v-if="!isPending && hasAttachments">
<div v-for="attachment in data.attachments" :key="attachment.id">
<bubble-image
v-if="attachment.file_type === 'image' && !hasImageError"
:url="attachment.data_url"
<div v-for="attachment in attachments" :key="attachment.id">
<bubble-image-audio-video
v-if="isAttachmentImageVideoAudio(attachment.file_type)"
:attachment="attachment"
@error="onImageLoadError"
/>
<audio
v-else-if="attachment.file_type === 'audio'"
controls
class="skip-context-menu"
>
<source :src="`${attachment.data_url}?t=${Date.now()}`" />
</audio>
<bubble-video
v-else-if="attachment.file_type === 'video'"
:url="attachment.data_url"
/>
<bubble-location
v-else-if="attachment.file_type === 'location'"
:latitude="attachment.coordinates_lat"
@@ -140,11 +133,12 @@ import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
import BubbleActions from './bubble/Actions';
import BubbleFile from './bubble/File';
import BubbleImage from './bubble/Image';
import BubbleVideo from './bubble/Video';
import BubbleImageAudioVideo from './bubble/ImageAudioVideo';
import BubbleIntegration from './bubble/Integration.vue';
import BubbleLocation from './bubble/Location';
import BubbleMailHead from './bubble/MailHead';
import BubbleText from './bubble/Text';
import BubbleVideo from './bubble/Video.vue';
import BubbleContact from './bubble/Contact';
import Spinner from 'shared/components/Spinner';
import ContextMenu from 'dashboard/modules/conversations/components/MessageContextMenu';
@@ -161,11 +155,12 @@ export default {
BubbleActions,
BubbleFile,
BubbleImage,
BubbleVideo,
BubbleImageAudioVideo,
BubbleIntegration,
BubbleLocation,
BubbleMailHead,
BubbleText,
BubbleVideo,
BubbleContact,
ContextMenu,
Spinner,
@@ -200,10 +195,18 @@ export default {
hasImageError: false,
contextMenuPosition: {},
showBackgroundHighlight: false,
hasStoryError: false,
hasImgStoryError: false,
};
},
computed: {
attachments() {
// Here it is used to get sender and created_at for each attachment
return this.data?.attachments.map(attachment => ({
...attachment,
sender: this.data.sender || {},
created_at: this.data.created_at || '',
}));
},
shouldRenderMessage() {
return (
this.hasAttachments ||
@@ -429,12 +432,12 @@ export default {
watch: {
data() {
this.hasImageError = false;
this.hasStoryError = false;
this.hasImgStoryError = false;
},
},
mounted() {
this.hasImageError = false;
this.hasStoryError = false;
this.hasImgStoryError = false;
bus.$on(BUS_EVENTS.ON_MESSAGE_LIST_SCROLL, this.closeContextMenu);
this.setupHighlightTimer();
},
@@ -443,6 +446,9 @@ export default {
clearTimeout(this.higlightTimeout);
},
methods: {
isAttachmentImageVideoAudio(fileType) {
return ['image', 'audio', 'video'].includes(fileType);
},
hasMediaAttachment(type) {
if (this.hasAttachments && this.data.attachments.length > 0) {
const { attachments = [{}] } = this.data;
@@ -464,7 +470,7 @@ export default {
this.hasImageError = true;
},
onStoryLoadError() {
this.hasStoryError = true;
this.hasImgStoryError = true;
},
openContextMenu(e) {
const shouldSkipContextMenu =
@@ -514,7 +520,7 @@ export default {
.image,
.video {
max-width: 32rem;
max-width: 20rem;
padding: var(--space-micro);
> img,
@@ -528,13 +534,13 @@ export default {
}
}
.video {
height: 18rem;
height: 11.25rem;
}
}
&.is-image.is-text > .message-text__wrap,
&.is-video.is-text > .message-text__wrap {
max-width: 32rem;
max-width: 20rem;
padding: var(--space-small) var(--space-normal);
}
@@ -36,6 +36,7 @@
v-for="message in getReadMessages"
:key="message.id"
class="message--read ph-no-capture"
data-clarity-mask="True"
:data="message"
:is-a-tweet="isATweet"
:is-a-whatsapp-channel="isAWhatsAppChannel"
@@ -56,6 +57,7 @@
v-for="message in getUnReadMessages"
:key="message.id"
class="message--unread ph-no-capture"
data-clarity-mask="True"
:data="message"
:is-a-tweet="isATweet"
:is-a-whatsapp-channel="isAWhatsAppChannel"
@@ -277,6 +279,7 @@ export default {
if (newChat.id === oldChat.id) {
return;
}
this.fetchAllAttachmentsFromCurrentChat();
this.selectedTweetId = null;
},
},
@@ -288,6 +291,7 @@ export default {
mounted() {
this.addScrollListener();
this.fetchAllAttachmentsFromCurrentChat();
},
beforeDestroy() {
@@ -296,6 +300,9 @@ export default {
},
methods: {
fetchAllAttachmentsFromCurrentChat() {
this.$store.dispatch('fetchAllAttachments', this.currentChat.id);
},
removeBusListeners() {
bus.$off(BUS_EVENTS.SCROLL_TO_MESSAGE, this.onScrollToMessage);
bus.$off(BUS_EVENTS.SET_TWEET_REPLY, this.setSelectedTweet);
@@ -423,22 +430,22 @@ export default {
.modal-mask {
&::v-deep {
.ProseMirror-woot-style {
max-height: 40rem;
max-height: 25rem;
}
.reply-box {
border: 1px solid var(--color-border);
max-width: 120rem;
max-width: 75rem;
width: 70%;
}
.reply-box .reply-box__top {
position: relative;
min-height: 44rem;
min-height: 27.5rem;
}
.reply-box__top .input {
min-height: 44rem;
min-height: 27.5rem;
}
.emoji-dialog {

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