Merge branch 'release/2.7.0'

This commit is contained in:
Sojan 2022-07-18 16:48:01 +02:00
commit 628f40c2ab
756 changed files with 18314 additions and 4376 deletions

View File

@ -100,6 +100,10 @@ jobs:
- run: - run:
name: Rubocop name: Rubocop
command: bundle exec rubocop command: bundle exec rubocop
# - run:
# name: Brakeman
# command: bundle exec brakeman
- run: - run:
name: eslint name: eslint

View File

@ -5,4 +5,4 @@ FROM ghcr.io/chatwoot/chatwoot_codespace:latest
# Do the set up required for chatwoot app # Do the set up required for chatwoot app
WORKDIR /workspace WORKDIR /workspace
COPY . /workspace COPY . /workspace
RUN yarn && gem install bundler && bundle install RUN yarn && gem install bundler && bundle install

View File

@ -1,6 +1,6 @@
# pre-build stage
ARG VARIANT=3 ARG VARIANT=ubuntu-20.04
FROM mcr.microsoft.com/vscode/devcontainers/ruby:${VARIANT} FROM mcr.microsoft.com/vscode/devcontainers/base:${VARIANT}
# Update args in docker-compose.yaml to set the UID/GID of the "vscode" user. # Update args in docker-compose.yaml to set the UID/GID of the "vscode" user.
ARG USER_UID=1000 ARG USER_UID=1000
@ -11,23 +11,36 @@ RUN if [ "$USER_GID" != "1000" ] || [ "$USER_UID" != "1000" ]; then \
&& chmod -R $USER_UID:$USER_GID /home/vscode; \ && chmod -R $USER_UID:$USER_GID /home/vscode; \
fi 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 \ RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
&& apt-get -y install --no-install-recommends \ && apt-get -y install --no-install-recommends \
build-essential \
libssl-dev \ libssl-dev \
zlib1g-dev \
gnupg2 \
tar \ tar \
tzdata \ tzdata \
postgresql-client \ postgresql-client \
libpq-dev \
yarn \ yarn \
git \ git \
imagemagick \ imagemagick \
tmux \ tmux \
zsh zsh \
git-flow \
npm
# Install rbenv and ruby
ARG RUBY_VERSION="3.0.4"
RUN git clone https://github.com/rbenv/rbenv.git ~/.rbenv \
&& echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc \
&& echo 'eval "$(rbenv init -)"' >> ~/.bashrc
ENV PATH "/root/.rbenv/bin/:/root/.rbenv/shims/:$PATH"
RUN git clone https://github.com/rbenv/ruby-build.git && \
PREFIX=/usr/local ./ruby-build/install.sh
RUN rbenv install $RUBY_VERSION && \
rbenv global $RUBY_VERSION && \
rbenv versions
# Install overmind # Install overmind
RUN curl -L https://github.com/DarthSim/overmind/releases/download/v2.1.0/overmind-v2.1.0-linux-amd64.gz > overmind.gz \ RUN curl -L https://github.com/DarthSim/overmind/releases/download/v2.1.0/overmind-v2.1.0-linux-amd64.gz > overmind.gz \
@ -35,11 +48,25 @@ RUN curl -L https://github.com/DarthSim/overmind/releases/download/v2.1.0/overmi
&& sudo mv overmind /usr/local/bin \ && sudo mv overmind /usr/local/bin \
&& chmod +x /usr/local/bin/overmind && chmod +x /usr/local/bin/overmind
# Install gh
RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg \
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null \
&& sudo apt update \
&& sudo apt install gh
# Do the set up required for chatwoot app # Do the set up required for chatwoot app
WORKDIR /workspace WORKDIR /workspace
COPY . /workspace COPY . /workspace
RUN yarn
# set up ruby
COPY Gemfile Gemfile.lock ./ COPY Gemfile Gemfile.lock ./
RUN gem install bundler && bundle install RUN gem install bundler && bundle install
# set up node js
RUN npm install npm@latest -g && \
npm install n -g && \
n latest
RUN npm install --global yarn
RUN yarn

View File

@ -23,17 +23,18 @@
// 5432 postgres // 5432 postgres
// 6379 redis // 6379 redis
// 1025,8025 mailhog // 1025,8025 mailhog
"forwardPorts": [8025], "forwardPorts": [8025, 3000, 3035],
//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],
"postCreateCommand": ".devcontainer/scripts/setup.sh && bundle exec rake db:chatwoot_prepare && yarn", "postCreateCommand": ".devcontainer/scripts/setup.sh && bundle exec rake db:chatwoot_prepare && yarn",
"portsAttributes": { "portsAttributes": {
"3000": { "3000": {
"label": "Rails Server" "label": "Rails Server"
}, },
"3035": {
"label": "Webpack Dev Server"
},
"8025": { "8025": {
"label": "Mailhog UI" "label": "Mailhog UI"
} }
}, }
} }

View File

@ -6,3 +6,8 @@ sed -i -e "/FRONTEND_URL/ s/=.*/=https:\/\/$CODESPACE_NAME-3000.githubpreview.de
sed -i -e "/WEBPACKER_DEV_SERVER_PUBLIC/ s/=.*/=https:\/\/$CODESPACE_NAME-3035.githubpreview.dev/" .env sed -i -e "/WEBPACKER_DEV_SERVER_PUBLIC/ s/=.*/=https:\/\/$CODESPACE_NAME-3035.githubpreview.dev/" .env
# uncomment the webpacker env variable # uncomment the webpacker env variable
sed -i -e '/WEBPACKER_DEV_SERVER_PUBLIC/s/^# //' .env sed -i -e '/WEBPACKER_DEV_SERVER_PUBLIC/s/^# //' .env
# fix the error with webpacker
echo 'export NODE_OPTIONS=--openssl-legacy-provider' >> ~/.zshrc
# codespaces make the ports public
gh codespace ports visibility 3000:public 3035:public 8025:public -c $CODESPACE_NAME

View File

@ -1,5 +1,10 @@
module.exports = { module.exports = {
extends: ['airbnb-base/legacy', 'prettier', 'plugin:vue/recommended'], extends: [
'airbnb-base/legacy',
'prettier',
'plugin:vue/recommended',
'plugin:storybook/recommended',
],
parserOptions: { parserOptions: {
parser: 'babel-eslint', parser: 'babel-eslint',
ecmaVersion: 2020, ecmaVersion: 2020,

View File

@ -4,6 +4,7 @@ import Vuex from 'vuex';
import VueI18n from 'vue-i18n'; import VueI18n from 'vue-i18n';
import Vuelidate from 'vuelidate'; import Vuelidate from 'vuelidate';
import Multiselect from 'vue-multiselect'; import Multiselect from 'vue-multiselect';
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon';
import WootUiKit from '../app/javascript/dashboard/components'; import WootUiKit from '../app/javascript/dashboard/components';
import i18n from '../app/javascript/dashboard/i18n'; import i18n from '../app/javascript/dashboard/i18n';
@ -15,6 +16,7 @@ Vue.use(Vuelidate);
Vue.use(WootUiKit); Vue.use(WootUiKit);
Vue.use(Vuex); Vue.use(Vuex);
Vue.component('multiselect', Multiselect); Vue.component('multiselect', Multiselect);
Vue.component('fluent-icon', FluentIcon);
const store = new Vuex.Store({}); const store = new Vuex.Store({});
const i18nConfig = new VueI18n({ const i18nConfig = new VueI18n({

11
Gemfile
View File

@ -4,7 +4,7 @@ ruby '3.0.4'
##-- base gems for rails --## ##-- base gems for rails --##
gem 'rack-cors', require: 'rack/cors' gem 'rack-cors', require: 'rack/cors'
gem 'rails' gem 'rails', '~>6.1'
# Reduces boot times through caching; required in config/boot.rb # Reduces boot times through caching; required in config/boot.rb
gem 'bootsnap', require: false gem 'bootsnap', require: false
@ -78,7 +78,7 @@ gem 'wisper', '2.0.0'
# TODO: bump up gem to 2.0 # TODO: bump up gem to 2.0
gem 'facebook-messenger' gem 'facebook-messenger'
gem 'line-bot-api' gem 'line-bot-api'
gem 'twilio-ruby', '~> 5.32.0' gem 'twilio-ruby', '~> 5.66'
# twitty will handle subscription of twitter account events # twitty will handle subscription of twitter account events
# gem 'twitty', git: 'https://github.com/chatwoot/twitty' # gem 'twitty', git: 'https://github.com/chatwoot/twitty'
gem 'twitty' gem 'twitty'
@ -89,10 +89,6 @@ gem 'slack-ruby-client'
# for dialogflow integrations # for dialogflow integrations
gem 'google-cloud-dialogflow' gem 'google-cloud-dialogflow'
##--- gems for debugging and error reporting ---##
# static analysis
gem 'brakeman'
##-- apm and error monitoring ---# ##-- apm and error monitoring ---#
gem 'ddtrace' gem 'ddtrace'
gem 'newrelic_rpm' gem 'newrelic_rpm'
@ -160,6 +156,9 @@ end
group :development, :test do group :development, :test do
gem 'active_record_query_trace' gem 'active_record_query_trace'
##--- gems for debugging and error reporting ---##
# static analysis
gem 'brakeman'
gem 'bundle-audit', require: false gem 'bundle-audit', require: false
gem 'byebug', platform: :mri gem 'byebug', platform: :mri
gem 'climate_control' gem 'climate_control'

View File

@ -9,63 +9,63 @@ GIT
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
actioncable (6.1.5.1) actioncable (6.1.6.1)
actionpack (= 6.1.5.1) actionpack (= 6.1.6.1)
activesupport (= 6.1.5.1) activesupport (= 6.1.6.1)
nio4r (~> 2.0) nio4r (~> 2.0)
websocket-driver (>= 0.6.1) websocket-driver (>= 0.6.1)
actionmailbox (6.1.5.1) actionmailbox (6.1.6.1)
actionpack (= 6.1.5.1) actionpack (= 6.1.6.1)
activejob (= 6.1.5.1) activejob (= 6.1.6.1)
activerecord (= 6.1.5.1) activerecord (= 6.1.6.1)
activestorage (= 6.1.5.1) activestorage (= 6.1.6.1)
activesupport (= 6.1.5.1) activesupport (= 6.1.6.1)
mail (>= 2.7.1) mail (>= 2.7.1)
actionmailer (6.1.5.1) actionmailer (6.1.6.1)
actionpack (= 6.1.5.1) actionpack (= 6.1.6.1)
actionview (= 6.1.5.1) actionview (= 6.1.6.1)
activejob (= 6.1.5.1) activejob (= 6.1.6.1)
activesupport (= 6.1.5.1) activesupport (= 6.1.6.1)
mail (~> 2.5, >= 2.5.4) mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
actionpack (6.1.5.1) actionpack (6.1.6.1)
actionview (= 6.1.5.1) actionview (= 6.1.6.1)
activesupport (= 6.1.5.1) activesupport (= 6.1.6.1)
rack (~> 2.0, >= 2.0.9) rack (~> 2.0, >= 2.0.9)
rack-test (>= 0.6.3) rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0)
actiontext (6.1.5.1) actiontext (6.1.6.1)
actionpack (= 6.1.5.1) actionpack (= 6.1.6.1)
activerecord (= 6.1.5.1) activerecord (= 6.1.6.1)
activestorage (= 6.1.5.1) activestorage (= 6.1.6.1)
activesupport (= 6.1.5.1) activesupport (= 6.1.6.1)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
actionview (6.1.5.1) actionview (6.1.6.1)
activesupport (= 6.1.5.1) activesupport (= 6.1.6.1)
builder (~> 3.1) builder (~> 3.1)
erubi (~> 1.4) erubi (~> 1.4)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.1, >= 1.2.0) rails-html-sanitizer (~> 1.1, >= 1.2.0)
active_record_query_trace (1.8) active_record_query_trace (1.8)
activejob (6.1.5.1) activejob (6.1.6.1)
activesupport (= 6.1.5.1) activesupport (= 6.1.6.1)
globalid (>= 0.3.6) globalid (>= 0.3.6)
activemodel (6.1.5.1) activemodel (6.1.6.1)
activesupport (= 6.1.5.1) activesupport (= 6.1.6.1)
activerecord (6.1.5.1) activerecord (6.1.6.1)
activemodel (= 6.1.5.1) activemodel (= 6.1.6.1)
activesupport (= 6.1.5.1) activesupport (= 6.1.6.1)
activerecord-import (1.3.0) activerecord-import (1.4.0)
activerecord (>= 4.2) activerecord (>= 4.2)
activestorage (6.1.5.1) activestorage (6.1.6.1)
actionpack (= 6.1.5.1) actionpack (= 6.1.6.1)
activejob (= 6.1.5.1) activejob (= 6.1.6.1)
activerecord (= 6.1.5.1) activerecord (= 6.1.6.1)
activesupport (= 6.1.5.1) activesupport (= 6.1.6.1)
marcel (~> 1.0) marcel (~> 1.0)
mini_mime (>= 1.1.0) mini_mime (>= 1.1.0)
activesupport (6.1.5.1) activesupport (6.1.6.1)
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2) i18n (>= 1.6, < 2)
minitest (>= 5.1) minitest (>= 5.1)
@ -91,20 +91,20 @@ GEM
ast (2.4.2) ast (2.4.2)
attr_extras (6.2.5) attr_extras (6.2.5)
aws-eventstream (1.2.0) aws-eventstream (1.2.0)
aws-partitions (1.556.0) aws-partitions (1.605.0)
aws-sdk-core (3.126.2) aws-sdk-core (3.131.2)
aws-eventstream (~> 1, >= 1.0.2) aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.525.0) aws-partitions (~> 1, >= 1.525.0)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.1)
jmespath (~> 1.0) jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.54.0) aws-sdk-kms (1.57.0)
aws-sdk-core (~> 3, >= 3.126.0) aws-sdk-core (~> 3, >= 3.127.0)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.112.0) aws-sdk-s3 (1.114.0)
aws-sdk-core (~> 3, >= 3.126.0) aws-sdk-core (~> 3, >= 3.127.0)
aws-sdk-kms (~> 1) aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.4) aws-sigv4 (~> 1.4)
aws-sigv4 (1.4.0) aws-sigv4 (1.5.0)
aws-eventstream (~> 1, >= 1.0.2) aws-eventstream (~> 1, >= 1.0.2)
azure-storage-blob (2.0.3) azure-storage-blob (2.0.3)
azure-storage-common (~> 2.0) azure-storage-common (~> 2.0)
@ -117,31 +117,31 @@ GEM
barnes (0.0.9) barnes (0.0.9)
multi_json (~> 1) multi_json (~> 1)
statsd-ruby (~> 1.1) statsd-ruby (~> 1.1)
bcrypt (3.1.16) bcrypt (3.1.18)
bindex (0.8.1) bindex (0.8.1)
bootsnap (1.10.3) bootsnap (1.12.0)
msgpack (~> 1.2) msgpack (~> 1.2)
brakeman (5.2.1) brakeman (5.2.3)
browser (5.3.1) browser (5.3.1)
builder (3.2.4) builder (3.2.4)
bullet (7.0.1) bullet (7.0.2)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
uniform_notifier (~> 1.11) uniform_notifier (~> 1.11)
bundle-audit (0.1.0) bundle-audit (0.1.0)
bundler-audit bundler-audit
bundler-audit (0.9.0.1) bundler-audit (0.9.1)
bundler (>= 1.2.0, < 3) bundler (>= 1.2.0, < 3)
thor (~> 1.0) thor (~> 1.0)
byebug (11.1.3) byebug (11.1.3)
climate_control (1.0.1) climate_control (1.1.1)
coderay (1.1.3) coderay (1.1.3)
commonmarker (0.23.4) commonmarker (0.23.5)
concurrent-ruby (1.1.10) concurrent-ruby (1.1.10)
connection_pool (2.2.5) connection_pool (2.2.5)
crack (0.4.5) crack (0.4.5)
rexml rexml
crass (1.0.6) crass (1.0.6)
cypress-on-rails (1.12.1) cypress-on-rails (1.13.1)
rack rack
database_cleaner (2.0.1) database_cleaner (2.0.1)
database_cleaner-active_record (~> 2.0.0) database_cleaner-active_record (~> 2.0.0)
@ -151,10 +151,12 @@ GEM
database_cleaner-core (2.0.1) database_cleaner-core (2.0.1)
datetime_picker_rails (0.0.7) datetime_picker_rails (0.0.7)
momentjs-rails (>= 2.8.1) momentjs-rails (>= 2.8.1)
ddtrace (0.54.2) ddtrace (1.2.0)
debase-ruby_core_source (<= 0.10.14) debase-ruby_core_source (= 0.10.16)
libddprof (~> 0.6.0.1.0)
libddwaf (~> 1.3.0.2.0)
msgpack msgpack
debase-ruby_core_source (0.10.14) debase-ruby_core_source (0.10.16)
declarative (0.0.20) declarative (0.0.20)
devise (4.8.1) devise (4.8.1)
bcrypt (~> 3.0) bcrypt (~> 3.0)
@ -176,7 +178,7 @@ GEM
dotenv-rails (2.7.6) dotenv-rails (2.7.6)
dotenv (= 2.7.6) dotenv (= 2.7.6)
railties (>= 3.2) railties (>= 3.2)
down (5.3.0) down (5.3.1)
addressable (~> 2.8) addressable (~> 2.8)
ecma-re-validator (0.4.0) ecma-re-validator (0.4.0)
regexp_parser (~> 2.2) regexp_parser (~> 2.2)
@ -188,36 +190,60 @@ GEM
facebook-messenger (2.0.1) facebook-messenger (2.0.1)
httparty (~> 0.13, >= 0.13.7) httparty (~> 0.13, >= 0.13.7)
rack (>= 1.4.5) rack (>= 1.4.5)
factory_bot (6.2.0) factory_bot (6.2.1)
activesupport (>= 5.0.0) activesupport (>= 5.0.0)
factory_bot_rails (6.2.0) factory_bot_rails (6.2.0)
factory_bot (~> 6.2.0) factory_bot (~> 6.2.0)
railties (>= 5.0.0) railties (>= 5.0.0)
faker (2.19.0) faker (2.21.0)
i18n (>= 1.6, < 2) i18n (>= 1.8.11, < 2)
faraday (1.0.1) faraday (1.10.0)
multipart-post (>= 1.2, < 3) faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
faraday-httpclient (~> 1.0)
faraday-multipart (~> 1.0)
faraday-net_http (~> 1.0)
faraday-net_http_persistent (~> 1.0)
faraday-patron (~> 1.0)
faraday-rack (~> 1.0)
faraday-retry (~> 1.0)
ruby2_keywords (>= 0.0.4)
faraday-em_http (1.0.0)
faraday-em_synchrony (1.0.0)
faraday-excon (1.1.0)
faraday-httpclient (1.0.1)
faraday-multipart (1.0.4)
multipart-post (~> 2)
faraday-net_http (1.0.1)
faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0)
faraday-rack (1.0.0)
faraday-retry (1.0.3)
faraday_middleware (1.2.0) faraday_middleware (1.2.0)
faraday (~> 1.0) faraday (~> 1.0)
fcm (1.0.5) fcm (1.0.8)
faraday (~> 1) faraday (>= 1.0.0, < 3.0)
googleauth (~> 1)
ffi (1.15.5) ffi (1.15.5)
flag_shih_tzu (0.3.23) flag_shih_tzu (0.3.23)
foreman (0.87.2) foreman (0.87.2)
fugit (1.5.3) fugit (1.5.3)
et-orbi (~> 1, >= 1.2.7) et-orbi (~> 1, >= 1.2.7)
raabro (~> 1.4) raabro (~> 1.4)
gapic-common (0.3.4) gapic-common (0.10.0)
google-protobuf (~> 3.12, >= 3.12.2) faraday (>= 1.9, < 3.a)
googleapis-common-protos (>= 1.3.9, < 2.0) faraday-retry (>= 1.0, < 3.a)
googleapis-common-protos-types (>= 1.0.4, < 2.0) google-protobuf (~> 3.14)
googleauth (~> 0.9) googleapis-common-protos (>= 1.3.12, < 2.a)
grpc (~> 1.25) googleapis-common-protos-types (>= 1.3.1, < 2.a)
geocoder (1.7.3) googleauth (~> 1.0)
grpc (~> 1.36)
geocoder (1.8.0)
gli (2.21.0) gli (2.21.0)
globalid (1.0.0) globalid (1.0.0)
activesupport (>= 5.0) activesupport (>= 5.0)
google-apis-core (0.4.2) google-apis-core (0.7.0)
addressable (~> 2.5, >= 2.5.1) addressable (~> 2.5, >= 2.5.1)
googleauth (>= 0.16.2, < 2.a) googleauth (>= 0.16.2, < 2.a)
httpclient (>= 2.8.1, < 3.a) httpclient (>= 2.8.1, < 3.a)
@ -226,23 +252,27 @@ GEM
retriable (>= 2.0, < 4.a) retriable (>= 2.0, < 4.a)
rexml rexml
webrick webrick
google-apis-iamcredentials_v1 (0.10.0) google-apis-iamcredentials_v1 (0.13.0)
google-apis-core (>= 0.4, < 2.a) google-apis-core (>= 0.7, < 2.a)
google-apis-storage_v1 (0.11.0) google-apis-storage_v1 (0.18.0)
google-apis-core (>= 0.4, < 2.a) google-apis-core (>= 0.7, < 2.a)
google-cloud-core (1.6.0) google-cloud-core (1.6.0)
google-cloud-env (~> 1.0) google-cloud-env (~> 1.0)
google-cloud-errors (~> 1.0) google-cloud-errors (~> 1.0)
google-cloud-dialogflow (1.2.0) google-cloud-dialogflow (1.5.0)
google-cloud-core (~> 1.5) google-cloud-core (~> 1.6)
google-cloud-dialogflow-v2 (~> 0.1) google-cloud-dialogflow-v2 (>= 0.15, < 2.a)
google-cloud-dialogflow-v2 (0.6.4) google-cloud-dialogflow-v2 (0.17.0)
gapic-common (~> 0.3) gapic-common (>= 0.10, < 2.a)
google-cloud-errors (~> 1.0) google-cloud-errors (~> 1.0)
google-cloud-env (1.5.0) google-cloud-location (>= 0.0, < 2.a)
faraday (>= 0.17.3, < 2.0) google-cloud-env (1.6.0)
faraday (>= 0.17.3, < 3.0)
google-cloud-errors (1.2.0) google-cloud-errors (1.2.0)
google-cloud-storage (1.36.1) google-cloud-location (0.2.0)
gapic-common (>= 0.10, < 2.a)
google-cloud-errors (~> 1.0)
google-cloud-storage (1.37.0)
addressable (~> 2.8) addressable (~> 2.8)
digest-crc (~> 0.4) digest-crc (~> 0.4)
google-apis-iamcredentials_v1 (~> 0.1) google-apis-iamcredentials_v1 (~> 0.1)
@ -250,32 +280,32 @@ GEM
google-cloud-core (~> 1.6) google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a) googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0) mini_mime (~> 1.0)
google-protobuf (3.19.4) google-protobuf (3.21.2)
google-protobuf (3.19.4-x86_64-darwin) google-protobuf (3.21.2-x86_64-darwin)
google-protobuf (3.19.4-x86_64-linux) google-protobuf (3.21.2-x86_64-linux)
googleapis-common-protos (1.3.12) googleapis-common-protos (1.3.12)
google-protobuf (~> 3.14) google-protobuf (~> 3.14)
googleapis-common-protos-types (~> 1.2) googleapis-common-protos-types (~> 1.2)
grpc (~> 1.27) grpc (~> 1.27)
googleapis-common-protos-types (1.3.0) googleapis-common-protos-types (1.3.2)
google-protobuf (~> 3.14) google-protobuf (~> 3.14)
googleauth (0.17.1) googleauth (1.2.0)
faraday (>= 0.17.3, < 2.0) faraday (>= 0.17.3, < 3.a)
jwt (>= 1.4, < 3.0) jwt (>= 1.4, < 3.0)
memoist (~> 0.16) memoist (~> 0.16)
multi_json (~> 1.11) multi_json (~> 1.11)
os (>= 0.9, < 2.0) os (>= 0.9, < 2.0)
signet (~> 0.15) signet (>= 0.16, < 2.a)
groupdate (6.0.1) groupdate (6.1.0)
activesupport (>= 5.2) activesupport (>= 5.2)
grpc (1.43.1) grpc (1.47.0)
google-protobuf (~> 3.18) google-protobuf (~> 3.19)
googleapis-common-protos-types (~> 1.0) googleapis-common-protos-types (~> 1.0)
grpc (1.43.1-universal-darwin) grpc (1.47.0-x86_64-darwin)
google-protobuf (~> 3.18) google-protobuf (~> 3.19)
googleapis-common-protos-types (~> 1.0) googleapis-common-protos-types (~> 1.0)
grpc (1.43.1-x86_64-linux) grpc (1.47.0-x86_64-linux)
google-protobuf (~> 3.18) google-protobuf (~> 3.19)
googleapis-common-protos-types (~> 1.0) googleapis-common-protos-types (~> 1.0)
haikunator (1.1.1) haikunator (1.1.1)
hairtrigger (0.2.25) hairtrigger (0.2.25)
@ -289,13 +319,13 @@ GEM
html2text (0.2.1) html2text (0.2.1)
nokogiri (~> 1.6) nokogiri (~> 1.6)
http-accept (1.7.0) http-accept (1.7.0)
http-cookie (1.0.4) http-cookie (1.0.5)
domain_name (~> 0.5) domain_name (~> 0.5)
httparty (0.20.0) httparty (0.20.0)
mime-types (~> 3.0) mime-types (~> 3.0)
multi_xml (>= 0.5.2) multi_xml (>= 0.5.2)
httpclient (2.8.3) httpclient (2.8.3)
i18n (1.10.0) i18n (1.11.0)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
image_processing (1.12.2) image_processing (1.12.2)
mini_magick (>= 4.9.5, < 5) mini_magick (>= 4.9.5, < 5)
@ -304,19 +334,19 @@ GEM
actionview (>= 5.0.0) actionview (>= 5.0.0)
activesupport (>= 5.0.0) activesupport (>= 5.0.0)
jmespath (1.6.1) jmespath (1.6.1)
jquery-rails (4.4.0) jquery-rails (4.5.0)
rails-dom-testing (>= 1, < 3) rails-dom-testing (>= 1, < 3)
railties (>= 4.2.0) railties (>= 4.2.0)
thor (>= 0.14, < 2.0) thor (>= 0.14, < 2.0)
json (2.6.1) json (2.6.2)
json_refs (0.1.7) json_refs (0.1.7)
hana hana
json_schemer (0.2.19) json_schemer (0.2.21)
ecma-re-validator (~> 0.3) ecma-re-validator (~> 0.3)
hana (~> 1.3) hana (~> 1.3)
regexp_parser (~> 2.0) regexp_parser (~> 2.0)
uri_template (~> 0.7) uri_template (~> 0.7)
jwt (2.3.0) jwt (2.4.1)
kaminari (1.2.2) kaminari (1.2.2)
activesupport (>= 4.1.0) activesupport (>= 4.1.0)
kaminari-actionview (= 1.2.2) kaminari-actionview (= 1.2.2)
@ -329,21 +359,31 @@ GEM
activerecord activerecord
kaminari-core (= 1.2.2) kaminari-core (= 1.2.2)
kaminari-core (1.2.2) kaminari-core (1.2.2)
koala (3.1.0) koala (3.2.0)
addressable addressable
faraday (< 2) faraday (< 2)
json (>= 1.8) json (>= 1.8)
rexml rexml
launchy (2.5.0) launchy (2.5.0)
addressable (~> 2.7) addressable (~> 2.7)
letter_opener (1.7.0) letter_opener (1.8.1)
launchy (~> 2.2) launchy (>= 2.2, < 3)
line-bot-api (1.23.0) libddprof (0.6.0.1.0)
liquid (5.1.0) libddprof (0.6.0.1.0-x86_64-linux)
libddwaf (1.3.0.2.0)
ffi (~> 1.0)
libddwaf (1.3.0.2.0-arm64-darwin)
ffi (~> 1.0)
libddwaf (1.3.0.2.0-x86_64-darwin)
ffi (~> 1.0)
libddwaf (1.3.0.2.0-x86_64-linux)
ffi (~> 1.0)
line-bot-api (1.25.0)
liquid (5.3.0)
listen (3.7.1) listen (3.7.1)
rb-fsevent (~> 0.10, >= 0.10.3) rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10) rb-inotify (~> 0.9, >= 0.9.10)
loofah (2.17.0) loofah (2.18.0)
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.5.9) nokogiri (>= 1.5.9)
mail (2.7.1) mail (2.7.1)
@ -358,36 +398,36 @@ GEM
mini_magick (4.11.0) mini_magick (4.11.0)
mini_mime (1.1.2) mini_mime (1.1.2)
mini_portile2 (2.8.0) mini_portile2 (2.8.0)
minitest (5.15.0) minitest (5.16.2)
mock_redis (0.30.0) mock_redis (0.32.0)
ruby2_keywords ruby2_keywords
momentjs-rails (2.29.1.1) momentjs-rails (2.29.1.1)
railties (>= 3.1) railties (>= 3.1)
msgpack (1.4.5) msgpack (1.5.3)
multi_json (1.15.0) multi_json (1.15.0)
multi_xml (0.6.0) multi_xml (0.6.0)
multipart-post (2.1.1) multipart-post (2.2.3)
net-http-persistent (4.0.1) net-http-persistent (4.0.1)
connection_pool (~> 2.2) connection_pool (~> 2.2)
netrc (0.11.0) netrc (0.11.0)
newrelic_rpm (8.7.0) newrelic_rpm (8.9.0)
nio4r (2.5.8) nio4r (2.5.8)
nokogiri (1.13.6) nokogiri (1.13.7)
mini_portile2 (~> 2.8.0) mini_portile2 (~> 2.8.0)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.13.6-arm64-darwin) nokogiri (1.13.7-arm64-darwin)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.13.6-x86_64-darwin) nokogiri (1.13.7-x86_64-darwin)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.13.6-x86_64-linux) nokogiri (1.13.7-x86_64-linux)
racc (~> 1.4) racc (~> 1.4)
oauth (0.5.8) oauth (0.5.10)
orm_adapter (0.5.0) orm_adapter (0.5.0)
os (1.1.4) os (1.1.4)
parallel (1.21.0) parallel (1.22.1)
parser (3.1.1.0) parser (3.1.2.0)
ast (~> 2.4.1) ast (~> 2.4.1)
pg (1.3.2) pg (1.4.1)
pg_search (2.3.6) pg_search (2.3.6)
activerecord (>= 5.2) activerecord (>= 5.2)
activesupport (>= 5.2) activesupport (>= 5.2)
@ -398,46 +438,46 @@ GEM
method_source (~> 1.0) method_source (~> 1.0)
pry-rails (0.3.9) pry-rails (0.3.9)
pry (>= 0.10.4) pry (>= 0.10.4)
public_suffix (4.0.6) public_suffix (4.0.7)
puma (5.6.4) puma (5.6.4)
nio4r (~> 2.0) nio4r (~> 2.0)
pundit (2.2.0) pundit (2.2.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
raabro (1.4.0) raabro (1.4.0)
racc (1.6.0) racc (1.6.0)
rack (2.2.3.1) rack (2.2.4)
rack-attack (6.6.0) rack-attack (6.6.1)
rack (>= 1.0, < 3) rack (>= 1.0, < 3)
rack-cors (1.1.1) rack-cors (1.1.1)
rack (>= 2.0.0) rack (>= 2.0.0)
rack-proxy (0.7.2) rack-proxy (0.7.2)
rack rack
rack-test (1.1.0) rack-test (2.0.2)
rack (>= 1.0, < 3) rack (>= 1.3)
rack-timeout (0.6.0) rack-timeout (0.6.3)
rails (6.1.5.1) rails (6.1.6.1)
actioncable (= 6.1.5.1) actioncable (= 6.1.6.1)
actionmailbox (= 6.1.5.1) actionmailbox (= 6.1.6.1)
actionmailer (= 6.1.5.1) actionmailer (= 6.1.6.1)
actionpack (= 6.1.5.1) actionpack (= 6.1.6.1)
actiontext (= 6.1.5.1) actiontext (= 6.1.6.1)
actionview (= 6.1.5.1) actionview (= 6.1.6.1)
activejob (= 6.1.5.1) activejob (= 6.1.6.1)
activemodel (= 6.1.5.1) activemodel (= 6.1.6.1)
activerecord (= 6.1.5.1) activerecord (= 6.1.6.1)
activestorage (= 6.1.5.1) activestorage (= 6.1.6.1)
activesupport (= 6.1.5.1) activesupport (= 6.1.6.1)
bundler (>= 1.15.0) bundler (>= 1.15.0)
railties (= 6.1.5.1) railties (= 6.1.6.1)
sprockets-rails (>= 2.0.0) sprockets-rails (>= 2.0.0)
rails-dom-testing (2.0.3) rails-dom-testing (2.0.3)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
nokogiri (>= 1.6) nokogiri (>= 1.6)
rails-html-sanitizer (1.4.2) rails-html-sanitizer (1.4.3)
loofah (~> 2.3) loofah (~> 2.3)
railties (6.1.5.1) railties (6.1.6.1)
actionpack (= 6.1.5.1) actionpack (= 6.1.6.1)
activesupport (= 6.1.5.1) activesupport (= 6.1.6.1)
method_source method_source
rake (>= 12.2) rake (>= 12.2)
thor (~> 1.0) thor (~> 1.0)
@ -446,11 +486,11 @@ GEM
rb-fsevent (0.11.1) rb-fsevent (0.11.1)
rb-inotify (0.10.1) rb-inotify (0.10.1)
ffi (~> 1.0) ffi (~> 1.0)
redis (4.6.0) redis (4.7.1)
redis-namespace (1.8.1) redis-namespace (1.8.2)
redis (>= 3.0.4) redis (>= 3.0.4)
regexp_parser (2.2.1) regexp_parser (2.5.0)
representable (3.1.1) representable (3.2.0)
declarative (< 0.1.0) declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0) trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0) uber (< 0.2.0)
@ -469,7 +509,7 @@ GEM
rspec-expectations (3.11.0) rspec-expectations (3.11.0)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.11.0) rspec-support (~> 3.11.0)
rspec-mocks (3.11.0) rspec-mocks (3.11.1)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.11.0) rspec-support (~> 3.11.0)
rspec-rails (5.0.3) rspec-rails (5.0.3)
@ -481,26 +521,27 @@ GEM
rspec-mocks (~> 3.10) rspec-mocks (~> 3.10)
rspec-support (~> 3.10) rspec-support (~> 3.10)
rspec-support (3.11.0) rspec-support (3.11.0)
rubocop (1.25.1) rubocop (1.31.2)
json (~> 2.3)
parallel (~> 1.10) parallel (~> 1.10)
parser (>= 3.1.0.0) parser (>= 3.1.0.0)
rainbow (>= 2.2.2, < 4.0) rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0) regexp_parser (>= 1.8, < 3.0)
rexml rexml (>= 3.2.5, < 4.0)
rubocop-ast (>= 1.15.1, < 2.0) rubocop-ast (>= 1.18.0, < 2.0)
ruby-progressbar (~> 1.7) ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 3.0) unicode-display_width (>= 1.4.0, < 3.0)
rubocop-ast (1.16.0) rubocop-ast (1.19.1)
parser (>= 3.1.1.0) parser (>= 3.1.1.0)
rubocop-performance (1.13.2) rubocop-performance (1.14.2)
rubocop (>= 1.7.0, < 2.0) rubocop (>= 1.7.0, < 2.0)
rubocop-ast (>= 0.4.0) rubocop-ast (>= 0.4.0)
rubocop-rails (2.13.2) rubocop-rails (2.15.2)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
rack (>= 1.1) rack (>= 1.1)
rubocop (>= 1.7.0, < 2.0) rubocop (>= 1.7.0, < 2.0)
rubocop-rspec (2.8.0) rubocop-rspec (2.12.1)
rubocop (~> 1.19) rubocop (~> 1.31)
ruby-progressbar (1.11.0) ruby-progressbar (1.11.0)
ruby-vips (2.1.4) ruby-vips (2.1.4)
ffi (~> 1.12) ffi (~> 1.12)
@ -508,7 +549,7 @@ GEM
ruby2ruby (2.4.4) ruby2ruby (2.4.4)
ruby_parser (~> 3.1) ruby_parser (~> 3.1)
sexp_processor (~> 4.6) sexp_processor (~> 4.6)
ruby_parser (3.18.1) ruby_parser (3.19.1)
sexp_processor (~> 4.16) sexp_processor (~> 4.16)
sassc (2.4.0) sassc (2.4.0)
ffi (~> 1.9) ffi (~> 1.9)
@ -518,37 +559,37 @@ GEM
sprockets (> 3.0) sprockets (> 3.0)
sprockets-rails sprockets-rails
tilt tilt
scout_apm (5.1.1) scout_apm (5.2.0)
parser parser
seed_dump (3.3.1) seed_dump (3.3.1)
activerecord (>= 4) activerecord (>= 4)
activesupport (>= 4) activesupport (>= 4)
selectize-rails (0.12.6) selectize-rails (0.12.6)
semantic_range (3.0.0) semantic_range (3.0.0)
sentry-rails (5.3.0) sentry-rails (5.3.1)
railties (>= 5.0) railties (>= 5.0)
sentry-ruby-core (~> 5.3.0) sentry-ruby-core (~> 5.3.1)
sentry-ruby (5.3.0) sentry-ruby (5.3.1)
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
sentry-ruby-core (= 5.3.0) sentry-ruby-core (= 5.3.1)
sentry-ruby-core (5.3.0) sentry-ruby-core (5.3.1)
concurrent-ruby concurrent-ruby
sentry-sidekiq (5.3.0) sentry-sidekiq (5.3.1)
sentry-ruby-core (~> 5.3.0) sentry-ruby-core (~> 5.3.1)
sidekiq (>= 3.0) sidekiq (>= 3.0)
sexp_processor (4.16.0) sexp_processor (4.16.1)
shoulda-matchers (5.1.0) shoulda-matchers (5.1.0)
activesupport (>= 5.2.0) activesupport (>= 5.2.0)
sidekiq (6.4.1) sidekiq (6.4.2)
connection_pool (>= 2.2.2) connection_pool (>= 2.2.2)
rack (~> 2.0) rack (~> 2.0)
redis (>= 4.2.0) redis (>= 4.2.0)
sidekiq-cron (1.4.0) sidekiq-cron (1.6.0)
fugit (~> 1) fugit (~> 1)
sidekiq (>= 4.2.1) sidekiq (>= 4.2.1)
signet (0.16.0) signet (0.17.0)
addressable (~> 2.8) addressable (~> 2.8)
faraday (>= 0.17.3, < 2.0) faraday (>= 0.17.5, < 3.a)
jwt (>= 1.5, < 3.0) jwt (>= 1.5, < 3.0)
multi_json (~> 1.10) multi_json (~> 1.10)
simplecov (0.17.1) simplecov (0.17.1)
@ -566,7 +607,7 @@ GEM
spring-watcher-listen (2.0.1) spring-watcher-listen (2.0.1)
listen (>= 2.7, < 4.0) listen (>= 2.7, < 4.0)
spring (>= 1.2, < 3.0) spring (>= 1.2, < 3.0)
sprockets (4.0.3) sprockets (4.1.1)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
rack (> 1, < 3) rack (> 1, < 3)
sprockets-rails (3.4.2) sprockets-rails (3.4.2)
@ -575,31 +616,31 @@ GEM
sprockets (>= 3.0.0) sprockets (>= 3.0.0)
squasher (0.6.2) squasher (0.6.2)
statsd-ruby (1.5.0) statsd-ruby (1.5.0)
telephone_number (1.4.13) telephone_number (1.4.16)
thor (1.2.1) thor (1.2.1)
tilt (2.0.10) tilt (2.0.10)
time_diff (0.3.0) time_diff (0.3.0)
activesupport activesupport
i18n i18n
trailblazer-option (0.1.2) trailblazer-option (0.1.2)
twilio-ruby (5.32.0) twilio-ruby (5.68.0)
faraday (~> 1.0.0) faraday (>= 0.9, < 3.0)
jwt (>= 1.5, <= 2.5) jwt (>= 1.5, <= 2.5)
nokogiri (>= 1.6, < 2.0) nokogiri (>= 1.6, < 2.0)
twitty (0.1.4) twitty (0.1.4)
oauth oauth
tzinfo (2.0.4) tzinfo (2.0.4)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
tzinfo-data (1.2021.5) tzinfo-data (1.2022.1)
tzinfo (>= 1.0.0) tzinfo (>= 1.0.0)
uber (0.1.0) uber (0.1.0)
uglifier (4.2.0) uglifier (4.2.0)
execjs (>= 0.3.0, < 3) execjs (>= 0.3.0, < 3)
unf (0.1.4) unf (0.1.4)
unf_ext unf_ext
unf_ext (0.0.8) unf_ext (0.0.8.2)
unicode-display_width (2.1.0) unicode-display_width (2.2.0)
uniform_notifier (1.14.2) uniform_notifier (1.16.0)
uri_template (0.7.0) uri_template (0.7.0)
valid_email2 (4.0.3) valid_email2 (4.0.3)
activemodel (>= 3.2) activemodel (>= 3.2)
@ -631,7 +672,7 @@ GEM
working_hours (1.4.1) working_hours (1.4.1)
activesupport (>= 3.2) activesupport (>= 3.2)
tzinfo tzinfo
zeitwerk (2.5.4) zeitwerk (2.6.0)
PLATFORMS PLATFORMS
arm64-darwin-20 arm64-darwin-20
@ -705,7 +746,7 @@ DEPENDENCIES
rack-attack rack-attack
rack-cors rack-cors
rack-timeout rack-timeout
rails rails (~> 6.1)
redis redis
redis-namespace redis-namespace
responders responders
@ -730,7 +771,7 @@ DEPENDENCIES
squasher squasher
telephone_number telephone_number
time_diff time_diff
twilio-ruby (~> 5.32.0) twilio-ruby (~> 5.66)
twitty twitty
tzinfo-data tzinfo-data
uglifier uglifier
@ -746,4 +787,4 @@ RUBY VERSION
ruby 3.0.4p208 ruby 3.0.4p208
BUNDLED WITH BUNDLED WITH
2.3.15 2.3.16

View File

@ -26,6 +26,7 @@ ___
<a href="https://huntr.dev/bounties/disclose"><img src="https://cdn.huntr.dev/huntr_security_badge_mono.svg" alt="Huntr"></a> <a href="https://huntr.dev/bounties/disclose"><img src="https://cdn.huntr.dev/huntr_security_badge_mono.svg" alt="Huntr"></a>
<a href="https://status.chatwoot.com"><img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fchatwoot%2Fstatus%2Fmaster%2Fapi%2Fchatwoot%2Fuptime.json" alt="uptime"></a> <a href="https://status.chatwoot.com"><img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fchatwoot%2Fstatus%2Fmaster%2Fapi%2Fchatwoot%2Fuptime.json" alt="uptime"></a>
<a href="https://status.chatwoot.com"><img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fchatwoot%2Fstatus%2Fmaster%2Fapi%2Fchatwoot%2Fresponse-time.json" alt="response time"></a> <a href="https://status.chatwoot.com"><img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fchatwoot%2Fstatus%2Fmaster%2Fapi%2Fchatwoot%2Fresponse-time.json" alt="response time"></a>
<a href="https://artifacthub.io/packages/helm/chatwoot/chatwoot"><img src="https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/artifact-hub" alt="Artifact HUB"></a>
</p> </p>
<img src="https://chatwoot-public-assets.s3.amazonaws.com/github/screenshot.png" width="100%" alt="Chat dashboard"/> <img src="https://chatwoot-public-assets.s3.amazonaws.com/github/screenshot.png" width="100%" alt="Chat dashboard"/>

1
VERSION_CW Normal file
View File

@ -0,0 +1 @@
2.6.0

1
VERSION_CWCTL Normal file
View File

@ -0,0 +1 @@
2.1.0

View File

@ -1,7 +1,16 @@
# retain_original_contact_name: false / true
# In case of setUser we want to update the name of the identified contact,
# which is the default behaviour
#
# But, In case of contact merge during prechat form contact update.
# We don't want to update the name of the identified original contact.
class ContactIdentifyAction class ContactIdentifyAction
pattr_initialize [:contact!, :params!] pattr_initialize [:contact!, :params!, { retain_original_contact_name: false, discard_invalid_attrs: false }]
def perform def perform
@attributes_to_update = [:identifier, :name, :email, :phone_number]
ActiveRecord::Base.transaction do ActiveRecord::Base.transaction do
merge_if_existing_identified_contact merge_if_existing_identified_contact
merge_if_existing_email_contact merge_if_existing_email_contact
@ -18,49 +27,89 @@ class ContactIdentifyAction
end end
def merge_if_existing_identified_contact def merge_if_existing_identified_contact
@contact = merge_contact(existing_identified_contact, @contact) if merge_contacts?(existing_identified_contact, @contact) return unless merge_contacts?(existing_identified_contact, :identifier)
process_contact_merge(existing_identified_contact)
end end
def merge_if_existing_email_contact def merge_if_existing_email_contact
@contact = merge_contact(existing_email_contact, @contact) if merge_contacts?(existing_email_contact, @contact) return unless merge_contacts?(existing_email_contact, :email)
process_contact_merge(existing_email_contact)
end end
def merge_if_existing_phone_number_contact def merge_if_existing_phone_number_contact
@contact = merge_contact(existing_phone_number_contact, @contact) if merge_contacts?(existing_phone_number_contact, @contact) return unless merge_contacts?(existing_phone_number_contact, :phone_number)
return unless mergable_phone_contact?
process_contact_merge(existing_phone_number_contact)
end
def process_contact_merge(mergee_contact)
@contact = merge_contact(mergee_contact, @contact)
@attributes_to_update.delete(:name) if retain_original_contact_name
end end
def existing_identified_contact def existing_identified_contact
return if params[:identifier].blank? return if params[:identifier].blank?
@existing_identified_contact ||= Contact.where(account_id: account.id).find_by(identifier: params[:identifier]) @existing_identified_contact ||= account.contacts.find_by(identifier: params[:identifier])
end end
def existing_email_contact def existing_email_contact
return if params[:email].blank? return if params[:email].blank?
@existing_email_contact ||= Contact.where(account_id: account.id).find_by(email: params[:email]) @existing_email_contact ||= account.contacts.find_by(email: params[:email])
end end
def existing_phone_number_contact def existing_phone_number_contact
return if params[:phone_number].blank? return if params[:phone_number].blank?
@existing_phone_number_contact ||= Contact.where(account_id: account.id).find_by(phone_number: params[:phone_number]) @existing_phone_number_contact ||= account.contacts.find_by(phone_number: params[:phone_number])
end end
def merge_contacts?(existing_contact, _contact) def merge_contacts?(existing_contact, key)
existing_contact && existing_contact.id != @contact.id return if existing_contact.blank?
return true if params[:identifier].blank?
# we want to prevent merging contacts with different identifiers
if existing_contact.identifier.present? && existing_contact.identifier != params[:identifier]
# we will remove attribute from update list
@attributes_to_update.delete(key)
return false
end
true
end
# case: contact 1: email: 1@test.com, phone: 123456789
# params: email: 2@test.com, phone: 123456789
# we don't want to overwrite 1@test.com since email parameter takes higer priority
def mergable_phone_contact?
return true if params[:email].blank?
if existing_phone_number_contact.email.present? && existing_phone_number_contact.email != params[:email]
@attributes_to_update.delete(:phone_number)
return false
end
true
end end
def update_contact def update_contact
@contact.attributes = params.slice(*@attributes_to_update).reject do |_k, v|
v.blank?
end.merge({ custom_attributes: custom_attributes, additional_attributes: additional_attributes })
# blank identifier or email will throw unique index error # blank identifier or email will throw unique index error
# TODO: replace reject { |_k, v| v.blank? } with compact_blank when rails is upgraded # TODO: replace reject { |_k, v| v.blank? } with compact_blank when rails is upgraded
@contact.update!(params.slice(:name, :email, :identifier, :phone_number).reject do |_k, v| @contact.discard_invalid_attrs if discard_invalid_attrs
v.blank? @contact.save!
end.merge({ custom_attributes: custom_attributes, additional_attributes: additional_attributes }))
ContactAvatarJob.perform_later(@contact, params[:avatar_url]) if params[:avatar_url].present? ContactAvatarJob.perform_later(@contact, params[:avatar_url]) if params[:avatar_url].present?
end end
def merge_contact(base_contact, merge_contact) def merge_contact(base_contact, merge_contact)
return base_contact if base_contact.id == merge_contact.id
ContactMergeAction.new( ContactMergeAction.new(
account: account, account: account,
base_contact: base_contact, base_contact: base_contact,
@ -69,14 +118,14 @@ class ContactIdentifyAction
end end
def custom_attributes def custom_attributes
params[:custom_attributes] ? @contact.custom_attributes.deep_merge(params[:custom_attributes].stringify_keys) : @contact.custom_attributes return @contact.custom_attributes if params[:custom_attributes].blank?
(@contact.custom_attributes || {}).deep_merge(params[:custom_attributes].stringify_keys)
end end
def additional_attributes def additional_attributes
if params[:additional_attributes] return @contact.additional_attributes if params[:additional_attributes].blank?
@contact.additional_attributes.deep_merge(params[:additional_attributes].stringify_keys)
else (@contact.additional_attributes || {}).deep_merge(params[:additional_attributes].stringify_keys)
@contact.additional_attributes
end
end end
end end

View File

@ -9,12 +9,15 @@ class Campaigns::CampaignConversationBuilder
@contact_inbox.lock! @contact_inbox.lock!
# We won't send campaigns if a conversation is already present # We won't send campaigns if a conversation is already present
return if @contact_inbox.reload.conversations.present? raise 'Conversation alread present' if @contact_inbox.reload.conversations.present?
@conversation = ::Conversation.create!(conversation_params) @conversation = ::Conversation.create!(conversation_params)
Messages::MessageBuilder.new(@campaign.sender, @conversation, message_params).perform Messages::MessageBuilder.new(@campaign.sender, @conversation, message_params).perform
end end
@conversation @conversation
rescue StandardError => e
Rails.logger.info(e.message)
nil
end end
private private

View File

@ -54,6 +54,9 @@ class Messages::Messenger::MessageBuilder
def fetch_story_link(attachment) def fetch_story_link(attachment)
message = attachment.message message = attachment.message
result = get_story_object_from_source_id(message.source_id) result = get_story_object_from_source_id(message.source_id)
return if result.blank?
story_id = result['story']['mention']['id'] story_id = result['story']['mention']['id']
story_sender = result['from']['username'] story_sender = result['from']['username']
message.content_attributes[:story_sender] = story_sender message.content_attributes[:story_sender] = story_sender
@ -68,6 +71,11 @@ class Messages::Messenger::MessageBuilder
rescue Koala::Facebook::AuthenticationError rescue Koala::Facebook::AuthenticationError
@inbox.channel.authorization_error! @inbox.channel.authorization_error!
raise raise
rescue Koala::Facebook::ClientError => e
# The exception occurs when we are trying fetch the deleted story or blocked story.
@message.update(content: I18n.t('conversations.messages.instagram_deleted_story_content'))
Rails.logger.error e
{}
rescue StandardError => e rescue StandardError => e
ChatwootExceptionTracker.new(e, account: @inbox.account).capture_exception ChatwootExceptionTracker.new(e, account: @inbox.account).capture_exception
{} {}

View File

@ -30,6 +30,6 @@ class Api::V1::Accounts::AgentBotsController < Api::V1::Accounts::BaseController
end end
def permitted_params def permitted_params
params.permit(:name, :description, :outgoing_url) params.permit(:name, :description, :outgoing_url, :bot_type, bot_config: [:csml_content])
end end
end end

View File

@ -1,14 +1,18 @@
class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
before_action :portal before_action :portal
before_action :check_authorization
before_action :fetch_article, except: [:index, :create] before_action :fetch_article, except: [:index, :create]
def index def index
@articles = @portal.articles @articles = @portal.articles
@articles.search(list_params) if params[:payload].present? @articles = @articles.search(list_params) if params[:payload].present?
end end
def create def create
@article = @portal.articles.create!(article_params) @article = @portal.articles.create!(article_params)
@article.associate_root_article(article_params[:associated_article_id])
@article.draft!
render json: { error: @article.errors.messages }, status: :unprocessable_entity and return unless @article.valid?
end end
def edit; end def edit; end
@ -36,7 +40,7 @@ class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
def article_params def article_params
params.require(:article).permit( params.require(:article).permit(
:title, :content, :description, :position, :category_id, :author_id :title, :content, :description, :position, :category_id, :author_id, :associated_article_id, :status
) )
end end

View File

@ -1,5 +1,6 @@
class Api::V1::Accounts::CategoriesController < Api::V1::Accounts::BaseController class Api::V1::Accounts::CategoriesController < Api::V1::Accounts::BaseController
before_action :portal before_action :portal
before_action :check_authorization
before_action :fetch_category, except: [:index, :create] before_action :fetch_category, except: [:index, :create]
def index def index
@ -8,10 +9,20 @@ class Api::V1::Accounts::CategoriesController < Api::V1::Accounts::BaseControlle
def create def create
@category = @portal.categories.create!(category_params) @category = @portal.categories.create!(category_params)
@category.related_categories << related_categories_records
render json: { error: @category.errors.messages }, status: :unprocessable_entity and return unless @category.valid?
@category.save!
end end
def show; end
def update def update
@category.update!(category_params) @category.update!(category_params)
@category.related_categories = related_categories_records if related_categories_records.any?
render json: { error: @category.errors.messages }, status: :unprocessable_entity and return unless @category.valid?
@category.save!
end end
def destroy def destroy
@ -29,9 +40,13 @@ class Api::V1::Accounts::CategoriesController < Api::V1::Accounts::BaseControlle
@portal ||= Current.account.portals.find_by(slug: params[:portal_id]) @portal ||= Current.account.portals.find_by(slug: params[:portal_id])
end end
def related_categories_records
@portal.categories.where(id: params[:category][:related_category_ids])
end
def category_params def category_params
params.require(:category).permit( params.require(:category).permit(
:name, :description, :position, :slug, :locale :name, :description, :position, :slug, :locale, :parent_category_id, :associated_category_id
) )
end end
end end

View File

@ -27,6 +27,8 @@ class Api::V1::Accounts::Channels::TwilioChannelsController < Api::V1::Accounts:
end end
def phone_number def phone_number
return if permitted_params[:phone_number].blank?
medium == 'sms' ? permitted_params[:phone_number] : "whatsapp:#{permitted_params[:phone_number]}" medium == 'sms' ? permitted_params[:phone_number] : "whatsapp:#{permitted_params[:phone_number]}"
end end
@ -38,6 +40,7 @@ class Api::V1::Accounts::Channels::TwilioChannelsController < Api::V1::Accounts:
@twilio_channel = Current.account.twilio_sms.create!( @twilio_channel = Current.account.twilio_sms.create!(
account_sid: permitted_params[:account_sid], account_sid: permitted_params[:account_sid],
auth_token: permitted_params[:auth_token], auth_token: permitted_params[:auth_token],
messaging_service_sid: permitted_params[:messaging_service_sid].presence,
phone_number: phone_number, phone_number: phone_number,
medium: medium medium: medium
) )
@ -49,7 +52,7 @@ class Api::V1::Accounts::Channels::TwilioChannelsController < Api::V1::Accounts:
def permitted_params def permitted_params
params.require(:twilio_channel).permit( params.require(:twilio_channel).permit(
:account_id, :phone_number, :account_sid, :auth_token, :name, :medium :account_id, :messaging_service_sid, :phone_number, :account_sid, :auth_token, :name, :medium
) )
end end
end end

View File

@ -12,7 +12,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
before_action :check_authorization before_action :check_authorization
before_action :set_current_page, only: [:index, :active, :search, :filter] before_action :set_current_page, only: [:index, :active, :search, :filter]
before_action :fetch_contact, only: [:show, :update, :destroy, :contactable_inboxes, :destroy_custom_attributes] before_action :fetch_contact, only: [:show, :update, :destroy, :avatar, :contactable_inboxes, :destroy_custom_attributes]
before_action :set_include_contact_inboxes, only: [:index, :search, :filter] before_action :set_include_contact_inboxes, only: [:index, :search, :filter]
def index def index
@ -72,15 +72,17 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
def create def create
ActiveRecord::Base.transaction do ActiveRecord::Base.transaction do
@contact = Current.account.contacts.new(contact_params) @contact = Current.account.contacts.new(permitted_params.except(:avatar_url))
@contact.save! @contact.save!
@contact_inbox = build_contact_inbox @contact_inbox = build_contact_inbox
process_avatar
end end
end end
def update def update
@contact.assign_attributes(contact_update_params) @contact.assign_attributes(contact_update_params)
@contact.save! @contact.save!
process_avatar if permitted_params[:avatar].present? || permitted_params[:avatar_url].present?
end end
def destroy def destroy
@ -95,6 +97,11 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
head :ok head :ok
end end
def avatar
@contact.avatar.purge if @contact.avatar.attached?
@contact
end
private private
# TODO: Move this to a finder class # TODO: Move this to a finder class
@ -131,19 +138,19 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
ContactInbox.create(contact: @contact, inbox: inbox, source_id: source_id) ContactInbox.create(contact: @contact, inbox: inbox, source_id: source_id)
end end
def contact_params def permitted_params
params.require(:contact).permit(:name, :identifier, :email, :phone_number, additional_attributes: {}, custom_attributes: {}) params.permit(:name, :identifier, :email, :phone_number, :avatar, :avatar_url, additional_attributes: {}, custom_attributes: {})
end end
def contact_custom_attributes def contact_custom_attributes
return @contact.custom_attributes.merge(contact_params[:custom_attributes]) if contact_params[:custom_attributes] return @contact.custom_attributes.merge(permitted_params[:custom_attributes]) if permitted_params[:custom_attributes]
@contact.custom_attributes @contact.custom_attributes
end end
def contact_update_params def contact_update_params
# we want the merged custom attributes not the original one # we want the merged custom attributes not the original one
contact_params.except(:custom_attributes).merge({ custom_attributes: contact_custom_attributes }) permitted_params.except(:custom_attributes, :avatar_url).merge({ custom_attributes: contact_custom_attributes })
end end
def set_include_contact_inboxes def set_include_contact_inboxes
@ -158,6 +165,16 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
@contact = Current.account.contacts.includes(contact_inboxes: [:inbox]).find(params[:id]) @contact = Current.account.contacts.includes(contact_inboxes: [:inbox]).find(params[:id])
end end
def process_avatar
if permitted_params[:avatar].blank? && permitted_params[:avatar_url].present?
::ContactAvatarJob.perform_later(@contact, params[:avatar_url])
elsif permitted_params[:avatar].blank? && permitted_params[:email].present?
hash = Digest::MD5.hexdigest(params[:email])
gravatar_url = "https://www.gravatar.com/avatar/#{hash}?d=404"
::ContactAvatarJob.perform_later(@contact, gravatar_url)
end
end
def render_error(error, error_status) def render_error(error, error_status)
render json: error, status: error_status render json: error, status: error_status
end end

View File

@ -1,18 +1,36 @@
class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
include ::FileTypeHelper
before_action :fetch_portal, except: [:index, :create] before_action :fetch_portal, except: [:index, :create]
before_action :check_authorization
def index def index
@portals = Current.account.portals @portals = Current.account.portals
end end
def add_members
agents = Current.account.agents.where(id: portal_member_params[:member_ids])
@portal.members << agents
end
def show; end def show; end
def create def create
@portal = Current.account.portals.create!(portal_params) @portal = Current.account.portals.build(portal_params)
render json: { error: @portal.errors.messages }, status: :unprocessable_entity and return unless @portal.valid?
@portal.save!
process_attached_logo
end end
def update def update
@portal.update!(portal_params) ActiveRecord::Base.transaction do
@portal.update!(portal_params) if params[:portal].present?
process_attached_logo
rescue StandardError => e
Rails.logger.error e
render json: { error: @portal.errors.messages }.to_json, status: :unprocessable_entity
end
end end
def destroy def destroy
@ -20,6 +38,15 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
head :ok head :ok
end end
def archive
@portal.update(archive: true)
head :ok
end
def process_attached_logo
@portal.logo.attach(params[:logo])
end
private private
def fetch_portal def fetch_portal
@ -32,7 +59,11 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
def portal_params def portal_params
params.require(:portal).permit( params.require(:portal).permit(
:account_id, :color, :custom_domain, :header_text, :homepage_link, :name, :page_title, :slug, :archived :account_id, :color, :custom_domain, :header_text, :homepage_link, :name, :page_title, :slug, :archived, config: { allowed_locales: [] }
) )
end end
def portal_member_params
params.require(:portal).permit(:account_id, member_ids: [])
end
end end

View File

@ -44,38 +44,6 @@ class Api::V1::Widget::BaseController < ApplicationController
} }
end end
def update_contact(email)
contact_with_email = @current_account.contacts.find_by(email: email)
if contact_with_email
@contact = ::ContactMergeAction.new(
account: @current_account,
base_contact: contact_with_email,
mergee_contact: @contact
).perform
else
@contact.update!(email: email)
update_contact_name
end
end
def update_contact_phone_number(phone_number)
contact_with_phone_number = @current_account.contacts.find_by(phone_number: phone_number)
if contact_with_phone_number
@contact = ::ContactMergeAction.new(
account: @current_account,
base_contact: contact_with_phone_number,
mergee_contact: @contact
).perform
else
@contact.update!(phone_number: phone_number)
update_contact_name
end
end
def update_contact_name
@contact.update!(name: contact_name) if contact_name.present?
end
def contact_email def contact_email
permitted_params.dig(:contact, :email)&.downcase permitted_params.dig(:contact, :email)&.downcase
end end

View File

@ -1,14 +1,27 @@
class Api::V1::Widget::ContactsController < Api::V1::Widget::BaseController class Api::V1::Widget::ContactsController < Api::V1::Widget::BaseController
before_action :process_hmac, only: [:update] include WidgetHelper
before_action :validate_hmac, only: [:set_user]
def show; end def show; end
def update def update
contact_identify_action = ContactIdentifyAction.new( identify_contact(@contact)
contact: @contact, end
params: permitted_params.to_h.deep_symbolize_keys
) def set_user
@contact = contact_identify_action.perform contact = nil
if a_different_contact?
@contact_inbox, @widget_auth_token = build_contact_inbox_with_token(@web_widget)
contact = @contact_inbox.contact
else
contact = @contact
end
@contact_inbox.update(hmac_verified: true) if should_verify_hmac? && valid_hmac?
identify_contact(contact)
end end
# TODO : clean up this with proper routes delete contacts/custom_attributes # TODO : clean up this with proper routes delete contacts/custom_attributes
@ -20,12 +33,23 @@ class Api::V1::Widget::ContactsController < Api::V1::Widget::BaseController
private private
def process_hmac def identify_contact(contact)
contact_identify_action = ContactIdentifyAction.new(
contact: contact,
params: permitted_params.to_h.deep_symbolize_keys,
discard_invalid_attrs: true
)
@contact = contact_identify_action.perform
end
def a_different_contact?
@contact.identifier.present? && @contact.identifier != permitted_params[:identifier]
end
def validate_hmac
return unless should_verify_hmac? return unless should_verify_hmac?
render json: { error: 'HMAC failed: Invalid Identifier Hash Provided' }, status: :unauthorized unless valid_hmac? render json: { error: 'HMAC failed: Invalid Identifier Hash Provided' }, status: :unauthorized unless valid_hmac?
@contact_inbox.update(hmac_verified: true)
end end
def should_verify_hmac? def should_verify_hmac?

View File

@ -14,8 +14,11 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
end end
def process_update_contact def process_update_contact
update_contact(contact_email) if @contact.email.blank? && contact_email.present? @contact = ContactIdentifyAction.new(
update_contact_phone_number(contact_phone_number) if @contact.phone_number.blank? && contact_phone_number.present? contact: @contact,
params: { email: contact_email, phone_number: contact_phone_number, name: contact_name },
retain_original_contact_name: true
).perform
end end
def update_last_seen def update_last_seen

View File

@ -15,7 +15,10 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
def update def update
if @message.content_type == 'input_email' if @message.content_type == 'input_email'
@message.update!(submitted_email: contact_email) @message.update!(submitted_email: contact_email)
update_contact(contact_email) ContactIdentifyAction.new(
contact: @contact,
params: { email: contact_email }
).perform
else else
@message.update!(message_update_params[:message]) @message.update!(message_update_params[:message])
end end

View File

@ -0,0 +1,20 @@
# services from Meta (Prev: Facebook) needs a token verification step for webhook subscriptions,
# This concern handles the token verification step.
module MetaTokenVerifyConcern
def verify
service = is_a?(Webhooks::WhatsappController) ? 'whatsapp' : 'instagram'
if valid_token?(params['hub.verify_token'])
Rails.logger.info("#{service.capitalize} webhook verified")
render json: params['hub.challenge']
else
render status: :unauthorized, json: { error: 'Error; wrong verify token' }
end
end
private
def valid_token?(_token)
raise 'Overwrite this method your controller'
end
end

View File

@ -14,7 +14,7 @@ class Platform::Api::V1::UsersController < PlatformController
def login def login
encoded_email = ERB::Util.url_encode(@resource.email) encoded_email = ERB::Util.url_encode(@resource.email)
render json: { url: "#{ENV['FRONTEND_URL']}/app/login?email=#{encoded_email}&sso_auth_token=#{@resource.generate_sso_auth_token}" } render json: { url: "#{ENV.fetch('FRONTEND_URL', nil)}/app/login?email=#{encoded_email}&sso_auth_token=#{@resource.generate_sso_auth_token}" }
end end
def show; end def show; end

View File

@ -0,0 +1,25 @@
class Public::Api::V1::Portals::ArticlesController < ApplicationController
before_action :set_portal
before_action :set_article, only: [:show]
def index
@articles = @portal.articles
@articles = @articles.search(list_params) if params[:payload].present?
end
def show; end
private
def set_article
@article = @portal.articles.find(params[:id])
end
def set_portal
@portal = ::Portal.find_by!(slug: params[:portal_slug], archived: false)
end
def list_params
params.require(:payload).permit(:query)
end
end

View File

@ -0,0 +1,20 @@
class Public::Api::V1::Portals::CategoriesController < PublicController
before_action :set_portal
before_action :set_category, only: [:show]
def index
@categories = @portal.categories
end
def show; end
private
def set_category
@category = @portal.categories.find_by!(slug: params[:slug])
end
def set_portal
@portal = ::Portal.find_by!(slug: params[:portal_slug], archived: false)
end
end

View File

@ -0,0 +1,11 @@
class Public::Api::V1::PortalsController < PublicController
before_action :set_portal
def show; end
private
def set_portal
@portal = ::Portal.find_by!(slug: params[:slug], archived: false)
end
end

View File

@ -7,7 +7,7 @@ class Twilio::CallbackController < ApplicationController
private private
def permitted_params def permitted_params # rubocop:disable Metrics/MethodLength
params.permit( params.permit(
:ApiVersion, :ApiVersion,
:SmsSid, :SmsSid,
@ -25,7 +25,8 @@ class Twilio::CallbackController < ApplicationController
:ToCountry, :ToCountry,
:FromState, :FromState,
:MediaUrl0, :MediaUrl0,
:MediaContentType0 :MediaContentType0,
:MessagingServiceSid
) )
end end
end end

View File

@ -1,15 +1,5 @@
class Webhooks::InstagramController < ApplicationController class Webhooks::InstagramController < ActionController::API
skip_before_action :authenticate_user!, raise: false include MetaTokenVerifyConcern
skip_before_action :set_current_user
def verify
if valid_instagram_token?(params['hub.verify_token'])
Rails.logger.info('Instagram webhook verified')
render json: params['hub.challenge']
else
render json: { error: 'Error; wrong verify token', status: 403 }
end
end
def events def events
Rails.logger.info('Instagram webhook received events') Rails.logger.info('Instagram webhook received events')
@ -24,7 +14,7 @@ class Webhooks::InstagramController < ApplicationController
private private
def valid_instagram_token?(token) def valid_token?(token)
token == GlobalConfigService.load('IG_VERIFY_TOKEN', '') token == GlobalConfigService.load('IG_VERIFY_TOKEN', '')
end end
end end

View File

@ -1,6 +1,16 @@
class Webhooks::WhatsappController < ActionController::API class Webhooks::WhatsappController < ActionController::API
include MetaTokenVerifyConcern
def process_payload def process_payload
Webhooks::WhatsappEventsJob.perform_later(params.to_unsafe_hash) Webhooks::WhatsappEventsJob.perform_later(params.to_unsafe_hash)
head :ok head :ok
end end
private
def valid_token?(token)
channel = Channel::Whatsapp.find_by(phone_number: params[:phone_number])
whatsapp_webhook_verify_token = channel.provider_config['webhook_verify_token'] if channel.present?
token == whatsapp_webhook_verify_token if whatsapp_webhook_verify_token.present?
end
end end

View File

@ -1,5 +1,7 @@
# TODO : Delete this and associated spec once 'api/widget/config' end point is merged # TODO : Delete this and associated spec once 'api/widget/config' end point is merged
class WidgetsController < ActionController::Base class WidgetsController < ActionController::Base
include WidgetHelper
before_action :set_global_config before_action :set_global_config
before_action :set_web_widget before_action :set_web_widget
before_action :set_token before_action :set_token
@ -40,11 +42,8 @@ class WidgetsController < ActionController::Base
def build_contact def build_contact
return if @contact.present? return if @contact.present?
@contact_inbox = @web_widget.create_contact_inbox(additional_attributes) @contact_inbox, @token = build_contact_inbox_with_token(@web_widget, additional_attributes)
@contact = @contact_inbox.contact @contact = @contact_inbox.contact
payload = { source_id: @contact_inbox.source_id, inbox_id: @web_widget.inbox.id }
@token = ::Widget::TokenService.new(payload: payload).generate_token
end end
def additional_attributes def additional_attributes

View File

@ -2,6 +2,11 @@ class ConversationFinder
attr_reader :current_user, :current_account, :params attr_reader :current_user, :current_account, :params
DEFAULT_STATUS = 'open'.freeze DEFAULT_STATUS = 'open'.freeze
SORT_OPTIONS = {
latest: 'latest',
sort_on_created_at: 'sort_on_created_at',
last_user_message_at: 'last_user_message_at'
}.with_indifferent_access
# assumptions # assumptions
# inbox_id if not given, take from all conversations, else specific to inbox # inbox_id if not given, take from all conversations, else specific to inbox
@ -133,10 +138,7 @@ class ConversationFinder
@conversations = @conversations.includes( @conversations = @conversations.includes(
:taggings, :inbox, { assignee: { avatar_attachment: [:blob] } }, { contact: { avatar_attachment: [:blob] } }, :team, :contact_inbox :taggings, :inbox, { assignee: { avatar_attachment: [:blob] } }, { contact: { avatar_attachment: [:blob] } }, :team, :contact_inbox
) )
if params[:conversation_type] == 'mention' sort_by = SORT_OPTIONS[params[:sort_by]] || SORT_OPTIONS['latest']
@conversations.page(current_page) @conversations.send(sort_by).page(current_page)
else
@conversations.latest.page(current_page)
end
end end
end end

View File

@ -59,7 +59,7 @@ module Api::V1::InboxesHelper
def check_smtp_connection(channel_data, smtp) def check_smtp_connection(channel_data, smtp)
smtp.start(channel_data[:smtp_domain], channel_data[:smtp_login], channel_data[:smtp_password], smtp.start(channel_data[:smtp_domain], channel_data[:smtp_login], channel_data[:smtp_password],
channel_data[:smtp_authentication]&.to_sym || :login) channel_data[:smtp_authentication]&.to_sym || :login)
smtp.finish unless smtp&.nil? smtp.finish
end end
def set_smtp_encryption(channel_data, smtp) def set_smtp_encryption(channel_data, smtp)

View File

@ -0,0 +1,9 @@
module WidgetHelper
def build_contact_inbox_with_token(web_widget, additional_attributes = {})
contact_inbox = web_widget.create_contact_inbox(additional_attributes)
payload = { source_id: contact_inbox.source_id, inbox_id: web_widget.inbox.id }
token = ::Widget::TokenService.new(payload: payload).generate_token
[contact_inbox, token]
end
end

View File

@ -71,6 +71,10 @@ class ContactAPI extends ApiClient {
custom_attributes: customAttributes, custom_attributes: customAttributes,
}); });
} }
destroyAvatar(contactId) {
return axios.delete(`${this.url}/${contactId}/avatar`);
}
} }
export default new ContactAPI(); export default new ContactAPI();

View File

@ -12,6 +12,7 @@ describe('#ContactsAPI', () => {
expect(contactAPI).toHaveProperty('delete'); expect(contactAPI).toHaveProperty('delete');
expect(contactAPI).toHaveProperty('getConversations'); expect(contactAPI).toHaveProperty('getConversations');
expect(contactAPI).toHaveProperty('filter'); expect(contactAPI).toHaveProperty('filter');
expect(contactAPI).toHaveProperty('destroyAvatar');
}); });
describeWithAPIMock('API calls', context => { describeWithAPIMock('API calls', context => {
@ -100,6 +101,13 @@ describe('#ContactsAPI', () => {
queryPayload queryPayload
); );
}); });
it('#destroyAvatar', () => {
contactAPI.destroyAvatar(1);
expect(context.axiosMock.delete).toHaveBeenCalledWith(
'/api/v1/contacts/1/avatar'
);
});
}); });
}); });

View File

@ -383,8 +383,8 @@ $form-button-radius: var(--border-radius-normal);
// 20. Label // 20. Label
// --------- // ---------
$label-background: $primary-color; $label-background: $white;
$label-color: $white; $label-color: $black;
$label-color-alt: $black; $label-color-alt: $black;
$label-palette: $foundation-palette; $label-palette: $foundation-palette;
$label-font-size: $font-size-mini; $label-font-size: $font-size-mini;

View File

@ -4,6 +4,7 @@
.page-sub-title { .page-sub-title {
font-size: $font-size-large; font-size: $font-size-large;
word-wrap: break-word;
} }
.block-title { .block-title {

View File

@ -99,3 +99,7 @@ $ionicons-font-path: '~ionicons/fonts';
// Transitions // Transitions
$transition-ease-in: all 0.250s ease-in; $transition-ease-in: all 0.250s ease-in;
:root {
--dashboard-app-tabs-height: 3.9rem;
}

View File

@ -1,9 +1,34 @@
.tabs--container {
display: flex;
}
.tabs--container--with-border {
@include border-normal-bottom;
}
.tabs { .tabs {
@include padding($zero $space-normal); @include padding($zero $space-normal);
@include border-normal-bottom;
border-left-width: 0; border-left-width: 0;
border-right-width: 0; border-right-width: 0;
border-top-width: 0; border-top-width: 0;
display: flex;
min-width: var(--space-mega);
}
.tabs--with-scroll {
max-width: calc(100% - 64px);
overflow: hidden;
padding: 0 var(--space-smaller);
}
.tabs--scroll-button {
align-items: center;
border-radius: 0;
cursor: pointer;
display: flex;
height: auto;
justify-content: center;
min-width: var(--space-large);
} }
// Tab chat type // Tab chat type
@ -22,6 +47,7 @@
.tabs-title { .tabs-title {
@include margin($zero $space-slab); @include margin($zero $space-slab);
flex-shrink: 0;
.badge { .badge {
background: $color-background; background: $color-background;

View File

@ -4,7 +4,7 @@
<h2 class="page-sub-title"> <h2 class="page-sub-title">
{{ headerTitle }} {{ headerTitle }}
</h2> </h2>
<p v-if="headerContent" class="small-12 column"> <p v-if="headerContent" class="small-12 column wrap-content">
{{ headerContent }} {{ headerContent }}
</p> </p>
<slot /> <slot />
@ -29,3 +29,8 @@ export default {
}, },
}; };
</script> </script>
<style scoped lang="scss">
.wrap-content {
word-wrap: break-word;
}
</style>

View File

@ -0,0 +1,63 @@
import ArticleItemComponent from './ArticleItem.vue';
const STATUS_LIST = {
published: 'published',
draft: 'draft',
archived: 'archived',
};
export default {
title: 'Components/Help Center',
component: ArticleItemComponent,
argTypes: {
title: {
defaultValue: 'Setup your account',
control: {
type: 'text',
},
},
readCount: {
defaultValue: 13,
control: {
type: 'number',
},
},
category: {
defaultValue: 'Getting started',
control: {
type: 'text',
},
},
status: {
defaultValue: 'Status',
control: {
type: 'select',
options: STATUS_LIST,
},
},
updatedAt: {
defaultValue: '1657255863',
control: {
type: 'number',
},
},
},
};
const Template = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: { ArticleItemComponent },
template:
'<article-item-component v-bind="$props" ></article-item-component>',
});
export const ArticleItem = Template.bind({});
ArticleItem.args = {
title: 'Setup your account',
author: {
name: 'John Doe',
},
category: 'Getting started',
readCount: 12,
status: 'published',
updatedAt: 1657255863,
};

View File

@ -0,0 +1,129 @@
<template>
<tr>
<td>
<div class="row--article-block">
<div class="article-block">
<h6 class="sub-block-title text-truncate">
<router-link class="article-name" :to="articlePath">
{{ title }}
</router-link>
</h6>
<div class="author">
<span class="by">{{ $t('HELP_CENTER.TABLE.COLUMNS.BY') }}</span>
<span class="name">{{ articleAuthorName }}</span>
</div>
</div>
</div>
</td>
<td>{{ category }}</td>
<td>{{ readCount }}</td>
<td>
<Label :title="status" :color-scheme="labelColor" />
</td>
<td>{{ lastUpdatedAt }}</td>
</tr>
</template>
<script>
import { frontendURL } from 'dashboard/helper/URLHelper';
import Label from 'dashboard/components/ui/Label';
import timeMixin from 'dashboard/mixins/time';
export default {
components: {
Label,
},
mixins: [timeMixin],
props: {
title: {
type: String,
default: '',
required: true,
},
author: {
type: Object,
default: () => {},
},
category: {
type: String,
default: '',
},
readCount: {
type: Number,
default: 0,
},
status: {
type: String,
default: 'draft',
values: ['archived', 'draft', 'published'],
},
updatedAt: {
type: Number,
default: 0,
},
},
computed: {
lastUpdatedAt() {
return this.dynamicTime(this.updatedAt);
},
articleAuthorName() {
return this.author.name;
},
labelColor() {
switch (this.status) {
case 'archived':
return 'secondary';
case 'draft':
return 'warning';
default:
return 'success';
}
},
articlePath() {
return frontendURL(`accounts/${this.accountId}/hc/articles/${this.id}`);
},
},
};
</script>
<style lang="scss" scoped>
td {
font-weight: var(--font-weight-normal);
color: var(--s-700);
font-size: var(--font-size-mini);
padding-left: 0;
}
.row--article-block {
align-items: center;
display: flex;
text-align: left;
.article-block {
min-width: 0;
}
.sub-block-title {
margin-bottom: 0;
}
.article-name {
font-size: var(--font-size-small);
font-weight: var(--font-weight-default);
margin: 0;
text-transform: capitalize;
color: var(--s-900);
}
.author {
.by {
font-weight: var(--font-weight-normal);
color: var(--s-500);
font-size: var(--font-size-small);
}
.name {
font-weight: var(--font-weight-medium);
color: var(--s-600);
font-size: var(--font-size-mini);
}
}
}
</style>

View File

@ -0,0 +1,72 @@
import ArticleTableComponent from './ArticleTable.vue';
import { action } from '@storybook/addon-actions';
export default {
title: 'Components/Help Center',
component: ArticleTableComponent,
argTypes: {
articles: {
defaultValue: [],
control: {
type: 'array',
},
},
articleCount: {
defaultValue: 10,
control: {
type: 'number',
},
},
currentPage: {
defaultValue: 1,
control: {
type: 'number',
},
},
},
};
const Template = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: { ArticleTableComponent },
template:
'<article-table-component @onPageChange="onPageChange" v-bind="$props" ></article-table-component>',
});
export const ArticleTable = Template.bind({});
ArticleTable.args = {
articles: [
{
title: 'Setup your account',
author: {
name: 'John Doe',
},
readCount: 13,
category: 'Getting started',
status: 'published',
updatedAt: 1657255863,
},
{
title: 'Docker Configuration',
author: {
name: 'Sam Manuel',
},
readCount: 13,
category: 'Engineering',
status: 'draft',
updatedAt: 1656658046,
},
{
title: 'Campaigns',
author: {
name: 'Sam Manuel',
},
readCount: 28,
category: 'Engineering',
status: 'archived',
updatedAt: 1657590446,
},
],
articleCount: 10,
currentPage: 1,
onPageChange: action('onPageChange'),
};

View File

@ -0,0 +1,84 @@
<template>
<div class="article-container">
<table>
<thead>
<tr>
<th scope="col">{{ $t('HELP_CENTER.TABLE.HEADERS.TITLE') }}</th>
<th scope="col">{{ $t('HELP_CENTER.TABLE.HEADERS.CATEGORY') }}</th>
<th scope="col">{{ $t('HELP_CENTER.TABLE.HEADERS.READ_COUNT') }}</th>
<th scope="col">{{ $t('HELP_CENTER.TABLE.HEADERS.STATUS') }}</th>
<th scope="col">{{ $t('HELP_CENTER.TABLE.HEADERS.LAST_EDITED') }}</th>
</tr>
</thead>
<tr>
<td colspan="100%" class="horizontal-line" />
</tr>
<tbody>
<ArticleItem
v-for="article in articles"
:key="article.id"
:title="article.title"
:author="article.author"
:category="article.category"
:read-count="article.readCount"
:status="article.status"
:updated-at="article.updatedAt"
/>
</tbody>
</table>
<table-footer
:on-page-change="onPageChange"
:current-page="Number(currentPage)"
:total-count="articleCount"
/>
</div>
</template>
<script>
import ArticleItem from './ArticleItem.vue';
import TableFooter from 'dashboard/components/widgets/TableFooter';
export default {
components: {
ArticleItem,
TableFooter,
},
props: {
articles: {
type: Array,
default: () => {},
},
articleCount: {
type: Number,
default: 0,
},
currentPage: {
type: Number,
default: 1,
},
},
methods: {
onPageChange() {
this.$emit('onPageChange');
},
},
};
</script>
<style lang="scss" scoped>
.article-container {
width: 100%;
table thead th {
font-weight: var(--font-weight-bold);
text-transform: capitalize;
color: var(--s-700);
font-size: var(--font-size-small);
padding-left: 0;
}
.horizontal-line {
border-bottom: 1px solid var(--color-border);
}
.footer {
padding: 0;
}
}
</style>

View File

@ -0,0 +1,44 @@
import { action } from '@storybook/addon-actions';
import ArticleHeader from './ArticleHeader';
export default {
title: 'Components/Help Center/Header',
component: ArticleHeader,
argTypes: {
headerTitle: {
defaultValue: 'All articles',
control: {
type: 'text',
},
},
count: {
defaultValue: 112,
control: {
type: 'number',
},
},
selectedValue: {
defaultValue: 'Status',
control: {
type: 'text',
},
},
},
};
const Template = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: { ArticleHeader },
template:
'<article-header v-bind="$props" @openModal="openFilterModal" @open="openDropdown" @close="closeDropdown" ></article-header>',
});
export const ArticleHeaderView = Template.bind({});
ArticleHeaderView.args = {
headerTitle: 'All articles',
count: 112,
selectedValue: 'Status',
openFilterModal: action('openedFilterModal'),
openDropdown: action('opened'),
closeDropdown: action('closed'),
};

View File

@ -0,0 +1,169 @@
<template>
<div class="header--wrap">
<div class="header-left--wrap">
<h3 class="page-title">{{ headerTitle }}</h3>
<span class="text-block-title count-view">{{ `(${count})` }}</span>
</div>
<div class="header-right--wrap">
<woot-button
class-names="article--buttons"
icon="filter"
color-scheme="secondary"
variant="hollow"
size="small"
@click="openFilterModal"
>
{{ $t('HELP_CENTER.HEADER.FILTER') }}
</woot-button>
<woot-button
class-names="article--buttons"
icon="arrow-sort"
color-scheme="secondary"
size="small"
variant="hollow"
@click="openDropdown"
>
{{ $t('HELP_CENTER.HEADER.SORT') }}
<span class="selected-value">
{{ selectedValue }}
<Fluent-icon class="dropdown-arrow" icon="chevron-down" size="14" />
</span>
</woot-button>
<div
v-if="showSortByDropdown"
v-on-clickaway="closeDropdown"
class="dropdown-pane dropdown-pane--open"
>
<woot-dropdown-menu>
<woot-dropdown-item>
<woot-button
variant="clear"
color-scheme="secondary"
size="small"
icon="send-clock"
>
{{ 'Status' }}
</woot-button>
</woot-dropdown-item>
<woot-dropdown-item>
<woot-button
variant="clear"
color-scheme="secondary"
size="small"
icon="dual-screen-clock"
>
{{ 'Created' }}
</woot-button>
</woot-dropdown-item>
<woot-dropdown-item>
<woot-button
variant="clear"
color-scheme="secondary"
size="small"
icon="calendar-clock"
>
{{ 'Last edited' }}
</woot-button>
</woot-dropdown-item>
</woot-dropdown-menu>
</div>
<woot-button
v-tooltip.top-end="$t('HELP_CENTER.HEADER.SETTINGS_BUTTON')"
icon="settings"
class-names="article--buttons"
variant="hollow"
size="small"
color-scheme="secondary"
/>
<woot-button
class-names="article--buttons"
size="small"
color-scheme="primary"
>
{{ $t('HELP_CENTER.HEADER.NEW_BUTTON') }}
</woot-button>
</div>
</div>
</template>
<script>
import { mixin as clickaway } from 'vue-clickaway';
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon';
export default {
components: {
FluentIcon,
WootDropdownItem,
WootDropdownMenu,
},
mixins: [clickaway],
props: {
headerTitle: {
type: String,
default: '',
},
count: {
type: Number,
default: 0,
},
selectedValue: {
type: String,
default: '',
},
},
data() {
return {
showSortByDropdown: false,
};
},
methods: {
openFilterModal() {
this.$emit('openModal');
},
openDropdown() {
this.$emit('open');
this.showSortByDropdown = true;
},
closeDropdown() {
this.$emit('close');
this.showSortByDropdown = false;
},
},
};
</script>
<style scoped lang="scss">
.header--wrap {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-small) var(--space-normal);
width: 100%;
height: var(--space-larger);
}
.header-left--wrap {
display: flex;
align-items: center;
}
.header-right--wrap {
display: flex;
align-items: center;
}
.count-view {
margin-left: var(--space-smaller);
}
.selected-value {
display: inline-flex;
margin-left: var(--space-smaller);
color: var(--b-900);
align-items: center;
}
.dropdown-arrow {
margin-left: var(--space-smaller);
}
.article--buttons {
margin-left: var(--space-smaller);
}
</style>

View File

@ -0,0 +1,39 @@
import { action } from '@storybook/addon-actions';
import EditArticleHeader from './EditArticleHeader';
export default {
title: 'Components/Help Center/Header',
component: EditArticleHeader,
argTypes: {
backButtonLabel: {
defaultValue: 'Articles',
control: {
type: 'text',
},
},
draftState: {
defaultValue: 'saving',
control: {
type: 'text',
},
},
},
};
const Template = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: { EditArticleHeader },
template:
'<edit-article-header v-bind="$props" @back="onClickGoBack" @show="showPreview" @add="onClickAdd" @open="openSidebar" @close="closeSidebar" ></edit-article-header>',
});
export const EditArticleHeaderView = Template.bind({});
EditArticleHeaderView.args = {
backButtonLabel: 'Articles',
draftState: 'saving',
onClickGoBack: action('goBack'),
showPreview: action('previewOpened'),
onClickAdd: action('added'),
openSidebar: action('openedSidebar'),
closeSidebar: action('closedSidebar'),
};

View File

@ -0,0 +1,152 @@
<template>
<div class="header--wrap">
<div class="header-left--wrap">
<woot-button
icon="chevron-left"
class-names="article--buttons"
variant="clear"
color-scheme="primary"
@click="onClickGoBack"
>
{{ backButtonLabel }}
</woot-button>
</div>
<div class="header-right--wrap">
<span v-if="showDraftStatus" class="draft-status">
{{ draftStatusText }}
</span>
<woot-button
class-names="article--buttons"
icon="globe"
color-scheme="secondary"
variant="hollow"
size="small"
@click="showPreview"
>
{{ $t('HELP_CENTER.EDIT_HEADER.PREVIEW') }}
</woot-button>
<woot-button
class-names="article--buttons"
icon="add"
color-scheme="secondary"
variant="hollow"
size="small"
@click="onClickAdd"
>
{{ $t('HELP_CENTER.EDIT_HEADER.ADD_TRANSLATION') }}
</woot-button>
<woot-button
v-if="isSidebarOpen"
v-tooltip.top-end="$t('HELP_CENTER.EDIT_HEADER.OPEN_SIDEBAR')"
icon="pane-open"
class-names="article--buttons"
variant="hollow"
size="small"
color-scheme="secondary"
@click="openSidebar"
/>
<woot-button
v-else
v-tooltip.top-end="$t('HELP_CENTER.EDIT_HEADER.CLOSE_SIDEBAR')"
icon="pane-close"
class-names="article--buttons"
variant="hollow"
size="small"
color-scheme="secondary"
@click="closeSidebar"
/>
<woot-button
class-names="article--buttons"
size="small"
color-scheme="primary"
>
{{ $t('HELP_CENTER.EDIT_HEADER.PUBLISH_BUTTON') }}
</woot-button>
</div>
</div>
</template>
<script>
export default {
props: {
backButtonLabel: {
type: String,
default: '',
},
draftState: {
type: String,
default: '',
},
},
data() {
return {
isSidebarOpen: true,
};
},
computed: {
isDraftStatusSavingOrSaved() {
return this.draftState === 'saving' || 'saved';
},
draftStatusText() {
if (this.draftState === 'saving') {
return this.$t('HELP_CENTER.EDIT_HEADER.SAVING');
}
if (this.draftState === 'saved') {
return this.$t('HELP_CENTER.EDIT_HEADER.SAVED');
}
return '';
},
showDraftStatus() {
return this.isDraftStatusSavingOrSaved;
},
},
methods: {
onClickGoBack() {
this.$emit('back');
},
showPreview() {
this.$emit('show');
},
onClickAdd() {
this.$emit('add');
},
openSidebar() {
this.$emit('open');
this.isSidebarOpen = true;
},
closeSidebar() {
this.$emit('close');
this.isSidebarOpen = false;
},
},
};
</script>
<style scoped lang="scss">
.header--wrap {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-small) var(--space-normal);
width: 100%;
height: var(--space-larger);
}
.header-left--wrap {
display: flex;
align-items: center;
}
.header-right--wrap {
display: flex;
align-items: center;
}
.article--buttons {
margin-left: var(--space-smaller);
}
.draft-status {
margin-right: var(--space-smaller);
margin-left: var(--space-normal);
color: var(--s-400);
align-items: center;
font-size: var(--font-size-mini);
}
</style>

View File

@ -0,0 +1,134 @@
import { action } from '@storybook/addon-actions';
import Sidebar from './Sidebar';
import Thumbnail from 'dashboard/components/widgets/Thumbnail';
export default {
title: 'Components/Help Center/Sidebar',
component: { Sidebar, Thumbnail },
argTypes: {
thumbnailSrc: {
defaultValue: '',
control: {
type: 'text',
},
},
headerTitle: {
defaultValue: '',
control: {
type: 'text',
},
},
subTitle: {
defaultValue: '',
control: {
type: 'text',
},
},
accessibleMenuItems: [],
additionalSecondaryMenuItems: [],
},
};
const Template = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: { Sidebar },
template: '<sidebar v-bind="$props" @input="onSearch"></sidebar>',
});
export const HelpCenterSidebarView = Template.bind({});
HelpCenterSidebarView.args = {
onSearch: action('search'),
thumbnailSrc: '',
headerTitle: 'Help Center',
subTitle: 'English',
accessibleMenuItems: [
{
icon: 'book',
label: 'HELP_CENTER.ALL_ARTICLES',
key: 'helpcenter_all',
count: 199,
toState: 'accounts/1/articles/all',
toolTip: 'All Articles',
toStateName: 'helpcenter_all',
},
{
icon: 'pen',
label: 'HELP_CENTER.MY_ARTICLES',
key: 'helpcenter_mine',
count: 112,
toState: 'accounts/1/articles/mine',
toolTip: 'My articles',
toStateName: 'helpcenter_mine',
},
{
icon: 'draft',
label: 'HELP_CENTER.DRAFT',
key: 'helpcenter_draft',
count: 32,
toState: 'accounts/1/articles/draft',
toolTip: 'Draft',
toStateName: 'helpcenter_draft',
},
{
icon: 'archive',
label: 'HELP_CENTER.ARCHIVED',
key: 'helpcenter_archive',
count: 10,
toState: 'accounts/1/articles/archived',
toolTip: 'Archived',
toStateName: 'helpcenter_archive',
},
],
additionalSecondaryMenuItems: [
{
icon: 'folder',
label: 'HELP_CENTER.CATEGORY',
hasSubMenu: true,
key: 'category',
children: [
{
id: 1,
label: 'Getting started',
count: 12,
truncateLabel: true,
toState: 'accounts/1/articles/categories/new',
},
{
id: 2,
label: 'Channel',
count: 19,
truncateLabel: true,
toState: 'accounts/1/articles/categories/channel',
},
{
id: 3,
label: 'Feature',
count: 24,
truncateLabel: true,
toState: 'accounts/1/articles/categories/feature',
},
{
id: 4,
label: 'Advanced',
count: 8,
truncateLabel: true,
toState: 'accounts/1/articles/categories/advanced',
},
{
id: 5,
label: 'Mobile app',
count: 3,
truncateLabel: true,
toState: 'accounts/1/articles/categories/mobile-app',
},
{
id: 6,
label: 'Others',
count: 39,
truncateLabel: true,
toState: 'accounts/1/articles/categories/others',
},
],
},
],
};

View File

@ -0,0 +1,88 @@
<template>
<div class="main-nav secondary-menu">
<sidebar-header
:thumbnail-src="thumbnailSrc"
:header-title="headerTitle"
:sub-title="subTitle"
/>
<sidebar-search @input="onSearch" />
<!-- <transition-group name="menu-list" tag="ul" class="menu vertical"> -->
<div name="menu-list" tag="ul" class="menu vertical">
<secondary-nav-item
v-for="menuItem in accessibleMenuItems"
:key="menuItem.toState"
:menu-item="menuItem"
:is-help-center-sidebar="true"
/>
</div>
<div name="menu-list" tag="ul" class="menu vertical">
<secondary-nav-item
v-for="menuItem in additionalSecondaryMenuItems"
:key="menuItem.key"
:menu-item="menuItem"
:is-help-center-sidebar="true"
/>
</div>
<!-- </transition-group> -->
</div>
</template>
<script>
import SecondaryNavItem from 'dashboard/components/layout/sidebarComponents/SecondaryNavItem';
import SidebarSearch from './SidebarSearch';
import SidebarHeader from './SidebarHeader';
export default {
components: {
SecondaryNavItem,
SidebarSearch,
SidebarHeader,
},
props: {
thumbnailSrc: {
type: String,
default: '',
},
headerTitle: {
type: String,
default: '',
},
subTitle: {
type: String,
default: '',
},
accessibleMenuItems: {
type: Array,
default: () => [],
},
additionalSecondaryMenuItems: {
type: Array,
default: () => [],
},
},
data() {
return {};
},
methods: {
onSearch(value) {
this.$emit('input', value);
},
},
};
</script>
<style scoped lang="scss">
.secondary-menu {
background: var(--white);
border-right: 1px solid var(--s-50);
height: 100%;
width: var(--space-giga);
flex-shrink: 0;
overflow: hidden;
padding: var(--space-small);
&:hover {
overflow: auto;
}
}
</style>

View File

@ -0,0 +1,113 @@
<template>
<div class="sidebar-header--wrap">
<div class="header-left--side">
<thumbnail
size="40px"
:src="thumbnailSrc"
:username="headerTitle"
variant="square"
/>
<div class="header-title--wrap">
<h4 class="sub-block-title title-view">{{ headerTitle }}</h4>
<span class="sub-title--view">{{ subTitle }}</span>
</div>
</div>
<div class="header-right--side">
<fluent-icon
icon="arrow-up-right"
size="28px"
class="pop-out--icon"
@click="popOutHelpCenter"
/>
<fluent-icon
icon="arrow-swap"
size="28px"
class="portal-switch--icon"
@click="openSwitchPortalModal"
/>
</div>
</div>
</template>
<script>
import Thumbnail from 'dashboard/components/widgets/Thumbnail';
export default {
components: {
Thumbnail,
},
props: {
thumbnailSrc: {
type: String,
default: '',
},
headerTitle: {
type: String,
default: '',
},
subTitle: {
type: String,
default: '',
},
},
methods: {
popOutHelpCenter() {
this.$emit('pop-out');
},
openSwitchPortalModal() {
this.$emit('open');
},
},
};
</script>
<style lang="scss" scoped>
.sidebar-header--wrap {
display: flex;
height: var(--space-jumbo);
align-items: center;
justify-content: space-between;
padding: var(--space-normal) 0;
border-bottom: 1px solid var(--color-border-light);
}
.header-title--wrap {
display: flex;
align-items: flex-start;
flex-direction: column;
margin-left: var(--space-small);
}
.title-view {
margin-bottom: var(--space-zero);
}
.sub-title--view {
font-size: var(--font-size-mini);
color: var(--b-600);
}
.header-left--side {
display: flex;
align-items: center;
}
.header-right--side {
display: flex;
align-items: center;
}
.pop-out--icon {
padding: var(--space-smaller);
}
.portal-switch--icon {
padding: var(--space-smaller);
margin-left: var(--space-small);
&:hover {
cursor: pointer;
background: var(--s-50);
border-radius: var(--border-radius-normal);
}
}
</style>

View File

@ -0,0 +1,62 @@
<template>
<div class="search-input--wrap">
<div class="search-icon--wrap">
<fluent-icon icon="search" size="18" class="search-icon" />
</div>
<input
v-model="searchValue"
class="search-input"
:placeholder="$t('HELP_CENTER.SIDEBAR.SEARCH.PLACEHOLDER')"
@input="onSearch"
/>
</div>
</template>
<script>
export default {
data() {
return {
searchValue: '',
};
},
methods: {
onSearch(e) {
this.$emit('input', e.target.value);
},
},
};
</script>
<style lang="scss" scoped>
.search-input--wrap {
display: flex;
padding: var(--space-small) var(--space-zero);
width: 100%;
}
.search-input {
width: 100%;
height: var(--space-large);
border-radius: var(--border-radius-normal);
background: var(--s-25);
font-size: var(--font-size-small);
padding: var(--space-small) var(--space-small) var(--space-small)
var(--space-large);
border: 1px solid var(--s-50);
&:focus {
border-color: var(--w-500);
}
}
.search-icon--wrap {
position: relative;
}
.search-icon {
position: absolute;
color: var(--s-500);
top: var(--space-small);
left: var(--space-small);
}
</style>

View File

@ -11,6 +11,7 @@
@open-notification-panel="openNotificationPanel" @open-notification-panel="openNotificationPanel"
/> />
<secondary-sidebar <secondary-sidebar
v-if="showSecondarySidebar"
:account-id="accountId" :account-id="accountId"
:inboxes="inboxes" :inboxes="inboxes"
:labels="labels" :labels="labels"
@ -50,6 +51,12 @@ export default {
SecondarySidebar, SecondarySidebar,
}, },
mixins: [adminMixin, alertMixin, eventListenerMixins], mixins: [adminMixin, alertMixin, eventListenerMixins],
props: {
showSecondarySidebar: {
type: Boolean,
default: true,
},
},
data() { data() {
return { return {
showOptionsMenu: false, showOptionsMenu: false,
@ -191,6 +198,7 @@ export default {
display: flex; display: flex;
min-height: 0; min-height: 0;
height: 100%; height: 100%;
width: fit-content;
} }
</style> </style>

View File

@ -18,6 +18,7 @@ const settings = accountId => ({
'settings_integrations_webhook', 'settings_integrations_webhook',
'settings_integrations_integration', 'settings_integrations_integration',
'settings_applications', 'settings_applications',
'settings_integrations_dashboard_apps',
'settings_applications_webhook', 'settings_applications_webhook',
'settings_applications_integration', 'settings_applications_integration',
'general_settings', 'general_settings',

View File

@ -83,7 +83,7 @@ export default {
border-bottom-right-radius: var(--border-radius-normal); border-bottom-right-radius: var(--border-radius-normal);
display: flex; display: flex;
height: 100%; height: 100%;
justify-content: end; justify-content: flex-end;
opacity: 1; opacity: 1;
position: absolute; position: absolute;
right: 0; right: 0;

View File

@ -26,6 +26,9 @@
:class="{ 'text-truncate': shouldTruncate }" :class="{ 'text-truncate': shouldTruncate }"
> >
{{ label }} {{ label }}
<span v-if="isHelpCenterSidebar && childItemCount" class="count-view">
{{ childItemCount }}
</span>
</span> </span>
<span v-if="count" class="badge" :class="{ secondary: !isActive }"> <span v-if="count" class="badge" :class="{ secondary: !isActive }">
{{ count }} {{ count }}
@ -73,6 +76,14 @@ export default {
type: String, type: String,
default: '', default: '',
}, },
isHelpCenterSidebar: {
type: Boolean,
default: false,
},
childItemCount: {
type: Number,
default: 0,
},
}, },
computed: { computed: {
showIcon() { showIcon() {
@ -146,6 +157,7 @@ $label-badge-size: var(--space-slab);
height: $label-badge-size; height: $label-badge-size;
min-width: $label-badge-size; min-width: $label-badge-size;
margin-left: var(--space-smaller); margin-left: var(--space-smaller);
border: 1px solid var(--color-border-light);
} }
.badge.secondary { .badge.secondary {
@ -154,4 +166,19 @@ $label-badge-size: var(--space-slab);
color: var(--s-600); color: var(--s-600);
font-weight: var(--font-weight-bold); font-weight: var(--font-weight-bold);
} }
.count-view {
background: var(--s-50);
border-radius: var(--border-radius-normal);
color: var(--s-600);
font-size: var(--font-size-micro);
font-weight: var(--font-weight-bold);
margin-left: var(--space-smaller);
padding: var(--space-zero) var(--space-smaller);
&.is-active {
background: var(--w-50);
color: var(--w-500);
}
}
</style> </style>

View File

@ -1,8 +1,14 @@
<template> <template>
<li class="sidebar-item"> <li class="sidebar-item">
<span v-if="hasSubMenu" class="secondary-menu--title fs-small"> <div v-if="hasSubMenu" class="secondary-menu--wrap">
{{ $t(`SIDEBAR.${menuItem.label}`) }} <span class="secondary-menu--title fs-small">
</span> {{ $t(`SIDEBAR.${menuItem.label}`) }}
</span>
<div v-if="isHelpCenterSidebar" class="submenu-icons">
<fluent-icon icon="search" class="submenu-icon" size="16" />
<fluent-icon icon="add" class="submenu-icon" size="16" />
</div>
</div>
<router-link <router-link
v-else v-else
class="secondary-menu--title secondary-menu--link fs-small" class="secondary-menu--title secondary-menu--link fs-small"
@ -15,6 +21,13 @@
size="14" size="14"
/> />
{{ $t(`SIDEBAR.${menuItem.label}`) }} {{ $t(`SIDEBAR.${menuItem.label}`) }}
<span
v-if="isHelpCenterSidebar"
class="count-view"
:class="computedClass"
>
{{ `${menuItem.count}` }}
</span>
<span <span
v-if="menuItem.label === 'AUTOMATION'" v-if="menuItem.label === 'AUTOMATION'"
data-view-component="true" data-view-component="true"
@ -35,6 +48,8 @@
:should-truncate="child.truncateLabel" :should-truncate="child.truncateLabel"
:icon="computedInboxClass(child)" :icon="computedInboxClass(child)"
:warning-icon="computedInboxErrorClass(child)" :warning-icon="computedInboxErrorClass(child)"
:is-help-center-sidebar="isHelpCenterSidebar"
:child-item-count="child.count"
/> />
<router-link <router-link
v-if="showItem(menuItem)" v-if="showItem(menuItem)"
@ -79,6 +94,10 @@ export default {
type: Object, type: Object,
default: () => ({}), default: () => ({}),
}, },
isHelpCenterSidebar: {
type: Boolean,
default: false,
},
}, },
computed: { computed: {
...mapGetters({ activeInbox: 'getSelectedInbox' }), ...mapGetters({ activeInbox: 'getSelectedInbox' }),
@ -120,17 +139,19 @@ export default {
// If active Inbox is present // If active Inbox is present
// donot highlight conversations // donot highlight conversations
if (this.activeInbox) return ' '; if (this.activeInbox) return ' ';
if (this.hasSubMenu) {
if ( if (
this.isInboxConversation || this.isInboxConversation ||
this.isTeamsSettings || this.isTeamsSettings ||
this.isInboxsSettings || this.isInboxsSettings ||
this.isIntegrationsSettings || this.isIntegrationsSettings ||
this.isApplicationsSettings this.isApplicationsSettings
) { ) {
return 'is-active'; return 'is-active';
}
return ' ';
} }
return ' '; return '';
}, },
}, },
methods: { methods: {
@ -170,6 +191,11 @@ export default {
margin: var(--space-smaller) 0 0; margin: var(--space-smaller) 0 0;
} }
.secondary-menu--wrap {
display: flex;
justify-content: space-between;
}
.secondary-menu--title { .secondary-menu--title {
color: var(--s-600); color: var(--s-600);
display: flex; display: flex;
@ -242,6 +268,7 @@ export default {
color: var(--w-500); color: var(--w-500);
} }
} }
.beta { .beta {
padding-right: var(--space-smaller) !important; padding-right: var(--space-smaller) !important;
padding-left: var(--space-smaller) !important; padding-left: var(--space-smaller) !important;
@ -255,4 +282,34 @@ export default {
color: var(--g-800); color: var(--g-800);
border-color: var(--g-700); border-color: var(--g-700);
} }
.count-view {
background: var(--s-50);
border-radius: var(--border-radius-normal);
color: var(--s-600);
font-size: var(--font-size-micro);
font-weight: var(--font-weight-bold);
margin-left: var(--space-smaller);
padding: var(--space-zero) var(--space-smaller);
&.is-active {
background: var(--w-50);
color: var(--w-500);
}
}
.submenu-icons {
display: flex;
align-items: center;
.submenu-icon {
margin-left: var(--space-small);
color: var(--s-600);
&:hover {
cursor: pointer;
color: var(--w-500);
}
}
}
</style> </style>

View File

@ -1,13 +1,18 @@
<template> <template>
<div :class="labelClass" :style="labelStyle" :title="description"> <div :class="labelClass" :style="labelStyle" :title="description">
<button v-if="icon" class="label-action--button" @click="onClick"> <span v-if="icon" class="label-action--button">
<fluent-icon :icon="icon" size="12" class="label--icon" /> <fluent-icon :icon="icon" size="12" class="label--icon" />
</button> </span>
<span
v-if="variant === 'smooth'"
:style="{ background: color }"
class="label-color-dot"
/>
<span v-if="!href">{{ title }}</span> <span v-if="!href">{{ title }}</span>
<a v-else :href="href" :style="anchorStyle">{{ title }}</a> <a v-else :href="href" :style="anchorStyle">{{ title }}</a>
<button <button
v-if="showClose" v-if="showClose"
class="label-action--button" class="label-close--button "
:style="{ color: textColor }" :style="{ color: textColor }"
@click="onClick" @click="onClick"
> >
@ -48,14 +53,23 @@ export default {
type: String, type: String,
default: '', default: '',
}, },
color: {
type: String,
default: '',
},
colorScheme: { colorScheme: {
type: String, type: String,
default: '', default: '',
}, },
variant: {
type: String,
default: '',
},
}, },
computed: { computed: {
textColor() { textColor() {
return getContrastingTextColor(this.bgColor); if (this.variant === 'smooth') return '';
return this.color || getContrastingTextColor(this.bgColor);
}, },
labelClass() { labelClass() {
return `label ${this.colorScheme} ${this.small ? 'small' : ''}`; return `label ${this.colorScheme} ${this.small ? 'small' : ''}`;
@ -94,9 +108,17 @@ export default {
font-weight: var(--font-weight-medium); font-weight: var(--font-weight-medium);
margin-right: var(--space-smaller); margin-right: var(--space-smaller);
margin-bottom: var(--space-smaller); margin-bottom: var(--space-smaller);
padding: var(--space-smaller);
background: var(--s-50);
color: var(--s-800);
border: 1px solid var(--s-75);
height: var(--space-medium);
&.small { &.small {
font-size: var(--font-size-micro); font-size: var(--font-size-micro);
padding: var(--space-micro) var(--space-smaller);
line-height: 1.2;
letter-spacing: 0.15px;
} }
.label--icon { .label--icon {
@ -104,11 +126,6 @@ export default {
margin-right: var(--space-smaller); margin-right: var(--space-smaller);
} }
.close--icon {
cursor: pointer;
margin-left: var(--space-smaller);
}
&.small .label--icon, &.small .label--icon,
&.small .close--icon { &.small .close--icon {
font-size: var(--font-size-nano); font-size: var(--font-size-nano);
@ -164,7 +181,31 @@ export default {
} }
} }
.label-close--button {
color: var(--s-800);
margin-bottom: var(--space-minus-micro);
margin-left: var(--space-smaller);
border-radius: var(--border-radius-small);
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
&:hover {
background: var(--s-100);
}
}
.label-action--button { .label-action--button {
margin-bottom: var(--space-minus-micro); margin-bottom: var(--space-minus-micro);
} }
.label-color-dot {
display: inline-block;
width: var(--space-one);
height: var(--space-one);
border-radius: var(--border-radius-small);
margin-right: var(--space-smaller);
}
</style> </style>

View File

@ -5,8 +5,61 @@ export default {
type: Number, type: Number,
default: 0, default: 0,
}, },
border: {
type: Boolean,
default: true,
},
}, },
render() { data() {
return { hasScroll: false };
},
created() {
window.addEventListener('resize', this.computeScrollWidth);
},
beforeDestroy() {
window.removeEventListener('resize', this.computeScrollWidth);
},
mounted() {
this.computeScrollWidth();
},
methods: {
computeScrollWidth() {
const tabElement = this.$el.getElementsByClassName('tabs')[0];
this.hasScroll = tabElement.scrollWidth > tabElement.clientWidth;
},
onScrollClick(direction) {
const tabElement = this.$el.getElementsByClassName('tabs')[0];
let scrollPosition = tabElement.scrollLeft;
if (direction === 'left') {
scrollPosition -= 100;
} else {
scrollPosition += 100;
}
tabElement.scrollTo({
top: 0,
left: scrollPosition,
behavior: 'smooth',
});
},
createScrollButton(createElement, direction) {
if (!this.hasScroll) {
return false;
}
return createElement(
'button',
{
class: 'tabs--scroll-button button clear secondary button--only-icon',
on: { click: () => this.onScrollClick(direction) },
},
[
createElement('fluent-icon', {
props: { icon: `chevron-${direction}`, size: 16 },
}),
]
);
},
},
render(createElement) {
const Tabs = this.$slots.default const Tabs = this.$slots.default
.filter( .filter(
node => node =>
@ -18,14 +71,21 @@ export default {
data.index = index; data.index = index;
return node; return node;
}); });
const leftButton = this.createScrollButton(createElement, 'left');
const rightButton = this.createScrollButton(createElement, 'right');
return ( return (
<ul <div
class={{ class={{
tabs: true, 'tabs--container--with-border': this.border,
'tabs--container': true,
}} }}
> >
{Tabs} {leftButton}
</ul> <ul class={{ tabs: true, 'tabs--with-scroll': this.hasScroll }}>
{Tabs}
</ul>
{rightButton}
</div>
); );
}, },
}; };

View File

@ -1,6 +1,7 @@
<template> <template>
<button <button
class="button" class="button"
:type="type"
:class="buttonClasses" :class="buttonClasses"
:disabled="isDisabled || isLoading" :disabled="isDisabled || isLoading"
@click="handleClick" @click="handleClick"
@ -24,6 +25,10 @@ export default {
name: 'WootButton', name: 'WootButton',
components: { EmojiOrIcon, Spinner }, components: { EmojiOrIcon, Spinner },
props: { props: {
type: {
type: String,
default: 'submit',
},
variant: { variant: {
type: String, type: String,
default: '', default: '',

View File

@ -40,13 +40,18 @@ export default {
type: Boolean, type: Boolean,
default: true, default: true,
}, },
variant: {
type: String,
default: 'circle',
},
}, },
computed: { computed: {
style() { style() {
let style = { let style = {
width: `${this.size}px`, width: `${this.size}px`,
height: `${this.size}px`, height: `${this.size}px`,
borderRadius: this.rounded ? '50%' : 0, borderRadius:
this.variant === 'square' ? 'var(--border-radius-large)' : '50%',
lineHeight: `${this.size + Math.floor(this.size / 20)}px`, lineHeight: `${this.size + Math.floor(this.size / 20)}px`,
fontSize: `${Math.floor(this.size / 2.5)}px`, fontSize: `${Math.floor(this.size / 2.5)}px`,
}; };

View File

@ -7,7 +7,8 @@
:title="label.title" :title="label.title"
:description="label.description" :description="label.description"
:show-close="true" :show-close="true"
:bg-color="label.color" :color="label.color"
variant="smooth"
@click="removeItem" @click="removeItem"
/> />
<div class="dropdown-wrap"> <div class="dropdown-wrap">

View File

@ -0,0 +1,61 @@
import Thumbnail from './Thumbnail.vue';
export default {
title: 'Components/Thumbnail',
component: Thumbnail,
argTypes: {
src: {
control: {
type: 'text',
},
},
size: {
control: {
type: 'text',
},
},
badge: {
control: {
type: 'select',
options: ['fb', 'whatsapp', 'sms', 'twitter-tweet', 'twitter-dm'],
},
},
variant: {
control: {
type: 'select',
options: ['circle', 'square'],
},
},
username: {
defaultValue: 'John Doe',
control: {
type: 'text',
},
},
status: {
defaultValue: 'circle',
control: {
type: 'select',
options: ['online', 'busy'],
},
},
hasBorder: {
control: {
type: 'boolean',
},
},
shouldShowStatusAlways: {
control: {
type: 'boolean',
},
},
},
};
const Template = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: { Thumbnail },
template: '<thumbnail v-bind="$props" @click="onClick">{{label}}</thumbnail>',
});
export const Primary = Template.bind({});

View File

@ -12,6 +12,7 @@
:username="username" :username="username"
:class="thumbnailClass" :class="thumbnailClass"
:size="avatarSize" :size="avatarSize"
:variant="variant"
/> />
<img <img
v-if="badge === 'instagram_direct_message'" v-if="badge === 'instagram_direct_message'"
@ -119,6 +120,10 @@ export default {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
variant: {
type: String,
default: 'circle',
},
}, },
data() { data() {
return { return {
@ -145,7 +150,9 @@ export default {
}, },
thumbnailClass() { thumbnailClass() {
const classname = this.hasBorder ? 'border' : ''; const classname = this.hasBorder ? 'border' : '';
return `user-thumbnail ${classname}`; const variant =
this.variant === 'circle' ? 'thumbnail-rounded' : 'thumbnail-square';
return `user-thumbnail ${classname} ${variant}`;
}, },
}, },
watch: { watch: {
@ -173,6 +180,9 @@ export default {
.user-thumbnail { .user-thumbnail {
border-radius: 50%; border-radius: 50%;
&.thumbnail-square {
border-radius: var(--border-radius-large);
}
height: 100%; height: 100%;
width: 100%; width: 100%;
box-sizing: border-box; box-sizing: border-box;

View File

@ -141,6 +141,7 @@ export default {
.dashboard-app--tabs { .dashboard-app--tabs {
background: var(--white); background: var(--white);
margin-top: -1px; margin-top: -1px;
min-height: var(--dashboard-app-tabs-height);
} }
.messages-and-sidebar { .messages-and-sidebar {

View File

@ -66,7 +66,7 @@
<spinner v-if="isPending" size="tiny" /> <spinner v-if="isPending" size="tiny" />
<div <div
v-if="showAvatar" v-if="showAvatar"
v-tooltip.top="tooltipForSender" v-tooltip.left="tooltipForSender"
class="sender--info" class="sender--info"
> >
<woot-thumbnail <woot-thumbnail
@ -313,7 +313,6 @@ export default {
return showTooltip return showTooltip
? { ? {
content: `${this.$t('CONVERSATION.SENT_BY')} ${name}`, content: `${this.$t('CONVERSATION.SENT_BY')} ${name}`,
classes: 'top',
} }
: false; : false;
}, },

View File

@ -22,7 +22,10 @@
/> />
</div> </div>
<div class="container"> <div class="container">
<div v-if="uiFlags.isUpdating" class="agent__list-loading"> <div
v-if="assignableAgentsUiFlags.isFetching"
class="agent__list-loading"
>
<spinner /> <spinner />
<p>{{ $t('BULK_ACTION.AGENT_LIST_LOADING') }}</p> <p>{{ $t('BULK_ACTION.AGENT_LIST_LOADING') }}</p>
</div> </div>
@ -43,7 +46,8 @@
<li v-for="agent in filteredAgents" :key="agent.id"> <li v-for="agent in filteredAgents" :key="agent.id">
<div class="agent-list-item" @click="assignAgent(agent)"> <div class="agent-list-item" @click="assignAgent(agent)">
<thumbnail <thumbnail
src="agent.thumbnail" :src="agent.thumbnail"
:status="agent.availability_status"
:username="agent.name" :username="agent.name"
size="22px" size="22px"
class="margin-right-small" class="margin-right-small"
@ -171,7 +175,7 @@ export default {
transform-origin: top right; transform-origin: top right;
width: auto; width: auto;
z-index: var(--z-index-twenty); z-index: var(--z-index-twenty);
min-width: var(--space-giga);
.header { .header {
padding: var(--space-one); padding: var(--space-one);
@ -182,7 +186,7 @@ export default {
} }
.container { .container {
max-height: 24rem; max-height: var(--space-giga);
overflow-y: auto; overflow-y: auto;
.agent__list-container { .agent__list-container {
height: 100%; height: 100%;
@ -264,5 +268,6 @@ ul {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
flex-direction: column; flex-direction: column;
padding: var(--space-two);
} }
</style> </style>

View File

@ -50,7 +50,7 @@
:value="label.title" :value="label.title"
class="label-checkbox" class="label-checkbox"
/> />
<span class="label-title">{{ label.title }}</span> <span class="label-title text-truncate">{{ label.title }}</span>
<span <span
class="label-pill" class="label-pill"
:style="{ backgroundColor: label.color }" :style="{ backgroundColor: label.color }"
@ -110,7 +110,7 @@ export default {
.labels-list { .labels-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
max-height: 24rem; max-height: var(--space-giga);
min-height: auto; min-height: auto;
.labels-list__header { .labels-list__header {
@ -157,8 +157,8 @@ export default {
border-radius: var(--border-radius-large); border-radius: var(--border-radius-large);
border: 1px solid var(--s-50); border: 1px solid var(--s-50);
box-shadow: var(--shadow-dropdown-pane); box-shadow: var(--shadow-dropdown-pane);
max-width: 24rem; max-width: var(--space-giga);
min-width: 24rem; min-width: var(--space-giga);
position: absolute; position: absolute;
right: 4.5rem; right: 4.5rem;
top: var(--space-larger); top: var(--space-larger);
@ -176,7 +176,7 @@ export default {
} }
.container { .container {
max-height: 24rem; max-height: var(--space-giga);
overflow-y: auto; overflow-y: auto;
.label__list-container { .label__list-container {
@ -204,7 +204,7 @@ export default {
.triangle { .triangle {
display: block; display: block;
position: absolute; position: absolute;
right: 2rem; right: var(--space-two);
text-align: left; text-align: left;
top: calc(var(--space-slab) * -1); top: calc(var(--space-slab) * -1);
z-index: var(--z-index-one); z-index: var(--z-index-one);
@ -249,6 +249,7 @@ ul {
.label-title { .label-title {
flex-grow: 1; flex-grow: 1;
width: 100%;
} }
.label-pill { .label-pill {
@ -256,6 +257,8 @@ ul {
border-radius: var(--border-radius-medium); border-radius: var(--border-radius-medium);
height: var(--space-slab); height: var(--space-slab);
width: var(--space-slab); width: var(--space-slab);
flex-shrink: 0;
border: 1px solid var(--color-border-light);
} }
} }
} }

View File

@ -3,12 +3,18 @@
<label> <label>
<span v-if="label">{{ label }}</span> <span v-if="label">{{ label }}</span>
</label> </label>
<woot-thumbnail v-if="src" size="80px" :src="src" /> <woot-thumbnail
v-if="src"
size="80px"
:src="src"
:username="usernameAvatar"
/>
<div v-if="src && deleteAvatar" class="avatar-delete-btn"> <div v-if="src && deleteAvatar" class="avatar-delete-btn">
<woot-button <woot-button
color-scheme="alert" color-scheme="alert"
variant="hollow" variant="hollow"
size="tiny" size="tiny"
type="button"
@click="onAvatarDelete" @click="onAvatarDelete"
> >
{{ this.$t('INBOX_MGMT.DELETE.AVATAR_DELETE_BUTTON_TEXT') }} {{ this.$t('INBOX_MGMT.DELETE.AVATAR_DELETE_BUTTON_TEXT') }}
@ -38,6 +44,10 @@ export default {
type: String, type: String,
default: '', default: '',
}, },
usernameAvatar: {
type: String,
default: '',
},
deleteAvatar: { deleteAvatar: {
type: Boolean, type: Boolean,
default: false, default: false,
@ -50,7 +60,7 @@ export default {
this.$emit('change', { this.$emit('change', {
file, file,
url: URL.createObjectURL(file), url: file ? URL.createObjectURL(file) : null,
}); });
}, },
onAvatarDelete() { onAvatarDelete() {

View File

@ -2,10 +2,10 @@
<modal :show.sync="show" :on-close="onClose"> <modal :show.sync="show" :on-close="onClose">
<woot-modal-header :header-title="title" :header-content="message" /> <woot-modal-header :header-title="title" :header-content="message" />
<div class="modal-footer delete-item"> <div class="modal-footer delete-item">
<button class="alert button nice" @click="onConfirm"> <button class="alert button nice text-truncate" @click="onConfirm">
{{ confirmText }} {{ confirmText }}
</button> </button>
<button class="button clear" @click="onClose"> <button class="button clear text-truncate" @click="onClose">
{{ rejectText }} {{ rejectText }}
</button> </button>
</div> </div>

View File

@ -12,7 +12,7 @@ export const getInboxClassByType = (type, phoneNumber) => {
return 'brand-twitter'; return 'brand-twitter';
case INBOX_TYPES.TWILIO: case INBOX_TYPES.TWILIO:
return phoneNumber.startsWith('whatsapp') return phoneNumber?.startsWith('whatsapp')
? 'brand-whatsapp' ? 'brand-whatsapp'
: 'brand-sms'; : 'brand-sms';

View File

@ -1,17 +1,29 @@
{ {
"BULK_ACTION": { "BULK_ACTION": {
"CONVERSATIONS_SELECTED": "%{conversationCount} المحادثات المحددة", "CONVERSATIONS_SELECTED": "%{conversationCount} المحادثات المحددة",
"AGENT_SELECT_LABEL": "اختر وكيل", "AGENT_SELECT_LABEL": "اختر وكيل",
"ASSIGN_CONFIRMATION_LABEL": "هل أنت متأكد من أنك تريد تعيين %{conversationCount} %{conversationLabel} إلى", "ASSIGN_CONFIRMATION_LABEL": "هل أنت متأكد من أنك تريد تعيين %{conversationCount} %{conversationLabel} إلى",
"GO_BACK_LABEL": "العودة للخلف", "GO_BACK_LABEL": "العودة للخلف",
"ASSIGN_LABEL": "تكليف", "ASSIGN_LABEL": "تكليف",
"ASSIGN_AGENT_TOOLTIP": "إسناد وكيل", "ASSIGN_AGENT_TOOLTIP": "إسناد وكيل",
"RESOLVE_TOOLTIP": "إغلاق المحادثة", "ASSIGN_SUCCESFUL": "تم تعيين المحادثات بنجاح",
"ASSIGN_SUCCESFUL": "تم تعيين المحادثات بنجاح", "ASSIGN_FAILED": "فشل في تعيين المحادثات، الرجاء المحاولة مرة أخرى",
"ASSIGN_FAILED": "فشل في تعيين المحادثات، الرجاء المحاولة مرة أخرى", "RESOLVE_SUCCESFUL": "تم تسوية المحادثات بنجاح",
"RESOLVE_SUCCESFUL": "تم تسوية المحادثات بنجاح", "RESOLVE_FAILED": "فشل في حل المحادثات، يرجى المحاولة مرة أخرى",
"RESOLVE_FAILED": "فشل في حل المحادثات، يرجى المحاولة مرة أخرى", "ALL_CONVERSATIONS_SELECTED_ALERT": "المحادثات المرئية في هذه الصفحة هي المحددة فقط.",
"ALL_CONVERSATIONS_SELECTED_ALERT": "المحادثات المرئية في هذه الصفحة هي المحددة فقط.", "AGENT_LIST_LOADING": "تحميل الوكلاء",
"AGENT_LIST_LOADING": "تحميل الوكلاء" "UPDATE": {
"CHANGE_STATUS": "تغيير الحالة",
"SNOOZE_UNTIL_NEXT_REPLY": "غفوة حتى الرد القادم",
"UPDATE_SUCCESFUL": "تم تحديث حالة المحادثة بنجاح.",
"UPDATE_FAILED": "فشل تحديث المحادثات، الرجاء المحاولة مرة أخرى"
},
"LABELS": {
"ASSIGN_LABELS": "تعيين التسميات",
"NO_LABELS_FOUND": "لم يتم العثور على تسميات لـ",
"ASSIGN_SELECTED_LABELS": "تعيين التسميات المحددة",
"ASSIGN_SUCCESFUL": "تم تعيين التسميات بنجاح",
"ASSIGN_FAILED": "فشل في تعيين التسميات ، الرجاء المحاولة مرة أخرى"
} }
}
} }

View File

@ -111,7 +111,8 @@
"EMAIL_ADDRESS": { "EMAIL_ADDRESS": {
"PLACEHOLDER": "أدخل عنوان البريد الإلكتروني الخاص بجهة الاتصال", "PLACEHOLDER": "أدخل عنوان البريد الإلكتروني الخاص بجهة الاتصال",
"LABEL": "عنوان البريد الإلكتروني", "LABEL": "عنوان البريد الإلكتروني",
"DUPLICATE": "عنوان البريد الإلكتروني هذا مستخدم لجهة اتصال أخرى." "DUPLICATE": "عنوان البريد الإلكتروني هذا مستخدم لجهة اتصال أخرى.",
"ERROR": "الرجاء إدخال عنوان بريد إلكتروني صحيح."
}, },
"PHONE_NUMBER": { "PHONE_NUMBER": {
"PLACEHOLDER": "أدخل رقم الهاتف الخاص بجهة الاتصال", "PLACEHOLDER": "أدخل رقم الهاتف الخاص بجهة الاتصال",
@ -147,6 +148,12 @@
} }
} }
}, },
"DELETE_AVATAR": {
"API": {
"SUCCESS_MESSAGE": "Contact avatar deleted successfully",
"ERROR_MESSAGE": "Could not delete the contact avatar. Please try again later."
}
},
"SUCCESS_MESSAGE": "تم حفظ جهة الاتصال بنجاح", "SUCCESS_MESSAGE": "تم حفظ جهة الاتصال بنجاح",
"ERROR_MESSAGE": "حدث خطأ، الرجاء المحاولة مرة أخرى" "ERROR_MESSAGE": "حدث خطأ، الرجاء المحاولة مرة أخرى"
}, },

View File

@ -38,7 +38,8 @@
"CUSTOM_ATTRIBUTE_LINK": "الرابط", "CUSTOM_ATTRIBUTE_LINK": "الرابط",
"CUSTOM_ATTRIBUTE_CHECKBOX": "مربع", "CUSTOM_ATTRIBUTE_CHECKBOX": "مربع",
"CREATED_AT": "تم إنشاؤها في", "CREATED_AT": "تم إنشاؤها في",
"LAST_ACTIVITY": "آخر نشاط" "LAST_ACTIVITY": "آخر نشاط",
"REFERER_LINK": "Referrer link"
}, },
"GROUPS": { "GROUPS": {
"STANDARD_FILTERS": "الفلاتر القياسية", "STANDARD_FILTERS": "الفلاتر القياسية",

View File

@ -0,0 +1,24 @@
{
"HELP_CENTER": {
"HEADER": {
"FILTER": "Filter by",
"SORT": "Sort by",
"SETTINGS_BUTTON": "الإعدادات",
"NEW_BUTTON": "New Article"
},
"EDIT_HEADER": {
"PUBLISH_BUTTON": "Publish",
"PREVIEW": "Preview",
"ADD_TRANSLATION": "Add translation",
"OPEN_SIDEBAR": "Open sidebar",
"CLOSE_SIDEBAR": "Close sidebar",
"SAVING": "Draft saving...",
"SAVED": "Draft saved"
},
"TABLE": {
"COLUMNS": {
"BY": "by"
}
}
}
}

View File

@ -30,7 +30,8 @@
"ADD": { "ADD": {
"CHANNEL_NAME": { "CHANNEL_NAME": {
"LABEL": "اسم صندوق الوارد لقناة التواصل", "LABEL": "اسم صندوق الوارد لقناة التواصل",
"PLACEHOLDER": "أدخل اسم صندوق الوارد الخاص بك (مثال: Acme Inc)" "PLACEHOLDER": "أدخل اسم صندوق الوارد الخاص بك (مثال: Acme Inc)",
"ERROR": "الرجاء إدخال اسم صندوق الوارد صالح"
}, },
"WEBSITE_NAME": { "WEBSITE_NAME": {
"LABEL": "اسم الموقع", "LABEL": "اسم الموقع",
@ -82,7 +83,7 @@
}, },
"CHANNEL_GREETING_TOGGLE": { "CHANNEL_GREETING_TOGGLE": {
"LABEL": "تفعيل رسالة الترحيب التلقائية", "LABEL": "تفعيل رسالة الترحيب التلقائية",
"HELP_TEXT": "إرسال رسالة ترحيب إلى المستخدم عند بدء المحادثة.", "HELP_TEXT": "إرسال رسالة تحية تلقائياً عند إنشاء محادثة جديدة.",
"ENABLED": "مفعل", "ENABLED": "مفعل",
"DISABLED": "معطّل" "DISABLED": "معطّل"
}, },
@ -97,7 +98,10 @@
"LABEL": "لون صندوق الدردشة", "LABEL": "لون صندوق الدردشة",
"PLACEHOLDER": "تحديث اللون الرئيسي لصندوق الدردشة" "PLACEHOLDER": "تحديث اللون الرئيسي لصندوق الدردشة"
}, },
"SUBMIT_BUTTON": "إنشاء قناة تواصل" "SUBMIT_BUTTON": "إنشاء قناة تواصل",
"API": {
"ERROR_MESSAGE": "لم نتمكن من إنشاء قناة موقع، الرجاء المحاولة مرة أخرى"
}
}, },
"TWILIO": { "TWILIO": {
"TITLE": "قناة Twilio SMS/WhatsApp", "TITLE": "قناة Twilio SMS/WhatsApp",
@ -107,6 +111,12 @@
"PLACEHOLDER": "الرجاء إدخال معرف حساب Twilio الخاص بك (يعرف أيضاً بـ Account SID)", "PLACEHOLDER": "الرجاء إدخال معرف حساب Twilio الخاص بك (يعرف أيضاً بـ Account SID)",
"ERROR": "هذا الحقل مطلوب" "ERROR": "هذا الحقل مطلوب"
}, },
"MESSAGING_SERVICE_SID": {
"LABEL": "Messaging Service SID",
"PLACEHOLDER": "Please enter your Twilio Messaging Service SID",
"ERROR": "هذا الحقل مطلوب",
"USE_MESSAGING_SERVICE": "Use a Twilio Messaging Service"
},
"CHANNEL_TYPE": { "CHANNEL_TYPE": {
"LABEL": "نوع القناة", "LABEL": "نوع القناة",
"ERROR": "الرجاء تحديد نوع القناة" "ERROR": "الرجاء تحديد نوع القناة"
@ -193,6 +203,7 @@
"PROVIDERS": { "PROVIDERS": {
"LABEL": "API Provider", "LABEL": "API Provider",
"TWILIO": "تويليو", "TWILIO": "تويليو",
"WHATSAPP_CLOUD": "واتساب السحابة",
"360_DIALOG": "360dialog" "360_DIALOG": "360dialog"
}, },
"INBOX_NAME": { "INBOX_NAME": {
@ -205,13 +216,31 @@
"PLACEHOLDER": "الرجاء إدخال رقم الهاتف الذي سيتم إرسال الرسائل منه.", "PLACEHOLDER": "الرجاء إدخال رقم الهاتف الذي سيتم إرسال الرسائل منه.",
"ERROR": "الرجاء إدخال قيمة صحيحة. يجب أن يبدأ رقم الهاتف بعلامة `+`." "ERROR": "الرجاء إدخال قيمة صحيحة. يجب أن يبدأ رقم الهاتف بعلامة `+`."
}, },
"PHONE_NUMBER_ID": {
"LABEL": "رقم الهاتف",
"PLACEHOLDER": "الرجاء إدخال رقم الهاتف الذي تم الحصول عليه من لوحة تحكم مطور الفيسبوك.",
"ERROR": "الرجاء إدخال اسم صالح."
},
"BUSINESS_ACCOUNT_ID": {
"LABEL": "معرف حساب الأعمال",
"PLACEHOLDER": "الرجاء إدخال معرف حساب الأعمال الذي تم الحصول عليه من لوحة تحكم مطور الفيسبوك.",
"ERROR": "الرجاء إدخال اسم صالح."
},
"WEBHOOK_VERIFY_TOKEN": {
"LABEL": "رمز التحقق من Webhook",
"PLACEHOLDER": "أدخل رمز التحقق الذي تريد إعداده لفيسبوك على شبكة الويب.",
"ERROR": "الرجاء إدخال اسم صالح."
},
"API_KEY": { "API_KEY": {
"LABEL": "مفتاح API", "LABEL": "مفتاح API",
"SUBTITLE": "تكوين مفتاح واتسآب API.", "SUBTITLE": "تكوين مفتاح واتسآب API.",
"PLACEHOLDER": "مفتاح API", "PLACEHOLDER": "مفتاح API",
"APPLY_FOR_ACCESS": "ليس لديك أي مفتاح API؟ تقدم بطلب الوصول إلى هنا",
"ERROR": "الرجاء إدخال اسم صالح." "ERROR": "الرجاء إدخال اسم صالح."
}, },
"API_CALLBACK": {
"TITLE": "عنوان Callback URL",
"SUBTITLE": "يجب عليك تكوين URL webhook في بوابة مطور فيسبوك مع عنوان URL المذكور هنا."
},
"SUBMIT_BUTTON": "إنشاء قناة واتساب", "SUBMIT_BUTTON": "إنشاء قناة واتساب",
"API": { "API": {
"ERROR_MESSAGE": "لم نتمكن من حفظ قناة واتساب" "ERROR_MESSAGE": "لم نتمكن من حفظ قناة واتساب"
@ -298,7 +327,7 @@
}, },
"AUTH": { "AUTH": {
"TITLE": "اختر قناة", "TITLE": "اختر قناة",
"DESC": "ندعم أداة الدردشة المباشرة، صفحة الفيسبوك، ملف تويتر الشخصي، واتسب، البريد الإلكتروني وما إلى ذلك، كقنوات. إذا كنت ترغب في إنشاء قناة مخصصة، يمكنك إنشاءها باستخدام قناة API. حدد قناة واحدة من الخيارات أدناه للمتابعة." "DESC": "يدعم أدوات الدردشة الحية، والميسنجر الفيسبوك، وملفات التويتر الشخصية، و WhatsApp، ورسائل البريد الإلكتروني، إلخ، كقنوات. إذا كنت ترغب في إنشاء قناة مخصصة، يمكنك إنشاءها باستخدام قناة API. للبدء، اختر إحدى القنوات أدناه."
}, },
"AGENTS": { "AGENTS": {
"TITLE": "موظف الدعم", "TITLE": "موظف الدعم",
@ -339,7 +368,7 @@
"API": { "API": {
"SUCCESS_MESSAGE": "تم تحديث إعدادات قناة التواصل بنجاح", "SUCCESS_MESSAGE": "تم تحديث إعدادات قناة التواصل بنجاح",
"AUTO_ASSIGNMENT_SUCCESS_MESSAGE": "تم تحديث إعدادات الإسناد التلقائي بنجاح", "AUTO_ASSIGNMENT_SUCCESS_MESSAGE": "تم تحديث إعدادات الإسناد التلقائي بنجاح",
"ERROR_MESSAGE": "تعذر تحديث لون صندوق الدردشة. الرجاء المحاولة مرة أخرى لاحقاً." "ERROR_MESSAGE": "تعذر تحديث إعدادات صندوق الوارد. الرجاء المحاولة مرة أخرى لاحقاً."
}, },
"EMAIL_COLLECT_BOX": { "EMAIL_COLLECT_BOX": {
"ENABLED": "مفعل", "ENABLED": "مفعل",
@ -412,15 +441,22 @@
"INBOX_UPDATE_SUB_TEXT": "تحديث إعدادات قناة التواصل", "INBOX_UPDATE_SUB_TEXT": "تحديث إعدادات قناة التواصل",
"AUTO_ASSIGNMENT_SUB_TEXT": "تمكين أو تعطيل الإسناد التلقائي للمحادثات الجديدة إلى الموظفين المضافين إلى قناة التواصل هذه.", "AUTO_ASSIGNMENT_SUB_TEXT": "تمكين أو تعطيل الإسناد التلقائي للمحادثات الجديدة إلى الموظفين المضافين إلى قناة التواصل هذه.",
"HMAC_VERIFICATION": "التحقق من هوية المستخدم", "HMAC_VERIFICATION": "التحقق من هوية المستخدم",
"HMAC_DESCRIPTION": "من أجل التحقق من هوية المستخدم، يسمح لك SDK بتمرير 'المعرفة_hash' لكل مستخدم. يمكنك إنشاء HMAC باستخدام 'sha256' مع المفتاح المعروض هنا.", "HMAC_DESCRIPTION": "من أجل التحقق من هوية المستخدم، يمكنك تمرير 'IDer_hash` لكل مستخدم. يمكنك إنشاء تجزئة HMAC sha256 باستخدام \"المعرف\" مع المفتاح المعروض هنا.",
"HMAC_MANDATORY_VERIFICATION": "فرض التحقق من هوية المستخدم", "HMAC_MANDATORY_VERIFICATION": "فرض التحقق من هوية المستخدم",
"HMAC_MANDATORY_DESCRIPTION": "في حالة التمكين، لن تعمل طريقة حساب SDKs إلا إذا تم توفير `IDer_hash` لكل مستخدم.", "HMAC_MANDATORY_DESCRIPTION": "في حالة التمكين، سيتم رفض الطلبات المفقودة 'IDer_hash'.",
"INBOX_IDENTIFIER": "معرف صندوق الوارد", "INBOX_IDENTIFIER": "معرف صندوق الوارد",
"INBOX_IDENTIFIER_SUB_TEXT": "استخدم رمز 'inbox_identifier' المعروض هنا للمصادقة على عملاء API الخاص بك.", "INBOX_IDENTIFIER_SUB_TEXT": "استخدم رمز 'inbox_identifier' المعروض هنا للمصادقة على عملاء API الخاص بك.",
"FORWARD_EMAIL_TITLE": "إعادة التوجيه إلى البريد الإلكتروني", "FORWARD_EMAIL_TITLE": "إعادة التوجيه إلى البريد الإلكتروني",
"FORWARD_EMAIL_SUB_TEXT": "بدء إعادة توجيه رسائل البريد الإلكتروني الخاصة بك إلى عنوان البريد الإلكتروني التالي.", "FORWARD_EMAIL_SUB_TEXT": "بدء إعادة توجيه رسائل البريد الإلكتروني الخاصة بك إلى عنوان البريد الإلكتروني التالي.",
"ALLOW_MESSAGES_AFTER_RESOLVED": "السماح بالرسائل بعد حل المحادثة", "ALLOW_MESSAGES_AFTER_RESOLVED": "السماح بالرسائل بعد حل المحادثة",
"ALLOW_MESSAGES_AFTER_RESOLVED_SUB_TEXT": "السماح للمستخدمين النهائيين بإرسال رسائل حتى بعد تسوية المحادثة." "ALLOW_MESSAGES_AFTER_RESOLVED_SUB_TEXT": "السماح للمستخدمين النهائيين بإرسال رسائل حتى بعد تسوية المحادثة.",
"WHATSAPP_SECTION_SUBHEADER": "يتم استخدام مفتاح API هذا للتكامل مع واتسب APIs.",
"WHATSAPP_SECTION_TITLE": "مفتاح API"
},
"AUTO_ASSIGNMENT": {
"MAX_ASSIGNMENT_LIMIT": "حد الإسناد التلقائي",
"MAX_ASSIGNMENT_LIMIT_RANGE_ERROR": "الرجاء إدخال قيمة أكبر من 0",
"MAX_ASSIGNMENT_LIMIT_SUB_TEXT": "تحديد الحد الأقصى لعدد المحادثات من علبة الوارد هذه التي يمكن تعيينها تلقائياً إلى وكيل"
}, },
"FACEBOOK_REAUTHORIZE": { "FACEBOOK_REAUTHORIZE": {
"TITLE": "إعادة التصريح", "TITLE": "إعادة التصريح",

View File

@ -84,6 +84,52 @@
}, },
"CONNECT": { "CONNECT": {
"BUTTON_TEXT": "ربط الاتصال" "BUTTON_TEXT": "ربط الاتصال"
},
"DASHBOARD_APPS": {
"TITLE": "Dashboard Apps",
"HEADER_BTN_TXT": "Add a new dashboard app",
"SIDEBAR_TXT": "<p><b>Dashboard Apps</b></p><p>Dashboard Apps allow organizations to embed an application inside the Chatwoot dashboard to provide the context for customer support agents. This feature allows you to create an application independently and embed that inside the dashboard to provide user information, their orders, or their previous payment history.</p><p>When you embed your application using the dashboard in Chatwoot, your application will get the context of the conversation and contact as a window event. Implement a listener for the message event on your page to receive the context.</p><p>To add a new dashboard app, click on the button 'Add a new dashboard app'.</p>",
"DESCRIPTION": "Dashboard Apps allow organizations to embed an application inside the dashboard to provide the context for customer support agents. This feature allows you to create an application independently and embed that to provide user information, their orders, or their previous payment history.",
"LIST": {
"404": "There are no dashboard apps configured on this account yet",
"LOADING": "Fetching dashboard apps...",
"TABLE_HEADER": [
"الاسم",
"Endpoint"
],
"EDIT_TOOLTIP": "Edit app",
"DELETE_TOOLTIP": "Delete app"
},
"FORM": {
"TITLE_LABEL": "الاسم",
"TITLE_PLACEHOLDER": "Enter a name for your dashboard app",
"TITLE_ERROR": "A name for the dashboard app is required",
"URL_LABEL": "Endpoint",
"URL_PLACEHOLDER": "Enter the endpoint URL where your app is hosted",
"URL_ERROR": "A valid URL is required"
},
"CREATE": {
"HEADER": "Add a new dashboard app",
"FORM_SUBMIT": "إرسال",
"FORM_CANCEL": "إلغاء",
"API_SUCCESS": "Dashboard app configured successfully",
"API_ERROR": "We couldn't create an app. Please try again later"
},
"UPDATE": {
"HEADER": "Edit dashboard app",
"FORM_SUBMIT": "تحديث",
"FORM_CANCEL": "إلغاء",
"API_SUCCESS": "Dashboard app updated successfully",
"API_ERROR": "We couldn't update the app. Please try again later"
},
"DELETE": {
"CONFIRM_YES": "Yes, delete it",
"CONFIRM_NO": "No, keep it",
"TITLE": "تأكيد الحذف",
"MESSAGE": "Are you sure to delete the app - %{appName}?",
"API_SUCCESS": "Dashboard app deleted successfully",
"API_ERROR": "We couldn't delete the app. Please try again later"
}
} }
} }
} }

View File

@ -65,6 +65,7 @@
"PLACEHOLDER": "اختر نطاق المدة" "PLACEHOLDER": "اختر نطاق المدة"
}, },
"GROUP_BY_FILTER_DROPDOWN_LABEL": "تجميع بواسطة", "GROUP_BY_FILTER_DROPDOWN_LABEL": "تجميع بواسطة",
"DURATION_FILTER_LABEL": "المدة",
"GROUP_BY_DAY_OPTIONS": [ "GROUP_BY_DAY_OPTIONS": [
{ {
"id": 1, "id": 1,

View File

@ -1,17 +1,29 @@
{ {
"BULK_ACTION": { "BULK_ACTION": {
"CONVERSATIONS_SELECTED": "%{conversationCount} conversations selected", "CONVERSATIONS_SELECTED": "%{conversationCount} conversations selected",
"AGENT_SELECT_LABEL": "Select Agent", "AGENT_SELECT_LABEL": "Select Agent",
"ASSIGN_CONFIRMATION_LABEL": "Are you sure you want to assign %{conversationCount} %{conversationLabel} to", "ASSIGN_CONFIRMATION_LABEL": "Are you sure you want to assign %{conversationCount} %{conversationLabel} to",
"GO_BACK_LABEL": "Go back", "GO_BACK_LABEL": "Go back",
"ASSIGN_LABEL": "Assign", "ASSIGN_LABEL": "Assign",
"ASSIGN_AGENT_TOOLTIP": "Assign Agent", "ASSIGN_AGENT_TOOLTIP": "Assign Agent",
"RESOLVE_TOOLTIP": "Resolve", "ASSIGN_SUCCESFUL": "Conversations assigned successfully",
"ASSIGN_SUCCESFUL": "Conversations assigned successfully", "ASSIGN_FAILED": "Failed to assign conversations, please try again",
"ASSIGN_FAILED": "Failed to assign conversations, please try again", "RESOLVE_SUCCESFUL": "Conversations resolved successfully",
"RESOLVE_SUCCESFUL": "Conversations resolved successfully", "RESOLVE_FAILED": "Failed to resolve conversations, please try again",
"RESOLVE_FAILED": "Failed to resolve conversations, please try again", "ALL_CONVERSATIONS_SELECTED_ALERT": "Conversations visible on this page are only selected.",
"ALL_CONVERSATIONS_SELECTED_ALERT": "Conversations visible on this page are only selected.", "AGENT_LIST_LOADING": "Loading Agents",
"AGENT_LIST_LOADING": "Loading Agents" "UPDATE": {
"CHANGE_STATUS": "Change status",
"SNOOZE_UNTIL_NEXT_REPLY": "Snooze until next reply",
"UPDATE_SUCCESFUL": "Conversation status updated successfully.",
"UPDATE_FAILED": "Failed to update conversations, please try again"
},
"LABELS": {
"ASSIGN_LABELS": "Assign Labels",
"NO_LABELS_FOUND": "No labels found for",
"ASSIGN_SELECTED_LABELS": "Assign selected labels",
"ASSIGN_SUCCESFUL": "Labels assigned successfully",
"ASSIGN_FAILED": "Failed to assign labels, please try again"
} }
}
} }

View File

@ -111,7 +111,8 @@
"EMAIL_ADDRESS": { "EMAIL_ADDRESS": {
"PLACEHOLDER": "Добавете имейл на контакта", "PLACEHOLDER": "Добавете имейл на контакта",
"LABEL": "Имейл адрес", "LABEL": "Имейл адрес",
"DUPLICATE": "Този имейл се използва от друг контакт." "DUPLICATE": "Този имейл се използва от друг контакт.",
"ERROR": "Please enter a valid email address."
}, },
"PHONE_NUMBER": { "PHONE_NUMBER": {
"PLACEHOLDER": "Добавете телефона на контакта", "PLACEHOLDER": "Добавете телефона на контакта",
@ -147,6 +148,12 @@
} }
} }
}, },
"DELETE_AVATAR": {
"API": {
"SUCCESS_MESSAGE": "Contact avatar deleted successfully",
"ERROR_MESSAGE": "Could not delete the contact avatar. Please try again later."
}
},
"SUCCESS_MESSAGE": "Успешно запазване на контакта", "SUCCESS_MESSAGE": "Успешно запазване на контакта",
"ERROR_MESSAGE": "Възникна грешка, моля опитайте отново" "ERROR_MESSAGE": "Възникна грешка, моля опитайте отново"
}, },

View File

@ -38,7 +38,8 @@
"CUSTOM_ATTRIBUTE_LINK": "Link", "CUSTOM_ATTRIBUTE_LINK": "Link",
"CUSTOM_ATTRIBUTE_CHECKBOX": "Checkbox", "CUSTOM_ATTRIBUTE_CHECKBOX": "Checkbox",
"CREATED_AT": "Created At", "CREATED_AT": "Created At",
"LAST_ACTIVITY": "Последна активност" "LAST_ACTIVITY": "Последна активност",
"REFERER_LINK": "Referrer link"
}, },
"GROUPS": { "GROUPS": {
"STANDARD_FILTERS": "Standard Filters", "STANDARD_FILTERS": "Standard Filters",

View File

@ -0,0 +1,24 @@
{
"HELP_CENTER": {
"HEADER": {
"FILTER": "Filter by",
"SORT": "Sort by",
"SETTINGS_BUTTON": "Settings",
"NEW_BUTTON": "New Article"
},
"EDIT_HEADER": {
"PUBLISH_BUTTON": "Publish",
"PREVIEW": "Preview",
"ADD_TRANSLATION": "Add translation",
"OPEN_SIDEBAR": "Open sidebar",
"CLOSE_SIDEBAR": "Close sidebar",
"SAVING": "Draft saving...",
"SAVED": "Draft saved"
},
"TABLE": {
"COLUMNS": {
"BY": "by"
}
}
}
}

View File

@ -30,7 +30,8 @@
"ADD": { "ADD": {
"CHANNEL_NAME": { "CHANNEL_NAME": {
"LABEL": "Inbox Name", "LABEL": "Inbox Name",
"PLACEHOLDER": "Enter your inbox name (eg: Acme Inc)" "PLACEHOLDER": "Enter your inbox name (eg: Acme Inc)",
"ERROR": "Please enter a valid inbox name"
}, },
"WEBSITE_NAME": { "WEBSITE_NAME": {
"LABEL": "Website Name", "LABEL": "Website Name",
@ -82,7 +83,7 @@
}, },
"CHANNEL_GREETING_TOGGLE": { "CHANNEL_GREETING_TOGGLE": {
"LABEL": "Enable channel greeting", "LABEL": "Enable channel greeting",
"HELP_TEXT": "Send a greeting message to the users when they starts the conversation.", "HELP_TEXT": "Automatically send a greeting message when a new conversation is created.",
"ENABLED": "Enabled", "ENABLED": "Enabled",
"DISABLED": "Disabled" "DISABLED": "Disabled"
}, },
@ -97,7 +98,10 @@
"LABEL": "Widget Color", "LABEL": "Widget Color",
"PLACEHOLDER": "Update the widget color used in widget" "PLACEHOLDER": "Update the widget color used in widget"
}, },
"SUBMIT_BUTTON": "Create inbox" "SUBMIT_BUTTON": "Create inbox",
"API": {
"ERROR_MESSAGE": "We were not able to create a website channel, please try again"
}
}, },
"TWILIO": { "TWILIO": {
"TITLE": "Twilio SMS/WhatsApp Channel", "TITLE": "Twilio SMS/WhatsApp Channel",
@ -107,6 +111,12 @@
"PLACEHOLDER": "Please enter your Twilio Account SID", "PLACEHOLDER": "Please enter your Twilio Account SID",
"ERROR": "This field is required" "ERROR": "This field is required"
}, },
"MESSAGING_SERVICE_SID": {
"LABEL": "Messaging Service SID",
"PLACEHOLDER": "Please enter your Twilio Messaging Service SID",
"ERROR": "This field is required",
"USE_MESSAGING_SERVICE": "Use a Twilio Messaging Service"
},
"CHANNEL_TYPE": { "CHANNEL_TYPE": {
"LABEL": "Channel Type", "LABEL": "Channel Type",
"ERROR": "Please select your Channel Type" "ERROR": "Please select your Channel Type"
@ -193,6 +203,7 @@
"PROVIDERS": { "PROVIDERS": {
"LABEL": "API Provider", "LABEL": "API Provider",
"TWILIO": "Twilio", "TWILIO": "Twilio",
"WHATSAPP_CLOUD": "WhatsApp Cloud",
"360_DIALOG": "360Dialog" "360_DIALOG": "360Dialog"
}, },
"INBOX_NAME": { "INBOX_NAME": {
@ -205,13 +216,31 @@
"PLACEHOLDER": "Please enter the phone number from which message will be sent.", "PLACEHOLDER": "Please enter the phone number from which message will be sent.",
"ERROR": "Please enter a valid value. Phone number should start with `+` sign." "ERROR": "Please enter a valid value. Phone number should start with `+` sign."
}, },
"PHONE_NUMBER_ID": {
"LABEL": "Phone number ID",
"PLACEHOLDER": "Please enter the Phone number ID obtained from Facebook developer dashboard.",
"ERROR": "Please enter a valid value."
},
"BUSINESS_ACCOUNT_ID": {
"LABEL": "Business Account ID",
"PLACEHOLDER": "Please enter the Business Account ID obtained from Facebook developer dashboard.",
"ERROR": "Please enter a valid value."
},
"WEBHOOK_VERIFY_TOKEN": {
"LABEL": "Webhook Verify Token",
"PLACEHOLDER": "Enter a verify token which you want to configure for facebook webhooks.",
"ERROR": "Please enter a valid value."
},
"API_KEY": { "API_KEY": {
"LABEL": "API key", "LABEL": "API key",
"SUBTITLE": "Configure the WhatsApp API key.", "SUBTITLE": "Configure the WhatsApp API key.",
"PLACEHOLDER": "API key", "PLACEHOLDER": "API key",
"APPLY_FOR_ACCESS": "Don't have any API key? Apply for access here",
"ERROR": "Please enter a valid value." "ERROR": "Please enter a valid value."
}, },
"API_CALLBACK": {
"TITLE": "Callback URL",
"SUBTITLE": "You have to configure the webhook URL in facebook developer portal with the URL mentioned here."
},
"SUBMIT_BUTTON": "Create WhatsApp Channel", "SUBMIT_BUTTON": "Create WhatsApp Channel",
"API": { "API": {
"ERROR_MESSAGE": "We were not able to save the WhatsApp channel" "ERROR_MESSAGE": "We were not able to save the WhatsApp channel"
@ -298,7 +327,7 @@
}, },
"AUTH": { "AUTH": {
"TITLE": "Choose a channel", "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." "DESC": "Chatwoot supports live-chat widgets, Facebook Messenger, Twitter profiles, WhatsApp, Emails, etc., as channels. If you want to build a custom channel, you can create it using the API channel. To get started, choose one of the channels below."
}, },
"AGENTS": { "AGENTS": {
"TITLE": "Агенти", "TITLE": "Агенти",
@ -339,7 +368,7 @@
"API": { "API": {
"SUCCESS_MESSAGE": "Inbox settings updated successfully", "SUCCESS_MESSAGE": "Inbox settings updated successfully",
"AUTO_ASSIGNMENT_SUCCESS_MESSAGE": "Auto assignment updated successfully", "AUTO_ASSIGNMENT_SUCCESS_MESSAGE": "Auto assignment updated successfully",
"ERROR_MESSAGE": "Could not update widget color. Please try again later." "ERROR_MESSAGE": "We couldn't update inbox settings. Please try again later."
}, },
"EMAIL_COLLECT_BOX": { "EMAIL_COLLECT_BOX": {
"ENABLED": "Enabled", "ENABLED": "Enabled",
@ -412,15 +441,22 @@
"INBOX_UPDATE_SUB_TEXT": "Update your inbox settings", "INBOX_UPDATE_SUB_TEXT": "Update your inbox settings",
"AUTO_ASSIGNMENT_SUB_TEXT": "Enable or disable the automatic assignment of new conversations to the agents added to this inbox.", "AUTO_ASSIGNMENT_SUB_TEXT": "Enable or disable the automatic assignment of new conversations to the agents added to this inbox.",
"HMAC_VERIFICATION": "User Identity Validation", "HMAC_VERIFICATION": "User Identity Validation",
"HMAC_DESCRIPTION": "Inorder to validate the user's identity, the SDK allows you to pass an `identifier_hash` for each user. You can generate HMAC using 'sha256' with the key shown here.", "HMAC_DESCRIPTION": "In order to validate the user's identity, you can pass an `identifier_hash` for each user. You can generate a HMAC sha256 hash using the `identifier` with the key shown here.",
"HMAC_MANDATORY_VERIFICATION": "Enforce User Identity Validation", "HMAC_MANDATORY_VERIFICATION": "Enforce User Identity Validation",
"HMAC_MANDATORY_DESCRIPTION": "If enabled, Chatwoot SDKs setUser method will not work unless the `identifier_hash` is provided for each user.", "HMAC_MANDATORY_DESCRIPTION": "If enabled, requests missing the `identifier_hash` will be rejected.",
"INBOX_IDENTIFIER": "Inbox Identifier", "INBOX_IDENTIFIER": "Inbox Identifier",
"INBOX_IDENTIFIER_SUB_TEXT": "Use the `inbox_identifier` token shown here to authentication your API clients.", "INBOX_IDENTIFIER_SUB_TEXT": "Use the `inbox_identifier` token shown here to authentication your API clients.",
"FORWARD_EMAIL_TITLE": "Forward to Email", "FORWARD_EMAIL_TITLE": "Forward to Email",
"FORWARD_EMAIL_SUB_TEXT": "Start forwarding your emails to the following email address.", "FORWARD_EMAIL_SUB_TEXT": "Start forwarding your emails to the following email address.",
"ALLOW_MESSAGES_AFTER_RESOLVED": "Allow messages after conversation resolved", "ALLOW_MESSAGES_AFTER_RESOLVED": "Allow messages after conversation resolved",
"ALLOW_MESSAGES_AFTER_RESOLVED_SUB_TEXT": "Allow the end-users to send messages even after the conversation is resolved." "ALLOW_MESSAGES_AFTER_RESOLVED_SUB_TEXT": "Allow the end-users to send messages even after the conversation is resolved.",
"WHATSAPP_SECTION_SUBHEADER": "This API Key is used for the integration with the WhatsApp APIs.",
"WHATSAPP_SECTION_TITLE": "API Key"
},
"AUTO_ASSIGNMENT": {
"MAX_ASSIGNMENT_LIMIT": "Auto assignment limit",
"MAX_ASSIGNMENT_LIMIT_RANGE_ERROR": "Please enter a value greater than 0",
"MAX_ASSIGNMENT_LIMIT_SUB_TEXT": "Limit the maximum number of conversations from this inbox that can be auto assigned to an agent"
}, },
"FACEBOOK_REAUTHORIZE": { "FACEBOOK_REAUTHORIZE": {
"TITLE": "Reauthorize", "TITLE": "Reauthorize",

View File

@ -84,6 +84,52 @@
}, },
"CONNECT": { "CONNECT": {
"BUTTON_TEXT": "Connect" "BUTTON_TEXT": "Connect"
},
"DASHBOARD_APPS": {
"TITLE": "Dashboard Apps",
"HEADER_BTN_TXT": "Add a new dashboard app",
"SIDEBAR_TXT": "<p><b>Dashboard Apps</b></p><p>Dashboard Apps allow organizations to embed an application inside the Chatwoot dashboard to provide the context for customer support agents. This feature allows you to create an application independently and embed that inside the dashboard to provide user information, their orders, or their previous payment history.</p><p>When you embed your application using the dashboard in Chatwoot, your application will get the context of the conversation and contact as a window event. Implement a listener for the message event on your page to receive the context.</p><p>To add a new dashboard app, click on the button 'Add a new dashboard app'.</p>",
"DESCRIPTION": "Dashboard Apps allow organizations to embed an application inside the dashboard to provide the context for customer support agents. This feature allows you to create an application independently and embed that to provide user information, their orders, or their previous payment history.",
"LIST": {
"404": "There are no dashboard apps configured on this account yet",
"LOADING": "Fetching dashboard apps...",
"TABLE_HEADER": [
"Име",
"Endpoint"
],
"EDIT_TOOLTIP": "Edit app",
"DELETE_TOOLTIP": "Delete app"
},
"FORM": {
"TITLE_LABEL": "Име",
"TITLE_PLACEHOLDER": "Enter a name for your dashboard app",
"TITLE_ERROR": "A name for the dashboard app is required",
"URL_LABEL": "Endpoint",
"URL_PLACEHOLDER": "Enter the endpoint URL where your app is hosted",
"URL_ERROR": "A valid URL is required"
},
"CREATE": {
"HEADER": "Add a new dashboard app",
"FORM_SUBMIT": "Изпращане",
"FORM_CANCEL": "Отмени",
"API_SUCCESS": "Dashboard app configured successfully",
"API_ERROR": "We couldn't create an app. Please try again later"
},
"UPDATE": {
"HEADER": "Edit dashboard app",
"FORM_SUBMIT": "Обновяване",
"FORM_CANCEL": "Отмени",
"API_SUCCESS": "Dashboard app updated successfully",
"API_ERROR": "We couldn't update the app. Please try again later"
},
"DELETE": {
"CONFIRM_YES": "Yes, delete it",
"CONFIRM_NO": "No, keep it",
"TITLE": "Потвърди изтриването",
"MESSAGE": "Are you sure to delete the app - %{appName}?",
"API_SUCCESS": "Dashboard app deleted successfully",
"API_ERROR": "We couldn't delete the app. Please try again later"
}
} }
} }
} }

View File

@ -65,6 +65,7 @@
"PLACEHOLDER": "Select date range" "PLACEHOLDER": "Select date range"
}, },
"GROUP_BY_FILTER_DROPDOWN_LABEL": "Group By", "GROUP_BY_FILTER_DROPDOWN_LABEL": "Group By",
"DURATION_FILTER_LABEL": "Duration",
"GROUP_BY_DAY_OPTIONS": [ "GROUP_BY_DAY_OPTIONS": [
{ {
"id": 1, "id": 1,

View File

@ -1,17 +1,29 @@
{ {
"BULK_ACTION": { "BULK_ACTION": {
"CONVERSATIONS_SELECTED": "%{conversationCount} conversations selected", "CONVERSATIONS_SELECTED": "%{conversationCount} conversations selected",
"AGENT_SELECT_LABEL": "Seleccionar Agent", "AGENT_SELECT_LABEL": "Seleccionar Agent",
"ASSIGN_CONFIRMATION_LABEL": "Are you sure you want to assign %{conversationCount} %{conversationLabel} to", "ASSIGN_CONFIRMATION_LABEL": "Are you sure you want to assign %{conversationCount} %{conversationLabel} to",
"GO_BACK_LABEL": "Go back", "GO_BACK_LABEL": "Go back",
"ASSIGN_LABEL": "Assignar", "ASSIGN_LABEL": "Assignar",
"ASSIGN_AGENT_TOOLTIP": "Assign Agent", "ASSIGN_AGENT_TOOLTIP": "Assign Agent",
"RESOLVE_TOOLTIP": "Resoldre", "ASSIGN_SUCCESFUL": "Conversations assigned successfully",
"ASSIGN_SUCCESFUL": "Conversations assigned successfully", "ASSIGN_FAILED": "Failed to assign conversations, please try again",
"ASSIGN_FAILED": "Failed to assign conversations, please try again", "RESOLVE_SUCCESFUL": "Conversations resolved successfully",
"RESOLVE_SUCCESFUL": "Conversations resolved successfully", "RESOLVE_FAILED": "Failed to resolve conversations, please try again",
"RESOLVE_FAILED": "Failed to resolve conversations, please try again", "ALL_CONVERSATIONS_SELECTED_ALERT": "Conversations visible on this page are only selected.",
"ALL_CONVERSATIONS_SELECTED_ALERT": "Conversations visible on this page are only selected.", "AGENT_LIST_LOADING": "Loading Agents",
"AGENT_LIST_LOADING": "Loading Agents" "UPDATE": {
"CHANGE_STATUS": "Change status",
"SNOOZE_UNTIL_NEXT_REPLY": "Snooze until next reply",
"UPDATE_SUCCESFUL": "Conversation status updated successfully.",
"UPDATE_FAILED": "Failed to update conversations, please try again"
},
"LABELS": {
"ASSIGN_LABELS": "Assign Labels",
"NO_LABELS_FOUND": "No labels found for",
"ASSIGN_SELECTED_LABELS": "Assign selected labels",
"ASSIGN_SUCCESFUL": "Labels assigned successfully",
"ASSIGN_FAILED": "Failed to assign labels, please try again"
} }
}
} }

View File

@ -111,7 +111,8 @@
"EMAIL_ADDRESS": { "EMAIL_ADDRESS": {
"PLACEHOLDER": "Introdueix l'adreça de correu electrònic del contacte", "PLACEHOLDER": "Introdueix l'adreça de correu electrònic del contacte",
"LABEL": "Adreça de correu electrònic", "LABEL": "Adreça de correu electrònic",
"DUPLICATE": "Aquesta adreça de correu electrònic sutilitza per a un altre contacte." "DUPLICATE": "Aquesta adreça de correu electrònic sutilitza per a un altre contacte.",
"ERROR": "Introduïu una adreça de correu electrònic vàlida."
}, },
"PHONE_NUMBER": { "PHONE_NUMBER": {
"PLACEHOLDER": "Introdueix el número de telèfon del contacte", "PLACEHOLDER": "Introdueix el número de telèfon del contacte",
@ -147,6 +148,12 @@
} }
} }
}, },
"DELETE_AVATAR": {
"API": {
"SUCCESS_MESSAGE": "Contact avatar deleted successfully",
"ERROR_MESSAGE": "Could not delete the contact avatar. Please try again later."
}
},
"SUCCESS_MESSAGE": "Contacte guardat correctament", "SUCCESS_MESSAGE": "Contacte guardat correctament",
"ERROR_MESSAGE": "S'ha produït un error; tornau-ho a provar" "ERROR_MESSAGE": "S'ha produït un error; tornau-ho a provar"
}, },

View File

@ -38,7 +38,8 @@
"CUSTOM_ATTRIBUTE_LINK": "Link", "CUSTOM_ATTRIBUTE_LINK": "Link",
"CUSTOM_ATTRIBUTE_CHECKBOX": "Checkbox", "CUSTOM_ATTRIBUTE_CHECKBOX": "Checkbox",
"CREATED_AT": "Created At", "CREATED_AT": "Created At",
"LAST_ACTIVITY": "Last Activity" "LAST_ACTIVITY": "Last Activity",
"REFERER_LINK": "Referrer link"
}, },
"GROUPS": { "GROUPS": {
"STANDARD_FILTERS": "Standard Filters", "STANDARD_FILTERS": "Standard Filters",

View File

@ -0,0 +1,24 @@
{
"HELP_CENTER": {
"HEADER": {
"FILTER": "Filter by",
"SORT": "Sort by",
"SETTINGS_BUTTON": "Configuracions",
"NEW_BUTTON": "New Article"
},
"EDIT_HEADER": {
"PUBLISH_BUTTON": "Publish",
"PREVIEW": "Preview",
"ADD_TRANSLATION": "Add translation",
"OPEN_SIDEBAR": "Open sidebar",
"CLOSE_SIDEBAR": "Close sidebar",
"SAVING": "Draft saving...",
"SAVED": "Draft saved"
},
"TABLE": {
"COLUMNS": {
"BY": "by"
}
}
}
}

View File

@ -30,7 +30,8 @@
"ADD": { "ADD": {
"CHANNEL_NAME": { "CHANNEL_NAME": {
"LABEL": "Nom de la safata d'entrada", "LABEL": "Nom de la safata d'entrada",
"PLACEHOLDER": "Enter your inbox name (eg: Acme Inc)" "PLACEHOLDER": "Enter your inbox name (eg: Acme Inc)",
"ERROR": "Please enter a valid inbox name"
}, },
"WEBSITE_NAME": { "WEBSITE_NAME": {
"LABEL": "Nom del lloc web", "LABEL": "Nom del lloc web",
@ -82,7 +83,7 @@
}, },
"CHANNEL_GREETING_TOGGLE": { "CHANNEL_GREETING_TOGGLE": {
"LABEL": "Activa la salutació del canal", "LABEL": "Activa la salutació del canal",
"HELP_TEXT": "Send a greeting message to the users when they starts the conversation.", "HELP_TEXT": "Automatically send a greeting message when a new conversation is created.",
"ENABLED": "Habilita", "ENABLED": "Habilita",
"DISABLED": "Inhabilita" "DISABLED": "Inhabilita"
}, },
@ -97,7 +98,10 @@
"LABEL": "Color del Widget", "LABEL": "Color del Widget",
"PLACEHOLDER": "Actualitza el color del widget" "PLACEHOLDER": "Actualitza el color del widget"
}, },
"SUBMIT_BUTTON": "Crea la safata entrada" "SUBMIT_BUTTON": "Crea la safata entrada",
"API": {
"ERROR_MESSAGE": "We were not able to create a website channel, please try again"
}
}, },
"TWILIO": { "TWILIO": {
"TITLE": "Twilio SMS/WhatsApp Channel", "TITLE": "Twilio SMS/WhatsApp Channel",
@ -107,6 +111,12 @@
"PLACEHOLDER": "Introduïu el vostre compte Twilio SID", "PLACEHOLDER": "Introduïu el vostre compte Twilio SID",
"ERROR": "Aquest camp és obligatori" "ERROR": "Aquest camp és obligatori"
}, },
"MESSAGING_SERVICE_SID": {
"LABEL": "Messaging Service SID",
"PLACEHOLDER": "Please enter your Twilio Messaging Service SID",
"ERROR": "Aquest camp és obligatori",
"USE_MESSAGING_SERVICE": "Use a Twilio Messaging Service"
},
"CHANNEL_TYPE": { "CHANNEL_TYPE": {
"LABEL": "Tipus de canal", "LABEL": "Tipus de canal",
"ERROR": "Seleccioneu el teu tipus de canal" "ERROR": "Seleccioneu el teu tipus de canal"
@ -193,6 +203,7 @@
"PROVIDERS": { "PROVIDERS": {
"LABEL": "API Provider", "LABEL": "API Provider",
"TWILIO": "Twilio", "TWILIO": "Twilio",
"WHATSAPP_CLOUD": "WhatsApp Cloud",
"360_DIALOG": "360Dialog" "360_DIALOG": "360Dialog"
}, },
"INBOX_NAME": { "INBOX_NAME": {
@ -205,13 +216,31 @@
"PLACEHOLDER": "Introduïu el número de telèfon des del qual serà enviat el missatge.", "PLACEHOLDER": "Introduïu el número de telèfon des del qual serà enviat el missatge.",
"ERROR": "Introduïu un valor vàlid. El número de telèfon hauria de començar amb el signe `+`." "ERROR": "Introduïu un valor vàlid. El número de telèfon hauria de començar amb el signe `+`."
}, },
"PHONE_NUMBER_ID": {
"LABEL": "Phone number ID",
"PLACEHOLDER": "Please enter the Phone number ID obtained from Facebook developer dashboard.",
"ERROR": "Please enter a valid value."
},
"BUSINESS_ACCOUNT_ID": {
"LABEL": "Business Account ID",
"PLACEHOLDER": "Please enter the Business Account ID obtained from Facebook developer dashboard.",
"ERROR": "Please enter a valid value."
},
"WEBHOOK_VERIFY_TOKEN": {
"LABEL": "Webhook Verify Token",
"PLACEHOLDER": "Enter a verify token which you want to configure for facebook webhooks.",
"ERROR": "Please enter a valid value."
},
"API_KEY": { "API_KEY": {
"LABEL": "API key", "LABEL": "API key",
"SUBTITLE": "Configure the WhatsApp API key.", "SUBTITLE": "Configure the WhatsApp API key.",
"PLACEHOLDER": "API key", "PLACEHOLDER": "API key",
"APPLY_FOR_ACCESS": "Don't have any API key? Apply for access here",
"ERROR": "Please enter a valid value." "ERROR": "Please enter a valid value."
}, },
"API_CALLBACK": {
"TITLE": "Callback URL",
"SUBTITLE": "You have to configure the webhook URL in facebook developer portal with the URL mentioned here."
},
"SUBMIT_BUTTON": "Create WhatsApp Channel", "SUBMIT_BUTTON": "Create WhatsApp Channel",
"API": { "API": {
"ERROR_MESSAGE": "We were not able to save the WhatsApp channel" "ERROR_MESSAGE": "We were not able to save the WhatsApp channel"
@ -298,7 +327,7 @@
}, },
"AUTH": { "AUTH": {
"TITLE": "Choose a channel", "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." "DESC": "Chatwoot supports live-chat widgets, Facebook Messenger, Twitter profiles, WhatsApp, Emails, etc., as channels. If you want to build a custom channel, you can create it using the API channel. To get started, choose one of the channels below."
}, },
"AGENTS": { "AGENTS": {
"TITLE": "Agents", "TITLE": "Agents",
@ -339,7 +368,7 @@
"API": { "API": {
"SUCCESS_MESSAGE": "El color del widget s'ha actualitzat correctament", "SUCCESS_MESSAGE": "El color del widget s'ha actualitzat correctament",
"AUTO_ASSIGNMENT_SUCCESS_MESSAGE": "Assignació automàtica actualitzada correctament", "AUTO_ASSIGNMENT_SUCCESS_MESSAGE": "Assignació automàtica actualitzada correctament",
"ERROR_MESSAGE": "No s'ha pogut actualitzar el color del widget. Torneu-ho a provar més endavant." "ERROR_MESSAGE": "We couldn't update inbox settings. Please try again later."
}, },
"EMAIL_COLLECT_BOX": { "EMAIL_COLLECT_BOX": {
"ENABLED": "Habilita", "ENABLED": "Habilita",
@ -412,15 +441,22 @@
"INBOX_UPDATE_SUB_TEXT": "Actualitza la 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", "AUTO_ASSIGNMENT_SUB_TEXT": "Activa o desactiva l'assignació automàtica d'agents disponibles a les noves converses",
"HMAC_VERIFICATION": "Validació de la Identitat del Usuari", "HMAC_VERIFICATION": "Validació de la Identitat del Usuari",
"HMAC_DESCRIPTION": "Inorder to validate the user's identity, the SDK allows you to pass an `identifier_hash` for each user. You can generate HMAC using 'sha256' with the key shown here.", "HMAC_DESCRIPTION": "In order to validate the user's identity, you can pass an `identifier_hash` for each user. You can generate a HMAC sha256 hash using the `identifier` with the key shown here.",
"HMAC_MANDATORY_VERIFICATION": "Enforce User Identity Validation", "HMAC_MANDATORY_VERIFICATION": "Enforce User Identity Validation",
"HMAC_MANDATORY_DESCRIPTION": "If enabled, Chatwoot SDKs setUser method will not work unless the `identifier_hash` is provided for each user.", "HMAC_MANDATORY_DESCRIPTION": "If enabled, requests missing the `identifier_hash` will be rejected.",
"INBOX_IDENTIFIER": "Inbox Identifier", "INBOX_IDENTIFIER": "Inbox Identifier",
"INBOX_IDENTIFIER_SUB_TEXT": "Use the `inbox_identifier` token shown here to authentication your API clients.", "INBOX_IDENTIFIER_SUB_TEXT": "Use the `inbox_identifier` token shown here to authentication your API clients.",
"FORWARD_EMAIL_TITLE": "Forward to Email", "FORWARD_EMAIL_TITLE": "Forward to Email",
"FORWARD_EMAIL_SUB_TEXT": "Comença a reenviar els teus correus electrònics a la següent adreça electrònica.", "FORWARD_EMAIL_SUB_TEXT": "Comença a reenviar els teus correus electrònics a la següent adreça electrònica.",
"ALLOW_MESSAGES_AFTER_RESOLVED": "Allow messages after conversation resolved", "ALLOW_MESSAGES_AFTER_RESOLVED": "Allow messages after conversation resolved",
"ALLOW_MESSAGES_AFTER_RESOLVED_SUB_TEXT": "Allow the end-users to send messages even after the conversation is resolved." "ALLOW_MESSAGES_AFTER_RESOLVED_SUB_TEXT": "Allow the end-users to send messages even after the conversation is resolved.",
"WHATSAPP_SECTION_SUBHEADER": "This API Key is used for the integration with the WhatsApp APIs.",
"WHATSAPP_SECTION_TITLE": "API Key"
},
"AUTO_ASSIGNMENT": {
"MAX_ASSIGNMENT_LIMIT": "Auto assignment limit",
"MAX_ASSIGNMENT_LIMIT_RANGE_ERROR": "Please enter a value greater than 0",
"MAX_ASSIGNMENT_LIMIT_SUB_TEXT": "Limit the maximum number of conversations from this inbox that can be auto assigned to an agent"
}, },
"FACEBOOK_REAUTHORIZE": { "FACEBOOK_REAUTHORIZE": {
"TITLE": "Reautoritza", "TITLE": "Reautoritza",

View File

@ -84,6 +84,52 @@
}, },
"CONNECT": { "CONNECT": {
"BUTTON_TEXT": "Connectar" "BUTTON_TEXT": "Connectar"
},
"DASHBOARD_APPS": {
"TITLE": "Dashboard Apps",
"HEADER_BTN_TXT": "Add a new dashboard app",
"SIDEBAR_TXT": "<p><b>Dashboard Apps</b></p><p>Dashboard Apps allow organizations to embed an application inside the Chatwoot dashboard to provide the context for customer support agents. This feature allows you to create an application independently and embed that inside the dashboard to provide user information, their orders, or their previous payment history.</p><p>When you embed your application using the dashboard in Chatwoot, your application will get the context of the conversation and contact as a window event. Implement a listener for the message event on your page to receive the context.</p><p>To add a new dashboard app, click on the button 'Add a new dashboard app'.</p>",
"DESCRIPTION": "Dashboard Apps allow organizations to embed an application inside the dashboard to provide the context for customer support agents. This feature allows you to create an application independently and embed that to provide user information, their orders, or their previous payment history.",
"LIST": {
"404": "There are no dashboard apps configured on this account yet",
"LOADING": "Fetching dashboard apps...",
"TABLE_HEADER": [
"Nom",
"Endpoint"
],
"EDIT_TOOLTIP": "Edit app",
"DELETE_TOOLTIP": "Delete app"
},
"FORM": {
"TITLE_LABEL": "Nom",
"TITLE_PLACEHOLDER": "Enter a name for your dashboard app",
"TITLE_ERROR": "A name for the dashboard app is required",
"URL_LABEL": "Endpoint",
"URL_PLACEHOLDER": "Enter the endpoint URL where your app is hosted",
"URL_ERROR": "A valid URL is required"
},
"CREATE": {
"HEADER": "Add a new dashboard app",
"FORM_SUBMIT": "Envia",
"FORM_CANCEL": "Cancel·la",
"API_SUCCESS": "Dashboard app configured successfully",
"API_ERROR": "We couldn't create an app. Please try again later"
},
"UPDATE": {
"HEADER": "Edit dashboard app",
"FORM_SUBMIT": "Actualitza",
"FORM_CANCEL": "Cancel·la",
"API_SUCCESS": "Dashboard app updated successfully",
"API_ERROR": "We couldn't update the app. Please try again later"
},
"DELETE": {
"CONFIRM_YES": "Yes, delete it",
"CONFIRM_NO": "No, keep it",
"TITLE": "Confirm deletion",
"MESSAGE": "Are you sure to delete the app - %{appName}?",
"API_SUCCESS": "Dashboard app deleted successfully",
"API_ERROR": "We couldn't delete the app. Please try again later"
}
} }
} }
} }

View File

@ -65,6 +65,7 @@
"PLACEHOLDER": "Select date range" "PLACEHOLDER": "Select date range"
}, },
"GROUP_BY_FILTER_DROPDOWN_LABEL": "Group By", "GROUP_BY_FILTER_DROPDOWN_LABEL": "Group By",
"DURATION_FILTER_LABEL": "Duration",
"GROUP_BY_DAY_OPTIONS": [ "GROUP_BY_DAY_OPTIONS": [
{ {
"id": 1, "id": 1,

View File

@ -1,17 +1,29 @@
{ {
"BULK_ACTION": { "BULK_ACTION": {
"CONVERSATIONS_SELECTED": "%{conversationCount} conversations selected", "CONVERSATIONS_SELECTED": "%{conversationCount} conversations selected",
"AGENT_SELECT_LABEL": "Vybrat agenta", "AGENT_SELECT_LABEL": "Vybrat agenta",
"ASSIGN_CONFIRMATION_LABEL": "Are you sure you want to assign %{conversationCount} %{conversationLabel} to", "ASSIGN_CONFIRMATION_LABEL": "Are you sure you want to assign %{conversationCount} %{conversationLabel} to",
"GO_BACK_LABEL": "Go back", "GO_BACK_LABEL": "Go back",
"ASSIGN_LABEL": "Přiřadit", "ASSIGN_LABEL": "Přiřadit",
"ASSIGN_AGENT_TOOLTIP": "Assign Agent", "ASSIGN_AGENT_TOOLTIP": "Assign Agent",
"RESOLVE_TOOLTIP": "Vyřešit", "ASSIGN_SUCCESFUL": "Conversations assigned successfully",
"ASSIGN_SUCCESFUL": "Conversations assigned successfully", "ASSIGN_FAILED": "Failed to assign conversations, please try again",
"ASSIGN_FAILED": "Failed to assign conversations, please try again", "RESOLVE_SUCCESFUL": "Conversations resolved successfully",
"RESOLVE_SUCCESFUL": "Conversations resolved successfully", "RESOLVE_FAILED": "Failed to resolve conversations, please try again",
"RESOLVE_FAILED": "Failed to resolve conversations, please try again", "ALL_CONVERSATIONS_SELECTED_ALERT": "Conversations visible on this page are only selected.",
"ALL_CONVERSATIONS_SELECTED_ALERT": "Conversations visible on this page are only selected.", "AGENT_LIST_LOADING": "Loading Agents",
"AGENT_LIST_LOADING": "Loading Agents" "UPDATE": {
"CHANGE_STATUS": "Change status",
"SNOOZE_UNTIL_NEXT_REPLY": "Snooze until next reply",
"UPDATE_SUCCESFUL": "Conversation status updated successfully.",
"UPDATE_FAILED": "Failed to update conversations, please try again"
},
"LABELS": {
"ASSIGN_LABELS": "Assign Labels",
"NO_LABELS_FOUND": "No labels found for",
"ASSIGN_SELECTED_LABELS": "Assign selected labels",
"ASSIGN_SUCCESFUL": "Labels assigned successfully",
"ASSIGN_FAILED": "Failed to assign labels, please try again"
} }
}
} }

View File

@ -111,7 +111,8 @@
"EMAIL_ADDRESS": { "EMAIL_ADDRESS": {
"PLACEHOLDER": "Zadejte e-mailovou adresu kontaktu", "PLACEHOLDER": "Zadejte e-mailovou adresu kontaktu",
"LABEL": "E-mailová adresa", "LABEL": "E-mailová adresa",
"DUPLICATE": "Tuto e-mailovou adresu již používá jiný kontakt." "DUPLICATE": "Tuto e-mailovou adresu již používá jiný kontakt.",
"ERROR": "Zadejte prosím platnou e-mailovou adresu."
}, },
"PHONE_NUMBER": { "PHONE_NUMBER": {
"PLACEHOLDER": "Zadejte telefonní číslo kontaktu", "PLACEHOLDER": "Zadejte telefonní číslo kontaktu",
@ -147,6 +148,12 @@
} }
} }
}, },
"DELETE_AVATAR": {
"API": {
"SUCCESS_MESSAGE": "Contact avatar deleted successfully",
"ERROR_MESSAGE": "Could not delete the contact avatar. Please try again later."
}
},
"SUCCESS_MESSAGE": "Kontakt byl úspěšně uložen", "SUCCESS_MESSAGE": "Kontakt byl úspěšně uložen",
"ERROR_MESSAGE": "Došlo k chybě, zkuste to prosím znovu" "ERROR_MESSAGE": "Došlo k chybě, zkuste to prosím znovu"
}, },

View File

@ -38,7 +38,8 @@
"CUSTOM_ATTRIBUTE_LINK": "Link", "CUSTOM_ATTRIBUTE_LINK": "Link",
"CUSTOM_ATTRIBUTE_CHECKBOX": "Checkbox", "CUSTOM_ATTRIBUTE_CHECKBOX": "Checkbox",
"CREATED_AT": "Created At", "CREATED_AT": "Created At",
"LAST_ACTIVITY": "Poslední aktivita" "LAST_ACTIVITY": "Poslední aktivita",
"REFERER_LINK": "Referrer link"
}, },
"GROUPS": { "GROUPS": {
"STANDARD_FILTERS": "Standard Filters", "STANDARD_FILTERS": "Standard Filters",

View File

@ -0,0 +1,24 @@
{
"HELP_CENTER": {
"HEADER": {
"FILTER": "Filter by",
"SORT": "Sort by",
"SETTINGS_BUTTON": "Nastavení",
"NEW_BUTTON": "New Article"
},
"EDIT_HEADER": {
"PUBLISH_BUTTON": "Publish",
"PREVIEW": "Preview",
"ADD_TRANSLATION": "Add translation",
"OPEN_SIDEBAR": "Open sidebar",
"CLOSE_SIDEBAR": "Close sidebar",
"SAVING": "Draft saving...",
"SAVED": "Draft saved"
},
"TABLE": {
"COLUMNS": {
"BY": "by"
}
}
}
}

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