diff --git a/.circleci/config.yml b/.circleci/config.yml index 87572fdc3..27918bfd6 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -100,6 +100,10 @@ jobs: - run: name: Rubocop command: bundle exec rubocop + + # - run: + # name: Brakeman + # command: bundle exec brakeman - run: name: eslint diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index e3eb2fd01..2418bd621 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -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 diff --git a/.devcontainer/Dockerfile.base b/.devcontainer/Dockerfile.base index de007e202..769be24f8 100644 --- a/.devcontainer/Dockerfile.base +++ b/.devcontainer/Dockerfile.base @@ -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 diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index c0301d87b..a70ba3788 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -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" } - }, + } } diff --git a/.devcontainer/scripts/setup.sh b/.devcontainer/scripts/setup.sh index 1b5842603..4ffee2d3a 100755 --- a/.devcontainer/scripts/setup.sh +++ b/.devcontainer/scripts/setup.sh @@ -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 diff --git a/.eslintrc.js b/.eslintrc.js index e699f0fd4..0fae1bc44 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -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, diff --git a/.storybook/preview.js b/.storybook/preview.js index e0c27a1ac..e553e514a 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -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({ diff --git a/Gemfile b/Gemfile index 9029ccbcb..270e6527c 100644 --- a/Gemfile +++ b/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' diff --git a/Gemfile.lock b/Gemfile.lock index cff30308b..7e0583757 100644 --- a/Gemfile.lock +++ b/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 diff --git a/README.md b/README.md index c3ad01a5e..09017fb47 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ ___ Huntr uptime response time + Artifact HUB

Chat dashboard diff --git a/VERSION_CW b/VERSION_CW new file mode 100644 index 000000000..e70b4523a --- /dev/null +++ b/VERSION_CW @@ -0,0 +1 @@ +2.6.0 diff --git a/VERSION_CWCTL b/VERSION_CWCTL new file mode 100644 index 000000000..7ec1d6db4 --- /dev/null +++ b/VERSION_CWCTL @@ -0,0 +1 @@ +2.1.0 diff --git a/app/actions/contact_identify_action.rb b/app/actions/contact_identify_action.rb index 746d1bfec..f19caac77 100644 --- a/app/actions/contact_identify_action.rb +++ b/app/actions/contact_identify_action.rb @@ -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 diff --git a/app/builders/campaigns/campaign_conversation_builder.rb b/app/builders/campaigns/campaign_conversation_builder.rb index b04c6d077..3b3f262c9 100644 --- a/app/builders/campaigns/campaign_conversation_builder.rb +++ b/app/builders/campaigns/campaign_conversation_builder.rb @@ -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 diff --git a/app/builders/messages/messenger/message_builder.rb b/app/builders/messages/messenger/message_builder.rb index 6d3ed0179..3c406f1e0 100644 --- a/app/builders/messages/messenger/message_builder.rb +++ b/app/builders/messages/messenger/message_builder.rb @@ -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 {} diff --git a/app/controllers/api/v1/accounts/agent_bots_controller.rb b/app/controllers/api/v1/accounts/agent_bots_controller.rb index 9f4ef2cd9..efb48c5c6 100644 --- a/app/controllers/api/v1/accounts/agent_bots_controller.rb +++ b/app/controllers/api/v1/accounts/agent_bots_controller.rb @@ -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 diff --git a/app/controllers/api/v1/accounts/articles_controller.rb b/app/controllers/api/v1/accounts/articles_controller.rb index 232eecd32..77a1fdf65 100644 --- a/app/controllers/api/v1/accounts/articles_controller.rb +++ b/app/controllers/api/v1/accounts/articles_controller.rb @@ -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 diff --git a/app/controllers/api/v1/accounts/categories_controller.rb b/app/controllers/api/v1/accounts/categories_controller.rb index a77f1fb2a..c4491b2e2 100644 --- a/app/controllers/api/v1/accounts/categories_controller.rb +++ b/app/controllers/api/v1/accounts/categories_controller.rb @@ -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 diff --git a/app/controllers/api/v1/accounts/channels/twilio_channels_controller.rb b/app/controllers/api/v1/accounts/channels/twilio_channels_controller.rb index f5a3c6a6d..55a9456cf 100644 --- a/app/controllers/api/v1/accounts/channels/twilio_channels_controller.rb +++ b/app/controllers/api/v1/accounts/channels/twilio_channels_controller.rb @@ -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 diff --git a/app/controllers/api/v1/accounts/contacts_controller.rb b/app/controllers/api/v1/accounts/contacts_controller.rb index 76fd206f4..44b1280ee 100644 --- a/app/controllers/api/v1/accounts/contacts_controller.rb +++ b/app/controllers/api/v1/accounts/contacts_controller.rb @@ -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 diff --git a/app/controllers/api/v1/accounts/portals_controller.rb b/app/controllers/api/v1/accounts/portals_controller.rb index 75ffc35e9..b081978b9 100644 --- a/app/controllers/api/v1/accounts/portals_controller.rb +++ b/app/controllers/api/v1/accounts/portals_controller.rb @@ -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 diff --git a/app/controllers/api/v1/widget/base_controller.rb b/app/controllers/api/v1/widget/base_controller.rb index fc4d33fc3..d854114c4 100644 --- a/app/controllers/api/v1/widget/base_controller.rb +++ b/app/controllers/api/v1/widget/base_controller.rb @@ -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 diff --git a/app/controllers/api/v1/widget/contacts_controller.rb b/app/controllers/api/v1/widget/contacts_controller.rb index fbc303a4f..5138fe675 100644 --- a/app/controllers/api/v1/widget/contacts_controller.rb +++ b/app/controllers/api/v1/widget/contacts_controller.rb @@ -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? diff --git a/app/controllers/api/v1/widget/conversations_controller.rb b/app/controllers/api/v1/widget/conversations_controller.rb index 8d054229c..a01d33cdf 100644 --- a/app/controllers/api/v1/widget/conversations_controller.rb +++ b/app/controllers/api/v1/widget/conversations_controller.rb @@ -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 diff --git a/app/controllers/api/v1/widget/messages_controller.rb b/app/controllers/api/v1/widget/messages_controller.rb index c2fa6304b..7ff77be32 100644 --- a/app/controllers/api/v1/widget/messages_controller.rb +++ b/app/controllers/api/v1/widget/messages_controller.rb @@ -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 diff --git a/app/controllers/concerns/meta_token_verify_concern.rb b/app/controllers/concerns/meta_token_verify_concern.rb new file mode 100644 index 000000000..b3f920644 --- /dev/null +++ b/app/controllers/concerns/meta_token_verify_concern.rb @@ -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 diff --git a/app/controllers/platform/api/v1/users_controller.rb b/app/controllers/platform/api/v1/users_controller.rb index 4a7eafc9c..12c87deb5 100644 --- a/app/controllers/platform/api/v1/users_controller.rb +++ b/app/controllers/platform/api/v1/users_controller.rb @@ -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 diff --git a/app/controllers/public/api/v1/portals/articles_controller.rb b/app/controllers/public/api/v1/portals/articles_controller.rb new file mode 100644 index 000000000..2a961b7a9 --- /dev/null +++ b/app/controllers/public/api/v1/portals/articles_controller.rb @@ -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 diff --git a/app/controllers/public/api/v1/portals/categories_controller.rb b/app/controllers/public/api/v1/portals/categories_controller.rb new file mode 100644 index 000000000..cf57d73ff --- /dev/null +++ b/app/controllers/public/api/v1/portals/categories_controller.rb @@ -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 diff --git a/app/controllers/public/api/v1/portals_controller.rb b/app/controllers/public/api/v1/portals_controller.rb new file mode 100644 index 000000000..aedf1a09d --- /dev/null +++ b/app/controllers/public/api/v1/portals_controller.rb @@ -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 diff --git a/app/controllers/twilio/callback_controller.rb b/app/controllers/twilio/callback_controller.rb index 44dcc9b6f..7723e5dd2 100644 --- a/app/controllers/twilio/callback_controller.rb +++ b/app/controllers/twilio/callback_controller.rb @@ -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 diff --git a/app/controllers/webhooks/instagram_controller.rb b/app/controllers/webhooks/instagram_controller.rb index e6fe93566..b658915ed 100644 --- a/app/controllers/webhooks/instagram_controller.rb +++ b/app/controllers/webhooks/instagram_controller.rb @@ -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 diff --git a/app/controllers/webhooks/whatsapp_controller.rb b/app/controllers/webhooks/whatsapp_controller.rb index 7560da1e4..8f408d2b0 100644 --- a/app/controllers/webhooks/whatsapp_controller.rb +++ b/app/controllers/webhooks/whatsapp_controller.rb @@ -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 diff --git a/app/controllers/widgets_controller.rb b/app/controllers/widgets_controller.rb index ad7d2e3c0..653d3360f 100644 --- a/app/controllers/widgets_controller.rb +++ b/app/controllers/widgets_controller.rb @@ -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 diff --git a/app/finders/conversation_finder.rb b/app/finders/conversation_finder.rb index ecbccfb88..58e911362 100644 --- a/app/finders/conversation_finder.rb +++ b/app/finders/conversation_finder.rb @@ -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 diff --git a/app/helpers/api/v1/inboxes_helper.rb b/app/helpers/api/v1/inboxes_helper.rb index aa28e4ccc..c88f2fb73 100644 --- a/app/helpers/api/v1/inboxes_helper.rb +++ b/app/helpers/api/v1/inboxes_helper.rb @@ -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) diff --git a/app/helpers/widget_helper.rb b/app/helpers/widget_helper.rb new file mode 100644 index 000000000..0798789eb --- /dev/null +++ b/app/helpers/widget_helper.rb @@ -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 diff --git a/app/javascript/dashboard/api/contacts.js b/app/javascript/dashboard/api/contacts.js index 8bea5f74b..3599961e1 100644 --- a/app/javascript/dashboard/api/contacts.js +++ b/app/javascript/dashboard/api/contacts.js @@ -71,6 +71,10 @@ class ContactAPI extends ApiClient { custom_attributes: customAttributes, }); } + + destroyAvatar(contactId) { + return axios.delete(`${this.url}/${contactId}/avatar`); + } } export default new ContactAPI(); diff --git a/app/javascript/dashboard/api/specs/contacts.spec.js b/app/javascript/dashboard/api/specs/contacts.spec.js index dfdc3a48b..dc034480a 100644 --- a/app/javascript/dashboard/api/specs/contacts.spec.js +++ b/app/javascript/dashboard/api/specs/contacts.spec.js @@ -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' + ); + }); }); }); diff --git a/app/javascript/dashboard/assets/scss/_foundation-settings.scss b/app/javascript/dashboard/assets/scss/_foundation-settings.scss index c6b7f6fd3..d13dcbe5e 100644 --- a/app/javascript/dashboard/assets/scss/_foundation-settings.scss +++ b/app/javascript/dashboard/assets/scss/_foundation-settings.scss @@ -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; diff --git a/app/javascript/dashboard/assets/scss/_typography.scss b/app/javascript/dashboard/assets/scss/_typography.scss index abab48564..7db0f2a46 100644 --- a/app/javascript/dashboard/assets/scss/_typography.scss +++ b/app/javascript/dashboard/assets/scss/_typography.scss @@ -4,6 +4,7 @@ .page-sub-title { font-size: $font-size-large; + word-wrap: break-word; } .block-title { diff --git a/app/javascript/dashboard/assets/scss/_variables.scss b/app/javascript/dashboard/assets/scss/_variables.scss index ea8334c57..f24f00830 100644 --- a/app/javascript/dashboard/assets/scss/_variables.scss +++ b/app/javascript/dashboard/assets/scss/_variables.scss @@ -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; +} diff --git a/app/javascript/dashboard/assets/scss/widgets/_tabs.scss b/app/javascript/dashboard/assets/scss/widgets/_tabs.scss index dbb68f6d5..234d7e171 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_tabs.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_tabs.scss @@ -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; diff --git a/app/javascript/dashboard/components/ModalHeader.vue b/app/javascript/dashboard/components/ModalHeader.vue index e0ce5f993..0f8d8931c 100644 --- a/app/javascript/dashboard/components/ModalHeader.vue +++ b/app/javascript/dashboard/components/ModalHeader.vue @@ -4,7 +4,7 @@

{{ headerTitle }}

-

+

{{ headerContent }}

@@ -29,3 +29,8 @@ export default { }, }; + diff --git a/app/javascript/dashboard/components/helpCenter/ArticleItem.stories.js b/app/javascript/dashboard/components/helpCenter/ArticleItem.stories.js new file mode 100644 index 000000000..18dc8295c --- /dev/null +++ b/app/javascript/dashboard/components/helpCenter/ArticleItem.stories.js @@ -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: + '', +}); + +export const ArticleItem = Template.bind({}); +ArticleItem.args = { + title: 'Setup your account', + author: { + name: 'John Doe', + }, + category: 'Getting started', + readCount: 12, + status: 'published', + updatedAt: 1657255863, +}; diff --git a/app/javascript/dashboard/components/helpCenter/ArticleItem.vue b/app/javascript/dashboard/components/helpCenter/ArticleItem.vue new file mode 100644 index 000000000..252215c8c --- /dev/null +++ b/app/javascript/dashboard/components/helpCenter/ArticleItem.vue @@ -0,0 +1,129 @@ + + + + diff --git a/app/javascript/dashboard/components/helpCenter/ArticleTable.stories.js b/app/javascript/dashboard/components/helpCenter/ArticleTable.stories.js new file mode 100644 index 000000000..f921c7eb1 --- /dev/null +++ b/app/javascript/dashboard/components/helpCenter/ArticleTable.stories.js @@ -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: + '', +}); + +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'), +}; diff --git a/app/javascript/dashboard/components/helpCenter/ArticleTable.vue b/app/javascript/dashboard/components/helpCenter/ArticleTable.vue new file mode 100644 index 000000000..b6b0f34fe --- /dev/null +++ b/app/javascript/dashboard/components/helpCenter/ArticleTable.vue @@ -0,0 +1,84 @@ + + + + diff --git a/app/javascript/dashboard/components/helpCenter/Header/ArticleHeader.stories.js b/app/javascript/dashboard/components/helpCenter/Header/ArticleHeader.stories.js new file mode 100644 index 000000000..8de3d12b6 --- /dev/null +++ b/app/javascript/dashboard/components/helpCenter/Header/ArticleHeader.stories.js @@ -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: + '', +}); + +export const ArticleHeaderView = Template.bind({}); +ArticleHeaderView.args = { + headerTitle: 'All articles', + count: 112, + selectedValue: 'Status', + openFilterModal: action('openedFilterModal'), + openDropdown: action('opened'), + closeDropdown: action('closed'), +}; diff --git a/app/javascript/dashboard/components/helpCenter/Header/ArticleHeader.vue b/app/javascript/dashboard/components/helpCenter/Header/ArticleHeader.vue new file mode 100644 index 000000000..d106bd93c --- /dev/null +++ b/app/javascript/dashboard/components/helpCenter/Header/ArticleHeader.vue @@ -0,0 +1,169 @@ + + + + + diff --git a/app/javascript/dashboard/components/helpCenter/Header/EditArticleHeader.stories.js b/app/javascript/dashboard/components/helpCenter/Header/EditArticleHeader.stories.js new file mode 100644 index 000000000..56e0f8770 --- /dev/null +++ b/app/javascript/dashboard/components/helpCenter/Header/EditArticleHeader.stories.js @@ -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: + '', +}); + +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'), +}; diff --git a/app/javascript/dashboard/components/helpCenter/Header/EditArticleHeader.vue b/app/javascript/dashboard/components/helpCenter/Header/EditArticleHeader.vue new file mode 100644 index 000000000..83f2bf987 --- /dev/null +++ b/app/javascript/dashboard/components/helpCenter/Header/EditArticleHeader.vue @@ -0,0 +1,152 @@ + + + + + diff --git a/app/javascript/dashboard/components/helpCenter/Sidebar/Sidebar.stories.js b/app/javascript/dashboard/components/helpCenter/Sidebar/Sidebar.stories.js new file mode 100644 index 000000000..245eded8b --- /dev/null +++ b/app/javascript/dashboard/components/helpCenter/Sidebar/Sidebar.stories.js @@ -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: '', +}); + +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', + }, + ], + }, + ], +}; diff --git a/app/javascript/dashboard/components/helpCenter/Sidebar/Sidebar.vue b/app/javascript/dashboard/components/helpCenter/Sidebar/Sidebar.vue new file mode 100644 index 000000000..2f8fdd2fd --- /dev/null +++ b/app/javascript/dashboard/components/helpCenter/Sidebar/Sidebar.vue @@ -0,0 +1,88 @@ + + + + + diff --git a/app/javascript/dashboard/components/helpCenter/Sidebar/SidebarHeader.vue b/app/javascript/dashboard/components/helpCenter/Sidebar/SidebarHeader.vue new file mode 100644 index 000000000..a68ea6b63 --- /dev/null +++ b/app/javascript/dashboard/components/helpCenter/Sidebar/SidebarHeader.vue @@ -0,0 +1,113 @@ + + + + + diff --git a/app/javascript/dashboard/components/helpCenter/Sidebar/SidebarSearch.vue b/app/javascript/dashboard/components/helpCenter/Sidebar/SidebarSearch.vue new file mode 100644 index 000000000..8af29af17 --- /dev/null +++ b/app/javascript/dashboard/components/helpCenter/Sidebar/SidebarSearch.vue @@ -0,0 +1,62 @@ + + + + + diff --git a/app/javascript/dashboard/components/layout/Sidebar.vue b/app/javascript/dashboard/components/layout/Sidebar.vue index 5017b3d4b..80b2486e6 100644 --- a/app/javascript/dashboard/components/layout/Sidebar.vue +++ b/app/javascript/dashboard/components/layout/Sidebar.vue @@ -11,6 +11,7 @@ @open-notification-panel="openNotificationPanel" /> diff --git a/app/javascript/dashboard/components/layout/config/sidebarItems/settings.js b/app/javascript/dashboard/components/layout/config/sidebarItems/settings.js index 176fd152c..0b78e8a21 100644 --- a/app/javascript/dashboard/components/layout/config/sidebarItems/settings.js +++ b/app/javascript/dashboard/components/layout/config/sidebarItems/settings.js @@ -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', diff --git a/app/javascript/dashboard/components/layout/sidebarComponents/AccountContext.vue b/app/javascript/dashboard/components/layout/sidebarComponents/AccountContext.vue index 71382d5e6..60b85cb22 100644 --- a/app/javascript/dashboard/components/layout/sidebarComponents/AccountContext.vue +++ b/app/javascript/dashboard/components/layout/sidebarComponents/AccountContext.vue @@ -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; diff --git a/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryChildNavItem.vue b/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryChildNavItem.vue index 4d2601774..de10dda4a 100644 --- a/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryChildNavItem.vue +++ b/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryChildNavItem.vue @@ -26,6 +26,9 @@ :class="{ 'text-truncate': shouldTruncate }" > {{ label }} + + {{ childItemCount }} + {{ 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); + } +} diff --git a/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryNavItem.vue b/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryNavItem.vue index 1fcde8948..e89a1ab51 100644 --- a/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryNavItem.vue +++ b/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryNavItem.vue @@ -1,8 +1,14 @@