diff --git a/.env.example b/.env.example
index 177cb7fc8..55b998f19 100644
--- a/.env.example
+++ b/.env.example
@@ -180,8 +180,6 @@ ANDROID_SHA256_CERT_FINGERPRINT=AC:73:8E:DE:EB:56:EA:CC:10:87:02:A7:65:37:7B:38:
## Sentry
# SENTRY_DSN=
-## LogRocket
-# LOG_ROCKET_PROJECT_ID=xxxxx/some-project
# MICROSOFT CLARITY
# MS_CLARITY_TOKEN=xxxxxxxxx
@@ -251,9 +249,6 @@ AZURE_APP_SECRET=
## OpenAI key
# OPENAI_API_KEY=
-# Sentiment analysis model file path
-SENTIMENT_FILE_PATH=
-
# Housekeeping/Performance related configurations
# Set to true if you want to remove stale contact inboxes
# contact_inboxes with no conversation older than 90 days will be removed
diff --git a/Gemfile b/Gemfile
index 0dac6e95f..f39655a5c 100644
--- a/Gemfile
+++ b/Gemfile
@@ -113,10 +113,10 @@ gem 'newrelic-sidekiq-metrics', '>= 1.6.2', require: false
gem 'scout_apm', require: false
gem 'sentry-rails', '>= 5.14.0', require: false
gem 'sentry-ruby', require: false
-gem 'sentry-sidekiq', '>= 5.14.0', require: false
+gem 'sentry-sidekiq', '>= 5.15.0', require: false
##-- background job processing --##
-gem 'sidekiq', '>= 7.2.1'
+gem 'sidekiq', '>= 7.2.4'
# We want cron jobs
gem 'sidekiq-cron', '>= 1.12.0'
@@ -175,9 +175,6 @@ gem 'pgvector'
# Convert Website HTML to Markdown
gem 'reverse_markdown'
-# Sentiment analysis
-gem 'informers'
-
### Gems required only in specific deployment environments ###
##############################################################
diff --git a/Gemfile.lock b/Gemfile.lock
index 55021a04b..b5ad38993 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -150,8 +150,8 @@ GEM
statsd-ruby (~> 1.1)
base64 (0.1.1)
bcrypt (3.1.20)
+ bigdecimal (3.1.7)
bindex (0.8.1)
- blingfire (0.1.8)
bootsnap (1.16.0)
msgpack (~> 1.2)
brakeman (5.4.1)
@@ -316,15 +316,15 @@ GEM
google-cloud-translate-v3 (0.6.0)
gapic-common (>= 0.17.1, < 2.a)
google-cloud-errors (~> 1.0)
- google-protobuf (3.25.2)
- google-protobuf (3.25.2-arm64-darwin)
- google-protobuf (3.25.2-x86_64-darwin)
- google-protobuf (3.25.2-x86_64-linux)
+ google-protobuf (3.25.3)
+ google-protobuf (3.25.3-arm64-darwin)
+ google-protobuf (3.25.3-x86_64-darwin)
+ google-protobuf (3.25.3-x86_64-linux)
googleapis-common-protos (1.4.0)
google-protobuf (~> 3.14)
googleapis-common-protos-types (~> 1.2)
grpc (~> 1.27)
- googleapis-common-protos-types (1.11.0)
+ googleapis-common-protos-types (1.14.0)
google-protobuf (~> 3.18)
googleauth (1.5.2)
faraday (>= 0.17.3, < 3.a)
@@ -335,14 +335,17 @@ GEM
signet (>= 0.16, < 2.a)
groupdate (6.2.1)
activesupport (>= 5.2)
- grpc (1.54.3)
- google-protobuf (~> 3.21)
+ grpc (1.62.0)
+ google-protobuf (~> 3.25)
googleapis-common-protos-types (~> 1.0)
- grpc (1.54.3-x86_64-darwin)
- google-protobuf (~> 3.21)
+ grpc (1.62.0-arm64-darwin)
+ google-protobuf (~> 3.25)
googleapis-common-protos-types (~> 1.0)
- grpc (1.54.3-x86_64-linux)
- google-protobuf (~> 3.21)
+ grpc (1.62.0-x86_64-darwin)
+ google-protobuf (~> 3.25)
+ googleapis-common-protos-types (~> 1.0)
+ grpc (1.62.0-x86_64-linux)
+ google-protobuf (~> 3.25)
googleapis-common-protos-types (~> 1.0)
haikunator (1.1.1)
hairtrigger (1.0.0)
@@ -371,10 +374,6 @@ 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)
@@ -500,7 +499,6 @@ GEM
racc (~> 1.4)
nokogiri (1.16.4-x86_64-linux)
racc (~> 1.4)
- numo-narray (0.9.2.1)
oauth (1.1.0)
oauth-tty (~> 1.0, >= 1.0.1)
snaky_hash (~> 2.0)
@@ -529,14 +527,6 @@ 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)
@@ -614,7 +604,7 @@ GEM
ffi (~> 1.0)
redis (5.0.6)
redis-client (>= 0.9.0)
- redis-client (0.19.1)
+ redis-client (0.22.1)
connection_pool
redis-namespace (1.10.0)
redis (>= 4)
@@ -713,18 +703,19 @@ GEM
activesupport (>= 4)
selectize-rails (0.12.6)
semantic_range (3.0.0)
- sentry-rails (5.14.0)
+ sentry-rails (5.17.3)
railties (>= 5.0)
- sentry-ruby (~> 5.14.0)
- sentry-ruby (5.14.0)
+ sentry-ruby (~> 5.17.3)
+ sentry-ruby (5.17.3)
+ bigdecimal
concurrent-ruby (~> 1.0, >= 1.0.2)
- sentry-sidekiq (5.14.0)
- sentry-ruby (~> 5.14.0)
+ sentry-sidekiq (5.17.3)
+ sentry-ruby (~> 5.17.3)
sidekiq (>= 3.0)
sexp_processor (4.17.0)
shoulda-matchers (5.3.0)
activesupport (>= 5.2.0)
- sidekiq (7.2.1)
+ sidekiq (7.2.4)
concurrent-ruby (< 2)
connection_pool (>= 2.3.0)
rack (>= 2.2.4)
@@ -892,7 +883,6 @@ DEPENDENCIES
hashie
html2text!
image_processing
- informers
jbuilder
json_refs
json_schemer
@@ -943,9 +933,9 @@ DEPENDENCIES
seed_dump
sentry-rails (>= 5.14.0)
sentry-ruby
- sentry-sidekiq (>= 5.14.0)
+ sentry-sidekiq (>= 5.15.0)
shoulda-matchers
- sidekiq (>= 7.2.1)
+ sidekiq (>= 7.2.4)
sidekiq-cron (>= 1.12.0)
simplecov (= 0.17.1)
slack-ruby-client (~> 2.2.0)
diff --git a/app/controllers/public/api/v1/portals_controller.rb b/app/controllers/public/api/v1/portals_controller.rb
index fe51ed161..e8fb867cb 100644
--- a/app/controllers/public/api/v1/portals_controller.rb
+++ b/app/controllers/public/api/v1/portals_controller.rb
@@ -6,6 +6,12 @@ class Public::Api::V1::PortalsController < Public::Api::V1::Portals::BaseControl
def show; end
+ def sitemap
+ @help_center_url = @portal.custom_domain || ChatwootApp.help_center_root
+ # if help_center_url does not contain a protocol, prepend it with https
+ @help_center_url = "https://#{@help_center_url}" unless @help_center_url.include?('://')
+ end
+
private
def portal
diff --git a/app/javascript/dashboard/assets/scss/_woot.scss b/app/javascript/dashboard/assets/scss/_woot.scss
index 06dabb7cb..4b33d5b10 100644
--- a/app/javascript/dashboard/assets/scss/_woot.scss
+++ b/app/javascript/dashboard/assets/scss/_woot.scss
@@ -42,19 +42,20 @@
}
@layer base {
+ // scss-lint:disable PropertySortOrder
:root {
- --color-amber-100: 255 243 208;
- --color-amber-200: 255 236 183;
--color-amber-25: 254 253 251;
- --color-amber-300: 255 224 161;
- --color-amber-400: 245 208 140;
--color-amber-50: 255 249 237;
- --color-amber-500: 228 187 120;
+ --color-amber-75: 255 243 208;
+ --color-amber-100: 255 236 183;
+ --color-amber-200: 255 224 161;
+ --color-amber-300: 245 208 140;
+ --color-amber-400: 228 187 120;
+ --color-amber-500: 214 163 92;
--color-amber-600: 214 163 92;
- --color-amber-700: 214 163 92;
- --color-amber-800: 255 186 26;
- --color-amber-900: 145 89 48;
- --color-amber-950: 79 52 34;
+ --color-amber-700: 255 186 26;
+ --color-amber-800: 145 89 48;
+ --color-amber-900: 79 52 34;
--color-ash-100: 235 235 239;
--color-ash-200: 228 228 233;
@@ -69,18 +70,18 @@
--color-ash-800: 96 100 108;
--color-ash-900: 28 32 36;
- --color-primary-100: 222 234 255;
- --color-primary-200: 206 223 255;
- --color-primary-25: 252 253 254;
- --color-primary-300: 186 209 255;
- --color-primary-400: 163 190 255;
- --color-primary-50: 246 249 255;
- --color-primary-500: 131 164 250;
- --color-primary-600: 69 110 255;
- --color-primary-700: 57 95 241;
- --color-primary-75: 236 242 255;
- --color-primary-800: 53 88 217;
- --color-primary-900: 27 44 99;
+ --color-primary-25: 251 253 255;
+ --color-primary-50: 245 249 255;
+ --color-primary-75: 233 243 255;
+ --color-primary-100: 218 236 255;
+ --color-primary-200: 201 226 255;
+ --color-primary-300: 181 213 255;
+ --color-primary-400: 155 195 252;
+ --color-primary-500: 117 171 247;
+ --color-primary-600: 39 129 246;
+ --color-primary-700: 16 115 233;
+ --color-primary-800: 8 109 224;
+ --color-primary-900: 11 50 101;
--color-ruby-100: 255 220 225;
--color-ruby-200: 255 206 214;
@@ -107,21 +108,125 @@
--color-teal-75: 236 249 255;
--color-teal-800: 0 133 115;
--color-teal-900: 13 61 56;
+
+ --color-green-25: 251 254 252;
+ --color-green-50: 244 251 246;
+ --color-green-75: 230 246 235;
+ --color-green-100: 214 241 223;
+ --color-green-200: 196 232 209;
+ --color-green-300: 173 221 192;
+ --color-green-400: 142 206 170;
+ --color-green-500: 91 185 139;
+ --color-green-600: 48 164 108;
+ --color-green-700: 43 154 102;
+ --color-green-800: 33 131 88;
+ --color-green-900: 25 59 45;
+
+ --color-mint-25: 249 254 253;
+ --color-mint-50: 242 251 249;
+ --color-mint-75: 221 249 242;
+ --color-mint-100: 200 244 233;
+ --color-mint-200: 179 236 222;
+ --color-mint-300: 156 224 208;
+ --color-mint-400: 126 207 189;
+ --color-mint-500: 76 187 165;
+ --color-mint-600: 134 234 212;
+ --color-mint-700: 125 224 203;
+ --color-mint-800: 2 120 100;
+ --color-mint-900: 22 67 60;
+
+ --color-sky-25: 249 254 255;
+ --color-sky-50: 241 250 253;
+ --color-sky-75: 225 246 253;
+ --color-sky-100: 209 240 250;
+ --color-sky-200: 190 231 245;
+ --color-sky-300: 169 218 237;
+ --color-sky-400: 141 202 227;
+ --color-sky-500: 96 179 215;
+ --color-sky-600: 124 226 254;
+ --color-sky-700: 116 218 248;
+ --color-sky-800: 0 116 158;
+ --color-sky-900: 29 62 86;
+
+ --color-indigo-25: 253 253 254;
+ --color-indigo-50: 247 249 255;
+ --color-indigo-75: 237 242 254;
+ --color-indigo-100: 225 233 255;
+ --color-indigo-200: 210 222 255;
+ --color-indigo-300: 193 208 255;
+ --color-indigo-400: 171 189 249;
+ --color-indigo-500: 141 164 239;
+ --color-indigo-600: 62 99 221;
+ --color-indigo-700: 51 88 212;
+ --color-indigo-800: 58 91 199;
+ --color-indigo-900: 31 45 92;
+
+ --color-iris-25: 253 253 255;
+ --color-iris-50: 248 248 255;
+ --color-iris-75: 240 241 254;
+ --color-iris-100: 230 231 255;
+ --color-iris-200: 218 220 255;
+ --color-iris-300: 203 205 255;
+ --color-iris-400: 184 186 248;
+ --color-iris-500: 155 158 240;
+ --color-iris-600: 91 91 214;
+ --color-iris-700: 81 81 205;
+ --color-iris-800: 87 83 198;
+ --color-iris-900: 39 41 98;
+
+ --color-violet-25: 253 252 254;
+ --color-violet-50: 250 248 255;
+ --color-violet-75: 244 240 254;
+ --color-violet-100: 235 228 255;
+ --color-violet-200: 225 217 255;
+ --color-violet-300: 212 202 254;
+ --color-violet-400: 194 181 245;
+ --color-violet-500: 170 153 236;
+ --color-violet-600: 110 86 207;
+ --color-violet-700: 101 77 196;
+ --color-violet-800: 101 80 185;
+ --color-violet-900: 47 38 95;
+
+ --color-pink-25: 255 252 254;
+ --color-pink-50: 254 247 251;
+ --color-pink-75: 254 233 245;
+ --color-pink-100: 251 220 239;
+ --color-pink-200: 246 206 231;
+ --color-pink-300: 239 191 221;
+ --color-pink-400: 231 172 208;
+ --color-pink-500: 221 147 194;
+ --color-pink-600: 214 64 159;
+ --color-pink-700: 207 56 151;
+ --color-pink-800: 194 41 138;
+ --color-pink-900: 101 18 73;
+
+ --color-orange-25: 254 252 251;
+ --color-orange-50: 255 247 237;
+ --color-orange-75: 255 239 214;
+ --color-orange-100: 255 223 181;
+ --color-orange-200: 255 209 154;
+ --color-orange-300: 255 193 130;
+ --color-orange-400: 245 174 115;
+ --color-orange-500: 236 148 85;
+ --color-orange-600: 247 107 21;
+ --color-orange-700: 239 95 0;
+ --color-orange-800: 204 78 0;
+ --color-orange-900: 88 45 29;
}
// scss-lint:disable QualifyingElement
body.dark {
+ --color-amber-25: 31 19 0;
+ --color-amber-50: 37 24 4;
+ --color-amber-75: 48 32 11;
--color-amber-100: 57 39 15;
--color-amber-200: 67 46 18;
- --color-amber-25: 31 19 0;
--color-amber-300: 83 57 22;
--color-amber-400: 111 77 29;
- --color-amber-50: 37 24 4;
--color-amber-500: 169 118 42;
--color-amber-600: 169 118 42;
--color-amber-700: 255 203 71;
--color-amber-800: 255 204 77;
--color-amber-900: 255 231 179;
- --color-amber-950: 255 231 179;
--color-ash-100: 46 48 53;
--color-ash-200: 53 55 60;
@@ -136,18 +241,18 @@
--color-ash-800: 173 177 184;
--color-ash-900: 237 238 240;
- --color-primary-100: 24 43 109;
- --color-primary-200: 31 53 129;
- --color-primary-25: 11 16 31;
- --color-primary-300: 40 64 146;
- --color-primary-400: 48 76 168;
- --color-primary-50: 16 23 41;
- --color-primary-500: 57 89 196;
- --color-primary-600: 69 110 255;
- --color-primary-700: 58 96 240;
- --color-primary-75: 20 34 81;
- --color-primary-800: 140 177 255;
- --color-primary-900: 210 225 255;
+ --color-primary-25: 10 17 28;
+ --color-primary-50: 15 24 38;
+ --color-primary-75: 15 39 72;
+ --color-primary-100: 10 49 99;
+ --color-primary-200: 18 61 117;
+ --color-primary-300: 29 74 134;
+ --color-primary-400: 40 89 156;
+ --color-primary-500: 48 106 186;
+ --color-primary-600: 39 129 246;
+ --color-primary-700: 21 116 231;
+ --color-primary-800: 126 182 255;
+ --color-primary-900: 205 227 255;
--color-ruby-100: 78 19 37;
--color-ruby-200: 94 26 46;
@@ -174,5 +279,108 @@
--color-teal-75: 13 45 42;
--color-teal-800: 11 216 182;
--color-teal-900: 173 240 221;
+
+ --color-green-25: 14 21 18;
+ --color-green-50: 18 27 23;
+ --color-green-75: 19 45 33;
+ --color-green-100: 17 59 41;
+ --color-green-200: 23 73 51;
+ --color-green-300: 32 87 62;
+ --color-green-400: 40 104 74;
+ --color-green-500: 47 124 87;
+ --color-green-600: 48 164 108;
+ --color-green-700: 51 176 116;
+ --color-green-800: 61 214 140;
+ --color-green-900: 177 241 203;
+
+ --color-mint-25: 14 21 21;
+ --color-mint-50: 15 27 27;
+ --color-mint-75: 9 44 43;
+ --color-mint-100: 0 58 56;
+ --color-mint-200: 0 71 68;
+ --color-mint-300: 16 86 80;
+ --color-mint-400: 30 104 95;
+ --color-mint-500: 39 127 112;
+ --color-mint-600: 134 234 212;
+ --color-mint-700: 168 245 229;
+ --color-mint-800: 88 213 186;
+ --color-mint-900: 196 245 225;
+
+ --color-sky-25: 14 21 21;
+ --color-sky-50: 15 27 27;
+ --color-sky-75: 9 44 43;
+ --color-sky-100: 0 58 56;
+ --color-sky-200: 0 71 68;
+ --color-sky-300: 16 86 80;
+ --color-sky-400: 30 104 95;
+ --color-sky-500: 39 127 112;
+ --color-sky-600: 134 234 212;
+ --color-sky-700: 168 245 229;
+ --color-sky-800: 88 213 186;
+ --color-sky-900: 196 245 225;
+
+ --color-indigo-25: 17 19 31;
+ --color-indigo-50: 20 23 38;
+ --color-indigo-75: 24 36 73;
+ --color-indigo-100: 29 46 98;
+ --color-indigo-200: 37 57 116;
+ --color-indigo-300: 48 67 132;
+ --color-indigo-400: 58 79 151;
+ --color-indigo-500: 67 93 177;
+ --color-indigo-600: 62 99 221;
+ --color-indigo-700: 84 114 228;
+ --color-indigo-800: 158 177 255;
+ --color-indigo-900: 214 225 255;
+
+ --color-iris-25: 19 19 30;
+ --color-iris-50: 23 22 37;
+ --color-iris-75: 32 34 72;
+ --color-iris-100: 38 42 101;
+ --color-iris-200: 48 51 116;
+ --color-iris-300: 61 62 130;
+ --color-iris-400: 74 74 149;
+ --color-iris-500: 89 88 177;
+ --color-iris-600: 91 91 214;
+ --color-iris-700: 110 106 222;
+ --color-iris-800: 177 169 255;
+ --color-iris-900: 224 223 254;
+
+ --color-violet-25: 20 18 31;
+ --color-violet-50: 27 21 37;
+ --color-violet-75: 41 31 67;
+ --color-violet-100: 51 37 91;
+ --color-violet-200: 60 46 105;
+ --color-violet-300: 71 56 118;
+ --color-violet-400: 86 70 139;
+ --color-violet-500: 105 88 173;
+ --color-violet-600: 110 86 207;
+ --color-violet-700: 125 102 217;
+ --color-violet-800: 186 167 255;
+ --color-violet-900: 226 221 254;
+
+ --color-pink-25: 25 17 23;
+ --color-pink-50: 33 18 29;
+ --color-pink-75: 55 23 47;
+ --color-pink-100: 75 20 61;
+ --color-pink-200: 89 28 71;
+ --color-pink-300: 105 41 85;
+ --color-pink-400: 131 56 105;
+ --color-pink-500: 168 72 133;
+ --color-pink-600: 214 64 159;
+ --color-pink-700: 222 81 168;
+ --color-pink-800: 255 141 204;
+ --color-pink-900: 253 209 234;
+ --color-orange-25: 23 18 14;
+ --color-orange-50: 30 22 15;
+ --color-orange-75: 51 30 11;
+ --color-orange-100: 70 33 0;
+ --color-orange-200: 86 40 0;
+ --color-orange-300: 102 53 12;
+ --color-orange-400: 126 69 29;
+ --color-orange-500: 163 88 41;
+ --color-orange-600: 247 107 21;
+ --color-orange-700: 255 128 31;
+ --color-orange-800: 255 160 87;
+ --color-orange-900: 255 224 194;
}
}
diff --git a/app/javascript/dashboard/components/ChatList.vue b/app/javascript/dashboard/components/ChatList.vue
index 26c057f98..e7fae6e01 100644
--- a/app/javascript/dashboard/components/ChatList.vue
+++ b/app/javascript/dashboard/components/ChatList.vue
@@ -185,7 +185,7 @@ import ConversationBasicFilter from './widgets/conversation/ConversationBasicFil
import ChatTypeTabs from './widgets/ChatTypeTabs.vue';
import ConversationItem from './ConversationItem.vue';
import timeMixin from '../mixins/time';
-import eventListenerMixins from 'shared/mixins/eventListenerMixins';
+import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
import conversationMixin from '../mixins/conversations';
import wootConstants from 'dashboard/constants/globals';
import advancedFilterTypes from './widgets/conversation/advancedFilterItems';
@@ -199,11 +199,6 @@ import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import languages from 'dashboard/components/widgets/conversation/advancedFilterItems/languages';
import countries from 'shared/constants/countries';
import { generateValuesForEditCustomViews } from 'dashboard/helper/customViewsHelper';
-
-import {
- hasPressedAltAndJKey,
- hasPressedAltAndKKey,
-} from 'shared/helpers/KeyboardHelpers';
import { conversationListPageURL } from '../helper/URLHelper';
import {
isOnMentionsView,
@@ -228,7 +223,7 @@ export default {
mixins: [
timeMixin,
conversationMixin,
- eventListenerMixins,
+ keyboardEventListenerMixins,
alertMixin,
filterMixin,
uiSettingsMixin,
@@ -691,30 +686,40 @@ export default {
lastConversationIndex,
};
},
- handleKeyEvents(e) {
- if (hasPressedAltAndJKey(e)) {
- const { allConversations, activeConversationIndex } =
- this.getKeyboardListenerParams();
- if (activeConversationIndex === -1) {
- allConversations[0].click();
- }
- if (activeConversationIndex >= 1) {
- allConversations[activeConversationIndex - 1].click();
- }
+ handlePreviousConversation() {
+ const { allConversations, activeConversationIndex } =
+ this.getKeyboardListenerParams();
+ if (activeConversationIndex === -1) {
+ allConversations[0].click();
}
- if (hasPressedAltAndKKey(e)) {
- const {
- allConversations,
- activeConversationIndex,
- lastConversationIndex,
- } = this.getKeyboardListenerParams();
- if (activeConversationIndex === -1) {
- allConversations[lastConversationIndex].click();
- } else if (activeConversationIndex < lastConversationIndex) {
- allConversations[activeConversationIndex + 1].click();
- }
+ if (activeConversationIndex >= 1) {
+ allConversations[activeConversationIndex - 1].click();
}
},
+ handleNextConversation() {
+ const {
+ allConversations,
+ activeConversationIndex,
+ lastConversationIndex,
+ } = this.getKeyboardListenerParams();
+ if (activeConversationIndex === -1) {
+ allConversations[lastConversationIndex].click();
+ } else if (activeConversationIndex < lastConversationIndex) {
+ allConversations[activeConversationIndex + 1].click();
+ }
+ },
+ getKeyboardEvents() {
+ return {
+ 'Alt+KeyJ': {
+ action: () => this.handlePreviousConversation(),
+ allowOnFocusedInput: true,
+ },
+ 'Alt+KeyK': {
+ action: () => this.handleNextConversation(),
+ allowOnFocusedInput: true,
+ },
+ };
+ },
resetAndFetchData() {
this.appliedFilter = [];
this.resetBulkActions();
diff --git a/app/javascript/dashboard/components/CustomAttribute.vue b/app/javascript/dashboard/components/CustomAttribute.vue
index b50ad54cb..2c0fb48f7 100644
--- a/app/javascript/dashboard/components/CustomAttribute.vue
+++ b/app/javascript/dashboard/components/CustomAttribute.vue
@@ -12,7 +12,7 @@
{{ label }}
+
diff --git a/app/javascript/dashboard/components/buttons/ResolveAction.vue b/app/javascript/dashboard/components/buttons/ResolveAction.vue
index 84e33cc42..61375dc21 100644
--- a/app/javascript/dashboard/components/buttons/ResolveAction.vue
+++ b/app/javascript/dashboard/components/buttons/ResolveAction.vue
@@ -88,15 +88,9 @@
+
+
+
+
diff --git a/app/javascript/dashboard/components/ui/DatePicker/components/CalendarAction.vue b/app/javascript/dashboard/components/ui/DatePicker/components/CalendarAction.vue
new file mode 100644
index 000000000..447a83bf3
--- /dev/null
+++ b/app/javascript/dashboard/components/ui/DatePicker/components/CalendarAction.vue
@@ -0,0 +1,79 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components/ui/DatePicker/components/CalendarDateInput.vue b/app/javascript/dashboard/components/ui/DatePicker/components/CalendarDateInput.vue
new file mode 100644
index 000000000..b9fe88993
--- /dev/null
+++ b/app/javascript/dashboard/components/ui/DatePicker/components/CalendarDateInput.vue
@@ -0,0 +1,74 @@
+
+
+
+
+
+ {{
+ calendarType === START_CALENDAR
+ ? $t('DATE_PICKER.DATE_RANGE_INPUT.START')
+ : $t('DATE_PICKER.DATE_RANGE_INPUT.END')
+ }}
+
+
+
+
diff --git a/app/javascript/dashboard/components/ui/DatePicker/components/CalendarDateRange.vue b/app/javascript/dashboard/components/ui/DatePicker/components/CalendarDateRange.vue
new file mode 100644
index 000000000..38d4dd76f
--- /dev/null
+++ b/app/javascript/dashboard/components/ui/DatePicker/components/CalendarDateRange.vue
@@ -0,0 +1,41 @@
+
+
+
+
+
+ {{ $t('DATE_PICKER.DATE_RANGE_OPTIONS.TITLE') }}
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components/ui/DatePicker/components/CalendarFooter.vue b/app/javascript/dashboard/components/ui/DatePicker/components/CalendarFooter.vue
new file mode 100644
index 000000000..828044e1c
--- /dev/null
+++ b/app/javascript/dashboard/components/ui/DatePicker/components/CalendarFooter.vue
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components/ui/DatePicker/components/CalendarMonth.vue b/app/javascript/dashboard/components/ui/DatePicker/components/CalendarMonth.vue
new file mode 100644
index 000000000..ee68e7341
--- /dev/null
+++ b/app/javascript/dashboard/components/ui/DatePicker/components/CalendarMonth.vue
@@ -0,0 +1,86 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components/ui/DatePicker/components/CalendarWeek.vue b/app/javascript/dashboard/components/ui/DatePicker/components/CalendarWeek.vue
new file mode 100644
index 000000000..c448e3eb1
--- /dev/null
+++ b/app/javascript/dashboard/components/ui/DatePicker/components/CalendarWeek.vue
@@ -0,0 +1,172 @@
+
+
+
+
+
+
+
+
+ {{ day.getDate() }}
+
+
+
+
+
diff --git a/app/javascript/dashboard/components/ui/DatePicker/components/CalendarWeekLabel.vue b/app/javascript/dashboard/components/ui/DatePicker/components/CalendarWeekLabel.vue
new file mode 100644
index 000000000..ebfe9a374
--- /dev/null
+++ b/app/javascript/dashboard/components/ui/DatePicker/components/CalendarWeekLabel.vue
@@ -0,0 +1,15 @@
+
+
+
+
+
diff --git a/app/javascript/dashboard/components/ui/DatePicker/components/CalendarYear.vue b/app/javascript/dashboard/components/ui/DatePicker/components/CalendarYear.vue
new file mode 100644
index 000000000..1f5e7224f
--- /dev/null
+++ b/app/javascript/dashboard/components/ui/DatePicker/components/CalendarYear.vue
@@ -0,0 +1,86 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components/ui/DatePicker/components/DatePickerButton.vue b/app/javascript/dashboard/components/ui/DatePicker/components/DatePickerButton.vue
new file mode 100644
index 000000000..c5ae54b6f
--- /dev/null
+++ b/app/javascript/dashboard/components/ui/DatePicker/components/DatePickerButton.vue
@@ -0,0 +1,71 @@
+
+
+
+
+
diff --git a/app/javascript/dashboard/components/ui/DatePicker/helpers/DatePickerHelper.js b/app/javascript/dashboard/components/ui/DatePicker/helpers/DatePickerHelper.js
new file mode 100644
index 000000000..85e41603f
--- /dev/null
+++ b/app/javascript/dashboard/components/ui/DatePicker/helpers/DatePickerHelper.js
@@ -0,0 +1,219 @@
+import {
+ startOfDay,
+ subDays,
+ endOfDay,
+ subMonths,
+ addMonths,
+ subYears,
+ addYears,
+ startOfMonth,
+ isSameMonth,
+ format,
+ startOfWeek,
+ addDays,
+ eachDayOfInterval,
+ endOfMonth,
+ isSameDay,
+ isWithinInterval,
+} from 'date-fns';
+
+// Constants for calendar and date ranges
+export const calendarWeeks = [
+ { id: 1, label: 'M' },
+ { id: 2, label: 'T' },
+ { id: 3, label: 'W' },
+ { id: 4, label: 'T' },
+ { id: 5, label: 'F' },
+ { id: 6, label: 'S' },
+ { id: 7, label: 'S' },
+];
+
+export const dateRanges = [
+ { label: 'DATE_PICKER.DATE_RANGE_OPTIONS.LAST_7_DAYS', value: 'last7days' },
+ { label: 'DATE_PICKER.DATE_RANGE_OPTIONS.LAST_30_DAYS', value: 'last30days' },
+ {
+ label: 'DATE_PICKER.DATE_RANGE_OPTIONS.LAST_3_MONTHS',
+ value: 'last3months',
+ },
+ {
+ label: 'DATE_PICKER.DATE_RANGE_OPTIONS.LAST_6_MONTHS',
+ value: 'last6months',
+ },
+ { label: 'DATE_PICKER.DATE_RANGE_OPTIONS.LAST_YEAR', value: 'lastYear' },
+ { label: 'DATE_PICKER.DATE_RANGE_OPTIONS.CUSTOM_RANGE', value: 'custom' },
+];
+
+export const DATE_RANGE_TYPES = {
+ LAST_7_DAYS: 'last7days',
+ LAST_30_DAYS: 'last30days',
+ LAST_3_MONTHS: 'last3months',
+ LAST_6_MONTHS: 'last6months',
+ LAST_YEAR: 'lastYear',
+ CUSTOM_RANGE: 'custom',
+};
+
+export const CALENDAR_TYPES = {
+ START_CALENDAR: 'start',
+ END_CALENDAR: 'end',
+};
+
+export const CALENDAR_PERIODS = {
+ WEEK: 'week',
+ MONTH: 'month',
+ YEAR: 'year',
+};
+
+// Utility functions for date operations
+export const monthName = currentDate => format(currentDate, 'MMMM');
+export const yearName = currentDate => format(currentDate, 'yyyy');
+
+export const getIntlDateFormatForLocale = () => {
+ const year = 2222;
+ const month = 12;
+ const day = 15;
+ const date = new Date(year, month - 1, day);
+ const formattedDate = new Intl.DateTimeFormat(navigator.language).format(
+ date
+ );
+ return formattedDate
+ .replace(`${year}`, 'yyyy')
+ .replace(`${month}`, 'MM')
+ .replace(`${day}`, 'dd');
+};
+
+// Utility functions for calendar operations
+export const chunk = (array, size) =>
+ Array.from({ length: Math.ceil(array.length / size) }, (_, index) =>
+ array.slice(index * size, index * size + size)
+ );
+
+export const getWeeksForMonth = (date, weekStartsOn = 1) => {
+ const startOfTheMonth = startOfMonth(date);
+ const startOfTheFirstWeek = startOfWeek(startOfTheMonth, { weekStartsOn });
+ const endOfTheLastWeek = addDays(startOfTheFirstWeek, 41); // Covering six weeks to fill the calendar
+ return chunk(
+ eachDayOfInterval({ start: startOfTheFirstWeek, end: endOfTheLastWeek }),
+ 7
+ );
+};
+
+export const moveCalendarDate = (
+ calendar,
+ startCurrentDate,
+ endCurrentDate,
+ direction,
+ period
+) => {
+ const adjustFunctions = {
+ month: { prev: subMonths, next: addMonths },
+ year: { prev: subYears, next: addYears },
+ };
+
+ const adjust = adjustFunctions[period][direction];
+
+ if (calendar === 'start') {
+ const newStart = adjust(startCurrentDate, 1);
+ return { start: newStart, end: endCurrentDate };
+ }
+ const newEnd = adjust(endCurrentDate, 1);
+ return { start: startCurrentDate, end: newEnd };
+};
+
+// Date comparison functions
+export const isToday = (currentDate, date) =>
+ date.getDate() === currentDate.getDate() &&
+ date.getMonth() === currentDate.getMonth() &&
+ date.getFullYear() === currentDate.getFullYear();
+
+export const isCurrentMonth = (day, referenceDate) =>
+ isSameMonth(day, referenceDate);
+
+export const isLastDayOfMonth = day => {
+ const lastDay = endOfMonth(day);
+ return day.getDate() === lastDay.getDate();
+};
+
+export const dayIsInRange = (date, startDate, endDate) => {
+ if (!startDate || !endDate) {
+ return false;
+ }
+ // Normalize dates to ignore time differences
+ let startOfDayStart = startOfDay(startDate);
+ let startOfDayEnd = startOfDay(endDate);
+ // Swap if start is greater than end
+ if (startOfDayStart > startOfDayEnd) {
+ [startOfDayStart, startOfDayEnd] = [startOfDayEnd, startOfDayStart];
+ }
+ // Check if the date is within the interval or is the same as the start date
+ return (
+ isSameDay(date, startOfDayStart) ||
+ isWithinInterval(date, {
+ start: startOfDayStart,
+ end: startOfDayEnd,
+ })
+ );
+};
+
+// Handling hovering states in date range pickers
+export const isHoveringDayInRange = (
+ day,
+ startDate,
+ endDate,
+ hoveredEndDate
+) => {
+ if (endDate && hoveredEndDate && startDate <= hoveredEndDate) {
+ // Ensure the start date is not after the hovered end date
+ return isWithinInterval(day, { start: startDate, end: hoveredEndDate });
+ }
+ return false;
+};
+
+export const isHoveringNextDayInRange = (
+ day,
+ startDate,
+ endDate,
+ hoveredEndDate
+) => {
+ if (startDate && !endDate && hoveredEndDate) {
+ // If a start date is selected, and we're hovering (but haven't clicked an end date yet)
+ const nextDay = addDays(day, 1);
+ return isWithinInterval(nextDay, { start: startDate, end: hoveredEndDate });
+ }
+ if (startDate && endDate) {
+ // Normal range checking between selected start and end dates
+ const nextDay = addDays(day, 1);
+ return isWithinInterval(nextDay, { start: startDate, end: endDate });
+ }
+ return false;
+};
+
+// Helper func to determine active date ranges based on user selection
+export const getActiveDateRange = (range, currentDate) => {
+ const ranges = {
+ last7days: () => ({
+ start: startOfDay(subDays(currentDate, 6)),
+ end: endOfDay(currentDate),
+ }),
+ last30days: () => ({
+ start: startOfDay(subDays(currentDate, 29)),
+ end: endOfDay(currentDate),
+ }),
+ last3months: () => ({
+ start: startOfDay(subMonths(currentDate, 3)),
+ end: endOfDay(currentDate),
+ }),
+ last6months: () => ({
+ start: startOfDay(subMonths(currentDate, 6)),
+ end: endOfDay(currentDate),
+ }),
+ lastYear: () => ({
+ start: startOfDay(subMonths(currentDate, 12)),
+ end: endOfDay(currentDate),
+ }),
+ custom: () => ({ start: currentDate, end: currentDate }),
+ };
+
+ return (
+ ranges[range] || (() => ({ start: currentDate, end: currentDate }))
+ )();
+};
diff --git a/app/javascript/dashboard/components/ui/DatePicker/helpers/specs/DatePickerHelper.spec.js b/app/javascript/dashboard/components/ui/DatePicker/helpers/specs/DatePickerHelper.spec.js
new file mode 100644
index 000000000..3510ee0f3
--- /dev/null
+++ b/app/javascript/dashboard/components/ui/DatePicker/helpers/specs/DatePickerHelper.spec.js
@@ -0,0 +1,309 @@
+import {
+ monthName,
+ yearName,
+ getIntlDateFormatForLocale,
+ getWeeksForMonth,
+ isToday,
+ isCurrentMonth,
+ isLastDayOfMonth,
+ dayIsInRange,
+ getActiveDateRange,
+ isHoveringDayInRange,
+ isHoveringNextDayInRange,
+ moveCalendarDate,
+ chunk,
+} from '../DatePickerHelper';
+
+describe('Date formatting functions', () => {
+ const testDate = new Date(2020, 4, 15); // May 15, 2020
+
+ beforeEach(() => {
+ jest.spyOn(navigator, 'language', 'get').mockReturnValue('en-US');
+ });
+
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
+ it('returns the correct month name from a date', () => {
+ expect(monthName(testDate)).toBe('May');
+ });
+
+ it('returns the correct year from a date', () => {
+ expect(yearName(testDate)).toBe('2020');
+ });
+
+ it('returns the correct date format for the current locale en-US', () => {
+ const expected = 'MM/dd/yyyy';
+ expect(getIntlDateFormatForLocale()).toBe(expected);
+ });
+
+ it('returns the correct date format for the current locale en-IN', () => {
+ jest.spyOn(navigator, 'language', 'get').mockReturnValue('en-IN');
+ const expected = 'dd/MM/yyyy';
+ expect(getIntlDateFormatForLocale()).toBe(expected);
+ });
+});
+
+describe('chunk', () => {
+ const array = [1, 2, 3, 4, 5, 6, 7, 8, 9];
+
+ it('correctly chunks an array into smaller arrays of given size', () => {
+ const expected = [
+ [1, 2, 3],
+ [4, 5, 6],
+ [7, 8, 9],
+ ];
+ expect(chunk(array, 3)).toEqual(expected);
+ });
+
+ it('handles arrays that do not divide evenly by the chunk size', () => {
+ const expected = [[1, 2], [3, 4], [5, 6], [7, 8], [9]];
+ expect(chunk(array, 2)).toEqual(expected);
+ });
+});
+
+describe('getWeeksForMonth', () => {
+ it('returns the correct weeks array for a month starting on Monday', () => {
+ const date = new Date(2020, 3, 1); // April 2020
+ const weeks = getWeeksForMonth(date, 1);
+ expect(weeks.length).toBe(6);
+ expect(weeks[0][0]).toEqual(new Date(2020, 2, 30)); // Check if first day of the first week is correct
+ });
+});
+
+describe('moveCalendarDate', () => {
+ it('handles the year transition when moving the start date backward by one month from January', () => {
+ const startDate = new Date(2021, 0, 1);
+ const endDate = new Date(2021, 0, 31);
+ const result = moveCalendarDate(
+ 'start',
+ startDate,
+ endDate,
+ 'prev',
+ 'month'
+ );
+ const expectedStartDate = new Date(2020, 11, 1);
+ expect(result.start.toISOString()).toEqual(expectedStartDate.toISOString());
+ expect(result.end.toISOString()).toEqual(endDate.toISOString());
+ });
+
+ it('handles the year transition when moving the start date forward by one month from January', () => {
+ const startDate = new Date(2020, 0, 1);
+ const endDate = new Date(2020, 1, 31);
+ const result = moveCalendarDate(
+ 'start',
+ startDate,
+ endDate,
+ 'next',
+ 'month'
+ );
+ const expectedStartDate = new Date(2020, 1, 1);
+ expect(result.start.toISOString()).toEqual(expectedStartDate.toISOString());
+ expect(result.end.toISOString()).toEqual(endDate.toISOString());
+ });
+
+ it('handles the year transition when moving the end date forward by one month from December', () => {
+ const startDate = new Date(2021, 11, 1);
+ const endDate = new Date(2021, 11, 31);
+ const result = moveCalendarDate('end', startDate, endDate, 'next', 'month');
+ const expectedEndDate = new Date(2022, 0, 31);
+ expect(result.start).toEqual(startDate);
+ expect(result.end.toISOString()).toEqual(expectedEndDate.toISOString());
+ });
+ it('handles the year transition when moving the end date backward by one month from December', () => {
+ const startDate = new Date(2021, 11, 1);
+ const endDate = new Date(2021, 11, 31);
+ const result = moveCalendarDate('end', startDate, endDate, 'prev', 'month');
+ const expectedEndDate = new Date(2021, 10, 30);
+
+ expect(result.start).toEqual(startDate);
+ expect(result.end.toISOString()).toEqual(expectedEndDate.toISOString());
+ });
+});
+
+describe('isToday', () => {
+ it('returns true if the dates are the same', () => {
+ const today = new Date();
+ const alsoToday = new Date(today);
+ expect(isToday(today, alsoToday)).toBeTruthy();
+ });
+
+ it('returns false if the dates are not the same', () => {
+ const today = new Date();
+ const notToday = new Date(
+ today.getFullYear(),
+ today.getMonth(),
+ today.getDate() - 1
+ );
+ expect(isToday(today, notToday)).toBeFalsy();
+ });
+});
+
+describe('isCurrentMonth', () => {
+ const referenceDate = new Date(2020, 6, 15); // July 15, 2020
+
+ it('returns true if the day is in the same month as the reference date', () => {
+ const testDay = new Date(2020, 6, 1);
+ expect(isCurrentMonth(testDay, referenceDate)).toBeTruthy();
+ });
+
+ it('returns false if the day is not in the same month as the reference date', () => {
+ const testDay = new Date(2020, 5, 30);
+ expect(isCurrentMonth(testDay, referenceDate)).toBeFalsy();
+ });
+});
+
+describe('isLastDayOfMonth', () => {
+ it('returns true if the day is the last day of the month', () => {
+ const testDay = new Date(2020, 6, 31); // July 31, 2020
+ expect(isLastDayOfMonth(testDay)).toBeTruthy();
+ });
+
+ it('returns false if the day is not the last day of the month', () => {
+ const testDay = new Date(2020, 6, 30); // July 30, 2020
+ expect(isLastDayOfMonth(testDay)).toBeFalsy();
+ });
+});
+
+describe('dayIsInRange', () => {
+ it('returns true if the date is within the range', () => {
+ const start = new Date(2020, 1, 10);
+ const end = new Date(2020, 1, 20);
+ const testDate = new Date(2020, 1, 15);
+ expect(dayIsInRange(testDate, start, end)).toBeTruthy();
+ });
+
+ it('returns true if the date is the same as the start date', () => {
+ const start = new Date(2020, 1, 10);
+ const end = new Date(2020, 1, 20);
+ const testDate = new Date(2020, 1, 10);
+ expect(dayIsInRange(testDate, start, end)).toBeTruthy();
+ });
+
+ it('returns false if the date is outside the range', () => {
+ const start = new Date(2020, 1, 10);
+ const end = new Date(2020, 1, 20);
+ const testDate = new Date(2020, 1, 9);
+ expect(dayIsInRange(testDate, start, end)).toBeFalsy();
+ });
+});
+
+describe('isHoveringDayInRange', () => {
+ const startDate = new Date(2020, 6, 10);
+ const endDate = new Date(2020, 6, 20);
+ const hoveredEndDate = new Date(2020, 6, 15);
+
+ it('returns true if the day is within the start and hovered end dates', () => {
+ const testDay = new Date(2020, 6, 12);
+ expect(
+ isHoveringDayInRange(testDay, startDate, endDate, hoveredEndDate)
+ ).toBeTruthy();
+ });
+
+ it('returns false if the day is outside the hovered date range', () => {
+ const testDay = new Date(2020, 6, 16);
+ expect(
+ isHoveringDayInRange(testDay, startDate, endDate, hoveredEndDate)
+ ).toBeFalsy();
+ });
+});
+
+describe('isHoveringNextDayInRange', () => {
+ const startDate = new Date(2020, 6, 10);
+ const hoveredEndDate = new Date(2020, 6, 15);
+
+ it('returns true if the next day after the given day is within the start and hovered end dates', () => {
+ const testDay = new Date(2020, 6, 14);
+ expect(
+ isHoveringNextDayInRange(testDay, startDate, null, hoveredEndDate)
+ ).toBeTruthy();
+ });
+
+ it('returns false if the next day is outside the start and hovered end dates', () => {
+ const testDay = new Date(2020, 6, 15);
+ expect(
+ isHoveringNextDayInRange(testDay, startDate, null, hoveredEndDate)
+ ).toBeFalsy();
+ });
+});
+
+describe('getActiveDateRange', () => {
+ const currentDate = new Date(2020, 5, 15, 12, 0); // May 15, 2020, at noon
+
+ beforeAll(() => {
+ // Mocking the current date to ensure consistency in tests
+ jest.useFakeTimers().setSystemTime(currentDate.getTime());
+ });
+
+ afterAll(() => {
+ jest.useRealTimers();
+ });
+
+ it('returns the correct range for "last7days"', () => {
+ const range = getActiveDateRange('last7days', new Date());
+ const expectedStart = new Date(2020, 5, 9);
+ expectedStart.setHours(0, 0, 0, 0);
+ const expectedEnd = new Date();
+ expectedEnd.setHours(23, 59, 59, 999);
+
+ expect(range.start).toEqual(expectedStart);
+ expect(range.end).toEqual(expectedEnd);
+ });
+
+ it('returns the correct range for "last30days"', () => {
+ const range = getActiveDateRange('last30days', new Date());
+ const expectedStart = new Date(2020, 4, 17);
+ expectedStart.setHours(0, 0, 0, 0);
+ const expectedEnd = new Date();
+ expectedEnd.setHours(23, 59, 59, 999);
+
+ expect(range.start).toEqual(expectedStart);
+ expect(range.end).toEqual(expectedEnd);
+ });
+
+ it('returns the correct range for "last3months"', () => {
+ const range = getActiveDateRange('last3months', new Date());
+ const expectedStart = new Date(2020, 2, 15);
+ expectedStart.setHours(0, 0, 0, 0);
+ const expectedEnd = new Date();
+ expectedEnd.setHours(23, 59, 59, 999);
+
+ expect(range.start).toEqual(expectedStart);
+ expect(range.end).toEqual(expectedEnd);
+ });
+
+ it('returns the correct range for "last6months"', () => {
+ const range = getActiveDateRange('last6months', new Date());
+ const expectedStart = new Date(2019, 11, 15);
+ expectedStart.setHours(0, 0, 0, 0);
+ const expectedEnd = new Date();
+ expectedEnd.setHours(23, 59, 59, 999);
+
+ expect(range.start).toEqual(expectedStart);
+ expect(range.end).toEqual(expectedEnd);
+ });
+
+ it('returns the correct range for "lastYear"', () => {
+ const range = getActiveDateRange('lastYear', new Date());
+ const expectedStart = new Date(2019, 5, 15);
+ expectedStart.setHours(0, 0, 0, 0);
+ const expectedEnd = new Date();
+ expectedEnd.setHours(23, 59, 59, 999);
+
+ expect(range.start).toEqual(expectedStart);
+ expect(range.end).toEqual(expectedEnd);
+ });
+
+ it('returns the correct range for "custom date range"', () => {
+ const range = getActiveDateRange('custom', new Date());
+ expect(range.start).toEqual(new Date(currentDate));
+ expect(range.end).toEqual(new Date(currentDate));
+ });
+
+ it('handles an unknown range label gracefully', () => {
+ const range = getActiveDateRange('unknown', new Date());
+ expect(range.start).toEqual(new Date(currentDate));
+ expect(range.end).toEqual(new Date(currentDate));
+ });
+});
diff --git a/app/javascript/dashboard/components/ui/HelperTextPopup.vue b/app/javascript/dashboard/components/ui/HelperTextPopup.vue
new file mode 100644
index 000000000..fb14692f6
--- /dev/null
+++ b/app/javascript/dashboard/components/ui/HelperTextPopup.vue
@@ -0,0 +1,24 @@
+
+
+
+
diff --git a/app/javascript/dashboard/components/widgets/AIAssistanceButton.vue b/app/javascript/dashboard/components/widgets/AIAssistanceButton.vue
index 2d7951b05..a64743547 100644
--- a/app/javascript/dashboard/components/widgets/AIAssistanceButton.vue
+++ b/app/javascript/dashboard/components/widgets/AIAssistanceButton.vue
@@ -40,8 +40,7 @@ import AIAssistanceModal from './AIAssistanceModal.vue';
import adminMixin from 'dashboard/mixins/aiMixin';
import aiMixin from 'dashboard/mixins/isAdmin';
import { CMD_AI_ASSIST } from 'dashboard/routes/dashboard/commands/commandBarBusEvents';
-import eventListenerMixins from 'shared/mixins/eventListenerMixins';
-import { buildHotKeys } from 'shared/helpers/KeyboardHelpers';
+import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import AIAssistanceCTAButton from './AIAssistanceCTAButton.vue';
@@ -51,7 +50,7 @@ export default {
AICTAModal,
AIAssistanceCTAButton,
},
- mixins: [aiMixin, eventListenerMixins, adminMixin, uiSettingsMixin],
+ mixins: [aiMixin, keyboardEventListenerMixins, adminMixin, uiSettingsMixin],
data: () => ({
showAIAssistanceModal: false,
showAICtaModal: false,
@@ -87,14 +86,17 @@ export default {
},
methods: {
- 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 = '';
- }
+ getKeyboardEvents() {
+ return {
+ '$mod+KeyZ': {
+ action: () => {
+ if (this.initialMessage) {
+ this.$emit('replace-text', this.initialMessage);
+ this.initialMessage = '';
+ }
+ },
+ },
+ };
},
hideAIAssistanceModal() {
this.recordAnalytics('DISMISS_AI_SUGGESTION', {
diff --git a/app/javascript/dashboard/components/widgets/AttachmentsPreview.vue b/app/javascript/dashboard/components/widgets/AttachmentsPreview.vue
index f73474977..32af9e499 100644
--- a/app/javascript/dashboard/components/widgets/AttachmentsPreview.vue
+++ b/app/javascript/dashboard/components/widgets/AttachmentsPreview.vue
@@ -1,17 +1,17 @@
-
+
![]()
-
+
📄
@@ -23,73 +23,62 @@
-
+
{{ formatFileSize(attachment.resource) }}
onRemoveAttachment(index)"
+ @click="onRemoveAttachment(index)"
/>
-
-
diff --git a/app/javascript/dashboard/components/widgets/ChatTypeTabs.vue b/app/javascript/dashboard/components/widgets/ChatTypeTabs.vue
index 64db13f5c..86e80748d 100644
--- a/app/javascript/dashboard/components/widgets/ChatTypeTabs.vue
+++ b/app/javascript/dashboard/components/widgets/ChatTypeTabs.vue
@@ -10,11 +10,10 @@
+
+
+
+
+ {{ title }}
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/routes/dashboard/helpcenter/components/PortalPopover.vue b/app/javascript/dashboard/routes/dashboard/helpcenter/components/PortalPopover.vue
index 47a1c59b5..3de2a6f52 100644
--- a/app/javascript/dashboard/routes/dashboard/helpcenter/components/PortalPopover.vue
+++ b/app/javascript/dashboard/routes/dashboard/helpcenter/components/PortalPopover.vue
@@ -47,13 +47,11 @@
-
+
+function onFileChange({ file }) {
+ if (checkFileSizeLimit(file, MAXIMUM_FILE_UPLOAD_SIZE)) {
+ uploadLogoToStorage(file);
+ } else {
+ const errorKey =
+ 'PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.IMAGE_UPLOAD_SIZE_ERROR';
+ useAlert(t(errorKey, { size: MAXIMUM_FILE_UPLOAD_SIZE }));
+ }
+}
+
+
+
+
+
+
+
+
+
+
+ {{ $t('PROFILE_SETTINGS.DELETE_AVATAR') }}
+
+
+
+
+ {{ $t('HELP_CENTER.PORTAL.ADD.LOGO.HELP_TEXT') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ submitButtonText }}
+
+
+
+
diff --git a/app/javascript/dashboard/routes/dashboard/helpcenter/components/PortalSettingsCustomizationForm.vue b/app/javascript/dashboard/routes/dashboard/helpcenter/components/PortalSettingsCustomizationForm.vue
index c00ab9beb..7572f5b80 100644
--- a/app/javascript/dashboard/routes/dashboard/helpcenter/components/PortalSettingsCustomizationForm.vue
+++ b/app/javascript/dashboard/routes/dashboard/helpcenter/components/PortalSettingsCustomizationForm.vue
@@ -1,68 +1,140 @@
+
+
-
-
-
- {{
- $t('HELP_CENTER.PORTAL.ADD.CREATE_FLOW_PAGE.CUSTOMIZATION_PAGE.TITLE')
- }}
-
-
-
-
-
-
-
-
- {{ $t('HELP_CENTER.PORTAL.ADD.THEME_COLOR.HELP_TEXT') }}
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+ {{ $t('HELP_CENTER.PORTAL.ADD.THEME_COLOR.HELP_TEXT') }}
+
+
+
+
+
+
+
+
+
+
-
+
{{
@@ -71,94 +143,12 @@
)
}}
-
-
+
+
-
diff --git a/app/javascript/dashboard/routes/dashboard/helpcenter/pages/categories/NameEmojiInput.vue b/app/javascript/dashboard/routes/dashboard/helpcenter/pages/categories/NameEmojiInput.vue
index 5d6862758..b888a5440 100644
--- a/app/javascript/dashboard/routes/dashboard/helpcenter/pages/categories/NameEmojiInput.vue
+++ b/app/javascript/dashboard/routes/dashboard/helpcenter/pages/categories/NameEmojiInput.vue
@@ -36,13 +36,10 @@
+
-
-
diff --git a/app/javascript/dashboard/routes/dashboard/helpcenter/pages/portals/EditPortalCustomization.vue b/app/javascript/dashboard/routes/dashboard/helpcenter/pages/portals/EditPortalCustomization.vue
index d1c9341f1..902dc3ca6 100644
--- a/app/javascript/dashboard/routes/dashboard/helpcenter/pages/portals/EditPortalCustomization.vue
+++ b/app/javascript/dashboard/routes/dashboard/helpcenter/pages/portals/EditPortalCustomization.vue
@@ -1,3 +1,48 @@
+
+
-
-
diff --git a/app/javascript/dashboard/routes/dashboard/helpcenter/pages/portals/EditPortalLocales.vue b/app/javascript/dashboard/routes/dashboard/helpcenter/pages/portals/EditPortalLocales.vue
index 3c1c4c872..e6cee06a4 100644
--- a/app/javascript/dashboard/routes/dashboard/helpcenter/pages/portals/EditPortalLocales.vue
+++ b/app/javascript/dashboard/routes/dashboard/helpcenter/pages/portals/EditPortalLocales.vue
@@ -1,6 +1,117 @@
+
+
-