Compare commits

..
Author SHA1 Message Date
Shivam Mishra 79a76bc9df refactor: use MutexApplicationJob 2023-08-23 11:59:24 +07:00
Shivam Mishra 77b1804eef feat: create mutex application job 2023-08-23 11:59:06 +07:00
Shivam Mishra a962308a23 feat: add mutex for slack send job 2023-08-23 11:59:06 +07:00
c1c3a62412 fix: Improve reply prompt (#7588)
Co-authored-by: Sojan Jose <sojan@chatwoot.com>
2023-08-09 10:25:37 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
b5dec067a1 chore(deps): bump commonmarker from 0.23.9 to 0.23.10 (#7695)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-08 14:25:42 -07:00
Sivin VargheseandGitHub f96f166196 fix: Default timezone is not working (#7692) 2023-08-08 21:30:29 +05:30
Sivin VargheseandGitHub 9ed701065e fix: Dark mode color fixes (#7693) 2023-08-08 21:07:15 +05:30
Vishnu NarayananandGitHub 180befbd42 feat: add gh action to check logging percentage (#7688) 2023-08-08 15:51:46 +05:30
Pranav Raj SandGitHub 4baa4363f4 fix: Update unread_count being 0 if agent has not seen the conversation (#7690) 2023-08-07 18:50:31 -07:00
Pranav Raj SandGitHub 1c8f41f387 fix: Rename logo-dark -> logo_dark to match the installation config (#7689)
Fix: #7676
2023-08-07 14:30:16 -07:00
7f0ca8b15d fix: Security upgrade rack-mini-profiler from 3.1.0 to 3.1.1 [Snyk] (#7657)
Co-authored-by: snyk-bot <snyk-bot@snyk.io>
2023-08-07 14:09:28 -07:00
Sojan JoseandGitHub 4c75bb68f4 chore: Fix OpenAI sentry errors (#7687)
- add check for nil condition
- add logs for openAI
2023-08-07 14:08:26 -07:00
Vishnu NarayananandGitHub ed82eb2932 refactor: remove beta label from automation, bots and macros (#7683)
Fixes: https://linear.app/chatwoot/issue/CW-2334/remove-beta-label-from-everything
2023-08-07 11:15:24 -07:00
Sivin VargheseandGitHub b62166a18a fix: Cannot read properties of undefined (#7682) 2023-08-07 20:49:13 +05:30
bf2b75b281 feat: Add message timestamp to the conversation transcript emails (#7648)
Co-authored-by: Cristian Duta <Cristian.Duta@ti8m.ch>
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2023-08-04 17:21:14 -07:00
c084ad5a68 [Snyk] Security upgrade lograge from 0.12.0 to 0.13.0 (#7635)
Co-authored-by: snyk-bot <snyk-bot@snyk.io>
2023-08-04 16:56:11 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Sojan Jose
dd3e658f19 chore(deps): bump semver from 7.3.5 to 7.5.2 (#7383)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2023-08-04 16:53:30 -07:00
453d7ad9fc fix: Update options interactive messages formatting in web widget (#7437)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2023-08-04 16:52:32 -07:00
Pranav Raj SandGitHub 47b6967dad fix: Update account data seeder to avoid invalid information (#7673)
- The message sender was incorrect. Incoming messages were created under the contact's name, and outgoing messages were created under the user's name instead of the reverse.
- The seed user's email address was incorrect in the message data.
- The Sendmail configuration overrode the Letter Opener config which made it difficult to test the email.
- This PR also fixes an ESLint lint issue on develop.
2023-08-04 16:32:54 -07:00
Sojan JoseandGitHub d06cf27800 chore: Specs for pageCrawler Service (#7632) 2023-08-04 15:44:51 -07:00
Sivin VargheseandGitHub 7f6a3b781f fix: Links are not readable in private notes dark mode (#7671) 2023-08-05 00:10:10 +05:30
fde2b180fe feat: Update design for campaign (#7668)
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2023-08-04 11:39:59 -07:00
Sivin VargheseandGitHub 2a1662c781 fix: Edit custom views folder (#7670) 2023-08-04 13:54:21 +05:30
10d6e9551d feat: Add the option to toggle the dark/light color-scheme (#7662)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2023-08-03 12:21:45 -07:00
Sivin VargheseandGitHub 69d46f278a feat: Dark mode improvements (#7658) 2023-08-03 12:05:22 -07:00
Muhsin KelothandGitHub 0921a7c817 fix: Enable cmd bar actions for all conversation routes (#7664) 2023-08-03 11:40:03 -07:00
Muhsin KelothandGitHub 2a73f124ca feat: Add analytics event for command bar usage (#7659) 2023-08-02 13:45:26 +05:30
Sojan JoseandGitHub ef33c60652 chore: Bump up node version (#7653)
Bump node version to 16.20.1.
 
ref: #6629
2023-08-01 12:48:02 -07:00
Pranav Raj SandGitHub 3a547de909 fix: Update the default status of the hooks to enabled (#7652)
In the recent Slack integration update, we made changes to how hooks are handled. Now, hooks require an additional check to be enabled. By default, new hooks are created in a disabled state, which might lead to issues with the hooks not being executed as expected.

This migration updates the status attribute of hooks to have 'enabled' as the default value, ensuring that new hooks are enabled by default and can function properly.
2023-08-01 12:14:47 -07:00
Shivam MishraandGitHub 62e9fc1bc5 feat: update color palette [CW-2293] (#7617) 2023-08-01 21:34:10 +05:30
Nithin David ThomasandGitHub e052a061f4 feat: Show popular articles on widget home (#7604) 2023-08-01 21:32:44 +05:30
LiamandGitHub 9efadf8804 fix: Update permitted_attributes to support the latest version of administrate (#7644)
This fixes an issue where permitted_attributes throws an error of ArgumentError (wrong number of arguments (given 1, expected 0)):

This is because we now bring in the latest version of administrate and administrate changes the way permitted_attributes works; it now passes an action parameter to permitted_attributes. This means we need to update permitted_attributes to accept that new parameter.

Fixes: #7592
2023-07-31 11:53:44 -07:00
Muhsin KelothandGitHub c8a40932cc chore: Add June event for AI reply suggest dismissal (#7601) 2023-07-31 13:20:47 +05:30
Pranav Raj SandGitHub 9ddd428935 feat: Update the slack integration-flow to allow users to select the channel (#7637) 2023-07-28 14:50:30 -07:00
Sojan JoseandGitHub 4d8ba0148c fix: Use Display name for campaigns (#7631)
fixes: #4299
2023-07-27 17:58:19 +03:00
65c4ad70e1 refactor: improve logging for channel callback controllers (#7555)
Co-authored-by: Tejaswini Chile <tejaswini@chatwoot.com>
2023-07-27 17:10:39 +05:30
9636478c2a chore: Improve logging for ip look up and audit logs [CW-2145] (#7512)
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2023-07-27 16:48:30 +05:30
Nic HippenmeyerandGitHub 65190422c4 fix: Rename unread_since scope (#7267)
- This renames the unread_since scope to created_since, which more accurately describes what the scope returns.
- The EXTRACT(EPOCH FROM created_at) > (?) clause was also simplified and rewritten as created_at > ?, which is equivalent:
2023-07-27 12:29:21 +03:00
Jordan BroughandGitHub a7bc855486 refactor: Remove deprecated "belongs_to" relationships in Message (#7434)
- Remove the deprecated columns in message model
2023-07-27 11:41:34 +03:00
Shivam MishraandGitHub ff97536095 feat(perf): contact page loading speed (#7629) 2023-07-27 13:32:22 +05:30
Sivin VargheseandGitHub fcf0ba12d0 feat: Dark Mode (PR -12) (#7590) 2023-07-27 12:28:06 +05:30
Shivam MishraandGitHub 2cc82af344 fix: apply event name (#7628) 2023-07-27 12:11:57 +05:30
c00855fce4 fix: Skip sentiment analysis for blank text (#7580)
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2023-07-27 12:11:48 +05:30
2d07577731 fix: [Snyk] Security upgrade rack-attack from 6.6.1 to 6.7.0 (#7625)
Co-authored-by: snyk-bot <snyk-bot@snyk.io>
2023-07-27 09:06:56 +03:00
BernieandGitHub 4c98006837 fix: Fix spelling mistakes in the copy (#7627) 2023-07-26 21:48:28 -07:00
Nithin David ThomasandGitHub 89e09857af feat: Add store and API to support articles in widget (#7616) 2023-07-26 14:08:27 -07:00
Shivam MishraandGitHub 12c338364e fix: sentry issues [CW-2222][CW-2232] (#7591) 2023-07-26 23:37:37 +05:30
Sojan JoseandGitHub 846f520ad2 feat: toggle typing and update last seen endpoints for client apis (#7621)
- Add toggle_typing endpoint for client APIs
- Adds update last seen endpoint for client APIs 

Fixes: #7581
2023-07-26 20:40:48 +03:00
Sojan JoseandGitHub 6cbe1ed911 chore: Disable message hooks for conversations without incoming message (#7620)
When using client APIs to create conversations and auto-assignment is turned on, welcome messages were getting triggered. This PR disable the behaviour and ensure template hooks are triggered only if there are incoming messages present. 

Fixes: https://linear.app/chatwoot/issue/CW-2187
2023-07-26 20:38:49 +03:00
LiamandGitHub 6c1ee4d965 fix: Contacts page sort by created_at [CW-2262] (#7584)
Fixes the contacts page sort by created_at.
fixes: #7577
2023-07-26 18:37:30 +03:00
OMAR.AandGitHub 1d8341504a fix: contacts count on filter (#7446)
- Fixes the wrong count shown during the contact filter when contact has multiple labels associated.
2023-07-26 17:31:04 +03:00
Shivam MishraandGitHub f8ae6cd95c fix: no method error when conversation is nil (#7566) 2023-07-26 17:40:41 +05:30
LiamandGitHub 63dd2c5a93 fix: Avoid profile options menu overlapping with message tabs (#7615) 2023-07-25 21:55:57 -07:00
Shivam MishraandSojan 1c47478104 fix: undefined method update_cache_key for nil:NilClass (#7568) 2023-07-25 13:00:34 +03:00
Shivam MishraandSojan 39f14bebbd fix: undefined method first for nil:NilClass (#7567)
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2023-07-25 13:00:21 +03:00
LiamandGitHub 4b27fdf4db fix: Increase the out of office message length to 10_000 (#7583) 2023-07-24 20:29:04 -07:00
Pranav Raj SandGitHub a6a0e78bbe feat: Sort articles based on views (#7599) 2023-07-24 20:27:43 -07:00
703e19304d feat: Components to render articles in widget home (#7596)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2023-07-24 16:45:55 -07:00
Pranav Raj SandGitHub fa7bbdb0b3 feat: Add support for plain design help center layout (#7598) 2023-07-25 05:13:52 +05:30
Nithin David ThomasandGitHub d837065545 chore: Update storybook settings to widget stories (#7595) 2023-07-24 16:28:20 -07:00
2a6f3356c9 chore: Show error message if the AI process APIs fails (#7560)
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
2023-07-24 19:15:34 +05:30
c83105ce4f feat: dismiss label suggestions only for 24 hours (#7579)
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2023-07-24 17:51:09 +05:30
203 changed files with 3028 additions and 1403 deletions
@@ -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
+1 -1
View File
@@ -1 +1 @@
14.17.4
16.20.1
+13 -1
View File
@@ -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
View File
@@ -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);
+3 -3
View File
@@ -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
View File
@@ -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?
+4 -1
View File
@@ -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
+4 -22
View File
@@ -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;
+10 -2
View File
@@ -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,
@@ -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 {
@@ -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 {
@@ -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;
}
@@ -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": {
+4 -25
View File
@@ -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 {
@@ -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>
@@ -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';
@@ -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>
@@ -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,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 }"
@@ -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>
@@ -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>
@@ -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