Compare commits

..
Author SHA1 Message Date
Muhsin Keloth f2171ffa76 fix: Retry message 2023-10-19 16:47:18 +05:30
1271 changed files with 26187 additions and 23841 deletions
+1 -1
View File
@@ -74,7 +74,7 @@ jobs:
name: yarn name: yarn
command: yarn install --frozen-lockfile --cache-folder ~/.cache/yarn command: yarn install --frozen-lockfile --cache-folder ~/.cache/yarn
# Store yarn cache # Store yarn / webpacker cache
- save_cache: - save_cache:
key: chatwoot-yarn-{{ .Environment.CACHE_VERSION }}-{{ checksum "yarn.lock" }} key: chatwoot-yarn-{{ .Environment.CACHE_VERSION }}-{{ checksum "yarn.lock" }}
paths: paths:
+1 -1
View File
@@ -2,7 +2,7 @@
# It has the dependencies already installed so that codespace will boot up fast # It has the dependencies already installed so that codespace will boot up fast
FROM ghcr.io/chatwoot/chatwoot_codespace:latest FROM ghcr.io/chatwoot/chatwoot_codespace:latest
# Do the set up required for chatwoot app # Do the set up required for chatwoot app
WORKDIR /workspace WORKDIR /workspace
COPY . /workspace COPY . /workspace
RUN yarn && gem install bundler && bundle install RUN yarn && gem install bundler && bundle install
+11 -14
View File
@@ -1,14 +1,10 @@
ARG VARIANT ARG VARIANT=ubuntu-20.04
FROM mcr.microsoft.com/vscode/devcontainers/base:${VARIANT}
FROM mcr.microsoft.com/vscode/devcontainers/base:0-${VARIANT}
ARG NODE_VERSION
ARG RUBY_VERSION
ARG USER_UID
ARG USER_GID
# Update args in docker-compose.yaml to set the UID/GID of the "vscode" user. # Update args in docker-compose.yaml to set the UID/GID of the "vscode" user.
ARG USER_UID=1000
ARG USER_GID=$USER_UID
RUN if [ "$USER_GID" != "1000" ] || [ "$USER_UID" != "1000" ]; then \ RUN if [ "$USER_GID" != "1000" ] || [ "$USER_UID" != "1000" ]; then \
groupmod --gid $USER_GID vscode \ groupmod --gid $USER_GID vscode \
&& usermod --uid $USER_UID --gid $USER_GID vscode \ && usermod --uid $USER_UID --gid $USER_GID vscode \
@@ -31,10 +27,10 @@ RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
tmux \ tmux \
zsh \ zsh \
git-flow \ git-flow \
npm \ npm
libyaml-dev
# Install rbenv and ruby # Install rbenv and ruby
ARG RUBY_VERSION="3.2.2"
RUN git clone https://github.com/rbenv/rbenv.git ~/.rbenv \ RUN git clone https://github.com/rbenv/rbenv.git ~/.rbenv \
&& echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc \ && echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc \
&& echo 'eval "$(rbenv init -)"' >> ~/.bashrc && echo 'eval "$(rbenv init -)"' >> ~/.bashrc
@@ -50,7 +46,7 @@ RUN rbenv install $RUBY_VERSION && \
RUN curl -L https://github.com/DarthSim/overmind/releases/download/v2.1.0/overmind-v2.1.0-linux-amd64.gz > overmind.gz \ RUN curl -L https://github.com/DarthSim/overmind/releases/download/v2.1.0/overmind-v2.1.0-linux-amd64.gz > overmind.gz \
&& gunzip overmind.gz \ && gunzip overmind.gz \
&& sudo mv overmind /usr/local/bin \ && sudo mv overmind /usr/local/bin \
&& chmod +x /usr/local/bin/overmind && chmod +x /usr/local/bin/overmind
# Install gh # Install gh
@@ -60,7 +56,7 @@ RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | s
&& sudo apt install gh && sudo apt install gh
# Do the set up required for chatwoot app # Do the set up required for chatwoot app
WORKDIR /workspace WORKDIR /workspace
COPY . /workspace COPY . /workspace
@@ -69,7 +65,8 @@ COPY Gemfile Gemfile.lock ./
RUN gem install bundler && bundle install RUN gem install bundler && bundle install
# set up node js # set up node js
RUN npm install n -g && \ RUN npm install npm@latest -g && \
n $NODE_VERSION npm install n -g && \
n latest
RUN npm install --global yarn RUN npm install --global yarn
RUN yarn RUN yarn
+13 -23
View File
@@ -1,41 +1,30 @@
# https://github.com/microsoft/vscode-dev-containers/blob/master/containers/python-3-postgres/.devcontainer/docker-compose.yml # https://github.com/microsoft/vscode-dev-containers/blob/master/containers/python-3-postgres/.devcontainer/docker-compose.yml
# https://github.com/microsoft/vscode-dev-containers/blob/master/containers/ruby-rails/.devcontainer/devcontainer.json # https://github.com/microsoft/vscode-dev-containers/blob/master/containers/ruby-rails/.devcontainer/devcontainer.json
# #
version: '3' version: '3'
services: services:
base: app:
build:
context: ..
dockerfile: .devcontainer/Dockerfile.base
args:
VARIANT: "ubuntu-22.04"
NODE_VERSION: "20.9.0"
RUBY_VERSION: "3.2.2"
# On Linux, you may need to update USER_UID and USER_GID below if not your local UID is not 1000.
USER_UID: "1000"
USER_GID: "1000"
image: base:latest
app:
build: build:
context: .. context: ..
dockerfile: .devcontainer/Dockerfile dockerfile: .devcontainer/Dockerfile
args: args:
VARIANT: "ubuntu-22.04" # Update 'VARIANT' to pick a Ruby version: https://github.com/microsoft/vscode-dev-containers/tree/main/containers/ruby
NODE_VERSION: "20.9.0" VARIANT: 3
RUBY_VERSION: "3.2.2" # [Choice] Install Node.js
INSTALL_NODE: "true"
NODE_VERSION: "lts/*"
# On Linux, you may need to update USER_UID and USER_GID below if not your local UID is not 1000. # On Linux, you may need to update USER_UID and USER_GID below if not your local UID is not 1000.
USER_UID: "1000" USER_UID: 1000
USER_GID: "1000" USER_GID: 1000
volumes: volumes:
- ..:/workspace:cached - ..:/workspace:cached
# Overrides default command so things don't shut down after the process ends. # Overrides default command so things don't shut down after the process ends.
command: sleep infinity command: sleep infinity
# Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function. # Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function.
network_mode: service:db network_mode: service:db
@@ -60,7 +49,8 @@ services:
restart: unless-stopped restart: unless-stopped
image: mailhog/mailhog image: mailhog/mailhog
network_mode: service:db network_mode: service:db
volumes: volumes:
postgres-data: postgres-data:
redis-data: redis-data:
+4 -4
View File
@@ -3,10 +3,10 @@ sed -i -e '/REDIS_URL/ s/=.*/=redis:\/\/localhost:6379/' .env
sed -i -e '/POSTGRES_HOST/ s/=.*/=localhost/' .env sed -i -e '/POSTGRES_HOST/ s/=.*/=localhost/' .env
sed -i -e '/SMTP_ADDRESS/ s/=.*/=localhost/' .env sed -i -e '/SMTP_ADDRESS/ s/=.*/=localhost/' .env
sed -i -e "/FRONTEND_URL/ s/=.*/=https:\/\/$CODESPACE_NAME-3000.githubpreview.dev/" .env sed -i -e "/FRONTEND_URL/ s/=.*/=https:\/\/$CODESPACE_NAME-3000.githubpreview.dev/" .env
sed -i -e "/DEV_SERVER_PUBLIC/ s/=.*/=https:\/\/$CODESPACE_NAME-3035.githubpreview.dev/" .env sed -i -e "/WEBPACKER_DEV_SERVER_PUBLIC/ s/=.*/=https:\/\/$CODESPACE_NAME-3035.githubpreview.dev/" .env
# uncomment the dev server env variable # uncomment the webpacker env variable
sed -i -e '/DEV_SERVER_PUBLIC/s/^# //' .env sed -i -e '/WEBPACKER_DEV_SERVER_PUBLIC/s/^# //' .env
# fix the error with dev server # fix the error with webpacker
echo 'export NODE_OPTIONS=--openssl-legacy-provider' >> ~/.zshrc echo 'export NODE_OPTIONS=--openssl-legacy-provider' >> ~/.zshrc
# codespaces make the ports public # codespaces make the ports public
+2 -21
View File
@@ -81,7 +81,7 @@ SMTP_OPENSSL_VERIFY_MODE=peer
# Mail Incoming # Mail Incoming
# This is the domain set for the reply emails when conversation continuity is enabled # This is the domain set for the reply emails when conversation continuity is enabled
MAILER_INBOUND_EMAIL_DOMAIN= MAILER_INBOUND_EMAIL_DOMAIN=
# Set this to the appropriate ingress channel with regards to incoming emails # Set this to appropriate ingress channel with regards to incoming emails
# Possible values are : # Possible values are :
# relay for Exim, Postfix, Qmail # relay for Exim, Postfix, Qmail
# mailgun for Mailgun # mailgun for Mailgun
@@ -91,20 +91,10 @@ MAILER_INBOUND_EMAIL_DOMAIN=
RAILS_INBOUND_EMAIL_SERVICE= RAILS_INBOUND_EMAIL_SERVICE=
# Use one of the following based on the email ingress service # Use one of the following based on the email ingress service
# Ref: https://edgeguides.rubyonrails.org/action_mailbox_basics.html # Ref: https://edgeguides.rubyonrails.org/action_mailbox_basics.html
# Set this to a password of your choice and use it in the Inbound webhook
RAILS_INBOUND_EMAIL_PASSWORD= RAILS_INBOUND_EMAIL_PASSWORD=
MAILGUN_INGRESS_SIGNING_KEY= MAILGUN_INGRESS_SIGNING_KEY=
MANDRILL_INGRESS_API_KEY= MANDRILL_INGRESS_API_KEY=
# Creating Your Inbound Webhook Instructions for Postmark and Sendgrid:
# Inbound webhook URL format:
# https://actionmailbox:[YOUR_RAILS_INBOUND_EMAIL_PASSWORD]@[YOUR_CHATWOOT_DOMAIN.COM]/rails/action_mailbox/[RAILS_INBOUND_EMAIL_SERVICE]/inbound_emails
# Note: Replace the values inside the brackets; do not include the brackets themselves.
# Example: https://actionmailbox:mYRandomPassword3@chatwoot.example.com/rails/action_mailbox/postmark/inbound_emails
# For Postmark
# Ensure the 'Include raw email content in JSON payload' checkbox is selected in the inbound webhook section.
# Storage # Storage
ACTIVE_STORAGE_SERVICE=local ACTIVE_STORAGE_SERVICE=local
@@ -186,9 +176,6 @@ ANDROID_SHA256_CERT_FINGERPRINT=AC:73:8E:DE:EB:56:EA:CC:10:87:02:A7:65:37:7B:38:
# MICROSOFT CLARITY # MICROSOFT CLARITY
# MS_CLARITY_TOKEN=xxxxxxxxx # MS_CLARITY_TOKEN=xxxxxxxxx
# GOOGLE_TAG_MANAGER
# GOOGLE_TAG = GTM-XXXXXXX
## Scout ## Scout
## https://scoutapm.com/docs/ruby/configuration ## https://scoutapm.com/docs/ruby/configuration
# SCOUT_KEY=YOURKEY # SCOUT_KEY=YOURKEY
@@ -223,7 +210,7 @@ ANDROID_SHA256_CERT_FINGERPRINT=AC:73:8E:DE:EB:56:EA:CC:10:87:02:A7:65:37:7B:38:
# if you want to use letter_opener for local emails # if you want to use letter_opener for local emails
# LETTER_OPENER=true # LETTER_OPENER=true
# meant to be used in github codespaces # meant to be used in github codespaces
# DEV_SERVER_PUBLIC= # WEBPACKER_DEV_SERVER_PUBLIC=
# If you want to use official mobile app, # If you want to use official mobile app,
# the notifications would be relayed via a Chatwoot server # the notifications would be relayed via a Chatwoot server
@@ -253,9 +240,3 @@ AZURE_APP_SECRET=
# Sentiment analysis model file path # Sentiment analysis model file path
SENTIMENT_FILE_PATH= SENTIMENT_FILE_PATH=
# Housekeeping/Performance related configurations
# Set to true if you want to remove stale contact inboxes
# contact_inboxes with no conversation older than 90 days will be removed
# REMOVE_STALE_CONTACT_INBOX_JOB_STATUS=false
+10 -3
View File
@@ -4,13 +4,13 @@ module.exports = {
'prettier', 'prettier',
'plugin:vue/recommended', 'plugin:vue/recommended',
'plugin:storybook/recommended', 'plugin:storybook/recommended',
'plugin:cypress/recommended',
], ],
parserOptions: { parserOptions: {
ecmaVersion: 2022, parser: '@babel/eslint-parser',
ecmaVersion: 2020,
sourceType: 'module', sourceType: 'module',
}, },
plugins: ['html', 'prettier'], plugins: ['html', 'prettier', 'babel'],
rules: { rules: {
'prettier/prettier': ['error'], 'prettier/prettier': ['error'],
camelcase: 'off', camelcase: 'off',
@@ -55,6 +55,13 @@ module.exports = {
'import/extensions': ['off'], 'import/extensions': ['off'],
'no-console': 'error', 'no-console': 'error',
}, },
settings: {
'import/resolver': {
webpack: {
config: 'config/webpack/resolve.js',
},
},
},
env: { env: {
browser: true, browser: true,
jest: true, jest: true,
@@ -19,6 +19,5 @@ jobs:
- name: Build the Codespace Base Image - name: Build the Codespace Base Image
run: | run: |
docker-compose -f .devcontainer/docker-compose.yml build base docker build . -t ghcr.io/chatwoot/chatwoot_codespace:latest -f .devcontainer/Dockerfile.base
docker tag base:latest ghcr.io/chatwoot/chatwoot_codespace:latest
docker push ghcr.io/chatwoot/chatwoot_codespace:latest docker push ghcr.io/chatwoot/chatwoot_codespace:latest
+1 -9
View File
@@ -75,12 +75,4 @@ yalc.lock
yarn-debug.log* yarn-debug.log*
.yarn-integrity .yarn-integrity
/storybook-static /storybook-static
# Vite Ruby
/public/vite*
node_modules
# Vite uses dotenv and suggests to ignore local-only env files. See
# https://vitejs.dev/guide/env-and-mode.html#env-files
*.local
+3 -1
View File
@@ -2,6 +2,7 @@ import { addDecorator } from '@storybook/vue';
import Vue from 'vue'; import Vue from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
import VueI18n from 'vue-i18n'; import VueI18n from 'vue-i18n';
import Vuelidate from 'vuelidate';
import Multiselect from 'vue-multiselect'; import Multiselect from 'vue-multiselect';
import VueDOMPurifyHTML from 'vue-dompurify-html'; import VueDOMPurifyHTML from 'vue-dompurify-html';
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon'; import FluentIcon from 'shared/components/FluentIcon/DashboardIcon';
@@ -13,6 +14,7 @@ import { domPurifyConfig } from 'shared/helpers/HTMLSanitizer';
import '../app/javascript/dashboard/assets/scss/storybook.scss'; import '../app/javascript/dashboard/assets/scss/storybook.scss';
Vue.use(VueI18n); Vue.use(VueI18n);
Vue.use(Vuelidate);
Vue.use(WootUiKit); Vue.use(WootUiKit);
Vue.use(Vuex); Vue.use(Vuex);
Vue.use(VueDOMPurifyHTML, domPurifyConfig); Vue.use(VueDOMPurifyHTML, domPurifyConfig);
@@ -30,7 +32,7 @@ addDecorator(() => ({
template: '<story/>', template: '<story/>',
i18n: i18nConfig, i18n: i18nConfig,
store, store,
beforeCreate: function () { beforeCreate: function() {
this.$root._i18n = this.$i18n; this.$root._i18n = this.$i18n;
}, },
})); }));
+13 -17
View File
@@ -39,8 +39,6 @@ gem 'rack-attack', '>= 6.7.0'
gem 'down' gem 'down'
# authentication type to fetch and send mail over oauth2.0 # authentication type to fetch and send mail over oauth2.0
gem 'gmail_xoauth' gem 'gmail_xoauth'
# Lock net-smtp to 0.3.4 to avoid issues with gmail_xoauth2
gem 'net-smtp', '~> 0.3.4'
# Prevent CSV injection # Prevent CSV injection
gem 'csv-safe' gem 'csv-safe'
@@ -64,7 +62,7 @@ gem 'activerecord-import'
gem 'dotenv-rails' gem 'dotenv-rails'
gem 'foreman' gem 'foreman'
gem 'puma' gem 'puma'
gem 'vite_rails' gem 'webpacker'
# metrics on heroku # metrics on heroku
gem 'barnes' gem 'barnes'
@@ -76,9 +74,9 @@ gem 'devise_token_auth'
gem 'jwt' gem 'jwt'
gem 'pundit' gem 'pundit'
# super admin # super admin
gem 'administrate', '>= 0.20.1' gem 'administrate', '>= 0.19.0'
gem 'administrate-field-active_storage', '>= 1.0.1' gem 'administrate-field-active_storage'
gem 'administrate-field-belongs_to_search', '>= 0.9.0' gem 'administrate-field-belongs_to_search'
##--- gems for pubsub service ---## ##--- gems for pubsub service ---##
# https://karolgalanciak.com/blog/2019/11/30/from-activerecord-callbacks-to-publish-slash-subscribe-pattern-and-event-driven-design/ # https://karolgalanciak.com/blog/2019/11/30/from-activerecord-callbacks-to-publish-slash-subscribe-pattern-and-event-driven-design/
@@ -109,16 +107,16 @@ gem 'google-cloud-translate-v3'
gem 'ddtrace', require: false gem 'ddtrace', require: false
gem 'elastic-apm', require: false gem 'elastic-apm', require: false
gem 'newrelic_rpm', require: false gem 'newrelic_rpm', require: false
gem 'newrelic-sidekiq-metrics', '>= 1.6.2', require: false gem 'newrelic-sidekiq-metrics', require: false
gem 'scout_apm', require: false gem 'scout_apm', require: false
gem 'sentry-rails', '>= 5.14.0', require: false gem 'sentry-rails', '>= 5.12.0', require: false
gem 'sentry-ruby', require: false gem 'sentry-ruby', require: false
gem 'sentry-sidekiq', '>= 5.14.0', require: false gem 'sentry-sidekiq', '>= 5.12.0', require: false
##-- background job processing --## ##-- background job processing --##
gem 'sidekiq', '>= 7.2.1' gem 'sidekiq', '>= 7.1.3'
# We want cron jobs # We want cron jobs
gem 'sidekiq-cron', '>= 1.12.0' gem 'sidekiq-cron', '>= 1.10.1'
##-- Push notification service --## ##-- Push notification service --##
gem 'fcm' gem 'fcm'
@@ -161,10 +159,10 @@ gem 'lograge', '~> 0.14.0', require: false
# worked with microsoft refresh token # worked with microsoft refresh token
gem 'omniauth-oauth2' gem 'omniauth-oauth2'
gem 'audited', '~> 5.4', '>= 5.4.1' gem 'audited', '~> 5.4', '>= 5.4.0'
# need for google auth # need for google auth
gem 'omniauth', '>= 2.1.2' gem 'omniauth'
gem 'omniauth-google-oauth2' gem 'omniauth-google-oauth2'
gem 'omniauth-rails_csrf_protection', '~> 1.0' gem 'omniauth-rails_csrf_protection', '~> 1.0'
@@ -200,10 +198,8 @@ group :development do
gem 'squasher' gem 'squasher'
# profiling # profiling
gem 'rack-mini-profiler', '>= 3.2.0', require: false gem 'rack-mini-profiler', '>= 3.1.1', require: false
gem 'stackprof' gem 'stackprof'
# Should install the associated chrome extension to view query logs
gem 'meta_request'
end end
group :test do group :test do
@@ -226,7 +222,7 @@ group :development, :test do
gem 'byebug', platform: :mri gem 'byebug', platform: :mri
gem 'climate_control' gem 'climate_control'
gem 'debug', '~> 1.8' gem 'debug', '~> 1.8'
gem 'factory_bot_rails', '>= 6.4.3' gem 'factory_bot_rails'
gem 'listen' gem 'listen'
gem 'mock_redis' gem 'mock_redis'
gem 'pry-rails' gem 'pry-rails'
+74 -85
View File
@@ -105,30 +105,30 @@ GEM
activerecord (>= 6.0, < 7.1) activerecord (>= 6.0, < 7.1)
addressable (2.8.4) addressable (2.8.4)
public_suffix (>= 2.0.2, < 6.0) public_suffix (>= 2.0.2, < 6.0)
administrate (0.20.1) administrate (0.19.0)
actionpack (>= 6.0, < 8.0) actionpack (>= 5.0)
actionview (>= 6.0, < 8.0) actionview (>= 5.0)
activerecord (>= 6.0, < 8.0) activerecord (>= 5.0)
jquery-rails (~> 4.6.0) jquery-rails (>= 4.0)
kaminari (~> 1.2.2) kaminari (>= 1.0)
sassc-rails (~> 2.1) sassc-rails (~> 2.1)
selectize-rails (~> 0.6) selectize-rails (~> 0.6)
administrate-field-active_storage (1.0.1) administrate-field-active_storage (0.4.2)
administrate (>= 0.2.2) administrate (>= 0.2.2)
rails (>= 7.0) rails (>= 7.0)
administrate-field-belongs_to_search (0.9.0) administrate-field-belongs_to_search (0.8.0)
administrate (>= 0.3, < 1.0) administrate (>= 0.3, < 1.0)
jbuilder (~> 2) jbuilder (~> 2)
rails (>= 4.2, < 7.2) rails (>= 4.2, < 7.1)
selectize-rails (~> 0.6) selectize-rails (~> 0.6)
annotate (3.2.0) annotate (3.2.0)
activerecord (>= 3.2, < 8.0) activerecord (>= 3.2, < 8.0)
rake (>= 10.4, < 14.0) rake (>= 10.4, < 14.0)
ast (2.4.2) ast (2.4.2)
attr_extras (7.1.0) attr_extras (7.1.0)
audited (5.4.1) audited (5.4.0)
activerecord (>= 5.0, < 7.7) activerecord (>= 5.0, < 7.2)
activesupport (>= 5.0, < 7.7) request_store (~> 1.2)
aws-eventstream (1.2.0) aws-eventstream (1.2.0)
aws-partitions (1.760.0) aws-partitions (1.760.0)
aws-sdk-core (3.171.1) aws-sdk-core (3.171.1)
@@ -148,7 +148,6 @@ GEM
barnes (0.0.9) barnes (0.0.9)
multi_json (~> 1) multi_json (~> 1)
statsd-ruby (~> 1.1) statsd-ruby (~> 1.1)
base64 (0.1.1)
bcrypt (3.1.19) bcrypt (3.1.19)
bindex (0.8.1) bindex (0.8.1)
blingfire (0.1.8) blingfire (0.1.8)
@@ -169,13 +168,13 @@ GEM
climate_control (1.2.0) climate_control (1.2.0)
coderay (1.1.3) coderay (1.1.3)
commonmarker (0.23.10) commonmarker (0.23.10)
concurrent-ruby (1.2.3) concurrent-ruby (1.2.2)
connection_pool (2.4.1) connection_pool (2.4.1)
crack (0.4.5) crack (0.4.5)
rexml rexml
crass (1.0.6) crass (1.0.6)
csv-safe (3.2.1) csv-safe (3.2.1)
cypress-on-rails (1.16.0) cypress-on-rails (1.13.1)
rack rack
database_cleaner (2.0.2) database_cleaner (2.0.2)
database_cleaner-active_record (>= 2, < 3) database_cleaner-active_record (>= 2, < 3)
@@ -183,7 +182,7 @@ GEM
activerecord (>= 5.a) activerecord (>= 5.a)
database_cleaner-core (~> 2.0.0) database_cleaner-core (~> 2.0.0)
database_cleaner-core (2.0.1) database_cleaner-core (2.0.1)
date (3.3.4) date (3.3.3)
ddtrace (1.11.1) ddtrace (1.11.1)
debase-ruby_core_source (>= 0.10.16, <= 3.2.0) debase-ruby_core_source (>= 0.10.16, <= 3.2.0)
libdatadog (~> 2.0.0.1.0) libdatadog (~> 2.0.0.1.0)
@@ -216,7 +215,6 @@ GEM
railties (>= 3.2) railties (>= 3.2)
down (5.4.0) down (5.4.0)
addressable (~> 2.8) addressable (~> 2.8)
dry-cli (1.0.0)
ecma-re-validator (0.4.0) ecma-re-validator (0.4.0)
regexp_parser (~> 2.2) regexp_parser (~> 2.2)
elastic-apm (4.6.2) elastic-apm (4.6.2)
@@ -231,10 +229,10 @@ GEM
facebook-messenger (2.0.1) facebook-messenger (2.0.1)
httparty (~> 0.13, >= 0.13.7) httparty (~> 0.13, >= 0.13.7)
rack (>= 1.4.5) rack (>= 1.4.5)
factory_bot (6.4.5) factory_bot (6.2.1)
activesupport (>= 5.0.0) activesupport (>= 5.0.0)
factory_bot_rails (6.4.3) factory_bot_rails (6.2.0)
factory_bot (~> 6.4) factory_bot (~> 6.2.0)
railties (>= 5.0.0) railties (>= 5.0.0)
faker (3.2.0) faker (3.2.0)
i18n (>= 1.8.11, < 2) i18n (>= 1.8.11, < 2)
@@ -257,13 +255,13 @@ GEM
fcm (1.0.8) fcm (1.0.8)
faraday (>= 1.0.0, < 3.0) faraday (>= 1.0.0, < 3.0)
googleauth (~> 1) googleauth (~> 1)
ffi (1.16.3) ffi (1.15.5)
ffi-compiler (1.0.1) ffi-compiler (1.0.1)
ffi (>= 1.0.0) ffi (>= 1.0.0)
rake rake
flag_shih_tzu (0.3.23) flag_shih_tzu (0.3.23)
foreman (0.87.2) foreman (0.87.2)
fugit (1.9.0) fugit (1.8.1)
et-orbi (~> 1, >= 1.2.7) et-orbi (~> 1, >= 1.2.7)
raabro (~> 1.4) raabro (~> 1.4)
gapic-common (0.18.0) gapic-common (0.18.0)
@@ -441,7 +439,7 @@ GEM
activesupport (>= 4) activesupport (>= 4)
railties (>= 4) railties (>= 4)
request_store (~> 1.0) request_store (~> 1.0)
loofah (2.22.0) loofah (2.21.4)
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.12.0) nokogiri (>= 1.12.0)
mail (2.8.1) mail (2.8.1)
@@ -452,17 +450,14 @@ GEM
marcel (1.0.2) marcel (1.0.2)
maxminddb (0.1.22) maxminddb (0.1.22)
memoist (0.16.2) memoist (0.16.2)
meta_request (0.7.4)
rack-contrib (>= 1.1, < 3)
railties (>= 3.0.0, < 7.1)
method_source (1.0.0) method_source (1.0.0)
mime-types (3.4.1) mime-types (3.4.1)
mime-types-data (~> 3.2015) mime-types-data (~> 3.2015)
mime-types-data (3.2023.0218.1) mime-types-data (3.2023.0218.1)
mini_magick (4.12.0) mini_magick (4.12.0)
mini_mime (1.1.5) mini_mime (1.1.5)
mini_portile2 (2.8.5) mini_portile2 (2.8.4)
minitest (5.21.2) minitest (5.20.0)
mock_redis (0.36.0) mock_redis (0.36.0)
ruby2_keywords ruby2_keywords
msgpack (1.7.0) msgpack (1.7.0)
@@ -473,30 +468,29 @@ GEM
activerecord (>= 5.2) activerecord (>= 5.2)
net-http-persistent (4.0.2) net-http-persistent (4.0.2)
connection_pool (~> 2.2) connection_pool (~> 2.2)
net-imap (0.4.9) net-imap (0.3.7)
date date
net-protocol net-protocol
net-pop (0.1.2) net-pop (0.1.2)
net-protocol net-protocol
net-protocol (0.2.2) net-protocol (0.2.1)
timeout timeout
net-smtp (0.3.4) net-smtp (0.3.3)
net-protocol net-protocol
netrc (0.11.0) netrc (0.11.0)
newrelic-sidekiq-metrics (1.6.2) newrelic-sidekiq-metrics (1.6.1)
newrelic_rpm (>= 8.0.0) newrelic_rpm (~> 8)
sidekiq sidekiq
newrelic_rpm (9.6.0) newrelic_rpm (8.16.0)
base64 nio4r (2.5.9)
nio4r (2.7.0) nokogiri (1.15.4)
nokogiri (1.16.0)
mini_portile2 (~> 2.8.2) mini_portile2 (~> 2.8.2)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.16.0-arm64-darwin) nokogiri (1.15.4-arm64-darwin)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.16.0-x86_64-darwin) nokogiri (1.15.4-x86_64-darwin)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.16.0-x86_64-linux) nokogiri (1.15.4-x86_64-linux)
racc (~> 1.4) racc (~> 1.4)
numo-narray (0.9.2.1) numo-narray (0.9.2.1)
oauth (1.1.0) oauth (1.1.0)
@@ -512,7 +506,7 @@ GEM
rack (>= 1.2, < 4) rack (>= 1.2, < 4)
snaky_hash (~> 2.0) snaky_hash (~> 2.0)
version_gem (~> 1.1) version_gem (~> 1.1)
omniauth (2.1.2) omniauth (2.1.1)
hashie (>= 3.4.6) hashie (>= 3.4.6)
rack (>= 2.2.3) rack (>= 2.2.3)
rack-protection rack-protection
@@ -554,24 +548,22 @@ GEM
pry-rails (0.3.9) pry-rails (0.3.9)
pry (>= 0.10.4) pry (>= 0.10.4)
public_suffix (5.0.1) public_suffix (5.0.1)
puma (6.4.2) puma (6.3.1)
nio4r (~> 2.0) nio4r (~> 2.0)
pundit (2.3.0) pundit (2.3.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
raabro (1.4.0) raabro (1.4.0)
racc (1.7.3) racc (1.7.1)
rack (2.2.8) rack (2.2.8)
rack-attack (6.7.0) rack-attack (6.7.0)
rack (>= 1.0, < 4) rack (>= 1.0, < 4)
rack-contrib (2.4.0)
rack (< 4)
rack-cors (2.0.1) rack-cors (2.0.1)
rack (>= 2.0.0) rack (>= 2.0.0)
rack-mini-profiler (3.2.0) rack-mini-profiler (3.1.1)
rack (>= 1.2.0) rack (>= 1.2.0)
rack-protection (3.1.0) rack-protection (3.0.6)
rack (~> 2.2, >= 2.2.4) rack
rack-proxy (0.7.7) rack-proxy (0.7.6)
rack rack
rack-test (2.1.0) rack-test (2.1.0)
rack (>= 1.3) rack (>= 1.3)
@@ -605,13 +597,13 @@ GEM
thor (~> 1.0) thor (~> 1.0)
zeitwerk (~> 2.5) zeitwerk (~> 2.5)
rainbow (3.1.1) rainbow (3.1.1)
rake (13.1.0) rake (13.0.6)
rb-fsevent (0.11.2) rb-fsevent (0.11.2)
rb-inotify (0.10.1) rb-inotify (0.10.1)
ffi (~> 1.0) ffi (~> 1.0)
redis (5.0.6) redis (5.0.6)
redis-client (>= 0.9.0) redis-client (>= 0.9.0)
redis-client (0.19.1) redis-client (0.17.0)
connection_pool connection_pool
redis-namespace (1.10.0) redis-namespace (1.10.0)
redis (>= 4) redis (>= 4)
@@ -709,23 +701,24 @@ GEM
activerecord (>= 4) activerecord (>= 4)
activesupport (>= 4) activesupport (>= 4)
selectize-rails (0.12.6) selectize-rails (0.12.6)
sentry-rails (5.14.0) semantic_range (3.0.0)
sentry-rails (5.12.0)
railties (>= 5.0) railties (>= 5.0)
sentry-ruby (~> 5.14.0) sentry-ruby (~> 5.12.0)
sentry-ruby (5.14.0) sentry-ruby (5.12.0)
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
sentry-sidekiq (5.14.0) sentry-sidekiq (5.12.0)
sentry-ruby (~> 5.14.0) sentry-ruby (~> 5.12.0)
sidekiq (>= 3.0) sidekiq (>= 3.0)
sexp_processor (4.17.0) sexp_processor (4.17.0)
shoulda-matchers (5.3.0) shoulda-matchers (5.3.0)
activesupport (>= 5.2.0) activesupport (>= 5.2.0)
sidekiq (7.2.1) sidekiq (7.1.6)
concurrent-ruby (< 2) concurrent-ruby (< 2)
connection_pool (>= 2.3.0) connection_pool (>= 2.3.0)
rack (>= 2.2.4) rack (>= 2.2.4)
redis-client (>= 0.19.0) redis-client (>= 0.14.0)
sidekiq-cron (1.12.0) sidekiq-cron (1.10.1)
fugit (~> 1.8) fugit (~> 1.8)
globalid (>= 1.0.1) globalid (>= 1.0.1)
sidekiq (>= 6) sidekiq (>= 6)
@@ -752,7 +745,7 @@ GEM
spring-watcher-listen (2.1.0) spring-watcher-listen (2.1.0)
listen (>= 2.7, < 4.0) listen (>= 2.7, < 4.0)
spring (>= 4) spring (>= 4)
sprockets (4.2.1) sprockets (4.2.0)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
rack (>= 2.2.4, < 4) rack (>= 2.2.4, < 4)
sprockets-rails (3.4.2) sprockets-rails (3.4.2)
@@ -765,12 +758,12 @@ GEM
stripe (8.5.0) stripe (8.5.0)
telephone_number (1.4.20) telephone_number (1.4.20)
test-prof (1.2.1) test-prof (1.2.1)
thor (1.3.0) thor (1.2.2)
tilt (2.3.0) tilt (2.2.0)
time_diff (0.3.0) time_diff (0.3.0)
activesupport activesupport
i18n i18n
timeout (0.4.1) timeout (0.4.0)
trailblazer-option (0.1.2) trailblazer-option (0.1.2)
twilio-ruby (5.77.0) twilio-ruby (5.77.0)
faraday (>= 0.9, < 3.0) faraday (>= 0.9, < 3.0)
@@ -795,13 +788,6 @@ GEM
activemodel (>= 3.2) activemodel (>= 3.2)
mail (~> 2.5) mail (~> 2.5)
version_gem (1.1.2) version_gem (1.1.2)
vite_rails (3.0.17)
railties (>= 5.1, < 8)
vite_ruby (~> 3.0, >= 3.2.2)
vite_ruby (3.5.0)
dry-cli (>= 0.7, < 2)
rack-proxy (~> 0.6, >= 0.6.1)
zeitwerk (~> 2.2)
warden (1.2.9) warden (1.2.9)
rack (>= 2.0.9) rack (>= 2.0.9)
web-console (4.2.1) web-console (4.2.1)
@@ -817,6 +803,11 @@ GEM
addressable (>= 2.8.0) addressable (>= 2.8.0)
crack (>= 0.3.2) crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0) hashdiff (>= 0.4.0, < 2.0.0)
webpacker (5.4.4)
activesupport (>= 5.2)
rack-proxy (>= 0.6.1)
railties (>= 5.2)
semantic_range (>= 2.3.0)
webrick (1.8.1) webrick (1.8.1)
websocket-driver (0.7.6) websocket-driver (0.7.6)
websocket-extensions (>= 0.1.0) websocket-extensions (>= 0.1.0)
@@ -842,12 +833,12 @@ DEPENDENCIES
active_record_query_trace active_record_query_trace
activerecord-import activerecord-import
acts-as-taggable-on acts-as-taggable-on
administrate (>= 0.20.1) administrate (>= 0.19.0)
administrate-field-active_storage (>= 1.0.1) administrate-field-active_storage
administrate-field-belongs_to_search (>= 0.9.0) administrate-field-belongs_to_search
annotate annotate
attr_extras attr_extras
audited (~> 5.4, >= 5.4.1) audited (~> 5.4, >= 5.4.0)
aws-sdk-s3 aws-sdk-s3
azure-storage-blob! azure-storage-blob!
barnes barnes
@@ -872,7 +863,7 @@ DEPENDENCIES
elastic-apm elastic-apm
email_reply_trimmer email_reply_trimmer
facebook-messenger facebook-messenger
factory_bot_rails (>= 6.4.3) factory_bot_rails
faker faker
fcm fcm
flag_shih_tzu flag_shih_tzu
@@ -902,13 +893,11 @@ DEPENDENCIES
listen listen
lograge (~> 0.14.0) lograge (~> 0.14.0)
maxminddb maxminddb
meta_request
mock_redis mock_redis
neighbor neighbor
net-smtp (~> 0.3.4) newrelic-sidekiq-metrics
newrelic-sidekiq-metrics (>= 1.6.2)
newrelic_rpm newrelic_rpm
omniauth (>= 2.1.2) omniauth
omniauth-google-oauth2 omniauth-google-oauth2
omniauth-oauth2 omniauth-oauth2
omniauth-rails_csrf_protection (~> 1.0) omniauth-rails_csrf_protection (~> 1.0)
@@ -921,7 +910,7 @@ DEPENDENCIES
pundit pundit
rack-attack (>= 6.7.0) rack-attack (>= 6.7.0)
rack-cors rack-cors
rack-mini-profiler (>= 3.2.0) rack-mini-profiler (>= 3.1.1)
rack-timeout rack-timeout
rails (~> 7.0.8.0) rails (~> 7.0.8.0)
redis redis
@@ -938,12 +927,12 @@ DEPENDENCIES
scout_apm scout_apm
scss_lint scss_lint
seed_dump seed_dump
sentry-rails (>= 5.14.0) sentry-rails (>= 5.12.0)
sentry-ruby sentry-ruby
sentry-sidekiq (>= 5.14.0) sentry-sidekiq (>= 5.12.0)
shoulda-matchers shoulda-matchers
sidekiq (>= 7.2.1) sidekiq (>= 7.1.3)
sidekiq-cron (>= 1.12.0) sidekiq-cron (>= 1.10.1)
simplecov (= 0.17.1) simplecov (= 0.17.1)
slack-ruby-client (~> 2.2.0) slack-ruby-client (~> 2.2.0)
spring spring
@@ -959,10 +948,10 @@ DEPENDENCIES
tzinfo-data tzinfo-data
uglifier uglifier
valid_email2 valid_email2
vite_rails
web-console (>= 4.2.1) web-console (>= 4.2.1)
web-push web-push
webmock webmock
webpacker
wisper (= 2.0.0) wisper (= 2.0.0)
working_hours working_hours
+1 -1
View File
@@ -1,4 +1,4 @@
backend: bin/rails s -p 3000 backend: bin/rails s -p 3000
frontend: export NODE_OPTIONS=--openssl-legacy-provider && bin/webpack-dev-server
# https://github.com/mperham/sidekiq/issues/3090#issuecomment-389748695 # https://github.com/mperham/sidekiq/issues/3090#issuecomment-389748695
worker: dotenv bundle exec sidekiq -C config/sidekiq.yml worker: dotenv bundle exec sidekiq -C config/sidekiq.yml
frontend: bin/vite dev
+2 -2
View File
@@ -1,3 +1,3 @@
backend: RAILS_ENV=test bin/rails s -p 5050 backend: RAILS_ENV=test bin/rails s -p 5050
frontend: bin/vite frontend: bin/webpack-dev-server
worker: RAILS_ENV=test dotenv bundle exec sidekiq -C config/sidekiq.yml worker: dotenv RAILS_ENV=test bundle exec sidekiq -C config/sidekiq.yml
+12 -11
View File
@@ -1,12 +1,10 @@
<img src="https://user-images.githubusercontent.com/2246121/282256557-1570674b-d142-4198-9740-69404cc6a339.png#gh-light-mode-only" width="100%" alt="Chat dashboard dark mode"/> <p align="center">
<img src="https://user-images.githubusercontent.com/2246121/282256632-87f6a01b-6467-4e0e-8a93-7bbf66d03a17.png#gh-dark-mode-only" width="100%" alt="Chat dashboard"/> <img src="https://s3.us-west-2.amazonaws.com/gh-assets.chatwoot.com/brand.svg" alt="Woot-logo" width="240" />
___ <p align="center">Customer engagement suite, an open-source alternative to Intercom, Zendesk, Salesforce Service Cloud etc.</p>
</p>
# Chatwoot <p align="center">
Customer engagement suite, an open-source alternative to Intercom, Zendesk, Salesforce Service Cloud etc.
<p>
<a href="https://heroku.com/deploy?template=https://github.com/chatwoot/chatwoot/tree/master" alt="Deploy to Heroku"> <a href="https://heroku.com/deploy?template=https://github.com/chatwoot/chatwoot/tree/master" alt="Deploy to Heroku">
<img width="150" alt="Deploy" src="https://www.herokucdn.com/deploy/button.svg"/> <img width="150" alt="Deploy" src="https://www.herokucdn.com/deploy/button.svg"/>
</a> </a>
@@ -15,7 +13,9 @@ Customer engagement suite, an open-source alternative to Intercom, Zendesk, Sale
</a> </a>
</p> </p>
<p> ___
<p align="center">
<a href="https://codeclimate.com/github/chatwoot/chatwoot/maintainability"><img src="https://api.codeclimate.com/v1/badges/e6e3f66332c91e5a4c0c/maintainability" alt="Maintainability"></a> <a href="https://codeclimate.com/github/chatwoot/chatwoot/maintainability"><img src="https://api.codeclimate.com/v1/badges/e6e3f66332c91e5a4c0c/maintainability" alt="Maintainability"></a>
<img src="https://img.shields.io/circleci/build/github/chatwoot/chatwoot" alt="CircleCI Badge"> <img src="https://img.shields.io/circleci/build/github/chatwoot/chatwoot" alt="CircleCI Badge">
<a href="https://hub.docker.com/r/chatwoot/chatwoot/"><img src="https://img.shields.io/docker/pulls/chatwoot/chatwoot" alt="Docker Pull Badge"></a> <a href="https://hub.docker.com/r/chatwoot/chatwoot/"><img src="https://img.shields.io/docker/pulls/chatwoot/chatwoot" alt="Docker Pull Badge"></a>
@@ -29,8 +29,9 @@ Customer engagement suite, an open-source alternative to Intercom, Zendesk, Sale
<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> <a href="https://artifacthub.io/packages/helm/chatwoot/chatwoot"><img src="https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/artifact-hub" alt="Artifact HUB"></a>
</p> </p>
<img src="https://user-images.githubusercontent.com/2246121/282255783-ee8a50c9-f42d-4752-8201-2d59965a663d.png#gh-light-mode-only" width="100%" alt="Chat dashboard dark mode"/> <img src="https://chatwoot-public-assets.s3.amazonaws.com/github/screenshot.png" width="100%" alt="Chat dashboard"/>
<img src="https://user-images.githubusercontent.com/2246121/282255784-3d1994ec-d895-4ff5-ac68-d819987e1869.png#gh-dark-mode-only" width="100%" alt="Chat dashboard"/>
Chatwoot is an open-source, self-hosted customer engagement suite. Chatwoot lets you view and manage your customer data, communicate with them irrespective of which medium they use, and re-engage them based on their profile. Chatwoot is an open-source, self-hosted customer engagement suite. Chatwoot lets you view and manage your customer data, communicate with them irrespective of which medium they use, and re-engage them based on their profile.
@@ -118,4 +119,4 @@ Thanks goes to all these [wonderful people](https://www.chatwoot.com/docs/contri
<a href="https://github.com/chatwoot/chatwoot/graphs/contributors"><img src="https://opencollective.com/chatwoot/contributors.svg?width=890&button=false" /></a> <a href="https://github.com/chatwoot/chatwoot/graphs/contributors"><img src="https://opencollective.com/chatwoot/contributors.svg?width=890&button=false" /></a>
*Chatwoot* &copy; 2017-2024, Chatwoot Inc - Released under the MIT License. *Chatwoot* &copy; 2017-2023, Chatwoot Inc - Released under the MIT License.
+1 -1
View File
@@ -1 +1 @@
3.3.1 3.1.0
+1 -1
View File
@@ -1 +1 @@
2.7.0 2.6.0
+1 -1
View File
@@ -104,7 +104,7 @@ class ContactIdentifyAction
# TODO: replace reject { |_k, v| v.blank? } with compact_blank when rails is upgraded # TODO: replace reject { |_k, v| v.blank? } with compact_blank when rails is upgraded
@contact.discard_invalid_attrs if discard_invalid_attrs @contact.discard_invalid_attrs if discard_invalid_attrs
@contact.save! @contact.save!
Avatar::AvatarFromUrlJob.perform_later(@contact, params[:avatar_url]) if params[:avatar_url].present? && !@contact.avatar.attached? Avatar::AvatarFromUrlJob.perform_later(@contact, params[:avatar_url]) if params[:avatar_url].present?
end end
def merge_contact(base_contact, merge_contact) def merge_contact(base_contact, merge_contact)
-5
View File
@@ -12,7 +12,6 @@ class ContactMergeAction
merge_conversations merge_conversations
merge_messages merge_messages
merge_contact_inboxes merge_contact_inboxes
merge_contact_notes
merge_and_remove_mergee_contact merge_and_remove_mergee_contact
end end
@base_contact @base_contact
@@ -34,10 +33,6 @@ class ContactMergeAction
Conversation.where(contact_id: @mergee_contact.id).update(contact_id: @base_contact.id) Conversation.where(contact_id: @mergee_contact.id).update(contact_id: @base_contact.id)
end end
def merge_contact_notes
Note.where(contact_id: @mergee_contact.id, account_id: @mergee_contact.account_id).update(contact_id: @base_contact.id)
end
def merge_messages def merge_messages
Message.where(sender: @mergee_contact).update(sender: @base_contact) Message.where(sender: @mergee_contact).update(sender: @base_contact)
end end
@@ -25,6 +25,7 @@
@import 'components/flashes'; @import 'components/flashes';
@import 'components/form-actions'; @import 'components/form-actions';
@import 'components/main-content'; @import 'components/main-content';
@import 'components/navigation';
@import 'components/pagination'; @import 'components/pagination';
@import 'components/search'; @import 'components/search';
@import 'components/reports'; @import 'components/reports';
@@ -1,7 +1,7 @@
html { html {
background-color: $color-white; background-color: $color-white;
box-sizing: border-box; box-sizing: border-box;
font-size: 16px; font-size: 10px;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
} }
@@ -16,8 +16,8 @@
.attribute-data { .attribute-data {
float: left; float: left;
margin-bottom: $base-spacing; margin-bottom: $base-spacing;
margin-left: 1.25rem; margin-left: 2rem;
width: calc(84% - 0.625rem); width: calc(84% - 1rem);
} }
.attribute--nested { .attribute--nested {
@@ -9,22 +9,22 @@
.field-unit__label { .field-unit__label {
float: left; float: left;
margin-left: 0.625rem; margin-left: 1rem;
text-align: right; text-align: right;
width: calc(15% - 0.625rem); width: calc(15% - 1rem);
} }
.field-unit__field { .field-unit__field {
float: left; float: left;
margin-left: 1.25rem; margin-left: 2rem;
max-width: 31.15rem; max-width: 50rem;
width: 100%; width: 100%;
} }
.field-unit--nested { .field-unit--nested {
border: $base-border; border: $base-border;
margin-left: 7.5%; margin-left: 7.5%;
max-width: 37.5rem; max-width: 60rem;
padding: $small-spacing; padding: $small-spacing;
width: 100%; width: 100%;
@@ -1,3 +1,3 @@
.form-actions { .form-actions {
margin-left: calc(15% + 1.25rem); margin-left: calc(15% + 2rem);
} }
@@ -13,10 +13,6 @@
table { table {
font-size: $font-size-small; font-size: $font-size-small;
} }
form {
margin-top: $space-two;
}
} }
.main-content__header { .main-content__header {
@@ -24,7 +20,7 @@
background-color: $color-white; background-color: $color-white;
border-bottom: 1px solid $color-border; border-bottom: 1px solid $color-border;
display: flex; display: flex;
min-height: 3.5rem; min-height: 5.6rem;
padding: $space-small $space-normal; padding: $space-small $space-normal;
} }
@@ -0,0 +1,88 @@
.logo-brand {
margin-bottom: $space-normal;
padding: $space-normal $space-smaller $space-small;
text-align: left;
img {
margin-bottom: $space-smaller;
max-height: 3rem;
}
}
.navigation {
background: $white;
border-right: 1px solid $color-border;
display: flex;
flex-direction: column;
font-size: $font-size-default;
font-weight: $font-weight-medium;
height: 100%;
justify-content: flex-start;
left: 0;
margin: 0;
overflow: auto;
padding: $space-normal;
position: fixed;
top: 0;
width: 21rem;
z-index: 1023;
li {
align-items: center;
display: flex;
font-size: $font-size-small;
a {
color: $color-gray;
text-decoration: none;
}
i {
min-width: $space-medium;
}
}
hr {
margin: $space-slab;
}
}
.navigation__link {
background-color: transparent;
color: $color-gray;
display: block;
line-height: 1;
margin-bottom: $space-smaller;
padding: $space-small;
&:hover {
color: $blue;
a {
color: $blue;
}
}
&.navigation__link--active {
background-color: $color-background;
border-radius: $base-border-radius;
color: $blue;
a {
color: $blue;
}
}
}
.logout {
bottom: $space-normal;
left: $space-normal;
position: fixed;
}
.app-version {
color: $color-gray;
font-size: $font-size-small;
padding-top: $space-smaller;
}
@@ -1,7 +1,7 @@
.search { .search {
margin-left: auto; margin-left: auto;
margin-right: 1.25rem; margin-right: 2rem;
max-width: 27.5rem; max-width: 44rem;
position: relative; position: relative;
width: 100%; width: 100%;
} }
@@ -1,10 +1,10 @@
// Typography // Typography
$base-font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", $base-font-family: PlusJakarta, Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif !default; sans-serif !default;
$heading-font-family: $base-font-family !default; $heading-font-family: $base-font-family !default;
$base-font-size: 16px !default; $base-font-size: 14px !default;
$base-line-height: 1.5 !default; $base-line-height: 1.5 !default;
$heading-line-height: 1.2 !default; $heading-line-height: 1.2 !default;
@@ -1,30 +1,30 @@
// Font sizes // Font sizes
$font-size-nano: 0.5rem; $font-size-nano: 0.8rem;
$font-size-micro: 0.675rem; $font-size-micro: 1.0rem;
$font-size-mini: 0.75rem; $font-size-mini: 1.2rem;
$font-size-small: 0.875rem; $font-size-small: 1.4rem;
$font-size-default: 1rem; $font-size-default: 1.6rem;
$font-size-medium: 1.125rem; $font-size-medium: 1.8rem;
$font-size-large: 1.375rem; $font-size-large: 2.2rem;
$font-size-big: 1.5rem; $font-size-big: 2.4rem;
$font-size-bigger: 1.75rem; $font-size-bigger: 3.0rem;
$font-size-mega: 2.125rem; $font-size-mega: 3.4rem;
$font-size-giga: 2.5rem; $font-size-giga: 4.0rem;
// spaces // spaces
$zero: 0; $zero: 0;
$space-micro: 0.125rem; $space-micro: 0.2rem;
$space-smaller: 0.25rem; $space-smaller: 0.4rem;
$space-small: 0.5rem; $space-small: 0.8rem;
$space-one: 0.675rem; $space-one: 1rem;
$space-slab: 0.75rem; $space-slab: 1.2rem;
$space-normal: 1rem; $space-normal: 1.6rem;
$space-two: 1.25rem; $space-two: 2.0rem;
$space-medium: 1.5rem; $space-medium: 2.4rem;
$space-large: 2rem; $space-large: 3.2rem;
$space-larger: 3rem; $space-larger: 4.8rem;
$space-jumbo: 4rem; $space-jumbo: 6.4rem;
$space-mega: 6.25rem; $space-mega: 10.0rem;
// font-weight // font-weight
$font-weight-feather: 100; $font-weight-feather: 100;
@@ -86,5 +86,8 @@ $swift-ease-out-duration: .4s !default;
$swift-ease-out-timing-function: cubic-bezier(.25, .8, .25, 1) !default; $swift-ease-out-timing-function: cubic-bezier(.25, .8, .25, 1) !default;
$swift-ease-out: all $swift-ease-out-duration $swift-ease-out-timing-function !default; $swift-ease-out: all $swift-ease-out-duration $swift-ease-out-timing-function !default;
// Ionicons
$ionicons-font-path: '~ionicons/fonts';
// Transitions // Transitions
$transition-ease-in: all 0.250s ease-in; $transition-ease-in: all 0.250s ease-in;
@@ -25,9 +25,7 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
build_contact_inbox build_contact_inbox
build_message build_message
end end
rescue Koala::Facebook::AuthenticationError => e rescue Koala::Facebook::AuthenticationError
Rails.logger.warn("Facebook authentication error for inbox: #{@inbox.id} with error: #{e.message}")
Rails.logger.error e
@inbox.channel.authorization_error! @inbox.channel.authorization_error!
rescue StandardError => e rescue StandardError => e
ChatwootExceptionTracker.new(e, account: @inbox.account).capture_exception ChatwootExceptionTracker.new(e, account: @inbox.account).capture_exception
@@ -110,15 +108,11 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
} }
end end
# rubocop:disable Metrics/AbcSize
# rubocop:disable Metrics/MethodLength
def contact_params def contact_params
begin begin
k = Koala::Facebook::API.new(@inbox.channel.page_access_token) if @inbox.facebook? k = Koala::Facebook::API.new(@inbox.channel.page_access_token) if @inbox.facebook?
result = k.get_object(@sender_id) || {} result = k.get_object(@sender_id) || {}
rescue Koala::Facebook::AuthenticationError => e rescue Koala::Facebook::AuthenticationError
Rails.logger.warn("Facebook authentication error for inbox: #{@inbox.id} with error: #{e.message}")
Rails.logger.error e
@inbox.channel.authorization_error! @inbox.channel.authorization_error!
raise raise
rescue Koala::Facebook::ClientError => e rescue Koala::Facebook::ClientError => e
@@ -136,6 +130,4 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
end end
process_contact_params_result(result) process_contact_params_result(result)
end end
# rubocop:enable Metrics/AbcSize
# rubocop:enable Metrics/MethodLength
end end
@@ -20,9 +20,7 @@ class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder
ActiveRecord::Base.transaction do ActiveRecord::Base.transaction do
build_message build_message
end end
rescue Koala::Facebook::AuthenticationError => e rescue Koala::Facebook::AuthenticationError
Rails.logger.warn("Instagram authentication error for inbox: #{@inbox.id} with error: #{e.message}")
Rails.logger.error e
@inbox.channel.authorization_error! @inbox.channel.authorization_error!
raise raise
rescue StandardError => e rescue StandardError => e
@@ -48,10 +46,6 @@ class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder
@outgoing_echo ? recipient_id : sender_id @outgoing_echo ? recipient_id : sender_id
end end
def message_is_unsupported?
message[:is_unsupported].present? && @messaging[:message][:is_unsupported] == true
end
def sender_id def sender_id
@messaging[:sender][:id] @messaging[:sender][:id]
end end
@@ -82,10 +76,6 @@ class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder
message[:reply_to][:story] if message[:reply_to].present? && message[:reply_to][:story].present? message[:reply_to][:story] if message[:reply_to].present? && message[:reply_to][:story].present?
end end
def message_reply_attributes
message[:reply_to][:mid] if message[:reply_to].present? && message[:reply_to][:mid].present?
end
def build_message def build_message
return if @outgoing_echo && already_sent_from_chatwoot? return if @outgoing_echo && already_sent_from_chatwoot?
return if message_content.blank? && all_unsupported_files? return if message_content.blank? && all_unsupported_files?
@@ -122,20 +112,14 @@ class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder
end end
def message_params def message_params
params = { {
account_id: conversation.account_id, account_id: conversation.account_id,
inbox_id: conversation.inbox_id, inbox_id: conversation.inbox_id,
message_type: message_type, message_type: message_type,
source_id: message_identifier, source_id: message_identifier,
content: message_content, content: message_content,
sender: @outgoing_echo ? nil : contact, sender: @outgoing_echo ? nil : contact
content_attributes: {
in_reply_to_external_id: message_reply_attributes
}
} }
params[:content_attributes][:is_unsupported] = true if message_is_unsupported?
params
end end
def already_sent_from_chatwoot? def already_sent_from_chatwoot?
+4 -37
View File
@@ -9,11 +9,11 @@ class Messages::MessageBuilder
@user = user @user = user
@message_type = params[:message_type] || 'outgoing' @message_type = params[:message_type] || 'outgoing'
@attachments = params[:attachments] @attachments = params[:attachments]
@automation_rule = content_attributes&.dig(:automation_rule_id) @automation_rule = @params&.dig(:content_attributes, :automation_rule_id)
return unless params.instance_of?(ActionController::Parameters) return unless params.instance_of?(ActionController::Parameters)
@in_reply_to = content_attributes&.dig(:in_reply_to) @in_reply_to = params.to_unsafe_h&.dig(:content_attributes, :in_reply_to)
@items = content_attributes&.dig(:items) @items = params.to_unsafe_h&.dig(:content_attributes, :items)
end end
def perform def perform
@@ -26,38 +26,6 @@ class Messages::MessageBuilder
private private
# Extracts content attributes from the given params.
# - Converts ActionController::Parameters to a regular hash if needed.
# - Attempts to parse a JSON string if content is a string.
# - Returns an empty hash if content is not present, if there's a parsing error, or if it's an unexpected type.
def content_attributes
params = convert_to_hash(@params)
content_attributes = params.fetch(:content_attributes, {})
return parse_json(content_attributes) if content_attributes.is_a?(String)
return content_attributes if content_attributes.is_a?(Hash)
{}
end
# Converts the given object to a hash.
# If it's an instance of ActionController::Parameters, converts it to an unsafe hash.
# Otherwise, returns the object as-is.
def convert_to_hash(obj)
return obj.to_unsafe_h if obj.instance_of?(ActionController::Parameters)
obj
end
# Attempts to parse a string as JSON.
# If successful, returns the parsed hash with symbolized names.
# If unsuccessful, returns nil.
def parse_json(content)
JSON.parse(content, symbolize_names: true)
rescue JSON::ParserError
{}
end
def process_attachments def process_attachments
return if @attachments.blank? return if @attachments.blank?
@@ -149,8 +117,7 @@ class Messages::MessageBuilder
content_type: @params[:content_type], content_type: @params[:content_type],
items: @items, items: @items,
in_reply_to: @in_reply_to, in_reply_to: @in_reply_to,
echo_id: @params[:echo_id], echo_id: @params[:echo_id]
source_id: @params[:source_id]
}.merge(external_created_at).merge(automation_rule_id).merge(campaign_id).merge(template_params) }.merge(external_created_at).merge(automation_rule_id).merge(campaign_id).merge(template_params)
end end
end end
+6 -8
View File
@@ -1,20 +1,22 @@
class NotificationBuilder class NotificationBuilder
pattr_initialize [:notification_type!, :user!, :account!, :primary_actor!, :secondary_actor] pattr_initialize [:notification_type!, :user!, :account!, :primary_actor!]
def perform def perform
return unless user_subscribed_to_notification?
build_notification build_notification
end end
private private
def current_user def secondary_actor
Current.user Current.user
end end
def user_subscribed_to_notification? def user_subscribed_to_notification?
notification_setting = user.notification_settings.find_by(account_id: account.id) notification_setting = user.notification_settings.find_by(account_id: account.id)
# added for the case where an assignee might be removed from the account but remains in conversation # added for the case where an assignee might be removed from the account but remains in conversation
return false if notification_setting.blank? return if notification_setting.blank?
return true if notification_setting.public_send("email_#{notification_type}?") return true if notification_setting.public_send("email_#{notification_type}?")
return true if notification_setting.public_send("push_#{notification_type}?") return true if notification_setting.public_send("push_#{notification_type}?")
@@ -23,15 +25,11 @@ class NotificationBuilder
end end
def build_notification def build_notification
# Create conversation_creation notification only if user is subscribed to it
return if notification_type == 'conversation_creation' && !user_subscribed_to_notification?
user.notifications.create!( user.notifications.create!(
notification_type: notification_type, notification_type: notification_type,
account: account, account: account,
primary_actor: primary_actor, primary_actor: primary_actor,
# secondary_actor is secondary_actor if present, else current_user secondary_actor: secondary_actor
secondary_actor: secondary_actor || current_user
) )
end end
end end
-8
View File
@@ -46,14 +46,6 @@ class V2::ReportBuilder
} }
end end
def short_summary
{
conversations_count: conversations.count,
avg_first_response_time: avg_first_response_time_summary,
avg_resolution_time: avg_resolution_time_summary
}
end
def conversation_metrics def conversation_metrics
if params[:type].equal?(:account) if params[:type].equal?(:account)
live_conversations live_conversations
@@ -10,18 +10,11 @@ class Api::V1::Accounts::AgentBotsController < Api::V1::Accounts::BaseController
def show; end def show; end
def create def create
@agent_bot = Current.account.agent_bots.create!(permitted_params.except(:avatar_url)) @agent_bot = Current.account.agent_bots.create!(permitted_params)
process_avatar_from_url
end end
def update def update
@agent_bot.update!(permitted_params.except(:avatar_url)) @agent_bot.update!(permitted_params)
process_avatar_from_url
end
def avatar
@agent_bot.avatar.purge if @agent_bot.avatar.attached?
@agent_bot
end end
def destroy def destroy
@@ -37,10 +30,6 @@ class Api::V1::Accounts::AgentBotsController < Api::V1::Accounts::BaseController
end end
def permitted_params def permitted_params
params.permit(:name, :description, :outgoing_url, :avatar, :avatar_url, :bot_type, bot_config: [:csml_content]) params.permit(:name, :description, :outgoing_url, :bot_type, bot_config: [:csml_content])
end
def process_avatar_from_url
::Avatar::AvatarFromUrlJob.perform_later(@agent_bot, params[:avatar_url]) if params[:avatar_url].present?
end end
end end
@@ -10,7 +10,7 @@ class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
@articles_count = @all_articles.count @articles_count = @all_articles.count
@articles = if list_params[:category_slug].present? @articles = if list_params[:category_slug].present?
@all_articles.order_by_position.page(@current_page) @all_articles.order_by_position.page(@current_page).per(50)
else else
@all_articles.order_by_updated_at.page(@current_page) @all_articles.order_by_updated_at.page(@current_page)
end end
@@ -48,7 +48,7 @@ class Api::V1::Accounts::CategoriesController < Api::V1::Accounts::BaseControlle
def category_params def category_params
params.require(:category).permit( params.require(:category).permit(
:name, :description, :position, :slug, :locale, :icon, :parent_category_id, :associated_category_id :name, :description, :position, :slug, :locale, :parent_category_id, :associated_category_id
) )
end end
@@ -83,14 +83,14 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
@contact = Current.account.contacts.new(permitted_params.except(:avatar_url)) @contact = Current.account.contacts.new(permitted_params.except(:avatar_url))
@contact.save! @contact.save!
@contact_inbox = build_contact_inbox @contact_inbox = build_contact_inbox
process_avatar_from_url process_avatar
end end
end end
def update def update
@contact.assign_attributes(contact_update_params) @contact.assign_attributes(contact_update_params)
@contact.save! @contact.save!
process_avatar_from_url process_avatar if permitted_params[:avatar].present? || permitted_params[:avatar_url].present?
end end
def destroy def destroy
@@ -174,7 +174,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
@contact = Current.account.contacts.includes(contact_inboxes: [:inbox]).find(params[:id]) @contact = Current.account.contacts.includes(contact_inboxes: [:inbox]).find(params[:id])
end end
def process_avatar_from_url def process_avatar
::Avatar::AvatarFromUrlJob.perform_later(@contact, params[:avatar_url]) if params[:avatar_url].present? ::Avatar::AvatarFromUrlJob.perform_later(@contact, params[:avatar_url]) if params[:avatar_url].present?
end end
@@ -14,8 +14,7 @@ class Api::V1::Accounts::Conversations::AssignmentsController < Api::V1::Account
def set_agent def set_agent
@agent = Current.account.users.find_by(id: params[:assignee_id]) @agent = Current.account.users.find_by(id: params[:assignee_id])
@conversation.assignee = @agent @conversation.update_assignee(@agent)
@conversation.save!
render_agent render_agent
end end
@@ -18,15 +18,6 @@ class Api::V1::Accounts::Conversations::MessagesController < Api::V1::Accounts::
end end
end end
def retry
return if message.blank?
message.update!(status: :sent, content_attributes: {})
::SendReplyJob.perform_later(message.id)
rescue StandardError => e
render_could_not_create_error(e.message)
end
def translate def translate
return head :ok if already_translated_content_available? return head :ok if already_translated_content_available?
@@ -60,26 +60,13 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
end end
def toggle_status def toggle_status
# FIXME: move this logic into a service object if params[:status].present?
if pending_to_open_by_bot?
@conversation.bot_handoff!
elsif params[:status].present?
set_conversation_status set_conversation_status
@status = @conversation.save! @status = @conversation.save!
else else
@status = @conversation.toggle_status @status = @conversation.toggle_status
end end
assign_conversation if should_assign_conversation? assign_conversation if @conversation.status == 'open' && Current.user.is_a?(User) && Current.user&.agent?
end
def pending_to_open_by_bot?
return false unless Current.user.is_a?(AgentBot)
@conversation.status == 'pending' && params[:status] == 'open'
end
def should_assign_conversation?
@conversation.status == 'open' && Current.user.is_a?(User) && Current.user&.agent?
end end
def toggle_priority def toggle_priority
@@ -123,8 +110,8 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
end end
def assign_conversation def assign_conversation
@conversation.assignee = current_user @agent = Current.account.users.find(current_user.id)
@conversation.save! @conversation.update_assignee(@agent)
end end
def conversation def conversation
@@ -39,8 +39,6 @@ class Api::V1::Accounts::CustomAttributeDefinitionsController < Api::V1::Account
:attribute_display_type, :attribute_display_type,
:attribute_key, :attribute_key,
:attribute_model, :attribute_model,
:regex_pattern,
:regex_cue,
attribute_values: [] attribute_values: []
) )
end end
@@ -1,15 +1,14 @@
class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseController class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseController
RESULTS_PER_PAGE = 15 RESULTS_PER_PAGE = 15
include DateRangeHelper
before_action :fetch_notification, only: [:update, :destroy, :snooze] before_action :fetch_notification, only: [:update]
before_action :set_primary_actor, only: [:read_all] before_action :set_primary_actor, only: [:read_all]
before_action :set_current_page, only: [:index] before_action :set_current_page, only: [:index]
def index def index
@unread_count = notification_finder.unread_count @unread_count = current_user.notifications.where(account_id: current_account.id, read_at: nil).count
@notifications = notification_finder.perform @count = notifications.count
@count = notification_finder.count @notifications = notifications.page(@current_page).per(RESULTS_PER_PAGE)
end end
def read_all def read_all
@@ -29,21 +28,11 @@ class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseContro
render json: @notification render json: @notification
end end
def destroy
@notification.destroy
head :ok
end
def unread_count def unread_count
@unread_count = notification_finder.unread_count @unread_count = current_user.notifications.where(account_id: current_account.id, read_at: nil).count
render json: @unread_count render json: @unread_count
end end
def snooze
@notification.update(snoozed_until: parse_date_time(params[:snoozed_until].to_s)) if params[:snoozed_until]
render json: @notification
end
private private
def set_primary_actor def set_primary_actor
@@ -61,7 +50,7 @@ class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseContro
@current_page = params[:page] || 1 @current_page = params[:page] || 1
end end
def notification_finder def notifications
@notification_finder ||= NotificationFinder.new(Current.user, Current.account, params) @notifications ||= current_user.notifications.where(account_id: current_account.id)
end end
end end
@@ -47,11 +47,6 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
head :ok head :ok
end end
def logo
@portal.logo.purge if @portal.logo.attached?
head :ok
end
def process_attached_logo def process_attached_logo
blob_id = params[:blob_id] blob_id = params[:blob_id]
blob = ActiveStorage::Blob.find_by(id: blob_id) blob = ActiveStorage::Blob.find_by(id: blob_id)
@@ -79,9 +79,6 @@ class Api::V1::Widget::BaseController < ApplicationController
sender: @contact, sender: @contact,
content: permitted_params[:message][:content], content: permitted_params[:message][:content],
inbox_id: conversation.inbox_id, inbox_id: conversation.inbox_id,
content_attributes: {
in_reply_to: permitted_params[:message][:reply_to]
},
echo_id: permitted_params[:message][:echo_id], echo_id: permitted_params[:message][:echo_id],
message_type: :incoming message_type: :incoming
} }
@@ -28,7 +28,7 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
conversation.contact_last_seen_at = DateTime.now.utc conversation.contact_last_seen_at = DateTime.now.utc
conversation.save! conversation.save!
::Conversations::UpdateMessageStatusJob.perform_later(conversation.id, conversation.contact_last_seen_at) ::Conversations::MarkMessagesAsReadJob.perform_later(conversation)
head :ok head :ok
end end
@@ -64,7 +64,7 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
def permitted_params def permitted_params
# timestamp parameter is used in create conversation method # timestamp parameter is used in create conversation method
params.permit(:id, :before, :after, :website_token, contact: [:name, :email], message: [:content, :referer_url, :timestamp, :echo_id, :reply_to]) params.permit(:id, :before, :after, :website_token, contact: [:name, :email], message: [:content, :referer_url, :timestamp, :echo_id])
end end
def set_message def set_message
@@ -14,14 +14,7 @@ module AccessTokenAuthHelper
render_unauthorized('Invalid Access Token') && return if @access_token.blank? render_unauthorized('Invalid Access Token') && return if @access_token.blank?
@resource = @access_token.owner @resource = @access_token.owner
Current.user = @resource if allowed_current_user_type?(@resource) Current.user = @resource if current_user.is_a?(User)
end
def allowed_current_user_type?(resource)
return true if resource.is_a?(User)
return true if resource.is_a?(AgentBot)
false
end end
def validate_bot_access_token! def validate_bot_access_token!
@@ -9,15 +9,13 @@ class Platform::Api::V1::AgentBotsController < PlatformController
def show; end def show; end
def create def create
@resource = AgentBot.new(agent_bot_params.except(:avatar_url)) @resource = AgentBot.new(agent_bot_params)
@resource.save! @resource.save!
process_avatar_from_url
@platform_app.platform_app_permissibles.find_or_create_by(permissible: @resource) @platform_app.platform_app_permissibles.find_or_create_by(permissible: @resource)
end end
def update def update
@resource.update!(agent_bot_params.except(:avatar_url)) @resource.update!(agent_bot_params)
process_avatar_from_url
end end
def destroy def destroy
@@ -25,11 +23,6 @@ class Platform::Api::V1::AgentBotsController < PlatformController
head :ok head :ok
end end
def avatar
@resource.avatar.purge if @resource.avatar.attached?
@resource
end
private private
def set_resource def set_resource
@@ -37,10 +30,6 @@ class Platform::Api::V1::AgentBotsController < PlatformController
end end
def agent_bot_params def agent_bot_params
params.permit(:name, :description, :account_id, :outgoing_url, :avatar, :avatar_url) params.permit(:name, :description, :account_id, :outgoing_url)
end
def process_avatar_from_url
::Avatar::AvatarFromUrlJob.perform_later(@resource, params[:avatar_url]) if params[:avatar_url].present?
end end
end end
@@ -23,7 +23,7 @@ class Public::Api::V1::Inboxes::ConversationsController < Public::Api::V1::Inbox
def update_last_seen def update_last_seen
@conversation.contact_last_seen_at = DateTime.now.utc @conversation.contact_last_seen_at = DateTime.now.utc
@conversation.save! @conversation.save!
::Conversations::UpdateMessageStatusJob.perform_later(@conversation.id, @conversation.contact_last_seen_at) ::Conversations::MarkMessagesAsReadJob.perform_later(@conversation)
head :ok head :ok
end end
@@ -39,6 +39,10 @@ class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::B
) )
end end
def portal
@portal ||= Portal.find_by!(slug: permitted_params[:slug], archived: false)
end
def list_params def list_params
params.permit(:query, :locale, :sort, :status) params.permit(:query, :locale, :sort, :status)
end end
@@ -1,7 +1,6 @@
class Public::Api::V1::Portals::BaseController < PublicController class Public::Api::V1::Portals::BaseController < PublicController
before_action :show_plain_layout before_action :show_plain_layout
before_action :set_color_scheme before_action :set_color_scheme
before_action :set_global_config
around_action :set_locale around_action :set_locale
after_action :allow_iframe_requests after_action :allow_iframe_requests
@@ -12,11 +11,11 @@ class Public::Api::V1::Portals::BaseController < PublicController
end end
def set_color_scheme def set_color_scheme
@theme_from_params = params[:theme] if %w[dark light].include?(params[:theme]) @theme = if %w[dark light].include?(params[:theme])
end params[:theme]
else
def portal ''
@portal ||= Portal.find_by!(slug: params[:slug], archived: false) end
end end
def set_locale(&) def set_locale(&)
@@ -41,8 +40,6 @@ class Public::Api::V1::Portals::BaseController < PublicController
def switch_locale_with_article(&) def switch_locale_with_article(&)
article = Article.find_by(slug: params[:article_slug]) article = Article.find_by(slug: params[:article_slug])
Rails.logger.info "Article: not found for slug: #{params[:article_slug]}"
render_404 && return if article.blank?
@locale = if article.category.present? @locale = if article.category.present?
article.category.locale article.category.locale
@@ -56,13 +53,4 @@ class Public::Api::V1::Portals::BaseController < PublicController
def allow_iframe_requests def allow_iframe_requests
response.headers.delete('X-Frame-Options') if @is_plain_layout_enabled response.headers.delete('X-Frame-Options') if @is_plain_layout_enabled
end end
def render_404
portal
render 'public/api/v1/portals/error/404', status: :not_found
end
def set_global_config
@global_config = GlobalConfig.get('LOGO_THUMBNAIL', 'BRAND_NAME', 'BRAND_URL')
end
end end
@@ -13,9 +13,10 @@ class Public::Api::V1::Portals::CategoriesController < Public::Api::V1::Portals:
private private
def set_category def set_category
@category = @portal.categories.find_by(locale: params[:locale], slug: params[:category_slug]) @category = @portal.categories.find_by!(locale: params[:locale], slug: params[:category_slug])
end
Rails.logger.info "Category: not found for slug: #{params[:category_slug]}" def portal
render_404 && return if @category.blank? @portal ||= Portal.find_by!(slug: params[:slug], archived: false)
end end
end end
@@ -1,41 +1,21 @@
class SuperAdmin::AppConfigsController < SuperAdmin::ApplicationController class SuperAdmin::AppConfigsController < SuperAdmin::ApplicationController
before_action :set_config
before_action :allowed_configs
def show def show
@allowed_configs = %w[FB_APP_ID FB_VERIFY_TOKEN FB_APP_SECRET]
# ref: https://github.com/rubocop/rubocop/issues/7767 # ref: https://github.com/rubocop/rubocop/issues/7767
# rubocop:disable Style/HashTransformValues # rubocop:disable Style/HashTransformValues
@app_config = InstallationConfig.where(name: @allowed_configs) @fb_config = InstallationConfig.where(name: @allowed_configs)
.pluck(:name, :serialized_value) .pluck(:name, :serialized_value)
.map { |name, serialized_value| [name, serialized_value['value']] } .map { |name, serialized_value| [name, serialized_value['value']] }
.to_h .to_h
# rubocop:enable Style/HashTransformValues # rubocop:enable Style/HashTransformValues
@installation_configs = ConfigLoader.new.general_configs.each_with_object({}) do |config_hash, result|
result[config_hash['name']] = config_hash.except('name')
end
end end
def create def create
params['app_config'].each do |key, value| params['app_config'].each do |key, value|
next unless @allowed_configs.include?(key)
i = InstallationConfig.where(name: key).first_or_create(value: value, locked: false) i = InstallationConfig.where(name: key).first_or_create(value: value, locked: false)
i.value = value i.value = value
i.save! i.save!
end end
# rubocop:disable Rails/I18nLocaleTexts redirect_to super_admin_app_config_url
redirect_to super_admin_settings_path, notice: 'App Configs updated successfully'
# rubocop:enable Rails/I18nLocaleTexts
end
private
def set_config
@config = params[:config]
end
def allowed_configs
@allowed_configs = %w[FB_APP_ID FB_VERIFY_TOKEN FB_APP_SECRET]
end end
end end
SuperAdmin::AppConfigsController.prepend_mod_with('SuperAdmin::AppConfigsController')
@@ -20,13 +20,4 @@ class SuperAdmin::ApplicationController < Administrate::ApplicationController
params.fetch(resource_name, {}).fetch(:direction, 'desc') params.fetch(resource_name, {}).fetch(:direction, 'desc')
) )
end end
private
def invalid_action_perfomed
# rubocop:disable Rails/I18nLocaleTexts
flash[:error] = 'Invalid action performed'
# rubocop:enable Rails/I18nLocaleTexts
redirect_back(fallback_location: root_path)
end
end end
@@ -27,8 +27,7 @@ class SuperAdmin::Devise::SessionsController < Devise::SessionsController
true true
rescue StandardError => e rescue StandardError => e
Rails.logger.error e.message @error_message = e.message
@error_message = 'Invalid credentials. Please try again.'
false false
end end
end end
@@ -1,5 +1,4 @@
class SuperAdmin::InstallationConfigsController < SuperAdmin::ApplicationController class SuperAdmin::InstallationConfigsController < SuperAdmin::ApplicationController
rescue_from ActiveRecord::RecordNotUnique, :with => :invalid_action_perfomed
# Overwrite any of the RESTful controller actions to implement custom behavior # Overwrite any of the RESTful controller actions to implement custom behavior
# For example, you may want to send an email after a foo is updated. # For example, you may want to send an email after a foo is updated.
# #
@@ -1,10 +0,0 @@
class SuperAdmin::SettingsController < SuperAdmin::ApplicationController
def show; end
def refresh
Internal::CheckNewVersionsJob.perform_now
# rubocop:disable Rails/I18nLocaleTexts
redirect_to super_admin_settings_path, notice: 'Instance status refreshed'
# rubocop:enable Rails/I18nLocaleTexts
end
end
+4
View File
@@ -30,7 +30,11 @@ class AccessTokenDashboard < Administrate::BaseDashboard
# SHOW_PAGE_ATTRIBUTES # SHOW_PAGE_ATTRIBUTES
# an array of attributes that will be displayed on the model's show page. # an array of attributes that will be displayed on the model's show page.
SHOW_PAGE_ATTRIBUTES = %i[ SHOW_PAGE_ATTRIBUTES = %i[
owner
id
token token
created_at
updated_at
].freeze ].freeze
# FORM_ATTRIBUTES # FORM_ATTRIBUTES
-1
View File
@@ -46,7 +46,6 @@ class AgentBotDashboard < Administrate::BaseDashboard
name name
description description
outgoing_url outgoing_url
access_token
].freeze ].freeze
# FORM_ATTRIBUTES # FORM_ATTRIBUTES
-1
View File
@@ -32,7 +32,6 @@ class PlatformAppDashboard < Administrate::BaseDashboard
name name
created_at created_at
updated_at updated_at
access_token
].freeze ].freeze
# FORM_ATTRIBUTES # FORM_ATTRIBUTES
+1 -3
View File
@@ -36,8 +36,7 @@ class UserDashboard < Administrate::BaseDashboard
updated_at: Field::DateTime, updated_at: Field::DateTime,
pubsub_token: Field::String, pubsub_token: Field::String,
type: Field::Select.with_options(collection: [nil, 'SuperAdmin']), type: Field::Select.with_options(collection: [nil, 'SuperAdmin']),
accounts: CountField, accounts: CountField
access_token: Field::HasOne
}.freeze }.freeze
# COLLECTION_ATTRIBUTES # COLLECTION_ATTRIBUTES
@@ -68,7 +67,6 @@ class UserDashboard < Administrate::BaseDashboard
updated_at updated_at
confirmed_at confirmed_at
account_users account_users
access_token
].freeze ].freeze
# FORM_ATTRIBUTES # FORM_ATTRIBUTES
-5
View File
@@ -18,9 +18,4 @@ class ContactDrop < BaseDrop
def last_name def last_name
@obj.try(:name).try(:split).try(:last).try(:capitalize) if @obj.try(:name).try(:split).try(:size) > 1 @obj.try(:name).try(:split).try(:last).try(:capitalize) if @obj.try(:name).try(:split).try(:size) > 1
end end
def custom_attribute
custom_attributes = @obj.try(:custom_attributes) || {}
custom_attributes.transform_keys(&:to_s)
end
end end
-5
View File
@@ -19,11 +19,6 @@ class ConversationDrop < BaseDrop
end end
end end
def custom_attribute
custom_attributes = @obj.try(:custom_attributes) || {}
custom_attributes.transform_keys(&:to_s)
end
private private
def message_sender_name(sender) def message_sender_name(sender)
+8 -17
View File
@@ -3,21 +3,13 @@ class ConversationFinder
DEFAULT_STATUS = 'open'.freeze DEFAULT_STATUS = 'open'.freeze
SORT_OPTIONS = { SORT_OPTIONS = {
'last_activity_at_asc' => %w[sort_on_last_activity_at asc], latest: 'latest',
'last_activity_at_desc' => %w[sort_on_last_activity_at desc], sort_on_created_at: 'sort_on_created_at',
'created_at_asc' => %w[sort_on_created_at asc], last_user_message_at: 'last_user_message_at',
'created_at_desc' => %w[sort_on_created_at desc], sort_on_priority: 'sort_on_priority',
'priority_asc' => %w[sort_on_priority asc], sort_on_waiting_since: 'sort_on_waiting_since'
'priority_desc' => %w[sort_on_priority desc],
'waiting_since_asc' => %w[sort_on_waiting_since asc],
'waiting_since_desc' => %w[sort_on_waiting_since desc],
# To be removed in v3.5.0
'latest' => %w[sort_on_last_activity_at desc],
'sort_on_created_at' => %w[sort_on_created_at asc],
'sort_on_priority' => %w[sort_on_priority desc],
'sort_on_waiting_since' => %w[sort_on_waiting_since asc]
}.with_indifferent_access }.with_indifferent_access
# assumptions # assumptions
# inbox_id if not given, take from all conversations, else specific to inbox # inbox_id if not given, take from all conversations, else specific to inbox
# assignee_type if not given, take 'all' # assignee_type if not given, take 'all'
@@ -167,8 +159,7 @@ class ConversationFinder
@conversations = @conversations.includes( @conversations = @conversations.includes(
:taggings, :inbox, { assignee: { avatar_attachment: [:blob] } }, { contact: { avatar_attachment: [:blob] } }, :team, :contact_inbox :taggings, :inbox, { assignee: { avatar_attachment: [:blob] } }, { contact: { avatar_attachment: [:blob] } }, :team, :contact_inbox
) )
sort_by = SORT_OPTIONS[params[:sort_by]] || SORT_OPTIONS['latest']
sort_by, sort_order = SORT_OPTIONS[params[:sort_by]] || SORT_OPTIONS['last_activity_at_desc'] @conversations.send(sort_by).page(current_page)
@conversations.send(sort_by, sort_order).page(current_page).per(ENV.fetch('CONVERSATION_RESULTS_PER_PAGE', '25').to_i)
end end
end end
-47
View File
@@ -1,47 +0,0 @@
class NotificationFinder
attr_reader :current_user, :current_account, :params
RESULTS_PER_PAGE = 15
def initialize(current_user, current_account, params = {})
@current_user = current_user
@current_account = current_account
@params = params
set_up
end
def perform
notifications
end
def unread_count
@notifications.where(read_at: nil).count
end
def count
@notifications.count
end
private
def set_up
find_all_notifications
filter_by_status
end
def find_all_notifications
@notifications = current_user.notifications.where(account_id: @current_account.id)
end
def filter_by_status
@notifications = @notifications.where('snoozed_until > ?', DateTime.now.utc) if params[:status] == 'snoozed'
end
def current_page
params[:page] || 1
end
def notifications
@notifications.page(current_page).per(RESULTS_PER_PAGE).order(last_activity_at: :desc)
end
end
+1 -1
View File
@@ -53,7 +53,7 @@ module Api::V1::InboxesHelper
rescue StandardError => e rescue StandardError => e
raise StandardError, e.message raise StandardError, e.message
ensure ensure
Rails.logger.error "[Api::V1::InboxesHelper] check_imap_connection failed with #{e.message}" if e.present? ChatwootExceptionTracker.new(e).capture_exception if e.present?
end end
def check_smtp_connection(channel_data, smtp) def check_smtp_connection(channel_data, smtp)
@@ -37,7 +37,7 @@ module Api::V2::Accounts::ReportsHelper
business_hours: ActiveModel::Type::Boolean.new.cast(params[:business_hours]) business_hours: ActiveModel::Type::Boolean.new.cast(params[:business_hours])
} }
) )
).short_summary ).summary
end end
private private
+3 -63
View File
@@ -1,71 +1,11 @@
module PortalHelper module PortalHelper
def generate_portal_bg_color(portal_color, theme) def generate_portal_bg_color(portal_color, theme)
base_color = theme == 'dark' ? 'black' : 'white' base_color = theme == 'dark' ? 'black' : 'white'
"color-mix(in srgb, #{portal_color} 20%, #{base_color})" "color-mix(in srgb, #{portal_color} 10%, #{base_color})"
end end
def generate_portal_bg(portal_color, theme) def generate_portal_bg(portal_color, theme)
bg_image = theme == 'dark' ? 'hexagon-dark.svg' : 'hexagon-light.svg' bg_image = theme == 'dark' ? 'grid_dark.svg' : 'grid.svg'
"url(/assets/images/hc/#{bg_image}) #{generate_portal_bg_color(portal_color, theme)}" "background: url(/assets/images/hc/#{bg_image}) #{generate_portal_bg_color(portal_color, theme)}"
end
def generate_gradient_to_bottom(theme)
base_color = theme == 'dark' ? '#151718' : 'white'
"linear-gradient(to bottom, transparent, #{base_color})"
end
def generate_portal_hover_color(portal_color, theme)
base_color = theme == 'dark' ? '#1B1B1B' : '#F9F9F9'
"color-mix(in srgb, #{portal_color} 5%, #{base_color})"
end
def language_name(locale)
language_map = YAML.load_file(Rails.root.join('config/languages/language_map.yml'))
language_map[locale] || locale
end
def theme_query_string(theme)
theme.present? && theme != 'system' ? "?theme=#{theme}" : ''
end
def generate_home_link(portal_slug, portal_locale, theme, is_plain_layout_enabled)
if is_plain_layout_enabled
"/hc/#{portal_slug}/#{portal_locale}#{theme_query_string(theme)}"
else
"/hc/#{portal_slug}/#{portal_locale}"
end
end
def generate_category_link(params)
portal_slug = params[:portal_slug]
category_locale = params[:category_locale]
category_slug = params[:category_slug]
theme = params[:theme]
is_plain_layout_enabled = params[:is_plain_layout_enabled]
if is_plain_layout_enabled
"/hc/#{portal_slug}/#{category_locale}/categories/#{category_slug}#{theme_query_string(theme)}"
else
"/hc/#{portal_slug}/#{category_locale}/categories/#{category_slug}"
end
end
def generate_article_link(portal_slug, article_slug, theme, is_plain_layout_enabled)
if is_plain_layout_enabled
"/hc/#{portal_slug}/articles/#{article_slug}#{theme_query_string(theme)}"
else
"/hc/#{portal_slug}/articles/#{article_slug}"
end
end
def render_category_content(content)
ChatwootMarkdownRenderer.new(content).render_markdown_to_plain_text
end
def thumbnail_bg_color(username)
colors = ['#6D95BA', '#A4C3C3', '#E19191']
return colors.sample if username.blank?
colors[username.length % colors.size]
end end
end end
@@ -1,7 +1,6 @@
/* global axios */ /* global axios */
import PortalsAPI from './portals'; import PortalsAPI from './portals';
import { getArticleSearchURL } from 'dashboard/helper/URLHelper.js';
class ArticlesAPI extends PortalsAPI { class ArticlesAPI extends PortalsAPI {
constructor() { constructor() {
@@ -13,31 +12,14 @@ class ArticlesAPI extends PortalsAPI {
portalSlug, portalSlug,
locale, locale,
status, status,
authorId, author_id,
categorySlug, category_slug,
sort,
}) { }) {
const url = getArticleSearchURL({ let baseUrl = `${this.url}/${portalSlug}/articles?page=${pageNumber}&locale=${locale}`;
pageNumber, if (status !== undefined) baseUrl += `&status=${status}`;
portalSlug, if (author_id) baseUrl += `&author_id=${author_id}`;
locale, if (category_slug) baseUrl += `&category_slug=${category_slug}`;
status, return axios.get(baseUrl);
authorId,
categorySlug,
sort,
host: this.url,
});
return axios.get(url);
}
searchArticles({ portalSlug, query }) {
const url = getArticleSearchURL({
portalSlug,
query,
host: this.url,
});
return axios.get(url);
} }
getArticle({ id, portalSlug }) { getArticle({ id, portalSlug }) {
@@ -17,10 +17,6 @@ class PortalsAPI extends ApiClient {
deletePortal(portalSlug) { deletePortal(portalSlug) {
return axios.delete(`${this.url}/${portalSlug}`); return axios.delete(`${this.url}/${portalSlug}`);
} }
deleteLogo(portalSlug) {
return axios.delete(`${this.url}/${portalSlug}/logo`);
}
} }
export default PortalsAPI; export default PortalsAPI;
@@ -26,13 +26,9 @@ export const buildCreatePayload = ({
payload.append('echo_id', echoId); payload.append('echo_id', echoId);
payload.append('cc_emails', ccEmails); payload.append('cc_emails', ccEmails);
payload.append('bcc_emails', bccEmails); payload.append('bcc_emails', bccEmails);
if (toEmails) { if (toEmails) {
payload.append('to_emails', toEmails); payload.append('to_emails', toEmails);
} }
if (contentAttributes) {
payload.append('content_attributes', JSON.stringify(contentAttributes));
}
} else { } else {
payload = { payload = {
content: message, content: message,
@@ -86,12 +82,6 @@ class MessageApi extends ApiClient {
return axios.delete(`${this.url}/${conversationID}/messages/${messageId}`); return axios.delete(`${this.url}/${conversationID}/messages/${messageId}`);
} }
retry(conversationID, messageId) {
return axios.post(
`${this.url}/${conversationID}/messages/${messageId}/retry`
);
}
getPreviousMessages({ conversationId, after, before }) { getPreviousMessages({ conversationId, after, before }) {
const params = { before }; const params = { before };
if (after && Number(after) !== Number(before)) { if (after && Number(after) !== Number(before)) {
@@ -34,7 +34,7 @@ describe('#PortalAPI', () => {
portalSlug: 'room-rental', portalSlug: 'room-rental',
locale: 'en-US', locale: 'en-US',
status: 'published', status: 'published',
authorId: '1', author_id: '1',
}); });
expect(axiosMock.get).toHaveBeenCalledWith( expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v1/portals/room-rental/articles?page=1&locale=en-US&status=published&author_id=1' '/api/v1/portals/room-rental/articles?page=1&locale=en-US&status=published&author_id=1'
@@ -85,33 +85,6 @@ describe('#PortalAPI', () => {
window.axios = originalAxios; window.axios = originalAxios;
}); });
it('#searchArticles', () => {
articlesAPI.searchArticles({
query: 'test',
portalSlug: 'room-rental',
});
expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v1/portals/room-rental/articles?query=test'
);
});
});
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
window.axios = axiosMock;
});
afterEach(() => {
window.axios = originalAxios;
});
it('#updateArticle', () => { it('#updateArticle', () => {
articlesAPI.updateArticle({ articlesAPI.updateArticle({
articleId: 1, articleId: 1,
@@ -50,7 +50,6 @@ describe('#ConversationAPI', () => {
message: 'test content', message: 'test content',
echoId: 12, echoId: 12,
isPrivate: true, isPrivate: true,
contentAttributes: { in_reply_to: 12 },
files: [new Blob(['test-content'], { type: 'application/pdf' })], files: [new Blob(['test-content'], { type: 'application/pdf' })],
}); });
expect(formPayload).toBeInstanceOf(FormData); expect(formPayload).toBeInstanceOf(FormData);
@@ -58,10 +57,6 @@ describe('#ConversationAPI', () => {
expect(formPayload.get('echo_id')).toEqual('12'); expect(formPayload.get('echo_id')).toEqual('12');
expect(formPayload.get('private')).toEqual('true'); expect(formPayload.get('private')).toEqual('true');
expect(formPayload.get('cc_emails')).toEqual(''); expect(formPayload.get('cc_emails')).toEqual('');
expect(formPayload.get('bcc_emails')).toEqual('');
expect(formPayload.get('content_attributes')).toEqual(
'{"in_reply_to":12}'
);
}); });
it('builds object payload if file is not available', () => { it('builds object payload if file is not available', () => {
Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 16 KiB

@@ -1,4 +1,8 @@
@import 'vue2-datepicker/scss/index'; @import '~vue2-datepicker/scss/index';
.mx-datepicker-popup {
@apply z-[99999];
}
.date-picker { .date-picker {
&.no-margin { &.no-margin {
@@ -25,52 +29,10 @@
.mx-input[readonly] { .mx-input[readonly] {
@apply bg-white dark:bg-slate-900 cursor-pointer; @apply bg-white dark:bg-slate-900 cursor-pointer;
} }
.mx-icon-calendar {
@apply dark:text-slate-500;
}
} }
.mx-datepicker-main { .mx-calendar-content .cell:hover {
@apply border-0 bg-white dark:bg-slate-800; @apply bg-slate-75 dark:bg-slate-700 text-slate-900 dark:text-slate-100;
.cell {
&.disabled {
@apply bg-slate-25 dark:bg-slate-900 text-slate-200 dark:text-slate-300;
}
&:hover,
&.hover-in-range,
&.in-range {
@apply bg-slate-75 dark:bg-slate-700 text-slate-900 dark:text-slate-100;
}
}
.mx-time {
@apply border-0 bg-white dark:bg-slate-800;
.mx-time-header {
@apply border-0;
}
.mx-time-item {
&.disabled {
@apply bg-slate-25 dark:bg-slate-900;
}
&:hover {
@apply bg-slate-75 dark:bg-slate-700;
}
}
}
.today {
@apply font-semibold;
}
}
.mx-datepicker-popup {
@apply z-[99999];
} }
.mx-datepicker-inline { .mx-datepicker-inline {
@@ -79,4 +41,24 @@
.mx-calendar { .mx-calendar {
@apply w-full; @apply w-full;
} }
.cell.disabled {
@apply bg-slate-25 dark:bg-slate-900 text-slate-200 dark:text-slate-300;
}
.mx-time-item.disabled {
@apply bg-slate-25 dark:bg-slate-900;
}
.today {
@apply font-semibold;
}
.mx-datepicker-main {
@apply border-0 bg-white dark:bg-slate-800;
}
.mx-time-header {
@apply border-0;
}
} }
@@ -1,4 +1,4 @@
@import 'dashboard/assets/scss/variables'; @import '~dashboard/assets/scss/variables';
.formulate-input { .formulate-input {
.formulate-input-errors { .formulate-input-errors {
@@ -16,23 +16,3 @@
width: 100%; width: 100%;
} }
} }
.integration-hooks {
.formulate-input[data-type='checkbox'] {
.formulate-input-wrapper {
@apply flex;
.formulate-input-element {
@apply pr-2;
input {
@apply mb-0;
}
}
}
.formulate-input-element-decorator {
@apply hidden;
}
}
}
@@ -0,0 +1,623 @@
// Foundation for Sites Settings
// -----------------------------
//
// Table of Contents:
//
// 1. Global
// 2. Breakpoints
// 3. The Grid
// 4. Base Typography
// 5. Typography Helpers
// 6. Abide
// 7. Accordion
// 8. Accordion Menu
// 9. Badge
// 10. Breadcrumbs
// 11. Button
// 12. Button Group
// 13. Callout
// 14. Card
// 15. Close Button
// 16. Drilldown
// 17. Dropdown
// 18. Dropdown Menu
// 19. Forms
// 20. Label
// 21. Media Object
// 22. Menu
// 23. Meter
// 24. Off-canvas
// 25. Orbit
// 26. Pagination
// 27. Progress Bar
// 28. Responsive Embed
// 29. Reveal
// 30. Slider
// 31. Switch
// 32. Table
// 33. Tabs
// 34. Thumbnail
// 35. Title Bar
// 36. Tooltip
// 37. Top Bar
@import '~foundation-sites/scss/util/util';
// 1. Global
// ---------
// Disable contrast warnings in Foundation.
$contrast-warnings: false;
$global-font-size: 16px;
$global-width: 100%;
$global-lineheight: 1.5;
$foundation-palette: (primary: $color-woot,
secondary: #5d7592,
success: #44ce4b,
warning: #ffc532,
alert: #ff382d);
$light-gray: #c0ccda;
$medium-gray: #8492a6;
$dark-gray: $color-gray;
$black: #000;
$white: #fff;
$body-background: $white;
$body-font-color: $color-body;
$body-font-family: 'PlusJakarta',
-apple-system,
system-ui,
BlinkMacSystemFont,
"Segoe UI",
Roboto,
"Helvetica Neue",
Tahoma,
Arial,
sans-serif;
$body-antialiased: true;
$global-margin: $space-small;
$global-padding: $space-small;
$global-weight-normal: normal;
$global-weight-bold: bold;
$global-radius: 0;
$global-text-direction: ltr;
$global-flexbox: false;
$print-transparent-backgrounds: true;
@include add-foundation-colors;
// 2. Breakpoints
// --------------
$breakpoints: (small: 0,
medium: 640px,
large: 1024px,
xlarge: 1200px,
xxlarge: 1400px,
xxxlarge: 1600px,
);
$print-breakpoint: large;
$breakpoint-classes: (small medium large);
// 3. The Grid
// -----------
$grid-row-width: $global-width;
$grid-column-count: 12;
$grid-column-gutter: (small: $zero,
medium: $zero);
$grid-column-align-edge: true;
$block-grid-max: 8;
// 4. Base Typography
// ------------------
$header-font-family: $body-font-family;
$header-font-weight: $font-weight-medium;
$header-font-style: normal;
$font-family-monospace: $body-font-family;
$header-color: $color-heading;
$header-lineheight: 1.4;
$header-margin-bottom: 0.3125rem;
$header-styles: (small: ("h1": ("font-size": 24),
"h2": ("font-size": 20),
"h3": ("font-size": 19),
"h4": ("font-size": 18),
"h5": ("font-size": 17),
"h6": ("font-size": 16)),
medium: ("h1": ("font-size": 48),
"h2": ("font-size": 40),
"h3": ("font-size": 31),
"h4": ("font-size": 25),
"h5": ("font-size": 20),
"h6": ("font-size": 16)));
$header-text-rendering: optimizeLegibility;
$small-font-size: 80%;
$header-small-font-color: $medium-gray;
$paragraph-lineheight: 1.65;
$paragraph-margin-bottom: var(--space-small);
$paragraph-text-rendering: optimizeLegibility;
$code-color: $black;
$code-font-family: $font-family-monospace;
$code-font-weight: $global-weight-normal;
$code-background: $light-gray;
$code-border: 1px solid $medium-gray;
$code-padding: rem-calc(2 5 1);
$anchor-color: $primary-color;
$anchor-color-hover: scale-color($anchor-color, $lightness: -14%);
$anchor-text-decoration: none;
$anchor-text-decoration-hover: none;
$hr-width: $global-width;
$hr-border: 1px solid $medium-gray;
$hr-margin: rem-calc(20) auto;
$list-lineheight: $paragraph-lineheight;
$list-margin-bottom: $paragraph-margin-bottom;
$list-style-type: disc;
$list-style-position: outside;
$list-side-margin: 0.78125rem;
$list-nested-side-margin: 0.78125rem;
$defnlist-margin-bottom: 0.6875rem;
$defnlist-term-weight: $global-weight-bold;
$defnlist-term-margin-bottom: 0.1875rem;
$blockquote-color: $dark-gray;
$blockquote-padding: rem-calc(9 20 0 19);
$blockquote-border: 1px solid $medium-gray;
$cite-font-size: rem-calc(13);
$cite-color: $dark-gray;
$cite-pseudo-content: '\2014 \0020';
$keystroke-font: $font-family-monospace;
$keystroke-color: $black;
$keystroke-background: $light-gray;
$keystroke-padding: rem-calc(2 4 0);
$keystroke-radius: $global-radius;
$abbr-underline: 1px dotted $black;
// 5. Typography Helpers
// ---------------------
$lead-font-size: $global-font-size * 1.25;
$lead-lineheight: 1.6;
$subheader-lineheight: 1.4;
$subheader-color: $dark-gray;
$subheader-font-weight: $global-weight-normal;
$subheader-margin-top: 0.125rem;
$subheader-margin-bottom: 0.3125rem;
$stat-font-size: 1.5625rem;
// 6. Abide
// --------
$abide-inputs: true;
$abide-labels: true;
$input-background-invalid: get-color(alert);
$form-label-color-invalid: get-color(alert);
$input-error-color: get-color(alert);
$input-error-font-size: rem-calc(12);
$input-error-font-weight: $global-weight-bold;
// 7. Accordion
// ------------
$accordion-background: $white;
$accordion-plusminus: true;
$accordion-title-font-size: rem-calc(12);
$accordion-item-color: $primary-color;
$accordion-item-background-hover: $light-gray;
$accordion-item-padding: 0.78125rem 0.625rem;
$accordion-content-background: $white;
$accordion-content-border: 1px solid $light-gray;
$accordion-content-color: $body-font-color;
$accordion-content-padding: 0.625rem;
// 8. Accordion Menu
// -----------------
$accordionmenu-arrows: true;
$accordionmenu-arrow-color: $primary-color;
$accordionmenu-arrow-size: 6px;
// 9. Badge
// --------
$badge-background: $primary-color;
$badge-color: $white;
$badge-color-alt: $black;
$badge-palette: $foundation-palette;
$badge-padding: var(--space-smaller);
$badge-minwidth: 2.1em;
$badge-font-size: var(--font-size-nano);
// 10. Breadcrumbs
// ---------------
$breadcrumbs-margin: 0 0 $global-margin 0;
$breadcrumbs-item-font-size: rem-calc(11);
$breadcrumbs-item-color: $primary-color;
$breadcrumbs-item-color-current: $black;
$breadcrumbs-item-color-disabled: $medium-gray;
$breadcrumbs-item-margin: 0.46875rem;
$breadcrumbs-item-uppercase: true;
$breadcrumbs-item-slash: true;
// 11. Button
// ----------
$button-padding: var(--space-smaller) 1em;
$button-margin: 0 0 $global-margin 0;
$button-fill: solid;
$button-background: $primary-color;
$button-background-hover: scale-color($button-background, $lightness: -15%);
$button-color: $white;
$button-color-alt: $white;
$button-radius: var(--border-radius-normal);
$button-sizes: (tiny: var(--font-size-micro),
small: var(--font-size-mini),
default: var(--font-size-small),
large: var(--font-size-medium));
$button-palette: $foundation-palette;
$button-opacity-disabled: 0.4;
$button-background-hover-lightness: -20%;
$button-hollow-hover-lightness: -50%;
$button-transition: background-color 0.25s ease-out,
color 0.25s ease-out;
// 12. Button Group
// ----------------
$buttongroup-margin: 0;
$buttongroup-spacing: 0;
$buttongroup-child-selector: '.button';
$buttongroup-expand-max: 6;
$buttongroup-radius-on-each: false;
// 13. Callout
// -----------
$callout-background: $white;
$callout-background-fade: 85%;
$callout-border: 1px solid rgba($black, 0.25);
$callout-margin: 0 0 0.625rem 0;
$callout-padding: 0.625rem;
$callout-font-color: $body-font-color;
$callout-font-color-alt: $body-background;
$callout-radius: $global-radius;
$callout-link-tint: 30%;
// 14. Card
// --------
$card-background: $white;
$card-font-color: $body-font-color;
$card-divider-background: $light-gray;
$card-border: 1px solid var(--color-border);
$card-shadow: var(--shadow-small);
$card-border-radius: var(--border-radius-normal);
$card-padding: var(--space-small);
$card-margin: $global-margin;
// 15. Close Button
// ----------------
$closebutton-position: right top;
$closebutton-offset-horizontal: (small: 0.66rem,
medium: 1rem);
$closebutton-offset-vertical: (small: 0.33em,
medium: 0.5rem);
$closebutton-size: (small: 1.5em,
medium: 2em);
$closebutton-lineheight: 1;
$closebutton-color: $dark-gray;
$closebutton-color-hover: $black;
// 16. Drilldown
// -------------
$drilldown-transition: transform 0.15s linear;
$drilldown-arrows: true;
$drilldown-arrow-color: $primary-color;
$drilldown-arrow-size: 6px;
$drilldown-background: $white;
// 17. Dropdown
// ------------
$dropdown-padding: 0.625rem;
$dropdown-background: $body-background;
$dropdown-border: 1px solid $medium-gray;
$dropdown-font-size: 0.625rem;
$dropdown-width: 300px;
$dropdown-radius: $global-radius;
$dropdown-sizes: (tiny: 100px,
small: 200px,
large: 400px);
// 18. Dropdown Menu
// -----------------
$dropdownmenu-arrows: true;
$dropdownmenu-arrow-color: $anchor-color;
$dropdownmenu-arrow-size: 6px;
$dropdownmenu-min-width: 200px;
$dropdownmenu-background: $white;
$dropdownmenu-border: 1px solid $medium-gray;
// 19. Forms
// ---------
$fieldset-border: 1px solid $light-gray;
$fieldset-padding: $space-two;
$fieldset-margin: $space-one $zero;
$legend-padding: rem-calc(0 3);
$form-spacing: $space-normal;
$helptext-color: $color-body;
$helptext-font-size: $font-size-small;
$helptext-font-style: italic;
$input-prefix-color: $color-body;
$input-prefix-background: var(--b-100);
$input-prefix-border: 1px solid $color-border;
$input-prefix-padding: 0.625rem;
$form-label-color: $color-body;
$form-label-font-size: rem-calc(14);
$form-label-font-weight: $font-weight-medium;
$form-label-line-height: 1.8;
$select-background: $white;
$select-triangle-color: $dark-gray;
$select-radius: var(--border-radius-normal);
$input-color: $color-body;
$input-placeholder-color: $light-gray;
$input-font-family: inherit;
$input-font-size: $font-size-default;
$input-font-weight: $global-weight-normal;
$input-background: $white;
$input-background-focus: $white;
$input-background-disabled: $light-gray;
$input-border: 1px solid var(--s-200);
$input-border-focus: 1px solid lighten($primary-color, 15%);
$input-shadow: 0;
$input-shadow-focus: 0;
$input-cursor-disabled: not-allowed;
$input-transition: border-color 0.25s ease-in-out;
$input-number-spinners: true;
$input-radius: var(--border-radius-normal);
$form-button-radius: var(--border-radius-normal);
// 20. Label
// ---------
$label-background: $white;
$label-color: $black;
$label-color-alt: $black;
$label-palette: $foundation-palette;
$label-font-size: $font-size-mini;
$label-padding: $space-smaller $space-small;
$label-radius: var(--border-radius-small);
// 21. Media Object
// ----------------
$mediaobject-margin-bottom: $global-margin;
$mediaobject-section-padding: $global-padding;
$mediaobject-image-width-stacked: 100%;
// 22. Menu
// --------
$menu-margin: 0;
$menu-margin-nested: $space-medium;
$menu-item-padding: $space-slab;
$menu-item-color-active: $white;
$menu-item-background-active: $color-background;
$menu-icon-spacing: 0.15625rem;
$menu-item-background-hover: $light-gray;
$menu-border: $light-gray;
// 23. Meter
// ---------
$meter-height: 0.625rem;
$meter-radius: $global-radius;
$meter-background: $medium-gray;
$meter-fill-good: $success-color;
$meter-fill-medium: $warning-color;
$meter-fill-bad: $alert-color;
// 24. Off-canvas
// --------------
$offcanvas-sizes: (small: 14.375,
medium: 14.375,
);
$offcanvas-vertical-sizes: (small: 14.375,
medium: 14.375,
);
$offcanvas-background: $light-gray;
$offcanvas-shadow: 0 0 10px rgba($black, 0.7);
$offcanvas-push-zindex: 1;
$offcanvas-overlap-zindex: 10;
$offcanvas-reveal-zindex: 1;
$offcanvas-transition-length: 0.5s;
$offcanvas-transition-timing: ease;
$offcanvas-fixed-reveal: true;
$offcanvas-exit-background: rgba($white, 0.25);
$maincontent-class: 'off-canvas-content';
// 25. Orbit
// ---------
$orbit-bullet-background: $medium-gray;
$orbit-bullet-background-active: $dark-gray;
$orbit-bullet-diameter: 0.75rem;
$orbit-bullet-margin: 0.0625rem;
$orbit-bullet-margin-top: 0.5rem;
$orbit-bullet-margin-bottom: 0.5rem;
$orbit-caption-background: rgba($black, 0.5);
$orbit-caption-padding: 0.625rem;
$orbit-control-background-hover: rgba($black, 0.5);
$orbit-control-padding: 0.625rem;
$orbit-control-zindex: 10;
// 26. Pagination
// --------------
$pagination-font-size: rem-calc(14);
$pagination-margin-bottom: $global-margin;
$pagination-item-color: $black;
$pagination-item-padding: rem-calc(3 10);
$pagination-item-spacing: rem-calc(1);
$pagination-radius: $global-radius;
$pagination-item-background-hover: $light-gray;
$pagination-item-background-current: $primary-color;
$pagination-item-color-current: $white;
$pagination-item-color-disabled: $medium-gray;
$pagination-ellipsis-color: $black;
$pagination-mobile-items: false;
$pagination-mobile-current-item: false;
$pagination-arrows: true;
// 27. Progress Bar
// ----------------
$progress-height: 0.625rem;
$progress-background: $medium-gray;
$progress-margin-bottom: $global-margin;
$progress-meter-background: $primary-color;
$progress-radius: $global-radius;
// 28. Responsive Embed
// --------------------
$responsive-embed-margin-bottom: rem-calc(16);
$responsive-embed-ratios: (default: 4 by 3,
widescreen: 16 by 9);
// 29. Reveal
// ----------
$reveal-background: $white;
$reveal-width: 600px;
$reveal-max-width: $global-width;
$reveal-padding: $global-padding;
$reveal-border: 1px solid $medium-gray;
$reveal-radius: $global-radius;
$reveal-zindex: 1005;
$reveal-overlay-background: rgba($black, 0.45);
// 30. Slider
// ----------
$slider-width-vertical: 0.3125rem;
$slider-transition: all 0.2s ease-in-out;
$slider-height: 0.3125rem;
$slider-background: $light-gray;
$slider-fill-background: $medium-gray;
$slider-handle-height: 0.875rem;
$slider-handle-width: 0.875rem;
$slider-handle-background: $primary-color;
$slider-opacity-disabled: 0.25;
$slider-radius: $global-radius;
// 31. Switch
// ----------
$switch-background: $light-gray;
$switch-background-active: $primary-color;
$switch-height: $space-two;
$switch-height-tiny: $space-slab;
$switch-height-small: $space-normal;
$switch-height-large: $space-large;
$switch-radius: $space-large;
$switch-margin: $global-margin;
$switch-paddle-background: $white;
$switch-paddle-offset: $space-micro;
$switch-paddle-radius: $space-large;
$switch-paddle-transition: all 0.15s ease-out;
// 32. Table
// ---------
$table-background: transparent;
$table-color-scale: 5%;
$table-border: 1px solid transparent;
$table-padding: rem-calc(8 10 10);
$table-hover-scale: 2%;
$table-row-hover: darken($table-background, $table-hover-scale);
$table-row-stripe-hover: darken($table-background,
$table-color-scale + $table-hover-scale);
$table-is-striped: false;
$table-striped-background: smart-scale($table-background, $table-color-scale);
$table-stripe: even;
$table-head-background: smart-scale($table-background, $table-color-scale / 2);
$table-head-row-hover: darken($table-head-background, $table-hover-scale);
$table-foot-background: smart-scale($table-background, $table-color-scale);
$table-foot-row-hover: darken($table-foot-background, $table-hover-scale);
$table-head-font-color: $body-font-color;
$table-foot-font-color: $body-font-color;
$show-header-for-stacked: false;
// 33. Tabs
// --------
$tab-margin: 0;
$tab-background: transparent;
$tab-background-active: transparent;
$tab-item-font-size: $font-size-small;
$tab-item-background-hover: transparent;
$tab-item-padding: $space-one $zero;
$tab-color: $primary-color;
$tab-active-color: $primary-color;
$tab-expand-max: 6;
$tab-content-background: transparent;
$tab-content-border: transparent;
$tab-content-color: foreground($tab-background, $primary-color);
$tab-content-padding: 0.625rem;
// 34. Thumbnail
// -------------
$thumbnail-border: solid 4px $white;
$thumbnail-margin-bottom: $global-margin;
$thumbnail-shadow: 0 0 0 1px rgba($black, 0.2);
$thumbnail-shadow-hover: 0 0 6px 1px rgba($primary-color, 0.5);
$thumbnail-transition: box-shadow 200ms ease-out;
$thumbnail-radius: $global-radius;
// 35. Title Bar
// -------------
$titlebar-background: $black;
$titlebar-color: $white;
$titlebar-padding: 0.3125rem;
$titlebar-text-font-weight: bold;
$titlebar-icon-color: $white;
$titlebar-icon-color-hover: $medium-gray;
$titlebar-icon-spacing: 0.15625rem;
// 36. Tooltip
// -----------
$has-tip-font-weight: $global-weight-bold;
$has-tip-border-bottom: dotted 1px $dark-gray;
$tooltip-background-color: $black;
$tooltip-color: $white;
$tooltip-padding: 0.46875rem;
$tooltip-font-size: $font-size-mini;
$tooltip-pip-width: 0.46875rem;
$tooltip-pip-height: $tooltip-pip-width * 0.866;
$tooltip-radius: $global-radius;
// 37. Top Bar
// -----------
$topbar-padding: 0.3125;
$topbar-background: $light-gray;
$topbar-submenu-background: $topbar-background;
$topbar-title-spacing: 0.3125 0.625rem 0.3125 0;
$topbar-input-width: 200px;
$topbar-unstack-breakpoint: medium;
// Internal variable that contains the flex justifying options
$-zf-flex-justify: -zf-flex-justify($global-text-direction);
$menu-items-padding: $space-one;
$xy-grid: false;
@@ -2,6 +2,11 @@
@apply bg-slate-25 dark:bg-slate-800; @apply bg-slate-25 dark:bg-slate-800;
} }
.flex-center {
@include flex-align(center, middle);
display: flex;
}
.bottom-space-fix { .bottom-space-fix {
margin-bottom: auto; margin-bottom: auto;
} }
@@ -1,5 +1,5 @@
@import 'dashboard/assets/scss/variables'; @import '~dashboard/assets/scss/variables';
@import 'widget/assets/scss/mixins'; @import '~widget/assets/scss/mixins';
$spinner-before-border-color: rgba(255, 255, 255, 0.7); $spinner-before-border-color: rgba(255, 255, 255, 0.7);
@@ -89,6 +89,9 @@ $swift-ease-out-duration: .4s !default;
$swift-ease-out-function: cubic-bezier(0.37, 0, 0.63, 1) !default; $swift-ease-out-function: cubic-bezier(0.37, 0, 0.63, 1) !default;
$swift-ease-out: all $swift-ease-out-duration $swift-ease-out-function !default; $swift-ease-out: all $swift-ease-out-duration $swift-ease-out-function !default;
// Ionicons
$ionicons-font-path: '~ionicons/fonts';
// Transitions // Transitions
$transition-ease-in: all 0.250s ease-in; $transition-ease-in: all 0.250s ease-in;
@@ -15,13 +15,36 @@
@import 'variables'; @import 'variables';
@import 'mixins'; @import 'mixins';
@import 'foundation-settings';
@import 'helper-classes'; @import 'helper-classes';
@import 'formulate'; @import 'formulate';
@import 'date-picker'; @import 'date-picker';
@import 'foundation-sites/scss/foundation';
@include foundation-everything($flex: true);
@include foundation-prototype-text-utilities;
@include foundation-prototype-text-transformation;
@include foundation-prototype-text-decoration;
@include foundation-prototype-font-styling;
@include foundation-prototype-list-style-type;
@include foundation-prototype-rounded;
@include foundation-prototype-bordered;
@include foundation-prototype-shadow;
@include foundation-prototype-separator;
@include foundation-prototype-overflow;
@include foundation-prototype-display;
@include foundation-prototype-position;
@include foundation-prototype-border-box;
@include foundation-prototype-border-none;
@include foundation-prototype-sizing;
@include foundation-prototype-spacing;
@import 'typography'; @import 'typography';
@import 'layout'; @import 'layout';
@import 'animations'; @import 'animations';
@import 'foundation-custom';
@import 'rtl'; @import 'rtl';
@import 'widgets/buttons'; @import 'widgets/buttons';
@@ -29,11 +52,15 @@
@import 'widgets/conversation-card'; @import 'widgets/conversation-card';
@import 'widgets/conversation-view'; @import 'widgets/conversation-view';
@import 'widgets/forms'; @import 'widgets/forms';
@import 'widgets/login';
@import 'widgets/modal'; @import 'widgets/modal';
@import 'widgets/reply-box'; @import 'widgets/reply-box';
@import 'widgets/report'; @import 'widgets/report';
@import 'widgets/search-box';
@import 'widgets/sidemenu';
@import 'widgets/snackbar'; @import 'widgets/snackbar';
@import 'widgets/states'; @import 'widgets/states';
@import 'widgets/status-bar';
@import 'widgets/tabs'; @import 'widgets/tabs';
@import 'widgets/woot-tables'; @import 'widgets/woot-tables';
@@ -42,6 +69,7 @@
@import 'plugins/multiselect'; @import 'plugins/multiselect';
@import 'plugins/dropdown'; @import 'plugins/dropdown';
@import '~shared/assets/stylesheets/ionicons';
@import 'utility-helpers'; @import 'utility-helpers';
.tooltip { .tooltip {
@@ -10,14 +10,21 @@
@import 'variables'; @import 'variables';
@import 'vue-multiselect/dist/vue-multiselect.min.css'; @import 'vue-multiselect/dist/vue-multiselect.min.css';
@import '~shared/assets/stylesheets/ionicons';
@import 'mixins'; @import 'mixins';
@import 'foundation-settings';
@import 'helper-classes'; @import 'helper-classes';
@import 'foundation-sites/scss/foundation';
@include foundation-prototype-spacing;
@include foundation-everything($flex: true);
@import 'typography'; @import 'typography';
@import 'layout'; @import 'layout';
@import 'animations'; @import 'animations';
@import 'foundation-custom';
@import 'widgets/buttons'; @import 'widgets/buttons';
@import 'widgets/forms'; @import 'widgets/forms';
@@ -1,8 +1,39 @@
@import 'shared/assets/fonts/inter'; @import '../variables';
@import 'tailwindcss/base';
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';
body { .superadmin-body {
font-family: Inter, -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif !important; background: var(--color-background);
.hero--title {
font-size: var(--font-size-mega);
font-weight: var(--font-weight-light);
margin-top: var(--space-large);
}
.update-subscription--checkbox {
display: flex;
input {
line-height: 1.5;
margin-right: var(--space-one);
margin-top: var(--space-smaller);
}
label {
font-size: var(--font-size-small);
line-height: 1.5;
margin-bottom: var(--space-normal);
}
}
} }
.alert-box {
background-color: var(--r-500);
border-radius: 5px;
color: var(--color-white);
font-size: 14px;
margin-bottom: 14px;
padding: 10px;
text-align: center;
}
@@ -0,0 +1,3 @@
@import 'shared/assets/fonts/plus-jakarta';
@import '../variables';
@import '~shared/assets/stylesheets/ionicons';
@@ -76,11 +76,7 @@
&.left { &.left {
.bubble { .bubble {
@apply rounded-r-lg rounded-l mr-auto break-words; @apply border border-slate-50 dark:border-slate-700 bg-white dark:bg-slate-700 text-black-900 dark:text-slate-50 rounded-r-lg rounded-l mr-auto break-words;
&:not(.is-unsupported) {
@apply border border-slate-50 dark:border-slate-700 bg-white dark:bg-slate-700 text-black-900 dark:text-slate-50
}
&.is-image { &.is-image {
@apply rounded-lg; @apply rounded-lg;
@@ -0,0 +1,66 @@
.auth-wrap {
width: 100%;
}
// Outside login wrapper
.login {
@include full-height;
overflow-y: auto;
padding-top: $space-larger * 1.2;
.login__hero {
margin-bottom: $space-larger;
.hero__logo {
width: 180px;
}
.hero__title {
font-weight: $font-weight-light;
margin-top: $space-larger;
}
.hero__sub {
color: $medium-gray;
font-size: $font-size-medium;
}
}
// Login box
.login-box {
@include background-white;
@include border-normal;
@include elegant-card;
border-radius: $space-smaller;
padding: $space-large;
label {
color: $color-gray;
font-size: $font-size-default;
input {
font-size: $font-size-default;
height: $space-larger;
padding: $space-slab;
}
.error {
font-size: $font-size-small;
}
}
.button {
height: $space-larger;
}
}
.sigin__footer {
font-size: $font-size-default;
padding: $space-medium;
> a {
font-weight: $font-weight-bold;
}
}
}
@@ -1,5 +1,7 @@
.modal-mask { .modal-mask {
@apply flex items-center justify-center bg-modal-backdrop-light dark:bg-modal-backdrop-dark z-[9990] h-full left-0 fixed top-0 w-full; // @include flex;
// @include flex-align(center, middle);
@apply flex items-center justify-center bg-modal dark:bg-modal z-[9990] h-full left-0 fixed top-0 w-full;
} }
.page-top-bar { .page-top-bar {
@@ -43,6 +45,8 @@
} }
.modal-footer { .modal-footer {
// @include flex;
// @include flex-align($x: flex-end, $y: middle);
@apply flex justify-end items-center py-2 px-0 gap-2; @apply flex justify-end items-center py-2 px-0 gap-2;
&.justify-content-end { &.justify-content-end {
@@ -8,7 +8,7 @@
.reply-box__top { .reply-box__top {
.icon { .icon {
color: var(--slate-500); color: $medium-gray;
cursor: pointer; cursor: pointer;
font-size: $font-size-medium; font-size: $font-size-medium;
margin-right: $space-small; margin-right: $space-small;
@@ -34,7 +34,7 @@
resize: none; resize: none;
} }
>textarea { > textarea {
@include ghost-input(); @include ghost-input();
background: transparent; background: transparent;
margin: 0; margin: 0;
@@ -52,7 +52,7 @@
.reply-box__top { .reply-box__top {
@apply bg-yellow-100 dark:bg-yellow-800; @apply bg-yellow-100 dark:bg-yellow-800;
>input { > input {
@apply bg-yellow-100 dark:bg-yellow-800; @apply bg-yellow-100 dark:bg-yellow-800;
} }
} }
@@ -1,10 +1,45 @@
.side-menu {
i {
margin-right: var(--space-smaller);
min-width: var(--space-two);
}
}
.sidebar {
z-index: 1024 - 1;
//logo
.logo {
img {
max-height: 108px;
padding: $woot-logo-padding;
}
}
.nested {
a {
font-size: var(--font-size-small);
margin-bottom: var(--space-micro);
margin-top: var(--space-micro);
.inbox-icon {
display: inline-block;
margin-right: var(--space-micro);
min-width: var(--space-normal);
text-align: center;
}
}
}
}
// bottom-nav // bottom-nav
.bottom-nav { .bottom-nav {
@include flex; @include flex;
@include space-between-column; @include space-between-column;
@include border-normal-top; @include border-normal-top;
flex-direction: column; flex-direction: column;
padding: var(--space-one) var(--space-normal) var(--space-one) var(--space-one); padding: var(--space-one) var(--space-normal) var(--space-one)
var(--space-one);
position: relative; position: relative;
&:hover { &:hover {
+48 -114
View File
@@ -126,33 +126,49 @@
@assign-team="onAssignTeamsForBulk" @assign-team="onAssignTeamsForBulk"
/> />
<div <div
ref="conversationList" ref="activeConversation"
class="conversations-list flex-1" class="conversations-list flex-1"
:class="{ 'overflow-hidden': isContextMenuOpen }" :class="{ 'overflow-hidden': isContextMenuOpen }"
> >
<virtual-list <div>
ref="conversationVirtualList" <conversation-card
:data-key="'id'" v-for="chat in conversationList"
:data-sources="conversationList" :key="chat.id"
:data-component="itemComponent" :active-label="label"
:extra-props="virtualListExtraProps" :team-id="teamId"
class="w-full overflow-auto h-full" :folders-id="foldersId"
footer-tag="div" :chat="chat"
:conversation-type="conversationType"
:show-assignee="showAssigneeInConversationCard"
:selected="isConversationSelected(chat.id)"
@select-conversation="selectConversation"
@de-select-conversation="deSelectConversation"
@assign-agent="onAssignAgent"
@assign-team="onAssignTeam"
@assign-label="onAssignLabels"
@update-conversation-status="toggleConversationStatus"
@context-menu-toggle="onContextMenuToggle"
@mark-as-unread="markAsUnread"
@assign-priority="assignPriority"
/>
</div>
<div v-if="chatListLoading" class="text-center">
<span class="spinner mt-4 mb-4" />
</div>
<woot-button
v-if="!hasCurrentPageEndReached && !chatListLoading"
variant="clear"
size="expanded"
class="load-more--button"
@click="loadMoreConversations"
> >
<template #footer> {{ $t('CHAT_LIST.LOAD_MORE_CONVERSATIONS') }}
<div v-if="chatListLoading" class="text-center"> </woot-button>
<span class="spinner mt-4 mb-4" />
</div> <p v-if="showEndOfListMessage" class="text-center text-muted p-4">
<p v-if="showEndOfListMessage" class="text-center text-muted p-4"> {{ $t('CHAT_LIST.EOF') }}
{{ $t('CHAT_LIST.EOF') }} </p>
</p>
<intersection-observer
v-if="!showEndOfListMessage && !chatListLoading"
:options="infiniteLoaderOptions"
@observed="loadMoreConversations"
/>
</template>
</virtual-list>
</div> </div>
<woot-modal <woot-modal
:show.sync="showAdvancedFilters" :show.sync="showAdvancedFilters"
@@ -175,12 +191,11 @@
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import VirtualList from 'vue-virtual-scroll-list';
import ConversationAdvancedFilter from './widgets/conversation/ConversationAdvancedFilter.vue'; import ConversationAdvancedFilter from './widgets/conversation/ConversationAdvancedFilter.vue';
import ConversationBasicFilter from './widgets/conversation/ConversationBasicFilter.vue'; import ConversationBasicFilter from './widgets/conversation/ConversationBasicFilter.vue';
import ChatTypeTabs from './widgets/ChatTypeTabs.vue'; import ChatTypeTabs from './widgets/ChatTypeTabs.vue';
import ConversationItem from './ConversationItem.vue'; import ConversationCard from './widgets/conversation/ConversationCard.vue';
import timeMixin from '../mixins/time'; import timeMixin from '../mixins/time';
import eventListenerMixins from 'shared/mixins/eventListenerMixins'; import eventListenerMixins from 'shared/mixins/eventListenerMixins';
import conversationMixin from '../mixins/conversations'; import conversationMixin from '../mixins/conversations';
@@ -192,7 +207,6 @@ import DeleteCustomViews from 'dashboard/routes/dashboard/customviews/DeleteCust
import ConversationBulkActions from './widgets/conversation/conversationBulkActions/Index.vue'; import ConversationBulkActions from './widgets/conversation/conversationBulkActions/Index.vue';
import alertMixin from 'shared/mixins/alertMixin'; import alertMixin from 'shared/mixins/alertMixin';
import filterMixin from 'shared/mixins/filterMixin'; import filterMixin from 'shared/mixins/filterMixin';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import languages from 'dashboard/components/widgets/conversation/advancedFilterItems/languages'; import languages from 'dashboard/components/widgets/conversation/advancedFilterItems/languages';
import countries from 'shared/constants/countries'; import countries from 'shared/constants/countries';
import { generateValuesForEditCustomViews } from 'dashboard/helper/customViewsHelper'; import { generateValuesForEditCustomViews } from 'dashboard/helper/customViewsHelper';
@@ -207,20 +221,16 @@ import {
isOnUnattendedView, isOnUnattendedView,
} from '../store/modules/conversations/helpers/actionHelpers'; } from '../store/modules/conversations/helpers/actionHelpers';
import { CONVERSATION_EVENTS } from '../helper/AnalyticsHelper/events'; import { CONVERSATION_EVENTS } from '../helper/AnalyticsHelper/events';
import IntersectionObserver from './IntersectionObserver.vue';
export default { export default {
components: { components: {
AddCustomViews, AddCustomViews,
ChatTypeTabs, ChatTypeTabs,
// eslint-disable-next-line vue/no-unused-components ConversationCard,
ConversationItem,
ConversationAdvancedFilter, ConversationAdvancedFilter,
DeleteCustomViews, DeleteCustomViews,
ConversationBulkActions, ConversationBulkActions,
ConversationBasicFilter, ConversationBasicFilter,
IntersectionObserver,
VirtualList,
}, },
mixins: [ mixins: [
timeMixin, timeMixin,
@@ -228,22 +238,7 @@ export default {
eventListenerMixins, eventListenerMixins,
alertMixin, alertMixin,
filterMixin, filterMixin,
uiSettingsMixin,
], ],
provide() {
return {
// Actions to be performed on virtual list item and context menu.
selectConversation: this.selectConversation,
deSelectConversation: this.deSelectConversation,
assignAgent: this.onAssignAgent,
assignTeam: this.onAssignTeam,
assignLabels: this.onAssignLabels,
updateConversationStatus: this.toggleConversationStatus,
toggleContextMenu: this.onContextMenuToggle,
markAsUnread: this.markAsUnread,
assignPriority: this.assignPriority,
};
},
props: { props: {
conversationInbox: { conversationInbox: {
type: [String, Number], type: [String, Number],
@@ -278,7 +273,7 @@ export default {
return { return {
activeAssigneeTab: wootConstants.ASSIGNEE_TYPE.ME, activeAssigneeTab: wootConstants.ASSIGNEE_TYPE.ME,
activeStatus: wootConstants.STATUS_TYPE.OPEN, activeStatus: wootConstants.STATUS_TYPE.OPEN,
activeSortBy: wootConstants.SORT_BY_TYPE.LAST_ACTIVITY_AT_DESC, activeSortBy: wootConstants.SORT_BY_TYPE.LATEST,
showAdvancedFilters: false, showAdvancedFilters: false,
advancedFilterTypes: advancedFilterTypes.map(filter => ({ advancedFilterTypes: advancedFilterTypes.map(filter => ({
...filter, ...filter,
@@ -294,21 +289,6 @@ export default {
selectedInboxes: [], selectedInboxes: [],
isContextMenuOpen: false, isContextMenuOpen: false,
appliedFilter: [], appliedFilter: [],
infiniteLoaderOptions: {
root: this.$refs.conversationList,
rootMargin: '100px 0px 100px 0px',
},
itemComponent: ConversationItem,
// virtualListExtraProps is to pass the props to the conversationItem component.
virtualListExtraProps: {
label: this.label,
teamId: this.teamId,
foldersId: this.foldersId,
conversationType: this.conversationType,
showAssignee: false,
isConversationSelected: this.isConversationSelected,
},
}; };
}, },
computed: { computed: {
@@ -428,12 +408,8 @@ export default {
}, },
conversationListPagination() { conversationListPagination() {
const conversationsPerPage = 25; const conversationsPerPage = 25;
const hasChatsOnView =
this.chatsOnView &&
Array.isArray(this.chatsOnView) &&
!this.chatsOnView.length;
const isNoFiltersOrFoldersAndChatListNotEmpty = const isNoFiltersOrFoldersAndChatListNotEmpty =
!this.hasAppliedFiltersOrActiveFolders && hasChatsOnView; !this.hasAppliedFiltersOrActiveFolders && this.chatsOnView !== [];
const isUnderPerPage = const isUnderPerPage =
this.chatsOnView.length < conversationsPerPage && this.chatsOnView.length < conversationsPerPage &&
this.activeAssigneeTabCount < conversationsPerPage && this.activeAssigneeTabCount < conversationsPerPage &&
@@ -519,9 +495,6 @@ export default {
}, },
}, },
watch: { watch: {
teamId() {
this.updateVirtualListProps('teamId', this.teamId);
},
activeTeam() { activeTeam() {
this.resetAndFetchData(); this.resetAndFetchData();
}, },
@@ -530,25 +503,18 @@ export default {
}, },
label() { label() {
this.resetAndFetchData(); this.resetAndFetchData();
this.updateVirtualListProps('label', this.label);
}, },
conversationType() { conversationType() {
this.resetAndFetchData(); this.resetAndFetchData();
this.updateVirtualListProps('conversationType', this.conversationType);
}, },
activeFolder() { activeFolder() {
this.resetAndFetchData(); this.resetAndFetchData();
this.updateVirtualListProps('foldersId', this.foldersId);
}, },
chatLists() { chatLists() {
this.chatsOnView = this.conversationList; this.chatsOnView = this.conversationList;
}, },
showAssigneeInConversationCard(newVal) {
this.updateVirtualListProps('showAssignee', newVal);
},
}, },
mounted() { mounted() {
this.setFiltersFromUISettings();
this.$store.dispatch('setChatStatusFilter', this.activeStatus); this.$store.dispatch('setChatStatusFilter', this.activeStatus);
this.$store.dispatch('setChatSortFilter', this.activeSortBy); this.$store.dispatch('setChatSortFilter', this.activeSortBy);
this.resetAndFetchData(); this.resetAndFetchData();
@@ -562,12 +528,6 @@ export default {
}); });
}, },
methods: { methods: {
updateVirtualListProps(key, value) {
this.virtualListExtraProps = {
...this.virtualListExtraProps,
[key]: value,
};
},
onApplyFilter(payload) { onApplyFilter(payload) {
this.resetBulkActions(); this.resetBulkActions();
this.foldersQuery = filterQueryGenerator(payload); this.foldersQuery = filterQueryGenerator(payload);
@@ -584,15 +544,6 @@ export default {
this.$store.dispatch('customViews/update', payloadData); this.$store.dispatch('customViews/update', payloadData);
this.closeAdvanceFiltersModal(); this.closeAdvanceFiltersModal();
}, },
setFiltersFromUISettings() {
const { conversations_filter_by: filterBy = {} } = this.uiSettings;
const { status, order_by: orderBy } = filterBy;
this.activeStatus = status || wootConstants.STATUS_TYPE.OPEN;
this.activeSortBy =
Object.keys(wootConstants.SORT_BY_TYPE).find(
sortField => sortField === orderBy
) || wootConstants.SORT_BY_TYPE.LAST_ACTIVITY_AT_DESC;
},
onClickOpenAddFoldersModal() { onClickOpenAddFoldersModal() {
this.showAddFoldersModal = true; this.showAddFoldersModal = true;
}, },
@@ -671,10 +622,10 @@ export default {
); );
}, },
getKeyboardListenerParams() { getKeyboardListenerParams() {
const allConversations = this.$refs.conversationList.querySelectorAll( const allConversations = this.$refs.activeConversation.querySelectorAll(
'div.conversations-list div.conversation' 'div.conversations-list div.conversation'
); );
const activeConversation = this.$refs.conversationList.querySelector( const activeConversation = this.$refs.activeConversation.querySelector(
'div.conversations-list div.conversation.active' 'div.conversations-list div.conversation.active'
); );
const activeConversationIndex = [...allConversations].indexOf( const activeConversationIndex = [...allConversations].indexOf(
@@ -730,12 +681,9 @@ export default {
fetchConversations() { fetchConversations() {
this.$store this.$store
.dispatch('fetchAllConversations', this.conversationFilters) .dispatch('fetchAllConversations', this.conversationFilters)
.then(this.emitConversationLoaded); .then(() => this.$emit('conversation-load'));
}, },
loadMoreConversations() { loadMoreConversations() {
if (this.hasCurrentPageEndReached || this.chatListLoading) {
return;
}
if (!this.hasAppliedFiltersOrActiveFolders) { if (!this.hasAppliedFiltersOrActiveFolders) {
this.fetchConversations(); this.fetchConversations();
} }
@@ -754,7 +702,7 @@ export default {
queryData: filterQueryGenerator(payload), queryData: filterQueryGenerator(payload),
page, page,
}) })
.then(this.emitConversationLoaded); .then(() => this.$emit('conversation-load'));
this.showAdvancedFilters = false; this.showAdvancedFilters = false;
}, },
fetchSavedFilteredConversations(payload) { fetchSavedFilteredConversations(payload) {
@@ -764,7 +712,7 @@ export default {
queryData: payload, queryData: payload,
page, page,
}) })
.then(this.emitConversationLoaded); .then(() => this.$emit('conversation-load'));
}, },
updateAssigneeTab(selectedTab) { updateAssigneeTab(selectedTab) {
if (this.activeAssigneeTab !== selectedTab) { if (this.activeAssigneeTab !== selectedTab) {
@@ -776,20 +724,6 @@ export default {
} }
} }
}, },
emitConversationLoaded() {
this.$emit('conversation-load');
this.$nextTick(() => {
// Addressing a known issue in the virtual list library where dynamically added items
// might not render correctly. This workaround involves a slight manual adjustment
// to the scroll position, triggering the list to refresh its rendering.
const virtualList = this.$refs.conversationVirtualList;
const scrollToOffset = virtualList?.scrollToOffset;
const currentOffset = virtualList?.getOffset() || 0;
if (scrollToOffset) {
scrollToOffset(currentOffset + 1);
}
});
},
resetBulkActions() { resetBulkActions() {
this.selectedConversations = []; this.selectedConversations = [];
this.selectedInboxes = []; this.selectedInboxes = [];
@@ -1,72 +0,0 @@
<template>
<conversation-card
:key="source.id"
:active-label="label"
:team-id="teamId"
:folders-id="foldersId"
:chat="source"
:conversation-type="conversationType"
:selected="isConversationSelected(source.id)"
:show-assignee="showAssignee"
:enable-context-menu="true"
@select-conversation="selectConversation"
@de-select-conversation="deSelectConversation"
@assign-agent="assignAgent"
@assign-team="assignTeam"
@assign-label="assignLabels"
@update-conversation-status="updateConversationStatus"
@context-menu-toggle="toggleContextMenu"
@mark-as-unread="markAsUnread"
@assign-priority="assignPriority"
/>
</template>
<script>
import ConversationCard from './widgets/conversation/ConversationCard.vue';
export default {
components: {
ConversationCard,
},
inject: [
'selectConversation',
'deSelectConversation',
'assignAgent',
'assignTeam',
'assignLabels',
'updateConversationStatus',
'toggleContextMenu',
'markAsUnread',
'assignPriority',
],
props: {
source: {
type: Object,
required: true,
},
teamId: {
type: [String, Number],
default: 0,
},
label: {
type: String,
default: '',
},
conversationType: {
type: String,
default: '',
},
foldersId: {
type: [String, Number],
default: 0,
},
isConversationSelected: {
type: Function,
default: () => {},
},
showAssignee: {
type: Boolean,
default: false,
},
},
};
</script>
@@ -13,7 +13,7 @@
<div class="flex items-center justify-between w-full"> <div class="flex items-center justify-between w-full">
<span <span
class="attribute-name" class="attribute-name"
:class="{ error: v$.editedValue.$error }" :class="{ error: $v.editedValue.$error }"
> >
{{ label }} {{ label }}
</span> </span>
@@ -39,8 +39,8 @@
:type="inputType" :type="inputType"
class="input-group-field" class="input-group-field"
autofocus="true" autofocus="true"
:class="{ error: v$.editedValue.$error }" :class="{ error: $v.editedValue.$error }"
@blur="v$.editedValue.$touch" @blur="$v.editedValue.$touch"
@keyup.enter="onUpdate" @keyup.enter="onUpdate"
/> />
<div class="input-group-button"> <div class="input-group-button">
@@ -122,37 +122,25 @@
<script> <script>
import { format, parseISO } from 'date-fns'; import { format, parseISO } from 'date-fns';
import { required, url } from '@vuelidate/validators'; import { required, url } from 'vuelidate/lib/validators';
import { BUS_EVENTS } from 'shared/constants/busEvents'; import { BUS_EVENTS } from 'shared/constants/busEvents';
import MultiselectDropdown from 'shared/components/ui/MultiselectDropdown.vue'; import MultiselectDropdown from 'shared/components/ui/MultiselectDropdown.vue';
import { isValidURL } from '../helper/URLHelper'; import { isValidURL } from '../helper/URLHelper';
import customAttributeMixin from '../mixins/customAttributeMixin';
const DATE_FORMAT = 'yyyy-MM-dd'; const DATE_FORMAT = 'yyyy-MM-dd';
import { useVuelidate } from '@vuelidate/core';
export default { export default {
components: { components: {
MultiselectDropdown, MultiselectDropdown,
}, },
mixins: [customAttributeMixin],
props: { props: {
label: { type: String, required: true }, label: { type: String, required: true },
values: { type: Array, default: () => [] }, values: { type: Array, default: () => [] },
value: { type: [String, Number, Boolean], default: '' }, value: { type: [String, Number, Boolean], default: '' },
showActions: { type: Boolean, default: false }, showActions: { type: Boolean, default: false },
attributeType: { type: String, default: 'text' }, attributeType: { type: String, default: 'text' },
attributeRegex: {
type: String,
default: null,
},
regexCue: { type: String, default: null },
regexEnabled: { type: Boolean, default: false },
attributeKey: { type: String, required: true }, attributeKey: { type: String, required: true },
contactId: { type: Number, default: null }, contactId: { type: Number, default: null },
}, },
setup() {
return { v$: useVuelidate() };
},
data() { data() {
return { return {
isEditing: false, isEditing: false,
@@ -210,17 +198,12 @@ export default {
return this.isAttributeTypeLink ? 'url' : this.attributeType; return this.isAttributeTypeLink ? 'url' : this.attributeType;
}, },
shouldShowErrorMessage() { shouldShowErrorMessage() {
return this.v$.editedValue.$error; return this.$v.editedValue.$error;
}, },
errorMessage() { errorMessage() {
if (this.v$.editedValue.url) { if (this.$v.editedValue.url) {
return this.$t('CUSTOM_ATTRIBUTES.VALIDATIONS.INVALID_URL'); return this.$t('CUSTOM_ATTRIBUTES.VALIDATIONS.INVALID_URL');
} }
if (!this.v$.editedValue.regexValidation) {
return this.regexCue
? this.regexCue
: this.$t('CUSTOM_ATTRIBUTES.VALIDATIONS.INVALID_INPUT');
}
return this.$t('CUSTOM_ATTRIBUTES.VALIDATIONS.REQUIRED'); return this.$t('CUSTOM_ATTRIBUTES.VALIDATIONS.REQUIRED');
}, },
}, },
@@ -238,15 +221,7 @@ export default {
}; };
} }
return { return {
editedValue: { editedValue: { required },
required,
regexValidation: value => {
return !(
this.attributeRegex &&
!this.getRegexp(this.attributeRegex).test(value)
);
},
},
}; };
}, },
mounted() { mounted() {
@@ -284,8 +259,8 @@ export default {
this.attributeType === 'date' this.attributeType === 'date'
? parseISO(this.editedValue) ? parseISO(this.editedValue)
: this.editedValue; : this.editedValue;
this.v$.$touch(); this.$v.$touch();
if (this.v$.$invalid) { if (this.$v.$invalid) {
return; return;
} }
this.isEditing = false; this.isEditing = false;
@@ -306,14 +281,11 @@ export default {
.checkbox-wrap { .checkbox-wrap {
@apply flex items-center; @apply flex items-center;
} }
.checkbox { .checkbox {
@apply my-0 mr-2 ml-0; @apply my-0 mr-2 ml-0;
} }
.attribute-name { .attribute-name {
@apply w-full text-slate-800 dark:text-slate-100; @apply w-full text-slate-800 dark:text-slate-100;
&.error { &.error {
@apply text-red-400 dark:text-red-500; @apply text-red-400 dark:text-red-500;
} }
@@ -330,7 +302,6 @@ export default {
.value { .value {
@apply bg-slate-50 dark:bg-slate-700 mb-0; @apply bg-slate-50 dark:bg-slate-700 mb-0;
} }
.edit-button { .edit-button {
@apply block; @apply block;
} }
@@ -340,12 +311,10 @@ export default {
::v-deep { ::v-deep {
.selector-wrap { .selector-wrap {
@apply m-0 top-1; @apply m-0 top-1;
.selector-name { .selector-name {
@apply ml-0; @apply ml-0;
} }
} }
.name { .name {
@apply ml-0; @apply ml-0;
} }
@@ -1,34 +0,0 @@
<template>
<div ref="observedElement" class="h-6 w-full" />
</template>
<script>
export default {
props: {
options: {
type: Object,
default: () => ({ root: document, rootMargin: '100px 0 100px 0)' }),
},
},
mounted() {
this.intersectionObserver = null;
this.registerInfiniteLoader();
},
beforeDestroy() {
this.unobserveInfiniteLoadObserver();
},
methods: {
registerInfiniteLoader() {
this.intersectionObserver = new IntersectionObserver(entries => {
if (entries && entries[0].isIntersecting) {
this.$emit('observed');
}
}, this.options);
this.intersectionObserver.observe(this.$refs.observedElement);
},
unobserveInfiniteLoadObserver() {
this.intersectionObserver.unobserve(this.$refs.observedElement);
},
},
};
</script>
@@ -1,33 +1,24 @@
<template> <template>
<transition name="network-notification-fade" tag="div"> <transition name="network-notification-fade" tag="div">
<div v-show="showNotification" class="fixed top-4 left-2 z-50 group"> <div v-show="showNotification" class="ui-notification-container">
<div <div class="ui-notification">
class="flex items-center justify-between py-1 px-2 w-full rounded-lg shadow-lg bg-yellow-200 dark:bg-yellow-700 relative" <fluent-icon icon="wifi-off" />
> <p class="ui-notification-text">
<fluent-icon {{
icon="wifi-off" useInstallationName(
class="text-yellow-700/50 dark:text-yellow-50" $t('NETWORK.NOTIFICATION.TEXT'),
size="18" globalConfig.installationName
/> )
<span }}
class="text-xs tracking-wide font-medium px-2 text-yellow-700/70 dark:text-yellow-50" </p>
> <woot-button variant="clear" size="small" @click="refreshPage">
{{ $t('NETWORK.NOTIFICATION.OFFLINE') }} {{ $t('NETWORK.BUTTON.REFRESH') }}
</span> </woot-button>
<woot-button <woot-button
:title="$t('NETWORK.BUTTON.REFRESH')" variant="smooth"
variant="clear"
size="small" size="small"
color-scheme="warning" color-scheme="warning"
icon="arrow-clockwise" icon="dismiss-circle"
class="visible transition-all duration-500 ease-in-out ml-1"
@click="refreshPage"
/>
<woot-button
variant="clear"
size="small"
color-scheme="warning"
icon="dismiss"
@click="closeNotification" @click="closeNotification"
/> />
</div> </div>
@@ -56,12 +47,7 @@ export default {
mounted() { mounted() {
window.addEventListener('offline', this.updateOnlineStatus); window.addEventListener('offline', this.updateOnlineStatus);
window.bus.$on(BUS_EVENTS.WEBSOCKET_DISCONNECT, () => { window.bus.$on(BUS_EVENTS.WEBSOCKET_DISCONNECT, () => {
// TODO: Remove this after completing the conversation list refetching this.updateOnlineStatus({ type: 'offline' });
// TODO: DIRTY FIX : CLEAN UP THIS WITH PROPER FIX, DELAYING THE RECONNECT FOR NOW
// THE CABLE IS FIRING IS VERY COMMON AND THUS INTERFERING USER EXPERIENCE
setTimeout(() => {
this.updateOnlineStatus({ type: 'offline' });
}, 4000);
}); });
}, },
@@ -86,3 +72,33 @@ export default {
}, },
}; };
</script> </script>
<style scoped lang="scss">
@import '~dashboard/assets/scss/mixins';
.ui-notification-container {
max-width: 25rem;
position: absolute;
right: var(--space-normal);
top: var(--space-normal);
z-index: var(--z-index-very-high);
}
.ui-notification {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
background-color: var(--y-100);
border-radius: var(--border-radius-medium);
box-shadow: var(--shadow-large);
min-width: 15rem;
padding: var(--space-normal);
}
.ui-notification-text {
margin: 0 var(--space-small);
}
</style>
+15 -15
View File
@@ -1,29 +1,29 @@
/* eslint no-plusplus: 0 */ /* eslint no-plusplus: 0 */
import AvatarUploader from './widgets/forms/AvatarUploader.vue'; import AvatarUploader from './widgets/forms/AvatarUploader.vue';
import Bar from './widgets/chart/BarChart'; import Bar from './widgets/chart/BarChart';
import Button from './ui/WootButton.vue'; import Button from './ui/WootButton';
import Code from './Code.vue'; import Code from './Code';
import ColorPicker from './widgets/ColorPicker.vue'; import ColorPicker from './widgets/ColorPicker';
import ConfirmDeleteModal from './widgets/modal/ConfirmDeleteModal.vue'; import ConfirmDeleteModal from './widgets/modal/ConfirmDeleteModal.vue';
import ConfirmModal from './widgets/modal/ConfirmationModal.vue'; import ConfirmModal from './widgets/modal/ConfirmationModal.vue';
import ContextMenu from './ui/ContextMenu.vue'; import ContextMenu from './ui/ContextMenu.vue';
import DeleteModal from './widgets/modal/DeleteModal.vue'; import DeleteModal from './widgets/modal/DeleteModal.vue';
import DropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue'; import DropdownItem from 'shared/components/ui/dropdown/DropdownItem';
import DropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue'; import DropdownMenu from 'shared/components/ui/dropdown/DropdownMenu';
import FeatureToggle from './widgets/FeatureToggle.vue'; import FeatureToggle from './widgets/FeatureToggle';
import HorizontalBar from './widgets/chart/HorizontalBarChart'; import HorizontalBar from './widgets/chart/HorizontalBarChart';
import Input from './widgets/forms/Input.vue'; import Input from './widgets/forms/Input.vue';
import PhoneInput from './widgets/forms/PhoneInput.vue'; import PhoneInput from './widgets/forms/PhoneInput.vue';
import Label from './ui/Label.vue'; import Label from './ui/Label';
import LoadingState from './widgets/LoadingState.vue'; import LoadingState from './widgets/LoadingState';
import Modal from './Modal.vue'; import Modal from './Modal';
import ModalHeader from './ModalHeader.vue'; import ModalHeader from './ModalHeader';
import ReportStatsCard from './widgets/ReportStatsCard.vue'; import ReportStatsCard from './widgets/ReportStatsCard';
import SidemenuIcon from './SidemenuIcon.vue'; import SidemenuIcon from './SidemenuIcon';
import Spinner from 'shared/components/Spinner.vue'; import Spinner from 'shared/components/Spinner';
import SubmitButton from './buttons/FormSubmitButton.vue'; import SubmitButton from './buttons/FormSubmitButton';
import Tabs from './ui/Tabs/Tabs'; import Tabs from './ui/Tabs/Tabs';
import TabsItem from './ui/Tabs/TabsItem.vue'; import TabsItem from './ui/Tabs/TabsItem';
import Thumbnail from './widgets/Thumbnail.vue'; import Thumbnail from './widgets/Thumbnail.vue';
const WootUIKit = { const WootUIKit = {
@@ -19,13 +19,13 @@
<form class="flex flex-col w-full" @submit.prevent="addAccount"> <form class="flex flex-col w-full" @submit.prevent="addAccount">
<div class="w-full"> <div class="w-full">
<label :class="{ error: v$.accountName.$error }"> <label :class="{ error: $v.accountName.$error }">
{{ $t('CREATE_ACCOUNT.FORM.NAME.LABEL') }} {{ $t('CREATE_ACCOUNT.FORM.NAME.LABEL') }}
<input <input
v-model.trim="accountName" v-model.trim="accountName"
type="text" type="text"
:placeholder="$t('CREATE_ACCOUNT.FORM.NAME.PLACEHOLDER')" :placeholder="$t('CREATE_ACCOUNT.FORM.NAME.PLACEHOLDER')"
@input="v$.accountName.$touch" @input="$v.accountName.$touch"
/> />
</label> </label>
</div> </div>
@@ -33,8 +33,8 @@
<div class="w-full"> <div class="w-full">
<woot-submit-button <woot-submit-button
:disabled=" :disabled="
v$.accountName.$invalid || $v.accountName.$invalid ||
v$.accountName.$invalid || $v.accountName.$invalid ||
uiFlags.isCreating uiFlags.isCreating
" "
:button-text="$t('CREATE_ACCOUNT.FORM.SUBMIT')" :button-text="$t('CREATE_ACCOUNT.FORM.SUBMIT')"
@@ -49,10 +49,9 @@
</template> </template>
<script> <script>
import { required, minLength } from '@vuelidate/validators'; import { required, minLength } from 'vuelidate/lib/validators';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import alertMixin from 'shared/mixins/alertMixin'; import alertMixin from 'shared/mixins/alertMixin';
import { useVuelidate } from '@vuelidate/core';
export default { export default {
mixins: [alertMixin], mixins: [alertMixin],
@@ -66,9 +65,6 @@ export default {
default: true, default: true,
}, },
}, },
setup() {
return { v$: useVuelidate() };
},
data() { data() {
return { return {
accountName: '', accountName: '',
@@ -136,7 +136,6 @@ export default {
toStateName: 'labels_list', toStateName: 'labels_list',
showModalForNewItem: true, showModalForNewItem: true,
modalName: 'AddLabel', modalName: 'AddLabel',
dataTestid: 'sidebar-new-label-button',
children: this.labels.map(label => ({ children: this.labels.map(label => ({
id: label.id, id: label.id,
label: label.title, label: label.title,
@@ -56,7 +56,7 @@
class="bg-slate-50 dark:bg-slate-700 rounded text-xxs font-medium mx-1 py-0 px-1" class="bg-slate-50 dark:bg-slate-700 rounded text-xxs font-medium mx-1 py-0 px-1"
:class=" :class="
isCountZero isCountZero
? `text-slate-300 dark:text-slate-500` ? `text-slate-300 dark:text-slate-700`
: `text-slate-700 dark:text-slate-50` : `text-slate-700 dark:text-slate-50`
" "
> >
@@ -78,7 +78,6 @@
variant="clear" variant="clear"
color-scheme="secondary" color-scheme="secondary"
icon="add" icon="add"
:data-testid="menuItem.dataTestid"
@click="e => newLinkClick(e, navigate)" @click="e => newLinkClick(e, navigate)"
> >
{{ $t(`SIDEBAR.${menuItem.newLinkTag}`) }} {{ $t(`SIDEBAR.${menuItem.newLinkTag}`) }}

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