Merge branch 'release/1.18.0'
This commit is contained in:
@@ -7,7 +7,7 @@ defaults: &defaults
|
||||
working_directory: ~/build
|
||||
docker:
|
||||
# specify the version you desire here
|
||||
- image: circleci/ruby:2.7.2-node-browsers
|
||||
- image: circleci/ruby:2.7.3-node-browsers
|
||||
|
||||
# Specify service dependencies here if necessary
|
||||
# CircleCI maintains a library of pre-built images
|
||||
|
||||
@@ -30,6 +30,5 @@ exclude_patterns:
|
||||
- "**/*.md"
|
||||
- "**/*.yml"
|
||||
- "app/javascript/dashboard/i18n/locale"
|
||||
- "stories/**/*"
|
||||
- "**/*.stories.js"
|
||||
- "**/stories/"
|
||||
- "stories/"
|
||||
|
||||
@@ -1,51 +1,8 @@
|
||||
# pre-build stage
|
||||
ARG VARIANT=2.7
|
||||
FROM mcr.microsoft.com/vscode/devcontainers/ruby:${VARIANT}
|
||||
|
||||
# Update args in docker-compose.yaml to set the UID/GID of the "vscode" user.
|
||||
ARG USER_UID=1000
|
||||
ARG USER_GID=$USER_UID
|
||||
RUN if [ "$USER_GID" != "1000" ] || [ "$USER_UID" != "1000" ]; then \
|
||||
groupmod --gid $USER_GID vscode \
|
||||
&& usermod --uid $USER_UID --gid $USER_GID vscode \
|
||||
&& chmod -R $USER_UID:$USER_GID /home/vscode; \
|
||||
fi
|
||||
|
||||
# [Option] Install Node.js
|
||||
ARG INSTALL_NODE="true"
|
||||
ARG NODE_VERSION="lts/*"
|
||||
RUN if [ "${INSTALL_NODE}" = "true" ]; then su vscode -c "source /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi
|
||||
|
||||
|
||||
# tmux is for overmind
|
||||
# TODO : install foreman in future
|
||||
# packages: postgresql-server-dev-all
|
||||
# may be postgres in same machine
|
||||
|
||||
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
||||
&& apt-get -y install --no-install-recommends \
|
||||
libssl-dev \
|
||||
tar \
|
||||
tzdata \
|
||||
postgresql-client \
|
||||
yarn \
|
||||
git \
|
||||
imagemagick \
|
||||
tmux \
|
||||
zsh
|
||||
|
||||
# [Optional] Uncomment this line to install global node packages.
|
||||
# RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g <your-package-here>" 2>&1
|
||||
|
||||
# The below image is created out of the Dockerfile.base
|
||||
# It has the dependencies already installed so that codespace will boot up fast
|
||||
FROM ghcr.io/chatwoot/chatwoot_codespace:latest
|
||||
|
||||
# Do the set up required for chatwoot app
|
||||
WORKDIR /workspace
|
||||
COPY . /workspace
|
||||
|
||||
# TODO: figure out installing rvm
|
||||
# RUN rvm install
|
||||
COPY Gemfile Gemfile.lock ./
|
||||
RUN gem install bundler
|
||||
RUN bundle install
|
||||
COPY package.json yarn.lock ./
|
||||
RUN yarn install
|
||||
RUN yarn && gem install bundler && bundle install
|
||||
|
||||
45
.devcontainer/Dockerfile.base
Normal file
45
.devcontainer/Dockerfile.base
Normal file
@@ -0,0 +1,45 @@
|
||||
# pre-build stage
|
||||
ARG VARIANT=2.7
|
||||
FROM mcr.microsoft.com/vscode/devcontainers/ruby:${VARIANT}
|
||||
|
||||
# Update args in docker-compose.yaml to set the UID/GID of the "vscode" user.
|
||||
ARG USER_UID=1000
|
||||
ARG USER_GID=$USER_UID
|
||||
RUN if [ "$USER_GID" != "1000" ] || [ "$USER_UID" != "1000" ]; then \
|
||||
groupmod --gid $USER_GID vscode \
|
||||
&& usermod --uid $USER_UID --gid $USER_GID vscode \
|
||||
&& chmod -R $USER_UID:$USER_GID /home/vscode; \
|
||||
fi
|
||||
|
||||
# [Option] Install Node.js
|
||||
ARG INSTALL_NODE="true"
|
||||
ARG NODE_VERSION="lts/*"
|
||||
RUN if [ "${INSTALL_NODE}" = "true" ]; then su vscode -c "source /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi
|
||||
|
||||
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
||||
&& apt-get -y install --no-install-recommends \
|
||||
libssl-dev \
|
||||
tar \
|
||||
tzdata \
|
||||
postgresql-client \
|
||||
yarn \
|
||||
git \
|
||||
imagemagick \
|
||||
tmux \
|
||||
zsh
|
||||
|
||||
|
||||
# Install overmind
|
||||
RUN curl -L https://github.com/DarthSim/overmind/releases/download/v2.1.0/overmind-v2.1.0-linux-amd64.gz > overmind.gz \
|
||||
&& gunzip overmind.gz \
|
||||
&& sudo mv overmind /usr/local/bin \
|
||||
&& chmod +x /usr/local/bin/overmind
|
||||
|
||||
# Do the set up required for chatwoot app
|
||||
WORKDIR /workspace
|
||||
COPY . /workspace
|
||||
RUN yarn
|
||||
|
||||
COPY Gemfile Gemfile.lock ./
|
||||
RUN gem install bundler && bundle install
|
||||
|
||||
@@ -12,22 +12,28 @@
|
||||
"extensions": [
|
||||
"rebornix.Ruby",
|
||||
"misogi.ruby-rubocop",
|
||||
"wingrunr21.vscode-ruby"
|
||||
"wingrunr21.vscode-ruby",
|
||||
"davidpallinder.rails-test-runner",
|
||||
"eamodio.gitlens",
|
||||
"github.copilot",
|
||||
"mrmlnc.vscode-duplicate"
|
||||
],
|
||||
|
||||
|
||||
// TODO: figure whether we can get all this ports work properly
|
||||
|
||||
// 3000 rails
|
||||
// 3035 webpacker
|
||||
// 5432 postgres
|
||||
// 6379 redis
|
||||
// 1025,8025 mailhog
|
||||
"forwardPorts": [5432, 6379, 1025, 8025],
|
||||
"forwardPorts": [8025],
|
||||
//your application may need to listen on all interfaces (0.0.0.0) not just localhost for it to be available externally. Defaults to []
|
||||
"appPort": [3000, 3035],
|
||||
|
||||
// Use 'postCreateCommand' to run commands after the container is created.
|
||||
// #TODO: can we move logic of copy env file into dockerfile ?
|
||||
"postCreateCommand": "cp .env.example .env",
|
||||
"postCreateCommand": ".devcontainer/scripts/setup.sh && bundle exec rake db:chatwoot_prepare && yarn",
|
||||
"portsAttributes": {
|
||||
"3000": {
|
||||
"label": "Rails Server"
|
||||
},
|
||||
"8025": {
|
||||
"label": "Mailhog UI"
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
8
.devcontainer/scripts/setup.sh
Executable file
8
.devcontainer/scripts/setup.sh
Executable file
@@ -0,0 +1,8 @@
|
||||
cp .env.example .env
|
||||
sed -i -e '/REDIS_URL/ s/=.*/=redis:\/\/localhost:6379/' .env
|
||||
sed -i -e '/POSTGRES_HOST/ s/=.*/=localhost/' .env
|
||||
sed -i -e '/SMTP_ADDRESS/ s/=.*/=localhost/' .env
|
||||
sed -i -e "/FRONTEND_URL/ s/=.*/=https:\/\/$CODESPACE_NAME-3000.githubpreview.dev/" .env
|
||||
sed -i -e "/WEBPACKER_DEV_SERVER_PUBLIC/ s/=.*/=https:\/\/$CODESPACE_NAME-3035.githubpreview.dev/" .env
|
||||
# uncomment the webpacker env variable
|
||||
sed -i -e '/WEBPACKER_DEV_SERVER_PUBLIC/s/^# //' .env
|
||||
@@ -155,3 +155,5 @@ USE_INBOX_AVATAR_FOR_BOT=true
|
||||
## Development Only Config
|
||||
# if you want to use letter_opener for local emails
|
||||
# LETTER_OPENER=true
|
||||
# meant to be used in github codespaces
|
||||
# WEBPACKER_DEV_SERVER_PUBLIC=
|
||||
|
||||
23
.github/workflows/publish_codespace_image.yml
vendored
Normal file
23
.github/workflows/publish_codespace_image.yml
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
name: Publish Codespace Base Image
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
publish-code-space-image:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build the Codespace Base Image
|
||||
run: |
|
||||
docker build . -t ghcr.io/chatwoot/chatwoot_codespace:latest -f .devcontainer/Dockerfile.base
|
||||
docker push ghcr.io/chatwoot/chatwoot_codespace:latest
|
||||
@@ -1 +1 @@
|
||||
2.7.2
|
||||
2.7.3
|
||||
|
||||
@@ -3,6 +3,7 @@ import Vue from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
import VueI18n from 'vue-i18n';
|
||||
import Vuelidate from 'vuelidate';
|
||||
import Multiselect from 'vue-multiselect';
|
||||
|
||||
import WootUiKit from '../app/javascript/dashboard/components';
|
||||
import i18n from '../app/javascript/dashboard/i18n';
|
||||
@@ -13,6 +14,7 @@ Vue.use(VueI18n);
|
||||
Vue.use(Vuelidate);
|
||||
Vue.use(WootUiKit);
|
||||
Vue.use(Vuex);
|
||||
Vue.component('multiselect', Multiselect);
|
||||
|
||||
const store = new Vuex.Store({});
|
||||
const i18nConfig = new VueI18n({
|
||||
|
||||
3
Gemfile
3
Gemfile
@@ -1,6 +1,6 @@
|
||||
source 'https://rubygems.org'
|
||||
|
||||
ruby '2.7.2'
|
||||
ruby '2.7.3'
|
||||
|
||||
##-- base gems for rails --##
|
||||
gem 'rack-cors', require: 'rack/cors'
|
||||
@@ -53,6 +53,7 @@ gem 'activerecord-import'
|
||||
gem 'dotenv-rails'
|
||||
gem 'foreman'
|
||||
gem 'puma'
|
||||
gem 'rack-timeout'
|
||||
gem 'webpacker', '~> 5.x'
|
||||
# metrics on heroku
|
||||
gem 'barnes'
|
||||
|
||||
@@ -75,7 +75,7 @@ GEM
|
||||
zeitwerk (~> 2.2, >= 2.2.2)
|
||||
acts-as-taggable-on (6.5.0)
|
||||
activerecord (>= 5.0, < 6.1)
|
||||
addressable (2.7.0)
|
||||
addressable (2.8.0)
|
||||
public_suffix (>= 2.0.2, < 5.0)
|
||||
administrate (0.16.0)
|
||||
actionpack (>= 5.0)
|
||||
@@ -384,6 +384,7 @@ GEM
|
||||
rack
|
||||
rack-test (1.1.0)
|
||||
rack (>= 1.0, < 3)
|
||||
rack-timeout (0.6.0)
|
||||
rails (6.0.3.7)
|
||||
actioncable (= 6.0.3.7)
|
||||
actionmailbox (= 6.0.3.7)
|
||||
@@ -666,6 +667,7 @@ DEPENDENCIES
|
||||
puma
|
||||
pundit
|
||||
rack-cors
|
||||
rack-timeout
|
||||
rails
|
||||
redis
|
||||
redis-namespace
|
||||
@@ -704,7 +706,7 @@ DEPENDENCIES
|
||||
wisper (= 2.0.0)
|
||||
|
||||
RUBY VERSION
|
||||
ruby 2.7.2p137
|
||||
ruby 2.7.3p183
|
||||
|
||||
BUNDLED WITH
|
||||
2.1.4
|
||||
|
||||
@@ -2,6 +2,10 @@ class ContactMergeAction
|
||||
pattr_initialize [:account!, :base_contact!, :mergee_contact!]
|
||||
|
||||
def perform
|
||||
# This case happens when an agent updates a contact email in dashboard,
|
||||
# while the contact also update his email via email collect box
|
||||
return @base_contact if base_contact.id == mergee_contact.id
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
validate_contacts
|
||||
merge_conversations
|
||||
|
||||
28
app/builders/csat_surveys/response_builder.rb
Normal file
28
app/builders/csat_surveys/response_builder.rb
Normal file
@@ -0,0 +1,28 @@
|
||||
class CsatSurveys::ResponseBuilder
|
||||
pattr_initialize [:message]
|
||||
|
||||
def perform
|
||||
raise 'Invalid Message' unless message.input_csat?
|
||||
|
||||
conversation = message.conversation
|
||||
rating = message.content_attributes.dig('submitted_values', 'csat_survey_response', 'rating')
|
||||
feedback_message = message.content_attributes.dig('submitted_values', 'csat_survey_response', 'feedback_message')
|
||||
|
||||
return if rating.blank?
|
||||
|
||||
process_csat_response(conversation, rating, feedback_message)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def process_csat_response(conversation, rating, feedback_message)
|
||||
csat_survey_response = message.csat_survey_response || CsatSurveyResponse.new(
|
||||
message_id: message.id, account_id: message.account_id, conversation_id: message.conversation_id,
|
||||
contact_id: conversation.contact_id, assigned_agent: conversation.assignee
|
||||
)
|
||||
csat_survey_response.rating = rating
|
||||
csat_survey_response.feedback_message = feedback_message
|
||||
csat_survey_response.save!
|
||||
csat_survey_response
|
||||
end
|
||||
end
|
||||
@@ -1,4 +1,5 @@
|
||||
class V2::ReportBuilder
|
||||
include DateRangeHelper
|
||||
attr_reader :account, :params
|
||||
|
||||
def initialize(account, params)
|
||||
@@ -83,10 +84,6 @@ class V2::ReportBuilder
|
||||
.average(:value)
|
||||
end
|
||||
|
||||
def range
|
||||
parse_date_time(params[:since])..parse_date_time(params[:until])
|
||||
end
|
||||
|
||||
# Taking average of average is not too accurate
|
||||
# https://en.wikipedia.org/wiki/Simpson's_paradox
|
||||
# TODO: Will optimize this later
|
||||
@@ -101,11 +98,4 @@ class V2::ReportBuilder
|
||||
|
||||
(avg_first_response_time.values.sum / avg_first_response_time.values.length)
|
||||
end
|
||||
|
||||
def parse_date_time(datetime)
|
||||
return datetime if datetime.is_a?(DateTime)
|
||||
return datetime.to_datetime if datetime.is_a?(Time) || datetime.is_a?(Date)
|
||||
|
||||
DateTime.strptime(datetime, '%s')
|
||||
end
|
||||
end
|
||||
|
||||
@@ -28,6 +28,7 @@ class Api::V1::Accounts::CampaignsController < Api::V1::Accounts::BaseController
|
||||
end
|
||||
|
||||
def campaign_params
|
||||
params.require(:campaign).permit(:title, :description, :message, :enabled, :inbox_id, :sender_id, trigger_rules: {})
|
||||
params.require(:campaign).permit(:title, :description, :message, :enabled, :inbox_id, :sender_id,
|
||||
:scheduled_at, audience: [:type, :id], trigger_rules: {})
|
||||
end
|
||||
end
|
||||
|
||||
@@ -7,7 +7,6 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
||||
sort_on :last_activity_at, type: :datetime
|
||||
|
||||
RESULTS_PER_PAGE = 15
|
||||
protect_from_forgery with: :null_session
|
||||
|
||||
before_action :check_authorization
|
||||
before_action :set_current_page, only: [:index, :active, :search]
|
||||
@@ -15,7 +14,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
||||
|
||||
def index
|
||||
@contacts_count = resolved_contacts.count
|
||||
@contacts = fetch_contact_last_seen_at(resolved_contacts)
|
||||
@contacts = fetch_contacts_with_conversation_count(resolved_contacts)
|
||||
end
|
||||
|
||||
def search
|
||||
@@ -26,7 +25,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
||||
search: "%#{params[:q]}%"
|
||||
)
|
||||
@contacts_count = contacts.count
|
||||
@contacts = fetch_contact_last_seen_at(contacts)
|
||||
@contacts = fetch_contacts_with_conversation_count(contacts)
|
||||
end
|
||||
|
||||
def import
|
||||
@@ -72,17 +71,22 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
||||
|
||||
private
|
||||
|
||||
# TODO: Move this to a finder class
|
||||
def resolved_contacts
|
||||
@resolved_contacts ||= Current.account.contacts
|
||||
.where.not(email: [nil, ''])
|
||||
.or(Current.account.contacts.where.not(phone_number: [nil, '']))
|
||||
return @resolved_contacts if @resolved_contacts
|
||||
|
||||
@resolved_contacts = Current.account.contacts
|
||||
.where.not(email: [nil, ''])
|
||||
.or(Current.account.contacts.where.not(phone_number: [nil, '']))
|
||||
@resolved_contacts = @resolved_contacts.tagged_with(params[:labels], any: true) if params[:labels].present?
|
||||
@resolved_contacts
|
||||
end
|
||||
|
||||
def set_current_page
|
||||
@current_page = params[:page] || 1
|
||||
end
|
||||
|
||||
def fetch_contact_last_seen_at(contacts)
|
||||
def fetch_contacts_with_conversation_count(contacts)
|
||||
filtrate(contacts).left_outer_joins(:conversations)
|
||||
.select('contacts.*, COUNT(conversations.id) as conversations_count')
|
||||
.group('contacts.id')
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
class Api::V1::Accounts::CsatSurveyResponsesController < Api::V1::Accounts::BaseController
|
||||
include Sift
|
||||
include DateRangeHelper
|
||||
|
||||
RESULTS_PER_PAGE = 25
|
||||
|
||||
before_action :check_authorization
|
||||
before_action :set_csat_survey_responses, only: [:index, :metrics]
|
||||
before_action :set_current_page, only: [:index]
|
||||
before_action :set_current_page_surveys, only: [:index]
|
||||
before_action :set_total_sent_messages_count, only: [:metrics]
|
||||
|
||||
sort_on :created_at, type: :datetime
|
||||
|
||||
def index; end
|
||||
|
||||
def metrics
|
||||
@total_count = @csat_survey_responses.count
|
||||
@ratings_count = @csat_survey_responses.group(:rating).count
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_total_sent_messages_count
|
||||
@csat_messages = Current.account.messages.input_csat
|
||||
@csat_messages = @csat_messages.where(created_at: range) if range.present?
|
||||
@total_sent_messages_count = @csat_messages.count
|
||||
end
|
||||
|
||||
def set_csat_survey_responses
|
||||
@csat_survey_responses = filtrate(
|
||||
Current.account.csat_survey_responses.includes([:conversation, :assigned_agent, :contact])
|
||||
)
|
||||
@csat_survey_responses = @csat_survey_responses.where(created_at: range) if range.present?
|
||||
end
|
||||
|
||||
def set_current_page_surveys
|
||||
@csat_survey_responses = @csat_survey_responses.page(@current_page).per(RESULTS_PER_PAGE)
|
||||
end
|
||||
|
||||
def set_current_page
|
||||
@current_page = params[:page] || 1
|
||||
end
|
||||
end
|
||||
49
app/controllers/api/v1/accounts/custom_filters_controller.rb
Normal file
49
app/controllers/api/v1/accounts/custom_filters_controller.rb
Normal file
@@ -0,0 +1,49 @@
|
||||
class Api::V1::Accounts::CustomFiltersController < Api::V1::Accounts::BaseController
|
||||
before_action :fetch_custom_filters, except: [:create]
|
||||
before_action :fetch_custom_filter, only: [:show, :update, :destroy]
|
||||
DEFAULT_FILTER_TYPE = 'conversation'.freeze
|
||||
|
||||
def index; end
|
||||
|
||||
def show; end
|
||||
|
||||
def create
|
||||
@custom_filter = current_user.custom_filters.create!(
|
||||
permitted_payload.merge(account_id: Current.account.id)
|
||||
)
|
||||
end
|
||||
|
||||
def update
|
||||
@custom_filter.update!(permitted_payload)
|
||||
end
|
||||
|
||||
def destroy
|
||||
@custom_filter.destroy
|
||||
head :no_content
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def fetch_custom_filters
|
||||
@custom_filters = current_user.custom_filters.where(
|
||||
account_id: Current.account.id,
|
||||
filter_type: permitted_params[:filter_type] || DEFAULT_FILTER_TYPE
|
||||
)
|
||||
end
|
||||
|
||||
def fetch_custom_filter
|
||||
@custom_filter = @custom_filters.find(permitted_params[:id])
|
||||
end
|
||||
|
||||
def permitted_payload
|
||||
params.require(:custom_filter).permit(
|
||||
:name,
|
||||
:filter_type,
|
||||
query: {}
|
||||
)
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:id, :filter_type)
|
||||
end
|
||||
end
|
||||
@@ -88,12 +88,12 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:id, :avatar, :name, :greeting_message, :greeting_enabled, :enable_email_collect, channel:
|
||||
params.permit(:id, :avatar, :name, :greeting_message, :greeting_enabled, :enable_email_collect, :csat_survey_enabled, channel:
|
||||
[:type, :website_url, :widget_color, :welcome_title, :welcome_tagline, :webhook_url, :email, :reply_time])
|
||||
end
|
||||
|
||||
def inbox_update_params
|
||||
params.permit(:enable_auto_assignment, :enable_email_collect, :name, :avatar, :greeting_message, :greeting_enabled,
|
||||
params.permit(:enable_auto_assignment, :enable_email_collect, :name, :avatar, :greeting_message, :greeting_enabled, :csat_survey_enabled,
|
||||
:working_hours_enabled, :out_of_office_message, :timezone,
|
||||
channel: [
|
||||
:website_url,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseController
|
||||
RESULTS_PER_PAGE = 15
|
||||
|
||||
protect_from_forgery with: :null_session
|
||||
before_action :fetch_notification, only: [:update]
|
||||
before_action :set_primary_actor, only: [:read_all]
|
||||
before_action :set_current_page, only: [:index]
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
class Api::V1::AccountsController < Api::BaseController
|
||||
include AuthHelper
|
||||
|
||||
skip_before_action :verify_authenticity_token, only: [:create]
|
||||
skip_before_action :authenticate_user!, :set_current_user, :handle_with_exception,
|
||||
only: [:create], raise: false
|
||||
before_action :check_signup_enabled, only: [:create]
|
||||
|
||||
@@ -54,7 +54,7 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
|
||||
end
|
||||
|
||||
def message_update_params
|
||||
params.permit(message: [{ submitted_values: [:name, :title, :value] }])
|
||||
params.permit(message: [{ submitted_values: [:name, :title, :value, { csat_survey_response: [:feedback_message, :rating] }] }])
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
|
||||
@@ -3,13 +3,12 @@ class ApplicationController < ActionController::Base
|
||||
include Pundit
|
||||
include SwitchLocale
|
||||
|
||||
protect_from_forgery with: :null_session
|
||||
skip_before_action :verify_authenticity_token
|
||||
|
||||
before_action :set_current_user, unless: :devise_controller?
|
||||
around_action :switch_locale
|
||||
around_action :handle_with_exception, unless: :devise_controller?
|
||||
|
||||
# after_action :verify_authorized
|
||||
rescue_from ActiveRecord::RecordInvalid, with: :render_record_invalid
|
||||
|
||||
private
|
||||
|
||||
@@ -27,7 +27,13 @@ class Installation::OnboardingController < ApplicationController
|
||||
|
||||
def finish_onboarding
|
||||
::Redis::Alfred.delete(::Redis::Alfred::CHATWOOT_INSTALLATION_ONBOARDING)
|
||||
ChatwootHub.register_instance(onboarding_params) if onboarding_params[:subscribe_to_updates]
|
||||
return if onboarding_params[:subscribe_to_updates].blank?
|
||||
|
||||
ChatwootHub.register_instance(
|
||||
onboarding_params.dig(:user, :company),
|
||||
onboarding_params.dig(:user, :name),
|
||||
onboarding_params.dig(:user, :email)
|
||||
)
|
||||
end
|
||||
|
||||
def ensure_installation_onboarding
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
class PlatformController < ActionController::Base
|
||||
protect_from_forgery with: :null_session
|
||||
|
||||
class PlatformController < ActionController::API
|
||||
before_action :ensure_access_token
|
||||
before_action :set_platform_app
|
||||
before_action :set_resource, only: [:update, :show, :destroy]
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# TODO: we should switch to ActionController::API for the base classes
|
||||
# One of the specs is failing when I tried doing that, lets revisit in future
|
||||
class PublicController < ActionController::Base
|
||||
skip_before_action :verify_authenticity_token
|
||||
end
|
||||
|
||||
@@ -10,10 +10,12 @@ class AsyncDispatcher < BaseDispatcher
|
||||
|
||||
def listeners
|
||||
[
|
||||
CampaignListener.instance,
|
||||
CsatSurveyListener.instance,
|
||||
EventListener.instance,
|
||||
WebhookListener.instance,
|
||||
InstallationWebhookListener.instance, HookListener.instance,
|
||||
CampaignListener.instance
|
||||
HookListener.instance,
|
||||
InstallationWebhookListener.instance,
|
||||
WebhookListener.instance
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -73,6 +73,8 @@ class ConversationFinder
|
||||
@conversations = @conversations.assigned_to(current_user)
|
||||
when 'unassigned'
|
||||
@conversations = @conversations.unassigned
|
||||
when 'assigned'
|
||||
@conversations = @conversations.where.not(assignee_id: nil)
|
||||
end
|
||||
@conversations
|
||||
end
|
||||
|
||||
19
app/helpers/date_range_helper.rb
Normal file
19
app/helpers/date_range_helper.rb
Normal file
@@ -0,0 +1,19 @@
|
||||
##############################################
|
||||
# Helpers to implement date range filtering to APIs
|
||||
# Include in your controller or service class where params is available
|
||||
##############################################
|
||||
|
||||
module DateRangeHelper
|
||||
def range
|
||||
return if params[:since].blank? || params[:until].blank?
|
||||
|
||||
parse_date_time(params[:since])..parse_date_time(params[:until])
|
||||
end
|
||||
|
||||
def parse_date_time(datetime)
|
||||
return datetime if datetime.is_a?(DateTime)
|
||||
return datetime.to_datetime if datetime.is_a?(Time) || datetime.is_a?(Date)
|
||||
|
||||
DateTime.strptime(datetime, '%s')
|
||||
end
|
||||
end
|
||||
6
app/helpers/message_format_helper.rb
Normal file
6
app/helpers/message_format_helper.rb
Normal file
@@ -0,0 +1,6 @@
|
||||
module MessageFormatHelper
|
||||
include RegexHelper
|
||||
def transform_user_mention_content(message_content)
|
||||
message_content.gsub(MENTION_REGEX, '\1')
|
||||
end
|
||||
end
|
||||
@@ -1,13 +1,30 @@
|
||||
/* global axios */
|
||||
import ApiClient from './ApiClient';
|
||||
|
||||
export const buildContactParams = (page, sortAttr, label, search) => {
|
||||
let params = `page=${page}&sort=${sortAttr}`;
|
||||
if (search) {
|
||||
params = `${params}&q=${search}`;
|
||||
}
|
||||
if (label) {
|
||||
params = `${params}&labels[]=${label}`;
|
||||
}
|
||||
return params;
|
||||
};
|
||||
|
||||
class ContactAPI extends ApiClient {
|
||||
constructor() {
|
||||
super('contacts', { accountScoped: true });
|
||||
}
|
||||
|
||||
get(page, sortAttr = 'name') {
|
||||
return axios.get(`${this.url}?page=${page}&sort=${sortAttr}`);
|
||||
get(page, sortAttr = 'name', label = '') {
|
||||
let requestURL = `${this.url}?${buildContactParams(
|
||||
page,
|
||||
sortAttr,
|
||||
label,
|
||||
''
|
||||
)}`;
|
||||
return axios.get(requestURL);
|
||||
}
|
||||
|
||||
getConversations(contactId) {
|
||||
@@ -26,10 +43,14 @@ class ContactAPI extends ApiClient {
|
||||
return axios.post(`${this.url}/${contactId}/labels`, { labels });
|
||||
}
|
||||
|
||||
search(search = '', page = 1, sortAttr = 'name') {
|
||||
return axios.get(
|
||||
`${this.url}/search?q=${search}&page=${page}&sort=${sortAttr}`
|
||||
);
|
||||
search(search = '', page = 1, sortAttr = 'name', label = '') {
|
||||
let requestURL = `${this.url}/search?${buildContactParams(
|
||||
page,
|
||||
sortAttr,
|
||||
label,
|
||||
search
|
||||
)}`;
|
||||
return axios.get(requestURL);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
22
app/javascript/dashboard/api/csatReports.js
Normal file
22
app/javascript/dashboard/api/csatReports.js
Normal file
@@ -0,0 +1,22 @@
|
||||
/* global axios */
|
||||
import ApiClient from './ApiClient';
|
||||
|
||||
class CSATReportsAPI extends ApiClient {
|
||||
constructor() {
|
||||
super('csat_survey_responses', { accountScoped: true });
|
||||
}
|
||||
|
||||
get({ page, from, to } = {}) {
|
||||
return axios.get(this.url, {
|
||||
params: { page, since: from, until: to, sort: '-created_at' },
|
||||
});
|
||||
}
|
||||
|
||||
getMetrics({ from, to } = {}) {
|
||||
return axios.get(`${this.url}/metrics`, {
|
||||
params: { since: from, until: to },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new CSATReportsAPI();
|
||||
@@ -30,6 +30,10 @@ class MessageApi extends ApiClient {
|
||||
});
|
||||
}
|
||||
|
||||
delete(conversationID, messageId) {
|
||||
return axios.delete(`${this.url}/${conversationID}/messages/${messageId}`);
|
||||
}
|
||||
|
||||
getPreviousMessages({ conversationId, before }) {
|
||||
return axios.get(`${this.url}/${conversationId}/messages`, {
|
||||
params: { before },
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import contactAPI from '../contacts';
|
||||
import contactAPI, { buildContactParams } from '../contacts';
|
||||
import ApiClient from '../ApiClient';
|
||||
import describeWithAPIMock from './apiSpecHelper';
|
||||
|
||||
@@ -15,9 +15,9 @@ describe('#ContactsAPI', () => {
|
||||
|
||||
describeWithAPIMock('API calls', context => {
|
||||
it('#get', () => {
|
||||
contactAPI.get(1, 'name');
|
||||
contactAPI.get(1, 'name', 'customer-support');
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v1/contacts?page=1&sort=name'
|
||||
'/api/v1/contacts?page=1&sort=name&labels[]=customer-support'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -54,10 +54,22 @@ describe('#ContactsAPI', () => {
|
||||
});
|
||||
|
||||
it('#search', () => {
|
||||
contactAPI.search('leads', 1, 'date');
|
||||
contactAPI.search('leads', 1, 'date', 'customer-support');
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v1/contacts/search?q=leads&page=1&sort=date'
|
||||
'/api/v1/contacts/search?page=1&sort=date&q=leads&labels[]=customer-support'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#buildContactParams', () => {
|
||||
it('returns correct string', () => {
|
||||
expect(buildContactParams(1, 'name', '', '')).toBe('page=1&sort=name');
|
||||
expect(buildContactParams(1, 'name', 'customer-support', '')).toBe(
|
||||
'page=1&sort=name&labels[]=customer-support'
|
||||
);
|
||||
expect(
|
||||
buildContactParams(1, 'name', 'customer-support', 'message-content')
|
||||
).toBe('page=1&sort=name&q=message-content&labels[]=customer-support');
|
||||
});
|
||||
});
|
||||
|
||||
37
app/javascript/dashboard/api/specs/csatReports.spec.js
Normal file
37
app/javascript/dashboard/api/specs/csatReports.spec.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import csatReportsAPI from '../csatReports';
|
||||
import ApiClient from '../ApiClient';
|
||||
import describeWithAPIMock from './apiSpecHelper';
|
||||
|
||||
describe('#Reports API', () => {
|
||||
it('creates correct instance', () => {
|
||||
expect(csatReportsAPI).toBeInstanceOf(ApiClient);
|
||||
expect(csatReportsAPI.apiVersion).toBe('/api/v1');
|
||||
expect(csatReportsAPI).toHaveProperty('get');
|
||||
expect(csatReportsAPI).toHaveProperty('getMetrics');
|
||||
});
|
||||
describeWithAPIMock('API calls', context => {
|
||||
it('#get', () => {
|
||||
csatReportsAPI.get({ page: 1, from: 1622485800, to: 1623695400 });
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v1/csat_survey_responses',
|
||||
{
|
||||
params: {
|
||||
page: 1,
|
||||
since: 1622485800,
|
||||
until: 1623695400,
|
||||
sort: '-created_at',
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
it('#getMetrics', () => {
|
||||
csatReportsAPI.getMetrics({ from: 1622485800, to: 1623695400 });
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v1/csat_survey_responses/metrics',
|
||||
{
|
||||
params: { since: 1622485800, until: 1623695400 },
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
BIN
app/javascript/dashboard/assets/images/channels/sms.png
Normal file
BIN
app/javascript/dashboard/assets/images/channels/sms.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
29
app/javascript/dashboard/assets/scss/_date-picker.scss
Normal file
29
app/javascript/dashboard/assets/scss/_date-picker.scss
Normal file
@@ -0,0 +1,29 @@
|
||||
@import '~vue2-datepicker/scss/index';
|
||||
|
||||
.mx-datepicker-popup {
|
||||
z-index: 99999;
|
||||
}
|
||||
|
||||
.date-picker {
|
||||
.mx-datepicker {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mx-datepicker-range {
|
||||
width: 320px;
|
||||
}
|
||||
|
||||
.mx-input {
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius-normal);
|
||||
box-shadow: none;
|
||||
display: flex;
|
||||
height: 4.6rem;
|
||||
}
|
||||
|
||||
.mx-input:disabled,
|
||||
.mx-input[readonly] {
|
||||
background-color: var(--white);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,14 @@ code {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.text-capitalize {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
// remove when grid gutters are fixed
|
||||
.columns.with-right-space {
|
||||
padding-right: var(--space-normal);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
@import 'foundation-settings';
|
||||
@import 'helper-classes';
|
||||
@import 'formulate';
|
||||
@import 'date-picker';
|
||||
|
||||
@import 'foundation-sites/scss/foundation';
|
||||
@import '~bourbon/core/bourbon';
|
||||
|
||||
@@ -137,6 +137,7 @@
|
||||
}
|
||||
|
||||
.sidebar-labels-wrap {
|
||||
|
||||
&.has-edited,
|
||||
&:hover {
|
||||
.multiselect {
|
||||
@@ -162,6 +163,12 @@
|
||||
.multiselect-wrap--small {
|
||||
$multiselect-height: 3.8rem;
|
||||
|
||||
.multiselect__tags,
|
||||
.multiselect__input {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.multiselect__tags,
|
||||
.multiselect__input,
|
||||
.multiselect {
|
||||
@@ -177,15 +184,24 @@
|
||||
}
|
||||
|
||||
.multiselect__single {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
font-size: var(--font-size-small);
|
||||
padding: var(--space-small) 0;
|
||||
margin: 0;
|
||||
padding: var(--space-smaller) var(--space-micro);
|
||||
}
|
||||
|
||||
.multiselect__placeholder {
|
||||
padding: var(--space-small) 0;
|
||||
margin: 0;
|
||||
padding: var(--space-smaller) var(--space-micro);
|
||||
}
|
||||
|
||||
.multiselect__select {
|
||||
min-height: $multiselect-height;
|
||||
}
|
||||
|
||||
.multiselect--disabled .multiselect__current,
|
||||
.multiselect--disabled .multiselect__select {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
@import 'variables';
|
||||
|
||||
@import '~spinkit/scss/spinners/7-three-bounce';
|
||||
@import '~vue-multiselect/dist/vue-multiselect.min.css';
|
||||
@import '~shared/assets/stylesheets/ionicons';
|
||||
|
||||
@import 'mixins';
|
||||
@@ -27,3 +28,5 @@
|
||||
@import 'foundation-custom';
|
||||
@import 'widgets/buttons';
|
||||
@import 'widgets/forms';
|
||||
|
||||
@import 'plugins/multiselect';
|
||||
|
||||
@@ -8,12 +8,12 @@
|
||||
.channel {
|
||||
@include flex;
|
||||
@include padding($space-normal $zero);
|
||||
@include margin($zero);
|
||||
@include background-white;
|
||||
@include border-light;
|
||||
flex-direction: column;
|
||||
|
||||
cursor: pointer;
|
||||
border-right-color: $color-white;
|
||||
flex-direction: column;
|
||||
margin: -1px;
|
||||
transition: all 0.200s ease-in;
|
||||
|
||||
&:last-child {
|
||||
@@ -32,14 +32,13 @@
|
||||
|
||||
img {
|
||||
@include margin($space-normal auto);
|
||||
flex: 1;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.channel__title{
|
||||
font-size: $font-size-large;
|
||||
text-align: center;
|
||||
.channel__title {
|
||||
color: $color-body;
|
||||
font-size: var(--font-size-default);
|
||||
text-align: center;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,10 @@ $default-button-height: 4.0rem;
|
||||
height: $default-button-height;
|
||||
margin-bottom: 0;
|
||||
|
||||
.button__content {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
padding: 0 var(--space-small);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
.report-card {
|
||||
@include padding($space-normal $space-small $space-normal $space-two);
|
||||
@include margin($zero);
|
||||
@include background-light;
|
||||
cursor: pointer;
|
||||
@include custom-border-top(3px, transparent);
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
/* eslint no-plusplus: 0 */
|
||||
/* eslint-env browser */
|
||||
import AvatarUploader from './widgets/forms/AvatarUploader.vue';
|
||||
import Bar from './widgets/chart/BarChart';
|
||||
import Button from './ui/WootButton';
|
||||
import Code from './Code';
|
||||
import ColorPicker from './widgets/ColorPicker';
|
||||
import DeleteModal from './widgets/modal/DeleteModal.vue';
|
||||
import ConfirmDeleteModal from './widgets/modal/ConfirmDeleteModal.vue';
|
||||
import DeleteModal from './widgets/modal/DeleteModal.vue';
|
||||
import DropdownItem from 'shared/components/ui/dropdown/DropdownItem';
|
||||
import DropdownMenu from 'shared/components/ui/dropdown/DropdownMenu';
|
||||
import HorizontalBar from './widgets/chart/HorizontalBarChart';
|
||||
import Input from './widgets/forms/Input.vue';
|
||||
import Label from './ui/Label';
|
||||
import LoadingState from './widgets/LoadingState';
|
||||
@@ -28,12 +28,14 @@ const WootUIKit = {
|
||||
Button,
|
||||
Code,
|
||||
ColorPicker,
|
||||
ConfirmDeleteModal,
|
||||
DeleteModal,
|
||||
DropdownItem,
|
||||
DropdownMenu,
|
||||
HorizontalBar,
|
||||
Input,
|
||||
LoadingState,
|
||||
Label,
|
||||
LoadingState,
|
||||
Modal,
|
||||
ModalHeader,
|
||||
ReportStatsCard,
|
||||
@@ -43,7 +45,6 @@ const WootUIKit = {
|
||||
Tabs,
|
||||
TabsItem,
|
||||
Thumbnail,
|
||||
ConfirmDeleteModal,
|
||||
install(Vue) {
|
||||
const keys = Object.keys(this);
|
||||
keys.pop(); // remove 'install' from keys
|
||||
|
||||
@@ -27,6 +27,13 @@
|
||||
v-if="shouldShowSidebarItem"
|
||||
:key="labelSection.toState"
|
||||
:menu-item="labelSection"
|
||||
@add-label="showAddLabelPopup"
|
||||
/>
|
||||
<sidebar-item
|
||||
v-if="showShowContactSideMenu"
|
||||
:key="contactLabelSection.key"
|
||||
:menu-item="contactLabelSection"
|
||||
@add-label="showAddLabelPopup"
|
||||
/>
|
||||
</transition-group>
|
||||
</div>
|
||||
@@ -57,6 +64,10 @@
|
||||
:show="showCreateAccountModal"
|
||||
@close-account-create-modal="closeCreateAccountModal"
|
||||
/>
|
||||
|
||||
<woot-modal :show.sync="showAddLabelModal" :on-close="hideAddLabelPopup">
|
||||
<add-label-modal @close="hideAddLabelPopup" />
|
||||
</woot-modal>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
@@ -74,6 +85,7 @@ import AgentDetails from './sidebarComponents/AgentDetails.vue';
|
||||
import OptionsMenu from './sidebarComponents/OptionsMenu.vue';
|
||||
import AccountSelector from './sidebarComponents/AccountSelector.vue';
|
||||
import AddAccountModal from './sidebarComponents/AddAccountModal.vue';
|
||||
import AddLabelModal from '../../routes/dashboard/settings/labels/AddLabel';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -84,6 +96,7 @@ export default {
|
||||
OptionsMenu,
|
||||
AccountSelector,
|
||||
AddAccountModal,
|
||||
AddLabelModal,
|
||||
},
|
||||
mixins: [adminMixin, alertMixin],
|
||||
data() {
|
||||
@@ -91,6 +104,7 @@ export default {
|
||||
showOptionsMenu: false,
|
||||
showAccountModal: false,
|
||||
showCreateAccountModal: false,
|
||||
showAddLabelModal: false,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -131,6 +145,9 @@ export default {
|
||||
shouldShowSidebarItem() {
|
||||
return this.sidemenuItems.common.routes.includes(this.currentRoute);
|
||||
},
|
||||
showShowContactSideMenu() {
|
||||
return this.sidemenuItems.contacts.routes.includes(this.currentRoute);
|
||||
},
|
||||
shouldShowTeams() {
|
||||
return this.shouldShowSidebarItem && this.teams.length;
|
||||
},
|
||||
@@ -177,6 +194,29 @@ export default {
|
||||
})),
|
||||
};
|
||||
},
|
||||
contactLabelSection() {
|
||||
return {
|
||||
icon: 'ion-pound',
|
||||
label: 'TAGGED_WITH',
|
||||
hasSubMenu: true,
|
||||
key: 'label',
|
||||
newLink: false,
|
||||
cssClass: 'menu-title align-justify',
|
||||
toState: frontendURL(`accounts/${this.accountId}/settings/labels`),
|
||||
toStateName: 'labels_list',
|
||||
showModalForNewItem: true,
|
||||
modalName: 'AddLabel',
|
||||
children: this.accountLabels.map(label => ({
|
||||
id: label.id,
|
||||
label: label.title,
|
||||
color: label.color,
|
||||
truncateLabel: true,
|
||||
toState: frontendURL(
|
||||
`accounts/${this.accountId}/labels/${label.title}/contacts`
|
||||
),
|
||||
})),
|
||||
};
|
||||
},
|
||||
teamSection() {
|
||||
return {
|
||||
icon: 'ion-ios-people',
|
||||
@@ -253,6 +293,12 @@ export default {
|
||||
closeCreateAccountModal() {
|
||||
this.showCreateAccountModal = false;
|
||||
},
|
||||
showAddLabelPopup() {
|
||||
this.showAddLabelModal = true;
|
||||
},
|
||||
hideAddLabelPopup() {
|
||||
this.showAddLabelModal = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -52,11 +52,6 @@
|
||||
</a>
|
||||
</router-link>
|
||||
</ul>
|
||||
<add-label-modal
|
||||
v-if="showAddLabel"
|
||||
:show.sync="showAddLabel"
|
||||
:on-close="hideAddLabelPopup"
|
||||
/>
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
@@ -66,17 +61,8 @@ import { mapGetters } from 'vuex';
|
||||
import router from '../../routes';
|
||||
import adminMixin from '../../mixins/isAdmin';
|
||||
import { getInboxClassByType } from 'dashboard/helper/inbox';
|
||||
import AddLabelModal from '../../routes/dashboard/settings/labels/AddLabel';
|
||||
export default {
|
||||
components: {
|
||||
AddLabelModal,
|
||||
},
|
||||
mixins: [adminMixin],
|
||||
data() {
|
||||
return {
|
||||
showAddLabel: false,
|
||||
};
|
||||
},
|
||||
props: {
|
||||
menuItem: {
|
||||
type: Object,
|
||||
@@ -127,19 +113,13 @@ export default {
|
||||
router.push({ name: item.newLinkRouteName, params: { page: 'new' } });
|
||||
} else if (item.showModalForNewItem) {
|
||||
if (item.modalName === 'AddLabel') {
|
||||
this.showAddLabelPopup();
|
||||
this.$emit('add-label');
|
||||
}
|
||||
}
|
||||
},
|
||||
showItem(item) {
|
||||
return this.isAdmin && item.newLink !== undefined;
|
||||
},
|
||||
showAddLabelPopup() {
|
||||
this.showAddLabel = true;
|
||||
},
|
||||
hideAddLabelPopup() {
|
||||
this.showAddLabel = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
40
app/javascript/dashboard/components/ui/DateRangePicker.vue
Normal file
40
app/javascript/dashboard/components/ui/DateRangePicker.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<div class="date-picker">
|
||||
<date-picker
|
||||
:range="true"
|
||||
:confirm="true"
|
||||
:clearable="false"
|
||||
:editable="false"
|
||||
:confirm-text="confirmText"
|
||||
:placeholder="placeholder"
|
||||
:value="value"
|
||||
@change="handleChange"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DatePicker from 'vue2-datepicker';
|
||||
export default {
|
||||
components: { DatePicker },
|
||||
props: {
|
||||
confirmText: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
value: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleChange(value) {
|
||||
this.$emit('change', value);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
47
app/javascript/dashboard/components/ui/DateTimePicker.vue
Normal file
47
app/javascript/dashboard/components/ui/DateTimePicker.vue
Normal file
@@ -0,0 +1,47 @@
|
||||
<template>
|
||||
<div class="date-picker">
|
||||
<date-picker
|
||||
type="datetime"
|
||||
:confirm="true"
|
||||
:clearable="false"
|
||||
:editable="false"
|
||||
:confirm-text="confirmText"
|
||||
:placeholder="placeholder"
|
||||
:value="value"
|
||||
:disabled-date="disableBeforeToday"
|
||||
@change="handleChange"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import addDays from 'date-fns/addDays';
|
||||
import DatePicker from 'vue2-datepicker';
|
||||
export default {
|
||||
components: { DatePicker },
|
||||
props: {
|
||||
confirmText: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
value: {
|
||||
type: Date,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
handleChange(value) {
|
||||
this.$emit('change', value);
|
||||
},
|
||||
disableBeforeToday(date) {
|
||||
const yesterdayDate = addDays(new Date(), -1);
|
||||
return date < yesterdayDate;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,38 @@
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import WootDateRangePicker from '../DateRangePicker.vue';
|
||||
|
||||
export default {
|
||||
title: 'Components/Date Picker/Date Range Picker',
|
||||
argTypes: {
|
||||
confirmText: {
|
||||
defaultValue: 'Apply',
|
||||
control: {
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
placeholder: {
|
||||
defaultValue: 'Select date range',
|
||||
control: {
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
value: {
|
||||
control: {
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const Template = (args, { argTypes }) => ({
|
||||
props: Object.keys(argTypes),
|
||||
components: { WootDateRangePicker },
|
||||
template:
|
||||
'<woot-date-range-picker v-bind="$props" @change="onChange"></woot-date-range-picker>',
|
||||
});
|
||||
|
||||
export const DateRangePicker = Template.bind({});
|
||||
DateRangePicker.args = {
|
||||
onChange: action('applied'),
|
||||
value: new Date(),
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import WootDateTimePicker from '../DateTimePicker.vue';
|
||||
|
||||
export default {
|
||||
title: 'Components/Date Picker/Date Time Picker',
|
||||
argTypes: {
|
||||
confirmText: {
|
||||
defaultValue: 'Apply',
|
||||
control: {
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
placeholder: {
|
||||
defaultValue: 'Select date time',
|
||||
control: {
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
value: {
|
||||
control: {
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const Template = (args, { argTypes }) => ({
|
||||
props: Object.keys(argTypes),
|
||||
components: { WootDateTimePicker },
|
||||
template:
|
||||
'<woot-date-time-picker v-bind="$props" @change="onChange"></woot-date-time-picker>',
|
||||
});
|
||||
|
||||
export const DateTimePicker = Template.bind({});
|
||||
DateTimePicker.args = {
|
||||
onChange: action('applied'),
|
||||
value: new Date(),
|
||||
};
|
||||
@@ -65,7 +65,11 @@ export default {
|
||||
display: flex;
|
||||
padding: var(--space-slab) 0 0;
|
||||
background: var(--color-background-light);
|
||||
background: transparent;
|
||||
background: var(--b-50);
|
||||
border-radius: var(--border-radius-normal);
|
||||
width: fit-content;
|
||||
padding: var(--space-smaller);
|
||||
margin-top: var(--space-normal);
|
||||
}
|
||||
|
||||
.thumb-wrap {
|
||||
@@ -114,6 +118,8 @@ export default {
|
||||
max-width: 50%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-left: var(--space-small);
|
||||
|
||||
.item {
|
||||
height: var(--space-normal);
|
||||
overflow: hidden;
|
||||
@@ -123,7 +129,7 @@ export default {
|
||||
}
|
||||
|
||||
.file-size-wrap {
|
||||
width: 20%;
|
||||
width: 30%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
class="small-3 columns channel"
|
||||
class="small-6 medium-4 large-3 columns channel"
|
||||
:class="{ inactive: !isActive }"
|
||||
@click="onItemClick"
|
||||
>
|
||||
@@ -37,8 +37,12 @@
|
||||
src="~dashboard/assets/images/channels/website.png"
|
||||
/>
|
||||
<img
|
||||
v-if="channel.key === 'twilio'"
|
||||
src="~dashboard/assets/images/channels/twilio.png"
|
||||
v-if="channel.key === 'sms'"
|
||||
src="~dashboard/assets/images/channels/sms.png"
|
||||
/>
|
||||
<img
|
||||
v-if="channel.key === 'whatsapp'"
|
||||
src="~dashboard/assets/images/channels/whatsapp.png"
|
||||
/>
|
||||
<h3 class="channel__title">
|
||||
{{ channel.name }}
|
||||
@@ -72,7 +76,7 @@ export default {
|
||||
if (key === 'email') {
|
||||
return this.enabledFeatures.channel_email;
|
||||
}
|
||||
return ['website', 'twilio', 'api'].includes(key);
|
||||
return ['website', 'twilio', 'api', 'whatsapp', 'sms'].includes(key);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -1,12 +1,21 @@
|
||||
<template>
|
||||
<div class="small-2 report-card" :class="{ 'active': selected }" v-on:click="onClick(index)">
|
||||
<h3 class="heading">{{heading}}</h3>
|
||||
<h4 class="metric">{{point}}</h4>
|
||||
<p class="desc">{{desc}}</p>
|
||||
<div
|
||||
class="small-2 report-card"
|
||||
:class="{ active: selected }"
|
||||
@click="onClick(index)"
|
||||
>
|
||||
<h3 class="heading">
|
||||
{{ heading }}
|
||||
</h3>
|
||||
<h4 class="metric">
|
||||
{{ point }}
|
||||
</h4>
|
||||
<p class="desc">
|
||||
{{ desc }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
|
||||
export default {
|
||||
props: {
|
||||
heading: String,
|
||||
|
||||
@@ -1,36 +1,37 @@
|
||||
<template>
|
||||
<div class="row--user-block">
|
||||
<Thumbnail
|
||||
:src="sender.thumbnail"
|
||||
size="20px"
|
||||
:username="sender.name"
|
||||
:status="sender.availability_status"
|
||||
<thumbnail
|
||||
:src="user.thumbnail"
|
||||
:size="size"
|
||||
:username="user.name"
|
||||
:status="user.availability_status"
|
||||
/>
|
||||
<div>
|
||||
<h6 class="text-block-title text-truncate">
|
||||
{{ sender.name }}
|
||||
</h6>
|
||||
</div>
|
||||
<h6 class="text-block-title text-truncate text-capitalize">
|
||||
{{ user.name }}
|
||||
</h6>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Thumbnail,
|
||||
},
|
||||
props: {
|
||||
sender: {
|
||||
user: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: '20px',
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '~dashboard/assets/scss/mixins';
|
||||
|
||||
.row--user-block {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
@@ -12,8 +12,11 @@
|
||||
/>
|
||||
|
||||
<file-upload
|
||||
ref="upload"
|
||||
:size="4096 * 4096"
|
||||
accept="image/*, application/pdf, audio/mpeg, video/mp4, audio/ogg, text/csv"
|
||||
:drop="true"
|
||||
:drop-directory="false"
|
||||
@input-file="onFileUpload"
|
||||
>
|
||||
<woot-button
|
||||
@@ -37,6 +40,17 @@
|
||||
:title="$t('CONVERSATION.REPLYBOX.TIP_FORMAT_ICON')"
|
||||
@click="toggleFormatMode"
|
||||
/>
|
||||
<transition name="modal-fade">
|
||||
<div
|
||||
v-show="$refs.upload && $refs.upload.dropActive"
|
||||
class="modal-mask"
|
||||
>
|
||||
<i class="ion-ios-cloud-upload-outline icon"></i>
|
||||
<h4 class="page-sub-title">
|
||||
{{ $t('CONVERSATION.REPLYBOX.DRAG_DROP') }}
|
||||
</h4>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
<div class="right-wrap">
|
||||
<div v-if="isFormatMode" class="enter-to-send--checkbox">
|
||||
@@ -198,4 +212,18 @@ export default {
|
||||
background: var(--s-100);
|
||||
}
|
||||
}
|
||||
|
||||
.modal-mask {
|
||||
color: var(--s-600);
|
||||
background: var(--white-transparent);
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.page-sub-title {
|
||||
color: var(--s-600);
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 8rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { HorizontalBar } from 'vue-chartjs';
|
||||
|
||||
const chartOptions = {
|
||||
responsive: true,
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
title: {
|
||||
display: false,
|
||||
},
|
||||
tooltips: {
|
||||
enabled: false,
|
||||
},
|
||||
scales: {
|
||||
xAxes: [
|
||||
{
|
||||
gridLines: {
|
||||
offsetGridLines: false,
|
||||
},
|
||||
display: false,
|
||||
stacked: true,
|
||||
},
|
||||
],
|
||||
yAxes: [
|
||||
{
|
||||
gridLines: {
|
||||
offsetGridLines: false,
|
||||
},
|
||||
display: false,
|
||||
stacked: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
extends: HorizontalBar,
|
||||
props: {
|
||||
collection: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
chartOptions: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.renderChart(this.collection, {
|
||||
...chartOptions,
|
||||
...this.chartOptions,
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -39,7 +39,7 @@
|
||||
:loading="uiFlags.isFetching"
|
||||
:allow-empty="true"
|
||||
deselect-label=""
|
||||
:options="agentList"
|
||||
:options="agentsList"
|
||||
:placeholder="$t('CONVERSATION.ASSIGNMENT.SELECT_AGENT')"
|
||||
select-label=""
|
||||
label="name"
|
||||
@@ -67,6 +67,7 @@
|
||||
import { mapGetters } from 'vuex';
|
||||
import MoreActions from './MoreActions';
|
||||
import Thumbnail from '../Thumbnail';
|
||||
import agentMixin from '../../../mixins/agentMixin.js';
|
||||
import AvailabilityStatusBadge from '../conversation/AvailabilityStatusBadge';
|
||||
|
||||
export default {
|
||||
@@ -75,7 +76,7 @@ export default {
|
||||
Thumbnail,
|
||||
AvailabilityStatusBadge,
|
||||
},
|
||||
|
||||
mixins: [agentMixin],
|
||||
props: {
|
||||
chat: {
|
||||
type: Object,
|
||||
@@ -90,12 +91,12 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
currentChatAssignee: null,
|
||||
inboxId: null,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({
|
||||
getAgents: 'inboxAssignableAgents/getAssignableAgents',
|
||||
uiFlags: 'inboxAssignableAgents/getUIFlags',
|
||||
currentChat: 'getSelectedChat',
|
||||
}),
|
||||
@@ -109,22 +110,10 @@ export default {
|
||||
this.chat.meta.sender.id
|
||||
);
|
||||
},
|
||||
|
||||
agentList() {
|
||||
const { inbox_id: inboxId } = this.chat;
|
||||
const agents = this.getAgents(inboxId) || [];
|
||||
return [
|
||||
{
|
||||
confirmed: true,
|
||||
name: 'None',
|
||||
id: 0,
|
||||
role: 'agent',
|
||||
account_id: 0,
|
||||
email: 'None',
|
||||
},
|
||||
...agents,
|
||||
];
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
const { inbox_id: inboxId } = this.chat;
|
||||
this.inboxId = inboxId;
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
@@ -44,7 +44,6 @@
|
||||
/>
|
||||
</div>
|
||||
<spinner v-if="isPending" size="tiny" />
|
||||
|
||||
<a
|
||||
v-if="isATweet && isIncoming && sender"
|
||||
class="sender--info"
|
||||
@@ -62,29 +61,47 @@
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="context-menu-wrap">
|
||||
<context-menu
|
||||
v-if="isBubble && !isMessageDeleted"
|
||||
:is-open="showContextMenu"
|
||||
:show-copy="hasText"
|
||||
:menu-position="contextMenuPosition"
|
||||
@toggle="handleContextMenuClick"
|
||||
@delete="handleDelete"
|
||||
@copy="handleCopy"
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
<script>
|
||||
import copy from 'copy-text-to-clipboard';
|
||||
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import timeMixin from '../../../mixins/time';
|
||||
import BubbleText from './bubble/Text';
|
||||
import BubbleImage from './bubble/Image';
|
||||
import BubbleFile from './bubble/File';
|
||||
import Spinner from 'shared/components/Spinner';
|
||||
import ContextMenu from 'dashboard/modules/conversations/components/MessageContextMenu';
|
||||
|
||||
import { isEmptyObject } from 'dashboard/helper/commons';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import contentTypeMixin from 'shared/mixins/contentTypeMixin';
|
||||
import BubbleActions from './bubble/Actions';
|
||||
import { MESSAGE_TYPE, MESSAGE_STATUS } from 'shared/constants/messages';
|
||||
import { generateBotMessageContent } from './helpers/botMessageContentHelper';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
BubbleActions,
|
||||
BubbleText,
|
||||
BubbleImage,
|
||||
BubbleFile,
|
||||
ContextMenu,
|
||||
Spinner,
|
||||
},
|
||||
mixins: [timeMixin, messageFormatterMixin, contentTypeMixin],
|
||||
mixins: [alertMixin, timeMixin, messageFormatterMixin, contentTypeMixin],
|
||||
props: {
|
||||
data: {
|
||||
type: Object,
|
||||
@@ -97,7 +114,7 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isHovered: false,
|
||||
showContextMenu: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -105,7 +122,13 @@ export default {
|
||||
const botMessageContent = generateBotMessageContent(
|
||||
this.contentType,
|
||||
this.contentAttributes,
|
||||
this.$t('CONVERSATION.NO_RESPONSE')
|
||||
{
|
||||
noResponseText: this.$t('CONVERSATION.NO_RESPONSE'),
|
||||
csat: {
|
||||
ratingTitle: this.$t('CONVERSATION.RATING_TITLE'),
|
||||
feedbackTitle: this.$t('CONVERSATION.FEEDBACK_TITLE'),
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const {
|
||||
@@ -115,12 +138,10 @@ export default {
|
||||
} = this.contentAttributes;
|
||||
|
||||
if ((replyHTMLContent || fullHTMLContent) && this.isIncoming) {
|
||||
let parsedContent = new DOMParser().parseFromString(
|
||||
replyHTMLContent || fullHTMLContent || '',
|
||||
'text/html'
|
||||
);
|
||||
if (!parsedContent.getElementsByTagName('parsererror').length) {
|
||||
return parsedContent.body.innerHTML;
|
||||
let contentToBeParsed = replyHTMLContent || fullHTMLContent || '';
|
||||
const parsedContent = this.stripStyleCharacters(contentToBeParsed);
|
||||
if (parsedContent) {
|
||||
return parsedContent;
|
||||
}
|
||||
}
|
||||
return (
|
||||
@@ -146,10 +167,19 @@ export default {
|
||||
},
|
||||
alignBubble() {
|
||||
const { message_type: messageType } = this.data;
|
||||
if (messageType === MESSAGE_TYPE.ACTIVITY) {
|
||||
return 'center';
|
||||
}
|
||||
return !messageType ? 'left' : 'right';
|
||||
const isCentered = messageType === MESSAGE_TYPE.ACTIVITY;
|
||||
const isLeftAligned = messageType === MESSAGE_TYPE.INCOMING;
|
||||
const isRightAligned =
|
||||
messageType === MESSAGE_TYPE.OUTGOING ||
|
||||
messageType === MESSAGE_TYPE.TEMPLATE;
|
||||
|
||||
return {
|
||||
center: isCentered,
|
||||
left: isLeftAligned,
|
||||
right: isRightAligned,
|
||||
'has-context-menu': this.showContextMenu,
|
||||
'has-tweet-menu': this.isATweet,
|
||||
};
|
||||
},
|
||||
readableTime() {
|
||||
return this.messageStamp(
|
||||
@@ -166,6 +196,9 @@ export default {
|
||||
hasAttachments() {
|
||||
return !!(this.data.attachments && this.data.attachments.length > 0);
|
||||
},
|
||||
isMessageDeleted() {
|
||||
return this.contentAttributes.deleted;
|
||||
},
|
||||
hasImageAttachment() {
|
||||
if (this.hasAttachments && this.data.attachments.length > 0) {
|
||||
const { attachments = [{}] } = this.data;
|
||||
@@ -178,9 +211,11 @@ export default {
|
||||
return !!this.data.content;
|
||||
},
|
||||
sentByMessage() {
|
||||
if (this.isMessageDeleted) {
|
||||
return false;
|
||||
}
|
||||
const { sender } = this;
|
||||
|
||||
return this.data.message_type === 1 && !this.isHovered && sender
|
||||
return this.data.message_type === 1 && !isEmptyObject(sender)
|
||||
? {
|
||||
content: `${this.$t('CONVERSATION.SENT_BY')} ${sender.name}`,
|
||||
classes: 'top',
|
||||
@@ -210,6 +245,33 @@ export default {
|
||||
if (this.isPending) return false;
|
||||
return !this.sender.type || this.sender.type === 'agent_bot';
|
||||
},
|
||||
contextMenuPosition() {
|
||||
const { message_type: messageType } = this.data;
|
||||
return messageType ? 'right' : 'left';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleContextMenuClick() {
|
||||
this.showContextMenu = !this.showContextMenu;
|
||||
},
|
||||
async handleDelete() {
|
||||
const { conversation_id: conversationId, id: messageId } = this.data;
|
||||
try {
|
||||
await this.$store.dispatch('deleteMessage', {
|
||||
conversationId,
|
||||
messageId,
|
||||
});
|
||||
this.showAlert(this.$t('CONVERSATION.SUCCESS_DELETE_MESSAGE'));
|
||||
this.showContextMenu = false;
|
||||
} catch (error) {
|
||||
this.showAlert(this.$t('CONVERSATION.FAIL_DELETE_MESSSAGE'));
|
||||
}
|
||||
},
|
||||
handleCopy() {
|
||||
copy(this.data.content);
|
||||
this.showAlert(this.$t('CONTACT_PANEL.COPY_SUCCESSFUL'));
|
||||
this.showContextMenu = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -294,4 +356,42 @@ export default {
|
||||
margin-left: var(--space-smaller);
|
||||
}
|
||||
}
|
||||
|
||||
.button--delete-message {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
li.left,
|
||||
li.right {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
|
||||
&:hover .button--delete-message {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
li.left.has-tweet-menu .context-menu {
|
||||
margin-bottom: var(--space-medium);
|
||||
}
|
||||
|
||||
li.right .context-menu-wrap {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
li.right {
|
||||
flex-direction: row-reverse;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.has-context-menu {
|
||||
background: var(--color-background);
|
||||
.button--delete-message {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
.context-menu {
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
<message
|
||||
v-for="message in getReadMessages"
|
||||
:key="message.id"
|
||||
class="message--read"
|
||||
:data="message"
|
||||
:is-a-tweet="isATweet"
|
||||
/>
|
||||
@@ -72,6 +73,7 @@
|
||||
<message
|
||||
v-for="message in getUnReadMessages"
|
||||
:key="message.id"
|
||||
class="message--unread"
|
||||
:data="message"
|
||||
:is-a-tweet="isATweet"
|
||||
/>
|
||||
@@ -106,6 +108,7 @@ import { getTypingUsersText } from '../../../helper/commons';
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
import { REPLY_POLICY } from 'shared/constants/links';
|
||||
import inboxMixin from 'shared/mixins/inboxMixin';
|
||||
import { calculateScrollTop } from './helpers/scrollTopCalculationHelper';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -259,7 +262,23 @@ export default {
|
||||
this.conversationPanel.removeEventListener('scroll', this.handleScroll);
|
||||
},
|
||||
scrollToBottom() {
|
||||
this.conversationPanel.scrollTop = this.conversationPanel.scrollHeight;
|
||||
let relevantMessages = [];
|
||||
if (this.getUnreadCount > 0) {
|
||||
// capturing only the unread messages
|
||||
relevantMessages = this.conversationPanel.querySelectorAll(
|
||||
'.message--unread'
|
||||
);
|
||||
} else {
|
||||
// capturing last message from the messages list
|
||||
relevantMessages = Array.from(
|
||||
this.conversationPanel.querySelectorAll('.message--read')
|
||||
).slice(-1);
|
||||
}
|
||||
this.conversationPanel.scrollTop = calculateScrollTop(
|
||||
this.conversationPanel.scrollHeight,
|
||||
this.$el.scrollHeight,
|
||||
relevantMessages
|
||||
);
|
||||
},
|
||||
onToggleContactPanel() {
|
||||
this.$emit('contact-panel-toggle');
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
const generateInputSelectContent = (contentType, contentAttributes) => {
|
||||
import { CSAT_RATINGS } from '../../../../../shared/constants/messages';
|
||||
|
||||
const generateInputSelectContent = contentAttributes => {
|
||||
const { submitted_values: submittedValues = [] } = contentAttributes;
|
||||
const [selectedOption] = submittedValues;
|
||||
|
||||
@@ -8,7 +10,7 @@ const generateInputSelectContent = (contentType, contentAttributes) => {
|
||||
return '';
|
||||
};
|
||||
|
||||
const generateInputEmailContent = (contentType, contentAttributes) => {
|
||||
const generateInputEmailContent = contentAttributes => {
|
||||
const { submitted_email: submittedEmail = '' } = contentAttributes;
|
||||
if (submittedEmail) {
|
||||
return `<strong>${submittedEmail}</strong>`;
|
||||
@@ -16,11 +18,7 @@ const generateInputEmailContent = (contentType, contentAttributes) => {
|
||||
return '';
|
||||
};
|
||||
|
||||
const generateFormContent = (
|
||||
contentType,
|
||||
contentAttributes,
|
||||
noResponseText
|
||||
) => {
|
||||
const generateFormContent = (contentAttributes, { noResponseText }) => {
|
||||
const { items, submitted_values: submittedValues = [] } = contentAttributes;
|
||||
if (submittedValues.length) {
|
||||
const submittedObject = submittedValues.reduce((acc, keyValuePair) => {
|
||||
@@ -38,20 +36,52 @@ const generateFormContent = (
|
||||
return '';
|
||||
};
|
||||
|
||||
const generateCSATContent = (
|
||||
contentAttributes,
|
||||
{ ratingTitle, feedbackTitle }
|
||||
) => {
|
||||
const {
|
||||
submitted_values: { csat_survey_response: surveyResponse = {} } = {},
|
||||
} = contentAttributes;
|
||||
const { rating, feedback_message } = surveyResponse || {};
|
||||
|
||||
let messageContent = '';
|
||||
if (rating) {
|
||||
const [ratingObject = {}] = CSAT_RATINGS.filter(
|
||||
csatRating => csatRating.value === rating
|
||||
);
|
||||
messageContent += `<div><strong>${ratingTitle}</strong></div>`;
|
||||
messageContent += `<p>${ratingObject.emoji}</p>`;
|
||||
}
|
||||
if (feedback_message) {
|
||||
messageContent += `<div><strong>${feedbackTitle}</strong></div>`;
|
||||
messageContent += `<p>${feedback_message}</p>`;
|
||||
}
|
||||
return messageContent;
|
||||
};
|
||||
|
||||
export const generateBotMessageContent = (
|
||||
contentType,
|
||||
contentAttributes,
|
||||
noResponseText = 'No response'
|
||||
{
|
||||
noResponseText = 'No response',
|
||||
csat: { ratingTitle = 'Rating', feedbackTitle = 'Feedback' } = {},
|
||||
} = {}
|
||||
) => {
|
||||
const contentTypeMethods = {
|
||||
input_select: generateInputSelectContent,
|
||||
input_email: generateInputEmailContent,
|
||||
form: generateFormContent,
|
||||
input_csat: generateCSATContent,
|
||||
};
|
||||
|
||||
const contentTypeMethod = contentTypeMethods[contentType];
|
||||
if (contentTypeMethod && typeof contentTypeMethod === 'function') {
|
||||
return contentTypeMethod(contentType, contentAttributes, noResponseText);
|
||||
return contentTypeMethod(contentAttributes, {
|
||||
noResponseText,
|
||||
ratingTitle,
|
||||
feedbackTitle,
|
||||
});
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
const totalMessageHeight = (total, element) => {
|
||||
return total + element.scrollHeight;
|
||||
};
|
||||
|
||||
export const calculateScrollTop = (
|
||||
conversationPanelHeight,
|
||||
parentHeight,
|
||||
relevantMessages
|
||||
) => {
|
||||
// add up scrollHeight of a `relevantMessages`
|
||||
let combinedMessageScrollHeight = [...relevantMessages].reduce(
|
||||
totalMessageHeight,
|
||||
0
|
||||
);
|
||||
return (
|
||||
conversationPanelHeight - combinedMessageScrollHeight - parentHeight / 2
|
||||
);
|
||||
};
|
||||
@@ -17,6 +17,33 @@ describe('#generateBotMessageContent', () => {
|
||||
).toEqual('<strong>hello@chatwoot.com</strong>');
|
||||
});
|
||||
|
||||
it('return correct input_csat content', () => {
|
||||
expect(
|
||||
generateBotMessageContent('input_csat', {
|
||||
submitted_values: {
|
||||
csat_survey_response: {
|
||||
rating: 5,
|
||||
feedback_message: 'Great Service',
|
||||
},
|
||||
},
|
||||
})
|
||||
).toEqual(
|
||||
'<div><strong>Rating</strong></div><p>😍</p><div><strong>Feedback</strong></div><p>Great Service</p>'
|
||||
);
|
||||
|
||||
expect(
|
||||
generateBotMessageContent(
|
||||
'input_csat',
|
||||
{
|
||||
submitted_values: {
|
||||
csat_survey_response: { rating: 1, feedback_message: '' },
|
||||
},
|
||||
},
|
||||
{ csat: { ratingTitle: 'റേറ്റിംഗ്', feedbackTitle: 'പ്രതികരണം' } }
|
||||
)
|
||||
).toEqual('<div><strong>റേറ്റിംഗ്</strong></div><p>😞</p>');
|
||||
});
|
||||
|
||||
it('return correct form content', () => {
|
||||
expect(
|
||||
generateBotMessageContent('form', {
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { calculateScrollTop } from '../scrollTopCalculationHelper';
|
||||
|
||||
describe('#calculateScrollTop', () => {
|
||||
it('returns calculated value of the scrollTop property', () => {
|
||||
class DOMElement {
|
||||
constructor(scrollHeight) {
|
||||
this.scrollHeight = scrollHeight;
|
||||
}
|
||||
}
|
||||
let count = 3;
|
||||
let relevantMessages = [];
|
||||
while (count > 0) {
|
||||
relevantMessages.push(new DOMElement(100));
|
||||
count -= 1;
|
||||
}
|
||||
expect(calculateScrollTop(1000, 300, relevantMessages)).toEqual(550);
|
||||
});
|
||||
});
|
||||
@@ -7,10 +7,7 @@ export const getSidebarItems = accountId => ({
|
||||
'inbox_dashboard',
|
||||
'inbox_conversation',
|
||||
'conversation_through_inbox',
|
||||
'contacts_dashboard',
|
||||
'contacts_dashboard_manage',
|
||||
'notifications_dashboard',
|
||||
'settings_account_reports',
|
||||
'profile_settings',
|
||||
'profile_settings_index',
|
||||
'label_conversations',
|
||||
@@ -59,6 +56,55 @@ export const getSidebarItems = accountId => ({
|
||||
},
|
||||
},
|
||||
},
|
||||
contacts: {
|
||||
routes: [
|
||||
'contacts_dashboard',
|
||||
'contacts_dashboard_manage',
|
||||
'contacts_labels_dashboard',
|
||||
],
|
||||
menuItems: {
|
||||
back: {
|
||||
icon: 'ion-ios-arrow-back',
|
||||
label: 'HOME',
|
||||
hasSubMenu: false,
|
||||
toStateName: 'home',
|
||||
toState: frontendURL(`accounts/${accountId}/dashboard`),
|
||||
},
|
||||
contacts: {
|
||||
icon: 'ion-person',
|
||||
label: 'ALL_CONTACTS',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/contacts`),
|
||||
toStateName: 'contacts_dashboard',
|
||||
},
|
||||
},
|
||||
},
|
||||
reports: {
|
||||
routes: ['settings_account_reports', 'csat_reports'],
|
||||
menuItems: {
|
||||
back: {
|
||||
icon: 'ion-ios-arrow-back',
|
||||
label: 'HOME',
|
||||
hasSubMenu: false,
|
||||
toStateName: 'home',
|
||||
toState: frontendURL(`accounts/${accountId}/dashboard`),
|
||||
},
|
||||
reportOverview: {
|
||||
icon: 'ion-arrow-graph-up-right',
|
||||
label: 'REPORTS_OVERVIEW',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/reports/overview`),
|
||||
toStateName: 'settings_account_reports',
|
||||
},
|
||||
csatReports: {
|
||||
icon: 'ion-happy',
|
||||
label: 'CSAT',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/reports/csat`),
|
||||
toStateName: 'csat_reports',
|
||||
},
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
routes: [
|
||||
'agent_list',
|
||||
|
||||
@@ -90,7 +90,7 @@
|
||||
}
|
||||
},
|
||||
"SEARCH": {
|
||||
"NO_RESULTS": "لم يتم العثور على موظفين."
|
||||
"NO_RESULTS": "لم يتم العثور على النتائج."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,17 @@
|
||||
"PLACEHOLDER": "Please enter the title of campaign",
|
||||
"ERROR": "Title is required"
|
||||
},
|
||||
"SCHEDULED_AT": {
|
||||
"LABEL": "Scheduled time",
|
||||
"PLACEHOLDER": "Please select the time",
|
||||
"CONFIRM": "Confirm",
|
||||
"ERROR": "Scheduled time is required"
|
||||
},
|
||||
"AUDIENCE": {
|
||||
"LABEL": "Audience",
|
||||
"PLACEHOLDER": "Select the customer labels",
|
||||
"ERROR": "Audience is required"
|
||||
},
|
||||
"MESSAGE": {
|
||||
"LABEL": "رسالة",
|
||||
"PLACEHOLDER": "Please enter the message of campaign",
|
||||
@@ -72,6 +83,7 @@
|
||||
"STATUS": "الحالة",
|
||||
"SENDER": "Sender",
|
||||
"URL": "URL",
|
||||
"SCHEDULED_AT": "Scheduled time",
|
||||
"TIME_ON_PAGE": "Time(Seconds)",
|
||||
"CREATED_AT": "Created at"
|
||||
},
|
||||
@@ -82,7 +94,9 @@
|
||||
},
|
||||
"STATUS": {
|
||||
"ENABLED": "مفعل",
|
||||
"DISABLED": "معطّل"
|
||||
"DISABLED": "معطّل",
|
||||
"COMPLETED": "Completed",
|
||||
"ACTIVE": "Active"
|
||||
},
|
||||
"SENDER": {
|
||||
"BOT": "رد آلي"
|
||||
|
||||
@@ -131,11 +131,13 @@
|
||||
},
|
||||
"CONTACTS_PAGE": {
|
||||
"HEADER": "جهات الاتصال",
|
||||
"FIELDS": "Contact fields",
|
||||
"SEARCH_BUTTON": "بحث",
|
||||
"SEARCH_INPUT_PLACEHOLDER": "بحث عن جهات الاتصال",
|
||||
"LIST": {
|
||||
"LOADING_MESSAGE": "جاري تحميل جهات الاتصال...",
|
||||
"404": "لا توجد جهات اتصال تطابق بحثك 🔍",
|
||||
"NO_CONTACTS": "There are no available contacts",
|
||||
"TABLE_HEADER": {
|
||||
"NAME": "الاسم",
|
||||
"PHONE_NUMBER": "رقم الهاتف",
|
||||
@@ -203,5 +205,33 @@
|
||||
"PLACEHOLDER": "Eg: 11901 "
|
||||
}
|
||||
}
|
||||
},
|
||||
"MERGE_CONTACTS": {
|
||||
"TITLE": "Merge contacts",
|
||||
"DESCRIPTION": "Merge contact is helpful when you have duplicated entries of the same contact. Merging action takes a primary contact and a child contact. After merging, all details in the primary contact will remain the same. If the primary contact doesn't have a field, then the value from the child contact will be used after merging. If a conflict happens, fields in primary contact will remain unaffected, but fields from secondary will be copied to the custom attributes in the primary contact.",
|
||||
"PRIMARY": {
|
||||
"TITLE": "Primary contact"
|
||||
},
|
||||
"CHILD": {
|
||||
"TITLE": "Contact to merge",
|
||||
"PLACEHOLDER": "Choose a contact"
|
||||
},
|
||||
"SUMMARY": {
|
||||
"TITLE": "Summary",
|
||||
"DELETE_WARNING": "Contact of <strong>%{childContactName}</strong>will be deleted.",
|
||||
"ATTRIBUTE_WARNING": "Contact details of <strong>%{childContactName}</strong> will be copied to <strong>%{primaryContactName}</strong>."
|
||||
},
|
||||
"SEARCH": {
|
||||
"ERROR": "ERROR_MESSAGE"
|
||||
},
|
||||
"FORM": {
|
||||
"SUBMIT": " Merge contacts",
|
||||
"CANCEL": "إلغاء",
|
||||
"CHILD_CONTACT": {
|
||||
"ERROR": "Select a child contact to merge"
|
||||
},
|
||||
"SUCCESS_MESSAGE": "Contact merged successfully",
|
||||
"ERROR_MESSAGE": "Could not merge contcts, try again!"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,11 @@
|
||||
"REMOVE_SELECTION": "إزالة التحديد",
|
||||
"DOWNLOAD": "تنزيل",
|
||||
"UPLOADING_ATTACHMENTS": "جاري تحميل المرفقات...",
|
||||
"SUCCESS_DELETE_MESSAGE": "Message deleted successfully",
|
||||
"FAIL_DELETE_MESSSAGE": "Couldn't delete message! Try again",
|
||||
"NO_RESPONSE": "لا توجد استجابة",
|
||||
"RATING_TITLE": "Rating",
|
||||
"FEEDBACK_TITLE": "Feedback",
|
||||
"HEADER": {
|
||||
"RESOLVE_ACTION": "إغلاق المحادثة",
|
||||
"REOPEN_ACTION": "إعادة فتح",
|
||||
@@ -52,7 +56,8 @@
|
||||
"TIP_FORMAT_ICON": "عرض محرر النصوص",
|
||||
"TIP_EMOJI_ICON": "إظهار قائمة الرموز التعبيرية",
|
||||
"TIP_ATTACH_ICON": "إرفاق الملفات",
|
||||
"ENTER_TO_SEND": "زر الإدخل للإرسال"
|
||||
"ENTER_TO_SEND": "زر الإدخل للإرسال",
|
||||
"DRAG_DROP": "Drag and drop here to attach"
|
||||
},
|
||||
"VISIBLE_TO_AGENTS": "ملاحظة خاصة: مرئية فقط لأعضاء فريق العمل والموظفين",
|
||||
"CHANGE_STATUS": "تم تغيير حالة المحادثة",
|
||||
@@ -64,6 +69,10 @@
|
||||
"SELECT_AGENT": "اختر وكيل",
|
||||
"REMOVE": "حذف",
|
||||
"ASSIGN": "تكليف"
|
||||
},
|
||||
"CONTEXT_MENU": {
|
||||
"COPY": "نسخ",
|
||||
"DELETE": "حذف"
|
||||
}
|
||||
},
|
||||
"EMAIL_TRANSCRIPT": {
|
||||
|
||||
6
app/javascript/dashboard/i18n/locale/ar/csatMgmt.json
Normal file
6
app/javascript/dashboard/i18n/locale/ar/csatMgmt.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"CSAT": {
|
||||
"TITLE": "Rate your conversation",
|
||||
"PLACEHOLDER": "Tell us more..."
|
||||
}
|
||||
}
|
||||
@@ -127,6 +127,14 @@
|
||||
"ERROR_MESSAGE": "تعذر تكوين المصادقة بواسطة بيانات الاعتماد الخاصة بحسابك على Twilio، يرجى المحاولة مرة أخرى"
|
||||
}
|
||||
},
|
||||
"SMS": {
|
||||
"TITLE": "SMS Channel via Twilio",
|
||||
"DESC": "Start supporting your customers via SMS with Twilio integration."
|
||||
},
|
||||
"WHATSAPP": {
|
||||
"TITLE": "Whatsapp Channel via Twilio",
|
||||
"DESC": "Start supporting your customers via Whatsapp with Twilio integration."
|
||||
},
|
||||
"API_CHANNEL": {
|
||||
"TITLE": "قناة API",
|
||||
"DESC": "اربط مع قناة API وابدأ في دعم عملائك.",
|
||||
@@ -165,8 +173,8 @@
|
||||
"FINISH_MESSAGE": "بدء إعادة توجيه رسائل البريد الإلكتروني الخاصة بك إلى عنوان البريد الإلكتروني التالي."
|
||||
},
|
||||
"AUTH": {
|
||||
"TITLE": "القنوات",
|
||||
"DESC": "ندعم حالياً تثبيت برنامج الدردشة المباشرة على موقعك وربط صفحات الفيس بوك وحسابات تويتر. جار العمل على دعم المزيد من المنصات الأخرى مثل واتساب، و البريد الإلكتروني، و تلغرام و لاين، والتي ستكون متاحة قريباً."
|
||||
"TITLE": "Choose a channel",
|
||||
"DESC": "Chatwoot supports live-chat widget, Facebook page, Twitter profile, Whatsapp, Email etc., as channels. If you want to build a custom channel, you can create it using the API channel. Select one channel from the options below to proceed."
|
||||
},
|
||||
"AGENTS": {
|
||||
"TITLE": "موظف الدعم",
|
||||
@@ -216,6 +224,10 @@
|
||||
"EMAIL_COLLECT_BOX": {
|
||||
"ENABLED": "مفعل",
|
||||
"DISABLED": "معطّل"
|
||||
},
|
||||
"ENABLE_CSAT": {
|
||||
"ENABLED": "مفعل",
|
||||
"DISABLED": "معطّل"
|
||||
}
|
||||
},
|
||||
"DELETE": {
|
||||
@@ -255,6 +267,8 @@
|
||||
"ENABLE_EMAIL_COLLECT_BOX": "Enable email collect box",
|
||||
"ENABLE_EMAIL_COLLECT_BOX_SUB_TEXT": "Enable or disable email collect box on new conversation",
|
||||
"AUTO_ASSIGNMENT": "تفعيل الإسناد التلقائي",
|
||||
"ENABLE_CSAT": "Enable CSAT",
|
||||
"ENABLE_CSAT_SUB_TEXT": "Enable/Disable CSAT(Customer satisfaction) survey after resolving a conversation",
|
||||
"INBOX_UPDATE_TITLE": "إعدادات قناة التواصل",
|
||||
"INBOX_UPDATE_SUB_TEXT": "تحديث إعدادات قناة التواصل",
|
||||
"AUTO_ASSIGNMENT_SUB_TEXT": "تمكين أو تعطيل الإسناد التلقائي للمحادثات الجديدة إلى الموظفين المضافين إلى قناة التواصل هذه.",
|
||||
|
||||
@@ -66,6 +66,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"SLACK": {
|
||||
"HELP_TEXT": {
|
||||
"TITLE": "Using Slack Integration",
|
||||
"BODY": "<br/><p>Chatwoot will now sync all the incoming conversations into the <b><i>customer-conversations</i></b> channel inside your slack workplace.</p><p>Replying to a conversation thread in <b><i>customer-conversations</i></b> slack channel will create a response back to the customer through chatwoot.</p><p>Start the replies with <b><i>note:</i></b> to create private notes instead of replies.</p><p>If the replier on slack has an agent profile in chatwoot under the same email, the replies will be associated accordingly.</p><p>When the replier doesn't have an associated agent profile, the replies will be made from the bot profile.</p>"
|
||||
}
|
||||
},
|
||||
"DELETE": {
|
||||
"BUTTON_TEXT": "حذف",
|
||||
"API": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"REPORT": {
|
||||
"HEADER": "التقارير",
|
||||
"HEADER": "Overview",
|
||||
"LOADING_CHART": "تحميل بيانات الرسم البياني...",
|
||||
"NO_ENOUGH_DATA": "لم يتم جمع بيانات بقدر كافي لإنشاء التقرير، الرجاء المحاولة مرة أخرى لاحقاً.",
|
||||
"DOWNLOAD_AGENT_REPORTS": "تنزيل تقارير الوكيل",
|
||||
@@ -50,7 +50,41 @@
|
||||
{
|
||||
"id": 4,
|
||||
"name": "Last year"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "Custom date range"
|
||||
}
|
||||
]
|
||||
],
|
||||
"CUSTOM_DATE_RANGE": {
|
||||
"CONFIRM": "Apply",
|
||||
"PLACEHOLDER": "Select date range"
|
||||
}
|
||||
},
|
||||
"CSAT_REPORTS": {
|
||||
"HEADER": "CSAT Reports",
|
||||
"NO_RECORDS": "There are no CSAT survey responses available.",
|
||||
"TABLE": {
|
||||
"HEADER": {
|
||||
"CONTACT_NAME": "Contact",
|
||||
"AGENT_NAME": "Assigned agent",
|
||||
"RATING": "Rating",
|
||||
"FEEDBACK_TEXT": "Feedback comment"
|
||||
}
|
||||
},
|
||||
"METRIC": {
|
||||
"TOTAL_RESPONSES": {
|
||||
"LABEL": "Total responses",
|
||||
"TOOLTIP": "Total number of responses collected"
|
||||
},
|
||||
"SATISFACTION_SCORE": {
|
||||
"LABEL": "Satisfaction score",
|
||||
"TOOLTIP": "Total number of positive responses / Total number of responses * 100"
|
||||
},
|
||||
"RESPONSE_RATE": {
|
||||
"LABEL": "Response rate",
|
||||
"TOOLTIP": "Total number of responses / Total number of CSAT survey messages sent * 100"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,9 @@
|
||||
"AUDIO_NOTIFICATIONS_SECTION": {
|
||||
"TITLE": "الإشعارات الصوتية",
|
||||
"NOTE": "تمكين التنبيهات الصوتية في لوحة التحكم للرسائل والمحادثات الجديدة.",
|
||||
"ENABLE_AUDIO": "تشغيل إشعار صوتي عند إنشاء محادثة جديدة أو وصول رسائل جديدة"
|
||||
"NONE": "لا شيء",
|
||||
"ASSIGNED": "Assigned Conversations",
|
||||
"ALL_CONVERSATIONS": "All Conversations"
|
||||
},
|
||||
"EMAIL_NOTIFICATIONS_SECTION": {
|
||||
"TITLE": "إشعارات البريد الإلكتروني",
|
||||
@@ -139,7 +141,11 @@
|
||||
"ACCOUNT_SETTINGS": "إعدادات الحساب",
|
||||
"APPLICATIONS": "Applications",
|
||||
"LABELS": "الوسوم",
|
||||
"TEAMS": "الفرق"
|
||||
"TEAMS": "الفرق",
|
||||
"ALL_CONTACTS": "All Contacts",
|
||||
"TAGGED_WITH": "Tagged with",
|
||||
"REPORTS_OVERVIEW": "Overview",
|
||||
"CSAT": "CSAT"
|
||||
},
|
||||
"CREATE_ACCOUNT": {
|
||||
"NEW_ACCOUNT": "حساب جديد",
|
||||
|
||||
@@ -90,7 +90,7 @@
|
||||
}
|
||||
},
|
||||
"SEARCH": {
|
||||
"NO_RESULTS": "No s'han trobat agents."
|
||||
"NO_RESULTS": "No results found."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,17 @@
|
||||
"PLACEHOLDER": "Please enter the title of campaign",
|
||||
"ERROR": "Title is required"
|
||||
},
|
||||
"SCHEDULED_AT": {
|
||||
"LABEL": "Scheduled time",
|
||||
"PLACEHOLDER": "Please select the time",
|
||||
"CONFIRM": "Confirm",
|
||||
"ERROR": "Scheduled time is required"
|
||||
},
|
||||
"AUDIENCE": {
|
||||
"LABEL": "Audience",
|
||||
"PLACEHOLDER": "Select the customer labels",
|
||||
"ERROR": "Audience is required"
|
||||
},
|
||||
"MESSAGE": {
|
||||
"LABEL": "Missatge",
|
||||
"PLACEHOLDER": "Please enter the message of campaign",
|
||||
@@ -72,6 +83,7 @@
|
||||
"STATUS": "Estat",
|
||||
"SENDER": "Sender",
|
||||
"URL": "URL",
|
||||
"SCHEDULED_AT": "Scheduled time",
|
||||
"TIME_ON_PAGE": "Time(Seconds)",
|
||||
"CREATED_AT": "Created at"
|
||||
},
|
||||
@@ -82,7 +94,9 @@
|
||||
},
|
||||
"STATUS": {
|
||||
"ENABLED": "Habilita",
|
||||
"DISABLED": "Inhabilita"
|
||||
"DISABLED": "Inhabilita",
|
||||
"COMPLETED": "Completed",
|
||||
"ACTIVE": "Active"
|
||||
},
|
||||
"SENDER": {
|
||||
"BOT": "Bot"
|
||||
|
||||
@@ -131,11 +131,13 @@
|
||||
},
|
||||
"CONTACTS_PAGE": {
|
||||
"HEADER": "Contactes",
|
||||
"FIELDS": "Contact fields",
|
||||
"SEARCH_BUTTON": "Cercar",
|
||||
"SEARCH_INPUT_PLACEHOLDER": "Cerca de contactes",
|
||||
"LIST": {
|
||||
"LOADING_MESSAGE": "Carregant contactes...",
|
||||
"404": "No hi ha cap contacte que coincideixi amb la vostra cerca 🔍",
|
||||
"NO_CONTACTS": "There are no available contacts",
|
||||
"TABLE_HEADER": {
|
||||
"NAME": "Nom",
|
||||
"PHONE_NUMBER": "Número de telèfon",
|
||||
@@ -203,5 +205,33 @@
|
||||
"PLACEHOLDER": "Eg: 11901 "
|
||||
}
|
||||
}
|
||||
},
|
||||
"MERGE_CONTACTS": {
|
||||
"TITLE": "Merge contacts",
|
||||
"DESCRIPTION": "Merge contact is helpful when you have duplicated entries of the same contact. Merging action takes a primary contact and a child contact. After merging, all details in the primary contact will remain the same. If the primary contact doesn't have a field, then the value from the child contact will be used after merging. If a conflict happens, fields in primary contact will remain unaffected, but fields from secondary will be copied to the custom attributes in the primary contact.",
|
||||
"PRIMARY": {
|
||||
"TITLE": "Primary contact"
|
||||
},
|
||||
"CHILD": {
|
||||
"TITLE": "Contact to merge",
|
||||
"PLACEHOLDER": "Choose a contact"
|
||||
},
|
||||
"SUMMARY": {
|
||||
"TITLE": "Summary",
|
||||
"DELETE_WARNING": "Contact of <strong>%{childContactName}</strong>will be deleted.",
|
||||
"ATTRIBUTE_WARNING": "Contact details of <strong>%{childContactName}</strong> will be copied to <strong>%{primaryContactName}</strong>."
|
||||
},
|
||||
"SEARCH": {
|
||||
"ERROR": "ERROR_MESSAGE"
|
||||
},
|
||||
"FORM": {
|
||||
"SUBMIT": " Merge contacts",
|
||||
"CANCEL": "Cancel·la",
|
||||
"CHILD_CONTACT": {
|
||||
"ERROR": "Select a child contact to merge"
|
||||
},
|
||||
"SUCCESS_MESSAGE": "Contact merged successfully",
|
||||
"ERROR_MESSAGE": "Could not merge contcts, try again!"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,11 @@
|
||||
"REMOVE_SELECTION": "Elimina la selecció",
|
||||
"DOWNLOAD": "Descarrega",
|
||||
"UPLOADING_ATTACHMENTS": "Pujant fitxers adjunts...",
|
||||
"SUCCESS_DELETE_MESSAGE": "Message deleted successfully",
|
||||
"FAIL_DELETE_MESSSAGE": "Couldn't delete message! Try again",
|
||||
"NO_RESPONSE": "Sense resposta",
|
||||
"RATING_TITLE": "Rating",
|
||||
"FEEDBACK_TITLE": "Feedback",
|
||||
"HEADER": {
|
||||
"RESOLVE_ACTION": "Resoldre",
|
||||
"REOPEN_ACTION": "Tornar a obrir",
|
||||
@@ -52,7 +56,8 @@
|
||||
"TIP_FORMAT_ICON": "Mostra l'editor de text enriquit",
|
||||
"TIP_EMOJI_ICON": "Mostra la selecció d'emoticones",
|
||||
"TIP_ATTACH_ICON": "Ajuntar fitxers",
|
||||
"ENTER_TO_SEND": "Intro per enviar"
|
||||
"ENTER_TO_SEND": "Intro per enviar",
|
||||
"DRAG_DROP": "Drag and drop here to attach"
|
||||
},
|
||||
"VISIBLE_TO_AGENTS": "Nota privada: Només és visible per tu i el vostre equip",
|
||||
"CHANGE_STATUS": "Estat de la conversa canviat",
|
||||
@@ -64,6 +69,10 @@
|
||||
"SELECT_AGENT": "Seleccionar Agent",
|
||||
"REMOVE": "Suprimeix",
|
||||
"ASSIGN": "Assignar"
|
||||
},
|
||||
"CONTEXT_MENU": {
|
||||
"COPY": "Copia",
|
||||
"DELETE": "Esborrar"
|
||||
}
|
||||
},
|
||||
"EMAIL_TRANSCRIPT": {
|
||||
|
||||
6
app/javascript/dashboard/i18n/locale/ca/csatMgmt.json
Normal file
6
app/javascript/dashboard/i18n/locale/ca/csatMgmt.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"CSAT": {
|
||||
"TITLE": "Rate your conversation",
|
||||
"PLACEHOLDER": "Tell us more..."
|
||||
}
|
||||
}
|
||||
@@ -127,6 +127,14 @@
|
||||
"ERROR_MESSAGE": "No hem pogut autenticar les credencials de Twilio, prova de nou"
|
||||
}
|
||||
},
|
||||
"SMS": {
|
||||
"TITLE": "SMS Channel via Twilio",
|
||||
"DESC": "Start supporting your customers via SMS with Twilio integration."
|
||||
},
|
||||
"WHATSAPP": {
|
||||
"TITLE": "Whatsapp Channel via Twilio",
|
||||
"DESC": "Start supporting your customers via Whatsapp with Twilio integration."
|
||||
},
|
||||
"API_CHANNEL": {
|
||||
"TITLE": "Canal de l'API",
|
||||
"DESC": "Integra’t amb el canal API i comença a donar suport als teus clients.",
|
||||
@@ -165,8 +173,8 @@
|
||||
"FINISH_MESSAGE": "Comença a reenviar els teus correus electrònics a la següent adreça electrònica."
|
||||
},
|
||||
"AUTH": {
|
||||
"TITLE": "Canals",
|
||||
"DESC": "Actualment estan suportats widgets de xat en directe per a llocs web, pàgines de Facebook i perfils de Twitter. Estem treballant en més plataformes com Whatsapp, correu electrònic, Telegram i Line, que estaran disponibles en breu."
|
||||
"TITLE": "Choose a channel",
|
||||
"DESC": "Chatwoot supports live-chat widget, Facebook page, Twitter profile, Whatsapp, Email etc., as channels. If you want to build a custom channel, you can create it using the API channel. Select one channel from the options below to proceed."
|
||||
},
|
||||
"AGENTS": {
|
||||
"TITLE": "Agents",
|
||||
@@ -216,6 +224,10 @@
|
||||
"EMAIL_COLLECT_BOX": {
|
||||
"ENABLED": "Habilita",
|
||||
"DISABLED": "Inhabilita"
|
||||
},
|
||||
"ENABLE_CSAT": {
|
||||
"ENABLED": "Habilita",
|
||||
"DISABLED": "Inhabilita"
|
||||
}
|
||||
},
|
||||
"DELETE": {
|
||||
@@ -255,6 +267,8 @@
|
||||
"ENABLE_EMAIL_COLLECT_BOX": "Enable email collect box",
|
||||
"ENABLE_EMAIL_COLLECT_BOX_SUB_TEXT": "Enable or disable email collect box on new conversation",
|
||||
"AUTO_ASSIGNMENT": "Activa l'assignació automàtica",
|
||||
"ENABLE_CSAT": "Enable CSAT",
|
||||
"ENABLE_CSAT_SUB_TEXT": "Enable/Disable CSAT(Customer satisfaction) survey after resolving a conversation",
|
||||
"INBOX_UPDATE_TITLE": "Configuració de la safata d'entrada",
|
||||
"INBOX_UPDATE_SUB_TEXT": "Actualitza la configuració de la safata d'entrada",
|
||||
"AUTO_ASSIGNMENT_SUB_TEXT": "Activa o desactiva l'assignació automàtica d'agents disponibles a les noves converses",
|
||||
|
||||
@@ -66,6 +66,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"SLACK": {
|
||||
"HELP_TEXT": {
|
||||
"TITLE": "Using Slack Integration",
|
||||
"BODY": "<br/><p>Chatwoot will now sync all the incoming conversations into the <b><i>customer-conversations</i></b> channel inside your slack workplace.</p><p>Replying to a conversation thread in <b><i>customer-conversations</i></b> slack channel will create a response back to the customer through chatwoot.</p><p>Start the replies with <b><i>note:</i></b> to create private notes instead of replies.</p><p>If the replier on slack has an agent profile in chatwoot under the same email, the replies will be associated accordingly.</p><p>When the replier doesn't have an associated agent profile, the replies will be made from the bot profile.</p>"
|
||||
}
|
||||
},
|
||||
"DELETE": {
|
||||
"BUTTON_TEXT": "Suprimeix",
|
||||
"API": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"REPORT": {
|
||||
"HEADER": "Informes",
|
||||
"HEADER": "Overview",
|
||||
"LOADING_CHART": "S'estan carregant dades del gràfic...",
|
||||
"NO_ENOUGH_DATA": "No hem rebut suficients punts de dades per generar l'informe. Torneu-ho a provar més endavant.",
|
||||
"DOWNLOAD_AGENT_REPORTS": "Descarregar Informes d'Agent",
|
||||
@@ -50,7 +50,41 @@
|
||||
{
|
||||
"id": 4,
|
||||
"name": "Last year"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "Custom date range"
|
||||
}
|
||||
]
|
||||
],
|
||||
"CUSTOM_DATE_RANGE": {
|
||||
"CONFIRM": "Apply",
|
||||
"PLACEHOLDER": "Select date range"
|
||||
}
|
||||
},
|
||||
"CSAT_REPORTS": {
|
||||
"HEADER": "CSAT Reports",
|
||||
"NO_RECORDS": "There are no CSAT survey responses available.",
|
||||
"TABLE": {
|
||||
"HEADER": {
|
||||
"CONTACT_NAME": "Contact",
|
||||
"AGENT_NAME": "Assigned agent",
|
||||
"RATING": "Rating",
|
||||
"FEEDBACK_TEXT": "Feedback comment"
|
||||
}
|
||||
},
|
||||
"METRIC": {
|
||||
"TOTAL_RESPONSES": {
|
||||
"LABEL": "Total responses",
|
||||
"TOOLTIP": "Total number of responses collected"
|
||||
},
|
||||
"SATISFACTION_SCORE": {
|
||||
"LABEL": "Satisfaction score",
|
||||
"TOOLTIP": "Total number of positive responses / Total number of responses * 100"
|
||||
},
|
||||
"RESPONSE_RATE": {
|
||||
"LABEL": "Response rate",
|
||||
"TOOLTIP": "Total number of responses / Total number of CSAT survey messages sent * 100"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,9 @@
|
||||
"AUDIO_NOTIFICATIONS_SECTION": {
|
||||
"TITLE": "Audio Notifications",
|
||||
"NOTE": "Enable audio notifications in dashboard for new messages and conversations.",
|
||||
"ENABLE_AUDIO": "Play audio notification when a new conversation is created or new messages arrives"
|
||||
"NONE": "None",
|
||||
"ASSIGNED": "Assigned Conversations",
|
||||
"ALL_CONVERSATIONS": "All Conversations"
|
||||
},
|
||||
"EMAIL_NOTIFICATIONS_SECTION": {
|
||||
"TITLE": "Notificacions per correu electrònic",
|
||||
@@ -139,7 +141,11 @@
|
||||
"ACCOUNT_SETTINGS": "Configuració del compte",
|
||||
"APPLICATIONS": "Applications",
|
||||
"LABELS": "Etiquetes",
|
||||
"TEAMS": "Equips"
|
||||
"TEAMS": "Equips",
|
||||
"ALL_CONTACTS": "All Contacts",
|
||||
"TAGGED_WITH": "Tagged with",
|
||||
"REPORTS_OVERVIEW": "Overview",
|
||||
"CSAT": "CSAT"
|
||||
},
|
||||
"CREATE_ACCOUNT": {
|
||||
"NEW_ACCOUNT": "Compte nou",
|
||||
|
||||
@@ -90,7 +90,7 @@
|
||||
}
|
||||
},
|
||||
"SEARCH": {
|
||||
"NO_RESULTS": "Nebyli nalezeni žádní agenti."
|
||||
"NO_RESULTS": "Žádné výsledky."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,17 @@
|
||||
"PLACEHOLDER": "Please enter the title of campaign",
|
||||
"ERROR": "Title is required"
|
||||
},
|
||||
"SCHEDULED_AT": {
|
||||
"LABEL": "Scheduled time",
|
||||
"PLACEHOLDER": "Please select the time",
|
||||
"CONFIRM": "Confirm",
|
||||
"ERROR": "Scheduled time is required"
|
||||
},
|
||||
"AUDIENCE": {
|
||||
"LABEL": "Audience",
|
||||
"PLACEHOLDER": "Select the customer labels",
|
||||
"ERROR": "Audience is required"
|
||||
},
|
||||
"MESSAGE": {
|
||||
"LABEL": "Zpráva",
|
||||
"PLACEHOLDER": "Please enter the message of campaign",
|
||||
@@ -72,6 +83,7 @@
|
||||
"STATUS": "Stav",
|
||||
"SENDER": "Sender",
|
||||
"URL": "URL",
|
||||
"SCHEDULED_AT": "Scheduled time",
|
||||
"TIME_ON_PAGE": "Time(Seconds)",
|
||||
"CREATED_AT": "Created at"
|
||||
},
|
||||
@@ -82,7 +94,9 @@
|
||||
},
|
||||
"STATUS": {
|
||||
"ENABLED": "Povoleno",
|
||||
"DISABLED": "Zakázáno"
|
||||
"DISABLED": "Zakázáno",
|
||||
"COMPLETED": "Completed",
|
||||
"ACTIVE": "Active"
|
||||
},
|
||||
"SENDER": {
|
||||
"BOT": "Bot"
|
||||
|
||||
@@ -131,11 +131,13 @@
|
||||
},
|
||||
"CONTACTS_PAGE": {
|
||||
"HEADER": "Kontakty",
|
||||
"FIELDS": "Contact fields",
|
||||
"SEARCH_BUTTON": "Hledat",
|
||||
"SEARCH_INPUT_PLACEHOLDER": "Hledat kontakty",
|
||||
"LIST": {
|
||||
"LOADING_MESSAGE": "Načítání kontaktů...",
|
||||
"404": "Vašemu hledání neodpovídají žádné kontakty 🔍",
|
||||
"NO_CONTACTS": "There are no available contacts",
|
||||
"TABLE_HEADER": {
|
||||
"NAME": "Název",
|
||||
"PHONE_NUMBER": "Telefonní číslo",
|
||||
@@ -203,5 +205,33 @@
|
||||
"PLACEHOLDER": "Eg: 11901 "
|
||||
}
|
||||
}
|
||||
},
|
||||
"MERGE_CONTACTS": {
|
||||
"TITLE": "Merge contacts",
|
||||
"DESCRIPTION": "Merge contact is helpful when you have duplicated entries of the same contact. Merging action takes a primary contact and a child contact. After merging, all details in the primary contact will remain the same. If the primary contact doesn't have a field, then the value from the child contact will be used after merging. If a conflict happens, fields in primary contact will remain unaffected, but fields from secondary will be copied to the custom attributes in the primary contact.",
|
||||
"PRIMARY": {
|
||||
"TITLE": "Primary contact"
|
||||
},
|
||||
"CHILD": {
|
||||
"TITLE": "Contact to merge",
|
||||
"PLACEHOLDER": "Choose a contact"
|
||||
},
|
||||
"SUMMARY": {
|
||||
"TITLE": "Summary",
|
||||
"DELETE_WARNING": "Contact of <strong>%{childContactName}</strong>will be deleted.",
|
||||
"ATTRIBUTE_WARNING": "Contact details of <strong>%{childContactName}</strong> will be copied to <strong>%{primaryContactName}</strong>."
|
||||
},
|
||||
"SEARCH": {
|
||||
"ERROR": "ERROR_MESSAGE"
|
||||
},
|
||||
"FORM": {
|
||||
"SUBMIT": " Merge contacts",
|
||||
"CANCEL": "Zrušit",
|
||||
"CHILD_CONTACT": {
|
||||
"ERROR": "Select a child contact to merge"
|
||||
},
|
||||
"SUCCESS_MESSAGE": "Contact merged successfully",
|
||||
"ERROR_MESSAGE": "Could not merge contcts, try again!"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,11 @@
|
||||
"REMOVE_SELECTION": "Odstranit výběr",
|
||||
"DOWNLOAD": "Stáhnout",
|
||||
"UPLOADING_ATTACHMENTS": "Nahrávání příloh...",
|
||||
"SUCCESS_DELETE_MESSAGE": "Message deleted successfully",
|
||||
"FAIL_DELETE_MESSSAGE": "Couldn't delete message! Try again",
|
||||
"NO_RESPONSE": "Bez odpovědi",
|
||||
"RATING_TITLE": "Rating",
|
||||
"FEEDBACK_TITLE": "Feedback",
|
||||
"HEADER": {
|
||||
"RESOLVE_ACTION": "Vyřešit",
|
||||
"REOPEN_ACTION": "Znovu otevřít",
|
||||
@@ -52,7 +56,8 @@
|
||||
"TIP_FORMAT_ICON": "Zobrazit formátovaný textový editor",
|
||||
"TIP_EMOJI_ICON": "Zobrazit výběr emoji",
|
||||
"TIP_ATTACH_ICON": "Přiložit soubory",
|
||||
"ENTER_TO_SEND": "Enter to send"
|
||||
"ENTER_TO_SEND": "Enter to send",
|
||||
"DRAG_DROP": "Drag and drop here to attach"
|
||||
},
|
||||
"VISIBLE_TO_AGENTS": "Soukromá poznámka: Viditelné pouze pro vás a váš tým",
|
||||
"CHANGE_STATUS": "Stav konverzace byl změněn",
|
||||
@@ -64,6 +69,10 @@
|
||||
"SELECT_AGENT": "Vybrat agenta",
|
||||
"REMOVE": "Odebrat",
|
||||
"ASSIGN": "Přiřadit"
|
||||
},
|
||||
"CONTEXT_MENU": {
|
||||
"COPY": "Kopírovat",
|
||||
"DELETE": "Vymazat"
|
||||
}
|
||||
},
|
||||
"EMAIL_TRANSCRIPT": {
|
||||
|
||||
6
app/javascript/dashboard/i18n/locale/cs/csatMgmt.json
Normal file
6
app/javascript/dashboard/i18n/locale/cs/csatMgmt.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"CSAT": {
|
||||
"TITLE": "Rate your conversation",
|
||||
"PLACEHOLDER": "Tell us more..."
|
||||
}
|
||||
}
|
||||
@@ -127,6 +127,14 @@
|
||||
"ERROR_MESSAGE": "Nebyli jsme schopni ověřit přihlašovací údaje Twilia, zkuste to prosím znovu"
|
||||
}
|
||||
},
|
||||
"SMS": {
|
||||
"TITLE": "SMS Channel via Twilio",
|
||||
"DESC": "Start supporting your customers via SMS with Twilio integration."
|
||||
},
|
||||
"WHATSAPP": {
|
||||
"TITLE": "Whatsapp Channel via Twilio",
|
||||
"DESC": "Start supporting your customers via Whatsapp with Twilio integration."
|
||||
},
|
||||
"API_CHANNEL": {
|
||||
"TITLE": "API Channel",
|
||||
"DESC": "Integrate with API channel and start supporting your customers.",
|
||||
@@ -165,8 +173,8 @@
|
||||
"FINISH_MESSAGE": "Start forwarding your emails to the following email address."
|
||||
},
|
||||
"AUTH": {
|
||||
"TITLE": "Kanály",
|
||||
"DESC": "V současné době podporujeme jako platformu widgety živého chatu, Facebook stránky a Twitter profily. Máme více platforem jako Whatsapp, Email, Telegram a Line v dílech, což bude brzy ven."
|
||||
"TITLE": "Choose a channel",
|
||||
"DESC": "Chatwoot supports live-chat widget, Facebook page, Twitter profile, Whatsapp, Email etc., as channels. If you want to build a custom channel, you can create it using the API channel. Select one channel from the options below to proceed."
|
||||
},
|
||||
"AGENTS": {
|
||||
"TITLE": "Agenti",
|
||||
@@ -216,6 +224,10 @@
|
||||
"EMAIL_COLLECT_BOX": {
|
||||
"ENABLED": "Povoleno",
|
||||
"DISABLED": "Zakázáno"
|
||||
},
|
||||
"ENABLE_CSAT": {
|
||||
"ENABLED": "Povoleno",
|
||||
"DISABLED": "Zakázáno"
|
||||
}
|
||||
},
|
||||
"DELETE": {
|
||||
@@ -255,6 +267,8 @@
|
||||
"ENABLE_EMAIL_COLLECT_BOX": "Enable email collect box",
|
||||
"ENABLE_EMAIL_COLLECT_BOX_SUB_TEXT": "Enable or disable email collect box on new conversation",
|
||||
"AUTO_ASSIGNMENT": "Povolit automatické přiřazení",
|
||||
"ENABLE_CSAT": "Enable CSAT",
|
||||
"ENABLE_CSAT_SUB_TEXT": "Enable/Disable CSAT(Customer satisfaction) survey after resolving a conversation",
|
||||
"INBOX_UPDATE_TITLE": "Nastavení doručené pošty",
|
||||
"INBOX_UPDATE_SUB_TEXT": "Aktualizujte nastavení doručené pošty",
|
||||
"AUTO_ASSIGNMENT_SUB_TEXT": "Povolit nebo zakázat automatické přiřazování nových konverzací agentům přidaným do této schránky.",
|
||||
|
||||
@@ -66,6 +66,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"SLACK": {
|
||||
"HELP_TEXT": {
|
||||
"TITLE": "Using Slack Integration",
|
||||
"BODY": "<br/><p>Chatwoot will now sync all the incoming conversations into the <b><i>customer-conversations</i></b> channel inside your slack workplace.</p><p>Replying to a conversation thread in <b><i>customer-conversations</i></b> slack channel will create a response back to the customer through chatwoot.</p><p>Start the replies with <b><i>note:</i></b> to create private notes instead of replies.</p><p>If the replier on slack has an agent profile in chatwoot under the same email, the replies will be associated accordingly.</p><p>When the replier doesn't have an associated agent profile, the replies will be made from the bot profile.</p>"
|
||||
}
|
||||
},
|
||||
"DELETE": {
|
||||
"BUTTON_TEXT": "Vymazat",
|
||||
"API": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"REPORT": {
|
||||
"HEADER": "Zprávy",
|
||||
"HEADER": "Overview",
|
||||
"LOADING_CHART": "Načítání dat mapy...",
|
||||
"NO_ENOUGH_DATA": "Pro vytvoření hlášení jsme neobdrželi dostatek dat, zkuste to prosím později.",
|
||||
"DOWNLOAD_AGENT_REPORTS": "Stáhnout reporty agentů",
|
||||
@@ -50,7 +50,41 @@
|
||||
{
|
||||
"id": 4,
|
||||
"name": "Last year"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "Custom date range"
|
||||
}
|
||||
]
|
||||
],
|
||||
"CUSTOM_DATE_RANGE": {
|
||||
"CONFIRM": "Apply",
|
||||
"PLACEHOLDER": "Select date range"
|
||||
}
|
||||
},
|
||||
"CSAT_REPORTS": {
|
||||
"HEADER": "CSAT Reports",
|
||||
"NO_RECORDS": "There are no CSAT survey responses available.",
|
||||
"TABLE": {
|
||||
"HEADER": {
|
||||
"CONTACT_NAME": "Contact",
|
||||
"AGENT_NAME": "Assigned agent",
|
||||
"RATING": "Rating",
|
||||
"FEEDBACK_TEXT": "Feedback comment"
|
||||
}
|
||||
},
|
||||
"METRIC": {
|
||||
"TOTAL_RESPONSES": {
|
||||
"LABEL": "Total responses",
|
||||
"TOOLTIP": "Total number of responses collected"
|
||||
},
|
||||
"SATISFACTION_SCORE": {
|
||||
"LABEL": "Satisfaction score",
|
||||
"TOOLTIP": "Total number of positive responses / Total number of responses * 100"
|
||||
},
|
||||
"RESPONSE_RATE": {
|
||||
"LABEL": "Response rate",
|
||||
"TOOLTIP": "Total number of responses / Total number of CSAT survey messages sent * 100"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,9 @@
|
||||
"AUDIO_NOTIFICATIONS_SECTION": {
|
||||
"TITLE": "Audio Notifications",
|
||||
"NOTE": "Enable audio notifications in dashboard for new messages and conversations.",
|
||||
"ENABLE_AUDIO": "Play audio notification when a new conversation is created or new messages arrives"
|
||||
"NONE": "Nic",
|
||||
"ASSIGNED": "Assigned Conversations",
|
||||
"ALL_CONVERSATIONS": "All Conversations"
|
||||
},
|
||||
"EMAIL_NOTIFICATIONS_SECTION": {
|
||||
"TITLE": "E-mailová oznámení",
|
||||
@@ -139,7 +141,11 @@
|
||||
"ACCOUNT_SETTINGS": "Nastavení účtu",
|
||||
"APPLICATIONS": "Applications",
|
||||
"LABELS": "Štítky",
|
||||
"TEAMS": "Týmy"
|
||||
"TEAMS": "Týmy",
|
||||
"ALL_CONTACTS": "All Contacts",
|
||||
"TAGGED_WITH": "Tagged with",
|
||||
"REPORTS_OVERVIEW": "Overview",
|
||||
"CSAT": "CSAT"
|
||||
},
|
||||
"CREATE_ACCOUNT": {
|
||||
"NEW_ACCOUNT": "Nový účet",
|
||||
|
||||
@@ -90,7 +90,7 @@
|
||||
}
|
||||
},
|
||||
"SEARCH": {
|
||||
"NO_RESULTS": "Ingen agenter fundet."
|
||||
"NO_RESULTS": "No results found."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,17 @@
|
||||
"PLACEHOLDER": "Please enter the title of campaign",
|
||||
"ERROR": "Title is required"
|
||||
},
|
||||
"SCHEDULED_AT": {
|
||||
"LABEL": "Scheduled time",
|
||||
"PLACEHOLDER": "Please select the time",
|
||||
"CONFIRM": "Confirm",
|
||||
"ERROR": "Scheduled time is required"
|
||||
},
|
||||
"AUDIENCE": {
|
||||
"LABEL": "Audience",
|
||||
"PLACEHOLDER": "Select the customer labels",
|
||||
"ERROR": "Audience is required"
|
||||
},
|
||||
"MESSAGE": {
|
||||
"LABEL": "Message",
|
||||
"PLACEHOLDER": "Please enter the message of campaign",
|
||||
@@ -72,6 +83,7 @@
|
||||
"STATUS": "Status",
|
||||
"SENDER": "Sender",
|
||||
"URL": "URL",
|
||||
"SCHEDULED_AT": "Scheduled time",
|
||||
"TIME_ON_PAGE": "Time(Seconds)",
|
||||
"CREATED_AT": "Created at"
|
||||
},
|
||||
@@ -82,7 +94,9 @@
|
||||
},
|
||||
"STATUS": {
|
||||
"ENABLED": "Aktiveret",
|
||||
"DISABLED": "Deaktiveret"
|
||||
"DISABLED": "Deaktiveret",
|
||||
"COMPLETED": "Completed",
|
||||
"ACTIVE": "Active"
|
||||
},
|
||||
"SENDER": {
|
||||
"BOT": "Bot"
|
||||
|
||||
@@ -131,11 +131,13 @@
|
||||
},
|
||||
"CONTACTS_PAGE": {
|
||||
"HEADER": "Kontakter",
|
||||
"FIELDS": "Contact fields",
|
||||
"SEARCH_BUTTON": "Søg",
|
||||
"SEARCH_INPUT_PLACEHOLDER": "Søg efter kontakter",
|
||||
"LIST": {
|
||||
"LOADING_MESSAGE": "Indlæser kontakter...",
|
||||
"404": "Ingen kontakter matcher din søgning 🔍",
|
||||
"NO_CONTACTS": "There are no available contacts",
|
||||
"TABLE_HEADER": {
|
||||
"NAME": "Navn",
|
||||
"PHONE_NUMBER": "Telefonnummer",
|
||||
@@ -203,5 +205,33 @@
|
||||
"PLACEHOLDER": "Eg: 11901 "
|
||||
}
|
||||
}
|
||||
},
|
||||
"MERGE_CONTACTS": {
|
||||
"TITLE": "Merge contacts",
|
||||
"DESCRIPTION": "Merge contact is helpful when you have duplicated entries of the same contact. Merging action takes a primary contact and a child contact. After merging, all details in the primary contact will remain the same. If the primary contact doesn't have a field, then the value from the child contact will be used after merging. If a conflict happens, fields in primary contact will remain unaffected, but fields from secondary will be copied to the custom attributes in the primary contact.",
|
||||
"PRIMARY": {
|
||||
"TITLE": "Primary contact"
|
||||
},
|
||||
"CHILD": {
|
||||
"TITLE": "Contact to merge",
|
||||
"PLACEHOLDER": "Choose a contact"
|
||||
},
|
||||
"SUMMARY": {
|
||||
"TITLE": "Summary",
|
||||
"DELETE_WARNING": "Contact of <strong>%{childContactName}</strong>will be deleted.",
|
||||
"ATTRIBUTE_WARNING": "Contact details of <strong>%{childContactName}</strong> will be copied to <strong>%{primaryContactName}</strong>."
|
||||
},
|
||||
"SEARCH": {
|
||||
"ERROR": "ERROR_MESSAGE"
|
||||
},
|
||||
"FORM": {
|
||||
"SUBMIT": " Merge contacts",
|
||||
"CANCEL": "Annuller",
|
||||
"CHILD_CONTACT": {
|
||||
"ERROR": "Select a child contact to merge"
|
||||
},
|
||||
"SUCCESS_MESSAGE": "Contact merged successfully",
|
||||
"ERROR_MESSAGE": "Could not merge contcts, try again!"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,11 @@
|
||||
"REMOVE_SELECTION": "Fjern Markering",
|
||||
"DOWNLOAD": "Download",
|
||||
"UPLOADING_ATTACHMENTS": "Uploading attachments...",
|
||||
"SUCCESS_DELETE_MESSAGE": "Message deleted successfully",
|
||||
"FAIL_DELETE_MESSSAGE": "Couldn't delete message! Try again",
|
||||
"NO_RESPONSE": "No response",
|
||||
"RATING_TITLE": "Rating",
|
||||
"FEEDBACK_TITLE": "Feedback",
|
||||
"HEADER": {
|
||||
"RESOLVE_ACTION": "Løs",
|
||||
"REOPEN_ACTION": "Genåben",
|
||||
@@ -52,7 +56,8 @@
|
||||
"TIP_FORMAT_ICON": "Show rich text editor",
|
||||
"TIP_EMOJI_ICON": "Show emoji selector",
|
||||
"TIP_ATTACH_ICON": "Attach files",
|
||||
"ENTER_TO_SEND": "Enter to send"
|
||||
"ENTER_TO_SEND": "Enter to send",
|
||||
"DRAG_DROP": "Drag and drop here to attach"
|
||||
},
|
||||
"VISIBLE_TO_AGENTS": "Privat Note: Kun synlig for dig og dit team",
|
||||
"CHANGE_STATUS": "Samtalestatus ændret",
|
||||
@@ -64,6 +69,10 @@
|
||||
"SELECT_AGENT": "Select Agent",
|
||||
"REMOVE": "Fjern",
|
||||
"ASSIGN": "Assign"
|
||||
},
|
||||
"CONTEXT_MENU": {
|
||||
"COPY": "Kopiér",
|
||||
"DELETE": "Slet"
|
||||
}
|
||||
},
|
||||
"EMAIL_TRANSCRIPT": {
|
||||
|
||||
6
app/javascript/dashboard/i18n/locale/da/csatMgmt.json
Normal file
6
app/javascript/dashboard/i18n/locale/da/csatMgmt.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"CSAT": {
|
||||
"TITLE": "Rate your conversation",
|
||||
"PLACEHOLDER": "Tell us more..."
|
||||
}
|
||||
}
|
||||
@@ -127,6 +127,14 @@
|
||||
"ERROR_MESSAGE": "Vi var ikke i stand til at godkende Twilio legitimationsoplysninger, prøv igen"
|
||||
}
|
||||
},
|
||||
"SMS": {
|
||||
"TITLE": "SMS Channel via Twilio",
|
||||
"DESC": "Start supporting your customers via SMS with Twilio integration."
|
||||
},
|
||||
"WHATSAPP": {
|
||||
"TITLE": "Whatsapp Channel via Twilio",
|
||||
"DESC": "Start supporting your customers via Whatsapp with Twilio integration."
|
||||
},
|
||||
"API_CHANNEL": {
|
||||
"TITLE": "API Kanal",
|
||||
"DESC": "Integrer med API-kanal og begynd at supportere dine kunder.",
|
||||
@@ -165,8 +173,8 @@
|
||||
"FINISH_MESSAGE": "Begynd at videresende dine e-mails til følgende e-mailadresse."
|
||||
},
|
||||
"AUTH": {
|
||||
"TITLE": "Kanaler",
|
||||
"DESC": "Currently we support Website live chat widgets, Facebook Pages and Twitter profiles as platforms. We have more platforms like Whatsapp, Email, Telegram and Line in the works, which will be out soon."
|
||||
"TITLE": "Choose a channel",
|
||||
"DESC": "Chatwoot supports live-chat widget, Facebook page, Twitter profile, Whatsapp, Email etc., as channels. If you want to build a custom channel, you can create it using the API channel. Select one channel from the options below to proceed."
|
||||
},
|
||||
"AGENTS": {
|
||||
"TITLE": "Agenter",
|
||||
@@ -216,6 +224,10 @@
|
||||
"EMAIL_COLLECT_BOX": {
|
||||
"ENABLED": "Aktiveret",
|
||||
"DISABLED": "Deaktiveret"
|
||||
},
|
||||
"ENABLE_CSAT": {
|
||||
"ENABLED": "Aktiveret",
|
||||
"DISABLED": "Deaktiveret"
|
||||
}
|
||||
},
|
||||
"DELETE": {
|
||||
@@ -255,6 +267,8 @@
|
||||
"ENABLE_EMAIL_COLLECT_BOX": "Enable email collect box",
|
||||
"ENABLE_EMAIL_COLLECT_BOX_SUB_TEXT": "Enable or disable email collect box on new conversation",
|
||||
"AUTO_ASSIGNMENT": "Aktiver automatisk tildeling",
|
||||
"ENABLE_CSAT": "Enable CSAT",
|
||||
"ENABLE_CSAT_SUB_TEXT": "Enable/Disable CSAT(Customer satisfaction) survey after resolving a conversation",
|
||||
"INBOX_UPDATE_TITLE": "Indbakke Indstillinger",
|
||||
"INBOX_UPDATE_SUB_TEXT": "Opdater dine indbakkeindstillinger",
|
||||
"AUTO_ASSIGNMENT_SUB_TEXT": "Aktiver eller deaktiver automatisk tildeling af nye samtaler til agenter tilføjet til denne indbakke.",
|
||||
|
||||
@@ -66,6 +66,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"SLACK": {
|
||||
"HELP_TEXT": {
|
||||
"TITLE": "Using Slack Integration",
|
||||
"BODY": "<br/><p>Chatwoot will now sync all the incoming conversations into the <b><i>customer-conversations</i></b> channel inside your slack workplace.</p><p>Replying to a conversation thread in <b><i>customer-conversations</i></b> slack channel will create a response back to the customer through chatwoot.</p><p>Start the replies with <b><i>note:</i></b> to create private notes instead of replies.</p><p>If the replier on slack has an agent profile in chatwoot under the same email, the replies will be associated accordingly.</p><p>When the replier doesn't have an associated agent profile, the replies will be made from the bot profile.</p>"
|
||||
}
|
||||
},
|
||||
"DELETE": {
|
||||
"BUTTON_TEXT": "Slet",
|
||||
"API": {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user