Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e90600c03 | ||
|
|
19410dfa3d | ||
|
|
78f94511ff | ||
|
|
d88d0bdd80 | ||
|
|
47f8b2cd0c | ||
|
|
ffd47081bd | ||
|
|
77db0d0701 | ||
|
|
e757237029 | ||
|
|
04ae38076f | ||
|
|
2ddf613c58 | ||
|
|
1bf3ad9a08 | ||
|
|
9086650fe2 | ||
|
|
8c813097db | ||
|
|
12403b75d6 | ||
|
|
480cf4952d |
@@ -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
|
||||||
# SENTRY_DSN=
|
# SENTRY_DSN=
|
||||||
|
|
||||||
## LogRocket
|
|
||||||
# LOG_ROCKET_PROJECT_ID=xxxxx/some-project
|
|
||||||
|
|
||||||
# MICROSOFT CLARITY
|
# MICROSOFT CLARITY
|
||||||
# MS_CLARITY_TOKEN=xxxxxxxxx
|
# MS_CLARITY_TOKEN=xxxxxxxxx
|
||||||
@@ -251,9 +249,6 @@ AZURE_APP_SECRET=
|
|||||||
## OpenAI key
|
## OpenAI key
|
||||||
# OPENAI_API_KEY=
|
# OPENAI_API_KEY=
|
||||||
|
|
||||||
# Sentiment analysis model file path
|
|
||||||
SENTIMENT_FILE_PATH=
|
|
||||||
|
|
||||||
# Housekeeping/Performance related configurations
|
# Housekeeping/Performance related configurations
|
||||||
# Set to true if you want to remove stale contact inboxes
|
# Set to true if you want to remove stale contact inboxes
|
||||||
# contact_inboxes with no conversation older than 90 days will be removed
|
# contact_inboxes with no conversation older than 90 days will be removed
|
||||||
|
|||||||
@@ -175,9 +175,6 @@ gem 'pgvector'
|
|||||||
# Convert Website HTML to Markdown
|
# Convert Website HTML to Markdown
|
||||||
gem 'reverse_markdown'
|
gem 'reverse_markdown'
|
||||||
|
|
||||||
# Sentiment analysis
|
|
||||||
gem 'informers'
|
|
||||||
|
|
||||||
### Gems required only in specific deployment environments ###
|
### Gems required only in specific deployment environments ###
|
||||||
##############################################################
|
##############################################################
|
||||||
|
|
||||||
|
|||||||
+14
-26
@@ -151,7 +151,6 @@ GEM
|
|||||||
base64 (0.1.1)
|
base64 (0.1.1)
|
||||||
bcrypt (3.1.20)
|
bcrypt (3.1.20)
|
||||||
bindex (0.8.1)
|
bindex (0.8.1)
|
||||||
blingfire (0.1.8)
|
|
||||||
bootsnap (1.16.0)
|
bootsnap (1.16.0)
|
||||||
msgpack (~> 1.2)
|
msgpack (~> 1.2)
|
||||||
brakeman (5.4.1)
|
brakeman (5.4.1)
|
||||||
@@ -316,15 +315,15 @@ GEM
|
|||||||
google-cloud-translate-v3 (0.6.0)
|
google-cloud-translate-v3 (0.6.0)
|
||||||
gapic-common (>= 0.17.1, < 2.a)
|
gapic-common (>= 0.17.1, < 2.a)
|
||||||
google-cloud-errors (~> 1.0)
|
google-cloud-errors (~> 1.0)
|
||||||
google-protobuf (3.25.2)
|
google-protobuf (3.25.3)
|
||||||
google-protobuf (3.25.2-arm64-darwin)
|
google-protobuf (3.25.3-arm64-darwin)
|
||||||
google-protobuf (3.25.2-x86_64-darwin)
|
google-protobuf (3.25.3-x86_64-darwin)
|
||||||
google-protobuf (3.25.2-x86_64-linux)
|
google-protobuf (3.25.3-x86_64-linux)
|
||||||
googleapis-common-protos (1.4.0)
|
googleapis-common-protos (1.4.0)
|
||||||
google-protobuf (~> 3.14)
|
google-protobuf (~> 3.14)
|
||||||
googleapis-common-protos-types (~> 1.2)
|
googleapis-common-protos-types (~> 1.2)
|
||||||
grpc (~> 1.27)
|
grpc (~> 1.27)
|
||||||
googleapis-common-protos-types (1.11.0)
|
googleapis-common-protos-types (1.14.0)
|
||||||
google-protobuf (~> 3.18)
|
google-protobuf (~> 3.18)
|
||||||
googleauth (1.5.2)
|
googleauth (1.5.2)
|
||||||
faraday (>= 0.17.3, < 3.a)
|
faraday (>= 0.17.3, < 3.a)
|
||||||
@@ -335,14 +334,17 @@ GEM
|
|||||||
signet (>= 0.16, < 2.a)
|
signet (>= 0.16, < 2.a)
|
||||||
groupdate (6.2.1)
|
groupdate (6.2.1)
|
||||||
activesupport (>= 5.2)
|
activesupport (>= 5.2)
|
||||||
grpc (1.54.3)
|
grpc (1.62.0)
|
||||||
google-protobuf (~> 3.21)
|
google-protobuf (~> 3.25)
|
||||||
googleapis-common-protos-types (~> 1.0)
|
googleapis-common-protos-types (~> 1.0)
|
||||||
grpc (1.54.3-x86_64-darwin)
|
grpc (1.62.0-arm64-darwin)
|
||||||
google-protobuf (~> 3.21)
|
google-protobuf (~> 3.25)
|
||||||
googleapis-common-protos-types (~> 1.0)
|
googleapis-common-protos-types (~> 1.0)
|
||||||
grpc (1.54.3-x86_64-linux)
|
grpc (1.62.0-x86_64-darwin)
|
||||||
google-protobuf (~> 3.21)
|
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)
|
googleapis-common-protos-types (~> 1.0)
|
||||||
haikunator (1.1.1)
|
haikunator (1.1.1)
|
||||||
hairtrigger (1.0.0)
|
hairtrigger (1.0.0)
|
||||||
@@ -371,10 +373,6 @@ GEM
|
|||||||
image_processing (1.12.2)
|
image_processing (1.12.2)
|
||||||
mini_magick (>= 4.9.5, < 5)
|
mini_magick (>= 4.9.5, < 5)
|
||||||
ruby-vips (>= 2.0.17, < 3)
|
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)
|
io-console (0.6.0)
|
||||||
irb (1.7.2)
|
irb (1.7.2)
|
||||||
reline (>= 0.3.6)
|
reline (>= 0.3.6)
|
||||||
@@ -500,7 +498,6 @@ GEM
|
|||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.16.4-x86_64-linux)
|
nokogiri (1.16.4-x86_64-linux)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
numo-narray (0.9.2.1)
|
|
||||||
oauth (1.1.0)
|
oauth (1.1.0)
|
||||||
oauth-tty (~> 1.0, >= 1.0.1)
|
oauth-tty (~> 1.0, >= 1.0.1)
|
||||||
snaky_hash (~> 2.0)
|
snaky_hash (~> 2.0)
|
||||||
@@ -529,14 +526,6 @@ GEM
|
|||||||
omniauth-rails_csrf_protection (1.0.1)
|
omniauth-rails_csrf_protection (1.0.1)
|
||||||
actionpack (>= 4.2)
|
actionpack (>= 4.2)
|
||||||
omniauth (~> 2.0)
|
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)
|
openssl (3.1.0)
|
||||||
orm_adapter (0.5.0)
|
orm_adapter (0.5.0)
|
||||||
os (1.1.4)
|
os (1.1.4)
|
||||||
@@ -892,7 +881,6 @@ DEPENDENCIES
|
|||||||
hashie
|
hashie
|
||||||
html2text!
|
html2text!
|
||||||
image_processing
|
image_processing
|
||||||
informers
|
|
||||||
jbuilder
|
jbuilder
|
||||||
json_refs
|
json_refs
|
||||||
json_schemer
|
json_schemer
|
||||||
|
|||||||
@@ -6,6 +6,12 @@ class Public::Api::V1::PortalsController < Public::Api::V1::Portals::BaseControl
|
|||||||
|
|
||||||
def show; end
|
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
|
private
|
||||||
|
|
||||||
def portal
|
def portal
|
||||||
|
|||||||
@@ -42,19 +42,20 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
|
// scss-lint:disable PropertySortOrder
|
||||||
:root {
|
:root {
|
||||||
--color-amber-100: 255 243 208;
|
|
||||||
--color-amber-200: 255 236 183;
|
|
||||||
--color-amber-25: 254 253 251;
|
--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-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-600: 214 163 92;
|
||||||
--color-amber-700: 214 163 92;
|
--color-amber-700: 255 186 26;
|
||||||
--color-amber-800: 255 186 26;
|
--color-amber-800: 145 89 48;
|
||||||
--color-amber-900: 145 89 48;
|
--color-amber-900: 79 52 34;
|
||||||
--color-amber-950: 79 52 34;
|
|
||||||
|
|
||||||
--color-ash-100: 235 235 239;
|
--color-ash-100: 235 235 239;
|
||||||
--color-ash-200: 228 228 233;
|
--color-ash-200: 228 228 233;
|
||||||
@@ -69,18 +70,18 @@
|
|||||||
--color-ash-800: 96 100 108;
|
--color-ash-800: 96 100 108;
|
||||||
--color-ash-900: 28 32 36;
|
--color-ash-900: 28 32 36;
|
||||||
|
|
||||||
--color-primary-100: 222 234 255;
|
--color-primary-25: 251 253 255;
|
||||||
--color-primary-200: 206 223 255;
|
--color-primary-50: 245 249 255;
|
||||||
--color-primary-25: 252 253 254;
|
--color-primary-75: 233 243 255;
|
||||||
--color-primary-300: 186 209 255;
|
--color-primary-100: 218 236 255;
|
||||||
--color-primary-400: 163 190 255;
|
--color-primary-200: 201 226 255;
|
||||||
--color-primary-50: 246 249 255;
|
--color-primary-300: 181 213 255;
|
||||||
--color-primary-500: 131 164 250;
|
--color-primary-400: 155 195 252;
|
||||||
--color-primary-600: 69 110 255;
|
--color-primary-500: 117 171 247;
|
||||||
--color-primary-700: 57 95 241;
|
--color-primary-600: 39 129 246;
|
||||||
--color-primary-75: 236 242 255;
|
--color-primary-700: 16 115 233;
|
||||||
--color-primary-800: 53 88 217;
|
--color-primary-800: 8 109 224;
|
||||||
--color-primary-900: 27 44 99;
|
--color-primary-900: 11 50 101;
|
||||||
|
|
||||||
--color-ruby-100: 255 220 225;
|
--color-ruby-100: 255 220 225;
|
||||||
--color-ruby-200: 255 206 214;
|
--color-ruby-200: 255 206 214;
|
||||||
@@ -107,21 +108,125 @@
|
|||||||
--color-teal-75: 236 249 255;
|
--color-teal-75: 236 249 255;
|
||||||
--color-teal-800: 0 133 115;
|
--color-teal-800: 0 133 115;
|
||||||
--color-teal-900: 13 61 56;
|
--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
|
// scss-lint:disable QualifyingElement
|
||||||
body.dark {
|
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-100: 57 39 15;
|
||||||
--color-amber-200: 67 46 18;
|
--color-amber-200: 67 46 18;
|
||||||
--color-amber-25: 31 19 0;
|
|
||||||
--color-amber-300: 83 57 22;
|
--color-amber-300: 83 57 22;
|
||||||
--color-amber-400: 111 77 29;
|
--color-amber-400: 111 77 29;
|
||||||
--color-amber-50: 37 24 4;
|
|
||||||
--color-amber-500: 169 118 42;
|
--color-amber-500: 169 118 42;
|
||||||
--color-amber-600: 169 118 42;
|
--color-amber-600: 169 118 42;
|
||||||
--color-amber-700: 255 203 71;
|
--color-amber-700: 255 203 71;
|
||||||
--color-amber-800: 255 204 77;
|
--color-amber-800: 255 204 77;
|
||||||
--color-amber-900: 255 231 179;
|
--color-amber-900: 255 231 179;
|
||||||
--color-amber-950: 255 231 179;
|
|
||||||
|
|
||||||
--color-ash-100: 46 48 53;
|
--color-ash-100: 46 48 53;
|
||||||
--color-ash-200: 53 55 60;
|
--color-ash-200: 53 55 60;
|
||||||
@@ -136,18 +241,18 @@
|
|||||||
--color-ash-800: 173 177 184;
|
--color-ash-800: 173 177 184;
|
||||||
--color-ash-900: 237 238 240;
|
--color-ash-900: 237 238 240;
|
||||||
|
|
||||||
--color-primary-100: 24 43 109;
|
--color-primary-25: 10 17 28;
|
||||||
--color-primary-200: 31 53 129;
|
--color-primary-50: 15 24 38;
|
||||||
--color-primary-25: 11 16 31;
|
--color-primary-75: 15 39 72;
|
||||||
--color-primary-300: 40 64 146;
|
--color-primary-100: 10 49 99;
|
||||||
--color-primary-400: 48 76 168;
|
--color-primary-200: 18 61 117;
|
||||||
--color-primary-50: 16 23 41;
|
--color-primary-300: 29 74 134;
|
||||||
--color-primary-500: 57 89 196;
|
--color-primary-400: 40 89 156;
|
||||||
--color-primary-600: 69 110 255;
|
--color-primary-500: 48 106 186;
|
||||||
--color-primary-700: 58 96 240;
|
--color-primary-600: 39 129 246;
|
||||||
--color-primary-75: 20 34 81;
|
--color-primary-700: 21 116 231;
|
||||||
--color-primary-800: 140 177 255;
|
--color-primary-800: 126 182 255;
|
||||||
--color-primary-900: 210 225 255;
|
--color-primary-900: 205 227 255;
|
||||||
|
|
||||||
--color-ruby-100: 78 19 37;
|
--color-ruby-100: 78 19 37;
|
||||||
--color-ruby-200: 94 26 46;
|
--color-ruby-200: 94 26 46;
|
||||||
@@ -174,5 +279,108 @@
|
|||||||
--color-teal-75: 13 45 42;
|
--color-teal-75: 13 45 42;
|
||||||
--color-teal-800: 11 216 182;
|
--color-teal-800: 11 216 182;
|
||||||
--color-teal-900: 173 240 221;
|
--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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ import ConversationBasicFilter from './widgets/conversation/ConversationBasicFil
|
|||||||
import ChatTypeTabs from './widgets/ChatTypeTabs.vue';
|
import ChatTypeTabs from './widgets/ChatTypeTabs.vue';
|
||||||
import ConversationItem from './ConversationItem.vue';
|
import ConversationItem from './ConversationItem.vue';
|
||||||
import timeMixin from '../mixins/time';
|
import timeMixin from '../mixins/time';
|
||||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||||
import conversationMixin from '../mixins/conversations';
|
import conversationMixin from '../mixins/conversations';
|
||||||
import wootConstants from 'dashboard/constants/globals';
|
import wootConstants from 'dashboard/constants/globals';
|
||||||
import advancedFilterTypes from './widgets/conversation/advancedFilterItems';
|
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 languages from 'dashboard/components/widgets/conversation/advancedFilterItems/languages';
|
||||||
import countries from 'shared/constants/countries';
|
import countries from 'shared/constants/countries';
|
||||||
import { generateValuesForEditCustomViews } from 'dashboard/helper/customViewsHelper';
|
import { generateValuesForEditCustomViews } from 'dashboard/helper/customViewsHelper';
|
||||||
|
|
||||||
import {
|
|
||||||
hasPressedAltAndJKey,
|
|
||||||
hasPressedAltAndKKey,
|
|
||||||
} from 'shared/helpers/KeyboardHelpers';
|
|
||||||
import { conversationListPageURL } from '../helper/URLHelper';
|
import { conversationListPageURL } from '../helper/URLHelper';
|
||||||
import {
|
import {
|
||||||
isOnMentionsView,
|
isOnMentionsView,
|
||||||
@@ -228,7 +223,7 @@ export default {
|
|||||||
mixins: [
|
mixins: [
|
||||||
timeMixin,
|
timeMixin,
|
||||||
conversationMixin,
|
conversationMixin,
|
||||||
eventListenerMixins,
|
keyboardEventListenerMixins,
|
||||||
alertMixin,
|
alertMixin,
|
||||||
filterMixin,
|
filterMixin,
|
||||||
uiSettingsMixin,
|
uiSettingsMixin,
|
||||||
@@ -691,30 +686,40 @@ export default {
|
|||||||
lastConversationIndex,
|
lastConversationIndex,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
handleKeyEvents(e) {
|
handlePreviousConversation() {
|
||||||
if (hasPressedAltAndJKey(e)) {
|
const { allConversations, activeConversationIndex } =
|
||||||
const { allConversations, activeConversationIndex } =
|
this.getKeyboardListenerParams();
|
||||||
this.getKeyboardListenerParams();
|
if (activeConversationIndex === -1) {
|
||||||
if (activeConversationIndex === -1) {
|
allConversations[0].click();
|
||||||
allConversations[0].click();
|
|
||||||
}
|
|
||||||
if (activeConversationIndex >= 1) {
|
|
||||||
allConversations[activeConversationIndex - 1].click();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (hasPressedAltAndKKey(e)) {
|
if (activeConversationIndex >= 1) {
|
||||||
const {
|
allConversations[activeConversationIndex - 1].click();
|
||||||
allConversations,
|
|
||||||
activeConversationIndex,
|
|
||||||
lastConversationIndex,
|
|
||||||
} = this.getKeyboardListenerParams();
|
|
||||||
if (activeConversationIndex === -1) {
|
|
||||||
allConversations[lastConversationIndex].click();
|
|
||||||
} else if (activeConversationIndex < lastConversationIndex) {
|
|
||||||
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() {
|
resetAndFetchData() {
|
||||||
this.appliedFilter = [];
|
this.appliedFilter = [];
|
||||||
this.resetBulkActions();
|
this.resetBulkActions();
|
||||||
|
|||||||
@@ -91,12 +91,7 @@ import { mapGetters } from 'vuex';
|
|||||||
import { mixin as clickaway } from 'vue-clickaway';
|
import { mixin as clickaway } from 'vue-clickaway';
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
import CustomSnoozeModal from 'dashboard/components/CustomSnoozeModal.vue';
|
import CustomSnoozeModal from 'dashboard/components/CustomSnoozeModal.vue';
|
||||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||||
import {
|
|
||||||
hasPressedAltAndEKey,
|
|
||||||
hasPressedCommandPlusAltAndEKey,
|
|
||||||
hasPressedAltAndMKey,
|
|
||||||
} from 'shared/helpers/KeyboardHelpers';
|
|
||||||
import { findSnoozeTime } from 'dashboard/helper/snoozeHelpers';
|
import { findSnoozeTime } from 'dashboard/helper/snoozeHelpers';
|
||||||
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
|
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
|
||||||
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
|
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
|
||||||
@@ -114,7 +109,7 @@ export default {
|
|||||||
WootDropdownMenu,
|
WootDropdownMenu,
|
||||||
CustomSnoozeModal,
|
CustomSnoozeModal,
|
||||||
},
|
},
|
||||||
mixins: [clickaway, alertMixin, eventListenerMixins],
|
mixins: [clickaway, alertMixin, keyboardEventListenerMixins],
|
||||||
props: { conversationId: { type: [String, Number], required: true } },
|
props: { conversationId: { type: [String, Number], required: true } },
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -159,37 +154,52 @@ export default {
|
|||||||
bus.$off(CMD_RESOLVE_CONVERSATION, this.onCmdResolveConversation);
|
bus.$off(CMD_RESOLVE_CONVERSATION, this.onCmdResolveConversation);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async handleKeyEvents(e) {
|
getKeyboardEvents() {
|
||||||
|
return {
|
||||||
|
'Alt+KeyM': {
|
||||||
|
action: () => this.$refs.arrowDownButton?.$el.click(),
|
||||||
|
allowOnFocusedInput: true,
|
||||||
|
},
|
||||||
|
'Alt+KeyE': this.resolveOrToast,
|
||||||
|
'$mod+Alt+KeyE': async event => {
|
||||||
|
const { all, activeIndex, lastIndex } = this.getConversationParams();
|
||||||
|
await this.resolveOrToast();
|
||||||
|
|
||||||
|
if (activeIndex < lastIndex) {
|
||||||
|
all[activeIndex + 1].click();
|
||||||
|
} else if (all.length > 1) {
|
||||||
|
all[0].click();
|
||||||
|
document.querySelector('.conversations-list').scrollTop = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getConversationParams() {
|
||||||
const allConversations = document.querySelectorAll(
|
const allConversations = document.querySelectorAll(
|
||||||
'.conversations-list .conversation'
|
'.conversations-list .conversation'
|
||||||
);
|
);
|
||||||
if (hasPressedAltAndMKey(e)) {
|
|
||||||
if (this.$refs.arrowDownButton) {
|
const activeConversation = document.querySelector(
|
||||||
this.$refs.arrowDownButton.$el.click();
|
'div.conversations-list div.conversation.active'
|
||||||
}
|
);
|
||||||
}
|
const activeConversationIndex = [...allConversations].indexOf(
|
||||||
if (hasPressedAltAndEKey(e)) {
|
activeConversation
|
||||||
const activeConversation = document.querySelector(
|
);
|
||||||
'div.conversations-list div.conversation.active'
|
const lastConversationIndex = allConversations.length - 1;
|
||||||
);
|
|
||||||
const activeConversationIndex = [...allConversations].indexOf(
|
return {
|
||||||
activeConversation
|
all: allConversations,
|
||||||
);
|
activeIndex: activeConversationIndex,
|
||||||
const lastConversationIndex = allConversations.length - 1;
|
lastIndex: lastConversationIndex,
|
||||||
try {
|
};
|
||||||
await this.toggleStatus(wootConstants.STATUS_TYPE.RESOLVED);
|
},
|
||||||
} catch (error) {
|
async resolveOrToast() {
|
||||||
// error
|
try {
|
||||||
}
|
await this.toggleStatus(wootConstants.STATUS_TYPE.RESOLVED);
|
||||||
if (hasPressedCommandPlusAltAndEKey(e)) {
|
} catch (error) {
|
||||||
if (activeConversationIndex < lastConversationIndex) {
|
// error
|
||||||
allConversations[activeConversationIndex + 1].click();
|
|
||||||
} else if (allConversations.length > 1) {
|
|
||||||
allConversations[0].click();
|
|
||||||
document.querySelector('.conversations-list').scrollTop = 0;
|
|
||||||
}
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onCmdSnoozeConversation(snoozeType) {
|
onCmdSnoozeConversation(snoozeType) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<aside class="h-full flex">
|
<aside class="flex h-full">
|
||||||
<primary-sidebar
|
<primary-sidebar
|
||||||
:logo-source="globalConfig.logoThumbnail"
|
:logo-source="globalConfig.logoThumbnail"
|
||||||
:installation-name="globalConfig.installationName"
|
:installation-name="globalConfig.installationName"
|
||||||
@@ -36,15 +36,7 @@ import alertMixin from 'shared/mixins/alertMixin';
|
|||||||
|
|
||||||
import PrimarySidebar from './sidebarComponents/Primary.vue';
|
import PrimarySidebar from './sidebarComponents/Primary.vue';
|
||||||
import SecondarySidebar from './sidebarComponents/Secondary.vue';
|
import SecondarySidebar from './sidebarComponents/Secondary.vue';
|
||||||
import {
|
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||||
hasPressedAltAndCKey,
|
|
||||||
hasPressedAltAndRKey,
|
|
||||||
hasPressedAltAndSKey,
|
|
||||||
hasPressedAltAndVKey,
|
|
||||||
hasPressedCommandAndForwardSlash,
|
|
||||||
isEscape,
|
|
||||||
} from 'shared/helpers/KeyboardHelpers';
|
|
||||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
|
||||||
import router from '../../routes';
|
import router from '../../routes';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -52,7 +44,7 @@ export default {
|
|||||||
PrimarySidebar,
|
PrimarySidebar,
|
||||||
SecondarySidebar,
|
SecondarySidebar,
|
||||||
},
|
},
|
||||||
mixins: [adminMixin, alertMixin, eventListenerMixins],
|
mixins: [adminMixin, alertMixin, keyboardEventListenerMixins],
|
||||||
props: {
|
props: {
|
||||||
showSecondarySidebar: {
|
showSecondarySidebar: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
@@ -173,30 +165,27 @@ export default {
|
|||||||
closeKeyShortcutModal() {
|
closeKeyShortcutModal() {
|
||||||
this.$emit('close-key-shortcut-modal');
|
this.$emit('close-key-shortcut-modal');
|
||||||
},
|
},
|
||||||
handleKeyEvents(e) {
|
getKeyboardEvents() {
|
||||||
if (hasPressedCommandAndForwardSlash(e)) {
|
return {
|
||||||
this.toggleKeyShortcutModal();
|
'$mod+Slash': this.toggleKeyShortcutModal,
|
||||||
}
|
'$mod+Escape': this.closeKeyShortcutModal,
|
||||||
if (isEscape(e)) {
|
'Alt+KeyC': {
|
||||||
this.closeKeyShortcutModal();
|
action: () => this.navigateToRoute('home'),
|
||||||
}
|
},
|
||||||
|
'Alt+KeyV': {
|
||||||
if (hasPressedAltAndCKey(e)) {
|
action: () => this.navigateToRoute('contacts_dashboard'),
|
||||||
if (!this.isCurrentRouteSameAsNavigation('home')) {
|
},
|
||||||
router.push({ name: 'home' });
|
'Alt+KeyR': {
|
||||||
}
|
action: () => this.navigateToRoute('account_overview_reports'),
|
||||||
} else if (hasPressedAltAndVKey(e)) {
|
},
|
||||||
if (!this.isCurrentRouteSameAsNavigation('contacts_dashboard')) {
|
'Alt+KeyS': {
|
||||||
router.push({ name: 'contacts_dashboard' });
|
action: () => this.navigateToRoute('agent_list'),
|
||||||
}
|
},
|
||||||
} else if (hasPressedAltAndRKey(e)) {
|
};
|
||||||
if (!this.isCurrentRouteSameAsNavigation('settings_account_reports')) {
|
},
|
||||||
router.push({ name: 'settings_account_reports' });
|
navigateToRoute(routeName) {
|
||||||
}
|
if (!this.isCurrentRouteSameAsNavigation(routeName)) {
|
||||||
} else if (hasPressedAltAndSKey(e)) {
|
router.push({ name: routeName });
|
||||||
if (!this.isCurrentRouteSameAsNavigation('agent_list')) {
|
|
||||||
router.push({ name: 'agent_list' });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
isCurrentRouteSameAsNavigation(routeName) {
|
isCurrentRouteSameAsNavigation(routeName) {
|
||||||
|
|||||||
@@ -40,8 +40,7 @@ import AIAssistanceModal from './AIAssistanceModal.vue';
|
|||||||
import adminMixin from 'dashboard/mixins/aiMixin';
|
import adminMixin from 'dashboard/mixins/aiMixin';
|
||||||
import aiMixin from 'dashboard/mixins/isAdmin';
|
import aiMixin from 'dashboard/mixins/isAdmin';
|
||||||
import { CMD_AI_ASSIST } from 'dashboard/routes/dashboard/commands/commandBarBusEvents';
|
import { CMD_AI_ASSIST } from 'dashboard/routes/dashboard/commands/commandBarBusEvents';
|
||||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||||
import { buildHotKeys } from 'shared/helpers/KeyboardHelpers';
|
|
||||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||||
import AIAssistanceCTAButton from './AIAssistanceCTAButton.vue';
|
import AIAssistanceCTAButton from './AIAssistanceCTAButton.vue';
|
||||||
|
|
||||||
@@ -51,7 +50,7 @@ export default {
|
|||||||
AICTAModal,
|
AICTAModal,
|
||||||
AIAssistanceCTAButton,
|
AIAssistanceCTAButton,
|
||||||
},
|
},
|
||||||
mixins: [aiMixin, eventListenerMixins, adminMixin, uiSettingsMixin],
|
mixins: [aiMixin, keyboardEventListenerMixins, adminMixin, uiSettingsMixin],
|
||||||
data: () => ({
|
data: () => ({
|
||||||
showAIAssistanceModal: false,
|
showAIAssistanceModal: false,
|
||||||
showAICtaModal: false,
|
showAICtaModal: false,
|
||||||
@@ -87,14 +86,17 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
onKeyDownHandler(event) {
|
getKeyboardEvents() {
|
||||||
const keyPattern = buildHotKeys(event);
|
return {
|
||||||
const shouldRevertTheContent =
|
'$mod+KeyZ': {
|
||||||
['meta+z', 'ctrl+z'].includes(keyPattern) && !!this.initialMessage;
|
action: () => {
|
||||||
if (shouldRevertTheContent) {
|
if (this.initialMessage) {
|
||||||
this.$emit('replace-text', this.initialMessage);
|
this.$emit('replace-text', this.initialMessage);
|
||||||
this.initialMessage = '';
|
this.initialMessage = '';
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
},
|
},
|
||||||
hideAIAssistanceModal() {
|
hideAIAssistanceModal() {
|
||||||
this.recordAnalytics('DISMISS_AI_SUGGESTION', {
|
this.recordAnalytics('DISMISS_AI_SUGGESTION', {
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="preview-item__wrap flex overflow-auto max-h-[12.5rem]">
|
<div class="flex overflow-auto max-h-[12.5rem]">
|
||||||
<div
|
<div
|
||||||
v-for="(attachment, index) in attachments"
|
v-for="(attachment, index) in nonRecordedAudioAttachments"
|
||||||
:key="attachment.id"
|
:key="attachment.id"
|
||||||
class="preview-item flex items-center p-1 bg-slate-50 dark:bg-slate-800 gap-1 rounded-md w-[15rem] mb-1"
|
class="preview-item flex items-center p-1 bg-slate-50 dark:bg-slate-800 gap-1 rounded-md w-[15rem] mb-1"
|
||||||
>
|
>
|
||||||
<div class="max-w-[4rem] flex-shrink-0 w-6 flex items-center">
|
<div class="max-w-[4rem] flex-shrink-0 w-6 flex items-center">
|
||||||
<img
|
<img
|
||||||
v-if="isTypeImage(attachment.resource)"
|
v-if="isTypeImage(attachment.resource)"
|
||||||
class="image-thumb"
|
class="object-cover w-6 h-6 rounded-sm"
|
||||||
:src="attachment.thumb"
|
:src="attachment.thumb"
|
||||||
/>
|
/>
|
||||||
<span v-else class="w-6 h-6 text-lg relative -top-px text-left">
|
<span v-else class="relative w-6 h-6 text-lg text-left -top-px">
|
||||||
📄
|
📄
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -23,73 +23,62 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-[30%] justify-center">
|
<div class="w-[30%] justify-center">
|
||||||
<span
|
<span class="overflow-hidden text-xs text-ellipsis whitespace-nowrap">
|
||||||
class="item overflow-hidden text-xs text-ellipsis whitespace-nowrap"
|
|
||||||
>
|
|
||||||
{{ formatFileSize(attachment.resource) }}
|
{{ formatFileSize(attachment.resource) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-center">
|
<div class="flex items-center justify-center">
|
||||||
<woot-button
|
<woot-button
|
||||||
v-if="!isTypeAudio(attachment.resource)"
|
class="!w-6 !h-6 text-sm rounded-md hover:bg-slate-50 dark:hover:bg-slate-800 clear secondary"
|
||||||
class="remove--attachment clear secondary"
|
|
||||||
icon="dismiss"
|
icon="dismiss"
|
||||||
@click="() => onRemoveAttachment(index)"
|
@click="onRemoveAttachment(index)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
|
||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
import { formatBytes } from 'shared/helpers/FileHelper';
|
import { formatBytes } from 'shared/helpers/FileHelper';
|
||||||
export default {
|
|
||||||
props: {
|
const props = defineProps({
|
||||||
attachments: {
|
attachments: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => [],
|
default: () => [],
|
||||||
},
|
|
||||||
removeAttachment: {
|
|
||||||
type: Function,
|
|
||||||
default: () => {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
onRemoveAttachment(index) {
|
|
||||||
this.removeAttachment(index);
|
|
||||||
},
|
|
||||||
formatFileSize(file) {
|
|
||||||
const size = file.byte_size || file.size;
|
|
||||||
return formatBytes(size, 0);
|
|
||||||
},
|
|
||||||
isTypeImage(file) {
|
|
||||||
const type = file.content_type || file.type;
|
|
||||||
return type.includes('image');
|
|
||||||
},
|
|
||||||
isTypeAudio(file) {
|
|
||||||
const type = file.content_type || file.type;
|
|
||||||
return type.includes('audio');
|
|
||||||
},
|
|
||||||
fileName(file) {
|
|
||||||
return file.filename || file.name;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emits = defineEmits(['remove-attachment']);
|
||||||
|
|
||||||
|
const nonRecordedAudioAttachments = computed(() => {
|
||||||
|
return props.attachments.filter(attachment => !attachment?.isRecordedAudio);
|
||||||
|
});
|
||||||
|
|
||||||
|
const recordedAudioAttachments = computed(() =>
|
||||||
|
props.attachments.filter(attachment => attachment.isRecordedAudio)
|
||||||
|
);
|
||||||
|
|
||||||
|
const onRemoveAttachment = itemIndex => {
|
||||||
|
emits(
|
||||||
|
'remove-attachment',
|
||||||
|
nonRecordedAudioAttachments.value
|
||||||
|
.filter((_, index) => index !== itemIndex)
|
||||||
|
.concat(recordedAudioAttachments.value)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatFileSize = file => {
|
||||||
|
const size = file.byte_size || file.size;
|
||||||
|
return formatBytes(size, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isTypeImage = file => {
|
||||||
|
const type = file.content_type || file.type;
|
||||||
|
return type.includes('image');
|
||||||
|
};
|
||||||
|
|
||||||
|
const fileName = file => {
|
||||||
|
return file.filename || file.name;
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
|
||||||
.image-thumb {
|
|
||||||
@apply w-6 h-6 object-cover rounded-sm;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-name-wrap,
|
|
||||||
.file-size-wrap {
|
|
||||||
@apply flex items-center py-0 px-1;
|
|
||||||
|
|
||||||
> .item {
|
|
||||||
@apply m-0 overflow-hidden text-xs font-medium;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.remove--attachment {
|
|
||||||
@apply w-6 h-6 rounded-md text-sm cursor-pointer hover:bg-slate-50 dark:hover:bg-slate-800;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -10,11 +10,10 @@
|
|||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import wootConstants from 'dashboard/constants/globals';
|
import wootConstants from 'dashboard/constants/globals';
|
||||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||||
import { hasPressedAltAndNKey } from 'shared/helpers/KeyboardHelpers';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [eventListenerMixins],
|
mixins: [keyboardEventListenerMixins],
|
||||||
props: {
|
props: {
|
||||||
items: {
|
items: {
|
||||||
type: Array,
|
type: Array,
|
||||||
@@ -31,14 +30,18 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
handleKeyEvents(e) {
|
getKeyboardEvents() {
|
||||||
if (hasPressedAltAndNKey(e)) {
|
return {
|
||||||
if (this.activeTab === wootConstants.ASSIGNEE_TYPE.ALL) {
|
'Alt+KeyN': {
|
||||||
this.onTabChange(0);
|
action: () => {
|
||||||
} else {
|
if (this.activeTab === wootConstants.ASSIGNEE_TYPE.ALL) {
|
||||||
this.onTabChange(this.activeTabIndex + 1);
|
this.onTabChange(0);
|
||||||
}
|
} else {
|
||||||
}
|
this.onTabChange(this.activeTabIndex + 1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
},
|
},
|
||||||
onTabChange(selectedTabIndex) {
|
onTabChange(selectedTabIndex) {
|
||||||
if (this.items[selectedTabIndex].key !== this.activeTab) {
|
if (this.items[selectedTabIndex].key !== this.activeTab) {
|
||||||
|
|||||||
@@ -31,15 +31,10 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import AddLabel from 'shared/components/ui/dropdown/AddLabel.vue';
|
import AddLabel from 'shared/components/ui/dropdown/AddLabel.vue';
|
||||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||||
import LabelDropdown from 'shared/components/ui/label/LabelDropdown.vue';
|
import LabelDropdown from 'shared/components/ui/label/LabelDropdown.vue';
|
||||||
import { mixin as clickaway } from 'vue-clickaway';
|
import { mixin as clickaway } from 'vue-clickaway';
|
||||||
import adminMixin from 'dashboard/mixins/isAdmin';
|
import adminMixin from 'dashboard/mixins/isAdmin';
|
||||||
import {
|
|
||||||
buildHotKeys,
|
|
||||||
isEscape,
|
|
||||||
isActiveElementTypeable,
|
|
||||||
} from 'shared/helpers/KeyboardHelpers';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@@ -47,7 +42,7 @@ export default {
|
|||||||
LabelDropdown,
|
LabelDropdown,
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [clickaway, adminMixin, eventListenerMixins],
|
mixins: [clickaway, adminMixin, keyboardEventListenerMixins],
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
allLabels: {
|
allLabels: {
|
||||||
@@ -88,17 +83,19 @@ export default {
|
|||||||
closeDropdownLabel() {
|
closeDropdownLabel() {
|
||||||
this.showSearchDropdownLabel = false;
|
this.showSearchDropdownLabel = false;
|
||||||
},
|
},
|
||||||
|
getKeyboardEvents() {
|
||||||
handleKeyEvents(e) {
|
return {
|
||||||
const keyPattern = buildHotKeys(e);
|
KeyL: {
|
||||||
|
action: e => {
|
||||||
if (keyPattern === 'l' && !isActiveElementTypeable(e)) {
|
this.toggleLabels();
|
||||||
this.toggleLabels();
|
e.preventDefault();
|
||||||
e.preventDefault();
|
},
|
||||||
} else if (isEscape(e) && this.showSearchDropdownLabel) {
|
},
|
||||||
this.closeDropdownLabel();
|
Escape: {
|
||||||
e.preventDefault();
|
action: () => this.closeDropdownLabel(),
|
||||||
}
|
allowOnFocusedInput: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -231,7 +231,18 @@ export default {
|
|||||||
@apply bg-transparent max-h-60 min-h-[3rem] pt-4 px-0 pb-0 resize-none;
|
@apply bg-transparent max-h-60 min-h-[3rem] pt-4 px-0 pb-0 resize-none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.video-js .vjs-control-bar {
|
// Added to override the default text and bg style to support dark and light mode.
|
||||||
background-color: transparent;
|
.video-js .vjs-control-bar,
|
||||||
|
.vjs-record.video-js .vjs-control.vjs-record-indicator:before {
|
||||||
|
@apply text-slate-600 dark:text-slate-200 bg-transparent dark:bg-transparent;
|
||||||
|
}
|
||||||
|
// Added to fix div overlays the screen and takes over the button clicks
|
||||||
|
// https://github.com/collab-project/videojs-record/issues/688
|
||||||
|
// https://github.com/collab-project/videojs-record/pull/709
|
||||||
|
.vjs-record.video-js .vjs-control.vjs-record-indicator.vjs-hidden,
|
||||||
|
.vjs-record.video-js .vjs-control.vjs-record-indicator,
|
||||||
|
.vjs-record.video-js .vjs-control.vjs-record-indicator:before,
|
||||||
|
.vjs-record.video-js .vjs-control.vjs-record-indicator:after {
|
||||||
|
@apply pointer-events-none;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -78,10 +78,8 @@ const MAXIMUM_FILE_UPLOAD_SIZE = 4; // in MB
|
|||||||
import {
|
import {
|
||||||
hasPressedEnterAndNotCmdOrShift,
|
hasPressedEnterAndNotCmdOrShift,
|
||||||
hasPressedCommandAndEnter,
|
hasPressedCommandAndEnter,
|
||||||
hasPressedAltAndPKey,
|
|
||||||
hasPressedAltAndLKey,
|
|
||||||
} from 'shared/helpers/KeyboardHelpers';
|
} from 'shared/helpers/KeyboardHelpers';
|
||||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||||
import { isEditorHotKeyEnabled } from 'dashboard/mixins/uiSettings';
|
import { isEditorHotKeyEnabled } from 'dashboard/mixins/uiSettings';
|
||||||
import {
|
import {
|
||||||
@@ -121,7 +119,7 @@ const createState = (
|
|||||||
export default {
|
export default {
|
||||||
name: 'WootMessageEditor',
|
name: 'WootMessageEditor',
|
||||||
components: { TagAgents, CannedResponse, VariableList },
|
components: { TagAgents, CannedResponse, VariableList },
|
||||||
mixins: [eventListenerMixins, uiSettingsMixin, alertMixin],
|
mixins: [keyboardEventListenerMixins, uiSettingsMixin, alertMixin],
|
||||||
props: {
|
props: {
|
||||||
value: { type: String, default: '' },
|
value: { type: String, default: '' },
|
||||||
editorId: { type: String, default: '' },
|
editorId: { type: String, default: '' },
|
||||||
@@ -522,13 +520,21 @@ export default {
|
|||||||
isCmdPlusEnterToSendEnabled() {
|
isCmdPlusEnterToSendEnabled() {
|
||||||
return isEditorHotKeyEnabled(this.uiSettings, 'cmd_enter');
|
return isEditorHotKeyEnabled(this.uiSettings, 'cmd_enter');
|
||||||
},
|
},
|
||||||
handleKeyEvents(e) {
|
getKeyboardEvents() {
|
||||||
if (hasPressedAltAndPKey(e)) {
|
return {
|
||||||
this.focusEditorInputField();
|
'Alt+KeyP': {
|
||||||
}
|
action: () => {
|
||||||
if (hasPressedAltAndLKey(e)) {
|
this.focusEditorInputField();
|
||||||
this.focusEditorInputField();
|
},
|
||||||
}
|
allowOnFocusedInput: true,
|
||||||
|
},
|
||||||
|
'Alt+KeyL': {
|
||||||
|
action: () => {
|
||||||
|
this.focusEditorInputField();
|
||||||
|
},
|
||||||
|
allowOnFocusedInput: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
},
|
},
|
||||||
focusEditorInputField(pos = 'end') {
|
focusEditorInputField(pos = 'end') {
|
||||||
const { tr } = this.editorView.state;
|
const { tr } = this.editorView.state;
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import {
|
|||||||
} from '@chatwoot/prosemirror-schema';
|
} from '@chatwoot/prosemirror-schema';
|
||||||
import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
|
import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||||
|
|
||||||
const MAXIMUM_FILE_UPLOAD_SIZE = 4; // in MB
|
const MAXIMUM_FILE_UPLOAD_SIZE = 4; // in MB
|
||||||
@@ -51,7 +51,7 @@ const createState = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [eventListenerMixins, uiSettingsMixin, alertMixin],
|
mixins: [keyboardEventListenerMixins, uiSettingsMixin, alertMixin],
|
||||||
props: {
|
props: {
|
||||||
value: { type: String, default: '' },
|
value: { type: String, default: '' },
|
||||||
editorId: { type: String, default: '' },
|
editorId: { type: String, default: '' },
|
||||||
|
|||||||
@@ -136,8 +136,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import FileUpload from 'vue-upload-component';
|
import FileUpload from 'vue-upload-component';
|
||||||
import * as ActiveStorage from 'activestorage';
|
import * as ActiveStorage from 'activestorage';
|
||||||
import { hasPressedAltAndAKey } from 'shared/helpers/KeyboardHelpers';
|
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
|
||||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||||
import inboxMixin from 'shared/mixins/inboxMixin';
|
import inboxMixin from 'shared/mixins/inboxMixin';
|
||||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||||
@@ -154,7 +153,7 @@ import { mapGetters } from 'vuex';
|
|||||||
export default {
|
export default {
|
||||||
name: 'ReplyBottomPanel',
|
name: 'ReplyBottomPanel',
|
||||||
components: { FileUpload, VideoCallButton, AIAssistanceButton },
|
components: { FileUpload, VideoCallButton, AIAssistanceButton },
|
||||||
mixins: [eventListenerMixins, uiSettingsMixin, inboxMixin],
|
mixins: [keyboardEventListenerMixins, uiSettingsMixin, inboxMixin],
|
||||||
props: {
|
props: {
|
||||||
mode: {
|
mode: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -340,10 +339,15 @@ export default {
|
|||||||
ActiveStorage.start();
|
ActiveStorage.start();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
handleKeyEvents(e) {
|
getKeyboardEvents() {
|
||||||
if (hasPressedAltAndAKey(e)) {
|
return {
|
||||||
this.$refs.upload.$children[1].$el.click();
|
'Alt+KeyA': {
|
||||||
}
|
action: () => {
|
||||||
|
this.$refs.upload.$children[1].$el.click();
|
||||||
|
},
|
||||||
|
allowOnFocusedInput: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
},
|
},
|
||||||
toggleMessageSignature() {
|
toggleMessageSignature() {
|
||||||
this.setSignatureFlagForInbox(this.channelType, !this.sendWithSignature);
|
this.setSignatureFlagForInbox(this.channelType, !this.sendWithSignature);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="bg-black-50 flex justify-between dark:bg-slate-800">
|
<div class="flex justify-between bg-black-50 dark:bg-slate-800">
|
||||||
<div class="button-group">
|
<div class="button-group">
|
||||||
<woot-button
|
<woot-button
|
||||||
variant="clear"
|
variant="clear"
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
{{ $t('CONVERSATION.REPLYBOX.PRIVATE_NOTE') }}
|
{{ $t('CONVERSATION.REPLYBOX.PRIVATE_NOTE') }}
|
||||||
</woot-button>
|
</woot-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center my-0 mx-4">
|
<div class="flex items-center mx-4 my-0">
|
||||||
<div v-if="isMessageLengthReachingThreshold" class="text-xs">
|
<div v-if="isMessageLengthReachingThreshold" class="text-xs">
|
||||||
<span :class="charLengthClass">
|
<span :class="charLengthClass">
|
||||||
{{ characterLengthWarning }}
|
{{ characterLengthWarning }}
|
||||||
@@ -48,14 +48,10 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { REPLY_EDITOR_MODES, CHAR_LENGTH_WARNING } from './constants';
|
import { REPLY_EDITOR_MODES, CHAR_LENGTH_WARNING } from './constants';
|
||||||
import {
|
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||||
hasPressedAltAndPKey,
|
|
||||||
hasPressedAltAndLKey,
|
|
||||||
} from 'shared/helpers/KeyboardHelpers';
|
|
||||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ReplyTopPanel',
|
name: 'ReplyTopPanel',
|
||||||
mixins: [eventListenerMixins],
|
mixins: [keyboardEventListenerMixins],
|
||||||
props: {
|
props: {
|
||||||
mode: {
|
mode: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -99,13 +95,17 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
handleKeyEvents(e) {
|
getKeyboardEvents() {
|
||||||
if (hasPressedAltAndPKey(e)) {
|
return {
|
||||||
this.handleNoteClick();
|
'Alt+KeyP': {
|
||||||
}
|
action: () => this.handleNoteClick(),
|
||||||
if (hasPressedAltAndLKey(e)) {
|
allowOnFocusedInput: true,
|
||||||
this.handleReplyClick();
|
},
|
||||||
}
|
'Alt+KeyL': {
|
||||||
|
action: () => this.handleReplyClick(),
|
||||||
|
allowOnFocusedInput: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
},
|
},
|
||||||
handleReplyClick() {
|
handleReplyClick() {
|
||||||
this.setReplyMode(REPLY_EDITOR_MODES.REPLY);
|
this.setReplyMode(REPLY_EDITOR_MODES.REPLY);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div
|
<div
|
||||||
class="relative flex items-start flex-grow-0 flex-shrink-0 w-auto max-w-full px-4 py-0 border-t-0 border-b-0 border-l-2 border-r-0 border-transparent border-solid cursor-pointer conversation hover:bg-slate-25 dark:hover:bg-slate-800 group"
|
class="relative flex items-start flex-grow-0 flex-shrink-0 w-auto max-w-full px-4 py-0 border-t-0 border-b-0 border-l-2 border-r-0 border-transparent border-solid cursor-pointer conversation hover:bg-slate-25 dark:hover:bg-slate-800 group"
|
||||||
:class="{
|
:class="{
|
||||||
'animate-card-select bg-slate-25 dark:bg-slate-800 border-woot-500':
|
'active animate-card-select bg-slate-25 dark:bg-slate-800 border-woot-500':
|
||||||
isActiveChat,
|
isActiveChat,
|
||||||
'unread-chat': hasUnread,
|
'unread-chat': hasUnread,
|
||||||
'has-inbox-name': showInboxName,
|
'has-inbox-name': showInboxName,
|
||||||
|
|||||||
@@ -77,11 +77,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import { hasPressedAltAndOKey } from 'shared/helpers/KeyboardHelpers';
|
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import agentMixin from '../../../mixins/agentMixin.js';
|
import agentMixin from '../../../mixins/agentMixin.js';
|
||||||
import BackButton from '../BackButton.vue';
|
import BackButton from '../BackButton.vue';
|
||||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||||
import inboxMixin from 'shared/mixins/inboxMixin';
|
import inboxMixin from 'shared/mixins/inboxMixin';
|
||||||
import InboxName from '../InboxName.vue';
|
import InboxName from '../InboxName.vue';
|
||||||
import MoreActions from './MoreActions.vue';
|
import MoreActions from './MoreActions.vue';
|
||||||
@@ -99,7 +98,7 @@ export default {
|
|||||||
Thumbnail,
|
Thumbnail,
|
||||||
SLACardLabel,
|
SLACardLabel,
|
||||||
},
|
},
|
||||||
mixins: [inboxMixin, agentMixin, eventListenerMixins],
|
mixins: [inboxMixin, agentMixin, keyboardEventListenerMixins],
|
||||||
props: {
|
props: {
|
||||||
chat: {
|
chat: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -182,10 +181,12 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
handleKeyEvents(e) {
|
getKeyboardEvents() {
|
||||||
if (hasPressedAltAndOKey(e)) {
|
return {
|
||||||
this.$emit('contact-panel-toggle');
|
'Alt+KeyO': {
|
||||||
}
|
action: () => this.$emit('contact-panel-toggle'),
|
||||||
|
},
|
||||||
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -116,13 +116,12 @@ import conversationMixin, {
|
|||||||
} from '../../../mixins/conversations';
|
} from '../../../mixins/conversations';
|
||||||
import inboxMixin, { INBOX_FEATURES } from 'shared/mixins/inboxMixin';
|
import inboxMixin, { INBOX_FEATURES } from 'shared/mixins/inboxMixin';
|
||||||
import configMixin from 'shared/mixins/configMixin';
|
import configMixin from 'shared/mixins/configMixin';
|
||||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||||
import aiMixin from 'dashboard/mixins/aiMixin';
|
import aiMixin from 'dashboard/mixins/aiMixin';
|
||||||
|
|
||||||
// utils
|
// utils
|
||||||
import { getTypingUsersText } from '../../../helper/commons';
|
import { getTypingUsersText } from '../../../helper/commons';
|
||||||
import { calculateScrollTop } from './helpers/scrollTopCalculationHelper';
|
import { calculateScrollTop } from './helpers/scrollTopCalculationHelper';
|
||||||
import { isEscape } from 'shared/helpers/KeyboardHelpers';
|
|
||||||
import { LocalStorage } from 'shared/helpers/localStorage';
|
import { LocalStorage } from 'shared/helpers/localStorage';
|
||||||
|
|
||||||
// constants
|
// constants
|
||||||
@@ -141,7 +140,7 @@ export default {
|
|||||||
mixins: [
|
mixins: [
|
||||||
conversationMixin,
|
conversationMixin,
|
||||||
inboxMixin,
|
inboxMixin,
|
||||||
eventListenerMixins,
|
keyboardEventListenerMixins,
|
||||||
configMixin,
|
configMixin,
|
||||||
aiMixin,
|
aiMixin,
|
||||||
],
|
],
|
||||||
@@ -418,10 +417,12 @@ export default {
|
|||||||
closePopoutReplyBox() {
|
closePopoutReplyBox() {
|
||||||
this.isPopoutReplyBox = false;
|
this.isPopoutReplyBox = false;
|
||||||
},
|
},
|
||||||
handleKeyEvents(e) {
|
getKeyboardEvents() {
|
||||||
if (isEscape(e)) {
|
return {
|
||||||
this.closePopoutReplyBox();
|
Escape: {
|
||||||
}
|
action: () => this.closePopoutReplyBox(),
|
||||||
|
},
|
||||||
|
};
|
||||||
},
|
},
|
||||||
addScrollListener() {
|
addScrollListener() {
|
||||||
this.conversationPanel = this.$el.querySelector('.conversation-panel');
|
this.conversationPanel = this.$el.querySelector('.conversation-panel');
|
||||||
|
|||||||
@@ -100,7 +100,7 @@
|
|||||||
<attachment-preview
|
<attachment-preview
|
||||||
class="flex-col mt-4"
|
class="flex-col mt-4"
|
||||||
:attachments="attachedFiles"
|
:attachments="attachedFiles"
|
||||||
:remove-attachment="removeAttachment"
|
@remove-attachment="removeAttachment"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<message-signature-missing-alert
|
<message-signature-missing-alert
|
||||||
@@ -155,6 +155,7 @@
|
|||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import { mixin as clickaway } from 'vue-clickaway';
|
import { mixin as clickaway } from 'vue-clickaway';
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
|
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||||
|
|
||||||
import CannedResponse from './CannedResponse.vue';
|
import CannedResponse from './CannedResponse.vue';
|
||||||
import ReplyToMessage from './ReplyToMessage.vue';
|
import ReplyToMessage from './ReplyToMessage.vue';
|
||||||
@@ -178,7 +179,6 @@ import {
|
|||||||
replaceVariablesInMessage,
|
replaceVariablesInMessage,
|
||||||
} from '@chatwoot/utils';
|
} from '@chatwoot/utils';
|
||||||
import WhatsappTemplates from './WhatsappTemplates/Modal.vue';
|
import WhatsappTemplates from './WhatsappTemplates/Modal.vue';
|
||||||
import { buildHotKeys } from 'shared/helpers/KeyboardHelpers';
|
|
||||||
import { MESSAGE_MAX_LENGTH } from 'shared/helpers/MessageTypeHelper';
|
import { MESSAGE_MAX_LENGTH } from 'shared/helpers/MessageTypeHelper';
|
||||||
import inboxMixin, { INBOX_FEATURES } from 'shared/mixins/inboxMixin';
|
import inboxMixin, { INBOX_FEATURES } from 'shared/mixins/inboxMixin';
|
||||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||||
@@ -225,6 +225,7 @@ export default {
|
|||||||
messageFormatterMixin,
|
messageFormatterMixin,
|
||||||
rtlMixin,
|
rtlMixin,
|
||||||
fileUploadMixin,
|
fileUploadMixin,
|
||||||
|
keyboardEventListenerMixins,
|
||||||
],
|
],
|
||||||
props: {
|
props: {
|
||||||
popoutReplyBox: {
|
popoutReplyBox: {
|
||||||
@@ -701,24 +702,41 @@ export default {
|
|||||||
this.$store.dispatch('draftMessages/delete', { key });
|
this.$store.dispatch('draftMessages/delete', { key });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handleKeyEvents(e) {
|
getKeyboardEvents() {
|
||||||
const keyCode = buildHotKeys(e);
|
return {
|
||||||
if (keyCode === 'escape') {
|
Escape: {
|
||||||
this.hideEmojiPicker();
|
action: () => {
|
||||||
this.hideMentions();
|
this.hideEmojiPicker();
|
||||||
} else if (keyCode === 'meta+k') {
|
this.hideMentions();
|
||||||
const ninja = document.querySelector('ninja-keys');
|
},
|
||||||
ninja.open();
|
allowOnFocusedInput: true,
|
||||||
e.preventDefault();
|
},
|
||||||
} else if (keyCode === 'enter' && this.isAValidEvent('enter')) {
|
'$mod+KeyK': {
|
||||||
this.onSendReply();
|
action: e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
} else if (
|
const ninja = document.querySelector('ninja-keys');
|
||||||
['meta+enter', 'ctrl+enter'].includes(keyCode) &&
|
ninja.open();
|
||||||
this.isAValidEvent('cmd_enter')
|
},
|
||||||
) {
|
allowOnFocusedInput: true,
|
||||||
this.onSendReply();
|
},
|
||||||
}
|
Enter: {
|
||||||
|
action: e => {
|
||||||
|
if (this.isAValidEvent('enter')) {
|
||||||
|
this.onSendReply();
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
allowOnFocusedInput: true,
|
||||||
|
},
|
||||||
|
'$mod+Enter': {
|
||||||
|
action: () => {
|
||||||
|
if (this.isAValidEvent('cmd_enter')) {
|
||||||
|
this.onSendReply();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
allowOnFocusedInput: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
},
|
},
|
||||||
isAValidEvent(selectedKey) {
|
isAValidEvent(selectedKey) {
|
||||||
return (
|
return (
|
||||||
@@ -937,6 +955,13 @@ export default {
|
|||||||
this.bccEmails = '';
|
this.bccEmails = '';
|
||||||
this.toEmails = '';
|
this.toEmails = '';
|
||||||
},
|
},
|
||||||
|
clearRecorder() {
|
||||||
|
this.isRecordingAudio = false;
|
||||||
|
// Only clear the recorded audio when we click toggle button.
|
||||||
|
this.attachedFiles = this.attachedFiles.filter(
|
||||||
|
file => !file?.isRecordedAudio
|
||||||
|
);
|
||||||
|
},
|
||||||
toggleEmojiPicker() {
|
toggleEmojiPicker() {
|
||||||
this.showEmojiPicker = !this.showEmojiPicker;
|
this.showEmojiPicker = !this.showEmojiPicker;
|
||||||
},
|
},
|
||||||
@@ -944,8 +969,7 @@ export default {
|
|||||||
this.isRecordingAudio = !this.isRecordingAudio;
|
this.isRecordingAudio = !this.isRecordingAudio;
|
||||||
this.isRecorderAudioStopped = !this.isRecordingAudio;
|
this.isRecorderAudioStopped = !this.isRecordingAudio;
|
||||||
if (!this.isRecordingAudio) {
|
if (!this.isRecordingAudio) {
|
||||||
this.clearMessage();
|
this.clearRecorder();
|
||||||
this.clearEmailField();
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
toggleAudioRecorderPlayPause() {
|
toggleAudioRecorderPlayPause() {
|
||||||
@@ -989,7 +1013,13 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onFinishRecorder(file) {
|
onFinishRecorder(file) {
|
||||||
return file && this.onFileUpload(file);
|
// Added a new key isRecordedAudio to the file to find it's and recorded audio
|
||||||
|
// Because to filter and show only non recorded audio and other attachments
|
||||||
|
const autoRecordedFile = {
|
||||||
|
...file,
|
||||||
|
isRecordedAudio: true,
|
||||||
|
};
|
||||||
|
return file && this.onFileUpload(autoRecordedFile);
|
||||||
},
|
},
|
||||||
toggleTyping(status) {
|
toggleTyping(status) {
|
||||||
const conversationId = this.currentChat.id;
|
const conversationId = this.currentChat.id;
|
||||||
@@ -1015,13 +1045,12 @@ export default {
|
|||||||
isPrivate: this.isPrivate,
|
isPrivate: this.isPrivate,
|
||||||
thumb: reader.result,
|
thumb: reader.result,
|
||||||
blobSignedId: blob ? blob.signed_id : undefined,
|
blobSignedId: blob ? blob.signed_id : undefined,
|
||||||
|
isRecordedAudio: file?.isRecordedAudio || false,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
removeAttachment(itemIndex) {
|
removeAttachment(attachments) {
|
||||||
this.attachedFiles = this.attachedFiles.filter(
|
this.attachedFiles = attachments;
|
||||||
(item, index) => itemIndex !== index
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
setReplyToInPayload(payload) {
|
setReplyToInPayload(payload) {
|
||||||
if (this.inReplyTo?.id) {
|
if (this.inReplyTo?.id) {
|
||||||
|
|||||||
@@ -75,9 +75,10 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
handleKeyboardEvent(e) {
|
adjustScroll() {
|
||||||
this.processKeyDownEvent(e);
|
this.$nextTick(() => {
|
||||||
this.$el.scrollTop = 50 * this.selectedIndex;
|
this.$el.scrollTop = 50 * this.selectedIndex;
|
||||||
|
});
|
||||||
},
|
},
|
||||||
onHover(index) {
|
onHover(index) {
|
||||||
this.selectedIndex = index;
|
this.selectedIndex = index;
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
@error="onImgError"
|
@error="onImgError"
|
||||||
@click="onClick"
|
@click="onClick"
|
||||||
/>
|
/>
|
||||||
<audio v-else-if="isAudio" controls class="skip-context-menu">
|
<audio v-else-if="isAudio" controls class="skip-context-menu mb-0.5">
|
||||||
<source :src="`${dataUrl}?t=${Date.now()}`" />
|
<source :src="`${dataUrl}?t=${Date.now()}`" />
|
||||||
</audio>
|
</audio>
|
||||||
<gallery-view
|
<gallery-view
|
||||||
|
|||||||
+26
-21
@@ -182,12 +182,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import { mixin as clickaway } from 'vue-clickaway';
|
import { mixin as clickaway } from 'vue-clickaway';
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import {
|
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||||
isEscape,
|
|
||||||
hasPressedArrowLeftKey,
|
|
||||||
hasPressedArrowRightKey,
|
|
||||||
} from 'shared/helpers/KeyboardHelpers';
|
|
||||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
|
||||||
import timeMixin from 'dashboard/mixins/time';
|
import timeMixin from 'dashboard/mixins/time';
|
||||||
|
|
||||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||||
@@ -205,7 +200,7 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
Thumbnail,
|
Thumbnail,
|
||||||
},
|
},
|
||||||
mixins: [eventListenerMixins, clickaway, timeMixin],
|
mixins: [keyboardEventListenerMixins, clickaway, timeMixin],
|
||||||
props: {
|
props: {
|
||||||
show: {
|
show: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
@@ -304,20 +299,30 @@ export default {
|
|||||||
this.activeAttachment = attachment;
|
this.activeAttachment = attachment;
|
||||||
this.activeFileType = type;
|
this.activeFileType = type;
|
||||||
},
|
},
|
||||||
onKeyDownHandler(e) {
|
getKeyboardEvents() {
|
||||||
if (isEscape(e)) {
|
return {
|
||||||
this.onClose();
|
Escape: {
|
||||||
} else if (hasPressedArrowLeftKey(e)) {
|
action: () => {
|
||||||
this.onClickChangeAttachment(
|
this.onClose();
|
||||||
this.allAttachments[this.activeImageIndex - 1],
|
},
|
||||||
this.activeImageIndex - 1
|
},
|
||||||
);
|
ArrowLeft: {
|
||||||
} else if (hasPressedArrowRightKey(e)) {
|
action: () => {
|
||||||
this.onClickChangeAttachment(
|
this.onClickChangeAttachment(
|
||||||
this.allAttachments[this.activeImageIndex + 1],
|
this.allAttachments[this.activeImageIndex - 1],
|
||||||
this.activeImageIndex + 1
|
this.activeImageIndex - 1
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
|
},
|
||||||
|
ArrowRight: {
|
||||||
|
action: () => {
|
||||||
|
this.onClickChangeAttachment(
|
||||||
|
this.allAttachments[this.activeImageIndex + 1],
|
||||||
|
this.activeImageIndex + 1
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
},
|
},
|
||||||
onClickDownload() {
|
onClickDownload() {
|
||||||
const { file_type: type, data_url: url } = this.activeAttachment;
|
const { file_type: type, data_url: url } = this.activeAttachment;
|
||||||
|
|||||||
@@ -74,15 +74,10 @@
|
|||||||
<script>
|
<script>
|
||||||
import countries from 'shared/constants/countries.js';
|
import countries from 'shared/constants/countries.js';
|
||||||
import parsePhoneNumber from 'libphonenumber-js';
|
import parsePhoneNumber from 'libphonenumber-js';
|
||||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||||
import {
|
|
||||||
hasPressedArrowUpKey,
|
|
||||||
hasPressedArrowDownKey,
|
|
||||||
isEnter,
|
|
||||||
} from 'shared/helpers/KeyboardHelpers';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [eventListenerMixins],
|
mixins: [keyboardEventListenerMixins],
|
||||||
props: {
|
props: {
|
||||||
value: {
|
value: {
|
||||||
type: [String, Number],
|
type: [String, Number],
|
||||||
@@ -183,6 +178,7 @@ export default {
|
|||||||
this.$emit('blur', e.target.value);
|
this.$emit('blur', e.target.value);
|
||||||
},
|
},
|
||||||
dropdownItem() {
|
dropdownItem() {
|
||||||
|
if (!this.showDropdown) return [];
|
||||||
return Array.from(
|
return Array.from(
|
||||||
this.$refs.dropdown.querySelectorAll(
|
this.$refs.dropdown.querySelectorAll(
|
||||||
'div.country-dropdown div.country-dropdown--item'
|
'div.country-dropdown div.country-dropdown--item'
|
||||||
@@ -190,34 +186,27 @@ export default {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
focusedItem() {
|
focusedItem() {
|
||||||
|
if (!this.showDropdown) return [];
|
||||||
return Array.from(
|
return Array.from(
|
||||||
this.$refs.dropdown.querySelectorAll('div.country-dropdown div.focus')
|
this.$refs.dropdown.querySelectorAll('div.country-dropdown div.focus')
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
focusedItemIndex() {
|
focusedItemIndex() {
|
||||||
|
if (!this.showDropdown) return -1;
|
||||||
return Array.from(this.dropdownItem()).indexOf(this.focusedItem()[0]);
|
return Array.from(this.dropdownItem()).indexOf(this.focusedItem()[0]);
|
||||||
},
|
},
|
||||||
onKeyDownHandler(e) {
|
moveUp() {
|
||||||
const { showDropdown, filteredCountriesBySearch, onSelectCountry } = this;
|
if (!this.showDropdown) return;
|
||||||
const { selectedIndex } = this;
|
this.selectedIndex = Math.max(this.selectedIndex - 1, 0);
|
||||||
|
this.$refs.dropdown.scrollTop = this.focusedItemIndex() * 28 - 56;
|
||||||
if (showDropdown) {
|
},
|
||||||
if (hasPressedArrowDownKey(e)) {
|
moveDown() {
|
||||||
e.preventDefault();
|
if (!this.showDropdown) return;
|
||||||
this.selectedIndex = Math.min(
|
this.selectedIndex = Math.min(
|
||||||
selectedIndex + 1,
|
this.selectedIndex + 1,
|
||||||
filteredCountriesBySearch.length - 1
|
this.filteredCountriesBySearch.length - 1
|
||||||
);
|
);
|
||||||
this.$refs.dropdown.scrollTop = this.focusedItemIndex() * 28;
|
this.$refs.dropdown.scrollTop = this.focusedItemIndex() * 28 - 56;
|
||||||
} else if (hasPressedArrowUpKey(e)) {
|
|
||||||
e.preventDefault();
|
|
||||||
this.selectedIndex = Math.max(selectedIndex - 1, 0);
|
|
||||||
this.$refs.dropdown.scrollTop = this.focusedItemIndex() * 28 - 56;
|
|
||||||
} else if (isEnter(e)) {
|
|
||||||
e.preventDefault();
|
|
||||||
onSelectCountry(filteredCountriesBySearch[selectedIndex]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onSelectCountry(country) {
|
onSelectCountry(country) {
|
||||||
this.activeCountryCode = country.id;
|
this.activeCountryCode = country.id;
|
||||||
@@ -235,6 +224,33 @@ export default {
|
|||||||
this.activeDialCode = number.countryCallingCode;
|
this.activeDialCode = number.countryCallingCode;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
getKeyboardEvents() {
|
||||||
|
return {
|
||||||
|
ArrowUp: {
|
||||||
|
action: e => {
|
||||||
|
e.preventDefault();
|
||||||
|
this.moveUp();
|
||||||
|
},
|
||||||
|
allowOnFocusedInput: true,
|
||||||
|
},
|
||||||
|
ArrowDown: {
|
||||||
|
action: e => {
|
||||||
|
e.preventDefault();
|
||||||
|
this.moveDown();
|
||||||
|
},
|
||||||
|
allowOnFocusedInput: true,
|
||||||
|
},
|
||||||
|
Enter: {
|
||||||
|
action: e => {
|
||||||
|
e.preventDefault();
|
||||||
|
this.onSelectCountry(
|
||||||
|
this.filteredCountriesBySearch[this.selectedIndex]
|
||||||
|
);
|
||||||
|
},
|
||||||
|
allowOnFocusedInput: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
toggleCountryDropdown() {
|
toggleCountryDropdown() {
|
||||||
this.showDropdown = !this.showDropdown;
|
this.showDropdown = !this.showDropdown;
|
||||||
this.selectedIndex = -1;
|
this.selectedIndex = -1;
|
||||||
|
|||||||
@@ -84,9 +84,7 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
handleKeyboardEvent(e) {
|
adjustScroll() {},
|
||||||
this.processKeyDownEvent(e);
|
|
||||||
},
|
|
||||||
onHover(index) {
|
onHover(index) {
|
||||||
this.selectedIndex = index;
|
this.selectedIndex = index;
|
||||||
},
|
},
|
||||||
|
|||||||
+42
-19
@@ -1,12 +1,7 @@
|
|||||||
import { buildHotKeys } from 'shared/helpers/KeyboardHelpers';
|
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mounted() {
|
mixins: [keyboardEventListenerMixins],
|
||||||
document.addEventListener('keydown', this.handleKeyboardEvent);
|
|
||||||
},
|
|
||||||
beforeDestroy() {
|
|
||||||
document.removeEventListener('keydown', this.handleKeyboardEvent);
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
moveSelectionUp() {
|
moveSelectionUp() {
|
||||||
if (!this.selectedIndex) {
|
if (!this.selectedIndex) {
|
||||||
@@ -14,6 +9,7 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
this.selectedIndex -= 1;
|
this.selectedIndex -= 1;
|
||||||
}
|
}
|
||||||
|
this.adjustScroll();
|
||||||
},
|
},
|
||||||
moveSelectionDown() {
|
moveSelectionDown() {
|
||||||
if (this.selectedIndex === this.items.length - 1) {
|
if (this.selectedIndex === this.items.length - 1) {
|
||||||
@@ -21,19 +17,46 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
this.selectedIndex += 1;
|
this.selectedIndex += 1;
|
||||||
}
|
}
|
||||||
|
this.adjustScroll();
|
||||||
},
|
},
|
||||||
processKeyDownEvent(e) {
|
getKeyboardEvents() {
|
||||||
const keyPattern = buildHotKeys(e);
|
return {
|
||||||
if (['arrowup', 'ctrl+p'].includes(keyPattern)) {
|
ArrowUp: {
|
||||||
this.moveSelectionUp();
|
action: e => {
|
||||||
e.preventDefault();
|
this.moveSelectionUp();
|
||||||
} else if (['arrowdown', 'ctrl+n'].includes(keyPattern)) {
|
e.preventDefault();
|
||||||
this.moveSelectionDown();
|
},
|
||||||
e.preventDefault();
|
allowOnFocusedInput: true,
|
||||||
} else if (keyPattern === 'enter') {
|
},
|
||||||
this.onSelect();
|
'Control+KeyP': {
|
||||||
e.preventDefault();
|
action: e => {
|
||||||
}
|
this.moveSelectionUp();
|
||||||
|
e.preventDefault();
|
||||||
|
},
|
||||||
|
allowOnFocusedInput: true,
|
||||||
|
},
|
||||||
|
ArrowDown: {
|
||||||
|
action: e => {
|
||||||
|
this.moveSelectionDown();
|
||||||
|
e.preventDefault();
|
||||||
|
},
|
||||||
|
allowOnFocusedInput: true,
|
||||||
|
},
|
||||||
|
'Control+KeyN': {
|
||||||
|
action: e => {
|
||||||
|
this.moveSelectionDown();
|
||||||
|
e.preventDefault();
|
||||||
|
},
|
||||||
|
allowOnFocusedInput: true,
|
||||||
|
},
|
||||||
|
Enter: {
|
||||||
|
action: e => {
|
||||||
|
this.onSelect();
|
||||||
|
e.preventDefault();
|
||||||
|
},
|
||||||
|
allowOnFocusedInput: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
+52
-47
@@ -1,64 +1,69 @@
|
|||||||
import mentionSelectionKeyboardMixin from '../mentionSelectionKeyboardMixin';
|
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||||
import { shallowMount } from '@vue/test-utils';
|
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||||
|
|
||||||
|
const localVue = createLocalVue();
|
||||||
|
|
||||||
const buildComponent = ({ data = {}, methods = {} }) => ({
|
const buildComponent = ({ data = {}, methods = {} }) => ({
|
||||||
render() {},
|
render() {},
|
||||||
data() {
|
data() {
|
||||||
return data;
|
return { ...data, selectedIndex: 0, items: [1, 2, 3] };
|
||||||
},
|
},
|
||||||
methods,
|
methods: { ...methods, onSelect: jest.fn(), adjustScroll: jest.fn() },
|
||||||
mixins: [mentionSelectionKeyboardMixin],
|
mixins: [keyboardEventListenerMixins],
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('mentionSelectionKeyboardMixin', () => {
|
describe('mentionSelectionKeyboardMixin', () => {
|
||||||
test('register listeners', () => {
|
let wrapper;
|
||||||
jest.spyOn(document, 'addEventListener');
|
|
||||||
|
beforeEach(() => {
|
||||||
const Component = buildComponent({});
|
const Component = buildComponent({});
|
||||||
shallowMount(Component);
|
wrapper = shallowMount(Component, { localVue });
|
||||||
// undefined expected as the method is not defined in the component
|
|
||||||
expect(document.addEventListener).toHaveBeenCalledWith(
|
|
||||||
'keydown',
|
|
||||||
undefined
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('processKeyDownEvent updates index on arrow up', () => {
|
it('ArrowUp and Control+KeyP update selectedIndex correctly', () => {
|
||||||
const Component = buildComponent({
|
const preventDefault = jest.fn();
|
||||||
data: { selectedIndex: 0, items: [1, 2, 3] },
|
const keyboardEvents = wrapper.vm.getKeyboardEvents();
|
||||||
});
|
|
||||||
const wrapper = shallowMount(Component);
|
if (keyboardEvents && keyboardEvents.ArrowUp) {
|
||||||
wrapper.vm.processKeyDownEvent({
|
keyboardEvents.ArrowUp.action({ preventDefault });
|
||||||
ctrlKey: true,
|
expect(wrapper.vm.selectedIndex).toBe(2);
|
||||||
key: 'p',
|
expect(preventDefault).toHaveBeenCalled();
|
||||||
preventDefault: jest.fn(),
|
}
|
||||||
});
|
|
||||||
expect(wrapper.vm.selectedIndex).toBe(2);
|
wrapper.setData({ selectedIndex: 1 });
|
||||||
|
if (keyboardEvents && keyboardEvents['Control+KeyP']) {
|
||||||
|
keyboardEvents['Control+KeyP'].action({ preventDefault });
|
||||||
|
expect(wrapper.vm.selectedIndex).toBe(0);
|
||||||
|
expect(preventDefault).toHaveBeenCalledTimes(2);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('processKeyDownEvent updates index on arrow down', () => {
|
it('ArrowDown and Control+KeyN update selectedIndex correctly', () => {
|
||||||
const Component = buildComponent({
|
const preventDefault = jest.fn();
|
||||||
data: { selectedIndex: 0, items: [1, 2, 3] },
|
const keyboardEvents = wrapper.vm.getKeyboardEvents();
|
||||||
});
|
|
||||||
const wrapper = shallowMount(Component);
|
if (keyboardEvents && keyboardEvents.ArrowDown) {
|
||||||
wrapper.vm.processKeyDownEvent({
|
keyboardEvents.ArrowDown.action({ preventDefault });
|
||||||
key: 'ArrowDown',
|
expect(wrapper.vm.selectedIndex).toBe(1);
|
||||||
preventDefault: jest.fn(),
|
expect(preventDefault).toHaveBeenCalled();
|
||||||
});
|
}
|
||||||
expect(wrapper.vm.selectedIndex).toBe(1);
|
|
||||||
|
wrapper.setData({ selectedIndex: 1 });
|
||||||
|
if (keyboardEvents && keyboardEvents['Control+KeyN']) {
|
||||||
|
keyboardEvents['Control+KeyN'].action({ preventDefault });
|
||||||
|
expect(wrapper.vm.selectedIndex).toBe(2);
|
||||||
|
expect(preventDefault).toHaveBeenCalledTimes(2);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('processKeyDownEvent calls select methods on Enter Key', () => {
|
it('Enter key triggers onSelect method', () => {
|
||||||
const onSelectMockFn = jest.fn();
|
const preventDefault = jest.fn();
|
||||||
const Component = buildComponent({
|
const keyboardEvents = wrapper.vm.getKeyboardEvents();
|
||||||
data: { selectedIndex: 0, items: [1, 2, 3] },
|
|
||||||
methods: { onSelect: () => onSelectMockFn('enterKey pressed') },
|
if (keyboardEvents && keyboardEvents.Enter) {
|
||||||
});
|
keyboardEvents.Enter.action({ preventDefault });
|
||||||
const wrapper = shallowMount(Component);
|
expect(wrapper.vm.onSelect).toHaveBeenCalled();
|
||||||
wrapper.vm.processKeyDownEvent({
|
expect(preventDefault).toHaveBeenCalled();
|
||||||
key: 'Enter',
|
}
|
||||||
preventDefault: jest.fn(),
|
|
||||||
});
|
|
||||||
expect(onSelectMockFn).toHaveBeenCalledWith('enterKey pressed');
|
|
||||||
wrapper.vm.onSelect();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -53,12 +53,6 @@ export const SHORTCUT_KEYS = [
|
|||||||
firstKey: 'Alt / ⌥',
|
firstKey: 'Alt / ⌥',
|
||||||
secondKey: 'S',
|
secondKey: 'S',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 10,
|
|
||||||
label: 'SWITCH_CONVERSATION_STATUS',
|
|
||||||
firstKey: 'Alt / ⌥',
|
|
||||||
secondKey: 'B',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 11,
|
id: 11,
|
||||||
label: 'SWITCH_TO_PRIVATE_NOTE',
|
label: 'SWITCH_TO_PRIVATE_NOTE',
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { computed, getCurrentInstance } from 'vue';
|
||||||
|
import Vue from 'vue';
|
||||||
|
import VueI18n from 'vue-i18n';
|
||||||
|
|
||||||
|
let i18nInstance = VueI18n;
|
||||||
|
|
||||||
|
export function useI18n() {
|
||||||
|
if (!i18nInstance) throw new Error('vue-i18n not initialized');
|
||||||
|
|
||||||
|
const i18n = i18nInstance;
|
||||||
|
|
||||||
|
const instance = getCurrentInstance();
|
||||||
|
const vm = instance?.proxy || instance || new Vue({});
|
||||||
|
|
||||||
|
const locale = computed({
|
||||||
|
get() {
|
||||||
|
return i18n.locale;
|
||||||
|
},
|
||||||
|
set(v) {
|
||||||
|
i18n.locale = v;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
locale,
|
||||||
|
t: vm.$t.bind(vm),
|
||||||
|
tc: vm.$tc.bind(vm),
|
||||||
|
d: vm.$d.bind(vm),
|
||||||
|
te: vm.$te.bind(vm),
|
||||||
|
n: vm.$n.bind(vm),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import AnalyticsHelper from './AnalyticsHelper';
|
import AnalyticsHelper from './AnalyticsHelper';
|
||||||
import LogRocket from 'logrocket';
|
|
||||||
import DashboardAudioNotificationHelper from './AudioAlerts/DashboardAudioNotificationHelper';
|
import DashboardAudioNotificationHelper from './AudioAlerts/DashboardAudioNotificationHelper';
|
||||||
|
|
||||||
export const CHATWOOT_SET_USER = 'CHATWOOT_SET_USER';
|
export const CHATWOOT_SET_USER = 'CHATWOOT_SET_USER';
|
||||||
@@ -11,12 +10,6 @@ export const ANALYTICS_RESET = 'ANALYTICS_RESET';
|
|||||||
export const initializeAnalyticsEvents = () => {
|
export const initializeAnalyticsEvents = () => {
|
||||||
window.bus.$on(ANALYTICS_IDENTITY, ({ user }) => {
|
window.bus.$on(ANALYTICS_IDENTITY, ({ user }) => {
|
||||||
AnalyticsHelper.identify(user);
|
AnalyticsHelper.identify(user);
|
||||||
if (window.logRocketProjectId) {
|
|
||||||
LogRocket.identify(user.id, {
|
|
||||||
email: user.email,
|
|
||||||
name: user.name,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -316,7 +316,6 @@
|
|||||||
"GO_TO_REPORTS_SIDEBAR": "Go to Reports sidebar",
|
"GO_TO_REPORTS_SIDEBAR": "Go to Reports sidebar",
|
||||||
"MOVE_TO_NEXT_TAB": "Move to next tab in conversation list",
|
"MOVE_TO_NEXT_TAB": "Move to next tab in conversation list",
|
||||||
"GO_TO_SETTINGS": "Go to Settings",
|
"GO_TO_SETTINGS": "Go to Settings",
|
||||||
"SWITCH_CONVERSATION_STATUS": "Switch to the next conversation status",
|
|
||||||
"SWITCH_TO_PRIVATE_NOTE": "Switch to Private Note",
|
"SWITCH_TO_PRIVATE_NOTE": "Switch to Private Note",
|
||||||
"SWITCH_TO_REPLY": "Switch to Reply",
|
"SWITCH_TO_REPLY": "Switch to Reply",
|
||||||
"TOGGLE_SNOOZE_DROPDOWN": "Toggle snooze dropdown"
|
"TOGGLE_SNOOZE_DROPDOWN": "Toggle snooze dropdown"
|
||||||
|
|||||||
@@ -10,7 +10,8 @@
|
|||||||
"TITLE": "Upgrade to create SLAs",
|
"TITLE": "Upgrade to create SLAs",
|
||||||
"AVAILABLE_ON": "The SLA feature is only available in the Business and Enterprise plans.",
|
"AVAILABLE_ON": "The SLA feature is only available in the Business and Enterprise plans.",
|
||||||
"UPGRADE_PROMPT": "Upgrade your plan to get access to advanced features like team management, automations, custom attributes, and more.",
|
"UPGRADE_PROMPT": "Upgrade your plan to get access to advanced features like team management, automations, custom attributes, and more.",
|
||||||
"UPGRADE_NOW": "Upgrade now"
|
"UPGRADE_NOW": "Upgrade now",
|
||||||
|
"CANCEL_ANYTIME": "You can change or cancel your plan anytime"
|
||||||
},
|
},
|
||||||
"ENTERPRISE_PAYWALL": {
|
"ENTERPRISE_PAYWALL": {
|
||||||
"AVAILABLE_ON": "The SLA feature is only available in the paid plans.",
|
"AVAILABLE_ON": "The SLA feature is only available in the paid plans.",
|
||||||
@@ -104,4 +105,4 @@
|
|||||||
"HIDE": "Hide {count} rows"
|
"HIDE": "Hide {count} rows"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -23,12 +23,12 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
|
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
|
||||||
import { hasPressedCommandAndEnter } from 'shared/helpers/KeyboardHelpers';
|
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
WootMessageEditor,
|
WootMessageEditor,
|
||||||
},
|
},
|
||||||
|
mixins: [keyboardEventListenerMixins],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
noteContent: '',
|
noteContent: '',
|
||||||
@@ -40,21 +40,14 @@ export default {
|
|||||||
return this.noteContent === '';
|
return this.noteContent === '';
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
|
||||||
document.addEventListener('keydown', this.onMetaEnter);
|
|
||||||
},
|
|
||||||
|
|
||||||
beforeDestroy() {
|
|
||||||
document.removeEventListener('keydown', this.onMetaEnter);
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
onMetaEnter(e) {
|
getKeyboardEvents() {
|
||||||
if (hasPressedCommandAndEnter(e)) {
|
return {
|
||||||
e.preventDefault();
|
'$mod+Enter': {
|
||||||
this.onAdd();
|
action: () => this.onAdd(),
|
||||||
}
|
allowOnFocusedInput: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
},
|
},
|
||||||
onAdd() {
|
onAdd() {
|
||||||
if (this.noteContent !== '') {
|
if (this.noteContent !== '') {
|
||||||
|
|||||||
@@ -23,6 +23,15 @@
|
|||||||
:key="element.name"
|
:key="element.name"
|
||||||
class="bg-white dark:bg-gray-800"
|
class="bg-white dark:bg-gray-800"
|
||||||
>
|
>
|
||||||
|
<div v-if="element.name === 'linear'" class="conversation--actions">
|
||||||
|
<accordion-item
|
||||||
|
title="Linear"
|
||||||
|
:is-open="isContactSidebarItemOpen('is_linear_open')"
|
||||||
|
@click="value => toggleSidebarUIState('is_linear_open', value)"
|
||||||
|
>
|
||||||
|
<linear :conversation-id="conversationId" />
|
||||||
|
</accordion-item>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="element.name === 'conversation_actions'"
|
v-if="element.name === 'conversation_actions'"
|
||||||
class="conversation--actions"
|
class="conversation--actions"
|
||||||
@@ -145,6 +154,7 @@ import CustomAttributes from './customAttributes/CustomAttributes.vue';
|
|||||||
import draggable from 'vuedraggable';
|
import draggable from 'vuedraggable';
|
||||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||||
import MacrosList from './Macros/List.vue';
|
import MacrosList from './Macros/List.vue';
|
||||||
|
import Linear from './linear/index.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@@ -157,6 +167,7 @@ export default {
|
|||||||
ConversationParticipant,
|
ConversationParticipant,
|
||||||
draggable,
|
draggable,
|
||||||
MacrosList,
|
MacrosList,
|
||||||
|
Linear,
|
||||||
},
|
},
|
||||||
mixins: [alertMixin, uiSettingsMixin],
|
mixins: [alertMixin, uiSettingsMixin],
|
||||||
props: {
|
props: {
|
||||||
|
|||||||
@@ -50,13 +50,8 @@ import LabelDropdown from 'shared/components/ui/label/LabelDropdown.vue';
|
|||||||
import AddLabel from 'shared/components/ui/dropdown/AddLabel.vue';
|
import AddLabel from 'shared/components/ui/dropdown/AddLabel.vue';
|
||||||
import { mixin as clickaway } from 'vue-clickaway';
|
import { mixin as clickaway } from 'vue-clickaway';
|
||||||
import adminMixin from 'dashboard/mixins/isAdmin';
|
import adminMixin from 'dashboard/mixins/isAdmin';
|
||||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||||
import conversationLabelMixin from 'dashboard/mixins/conversation/labelMixin';
|
import conversationLabelMixin from 'dashboard/mixins/conversation/labelMixin';
|
||||||
import {
|
|
||||||
buildHotKeys,
|
|
||||||
isEscape,
|
|
||||||
isActiveElementTypeable,
|
|
||||||
} from 'shared/helpers/KeyboardHelpers';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@@ -65,7 +60,12 @@ export default {
|
|||||||
AddLabel,
|
AddLabel,
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [clickaway, conversationLabelMixin, adminMixin, eventListenerMixins],
|
mixins: [
|
||||||
|
clickaway,
|
||||||
|
conversationLabelMixin,
|
||||||
|
adminMixin,
|
||||||
|
keyboardEventListenerMixins,
|
||||||
|
],
|
||||||
props: {
|
props: {
|
||||||
conversationId: {
|
conversationId: {
|
||||||
type: Number,
|
type: Number,
|
||||||
@@ -93,16 +93,23 @@ export default {
|
|||||||
closeDropdownLabel() {
|
closeDropdownLabel() {
|
||||||
this.showSearchDropdownLabel = false;
|
this.showSearchDropdownLabel = false;
|
||||||
},
|
},
|
||||||
handleKeyEvents(e) {
|
getKeyboardEvents() {
|
||||||
const keyPattern = buildHotKeys(e);
|
return {
|
||||||
|
KeyL: {
|
||||||
if (keyPattern === 'l' && !isActiveElementTypeable(e)) {
|
action: e => {
|
||||||
this.toggleLabels();
|
e.preventDefault();
|
||||||
e.preventDefault();
|
this.toggleLabels();
|
||||||
} else if (isEscape(e) && this.showSearchDropdownLabel) {
|
},
|
||||||
this.closeDropdownLabel();
|
},
|
||||||
e.preventDefault();
|
Escape: {
|
||||||
}
|
action: () => {
|
||||||
|
if (this.showSearchDropdownLabel) {
|
||||||
|
this.toggleLabels();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
allowOnFocusedInput: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,242 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col h-auto overflow-auto">
|
||||||
|
<woot-modal-header
|
||||||
|
header-title="Create/link linear issue"
|
||||||
|
header-content="Quickly create/link a linear issue from this conversation"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="flex flex-col h-auto overflow-auto">
|
||||||
|
<div class="flex flex-col px-8 pb-4" @submit.prevent="onSubmit">
|
||||||
|
<woot-input
|
||||||
|
v-model="title"
|
||||||
|
label="Title"
|
||||||
|
class="w-full"
|
||||||
|
placeholder="Enter title"
|
||||||
|
type="text"
|
||||||
|
@blur="$v.title.$touch"
|
||||||
|
/>
|
||||||
|
<label>
|
||||||
|
Description
|
||||||
|
<textarea
|
||||||
|
v-model="description"
|
||||||
|
rows="6"
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter description"
|
||||||
|
@blur="$v.description.$touch"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
Team
|
||||||
|
<select v-model="teamId" @change="onChangeTeam($event)">
|
||||||
|
<option v-for="item in teams" :key="item.name" :value="item.id">
|
||||||
|
{{ item.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Assignee
|
||||||
|
<select v-model="assigneeId">
|
||||||
|
<option v-for="item in assignees" :key="item.id" :value="item.id">
|
||||||
|
{{ item.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
Priority
|
||||||
|
<select v-model="priority">
|
||||||
|
<option v-for="item in priorities" :key="item.id" :value="item.id">
|
||||||
|
{{ item.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
Label
|
||||||
|
<select v-model="labelId">
|
||||||
|
<option v-for="item in labels" :key="item.id" :value="item.id">
|
||||||
|
{{ item.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
Status
|
||||||
|
<select v-model="stateId">
|
||||||
|
<option v-for="item in statuses" :key="item.id" :value="item.id">
|
||||||
|
{{ item.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-end w-full gap-2 mt-8">
|
||||||
|
<woot-button
|
||||||
|
class="px-4 rounded-xl button clear outline-woot-200/50 outline"
|
||||||
|
@click.prevent="onClose"
|
||||||
|
>
|
||||||
|
{{ $t('SLA.FORM.CANCEL') }}
|
||||||
|
</woot-button>
|
||||||
|
<woot-button
|
||||||
|
:is-disabled="isSubmitDisabled"
|
||||||
|
class="px-4 rounded-xl"
|
||||||
|
:is-loading="isCreating"
|
||||||
|
@click.prevent="createIssue"
|
||||||
|
>
|
||||||
|
Create
|
||||||
|
</woot-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
|
import { frontendURL, conversationUrl } from 'dashboard/helper/URLHelper.js';
|
||||||
|
import { LinearClient } from '@linear/sdk';
|
||||||
|
|
||||||
|
// const chatwootAPIKey = '';
|
||||||
|
const localAPIKey = '';
|
||||||
|
const linearClient = new LinearClient({
|
||||||
|
apiKey: localAPIKey,
|
||||||
|
});
|
||||||
|
// const BASE_URL = 'https:app.chatwoot.com';
|
||||||
|
const BASE_URL = 'http://localhost:3000';
|
||||||
|
|
||||||
|
import validations from './validations';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [alertMixin],
|
||||||
|
props: {
|
||||||
|
accountId: {
|
||||||
|
type: [Number, String],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
conversationId: {
|
||||||
|
type: [Number, String],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
title: '',
|
||||||
|
description: '',
|
||||||
|
teamId: '',
|
||||||
|
assigneeId: '',
|
||||||
|
stateId: '',
|
||||||
|
labelId: '',
|
||||||
|
priority: 0,
|
||||||
|
teams: [],
|
||||||
|
assignees: [],
|
||||||
|
labels: [],
|
||||||
|
statuses: [],
|
||||||
|
searchTerm: '',
|
||||||
|
priorities: [
|
||||||
|
{
|
||||||
|
id: 0,
|
||||||
|
name: 'No priority',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'Urgent',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: 'High',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: 'Normal',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
name: 'Low',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isCreating: false,
|
||||||
|
searchResults: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: 'Test',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
validations,
|
||||||
|
computed: {
|
||||||
|
isSubmitDisabled() {
|
||||||
|
return this.$v.title.$invalid || this.isCreating;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.getTeams();
|
||||||
|
this.getAssignees();
|
||||||
|
this.getLabels();
|
||||||
|
this.getWorkflowStates();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onClose() {
|
||||||
|
this.$emit('close');
|
||||||
|
},
|
||||||
|
onChangeTeam(event) {
|
||||||
|
this.teamId = event.target.value;
|
||||||
|
this.getLabels();
|
||||||
|
this.getWorkflowStates();
|
||||||
|
this.getAssignees();
|
||||||
|
},
|
||||||
|
async getTeams() {
|
||||||
|
const teams = await linearClient.teams();
|
||||||
|
this.teams = teams.nodes;
|
||||||
|
},
|
||||||
|
async getAssignees() {
|
||||||
|
const assignees = await linearClient.users();
|
||||||
|
this.assignees = assignees.nodes;
|
||||||
|
},
|
||||||
|
async getLabels() {
|
||||||
|
const labels = await linearClient.issueLabels({
|
||||||
|
teamId: this.teamId,
|
||||||
|
});
|
||||||
|
this.labels = labels.nodes;
|
||||||
|
},
|
||||||
|
async getWorkflowStates() {
|
||||||
|
const states = await linearClient.workflowStates();
|
||||||
|
this.statuses = states.nodes;
|
||||||
|
},
|
||||||
|
async createIssue() {
|
||||||
|
this.isCreating = true;
|
||||||
|
const response = await linearClient.createIssue({
|
||||||
|
teamId: this.teamId,
|
||||||
|
title: this.title,
|
||||||
|
description: this.description,
|
||||||
|
assigneeId: this.assigneeId,
|
||||||
|
priority: this.priority,
|
||||||
|
labelIds: this.labelId ? [this.labelId] : [],
|
||||||
|
});
|
||||||
|
const stringifyResponse = JSON.stringify(response);
|
||||||
|
const {
|
||||||
|
_issue: { id: issueId },
|
||||||
|
} = JSON.parse(stringifyResponse);
|
||||||
|
this.attachmentLinkURL(issueId);
|
||||||
|
this.showAlert('Linear issue created successfully');
|
||||||
|
},
|
||||||
|
async attachmentLinkURL(issueId) {
|
||||||
|
const url =
|
||||||
|
BASE_URL +
|
||||||
|
frontendURL(
|
||||||
|
conversationUrl({
|
||||||
|
accountId: this.accountId,
|
||||||
|
id: this.conversationId,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
await linearClient.attachmentLinkURL(issueId, url);
|
||||||
|
this.isCreating = false;
|
||||||
|
this.onClose();
|
||||||
|
},
|
||||||
|
|
||||||
|
openSearch() {
|
||||||
|
this.showSearchBox = true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div class="flex items-center justify-between group">
|
||||||
|
<!-- <woot-button
|
||||||
|
size="tiny"
|
||||||
|
variant="link"
|
||||||
|
color-scheme="secondary"
|
||||||
|
icon="delete"
|
||||||
|
@click="deleteIssue(issue)"
|
||||||
|
/> -->
|
||||||
|
<a
|
||||||
|
:href="issue.link"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="inline-block rounded-sm mb-0 break-all py-0.5 text-primary-600"
|
||||||
|
>
|
||||||
|
{{ `${issue.identifier} ${issue.title}` }}
|
||||||
|
</a>
|
||||||
|
<woot-button
|
||||||
|
size="small"
|
||||||
|
variant="clear"
|
||||||
|
color-scheme="secondary"
|
||||||
|
class="!px-2 hover:!bg-transparent hidden dark:hover:!bg-transparent underline group-hover:block"
|
||||||
|
@click="deleteIssue(issue)"
|
||||||
|
>
|
||||||
|
Unlink
|
||||||
|
</woot-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span class="text-sm font-normal text-slate-900 dark:text-slate-25">
|
||||||
|
{{ issue.description }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-between w-full">
|
||||||
|
<span
|
||||||
|
class="text-sm sticky top-0 h-fit font-normal tracking-[-0.6%] min-w-[140px] truncate text-slate-600 dark:text-slate-200"
|
||||||
|
>
|
||||||
|
Status
|
||||||
|
</span>
|
||||||
|
<div class="flex flex-col w-full gap-2">
|
||||||
|
<span
|
||||||
|
class="text-sm font-normal text-left text-slate-900 dark:text-slate-25 tabular-nums"
|
||||||
|
>
|
||||||
|
Todo
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-between w-full">
|
||||||
|
<span
|
||||||
|
class="text-sm sticky top-0 h-fit font-normal tracking-[-0.6%] min-w-[140px] truncate text-slate-600 dark:text-slate-200"
|
||||||
|
>
|
||||||
|
Priority
|
||||||
|
</span>
|
||||||
|
<div class="flex flex-col w-full gap-2">
|
||||||
|
<span
|
||||||
|
class="text-sm font-normal text-left text-slate-900 dark:text-slate-25 tabular-nums"
|
||||||
|
>
|
||||||
|
{{ issue.priorityLabel }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between w-full">
|
||||||
|
<span
|
||||||
|
class="text-sm sticky top-0 h-fit font-normal tracking-[-0.6%] min-w-[140px] truncate text-slate-600 dark:text-slate-200"
|
||||||
|
>
|
||||||
|
Assignee
|
||||||
|
</span>
|
||||||
|
<div class="flex flex-col w-full gap-2">
|
||||||
|
<span
|
||||||
|
class="text-sm font-normal text-left text-slate-900 dark:text-slate-25 tabular-nums"
|
||||||
|
>
|
||||||
|
Muhsin
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between w-full">
|
||||||
|
<span
|
||||||
|
class="text-sm sticky top-0 h-fit font-normal tracking-[-0.6%] min-w-[140px] truncate text-slate-600 dark:text-slate-200"
|
||||||
|
>
|
||||||
|
Labels
|
||||||
|
</span>
|
||||||
|
<div class="flex flex-col w-full gap-2">
|
||||||
|
<span
|
||||||
|
class="text-sm font-normal text-left text-slate-900 dark:text-slate-25 tabular-nums"
|
||||||
|
>
|
||||||
|
---
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between w-full">
|
||||||
|
<span
|
||||||
|
class="text-sm sticky top-0 h-fit font-normal tracking-[-0.6%] min-w-[140px] truncate text-slate-600 dark:text-slate-200"
|
||||||
|
>
|
||||||
|
Created At
|
||||||
|
</span>
|
||||||
|
<div class="flex flex-col w-full gap-2">
|
||||||
|
<span
|
||||||
|
class="text-sm font-normal text-left text-slate-900 dark:text-slate-25 tabular-nums"
|
||||||
|
>
|
||||||
|
{{ formatDate(issue.createdAt) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { format } from 'date-fns';
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
issue: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
deleteIssue(issue) {
|
||||||
|
this.$emit('delete', issue);
|
||||||
|
},
|
||||||
|
formatDate(timestamp) {
|
||||||
|
return format(timestamp, 'MMM dd, hh:mm a');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<div class="flex flex-row gap-2">
|
||||||
|
<woot-button
|
||||||
|
v-if="!isLoading && !issues.length"
|
||||||
|
variant="smooth"
|
||||||
|
icon="add"
|
||||||
|
size="tiny"
|
||||||
|
@click="shoeCreateIssuePopup"
|
||||||
|
>
|
||||||
|
Create or Link Issue
|
||||||
|
</woot-button>
|
||||||
|
</div>
|
||||||
|
<div v-if="isLoading" class="flex items-center justify-center">
|
||||||
|
<span class="text-sm font-medium text-slate-800 dark:text-slate-100">
|
||||||
|
Loading..
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="!isLoading && issues.length" class="flex flex-col gap-2">
|
||||||
|
<issue-item
|
||||||
|
v-for="issue in issues"
|
||||||
|
:key="issue.id"
|
||||||
|
:issue="issue"
|
||||||
|
:conversation-id="conversationId"
|
||||||
|
@delete="deleteIssue"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<woot-modal :show.sync="showAddPopup" :on-close="hideAddPopup">
|
||||||
|
<add-issue
|
||||||
|
:conversation-id="conversationId"
|
||||||
|
:account-id="currentAccountId"
|
||||||
|
@close="hideAddPopup"
|
||||||
|
/>
|
||||||
|
</woot-modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import { LinearClient } from '@linear/sdk';
|
||||||
|
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import IssueItem from './IssueItem.vue';
|
||||||
|
import accountMixin from 'dashboard/mixins/account.js';
|
||||||
|
import AddIssue from './AddIssue.vue';
|
||||||
|
|
||||||
|
import { frontendURL, conversationUrl } from 'dashboard/helper/URLHelper.js';
|
||||||
|
|
||||||
|
// const chatwootAPIKey = '';
|
||||||
|
const localAPIKey = '';
|
||||||
|
const linearClient = new LinearClient({
|
||||||
|
apiKey: localAPIKey,
|
||||||
|
});
|
||||||
|
// const BASE_URL = 'https:app.chatwoot.com';
|
||||||
|
const BASE_URL = 'http://localhost:3000';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
IssueItem,
|
||||||
|
AddIssue,
|
||||||
|
},
|
||||||
|
mixins: [accountMixin],
|
||||||
|
props: {
|
||||||
|
conversationId: {
|
||||||
|
type: [Number, String],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isLoading: false,
|
||||||
|
showAddPopup: false,
|
||||||
|
issues: [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters({
|
||||||
|
currentAccountId: 'getCurrentAccountId',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.loadIssues();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
shoeCreateIssuePopup() {
|
||||||
|
this.showAddPopup = true;
|
||||||
|
},
|
||||||
|
hideAddPopup() {
|
||||||
|
this.showAddPopup = false;
|
||||||
|
this.loadIssues();
|
||||||
|
},
|
||||||
|
async loadIssues() {
|
||||||
|
const url =
|
||||||
|
BASE_URL +
|
||||||
|
frontendURL(
|
||||||
|
conversationUrl({
|
||||||
|
accountId: this.accountId,
|
||||||
|
id: this.conversationId,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
this.isLoading = true;
|
||||||
|
linearClient.attachmentsForURL(url).then(issues => {
|
||||||
|
if (issues.nodes.length) {
|
||||||
|
issues.nodes.map(issue1 => {
|
||||||
|
const linearIssueId = issue1._issue.id;
|
||||||
|
const attachmentId = issue1.id;
|
||||||
|
if (linearIssueId) {
|
||||||
|
linearClient.issue(linearIssueId).then(issue2 => {
|
||||||
|
console.log('Issue:', issue2, attachmentId);
|
||||||
|
this.issues.push({
|
||||||
|
id: issue2.id,
|
||||||
|
title: issue2.title,
|
||||||
|
link: issue2.url,
|
||||||
|
identifier: issue2.identifier,
|
||||||
|
attachmentId,
|
||||||
|
...issue2,
|
||||||
|
});
|
||||||
|
this.isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.isLoading = false;
|
||||||
|
console.log('No issues');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async deleteIssue(issue) {
|
||||||
|
const index = this.issues.findIndex(i => i.id === issue.id);
|
||||||
|
if (index > -1) {
|
||||||
|
this.issues.splice(index, 1);
|
||||||
|
}
|
||||||
|
await linearClient.deleteAttachment(issue.attachmentId);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.macros_list--empty-state {
|
||||||
|
padding: var(--space-slab);
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.macros_add-button {
|
||||||
|
margin: var(--space-small) auto 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { required } from 'vuelidate/lib/validators';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: {
|
||||||
|
required,
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
required,
|
||||||
|
},
|
||||||
|
teamId: {},
|
||||||
|
assigneeId: {},
|
||||||
|
};
|
||||||
+11
-13
@@ -34,15 +34,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||||
import {
|
|
||||||
buildHotKeys,
|
|
||||||
isActiveElementTypeable,
|
|
||||||
} from 'shared/helpers/KeyboardHelpers';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ChatwootSearch',
|
name: 'ChatwootSearch',
|
||||||
mixins: [eventListenerMixins],
|
mixins: [keyboardEventListenerMixins],
|
||||||
props: {
|
props: {
|
||||||
title: {
|
title: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -71,13 +67,15 @@ export default {
|
|||||||
onBlur() {
|
onBlur() {
|
||||||
this.isInputFocused = false;
|
this.isInputFocused = false;
|
||||||
},
|
},
|
||||||
handleKeyEvents(e) {
|
getKeyboardEvents() {
|
||||||
const keyPattern = buildHotKeys(e);
|
return {
|
||||||
|
Slash: {
|
||||||
if (keyPattern === '/' && !isActiveElementTypeable(e)) {
|
action: e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.$refs.searchInput.focus();
|
this.$refs.searchInput.focus();
|
||||||
}
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
+11
-14
@@ -35,10 +35,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import { debounce } from '@chatwoot/utils';
|
import { debounce } from '@chatwoot/utils';
|
||||||
import { mixin as clickaway } from 'vue-clickaway';
|
import { mixin as clickaway } from 'vue-clickaway';
|
||||||
import {
|
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||||
isEscape,
|
|
||||||
isActiveElementTypeable,
|
|
||||||
} from 'shared/helpers/KeyboardHelpers';
|
|
||||||
|
|
||||||
import SearchHeader from './Header.vue';
|
import SearchHeader from './Header.vue';
|
||||||
import SearchResults from './SearchResults.vue';
|
import SearchResults from './SearchResults.vue';
|
||||||
@@ -55,7 +52,7 @@ export default {
|
|||||||
SearchResults,
|
SearchResults,
|
||||||
ArticleView,
|
ArticleView,
|
||||||
},
|
},
|
||||||
mixins: [clickaway, portalMixin, alertMixin],
|
mixins: [clickaway, portalMixin, alertMixin, keyboardEventListenerMixins],
|
||||||
props: {
|
props: {
|
||||||
selectedPortalSlug: {
|
selectedPortalSlug: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -97,10 +94,6 @@ export default {
|
|||||||
mounted() {
|
mounted() {
|
||||||
this.fetchArticlesByQuery(this.searchQuery);
|
this.fetchArticlesByQuery(this.searchQuery);
|
||||||
this.debounceSearch = debounce(this.fetchArticlesByQuery, 500, false);
|
this.debounceSearch = debounce(this.fetchArticlesByQuery, 500, false);
|
||||||
document.body.addEventListener('keydown', this.closeOnEsc);
|
|
||||||
},
|
|
||||||
beforeDestroy() {
|
|
||||||
document.body.removeEventListener('keydown', this.closeOnEsc);
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
generateArticleUrl(article) {
|
generateArticleUrl(article) {
|
||||||
@@ -158,11 +151,15 @@ export default {
|
|||||||
);
|
);
|
||||||
this.onClose();
|
this.onClose();
|
||||||
},
|
},
|
||||||
closeOnEsc(e) {
|
getKeyboardEvents() {
|
||||||
if (isEscape(e) && !isActiveElementTypeable(e)) {
|
return {
|
||||||
e.preventDefault();
|
Escape: {
|
||||||
this.onClose();
|
action: () => {
|
||||||
}
|
this.onClose();
|
||||||
|
},
|
||||||
|
allowOnFocusedInput: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
+139
-274
@@ -1,308 +1,173 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
import format from 'date-fns/format';
|
||||||
|
import getDay from 'date-fns/getDay';
|
||||||
|
|
||||||
|
import { getQuantileIntervals } from '@chatwoot/utils';
|
||||||
|
|
||||||
|
import { groupHeatmapByDay } from 'helpers/ReportsDataHelper';
|
||||||
|
import { useI18n } from 'dashboard/composables/useI18n';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const props = defineProps({
|
||||||
|
heatData: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
isLoading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const processedData = computed(() => {
|
||||||
|
return groupHeatmapByDay(props.heatData);
|
||||||
|
});
|
||||||
|
|
||||||
|
const quantileRange = computed(() => {
|
||||||
|
const flattendedData = props.heatData.map(data => data.value);
|
||||||
|
return getQuantileIntervals(flattendedData, [0.2, 0.4, 0.6, 0.8, 0.9, 0.99]);
|
||||||
|
});
|
||||||
|
|
||||||
|
function getCountTooltip(value) {
|
||||||
|
if (!value) {
|
||||||
|
return t('OVERVIEW_REPORTS.CONVERSATION_HEATMAP.NO_CONVERSATIONS');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value === 1) {
|
||||||
|
return t('OVERVIEW_REPORTS.CONVERSATION_HEATMAP.CONVERSATION', {
|
||||||
|
count: value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return t('OVERVIEW_REPORTS.CONVERSATION_HEATMAP.CONVERSATIONS', {
|
||||||
|
count: value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(dateString) {
|
||||||
|
return format(new Date(dateString), 'MMM d, yyyy');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDayOfTheWeek(date) {
|
||||||
|
const dayIndex = getDay(date);
|
||||||
|
const days = [
|
||||||
|
t('DAYS_OF_WEEK.SUNDAY'),
|
||||||
|
t('DAYS_OF_WEEK.MONDAY'),
|
||||||
|
t('DAYS_OF_WEEK.TUESDAY'),
|
||||||
|
t('DAYS_OF_WEEK.WEDNESDAY'),
|
||||||
|
t('DAYS_OF_WEEK.THURSDAY'),
|
||||||
|
t('DAYS_OF_WEEK.FRIDAY'),
|
||||||
|
t('DAYS_OF_WEEK.SATURDAY'),
|
||||||
|
];
|
||||||
|
return days[dayIndex];
|
||||||
|
}
|
||||||
|
function getHeatmapLevelClass(value) {
|
||||||
|
if (!value)
|
||||||
|
return 'outline-slate-100 dark:outline-slate-700 dark:bg-slate-700/40 bg-slate-50/50';
|
||||||
|
|
||||||
|
let level = [...quantileRange.value, Infinity].findIndex(
|
||||||
|
range => value <= range && value > 0
|
||||||
|
);
|
||||||
|
|
||||||
|
if (level > 6) level = 5;
|
||||||
|
|
||||||
|
if (level === 0) {
|
||||||
|
return 'outline-slate-100 dark:outline-slate-700 dark:bg-slate-700/40 bg-slate-50/50';
|
||||||
|
}
|
||||||
|
|
||||||
|
const classes = [
|
||||||
|
'bg-woot-50 dark:bg-woot-800/40 dark:outline-woot-800/80',
|
||||||
|
'bg-woot-100 dark:bg-woot-800/30 dark:outline-woot-800/80',
|
||||||
|
'bg-woot-200 dark:bg-woot-500/40 dark:outline-woot-700/80',
|
||||||
|
'bg-woot-300 dark:bg-woot-500/60 dark:outline-woot-600/80',
|
||||||
|
'bg-woot-600 dark:bg-woot-500/80 dark:outline-woot-500/80',
|
||||||
|
'bg-woot-800 dark:bg-woot-500 dark:outline-woot-400/80',
|
||||||
|
];
|
||||||
|
|
||||||
|
return classes[level - 1];
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="heatmap-container">
|
<div
|
||||||
|
class="grid relative w-full gap-x-4 gap-y-2.5 overflow-y-scroll md:overflow-visible grid-cols-[80px_1fr] min-h-72"
|
||||||
|
>
|
||||||
<template v-if="isLoading">
|
<template v-if="isLoading">
|
||||||
<div class="heatmap-labels">
|
<div class="grid gap-[5px] flex-shrink-0">
|
||||||
<div
|
<div
|
||||||
v-for="ii in 7"
|
v-for="ii in 7"
|
||||||
:key="ii"
|
:key="ii"
|
||||||
class="loading-cell heatmap-axis-label"
|
class="w-full rounded-sm bg-slate-100 dark:bg-slate-900 animate-loader-pulse h-8 min-w-[70px]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="heatmap-grid">
|
<div class="grid gap-[5px] w-full min-w-[700px]">
|
||||||
<div v-for="ii in 7" :key="ii" class="heatmap-grid-row">
|
<div
|
||||||
<div v-for="jj in 24" :key="jj" class="heatmap-tile loading-cell">
|
v-for="ii in 7"
|
||||||
<div class="heatmap-tile__label loading-cell" />
|
:key="ii"
|
||||||
</div>
|
class="grid gap-[5px] grid-cols-[repeat(24,_1fr)]"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="jj in 24"
|
||||||
|
:key="jj"
|
||||||
|
class="w-full h-8 rounded-sm bg-slate-100 dark:bg-slate-900 animate-loader-pulse"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="heatmap-timeline" />
|
<div />
|
||||||
<div class="heatmap-markers">
|
<div
|
||||||
<div v-for="ii in 24" :key="ii">{{ ii - 1 }} – {{ ii }}</div>
|
class="grid grid-cols-[repeat(24,_1fr)] gap-[5px] w-full text-[8px] font-semibold h-5 text-slate-800 dark:text-slate-200"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="ii in 24"
|
||||||
|
:key="ii"
|
||||||
|
class="flex items-center justify-center"
|
||||||
|
>
|
||||||
|
{{ ii - 1 }} – {{ ii }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="heatmap-labels">
|
<div class="grid gap-[5px] flex-shrink-0">
|
||||||
<div
|
<div
|
||||||
v-for="dateKey in processedData.keys()"
|
v-for="dateKey in processedData.keys()"
|
||||||
:key="dateKey"
|
:key="dateKey"
|
||||||
class="heatmap-axis-label"
|
class="h-8 min-w-[70px] text-slate-800 dark:text-slate-200 text-[10px] font-semibold flex flex-col items-end justify-center"
|
||||||
>
|
>
|
||||||
{{ getDayOfTheWeek(new Date(dateKey)) }}
|
{{ getDayOfTheWeek(new Date(dateKey)) }}
|
||||||
<time>{{ formatDate(dateKey) }}</time>
|
<time class="font-normal text-slate-700 dark:text-slate-200">
|
||||||
|
{{ formatDate(dateKey) }}
|
||||||
|
</time>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="heatmap-grid">
|
<div class="grid gap-[5px] w-full min-w-[700px]">
|
||||||
<div
|
<div
|
||||||
v-for="dateKey in processedData.keys()"
|
v-for="dateKey in processedData.keys()"
|
||||||
:key="dateKey"
|
:key="dateKey"
|
||||||
class="heatmap-grid-row"
|
class="grid gap-[5px] grid-cols-[repeat(24,_1fr)]"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="data in processedData.get(dateKey)"
|
v-for="data in processedData.get(dateKey)"
|
||||||
:key="data.timestamp"
|
:key="data.timestamp"
|
||||||
v-tooltip.top="getCountTooltip(data.value)"
|
v-tooltip.top="getCountTooltip(data.value)"
|
||||||
class="heatmap-tile"
|
class="h-8 rounded-sm shadow-inner dark:outline dark:outline-1 shadow-black"
|
||||||
:class="getHeatmapLevelClass(data.value)"
|
:class="getHeatmapLevelClass(data.value)"
|
||||||
>
|
/>
|
||||||
<div class="heatmap-tile__label" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="heatmap-timeline" />
|
<div />
|
||||||
<div class="heatmap-markers">
|
<div
|
||||||
<div v-for="ii in 24" :key="ii">{{ ii - 1 }} – {{ ii }}</div>
|
class="grid grid-cols-[repeat(24,_1fr)] gap-[5px] w-full text-[8px] font-semibold h-5 text-slate-800 dark:text-slate-200"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="ii in 24"
|
||||||
|
:key="ii"
|
||||||
|
class="flex items-center justify-center"
|
||||||
|
>
|
||||||
|
{{ ii - 1 }} – {{ ii }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
|
||||||
import { getQuantileIntervals } from '@chatwoot/utils';
|
|
||||||
import format from 'date-fns/format';
|
|
||||||
import getDay from 'date-fns/getDay';
|
|
||||||
|
|
||||||
import { groupHeatmapByDay } from 'helpers/ReportsDataHelper';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'Heatmap',
|
|
||||||
props: {
|
|
||||||
heatData: {
|
|
||||||
type: Array,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
isLoading: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
processedData() {
|
|
||||||
return groupHeatmapByDay(this.heatData);
|
|
||||||
},
|
|
||||||
quantileRange() {
|
|
||||||
const flattendedData = this.heatData.map(data => data.value);
|
|
||||||
return getQuantileIntervals(
|
|
||||||
flattendedData,
|
|
||||||
[0.2, 0.4, 0.6, 0.8, 0.9, 0.99]
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
getCountTooltip(value) {
|
|
||||||
if (!value) {
|
|
||||||
return this.$t(
|
|
||||||
'OVERVIEW_REPORTS.CONVERSATION_HEATMAP.NO_CONVERSATIONS'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value === 1) {
|
|
||||||
return this.$t('OVERVIEW_REPORTS.CONVERSATION_HEATMAP.CONVERSATION', {
|
|
||||||
count: value,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.$t('OVERVIEW_REPORTS.CONVERSATION_HEATMAP.CONVERSATIONS', {
|
|
||||||
count: value,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
formatDate(dateString) {
|
|
||||||
return format(new Date(dateString), 'MMM d, yyyy');
|
|
||||||
},
|
|
||||||
getDayOfTheWeek(date) {
|
|
||||||
const dayIndex = getDay(date);
|
|
||||||
const days = [
|
|
||||||
this.$t('DAYS_OF_WEEK.SUNDAY'),
|
|
||||||
this.$t('DAYS_OF_WEEK.MONDAY'),
|
|
||||||
this.$t('DAYS_OF_WEEK.TUESDAY'),
|
|
||||||
this.$t('DAYS_OF_WEEK.WEDNESDAY'),
|
|
||||||
this.$t('DAYS_OF_WEEK.THURSDAY'),
|
|
||||||
this.$t('DAYS_OF_WEEK.FRIDAY'),
|
|
||||||
this.$t('DAYS_OF_WEEK.SATURDAY'),
|
|
||||||
];
|
|
||||||
return days[dayIndex];
|
|
||||||
},
|
|
||||||
getHeatmapLevelClass(value) {
|
|
||||||
if (!value) return '';
|
|
||||||
|
|
||||||
const level = [...this.quantileRange, Infinity].findIndex(
|
|
||||||
range => value <= range && value > 0
|
|
||||||
);
|
|
||||||
|
|
||||||
if (level > 6) {
|
|
||||||
return 'l6';
|
|
||||||
}
|
|
||||||
|
|
||||||
return `l${level}`;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
$heatmap-colors: (
|
|
||||||
level-1: var(--w-50),
|
|
||||||
level-2: var(--w-100),
|
|
||||||
level-3: var(--w-300),
|
|
||||||
level-4: var(--w-500),
|
|
||||||
level-5: var(--w-700),
|
|
||||||
level-6: var(--w-900),
|
|
||||||
);
|
|
||||||
|
|
||||||
$heatmap-hover-border-color: (
|
|
||||||
level-1: var(--w-25),
|
|
||||||
level-2: var(--w-50),
|
|
||||||
level-3: var(--w-100),
|
|
||||||
level-4: var(--w-300),
|
|
||||||
level-5: var(--w-500),
|
|
||||||
level-6: var(--w-700),
|
|
||||||
);
|
|
||||||
|
|
||||||
$tile-height: 1.875rem;
|
|
||||||
$tile-gap: var(--space-smaller);
|
|
||||||
$container-gap-row: var(--space-one);
|
|
||||||
$container-gap-column: var(--space-two);
|
|
||||||
$marker-height: var(--space-two);
|
|
||||||
|
|
||||||
@mixin heatmap-level($level) {
|
|
||||||
$color: map-get($heatmap-colors, 'level-#{$level}');
|
|
||||||
background-color: $color;
|
|
||||||
&:hover {
|
|
||||||
border: 1px solid map-get($heatmap-hover-border-color, 'level-#{$level}');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 768px) {
|
|
||||||
.heatmap-container {
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-cell {
|
|
||||||
background-color: var(--color-background-light);
|
|
||||||
border: 0px;
|
|
||||||
|
|
||||||
animation: loading-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes loading-pulse {
|
|
||||||
0%,
|
|
||||||
100% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.heatmap-container {
|
|
||||||
display: grid;
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
gap: $container-gap-row $container-gap-column;
|
|
||||||
grid-template-columns: 80px 1fr;
|
|
||||||
min-height: calc(
|
|
||||||
7 * #{$tile-height} + 6 * #{$tile-gap} + #{$container-gap-row} + #{$marker-height}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
.heatmap-labels {
|
|
||||||
display: grid;
|
|
||||||
grid-template-rows: 1fr;
|
|
||||||
gap: $tile-gap;
|
|
||||||
flex-shrink: 0;
|
|
||||||
|
|
||||||
.heatmap-axis-label {
|
|
||||||
height: $tile-height;
|
|
||||||
min-width: 70px;
|
|
||||||
font-size: var(--font-size-micro);
|
|
||||||
font-weight: var(--font-weight-bold);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-end;
|
|
||||||
justify-content: center;
|
|
||||||
@apply text-slate-800 dark:text-slate-200;
|
|
||||||
|
|
||||||
time {
|
|
||||||
font-size: var(--font-size-micro);
|
|
||||||
font-weight: var(--font-weight-normal);
|
|
||||||
@apply text-slate-700 dark:text-slate-200;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.heatmap-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-rows: 1fr;
|
|
||||||
gap: $tile-gap;
|
|
||||||
min-width: 700px;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
.heatmap-grid-row {
|
|
||||||
display: grid;
|
|
||||||
gap: $tile-gap;
|
|
||||||
grid-template-columns: repeat(24, 1fr);
|
|
||||||
}
|
|
||||||
|
|
||||||
.heatmap-tile {
|
|
||||||
width: auto;
|
|
||||||
height: $tile-height;
|
|
||||||
border-radius: var(--border-radius-normal);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
box-shadow: var(--shadow-large);
|
|
||||||
|
|
||||||
transform: translateY(-2px);
|
|
||||||
transition:
|
|
||||||
transform 0.2s ease-in-out,
|
|
||||||
box-shadow 0.2s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:not(.l1):not(.l2):not(.l3):not(.l4):not(.l5):not(.l6) {
|
|
||||||
background-color: var(--color-background-light);
|
|
||||||
border: 1px solid var(--color-border-light);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
transform: translateY(0);
|
|
||||||
box-shadow: none;
|
|
||||||
border: 1px solid var(--color-border-light);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.l1 {
|
|
||||||
@include heatmap-level(1);
|
|
||||||
}
|
|
||||||
&.l2 {
|
|
||||||
@include heatmap-level(2);
|
|
||||||
}
|
|
||||||
&.l3 {
|
|
||||||
@include heatmap-level(3);
|
|
||||||
}
|
|
||||||
&.l4 {
|
|
||||||
@include heatmap-level(4);
|
|
||||||
}
|
|
||||||
&.l5 {
|
|
||||||
@include heatmap-level(5);
|
|
||||||
}
|
|
||||||
&.l6 {
|
|
||||||
@include heatmap-level(6);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.heatmap-markers {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(24, 1fr);
|
|
||||||
gap: $tile-gap;
|
|
||||||
width: 100%;
|
|
||||||
font-size: var(--font-size-nano);
|
|
||||||
font-weight: var(--font-weight-bold);
|
|
||||||
height: $marker-height;
|
|
||||||
color: var(--color-body);
|
|
||||||
@apply text-slate-800 dark:text-slate-200;
|
|
||||||
|
|
||||||
div {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
+5
-2
@@ -45,7 +45,7 @@ const i18nKey = props.isOnChatwootCloud ? 'PAYWALL' : 'ENTERPRISE_PAYWALL';
|
|||||||
{{ $t('SLA.ENTERPRISE_PAYWALL.ASK_ADMIN') }}
|
{{ $t('SLA.ENTERPRISE_PAYWALL.ASK_ADMIN') }}
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
<template v-if="isOnChatwootCloud">
|
<template v-if="isOnChatwootCloud || true">
|
||||||
<woot-button
|
<woot-button
|
||||||
color-scheme="primary"
|
color-scheme="primary"
|
||||||
class="w-full mt-2 text-center rounded-xl"
|
class="w-full mt-2 text-center rounded-xl"
|
||||||
@@ -55,8 +55,11 @@ const i18nKey = props.isOnChatwootCloud ? 'PAYWALL' : 'ENTERPRISE_PAYWALL';
|
|||||||
>
|
>
|
||||||
{{ $t('SLA.PAYWALL.UPGRADE_NOW') }}
|
{{ $t('SLA.PAYWALL.UPGRADE_NOW') }}
|
||||||
</woot-button>
|
</woot-button>
|
||||||
|
<span class="mt-2 text-xs tracking-tight text-center">
|
||||||
|
{{ $t('SLA.PAYWALL.CANCEL_ANYTIME') }}
|
||||||
|
</span>
|
||||||
</template>
|
</template>
|
||||||
<template v-if="isSuperAdmin">
|
<template v-else-if="isSuperAdmin">
|
||||||
<a href="/super_admin" class="block w-full">
|
<a href="/super_admin" class="block w-full">
|
||||||
<woot-button
|
<woot-button
|
||||||
color-scheme="primary"
|
color-scheme="primary"
|
||||||
|
|||||||
@@ -46,26 +46,8 @@ import webhooks from './modules/webhooks';
|
|||||||
import draftMessages from './modules/draftMessages';
|
import draftMessages from './modules/draftMessages';
|
||||||
import SLAReports from './modules/SLAReports';
|
import SLAReports from './modules/SLAReports';
|
||||||
|
|
||||||
import LogRocket from 'logrocket';
|
|
||||||
import createPlugin from 'logrocket-vuex';
|
|
||||||
|
|
||||||
const plugins = [];
|
const plugins = [];
|
||||||
|
|
||||||
if (window.logRocketProjectId) {
|
|
||||||
LogRocket.init(window.logRocketProjectId);
|
|
||||||
// eslint-disable-next-line func-names
|
|
||||||
const logRocketPlugin = createPlugin(LogRocket, function (mutation) {
|
|
||||||
const eventsToIgnore = ['SET_CURRENT_USER', 'AUTHENTICATE', 'CLEAR_USER'];
|
|
||||||
if (eventsToIgnore.includes(mutation.type)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return mutation;
|
|
||||||
});
|
|
||||||
|
|
||||||
plugins.push(logRocketPlugin);
|
|
||||||
}
|
|
||||||
|
|
||||||
Vue.use(Vuex);
|
Vue.use(Vuex);
|
||||||
export default new Vuex.Store({
|
export default new Vuex.Store({
|
||||||
modules: {
|
modules: {
|
||||||
|
|||||||
@@ -109,9 +109,10 @@ export default {
|
|||||||
generateArticleUrl(article) {
|
generateArticleUrl(article) {
|
||||||
return `/hc/${article.portal.slug}/articles/${article.slug}`;
|
return `/hc/${article.portal.slug}/articles/${article.slug}`;
|
||||||
},
|
},
|
||||||
handleKeyboardEvent(e) {
|
adjustScroll() {
|
||||||
this.processKeyDownEvent(e);
|
this.$nextTick(() => {
|
||||||
this.$el.scrollTop = 102 * this.selectedIndex;
|
this.$el.scrollTop = 102 * this.selectedIndex;
|
||||||
|
});
|
||||||
},
|
},
|
||||||
prepareContent(content) {
|
prepareContent(content) {
|
||||||
return this.highlightContent(
|
return this.highlightContent(
|
||||||
|
|||||||
@@ -8,16 +8,12 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||||
import {
|
|
||||||
hasPressedArrowUpKey,
|
|
||||||
hasPressedArrowDownKey,
|
|
||||||
} from 'shared/helpers/KeyboardHelpers';
|
|
||||||
export default {
|
export default {
|
||||||
name: 'WootDropdownMenu',
|
name: 'WootDropdownMenu',
|
||||||
componentName: 'WootDropdownMenu',
|
componentName: 'WootDropdownMenu',
|
||||||
|
|
||||||
mixins: [eventListenerMixins],
|
mixins: [keyboardEventListenerMixins],
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
placement: {
|
placement: {
|
||||||
@@ -31,38 +27,46 @@ export default {
|
|||||||
'ul.dropdown li.dropdown-menu__item .button'
|
'ul.dropdown li.dropdown-menu__item .button'
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
activeElementIndex() {
|
getActiveButtonIndex(menuButtons) {
|
||||||
const menuButtons = this.dropdownMenuButtons();
|
|
||||||
const focusedButton = this.$refs.dropdownMenu.querySelector(
|
const focusedButton = this.$refs.dropdownMenu.querySelector(
|
||||||
'ul.dropdown li.dropdown-menu__item .button:focus'
|
'ul.dropdown li.dropdown-menu__item .button:focus'
|
||||||
);
|
);
|
||||||
const activeIndex = [...menuButtons].indexOf(focusedButton);
|
return Array.from(menuButtons).indexOf(focusedButton);
|
||||||
return activeIndex;
|
|
||||||
},
|
},
|
||||||
handleKeyEvents(e) {
|
getKeyboardEvents() {
|
||||||
const menuButtons = this.dropdownMenuButtons();
|
const menuButtons = this.dropdownMenuButtons();
|
||||||
const lastElementIndex = menuButtons.length - 1;
|
return {
|
||||||
|
ArrowUp: {
|
||||||
|
action: e => {
|
||||||
|
e.preventDefault();
|
||||||
|
this.focusPreviousButton(menuButtons);
|
||||||
|
},
|
||||||
|
allowOnFocusedInput: true,
|
||||||
|
},
|
||||||
|
ArrowDown: {
|
||||||
|
action: e => {
|
||||||
|
e.preventDefault();
|
||||||
|
this.focusNextButton(menuButtons);
|
||||||
|
},
|
||||||
|
allowOnFocusedInput: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
focusPreviousButton(menuButtons) {
|
||||||
|
const activeIndex = this.getActiveButtonIndex(menuButtons);
|
||||||
|
const newIndex =
|
||||||
|
activeIndex >= 1 ? activeIndex - 1 : menuButtons.length - 1;
|
||||||
|
this.focusButton(menuButtons, newIndex);
|
||||||
|
},
|
||||||
|
focusNextButton(menuButtons) {
|
||||||
|
const activeIndex = this.getActiveButtonIndex(menuButtons);
|
||||||
|
const newIndex =
|
||||||
|
activeIndex === menuButtons.length - 1 ? 0 : activeIndex + 1;
|
||||||
|
this.focusButton(menuButtons, newIndex);
|
||||||
|
},
|
||||||
|
focusButton(menuButtons, newIndex) {
|
||||||
if (menuButtons.length === 0) return;
|
if (menuButtons.length === 0) return;
|
||||||
|
menuButtons[newIndex].focus();
|
||||||
if (hasPressedArrowUpKey(e)) {
|
|
||||||
const activeIndex = this.activeElementIndex();
|
|
||||||
|
|
||||||
if (activeIndex >= 1) {
|
|
||||||
menuButtons[activeIndex - 1].focus();
|
|
||||||
} else {
|
|
||||||
menuButtons[lastElementIndex].focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (hasPressedArrowDownKey(e)) {
|
|
||||||
const activeIndex = this.activeElementIndex();
|
|
||||||
|
|
||||||
if (activeIndex === lastElementIndex) {
|
|
||||||
menuButtons[0].focus();
|
|
||||||
} else {
|
|
||||||
menuButtons[activeIndex + 1].focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
export const isEnter = e => {
|
export const isEnter = e => {
|
||||||
return e.keyCode === 13;
|
return e.key === 'Enter';
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isEscape = e => {
|
export const isEscape = e => {
|
||||||
return e.keyCode === 27;
|
return e.key === 'Escape';
|
||||||
};
|
};
|
||||||
|
|
||||||
export const hasPressedShift = e => {
|
export const hasPressedShift = e => {
|
||||||
@@ -19,118 +19,7 @@ export const hasPressedEnterAndNotCmdOrShift = e => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const hasPressedCommandAndEnter = e => {
|
export const hasPressedCommandAndEnter = e => {
|
||||||
return e.metaKey && e.keyCode === 13;
|
return hasPressedCommand(e) && isEnter(e);
|
||||||
};
|
|
||||||
|
|
||||||
export const hasPressedCommandAndForwardSlash = e => {
|
|
||||||
return e.metaKey && e.keyCode === 191;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const hasPressedAltAndCKey = e => {
|
|
||||||
return e.altKey && e.keyCode === 67;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const hasPressedAltAndVKey = e => {
|
|
||||||
return e.altKey && e.keyCode === 86;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const hasPressedAltAndRKey = e => {
|
|
||||||
return e.altKey && e.keyCode === 82;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const hasPressedAltAndSKey = e => {
|
|
||||||
return e.altKey && e.keyCode === 83;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const hasPressedAltAndBKey = e => {
|
|
||||||
return e.altKey && e.keyCode === 66;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const hasPressedAltAndNKey = e => {
|
|
||||||
return e.altKey && e.keyCode === 78;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const hasPressedAltAndAKey = e => {
|
|
||||||
return e.altKey && e.keyCode === 65;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const hasPressedAltAndPKey = e => {
|
|
||||||
return e.altKey && e.keyCode === 80;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const hasPressedAltAndLKey = e => {
|
|
||||||
return e.altKey && e.keyCode === 76;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const hasPressedAltAndEKey = e => {
|
|
||||||
return e.altKey && e.keyCode === 69;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const hasPressedCommandPlusAltAndEKey = e => {
|
|
||||||
return e.metaKey && e.altKey && e.keyCode === 69;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const hasPressedAltAndOKey = e => {
|
|
||||||
return e.altKey && e.keyCode === 79;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const hasPressedAltAndJKey = e => {
|
|
||||||
return e.altKey && e.keyCode === 74;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const hasPressedAltAndKKey = e => {
|
|
||||||
return e.altKey && e.keyCode === 75;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const hasPressedAltAndMKey = e => {
|
|
||||||
return e.altKey && e.keyCode === 77;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const hasPressedArrowUpKey = e => {
|
|
||||||
return e.keyCode === 38;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const hasPressedArrowDownKey = e => {
|
|
||||||
return e.keyCode === 40;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const hasPressedArrowLeftKey = e => {
|
|
||||||
return e.keyCode === 37;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const hasPressedArrowRightKey = e => {
|
|
||||||
return e.keyCode === 39;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const hasPressedCommandPlusKKey = e => {
|
|
||||||
return e.metaKey && e.keyCode === 75;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a string representation of the hotkey pattern based on the provided event object.
|
|
||||||
* @param {KeyboardEvent} e - The keyboard event object.
|
|
||||||
* @returns {string} - The hotkey pattern string.
|
|
||||||
*/
|
|
||||||
export const buildHotKeys = e => {
|
|
||||||
const key = e.key.toLowerCase();
|
|
||||||
if (['shift', 'meta', 'alt', 'control'].includes(key)) {
|
|
||||||
return key;
|
|
||||||
}
|
|
||||||
let hotKeyPattern = '';
|
|
||||||
if (e.altKey) {
|
|
||||||
hotKeyPattern += 'alt+';
|
|
||||||
}
|
|
||||||
if (e.ctrlKey) {
|
|
||||||
hotKeyPattern += 'ctrl+';
|
|
||||||
}
|
|
||||||
if (e.metaKey && !e.ctrlKey) {
|
|
||||||
hotKeyPattern += 'meta+';
|
|
||||||
}
|
|
||||||
if (e.shiftKey) {
|
|
||||||
hotKeyPattern += 'shift+';
|
|
||||||
}
|
|
||||||
hotKeyPattern += key;
|
|
||||||
return hotKeyPattern;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -3,20 +3,19 @@ import {
|
|||||||
isEscape,
|
isEscape,
|
||||||
hasPressedShift,
|
hasPressedShift,
|
||||||
hasPressedCommand,
|
hasPressedCommand,
|
||||||
buildHotKeys,
|
|
||||||
isActiveElementTypeable,
|
isActiveElementTypeable,
|
||||||
} from '../KeyboardHelpers';
|
} from '../KeyboardHelpers';
|
||||||
|
|
||||||
describe('#KeyboardHelpers', () => {
|
describe('#KeyboardHelpers', () => {
|
||||||
describe('#isEnter', () => {
|
describe('#isEnter', () => {
|
||||||
it('return correct values', () => {
|
it('return correct values', () => {
|
||||||
expect(isEnter({ keyCode: 13 })).toEqual(true);
|
expect(isEnter({ key: 'Enter' })).toEqual(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('#isEscape', () => {
|
describe('#isEscape', () => {
|
||||||
it('return correct values', () => {
|
it('return correct values', () => {
|
||||||
expect(isEscape({ keyCode: 27 })).toEqual(true);
|
expect(isEscape({ key: 'Escape' })).toEqual(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -31,14 +30,6 @@ describe('#KeyboardHelpers', () => {
|
|||||||
expect(hasPressedCommand({ metaKey: true })).toEqual(true);
|
expect(hasPressedCommand({ metaKey: true })).toEqual(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('#buildHotKeys', () => {
|
|
||||||
it('returns hot keys', () => {
|
|
||||||
expect(buildHotKeys({ altKey: true, key: 'alt' })).toEqual('alt');
|
|
||||||
expect(buildHotKeys({ ctrlKey: true, key: 'a' })).toEqual('ctrl+a');
|
|
||||||
expect(buildHotKeys({ metaKey: true, key: 'b' })).toEqual('meta+b');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('isActiveElementTypeable', () => {
|
describe('isActiveElementTypeable', () => {
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
import { isActiveElementTypeable, isEscape } from '../helpers/KeyboardHelpers';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
mounted() {
|
|
||||||
document.addEventListener('keydown', this.onKeyDownHandler);
|
|
||||||
},
|
|
||||||
beforeDestroy() {
|
|
||||||
document.removeEventListener('keydown', this.onKeyDownHandler);
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
onKeyDownHandler(e) {
|
|
||||||
const isTypeable = isActiveElementTypeable(e);
|
|
||||||
|
|
||||||
if (isTypeable) {
|
|
||||||
if (isEscape(e)) {
|
|
||||||
e.target.blur();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.handleKeyEvents(e);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { isActiveElementTypeable, isEscape } from '../helpers/KeyboardHelpers';
|
||||||
|
|
||||||
|
import { createKeybindingsHandler } from 'tinykeys';
|
||||||
|
|
||||||
|
// this is a store that stores the handler globally, and only gets reset on reload
|
||||||
|
const taggedHandlers = [];
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mounted() {
|
||||||
|
const events = this.getKeyboardEvents();
|
||||||
|
if (events) {
|
||||||
|
const wrappedEvents = this.wrapEventsInKeybindingsHandler(events);
|
||||||
|
const keydownHandler = createKeybindingsHandler(wrappedEvents);
|
||||||
|
this.appendToHandler(keydownHandler);
|
||||||
|
document.addEventListener('keydown', keydownHandler);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
if (this.$el && this.$el.dataset.keydownHandlerIndex) {
|
||||||
|
const handlerToRemove =
|
||||||
|
taggedHandlers[this.$el.dataset.keydownHandlerIndex];
|
||||||
|
document.removeEventListener('keydown', handlerToRemove);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
appendToHandler(keydownHandler) {
|
||||||
|
const indexToAppend = taggedHandlers.push(keydownHandler) - 1;
|
||||||
|
const root = this.$el;
|
||||||
|
root.dataset.keydownHandlerIndex = indexToAppend;
|
||||||
|
},
|
||||||
|
getKeyboardEvents() {
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
wrapEventsInKeybindingsHandler(events) {
|
||||||
|
const wrappedEvents = {};
|
||||||
|
Object.keys(events).forEach(eventName => {
|
||||||
|
wrappedEvents[eventName] = this.keydownWrapper(events[eventName]);
|
||||||
|
});
|
||||||
|
|
||||||
|
return wrappedEvents;
|
||||||
|
},
|
||||||
|
keydownWrapper(handler) {
|
||||||
|
return e => {
|
||||||
|
const actionToPerform =
|
||||||
|
typeof handler === 'function' ? handler : handler.action;
|
||||||
|
const allowOnFocusedInput =
|
||||||
|
typeof handler === 'function' ? false : handler.allowOnFocusedInput;
|
||||||
|
|
||||||
|
const isTypeable = isActiveElementTypeable(e);
|
||||||
|
|
||||||
|
if (isTypeable) {
|
||||||
|
if (isEscape(e)) {
|
||||||
|
e.target.blur();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!allowOnFocusedInput) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
actionToPerform(e);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -33,8 +33,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { buildHotKeys } from 'shared/helpers/KeyboardHelpers';
|
|
||||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||||
|
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||||
import Header from '../../components/playground/Header.vue';
|
import Header from '../../components/playground/Header.vue';
|
||||||
import UserMessage from '../../components/playground/UserMessage.vue';
|
import UserMessage from '../../components/playground/UserMessage.vue';
|
||||||
import BotMessage from '../../components/playground/BotMessage.vue';
|
import BotMessage from '../../components/playground/BotMessage.vue';
|
||||||
@@ -47,7 +47,7 @@ export default {
|
|||||||
BotMessage,
|
BotMessage,
|
||||||
TypingIndicator,
|
TypingIndicator,
|
||||||
},
|
},
|
||||||
mixins: [messageFormatterMixin],
|
mixins: [messageFormatterMixin, keyboardEventListenerMixins],
|
||||||
props: {
|
props: {
|
||||||
componentData: {
|
componentData: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -67,17 +67,17 @@ export default {
|
|||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.focusInput();
|
this.focusInput();
|
||||||
document.addEventListener('keydown', this.handleKeyEvents);
|
|
||||||
},
|
|
||||||
beforeDestroy() {
|
|
||||||
document.removeEventListener('keydown', this.handleKeyEvents);
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
handleKeyEvents(e) {
|
getKeyboardEvents() {
|
||||||
const keyCode = buildHotKeys(e);
|
return {
|
||||||
if (['meta+enter', 'ctrl+enter'].includes(keyCode)) {
|
'$mod+Enter': {
|
||||||
this.onMessageSend();
|
action: () => {
|
||||||
}
|
this.onMessageSend();
|
||||||
|
},
|
||||||
|
allowOnFocusedInput: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
},
|
},
|
||||||
focusInput() {
|
focusInput() {
|
||||||
this.$refs.messageInput.focus();
|
this.$refs.messageInput.focus();
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
<template>
|
||||||
|
<button
|
||||||
|
class="inline-flex items-center gap-1 text-sm font-medium reset-base rounded-xl w-fit"
|
||||||
|
:class="buttonClasses"
|
||||||
|
v-bind="$attrs"
|
||||||
|
>
|
||||||
|
<fluent-icon
|
||||||
|
v-if="icon && !trailingIcon"
|
||||||
|
size="1.16em"
|
||||||
|
:icon="icon"
|
||||||
|
class="flex-shrink-0"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
v-if="$slots.default"
|
||||||
|
class="text-sm font-medium truncate ltr:text-left rtl:text-right"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</span>
|
||||||
|
<fluent-icon
|
||||||
|
v-if="icon && trailingIcon"
|
||||||
|
size="1.16em"
|
||||||
|
:icon="icon"
|
||||||
|
class="flex-shrink-0"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import { computed, defineProps, useAttrs } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
variant: {
|
||||||
|
type: String,
|
||||||
|
default: 'solid',
|
||||||
|
validator: value => ['outline', 'ghost', 'solid'].includes(value),
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: String,
|
||||||
|
default: 'large',
|
||||||
|
validator: value => ['medium', 'large'].includes(value),
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
colorScheme: {
|
||||||
|
type: String,
|
||||||
|
default: 'primary',
|
||||||
|
validator: value => ['primary', 'secondary', 'danger'].includes(value),
|
||||||
|
},
|
||||||
|
trailingIcon: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const attrs = useAttrs();
|
||||||
|
|
||||||
|
const baseClasses = {
|
||||||
|
outline: 'outline outline-1 -outline-offset-1 focus:ring focus:ring-offset-1',
|
||||||
|
ghost: 'hover:text-600 active:text-600 focus:ring focus:ring-offset-1',
|
||||||
|
solid: 'hover:bg-700 active:bg-700 focus:ring focus:ring-offset-1',
|
||||||
|
};
|
||||||
|
|
||||||
|
const colorClass = computed(() => {
|
||||||
|
if (attrs.disabled) {
|
||||||
|
return 'bg-ash-200 text-ash-600 cursor-not-allowed';
|
||||||
|
}
|
||||||
|
|
||||||
|
const styleMap = {
|
||||||
|
primary: {
|
||||||
|
outline: `${baseClasses.outline} outline-primary-400 hover:text-primary-600 active:text-primary-600 focus:ring-primary-400`,
|
||||||
|
ghost: `${baseClasses.ghost} focus:ring-primary-400`,
|
||||||
|
solid: `${baseClasses.solid} bg-primary-600 text-white focus:ring-primary-400`,
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
outline: `${baseClasses.outline} outline-ash-400 hover:text-ash-600 active:text-ash-600 focus:ring-ash-400`,
|
||||||
|
ghost: `${baseClasses.ghost} focus:ring-ash-400`,
|
||||||
|
solid: `${baseClasses.solid} bg-ash-100 text-ash-900 focus:ring-ash-400`,
|
||||||
|
},
|
||||||
|
danger: {
|
||||||
|
outline: `${baseClasses.outline} outline-ruby-400 hover:text-ruby-600 active:text-ruby-600 focus:ring-ruby-400`,
|
||||||
|
ghost: `${baseClasses.ghost} focus:ring-ruby-400`,
|
||||||
|
solid: `${baseClasses.solid} bg-ruby-600 text-white focus:ring-ruby-400`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const schemeStyles = styleMap[props.colorScheme];
|
||||||
|
return schemeStyles[props.variant] || schemeStyles.solid;
|
||||||
|
});
|
||||||
|
|
||||||
|
const sizeClass = computed(() => {
|
||||||
|
if (props.size === 'medium') {
|
||||||
|
return 'h-8 px-3 py-1.5 text-sm';
|
||||||
|
}
|
||||||
|
return 'h-10 px-4 py-2.5 text';
|
||||||
|
});
|
||||||
|
|
||||||
|
const buttonClasses = computed(() => [colorClass.value, sizeClass.value]);
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="rounded-xl flex leading-[100%] font-medium items-center justify-center text-center cursor-default"
|
||||||
|
:class="`h-[${size}px] w-[${size}px] ${colorClass}`"
|
||||||
|
:style="style"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<slot>{{ initial }}</slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { userInitial } from 'v3/helpers/CommonHelper';
|
||||||
|
const colors = {
|
||||||
|
1: 'bg-ash-200 text-ash-900',
|
||||||
|
2: 'bg-amber-200 text-amber-900',
|
||||||
|
3: 'bg-pink-100 text-pink-800',
|
||||||
|
4: 'bg-purple-100 text-purple-800',
|
||||||
|
5: 'bg-indigo-100 text-indigo-800',
|
||||||
|
6: 'bg-grass-100 text-grass-800',
|
||||||
|
7: 'bg-mint-100 text-mint-800',
|
||||||
|
8: 'bg-orange-100 text-orange-800',
|
||||||
|
};
|
||||||
|
const props = defineProps({
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: Number,
|
||||||
|
default: 72,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const style = computed(() => ({
|
||||||
|
fontSize: `${Math.floor(props.size / 2.5)}px`,
|
||||||
|
}));
|
||||||
|
const colorClass = computed(() => {
|
||||||
|
return colors[(props.name.length % 8) + 1];
|
||||||
|
});
|
||||||
|
|
||||||
|
const initial = computed(() => userInitial(props.name));
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
<template>
|
||||||
|
<div class="relative rounded-xl h-[72px] w-[72px] cursor-pointe group">
|
||||||
|
<img
|
||||||
|
v-if="shouldShowImage"
|
||||||
|
class="rounded-xl h-[72px] w-[72px]"
|
||||||
|
:alt="name"
|
||||||
|
:src="src"
|
||||||
|
draggable="false"
|
||||||
|
@load="onImageLoad"
|
||||||
|
@error="onImageLoadError"
|
||||||
|
/>
|
||||||
|
<initials-avatar v-else-if="!shouldShowImage" :name="name" :size="72" />
|
||||||
|
|
||||||
|
<input
|
||||||
|
ref="fileInputRef"
|
||||||
|
type="file"
|
||||||
|
accept="image/png, image/jpeg, image/jpg, image/gif, image/webp"
|
||||||
|
hidden
|
||||||
|
@change="onImageUpload"
|
||||||
|
/>
|
||||||
|
<div class="hidden group-hover:block">
|
||||||
|
<button
|
||||||
|
v-if="src"
|
||||||
|
class="absolute z-10 flex items-center justify-center w-6 h-6 p-1 border border-white rounded-full select-none dark:border-ash-75 reset-base -top-2 -right-2 bg-ash-300"
|
||||||
|
@click="onAvatarDelete"
|
||||||
|
>
|
||||||
|
<fluent-icon icon="dismiss" size="16" class="text-ash-900" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="reset-base absolute h-[72px] w-[72px] top-0 left-0 rounded-xl select-none flex items-center justify-center bg-modal-backdrop-dark dark:bg-modal-backdrop-dark"
|
||||||
|
@click="openFileInput"
|
||||||
|
>
|
||||||
|
<fluent-icon icon="avatar-upload" size="32" class="text-white" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import InitialsAvatar from './InitialsAvatar.vue';
|
||||||
|
const props = defineProps({
|
||||||
|
src: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emits = defineEmits(['change', 'delete']);
|
||||||
|
|
||||||
|
const hasImageLoaded = ref(false);
|
||||||
|
const imageLoadedError = ref(false);
|
||||||
|
const fileInputRef = ref(null);
|
||||||
|
|
||||||
|
const shouldShowImage = computed(() => props.src && !imageLoadedError.value);
|
||||||
|
|
||||||
|
const onImageLoadError = () => {
|
||||||
|
imageLoadedError.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onImageLoad = () => {
|
||||||
|
hasImageLoaded.value = true;
|
||||||
|
imageLoadedError.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const openFileInput = () => {
|
||||||
|
fileInputRef.value.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onImageUpload = event => {
|
||||||
|
const [file] = event.target.files;
|
||||||
|
emits('change', {
|
||||||
|
file,
|
||||||
|
url: file ? URL.createObjectURL(file) : null,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onAvatarDelete = () => {
|
||||||
|
emits('delete');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -1,3 +1,9 @@
|
|||||||
export const replaceRouteWithReload = url => {
|
export const replaceRouteWithReload = url => {
|
||||||
window.location = url;
|
window.location = url;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const userInitial = name => {
|
||||||
|
const parts = name.split(/[ -]/).filter(Boolean);
|
||||||
|
let initials = parts.map(part => part[0].toUpperCase()).join('');
|
||||||
|
return initials.slice(0, 2);
|
||||||
|
};
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { userInitial } from '../CommonHelper';
|
||||||
|
|
||||||
|
describe('#userInitial', () => {
|
||||||
|
it('returns the initials of the user', () => {
|
||||||
|
expect(userInitial('John Doe')).toEqual('JD');
|
||||||
|
expect(userInitial('John')).toEqual('J');
|
||||||
|
expect(userInitial('John-Doe')).toEqual('JD');
|
||||||
|
expect(userInitial('John Doe Smith')).toEqual('JD');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -220,25 +220,26 @@ export default {
|
|||||||
},
|
},
|
||||||
dropdownItem() {
|
dropdownItem() {
|
||||||
// This function is used to get all the items in the dropdown.
|
// This function is used to get all the items in the dropdown.
|
||||||
|
if (!this.showDropdown) return [];
|
||||||
return Array.from(
|
return Array.from(
|
||||||
this.$refs.dropdown.querySelectorAll(
|
this.$refs.dropdown?.querySelectorAll(
|
||||||
'div.country-dropdown div.country-dropdown--item'
|
'div.country-dropdown div.country-dropdown--item'
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
focusedOrActiveItem(className) {
|
focusedOrActiveItem(className) {
|
||||||
// This function is used to get the focused or active item in the dropdown.
|
// This function is used to get the focused or active item in the dropdown.
|
||||||
|
if (!this.showDropdown) return [];
|
||||||
return Array.from(
|
return Array.from(
|
||||||
this.$refs.dropdown.querySelectorAll(
|
this.$refs.dropdown?.querySelectorAll(
|
||||||
`div.country-dropdown div.country-dropdown--item.${className}`
|
`div.country-dropdown div.country-dropdown--item.${className}`
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
handleKeyboardEvent(e) {
|
adjustScroll() {
|
||||||
if (this.showDropdown) {
|
this.$nextTick(() => {
|
||||||
this.processKeyDownEvent(e);
|
|
||||||
this.scrollToFocusedOrActiveItem(this.focusedOrActiveItem('focus'));
|
this.scrollToFocusedOrActiveItem(this.focusedOrActiveItem('focus'));
|
||||||
}
|
});
|
||||||
},
|
},
|
||||||
onSelect() {
|
onSelect() {
|
||||||
this.onSelectCountry(this.items[this.selectedIndex]);
|
this.onSelectCountry(this.items[this.selectedIndex]);
|
||||||
|
|||||||
@@ -3,13 +3,13 @@ require 'net/imap'
|
|||||||
class Inboxes::FetchImapEmailsJob < MutexApplicationJob
|
class Inboxes::FetchImapEmailsJob < MutexApplicationJob
|
||||||
queue_as :scheduled_jobs
|
queue_as :scheduled_jobs
|
||||||
|
|
||||||
def perform(channel)
|
def perform(channel, interval = 1)
|
||||||
return unless should_fetch_email?(channel)
|
return unless should_fetch_email?(channel)
|
||||||
|
|
||||||
key = format(::Redis::Alfred::EMAIL_MESSAGE_MUTEX, inbox_id: channel.inbox.id)
|
key = format(::Redis::Alfred::EMAIL_MESSAGE_MUTEX, inbox_id: channel.inbox.id)
|
||||||
|
|
||||||
with_lock(key, 5.minutes) do
|
with_lock(key, 5.minutes) do
|
||||||
process_email_for_channel(channel)
|
process_email_for_channel(channel, interval)
|
||||||
end
|
end
|
||||||
rescue *ExceptionList::IMAP_EXCEPTIONS => e
|
rescue *ExceptionList::IMAP_EXCEPTIONS => e
|
||||||
Rails.logger.error "Authorization error for email channel - #{channel.inbox.id} : #{e.message}"
|
Rails.logger.error "Authorization error for email channel - #{channel.inbox.id} : #{e.message}"
|
||||||
@@ -28,11 +28,11 @@ class Inboxes::FetchImapEmailsJob < MutexApplicationJob
|
|||||||
channel.imap_enabled? && !channel.reauthorization_required?
|
channel.imap_enabled? && !channel.reauthorization_required?
|
||||||
end
|
end
|
||||||
|
|
||||||
def process_email_for_channel(channel)
|
def process_email_for_channel(channel, interval)
|
||||||
inbound_emails = if channel.microsoft?
|
inbound_emails = if channel.microsoft?
|
||||||
Imap::MicrosoftFetchEmailService.new(channel: channel).perform
|
Imap::MicrosoftFetchEmailService.new(channel: channel, interval: interval).perform
|
||||||
else
|
else
|
||||||
Imap::FetchEmailService.new(channel: channel).perform
|
Imap::FetchEmailService.new(channel: channel, interval: interval).perform
|
||||||
end
|
end
|
||||||
inbound_emails.map do |inbound_mail|
|
inbound_emails.map do |inbound_mail|
|
||||||
process_mail(inbound_mail, channel)
|
process_mail(inbound_mail, channel)
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class ActionCableListener < BaseListener
|
|||||||
|
|
||||||
notification, account, unread_count, count = extract_notification_and_account(event)
|
notification, account, unread_count, count = extract_notification_and_account(event)
|
||||||
tokens = [event.data[:notification].user.pubsub_token]
|
tokens = [event.data[:notification].user.pubsub_token]
|
||||||
broadcast(account, tokens, NOTIFICATION_DELETED, { notification: notification.push_event_data, unread_count: unread_count, count: count })
|
broadcast(account, tokens, NOTIFICATION_DELETED, { notification: { id: notification.id }, unread_count: unread_count, count: count })
|
||||||
end
|
end
|
||||||
|
|
||||||
def account_cache_invalidated(event)
|
def account_cache_invalidated(event)
|
||||||
|
|||||||
@@ -310,5 +310,4 @@ class Conversation < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
Conversation.include_mod_with('Concerns::Conversation')
|
Conversation.include_mod_with('Concerns::Conversation')
|
||||||
Conversation.include_mod_with('SentimentAnalysisHelper')
|
|
||||||
Conversation.prepend_mod_with('Conversation')
|
Conversation.prepend_mod_with('Conversation')
|
||||||
|
|||||||
@@ -269,7 +269,6 @@ class Message < ApplicationRecord
|
|||||||
reopen_conversation
|
reopen_conversation
|
||||||
notify_via_mail
|
notify_via_mail
|
||||||
set_conversation_activity
|
set_conversation_activity
|
||||||
update_message_sentiments
|
|
||||||
dispatch_create_events
|
dispatch_create_events
|
||||||
send_reply
|
send_reply
|
||||||
execute_message_template_hooks
|
execute_message_template_hooks
|
||||||
@@ -406,10 +405,6 @@ class Message < ApplicationRecord
|
|||||||
conversation.update_columns(last_activity_at: created_at)
|
conversation.update_columns(last_activity_at: created_at)
|
||||||
# rubocop:enable Rails/SkipsModelValidations
|
# rubocop:enable Rails/SkipsModelValidations
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_message_sentiments
|
|
||||||
# override in the enterprise ::Enterprise::SentimentAnalysisJob.perform_later(self)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
Message.prepend_mod_with('Message')
|
Message.prepend_mod_with('Message')
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
require 'net/imap'
|
require 'net/imap'
|
||||||
|
|
||||||
class Imap::BaseFetchEmailService
|
class Imap::BaseFetchEmailService
|
||||||
pattr_initialize [:channel!]
|
pattr_initialize [:channel!, :interval]
|
||||||
|
|
||||||
def fetch_emails
|
def fetch_emails
|
||||||
# Override this method
|
# Override this method
|
||||||
@@ -99,10 +99,10 @@ class Imap::BaseFetchEmailService
|
|||||||
end
|
end
|
||||||
|
|
||||||
# Sends a SEARCH command to search the mailbox for messages that were
|
# Sends a SEARCH command to search the mailbox for messages that were
|
||||||
# created between yesterday and today and returns message sequence numbers.
|
# created between yesterday (or given date) and today and returns message sequence numbers.
|
||||||
# Return <message set>
|
# Return <message set>
|
||||||
def fetch_available_mail_sequence_numbers
|
def fetch_available_mail_sequence_numbers
|
||||||
imap_client.search(['SINCE', yesterday])
|
imap_client.search(['SINCE', since])
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_imap_client
|
def build_imap_client
|
||||||
@@ -123,7 +123,8 @@ class Imap::BaseFetchEmailService
|
|||||||
Mail.read_from_string(raw_email_content)
|
Mail.read_from_string(raw_email_content)
|
||||||
end
|
end
|
||||||
|
|
||||||
def yesterday
|
def since
|
||||||
(Time.zone.today - 1).strftime('%d-%b-%Y')
|
previous_day = Time.zone.today - (interval || 1).to_i
|
||||||
|
previous_day.strftime('%d-%b-%Y')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ By default, it renders:
|
|||||||
tocHeader: '<%= I18n.t('public_portal.toc_header') %>'
|
tocHeader: '<%= I18n.t('public_portal.toc_header') %>'
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<% if @portal.channel_web_widget.present? %>
|
<% if @portal.channel_web_widget.present? && !@is_plain_layout_enabled %>
|
||||||
<%= @portal.channel_web_widget.web_widget_script.html_safe %>
|
<%= @portal.channel_web_widget.web_widget_script.html_safe %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -55,7 +55,6 @@
|
|||||||
browser_name: '<%= browser.name %>',
|
browser_name: '<%= browser.name %>',
|
||||||
}
|
}
|
||||||
window.errorLoggingConfig = '<%= ENV.fetch('SENTRY_DSN', '') %>'
|
window.errorLoggingConfig = '<%= ENV.fetch('SENTRY_DSN', '') %>'
|
||||||
window.logRocketProjectId = '<%= ENV.fetch('LOG_ROCKET_PROJECT_ID', '') %>'
|
|
||||||
</script>
|
</script>
|
||||||
<% if @global_config['ANALYTICS_TOKEN'].present? %>
|
<% if @global_config['ANALYTICS_TOKEN'].present? %>
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<sitemapindex>
|
||||||
|
<% @portal.articles.where(status: :published).each do |article| %>
|
||||||
|
<sitemap>
|
||||||
|
<loc><%= @help_center_url %><%= generate_article_link(@portal.slug, article.slug, false, false) %></loc>
|
||||||
|
<lastmod><%= article.updated_at.strftime("%Y-%m-%d") %></lastmod>
|
||||||
|
</sitemap>
|
||||||
|
<% end %>
|
||||||
|
</sitemapindex>
|
||||||
@@ -393,6 +393,7 @@ Rails.application.routes.draw do
|
|||||||
end
|
end
|
||||||
|
|
||||||
get 'hc/:slug', to: 'public/api/v1/portals#show'
|
get 'hc/:slug', to: 'public/api/v1/portals#show'
|
||||||
|
get 'hc/:slug/sitemap.xml', to: 'public/api/v1/portals#sitemap'
|
||||||
get 'hc/:slug/:locale', to: 'public/api/v1/portals#show'
|
get 'hc/:slug/:locale', to: 'public/api/v1/portals#show'
|
||||||
get 'hc/:slug/:locale/articles', to: 'public/api/v1/portals/articles#index'
|
get 'hc/:slug/:locale/articles', to: 'public/api/v1/portals/articles#index'
|
||||||
get 'hc/:slug/:locale/categories', to: 'public/api/v1/portals/categories#index'
|
get 'hc/:slug/:locale/categories', to: 'public/api/v1/portals/categories#index'
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
class Enterprise::SentimentAnalysisJob < ApplicationJob
|
|
||||||
queue_as :low
|
|
||||||
|
|
||||||
def perform(message)
|
|
||||||
return if message.account.locale != 'en' || !valid_incoming_message?(message)
|
|
||||||
|
|
||||||
save_message_sentiment(message)
|
|
||||||
rescue StandardError => e
|
|
||||||
Rails.logger.error("Sentiment Analysis Error for message #{message.id}: #{e}")
|
|
||||||
ChatwootExceptionTracker.new(e, account: message.account).capture_exception
|
|
||||||
end
|
|
||||||
|
|
||||||
def save_message_sentiment(message)
|
|
||||||
# We are truncating the data here to avoind the OnnxRuntime::Error
|
|
||||||
# Indices element out of data bounds, idx=512 must be within the inclusive range [-512,511]
|
|
||||||
# While gathering the maningfull node the Array/tensor index is going out of bound
|
|
||||||
|
|
||||||
text = message.content&.truncate(2900)
|
|
||||||
return if model.blank?
|
|
||||||
|
|
||||||
sentiment = model.predict(text)
|
|
||||||
message.sentiment = sentiment.merge(value: label_val(sentiment))
|
|
||||||
|
|
||||||
message.save!
|
|
||||||
end
|
|
||||||
|
|
||||||
# Model initializes OnnxRuntime::Model, with given file for inference session and to create the tensor
|
|
||||||
def model
|
|
||||||
model_file = save_and_open_sentiment_file
|
|
||||||
|
|
||||||
return if File.empty?(model_file)
|
|
||||||
|
|
||||||
Informers::SentimentAnalysis.new(model_file)
|
|
||||||
end
|
|
||||||
|
|
||||||
def label_val(sentiment)
|
|
||||||
sentiment[:label] == 'positive' ? 1 : -1
|
|
||||||
end
|
|
||||||
|
|
||||||
def valid_incoming_message?(message)
|
|
||||||
message.incoming? && message.content.present? && !message.private?
|
|
||||||
end
|
|
||||||
|
|
||||||
# returns the sentiment file from vendor folder else download it to the path from AWS-S3
|
|
||||||
def save_and_open_sentiment_file
|
|
||||||
model_path = ENV.fetch('SENTIMENT_FILE_PATH', nil)
|
|
||||||
|
|
||||||
sentiment_file = Rails.root.join('vendor/db/sentiment-analysis.onnx')
|
|
||||||
|
|
||||||
return sentiment_file if File.exist?(sentiment_file)
|
|
||||||
|
|
||||||
source_file = Down.download(model_path) # Download file from AWS-S3
|
|
||||||
File.rename(source_file, sentiment_file) # Change the file path
|
|
||||||
|
|
||||||
sentiment_file
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
module Enterprise::Message
|
|
||||||
def update_message_sentiments
|
|
||||||
::Enterprise::SentimentAnalysisJob.perform_later(self) if ENV.fetch('SENTIMENT_FILE_PATH', nil).present?
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
module Enterprise::SentimentAnalysisHelper
|
|
||||||
extend ActiveSupport::Concern
|
|
||||||
|
|
||||||
included do
|
|
||||||
def opening_sentiments
|
|
||||||
records = incoming_messages.first(average_message_count)
|
|
||||||
average_sentiment(records)
|
|
||||||
end
|
|
||||||
|
|
||||||
def closing_sentiments
|
|
||||||
return unless resolved?
|
|
||||||
|
|
||||||
records = incoming_messages.last(average_message_count)
|
|
||||||
average_sentiment(records)
|
|
||||||
end
|
|
||||||
|
|
||||||
def average_sentiment(records)
|
|
||||||
{
|
|
||||||
label: average_sentiment_label(records),
|
|
||||||
score: average_sentiment_score(records)
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def average_sentiment_label(records)
|
|
||||||
value = records.pluck(:sentiment).sum { |a| a['value'].to_i }
|
|
||||||
value.negative? ? 'negative' : 'positive'
|
|
||||||
end
|
|
||||||
|
|
||||||
def average_sentiment_score(records)
|
|
||||||
total = records.pluck(:sentiment).sum { |a| a['score'].to_f }
|
|
||||||
total / average_message_count
|
|
||||||
end
|
|
||||||
|
|
||||||
def average_message_count
|
|
||||||
# incoming_messages.count >= 10 ? 5 : ((incoming_messages.count / 2) - 1)
|
|
||||||
5
|
|
||||||
end
|
|
||||||
|
|
||||||
def incoming_messages
|
|
||||||
messages.incoming.where(private: false)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -21,6 +21,10 @@ module ChatwootApp
|
|||||||
@custom ||= root.join('custom').exist?
|
@custom ||= root.join('custom').exist?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.help_center_root
|
||||||
|
ENV.fetch('HELPCENTER_URL', nil) || ENV.fetch('FRONTEND_URL', nil)
|
||||||
|
end
|
||||||
|
|
||||||
def self.extensions
|
def self.extensions
|
||||||
if custom?
|
if custom?
|
||||||
%w[enterprise custom]
|
%w[enterprise custom]
|
||||||
|
|||||||
+3
-3
@@ -22,7 +22,7 @@
|
|||||||
"size-limit": [
|
"size-limit": [
|
||||||
{
|
{
|
||||||
"path": "public/packs/js/widget-*.js",
|
"path": "public/packs/js/widget-*.js",
|
||||||
"limit": "281 KB"
|
"limit": "300 KB"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "public/packs/js/sdk.js",
|
"path": "public/packs/js/sdk.js",
|
||||||
@@ -35,6 +35,7 @@
|
|||||||
"@chatwoot/utils": "^0.0.23",
|
"@chatwoot/utils": "^0.0.23",
|
||||||
"@hcaptcha/vue-hcaptcha": "^0.3.2",
|
"@hcaptcha/vue-hcaptcha": "^0.3.2",
|
||||||
"@june-so/analytics-next": "^2.0.0",
|
"@june-so/analytics-next": "^2.0.0",
|
||||||
|
"@linear/sdk": "^20.0.0",
|
||||||
"@radix-ui/colors": "^1.0.1",
|
"@radix-ui/colors": "^1.0.1",
|
||||||
"@rails/actioncable": "6.1.3",
|
"@rails/actioncable": "6.1.3",
|
||||||
"@rails/ujs": "^7.0.3-1",
|
"@rails/ujs": "^7.0.3-1",
|
||||||
@@ -61,8 +62,6 @@
|
|||||||
"ionicons": "~2.0.1",
|
"ionicons": "~2.0.1",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"libphonenumber-js": "^1.10.24",
|
"libphonenumber-js": "^1.10.24",
|
||||||
"logrocket": "^3.0.1",
|
|
||||||
"logrocket-vuex": "^0.0.3",
|
|
||||||
"markdown-it": "^13.0.2",
|
"markdown-it": "^13.0.2",
|
||||||
"markdown-it-link-attributes": "^4.0.1",
|
"markdown-it-link-attributes": "^4.0.1",
|
||||||
"md5": "^2.3.0",
|
"md5": "^2.3.0",
|
||||||
@@ -72,6 +71,7 @@
|
|||||||
"postcss-loader": "^4.2.0",
|
"postcss-loader": "^4.2.0",
|
||||||
"semver": "7.5.3",
|
"semver": "7.5.3",
|
||||||
"tailwindcss": "^3.3.2",
|
"tailwindcss": "^3.3.2",
|
||||||
|
"tinykeys": "^2.1.0",
|
||||||
"turbolinks": "^5.2.0",
|
"turbolinks": "^5.2.0",
|
||||||
"url-loader": "^2.0.0",
|
"url-loader": "^2.0.0",
|
||||||
"urlpattern-polyfill": "^6.0.2",
|
"urlpattern-polyfill": "^6.0.2",
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe 'Public Portals API', type: :request do
|
RSpec.describe Public::Api::V1::PortalsController, type: :request do
|
||||||
let!(:account) { create(:account) }
|
let!(:account) { create(:account) }
|
||||||
|
let!(:agent) { create(:user, account: account, role: :agent) }
|
||||||
let!(:portal) { create(:portal, slug: 'test-portal', account_id: account.id, custom_domain: 'www.example.com') }
|
let!(:portal) { create(:portal, slug: 'test-portal', account_id: account.id, custom_domain: 'www.example.com') }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
create(:portal, slug: 'test-portal-1', account_id: account.id)
|
create(:portal, slug: 'test-portal-1', account_id: account.id)
|
||||||
create(:portal, slug: 'test-portal-2', account_id: account.id)
|
create(:portal, slug: 'test-portal-2', account_id: account.id)
|
||||||
|
create_list(:article, 3, account: account, author: agent, portal: portal, status: :published)
|
||||||
|
create_list(:article, 2, account: account, author: agent, portal: portal, status: :draft)
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'GET /public/api/v1/portals/{portal_slug}' do
|
describe 'GET /public/api/v1/portals/{portal_slug}' do
|
||||||
@@ -28,4 +31,28 @@ RSpec.describe 'Public Portals API', type: :request do
|
|||||||
Please send us an email at support@chatwoot.com with the custom domain name and account API key"
|
Please send us an email at support@chatwoot.com with the custom domain name and account API key"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'GET /public/api/v1/portals/{portal_slug}/sitemap' do
|
||||||
|
context 'when custom_domain is present' do
|
||||||
|
it 'gets a valid sitemap' do
|
||||||
|
get "/hc/#{portal.slug}/sitemap.xml"
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
expect(response.body).to match(/<sitemap/)
|
||||||
|
expect(Nokogiri::XML(response.body).errors).to be_empty
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'has valid sitemap links' do
|
||||||
|
get "/hc/#{portal.slug}/sitemap.xml"
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
parsed_xml = Nokogiri::XML(response.body)
|
||||||
|
links = parsed_xml.css('loc')
|
||||||
|
|
||||||
|
links.each do |link|
|
||||||
|
expect(link.text).to match(%r{https://www\.example\.com/hc/test-portal/articles/\d+})
|
||||||
|
end
|
||||||
|
|
||||||
|
expect(links.length).to eq 3
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,117 +0,0 @@
|
|||||||
require 'rails_helper'
|
|
||||||
|
|
||||||
RSpec.describe Enterprise::SentimentAnalysisJob do
|
|
||||||
context 'when account locale set to english language' do
|
|
||||||
let(:account) { create(:account, locale: 'en') }
|
|
||||||
let(:message) { build(:message, content_type: nil, account: account) }
|
|
||||||
|
|
||||||
context 'when update the message sentiments' do
|
|
||||||
let(:model_path) { Rails.root.join('vendor/db/sentiment-analysis.onnx') }
|
|
||||||
let(:model) { double }
|
|
||||||
|
|
||||||
before do
|
|
||||||
allow(Informers::SentimentAnalysis).to receive(:new).with(model_path).and_return(model)
|
|
||||||
allow(model).to receive(:predict).and_return({ label: 'positive', score: '0.6' })
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'with incoming message' do
|
|
||||||
with_modified_env SENTIMENT_FILE_PATH: 'sentiment-analysis.onnx' do
|
|
||||||
message.update(message_type: :incoming)
|
|
||||||
|
|
||||||
described_class.perform_now(message)
|
|
||||||
|
|
||||||
expect(message.sentiment).not_to be_empty
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'update sentiment label for positive message' do
|
|
||||||
with_modified_env SENTIMENT_FILE_PATH: 'sentiment-analysis.onnx' do
|
|
||||||
message.update(message_type: :incoming, content: 'I like your product')
|
|
||||||
|
|
||||||
described_class.perform_now(message)
|
|
||||||
|
|
||||||
expect(message.sentiment).not_to be_empty
|
|
||||||
expect(message.sentiment['label']).to eq('positive')
|
|
||||||
expect(message.sentiment['value']).to eq(1)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'update sentiment label for negative message' do
|
|
||||||
with_modified_env SENTIMENT_FILE_PATH: 'sentiment-analysis.onnx' do
|
|
||||||
message.update(message_type: :incoming, content: 'I did not like your product')
|
|
||||||
allow(model).to receive(:predict).and_return({ label: 'negative', score: '0.6' })
|
|
||||||
|
|
||||||
described_class.perform_now(message)
|
|
||||||
|
|
||||||
expect(message.sentiment).not_to be_empty
|
|
||||||
expect(message.sentiment['label']).to eq('negative')
|
|
||||||
expect(message.sentiment['value']).to eq(-1)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with download sentiment files' do
|
|
||||||
let(:model_path) { nil }
|
|
||||||
let(:model) { double }
|
|
||||||
|
|
||||||
before do
|
|
||||||
allow(Informers::SentimentAnalysis).to receive(:new).with(model_path).and_return(model)
|
|
||||||
allow(model).to receive(:predict).and_return({ label: 'positive', score: '0.6' })
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'fetch saved file in the server' do
|
|
||||||
with_modified_env SENTIMENT_FILE_PATH: 'sentiment-analysis.onnx' do
|
|
||||||
message.update(message_type: :incoming, content: 'I did not like your product')
|
|
||||||
|
|
||||||
described_class.new(message).save_and_open_sentiment_file
|
|
||||||
|
|
||||||
sentiment_file = Rails.root.join('vendor/db/sentiment-analysis.onnx')
|
|
||||||
expect(File).to exist(sentiment_file)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'fetch file from the storage' do
|
|
||||||
with_modified_env SENTIMENT_FILE_PATH: 'sentiment-analysis.onnx' do
|
|
||||||
message.update(message_type: :incoming, content: 'I did not like your product')
|
|
||||||
allow(File).to receive(:exist?).and_return(false)
|
|
||||||
allow(Down).to receive(:download).and_return('./sentiment-analysis.onnx')
|
|
||||||
allow(File).to receive(:rename).and_return(100)
|
|
||||||
|
|
||||||
described_class.new(message).save_and_open_sentiment_file
|
|
||||||
|
|
||||||
sentiment_file = Rails.root.join('vendor/db/sentiment-analysis.onnx')
|
|
||||||
expect(sentiment_file).to be_present
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when does not update the message sentiments' do
|
|
||||||
it 'with outgoing message' do
|
|
||||||
message.update(message_type: :outgoing)
|
|
||||||
|
|
||||||
described_class.perform_now(message)
|
|
||||||
|
|
||||||
expect(message.sentiment).to be_empty
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'with private message' do
|
|
||||||
message.update(private: true)
|
|
||||||
|
|
||||||
described_class.perform_now(message)
|
|
||||||
|
|
||||||
expect(message.sentiment).to be_empty
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when account locale is not set to english language' do
|
|
||||||
let(:account) { create(:account, locale: 'es') }
|
|
||||||
let(:message) { build(:message, content_type: nil, account: account) }
|
|
||||||
|
|
||||||
it 'does not update the message sentiments' do
|
|
||||||
described_class.perform_now(message)
|
|
||||||
|
|
||||||
expect(message.sentiment).to be_empty
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -36,36 +36,6 @@ RSpec.describe Conversation, type: :model do
|
|||||||
# end
|
# end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'conversation sentiments' do
|
|
||||||
include ActiveJob::TestHelper
|
|
||||||
|
|
||||||
let(:conversation) { create(:conversation, additional_attributes: { referer: 'https://www.chatwoot.com/' }) }
|
|
||||||
|
|
||||||
before do
|
|
||||||
10.times do
|
|
||||||
message = create(:message, conversation_id: conversation.id, account_id: conversation.account_id, message_type: 'incoming')
|
|
||||||
message.update(sentiment: { 'label': 'positive', score: '0.4' })
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns opening sentiments' do
|
|
||||||
sentiments = conversation.opening_sentiments
|
|
||||||
expect(sentiments[:label]).to eq('positive')
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns closing sentiments if conversation is not resolved' do
|
|
||||||
sentiments = conversation.closing_sentiments
|
|
||||||
expect(sentiments).to be_nil
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns closing sentiments if it is resolved' do
|
|
||||||
conversation.resolved!
|
|
||||||
|
|
||||||
sentiments = conversation.closing_sentiments
|
|
||||||
expect(sentiments[:label]).to eq('positive')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'sla_policy' do
|
describe 'sla_policy' do
|
||||||
let(:account) { create(:account) }
|
let(:account) { create(:account) }
|
||||||
let(:conversation) { create(:conversation, account: account) }
|
let(:conversation) { create(:conversation, account: account) }
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require 'rails_helper'
|
|
||||||
require Rails.root.join 'spec/models/concerns/liquidable_shared.rb'
|
|
||||||
|
|
||||||
RSpec.describe Message do
|
|
||||||
context 'with sentiment analysis' do
|
|
||||||
let(:message) { build(:message, message_type: :incoming, content_type: nil, account: create(:account)) }
|
|
||||||
|
|
||||||
it 'calls SentimentAnalysisJob' do
|
|
||||||
with_modified_env SENTIMENT_FILE_PATH: 'sentiment-analysis.onnx' do
|
|
||||||
allow(Enterprise::SentimentAnalysisJob).to receive(:perform_later).and_return(:perform_later).with(message)
|
|
||||||
|
|
||||||
message.save!
|
|
||||||
|
|
||||||
expect(Enterprise::SentimentAnalysisJob).to have_received(:perform_later)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -41,18 +41,27 @@ RSpec.describe Inboxes::FetchImapEmailsJob do
|
|||||||
context 'when the channel is regular imap' do
|
context 'when the channel is regular imap' do
|
||||||
it 'calls the imap fetch service' do
|
it 'calls the imap fetch service' do
|
||||||
fetch_service = double
|
fetch_service = double
|
||||||
allow(Imap::FetchEmailService).to receive(:new).with(channel: imap_email_channel).and_return(fetch_service)
|
allow(Imap::FetchEmailService).to receive(:new).with(channel: imap_email_channel, interval: 1).and_return(fetch_service)
|
||||||
allow(fetch_service).to receive(:perform).and_return([])
|
allow(fetch_service).to receive(:perform).and_return([])
|
||||||
|
|
||||||
described_class.perform_now(imap_email_channel)
|
described_class.perform_now(imap_email_channel)
|
||||||
expect(fetch_service).to have_received(:perform)
|
expect(fetch_service).to have_received(:perform)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'calls the imap fetch service with the correct interval' do
|
||||||
|
fetch_service = double
|
||||||
|
allow(Imap::FetchEmailService).to receive(:new).with(channel: imap_email_channel, interval: 4).and_return(fetch_service)
|
||||||
|
allow(fetch_service).to receive(:perform).and_return([])
|
||||||
|
|
||||||
|
described_class.perform_now(imap_email_channel, 4)
|
||||||
|
expect(fetch_service).to have_received(:perform)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when the channel is Microsoft' do
|
context 'when the channel is Microsoft' do
|
||||||
it 'calls the Microsoft fetch service' do
|
it 'calls the Microsoft fetch service' do
|
||||||
fetch_service = double
|
fetch_service = double
|
||||||
allow(Imap::MicrosoftFetchEmailService).to receive(:new).with(channel: microsoft_imap_email_channel).and_return(fetch_service)
|
allow(Imap::MicrosoftFetchEmailService).to receive(:new).with(channel: microsoft_imap_email_channel, interval: 1).and_return(fetch_service)
|
||||||
allow(fetch_service).to receive(:perform).and_return([])
|
allow(fetch_service).to receive(:perform).and_return([])
|
||||||
|
|
||||||
described_class.perform_now(microsoft_imap_email_channel)
|
described_class.perform_now(microsoft_imap_email_channel)
|
||||||
@@ -62,7 +71,7 @@ RSpec.describe Inboxes::FetchImapEmailsJob do
|
|||||||
|
|
||||||
context 'when IMAP connection errors out' do
|
context 'when IMAP connection errors out' do
|
||||||
it 'mark the connection for authorization required' do
|
it 'mark the connection for authorization required' do
|
||||||
allow(Imap::FetchEmailService).to receive(:new).with(channel: imap_email_channel).and_raise(Errno::ECONNREFUSED)
|
allow(Imap::FetchEmailService).to receive(:new).with(channel: imap_email_channel, interval: 1).and_raise(Errno::ECONNREFUSED)
|
||||||
allow(Redis::Alfred).to receive(:incr)
|
allow(Redis::Alfred).to receive(:incr)
|
||||||
|
|
||||||
expect(Redis::Alfred).to receive(:incr).with("AUTHORIZATION_ERROR_COUNT:channel_email:#{imap_email_channel.id}")
|
expect(Redis::Alfred).to receive(:incr).with("AUTHORIZATION_ERROR_COUNT:channel_email:#{imap_email_channel.id}")
|
||||||
@@ -80,14 +89,14 @@ RSpec.describe Inboxes::FetchImapEmailsJob do
|
|||||||
allow(Imap::ImapMailbox).to receive(:new).and_return(mailbox)
|
allow(Imap::ImapMailbox).to receive(:new).and_return(mailbox)
|
||||||
allow(ChatwootExceptionTracker).to receive(:new).and_return(exception_tracker)
|
allow(ChatwootExceptionTracker).to receive(:new).and_return(exception_tracker)
|
||||||
|
|
||||||
allow(Imap::FetchEmailService).to receive(:new).with(channel: imap_email_channel).and_return(fetch_service)
|
allow(Imap::FetchEmailService).to receive(:new).with(channel: imap_email_channel, interval: 1).and_return(fetch_service)
|
||||||
allow(fetch_service).to receive(:perform).and_return([inbound_mail])
|
allow(fetch_service).to receive(:perform).and_return([inbound_mail])
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'calls the mailbox to create emails' do
|
it 'calls the mailbox to create emails' do
|
||||||
allow(mailbox).to receive(:process)
|
allow(mailbox).to receive(:process)
|
||||||
|
|
||||||
expect(Imap::FetchEmailService).to receive(:new).with(channel: imap_email_channel).and_return(fetch_service)
|
expect(Imap::FetchEmailService).to receive(:new).with(channel: imap_email_channel, interval: 1).and_return(fetch_service)
|
||||||
expect(fetch_service).to receive(:perform).and_return([inbound_mail])
|
expect(fetch_service).to receive(:perform).and_return([inbound_mail])
|
||||||
expect(mailbox).to receive(:process).with(inbound_mail, imap_email_channel)
|
expect(mailbox).to receive(:process).with(inbound_mail, imap_email_channel)
|
||||||
|
|
||||||
|
|||||||
@@ -142,7 +142,9 @@ describe ActionCableListener do
|
|||||||
'notification.deleted',
|
'notification.deleted',
|
||||||
{
|
{
|
||||||
account_id: notification.account_id,
|
account_id: notification.account_id,
|
||||||
notification: notification.push_event_data,
|
notification: {
|
||||||
|
id: notification.id
|
||||||
|
},
|
||||||
unread_count: 1,
|
unread_count: 1,
|
||||||
count: 1
|
count: 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,5 +50,31 @@ RSpec.describe Imap::MicrosoftFetchEmailService do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when the interval is passed during an IMAP Sync' do
|
||||||
|
it 'fetches the emails based on the interval specified in the job' do
|
||||||
|
travel_to '26.10.2020 10:00'.to_datetime do
|
||||||
|
email_object = create_inbound_email_from_fixture('only_text.eml')
|
||||||
|
email_header = Net::IMAP::FetchData.new(1, 'BODY[HEADER]' => eml_content_with_message_id)
|
||||||
|
imap_fetch_mail = Net::IMAP::FetchData.new(1, 'RFC822' => eml_content_with_message_id)
|
||||||
|
|
||||||
|
allow(imap).to receive(:search).with(%w[SINCE 18-Oct-2020]).and_return([1])
|
||||||
|
allow(imap).to receive(:fetch).with([1], 'BODY.PEEK[HEADER]').and_return([email_header])
|
||||||
|
allow(imap).to receive(:fetch).with(1, 'RFC822').and_return([imap_fetch_mail])
|
||||||
|
allow(imap).to receive(:logout)
|
||||||
|
|
||||||
|
result = described_class.new(channel: microsoft_channel, interval: 8).perform
|
||||||
|
|
||||||
|
expect(refresh_token_service).to have_received(:access_token)
|
||||||
|
|
||||||
|
expect(result.length).to eq 1
|
||||||
|
expect(result[0].message_id).to eq email_object.message_id
|
||||||
|
expect(imap).to have_received(:search).with(%w[SINCE 18-Oct-2020])
|
||||||
|
expect(imap).to have_received(:fetch).with([1], 'BODY.PEEK[HEADER]')
|
||||||
|
expect(imap).to have_received(:fetch).with(1, 'RFC822')
|
||||||
|
expect(logger).to have_received(:info).with("[IMAP::FETCH_EMAIL_SERVICE] Fetching mails from #{microsoft_channel.email}, found 1.")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
+3
-171
@@ -1,17 +1,5 @@
|
|||||||
const {
|
const { slateDark } = require('@radix-ui/colors');
|
||||||
blue,
|
import { colors } from './theme/colors';
|
||||||
blueDark,
|
|
||||||
green,
|
|
||||||
greenDark,
|
|
||||||
yellow,
|
|
||||||
yellowDark,
|
|
||||||
slate,
|
|
||||||
slateDark,
|
|
||||||
red,
|
|
||||||
redDark,
|
|
||||||
violet,
|
|
||||||
violetDark,
|
|
||||||
} = require('@radix-ui/colors');
|
|
||||||
const defaultTheme = require('tailwindcss/defaultTheme');
|
const defaultTheme = require('tailwindcss/defaultTheme');
|
||||||
module.exports = {
|
module.exports = {
|
||||||
darkMode: 'class',
|
darkMode: 'class',
|
||||||
@@ -41,163 +29,7 @@ module.exports = {
|
|||||||
'modal-backdrop-light': 'rgba(0, 0, 0, 0.4)',
|
'modal-backdrop-light': 'rgba(0, 0, 0, 0.4)',
|
||||||
'modal-backdrop-dark': 'rgba(0, 0, 0, 0.6)',
|
'modal-backdrop-dark': 'rgba(0, 0, 0, 0.6)',
|
||||||
current: 'currentColor',
|
current: 'currentColor',
|
||||||
woot: {
|
...colors,
|
||||||
25: blue.blue2,
|
|
||||||
50: blue.blue3,
|
|
||||||
75: blue.blue4,
|
|
||||||
100: blue.blue5,
|
|
||||||
200: blue.blue7,
|
|
||||||
300: blue.blue8,
|
|
||||||
400: blueDark.blue11,
|
|
||||||
500: blueDark.blue10,
|
|
||||||
600: blueDark.blue9,
|
|
||||||
700: blueDark.blue8,
|
|
||||||
800: blueDark.blue6,
|
|
||||||
900: blueDark.blue2,
|
|
||||||
},
|
|
||||||
green: {
|
|
||||||
50: greenDark.green12,
|
|
||||||
100: green.green6,
|
|
||||||
200: green.green7,
|
|
||||||
300: green.green8,
|
|
||||||
400: greenDark.green10,
|
|
||||||
500: greenDark.green9,
|
|
||||||
600: green.green10,
|
|
||||||
700: green.green11,
|
|
||||||
800: greenDark.green7,
|
|
||||||
900: greenDark.green6,
|
|
||||||
},
|
|
||||||
yellow: {
|
|
||||||
50: yellow.yellow2,
|
|
||||||
100: yellow.yellow3,
|
|
||||||
200: yellow.yellow5,
|
|
||||||
300: yellowDark.yellow10,
|
|
||||||
400: yellowDark.yellow9,
|
|
||||||
500: yellowDark.yellow11,
|
|
||||||
600: yellow.yellow8,
|
|
||||||
700: yellowDark.yellow7,
|
|
||||||
800: yellowDark.yellow2,
|
|
||||||
900: yellowDark.yellow1,
|
|
||||||
},
|
|
||||||
slate: {
|
|
||||||
25: slate.slate2,
|
|
||||||
50: slate.slate3,
|
|
||||||
75: slate.slate4,
|
|
||||||
100: slate.slate5,
|
|
||||||
200: slate.slate7,
|
|
||||||
300: slate.slate8,
|
|
||||||
400: slateDark.slate11,
|
|
||||||
500: slateDark.slate10,
|
|
||||||
600: slate.slate11,
|
|
||||||
700: slateDark.slate8,
|
|
||||||
800: slateDark.slate4,
|
|
||||||
900: slateDark.slate1,
|
|
||||||
},
|
|
||||||
black: {
|
|
||||||
50: slate.slate2,
|
|
||||||
100: slateDark.slate12,
|
|
||||||
200: slate.slate7,
|
|
||||||
300: slate.slate8,
|
|
||||||
400: slateDark.slate11,
|
|
||||||
500: slate.slate9,
|
|
||||||
600: slateDark.slate9,
|
|
||||||
700: slateDark.slate8,
|
|
||||||
800: slateDark.slate7,
|
|
||||||
900: slateDark.slate2,
|
|
||||||
},
|
|
||||||
red: {
|
|
||||||
50: redDark.red12,
|
|
||||||
100: red.red6,
|
|
||||||
200: red.red8,
|
|
||||||
300: redDark.red11,
|
|
||||||
400: redDark.red10,
|
|
||||||
500: red.red9,
|
|
||||||
600: red.red10,
|
|
||||||
700: red.red11,
|
|
||||||
800: redDark.red8,
|
|
||||||
900: red.red12,
|
|
||||||
},
|
|
||||||
violet: {
|
|
||||||
50: violet.violet1,
|
|
||||||
100: violetDark.violet12,
|
|
||||||
200: violet.violet6,
|
|
||||||
300: violet.violet8,
|
|
||||||
400: violet.violet11,
|
|
||||||
500: violet.violet9,
|
|
||||||
600: violetDark.violet8,
|
|
||||||
700: violetDark.violet7,
|
|
||||||
800: violetDark.violet6,
|
|
||||||
900: violet.violet12,
|
|
||||||
},
|
|
||||||
primary: {
|
|
||||||
25: 'rgb(var(--color-primary-25) / <alpha-value>)',
|
|
||||||
50: 'rgb(var(--color-primary-50) / <alpha-value>)',
|
|
||||||
75: 'rgb(var(--color-primary-75) / <alpha-value>)',
|
|
||||||
100: 'rgb(var(--color-primary-100) / <alpha-value>)',
|
|
||||||
200: 'rgb(var(--color-primary-200) / <alpha-value>)',
|
|
||||||
300: 'rgb(var(--color-primary-300) / <alpha-value>)',
|
|
||||||
400: 'rgb(var(--color-primary-400) / <alpha-value>)',
|
|
||||||
500: 'rgb(var(--color-primary-500) / <alpha-value>)',
|
|
||||||
600: 'rgb(var(--color-primary-600) / <alpha-value>)',
|
|
||||||
700: 'rgb(var(--color-primary-700) / <alpha-value>)',
|
|
||||||
800: 'rgb(var(--color-primary-800) / <alpha-value>)',
|
|
||||||
900: 'rgb(var(--color-primary-900) / <alpha-value>)',
|
|
||||||
},
|
|
||||||
ash: {
|
|
||||||
25: 'rgb(var(--color-ash-25) / <alpha-value>)',
|
|
||||||
50: 'rgb(var(--color-ash-50) / <alpha-value>)',
|
|
||||||
75: 'rgb(var(--color-ash-75) / <alpha-value>)',
|
|
||||||
100: 'rgb(var(--color-ash-100) / <alpha-value>)',
|
|
||||||
200: 'rgb(var(--color-ash-200) / <alpha-value>)',
|
|
||||||
300: 'rgb(var(--color-ash-300) / <alpha-value>)',
|
|
||||||
400: 'rgb(var(--color-ash-400) / <alpha-value>)',
|
|
||||||
500: 'rgb(var(--color-ash-500) / <alpha-value>)',
|
|
||||||
600: 'rgb(var(--color-ash-600) / <alpha-value>)',
|
|
||||||
700: 'rgb(var(--color-ash-700) / <alpha-value>)',
|
|
||||||
800: 'rgb(var(--color-ash-800) / <alpha-value>)',
|
|
||||||
900: 'rgb(var(--color-ash-900) / <alpha-value>)',
|
|
||||||
},
|
|
||||||
teal: {
|
|
||||||
25: 'rgb(var(--color-teal-25) / <alpha-value>)',
|
|
||||||
50: 'rgb(var(--color-teal-50) / <alpha-value>)',
|
|
||||||
100: 'rgb(var(--color-teal-100) / <alpha-value>)',
|
|
||||||
200: 'rgb(var(--color-teal-200) / <alpha-value>)',
|
|
||||||
300: 'rgb(var(--color-teal-300) / <alpha-value>)',
|
|
||||||
400: 'rgb(var(--color-teal-400) / <alpha-value>)',
|
|
||||||
500: 'rgb(var(--color-teal-500) / <alpha-value>)',
|
|
||||||
600: 'rgb(var(--color-teal-600) / <alpha-value>)',
|
|
||||||
700: 'rgb(var(--color-teal-700) / <alpha-value>)',
|
|
||||||
800: 'rgb(var(--color-teal-800) / <alpha-value>)',
|
|
||||||
900: 'rgb(var(--color-teal-900) / <alpha-value>)',
|
|
||||||
},
|
|
||||||
amber: {
|
|
||||||
25: 'rgb(var(--color-amber-25) / <alpha-value>)',
|
|
||||||
50: 'rgb(var(--color-amber-50) / <alpha-value>)',
|
|
||||||
75: 'rgb(var(--color-amber-75) / <alpha-value>)',
|
|
||||||
100: 'rgb(var(--color-amber-100) / <alpha-value>)',
|
|
||||||
200: 'rgb(var(--color-amber-200) / <alpha-value>)',
|
|
||||||
300: 'rgb(var(--color-amber-300) / <alpha-value>)',
|
|
||||||
400: 'rgb(var(--color-amber-400) / <alpha-value>)',
|
|
||||||
500: 'rgb(var(--color-amber-500) / <alpha-value>)',
|
|
||||||
600: 'rgb(var(--color-amber-600) / <alpha-value>)',
|
|
||||||
700: 'rgb(var(--color-amber-700) / <alpha-value>)',
|
|
||||||
800: 'rgb(var(--color-amber-800) / <alpha-value>)',
|
|
||||||
900: 'rgb(var(--color-amber-900) / <alpha-value>)',
|
|
||||||
},
|
|
||||||
ruby: {
|
|
||||||
25: 'rgb(var(--color-ruby-25) / <alpha-value>)',
|
|
||||||
50: 'rgb(var(--color-ruby-50) / <alpha-value>)',
|
|
||||||
75: 'rgb(var(--color-ruby-75) / <alpha-value>)',
|
|
||||||
100: 'rgb(var(--color-ruby-100) / <alpha-value>)',
|
|
||||||
200: 'rgb(var(--color-ruby-200) / <alpha-value>)',
|
|
||||||
300: 'rgb(var(--color-ruby-300) / <alpha-value>)',
|
|
||||||
400: 'rgb(var(--color-ruby-400) / <alpha-value>)',
|
|
||||||
500: 'rgb(var(--color-ruby-500) / <alpha-value>)',
|
|
||||||
600: 'rgb(var(--color-ruby-600) / <alpha-value>)',
|
|
||||||
700: 'rgb(var(--color-ruby-700) / <alpha-value>)',
|
|
||||||
800: 'rgb(var(--color-ruby-800) / <alpha-value>)',
|
|
||||||
900: 'rgb(var(--color-ruby-900) / <alpha-value>)',
|
|
||||||
},
|
|
||||||
body: slateDark.slate7,
|
body: slateDark.slate7,
|
||||||
},
|
},
|
||||||
keyframes: {
|
keyframes: {
|
||||||
|
|||||||
+285
@@ -0,0 +1,285 @@
|
|||||||
|
const {
|
||||||
|
blue,
|
||||||
|
blueDark,
|
||||||
|
green,
|
||||||
|
greenDark,
|
||||||
|
yellow,
|
||||||
|
yellowDark,
|
||||||
|
slate,
|
||||||
|
slateDark,
|
||||||
|
red,
|
||||||
|
redDark,
|
||||||
|
violet,
|
||||||
|
violetDark,
|
||||||
|
} = require('@radix-ui/colors');
|
||||||
|
export const colors = {
|
||||||
|
woot: {
|
||||||
|
25: blue.blue2,
|
||||||
|
50: blue.blue3,
|
||||||
|
75: blue.blue4,
|
||||||
|
100: blue.blue5,
|
||||||
|
200: blue.blue7,
|
||||||
|
300: blue.blue8,
|
||||||
|
400: blueDark.blue11,
|
||||||
|
500: blueDark.blue10,
|
||||||
|
600: blueDark.blue9,
|
||||||
|
700: blueDark.blue8,
|
||||||
|
800: blueDark.blue6,
|
||||||
|
900: blueDark.blue2,
|
||||||
|
},
|
||||||
|
green: {
|
||||||
|
50: greenDark.green12,
|
||||||
|
100: green.green6,
|
||||||
|
200: green.green7,
|
||||||
|
300: green.green8,
|
||||||
|
400: greenDark.green10,
|
||||||
|
500: greenDark.green9,
|
||||||
|
600: green.green10,
|
||||||
|
700: green.green11,
|
||||||
|
800: greenDark.green7,
|
||||||
|
900: greenDark.green6,
|
||||||
|
},
|
||||||
|
yellow: {
|
||||||
|
50: yellow.yellow2,
|
||||||
|
100: yellow.yellow3,
|
||||||
|
200: yellow.yellow5,
|
||||||
|
300: yellowDark.yellow10,
|
||||||
|
400: yellowDark.yellow9,
|
||||||
|
500: yellowDark.yellow11,
|
||||||
|
600: yellow.yellow8,
|
||||||
|
700: yellowDark.yellow7,
|
||||||
|
800: yellowDark.yellow2,
|
||||||
|
900: yellowDark.yellow1,
|
||||||
|
},
|
||||||
|
slate: {
|
||||||
|
25: slate.slate2,
|
||||||
|
50: slate.slate3,
|
||||||
|
75: slate.slate4,
|
||||||
|
100: slate.slate5,
|
||||||
|
200: slate.slate7,
|
||||||
|
300: slate.slate8,
|
||||||
|
400: slateDark.slate11,
|
||||||
|
500: slateDark.slate10,
|
||||||
|
600: slate.slate11,
|
||||||
|
700: slateDark.slate8,
|
||||||
|
800: slateDark.slate4,
|
||||||
|
900: slateDark.slate1,
|
||||||
|
},
|
||||||
|
black: {
|
||||||
|
50: slate.slate2,
|
||||||
|
100: slateDark.slate12,
|
||||||
|
200: slate.slate7,
|
||||||
|
300: slate.slate8,
|
||||||
|
400: slateDark.slate11,
|
||||||
|
500: slate.slate9,
|
||||||
|
600: slateDark.slate9,
|
||||||
|
700: slateDark.slate8,
|
||||||
|
800: slateDark.slate7,
|
||||||
|
900: slateDark.slate2,
|
||||||
|
},
|
||||||
|
red: {
|
||||||
|
50: redDark.red12,
|
||||||
|
100: red.red6,
|
||||||
|
200: red.red8,
|
||||||
|
300: redDark.red11,
|
||||||
|
400: redDark.red10,
|
||||||
|
500: red.red9,
|
||||||
|
600: red.red10,
|
||||||
|
700: red.red11,
|
||||||
|
800: redDark.red8,
|
||||||
|
900: red.red12,
|
||||||
|
},
|
||||||
|
violet: {
|
||||||
|
50: violet.violet1,
|
||||||
|
100: violetDark.violet12,
|
||||||
|
200: violet.violet6,
|
||||||
|
300: violet.violet8,
|
||||||
|
400: violet.violet11,
|
||||||
|
500: violet.violet9,
|
||||||
|
600: violetDark.violet8,
|
||||||
|
700: violetDark.violet7,
|
||||||
|
800: violetDark.violet6,
|
||||||
|
900: violet.violet12,
|
||||||
|
},
|
||||||
|
primary: {
|
||||||
|
25: 'rgb(var(--color-primary-25) / <alpha-value>)',
|
||||||
|
50: 'rgb(var(--color-primary-50) / <alpha-value>)',
|
||||||
|
75: 'rgb(var(--color-primary-75) / <alpha-value>)',
|
||||||
|
100: 'rgb(var(--color-primary-100) / <alpha-value>)',
|
||||||
|
200: 'rgb(var(--color-primary-200) / <alpha-value>)',
|
||||||
|
300: 'rgb(var(--color-primary-300) / <alpha-value>)',
|
||||||
|
400: 'rgb(var(--color-primary-400) / <alpha-value>)',
|
||||||
|
500: 'rgb(var(--color-primary-500) / <alpha-value>)',
|
||||||
|
600: 'rgb(var(--color-primary-600) / <alpha-value>)',
|
||||||
|
700: 'rgb(var(--color-primary-700) / <alpha-value>)',
|
||||||
|
800: 'rgb(var(--color-primary-800) / <alpha-value>)',
|
||||||
|
900: 'rgb(var(--color-primary-900) / <alpha-value>)',
|
||||||
|
},
|
||||||
|
ash: {
|
||||||
|
25: 'rgb(var(--color-ash-25) / <alpha-value>)',
|
||||||
|
50: 'rgb(var(--color-ash-50) / <alpha-value>)',
|
||||||
|
75: 'rgb(var(--color-ash-75) / <alpha-value>)',
|
||||||
|
100: 'rgb(var(--color-ash-100) / <alpha-value>)',
|
||||||
|
200: 'rgb(var(--color-ash-200) / <alpha-value>)',
|
||||||
|
300: 'rgb(var(--color-ash-300) / <alpha-value>)',
|
||||||
|
400: 'rgb(var(--color-ash-400) / <alpha-value>)',
|
||||||
|
500: 'rgb(var(--color-ash-500) / <alpha-value>)',
|
||||||
|
600: 'rgb(var(--color-ash-600) / <alpha-value>)',
|
||||||
|
700: 'rgb(var(--color-ash-700) / <alpha-value>)',
|
||||||
|
800: 'rgb(var(--color-ash-800) / <alpha-value>)',
|
||||||
|
900: 'rgb(var(--color-ash-900) / <alpha-value>)',
|
||||||
|
},
|
||||||
|
teal: {
|
||||||
|
25: 'rgb(var(--color-teal-25) / <alpha-value>)',
|
||||||
|
50: 'rgb(var(--color-teal-50) / <alpha-value>)',
|
||||||
|
100: 'rgb(var(--color-teal-100) / <alpha-value>)',
|
||||||
|
200: 'rgb(var(--color-teal-200) / <alpha-value>)',
|
||||||
|
300: 'rgb(var(--color-teal-300) / <alpha-value>)',
|
||||||
|
400: 'rgb(var(--color-teal-400) / <alpha-value>)',
|
||||||
|
500: 'rgb(var(--color-teal-500) / <alpha-value>)',
|
||||||
|
600: 'rgb(var(--color-teal-600) / <alpha-value>)',
|
||||||
|
700: 'rgb(var(--color-teal-700) / <alpha-value>)',
|
||||||
|
800: 'rgb(var(--color-teal-800) / <alpha-value>)',
|
||||||
|
900: 'rgb(var(--color-teal-900) / <alpha-value>)',
|
||||||
|
},
|
||||||
|
amber: {
|
||||||
|
25: 'rgb(var(--color-amber-25) / <alpha-value>)',
|
||||||
|
50: 'rgb(var(--color-amber-50) / <alpha-value>)',
|
||||||
|
75: 'rgb(var(--color-amber-75) / <alpha-value>)',
|
||||||
|
100: 'rgb(var(--color-amber-100) / <alpha-value>)',
|
||||||
|
200: 'rgb(var(--color-amber-200) / <alpha-value>)',
|
||||||
|
300: 'rgb(var(--color-amber-300) / <alpha-value>)',
|
||||||
|
400: 'rgb(var(--color-amber-400) / <alpha-value>)',
|
||||||
|
500: 'rgb(var(--color-amber-500) / <alpha-value>)',
|
||||||
|
600: 'rgb(var(--color-amber-600) / <alpha-value>)',
|
||||||
|
700: 'rgb(var(--color-amber-700) / <alpha-value>)',
|
||||||
|
800: 'rgb(var(--color-amber-800) / <alpha-value>)',
|
||||||
|
900: 'rgb(var(--color-amber-900) / <alpha-value>)',
|
||||||
|
},
|
||||||
|
ruby: {
|
||||||
|
25: 'rgb(var(--color-ruby-25) / <alpha-value>)',
|
||||||
|
50: 'rgb(var(--color-ruby-50) / <alpha-value>)',
|
||||||
|
75: 'rgb(var(--color-ruby-75) / <alpha-value>)',
|
||||||
|
100: 'rgb(var(--color-ruby-100) / <alpha-value>)',
|
||||||
|
200: 'rgb(var(--color-ruby-200) / <alpha-value>)',
|
||||||
|
300: 'rgb(var(--color-ruby-300) / <alpha-value>)',
|
||||||
|
400: 'rgb(var(--color-ruby-400) / <alpha-value>)',
|
||||||
|
500: 'rgb(var(--color-ruby-500) / <alpha-value>)',
|
||||||
|
600: 'rgb(var(--color-ruby-600) / <alpha-value>)',
|
||||||
|
700: 'rgb(var(--color-ruby-700) / <alpha-value>)',
|
||||||
|
800: 'rgb(var(--color-ruby-800) / <alpha-value>)',
|
||||||
|
900: 'rgb(var(--color-ruby-900) / <alpha-value>)',
|
||||||
|
},
|
||||||
|
grass: {
|
||||||
|
25: 'rgb(var(--color-green-25) / <alpha-value>)',
|
||||||
|
50: 'rgb(var(--color-green-50) / <alpha-value>)',
|
||||||
|
75: 'rgb(var(--color-green-75) / <alpha-value>)',
|
||||||
|
100: 'rgb(var(--color-green-100) / <alpha-value>)',
|
||||||
|
200: 'rgb(var(--color-green-200) / <alpha-value>)',
|
||||||
|
300: 'rgb(var(--color-green-300) / <alpha-value>)',
|
||||||
|
400: 'rgb(var(--color-green-400) / <alpha-value>)',
|
||||||
|
500: 'rgb(var(--color-green-500) / <alpha-value>)',
|
||||||
|
600: 'rgb(var(--color-green-600) / <alpha-value>)',
|
||||||
|
700: 'rgb(var(--color-green-700) / <alpha-value>)',
|
||||||
|
800: 'rgb(var(--color-green-800) / <alpha-value>)',
|
||||||
|
900: 'rgb(var(--color-green-900) / <alpha-value>)',
|
||||||
|
},
|
||||||
|
mint: {
|
||||||
|
25: 'rgb(var(--color-mint-25) / <alpha-value>)',
|
||||||
|
50: 'rgb(var(--color-mint-50) / <alpha-value>)',
|
||||||
|
75: 'rgb(var(--color-mint-75) / <alpha-value>)',
|
||||||
|
100: 'rgb(var(--color-mint-100) / <alpha-value>)',
|
||||||
|
200: 'rgb(var(--color-mint-200) / <alpha-value>)',
|
||||||
|
300: 'rgb(var(--color-mint-300) / <alpha-value>)',
|
||||||
|
400: 'rgb(var(--color-mint-400) / <alpha-value>)',
|
||||||
|
500: 'rgb(var(--color-mint-500) / <alpha-value>)',
|
||||||
|
600: 'rgb(var(--color-mint-600) / <alpha-value>)',
|
||||||
|
700: 'rgb(var(--color-mint-700) / <alpha-value>)',
|
||||||
|
800: 'rgb(var(--color-mint-800) / <alpha-value>)',
|
||||||
|
900: 'rgb(var(--color-mint-900) / <alpha-value>)',
|
||||||
|
},
|
||||||
|
sky: {
|
||||||
|
25: 'rgb(var(--color-sky-25) / <alpha-value>)',
|
||||||
|
50: 'rgb(var(--color-sky-50) / <alpha-value>)',
|
||||||
|
75: 'rgb(var(--color-sky-75) / <alpha-value>)',
|
||||||
|
100: 'rgb(var(--color-sky-100) / <alpha-value>)',
|
||||||
|
200: 'rgb(var(--color-sky-200) / <alpha-value>)',
|
||||||
|
300: 'rgb(var(--color-sky-300) / <alpha-value>)',
|
||||||
|
400: 'rgb(var(--color-sky-400) / <alpha-value>)',
|
||||||
|
500: 'rgb(var(--color-sky-500) / <alpha-value>)',
|
||||||
|
600: 'rgb(var(--color-sky-600) / <alpha-value>)',
|
||||||
|
700: 'rgb(var(--color-sky-700) / <alpha-value>)',
|
||||||
|
800: 'rgb(var(--color-sky-800) / <alpha-value>)',
|
||||||
|
900: 'rgb(var(--color-sky-900) / <alpha-value>)',
|
||||||
|
},
|
||||||
|
indigo: {
|
||||||
|
25: 'rgb(var(--color-indigo-25) / <alpha-value>)',
|
||||||
|
50: 'rgb(var(--color-indigo-50) / <alpha-value>)',
|
||||||
|
75: 'rgb(var(--color-indigo-75) / <alpha-value>)',
|
||||||
|
100: 'rgb(var(--color-indigo-100) / <alpha-value>)',
|
||||||
|
200: 'rgb(var(--color-indigo-200) / <alpha-value>)',
|
||||||
|
300: 'rgb(var(--color-indigo-300) / <alpha-value>)',
|
||||||
|
400: 'rgb(var(--color-indigo-400) / <alpha-value>)',
|
||||||
|
500: 'rgb(var(--color-indigo-500) / <alpha-value>)',
|
||||||
|
600: 'rgb(var(--color-indigo-600) / <alpha-value>)',
|
||||||
|
700: 'rgb(var(--color-indigo-700) / <alpha-value>)',
|
||||||
|
800: 'rgb(var(--color-indigo-800) / <alpha-value>)',
|
||||||
|
900: 'rgb(var(--color-indigo-900) / <alpha-value>)',
|
||||||
|
},
|
||||||
|
iris: {
|
||||||
|
25: 'rgb(var(--color-iris-25) / <alpha-value>)',
|
||||||
|
50: 'rgb(var(--color-iris-50) / <alpha-value>)',
|
||||||
|
75: 'rgb(var(--color-iris-75) / <alpha-value>)',
|
||||||
|
100: 'rgb(var(--color-iris-100) / <alpha-value>)',
|
||||||
|
200: 'rgb(var(--color-iris-200) / <alpha-value>)',
|
||||||
|
300: 'rgb(var(--color-iris-300) / <alpha-value>)',
|
||||||
|
400: 'rgb(var(--color-iris-400) / <alpha-value>)',
|
||||||
|
500: 'rgb(var(--color-iris-500) / <alpha-value>)',
|
||||||
|
600: 'rgb(var(--color-iris-600) / <alpha-value>)',
|
||||||
|
700: 'rgb(var(--color-iris-700) / <alpha-value>)',
|
||||||
|
800: 'rgb(var(--color-iris-800) / <alpha-value>)',
|
||||||
|
900: 'rgb(var(--color-iris-900) / <alpha-value>)',
|
||||||
|
},
|
||||||
|
purple: {
|
||||||
|
25: 'rgb(var(--color-violet-25) / <alpha-value>)',
|
||||||
|
50: 'rgb(var(--color-violet-50) / <alpha-value>)',
|
||||||
|
75: 'rgb(var(--color-violet-75) / <alpha-value>)',
|
||||||
|
100: 'rgb(var(--color-violet-100) / <alpha-value>)',
|
||||||
|
200: 'rgb(var(--color-violet-200) / <alpha-value>)',
|
||||||
|
300: 'rgb(var(--color-violet-300) / <alpha-value>)',
|
||||||
|
400: 'rgb(var(--color-violet-400) / <alpha-value>)',
|
||||||
|
500: 'rgb(var(--color-violet-500) / <alpha-value>)',
|
||||||
|
600: 'rgb(var(--color-violet-600) / <alpha-value>)',
|
||||||
|
700: 'rgb(var(--color-violet-700) / <alpha-value>)',
|
||||||
|
800: 'rgb(var(--color-violet-800) / <alpha-value>)',
|
||||||
|
900: 'rgb(var(--color-violet-900) / <alpha-value>)',
|
||||||
|
},
|
||||||
|
pink: {
|
||||||
|
25: 'rgb(var(--color-pink-25) / <alpha-value>)',
|
||||||
|
50: 'rgb(var(--color-pink-50) / <alpha-value>)',
|
||||||
|
75: 'rgb(var(--color-pink-75) / <alpha-value>)',
|
||||||
|
100: 'rgb(var(--color-pink-100) / <alpha-value>)',
|
||||||
|
200: 'rgb(var(--color-pink-200) / <alpha-value>)',
|
||||||
|
300: 'rgb(var(--color-pink-300) / <alpha-value>)',
|
||||||
|
400: 'rgb(var(--color-pink-400) / <alpha-value>)',
|
||||||
|
500: 'rgb(var(--color-pink-500) / <alpha-value>)',
|
||||||
|
600: 'rgb(var(--color-pink-600) / <alpha-value>)',
|
||||||
|
700: 'rgb(var(--color-pink-700) / <alpha-value>)',
|
||||||
|
800: 'rgb(var(--color-pink-800) / <alpha-value>)',
|
||||||
|
900: 'rgb(var(--color-pink-900) / <alpha-value>)',
|
||||||
|
},
|
||||||
|
orange: {
|
||||||
|
25: 'rgb(var(--color-orange-25) / <alpha-value>)',
|
||||||
|
50: 'rgb(var(--color-orange-50) / <alpha-value>)',
|
||||||
|
75: 'rgb(var(--color-orange-75) / <alpha-value>)',
|
||||||
|
100: 'rgb(var(--color-orange-100) / <alpha-value>)',
|
||||||
|
200: 'rgb(var(--color-orange-200) / <alpha-value>)',
|
||||||
|
300: 'rgb(var(--color-orange-300) / <alpha-value>)',
|
||||||
|
400: 'rgb(var(--color-orange-400) / <alpha-value>)',
|
||||||
|
500: 'rgb(var(--color-orange-500) / <alpha-value>)',
|
||||||
|
600: 'rgb(var(--color-orange-600) / <alpha-value>)',
|
||||||
|
700: 'rgb(var(--color-orange-700) / <alpha-value>)',
|
||||||
|
800: 'rgb(var(--color-orange-800) / <alpha-value>)',
|
||||||
|
900: 'rgb(var(--color-orange-900) / <alpha-value>)',
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -3501,6 +3501,11 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.50.0.tgz#9e93b850f0f3fa35f5fa59adfd03adae8488e484"
|
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.50.0.tgz#9e93b850f0f3fa35f5fa59adfd03adae8488e484"
|
||||||
integrity sha512-NCC3zz2+nvYd+Ckfh87rA47zfu2QsQpvc6k1yzTk+b9KzRj0wkGa8LSoGOXN6Zv4lRf/EIoZ80biDh9HOI+RNQ==
|
integrity sha512-NCC3zz2+nvYd+Ckfh87rA47zfu2QsQpvc6k1yzTk+b9KzRj0wkGa8LSoGOXN6Zv4lRf/EIoZ80biDh9HOI+RNQ==
|
||||||
|
|
||||||
|
"@graphql-typed-document-node/core@^3.1.0":
|
||||||
|
version "3.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.2.0.tgz#5f3d96ec6b2354ad6d8a28bf216a1d97b5426861"
|
||||||
|
integrity sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==
|
||||||
|
|
||||||
"@hcaptcha/vue-hcaptcha@^0.3.2":
|
"@hcaptcha/vue-hcaptcha@^0.3.2":
|
||||||
version "0.3.2"
|
version "0.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/@hcaptcha/vue-hcaptcha/-/vue-hcaptcha-0.3.2.tgz#0f77d6fc19bc47eadb6b2181eee5fc132441a942"
|
resolved "https://registry.yarnpkg.com/@hcaptcha/vue-hcaptcha/-/vue-hcaptcha-0.3.2.tgz#0f77d6fc19bc47eadb6b2181eee5fc132441a942"
|
||||||
@@ -3872,6 +3877,15 @@
|
|||||||
typescript "^4.9.5"
|
typescript "^4.9.5"
|
||||||
unfetch "^4.1.0"
|
unfetch "^4.1.0"
|
||||||
|
|
||||||
|
"@linear/sdk@^20.0.0":
|
||||||
|
version "20.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@linear/sdk/-/sdk-20.0.0.tgz#a15afb3abf3cd2d48375f42d39d99d127dc2aa17"
|
||||||
|
integrity sha512-LljgBsSRzU++Jl7eIJhL8Z8Btso+rFN4lWeV3fg0RVfONlW0PEPLkaKJDBdA2NQ2TzpMTCJ6xcDwvIvDSQo6Ww==
|
||||||
|
dependencies:
|
||||||
|
"@graphql-typed-document-node/core" "^3.1.0"
|
||||||
|
graphql "^15.4.0"
|
||||||
|
isomorphic-unfetch "^3.1.0"
|
||||||
|
|
||||||
"@lit-labs/ssr-dom-shim@^1.0.0", "@lit-labs/ssr-dom-shim@^1.1.0":
|
"@lit-labs/ssr-dom-shim@^1.0.0", "@lit-labs/ssr-dom-shim@^1.1.0":
|
||||||
version "1.1.1"
|
version "1.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.1.1.tgz#64df34e2f12e68e78ac57e571d25ec07fa460ca9"
|
resolved "https://registry.yarnpkg.com/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.1.1.tgz#64df34e2f12e68e78ac57e571d25ec07fa460ca9"
|
||||||
@@ -11934,6 +11948,11 @@ graphemer@^1.4.0:
|
|||||||
resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6"
|
resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6"
|
||||||
integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==
|
integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==
|
||||||
|
|
||||||
|
graphql@^15.4.0:
|
||||||
|
version "15.8.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/graphql/-/graphql-15.8.0.tgz#33410e96b012fa3bdb1091cc99a94769db212b38"
|
||||||
|
integrity sha512-5gghUc24tP9HRznNpV2+FIoq3xKkj5dTQqf4v0CpdPbFVwFkWoxOM+o+2OC9ZSvjEMTjfmG9QT+gcvggTwW1zw==
|
||||||
|
|
||||||
handle-thing@^2.0.0:
|
handle-thing@^2.0.0:
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e"
|
resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e"
|
||||||
@@ -14396,16 +14415,6 @@ loglevel@^1.6.8:
|
|||||||
resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.7.1.tgz#005fde2f5e6e47068f935ff28573e125ef72f197"
|
resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.7.1.tgz#005fde2f5e6e47068f935ff28573e125ef72f197"
|
||||||
integrity sha512-Hesni4s5UkWkwCGJMQGAh71PaLUmKFM60dHvq0zi/vDhhrzuk+4GgNbTXJ12YYQJn6ZKBDNIjYcuQGKudvqrIw==
|
integrity sha512-Hesni4s5UkWkwCGJMQGAh71PaLUmKFM60dHvq0zi/vDhhrzuk+4GgNbTXJ12YYQJn6ZKBDNIjYcuQGKudvqrIw==
|
||||||
|
|
||||||
logrocket-vuex@^0.0.3:
|
|
||||||
version "0.0.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/logrocket-vuex/-/logrocket-vuex-0.0.3.tgz#e11d65d384f0c9fdac72f984436cc038adf3db90"
|
|
||||||
integrity sha512-Xnl3pBEkc/bbtZ5JEtFwqNjVuLzVEohlBpyQHP47BlO+/6sFmSM2JcNWojysYUo4cp3IWpmWXcezax2+Cq4RsA==
|
|
||||||
|
|
||||||
logrocket@^3.0.1:
|
|
||||||
version "3.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/logrocket/-/logrocket-3.0.1.tgz#c746e8df3d5fee999b152975e51db1908357a6f0"
|
|
||||||
integrity sha512-jOWG+jEzobKVxGytzZ+4KGm2kiMQMRTHab2uDWQyVZcHfEF38BlCH1yjQVY4LCmuQUwZitP9biMzJZnyUQ0dtQ==
|
|
||||||
|
|
||||||
loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.4.0:
|
loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.4.0:
|
||||||
version "1.4.0"
|
version "1.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
|
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
|
||||||
@@ -19625,6 +19634,11 @@ tinycolor2@^1.1.2:
|
|||||||
resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.4.2.tgz#3f6a4d1071ad07676d7fa472e1fac40a719d8803"
|
resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.4.2.tgz#3f6a4d1071ad07676d7fa472e1fac40a719d8803"
|
||||||
integrity sha512-vJhccZPs965sV/L2sU4oRQVAos0pQXwsvTLkWYdqJ+a8Q5kPFzJTuOFwy7UniPli44NKQGAglksjvOcpo95aZA==
|
integrity sha512-vJhccZPs965sV/L2sU4oRQVAos0pQXwsvTLkWYdqJ+a8Q5kPFzJTuOFwy7UniPli44NKQGAglksjvOcpo95aZA==
|
||||||
|
|
||||||
|
tinykeys@^2.1.0:
|
||||||
|
version "2.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/tinykeys/-/tinykeys-2.1.0.tgz#1341563e92a7fac9ca90053fddaf2b7553500298"
|
||||||
|
integrity sha512-/MESnqBD1xItZJn5oGQ4OsNORQgJfPP96XSGoyu4eLpwpL0ifO0SYR5OD76u0YMhMXsqkb0UqvI9+yXTh4xv8Q==
|
||||||
|
|
||||||
titleize@^3.0.0:
|
titleize@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/titleize/-/titleize-3.0.0.tgz#71c12eb7fdd2558aa8a44b0be83b8a76694acd53"
|
resolved "https://registry.yarnpkg.com/titleize/-/titleize-3.0.0.tgz#71c12eb7fdd2558aa8a44b0be83b8a76694acd53"
|
||||||
|
|||||||
Reference in New Issue
Block a user