Merge branch 'release/2.7.0'
This commit is contained in:
@@ -100,6 +100,10 @@ jobs:
|
||||
- run:
|
||||
name: Rubocop
|
||||
command: bundle exec rubocop
|
||||
|
||||
# - run:
|
||||
# name: Brakeman
|
||||
# command: bundle exec brakeman
|
||||
|
||||
- run:
|
||||
name: eslint
|
||||
|
||||
@@ -5,4 +5,4 @@ FROM ghcr.io/chatwoot/chatwoot_codespace:latest
|
||||
# Do the set up required for chatwoot app
|
||||
WORKDIR /workspace
|
||||
COPY . /workspace
|
||||
RUN yarn && gem install bundler && bundle install
|
||||
RUN yarn && gem install bundler && bundle install
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# pre-build stage
|
||||
ARG VARIANT=3
|
||||
FROM mcr.microsoft.com/vscode/devcontainers/ruby:${VARIANT}
|
||||
|
||||
ARG VARIANT=ubuntu-20.04
|
||||
FROM mcr.microsoft.com/vscode/devcontainers/base:${VARIANT}
|
||||
|
||||
# Update args in docker-compose.yaml to set the UID/GID of the "vscode" user.
|
||||
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; \
|
||||
fi
|
||||
|
||||
# [Option] Install Node.js
|
||||
ARG INSTALL_NODE="true"
|
||||
ARG NODE_VERSION="lts/*"
|
||||
RUN if [ "${INSTALL_NODE}" = "true" ]; then su vscode -c "source /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi
|
||||
|
||||
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
||||
&& apt-get -y install --no-install-recommends \
|
||||
build-essential \
|
||||
libssl-dev \
|
||||
zlib1g-dev \
|
||||
gnupg2 \
|
||||
tar \
|
||||
tzdata \
|
||||
postgresql-client \
|
||||
libpq-dev \
|
||||
yarn \
|
||||
git \
|
||||
imagemagick \
|
||||
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
|
||||
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 \
|
||||
&& 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
|
||||
WORKDIR /workspace
|
||||
COPY . /workspace
|
||||
RUN yarn
|
||||
|
||||
# set up ruby
|
||||
COPY Gemfile Gemfile.lock ./
|
||||
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
|
||||
|
||||
@@ -23,17 +23,18 @@
|
||||
// 5432 postgres
|
||||
// 6379 redis
|
||||
// 1025,8025 mailhog
|
||||
"forwardPorts": [8025],
|
||||
//your application may need to listen on all interfaces (0.0.0.0) not just localhost for it to be available externally. Defaults to []
|
||||
"appPort": [3000, 3035],
|
||||
"forwardPorts": [8025, 3000, 3035],
|
||||
|
||||
"postCreateCommand": ".devcontainer/scripts/setup.sh && bundle exec rake db:chatwoot_prepare && yarn",
|
||||
"portsAttributes": {
|
||||
"3000": {
|
||||
"label": "Rails Server"
|
||||
},
|
||||
"3035": {
|
||||
"label": "Webpack Dev Server"
|
||||
},
|
||||
"8025": {
|
||||
"label": "Mailhog UI"
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
# uncomment the webpacker env variable
|
||||
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
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
module.exports = {
|
||||
extends: ['airbnb-base/legacy', 'prettier', 'plugin:vue/recommended'],
|
||||
extends: [
|
||||
'airbnb-base/legacy',
|
||||
'prettier',
|
||||
'plugin:vue/recommended',
|
||||
'plugin:storybook/recommended',
|
||||
],
|
||||
parserOptions: {
|
||||
parser: 'babel-eslint',
|
||||
ecmaVersion: 2020,
|
||||
|
||||
@@ -4,6 +4,7 @@ import Vuex from 'vuex';
|
||||
import VueI18n from 'vue-i18n';
|
||||
import Vuelidate from 'vuelidate';
|
||||
import Multiselect from 'vue-multiselect';
|
||||
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon';
|
||||
|
||||
import WootUiKit from '../app/javascript/dashboard/components';
|
||||
import i18n from '../app/javascript/dashboard/i18n';
|
||||
@@ -15,6 +16,7 @@ Vue.use(Vuelidate);
|
||||
Vue.use(WootUiKit);
|
||||
Vue.use(Vuex);
|
||||
Vue.component('multiselect', Multiselect);
|
||||
Vue.component('fluent-icon', FluentIcon);
|
||||
|
||||
const store = new Vuex.Store({});
|
||||
const i18nConfig = new VueI18n({
|
||||
|
||||
11
Gemfile
11
Gemfile
@@ -4,7 +4,7 @@ ruby '3.0.4'
|
||||
|
||||
##-- base gems for rails --##
|
||||
gem 'rack-cors', require: 'rack/cors'
|
||||
gem 'rails'
|
||||
gem 'rails', '~>6.1'
|
||||
# Reduces boot times through caching; required in config/boot.rb
|
||||
gem 'bootsnap', require: false
|
||||
|
||||
@@ -78,7 +78,7 @@ gem 'wisper', '2.0.0'
|
||||
# TODO: bump up gem to 2.0
|
||||
gem 'facebook-messenger'
|
||||
gem 'line-bot-api'
|
||||
gem 'twilio-ruby', '~> 5.32.0'
|
||||
gem 'twilio-ruby', '~> 5.66'
|
||||
# twitty will handle subscription of twitter account events
|
||||
# gem 'twitty', git: 'https://github.com/chatwoot/twitty'
|
||||
gem 'twitty'
|
||||
@@ -89,10 +89,6 @@ gem 'slack-ruby-client'
|
||||
# for dialogflow integrations
|
||||
gem 'google-cloud-dialogflow'
|
||||
|
||||
##--- gems for debugging and error reporting ---##
|
||||
# static analysis
|
||||
gem 'brakeman'
|
||||
|
||||
##-- apm and error monitoring ---#
|
||||
gem 'ddtrace'
|
||||
gem 'newrelic_rpm'
|
||||
@@ -160,6 +156,9 @@ end
|
||||
|
||||
group :development, :test do
|
||||
gem 'active_record_query_trace'
|
||||
##--- gems for debugging and error reporting ---##
|
||||
# static analysis
|
||||
gem 'brakeman'
|
||||
gem 'bundle-audit', require: false
|
||||
gem 'byebug', platform: :mri
|
||||
gem 'climate_control'
|
||||
|
||||
413
Gemfile.lock
413
Gemfile.lock
@@ -9,63 +9,63 @@ GIT
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
actioncable (6.1.5.1)
|
||||
actionpack (= 6.1.5.1)
|
||||
activesupport (= 6.1.5.1)
|
||||
actioncable (6.1.6.1)
|
||||
actionpack (= 6.1.6.1)
|
||||
activesupport (= 6.1.6.1)
|
||||
nio4r (~> 2.0)
|
||||
websocket-driver (>= 0.6.1)
|
||||
actionmailbox (6.1.5.1)
|
||||
actionpack (= 6.1.5.1)
|
||||
activejob (= 6.1.5.1)
|
||||
activerecord (= 6.1.5.1)
|
||||
activestorage (= 6.1.5.1)
|
||||
activesupport (= 6.1.5.1)
|
||||
actionmailbox (6.1.6.1)
|
||||
actionpack (= 6.1.6.1)
|
||||
activejob (= 6.1.6.1)
|
||||
activerecord (= 6.1.6.1)
|
||||
activestorage (= 6.1.6.1)
|
||||
activesupport (= 6.1.6.1)
|
||||
mail (>= 2.7.1)
|
||||
actionmailer (6.1.5.1)
|
||||
actionpack (= 6.1.5.1)
|
||||
actionview (= 6.1.5.1)
|
||||
activejob (= 6.1.5.1)
|
||||
activesupport (= 6.1.5.1)
|
||||
actionmailer (6.1.6.1)
|
||||
actionpack (= 6.1.6.1)
|
||||
actionview (= 6.1.6.1)
|
||||
activejob (= 6.1.6.1)
|
||||
activesupport (= 6.1.6.1)
|
||||
mail (~> 2.5, >= 2.5.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
actionpack (6.1.5.1)
|
||||
actionview (= 6.1.5.1)
|
||||
activesupport (= 6.1.5.1)
|
||||
actionpack (6.1.6.1)
|
||||
actionview (= 6.1.6.1)
|
||||
activesupport (= 6.1.6.1)
|
||||
rack (~> 2.0, >= 2.0.9)
|
||||
rack-test (>= 0.6.3)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
||||
actiontext (6.1.5.1)
|
||||
actionpack (= 6.1.5.1)
|
||||
activerecord (= 6.1.5.1)
|
||||
activestorage (= 6.1.5.1)
|
||||
activesupport (= 6.1.5.1)
|
||||
actiontext (6.1.6.1)
|
||||
actionpack (= 6.1.6.1)
|
||||
activerecord (= 6.1.6.1)
|
||||
activestorage (= 6.1.6.1)
|
||||
activesupport (= 6.1.6.1)
|
||||
nokogiri (>= 1.8.5)
|
||||
actionview (6.1.5.1)
|
||||
activesupport (= 6.1.5.1)
|
||||
actionview (6.1.6.1)
|
||||
activesupport (= 6.1.6.1)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
||||
active_record_query_trace (1.8)
|
||||
activejob (6.1.5.1)
|
||||
activesupport (= 6.1.5.1)
|
||||
activejob (6.1.6.1)
|
||||
activesupport (= 6.1.6.1)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (6.1.5.1)
|
||||
activesupport (= 6.1.5.1)
|
||||
activerecord (6.1.5.1)
|
||||
activemodel (= 6.1.5.1)
|
||||
activesupport (= 6.1.5.1)
|
||||
activerecord-import (1.3.0)
|
||||
activemodel (6.1.6.1)
|
||||
activesupport (= 6.1.6.1)
|
||||
activerecord (6.1.6.1)
|
||||
activemodel (= 6.1.6.1)
|
||||
activesupport (= 6.1.6.1)
|
||||
activerecord-import (1.4.0)
|
||||
activerecord (>= 4.2)
|
||||
activestorage (6.1.5.1)
|
||||
actionpack (= 6.1.5.1)
|
||||
activejob (= 6.1.5.1)
|
||||
activerecord (= 6.1.5.1)
|
||||
activesupport (= 6.1.5.1)
|
||||
activestorage (6.1.6.1)
|
||||
actionpack (= 6.1.6.1)
|
||||
activejob (= 6.1.6.1)
|
||||
activerecord (= 6.1.6.1)
|
||||
activesupport (= 6.1.6.1)
|
||||
marcel (~> 1.0)
|
||||
mini_mime (>= 1.1.0)
|
||||
activesupport (6.1.5.1)
|
||||
activesupport (6.1.6.1)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
i18n (>= 1.6, < 2)
|
||||
minitest (>= 5.1)
|
||||
@@ -91,20 +91,20 @@ GEM
|
||||
ast (2.4.2)
|
||||
attr_extras (6.2.5)
|
||||
aws-eventstream (1.2.0)
|
||||
aws-partitions (1.556.0)
|
||||
aws-sdk-core (3.126.2)
|
||||
aws-partitions (1.605.0)
|
||||
aws-sdk-core (3.131.2)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
aws-partitions (~> 1, >= 1.525.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
jmespath (~> 1.0)
|
||||
aws-sdk-kms (1.54.0)
|
||||
aws-sdk-core (~> 3, >= 3.126.0)
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
aws-sdk-kms (1.57.0)
|
||||
aws-sdk-core (~> 3, >= 3.127.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sdk-s3 (1.112.0)
|
||||
aws-sdk-core (~> 3, >= 3.126.0)
|
||||
aws-sdk-s3 (1.114.0)
|
||||
aws-sdk-core (~> 3, >= 3.127.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.4)
|
||||
aws-sigv4 (1.4.0)
|
||||
aws-sigv4 (1.5.0)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
azure-storage-blob (2.0.3)
|
||||
azure-storage-common (~> 2.0)
|
||||
@@ -117,31 +117,31 @@ GEM
|
||||
barnes (0.0.9)
|
||||
multi_json (~> 1)
|
||||
statsd-ruby (~> 1.1)
|
||||
bcrypt (3.1.16)
|
||||
bcrypt (3.1.18)
|
||||
bindex (0.8.1)
|
||||
bootsnap (1.10.3)
|
||||
bootsnap (1.12.0)
|
||||
msgpack (~> 1.2)
|
||||
brakeman (5.2.1)
|
||||
brakeman (5.2.3)
|
||||
browser (5.3.1)
|
||||
builder (3.2.4)
|
||||
bullet (7.0.1)
|
||||
bullet (7.0.2)
|
||||
activesupport (>= 3.0.0)
|
||||
uniform_notifier (~> 1.11)
|
||||
bundle-audit (0.1.0)
|
||||
bundler-audit
|
||||
bundler-audit (0.9.0.1)
|
||||
bundler-audit (0.9.1)
|
||||
bundler (>= 1.2.0, < 3)
|
||||
thor (~> 1.0)
|
||||
byebug (11.1.3)
|
||||
climate_control (1.0.1)
|
||||
climate_control (1.1.1)
|
||||
coderay (1.1.3)
|
||||
commonmarker (0.23.4)
|
||||
commonmarker (0.23.5)
|
||||
concurrent-ruby (1.1.10)
|
||||
connection_pool (2.2.5)
|
||||
crack (0.4.5)
|
||||
rexml
|
||||
crass (1.0.6)
|
||||
cypress-on-rails (1.12.1)
|
||||
cypress-on-rails (1.13.1)
|
||||
rack
|
||||
database_cleaner (2.0.1)
|
||||
database_cleaner-active_record (~> 2.0.0)
|
||||
@@ -151,10 +151,12 @@ GEM
|
||||
database_cleaner-core (2.0.1)
|
||||
datetime_picker_rails (0.0.7)
|
||||
momentjs-rails (>= 2.8.1)
|
||||
ddtrace (0.54.2)
|
||||
debase-ruby_core_source (<= 0.10.14)
|
||||
ddtrace (1.2.0)
|
||||
debase-ruby_core_source (= 0.10.16)
|
||||
libddprof (~> 0.6.0.1.0)
|
||||
libddwaf (~> 1.3.0.2.0)
|
||||
msgpack
|
||||
debase-ruby_core_source (0.10.14)
|
||||
debase-ruby_core_source (0.10.16)
|
||||
declarative (0.0.20)
|
||||
devise (4.8.1)
|
||||
bcrypt (~> 3.0)
|
||||
@@ -176,7 +178,7 @@ GEM
|
||||
dotenv-rails (2.7.6)
|
||||
dotenv (= 2.7.6)
|
||||
railties (>= 3.2)
|
||||
down (5.3.0)
|
||||
down (5.3.1)
|
||||
addressable (~> 2.8)
|
||||
ecma-re-validator (0.4.0)
|
||||
regexp_parser (~> 2.2)
|
||||
@@ -188,36 +190,60 @@ GEM
|
||||
facebook-messenger (2.0.1)
|
||||
httparty (~> 0.13, >= 0.13.7)
|
||||
rack (>= 1.4.5)
|
||||
factory_bot (6.2.0)
|
||||
factory_bot (6.2.1)
|
||||
activesupport (>= 5.0.0)
|
||||
factory_bot_rails (6.2.0)
|
||||
factory_bot (~> 6.2.0)
|
||||
railties (>= 5.0.0)
|
||||
faker (2.19.0)
|
||||
i18n (>= 1.6, < 2)
|
||||
faraday (1.0.1)
|
||||
multipart-post (>= 1.2, < 3)
|
||||
faker (2.21.0)
|
||||
i18n (>= 1.8.11, < 2)
|
||||
faraday (1.10.0)
|
||||
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 (~> 1.0)
|
||||
fcm (1.0.5)
|
||||
faraday (~> 1)
|
||||
fcm (1.0.8)
|
||||
faraday (>= 1.0.0, < 3.0)
|
||||
googleauth (~> 1)
|
||||
ffi (1.15.5)
|
||||
flag_shih_tzu (0.3.23)
|
||||
foreman (0.87.2)
|
||||
fugit (1.5.3)
|
||||
et-orbi (~> 1, >= 1.2.7)
|
||||
raabro (~> 1.4)
|
||||
gapic-common (0.3.4)
|
||||
google-protobuf (~> 3.12, >= 3.12.2)
|
||||
googleapis-common-protos (>= 1.3.9, < 2.0)
|
||||
googleapis-common-protos-types (>= 1.0.4, < 2.0)
|
||||
googleauth (~> 0.9)
|
||||
grpc (~> 1.25)
|
||||
geocoder (1.7.3)
|
||||
gapic-common (0.10.0)
|
||||
faraday (>= 1.9, < 3.a)
|
||||
faraday-retry (>= 1.0, < 3.a)
|
||||
google-protobuf (~> 3.14)
|
||||
googleapis-common-protos (>= 1.3.12, < 2.a)
|
||||
googleapis-common-protos-types (>= 1.3.1, < 2.a)
|
||||
googleauth (~> 1.0)
|
||||
grpc (~> 1.36)
|
||||
geocoder (1.8.0)
|
||||
gli (2.21.0)
|
||||
globalid (1.0.0)
|
||||
activesupport (>= 5.0)
|
||||
google-apis-core (0.4.2)
|
||||
google-apis-core (0.7.0)
|
||||
addressable (~> 2.5, >= 2.5.1)
|
||||
googleauth (>= 0.16.2, < 2.a)
|
||||
httpclient (>= 2.8.1, < 3.a)
|
||||
@@ -226,23 +252,27 @@ GEM
|
||||
retriable (>= 2.0, < 4.a)
|
||||
rexml
|
||||
webrick
|
||||
google-apis-iamcredentials_v1 (0.10.0)
|
||||
google-apis-core (>= 0.4, < 2.a)
|
||||
google-apis-storage_v1 (0.11.0)
|
||||
google-apis-core (>= 0.4, < 2.a)
|
||||
google-apis-iamcredentials_v1 (0.13.0)
|
||||
google-apis-core (>= 0.7, < 2.a)
|
||||
google-apis-storage_v1 (0.18.0)
|
||||
google-apis-core (>= 0.7, < 2.a)
|
||||
google-cloud-core (1.6.0)
|
||||
google-cloud-env (~> 1.0)
|
||||
google-cloud-errors (~> 1.0)
|
||||
google-cloud-dialogflow (1.2.0)
|
||||
google-cloud-core (~> 1.5)
|
||||
google-cloud-dialogflow-v2 (~> 0.1)
|
||||
google-cloud-dialogflow-v2 (0.6.4)
|
||||
gapic-common (~> 0.3)
|
||||
google-cloud-dialogflow (1.5.0)
|
||||
google-cloud-core (~> 1.6)
|
||||
google-cloud-dialogflow-v2 (>= 0.15, < 2.a)
|
||||
google-cloud-dialogflow-v2 (0.17.0)
|
||||
gapic-common (>= 0.10, < 2.a)
|
||||
google-cloud-errors (~> 1.0)
|
||||
google-cloud-env (1.5.0)
|
||||
faraday (>= 0.17.3, < 2.0)
|
||||
google-cloud-location (>= 0.0, < 2.a)
|
||||
google-cloud-env (1.6.0)
|
||||
faraday (>= 0.17.3, < 3.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)
|
||||
digest-crc (~> 0.4)
|
||||
google-apis-iamcredentials_v1 (~> 0.1)
|
||||
@@ -250,32 +280,32 @@ GEM
|
||||
google-cloud-core (~> 1.6)
|
||||
googleauth (>= 0.16.2, < 2.a)
|
||||
mini_mime (~> 1.0)
|
||||
google-protobuf (3.19.4)
|
||||
google-protobuf (3.19.4-x86_64-darwin)
|
||||
google-protobuf (3.19.4-x86_64-linux)
|
||||
google-protobuf (3.21.2)
|
||||
google-protobuf (3.21.2-x86_64-darwin)
|
||||
google-protobuf (3.21.2-x86_64-linux)
|
||||
googleapis-common-protos (1.3.12)
|
||||
google-protobuf (~> 3.14)
|
||||
googleapis-common-protos-types (~> 1.2)
|
||||
grpc (~> 1.27)
|
||||
googleapis-common-protos-types (1.3.0)
|
||||
googleapis-common-protos-types (1.3.2)
|
||||
google-protobuf (~> 3.14)
|
||||
googleauth (0.17.1)
|
||||
faraday (>= 0.17.3, < 2.0)
|
||||
googleauth (1.2.0)
|
||||
faraday (>= 0.17.3, < 3.a)
|
||||
jwt (>= 1.4, < 3.0)
|
||||
memoist (~> 0.16)
|
||||
multi_json (~> 1.11)
|
||||
os (>= 0.9, < 2.0)
|
||||
signet (~> 0.15)
|
||||
groupdate (6.0.1)
|
||||
signet (>= 0.16, < 2.a)
|
||||
groupdate (6.1.0)
|
||||
activesupport (>= 5.2)
|
||||
grpc (1.43.1)
|
||||
google-protobuf (~> 3.18)
|
||||
grpc (1.47.0)
|
||||
google-protobuf (~> 3.19)
|
||||
googleapis-common-protos-types (~> 1.0)
|
||||
grpc (1.43.1-universal-darwin)
|
||||
google-protobuf (~> 3.18)
|
||||
grpc (1.47.0-x86_64-darwin)
|
||||
google-protobuf (~> 3.19)
|
||||
googleapis-common-protos-types (~> 1.0)
|
||||
grpc (1.43.1-x86_64-linux)
|
||||
google-protobuf (~> 3.18)
|
||||
grpc (1.47.0-x86_64-linux)
|
||||
google-protobuf (~> 3.19)
|
||||
googleapis-common-protos-types (~> 1.0)
|
||||
haikunator (1.1.1)
|
||||
hairtrigger (0.2.25)
|
||||
@@ -289,13 +319,13 @@ GEM
|
||||
html2text (0.2.1)
|
||||
nokogiri (~> 1.6)
|
||||
http-accept (1.7.0)
|
||||
http-cookie (1.0.4)
|
||||
http-cookie (1.0.5)
|
||||
domain_name (~> 0.5)
|
||||
httparty (0.20.0)
|
||||
mime-types (~> 3.0)
|
||||
multi_xml (>= 0.5.2)
|
||||
httpclient (2.8.3)
|
||||
i18n (1.10.0)
|
||||
i18n (1.11.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
image_processing (1.12.2)
|
||||
mini_magick (>= 4.9.5, < 5)
|
||||
@@ -304,19 +334,19 @@ GEM
|
||||
actionview (>= 5.0.0)
|
||||
activesupport (>= 5.0.0)
|
||||
jmespath (1.6.1)
|
||||
jquery-rails (4.4.0)
|
||||
jquery-rails (4.5.0)
|
||||
rails-dom-testing (>= 1, < 3)
|
||||
railties (>= 4.2.0)
|
||||
thor (>= 0.14, < 2.0)
|
||||
json (2.6.1)
|
||||
json (2.6.2)
|
||||
json_refs (0.1.7)
|
||||
hana
|
||||
json_schemer (0.2.19)
|
||||
json_schemer (0.2.21)
|
||||
ecma-re-validator (~> 0.3)
|
||||
hana (~> 1.3)
|
||||
regexp_parser (~> 2.0)
|
||||
uri_template (~> 0.7)
|
||||
jwt (2.3.0)
|
||||
jwt (2.4.1)
|
||||
kaminari (1.2.2)
|
||||
activesupport (>= 4.1.0)
|
||||
kaminari-actionview (= 1.2.2)
|
||||
@@ -329,21 +359,31 @@ GEM
|
||||
activerecord
|
||||
kaminari-core (= 1.2.2)
|
||||
kaminari-core (1.2.2)
|
||||
koala (3.1.0)
|
||||
koala (3.2.0)
|
||||
addressable
|
||||
faraday (< 2)
|
||||
json (>= 1.8)
|
||||
rexml
|
||||
launchy (2.5.0)
|
||||
addressable (~> 2.7)
|
||||
letter_opener (1.7.0)
|
||||
launchy (~> 2.2)
|
||||
line-bot-api (1.23.0)
|
||||
liquid (5.1.0)
|
||||
letter_opener (1.8.1)
|
||||
launchy (>= 2.2, < 3)
|
||||
libddprof (0.6.0.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)
|
||||
rb-fsevent (~> 0.10, >= 0.10.3)
|
||||
rb-inotify (~> 0.9, >= 0.9.10)
|
||||
loofah (2.17.0)
|
||||
loofah (2.18.0)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.5.9)
|
||||
mail (2.7.1)
|
||||
@@ -358,36 +398,36 @@ GEM
|
||||
mini_magick (4.11.0)
|
||||
mini_mime (1.1.2)
|
||||
mini_portile2 (2.8.0)
|
||||
minitest (5.15.0)
|
||||
mock_redis (0.30.0)
|
||||
minitest (5.16.2)
|
||||
mock_redis (0.32.0)
|
||||
ruby2_keywords
|
||||
momentjs-rails (2.29.1.1)
|
||||
railties (>= 3.1)
|
||||
msgpack (1.4.5)
|
||||
msgpack (1.5.3)
|
||||
multi_json (1.15.0)
|
||||
multi_xml (0.6.0)
|
||||
multipart-post (2.1.1)
|
||||
multipart-post (2.2.3)
|
||||
net-http-persistent (4.0.1)
|
||||
connection_pool (~> 2.2)
|
||||
netrc (0.11.0)
|
||||
newrelic_rpm (8.7.0)
|
||||
newrelic_rpm (8.9.0)
|
||||
nio4r (2.5.8)
|
||||
nokogiri (1.13.6)
|
||||
nokogiri (1.13.7)
|
||||
mini_portile2 (~> 2.8.0)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.13.6-arm64-darwin)
|
||||
nokogiri (1.13.7-arm64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.13.6-x86_64-darwin)
|
||||
nokogiri (1.13.7-x86_64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.13.6-x86_64-linux)
|
||||
nokogiri (1.13.7-x86_64-linux)
|
||||
racc (~> 1.4)
|
||||
oauth (0.5.8)
|
||||
oauth (0.5.10)
|
||||
orm_adapter (0.5.0)
|
||||
os (1.1.4)
|
||||
parallel (1.21.0)
|
||||
parser (3.1.1.0)
|
||||
parallel (1.22.1)
|
||||
parser (3.1.2.0)
|
||||
ast (~> 2.4.1)
|
||||
pg (1.3.2)
|
||||
pg (1.4.1)
|
||||
pg_search (2.3.6)
|
||||
activerecord (>= 5.2)
|
||||
activesupport (>= 5.2)
|
||||
@@ -398,46 +438,46 @@ GEM
|
||||
method_source (~> 1.0)
|
||||
pry-rails (0.3.9)
|
||||
pry (>= 0.10.4)
|
||||
public_suffix (4.0.6)
|
||||
public_suffix (4.0.7)
|
||||
puma (5.6.4)
|
||||
nio4r (~> 2.0)
|
||||
pundit (2.2.0)
|
||||
activesupport (>= 3.0.0)
|
||||
raabro (1.4.0)
|
||||
racc (1.6.0)
|
||||
rack (2.2.3.1)
|
||||
rack-attack (6.6.0)
|
||||
rack (2.2.4)
|
||||
rack-attack (6.6.1)
|
||||
rack (>= 1.0, < 3)
|
||||
rack-cors (1.1.1)
|
||||
rack (>= 2.0.0)
|
||||
rack-proxy (0.7.2)
|
||||
rack
|
||||
rack-test (1.1.0)
|
||||
rack (>= 1.0, < 3)
|
||||
rack-timeout (0.6.0)
|
||||
rails (6.1.5.1)
|
||||
actioncable (= 6.1.5.1)
|
||||
actionmailbox (= 6.1.5.1)
|
||||
actionmailer (= 6.1.5.1)
|
||||
actionpack (= 6.1.5.1)
|
||||
actiontext (= 6.1.5.1)
|
||||
actionview (= 6.1.5.1)
|
||||
activejob (= 6.1.5.1)
|
||||
activemodel (= 6.1.5.1)
|
||||
activerecord (= 6.1.5.1)
|
||||
activestorage (= 6.1.5.1)
|
||||
activesupport (= 6.1.5.1)
|
||||
rack-test (2.0.2)
|
||||
rack (>= 1.3)
|
||||
rack-timeout (0.6.3)
|
||||
rails (6.1.6.1)
|
||||
actioncable (= 6.1.6.1)
|
||||
actionmailbox (= 6.1.6.1)
|
||||
actionmailer (= 6.1.6.1)
|
||||
actionpack (= 6.1.6.1)
|
||||
actiontext (= 6.1.6.1)
|
||||
actionview (= 6.1.6.1)
|
||||
activejob (= 6.1.6.1)
|
||||
activemodel (= 6.1.6.1)
|
||||
activerecord (= 6.1.6.1)
|
||||
activestorage (= 6.1.6.1)
|
||||
activesupport (= 6.1.6.1)
|
||||
bundler (>= 1.15.0)
|
||||
railties (= 6.1.5.1)
|
||||
railties (= 6.1.6.1)
|
||||
sprockets-rails (>= 2.0.0)
|
||||
rails-dom-testing (2.0.3)
|
||||
activesupport (>= 4.2.0)
|
||||
nokogiri (>= 1.6)
|
||||
rails-html-sanitizer (1.4.2)
|
||||
rails-html-sanitizer (1.4.3)
|
||||
loofah (~> 2.3)
|
||||
railties (6.1.5.1)
|
||||
actionpack (= 6.1.5.1)
|
||||
activesupport (= 6.1.5.1)
|
||||
railties (6.1.6.1)
|
||||
actionpack (= 6.1.6.1)
|
||||
activesupport (= 6.1.6.1)
|
||||
method_source
|
||||
rake (>= 12.2)
|
||||
thor (~> 1.0)
|
||||
@@ -446,11 +486,11 @@ GEM
|
||||
rb-fsevent (0.11.1)
|
||||
rb-inotify (0.10.1)
|
||||
ffi (~> 1.0)
|
||||
redis (4.6.0)
|
||||
redis-namespace (1.8.1)
|
||||
redis (4.7.1)
|
||||
redis-namespace (1.8.2)
|
||||
redis (>= 3.0.4)
|
||||
regexp_parser (2.2.1)
|
||||
representable (3.1.1)
|
||||
regexp_parser (2.5.0)
|
||||
representable (3.2.0)
|
||||
declarative (< 0.1.0)
|
||||
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||
uber (< 0.2.0)
|
||||
@@ -469,7 +509,7 @@ GEM
|
||||
rspec-expectations (3.11.0)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.11.0)
|
||||
rspec-mocks (3.11.0)
|
||||
rspec-mocks (3.11.1)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.11.0)
|
||||
rspec-rails (5.0.3)
|
||||
@@ -481,26 +521,27 @@ GEM
|
||||
rspec-mocks (~> 3.10)
|
||||
rspec-support (~> 3.10)
|
||||
rspec-support (3.11.0)
|
||||
rubocop (1.25.1)
|
||||
rubocop (1.31.2)
|
||||
json (~> 2.3)
|
||||
parallel (~> 1.10)
|
||||
parser (>= 3.1.0.0)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
regexp_parser (>= 1.8, < 3.0)
|
||||
rexml
|
||||
rubocop-ast (>= 1.15.1, < 2.0)
|
||||
rexml (>= 3.2.5, < 4.0)
|
||||
rubocop-ast (>= 1.18.0, < 2.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 1.4.0, < 3.0)
|
||||
rubocop-ast (1.16.0)
|
||||
rubocop-ast (1.19.1)
|
||||
parser (>= 3.1.1.0)
|
||||
rubocop-performance (1.13.2)
|
||||
rubocop-performance (1.14.2)
|
||||
rubocop (>= 1.7.0, < 2.0)
|
||||
rubocop-ast (>= 0.4.0)
|
||||
rubocop-rails (2.13.2)
|
||||
rubocop-rails (2.15.2)
|
||||
activesupport (>= 4.2.0)
|
||||
rack (>= 1.1)
|
||||
rubocop (>= 1.7.0, < 2.0)
|
||||
rubocop-rspec (2.8.0)
|
||||
rubocop (~> 1.19)
|
||||
rubocop-rspec (2.12.1)
|
||||
rubocop (~> 1.31)
|
||||
ruby-progressbar (1.11.0)
|
||||
ruby-vips (2.1.4)
|
||||
ffi (~> 1.12)
|
||||
@@ -508,7 +549,7 @@ GEM
|
||||
ruby2ruby (2.4.4)
|
||||
ruby_parser (~> 3.1)
|
||||
sexp_processor (~> 4.6)
|
||||
ruby_parser (3.18.1)
|
||||
ruby_parser (3.19.1)
|
||||
sexp_processor (~> 4.16)
|
||||
sassc (2.4.0)
|
||||
ffi (~> 1.9)
|
||||
@@ -518,37 +559,37 @@ GEM
|
||||
sprockets (> 3.0)
|
||||
sprockets-rails
|
||||
tilt
|
||||
scout_apm (5.1.1)
|
||||
scout_apm (5.2.0)
|
||||
parser
|
||||
seed_dump (3.3.1)
|
||||
activerecord (>= 4)
|
||||
activesupport (>= 4)
|
||||
selectize-rails (0.12.6)
|
||||
semantic_range (3.0.0)
|
||||
sentry-rails (5.3.0)
|
||||
sentry-rails (5.3.1)
|
||||
railties (>= 5.0)
|
||||
sentry-ruby-core (~> 5.3.0)
|
||||
sentry-ruby (5.3.0)
|
||||
sentry-ruby-core (~> 5.3.1)
|
||||
sentry-ruby (5.3.1)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
sentry-ruby-core (= 5.3.0)
|
||||
sentry-ruby-core (5.3.0)
|
||||
sentry-ruby-core (= 5.3.1)
|
||||
sentry-ruby-core (5.3.1)
|
||||
concurrent-ruby
|
||||
sentry-sidekiq (5.3.0)
|
||||
sentry-ruby-core (~> 5.3.0)
|
||||
sentry-sidekiq (5.3.1)
|
||||
sentry-ruby-core (~> 5.3.1)
|
||||
sidekiq (>= 3.0)
|
||||
sexp_processor (4.16.0)
|
||||
sexp_processor (4.16.1)
|
||||
shoulda-matchers (5.1.0)
|
||||
activesupport (>= 5.2.0)
|
||||
sidekiq (6.4.1)
|
||||
sidekiq (6.4.2)
|
||||
connection_pool (>= 2.2.2)
|
||||
rack (~> 2.0)
|
||||
redis (>= 4.2.0)
|
||||
sidekiq-cron (1.4.0)
|
||||
sidekiq-cron (1.6.0)
|
||||
fugit (~> 1)
|
||||
sidekiq (>= 4.2.1)
|
||||
signet (0.16.0)
|
||||
signet (0.17.0)
|
||||
addressable (~> 2.8)
|
||||
faraday (>= 0.17.3, < 2.0)
|
||||
faraday (>= 0.17.5, < 3.a)
|
||||
jwt (>= 1.5, < 3.0)
|
||||
multi_json (~> 1.10)
|
||||
simplecov (0.17.1)
|
||||
@@ -566,7 +607,7 @@ GEM
|
||||
spring-watcher-listen (2.0.1)
|
||||
listen (>= 2.7, < 4.0)
|
||||
spring (>= 1.2, < 3.0)
|
||||
sprockets (4.0.3)
|
||||
sprockets (4.1.1)
|
||||
concurrent-ruby (~> 1.0)
|
||||
rack (> 1, < 3)
|
||||
sprockets-rails (3.4.2)
|
||||
@@ -575,31 +616,31 @@ GEM
|
||||
sprockets (>= 3.0.0)
|
||||
squasher (0.6.2)
|
||||
statsd-ruby (1.5.0)
|
||||
telephone_number (1.4.13)
|
||||
telephone_number (1.4.16)
|
||||
thor (1.2.1)
|
||||
tilt (2.0.10)
|
||||
time_diff (0.3.0)
|
||||
activesupport
|
||||
i18n
|
||||
trailblazer-option (0.1.2)
|
||||
twilio-ruby (5.32.0)
|
||||
faraday (~> 1.0.0)
|
||||
twilio-ruby (5.68.0)
|
||||
faraday (>= 0.9, < 3.0)
|
||||
jwt (>= 1.5, <= 2.5)
|
||||
nokogiri (>= 1.6, < 2.0)
|
||||
twitty (0.1.4)
|
||||
oauth
|
||||
tzinfo (2.0.4)
|
||||
concurrent-ruby (~> 1.0)
|
||||
tzinfo-data (1.2021.5)
|
||||
tzinfo-data (1.2022.1)
|
||||
tzinfo (>= 1.0.0)
|
||||
uber (0.1.0)
|
||||
uglifier (4.2.0)
|
||||
execjs (>= 0.3.0, < 3)
|
||||
unf (0.1.4)
|
||||
unf_ext
|
||||
unf_ext (0.0.8)
|
||||
unicode-display_width (2.1.0)
|
||||
uniform_notifier (1.14.2)
|
||||
unf_ext (0.0.8.2)
|
||||
unicode-display_width (2.2.0)
|
||||
uniform_notifier (1.16.0)
|
||||
uri_template (0.7.0)
|
||||
valid_email2 (4.0.3)
|
||||
activemodel (>= 3.2)
|
||||
@@ -631,7 +672,7 @@ GEM
|
||||
working_hours (1.4.1)
|
||||
activesupport (>= 3.2)
|
||||
tzinfo
|
||||
zeitwerk (2.5.4)
|
||||
zeitwerk (2.6.0)
|
||||
|
||||
PLATFORMS
|
||||
arm64-darwin-20
|
||||
@@ -705,7 +746,7 @@ DEPENDENCIES
|
||||
rack-attack
|
||||
rack-cors
|
||||
rack-timeout
|
||||
rails
|
||||
rails (~> 6.1)
|
||||
redis
|
||||
redis-namespace
|
||||
responders
|
||||
@@ -730,7 +771,7 @@ DEPENDENCIES
|
||||
squasher
|
||||
telephone_number
|
||||
time_diff
|
||||
twilio-ruby (~> 5.32.0)
|
||||
twilio-ruby (~> 5.66)
|
||||
twitty
|
||||
tzinfo-data
|
||||
uglifier
|
||||
@@ -746,4 +787,4 @@ RUBY VERSION
|
||||
ruby 3.0.4p208
|
||||
|
||||
BUNDLED WITH
|
||||
2.3.15
|
||||
2.3.16
|
||||
|
||||
@@ -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://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://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>
|
||||
|
||||
<img src="https://chatwoot-public-assets.s3.amazonaws.com/github/screenshot.png" width="100%" alt="Chat dashboard"/>
|
||||
|
||||
1
VERSION_CW
Normal file
1
VERSION_CW
Normal file
@@ -0,0 +1 @@
|
||||
2.6.0
|
||||
1
VERSION_CWCTL
Normal file
1
VERSION_CWCTL
Normal file
@@ -0,0 +1 @@
|
||||
2.1.0
|
||||
@@ -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
|
||||
pattr_initialize [:contact!, :params!]
|
||||
pattr_initialize [:contact!, :params!, { retain_original_contact_name: false, discard_invalid_attrs: false }]
|
||||
|
||||
def perform
|
||||
@attributes_to_update = [:identifier, :name, :email, :phone_number]
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
merge_if_existing_identified_contact
|
||||
merge_if_existing_email_contact
|
||||
@@ -18,49 +27,89 @@ class ContactIdentifyAction
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
def existing_identified_contact
|
||||
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
|
||||
|
||||
def existing_email_contact
|
||||
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
|
||||
|
||||
def existing_phone_number_contact
|
||||
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
|
||||
|
||||
def merge_contacts?(existing_contact, _contact)
|
||||
existing_contact && existing_contact.id != @contact.id
|
||||
def merge_contacts?(existing_contact, key)
|
||||
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
|
||||
|
||||
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
|
||||
# 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|
|
||||
v.blank?
|
||||
end.merge({ custom_attributes: custom_attributes, additional_attributes: additional_attributes }))
|
||||
@contact.discard_invalid_attrs if discard_invalid_attrs
|
||||
@contact.save!
|
||||
ContactAvatarJob.perform_later(@contact, params[:avatar_url]) if params[:avatar_url].present?
|
||||
end
|
||||
|
||||
def merge_contact(base_contact, merge_contact)
|
||||
return base_contact if base_contact.id == merge_contact.id
|
||||
|
||||
ContactMergeAction.new(
|
||||
account: account,
|
||||
base_contact: base_contact,
|
||||
@@ -69,14 +118,14 @@ class ContactIdentifyAction
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
def additional_attributes
|
||||
if params[:additional_attributes]
|
||||
@contact.additional_attributes.deep_merge(params[:additional_attributes].stringify_keys)
|
||||
else
|
||||
@contact.additional_attributes
|
||||
end
|
||||
return @contact.additional_attributes if params[:additional_attributes].blank?
|
||||
|
||||
(@contact.additional_attributes || {}).deep_merge(params[:additional_attributes].stringify_keys)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -9,12 +9,15 @@ class Campaigns::CampaignConversationBuilder
|
||||
@contact_inbox.lock!
|
||||
|
||||
# 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)
|
||||
Messages::MessageBuilder.new(@campaign.sender, @conversation, message_params).perform
|
||||
end
|
||||
@conversation
|
||||
rescue StandardError => e
|
||||
Rails.logger.info(e.message)
|
||||
nil
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -54,6 +54,9 @@ class Messages::Messenger::MessageBuilder
|
||||
def fetch_story_link(attachment)
|
||||
message = attachment.message
|
||||
result = get_story_object_from_source_id(message.source_id)
|
||||
|
||||
return if result.blank?
|
||||
|
||||
story_id = result['story']['mention']['id']
|
||||
story_sender = result['from']['username']
|
||||
message.content_attributes[:story_sender] = story_sender
|
||||
@@ -68,6 +71,11 @@ class Messages::Messenger::MessageBuilder
|
||||
rescue Koala::Facebook::AuthenticationError
|
||||
@inbox.channel.authorization_error!
|
||||
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
|
||||
ChatwootExceptionTracker.new(e, account: @inbox.account).capture_exception
|
||||
{}
|
||||
|
||||
@@ -30,6 +30,6 @@ class Api::V1::Accounts::AgentBotsController < Api::V1::Accounts::BaseController
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:name, :description, :outgoing_url)
|
||||
params.permit(:name, :description, :outgoing_url, :bot_type, bot_config: [:csml_content])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
|
||||
before_action :portal
|
||||
before_action :check_authorization
|
||||
before_action :fetch_article, except: [:index, :create]
|
||||
|
||||
def index
|
||||
@articles = @portal.articles
|
||||
@articles.search(list_params) if params[:payload].present?
|
||||
@articles = @articles.search(list_params) if params[:payload].present?
|
||||
end
|
||||
|
||||
def create
|
||||
@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
|
||||
|
||||
def edit; end
|
||||
@@ -36,7 +40,7 @@ class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
|
||||
|
||||
def article_params
|
||||
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
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
class Api::V1::Accounts::CategoriesController < Api::V1::Accounts::BaseController
|
||||
before_action :portal
|
||||
before_action :check_authorization
|
||||
before_action :fetch_category, except: [:index, :create]
|
||||
|
||||
def index
|
||||
@@ -8,10 +9,20 @@ class Api::V1::Accounts::CategoriesController < Api::V1::Accounts::BaseControlle
|
||||
|
||||
def create
|
||||
@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
|
||||
|
||||
def show; end
|
||||
|
||||
def update
|
||||
@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
|
||||
|
||||
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])
|
||||
end
|
||||
|
||||
def related_categories_records
|
||||
@portal.categories.where(id: params[:category][:related_category_ids])
|
||||
end
|
||||
|
||||
def category_params
|
||||
params.require(:category).permit(
|
||||
:name, :description, :position, :slug, :locale
|
||||
:name, :description, :position, :slug, :locale, :parent_category_id, :associated_category_id
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -27,6 +27,8 @@ class Api::V1::Accounts::Channels::TwilioChannelsController < Api::V1::Accounts:
|
||||
end
|
||||
|
||||
def phone_number
|
||||
return if permitted_params[:phone_number].blank?
|
||||
|
||||
medium == 'sms' ? permitted_params[:phone_number] : "whatsapp:#{permitted_params[:phone_number]}"
|
||||
end
|
||||
|
||||
@@ -38,6 +40,7 @@ class Api::V1::Accounts::Channels::TwilioChannelsController < Api::V1::Accounts:
|
||||
@twilio_channel = Current.account.twilio_sms.create!(
|
||||
account_sid: permitted_params[:account_sid],
|
||||
auth_token: permitted_params[:auth_token],
|
||||
messaging_service_sid: permitted_params[:messaging_service_sid].presence,
|
||||
phone_number: phone_number,
|
||||
medium: medium
|
||||
)
|
||||
@@ -49,7 +52,7 @@ class Api::V1::Accounts::Channels::TwilioChannelsController < Api::V1::Accounts:
|
||||
|
||||
def permitted_params
|
||||
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
|
||||
|
||||
@@ -12,7 +12,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
||||
|
||||
before_action :check_authorization
|
||||
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]
|
||||
|
||||
def index
|
||||
@@ -72,15 +72,17 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
||||
|
||||
def create
|
||||
ActiveRecord::Base.transaction do
|
||||
@contact = Current.account.contacts.new(contact_params)
|
||||
@contact = Current.account.contacts.new(permitted_params.except(:avatar_url))
|
||||
@contact.save!
|
||||
@contact_inbox = build_contact_inbox
|
||||
process_avatar
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
@contact.assign_attributes(contact_update_params)
|
||||
@contact.save!
|
||||
process_avatar if permitted_params[:avatar].present? || permitted_params[:avatar_url].present?
|
||||
end
|
||||
|
||||
def destroy
|
||||
@@ -95,6 +97,11 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
||||
head :ok
|
||||
end
|
||||
|
||||
def avatar
|
||||
@contact.avatar.purge if @contact.avatar.attached?
|
||||
@contact
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# 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)
|
||||
end
|
||||
|
||||
def contact_params
|
||||
params.require(:contact).permit(:name, :identifier, :email, :phone_number, additional_attributes: {}, custom_attributes: {})
|
||||
def permitted_params
|
||||
params.permit(:name, :identifier, :email, :phone_number, :avatar, :avatar_url, additional_attributes: {}, custom_attributes: {})
|
||||
end
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
def contact_update_params
|
||||
# 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
|
||||
|
||||
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])
|
||||
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)
|
||||
render json: error, status: error_status
|
||||
end
|
||||
|
||||
@@ -1,18 +1,36 @@
|
||||
class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
|
||||
include ::FileTypeHelper
|
||||
|
||||
before_action :fetch_portal, except: [:index, :create]
|
||||
before_action :check_authorization
|
||||
|
||||
def index
|
||||
@portals = Current.account.portals
|
||||
end
|
||||
|
||||
def add_members
|
||||
agents = Current.account.agents.where(id: portal_member_params[:member_ids])
|
||||
@portal.members << agents
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
def destroy
|
||||
@@ -20,6 +38,15 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
|
||||
head :ok
|
||||
end
|
||||
|
||||
def archive
|
||||
@portal.update(archive: true)
|
||||
head :ok
|
||||
end
|
||||
|
||||
def process_attached_logo
|
||||
@portal.logo.attach(params[:logo])
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def fetch_portal
|
||||
@@ -32,7 +59,11 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
|
||||
|
||||
def portal_params
|
||||
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
|
||||
|
||||
def portal_member_params
|
||||
params.require(:portal).permit(:account_id, member_ids: [])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -44,38 +44,6 @@ class Api::V1::Widget::BaseController < ApplicationController
|
||||
}
|
||||
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
|
||||
permitted_params.dig(:contact, :email)&.downcase
|
||||
end
|
||||
|
||||
@@ -1,14 +1,27 @@
|
||||
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 update
|
||||
contact_identify_action = ContactIdentifyAction.new(
|
||||
contact: @contact,
|
||||
params: permitted_params.to_h.deep_symbolize_keys
|
||||
)
|
||||
@contact = contact_identify_action.perform
|
||||
identify_contact(@contact)
|
||||
end
|
||||
|
||||
def set_user
|
||||
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
|
||||
|
||||
# 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
|
||||
|
||||
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?
|
||||
|
||||
render json: { error: 'HMAC failed: Invalid Identifier Hash Provided' }, status: :unauthorized unless valid_hmac?
|
||||
|
||||
@contact_inbox.update(hmac_verified: true)
|
||||
end
|
||||
|
||||
def should_verify_hmac?
|
||||
|
||||
@@ -14,8 +14,11 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
||||
end
|
||||
|
||||
def process_update_contact
|
||||
update_contact(contact_email) if @contact.email.blank? && contact_email.present?
|
||||
update_contact_phone_number(contact_phone_number) if @contact.phone_number.blank? && contact_phone_number.present?
|
||||
@contact = ContactIdentifyAction.new(
|
||||
contact: @contact,
|
||||
params: { email: contact_email, phone_number: contact_phone_number, name: contact_name },
|
||||
retain_original_contact_name: true
|
||||
).perform
|
||||
end
|
||||
|
||||
def update_last_seen
|
||||
|
||||
@@ -15,7 +15,10 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
|
||||
def update
|
||||
if @message.content_type == 'input_email'
|
||||
@message.update!(submitted_email: contact_email)
|
||||
update_contact(contact_email)
|
||||
ContactIdentifyAction.new(
|
||||
contact: @contact,
|
||||
params: { email: contact_email }
|
||||
).perform
|
||||
else
|
||||
@message.update!(message_update_params[:message])
|
||||
end
|
||||
|
||||
20
app/controllers/concerns/meta_token_verify_concern.rb
Normal file
20
app/controllers/concerns/meta_token_verify_concern.rb
Normal 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
|
||||
@@ -14,7 +14,7 @@ class Platform::Api::V1::UsersController < PlatformController
|
||||
|
||||
def login
|
||||
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
|
||||
|
||||
def show; end
|
||||
|
||||
25
app/controllers/public/api/v1/portals/articles_controller.rb
Normal file
25
app/controllers/public/api/v1/portals/articles_controller.rb
Normal 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
|
||||
@@ -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
|
||||
11
app/controllers/public/api/v1/portals_controller.rb
Normal file
11
app/controllers/public/api/v1/portals_controller.rb
Normal 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
|
||||
@@ -7,7 +7,7 @@ class Twilio::CallbackController < ApplicationController
|
||||
|
||||
private
|
||||
|
||||
def permitted_params
|
||||
def permitted_params # rubocop:disable Metrics/MethodLength
|
||||
params.permit(
|
||||
:ApiVersion,
|
||||
:SmsSid,
|
||||
@@ -25,7 +25,8 @@ class Twilio::CallbackController < ApplicationController
|
||||
:ToCountry,
|
||||
:FromState,
|
||||
:MediaUrl0,
|
||||
:MediaContentType0
|
||||
:MediaContentType0,
|
||||
:MessagingServiceSid
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,15 +1,5 @@
|
||||
class Webhooks::InstagramController < ApplicationController
|
||||
skip_before_action :authenticate_user!, raise: false
|
||||
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
|
||||
class Webhooks::InstagramController < ActionController::API
|
||||
include MetaTokenVerifyConcern
|
||||
|
||||
def events
|
||||
Rails.logger.info('Instagram webhook received events')
|
||||
@@ -24,7 +14,7 @@ class Webhooks::InstagramController < ApplicationController
|
||||
|
||||
private
|
||||
|
||||
def valid_instagram_token?(token)
|
||||
def valid_token?(token)
|
||||
token == GlobalConfigService.load('IG_VERIFY_TOKEN', '')
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
class Webhooks::WhatsappController < ActionController::API
|
||||
include MetaTokenVerifyConcern
|
||||
|
||||
def process_payload
|
||||
Webhooks::WhatsappEventsJob.perform_later(params.to_unsafe_hash)
|
||||
head :ok
|
||||
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
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# TODO : Delete this and associated spec once 'api/widget/config' end point is merged
|
||||
class WidgetsController < ActionController::Base
|
||||
include WidgetHelper
|
||||
|
||||
before_action :set_global_config
|
||||
before_action :set_web_widget
|
||||
before_action :set_token
|
||||
@@ -40,11 +42,8 @@ class WidgetsController < ActionController::Base
|
||||
def build_contact
|
||||
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
|
||||
|
||||
payload = { source_id: @contact_inbox.source_id, inbox_id: @web_widget.inbox.id }
|
||||
@token = ::Widget::TokenService.new(payload: payload).generate_token
|
||||
end
|
||||
|
||||
def additional_attributes
|
||||
|
||||
@@ -2,6 +2,11 @@ class ConversationFinder
|
||||
attr_reader :current_user, :current_account, :params
|
||||
|
||||
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
|
||||
# inbox_id if not given, take from all conversations, else specific to inbox
|
||||
@@ -133,10 +138,7 @@ class ConversationFinder
|
||||
@conversations = @conversations.includes(
|
||||
:taggings, :inbox, { assignee: { avatar_attachment: [:blob] } }, { contact: { avatar_attachment: [:blob] } }, :team, :contact_inbox
|
||||
)
|
||||
if params[:conversation_type] == 'mention'
|
||||
@conversations.page(current_page)
|
||||
else
|
||||
@conversations.latest.page(current_page)
|
||||
end
|
||||
sort_by = SORT_OPTIONS[params[:sort_by]] || SORT_OPTIONS['latest']
|
||||
@conversations.send(sort_by).page(current_page)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -59,7 +59,7 @@ module Api::V1::InboxesHelper
|
||||
def check_smtp_connection(channel_data, smtp)
|
||||
smtp.start(channel_data[:smtp_domain], channel_data[:smtp_login], channel_data[:smtp_password],
|
||||
channel_data[:smtp_authentication]&.to_sym || :login)
|
||||
smtp.finish unless smtp&.nil?
|
||||
smtp.finish
|
||||
end
|
||||
|
||||
def set_smtp_encryption(channel_data, smtp)
|
||||
|
||||
9
app/helpers/widget_helper.rb
Normal file
9
app/helpers/widget_helper.rb
Normal 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
|
||||
@@ -71,6 +71,10 @@ class ContactAPI extends ApiClient {
|
||||
custom_attributes: customAttributes,
|
||||
});
|
||||
}
|
||||
|
||||
destroyAvatar(contactId) {
|
||||
return axios.delete(`${this.url}/${contactId}/avatar`);
|
||||
}
|
||||
}
|
||||
|
||||
export default new ContactAPI();
|
||||
|
||||
@@ -12,6 +12,7 @@ describe('#ContactsAPI', () => {
|
||||
expect(contactAPI).toHaveProperty('delete');
|
||||
expect(contactAPI).toHaveProperty('getConversations');
|
||||
expect(contactAPI).toHaveProperty('filter');
|
||||
expect(contactAPI).toHaveProperty('destroyAvatar');
|
||||
});
|
||||
|
||||
describeWithAPIMock('API calls', context => {
|
||||
@@ -100,6 +101,13 @@ describe('#ContactsAPI', () => {
|
||||
queryPayload
|
||||
);
|
||||
});
|
||||
|
||||
it('#destroyAvatar', () => {
|
||||
contactAPI.destroyAvatar(1);
|
||||
expect(context.axiosMock.delete).toHaveBeenCalledWith(
|
||||
'/api/v1/contacts/1/avatar'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -383,8 +383,8 @@ $form-button-radius: var(--border-radius-normal);
|
||||
// 20. Label
|
||||
// ---------
|
||||
|
||||
$label-background: $primary-color;
|
||||
$label-color: $white;
|
||||
$label-background: $white;
|
||||
$label-color: $black;
|
||||
$label-color-alt: $black;
|
||||
$label-palette: $foundation-palette;
|
||||
$label-font-size: $font-size-mini;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
.page-sub-title {
|
||||
font-size: $font-size-large;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.block-title {
|
||||
|
||||
@@ -99,3 +99,7 @@ $ionicons-font-path: '~ionicons/fonts';
|
||||
|
||||
// Transitions
|
||||
$transition-ease-in: all 0.250s ease-in;
|
||||
|
||||
:root {
|
||||
--dashboard-app-tabs-height: 3.9rem;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,34 @@
|
||||
.tabs--container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.tabs--container--with-border {
|
||||
@include border-normal-bottom;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
@include padding($zero $space-normal);
|
||||
@include border-normal-bottom;
|
||||
border-left-width: 0;
|
||||
border-right-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
|
||||
@@ -22,6 +47,7 @@
|
||||
|
||||
.tabs-title {
|
||||
@include margin($zero $space-slab);
|
||||
flex-shrink: 0;
|
||||
|
||||
.badge {
|
||||
background: $color-background;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<h2 class="page-sub-title">
|
||||
{{ headerTitle }}
|
||||
</h2>
|
||||
<p v-if="headerContent" class="small-12 column">
|
||||
<p v-if="headerContent" class="small-12 column wrap-content">
|
||||
{{ headerContent }}
|
||||
</p>
|
||||
<slot />
|
||||
@@ -29,3 +29,8 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.wrap-content {
|
||||
word-wrap: break-word;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
129
app/javascript/dashboard/components/helpCenter/ArticleItem.vue
Normal file
129
app/javascript/dashboard/components/helpCenter/ArticleItem.vue
Normal 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>
|
||||
@@ -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'),
|
||||
};
|
||||
@@ -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>
|
||||
@@ -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'),
|
||||
};
|
||||
@@ -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>
|
||||
@@ -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'),
|
||||
};
|
||||
@@ -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>
|
||||
@@ -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',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -11,6 +11,7 @@
|
||||
@open-notification-panel="openNotificationPanel"
|
||||
/>
|
||||
<secondary-sidebar
|
||||
v-if="showSecondarySidebar"
|
||||
:account-id="accountId"
|
||||
:inboxes="inboxes"
|
||||
:labels="labels"
|
||||
@@ -50,6 +51,12 @@ export default {
|
||||
SecondarySidebar,
|
||||
},
|
||||
mixins: [adminMixin, alertMixin, eventListenerMixins],
|
||||
props: {
|
||||
showSecondarySidebar: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showOptionsMenu: false,
|
||||
@@ -191,6 +198,7 @@ export default {
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
width: fit-content;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ const settings = accountId => ({
|
||||
'settings_integrations_webhook',
|
||||
'settings_integrations_integration',
|
||||
'settings_applications',
|
||||
'settings_integrations_dashboard_apps',
|
||||
'settings_applications_webhook',
|
||||
'settings_applications_integration',
|
||||
'general_settings',
|
||||
|
||||
@@ -83,7 +83,7 @@ export default {
|
||||
border-bottom-right-radius: var(--border-radius-normal);
|
||||
display: flex;
|
||||
height: 100%;
|
||||
justify-content: end;
|
||||
justify-content: flex-end;
|
||||
opacity: 1;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
|
||||
@@ -26,6 +26,9 @@
|
||||
:class="{ 'text-truncate': shouldTruncate }"
|
||||
>
|
||||
{{ label }}
|
||||
<span v-if="isHelpCenterSidebar && childItemCount" class="count-view">
|
||||
{{ childItemCount }}
|
||||
</span>
|
||||
</span>
|
||||
<span v-if="count" class="badge" :class="{ secondary: !isActive }">
|
||||
{{ count }}
|
||||
@@ -73,6 +76,14 @@ export default {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
isHelpCenterSidebar: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
childItemCount: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
showIcon() {
|
||||
@@ -146,6 +157,7 @@ $label-badge-size: var(--space-slab);
|
||||
height: $label-badge-size;
|
||||
min-width: $label-badge-size;
|
||||
margin-left: var(--space-smaller);
|
||||
border: 1px solid var(--color-border-light);
|
||||
}
|
||||
|
||||
.badge.secondary {
|
||||
@@ -154,4 +166,19 @@ $label-badge-size: var(--space-slab);
|
||||
color: var(--s-600);
|
||||
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>
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
<template>
|
||||
<li class="sidebar-item">
|
||||
<span v-if="hasSubMenu" class="secondary-menu--title fs-small">
|
||||
{{ $t(`SIDEBAR.${menuItem.label}`) }}
|
||||
</span>
|
||||
<div v-if="hasSubMenu" class="secondary-menu--wrap">
|
||||
<span class="secondary-menu--title fs-small">
|
||||
{{ $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
|
||||
v-else
|
||||
class="secondary-menu--title secondary-menu--link fs-small"
|
||||
@@ -15,6 +21,13 @@
|
||||
size="14"
|
||||
/>
|
||||
{{ $t(`SIDEBAR.${menuItem.label}`) }}
|
||||
<span
|
||||
v-if="isHelpCenterSidebar"
|
||||
class="count-view"
|
||||
:class="computedClass"
|
||||
>
|
||||
{{ `${menuItem.count}` }}
|
||||
</span>
|
||||
<span
|
||||
v-if="menuItem.label === 'AUTOMATION'"
|
||||
data-view-component="true"
|
||||
@@ -35,6 +48,8 @@
|
||||
:should-truncate="child.truncateLabel"
|
||||
:icon="computedInboxClass(child)"
|
||||
:warning-icon="computedInboxErrorClass(child)"
|
||||
:is-help-center-sidebar="isHelpCenterSidebar"
|
||||
:child-item-count="child.count"
|
||||
/>
|
||||
<router-link
|
||||
v-if="showItem(menuItem)"
|
||||
@@ -79,6 +94,10 @@ export default {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
isHelpCenterSidebar: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({ activeInbox: 'getSelectedInbox' }),
|
||||
@@ -120,17 +139,19 @@ export default {
|
||||
// If active Inbox is present
|
||||
// donot highlight conversations
|
||||
if (this.activeInbox) return ' ';
|
||||
|
||||
if (
|
||||
this.isInboxConversation ||
|
||||
this.isTeamsSettings ||
|
||||
this.isInboxsSettings ||
|
||||
this.isIntegrationsSettings ||
|
||||
this.isApplicationsSettings
|
||||
) {
|
||||
return 'is-active';
|
||||
if (this.hasSubMenu) {
|
||||
if (
|
||||
this.isInboxConversation ||
|
||||
this.isTeamsSettings ||
|
||||
this.isInboxsSettings ||
|
||||
this.isIntegrationsSettings ||
|
||||
this.isApplicationsSettings
|
||||
) {
|
||||
return 'is-active';
|
||||
}
|
||||
return ' ';
|
||||
}
|
||||
return ' ';
|
||||
return '';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
@@ -170,6 +191,11 @@ export default {
|
||||
margin: var(--space-smaller) 0 0;
|
||||
}
|
||||
|
||||
.secondary-menu--wrap {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.secondary-menu--title {
|
||||
color: var(--s-600);
|
||||
display: flex;
|
||||
@@ -242,6 +268,7 @@ export default {
|
||||
color: var(--w-500);
|
||||
}
|
||||
}
|
||||
|
||||
.beta {
|
||||
padding-right: var(--space-smaller) !important;
|
||||
padding-left: var(--space-smaller) !important;
|
||||
@@ -255,4 +282,34 @@ export default {
|
||||
color: var(--g-800);
|
||||
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>
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
<template>
|
||||
<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" />
|
||||
</button>
|
||||
</span>
|
||||
<span
|
||||
v-if="variant === 'smooth'"
|
||||
:style="{ background: color }"
|
||||
class="label-color-dot"
|
||||
/>
|
||||
<span v-if="!href">{{ title }}</span>
|
||||
<a v-else :href="href" :style="anchorStyle">{{ title }}</a>
|
||||
<button
|
||||
v-if="showClose"
|
||||
class="label-action--button"
|
||||
class="label-close--button "
|
||||
:style="{ color: textColor }"
|
||||
@click="onClick"
|
||||
>
|
||||
@@ -48,14 +53,23 @@ export default {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
colorScheme: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
variant: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
textColor() {
|
||||
return getContrastingTextColor(this.bgColor);
|
||||
if (this.variant === 'smooth') return '';
|
||||
return this.color || getContrastingTextColor(this.bgColor);
|
||||
},
|
||||
labelClass() {
|
||||
return `label ${this.colorScheme} ${this.small ? 'small' : ''}`;
|
||||
@@ -94,9 +108,17 @@ export default {
|
||||
font-weight: var(--font-weight-medium);
|
||||
margin-right: 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 {
|
||||
font-size: var(--font-size-micro);
|
||||
padding: var(--space-micro) var(--space-smaller);
|
||||
line-height: 1.2;
|
||||
letter-spacing: 0.15px;
|
||||
}
|
||||
|
||||
.label--icon {
|
||||
@@ -104,11 +126,6 @@ export default {
|
||||
margin-right: var(--space-smaller);
|
||||
}
|
||||
|
||||
.close--icon {
|
||||
cursor: pointer;
|
||||
margin-left: var(--space-smaller);
|
||||
}
|
||||
|
||||
&.small .label--icon,
|
||||
&.small .close--icon {
|
||||
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 {
|
||||
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>
|
||||
|
||||
@@ -5,8 +5,61 @@ export default {
|
||||
type: Number,
|
||||
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
|
||||
.filter(
|
||||
node =>
|
||||
@@ -18,14 +71,21 @@ export default {
|
||||
data.index = index;
|
||||
return node;
|
||||
});
|
||||
const leftButton = this.createScrollButton(createElement, 'left');
|
||||
const rightButton = this.createScrollButton(createElement, 'right');
|
||||
return (
|
||||
<ul
|
||||
<div
|
||||
class={{
|
||||
tabs: true,
|
||||
'tabs--container--with-border': this.border,
|
||||
'tabs--container': true,
|
||||
}}
|
||||
>
|
||||
{Tabs}
|
||||
</ul>
|
||||
{leftButton}
|
||||
<ul class={{ tabs: true, 'tabs--with-scroll': this.hasScroll }}>
|
||||
{Tabs}
|
||||
</ul>
|
||||
{rightButton}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<button
|
||||
class="button"
|
||||
:type="type"
|
||||
:class="buttonClasses"
|
||||
:disabled="isDisabled || isLoading"
|
||||
@click="handleClick"
|
||||
@@ -24,6 +25,10 @@ export default {
|
||||
name: 'WootButton',
|
||||
components: { EmojiOrIcon, Spinner },
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
default: 'submit',
|
||||
},
|
||||
variant: {
|
||||
type: String,
|
||||
default: '',
|
||||
|
||||
@@ -40,13 +40,18 @@ export default {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
variant: {
|
||||
type: String,
|
||||
default: 'circle',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
style() {
|
||||
let style = {
|
||||
width: `${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`,
|
||||
fontSize: `${Math.floor(this.size / 2.5)}px`,
|
||||
};
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
:title="label.title"
|
||||
:description="label.description"
|
||||
:show-close="true"
|
||||
:bg-color="label.color"
|
||||
:color="label.color"
|
||||
variant="smooth"
|
||||
@click="removeItem"
|
||||
/>
|
||||
<div class="dropdown-wrap">
|
||||
|
||||
@@ -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({});
|
||||
@@ -12,6 +12,7 @@
|
||||
:username="username"
|
||||
:class="thumbnailClass"
|
||||
:size="avatarSize"
|
||||
:variant="variant"
|
||||
/>
|
||||
<img
|
||||
v-if="badge === 'instagram_direct_message'"
|
||||
@@ -119,6 +120,10 @@ export default {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
variant: {
|
||||
type: String,
|
||||
default: 'circle',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -145,7 +150,9 @@ export default {
|
||||
},
|
||||
thumbnailClass() {
|
||||
const classname = this.hasBorder ? 'border' : '';
|
||||
return `user-thumbnail ${classname}`;
|
||||
const variant =
|
||||
this.variant === 'circle' ? 'thumbnail-rounded' : 'thumbnail-square';
|
||||
return `user-thumbnail ${classname} ${variant}`;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
@@ -173,6 +180,9 @@ export default {
|
||||
|
||||
.user-thumbnail {
|
||||
border-radius: 50%;
|
||||
&.thumbnail-square {
|
||||
border-radius: var(--border-radius-large);
|
||||
}
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
@@ -141,6 +141,7 @@ export default {
|
||||
.dashboard-app--tabs {
|
||||
background: var(--white);
|
||||
margin-top: -1px;
|
||||
min-height: var(--dashboard-app-tabs-height);
|
||||
}
|
||||
|
||||
.messages-and-sidebar {
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
<spinner v-if="isPending" size="tiny" />
|
||||
<div
|
||||
v-if="showAvatar"
|
||||
v-tooltip.top="tooltipForSender"
|
||||
v-tooltip.left="tooltipForSender"
|
||||
class="sender--info"
|
||||
>
|
||||
<woot-thumbnail
|
||||
@@ -313,7 +313,6 @@ export default {
|
||||
return showTooltip
|
||||
? {
|
||||
content: `${this.$t('CONVERSATION.SENT_BY')} ${name}`,
|
||||
classes: 'top',
|
||||
}
|
||||
: false;
|
||||
},
|
||||
|
||||
@@ -22,7 +22,10 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div v-if="uiFlags.isUpdating" class="agent__list-loading">
|
||||
<div
|
||||
v-if="assignableAgentsUiFlags.isFetching"
|
||||
class="agent__list-loading"
|
||||
>
|
||||
<spinner />
|
||||
<p>{{ $t('BULK_ACTION.AGENT_LIST_LOADING') }}</p>
|
||||
</div>
|
||||
@@ -43,7 +46,8 @@
|
||||
<li v-for="agent in filteredAgents" :key="agent.id">
|
||||
<div class="agent-list-item" @click="assignAgent(agent)">
|
||||
<thumbnail
|
||||
src="agent.thumbnail"
|
||||
:src="agent.thumbnail"
|
||||
:status="agent.availability_status"
|
||||
:username="agent.name"
|
||||
size="22px"
|
||||
class="margin-right-small"
|
||||
@@ -171,7 +175,7 @@ export default {
|
||||
transform-origin: top right;
|
||||
width: auto;
|
||||
z-index: var(--z-index-twenty);
|
||||
|
||||
min-width: var(--space-giga);
|
||||
.header {
|
||||
padding: var(--space-one);
|
||||
|
||||
@@ -182,7 +186,7 @@ export default {
|
||||
}
|
||||
|
||||
.container {
|
||||
max-height: 24rem;
|
||||
max-height: var(--space-giga);
|
||||
overflow-y: auto;
|
||||
.agent__list-container {
|
||||
height: 100%;
|
||||
@@ -264,5 +268,6 @@ ul {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
padding: var(--space-two);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
:value="label.title"
|
||||
class="label-checkbox"
|
||||
/>
|
||||
<span class="label-title">{{ label.title }}</span>
|
||||
<span class="label-title text-truncate">{{ label.title }}</span>
|
||||
<span
|
||||
class="label-pill"
|
||||
:style="{ backgroundColor: label.color }"
|
||||
@@ -110,7 +110,7 @@ export default {
|
||||
.labels-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 24rem;
|
||||
max-height: var(--space-giga);
|
||||
min-height: auto;
|
||||
|
||||
.labels-list__header {
|
||||
@@ -157,8 +157,8 @@ export default {
|
||||
border-radius: var(--border-radius-large);
|
||||
border: 1px solid var(--s-50);
|
||||
box-shadow: var(--shadow-dropdown-pane);
|
||||
max-width: 24rem;
|
||||
min-width: 24rem;
|
||||
max-width: var(--space-giga);
|
||||
min-width: var(--space-giga);
|
||||
position: absolute;
|
||||
right: 4.5rem;
|
||||
top: var(--space-larger);
|
||||
@@ -176,7 +176,7 @@ export default {
|
||||
}
|
||||
|
||||
.container {
|
||||
max-height: 24rem;
|
||||
max-height: var(--space-giga);
|
||||
overflow-y: auto;
|
||||
|
||||
.label__list-container {
|
||||
@@ -204,7 +204,7 @@ export default {
|
||||
.triangle {
|
||||
display: block;
|
||||
position: absolute;
|
||||
right: 2rem;
|
||||
right: var(--space-two);
|
||||
text-align: left;
|
||||
top: calc(var(--space-slab) * -1);
|
||||
z-index: var(--z-index-one);
|
||||
@@ -249,6 +249,7 @@ ul {
|
||||
|
||||
.label-title {
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.label-pill {
|
||||
@@ -256,6 +257,8 @@ ul {
|
||||
border-radius: var(--border-radius-medium);
|
||||
height: var(--space-slab);
|
||||
width: var(--space-slab);
|
||||
flex-shrink: 0;
|
||||
border: 1px solid var(--color-border-light);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,18 @@
|
||||
<label>
|
||||
<span v-if="label">{{ label }}</span>
|
||||
</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">
|
||||
<woot-button
|
||||
color-scheme="alert"
|
||||
variant="hollow"
|
||||
size="tiny"
|
||||
type="button"
|
||||
@click="onAvatarDelete"
|
||||
>
|
||||
{{ this.$t('INBOX_MGMT.DELETE.AVATAR_DELETE_BUTTON_TEXT') }}
|
||||
@@ -38,6 +44,10 @@ export default {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
usernameAvatar: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
deleteAvatar: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
@@ -50,7 +60,7 @@ export default {
|
||||
|
||||
this.$emit('change', {
|
||||
file,
|
||||
url: URL.createObjectURL(file),
|
||||
url: file ? URL.createObjectURL(file) : null,
|
||||
});
|
||||
},
|
||||
onAvatarDelete() {
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
<modal :show.sync="show" :on-close="onClose">
|
||||
<woot-modal-header :header-title="title" :header-content="message" />
|
||||
<div class="modal-footer delete-item">
|
||||
<button class="alert button nice" @click="onConfirm">
|
||||
<button class="alert button nice text-truncate" @click="onConfirm">
|
||||
{{ confirmText }}
|
||||
</button>
|
||||
<button class="button clear" @click="onClose">
|
||||
<button class="button clear text-truncate" @click="onClose">
|
||||
{{ rejectText }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -12,7 +12,7 @@ export const getInboxClassByType = (type, phoneNumber) => {
|
||||
return 'brand-twitter';
|
||||
|
||||
case INBOX_TYPES.TWILIO:
|
||||
return phoneNumber.startsWith('whatsapp')
|
||||
return phoneNumber?.startsWith('whatsapp')
|
||||
? 'brand-whatsapp'
|
||||
: 'brand-sms';
|
||||
|
||||
|
||||
@@ -1,17 +1,29 @@
|
||||
{
|
||||
"BULK_ACTION": {
|
||||
"CONVERSATIONS_SELECTED": "%{conversationCount} المحادثات المحددة",
|
||||
"AGENT_SELECT_LABEL": "اختر وكيل",
|
||||
"ASSIGN_CONFIRMATION_LABEL": "هل أنت متأكد من أنك تريد تعيين %{conversationCount} %{conversationLabel} إلى",
|
||||
"GO_BACK_LABEL": "العودة للخلف",
|
||||
"ASSIGN_LABEL": "تكليف",
|
||||
"ASSIGN_AGENT_TOOLTIP": "إسناد وكيل",
|
||||
"RESOLVE_TOOLTIP": "إغلاق المحادثة",
|
||||
"ASSIGN_SUCCESFUL": "تم تعيين المحادثات بنجاح",
|
||||
"ASSIGN_FAILED": "فشل في تعيين المحادثات، الرجاء المحاولة مرة أخرى",
|
||||
"RESOLVE_SUCCESFUL": "تم تسوية المحادثات بنجاح",
|
||||
"RESOLVE_FAILED": "فشل في حل المحادثات، يرجى المحاولة مرة أخرى",
|
||||
"ALL_CONVERSATIONS_SELECTED_ALERT": "المحادثات المرئية في هذه الصفحة هي المحددة فقط.",
|
||||
"AGENT_LIST_LOADING": "تحميل الوكلاء"
|
||||
"BULK_ACTION": {
|
||||
"CONVERSATIONS_SELECTED": "%{conversationCount} المحادثات المحددة",
|
||||
"AGENT_SELECT_LABEL": "اختر وكيل",
|
||||
"ASSIGN_CONFIRMATION_LABEL": "هل أنت متأكد من أنك تريد تعيين %{conversationCount} %{conversationLabel} إلى",
|
||||
"GO_BACK_LABEL": "العودة للخلف",
|
||||
"ASSIGN_LABEL": "تكليف",
|
||||
"ASSIGN_AGENT_TOOLTIP": "إسناد وكيل",
|
||||
"ASSIGN_SUCCESFUL": "تم تعيين المحادثات بنجاح",
|
||||
"ASSIGN_FAILED": "فشل في تعيين المحادثات، الرجاء المحاولة مرة أخرى",
|
||||
"RESOLVE_SUCCESFUL": "تم تسوية المحادثات بنجاح",
|
||||
"RESOLVE_FAILED": "فشل في حل المحادثات، يرجى المحاولة مرة أخرى",
|
||||
"ALL_CONVERSATIONS_SELECTED_ALERT": "المحادثات المرئية في هذه الصفحة هي المحددة فقط.",
|
||||
"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": "فشل في تعيين التسميات ، الرجاء المحاولة مرة أخرى"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,7 +111,8 @@
|
||||
"EMAIL_ADDRESS": {
|
||||
"PLACEHOLDER": "أدخل عنوان البريد الإلكتروني الخاص بجهة الاتصال",
|
||||
"LABEL": "عنوان البريد الإلكتروني",
|
||||
"DUPLICATE": "عنوان البريد الإلكتروني هذا مستخدم لجهة اتصال أخرى."
|
||||
"DUPLICATE": "عنوان البريد الإلكتروني هذا مستخدم لجهة اتصال أخرى.",
|
||||
"ERROR": "الرجاء إدخال عنوان بريد إلكتروني صحيح."
|
||||
},
|
||||
"PHONE_NUMBER": {
|
||||
"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": "تم حفظ جهة الاتصال بنجاح",
|
||||
"ERROR_MESSAGE": "حدث خطأ، الرجاء المحاولة مرة أخرى"
|
||||
},
|
||||
|
||||
@@ -38,7 +38,8 @@
|
||||
"CUSTOM_ATTRIBUTE_LINK": "الرابط",
|
||||
"CUSTOM_ATTRIBUTE_CHECKBOX": "مربع",
|
||||
"CREATED_AT": "تم إنشاؤها في",
|
||||
"LAST_ACTIVITY": "آخر نشاط"
|
||||
"LAST_ACTIVITY": "آخر نشاط",
|
||||
"REFERER_LINK": "Referrer link"
|
||||
},
|
||||
"GROUPS": {
|
||||
"STANDARD_FILTERS": "الفلاتر القياسية",
|
||||
|
||||
24
app/javascript/dashboard/i18n/locale/ar/helpCenter.json
Normal file
24
app/javascript/dashboard/i18n/locale/ar/helpCenter.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,8 @@
|
||||
"ADD": {
|
||||
"CHANNEL_NAME": {
|
||||
"LABEL": "اسم صندوق الوارد لقناة التواصل",
|
||||
"PLACEHOLDER": "أدخل اسم صندوق الوارد الخاص بك (مثال: Acme Inc)"
|
||||
"PLACEHOLDER": "أدخل اسم صندوق الوارد الخاص بك (مثال: Acme Inc)",
|
||||
"ERROR": "الرجاء إدخال اسم صندوق الوارد صالح"
|
||||
},
|
||||
"WEBSITE_NAME": {
|
||||
"LABEL": "اسم الموقع",
|
||||
@@ -82,7 +83,7 @@
|
||||
},
|
||||
"CHANNEL_GREETING_TOGGLE": {
|
||||
"LABEL": "تفعيل رسالة الترحيب التلقائية",
|
||||
"HELP_TEXT": "إرسال رسالة ترحيب إلى المستخدم عند بدء المحادثة.",
|
||||
"HELP_TEXT": "إرسال رسالة تحية تلقائياً عند إنشاء محادثة جديدة.",
|
||||
"ENABLED": "مفعل",
|
||||
"DISABLED": "معطّل"
|
||||
},
|
||||
@@ -97,7 +98,10 @@
|
||||
"LABEL": "لون صندوق الدردشة",
|
||||
"PLACEHOLDER": "تحديث اللون الرئيسي لصندوق الدردشة"
|
||||
},
|
||||
"SUBMIT_BUTTON": "إنشاء قناة تواصل"
|
||||
"SUBMIT_BUTTON": "إنشاء قناة تواصل",
|
||||
"API": {
|
||||
"ERROR_MESSAGE": "لم نتمكن من إنشاء قناة موقع، الرجاء المحاولة مرة أخرى"
|
||||
}
|
||||
},
|
||||
"TWILIO": {
|
||||
"TITLE": "قناة Twilio SMS/WhatsApp",
|
||||
@@ -107,6 +111,12 @@
|
||||
"PLACEHOLDER": "الرجاء إدخال معرف حساب Twilio الخاص بك (يعرف أيضاً بـ Account SID)",
|
||||
"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": {
|
||||
"LABEL": "نوع القناة",
|
||||
"ERROR": "الرجاء تحديد نوع القناة"
|
||||
@@ -193,6 +203,7 @@
|
||||
"PROVIDERS": {
|
||||
"LABEL": "API Provider",
|
||||
"TWILIO": "تويليو",
|
||||
"WHATSAPP_CLOUD": "واتساب السحابة",
|
||||
"360_DIALOG": "360dialog"
|
||||
},
|
||||
"INBOX_NAME": {
|
||||
@@ -205,13 +216,31 @@
|
||||
"PLACEHOLDER": "الرجاء إدخال رقم الهاتف الذي سيتم إرسال الرسائل منه.",
|
||||
"ERROR": "الرجاء إدخال قيمة صحيحة. يجب أن يبدأ رقم الهاتف بعلامة `+`."
|
||||
},
|
||||
"PHONE_NUMBER_ID": {
|
||||
"LABEL": "رقم الهاتف",
|
||||
"PLACEHOLDER": "الرجاء إدخال رقم الهاتف الذي تم الحصول عليه من لوحة تحكم مطور الفيسبوك.",
|
||||
"ERROR": "الرجاء إدخال اسم صالح."
|
||||
},
|
||||
"BUSINESS_ACCOUNT_ID": {
|
||||
"LABEL": "معرف حساب الأعمال",
|
||||
"PLACEHOLDER": "الرجاء إدخال معرف حساب الأعمال الذي تم الحصول عليه من لوحة تحكم مطور الفيسبوك.",
|
||||
"ERROR": "الرجاء إدخال اسم صالح."
|
||||
},
|
||||
"WEBHOOK_VERIFY_TOKEN": {
|
||||
"LABEL": "رمز التحقق من Webhook",
|
||||
"PLACEHOLDER": "أدخل رمز التحقق الذي تريد إعداده لفيسبوك على شبكة الويب.",
|
||||
"ERROR": "الرجاء إدخال اسم صالح."
|
||||
},
|
||||
"API_KEY": {
|
||||
"LABEL": "مفتاح API",
|
||||
"SUBTITLE": "تكوين مفتاح واتسآب API.",
|
||||
"PLACEHOLDER": "مفتاح API",
|
||||
"APPLY_FOR_ACCESS": "ليس لديك أي مفتاح API؟ تقدم بطلب الوصول إلى هنا",
|
||||
"ERROR": "الرجاء إدخال اسم صالح."
|
||||
},
|
||||
"API_CALLBACK": {
|
||||
"TITLE": "عنوان Callback URL",
|
||||
"SUBTITLE": "يجب عليك تكوين URL webhook في بوابة مطور فيسبوك مع عنوان URL المذكور هنا."
|
||||
},
|
||||
"SUBMIT_BUTTON": "إنشاء قناة واتساب",
|
||||
"API": {
|
||||
"ERROR_MESSAGE": "لم نتمكن من حفظ قناة واتساب"
|
||||
@@ -298,7 +327,7 @@
|
||||
},
|
||||
"AUTH": {
|
||||
"TITLE": "اختر قناة",
|
||||
"DESC": "ندعم أداة الدردشة المباشرة، صفحة الفيسبوك، ملف تويتر الشخصي، واتسب، البريد الإلكتروني وما إلى ذلك، كقنوات. إذا كنت ترغب في إنشاء قناة مخصصة، يمكنك إنشاءها باستخدام قناة API. حدد قناة واحدة من الخيارات أدناه للمتابعة."
|
||||
"DESC": "يدعم أدوات الدردشة الحية، والميسنجر الفيسبوك، وملفات التويتر الشخصية، و WhatsApp، ورسائل البريد الإلكتروني، إلخ، كقنوات. إذا كنت ترغب في إنشاء قناة مخصصة، يمكنك إنشاءها باستخدام قناة API. للبدء، اختر إحدى القنوات أدناه."
|
||||
},
|
||||
"AGENTS": {
|
||||
"TITLE": "موظف الدعم",
|
||||
@@ -339,7 +368,7 @@
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "تم تحديث إعدادات قناة التواصل بنجاح",
|
||||
"AUTO_ASSIGNMENT_SUCCESS_MESSAGE": "تم تحديث إعدادات الإسناد التلقائي بنجاح",
|
||||
"ERROR_MESSAGE": "تعذر تحديث لون صندوق الدردشة. الرجاء المحاولة مرة أخرى لاحقاً."
|
||||
"ERROR_MESSAGE": "تعذر تحديث إعدادات صندوق الوارد. الرجاء المحاولة مرة أخرى لاحقاً."
|
||||
},
|
||||
"EMAIL_COLLECT_BOX": {
|
||||
"ENABLED": "مفعل",
|
||||
@@ -412,15 +441,22 @@
|
||||
"INBOX_UPDATE_SUB_TEXT": "تحديث إعدادات قناة التواصل",
|
||||
"AUTO_ASSIGNMENT_SUB_TEXT": "تمكين أو تعطيل الإسناد التلقائي للمحادثات الجديدة إلى الموظفين المضافين إلى قناة التواصل هذه.",
|
||||
"HMAC_VERIFICATION": "التحقق من هوية المستخدم",
|
||||
"HMAC_DESCRIPTION": "من أجل التحقق من هوية المستخدم، يسمح لك SDK بتمرير 'المعرفة_hash' لكل مستخدم. يمكنك إنشاء HMAC باستخدام 'sha256' مع المفتاح المعروض هنا.",
|
||||
"HMAC_DESCRIPTION": "من أجل التحقق من هوية المستخدم، يمكنك تمرير 'IDer_hash` لكل مستخدم. يمكنك إنشاء تجزئة HMAC sha256 باستخدام \"المعرف\" مع المفتاح المعروض هنا.",
|
||||
"HMAC_MANDATORY_VERIFICATION": "فرض التحقق من هوية المستخدم",
|
||||
"HMAC_MANDATORY_DESCRIPTION": "في حالة التمكين، لن تعمل طريقة حساب SDKs إلا إذا تم توفير `IDer_hash` لكل مستخدم.",
|
||||
"HMAC_MANDATORY_DESCRIPTION": "في حالة التمكين، سيتم رفض الطلبات المفقودة 'IDer_hash'.",
|
||||
"INBOX_IDENTIFIER": "معرف صندوق الوارد",
|
||||
"INBOX_IDENTIFIER_SUB_TEXT": "استخدم رمز 'inbox_identifier' المعروض هنا للمصادقة على عملاء API الخاص بك.",
|
||||
"FORWARD_EMAIL_TITLE": "إعادة التوجيه إلى البريد الإلكتروني",
|
||||
"FORWARD_EMAIL_SUB_TEXT": "بدء إعادة توجيه رسائل البريد الإلكتروني الخاصة بك إلى عنوان البريد الإلكتروني التالي.",
|
||||
"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": {
|
||||
"TITLE": "إعادة التصريح",
|
||||
|
||||
@@ -84,6 +84,52 @@
|
||||
},
|
||||
"CONNECT": {
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,6 +65,7 @@
|
||||
"PLACEHOLDER": "اختر نطاق المدة"
|
||||
},
|
||||
"GROUP_BY_FILTER_DROPDOWN_LABEL": "تجميع بواسطة",
|
||||
"DURATION_FILTER_LABEL": "المدة",
|
||||
"GROUP_BY_DAY_OPTIONS": [
|
||||
{
|
||||
"id": 1,
|
||||
|
||||
@@ -1,17 +1,29 @@
|
||||
{
|
||||
"BULK_ACTION": {
|
||||
"CONVERSATIONS_SELECTED": "%{conversationCount} conversations selected",
|
||||
"AGENT_SELECT_LABEL": "Select Agent",
|
||||
"ASSIGN_CONFIRMATION_LABEL": "Are you sure you want to assign %{conversationCount} %{conversationLabel} to",
|
||||
"GO_BACK_LABEL": "Go back",
|
||||
"ASSIGN_LABEL": "Assign",
|
||||
"ASSIGN_AGENT_TOOLTIP": "Assign Agent",
|
||||
"RESOLVE_TOOLTIP": "Resolve",
|
||||
"ASSIGN_SUCCESFUL": "Conversations assigned successfully",
|
||||
"ASSIGN_FAILED": "Failed to assign conversations, please try again",
|
||||
"RESOLVE_SUCCESFUL": "Conversations resolved successfully",
|
||||
"RESOLVE_FAILED": "Failed to resolve conversations, please try again",
|
||||
"ALL_CONVERSATIONS_SELECTED_ALERT": "Conversations visible on this page are only selected.",
|
||||
"AGENT_LIST_LOADING": "Loading Agents"
|
||||
"BULK_ACTION": {
|
||||
"CONVERSATIONS_SELECTED": "%{conversationCount} conversations selected",
|
||||
"AGENT_SELECT_LABEL": "Select Agent",
|
||||
"ASSIGN_CONFIRMATION_LABEL": "Are you sure you want to assign %{conversationCount} %{conversationLabel} to",
|
||||
"GO_BACK_LABEL": "Go back",
|
||||
"ASSIGN_LABEL": "Assign",
|
||||
"ASSIGN_AGENT_TOOLTIP": "Assign Agent",
|
||||
"ASSIGN_SUCCESFUL": "Conversations assigned successfully",
|
||||
"ASSIGN_FAILED": "Failed to assign conversations, please try again",
|
||||
"RESOLVE_SUCCESFUL": "Conversations resolved successfully",
|
||||
"RESOLVE_FAILED": "Failed to resolve conversations, please try again",
|
||||
"ALL_CONVERSATIONS_SELECTED_ALERT": "Conversations visible on this page are only selected.",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,7 +111,8 @@
|
||||
"EMAIL_ADDRESS": {
|
||||
"PLACEHOLDER": "Добавете имейл на контакта",
|
||||
"LABEL": "Имейл адрес",
|
||||
"DUPLICATE": "Този имейл се използва от друг контакт."
|
||||
"DUPLICATE": "Този имейл се използва от друг контакт.",
|
||||
"ERROR": "Please enter a valid email address."
|
||||
},
|
||||
"PHONE_NUMBER": {
|
||||
"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": "Успешно запазване на контакта",
|
||||
"ERROR_MESSAGE": "Възникна грешка, моля опитайте отново"
|
||||
},
|
||||
|
||||
@@ -38,7 +38,8 @@
|
||||
"CUSTOM_ATTRIBUTE_LINK": "Link",
|
||||
"CUSTOM_ATTRIBUTE_CHECKBOX": "Checkbox",
|
||||
"CREATED_AT": "Created At",
|
||||
"LAST_ACTIVITY": "Последна активност"
|
||||
"LAST_ACTIVITY": "Последна активност",
|
||||
"REFERER_LINK": "Referrer link"
|
||||
},
|
||||
"GROUPS": {
|
||||
"STANDARD_FILTERS": "Standard Filters",
|
||||
|
||||
24
app/javascript/dashboard/i18n/locale/bg/helpCenter.json
Normal file
24
app/javascript/dashboard/i18n/locale/bg/helpCenter.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,8 @@
|
||||
"ADD": {
|
||||
"CHANNEL_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": {
|
||||
"LABEL": "Website Name",
|
||||
@@ -82,7 +83,7 @@
|
||||
},
|
||||
"CHANNEL_GREETING_TOGGLE": {
|
||||
"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",
|
||||
"DISABLED": "Disabled"
|
||||
},
|
||||
@@ -97,7 +98,10 @@
|
||||
"LABEL": "Widget Color",
|
||||
"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": {
|
||||
"TITLE": "Twilio SMS/WhatsApp Channel",
|
||||
@@ -107,6 +111,12 @@
|
||||
"PLACEHOLDER": "Please enter your Twilio Account SID",
|
||||
"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": {
|
||||
"LABEL": "Channel Type",
|
||||
"ERROR": "Please select your Channel Type"
|
||||
@@ -193,6 +203,7 @@
|
||||
"PROVIDERS": {
|
||||
"LABEL": "API Provider",
|
||||
"TWILIO": "Twilio",
|
||||
"WHATSAPP_CLOUD": "WhatsApp Cloud",
|
||||
"360_DIALOG": "360Dialog"
|
||||
},
|
||||
"INBOX_NAME": {
|
||||
@@ -205,13 +216,31 @@
|
||||
"PLACEHOLDER": "Please enter the phone number from which message will be sent.",
|
||||
"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": {
|
||||
"LABEL": "API key",
|
||||
"SUBTITLE": "Configure the WhatsApp API key.",
|
||||
"PLACEHOLDER": "API key",
|
||||
"APPLY_FOR_ACCESS": "Don't have any API key? Apply for access here",
|
||||
"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",
|
||||
"API": {
|
||||
"ERROR_MESSAGE": "We were not able to save the WhatsApp channel"
|
||||
@@ -298,7 +327,7 @@
|
||||
},
|
||||
"AUTH": {
|
||||
"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": {
|
||||
"TITLE": "Агенти",
|
||||
@@ -339,7 +368,7 @@
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Inbox settings 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": {
|
||||
"ENABLED": "Enabled",
|
||||
@@ -412,15 +441,22 @@
|
||||
"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.",
|
||||
"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_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_SUB_TEXT": "Use the `inbox_identifier` token shown here to authentication your API clients.",
|
||||
"FORWARD_EMAIL_TITLE": "Forward to Email",
|
||||
"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_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": {
|
||||
"TITLE": "Reauthorize",
|
||||
|
||||
@@ -84,6 +84,52 @@
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,6 +65,7 @@
|
||||
"PLACEHOLDER": "Select date range"
|
||||
},
|
||||
"GROUP_BY_FILTER_DROPDOWN_LABEL": "Group By",
|
||||
"DURATION_FILTER_LABEL": "Duration",
|
||||
"GROUP_BY_DAY_OPTIONS": [
|
||||
{
|
||||
"id": 1,
|
||||
|
||||
@@ -1,17 +1,29 @@
|
||||
{
|
||||
"BULK_ACTION": {
|
||||
"CONVERSATIONS_SELECTED": "%{conversationCount} conversations selected",
|
||||
"AGENT_SELECT_LABEL": "Seleccionar Agent",
|
||||
"ASSIGN_CONFIRMATION_LABEL": "Are you sure you want to assign %{conversationCount} %{conversationLabel} to",
|
||||
"GO_BACK_LABEL": "Go back",
|
||||
"ASSIGN_LABEL": "Assignar",
|
||||
"ASSIGN_AGENT_TOOLTIP": "Assign Agent",
|
||||
"RESOLVE_TOOLTIP": "Resoldre",
|
||||
"ASSIGN_SUCCESFUL": "Conversations assigned successfully",
|
||||
"ASSIGN_FAILED": "Failed to assign conversations, please try again",
|
||||
"RESOLVE_SUCCESFUL": "Conversations resolved successfully",
|
||||
"RESOLVE_FAILED": "Failed to resolve conversations, please try again",
|
||||
"ALL_CONVERSATIONS_SELECTED_ALERT": "Conversations visible on this page are only selected.",
|
||||
"AGENT_LIST_LOADING": "Loading Agents"
|
||||
"BULK_ACTION": {
|
||||
"CONVERSATIONS_SELECTED": "%{conversationCount} conversations selected",
|
||||
"AGENT_SELECT_LABEL": "Seleccionar Agent",
|
||||
"ASSIGN_CONFIRMATION_LABEL": "Are you sure you want to assign %{conversationCount} %{conversationLabel} to",
|
||||
"GO_BACK_LABEL": "Go back",
|
||||
"ASSIGN_LABEL": "Assignar",
|
||||
"ASSIGN_AGENT_TOOLTIP": "Assign Agent",
|
||||
"ASSIGN_SUCCESFUL": "Conversations assigned successfully",
|
||||
"ASSIGN_FAILED": "Failed to assign conversations, please try again",
|
||||
"RESOLVE_SUCCESFUL": "Conversations resolved successfully",
|
||||
"RESOLVE_FAILED": "Failed to resolve conversations, please try again",
|
||||
"ALL_CONVERSATIONS_SELECTED_ALERT": "Conversations visible on this page are only selected.",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,7 +111,8 @@
|
||||
"EMAIL_ADDRESS": {
|
||||
"PLACEHOLDER": "Introdueix l'adreça de correu electrònic del contacte",
|
||||
"LABEL": "Adreça de correu electrònic",
|
||||
"DUPLICATE": "Aquesta adreça de correu electrònic s’utilitza per a un altre contacte."
|
||||
"DUPLICATE": "Aquesta adreça de correu electrònic s’utilitza per a un altre contacte.",
|
||||
"ERROR": "Introduïu una adreça de correu electrònic vàlida."
|
||||
},
|
||||
"PHONE_NUMBER": {
|
||||
"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",
|
||||
"ERROR_MESSAGE": "S'ha produït un error; tornau-ho a provar"
|
||||
},
|
||||
|
||||
@@ -38,7 +38,8 @@
|
||||
"CUSTOM_ATTRIBUTE_LINK": "Link",
|
||||
"CUSTOM_ATTRIBUTE_CHECKBOX": "Checkbox",
|
||||
"CREATED_AT": "Created At",
|
||||
"LAST_ACTIVITY": "Last Activity"
|
||||
"LAST_ACTIVITY": "Last Activity",
|
||||
"REFERER_LINK": "Referrer link"
|
||||
},
|
||||
"GROUPS": {
|
||||
"STANDARD_FILTERS": "Standard Filters",
|
||||
|
||||
24
app/javascript/dashboard/i18n/locale/ca/helpCenter.json
Normal file
24
app/javascript/dashboard/i18n/locale/ca/helpCenter.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,8 @@
|
||||
"ADD": {
|
||||
"CHANNEL_NAME": {
|
||||
"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": {
|
||||
"LABEL": "Nom del lloc web",
|
||||
@@ -82,7 +83,7 @@
|
||||
},
|
||||
"CHANNEL_GREETING_TOGGLE": {
|
||||
"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",
|
||||
"DISABLED": "Inhabilita"
|
||||
},
|
||||
@@ -97,7 +98,10 @@
|
||||
"LABEL": "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": {
|
||||
"TITLE": "Twilio SMS/WhatsApp Channel",
|
||||
@@ -107,6 +111,12 @@
|
||||
"PLACEHOLDER": "Introduïu el vostre compte Twilio SID",
|
||||
"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": {
|
||||
"LABEL": "Tipus de canal",
|
||||
"ERROR": "Seleccioneu el teu tipus de canal"
|
||||
@@ -193,6 +203,7 @@
|
||||
"PROVIDERS": {
|
||||
"LABEL": "API Provider",
|
||||
"TWILIO": "Twilio",
|
||||
"WHATSAPP_CLOUD": "WhatsApp Cloud",
|
||||
"360_DIALOG": "360Dialog"
|
||||
},
|
||||
"INBOX_NAME": {
|
||||
@@ -205,13 +216,31 @@
|
||||
"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 `+`."
|
||||
},
|
||||
"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": {
|
||||
"LABEL": "API key",
|
||||
"SUBTITLE": "Configure the WhatsApp API key.",
|
||||
"PLACEHOLDER": "API key",
|
||||
"APPLY_FOR_ACCESS": "Don't have any API key? Apply for access here",
|
||||
"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",
|
||||
"API": {
|
||||
"ERROR_MESSAGE": "We were not able to save the WhatsApp channel"
|
||||
@@ -298,7 +327,7 @@
|
||||
},
|
||||
"AUTH": {
|
||||
"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": {
|
||||
"TITLE": "Agents",
|
||||
@@ -339,7 +368,7 @@
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "El color del widget s'ha actualitzat 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": {
|
||||
"ENABLED": "Habilita",
|
||||
@@ -412,15 +441,22 @@
|
||||
"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",
|
||||
"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_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_SUB_TEXT": "Use the `inbox_identifier` token shown here to authentication your API clients.",
|
||||
"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.",
|
||||
"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": {
|
||||
"TITLE": "Reautoritza",
|
||||
|
||||
@@ -84,6 +84,52 @@
|
||||
},
|
||||
"CONNECT": {
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,6 +65,7 @@
|
||||
"PLACEHOLDER": "Select date range"
|
||||
},
|
||||
"GROUP_BY_FILTER_DROPDOWN_LABEL": "Group By",
|
||||
"DURATION_FILTER_LABEL": "Duration",
|
||||
"GROUP_BY_DAY_OPTIONS": [
|
||||
{
|
||||
"id": 1,
|
||||
|
||||
@@ -1,17 +1,29 @@
|
||||
{
|
||||
"BULK_ACTION": {
|
||||
"CONVERSATIONS_SELECTED": "%{conversationCount} conversations selected",
|
||||
"AGENT_SELECT_LABEL": "Vybrat agenta",
|
||||
"ASSIGN_CONFIRMATION_LABEL": "Are you sure you want to assign %{conversationCount} %{conversationLabel} to",
|
||||
"GO_BACK_LABEL": "Go back",
|
||||
"ASSIGN_LABEL": "Přiřadit",
|
||||
"ASSIGN_AGENT_TOOLTIP": "Assign Agent",
|
||||
"RESOLVE_TOOLTIP": "Vyřešit",
|
||||
"ASSIGN_SUCCESFUL": "Conversations assigned successfully",
|
||||
"ASSIGN_FAILED": "Failed to assign conversations, please try again",
|
||||
"RESOLVE_SUCCESFUL": "Conversations resolved successfully",
|
||||
"RESOLVE_FAILED": "Failed to resolve conversations, please try again",
|
||||
"ALL_CONVERSATIONS_SELECTED_ALERT": "Conversations visible on this page are only selected.",
|
||||
"AGENT_LIST_LOADING": "Loading Agents"
|
||||
"BULK_ACTION": {
|
||||
"CONVERSATIONS_SELECTED": "%{conversationCount} conversations selected",
|
||||
"AGENT_SELECT_LABEL": "Vybrat agenta",
|
||||
"ASSIGN_CONFIRMATION_LABEL": "Are you sure you want to assign %{conversationCount} %{conversationLabel} to",
|
||||
"GO_BACK_LABEL": "Go back",
|
||||
"ASSIGN_LABEL": "Přiřadit",
|
||||
"ASSIGN_AGENT_TOOLTIP": "Assign Agent",
|
||||
"ASSIGN_SUCCESFUL": "Conversations assigned successfully",
|
||||
"ASSIGN_FAILED": "Failed to assign conversations, please try again",
|
||||
"RESOLVE_SUCCESFUL": "Conversations resolved successfully",
|
||||
"RESOLVE_FAILED": "Failed to resolve conversations, please try again",
|
||||
"ALL_CONVERSATIONS_SELECTED_ALERT": "Conversations visible on this page are only selected.",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,7 +111,8 @@
|
||||
"EMAIL_ADDRESS": {
|
||||
"PLACEHOLDER": "Zadejte e-mailovou adresu kontaktu",
|
||||
"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": {
|
||||
"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",
|
||||
"ERROR_MESSAGE": "Došlo k chybě, zkuste to prosím znovu"
|
||||
},
|
||||
|
||||
@@ -38,7 +38,8 @@
|
||||
"CUSTOM_ATTRIBUTE_LINK": "Link",
|
||||
"CUSTOM_ATTRIBUTE_CHECKBOX": "Checkbox",
|
||||
"CREATED_AT": "Created At",
|
||||
"LAST_ACTIVITY": "Poslední aktivita"
|
||||
"LAST_ACTIVITY": "Poslední aktivita",
|
||||
"REFERER_LINK": "Referrer link"
|
||||
},
|
||||
"GROUPS": {
|
||||
"STANDARD_FILTERS": "Standard Filters",
|
||||
|
||||
24
app/javascript/dashboard/i18n/locale/cs/helpCenter.json
Normal file
24
app/javascript/dashboard/i18n/locale/cs/helpCenter.json
Normal 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
Reference in New Issue
Block a user