Compare commits
18
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e6346326d | ||
|
|
86d37622c8 | ||
|
|
99c699ea34 | ||
|
|
a3290bfd42 | ||
|
|
ccb117d36a | ||
|
|
10a0333980 | ||
|
|
9a405d65ba | ||
|
|
1b0e94ec95 | ||
|
|
aebcbb63e4 | ||
|
|
472f6d9345 | ||
|
|
2ce7c8b845 | ||
|
|
a11746106f | ||
|
|
1f75bed30f | ||
|
|
2989fab4d1 | ||
|
|
bf3eace7a2 | ||
|
|
226d0db11c | ||
|
|
82c73f18d9 | ||
|
|
16432a6ebc |
+58
-58
@@ -33,70 +33,70 @@ GIT
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
actioncable (7.0.8.5)
|
||||
actionpack (= 7.0.8.5)
|
||||
activesupport (= 7.0.8.5)
|
||||
actioncable (7.0.8.7)
|
||||
actionpack (= 7.0.8.7)
|
||||
activesupport (= 7.0.8.7)
|
||||
nio4r (~> 2.0)
|
||||
websocket-driver (>= 0.6.1)
|
||||
actionmailbox (7.0.8.5)
|
||||
actionpack (= 7.0.8.5)
|
||||
activejob (= 7.0.8.5)
|
||||
activerecord (= 7.0.8.5)
|
||||
activestorage (= 7.0.8.5)
|
||||
activesupport (= 7.0.8.5)
|
||||
actionmailbox (7.0.8.7)
|
||||
actionpack (= 7.0.8.7)
|
||||
activejob (= 7.0.8.7)
|
||||
activerecord (= 7.0.8.7)
|
||||
activestorage (= 7.0.8.7)
|
||||
activesupport (= 7.0.8.7)
|
||||
mail (>= 2.7.1)
|
||||
net-imap
|
||||
net-pop
|
||||
net-smtp
|
||||
actionmailer (7.0.8.5)
|
||||
actionpack (= 7.0.8.5)
|
||||
actionview (= 7.0.8.5)
|
||||
activejob (= 7.0.8.5)
|
||||
activesupport (= 7.0.8.5)
|
||||
actionmailer (7.0.8.7)
|
||||
actionpack (= 7.0.8.7)
|
||||
actionview (= 7.0.8.7)
|
||||
activejob (= 7.0.8.7)
|
||||
activesupport (= 7.0.8.7)
|
||||
mail (~> 2.5, >= 2.5.4)
|
||||
net-imap
|
||||
net-pop
|
||||
net-smtp
|
||||
rails-dom-testing (~> 2.0)
|
||||
actionpack (7.0.8.5)
|
||||
actionview (= 7.0.8.5)
|
||||
activesupport (= 7.0.8.5)
|
||||
actionpack (7.0.8.7)
|
||||
actionview (= 7.0.8.7)
|
||||
activesupport (= 7.0.8.7)
|
||||
rack (~> 2.0, >= 2.2.4)
|
||||
rack-test (>= 0.6.3)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
||||
actiontext (7.0.8.5)
|
||||
actionpack (= 7.0.8.5)
|
||||
activerecord (= 7.0.8.5)
|
||||
activestorage (= 7.0.8.5)
|
||||
activesupport (= 7.0.8.5)
|
||||
actiontext (7.0.8.7)
|
||||
actionpack (= 7.0.8.7)
|
||||
activerecord (= 7.0.8.7)
|
||||
activestorage (= 7.0.8.7)
|
||||
activesupport (= 7.0.8.7)
|
||||
globalid (>= 0.6.0)
|
||||
nokogiri (>= 1.8.5)
|
||||
actionview (7.0.8.5)
|
||||
activesupport (= 7.0.8.5)
|
||||
actionview (7.0.8.7)
|
||||
activesupport (= 7.0.8.7)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
||||
active_record_query_trace (1.8)
|
||||
activejob (7.0.8.5)
|
||||
activesupport (= 7.0.8.5)
|
||||
activejob (7.0.8.7)
|
||||
activesupport (= 7.0.8.7)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (7.0.8.5)
|
||||
activesupport (= 7.0.8.5)
|
||||
activerecord (7.0.8.5)
|
||||
activemodel (= 7.0.8.5)
|
||||
activesupport (= 7.0.8.5)
|
||||
activemodel (7.0.8.7)
|
||||
activesupport (= 7.0.8.7)
|
||||
activerecord (7.0.8.7)
|
||||
activemodel (= 7.0.8.7)
|
||||
activesupport (= 7.0.8.7)
|
||||
activerecord-import (1.4.1)
|
||||
activerecord (>= 4.2)
|
||||
activestorage (7.0.8.5)
|
||||
actionpack (= 7.0.8.5)
|
||||
activejob (= 7.0.8.5)
|
||||
activerecord (= 7.0.8.5)
|
||||
activesupport (= 7.0.8.5)
|
||||
activestorage (7.0.8.7)
|
||||
actionpack (= 7.0.8.7)
|
||||
activejob (= 7.0.8.7)
|
||||
activerecord (= 7.0.8.7)
|
||||
activesupport (= 7.0.8.7)
|
||||
marcel (~> 1.0)
|
||||
mini_mime (>= 1.1.0)
|
||||
activesupport (7.0.8.5)
|
||||
activesupport (7.0.8.7)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
i18n (>= 1.6, < 2)
|
||||
minitest (>= 5.1)
|
||||
@@ -472,7 +472,7 @@ GEM
|
||||
mini_magick (4.12.0)
|
||||
mini_mime (1.1.5)
|
||||
mini_portile2 (2.8.8)
|
||||
minitest (5.25.1)
|
||||
minitest (5.25.4)
|
||||
mock_redis (0.36.0)
|
||||
ruby2_keywords
|
||||
msgpack (1.7.0)
|
||||
@@ -502,14 +502,14 @@ GEM
|
||||
newrelic_rpm (9.6.0)
|
||||
base64
|
||||
nio4r (2.7.3)
|
||||
nokogiri (1.16.8)
|
||||
nokogiri (1.17.1)
|
||||
mini_portile2 (~> 2.8.2)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.16.8-arm64-darwin)
|
||||
nokogiri (1.17.1-arm64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.16.8-x86_64-darwin)
|
||||
nokogiri (1.17.1-x86_64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.16.8-x86_64-linux)
|
||||
nokogiri (1.17.1-x86_64-linux)
|
||||
racc (~> 1.4)
|
||||
oauth (1.1.0)
|
||||
oauth-tty (~> 1.0, >= 1.0.1)
|
||||
@@ -581,20 +581,20 @@ GEM
|
||||
rack-test (2.1.0)
|
||||
rack (>= 1.3)
|
||||
rack-timeout (0.6.3)
|
||||
rails (7.0.8.5)
|
||||
actioncable (= 7.0.8.5)
|
||||
actionmailbox (= 7.0.8.5)
|
||||
actionmailer (= 7.0.8.5)
|
||||
actionpack (= 7.0.8.5)
|
||||
actiontext (= 7.0.8.5)
|
||||
actionview (= 7.0.8.5)
|
||||
activejob (= 7.0.8.5)
|
||||
activemodel (= 7.0.8.5)
|
||||
activerecord (= 7.0.8.5)
|
||||
activestorage (= 7.0.8.5)
|
||||
activesupport (= 7.0.8.5)
|
||||
rails (7.0.8.7)
|
||||
actioncable (= 7.0.8.7)
|
||||
actionmailbox (= 7.0.8.7)
|
||||
actionmailer (= 7.0.8.7)
|
||||
actionpack (= 7.0.8.7)
|
||||
actiontext (= 7.0.8.7)
|
||||
actionview (= 7.0.8.7)
|
||||
activejob (= 7.0.8.7)
|
||||
activemodel (= 7.0.8.7)
|
||||
activerecord (= 7.0.8.7)
|
||||
activestorage (= 7.0.8.7)
|
||||
activesupport (= 7.0.8.7)
|
||||
bundler (>= 1.15.0)
|
||||
railties (= 7.0.8.5)
|
||||
railties (= 7.0.8.7)
|
||||
rails-dom-testing (2.2.0)
|
||||
activesupport (>= 5.0.0)
|
||||
minitest
|
||||
@@ -602,9 +602,9 @@ GEM
|
||||
rails-html-sanitizer (1.6.1)
|
||||
loofah (~> 2.21)
|
||||
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
|
||||
railties (7.0.8.5)
|
||||
actionpack (= 7.0.8.5)
|
||||
activesupport (= 7.0.8.5)
|
||||
railties (7.0.8.7)
|
||||
actionpack (= 7.0.8.7)
|
||||
activesupport (= 7.0.8.7)
|
||||
method_source
|
||||
rake (>= 12.2)
|
||||
thor (~> 1.0)
|
||||
|
||||
@@ -2,10 +2,22 @@ class Api::V1::Accounts::Integrations::CaptainController < Api::V1::Accounts::Ba
|
||||
before_action :hook
|
||||
|
||||
def proxy
|
||||
request_url = build_request_url(request_path)
|
||||
response = HTTParty.send(request_method, request_url, body: permitted_params[:body].to_json, headers: headers)
|
||||
render plain: response.body, status: response.code
|
||||
end
|
||||
|
||||
def copilot
|
||||
request_url = build_request_url(build_request_path("/assistants/#{hook.settings['assistant_id']}/copilot"))
|
||||
params = {
|
||||
previous_messages: copilot_params[:previous_messages],
|
||||
conversation_history: conversation_history,
|
||||
message: copilot_params[:message]
|
||||
}
|
||||
response = HTTParty.send(:post, request_url, body: params.to_json, headers: headers)
|
||||
render plain: response.body, status: response.code
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def headers
|
||||
@@ -17,15 +29,19 @@ class Api::V1::Accounts::Integrations::CaptainController < Api::V1::Accounts::Ba
|
||||
}
|
||||
end
|
||||
|
||||
def build_request_path(route)
|
||||
"api/accounts/#{hook.settings['account_id']}#{route}"
|
||||
end
|
||||
|
||||
def request_path
|
||||
request_route = with_leading_hash_on_route(params[:route])
|
||||
|
||||
return 'api/sessions/profile' if request_route == '/sessions/profile'
|
||||
|
||||
"api/accounts/#{hook.settings['account_id']}#{request_route}"
|
||||
build_request_path(request_route)
|
||||
end
|
||||
|
||||
def request_url
|
||||
def build_request_url(request_path)
|
||||
base_url = InstallationConfig.find_by(name: 'CAPTAIN_API_URL').value
|
||||
URI.join(base_url, request_path).to_s
|
||||
end
|
||||
@@ -47,6 +63,15 @@ class Api::V1::Accounts::Integrations::CaptainController < Api::V1::Accounts::Ba
|
||||
request_route.start_with?('/') ? request_route : "/#{request_route}"
|
||||
end
|
||||
|
||||
def conversation_history
|
||||
conversation = Current.account.conversations.find_by!(display_id: copilot_params[:conversation_id])
|
||||
conversation.to_llm_text
|
||||
end
|
||||
|
||||
def copilot_params
|
||||
params.permit(:previous_messages, :conversation_id, :message)
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:method, :route, body: {})
|
||||
end
|
||||
|
||||
@@ -36,6 +36,10 @@ class IntegrationsAPI extends ApiClient {
|
||||
requestCaptain(body) {
|
||||
return axios.post(`${this.baseUrl()}/integrations/captain/proxy`, body);
|
||||
}
|
||||
|
||||
requestCaptainCopilot(body) {
|
||||
return axios.post(`${this.baseUrl()}/integrations/captain/copilot`, body);
|
||||
}
|
||||
}
|
||||
|
||||
export default new IntegrationsAPI();
|
||||
|
||||
@@ -72,6 +72,19 @@
|
||||
--slate-11: 96 100 108;
|
||||
--slate-12: 28 32 36;
|
||||
|
||||
--iris-1: 253 253 255;
|
||||
--iris-2: 248 248 255;
|
||||
--iris-3: 240 241 254;
|
||||
--iris-4: 230 231 255;
|
||||
--iris-5: 218 220 255;
|
||||
--iris-6: 203 205 255;
|
||||
--iris-7: 184 186 248;
|
||||
--iris-8: 155 158 240;
|
||||
--iris-9: 91 91 214;
|
||||
--iris-10: 81 81 205;
|
||||
--iris-11: 87 83 198;
|
||||
--iris-12: 39 41 98;
|
||||
|
||||
--ruby-1: 255 252 253;
|
||||
--ruby-2: 255 247 248;
|
||||
--ruby-3: 254 234 237;
|
||||
@@ -147,6 +160,19 @@
|
||||
--slate-11: 176 180 186;
|
||||
--slate-12: 237 238 240;
|
||||
|
||||
--iris-1: 19 19 30;
|
||||
--iris-2: 23 22 37;
|
||||
--iris-3: 32 34 72;
|
||||
--iris-4: 38 42 101;
|
||||
--iris-5: 48 51 116;
|
||||
--iris-6: 61 62 130;
|
||||
--iris-7: 74 74 149;
|
||||
--iris-8: 89 88 177;
|
||||
--iris-9: 91 91 214;
|
||||
--iris-10: 84 114 228;
|
||||
--iris-11: 158 177 255;
|
||||
--iris-12: 224 223 254;
|
||||
|
||||
--ruby-1: 25 17 19;
|
||||
--ruby-2: 30 21 23;
|
||||
--ruby-3: 58 20 30;
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
}
|
||||
|
||||
&.multiselect--disabled {
|
||||
@apply opacity-50 border border-slate-200 dark:border-slate-600 rounded-md cursor-not-allowed;
|
||||
@apply opacity-50 border border-n-weak rounded-md cursor-not-allowed;
|
||||
|
||||
.multiselect__select {
|
||||
@apply cursor-not-allowed bg-white dark:bg-slate-900 rounded-md;
|
||||
@@ -44,7 +44,7 @@
|
||||
}
|
||||
|
||||
.multiselect__content-wrapper {
|
||||
@apply bg-white dark:bg-slate-900 border border-solid border-slate-200 dark:border-slate-600 text-slate-800 dark:text-slate-100;
|
||||
@apply bg-white dark:bg-slate-900 border border-solid border-n-weak text-slate-800 dark:text-slate-100;
|
||||
}
|
||||
|
||||
.multiselect__content {
|
||||
@@ -96,7 +96,7 @@
|
||||
}
|
||||
|
||||
.multiselect__tags {
|
||||
@apply bg-white dark:bg-slate-900 border border-solid border-slate-200 dark:border-slate-600 m-0 min-h-[2.875rem] pt-0;
|
||||
@apply bg-n-slate-1 border border-solid border-n-weak m-0 min-h-[2.875rem] pt-0;
|
||||
|
||||
input {
|
||||
@apply border-0 border-none;
|
||||
@@ -121,7 +121,7 @@
|
||||
|
||||
.multiselect__input {
|
||||
@include ghost-input;
|
||||
@apply text-sm h-[2.875rem] mb-0 p-0;
|
||||
@apply text-sm !h-[2.375rem] mb-0 !py-0 ;
|
||||
}
|
||||
|
||||
.multiselect__single {
|
||||
@@ -179,7 +179,7 @@
|
||||
.multiselect__tags,
|
||||
.multiselect__input,
|
||||
.multiselect {
|
||||
@apply bg-white dark:bg-slate-900 text-slate-800 dark:text-slate-100 rounded-[5px] text-sm min-h-[2.5rem];
|
||||
@apply bg-white dark:bg-slate-900 text-slate-800 dark:text-slate-100 rounded-lg text-sm min-h-[2.5rem];
|
||||
}
|
||||
|
||||
.multiselect__input {
|
||||
|
||||
@@ -74,7 +74,10 @@ input[type='password']:not(.reset-base),
|
||||
input[type='date']:not(.reset-base),
|
||||
input[type='email']:not(.reset-base),
|
||||
input[type='url']:not(.reset-base) {
|
||||
@apply block box-border w-full transition-colors focus:border-woot-500 dark:focus:border-woot-600 duration-[0.25s] ease-[ease-in-out] h-10 appearance-none mx-0 mt-0 mb-4 p-2 rounded-md text-base font-normal bg-white dark:bg-slate-900 focus:bg-white focus:dark:bg-slate-900 text-slate-900 dark:text-slate-100 border border-solid border-slate-200 dark:border-slate-600;
|
||||
@apply block box-border w-full transition-colors;
|
||||
@apply focus:border-woot-500 dark:focus:border-woot-600 duration-[0.25s] ease-[ease-in-out];
|
||||
@apply h-10 appearance-none mx-0 mt-0 mb-4 p-2 rounded-md font-normal text-sm;
|
||||
@apply bg-n-slate-1 text-n-slate-12 border border-n-weak hover:border-n-slate-6 dark:hover:border-n-slate-6 focus:border-n-brand dark:focus:border-n-brand;
|
||||
|
||||
&[disabled] {
|
||||
@apply bg-slate-200 dark:bg-slate-700 text-slate-400 dark:text-slate-400 border-slate-200 dark:border-slate-600 cursor-not-allowed;
|
||||
@@ -90,7 +93,9 @@ select {
|
||||
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' version='1.1' width='32' height='24' viewBox='0 0 32 24'><polygon points='0,0 32,0 16,24' style='fill: rgb%28110, 111, 115%29'></polygon></svg>");
|
||||
background-position: right -1rem center;
|
||||
background-size: 9px 6px;
|
||||
@apply h-10 mx-0 mt-0 mb-4 bg-origin-content focus-visible:outline-none bg-no-repeat py-2 pr-6 pl-2 rounded-md w-full text-base font-normal appearance-none transition-colors focus:border-woot-500 dark:focus:border-woot-600 duration-[0.25s] ease-[ease-in-out] bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-100 border border-solid border-slate-200 dark:border-slate-600;
|
||||
@apply h-10 mx-0 mt-0 mb-4 bg-origin-content focus-visible:outline-none bg-no-repeat py-2 pr-6 pl-2;
|
||||
@apply rounded-md w-full text-base font-normal appearance-none transition-colors;
|
||||
@apply text-sm duration-[0.25s] ease-[ease-in-out] bg-n-slate-1 text-n-slate-12 border border-n-weak hover:border-n-slate-6 dark:hover:border-n-slate-6 focus:border-n-brand dark:focus:border-n-brand;
|
||||
}
|
||||
|
||||
// Textarea
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
}
|
||||
|
||||
.tabs--container--with-border {
|
||||
@apply border-b border-slate-50 dark:border-slate-800/50;
|
||||
@apply border-b border-n-weak;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
@@ -19,22 +19,12 @@
|
||||
@apply items-center rounded-none cursor-pointer flex h-auto justify-center min-w-[2rem];
|
||||
}
|
||||
|
||||
// Tab chat type
|
||||
.tab--chat-type {
|
||||
@apply flex;
|
||||
|
||||
.tabs-title {
|
||||
a {
|
||||
@apply text-base font-medium py-3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tabs-title {
|
||||
@apply flex-shrink-0 my-0 mx-2;
|
||||
|
||||
.badge {
|
||||
@apply bg-slate-50 dark:bg-slate-800 rounded-md text-slate-600 dark:text-slate-100 h-5 flex items-center justify-center text-xxs font-semibold my-0 mx-1 px-1 py-0;
|
||||
@apply bg-n-slate-3 rounded-xl text-n-slate-11 h-5 flex items-center justify-center text-xxs font-semibold my-0 mx-1 px-1 py-0;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
@@ -48,22 +38,22 @@
|
||||
&:hover,
|
||||
&:focus {
|
||||
a {
|
||||
@apply text-slate-800 dark:text-slate-100;
|
||||
@apply text-n-slate-11;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
@apply flex items-center flex-row border-b py-2.5 select-none cursor-pointer border-transparent text-slate-500 dark:text-slate-200 text-sm top-[1px] relative;
|
||||
@apply flex items-center flex-row border-b py-2.5 select-none cursor-pointer border-transparent text-n-slate-10 text-sm top-[1px] relative;
|
||||
transition: border-color 0.15s $swift-ease-out-function;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
a {
|
||||
@apply border-b border-woot-500 text-woot-500 dark:text-woot-500;
|
||||
@apply border-b border-n-brand text-n-brand;
|
||||
}
|
||||
|
||||
.badge {
|
||||
@apply bg-woot-50 dark:bg-woot-500 text-woot-500 dark:text-woot-50 dark:bg-opacity-40;
|
||||
@apply bg-n-slate-3 text-n-brand;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import CardLayout from 'dashboard/components-next/CardLayout.vue';
|
||||
import ContactsForm from 'dashboard/components-next/Contacts/ContactsForm/ContactsForm.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import Avatar from 'dashboard/components-next/avatar/Avatar.vue';
|
||||
import Flag from 'dashboard/components-next/flag/Flag.vue';
|
||||
import countries from 'shared/constants/countries';
|
||||
|
||||
const props = defineProps({
|
||||
@@ -56,13 +57,19 @@ const countryDetails = computed(() => {
|
||||
|
||||
if (!activeCountry) return null;
|
||||
|
||||
const parts = [
|
||||
activeCountry.emoji,
|
||||
city ? `${city},` : null,
|
||||
activeCountry.name,
|
||||
].filter(Boolean);
|
||||
return {
|
||||
countryCode: activeCountry.id,
|
||||
city: city ? `${city},` : null,
|
||||
name: activeCountry.name,
|
||||
};
|
||||
});
|
||||
|
||||
return parts.length ? parts.join(' ') : null;
|
||||
const formattedLocation = computed(() => {
|
||||
if (!countryDetails.value) return '';
|
||||
|
||||
return [countryDetails.value.city, countryDetails.value.name]
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
});
|
||||
|
||||
const handleFormUpdate = updatedData => {
|
||||
@@ -114,8 +121,12 @@ const onClickViewDetails = () => emit('showContact', props.id);
|
||||
{{ phoneNumber }}
|
||||
</span>
|
||||
<div v-if="phoneNumber" class="w-px h-3 truncate bg-n-slate-6" />
|
||||
<span v-if="countryDetails" class="text-sm truncate text-n-slate-11">
|
||||
{{ countryDetails }}
|
||||
<span
|
||||
v-if="countryDetails"
|
||||
class="inline-flex items-center gap-2 text-sm truncate text-n-slate-11"
|
||||
>
|
||||
<Flag :country="countryDetails.countryCode" class="size-3.5" />
|
||||
{{ formattedLocation }}
|
||||
</span>
|
||||
<div v-if="countryDetails" class="w-px h-3 truncate bg-n-slate-6" />
|
||||
<Button
|
||||
|
||||
+7
-2
@@ -106,10 +106,15 @@ const onImport = async file => {
|
||||
try {
|
||||
await store.dispatch('contacts/import', file);
|
||||
contactImportDialogRef.value?.dialogRef.close();
|
||||
useAlert(t('IMPORT_CONTACTS.SUCCESS_MESSAGE'));
|
||||
useAlert(
|
||||
t('CONTACTS_LAYOUT.HEADER.ACTIONS.IMPORT_CONTACT.SUCCESS_MESSAGE')
|
||||
);
|
||||
useTrack(CONTACTS_EVENTS.IMPORT_SUCCESS);
|
||||
} catch (error) {
|
||||
useAlert(error.message ?? t('IMPORT_CONTACTS.ERROR_MESSAGE'));
|
||||
useAlert(
|
||||
error.message ??
|
||||
t('CONTACTS_LAYOUT.HEADER.ACTIONS.IMPORT_CONTACT.ERROR_MESSAGE')
|
||||
);
|
||||
useTrack(CONTACTS_EVENTS.IMPORT_FAILURE);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import Copilot from './Copilot.vue';
|
||||
|
||||
const supportAgent = {
|
||||
available_name: 'Pranav Raj',
|
||||
avatar_url:
|
||||
'https://app.chatwoot.com/rails/active_storage/representations/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBd3FodGc9PSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--d218a325af0ef45061eefd352f8efb9ac84275e8/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdCem9MWm05eWJXRjBTU0lKYW5CbFp3WTZCa1ZVT2hOeVpYTnBlbVZmZEc5ZlptbHNiRnNIYVFINk1BPT0iLCJleHAiOm51bGwsInB1ciI6InZhcmlhdGlvbiJ9fQ==--533c3ad7218e24c4b0e8f8959dc1953ce1d279b9/1707423736896.jpeg',
|
||||
};
|
||||
|
||||
const messages = ref([
|
||||
{
|
||||
id: 1,
|
||||
role: 'user',
|
||||
content: 'Hi there! How can I help you today?',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
role: 'assistant',
|
||||
content:
|
||||
"Hello! I'm the AI assistant. I'll be helping the support team today.",
|
||||
},
|
||||
]);
|
||||
|
||||
const isCaptainTyping = ref(false);
|
||||
|
||||
const sendMessage = message => {
|
||||
// Add user message
|
||||
messages.value.push({
|
||||
id: messages.value.length + 1,
|
||||
role: 'user',
|
||||
content: message,
|
||||
});
|
||||
|
||||
// Simulate AI response
|
||||
isCaptainTyping.value = true;
|
||||
setTimeout(() => {
|
||||
isCaptainTyping.value = false;
|
||||
messages.value.push({
|
||||
id: messages.value.length + 1,
|
||||
role: 'assistant',
|
||||
content: 'This is a simulated AI response.',
|
||||
});
|
||||
}, 2000);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Story
|
||||
title="Captain/Copilot"
|
||||
:layout="{ type: 'grid', width: '400px', height: '800px' }"
|
||||
>
|
||||
<Copilot
|
||||
:support-agent="supportAgent"
|
||||
:messages="messages"
|
||||
:is-captain-typing="isCaptainTyping"
|
||||
@send-message="sendMessage"
|
||||
/>
|
||||
</Story>
|
||||
</template>
|
||||
@@ -0,0 +1,68 @@
|
||||
<script setup>
|
||||
import CopilotInput from './CopilotInput.vue';
|
||||
import CopilotLoader from './CopilotLoader.vue';
|
||||
import CopilotAgentMessage from './CopilotAgentMessage.vue';
|
||||
import CopilotAssistantMessage from './CopilotAssistantMessage.vue';
|
||||
import { nextTick, ref, watch } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
supportAgent: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
messages: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
isCaptainTyping: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['sendMessage']);
|
||||
|
||||
const COPILOT_USER_ROLES = ['assistant', 'system'];
|
||||
|
||||
const sendMessage = message => {
|
||||
emit('sendMessage', message);
|
||||
};
|
||||
const chatContainer = ref(null);
|
||||
|
||||
const scrollToBottom = async () => {
|
||||
await nextTick();
|
||||
if (chatContainer.value) {
|
||||
chatContainer.value.scrollTop = chatContainer.value.scrollHeight;
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
[() => props.messages, () => props.isCaptainTyping],
|
||||
() => {
|
||||
scrollToBottom();
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col ]mx-auto h-full text-sm leading-6 tracking-tight">
|
||||
<div ref="chatContainer" class="flex-1 overflow-y-auto py-4 space-y-6 px-4">
|
||||
<template v-for="message in messages" :key="message.id">
|
||||
<CopilotAgentMessage
|
||||
v-if="message.role === 'user'"
|
||||
:support-agent="supportAgent"
|
||||
:message="message"
|
||||
/>
|
||||
<CopilotAssistantMessage
|
||||
v-else-if="COPILOT_USER_ROLES.includes(message.role)"
|
||||
:message="message"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<CopilotLoader v-if="isCaptainTyping" />
|
||||
</div>
|
||||
|
||||
<CopilotInput class="mx-3 mb-4 mt-px" @send="sendMessage" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,31 @@
|
||||
<script setup>
|
||||
import Avatar from '../avatar/Avatar.vue';
|
||||
|
||||
defineProps({
|
||||
message: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
supportAgent: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-row gap-2">
|
||||
<Avatar
|
||||
:name="supportAgent.available_name"
|
||||
:src="supportAgent.avatar_url"
|
||||
:size="24"
|
||||
rounded-full
|
||||
/>
|
||||
<div class="space-y-1 text-n-slate-12">
|
||||
<div class="font-medium">{{ $t('CAPTAIN.COPILOT.YOU') }}</div>
|
||||
<div class="break-words">
|
||||
{{ message.content }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,27 @@
|
||||
<script setup>
|
||||
import Avatar from '../avatar/Avatar.vue';
|
||||
|
||||
defineProps({
|
||||
message: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-row gap-2">
|
||||
<Avatar
|
||||
name="Captain Copilot"
|
||||
icon-name="i-woot-captain"
|
||||
:size="24"
|
||||
rounded-full
|
||||
/>
|
||||
<div class="flex flex-col gap-1 text-n-slate-12">
|
||||
<div class="font-medium">{{ $t('CAPTAIN.NAME') }}</div>
|
||||
<div class="break-words">
|
||||
{{ message.content }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,34 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
const emit = defineEmits(['send']);
|
||||
const message = ref('');
|
||||
|
||||
const sendMessage = () => {
|
||||
if (message.value.trim()) {
|
||||
emit('send', message.value);
|
||||
message.value = '';
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form
|
||||
class="border border-n-weak bg-n-alpha-3 rounded-lg h-12 flex"
|
||||
@submit.prevent="sendMessage"
|
||||
>
|
||||
<input
|
||||
v-model="message"
|
||||
type="text"
|
||||
:placeholder="$t('CAPTAIN.COPILOT.SEND_MESSAGE')"
|
||||
class="w-full reset-base bg-transparent px-4 py-3 text-n-slate-11 text-sm"
|
||||
@keyup.enter="sendMessage"
|
||||
/>
|
||||
<button
|
||||
class="h-auto w-12 flex items-center justify-center text-n-slate-11"
|
||||
type="submit"
|
||||
>
|
||||
<i class="i-ph-arrow-up" />
|
||||
</button>
|
||||
</form>
|
||||
</template>
|
||||
@@ -0,0 +1,12 @@
|
||||
<script setup>
|
||||
import CopilotLoader from './CopilotLoader.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Story
|
||||
title="Captain/CopilotLoader"
|
||||
:layout="{ type: 'grid', width: '400px', height: '800px' }"
|
||||
>
|
||||
<CopilotLoader />
|
||||
</Story>
|
||||
</template>
|
||||
@@ -0,0 +1,22 @@
|
||||
<script>
|
||||
// Copilot Loader Component
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex justify-start">
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="text-n-iris-11 font-medium">
|
||||
{{ $t('CAPTAIN.COPILOT.LOADER') }}
|
||||
</span>
|
||||
<div class="flex space-x-1">
|
||||
<div
|
||||
class="w-2 h-2 rounded-full bg-n-iris-9 animate-bounce [animation-delay:-0.3s]"
|
||||
/>
|
||||
<div
|
||||
class="w-2 h-2 rounded-full bg-n-iris-9 animate-bounce [animation-delay:-0.15s]"
|
||||
/>
|
||||
<div class="w-2 h-2 rounded-full bg-n-iris-9 animate-bounce" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -6,7 +6,8 @@ import { useDropdownContext } from './provider.js';
|
||||
const props = defineProps({
|
||||
label: { type: String, default: '' },
|
||||
icon: { type: [String, Object, Function], default: '' },
|
||||
link: { type: String, default: '' },
|
||||
link: { type: [String, Object], default: '' },
|
||||
nativeLink: { type: Boolean, default: false },
|
||||
click: { type: Function, default: null },
|
||||
preserveOpen: { type: Boolean, default: false },
|
||||
});
|
||||
@@ -18,7 +19,13 @@ defineOptions({
|
||||
const { closeMenu } = useDropdownContext();
|
||||
|
||||
const componentIs = computed(() => {
|
||||
if (props.link) return 'router-link';
|
||||
if (props.link) {
|
||||
if (props.nativeLink && typeof props.link === 'string') {
|
||||
return 'a';
|
||||
}
|
||||
|
||||
return 'router-link';
|
||||
}
|
||||
if (props.click) return 'button';
|
||||
|
||||
return 'div';
|
||||
@@ -41,7 +48,8 @@ const triggerClick = () => {
|
||||
:class="{
|
||||
'hover:bg-n-alpha-2 rounded-lg w-full gap-3': !$slots.default,
|
||||
}"
|
||||
:href="props.link || null"
|
||||
:href="componentIs === 'a' ? props.link : null"
|
||||
:to="componentIs === 'router-link' ? props.link : null"
|
||||
@click="triggerClick"
|
||||
>
|
||||
<slot>
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
<script setup>
|
||||
import { h, defineProps } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
country: { type: String, required: true },
|
||||
squared: { type: Boolean, default: false },
|
||||
});
|
||||
|
||||
const renderFlag = () => {
|
||||
const classes = ['fi', `fi-${props.country.toLowerCase()}`, 'flex-shrink-0'];
|
||||
if (props.squared) {
|
||||
classes.push('fis');
|
||||
}
|
||||
return h('span', { class: classes });
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component :is="renderFlag" />
|
||||
</template>
|
||||
|
||||
<style>
|
||||
@import 'flag-icons/css/flag-icons.min.css';
|
||||
</style>
|
||||
@@ -0,0 +1,93 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import Flag from '../Flag.vue';
|
||||
import countries from 'shared/constants/countries';
|
||||
|
||||
const BasicTemplate = {
|
||||
components: { Flag },
|
||||
props: {
|
||||
country: {
|
||||
type: String,
|
||||
default: 'us',
|
||||
},
|
||||
squared: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
template: `
|
||||
<div class="flex items-center gap-4 p-4 border rounded border-n-weak">
|
||||
<Flag :country="country" :squared="squared" />
|
||||
</div>
|
||||
`,
|
||||
};
|
||||
|
||||
const SizeVariants = {
|
||||
components: { Flag },
|
||||
setup() {
|
||||
const isSquared = ref(false);
|
||||
return { isSquared };
|
||||
},
|
||||
template: `
|
||||
<div class="flex flex-col gap-4">
|
||||
<label class="flex items-center gap-2">
|
||||
<input type="checkbox" v-model="isSquared">
|
||||
Squared flags
|
||||
</label>
|
||||
<div class="flex items-center gap-4 p-4 border rounded border-n-weak">
|
||||
<Flag country="in" class="!size-4" :squared="isSquared" />
|
||||
<Flag country="in" class="!size-6" :squared="isSquared" />
|
||||
<Flag country="in" class="!size-8" :squared="isSquared" />
|
||||
<Flag country="in" class="!size-10" :squared="isSquared" />
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
};
|
||||
|
||||
const AllFlags = {
|
||||
components: { Flag },
|
||||
setup() {
|
||||
const isSquared = ref(false);
|
||||
return { countries, isSquared };
|
||||
},
|
||||
template: `
|
||||
<div class="flex flex-col gap-4">
|
||||
<label class="flex items-center gap-2">
|
||||
<input type="checkbox" v-model="isSquared">
|
||||
Squared flags
|
||||
</label>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4 p-4 border rounded border-n-strong md:grid-cols-3 lg:grid-cols-4">
|
||||
<div
|
||||
v-for="country in countries"
|
||||
:key="country.id"
|
||||
class="flex items-center gap-2 px-4 py-2 border rounded border-n-strong"
|
||||
>
|
||||
<Flag
|
||||
:country="country.id"
|
||||
:squared="isSquared"
|
||||
class="size-6"
|
||||
/>
|
||||
<span class="text-sm">{{ country.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Story title="Components/Flag">
|
||||
<Variant title="Basic Usage">
|
||||
<BasicTemplate country="us" :squared="false" />
|
||||
</Variant>
|
||||
|
||||
<Variant title="Size Variants">
|
||||
<SizeVariants />
|
||||
</Variant>
|
||||
|
||||
<Variant title="All Flags">
|
||||
<AllFlags />
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { computed, onMounted, nextTick } from 'vue';
|
||||
import { useSidebarContext } from './provider';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import Policy from 'dashboard/components/policy.vue';
|
||||
@@ -113,6 +113,13 @@ const toggleTrigger = () => {
|
||||
}
|
||||
setExpandedItem(props.name);
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await nextTick();
|
||||
if (hasActiveChild.value) {
|
||||
setExpandedItem(props.name);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- eslint-disable-next-line vue/no-root-v-if -->
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import Auth from 'dashboard/api/auth';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import Avatar from 'next/avatar/Avatar.vue';
|
||||
@@ -21,7 +20,6 @@ defineOptions({
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
const router = useRouter();
|
||||
|
||||
const globalConfig = useMapGetter('globalConfig/get');
|
||||
const currentUser = useMapGetter('getCurrentUser');
|
||||
@@ -49,9 +47,7 @@ const menuItems = computed(() => {
|
||||
show: true,
|
||||
label: t('SIDEBAR_ITEMS.PROFILE_SETTINGS'),
|
||||
icon: 'i-lucide-user-pen',
|
||||
click: () => {
|
||||
router.push({ name: 'profile_settings_index' });
|
||||
},
|
||||
link: { name: 'profile_settings_index' },
|
||||
},
|
||||
{
|
||||
show: true,
|
||||
@@ -66,15 +62,16 @@ const menuItems = computed(() => {
|
||||
show: true,
|
||||
label: t('SIDEBAR_ITEMS.DOCS'),
|
||||
icon: 'i-lucide-book',
|
||||
click: () => {
|
||||
window.open('https://www.chatwoot.com/hc/user-guide/en', '_blank');
|
||||
},
|
||||
link: 'https://www.chatwoot.com/hc/user-guide/en',
|
||||
nativeLink: true,
|
||||
target: '_blank',
|
||||
},
|
||||
{
|
||||
show: currentUser.value.type === 'SuperAdmin',
|
||||
label: t('SIDEBAR_ITEMS.SUPER_ADMIN_CONSOLE'),
|
||||
icon: 'i-lucide-castle',
|
||||
link: '/super_admin',
|
||||
nativeLink: true,
|
||||
target: '_blank',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -782,10 +782,10 @@ watch(conversationFilters, (newVal, oldVal) => {
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col flex-shrink-0 border-r conversations-list-wrap rtl:border-r-0 rtl:border-l border-slate-50 dark:border-slate-800/50"
|
||||
class="flex flex-col flex-shrink-0 conversations-list-wrap"
|
||||
:class="[
|
||||
{ hidden: !showConversationList },
|
||||
isOnExpandedLayout ? 'basis-full' : 'flex-basis-clamp',
|
||||
isOnExpandedLayout ? 'basis-full' : 'w-[360px] 2xl:w-[420px]',
|
||||
]"
|
||||
>
|
||||
<slot />
|
||||
@@ -916,12 +916,3 @@ watch(conversationFilters, (newVal, oldVal) => {
|
||||
</Teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@tailwind components;
|
||||
@layer components {
|
||||
.flex-basis-clamp {
|
||||
flex-basis: clamp(20rem, 4vw + 21.25rem, 27.5rem);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -75,9 +75,9 @@ onMounted(() => {
|
||||
@mousedown="handleMouseDown"
|
||||
>
|
||||
<div
|
||||
class="relative max-h-full overflow-auto bg-white shadow-md modal-container rtl:text-right dark:bg-slate-800 skip-context-menu"
|
||||
class="relative max-h-full overflow-auto shadow-xl modal-container rtl:text-right bg-n-alpha-3 backdrop-blur-[100px] skip-context-menu"
|
||||
:class="{
|
||||
'rounded-xl w-[37.5rem]': !fullWidth,
|
||||
'rounded-xl w-[36rem]': !fullWidth,
|
||||
'items-center rounded-none flex h-full justify-center w-full':
|
||||
fullWidth,
|
||||
[size]: true,
|
||||
@@ -102,38 +102,44 @@ onMounted(() => {
|
||||
<style lang="scss">
|
||||
.modal-mask {
|
||||
@apply flex items-center justify-center bg-modal-backdrop-light dark:bg-modal-backdrop-dark z-[9990] h-full left-0 fixed top-0 w-full;
|
||||
|
||||
.modal-container {
|
||||
&.medium {
|
||||
@apply max-w-[80%] w-[56.25rem];
|
||||
}
|
||||
// .content-box {
|
||||
// @apply h-auto p-0;
|
||||
// }
|
||||
|
||||
.content {
|
||||
@apply p-8;
|
||||
@apply p-6;
|
||||
}
|
||||
|
||||
form,
|
||||
.modal-content {
|
||||
@apply pt-4 pb-8 px-8 self-center;
|
||||
|
||||
a {
|
||||
@apply p-4;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-big {
|
||||
@apply w-full;
|
||||
}
|
||||
|
||||
.modal-mask.right-aligned {
|
||||
@apply justify-end;
|
||||
|
||||
.modal-container {
|
||||
@apply rounded-none h-full w-[30rem];
|
||||
}
|
||||
}
|
||||
|
||||
.modal-enter,
|
||||
.modal-leave {
|
||||
@apply opacity-0;
|
||||
}
|
||||
|
||||
.modal-enter .modal-container,
|
||||
.modal-leave .modal-container {
|
||||
transform: scale(1.1);
|
||||
|
||||
@@ -23,13 +23,13 @@ export default {
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="ml-0 mr-0 flex py-8 w-full xl:w-3/4 flex-col xl:flex-row"
|
||||
class="ml-0 mr-0 flex py-8 w-full flex-col xl:flex-row"
|
||||
:class="{
|
||||
'border-b border-solid border-slate-50 dark:border-slate-700/30':
|
||||
showBorder,
|
||||
}"
|
||||
>
|
||||
<div class="w-full xl:w-1/4 min-w-0 xl:max-w-[30%] pr-12">
|
||||
<div class="w-full xl:w-1/3 min-w-0 xl:max-w-[40%] pr-12">
|
||||
<p
|
||||
v-if="title"
|
||||
class="text-base text-woot-500 dark:text-woot-500 mb-0 font-medium"
|
||||
@@ -48,7 +48,7 @@ export default {
|
||||
{{ note }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="w-full xl:w-1/2 min-w-0 xl:max-w-[50%]">
|
||||
<div class="w-full xl:w-2/3 min-w-0 xl:max-w-[50%]">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
<script setup>
|
||||
import Copilot from 'dashboard/components-next/copilot/Copilot.vue';
|
||||
import IntegrationsAPI from 'dashboard/api/integrations';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
import { ref } from 'vue';
|
||||
const props = defineProps({
|
||||
conversationId: {
|
||||
type: [Number, String],
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
const currentUser = useMapGetter('getCurrentUser');
|
||||
const messages = ref([]);
|
||||
|
||||
const isCaptainTyping = ref(false);
|
||||
|
||||
const sendMessage = async message => {
|
||||
// Add user message
|
||||
messages.value.push({
|
||||
id: messages.value.length + 1,
|
||||
role: 'user',
|
||||
content: message,
|
||||
});
|
||||
isCaptainTyping.value = true;
|
||||
|
||||
try {
|
||||
const { data } = await IntegrationsAPI.requestCaptainCopilot({
|
||||
previous_history: messages.value
|
||||
.map(m => ({
|
||||
role: m.role,
|
||||
content: m.content,
|
||||
}))
|
||||
.slice(0, -1),
|
||||
message,
|
||||
conversation_id: props.conversationId,
|
||||
});
|
||||
messages.value.push({
|
||||
id: new Date().getTime(),
|
||||
role: 'assistant',
|
||||
content: data.message,
|
||||
});
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line
|
||||
console.log(error);
|
||||
} finally {
|
||||
isCaptainTyping.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Copilot
|
||||
:messages="messages"
|
||||
:support-agent="currentUser"
|
||||
:is-captain-typing="isCaptainTyping"
|
||||
@send-message="sendMessage"
|
||||
/>
|
||||
</template>
|
||||
@@ -89,7 +89,7 @@ export default {
|
||||
@apply bg-woot-500 dark:bg-woot-500;
|
||||
}
|
||||
|
||||
& + .item {
|
||||
&+.item {
|
||||
&::before {
|
||||
@apply bg-woot-500 dark:bg-woot-500;
|
||||
}
|
||||
|
||||
@@ -201,10 +201,10 @@ export default {
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.filter {
|
||||
@apply bg-slate-50 dark:bg-slate-800 p-2 border border-solid border-slate-75 dark:border-slate-600 rounded-md mb-2;
|
||||
@apply p-2 border border-solid border-slate-75 dark:border-slate-600 rounded-md mb-2;
|
||||
|
||||
&.is-a-macro {
|
||||
@apply mb-0 bg-white dark:bg-slate-700 p-0 border-0 rounded-none;
|
||||
@apply mb-0 p-0 border-0 rounded-none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,6 +244,7 @@ export default {
|
||||
@apply mb-0;
|
||||
}
|
||||
}
|
||||
|
||||
.filter__answer {
|
||||
&.answer--text-input {
|
||||
@apply mb-0;
|
||||
@@ -270,9 +271,11 @@ export default {
|
||||
.multiselect {
|
||||
@apply mb-0;
|
||||
}
|
||||
|
||||
.action-message {
|
||||
@apply mt-2 mx-0 mb-0;
|
||||
}
|
||||
|
||||
// Prosemirror does not have a native way of hiding the menu bar, hence
|
||||
::v-deep .ProseMirror-menubar {
|
||||
@apply hidden;
|
||||
|
||||
@@ -23,14 +23,12 @@ const goBack = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const buttonStyleClass = props.compact
|
||||
? 'text-sm text-slate-600 dark:text-slate-300'
|
||||
: 'text-base text-woot-500 dark:text-woot-500';
|
||||
const buttonStyleClass = props.compact ? 'text-sm' : 'text-base';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
class="flex items-center p-0 font-normal cursor-pointer"
|
||||
class="flex items-center p-0 font-normal cursor-pointer text-n-slate-11"
|
||||
:class="buttonStyleClass"
|
||||
@click.capture="goBack"
|
||||
>
|
||||
|
||||
@@ -48,7 +48,7 @@ useKeyboardEvents(keyboardEvents);
|
||||
<template>
|
||||
<woot-tabs
|
||||
:index="activeTabIndex"
|
||||
class="w-full px-4 py-0 tab--chat-type"
|
||||
class="w-full px-4 py-0 font-medium [&_a]:text-base [&_.tabs]:p-0"
|
||||
@change="onTabChange"
|
||||
>
|
||||
<woot-tabs-item
|
||||
@@ -60,13 +60,3 @@ useKeyboardEvents(keyboardEvents);
|
||||
/>
|
||||
</woot-tabs>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.tab--chat-type {
|
||||
::v-deep {
|
||||
.tabs {
|
||||
@apply p-0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -14,16 +14,11 @@ export default {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="bg-slate-25 dark:bg-slate-900 pt-4 pb-0 px-8 border-b border-solid border-slate-50 dark:border-slate-800/50"
|
||||
>
|
||||
<h2 class="text-2xl text-slate-800 dark:text-slate-100 mb-1 font-medium">
|
||||
<div class="pb-0 px-8 border-b border-n-weak">
|
||||
<h2 class="text-2xl text-n-slate-12 mb-1 font-medium">
|
||||
{{ headerTitle }}
|
||||
</h2>
|
||||
<p
|
||||
v-if="headerContent"
|
||||
class="w-full text-slate-600 dark:text-slate-300 text-sm mb-2"
|
||||
>
|
||||
<p v-if="headerContent" class="w-full text-n-slate-11 text-sm mb-2">
|
||||
{{ headerContent }}
|
||||
</p>
|
||||
<slot />
|
||||
|
||||
-407
@@ -1,407 +0,0 @@
|
||||
<script>
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import FilterInputBox from '../FilterInput/Index.vue';
|
||||
import languages from './advancedFilterItems/languages';
|
||||
import countries from 'shared/constants/countries.js';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { filterAttributeGroups } from './advancedFilterItems';
|
||||
import { useFilter } from 'shared/composables/useFilter';
|
||||
import * as OPERATORS from 'dashboard/components/widgets/FilterInput/FilterOperatorTypes.js';
|
||||
import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
|
||||
import { validateConversationOrContactFilters } from 'dashboard/helper/validations.js';
|
||||
import { useTrack } from 'dashboard/composables';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
FilterInputBox,
|
||||
},
|
||||
props: {
|
||||
onClose: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
initialFilterTypes: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
initialAppliedFilters: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
activeFolderName: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
isFolderView: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ['applyFilter', 'updateFolder'],
|
||||
setup() {
|
||||
const { setFilterAttributes } = useFilter({
|
||||
filteri18nKey: 'FILTER',
|
||||
attributeModel: 'conversation_attribute',
|
||||
});
|
||||
return {
|
||||
setFilterAttributes,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
show: true,
|
||||
appliedFilters: this.initialAppliedFilters,
|
||||
activeFolderNewName: this.activeFolderName,
|
||||
filterTypes: this.initialFilterTypes,
|
||||
filterAttributeGroups,
|
||||
filterGroups: [],
|
||||
allCustomAttributes: [],
|
||||
attributeModel: 'conversation_attribute',
|
||||
filtersFori18n: 'FILTER',
|
||||
validationErrors: {},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
getAppliedConversationFilters: 'getAppliedConversationFilters',
|
||||
}),
|
||||
filterModalHeaderTitle() {
|
||||
return !this.isFolderView
|
||||
? this.$t('FILTER.TITLE')
|
||||
: this.$t('FILTER.EDIT_CUSTOM_FILTER');
|
||||
},
|
||||
filterModalSubTitle() {
|
||||
return !this.isFolderView
|
||||
? this.$t('FILTER.SUBTITLE')
|
||||
: this.$t('FILTER.CUSTOM_VIEWS_SUBTITLE');
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
const { filterGroups, filterTypes } = this.setFilterAttributes();
|
||||
|
||||
this.filterTypes = [...this.filterTypes, ...filterTypes];
|
||||
this.filterGroups = filterGroups;
|
||||
|
||||
this.$store.dispatch('campaigns/get');
|
||||
if (this.getAppliedConversationFilters.length) {
|
||||
this.appliedFilters = [];
|
||||
this.appliedFilters = [...this.getAppliedConversationFilters];
|
||||
} else if (!this.isFolderView) {
|
||||
this.appliedFilters.push({
|
||||
attribute_key: 'status',
|
||||
filter_operator: 'equal_to',
|
||||
values: '',
|
||||
query_operator: 'and',
|
||||
attribute_model: 'standard',
|
||||
});
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getOperatorTypes(key) {
|
||||
switch (key) {
|
||||
case 'list':
|
||||
return OPERATORS.OPERATOR_TYPES_1;
|
||||
case 'text':
|
||||
return OPERATORS.OPERATOR_TYPES_3;
|
||||
case 'number':
|
||||
return OPERATORS.OPERATOR_TYPES_1;
|
||||
case 'link':
|
||||
return OPERATORS.OPERATOR_TYPES_1;
|
||||
case 'date':
|
||||
return OPERATORS.OPERATOR_TYPES_4;
|
||||
case 'checkbox':
|
||||
return OPERATORS.OPERATOR_TYPES_1;
|
||||
default:
|
||||
return OPERATORS.OPERATOR_TYPES_1;
|
||||
}
|
||||
},
|
||||
customAttributeInputType(key) {
|
||||
switch (key) {
|
||||
case 'date':
|
||||
return 'date';
|
||||
case 'text':
|
||||
return 'plain_text';
|
||||
case 'list':
|
||||
return 'search_select';
|
||||
case 'checkbox':
|
||||
return 'search_select';
|
||||
default:
|
||||
return 'plain_text';
|
||||
}
|
||||
},
|
||||
getAttributeModel(key) {
|
||||
const type = this.filterTypes.find(filter => filter.attributeKey === key);
|
||||
return type.attributeModel;
|
||||
},
|
||||
getInputType(key, operator) {
|
||||
if (key === 'created_at' || key === 'last_activity_at')
|
||||
if (operator === 'days_before') return 'plain_text';
|
||||
const type = this.filterTypes.find(filter => filter.attributeKey === key);
|
||||
return type?.inputType;
|
||||
},
|
||||
getOperators(key) {
|
||||
const type = this.filterTypes.find(filter => filter.attributeKey === key);
|
||||
return type?.filterOperators;
|
||||
},
|
||||
statusFilterItems() {
|
||||
return {
|
||||
open: {
|
||||
TEXT: this.$t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS.open.TEXT'),
|
||||
},
|
||||
resolved: {
|
||||
TEXT: this.$t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS.resolved.TEXT'),
|
||||
},
|
||||
pending: {
|
||||
TEXT: this.$t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS.pending.TEXT'),
|
||||
},
|
||||
snoozed: {
|
||||
TEXT: this.$t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS.snoozed.TEXT'),
|
||||
},
|
||||
all: {
|
||||
TEXT: this.$t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS.all.TEXT'),
|
||||
},
|
||||
};
|
||||
},
|
||||
getDropdownValues(type) {
|
||||
const statusFilters = this.statusFilterItems();
|
||||
const allCustomAttributes = this.$store.getters[
|
||||
'attributes/getAttributesByModel'
|
||||
](this.attributeModel);
|
||||
|
||||
const isCustomAttributeCheckbox = allCustomAttributes.find(attr => {
|
||||
return (
|
||||
attr.attribute_key === type &&
|
||||
attr.attribute_display_type === 'checkbox'
|
||||
);
|
||||
});
|
||||
|
||||
if (isCustomAttributeCheckbox) {
|
||||
return [
|
||||
{
|
||||
id: true,
|
||||
name: this.$t('FILTER.ATTRIBUTE_LABELS.TRUE'),
|
||||
},
|
||||
{
|
||||
id: false,
|
||||
name: this.$t('FILTER.ATTRIBUTE_LABELS.FALSE'),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const isCustomAttributeList = allCustomAttributes.find(attr => {
|
||||
return (
|
||||
attr.attribute_key === type && attr.attribute_display_type === 'list'
|
||||
);
|
||||
});
|
||||
|
||||
if (isCustomAttributeList) {
|
||||
return allCustomAttributes
|
||||
.find(attr => attr.attribute_key === type)
|
||||
.attribute_values.map(item => {
|
||||
return {
|
||||
id: item,
|
||||
name: item,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case 'status':
|
||||
return [
|
||||
...Object.keys(statusFilters).map(status => {
|
||||
return {
|
||||
id: status,
|
||||
name: statusFilters[status].TEXT,
|
||||
};
|
||||
}),
|
||||
];
|
||||
case 'assignee_id':
|
||||
return this.$store.getters['agents/getAgents'];
|
||||
case 'contact':
|
||||
return this.$store.getters['contacts/getContacts'];
|
||||
case 'inbox_id':
|
||||
return this.$store.getters['inboxes/getInboxes'];
|
||||
case 'team_id':
|
||||
return this.$store.getters['teams/getTeams'];
|
||||
case 'campaign_id':
|
||||
return this.$store.getters['campaigns/getAllCampaigns'].map(i => {
|
||||
return {
|
||||
id: i.id,
|
||||
name: i.title,
|
||||
};
|
||||
});
|
||||
case 'labels':
|
||||
return this.$store.getters['labels/getLabels'].map(i => {
|
||||
return {
|
||||
id: i.title,
|
||||
name: i.title,
|
||||
};
|
||||
});
|
||||
case 'browser_language':
|
||||
return languages;
|
||||
case 'country_code':
|
||||
return countries;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
appendNewFilter() {
|
||||
if (this.isFolderView) {
|
||||
this.setQueryOperatorOnLastQuery();
|
||||
} else {
|
||||
this.appliedFilters.push({
|
||||
attribute_key: 'status',
|
||||
filter_operator: 'equal_to',
|
||||
values: '',
|
||||
query_operator: 'and',
|
||||
});
|
||||
}
|
||||
},
|
||||
setQueryOperatorOnLastQuery() {
|
||||
const lastItemIndex = this.appliedFilters.length - 1;
|
||||
this.appliedFilters[lastItemIndex] = {
|
||||
...this.appliedFilters[lastItemIndex],
|
||||
query_operator: 'and',
|
||||
};
|
||||
this.$nextTick(() => {
|
||||
this.appliedFilters.push({
|
||||
attribute_key: 'status',
|
||||
filter_operator: 'equal_to',
|
||||
values: '',
|
||||
query_operator: 'and',
|
||||
});
|
||||
});
|
||||
},
|
||||
removeFilter(index) {
|
||||
if (this.appliedFilters.length <= 1) {
|
||||
useAlert(this.$t('FILTER.FILTER_DELETE_ERROR'));
|
||||
} else {
|
||||
this.appliedFilters.splice(index, 1);
|
||||
}
|
||||
},
|
||||
submitFilterQuery() {
|
||||
this.validationErrors = validateConversationOrContactFilters(
|
||||
this.appliedFilters
|
||||
);
|
||||
if (Object.keys(this.validationErrors).length === 0) {
|
||||
this.$store.dispatch(
|
||||
'setConversationFilters',
|
||||
JSON.parse(JSON.stringify(this.appliedFilters))
|
||||
);
|
||||
this.$emit('applyFilter', this.appliedFilters);
|
||||
useTrack(CONVERSATION_EVENTS.APPLY_FILTER, {
|
||||
applied_filters: this.appliedFilters.map(filter => ({
|
||||
key: filter.attribute_key,
|
||||
operator: filter.filter_operator,
|
||||
query_operator: filter.query_operator,
|
||||
})),
|
||||
});
|
||||
}
|
||||
},
|
||||
updateSavedCustomViews() {
|
||||
this.$emit('updateFolder', this.appliedFilters, this.activeFolderNewName);
|
||||
},
|
||||
resetFilter(index, currentFilter) {
|
||||
this.appliedFilters[index].filter_operator = this.filterTypes.find(
|
||||
filter => filter.attributeKey === currentFilter.attribute_key
|
||||
).filterOperators[0].value;
|
||||
this.appliedFilters[index].values = '';
|
||||
},
|
||||
showUserInput(operatorType) {
|
||||
if (operatorType === 'is_present' || operatorType === 'is_not_present')
|
||||
return false;
|
||||
return true;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<woot-modal-header :header-title="filterModalHeaderTitle">
|
||||
<p class="text-slate-600 dark:text-slate-200">
|
||||
{{ filterModalSubTitle }}
|
||||
</p>
|
||||
</woot-modal-header>
|
||||
<div class="p-8">
|
||||
<div v-if="isFolderView">
|
||||
<label class="input-label" :class="{ error: !activeFolderNewName }">
|
||||
{{ $t('FILTER.FOLDER_LABEL') }}
|
||||
<input
|
||||
v-model="activeFolderNewName"
|
||||
type="text"
|
||||
class="bg-white folder-input border-slate-75 dark:border-slate-600 dark:bg-slate-900 text-slate-900 dark:text-slate-100"
|
||||
/>
|
||||
<span v-if="!activeFolderNewName" class="message">
|
||||
{{ $t('FILTER.EMPTY_VALUE_ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
<label class="mb-1">
|
||||
{{ $t('FILTER.FOLDER_QUERY_LABEL') }}
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="p-4 mb-4 border border-solid rounded-lg bg-slate-25 dark:bg-slate-900 border-slate-50 dark:border-slate-700/50"
|
||||
>
|
||||
<FilterInputBox
|
||||
v-for="(filter, i) in appliedFilters"
|
||||
:key="i"
|
||||
v-model="appliedFilters[i]"
|
||||
:filter-groups="filterGroups"
|
||||
:input-type="
|
||||
getInputType(
|
||||
appliedFilters[i].attribute_key,
|
||||
appliedFilters[i].filter_operator
|
||||
)
|
||||
"
|
||||
:operators="getOperators(appliedFilters[i].attribute_key)"
|
||||
:dropdown-values="getDropdownValues(appliedFilters[i].attribute_key)"
|
||||
:show-query-operator="i !== appliedFilters.length - 1"
|
||||
:show-user-input="showUserInput(appliedFilters[i].filter_operator)"
|
||||
grouped-filters
|
||||
:error-message="
|
||||
validationErrors[`filter_${i}`]
|
||||
? $t(`CONTACTS_FILTER.ERRORS.VALUE_REQUIRED`)
|
||||
: ''
|
||||
"
|
||||
@reset-filter="resetFilter(i, appliedFilters[i])"
|
||||
@remove-filter="removeFilter(i)"
|
||||
/>
|
||||
<div class="mt-4">
|
||||
<woot-button
|
||||
icon="add"
|
||||
color-scheme="success"
|
||||
variant="smooth"
|
||||
size="small"
|
||||
@click="appendNewFilter"
|
||||
>
|
||||
{{ $t('FILTER.ADD_NEW_FILTER') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<div class="flex flex-row justify-end w-full gap-2 px-0 py-2">
|
||||
<woot-button class="button clear" @click.prevent="onClose">
|
||||
{{ $t('FILTER.CANCEL_BUTTON_LABEL') }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
v-if="isFolderView"
|
||||
:disabled="!activeFolderNewName"
|
||||
@click="updateSavedCustomViews"
|
||||
>
|
||||
{{ $t('FILTER.UPDATE_BUTTON_LABEL') }}
|
||||
</woot-button>
|
||||
<woot-button v-else @click="submitFilterQuery">
|
||||
{{ $t('FILTER.SUBMIT_BUTTON_LABEL') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.folder-input {
|
||||
@apply w-[50%];
|
||||
}
|
||||
</style>
|
||||
@@ -1,14 +1,14 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import ContactPanel from 'dashboard/routes/dashboard/conversation/ContactPanel.vue';
|
||||
import ConversationHeader from './ConversationHeader.vue';
|
||||
import DashboardAppFrame from '../DashboardApp/Frame.vue';
|
||||
import EmptyState from './EmptyState/EmptyState.vue';
|
||||
import MessagesView from './MessagesView.vue';
|
||||
import ConversationSidebar from './ConversationSidebar.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ContactPanel,
|
||||
ConversationSidebar,
|
||||
ConversationHeader,
|
||||
DashboardAppFrame,
|
||||
EmptyState,
|
||||
@@ -98,8 +98,10 @@ export default {
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="conversation-details-wrap bg-slate-25 dark:bg-slate-800"
|
||||
:class="{ 'with-border-right': !isOnExpandedLayout }"
|
||||
class="conversation-details-wrap bg-n-background"
|
||||
:class="{
|
||||
'border-l rtl:border-l-0 rtl:border-r border-n-weak': !isOnExpandedLayout,
|
||||
}"
|
||||
>
|
||||
<ConversationHeader
|
||||
v-if="currentChat.id"
|
||||
@@ -138,17 +140,11 @@ export default {
|
||||
v-if="!currentChat.id && !isInboxView"
|
||||
:is-on-expanded-layout="isOnExpandedLayout"
|
||||
/>
|
||||
<div
|
||||
v-show="showContactPanel"
|
||||
class="conversation-sidebar-wrap basis-full sm:basis-[17.5rem] md:basis-[18.75rem] lg:basis-[19.375rem] xl:basis-[20.625rem] 2xl:basis-[25rem] rtl:border-r border-slate-50 dark:border-slate-700 h-auto overflow-auto z-10 flex-shrink-0 flex-grow-0"
|
||||
>
|
||||
<ContactPanel
|
||||
v-if="showContactPanel"
|
||||
:conversation-id="currentChat.id"
|
||||
:inbox-id="currentChat.inbox_id"
|
||||
:on-toggle="onToggleContactPanel"
|
||||
/>
|
||||
</div>
|
||||
<ConversationSidebar
|
||||
v-if="showContactPanel"
|
||||
:current-chat="currentChat"
|
||||
@toggle-contact-panel="onToggleContactPanel"
|
||||
/>
|
||||
</div>
|
||||
<DashboardAppFrame
|
||||
v-for="(dashboardApp, index) in dashboardApps"
|
||||
@@ -165,10 +161,6 @@ export default {
|
||||
<style lang="scss" scoped>
|
||||
.conversation-details-wrap {
|
||||
@apply flex flex-col min-w-0 w-full;
|
||||
|
||||
&.with-border-right {
|
||||
@apply border-r border-slate-50 dark:border-slate-700;
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-app--tabs {
|
||||
@@ -180,10 +172,4 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.conversation-sidebar-wrap {
|
||||
&::v-deep .contact--panel {
|
||||
@apply w-full h-full max-w-full;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
<script setup>
|
||||
import { useStoreGetters } from 'dashboard/composables/store';
|
||||
import { computed, ref } from 'vue';
|
||||
import CopilotContainer from '../../copilot/CopilotContainer.vue';
|
||||
import ContactPanel from 'dashboard/routes/dashboard/conversation/ContactPanel.vue';
|
||||
import TabBar from 'dashboard/components-next/tabbar/TabBar.vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
defineProps({
|
||||
currentChat: {
|
||||
required: true,
|
||||
type: Object,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['toggleContactPanel']);
|
||||
|
||||
const getters = useStoreGetters();
|
||||
|
||||
const captainIntegration = computed(() =>
|
||||
getters['integrations/getIntegration'].value('captain', null)
|
||||
);
|
||||
const { t } = useI18n();
|
||||
|
||||
const CONTACT_TABS_OPTIONS = [
|
||||
{ key: 'CONTACT', value: 'contact' },
|
||||
{ key: 'COPILOT', value: 'copilot' },
|
||||
];
|
||||
|
||||
const tabs = computed(() => {
|
||||
return CONTACT_TABS_OPTIONS.map(tab => ({
|
||||
label: t(`CONVERSATION.SIDEBAR.${tab.key}`),
|
||||
value: tab.value,
|
||||
}));
|
||||
});
|
||||
const activeTab = ref(0);
|
||||
const toggleContactPanel = () => {
|
||||
emit('toggleContactPanel');
|
||||
};
|
||||
|
||||
const handleTabChange = selectedTab => {
|
||||
activeTab.value = tabs.value.findIndex(
|
||||
tabItem => tabItem.value === selectedTab.value
|
||||
);
|
||||
};
|
||||
|
||||
const showCopilotTab = computed(() => {
|
||||
return captainIntegration.value && captainIntegration.value.enabled;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="ltr:border-l rtl:border-r border-n-weak h-full overflow-hidden z-10 min-w-[300px] w-[300px] 2xl:min-w-96 2xl:w-96 flex flex-col bg-n-background"
|
||||
>
|
||||
<div v-if="showCopilotTab" class="p-2">
|
||||
<TabBar
|
||||
:tabs="tabs"
|
||||
:initial-active-tab="activeTab"
|
||||
class="w-full [&>button]:w-full"
|
||||
@tab-changed="handleTabChange"
|
||||
/>
|
||||
</div>
|
||||
<div class="overflow-auto flex flex-1">
|
||||
<ContactPanel
|
||||
v-if="!activeTab"
|
||||
:conversation-id="currentChat.id"
|
||||
:inbox-id="currentChat.inbox_id"
|
||||
:on-toggle="toggleContactPanel"
|
||||
/>
|
||||
<CopilotContainer
|
||||
v-else-if="activeTab === 1 && showCopilotTab"
|
||||
:key="currentChat.id"
|
||||
:conversation-id="currentChat.id"
|
||||
class="flex-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -468,9 +468,7 @@ export default {
|
||||
size="tiny"
|
||||
color-scheme="secondary"
|
||||
class="box-border fixed z-10 bg-white border border-r-0 border-solid rounded-bl-calc rtl:rotate-180 rounded-tl-calc dark:bg-slate-700 border-slate-50 dark:border-slate-600"
|
||||
:class="
|
||||
isInboxView ? 'top-52 md:top-40' : 'top-[9.5rem] md:top-[6.25rem]'
|
||||
"
|
||||
:class="isInboxView ? 'top-52 md:top-40' : 'top-32'"
|
||||
:icon="isRightOrLeftIcon"
|
||||
@click="onToggleContactPanel"
|
||||
/>
|
||||
@@ -558,6 +556,7 @@ export default {
|
||||
|
||||
<style scoped>
|
||||
@tailwind components;
|
||||
|
||||
@layer components {
|
||||
.rounded-bl-calc {
|
||||
border-bottom-left-radius: calc(1.5rem + 1px);
|
||||
|
||||
@@ -36,7 +36,7 @@ defineProps({
|
||||
</div>
|
||||
<div class="mt-auto">
|
||||
<p
|
||||
class="text-base text-slate-800 dark:text-slate-100 font-interDisplay font-semibold tracking-[0.3px]"
|
||||
class="text-base text-slate-800 dark:text-slate-100 font-semibold tracking-[0.3px]"
|
||||
>
|
||||
{{ title }}
|
||||
</p>
|
||||
|
||||
@@ -32,7 +32,7 @@ const greetingMessage = computed(() => {
|
||||
>
|
||||
<div class="col-span-full self-start">
|
||||
<p
|
||||
class="text-xl font-semibold text-slate-900 dark:text-white font-interDisplay tracking-[0.3px]"
|
||||
class="text-xl font-semibold text-slate-900 dark:text-white tracking-[0.3px]"
|
||||
>
|
||||
{{ greetingMessage }}
|
||||
</p>
|
||||
|
||||
@@ -35,7 +35,7 @@ export default {
|
||||
HELP_CENTER_DOCS_URL:
|
||||
'https://www.chatwoot.com/docs/product/others/help-center',
|
||||
TESTIMONIAL_URL: 'https://testimonials.cdn.chatwoot.com/content.json',
|
||||
SMALL_SCREEN_BREAKPOINT: 1024,
|
||||
SMALL_SCREEN_BREAKPOINT: 1200,
|
||||
AVAILABILITY_STATUS_KEYS: ['online', 'busy', 'offline'],
|
||||
SNOOZE_OPTIONS: {
|
||||
UNTIL_NEXT_REPLY: 'until_next_reply',
|
||||
|
||||
@@ -1,46 +1,5 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
validateConversationOrContactFilters,
|
||||
validateAutomation,
|
||||
} from '../validations';
|
||||
|
||||
describe('validateConversationOrContactFilters', () => {
|
||||
it('should return no errors for valid filters', () => {
|
||||
const validFilters = [
|
||||
{ attribute_key: 'name', filter_operator: 'contains', values: 'John' },
|
||||
{ attribute_key: 'email', filter_operator: 'is_present' },
|
||||
];
|
||||
const errors = validateConversationOrContactFilters(validFilters);
|
||||
expect(errors).toEqual({});
|
||||
});
|
||||
|
||||
it('should return errors for invalid filters', () => {
|
||||
const invalidFilters = [
|
||||
{ attribute_key: '', filter_operator: 'contains', values: 'John' },
|
||||
{ attribute_key: 'email', filter_operator: '' },
|
||||
{ attribute_key: 'age', filter_operator: 'equals' },
|
||||
];
|
||||
const errors = validateConversationOrContactFilters(invalidFilters);
|
||||
expect(errors).toEqual({
|
||||
filter_0: 'ATTRIBUTE_KEY_REQUIRED',
|
||||
filter_1: 'FILTER_OPERATOR_REQUIRED',
|
||||
filter_2: 'VALUE_REQUIRED',
|
||||
});
|
||||
});
|
||||
|
||||
it('should validate days_before operator correctly', () => {
|
||||
const filters = [
|
||||
{ attribute_key: 'date', filter_operator: 'days_before', values: '0' },
|
||||
{ attribute_key: 'date', filter_operator: 'days_before', values: '999' },
|
||||
{ attribute_key: 'date', filter_operator: 'days_before', values: '500' },
|
||||
];
|
||||
const errors = validateConversationOrContactFilters(filters);
|
||||
expect(errors).toEqual({
|
||||
filter_0: 'VALUE_MUST_BE_BETWEEN_1_AND_998',
|
||||
filter_1: 'VALUE_MUST_BE_BETWEEN_1_AND_998',
|
||||
});
|
||||
});
|
||||
});
|
||||
import { validateAutomation } from '../validations';
|
||||
|
||||
describe('validateAutomation', () => {
|
||||
it('should return no errors for a valid automation', () => {
|
||||
|
||||
@@ -65,29 +65,6 @@ export const validateSingleFilter = filter => {
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates filters for conversations or contacts.
|
||||
*
|
||||
* @param {Array} filters - An array of filter objects to validate.
|
||||
* @param {string} filters[].attribute_key - The key of the attribute to filter on.
|
||||
* @param {string} filters[].filter_operator - The operator to use for filtering.
|
||||
* @param {string|number} [filters[].values] - The value(s) to filter by (required for most operators).
|
||||
*
|
||||
* @returns {Object} An object containing any validation errors, keyed by filter index.
|
||||
*/
|
||||
export const validateConversationOrContactFilters = filters => {
|
||||
const errors = {};
|
||||
|
||||
filters.forEach((filter, index) => {
|
||||
const error = validateSingleFilter(filter);
|
||||
if (error) {
|
||||
errors[`filter_${index}`] = error;
|
||||
}
|
||||
});
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// ---------------------- Automation Validation ---------------------
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
@@ -57,46 +57,6 @@
|
||||
"TITLE": "Edit contact",
|
||||
"DESC": "Edit contact details"
|
||||
},
|
||||
"CREATE_CONTACT": {
|
||||
"BUTTON_LABEL": "New Contact",
|
||||
"TITLE": "Create new contact",
|
||||
"DESC": "Add basic information details about the contact."
|
||||
},
|
||||
"IMPORT_CONTACTS": {
|
||||
"BUTTON_LABEL": "Import",
|
||||
"TITLE": "Import Contacts",
|
||||
"DESC": "Import contacts through a CSV file.",
|
||||
"DOWNLOAD_LABEL": "Download a sample csv.",
|
||||
"FORM": {
|
||||
"LABEL": "CSV File",
|
||||
"SUBMIT": "Import",
|
||||
"CANCEL": "Cancel"
|
||||
},
|
||||
"SUCCESS_MESSAGE": "You will be notified via email when the import is complete.",
|
||||
"ERROR_MESSAGE": "There was an error, please try again"
|
||||
},
|
||||
"EXPORT_CONTACTS": {
|
||||
"BUTTON_LABEL": "Export",
|
||||
"TITLE": "Export Contacts",
|
||||
"DESC": "Export contacts to a CSV file.",
|
||||
"SUCCESS_MESSAGE": "Export is in progress. You will be notified on email when the export file is ready to download.",
|
||||
"ERROR_MESSAGE": "There was an error, please try again",
|
||||
"CONFIRM": {
|
||||
"TITLE": "Export Contacts",
|
||||
"MESSAGE": "Are you sure you want to export all contacts?",
|
||||
"FILTERED_MESSAGE": "Are you sure you want to export all the filtered contacts?",
|
||||
"YES": "Yes, Export",
|
||||
"NO": "No, Cancel"
|
||||
}
|
||||
},
|
||||
"DELETE_NOTE": {
|
||||
"CONFIRM": {
|
||||
"TITLE": "Confirm Deletion",
|
||||
"MESSAGE": "Are you want sure to delete this note?",
|
||||
"YES": "Yes, Delete it",
|
||||
"NO": "No, Keep it"
|
||||
}
|
||||
},
|
||||
"DELETE_CONTACT": {
|
||||
"BUTTON_LABEL": "Delete Contact",
|
||||
"TITLE": "Delete contact",
|
||||
@@ -224,79 +184,14 @@
|
||||
}
|
||||
},
|
||||
"CONTACTS_PAGE": {
|
||||
"HEADER": "Contacts",
|
||||
"FIELDS": "Contact fields",
|
||||
"SEARCH_BUTTON": "Search",
|
||||
"SEARCH_INPUT_PLACEHOLDER": "Search for contacts",
|
||||
"FILTER_CONTACTS": "Filter",
|
||||
"FILTER_CONTACTS_SAVE": "Save filter",
|
||||
"FILTER_CONTACTS_DELETE": "Delete filter",
|
||||
"FILTER_CONTACTS_EDIT": "Edit segment",
|
||||
"LIST": {
|
||||
"LOADING_MESSAGE": "Loading contacts...",
|
||||
"404": "No contacts matches your search 🔍",
|
||||
"NO_CONTACTS": "There are no available contacts",
|
||||
"TABLE_HEADER": {
|
||||
"NAME": "Name",
|
||||
"PHONE_NUMBER": "Phone Number",
|
||||
"CONVERSATIONS": "Conversations",
|
||||
"LAST_ACTIVITY": "Last Activity",
|
||||
"CREATED_AT": "Created At",
|
||||
"COUNTRY": "Country",
|
||||
"CITY": "City",
|
||||
"SOCIAL_PROFILES": "Social Profiles",
|
||||
"COMPANY": "Company",
|
||||
"EMAIL_ADDRESS": "Email Address"
|
||||
},
|
||||
"VIEW_DETAILS": "View details"
|
||||
}
|
||||
},
|
||||
"CONTACT_PROFILE": {
|
||||
"BACK_BUTTON": "Contacts",
|
||||
"LOADING": "Loading contact profile..."
|
||||
},
|
||||
"REMINDER": {
|
||||
"ADD_BUTTON": {
|
||||
"BUTTON": "Add",
|
||||
"TITLE": "Shift + Enter to create a task"
|
||||
},
|
||||
"FOOTER": {
|
||||
"DUE_DATE": "Due date",
|
||||
"LABEL_TITLE": "Set type"
|
||||
}
|
||||
},
|
||||
"NOTES": {
|
||||
"FETCHING_NOTES": "Fetching notes...",
|
||||
"NOT_AVAILABLE": "There are no notes created for this contact",
|
||||
"HEADER": {
|
||||
"TITLE": "Notes"
|
||||
},
|
||||
"LIST": {
|
||||
"LABEL": "added a note"
|
||||
},
|
||||
"ADD": {
|
||||
"BUTTON": "Add",
|
||||
"PLACEHOLDER": "Add a note",
|
||||
"TITLE": "Shift + Enter to create a note"
|
||||
},
|
||||
"CONTENT_HEADER": {
|
||||
"DELETE": "Delete note"
|
||||
}
|
||||
},
|
||||
"EVENTS": {
|
||||
"HEADER": {
|
||||
"TITLE": "Activities"
|
||||
},
|
||||
"BUTTON": {
|
||||
"PILL_BUTTON_NOTES": "notes",
|
||||
"PILL_BUTTON_EVENTS": "events",
|
||||
"PILL_BUTTON_CONVO": "conversations"
|
||||
"SOCIAL_PROFILES": "Social Profiles"
|
||||
}
|
||||
}
|
||||
},
|
||||
"CUSTOM_ATTRIBUTES": {
|
||||
"ADD_BUTTON_TEXT": "Add attributes",
|
||||
"BUTTON": "Add custom attribute",
|
||||
"NOT_AVAILABLE": "There are no custom attributes available for this contact.",
|
||||
"COPY_SUCCESSFUL": "Copied to clipboard successfully",
|
||||
"SHOW_MORE": "Show all attributes",
|
||||
"SHOW_LESS": "Show less attributes",
|
||||
|
||||
@@ -219,6 +219,10 @@
|
||||
"DELETE": "Delete",
|
||||
"CANCEL": "Cancel"
|
||||
}
|
||||
},
|
||||
"SIDEBAR": {
|
||||
"CONTACT": "Contact",
|
||||
"COPILOT": "Copilot"
|
||||
}
|
||||
},
|
||||
"EMAIL_TRANSCRIPT": {
|
||||
|
||||
@@ -299,5 +299,13 @@
|
||||
"ERROR": "There was an error unlinking the issue, please try again"
|
||||
}
|
||||
}
|
||||
},
|
||||
"CAPTAIN": {
|
||||
"NAME": "Captain",
|
||||
"COPILOT": {
|
||||
"SEND_MESSAGE": "Send message...",
|
||||
"LOADER": "Captain is thinking",
|
||||
"YOU": "You"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
<script>
|
||||
import Modal from 'dashboard/components/Modal.vue';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { required, minLength } from '@vuelidate/validators';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Modal,
|
||||
},
|
||||
props: {
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isCreating: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ['create', 'cancel', 'update:show'],
|
||||
setup() {
|
||||
return { v$: useVuelidate() };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
attributeValue: '',
|
||||
attributeName: '',
|
||||
};
|
||||
},
|
||||
validations: {
|
||||
attributeName: {
|
||||
required,
|
||||
minLength: minLength(2),
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
localShow: {
|
||||
get() {
|
||||
return this.show;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('update:show', value);
|
||||
},
|
||||
},
|
||||
attributeNameError() {
|
||||
if (this.v$.attributeName.$error) {
|
||||
return this.$t('CUSTOM_ATTRIBUTES.FORM.NAME.ERROR');
|
||||
}
|
||||
return '';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
addCustomAttribute() {
|
||||
this.$emit('create', {
|
||||
attributeValue: this.attributeValue,
|
||||
attributeName: this.attributeName || '',
|
||||
});
|
||||
this.reset();
|
||||
this.$emit('cancel');
|
||||
},
|
||||
onClose() {
|
||||
this.reset();
|
||||
this.$emit('cancel');
|
||||
},
|
||||
reset() {
|
||||
this.attributeValue = '';
|
||||
this.attributeName = '';
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal v-model:show="localShow" :on-close="onClose">
|
||||
<woot-modal-header
|
||||
:header-title="$t('CUSTOM_ATTRIBUTES.ADD.TITLE')"
|
||||
:header-content="$t('CUSTOM_ATTRIBUTES.ADD.DESC')"
|
||||
/>
|
||||
<form class="w-full" @submit.prevent="addCustomAttribute">
|
||||
<woot-input
|
||||
v-model="attributeName"
|
||||
:class="{ error: v$.attributeName.$error }"
|
||||
class="w-full"
|
||||
:error="attributeNameError"
|
||||
:label="$t('CUSTOM_ATTRIBUTES.FORM.NAME.LABEL')"
|
||||
:placeholder="$t('CUSTOM_ATTRIBUTES.FORM.NAME.PLACEHOLDER')"
|
||||
@blur="v$.attributeName.$touch"
|
||||
@input="v$.attributeName.$touch"
|
||||
/>
|
||||
<woot-input
|
||||
v-model="attributeValue"
|
||||
class="w-full"
|
||||
:label="$t('CUSTOM_ATTRIBUTES.FORM.VALUE.LABEL')"
|
||||
:placeholder="$t('CUSTOM_ATTRIBUTES.FORM.VALUE.PLACEHOLDER')"
|
||||
/>
|
||||
<div class="flex items-center justify-end gap-2 px-0 py-2">
|
||||
<woot-button
|
||||
:is-disabled="v$.attributeName.$invalid || isCreating"
|
||||
:is-loading="isCreating"
|
||||
>
|
||||
{{ $t('CUSTOM_ATTRIBUTES.FORM.CREATE') }}
|
||||
</woot-button>
|
||||
<woot-button variant="clear" @click.prevent="onClose">
|
||||
{{ $t('CUSTOM_ATTRIBUTES.FORM.CANCEL') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
</template>
|
||||
@@ -1,131 +0,0 @@
|
||||
<script>
|
||||
import EmojiOrIcon from 'shared/components/EmojiOrIcon.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
EmojiOrIcon,
|
||||
},
|
||||
props: {
|
||||
label: { type: String, required: true },
|
||||
icon: { type: String, default: '' },
|
||||
emoji: { type: String, default: '' },
|
||||
value: { type: [String, Number], default: '' },
|
||||
showEdit: { type: Boolean, default: false },
|
||||
},
|
||||
emits: ['update'],
|
||||
data() {
|
||||
return {
|
||||
isEditing: false,
|
||||
editedValue: this.value,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
focusInput() {
|
||||
this.$refs.inputfield.focus();
|
||||
},
|
||||
onEdit() {
|
||||
this.isEditing = true;
|
||||
this.$nextTick(() => {
|
||||
this.focusInput();
|
||||
});
|
||||
},
|
||||
onUpdate() {
|
||||
this.isEditing = false;
|
||||
this.$emit('update', this.editedValue);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="contact-attribute">
|
||||
<div class="title-wrap">
|
||||
<h4 class="text-sm title">
|
||||
<div class="title--icon">
|
||||
<EmojiOrIcon :icon="icon" :emoji="emoji" />
|
||||
</div>
|
||||
{{ label }}
|
||||
</h4>
|
||||
</div>
|
||||
<div v-show="isEditing">
|
||||
<div class="mb-2 w-full flex items-center">
|
||||
<input
|
||||
ref="inputfield"
|
||||
v-model="editedValue"
|
||||
type="text"
|
||||
class="!h-8 ltr:rounded-r-none rtl:rounded-l-none !mb-0 !text-sm"
|
||||
autofocus="true"
|
||||
@keyup.enter="onUpdate"
|
||||
/>
|
||||
<div>
|
||||
<woot-button
|
||||
size="small"
|
||||
icon="ion-checkmark"
|
||||
class="rounded-l-none rtl:rounded-r-none"
|
||||
@click="onUpdate"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-show="!isEditing"
|
||||
class="value--view"
|
||||
:class="{ 'is-editable': showEdit }"
|
||||
>
|
||||
<p class="value">
|
||||
{{ value || '---' }}
|
||||
</p>
|
||||
<woot-button
|
||||
v-if="showEdit"
|
||||
variant="clear link"
|
||||
size="small"
|
||||
color-scheme="secondary"
|
||||
icon="edit"
|
||||
class-names="edit-button"
|
||||
@click="onEdit"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.contact-attribute {
|
||||
margin-bottom: var(--space-small);
|
||||
}
|
||||
.title-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: var(--space-mini);
|
||||
}
|
||||
.title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
}
|
||||
.title--icon {
|
||||
width: var(--space-two);
|
||||
}
|
||||
.edit-button {
|
||||
display: none;
|
||||
}
|
||||
.value--view {
|
||||
display: flex;
|
||||
|
||||
&.is-editable:hover {
|
||||
.value {
|
||||
background: var(--color-background);
|
||||
}
|
||||
.edit-button {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
.value {
|
||||
display: inline-block;
|
||||
min-width: var(--space-mega);
|
||||
border-radius: var(--border-radius-small);
|
||||
word-break: break-all;
|
||||
margin: 0 var(--space-smaller) 0 var(--space-normal);
|
||||
padding: var(--space-micro) var(--space-smaller);
|
||||
}
|
||||
</style>
|
||||
@@ -1,115 +0,0 @@
|
||||
<script>
|
||||
import Attribute from './ContactAttribute.vue';
|
||||
|
||||
export default {
|
||||
components: { Attribute },
|
||||
props: {
|
||||
contact: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
emits: ['update', 'createAttribute'],
|
||||
|
||||
computed: {
|
||||
additionalAttributes() {
|
||||
return this.contact.additional_attributes || {};
|
||||
},
|
||||
company() {
|
||||
const { company = {} } = this.contact;
|
||||
return company;
|
||||
},
|
||||
customAttributes() {
|
||||
const { custom_attributes: customAttributes = {} } = this.contact;
|
||||
return customAttributes;
|
||||
},
|
||||
customAttributekeys() {
|
||||
return Object.keys(this.customAttributes).filter(key => {
|
||||
const value = this.customAttributes[key];
|
||||
return value !== null && value !== undefined;
|
||||
});
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onEmailUpdate(value) {
|
||||
this.$emit('update', { email: value });
|
||||
},
|
||||
onPhoneUpdate(value) {
|
||||
this.$emit('update', { phone_number: value });
|
||||
},
|
||||
onLocationUpdate(value) {
|
||||
this.$emit('update', { location: value });
|
||||
},
|
||||
handleCustomCreate() {
|
||||
this.$emit('createAttribute');
|
||||
},
|
||||
onCustomAttributeUpdate(key, value) {
|
||||
this.$emit('update', { custom_attributes: { [key]: value } });
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="contact-fields">
|
||||
<h3 class="text-lg title">
|
||||
{{ $t('CONTACTS_PAGE.FIELDS') }}
|
||||
</h3>
|
||||
<Attribute
|
||||
:label="$t('CONTACT_PANEL.EMAIL_ADDRESS')"
|
||||
icon="mail"
|
||||
emoji=""
|
||||
:value="contact.email"
|
||||
show-edit
|
||||
@update="onEmailUpdate"
|
||||
/>
|
||||
<Attribute
|
||||
:label="$t('CONTACT_PANEL.PHONE_NUMBER')"
|
||||
icon="call"
|
||||
emoji=""
|
||||
:value="contact.phone_number"
|
||||
show-edit
|
||||
@update="onPhoneUpdate"
|
||||
/>
|
||||
<Attribute
|
||||
v-if="additionalAttributes.location"
|
||||
:label="$t('CONTACT_PANEL.LOCATION')"
|
||||
icon="map"
|
||||
emoji="🌍"
|
||||
:value="additionalAttributes.location"
|
||||
show-edit
|
||||
@update="onLocationUpdate"
|
||||
/>
|
||||
<div
|
||||
v-for="attribute in customAttributekeys"
|
||||
:key="attribute"
|
||||
class="custom-attribute--row"
|
||||
>
|
||||
<Attribute
|
||||
:label="attribute"
|
||||
icon="chevron-right"
|
||||
:value="customAttributes[attribute]"
|
||||
show-edit
|
||||
@update="value => onCustomAttributeUpdate(attribute, value)"
|
||||
/>
|
||||
</div>
|
||||
<woot-button
|
||||
size="small"
|
||||
variant="link"
|
||||
icon="add"
|
||||
@click="handleCustomCreate"
|
||||
>
|
||||
{{ $t('CUSTOM_ATTRIBUTES.ADD.TITLE') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.contact-fields {
|
||||
margin-top: var(--space-medium);
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-bottom: var(--space-normal);
|
||||
}
|
||||
</style>
|
||||
@@ -1,106 +0,0 @@
|
||||
<script>
|
||||
import EditContact from 'dashboard/routes/dashboard/conversation/contact/EditContact.vue';
|
||||
import NewConversation from 'dashboard/routes/dashboard/conversation/contact/NewConversation.vue';
|
||||
import AddCustomAttribute from 'dashboard/modules/contact/components/AddCustomAttribute.vue';
|
||||
import ContactIntro from './ContactIntro.vue';
|
||||
import ContactFields from './ContactFields.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AddCustomAttribute,
|
||||
ContactFields,
|
||||
ContactIntro,
|
||||
EditContact,
|
||||
NewConversation,
|
||||
},
|
||||
props: {
|
||||
contact: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showCustomAttributeModal: false,
|
||||
showEditModal: false,
|
||||
showConversationModal: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
enableNewConversation() {
|
||||
return this.contact && this.contact.id;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
toggleCustomAttributeModal() {
|
||||
this.showCustomAttributeModal = !this.showCustomAttributeModal;
|
||||
},
|
||||
toggleEditModal() {
|
||||
this.showEditModal = !this.showEditModal;
|
||||
},
|
||||
toggleConversationModal() {
|
||||
this.showConversationModal = !this.showConversationModal;
|
||||
},
|
||||
createCustomAttribute(data) {
|
||||
const { id } = this.contact;
|
||||
const { attributeValue, attributeName } = data;
|
||||
const updatedFields = {
|
||||
id,
|
||||
custom_attributes: {
|
||||
[attributeName]: attributeValue,
|
||||
},
|
||||
};
|
||||
this.updateContact(updatedFields);
|
||||
},
|
||||
updateField(data) {
|
||||
const { id } = this.contact;
|
||||
const updatedFields = {
|
||||
id,
|
||||
...data,
|
||||
};
|
||||
this.updateContact(updatedFields);
|
||||
},
|
||||
updateContact(contactItem) {
|
||||
this.$store.dispatch('contacts/update', contactItem);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="panel">
|
||||
<ContactIntro
|
||||
:contact="contact"
|
||||
@message="toggleConversationModal"
|
||||
@edit="toggleEditModal"
|
||||
/>
|
||||
<ContactFields
|
||||
:contact="contact"
|
||||
@update="updateField"
|
||||
@create-attribute="toggleCustomAttributeModal"
|
||||
/>
|
||||
<EditContact
|
||||
v-if="showEditModal"
|
||||
:show="showEditModal"
|
||||
:contact="contact"
|
||||
@cancel="toggleEditModal"
|
||||
/>
|
||||
<NewConversation
|
||||
v-if="enableNewConversation"
|
||||
:show="showConversationModal"
|
||||
:contact="contact"
|
||||
@cancel="toggleConversationModal"
|
||||
/>
|
||||
<AddCustomAttribute
|
||||
:show="showCustomAttributeModal"
|
||||
@cancel="toggleCustomAttributeModal"
|
||||
@create="createCustomAttribute"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.panel {
|
||||
padding: var(--space-normal) var(--space-normal);
|
||||
}
|
||||
</style>
|
||||
@@ -1,50 +0,0 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import NoteList from './components/NoteList.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NoteList,
|
||||
},
|
||||
props: {
|
||||
contactId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({ uiFlags: 'contactNotes/getUIFlags' }),
|
||||
notes() {
|
||||
return this.$store.getters['contactNotes/getAllNotesByContact'](
|
||||
this.contactId
|
||||
);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.fetchContactNotes();
|
||||
},
|
||||
methods: {
|
||||
fetchContactNotes() {
|
||||
const { contactId } = this;
|
||||
if (contactId) this.$store.dispatch('contactNotes/get', { contactId });
|
||||
},
|
||||
onAdd(content) {
|
||||
const { contactId } = this;
|
||||
this.$store.dispatch('contactNotes/create', { content, contactId });
|
||||
},
|
||||
onDelete(noteId) {
|
||||
const { contactId } = this;
|
||||
this.$store.dispatch('contactNotes/delete', { noteId, contactId });
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NoteList
|
||||
:is-fetching="uiFlags.isFetching"
|
||||
:notes="notes"
|
||||
@add="onAdd"
|
||||
@delete="onDelete"
|
||||
/>
|
||||
</template>
|
||||
@@ -1,62 +0,0 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
|
||||
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
|
||||
|
||||
const emit = defineEmits(['add']);
|
||||
|
||||
const noteContent = ref('');
|
||||
|
||||
const buttonDisabled = computed(() => noteContent.value === '');
|
||||
|
||||
const onAdd = () => {
|
||||
if (noteContent.value !== '') {
|
||||
emit('add', noteContent.value);
|
||||
}
|
||||
noteContent.value = '';
|
||||
};
|
||||
|
||||
const keyboardEvents = {
|
||||
'$mod+Enter': {
|
||||
action: () => onAdd(),
|
||||
allowOnFocusedInput: true,
|
||||
},
|
||||
};
|
||||
useKeyboardEvents(keyboardEvents);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col flex-grow p-4 mb-2 overflow-hidden bg-white border border-solid rounded-md shadow-sm border-slate-75 dark:border-slate-700 dark:bg-slate-900 text-slate-700 dark:text-slate-100"
|
||||
>
|
||||
<WootMessageEditor
|
||||
v-model="noteContent"
|
||||
class="input--note"
|
||||
:placeholder="$t('NOTES.ADD.PLACEHOLDER')"
|
||||
:enable-suggestions="false"
|
||||
/>
|
||||
<div class="flex justify-end w-full">
|
||||
<woot-button
|
||||
color-scheme="warning"
|
||||
:title="$t('NOTES.ADD.TITLE')"
|
||||
:is-disabled="buttonDisabled"
|
||||
@click="onAdd"
|
||||
>
|
||||
{{ $t('NOTES.ADD.BUTTON') }} {{ '(⌘⏎)' }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.input--note {
|
||||
&::v-deep .ProseMirror-menubar {
|
||||
padding: 0;
|
||||
margin-top: var(--space-minus-small);
|
||||
}
|
||||
|
||||
&::v-deep .ProseMirror-woot-style {
|
||||
max-height: 22.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,139 +0,0 @@
|
||||
<script>
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
||||
import { dynamicTime } from 'shared/helpers/timeHelper';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Thumbnail,
|
||||
},
|
||||
props: {
|
||||
id: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
note: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
user: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
createdAt: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
emits: ['delete'],
|
||||
setup() {
|
||||
const { formatMessage } = useMessageFormatter();
|
||||
return {
|
||||
formatMessage,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showDeleteModal: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
readableTime() {
|
||||
return dynamicTime(this.createdAt);
|
||||
},
|
||||
noteAuthor() {
|
||||
return this.user || {};
|
||||
},
|
||||
noteAuthorName() {
|
||||
return this.noteAuthor.name || this.$t('APP_GLOBAL.DELETED_USER');
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
toggleDeleteModal() {
|
||||
this.showDeleteModal = !this.showDeleteModal;
|
||||
},
|
||||
onDelete() {
|
||||
this.$emit('delete', this.id);
|
||||
},
|
||||
confirmDeletion() {
|
||||
this.onDelete();
|
||||
this.closeDelete();
|
||||
},
|
||||
closeDelete() {
|
||||
this.showDeleteModal = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col flex-grow p-4 mb-2 overflow-hidden bg-white border border-solid rounded-md shadow-sm border-slate-75 dark:border-slate-700 dark:bg-slate-900 text-slate-700 dark:text-slate-100 note-wrap"
|
||||
>
|
||||
<div class="flex items-end justify-between gap-1 text-xs">
|
||||
<div class="flex items-center">
|
||||
<Thumbnail
|
||||
:title="noteAuthorName"
|
||||
:src="noteAuthor.thumbnail"
|
||||
:username="noteAuthorName"
|
||||
size="20px"
|
||||
/>
|
||||
<div class="my-0 mx-1 p-0.5 flex flex-row gap-1">
|
||||
<span class="font-medium text-slate-800 dark:text-slate-100">
|
||||
{{ noteAuthorName }}
|
||||
</span>
|
||||
<span class="text-slate-700 dark:text-slate-100">
|
||||
{{ $t('NOTES.LIST.LABEL') }}
|
||||
</span>
|
||||
<span class="font-medium text-slate-700 dark:text-slate-100">
|
||||
{{ readableTime }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex invisible actions">
|
||||
<woot-button
|
||||
v-tooltip="$t('NOTES.CONTENT_HEADER.DELETE')"
|
||||
variant="smooth"
|
||||
size="tiny"
|
||||
icon="delete"
|
||||
color-scheme="secondary"
|
||||
@click="toggleDeleteModal"
|
||||
/>
|
||||
</div>
|
||||
<woot-delete-modal
|
||||
v-if="showDeleteModal"
|
||||
v-model:show="showDeleteModal"
|
||||
:on-close="closeDelete"
|
||||
:on-confirm="confirmDeletion"
|
||||
:title="$t('DELETE_NOTE.CONFIRM.TITLE')"
|
||||
:message="$t('DELETE_NOTE.CONFIRM.MESSAGE')"
|
||||
:confirm-text="$t('DELETE_NOTE.CONFIRM.YES')"
|
||||
:reject-text="$t('DELETE_NOTE.CONFIRM.NO')"
|
||||
/>
|
||||
</div>
|
||||
<p
|
||||
v-dompurify-html="formatMessage(note || '')"
|
||||
class="mt-4 note__content"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// For RTL direction view
|
||||
.app-rtl--wrapper {
|
||||
.note__content {
|
||||
::v-deep {
|
||||
p {
|
||||
unicode-bidi: plaintext;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.note-wrap:hover {
|
||||
.actions {
|
||||
@apply visible;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,61 +0,0 @@
|
||||
<script>
|
||||
import AddNote from './AddNote.vue';
|
||||
import ContactNote from './ContactNote.vue';
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AddNote,
|
||||
ContactNote,
|
||||
Spinner,
|
||||
},
|
||||
|
||||
props: {
|
||||
notes: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
isFetching: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ['add', 'edit', 'delete'],
|
||||
|
||||
methods: {
|
||||
onAddNote(value) {
|
||||
this.$emit('add', value);
|
||||
},
|
||||
onEditNote(value) {
|
||||
this.$emit('edit', value);
|
||||
},
|
||||
onDeleteNote(value) {
|
||||
this.$emit('delete', value);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<AddNote @add="onAddNote" />
|
||||
<ContactNote
|
||||
v-for="note in notes"
|
||||
:id="note.id"
|
||||
:key="note.id"
|
||||
:note="note.content"
|
||||
:user="note.user"
|
||||
:created-at="note.created_at"
|
||||
@edit="onEditNote"
|
||||
@delete="onDeleteNote"
|
||||
/>
|
||||
|
||||
<div v-if="isFetching" class="text-center p-4 text-base">
|
||||
<Spinner size="" />
|
||||
<span>{{ $t('NOTES.FETCHING_NOTES') }}</span>
|
||||
</div>
|
||||
<div v-else-if="!notes.length" class="text-center p-4 text-base">
|
||||
<span>{{ $t('NOTES.NOT_AVAILABLE') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,140 +0,0 @@
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
options: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
emits: ['add'],
|
||||
data() {
|
||||
return {
|
||||
content: '',
|
||||
date: '',
|
||||
label: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
buttonDisabled() {
|
||||
return this.content && this.date === '';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
resetValue() {
|
||||
this.content = '';
|
||||
this.date = '';
|
||||
},
|
||||
|
||||
optionSelected(event) {
|
||||
this.label = event.target.value;
|
||||
},
|
||||
|
||||
onAdd() {
|
||||
const task = {
|
||||
content: this.content,
|
||||
date: this.date,
|
||||
label: this.label,
|
||||
};
|
||||
this.$emit('add', task);
|
||||
this.resetValue();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="wrap">
|
||||
<div class="input-select-wrap">
|
||||
<textarea
|
||||
v-model="content"
|
||||
class="input--reminder"
|
||||
@keydown.enter.shift.exact="onAdd"
|
||||
/>
|
||||
<div class="select-wrap">
|
||||
<div class="select">
|
||||
<div class="input-group">
|
||||
<i class="ion-android-calendar input-group-label" />
|
||||
<input
|
||||
v-model="date"
|
||||
type="text"
|
||||
:placeholder="$t('REMINDER.FOOTER.DUE_DATE')"
|
||||
class="input-group-field"
|
||||
/>
|
||||
</div>
|
||||
<div class="task-wrap">
|
||||
<select class="task__type" @change="optionSelected($event)">
|
||||
<option value="" disabled selected>
|
||||
{{ $t('REMINDER.FOOTER.LABEL_TITLE') }}
|
||||
</option>
|
||||
<option
|
||||
v-for="option in options"
|
||||
:key="option.id"
|
||||
:value="option.id"
|
||||
>
|
||||
{{ option.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<woot-button
|
||||
size="tiny"
|
||||
color-scheme="primary"
|
||||
class-names="add-button"
|
||||
:title="$t('REMINDER.ADD_BUTTON.TITLE')"
|
||||
:is-disabled="buttonDisabled"
|
||||
@click="onAdd"
|
||||
>
|
||||
{{ $t('REMINDER.ADD_BUTTON.BUTTON') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.wrap {
|
||||
display: flex;
|
||||
margin-bottom: var(--space-smaller);
|
||||
width: 100%;
|
||||
|
||||
.input-select-wrap {
|
||||
padding: var(--space-small) var(--space-small);
|
||||
width: 100%;
|
||||
|
||||
.input--reminder {
|
||||
font-size: var(--font-size-mini);
|
||||
margin-bottom: var(--space-small);
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.select-wrap {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.select {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.input-group {
|
||||
margin-bottom: 0;
|
||||
font-size: var(--font-size-mini);
|
||||
|
||||
.input-group-field {
|
||||
height: var(--space-medium);
|
||||
font-size: var(--font-size-micro);
|
||||
}
|
||||
}
|
||||
|
||||
.task-wrap {
|
||||
.task__type {
|
||||
margin: 0 0 0 var(--space-smaller);
|
||||
height: var(--space-medium);
|
||||
width: fit-content;
|
||||
padding: 0 var(--space-two) 0 var(--space-smaller);
|
||||
font-size: var(--font-size-micro);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,175 +0,0 @@
|
||||
<script>
|
||||
import AccordionItem from 'dashboard/components/Accordion/AccordionItem.vue';
|
||||
import ContactConversations from 'dashboard/routes/dashboard/conversation/ContactConversations.vue';
|
||||
import ContactInfo from 'dashboard/routes/dashboard/conversation/contact/ContactInfo.vue';
|
||||
import ContactLabel from 'dashboard/routes/dashboard/contacts/components/ContactLabels.vue';
|
||||
import CustomAttributes from 'dashboard/routes/dashboard/conversation/customAttributes/CustomAttributes.vue';
|
||||
import Draggable from 'vuedraggable';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AccordionItem,
|
||||
ContactConversations,
|
||||
ContactInfo,
|
||||
ContactLabel,
|
||||
CustomAttributes,
|
||||
Draggable,
|
||||
},
|
||||
props: {
|
||||
contact: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
onClose: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
showAvatar: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
showCloseButton: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const {
|
||||
updateUISettings,
|
||||
isContactSidebarItemOpen,
|
||||
contactSidebarItemsOrder,
|
||||
toggleSidebarUIState,
|
||||
} = useUISettings();
|
||||
|
||||
return {
|
||||
updateUISettings,
|
||||
isContactSidebarItemOpen,
|
||||
contactSidebarItemsOrder,
|
||||
toggleSidebarUIState,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dragEnabled: true,
|
||||
contactSidebarItems: [],
|
||||
dragging: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
hasContactAttributes() {
|
||||
const { custom_attributes: customAttributes } = this.contact;
|
||||
return customAttributes && Object.keys(customAttributes).length;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.contactSidebarItems = this.contactSidebarItemsOrder;
|
||||
},
|
||||
methods: {
|
||||
onDragEnd() {
|
||||
this.dragging = false;
|
||||
this.updateUISettings({
|
||||
contact_sidebar_items_order: this.contactSidebarItems,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="relative w-1/4 h-full min-h-screen overflow-y-auto text-sm bg-white dark:bg-slate-900 border-slate-50 dark:border-slate-800/50"
|
||||
:class="showAvatar ? 'border-l border-solid ' : 'border-r border-solid'"
|
||||
>
|
||||
<ContactInfo
|
||||
:show-close-button="showCloseButton"
|
||||
:show-avatar="showAvatar"
|
||||
:contact="contact"
|
||||
close-icon-name="dismiss"
|
||||
@panel-close="onClose"
|
||||
@toggle-panel="onClose"
|
||||
/>
|
||||
<div class="list-group">
|
||||
<Draggable
|
||||
:list="contactSidebarItems"
|
||||
:disabled="!dragEnabled"
|
||||
ghost-class="ghost"
|
||||
item-key="name"
|
||||
@start="dragging = true"
|
||||
@end="onDragEnd"
|
||||
>
|
||||
<template #item="{ element }">
|
||||
<div :key="element.name" class="list-group-item">
|
||||
<div v-if="element.name === 'contact_attributes'">
|
||||
<AccordionItem
|
||||
:title="$t('CONVERSATION_SIDEBAR.ACCORDION.CONTACT_ATTRIBUTES')"
|
||||
:is-open="isContactSidebarItemOpen('is_ct_custom_attr_open')"
|
||||
compact
|
||||
@toggle="
|
||||
value => toggleSidebarUIState('is_ct_custom_attr_open', value)
|
||||
"
|
||||
>
|
||||
<CustomAttributes
|
||||
:contact-id="contact.id"
|
||||
attribute-type="contact_attribute"
|
||||
attribute-from="contact_panel"
|
||||
:custom-attributes="contact.custom_attributes"
|
||||
:empty-state-message="
|
||||
$t('CONTACT_PANEL.SIDEBAR_SECTIONS.NO_RECORDS_FOUND')
|
||||
"
|
||||
/>
|
||||
</AccordionItem>
|
||||
</div>
|
||||
<div v-if="element.name === 'contact_labels'">
|
||||
<AccordionItem
|
||||
:title="$t('CONTACT_PANEL.SIDEBAR_SECTIONS.CONTACT_LABELS')"
|
||||
:is-open="isContactSidebarItemOpen('is_ct_labels_open')"
|
||||
@toggle="
|
||||
value => toggleSidebarUIState('is_ct_labels_open', value)
|
||||
"
|
||||
>
|
||||
<ContactLabel :contact-id="contact.id" class="contact-labels" />
|
||||
</AccordionItem>
|
||||
</div>
|
||||
<div v-if="element.name === 'previous_conversation'">
|
||||
<AccordionItem
|
||||
:title="
|
||||
$t('CONTACT_PANEL.SIDEBAR_SECTIONS.PREVIOUS_CONVERSATIONS')
|
||||
"
|
||||
:is-open="isContactSidebarItemOpen('is_ct_prev_conv_open')"
|
||||
compact
|
||||
@toggle="
|
||||
value => toggleSidebarUIState('is_ct_prev_conv_open', value)
|
||||
"
|
||||
>
|
||||
<ContactConversations
|
||||
v-if="contact.id"
|
||||
:contact-id="contact.id"
|
||||
conversation-id=""
|
||||
/>
|
||||
</AccordionItem>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Draggable>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
::v-deep {
|
||||
.contact--profile {
|
||||
@apply pb-3 mb-4;
|
||||
}
|
||||
}
|
||||
|
||||
.list-group {
|
||||
.list-group-item {
|
||||
@apply bg-white dark:bg-slate-900;
|
||||
}
|
||||
}
|
||||
|
||||
.conversation--details {
|
||||
@apply py-0 px-4;
|
||||
}
|
||||
</style>
|
||||
@@ -1,85 +0,0 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import LabelSelector from 'dashboard/components/widgets/LabelSelector.vue';
|
||||
|
||||
export default {
|
||||
components: { LabelSelector },
|
||||
props: {
|
||||
contactId: {
|
||||
type: [String, Number],
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
savedLabels() {
|
||||
const availableContactLabels = this.$store.getters[
|
||||
'contactLabels/getContactLabels'
|
||||
](this.contactId);
|
||||
return this.allLabels.filter(({ title }) =>
|
||||
availableContactLabels.includes(title)
|
||||
);
|
||||
},
|
||||
|
||||
...mapGetters({
|
||||
allLabels: 'labels/getLabels',
|
||||
}),
|
||||
},
|
||||
|
||||
watch: {
|
||||
contactId(newContactId, prevContactId) {
|
||||
if (newContactId && newContactId !== prevContactId) {
|
||||
this.fetchLabels(newContactId);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
const { contactId } = this;
|
||||
this.fetchLabels(contactId);
|
||||
},
|
||||
|
||||
methods: {
|
||||
async onUpdateLabels(selectedLabels) {
|
||||
try {
|
||||
await this.$store.dispatch('contactLabels/update', {
|
||||
contactId: this.contactId,
|
||||
labels: selectedLabels,
|
||||
});
|
||||
} catch (error) {
|
||||
useAlert(this.$t('CONTACT_PANEL.LABELS.CONTACT.ERROR'));
|
||||
}
|
||||
},
|
||||
|
||||
addItem(value) {
|
||||
const result = this.savedLabels.map(item => item.title);
|
||||
result.push(value.title);
|
||||
this.onUpdateLabels(result);
|
||||
},
|
||||
|
||||
removeItem(value) {
|
||||
const result = this.savedLabels
|
||||
.map(label => label.title)
|
||||
.filter(label => label !== value);
|
||||
this.onUpdateLabels(result);
|
||||
},
|
||||
|
||||
async fetchLabels(contactId) {
|
||||
if (!contactId) {
|
||||
return;
|
||||
}
|
||||
this.$store.dispatch('contactLabels/get', contactId);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LabelSelector
|
||||
:all-labels="allLabels"
|
||||
:saved-labels="savedLabels"
|
||||
@add="addItem"
|
||||
@remove="removeItem"
|
||||
/>
|
||||
</template>
|
||||
-368
@@ -1,368 +0,0 @@
|
||||
<script>
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import FilterInputBox from '../../../../components/widgets/FilterInput/Index.vue';
|
||||
import countries from 'shared/constants/countries.js';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { filterAttributeGroups } from '../contactFilterItems';
|
||||
import { useFilter } from 'shared/composables/useFilter';
|
||||
import * as OPERATORS from 'dashboard/components/widgets/FilterInput/FilterOperatorTypes.js';
|
||||
import { CONTACTS_EVENTS } from '../../../../helper/AnalyticsHelper/events';
|
||||
import { validateConversationOrContactFilters } from 'dashboard/helper/validations.js';
|
||||
import { useTrack } from 'dashboard/composables';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
FilterInputBox,
|
||||
},
|
||||
props: {
|
||||
onClose: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
initialFilterTypes: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
initialAppliedFilters: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
isSegmentsView: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
activeSegmentName: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
emits: ['applyFilter', 'clearFilters', 'updateSegment'],
|
||||
setup() {
|
||||
const { setFilterAttributes } = useFilter({
|
||||
filteri18nKey: 'CONTACTS_FILTER',
|
||||
attributeModel: 'contact_attribute',
|
||||
});
|
||||
return {
|
||||
setFilterAttributes,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
show: true,
|
||||
appliedFilters: this.initialAppliedFilters,
|
||||
activeSegmentNewName: this.activeSegmentName,
|
||||
filterTypes: this.initialFilterTypes,
|
||||
filterGroups: [],
|
||||
allCustomAttributes: [],
|
||||
filterAttributeGroups,
|
||||
attributeModel: 'contact_attribute',
|
||||
filtersFori18n: 'CONTACTS_FILTER',
|
||||
validationErrors: {},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
getAppliedContactFilters: 'contacts/getAppliedContactFilters',
|
||||
}),
|
||||
hasAppliedFilters() {
|
||||
return this.getAppliedContactFilters.length;
|
||||
},
|
||||
filterModalHeaderTitle() {
|
||||
return !this.isSegmentsView
|
||||
? this.$t('CONTACTS_FILTER.TITLE')
|
||||
: this.$t('CONTACTS_FILTER.EDIT_CUSTOM_SEGMENT');
|
||||
},
|
||||
filterModalSubTitle() {
|
||||
return !this.isSegmentsView
|
||||
? this.$t('CONTACTS_FILTER.SUBTITLE')
|
||||
: this.$t('CONTACTS_FILTER.CUSTOM_VIEWS_SUBTITLE');
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
const { filterGroups, filterTypes } = this.setFilterAttributes();
|
||||
this.filterTypes = [...this.filterTypes, ...filterTypes];
|
||||
this.filterGroups = filterGroups;
|
||||
|
||||
if (this.getAppliedContactFilters.length && !this.isSegmentsView) {
|
||||
this.appliedFilters = [...this.getAppliedContactFilters];
|
||||
} else if (!this.isSegmentsView) {
|
||||
this.appliedFilters.push({
|
||||
attribute_key: 'name',
|
||||
filter_operator: 'equal_to',
|
||||
values: '',
|
||||
query_operator: 'and',
|
||||
attribute_model: 'standard',
|
||||
});
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getOperatorTypes(key) {
|
||||
switch (key) {
|
||||
case 'list':
|
||||
return OPERATORS.OPERATOR_TYPES_1;
|
||||
case 'text':
|
||||
return OPERATORS.OPERATOR_TYPES_3;
|
||||
case 'number':
|
||||
return OPERATORS.OPERATOR_TYPES_1;
|
||||
case 'link':
|
||||
return OPERATORS.OPERATOR_TYPES_1;
|
||||
case 'date':
|
||||
return OPERATORS.OPERATOR_TYPES_4;
|
||||
case 'checkbox':
|
||||
return OPERATORS.OPERATOR_TYPES_1;
|
||||
default:
|
||||
return OPERATORS.OPERATOR_TYPES_1;
|
||||
}
|
||||
},
|
||||
customAttributeInputType(key) {
|
||||
switch (key) {
|
||||
case 'date':
|
||||
return 'date';
|
||||
case 'text':
|
||||
return 'plain_text';
|
||||
case 'list':
|
||||
return 'search_select';
|
||||
case 'checkbox':
|
||||
return 'search_select';
|
||||
default:
|
||||
return 'plain_text';
|
||||
}
|
||||
},
|
||||
getAttributeModel(key) {
|
||||
const type = this.filterTypes.find(filter => filter.attributeKey === key);
|
||||
return type.attributeModel;
|
||||
},
|
||||
getInputType(key, operator) {
|
||||
if (key === 'created_at' || key === 'last_activity_at')
|
||||
if (operator === 'days_before') return 'plain_text';
|
||||
const type = this.filterTypes.find(filter => filter.attributeKey === key);
|
||||
return type?.inputType;
|
||||
},
|
||||
getOperators(key) {
|
||||
const type = this.filterTypes.find(filter => filter.attributeKey === key);
|
||||
return type?.filterOperators;
|
||||
},
|
||||
getDropdownValues(type) {
|
||||
const allCustomAttributes = this.$store.getters[
|
||||
'attributes/getAttributesByModel'
|
||||
](this.attributeModel);
|
||||
const isCustomAttributeCheckbox = allCustomAttributes.find(attr => {
|
||||
return (
|
||||
attr.attribute_key === type &&
|
||||
attr.attribute_display_type === 'checkbox'
|
||||
);
|
||||
});
|
||||
if (isCustomAttributeCheckbox || type === 'blocked') {
|
||||
return [
|
||||
{
|
||||
id: true,
|
||||
name: this.$t('FILTER.ATTRIBUTE_LABELS.TRUE'),
|
||||
},
|
||||
{
|
||||
id: false,
|
||||
name: this.$t('FILTER.ATTRIBUTE_LABELS.FALSE'),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const isCustomAttributeList = allCustomAttributes.find(attr => {
|
||||
return (
|
||||
attr.attribute_key === type && attr.attribute_display_type === 'list'
|
||||
);
|
||||
});
|
||||
|
||||
if (isCustomAttributeList) {
|
||||
return allCustomAttributes
|
||||
.find(attr => attr.attribute_key === type)
|
||||
.attribute_values.map(item => {
|
||||
return {
|
||||
id: item,
|
||||
name: item,
|
||||
};
|
||||
});
|
||||
}
|
||||
switch (type) {
|
||||
case 'country_code':
|
||||
return countries;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
appendNewFilter() {
|
||||
if (this.isSegmentsView) {
|
||||
this.setQueryOperatorOnLastQuery();
|
||||
} else {
|
||||
this.appliedFilters.push({
|
||||
attribute_key: 'name',
|
||||
filter_operator: 'equal_to',
|
||||
values: '',
|
||||
query_operator: 'and',
|
||||
});
|
||||
}
|
||||
},
|
||||
setQueryOperatorOnLastQuery() {
|
||||
const lastItemIndex = this.appliedFilters.length - 1;
|
||||
this.appliedFilters[lastItemIndex] = {
|
||||
...this.appliedFilters[lastItemIndex],
|
||||
query_operator: 'and',
|
||||
};
|
||||
this.$nextTick(() => {
|
||||
this.appliedFilters.push({
|
||||
attribute_key: 'name',
|
||||
filter_operator: 'equal_to',
|
||||
values: '',
|
||||
query_operator: 'and',
|
||||
});
|
||||
});
|
||||
},
|
||||
removeFilter(index) {
|
||||
if (this.appliedFilters.length <= 1) {
|
||||
useAlert(this.$t('CONTACTS_FILTER.FILTER_DELETE_ERROR'));
|
||||
} else {
|
||||
this.appliedFilters.splice(index, 1);
|
||||
}
|
||||
},
|
||||
submitFilterQuery() {
|
||||
this.validationErrors = validateConversationOrContactFilters(
|
||||
this.appliedFilters
|
||||
);
|
||||
if (Object.keys(this.validationErrors).length === 0) {
|
||||
this.$store.dispatch(
|
||||
'contacts/setContactFilters',
|
||||
JSON.parse(JSON.stringify(this.appliedFilters))
|
||||
);
|
||||
this.$emit('applyFilter', this.appliedFilters);
|
||||
useTrack(CONTACTS_EVENTS.APPLY_FILTER, {
|
||||
applied_filters: this.appliedFilters.map(filter => ({
|
||||
key: filter.attribute_key,
|
||||
operator: filter.filter_operator,
|
||||
query_operator: filter.query_operator,
|
||||
})),
|
||||
});
|
||||
}
|
||||
},
|
||||
updateSegment() {
|
||||
this.$emit(
|
||||
'updateSegment',
|
||||
this.appliedFilters,
|
||||
this.activeSegmentNewName
|
||||
);
|
||||
},
|
||||
resetFilter(index, currentFilter) {
|
||||
this.appliedFilters[index].filter_operator = this.filterTypes.find(
|
||||
filter => filter.attributeKey === currentFilter.attribute_key
|
||||
).filterOperators[0].value;
|
||||
this.appliedFilters[index].values = '';
|
||||
},
|
||||
showUserInput(operatorType) {
|
||||
if (operatorType === 'is_present' || operatorType === 'is_not_present')
|
||||
return false;
|
||||
return true;
|
||||
},
|
||||
clearFilters() {
|
||||
this.$emit('clearFilters');
|
||||
this.onClose();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<woot-modal-header :header-title="filterModalHeaderTitle">
|
||||
<p class="text-slate-600 dark:text-slate-200">
|
||||
{{ filterModalSubTitle }}
|
||||
</p>
|
||||
</woot-modal-header>
|
||||
<div class="p-8">
|
||||
<div v-if="isSegmentsView">
|
||||
<label class="input-label" :class="{ error: !activeSegmentNewName }">
|
||||
{{ $t('CONTACTS_FILTER.SEGMENT_LABEL') }}
|
||||
<input
|
||||
v-model="activeSegmentNewName"
|
||||
type="text"
|
||||
class="bg-white folder-input border-slate-75 dark:border-slate-600 dark:bg-slate-900 text-slate-900 dark:text-slate-100"
|
||||
/>
|
||||
<span v-if="!activeSegmentNewName" class="message">
|
||||
{{ $t('CONTACTS_FILTER.EMPTY_VALUE_ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
<label class="input-label">
|
||||
{{ $t('CONTACTS_FILTER.SEGMENT_QUERY_LABEL') }}
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="p-4 mb-4 border border-solid rounded-lg bg-slate-25 dark:bg-slate-900 border-slate-50 dark:border-slate-700/50"
|
||||
>
|
||||
<FilterInputBox
|
||||
v-for="(filter, i) in appliedFilters"
|
||||
:key="i"
|
||||
v-model="appliedFilters[i]"
|
||||
:filter-groups="filterGroups"
|
||||
grouped-filters
|
||||
:input-type="
|
||||
getInputType(
|
||||
appliedFilters[i].attribute_key,
|
||||
appliedFilters[i].filter_operator
|
||||
)
|
||||
"
|
||||
:operators="getOperators(appliedFilters[i].attribute_key)"
|
||||
:dropdown-values="getDropdownValues(appliedFilters[i].attribute_key)"
|
||||
:show-query-operator="i !== appliedFilters.length - 1"
|
||||
:show-user-input="showUserInput(appliedFilters[i].filter_operator)"
|
||||
:error-message="
|
||||
validationErrors[`filter_${i}`]
|
||||
? $t(`CONTACTS_FILTER.ERRORS.VALUE_REQUIRED`)
|
||||
: ''
|
||||
"
|
||||
@reset-filter="resetFilter(i, appliedFilters[i])"
|
||||
@remove-filter="removeFilter(i)"
|
||||
/>
|
||||
<div class="flex items-center gap-2 mt-4">
|
||||
<woot-button
|
||||
icon="add"
|
||||
color-scheme="success"
|
||||
variant="smooth"
|
||||
size="small"
|
||||
@click="appendNewFilter"
|
||||
>
|
||||
{{ $t('CONTACTS_FILTER.ADD_NEW_FILTER') }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
v-if="hasAppliedFilters && !isSegmentsView"
|
||||
icon="subtract"
|
||||
color-scheme="alert"
|
||||
variant="smooth"
|
||||
size="small"
|
||||
@click="clearFilters"
|
||||
>
|
||||
{{ $t('CONTACTS_FILTER.CLEAR_ALL_FILTERS') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<div class="flex flex-row justify-end w-full gap-2 px-0 py-2">
|
||||
<woot-button class="button clear" @click.prevent="onClose">
|
||||
{{ $t('CONTACTS_FILTER.CANCEL_BUTTON_LABEL') }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
v-if="isSegmentsView"
|
||||
:disabled="!activeSegmentNewName"
|
||||
@click="updateSegment"
|
||||
>
|
||||
{{ $t('CONTACTS_FILTER.UPDATE_BUTTON_LABEL') }}
|
||||
</woot-button>
|
||||
<woot-button v-else @click="submitFilterQuery">
|
||||
{{ $t('CONTACTS_FILTER.SUBMIT_BUTTON_LABEL') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.folder-input {
|
||||
@apply w-[50%];
|
||||
}
|
||||
</style>
|
||||
@@ -1,190 +0,0 @@
|
||||
<script setup>
|
||||
import { h, ref, computed, defineEmits } from 'vue';
|
||||
import {
|
||||
useVueTable,
|
||||
createColumnHelper,
|
||||
getCoreRowModel,
|
||||
} from '@tanstack/vue-table';
|
||||
import { dynamicTime } from 'shared/helpers/timeHelper';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
import EmptyState from 'dashboard/components/widgets/EmptyState.vue';
|
||||
|
||||
// Table components
|
||||
import Table from 'dashboard/components/table/Table.vue';
|
||||
import NameCell from './ContactsTable/NameCell.vue';
|
||||
import EmailCell from './ContactsTable/EmailCell.vue';
|
||||
import TelCell from './ContactsTable/TelCell.vue';
|
||||
import CountryCell from './ContactsTable/CountryCell.vue';
|
||||
import ProfilesCell from './ContactsTable/ProfilesCell.vue';
|
||||
|
||||
const props = defineProps({
|
||||
contacts: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
showSearchEmptyState: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['onSortChange']);
|
||||
const { t } = useI18n();
|
||||
|
||||
const tableData = computed(() => {
|
||||
if (props.isLoading) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return props.contacts.map(item => {
|
||||
// Note: The attributes used here is in snake case
|
||||
// as it simplier the sort attribute calculation
|
||||
const additional = item.additional_attributes || {};
|
||||
const { last_activity_at: lastActivityAt } = item;
|
||||
const { created_at: createdAt } = item;
|
||||
return {
|
||||
...item,
|
||||
profiles: additional.social_profiles || {},
|
||||
last_activity_at: lastActivityAt ? dynamicTime(lastActivityAt) : null,
|
||||
created_at: createdAt ? dynamicTime(createdAt) : null,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
const defaulSpanRender = cellProps =>
|
||||
h(
|
||||
'span',
|
||||
{
|
||||
class: cellProps.getValue() ? '' : 'text-slate-300 dark:text-slate-700',
|
||||
},
|
||||
cellProps.getValue() ? cellProps.getValue() : '---'
|
||||
);
|
||||
|
||||
const columnHelper = createColumnHelper();
|
||||
const columns = [
|
||||
columnHelper.accessor('name', {
|
||||
header: t('CONTACTS_PAGE.LIST.TABLE_HEADER.NAME'),
|
||||
cell: cellProps => h(NameCell, cellProps),
|
||||
size: 250,
|
||||
}),
|
||||
columnHelper.accessor('email', {
|
||||
header: t('CONTACTS_PAGE.LIST.TABLE_HEADER.EMAIL_ADDRESS'),
|
||||
cell: cellProps => h(EmailCell, { email: cellProps.getValue() }),
|
||||
size: 250,
|
||||
}),
|
||||
columnHelper.accessor('phone_number', {
|
||||
header: t('CONTACTS_PAGE.LIST.TABLE_HEADER.PHONE_NUMBER'),
|
||||
size: 200,
|
||||
cell: cellProps =>
|
||||
h(TelCell, {
|
||||
phoneNumber: cellProps.getValue(),
|
||||
defaultCountry: cellProps.row.original.country_code,
|
||||
}),
|
||||
}),
|
||||
columnHelper.accessor('company_name', {
|
||||
header: t('CONTACTS_PAGE.LIST.TABLE_HEADER.COMPANY'),
|
||||
size: 200,
|
||||
cell: defaulSpanRender,
|
||||
}),
|
||||
columnHelper.accessor('city', {
|
||||
header: t('CONTACTS_PAGE.LIST.TABLE_HEADER.CITY'),
|
||||
cell: defaulSpanRender,
|
||||
size: 200,
|
||||
}),
|
||||
columnHelper.accessor('country', {
|
||||
header: t('CONTACTS_PAGE.LIST.TABLE_HEADER.COUNTRY'),
|
||||
size: 200,
|
||||
cell: cellProps =>
|
||||
h(CountryCell, {
|
||||
countryCode: cellProps.row.original.country_code,
|
||||
country: cellProps.getValue(),
|
||||
}),
|
||||
}),
|
||||
columnHelper.accessor('profiles', {
|
||||
header: t('CONTACTS_PAGE.LIST.TABLE_HEADER.SOCIAL_PROFILES'),
|
||||
size: 200,
|
||||
enableSorting: false,
|
||||
cell: cellProps =>
|
||||
h(ProfilesCell, {
|
||||
profiles: cellProps.getValue(),
|
||||
}),
|
||||
}),
|
||||
columnHelper.accessor('last_activity_at', {
|
||||
header: t('CONTACTS_PAGE.LIST.TABLE_HEADER.LAST_ACTIVITY'),
|
||||
size: 200,
|
||||
cell: defaulSpanRender,
|
||||
}),
|
||||
columnHelper.accessor('created_at', {
|
||||
header: t('CONTACTS_PAGE.LIST.TABLE_HEADER.CREATED_AT'),
|
||||
size: 200,
|
||||
cell: defaulSpanRender,
|
||||
}),
|
||||
];
|
||||
|
||||
// type ColumnSort = {
|
||||
// id: string
|
||||
// desc: boolean
|
||||
// }
|
||||
// type SortingState = ColumnSort[]
|
||||
const sortingState = ref([{ last_activity_at: 'desc' }]);
|
||||
|
||||
const table = useVueTable({
|
||||
get data() {
|
||||
return tableData.value;
|
||||
},
|
||||
columns,
|
||||
enableMultiSort: false,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
state: {
|
||||
get sorting() {
|
||||
return sortingState.value;
|
||||
},
|
||||
},
|
||||
onSortingChange: updater => {
|
||||
// onSortingChange returns a callback that allows us to trigger the sort when we need
|
||||
// See more docs here: https://tanstack.com/table/latest/docs/api/features/sorting#onsortingchange
|
||||
// IMO, I don't like this API, but it's what we have for now. Would be great if we could just listen the the changes
|
||||
// to the sorting state and emit the event when it changes
|
||||
// But we can easily wrap this later as a separate composable
|
||||
const updatedSortState = updater(sortingState.value);
|
||||
// we pick the first item from the array, as we are not using multi-sorting
|
||||
const [sort] = updatedSortState;
|
||||
|
||||
if (sort) {
|
||||
sortingState.value = updatedSortState;
|
||||
emit('onSortChange', { [sort.id]: sort.desc ? 'desc' : 'asc' });
|
||||
} else {
|
||||
// If the sorting is empty, we reset to the default sorting
|
||||
sortingState.value = [{ last_activity_at: 'desc' }];
|
||||
emit('onSortChange', {});
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="flex-1 h-full overflow-auto bg-white dark:bg-slate-900">
|
||||
<section class="overflow-x-auto">
|
||||
<Table fixed :table="table" type="compact" />
|
||||
</section>
|
||||
|
||||
<EmptyState
|
||||
v-if="showSearchEmptyState"
|
||||
:title="$t('CONTACTS_PAGE.LIST.404')"
|
||||
/>
|
||||
<EmptyState
|
||||
v-else-if="!isLoading && !contacts.length"
|
||||
:title="$t('CONTACTS_PAGE.LIST.NO_CONTACTS')"
|
||||
/>
|
||||
<div v-if="isLoading" class="flex items-center justify-center text-base">
|
||||
<Spinner />
|
||||
<span>{{ $t('CONTACTS_PAGE.LIST.LOADING_MESSAGE') }}</span>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
-36
@@ -1,36 +0,0 @@
|
||||
<script setup>
|
||||
import BaseCell from 'dashboard/components/table/BaseCell.vue';
|
||||
import { computed } from 'vue';
|
||||
import { getCountryFlag } from 'dashboard/helper/flag';
|
||||
|
||||
const props = defineProps({
|
||||
countryCode: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
country: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
const countryFlag = computed(() => {
|
||||
if (!props.countryCode) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return getCountryFlag(props.countryCode);
|
||||
});
|
||||
|
||||
const formattedCountryName = computed(() => {
|
||||
if (!props.country) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return `${countryFlag.value} ${props.country}`;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseCell :content="formattedCountryName" />
|
||||
</template>
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
<script setup>
|
||||
import BaseCell from 'dashboard/components/table/BaseCell.vue';
|
||||
defineProps({
|
||||
email: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseCell>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer nofollow"
|
||||
class="text-woot-500 dark:text-woot-500"
|
||||
:href="`mailto:${email}`"
|
||||
>
|
||||
{{ email }}
|
||||
</a>
|
||||
</BaseCell>
|
||||
</template>
|
||||
-52
@@ -1,52 +0,0 @@
|
||||
<script setup>
|
||||
import BaseCell from 'dashboard/components/table/BaseCell.vue';
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { inject } from 'vue';
|
||||
|
||||
defineProps({
|
||||
row: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const route = useRoute();
|
||||
const openContactInfoPanel = inject('openContactInfoPanel');
|
||||
const isRTL = useMapGetter('accounts/isRTL');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseCell>
|
||||
<woot-button
|
||||
variant="clear"
|
||||
class="!px-0"
|
||||
@click="() => openContactInfoPanel(row.original.id)"
|
||||
>
|
||||
<div class="items-center flex" :class="{ 'flex-row-reverse': isRTL }">
|
||||
<Thumbnail
|
||||
size="32px"
|
||||
:src="row.original.thumbnail"
|
||||
:username="row.original.name"
|
||||
:status="row.original.availability_status"
|
||||
/>
|
||||
<div class="items-start flex flex-col my-0 mx-2">
|
||||
<h6 class="overflow-hidden text-base whitespace-nowrap text-ellipsis">
|
||||
<router-link
|
||||
:to="`/app/accounts/${route.params.accountId}/contacts/${row.original.id}`"
|
||||
class="text-sm font-medium m-0 capitalize"
|
||||
>
|
||||
{{ row.original.name }}
|
||||
</router-link>
|
||||
</h6>
|
||||
<button
|
||||
class="button clear small link text-slate-600 dark:text-slate-200"
|
||||
>
|
||||
{{ $t('CONTACTS_PAGE.LIST.VIEW_DETAILS') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</woot-button>
|
||||
</BaseCell>
|
||||
</template>
|
||||
-33
@@ -1,33 +0,0 @@
|
||||
<script setup>
|
||||
import BaseCell from 'dashboard/components/table/BaseCell.vue';
|
||||
import { computed } from 'vue';
|
||||
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue';
|
||||
|
||||
const props = defineProps({
|
||||
profiles: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const filteredProfiles = computed(() =>
|
||||
Object.keys(props.profiles).filter(profile => props.profiles[profile])
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseCell class="flex gap-2 items-center text-slate-300 dark:text-slate-400">
|
||||
<template v-if="filteredProfiles.length">
|
||||
<a
|
||||
v-for="profile in filteredProfiles"
|
||||
:key="profile"
|
||||
:href="`https://${profile}.com/${profiles[profile]}`"
|
||||
target="_blank"
|
||||
class="hover:text-slate-500"
|
||||
rel="noopener noreferrer nofollow"
|
||||
>
|
||||
<FluentIcon class="size-4" :icon="`brand-${profile}`" />
|
||||
</a>
|
||||
</template>
|
||||
</BaseCell>
|
||||
</template>
|
||||
-40
@@ -1,40 +0,0 @@
|
||||
<script setup>
|
||||
import BaseCell from 'dashboard/components/table/BaseCell.vue';
|
||||
import { computed } from 'vue';
|
||||
import { parsePhoneNumber } from 'libphonenumber-js';
|
||||
|
||||
const props = defineProps({
|
||||
phoneNumber: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
defaultCountry: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
const formattedNumber = computed(() => {
|
||||
if (!props.phoneNumber) {
|
||||
return '';
|
||||
}
|
||||
|
||||
try {
|
||||
const parsedNumber = parsePhoneNumber(
|
||||
props.phoneNumber,
|
||||
props.defaultCountry
|
||||
);
|
||||
|
||||
if (parsedNumber) {
|
||||
return parsedNumber.formatInternational();
|
||||
}
|
||||
return props.phoneNumber;
|
||||
} catch (error) {
|
||||
return props.phoneNumber;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseCell :content="formattedNumber" />
|
||||
</template>
|
||||
@@ -1,475 +0,0 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
|
||||
import ContactsHeader from './Header.vue';
|
||||
import ContactsTable from './ContactsTable.vue';
|
||||
import ContactInfoPanel from './ContactInfoPanel.vue';
|
||||
import CreateContact from 'dashboard/routes/dashboard/conversation/contact/CreateContact.vue';
|
||||
import TableFooter from 'dashboard/components/widgets/TableFooter.vue';
|
||||
import ImportContacts from './ImportContacts.vue';
|
||||
import ContactsAdvancedFilters from './ContactsAdvancedFilters.vue';
|
||||
import contactFilterItems from '../contactFilterItems';
|
||||
import filterQueryGenerator from '../../../../helper/filterQueryGenerator';
|
||||
import AddCustomViews from 'dashboard/routes/dashboard/customviews/AddCustomViews.vue';
|
||||
import DeleteCustomViews from 'dashboard/routes/dashboard/customviews/DeleteCustomViews.vue';
|
||||
import { CONTACTS_EVENTS } from '../../../../helper/AnalyticsHelper/events';
|
||||
import countries from 'shared/constants/countries.js';
|
||||
import { generateValuesForEditCustomViews } from 'dashboard/helper/customViewsHelper';
|
||||
import { useTrack } from 'dashboard/composables';
|
||||
|
||||
const DEFAULT_PAGE = 1;
|
||||
const FILTER_TYPE_CONTACT = 1;
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ContactsHeader,
|
||||
ContactsTable,
|
||||
TableFooter,
|
||||
ContactInfoPanel,
|
||||
CreateContact,
|
||||
ImportContacts,
|
||||
ContactsAdvancedFilters,
|
||||
AddCustomViews,
|
||||
DeleteCustomViews,
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
openContactInfoPanel: this.openContactInfoPanel,
|
||||
};
|
||||
},
|
||||
props: {
|
||||
label: { type: String, default: '' },
|
||||
segmentsId: {
|
||||
type: [String, Number],
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
searchQuery: '',
|
||||
showCreateModal: false,
|
||||
showImportModal: false,
|
||||
selectedContactId: '',
|
||||
sortConfig: { last_activity_at: 'desc' },
|
||||
showFiltersModal: false,
|
||||
contactFilterItems: contactFilterItems.map(filter => ({
|
||||
...filter,
|
||||
attributeName: this.$t(
|
||||
`CONTACTS_FILTER.ATTRIBUTES.${filter.attributeI18nKey}`
|
||||
),
|
||||
})),
|
||||
segmentsQuery: {},
|
||||
filterType: FILTER_TYPE_CONTACT,
|
||||
showAddSegmentsModal: false,
|
||||
showDeleteSegmentsModal: false,
|
||||
appliedFilter: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
records: 'contacts/getContacts',
|
||||
uiFlags: 'contacts/getUIFlags',
|
||||
meta: 'contacts/getMeta',
|
||||
segments: 'customViews/getContactCustomViews',
|
||||
getAppliedContactFilters: 'contacts/getAppliedContactFilters',
|
||||
}),
|
||||
showEmptySearchResult() {
|
||||
const hasEmptyResults = !!this.searchQuery && this.records.length === 0;
|
||||
return hasEmptyResults;
|
||||
},
|
||||
hasAppliedFilters() {
|
||||
return this.getAppliedContactFilters.length;
|
||||
},
|
||||
hasActiveSegments() {
|
||||
return this.activeSegment && this.segmentsId !== 0;
|
||||
},
|
||||
isContactAndLabelDashboard() {
|
||||
return (
|
||||
this.$route.name === 'contacts_dashboard' ||
|
||||
this.$route.name === 'contacts_labels_dashboard'
|
||||
);
|
||||
},
|
||||
pageTitle() {
|
||||
if (this.hasActiveSegments) {
|
||||
return this.activeSegment.name;
|
||||
}
|
||||
if (this.label) {
|
||||
return `#${this.label}`;
|
||||
}
|
||||
return this.$t('CONTACTS_PAGE.HEADER');
|
||||
},
|
||||
selectedContact() {
|
||||
if (this.selectedContactId) {
|
||||
const contact = this.records.find(
|
||||
item => this.selectedContactId === item.id
|
||||
);
|
||||
return contact;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
showContactViewPane() {
|
||||
return this.selectedContactId !== '';
|
||||
},
|
||||
wrapClass() {
|
||||
return this.showContactViewPane ? 'w-[75%]' : 'w-full';
|
||||
},
|
||||
pageParameter() {
|
||||
const selectedPageNumber = Number(this.$route.query?.page);
|
||||
return !Number.isNaN(selectedPageNumber) &&
|
||||
selectedPageNumber >= DEFAULT_PAGE
|
||||
? selectedPageNumber
|
||||
: DEFAULT_PAGE;
|
||||
},
|
||||
activeSegment() {
|
||||
if (this.segmentsId) {
|
||||
const [firstValue] = this.segments.filter(
|
||||
view => view.id === Number(this.segmentsId)
|
||||
);
|
||||
return firstValue;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
activeSegmentName() {
|
||||
return this.activeSegment?.name;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
label() {
|
||||
this.fetchContacts(DEFAULT_PAGE);
|
||||
if (this.hasAppliedFilters) {
|
||||
this.clearFilters();
|
||||
}
|
||||
},
|
||||
activeSegment() {
|
||||
if (this.hasActiveSegments) {
|
||||
const payload = this.activeSegment.query;
|
||||
this.fetchSavedFilteredContact(payload, DEFAULT_PAGE);
|
||||
}
|
||||
if (this.hasAppliedFilters && this.$route.name === 'contacts_dashboard') {
|
||||
this.fetchFilteredContacts(DEFAULT_PAGE);
|
||||
} else {
|
||||
this.fetchContacts(DEFAULT_PAGE);
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.fetchContacts(this.pageParameter);
|
||||
},
|
||||
methods: {
|
||||
updatePageParam(page) {
|
||||
window.history.pushState({}, null, `${this.$route.path}?page=${page}`);
|
||||
},
|
||||
getSortAttribute() {
|
||||
let sortAttr = Object.keys(this.sortConfig).reduce((acc, sortKey) => {
|
||||
const sortOrder = this.sortConfig[sortKey];
|
||||
if (sortOrder) {
|
||||
const sortOrderSign = sortOrder === 'asc' ? '' : '-';
|
||||
return `${sortOrderSign}${sortKey}`;
|
||||
}
|
||||
return acc;
|
||||
}, '');
|
||||
if (!sortAttr) {
|
||||
this.sortConfig = { last_activity_at: 'desc' };
|
||||
sortAttr = '-last_activity_at';
|
||||
}
|
||||
return sortAttr;
|
||||
},
|
||||
fetchContacts(page) {
|
||||
if (this.isContactAndLabelDashboard) {
|
||||
this.updatePageParam(page);
|
||||
let value = '';
|
||||
if (this.searchQuery.charAt(0) === '+') {
|
||||
value = this.searchQuery.substring(1);
|
||||
} else {
|
||||
value = this.searchQuery;
|
||||
}
|
||||
const requestParams = {
|
||||
page,
|
||||
sortAttr: this.getSortAttribute(),
|
||||
label: this.label,
|
||||
};
|
||||
if (!value) {
|
||||
this.$store.dispatch('contacts/get', requestParams);
|
||||
} else {
|
||||
this.$store.dispatch('contacts/search', {
|
||||
search: encodeURIComponent(value),
|
||||
...requestParams,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
fetchSavedFilteredContact(payload, page) {
|
||||
if (this.hasActiveSegments) {
|
||||
this.updatePageParam(page);
|
||||
this.$store.dispatch('contacts/filter', {
|
||||
queryPayload: payload,
|
||||
page,
|
||||
});
|
||||
}
|
||||
},
|
||||
fetchFilteredContacts(page) {
|
||||
if (this.hasAppliedFilters) {
|
||||
const payload = this.segmentsQuery;
|
||||
this.updatePageParam(page);
|
||||
this.$store.dispatch('contacts/filter', {
|
||||
queryPayload: payload,
|
||||
page,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
onInputSearch(event) {
|
||||
const newQuery = event.target.value;
|
||||
const refetchAllContacts = !!this.searchQuery && newQuery === '';
|
||||
this.searchQuery = newQuery;
|
||||
if (refetchAllContacts) {
|
||||
this.fetchContacts(DEFAULT_PAGE);
|
||||
}
|
||||
},
|
||||
onSearchSubmit() {
|
||||
this.selectedContactId = '';
|
||||
if (this.searchQuery) {
|
||||
this.fetchContacts(DEFAULT_PAGE);
|
||||
}
|
||||
},
|
||||
onPageChange(page) {
|
||||
this.selectedContactId = '';
|
||||
if (this.segmentsId !== 0) {
|
||||
const payload = this.activeSegment.query;
|
||||
this.fetchSavedFilteredContact(payload, page);
|
||||
}
|
||||
if (this.hasAppliedFilters) {
|
||||
this.fetchFilteredContacts(page);
|
||||
} else {
|
||||
this.fetchContacts(page);
|
||||
}
|
||||
},
|
||||
openContactInfoPanel(contactId) {
|
||||
this.selectedContactId = contactId;
|
||||
this.showContactInfoPanelPane = true;
|
||||
},
|
||||
closeContactInfoPanel() {
|
||||
this.selectedContactId = '';
|
||||
this.showContactInfoPanelPane = false;
|
||||
},
|
||||
onToggleCreate() {
|
||||
this.showCreateModal = !this.showCreateModal;
|
||||
},
|
||||
onToggleSaveFilters() {
|
||||
this.showAddSegmentsModal = true;
|
||||
},
|
||||
onCloseAddSegmentsModal() {
|
||||
this.showAddSegmentsModal = false;
|
||||
},
|
||||
onToggleDeleteFilters() {
|
||||
this.showDeleteSegmentsModal = true;
|
||||
},
|
||||
onCloseDeleteSegmentsModal() {
|
||||
this.showDeleteSegmentsModal = false;
|
||||
},
|
||||
onToggleImport() {
|
||||
this.showImportModal = !this.showImportModal;
|
||||
},
|
||||
onSortChange(params) {
|
||||
this.sortConfig = params;
|
||||
this.fetchContacts(this.meta.currentPage);
|
||||
|
||||
const sortBy =
|
||||
Object.entries(params).find(pair => Boolean(pair[1])) || [];
|
||||
|
||||
useTrack(CONTACTS_EVENTS.APPLY_SORT, {
|
||||
appliedOn: sortBy[0],
|
||||
order: sortBy[1],
|
||||
});
|
||||
},
|
||||
onToggleFilters() {
|
||||
if (this.hasActiveSegments) {
|
||||
this.initializeSegmentToFilterModal(this.activeSegment);
|
||||
}
|
||||
this.showFiltersModal = true;
|
||||
},
|
||||
closeAdvanceFiltersModal() {
|
||||
this.showFiltersModal = false;
|
||||
this.appliedFilter = [];
|
||||
},
|
||||
onApplyFilter(payload) {
|
||||
this.closeContactInfoPanel();
|
||||
this.segmentsQuery = filterQueryGenerator(payload);
|
||||
this.$store.dispatch('contacts/filter', {
|
||||
queryPayload: filterQueryGenerator(payload),
|
||||
});
|
||||
this.showFiltersModal = false;
|
||||
},
|
||||
onUpdateSegment(payload, segmentName) {
|
||||
const payloadData = {
|
||||
...this.activeSegment,
|
||||
name: segmentName,
|
||||
query: filterQueryGenerator(payload),
|
||||
};
|
||||
this.$store.dispatch('customViews/update', payloadData);
|
||||
this.closeAdvanceFiltersModal();
|
||||
},
|
||||
clearFilters() {
|
||||
this.$store.dispatch('contacts/clearContactFilters');
|
||||
this.fetchContacts(this.pageParameter);
|
||||
},
|
||||
onExportSubmit() {
|
||||
let query = { payload: [] };
|
||||
|
||||
if (this.hasActiveSegments) {
|
||||
query = this.activeSegment.query;
|
||||
} else if (this.hasAppliedFilters) {
|
||||
query = filterQueryGenerator(this.getAppliedContactFilters);
|
||||
}
|
||||
|
||||
try {
|
||||
this.$store.dispatch('contacts/export', {
|
||||
...query,
|
||||
label: this.label,
|
||||
});
|
||||
useAlert(this.$t('EXPORT_CONTACTS.SUCCESS_MESSAGE'));
|
||||
} catch (error) {
|
||||
useAlert(error.message || this.$t('EXPORT_CONTACTS.ERROR_MESSAGE'));
|
||||
}
|
||||
},
|
||||
setParamsForEditSegmentModal() {
|
||||
// Here we are setting the params for edit segment modal to show the existing values.
|
||||
|
||||
// For custom attributes we get only attribute key.
|
||||
// So we are mapping it to find the input type of the attribute to show in the edit segment modal.
|
||||
const params = {
|
||||
countries: countries,
|
||||
filterTypes: contactFilterItems,
|
||||
allCustomAttributes:
|
||||
this.$store.getters['attributes/getAttributesByModel'](
|
||||
'contact_attribute'
|
||||
),
|
||||
};
|
||||
return params;
|
||||
},
|
||||
initializeSegmentToFilterModal(activeSegment) {
|
||||
// Here we are setting the params for edit segment modal.
|
||||
// To show the existing values. when we click on edit segment button.
|
||||
|
||||
// Here we get the query from the active segment.
|
||||
// And we are mapping the query to the actual values.
|
||||
// To show in the edit segment modal by the help of generateValuesForEditCustomViews helper.
|
||||
const query = activeSegment?.query?.payload;
|
||||
if (!Array.isArray(query)) return;
|
||||
|
||||
this.appliedFilter.push(
|
||||
...query.map(filter => ({
|
||||
attribute_key: filter.attribute_key,
|
||||
attribute_model: filter.attribute_model,
|
||||
filter_operator: filter.filter_operator,
|
||||
values: Array.isArray(filter.values)
|
||||
? generateValuesForEditCustomViews(
|
||||
filter,
|
||||
this.setParamsForEditSegmentModal()
|
||||
)
|
||||
: [],
|
||||
query_operator: filter.query_operator,
|
||||
custom_attribute_type: filter.custom_attribute_type,
|
||||
}))
|
||||
);
|
||||
},
|
||||
openSavedItemInSegment() {
|
||||
const lastItemInSegments = this.segments[this.segments.length - 1];
|
||||
const lastItemId = lastItemInSegments.id;
|
||||
this.$router.push({
|
||||
name: 'contacts_segments_dashboard',
|
||||
params: { id: lastItemId },
|
||||
});
|
||||
},
|
||||
openLastItemAfterDeleteInSegment() {
|
||||
if (this.segments.length > 0) {
|
||||
this.openSavedItemInSegment();
|
||||
} else {
|
||||
this.$router.push({ name: 'contacts_dashboard' });
|
||||
this.fetchContacts(DEFAULT_PAGE);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-row w-full">
|
||||
<div class="flex flex-col h-full" :class="wrapClass">
|
||||
<ContactsHeader
|
||||
:search-query="searchQuery"
|
||||
:header-title="pageTitle"
|
||||
:segments-id="segmentsId"
|
||||
this-selected-contact-id=""
|
||||
@on-input-search="onInputSearch"
|
||||
@on-toggle-create="onToggleCreate"
|
||||
@on-toggle-filter="onToggleFilters"
|
||||
@on-search-submit="onSearchSubmit"
|
||||
@on-toggle-import="onToggleImport"
|
||||
@on-export-submit="onExportSubmit"
|
||||
@on-toggle-save-filter="onToggleSaveFilters"
|
||||
@on-toggle-delete-filter="onToggleDeleteFilters"
|
||||
@on-toggle-edit-filter="onToggleFilters"
|
||||
/>
|
||||
<ContactsTable
|
||||
:contacts="records"
|
||||
:show-search-empty-state="showEmptySearchResult"
|
||||
:is-loading="uiFlags.isFetching"
|
||||
:active-contact-id="selectedContactId"
|
||||
@on-sort-change="onSortChange"
|
||||
/>
|
||||
<TableFooter
|
||||
class="border-t border-slate-75 dark:border-slate-700/50"
|
||||
:current-page="Number(meta.currentPage)"
|
||||
:total-count="meta.count"
|
||||
:page-size="15"
|
||||
@page-change="onPageChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<AddCustomViews
|
||||
v-if="showAddSegmentsModal"
|
||||
:custom-views-query="segmentsQuery"
|
||||
:filter-type="filterType"
|
||||
:open-last-saved-item="openSavedItemInSegment"
|
||||
@close="onCloseAddSegmentsModal"
|
||||
/>
|
||||
<DeleteCustomViews
|
||||
v-if="showDeleteSegmentsModal"
|
||||
v-model:show="showDeleteSegmentsModal"
|
||||
:active-custom-view="activeSegment"
|
||||
:custom-views-id="segmentsId"
|
||||
:active-filter-type="filterType"
|
||||
:open-last-item-after-delete="openLastItemAfterDeleteInSegment"
|
||||
@close="onCloseDeleteSegmentsModal"
|
||||
/>
|
||||
|
||||
<ContactInfoPanel
|
||||
v-if="showContactViewPane"
|
||||
:contact="selectedContact"
|
||||
:on-close="closeContactInfoPanel"
|
||||
/>
|
||||
<CreateContact :show="showCreateModal" @cancel="onToggleCreate" />
|
||||
<woot-modal v-model:show="showImportModal" :on-close="onToggleImport">
|
||||
<ImportContacts v-if="showImportModal" :on-close="onToggleImport" />
|
||||
</woot-modal>
|
||||
<woot-modal
|
||||
v-model:show="showFiltersModal"
|
||||
:on-close="closeAdvanceFiltersModal"
|
||||
size="medium"
|
||||
>
|
||||
<ContactsAdvancedFilters
|
||||
v-if="showFiltersModal"
|
||||
:on-close="closeAdvanceFiltersModal"
|
||||
:initial-filter-types="contactFilterItems"
|
||||
:initial-applied-filters="appliedFilter"
|
||||
:active-segment-name="activeSegmentName"
|
||||
:is-segments-view="hasActiveSegments"
|
||||
@apply-filter="onApplyFilter"
|
||||
@update-segment="onUpdateSegment"
|
||||
@clear-filters="clearFilters"
|
||||
/>
|
||||
</woot-modal>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,230 +0,0 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useAdmin } from 'dashboard/composables/useAdmin';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
headerTitle: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
searchQuery: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
segmentsId: {
|
||||
type: [String, Number],
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
emits: [
|
||||
'onToggleSaveFilter',
|
||||
'onToggleEditFilter',
|
||||
'onToggleDeleteFilter',
|
||||
'onToggleCreate',
|
||||
'onToggleFilter',
|
||||
'onToggleImport',
|
||||
'onExportSubmit',
|
||||
'onSearchSubmit',
|
||||
'onInputSearch',
|
||||
],
|
||||
setup() {
|
||||
const { isAdmin } = useAdmin();
|
||||
return {
|
||||
isAdmin,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showCreateModal: false,
|
||||
showImportModal: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
searchButtonClass() {
|
||||
return this.searchQuery !== ''
|
||||
? 'opacity-100 translate-x-0 visible'
|
||||
: '-translate-x-px opacity-0 invisible';
|
||||
},
|
||||
...mapGetters({
|
||||
getAppliedContactFilters: 'contacts/getAppliedContactFilters',
|
||||
}),
|
||||
hasAppliedFilters() {
|
||||
return this.getAppliedContactFilters.length;
|
||||
},
|
||||
hasActiveSegments() {
|
||||
return this.segmentsId !== 0;
|
||||
},
|
||||
exportDescription() {
|
||||
return this.hasAppliedFilters
|
||||
? this.$t('EXPORT_CONTACTS.CONFIRM.FILTERED_MESSAGE')
|
||||
: this.$t('EXPORT_CONTACTS.CONFIRM.MESSAGE');
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onToggleSegmentsModal() {
|
||||
this.$emit('onToggleSaveFilter');
|
||||
},
|
||||
onToggleEditSegmentsModal() {
|
||||
this.$emit('onToggleEditFilter');
|
||||
},
|
||||
onToggleDeleteSegmentsModal() {
|
||||
this.$emit('onToggleDeleteFilter');
|
||||
},
|
||||
toggleCreate() {
|
||||
this.$emit('onToggleCreate');
|
||||
},
|
||||
toggleFilter() {
|
||||
this.$emit('onToggleFilter');
|
||||
},
|
||||
toggleImport() {
|
||||
this.$emit('onToggleImport');
|
||||
},
|
||||
async submitExport() {
|
||||
const ok =
|
||||
await this.$refs.confirmExportContactsDialog.showConfirmation();
|
||||
|
||||
if (ok) {
|
||||
this.$emit('onExportSubmit');
|
||||
}
|
||||
},
|
||||
submitSearch() {
|
||||
this.$emit('onSearchSubmit');
|
||||
},
|
||||
inputSearch(event) {
|
||||
this.$emit('onInputSearch', event);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header
|
||||
class="bg-white border-b dark:bg-slate-900 border-slate-50 dark:border-slate-800"
|
||||
>
|
||||
<div class="flex justify-between w-full px-4 py-2">
|
||||
<div class="flex flex-col items-center w-full gap-2 md:flex-row">
|
||||
<div class="flex justify-between w-full gap-2">
|
||||
<div
|
||||
class="flex items-center justify-center max-w-full min-w-[6.25rem]"
|
||||
>
|
||||
<woot-sidemenu-icon />
|
||||
<h1
|
||||
class="m-0 mx-2 my-0 overflow-hidden text-xl text-slate-900 dark:text-slate-100 whitespace-nowrap text-ellipsis"
|
||||
>
|
||||
{{ headerTitle }}
|
||||
</h1>
|
||||
</div>
|
||||
<div
|
||||
class="max-w-[400px] min-w-[100px] flex items-center relative mx-2"
|
||||
>
|
||||
<div class="flex items-center absolute h-full left-2.5">
|
||||
<fluent-icon
|
||||
icon="search"
|
||||
class="h-5 text-sm leading-9 text-slate-700 dark:text-slate-200"
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
:placeholder="$t('CONTACTS_PAGE.SEARCH_INPUT_PLACEHOLDER')"
|
||||
class="!pl-9 !pr-[3.75rem] !text-sm !w-full !h-[2.375rem] !m-0 border-slate-100 dark:border-slate-600"
|
||||
:value="searchQuery"
|
||||
@keyup.enter="submitSearch"
|
||||
@input="inputSearch"
|
||||
/>
|
||||
<woot-button
|
||||
:is-loading="false"
|
||||
class="absolute h-8 px-2 py-0 ml-2 transition-transform duration-100 ease-linear clear right-1"
|
||||
:class-names="searchButtonClass"
|
||||
@click="submitSearch"
|
||||
>
|
||||
{{ $t('CONTACTS_PAGE.SEARCH_BUTTON') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-1">
|
||||
<div v-if="hasActiveSegments" class="flex gap-2">
|
||||
<woot-button
|
||||
class="clear [&>span]:hidden xs:[&>span]:block"
|
||||
color-scheme="secondary"
|
||||
icon="edit"
|
||||
@click="onToggleEditSegmentsModal"
|
||||
>
|
||||
{{ $t('CONTACTS_PAGE.FILTER_CONTACTS_EDIT') }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
class="clear [&>span]:hidden xs:[&>span]:block"
|
||||
color-scheme="alert"
|
||||
icon="delete"
|
||||
@click="onToggleDeleteSegmentsModal"
|
||||
>
|
||||
{{ $t('CONTACTS_PAGE.FILTER_CONTACTS_DELETE') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
<div v-if="!hasActiveSegments" class="relative">
|
||||
<div
|
||||
v-if="hasAppliedFilters"
|
||||
class="absolute w-2 h-2 rounded-full top-1 right-3 bg-slate-500 dark:bg-slate-500"
|
||||
/>
|
||||
<woot-button
|
||||
class="clear [&>span]:hidden xs:[&>span]:block"
|
||||
color-scheme="secondary"
|
||||
data-testid="create-new-contact"
|
||||
icon="filter"
|
||||
@click="toggleFilter"
|
||||
>
|
||||
{{ $t('CONTACTS_PAGE.FILTER_CONTACTS') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
|
||||
<woot-button
|
||||
v-if="hasAppliedFilters && !hasActiveSegments"
|
||||
class="clear [&>span]:hidden xs:[&>span]:block"
|
||||
color-scheme="alert"
|
||||
variant="clear"
|
||||
icon="save"
|
||||
@click="onToggleSegmentsModal"
|
||||
>
|
||||
{{ $t('CONTACTS_PAGE.FILTER_CONTACTS_SAVE') }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
class="clear [&>span]:hidden xs:[&>span]:block"
|
||||
color-scheme="success"
|
||||
icon="person-add"
|
||||
data-testid="create-new-contact"
|
||||
@click="toggleCreate"
|
||||
>
|
||||
{{ $t('CREATE_CONTACT.BUTTON_LABEL') }}
|
||||
</woot-button>
|
||||
|
||||
<woot-button
|
||||
v-if="isAdmin"
|
||||
color-scheme="info"
|
||||
icon="upload"
|
||||
class="clear [&>span]:hidden xs:[&>span]:block"
|
||||
@click="toggleImport"
|
||||
>
|
||||
{{ $t('IMPORT_CONTACTS.BUTTON_LABEL') }}
|
||||
</woot-button>
|
||||
|
||||
<woot-button
|
||||
v-if="isAdmin"
|
||||
color-scheme="info"
|
||||
icon="download"
|
||||
class="clear [&>span]:hidden xs:[&>span]:block"
|
||||
@click="submitExport"
|
||||
>
|
||||
{{ $t('EXPORT_CONTACTS.BUTTON_LABEL') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<woot-confirm-modal
|
||||
ref="confirmExportContactsDialog"
|
||||
:title="$t('EXPORT_CONTACTS.CONFIRM.TITLE')"
|
||||
:description="exportDescription"
|
||||
:confirm-label="$t('EXPORT_CONTACTS.CONFIRM.YES')"
|
||||
:cancel-label="$t('EXPORT_CONTACTS.CONFIRM.NO')"
|
||||
/>
|
||||
</header>
|
||||
</template>
|
||||
@@ -1,96 +0,0 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { CONTACTS_EVENTS } from '../../../../helper/AnalyticsHelper/events';
|
||||
import Modal from '../../../../components/Modal.vue';
|
||||
import { useTrack } from 'dashboard/composables';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Modal,
|
||||
},
|
||||
props: {
|
||||
onClose: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
show: true,
|
||||
file: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
uiFlags: 'contacts/getUIFlags',
|
||||
}),
|
||||
csvUrl() {
|
||||
return '/downloads/import-contacts-sample.csv';
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
useTrack(CONTACTS_EVENTS.IMPORT_MODAL_OPEN);
|
||||
},
|
||||
methods: {
|
||||
async uploadFile() {
|
||||
try {
|
||||
if (!this.file) return;
|
||||
await this.$store.dispatch('contacts/import', this.file);
|
||||
this.onClose();
|
||||
useAlert(this.$t('IMPORT_CONTACTS.SUCCESS_MESSAGE'));
|
||||
useTrack(CONTACTS_EVENTS.IMPORT_SUCCESS);
|
||||
} catch (error) {
|
||||
useAlert(error.message || this.$t('IMPORT_CONTACTS.ERROR_MESSAGE'));
|
||||
useTrack(CONTACTS_EVENTS.IMPORT_FAILURE);
|
||||
}
|
||||
},
|
||||
handleFileUpload() {
|
||||
this.file = this.$refs.file.files[0];
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal v-model:show="show" :on-close="onClose">
|
||||
<div class="flex flex-col h-auto overflow-auto">
|
||||
<woot-modal-header :header-title="$t('IMPORT_CONTACTS.TITLE')">
|
||||
<p>
|
||||
{{ $t('IMPORT_CONTACTS.DESC') }}
|
||||
<a :href="csvUrl" download="import-contacts-sample">{{
|
||||
$t('IMPORT_CONTACTS.DOWNLOAD_LABEL')
|
||||
}}</a>
|
||||
</p>
|
||||
</woot-modal-header>
|
||||
<div class="flex flex-col p-8">
|
||||
<div class="w-full">
|
||||
<label>
|
||||
<span>{{ $t('IMPORT_CONTACTS.FORM.LABEL') }}</span>
|
||||
<input
|
||||
id="file"
|
||||
ref="file"
|
||||
type="file"
|
||||
accept="text/csv"
|
||||
@change="handleFileUpload"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex flex-row justify-end w-full gap-2 px-0 py-2">
|
||||
<div class="w-full">
|
||||
<woot-button
|
||||
:disabled="uiFlags.isCreating || !file"
|
||||
:loading="uiFlags.isCreating"
|
||||
@click="uploadFile"
|
||||
>
|
||||
{{ $t('IMPORT_CONTACTS.FORM.SUBMIT') }}
|
||||
</woot-button>
|
||||
<button class="button clear" @click.prevent="onClose">
|
||||
{{ $t('IMPORT_CONTACTS.FORM.CANCEL') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
@@ -1,126 +0,0 @@
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
id: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
isCompleted: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
date: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
emits: ['completed', 'edit', 'delete'],
|
||||
|
||||
methods: {
|
||||
onClick() {
|
||||
this.$emit('completed', this.isCompleted);
|
||||
},
|
||||
onEdit() {
|
||||
this.$emit('edit', this.id);
|
||||
},
|
||||
onDelete() {
|
||||
this.$emit('delete', this.id);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="reminder-wrap">
|
||||
<div class="status-wrap">
|
||||
<input :checked="isCompleted" type="radio" @click="onClick" />
|
||||
</div>
|
||||
<div class="wrap">
|
||||
<p class="content">
|
||||
{{ text }}
|
||||
</p>
|
||||
<div class="footer">
|
||||
<div class="meta">
|
||||
<woot-label
|
||||
:title="date"
|
||||
description="date"
|
||||
icon="ion-android-calendar"
|
||||
color-scheme="secondary"
|
||||
/>
|
||||
<woot-label
|
||||
:title="label"
|
||||
description="label"
|
||||
color-scheme="secondary"
|
||||
/>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<woot-button
|
||||
variant="smooth"
|
||||
size="small"
|
||||
icon="edit"
|
||||
color-scheme="secondary"
|
||||
class="action-button"
|
||||
@click="onEdit"
|
||||
/>
|
||||
<woot-button
|
||||
variant="smooth"
|
||||
size="small"
|
||||
icon="ion-trash-b"
|
||||
color-scheme="secondary"
|
||||
class="action-button"
|
||||
@click="onDelete"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.reminder-wrap {
|
||||
display: flex;
|
||||
margin-bottom: var(--space-smaller);
|
||||
|
||||
.status-wrap {
|
||||
padding: var(--space-small) var(--space-smaller);
|
||||
margin-top: var(--space-smaller);
|
||||
}
|
||||
|
||||
.wrap {
|
||||
padding: var(--space-small);
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.meta {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: none;
|
||||
|
||||
.action-button {
|
||||
margin-right: var(--space-small);
|
||||
height: var(--space-medium);
|
||||
width: var(--space-medium);
|
||||
}
|
||||
}
|
||||
|
||||
.reminder-wrap:hover {
|
||||
.actions {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,73 +0,0 @@
|
||||
<script>
|
||||
export default {
|
||||
emits: ['notes', 'events', 'conversation'],
|
||||
|
||||
methods: {
|
||||
onClickNotes() {
|
||||
this.$emit('notes');
|
||||
},
|
||||
|
||||
onClickEvents() {
|
||||
this.$emit('events');
|
||||
},
|
||||
|
||||
onClickConversation() {
|
||||
this.$emit('conversation');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="wrap">
|
||||
<div class="header">
|
||||
<h5 class="text-lg text-black-900 dark:text-slate-200">
|
||||
{{ $t('EVENTS.HEADER.TITLE') }}
|
||||
</h5>
|
||||
</div>
|
||||
<div class="button-wrap">
|
||||
<woot-button
|
||||
variant="hollow"
|
||||
size="tiny"
|
||||
color-scheme="secondary"
|
||||
class-names="pill-button"
|
||||
@click="onClickNotes"
|
||||
>
|
||||
{{ $t('EVENTS.BUTTON.PILL_BUTTON_NOTES') }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
variant="hollow"
|
||||
size="tiny"
|
||||
color-scheme="secondary"
|
||||
class-names="pill-button"
|
||||
@click="onClickEvents"
|
||||
>
|
||||
{{ $t('EVENTS.BUTTON.PILL_BUTTON_EVENTS') }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
variant="hollow"
|
||||
size="tiny"
|
||||
color-scheme="secondary"
|
||||
class-names="pill-button"
|
||||
@click="onClickConversation"
|
||||
>
|
||||
{{ $t('EVENTS.BUTTON.PILL_BUTTON_CONVO') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.wrap {
|
||||
width: 100%;
|
||||
padding: var(--space-normal);
|
||||
|
||||
.button-wrap {
|
||||
display: flex;
|
||||
|
||||
.pill-button {
|
||||
margin-right: var(--space-small);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,133 +0,0 @@
|
||||
<!-- Unused file deprecated -->
|
||||
<script>
|
||||
import { dynamicTime } from 'shared/helpers/timeHelper';
|
||||
export default {
|
||||
props: {
|
||||
eventType: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
eventPath: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
eventBody: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
timeStamp: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
emits: ['more'],
|
||||
|
||||
computed: {
|
||||
readableTime() {
|
||||
return dynamicTime(this.timeStamp);
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onClick() {
|
||||
this.$emit('more');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="timeline-card-wrap">
|
||||
<div class="icon-chatbox">
|
||||
<i class="ion-chatboxes" />
|
||||
</div>
|
||||
<div class="card-wrap">
|
||||
<div class="header">
|
||||
<div class="text-wrap">
|
||||
<h6 class="text-sm">
|
||||
{{ eventType }}
|
||||
</h6>
|
||||
<span class="event-path">{{ 'on' }} {{ eventPath }}</span>
|
||||
</div>
|
||||
<div class="date-wrap">
|
||||
<span>{{ readableTime }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="comment-wrap">
|
||||
<p class="comment">
|
||||
{{ eventBody }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="icon-more" @click="onClick">
|
||||
<i class="ion-android-more-vertical" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.timeline-card-wrap {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
color: var(--color-body);
|
||||
padding: var(--space-small);
|
||||
|
||||
.icon-chatbox {
|
||||
width: var(--space-large);
|
||||
height: var(--space-large);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
justify-content: center;
|
||||
border: 1px solid var(--color-border);
|
||||
background-color: var(--color-background);
|
||||
|
||||
.ion-chatboxes {
|
||||
font-size: var(--font-size-default);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.card-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
padding: var(--space-smaller) var(--space-normal) 0;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.text-wrap {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.event-path {
|
||||
font-size: var(--font-size-mini);
|
||||
margin-left: var(--space-smaller);
|
||||
}
|
||||
|
||||
.date-wrap {
|
||||
font-size: var(--font-size-micro);
|
||||
}
|
||||
}
|
||||
|
||||
.comment-wrap {
|
||||
border: 1px solid var(--color-border-light);
|
||||
|
||||
.comment {
|
||||
padding: var(--space-small);
|
||||
font-size: var(--font-size-mini);
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.icon-more {
|
||||
.ion-android-more-vertical {
|
||||
font-size: var(--font-size-medium);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
-46
@@ -1,46 +0,0 @@
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import AddReminder from '../AddReminder.vue';
|
||||
|
||||
let wrapper;
|
||||
|
||||
describe('AddReminder', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = shallowMount(AddReminder, {
|
||||
mocks: {
|
||||
$t: x => x,
|
||||
$store: { getters: {}, state: {} },
|
||||
},
|
||||
stubs: { WootButton: { template: '<button />' } },
|
||||
});
|
||||
});
|
||||
|
||||
it('tests resetValue', () => {
|
||||
const resetValue = vi.spyOn(wrapper.vm, 'resetValue');
|
||||
wrapper.vm.content = 'test';
|
||||
wrapper.vm.date = '08/11/2022';
|
||||
wrapper.vm.resetValue();
|
||||
expect(wrapper.vm.content).toEqual('');
|
||||
expect(wrapper.vm.date).toEqual('');
|
||||
expect(resetValue).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('tests optionSelected', () => {
|
||||
const optionSelected = vi.spyOn(wrapper.vm, 'optionSelected');
|
||||
wrapper.vm.label = '';
|
||||
wrapper.vm.optionSelected({ target: { value: 'test' } });
|
||||
expect(wrapper.vm.label).toEqual('test');
|
||||
expect(optionSelected).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('tests onAdd', () => {
|
||||
const onAdd = vi.spyOn(wrapper.vm, 'onAdd');
|
||||
wrapper.vm.label = 'label';
|
||||
wrapper.vm.content = 'content';
|
||||
wrapper.vm.date = '08/11/2022';
|
||||
wrapper.vm.onAdd();
|
||||
expect(onAdd).toHaveBeenCalled();
|
||||
expect(wrapper.emitted().add[0]).toEqual([
|
||||
{ content: 'content', date: '08/11/2022', label: 'label' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -1,67 +0,0 @@
|
||||
<script>
|
||||
import MessageFormatter from 'shared/helpers/MessageFormatter.js';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
customAttributes: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
listOfAttributes() {
|
||||
return Object.keys(this.customAttributes).filter(key => {
|
||||
const value = this.customAttributes[key];
|
||||
return value !== null && value !== undefined && value !== '';
|
||||
});
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
valueWithLink(attribute) {
|
||||
const parsedAttribute = this.parseAttributeToString(attribute);
|
||||
const messageFormatter = new MessageFormatter(parsedAttribute);
|
||||
return messageFormatter.formattedMessage;
|
||||
},
|
||||
parseAttributeToString(attribute) {
|
||||
switch (typeof attribute) {
|
||||
case 'string':
|
||||
return attribute;
|
||||
case 'object':
|
||||
return JSON.stringify(attribute);
|
||||
default:
|
||||
return `${attribute}`;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="custom-attributes--panel">
|
||||
<div
|
||||
v-for="attribute in listOfAttributes"
|
||||
:key="attribute"
|
||||
class="custom-attribute--row"
|
||||
>
|
||||
<div class="custom-attribute--row__attribute">
|
||||
{{ attribute }}
|
||||
</div>
|
||||
<div>
|
||||
<span v-dompurify-html="valueWithLink(customAttributes[attribute])" />
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="!listOfAttributes.length">
|
||||
{{ $t('CUSTOM_ATTRIBUTES.NOT_AVAILABLE') }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.custom-attributes--panel {
|
||||
margin-bottom: var(--space-normal);
|
||||
}
|
||||
|
||||
.custom-attribute--row__attribute {
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
@@ -92,9 +92,7 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="overflow-y-auto bg-white border-l dark:bg-slate-900 text-slate-900 dark:text-slate-300 border-slate-50 dark:border-slate-800/50 rtl:border-l-0 rtl:border-r contact--panel"
|
||||
>
|
||||
<div class="w-full">
|
||||
<ContactInfo
|
||||
:contact="contact"
|
||||
:channel-type="channelType"
|
||||
|
||||
@@ -193,7 +193,7 @@ export default {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="bg-white conversation-page dark:bg-slate-900">
|
||||
<section class="flex w-full h-full">
|
||||
<ChatList
|
||||
:show-conversation-list="showConversationList"
|
||||
:conversation-inbox="inboxId"
|
||||
@@ -219,11 +219,3 @@ export default {
|
||||
<CmdBarConversationSnooze />
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.conversation-page {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -9,7 +9,6 @@ import SocialIcons from './SocialIcons.vue';
|
||||
import EditContact from './EditContact.vue';
|
||||
import NewConversation from './NewConversation.vue';
|
||||
import ContactMergeModal from 'dashboard/modules/contact/ContactMergeModal.vue';
|
||||
import { getCountryFlag } from 'dashboard/helper/flag';
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
import {
|
||||
isAConversationRoute,
|
||||
@@ -127,8 +126,12 @@ export default {
|
||||
},
|
||||
findCountryFlag(countryCode, cityAndCountry) {
|
||||
try {
|
||||
const countryFlag = countryCode ? getCountryFlag(countryCode) : '🌎';
|
||||
return `${cityAndCountry} ${countryFlag}`;
|
||||
if (!countryCode) {
|
||||
return `${cityAndCountry} 🌎`;
|
||||
}
|
||||
|
||||
const code = countryCode?.toLowerCase();
|
||||
return `${cityAndCountry} <span class="fi fi-${code} size-3.5"></span>`;
|
||||
} catch (error) {
|
||||
return '';
|
||||
}
|
||||
@@ -171,7 +174,7 @@ export default {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative items-center w-full p-4 bg-white dark:bg-slate-900">
|
||||
<div class="relative items-center w-full p-4">
|
||||
<div class="flex flex-col w-full gap-2 text-left rtl:text-right">
|
||||
<div class="flex flex-row justify-between">
|
||||
<Thumbnail
|
||||
|
||||
@@ -87,10 +87,9 @@ export default {
|
||||
/>
|
||||
<span
|
||||
v-if="value"
|
||||
v-dompurify-html="value"
|
||||
class="overflow-hidden text-sm whitespace-nowrap text-ellipsis"
|
||||
>
|
||||
{{ value }}
|
||||
</span>
|
||||
/>
|
||||
<span v-else class="text-sm text-slate-300 dark:text-slate-600">{{
|
||||
$t('CONTACT_PANEL.NOT_AVAILABLE')
|
||||
}}</span>
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import ContactForm from './ContactForm.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ContactForm,
|
||||
},
|
||||
props: {
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ['cancel', 'update:show'],
|
||||
computed: {
|
||||
...mapGetters({
|
||||
uiFlags: 'contacts/getUIFlags',
|
||||
}),
|
||||
localShow: {
|
||||
get() {
|
||||
return this.show;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('update:show', value);
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onCancel() {
|
||||
this.$emit('cancel');
|
||||
},
|
||||
onSuccess() {
|
||||
this.$emit('cancel');
|
||||
},
|
||||
async onSubmit(contactItem) {
|
||||
await this.$store.dispatch('contacts/create', contactItem);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<woot-modal
|
||||
v-model:show="localShow"
|
||||
:on-close="onCancel"
|
||||
modal-type="right-aligned"
|
||||
>
|
||||
<div class="flex flex-col h-auto overflow-auto">
|
||||
<woot-modal-header
|
||||
:header-title="$t('CREATE_CONTACT.TITLE')"
|
||||
:header-content="$t('CREATE_CONTACT.DESC')"
|
||||
/>
|
||||
<ContactForm
|
||||
:in-progress="uiFlags.isCreating"
|
||||
:on-submit="onSubmit"
|
||||
@success="onSuccess"
|
||||
@cancel="onCancel"
|
||||
/>
|
||||
</div>
|
||||
</woot-modal>
|
||||
</template>
|
||||
@@ -54,11 +54,9 @@ export default {
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex justify-between items-center h-14 min-h-[3.5rem] px-4 py-2 bg-white dark:bg-slate-900 border-b border-slate-50 dark:border-slate-800/50"
|
||||
class="flex justify-between items-center h-20 min-h-[3.5rem] py-2 max-w-[960px] mx-auto w-full"
|
||||
>
|
||||
<h1
|
||||
class="flex items-center mb-0 text-2xl text-slate-900 dark:text-slate-100"
|
||||
>
|
||||
<h1 class="flex items-center mb-0 text-xl font-medium text-n-slate-12">
|
||||
<woot-sidemenu-icon v-if="showSidemenuIcon" />
|
||||
<BackButton
|
||||
v-if="showBackButton"
|
||||
@@ -66,23 +64,20 @@ export default {
|
||||
:back-url="backUrl"
|
||||
class="ml-2 mr-4"
|
||||
/>
|
||||
<fluent-icon
|
||||
v-if="icon"
|
||||
:icon="icon"
|
||||
:class="iconClass"
|
||||
class="hidden ml-1 mr-2 rtl:ml-2 rtl:mr-1 md:block"
|
||||
/>
|
||||
<slot />
|
||||
<span class="text-2xl font-medium text-slate-900 dark:text-slate-100">
|
||||
<span
|
||||
class="text-xl font-medium text-n-slate-12 font-interDisplay tracking-[0.3px]"
|
||||
>
|
||||
{{ headerTitle }}
|
||||
</span>
|
||||
</h1>
|
||||
<slot name="action-button" />
|
||||
<router-link
|
||||
v-if="showNewButton && isAdmin"
|
||||
:to="buttonRoute"
|
||||
class="button success button--fixed-top px-3.5 py-1 rounded-[5px] flex gap-2"
|
||||
class="button success px-3.5 py-1 rounded-[5px] flex gap-2"
|
||||
>
|
||||
<fluent-icon icon="add-circle" />
|
||||
<fluent-icon icon="add" />
|
||||
<span class="button__content">
|
||||
{{ buttonText }}
|
||||
</span>
|
||||
|
||||
@@ -20,10 +20,10 @@ defineProps({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col w-full h-full gap-8 font-inter">
|
||||
<div class="flex flex-col w-full h-full gap-8">
|
||||
<slot name="header" />
|
||||
<!-- Added to render any templates that should be rendered before body -->
|
||||
<div>
|
||||
<div class="mx-auto max-w-[960px] w-full pb-10">
|
||||
<slot name="preBody" />
|
||||
<slot v-if="isLoading" name="loading">
|
||||
<woot-loading-state :message="loadingMessage" />
|
||||
|
||||
@@ -8,9 +8,7 @@ defineProps({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col w-full h-full m-0 p-6 sm:py-8 lg:px-16 overflow-auto bg-white dark:bg-slate-900 font-inter"
|
||||
>
|
||||
<div class="flex flex-col w-full h-full m-0 overflow-auto bg-n-background">
|
||||
<div class="flex items-start w-full max-w-6xl mx-auto">
|
||||
<router-view v-slot="{ Component }">
|
||||
<keep-alive v-if="keepAlive">
|
||||
|
||||
@@ -22,9 +22,7 @@ const showNewButton = computed(
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-1 h-full justify-between flex-col m-0 bg-slate-25 dark:bg-slate-900 overflow-auto"
|
||||
>
|
||||
<div class="w-full h-full bg-n-background overflow-auto">
|
||||
<SettingsHeader
|
||||
button-route="new"
|
||||
:icon="icon"
|
||||
|
||||
@@ -161,100 +161,104 @@ export default {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-grow flex-shrink min-w-0 p-6 overflow-auto">
|
||||
<form v-if="!uiFlags.isFetchingItem" @submit.prevent="updateAccount">
|
||||
<div
|
||||
class="flex flex-row p-4 border-b border-slate-25 dark:border-slate-800"
|
||||
>
|
||||
<div
|
||||
class="flex-grow-0 flex-shrink-0 flex-[25%] min-w-0 py-4 pr-6 pl-0"
|
||||
>
|
||||
<h4 class="text-lg font-medium text-black-900 dark:text-slate-200">
|
||||
{{ $t('GENERAL_SETTINGS.FORM.GENERAL_SECTION.TITLE') }}
|
||||
</h4>
|
||||
<p>{{ $t('GENERAL_SETTINGS.FORM.GENERAL_SECTION.NOTE') }}</p>
|
||||
</div>
|
||||
<div class="p-4 flex-grow-0 flex-shrink-0 flex-[50%]">
|
||||
<label :class="{ error: v$.name.$error }">
|
||||
{{ $t('GENERAL_SETTINGS.FORM.NAME.LABEL') }}
|
||||
<input
|
||||
v-model="name"
|
||||
type="text"
|
||||
:placeholder="$t('GENERAL_SETTINGS.FORM.NAME.PLACEHOLDER')"
|
||||
@blur="v$.name.$touch"
|
||||
/>
|
||||
<span v-if="v$.name.$error" class="message">
|
||||
{{ $t('GENERAL_SETTINGS.FORM.NAME.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
<label :class="{ error: v$.locale.$error }">
|
||||
{{ $t('GENERAL_SETTINGS.FORM.LANGUAGE.LABEL') }}
|
||||
<select v-model="locale">
|
||||
<option
|
||||
v-for="lang in languagesSortedByCode"
|
||||
:key="lang.iso_639_1_code"
|
||||
:value="lang.iso_639_1_code"
|
||||
>
|
||||
{{ lang.name }}
|
||||
</option>
|
||||
</select>
|
||||
<span v-if="v$.locale.$error" class="message">
|
||||
{{ $t('GENERAL_SETTINGS.FORM.LANGUAGE.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
<label v-if="featureInboundEmailEnabled">
|
||||
{{ $t('GENERAL_SETTINGS.FORM.FEATURES.INBOUND_EMAIL_ENABLED') }}
|
||||
</label>
|
||||
<label v-if="featureCustomReplyDomainEnabled">
|
||||
{{
|
||||
$t('GENERAL_SETTINGS.FORM.FEATURES.CUSTOM_EMAIL_DOMAIN_ENABLED')
|
||||
}}
|
||||
</label>
|
||||
<label v-if="featureCustomReplyDomainEnabled">
|
||||
{{ $t('GENERAL_SETTINGS.FORM.DOMAIN.LABEL') }}
|
||||
<input
|
||||
v-model="domain"
|
||||
type="text"
|
||||
:placeholder="$t('GENERAL_SETTINGS.FORM.DOMAIN.PLACEHOLDER')"
|
||||
/>
|
||||
</label>
|
||||
<label v-if="featureCustomReplyEmailEnabled">
|
||||
{{ $t('GENERAL_SETTINGS.FORM.SUPPORT_EMAIL.LABEL') }}
|
||||
<input
|
||||
v-model="supportEmail"
|
||||
type="text"
|
||||
:placeholder="
|
||||
$t('GENERAL_SETTINGS.FORM.SUPPORT_EMAIL.PLACEHOLDER')
|
||||
"
|
||||
/>
|
||||
</label>
|
||||
<label
|
||||
v-if="showAutoResolutionConfig"
|
||||
:class="{ error: v$.autoResolveDuration.$error }"
|
||||
<div
|
||||
class="flex-grow flex-shrink min-w-0 p-6 overflow-auto shadow outline-1 outline outline-n-container rounded-2xl bg-n-solid-2 px-6 py-5 max-w-[960px] w-full mx-auto mb-10"
|
||||
>
|
||||
<div v-if="!uiFlags.isFetchingItem">
|
||||
<form @submit.prevent="updateAccount">
|
||||
<div class="flex flex-row p-4 border-n-weak border-b">
|
||||
<div
|
||||
class="flex-grow-0 flex-shrink-0 flex-[30%] min-w-0 py-4 pr-6 pl-0"
|
||||
>
|
||||
{{ $t('GENERAL_SETTINGS.FORM.AUTO_RESOLVE_DURATION.LABEL') }}
|
||||
<input
|
||||
v-model="autoResolveDuration"
|
||||
type="number"
|
||||
:placeholder="
|
||||
$t('GENERAL_SETTINGS.FORM.AUTO_RESOLVE_DURATION.PLACEHOLDER')
|
||||
"
|
||||
@blur="v$.autoResolveDuration.$touch"
|
||||
/>
|
||||
<span v-if="v$.autoResolveDuration.$error" class="message">
|
||||
{{ $t('GENERAL_SETTINGS.FORM.AUTO_RESOLVE_DURATION.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<h4 class="text-lg font-medium text-n-slate-12">
|
||||
{{ $t('GENERAL_SETTINGS.FORM.GENERAL_SECTION.TITLE') }}
|
||||
</h4>
|
||||
<p>{{ $t('GENERAL_SETTINGS.FORM.GENERAL_SECTION.NOTE') }}</p>
|
||||
</div>
|
||||
<div class="p-4 flex-grow-0 flex-shrink-0 flex-[50%]">
|
||||
<label :class="{ error: v$.name.$error }">
|
||||
{{ $t('GENERAL_SETTINGS.FORM.NAME.LABEL') }}
|
||||
<input
|
||||
v-model="name"
|
||||
type="text"
|
||||
:placeholder="$t('GENERAL_SETTINGS.FORM.NAME.PLACEHOLDER')"
|
||||
@blur="v$.name.$touch"
|
||||
/>
|
||||
<span v-if="v$.name.$error" class="message">
|
||||
{{ $t('GENERAL_SETTINGS.FORM.NAME.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
<label :class="{ error: v$.locale.$error }">
|
||||
{{ $t('GENERAL_SETTINGS.FORM.LANGUAGE.LABEL') }}
|
||||
<select v-model="locale">
|
||||
<option
|
||||
v-for="lang in languagesSortedByCode"
|
||||
:key="lang.iso_639_1_code"
|
||||
:value="lang.iso_639_1_code"
|
||||
>
|
||||
{{ lang.name }}
|
||||
</option>
|
||||
</select>
|
||||
<span v-if="v$.locale.$error" class="message">
|
||||
{{ $t('GENERAL_SETTINGS.FORM.LANGUAGE.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
<label v-if="featureInboundEmailEnabled">
|
||||
{{ $t('GENERAL_SETTINGS.FORM.FEATURES.INBOUND_EMAIL_ENABLED') }}
|
||||
</label>
|
||||
<label v-if="featureCustomReplyDomainEnabled">
|
||||
{{
|
||||
$t('GENERAL_SETTINGS.FORM.FEATURES.CUSTOM_EMAIL_DOMAIN_ENABLED')
|
||||
}}
|
||||
</label>
|
||||
<label v-if="featureCustomReplyDomainEnabled">
|
||||
{{ $t('GENERAL_SETTINGS.FORM.DOMAIN.LABEL') }}
|
||||
<input
|
||||
v-model="domain"
|
||||
type="text"
|
||||
:placeholder="$t('GENERAL_SETTINGS.FORM.DOMAIN.PLACEHOLDER')"
|
||||
/>
|
||||
</label>
|
||||
<label v-if="featureCustomReplyEmailEnabled">
|
||||
{{ $t('GENERAL_SETTINGS.FORM.SUPPORT_EMAIL.LABEL') }}
|
||||
<input
|
||||
v-model="supportEmail"
|
||||
type="text"
|
||||
:placeholder="$t('GENERAL_SETTINGS.FORM.SUPPORT_EMAIL.PLACEHOLDER')
|
||||
"
|
||||
/>
|
||||
</label>
|
||||
<label
|
||||
v-if="showAutoResolutionConfig"
|
||||
:class="{ error: v$.autoResolveDuration.$error }"
|
||||
>
|
||||
{{ $t('GENERAL_SETTINGS.FORM.AUTO_RESOLVE_DURATION.LABEL') }}
|
||||
<input
|
||||
v-model="autoResolveDuration"
|
||||
type="number"
|
||||
:placeholder="$t('GENERAL_SETTINGS.FORM.AUTO_RESOLVE_DURATION.PLACEHOLDER')
|
||||
"
|
||||
@blur="v$.autoResolveDuration.$touch"
|
||||
/>
|
||||
<span v-if="v$.autoResolveDuration.$error" class="message">
|
||||
{{ $t('GENERAL_SETTINGS.FORM.AUTO_RESOLVE_DURATION.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<div
|
||||
class="flex flex-row p-4 border-slate-25 dark:border-slate-700 text-black-900 dark:text-slate-300"
|
||||
>
|
||||
<woot-submit-button
|
||||
class="button nice"
|
||||
:button-text="$t('GENERAL_SETTINGS.SUBMIT')"
|
||||
:loading="isUpdating"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="flex flex-row p-4 text-n-slate-12">
|
||||
<div
|
||||
class="flex-grow-0 flex-shrink-0 flex-[25%] min-w-0 py-4 pr-6 pl-0"
|
||||
class="flex-grow-0 flex-shrink-0 flex-[30%] min-w-0 py-4 pr-6 pl-0"
|
||||
>
|
||||
<h4 class="text-lg font-medium text-black-900 dark:text-slate-200">
|
||||
<h4 class="text-lg font-medium text-n-slate-12">
|
||||
{{ $t('GENERAL_SETTINGS.FORM.ACCOUNT_ID.TITLE') }}
|
||||
</h4>
|
||||
<p>
|
||||
@@ -265,7 +269,7 @@ export default {
|
||||
<woot-code :script="getAccountId" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-4 text-sm text-center">
|
||||
<div class="p-4 text-sm text-center text-n-slate-11">
|
||||
<div>{{ `v${globalConfig.appVersion}` }}</div>
|
||||
<div v-if="hasAnUpdateAvailable && globalConfig.displayManifest">
|
||||
{{
|
||||
@@ -278,14 +282,7 @@ export default {
|
||||
<div>{{ `Build ${globalConfig.gitSha}` }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<woot-submit-button
|
||||
class="button nice success button--fixed-top"
|
||||
:button-text="$t('GENERAL_SETTINGS.SUBMIT')"
|
||||
:loading="isUpdating"
|
||||
/>
|
||||
</form>
|
||||
|
||||
<woot-loading-state v-if="uiFlags.isFetchingItem" />
|
||||
</div>
|
||||
<woot-loading-state v-else />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -45,7 +45,9 @@ export default {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-1 p-4 overflow-auto">
|
||||
<div
|
||||
class="flex-1 p-4 max-w-[960px] w-full mx-auto shadow outline-1 outline outline-n-container rounded-2xl bg-n-solid-2 px-6 py-5"
|
||||
>
|
||||
<div class="flex flex-row gap-4">
|
||||
<div class="w-full lg:w-3/5">
|
||||
<woot-loading-state
|
||||
@@ -76,7 +78,7 @@ export default {
|
||||
<woot-button
|
||||
color-scheme="success"
|
||||
class-names="button--fixed-top"
|
||||
icon="add-circle"
|
||||
icon="add"
|
||||
>
|
||||
<router-link :to="newAgentBotsURL" class="white-text">
|
||||
{{ $t('AGENT_BOTS.ADD.TITLE') }}
|
||||
|
||||
+7
-2
@@ -46,11 +46,16 @@ export default {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col h-auto overflow-auto">
|
||||
<div
|
||||
class="max-w-[960px] mx-auto w-full shadow outline-1 outline outline-n-container rounded-2xl bg-n-solid-2 mb-10"
|
||||
>
|
||||
<div class="flex flex-row">
|
||||
<div class="w-[68%]">
|
||||
<div class="h-[calc(100vh-56px)] relative">
|
||||
<CsmlMonacoEditor v-model="bot.csmlContent" class="w-full h-full" />
|
||||
<CsmlMonacoEditor
|
||||
v-model="bot.csmlContent"
|
||||
class="w-full h-full rounded-tl-xl"
|
||||
/>
|
||||
<div
|
||||
v-if="v$.bot.csmlContent.$error"
|
||||
class="bg-red-100 dark:bg-red-200 text-white dark:text-white absolute bottom-0 w-full p-2.5 flex items-center text-xs justify-center flex-shrink-0"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import {
|
||||
useStoreGetters,
|
||||
@@ -149,20 +150,19 @@ const confirmDeletion = () => {
|
||||
feature-name="agents"
|
||||
>
|
||||
<template #actions>
|
||||
<woot-button
|
||||
class="rounded-md button nice"
|
||||
icon="add-circle"
|
||||
<Button
|
||||
:label="$t('AGENT_MGMT.HEADER_BTN_TXT')"
|
||||
icon="i-lucide-plus"
|
||||
size="sm"
|
||||
@click="openAddPopup"
|
||||
>
|
||||
{{ $t('AGENT_MGMT.HEADER_BTN_TXT') }}
|
||||
</woot-button>
|
||||
/>
|
||||
</template>
|
||||
</BaseSettingsHeader>
|
||||
</template>
|
||||
<template #body>
|
||||
<table class="divide-y divide-slate-75 dark:divide-slate-700">
|
||||
<table class="divide-y divide-n-weak">
|
||||
<tbody
|
||||
class="divide-y divide-slate-50 dark:divide-slate-800 text-slate-700 dark:text-slate-300"
|
||||
class="divide-y divide-n-weak text-slate-700 dark:text-slate-300"
|
||||
>
|
||||
<tr v-for="(agent, index) in agentList" :key="agent.email">
|
||||
<td class="py-4 ltr:pr-4 rtl:pl-4">
|
||||
|
||||
@@ -158,130 +158,126 @@ export default {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<woot-modal v-model:show="show" :on-close="onClose">
|
||||
<div class="flex flex-col h-auto overflow-auto">
|
||||
<woot-modal-header :header-title="$t('ATTRIBUTES_MGMT.ADD.TITLE')" />
|
||||
<div class="flex flex-col h-auto overflow-auto">
|
||||
<woot-modal-header :header-title="$t('ATTRIBUTES_MGMT.ADD.TITLE')" />
|
||||
|
||||
<form class="flex w-full" @submit.prevent="addAttributes">
|
||||
<div class="w-full">
|
||||
<label :class="{ error: v$.attributeModel.$error }">
|
||||
{{ $t('ATTRIBUTES_MGMT.ADD.FORM.MODEL.LABEL') }}
|
||||
<select v-model="attributeModel">
|
||||
<option v-for="model in models" :key="model.id" :value="model.id">
|
||||
{{ model.option }}
|
||||
</option>
|
||||
</select>
|
||||
<span v-if="v$.attributeModel.$error" class="message">
|
||||
{{ $t('ATTRIBUTES_MGMT.ADD.FORM.MODEL.ERROR') }}
|
||||
</span>
|
||||
<form class="flex w-full" @submit.prevent="addAttributes">
|
||||
<div class="w-full">
|
||||
<label :class="{ error: v$.attributeModel.$error }">
|
||||
{{ $t('ATTRIBUTES_MGMT.ADD.FORM.MODEL.LABEL') }}
|
||||
<select v-model="attributeModel">
|
||||
<option v-for="model in models" :key="model.id" :value="model.id">
|
||||
{{ model.option }}
|
||||
</option>
|
||||
</select>
|
||||
<span v-if="v$.attributeModel.$error" class="message">
|
||||
{{ $t('ATTRIBUTES_MGMT.ADD.FORM.MODEL.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
<woot-input
|
||||
v-model="displayName"
|
||||
:label="$t('ATTRIBUTES_MGMT.ADD.FORM.NAME.LABEL')"
|
||||
type="text"
|
||||
:class="{ error: v$.displayName.$error }"
|
||||
:error="
|
||||
v$.displayName.$error
|
||||
? $t('ATTRIBUTES_MGMT.ADD.FORM.NAME.ERROR')
|
||||
: ''
|
||||
"
|
||||
:placeholder="$t('ATTRIBUTES_MGMT.ADD.FORM.NAME.PLACEHOLDER')"
|
||||
@update:model-value="onDisplayNameChange"
|
||||
@blur="v$.displayName.$touch"
|
||||
/>
|
||||
<woot-input
|
||||
v-model="attributeKey"
|
||||
:label="$t('ATTRIBUTES_MGMT.ADD.FORM.KEY.LABEL')"
|
||||
type="text"
|
||||
:class="{ error: v$.attributeKey.$error }"
|
||||
:error="v$.attributeKey.$error ? keyErrorMessage : ''"
|
||||
:placeholder="$t('ATTRIBUTES_MGMT.ADD.FORM.KEY.PLACEHOLDER')"
|
||||
@blur="v$.attributeKey.$touch"
|
||||
/>
|
||||
<label :class="{ error: v$.description.$error }">
|
||||
{{ $t('ATTRIBUTES_MGMT.ADD.FORM.DESC.LABEL') }}
|
||||
<textarea
|
||||
v-model="description"
|
||||
rows="3"
|
||||
type="text"
|
||||
:placeholder="$t('ATTRIBUTES_MGMT.ADD.FORM.DESC.PLACEHOLDER')"
|
||||
@blur="v$.description.$touch"
|
||||
/>
|
||||
<span v-if="v$.description.$error" class="message">
|
||||
{{ $t('ATTRIBUTES_MGMT.ADD.FORM.DESC.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
<label :class="{ error: v$.attributeType.$error }">
|
||||
{{ $t('ATTRIBUTES_MGMT.ADD.FORM.TYPE.LABEL') }}
|
||||
<select v-model="attributeType">
|
||||
<option v-for="type in types" :key="type.id" :value="type.id">
|
||||
{{ type.option }}
|
||||
</option>
|
||||
</select>
|
||||
<span v-if="v$.attributeType.$error" class="message">
|
||||
{{ $t('ATTRIBUTES_MGMT.ADD.FORM.TYPE.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
<div v-if="isAttributeTypeList" class="multiselect--wrap">
|
||||
<label>
|
||||
{{ $t('ATTRIBUTES_MGMT.ADD.FORM.TYPE.LIST.LABEL') }}
|
||||
</label>
|
||||
<woot-input
|
||||
v-model="displayName"
|
||||
:label="$t('ATTRIBUTES_MGMT.ADD.FORM.NAME.LABEL')"
|
||||
type="text"
|
||||
:class="{ error: v$.displayName.$error }"
|
||||
:error="
|
||||
v$.displayName.$error
|
||||
? $t('ATTRIBUTES_MGMT.ADD.FORM.NAME.ERROR')
|
||||
: ''
|
||||
"
|
||||
:placeholder="$t('ATTRIBUTES_MGMT.ADD.FORM.NAME.PLACEHOLDER')"
|
||||
@update:model-value="onDisplayNameChange"
|
||||
@blur="v$.displayName.$touch"
|
||||
<multiselect
|
||||
ref="tagInput"
|
||||
v-model="values"
|
||||
:placeholder="$t('ATTRIBUTES_MGMT.ADD.FORM.TYPE.LIST.PLACEHOLDER')"
|
||||
label="name"
|
||||
track-by="name"
|
||||
:class="{ invalid: isMultiselectInvalid }"
|
||||
:options="options"
|
||||
multiple
|
||||
taggable
|
||||
@close="onTouch"
|
||||
@tag="addTagValue"
|
||||
/>
|
||||
<woot-input
|
||||
v-model="attributeKey"
|
||||
:label="$t('ATTRIBUTES_MGMT.ADD.FORM.KEY.LABEL')"
|
||||
type="text"
|
||||
:class="{ error: v$.attributeKey.$error }"
|
||||
:error="v$.attributeKey.$error ? keyErrorMessage : ''"
|
||||
:placeholder="$t('ATTRIBUTES_MGMT.ADD.FORM.KEY.PLACEHOLDER')"
|
||||
@blur="v$.attributeKey.$touch"
|
||||
/>
|
||||
<label :class="{ error: v$.description.$error }">
|
||||
{{ $t('ATTRIBUTES_MGMT.ADD.FORM.DESC.LABEL') }}
|
||||
<textarea
|
||||
v-model="description"
|
||||
rows="3"
|
||||
type="text"
|
||||
:placeholder="$t('ATTRIBUTES_MGMT.ADD.FORM.DESC.PLACEHOLDER')"
|
||||
@blur="v$.description.$touch"
|
||||
/>
|
||||
<span v-if="v$.description.$error" class="message">
|
||||
{{ $t('ATTRIBUTES_MGMT.ADD.FORM.DESC.ERROR') }}
|
||||
</span>
|
||||
<label v-show="isMultiselectInvalid" class="error-message">
|
||||
{{ $t('ATTRIBUTES_MGMT.ADD.FORM.TYPE.LIST.ERROR') }}
|
||||
</label>
|
||||
<label :class="{ error: v$.attributeType.$error }">
|
||||
{{ $t('ATTRIBUTES_MGMT.ADD.FORM.TYPE.LABEL') }}
|
||||
<select v-model="attributeType">
|
||||
<option v-for="type in types" :key="type.id" :value="type.id">
|
||||
{{ type.option }}
|
||||
</option>
|
||||
</select>
|
||||
<span v-if="v$.attributeType.$error" class="message">
|
||||
{{ $t('ATTRIBUTES_MGMT.ADD.FORM.TYPE.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
<div v-if="isAttributeTypeList" class="multiselect--wrap">
|
||||
<label>
|
||||
{{ $t('ATTRIBUTES_MGMT.ADD.FORM.TYPE.LIST.LABEL') }}
|
||||
</label>
|
||||
<multiselect
|
||||
ref="tagInput"
|
||||
v-model="values"
|
||||
:placeholder="
|
||||
$t('ATTRIBUTES_MGMT.ADD.FORM.TYPE.LIST.PLACEHOLDER')
|
||||
"
|
||||
label="name"
|
||||
track-by="name"
|
||||
:class="{ invalid: isMultiselectInvalid }"
|
||||
:options="options"
|
||||
multiple
|
||||
taggable
|
||||
@close="onTouch"
|
||||
@tag="addTagValue"
|
||||
/>
|
||||
<label v-show="isMultiselectInvalid" class="error-message">
|
||||
{{ $t('ATTRIBUTES_MGMT.ADD.FORM.TYPE.LIST.ERROR') }}
|
||||
</label>
|
||||
</div>
|
||||
<div v-if="isAttributeTypeText">
|
||||
<input
|
||||
v-model="regexEnabled"
|
||||
type="checkbox"
|
||||
@input="toggleRegexEnabled"
|
||||
/>
|
||||
{{ $t('ATTRIBUTES_MGMT.ADD.FORM.ENABLE_REGEX.LABEL') }}
|
||||
</div>
|
||||
<woot-input
|
||||
v-if="isAttributeTypeText && isRegexEnabled"
|
||||
v-model="regexPattern"
|
||||
:label="$t('ATTRIBUTES_MGMT.ADD.FORM.REGEX_PATTERN.LABEL')"
|
||||
type="text"
|
||||
:placeholder="
|
||||
$t('ATTRIBUTES_MGMT.ADD.FORM.REGEX_PATTERN.PLACEHOLDER')
|
||||
"
|
||||
/>
|
||||
<woot-input
|
||||
v-if="isAttributeTypeText && isRegexEnabled"
|
||||
v-model="regexCue"
|
||||
:label="$t('ATTRIBUTES_MGMT.ADD.FORM.REGEX_CUE.LABEL')"
|
||||
type="text"
|
||||
:placeholder="$t('ATTRIBUTES_MGMT.ADD.FORM.REGEX_CUE.PLACEHOLDER')"
|
||||
/>
|
||||
<div class="flex flex-row justify-end w-full gap-2 px-0 py-2">
|
||||
<woot-submit-button
|
||||
:disabled="isButtonDisabled"
|
||||
:button-text="$t('ATTRIBUTES_MGMT.ADD.SUBMIT')"
|
||||
/>
|
||||
<button class="button clear" @click.prevent="onClose">
|
||||
{{ $t('ATTRIBUTES_MGMT.ADD.CANCEL_BUTTON_TEXT') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</woot-modal>
|
||||
<div v-if="isAttributeTypeText">
|
||||
<input
|
||||
v-model="regexEnabled"
|
||||
type="checkbox"
|
||||
@input="toggleRegexEnabled"
|
||||
/>
|
||||
{{ $t('ATTRIBUTES_MGMT.ADD.FORM.ENABLE_REGEX.LABEL') }}
|
||||
</div>
|
||||
<woot-input
|
||||
v-if="isAttributeTypeText && isRegexEnabled"
|
||||
v-model="regexPattern"
|
||||
:label="$t('ATTRIBUTES_MGMT.ADD.FORM.REGEX_PATTERN.LABEL')"
|
||||
type="text"
|
||||
:placeholder="
|
||||
$t('ATTRIBUTES_MGMT.ADD.FORM.REGEX_PATTERN.PLACEHOLDER')
|
||||
"
|
||||
/>
|
||||
<woot-input
|
||||
v-if="isAttributeTypeText && isRegexEnabled"
|
||||
v-model="regexCue"
|
||||
:label="$t('ATTRIBUTES_MGMT.ADD.FORM.REGEX_CUE.LABEL')"
|
||||
type="text"
|
||||
:placeholder="$t('ATTRIBUTES_MGMT.ADD.FORM.REGEX_CUE.PLACEHOLDER')"
|
||||
/>
|
||||
<div class="flex flex-row justify-end w-full gap-2 px-0 py-2">
|
||||
<woot-submit-button
|
||||
:disabled="isButtonDisabled"
|
||||
:button-text="$t('ATTRIBUTES_MGMT.ADD.SUBMIT')"
|
||||
/>
|
||||
<button class="button clear" @click.prevent="onClose">
|
||||
{{ $t('ATTRIBUTES_MGMT.ADD.CANCEL_BUTTON_TEXT') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import BaseSettingsHeader from '../components/BaseSettingsHeader.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
import AddAttribute from './AddAttribute.vue';
|
||||
import CustomAttribute from './CustomAttribute.vue';
|
||||
import SettingsLayout from '../SettingsLayout.vue';
|
||||
@@ -68,13 +70,12 @@ const onClickTabChange = index => {
|
||||
feature-name="custom_attributes"
|
||||
>
|
||||
<template #actions>
|
||||
<woot-button
|
||||
class="button nice rounded-md"
|
||||
icon="add-circle"
|
||||
<Button
|
||||
:label="$t('ATTRIBUTES_MGMT.HEADER_BTN_TXT')"
|
||||
icon="i-lucide-plus"
|
||||
size="sm"
|
||||
@click="openAddPopup"
|
||||
>
|
||||
{{ $t('ATTRIBUTES_MGMT.HEADER_BTN_TXT') }}
|
||||
</woot-button>
|
||||
/>
|
||||
</template>
|
||||
</BaseSettingsHeader>
|
||||
</template>
|
||||
|
||||
@@ -83,7 +83,9 @@ const tableHeaders = computed(() => {
|
||||
feature-name="audit_logs"
|
||||
/>
|
||||
|
||||
<div class="mt-6 flex-1 text-slate-700 dark:text-slate-300">
|
||||
<div
|
||||
class="mt-6 flex-1 w-full max-w-[960px] mx-auto text-slate-700 dark:text-slate-300"
|
||||
>
|
||||
<woot-loading-state
|
||||
v-if="uiFlags.fetchingList"
|
||||
:message="$t('AUDIT_LOGS.LOADING')"
|
||||
@@ -95,7 +97,7 @@ const tableHeaders = computed(() => {
|
||||
{{ $t('AUDIT_LOGS.LIST.404') }}
|
||||
</p>
|
||||
<div v-else class="min-w-full overflow-x-auto">
|
||||
<table class="divide-y divide-slate-75 dark:divide-slate-700">
|
||||
<table class="divide-y divide-n-weak">
|
||||
<thead>
|
||||
<th
|
||||
v-for="thHeader in tableHeaders"
|
||||
@@ -106,7 +108,7 @@ const tableHeaders = computed(() => {
|
||||
</th>
|
||||
</thead>
|
||||
<tbody
|
||||
class="divide-y divide-slate-50 dark:divide-slate-800 text-slate-700 dark:text-slate-300"
|
||||
class="divide-y divide-n-weak text-slate-700 dark:text-slate-300"
|
||||
>
|
||||
<tr v-for="auditLogItem in records" :key="auditLogItem.id">
|
||||
<td class="py-4 pr-4 break-all whitespace-nowrap">
|
||||
|
||||
@@ -3,6 +3,8 @@ import { useAlert } from 'dashboard/composables';
|
||||
import AddAutomationRule from './AddAutomationRule.vue';
|
||||
import EditAutomationRule from './EditAutomationRule.vue';
|
||||
import BaseSettingsHeader from '../components/BaseSettingsHeader.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
import SettingsLayout from '../SettingsLayout.vue';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
@@ -185,18 +187,17 @@ const tableHeaders = computed(() => {
|
||||
feature-name="automation"
|
||||
>
|
||||
<template #actions>
|
||||
<woot-button
|
||||
class="button nice rounded-md"
|
||||
icon="add-circle"
|
||||
<Button
|
||||
:label="$t('AUTOMATION.HEADER_BTN_TXT')"
|
||||
icon="i-lucide-plus"
|
||||
size="sm"
|
||||
@click="openAddPopup"
|
||||
>
|
||||
{{ $t('AUTOMATION.HEADER_BTN_TXT') }}
|
||||
</woot-button>
|
||||
/>
|
||||
</template>
|
||||
</BaseSettingsHeader>
|
||||
</template>
|
||||
<template #body>
|
||||
<table class="min-w-full divide-y divide-slate-75 dark:divide-slate-700">
|
||||
<table class="min-w-full divide-y divide-n-weak">
|
||||
<thead>
|
||||
<th
|
||||
v-for="thHeader in tableHeaders"
|
||||
@@ -207,7 +208,7 @@ const tableHeaders = computed(() => {
|
||||
</th>
|
||||
</thead>
|
||||
<tbody
|
||||
class="divide-y divide-slate-50 dark:divide-slate-800 text-slate-700 dark:text-slate-300"
|
||||
class="divide-y divide-n-weak text-slate-700 dark:text-slate-300"
|
||||
>
|
||||
<AutomationRuleRow
|
||||
v-for="automation in records"
|
||||
|
||||
@@ -58,23 +58,24 @@ export default {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-1 p-6 overflow-auto dark:bg-slate-900">
|
||||
<div class="flex-1 py-6 overflow-auto mx-auto max-w-[960px] w-full p-1">
|
||||
<woot-loading-state v-if="uiFlags.isFetchingItem" />
|
||||
<div v-else-if="!hasABillingPlan">
|
||||
<p>{{ $t('BILLING_SETTINGS.NO_BILLING_USER') }}</p>
|
||||
</div>
|
||||
<div v-else class="w-full">
|
||||
<div class="current-plan--details">
|
||||
<h6>{{ $t('BILLING_SETTINGS.CURRENT_PLAN.TITLE') }}</h6>
|
||||
<div v-else class="w-full flex flex-col gap-5">
|
||||
<div class="border-b border-n-weak pb-4">
|
||||
<h6 class="text-n-slate-12">
|
||||
{{ $t('BILLING_SETTINGS.CURRENT_PLAN.TITLE') }}
|
||||
</h6>
|
||||
<div
|
||||
v-dompurify-html="
|
||||
formatMessage(
|
||||
$t('BILLING_SETTINGS.CURRENT_PLAN.PLAN_NOTE', {
|
||||
plan: planName,
|
||||
quantity: subscribedQuantity,
|
||||
})
|
||||
)
|
||||
"
|
||||
v-dompurify-html="formatMessage(
|
||||
$t('BILLING_SETTINGS.CURRENT_PLAN.PLAN_NOTE', {
|
||||
plan: planName,
|
||||
quantity: subscribedQuantity,
|
||||
})
|
||||
)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<BillingItem
|
||||
@@ -93,31 +94,3 @@ export default {
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.manage-subscription {
|
||||
@apply bg-white dark:bg-slate-800 flex justify-between mb-2 py-6 px-4 items-center rounded-md border border-solid border-slate-75 dark:border-slate-700;
|
||||
}
|
||||
|
||||
.current-plan--details {
|
||||
@apply border-b border-solid border-slate-75 dark:border-slate-800 mb-4 pb-4;
|
||||
|
||||
h6 {
|
||||
@apply text-slate-800 dark:text-slate-100;
|
||||
}
|
||||
|
||||
p {
|
||||
@apply text-slate-600 dark:text-slate-200;
|
||||
}
|
||||
}
|
||||
|
||||
.manage-subscription {
|
||||
.manage-subscription--description {
|
||||
@apply mb-0 text-slate-600 dark:text-slate-200;
|
||||
}
|
||||
|
||||
h6 {
|
||||
@apply text-slate-800 dark:text-slate-100;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
+32
-28
@@ -1,40 +1,44 @@
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
buttonLabel: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
buttonIcon: {
|
||||
type: String,
|
||||
default: 'edit',
|
||||
},
|
||||
<script setup>
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
emits: ['open'],
|
||||
};
|
||||
description: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
buttonLabel: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
buttonIcon: {
|
||||
type: String,
|
||||
default: 'edit',
|
||||
},
|
||||
});
|
||||
defineEmits(['open']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="manage-subscription">
|
||||
<div
|
||||
class="flex justify-between items-center shadow outline-1 outline outline-n-container rounded-2xl bg-n-solid-2 px-6 py-5"
|
||||
>
|
||||
<div>
|
||||
<h6>{{ title }}</h6>
|
||||
<p class="manage-subscription--description">
|
||||
<h6 class="text-n-slate-12">{{ title }}</h6>
|
||||
<p class="mb-0 text-n-slate-11">
|
||||
{{ description }}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<woot-button variant="smooth" :icon="buttonIcon" @click="$emit('open')">
|
||||
{{ buttonLabel }}
|
||||
</woot-button>
|
||||
<Button
|
||||
:label="buttonLabel"
|
||||
variant="faded"
|
||||
size="sm"
|
||||
@click="$emit('open')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -4,14 +4,12 @@ import { required, minLength } from '@vuelidate/validators';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
|
||||
import WootSubmitButton from '../../../../components/buttons/FormSubmitButton.vue';
|
||||
import Modal from '../../../../components/Modal.vue';
|
||||
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
|
||||
|
||||
export default {
|
||||
name: 'AddCanned',
|
||||
components: {
|
||||
WootSubmitButton,
|
||||
Modal,
|
||||
WootMessageEditor,
|
||||
},
|
||||
props: {
|
||||
@@ -82,58 +80,56 @@ export default {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal v-model:show="show" :on-close="onClose">
|
||||
<div class="flex flex-col h-auto overflow-auto">
|
||||
<woot-modal-header
|
||||
:header-title="$t('CANNED_MGMT.ADD.TITLE')"
|
||||
:header-content="$t('CANNED_MGMT.ADD.DESC')"
|
||||
/>
|
||||
<form class="flex flex-col w-full" @submit.prevent="addCannedResponse()">
|
||||
<div class="w-full">
|
||||
<label :class="{ error: v$.shortCode.$error }">
|
||||
{{ $t('CANNED_MGMT.ADD.FORM.SHORT_CODE.LABEL') }}
|
||||
<input
|
||||
v-model="shortCode"
|
||||
type="text"
|
||||
:placeholder="$t('CANNED_MGMT.ADD.FORM.SHORT_CODE.PLACEHOLDER')"
|
||||
@blur="v$.shortCode.$touch"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex flex-col h-auto overflow-auto">
|
||||
<woot-modal-header
|
||||
:header-title="$t('CANNED_MGMT.ADD.TITLE')"
|
||||
:header-content="$t('CANNED_MGMT.ADD.DESC')"
|
||||
/>
|
||||
<form class="flex flex-col w-full" @submit.prevent="addCannedResponse()">
|
||||
<div class="w-full">
|
||||
<label :class="{ error: v$.shortCode.$error }">
|
||||
{{ $t('CANNED_MGMT.ADD.FORM.SHORT_CODE.LABEL') }}
|
||||
<input
|
||||
v-model="shortCode"
|
||||
type="text"
|
||||
:placeholder="$t('CANNED_MGMT.ADD.FORM.SHORT_CODE.PLACEHOLDER')"
|
||||
@blur="v$.shortCode.$touch"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="w-full">
|
||||
<label :class="{ error: v$.content.$error }">
|
||||
{{ $t('CANNED_MGMT.ADD.FORM.CONTENT.LABEL') }}
|
||||
</label>
|
||||
<div class="editor-wrap">
|
||||
<WootMessageEditor
|
||||
v-model="content"
|
||||
class="message-editor [&>div]:px-1"
|
||||
:class="{ editor_warning: v$.content.$error }"
|
||||
enable-variables
|
||||
:enable-canned-responses="false"
|
||||
:placeholder="$t('CANNED_MGMT.ADD.FORM.CONTENT.PLACEHOLDER')"
|
||||
@blur="v$.content.$touch"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row justify-end w-full gap-2 px-0 py-2">
|
||||
<WootSubmitButton
|
||||
:disabled="
|
||||
v$.content.$invalid ||
|
||||
v$.shortCode.$invalid ||
|
||||
addCanned.showLoading
|
||||
"
|
||||
:button-text="$t('CANNED_MGMT.ADD.FORM.SUBMIT')"
|
||||
:loading="addCanned.showLoading"
|
||||
<div class="w-full">
|
||||
<label :class="{ error: v$.content.$error }">
|
||||
{{ $t('CANNED_MGMT.ADD.FORM.CONTENT.LABEL') }}
|
||||
</label>
|
||||
<div class="editor-wrap">
|
||||
<WootMessageEditor
|
||||
v-model="content"
|
||||
class="message-editor [&>div]:px-1"
|
||||
:class="{ editor_warning: v$.content.$error }"
|
||||
enable-variables
|
||||
:enable-canned-responses="false"
|
||||
:placeholder="$t('CANNED_MGMT.ADD.FORM.CONTENT.PLACEHOLDER')"
|
||||
@blur="v$.content.$touch"
|
||||
/>
|
||||
<button class="button clear" @click.prevent="onClose">
|
||||
{{ $t('CANNED_MGMT.ADD.CANCEL_BUTTON_TEXT') }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
<div class="flex flex-row justify-end w-full gap-2 px-0 py-2">
|
||||
<WootSubmitButton
|
||||
:disabled="
|
||||
v$.content.$invalid ||
|
||||
v$.shortCode.$invalid ||
|
||||
addCanned.showLoading
|
||||
"
|
||||
:button-text="$t('CANNED_MGMT.ADD.FORM.SUBMIT')"
|
||||
:loading="addCanned.showLoading"
|
||||
/>
|
||||
<button class="button clear" @click.prevent="onClose">
|
||||
{{ $t('CANNED_MGMT.ADD.CANCEL_BUTTON_TEXT') }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useAlert } from 'dashboard/composables';
|
||||
import AddCanned from './AddCanned.vue';
|
||||
import EditCanned from './EditCanned.vue';
|
||||
import BaseSettingsHeader from '../components/BaseSettingsHeader.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import { computed, onMounted, ref, defineOptions } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useStoreGetters, useStore } from 'dashboard/composables/store';
|
||||
@@ -124,17 +125,15 @@ const tableHeaders = computed(() => {
|
||||
feature-name="canned_responses"
|
||||
>
|
||||
<template #actions>
|
||||
<woot-button
|
||||
class="button nice rounded-md"
|
||||
icon="add-circle"
|
||||
<Button
|
||||
:label="$t('CANNED_MGMT.HEADER_BTN_TXT')"
|
||||
icon="i-lucide-plus"
|
||||
size="sm"
|
||||
@click="openAddPopup"
|
||||
>
|
||||
{{ $t('CANNED_MGMT.HEADER_BTN_TXT') }}
|
||||
</woot-button>
|
||||
/>
|
||||
</template>
|
||||
</BaseSettingsHeader>
|
||||
|
||||
<div class="mt-6 flex-1">
|
||||
<div class="mt-6 flex-1 w-full max-w-[960px] mx-auto">
|
||||
<woot-loading-state
|
||||
v-if="uiFlags.fetchingList"
|
||||
:message="$t('CANNED_MGMT.LOADING')"
|
||||
@@ -145,10 +144,7 @@ const tableHeaders = computed(() => {
|
||||
>
|
||||
{{ $t('CANNED_MGMT.LIST.404') }}
|
||||
</p>
|
||||
<table
|
||||
v-else
|
||||
class="min-w-full overflow-x-auto divide-y divide-slate-75 dark:divide-slate-700"
|
||||
>
|
||||
<table v-else class="min-w-full overflow-x-auto divide-y divide-n-weak">
|
||||
<thead>
|
||||
<th
|
||||
v-for="thHeader in tableHeaders"
|
||||
@@ -174,7 +170,7 @@ const tableHeaders = computed(() => {
|
||||
</th>
|
||||
</thead>
|
||||
<tbody
|
||||
class="divide-y divide-slate-50 dark:divide-slate-800 text-slate-700 dark:text-slate-300"
|
||||
class="divide-y divide-n-weak text-slate-700 dark:text-slate-300"
|
||||
>
|
||||
<tr
|
||||
v-for="(cannedItem, index) in records"
|
||||
@@ -213,7 +209,6 @@ const tableHeaders = computed(() => {
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<woot-modal v-model:show="showAddPopup" :on-close="hideAddPopup">
|
||||
<AddCanned :on-close="hideAddPopup" />
|
||||
</woot-modal>
|
||||
|
||||
+12
-29
@@ -38,31 +38,14 @@ const openInNewTab = url => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col items-start w-full gap-2 pt-4">
|
||||
<BackButton
|
||||
v-if="backButtonLabel"
|
||||
compact
|
||||
:button-label="backButtonLabel"
|
||||
/>
|
||||
<div class="flex items-center justify-between w-full gap-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
v-if="iconName"
|
||||
class="flex items-center w-10 h-10 p-1 rounded-full bg-woot-25/60 dark:bg-woot-900/60"
|
||||
>
|
||||
<div
|
||||
class="flex items-center justify-center w-full h-full rounded-full bg-woot-75/70 dark:bg-woot-800/40"
|
||||
>
|
||||
<fluent-icon
|
||||
size="14"
|
||||
:icon="iconName"
|
||||
type="outline"
|
||||
class="flex-shrink-0 text-woot-500 dark:text-woot-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col items-start w-full gap-2 max-w-[960px] mx-auto">
|
||||
<div
|
||||
class="flex items-center justify-between w-full gap-4 h-20 border-b border-n-weak"
|
||||
>
|
||||
<div class="flex gap-3">
|
||||
<BackButton v-if="backButtonLabel" compact />
|
||||
<h1
|
||||
class="text-2xl font-semibold font-interDisplay tracking-[0.3px] text-slate-900 dark:text-slate-25"
|
||||
class="text-xl font-interDisplay tracking-[0.3px] font-medium text-n-slate-12"
|
||||
>
|
||||
{{ title }}
|
||||
</h1>
|
||||
@@ -72,9 +55,9 @@ const openInNewTab = url => {
|
||||
<slot name="actions" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-3 text-slate-600 dark:text-slate-300 w-full">
|
||||
<div class="flex flex-col gap-4 text-n-slate-11 w-full pt-3">
|
||||
<p
|
||||
class="mb-0 text-base font-normal line-clamp-5 sm:line-clamp-none max-w-3xl tracking-[-0.1px]"
|
||||
class="mb-0 text-sm font-normal line-clamp-5 sm:line-clamp-none max-w-3xl leading-6"
|
||||
>
|
||||
<slot name="description">{{ description }}</slot>
|
||||
</p>
|
||||
@@ -84,14 +67,14 @@ const openInNewTab = url => {
|
||||
:href="helpURL"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="sm:inline-flex hidden gap-1 w-fit items-center text-woot-500 dark:text-woot-500 text-sm font-medium hover:underline"
|
||||
class="sm:inline-flex hidden gap-1 w-fit items-center text-n-brand text-sm font-medium hover:underline"
|
||||
>
|
||||
{{ linkText }}
|
||||
<fluent-icon
|
||||
size="16"
|
||||
icon="chevron-right"
|
||||
type="outline"
|
||||
class="flex-shrink-0 text-woot-500 dark:text-woot-500"
|
||||
class="flex-shrink-0 text-n-brand"
|
||||
/>
|
||||
</a>
|
||||
</CustomBrandPolicyWrapper>
|
||||
@@ -105,7 +88,7 @@ const openInNewTab = url => {
|
||||
v-if="helpURL && linkText"
|
||||
color-scheme="secondary"
|
||||
icon="arrow-outwards"
|
||||
class="flex-row-reverse rounded-md min-w-0 !bg-slate-50 !text-slate-900 dark:!text-white dark:!bg-slate-800"
|
||||
class="flex-row-reverse rounded-md text-sm min-w-0 !bg-slate-50 !text-slate-900 dark:!text-white dark:!bg-slate-800"
|
||||
@click="openInNewTab(helpURL)"
|
||||
>
|
||||
{{ linkText }}
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ defineProps({
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex relative flex-col sm:flex-row p-4 gap-4 sm:p-6 justify-between shadow-sm group bg-white border border-solid rounded-xl dark:bg-slate-800 border-slate-75 dark:border-slate-700/50 w-full"
|
||||
class="flex relative flex-col sm:flex-row px-4 py-4 gap-4 sm:px-6 sm:py-5 justify-between shadow group bg-n-solid-2 rounded-xl outline-1 outline outline-n-container w-full"
|
||||
>
|
||||
<slot name="leftSection">
|
||||
<div class="flex flex-col min-w-0 items-start gap-3 max-w-[480px] w-full">
|
||||
|
||||
@@ -8,6 +8,7 @@ import CustomRolePaywall from './component/CustomRolePaywall.vue';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useStore, useMapGetter } from 'dashboard/composables/store';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
const store = useStore();
|
||||
const { t } = useI18n();
|
||||
@@ -134,14 +135,13 @@ const confirmDeletion = () => {
|
||||
feature-name="canned_responses"
|
||||
>
|
||||
<template #actions>
|
||||
<woot-button
|
||||
class="rounded-md button nice"
|
||||
icon="add-circle"
|
||||
<Button
|
||||
:label="$t('CUSTOM_ROLE.HEADER_BTN_TXT')"
|
||||
icon="i-lucide-plus"
|
||||
size="sm"
|
||||
:disabled="isBehindAPaywall"
|
||||
@click="openAddModal"
|
||||
>
|
||||
{{ $t('CUSTOM_ROLE.HEADER_BTN_TXT') }}
|
||||
</woot-button>
|
||||
/>
|
||||
</template>
|
||||
</BaseSettingsHeader>
|
||||
</template>
|
||||
@@ -150,7 +150,7 @@ const confirmDeletion = () => {
|
||||
<CustomRolePaywall v-if="isBehindAPaywall" />
|
||||
<table
|
||||
v-else
|
||||
class="min-w-full overflow-x-auto divide-y divide-slate-75 dark:divide-slate-700"
|
||||
class="min-w-full overflow-x-auto divide-y divide-n-weak"
|
||||
>
|
||||
<thead>
|
||||
<th
|
||||
|
||||
+1
-1
@@ -26,7 +26,7 @@ const getFormattedPermissions = role => {
|
||||
|
||||
<template>
|
||||
<tbody
|
||||
class="divide-y divide-slate-50 dark:divide-slate-800 text-slate-700 dark:text-slate-300"
|
||||
class="divide-y divide-n-weak text-slate-700 dark:text-slate-300"
|
||||
>
|
||||
<tr v-for="(customRole, index) in roles" :key="index">
|
||||
<td
|
||||
|
||||
@@ -62,7 +62,7 @@ export default {
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="border border-slate-25 dark:border-slate-800/60 bg-white dark:bg-slate-900 h-full p-6 w-full max-w-full md:w-3/4 md:max-w-[75%] flex-shrink-0 flex-grow-0"
|
||||
class="h-full p-6 w-full max-w-full md:w-3/4 md:max-w-[75%] flex-shrink-0 flex-grow-0"
|
||||
>
|
||||
<form class="flex flex-wrap mx-0" @submit.prevent="addAgents()">
|
||||
<div class="w-full">
|
||||
|
||||
@@ -62,17 +62,16 @@ export default {
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="border border-slate-25 dark:border-slate-800/60 bg-white dark:bg-slate-900 h-full p-6 w-full max-w-full md:w-3/4 md:max-w-[75%] flex-shrink-0 flex-grow-0"
|
||||
class=" h-full p-6 w-full max-w-full md:w-3/4 md:max-w-[75%] flex-shrink-0 flex-grow-0"
|
||||
>
|
||||
<PageHeader
|
||||
class="max-w-4xl"
|
||||
:header-title="$t('INBOX_MGMT.ADD.AUTH.TITLE')"
|
||||
:header-content="
|
||||
useInstallationName(
|
||||
$t('INBOX_MGMT.ADD.AUTH.DESC'),
|
||||
globalConfig.installationName
|
||||
)
|
||||
"
|
||||
:header-content="useInstallationName(
|
||||
$t('INBOX_MGMT.ADD.AUTH.DESC'),
|
||||
globalConfig.installationName
|
||||
)
|
||||
"
|
||||
/>
|
||||
<div
|
||||
class="grid max-w-3xl grid-cols-2 mx-0 mt-6 sm:grid-cols-3 lg:grid-cols-4"
|
||||
|
||||
@@ -70,7 +70,7 @@ export default {
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="border border-slate-25 dark:border-slate-800/60 bg-white dark:bg-slate-900 h-full p-6 w-full max-w-full md:w-3/4 md:max-w-[75%] flex-shrink-0 flex-grow-0"
|
||||
class=" h-full p-6 w-full max-w-full md:w-3/4 md:max-w-[75%] flex-shrink-0 flex-grow-0"
|
||||
>
|
||||
<EmptyState
|
||||
:title="$t('INBOX_MGMT.FINISH.TITLE')"
|
||||
|
||||
@@ -40,14 +40,16 @@ export default {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-row overflow-auto p-4 h-full bg-slate-25 dark:bg-slate-800"
|
||||
>
|
||||
<woot-wizard
|
||||
class="hidden md:block w-1/4"
|
||||
:global-config="globalConfig"
|
||||
:items="items"
|
||||
/>
|
||||
<router-view />
|
||||
<div class="max-w-[960px] mx-auto w-full flex">
|
||||
<div
|
||||
class="flex flex-row overflow-auto shadow outline-1 outline outline-n-container rounded-2xl bg-n-solid-2 px-6 py-5 mb-10"
|
||||
>
|
||||
<woot-wizard
|
||||
class="hidden md:block w-1/4"
|
||||
:global-config="globalConfig"
|
||||
:items="items"
|
||||
/>
|
||||
<router-view />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -8,9 +8,12 @@ import { computed, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useStoreGetters, useStore } from 'dashboard/composables/store';
|
||||
import ChannelName from './components/ChannelName.vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
const getters = useStoreGetters();
|
||||
const store = useStore();
|
||||
const router = useRouter();
|
||||
const { t } = useI18n();
|
||||
const { isAdmin } = useAdmin();
|
||||
|
||||
@@ -59,6 +62,10 @@ const openDelete = inbox => {
|
||||
showDeletePopup.value = true;
|
||||
selectedInbox.value = inbox;
|
||||
};
|
||||
|
||||
const onClickNewInbox = () => {
|
||||
router.push({ name: 'settings_inbox_new' });
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -75,21 +82,18 @@ const openDelete = inbox => {
|
||||
feature-name="inboxes"
|
||||
>
|
||||
<template #actions>
|
||||
<router-link
|
||||
<Button
|
||||
v-if="isAdmin"
|
||||
class="button nice rounded-md"
|
||||
:to="{ name: 'settings_inbox_new' }"
|
||||
>
|
||||
<fluent-icon icon="add-circle" />
|
||||
{{ $t('SETTINGS.INBOXES.NEW_INBOX') }}
|
||||
</router-link>
|
||||
:label="$t('SETTINGS.INBOXES.NEW_INBOX')"
|
||||
icon="i-lucide-plus"
|
||||
size="sm"
|
||||
@click="onClickNewInbox"
|
||||
/>
|
||||
</template>
|
||||
</BaseSettingsHeader>
|
||||
</template>
|
||||
<template #body>
|
||||
<table
|
||||
class="min-w-full overflow-x-auto divide-y divide-slate-75 dark:divide-slate-700"
|
||||
>
|
||||
<table class="min-w-full overflow-x-auto divide-y divide-n-weak">
|
||||
<tbody
|
||||
class="divide-y divide-slate-25 dark:divide-slate-800 flex-1 text-slate-700 dark:text-slate-100"
|
||||
>
|
||||
|
||||
@@ -161,9 +161,8 @@ export default {
|
||||
|
||||
inboxName() {
|
||||
if (this.isATwilioSMSChannel || this.isATwilioWhatsAppChannel) {
|
||||
return `${this.inbox.name} (${
|
||||
this.inbox.messaging_service_sid || this.inbox.phone_number
|
||||
})`;
|
||||
return `${this.inbox.name} (${this.inbox.messaging_service_sid || this.inbox.phone_number
|
||||
})`;
|
||||
}
|
||||
if (this.isAWhatsAppChannel) {
|
||||
return `${this.inbox.name} (${this.inbox.phone_number})`;
|
||||
@@ -175,7 +174,10 @@ export default {
|
||||
},
|
||||
canLocktoSingleConversation() {
|
||||
return (
|
||||
this.isASmsInbox || this.isAWhatsAppChannel || this.isAFacebookInbox
|
||||
this.isASmsInbox ||
|
||||
this.isAWhatsAppChannel ||
|
||||
this.isAFacebookInbox ||
|
||||
this.isAPIInbox
|
||||
);
|
||||
},
|
||||
inboxNameLabel() {
|
||||
@@ -289,8 +291,8 @@ export default {
|
||||
greeting_message: this.greetingMessage || '',
|
||||
portal_id: this.selectedPortalSlug
|
||||
? this.portals.find(
|
||||
portal => portal.slug === this.selectedPortalSlug
|
||||
).id
|
||||
portal => portal.slug === this.selectedPortalSlug
|
||||
).id
|
||||
: null,
|
||||
lock_to_single_conversation: this.locktoSingleConversation,
|
||||
sender_name_type: this.senderNameType,
|
||||
@@ -359,7 +361,7 @@ export default {
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex-grow flex-shrink w-full min-w-0 pl-0 pr-0 overflow-auto bg-white settings dark:bg-slate-800"
|
||||
class="flex-grow flex-shrink w-full min-w-0 pl-0 pr-0 max-w-[960px] mx-auto shadow outline-1 outline outline-n-container rounded-2xl bg-n-solid-2 px-6 py-5 mb-10"
|
||||
>
|
||||
<SettingIntroBanner
|
||||
:header-image="inbox.avatarUrl"
|
||||
@@ -404,11 +406,10 @@ export default {
|
||||
:class="{ error: v$.selectedInboxName.$error }"
|
||||
:label="inboxNameLabel"
|
||||
:placeholder="inboxNamePlaceHolder"
|
||||
:error="
|
||||
v$.selectedInboxName.$error
|
||||
:error="v$.selectedInboxName.$error
|
||||
? $t('INBOX_MGMT.ADD.CHANNEL_NAME.ERROR')
|
||||
: ''
|
||||
"
|
||||
"
|
||||
@blur="v$.selectedInboxName.$touch"
|
||||
/>
|
||||
<woot-input
|
||||
@@ -416,17 +417,14 @@ export default {
|
||||
v-model="webhookUrl"
|
||||
class="w-3/4 pb-4"
|
||||
:class="{ error: v$.webhookUrl.$error }"
|
||||
:label="
|
||||
$t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_WEBHOOK_URL.LABEL')
|
||||
"
|
||||
:placeholder="
|
||||
$t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_WEBHOOK_URL.PLACEHOLDER')
|
||||
"
|
||||
:error="
|
||||
v$.webhookUrl.$error
|
||||
:label="$t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_WEBHOOK_URL.LABEL')
|
||||
"
|
||||
:placeholder="$t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_WEBHOOK_URL.PLACEHOLDER')
|
||||
"
|
||||
:error="v$.webhookUrl.$error
|
||||
? $t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_WEBHOOK_URL.ERROR')
|
||||
: ''
|
||||
"
|
||||
"
|
||||
@blur="v$.webhookUrl.$touch"
|
||||
/>
|
||||
<woot-input
|
||||
@@ -434,36 +432,31 @@ export default {
|
||||
v-model="channelWebsiteUrl"
|
||||
class="w-3/4 pb-4"
|
||||
:label="$t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_DOMAIN.LABEL')"
|
||||
:placeholder="
|
||||
$t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_DOMAIN.PLACEHOLDER')
|
||||
"
|
||||
:placeholder="$t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_DOMAIN.PLACEHOLDER')
|
||||
"
|
||||
/>
|
||||
<woot-input
|
||||
v-if="isAWebWidgetInbox"
|
||||
v-model="channelWelcomeTitle"
|
||||
class="w-3/4 pb-4"
|
||||
:label="
|
||||
$t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_WELCOME_TITLE.LABEL')
|
||||
"
|
||||
:placeholder="
|
||||
$t(
|
||||
'INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_WELCOME_TITLE.PLACEHOLDER'
|
||||
)
|
||||
"
|
||||
:label="$t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_WELCOME_TITLE.LABEL')
|
||||
"
|
||||
:placeholder="$t(
|
||||
'INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_WELCOME_TITLE.PLACEHOLDER'
|
||||
)
|
||||
"
|
||||
/>
|
||||
|
||||
<woot-input
|
||||
v-if="isAWebWidgetInbox"
|
||||
v-model="channelWelcomeTagline"
|
||||
class="w-3/4 pb-4"
|
||||
:label="
|
||||
$t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_WELCOME_TAGLINE.LABEL')
|
||||
"
|
||||
:placeholder="
|
||||
$t(
|
||||
'INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_WELCOME_TAGLINE.PLACEHOLDER'
|
||||
)
|
||||
"
|
||||
:label="$t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_WELCOME_TAGLINE.LABEL')
|
||||
"
|
||||
:placeholder="$t(
|
||||
'INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_WELCOME_TAGLINE.PLACEHOLDER'
|
||||
)
|
||||
"
|
||||
/>
|
||||
|
||||
<label v-if="isAWebWidgetInbox" class="w-3/4 pb-4">
|
||||
@@ -507,16 +500,14 @@ export default {
|
||||
<div v-if="greetingEnabled" class="pb-4">
|
||||
<GreetingsEditor
|
||||
v-model="greetingMessage"
|
||||
:label="
|
||||
$t(
|
||||
'INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_GREETING_MESSAGE.LABEL'
|
||||
)
|
||||
"
|
||||
:placeholder="
|
||||
$t(
|
||||
'INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_GREETING_MESSAGE.PLACEHOLDER'
|
||||
)
|
||||
"
|
||||
:label="$t(
|
||||
'INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_GREETING_MESSAGE.LABEL'
|
||||
)
|
||||
"
|
||||
:placeholder="$t(
|
||||
'INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_GREETING_MESSAGE.PLACEHOLDER'
|
||||
)
|
||||
"
|
||||
:richtext="!textAreaChannels"
|
||||
/>
|
||||
</div>
|
||||
@@ -723,11 +714,10 @@ export default {
|
||||
<input
|
||||
ref="businessNameInput"
|
||||
v-model="businessName"
|
||||
:placeholder="
|
||||
$t(
|
||||
'INBOX_MGMT.EDIT.SENDER_NAME_SECTION.BUSINESS_NAME.PLACEHOLDER'
|
||||
)
|
||||
"
|
||||
:placeholder="$t(
|
||||
'INBOX_MGMT.EDIT.SENDER_NAME_SECTION.BUSINESS_NAME.PLACEHOLDER'
|
||||
)
|
||||
"
|
||||
class="mb-0"
|
||||
type="text"
|
||||
/>
|
||||
|
||||
@@ -64,7 +64,7 @@ export default {
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="border border-slate-25 dark:border-slate-800/60 bg-white dark:bg-slate-900 h-full p-6 w-full max-w-full md:w-3/4 md:max-w-[75%] flex-shrink-0 flex-grow-0"
|
||||
class=" h-full p-6 w-full max-w-full md:w-3/4 md:max-w-[75%] flex-shrink-0 flex-grow-0"
|
||||
>
|
||||
<PageHeader
|
||||
:header-title="$t('INBOX_MGMT.ADD.API_CHANNEL.TITLE')"
|
||||
@@ -77,9 +77,8 @@ export default {
|
||||
<input
|
||||
v-model="channelName"
|
||||
type="text"
|
||||
:placeholder="
|
||||
$t('INBOX_MGMT.ADD.API_CHANNEL.CHANNEL_NAME.PLACEHOLDER')
|
||||
"
|
||||
:placeholder="$t('INBOX_MGMT.ADD.API_CHANNEL.CHANNEL_NAME.PLACEHOLDER')
|
||||
"
|
||||
@blur="v$.channelName.$touch"
|
||||
/>
|
||||
<span v-if="v$.channelName.$error" class="message">{{
|
||||
@@ -94,9 +93,8 @@ export default {
|
||||
<input
|
||||
v-model="webhookUrl"
|
||||
type="text"
|
||||
:placeholder="
|
||||
$t('INBOX_MGMT.ADD.API_CHANNEL.WEBHOOK_URL.PLACEHOLDER')
|
||||
"
|
||||
:placeholder="$t('INBOX_MGMT.ADD.API_CHANNEL.WEBHOOK_URL.PLACEHOLDER')
|
||||
"
|
||||
@blur="v$.webhookUrl.$touch"
|
||||
/>
|
||||
</label>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user