Compare commits

..

1 commit

Author SHA1 Message Date
Pranav Raj S
ead7a5fc87 feat: Link to a message 2022-11-09 19:11:20 -08:00
831 changed files with 3438 additions and 24619 deletions

View file

@ -60,10 +60,9 @@ MAILER_SENDER_EMAIL=Chatwoot <accounts@chatwoot.com>
#SMTP domain key is set up for HELO checking
SMTP_DOMAIN=chatwoot.com
# Set the value to "mailhog" if using docker-compose for development environments,
# the default value is set "mailhog" and is used by docker-compose for development environments,
# Set the value as "localhost" or your SMTP address in other environments
# If SMTP_ADDRESS is empty, Chatwoot would try to use sendmail(postfix)
SMTP_ADDRESS=
SMTP_ADDRESS=mailhog
SMTP_PORT=1025
SMTP_USERNAME=
SMTP_PASSWORD=

2
.gitignore vendored
View file

@ -60,5 +60,3 @@ test/cypress/videos/*
/config/master.key
/config/*.enc
.vscode/settings.json

View file

@ -16,6 +16,7 @@ Metrics/ClassLength:
- 'app/models/message.rb'
- 'app/builders/messages/facebook/message_builder.rb'
- 'app/controllers/api/v1/accounts/contacts_controller.rb'
- 'app/controllers/api/v1/accounts/conversations_controller.rb'
- 'app/listeners/action_cable_listener.rb'
- 'app/models/conversation.rb'
RSpec/ExampleLength:

View file

@ -1 +0,0 @@
{}

View file

@ -4,7 +4,7 @@ ruby '3.0.4'
##-- base gems for rails --##
gem 'rack-cors', require: 'rack/cors'
gem 'rails', '~> 6.1', '>= 6.1.6.1'
gem 'rails', '~>6.1'
# Reduces boot times through caching; required in config/boot.rb
gem 'bootsnap', require: false
@ -56,7 +56,7 @@ gem 'activerecord-import'
gem 'dotenv-rails'
gem 'foreman'
gem 'puma'
gem 'webpacker', '~> 5.4', '>= 5.4.3'
gem 'webpacker', '~> 5.x'
# metrics on heroku
gem 'barnes'
@ -94,7 +94,7 @@ gem 'ddtrace'
gem 'elastic-apm'
gem 'newrelic_rpm'
gem 'scout_apm'
gem 'sentry-rails', '~> 5.3', '>= 5.3.1'
gem 'sentry-rails', '~> 5.3'
gem 'sentry-ruby', '~> 5.3'
gem 'sentry-sidekiq', '~> 5.3'
@ -175,7 +175,7 @@ group :development, :test do
gem 'mock_redis'
gem 'pry-rails'
gem 'rspec_junit_formatter'
gem 'rspec-rails', '~> 5.0.3'
gem 'rspec-rails', '~> 5.0.0'
gem 'rubocop', require: false
gem 'rubocop-performance', require: false
gem 'rubocop-rails', require: false

View file

@ -398,7 +398,7 @@ GEM
llhttp-ffi (0.4.0)
ffi-compiler (~> 1.0)
rake (~> 13.0)
loofah (2.19.1)
loofah (2.18.0)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
mail (2.7.1)
@ -427,14 +427,14 @@ GEM
netrc (0.11.0)
newrelic_rpm (8.9.0)
nio4r (2.5.8)
nokogiri (1.13.10)
nokogiri (1.13.9)
mini_portile2 (~> 2.8.0)
racc (~> 1.4)
nokogiri (1.13.10-arm64-darwin)
nokogiri (1.13.9-arm64-darwin)
racc (~> 1.4)
nokogiri (1.13.10-x86_64-darwin)
nokogiri (1.13.9-x86_64-darwin)
racc (~> 1.4)
nokogiri (1.13.10-x86_64-linux)
nokogiri (1.13.9-x86_64-linux)
racc (~> 1.4)
oauth (0.5.10)
orm_adapter (0.5.0)
@ -459,7 +459,7 @@ GEM
pundit (2.2.0)
activesupport (>= 3.0.0)
raabro (1.4.0)
racc (1.6.1)
racc (1.6.0)
rack (2.2.4)
rack-attack (6.6.1)
rack (>= 1.0, < 3)
@ -488,8 +488,8 @@ GEM
rails-dom-testing (2.0.3)
activesupport (>= 4.2.0)
nokogiri (>= 1.6)
rails-html-sanitizer (1.4.4)
loofah (~> 2.19, >= 2.19.1)
rails-html-sanitizer (1.4.3)
loofah (~> 2.3)
railties (6.1.6.1)
actionpack (= 6.1.6.1)
activesupport (= 6.1.6.1)
@ -765,12 +765,12 @@ DEPENDENCIES
rack-attack
rack-cors
rack-timeout
rails (~> 6.1, >= 6.1.6.1)
rails (~> 6.1)
redis
redis-namespace
responders
rest-client
rspec-rails (~> 5.0.3)
rspec-rails (~> 5.0.0)
rspec_junit_formatter
rubocop
rubocop-performance
@ -778,7 +778,7 @@ DEPENDENCIES
rubocop-rspec
scout_apm
seed_dump
sentry-rails (~> 5.3, >= 5.3.1)
sentry-rails (~> 5.3)
sentry-ruby (~> 5.3)
sentry-sidekiq (~> 5.3)
shoulda-matchers
@ -799,7 +799,7 @@ DEPENDENCIES
valid_email2
web-console
webmock
webpacker (~> 5.4, >= 5.4.3)
webpacker (~> 5.x)
webpush
wisper (= 2.0.0)
working_hours

View file

@ -41,24 +41,16 @@
"formation": {
"web": {
"quantity": 1,
"size": "basic"
"size": "FREE"
},
"worker": {
"quantity": 1,
"size": "basic"
"size": "FREE"
}
},
"stack": "heroku-20",
"image": "heroku/ruby",
"addons": [
{
"plan": "heroku-redis:mini"
},
{
"plan": "heroku-postgresql:mini"
}
],
"stack": "heroku-20",
"addons": [ "heroku-redis", "heroku-postgresql"],
"buildpacks": [
{
"url": "heroku/ruby"

View file

@ -1,40 +0,0 @@
class ConversationBuilder
pattr_initialize [:params!, :contact_inbox!]
def perform
look_up_exising_conversation || create_new_conversation
end
private
def look_up_exising_conversation
return unless @contact_inbox.inbox.lock_to_single_conversation?
@contact_inbox.conversations.last
end
def create_new_conversation
::Conversation.create!(conversation_params)
end
def conversation_params
additional_attributes = params[:additional_attributes]&.permit! || {}
custom_attributes = params[:custom_attributes]&.permit! || {}
status = params[:status].present? ? { status: params[:status] } : {}
# TODO: temporary fallback for the old bot status in conversation, we will remove after couple of releases
# commenting this out to see if there are any errors, if not we can remove this in subsequent releases
# status = { status: 'pending' } if status[:status] == 'bot'
{
account_id: @contact_inbox.inbox.account_id,
inbox_id: @contact_inbox.inbox_id,
contact_id: @contact_inbox.contact_id,
contact_inbox_id: @contact_inbox.id,
additional_attributes: additional_attributes,
custom_attributes: custom_attributes,
snoozed_until: params[:snoozed_until],
assignee_id: params[:assignee_id],
team_id: params[:team_id]
}.merge(status)
end
end

View file

@ -46,7 +46,6 @@ class Messages::Messenger::MessageBuilder
end
def update_attachment_file_type(attachment)
return if @message.reload.attachments.blank?
return unless attachment.file_type == 'share' || attachment.file_type == 'story_mention'
attachment.file_type = file_type(attachment.file&.content_type)
@ -63,7 +62,6 @@ class Messages::Messenger::MessageBuilder
story_sender = result['from']['username']
message.content_attributes[:story_sender] = story_sender
message.content_attributes[:story_id] = story_id
message.content_attributes[:image_type] = 'story_mention'
message.content = I18n.t('conversations.messages.instagram_story_content', story_sender: story_sender)
message.save!
end
@ -76,7 +74,6 @@ class Messages::Messenger::MessageBuilder
raise
rescue Koala::Facebook::ClientError => e
# The exception occurs when we are trying fetch the deleted story or blocked story.
@message.attachments.destroy_all
@message.update(content: I18n.t('conversations.messages.instagram_deleted_story_content'))
Rails.logger.error e
{}

View file

@ -2,7 +2,7 @@ class Api::V1::Accounts::Contacts::ConversationsController < Api::V1::Accounts::
def index
@conversations = Current.account.conversations.includes(
:assignee, :contact, :inbox, :taggings
).where(inbox_id: inbox_ids, contact_id: @contact.id).order(id: :desc).limit(20)
).where(inbox_id: inbox_ids, contact_id: @contact.id)
end
private

View file

@ -24,7 +24,7 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
def create
ActiveRecord::Base.transaction do
@conversation = ConversationBuilder.new(params: params, contact_inbox: @contact_inbox).perform
@conversation = ::Conversation.create!(conversation_params)
Messages::MessageBuilder.new(Current.user, @conversation, params[:message]).perform if params[:message].present?
end
end
@ -75,13 +75,10 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
end
def update_last_seen
update_last_seen_on_conversation(DateTime.now.utc, assignee?)
end
def unread
last_incoming_message = @conversation.messages.incoming.last
last_seen_at = last_incoming_message.created_at - 1.second if last_incoming_message.present?
update_last_seen_on_conversation(last_seen_at, true)
# rubocop:disable Rails/SkipsModelValidations
@conversation.update_column(:agent_last_seen_at, DateTime.now.utc)
@conversation.update_column(:assignee_last_seen_at, DateTime.now.utc) if assignee?
# rubocop:enable Rails/SkipsModelValidations
end
def custom_attributes
@ -91,18 +88,9 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
private
def update_last_seen_on_conversation(last_seen_at, update_assignee)
# rubocop:disable Rails/SkipsModelValidations
@conversation.update_column(:agent_last_seen_at, last_seen_at)
@conversation.update_column(:assignee_last_seen_at, last_seen_at) if update_assignee.present?
# rubocop:enable Rails/SkipsModelValidations
end
def set_conversation_status
# TODO: temporary fallback for the old bot status in conversation, we will remove after couple of releases
# commenting this out to see if there are any errors, if not we can remove this in subsequent releases
# status = params[:status] == 'bot' ? 'pending' : params[:status]
@conversation.status = params[:status]
status = params[:status] == 'bot' ? 'pending' : params[:status]
@conversation.status = status
@conversation.snoozed_until = parse_date_time(params[:snoozed_until].to_s) if params[:snoozed_until]
end
@ -154,11 +142,31 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
).perform
end
def conversation_params
additional_attributes = params[:additional_attributes]&.permit! || {}
custom_attributes = params[:custom_attributes]&.permit! || {}
status = params[:status].present? ? { status: params[:status] } : {}
# TODO: temporary fallback for the old bot status in conversation, we will remove after couple of releases
status = { status: 'pending' } if status[:status] == 'bot'
{
account_id: Current.account.id,
inbox_id: @contact_inbox.inbox_id,
contact_id: @contact_inbox.contact_id,
contact_inbox_id: @contact_inbox.id,
additional_attributes: additional_attributes,
custom_attributes: custom_attributes,
snoozed_until: params[:snoozed_until],
assignee_id: params[:assignee_id],
team_id: params[:team_id]
}.merge(status)
end
def conversation_finder
@conversation_finder ||= ConversationFinder.new(Current.user, params)
@conversation_finder ||= ConversationFinder.new(current_user, params)
end
def assignee?
@conversation.assignee_id? && Current.user == @conversation.assignee
@conversation.assignee_id? && current_user == @conversation.assignee
end
end

View file

@ -113,8 +113,7 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
def inbox_attributes
[:name, :avatar, :greeting_enabled, :greeting_message, :enable_email_collect, :csat_survey_enabled,
:enable_auto_assignment, :working_hours_enabled, :out_of_office_message, :timezone, :allow_messages_after_resolved,
:lock_to_single_conversation]
:enable_auto_assignment, :working_hours_enabled, :out_of_office_message, :timezone, :allow_messages_after_resolved]
end
def permitted_params(channel_attributes = [])

View file

@ -21,7 +21,6 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
def create
@portal = Current.account.portals.build(portal_params)
@portal.custom_domain = parsed_custom_domain
@portal.save!
process_attached_logo
end
@ -29,7 +28,6 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
def update
ActiveRecord::Base.transaction do
@portal.update!(portal_params) if params[:portal].present?
# @portal.custom_domain = parsed_custom_domain
process_attached_logo
rescue StandardError => e
Rails.logger.error e
@ -75,9 +73,4 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
def set_current_page
@current_page = params[:page] || 1
end
def parsed_custom_domain
domain = URI.parse(@portal.custom_domain)
domain.is_a?(URI::HTTP) ? domain.host : @portal.custom_domain
end
end

View file

@ -18,10 +18,6 @@ class Api::V1::ProfilesController < Api::BaseController
head :ok
end
def auto_offline
@user.account_users.find_by!(account_id: auto_offline_params[:account_id]).update!(auto_offline: auto_offline_params[:auto_offline] || false)
end
def availability
@user.account_users.find_by!(account_id: availability_params[:account_id]).update!(availability: availability_params[:availability])
end
@ -41,10 +37,6 @@ class Api::V1::ProfilesController < Api::BaseController
params.require(:profile).permit(:account_id, :availability)
end
def auto_offline_params
params.require(:profile).permit(:account_id, :auto_offline)
end
def profile_params
params.require(:profile).permit(
:email,

View file

@ -50,9 +50,7 @@ class Api::V1::Widget::BaseController < ApplicationController
end
def contact_name
return if @contact.email.present? || @contact.phone_number.present? || @contact.identifier.present?
permitted_params.dig(:contact, :name) || (contact_email.split('@')[0] if contact_email.present?)
params[:contact][:name] || contact_email.split('@')[0] if contact_email.present?
end
def contact_phone_number

View file

@ -16,7 +16,8 @@ class DashboardController < ActionController::Base
@global_config = GlobalConfig.get(
'LOGO', 'LOGO_THUMBNAIL',
'INSTALLATION_NAME',
'WIDGET_BRAND_URL', 'TERMS_URL',
'WIDGET_BRAND_URL',
'TERMS_URL',
'PRIVACY_URL',
'DISPLAY_MANIFEST',
'CREATE_NEW_ACCOUNT_FROM_DASHBOARD',
@ -24,12 +25,12 @@ class DashboardController < ActionController::Base
'API_CHANNEL_NAME',
'API_CHANNEL_THUMBNAIL',
'ANALYTICS_TOKEN',
'ANALYTICS_HOST',
'DIRECT_UPLOADS_ENABLED',
'HCAPTCHA_SITE_KEY',
'LOGOUT_REDIRECT_LINK',
'DISABLE_USER_PROFILE_UPDATE',
'DEPLOYMENT_ENV',
'CSML_EDITOR_HOST'
'DEPLOYMENT_ENV'
).merge(app_config)
end

View file

@ -1,7 +1,8 @@
class Platform::Api::V1::AccountsController < PlatformController
def create
@resource = Account.create!(account_params)
@resource = Account.new(account_params)
update_resource_features
@resource.save!
@platform_app.platform_app_permissibles.find_or_create_by(permissible: @resource)
end

View file

@ -56,6 +56,7 @@ class ConversationFinder
filter_by_team if @team
filter_by_labels if params[:labels]
filter_by_query if params[:q]
filter_by_reply_status
end
def set_inboxes
@ -75,9 +76,12 @@ class ConversationFinder
end
def find_all_conversations
if params[:conversation_type] == 'mention'
conversation_ids = current_account.mentions.where(user: current_user).pluck(:conversation_id)
@conversations = current_account.conversations.where(id: conversation_ids)
else
@conversations = current_account.conversations.where(inbox_id: @inbox_ids)
filter_by_conversation_type if params[:conversation_type]
@conversations
end
end
def filter_by_assignee_type
@ -92,15 +96,8 @@ class ConversationFinder
@conversations
end
def filter_by_conversation_type
case @params[:conversation_type]
when 'mention'
conversation_ids = current_account.mentions.where(user: current_user).pluck(:conversation_id)
@conversations = @conversations.where(id: conversation_ids)
when 'unattended'
@conversations = @conversations.where(first_reply_created_at: nil)
end
@conversations
def filter_by_reply_status
@conversations = @conversations.where(first_reply_created_at: nil) if params[:reply_status] == 'unattended'
end
def filter_by_query

View file

@ -20,13 +20,22 @@ class MessageFinder
conversation_messages.where.not('private = ? OR message_type = ?', true, 2)
end
def current_messages
if @params[:after].present?
messages.reorder('created_at asc').where('id >= ?', @params[:before].to_i).limit(20)
elsif @params[:before].present?
messages.reorder('created_at desc').where('id < ?', @params[:before].to_i).limit(20).reverse
else
messages.reorder('created_at desc').limit(20).reverse
def messages_in_desc_order
messages.reorder('created_at desc')
end
def current_messages
messages_to_display = messages_in_desc_order
if @params[:before].present?
messages_to_display = messages_to_display.where('id < ?', @params[:before].to_i)
if @params[:after].present? && @params[:after].to_i < @params[:before].to_i - 25
messages_to_display = messages_to_display.where('id >= ?', @params[:after].to_i)
return messages_to_display.reverse
end
end
messages_to_display.limit(20).reverse
end
end

View file

@ -144,12 +144,6 @@ export default {
});
},
updateAutoOffline(accountId, autoOffline = false) {
return axios.post(endPoints('autoOffline').url, {
profile: { account_id: accountId, auto_offline: autoOffline },
});
},
deleteAvatar() {
return axios.delete(endPoints('deleteAvatar').url);
},

View file

@ -16,9 +16,6 @@ const endPoints = {
availabilityUpdate: {
url: '/api/v1/profile/availability',
},
autoOffline: {
url: '/api/v1/profile/auto_offline',
},
logout: {
url: 'auth/sign_out',
},

View file

@ -68,10 +68,6 @@ class ConversationApi extends ApiClient {
return axios.post(`${this.url}/${id}/update_last_seen`);
}
markMessagesUnread({ id }) {
return axios.post(`${this.url}/${id}/unread`);
}
toggleTyping({ conversationId, status, isPrivate }) {
return axios.post(`${this.url}/${conversationId}/toggle_typing_status`, {
typing_status: status,

View file

@ -75,9 +75,9 @@ class MessageApi extends ApiClient {
return axios.delete(`${this.url}/${conversationID}/messages/${messageId}`);
}
getPreviousMessages({ conversationId, before }) {
getPreviousMessages({ conversationId, after, before }) {
return axios.get(`${this.url}/${conversationId}/messages`, {
params: { before },
params: { after, before },
});
}
}

View file

@ -13,16 +13,6 @@ class Inboxes extends ApiClient {
deleteInboxAvatar(inboxId) {
return axios.delete(`${this.url}/${inboxId}/avatar`);
}
getAgentBot(inboxId) {
return axios.get(`${this.url}/${inboxId}/agent_bot`);
}
setAgentBot(inboxId, botId) {
return axios.post(`${this.url}/${inboxId}/set_agent_bot`, {
agent_bot: botId,
});
}
}
export default new Inboxes();

View file

@ -11,8 +11,6 @@ describe('#InboxesAPI', () => {
expect(inboxesAPI).toHaveProperty('update');
expect(inboxesAPI).toHaveProperty('delete');
expect(inboxesAPI).toHaveProperty('getCampaigns');
expect(inboxesAPI).toHaveProperty('getAgentBot');
expect(inboxesAPI).toHaveProperty('setAgentBot');
});
describeWithAPIMock('API calls', context => {
it('#getCampaigns', () => {

View file

@ -1,6 +0,0 @@
/* global axios */
import wootConstants from 'dashboard/constants';
export const getTestimonialContent = () => {
return axios.get(wootConstants.TESTIMONIAL_URL);
};

View file

@ -74,8 +74,8 @@ Tahoma,
Arial,
sans-serif;
$body-antialiased: true;
$global-margin: $space-small;
$global-padding: $space-micro;
$global-margin: $space-one;
$global-padding: $space-one;
$global-weight-normal: normal;
$global-weight-bold: bold;
$global-radius: 0;

View file

@ -20,24 +20,6 @@
@include foundation-everything($flex: true);
@include foundation-prototype-text-utilities;
@include foundation-prototype-text-transformation;
@include foundation-prototype-text-decoration;
@include foundation-prototype-font-styling;
@include foundation-prototype-list-style-type;
@include foundation-prototype-rounded;
@include foundation-prototype-bordered;
@include foundation-prototype-shadow;
@include foundation-prototype-separator;
@include foundation-prototype-overflow;
@include foundation-prototype-display;
@include foundation-prototype-position;
@include foundation-prototype-border-box;
@include foundation-prototype-border-none;
@include foundation-prototype-sizing;
@include foundation-prototype-spacing;
@import 'typography';
@import 'layout';
@import 'animations';

View file

@ -155,20 +155,12 @@ $default-button-height: 4.0rem;
// Sizes
&.tiny {
height: var(--space-medium);
.icon+.button__content {
padding-left: var(--space-micro);
}
}
&.small {
height: var(--space-large);
padding-bottom: var(--space-smaller);
padding-top: var(--space-smaller);
.icon+.button__content {
padding-left: var(--space-smaller);
}
}
&.large {
@ -198,10 +190,6 @@ $default-button-height: 4.0rem;
height: auto;
margin: 0;
padding: 0;
&:hover {
text-decoration: underline;
}
}
}

View file

@ -59,8 +59,12 @@
.hamburger--menu {
cursor: pointer;
display: block;
display: none;
margin-right: $space-normal;
@media screen and (max-width: 1200px) {
display: block;
}
}
.header--icon {

View file

@ -102,7 +102,6 @@
@assign-agent="onAssignAgent"
@update-conversations="onUpdateConversations"
@assign-labels="onAssignLabels"
@assign-team="onAssignTeamsForBulk"
/>
<div
ref="activeConversation"
@ -126,7 +125,6 @@
@assign-label="onAssignLabels"
@update-conversation-status="toggleConversationStatus"
@context-menu-toggle="onContextMenuToggle"
@mark-as-unread="markAsUnread"
/>
<div v-if="chatListLoading" class="text-center">
@ -186,11 +184,6 @@ import {
hasPressedAltAndJKey,
hasPressedAltAndKKey,
} from 'shared/helpers/KeyboardHelpers';
import { conversationListPageURL } from '../helper/URLHelper';
import {
isOnMentionsView,
isOnUnattendedView,
} from '../store/modules/conversations/helpers/actionHelpers';
export default {
components: {
@ -339,15 +332,14 @@ export default {
status: this.activeStatus,
page: this.currentPage + 1,
labels: this.label ? [this.label] : undefined,
teamId: this.teamId || undefined,
conversationType: this.conversationType || undefined,
teamId: this.teamId ? this.teamId : undefined,
conversationType: this.conversationType
? this.conversationType
: undefined,
folders: this.hasActiveFolders ? this.savedFoldersValue : undefined,
};
},
pageTitle() {
if (this.hasAppliedFilters) {
return this.$t('CHAT_LIST.TAB_HEADING');
}
if (this.inbox.name) {
return this.inbox.name;
}
@ -360,9 +352,6 @@ export default {
if (this.conversationType === 'mention') {
return this.$t('CHAT_LIST.MENTION_HEADING');
}
if (this.conversationType === 'unattended') {
return this.$t('CHAT_LIST.UNATTENDED_HEADING');
}
if (this.hasActiveFolders) {
return this.activeFolder.name;
}
@ -442,6 +431,9 @@ export default {
},
methods: {
onApplyFilter(payload) {
if (this.$route.name !== 'home') {
this.$router.push({ name: 'home' });
}
this.resetBulkActions();
this.foldersQuery = filterQueryGenerator(payload);
this.$store.dispatch('conversationPage/reset');
@ -644,35 +636,6 @@ export default {
this.showAlert(this.$t('BULK_ACTION.ASSIGN_FAILED'));
}
},
async markAsUnread(conversationId) {
try {
await this.$store.dispatch('markMessagesUnread', {
id: conversationId,
});
const {
params: { accountId, inbox_id: inboxId, label, teamId },
name,
} = this.$route;
let conversationType = '';
if (isOnMentionsView({ route: { name } })) {
conversationType = 'mention';
} else if (isOnUnattendedView({ route: { name } })) {
conversationType = 'unattended';
}
this.$router.push(
conversationListPageURL({
accountId,
conversationType: conversationType,
customViewId: this.foldersId,
inboxId,
label,
teamId,
})
);
} catch (error) {
// Ignore error
}
},
async onAssignTeam(team, conversationId = null) {
try {
await this.$store.dispatch('assignTeam', {
@ -722,21 +685,6 @@ export default {
this.showAlert(this.$t('BULK_ACTION.LABELS.ASSIGN_FAILED'));
}
},
async onAssignTeamsForBulk(team) {
try {
await this.$store.dispatch('bulkActions/process', {
type: 'Conversation',
ids: this.selectedConversations,
fields: {
team_id: team.id,
},
});
this.selectedConversations = [];
this.showAlert(this.$t('BULK_ACTION.TEAMS.ASSIGN_SUCCESFUL'));
} catch (err) {
this.showAlert(this.$t('BULK_ACTION.TEAMS.ASSIGN_FAILED'));
}
},
async onUpdateConversations(status) {
try {
await this.$store.dispatch('bulkActions/process', {

View file

@ -1,12 +1,7 @@
<template>
<woot-button
size="small"
variant="clear"
color-scheme="secondary"
icon="list"
class="toggle-sidebar"
@click="onMenuItemClick"
/>
<button @click="onMenuItemClick">
<fluent-icon class="hamburger--menu" icon="list" />
</button>
</template>
<script>
@ -21,8 +16,13 @@ export default {
};
</script>
<style scoped lang="scss">
.toggle-sidebar {
margin-right: var(--space-small);
margin-left: var(--space-minus-small);
.hamburger--menu {
cursor: pointer;
display: none;
margin-right: var(--space-normal);
@media screen and (max-width: 1200px) {
display: block;
}
}
</style>

View file

@ -18,35 +18,12 @@
</woot-button>
</woot-dropdown-item>
<woot-dropdown-divider />
<woot-dropdown-item class="auto-offline--toggle">
<div class="info-wrap">
<fluent-icon
v-tooltip.right-start="$t('SIDEBAR.SET_AUTO_OFFLINE.INFO_TEXT')"
icon="info"
size="14"
class="info-icon"
/>
<span class="auto-offline--text">
{{ $t('SIDEBAR.SET_AUTO_OFFLINE.TEXT') }}
</span>
</div>
<woot-switch
size="small"
class="auto-offline--switch"
:value="currentUserAutoOffline"
@input="updateAutoOffline"
/>
</woot-dropdown-item>
<woot-dropdown-divider />
</woot-dropdown-menu>
</template>
<script>
import { mapGetters } from 'vuex';
import { mixin as clickaway } from 'vue-clickaway';
import alertMixin from 'shared/mixins/alertMixin';
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem';
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu';
import WootDropdownHeader from 'shared/components/ui/dropdown/DropdownHeader';
@ -64,7 +41,7 @@ export default {
AvailabilityStatusBadge,
},
mixins: [clickaway, alertMixin],
mixins: [clickaway],
data() {
return {
@ -77,7 +54,6 @@ export default {
...mapGetters({
getCurrentUserAvailability: 'getCurrentUserAvailability',
currentAccountId: 'getCurrentAccountId',
currentUserAutoOffline: 'getCurrentUserAutoOffline',
}),
availabilityDisplayLabel() {
const availabilityIndex = AVAILABILITY_STATUS_KEYS.findIndex(
@ -109,30 +85,21 @@ export default {
closeStatusMenu() {
this.isStatusMenuOpened = false;
},
updateAutoOffline(autoOffline) {
this.$store.dispatch('updateAutoOffline', {
accountId: this.currentAccountId,
autoOffline,
});
},
changeAvailabilityStatus(availability) {
const accountId = this.currentAccountId;
if (this.isUpdating) {
return;
}
this.isUpdating = true;
try {
this.$store.dispatch('updateAvailability', {
availability,
account_id: this.currentAccountId,
});
} catch (error) {
this.showAlert(
this.$t('PROFILE_SETTINGS.FORM.AVAILABILITY.SET_AVAILABILITY_ERROR')
);
} finally {
this.$store
.dispatch('updateAvailability', {
availability: availability,
account_id: accountId,
})
.finally(() => {
this.isUpdating = false;
}
});
},
},
};
@ -176,32 +143,4 @@ export default {
align-items: baseline;
}
}
.auto-offline--toggle {
align-items: center;
display: flex;
justify-content: space-between;
padding: var(--space-smaller) 0 var(--space-smaller) var(--space-small);
margin: 0;
.info-wrap {
display: flex;
align-items: center;
}
.info-icon {
margin-top: -1px;
}
.auto-offline--switch {
margin: -1px var(--space-micro) 0;
}
.auto-offline--text {
margin: 0 var(--space-smaller);
font-size: var(--font-size-mini);
font-weight: var(--font-weight-medium);
color: var(--s-700);
}
}
</style>

View file

@ -16,8 +16,6 @@ const conversations = accountId => ({
'conversation_through_mentions',
'folder_conversations',
'conversations_through_folders',
'conversation_unattended',
'conversation_through_unattended',
],
menuItems: [
{
@ -35,13 +33,6 @@ const conversations = accountId => ({
toState: frontendURL(`accounts/${accountId}/mentions/conversations`),
toStateName: 'conversation_mentions',
},
{
icon: 'mail-unread',
label: 'UNATTENDED_CONVERSATIONS',
key: 'conversation_unattended',
toState: frontendURL(`accounts/${accountId}/unattended/conversations`),
toStateName: 'conversation_unattended',
},
],
});

View file

@ -1,4 +1,3 @@
import { FEATURE_FLAGS } from '../../../../featureFlags';
import { frontendURL } from '../../../../helper/URLHelper';
const primaryMenuItems = accountId => [
@ -14,7 +13,6 @@ const primaryMenuItems = accountId => [
icon: 'book-contacts',
key: 'contacts',
label: 'CONTACTS',
featureFlag: FEATURE_FLAGS.CRM,
toState: frontendURL(`accounts/${accountId}/contacts`),
toStateName: 'contacts_dashboard',
roles: ['administrator', 'agent'],
@ -23,7 +21,6 @@ const primaryMenuItems = accountId => [
icon: 'arrow-trending-lines',
key: 'reports',
label: 'REPORTS',
featureFlag: FEATURE_FLAGS.REPORTS,
toState: frontendURL(`accounts/${accountId}/reports`),
toStateName: 'settings_account_reports',
roles: ['administrator'],
@ -32,7 +29,6 @@ const primaryMenuItems = accountId => [
icon: 'megaphone',
key: 'campaigns',
label: 'CAMPAIGNS',
featureFlag: FEATURE_FLAGS.CAMPAIGNS,
toState: frontendURL(`accounts/${accountId}/campaigns`),
toStateName: 'settings_account_campaigns',
roles: ['administrator'],
@ -41,7 +37,7 @@ const primaryMenuItems = accountId => [
icon: 'library',
key: 'helpcenter',
label: 'HELP_CENTER.TITLE',
featureFlag: FEATURE_FLAGS.HELP_CENTER,
featureFlag: 'help_center',
toState: frontendURL(`accounts/${accountId}/portals`),
toStateName: 'default_portal_articles',
roles: ['administrator'],

View file

@ -102,7 +102,6 @@ const settings = accountId => ({
label: 'AGENT_BOTS',
beta: true,
hasSubMenu: false,
globalConfigFlag: 'csmlEditorHost',
toState: frontendURL(`accounts/${accountId}/settings/agent-bots`),
toStateName: 'agent_bots',
featureFlag: FEATURE_FLAGS.AGENT_BOTS,

View file

@ -61,24 +61,6 @@
</a>
</router-link>
</woot-dropdown-item>
<woot-dropdown-item v-if="currentUser.type === 'SuperAdmin'">
<a
href="/super_admin"
class="button small clear secondary"
target="_blank"
rel="noopener nofollow noreferrer"
@click="$emit('close')"
>
<fluent-icon
icon="content-settings"
size="14"
class="icon icon--font"
/>
<span class="button__content">
{{ $t('SIDEBAR_ITEMS.SUPER_ADMIN_CONSOLE') }}
</span>
</a>
</woot-dropdown-item>
<woot-dropdown-item>
<woot-button
variant="clear"
@ -153,7 +135,7 @@ export default {
.dropdown-pane {
left: var(--space-slab);
bottom: var(--space-larger);
min-width: 22rem;
z-index: var(--z-index-low);
min-width: 16.8rem;
z-index: var(--z-index-much-higher);
}
</style>

View file

@ -261,7 +261,14 @@ export default {
width: 20rem;
flex-shrink: 0;
overflow-y: hidden;
@include breakpoint(xlarge down) {
position: absolute;
}
@include breakpoint(xlarge up) {
position: unset;
}
&:hover {
overflow-y: hidden;

View file

@ -112,7 +112,6 @@ $label-badge-size: var(--space-slab);
padding: var(--space-smaller) var(--space-smaller);
margin: var(--space-smaller) 0;
text-align: left;
line-height: 1.2;
&:hover {
background: var(--s-25);
@ -136,6 +135,8 @@ $label-badge-size: var(--space-slab);
.menu-label {
flex-grow: 1;
display: inline-flex;
align-items: center;
}
.inbox-icon {

View file

@ -87,10 +87,6 @@ import {
} from 'dashboard/helper/inbox';
import SecondaryChildNavItem from './SecondaryChildNavItem';
import {
isOnMentionsView,
isOnUnattendedView,
} from '../../../store/modules/conversations/helpers/actionHelpers';
export default {
components: { SecondaryChildNavItem },
@ -106,48 +102,32 @@ export default {
activeInbox: 'getSelectedInbox',
accountId: 'getCurrentAccountId',
isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount',
globalConfig: 'globalConfig/get',
}),
hasSubMenu() {
return !!this.menuItem.children;
},
isMenuItemVisible() {
if (this.menuItem.globalConfigFlag) {
return !!this.globalConfig[this.menuItem.globalConfigFlag];
if (!this.menuItem.featureFlag) {
return true;
}
if (this.menuItem.featureFlag) {
return this.isFeatureEnabledonAccount(
this.accountId,
this.menuItem.featureFlag
);
}
return true;
},
isAllConversations() {
isInboxConversation() {
return (
this.$store.state.route.name === 'inbox_conversation' &&
this.menuItem.toStateName === 'home'
);
},
isMentions() {
return (
isOnMentionsView({ route: this.$route }) &&
this.menuItem.toStateName === 'conversation_mentions'
);
},
isUnattended() {
return (
isOnUnattendedView({ route: this.$route }) &&
this.menuItem.toStateName === 'conversation_unattended'
);
},
isTeamsSettings() {
return (
this.$store.state.route.name === 'settings_teams_edit' &&
this.menuItem.toStateName === 'settings_teams_list'
);
},
isInboxSettings() {
isInboxsSettings() {
return (
this.$store.state.route.name === 'settings_inbox_show' &&
this.menuItem.toStateName === 'settings_inbox_list'
@ -170,20 +150,14 @@ export default {
},
computedClass() {
// If active inbox is present, do not highlight conversations
// If active Inbox is present
// donot highlight conversations
if (this.activeInbox) return ' ';
if (
this.isAllConversations ||
this.isMentions ||
this.isUnattended ||
this.isCurrentRoute
) {
return 'is-active';
}
if (this.hasSubMenu) {
if (
this.isInboxConversation ||
this.isTeamsSettings ||
this.isInboxSettings ||
this.isInboxsSettings ||
this.isIntegrationsSettings ||
this.isApplicationsSettings
) {
@ -192,6 +166,10 @@ export default {
return ' ';
}
if (this.isCurrentRoute) {
return 'is-active';
}
return '';
},
},

View file

@ -1,11 +1,10 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SidemenuIcon matches snapshot 1`] = `
<woot-button
class="toggle-sidebar"
color-scheme="secondary"
<button>
<fluent-icon
class="hamburger--menu"
icon="list"
size="small"
variant="clear"
/>
/>
</button>
`;

View file

@ -4,7 +4,7 @@
<fluent-icon :icon="icon" size="12" class="label--icon" />
</span>
<span
v-if="variant === 'smooth' && title && !icon"
v-if="variant === 'smooth'"
:style="{ background: color }"
class="label-color-dot"
/>
@ -117,16 +117,14 @@ export default {
height: var(--space-medium);
&.small {
font-size: var(--font-size-mini);
font-size: var(--font-size-micro);
padding: var(--space-micro) var(--space-smaller);
line-height: 1.2;
height: var(--space-two);
letter-spacing: 0.15px;
}
.label--icon {
cursor: pointer;
}
.label-color-dot {
margin-right: var(--space-smaller);
}
@ -201,8 +199,8 @@ export default {
&.smooth {
background: transparent;
border: 1px solid var(--s-100);
color: var(--s-700);
border: 1px solid var(--s-75);
color: var(--s-800);
}
}
@ -223,22 +221,14 @@ export default {
}
.label-action--button {
display: flex;
margin-right: var(--space-smaller);
margin-bottom: var(--space-minus-micro);
}
.label-color-dot {
display: inline-block;
width: var(--space-slab);
height: var(--space-slab);
width: var(--space-one);
height: var(--space-one);
border-radius: var(--border-radius-small);
margin-right: var(--space-smaller);
box-shadow: var(--shadow-small);
}
.label.small .label-color-dot {
width: var(--space-small);
height: var(--space-small);
border-radius: var(--border-radius-small);
box-shadow: var(--shadow-small);
}
</style>

View file

@ -2,7 +2,7 @@
<button
type="button"
class="toggle-button"
:class="{ active: value, small: size === 'small' }"
:class="{ active: value }"
role="switch"
:aria-checked="value.toString()"
@click="onClick"
@ -15,7 +15,6 @@
export default {
props: {
value: { type: Boolean, default: false },
size: { type: String, default: '' },
},
methods: {
onClick() {
@ -46,20 +45,6 @@ export default {
background-color: var(--w-500);
}
&.small {
width: 22px;
height: 14px;
span {
height: var(--space-one);
width: var(--space-one);
&.active {
transform: translate(var(--space-small), var(--space-zero));
}
}
}
span {
--space-one-point-five: 1.5rem;
background-color: var(--white);

View file

@ -18,32 +18,14 @@
<div v-if="showActionInput" class="filter__answer--wrap">
<div v-if="inputType">
<div
v-if="inputType === 'search_select'"
v-if="inputType === 'multi_select'"
class="multiselect-wrap--small"
>
<multiselect
v-model="action_params"
track-by="id"
label="name"
:placeholder="$t('FORMS.MULTISELECT.SELECT')"
selected-label
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
deselect-label=""
:max-height="160"
:options="dropdownValues"
:allow-empty="false"
:option-height="104"
/>
</div>
<div
v-else-if="inputType === 'multi_select'"
class="multiselect-wrap--small"
>
<multiselect
v-model="action_params"
track-by="id"
label="name"
:placeholder="$t('FORMS.MULTISELECT.SELECT')"
:placeholder="'Select'"
:multiple="true"
selected-label
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
@ -51,7 +33,6 @@
:max-height="160"
:options="dropdownValues"
:allow-empty="false"
:option-height="104"
/>
</div>
<input
@ -279,6 +260,6 @@ export default {
margin-bottom: var(--space-zero);
}
.action-message {
margin: var(--space-small) var(--space-zero) var(--space-zero);
margin: var(--space-small) 0 0;
}
</style>

View file

@ -48,6 +48,5 @@ export default {
text-align: center;
background-image: linear-gradient(to top, var(--w-100) 0%, var(--w-75) 100%);
color: var(--w-600);
cursor: default;
}
</style>

View file

@ -67,9 +67,6 @@ export default {
if (Object.keys(this.enabledFeatures).length === 0) {
return false;
}
if (key === 'website') {
return this.enabledFeatures.channel_website;
}
if (key === 'facebook') {
return this.enabledFeatures.channel_facebook;
}

View file

@ -61,7 +61,6 @@ export default {
}
.colorpicker--selected {
border: 1px solid var(--color-border-light);
border-radius: $space-smaller;
cursor: pointer;
height: $space-large;

View file

@ -46,16 +46,11 @@ export default {
return {
conversation: this.currentChat,
contact: this.$store.getters['contacts/getContact'](this.contactId),
currentAgent: this.currentAgent,
};
},
contactId() {
return this.currentChat?.meta?.sender?.id;
},
currentAgent() {
const { id, name, email } = this.$store.getters.getCurrentUser;
return { id, name, email };
},
},
mounted() {

View file

@ -32,7 +32,6 @@
v-for="attribute in filterAttributes"
:key="attribute.key"
:value="attribute.key"
:disabled="attribute.disabled"
>
{{ attribute.name }}
</option>
@ -174,10 +173,6 @@ export default {
type: Array,
default: () => [],
},
customAttributeType: {
type: String,
default: '',
},
},
computed: {
attributeKey: {

View file

@ -1,11 +1,7 @@
<template>
<span>
{{ textToBeDisplayed }}
<button
v-if="text.length > limit"
class="show-more--button"
@click="toggleShowMore"
>
<button class="show-more--button" @click="toggleShowMore">
{{ buttonLabel }}
</button>
</span>
@ -29,7 +25,7 @@ export default {
},
computed: {
textToBeDisplayed() {
if (this.showMore || this.text.length <= this.limit) {
if (this.showMore) {
return this.text;
}

View file

@ -1,9 +1,5 @@
<template>
<div
:class="thumbnailBoxClass"
:style="{ height: size, width: size }"
:title="title"
>
<div :class="thumbnailBoxClass" :style="{ height: size, width: size }">
<!-- Using v-show instead of v-if to avoid flickering as v-if removes dom elements. -->
<img
v-show="shouldShowImage"
@ -76,10 +72,6 @@ export default {
type: Boolean,
default: false,
},
title: {
type: String,
default: '',
},
variant: {
type: String,
default: 'circle',

View file

@ -3,13 +3,11 @@
<thumbnail
v-for="user in usersList"
:key="user.id"
v-tooltip="user.name"
:title="user.name"
:src="user.thumbnail"
:username="user.name"
:has-border="true"
:size="size"
:class="`overlapping-thumbnail gap-${gap}`"
class="overlapping-thumbnail"
/>
<span v-if="showMoreThumbnailsCount" class="thumbnail-more-text">
{{ moreThumbnailsText }}
@ -40,14 +38,6 @@ export default {
type: String,
default: '',
},
gap: {
type: String,
default: 'normal',
validator(value) {
// The value must match one of these strings
return ['normal', '', 'tight'].includes(value);
},
},
},
};
</script>
@ -62,10 +52,6 @@ export default {
box-shadow: var(--shadow-small);
&:not(:first-child) {
margin-left: var(--space-minus-smaller);
}
.gap-tight {
margin-left: var(--space-minus-small);
}
}

View file

@ -10,11 +10,11 @@ import 'videojs-record/dist/css/videojs.record.css';
import videojs from 'video.js';
import inboxMixin from '../../../../shared/mixins/inboxMixin';
import alertMixin from '../../../../shared/mixins/alertMixin';
import Recorder from 'opus-recorder';
import encoderWorker from 'opus-recorder/dist/encoderWorker.min';
import waveWorker from 'opus-recorder/dist/waveWorker.min';
import WaveSurfer from 'wavesurfer.js';
import MicrophonePlugin from 'wavesurfer.js/dist/plugin/wavesurfer.microphone.js';
@ -23,25 +23,19 @@ import 'videojs-wavesurfer/dist/videojs.wavesurfer.js';
import 'videojs-record/dist/videojs.record.js';
import 'videojs-record/dist/plugins/videojs.record.opus-recorder.js';
import { format, addSeconds } from 'date-fns';
import { AUDIO_FORMATS } from 'shared/constants/messages';
WaveSurfer.microphone = MicrophonePlugin;
export default {
name: 'WootAudioRecorder',
mixins: [alertMixin],
props: {
audioRecordFormat: {
type: String,
default: AUDIO_FORMATS.WEBM,
},
},
mixins: [inboxMixin, alertMixin],
data() {
return {
player: false,
recordingDateStarted: new Date(0),
initialTimeDuration: '00:00',
recorderOptions: {
debug: true,
controls: true,
bigPlayButton: false,
fluid: false,
@ -76,28 +70,13 @@ export default {
record: {
audio: true,
video: false,
maxLength: 900,
timeSlice: 1000,
maxFileSize: 15 * 1024 * 1024,
...(this.audioRecordFormat === AUDIO_FORMATS.WEBM && {
monitorGain: 0,
recordingGain: 1,
numberOfChannels: 1,
encoderSampleRate: 16000,
originalSampleRateOverride: 16000,
streamPages: true,
maxFramesPerPage: 1,
encoderFrameSize: 1,
encoderPath: waveWorker,
}),
...(this.audioRecordFormat === AUDIO_FORMATS.OGG && {
displayMilliseconds: false,
maxLength: 300,
audioEngine: 'opus-recorder',
audioWorkerURL: encoderWorker,
audioChannels: 1,
audioSampleRate: 48000,
audioBitRate: 128,
}),
},
},
},

View file

@ -39,17 +39,10 @@ const TYPING_INDICATOR_IDLE_TIME = 4000;
import '@chatwoot/prosemirror-schema/src/woot-editor.css';
import {
hasPressedEnterAndNotCmdOrShift,
hasPressedCommandAndEnter,
hasPressedAltAndPKey,
hasPressedAltAndLKey,
} from 'shared/helpers/KeyboardHelpers';
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import { isEditorHotKeyEnabled } from 'dashboard/mixins/uiSettings';
import AnalyticsHelper, {
ANALYTICS_EVENTS,
} from '../../../helper/AnalyticsHelper';
const createState = (content, placeholder, plugins = []) => {
return EditorState.create({
@ -65,15 +58,13 @@ const createState = (content, placeholder, plugins = []) => {
export default {
name: 'WootMessageEditor',
components: { TagAgents, CannedResponse },
mixins: [eventListenerMixins, uiSettingsMixin],
mixins: [eventListenerMixins],
props: {
value: { type: String, default: '' },
editorId: { type: String, default: '' },
placeholder: { type: String, default: '' },
isPrivate: { type: Boolean, default: false },
enableSuggestions: { type: Boolean, default: true },
overrideLineBreaks: { type: Boolean, default: false },
updateSelectionWith: { type: String, default: '' },
},
data() {
return {
@ -171,25 +162,6 @@ export default {
isPrivate() {
this.reloadState();
},
updateSelectionWith(newValue, oldValue) {
if (!this.editorView) {
return null;
}
if (newValue !== oldValue) {
if (this.updateSelectionWith !== '') {
const node = this.editorView.state.schema.text(
this.updateSelectionWith
);
const tr = this.editorView.state.tr.replaceSelectionWith(node);
this.editorView.focus();
this.state = this.editorView.state.apply(tr);
this.emitOnChange();
this.$emit('clear-selection');
}
}
return null;
},
},
created() {
this.state = createState(this.value, this.placeholder, this.plugins);
@ -216,9 +188,6 @@ export default {
keyup: () => {
this.onKeyup();
},
keydown: (view, event) => {
this.onKeydown(event);
},
focus: () => {
this.onFocus();
},
@ -234,12 +203,6 @@ export default {
},
});
},
isEnterToSendEnabled() {
return isEditorHotKeyEnabled(this.uiSettings, 'enter');
},
isCmdPlusEnterToSendEnabled() {
return isEditorHotKeyEnabled(this.uiSettings, 'cmd_enter');
},
handleKeyEvents(e) {
if (hasPressedAltAndPKey(e)) {
this.focusEditorInputField();
@ -270,10 +233,7 @@ export default {
node
);
this.state = this.editorView.state.apply(tr);
this.emitOnChange();
AnalyticsHelper.track(ANALYTICS_EVENTS.USED_MENTIONS);
return false;
return this.emitOnChange();
},
insertCannedResponse(cannedItem) {
@ -281,27 +241,22 @@ export default {
return null;
}
let from = this.range.from - 1;
let node = addMentionsToMarkdownParser(defaultMarkdownParser).parse(
cannedItem
const tr = this.editorView.state.tr.insertText(
cannedItem,
this.range.from,
this.range.to
);
if (node.childCount === 1) {
node = this.editorView.state.schema.text(cannedItem);
from = this.range.from;
}
const tr = this.editorView.state.tr.replaceWith(
from,
this.range.to,
node
);
this.state = this.editorView.state.apply(tr);
this.emitOnChange();
tr.scrollIntoView();
AnalyticsHelper.track(ANALYTICS_EVENTS.INSERTED_A_CANNED_RESPONSE);
// Hacky fix for #5501
this.state = createState(
this.contentFromEditor,
this.placeholder,
this.plugins
);
this.editorView.updateState(this.state);
this.focusEditorInputField();
return false;
},
@ -323,24 +278,6 @@ export default {
clearTimeout(this.idleTimer);
}
},
handleLineBreakWhenEnterToSendEnabled(event) {
if (
hasPressedEnterAndNotCmdOrShift(event) &&
this.isEnterToSendEnabled() &&
!this.overrideLineBreaks
) {
event.preventDefault();
}
},
handleLineBreakWhenCmdAndEnterToSendEnabled(event) {
if (
hasPressedCommandAndEnter(event) &&
this.isCmdPlusEnterToSendEnabled() &&
!this.overrideLineBreaks
) {
event.preventDefault();
}
},
onKeyup() {
if (!this.idleTimer) {
this.$emit('typing-on');
@ -351,14 +288,6 @@ export default {
TYPING_INDICATOR_IDLE_TIME
);
},
onKeydown(event) {
if (this.isEnterToSendEnabled()) {
this.handleLineBreakWhenEnterToSendEnabled(event);
}
if (this.isCmdPlusEnterToSendEnabled()) {
this.handleLineBreakWhenCmdAndEnterToSendEnabled(event);
}
},
onBlur() {
this.turnOffIdleTimer();
this.resetTyping();

View file

@ -232,18 +232,11 @@ export default {
return this.showFileUpload || this.isNote;
},
showAudioRecorderButton() {
// Disable audio recorder for safari browser as recording is not supported
const isSafari = /^((?!chrome|android|crios|fxios).)*safari/i.test(
navigator.userAgent
);
return (
this.isFeatureEnabledonAccount(
this.accountId,
FEATURE_FLAGS.VOICE_RECORDER
) &&
this.showAudioRecorder &&
!isSafari
) && this.showAudioRecorder
);
},
showAudioPlayStopButton() {

View file

@ -91,7 +91,6 @@
</span>
<span class="unread">{{ unreadCount > 9 ? '9+' : unreadCount }}</span>
</div>
<card-labels :conversation-id="chat.id" />
</div>
<woot-context-menu
v-if="showContextMenu"
@ -103,12 +102,10 @@
<conversation-context-menu
:status="chat.status"
:inbox-id="inbox.id"
:has-unread-messages="hasUnread"
@update-conversation="onUpdateConversation"
@assign-agent="onAssignAgent"
@assign-label="onAssignLabel"
@assign-team="onAssignTeam"
@mark-as-unread="markAsUnread"
/>
</woot-context-menu>
</div>
@ -126,8 +123,8 @@ import InboxName from '../InboxName';
import inboxMixin from 'shared/mixins/inboxMixin';
import ConversationContextMenu from './contextMenu/Index.vue';
import alertMixin from 'shared/mixins/alertMixin';
import TimeAgo from 'dashboard/components/ui/TimeAgo';
import CardLabels from './conversationCardComponents/CardLabels.vue';
import timeAgo from 'dashboard/components/ui/TimeAgo';
const ATTACHMENT_ICONS = {
image: 'image',
audio: 'headphones-sound-wave',
@ -139,11 +136,10 @@ const ATTACHMENT_ICONS = {
export default {
components: {
CardLabels,
InboxName,
Thumbnail,
ConversationContextMenu,
TimeAgo,
timeAgo,
},
mixins: [
@ -245,7 +241,7 @@ export default {
},
unreadCount() {
return this.chat.unread_count;
return this.unreadMessagesCount(this.chat);
},
hasUnread() {
@ -363,24 +359,16 @@ export default {
this.$emit('assign-team', team, this.chat.id);
this.closeContextMenu();
},
async markAsUnread() {
this.$emit('mark-as-unread', this.chat.id);
this.closeContextMenu();
},
},
};
</script>
<style lang="scss" scoped>
.conversation {
align-items: flex-start;
align-items: center;
&:hover {
background: var(--color-background-light);
}
&::v-deep .user-thumbnail-box {
margin-top: var(--space-normal);
}
}
.conversation-selected {
@ -389,10 +377,8 @@ export default {
.has-inbox-name {
&::v-deep .user-thumbnail-box {
margin-top: var(--space-large);
}
.checkbox-wrapper {
margin-top: var(--space-large);
margin-top: var(--space-normal);
align-items: flex-start;
}
.conversation--meta {
margin-top: var(--space-normal);
@ -437,7 +423,6 @@ export default {
margin-top: var(--space-minus-micro);
vertical-align: middle;
}
.checkbox-wrapper {
height: 40px;
width: 40px;
@ -447,7 +432,6 @@ export default {
border-radius: 100%;
margin-top: var(--space-normal);
cursor: pointer;
&:hover {
background-color: var(--w-100);
}

View file

@ -21,11 +21,7 @@
/>
</h3>
<div class="conversation--header--actions">
<inbox-name
v-if="hasMultipleInboxes"
:inbox="inbox"
class="margin-right-small"
/>
<inbox-name :inbox="inbox" class="margin-right-small" />
<span
v-if="isSnoozed"
class="snoozed--display-text margin-right-small"
@ -149,9 +145,6 @@ export default {
const { inbox_id: inboxId } = this.chat;
return this.$store.getters['inboxes/getInbox'](inboxId);
},
hasMultipleInboxes() {
return this.$store.getters['inboxes/getInboxes'].length > 1;
},
},
methods: {

View file

@ -1,6 +1,7 @@
<template>
<li
v-if="hasAttachments || data.content || isEmailContentType"
:id="'message' + data.id"
:class="alignBubble"
>
<div :class="wrapClass">
@ -15,6 +16,7 @@
v-if="data.content"
:message="message"
:is-email="isEmailContentType"
:readable-time="readableTime"
:display-quoted-button="displayQuotedButton"
/>
<span
@ -28,6 +30,7 @@
<bubble-image
v-if="attachment.file_type === 'image' && !hasImageError"
:url="attachment.data_url"
:readable-time="readableTime"
@error="onImageLoadError"
/>
<audio v-else-if="attachment.file_type === 'audio'" controls>
@ -36,6 +39,7 @@
<bubble-video
v-else-if="attachment.file_type === 'video'"
:url="attachment.data_url"
:readable-time="readableTime"
/>
<bubble-location
v-else-if="attachment.file_type === 'location'"
@ -43,7 +47,11 @@
:longitude="attachment.coordinates_long"
:name="attachment.fallback_title"
/>
<bubble-file v-else :url="attachment.data_url" />
<bubble-file
v-else
:url="attachment.data_url"
:readable-time="readableTime"
/>
</div>
</div>
<bubble-actions
@ -52,15 +60,14 @@
:story-sender="storySender"
:story-id="storyId"
:is-a-tweet="isATweet"
:is-a-whatsapp-channel="isAWhatsAppChannel"
:has-instagram-story="hasInstagramStory"
:is-email="isEmailContentType"
:is-private="data.private"
:message-type="data.message_type"
:message-status="status"
:readable-time="readableTime"
:source-id="data.source_id"
:inbox-id="data.inbox_id"
:created-at="createdAt"
:message-read="showReadTicks"
/>
</div>
<spinner v-if="isPending" size="tiny" />
@ -98,8 +105,10 @@
<div v-if="shouldShowContextMenu" class="context-menu-wrap">
<context-menu
v-if="isBubble && !isMessageDeleted"
:id="data.id"
:is-open="showContextMenu"
:show-copy="hasText"
:conversation-id="data.conversation_id"
:show-canned-response-option="isOutgoing"
:menu-position="contextMenuPosition"
:message-content="data.content"
@ -111,6 +120,8 @@
</template>
<script>
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
import timeMixin from '../../../mixins/time';
import BubbleMailHead from './bubble/MailHead';
import BubbleText from './bubble/Text';
import BubbleImage from './bubble/Image';
@ -139,7 +150,7 @@ export default {
ContextMenu,
Spinner,
},
mixins: [alertMixin, messageFormatterMixin, contentTypeMixin],
mixins: [alertMixin, timeMixin, messageFormatterMixin, contentTypeMixin],
props: {
data: {
type: Object,
@ -149,11 +160,11 @@ export default {
type: Boolean,
default: false,
},
isAWhatsAppChannel: {
hasInstagramStory: {
type: Boolean,
default: false,
},
hasInstagramStory: {
hasUserReadMessage: {
type: Boolean,
default: false,
},
@ -223,9 +234,6 @@ export default {
sender() {
return this.data.sender || {};
},
status() {
return this.data.status;
},
storySender() {
return this.contentAttributes.story_sender || null;
},
@ -259,8 +267,11 @@ export default {
'has-tweet-menu': this.isATweet,
};
},
createdAt() {
return this.contentAttributes.external_created_at || this.data.created_at;
readableTime() {
return this.messageStamp(
this.contentAttributes.external_created_at || this.data.created_at,
'LLL d, h:mm a'
);
},
isBubble() {
return [0, 1, 3].includes(this.data.message_type);
@ -271,6 +282,14 @@ export default {
isOutgoing() {
return this.data.message_type === MESSAGE_TYPE.OUTGOING;
},
showReadTicks() {
return (
(this.isOutgoing || this.isTemplate) &&
this.hasUserReadMessage &&
this.isWebWidgetInbox &&
!this.data.private
);
},
isTemplate() {
return this.data.message_type === MESSAGE_TYPE.TEMPLATE;
},

View file

@ -35,18 +35,20 @@
<message
v-for="message in getReadMessages"
:key="message.id"
class="message--read ph-no-capture"
class="message--read"
:data="message"
:is-a-tweet="isATweet"
:is-a-whatsapp-channel="isAWhatsAppChannel"
:has-instagram-story="hasInstagramStory"
:has-user-read-message="
hasUserReadMessage(message.created_at, getLastSeenAt)
"
:is-web-widget-inbox="isAWebWidgetInbox"
/>
<li v-show="unreadMessageCount != 0" class="unread--toast">
<li v-show="getUnreadCount != 0" class="unread--toast">
<span class="text-uppercase">
{{ unreadMessageCount }}
{{ getUnreadCount }}
{{
unreadMessageCount > 1
getUnreadCount > 1
? $t('CONVERSATION.UNREAD_MESSAGES')
: $t('CONVERSATION.UNREAD_MESSAGE')
}}
@ -55,11 +57,13 @@
<message
v-for="message in getUnReadMessages"
:key="message.id"
class="message--unread ph-no-capture"
class="message--unread"
:data="message"
:is-a-tweet="isATweet"
:is-a-whatsapp-channel="isAWhatsAppChannel"
:has-instagram-story="hasInstagramStory"
:has-user-read-message="
hasUserReadMessage(message.created_at, getLastSeenAt)
"
:is-web-widget-inbox="isAWebWidgetInbox"
/>
</ul>
@ -133,6 +137,7 @@ export default {
allConversations: 'getAllConversations',
inboxesList: 'inboxes/getInboxes',
listLoadingStatus: 'getAllMessagesLoaded',
getUnreadCount: 'getUnreadCount',
loadingChatList: 'getChatListLoadingStatus',
}),
inboxId() {
@ -266,9 +271,6 @@ export default {
}
return '';
},
unreadMessageCount() {
return this.currentChat.unread_count;
},
},
watch: {
@ -302,8 +304,15 @@ export default {
setSelectedTweet(tweetId) {
this.selectedTweetId = tweetId;
},
onScrollToMessage() {
this.$nextTick(() => this.scrollToBottom());
onScrollToMessage({ messageId = '' } = {}) {
this.$nextTick(() => {
const messageElement = document.getElementById('message' + messageId);
if (messageElement) {
messageElement.scrollIntoView({ behavior: 'smooth' });
} else {
this.scrollToBottom();
}
});
this.makeMessagesRead();
},
showPopoutReplyBox() {
@ -329,7 +338,7 @@ export default {
},
scrollToBottom() {
let relevantMessages = [];
if (this.unreadMessageCount > 0) {
if (this.getUnreadCount > 0) {
// capturing only the unread messages
relevantMessages = this.conversationPanel.querySelectorAll(
'.message--unread'
@ -427,7 +436,12 @@ export default {
position: fixed;
left: unset;
position: absolute;
bottom: var(--space-smaller);
&::before {
transform: rotate(0deg);
left: var(--space-smaller);
bottom: var(--space-minus-slab);
}
}
}
}

View file

@ -37,7 +37,6 @@
<woot-audio-recorder
v-if="showAudioRecorderEditor"
ref="audioRecorderInput"
:audio-record-format="audioRecordFormat"
@state-recorder-progress-changed="onStateProgressRecorderChanged"
@state-recorder-changed="onStateRecorderChanged"
@finish-record="onFinishRecorder"
@ -61,7 +60,6 @@
class="input"
:is-private="isOnPrivateNote"
:placeholder="messagePlaceHolder"
:update-selection-with="updateEditorSelectionWith"
:min-height="4"
@typing-off="onTypingOff"
@typing-on="onTypingOn"
@ -69,7 +67,6 @@
@blur="onBlur"
@toggle-user-mention="toggleUserMention"
@toggle-canned-menu="toggleCannedMenu"
@clear-selection="clearEditorSelection"
/>
</div>
<div v-if="hasAttachments" class="attachment-preview-box" @paste="onPaste">
@ -133,6 +130,7 @@ import { mapGetters } from 'vuex';
import { mixin as clickaway } from 'vue-clickaway';
import alertMixin from 'shared/mixins/alertMixin';
import EmojiInput from 'shared/components/emoji/EmojiInput';
import CannedResponse from './CannedResponse';
import ResizableTextArea from 'shared/components/ResizableTextArea';
import AttachmentPreview from 'dashboard/components/widgets/AttachmentsPreview';
@ -148,7 +146,6 @@ import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
import {
MAXIMUM_FILE_UPLOAD_SIZE,
MAXIMUM_FILE_UPLOAD_SIZE_TWILIO_SMS_CHANNEL,
AUDIO_FORMATS,
} from 'shared/constants/messages';
import { BUS_EVENTS } from 'shared/constants/busEvents';
@ -163,11 +160,6 @@ import { LocalStorage, LOCAL_STORAGE_KEYS } from '../../../helper/localStorage';
import { trimContent, debounce } from '@chatwoot/utils';
import wootConstants from 'dashboard/constants';
import { isEditorHotKeyEnabled } from 'dashboard/mixins/uiSettings';
import AnalyticsHelper, {
ANALYTICS_EVENTS,
} from '../../../helper/AnalyticsHelper';
const EmojiInput = () => import('shared/components/emoji/EmojiInput');
export default {
components: {
@ -223,7 +215,6 @@ export default {
ccEmails: '',
doAutoSaveDraft: () => {},
showWhatsAppTemplatesModal: false,
updateEditorSelectionWith: '',
};
},
computed: {
@ -407,7 +398,7 @@ export default {
return conversationDisplayType !== CONDENSED;
},
emojiDialogClassOnExpanedLayout() {
return this.isOnExpandedLayout || this.popoutReplyBox
return this.isOnExpandedLayout && !this.popoutReplyBox
? 'emoji-dialog--expanded'
: '';
},
@ -459,17 +450,12 @@ export default {
return this.currentChat.id;
},
conversationIdByRoute() {
return this.conversationId;
const { conversation_id: conversationId } = this.$route.params;
return conversationId;
},
editorStateId() {
return `draft-${this.conversationIdByRoute}-${this.replyType}`;
},
audioRecordFormat() {
if (this.isAWebWidgetInbox) {
return AUDIO_FORMATS.WEBM;
}
return AUDIO_FORMATS.OGG;
},
},
watch: {
currentChat(conversation) {
@ -601,7 +587,6 @@ export default {
e.preventDefault();
} else if (keyCode === 'enter' && this.isAValidEvent('enter')) {
this.onSendReply();
e.preventDefault();
} else if (
['meta+enter', 'ctrl+enter'].includes(keyCode) &&
this.isAValidEvent('cmd_enter')
@ -709,7 +694,6 @@ export default {
},
replaceText(message) {
setTimeout(() => {
AnalyticsHelper.track(ANALYTICS_EVENTS.INSERTED_A_CANNED_RESPONSE);
this.message = message;
}, 100);
},
@ -724,26 +708,8 @@ export default {
}
this.$nextTick(() => this.$refs.messageInput.focus());
},
clearEditorSelection() {
this.updateEditorSelectionWith = '';
},
insertEmoji(emoji, selectionStart, selectionEnd) {
const { message } = this;
const newMessage =
message.slice(0, selectionStart) +
emoji +
message.slice(selectionEnd, message.length);
this.message = newMessage;
},
emojiOnClick(emoji) {
if (this.showRichContentEditor) {
this.updateEditorSelectionWith = emoji;
this.onFocus();
}
if (!this.showRichContentEditor) {
const { selectionStart, selectionEnd } = this.$refs.messageInput.$el;
this.insertEmoji(emoji, selectionStart, selectionEnd);
}
this.message = `${this.message}${emoji} `;
},
clearMessage() {
this.message = '';
@ -998,13 +964,13 @@ export default {
.emoji-dialog {
top: unset;
bottom: var(--space-normal);
bottom: 12px;
left: -320px;
right: unset;
&::before {
right: var(--space-minus-normal);
bottom: var(--space-small);
right: -16px;
bottom: 10px;
transform: rotate(270deg);
filter: drop-shadow(0px 4px 4px rgba(0, 0, 0, 0.08));
}
@ -1018,7 +984,7 @@ export default {
&::before {
transform: rotate(0deg);
left: var(--space-smaller);
bottom: var(--space-minus-small);
bottom: var(--space-minus-slab);
}
}
.message-signature {

View file

@ -1,38 +1,22 @@
<template>
<div class="message-text--metadata">
<span
class="time"
:class="{
'has-status-icon':
showSentIndicator || showDeliveredIndicator || showReadIndicator,
}"
>
{{ readableTime }}
</span>
<span v-if="showReadIndicator" class="read-indicator-wrap">
<fluent-icon
v-tooltip.top-start="$t('CHAT_LIST.MESSAGE_READ')"
icon="checkmark-double"
class="action--icon read-tick read-indicator"
size="14"
/>
</span>
<span v-else-if="showDeliveredIndicator" class="read-indicator-wrap">
<fluent-icon
v-tooltip.top-start="$t('CHAT_LIST.DELIVERED')"
icon="checkmark-double"
class="action--icon read-tick"
size="14"
/>
</span>
<span v-else-if="showSentIndicator" class="read-indicator-wrap">
<span class="time" :class="{ delivered: messageRead }">{{
readableTime
}}</span>
<span v-if="showSentIndicator" class="time">
<fluent-icon
v-tooltip.top-start="$t('CHAT_LIST.SENT')"
icon="checkmark"
class="action--icon read-tick"
size="14"
/>
</span>
<fluent-icon
v-if="messageRead"
v-tooltip.top-start="$t('CHAT_LIST.MESSAGE_READ')"
icon="checkmark-double"
class="action--icon read-tick"
size="12"
/>
<fluent-icon
v-if="isEmail"
v-tooltip.top-start="$t('CHAT_LIST.RECEIVED_VIA_EMAIL')"
@ -60,6 +44,19 @@
size="16"
/>
</button>
<a
v-if="hasInstagramStory && (isIncoming || isOutgoing) && linkToStory"
:href="linkToStory"
target="_blank"
rel="noopener noreferrer nofollow"
>
<fluent-icon
v-tooltip.top-start="$t('CHAT_LIST.LINK_TO_STORY')"
icon="open"
class="action--icon cursor-pointer"
size="16"
/>
</a>
<a
v-if="isATweet && (isOutgoing || isIncoming) && linkToTweet"
:href="linkToTweet"
@ -77,22 +74,20 @@
</template>
<script>
import { MESSAGE_TYPE, MESSAGE_STATUS } from 'shared/constants/messages';
import { MESSAGE_TYPE } from 'shared/constants/messages';
import { BUS_EVENTS } from 'shared/constants/busEvents';
import inboxMixin from 'shared/mixins/inboxMixin';
import { mapGetters } from 'vuex';
import timeMixin from '../../../../mixins/time';
export default {
mixins: [inboxMixin, timeMixin],
mixins: [inboxMixin],
props: {
sender: {
type: Object,
default: () => ({}),
},
createdAt: {
type: Number,
default: 0,
readableTime: {
type: String,
default: '',
},
storySender: {
type: String,
@ -122,10 +117,6 @@ export default {
type: Number,
default: 1,
},
messageStatus: {
type: String,
default: '',
},
sourceId: {
type: String,
default: '',
@ -138,9 +129,12 @@ export default {
type: [String, Number],
default: 0,
},
messageRead: {
type: Boolean,
default: false,
},
},
computed: {
...mapGetters({ currentChat: 'getSelectedChat' }),
inbox() {
return this.$store.getters['inboxes/getInbox'](this.inboxId);
},
@ -150,21 +144,6 @@ export default {
isOutgoing() {
return MESSAGE_TYPE.OUTGOING === this.messageType;
},
isTemplate() {
return MESSAGE_TYPE.TEMPLATE === this.messageType;
},
isDelivered() {
return MESSAGE_STATUS.DELIVERED === this.messageStatus;
},
isRead() {
return MESSAGE_STATUS.READ === this.messageStatus;
},
isSent() {
return MESSAGE_STATUS.SENT === this.messageStatus;
},
readableTime() {
return this.messageStamp(this.createdAt, 'LLL d, h:mm a');
},
screenName() {
const { additional_attributes: additionalAttributes = {} } =
this.sender || {};
@ -185,52 +164,12 @@ export default {
const { storySender, storyId } = this;
return `https://www.instagram.com/stories/${storySender}/${storyId}`;
},
showStatusIndicators() {
if ((this.isOutgoing || this.isTemplate) && !this.private) {
return true;
}
return false;
},
showSentIndicator() {
if (!this.showStatusIndicators) {
return false;
}
if (this.isAnEmailChannel) {
return !!this.sourceId;
}
if (this.isAWhatsAppChannel) {
return this.sourceId && this.isSent;
}
return false;
},
showDeliveredIndicator() {
if (!this.showStatusIndicators) {
return false;
}
if (this.isAWhatsAppChannel) {
return this.sourceId && this.isDelivered;
}
return false;
},
showReadIndicator() {
if (!this.showStatusIndicators) {
return false;
}
if (this.isAWebWidgetInbox) {
const { contact_last_seen_at: contactLastSeenAt } = this.currentChat;
return contactLastSeenAt >= this.createdAt;
}
if (this.isAWhatsAppChannel) {
return this.sourceId && this.isRead;
}
return false;
return (
this.isOutgoing &&
this.sourceId &&
(this.isAnEmailChannel || this.isAWhatsAppChannel)
);
},
},
methods: {
@ -246,21 +185,16 @@ export default {
.right {
.message-text--metadata {
align-items: center;
.time {
color: var(--w-100);
}
.action--icon {
color: var(--white);
&.read-tick {
color: var(--v-100);
margin-top: calc(var(--space-micro) + var(--space-micro) / 2);
}
&.read-indicator {
color: var(--g-200);
}
color: var(--white);
}
.lock--icon--private {
@ -324,9 +258,8 @@ export default {
position: absolute;
right: var(--space-small);
white-space: nowrap;
&.has-status-icon {
right: var(--space-large);
&.delivered {
right: var(--space-medium);
line-height: 2;
}
}
@ -363,10 +296,4 @@ export default {
.delivered-icon {
margin-left: -var(--space-normal);
}
.read-indicator-wrap {
line-height: 1;
display: flex;
align-items: center;
}
</style>

View file

@ -35,6 +35,10 @@ export default {
type: String,
default: '',
},
readableTime: {
type: String,
default: '',
},
isEmail: {
type: Boolean,
default: true,

View file

@ -1,11 +1,5 @@
<template>
<div class="menu-container">
<menu-item
v-if="!hasUnreadMessages"
:option="unreadOption"
variant="icon"
@click="$emit('mark-as-unread')"
/>
<template v-for="option in statusMenuConfig">
<menu-item
v-if="show(option.key)"
@ -85,10 +79,6 @@ export default {
type: String,
default: '',
},
hasUnreadMessages: {
type: Boolean,
default: false,
},
inboxId: {
type: Number,
default: null,
@ -97,10 +87,6 @@ export default {
data() {
return {
STATUS_TYPE: wootConstants.STATUS_TYPE,
unreadOption: {
label: this.$t('CONVERSATION.CARD_CONTEXT_MENU.MARK_AS_UNREAD'),
icon: 'mail',
},
statusMenuConfig: [
{
key: wootConstants.STATUS_TYPE.RESOLVED,

View file

@ -1,6 +1,6 @@
<template>
<div class="bulk-action__agents">
<div class="triangle" :style="cssVars">
<div class="triangle">
<svg height="12" viewBox="0 0 24 12" width="24">
<path
d="M20 12l-8-8-12 12"
@ -105,14 +105,13 @@ import { mapGetters } from 'vuex';
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
import Spinner from 'shared/components/Spinner';
import { mixin as clickaway } from 'vue-clickaway';
import bulkActionsMixin from 'dashboard/mixins/bulkActionsMixin.js';
export default {
components: {
Thumbnail,
Spinner,
},
mixins: [clickaway, bulkActionsMixin],
mixins: [clickaway],
props: {
selectedInboxes: {
type: Array,
@ -234,7 +233,7 @@ export default {
z-index: var(--z-index-one);
position: absolute;
top: calc(var(--space-slab) * -1);
right: var(--triangle-position);
right: var(--space-micro);
text-align: left;
}
}

View file

@ -43,26 +43,25 @@
variant="smooth"
color-scheme="secondary"
icon="person-assign"
class="margin-right-smaller"
@click="toggleAgentList"
/>
<woot-button
v-tooltip="$t('BULK_ACTION.ASSIGN_TEAM_TOOLTIP')"
size="tiny"
variant="smooth"
color-scheme="secondary"
icon="people-team-add"
@click="toggleTeamsList"
/>
</div>
<transition name="popover-animation">
<label-actions
v-if="showLabelActions"
triangle-position="8.5"
@assign="assignLabels"
@close="showLabelActions = false"
/>
</transition>
<transition name="popover-animation">
<agent-selector
v-if="showAgentsList"
:selected-inboxes="selectedInboxes"
:conversation-count="conversations.length"
@select="submit"
@close="showAgentsList = false"
/>
</transition>
<transition name="popover-animation">
<update-actions
v-if="showUpdateActions"
@ -71,29 +70,10 @@
:show-resolve="!showResolvedAction"
:show-reopen="!showOpenAction"
:show-snooze="!showSnoozedAction"
triangle-position="5.6"
@update="updateConversations"
@close="showUpdateActions = false"
/>
</transition>
<transition name="popover-animation">
<agent-selector
v-if="showAgentsList"
:selected-inboxes="selectedInboxes"
:conversation-count="conversations.length"
triangle-position="2.8"
@select="submit"
@close="showAgentsList = false"
/>
</transition>
<transition name="popover-animation">
<team-actions
v-if="showTeamsList"
triangle-position="0.2"
@assign-team="assignTeam"
@close="showTeamsList = false"
/>
</transition>
</div>
<div v-if="allConversationsSelected" class="bulk-action__alert">
{{ $t('BULK_ACTION.ALL_CONVERSATIONS_SELECTED_ALERT') }}
@ -105,13 +85,11 @@
import AgentSelector from './AgentSelector.vue';
import UpdateActions from './UpdateActions.vue';
import LabelActions from './LabelActions.vue';
import TeamActions from './TeamActions.vue';
export default {
components: {
AgentSelector,
UpdateActions,
LabelActions,
TeamActions,
},
props: {
conversations: {
@ -144,8 +122,6 @@ export default {
showAgentsList: false,
showUpdateActions: false,
showLabelActions: false,
showTeamsList: false,
popoverPositions: {},
};
},
methods: {
@ -161,9 +137,6 @@ export default {
assignLabels(labels) {
this.$emit('assign-labels', labels);
},
assignTeam(team) {
this.$emit('assign-team', team);
},
resolveConversations() {
this.$emit('resolve-conversations');
},
@ -176,9 +149,6 @@ export default {
toggleAgentList() {
this.showAgentsList = !this.showAgentsList;
},
toggleTeamsList() {
this.showTeamsList = !this.showTeamsList;
},
},
};
</script>

View file

@ -1,6 +1,6 @@
<template>
<div v-on-clickaway="onClose" class="labels-container">
<div class="triangle" :style="cssVars">
<div class="triangle">
<svg height="12" viewBox="0 0 24 12" width="24">
<path
d="M20 12l-8-8-12 12"
@ -75,10 +75,9 @@
<script>
import { mixin as clickaway } from 'vue-clickaway';
import { mapGetters } from 'vuex';
import bulkActionsMixin from 'dashboard/mixins/bulkActionsMixin.js';
export default {
mixins: [clickaway, bulkActionsMixin],
mixins: [clickaway],
data() {
return {
query: '',
@ -161,7 +160,7 @@ export default {
max-width: var(--space-giga);
min-width: var(--space-giga);
position: absolute;
right: var(--space-small);
right: 4.5rem;
top: var(--space-larger);
transform-origin: top right;
width: auto;
@ -205,7 +204,7 @@ export default {
.triangle {
display: block;
position: absolute;
right: var(--triangle-position);
right: var(--space-two);
text-align: left;
top: calc(var(--space-slab) * -1);
z-index: var(--z-index-one);

View file

@ -1,174 +0,0 @@
<template>
<div v-on-clickaway="onClose" class="bulk-action__teams">
<div class="triangle" :style="cssVars">
<svg height="12" viewBox="0 0 24 12" width="24">
<path
d="M20 12l-8-8-12 12"
fill="var(--white)"
fill-rule="evenodd"
stroke="var(--s-50)"
stroke-width="1px"
/>
</svg>
</div>
<div class="header flex-between">
<span>{{ $t('BULK_ACTION.TEAMS.TEAM_SELECT_LABEL') }}</span>
<woot-button
size="tiny"
variant="clear"
color-scheme="secondary"
icon="dismiss"
@click="onClose"
/>
</div>
<div class="container">
<div class="team__list-container">
<ul>
<li class="search-container">
<div class="agent-list-search flex-between">
<fluent-icon icon="search" class="search-icon" size="16" />
<input
ref="search"
v-model="query"
type="search"
placeholder="Search"
class="agent--search_input"
/>
</div>
</li>
<template v-if="filteredTeams.length">
<li v-for="team in filteredTeams" :key="team.id">
<div class="team__list-item" @click="assignTeam(team)">
<span class="reports-option__title">{{ team.name }}</span>
</div>
</li>
</template>
<li v-else>
<div class="team__list-item">
<span class="reports-option__title">{{
$t('BULK_ACTION.TEAMS.NO_TEAMS_AVAILABLE')
}}</span>
</div>
</li>
</ul>
</div>
</div>
</div>
</template>
<script>
import { mixin as clickaway } from 'vue-clickaway';
import { mapGetters } from 'vuex';
import bulkActionsMixin from 'dashboard/mixins/bulkActionsMixin.js';
export default {
mixins: [clickaway, bulkActionsMixin],
data() {
return {
query: '',
selectedteams: [],
};
},
computed: {
...mapGetters({ teams: 'teams/getTeams' }),
filteredTeams() {
return [
{ name: 'None', id: 0 },
...this.teams.filter(team =>
team.name.toLowerCase().includes(this.query.toLowerCase())
),
];
},
},
methods: {
assignTeam(key) {
this.$emit('assign-team', key);
},
onClose() {
this.$emit('close');
},
},
};
</script>
<style scoped lang="scss">
.bulk-action__teams {
background-color: var(--white);
border-radius: var(--border-radius-large);
border: 1px solid var(--s-50);
box-shadow: var(--shadow-dropdown-pane);
max-width: 75%;
position: absolute;
right: var(--space-small);
top: var(--space-larger);
transform-origin: top right;
width: auto;
z-index: var(--z-index-twenty);
min-width: var(--space-giga);
.header {
padding: var(--space-one);
span {
font-size: var(--font-size-small);
font-weight: var(--font-weight-medium);
}
}
.container {
max-height: var(--space-giga);
overflow-y: auto;
.team__list-container {
height: 100%;
}
.agent-list-search {
padding: 0 var(--space-one);
border: 1px solid var(--s-100);
border-radius: var(--border-radius-medium);
background-color: var(--s-50);
.search-icon {
color: var(--s-400);
}
.agent--search_input {
border: 0;
font-size: var(--font-size-mini);
margin: 0;
background-color: transparent;
height: unset;
}
}
}
.triangle {
display: block;
z-index: var(--z-index-one);
position: absolute;
top: calc(var(--space-slab) * -1);
right: var(--triangle-position);
text-align: left;
}
}
ul {
margin: 0;
list-style: none;
}
.team__list-item {
display: flex;
align-items: center;
padding: var(--space-one);
cursor: pointer;
&:hover {
background-color: var(--s-50);
}
span {
font-size: var(--font-size-small);
}
}
.search-container {
padding: 0 var(--space-one);
position: sticky;
top: 0;
z-index: var(--z-index-twenty);
background-color: var(--white);
}
</style>

View file

@ -1,6 +1,6 @@
<template>
<div v-on-clickaway="onClose" class="actions-container">
<div class="triangle" :style="cssVars">
<div class="triangle">
<svg height="12" viewBox="0 0 24 12" width="24">
<path
d="M20 12l-8-8-12 12"
@ -45,14 +45,12 @@
import { mixin as clickaway } from 'vue-clickaway';
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
import bulkActionsMixin from 'dashboard/mixins/bulkActionsMixin.js';
export default {
components: {
WootDropdownItem,
WootDropdownMenu,
},
mixins: [clickaway, bulkActionsMixin],
mixins: [clickaway],
props: {
selectedInboxes: {
type: Array,
@ -133,7 +131,7 @@ export default {
box-shadow: var(--shadow-dropdown-pane);
position: absolute;
right: var(--space-small);
top: var(--space-larger);
top: 48px;
transform-origin: top right;
width: auto;
z-index: var(--z-index-twenty);
@ -154,7 +152,7 @@ export default {
.triangle {
display: block;
position: absolute;
right: var(--triangle-position);
right: 2.8rem;
text-align: left;
top: calc(var(--space-slab) * -1);
z-index: var(--z-index-one);

View file

@ -1,136 +0,0 @@
<template>
<div
v-show="activeLabels.length"
ref="labelContainer"
class="label-container"
>
<div class="labels-wrap" :class="{ expand: showAllLabels }">
<woot-label
v-for="(label, index) in activeLabels"
:key="label.id"
:title="label.title"
:description="label.description"
:color="label.color"
variant="smooth"
small
:class="{ hidden: !showAllLabels && index > labelPosition }"
/>
<woot-button
v-if="showExpandLabelButton"
:title="
showAllLabels
? $t('CONVERSATION.CARD.HIDE_LABELS')
: $t('CONVERSATION.CARD.SHOW_LABELS')
"
class="show-more--button"
color-scheme="secondary"
variant="hollow"
:icon="showAllLabels ? 'chevron-left' : 'chevron-right'"
size="tiny"
@click="onShowLabels"
/>
</div>
</div>
</template>
<script>
import conversationLabelMixin from 'dashboard/mixins/conversation/labelMixin';
export default {
mixins: [conversationLabelMixin],
props: {
conversationId: {
type: Number,
required: true,
},
},
data() {
return {
showAllLabels: false,
showExpandLabelButton: false,
labelPosition: -1,
};
},
watch: {
activeLabels() {
this.$nextTick(() => this.computeVisibleLabelPosition());
},
},
mounted() {
this.computeVisibleLabelPosition();
},
methods: {
onShowLabels(e) {
e.stopPropagation();
this.showAllLabels = !this.showAllLabels;
},
computeVisibleLabelPosition() {
const labelContainer = this.$refs.labelContainer;
const labels = this.$refs.labelContainer.querySelectorAll('.label');
let labelOffset = 0;
this.showExpandLabelButton = false;
Array.from(labels).forEach((label, index) => {
labelOffset += label.offsetWidth + 8;
if (labelOffset < labelContainer.clientWidth - 16) {
this.labelPosition = index;
} else {
this.showExpandLabelButton = true;
}
});
},
},
};
</script>
<style lang="scss" scoped>
.show-more--button {
height: var(--space-two);
position: sticky;
flex-shrink: 0;
right: 0;
margin-right: var(--space-medium);
&.secondary:focus {
color: var(--s-700);
border-color: var(--s-300);
}
}
.label-container {
margin-top: var(--space-micro);
}
.labels-wrap {
display: flex;
align-items: center;
min-width: 0;
flex-shrink: 1;
&.expand {
height: auto;
overflow: visible;
flex-flow: row wrap;
.label {
margin-bottom: var(--space-smaller);
}
.show-more--button {
margin-bottom: var(--space-smaller);
}
}
.secondary {
border: 1px solid var(--s-100);
}
.label {
margin-bottom: 0;
}
}
.hidden {
visibility: hidden;
position: absolute;
}
</style>

View file

@ -22,6 +22,5 @@ export default {
EXPANDED: 'expanded',
},
DOCS_URL: '//www.chatwoot.com/docs/product/',
TESTIMONIAL_URL: 'https://testimonials.cdn.chatwoot.com/content.json',
};
export const DEFAULT_REDIRECT_URL = '/app/';

View file

@ -1,18 +1,13 @@
export const FEATURE_FLAGS = {
AGENT_BOTS: 'agent_bots',
AGENT_MANAGEMENT: 'agent_management',
AUTO_RESOLVE_CONVERSATIONS: 'auto_resolve_conversations',
AUTOMATIONS: 'automations',
CAMPAIGNS: 'campaigns',
CANNED_RESPONSES: 'canned_responses',
CRM: 'crm',
CUSTOM_ATTRIBUTES: 'custom_attributes',
INBOX_MANAGEMENT: 'inbox_management',
INTEGRATIONS: 'integrations',
LABELS: 'labels',
MACROS: 'macros',
HELP_CENTER: 'help_center',
REPORTS: 'reports',
TEAM_MANAGEMENT: 'team_management',
VOICE_RECORDER: 'voice_recorder',
};

View file

@ -1,9 +0,0 @@
export const EXECUTED_A_MACRO = 'Executed a macro';
export const SENT_MESSAGE = 'Sent a message';
export const SENT_PRIVATE_NOTE = 'Sent a private note';
export const INSERTED_A_CANNED_RESPONSE = 'Inserted a canned response';
export const USED_MENTIONS = 'Used mentions';
export const MERGED_CONTACTS = 'Used merge contact option';
export const ADDED_TO_CANNED_RESPONSE = 'Used added to canned response option';
export const ADDED_A_CUSTOM_ATTRIBUTE = 'Added a custom attribute';
export const ADDED_AN_INBOX = 'Added an inbox';

View file

@ -1,67 +0,0 @@
import { AnalyticsBrowser } from '@june-so/analytics-next';
class AnalyticsHelper {
constructor({ token: analyticsToken } = {}) {
this.analyticsToken = analyticsToken;
this.analytics = null;
this.user = {};
}
async init() {
if (!this.analyticsToken) {
return;
}
let [analytics] = await AnalyticsBrowser.load({
writeKey: this.analyticsToken,
});
this.analytics = analytics;
}
identify(user) {
if (!this.analytics) {
return;
}
this.user = user;
this.analytics.identify(this.user.email, {
userId: this.user.id,
email: this.user.email,
name: this.user.name,
avatar: this.user.avatar_url,
});
const { accounts, account_id: accountId } = this.user;
const [currentAccount] = accounts.filter(
account => account.id === accountId
);
if (currentAccount) {
this.analytics.group(currentAccount.id, this.user.id, {
name: currentAccount.name,
});
}
}
track(eventName, properties = {}) {
if (!this.analytics) {
return;
}
this.analytics.track({
userId: this.user.id,
event: eventName,
properties,
});
}
page(params) {
if (!this.analytics) {
return;
}
this.analytics.page(params);
}
}
export * as ANALYTICS_EVENTS from './events';
export default new AnalyticsHelper(window.analyticsConfig);

View file

@ -56,8 +56,6 @@ export const conversationUrl = ({
url = `accounts/${accountId}/custom_view/${foldersId}/conversations/${id}`;
} else if (conversationType === 'mention') {
url = `accounts/${accountId}/mentions/conversations/${id}`;
} else if (conversationType === 'unattended') {
url = `accounts/${accountId}/unattended/conversations/${id}`;
}
return url;
};
@ -68,23 +66,16 @@ export const conversationListPageURL = ({
inboxId,
label,
teamId,
customViewId,
}) => {
let url = `accounts/${accountId}/dashboard`;
if (label) {
url = `accounts/${accountId}/label/${label}`;
} else if (teamId) {
url = `accounts/${accountId}/team/${teamId}`;
} else if (conversationType === 'mention') {
url = `accounts/${accountId}/mentions/conversations`;
} else if (inboxId) {
url = `accounts/${accountId}/inbox/${inboxId}`;
} else if (customViewId) {
url = `accounts/${accountId}/custom_view/${customViewId}`;
} else if (conversationType) {
const urlMap = {
mention: 'mentions/conversations',
unattended: 'unattended/conversations',
};
url = `accounts/${accountId}/${urlMap[conversationType]}`;
}
return frontendURL(url);
};

View file

@ -17,22 +17,13 @@ const formatArray = params => {
return params;
};
const generatePayloadForObject = item => {
if (item.action_params.id) {
item.action_params = [item.action_params.id];
} else {
item.action_params = [item.action_params];
}
return item.action_params;
};
const generatePayload = data => {
const actions = JSON.parse(JSON.stringify(data));
let payload = actions.map(item => {
if (Array.isArray(item.action_params)) {
item.action_params = formatArray(item.action_params);
} else if (typeof item.action_params === 'object') {
item.action_params = generatePayloadForObject(item);
} else if (typeof item.values === 'object') {
item.action_params = [item.action_params.id];
} else if (!item.action_params) {
item.action_params = [];
} else {

View file

@ -1,242 +0,0 @@
import {
OPERATOR_TYPES_1,
OPERATOR_TYPES_3,
OPERATOR_TYPES_4,
} from 'dashboard/routes/dashboard/settings/automation/operators';
import filterQueryGenerator from './filterQueryGenerator';
import actionQueryGenerator from './actionQueryGenerator';
const MESSAGE_CONDITION_VALUES = [
{
id: 'incoming',
name: 'Incoming Message',
},
{
id: 'outgoing',
name: 'Outgoing Message',
},
];
export const getCustomAttributeInputType = key => {
const customAttributeMap = {
date: 'date',
text: 'plain_text',
list: 'search_select',
checkbox: 'search_select',
};
return customAttributeMap[key] || 'plain_text';
};
export const isACustomAttribute = (customAttributes, key) => {
return customAttributes.find(attr => {
return attr.attribute_key === key;
});
};
export const getCustomAttributeListDropdownValues = (
customAttributes,
type
) => {
return customAttributes
.find(attr => attr.attribute_key === type)
.attribute_values.map(item => {
return {
id: item,
name: item,
};
});
};
export const isCustomAttributeCheckbox = (customAttributes, key) => {
return customAttributes.find(attr => {
return (
attr.attribute_key === key && attr.attribute_display_type === 'checkbox'
);
});
};
export const isCustomAttributeList = (customAttributes, type) => {
return customAttributes.find(attr => {
return (
attr.attribute_key === type && attr.attribute_display_type === 'list'
);
});
};
export const getOperatorTypes = key => {
const operatorMap = {
list: OPERATOR_TYPES_1,
text: OPERATOR_TYPES_3,
number: OPERATOR_TYPES_1,
link: OPERATOR_TYPES_1,
date: OPERATOR_TYPES_4,
checkbox: OPERATOR_TYPES_1,
};
return operatorMap[key] || OPERATOR_TYPES_1;
};
export const generateCustomAttributeTypes = (customAttributes, type) => {
return customAttributes.map(attr => {
return {
key: attr.attribute_key,
name: attr.attribute_display_name,
inputType: getCustomAttributeInputType(attr.attribute_display_type),
filterOperators: getOperatorTypes(attr.attribute_display_type),
customAttributeType: type,
};
});
};
export const generateConditionOptions = (options, key = 'id') => {
return options.map(i => {
return {
id: i[key],
name: i.title,
};
});
};
export const getActionOptions = ({ teams, labels, type }) => {
const actionsMap = {
assign_team: teams,
send_email_to_team: teams,
add_label: generateConditionOptions(labels, 'title'),
};
return actionsMap[type];
};
export const getConditionOptions = ({
agents,
booleanFilterOptions,
campaigns,
contacts,
countries,
customAttributes,
inboxes,
languages,
statusFilterOptions,
teams,
type,
}) => {
if (isCustomAttributeCheckbox(customAttributes, type)) {
return booleanFilterOptions;
}
if (isCustomAttributeList(customAttributes, type)) {
return getCustomAttributeListDropdownValues(customAttributes, type);
}
const conditionFilterMaps = {
status: statusFilterOptions,
assignee_id: agents,
contact: contacts,
inbox_id: inboxes,
team_id: teams,
campaigns: generateConditionOptions(campaigns),
browser_language: languages,
country_code: countries,
message_type: MESSAGE_CONDITION_VALUES,
};
return conditionFilterMaps[type];
};
export const getFileName = (action, files = []) => {
const blobId = action.action_params[0];
if (!blobId) return '';
if (action.action_name === 'send_attachment') {
const file = files.find(item => item.blob_id === blobId);
if (file) return file.filename.toString();
}
return '';
};
export const getDefaultConditions = eventName => {
if (eventName === 'message_created') {
return [
{
attribute_key: 'message_type',
filter_operator: 'equal_to',
values: '',
query_operator: 'and',
custom_attribute_type: '',
},
];
}
return [
{
attribute_key: 'status',
filter_operator: 'equal_to',
values: '',
query_operator: 'and',
custom_attribute_type: '',
},
];
};
export const getDefaultActions = () => {
return [
{
action_name: 'assign_team',
action_params: [],
},
];
};
export const filterCustomAttributes = customAttributes => {
return customAttributes.map(attr => {
return {
key: attr.attribute_key,
name: attr.attribute_display_name,
type: attr.attribute_display_type,
};
});
};
export const getStandardAttributeInputType = (automationTypes, event, key) => {
return automationTypes[event].conditions.find(item => item.key === key)
.inputType;
};
export const generateAutomationPayload = payload => {
const automation = JSON.parse(JSON.stringify(payload));
automation.conditions[automation.conditions.length - 1].query_operator = null;
automation.conditions = filterQueryGenerator(automation.conditions).payload;
automation.actions = actionQueryGenerator(automation.actions);
return automation;
};
export const isCustomAttribute = (attrs, key) => {
return attrs.find(attr => attr.key === key);
};
export const generateCustomAttributes = (
conversationAttributes = [],
contactAttribtues = [],
conversationlabel,
contactlabel
) => {
const customAttributes = [];
if (conversationAttributes.length) {
customAttributes.push(
{
key: `conversation_custom_attribute`,
name: conversationlabel,
disabled: true,
},
...conversationAttributes
);
}
if (contactAttribtues.length) {
customAttributes.push(
{
key: `contact_custom_attribute`,
name: contactlabel,
disabled: true,
},
...contactAttribtues
);
}
return customAttributes;
};

View file

@ -1,3 +1,15 @@
const lowerCaseValues = (operator, values) => {
if (operator === 'equal_to' || operator === 'not_equal_to') {
values = values.map(val => {
if (typeof val === 'string') {
return val.toLowerCase();
}
return val;
});
}
return values;
};
const generatePayload = data => {
// Make a copy of data to avoid vue data reactivity issues
const filters = JSON.parse(JSON.stringify(data));
@ -11,6 +23,8 @@ const generatePayload = data => {
} else {
item.values = [item.values];
}
// Convert all values to lowerCase if operator_type is 'equal_to' or 'not_equal_to'
item.values = lowerCaseValues(item.filter_operator, item.values);
return item;
});
// For every query added, the query_operator is set default to and so the

View file

@ -60,11 +60,15 @@ export const getFormattedPreChatFields = ({ preChatFields }) => {
return {
...item,
label: getLabel({
key: item.name,
key: standardFieldKeys[item.name]
? standardFieldKeys[item.name].key
: item.name,
label: item.label ? item.label : item.name,
}),
placeholder: getPlaceHolder({
key: item.name,
key: standardFieldKeys[item.name]
? standardFieldKeys[item.name].key
: item.name,
placeholder: item.placeholder ? item.placeholder : item.name,
}),
};

View file

@ -1,4 +1,4 @@
import AnalyticsHelper from './AnalyticsHelper';
import posthog from 'posthog-js';
export const CHATWOOT_SET_USER = 'CHATWOOT_SET_USER';
export const CHATWOOT_RESET = 'CHATWOOT_RESET';
@ -8,9 +8,16 @@ export const ANALYTICS_RESET = 'ANALYTICS_RESET';
export const initializeAnalyticsEvents = () => {
window.bus.$on(ANALYTICS_IDENTITY, ({ user }) => {
AnalyticsHelper.identify(user);
if (window.analyticsConfig) {
posthog.identify(user.id, { name: user.name, email: user.email });
}
});
window.bus.$on(ANALYTICS_RESET, () => {
if (window.analyticsConfig) {
posthog.reset();
}
});
window.bus.$on(ANALYTICS_RESET, () => {});
};
export const initializeChatwootEvents = () => {

View file

@ -29,12 +29,6 @@ describe('#URL Helpers', () => {
'/app/accounts/1/team/1'
);
});
it('should return url to custom view', () => {
expect(conversationListPageURL({ accountId: 1, customViewId: 1 })).toBe(
'/app/accounts/1/custom_view/1'
);
});
});
describe('conversationUrl', () => {
it('should return direct conversation URL if activeInbox is nil', () => {

View file

@ -5,7 +5,7 @@ const testData = [
attribute_key: 'status',
filter_operator: 'equal_to',
values: [
{ id: 'pending', name: 'Pending' },
{ id: 'PENDING', name: 'Pending' },
{ id: 'resolved', name: 'Resolved' },
],
query_operator: 'and',
@ -18,7 +18,7 @@ const testData = [
account_id: 1,
auto_offline: true,
confirmed: true,
email: 'fayaz@test.com',
email: 'fayazara@gmail.com',
available_name: 'Fayaz',
name: 'Fayaz',
role: 'agent',
@ -52,7 +52,7 @@ const finalResult = {
{
attribute_key: 'id',
filter_operator: 'equal_to',
values: ['This is a test'],
values: ['this is a test'],
},
],
};

View file

@ -15,7 +15,6 @@ import id from './locale/id';
import it from './locale/it';
import ja from './locale/ja';
import ko from './locale/ko';
import lv from './locale/lv';
import ml from './locale/ml';
import nl from './locale/nl';
import no from './locale/no';
@ -53,7 +52,6 @@ export default {
ja,
ko,
ml,
lv,
nl,
no,
pl,

View file

@ -1,70 +1,5 @@
{
"AGENT_BOTS": {
"HEADER": "Bots",
"LOADING_EDITOR": "Loading Editor...",
"HEADER_BTN_TXT": "Add Bot Configuration",
"SIDEBAR_TXT": "<p><b>Agent Bots</b> <p>Agent bots allows you to automate the conversations</p>",
"CSML_BOT_EDITOR": {
"NAME": {
"LABEL": "Bot Name",
"PLACEHOLDER": "Give your bot a name",
"ERROR": "Bot name is required"
},
"DESCRIPTION": {
"LABEL": "Bot Description",
"PLACEHOLDER": "What does this bot do?"
},
"BOT_CONFIG": {
"ERROR": "Please enter your CSML bot configuration above",
"API_ERROR": "Your CSML configuration is invalid, please fix it and try again."
},
"SUBMIT": "Validate and save"
},
"BOT_CONFIGURATION": {
"TITLE": "Select an agent bot",
"DESC": "You can set an agent bot from the list to this inbox. The bot can initially handle the conversation and transfer it to an agent when needed.",
"SUBMIT": "تحديث",
"SUCCESS_MESSAGE": "Successfully updated the agent bot",
"ERROR_MESSAGE": "Could not update the agent bot, please try again later",
"SELECT_PLACEHOLDER": "Select Bot"
},
"ADD": {
"TITLE": "Configure new bot",
"CANCEL_BUTTON_TEXT": "إلغاء",
"API": {
"SUCCESS_MESSAGE": "Bot added successfully",
"ERROR_MESSAGE": "Could not add bot, Please try again later"
}
},
"LIST": {
"404": "No Bots found, you can create a bot by clicking the 'Configure new bot' Button ↗",
"LOADING": "Fetching Bots...",
"TYPE": "Bot Type"
},
"DELETE": {
"BUTTON_TEXT": "حذف",
"TITLE": "Delete Bot",
"SUBMIT": "حذف",
"CANCEL_BUTTON_TEXT": "إلغاء",
"DESCRIPTION": "Are you sure you want to delete this bot? This action is irreversible",
"API": {
"SUCCESS_MESSAGE": "Bot deleted successfully",
"ERROR_MESSAGE": "Could not able to delete bot, Please try again later"
}
},
"EDIT": {
"BUTTON_TEXT": "تعديل",
"LOADING": "Fetching Bots...",
"TITLE": "Edit Bot",
"CANCEL_BUTTON_TEXT": "إلغاء",
"API": {
"SUCCESS_MESSAGE": "Bot updated successfully",
"ERROR_MESSAGE": "Could not update bot, Please try again later"
}
},
"TYPES": {
"WEBHOOK": "Webhook Bot",
"CSML": "CSML Bot"
}
"HEADER": "Bots"
}
}

View file

@ -86,9 +86,7 @@
"RESET_MESSAGE": "تغيير نوع الحدث سوف يعيد تعيين الشروط والأحداث التي أضفتها أدناه"
},
"CONDITION": {
"DELETE_MESSAGE": "يجب أن يكون لديك على الأقل شرط واحد للحفظ",
"CONTACT_CUSTOM_ATTR_LABEL": "Contact Custom Attributes",
"CONVERSATION_CUSTOM_ATTR_LABEL": "Conversation Custom Attributes"
"DELETE_MESSAGE": "يجب أن يكون لديك على الأقل شرط واحد للحفظ"
},
"ACTION": {
"DELETE_MESSAGE": "يجب أن يكون لديك على الأقل شرط واحد للحفظ",

View file

@ -8,7 +8,6 @@
"ASSIGN_LABEL": "تكليف",
"YES": "نعم",
"ASSIGN_AGENT_TOOLTIP": "إسناد وكيل",
"ASSIGN_TEAM_TOOLTIP": "تعيين فريق",
"ASSIGN_SUCCESFUL": "تم تعيين المحادثات بنجاح",
"ASSIGN_FAILED": "فشل في تعيين المحادثات، الرجاء المحاولة مرة أخرى",
"RESOLVE_SUCCESFUL": "تم تسوية المحادثات بنجاح",
@ -27,14 +26,6 @@
"ASSIGN_SELECTED_LABELS": "تعيين التسميات المحددة",
"ASSIGN_SUCCESFUL": "تم تعيين التسميات بنجاح",
"ASSIGN_FAILED": "فشل في تعيين التسميات ، الرجاء المحاولة مرة أخرى"
},
"TEAMS": {
"TEAM_SELECT_LABEL": "اختيار فريق",
"NONE": "لا شيء",
"NO_TEAMS_AVAILABLE": "There are no teams added to this account yet.",
"ASSIGN_SELECTED_TEAMS": "Assign selected team",
"ASSIGN_SUCCESFUL": "Teams assiged successfully",
"ASSIGN_FAILED": "Failed to assign team, please try again"
}
}
}

View file

@ -8,7 +8,6 @@
},
"TAB_HEADING": "المحادثات",
"MENTION_HEADING": "الإشارات",
"UNATTENDED_HEADING": "بدون حضور",
"SEARCH": {
"INPUT": "البحث عن جهات الاتصال، المحادثات، قوالب الردود الجاهزة .."
},
@ -57,8 +56,6 @@
"REPLY_TO_TWEET": "الرد على هذه التغريدة",
"LINK_TO_STORY": "الذهاب إلى قصة الإنستقرام",
"SENT": "Sent successfully",
"READ": "Read successfully",
"DELIVERED": "Delivered successfully",
"NO_MESSAGES": "لا توجد رسائل",
"NO_CONTENT": "لم يتم العثور على محتوى",
"HIDE_QUOTED_TEXT": "Hide Quoted Text",

View file

@ -41,10 +41,6 @@
"NO_RESPONSE": "لا توجد استجابة",
"RATING_TITLE": "التقييم",
"FEEDBACK_TITLE": "الملاحظات",
"CARD": {
"SHOW_LABELS": "Show labels",
"HIDE_LABELS": "Hide labels"
},
"HEADER": {
"RESOLVE_ACTION": "إغلاق المحادثة",
"REOPEN_ACTION": "إعادة فتح",
@ -68,7 +64,6 @@
"CARD_CONTEXT_MENU": {
"PENDING": "تحديد كمعلق",
"RESOLVED": "تحديد كمحلولة",
"MARK_AS_UNREAD": "Mark as unread",
"REOPEN": "إعادة فتح المحادثة",
"SNOOZE": {
"TITLE": "غفوة",

View file

@ -1,6 +0,0 @@
{
"EMOJI": {
"PLACEHOLDER": "Search emojis",
"NOT_FOUND": "No emoji match your search"
}
}

View file

@ -23,7 +23,7 @@
"ERROR": "الرجاء إدخال اسم حساب صحيح"
},
"LANGUAGE": {
"LABEL": "Site language",
"LABEL": "لغة الموقع (تجريبي)",
"PLACEHOLDER": "اسم الحساب الخاص بك",
"ERROR": ""
},
@ -54,8 +54,7 @@
"MULTISELECT": {
"ENTER_TO_SELECT": "اضغط على زر الإدخال للاختيار",
"ENTER_TO_REMOVE": "اضغط على زر الإدخال للحذف",
"SELECT_ONE": "اختر واحدا",
"SELECT": "Select"
"SELECT_ONE": "اختر واحدا"
}
},
"NOTIFICATIONS_PAGE": {
@ -137,8 +136,5 @@
"UNTIL_NEXT_WEEK": "حتى الأسبوع القادم",
"UNTIL_TOMORROW": "حتى الغد"
}
},
"DASHBOARD_APPS": {
"LOADING_MESSAGE": "Loading Dashboard App..."
}
}

View file

@ -134,7 +134,7 @@
"PHONE_NUMBER": {
"LABEL": "رقم الهاتف",
"PLACEHOLDER": "الرجاء إدخال رقم الهاتف الذي سيتم إرسال الرسائل منه.",
"ERROR": "Please provide a valid phone number that starts with a `+` sign and does not contain any spaces."
"ERROR": "الرجاء إدخال قيمة صحيحة. يجب أن يبدأ رقم الهاتف بعلامة `+`."
},
"API_CALLBACK": {
"TITLE": "عنوان Callback URL",
@ -185,7 +185,7 @@
"PHONE_NUMBER": {
"LABEL": "رقم الهاتف",
"PLACEHOLDER": "الرجاء إدخال رقم الهاتف الذي سيتم إرسال الرسائل منه.",
"ERROR": "Please provide a valid phone number that starts with a `+` sign and does not contain any spaces."
"ERROR": "الرجاء إدخال قيمة صحيحة. يجب أن يبدأ رقم الهاتف بعلامة `+`."
},
"SUBMIT_BUTTON": "إنشاء قناة عرض التردد",
"API": {
@ -214,7 +214,7 @@
"PHONE_NUMBER": {
"LABEL": "رقم الهاتف",
"PLACEHOLDER": "الرجاء إدخال رقم الهاتف الذي سيتم إرسال الرسائل منه.",
"ERROR": "Please provide a valid phone number that starts with a `+` sign and does not contain any spaces."
"ERROR": "الرجاء إدخال قيمة صحيحة. يجب أن يبدأ رقم الهاتف بعلامة `+`."
},
"PHONE_NUMBER_ID": {
"LABEL": "رقم الهاتف",
@ -388,10 +388,6 @@
"ENABLED": "مفعل",
"DISABLED": "معطّل"
},
"LOCK_TO_SINGLE_CONVERSATION": {
"ENABLED": "مفعل",
"DISABLED": "معطّل"
},
"ENABLE_HMAC": {
"LABEL": "تمكين"
}
@ -420,8 +416,7 @@
"CAMPAIGN": "الحملات",
"PRE_CHAT_FORM": "نموذج ما قبل الدردشة",
"BUSINESS_HOURS": "ساعات العمل",
"WIDGET_BUILDER": "منشئ اللايف شات",
"BOT_CONFIGURATION": "Bot Configuration"
"WIDGET_BUILDER": "منشئ اللايف شات"
},
"SETTINGS": "الإعدادات",
"FEATURES": {
@ -445,8 +440,6 @@
"ENABLE_CSAT_SUB_TEXT": "تمكين/تعطيل تقييم خدمة العملاء بعد إنتهاء المحادثة",
"ENABLE_CONTINUITY_VIA_EMAIL": "تمكين استمرارية المحادثة عبر البريد الإلكتروني",
"ENABLE_CONTINUITY_VIA_EMAIL_SUB_TEXT": "المحادثات ستستمر عبر البريد الإلكتروني إذا كان عنوان البريد الإلكتروني لجهة الاتصال متاحاً.",
"LOCK_TO_SINGLE_CONVERSATION": "Lock to single conversation",
"LOCK_TO_SINGLE_CONVERSATION_SUB_TEXT": "Enable or disable multiple conversations for the same contact in this inbox",
"INBOX_UPDATE_TITLE": "إعدادات قناة التواصل",
"INBOX_UPDATE_SUB_TEXT": "تحديث إعدادات قناة التواصل",
"AUTO_ASSIGNMENT_SUB_TEXT": "تمكين أو تعطيل الإسناد التلقائي للمحادثات الجديدة إلى الموظفين المضافين إلى قناة التواصل هذه.",

View file

@ -103,9 +103,7 @@
"متصل",
"مشغول",
"غير متصل"
],
"SET_AVAILABILITY_SUCCESS": "Availability has been set successfully",
"SET_AVAILABILITY_ERROR": "Couldn't set availability, please try again"
]
},
"EMAIL": {
"LABEL": "عنوان البريد الإلكتروني الخاص بك",
@ -136,7 +134,6 @@
"SELECTOR_SUBTITLE": "اختر حساباً من القائمة التالية",
"PROFILE_SETTINGS": "إعدادات الملف الشخصي",
"KEYBOARD_SHORTCUTS": "اختصارات لوحة المفاتيح",
"SUPER_ADMIN_CONSOLE": "Super Admin Console",
"LOGOUT": "تسجيل الخروج"
},
"APP_GLOBAL": {
@ -161,9 +158,6 @@
"DOWNLOAD": "تنزيل",
"UPLOADING": "جاري الرفع..."
},
"LOCATION_BUBBLE": {
"SEE_ON_MAP": "See on map"
},
"FORM_BUBBLE": {
"SUBMIT": "إرسال"
}
@ -180,7 +174,6 @@
"CONVERSATIONS": "المحادثات",
"ALL_CONVERSATIONS": "كل المحادثات",
"MENTIONED_CONVERSATIONS": "الإشارات",
"UNATTENDED_CONVERSATIONS": "بدون حضور",
"REPORTS": "التقارير",
"SETTINGS": "الإعدادات",
"CONTACTS": "جهات الاتصال",
@ -229,10 +222,6 @@
"CATEGORY": "الفئة",
"CATEGORY_EMPTY_MESSAGE": "لم يتم العثور على فئات"
},
"SET_AUTO_OFFLINE": {
"TEXT": "Mark offline automatically",
"INFO_TEXT": "Let the system automatically mark you offline when you aren't using the app or dashboard."
},
"DOCS": "قراءة المستندات"
},
"BILLING_SETTINGS": {
@ -264,7 +253,7 @@
},
"FORM": {
"NAME": {
"LABEL": "اسم الشركة",
"LABEL": "اسم الحساب",
"PLACEHOLDER": "مؤسسة Wayne"
},
"SUBMIT": "إرسال"

View file

@ -2,13 +2,11 @@
"REGISTER": {
"TRY_WOOT": "تسجيل حساب",
"TITLE": "تسجيل",
"TESTIMONIAL_HEADER": "All it takes is one step to move forward",
"TESTIMONIAL_CONTENT": "You're one step away from engaging your customers, retaining them and finding new ones.",
"TERMS_ACCEPT": "من خلال التسجيل، فإنك توافق على <a href=\"https://www.chatwoot.com/terms\">شروط الخدمة</a> و <a href=\"https://www.chatwoot.com/privacy-policy\">سياسة الخصوصية</a>",
"COMPANY_NAME": {
"LABEL": "Company name",
"PLACEHOLDER": "Enter your company name. eg: Wayne Enterprises",
"ERROR": "Company name is too short"
"ACCOUNT_NAME": {
"LABEL": "اسم الحساب",
"PLACEHOLDER": "أدخل اسم الحساب. مثال: Wayne Enterprises",
"ERROR": "اسم الحساب قصير جداً"
},
"FULL_NAME": {
"LABEL": "الاسم الكامل",
@ -18,7 +16,7 @@
"EMAIL": {
"LABEL": "البريد الإلكتروني للعمل",
"PLACEHOLDER": "أدخل عنوان بريدك الإلكتروني للعمل. مثال: bruce@wayne.enterprises",
"ERROR": "Please enter a valid work email address"
"ERROR": "عنوان البريد الإلكتروني غير صالح"
},
"PASSWORD": {
"LABEL": "كلمة المرور",

View file

@ -1,70 +1,5 @@
{
"AGENT_BOTS": {
"HEADER": "Bots",
"LOADING_EDITOR": "Loading Editor...",
"HEADER_BTN_TXT": "Add Bot Configuration",
"SIDEBAR_TXT": "<p><b>Agent Bots</b> <p>Agent bots allows you to automate the conversations</p>",
"CSML_BOT_EDITOR": {
"NAME": {
"LABEL": "Bot Name",
"PLACEHOLDER": "Give your bot a name",
"ERROR": "Bot name is required"
},
"DESCRIPTION": {
"LABEL": "Bot Description",
"PLACEHOLDER": "What does this bot do?"
},
"BOT_CONFIG": {
"ERROR": "Please enter your CSML bot configuration above",
"API_ERROR": "Your CSML configuration is invalid, please fix it and try again."
},
"SUBMIT": "Validate and save"
},
"BOT_CONFIGURATION": {
"TITLE": "Select an agent bot",
"DESC": "You can set an agent bot from the list to this inbox. The bot can initially handle the conversation and transfer it to an agent when needed.",
"SUBMIT": "Обновяване",
"SUCCESS_MESSAGE": "Successfully updated the agent bot",
"ERROR_MESSAGE": "Could not update the agent bot, please try again later",
"SELECT_PLACEHOLDER": "Select Bot"
},
"ADD": {
"TITLE": "Configure new bot",
"CANCEL_BUTTON_TEXT": "Отмени",
"API": {
"SUCCESS_MESSAGE": "Bot added successfully",
"ERROR_MESSAGE": "Could not add bot, Please try again later"
}
},
"LIST": {
"404": "No Bots found, you can create a bot by clicking the 'Configure new bot' Button ↗",
"LOADING": "Fetching Bots...",
"TYPE": "Bot Type"
},
"DELETE": {
"BUTTON_TEXT": "Изтрий",
"TITLE": "Delete Bot",
"SUBMIT": "Изтрий",
"CANCEL_BUTTON_TEXT": "Отмени",
"DESCRIPTION": "Are you sure you want to delete this bot? This action is irreversible",
"API": {
"SUCCESS_MESSAGE": "Bot deleted successfully",
"ERROR_MESSAGE": "Could not able to delete bot, Please try again later"
}
},
"EDIT": {
"BUTTON_TEXT": "Редактирай",
"LOADING": "Fetching Bots...",
"TITLE": "Edit Bot",
"CANCEL_BUTTON_TEXT": "Отмени",
"API": {
"SUCCESS_MESSAGE": "Bot updated successfully",
"ERROR_MESSAGE": "Could not update bot, Please try again later"
}
},
"TYPES": {
"WEBHOOK": "Webhook Bot",
"CSML": "CSML Bot"
}
"HEADER": "Bots"
}
}

View file

@ -86,9 +86,7 @@
"RESET_MESSAGE": "Changing event type will reset the conditions and events you have added below"
},
"CONDITION": {
"DELETE_MESSAGE": "You need to have atleast one condition to save",
"CONTACT_CUSTOM_ATTR_LABEL": "Contact Custom Attributes",
"CONVERSATION_CUSTOM_ATTR_LABEL": "Conversation Custom Attributes"
"DELETE_MESSAGE": "You need to have atleast one condition to save"
},
"ACTION": {
"DELETE_MESSAGE": "You need to have atleast one action to save",

View file

@ -8,7 +8,6 @@
"ASSIGN_LABEL": "Assign",
"YES": "Yes",
"ASSIGN_AGENT_TOOLTIP": "Assign Agent",
"ASSIGN_TEAM_TOOLTIP": "Assign team",
"ASSIGN_SUCCESFUL": "Conversations assigned successfully",
"ASSIGN_FAILED": "Failed to assign conversations, please try again",
"RESOLVE_SUCCESFUL": "Conversations resolved successfully",
@ -27,14 +26,6 @@
"ASSIGN_SELECTED_LABELS": "Assign selected labels",
"ASSIGN_SUCCESFUL": "Labels assigned successfully",
"ASSIGN_FAILED": "Failed to assign labels, please try again"
},
"TEAMS": {
"TEAM_SELECT_LABEL": "Select Team",
"NONE": "Нито един",
"NO_TEAMS_AVAILABLE": "There are no teams added to this account yet.",
"ASSIGN_SELECTED_TEAMS": "Assign selected team",
"ASSIGN_SUCCESFUL": "Teams assiged successfully",
"ASSIGN_FAILED": "Failed to assign team, please try again"
}
}
}

View file

@ -8,7 +8,6 @@
},
"TAB_HEADING": "Разговори",
"MENTION_HEADING": "Споменавания",
"UNATTENDED_HEADING": "Unattended",
"SEARCH": {
"INPUT": "Търсене на хора, чатове, запазени отговори .."
},
@ -57,8 +56,6 @@
"REPLY_TO_TWEET": "Отговори на този туит",
"LINK_TO_STORY": "Go to instagram story",
"SENT": "Успено изпратено",
"READ": "Read successfully",
"DELIVERED": "Delivered successfully",
"NO_MESSAGES": "Няма съобщения",
"NO_CONTENT": "Няма налично съдържание",
"HIDE_QUOTED_TEXT": "Скриване на цитирания текст",

View file

@ -41,10 +41,6 @@
"NO_RESPONSE": "No response",
"RATING_TITLE": "Rating",
"FEEDBACK_TITLE": "Feedback",
"CARD": {
"SHOW_LABELS": "Show labels",
"HIDE_LABELS": "Hide labels"
},
"HEADER": {
"RESOLVE_ACTION": "Resolve",
"REOPEN_ACTION": "Reopen",
@ -68,7 +64,6 @@
"CARD_CONTEXT_MENU": {
"PENDING": "Mark as pending",
"RESOLVED": "Mark as resolved",
"MARK_AS_UNREAD": "Mark as unread",
"REOPEN": "Reopen conversation",
"SNOOZE": {
"TITLE": "Snooze",

View file

@ -1,6 +0,0 @@
{
"EMOJI": {
"PLACEHOLDER": "Search emojis",
"NOT_FOUND": "No emoji match your search"
}
}

View file

@ -23,7 +23,7 @@
"ERROR": "Please enter a valid account name"
},
"LANGUAGE": {
"LABEL": "Site language",
"LABEL": "Site language (Beta)",
"PLACEHOLDER": "Your account name",
"ERROR": ""
},
@ -54,8 +54,7 @@
"MULTISELECT": {
"ENTER_TO_SELECT": "Press enter to select",
"ENTER_TO_REMOVE": "Press enter to remove",
"SELECT_ONE": "Select one",
"SELECT": "Select"
"SELECT_ONE": "Select one"
}
},
"NOTIFICATIONS_PAGE": {
@ -137,8 +136,5 @@
"UNTIL_NEXT_WEEK": "Until next week",
"UNTIL_TOMORROW": "Until tomorrow"
}
},
"DASHBOARD_APPS": {
"LOADING_MESSAGE": "Loading Dashboard App..."
}
}

View file

@ -134,7 +134,7 @@
"PHONE_NUMBER": {
"LABEL": "Phone number",
"PLACEHOLDER": "Please enter the phone number from which message will be sent.",
"ERROR": "Please provide a valid phone number that starts with a `+` sign and does not contain any spaces."
"ERROR": "Please enter a valid value. Phone number should start with `+` sign."
},
"API_CALLBACK": {
"TITLE": "Callback URL",
@ -185,7 +185,7 @@
"PHONE_NUMBER": {
"LABEL": "Телефон",
"PLACEHOLDER": "Please enter the phone number from which message will be sent.",
"ERROR": "Please provide a valid phone number that starts with a `+` sign and does not contain any spaces."
"ERROR": "Please enter a valid value. Phone number should start with `+` sign."
},
"SUBMIT_BUTTON": "Create Bandwidth Channel",
"API": {
@ -214,7 +214,7 @@
"PHONE_NUMBER": {
"LABEL": "Phone number",
"PLACEHOLDER": "Please enter the phone number from which message will be sent.",
"ERROR": "Please provide a valid phone number that starts with a `+` sign and does not contain any spaces."
"ERROR": "Please enter a valid value. Phone number should start with `+` sign."
},
"PHONE_NUMBER_ID": {
"LABEL": "Phone number ID",
@ -388,10 +388,6 @@
"ENABLED": "Включен",
"DISABLED": "Изключен"
},
"LOCK_TO_SINGLE_CONVERSATION": {
"ENABLED": "Включен",
"DISABLED": "Изключен"
},
"ENABLE_HMAC": {
"LABEL": "Enable"
}
@ -420,8 +416,7 @@
"CAMPAIGN": "Campaigns",
"PRE_CHAT_FORM": "Pre Chat Form",
"BUSINESS_HOURS": "Business Hours",
"WIDGET_BUILDER": "Widget Builder",
"BOT_CONFIGURATION": "Bot Configuration"
"WIDGET_BUILDER": "Widget Builder"
},
"SETTINGS": "Settings",
"FEATURES": {
@ -445,8 +440,6 @@
"ENABLE_CSAT_SUB_TEXT": "Enable/Disable CSAT(Customer satisfaction) survey after resolving a conversation",
"ENABLE_CONTINUITY_VIA_EMAIL": "Enable conversation continuity via email",
"ENABLE_CONTINUITY_VIA_EMAIL_SUB_TEXT": "Conversations will continue over email if the contact email address is available.",
"LOCK_TO_SINGLE_CONVERSATION": "Lock to single conversation",
"LOCK_TO_SINGLE_CONVERSATION_SUB_TEXT": "Enable or disable multiple conversations for the same contact in this inbox",
"INBOX_UPDATE_TITLE": "Inbox Settings",
"INBOX_UPDATE_SUB_TEXT": "Update your inbox settings",
"AUTO_ASSIGNMENT_SUB_TEXT": "Enable or disable the automatic assignment of new conversations to the agents added to this inbox.",

Some files were not shown because too many files have changed in this diff Show more