Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e90600c03 | ||
|
|
19410dfa3d | ||
|
|
78f94511ff | ||
|
|
d88d0bdd80 | ||
|
|
47f8b2cd0c | ||
|
|
ffd47081bd |
@@ -249,9 +249,6 @@ AZURE_APP_SECRET=
|
||||
## OpenAI key
|
||||
# OPENAI_API_KEY=
|
||||
|
||||
# Sentiment analysis model file path
|
||||
SENTIMENT_FILE_PATH=
|
||||
|
||||
# Housekeeping/Performance related configurations
|
||||
# Set to true if you want to remove stale contact inboxes
|
||||
# contact_inboxes with no conversation older than 90 days will be removed
|
||||
|
||||
@@ -175,9 +175,6 @@ gem 'pgvector'
|
||||
# Convert Website HTML to Markdown
|
||||
gem 'reverse_markdown'
|
||||
|
||||
# Sentiment analysis
|
||||
gem 'informers'
|
||||
|
||||
### Gems required only in specific deployment environments ###
|
||||
##############################################################
|
||||
|
||||
|
||||
+14
-26
@@ -151,7 +151,6 @@ GEM
|
||||
base64 (0.1.1)
|
||||
bcrypt (3.1.20)
|
||||
bindex (0.8.1)
|
||||
blingfire (0.1.8)
|
||||
bootsnap (1.16.0)
|
||||
msgpack (~> 1.2)
|
||||
brakeman (5.4.1)
|
||||
@@ -316,15 +315,15 @@ GEM
|
||||
google-cloud-translate-v3 (0.6.0)
|
||||
gapic-common (>= 0.17.1, < 2.a)
|
||||
google-cloud-errors (~> 1.0)
|
||||
google-protobuf (3.25.2)
|
||||
google-protobuf (3.25.2-arm64-darwin)
|
||||
google-protobuf (3.25.2-x86_64-darwin)
|
||||
google-protobuf (3.25.2-x86_64-linux)
|
||||
google-protobuf (3.25.3)
|
||||
google-protobuf (3.25.3-arm64-darwin)
|
||||
google-protobuf (3.25.3-x86_64-darwin)
|
||||
google-protobuf (3.25.3-x86_64-linux)
|
||||
googleapis-common-protos (1.4.0)
|
||||
google-protobuf (~> 3.14)
|
||||
googleapis-common-protos-types (~> 1.2)
|
||||
grpc (~> 1.27)
|
||||
googleapis-common-protos-types (1.11.0)
|
||||
googleapis-common-protos-types (1.14.0)
|
||||
google-protobuf (~> 3.18)
|
||||
googleauth (1.5.2)
|
||||
faraday (>= 0.17.3, < 3.a)
|
||||
@@ -335,14 +334,17 @@ GEM
|
||||
signet (>= 0.16, < 2.a)
|
||||
groupdate (6.2.1)
|
||||
activesupport (>= 5.2)
|
||||
grpc (1.54.3)
|
||||
google-protobuf (~> 3.21)
|
||||
grpc (1.62.0)
|
||||
google-protobuf (~> 3.25)
|
||||
googleapis-common-protos-types (~> 1.0)
|
||||
grpc (1.54.3-x86_64-darwin)
|
||||
google-protobuf (~> 3.21)
|
||||
grpc (1.62.0-arm64-darwin)
|
||||
google-protobuf (~> 3.25)
|
||||
googleapis-common-protos-types (~> 1.0)
|
||||
grpc (1.54.3-x86_64-linux)
|
||||
google-protobuf (~> 3.21)
|
||||
grpc (1.62.0-x86_64-darwin)
|
||||
google-protobuf (~> 3.25)
|
||||
googleapis-common-protos-types (~> 1.0)
|
||||
grpc (1.62.0-x86_64-linux)
|
||||
google-protobuf (~> 3.25)
|
||||
googleapis-common-protos-types (~> 1.0)
|
||||
haikunator (1.1.1)
|
||||
hairtrigger (1.0.0)
|
||||
@@ -371,10 +373,6 @@ GEM
|
||||
image_processing (1.12.2)
|
||||
mini_magick (>= 4.9.5, < 5)
|
||||
ruby-vips (>= 2.0.17, < 3)
|
||||
informers (0.2.0)
|
||||
blingfire (>= 0.1.7)
|
||||
numo-narray
|
||||
onnxruntime (>= 0.5.1)
|
||||
io-console (0.6.0)
|
||||
irb (1.7.2)
|
||||
reline (>= 0.3.6)
|
||||
@@ -500,7 +498,6 @@ GEM
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.16.4-x86_64-linux)
|
||||
racc (~> 1.4)
|
||||
numo-narray (0.9.2.1)
|
||||
oauth (1.1.0)
|
||||
oauth-tty (~> 1.0, >= 1.0.1)
|
||||
snaky_hash (~> 2.0)
|
||||
@@ -529,14 +526,6 @@ GEM
|
||||
omniauth-rails_csrf_protection (1.0.1)
|
||||
actionpack (>= 4.2)
|
||||
omniauth (~> 2.0)
|
||||
onnxruntime (0.7.6)
|
||||
ffi
|
||||
onnxruntime (0.7.6-arm64-darwin)
|
||||
ffi
|
||||
onnxruntime (0.7.6-x86_64-darwin)
|
||||
ffi
|
||||
onnxruntime (0.7.6-x86_64-linux)
|
||||
ffi
|
||||
openssl (3.1.0)
|
||||
orm_adapter (0.5.0)
|
||||
os (1.1.4)
|
||||
@@ -892,7 +881,6 @@ DEPENDENCIES
|
||||
hashie
|
||||
html2text!
|
||||
image_processing
|
||||
informers
|
||||
jbuilder
|
||||
json_refs
|
||||
json_schemer
|
||||
|
||||
@@ -6,6 +6,12 @@ class Public::Api::V1::PortalsController < Public::Api::V1::Portals::BaseControl
|
||||
|
||||
def show; end
|
||||
|
||||
def sitemap
|
||||
@help_center_url = @portal.custom_domain || ChatwootApp.help_center_root
|
||||
# if help_center_url does not contain a protocol, prepend it with https
|
||||
@help_center_url = "https://#{@help_center_url}" unless @help_center_url.include?('://')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def portal
|
||||
|
||||
@@ -108,6 +108,110 @@
|
||||
--color-teal-75: 236 249 255;
|
||||
--color-teal-800: 0 133 115;
|
||||
--color-teal-900: 13 61 56;
|
||||
|
||||
--color-green-25: 251 254 252;
|
||||
--color-green-50: 244 251 246;
|
||||
--color-green-75: 230 246 235;
|
||||
--color-green-100: 214 241 223;
|
||||
--color-green-200: 196 232 209;
|
||||
--color-green-300: 173 221 192;
|
||||
--color-green-400: 142 206 170;
|
||||
--color-green-500: 91 185 139;
|
||||
--color-green-600: 48 164 108;
|
||||
--color-green-700: 43 154 102;
|
||||
--color-green-800: 33 131 88;
|
||||
--color-green-900: 25 59 45;
|
||||
|
||||
--color-mint-25: 249 254 253;
|
||||
--color-mint-50: 242 251 249;
|
||||
--color-mint-75: 221 249 242;
|
||||
--color-mint-100: 200 244 233;
|
||||
--color-mint-200: 179 236 222;
|
||||
--color-mint-300: 156 224 208;
|
||||
--color-mint-400: 126 207 189;
|
||||
--color-mint-500: 76 187 165;
|
||||
--color-mint-600: 134 234 212;
|
||||
--color-mint-700: 125 224 203;
|
||||
--color-mint-800: 2 120 100;
|
||||
--color-mint-900: 22 67 60;
|
||||
|
||||
--color-sky-25: 249 254 255;
|
||||
--color-sky-50: 241 250 253;
|
||||
--color-sky-75: 225 246 253;
|
||||
--color-sky-100: 209 240 250;
|
||||
--color-sky-200: 190 231 245;
|
||||
--color-sky-300: 169 218 237;
|
||||
--color-sky-400: 141 202 227;
|
||||
--color-sky-500: 96 179 215;
|
||||
--color-sky-600: 124 226 254;
|
||||
--color-sky-700: 116 218 248;
|
||||
--color-sky-800: 0 116 158;
|
||||
--color-sky-900: 29 62 86;
|
||||
|
||||
--color-indigo-25: 253 253 254;
|
||||
--color-indigo-50: 247 249 255;
|
||||
--color-indigo-75: 237 242 254;
|
||||
--color-indigo-100: 225 233 255;
|
||||
--color-indigo-200: 210 222 255;
|
||||
--color-indigo-300: 193 208 255;
|
||||
--color-indigo-400: 171 189 249;
|
||||
--color-indigo-500: 141 164 239;
|
||||
--color-indigo-600: 62 99 221;
|
||||
--color-indigo-700: 51 88 212;
|
||||
--color-indigo-800: 58 91 199;
|
||||
--color-indigo-900: 31 45 92;
|
||||
|
||||
--color-iris-25: 253 253 255;
|
||||
--color-iris-50: 248 248 255;
|
||||
--color-iris-75: 240 241 254;
|
||||
--color-iris-100: 230 231 255;
|
||||
--color-iris-200: 218 220 255;
|
||||
--color-iris-300: 203 205 255;
|
||||
--color-iris-400: 184 186 248;
|
||||
--color-iris-500: 155 158 240;
|
||||
--color-iris-600: 91 91 214;
|
||||
--color-iris-700: 81 81 205;
|
||||
--color-iris-800: 87 83 198;
|
||||
--color-iris-900: 39 41 98;
|
||||
|
||||
--color-violet-25: 253 252 254;
|
||||
--color-violet-50: 250 248 255;
|
||||
--color-violet-75: 244 240 254;
|
||||
--color-violet-100: 235 228 255;
|
||||
--color-violet-200: 225 217 255;
|
||||
--color-violet-300: 212 202 254;
|
||||
--color-violet-400: 194 181 245;
|
||||
--color-violet-500: 170 153 236;
|
||||
--color-violet-600: 110 86 207;
|
||||
--color-violet-700: 101 77 196;
|
||||
--color-violet-800: 101 80 185;
|
||||
--color-violet-900: 47 38 95;
|
||||
|
||||
--color-pink-25: 255 252 254;
|
||||
--color-pink-50: 254 247 251;
|
||||
--color-pink-75: 254 233 245;
|
||||
--color-pink-100: 251 220 239;
|
||||
--color-pink-200: 246 206 231;
|
||||
--color-pink-300: 239 191 221;
|
||||
--color-pink-400: 231 172 208;
|
||||
--color-pink-500: 221 147 194;
|
||||
--color-pink-600: 214 64 159;
|
||||
--color-pink-700: 207 56 151;
|
||||
--color-pink-800: 194 41 138;
|
||||
--color-pink-900: 101 18 73;
|
||||
|
||||
--color-orange-25: 254 252 251;
|
||||
--color-orange-50: 255 247 237;
|
||||
--color-orange-75: 255 239 214;
|
||||
--color-orange-100: 255 223 181;
|
||||
--color-orange-200: 255 209 154;
|
||||
--color-orange-300: 255 193 130;
|
||||
--color-orange-400: 245 174 115;
|
||||
--color-orange-500: 236 148 85;
|
||||
--color-orange-600: 247 107 21;
|
||||
--color-orange-700: 239 95 0;
|
||||
--color-orange-800: 204 78 0;
|
||||
--color-orange-900: 88 45 29;
|
||||
}
|
||||
// scss-lint:disable QualifyingElement
|
||||
body.dark {
|
||||
@@ -175,5 +279,108 @@
|
||||
--color-teal-75: 13 45 42;
|
||||
--color-teal-800: 11 216 182;
|
||||
--color-teal-900: 173 240 221;
|
||||
|
||||
--color-green-25: 14 21 18;
|
||||
--color-green-50: 18 27 23;
|
||||
--color-green-75: 19 45 33;
|
||||
--color-green-100: 17 59 41;
|
||||
--color-green-200: 23 73 51;
|
||||
--color-green-300: 32 87 62;
|
||||
--color-green-400: 40 104 74;
|
||||
--color-green-500: 47 124 87;
|
||||
--color-green-600: 48 164 108;
|
||||
--color-green-700: 51 176 116;
|
||||
--color-green-800: 61 214 140;
|
||||
--color-green-900: 177 241 203;
|
||||
|
||||
--color-mint-25: 14 21 21;
|
||||
--color-mint-50: 15 27 27;
|
||||
--color-mint-75: 9 44 43;
|
||||
--color-mint-100: 0 58 56;
|
||||
--color-mint-200: 0 71 68;
|
||||
--color-mint-300: 16 86 80;
|
||||
--color-mint-400: 30 104 95;
|
||||
--color-mint-500: 39 127 112;
|
||||
--color-mint-600: 134 234 212;
|
||||
--color-mint-700: 168 245 229;
|
||||
--color-mint-800: 88 213 186;
|
||||
--color-mint-900: 196 245 225;
|
||||
|
||||
--color-sky-25: 14 21 21;
|
||||
--color-sky-50: 15 27 27;
|
||||
--color-sky-75: 9 44 43;
|
||||
--color-sky-100: 0 58 56;
|
||||
--color-sky-200: 0 71 68;
|
||||
--color-sky-300: 16 86 80;
|
||||
--color-sky-400: 30 104 95;
|
||||
--color-sky-500: 39 127 112;
|
||||
--color-sky-600: 134 234 212;
|
||||
--color-sky-700: 168 245 229;
|
||||
--color-sky-800: 88 213 186;
|
||||
--color-sky-900: 196 245 225;
|
||||
|
||||
--color-indigo-25: 17 19 31;
|
||||
--color-indigo-50: 20 23 38;
|
||||
--color-indigo-75: 24 36 73;
|
||||
--color-indigo-100: 29 46 98;
|
||||
--color-indigo-200: 37 57 116;
|
||||
--color-indigo-300: 48 67 132;
|
||||
--color-indigo-400: 58 79 151;
|
||||
--color-indigo-500: 67 93 177;
|
||||
--color-indigo-600: 62 99 221;
|
||||
--color-indigo-700: 84 114 228;
|
||||
--color-indigo-800: 158 177 255;
|
||||
--color-indigo-900: 214 225 255;
|
||||
|
||||
--color-iris-25: 19 19 30;
|
||||
--color-iris-50: 23 22 37;
|
||||
--color-iris-75: 32 34 72;
|
||||
--color-iris-100: 38 42 101;
|
||||
--color-iris-200: 48 51 116;
|
||||
--color-iris-300: 61 62 130;
|
||||
--color-iris-400: 74 74 149;
|
||||
--color-iris-500: 89 88 177;
|
||||
--color-iris-600: 91 91 214;
|
||||
--color-iris-700: 110 106 222;
|
||||
--color-iris-800: 177 169 255;
|
||||
--color-iris-900: 224 223 254;
|
||||
|
||||
--color-violet-25: 20 18 31;
|
||||
--color-violet-50: 27 21 37;
|
||||
--color-violet-75: 41 31 67;
|
||||
--color-violet-100: 51 37 91;
|
||||
--color-violet-200: 60 46 105;
|
||||
--color-violet-300: 71 56 118;
|
||||
--color-violet-400: 86 70 139;
|
||||
--color-violet-500: 105 88 173;
|
||||
--color-violet-600: 110 86 207;
|
||||
--color-violet-700: 125 102 217;
|
||||
--color-violet-800: 186 167 255;
|
||||
--color-violet-900: 226 221 254;
|
||||
|
||||
--color-pink-25: 25 17 23;
|
||||
--color-pink-50: 33 18 29;
|
||||
--color-pink-75: 55 23 47;
|
||||
--color-pink-100: 75 20 61;
|
||||
--color-pink-200: 89 28 71;
|
||||
--color-pink-300: 105 41 85;
|
||||
--color-pink-400: 131 56 105;
|
||||
--color-pink-500: 168 72 133;
|
||||
--color-pink-600: 214 64 159;
|
||||
--color-pink-700: 222 81 168;
|
||||
--color-pink-800: 255 141 204;
|
||||
--color-pink-900: 253 209 234;
|
||||
--color-orange-25: 23 18 14;
|
||||
--color-orange-50: 30 22 15;
|
||||
--color-orange-75: 51 30 11;
|
||||
--color-orange-100: 70 33 0;
|
||||
--color-orange-200: 86 40 0;
|
||||
--color-orange-300: 102 53 12;
|
||||
--color-orange-400: 126 69 29;
|
||||
--color-orange-500: 163 88 41;
|
||||
--color-orange-600: 247 107 21;
|
||||
--color-orange-700: 255 128 31;
|
||||
--color-orange-800: 255 160 87;
|
||||
--color-orange-900: 255 224 194;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +185,7 @@ import ConversationBasicFilter from './widgets/conversation/ConversationBasicFil
|
||||
import ChatTypeTabs from './widgets/ChatTypeTabs.vue';
|
||||
import ConversationItem from './ConversationItem.vue';
|
||||
import timeMixin from '../mixins/time';
|
||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||
import conversationMixin from '../mixins/conversations';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import advancedFilterTypes from './widgets/conversation/advancedFilterItems';
|
||||
@@ -199,11 +199,6 @@ import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||
import languages from 'dashboard/components/widgets/conversation/advancedFilterItems/languages';
|
||||
import countries from 'shared/constants/countries';
|
||||
import { generateValuesForEditCustomViews } from 'dashboard/helper/customViewsHelper';
|
||||
|
||||
import {
|
||||
hasPressedAltAndJKey,
|
||||
hasPressedAltAndKKey,
|
||||
} from 'shared/helpers/KeyboardHelpers';
|
||||
import { conversationListPageURL } from '../helper/URLHelper';
|
||||
import {
|
||||
isOnMentionsView,
|
||||
@@ -228,7 +223,7 @@ export default {
|
||||
mixins: [
|
||||
timeMixin,
|
||||
conversationMixin,
|
||||
eventListenerMixins,
|
||||
keyboardEventListenerMixins,
|
||||
alertMixin,
|
||||
filterMixin,
|
||||
uiSettingsMixin,
|
||||
@@ -691,30 +686,40 @@ export default {
|
||||
lastConversationIndex,
|
||||
};
|
||||
},
|
||||
handleKeyEvents(e) {
|
||||
if (hasPressedAltAndJKey(e)) {
|
||||
const { allConversations, activeConversationIndex } =
|
||||
this.getKeyboardListenerParams();
|
||||
if (activeConversationIndex === -1) {
|
||||
allConversations[0].click();
|
||||
}
|
||||
if (activeConversationIndex >= 1) {
|
||||
allConversations[activeConversationIndex - 1].click();
|
||||
}
|
||||
handlePreviousConversation() {
|
||||
const { allConversations, activeConversationIndex } =
|
||||
this.getKeyboardListenerParams();
|
||||
if (activeConversationIndex === -1) {
|
||||
allConversations[0].click();
|
||||
}
|
||||
if (hasPressedAltAndKKey(e)) {
|
||||
const {
|
||||
allConversations,
|
||||
activeConversationIndex,
|
||||
lastConversationIndex,
|
||||
} = this.getKeyboardListenerParams();
|
||||
if (activeConversationIndex === -1) {
|
||||
allConversations[lastConversationIndex].click();
|
||||
} else if (activeConversationIndex < lastConversationIndex) {
|
||||
allConversations[activeConversationIndex + 1].click();
|
||||
}
|
||||
if (activeConversationIndex >= 1) {
|
||||
allConversations[activeConversationIndex - 1].click();
|
||||
}
|
||||
},
|
||||
handleNextConversation() {
|
||||
const {
|
||||
allConversations,
|
||||
activeConversationIndex,
|
||||
lastConversationIndex,
|
||||
} = this.getKeyboardListenerParams();
|
||||
if (activeConversationIndex === -1) {
|
||||
allConversations[lastConversationIndex].click();
|
||||
} else if (activeConversationIndex < lastConversationIndex) {
|
||||
allConversations[activeConversationIndex + 1].click();
|
||||
}
|
||||
},
|
||||
getKeyboardEvents() {
|
||||
return {
|
||||
'Alt+KeyJ': {
|
||||
action: () => this.handlePreviousConversation(),
|
||||
allowOnFocusedInput: true,
|
||||
},
|
||||
'Alt+KeyK': {
|
||||
action: () => this.handleNextConversation(),
|
||||
allowOnFocusedInput: true,
|
||||
},
|
||||
};
|
||||
},
|
||||
resetAndFetchData() {
|
||||
this.appliedFilter = [];
|
||||
this.resetBulkActions();
|
||||
|
||||
@@ -91,12 +91,7 @@ import { mapGetters } from 'vuex';
|
||||
import { mixin as clickaway } from 'vue-clickaway';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import CustomSnoozeModal from 'dashboard/components/CustomSnoozeModal.vue';
|
||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||
import {
|
||||
hasPressedAltAndEKey,
|
||||
hasPressedCommandPlusAltAndEKey,
|
||||
hasPressedAltAndMKey,
|
||||
} from 'shared/helpers/KeyboardHelpers';
|
||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||
import { findSnoozeTime } from 'dashboard/helper/snoozeHelpers';
|
||||
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
|
||||
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
|
||||
@@ -114,7 +109,7 @@ export default {
|
||||
WootDropdownMenu,
|
||||
CustomSnoozeModal,
|
||||
},
|
||||
mixins: [clickaway, alertMixin, eventListenerMixins],
|
||||
mixins: [clickaway, alertMixin, keyboardEventListenerMixins],
|
||||
props: { conversationId: { type: [String, Number], required: true } },
|
||||
data() {
|
||||
return {
|
||||
@@ -159,37 +154,52 @@ export default {
|
||||
bus.$off(CMD_RESOLVE_CONVERSATION, this.onCmdResolveConversation);
|
||||
},
|
||||
methods: {
|
||||
async handleKeyEvents(e) {
|
||||
getKeyboardEvents() {
|
||||
return {
|
||||
'Alt+KeyM': {
|
||||
action: () => this.$refs.arrowDownButton?.$el.click(),
|
||||
allowOnFocusedInput: true,
|
||||
},
|
||||
'Alt+KeyE': this.resolveOrToast,
|
||||
'$mod+Alt+KeyE': async event => {
|
||||
const { all, activeIndex, lastIndex } = this.getConversationParams();
|
||||
await this.resolveOrToast();
|
||||
|
||||
if (activeIndex < lastIndex) {
|
||||
all[activeIndex + 1].click();
|
||||
} else if (all.length > 1) {
|
||||
all[0].click();
|
||||
document.querySelector('.conversations-list').scrollTop = 0;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
},
|
||||
};
|
||||
},
|
||||
getConversationParams() {
|
||||
const allConversations = document.querySelectorAll(
|
||||
'.conversations-list .conversation'
|
||||
);
|
||||
if (hasPressedAltAndMKey(e)) {
|
||||
if (this.$refs.arrowDownButton) {
|
||||
this.$refs.arrowDownButton.$el.click();
|
||||
}
|
||||
}
|
||||
if (hasPressedAltAndEKey(e)) {
|
||||
const activeConversation = document.querySelector(
|
||||
'div.conversations-list div.conversation.active'
|
||||
);
|
||||
const activeConversationIndex = [...allConversations].indexOf(
|
||||
activeConversation
|
||||
);
|
||||
const lastConversationIndex = allConversations.length - 1;
|
||||
try {
|
||||
await this.toggleStatus(wootConstants.STATUS_TYPE.RESOLVED);
|
||||
} catch (error) {
|
||||
// error
|
||||
}
|
||||
if (hasPressedCommandPlusAltAndEKey(e)) {
|
||||
if (activeConversationIndex < lastConversationIndex) {
|
||||
allConversations[activeConversationIndex + 1].click();
|
||||
} else if (allConversations.length > 1) {
|
||||
allConversations[0].click();
|
||||
document.querySelector('.conversations-list').scrollTop = 0;
|
||||
}
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
const activeConversation = document.querySelector(
|
||||
'div.conversations-list div.conversation.active'
|
||||
);
|
||||
const activeConversationIndex = [...allConversations].indexOf(
|
||||
activeConversation
|
||||
);
|
||||
const lastConversationIndex = allConversations.length - 1;
|
||||
|
||||
return {
|
||||
all: allConversations,
|
||||
activeIndex: activeConversationIndex,
|
||||
lastIndex: lastConversationIndex,
|
||||
};
|
||||
},
|
||||
async resolveOrToast() {
|
||||
try {
|
||||
await this.toggleStatus(wootConstants.STATUS_TYPE.RESOLVED);
|
||||
} catch (error) {
|
||||
// error
|
||||
}
|
||||
},
|
||||
onCmdSnoozeConversation(snoozeType) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<aside class="h-full flex">
|
||||
<aside class="flex h-full">
|
||||
<primary-sidebar
|
||||
:logo-source="globalConfig.logoThumbnail"
|
||||
:installation-name="globalConfig.installationName"
|
||||
@@ -36,15 +36,7 @@ import alertMixin from 'shared/mixins/alertMixin';
|
||||
|
||||
import PrimarySidebar from './sidebarComponents/Primary.vue';
|
||||
import SecondarySidebar from './sidebarComponents/Secondary.vue';
|
||||
import {
|
||||
hasPressedAltAndCKey,
|
||||
hasPressedAltAndRKey,
|
||||
hasPressedAltAndSKey,
|
||||
hasPressedAltAndVKey,
|
||||
hasPressedCommandAndForwardSlash,
|
||||
isEscape,
|
||||
} from 'shared/helpers/KeyboardHelpers';
|
||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||
import router from '../../routes';
|
||||
|
||||
export default {
|
||||
@@ -52,7 +44,7 @@ export default {
|
||||
PrimarySidebar,
|
||||
SecondarySidebar,
|
||||
},
|
||||
mixins: [adminMixin, alertMixin, eventListenerMixins],
|
||||
mixins: [adminMixin, alertMixin, keyboardEventListenerMixins],
|
||||
props: {
|
||||
showSecondarySidebar: {
|
||||
type: Boolean,
|
||||
@@ -173,30 +165,27 @@ export default {
|
||||
closeKeyShortcutModal() {
|
||||
this.$emit('close-key-shortcut-modal');
|
||||
},
|
||||
handleKeyEvents(e) {
|
||||
if (hasPressedCommandAndForwardSlash(e)) {
|
||||
this.toggleKeyShortcutModal();
|
||||
}
|
||||
if (isEscape(e)) {
|
||||
this.closeKeyShortcutModal();
|
||||
}
|
||||
|
||||
if (hasPressedAltAndCKey(e)) {
|
||||
if (!this.isCurrentRouteSameAsNavigation('home')) {
|
||||
router.push({ name: 'home' });
|
||||
}
|
||||
} else if (hasPressedAltAndVKey(e)) {
|
||||
if (!this.isCurrentRouteSameAsNavigation('contacts_dashboard')) {
|
||||
router.push({ name: 'contacts_dashboard' });
|
||||
}
|
||||
} else if (hasPressedAltAndRKey(e)) {
|
||||
if (!this.isCurrentRouteSameAsNavigation('settings_account_reports')) {
|
||||
router.push({ name: 'settings_account_reports' });
|
||||
}
|
||||
} else if (hasPressedAltAndSKey(e)) {
|
||||
if (!this.isCurrentRouteSameAsNavigation('agent_list')) {
|
||||
router.push({ name: 'agent_list' });
|
||||
}
|
||||
getKeyboardEvents() {
|
||||
return {
|
||||
'$mod+Slash': this.toggleKeyShortcutModal,
|
||||
'$mod+Escape': this.closeKeyShortcutModal,
|
||||
'Alt+KeyC': {
|
||||
action: () => this.navigateToRoute('home'),
|
||||
},
|
||||
'Alt+KeyV': {
|
||||
action: () => this.navigateToRoute('contacts_dashboard'),
|
||||
},
|
||||
'Alt+KeyR': {
|
||||
action: () => this.navigateToRoute('account_overview_reports'),
|
||||
},
|
||||
'Alt+KeyS': {
|
||||
action: () => this.navigateToRoute('agent_list'),
|
||||
},
|
||||
};
|
||||
},
|
||||
navigateToRoute(routeName) {
|
||||
if (!this.isCurrentRouteSameAsNavigation(routeName)) {
|
||||
router.push({ name: routeName });
|
||||
}
|
||||
},
|
||||
isCurrentRouteSameAsNavigation(routeName) {
|
||||
|
||||
@@ -40,8 +40,7 @@ import AIAssistanceModal from './AIAssistanceModal.vue';
|
||||
import adminMixin from 'dashboard/mixins/aiMixin';
|
||||
import aiMixin from 'dashboard/mixins/isAdmin';
|
||||
import { CMD_AI_ASSIST } from 'dashboard/routes/dashboard/commands/commandBarBusEvents';
|
||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||
import { buildHotKeys } from 'shared/helpers/KeyboardHelpers';
|
||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||
import AIAssistanceCTAButton from './AIAssistanceCTAButton.vue';
|
||||
|
||||
@@ -51,7 +50,7 @@ export default {
|
||||
AICTAModal,
|
||||
AIAssistanceCTAButton,
|
||||
},
|
||||
mixins: [aiMixin, eventListenerMixins, adminMixin, uiSettingsMixin],
|
||||
mixins: [aiMixin, keyboardEventListenerMixins, adminMixin, uiSettingsMixin],
|
||||
data: () => ({
|
||||
showAIAssistanceModal: false,
|
||||
showAICtaModal: false,
|
||||
@@ -87,14 +86,17 @@ export default {
|
||||
},
|
||||
|
||||
methods: {
|
||||
onKeyDownHandler(event) {
|
||||
const keyPattern = buildHotKeys(event);
|
||||
const shouldRevertTheContent =
|
||||
['meta+z', 'ctrl+z'].includes(keyPattern) && !!this.initialMessage;
|
||||
if (shouldRevertTheContent) {
|
||||
this.$emit('replace-text', this.initialMessage);
|
||||
this.initialMessage = '';
|
||||
}
|
||||
getKeyboardEvents() {
|
||||
return {
|
||||
'$mod+KeyZ': {
|
||||
action: () => {
|
||||
if (this.initialMessage) {
|
||||
this.$emit('replace-text', this.initialMessage);
|
||||
this.initialMessage = '';
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
hideAIAssistanceModal() {
|
||||
this.recordAnalytics('DISMISS_AI_SUGGESTION', {
|
||||
|
||||
@@ -10,11 +10,10 @@
|
||||
</template>
|
||||
<script>
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||
import { hasPressedAltAndNKey } from 'shared/helpers/KeyboardHelpers';
|
||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||
|
||||
export default {
|
||||
mixins: [eventListenerMixins],
|
||||
mixins: [keyboardEventListenerMixins],
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
@@ -31,14 +30,18 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleKeyEvents(e) {
|
||||
if (hasPressedAltAndNKey(e)) {
|
||||
if (this.activeTab === wootConstants.ASSIGNEE_TYPE.ALL) {
|
||||
this.onTabChange(0);
|
||||
} else {
|
||||
this.onTabChange(this.activeTabIndex + 1);
|
||||
}
|
||||
}
|
||||
getKeyboardEvents() {
|
||||
return {
|
||||
'Alt+KeyN': {
|
||||
action: () => {
|
||||
if (this.activeTab === wootConstants.ASSIGNEE_TYPE.ALL) {
|
||||
this.onTabChange(0);
|
||||
} else {
|
||||
this.onTabChange(this.activeTabIndex + 1);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
onTabChange(selectedTabIndex) {
|
||||
if (this.items[selectedTabIndex].key !== this.activeTab) {
|
||||
|
||||
@@ -31,15 +31,10 @@
|
||||
|
||||
<script>
|
||||
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 { mixin as clickaway } from 'vue-clickaway';
|
||||
import adminMixin from 'dashboard/mixins/isAdmin';
|
||||
import {
|
||||
buildHotKeys,
|
||||
isEscape,
|
||||
isActiveElementTypeable,
|
||||
} from 'shared/helpers/KeyboardHelpers';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -47,7 +42,7 @@ export default {
|
||||
LabelDropdown,
|
||||
},
|
||||
|
||||
mixins: [clickaway, adminMixin, eventListenerMixins],
|
||||
mixins: [clickaway, adminMixin, keyboardEventListenerMixins],
|
||||
|
||||
props: {
|
||||
allLabels: {
|
||||
@@ -88,17 +83,19 @@ export default {
|
||||
closeDropdownLabel() {
|
||||
this.showSearchDropdownLabel = false;
|
||||
},
|
||||
|
||||
handleKeyEvents(e) {
|
||||
const keyPattern = buildHotKeys(e);
|
||||
|
||||
if (keyPattern === 'l' && !isActiveElementTypeable(e)) {
|
||||
this.toggleLabels();
|
||||
e.preventDefault();
|
||||
} else if (isEscape(e) && this.showSearchDropdownLabel) {
|
||||
this.closeDropdownLabel();
|
||||
e.preventDefault();
|
||||
}
|
||||
getKeyboardEvents() {
|
||||
return {
|
||||
KeyL: {
|
||||
action: e => {
|
||||
this.toggleLabels();
|
||||
e.preventDefault();
|
||||
},
|
||||
},
|
||||
Escape: {
|
||||
action: () => this.closeDropdownLabel(),
|
||||
allowOnFocusedInput: true,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -78,10 +78,8 @@ const MAXIMUM_FILE_UPLOAD_SIZE = 4; // in MB
|
||||
import {
|
||||
hasPressedEnterAndNotCmdOrShift,
|
||||
hasPressedCommandAndEnter,
|
||||
hasPressedAltAndPKey,
|
||||
hasPressedAltAndLKey,
|
||||
} from 'shared/helpers/KeyboardHelpers';
|
||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||
import { isEditorHotKeyEnabled } from 'dashboard/mixins/uiSettings';
|
||||
import {
|
||||
@@ -121,7 +119,7 @@ const createState = (
|
||||
export default {
|
||||
name: 'WootMessageEditor',
|
||||
components: { TagAgents, CannedResponse, VariableList },
|
||||
mixins: [eventListenerMixins, uiSettingsMixin, alertMixin],
|
||||
mixins: [keyboardEventListenerMixins, uiSettingsMixin, alertMixin],
|
||||
props: {
|
||||
value: { type: String, default: '' },
|
||||
editorId: { type: String, default: '' },
|
||||
@@ -522,13 +520,21 @@ export default {
|
||||
isCmdPlusEnterToSendEnabled() {
|
||||
return isEditorHotKeyEnabled(this.uiSettings, 'cmd_enter');
|
||||
},
|
||||
handleKeyEvents(e) {
|
||||
if (hasPressedAltAndPKey(e)) {
|
||||
this.focusEditorInputField();
|
||||
}
|
||||
if (hasPressedAltAndLKey(e)) {
|
||||
this.focusEditorInputField();
|
||||
}
|
||||
getKeyboardEvents() {
|
||||
return {
|
||||
'Alt+KeyP': {
|
||||
action: () => {
|
||||
this.focusEditorInputField();
|
||||
},
|
||||
allowOnFocusedInput: true,
|
||||
},
|
||||
'Alt+KeyL': {
|
||||
action: () => {
|
||||
this.focusEditorInputField();
|
||||
},
|
||||
allowOnFocusedInput: true,
|
||||
},
|
||||
};
|
||||
},
|
||||
focusEditorInputField(pos = 'end') {
|
||||
const { tr } = this.editorView.state;
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
} from '@chatwoot/prosemirror-schema';
|
||||
import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||
|
||||
const MAXIMUM_FILE_UPLOAD_SIZE = 4; // in MB
|
||||
@@ -51,7 +51,7 @@ const createState = (
|
||||
};
|
||||
|
||||
export default {
|
||||
mixins: [eventListenerMixins, uiSettingsMixin, alertMixin],
|
||||
mixins: [keyboardEventListenerMixins, uiSettingsMixin, alertMixin],
|
||||
props: {
|
||||
value: { type: String, default: '' },
|
||||
editorId: { type: String, default: '' },
|
||||
|
||||
@@ -136,8 +136,7 @@
|
||||
<script>
|
||||
import FileUpload from 'vue-upload-component';
|
||||
import * as ActiveStorage from 'activestorage';
|
||||
import { hasPressedAltAndAKey } from 'shared/helpers/KeyboardHelpers';
|
||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||
import inboxMixin from 'shared/mixins/inboxMixin';
|
||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||
@@ -154,7 +153,7 @@ import { mapGetters } from 'vuex';
|
||||
export default {
|
||||
name: 'ReplyBottomPanel',
|
||||
components: { FileUpload, VideoCallButton, AIAssistanceButton },
|
||||
mixins: [eventListenerMixins, uiSettingsMixin, inboxMixin],
|
||||
mixins: [keyboardEventListenerMixins, uiSettingsMixin, inboxMixin],
|
||||
props: {
|
||||
mode: {
|
||||
type: String,
|
||||
@@ -340,10 +339,15 @@ export default {
|
||||
ActiveStorage.start();
|
||||
},
|
||||
methods: {
|
||||
handleKeyEvents(e) {
|
||||
if (hasPressedAltAndAKey(e)) {
|
||||
this.$refs.upload.$children[1].$el.click();
|
||||
}
|
||||
getKeyboardEvents() {
|
||||
return {
|
||||
'Alt+KeyA': {
|
||||
action: () => {
|
||||
this.$refs.upload.$children[1].$el.click();
|
||||
},
|
||||
allowOnFocusedInput: true,
|
||||
},
|
||||
};
|
||||
},
|
||||
toggleMessageSignature() {
|
||||
this.setSignatureFlagForInbox(this.channelType, !this.sendWithSignature);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<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">
|
||||
<woot-button
|
||||
variant="clear"
|
||||
@@ -20,7 +20,7 @@
|
||||
{{ $t('CONVERSATION.REPLYBOX.PRIVATE_NOTE') }}
|
||||
</woot-button>
|
||||
</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">
|
||||
<span :class="charLengthClass">
|
||||
{{ characterLengthWarning }}
|
||||
@@ -48,14 +48,10 @@
|
||||
|
||||
<script>
|
||||
import { REPLY_EDITOR_MODES, CHAR_LENGTH_WARNING } from './constants';
|
||||
import {
|
||||
hasPressedAltAndPKey,
|
||||
hasPressedAltAndLKey,
|
||||
} from 'shared/helpers/KeyboardHelpers';
|
||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||
export default {
|
||||
name: 'ReplyTopPanel',
|
||||
mixins: [eventListenerMixins],
|
||||
mixins: [keyboardEventListenerMixins],
|
||||
props: {
|
||||
mode: {
|
||||
type: String,
|
||||
@@ -99,13 +95,17 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleKeyEvents(e) {
|
||||
if (hasPressedAltAndPKey(e)) {
|
||||
this.handleNoteClick();
|
||||
}
|
||||
if (hasPressedAltAndLKey(e)) {
|
||||
this.handleReplyClick();
|
||||
}
|
||||
getKeyboardEvents() {
|
||||
return {
|
||||
'Alt+KeyP': {
|
||||
action: () => this.handleNoteClick(),
|
||||
allowOnFocusedInput: true,
|
||||
},
|
||||
'Alt+KeyL': {
|
||||
action: () => this.handleReplyClick(),
|
||||
allowOnFocusedInput: true,
|
||||
},
|
||||
};
|
||||
},
|
||||
handleReplyClick() {
|
||||
this.setReplyMode(REPLY_EDITOR_MODES.REPLY);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<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="{
|
||||
'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,
|
||||
'unread-chat': hasUnread,
|
||||
'has-inbox-name': showInboxName,
|
||||
|
||||
@@ -77,11 +77,10 @@
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { hasPressedAltAndOKey } from 'shared/helpers/KeyboardHelpers';
|
||||
import { mapGetters } from 'vuex';
|
||||
import agentMixin from '../../../mixins/agentMixin.js';
|
||||
import BackButton from '../BackButton.vue';
|
||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||
import inboxMixin from 'shared/mixins/inboxMixin';
|
||||
import InboxName from '../InboxName.vue';
|
||||
import MoreActions from './MoreActions.vue';
|
||||
@@ -99,7 +98,7 @@ export default {
|
||||
Thumbnail,
|
||||
SLACardLabel,
|
||||
},
|
||||
mixins: [inboxMixin, agentMixin, eventListenerMixins],
|
||||
mixins: [inboxMixin, agentMixin, keyboardEventListenerMixins],
|
||||
props: {
|
||||
chat: {
|
||||
type: Object,
|
||||
@@ -182,10 +181,12 @@ export default {
|
||||
},
|
||||
|
||||
methods: {
|
||||
handleKeyEvents(e) {
|
||||
if (hasPressedAltAndOKey(e)) {
|
||||
this.$emit('contact-panel-toggle');
|
||||
}
|
||||
getKeyboardEvents() {
|
||||
return {
|
||||
'Alt+KeyO': {
|
||||
action: () => this.$emit('contact-panel-toggle'),
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -116,13 +116,12 @@ import conversationMixin, {
|
||||
} from '../../../mixins/conversations';
|
||||
import inboxMixin, { INBOX_FEATURES } from 'shared/mixins/inboxMixin';
|
||||
import configMixin from 'shared/mixins/configMixin';
|
||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||
import aiMixin from 'dashboard/mixins/aiMixin';
|
||||
|
||||
// utils
|
||||
import { getTypingUsersText } from '../../../helper/commons';
|
||||
import { calculateScrollTop } from './helpers/scrollTopCalculationHelper';
|
||||
import { isEscape } from 'shared/helpers/KeyboardHelpers';
|
||||
import { LocalStorage } from 'shared/helpers/localStorage';
|
||||
|
||||
// constants
|
||||
@@ -141,7 +140,7 @@ export default {
|
||||
mixins: [
|
||||
conversationMixin,
|
||||
inboxMixin,
|
||||
eventListenerMixins,
|
||||
keyboardEventListenerMixins,
|
||||
configMixin,
|
||||
aiMixin,
|
||||
],
|
||||
@@ -418,10 +417,12 @@ export default {
|
||||
closePopoutReplyBox() {
|
||||
this.isPopoutReplyBox = false;
|
||||
},
|
||||
handleKeyEvents(e) {
|
||||
if (isEscape(e)) {
|
||||
this.closePopoutReplyBox();
|
||||
}
|
||||
getKeyboardEvents() {
|
||||
return {
|
||||
Escape: {
|
||||
action: () => this.closePopoutReplyBox(),
|
||||
},
|
||||
};
|
||||
},
|
||||
addScrollListener() {
|
||||
this.conversationPanel = this.$el.querySelector('.conversation-panel');
|
||||
|
||||
@@ -155,6 +155,7 @@
|
||||
import { mapGetters } from 'vuex';
|
||||
import { mixin as clickaway } from 'vue-clickaway';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||
|
||||
import CannedResponse from './CannedResponse.vue';
|
||||
import ReplyToMessage from './ReplyToMessage.vue';
|
||||
@@ -178,7 +179,6 @@ import {
|
||||
replaceVariablesInMessage,
|
||||
} from '@chatwoot/utils';
|
||||
import WhatsappTemplates from './WhatsappTemplates/Modal.vue';
|
||||
import { buildHotKeys } from 'shared/helpers/KeyboardHelpers';
|
||||
import { MESSAGE_MAX_LENGTH } from 'shared/helpers/MessageTypeHelper';
|
||||
import inboxMixin, { INBOX_FEATURES } from 'shared/mixins/inboxMixin';
|
||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||
@@ -225,6 +225,7 @@ export default {
|
||||
messageFormatterMixin,
|
||||
rtlMixin,
|
||||
fileUploadMixin,
|
||||
keyboardEventListenerMixins,
|
||||
],
|
||||
props: {
|
||||
popoutReplyBox: {
|
||||
@@ -701,24 +702,41 @@ export default {
|
||||
this.$store.dispatch('draftMessages/delete', { key });
|
||||
}
|
||||
},
|
||||
handleKeyEvents(e) {
|
||||
const keyCode = buildHotKeys(e);
|
||||
if (keyCode === 'escape') {
|
||||
this.hideEmojiPicker();
|
||||
this.hideMentions();
|
||||
} else if (keyCode === 'meta+k') {
|
||||
const ninja = document.querySelector('ninja-keys');
|
||||
ninja.open();
|
||||
e.preventDefault();
|
||||
} else if (keyCode === 'enter' && this.isAValidEvent('enter')) {
|
||||
this.onSendReply();
|
||||
e.preventDefault();
|
||||
} else if (
|
||||
['meta+enter', 'ctrl+enter'].includes(keyCode) &&
|
||||
this.isAValidEvent('cmd_enter')
|
||||
) {
|
||||
this.onSendReply();
|
||||
}
|
||||
getKeyboardEvents() {
|
||||
return {
|
||||
Escape: {
|
||||
action: () => {
|
||||
this.hideEmojiPicker();
|
||||
this.hideMentions();
|
||||
},
|
||||
allowOnFocusedInput: true,
|
||||
},
|
||||
'$mod+KeyK': {
|
||||
action: e => {
|
||||
e.preventDefault();
|
||||
const ninja = document.querySelector('ninja-keys');
|
||||
ninja.open();
|
||||
},
|
||||
allowOnFocusedInput: true,
|
||||
},
|
||||
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) {
|
||||
return (
|
||||
|
||||
@@ -75,9 +75,10 @@ export default {
|
||||
},
|
||||
|
||||
methods: {
|
||||
handleKeyboardEvent(e) {
|
||||
this.processKeyDownEvent(e);
|
||||
this.$el.scrollTop = 50 * this.selectedIndex;
|
||||
adjustScroll() {
|
||||
this.$nextTick(() => {
|
||||
this.$el.scrollTop = 50 * this.selectedIndex;
|
||||
});
|
||||
},
|
||||
onHover(index) {
|
||||
this.selectedIndex = index;
|
||||
|
||||
+26
-21
@@ -182,12 +182,7 @@
|
||||
<script>
|
||||
import { mixin as clickaway } from 'vue-clickaway';
|
||||
import { mapGetters } from 'vuex';
|
||||
import {
|
||||
isEscape,
|
||||
hasPressedArrowLeftKey,
|
||||
hasPressedArrowRightKey,
|
||||
} from 'shared/helpers/KeyboardHelpers';
|
||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||
import timeMixin from 'dashboard/mixins/time';
|
||||
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
@@ -205,7 +200,7 @@ export default {
|
||||
components: {
|
||||
Thumbnail,
|
||||
},
|
||||
mixins: [eventListenerMixins, clickaway, timeMixin],
|
||||
mixins: [keyboardEventListenerMixins, clickaway, timeMixin],
|
||||
props: {
|
||||
show: {
|
||||
type: Boolean,
|
||||
@@ -304,20 +299,30 @@ export default {
|
||||
this.activeAttachment = attachment;
|
||||
this.activeFileType = type;
|
||||
},
|
||||
onKeyDownHandler(e) {
|
||||
if (isEscape(e)) {
|
||||
this.onClose();
|
||||
} else if (hasPressedArrowLeftKey(e)) {
|
||||
this.onClickChangeAttachment(
|
||||
this.allAttachments[this.activeImageIndex - 1],
|
||||
this.activeImageIndex - 1
|
||||
);
|
||||
} else if (hasPressedArrowRightKey(e)) {
|
||||
this.onClickChangeAttachment(
|
||||
this.allAttachments[this.activeImageIndex + 1],
|
||||
this.activeImageIndex + 1
|
||||
);
|
||||
}
|
||||
getKeyboardEvents() {
|
||||
return {
|
||||
Escape: {
|
||||
action: () => {
|
||||
this.onClose();
|
||||
},
|
||||
},
|
||||
ArrowLeft: {
|
||||
action: () => {
|
||||
this.onClickChangeAttachment(
|
||||
this.allAttachments[this.activeImageIndex - 1],
|
||||
this.activeImageIndex - 1
|
||||
);
|
||||
},
|
||||
},
|
||||
ArrowRight: {
|
||||
action: () => {
|
||||
this.onClickChangeAttachment(
|
||||
this.allAttachments[this.activeImageIndex + 1],
|
||||
this.activeImageIndex + 1
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
onClickDownload() {
|
||||
const { file_type: type, data_url: url } = this.activeAttachment;
|
||||
|
||||
@@ -74,15 +74,10 @@
|
||||
<script>
|
||||
import countries from 'shared/constants/countries.js';
|
||||
import parsePhoneNumber from 'libphonenumber-js';
|
||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||
import {
|
||||
hasPressedArrowUpKey,
|
||||
hasPressedArrowDownKey,
|
||||
isEnter,
|
||||
} from 'shared/helpers/KeyboardHelpers';
|
||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||
|
||||
export default {
|
||||
mixins: [eventListenerMixins],
|
||||
mixins: [keyboardEventListenerMixins],
|
||||
props: {
|
||||
value: {
|
||||
type: [String, Number],
|
||||
@@ -183,6 +178,7 @@ export default {
|
||||
this.$emit('blur', e.target.value);
|
||||
},
|
||||
dropdownItem() {
|
||||
if (!this.showDropdown) return [];
|
||||
return Array.from(
|
||||
this.$refs.dropdown.querySelectorAll(
|
||||
'div.country-dropdown div.country-dropdown--item'
|
||||
@@ -190,34 +186,27 @@ export default {
|
||||
);
|
||||
},
|
||||
focusedItem() {
|
||||
if (!this.showDropdown) return [];
|
||||
return Array.from(
|
||||
this.$refs.dropdown.querySelectorAll('div.country-dropdown div.focus')
|
||||
);
|
||||
},
|
||||
focusedItemIndex() {
|
||||
if (!this.showDropdown) return -1;
|
||||
return Array.from(this.dropdownItem()).indexOf(this.focusedItem()[0]);
|
||||
},
|
||||
onKeyDownHandler(e) {
|
||||
const { showDropdown, filteredCountriesBySearch, onSelectCountry } = this;
|
||||
const { selectedIndex } = this;
|
||||
|
||||
if (showDropdown) {
|
||||
if (hasPressedArrowDownKey(e)) {
|
||||
e.preventDefault();
|
||||
this.selectedIndex = Math.min(
|
||||
selectedIndex + 1,
|
||||
filteredCountriesBySearch.length - 1
|
||||
);
|
||||
this.$refs.dropdown.scrollTop = this.focusedItemIndex() * 28;
|
||||
} 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]);
|
||||
}
|
||||
}
|
||||
moveUp() {
|
||||
if (!this.showDropdown) return;
|
||||
this.selectedIndex = Math.max(this.selectedIndex - 1, 0);
|
||||
this.$refs.dropdown.scrollTop = this.focusedItemIndex() * 28 - 56;
|
||||
},
|
||||
moveDown() {
|
||||
if (!this.showDropdown) return;
|
||||
this.selectedIndex = Math.min(
|
||||
this.selectedIndex + 1,
|
||||
this.filteredCountriesBySearch.length - 1
|
||||
);
|
||||
this.$refs.dropdown.scrollTop = this.focusedItemIndex() * 28 - 56;
|
||||
},
|
||||
onSelectCountry(country) {
|
||||
this.activeCountryCode = country.id;
|
||||
@@ -235,6 +224,33 @@ export default {
|
||||
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() {
|
||||
this.showDropdown = !this.showDropdown;
|
||||
this.selectedIndex = -1;
|
||||
|
||||
@@ -84,9 +84,7 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleKeyboardEvent(e) {
|
||||
this.processKeyDownEvent(e);
|
||||
},
|
||||
adjustScroll() {},
|
||||
onHover(index) {
|
||||
this.selectedIndex = index;
|
||||
},
|
||||
|
||||
+42
-19
@@ -1,12 +1,7 @@
|
||||
import { buildHotKeys } from 'shared/helpers/KeyboardHelpers';
|
||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||
|
||||
export default {
|
||||
mounted() {
|
||||
document.addEventListener('keydown', this.handleKeyboardEvent);
|
||||
},
|
||||
beforeDestroy() {
|
||||
document.removeEventListener('keydown', this.handleKeyboardEvent);
|
||||
},
|
||||
mixins: [keyboardEventListenerMixins],
|
||||
methods: {
|
||||
moveSelectionUp() {
|
||||
if (!this.selectedIndex) {
|
||||
@@ -14,6 +9,7 @@ export default {
|
||||
} else {
|
||||
this.selectedIndex -= 1;
|
||||
}
|
||||
this.adjustScroll();
|
||||
},
|
||||
moveSelectionDown() {
|
||||
if (this.selectedIndex === this.items.length - 1) {
|
||||
@@ -21,19 +17,46 @@ export default {
|
||||
} else {
|
||||
this.selectedIndex += 1;
|
||||
}
|
||||
this.adjustScroll();
|
||||
},
|
||||
processKeyDownEvent(e) {
|
||||
const keyPattern = buildHotKeys(e);
|
||||
if (['arrowup', 'ctrl+p'].includes(keyPattern)) {
|
||||
this.moveSelectionUp();
|
||||
e.preventDefault();
|
||||
} else if (['arrowdown', 'ctrl+n'].includes(keyPattern)) {
|
||||
this.moveSelectionDown();
|
||||
e.preventDefault();
|
||||
} else if (keyPattern === 'enter') {
|
||||
this.onSelect();
|
||||
e.preventDefault();
|
||||
}
|
||||
getKeyboardEvents() {
|
||||
return {
|
||||
ArrowUp: {
|
||||
action: e => {
|
||||
this.moveSelectionUp();
|
||||
e.preventDefault();
|
||||
},
|
||||
allowOnFocusedInput: true,
|
||||
},
|
||||
'Control+KeyP': {
|
||||
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 } from '@vue/test-utils';
|
||||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
|
||||
const buildComponent = ({ data = {}, methods = {} }) => ({
|
||||
render() {},
|
||||
data() {
|
||||
return data;
|
||||
return { ...data, selectedIndex: 0, items: [1, 2, 3] };
|
||||
},
|
||||
methods,
|
||||
mixins: [mentionSelectionKeyboardMixin],
|
||||
methods: { ...methods, onSelect: jest.fn(), adjustScroll: jest.fn() },
|
||||
mixins: [keyboardEventListenerMixins],
|
||||
});
|
||||
|
||||
describe('mentionSelectionKeyboardMixin', () => {
|
||||
test('register listeners', () => {
|
||||
jest.spyOn(document, 'addEventListener');
|
||||
let wrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
const Component = buildComponent({});
|
||||
shallowMount(Component);
|
||||
// undefined expected as the method is not defined in the component
|
||||
expect(document.addEventListener).toHaveBeenCalledWith(
|
||||
'keydown',
|
||||
undefined
|
||||
);
|
||||
wrapper = shallowMount(Component, { localVue });
|
||||
});
|
||||
|
||||
test('processKeyDownEvent updates index on arrow up', () => {
|
||||
const Component = buildComponent({
|
||||
data: { selectedIndex: 0, items: [1, 2, 3] },
|
||||
});
|
||||
const wrapper = shallowMount(Component);
|
||||
wrapper.vm.processKeyDownEvent({
|
||||
ctrlKey: true,
|
||||
key: 'p',
|
||||
preventDefault: jest.fn(),
|
||||
});
|
||||
expect(wrapper.vm.selectedIndex).toBe(2);
|
||||
it('ArrowUp and Control+KeyP update selectedIndex correctly', () => {
|
||||
const preventDefault = jest.fn();
|
||||
const keyboardEvents = wrapper.vm.getKeyboardEvents();
|
||||
|
||||
if (keyboardEvents && keyboardEvents.ArrowUp) {
|
||||
keyboardEvents.ArrowUp.action({ preventDefault });
|
||||
expect(wrapper.vm.selectedIndex).toBe(2);
|
||||
expect(preventDefault).toHaveBeenCalled();
|
||||
}
|
||||
|
||||
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', () => {
|
||||
const Component = buildComponent({
|
||||
data: { selectedIndex: 0, items: [1, 2, 3] },
|
||||
});
|
||||
const wrapper = shallowMount(Component);
|
||||
wrapper.vm.processKeyDownEvent({
|
||||
key: 'ArrowDown',
|
||||
preventDefault: jest.fn(),
|
||||
});
|
||||
expect(wrapper.vm.selectedIndex).toBe(1);
|
||||
it('ArrowDown and Control+KeyN update selectedIndex correctly', () => {
|
||||
const preventDefault = jest.fn();
|
||||
const keyboardEvents = wrapper.vm.getKeyboardEvents();
|
||||
|
||||
if (keyboardEvents && keyboardEvents.ArrowDown) {
|
||||
keyboardEvents.ArrowDown.action({ preventDefault });
|
||||
expect(wrapper.vm.selectedIndex).toBe(1);
|
||||
expect(preventDefault).toHaveBeenCalled();
|
||||
}
|
||||
|
||||
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', () => {
|
||||
const onSelectMockFn = jest.fn();
|
||||
const Component = buildComponent({
|
||||
data: { selectedIndex: 0, items: [1, 2, 3] },
|
||||
methods: { onSelect: () => onSelectMockFn('enterKey pressed') },
|
||||
});
|
||||
const wrapper = shallowMount(Component);
|
||||
wrapper.vm.processKeyDownEvent({
|
||||
key: 'Enter',
|
||||
preventDefault: jest.fn(),
|
||||
});
|
||||
expect(onSelectMockFn).toHaveBeenCalledWith('enterKey pressed');
|
||||
wrapper.vm.onSelect();
|
||||
it('Enter key triggers onSelect method', () => {
|
||||
const preventDefault = jest.fn();
|
||||
const keyboardEvents = wrapper.vm.getKeyboardEvents();
|
||||
|
||||
if (keyboardEvents && keyboardEvents.Enter) {
|
||||
keyboardEvents.Enter.action({ preventDefault });
|
||||
expect(wrapper.vm.onSelect).toHaveBeenCalled();
|
||||
expect(preventDefault).toHaveBeenCalled();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -53,12 +53,6 @@ export const SHORTCUT_KEYS = [
|
||||
firstKey: 'Alt / ⌥',
|
||||
secondKey: 'S',
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
label: 'SWITCH_CONVERSATION_STATUS',
|
||||
firstKey: 'Alt / ⌥',
|
||||
secondKey: 'B',
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
label: 'SWITCH_TO_PRIVATE_NOTE',
|
||||
|
||||
@@ -316,7 +316,6 @@
|
||||
"GO_TO_REPORTS_SIDEBAR": "Go to Reports sidebar",
|
||||
"MOVE_TO_NEXT_TAB": "Move to next tab in conversation list",
|
||||
"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_REPLY": "Switch to Reply",
|
||||
"TOGGLE_SNOOZE_DROPDOWN": "Toggle snooze dropdown"
|
||||
|
||||
@@ -23,12 +23,12 @@
|
||||
|
||||
<script>
|
||||
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
|
||||
import { hasPressedCommandAndEnter } from 'shared/helpers/KeyboardHelpers';
|
||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||
export default {
|
||||
components: {
|
||||
WootMessageEditor,
|
||||
},
|
||||
|
||||
mixins: [keyboardEventListenerMixins],
|
||||
data() {
|
||||
return {
|
||||
noteContent: '',
|
||||
@@ -40,21 +40,14 @@ export default {
|
||||
return this.noteContent === '';
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
document.addEventListener('keydown', this.onMetaEnter);
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
document.removeEventListener('keydown', this.onMetaEnter);
|
||||
},
|
||||
|
||||
methods: {
|
||||
onMetaEnter(e) {
|
||||
if (hasPressedCommandAndEnter(e)) {
|
||||
e.preventDefault();
|
||||
this.onAdd();
|
||||
}
|
||||
getKeyboardEvents() {
|
||||
return {
|
||||
'$mod+Enter': {
|
||||
action: () => this.onAdd(),
|
||||
allowOnFocusedInput: true,
|
||||
},
|
||||
};
|
||||
},
|
||||
onAdd() {
|
||||
if (this.noteContent !== '') {
|
||||
|
||||
@@ -23,6 +23,15 @@
|
||||
:key="element.name"
|
||||
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
|
||||
v-if="element.name === 'conversation_actions'"
|
||||
class="conversation--actions"
|
||||
@@ -145,6 +154,7 @@ import CustomAttributes from './customAttributes/CustomAttributes.vue';
|
||||
import draggable from 'vuedraggable';
|
||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||
import MacrosList from './Macros/List.vue';
|
||||
import Linear from './linear/index.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -157,6 +167,7 @@ export default {
|
||||
ConversationParticipant,
|
||||
draggable,
|
||||
MacrosList,
|
||||
Linear,
|
||||
},
|
||||
mixins: [alertMixin, uiSettingsMixin],
|
||||
props: {
|
||||
|
||||
@@ -50,13 +50,8 @@ import LabelDropdown from 'shared/components/ui/label/LabelDropdown.vue';
|
||||
import AddLabel from 'shared/components/ui/dropdown/AddLabel.vue';
|
||||
import { mixin as clickaway } from 'vue-clickaway';
|
||||
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 {
|
||||
buildHotKeys,
|
||||
isEscape,
|
||||
isActiveElementTypeable,
|
||||
} from 'shared/helpers/KeyboardHelpers';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -65,7 +60,12 @@ export default {
|
||||
AddLabel,
|
||||
},
|
||||
|
||||
mixins: [clickaway, conversationLabelMixin, adminMixin, eventListenerMixins],
|
||||
mixins: [
|
||||
clickaway,
|
||||
conversationLabelMixin,
|
||||
adminMixin,
|
||||
keyboardEventListenerMixins,
|
||||
],
|
||||
props: {
|
||||
conversationId: {
|
||||
type: Number,
|
||||
@@ -93,16 +93,23 @@ export default {
|
||||
closeDropdownLabel() {
|
||||
this.showSearchDropdownLabel = false;
|
||||
},
|
||||
handleKeyEvents(e) {
|
||||
const keyPattern = buildHotKeys(e);
|
||||
|
||||
if (keyPattern === 'l' && !isActiveElementTypeable(e)) {
|
||||
this.toggleLabels();
|
||||
e.preventDefault();
|
||||
} else if (isEscape(e) && this.showSearchDropdownLabel) {
|
||||
this.closeDropdownLabel();
|
||||
e.preventDefault();
|
||||
}
|
||||
getKeyboardEvents() {
|
||||
return {
|
||||
KeyL: {
|
||||
action: e => {
|
||||
e.preventDefault();
|
||||
this.toggleLabels();
|
||||
},
|
||||
},
|
||||
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>
|
||||
|
||||
<script>
|
||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||
import {
|
||||
buildHotKeys,
|
||||
isActiveElementTypeable,
|
||||
} from 'shared/helpers/KeyboardHelpers';
|
||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||
|
||||
export default {
|
||||
name: 'ChatwootSearch',
|
||||
mixins: [eventListenerMixins],
|
||||
mixins: [keyboardEventListenerMixins],
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
@@ -71,13 +67,15 @@ export default {
|
||||
onBlur() {
|
||||
this.isInputFocused = false;
|
||||
},
|
||||
handleKeyEvents(e) {
|
||||
const keyPattern = buildHotKeys(e);
|
||||
|
||||
if (keyPattern === '/' && !isActiveElementTypeable(e)) {
|
||||
e.preventDefault();
|
||||
this.$refs.searchInput.focus();
|
||||
}
|
||||
getKeyboardEvents() {
|
||||
return {
|
||||
Slash: {
|
||||
action: e => {
|
||||
e.preventDefault();
|
||||
this.$refs.searchInput.focus();
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
+11
-14
@@ -35,10 +35,7 @@
|
||||
<script>
|
||||
import { debounce } from '@chatwoot/utils';
|
||||
import { mixin as clickaway } from 'vue-clickaway';
|
||||
import {
|
||||
isEscape,
|
||||
isActiveElementTypeable,
|
||||
} from 'shared/helpers/KeyboardHelpers';
|
||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||
|
||||
import SearchHeader from './Header.vue';
|
||||
import SearchResults from './SearchResults.vue';
|
||||
@@ -55,7 +52,7 @@ export default {
|
||||
SearchResults,
|
||||
ArticleView,
|
||||
},
|
||||
mixins: [clickaway, portalMixin, alertMixin],
|
||||
mixins: [clickaway, portalMixin, alertMixin, keyboardEventListenerMixins],
|
||||
props: {
|
||||
selectedPortalSlug: {
|
||||
type: String,
|
||||
@@ -97,10 +94,6 @@ export default {
|
||||
mounted() {
|
||||
this.fetchArticlesByQuery(this.searchQuery);
|
||||
this.debounceSearch = debounce(this.fetchArticlesByQuery, 500, false);
|
||||
document.body.addEventListener('keydown', this.closeOnEsc);
|
||||
},
|
||||
beforeDestroy() {
|
||||
document.body.removeEventListener('keydown', this.closeOnEsc);
|
||||
},
|
||||
methods: {
|
||||
generateArticleUrl(article) {
|
||||
@@ -158,11 +151,15 @@ export default {
|
||||
);
|
||||
this.onClose();
|
||||
},
|
||||
closeOnEsc(e) {
|
||||
if (isEscape(e) && !isActiveElementTypeable(e)) {
|
||||
e.preventDefault();
|
||||
this.onClose();
|
||||
}
|
||||
getKeyboardEvents() {
|
||||
return {
|
||||
Escape: {
|
||||
action: () => {
|
||||
this.onClose();
|
||||
},
|
||||
allowOnFocusedInput: true,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -109,9 +109,10 @@ export default {
|
||||
generateArticleUrl(article) {
|
||||
return `/hc/${article.portal.slug}/articles/${article.slug}`;
|
||||
},
|
||||
handleKeyboardEvent(e) {
|
||||
this.processKeyDownEvent(e);
|
||||
this.$el.scrollTop = 102 * this.selectedIndex;
|
||||
adjustScroll() {
|
||||
this.$nextTick(() => {
|
||||
this.$el.scrollTop = 102 * this.selectedIndex;
|
||||
});
|
||||
},
|
||||
prepareContent(content) {
|
||||
return this.highlightContent(
|
||||
|
||||
@@ -8,16 +8,12 @@
|
||||
</ul>
|
||||
</template>
|
||||
<script>
|
||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||
import {
|
||||
hasPressedArrowUpKey,
|
||||
hasPressedArrowDownKey,
|
||||
} from 'shared/helpers/KeyboardHelpers';
|
||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||
export default {
|
||||
name: 'WootDropdownMenu',
|
||||
componentName: 'WootDropdownMenu',
|
||||
|
||||
mixins: [eventListenerMixins],
|
||||
mixins: [keyboardEventListenerMixins],
|
||||
|
||||
props: {
|
||||
placement: {
|
||||
@@ -31,38 +27,46 @@ export default {
|
||||
'ul.dropdown li.dropdown-menu__item .button'
|
||||
);
|
||||
},
|
||||
activeElementIndex() {
|
||||
const menuButtons = this.dropdownMenuButtons();
|
||||
getActiveButtonIndex(menuButtons) {
|
||||
const focusedButton = this.$refs.dropdownMenu.querySelector(
|
||||
'ul.dropdown li.dropdown-menu__item .button:focus'
|
||||
);
|
||||
const activeIndex = [...menuButtons].indexOf(focusedButton);
|
||||
return activeIndex;
|
||||
return Array.from(menuButtons).indexOf(focusedButton);
|
||||
},
|
||||
handleKeyEvents(e) {
|
||||
getKeyboardEvents() {
|
||||
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 (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();
|
||||
}
|
||||
}
|
||||
menuButtons[newIndex].focus();
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
export const isEnter = e => {
|
||||
return e.keyCode === 13;
|
||||
return e.key === 'Enter';
|
||||
};
|
||||
|
||||
export const isEscape = e => {
|
||||
return e.keyCode === 27;
|
||||
return e.key === 'Escape';
|
||||
};
|
||||
|
||||
export const hasPressedShift = e => {
|
||||
@@ -19,118 +19,7 @@ export const hasPressedEnterAndNotCmdOrShift = e => {
|
||||
};
|
||||
|
||||
export const hasPressedCommandAndEnter = e => {
|
||||
return e.metaKey && e.keyCode === 13;
|
||||
};
|
||||
|
||||
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;
|
||||
return hasPressedCommand(e) && isEnter(e);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,20 +3,19 @@ import {
|
||||
isEscape,
|
||||
hasPressedShift,
|
||||
hasPressedCommand,
|
||||
buildHotKeys,
|
||||
isActiveElementTypeable,
|
||||
} from '../KeyboardHelpers';
|
||||
|
||||
describe('#KeyboardHelpers', () => {
|
||||
describe('#isEnter', () => {
|
||||
it('return correct values', () => {
|
||||
expect(isEnter({ keyCode: 13 })).toEqual(true);
|
||||
expect(isEnter({ key: 'Enter' })).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#isEscape', () => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
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', () => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
<script>
|
||||
import { buildHotKeys } from 'shared/helpers/KeyboardHelpers';
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||
import Header from '../../components/playground/Header.vue';
|
||||
import UserMessage from '../../components/playground/UserMessage.vue';
|
||||
import BotMessage from '../../components/playground/BotMessage.vue';
|
||||
@@ -47,7 +47,7 @@ export default {
|
||||
BotMessage,
|
||||
TypingIndicator,
|
||||
},
|
||||
mixins: [messageFormatterMixin],
|
||||
mixins: [messageFormatterMixin, keyboardEventListenerMixins],
|
||||
props: {
|
||||
componentData: {
|
||||
type: Object,
|
||||
@@ -67,17 +67,17 @@ export default {
|
||||
},
|
||||
mounted() {
|
||||
this.focusInput();
|
||||
document.addEventListener('keydown', this.handleKeyEvents);
|
||||
},
|
||||
beforeDestroy() {
|
||||
document.removeEventListener('keydown', this.handleKeyEvents);
|
||||
},
|
||||
methods: {
|
||||
handleKeyEvents(e) {
|
||||
const keyCode = buildHotKeys(e);
|
||||
if (['meta+enter', 'ctrl+enter'].includes(keyCode)) {
|
||||
this.onMessageSend();
|
||||
}
|
||||
getKeyboardEvents() {
|
||||
return {
|
||||
'$mod+Enter': {
|
||||
action: () => {
|
||||
this.onMessageSend();
|
||||
},
|
||||
allowOnFocusedInput: true,
|
||||
},
|
||||
};
|
||||
},
|
||||
focusInput() {
|
||||
this.$refs.messageInput.focus();
|
||||
|
||||
@@ -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 => {
|
||||
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() {
|
||||
// This function is used to get all the items in the dropdown.
|
||||
if (!this.showDropdown) return [];
|
||||
return Array.from(
|
||||
this.$refs.dropdown.querySelectorAll(
|
||||
this.$refs.dropdown?.querySelectorAll(
|
||||
'div.country-dropdown div.country-dropdown--item'
|
||||
)
|
||||
);
|
||||
},
|
||||
focusedOrActiveItem(className) {
|
||||
// This function is used to get the focused or active item in the dropdown.
|
||||
if (!this.showDropdown) return [];
|
||||
return Array.from(
|
||||
this.$refs.dropdown.querySelectorAll(
|
||||
this.$refs.dropdown?.querySelectorAll(
|
||||
`div.country-dropdown div.country-dropdown--item.${className}`
|
||||
)
|
||||
);
|
||||
},
|
||||
handleKeyboardEvent(e) {
|
||||
if (this.showDropdown) {
|
||||
this.processKeyDownEvent(e);
|
||||
adjustScroll() {
|
||||
this.$nextTick(() => {
|
||||
this.scrollToFocusedOrActiveItem(this.focusedOrActiveItem('focus'));
|
||||
}
|
||||
});
|
||||
},
|
||||
onSelect() {
|
||||
this.onSelectCountry(this.items[this.selectedIndex]);
|
||||
|
||||
@@ -310,5 +310,4 @@ class Conversation < ApplicationRecord
|
||||
end
|
||||
|
||||
Conversation.include_mod_with('Concerns::Conversation')
|
||||
Conversation.include_mod_with('SentimentAnalysisHelper')
|
||||
Conversation.prepend_mod_with('Conversation')
|
||||
|
||||
@@ -236,6 +236,7 @@ class Message < ApplicationRecord
|
||||
# there are cases where automations can result in message loops, we need to prevent such cases.
|
||||
if conversation.messages.where('created_at >= ?', 1.minute.ago).count >= Limits.conversation_message_per_minute_limit
|
||||
Rails.logger.error "Too many message: Account Id - #{account_id} : Conversation id - #{conversation_id}"
|
||||
errors.add(:base, 'Too many messages')
|
||||
end
|
||||
end
|
||||
|
||||
@@ -268,7 +269,6 @@ class Message < ApplicationRecord
|
||||
reopen_conversation
|
||||
notify_via_mail
|
||||
set_conversation_activity
|
||||
update_message_sentiments
|
||||
dispatch_create_events
|
||||
send_reply
|
||||
execute_message_template_hooks
|
||||
@@ -405,10 +405,6 @@ class Message < ApplicationRecord
|
||||
conversation.update_columns(last_activity_at: created_at)
|
||||
# rubocop:enable Rails/SkipsModelValidations
|
||||
end
|
||||
|
||||
def update_message_sentiments
|
||||
# override in the enterprise ::Enterprise::SentimentAnalysisJob.perform_later(self)
|
||||
end
|
||||
end
|
||||
|
||||
Message.prepend_mod_with('Message')
|
||||
|
||||
@@ -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
|
||||
|
||||
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/articles', to: 'public/api/v1/portals/articles#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?
|
||||
end
|
||||
|
||||
def self.help_center_root
|
||||
ENV.fetch('HELPCENTER_URL', nil) || ENV.fetch('FRONTEND_URL', nil)
|
||||
end
|
||||
|
||||
def self.extensions
|
||||
if custom?
|
||||
%w[enterprise custom]
|
||||
|
||||
+3
-1
@@ -35,6 +35,7 @@
|
||||
"@chatwoot/utils": "^0.0.23",
|
||||
"@hcaptcha/vue-hcaptcha": "^0.3.2",
|
||||
"@june-so/analytics-next": "^2.0.0",
|
||||
"@linear/sdk": "^20.0.0",
|
||||
"@radix-ui/colors": "^1.0.1",
|
||||
"@rails/actioncable": "6.1.3",
|
||||
"@rails/ujs": "^7.0.3-1",
|
||||
@@ -70,6 +71,7 @@
|
||||
"postcss-loader": "^4.2.0",
|
||||
"semver": "7.5.3",
|
||||
"tailwindcss": "^3.3.2",
|
||||
"tinykeys": "^2.1.0",
|
||||
"turbolinks": "^5.2.0",
|
||||
"url-loader": "^2.0.0",
|
||||
"urlpattern-polyfill": "^6.0.2",
|
||||
@@ -166,4 +168,4 @@
|
||||
"scss-lint"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
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!(:agent) { create(:user, account: account, role: :agent) }
|
||||
let!(:portal) { create(:portal, slug: 'test-portal', account_id: account.id, custom_domain: 'www.example.com') }
|
||||
|
||||
before do
|
||||
create(:portal, slug: 'test-portal-1', 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
|
||||
|
||||
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"
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
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
|
||||
let(:account) { create(: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
|
||||
+3
-171
@@ -1,17 +1,5 @@
|
||||
const {
|
||||
blue,
|
||||
blueDark,
|
||||
green,
|
||||
greenDark,
|
||||
yellow,
|
||||
yellowDark,
|
||||
slate,
|
||||
slateDark,
|
||||
red,
|
||||
redDark,
|
||||
violet,
|
||||
violetDark,
|
||||
} = require('@radix-ui/colors');
|
||||
const { slateDark } = require('@radix-ui/colors');
|
||||
import { colors } from './theme/colors';
|
||||
const defaultTheme = require('tailwindcss/defaultTheme');
|
||||
module.exports = {
|
||||
darkMode: 'class',
|
||||
@@ -41,163 +29,7 @@ module.exports = {
|
||||
'modal-backdrop-light': 'rgba(0, 0, 0, 0.4)',
|
||||
'modal-backdrop-dark': 'rgba(0, 0, 0, 0.6)',
|
||||
current: 'currentColor',
|
||||
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>)',
|
||||
},
|
||||
...colors,
|
||||
body: slateDark.slate7,
|
||||
},
|
||||
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"
|
||||
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":
|
||||
version "0.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@hcaptcha/vue-hcaptcha/-/vue-hcaptcha-0.3.2.tgz#0f77d6fc19bc47eadb6b2181eee5fc132441a942"
|
||||
@@ -3872,6 +3877,15 @@
|
||||
typescript "^4.9.5"
|
||||
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":
|
||||
version "1.1.1"
|
||||
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"
|
||||
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:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e"
|
||||
@@ -19615,6 +19634,11 @@ tinycolor2@^1.1.2:
|
||||
resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.4.2.tgz#3f6a4d1071ad07676d7fa472e1fac40a719d8803"
|
||||
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:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/titleize/-/titleize-3.0.0.tgz#71c12eb7fdd2558aa8a44b0be83b8a76694acd53"
|
||||
|
||||
Reference in New Issue
Block a user