Compare commits
62
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
79a76bc9df | ||
|
|
77b1804eef | ||
|
|
a962308a23 | ||
|
|
c1c3a62412 | ||
|
|
b5dec067a1 | ||
|
|
f96f166196 | ||
|
|
9ed701065e | ||
|
|
180befbd42 | ||
|
|
4baa4363f4 | ||
|
|
1c8f41f387 | ||
|
|
7f0ca8b15d | ||
|
|
4c75bb68f4 | ||
|
|
ed82eb2932 | ||
|
|
b62166a18a | ||
|
|
bf2b75b281 | ||
|
|
c084ad5a68 | ||
|
|
dd3e658f19 | ||
|
|
453d7ad9fc | ||
|
|
47b6967dad | ||
|
|
d06cf27800 | ||
|
|
7f6a3b781f | ||
|
|
fde2b180fe | ||
|
|
2a1662c781 | ||
|
|
10d6e9551d | ||
|
|
69d46f278a | ||
|
|
0921a7c817 | ||
|
|
2a73f124ca | ||
|
|
ef33c60652 | ||
|
|
3a547de909 | ||
|
|
62e9fc1bc5 | ||
|
|
e052a061f4 | ||
|
|
9efadf8804 | ||
|
|
c8a40932cc | ||
|
|
9ddd428935 | ||
|
|
4d8ba0148c | ||
|
|
65c4ad70e1 | ||
|
|
9636478c2a | ||
|
|
65190422c4 | ||
|
|
a7bc855486 | ||
|
|
ff97536095 | ||
|
|
fcf0ba12d0 | ||
|
|
2cc82af344 | ||
|
|
c00855fce4 | ||
|
|
2d07577731 | ||
|
|
4c98006837 | ||
|
|
89e09857af | ||
|
|
12c338364e | ||
|
|
846f520ad2 | ||
|
|
6cbe1ed911 | ||
|
|
6c1ee4d965 | ||
|
|
1d8341504a | ||
|
|
f8ae6cd95c | ||
|
|
63dd2c5a93 | ||
|
|
1c47478104 | ||
|
|
39f14bebbd | ||
|
|
4b27fdf4db | ||
|
|
a6a0e78bbe | ||
|
|
703e19304d | ||
|
|
fa7bbdb0b3 | ||
|
|
d837065545 | ||
|
|
2a6f3356c9 | ||
|
|
c83105ce4f |
@@ -0,0 +1,55 @@
|
||||
name: Log Lines Percentage Check
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- develop
|
||||
|
||||
jobs:
|
||||
log_lines_check:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Check for log lines and calculate percentage
|
||||
run: |
|
||||
# Define the log line pattern
|
||||
LOG_LINE_PATTERN="Rails\.logger"
|
||||
|
||||
# Get the list of changed files in the pull request
|
||||
CHANGED_FILES=$(git diff --name-only)
|
||||
|
||||
# Initialize a flag to track if any files have insufficient log lines
|
||||
INSUFFICIENT_LOGS=0
|
||||
|
||||
for file in $CHANGED_FILES; do
|
||||
if [[ $file =~ \.rb$ && ! $file =~ _spec\.rb$ ]]; then
|
||||
# Count the total number of lines in the file
|
||||
total_lines=$(wc -l < "$file")
|
||||
|
||||
# Count the number of log lines in the file
|
||||
log_lines=$(grep -c "$LOG_LINE_PATTERN" "$file")
|
||||
|
||||
# Calculate the percentage of log lines
|
||||
if [ "$total_lines" -gt 0 ]; then
|
||||
percentage=$(awk "BEGIN { pc=100*${log_lines}/${total_lines}; i=int(pc); print (pc-i<0.5)?i:i+1 }")
|
||||
else
|
||||
percentage=0
|
||||
fi
|
||||
|
||||
# Check if the percentage is less than 5%
|
||||
if [ "$percentage" -lt 5 ]; then
|
||||
echo "Error: Log lines percentage is less than 5% ($percentage%) in $file. Please add more log lines using Rails.logger statements."
|
||||
INSUFFICIENT_LOGS=1
|
||||
else
|
||||
echo "Log lines percentage is $percentage% in $file. Code looks good!"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# If any files have insufficient log lines, fail the action
|
||||
if [ "$INSUFFICIENT_LOGS" -eq 1 ]; then
|
||||
exit 1
|
||||
fi
|
||||
+13
-1
@@ -23,6 +23,18 @@ module.exports = {
|
||||
},
|
||||
'@storybook/addon-links',
|
||||
'@storybook/addon-essentials',
|
||||
{
|
||||
/**
|
||||
* Fix Storybook issue with PostCSS@8
|
||||
* @see https://github.com/storybookjs/storybook/issues/12668#issuecomment-773958085
|
||||
*/
|
||||
name: '@storybook/addon-postcss',
|
||||
options: {
|
||||
postcssLoaderOptions: {
|
||||
implementation: require('postcss'),
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
webpackFinal: config => {
|
||||
const newConfig = {
|
||||
@@ -35,7 +47,7 @@ module.exports = {
|
||||
|
||||
newConfig.module.rules.push({
|
||||
test: /\.scss$/,
|
||||
use: ['style-loader', 'css-loader', 'sass-loader'],
|
||||
use: ['style-loader', 'css-loader', 'postcss-loader', 'sass-loader'],
|
||||
include: path.resolve(__dirname, '../app/javascript'),
|
||||
});
|
||||
|
||||
|
||||
@@ -4,10 +4,12 @@ import Vuex from 'vuex';
|
||||
import VueI18n from 'vue-i18n';
|
||||
import Vuelidate from 'vuelidate';
|
||||
import Multiselect from 'vue-multiselect';
|
||||
import VueDOMPurifyHTML from 'vue-dompurify-html';
|
||||
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon';
|
||||
|
||||
import WootUiKit from '../app/javascript/dashboard/components';
|
||||
import i18n from '../app/javascript/dashboard/i18n';
|
||||
import { domPurifyConfig } from 'shared/helpers/HTMLSanitizer';
|
||||
|
||||
import '../app/javascript/dashboard/assets/scss/storybook.scss';
|
||||
|
||||
@@ -15,6 +17,8 @@ Vue.use(VueI18n);
|
||||
Vue.use(Vuelidate);
|
||||
Vue.use(WootUiKit);
|
||||
Vue.use(Vuex);
|
||||
Vue.use(VueDOMPurifyHTML, domPurifyConfig);
|
||||
|
||||
Vue.component('multiselect', Multiselect);
|
||||
Vue.component('fluent-icon', FluentIcon);
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ gem 'commonmarker'
|
||||
# Validate Data against JSON Schema
|
||||
gem 'json_schemer'
|
||||
# Rack middleware for blocking & throttling abusive requests
|
||||
gem 'rack-attack'
|
||||
gem 'rack-attack', '>= 6.7.0'
|
||||
# a utility tool for streaming, flexible and safe downloading of remote files
|
||||
gem 'down'
|
||||
# authentication type to fetch and send mail over oauth2.0
|
||||
@@ -153,7 +153,7 @@ gem 'stripe'
|
||||
gem 'faker'
|
||||
|
||||
# Include logrange conditionally in intializer using env variable
|
||||
gem 'lograge', '~> 0.12.0', require: false
|
||||
gem 'lograge', '~> 0.13.0', require: false
|
||||
|
||||
# worked with microsoft refresh token
|
||||
gem 'omniauth-oauth2'
|
||||
@@ -197,7 +197,7 @@ group :development do
|
||||
gem 'squasher'
|
||||
|
||||
# profiling
|
||||
gem 'rack-mini-profiler', require: false
|
||||
gem 'rack-mini-profiler', '>= 3.1.1', require: false
|
||||
gem 'stackprof'
|
||||
end
|
||||
|
||||
|
||||
+11
-11
@@ -162,7 +162,7 @@ GEM
|
||||
byebug (11.1.3)
|
||||
climate_control (1.2.0)
|
||||
coderay (1.1.3)
|
||||
commonmarker (0.23.9)
|
||||
commonmarker (0.23.10)
|
||||
concurrent-ruby (1.2.2)
|
||||
connection_pool (2.4.1)
|
||||
crack (0.4.5)
|
||||
@@ -429,7 +429,7 @@ GEM
|
||||
llhttp-ffi (0.4.0)
|
||||
ffi-compiler (~> 1.0)
|
||||
rake (~> 13.0)
|
||||
lograge (0.12.0)
|
||||
lograge (0.13.0)
|
||||
actionpack (>= 4)
|
||||
activesupport (>= 4)
|
||||
railties (>= 4)
|
||||
@@ -452,7 +452,7 @@ GEM
|
||||
mini_magick (4.12.0)
|
||||
mini_mime (1.1.2)
|
||||
mini_portile2 (2.8.4)
|
||||
minitest (5.18.1)
|
||||
minitest (5.19.0)
|
||||
mock_redis (0.36.0)
|
||||
ruby2_keywords
|
||||
msgpack (1.7.0)
|
||||
@@ -549,12 +549,12 @@ GEM
|
||||
activesupport (>= 3.0.0)
|
||||
raabro (1.4.0)
|
||||
racc (1.7.1)
|
||||
rack (2.2.7)
|
||||
rack-attack (6.6.1)
|
||||
rack (>= 1.0, < 3)
|
||||
rack (2.2.8)
|
||||
rack-attack (6.7.0)
|
||||
rack (>= 1.0, < 4)
|
||||
rack-cors (2.0.1)
|
||||
rack (>= 2.0.0)
|
||||
rack-mini-profiler (3.1.0)
|
||||
rack-mini-profiler (3.1.1)
|
||||
rack (>= 1.2.0)
|
||||
rack-protection (3.0.6)
|
||||
rack
|
||||
@@ -812,7 +812,7 @@ GEM
|
||||
working_hours (1.4.1)
|
||||
activesupport (>= 3.2)
|
||||
tzinfo
|
||||
zeitwerk (2.6.8)
|
||||
zeitwerk (2.6.9)
|
||||
|
||||
PLATFORMS
|
||||
arm64-darwin-20
|
||||
@@ -886,7 +886,7 @@ DEPENDENCIES
|
||||
line-bot-api
|
||||
liquid
|
||||
listen
|
||||
lograge (~> 0.12.0)
|
||||
lograge (~> 0.13.0)
|
||||
maxminddb
|
||||
mock_redis
|
||||
neighbor
|
||||
@@ -903,9 +903,9 @@ DEPENDENCIES
|
||||
pry-rails
|
||||
puma
|
||||
pundit
|
||||
rack-attack
|
||||
rack-attack (>= 6.7.0)
|
||||
rack-cors
|
||||
rack-mini-profiler
|
||||
rack-mini-profiler (>= 3.1.1)
|
||||
rack-timeout
|
||||
rails (~> 7.0.5.1)
|
||||
redis
|
||||
|
||||
@@ -14,8 +14,18 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController
|
||||
@facebook_inbox = Current.account.inboxes.create!(name: inbox_name, channel: facebook_channel)
|
||||
set_instagram_id(page_access_token, facebook_channel)
|
||||
set_avatar(@facebook_inbox, page_id)
|
||||
rescue StandardError => e
|
||||
ChatwootExceptionTracker.new(e).capture_exception
|
||||
end
|
||||
rescue StandardError => e
|
||||
ChatwootExceptionTracker.new(e).capture_exception
|
||||
Rails.logger.error "Error in register_facebook_page: #{e.message}"
|
||||
# Additional log statements
|
||||
log_additional_info
|
||||
end
|
||||
|
||||
def log_additional_info
|
||||
Rails.logger.debug do
|
||||
"user_access_token: #{params[:user_access_token]} , page_access_token: #{params[:page_access_token]} ,
|
||||
page_id: #{params[:page_id]}, inbox_name: #{params[:inbox_name]}"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -30,6 +40,8 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController
|
||||
|
||||
instagram_id = response['instagram_business_account']['id']
|
||||
facebook_channel.update(instagram_id: instagram_id)
|
||||
rescue StandardError => e
|
||||
Rails.logger.error "Error in set_instagram_id: #{e.message}"
|
||||
end
|
||||
|
||||
# get params[:inbox_id], current_account. params[:omniauth_token]
|
||||
@@ -61,6 +73,7 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController
|
||||
fb_page&.reauthorized!
|
||||
rescue StandardError => e
|
||||
ChatwootExceptionTracker.new(e).capture_exception
|
||||
Rails.logger.error "Error in update_fb_page: #{e.message}"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -77,7 +90,7 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController
|
||||
koala = Koala::Facebook::OAuth.new(GlobalConfigService.load('FB_APP_ID', ''), GlobalConfigService.load('FB_APP_SECRET', ''))
|
||||
koala.exchange_access_token_info(omniauth_token)['access_token']
|
||||
rescue StandardError => e
|
||||
Rails.logger.error e
|
||||
Rails.logger.error "Error in long_lived_token: #{e.message}"
|
||||
end
|
||||
|
||||
def mark_already_existing_facebook_pages(data)
|
||||
|
||||
@@ -4,6 +4,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
||||
sort_on :name, internal_name: :order_on_name, type: :scope, scope_params: [:direction]
|
||||
sort_on :phone_number, type: :string
|
||||
sort_on :last_activity_at, internal_name: :order_on_last_activity_at, type: :scope, scope_params: [:direction]
|
||||
sort_on :created_at, internal_name: :order_on_created_at, type: :scope, scope_params: [:direction]
|
||||
sort_on :company, internal_name: :order_on_company_name, type: :scope, scope_params: [:direction]
|
||||
sort_on :city, internal_name: :order_on_city, type: :scope, scope_params: [:direction]
|
||||
sort_on :country, internal_name: :order_on_country_name, type: :scope, scope_params: [:direction]
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
class Api::V1::Accounts::Integrations::SlackController < Api::V1::Accounts::BaseController
|
||||
before_action :check_admin_authorization?
|
||||
before_action :fetch_hook, only: [:update, :destroy]
|
||||
before_action :fetch_hook, only: [:update, :destroy, :list_all_channels]
|
||||
|
||||
def list_all_channels
|
||||
@channels = channel_builder.fetch_channels
|
||||
end
|
||||
|
||||
def create
|
||||
ActiveRecord::Base.transaction do
|
||||
builder = Integrations::Slack::HookBuilder.new(
|
||||
account: Current.account,
|
||||
code: params[:code],
|
||||
inbox_id: params[:inbox_id]
|
||||
)
|
||||
@hook = builder.perform
|
||||
create_chatwoot_slack_channel
|
||||
end
|
||||
hook_builder = Integrations::Slack::HookBuilder.new(
|
||||
account: Current.account,
|
||||
code: params[:code],
|
||||
inbox_id: params[:inbox_id]
|
||||
)
|
||||
@hook = hook_builder.perform
|
||||
end
|
||||
|
||||
def update
|
||||
create_chatwoot_slack_channel
|
||||
render json: @hook
|
||||
@hook = channel_builder.update(permitted_params[:reference_id])
|
||||
render json: { error: I18n.t('errors.slack.invalid_channel_id') }, status: :unprocessable_entity if @hook.blank?
|
||||
end
|
||||
|
||||
def destroy
|
||||
@hook.destroy!
|
||||
|
||||
head :ok
|
||||
end
|
||||
|
||||
@@ -31,11 +31,11 @@ class Api::V1::Accounts::Integrations::SlackController < Api::V1::Accounts::Base
|
||||
@hook = Integrations::Hook.where(account: Current.account).find_by(app_id: 'slack')
|
||||
end
|
||||
|
||||
def create_chatwoot_slack_channel
|
||||
channel = params[:channel] || 'customer-conversations'
|
||||
builder = Integrations::Slack::ChannelBuilder.new(
|
||||
hook: @hook, channel: channel
|
||||
)
|
||||
builder.perform
|
||||
def channel_builder
|
||||
Integrations::Slack::ChannelBuilder.new(hook: @hook)
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:reference_id)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
class Public::Api::V1::Inboxes::ConversationsController < Public::Api::V1::InboxesController
|
||||
include Events::Types
|
||||
before_action :set_conversation, only: [:toggle_typing, :update_last_seen]
|
||||
|
||||
def index
|
||||
@conversations = @contact_inbox.hmac_verified? ? @contact.conversations : @contact_inbox.conversations
|
||||
end
|
||||
@@ -7,12 +10,36 @@ class Public::Api::V1::Inboxes::ConversationsController < Public::Api::V1::Inbox
|
||||
@conversation = create_conversation
|
||||
end
|
||||
|
||||
def toggle_typing
|
||||
case params[:typing_status]
|
||||
when 'on'
|
||||
trigger_typing_event(CONVERSATION_TYPING_ON)
|
||||
when 'off'
|
||||
trigger_typing_event(CONVERSATION_TYPING_OFF)
|
||||
end
|
||||
head :ok
|
||||
end
|
||||
|
||||
def update_last_seen
|
||||
@conversation.contact_last_seen_at = DateTime.now.utc
|
||||
@conversation.save!
|
||||
head :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_conversation
|
||||
@conversation = @contact_inbox.contact.conversations.find_by!(display_id: params[:id])
|
||||
end
|
||||
|
||||
def create_conversation
|
||||
::Conversation.create!(conversation_params)
|
||||
end
|
||||
|
||||
def trigger_typing_event(event)
|
||||
Rails.configuration.dispatcher.dispatch(event, Time.zone.now, conversation: @conversation, user: @conversation.contact)
|
||||
end
|
||||
|
||||
def conversation_params
|
||||
{
|
||||
account_id: @contact_inbox.contact.account_id,
|
||||
|
||||
@@ -8,13 +8,22 @@ class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::B
|
||||
def index
|
||||
@articles = @portal.articles
|
||||
@articles = @articles.search(list_params) if list_params.present?
|
||||
@articles.order(position: :asc)
|
||||
order_by_sort_param
|
||||
@articles.page(list_params[:page]) if list_params[:page].present?
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
private
|
||||
|
||||
def order_by_sort_param
|
||||
@articles = if list_params[:sort].present? && list_params[:sort] == 'views'
|
||||
@articles.order_by_views
|
||||
else
|
||||
@articles.order_by_position
|
||||
end
|
||||
end
|
||||
|
||||
def set_article
|
||||
@article = @portal.articles.find_by(slug: permitted_params[:article_slug])
|
||||
@article.increment_view_count
|
||||
@@ -35,7 +44,7 @@ class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::B
|
||||
end
|
||||
|
||||
def list_params
|
||||
params.permit(:query, :locale)
|
||||
params.permit(:query, :locale, :sort)
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
class Public::Api::V1::Portals::BaseController < PublicController
|
||||
before_action :show_plain_layout
|
||||
around_action :set_locale
|
||||
|
||||
private
|
||||
|
||||
def show_plain_layout
|
||||
@is_plain_layout_enabled = params[:show_plain_layout] == 'true'
|
||||
end
|
||||
|
||||
def set_locale(&)
|
||||
switch_locale_with_portal(&) if params[:locale].present?
|
||||
switch_locale_with_article(&) if params[:article_slug].present?
|
||||
|
||||
@@ -86,7 +86,10 @@ class AccountDashboard < Administrate::BaseDashboard
|
||||
"##{account.id} #{account.name}"
|
||||
end
|
||||
|
||||
def permitted_attributes
|
||||
# We do not use the action parameter but we still need to define it
|
||||
# to prevent an error from being raised (wrong number of arguments)
|
||||
# Reference: https://github.com/thoughtbot/administrate/pull/2356/files#diff-4e220b661b88f9a19ac527c50d6f1577ef6ab7b0bed2bfdf048e22e6bfa74a05R204
|
||||
def permitted_attributes(action)
|
||||
super + [limits: {}]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
v-if="!authUIFlags.isFetching"
|
||||
id="app"
|
||||
class="app-wrapper app-root"
|
||||
:class="{ 'app-rtl--wrapper': isRTLView, dark: theme === 'dark' }"
|
||||
:class="{ 'app-rtl--wrapper': isRTLView }"
|
||||
:dir="isRTLView ? 'rtl' : 'ltr'"
|
||||
>
|
||||
<update-banner :latest-chatwoot-version="latestChatwootVersion" />
|
||||
@@ -35,12 +35,11 @@ import PaymentPendingBanner from './components/app/PaymentPendingBanner.vue';
|
||||
import vueActionCable from './helper/actionCable';
|
||||
import WootSnackbarBox from './components/SnackbarContainer';
|
||||
import rtlMixin from 'shared/mixins/rtlMixin';
|
||||
import { LocalStorage } from 'shared/helpers/localStorage';
|
||||
import { setColorTheme } from './helper/themeHelper';
|
||||
import {
|
||||
registerSubscription,
|
||||
verifyServiceWorkerExistence,
|
||||
} from './helper/pushHelper';
|
||||
import { LOCAL_STORAGE_KEYS } from './constants/localStorage';
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
@@ -61,7 +60,6 @@ export default {
|
||||
return {
|
||||
showAddAccountModal: false,
|
||||
latestChatwootVersion: null,
|
||||
theme: 'light',
|
||||
};
|
||||
},
|
||||
|
||||
@@ -99,27 +97,11 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
initializeColorTheme() {
|
||||
this.setColorTheme(
|
||||
window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
);
|
||||
},
|
||||
setColorTheme(isOSOnDarkMode) {
|
||||
const selectedColorScheme =
|
||||
LocalStorage.get(LOCAL_STORAGE_KEYS.COLOR_SCHEME) || 'light';
|
||||
if (
|
||||
(selectedColorScheme === 'auto' && isOSOnDarkMode) ||
|
||||
selectedColorScheme === 'dark'
|
||||
) {
|
||||
this.theme = 'dark';
|
||||
document.body.classList.add('dark');
|
||||
} else {
|
||||
this.theme = 'light ';
|
||||
document.body.classList.remove('dark');
|
||||
}
|
||||
setColorTheme(window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||||
},
|
||||
listenToThemeChanges() {
|
||||
const mql = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
mql.onchange = e => this.setColorTheme(e.matches);
|
||||
mql.onchange = e => setColorTheme(e.matches);
|
||||
},
|
||||
setLocale(locale) {
|
||||
this.$root.$i18n.locale = locale;
|
||||
|
||||
@@ -8,11 +8,19 @@ class IntegrationsAPI extends ApiClient {
|
||||
}
|
||||
|
||||
connectSlack(code) {
|
||||
return axios.post(`${this.baseUrl()}/integrations/slack`, {
|
||||
code: code,
|
||||
return axios.post(`${this.baseUrl()}/integrations/slack`, { code });
|
||||
}
|
||||
|
||||
updateSlack({ referenceId }) {
|
||||
return axios.patch(`${this.baseUrl()}/integrations/slack`, {
|
||||
reference_id: referenceId,
|
||||
});
|
||||
}
|
||||
|
||||
listAllSlackChannels() {
|
||||
return axios.get(`${this.baseUrl()}/integrations/slack/list_all_channels`);
|
||||
}
|
||||
|
||||
delete(integrationId) {
|
||||
return axios.delete(`${this.baseUrl()}/integrations/${integrationId}`);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,9 @@ describe('#integrationAPI', () => {
|
||||
expect(integrationAPI).toHaveProperty('update');
|
||||
expect(integrationAPI).toHaveProperty('delete');
|
||||
expect(integrationAPI).toHaveProperty('connectSlack');
|
||||
expect(integrationAPI).toHaveProperty('createHook');
|
||||
expect(integrationAPI).toHaveProperty('updateSlack');
|
||||
expect(integrationAPI).toHaveProperty('updateSlack');
|
||||
expect(integrationAPI).toHaveProperty('listAllSlackChannels');
|
||||
expect(integrationAPI).toHaveProperty('deleteHook');
|
||||
});
|
||||
describeWithAPIMock('API calls', context => {
|
||||
@@ -26,6 +28,24 @@ describe('#integrationAPI', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('#updateSlack', () => {
|
||||
const updateObj = { referenceId: 'SDFSDGSVE' };
|
||||
integrationAPI.updateSlack(updateObj);
|
||||
expect(context.axiosMock.patch).toHaveBeenCalledWith(
|
||||
'/api/v1/integrations/slack',
|
||||
{
|
||||
reference_id: updateObj.referenceId,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('#listAllSlackChannels', () => {
|
||||
integrationAPI.listAllSlackChannels();
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v1/integrations/slack/list_all_channels'
|
||||
);
|
||||
});
|
||||
|
||||
it('#delete', () => {
|
||||
integrationAPI.delete(2);
|
||||
expect(context.axiosMock.delete).toHaveBeenCalledWith(
|
||||
|
||||
@@ -26,6 +26,7 @@ code {
|
||||
background: $color-background;
|
||||
border-radius: var(--border-radius-large);
|
||||
padding: $space-two;
|
||||
@apply bg-slate-50 dark:bg-slate-600 text-slate-800 dark:text-slate-100;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -72,7 +72,6 @@
|
||||
@import '~shared/assets/stylesheets/ionicons';
|
||||
@import 'utility-helpers';
|
||||
|
||||
|
||||
.tooltip {
|
||||
@apply bg-slate-900 text-white py-1 px-2 z-40 text-xs rounded-md dark:bg-slate-200 dark:text-slate-900;
|
||||
}
|
||||
|
||||
@@ -18,7 +18,15 @@
|
||||
}
|
||||
|
||||
&.multiselect--disabled {
|
||||
@apply opacity-80;
|
||||
@apply opacity-50 border border-slate-200 dark:border-slate-600 rounded-md cursor-not-allowed;
|
||||
|
||||
.multiselect__select {
|
||||
@apply cursor-not-allowed bg-white dark:bg-slate-900 rounded-md;
|
||||
}
|
||||
|
||||
.multiselect__tags {
|
||||
@apply border-0;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect--active {
|
||||
@@ -36,7 +44,7 @@
|
||||
}
|
||||
|
||||
.multiselect__content-wrapper {
|
||||
@apply bg-white dark:bg-slate-900 border 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-slate-200 dark:border-slate-600 text-slate-800 dark:text-slate-100;
|
||||
}
|
||||
|
||||
.multiselect__content {
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
@import 'variables';
|
||||
|
||||
@import '~spinkit/scss/spinners/7-three-bounce';
|
||||
@import '~vue-multiselect/dist/vue-multiselect.min.css';
|
||||
@import 'vue-multiselect/dist/vue-multiselect.min.css';
|
||||
@import '~shared/assets/stylesheets/ionicons';
|
||||
|
||||
@import 'mixins';
|
||||
@@ -30,3 +30,18 @@
|
||||
@import 'widgets/forms';
|
||||
|
||||
@import 'plugins/multiselect';
|
||||
|
||||
@import 'widget/assets/scss/reset';
|
||||
@import 'tailwindcss/base';
|
||||
@import 'tailwindcss/components';
|
||||
@import 'tailwindcss/utilities';
|
||||
@import 'widget/assets/scss/utilities';
|
||||
|
||||
|
||||
html,
|
||||
body {
|
||||
font-family: 'PlusJakarta', sans-serif;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@@ -64,19 +64,19 @@
|
||||
@apply bg-slate-75 dark:bg-slate-900 border-slate-100 dark:border-slate-700;
|
||||
|
||||
&.secondary {
|
||||
@apply border-slate-100 dark:border-slate-700;
|
||||
@apply border-slate-100 dark:border-slate-700 text-slate-800 dark:text-slate-100;
|
||||
}
|
||||
|
||||
&.success {
|
||||
@apply border-slate-100 dark:border-slate-700;
|
||||
@apply border-slate-100 dark:border-slate-700 text-green-800 dark:text-green-100;
|
||||
}
|
||||
|
||||
&.alert {
|
||||
@apply border-slate-100 dark:border-slate-700;
|
||||
@apply border-slate-100 dark:border-slate-700 text-red-700 dark:text-red-100;
|
||||
}
|
||||
|
||||
&.warning {
|
||||
@apply border-slate-100 dark:border-slate-700;
|
||||
@apply border-slate-100 dark:border-slate-700 text-yellow-700 dark:text-yellow-700;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -90,7 +90,7 @@
|
||||
}
|
||||
|
||||
&.success {
|
||||
@apply bg-green-50 dark:bg-green-700 text-green-700 dark:text-green-100 hover:bg-green-100 dark:hover:bg-green-800;
|
||||
@apply bg-green-50 dark:bg-green-700 text-green-700 dark:text-green-100 hover:bg-green-100 dark:hover:bg-green-800 hover:text-green-800 dark:hover:text-green-100;
|
||||
}
|
||||
|
||||
&.alert {
|
||||
@@ -118,26 +118,26 @@
|
||||
}
|
||||
|
||||
&.warning {
|
||||
@apply text-yellow-700 dark:text-yellow-700;
|
||||
@apply text-yellow-700 dark:text-yellow-600;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@apply hover:bg-woot-50 dark:hover:bg-woot-900/50 hover:text-woot-500 dark:hover:text-woot-100;
|
||||
|
||||
&.secondary {
|
||||
@apply hover:bg-slate-50 dark:hover:bg-slate-700;
|
||||
@apply hover:bg-slate-50 dark:hover:bg-slate-700 hover:text-slate-800 dark:hover:text-slate-100;
|
||||
}
|
||||
|
||||
&.success {
|
||||
@apply hover:bg-green-50 dark:hover:bg-green-800;
|
||||
@apply hover:bg-green-50 dark:hover:bg-green-800 hover:text-green-800 dark:hover:text-green-100;
|
||||
}
|
||||
|
||||
&.alert {
|
||||
@apply hover:bg-red-50 dark:hover:bg-red-800;
|
||||
@apply hover:bg-red-50 dark:hover:bg-red-800 hover:text-red-700 dark:hover:text-red-100;
|
||||
}
|
||||
|
||||
&.warning {
|
||||
@apply hover:bg-yellow-50 dark:hover:bg-yellow-100;
|
||||
@apply hover:bg-yellow-100 dark:hover:bg-yellow-800 hover:text-yellow-700 dark:hover:text-yellow-600;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,34 +1,45 @@
|
||||
// scss-lint:disable MergeableSelector
|
||||
|
||||
@mixin bubble-with-types {
|
||||
@apply py-2 text-sm font-normal bg-woot-500 dark:bg-woot-500 relative px-4 m-0 text-white dark:text-white;
|
||||
|
||||
.message-text__wrap {
|
||||
@apply relative;
|
||||
|
||||
.link {
|
||||
@apply text-white dark:text-white underline;
|
||||
}
|
||||
@tailwind utilities;
|
||||
@layer utilities {
|
||||
.custom-gradient {
|
||||
background-image: linear-gradient(
|
||||
-180deg,
|
||||
transparent 3%,
|
||||
rgb(76 81 85) 130%
|
||||
);
|
||||
}
|
||||
|
||||
.image,
|
||||
.video {
|
||||
@apply cursor-pointer relative;
|
||||
.bubble-with-types {
|
||||
@apply py-2 text-sm font-normal bg-woot-500 dark:bg-woot-500 relative px-4 m-0 text-white dark:text-white;
|
||||
|
||||
.modal-container {
|
||||
@apply text-center;
|
||||
.message-text__wrap {
|
||||
@apply relative;
|
||||
|
||||
.link {
|
||||
@apply text-white dark:text-white underline;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-image {
|
||||
@apply max-h-[76vh] max-w-[76vw];
|
||||
}
|
||||
.image,
|
||||
.video {
|
||||
@apply cursor-pointer relative;
|
||||
|
||||
.modal-video {
|
||||
@apply max-h-[76vh] max-w-[76vw];
|
||||
}
|
||||
.modal-container {
|
||||
@apply text-center;
|
||||
}
|
||||
|
||||
&::before {
|
||||
@apply bg-gradient-to-t from-transparent via-transparent to-slate-800 bg-repeat bg-clip-border bottom-0 h-[20%] content-[''] left-0 absolute w-full opacity-80;
|
||||
.modal-image {
|
||||
@apply max-h-[76vh] max-w-[76vw];
|
||||
}
|
||||
|
||||
.modal-video {
|
||||
@apply max-h-[76vh] max-w-[76vw];
|
||||
}
|
||||
|
||||
&::before {
|
||||
@apply custom-gradient bottom-0 h-[20%] content-[''] left-0 absolute w-full opacity-80;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -55,8 +66,7 @@
|
||||
}
|
||||
|
||||
.bubble {
|
||||
@include bubble-with-types;
|
||||
@apply text-left break-words;
|
||||
@apply bubble-with-types text-left break-words;
|
||||
|
||||
.aplayer {
|
||||
@apply shadow-none;
|
||||
@@ -129,7 +139,15 @@
|
||||
@apply ml-auto break-words rounded-l-lg rounded-r;
|
||||
|
||||
&.is-private {
|
||||
@apply text-black-900 dark:text-black-900 relative border border-solid bg-[#fff3d5] border-[#ffd97a] dark:bg-[#fff3d5] dark:border-[#ffd97a];
|
||||
@apply text-black-900 dark:text-white relative border border-solid bg-yellow-100 dark:bg-yellow-700 border-yellow-200 dark:border-yellow-600/25;
|
||||
|
||||
blockquote {
|
||||
@apply border-slate-400 dark:border-slate-400 text-slate-800 dark:text-slate-300;
|
||||
|
||||
p {
|
||||
@apply text-slate-600 dark:text-slate-300;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-image {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.reply-box {
|
||||
transition: box-shadow 0.35s $swift-ease-out-function,
|
||||
height 2s $swift-ease-out-function;
|
||||
height 2s $swift-ease-out-function;
|
||||
|
||||
&.is-focused {
|
||||
box-shadow: var(--shadow);
|
||||
@@ -47,13 +47,13 @@
|
||||
}
|
||||
|
||||
&.is-private {
|
||||
background: var(--y-50);
|
||||
@apply bg-yellow-100 dark:bg-yellow-800;
|
||||
|
||||
.reply-box__top {
|
||||
background: var(--y-50);
|
||||
@apply bg-yellow-100 dark:bg-yellow-800;
|
||||
|
||||
> input {
|
||||
background: var(--y-50);
|
||||
@apply bg-yellow-100 dark:bg-yellow-800;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
}
|
||||
|
||||
.tabs-title {
|
||||
@apply flex-shrink-0 my-0 mx-2;
|
||||
@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;
|
||||
@@ -53,7 +53,7 @@
|
||||
}
|
||||
|
||||
a {
|
||||
@apply flex items-center flex-row border-b border-transparent text-slate-600 dark:text-slate-200 text-sm top-[1px] relative;
|
||||
@apply flex items-center flex-row border-b border-transparent text-slate-500 dark:text-slate-200 text-sm top-[1px] relative;
|
||||
transition: border-color 0.15s $swift-ease-out-function;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
class="conversations-list-wrap flex-shrink-0 flex-basis-custom overflow-hidden flex flex-col border-r rtl:border-r-0 rtl:border-l border-slate-50 dark:border-slate-800/50"
|
||||
class="conversations-list-wrap flex-basis-clamp flex-shrink-0 flex-basis-custom overflow-hidden flex flex-col border-r rtl:border-r-0 rtl:border-l border-slate-50 dark:border-slate-800/50"
|
||||
:class="{
|
||||
hide: !showConversationList,
|
||||
'list--full-width': isOnExpandedLayout,
|
||||
@@ -15,7 +15,7 @@
|
||||
>
|
||||
<div class="flex max-w-[85%] justify-center items-center">
|
||||
<h1
|
||||
class="text-xl break-words overflow-hidden whitespace-nowrap text-ellipsis text-black-800 dark:text-slate-100 overflow-hidden whitespace-nowrap text-ellipsis mb-0"
|
||||
class="text-xl break-words overflow-hidden whitespace-nowrap text-ellipsis text-black-900 dark:text-slate-100 mb-0"
|
||||
:title="pageTitle"
|
||||
>
|
||||
{{ pageTitle }}
|
||||
@@ -969,8 +969,6 @@ export default {
|
||||
|
||||
<style scoped lang="scss">
|
||||
.conversations-list-wrap {
|
||||
@apply flex-basis-clamp;
|
||||
|
||||
&.hide {
|
||||
@apply hidden;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div
|
||||
class="ml-0 mr-0 flex pt-0 pr-4 pb-4 pl-0"
|
||||
:class="{
|
||||
'pt-4 border-b border-solid border-slate-50 dark:border-slate-600': showBorder,
|
||||
'pt-4 border-b border-solid border-slate-50 dark:border-slate-700/30': showBorder,
|
||||
}"
|
||||
>
|
||||
<div class="w-[30%] min-w-0 max-w-[30%] pr-12">
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
>
|
||||
<fluent-icon v-if="!!iconClass" :icon="iconClass" class="icon" />
|
||||
<span>{{ buttonText }}</span>
|
||||
<spinner v-if="loading" />
|
||||
<spinner v-if="loading" class="ml-2" :color-scheme="spinnerClass" />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
@@ -40,6 +40,10 @@ export default {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
spinnerClass: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'submit',
|
||||
@@ -47,7 +51,7 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
computedClass() {
|
||||
return `button nice ${this.buttonClass || ' '}`;
|
||||
return `button nice gap-2 ${this.buttonClass || ' '}`;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
@@ -59,11 +63,9 @@ export default {
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
button:disabled {
|
||||
opacity: 1;
|
||||
background-color: var(--w-100);
|
||||
|
||||
@apply bg-woot-100 dark:bg-woot-500/25 dark:text-slate-500 opacity-100;
|
||||
&:hover {
|
||||
background-color: var(--w-100);
|
||||
@apply bg-woot-100 dark:bg-woot-500/25;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -29,6 +29,7 @@ const settings = accountId => ({
|
||||
'settings_inboxes_page_channel',
|
||||
'settings_integrations_dashboard_apps',
|
||||
'settings_integrations_integration',
|
||||
'settings_integrations_slack',
|
||||
'settings_integrations_webhook',
|
||||
'settings_integrations',
|
||||
'settings_teams_add_agents',
|
||||
@@ -92,7 +93,6 @@ const settings = accountId => ({
|
||||
{
|
||||
icon: 'automation',
|
||||
label: 'AUTOMATION',
|
||||
beta: true,
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/settings/automation/list`),
|
||||
toStateName: 'automation_list',
|
||||
@@ -101,7 +101,6 @@ const settings = accountId => ({
|
||||
{
|
||||
icon: 'bot',
|
||||
label: 'AGENT_BOTS',
|
||||
beta: true,
|
||||
hasSubMenu: false,
|
||||
globalConfigFlag: 'csmlEditorHost',
|
||||
toState: frontendURL(`accounts/${accountId}/settings/agent-bots`),
|
||||
@@ -114,7 +113,6 @@ const settings = accountId => ({
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/settings/macros`),
|
||||
toStateName: 'macros_wrapper',
|
||||
beta: true,
|
||||
featureFlag: FEATURE_FLAGS.MACROS,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="showShowCurrentAccountContext"
|
||||
class="rounded-md text-xs py-2 px-2 mt-2 relative border border-slate-50 dark:border-slate-800/50 hover:bg-slate-50 dark:hover:bg-slate-700 cursor-pointer"
|
||||
class="text-slate-700 dark:text-slate-200 rounded-md text-xs py-2 px-2 mt-2 relative border border-slate-50 dark:border-slate-800/50 hover:bg-slate-50 dark:hover:bg-slate-800 cursor-pointer"
|
||||
@mouseover="setShowSwitch"
|
||||
@mouseleave="resetShowSwitch"
|
||||
>
|
||||
{{ $t('SIDEBAR.CURRENTLY_VIEWING_ACCOUNT') }}
|
||||
<p class="text-ellipsis font-medium mb-0">
|
||||
<p
|
||||
class="text-ellipsis overflow-hidden whitespace-nowrap font-medium mb-0 text-slate-800 dark:text-slate-100"
|
||||
>
|
||||
{{ account.name }}
|
||||
</p>
|
||||
<transition name="fade">
|
||||
<div
|
||||
v-if="showSwitchButton"
|
||||
class="bg-gradient-to-r rtl:bg-gradient-to-l from-transparent via-white dark:via-slate-700 to-white dark:to-slate-700 flex items-center h-full justify-end absolute top-0 right-0 w-full"
|
||||
class="ltr:overlay-shadow ltr:dark:overlay-shadow-dark rtl:rtl-overlay-shadow rtl:dark:rtl-overlay-shadow-dark flex items-center h-full rounded-md justify-end absolute top-0 right-0 w-full"
|
||||
>
|
||||
<div class="my-0 mx-2">
|
||||
<woot-button
|
||||
@@ -54,7 +56,41 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
<style scoped>
|
||||
@tailwind components;
|
||||
@layer components {
|
||||
.overlay-shadow {
|
||||
background-image: linear-gradient(
|
||||
to right,
|
||||
rgba(255, 255, 255, 0) 0%,
|
||||
rgba(255, 255, 255, 1) 50%
|
||||
);
|
||||
}
|
||||
|
||||
.overlay-shadow-dark {
|
||||
background-image: linear-gradient(
|
||||
to right,
|
||||
rgba(0, 0, 0, 0) 0%,
|
||||
rgb(21, 23, 24) 50%
|
||||
);
|
||||
}
|
||||
|
||||
.rtl-overlay-shadow {
|
||||
background-image: linear-gradient(
|
||||
to left,
|
||||
rgba(255, 255, 255, 0) 0%,
|
||||
rgba(255, 255, 255, 1) 50%
|
||||
);
|
||||
}
|
||||
|
||||
.rtl-overlay-shadow-dark {
|
||||
background-image: linear-gradient(
|
||||
to left,
|
||||
rgba(0, 0, 0, 0) 0%,
|
||||
rgb(21, 23, 24) 50%
|
||||
);
|
||||
}
|
||||
}
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 300ms ease;
|
||||
@@ -62,6 +98,6 @@ export default {
|
||||
|
||||
.fade-enter,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
@apply opacity-0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="mb-4">
|
||||
<button
|
||||
class="text-slate-600 dark:text-slate-100 w-10 h-10 my-2 flex items-center justify-center rounded-lg hover:bg-slate-25 dark:hover:bg-slate-800 dark:hover:text-slate-100 hover:text-slate-600 relative"
|
||||
class="text-slate-600 dark:text-slate-100 w-10 h-10 my-2 flex items-center justify-center rounded-lg hover:bg-slate-25 dark:hover:bg-slate-700 dark:hover:text-slate-100 hover:text-slate-600 relative"
|
||||
:class="{
|
||||
'bg-woot-50 dark:bg-slate-800 text-woot-500 hover:bg-woot-50': isNotificationPanelActive,
|
||||
}"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div
|
||||
v-if="show"
|
||||
v-on-clickaway="onClickAway"
|
||||
class="left-3 rtl:left-auto rtl:right-3 bottom-16 w-64 absolute z-10 rounded-md shadow-xl bg-white dark:bg-slate-800 py-2 px-2 border border-slate-25 dark:border-slate-700"
|
||||
class="left-3 rtl:left-auto rtl:right-3 bottom-16 w-64 absolute z-20 rounded-md shadow-xl bg-white dark:bg-slate-800 py-2 px-2 border border-slate-25 dark:border-slate-700"
|
||||
:class="{ 'block visible': show }"
|
||||
>
|
||||
<availability-status />
|
||||
@@ -60,6 +60,17 @@
|
||||
</a>
|
||||
</router-link>
|
||||
</woot-dropdown-item>
|
||||
<woot-dropdown-item>
|
||||
<woot-button
|
||||
variant="clear"
|
||||
color-scheme="secondary"
|
||||
size="small"
|
||||
icon="appearance"
|
||||
@click="openAppearanceOptions"
|
||||
>
|
||||
{{ $t('SIDEBAR_ITEMS.APPEARANCE') }}
|
||||
</woot-button>
|
||||
</woot-dropdown-item>
|
||||
<woot-dropdown-item v-if="currentUser.type === 'SuperAdmin'">
|
||||
<a
|
||||
href="/super_admin"
|
||||
@@ -145,6 +156,10 @@ export default {
|
||||
onClickAway() {
|
||||
if (this.show) this.$emit('close');
|
||||
},
|
||||
openAppearanceOptions() {
|
||||
const ninja = document.querySelector('ninja-keys');
|
||||
ninja.open({ parent: 'appearance_settings' });
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<a
|
||||
v-tooltip.right="$t(`SIDEBAR.${name}`)"
|
||||
:href="href"
|
||||
class="text-slate-600 dark:text-slate-100 w-10 h-10 my-2 flex items-center justify-center rounded-lg hover:bg-slate-25 dark:hover:bg-slate-700 dark:hover:text-slate-100 hover:text-slate-600 relative"
|
||||
class="text-slate-700 dark:text-slate-100 w-10 h-10 my-2 flex items-center justify-center rounded-lg hover:bg-slate-25 dark:hover:bg-slate-700 dark:hover:text-slate-100 hover:text-slate-600 relative"
|
||||
:class="{
|
||||
'bg-woot-50 dark:bg-slate-800 text-woot-500 hover:bg-woot-50':
|
||||
isActive || isChildMenuActive,
|
||||
|
||||
+4
-4
@@ -22,7 +22,7 @@
|
||||
class="inline-flex items-center justify-center w-4 rounded-sm bg-slate-100 dark:bg-slate-700 p-0.5 mr-1.5 rtl:mr-0 rtl:ml-1.5"
|
||||
>
|
||||
<fluent-icon
|
||||
class="text-xxs text-slate-600 dark:text-slate-200"
|
||||
class="text-xxs text-slate-700 dark:text-slate-200"
|
||||
:class="{
|
||||
'text-woot-500 dark:text-woot-500': isActive,
|
||||
}"
|
||||
@@ -41,7 +41,7 @@
|
||||
>
|
||||
<span
|
||||
:title="menuTitle"
|
||||
class="text-sm text-slate-600 dark:text-slate-100"
|
||||
class="text-sm text-slate-700 dark:text-slate-100"
|
||||
:class="{
|
||||
'text-woot-500 dark:text-woot-500': isActive,
|
||||
'text-ellipsis overflow-hidden whitespace-nowrap max-w-full': shouldTruncate,
|
||||
@@ -54,8 +54,8 @@
|
||||
class="bg-slate-50 dark:bg-slate-700 rounded text-xxs font-medium mx-1 py-0 px-1"
|
||||
:class="
|
||||
isCountZero
|
||||
? `text-slate-300 dark:text-slate-600`
|
||||
: `text-slate-600 dark:text-slate-50`
|
||||
? `text-slate-300 dark:text-slate-700`
|
||||
: `text-slate-700 dark:text-slate-50`
|
||||
"
|
||||
>
|
||||
{{ childItemCount }}
|
||||
|
||||
@@ -96,87 +96,67 @@ export default {
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.banner {
|
||||
--x-padding: var(--space-normal);
|
||||
--y-padding: var(--space-slab);
|
||||
|
||||
display: flex;
|
||||
gap: var(--x-padding);
|
||||
color: var(--white);
|
||||
font-size: var(--font-size-mini);
|
||||
padding: var(--y-padding) var(--x-padding);
|
||||
justify-content: center;
|
||||
@apply flex items-center gap-4 text-white dark:text-white text-xs py-3 px-4 justify-center;
|
||||
|
||||
&.primary {
|
||||
background: var(--w-500);
|
||||
@apply bg-woot-500 dark:bg-woot-500;
|
||||
.banner-action__button {
|
||||
background: var(--w-600);
|
||||
border: none;
|
||||
color: var(--white);
|
||||
@apply bg-woot-600 dark:bg-woot-600 border-none text-white;
|
||||
|
||||
&:hover {
|
||||
background: var(--w-800);
|
||||
@apply bg-woot-700 dark:bg-woot-700;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.secondary {
|
||||
background: var(--s-200);
|
||||
color: var(--s-800);
|
||||
@apply bg-slate-200 dark:bg-slate-300 text-slate-800 dark:text-slate-800;
|
||||
a {
|
||||
color: var(--s-800);
|
||||
@apply text-slate-800 dark:text-slate-800;
|
||||
}
|
||||
}
|
||||
|
||||
&.alert {
|
||||
background: var(--r-500);
|
||||
@apply bg-red-500 dark:bg-red-500;
|
||||
.banner-action__button {
|
||||
background: var(--r-700);
|
||||
border: none;
|
||||
color: var(--white);
|
||||
@apply bg-red-700 dark:bg-red-700 border-none text-white dark:text-white;
|
||||
|
||||
&:hover {
|
||||
background: var(--r-800);
|
||||
@apply bg-red-800 dark:bg-red-800;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.warning {
|
||||
background: var(--y-600);
|
||||
color: var(--y-500);
|
||||
@apply bg-yellow-500 dark:bg-yellow-500 text-yellow-500 dark:text-yellow-500;
|
||||
a {
|
||||
color: var(--y-500);
|
||||
@apply text-yellow-500 dark:text-yellow-500;
|
||||
}
|
||||
}
|
||||
|
||||
&.gray {
|
||||
background: var(--b-500);
|
||||
@apply text-black-500 dark:text-black-500;
|
||||
.banner-action__button {
|
||||
color: var(--white);
|
||||
@apply text-white dark:text-white;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
margin-left: var(--space-smaller);
|
||||
text-decoration: underline;
|
||||
color: var(--white);
|
||||
font-size: var(--font-size-mini);
|
||||
@apply ml-1 underline text-white dark:text-white text-xs;
|
||||
}
|
||||
|
||||
.banner-action__button {
|
||||
::v-deep .button__content {
|
||||
white-space: nowrap;
|
||||
@apply whitespace-nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.banner-message {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@apply flex items-center;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: var(--space-smaller);
|
||||
right: var(--y-padding);
|
||||
@apply flex gap-1 right-3;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -61,6 +61,9 @@ export default {
|
||||
}
|
||||
},
|
||||
hideAIAssistanceModal() {
|
||||
this.recordAnalytics('DISMISS_AI_SUGGESTION', {
|
||||
aiOption: this.aiOption,
|
||||
});
|
||||
this.showAIAssistanceModal = false;
|
||||
},
|
||||
openAIAssist() {
|
||||
|
||||
@@ -213,7 +213,7 @@ export default {
|
||||
}
|
||||
|
||||
.filter.has-error {
|
||||
@apply bg-red-50 dark:bg-red-100;
|
||||
@apply bg-red-50 dark:bg-red-800/50 border-red-100 dark:border-red-700/50;
|
||||
}
|
||||
|
||||
.filter-inputs {
|
||||
@@ -221,7 +221,7 @@ export default {
|
||||
}
|
||||
|
||||
.filter-error {
|
||||
@apply text-red-500 dark:text-red-600 block my-1 mx-0;
|
||||
@apply text-red-500 dark:text-red-200 block my-1 mx-0;
|
||||
}
|
||||
|
||||
.action__question,
|
||||
|
||||
@@ -61,21 +61,16 @@ export default {
|
||||
}
|
||||
|
||||
.colorpicker--selected {
|
||||
border: 1px solid var(--color-border-light);
|
||||
border-radius: $space-smaller;
|
||||
cursor: pointer;
|
||||
height: $space-large;
|
||||
margin-bottom: $space-normal;
|
||||
width: $space-large;
|
||||
@apply border border-solid border-slate-50 dark:border-slate-600 rounded cursor-pointer h-8 w-8 mb-4;
|
||||
}
|
||||
|
||||
.colorpicker--chrome.vc-chrome {
|
||||
@include elegant-card;
|
||||
@apply shadow-lg -mt-2.5 absolute z-[9999] border border-solid border-slate-75 dark:border-slate-600 rounded;
|
||||
|
||||
border: 1px solid $color-border;
|
||||
border-radius: $space-smaller;
|
||||
margin-top: -$space-one;
|
||||
position: absolute;
|
||||
z-index: 9999;
|
||||
::v-deep {
|
||||
input {
|
||||
@apply bg-white dark:bg-white;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -114,10 +114,7 @@
|
||||
@click="removeFilter"
|
||||
/>
|
||||
</div>
|
||||
<p
|
||||
v-if="v.values.$dirty && v.values.$error"
|
||||
class="text-red-500 dark:text-red-600 block my-1 mx-0"
|
||||
>
|
||||
<p v-if="v.values.$dirty && v.values.$error" class="filter-error">
|
||||
{{ $t('FILTER.EMPTY_VALUE_ERROR') }}
|
||||
</p>
|
||||
</div>
|
||||
@@ -127,7 +124,7 @@
|
||||
class="flex items-center justify-center relative my-2.5 mx-0"
|
||||
>
|
||||
<hr
|
||||
class="w-full absolute border-b border-solid border-slate-75 dark:border-slate-600"
|
||||
class="w-full absolute border-b border-solid border-slate-75 dark:border-slate-800"
|
||||
/>
|
||||
<select
|
||||
v-model="query_operator"
|
||||
@@ -273,8 +270,8 @@ export default {
|
||||
},
|
||||
getInputErrorClass(isDirty, hasError) {
|
||||
return isDirty && hasError
|
||||
? 'bg-red-50 dark:bg-red-200 border-red-100 dark:border-red-300 '
|
||||
: 'bg-slate-50 dark:bg-slate-800 border-slate-75 dark:border-slate-600 ';
|
||||
? 'bg-red-50 dark:bg-red-800/50 border-red-100 dark:border-red-700/50'
|
||||
: 'bg-slate-50 dark:bg-slate-800 border-slate-75 dark:border-slate-700/50';
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -286,6 +283,10 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
.filter-error {
|
||||
@apply text-red-500 dark:text-red-200 block my-1 mx-0;
|
||||
}
|
||||
|
||||
.multiselect {
|
||||
@apply mb-0;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="row--user-block">
|
||||
<div class="flex items-center text-left">
|
||||
<thumbnail
|
||||
:src="user.thumbnail"
|
||||
:size="size"
|
||||
@@ -7,7 +7,8 @@
|
||||
:status="user.availability_status"
|
||||
/>
|
||||
<h6
|
||||
class="text-block-title user-name overflow-hidden whitespace-nowrap text-ellipsis text-capitalize"
|
||||
class="my-0 mx-2 dark:text-slate-100 overflow-hidden whitespace-nowrap text-ellipsis text-capitalize"
|
||||
:class="textClass"
|
||||
>
|
||||
{{ user.name }}
|
||||
</h6>
|
||||
@@ -29,16 +30,10 @@ export default {
|
||||
type: String,
|
||||
default: '20px',
|
||||
},
|
||||
textClass: {
|
||||
type: String,
|
||||
default: 'text-xs text-slate-600',
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.row--user-block {
|
||||
@apply items-center flex text-left;
|
||||
|
||||
.user-name {
|
||||
@apply my-0 mx-2 text-slate-700 dark:text-slate-100;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -456,6 +456,28 @@ export default {
|
||||
}
|
||||
> .ProseMirror {
|
||||
@apply p-0 break-words text-slate-800 dark:text-slate-100;
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
p {
|
||||
@apply text-slate-800 dark:text-slate-100;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
@apply border-slate-400 dark:border-slate-500;
|
||||
|
||||
p {
|
||||
@apply text-slate-600 dark:text-slate-400;
|
||||
}
|
||||
}
|
||||
|
||||
ol li {
|
||||
@apply list-item list-decimal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -473,16 +495,20 @@ export default {
|
||||
|
||||
.is-private {
|
||||
.prosemirror-mention-node {
|
||||
@apply font-medium bg-slate-50 dark:bg-slate-200 text-slate-900 dark:text-slate-900 py-0 px-1;
|
||||
@apply font-medium bg-yellow-100 dark:bg-yellow-800 text-slate-900 dark:text-slate-25 py-0 px-1;
|
||||
}
|
||||
|
||||
.ProseMirror-menubar-wrapper {
|
||||
.ProseMirror-menubar {
|
||||
@apply bg-yellow-50 dark:bg-yellow-50 text-slate-700 dark:text-slate-700;
|
||||
@apply bg-yellow-100 dark:bg-yellow-800 text-slate-700 dark:text-slate-25;
|
||||
}
|
||||
|
||||
> .ProseMirror {
|
||||
@apply text-slate-800 dark:text-slate-800;
|
||||
@apply text-slate-800 dark:text-slate-25;
|
||||
|
||||
p {
|
||||
@apply text-slate-800 dark:text-slate-25;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -326,7 +326,7 @@ export default {
|
||||
@apply flex justify-between py-3 px-4;
|
||||
|
||||
&.is-note-mode {
|
||||
@apply bg-yellow-50 dark:bg-yellow-50;
|
||||
@apply bg-yellow-100 dark:bg-yellow-800;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -342,7 +342,7 @@ export default {
|
||||
label {
|
||||
@apply cursor-pointer;
|
||||
}
|
||||
&:hover .button {
|
||||
&:hover button {
|
||||
@apply dark:bg-slate-800 bg-slate-100;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@ export default {
|
||||
.button--note {
|
||||
@apply border-l-0 rounded-none;
|
||||
&.is-active {
|
||||
@apply border-r border-b-0 bg-yellow-50 dark:bg-yellow-50 border-t-0 border-slate-50 dark:border-slate-700;
|
||||
@apply border-r border-b-0 bg-yellow-100 dark:bg-yellow-800 border-t-0 border-slate-50 dark:border-slate-700;
|
||||
}
|
||||
&:hover,
|
||||
&:active {
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="p-4 rounded-lg bg-slate-25 dark:bg-slate-700 border border-solid border-slate-50 dark:border-slate-700 mb-4"
|
||||
class="p-4 rounded-lg bg-slate-25 dark:bg-slate-900 border border-solid border-slate-50 dark:border-slate-700/50 mb-4"
|
||||
>
|
||||
<filter-input-box
|
||||
v-for="(filter, i) in appliedFilters"
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div
|
||||
class="conversation flex flex-shrink-0 flex-grow-0 w-auto max-w-full cursor-pointer relative py-0 px-4 border-transparent border-l-2 border-t-0 border-b-0 border-r-0 border-solid items-start hover:bg-slate-50 dark:hover:bg-slate-800 group"
|
||||
class="conversation flex flex-shrink-0 flex-grow-0 w-auto max-w-full cursor-pointer relative py-0 px-4 border-transparent border-l-2 border-t-0 border-b-0 border-r-0 border-solid items-start hover:bg-slate-25 dark:hover:bg-slate-800 group"
|
||||
:class="{
|
||||
'active bg-slate-50 dark:bg-slate-800 border-woot-500': isActiveChat,
|
||||
'active bg-slate-25 dark:bg-slate-800 border-woot-500': isActiveChat,
|
||||
'unread-chat': hasUnread,
|
||||
'has-inbox-name': showInboxName,
|
||||
'conversation-selected': selected,
|
||||
@@ -84,7 +84,7 @@
|
||||
<fluent-icon
|
||||
v-if="attachmentIcon"
|
||||
size="16"
|
||||
class="-mt-0.5 align-middle inline-block"
|
||||
class="-mt-0.5 align-middle inline-block text-slate-600 dark:text-slate-300"
|
||||
:icon="attachmentIcon"
|
||||
/>
|
||||
{{ this.$t(`${attachmentMessageContent}`) }}
|
||||
@@ -99,8 +99,8 @@
|
||||
>
|
||||
<fluent-icon
|
||||
size="16"
|
||||
class="-mt-0.5 align-middle"
|
||||
icon="info inline-block"
|
||||
class="-mt-0.5 align-middle inline-block text-slate-600 dark:text-slate-300"
|
||||
icon="info"
|
||||
/>
|
||||
<span>
|
||||
{{ this.$t(`CHAT_LIST.NO_MESSAGES`) }}
|
||||
|
||||
@@ -552,10 +552,10 @@ export default {
|
||||
}
|
||||
|
||||
&.is-private.is-text > .message-text__wrap .link {
|
||||
@apply text-woot-700 dark:text-woot-700;
|
||||
@apply text-woot-600 dark:text-woot-200;
|
||||
}
|
||||
&.is-private.is-text > .message-text__wrap .prosemirror-mention-node {
|
||||
@apply font-bold bg-none rounded-sm p-0 text-slate-700 dark:text-slate-700 underline;
|
||||
@apply font-bold bg-none rounded-sm p-0 bg-yellow-100 dark:bg-yellow-700 text-slate-700 dark:text-slate-25 underline;
|
||||
}
|
||||
|
||||
&.is-from-bot {
|
||||
@@ -651,6 +651,10 @@ li.right {
|
||||
@apply bg-slate-75 dark:bg-slate-700 inline-block leading-none rounded-sm p-1;
|
||||
}
|
||||
|
||||
ol li {
|
||||
@apply list-item list-decimal;
|
||||
}
|
||||
|
||||
pre {
|
||||
@apply bg-slate-75 dark:bg-slate-700 block border-slate-75 dark:border-slate-700 text-slate-800 dark:text-slate-100 rounded-md p-2 mt-1 mb-2 leading-relaxed whitespace-pre-wrap;
|
||||
|
||||
@@ -660,7 +664,11 @@ li.right {
|
||||
}
|
||||
|
||||
blockquote {
|
||||
@apply border-l-4 mx-0 my-1 pt-2 pr-2 pb-0 pl-4 border-slate-75 border-solid dark:border-slate-700 text-slate-800 dark:text-slate-100;
|
||||
@apply border-l-4 mx-0 my-1 pt-2 pr-2 pb-0 pl-4 border-slate-75 border-solid dark:border-slate-600 text-slate-800 dark:text-slate-100;
|
||||
|
||||
p {
|
||||
@apply text-slate-800 dark:text-slate-300;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -119,11 +119,13 @@ import aiMixin from 'dashboard/mixins/aiMixin';
|
||||
import { getTypingUsersText } from '../../../helper/commons';
|
||||
import { calculateScrollTop } from './helpers/scrollTopCalculationHelper';
|
||||
import { isEscape } from 'shared/helpers/KeyboardHelpers';
|
||||
import { LocalStorage } from 'shared/helpers/localStorage';
|
||||
|
||||
// constants
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
import { REPLY_POLICY } from 'shared/constants/links';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -392,8 +394,11 @@ export default {
|
||||
});
|
||||
},
|
||||
isLabelSuggestionDismissed() {
|
||||
const dismissed = this.getDismissedConversations(this.currentAccountId);
|
||||
return dismissed[this.currentAccountId].includes(this.conversationId);
|
||||
return LocalStorage.getFlag(
|
||||
LOCAL_STORAGE_KEYS.DISMISSED_LABEL_SUGGESTIONS,
|
||||
this.currentAccountId,
|
||||
this.currentChat.id
|
||||
);
|
||||
},
|
||||
fetchAllAttachmentsFromCurrentChat() {
|
||||
this.$store.dispatch('fetchAllAttachments', this.currentChat.id);
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
<div class="reply-box" :class="replyBoxClass">
|
||||
<banner
|
||||
v-if="showSelfAssignBanner"
|
||||
action-button-variant="link"
|
||||
action-button-variant="clear"
|
||||
color-scheme="secondary"
|
||||
class="banner--self-assign"
|
||||
:banner-message="$t('CONVERSATION.NOT_ASSIGNED_TO_YOU')"
|
||||
:has-action-button="true"
|
||||
:action-button-label="$t('CONVERSATION.ASSIGN_TO_ME')"
|
||||
@@ -1094,6 +1095,10 @@ export default {
|
||||
@apply mb-0;
|
||||
}
|
||||
|
||||
.banner--self-assign {
|
||||
@apply py-2;
|
||||
}
|
||||
|
||||
.message-signature-wrap {
|
||||
@apply my-0 mx-4 px-1 flex max-h-[8vh] items-baseline justify-between hover:bg-slate-25 dark:hover:bg-slate-800 border border-dashed border-slate-100 dark:border-slate-700 rounded-sm overflow-auto;
|
||||
}
|
||||
@@ -1110,7 +1115,7 @@ export default {
|
||||
@apply border-t border-slate-50 dark:border-slate-700 bg-white dark:bg-slate-900;
|
||||
|
||||
&.is-private {
|
||||
@apply bg-yellow-50 dark:bg-yellow-50;
|
||||
@apply bg-yellow-50 dark:bg-yellow-200;
|
||||
}
|
||||
}
|
||||
.send-button {
|
||||
@@ -1122,41 +1127,29 @@ export default {
|
||||
}
|
||||
|
||||
.emoji-dialog {
|
||||
top: unset;
|
||||
bottom: -40px;
|
||||
left: -320px;
|
||||
right: unset;
|
||||
@apply top-[unset] -bottom-10 -left-80 right-[unset];
|
||||
|
||||
&::before {
|
||||
right: var(--space-minus-normal);
|
||||
bottom: var(--space-small);
|
||||
transform: rotate(270deg);
|
||||
filter: drop-shadow(0px 4px 4px rgba(0, 0, 0, 0.08));
|
||||
@apply -right-4 bottom-2 rtl:right-0 rtl:-left-4;
|
||||
}
|
||||
}
|
||||
|
||||
.emoji-dialog--rtl {
|
||||
left: unset;
|
||||
right: -320px;
|
||||
@apply left-[unset] -right-80;
|
||||
&::before {
|
||||
left: var(--space-minus-normal);
|
||||
transform: rotate(90deg);
|
||||
right: 0;
|
||||
bottom: var(--space-small);
|
||||
filter: drop-shadow(0px 4px 4px rgba(0, 0, 0, 0.08));
|
||||
}
|
||||
}
|
||||
|
||||
.emoji-dialog--expanded {
|
||||
left: unset;
|
||||
bottom: var(--space-jumbo);
|
||||
position: absolute;
|
||||
z-index: var(--z-index-normal);
|
||||
@apply left-[unset] bottom-0 absolute z-[100];
|
||||
|
||||
&::before {
|
||||
transform: rotate(0deg);
|
||||
left: var(--space-smaller);
|
||||
bottom: var(--space-minus-small);
|
||||
@apply left-1 -bottom-2;
|
||||
}
|
||||
}
|
||||
.message-signature {
|
||||
|
||||
+4
-2
@@ -21,7 +21,9 @@
|
||||
<p class="label-title">
|
||||
{{ template.name }}
|
||||
</p>
|
||||
<span class="label-lang label">
|
||||
<span
|
||||
class="inline-block py-1 px-2 rounded-sm text-xs leading-none cursor-default bg-white dark:bg-slate-700 text-slate-800 dark:text-slate-100"
|
||||
>
|
||||
{{ $t('WHATSAPP_TEMPLATES.PICKER.LABELS.LANGUAGE') }} :
|
||||
{{ template.language }}
|
||||
</span>
|
||||
@@ -114,7 +116,7 @@ export default {
|
||||
@apply bg-slate-25 dark:bg-slate-900 rounded-md max-h-[18.75rem] overflow-y-auto p-2.5;
|
||||
|
||||
.template__list-item {
|
||||
@apply rounded-lg cursor-pointer block p-2.5 text-left w-full hover:bg-woot-50 dark:hover:bg-slate-600;
|
||||
@apply rounded-lg cursor-pointer block p-2.5 text-left w-full hover:bg-woot-50 dark:hover:bg-slate-800;
|
||||
|
||||
.label-title {
|
||||
@apply text-sm;
|
||||
|
||||
@@ -233,7 +233,7 @@ export default {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.isAWebWidgetInbox) {
|
||||
if (this.isAWebWidgetInbox || this.isAPIInbox) {
|
||||
const { contact_last_seen_at: contactLastSeenAt } = this.currentChat;
|
||||
return contactLastSeenAt >= this.createdAt;
|
||||
}
|
||||
|
||||
+11
-15
@@ -8,7 +8,7 @@
|
||||
<div class="wrap">
|
||||
<div class="label-suggestion--container">
|
||||
<h6 class="label-suggestion--title">Suggested labels</h6>
|
||||
<div v-if="!fetchingSuggestions" class="label-suggestion--options">
|
||||
<div class="label-suggestion--options">
|
||||
<button
|
||||
v-for="label in preparedLabels"
|
||||
:key="label.title"
|
||||
@@ -123,7 +123,6 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
isDismissed: false,
|
||||
fetchingSuggestions: false,
|
||||
isHovered: false,
|
||||
selectedLabels: [],
|
||||
};
|
||||
@@ -160,11 +159,7 @@ export default {
|
||||
if (this.isDismissed) return false;
|
||||
if (!this.isAIIntegrationEnabled) return false;
|
||||
|
||||
return (
|
||||
!this.fetchingSuggestions &&
|
||||
this.preparedLabels.length &&
|
||||
this.chatLabels.length === 0
|
||||
);
|
||||
return this.preparedLabels.length && this.chatLabels.length === 0;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
@@ -190,12 +185,10 @@ export default {
|
||||
}
|
||||
},
|
||||
dismissSuggestions() {
|
||||
const dismissed = this.getDismissedConversations(this.currentAccountId);
|
||||
dismissed[this.currentAccountId].push(this.conversationId);
|
||||
|
||||
LocalStorage.set(
|
||||
LocalStorage.setFlag(
|
||||
LOCAL_STORAGE_KEYS.DISMISSED_LABEL_SUGGESTIONS,
|
||||
dismissed
|
||||
this.currentAccountId,
|
||||
this.conversationId
|
||||
);
|
||||
|
||||
// dismiss this once the values are set
|
||||
@@ -203,8 +196,11 @@ export default {
|
||||
this.trackLabelEvent(OPEN_AI_EVENTS.DISMISS_LABEL_SUGGESTION);
|
||||
},
|
||||
isConversationDismissed() {
|
||||
const dismissed = this.getDismissedConversations(this.currentAccountId);
|
||||
return dismissed[this.currentAccountId].includes(this.conversationId);
|
||||
return LocalStorage.getFlag(
|
||||
LOCAL_STORAGE_KEYS.DISMISSED_LABEL_SUGGESTIONS,
|
||||
this.currentAccountId,
|
||||
this.conversationId
|
||||
);
|
||||
},
|
||||
addAllLabels() {
|
||||
let labelsToAdd = this.selectedLabels;
|
||||
@@ -215,7 +211,7 @@ export default {
|
||||
conversationId: this.conversationId,
|
||||
labels: labelsToAdd,
|
||||
});
|
||||
this.trackLabelEvent(OPEN_AI_EVENTS.LABEL_SUGGESTION_APPLIED);
|
||||
this.trackLabelEvent(OPEN_AI_EVENTS.APPLY_LABEL_SUGGESTION);
|
||||
},
|
||||
trackLabelEvent(event) {
|
||||
const payload = {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="phone-input--wrap relative">
|
||||
<div class="phone-input" :class="{ 'has-error': error }">
|
||||
<div
|
||||
class="cursor-pointer py-2 pr-1 pl-2 rounded-tl-md rounded-bl-md flex items-center justify-center gap-2 bg-slate-25 dark:bg-slate-700 h-10 w-[3.25rem]"
|
||||
class="cursor-pointer py-2 pr-1.5 pl-2 rounded-tl-md rounded-bl-md flex items-center justify-center gap-1.5 bg-slate-25 dark:bg-slate-700 h-10 w-14"
|
||||
@click="toggleCountryDropdown"
|
||||
>
|
||||
<h5 v-if="activeCountry.emoji" class="mb-0">
|
||||
|
||||
@@ -3,5 +3,5 @@ export const LOCAL_STORAGE_KEYS = {
|
||||
WIDGET_BUILDER: 'widgetBubble_',
|
||||
DRAFT_MESSAGES: 'draftMessages',
|
||||
COLOR_SCHEME: 'color_scheme',
|
||||
DISMISSED_LABEL_SUGGESTIONS: 'dismissedLabelSuggestions',
|
||||
DISMISSED_LABEL_SUGGESTIONS: 'labelSuggestionsDismissed',
|
||||
};
|
||||
|
||||
@@ -89,4 +89,9 @@ export const OPEN_AI_EVENTS = Object.freeze({
|
||||
SIMPLIFY: 'OpenAI: Used simplify',
|
||||
APPLY_LABEL_SUGGESTION: 'OpenAI: Apply label from suggestion',
|
||||
DISMISS_LABEL_SUGGESTION: 'OpenAI: Dismiss label suggestions',
|
||||
DISMISS_AI_SUGGESTION: 'OpenAI: Dismiss AI suggestions',
|
||||
});
|
||||
|
||||
export const GENERAL_EVENTS = Object.freeze({
|
||||
COMMAND_BAR: 'Used commandbar',
|
||||
});
|
||||
|
||||
@@ -36,6 +36,10 @@ export const getValuesName = (values, list, idKey, nameKey) => {
|
||||
};
|
||||
};
|
||||
|
||||
export const getValuesForStatus = values => {
|
||||
return values.map(value => ({ id: value, name: value }));
|
||||
};
|
||||
|
||||
const getValuesForLabels = (values, labels) => {
|
||||
const selectedLabels = labels.filter(label => values.includes(label.title));
|
||||
return selectedLabels.map(({ title }) => ({
|
||||
@@ -76,6 +80,8 @@ export const getValuesForFilter = (filter, params) => {
|
||||
labels,
|
||||
} = params;
|
||||
switch (attribute_key) {
|
||||
case 'status':
|
||||
return getValuesForStatus(values);
|
||||
case 'assignee_id':
|
||||
return getValuesName(values, agents, 'id', 'name');
|
||||
case 'inbox_id':
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
getAttributeInputType,
|
||||
getInputType,
|
||||
getValuesName,
|
||||
getValuesForStatus,
|
||||
getValuesForFilter,
|
||||
generateValuesForEditCustomViews,
|
||||
generateCustomAttributesInputType,
|
||||
@@ -106,7 +107,33 @@ describe('customViewsHelper', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getValuesForStatus', () => {
|
||||
it('should return id and name if value is present', () => {
|
||||
const values = ['open'];
|
||||
expect(getValuesForStatus(values)).toEqual([
|
||||
{ id: 'open', name: 'open' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return id and name if multiple values are present', () => {
|
||||
const values = ['open', 'resolved'];
|
||||
expect(getValuesForStatus(values)).toEqual([
|
||||
{ id: 'open', name: 'open' },
|
||||
{ id: 'resolved', name: 'resolved' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getValuesForFilter', () => {
|
||||
it('should return id and name if attribute_key is status', () => {
|
||||
const filter = { attribute_key: 'status', values: ['open', 'resolved'] };
|
||||
const params = {};
|
||||
expect(getValuesForFilter(filter, params)).toEqual([
|
||||
{ id: 'open', name: 'open' },
|
||||
{ id: 'resolved', name: 'resolved' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return id and name if attribute_key is assignee_id', () => {
|
||||
const filter = { attribute_key: 'assignee_id', values: [1] };
|
||||
const params = { agents: [{ id: 1, name: 'test' }] };
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import { setColorTheme } from 'dashboard/helper/themeHelper.js';
|
||||
import { LocalStorage } from 'shared/helpers/localStorage';
|
||||
|
||||
jest.mock('shared/helpers/localStorage');
|
||||
|
||||
describe('setColorTheme', () => {
|
||||
it('should set body class to dark if selectedColorScheme is dark', () => {
|
||||
LocalStorage.get.mockReturnValue('dark');
|
||||
setColorTheme(true);
|
||||
expect(document.body.classList.contains('dark')).toBe(true);
|
||||
});
|
||||
|
||||
it('should set body class to dark if selectedColorScheme is auto and isOSOnDarkMode is true', () => {
|
||||
LocalStorage.get.mockReturnValue('auto');
|
||||
setColorTheme(true);
|
||||
expect(document.body.classList.contains('dark')).toBe(true);
|
||||
});
|
||||
|
||||
it('should not set body class to dark if selectedColorScheme is auto and isOSOnDarkMode is false', () => {
|
||||
LocalStorage.get.mockReturnValue('auto');
|
||||
setColorTheme(false);
|
||||
expect(document.body.classList.contains('dark')).toBe(false);
|
||||
});
|
||||
|
||||
it('should not set body class to dark if selectedColorScheme is light', () => {
|
||||
LocalStorage.get.mockReturnValue('light');
|
||||
setColorTheme(true);
|
||||
expect(document.body.classList.contains('dark')).toBe(false);
|
||||
});
|
||||
|
||||
it('should not set body class to dark if selectedColorScheme is undefined', () => {
|
||||
LocalStorage.get.mockReturnValue(undefined);
|
||||
setColorTheme(true);
|
||||
expect(document.body.classList.contains('dark')).toBe(true);
|
||||
});
|
||||
|
||||
it('should set documentElement style to dark if selectedColorScheme is dark', () => {
|
||||
LocalStorage.get.mockReturnValue('dark');
|
||||
setColorTheme(true);
|
||||
expect(document.documentElement.getAttribute('style')).toBe(
|
||||
'color-scheme: dark;'
|
||||
);
|
||||
});
|
||||
|
||||
it('should set documentElement style to dark if selectedColorScheme is auto and isOSOnDarkMode is true', () => {
|
||||
LocalStorage.get.mockReturnValue('auto');
|
||||
setColorTheme(true);
|
||||
expect(document.documentElement.getAttribute('style')).toBe(
|
||||
'color-scheme: dark;'
|
||||
);
|
||||
});
|
||||
|
||||
it('should set documentElement style to light if selectedColorScheme is auto and isOSOnDarkMode is false', () => {
|
||||
LocalStorage.get.mockReturnValue('auto');
|
||||
setColorTheme(false);
|
||||
expect(document.documentElement.getAttribute('style')).toBe(
|
||||
'color-scheme: light;'
|
||||
);
|
||||
});
|
||||
|
||||
it('should set documentElement style to light if selectedColorScheme is light', () => {
|
||||
LocalStorage.get.mockReturnValue('light');
|
||||
setColorTheme(true);
|
||||
expect(document.documentElement.getAttribute('style')).toBe(
|
||||
'color-scheme: light;'
|
||||
);
|
||||
});
|
||||
|
||||
it('should set documentElement style to light if selectedColorScheme is undefined', () => {
|
||||
LocalStorage.get.mockReturnValue(undefined);
|
||||
setColorTheme(true);
|
||||
expect(document.documentElement.getAttribute('style')).toBe(
|
||||
'color-scheme: dark;'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
import { LocalStorage } from 'shared/helpers/localStorage';
|
||||
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
|
||||
|
||||
export const setColorTheme = isOSOnDarkMode => {
|
||||
const selectedColorScheme =
|
||||
LocalStorage.get(LOCAL_STORAGE_KEYS.COLOR_SCHEME) || 'auto';
|
||||
if (
|
||||
(selectedColorScheme === 'auto' && isOSOnDarkMode) ||
|
||||
selectedColorScheme === 'dark'
|
||||
) {
|
||||
document.body.classList.add('dark');
|
||||
document.documentElement.setAttribute('style', 'color-scheme: dark;');
|
||||
} else {
|
||||
document.body.classList.remove('dark');
|
||||
document.documentElement.setAttribute('style', 'color-scheme: light;');
|
||||
}
|
||||
};
|
||||
@@ -111,7 +111,8 @@
|
||||
"ADD_LABEL": "Add label to the conversation",
|
||||
"REMOVE_LABEL": "Remove label from the conversation",
|
||||
"SETTINGS": "Settings",
|
||||
"AI_ASSIST": "AI Assist"
|
||||
"AI_ASSIST": "AI Assist",
|
||||
"APPEARANCE": "Appearance"
|
||||
},
|
||||
"COMMANDS": {
|
||||
"GO_TO_CONVERSATION_DASHBOARD": "Go to Conversation Dashboard",
|
||||
@@ -148,7 +149,11 @@
|
||||
"UNTIL_TOMORROW": "Until tomorrow",
|
||||
"UNTIL_NEXT_MONTH": "Until next month",
|
||||
"AN_HOUR_FROM_NOW": "Until an hour from now",
|
||||
"CUSTOM": "Custom..."
|
||||
"CUSTOM": "Custom...",
|
||||
"CHANGE_APPEARANCE": "Change Appearance",
|
||||
"LIGHT_MODE": "Light",
|
||||
"DARK_MODE": "Dark",
|
||||
"SYSTEM_MODE": "System"
|
||||
}
|
||||
},
|
||||
"DASHBOARD_APPS": {
|
||||
|
||||
@@ -70,10 +70,26 @@
|
||||
}
|
||||
},
|
||||
"SLACK": {
|
||||
"DELETE": "Delete",
|
||||
"DELETE_CONFIRMATION": {
|
||||
"TITLE": "Delete the integration",
|
||||
"MESSAGE": "Are you sure you want to delete the integration? Doing so will result in the loss of access to conversations on your Slack workspace."
|
||||
},
|
||||
"HELP_TEXT": {
|
||||
"TITLE": "Using Slack Integration",
|
||||
"BODY": "<br/><p>Chatwoot will now sync all the incoming conversations into the <b><i>customer-conversations</i></b> channel inside your slack workplace.</p><p>Replying to a conversation thread in <b><i>customer-conversations</i></b> slack channel will create a response back to the customer through chatwoot.</p><p>Start the replies with <b><i>note:</i></b> to create private notes instead of replies.</p><p>If the replier on slack has an agent profile in chatwoot under the same email, the replies will be associated accordingly.</p><p>When the replier doesn't have an associated agent profile, the replies will be made from the bot profile.</p>"
|
||||
}
|
||||
"TITLE": "How to use the Slack Integration?",
|
||||
"BODY": "With this integration, all of your incoming conversations will be synced to the ***%{selectedChannelName}*** channel in your Slack workspace. You can manage all your customer conversations right within the channel and never miss a message.\n\nHere are the main features of the integration:\n\n**Respond to conversations from within Slack:** To respond to a conversation in the ***%{selectedChannelName}*** Slack channel, simply type out your message and send it as a thread. This will create a response back to the customer through Chatwoot. It's that simple!\n\n **Create private notes:** If you want to create private notes instead of replies, start your message with ***`note:`***. This ensures that your message is kept private and won't be visible to the customer.\n\n**Associate an agent profile:** If the person who replied on Slack has an agent profile in Chatwoot under the same email, the replies will be associated with that agent profile automatically. This means you can easily track who said what and when. On the other hand, when the replier doesn't have an associated agent profile, the replies will appear from the bot profile to the customer.",
|
||||
"SELECTED": "selected"
|
||||
},
|
||||
"SELECT_CHANNEL": {
|
||||
"OPTION_LABEL": "Select a channel",
|
||||
"UPDATE": "Update",
|
||||
"BUTTON_TEXT": "Connect channel",
|
||||
"DESCRIPTION": "Your Slack workspace is now linked with Chatwoot. However, the integration is currently inactive. To activate the integration and connect a channel to Chatwoot, please click the button below.\n\n**Note:** If you are attempting to connect a private channel, add the Chatwoot app to the Slack channel before proceeding with this step.",
|
||||
"ATTENTION_REQUIRED": "Attention required"
|
||||
},
|
||||
"UPDATE_ERROR": "There was an error updating the integration, please try again",
|
||||
"UPDATE_SUCCESS": "The channel is connected successfully",
|
||||
"FAILED_TO_FETCH_CHANNELS": "There was an error fetching the channels from Slack, please try again"
|
||||
},
|
||||
"DYTE": {
|
||||
"CLICK_HERE_TO_JOIN": "Click here to join",
|
||||
@@ -121,7 +137,7 @@
|
||||
"GENERATING": "Generating...",
|
||||
"CANCEL": "Cancel"
|
||||
},
|
||||
"GENERATE_ERROR": "There was an error processing the content, please try again"
|
||||
"GENERATE_ERROR": "There was an error processing the content, please verify your OpenAI API key and try again"
|
||||
},
|
||||
"DELETE": {
|
||||
"BUTTON_TEXT": "Delete",
|
||||
|
||||
@@ -145,6 +145,7 @@
|
||||
"SELECTOR_SUBTITLE": "Select an account from the following list",
|
||||
"PROFILE_SETTINGS": "Profile Settings",
|
||||
"KEYBOARD_SHORTCUTS": "Keyboard Shortcuts",
|
||||
"APPEARANCE": "Change Appearance",
|
||||
"SUPER_ADMIN_CONSOLE": "Super Admin Console",
|
||||
"LOGOUT": "Logout"
|
||||
},
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
"BUTTON_LABEL": "Export",
|
||||
"TITLE": "Export Contacts",
|
||||
"DESC": "Export contacts to a CSV file.",
|
||||
"SUCCESS_MESSAGE": "Export is in progress, You will be notified via email when export file is ready to dowanlod.",
|
||||
"SUCCESS_MESSAGE": "Export is in progress, You will be notified via email when export file is ready to download.",
|
||||
"ERROR_MESSAGE": "There was an error, please try again"
|
||||
},
|
||||
"DELETE_NOTE": {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { mapGetters } from 'vuex';
|
||||
import { OPEN_AI_EVENTS } from '../helper/AnalyticsHelper/events';
|
||||
import { LOCAL_STORAGE_KEYS } from '../constants/localStorage';
|
||||
import { LocalStorage } from '../../shared/helpers/localStorage';
|
||||
import OpenAPI from '../api/integrations/openapi';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
|
||||
export default {
|
||||
mixins: [alertMixin],
|
||||
mounted() {
|
||||
this.fetchIntegrationsIfRequired();
|
||||
},
|
||||
@@ -50,6 +50,8 @@ export default {
|
||||
}
|
||||
},
|
||||
async fetchLabelSuggestions({ conversationId }) {
|
||||
if (!conversationId) return [];
|
||||
|
||||
try {
|
||||
const result = await OpenAPI.processEvent({
|
||||
type: 'label_suggestion',
|
||||
@@ -66,29 +68,6 @@ export default {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
getDismissedConversations(accountId) {
|
||||
const suggestionKey = LOCAL_STORAGE_KEYS.DISMISSED_LABEL_SUGGESTIONS;
|
||||
|
||||
// fetch the value from Storage
|
||||
const valueFromStorage = LocalStorage.get(suggestionKey);
|
||||
|
||||
// Case 1: the key is not initialized
|
||||
if (!valueFromStorage) {
|
||||
LocalStorage.set(suggestionKey, {
|
||||
[accountId]: [],
|
||||
});
|
||||
return LocalStorage.get(suggestionKey);
|
||||
}
|
||||
|
||||
// Case 2: the key is initialized, but account ID is not present
|
||||
if (!valueFromStorage[accountId]) {
|
||||
valueFromStorage[accountId] = [];
|
||||
LocalStorage.set(suggestionKey, valueFromStorage);
|
||||
return LocalStorage.get(suggestionKey);
|
||||
}
|
||||
|
||||
return valueFromStorage;
|
||||
},
|
||||
cleanLabels(labels) {
|
||||
return labels
|
||||
.toLowerCase() // Set it to lowercase
|
||||
|
||||
@@ -88,47 +88,6 @@ describe('aiMixin', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('gets dismissed conversations', () => {
|
||||
const getSpy = jest.spyOn(LocalStorage, 'get');
|
||||
const setSpy = jest.spyOn(LocalStorage, 'set');
|
||||
|
||||
const accountId = 123;
|
||||
const valueFromStorage = { [accountId]: ['conv1', 'conv2'] };
|
||||
|
||||
// in case key is not initialized
|
||||
getSpy.mockReturnValueOnce(null);
|
||||
wrapper.vm.getDismissedConversations(accountId);
|
||||
|
||||
expect(getSpy).toHaveBeenCalledWith('dismissedLabelSuggestions');
|
||||
expect(setSpy).toHaveBeenCalledWith('dismissedLabelSuggestions', {
|
||||
[accountId]: [],
|
||||
});
|
||||
|
||||
// rest spy
|
||||
getSpy.mockReset();
|
||||
setSpy.mockReset();
|
||||
|
||||
// in case we get the value from storage
|
||||
getSpy.mockReturnValueOnce(valueFromStorage);
|
||||
const result = wrapper.vm.getDismissedConversations(accountId);
|
||||
expect(result).toEqual(valueFromStorage);
|
||||
expect(getSpy).toHaveBeenCalledWith('dismissedLabelSuggestions');
|
||||
expect(setSpy).not.toHaveBeenCalled();
|
||||
|
||||
// rest spy
|
||||
getSpy.mockReset();
|
||||
setSpy.mockReset();
|
||||
|
||||
// in case we get the value from storage but accountId is not present
|
||||
getSpy.mockReturnValueOnce(valueFromStorage);
|
||||
wrapper.vm.getDismissedConversations(234);
|
||||
expect(getSpy).toHaveBeenCalledWith('dismissedLabelSuggestions');
|
||||
expect(setSpy).toHaveBeenCalledWith('dismissedLabelSuggestions', {
|
||||
...valueFromStorage,
|
||||
234: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('cleans labels', () => {
|
||||
const labels = 'label1, label2, label1';
|
||||
expect(wrapper.vm.cleanLabels(labels)).toEqual(['label1', 'label2']);
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
<template>
|
||||
<div class="card">
|
||||
<div
|
||||
class="flex flex-col mb-2 p-4 border border-solid border-slate-75 dark:border-slate-700 overflow-hidden rounded-md flex-grow shadow-sm bg-white dark:bg-slate-900 text-slate-700 dark:text-slate-100"
|
||||
>
|
||||
<woot-message-editor
|
||||
v-model="noteContent"
|
||||
class="input--note"
|
||||
:placeholder="$t('NOTES.ADD.PLACEHOLDER')"
|
||||
:enable-suggestions="false"
|
||||
/>
|
||||
<div class="footer">
|
||||
<div class="flex justify-end w-full">
|
||||
<woot-button
|
||||
color-scheme="warning"
|
||||
:title="$t('NOTES.ADD.TITLE')"
|
||||
@@ -75,10 +77,4 @@ export default {
|
||||
max-height: 22.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,20 +1,28 @@
|
||||
<template>
|
||||
<div class="card note-wrap">
|
||||
<div class="header">
|
||||
<div class="meta">
|
||||
<div
|
||||
class="flex flex-col mb-2 p-4 border border-solid border-slate-75 dark:border-slate-700 overflow-hidden rounded-md flex-grow shadow-sm bg-white dark:bg-slate-900 text-slate-700 dark:text-slate-100 note-wrap"
|
||||
>
|
||||
<div class="flex justify-between items-end text-xs gap-1">
|
||||
<div class="flex items-center">
|
||||
<thumbnail
|
||||
:title="noteAuthorName"
|
||||
:src="noteAuthor.thumbnail"
|
||||
:username="noteAuthorName"
|
||||
size="20px"
|
||||
/>
|
||||
<div class="date-wrap">
|
||||
<span class="fw-medium"> {{ noteAuthorName }} </span>
|
||||
<span> {{ $t('NOTES.LIST.LABEL') }} </span>
|
||||
<span class="fw-medium time-stamp"> {{ readableTime }} </span>
|
||||
<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="actions">
|
||||
<div class="actions flex invisible">
|
||||
<woot-button
|
||||
v-tooltip="$t('NOTES.CONTENT_HEADER.DELETE')"
|
||||
variant="smooth"
|
||||
@@ -35,7 +43,10 @@
|
||||
:reject-text="$t('DELETE_NOTE.CONFIRM.NO')"
|
||||
/>
|
||||
</div>
|
||||
<p v-dompurify-html="formatMessage(note || '')" class="note__content" />
|
||||
<p
|
||||
v-dompurify-html="formatMessage(note || '')"
|
||||
class="note__content mt-4"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -116,39 +127,9 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
.note__content {
|
||||
margin-top: var(--space-normal);
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
font-size: var(--font-size-mini);
|
||||
|
||||
.meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.date-wrap {
|
||||
margin: 0 var(--space-smaller);
|
||||
padding: var(--space-micro);
|
||||
color: var(--color-body);
|
||||
}
|
||||
}
|
||||
.actions {
|
||||
display: flex;
|
||||
visibility: hidden;
|
||||
|
||||
.button {
|
||||
margin-left: var(--space-small);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.note-wrap:hover {
|
||||
.actions {
|
||||
visibility: visible;
|
||||
@apply visible;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<blockquote
|
||||
ref="messageContainer"
|
||||
class="message border-l-2 border-slate-100 dark:border-slate-800"
|
||||
class="message border-l-2 border-slate-100 dark:border-slate-700"
|
||||
>
|
||||
<p class="header">
|
||||
<strong class="text-slate-700 dark:text-slate-100">
|
||||
@@ -49,13 +49,20 @@ export default {
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
const wrap = this.$refs.messageContainer;
|
||||
const message = wrap.querySelector('.message-content');
|
||||
this.isOverflowing = message.offsetHeight > 150;
|
||||
});
|
||||
this.$watch(() => {
|
||||
return this.$refs.messageContainer;
|
||||
}, this.setOverflow);
|
||||
|
||||
this.$nextTick(this.setOverflow);
|
||||
},
|
||||
methods: {
|
||||
setOverflow() {
|
||||
const wrap = this.$refs.messageContainer;
|
||||
if (wrap) {
|
||||
const message = wrap.querySelector('.message-content');
|
||||
this.isOverflowing = message.offsetHeight > 150;
|
||||
}
|
||||
},
|
||||
escapeHtml(html) {
|
||||
var text = document.createTextNode(html);
|
||||
var p = document.createElement('p');
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
<template>
|
||||
<div class="read-more">
|
||||
<div ref="content" :class="{ 'shrink-container': shrink }">
|
||||
<div
|
||||
ref="content"
|
||||
:class="{
|
||||
'shrink-container after:shrink-gradient-light dark:after:shrink-gradient-dark': shrink,
|
||||
}"
|
||||
>
|
||||
<slot />
|
||||
<woot-button
|
||||
v-if="shrink"
|
||||
@@ -27,30 +32,32 @@ export default {
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
<style scoped>
|
||||
@tailwind components;
|
||||
@layer components {
|
||||
.shrink-gradient-light {
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(255, 255, 255, 0),
|
||||
rgba(255, 255, 255, 1) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.shrink-gradient-dark {
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(0, 0, 0, 0),
|
||||
rgb(21, 23, 24) 100%
|
||||
);
|
||||
}
|
||||
}
|
||||
.shrink-container {
|
||||
max-height: 100px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
@apply max-h-[100px] overflow-hidden relative;
|
||||
}
|
||||
.shrink-container::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 50px;
|
||||
background: linear-gradient(to bottom, rgba(255, 255, 255, 0), #fff 100%);
|
||||
z-index: 4;
|
||||
@apply content-[''] absolute bottom-0 left-0 right-0 h-[50px] z-10;
|
||||
}
|
||||
.read-more-button {
|
||||
max-width: max-content;
|
||||
position: absolute;
|
||||
bottom: var(--space-small);
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: 0 auto;
|
||||
z-index: 5;
|
||||
box-shadow: var(--box-shadow);
|
||||
@apply max-w-max absolute bottom-2 left-0 right-0 mx-auto mt-0 z-20 shadow-sm;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,11 +2,17 @@
|
||||
<router-link :to="navigateTo" class="contact-item">
|
||||
<woot-thumbnail :src="thumbnail" :username="name" size="24px" />
|
||||
<div class="contact-details">
|
||||
<h5 class="text-block-title name">{{ name }}</h5>
|
||||
<h5 class="text-block-title name text-slate-800 dark:text-slate-200">
|
||||
{{ name }}
|
||||
</h5>
|
||||
<p class="details-meta">
|
||||
<span v-if="email" class="email">{{ email }}</span>
|
||||
<span v-if="phone" class="separator">•</span>
|
||||
<span v-if="phone" class="phone">
|
||||
<span v-if="email" class="email text-slate-800 dark:text-slate-200">{{
|
||||
email
|
||||
}}</span>
|
||||
<span v-if="phone" class="separator text-slate-700 dark:text-slate-200">
|
||||
•
|
||||
</span>
|
||||
<span v-if="phone" class="phone text-slate-800 dark:text-slate-200">
|
||||
{{ phone }}
|
||||
</span>
|
||||
</p>
|
||||
|
||||
@@ -21,13 +21,16 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="user-details">
|
||||
<h5 v-if="name" class="text-block-title name">
|
||||
<h5
|
||||
v-if="name"
|
||||
class="text-block-title name text-slate-800 dark:text-slate-100"
|
||||
>
|
||||
<span class="pre-text"> {{ $t('SEARCH.FROM') }}: </span>
|
||||
{{ name }}
|
||||
</h5>
|
||||
<h5
|
||||
v-if="email"
|
||||
class="text-block-title email overflow-hidden whitespace-nowrap text-ellipsis"
|
||||
class="text-block-title email text-slate-700 dark:text-slate-200 overflow-hidden whitespace-nowrap text-ellipsis"
|
||||
>
|
||||
<span class="pre-text">{{ $t('SEARCH.EMAIL') }}:</span>
|
||||
{{ email }}
|
||||
|
||||
@@ -66,3 +66,8 @@ export const ICON_AI_SPELLING = `<svg role="img" class="ninja-icon ninja-icon--f
|
||||
export const ICON_AI_EXPAND = `<svg role="img" class="ninja-icon ninja-icon--fluent" width="18" height="18" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M6.75 19.5h14.5a.75.75 0 0 0 .102-1.493L21.25 18H6.75a.75.75 0 0 0-.102 1.493l.102.007Zm0-15h14.5a.75.75 0 0 0 .102-1.493L21.25 3H6.75a.75.75 0 0 0-.102 1.493l.102.007Zm7 3.5a.75.75 0 0 0 0 1.5h7.5a.75.75 0 0 0 0-1.5h-7.5ZM13 13.75a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 0 1.5h-7.5a.75.75 0 0 1-.75-.75Zm-2-2.25a4.5 4.5 0 1 1-9 0a4.5 4.5 0 0 1 9 0Zm-4-2a.5.5 0 0 0-1 0V11H4.5a.5.5 0 0 0 0 1H6v1.5a.5.5 0 0 0 1 0V12h1.5a.5.5 0 0 0 0-1H7V9.5Z" fill="currentColor"/></svg>`;
|
||||
export const ICON_AI_SHORTEN = `<svg role="img" class="ninja-icon ninja-icon--fluent" width="18" height="18" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M6.75 4.5h14.5a.75.75 0 0 0 .102-1.493L21.25 3H6.75a.75.75 0 0 0-.102 1.493l.102.007Zm0 15h14.5a.75.75 0 0 0 .102-1.493L21.25 18H6.75a.75.75 0 0 0-.102 1.493l.102.007Zm7-11.5a.75.75 0 0 0 0 1.5h7.5a.75.75 0 0 0 0-1.5h-7.5ZM13 13.75a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 0 1.5h-7.5a.75.75 0 0 1-.75-.75Zm-2-2.25a4.5 4.5 0 1 1-9 0a4.5 4.5 0 0 1 9 0Zm-2 0a.5.5 0 0 0-.5-.5h-4a.5.5 0 0 0 0 1h4a.5.5 0 0 0 .5-.5Z" fill="currentColor"/></svg>`;
|
||||
export const ICON_AI_GRAMMAR = `<svg role="img" class="ninja-icon ninja-icon--fluent" width="18" height="18" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M3 17h7.522l-2 2H3a1 1 0 0 1-.117-1.993L3 17Zm0-2h7.848a1.75 1.75 0 0 1-.775-2H3l-.117.007A1 1 0 0 0 3 15Zm0-8h18l.117-.007A1 1 0 0 0 21 5H3l-.117.007A1 1 0 0 0 3 7Zm9.72 9.216a.75.75 0 1 1 1.06 1.06l-4.5 4.5a.75.75 0 1 1-1.06-1.06l4.5-4.5ZM3 9h10a1 1 0 0 1 .117 1.993L13 11H3a1 1 0 0 1-.117-1.993L3 9Zm13.5-1a.75.75 0 0 1 .744.658l.14 1.13a3.25 3.25 0 0 0 2.828 2.829l1.13.139a.75.75 0 0 1 0 1.488l-1.13.14a3.25 3.25 0 0 0-2.829 2.828l-.139 1.13a.75.75 0 0 1-1.488 0l-.14-1.13a3.25 3.25 0 0 0-2.828-2.829l-1.13-.139a.75.75 0 0 1 0-1.488l1.13-.14a3.25 3.25 0 0 0 2.829-2.828l.139-1.13A.75.75 0 0 1 16.5 8Z" fill="currentColor"/></svg>`;
|
||||
|
||||
export const ICON_APPEARANCE = `<svg role="img" class="ninja-icon ninja-icon--fluent" xmlns="http://www.w3.org/2000/svg" width="18" height="18"viewBox="0 0 24 24"><path fill="currentColor" d="M3.839 5.858c2.94-3.916 9.03-5.055 13.364-2.36c4.28 2.66 5.854 7.777 4.1 12.577c-1.655 4.533-6.016 6.328-9.159 4.048c-1.177-.854-1.634-1.925-1.854-3.664l-.106-.987l-.045-.398c-.123-.934-.311-1.352-.705-1.572c-.535-.298-.892-.305-1.595-.033l-.351.146l-.179.078c-1.014.44-1.688.595-2.541.416l-.2-.047l-.164-.047c-2.789-.864-3.202-4.647-.565-8.157Zm.984 6.716l.123.037l.134.03c.439.087.814.015 1.437-.242l.602-.257c1.202-.493 1.985-.54 3.046.05c.917.512 1.275 1.298 1.457 2.66l.053.459l.055.532l.047.422c.172 1.361.485 2.09 1.248 2.644c2.275 1.65 5.534.309 6.87-3.349c1.516-4.152.174-8.514-3.484-10.789c-3.675-2.284-8.899-1.306-11.373 1.987c-2.075 2.763-1.82 5.28-.215 5.816Zm11.225-1.994a1.25 1.25 0 1 1 2.414-.647a1.25 1.25 0 0 1-2.414.647Zm.494 3.488a1.25 1.25 0 1 1 2.415-.647a1.25 1.25 0 0 1-2.415.647ZM14.07 7.577a1.25 1.25 0 1 1 2.415-.647a1.25 1.25 0 0 1-2.415.647Zm-.028 8.998a1.25 1.25 0 1 1 2.414-.647a1.25 1.25 0 0 1-2.414.647Zm-3.497-9.97a1.25 1.25 0 1 1 2.415-.646a1.25 1.25 0 0 1-2.415.646Z"/></svg>`;
|
||||
export const ICON_LIGHT_MODE = `<svg role="img" class="ninja-icon ninja-icon--fluent" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24"><path fill="currentColor" d="M12 2a.75.75 0 0 1 .75.75v1.5a.75.75 0 0 1-1.5 0v-1.5A.75.75 0 0 1 12 2Zm5 10a5 5 0 1 1-10 0a5 5 0 0 1 10 0Zm4.25.75a.75.75 0 0 0 0-1.5h-1.5a.75.75 0 0 0 0 1.5h1.5ZM12 19a.75.75 0 0 1 .75.75v1.5a.75.75 0 0 1-1.5 0v-1.5A.75.75 0 0 1 12 19Zm-7.75-6.25a.75.75 0 0 0 0-1.5h-1.5a.75.75 0 0 0 0 1.5h1.5Zm-.03-8.53a.75.75 0 0 1 1.06 0l1.5 1.5a.75.75 0 0 1-1.06 1.06l-1.5-1.5a.75.75 0 0 1 0-1.06Zm1.06 15.56a.75.75 0 1 1-1.06-1.06l1.5-1.5a.75.75 0 1 1 1.06 1.06l-1.5 1.5Zm14.5-15.56a.75.75 0 0 0-1.06 0l-1.5 1.5a.75.75 0 0 0 1.06 1.06l1.5-1.5a.75.75 0 0 0 0-1.06Zm-1.06 15.56a.75.75 0 1 0 1.06-1.06l-1.5-1.5a.75.75 0 1 0-1.06 1.06l1.5 1.5Z"/></svg>`;
|
||||
export const ICON_DARK_MODE = `<svg role="img" class="ninja-icon ninja-icon--fluent" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24"><path fill="currentColor" d="M20.026 17.001c-2.762 4.784-8.879 6.423-13.663 3.661A9.965 9.965 0 0 1 3.13 17.68a.75.75 0 0 1 .365-1.132c3.767-1.348 5.785-2.91 6.956-5.146c1.232-2.353 1.551-4.93.689-8.463a.75.75 0 0 1 .769-.927a9.961 9.961 0 0 1 4.457 1.327c4.784 2.762 6.423 8.879 3.66 13.662Zm-8.248-4.903c-1.25 2.389-3.31 4.1-6.817 5.499a8.49 8.49 0 0 0 2.152 1.766a8.502 8.502 0 0 0 8.502-14.725a8.484 8.484 0 0 0-2.792-1.015c.647 3.384.23 6.043-1.045 8.475Z"/></svg>`;
|
||||
export const ICON_SYSTEM_MODE = `<svg role="img" class="ninja-icon ninja-icon--fluent" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24"><path fill="currentColor" d="M4.25 3A2.25 2.25 0 0 0 2 5.25v10.5A2.25 2.25 0 0 0 4.25 18H9.5v1.25c0 .69-.56 1.25-1.25 1.25h-.5a.75.75 0 0 0 0 1.5h8.5a.75.75 0 0 0 0-1.5h-.5c-.69 0-1.25-.56-1.25-1.25V18h5.25A2.25 2.25 0 0 0 22 15.75V5.25A2.25 2.25 0 0 0 19.75 3H4.25ZM13 18v1.25c0 .45.108.875.3 1.25h-2.6c.192-.375.3-.8.3-1.25V18h2ZM3.5 5.25a.75.75 0 0 1 .75-.75h15.5a.75.75 0 0 1 .75.75V13h-17V5.25Zm0 9.25h17v1.25a.75.75 0 0 1-.75.75H4.25a.75.75 0 0 1-.75-.75V14.5Z"/></svg>`;
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import {
|
||||
ICON_APPEARANCE,
|
||||
ICON_LIGHT_MODE,
|
||||
ICON_DARK_MODE,
|
||||
ICON_SYSTEM_MODE,
|
||||
} from './CommandBarIcons';
|
||||
import { LocalStorage } from 'shared/helpers/localStorage';
|
||||
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
|
||||
import { setColorTheme } from 'dashboard/helper/themeHelper.js';
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
themeOptions() {
|
||||
return [
|
||||
{
|
||||
key: 'light',
|
||||
label: this.$t('COMMAND_BAR.COMMANDS.LIGHT_MODE'),
|
||||
icon: ICON_LIGHT_MODE,
|
||||
},
|
||||
{
|
||||
key: 'dark',
|
||||
label: this.$t('COMMAND_BAR.COMMANDS.DARK_MODE'),
|
||||
icon: ICON_DARK_MODE,
|
||||
},
|
||||
|
||||
{
|
||||
key: 'auto',
|
||||
label: this.$t('COMMAND_BAR.COMMANDS.SYSTEM_MODE'),
|
||||
icon: ICON_SYSTEM_MODE,
|
||||
},
|
||||
];
|
||||
},
|
||||
goToAppearanceHotKeys() {
|
||||
const options = this.themeOptions.map(theme => ({
|
||||
id: theme.key,
|
||||
title: theme.label,
|
||||
parent: 'appearance_settings',
|
||||
section: this.$t('COMMAND_BAR.SECTIONS.APPEARANCE'),
|
||||
icon: theme.icon,
|
||||
handler: () => {
|
||||
this.setAppearance(theme.key);
|
||||
},
|
||||
}));
|
||||
return [
|
||||
{
|
||||
id: 'appearance_settings',
|
||||
title: this.$t('COMMAND_BAR.COMMANDS.CHANGE_APPEARANCE'),
|
||||
section: this.$t('COMMAND_BAR.SECTIONS.APPEARANCE'),
|
||||
icon: ICON_APPEARANCE,
|
||||
children: options.map(option => option.id),
|
||||
},
|
||||
...options,
|
||||
];
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
setAppearance(theme) {
|
||||
LocalStorage.set(LOCAL_STORAGE_KEYS.COLOR_SCHEME, theme);
|
||||
const isOSOnDarkMode = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
.matches;
|
||||
setColorTheme(isOSOnDarkMode);
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -5,7 +5,7 @@
|
||||
:no-auto-load-md-icons="true"
|
||||
hideBreadcrumbs
|
||||
:placeholder="placeholder"
|
||||
@selected="setCommandbarData"
|
||||
@selected="onSelected"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -13,10 +13,12 @@
|
||||
import 'ninja-keys';
|
||||
import conversationHotKeysMixin from './conversationHotKeys';
|
||||
import goToCommandHotKeys from './goToCommandHotKeys';
|
||||
import appearanceHotKeys from './appearanceHotKeys';
|
||||
import agentMixin from 'dashboard/mixins/agentMixin';
|
||||
import conversationLabelMixin from 'dashboard/mixins/conversation/labelMixin';
|
||||
import conversationTeamMixin from 'dashboard/mixins/conversation/teamMixin';
|
||||
import adminMixin from 'dashboard/mixins/isAdmin';
|
||||
import { GENERAL_EVENTS } from '../../../helper/AnalyticsHelper/events';
|
||||
|
||||
export default {
|
||||
mixins: [
|
||||
@@ -25,6 +27,7 @@ export default {
|
||||
conversationHotKeysMixin,
|
||||
conversationLabelMixin,
|
||||
conversationTeamMixin,
|
||||
appearanceHotKeys,
|
||||
goToCommandHotKeys,
|
||||
],
|
||||
|
||||
@@ -41,7 +44,11 @@ export default {
|
||||
return this.$route.name;
|
||||
},
|
||||
hotKeys() {
|
||||
return [...this.conversationHotKeys, ...this.goToCommandHotKeys];
|
||||
return [
|
||||
...this.conversationHotKeys,
|
||||
...this.goToCommandHotKeys,
|
||||
...this.goToAppearanceHotKeys,
|
||||
];
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
@@ -56,14 +63,39 @@ export default {
|
||||
setCommandbarData() {
|
||||
this.$refs.ninjakeys.data = this.hotKeys;
|
||||
},
|
||||
onSelected(item) {
|
||||
const {
|
||||
detail: { action: { title = null, section = null } = {} } = {},
|
||||
} = item;
|
||||
this.$track(GENERAL_EVENTS.COMMAND_BAR, {
|
||||
section,
|
||||
action: title,
|
||||
});
|
||||
this.setCommandbarData();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
<style lang="scss">
|
||||
ninja-keys {
|
||||
--ninja-accent-color: var(--w-500);
|
||||
--ninja-font-family: 'PlusJakarta';
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
// Wrapped with body.dark to avoid overriding the default theme
|
||||
// If OS is in dark theme and app is in light mode, It will prevent showing dark theme in command bar
|
||||
body.dark {
|
||||
ninja-keys {
|
||||
--ninja-overflow-background: rgba(26, 29, 30, 0.5);
|
||||
--ninja-modal-background: #151718;
|
||||
--ninja-secondary-background-color: #26292b;
|
||||
--ninja-selected-background: #26292b;
|
||||
--ninja-footer-background: #2b2f31;
|
||||
--ninja-text-color: #f8faf9;
|
||||
--ninja-icon-color: #f8faf9;
|
||||
--ninja-secondary-text-color: #c2c9c6;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -38,6 +38,9 @@ export const isAConversationRoute = routeName =>
|
||||
'conversation_through_inbox',
|
||||
'conversations_through_label',
|
||||
'conversations_through_team',
|
||||
'conversations_through_folders',
|
||||
'conversation_through_unattended',
|
||||
'conversation_through_participating',
|
||||
].includes(routeName);
|
||||
|
||||
export default {
|
||||
|
||||
@@ -152,7 +152,7 @@ export default {
|
||||
}
|
||||
}
|
||||
.contact--panel {
|
||||
@apply border-r border-slate-50 dark:border-slate-700 h-full text-sm overflow-y-auto relative;
|
||||
@apply border-r border-slate-50 dark:border-slate-800/50 h-full text-sm overflow-y-auto relative;
|
||||
}
|
||||
|
||||
.list-group {
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="p-4 rounded-lg bg-slate-25 dark:bg-slate-700 border border-solid border-slate-50 dark:border-slate-700 mb-4"
|
||||
class="p-4 rounded-lg bg-slate-25 dark:bg-slate-900 border border-solid border-slate-50 dark:border-slate-700/50 mb-4"
|
||||
>
|
||||
<filter-input-box
|
||||
v-for="(filter, i) in appliedFilters"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<div class="flex items-center justify-center max-w-full min-w-[6.25rem]">
|
||||
<woot-sidemenu-icon />
|
||||
<h1
|
||||
class="m-0 text-2xl text-slate-900 dark:text-slate-100 overflow-hidden whitespace-nowrap text-ellipsis my-0 mx-2"
|
||||
class="m-0 text-xl text-slate-900 dark:text-slate-100 overflow-hidden whitespace-nowrap text-ellipsis my-0 mx-2"
|
||||
>
|
||||
{{ headerTitle }}
|
||||
</h1>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<template>
|
||||
<div class="view-box columns bg-white">
|
||||
<div
|
||||
class="flex justify-between flex-col h-full m-0 flex-1 bg-white dark:bg-slate-900"
|
||||
>
|
||||
<settings-header
|
||||
button-route="new"
|
||||
:header-title="contact.name"
|
||||
@@ -13,28 +15,22 @@
|
||||
:src="contact.thumbnail"
|
||||
:username="contact.name"
|
||||
size="32px"
|
||||
class="margin-right-1"
|
||||
class="mr-2 rtl:mr-0 rtl:ml-2"
|
||||
/>
|
||||
</settings-header>
|
||||
|
||||
<div
|
||||
v-if="uiFlags.isFetchingItem"
|
||||
class="text-center p-normal fs-default h-full"
|
||||
>
|
||||
<div v-if="uiFlags.isFetchingItem" class="text-center p-4 text-base h-full">
|
||||
<spinner size="" />
|
||||
<span>{{ $t('CONTACT_PROFILE.LOADING') }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="contact.id"
|
||||
class="overflow-hidden column contact--dashboard-content"
|
||||
>
|
||||
<div class="row h-full">
|
||||
<div v-else-if="contact.id" class="overflow-hidden flex-1 min-w-0">
|
||||
<div class="flex flex-wrap ml-auto mr-auto max-w-full h-full">
|
||||
<contact-info-panel
|
||||
:show-close-button="false"
|
||||
:show-avatar="false"
|
||||
:contact="contact"
|
||||
/>
|
||||
<div class="small-12 medium-9 h-full">
|
||||
<div class="w-[75%] h-full">
|
||||
<woot-tabs :index="selectedTabIndex" @change="onClickTabChange">
|
||||
<woot-tabs-item
|
||||
v-for="tab in tabs"
|
||||
@@ -43,7 +39,9 @@
|
||||
:show-badge="false"
|
||||
/>
|
||||
</woot-tabs>
|
||||
<div class="tab-content overflow-auto">
|
||||
<div
|
||||
class="bg-slate-25 dark:bg-slate-800 h-[calc(100%-40px)] p-4 overflow-auto"
|
||||
>
|
||||
<contact-notes
|
||||
v-if="selectedTabIndex === 0"
|
||||
:contact-id="Number(contactId)"
|
||||
@@ -119,22 +117,3 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~dashboard/assets/scss/mixins';
|
||||
|
||||
.left {
|
||||
@apply border-r border-slate-50 dark:border-slate-700;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.right {
|
||||
padding: var(--space-normal);
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
background: var(--color-background-light);
|
||||
height: calc(100% - 40px);
|
||||
padding: var(--space-normal);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -97,7 +97,6 @@
|
||||
<woot-button
|
||||
v-tooltip="$t('CONTACT_PANEL.NEW_MESSAGE')"
|
||||
title="$t('CONTACT_PANEL.NEW_MESSAGE')"
|
||||
class="mr-2 rtl:ml-2 rtl:mr-0"
|
||||
icon="chat"
|
||||
size="small"
|
||||
@click="toggleConversationModal"
|
||||
@@ -105,7 +104,6 @@
|
||||
<woot-button
|
||||
v-tooltip="$t('EDIT_CONTACT.BUTTON_LABEL')"
|
||||
title="$t('EDIT_CONTACT.BUTTON_LABEL')"
|
||||
class="mr-2 rtl:ml-2 rtl:mr-0"
|
||||
icon="edit"
|
||||
variant="smooth"
|
||||
size="small"
|
||||
@@ -114,7 +112,6 @@
|
||||
<woot-button
|
||||
v-tooltip="$t('CONTACT_PANEL.MERGE_CONTACT')"
|
||||
title="$t('CONTACT_PANEL.MERGE_CONTACT')"
|
||||
class="mr-2 rtl:ml-2 rtl:mr-0"
|
||||
icon="merge"
|
||||
variant="smooth"
|
||||
size="small"
|
||||
@@ -126,7 +123,6 @@
|
||||
v-if="isAdmin"
|
||||
v-tooltip="$t('DELETE_CONTACT.BUTTON_LABEL')"
|
||||
title="$t('DELETE_CONTACT.BUTTON_LABEL')"
|
||||
class="mr-2 rtl:ml-2 rtl:mr-0"
|
||||
icon="delete"
|
||||
variant="smooth"
|
||||
size="small"
|
||||
|
||||
@@ -242,7 +242,9 @@ export default {
|
||||
get() {
|
||||
const inboxList = this.contact.contactableInboxes || [];
|
||||
return (
|
||||
inboxList.find(inbox => inbox.inbox.id === this.targetInbox.id) || {
|
||||
inboxList.find(inbox => {
|
||||
return inbox.inbox?.id && inbox.inbox?.id === this.targetInbox?.id;
|
||||
}) || {
|
||||
inbox: {},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
<template>
|
||||
<div v-if="availableProfiles.length" class="social--icons">
|
||||
<div v-if="availableProfiles.length" class="flex items-end mx-0 my-2 gap-3">
|
||||
<a
|
||||
v-for="profile in availableProfiles"
|
||||
:key="profile.key"
|
||||
:href="`${profile.link}${socialProfiles[profile.key]}`"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer nofollow"
|
||||
class="contact--social-icon"
|
||||
>
|
||||
<fluent-icon :icon="`brand-${profile.key}`" size="16" />
|
||||
<fluent-icon
|
||||
:icon="`brand-${profile.key}`"
|
||||
size="16"
|
||||
class="text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-slate-200"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
@@ -41,16 +44,3 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.social--icons {
|
||||
align-items: flex-end;
|
||||
display: flex;
|
||||
margin: var(--space-small) 0 var(--space-smaller);
|
||||
}
|
||||
|
||||
.contact--social-icon {
|
||||
padding-right: var(--space-slab);
|
||||
color: var(--color-body);
|
||||
}
|
||||
</style>
|
||||
|
||||
+5
-1
@@ -1,5 +1,9 @@
|
||||
<template>
|
||||
<div>Loading...</div>
|
||||
<div
|
||||
class="text-slate-600 dark:text-slate-200 flex items-center justify-center w-full"
|
||||
>
|
||||
Loading...
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||
|
||||
+8
-18
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="container overflow-auto">
|
||||
<div class="py-0 px-4 w-full max-w-full overflow-auto">
|
||||
<article-header
|
||||
:header-title="headerTitle"
|
||||
:count="meta.count"
|
||||
@@ -13,9 +13,14 @@
|
||||
@page-change="onPageChange"
|
||||
@reorder="onReorder"
|
||||
/>
|
||||
<div v-if="shouldShowLoader" class="articles--loader">
|
||||
<div
|
||||
v-if="shouldShowLoader"
|
||||
class="items-center flex text-base justify-center py-6 px-4 text-slate-600 dark:text-slate-200"
|
||||
>
|
||||
<spinner />
|
||||
<span>{{ $t('HELP_CENTER.TABLE.LOADING_MESSAGE') }}</span>
|
||||
<span class="text-slate-600 dark:text-slate-200">{{
|
||||
$t('HELP_CENTER.TABLE.LOADING_MESSAGE')
|
||||
}}</span>
|
||||
</div>
|
||||
<empty-state
|
||||
v-else-if="shouldShowEmptyState"
|
||||
@@ -148,18 +153,3 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.container {
|
||||
padding: 0 var(--space-normal);
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
.articles--loader {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
font-size: var(--font-size-default);
|
||||
justify-content: center;
|
||||
padding: var(--space-big);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
+12
-41
@@ -1,13 +1,15 @@
|
||||
<template>
|
||||
<div class="category-list--container">
|
||||
<header>
|
||||
<div class="header-left--wrap">
|
||||
<label class="sub-block-title header-text">{{
|
||||
$t('HELP_CENTER.PORTAL.EDIT.CATEGORIES.TITLE')
|
||||
}}</label>
|
||||
<div class="w-full pl-4">
|
||||
<header class="flex justify-between items-center mb-4">
|
||||
<div class="flex items-center w-full gap-3">
|
||||
<label
|
||||
class="font-normal mb-0 text-base text-slate-800 dark:text-slate-100"
|
||||
>
|
||||
{{ $t('HELP_CENTER.PORTAL.EDIT.CATEGORIES.TITLE') }}
|
||||
</label>
|
||||
<select
|
||||
:value="currentLocaleCode"
|
||||
class="row small-2 select-locale"
|
||||
class="w-[15%] select-locale"
|
||||
@change="changeCurrentCategory"
|
||||
>
|
||||
<option
|
||||
@@ -19,7 +21,7 @@
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="header-right--wrap">
|
||||
<div class="flex-none items-center">
|
||||
<woot-button
|
||||
size="small"
|
||||
variant="smooth"
|
||||
@@ -168,38 +170,7 @@ export default {
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.category-list--container {
|
||||
width: 100%;
|
||||
padding-left: var(--space-normal);
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--space-normal);
|
||||
|
||||
.header-left--wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
|
||||
.header-text {
|
||||
font-weight: var(--font-weight-normal);
|
||||
margin-right: var(--space-slab);
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.header-right--wrap {
|
||||
flex: none;
|
||||
align-items: center;
|
||||
}
|
||||
.select-locale {
|
||||
height: var(--space-large);
|
||||
margin-bottom: 0;
|
||||
padding-top: var(--space-micro);
|
||||
padding-bottom: var(--space-micro);
|
||||
}
|
||||
}
|
||||
.select-locale {
|
||||
@apply h-8 mb-0 py-0.5;
|
||||
}
|
||||
</style>
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="container py-2 px-4 w-full">
|
||||
<div class="py-2 px-4 w-full max-w-full">
|
||||
<div class="flex justify-between items-center mt-0 mb-2 mx-0 h-12">
|
||||
<div class="flex items-center">
|
||||
<woot-sidemenu-icon />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-1 h-full justify-between flex-col m-0 bg-light dark:bg-slate-900"
|
||||
class="flex flex-1 h-full justify-between flex-col m-0 bg-slate-25 dark:bg-slate-900"
|
||||
>
|
||||
<settings-header
|
||||
button-route="new"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<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-700"
|
||||
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"
|
||||
|
||||
@@ -11,7 +11,10 @@
|
||||
</woot-tabs>
|
||||
|
||||
<div class="w-full">
|
||||
<p v-if="!uiFlags.isFetching && !attributes.length" class="mt-12">
|
||||
<p
|
||||
v-if="!uiFlags.isFetching && !attributes.length"
|
||||
class="mt-12 flex items-center justify-center"
|
||||
>
|
||||
{{ $t('ATTRIBUTES_MGMT.LIST.EMPTY_RESULT.404') }}
|
||||
</p>
|
||||
<woot-loading-state
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<div class="flex-1 overflow-auto p-4">
|
||||
<div class="flex-1 overflow-auto">
|
||||
<campaigns-table
|
||||
:campaigns="campaigns"
|
||||
:show-empty-result="showEmptyResult"
|
||||
:is-loading="uiFlags.isFetching"
|
||||
:campaign-type="type"
|
||||
@on-edit-click="openEditPopup"
|
||||
@on-delete-click="openDeletePopup"
|
||||
@edit="openEditPopup"
|
||||
@delete="openDeletePopup"
|
||||
/>
|
||||
<woot-modal :show.sync="showEditPopup" :on-close="hideEditPopup">
|
||||
<edit-campaign
|
||||
@@ -65,26 +65,23 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
openEditPopup(response) {
|
||||
const { row: campaign } = response;
|
||||
openEditPopup(campaign) {
|
||||
this.selectedCampaign = campaign;
|
||||
this.showEditPopup = true;
|
||||
},
|
||||
hideEditPopup() {
|
||||
this.showEditPopup = false;
|
||||
},
|
||||
openDeletePopup(response) {
|
||||
openDeletePopup(campaign) {
|
||||
this.showDeleteConfirmationPopup = true;
|
||||
this.selectedCampaign = response;
|
||||
this.selectedCampaign = campaign;
|
||||
},
|
||||
closeDeletePopup() {
|
||||
this.showDeleteConfirmationPopup = false;
|
||||
},
|
||||
confirmDeletion() {
|
||||
this.closeDeletePopup();
|
||||
const {
|
||||
row: { id },
|
||||
} = this.selectedCampaign;
|
||||
const { id } = this.selectedCampaign;
|
||||
this.deleteCampaign(id);
|
||||
},
|
||||
async deleteCampaign(id) {
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<div
|
||||
class="bg-white dark:bg-slate-800 mb-2 border border-slate-50 dark:border-slate-900 rounded-md px-5 py-4"
|
||||
>
|
||||
<div class="flex flex-row items-start justify-between">
|
||||
<div class="flex flex-col">
|
||||
<div class="text-base font-medium -mt-1 mb-1">
|
||||
{{ campaign.title }}
|
||||
</div>
|
||||
<div
|
||||
v-dompurify-html="formatMessage(campaign.message)"
|
||||
class="text-sm line-clamp-1 [&>p]:mb-0"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-row space-x-4">
|
||||
<woot-button
|
||||
v-if="isOngoingType"
|
||||
variant="link"
|
||||
icon="edit"
|
||||
color-scheme="secondary"
|
||||
size="small"
|
||||
@click="$emit('edit', campaign)"
|
||||
>
|
||||
{{ $t('CAMPAIGN.LIST.BUTTONS.EDIT') }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
variant="link"
|
||||
icon="dismiss-circle"
|
||||
size="small"
|
||||
color-scheme="secondary"
|
||||
@click="$emit('delete', campaign)"
|
||||
>
|
||||
{{ $t('CAMPAIGN.LIST.BUTTONS.DELETE') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row mt-5 items-center space-x-3">
|
||||
<woot-label
|
||||
small
|
||||
:title="campaignStatus"
|
||||
:color-scheme="colorScheme"
|
||||
class="text-xs mr-3"
|
||||
/>
|
||||
<inbox-name :inbox="campaign.inbox" class="ltr:ml-0 rtl:mr-0 mb-1" />
|
||||
<user-avatar-with-name
|
||||
v-if="campaign.sender"
|
||||
:user="campaign.sender"
|
||||
class="mb-1"
|
||||
/>
|
||||
<div
|
||||
v-if="campaign.trigger_rules.url"
|
||||
class="text-xs text-woot-600 mb-1 text-truncate w-1/4"
|
||||
>
|
||||
{{ campaign.trigger_rules.url }}
|
||||
</div>
|
||||
<div
|
||||
v-if="campaign.scheduled_at"
|
||||
class="text-xs text-slate-700 dark:text-slate-500 mb-1"
|
||||
>
|
||||
{{ messageStamp(new Date(campaign.scheduled_at), 'LLL d, h:mm a') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import UserAvatarWithName from 'dashboard/components/widgets/UserAvatarWithName';
|
||||
import InboxName from 'dashboard/components/widgets/InboxName';
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import timeMixin from 'dashboard/mixins/time';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
UserAvatarWithName,
|
||||
InboxName,
|
||||
},
|
||||
mixins: [messageFormatterMixin, timeMixin],
|
||||
props: {
|
||||
campaign: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
isOngoingType: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
campaignStatus() {
|
||||
if (this.isOngoingType) {
|
||||
return this.campaign.enabled
|
||||
? this.$t('CAMPAIGN.LIST.STATUS.ENABLED')
|
||||
: this.$t('CAMPAIGN.LIST.STATUS.DISABLED');
|
||||
}
|
||||
|
||||
return this.campaign.campaign_status === 'completed'
|
||||
? this.$t('CAMPAIGN.LIST.STATUS.COMPLETED')
|
||||
: this.$t('CAMPAIGN.LIST.STATUS.ACTIVE');
|
||||
},
|
||||
colorScheme() {
|
||||
if (this.isOngoingType) {
|
||||
return this.campaign.enabled ? 'success' : 'secondary';
|
||||
}
|
||||
return this.campaign.campaign_status === 'completed'
|
||||
? 'secondary'
|
||||
: 'success';
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,49 +1,40 @@
|
||||
<template>
|
||||
<section class="campaigns-table-wrap">
|
||||
<empty-state v-if="showEmptyResult" :title="emptyMessage" />
|
||||
<ve-table
|
||||
v-else
|
||||
:columns="columns"
|
||||
scroll-width="190rem"
|
||||
:table-data="tableData"
|
||||
:border-around="true"
|
||||
style="max-width: calc(100vw - 18rem)"
|
||||
/>
|
||||
<div class="flex items-center flex-col">
|
||||
<div v-if="isLoading" class="items-center flex text-base justify-center">
|
||||
<spinner />
|
||||
<spinner color-scheme="primary" />
|
||||
<span>{{ $t('CAMPAIGN.LIST.LOADING_MESSAGE') }}</span>
|
||||
</div>
|
||||
</section>
|
||||
<div v-else class="w-full">
|
||||
<empty-state v-if="showEmptyResult" :title="emptyMessage" />
|
||||
<div v-else class="w-full">
|
||||
<campaign-card
|
||||
v-for="campaign in campaigns"
|
||||
:key="campaign.id"
|
||||
:campaign="campaign"
|
||||
:is-ongoing-type="isOngoingType"
|
||||
@edit="campaign => $emit('edit', campaign)"
|
||||
@delete="campaign => $emit('delete', campaign)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mixin as clickaway } from 'vue-clickaway';
|
||||
import { VeTable } from 'vue-easytable';
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
import Label from 'dashboard/components/ui/Label';
|
||||
import EmptyState from 'dashboard/components/widgets/EmptyState.vue';
|
||||
import WootButton from 'dashboard/components/ui/WootButton.vue';
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import UserAvatarWithName from 'dashboard/components/widgets/UserAvatarWithName';
|
||||
import campaignMixin from 'shared/mixins/campaignMixin';
|
||||
import timeMixin from 'dashboard/mixins/time';
|
||||
import rtlMixin from 'shared/mixins/rtlMixin';
|
||||
import InboxName from 'dashboard/components/widgets/InboxName';
|
||||
import CampaignCard from './CampaignCard.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
EmptyState,
|
||||
Spinner,
|
||||
VeTable,
|
||||
CampaignCard,
|
||||
},
|
||||
|
||||
mixins: [
|
||||
clickaway,
|
||||
timeMixin,
|
||||
campaignMixin,
|
||||
messageFormatterMixin,
|
||||
rtlMixin,
|
||||
],
|
||||
mixins: [clickaway, campaignMixin],
|
||||
|
||||
props: {
|
||||
campaigns: {
|
||||
@@ -84,216 +75,6 @@ export default {
|
||||
? this.$t('CAMPAIGN.ONE_OFF.404')
|
||||
: this.$t('CAMPAIGN.ONE_OFF.INBOXES_NOT_FOUND');
|
||||
},
|
||||
tableData() {
|
||||
if (this.isLoading) {
|
||||
return [];
|
||||
}
|
||||
return this.campaigns.map(item => {
|
||||
return {
|
||||
...item,
|
||||
url: item.trigger_rules.url,
|
||||
timeOnPage: item.trigger_rules.time_on_page,
|
||||
scheduledAt: item.scheduled_at
|
||||
? this.messageStamp(new Date(item.scheduled_at), 'LLL d, h:mm a')
|
||||
: '---',
|
||||
};
|
||||
});
|
||||
},
|
||||
columns() {
|
||||
const visibleToAllTable = [
|
||||
{
|
||||
field: 'title',
|
||||
key: 'title',
|
||||
title: this.$t('CAMPAIGN.LIST.TABLE_HEADER.TITLE'),
|
||||
fixed: 'left',
|
||||
align: this.isRTLView ? 'right' : 'left',
|
||||
renderBodyCell: ({ row }) => (
|
||||
<div class="row--title-block">
|
||||
<h6 class="text-sm m-0 capitalize text-slate-900 dark:text-slate-100 overflow-hidden whitespace-nowrap text-ellipsis">
|
||||
{row.title}
|
||||
</h6>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
field: 'message',
|
||||
key: 'message',
|
||||
title: this.$t('CAMPAIGN.LIST.TABLE_HEADER.MESSAGE'),
|
||||
align: this.isRTLView ? 'right' : 'left',
|
||||
width: 350,
|
||||
renderBodyCell: ({ row }) => {
|
||||
if (row.message) {
|
||||
return (
|
||||
<div class="overflow-hidden whitespace-nowrap text-ellipsis">
|
||||
<span
|
||||
domPropsInnerHTML={this.formatMessage(row.message)}
|
||||
></span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return '';
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'inbox',
|
||||
key: 'inbox',
|
||||
title: this.$t('CAMPAIGN.LIST.TABLE_HEADER.INBOX'),
|
||||
align: this.isRTLView ? 'right' : 'left',
|
||||
renderBodyCell: ({ row }) => {
|
||||
return <InboxName inbox={row.inbox} />;
|
||||
},
|
||||
},
|
||||
];
|
||||
if (this.isOngoingType) {
|
||||
return [
|
||||
...visibleToAllTable,
|
||||
{
|
||||
field: 'enabled',
|
||||
key: 'enabled',
|
||||
title: this.$t('CAMPAIGN.LIST.TABLE_HEADER.STATUS'),
|
||||
align: this.isRTLView ? 'right' : 'left',
|
||||
renderBodyCell: ({ row }) => {
|
||||
const labelText = row.enabled
|
||||
? this.$t('CAMPAIGN.LIST.STATUS.ENABLED')
|
||||
: this.$t('CAMPAIGN.LIST.STATUS.DISABLED');
|
||||
const colorScheme = row.enabled ? 'success' : 'secondary';
|
||||
return <Label title={labelText} colorScheme={colorScheme} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'sender',
|
||||
key: 'sender',
|
||||
title: this.$t('CAMPAIGN.LIST.TABLE_HEADER.SENDER'),
|
||||
align: this.isRTLView ? 'right' : 'left',
|
||||
renderBodyCell: ({ row }) => {
|
||||
if (row.sender) return <UserAvatarWithName user={row.sender} />;
|
||||
return this.$t('CAMPAIGN.LIST.SENDER.BOT');
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'url',
|
||||
key: 'url',
|
||||
title: this.$t('CAMPAIGN.LIST.TABLE_HEADER.URL'),
|
||||
align: this.isRTLView ? 'right' : 'left',
|
||||
renderBodyCell: ({ row }) => (
|
||||
<div class="overflow-hidden whitespace-nowrap text-ellipsis">
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer nofollow"
|
||||
href={row.url}
|
||||
title={row.url}
|
||||
>
|
||||
{row.url}
|
||||
</a>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'timeOnPage',
|
||||
key: 'timeOnPage',
|
||||
title: this.$t('CAMPAIGN.LIST.TABLE_HEADER.TIME_ON_PAGE'),
|
||||
align: this.isRTLView ? 'right' : 'left',
|
||||
},
|
||||
|
||||
{
|
||||
field: 'buttons',
|
||||
key: 'buttons',
|
||||
title: '',
|
||||
align: this.isRTLView ? 'right' : 'left',
|
||||
renderBodyCell: row => (
|
||||
<div class="justify-evenly flex flex-row min-w-[12.5rem]">
|
||||
<WootButton
|
||||
variant="clear"
|
||||
icon="edit"
|
||||
color-scheme="secondary"
|
||||
classNames="grey-btn"
|
||||
onClick={() => this.$emit('on-edit-click', row)}
|
||||
>
|
||||
{this.$t('CAMPAIGN.LIST.BUTTONS.EDIT')}
|
||||
</WootButton>
|
||||
<WootButton
|
||||
variant="link"
|
||||
icon="dismiss-circle"
|
||||
color-scheme="secondary"
|
||||
onClick={() => this.$emit('on-delete-click', row)}
|
||||
>
|
||||
{this.$t('CAMPAIGN.LIST.BUTTONS.DELETE')}
|
||||
</WootButton>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
return [
|
||||
...visibleToAllTable,
|
||||
{
|
||||
field: 'campaign_status',
|
||||
key: 'campaign_status',
|
||||
title: this.$t('CAMPAIGN.LIST.TABLE_HEADER.STATUS'),
|
||||
align: this.isRTLView ? 'right' : 'left',
|
||||
renderBodyCell: ({ row }) => {
|
||||
const labelText =
|
||||
row.campaign_status === 'completed'
|
||||
? this.$t('CAMPAIGN.LIST.STATUS.COMPLETED')
|
||||
: this.$t('CAMPAIGN.LIST.STATUS.ACTIVE');
|
||||
const colorScheme =
|
||||
row.campaign_status === 'completed' ? 'secondary' : 'success';
|
||||
return <Label title={labelText} colorScheme={colorScheme} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'scheduledAt',
|
||||
key: 'scheduledAt',
|
||||
title: this.$t('CAMPAIGN.LIST.TABLE_HEADER.SCHEDULED_AT'),
|
||||
align: this.isRTLView ? 'right' : 'left',
|
||||
},
|
||||
{
|
||||
field: 'buttons',
|
||||
key: 'buttons',
|
||||
title: '',
|
||||
align: this.isRTLView ? 'right' : 'left',
|
||||
renderBodyCell: row => (
|
||||
<div class="justify-evenly flex flex-row min-w-[12.5rem]">
|
||||
<WootButton
|
||||
variant="link"
|
||||
icon="dismiss-circle"
|
||||
color-scheme="secondary"
|
||||
onClick={() => this.$emit('on-delete-click', row)}
|
||||
>
|
||||
{this.$t('CAMPAIGN.LIST.BUTTONS.DELETE')}
|
||||
</WootButton>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.campaigns-table-wrap::v-deep {
|
||||
.ve-table {
|
||||
@apply pb-12;
|
||||
|
||||
thead.ve-table-header .ve-table-header-tr .ve-table-header-th {
|
||||
@apply text-xs py-2 px-5;
|
||||
}
|
||||
tbody.ve-table-body .ve-table-body-tr .ve-table-body-td {
|
||||
@apply py-3 px-5;
|
||||
|
||||
.inbox--name {
|
||||
@apply m-0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.row--title-block {
|
||||
@apply items-center flex text-left;
|
||||
}
|
||||
.label {
|
||||
@apply py-1 px-2;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<label class="editor-wrap">
|
||||
{{ $t('CAMPAIGN.ADD.FORM.MESSAGE.LABEL') }}
|
||||
<woot-message-editor
|
||||
v-model.trim="message"
|
||||
v-model="message"
|
||||
class="message-editor"
|
||||
:is-format-mode="true"
|
||||
:class="{ editor_warning: $v.message.$error }"
|
||||
|
||||
+5
-3
@@ -85,8 +85,8 @@ import {
|
||||
} from '../helpers/businessHour';
|
||||
|
||||
const DEFAULT_TIMEZONE = {
|
||||
label: 'America/Los_Angeles',
|
||||
key: 'America/Los_Angeles',
|
||||
label: 'Pacific Time (US & Canada) (GMT-07:00)',
|
||||
value: 'America/Los_Angeles',
|
||||
};
|
||||
|
||||
export default {
|
||||
@@ -183,7 +183,9 @@ export default {
|
||||
await this.$store.dispatch('inboxes/updateInbox', payload);
|
||||
this.showAlert(this.$t('INBOX_MGMT.EDIT.API.SUCCESS_MESSAGE'));
|
||||
} catch (error) {
|
||||
this.showAlert(this.$t('INBOX_MGMT.EDIT.API.SUCCESS_MESSAGE'));
|
||||
this.showAlert(
|
||||
error.message || this.$t('INBOX_MGMT.EDIT.API.ERROR_MESSAGE')
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
<template>
|
||||
<div class="flex">
|
||||
<div class="flex h-[6.25rem] w-[6.25rem]">
|
||||
<div class="flex flex-col md:flex-row items-start md:items-center">
|
||||
<div class="flex items-center justify-center m-0 mx-4 flex-1">
|
||||
<img
|
||||
:src="'/dashboard/images/integrations/' + integrationLogo"
|
||||
class="max-w-full p-6"
|
||||
class="p-2 h-16 w-16 mr-4"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col justify-center m-0 mx-4 flex-1">
|
||||
<h3 class="text-xl text-slate-800 dark:text-slate-100">
|
||||
{{ integrationName }}
|
||||
</h3>
|
||||
<p>
|
||||
{{
|
||||
useInstallationName(
|
||||
integrationDescription,
|
||||
globalConfig.installationName
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
<div>
|
||||
<h3 class="text-xl text-slate-800 dark:text-slate-100">
|
||||
{{ integrationName }}
|
||||
</h3>
|
||||
<p>
|
||||
{{
|
||||
useInstallationName(
|
||||
integrationDescription,
|
||||
globalConfig.installationName
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-center items-center mb-0 w-[15%]">
|
||||
<router-link
|
||||
@@ -29,13 +29,13 @@
|
||||
>
|
||||
<div v-if="integrationEnabled">
|
||||
<div v-if="integrationAction === 'disconnect'">
|
||||
<div @click="openDeletePopup()">
|
||||
<div @click="openDeletePopup">
|
||||
<woot-submit-button
|
||||
:button-text="
|
||||
$t('INTEGRATION_SETTINGS.WEBHOOK.DELETE.BUTTON_TEXT')
|
||||
actionButtonText ||
|
||||
$t('INTEGRATION_SETTINGS.WEBHOOK.DELETE.BUTTON_TEXT')
|
||||
"
|
||||
icon-class="dismiss-circle"
|
||||
button-class="nice alert"
|
||||
button-class="smooth alert"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -56,8 +56,14 @@
|
||||
:show.sync="showDeleteConfirmationPopup"
|
||||
:on-close="closeDeletePopup"
|
||||
:on-confirm="confirmDeletion"
|
||||
:title="$t('INTEGRATION_SETTINGS.WEBHOOK.DELETE.CONFIRM.TITLE')"
|
||||
:message="$t('INTEGRATION_SETTINGS.WEBHOOK.DELETE.CONFIRM.MESSAGE')"
|
||||
:title="
|
||||
deleteConfirmationText.title ||
|
||||
$t('INTEGRATION_SETTINGS.WEBHOOK.DELETE.CONFIRM.TITLE')
|
||||
"
|
||||
:message="
|
||||
deleteConfirmationText.message ||
|
||||
$t('INTEGRATION_SETTINGS.WEBHOOK.DELETE.CONFIRM.MESSAGE')
|
||||
"
|
||||
:confirm-text="$t('INTEGRATION_SETTINGS.WEBHOOK.DELETE.CONFIRM.YES')"
|
||||
:reject-text="$t('INTEGRATION_SETTINGS.WEBHOOK.DELETE.CONFIRM.NO')"
|
||||
/>
|
||||
@@ -81,6 +87,8 @@ export default {
|
||||
integrationDescription: { type: String, default: '' },
|
||||
integrationEnabled: { type: Boolean, default: false },
|
||||
integrationAction: { type: String, default: '' },
|
||||
actionButtonText: { type: String, default: '' },
|
||||
deleteConfirmationText: { type: Object, default: () => ({}) },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
@@ -65,7 +65,7 @@ export default {
|
||||
}),
|
||||
},
|
||||
mounted() {
|
||||
this.intializeSlackIntegration();
|
||||
this.fetchIntegrations();
|
||||
},
|
||||
methods: {
|
||||
integrationAction() {
|
||||
@@ -74,13 +74,8 @@ export default {
|
||||
}
|
||||
return this.integration.action;
|
||||
},
|
||||
async intializeSlackIntegration() {
|
||||
async fetchIntegrations() {
|
||||
await this.$store.dispatch('integrations/get', this.integrationId);
|
||||
if (this.code) {
|
||||
await this.$store.dispatch('integrations/connectSlack', this.code);
|
||||
// we are clearing code from the path as subsequent request would throw error
|
||||
this.$router.replace(this.$route.path);
|
||||
}
|
||||
this.integrationLoaded = true;
|
||||
},
|
||||
},
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="integrationLoaded && !uiFlags.isCreatingSlack"
|
||||
class="flex flex-col flex-1 overflow-auto"
|
||||
>
|
||||
<div
|
||||
class="bg-white dark:bg-slate-800 border-b border-solid border-slate-75 dark:border-slate-700/50 rounded-sm p-4"
|
||||
>
|
||||
<integration
|
||||
:integration-id="integration.id"
|
||||
:integration-logo="integration.logo"
|
||||
:integration-name="integration.name"
|
||||
:integration-description="integration.description"
|
||||
:integration-enabled="integration.enabled"
|
||||
:integration-action="integrationAction"
|
||||
:action-button-text="$t('INTEGRATION_SETTINGS.SLACK.DELETE')"
|
||||
:delete-confirmation-text="{
|
||||
title: $t('INTEGRATION_SETTINGS.SLACK.DELETE_CONFIRMATION.TITLE'),
|
||||
message: $t('INTEGRATION_SETTINGS.SLACK.DELETE_CONFIRMATION.MESSAGE'),
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="areHooksAvailable" class="p-6 flex-1">
|
||||
<select-channel-warning v-if="!isIntegrationHookEnabled" />
|
||||
<slack-integration-help-text
|
||||
:selected-channel-name="selectedChannelName"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="flex flex-1 items-center justify-center">
|
||||
<spinner size="" color-scheme="primary" />
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
|
||||
import Integration from './Integration.vue';
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import SelectChannelWarning from './Slack/SelectChannelWarning.vue';
|
||||
import SlackIntegrationHelpText from './Slack/SlackIntegrationHelpText.vue';
|
||||
import Spinner from 'shared/components/Spinner';
|
||||
export default {
|
||||
components: {
|
||||
Spinner,
|
||||
Integration,
|
||||
SelectChannelWarning,
|
||||
SlackIntegrationHelpText,
|
||||
},
|
||||
mixins: [globalConfigMixin, messageFormatterMixin],
|
||||
props: {
|
||||
code: { type: String, default: '' },
|
||||
},
|
||||
data() {
|
||||
return { integrationLoaded: false };
|
||||
},
|
||||
computed: {
|
||||
integration() {
|
||||
return this.$store.getters['integrations/getIntegration']('slack');
|
||||
},
|
||||
areHooksAvailable() {
|
||||
const { hooks = [] } = this.integration || {};
|
||||
return !!hooks.length;
|
||||
},
|
||||
isIntegrationHookEnabled() {
|
||||
const { hooks = [] } = this.integration || {};
|
||||
const [hook = {}] = hooks;
|
||||
return hook.status || false;
|
||||
},
|
||||
selectedChannelName() {
|
||||
const { hooks = [] } = this.integration || {};
|
||||
const [hook = {}] = hooks;
|
||||
if (hook.status) {
|
||||
const { settings: { channel_name: channelName = '' } = {} } = hook;
|
||||
return channelName || 'customer-conversations';
|
||||
}
|
||||
return this.$t('INTEGRATION_SETTINGS.SLACK.HELP_TEXT.SELECTED');
|
||||
},
|
||||
...mapGetters({
|
||||
currentUser: 'getCurrentUser',
|
||||
globalConfig: 'globalConfig/get',
|
||||
accountId: 'getCurrentAccountId',
|
||||
uiFlags: 'integrations/getUIFlags',
|
||||
}),
|
||||
|
||||
integrationAction() {
|
||||
if (this.integration.enabled) {
|
||||
return 'disconnect';
|
||||
}
|
||||
return this.integration.action;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.intializeSlackIntegration();
|
||||
},
|
||||
methods: {
|
||||
async intializeSlackIntegration() {
|
||||
await this.$store.dispatch('integrations/get', 'slack');
|
||||
if (this.code) {
|
||||
await this.$store.dispatch('integrations/connectSlack', this.code);
|
||||
// Clear the query param `code` from the URL as the
|
||||
// subsequent reloads would result in an error
|
||||
this.$router.replace(this.$route.path);
|
||||
}
|
||||
this.integrationLoaded = true;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
<template>
|
||||
<div
|
||||
class="rounded-md bg-yellow-50 border border-yellow-200 dark:border-slate-700 dark:bg-slate-800 px-6 py-4 mb-4"
|
||||
>
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0 mt-0.5">
|
||||
<fluent-icon
|
||||
icon="alert"
|
||||
class="text-yellow-500 dark:text-yellow-400"
|
||||
size="24"
|
||||
/>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p
|
||||
class="text-base font-semibold text-yellow-900 dark:text-yellow-500 mb-1"
|
||||
>
|
||||
{{
|
||||
$t('INTEGRATION_SETTINGS.SLACK.SELECT_CHANNEL.ATTENTION_REQUIRED')
|
||||
}}
|
||||
</p>
|
||||
<div class="text-sm text-yellow-800 dark:text-yellow-600 mt-2">
|
||||
<p
|
||||
v-dompurify-html="
|
||||
formatMessage(
|
||||
useInstallationName(
|
||||
$t('INTEGRATION_SETTINGS.SLACK.SELECT_CHANNEL.DESCRIPTION'),
|
||||
globalConfig.installationName
|
||||
),
|
||||
false
|
||||
)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-8 mt-2">
|
||||
<woot-submit-button
|
||||
v-if="!availableChannels.length"
|
||||
button-class="smooth small warning"
|
||||
:loading="uiFlags.isFetchingSlackChannels"
|
||||
:button-text="
|
||||
$t('INTEGRATION_SETTINGS.SLACK.SELECT_CHANNEL.BUTTON_TEXT')
|
||||
"
|
||||
spinner-class="warning"
|
||||
@click="fetchChannels"
|
||||
/>
|
||||
<div v-else class="inline-flex">
|
||||
<select
|
||||
v-model="selectedChannelId"
|
||||
class="h-8 border-yellow-300 border mr-4 text-xs leading-4 py-1"
|
||||
>
|
||||
<option value="">
|
||||
{{ $t('INTEGRATION_SETTINGS.SLACK.SELECT_CHANNEL.OPTION_LABEL') }}
|
||||
</option>
|
||||
<option
|
||||
v-for="channel in availableChannels"
|
||||
:key="channel.id"
|
||||
:value="channel.id"
|
||||
>
|
||||
#{{ channel.name }}
|
||||
</option>
|
||||
</select>
|
||||
<woot-submit-button
|
||||
button-class="smooth small success"
|
||||
:button-text="$t('INTEGRATION_SETTINGS.SLACK.SELECT_CHANNEL.UPDATE')"
|
||||
spinner-class="success"
|
||||
:loading="uiFlags.isUpdatingSlack"
|
||||
@click="updateIntegration"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
|
||||
export default {
|
||||
mixins: [alertMixin, globalConfigMixin, messageFormatterMixin],
|
||||
data() {
|
||||
return { selectedChannelId: '', availableChannels: [] };
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
globalConfig: 'globalConfig/get',
|
||||
uiFlags: 'integrations/getUIFlags',
|
||||
}),
|
||||
},
|
||||
methods: {
|
||||
async fetchChannels() {
|
||||
try {
|
||||
this.availableChannels = await this.$store.dispatch(
|
||||
'integrations/listAllSlackChannels'
|
||||
);
|
||||
this.availableChannels.sort((c1, c2) => c1.name - c2.name);
|
||||
} catch {
|
||||
this.$t('INTEGRATION_SETTINGS.SLACK.FAILED_TO_FETCH_CHANNELS');
|
||||
this.availableChannels = [];
|
||||
}
|
||||
},
|
||||
async updateIntegration() {
|
||||
try {
|
||||
await this.$store.dispatch('integrations/updateSlack', {
|
||||
referenceId: this.selectedChannelId,
|
||||
});
|
||||
this.showAlert(this.$t('INTEGRATION_SETTINGS.SLACK.UPDATE_SUCCESS'));
|
||||
} catch (error) {
|
||||
this.showAlert(
|
||||
error.message || 'INTEGRATION_SETTINGS.SLACK.UPDATE_ERROR'
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex-1 w-full p-6 bg-white rounded-md border border-slate-50 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-200 "
|
||||
>
|
||||
<div class="prose-lg max-w-5xl">
|
||||
<h5 class="dark:text-slate-100">
|
||||
{{ $t('INTEGRATION_SETTINGS.SLACK.HELP_TEXT.TITLE') }}
|
||||
</h5>
|
||||
<p>
|
||||
<span
|
||||
v-dompurify-html="
|
||||
formatMessage(
|
||||
$t('INTEGRATION_SETTINGS.SLACK.HELP_TEXT.BODY', {
|
||||
selectedChannelName: selectedChannelName,
|
||||
}),
|
||||
false
|
||||
)
|
||||
"
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
export default {
|
||||
mixins: [messageFormatterMixin],
|
||||
props: {
|
||||
selectedChannelName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -3,6 +3,7 @@ import SettingsContent from '../Wrapper';
|
||||
import Webhook from './Webhooks/Index';
|
||||
import DashboardApps from './DashboardApps/Index';
|
||||
import ShowIntegration from './ShowIntegration';
|
||||
import Slack from './Slack';
|
||||
import { frontendURL } from '../../../../helper/URLHelper';
|
||||
|
||||
export default {
|
||||
@@ -42,6 +43,13 @@ export default {
|
||||
name: 'settings_integrations_dashboard_apps',
|
||||
roles: ['administrator'],
|
||||
},
|
||||
{
|
||||
path: 'slack',
|
||||
name: 'settings_integrations_slack',
|
||||
component: Slack,
|
||||
roles: ['administrator'],
|
||||
props: route => ({ code: route.query.code }),
|
||||
},
|
||||
{
|
||||
path: ':integration_id',
|
||||
name: 'settings_integrations_integration',
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="flex-1 overflow-auto p-4">
|
||||
<router-link
|
||||
:to="addAccountScoping('settings/macros/new')"
|
||||
class="button success button--fixed-top"
|
||||
class="button success button--fixed-top flex gap-1"
|
||||
>
|
||||
<fluent-icon icon="add-circle" />
|
||||
<span class="button__content">
|
||||
|
||||
@@ -116,7 +116,7 @@ export default {
|
||||
|
||||
&.has-error {
|
||||
animation: shake 0.3s ease-in-out 0s 2;
|
||||
@apply bg-red-50 dark:bg-red-100;
|
||||
@apply bg-red-50 dark:bg-red-800;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user