Merge branch 'release/1.5.0'

This commit is contained in:
Sojan 2020-06-02 23:53:19 +05:30
commit 89dafa366e
403 changed files with 7809 additions and 1513 deletions

View file

@ -7,7 +7,7 @@ defaults: &defaults
working_directory: ~/build
docker:
# specify the version you desire here
- image: circleci/ruby:2.7.0-node-browsers
- image: circleci/ruby:2.7.1-node-browsers
# Specify service dependencies here if necessary
# CircleCI maintains a library of pre-built images

View file

@ -73,10 +73,6 @@ RAILS_LOG_TO_STDOUT=true
LOG_LEVEL=info
LOG_SIZE=500
# Credentials to access sidekiq dashboard in production
SIDEKIQ_AUTH_USERNAME=
SIDEKIQ_AUTH_PASSWORD=
### This environment variables are only required if you are setting up social media channels
#facebook
FB_VERIFY_TOKEN=
@ -106,3 +102,10 @@ CHARGEBEE_WEBHOOK_PASSWORD=
## generate a new key value here : https://d3v.one/vapid-key-generator/
# VAPID_PUBLIC_KEY=
# VAPID_PRIVATE_KEY=
## Bot Customizations
USE_INBOX_AVATAR_FOR_BOT=true
## Development Only Config
# if you want to use letter_opener for local emails
# LETTER_OPENER=true

View file

@ -12,6 +12,8 @@ Layout/LineLength:
Max: 150
Metrics/ClassLength:
Max: 125
Exclude:
- 'app/models/conversation.rb'
RSpec/ExampleLength:
Max: 25
Style/Documentation:
@ -30,6 +32,7 @@ Style/GlobalVars:
Exclude:
- 'config/initializers/redis.rb'
- 'lib/redis/alfred.rb'
- 'lib/global_config.rb'
Metrics/BlockLength:
Exclude:
- spec/**/*
@ -94,6 +97,8 @@ Rails/UniqueValidationWithoutIndex:
Exclude:
- 'app/models/channel/twitter_profile.rb'
- 'app/models/webhook.rb'
RSpec/NamedSubject:
Enabled: false
AllCops:
Exclude:
- 'bin/**/*'

View file

@ -1 +1 @@
2.7.0
2.7.1

View file

@ -177,6 +177,8 @@ linters:
allow_element_with_attribute: false
allow_element_with_class: false
allow_element_with_id: false
exclude:
- 'app/assets/stylesheets/administrate/components/_buttons.scss'
SelectorDepth:
enabled: true
@ -279,3 +281,4 @@ linters:
exclude:
- 'app/javascript/widget/assets/scss/_reset.scss'
- 'app/javascript/widget/assets/scss/sdk.css'
- 'app/assets/stylesheets/administrate/reset/_normalize.scss'

View file

@ -1,6 +1,6 @@
source 'https://rubygems.org'
ruby '2.7.0'
ruby '2.7.1'
##-- base gems for rails --##
gem 'rack-cors', require: 'rack/cors'
@ -49,6 +49,8 @@ gem 'devise_token_auth'
# authorization
gem 'jwt'
gem 'pundit'
# super admin
gem 'administrate'
##--- gems for pubsub service ---##
# https://karolgalanciak.com/blog/2019/11/30/from-activerecord-callbacks-to-publish-slash-subscribe-pattern-and-event-driven-design/
@ -62,7 +64,8 @@ gem 'facebook-messenger'
gem 'telegram-bot-ruby'
gem 'twilio-ruby', '~> 5.32.0'
# twitty will handle subscription of twitter account events
gem 'twitty', git: 'https://github.com/chatwoot/twitty'
# gem 'twitty', git: 'https://github.com/chatwoot/twitty'
gem 'twitty'
# facebook client
gem 'koala'
# Random name generator

View file

@ -1,10 +1,3 @@
GIT
remote: https://github.com/chatwoot/twitty
revision: af4f3e45dca55e42c64f7741a1fedfaa94d36419
specs:
twitty (0.1.0)
oauth
GIT
remote: https://github.com/sds/mock_redis
revision: 16d00789f0341a3aac35126c0ffe97a596753ff9
@ -25,85 +18,98 @@ GEM
specs:
action-cable-testing (0.6.1)
actioncable (>= 5.0)
actioncable (6.0.2.2)
actionpack (= 6.0.2.2)
actioncable (6.0.3.1)
actionpack (= 6.0.3.1)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
actionmailbox (6.0.2.2)
actionpack (= 6.0.2.2)
activejob (= 6.0.2.2)
activerecord (= 6.0.2.2)
activestorage (= 6.0.2.2)
activesupport (= 6.0.2.2)
actionmailbox (6.0.3.1)
actionpack (= 6.0.3.1)
activejob (= 6.0.3.1)
activerecord (= 6.0.3.1)
activestorage (= 6.0.3.1)
activesupport (= 6.0.3.1)
mail (>= 2.7.1)
actionmailer (6.0.2.2)
actionpack (= 6.0.2.2)
actionview (= 6.0.2.2)
activejob (= 6.0.2.2)
actionmailer (6.0.3.1)
actionpack (= 6.0.3.1)
actionview (= 6.0.3.1)
activejob (= 6.0.3.1)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
actionpack (6.0.2.2)
actionview (= 6.0.2.2)
activesupport (= 6.0.2.2)
actionpack (6.0.3.1)
actionview (= 6.0.3.1)
activesupport (= 6.0.3.1)
rack (~> 2.0, >= 2.0.8)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
actiontext (6.0.2.2)
actionpack (= 6.0.2.2)
activerecord (= 6.0.2.2)
activestorage (= 6.0.2.2)
activesupport (= 6.0.2.2)
actiontext (6.0.3.1)
actionpack (= 6.0.3.1)
activerecord (= 6.0.3.1)
activestorage (= 6.0.3.1)
activesupport (= 6.0.3.1)
nokogiri (>= 1.8.5)
actionview (6.0.2.2)
activesupport (= 6.0.2.2)
actionview (6.0.3.1)
activesupport (= 6.0.3.1)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.1, >= 1.2.0)
activejob (6.0.2.2)
activesupport (= 6.0.2.2)
activejob (6.0.3.1)
activesupport (= 6.0.3.1)
globalid (>= 0.3.6)
activemodel (6.0.2.2)
activesupport (= 6.0.2.2)
activerecord (6.0.2.2)
activemodel (= 6.0.2.2)
activesupport (= 6.0.2.2)
activestorage (6.0.2.2)
actionpack (= 6.0.2.2)
activejob (= 6.0.2.2)
activerecord (= 6.0.2.2)
activemodel (6.0.3.1)
activesupport (= 6.0.3.1)
activerecord (6.0.3.1)
activemodel (= 6.0.3.1)
activesupport (= 6.0.3.1)
activestorage (6.0.3.1)
actionpack (= 6.0.3.1)
activejob (= 6.0.3.1)
activerecord (= 6.0.3.1)
marcel (~> 0.3.1)
activesupport (6.0.2.2)
activesupport (6.0.3.1)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 0.7, < 2)
minitest (~> 5.1)
tzinfo (~> 1.1)
zeitwerk (~> 2.2)
zeitwerk (~> 2.2, >= 2.2.2)
acts-as-taggable-on (6.5.0)
activerecord (>= 5.0, < 6.1)
addressable (2.7.0)
public_suffix (>= 2.0.2, < 5.0)
administrate (0.13.0)
actionpack (>= 4.2)
actionview (>= 4.2)
activerecord (>= 4.2)
autoprefixer-rails (>= 6.0)
datetime_picker_rails (~> 0.0.7)
jquery-rails (>= 4.0)
kaminari (>= 1.0)
momentjs-rails (~> 2.8)
sassc-rails (~> 2.1)
selectize-rails (~> 0.6)
annotate (3.1.1)
activerecord (>= 3.2, < 7.0)
rake (>= 10.4, < 14.0)
ast (2.4.0)
attr_extras (6.2.3)
autoprefixer-rails (9.7.6)
execjs
aws-eventstream (1.1.0)
aws-partitions (1.296.0)
aws-sdk-core (3.94.0)
aws-partitions (1.317.0)
aws-sdk-core (3.96.1)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.239.0)
aws-sigv4 (~> 1.1)
jmespath (~> 1.0)
aws-sdk-kms (1.30.0)
aws-sdk-kms (1.31.0)
aws-sdk-core (~> 3, >= 3.71.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.61.2)
aws-sdk-core (~> 3, >= 3.83.0)
aws-sdk-s3 (1.65.0)
aws-sdk-core (~> 3, >= 3.96.1)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.1)
aws-sigv4 (1.1.1)
aws-sigv4 (1.1.3)
aws-eventstream (~> 1.0, >= 1.0.2)
axiom-types (0.1.1)
descendants_tracker (~> 0.0.4)
@ -120,8 +126,8 @@ GEM
bindex (0.8.1)
bootsnap (1.4.6)
msgpack (~> 1.0)
brakeman (4.8.1)
browser (4.0.0)
brakeman (4.8.2)
browser (4.1.0)
builder (3.2.4)
bullet (6.1.0)
activesupport (>= 3.0.0)
@ -131,7 +137,7 @@ GEM
bundler-audit (0.6.1)
bundler (>= 1.2.0, < 3)
thor (~> 0.18)
byebug (11.1.1)
byebug (11.1.3)
chargebee (2.7.5)
json_pure (~> 2.1)
rest-client (>= 1.8, < 3.0)
@ -141,6 +147,8 @@ GEM
concurrent-ruby (1.1.6)
connection_pool (2.2.2)
crass (1.0.6)
datetime_picker_rails (0.0.7)
momentjs-rails (>= 2.8.1)
declarative (0.0.10)
declarative-option (0.1.0)
descendants_tracker (0.0.4)
@ -167,13 +175,13 @@ GEM
equalizer (0.0.11)
erubi (1.9.0)
execjs (2.7.0)
facebook-messenger (1.4.1)
facebook-messenger (1.5.0)
httparty (~> 0.13, >= 0.13.7)
rack (>= 1.4.5)
factory_bot (5.1.2)
factory_bot (5.2.0)
activesupport (>= 4.2.0)
factory_bot_rails (5.1.1)
factory_bot (~> 5.1.0)
factory_bot_rails (5.2.0)
factory_bot (~> 5.2.0)
railties (>= 4.2.0)
faker (2.11.0)
i18n (>= 1.6, < 2)
@ -186,7 +194,7 @@ GEM
foreman (0.87.1)
globalid (0.4.2)
activesupport (>= 4.2.0)
google-api-client (0.38.0)
google-api-client (0.39.4)
addressable (~> 2.5, >= 2.5.1)
googleauth (~> 0.9)
httpclient (>= 2.8.1, < 3.0)
@ -200,7 +208,7 @@ GEM
google-cloud-env (1.3.1)
faraday (>= 0.17.3, < 2.0)
google-cloud-errors (1.0.0)
google-cloud-storage (1.26.0)
google-cloud-storage (1.26.1)
addressable (~> 2.5)
digest-crc (~> 0.4)
google-api-client (~> 0.33)
@ -217,7 +225,7 @@ GEM
groupdate (5.0.0)
activesupport (>= 5)
haikunator (1.1.0)
hana (1.3.5)
hana (1.3.6)
hashie (4.1.0)
hkdf (0.3.0)
http-accept (1.7.0)
@ -231,25 +239,28 @@ GEM
concurrent-ruby (~> 1.0)
ice_nine (0.11.2)
inflecto (0.0.2)
jaro_winkler (1.5.4)
jbuilder (2.10.0)
activesupport (>= 5.0.0)
jmespath (1.4.0)
jquery-rails (4.4.0)
rails-dom-testing (>= 1, < 3)
railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
json (2.3.0)
json_pure (2.3.0)
jwt (2.2.1)
kaminari (1.2.0)
kaminari (1.2.1)
activesupport (>= 4.1.0)
kaminari-actionview (= 1.2.0)
kaminari-activerecord (= 1.2.0)
kaminari-core (= 1.2.0)
kaminari-actionview (1.2.0)
kaminari-actionview (= 1.2.1)
kaminari-activerecord (= 1.2.1)
kaminari-core (= 1.2.1)
kaminari-actionview (1.2.1)
actionview
kaminari-core (= 1.2.0)
kaminari-activerecord (1.2.0)
kaminari-core (= 1.2.1)
kaminari-activerecord (1.2.1)
activerecord
kaminari-core (= 1.2.0)
kaminari-core (1.2.0)
kaminari-core (= 1.2.1)
kaminari-core (1.2.1)
koala (3.0.0)
addressable
faraday
@ -272,12 +283,14 @@ GEM
method_source (1.0.0)
mime-types (3.3.1)
mime-types-data (~> 3.2015)
mime-types-data (3.2019.1009)
mimemagic (0.3.4)
mime-types-data (3.2020.0512)
mimemagic (0.3.5)
mini_magick (4.10.1)
mini_mime (1.0.2)
mini_portile2 (2.4.0)
minitest (5.14.0)
minitest (5.14.1)
momentjs-rails (2.20.1)
railties (>= 3.1)
msgpack (1.3.3)
multi_json (1.14.1)
multi_xml (0.6.0)
@ -290,7 +303,7 @@ GEM
orm_adapter (0.5.0)
os (1.1.0)
parallel (1.19.1)
parser (2.7.1.1)
parser (2.7.1.2)
ast (~> 2.4.0)
pg (1.2.3)
pry (0.13.1)
@ -298,8 +311,8 @@ GEM
method_source (~> 1.0)
pry-rails (0.3.9)
pry (>= 0.10.4)
public_suffix (4.0.4)
puma (4.3.3)
public_suffix (4.0.5)
puma (4.3.5)
nio4r (~> 2.0)
pundit (2.1.0)
activesupport (>= 3.0.0)
@ -314,38 +327,38 @@ GEM
rack
rack-test (1.1.0)
rack (>= 1.0, < 3)
rails (6.0.2.2)
actioncable (= 6.0.2.2)
actionmailbox (= 6.0.2.2)
actionmailer (= 6.0.2.2)
actionpack (= 6.0.2.2)
actiontext (= 6.0.2.2)
actionview (= 6.0.2.2)
activejob (= 6.0.2.2)
activemodel (= 6.0.2.2)
activerecord (= 6.0.2.2)
activestorage (= 6.0.2.2)
activesupport (= 6.0.2.2)
rails (6.0.3.1)
actioncable (= 6.0.3.1)
actionmailbox (= 6.0.3.1)
actionmailer (= 6.0.3.1)
actionpack (= 6.0.3.1)
actiontext (= 6.0.3.1)
actionview (= 6.0.3.1)
activejob (= 6.0.3.1)
activemodel (= 6.0.3.1)
activerecord (= 6.0.3.1)
activestorage (= 6.0.3.1)
activesupport (= 6.0.3.1)
bundler (>= 1.3.0)
railties (= 6.0.2.2)
railties (= 6.0.3.1)
sprockets-rails (>= 2.0.0)
rails-dom-testing (2.0.3)
activesupport (>= 4.2.0)
nokogiri (>= 1.6)
rails-html-sanitizer (1.3.0)
loofah (~> 2.3)
railties (6.0.2.2)
actionpack (= 6.0.2.2)
activesupport (= 6.0.2.2)
railties (6.0.3.1)
actionpack (= 6.0.3.1)
activesupport (= 6.0.3.1)
method_source
rake (>= 0.8.7)
thor (>= 0.20.3, < 2.0)
rainbow (3.0.0)
rake (13.0.1)
rb-fsevent (0.10.3)
rb-fsevent (0.10.4)
rb-inotify (0.10.1)
ffi (~> 1.0)
redis (4.1.3)
redis (4.1.4)
redis-namespace (1.7.0)
redis (>= 3.0.4)
redis-rack-cache (2.2.1)
@ -367,15 +380,15 @@ GEM
netrc (~> 0.8)
retriable (3.1.2)
rexml (3.2.4)
rspec-core (3.9.1)
rspec-support (~> 3.9.1)
rspec-expectations (3.9.1)
rspec-core (3.9.2)
rspec-support (~> 3.9.3)
rspec-expectations (3.9.2)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.9.0)
rspec-mocks (3.9.1)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.9.0)
rspec-rails (4.0.0)
rspec-rails (4.0.1)
actionpack (>= 4.2)
activesupport (>= 4.2)
railties (>= 4.2)
@ -383,9 +396,8 @@ GEM
rspec-expectations (~> 3.9)
rspec-mocks (~> 3.9)
rspec-support (~> 3.9)
rspec-support (3.9.2)
rubocop (0.81.0)
jaro_winkler (~> 1.5.1)
rspec-support (3.9.3)
rubocop (0.83.0)
parallel (~> 1.10)
parser (>= 2.7.0.1)
rainbow (>= 2.2.2, < 4.0)
@ -398,7 +410,7 @@ GEM
activesupport
rack (>= 1.1)
rubocop (>= 0.72.0)
rubocop-rspec (1.38.1)
rubocop-rspec (1.39.0)
rubocop (>= 0.68.1)
ruby-progressbar (1.10.1)
sass (3.7.4)
@ -406,6 +418,14 @@ GEM
sass-listen (4.0.0)
rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7)
sassc (2.3.0)
ffi (~> 1.9)
sassc-rails (2.1.2)
railties (>= 4.0.0)
sassc (>= 2.0)
sprockets (> 3.0)
sprockets-rails
tilt
scout_apm (2.6.7)
parser
scss_lint (0.59.0)
@ -413,12 +433,13 @@ GEM
seed_dump (3.3.1)
activerecord (>= 4)
activesupport (>= 4)
selectize-rails (0.12.6)
semantic_range (2.3.0)
sentry-raven (3.0.0)
faraday (>= 1.0)
shoulda-matchers (4.3.0)
activesupport (>= 4.2.0)
sidekiq (6.0.6)
sidekiq (6.0.7)
connection_pool (>= 2.2.2)
rack (~> 2.0)
rack-protection (>= 2.0.0)
@ -448,9 +469,10 @@ GEM
faraday
inflecto
virtus
telephone_number (1.4.6)
telephone_number (1.4.7)
thor (0.20.3)
thread_safe (0.3.6)
tilt (2.0.10)
time_diff (0.3.0)
activesupport
i18n
@ -458,9 +480,11 @@ GEM
faraday (~> 1.0.0)
jwt (>= 1.5, <= 2.5)
nokogiri (>= 1.6, < 2.0)
twitty (0.1.1)
oauth
tzinfo (1.2.7)
thread_safe (~> 0.1)
tzinfo-data (1.2019.3)
tzinfo-data (1.2020.1)
tzinfo (>= 1.0.0)
uber (0.1.0)
uglifier (4.2.0)
@ -480,12 +504,12 @@ GEM
equalizer (~> 0.0, >= 0.0.9)
warden (1.2.8)
rack (>= 2.0.6)
web-console (4.0.1)
web-console (4.0.2)
actionview (>= 6.0.0)
activemodel (>= 6.0.0)
bindex (>= 0.4.0)
railties (>= 6.0.0)
webpacker (5.0.1)
webpacker (5.1.1)
activesupport (>= 5.2)
rack-proxy (>= 0.6.1)
railties (>= 5.2)
@ -505,6 +529,7 @@ PLATFORMS
DEPENDENCIES
action-cable-testing
acts-as-taggable-on
administrate
annotate
attr_extras
aws-sdk-s3
@ -566,7 +591,7 @@ DEPENDENCIES
telephone_number
time_diff
twilio-ruby (~> 5.32.0)
twitty!
twitty
tzinfo-data
uglifier
valid_email2
@ -576,7 +601,7 @@ DEPENDENCIES
wisper (= 2.0.0)
RUBY VERSION
ruby 2.7.0p0
ruby 2.7.1p83
BUNDLED WITH
2.1.2
2.1.4

View file

@ -1,2 +1,3 @@
release: bundle exec rake db:migrate
web: bin/rails server -p $PORT -e $RAILS_ENV
worker: bundle exec sidekiq -C config/sidekiq.yml

View file

@ -19,8 +19,8 @@ ___
<a href="https://hub.docker.com/r/chatwoot/chatwoot/"><img src="https://img.shields.io/docker/cloud/build/chatwoot/chatwoot" alt="Docker Build Badge"></a>
<img src="https://img.shields.io/github/license/chatwoot/chatwoot" alt="License">
<img src="https://img.shields.io/github/commit-activity/m/chatwoot/chatwoot" alt="Commits-per-month">
<img src="https://img.shields.io/discord/647412545203994635" alt="Discord">
<a href="https://discord.gg/cJXdrwS"><img src="https://img.shields.io/badge/chat-Discord-violet?logo=discord" alt="Chat on Discord"></a>
<a title="Crowdin" target="_self" href="https://chatwoot.crowdin.com/chatwoot"><img src="https://badges.crowdin.net/e/37ced7eba411064bd792feb3b7a28b16/localized.svg"></a>
<a href="https://discord.gg/cJXdrwS"><img src="https://img.shields.io/discord/647412545203994635" alt="Discord"></a>
</p>
![ChatUI progess](https://s3.us-west-2.amazonaws.com/gh-assets.chatwoot.com/chatwoot-dashboard-assets.png)

File diff suppressed because one or more lines are too long

View file

@ -1 +1,4 @@
//= link_tree ../images
//= link administrate/application.css
//= link administrate/application.js
//= link dashboardChart.js

View file

@ -0,0 +1,55 @@
// eslint-disable-next-line
function prepareData(data) {
var labels = [];
var dataSet = [];
data.forEach(item => {
labels.push(item[0]);
dataSet.push(item[1]);
});
return { labels, dataSet };
}
function getChartOptions() {
var fontFamily =
'Inter,-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif';
return {
responsive: true,
legend: { labels: { fontFamily } },
scales: {
xAxes: [
{
barPercentage: 1.26,
ticks: { fontFamily },
gridLines: { display: false },
},
],
yAxes: [
{
ticks: { fontFamily },
gridLines: { display: false },
},
],
},
};
}
// eslint-disable-next-line
function drawSuperAdminDashboard(data) {
var ctx = document.getElementById('dashboard-chart').getContext('2d');
var chartData = prepareData(data);
// eslint-disable-next-line
new Chart(ctx, {
type: 'bar',
data: {
labels: chartData.labels,
datasets: [
{
label: 'Conversations',
data: chartData.dataSet,
backgroundColor: '#1f93ff',
},
],
},
options: getChartOptions(),
});
}

View file

@ -0,0 +1,32 @@
@charset 'utf-8';
@import 'reset/normalize';
@import 'utilities/variables';
@import 'utilities/text-color';
@import 'selectize';
@import 'datetime_picker';
@import 'library/clearfix';
@import 'library/data-label';
@import 'library/variables';
@import 'base/forms';
@import 'base/layout';
@import 'base/lists';
@import 'base/tables';
@import 'base/typography';
@import 'components/app-container';
@import 'components/attributes';
@import 'components/buttons';
@import 'components/cells';
@import 'components/field-unit';
@import 'components/flashes';
@import 'components/form-actions';
@import 'components/main-content';
@import 'components/navigation';
@import 'components/pagination';
@import 'components/search';
@import 'components/reports';

View file

@ -0,0 +1,103 @@
fieldset {
background-color: transparent;
border: 0;
margin: 0;
padding: 0;
}
legend {
font-weight: $font-weight-medium;
margin: 0;
padding: 0;
}
label {
display: block;
font-weight: $font-weight-medium;
margin: 0;
}
input,
select {
display: block;
font-family: $base-font-family;
font-size: $base-font-size;
}
input,
select,
textarea {
display: block;
font-family: $base-font-family;
font-size: 16px;
}
[type="color"],
[type="date"],
[type="datetime-local"],
[type="email"],
[type="month"],
[type="number"],
[type="password"],
[type="search"],
[type="tel"],
[type="text"],
[type="time"],
[type="url"],
[type="week"],
input:not([type]),
textarea {
appearance: none;
background-color: $white;
border: $base-border;
border-radius: $base-border-radius;
padding: 0.5em;
transition: border-color $base-duration $base-timing;
width: 100%;
&:hover {
border-color: mix($black, $base-border-color, 20%);
}
&:focus {
border-color: $action-color;
outline: none;
}
&:disabled {
background-color: mix($black, $white, 5%);
cursor: not-allowed;
&:hover {
border: $base-border;
}
}
}
textarea {
resize: vertical;
}
[type="checkbox"],
[type="radio"] {
display: inline;
margin-right: $small-spacing / 2;
}
[type="file"] {
width: 100%;
}
select {
width: 100%;
}
[type="checkbox"],
[type="radio"],
[type="file"],
select {
&:focus {
outline: $focus-outline;
outline-offset: $focus-outline-offset;
}
}

View file

@ -0,0 +1,22 @@
html {
background-color: $color-white;
box-sizing: border-box;
font-size: 10px;
-webkit-font-smoothing: antialiased;
}
*,
*::before,
*::after {
box-sizing: inherit;
}
figure {
margin: 0;
}
img,
picture {
margin: 0;
max-width: 100%;
}

View file

@ -0,0 +1,19 @@
ul,
ol {
list-style-type: none;
margin: 0;
padding: 0;
}
dl {
margin-bottom: $small-spacing;
dt {
font-weight: $font-weight-medium;
margin-top: $small-spacing;
}
dd {
margin: 0;
}
}

View file

@ -0,0 +1,71 @@
table {
border-collapse: collapse;
font-size: $font-size-default;
text-align: left;
width: 100%;
a {
color: inherit;
text-decoration: none;
}
}
tr {
border-bottom: $base-border;
th {
font-weight: $font-weight-medium;
&.cell-label--avatar-field {
a {
display: none;
}
}
}
}
tbody tr {
&:hover {
background-color: $base-background-color;
cursor: pointer;
}
&:focus {
outline: $focus-outline;
outline-offset: -($focus-outline-width);
}
td {
&.cell-data--avatar-field {
line-height: 1;
text-align: center;
img {
border-radius: 50%;
height: $space-large;
max-height: $space-large;
width: $space-large;
}
}
}
}
td,
th {
padding: $space-slab;
vertical-align: middle;
}
td:first-child,
th:first-child {
padding-left: 0;
}
td:last-child,
th:last-child {
padding-right: 0;
}
td img {
max-height: 2rem;
}

View file

@ -0,0 +1,44 @@
body {
color: $base-font-color;
font-family: $base-font-family;
font-size: $base-font-size;
line-height: $base-line-height;
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: $heading-font-family;
font-size: $base-font-size;
line-height: $heading-line-height;
margin: 0;
}
p {
margin: 0 0 $small-spacing;
}
a {
color: $action-color;
transition: color $base-duration $base-timing;
&:hover {
color: mix($black, $action-color, 25%);
}
&:focus {
outline: $focus-outline;
outline-offset: $focus-outline-offset;
}
}
hr {
border-bottom: $base-border;
border-left: 0;
border-right: 0;
border-top: 0;
margin: $base-spacing 0;
}

View file

@ -0,0 +1,8 @@
.app-container {
align-items: stretch;
display: flex;
margin-left: auto;
margin-right: auto;
max-width: 100rem;
min-height: 100vh;
}

View file

@ -0,0 +1,26 @@
.attribute-label {
@include data-label;
clear: left;
float: left;
margin-bottom: $base-spacing;
margin-top: 0.25em;
text-align: right;
width: calc(15% - 1rem);
}
.preserve-whitespace {
white-space: pre-wrap;
word-wrap: break-word;
}
.attribute-data {
float: left;
margin-bottom: $base-spacing;
margin-left: 2rem;
width: calc(85% - 1rem);
}
.attribute--nested {
border: $base-border;
padding: $small-spacing;
}

View file

@ -0,0 +1,50 @@
button,
input[type="button"],
input[type="reset"],
input[type="submit"],
.button {
appearance: none;
background-color: $color-woot;
border: 0;
border-radius: $base-border-radius;
color: $white;
cursor: pointer;
display: inline-block;
font-size: $font-size-default;
-webkit-font-smoothing: antialiased;
font-weight: $font-weight-medium;
line-height: 1;
padding: $space-one $space-two;
text-decoration: none;
transition: background-color $base-duration $base-timing;
user-select: none;
vertical-align: middle;
white-space: nowrap;
&:hover {
background-color: mix($black, $color-woot, 20%);
color: $white;
}
&:focus {
outline: $focus-outline;
outline-offset: $focus-outline-offset;
}
&:disabled {
cursor: not-allowed;
opacity: 0.5;
&:hover {
background-color: $color-woot;
}
}
}
.button--alt {
background-color: transparent;
border: $base-border;
border-color: $blue;
color: $blue;
margin-bottom: $base-spacing;
}

View file

@ -0,0 +1,45 @@
.cell-label {
&:hover {
a {
color: $action-color;
}
svg {
fill: $action-color;
transform: rotate(180deg);
}
}
a {
color: inherit;
display: inline-block;
transition: color $base-duration $base-timing;
width: 100%;
}
}
.cell-label--asc,
.cell-label--desc {
font-weight: $font-weight-medium;
}
.cell-label__sort-indicator {
float: right;
margin-left: 5px;
svg {
fill: $hint-grey;
height: 13px;
transition: transform $base-duration $base-timing;
width: 13px;
}
}
.cell-label__sort-indicator--desc {
transform: rotate(180deg);
}
.cell-data--number,
.cell-label--number {
text-align: right;
}

View file

@ -0,0 +1,54 @@
.field-unit {
@include administrate-clearfix;
align-items: center;
display: flex;
margin-bottom: $base-spacing;
position: relative;
width: 100%;
}
.field-unit__label {
float: left;
margin-left: 1rem;
text-align: right;
width: calc(15% - 1rem);
}
.field-unit__field {
float: left;
margin-left: 2rem;
max-width: 50rem;
width: 100%;
}
.field-unit--nested {
border: $base-border;
margin-left: 7.5%;
max-width: 60rem;
padding: $small-spacing;
width: 100%;
.field-unit__field {
width: 100%;
}
.field-unit__label {
width: 10rem;
}
}
.field-unit--required {
label::after {
color: $red;
content: ' *';
}
}
.attribute-data--avatar-field {
height: $space-larger;
width: $space-larger;
img {
border-radius: 50%;
}
}

View file

@ -0,0 +1,28 @@
$base-spacing: 1.5em !default;
$flashes: (
"alert": #fff6bf,
"error": #fbe3e4,
"notice": #e5edf8,
"success": #e6efc2,
) !default;
@each $flash-type, $color in $flashes {
.flash-#{$flash-type} {
background-color: $color;
color: mix($black, $color, 60%);
display: block;
margin-bottom: $base-spacing / 2;
padding: $base-spacing / 2;
text-align: center;
a {
color: mix($black, $color, 70%);
text-decoration: underline;
&:focus,
&:hover {
color: mix($black, $color, 90%);
}
}
}
}

View file

@ -0,0 +1,3 @@
.form-actions {
margin-left: calc(15% + 2rem);
}

View file

@ -0,0 +1,26 @@
.main-content {
font-size: $font-size-default;
left: 23rem;
position: absolute;
right: 0;
top: 0;
}
.main-content__body {
padding: $space-two;
}
.main-content__header {
align-items: center;
background-color: $color-white;
border-bottom: 1px solid $color-border;
display: flex;
min-height: 5.6rem;
padding: $space-small $space-normal;
}
.main-content__page-title {
font-size: $font-size-large;
font-weight: $font-weight-medium;
margin-right: auto;
}

View file

@ -0,0 +1,72 @@
.logo-brand {
margin-bottom: $space-normal;
padding: $space-normal $space-smaller;
text-align: center;
}
.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: 23rem;
z-index: 1023;
li {
align-items: center;
display: flex;
a {
color: $color-gray;
text-decoration: none;
}
i {
min-width: $space-medium;
}
}
}
.navigation__link {
background-color: transparent;
color: $color-gray;
display: block;
line-height: 1;
margin-bottom: $space-smaller;
padding: $space-one;
&: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;
}

View file

@ -0,0 +1,19 @@
.pagination {
font-size: $font-size-default;
margin-top: $base-spacing;
padding-left: $base-spacing;
padding-right: $base-spacing;
text-align: center;
.first,
.prev,
.page,
.next,
.last {
margin: $small-spacing;
}
.current {
font-weight: $font-weight-medium;
}
}

View file

@ -0,0 +1,15 @@
.report--list {
display: flex;
padding: 0 $space-two $space-larger;
}
.report-card {
flex: 1;
font-size: $font-size-small;
text-align: center;
.metric {
font-size: $font-size-bigger;
font-weight: 200;
}
}

View file

@ -0,0 +1,44 @@
.search {
margin-left: auto;
margin-right: 2rem;
max-width: 44rem;
position: relative;
width: 100%;
}
.search__input {
background: $grey-1;
padding-left: $space-normal * 2.5;
padding-right: $space-normal * 2.5;
}
.search__eyeglass-icon {
fill: $grey-7;
height: $space-normal;
left: $space-normal;
position: absolute;
top: 50%;
transform: translateY(-50%);
width: $space-normal;
}
.search__clear-link {
height: $space-normal;
position: absolute;
right: $space-normal * 0.75;
top: 50%;
transform: translateY(-50%);
width: $space-normal;
}
.search__clear-icon {
fill: $grey-5;
height: $space-normal;
position: absolute;
transition: fill $base-duration $base-timing;
width: $space-normal;
&:hover {
fill: $action-color;
}
}

View file

@ -0,0 +1,7 @@
@mixin administrate-clearfix {
&::after {
clear: both;
content: '';
display: block;
}
}

View file

@ -0,0 +1,8 @@
@mixin data-label {
color: $hint-grey;
font-size: 0.8em;
font-weight: 400;
letter-spacing: 0.0357em;
position: relative;
text-transform: uppercase;
}

View file

@ -0,0 +1,61 @@
// Typography
$base-font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif !default;
$heading-font-family: $base-font-family !default;
$base-font-size: 10px !default;
$base-line-height: 1.5 !default;
$heading-line-height: 1.2 !default;
// Other Sizes
$base-border-radius: 4px !default;
$base-spacing: $base-line-height * 1em !default;
$small-spacing: $base-spacing / 2 !default;
// Colors
$white: #fff !default;
$black: #000 !default;
$blue: #1f93ff !default;
$red: #ff382d !default;
$light-yellow: #ffc532 !default;
$light-green: #44ce4b !default;
$grey-0: #f6f7f7 !default;
$grey-1: #f0f4f5 !default;
$grey-2: #cfd8dc !default;
$grey-5: #adb5bd !default;
$grey-7: #293f54 !default;
$hint-grey: #7b808c !default;
// Font Colors
$base-font-color: $grey-7 !default;
$action-color: $blue !default;
// Background Colors
$base-background-color: $grey-0 !default;
// Focus
$focus-outline-color: transparentize($action-color, 0.4);
$focus-outline-width: 3px;
$focus-outline: $focus-outline-width solid $focus-outline-color;
$focus-outline-offset: 1px;
// Flash Colors
$flash-colors: (
alert: $light-yellow,
error: $red,
notice: mix($white, $blue, 50%),
success: $light-green
);
// Border
$base-border-color: $grey-1 !default;
$base-border: 1px solid $base-border-color !default;
// Transitions
$base-duration: 250ms !default;
$base-timing: ease-in-out !default;

View file

@ -0,0 +1,447 @@
/*! normalize.css v7.0.0 | MIT License | github.com/necolas/normalize.css */
/* Document
========================================================================== */
/**
* 1. Correct the line height in all browsers.
* 2. Prevent adjustments of font size after orientation changes in
* IE on Windows Phone and in iOS.
*/
html {
line-height: 1.15; /* 1 */
-ms-text-size-adjust: 100%; /* 2 */
-webkit-text-size-adjust: 100%; /* 2 */
}
/* Sections
========================================================================== */
/**
* Remove the margin in all browsers (opinionated).
*/
body {
margin: 0;
}
/**
* Add the correct display in IE 9-.
*/
article,
aside,
footer,
header,
nav,
section {
display: block;
}
/**
* Correct the font size and margin on `h1` elements within `section` and
* `article` contexts in Chrome, Firefox, and Safari.
*/
h1 {
font-size: 2em;
margin: 0.67em 0;
}
/* Grouping content
========================================================================== */
/**
* Add the correct display in IE 9-.
* 1. Add the correct display in IE.
*/
figcaption,
figure,
main { /* 1 */
display: block;
}
/**
* Add the correct margin in IE 8.
*/
figure {
margin: 1em 40px;
}
/**
* 1. Add the correct box sizing in Firefox.
* 2. Show the overflow in Edge and IE.
*/
hr {
box-sizing: content-box; /* 1 */
height: 0; /* 1 */
overflow: visible; /* 2 */
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
pre {
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
}
/* Text-level semantics
========================================================================== */
/**
* 1. Remove the gray background on active links in IE 10.
* 2. Remove gaps in links underline in iOS 8+ and Safari 8+.
*/
a {
background-color: transparent; /* 1 */
-webkit-text-decoration-skip: objects; /* 2 */
}
/**
* 1. Remove the bottom border in Chrome 57- and Firefox 39-.
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
*/
abbr[title] {
border-bottom: none; /* 1 */
text-decoration: underline; /* 2 */
text-decoration: underline dotted; /* 2 */
}
/**
* Prevent the duplicate application of `bolder` by the next rule in Safari 6.
*/
b,
strong {
font-weight: inherit;
}
/**
* Add the correct font weight in Chrome, Edge, and Safari.
*/
b,
strong {
font-weight: bolder;
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp {
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
}
/**
* Add the correct font style in Android 4.3-.
*/
dfn {
font-style: italic;
}
/**
* Add the correct background and color in IE 9-.
*/
mark {
background-color: #ff0;
color: #000;
}
/**
* Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/**
* Prevent `sub` and `sup` elements from affecting the line height in
* all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/* Embedded content
========================================================================== */
/**
* Add the correct display in IE 9-.
*/
audio,
video {
display: inline-block;
}
/**
* Add the correct display in iOS 4-7.
*/
audio:not([controls]) {
display: none;
height: 0;
}
/**
* Remove the border on images inside links in IE 10-.
*/
img {
border-style: none;
}
/**
* Hide the overflow in IE.
*/
svg:not(:root) {
overflow: hidden;
}
/* Forms
========================================================================== */
/**
* 1. Change the font styles in all browsers (opinionated).
* 2. Remove the margin in Firefox and Safari.
*/
button,
input,
optgroup,
select,
textarea {
font-family: sans-serif; /* 1 */
font-size: 100%; /* 1 */
line-height: 1.15; /* 1 */
margin: 0; /* 2 */
}
/**
* Show the overflow in IE.
* 1. Show the overflow in Edge.
*/
button,
input { /* 1 */
overflow: visible;
}
/**
* Remove the inheritance of text transform in Edge, Firefox, and IE.
* 1. Remove the inheritance of text transform in Firefox.
*/
button,
select { /* 1 */
text-transform: none;
}
/**
* 1. Prevent a WebKit bug where (2) destroys native `audio` and `video`
* controls in Android 4.
* 2. Correct the inability to style clickable types in iOS and Safari.
*/
button,
html [type="button"], /* 1 */
[type="reset"],
[type="submit"] {
-webkit-appearance: button; /* 2 */
}
/**
* Remove the inner border and padding in Firefox.
*/
button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
border-style: none;
padding: 0;
}
/**
* Restore the focus styles unset by the previous rule.
*/
button:-moz-focusring,
[type="button"]:-moz-focusring,
[type="reset"]:-moz-focusring,
[type="submit"]:-moz-focusring {
outline: 1px dotted ButtonText;
}
/**
* Correct the padding in Firefox.
*/
fieldset {
padding: 0.35em 0.75em 0.625em;
}
/**
* 1. Correct the text wrapping in Edge and IE.
* 2. Correct the color inheritance from `fieldset` elements in IE.
* 3. Remove the padding so developers are not caught out when they zero out
* `fieldset` elements in all browsers.
*/
legend {
box-sizing: border-box; /* 1 */
color: inherit; /* 2 */
display: table; /* 1 */
max-width: 100%; /* 1 */
padding: 0; /* 3 */
white-space: normal; /* 1 */
}
/**
* 1. Add the correct display in IE 9-.
* 2. Add the correct vertical alignment in Chrome, Firefox, and Opera.
*/
progress {
display: inline-block; /* 1 */
vertical-align: baseline; /* 2 */
}
/**
* Remove the default vertical scrollbar in IE.
*/
textarea {
overflow: auto;
}
/**
* 1. Add the correct box sizing in IE 10-.
* 2. Remove the padding in IE 10-.
*/
[type="checkbox"],
[type="radio"] {
box-sizing: border-box; /* 1 */
padding: 0; /* 2 */
}
/**
* Correct the cursor style of increment and decrement buttons in Chrome.
*/
[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
height: auto;
}
/**
* 1. Correct the odd appearance in Chrome and Safari.
* 2. Correct the outline style in Safari.
*/
[type="search"] {
-webkit-appearance: textfield; /* 1 */
outline-offset: -2px; /* 2 */
}
/**
* Remove the inner padding and cancel buttons in Chrome and Safari on macOS.
*/
[type="search"]::-webkit-search-cancel-button,
[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* 1. Correct the inability to style clickable types in iOS and Safari.
* 2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button; /* 1 */
font: inherit; /* 2 */
}
/* Interactive
========================================================================== */
/*
* Add the correct display in IE 9-.
* 1. Add the correct display in Edge, IE, and Firefox.
*/
details, /* 1 */
menu {
display: block;
}
/*
* Add the correct display in all browsers.
*/
summary {
display: list-item;
}
/* Scripting
========================================================================== */
/**
* Add the correct display in IE 9-.
*/
canvas {
display: inline-block;
}
/**
* Add the correct display in IE.
*/
template {
display: none;
}
/* Hidden
========================================================================== */
/**
* Add the correct display in IE 10-.
*/
[hidden] {
display: none;
}

View file

@ -0,0 +1,3 @@
.text-color-red {
color: $alert-color;
}

View file

@ -0,0 +1,98 @@
// Font sizes
$font-size-nano: 0.8rem;
$font-size-micro: 1.0rem;
$font-size-mini: 1.2rem;
$font-size-small: 1.4rem;
$font-size-default: 1.6rem;
$font-size-medium: 1.8rem;
$font-size-large: 2.2rem;
$font-size-big: 2.4rem;
$font-size-bigger: 3.0rem;
$font-size-mega: 3.4rem;
$font-size-giga: 4.0rem;
// spaces
$zero: 0;
$space-micro: 0.2rem;
$space-smaller: 0.4rem;
$space-small: 0.8rem;
$space-one: 1rem;
$space-slab: 1.2rem;
$space-normal: 1.6rem;
$space-two: 2.0rem;
$space-medium: 2.4rem;
$space-large: 3.2rem;
$space-larger: 4.8rem;
$space-jumbo: 6.4rem;
$space-mega: 10.0rem;
// font-weight
$font-weight-feather: 100;
$font-weight-light: 300;
$font-weight-normal: 400;
$font-weight-medium: 500;
$font-weight-bold: 600;
$font-weight-black: 700;
//Navbar
$nav-bar-width: 23rem;
$header-height: 5.6rem;
$woot-logo-padding: $space-large $space-two;
// Colors
$color-woot: #1f93ff;
$color-gray: #6e6f73;
$color-light-gray: #999a9b;
$color-border: #e0e6ed;
$color-border-light: #f0f4f5;
$color-background: #f4f6fb;
$color-border-dark: #cad0d4;
$color-background-light: #f9fafc;
$color-white: #fff;
$color-body: #3c4858;
$color-heading: #1f2d3d;
$color-extra-light-blue: #f5f7f9;
$primary-color: $color-woot;
$secondary-color: #35c5ff;
$success-color: #44ce4b;
$warning-color: #ffc532;
$alert-color: #ff382d;
$masked-bg: rgba(0, 0, 0, .4);
// Color-palettes
$color-primary-light: #c7e3ff;
$color-primary-dark: darken($color-woot, 20%);
// Thumbnail
$thumbnail-radius: 4rem;
// chat-header
$conv-header-height: 4rem;
// Inbox List
$inbox-thumb-size: 4.8rem;
// Spinner
$spinkit-spinner-color: $color-white !default;
$spinkit-spinner-margin: 0 0 0 1.6rem !default;
$spinkit-size: 1.6rem !default;
// Snackbar default
$woot-snackbar-bg: #323232;
$woot-snackbar-button: #ffeb3b;
$swift-ease-out-duration: .4s !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;
// Ionicons
$ionicons-font-path: '~ionicons/fonts';
// Transitions
$transition-ease-in: all 0.250s ease-in;

View file

@ -1,19 +0,0 @@
# app/bot/facebook_bot.rb
require 'facebook/messenger'
include Facebook::Messenger
Bot.on :message do |message|
response = ::Integrations::Facebook::MessageParser.new(message)
::Integrations::Facebook::MessageCreator.new(response).perform
end
Bot.on :delivery do |delivery|
# delivery.ids # => 'mid.1457764197618:41d102a3e1ae206a38'
# delivery.sender # => { 'id' => '1008372609250235' }
# delivery.recipient # => { 'id' => '2015573629214912' }
# delivery.at # => 2016-04-22 21:30:36 +0200
# delivery.seq # => 37
updater = Integrations::Facebook::DeliveryStatus.new(delivery)
updater.perform
puts "Human was online at #{delivery.at}"
end

22
app/bot/facebook_bot.rb Normal file
View file

@ -0,0 +1,22 @@
require 'facebook/messenger'
class FacebookBot
include Facebook::Messenger
Bot.on :message do |message|
Rails.logger.info "MESSAGE_RECIEVED #{message}"
response = ::Integrations::Facebook::MessageParser.new(message)
::Integrations::Facebook::MessageCreator.new(response).perform
end
Bot.on :delivery do |delivery|
# delivery.ids # => 'mid.1457764197618:41d102a3e1ae206a38'
# delivery.sender # => { 'id' => '1008372609250235' }
# delivery.recipient # => { 'id' => '2015573629214912' }
# delivery.at # => 2016-04-22 21:30:36 +0200
# delivery.seq # => 37
updater = Integrations::Facebook::DeliveryStatus.new(delivery)
updater.perform
Rails.logger.info "Human was online at #{delivery.at}"
end
end

View file

@ -2,7 +2,7 @@
class AccountBuilder
include CustomExceptions::Account
pattr_initialize [:account_name!, :email!]
pattr_initialize [:account_name!, :email!, :confirmed!]
def perform
validate_email
@ -38,6 +38,7 @@ class AccountBuilder
def create_account
@account = Account.create!(name: @account_name)
Current.account = @account
end
def create_and_link_user
@ -46,6 +47,7 @@ class AccountBuilder
password: password,
password_confirmation: password,
name: email_to_name(@email))
@user.confirm if @confirmed
if @user.save!
link_user_to_account(@user, @account)
@user

View file

@ -14,6 +14,18 @@ class ContactBuilder
@account ||= inbox.account
end
def create_contact_inbox(contact)
::ContactInbox.create!(
contact_id: contact.id,
inbox_id: inbox.id,
source_id: source_id
)
end
def update_contact_avatar(contact)
::ContactAvatarJob.perform_later(contact, contact_attributes[:avatar_url]) if contact_attributes[:avatar_url]
end
def build_contact
ActiveRecord::Base.transaction do
contact = account.contacts.create!(
@ -23,16 +35,12 @@ class ContactBuilder
identifier: contact_attributes[:identifier],
additional_attributes: contact_attributes[:additional_attributes]
)
contact_inbox = ::ContactInbox.create!(
contact_id: contact.id,
inbox_id: inbox.id,
source_id: source_id
)
::ContactAvatarJob.perform_later(contact, contact_attributes[:avatar_url]) if contact_attributes[:avatar_url]
contact_inbox = create_contact_inbox(contact)
update_contact_avatar(contact)
contact_inbox
rescue StandardError => e
Rails.logger e
Rails.logger.info e
end
end
end

View file

@ -5,8 +5,8 @@ class Api::V1::Accounts::AccountsController < Api::BaseController
skip_before_action :authenticate_user!, :set_current_user, :check_subscription, :handle_with_exception,
only: [:create], raise: false
before_action :check_signup_enabled, only: [:create]
before_action :check_authorization, except: [:create]
before_action :fetch_account, except: [:create]
before_action :check_authorization, except: [:create]
rescue_from CustomExceptions::Account::InvalidEmail,
CustomExceptions::Account::UserExists,
@ -16,11 +16,12 @@ class Api::V1::Accounts::AccountsController < Api::BaseController
def create
@user = AccountBuilder.new(
account_name: account_params[:account_name],
email: account_params[:email]
email: account_params[:email],
confirmed: confirmed?
).perform
if @user
send_auth_headers(@user)
render 'devise/auth.json', locals: { resource: @user }
render partial: 'devise/auth.json', locals: { resource: @user }
else
render_error_response(CustomExceptions::Account::SignupFailed.new({}))
end
@ -34,14 +35,25 @@ class Api::V1::Accounts::AccountsController < Api::BaseController
@account.update!(account_params.slice(:name, :locale, :domain, :support_email, :domain_emails_enabled))
end
def update_active_at
@current_account_user.active_at = Time.now.utc
@current_account_user.save!
head :ok
end
private
def check_authorization
authorize(Account)
end
def confirmed?
super_admin? && params[:confirmed]
end
def fetch_account
@account = current_user.accounts.find(params[:id])
@current_account_user = @account.account_users.find_by(user_id: current_user.id)
end
def account_params
@ -51,4 +63,12 @@ class Api::V1::Accounts::AccountsController < Api::BaseController
def check_signup_enabled
raise ActionController::RoutingError, 'Not Found' if ENV.fetch('ENABLE_ACCOUNT_SIGNUP', true) == 'false'
end
def pundit_user
{
user: current_user,
account: @account,
account_user: @current_account_user
}
end
end

View file

@ -10,18 +10,18 @@ class Api::V1::Accounts::AgentsController < Api::BaseController
end
def destroy
@agent.account_user.destroy
@agent.current_account_user.destroy
head :ok
end
def update
@agent.update!(agent_params.except(:role))
@agent.account_user.update!(role: agent_params[:role]) if agent_params[:role]
render 'api/v1/models/user.json', locals: { resource: @agent }
@agent.current_account_user.update!(role: agent_params[:role]) if agent_params[:role]
render partial: 'api/v1/models/agent.json.jbuilder', locals: { resource: @agent }
end
def create
render 'api/v1/models/user.json', locals: { resource: @user }
render partial: 'api/v1/models/agent.json.jbuilder', locals: { resource: @user }
end
private

View file

@ -14,7 +14,7 @@ class Api::V1::Accounts::CallbacksController < Api::BaseController
@facebook_inbox = current_account.inboxes.create!(name: inbox_name, channel: facebook_channel)
set_avatar(@facebook_inbox, page_id)
rescue StandardError => e
Rails.logger e
Rails.logger.info e
end
end
@ -62,7 +62,7 @@ class Api::V1::Accounts::CallbacksController < Api::BaseController
koala = Koala::Facebook::OAuth.new(ENV['FB_APP_ID'], ENV['FB_APP_SECRET'])
koala.exchange_access_token_info(omniauth_token)['access_token']
rescue StandardError => e
Rails.logger e
Rails.logger.info e
end
def mark_already_existing_facebook_pages(data)

View file

@ -1,4 +1,5 @@
class Api::V1::Accounts::Channels::TwilioChannelsController < Api::BaseController
before_action :current_account
before_action :authorize_request
def create

View file

@ -1,5 +1,6 @@
class Api::V1::Accounts::ConversationsController < Api::BaseController
include Events::Types
before_action :current_account
before_action :conversation, except: [:index]
before_action :contact_inbox, only: [:create]
@ -20,9 +21,19 @@ class Api::V1::Accounts::ConversationsController < Api::BaseController
def show; end
def mute
@conversation.mute!
head :ok
end
def toggle_status
if params[:status]
@conversation.status = params[:status]
@status = @conversation.save
else
@status = @conversation.toggle_status
end
end
def toggle_typing_status
if params[:typing_status] == 'on'

View file

@ -1,7 +1,8 @@
class Api::V1::Accounts::InboxesController < Api::BaseController
before_action :check_authorization
before_action :current_account
before_action :fetch_inbox, except: [:index, :create]
before_action :fetch_agent_bot, only: [:set_agent_bot]
before_action :check_authorization
def index
@inboxes = policy_scope(current_account.inboxes)

View file

@ -1,4 +1,5 @@
class Api::V1::Accounts::WebhooksController < Api::BaseController
before_action :current_account
before_action :check_authorization
before_action :fetch_webhook, only: [:update, :destroy]

View file

@ -2,7 +2,7 @@ class Api::V1::ProfilesController < Api::BaseController
before_action :set_user
def show
render json: @user
render partial: 'api/v1/models/user.json.jbuilder', locals: { resource: @user }
end
def update

View file

@ -3,6 +3,10 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
before_action :set_web_widget
before_action :set_contact
def index
@conversation = conversation
end
def toggle_typing
head :ok && return if conversation.nil?

View file

@ -11,10 +11,6 @@ class Api::V2::Accounts::ReportsController < Api::BaseController
private
def current_account
current_user.account
end
def account_summary_params
{
type: :account,

View file

@ -14,7 +14,8 @@ class ApplicationController < ActionController::Base
private
def current_account
@_ ||= find_current_account
@current_account ||= find_current_account
Current.account = @current_account
end
def find_current_account
@ -29,11 +30,17 @@ class ApplicationController < ActionController::Base
end
def switch_locale(account)
I18n.locale = (I18n.available_locales.map(&:to_s).include?(account.locale) ? account.locale : nil) || I18n.default_locale
# priority is for locale set in query string (mostly for widget/from js sdk)
locale ||= (I18n.available_locales.map(&:to_s).include?(params[:locale]) ? params[:locale] : nil)
# if local is not set in param, lets try account
locale ||= (I18n.available_locales.map(&:to_s).include?(account.locale) ? account.locale : nil)
I18n.locale = locale || I18n.default_locale
end
def account_accessible_for_user?(account)
render_unauthorized('You are not authorized to access this account') unless account.account_users.find_by(user_id: current_user.id)
@current_account_user = account.account_users.find_by(user_id: current_user.id)
Current.account_user = @current_account_user
render_unauthorized('You are not authorized to access this account') unless @current_account_user
end
def account_accessible_for_bot?(account)
@ -98,4 +105,12 @@ class ApplicationController < ActionController::Base
render json: { error: 'Account Suspended' }, status: :account_suspended
end
end
def pundit_user
{
user: Current.user,
account: Current.account,
account_user: Current.account_user
}
end
end

View file

@ -4,17 +4,25 @@ module AccessTokenAuthHelper
'api/v1/accounts/conversations/messages' => ['create']
}.freeze
def authenticate_access_token!
def ensure_access_token
token = request.headers[:api_access_token] || request.headers[:HTTP_API_ACCESS_TOKEN]
access_token = AccessToken.find_by(token: token)
render_unauthorized('Invalid Access Token') && return unless access_token
@access_token = AccessToken.find_by(token: token) if token.present?
end
token_owner = access_token.owner
@resource = token_owner
def authenticate_access_token!
ensure_access_token
render_unauthorized('Invalid Access Token') && return if @access_token.blank?
@resource = @access_token.owner
end
def super_admin?
@resource.present? && @resource.is_a?(SuperAdmin)
end
def validate_bot_access_token!
return if current_user.is_a?(User)
return if super_admin?
return if agent_bot_accessible?
render_unauthorized('Access to this endpoint is not authorized for bots')

View file

@ -1,5 +1,21 @@
class DashboardController < ActionController::Base
before_action :set_global_config
layout 'vueapp'
def index; end
private
def set_global_config
@global_config = GlobalConfig.get(
'LOGO',
'LOGO_THUMBNAIL',
'INSTALLATION_NAME',
'WIDGET_BRAND_URL',
'TERMS_URL',
'PRIVACY_URL',
'DISPLAY_MANIFEST'
)
end
end

View file

@ -11,7 +11,7 @@ class DeviseOverrides::PasswordsController < Devise::PasswordsController
@recoverable = User.find_by(reset_password_token: reset_password_token)
if @recoverable && reset_password_and_confirmation(@recoverable)
send_auth_headers(@recoverable)
render 'devise/auth.json', locals: { resource: @recoverable }
render partial: 'devise/auth.json', locals: { resource: @recoverable }
else
render json: { "message": 'Invalid token', "redirect_url": '/' }, status: 422
end

View file

@ -4,6 +4,6 @@ class DeviseOverrides::SessionsController < ::DeviseTokenAuth::SessionsControlle
wrap_parameters format: []
def render_create_success
render 'devise/auth.json', locals: { resource: @resource }
render partial: 'devise/auth.json', locals: { resource: @resource }
end
end

View file

@ -0,0 +1,44 @@
class SuperAdmin::AccessTokensController < SuperAdmin::ApplicationController
# 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.
#
# def update
# super
# send_foo_updated_email(requested_resource)
# end
# Override this method to specify custom lookup behavior.
# This will be used to set the resource for the `show`, `edit`, and `update`
# actions.
#
# def find_resource(param)
# Foo.find_by!(slug: param)
# end
# The result of this lookup will be available as `requested_resource`
# Override this if you have certain roles that require a subset
# this will be used to set the records shown on the `index` action.
#
# def scoped_resource
# if current_user.super_admin?
# resource_class
# else
# resource_class.with_less_stuff
# end
# end
# Override `resource_params` if you want to transform the submitted
# data before it's persisted. For example, the following would turn all
# empty values into nil values. It uses other APIs such as `resource_class`
# and `dashboard`:
#
# def resource_params
# params.require(resource_class.model_name.param_key).
# permit(dashboard.permitted_attributes).
# transform_values { |value| value == "" ? nil : value }
# end
# See https://administrate-prototype.herokuapp.com/customizing_controller_actions
# for more information
end

View file

@ -0,0 +1,56 @@
class SuperAdmin::AccountUsersController < SuperAdmin::ApplicationController
# 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.
#
def create
resource = resource_class.new(resource_params)
authorize_resource(resource)
notice = resource.save ? translate_with_resource('create.success') : resource.errors.full_messages.first
redirect_back(fallback_location: [namespace, resource.account], notice: notice)
end
def destroy
if requested_resource.destroy
flash[:notice] = translate_with_resource('destroy.success')
else
flash[:error] = requested_resource.errors.full_messages.join('<br/>')
end
redirect_back(fallback_location: [namespace, requested_resource.account])
end
# Override this method to specify custom lookup behavior.
# This will be used to set the resource for the `show`, `edit`, and `update`
# actions.
#
# def find_resource(param)
# Foo.find_by!(slug: param)
# end
# The result of this lookup will be available as `requested_resource`
# Override this if you have certain roles that require a subset
# this will be used to set the records shown on the `index` action.
#
# def scoped_resource
# if current_user.super_admin?
# resource_class
# else
# resource_class.with_less_stuff
# end
# end
# Override `resource_params` if you want to transform the submitted
# data before it's persisted. For example, the following would turn all
# empty values into nil values. It uses other APIs such as `resource_class`
# and `dashboard`:
#
# def resource_params
# params.require(resource_class.model_name.param_key).
# permit(dashboard.permitted_attributes).
# transform_values { |value| value == "" ? nil : value }
# end
# See https://administrate-prototype.herokuapp.com/customizing_controller_actions
# for more information
end

View file

@ -0,0 +1,44 @@
class SuperAdmin::AccountsController < SuperAdmin::ApplicationController
# 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.
#
# def update
# super
# send_foo_updated_email(requested_resource)
# end
# Override this method to specify custom lookup behavior.
# This will be used to set the resource for the `show`, `edit`, and `update`
# actions.
#
# def find_resource(param)
# Foo.find_by!(slug: param)
# end
# The result of this lookup will be available as `requested_resource`
# Override this if you have certain roles that require a subset
# this will be used to set the records shown on the `index` action.
#
# def scoped_resource
# if current_user.super_admin?
# resource_class
# else
# resource_class.with_less_stuff
# end
# end
# Override `resource_params` if you want to transform the submitted
# data before it's persisted. For example, the following would turn all
# empty values into nil values. It uses other APIs such as `resource_class`
# and `dashboard`:
#
# def resource_params
# params.require(resource_class.model_name.param_key).
# permit(dashboard.permitted_attributes).
# transform_values { |value| value == "" ? nil : value }
# end
# See https://administrate-prototype.herokuapp.com/customizing_controller_actions
# for more information
end

View file

@ -0,0 +1,44 @@
class SuperAdmin::AgentBotsController < SuperAdmin::ApplicationController
# 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.
#
# def update
# super
# send_foo_updated_email(requested_resource)
# end
# Override this method to specify custom lookup behavior.
# This will be used to set the resource for the `show`, `edit`, and `update`
# actions.
#
# def find_resource(param)
# Foo.find_by!(slug: param)
# end
# The result of this lookup will be available as `requested_resource`
# Override this if you have certain roles that require a subset
# this will be used to set the records shown on the `index` action.
#
# def scoped_resource
# if current_user.super_admin?
# resource_class
# else
# resource_class.with_less_stuff
# end
# end
# Override `resource_params` if you want to transform the submitted
# data before it's persisted. For example, the following would turn all
# empty values into nil values. It uses other APIs such as `resource_class`
# and `dashboard`:
#
# def resource_params
# params.require(resource_class.model_name.param_key).
# permit(dashboard.permitted_attributes).
# transform_values { |value| value == "" ? nil : value }
# end
# See https://administrate-prototype.herokuapp.com/customizing_controller_actions
# for more information
end

View file

@ -0,0 +1,23 @@
# All Administrate controllers inherit from this
# `Administrate::ApplicationController`, making it the ideal place to put
# authentication logic or other before_actions.
#
# If you want to add pagination or other controller-level concerns,
# you're free to overwrite the RESTful controller actions.
class SuperAdmin::ApplicationController < Administrate::ApplicationController
# authenticiation done via devise : SuperAdmin Model
before_action :authenticate_super_admin!
# Override this value to specify the number of elements to display at a time
# on index pages. Defaults to 20.
# def records_per_page
# params[:per_page] || 20
# end
def order
@order ||= Administrate::Order.new(
params.fetch(resource_name, {}).fetch(:order, 'id'),
params.fetch(resource_name, {}).fetch(:direction, 'desc')
)
end
end

View file

@ -0,0 +1,12 @@
class SuperAdmin::DashboardController < SuperAdmin::ApplicationController
include ActionView::Helpers::NumberHelper
def index
@data = Conversation.unscoped.group_by_day(:created_at, range: 30.days.ago..2.seconds.ago).count.to_a
@accounts_count = number_with_delimiter(Account.all.length)
@users_count = number_with_delimiter(User.all.length)
@inboxes_count = number_with_delimiter(Inbox.all.length)
@conversations_count = number_with_delimiter(Conversation.all.length)
@messages_count = number_with_delimiter(Message.all.length)
end
end

View file

@ -0,0 +1,33 @@
# frozen_string_literal: true
class SuperAdmin::Devise::SessionsController < Devise::SessionsController
def new
self.resource = resource_class.new(sign_in_params)
end
def create
redirect_to(super_admin_session_path, flash: { error: @error_message }) && return unless valid_credentials?
sign_in(@super_admin, scope: :super_admin)
flash.discard
redirect_to super_admin_users_path
end
def destroy
sign_out
flash.discard
redirect_to '/'
end
private
def valid_credentials?
@super_admin = SuperAdmin.find_by!(email: params[:super_admin][:email])
raise StandardError, 'Invalid Password' unless @super_admin.valid_password?(params[:super_admin][:password])
true
rescue StandardError => e
@error_message = e.message
false
end
end

View file

@ -0,0 +1,44 @@
class SuperAdmin::SuperAdminsController < SuperAdmin::ApplicationController
# 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.
#
# def update
# super
# send_foo_updated_email(requested_resource)
# end
# Override this method to specify custom lookup behavior.
# This will be used to set the resource for the `show`, `edit`, and `update`
# actions.
#
# def find_resource(param)
# Foo.find_by!(slug: param)
# end
# The result of this lookup will be available as `requested_resource`
# Override this if you have certain roles that require a subset
# this will be used to set the records shown on the `index` action.
#
# def scoped_resource
# if current_user.super_admin?
# resource_class
# else
# resource_class.with_less_stuff
# end
# end
# Override `resource_params` if you want to transform the submitted
# data before it's persisted. For example, the following would turn all
# empty values into nil values. It uses other APIs such as `resource_class`
# and `dashboard`:
#
# def resource_params
# params.require(resource_class.model_name.param_key).
# permit(dashboard.permitted_attributes).
# transform_values { |value| value == "" ? nil : value }
# end
# See https://administrate-prototype.herokuapp.com/customizing_controller_actions
# for more information
end

View file

@ -0,0 +1,44 @@
class SuperAdmin::UsersController < SuperAdmin::ApplicationController
# 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.
#
# def update
# super
# send_foo_updated_email(requested_resource)
# end
# Override this method to specify custom lookup behavior.
# This will be used to set the resource for the `show`, `edit`, and `update`
# actions.
#
# def find_resource(param)
# Foo.find_by!(slug: param)
# end
# The result of this lookup will be available as `requested_resource`
# Override this if you have certain roles that require a subset
# this will be used to set the records shown on the `index` action.
#
# def scoped_resource
# if current_user.super_admin?
# resource_class
# else
# resource_class.with_less_stuff
# end
# end
# Override `resource_params` if you want to transform the submitted
# data before it's persisted. For example, the following would turn all
# empty values into nil values. It uses other APIs such as `resource_class`
# and `dashboard`:
#
# def resource_params
# params.require(resource_class.model_name.param_key).
# permit(dashboard.permitted_attributes).
# transform_values { |value| value == "" ? nil : value }
# end
# See https://administrate-prototype.herokuapp.com/customizing_controller_actions
# for more information
end

View file

@ -2,18 +2,18 @@ class Twitter::CallbacksController < Twitter::BaseController
def show
return redirect_to twitter_app_redirect_url if permitted_params[:denied]
@response = twitter_client.access_token(
oauth_token: permitted_params[:oauth_token],
oauth_verifier: permitted_params[:oauth_verifier]
)
if @response.status == '200'
inbox = build_inbox
@response = ensure_access_token
return redirect_to twitter_app_redirect_url if @response.status != '200'
ActiveRecord::Base.transaction do
inbox = create_inbox
::Redis::Alfred.delete(permitted_params[:oauth_token])
::Twitter::WebhookSubscribeService.new(inbox_id: inbox.id).perform
redirect_to app_twitter_inbox_agents_url(account_id: account.id, inbox_id: inbox.id)
else
redirect_to twitter_app_redirect_url
end
rescue StandardError => e
Rails.logger.info e
redirect_to twitter_app_redirect_url
end
private
@ -34,8 +34,14 @@ class Twitter::CallbacksController < Twitter::BaseController
app_new_twitter_inbox_url(account_id: account.id)
end
def build_inbox
ActiveRecord::Base.transaction do
def ensure_access_token
twitter_client.access_token(
oauth_token: permitted_params[:oauth_token],
oauth_verifier: permitted_params[:oauth_verifier]
)
end
def create_inbox
twitter_profile = account.twitter_profiles.create(
twitter_access_token: parsed_body['oauth_token'],
twitter_access_token_secret: parsed_body['oauth_token_secret'],
@ -45,9 +51,6 @@ class Twitter::CallbacksController < Twitter::BaseController
name: parsed_body['screen_name'],
channel: twitter_profile
)
rescue StandardError => e
Rails.logger e
end
end
def permitted_params

View file

@ -1,4 +1,5 @@
class WidgetsController < ActionController::Base
before_action :set_global_config
before_action :set_web_widget
before_action :set_token
before_action :set_contact
@ -8,6 +9,10 @@ class WidgetsController < ActionController::Base
private
def set_global_config
@global_config = GlobalConfig.get('LOGO_THUMBNAIL', 'INSTALLATION_NAME', 'WIDGET_BRAND_URL')
end
def set_web_widget
@web_widget = ::Channel::WebWidget.find_by!(website_token: permitted_params[:website_token])
end

View file

@ -0,0 +1,70 @@
require 'administrate/base_dashboard'
class AccessTokenDashboard < Administrate::BaseDashboard
# ATTRIBUTE_TYPES
# a hash that describes the type of each of the model's fields.
#
# Each different type represents an Administrate::Field object,
# which determines how the attribute is displayed
# on pages throughout the dashboard.
ATTRIBUTE_TYPES = {
owner: Field::Polymorphic,
id: Field::Number,
token: Field::String,
created_at: Field::DateTime,
updated_at: Field::DateTime
}.freeze
# COLLECTION_ATTRIBUTES
# an array of attributes that will be displayed on the model's index page.
#
# By default, it's limited to four items to reduce clutter on index pages.
# Feel free to add, remove, or rearrange items.
COLLECTION_ATTRIBUTES = %i[
owner
id
token
created_at
].freeze
# SHOW_PAGE_ATTRIBUTES
# an array of attributes that will be displayed on the model's show page.
SHOW_PAGE_ATTRIBUTES = %i[
owner
id
token
created_at
updated_at
].freeze
# FORM_ATTRIBUTES
# an array of attributes that will be displayed
# on the model's form (`new` and `edit`) pages.
FORM_ATTRIBUTES = %i[
owner
token
].freeze
# COLLECTION_FILTERS
# a hash that defines filters that can be used while searching via the search
# field of the dashboard.
#
# For example to add an option to search for open resources by typing "open:"
# in the search field:
#
# COLLECTION_FILTERS = {
# open: ->(resources) { resources.where(open: true) }
# }.freeze
COLLECTION_FILTERS = {
user: ->(resources) { resources.where(owner_type: 'User') },
super_admin: ->(resources) { resources.where(owner_type: 'SuperAdmin') },
agent_bot: ->(resources) { resources.where(owner_type: 'AgentBot') }
}.freeze
# Overwrite this method to customize how access tokens are displayed
# across all pages of the admin dashboard.
#
# def display_resource(access_token)
# "AccessToken ##{access_token.id}"
# end
end

View file

@ -0,0 +1,72 @@
require 'administrate/base_dashboard'
class AccountDashboard < Administrate::BaseDashboard
# ATTRIBUTE_TYPES
# a hash that describes the type of each of the model's fields.
#
# Each different type represents an Administrate::Field object,
# which determines how the attribute is displayed
# on pages throughout the dashboard.
ATTRIBUTE_TYPES = {
id: Field::Number,
name: Field::String,
created_at: Field::DateTime,
updated_at: Field::DateTime,
users: CountField,
conversations: CountField,
locale: Field::Select.with_options(collection: LANGUAGES_CONFIG.map { |_x, y| y[:iso_639_1_code] }),
account_users: Field::HasMany
}.freeze
# COLLECTION_ATTRIBUTES
# an array of attributes that will be displayed on the model's index page.
#
# By default, it's limited to four items to reduce clutter on index pages.
# Feel free to add, remove, or rearrange items.
COLLECTION_ATTRIBUTES = %i[
id
name
locale
users
conversations
].freeze
# SHOW_PAGE_ATTRIBUTES
# an array of attributes that will be displayed on the model's show page.
SHOW_PAGE_ATTRIBUTES = %i[
id
name
created_at
updated_at
locale
conversations
account_users
].freeze
# FORM_ATTRIBUTES
# an array of attributes that will be displayed
# on the model's form (`new` and `edit`) pages.
FORM_ATTRIBUTES = %i[
name
locale
].freeze
# COLLECTION_FILTERS
# a hash that defines filters that can be used while searching via the search
# field of the dashboard.
#
# For example to add an option to search for open resources by typing "open:"
# in the search field:
#
# COLLECTION_FILTERS = {
# open: ->(resources) { resources.where(open: true) }
# }.freeze
COLLECTION_FILTERS = {}.freeze
# Overwrite this method to customize how accounts are displayed
# across all pages of the admin dashboard.
#
def display_resource(account)
"##{account.id} #{account.name}"
end
end

View file

@ -0,0 +1,71 @@
require 'administrate/base_dashboard'
class AccountUserDashboard < Administrate::BaseDashboard
# ATTRIBUTE_TYPES
# a hash that describes the type of each of the model's fields.
#
# Each different type represents an Administrate::Field object,
# which determines how the attribute is displayed
# on pages throughout the dashboard.
ATTRIBUTE_TYPES = {
account: Field::BelongsTo.with_options(searchable: true, searchable_field: 'name', order: 'id DESC'),
user: Field::BelongsTo.with_options(searchable: true, searchable_field: 'name', order: 'id DESC'),
inviter: Field::BelongsTo.with_options(class_name: 'User', searchable: true, searchable_field: 'name'),
id: Field::Number,
role: Field::Select.with_options(collection: AccountUser.roles.keys),
created_at: Field::DateTime,
updated_at: Field::DateTime
}.freeze
# COLLECTION_ATTRIBUTES
# an array of attributes that will be displayed on the model's index page.
#
# By default, it's limited to four items to reduce clutter on index pages.
# Feel free to add, remove, or rearrange items.
COLLECTION_ATTRIBUTES = %i[
account
user
inviter
role
].freeze
# SHOW_PAGE_ATTRIBUTES
# an array of attributes that will be displayed on the model's show page.
SHOW_PAGE_ATTRIBUTES = %i[
account
user
inviter
id
role
created_at
updated_at
].freeze
# FORM_ATTRIBUTES
# an array of attributes that will be displayed
# on the model's form (`new` and `edit`) pages.
FORM_ATTRIBUTES = %i[
account
user
role
].freeze
# COLLECTION_FILTERS
# a hash that defines filters that can be used while searching via the search
# field of the dashboard.
#
# For example to add an option to search for open resources by typing "open:"
# in the search field:
#
# COLLECTION_FILTERS = {
# open: ->(resources) { resources.where(open: true) }
# }.freeze
COLLECTION_FILTERS = {}.freeze
# Overwrite this method to customize how account users are displayed
# across all pages of the admin dashboard.
#
def display_resource(account_user)
"AccountUser ##{account_user.id}"
end
end

View file

@ -0,0 +1,73 @@
require 'administrate/base_dashboard'
class AgentBotDashboard < Administrate::BaseDashboard
# ATTRIBUTE_TYPES
# a hash that describes the type of each of the model's fields.
#
# Each different type represents an Administrate::Field object,
# which determines how the attribute is displayed
# on pages throughout the dashboard.
ATTRIBUTE_TYPES = {
access_token: Field::HasOne,
avatar_url: AvatarField,
id: Field::Number,
name: Field::String,
description: Field::String,
outgoing_url: Field::String,
created_at: Field::DateTime,
updated_at: Field::DateTime,
hide_input_for_bot_conversations: Field::Boolean
}.freeze
# COLLECTION_ATTRIBUTES
# an array of attributes that will be displayed on the model's index page.
#
# By default, it's limited to four items to reduce clutter on index pages.
# Feel free to add, remove, or rearrange items.
COLLECTION_ATTRIBUTES = %i[
id
avatar_url
name
outgoing_url
].freeze
# SHOW_PAGE_ATTRIBUTES
# an array of attributes that will be displayed on the model's show page.
SHOW_PAGE_ATTRIBUTES = %i[
id
avatar_url
name
description
outgoing_url
hide_input_for_bot_conversations
].freeze
# FORM_ATTRIBUTES
# an array of attributes that will be displayed
# on the model's form (`new` and `edit`) pages.
FORM_ATTRIBUTES = %i[
name
description
outgoing_url
hide_input_for_bot_conversations
].freeze
# COLLECTION_FILTERS
# a hash that defines filters that can be used while searching via the search
# field of the dashboard.
#
# For example to add an option to search for open resources by typing "open:"
# in the search field:
#
# COLLECTION_FILTERS = {
# open: ->(resources) { resources.where(open: true) }
# }.freeze
COLLECTION_FILTERS = {}.freeze
# Overwrite this method to customize how agent bots are displayed
# across all pages of the admin dashboard.
#
# def display_resource(agent_bot)
# "AgentBot ##{agent_bot.id}"
# end
end

View file

@ -0,0 +1,77 @@
require 'administrate/base_dashboard'
class SuperAdminDashboard < Administrate::BaseDashboard
# ATTRIBUTE_TYPES
# a hash that describes the type of each of the model's fields.
#
# Each different type represents an Administrate::Field object,
# which determines how the attribute is displayed
# on pages throughout the dashboard.
ATTRIBUTE_TYPES = {
id: Field::Number,
email: Field::String,
password: Field::Password,
access_token: Field::HasOne,
remember_created_at: Field::DateTime,
sign_in_count: Field::Number,
current_sign_in_at: Field::DateTime,
last_sign_in_at: Field::DateTime,
current_sign_in_ip: Field::String.with_options(searchable: false),
last_sign_in_ip: Field::String.with_options(searchable: false),
created_at: Field::DateTime,
updated_at: Field::DateTime
}.freeze
# COLLECTION_ATTRIBUTES
# an array of attributes that will be displayed on the model's index page.
#
# By default, it's limited to four items to reduce clutter on index pages.
# Feel free to add, remove, or rearrange items.
COLLECTION_ATTRIBUTES = %i[
id
email
access_token
].freeze
# SHOW_PAGE_ATTRIBUTES
# an array of attributes that will be displayed on the model's show page.
SHOW_PAGE_ATTRIBUTES = %i[
id
email
remember_created_at
sign_in_count
current_sign_in_at
last_sign_in_at
current_sign_in_ip
last_sign_in_ip
created_at
updated_at
].freeze
# FORM_ATTRIBUTES
# an array of attributes that will be displayed
# on the model's form (`new` and `edit`) pages.
FORM_ATTRIBUTES = %i[
email
password
].freeze
# COLLECTION_FILTERS
# a hash that defines filters that can be used while searching via the search
# field of the dashboard.
#
# For example to add an option to search for open resources by typing "open:"
# in the search field:
#
# COLLECTION_FILTERS = {
# open: ->(resources) { resources.where(open: true) }
# }.freeze
COLLECTION_FILTERS = {}.freeze
# Overwrite this method to customize how super admins are displayed
# across all pages of the admin dashboard.
#
# def display_resource(super_admin)
# "SuperAdmin ##{super_admin.id}"
# end
end

View file

@ -0,0 +1,91 @@
require 'administrate/base_dashboard'
class UserDashboard < Administrate::BaseDashboard
# ATTRIBUTE_TYPES
# a hash that describes the type of each of the model's fields.
#
# Each different type represents an Administrate::Field object,
# which determines how the attribute is displayed
# on pages throughout the dashboard.
ATTRIBUTE_TYPES = {
account_users: Field::HasMany,
id: Field::Number,
avatar_url: AvatarField,
provider: Field::String,
uid: Field::String,
password: Field::Password,
sign_in_count: Field::Number,
current_sign_in_at: Field::DateTime,
last_sign_in_at: Field::DateTime,
current_sign_in_ip: Field::String,
last_sign_in_ip: Field::String,
confirmation_token: Field::String,
confirmed_at: Field::DateTime,
confirmation_sent_at: Field::DateTime,
unconfirmed_email: Field::String,
name: Field::String,
nickname: Field::String,
email: Field::String,
tokens: Field::String.with_options(searchable: false),
created_at: Field::DateTime,
updated_at: Field::DateTime,
pubsub_token: Field::String,
accounts: CountField
}.freeze
# COLLECTION_ATTRIBUTES
# an array of attributes that will be displayed on the model's index page.
#
# By default, it's limited to four items to reduce clutter on index pages.
# Feel free to add, remove, or rearrange items.
COLLECTION_ATTRIBUTES = %i[
id
avatar_url
name
email
accounts
].freeze
# SHOW_PAGE_ATTRIBUTES
# an array of attributes that will be displayed on the model's show page.
SHOW_PAGE_ATTRIBUTES = %i[
id
avatar_url
unconfirmed_email
name
nickname
email
created_at
updated_at
account_users
].freeze
# FORM_ATTRIBUTES
# an array of attributes that will be displayed
# on the model's form (`new` and `edit`) pages.
FORM_ATTRIBUTES = %i[
name
nickname
email
password
].freeze
# COLLECTION_FILTERS
# a hash that defines filters that can be used while searching via the search
# field of the dashboard.
#
# For example to add an option to search for open resources by typing "open:"
# in the search field:
#
# COLLECTION_FILTERS = {
# open: ->(resources) { resources.where(open: true) }
# }.freeze
COLLECTION_FILTERS = {}.freeze
# Overwrite this method to customize how users are displayed
# across all pages of the admin dashboard.
#
def display_resource(user)
"##{user.id} #{user.name}"
end
end

View file

@ -0,0 +1,7 @@
require 'administrate/field/base'
class AvatarField < Administrate::Field::Base
def avatar_url
data.presence || '/admin/avatar.png'
end
end

View file

@ -0,0 +1,7 @@
require 'administrate/field/base'
class CountField < Administrate::Field::Base
def to_s
data.count
end
end

View file

@ -39,6 +39,10 @@ class ConversationApi extends ApiClient {
typing_status: status,
});
}
mute(conversationId) {
return axios.post(`${this.url}/${conversationId}/mute`);
}
}
export default new ConversationApi();

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 15 KiB

View file

@ -1,13 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="504px" height="470px" viewBox="0 0 504 470" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 44 (41411) - http://www.bohemiancoding.com/sketch -->
<title>canned</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="canned" fill-rule="nonzero">
<path d="M329.386,362.733 L39.253,362.733 C20.48,362.733 5.12,347.373 5.12,328.6 L5.12,38.467 C5.12,19.694 20.48,4.334 39.253,4.334 L465.92,4.334 C484.693,4.334 500.053,19.694 500.053,38.467 L500.053,328.6 C500.053,347.373 484.693,362.733 465.92,362.733 L431.787,362.733 L431.787,465.133 L329.386,362.733 Z" id="Shape" fill="#B9ECFF"></path>
<path d="M431.786,469.4 C430.933,469.4 429.226,468.547 428.373,468.547 L308.907,349.08 C307.2,347.373 307.2,344.813 308.907,343.107 C310.614,341.4 313.174,341.4 314.88,343.107 L426.667,454.894 L426.667,362.734 C426.667,360.174 428.374,358.467 430.934,358.467 L465.067,358.467 C481.28,358.467 494.934,344.814 494.934,328.6 L494.934,38.467 C494.934,22.254 481.281,8.6 465.067,8.6 L38.4,8.6 C22.187,8.6 8.533,22.253 8.533,38.467 L8.533,328.6 C8.533,344.813 22.186,358.467 38.4,358.467 L285.867,358.467 C288.427,358.467 290.134,360.174 290.134,362.734 C290.134,365.294 288.427,367.001 285.867,367.001 L38.4,367.001 C17.067,367 0,349.933 0,328.6 L0,38.467 C0,17.134 17.067,0.067 38.4,0.067 L465.067,0.067 C486.4,0.067 503.467,17.134 503.467,38.467 L503.467,328.6 C503.467,349.933 486.4,367 465.067,367 L435.2,367 L435.2,465.133 C435.2,466.84 434.347,468.546 432.64,469.4 C432.64,469.4 432.64,469.4 431.786,469.4 Z M397.653,264.6 L295.253,264.6 C292.693,264.6 290.986,262.893 290.986,260.333 C290.986,257.773 292.693,256.066 295.253,256.066 L397.653,256.066 C400.213,256.066 401.92,257.773 401.92,260.333 C401.92,262.893 400.213,264.6 397.653,264.6 Z M261.12,264.6 L107.52,264.6 C104.96,264.6 103.253,262.893 103.253,260.333 C103.253,257.773 104.96,256.066 107.52,256.066 L261.12,256.066 C263.68,256.066 265.387,257.773 265.387,260.333 C265.387,262.893 263.68,264.6 261.12,264.6 Z M380.586,213.4 L278.186,213.4 C275.626,213.4 273.919,211.693 273.919,209.133 C273.919,206.573 275.626,204.866 278.186,204.866 L380.586,204.866 C383.146,204.866 384.853,206.573 384.853,209.133 C384.853,211.693 383.147,213.4 380.586,213.4 Z M244.053,213.4 L107.52,213.4 C104.96,213.4 103.253,211.693 103.253,209.133 C103.253,206.573 104.96,204.866 107.52,204.866 L244.053,204.866 C246.613,204.866 248.32,206.573 248.32,209.133 C248.32,211.693 246.613,213.4 244.053,213.4 Z M397.653,162.2 L346.453,162.2 C343.893,162.2 342.186,160.493 342.186,157.933 C342.186,155.373 343.893,153.666 346.453,153.666 L397.653,153.666 C400.213,153.666 401.92,155.373 401.92,157.933 C401.92,160.493 400.213,162.2 397.653,162.2 Z M312.32,162.2 L244.053,162.2 C241.493,162.2 239.786,160.493 239.786,157.933 C239.786,155.373 241.493,153.666 244.053,153.666 L312.32,153.666 C314.88,153.666 316.587,155.373 316.587,157.933 C316.586,160.493 314.88,162.2 312.32,162.2 Z M209.92,162.2 L107.52,162.2 C104.96,162.2 103.253,160.493 103.253,157.933 C103.253,155.373 104.96,153.666 107.52,153.666 L209.92,153.666 C212.48,153.666 214.187,155.373 214.187,157.933 C214.186,160.493 212.48,162.2 209.92,162.2 Z M380.586,111 L209.92,111 C207.36,111 205.653,109.293 205.653,106.733 C205.653,104.173 207.36,102.466 209.92,102.466 L380.587,102.466 C383.147,102.466 384.854,104.173 384.854,106.733 C384.853,109.293 383.147,111 380.586,111 Z M175.786,111 L107.52,111 C104.96,111 103.253,109.293 103.253,106.733 C103.253,104.173 104.96,102.466 107.52,102.466 L175.787,102.466 C178.347,102.466 180.054,104.173 180.054,106.733 C180.053,109.293 178.346,111 175.786,111 Z" id="Shape" fill="#6C6C6C"></path>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.8 KiB

View file

@ -59,6 +59,9 @@ $secondary-color: #35c5ff;
$success-color: #44ce4b;
$warning-color: #ffc532;
$alert-color: #ff382d;
$masked-bg: rgba(0, 0, 0, .4);
// Color-palettes
$color-primary-light: #c7e3ff;
@ -70,8 +73,6 @@ $thumbnail-radius: 4rem;
// chat-header
$conv-header-height: 4rem;
// login
// Inbox List
$inbox-thumb-size: 4.8rem;

View file

@ -5,7 +5,6 @@
@import 'foundation-custom';
@import 'widgets/billing';
@import 'widgets/buttons';
@import 'widgets/colorpicker';
@import 'widgets/conv-header';
@import 'widgets/conversation-card';
@import 'widgets/conversation-view';

View file

@ -91,7 +91,6 @@
}
.sidebar-labels-wrap {
&.has-edited,
&:hover {
.multiselect {
@ -108,8 +107,6 @@
}
.multiselect {
margin-top: $space-small;
>.multiselect__select {
visibility: hidden;
}

View file

@ -0,0 +1,15 @@
@import '../variables';
.superadmin-body {
background: $color-background;
}
.alert-box {
background-color: $alert-color;
border-radius: 5px;
color: $color-white;
font-size: 14px;
margin-bottom: 14px;
padding: 10px;
text-align: center;
}

View file

@ -0,0 +1,3 @@
@import 'shared/assets/fonts/inter';
@import '../variables';
@import '~ionicons/scss/ionicons';

View file

@ -1,4 +1,6 @@
.button {
margin-bottom: 0;
&.icon {
padding-left: $space-normal;
padding-right: $space-normal;

View file

@ -1,13 +0,0 @@
@import '~dashboard/assets/scss/variables';
.widget-color--selector.vc-compact {
border: 1px solid $color-border;
box-shadow: none;
margin-bottom: $space-normal;
width: 356px;
.vc-compact-color-item {
height: 24px;
width: 24px;
}
}

View file

@ -1,8 +1,10 @@
.modal-mask {
@import '~dashboard/assets/scss/variables';
@import '~dashboard/assets/scss/mixins';
.modal-mask {
@include flex;
@include flex-align(center, middle);
background-color: $color-white;
background-color: $masked-bg;
height: 100%;
left: 0;
position: fixed;
@ -19,8 +21,8 @@
line-height: $space-normal;
padding: $space-normal $space-two;
position: absolute;
right: $space-large;
top: $space-large;
right: $space-micro;
top: $space-micro;
&:hover {
background: $color-background;
@ -29,7 +31,7 @@
.page-top-bar {
@include padding($zero $space-two);
@include padding($space-large $space-large $zero);
img {
max-height: 6rem;
@ -37,8 +39,10 @@
}
.modal-container {
@include normal-shadow;
background-color: $color-white;
border-radius: $space-small;
border-radius: $space-smaller;
max-height: 100%;
overflow: auto;
position: relative;
@ -52,21 +56,21 @@
h2 {
color: $color-heading;
font-size: $font-size-medium;
color: $color-woot;
font-weight: $font-weight-normal;
@include padding($space-small $zero $zero $zero);
font-weight: $font-weight-bold;
}
p {
font-size: $font-size-small;
@include padding($zero);
@include margin($zero);
@include padding($zero);
font-size: $font-size-small;
}
form {
@include padding($space-large);
align-self: center;
@include padding($space-two);
a {
@include padding($space-normal);
}
@ -74,15 +78,16 @@
.modal-footer {
@include flex;
@include flex-align($x: justify, $y: middle);
@include padding($space-small $zero $space-medium $zero);
@include flex-align($x: flex-start, $y: middle);
@include padding($space-small $zero);
button {
font-size: $font-size-small;
}
}
.delete-item {
@include padding($space-normal);
@include padding($space-large);
button {
@include margin($zero);
}
@ -90,12 +95,12 @@
}
.modal-enter, .modal-leave {
.modal-enter,
.modal-leave {
opacity: 0;
}
.modal-enter .modal-container,
.modal-leave .modal-container {
-webkit-transform: scale(1.1);
transform: scale(1.1);
}

View file

@ -72,7 +72,7 @@
@include border-light;
display: block;
left: 18%;
top: -110%;
top: -110px;
visibility: visible;
width: 80%;
z-index: 999;

View file

@ -1,6 +1,6 @@
table {
font-size: $font-size-small;
border-spacing: 0;
font-size: $font-size-small;
thead {
th {
@ -10,9 +10,12 @@ table {
}
tbody {
tr {
border-bottom: 1px solid $color-border-light;
}
td {
@include padding($space-one $space-small);
border-bottom: 1px solid $color-border-light;
}
}
}
@ -32,22 +35,22 @@ table {
}
.agent-name {
font-weight: $font-weight-medium;
display: block;
font-weight: $font-weight-medium;
text-transform: capitalize;
}
.woot-thumbnail {
border-radius: 50%;
width: 5rem;
height: 5rem;
width: 5rem;
}
.button-wrapper {
min-width: 20rem;
@include flex;
@include flex-align(left, null);
@include flex;
flex-direction: row;
min-width: 20rem;
}
.button {

View file

@ -157,11 +157,7 @@ export default {
} else {
copyList = this.allChatList.slice();
}
const sorted = copyList.sort(
(a, b) =>
this.lastMessage(b).created_at - this.lastMessage(a).created_at
);
return sorted;
return copyList;
},
},
};

View file

@ -6,8 +6,8 @@
transition="modal"
@click="onBackDropClick"
>
<i class="ion-android-close modal--close" @click="close"></i>
<div class="modal-container" :class="className" @click.stop>
<i class="ion-android-close modal--close" @click="close"></i>
<slot />
</div>
</div>

View file

@ -1,6 +1,6 @@
<template>
<div class="row settings--section">
<div class="medium-4">
<div class="medium-4 small-12">
<p class="sub-block-title">
{{ title }}
</p>
@ -8,7 +8,7 @@
{{ subTitle }}
</p>
</div>
<div class="medium-6">
<div class="medium-6 small-12">
<slot></slot>
</div>
</div>

View file

@ -1,21 +1,29 @@
/* eslint no-plusplus: 0 */
/* eslint-env browser */
import Spinner from 'shared/components/Spinner';
import AvatarUploader from './widgets/forms/AvatarUploader.vue';
import Bar from './widgets/chart/BarChart';
import Code from './Code';
import ColorPicker from './widgets/ColorPicker';
import DeleteModal from './widgets/modal/DeleteModal.vue';
import Input from './widgets/forms/Input.vue';
import LoadingState from './widgets/LoadingState';
import Modal from './Modal';
import ModalHeader from './ModalHeader';
import ReportStatsCard from './widgets/ReportStatsCard';
import SidemenuIcon from './SidemenuIcon';
import Spinner from 'shared/components/Spinner';
import SubmitButton from './buttons/FormSubmitButton';
import Tabs from './ui/Tabs/Tabs';
import TabsItem from './ui/Tabs/TabsItem';
import Thumbnail from './widgets/Thumbnail.vue';
const WootUIKit = {
AvatarUploader,
Bar,
Code,
ColorPicker,
DeleteModal,
Input,
LoadingState,
Modal,
ModalHeader,
@ -25,6 +33,7 @@ const WootUIKit = {
SubmitButton,
Tabs,
TabsItem,
Thumbnail,
install(Vue) {
const keys = Object.keys(this);
keys.pop(); // remove 'install' from keys

View file

@ -2,7 +2,7 @@
<aside class="sidebar animated shrink columns">
<div class="logo">
<router-link :to="dashboardPath" replace>
<img src="~dashboard/assets/images/woot-logo.svg" alt="Woot-logo" />
<img :src="globalConfig.logo" :alt="globalConfig.installationName" />
</router-link>
</div>
@ -13,7 +13,6 @@
:key="item.toState"
:menu-item="item"
/>
<sidebar-item
v-if="shouldShowInboxes"
:key="inboxSection.toState"
@ -30,7 +29,7 @@
:button-text="$t('APP_GLOBAL.TRAIL_BUTTON')"
:button-route="{ name: 'billing' }"
:type="statusBarClass"
:show-button="isAdmin()"
:show-button="isAdmin"
/>
</transition>
@ -42,6 +41,14 @@
class="dropdown-pane top"
>
<ul class="vertical dropdown menu">
<li v-if="currentUser.accounts.length > 1">
<button
class="button clear change-accounts--button"
@click="changeAccount"
>
{{ $t('SIDEBAR_ITEMS.CHANGE_ACCOUNTS') }}
</button>
</li>
<li>
<router-link :to="`/app/accounts/${accountId}/profile/settings`">
{{ $t('SIDEBAR_ITEMS.PROFILE_SETTINGS') }}
@ -62,13 +69,35 @@
{{ currentUser.name }}
</h3>
<h5 class="current-user--role">
{{ currentUser.role }}
{{ currentRole }}
</h5>
</div>
<span class="current-user--options icon ion-android-more-vertical">
</span>
<span class="current-user--options icon ion-android-more-vertical" />
</div>
</div>
<woot-modal
:show="showAccountModal"
:on-close="onClose"
class="account-selector--modal"
>
<woot-modal-header
:header-title="$t('SIDEBAR_ITEMS.CHANGE_ACCOUNTS')"
:header-content="$t('SIDEBAR_ITEMS.SELECTOR_SUBTITLE')"
/>
<div
v-for="account in currentUser.accounts"
:key="account.id"
class="account-selector"
>
<a :href="`/app/accounts/${account.id}/dashboard`">
<i v-if="account.id === accountId" class="ion ion-ios-checkmark" />
<label :for="account.name" class="account--details">
<div class="account--name">{{ account.name }}</div>
<div class="account--role">{{ account.role }}</div>
</label>
</a>
</div>
</woot-modal>
</aside>
</template>
@ -82,7 +111,7 @@ import SidebarItem from './SidebarItem';
import WootStatusBar from '../widgets/StatusBar';
import { frontendURL } from '../../helper/URLHelper';
import Thumbnail from '../widgets/Thumbnail';
import sidemenuItems from '../../i18n/default-sidebar';
import { getSidebarItems } from '../../i18n/default-sidebar';
export default {
components: {
@ -100,23 +129,30 @@ export default {
data() {
return {
showOptionsMenu: false,
showAccountModal: false,
};
},
computed: {
...mapGetters({
daysLeft: 'getTrialLeft',
subscriptionData: 'getSubscription',
inboxes: 'inboxes/getInboxes',
currentUser: 'getCurrentUser',
daysLeft: 'getTrialLeft',
globalConfig: 'globalConfig/get',
inboxes: 'inboxes/getInboxes',
subscriptionData: 'getSubscription',
accountId: 'getCurrentAccountId',
currentRole: 'getCurrentRole',
}),
sidemenuItems() {
return getSidebarItems(this.accountId);
},
accessibleMenuItems() {
// get all keys in menuGroup
const groupKey = Object.keys(sidemenuItems);
const groupKey = Object.keys(this.sidemenuItems);
let menuItems = [];
// Iterate over menuGroup to find the correct group
for (let i = 0; i < groupKey.length; i += 1) {
const groupItem = sidemenuItems[groupKey[i]];
const groupItem = this.sidemenuItems[groupKey[i]];
// Check if current route is included
const isRouteIncluded = groupItem.routes.includes(this.currentRoute);
if (isRouteIncluded) {
@ -134,7 +170,7 @@ export default {
return this.$store.state.route.name;
},
shouldShowInboxes() {
return sidemenuItems.common.routes.includes(this.currentRoute);
return this.sidemenuItems.common.routes.includes(this.currentRoute);
},
inboxSection() {
return {
@ -176,9 +212,6 @@ export default {
trialMessage() {
return `${this.daysLeft} ${this.$t('APP_GLOBAL.TRIAL_MESSAGE')}`;
},
accountId() {
return this.currentUser.account_id;
},
},
mounted() {
this.$store.dispatch('inboxes/get');
@ -190,13 +223,14 @@ export default {
);
},
filterMenuItemsByRole(menuItems) {
const { role } = this.currentUser;
if (!role) {
if (!this.currentRole) {
return [];
}
return menuItems.filter(
menuItem =>
window.roleWiseRoutes[role].indexOf(menuItem.toStateName) > -1
window.roleWiseRoutes[this.currentRole].indexOf(
menuItem.toStateName
) > -1
);
},
logout() {
@ -205,6 +239,80 @@ export default {
showOptions() {
this.showOptionsMenu = !this.showOptionsMenu;
},
changeAccount() {
this.showAccountModal = true;
},
onClose() {
this.showAccountModal = false;
},
},
};
</script>
<style lang="scss">
@import '~dashboard/assets/scss/variables';
.account-selector--modal {
.modal-container {
width: 40rem;
}
.page-top-bar {
padding-bottom: $space-two;
}
}
.change-accounts--button.button {
font-weight: $font-weight-normal;
font-size: $font-size-small;
padding: $space-small $space-one;
}
.dropdown-pane {
li {
a {
padding: $space-small $space-one !important;
}
}
}
.account-selector {
cursor: pointer;
padding: $space-small $space-large;
.ion-ios-checkmark {
font-size: $font-size-big;
& + .account--details {
padding-left: $space-normal;
}
}
.account--details {
padding-left: $space-large + $space-smaller;
}
&:last-child {
margin-bottom: $space-large;
}
a {
align-items: center;
cursor: pointer;
display: flex;
.account--name {
cursor: pointer;
font-size: $font-size-medium;
font-weight: $font-weight-medium;
line-height: 1;
}
.account--role {
cursor: pointer;
font-size: $font-size-mini;
text-transform: capitalize;
}
}
}
</style>

View file

@ -50,7 +50,7 @@
import { mapGetters } from 'vuex';
import router from '../../routes';
import auth from '../../api/auth';
import adminMixin from '../../mixins/isAdmin';
const INBOX_TYPES = {
WEB: 'Channel::WebWidget',
@ -78,6 +78,7 @@ const getInboxClassByType = type => {
};
export default {
mixins: [adminMixin],
props: {
menuItem: {
type: Object,
@ -119,7 +120,7 @@ export default {
router.push({ name: 'settings_inbox_new', params: { page: 'new' } });
},
showItem(item) {
return auth.isAdmin() && item.newLink !== undefined;
return this.isAdmin && item.newLink !== undefined;
},
},
};

View file

@ -27,11 +27,17 @@
<script>
/* eslint no-console: 0 */
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
export default {
mixins: [globalConfigMixin],
props: {
isFullwidth: Boolean,
items: {
type: Array,
default: () => [],
},
},
computed: {
classObject() {
return 'full-width';
@ -39,9 +45,6 @@ export default {
activeIndex() {
return this.items.findIndex(i => i.route === this.$route.name);
},
items() {
return this.$t('INBOX_MGMT.CREATE_FLOW');
},
},
methods: {
isActive(item) {

View file

@ -0,0 +1,80 @@
<template>
<div class="colorpicker">
<div
class="colorpicker--selected"
:style="`background-color: ${value}`"
@click.prevent="toggleColorPicker"
/>
<chrome
v-if="isPickerOpen"
v-on-clickaway="closeTogglePicker"
:disable-alpha="true"
:value="value"
class="colorpicker--chrome"
@input="updateColor"
/>
</div>
</template>
<script>
import { Chrome } from 'vue-color';
import { mixin as clickaway } from 'vue-clickaway';
export default {
components: {
Chrome,
},
mixins: [clickaway],
props: {
value: {
type: String,
default: '',
},
},
data() {
return {
isPickerOpen: false,
};
},
methods: {
closeTogglePicker() {
if (this.isPickerOpen) {
this.toggleColorPicker();
}
},
toggleColorPicker() {
this.isPickerOpen = !this.isPickerOpen;
},
updateColor(e) {
this.$emit('input', e.hex);
},
},
};
</script>
<style scoped lang="scss">
@import '~dashboard/assets/scss/variables';
@import '~dashboard/assets/scss/mixins';
.colorpicker {
position: relative;
}
.colorpicker--selected {
border-radius: $space-smaller;
cursor: pointer;
height: $space-large;
margin-bottom: $space-normal;
width: $space-large;
}
.colorpicker--chrome.vc-chrome {
@include elegant-card;
border: 1px solid $color-border;
border-radius: $space-smaller;
margin-top: -$space-one;
position: absolute;
z-index: 9999;
}
</style>

View file

@ -105,6 +105,15 @@ export default {
return `user-thumbnail ${classname}`;
},
},
watch: {
src: {
handler(value, oldValue) {
if (value !== oldValue && this.imgError) {
this.imgError = false;
}
},
},
},
methods: {
onImgError() {
this.imgError = true;

View file

@ -6,15 +6,15 @@
>
<Thumbnail
v-if="!hideThumbnail"
:src="chat.meta.sender.thumbnail"
:badge="chat.meta.sender.channel"
:src="currentContact.thumbnail"
:badge="currentContact.channel"
class="columns"
:username="chat.meta.sender.name"
:username="currentContact.name"
size="40px"
/>
<div class="conversation--details columns">
<h4 class="conversation--user">
{{ chat.meta.sender.name }}
{{ currentContact.name }}
<span
v-if="!hideInboxName && isInboxNameVisible"
v-tooltip.bottom="inboxName(chat.inbox_id)"
@ -25,12 +25,13 @@
</h4>
<p
class="conversation--message"
v-html="extractMessageText(lastMessage(chat))"
></p>
v-html="extractMessageText(lastMessageInChat)"
/>
<div class="conversation--meta">
<span class="timestamp">
{{ dynamicTime(lastMessage(chat).created_at) }}
{{
lastMessageInChat ? dynamicTime(lastMessageInChat.created_at) : ''
}}
</span>
<span class="unread">{{ getUnreadCount }}</span>
</div>
@ -75,8 +76,15 @@ export default {
inboxesList: 'inboxes/getInboxes',
activeInbox: 'getSelectedInbox',
currentUser: 'getCurrentUser',
accountId: 'getCurrentAccountId',
}),
currentContact() {
return this.$store.getters['contacts/getContact'](
this.chat.meta.sender.id
);
},
isActiveChat() {
return this.currentChat.id === this.chat.id;
},
@ -92,19 +100,23 @@ export default {
isInboxNameVisible() {
return !this.activeInbox;
},
lastMessageInChat() {
return this.lastMessage(this.chat);
},
},
methods: {
cardClick(chat) {
const { activeInbox } = this;
const path = conversationUrl(
this.currentUser.account_id,
activeInbox,
chat.id
);
const path = conversationUrl(this.accountId, activeInbox, chat.id);
router.push({ path: frontendURL(path) });
},
extractMessageText(chatItem) {
if (!chatItem) {
return '';
}
const { content, attachments } = chatItem;
if (content) {

View file

@ -2,14 +2,14 @@
<div class="conv-header">
<div class="user">
<Thumbnail
:src="chat.meta.sender.thumbnail"
:src="currentContact.thumbnail"
size="40px"
:badge="chat.meta.sender.channel"
:username="chat.meta.sender.name"
:badge="currentContact.channel"
:username="currentContact.name"
/>
<div class="user--profile__meta">
<h3 v-if="!isContactPanelOpen" class="user--name text-truncate">
{{ chat.meta.sender.name }}
{{ currentContact.name }}
</h3>
<button
class="user--profile__button clear button small"
@ -79,6 +79,13 @@ export default {
agents: 'agents/getVerifiedAgents',
currentChat: 'getSelectedChat',
}),
currentContact() {
return this.$store.getters['contacts/getContact'](
this.chat.meta.sender.id
);
},
agentList() {
return [
{

View file

@ -9,7 +9,7 @@
<!-- No inboxes attached -->
<div v-if="!inboxesList.length">
<img src="~dashboard/assets/images/inboxes.svg" alt="No Inboxes" />
<span v-if="isAdmin()">
<span v-if="isAdmin">
{{ $t('CONVERSATION.NO_INBOX_1') }}
<br />
<router-link :to="newInboxURL">
@ -17,7 +17,7 @@
</router-link>
{{ $t('CONVERSATION.NO_INBOX_2') }}
</span>
<span v-if="!isAdmin()">
<span v-if="!isAdmin">
{{ $t('CONVERSATION.NO_INBOX_AGENT') }}
</span>
</div>

View file

@ -208,7 +208,9 @@ export default {
async sendMessage() {
const isMessageEmpty = !this.message.replace(/\n/g, '').length;
if (isMessageEmpty) return;
if (this.message.length > this.maxLength) {
return;
}
if (!this.showCannedResponsesList) {
try {
await this.$store.dispatch('sendMessage', {

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