Compare commits

..
Author SHA1 Message Date
Shivam MishraandGitHub 97987dcda2 Merge branch 'develop' into feature/cw-2248 2023-07-20 08:41:03 +05:30
Shivam MishraandGitHub bda0234618 refactor: don't save summary to the conversations model 2023-07-20 08:40:42 +05:30
Shivam MishraandGitHub 1083d0e974 chore: Update summary.txt 2023-07-20 08:37:37 +05:30
Shivam MishraandGitHub 9f879c440e chore: revert to old prompt response template 2023-07-20 08:36:44 +05:30
Pranav Raj SandGitHub d7566c453d chore: Take the count directly rather than grouping the conversations (#7535) 2023-07-19 12:12:30 -07:00
Pranav Raj SandGitHub 25ed66edf5 feat: Update reports UI to make it better (#7544) 2023-07-19 12:12:15 -07:00
f72be94323 chore: [Snyk] Fix for 1 vulnerabilities (#7466)
Co-authored-by: snyk-bot <snyk-bot@snyk.io>
2023-07-19 21:41:55 +03:00
Vishnu NarayananandGitHub ea825d49da feat: add auditlogs for account model (#7511) 2023-07-19 23:52:34 +05:30
Sojan JoseandGitHub 1d718b92b7 chore: Fix schema disparities (#7554)
- Fixing the schema disparities that crept up during merges
- Also, fix the issue with migrate command regenerating the schema for contacts.rb model
2023-07-19 19:15:43 +03:00
Tejaswini ChileandGitHub f3f665cabb fix: Sentiment analysis queue update (#7552) 2023-07-19 20:27:20 +05:30
Vishnu NarayananandGitHub 8babf7dec3 feat: add debug gem and more make commands (#7509) 2023-07-18 13:22:02 +05:30
Tejaswini ChileandGitHub 5b480f563d feat: Sentiment model download and upload to vendor (#7526) 2023-07-18 12:17:50 +05:30
Pranav Raj SandGitHub 3a77e672f8 feat: Compute average response time of replies (#7530) 2023-07-17 11:21:31 -07:00
Shivam Mishra 811dc077fc fix: schema changes 2023-07-17 21:46:33 +05:30
Shivam Mishra 445d948af0 refactor: move event data fns to separate concern 2023-07-17 21:06:32 +05:30
Shivam Mishra d99eb145f2 refactor: update empty header method 2023-07-17 21:03:59 +05:30
Shivam Mishra 76820e2834 Merge branch 'feature/cw-2248' of github.com:chatwoot/chatwoot into feature/cw-2248 2023-07-17 17:00:28 +05:30
Shivam Mishra c862f6d419 test: remove_empty_headers 2023-07-17 16:59:52 +05:30
Shivam Mishra 16b7cabcc8 feat: strip data before sending 2023-07-17 16:51:19 +05:30
Shivam Mishra 95a547d717 feat: clean summary before rendering 2023-07-17 16:37:52 +05:30
Shivam Mishra e387f54130 feat: add method to clean empty headers 2023-07-17 16:37:26 +05:30
Shivam Mishra 1ccb4e1cc2 refactor: update format for summary 2023-07-17 16:24:36 +05:30
Shivam Mishra 23e840dabe feat: show rendered summary 2023-07-17 16:23:20 +05:30
Shivam MishraandGitHub 4b30abf44c Merge branch 'develop' into feature/cw-2248 2023-07-17 15:31:15 +05:30
Shivam MishraandGitHub 3e75ac2de5 feat: update the UX around label suggestions (#7532) 2023-07-17 15:10:56 +05:30
Shivam Mishra b65a2f7ebd feat: update summary to be a text field 2023-07-17 14:00:36 +05:30
Shivam Mishra babe1028e2 feat: add summary to conversation json 2023-07-17 13:43:41 +05:30
Shivam Mishra 8b27123f8c feat: save conversation summary to db 2023-07-17 13:41:00 +05:30
Shivam Mishra 0b37e11ebf feat: add conversation summary fields 2023-07-17 13:38:09 +05:30
91c1061214 feat: Add more AI options (#7502)
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
Co-authored-by: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com>
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2023-07-16 12:25:16 -07:00
ec65b43993 feat: Add support for API key authentication in Twilio (#7523)
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2023-07-14 19:20:54 -07:00
Shivam MishraandGitHub 1a81245987 feat: Update label suggestion visibility (#7525) 2023-07-14 19:20:03 -07:00
Pranav Raj SandGitHub 2c3ad73542 fix: Update the case for validation where SSO link is used (#7527) 2023-07-14 08:30:08 -07:00
Shivam MishraandGitHub 59b1d59574 fix: idb is not available in firefox private mode [CW-2217] (#7524) 2023-07-14 13:35:30 +05:30
Pranav Raj SandGitHub 9de2edd300 fix: Cannot read properties of undefined (reading 'filter') (#7522) 2023-07-13 15:56:28 -07:00
Pranav Raj SandGitHub 66751b71dd fix: Fix issue null is not iterable (#7521) 2023-07-13 15:47:33 -07:00
Pranav Raj SandGitHub 7e415fbee3 fix: Disable predictions if model is absent (#7520) 2023-07-13 14:40:16 -07:00
Muhsin KelothandGitHub 50a927bac2 feat: Add reply editor mode to the store (#7515) 2023-07-13 14:56:53 +05:30
Muhsin KelothandGitHub 19ff738211 feat: Add more options for AI reply suggestions (#7493) 2023-07-13 10:26:25 +05:30
Shivam MishraandGitHub 7c080fa9fa feat: label suggestion UI (#7480) 2023-07-13 09:16:09 +05:30
91e2da5e74 fix: Uncategorized article display error - HelpCenter-> Portal -> Uncategorised Article (#7504)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2023-07-12 16:34:16 -07:00
Pranav Raj SandGitHub 21691f2407 fix: Update breaking specs on sentiment and open AI integration (#7513) 2023-07-12 14:43:11 -07:00
d495411ef5 feat: Minor improvements to login UI (#7494)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2023-07-12 13:35:02 -07:00
918e1f25c2 feat: Improve summary prompt (#7510)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2023-07-12 13:09:19 -07:00
Tejaswini ChileandGitHub 10dd0ba647 feat: Sentiment Analysis (#7475) 2023-07-12 15:03:31 +05:30
Pranav Raj SandGitHub 550bea099c fix: Add appearance-none to input field for ring to work (#7507) 2023-07-11 19:07:02 -07:00
Nithin David ThomasandGitHub c7bf93ef8a fix: Adds a space after mention to improve UX (#7506) 2023-07-11 17:37:37 -07:00
Shivam MishraandGitHub ec7434d000 feat: Remove twitter (#7505) 2023-07-11 14:00:11 -07:00
Muhsin KelothandGitHub 941e7e06cf feat: Add draft messages store (#7476) 2023-07-11 18:29:42 +05:30
Pranav Raj SandGitHub 09f46aa912 chore: Update onMessage event to include conversation id (#7497) 2023-07-10 15:04:31 -07:00
Vishnu NarayananandGitHub 332ab5888c feat: Disable fb/instagram if env variables are not configured (#7498) 2023-07-10 12:28:55 -07:00
Shivam MishraandGitHub a940f4c1d2 feat: Allow label suggestions with OpenAI APIs (#7428) 2023-07-10 14:40:54 +05:30
Muhsin KelothandGitHub 5b4f92ccd2 fix: Add the option to revert the AI generated content (#7458) 2023-07-10 11:30:22 +05:30
Pranav Raj SandGitHub ad03be4529 fix: Update the index files to fix missing translations (#7484) 2023-07-07 20:11:34 -07:00
Sivin VargheseandGitHub 571e6bd0ec fix: Update broken design elements in dashboard (#7468) 2023-07-06 11:58:54 -07:00
528da1d2cf feat: Add support for dark mode in more pages
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2023-07-05 19:26:24 -07:00
Pranav Raj SandGitHub 4e8d17f017 chore: Update specs and warnings in console (#7467) 2023-07-05 18:32:55 -07:00
3054a4cb59 feat: Add support for dark mode in dashboard (#7460)
- Add config for TailwindCSS
- Enable HMR
- Add a config in LocalStorage for Dark Mode

Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2023-07-05 12:13:32 -07:00
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
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
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
769 changed files with 16209 additions and 8796 deletions
+18 -18
View File
@@ -73,7 +73,7 @@ jobs:
- run:
name: yarn
command: yarn install --cache-folder ~/.cache/yarn
command: yarn install --frozen-lockfile --cache-folder ~/.cache/yarn
# Store yarn / webpacker cache
- save_cache:
@@ -104,9 +104,8 @@ jobs:
fi
curl -L https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/6.3.0/openapi-generator-cli-6.3.0.jar > ~/tmp/openapi-generator-cli-6.3.0.jar
java -jar ~/tmp/openapi-generator-cli-6.3.0.jar validate -i swagger/swagger.json
# Database setup
- run: yarn install --check-files
- run: bundle exec rake db:create
- run: bundle exec rake db:schema:load
@@ -117,7 +116,7 @@ jobs:
- run:
name: Rubocop
command: bundle exec rubocop
# - run:
# name: Brakeman
# command: bundle exec brakeman
@@ -126,6 +125,21 @@ jobs:
name: eslint
command: yarn run eslint
# Run frontend tests
- run:
name: Run frontend tests
command: |
mkdir -p ~/tmp/test-results/frontend_specs
~/tmp/cc-test-reporter before-build
TESTFILES=$(circleci tests glob **/specs/*.spec.js | circleci tests split --split-by=timings)
yarn test:coverage --profile 10 \
--out ~/tmp/test-results/yarn.xml \
-- ${TESTFILES}
- run:
name: Code Climate Test Coverage
command: |
~/tmp/cc-test-reporter format-coverage -t lcov -o "coverage/codeclimate.frontend_$CIRCLE_NODE_INDEX.json"
# Run rails tests
- run:
name: Run backend tests
@@ -145,20 +159,6 @@ jobs:
command: |
~/tmp/cc-test-reporter format-coverage -t simplecov -o "coverage/codeclimate.$CIRCLE_NODE_INDEX.json"
- run:
name: Run frontend tests
command: |
mkdir -p ~/tmp/test-results/frontend_specs
~/tmp/cc-test-reporter before-build
TESTFILES=$(circleci tests glob **/specs/*.spec.js | circleci tests split --split-by=timings)
yarn test:coverage --profile 10 \
--out ~/tmp/test-results/yarn.xml \
-- ${TESTFILES}
- run:
name: Code Climate Test Coverage
command: |
~/tmp/cc-test-reporter format-coverage -t lcov -o "coverage/codeclimate.frontend_$CIRCLE_NODE_INDEX.json"
- persist_to_workspace:
root: coverage
paths:
+3
View File
@@ -230,3 +230,6 @@ AZURE_APP_SECRET=
## Change these values to fine tune performance
# control the concurrency setting of sidekiq
# SIDEKIQ_CONCURRENCY=10
# Sentiment analysis model file path
SENTIMENT_FILE_PATH=
+2
View File
@@ -74,3 +74,5 @@ yalc.lock
/yarn-error.log
yarn-debug.log*
.yarn-integrity
/storybook-static
+7 -3
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
@@ -108,9 +108,9 @@ gem 'elastic-apm', require: false
gem 'newrelic_rpm', require: false
gem 'newrelic-sidekiq-metrics', require: false
gem 'scout_apm', require: false
gem 'sentry-rails', require: false
gem 'sentry-rails', '>= 5.10.0', require: false
gem 'sentry-ruby', require: false
gem 'sentry-sidekiq', require: false
gem 'sentry-sidekiq', '>= 5.10.0', require: false
##-- background job processing --##
gem 'sidekiq'
@@ -165,6 +165,9 @@ gem 'omniauth'
gem 'omniauth-google-oauth2'
gem 'omniauth-rails_csrf_protection', '~> 1.0'
# Sentiment analysis
gem 'informers'
### Gems required only in specific deployment environments ###
##############################################################
@@ -210,6 +213,7 @@ group :development, :test do
gem 'bundle-audit', require: false
gem 'byebug', platform: :mri
gem 'climate_control'
gem 'debug', '~> 1.8'
gem 'factory_bot_rails'
gem 'listen'
gem 'mock_redis'
+98 -73
View File
@@ -33,70 +33,70 @@ GIT
GEM
remote: https://rubygems.org/
specs:
actioncable (7.0.5)
actionpack (= 7.0.5)
activesupport (= 7.0.5)
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.5)
actionpack (= 7.0.5)
activejob (= 7.0.5)
activerecord (= 7.0.5)
activestorage (= 7.0.5)
activesupport (= 7.0.5)
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.5)
actionpack (= 7.0.5)
actionview (= 7.0.5)
activejob (= 7.0.5)
activesupport (= 7.0.5)
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.5)
actionview (= 7.0.5)
activesupport (= 7.0.5)
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.5)
actionpack (= 7.0.5)
activerecord (= 7.0.5)
activestorage (= 7.0.5)
activesupport (= 7.0.5)
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.5)
activesupport (= 7.0.5)
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.5)
activesupport (= 7.0.5)
activejob (7.0.5.1)
activesupport (= 7.0.5.1)
globalid (>= 0.3.6)
activemodel (7.0.5)
activesupport (= 7.0.5)
activerecord (7.0.5)
activemodel (= 7.0.5)
activesupport (= 7.0.5)
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.5)
actionpack (= 7.0.5)
activejob (= 7.0.5)
activerecord (= 7.0.5)
activesupport (= 7.0.5)
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.5)
activesupport (7.0.5.1)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
@@ -145,6 +145,7 @@ GEM
statsd-ruby (~> 1.1)
bcrypt (3.1.18)
bindex (0.8.1)
blingfire (0.1.8)
bootsnap (1.16.0)
msgpack (~> 1.2)
brakeman (5.4.1)
@@ -163,7 +164,7 @@ GEM
coderay (1.1.3)
commonmarker (0.23.9)
concurrent-ruby (1.2.2)
connection_pool (2.4.0)
connection_pool (2.4.1)
crack (0.4.5)
rexml
crass (1.0.6)
@@ -183,6 +184,9 @@ GEM
libddwaf (~> 1.8.2.0.0)
msgpack
debase-ruby_core_source (3.2.0)
debug (1.8.0)
irb (>= 1.5.0)
reline (>= 0.3.1)
declarative (0.0.20)
devise (4.9.2)
bcrypt (~> 3.0)
@@ -361,6 +365,13 @@ GEM
image_processing (1.12.2)
mini_magick (>= 4.9.5, < 5)
ruby-vips (>= 2.0.17, < 3)
informers (0.2.0)
blingfire (>= 0.1.7)
numo-narray
onnxruntime (>= 0.5.1)
io-console (0.6.0)
irb (1.7.2)
reline (>= 0.3.6)
jbuilder (2.11.5)
actionview (>= 5.0.0)
activesupport (>= 5.0.0)
@@ -441,7 +452,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)
@@ -450,7 +461,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)
@@ -465,15 +476,16 @@ GEM
sidekiq
newrelic_rpm (8.16.0)
nio4r (2.5.9)
nokogiri (1.15.2)
nokogiri (1.15.3)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
nokogiri (1.15.2-arm64-darwin)
nokogiri (1.15.3-arm64-darwin)
racc (~> 1.4)
nokogiri (1.15.2-x86_64-darwin)
nokogiri (1.15.3-x86_64-darwin)
racc (~> 1.4)
nokogiri (1.15.2-x86_64-linux)
nokogiri (1.15.3-x86_64-linux)
racc (~> 1.4)
numo-narray (0.9.2.1)
oauth (1.1.0)
oauth-tty (~> 1.0, >= 1.0.1)
snaky_hash (~> 2.0)
@@ -502,6 +514,14 @@ GEM
omniauth-rails_csrf_protection (1.0.1)
actionpack (>= 4.2)
omniauth (~> 2.0)
onnxruntime (0.7.6)
ffi
onnxruntime (0.7.6-arm64-darwin)
ffi
onnxruntime (0.7.6-x86_64-darwin)
ffi
onnxruntime (0.7.6-x86_64-linux)
ffi
openssl (3.1.0)
orm_adapter (0.5.0)
os (1.1.4)
@@ -525,7 +545,7 @@ GEM
pundit (2.3.0)
activesupport (>= 3.0.0)
raabro (1.4.0)
racc (1.7.0)
racc (1.7.1)
rack (2.2.7)
rack-attack (6.6.1)
rack (>= 1.0, < 3)
@@ -540,29 +560,30 @@ GEM
rack-test (2.1.0)
rack (>= 1.3)
rack-timeout (0.6.3)
rails (7.0.5)
actioncable (= 7.0.5)
actionmailbox (= 7.0.5)
actionmailer (= 7.0.5)
actionpack (= 7.0.5)
actiontext (= 7.0.5)
actionview (= 7.0.5)
activejob (= 7.0.5)
activemodel (= 7.0.5)
activerecord (= 7.0.5)
activestorage (= 7.0.5)
activesupport (= 7.0.5)
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.5)
rails-dom-testing (2.0.3)
activesupport (>= 4.2.0)
railties (= 7.0.5.1)
rails-dom-testing (2.1.1)
activesupport (>= 5.0.0)
minitest
nokogiri (>= 1.6)
rails-html-sanitizer (1.6.0)
loofah (~> 2.21)
nokogiri (~> 1.14)
railties (7.0.5)
actionpack (= 7.0.5)
activesupport (= 7.0.5)
railties (7.0.5.1)
actionpack (= 7.0.5.1)
activesupport (= 7.0.5.1)
method_source
rake (>= 12.2)
thor (~> 1.0)
@@ -579,6 +600,8 @@ GEM
redis-namespace (1.10.0)
redis (>= 4)
regexp_parser (2.8.0)
reline (0.3.6)
io-console (~> 0.5)
representable (3.2.0)
declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0)
@@ -669,18 +692,18 @@ GEM
activesupport (>= 4)
selectize-rails (0.12.6)
semantic_range (3.0.0)
sentry-rails (5.9.0)
sentry-rails (5.10.0)
railties (>= 5.0)
sentry-ruby (~> 5.9.0)
sentry-ruby (5.9.0)
sentry-ruby (~> 5.10.0)
sentry-ruby (5.10.0)
concurrent-ruby (~> 1.0, >= 1.0.2)
sentry-sidekiq (5.9.0)
sentry-ruby (~> 5.9.0)
sentry-sidekiq (5.10.0)
sentry-ruby (~> 5.10.0)
sidekiq (>= 3.0)
sexp_processor (4.17.0)
shoulda-matchers (5.3.0)
activesupport (>= 5.2.0)
sidekiq (7.1.0)
sidekiq (7.1.2)
concurrent-ruby (< 2)
connection_pool (>= 2.3.0)
rack (>= 2.2.4)
@@ -731,7 +754,7 @@ GEM
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)
@@ -821,6 +844,7 @@ DEPENDENCIES
cypress-on-rails
database_cleaner
ddtrace
debug (~> 1.8)
devise
devise-secure_password!
devise_token_auth
@@ -846,6 +870,7 @@ DEPENDENCIES
hashie
html2text!
image_processing
informers
jbuilder
json_refs
json_schemer
@@ -875,7 +900,7 @@ DEPENDENCIES
rack-cors
rack-mini-profiler
rack-timeout
rails (~> 7)
rails (~> 7.0.5.1)
redis
redis-namespace
responders
@@ -889,9 +914,9 @@ DEPENDENCIES
scout_apm
scss_lint
seed_dump
sentry-rails
sentry-rails (>= 5.10.0)
sentry-ruby
sentry-sidekiq
sentry-sidekiq (>= 5.10.0)
shoulda-matchers
sidekiq
sidekiq-cron
+15 -1
View File
@@ -30,9 +30,23 @@ burn:
bundle && yarn
run:
@if [ -f ./.overmind.sock ]; then \
echo "Overmind is already running. Use 'make force_run' to start a new instance."; \
else \
overmind start -f Procfile.dev; \
fi
force_run:
rm -f ./.overmind.sock
overmind start -f Procfile.dev
debug:
overmind connect backend
debug_worker:
overmind connect worker
docker:
docker build -t $(APP_NAME) -f ./docker/Dockerfile .
.PHONY: setup db_create db_migrate db_seed db console server burn docker run
.PHONY: setup db_create db_migrate db_seed db console server burn docker run force_run debug debug_worker
+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;
+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)
+7 -7
View File
@@ -33,18 +33,18 @@ class V2::ReportBuilder
def summary
{
conversations_count: conversations_count.values.sum,
incoming_messages_count: incoming_messages_count.values.sum,
outgoing_messages_count: outgoing_messages_count.values.sum,
conversations_count: conversations.count,
incoming_messages_count: incoming_messages.count,
outgoing_messages_count: outgoing_messages.count,
avg_first_response_time: avg_first_response_time_summary,
avg_resolution_time: avg_resolution_time_summary,
resolutions_count: resolutions_count.values.sum
resolutions_count: resolutions.count
}
end
def conversation_metrics
if params[:type].equal?(:account)
conversations
live_conversations
else
agent_metrics.sort_by { |hash| hash[:metric][:open] }.reverse
end
@@ -89,12 +89,12 @@ class V2::ReportBuilder
email: @user.email,
thumbnail: @user.avatar_url,
availability: account_user.availability_status,
metric: conversations
metric: live_conversations
}
end
end
def conversations
def live_conversations
@open_conversations = scope.conversations.where(account_id: @account.id).open
metric = {
open: @open_conversations.count,
@@ -18,7 +18,11 @@ class Api::V1::Accounts::Channels::TwilioChannelsController < Api::V1::Accounts:
end
def authenticate_twilio
client = Twilio::REST::Client.new(permitted_params[:account_sid], permitted_params[:auth_token])
client = if permitted_params[:api_key_sid].present?
Twilio::REST::Client.new(permitted_params[:api_key_sid], permitted_params[:auth_token], permitted_params[:account_sid])
else
Twilio::REST::Client.new(permitted_params[:account_sid], permitted_params[:auth_token])
end
client.messages.list(limit: 1)
end
@@ -40,6 +44,7 @@ class Api::V1::Accounts::Channels::TwilioChannelsController < Api::V1::Accounts:
@twilio_channel = Current.account.twilio_sms.create!(
account_sid: permitted_params[:account_sid],
auth_token: permitted_params[:auth_token],
api_key_sid: permitted_params[:api_key_sid],
messaging_service_sid: permitted_params[:messaging_service_sid].presence,
phone_number: phone_number,
medium: medium
@@ -52,7 +57,7 @@ class Api::V1::Accounts::Channels::TwilioChannelsController < Api::V1::Accounts:
def permitted_params
params.require(:twilio_channel).permit(
:account_id, :messaging_service_sid, :phone_number, :account_sid, :auth_token, :name, :medium
:account_id, :messaging_service_sid, :phone_number, :account_sid, :auth_token, :name, :medium, :api_key_sid
)
end
end
@@ -63,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
@@ -124,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 = [])
@@ -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])
+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
+2 -1
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
+31 -8
View File
@@ -17,21 +17,36 @@ module ReportHelper
end
def conversations_count
(get_grouped_values scope.conversations.where(account_id: account.id)).count
(get_grouped_values conversations).count
end
def incoming_messages_count
(get_grouped_values scope.messages.where(account_id: account.id).incoming.unscope(:order)).count
(get_grouped_values incoming_messages).count
end
def outgoing_messages_count
(get_grouped_values scope.messages.where(account_id: account.id).outgoing.unscope(:order)).count
(get_grouped_values outgoing_messages).count
end
def resolutions_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
(get_grouped_values resolutions).count
end
def conversations
scope.conversations.where(account_id: account.id, created_at: range)
end
def incoming_messages
scope.messages.where(account_id: account.id, created_at: range).incoming.unscope(:order)
end
def outgoing_messages
scope.messages.where(account_id: account.id, created_at: range).outgoing.unscope(:order)
end
def resolutions
scope.reporting_events.joins(:conversation).select(:conversation_id).where(account_id: account.id, name: :conversation_resolved,
conversations: { status: :resolved }, created_at: range).distinct
end
def avg_first_response_time
@@ -51,7 +66,11 @@ module ReportHelper
def avg_resolution_time_summary
reporting_events = scope.reporting_events
.where(name: 'conversation_resolved', account_id: account.id, created_at: range)
avg_rt = params[:business_hours] ? reporting_events.average(:value_in_business_hours) : reporting_events.average(:value)
avg_rt = if params[:business_hours].present?
reporting_events.average(:value_in_business_hours)
else
reporting_events.average(:value)
end
return 0 if avg_rt.blank?
@@ -61,7 +80,11 @@ module ReportHelper
def avg_first_response_time_summary
reporting_events = scope.reporting_events
.where(name: 'first_response', account_id: account.id, created_at: range)
avg_frt = params[:business_hours] ? reporting_events.average(:value_in_business_hours) : reporting_events.average(:value)
avg_frt = if params[:business_hours].present?
reporting_events.average(:value_in_business_hours)
else
reporting_events.average(:value)
end
return 0 if avg_frt.blank?
+30 -1
View File
@@ -3,7 +3,8 @@
v-if="!authUIFlags.isFetching"
id="app"
class="app-wrapper app-root"
:class="{ 'app-rtl--wrapper': isRTLView }"
:class="{ 'app-rtl--wrapper': isRTLView, dark: theme === 'dark' }"
:dir="isRTLView ? 'rtl' : 'ltr'"
>
<update-banner :latest-chatwoot-version="latestChatwootVersion" />
<template v-if="!accountUIFlags.isFetchingItem && currentAccountId">
@@ -34,10 +35,12 @@ import PaymentPendingBanner from './components/app/PaymentPendingBanner.vue';
import vueActionCable from './helper/actionCable';
import WootSnackbarBox from './components/SnackbarContainer';
import rtlMixin from 'shared/mixins/rtlMixin';
import { LocalStorage } from 'shared/helpers/localStorage';
import {
registerSubscription,
verifyServiceWorkerExistence,
} from './helper/pushHelper';
import { LOCAL_STORAGE_KEYS } from './constants/localStorage';
export default {
name: 'App',
@@ -58,6 +61,7 @@ export default {
return {
showAddAccountModal: false,
latestChatwootVersion: null,
theme: 'light',
};
},
@@ -89,9 +93,34 @@ export default {
},
},
mounted() {
this.initializeColorTheme();
this.listenToThemeChanges();
this.setLocale(window.chatwootConfig.selectedLocale);
},
methods: {
initializeColorTheme() {
this.setColorTheme(
window.matchMedia('(prefers-color-scheme: dark)').matches
);
},
setColorTheme(isOSOnDarkMode) {
const selectedColorScheme =
LocalStorage.get(LOCAL_STORAGE_KEYS.COLOR_SCHEME) || 'light';
if (
(selectedColorScheme === 'auto' && isOSOnDarkMode) ||
selectedColorScheme === 'dark'
) {
this.theme = 'dark';
document.body.classList.add('dark');
} else {
this.theme = 'light ';
document.body.classList.remove('dark');
}
},
listenToThemeChanges() {
const mql = window.matchMedia('(prefers-color-scheme: dark)');
mql.onchange = e => this.setColorTheme(e.matches);
},
setLocale(locale) {
this.$root.$i18n.locale = locale;
},
@@ -18,6 +18,10 @@ class CacheEnabledApiClient extends ApiClient {
return this.getFromCache();
}
return this.getFromNetwork();
}
getFromNetwork() {
return axios.get(this.url);
}
@@ -32,7 +36,12 @@ class CacheEnabledApiClient extends ApiClient {
}
async getFromCache() {
await this.dataManager.initDb();
try {
// IDB is not supported in Firefox private mode: https://bugzilla.mozilla.org/show_bug.cgi?id=781982
await this.dataManager.initDb();
} catch {
return this.getFromNetwork();
}
const { data } = await axios.get(
`/api/v1/accounts/${this.accountIdFromRoute}/cache_keys`
@@ -55,16 +64,22 @@ class CacheEnabledApiClient extends ApiClient {
}
async refetchAndCommit(newKey = null) {
await this.dataManager.initDb();
const response = await axios.get(this.url);
this.dataManager.replace({
modelName: this.cacheModelName,
data: this.extractDataFromResponse(response),
});
const response = await this.getFromNetwork();
await this.dataManager.setCacheKeys({
[this.cacheModelName]: newKey,
});
try {
await this.dataManager.initDb();
this.dataManager.replace({
modelName: this.cacheModelName,
data: this.extractDataFromResponse(response),
});
await this.dataManager.setCacheKeys({
[this.cacheModelName]: newKey,
});
} catch {
// Ignore error
}
return response;
}
-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,
@@ -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,
}),
});
@@ -2,18 +2,62 @@
import ApiClient from '../ApiClient';
/**
* Represents the data object for a OpenAI hook.
* @typedef {Object} ConversationMessageData
* @property {string} [tone] - The tone of the message.
* @property {string} [content] - The content of the message.
* @property {string} [conversation_display_id] - The display ID of the conversation (optional).
*/
/**
* A client for the OpenAI API.
* @extends ApiClient
*/
class OpenAIAPI extends ApiClient {
/**
* Creates a new OpenAIAPI instance.
*/
constructor() {
super('integrations', { accountScoped: true });
/**
* The conversation events supported by the API.
* @type {string[]}
*/
this.conversation_events = [
'summarize',
'reply_suggestion',
'label_suggestion',
];
/**
* The message events supported by the API.
* @type {string[]}
*/
this.message_events = ['rephrase'];
}
/**
* Processes an event using the OpenAI API.
* @param {Object} options - The options for the event.
* @param {string} [options.type='rephrase'] - The type of event to process.
* @param {string} [options.content] - The content of the event.
* @param {string} [options.tone] - The tone of the event.
* @param {string} [options.conversationId] - The ID of the conversation to process the event for.
* @param {string} options.hookId - The ID of the hook to use for processing the event.
* @returns {Promise} A promise that resolves with the result of the event processing.
*/
processEvent({ type = 'rephrase', content, tone, conversationId, hookId }) {
/**
* @type {ConversationMessageData}
*/
let data = {
tone,
content,
};
if (type === 'reply_suggestion' || type === 'summarize') {
if (this.conversation_events.includes(type)) {
data = {
conversation_display_id: conversationId,
};
@@ -26,7 +26,7 @@
border-radius: var(--border-radius-normal);
box-shadow: none;
display: flex;
height: 4rem;
height: 2.5rem;
}
.mx-input:disabled,
@@ -8,7 +8,7 @@
}
select {
height: 4.0rem;
height: 2.5rem;
}
.card {
@@ -20,15 +20,6 @@ select {
margin-left: var(--space-normal);
}
.tooltip {
background-color: var(--black-transparent);
border-radius: $space-smaller;
font-size: $font-size-mini;
max-width: var(--space-giga);
padding: $space-smaller $space-small;
z-index: 999;
}
code {
border: 0;
font-family: 'ui-monospace', 'SFMono-Regular', 'Menlo', 'Monaco', 'Consolas',
@@ -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;
@@ -1,5 +1,5 @@
.bg-light {
@include background-light;
@apply bg-slate-25 dark:bg-slate-800;
}
.flex-center {
@@ -22,43 +22,43 @@ $spinner-before-border-color: rgba(255, 255, 255, 0.7);
}
@mixin border-normal() {
border: 1px solid $color-border;
@apply border border-slate-50 dark:border-slate-700;
}
@mixin border-normal-left() {
border-left: 1px solid $color-border;
@apply border-l border-slate-50 dark:border-slate-700;
}
@mixin border-normal-top() {
border-top: 1px solid $color-border;
@apply border-t border-slate-50 dark:border-slate-700;
}
@mixin border-normal-right() {
border-right: 1px solid $color-border;
@apply border-r border-slate-50 dark:border-slate-700;
}
@mixin border-normal-bottom() {
border-bottom: 1px solid $color-border;
@apply border-b border-slate-50 dark:border-slate-700;
}
@mixin border-light() {
border: 1px solid $color-border-light;
@apply border border-slate-25 dark:border-slate-700;
}
@mixin border-light-left() {
border-left: 1px solid $color-border-light;
@apply border-l border-slate-25 dark:border-slate-700;
}
@mixin border-light-top() {
border-top: 1px solid $color-border-light;
@apply border-t border-slate-25 dark:border-slate-700;
}
@mixin border-light-right() {
border-right: 1px solid $color-border-light;
@apply border-r border-slate-25 dark:border-slate-700;
}
@mixin border-light-bottom() {
border-bottom: 1px solid $color-border-light;
@apply border-b border-slate-25 dark:border-slate-700;
}
// background
@@ -67,11 +67,11 @@ $spinner-before-border-color: rgba(255, 255, 255, 0.7);
}
@mixin background-light() {
background: $color-background-light;
@apply bg-slate-50 dark:bg-slate-800;
}
@mixin background-white() {
background: $color-white;
@apply bg-white dark:bg-slate-900;
}
// input form
@@ -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;
+1 -61
View File
@@ -3,9 +3,6 @@
// Primary sidebar
.primary--sidebar {
border-left: 1px solid var(--s-50);
border-right: 0;
.options-menu.dropdown-pane {
right: var(--space-smaller);
@@ -20,63 +17,6 @@
}
}
// Secondary sidebar
.secondary-sidebar {
.secondary-menu {
border-left: 1px solid var(--s-50);
border-right: 0;
.nested.vertical.menu {
.badge--icon {
margin-left: var(--space-smaller);
margin-right: unset;
}
.menu-label {
text-align: right;
}
}
.secondary-menu--icon {
margin-left: var(--space-smaller);
margin-right: unset;
}
.account-context--group .account-context--switch-group {
--overlay-shadow: linear-gradient(
to left,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 1) 50%
);
background-image: var(--overlay-shadow);
}
// Help center sidebar
.sidebar-header--wrap .header-title--wrap {
margin-left: unset;
margin-right: var(--space-small);
}
}
}
// Woot button
.button {
.icon--emoji + .button__content {
padding-left: 0;
padding-right: var(--space-small);
}
.icon--font + .button__content {
padding-left: 0;
padding-right: var(--space-small);
}
.icon + .button__content {
padding-left: 0;
padding-right: var(--space-small);
}
}
// Settings header
.settings-header {
.header--icon {
@@ -439,7 +379,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;
}
@@ -43,17 +43,13 @@
}
.border-right {
border-right: 1px solid var(--color-border);
@apply border-r border-slate-50 dark:border-slate-700;
}
.border-left {
border-left: 1px solid var(--color-border);
}
.bg-white {
background-color: var(--white);
}
.text-ellipsis {
overflow: hidden;
text-overflow: ellipsis;
@@ -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,8 @@
@import 'shared/assets/fonts/inter';
@import 'tailwindcss/base';
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';
@import 'shared/assets/fonts/plus-jakarta';
@import 'shared/assets/stylesheets/animations';
@import 'shared/assets/stylesheets/colors';
@import 'shared/assets/stylesheets/spacing';
@@ -67,3 +71,8 @@
@import 'plugins/dropdown';
@import '~shared/assets/stylesheets/ionicons';
@import 'utility-helpers';
.tooltip {
@apply bg-slate-900 text-white py-1 px-2 z-40 text-xs rounded-md dark:bg-slate-200 dark:text-slate-900;
}
@@ -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';
@@ -4,10 +4,8 @@
// Conversation header - Light BG
.settings-header {
@include background-white;
@include flex;
@include flex-align($x: justify, $y: middle);
border-bottom: 1px solid var(--s-50);
height: $header-height;
min-height: $header-height;
padding: $space-small $space-normal;
@@ -27,7 +25,7 @@
.wizard-box {
.item {
@include background-light;
// @include background-light;
cursor: pointer;
padding: $space-normal $space-normal $space-normal $space-medium;
@@ -128,8 +126,8 @@
}
.wizard-body {
@include background-white;
@include border-light;
@include background-white;
@include full-height();
padding: $space-medium;
@@ -8,8 +8,8 @@
.integration--image {
display: flex;
height: 10rem;
width: 10rem;
height: 6.25rem;
width: 6.25rem;
img {
max-width: 100%;
@@ -1,207 +1,201 @@
$default-button-height: 4.0rem;
.button {
align-items: center;
display: inline-flex;
height: $default-button-height;
margin-bottom: 0;
@apply items-center inline-flex h-10 mb-0;
.button__content {
width: 100%;
@apply w-full;
img,
svg {
@apply inline-block;
}
}
.spinner {
padding: 0 var(--space-small);
@apply px-2 py-0;
}
.icon--emoji+.button__content {
padding-left: var(--space-small);
.icon--emoji + .button__content {
@apply pl-2 rtl:pr-2 rtl:pl-0;
}
.icon--font+.button__content {
padding-left: var(--space-small);
.icon--font + .button__content {
@apply pl-2 rtl:pr-2 rtl:pl-0;
}
// @TODDO - Remove after moving all buttons to woot-button
.icon+.button__content {
padding-left: var(--space-small);
width: auto;
.icon + .button__content {
@apply pl-2 w-auto rtl:pr-2 rtl:pl-0;
}
&.expanded {
display: flex;
justify-content: center;
@apply flex justify-center text-center;
}
&.round {
border-radius: $space-larger;
@apply rounded-full;
}
// @TODO Use with link
&.compact {
padding-bottom: 0;
padding-top: 0;
@apply pb-0 pt-0;
}
&.hollow {
border-color: var(--s-200);
color: var(--w-700);
@apply border border-slate-200 text-woot-700 dark:text-woot-100 hover:bg-woot-50 dark:hover:bg-woot-900;
&.secondary {
border-color: var(--s-200);
color: var(--s-700);
@apply text-slate-700 dark:text-slate-100 hover:bg-slate-50 dark:hover:bg-slate-700;
}
&.success {
border-color: var(--s-200);
color: var(--g-700);
@apply text-green-700 dark:text-green-100 hover:bg-green-50 dark:hover:bg-green-800;
}
&.alert {
border-color: var(--s-200);
color: var(--r-700);
@apply text-red-700 dark:text-red-100 hover:bg-red-50 dark:hover:bg-red-800;
}
&.warning {
border-color: var(--s-200);
color: var(--y-700);
@apply text-yellow-700 dark:text-yellow-100 hover:bg-yellow-50 dark:hover:bg-yellow-800;
}
&:hover {
background: var(--s-75);
border-color: var(--s-100);
@apply bg-slate-75 dark:bg-slate-900 border-slate-100 dark:border-slate-700;
&.secondary {
border-color: var(--s-100);
@apply border-slate-100 dark:border-slate-700;
}
&.success {
border-color: var(--s-100);
@apply border-slate-100 dark:border-slate-700;
}
&.alert {
border-color: var(--s-100);
@apply border-slate-100 dark:border-slate-700;
}
&.warning {
border-color: var(--s-100);
@apply border-slate-100 dark:border-slate-700;
}
}
}
// Smooth style
&.smooth {
@include button-style(var(--w-50), var(--w-100), var(--w-700));
@apply bg-woot-50 dark:bg-woot-800 text-woot-700 dark:text-woot-100 hover:text-woot-700 dark:hover:text-woot-700 hover:bg-woot-100 dark:hover:bg-woot-900;
&.secondary {
@include button-style(var(--s-50), var(--s-100), var(--s-700));
@apply bg-slate-50 dark:bg-slate-700 text-slate-700 dark:text-slate-100 hover:bg-slate-100 dark:hover:bg-slate-800;
}
&.success {
@include button-style(var(--g-50), var(--g-100), var(--g-700));
@apply bg-green-50 dark:bg-green-700 text-green-700 dark:text-green-100 hover:bg-green-100 dark:hover:bg-green-800;
}
&.alert {
@include button-style(var(--r-50), var(--r-100), var(--r-700));
@apply bg-red-50 dark:bg-red-700 dark:bg-opacity-50 text-red-700 dark:text-red-100 hover:bg-red-100 dark:hover:bg-red-800 dark:hover:bg-opacity-30;
}
&.warning {
@include button-style(var(--y-100), var(--y-200), var(--y-700));
@apply bg-yellow-100 dark:bg-yellow-100 text-yellow-700 dark:text-yellow-100 hover:bg-yellow-200 dark:hover:bg-yellow-200;
}
}
&.clear {
color: var(--w-700);
@apply text-woot-500 dark:text-woot-500;
&.secondary {
color: var(--s-700);
@apply text-slate-700 dark:text-slate-100;
}
&.success {
color: var(--g-700);
@apply text-green-700 dark:text-green-100;
}
&.alert {
color: var(--r-700);
@apply text-red-700 dark:text-red-100;
}
&.warning {
color: var(--y-700);
@apply text-yellow-700 dark:text-yellow-100;
}
&:hover {
background: var(--w-50);
@apply hover:bg-woot-50 dark:hover:bg-woot-300;
&.secondary {
background: var(--s-50);
@apply hover:bg-slate-50 dark:hover:bg-slate-700;
}
&.success {
background: var(--g-50);
@apply hover:bg-green-50 dark:hover:bg-green-800;
}
&.alert {
background: var(--r-50);
@apply hover:bg-red-50 dark:hover:bg-red-800;
}
&.warning {
background: var(--y-50);
@apply hover:bg-yellow-50 dark:hover:bg-yellow-800;
}
}
&:active {
&.secondary {
@apply active:bg-slate-100 dark:active:bg-slate-900;
}
}
&:focus {
&.secondary {
@apply focus:bg-slate-50 dark:focus:bg-slate-700;
}
}
}
// Sizes
&.tiny {
height: var(--space-medium);
@apply h-6;
.icon+.button__content {
padding-left: var(--space-micro);
.icon + .button__content {
@apply pl-1 rtl:pr-1 rtl:pl-0;
}
}
&.small {
height: var(--space-large);
padding-bottom: var(--space-smaller);
padding-top: var(--space-smaller);
@apply h-8 pb-1 pt-1;
.icon+.button__content {
padding-left: var(--space-smaller);
.icon + .button__content {
@apply pl-1 rtl:pr-1 rtl:pl-0;
}
}
&.large {
height: var(--space-larger);
@apply h-12;
}
&.button--only-icon {
justify-content: center;
padding-left: 0;
padding-right: 0;
width: $default-button-height;
@apply justify-center pl-0 pr-0 w-10;
&.tiny {
width: var(--space-medium);
@apply w-6;
}
&.small {
width: var(--space-large);
@apply w-8;
}
&.large {
width: var(--space-larger);
@apply w-12;
}
}
&.link {
height: auto;
margin: 0;
padding: 0;
@apply h-auto m-0 p-0;
&:hover {
text-decoration: underline;
@apply underline;
}
}
}
@@ -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;
}
@@ -59,11 +59,6 @@ $resolve-button-width: 13.2rem;
flex-direction: row;
flex-grow: 1;
justify-content: flex-end;
margin-top: var(--space-small);
@include breakpoint(medium up) {
margin-top: 0;
}
&.has-open-sidebar {
justify-content: flex-end;
@@ -12,9 +12,7 @@
.conversation {
@include flex;
@include flex-shrink;
border-bottom: 1px solid transparent;
border-left: var(--space-micro) solid transparent;
border-top: 1px solid transparent;
cursor: pointer;
padding: 0 var(--space-normal);
position: relative;
@@ -22,35 +20,11 @@
&.active {
animation: left-shift-animation 0.25s $swift-ease-out-function;
background: var(--color-background);
border-bottom-color: var(--color-border-light);
border-left-color: var(--color-woot);
border-top-color: var(--color-border-light);
.conversation--details {
border-top-color: transparent;
}
+ .conversation .conversation--details {
border-top-color: transparent;
}
}
&:first-child {
.conversation--details {
border-top-color: transparent;
}
}
&:last-child {
.conversation--details {
border-bottom-color: var(--color-border-light);
}
}
.conversation--details {
@include border-light-bottom;
@include border-light-top;
border-bottom-color: transparent;
padding: var(--space-slab) 0;
}
@@ -81,7 +55,12 @@
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 27rem;
width: 16.875rem;
img,
svg {
display: inline-block;
}
}
.conversation--meta {
@@ -273,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%);
@@ -390,3 +390,42 @@
margin-bottom: 0;
}
}
.conversation-sidebar-wrap {
flex: 0 0;
flex-basis: 100%;
height: auto;
overflow: auto;
z-index: var(--z-index-low);
@include breakpoint(medium up) {
flex-basis: 17.5rem;
}
@include breakpoint(large up) {
flex-basis: 18.75rem;
}
@include breakpoint(xlarge up) {
flex-basis: 19.375rem;
}
@include breakpoint(xxlarge up) {
flex-basis: 20.625rem;
}
@include breakpoint(xxxlarge up) {
flex-basis: 25rem;
}
}
.activity-wrap {
.message-text--metadata {
.time {
@include breakpoint(xlarge up) {
margin-left: var(--space-small);
}
}
}
}
@@ -61,3 +61,7 @@ input {
border-color: var(--r-400);
}
}
label {
@apply dark:text-slate-300;
}
@@ -27,23 +27,22 @@
padding: $space-large $space-large $zero;
img {
max-height: 6rem;
max-height: 3.75rem;
}
}
.modal-container {
@include normal-shadow;
background-color: $color-white;
border-radius: $space-smaller;
max-height: 100%;
overflow: auto;
position: relative;
width: 60rem;
width: 37.5rem;
&.medium {
max-width: 80%;
width: 90rem;
width: 56.25rem;
}
.content-box {
@@ -1,6 +1,6 @@
.reply-box {
transition: box-shadow 0.35s $swift-ease-out-function,
height 2s $swift-ease-out-function;
height 2s $swift-ease-out-function;
&.is-focused {
box-shadow: var(--shadow);
@@ -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;
}
@@ -58,31 +58,3 @@
text-transform: capitalize;
}
}
.report-bar {
@include background-white;
@include border-light;
margin: var(--space-minus-micro) 0;
padding: var(--space-small) var(--space-medium);
.chart-container {
@include flex;
@include flex-align(center, middle);
flex-direction: column;
div {
width: 100%;
}
.empty-state {
color: $color-gray;
font-size: var(--font-size-default);
margin: var(--space-jumbo);
}
.business-hours {
margin: var(--space-normal);
text-align: center;
}
}
}
@@ -22,18 +22,6 @@
margin: 0 var(--space-small);
}
.business-hours {
align-items: center;
display: flex;
justify-content: flex-start;
margin-left: auto;
padding-right: var(--space-normal);
}
.business-hours-text {
font-size: var(--font-size-small);
margin: 0 var(--space-small);
}
.switch {
margin-bottom: var(--space-zero);
@@ -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;
@@ -1,18 +1,3 @@
.loading-state {
padding: $space-jumbo $space-smaller;
.message {
color: $color-gray;
display: block;
text-align: center;
width: 100%;
}
.spinner {
float: none;
top: -$space-smaller;
}
}
// EMPTY STATES
.empty-state {
@@ -4,15 +4,13 @@ table {
thead {
th {
font-weight: var(--font-weight-bold);
text-align: left;
text-transform: uppercase;
@apply font-medium text-left uppercase text-slate-900 dark:text-slate-200;
}
}
tbody {
tr {
border-bottom: 1px solid var(--color-border-light);
@apply border-b border-slate-50 dark:border-slate-700;
}
td {
@@ -43,15 +41,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 {
@@ -126,27 +126,28 @@
class="conversations-list"
:class="{ 'is-context-menu-open': isContextMenuOpen }"
>
<conversation-card
v-for="chat in conversationList"
:key="chat.id"
:active-label="label"
:team-id="teamId"
:folders-id="foldersId"
:chat="chat"
:conversation-type="conversationType"
:show-assignee="showAssigneeInConversationCard"
:selected="isConversationSelected(chat.id)"
@select-conversation="selectConversation"
@de-select-conversation="deSelectConversation"
@assign-agent="onAssignAgent"
@assign-team="onAssignTeam"
@assign-label="onAssignLabels"
@update-conversation-status="toggleConversationStatus"
@context-menu-toggle="onContextMenuToggle"
@mark-as-unread="markAsUnread"
@assign-priority="assignPriority"
/>
<div>
<conversation-card
v-for="chat in conversationList"
:key="chat.id"
:active-label="label"
:team-id="teamId"
:folders-id="foldersId"
:chat="chat"
:conversation-type="conversationType"
:show-assignee="showAssigneeInConversationCard"
:selected="isConversationSelected(chat.id)"
@select-conversation="selectConversation"
@de-select-conversation="deSelectConversation"
@assign-agent="onAssignAgent"
@assign-team="onAssignTeam"
@assign-label="onAssignLabels"
@update-conversation-status="toggleConversationStatus"
@context-menu-toggle="onContextMenuToggle"
@mark-as-unread="markAsUnread"
@assign-priority="assignPriority"
/>
</div>
<div v-if="chatListLoading" class="text-center">
<span class="spinner" />
</div>
@@ -155,6 +156,7 @@
v-if="!hasCurrentPageEndReached && !chatListLoading"
variant="clear"
size="expanded"
class="text-center"
@click="loadMoreConversations"
>
{{ $t('CHAT_LIST.LOAD_MORE_CONVERSATIONS') }}
@@ -956,8 +958,6 @@ export default {
</script>
<style scoped lang="scss">
@import '~dashboard/assets/scss/woot';
.spinner {
margin-top: var(--space-normal);
margin-bottom: var(--space-normal);
@@ -972,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 {
@@ -343,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%;
}
@@ -52,7 +52,8 @@ export default {
},
computed: {
modalContainerClassName() {
let className = 'modal-container skip-context-menu';
let className =
'modal-container bg-white dark:bg-slate-800 skip-context-menu';
if (this.fullWidth) {
return `${className} modal-container--full-width`;
}
@@ -106,7 +107,7 @@ export default {
.modal-container {
border-radius: 0;
height: 100%;
width: 48rem;
width: 30rem;
}
}
.modal-big {
@@ -1,10 +1,17 @@
<template>
<div class="column page-top-bar">
<img v-if="headerImage" :src="headerImage" alt="No image" />
<h2 class="page-sub-title">
<h2
ref="modalHeaderTitle"
class="text-slate-800 text-lg dark:text-slate-100"
>
{{ headerTitle }}
</h2>
<p v-if="headerContent" class="small-12 column wrap-content">
<p
v-if="headerContent"
ref="modalHeaderContent"
class="small-12 column wrap-content"
>
{{ headerContent }}
<span v-if="headerContentValue" class="content-value">
{{ headerContentValue }}
@@ -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-bottom': 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-bottom {
padding-top: $space-normal;
border-bottom: 1px solid $color-border;
}
.sub-block-title {
color: $color-woot;
@@ -3,8 +3,8 @@
size="small"
variant="clear"
color-scheme="secondary"
class="-ml-3 text-black-900 dark:text-slate-300"
icon="list"
class="toggle-sidebar"
@click="onMenuItemClick"
/>
</template>
@@ -20,8 +20,3 @@ export default {
},
};
</script>
<style scoped lang="scss">
.toggle-sidebar {
margin-left: var(--space-minus-small);
}
</style>
@@ -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>
@@ -50,15 +50,6 @@
>
<woot-dropdown-menu>
<woot-dropdown-item v-if="!isPending">
<woot-button
variant="clear"
color-scheme="secondary"
size="small"
icon="book-clock"
@click="() => toggleStatus(STATUS_TYPE.PENDING)"
>
{{ this.$t('CONVERSATION.RESOLVE_DROPDOWN.MARK_PENDING') }}
</woot-button>
<woot-button
variant="clear"
color-scheme="secondary"
@@ -69,6 +60,17 @@
{{ this.$t('CONVERSATION.RESOLVE_DROPDOWN.SNOOZE_UNTIL') }}
</woot-button>
</woot-dropdown-item>
<woot-dropdown-item v-if="!isPending">
<woot-button
variant="clear"
color-scheme="secondary"
size="small"
icon="book-clock"
@click="() => toggleStatus(STATUS_TYPE.PENDING)"
>
{{ this.$t('CONVERSATION.RESOLVE_DROPDOWN.MARK_PENDING') }}
</woot-button>
</woot-dropdown-item>
</woot-dropdown-menu>
</div>
<woot-modal
@@ -258,11 +260,11 @@ 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;
@@ -4,7 +4,7 @@
<woot-dropdown-item
v-for="status in availabilityStatuses"
:key="status.value"
class="status-items"
class="flex items-baseline"
>
<woot-button
size="small"
@@ -18,23 +18,25 @@
</woot-button>
</woot-dropdown-item>
<woot-dropdown-divider />
<woot-dropdown-item class="auto-offline--toggle">
<div class="info-wrap">
<woot-dropdown-item class="m-0 flex items-center justify-between p-2">
<div class="flex items-center">
<fluent-icon
v-tooltip.right-start="$t('SIDEBAR.SET_AUTO_OFFLINE.INFO_TEXT')"
icon="info"
size="14"
class="info-icon"
class="mt-px"
/>
<span class="auto-offline--text">
<span
class="my-0 mx-1 text-xs font-medium text-slate-600 dark:text-slate-100"
>
{{ $t('SIDEBAR.SET_AUTO_OFFLINE.TEXT') }}
</span>
</div>
<woot-switch
size="small"
class="auto-offline--switch"
class="mt-px mx-1 mb-0"
:value="currentUserAutoOffline"
@input="updateAutoOffline"
/>
@@ -138,71 +140,3 @@ export default {
},
};
</script>
<style lang="scss">
@import '~dashboard/assets/scss/variables';
.status {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
padding: var(--space-micro) var(--space-smaller);
}
.status-view {
display: flex;
align-items: baseline;
& &--title {
color: var(--b-600);
font-size: var(--font-size-small);
font-weight: var(--font-weight-medium);
margin-left: var(--space-small);
&:first-letter {
text-transform: capitalize;
}
}
}
.status-change {
.dropdown-pane {
top: -132px;
right: var(--space-normal);
}
.status-items {
display: flex;
align-items: baseline;
}
}
.auto-offline--toggle {
align-items: center;
display: flex;
justify-content: space-between;
padding: var(--space-smaller);
margin: 0;
.info-wrap {
display: flex;
align-items: center;
}
.info-icon {
margin-top: -1px;
}
.auto-offline--switch {
margin: -1px var(--space-micro) 0;
}
.auto-offline--text {
margin: 0 var(--space-smaller);
font-size: var(--font-size-mini);
font-weight: var(--font-weight-medium);
color: var(--s-700);
}
}
</style>
@@ -1,5 +1,5 @@
<template>
<aside class="woot-sidebar">
<aside class="h-full flex">
<primary-sidebar
:logo-source="globalConfig.logoThumbnail"
:installation-name="globalConfig.installationName"
@@ -11,22 +11,21 @@
@key-shortcut-modal="toggleKeyShortcutModal"
@open-notification-panel="openNotificationPanel"
/>
<div class="secondary-sidebar">
<secondary-sidebar
v-if="showSecondarySidebar"
:class="sidebarClassName"
:account-id="accountId"
:inboxes="inboxes"
:labels="labels"
:teams="teams"
:custom-views="customViews"
:menu-config="activeSecondaryMenu"
:current-role="currentRole"
:is-on-chatwoot-cloud="isOnChatwootCloud"
@add-label="showAddLabelPopup"
@toggle-accounts="toggleAccountModal"
/>
</div>
<secondary-sidebar
v-if="showSecondarySidebar"
:class="sidebarClassName"
:account-id="accountId"
:inboxes="inboxes"
:labels="labels"
:teams="teams"
:custom-views="customViews"
:menu-config="activeSecondaryMenu"
:current-role="currentRole"
:is-on-chatwoot-cloud="isOnChatwootCloud"
@add-label="showAddLabelPopup"
@toggle-accounts="toggleAccountModal"
/>
</aside>
</template>
@@ -214,87 +213,3 @@ export default {
},
};
</script>
<style lang="scss" scoped>
.woot-sidebar {
background: var(--white);
display: flex;
min-height: 0;
height: 100%;
width: fit-content;
}
</style>
<style lang="scss">
@import '~dashboard/assets/scss/variables';
.account-selector--modal {
.modal-container {
width: 40rem;
}
}
.secondary-sidebar {
overflow-y: auto;
height: 100%;
}
.account-selector {
cursor: pointer;
padding: $space-small $space-large;
.selected--account {
margin-top: -$space-smaller;
& + .account--details {
padding-left: $space-normal - $space-micro;
}
}
.account--details {
padding-left: $space-large + $space-smaller;
}
&:last-child {
margin-bottom: $space-large;
}
a {
align-items: center;
cursor: pointer;
display: flex;
.account--name {
cursor: pointer;
font-size: $font-size-medium;
font-weight: $font-weight-medium;
line-height: 1;
}
.account--role {
cursor: pointer;
font-size: $font-size-mini;
text-transform: capitalize;
}
}
}
.app-context-menu {
align-items: center;
cursor: pointer;
display: flex;
flex-direction: row;
height: 6rem;
}
.current-user--options {
font-size: $font-size-big;
margin-bottom: auto;
margin-left: auto;
margin-top: auto;
}
.secondary-menu .nested.vertical.menu {
margin-left: var(--space-small);
}
</style>
@@ -1,25 +1,29 @@
<template>
<div
v-if="showShowCurrentAccountContext"
class="account-context--group"
class="rounded-md text-xs py-2 px-2 mt-2 relative border border-slate-50 dark:border-slate-700 hover:bg-slate-50 dark:hover:bg-slate-700 cursor-pointer"
@mouseover="setShowSwitch"
@mouseleave="resetShowSwitch"
>
{{ $t('SIDEBAR.CURRENTLY_VIEWING_ACCOUNT') }}
<p class="account-context--name text-ellipsis">
<p class="text-ellipsis font-medium mb-0">
{{ account.name }}
</p>
<transition name="fade">
<div v-if="showSwitchButton" class="account-context--switch-group">
<woot-button
variant="clear"
size="tiny"
icon="arrow-swap"
class="switch-button"
@click="$emit('toggle-accounts')"
>
{{ $t('SIDEBAR.SWITCH') }}
</woot-button>
<div
v-if="showSwitchButton"
class="bg-gradient-to-r rtl:bg-gradient-to-l from-transparent via-white dark:via-slate-700 to-white dark:to-slate-700 flex items-center h-full justify-end absolute top-0 right-0 w-full"
>
<div class="my-0 mx-2">
<woot-button
variant="clear"
size="tiny"
icon="arrow-swap"
@click="$emit('toggle-accounts')"
>
{{ $t('SIDEBAR.SWITCH') }}
</woot-button>
</div>
</div>
</transition>
</div>
@@ -51,51 +55,6 @@ export default {
};
</script>
<style scoped lang="scss">
.account-context--group {
border-radius: var(--border-radius-normal);
border: 1px solid var(--color-border);
font-size: var(--font-size-mini);
padding: var(--space-small);
margin: var(--space-small) var(--space-small) 0 var(--space-small);
position: relative;
&:hover {
background: var(--b-100);
}
.account-context--name {
font-weight: var(--font-weight-medium);
margin-bottom: 0;
}
}
.switch-button {
margin: 0 var(--space-small);
}
.account-context--switch-group {
--overlay-shadow: linear-gradient(
to right,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 1) 50%
);
align-items: center;
background-image: var(--overlay-shadow);
border-top-left-radius: 0;
border-top-right-radius: var(--border-radius-normal);
border-bottom-left-radius: 0;
border-bottom-right-radius: var(--border-radius-normal);
display: flex;
height: 100%;
justify-content: flex-end;
opacity: 1;
position: absolute;
right: 0;
top: 0;
width: 100%;
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 300ms ease;
@@ -2,31 +2,39 @@
<woot-modal
:show="showAccountModal"
:on-close="() => $emit('close-account-modal')"
class="account-selector--modal"
>
<woot-modal-header
:header-title="$t('SIDEBAR_ITEMS.CHANGE_ACCOUNTS')"
:header-content="$t('SIDEBAR_ITEMS.SELECTOR_SUBTITLE')"
/>
<div class="account-selector--wrap">
<div class="px-8 pt-4 pb-8">
<div
v-for="account in currentUser.accounts"
:id="`account-${account.id}`"
:key="account.id"
class="account-selector"
class="pt-0 pb-0"
>
<button
class="button expanded clear link"
class="flex justify-between items-center expanded clear link cursor-pointer px-4 py-3 w-full rounded-lg hover:underline hover:bg-slate-25 dark:hover:bg-slate-900"
@click="onChangeAccount(account.id)"
>
<span class="button__content">
<label :for="account.name" class="account-details--wrap">
<div class="account--name">{{ account.name }}</div>
<div class="account--role">{{ account.role }}</div>
<span class="w-full">
<label :for="account.name" class="text-left rtl:text-right">
<div
class="text-slate-700 text-lg dark:text-slate-100 font-medium hover:underline-offset-4 leading-5"
>
{{ account.name }}
</div>
<div
class="text-slate-500 text-xs dark:text-slate-500 font-medium hover:underline-offset-4"
>
{{ account.role }}
</div>
</label>
</span>
<fluent-icon
v-show="account.id === accountId"
class="selected--account"
class="text-slate-800 dark:text-slate-100"
icon="checkmark-circle"
type="solid"
size="24"
@@ -74,32 +82,3 @@ export default {
},
};
</script>
<style lang="scss" scoped>
.account-selector--wrap {
margin-top: var(--space-normal);
}
.account-selector {
padding-top: 0;
padding-bottom: 0;
.button {
display: flex;
justify-content: space-between;
padding: var(--space-one) var(--space-normal);
.account-details--wrap {
text-align: left;
.account--name {
cursor: pointer;
font-size: var(--font-size-medium);
font-weight: var(--font-weight-medium);
line-height: 1;
}
.account--role {
cursor: pointer;
font-size: var(--font-size-mini);
text-transform: capitalize;
}
}
}
}
</style>
@@ -1,5 +1,5 @@
<template>
<div class="logo">
<div class="w-8 h-8">
<router-link :to="dashboardPath" replace>
<img :src="source" :alt="name" />
</router-link>
@@ -30,17 +30,3 @@ export default {
},
};
</script>
<style lang="scss" scoped>
$logo-size: 32px;
.logo {
padding: var(--space-normal);
img {
width: $logo-size;
height: $logo-size;
object-fit: cover;
object-position: left center;
}
}
</style>
@@ -1,15 +1,25 @@
<template>
<div class="notifications-link">
<woot-button
class-names="notifications-link--button"
variant="clear"
color-scheme="secondary"
:class="{ 'is-active': isNotificationPanelActive }"
<div class="mb-4">
<button
class="text-slate-600 dark:text-slate-100 w-10 h-10 my-2 flex items-center justify-center rounded-lg hover:bg-slate-25 dark:hover:bg-slate-800 dark:hover:text-slate-100 hover:text-slate-600 relative"
:class="{
'bg-woot-50 dark:bg-slate-800 text-woot-500 hover:bg-woot-50': isNotificationPanelActive,
}"
@click="openNotificationPanel"
>
<fluent-icon icon="alert" />
<span v-if="unreadCount" class="badge warning">{{ unreadCount }}</span>
</woot-button>
<fluent-icon
icon="alert"
:class="{
'text-woot-500': isNotificationPanelActive,
}"
/>
<span
v-if="unreadCount"
class="text-black-900 bg-yellow-300 absolute -top-0.5 -right-1 p-1 text-xxs min-w-[1rem] rounded-full"
>
{{ unreadCount }}
</span>
</button>
</div>
</template>
<script>
@@ -43,37 +53,3 @@ export default {
},
};
</script>
<style scoped lang="scss">
.notifications-link {
margin-bottom: var(--space-small);
}
.badge {
position: absolute;
right: var(--space-minus-smaller);
top: var(--space-minus-smaller);
}
.notifications-link--button {
display: flex;
position: relative;
border-radius: var(--border-radius-large);
border: 1px solid transparent;
color: var(--s-600);
margin: var(--space-small) 0;
&:hover {
background: var(--w-50);
color: var(--s-600);
}
&:focus {
border-color: var(--w-500);
}
&.is-active {
background: var(--w-50);
color: var(--w-500);
}
}
</style>
@@ -3,11 +3,10 @@
<div
v-if="show"
v-on-clickaway="onClickAway"
class="options-menu dropdown-pane"
:class="{ 'dropdown-pane--open': show }"
class="left-3 rtl:left-auto rtl:right-3 bottom-16 w-64 absolute z-10 rounded-md shadow-xl bg-white dark:bg-slate-800 py-2 px-2 border border-slate-25 dark:border-slate-700"
:class="{ 'block visible': show }"
>
<availability-status />
<li class="divider" />
<woot-dropdown-menu>
<woot-dropdown-item v-if="showChangeAccountOption">
<woot-button
@@ -50,7 +49,7 @@
>
<a
:href="href"
class="button small clear secondary"
class="button small clear secondary bg-white dark:bg-slate-800 h-8"
:class="{ 'is-active': isActive }"
@click="e => handleProfileSettingClick(e, navigate)"
>
@@ -64,7 +63,7 @@
<woot-dropdown-item v-if="currentUser.type === 'SuperAdmin'">
<a
href="/super_admin"
class="button small clear secondary"
class="button small clear secondary bg-white dark:bg-slate-800 h-8"
target="_blank"
rel="noopener nofollow noreferrer"
@click="$emit('close')"
@@ -149,12 +148,3 @@ export default {
},
};
</script>
<style lang="scss" scoped>
.options-menu.dropdown-pane {
left: var(--space-slab);
bottom: var(--space-larger);
min-width: var(--space-giga);
top: unset;
z-index: var(--z-index-low);
}
</style>
@@ -1,11 +1,14 @@
<template>
<div class="primary--sidebar">
<logo
:source="logoSource"
:name="installationName"
:account-id="accountId"
/>
<nav class="menu vertical">
<div
class="h-full w-16 bg-white dark:bg-slate-900 border-r border-slate-50 dark:border-slate-700 rtl:border-l rtl:border-r-0 flex justify-between flex-col"
>
<div class="flex flex-col items-center">
<logo
:source="logoSource"
:name="installationName"
:account-id="accountId"
class="m-4 mb-10"
/>
<primary-nav-item
v-for="menuItem in menuItems"
:key="menuItem.toState"
@@ -14,8 +17,8 @@
:to="menuItem.toState"
:is-child-menu-active="menuItem.key === activeMenuItem"
/>
</nav>
<div class="menu vertical user-menu">
</div>
<div class="flex flex-col items-center justify-end pb-6">
<primary-nav-item
v-if="!isACustomBrandedInstance"
icon="book-open-globe"
@@ -101,27 +104,3 @@ export default {
},
};
</script>
<style lang="scss" scoped>
.primary--sidebar {
display: flex;
flex-direction: column;
width: var(--space-jumbo);
border-right: 1px solid var(--s-50);
box-sizing: content-box;
height: 100%;
flex-shrink: 0;
}
.menu {
align-items: center;
margin-top: var(--space-medium);
}
.user-menu {
display: flex;
flex-direction: column;
flex-grow: 1;
justify-content: flex-end;
margin-bottom: var(--space-normal);
}
</style>
@@ -3,15 +3,28 @@
<a
v-tooltip.right="$t(`SIDEBAR.${name}`)"
:href="href"
class="button clear button--only-icon menu-item"
:class="{ 'is-active': isActive || isChildMenuActive }"
class="text-slate-600 dark:text-slate-100 w-10 h-10 my-2 flex items-center justify-center rounded-lg hover:bg-slate-25 dark:hover:bg-slate-800 dark:hover:text-slate-100 hover:text-slate-600 relative"
:class="{
'bg-woot-50 dark:bg-slate-800 text-woot-500 hover:bg-woot-50':
isActive || isChildMenuActive,
}"
:rel="openInNewPage ? 'noopener noreferrer nofollow' : undefined"
:target="openInNewPage ? '_blank' : undefined"
@click="navigate"
>
<fluent-icon :icon="icon" />
<span class="show-for-sr">{{ name }}</span>
<span v-if="count" class="badge warning">{{ count }}</span>
<fluent-icon
:icon="icon"
:class="{
'text-woot-500': isActive || isChildMenuActive,
}"
/>
<span class="sr-only">{{ name }}</span>
<span
v-if="count"
class="text-black-900 bg-yellow-500 absolute -top-1 -right-1"
>
{{ count }}
</span>
</a>
</router-link>
</template>
@@ -45,40 +58,3 @@ export default {
},
};
</script>
<style lang="scss" scoped>
.button {
margin: var(--space-small) 0;
}
.menu-item {
display: inline-flex;
position: relative;
border-radius: var(--border-radius-large);
border: 1px solid transparent;
color: var(--s-600);
&:hover {
background: var(--w-25);
color: var(--s-600);
}
&:focus {
border-color: var(--w-500);
}
&.is-active {
background: var(--w-50);
color: var(--w-500);
}
}
.icon {
font-size: var(--font-size-default);
}
.badge {
position: absolute;
right: var(--space-minus-smaller);
top: var(--space-minus-smaller);
}
</style>
@@ -1,7 +1,14 @@
<template>
<div v-if="hasSecondaryMenu" class="main-nav secondary-menu">
<div
v-if="hasSecondaryMenu"
class="h-full overflow-auto w-48 flex flex-col bg-white dark:bg-slate-900 border-r dark:border-slate-700 rtl:border-r-0 rtl:border-l border-slate-50 text-sm px-2 pb-8"
>
<account-context @toggle-accounts="toggleAccountModal" />
<transition-group name="menu-list" tag="ul" class="menu vertical">
<transition-group
name="menu-list"
tag="ul"
class="pt-2 list-none ml-0 mb-0"
>
<secondary-nav-item
v-for="menuItem in accessibleMenuItems"
:key="menuItem.toState"
@@ -249,27 +256,3 @@ export default {
},
};
</script>
<style lang="scss" scoped>
@import '~dashboard/assets/scss/woot';
.secondary-menu {
display: flex;
flex-direction: column;
background: var(--white);
border-right: 1px solid var(--s-50);
height: 100%;
width: 20rem;
flex-shrink: 0;
overflow-y: hidden;
position: unset;
&:hover {
overflow-y: hidden;
}
.menu {
padding: var(--space-small);
overflow-y: auto;
}
}
</style>
@@ -5,38 +5,69 @@
custom
active-class="active"
>
<li :class="{ active: isActive }">
<li
class="font-medium h-7 my-1 hover:bg-slate-25 hover:text-bg-50 flex items-center px-2 rounded-md dark:hover:bg-slate-800"
:class="{
'bg-woot-25 dark:bg-slate-800': isActive,
'text-ellipsis overflow-hidden whitespace-nowrap max-w-full': shouldTruncate,
}"
@click="navigate"
>
<a
:href="href"
class="button clear menu-item text-truncate"
:class="{ 'is-active': isActive, 'text-truncate': shouldTruncate }"
@click="navigate"
class="inline-flex text-left max-w-full w-full items-center"
>
<span v-if="icon" class="badge--icon">
<fluent-icon class="inbox-icon" :icon="icon" size="12" />
<span
v-if="icon"
class="inline-flex items-center justify-center w-4 rounded-sm bg-slate-100 dark:bg-slate-700 p-0.5 mr-1.5 rtl:mr-0 rtl:ml-1.5"
>
<fluent-icon
class="text-xxs text-slate-600 dark:text-slate-200"
:class="{
'text-woot-500 dark:text-woot-500': isActive,
}"
:icon="icon"
size="12"
/>
</span>
<span
v-if="labelColor"
class="badge--label"
class="inline-flex rounded-sm bg-slate-100 h-3 w-3.5 mr-1.5 rtl:mr-0 rtl:ml-1.5 border border-slate-50 dark:border-slate-900"
:style="{ backgroundColor: labelColor }"
/>
<span
:title="menuTitle"
class="menu-label button__content"
:class="{ 'text-truncate': shouldTruncate }"
<div
class="items-center flex overflow-hidden whitespace-nowrap text-ellipsis w-full justify-between"
>
{{ label }}
<span v-if="showChildCount" class="count-view">
<span
:title="menuTitle"
class="text-sm text-slate-600 dark:text-slate-100"
:class="{
'text-woot-500 dark:text-woot-500': isActive,
'text-ellipsis overflow-hidden whitespace-nowrap max-w-full': shouldTruncate,
}"
>
{{ label }}
</span>
<span
v-if="showChildCount"
class="bg-slate-50 dark:bg-slate-700 rounded text-xxs font-medium mx-1 py-0 px-1"
:class="
isCountZero
? `text-slate-300 dark:text-slate-600`
: `text-slate-600 dark:text-slate-50`
"
>
{{ childItemCount }}
</span>
</span>
<span v-if="count" class="badge" :class="{ secondary: !isActive }">
{{ count }}
</span>
<span v-if="warningIcon" class="badge--icon">
</div>
<span
v-if="warningIcon"
class="inline-flex rounded-sm mr-1 bg-slate-100"
>
<fluent-icon
v-tooltip.top-end="$t('SIDEBAR.FACEBOOK_REAUTHORIZE')"
class="inbox-icon"
class="text-xxs"
:icon="warningIcon"
size="12"
/>
@@ -72,10 +103,6 @@ export default {
type: String,
default: '',
},
count: {
type: String,
default: '',
},
showChildCount: {
type: Boolean,
default: false,
@@ -89,96 +116,12 @@ export default {
showIcon() {
return { 'text-truncate': this.shouldTruncate };
},
isCountZero() {
return this.childItemCount === 0;
},
menuTitle() {
return this.shouldTruncate ? this.label : '';
},
},
};
</script>
<style lang="scss" scoped>
$badge-size: var(--space-normal);
$label-badge-size: var(--space-slab);
.button {
margin: var(--space-small) 0;
}
.menu-item {
display: inline-flex;
color: var(--s-600);
font-weight: var(--font-weight-medium);
width: 100%;
height: var(--space-medium);
padding: var(--space-smaller) var(--space-smaller);
margin: var(--space-smaller) 0;
text-align: left;
line-height: 1.2;
&:hover {
background: var(--s-25);
color: var(--s-600);
}
&:focus {
border-color: var(--w-300);
}
&.is-active {
background: var(--w-25);
color: var(--w-500);
border-color: var(--w-25);
}
&.is-active .count-view {
background: var(--w-75);
color: var(--w-500);
}
}
.menu-label {
flex-grow: 1;
}
.inbox-icon {
font-size: var(--font-size-nano);
}
.badge--label,
.badge--icon {
display: inline-flex;
border-radius: var(--border-radius-small);
margin-right: var(--space-smaller);
background: var(--s-100);
}
.badge--icon {
align-items: center;
height: $badge-size;
justify-content: center;
min-width: $badge-size;
}
.badge--label {
height: $label-badge-size;
min-width: $label-badge-size;
margin-left: var(--space-smaller);
border: 1px solid var(--color-border-light);
}
.badge.secondary {
min-width: unset;
background: var(--s-75);
color: var(--s-600);
font-weight: var(--font-weight-bold);
}
.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);
}
</style>
@@ -1,46 +1,57 @@
<template>
<li v-show="isMenuItemVisible" class="sidebar-item">
<div v-if="hasSubMenu" class="secondary-menu--wrap">
<span class="secondary-menu--header fs-small">
<li v-show="isMenuItemVisible" class="mt-1">
<div v-if="hasSubMenu" class="flex justify-between">
<span
class="text-sm text-slate-700 dark:text-slate-200 font-semibold my-2 px-2 pt-1"
>
{{ $t(`SIDEBAR.${menuItem.label}`) }}
</span>
<div v-if="menuItem.showNewButton" class="submenu-icons">
<div v-if="menuItem.showNewButton" class="flex items-center">
<woot-button
size="tiny"
variant="clear"
color-scheme="secondary"
icon="add"
class="submenu-icon"
class="p-0 ml-2"
@click="onClickOpen"
/>
</div>
</div>
<router-link
v-else
class="secondary-menu--title secondary-menu--link fs-small"
class="rounded-lg leading-4 font-medium flex items-center p-2 m-0 text-sm text-slate-700 dark:text-slate-100 hover:bg-slate-25 dark:hover:bg-slate-800"
:class="computedClass"
:to="menuItem && menuItem.toState"
>
<fluent-icon
:icon="menuItem.icon"
class="secondary-menu--icon"
class="min-w-[1rem] mr-1.5 rtl:mr-0 rtl:ml-1.5"
size="14"
/>
{{ $t(`SIDEBAR.${menuItem.label}`) }}
<span v-if="showChildCount(menuItem.count)" class="count-view">
<span
v-if="showChildCount(menuItem.count)"
class="rounded-md text-xxs font-medium mx-1 py-0 px-1"
:class="{
'text-slate-300 dark:text-slate-600': isCountZero && !isActiveView,
'text-slate-600 dark:text-slate-50': !isCountZero && !isActiveView,
'bg-woot-75 dark:bg-woot-200 text-woot-600 dark:text-woot-600': isActiveView,
'bg-slate-50 dark:bg-slate-700': !isActiveView,
}"
>
{{ `${menuItem.count}` }}
</span>
<span
v-if="menuItem.beta"
data-view-component="true"
label="Beta"
class="beta"
class="px-1 mx-1 inline-block font-medium leading-4 border border-green-400 text-green-500 rounded-lg text-xxs"
>
{{ $t('SIDEBAR.BETA') }}
</span>
</router-link>
<ul v-if="hasSubMenu" class="nested vertical menu">
<ul v-if="hasSubMenu" class="list-none ml-0 mb-0">
<secondary-child-nav-item
v-for="child in menuItem.children"
:key="child.id"
@@ -55,21 +66,21 @@
/>
<router-link
v-if="showItem(menuItem)"
v-slot="{ href, isActive, navigate }"
v-slot="{ href, navigate }"
:to="menuItem.toState"
custom
>
<li class="menu-item--new">
<a
:href="href"
class="button small link clear secondary"
:class="{ 'is-active': isActive }"
@click="e => newLinkClick(e, navigate)"
>
<fluent-icon icon="add" size="16" />
<span class="button__content">
<li class="pl-1">
<a :href="href">
<woot-button
size="tiny"
variant="clear"
color-scheme="secondary"
icon="add"
@click="e => newLinkClick(e, navigate)"
>
{{ $t(`SIDEBAR.${menuItem.newLinkTag}`) }}
</span>
</woot-button>
</a>
</li>
</router-link>
@@ -108,6 +119,12 @@ export default {
isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount',
globalConfig: 'globalConfig/get',
}),
isCountZero() {
return this.menuItem.count === 0;
},
isActiveView() {
return this.computedClass.includes('active-view');
},
hasSubMenu() {
return !!this.menuItem.children;
},
@@ -178,7 +195,7 @@ export default {
this.isUnattended ||
this.isCurrentRoute
) {
return 'is-active';
return 'bg-woot-25 dark:bg-slate-800 text-woot-500 dark:text-woot-500 hover:text-woot-500 dark:hover:text-woot-500 active-view';
}
if (this.hasSubMenu) {
if (
@@ -187,12 +204,12 @@ export default {
this.isIntegrationsSettings ||
this.isApplicationsSettings
) {
return 'is-active';
return 'bg-woot-25 dark:bg-slate-800 text-woot-500 dark:text-woot-500 hover:text-woot-500 dark:hover:text-woot-500 active-view';
}
return ' ';
return 'hover:text-slate-700 dark:hover:text-slate-100';
}
return '';
return 'hover:text-slate-700 dark:hover:text-slate-100';
},
},
methods: {
@@ -233,132 +250,3 @@ export default {
},
};
</script>
<style lang="scss" scoped>
.sidebar-item {
margin: var(--space-smaller) 0 0;
}
.secondary-menu--wrap {
display: flex;
justify-content: space-between;
margin-top: var(--space-small);
}
.secondary-menu--header {
color: var(--s-700);
display: flex;
font-weight: var(--font-weight-bold);
line-height: var(--space-normal);
margin: var(--space-small) 0;
padding: 0 var(--space-small);
}
.secondary-menu--title {
color: var(--s-600);
display: flex;
font-weight: var(--font-weight-medium);
line-height: var(--space-normal);
margin: var(--space-small) 0;
padding: 0 var(--space-small);
}
.secondary-menu--link {
display: flex;
align-items: center;
margin: 0;
padding: var(--space-small);
font-weight: var(--font-weight-medium);
border-radius: var(--border-radius-normal);
color: var(--s-700);
&:hover {
background: var(--s-25);
color: var(--s-600);
}
&:focus {
border-color: var(--w-300);
}
&.router-link-exact-active,
&.is-active {
background: var(--w-25);
color: var(--w-500);
border-color: var(--w-25);
}
&.is-active .count-view {
background: var(--w-75);
color: var(--w-600);
}
}
.secondary-menu--icon {
margin-right: var(--space-smaller);
min-width: var(--space-normal);
}
.sub-menu-link {
color: var(--s-600);
}
.wrap {
display: flex;
align-items: center;
}
.label-color--display {
border-radius: var(--space-smaller);
height: var(--space-normal);
margin-right: var(--space-small);
min-width: var(--space-normal);
width: var(--space-normal);
}
.inbox-icon {
position: relative;
top: -1px;
}
.sidebar-item .menu-item--new {
padding: var(--space-small) 0;
.button {
display: inline-flex;
color: var(--s-500);
}
}
.beta {
padding-right: var(--space-smaller) !important;
padding-left: var(--space-smaller) !important;
margin: 0 var(--space-smaller) !important;
display: inline-block;
font-size: var(--font-size-micro);
font-weight: var(--font-weight-medium);
line-height: 14px;
border: 1px solid transparent;
border-radius: 2em;
color: var(--g-800);
border-color: var(--g-700);
}
.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: 0 var(--space-smaller);
padding: var(--space-zero) var(--space-smaller);
}
.submenu-icons {
display: flex;
align-items: center;
.submenu-icon {
padding: 0;
margin-left: var(--space-small);
}
}
</style>
@@ -4,7 +4,6 @@ import Vuex from 'vuex';
import VueI18n from 'vue-i18n';
import i18n from 'dashboard/i18n';
import WootModal from 'dashboard/components/Modal';
import WootModalHeader from 'dashboard/components/ModalHeader';
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon';
@@ -38,9 +37,7 @@ describe('accountSelctor', () => {
},
],
};
const accountId = 1;
const globalConfig = { createNewAccountFromDashboard: false };
let store = null;
let actions = null;
let modules = null;
@@ -49,44 +46,46 @@ describe('accountSelctor', () => {
modules = {
auth: {
getters: {
getCurrentAccountId: () => accountId,
getCurrentAccountId: () => 1,
getCurrentUser: () => currentUser,
},
},
globalConfig: {
getters: {
'globalConfig/get': () => globalConfig,
'globalConfig/get': () => ({ createNewAccountFromDashboard: false }),
},
},
};
store = new Vuex.Store({
actions,
modules,
});
let store = new Vuex.Store({ actions, modules });
accountSelector = mount(AccountSelector, {
store,
localVue,
i18n: i18nConfig,
propsData: {
showAccountModal: true,
},
propsData: { showAccountModal: true },
stubs: { WootButton: { template: '<button />' } },
});
});
it('title and sub title exist', () => {
const headerComponent = accountSelector.findComponent(WootModalHeader);
const topBar = headerComponent.find('.page-top-bar');
const titleComponent = topBar.find('.page-sub-title');
expect(titleComponent.text()).toBe('Switch Account');
const subTitleComponent = topBar.find('p');
expect(subTitleComponent.text()).toBe(
'Select an account from the following list'
);
const title = headerComponent.findComponent({ ref: 'modalHeaderTitle' });
expect(title.text()).toBe('Switch Account');
const content = headerComponent.findComponent({
ref: 'modalHeaderContent',
});
expect(content.text()).toBe('Select an account from the following list');
});
it('first account item is checked', () => {
const accountFirstItem = accountSelector.find('.account-selector svg');
expect(accountFirstItem.exists()).toBe(true);
const selectedAccountCheckmark = accountSelector.find(
'#account-1 > button > svg'
);
expect(selectedAccountCheckmark.exists()).toBe(true);
const otherAccountCheckmark = accountSelector.find(
'#account-2 > button > svg'
);
expect(otherAccountCheckmark.exists()).toBe(true);
});
});
@@ -19,10 +19,7 @@ localVue.component('woot-dropdown-menu', WootDropdownMenu);
localVue.component('woot-dropdown-divider', WootDropdownDivider);
localVue.component('woot-dropdown-item', WootDropdownItem);
const i18nConfig = new VueI18n({
locale: 'en',
messages: i18n,
});
const i18nConfig = new VueI18n({ locale: 'en', messages: i18n });
describe('AvailabilityStatus', () => {
const currentAvailability = 'online';
@@ -48,15 +45,13 @@ describe('AvailabilityStatus', () => {
},
};
store = new Vuex.Store({
actions,
modules,
});
store = new Vuex.Store({ actions, modules });
availabilityStatus = mount(AvailabilityStatus, {
store,
localVue,
i18n: i18nConfig,
stubs: { WootSwitch: { template: '<button />' } },
});
});
@@ -3,7 +3,9 @@ import SidemenuIcon from '../SidemenuIcon';
describe('SidemenuIcon', () => {
test('matches snapshot', () => {
const wrapper = shallowMount(SidemenuIcon);
const wrapper = shallowMount(SidemenuIcon, {
stubs: { WootButton: { template: '<button><slot /></button>' } },
});
expect(wrapper.vm).toBeTruthy();
expect(wrapper.element).toMatchSnapshot();
});
@@ -1,8 +1,8 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SidemenuIcon matches snapshot 1`] = `
<woot-button
class="toggle-sidebar"
<button
class="-ml-3 text-black-900 dark:text-slate-300"
color-scheme="secondary"
icon="list"
size="small"
@@ -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>
@@ -4,7 +4,7 @@
<fluent-icon :icon="icon" size="12" class="label--icon" />
</span>
<span
v-if="variant === 'smooth' && title && !icon"
v-if="['smooth', 'dashed'].includes(variant) && title && !icon"
:style="{ background: color }"
class="label-color-dot"
/>
@@ -69,6 +69,7 @@ export default {
computed: {
textColor() {
if (this.variant === 'smooth') return '';
if (this.variant === 'dashed') return '';
return this.color || getContrastingTextColor(this.bgColor);
},
labelClass() {
@@ -199,8 +200,14 @@ export default {
&.smooth {
background: transparent;
border: 1px solid var(--s-100);
color: var(--s-700);
border: 1px solid var(--s-100);
}
&.dashed {
background: transparent;
color: var(--s-700);
border: 1px dashed var(--s-100);
}
}
@@ -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);
@@ -5,7 +5,7 @@
delay: { show: 1500, hide: 0 },
hideOnClick: true,
}"
class="time-ago"
class="text-xxs text-black-600 leading-4 ml-auto hover:text-black-900"
>
<span>{{ `${createdAtTime}${lastActivityTime}` }}</span>
</div>
@@ -109,16 +109,3 @@ export default {
},
};
</script>
<style lang="scss" scoped>
.time-ago {
color: var(--b-600);
font-size: var(--font-size-micro);
font-weight: var(--font-weight-normal);
line-height: var(--space-normal);
margin-left: auto;
&:hover {
color: var(--b-900);
}
}
</style>
@@ -18,7 +18,11 @@
:icon="icon"
:icon-size="iconSize"
/>
<span v-if="$slots.default" class="button__content">
<span
v-if="$slots.default"
class="button__content"
:class="{ 'text-left rtl:text-right': size !== 'expanded' }"
>
<slot />
</span>
</button>
@@ -1,240 +1,80 @@
<template>
<div v-if="isAIIntegrationEnabled" class="position-relative">
<div v-if="!message">
<woot-button
v-if="isPrivateNote"
v-tooltip.top-end="$t('INTEGRATION_SETTINGS.OPEN_AI.SUMMARY_TITLE')"
icon="book-pulse"
color-scheme="secondary"
variant="smooth"
size="small"
:is-loading="uiFlags.summarize"
@click="processEvent('summarize')"
<woot-button
v-tooltip.top-end="$t('INTEGRATION_SETTINGS.OPEN_AI.AI_ASSIST')"
icon="wand"
color-scheme="secondary"
variant="smooth"
size="small"
@click="openAIAssist"
/>
<woot-modal
:show.sync="showAIAssistanceModal"
:on-close="hideAIAssistanceModal"
>
<AIAssistanceModal
:ai-option="aiOption"
@apply-text="insertText"
@close="hideAIAssistanceModal"
/>
<woot-button
v-else
v-tooltip.top-end="$t('INTEGRATION_SETTINGS.OPEN_AI.REPLY_TITLE')"
icon="wand"
color-scheme="secondary"
variant="smooth"
size="small"
:is-loading="uiFlags.reply_suggestion"
@click="processEvent('reply_suggestion')"
/>
</div>
<div v-else>
<woot-button
v-tooltip.top-end="$t('INTEGRATION_SETTINGS.OPEN_AI.TITLE')"
icon="text-grammar-wand"
color-scheme="secondary"
variant="smooth"
size="small"
@click="toggleDropdown"
/>
<div
v-if="showDropdown"
v-on-clickaway="closeDropdown"
class="dropdown-pane dropdown-pane--open ai-modal"
>
<h4 class="sub-block-title margin-top-1">
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.TITLE') }}
</h4>
<p>
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.SUBTITLE') }}
</p>
<label>
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.TONE.TITLE') }}
</label>
<div class="tone__item">
<select v-model="activeTone" class="status--filter small">
<option v-for="tone in tones" :key="tone.key" :value="tone.key">
{{ tone.value }}
</option>
</select>
</div>
<div v-if="uiFlags.isContentGenerated">
<label>
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.GENERATE_TITLE') }}
</label>
<p>
{{ generatedMessage }}
</p>
</div>
<div class="modal-footer flex-container align-right">
<woot-button variant="clear" size="small" @click="closeDropdown">
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.BUTTONS.CANCEL') }}
</woot-button>
<woot-button
v-if="!uiFlags.isContentGenerated"
:is-loading="uiFlags.rephrase"
size="small"
@click="processEvent('rephrase')"
>
{{ buttonText }}
</woot-button>
<woot-button v-else size="small" @click="replaceText('rephrase')">
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.BUTTONS.REPLACE') }}
</woot-button>
</div>
</div>
</div>
</woot-modal>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import { mixin as clickaway } from 'vue-clickaway';
import OpenAPI from 'dashboard/api/integrations/openapi';
import alertMixin from 'shared/mixins/alertMixin';
import { OPEN_AI_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
import AIAssistanceModal from './AIAssistanceModal.vue';
import aiMixin from 'dashboard/mixins/aiMixin';
import { CMD_AI_ASSIST } from 'dashboard/routes/dashboard/commands/commandBarBusEvents';
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
import { buildHotKeys } from 'shared/helpers/KeyboardHelpers';
export default {
mixins: [alertMixin, clickaway],
props: {
conversationId: {
type: Number,
default: 0,
},
message: {
type: String,
default: '',
},
isPrivateNote: {
type: Boolean,
default: false,
},
},
data() {
return {
uiFlags: {
rephrase: false,
reply_suggestion: false,
summarize: false,
isisContentGenerated: false,
},
generatedMessage: '',
showDropdown: false,
activeTone: 'professional',
tones: [
{
key: 'professional',
value: this.$t(
'INTEGRATION_SETTINGS.OPEN_AI.TONE.OPTIONS.PROFESSIONAL'
),
},
{
key: 'friendly',
value: this.$t('INTEGRATION_SETTINGS.OPEN_AI.TONE.OPTIONS.FRIENDLY'),
},
],
};
components: {
AIAssistanceModal,
},
mixins: [aiMixin, eventListenerMixins],
data: () => ({
showAIAssistanceModal: false,
aiOption: '',
initialMessage: '',
}),
computed: {
...mapGetters({ appIntegrations: 'integrations/getAppIntegrations' }),
isAIIntegrationEnabled() {
return this.appIntegrations.find(
integration => integration.id === 'openai' && !!integration.hooks.length
);
},
hookId() {
return this.appIntegrations.find(
integration => integration.id === 'openai' && !!integration.hooks.length
).hooks[0].id;
},
buttonText() {
return this.uiFlags.isRephrasing
? this.$t('INTEGRATION_SETTINGS.OPEN_AI.BUTTONS.GENERATING')
: this.$t('INTEGRATION_SETTINGS.OPEN_AI.BUTTONS.GENERATE');
},
...mapGetters({
currentChat: 'getSelectedChat',
}),
},
mounted() {
if (!this.appIntegrations.length) {
this.$store.dispatch('integrations/get');
}
bus.$on(CMD_AI_ASSIST, this.onAIAssist);
this.initialMessage = this.draftMessage;
},
methods: {
toggleDropdown() {
this.uiFlags.isContentGenerated = false;
this.generatedMessage = '';
this.showDropdown = !this.showDropdown;
},
closeDropdown() {
this.showDropdown = false;
},
async recordAnalytics({ type, tone }) {
const event = OPEN_AI_EVENTS[type.toUpperCase()];
if (event) {
this.$track(event, {
type,
tone,
});
onKeyDownHandler(event) {
const keyPattern = buildHotKeys(event);
const shouldRevertTheContent =
['meta+z', 'ctrl+z'].includes(keyPattern) && !!this.initialMessage;
if (shouldRevertTheContent) {
this.$emit('replace-text', this.initialMessage);
this.initialMessage = '';
}
},
replaceText(type) {
this.$emit('replace-text', this.generatedMessage || this.message);
this.recordAnalytics({ type, tone: this.activeTone });
this.closeDropdown();
hideAIAssistanceModal() {
this.showAIAssistanceModal = false;
},
async processEvent(type = 'rephrase') {
this.uiFlags[type] = true;
try {
const result = await OpenAPI.processEvent({
hookId: this.hookId,
type,
content: this.message,
tone: this.activeTone,
conversationId: this.conversationId,
});
const {
data: { message: generatedMessage },
} = result;
this.uiFlags.isContentGenerated = true;
this.generatedMessage = generatedMessage;
// It the type is not rephrase, replace the text
if (type !== 'rephrase') {
this.replaceText(type);
}
} catch (error) {
this.showAlert(this.$t('INTEGRATION_SETTINGS.OPEN_AI.GENERATE_ERROR'));
} finally {
this.uiFlags[type] = false;
}
openAIAssist() {
this.initialMessage = this.draftMessage;
const ninja = document.querySelector('ninja-keys');
ninja.open({ parent: 'ai_assist' });
},
onAIAssist(option) {
this.aiOption = option;
this.showAIAssistanceModal = true;
},
insertText(message) {
this.$emit('replace-text', message);
},
},
};
</script>
<style lang="scss" scoped>
.ai-modal {
width: 400px;
right: 0;
left: 0;
padding: var(--space-normal);
bottom: 34px;
position: absolute;
span {
font-size: var(--font-size-small);
font-weight: var(--font-weight-medium);
}
p {
color: var(--s-600);
}
label {
margin-bottom: var(--space-smaller);
}
.status--filter {
background-color: var(--color-background-light);
border: 1px solid var(--color-border);
font-size: var(--font-size-small);
height: var(--space-large);
padding: 0 var(--space-medium) 0 var(--space-small);
}
.modal-footer {
gap: var(--space-smaller);
}
}
</style>
@@ -0,0 +1,107 @@
<template>
<div class="column">
<woot-modal-header :header-title="headerTitle" />
<form class="row modal-content" @submit.prevent="applyText">
<div v-if="draftMessage" class="w-full">
<h4 class="sub-block-title margin-top-1 ">
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.ASSISTANCE_MODAL.DRAFT_TITLE') }}
</h4>
<p v-dompurify-html="formatMessage(draftMessage, false)" />
<h4 class="sub-block-title margin-top-1">
{{
$t('INTEGRATION_SETTINGS.OPEN_AI.ASSISTANCE_MODAL.GENERATED_TITLE')
}}
</h4>
</div>
<div>
<AILoader v-if="isGenerating" />
<p v-else v-dompurify-html="formatMessage(generatedContent, false)" />
</div>
<div class="modal-footer justify-content-end w-full">
<woot-button variant="clear" @click.prevent="onClose">
{{
$t('INTEGRATION_SETTINGS.OPEN_AI.ASSISTANCE_MODAL.BUTTONS.CANCEL')
}}
</woot-button>
<woot-button :disabled="!generatedContent">
{{
$t('INTEGRATION_SETTINGS.OPEN_AI.ASSISTANCE_MODAL.BUTTONS.APPLY')
}}
</woot-button>
</div>
</form>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
import AILoader from './AILoader.vue';
import aiMixin from 'dashboard/mixins/aiMixin';
export default {
components: {
AILoader,
},
mixins: [aiMixin, messageFormatterMixin],
props: {
aiOption: {
type: String,
required: true,
},
},
data() {
return {
generatedContent: '',
isGenerating: true,
};
},
computed: {
...mapGetters({
appIntegrations: 'integrations/getAppIntegrations',
}),
headerTitle() {
const translationKey = this.aiOption?.toUpperCase();
return translationKey
? this.$t(`INTEGRATION_SETTINGS.OPEN_AI.WITH_AI`, {
option: this.$t(
`INTEGRATION_SETTINGS.OPEN_AI.OPTIONS.${translationKey}`
),
})
: '';
},
},
mounted() {
this.generateAIContent(this.aiOption);
},
methods: {
onClose() {
this.$emit('close');
},
async generateAIContent(type = 'rephrase') {
this.isGenerating = true;
this.generatedContent = await this.processEvent(type);
this.isGenerating = false;
},
applyText() {
this.recordAnalytics(this.aiOption);
this.$emit('apply-text', this.generatedContent);
this.onClose();
},
},
};
</script>
<style lang="scss" scoped>
.modal-content {
padding-top: var(--space-small);
}
.container {
width: 100%;
}
</style>
@@ -0,0 +1,67 @@
<template>
<div class="animation-container margin-top-1">
<div class="ai-typing--wrap ">
<fluent-icon icon="wand" size="14" class="ai-typing--icon" />
<label>
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.ASSISTANCE_MODAL.AI_WRITING') }}
</label>
</div>
<span class="loader" />
<span class="loader" />
<span class="loader" />
</div>
</template>
<script></script>
<style lang="scss" scoped>
.animation-container {
position: relative;
display: flex;
.ai-typing--wrap {
display: flex;
align-items: center;
gap: 4px;
.ai-typing--icon {
color: var(--v-500);
}
}
label {
display: inline-block;
margin-right: var(--space-smaller);
color: var(--v-400);
}
.loader {
display: inline-block;
width: 6px;
height: 6px;
margin-right: var(--space-smaller);
margin-top: var(--space-slab);
background-color: var(--v-300);
border-radius: 50%;
animation: bubble-scale 1.2s infinite;
}
.loader:nth-child(2) {
animation-delay: 0.4s;
}
.loader:nth-child(3) {
animation-delay: 0.8s;
}
@keyframes bubble-scale {
0%,
100% {
transform: scale(1);
}
25% {
transform: scale(1.3);
}
50% {
transform: scale(1);
}
}
}
</style>
@@ -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);
}
@@ -1,6 +1,6 @@
<template>
<div class="avatar-container" :style="style" aria-hidden="true">
{{ userInitial }}
<slot>{{ userInitial }}</slot>
</div>
</template>
@@ -21,6 +21,9 @@ export default {
},
},
computed: {
hasFbConfigured() {
return window.chatwootConfig?.fbAppId;
},
isActive() {
const { key } = this.channel;
if (Object.keys(this.enabledFeatures).length === 0) {
@@ -30,10 +33,7 @@ export default {
return this.enabledFeatures.channel_website;
}
if (key === 'facebook') {
return this.enabledFeatures.channel_facebook;
}
if (key === 'twitter') {
return this.enabledFeatures.channel_twitter;
return this.enabledFeatures.channel_facebook && this.hasFbConfigured;
}
if (key === 'email') {
return this.enabledFeatures.channel_email;
@@ -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>
@@ -1,6 +1,11 @@
<template>
<div class="row loading-state">
<h6 class="message">{{ message }}<span class="spinner" /></h6>
<div class="flex items-center justify-center p-8">
<h6
class="block text-base text-center w-100 text-slate-800 dark:text-slate-300"
>
<span class="mr-3">{{ message }}</span>
<span class="spinner" />
</h6>
</div>
</template>
<script>
@@ -5,19 +5,21 @@
:title="title"
>
<!-- Using v-show instead of v-if to avoid flickering as v-if removes dom elements. -->
<img
v-show="shouldShowImage"
:src="src"
:class="thumbnailClass"
@load="onImgLoad"
@error="onImgError"
/>
<Avatar
v-show="!shouldShowImage"
:username="userNameWithoutEmoji"
:class="thumbnailClass"
:size="avatarSize"
/>
<slot>
<img
v-show="shouldShowImage"
:src="src"
:class="thumbnailClass"
@load="onImgLoad"
@error="onImgError"
/>
<Avatar
v-show="!shouldShowImage"
:username="userNameWithoutEmoji"
:class="thumbnailClass"
:size="avatarSize"
/>
</slot>
<img
v-if="badgeSrc"
class="source-badge"
@@ -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;
@@ -310,11 +310,9 @@ export default {
userFullName: mentionItem.name,
});
const tr = this.editorView.state.tr.replaceWith(
this.range.from,
this.range.to,
node
);
const tr = this.editorView.state.tr
.replaceWith(this.range.from, this.range.to, node)
.insertText(` `);
this.state = this.editorView.state.apply(tr);
this.emitOnChange();
this.$track(CONVERSATION_EVENTS.USED_MENTIONS);
@@ -468,8 +466,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>
@@ -121,8 +121,7 @@ export default {
.top-box {
display: flex;
justify-content: space-between;
background: var(--b-50);
@apply bg-black-50 dark:bg-slate-800;
}
.button-group {
@@ -145,11 +144,11 @@ export default {
.button--reply {
border-radius: 0;
border-right: 1px solid var(--color-border);
@apply border-r border-b-0 border-l-0 border-slate-50 dark:border-slate-700;
&:hover,
&:focus {
border-right: 1px solid var(--color-border);
@apply border-r border-slate-50 dark:border-slate-700;
}
}
@@ -157,7 +156,7 @@ export default {
border-radius: 0;
&.is-active {
border-right: 1px solid var(--color-border);
@apply border-r border-b-0 border-slate-50 dark:border-slate-700;
background: var(--y-50);
}
@@ -1,16 +1,20 @@
import { Bar } from 'vue-chartjs';
const fontFamily =
'-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';
const defaultChartOptions = {
responsive: true,
maintainAspectRatio: false,
legend: {
display: false,
labels: {
fontFamily,
},
},
animation: {
duration: 0,
},
datasets: {
bar: {
barPercentage: 1.0,
@@ -46,11 +50,11 @@ export default {
props: {
collection: {
type: Object,
default: () => {},
default: () => ({}),
},
chartOptions: {
type: Object,
default: () => {},
default: () => ({}),
},
},
mounted() {
@@ -1,5 +1,9 @@
<template>
<div :class="`status-badge status-badge__${status}`" />
<div
:class="
`status-badge status-badge__${status} rounded-full w-2.5 h-2.5 mr-0.5 rtl:mr-0 rtl:ml-0.5 inline-flex`
"
/>
</template>
<script>
export default {
@@ -9,21 +13,15 @@ export default {
};
</script>
<style lang="scss" scoped>
@import '~dashboard/assets/scss/variables';
.status-badge {
width: var(--space-one);
height: var(--space-one);
margin-right: var(--space-micro);
display: inline-block;
border-radius: 50%;
&__online {
background: var(--g-400);
@apply bg-green-400;
}
&__offline {
background: var(--s-500);
@apply bg-slate-500;
}
&__busy {
background: var(--y-500);
@apply bg-yellow-500;
}
}
</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);

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