Compare commits

..
1033 changed files with 5682 additions and 20491 deletions
+1 -1
View File
@@ -199,7 +199,7 @@ ANDROID_SHA256_CERT_FINGERPRINT=AC:73:8E:DE:EB:56:EA:CC:10:87:02:A7:65:37:7B:38:
## Rack Attack configuration
## To prevent and throttle abusive requests
# ENABLE_RACK_ATTACK=true
# RACK_ATTACK_LIMIT=300
# RACK_ATTACK_IP_LIMIT=3000
# ENABLE_RACK_ATTACK_WIDGET_API=true
## Running chatwoot as an API only server
+3 -7
View File
@@ -6,7 +6,7 @@ module.exports = {
'plugin:storybook/recommended',
],
parserOptions: {
parser: '@babel/eslint-parser',
parser: 'babel-eslint',
ecmaVersion: 2020,
sourceType: 'module',
},
@@ -24,16 +24,13 @@ module.exports = {
'jsx-a11y/label-has-for': 'off',
'jsx-a11y/anchor-is-valid': 'off',
'import/no-unresolved': 'off',
'vue/html-indent': 'off',
'vue/multi-word-component-names': 'off',
'vue/max-attributes-per-line': [
'error',
{
singleline: {
max: 20,
},
singleline: 20,
multiline: {
max: 1,
allowFirstLine: false,
},
},
],
@@ -50,7 +47,6 @@ module.exports = {
},
],
'vue/no-v-html': 'off',
'vue/component-definition-name-casing': 'off',
'vue/singleline-html-element-content-newline': 'off',
'import/extensions': ['off'],
'no-console': 'error',
+1 -6
View File
@@ -69,12 +69,7 @@ jobs:
# Run Response Bot specs
- name: Run backend tests
run: |
bundle exec rspec \
spec/enterprise/controllers/api/v1/accounts/response_sources_controller_spec.rb \
spec/enterprise/services/enterprise/message_templates/response_bot_service_spec.rb \
spec/enterprise/controllers/enterprise/api/v1/accounts/inboxes_controller_spec.rb:47 \
--profile=10 \
--format documentation
bundle exec rspec spec/enterprise/controllers/api/v1/accounts/response_sources_controller_spec.rb spec/enterprise/controllers/enterprise/api/v1/accounts/inboxes_controller_spec.rb:47 --profile=10 --format documentation
- name: Upload rails log folder
uses: actions/upload-artifact@v3
-1
View File
@@ -58,7 +58,6 @@ Metrics/BlockLength:
Metrics/ModuleLength:
Exclude:
- lib/seeders/message_seeder.rb
- spec/support/slack_stubs.rb
Rails/ApplicationController:
Exclude:
- 'app/controllers/api/v1/widget/messages_controller.rb'
+7 -8
View File
@@ -15,7 +15,7 @@ gem 'browser'
gem 'hashie'
gem 'jbuilder'
gem 'kaminari'
gem 'responders', '>= 3.1.1'
gem 'responders'
gem 'rest-client'
gem 'telephone_number'
gem 'time_diff'
@@ -67,7 +67,7 @@ gem 'webpacker'
gem 'barnes'
##--- gems for authentication & authorization ---##
gem 'devise', '>= 4.9.3'
gem 'devise'
gem 'devise-secure_password', git: 'https://github.com/chatwoot/devise-secure_password', branch: 'chatwoot'
gem 'devise_token_auth'
# authorization
@@ -76,7 +76,6 @@ gem 'pundit'
# super admin
gem 'administrate', '>= 0.19.0'
gem 'administrate-field-active_storage'
gem 'administrate-field-belongs_to_search'
##--- gems for pubsub service ---##
# https://karolgalanciak.com/blog/2019/11/30/from-activerecord-callbacks-to-publish-slash-subscribe-pattern-and-event-driven-design/
@@ -92,7 +91,7 @@ gem 'twitty', '~> 0.1.5'
# facebook client
gem 'koala'
# slack client
gem 'slack-ruby-client', '~> 2.2.0'
gem 'slack-ruby-client', '~> 2.0.0'
# for dialogflow integrations
gem 'google-cloud-dialogflow-v2'
gem 'grpc'
@@ -109,9 +108,9 @@ gem 'elastic-apm', require: false
gem 'newrelic_rpm', require: false
gem 'newrelic-sidekiq-metrics', require: false
gem 'scout_apm', require: false
gem 'sentry-rails', '>= 5.12.0', require: false
gem 'sentry-rails', '>= 5.11.0', require: false
gem 'sentry-ruby', require: false
gem 'sentry-sidekiq', '>= 5.12.0', require: false
gem 'sentry-sidekiq', '>= 5.11.0', require: false
##-- background job processing --##
gem 'sidekiq', '>= 7.1.3'
@@ -154,12 +153,12 @@ gem 'stripe'
gem 'faker'
# Include logrange conditionally in intializer using env variable
gem 'lograge', '~> 0.14.0', require: false
gem 'lograge', '~> 0.13.0', require: false
# worked with microsoft refresh token
gem 'omniauth-oauth2'
gem 'audited', '~> 5.4', '>= 5.4.0'
gem 'audited', '~> 5.3'
# need for google auth
gem 'omniauth'
+24 -29
View File
@@ -116,18 +116,13 @@ GEM
administrate-field-active_storage (0.4.2)
administrate (>= 0.2.2)
rails (>= 7.0)
administrate-field-belongs_to_search (0.8.0)
administrate (>= 0.3, < 1.0)
jbuilder (~> 2)
rails (>= 4.2, < 7.1)
selectize-rails (~> 0.6)
annotate (3.2.0)
activerecord (>= 3.2, < 8.0)
rake (>= 10.4, < 14.0)
ast (2.4.2)
attr_extras (7.1.0)
audited (5.4.0)
activerecord (>= 5.0, < 7.2)
audited (5.3.3)
activerecord (>= 5.0, < 7.1)
request_store (~> 1.2)
aws-eventstream (1.2.0)
aws-partitions (1.760.0)
@@ -148,7 +143,7 @@ GEM
barnes (0.0.9)
multi_json (~> 1)
statsd-ruby (~> 1.1)
bcrypt (3.1.19)
bcrypt (3.1.18)
bindex (0.8.1)
blingfire (0.1.8)
bootsnap (1.16.0)
@@ -193,7 +188,7 @@ GEM
irb (>= 1.5.0)
reline (>= 0.3.1)
declarative (0.0.20)
devise (4.9.3)
devise (4.9.2)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
railties (>= 4.1.0)
@@ -273,7 +268,7 @@ GEM
googleauth (~> 1.0)
grpc (~> 1.36)
geocoder (1.8.1)
gli (2.21.1)
gli (2.21.0)
globalid (1.2.1)
activesupport (>= 6.1)
gmail_xoauth (0.4.2)
@@ -434,12 +429,12 @@ GEM
llhttp-ffi (0.4.0)
ffi-compiler (~> 1.0)
rake (~> 13.0)
lograge (0.14.0)
lograge (0.13.0)
actionpack (>= 4)
activesupport (>= 4)
railties (>= 4)
request_store (~> 1.0)
loofah (2.21.4)
loofah (2.21.3)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
mail (2.8.1)
@@ -616,7 +611,7 @@ GEM
uber (< 0.2.0)
request_store (1.5.1)
rack (>= 1.4)
responders (3.1.1)
responders (3.1.0)
actionpack (>= 5.2)
railties (>= 5.2)
rest-client (2.1.0)
@@ -702,18 +697,18 @@ GEM
activesupport (>= 4)
selectize-rails (0.12.6)
semantic_range (3.0.0)
sentry-rails (5.12.0)
sentry-rails (5.11.0)
railties (>= 5.0)
sentry-ruby (~> 5.12.0)
sentry-ruby (5.12.0)
sentry-ruby (~> 5.11.0)
sentry-ruby (5.11.0)
concurrent-ruby (~> 1.0, >= 1.0.2)
sentry-sidekiq (5.12.0)
sentry-ruby (~> 5.12.0)
sentry-sidekiq (5.11.0)
sentry-ruby (~> 5.11.0)
sidekiq (>= 3.0)
sexp_processor (4.17.0)
shoulda-matchers (5.3.0)
activesupport (>= 5.2.0)
sidekiq (7.1.6)
sidekiq (7.1.3)
concurrent-ruby (< 2)
connection_pool (>= 2.3.0)
rack (>= 2.2.4)
@@ -732,12 +727,13 @@ GEM
json (>= 1.8, < 3)
simplecov-html (~> 0.10.0)
simplecov-html (0.10.2)
slack-ruby-client (2.2.0)
slack-ruby-client (2.0.0)
faraday (>= 2.0)
faraday-mashify
faraday-multipart
gli
hashie
websocket-driver
snaky_hash (2.0.1)
hashie
version_gem (~> 1.1, >= 1.1.1)
@@ -816,7 +812,7 @@ GEM
working_hours (1.4.1)
activesupport (>= 3.2)
tzinfo
zeitwerk (2.6.12)
zeitwerk (2.6.11)
PLATFORMS
arm64-darwin-20
@@ -835,10 +831,9 @@ DEPENDENCIES
acts-as-taggable-on
administrate (>= 0.19.0)
administrate-field-active_storage
administrate-field-belongs_to_search
annotate
attr_extras
audited (~> 5.4, >= 5.4.0)
audited (~> 5.3)
aws-sdk-s3
azure-storage-blob!
barnes
@@ -855,7 +850,7 @@ DEPENDENCIES
database_cleaner
ddtrace
debug (~> 1.8)
devise (>= 4.9.3)
devise
devise-secure_password!
devise_token_auth
dotenv-rails
@@ -891,7 +886,7 @@ DEPENDENCIES
line-bot-api
liquid
listen
lograge (~> 0.14.0)
lograge (~> 0.13.0)
maxminddb
mock_redis
neighbor
@@ -915,7 +910,7 @@ DEPENDENCIES
rails (~> 7.0.8.0)
redis
redis-namespace
responders (>= 3.1.1)
responders
rest-client
reverse_markdown
rspec-rails
@@ -927,14 +922,14 @@ DEPENDENCIES
scout_apm
scss_lint
seed_dump
sentry-rails (>= 5.12.0)
sentry-rails (>= 5.11.0)
sentry-ruby
sentry-sidekiq (>= 5.12.0)
sentry-sidekiq (>= 5.11.0)
shoulda-matchers
sidekiq (>= 7.1.3)
sidekiq-cron (>= 1.10.1)
simplecov (= 0.17.1)
slack-ruby-client (~> 2.2.0)
slack-ruby-client (~> 2.0.0)
spring
spring-watcher-listen
squasher
+1 -1
View File
@@ -1 +1 @@
3.1.0
2.17.0
+1 -1
View File
@@ -1 +1 @@
2.6.0
2.3.0
@@ -29,5 +29,3 @@
@import 'components/pagination';
@import 'components/search';
@import 'components/reports';
@import 'custom_styles';
@@ -24,6 +24,14 @@ select {
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"],
@@ -43,7 +51,6 @@ textarea {
background-color: $white;
border: $base-border;
border-radius: $base-border-radius;
font-family: $base-font-family;
padding: 0.5em;
transition: border-color $base-duration $base-timing;
width: 100%;
@@ -4,8 +4,8 @@
float: left;
margin-bottom: $base-spacing;
margin-top: 0.25em;
text-align: left;
width: calc(16% - 1rem);
text-align: right;
width: calc(15% - 1rem);
}
.preserve-whitespace {
@@ -17,7 +17,7 @@
float: left;
margin-bottom: $base-spacing;
margin-left: 2rem;
width: calc(84% - 1rem);
width: calc(85% - 1rem);
}
.attribute--nested {
@@ -10,7 +10,7 @@ input[type="submit"],
color: $white;
cursor: pointer;
display: inline-block;
font-size: $font-size-small;
font-size: $font-size-default;
-webkit-font-smoothing: antialiased;
font-weight: $font-weight-medium;
line-height: 1;
@@ -1,18 +1,13 @@
.main-content {
font-size: $font-size-default;
left: 21rem;
left: 23rem;
position: absolute;
right: 0;
top: 0;
}
.main-content__body {
font-size: $font-size-small;
padding: $space-two;
table {
font-size: $font-size-small;
}
}
.main-content__header {
@@ -25,7 +20,7 @@
}
.main-content__page-title {
font-size: $font-size-medium;
font-size: $font-size-large;
font-weight: $font-weight-medium;
margin-right: auto;
}
@@ -1,12 +1,7 @@
.logo-brand {
margin-bottom: $space-normal;
padding: $space-normal $space-smaller $space-small;
text-align: left;
img {
margin-bottom: $space-smaller;
max-height: 3rem;
}
text-align: center;
}
.navigation {
@@ -24,13 +19,12 @@
padding: $space-normal;
position: fixed;
top: 0;
width: 21rem;
width: 23rem;
z-index: 1023;
li {
align-items: center;
display: flex;
font-size: $font-size-small;
a {
color: $color-gray;
@@ -41,10 +35,6 @@
min-width: $space-medium;
}
}
hr {
margin: $space-slab;
}
}
.navigation__link {
@@ -53,7 +43,7 @@
display: block;
line-height: 1;
margin-bottom: $space-smaller;
padding: $space-small;
padding: $space-one;
&:hover {
color: $blue;
@@ -1,24 +0,0 @@
// custom styles for the dashboard
.feature-cell {
background: $color-extra-light-blue;
border-radius: 10px;
float: left;
margin-left: 8px;
margin-top: 6px;
padding: 4px 12px;
.icon-container {
margin-right: 4px;
}
.value-container {
margin-left: 6px;
}
}
.feature-container {
max-width: 100rem;
}
@@ -4,7 +4,7 @@ $base-font-family: PlusJakarta, Inter, -apple-system, BlinkMacSystemFont, "Segoe
sans-serif !default;
$heading-font-family: $base-font-family !default;
$base-font-size: 14px !default;
$base-font-size: 16px !default;
$base-line-height: 1.5 !default;
$heading-line-height: 1.2 !default;
@@ -78,6 +78,11 @@ $conv-header-height: 4rem;
$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;
@@ -93,9 +93,6 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
message_type: @message_type,
content: response.content,
source_id: response.identifier,
content_attributes: {
in_reply_to_external_id: response.in_reply_to_external_id
},
sender: @outgoing_echo ? nil : @contact_inbox.contact
}
end
+1 -14
View File
@@ -15,10 +15,7 @@ class V2::ReportBuilder
end
def timeseries
return send(params[:metric]) if metric_valid?
Rails.logger.error "ReportBuilder: Invalid metric - #{params[:metric]}"
{}
send(params[:metric])
end
# For backward compatible with old report
@@ -56,16 +53,6 @@ class V2::ReportBuilder
private
def metric_valid?
%w[conversations_count
incoming_messages_count
outgoing_messages_count
avg_first_response_time
avg_resolution_time reply_time
resolutions_count
reply_time].include?(params[:metric])
end
def inbox
@inbox ||= account.inboxes.find(params[:id])
end
@@ -9,6 +9,7 @@ class Api::V1::Accounts::Actions::ContactMergesController < Api::V1::Accounts::B
mergee_contact: @mergee_contact
)
contact_merge_action.perform
render json: @base_contact
end
private
@@ -1,21 +0,0 @@
class Api::V1::Accounts::ContactInboxesController < Api::V1::Accounts::BaseController
before_action :ensure_inbox
def filter
contact_inbox = @inbox.contact_inboxes.where(inbox_id: permitted_params[:inbox_id], source_id: permitted_params[:source_id])
return head :not_found if contact_inbox.empty?
@contact = contact_inbox.first.contact
end
private
def ensure_inbox
@inbox = Current.account.inboxes.find(permitted_params[:inbox_id])
authorize @inbox, :show?
end
def permitted_params
params.permit(:inbox_id, :source_id)
end
end
@@ -25,8 +25,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
render json: { error: 'Specify search string with parameter q' }, status: :unprocessable_entity if params[:q].blank? && return
contacts = resolved_contacts.where(
'name ILIKE :search OR email ILIKE :search OR phone_number ILIKE :search OR contacts.identifier LIKE :search
OR contacts.additional_attributes->>\'company_name\' ILIKE :search',
'name ILIKE :search OR email ILIKE :search OR phone_number ILIKE :search OR contacts.identifier LIKE :search',
search: "%#{params[:q].strip}%"
)
@contacts_count = contacts.count
@@ -60,7 +60,7 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
end
def toggle_status
if params[:status].present?
if params[:status]
set_conversation_status
@status = @conversation.save!
else
@@ -1,15 +1,7 @@
class Api::V1::Integrations::WebhooksController < ApplicationController
def create
builder = Integrations::Slack::IncomingMessageBuilder.new(permitted_params)
builder = Integrations::Slack::IncomingMessageBuilder.new(params)
response = builder.perform
render json: response
end
private
# TODO: This is a temporary solution to permit all params for slack unfurling job.
# We should only permit the params that we use. Handle all the params based on events and send it to the respective services.
def permitted_params
params.permit!
end
end
@@ -1,4 +1,4 @@
class Api::V1::Accounts::UploadController < Api::V1::Accounts::BaseController
class Api::V1::UploadController < Api::BaseController
def create
file_blob = ActiveStorage::Blob.create_and_upload!(
key: nil,
@@ -1,6 +1,5 @@
class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
include Events::Types
before_action :render_not_found_if_empty, only: [:toggle_typing, :toggle_status, :set_custom_attributes, :destroy_custom_attributes]
def index
@conversation = conversation
@@ -28,7 +27,6 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
conversation.contact_last_seen_at = DateTime.now.utc
conversation.save!
::Conversations::MarkMessagesAsReadJob.perform_later(conversation)
head :ok
end
@@ -43,6 +41,8 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
end
def toggle_typing
head :ok && return if conversation.nil?
case permitted_params[:typing_status]
when 'on'
trigger_typing_event(CONVERSATION_TYPING_ON)
@@ -54,6 +54,8 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
end
def toggle_status
return head :not_found if conversation.nil?
return head :forbidden unless @web_widget.end_conversation?
unless conversation.resolved?
@@ -79,10 +81,6 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
Rails.configuration.dispatcher.dispatch(event, Time.zone.now, conversation: conversation, user: @contact)
end
def render_not_found_if_empty
return head :not_found if conversation.nil?
end
def permitted_params
params.permit(:id, :typing_status, :website_token, :email, contact: [:name, :email, :phone_number],
message: [:content, :referer_url, :timestamp, :echo_id],
@@ -9,14 +9,11 @@ module RequestExceptionHandler
def handle_with_exception
yield
rescue ActiveRecord::RecordNotFound => e
log_handled_error(e)
rescue ActiveRecord::RecordNotFound
render_not_found_error('Resource could not be found')
rescue Pundit::NotAuthorizedError => e
log_handled_error(e)
rescue Pundit::NotAuthorizedError
render_unauthorized('You are not authorized to do this action')
rescue ActionController::ParameterMissing => e
log_handled_error(e)
render_could_not_create_error(e.message)
ensure
# to address the thread variable leak issues in Puma/Thin webserver
@@ -44,7 +41,6 @@ module RequestExceptionHandler
end
def render_record_invalid(exception)
log_handled_error(exception)
render json: {
message: exception.record.errors.full_messages.join(', '),
attributes: exception.record.errors.attribute_names
@@ -52,11 +48,6 @@ module RequestExceptionHandler
end
def render_error_response(exception)
log_handled_error(exception)
render json: exception.to_hash, status: exception.http_status
end
def log_handled_error(exception)
logger.info("Handled error: #{exception.inspect}")
end
end
+1 -1
View File
@@ -55,7 +55,7 @@ class DashboardController < ActionController::Base
VAPID_PUBLIC_KEY: VapidService.public_key,
ENABLE_ACCOUNT_SIGNUP: GlobalConfigService.load('ENABLE_ACCOUNT_SIGNUP', 'false'),
FB_APP_ID: GlobalConfigService.load('FB_APP_ID', ''),
FACEBOOK_API_VERSION: 'v14.0',
FACEBOOK_API_VERSION: 'v17.0',
IS_ENTERPRISE: ChatwootApp.enterprise?,
AZURE_APP_ID: ENV.fetch('AZURE_APP_ID', ''),
GIT_SHA: GIT_HASH
@@ -23,7 +23,6 @@ class Public::Api::V1::Inboxes::ConversationsController < Public::Api::V1::Inbox
def update_last_seen
@conversation.contact_last_seen_at = DateTime.now.utc
@conversation.save!
::Conversations::MarkMessagesAsReadJob.perform_later(@conversation)
head :ok
end
@@ -34,7 +33,7 @@ class Public::Api::V1::Inboxes::ConversationsController < Public::Api::V1::Inbox
end
def create_conversation
ConversationBuilder.new(params: conversation_params, contact_inbox: @contact_inbox).perform
::Conversation.create!(conversation_params)
end
def trigger_typing_event(event)
@@ -42,6 +41,11 @@ class Public::Api::V1::Inboxes::ConversationsController < Public::Api::V1::Inbox
end
def conversation_params
params.permit(custom_attributes: {})
{
account_id: @contact_inbox.contact.account_id,
inbox_id: @contact_inbox.inbox_id,
contact_id: @contact_inbox.contact_id,
contact_inbox_id: @contact_inbox.id
}
end
end
@@ -44,7 +44,7 @@ class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::B
end
def list_params
params.permit(:query, :locale, :sort, :status)
params.permit(:query, :locale, :sort)
end
def permitted_params
@@ -1,6 +1,5 @@
class Public::Api::V1::Portals::BaseController < PublicController
before_action :show_plain_layout
before_action :set_color_scheme
around_action :set_locale
after_action :allow_iframe_requests
@@ -10,14 +9,6 @@ class Public::Api::V1::Portals::BaseController < PublicController
@is_plain_layout_enabled = params[:show_plain_layout] == 'true'
end
def set_color_scheme
@theme = if %w[dark light].include?(params[:theme])
params[:theme]
else
''
end
end
def set_locale(&)
switch_locale_with_portal(&) if params[:locale].present?
switch_locale_with_article(&) if params[:article_slug].present?
@@ -1,27 +0,0 @@
class SlackUploadsController < ApplicationController
include Rails.application.routes.url_helpers
before_action :set_blob, only: [:show]
def show
if @blob
redirect_to blob_url
else
redirect_to avatar_url
end
end
private
def set_blob
@blob = ActiveStorage::Blob.find_by(key: params[:blob_key])
end
def blob_url
url_for(@blob.representation(resize_to_fill: [250, nil]))
end
def avatar_url
base_url = ENV.fetch('FRONTEND_URL', nil)
"#{base_url}/integrations/slack/#{params[:sender_type]}.png"
end
end
@@ -6,7 +6,6 @@ class SuperAdmin::InstanceStatusesController < SuperAdmin::ApplicationController
postgres_status
redis_metrics
chatwoot_edition
instance_meta
end
def chatwoot_edition
@@ -19,10 +18,6 @@ class SuperAdmin::InstanceStatusesController < SuperAdmin::ApplicationController
end
end
def instance_meta
@metrics['Database Migrations'] = ActiveRecord::Base.connection.migration_context.needs_migration? ? 'pending' : 'completed'
end
def chatwoot_version
@metrics['Chatwoot version'] = Chatwoot.config[:version]
end
@@ -1,21 +0,0 @@
class Twilio::DeliveryStatusController < ApplicationController
def create
::Twilio::DeliveryStatusService.new(params: permitted_params).perform
head :no_content
end
private
def permitted_params
params.permit(
:AccountSid,
:From,
:MessageSid,
:MessagingServiceSid,
:MessageStatus,
:ErrorCode,
:ErrorMessage
)
end
end
+2 -2
View File
@@ -18,8 +18,8 @@ class AccountDashboard < Administrate::BaseDashboard
end
ATTRIBUTE_TYPES = {
id: Field::Number.with_options(searchable: true),
name: Field::String.with_options(searchable: true),
id: Field::Number,
name: Field::String,
created_at: Field::DateTime,
updated_at: Field::DateTime,
users: CountField,
+3 -3
View File
@@ -8,9 +8,9 @@ class AccountUserDashboard < Administrate::BaseDashboard
# which determines how the attribute is displayed
# on pages throughout the dashboard.
ATTRIBUTE_TYPES = {
account: Field::BelongsToSearch.with_options(class_name: 'Account', searchable: true, searchable_field: [:name, :id], order: 'id DESC'),
user: Field::BelongsToSearch.with_options(class_name: 'User', searchable: true, searchable_field: [:name, :email, :id], order: 'id DESC'),
inviter: Field::BelongsToSearch.with_options(class_name: 'User', searchable: true, searchable_field: [:name, :email, :id], order: 'id DESC'),
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,
+3 -3
View File
@@ -9,7 +9,7 @@ class UserDashboard < Administrate::BaseDashboard
# on pages throughout the dashboard.
ATTRIBUTE_TYPES = {
account_users: Field::HasMany,
id: Field::Number.with_options(searchable: true),
id: Field::Number,
avatar_url: AvatarField,
avatar: Field::ActiveStorage.with_options(
destroy_url: proc do |_namespace, _resource, attachment|
@@ -28,9 +28,9 @@ class UserDashboard < Administrate::BaseDashboard
confirmed_at: Field::DateTime,
confirmation_sent_at: Field::DateTime,
unconfirmed_email: Field::String,
name: Field::String.with_options(searchable: true),
name: Field::String,
display_name: Field::String,
email: Field::String.with_options(searchable: true),
email: Field::String,
tokens: Field::String.with_options(searchable: false),
created_at: Field::DateTime,
updated_at: Field::DateTime,
-11
View File
@@ -1,11 +0,0 @@
module PortalHelper
def generate_portal_bg_color(portal_color, theme)
base_color = theme == 'dark' ? 'black' : 'white'
"color-mix(in srgb, #{portal_color} 10%, #{base_color})"
end
def generate_portal_bg(portal_color, theme)
bg_image = theme == 'dark' ? 'grid_dark.svg' : 'grid.svg'
"background: url(/assets/images/hc/#{bg_image}) #{generate_portal_bg_color(portal_color, theme)}"
end
end
+9 -7
View File
@@ -1,13 +1,13 @@
<template>
<div
v-if="!authUIFlags.isFetching && !accountUIFlags.isFetchingItem"
v-if="!authUIFlags.isFetching"
id="app"
class="app-wrapper h-full flex-grow-0 min-h-0 w-full"
:class="{ 'app-rtl--wrapper': isRTLView }"
:dir="isRTLView ? 'rtl' : 'ltr'"
>
<update-banner :latest-chatwoot-version="latestChatwootVersion" />
<template v-if="currentAccountId">
<template v-if="!accountUIFlags.isFetchingItem && currentAccountId">
<payment-pending-banner />
<upgrade-banner />
</template>
@@ -26,14 +26,14 @@
<script>
import { mapGetters } from 'vuex';
import AddAccountModal from '../dashboard/components/layout/sidebarComponents/AddAccountModal.vue';
import AddAccountModal from '../dashboard/components/layout/sidebarComponents/AddAccountModal';
import LoadingState from './components/widgets/LoadingState.vue';
import NetworkNotification from './components/NetworkNotification.vue';
import NetworkNotification from './components/NetworkNotification';
import UpdateBanner from './components/app/UpdateBanner.vue';
import UpgradeBanner from './components/app/UpgradeBanner.vue';
import PaymentPendingBanner from './components/app/PaymentPendingBanner.vue';
import vueActionCable from './helper/actionCable';
import WootSnackbarBox from './components/SnackbarContainer.vue';
import WootSnackbarBox from './components/SnackbarContainer';
import rtlMixin from 'shared/mixins/rtlMixin';
import { setColorTheme } from './helper/themeHelper';
import {
@@ -111,8 +111,10 @@ export default {
this.$store.dispatch('setActiveAccount', {
accountId: this.currentAccountId,
});
const { locale, latest_chatwoot_version: latestChatwootVersion } =
this.getAccount(this.currentAccountId);
const {
locale,
latest_chatwoot_version: latestChatwootVersion,
} = this.getAccount(this.currentAccountId);
const { pubsub_token: pubsubToken } = this.currentUser || {};
this.setLocale(locale);
this.updateRTLDirectionView(locale);
+3 -2
View File
@@ -15,8 +15,9 @@ class ApiClient {
// eslint-disable-next-line class-methods-use-this
get accountIdFromRoute() {
const isInsideAccountScopedURLs =
window.location.pathname.includes('/app/accounts');
const isInsideAccountScopedURLs = window.location.pathname.includes(
'/app/accounts'
);
if (isInsideAccountScopedURLs) {
return window.location.pathname.split('/')[3];
-5
View File
@@ -84,11 +84,6 @@ export default {
return axios.delete(endPoints('deleteAvatar').url);
},
resetPassword({ email }) {
const urlData = endPoints('resetPassword');
return axios.post(urlData.url, { email });
},
setActiveAccount({ accountId }) {
const urlData = endPoints('setActiveAccount');
return axios.put(urlData.url, {
-1
View File
@@ -53,7 +53,6 @@ class ContactAPI extends ApiClient {
return axios.get(requestURL);
}
// eslint-disable-next-line default-param-last
filter(page = 1, sortAttr = 'name', queryPayload) {
let requestURL = `${this.url}/filter?${buildContactParams(page, sortAttr)}`;
return axios.post(requestURL, queryPayload);
@@ -1,5 +1,6 @@
import accountAPI from '../account';
import ApiClient from '../../ApiClient';
import describeWithAPIMock from '../../specs/apiSpecHelper';
describe('#enterpriseAccountAPI', () => {
it('creates correct instance', () => {
@@ -12,33 +13,17 @@ describe('#enterpriseAccountAPI', () => {
expect(accountAPI).toHaveProperty('checkout');
});
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
window.axios = axiosMock;
});
afterEach(() => {
window.axios = originalAxios;
});
describeWithAPIMock('API calls', context => {
it('#checkout', () => {
accountAPI.checkout();
expect(axiosMock.post).toHaveBeenCalledWith(
expect(context.axiosMock.post).toHaveBeenCalledWith(
'/enterprise/api/v1/checkout'
);
});
it('#subscription', () => {
accountAPI.subscription();
expect(axiosMock.post).toHaveBeenCalledWith(
expect(context.axiosMock.post).toHaveBeenCalledWith(
'/enterprise/api/v1/subscription'
);
});
-1
View File
@@ -31,7 +31,6 @@ class ReportsAPI extends ApiClient {
});
}
// eslint-disable-next-line default-param-last
getSummary(since, until, type = 'account', id, groupBy, businessHours) {
return axios.get(`${this.url}/summary`, {
params: {
@@ -1,5 +1,6 @@
import accountAPI from '../account';
import ApiClient from '../ApiClient';
import describeWithAPIMock from './apiSpecHelper';
describe('#accountAPI', () => {
it('creates correct instance', () => {
@@ -12,28 +13,12 @@ describe('#accountAPI', () => {
expect(accountAPI).toHaveProperty('createAccount');
});
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
window.axios = axiosMock;
});
afterEach(() => {
window.axios = originalAxios;
});
describeWithAPIMock('API calls', context => {
it('#createAccount', () => {
accountAPI.createAccount({
name: 'Chatwoot',
});
expect(axiosMock.post).toHaveBeenCalledWith('/api/v1/accounts', {
expect(context.axiosMock.post).toHaveBeenCalledWith('/api/v1/accounts', {
name: 'Chatwoot',
});
});
@@ -1,5 +1,6 @@
import accountActionsAPI from '../accountActions';
import ApiClient from '../ApiClient';
import describeWithAPIMock from './apiSpecHelper';
describe('#ContactsAPI', () => {
it('creates correct instance', () => {
@@ -7,26 +8,10 @@ describe('#ContactsAPI', () => {
expect(accountActionsAPI).toHaveProperty('merge');
});
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
window.axios = axiosMock;
});
afterEach(() => {
window.axios = originalAxios;
});
describeWithAPIMock('API calls', context => {
it('#merge', () => {
accountActionsAPI.merge(1, 2);
expect(axiosMock.post).toHaveBeenCalledWith(
expect(context.axiosMock.post).toHaveBeenCalledWith(
'/api/v1/actions/contact_merge',
{
base_contact_id: 1,
@@ -0,0 +1,27 @@
function apiSpecHelper() {
beforeEach(() => {
this.originalAxios = window.axios;
this.axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
window.axios = this.axiosMock;
});
afterEach(() => {
window.axios = this.originalAxios;
});
}
// https://stackoverflow.com/a/59344023/3901856
const sharedWrapper = describe('sharedWrapper', () => {});
export default function describeWithAPIMock(skillName, testFn) {
return describe(skillName, function configureContext() {
function Context() {}
Context.prototype = sharedWrapper.ctx;
this.ctx = new Context();
apiSpecHelper.call(this);
testFn.call(this, this);
});
}
@@ -1,5 +1,6 @@
import articlesAPI from '../helpCenter/articles';
import ApiClient from 'dashboard/api/helpCenter/portals';
import describeWithAPIMock from './apiSpecHelper';
describe('#PortalAPI', () => {
it('creates correct instance', () => {
@@ -11,23 +12,7 @@ describe('#PortalAPI', () => {
expect(articlesAPI).toHaveProperty('delete');
expect(articlesAPI).toHaveProperty('getArticles');
});
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
window.axios = axiosMock;
});
afterEach(() => {
window.axios = originalAxios;
});
describeWithAPIMock('API calls', context => {
it('#getArticles', () => {
articlesAPI.getArticles({
pageNumber: 1,
@@ -36,62 +21,30 @@ describe('#PortalAPI', () => {
status: 'published',
author_id: '1',
});
expect(axiosMock.get).toHaveBeenCalledWith(
expect(context.axiosMock.get).toHaveBeenCalledWith(
'/api/v1/portals/room-rental/articles?page=1&locale=en-US&status=published&author_id=1'
);
});
});
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
window.axios = axiosMock;
});
afterEach(() => {
window.axios = originalAxios;
});
describeWithAPIMock('API calls', context => {
it('#getArticle', () => {
articlesAPI.getArticle({
id: 1,
portalSlug: 'room-rental',
});
expect(axiosMock.get).toHaveBeenCalledWith(
expect(context.axiosMock.get).toHaveBeenCalledWith(
'/api/v1/portals/room-rental/articles/1'
);
});
});
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
window.axios = axiosMock;
});
afterEach(() => {
window.axios = originalAxios;
});
describeWithAPIMock('API calls', context => {
it('#updateArticle', () => {
articlesAPI.updateArticle({
articleId: 1,
portalSlug: 'room-rental',
articleObj: { title: 'Update shipping address' },
});
expect(axiosMock.patch).toHaveBeenCalledWith(
expect(context.axiosMock.patch).toHaveBeenCalledWith(
'/api/v1/portals/room-rental/articles/1',
{
title: 'Update shipping address',
@@ -99,29 +52,13 @@ describe('#PortalAPI', () => {
);
});
});
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
window.axios = axiosMock;
});
afterEach(() => {
window.axios = originalAxios;
});
describeWithAPIMock('API calls', context => {
it('#deleteArticle', () => {
articlesAPI.deleteArticle({
articleId: 1,
portalSlug: 'room-rental',
});
expect(axiosMock.delete).toHaveBeenCalledWith(
expect(context.axiosMock.delete).toHaveBeenCalledWith(
'/api/v1/portals/room-rental/articles/1'
);
});
@@ -1,30 +1,18 @@
import assignableAgentsAPI from '../assignableAgents';
import describeWithAPIMock from './apiSpecHelper';
describe('#AssignableAgentsAPI', () => {
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
window.axios = axiosMock;
});
afterEach(() => {
window.axios = originalAxios;
});
describeWithAPIMock('API calls', context => {
it('#getAssignableAgents', () => {
assignableAgentsAPI.get([1]);
expect(axiosMock.get).toHaveBeenCalledWith('/api/v1/assignable_agents', {
params: {
inbox_ids: [1],
},
});
expect(context.axiosMock.get).toHaveBeenCalledWith(
'/api/v1/assignable_agents',
{
params: {
inbox_ids: [1],
},
}
);
});
});
});
@@ -1,5 +1,6 @@
import fbChannel from '../../channel/fbChannel';
import ApiClient from '../../ApiClient';
import describeWithAPIMock from '../apiSpecHelper';
describe('#FBChannel', () => {
it('creates correct instance', () => {
@@ -10,26 +11,10 @@ describe('#FBChannel', () => {
expect(fbChannel).toHaveProperty('update');
expect(fbChannel).toHaveProperty('delete');
});
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
window.axios = axiosMock;
});
afterEach(() => {
window.axios = originalAxios;
});
describeWithAPIMock('API calls', context => {
it('#create', () => {
fbChannel.create({ omniauthToken: 'ASFM131CSF@#@$', appId: 'chatwoot' });
expect(axiosMock.post).toHaveBeenCalledWith(
expect(context.axiosMock.post).toHaveBeenCalledWith(
'/api/v1/callbacks/register_facebook_page',
{
omniauthToken: 'ASFM131CSF@#@$',
@@ -42,7 +27,7 @@ describe('#FBChannel', () => {
omniauthToken: 'ASFM131CSF@#@$',
inboxId: 1,
});
expect(axiosMock.post).toHaveBeenCalledWith(
expect(context.axiosMock.post).toHaveBeenCalledWith(
'/api/v1/callbacks/reauthorize_page',
{
omniauth_token: 'ASFM131CSF@#@$',
@@ -1,5 +1,6 @@
import contactAPI, { buildContactParams } from '../contacts';
import ApiClient from '../ApiClient';
import describeWithAPIMock from './apiSpecHelper';
describe('#ContactsAPI', () => {
it('creates correct instance', () => {
@@ -14,67 +15,56 @@ describe('#ContactsAPI', () => {
expect(contactAPI).toHaveProperty('destroyAvatar');
});
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
window.axios = axiosMock;
});
afterEach(() => {
window.axios = originalAxios;
});
describeWithAPIMock('API calls', context => {
it('#get', () => {
contactAPI.get(1, 'name', 'customer-support');
expect(axiosMock.get).toHaveBeenCalledWith(
expect(context.axiosMock.get).toHaveBeenCalledWith(
'/api/v1/contacts?include_contact_inboxes=false&page=1&sort=name&labels[]=customer-support'
);
});
it('#getConversations', () => {
contactAPI.getConversations(1);
expect(axiosMock.get).toHaveBeenCalledWith(
expect(context.axiosMock.get).toHaveBeenCalledWith(
'/api/v1/contacts/1/conversations'
);
});
it('#getContactableInboxes', () => {
contactAPI.getContactableInboxes(1);
expect(axiosMock.get).toHaveBeenCalledWith(
expect(context.axiosMock.get).toHaveBeenCalledWith(
'/api/v1/contacts/1/contactable_inboxes'
);
});
it('#getContactLabels', () => {
contactAPI.getContactLabels(1);
expect(axiosMock.get).toHaveBeenCalledWith('/api/v1/contacts/1/labels');
expect(context.axiosMock.get).toHaveBeenCalledWith(
'/api/v1/contacts/1/labels'
);
});
it('#updateContactLabels', () => {
const labels = ['support-query'];
contactAPI.updateContactLabels(1, labels);
expect(axiosMock.post).toHaveBeenCalledWith('/api/v1/contacts/1/labels', {
labels,
});
expect(context.axiosMock.post).toHaveBeenCalledWith(
'/api/v1/contacts/1/labels',
{
labels,
}
);
});
it('#search', () => {
contactAPI.search('leads', 1, 'date', 'customer-support');
expect(axiosMock.get).toHaveBeenCalledWith(
expect(context.axiosMock.get).toHaveBeenCalledWith(
'/api/v1/contacts/search?include_contact_inboxes=false&page=1&sort=date&q=leads&labels[]=customer-support'
);
});
it('#destroyCustomAttributes', () => {
contactAPI.destroyCustomAttributes(1, ['cloudCustomer']);
expect(axiosMock.post).toHaveBeenCalledWith(
expect(context.axiosMock.post).toHaveBeenCalledWith(
'/api/v1/contacts/1/destroy_custom_attributes',
{
custom_attributes: ['cloudCustomer'],
@@ -85,7 +75,7 @@ describe('#ContactsAPI', () => {
it('#importContacts', () => {
const file = 'file';
contactAPI.importContacts(file);
expect(axiosMock.post).toHaveBeenCalledWith(
expect(context.axiosMock.post).toHaveBeenCalledWith(
'/api/v1/contacts/import',
expect.any(FormData),
{
@@ -106,7 +96,7 @@ describe('#ContactsAPI', () => {
],
};
contactAPI.filter(1, 'name', queryPayload);
expect(axiosMock.post).toHaveBeenCalledWith(
expect(context.axiosMock.post).toHaveBeenCalledWith(
'/api/v1/contacts/filter?include_contact_inboxes=false&page=1&sort=name',
queryPayload
);
@@ -114,7 +104,7 @@ describe('#ContactsAPI', () => {
it('#destroyAvatar', () => {
contactAPI.destroyAvatar(1);
expect(axiosMock.delete).toHaveBeenCalledWith(
expect(context.axiosMock.delete).toHaveBeenCalledWith(
'/api/v1/contacts/1/avatar'
);
});
@@ -1,5 +1,6 @@
import conversationsAPI from '../conversations';
import ApiClient from '../ApiClient';
import describeWithAPIMock from './apiSpecHelper';
describe('#ConversationApi', () => {
it('creates correct instance', () => {
@@ -13,26 +14,10 @@ describe('#ConversationApi', () => {
expect(conversationsAPI).toHaveProperty('updateLabels');
});
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
window.axios = axiosMock;
});
afterEach(() => {
window.axios = originalAxios;
});
describeWithAPIMock('API calls', context => {
it('#getLabels', () => {
conversationsAPI.getLabels(1);
expect(axiosMock.get).toHaveBeenCalledWith(
expect(context.axiosMock.get).toHaveBeenCalledWith(
'/api/v1/conversations/1/labels'
);
});
@@ -40,7 +25,7 @@ describe('#ConversationApi', () => {
it('#updateLabels', () => {
const labels = ['support-query'];
conversationsAPI.updateLabels(1, labels);
expect(axiosMock.post).toHaveBeenCalledWith(
expect(context.axiosMock.post).toHaveBeenCalledWith(
'/api/v1/conversations/1/labels',
{
labels,
@@ -1,5 +1,6 @@
import csatReportsAPI from '../csatReports';
import ApiClient from '../ApiClient';
import describeWithAPIMock from './apiSpecHelper';
describe('#Reports API', () => {
it('creates correct instance', () => {
@@ -8,26 +9,10 @@ describe('#Reports API', () => {
expect(csatReportsAPI).toHaveProperty('get');
expect(csatReportsAPI).toHaveProperty('getMetrics');
});
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
window.axios = axiosMock;
});
afterEach(() => {
window.axios = originalAxios;
});
describeWithAPIMock('API calls', context => {
it('#get', () => {
csatReportsAPI.get({ page: 1, from: 1622485800, to: 1623695400 });
expect(axiosMock.get).toHaveBeenCalledWith(
expect(context.axiosMock.get).toHaveBeenCalledWith(
'/api/v1/csat_survey_responses',
{
params: {
@@ -41,7 +26,7 @@ describe('#Reports API', () => {
});
it('#getMetrics', () => {
csatReportsAPI.getMetrics({ from: 1622485800, to: 1623695400 });
expect(axiosMock.get).toHaveBeenCalledWith(
expect(context.axiosMock.get).toHaveBeenCalledWith(
'/api/v1/csat_survey_responses/metrics',
{
params: { since: 1622485800, until: 1623695400 },
@@ -54,7 +39,7 @@ describe('#Reports API', () => {
to: 1623695400,
user_ids: 1,
});
expect(axiosMock.get).toHaveBeenCalledWith(
expect(context.axiosMock.get).toHaveBeenCalledWith(
'/api/v1/csat_survey_responses/download',
{
params: {
@@ -1,5 +1,6 @@
import conversationAPI from '../../inbox/conversation';
import ApiClient from '../../ApiClient';
import describeWithAPIMock from '../apiSpecHelper';
describe('#ConversationAPI', () => {
it('creates correct instance', () => {
@@ -21,23 +22,7 @@ describe('#ConversationAPI', () => {
expect(conversationAPI).toHaveProperty('filter');
});
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
window.axios = axiosMock;
});
afterEach(() => {
window.axios = originalAxios;
});
describeWithAPIMock('API calls', context => {
it('#get conversations', () => {
conversationAPI.get({
inboxId: 1,
@@ -47,16 +32,19 @@ describe('#ConversationAPI', () => {
labels: [],
teamId: 1,
});
expect(axiosMock.get).toHaveBeenCalledWith('/api/v1/conversations', {
params: {
inbox_id: 1,
team_id: 1,
status: 'open',
assignee_type: 'me',
page: 1,
labels: [],
},
});
expect(context.axiosMock.get).toHaveBeenCalledWith(
'/api/v1/conversations',
{
params: {
inbox_id: 1,
team_id: 1,
status: 'open',
assignee_type: 'me',
page: 1,
labels: [],
},
}
);
});
it('#search', () => {
@@ -65,7 +53,7 @@ describe('#ConversationAPI', () => {
page: 1,
});
expect(axiosMock.get).toHaveBeenCalledWith(
expect(context.axiosMock.get).toHaveBeenCalledWith(
'/api/v1/conversations/search',
{
params: {
@@ -78,7 +66,7 @@ describe('#ConversationAPI', () => {
it('#toggleStatus', () => {
conversationAPI.toggleStatus({ conversationId: 12, status: 'online' });
expect(axiosMock.post).toHaveBeenCalledWith(
expect(context.axiosMock.post).toHaveBeenCalledWith(
`/api/v1/conversations/12/toggle_status`,
{
status: 'online',
@@ -89,7 +77,7 @@ describe('#ConversationAPI', () => {
it('#assignAgent', () => {
conversationAPI.assignAgent({ conversationId: 12, agentId: 34 });
expect(axiosMock.post).toHaveBeenCalledWith(
expect(context.axiosMock.post).toHaveBeenCalledWith(
`/api/v1/conversations/12/assignments?assignee_id=34`,
{}
);
@@ -97,7 +85,7 @@ describe('#ConversationAPI', () => {
it('#assignTeam', () => {
conversationAPI.assignTeam({ conversationId: 12, teamId: 1 });
expect(axiosMock.post).toHaveBeenCalledWith(
expect(context.axiosMock.post).toHaveBeenCalledWith(
`/api/v1/conversations/12/assignments`,
{
team_id: 1,
@@ -107,7 +95,7 @@ describe('#ConversationAPI', () => {
it('#markMessageRead', () => {
conversationAPI.markMessageRead({ id: 12 });
expect(axiosMock.post).toHaveBeenCalledWith(
expect(context.axiosMock.post).toHaveBeenCalledWith(
`/api/v1/conversations/12/update_last_seen`
);
});
@@ -117,7 +105,7 @@ describe('#ConversationAPI', () => {
conversationId: 12,
status: 'typing_on',
});
expect(axiosMock.post).toHaveBeenCalledWith(
expect(context.axiosMock.post).toHaveBeenCalledWith(
`/api/v1/conversations/12/toggle_typing_status`,
{
typing_status: 'typing_on',
@@ -127,14 +115,14 @@ describe('#ConversationAPI', () => {
it('#mute', () => {
conversationAPI.mute(45);
expect(axiosMock.post).toHaveBeenCalledWith(
expect(context.axiosMock.post).toHaveBeenCalledWith(
'/api/v1/conversations/45/mute'
);
});
it('#unmute', () => {
conversationAPI.unmute(45);
expect(axiosMock.post).toHaveBeenCalledWith(
expect(context.axiosMock.post).toHaveBeenCalledWith(
'/api/v1/conversations/45/unmute'
);
});
@@ -147,15 +135,18 @@ describe('#ConversationAPI', () => {
labels: [],
teamId: 1,
});
expect(axiosMock.get).toHaveBeenCalledWith('/api/v1/conversations/meta', {
params: {
inbox_id: 1,
team_id: 1,
status: 'open',
assignee_type: 'me',
labels: [],
},
});
expect(context.axiosMock.get).toHaveBeenCalledWith(
'/api/v1/conversations/meta',
{
params: {
inbox_id: 1,
team_id: 1,
status: 'open',
assignee_type: 'me',
labels: [],
},
}
);
});
it('#sendEmailTranscript', () => {
@@ -163,7 +154,7 @@ describe('#ConversationAPI', () => {
conversationId: 45,
email: 'john@acme.inc',
});
expect(axiosMock.post).toHaveBeenCalledWith(
expect(context.axiosMock.post).toHaveBeenCalledWith(
'/api/v1/conversations/45/transcript',
{
email: 'john@acme.inc',
@@ -176,7 +167,7 @@ describe('#ConversationAPI', () => {
conversationId: 45,
customAttributes: { order_d: '1001' },
});
expect(axiosMock.post).toHaveBeenCalledWith(
expect(context.axiosMock.post).toHaveBeenCalledWith(
'/api/v1/conversations/45/custom_attributes',
{
custom_attributes: { order_d: '1001' },
@@ -211,7 +202,9 @@ describe('#ConversationAPI', () => {
},
};
conversationAPI.filter(payload);
expect(axiosMock.post).toHaveBeenCalledWith(
expect(
context.axiosMock.post
).toHaveBeenCalledWith(
'/api/v1/conversations/filter',
payload.queryData,
{ params: { page: payload.page } }
@@ -220,7 +213,7 @@ describe('#ConversationAPI', () => {
it('#getAllAttachments', () => {
conversationAPI.getAllAttachments(1);
expect(axiosMock.get).toHaveBeenCalledWith(
expect(context.axiosMock.get).toHaveBeenCalledWith(
'/api/v1/conversations/1/attachments'
);
});
@@ -1,5 +1,6 @@
import messageAPI, { buildCreatePayload } from '../../inbox/message';
import ApiClient from '../../ApiClient';
import describeWithAPIMock from '../apiSpecHelper';
describe('#ConversationAPI', () => {
it('creates correct instance', () => {
@@ -12,29 +13,13 @@ describe('#ConversationAPI', () => {
expect(messageAPI).toHaveProperty('getPreviousMessages');
});
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
window.axios = axiosMock;
});
afterEach(() => {
window.axios = originalAxios;
});
describeWithAPIMock('API calls', context => {
it('#getPreviousMessages', () => {
messageAPI.getPreviousMessages({
conversationId: 12,
before: 4573,
});
expect(axiosMock.get).toHaveBeenCalledWith(
expect(context.axiosMock.get).toHaveBeenCalledWith(
`/api/v1/conversations/12/messages`,
{
params: {
@@ -50,6 +35,7 @@ describe('#ConversationAPI', () => {
message: 'test content',
echoId: 12,
isPrivate: true,
files: [new Blob(['test-content'], { type: 'application/pdf' })],
});
expect(formPayload).toBeInstanceOf(FormData);
@@ -72,10 +58,8 @@ describe('#ConversationAPI', () => {
private: false,
echo_id: 12,
content_attributes: { in_reply_to: 12 },
cc_emails: '',
bcc_emails: '',
to_emails: '',
template_params: undefined,
cc_emails: '',
});
});
});
@@ -1,5 +1,6 @@
import inboxesAPI from '../inboxes';
import ApiClient from '../ApiClient';
import describeWithAPIMock from './apiSpecHelper';
describe('#InboxesAPI', () => {
it('creates correct instance', () => {
@@ -13,32 +14,19 @@ describe('#InboxesAPI', () => {
expect(inboxesAPI).toHaveProperty('getAgentBot');
expect(inboxesAPI).toHaveProperty('setAgentBot');
});
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
window.axios = axiosMock;
});
afterEach(() => {
window.axios = originalAxios;
});
describeWithAPIMock('API calls', context => {
it('#getCampaigns', () => {
inboxesAPI.getCampaigns(2);
expect(axiosMock.get).toHaveBeenCalledWith('/api/v1/inboxes/2/campaigns');
expect(context.axiosMock.get).toHaveBeenCalledWith(
'/api/v1/inboxes/2/campaigns'
);
});
it('#deleteInboxAvatar', () => {
inboxesAPI.deleteInboxAvatar(2);
expect(axiosMock.delete).toHaveBeenCalledWith('/api/v1/inboxes/2/avatar');
expect(context.axiosMock.delete).toHaveBeenCalledWith(
'/api/v1/inboxes/2/avatar'
);
});
});
});
@@ -1,5 +1,6 @@
import integrationAPI from '../integrations';
import ApiClient from '../ApiClient';
import describeWithAPIMock from './apiSpecHelper';
describe('#integrationAPI', () => {
it('creates correct instance', () => {
@@ -15,27 +16,11 @@ describe('#integrationAPI', () => {
expect(integrationAPI).toHaveProperty('listAllSlackChannels');
expect(integrationAPI).toHaveProperty('deleteHook');
});
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
window.axios = axiosMock;
});
afterEach(() => {
window.axios = originalAxios;
});
describeWithAPIMock('API calls', context => {
it('#connectSlack', () => {
const code = 'SDNFJNSDFNDSJN';
integrationAPI.connectSlack(code);
expect(axiosMock.post).toHaveBeenCalledWith(
expect(context.axiosMock.post).toHaveBeenCalledWith(
'/api/v1/integrations/slack',
{
code,
@@ -46,7 +31,7 @@ describe('#integrationAPI', () => {
it('#updateSlack', () => {
const updateObj = { referenceId: 'SDFSDGSVE' };
integrationAPI.updateSlack(updateObj);
expect(axiosMock.patch).toHaveBeenCalledWith(
expect(context.axiosMock.patch).toHaveBeenCalledWith(
'/api/v1/integrations/slack',
{
reference_id: updateObj.referenceId,
@@ -56,14 +41,16 @@ describe('#integrationAPI', () => {
it('#listAllSlackChannels', () => {
integrationAPI.listAllSlackChannels();
expect(axiosMock.get).toHaveBeenCalledWith(
expect(context.axiosMock.get).toHaveBeenCalledWith(
'/api/v1/integrations/slack/list_all_channels'
);
});
it('#delete', () => {
integrationAPI.delete(2);
expect(axiosMock.delete).toHaveBeenCalledWith('/api/v1/integrations/2');
expect(context.axiosMock.delete).toHaveBeenCalledWith(
'/api/v1/integrations/2'
);
});
it('#createHook', () => {
@@ -72,7 +59,7 @@ describe('#integrationAPI', () => {
settings: { api_key: 'SDFSDGSVE' },
};
integrationAPI.createHook(hookData);
expect(axiosMock.post).toHaveBeenCalledWith(
expect(context.axiosMock.post).toHaveBeenCalledWith(
'/api/v1/integrations/hooks',
hookData
);
@@ -80,7 +67,7 @@ describe('#integrationAPI', () => {
it('#deleteHook', () => {
integrationAPI.deleteHook(2);
expect(axiosMock.delete).toHaveBeenCalledWith(
expect(context.axiosMock.delete).toHaveBeenCalledWith(
'/api/v1/integrations/hooks/2'
);
});
@@ -1,5 +1,6 @@
import DyteAPIClient from '../../integrations/dyte';
import ApiClient from '../../ApiClient';
import describeWithAPIMock from '../apiSpecHelper';
describe('#accountAPI', () => {
it('creates correct instance', () => {
@@ -8,26 +9,10 @@ describe('#accountAPI', () => {
expect(DyteAPIClient).toHaveProperty('addParticipantToMeeting');
});
describe('createAMeeting', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
window.axios = axiosMock;
});
afterEach(() => {
window.axios = originalAxios;
});
describeWithAPIMock('createAMeeting', context => {
it('creates a valid request', () => {
DyteAPIClient.createAMeeting(1);
expect(axiosMock.post).toHaveBeenCalledWith(
expect(context.axiosMock.post).toHaveBeenCalledWith(
'/api/v1/integrations/dyte/create_a_meeting',
{
conversation_id: 1,
@@ -36,26 +21,10 @@ describe('#accountAPI', () => {
});
});
describe('addParticipantToMeeting', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
window.axios = axiosMock;
});
afterEach(() => {
window.axios = originalAxios;
});
describeWithAPIMock('addParticipantToMeeting', context => {
it('creates a valid request', () => {
DyteAPIClient.addParticipantToMeeting(1);
expect(axiosMock.post).toHaveBeenCalledWith(
expect(context.axiosMock.post).toHaveBeenCalledWith(
'/api/v1/integrations/dyte/add_participant_to_meeting',
{
message_id: 1,
@@ -1,5 +1,6 @@
import notificationsAPI from '../notifications';
import ApiClient from '../ApiClient';
import describeWithAPIMock from './apiSpecHelper';
describe('#NotificationAPI', () => {
it('creates correct instance', () => {
@@ -10,47 +11,31 @@ describe('#NotificationAPI', () => {
expect(notificationsAPI).toHaveProperty('read');
expect(notificationsAPI).toHaveProperty('readAll');
});
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
window.axios = axiosMock;
});
afterEach(() => {
window.axios = originalAxios;
});
describeWithAPIMock('API calls', context => {
it('#get', () => {
notificationsAPI.get(1);
expect(axiosMock.get).toHaveBeenCalledWith(
expect(context.axiosMock.get).toHaveBeenCalledWith(
'/api/v1/notifications?page=1'
);
});
it('#getNotifications', () => {
notificationsAPI.getNotifications(1);
expect(axiosMock.get).toHaveBeenCalledWith(
expect(context.axiosMock.get).toHaveBeenCalledWith(
'/api/v1/notifications/1/notifications'
);
});
it('#getUnreadCount', () => {
notificationsAPI.getUnreadCount();
expect(axiosMock.get).toHaveBeenCalledWith(
expect(context.axiosMock.get).toHaveBeenCalledWith(
'/api/v1/notifications/unread_count'
);
});
it('#read', () => {
notificationsAPI.read(48670, 'Conversation');
expect(axiosMock.post).toHaveBeenCalledWith(
expect(context.axiosMock.post).toHaveBeenCalledWith(
'/api/v1/notifications/read_all',
{
primary_actor_id: 'Conversation',
@@ -61,7 +46,7 @@ describe('#NotificationAPI', () => {
it('#readAll', () => {
notificationsAPI.readAll();
expect(axiosMock.post).toHaveBeenCalledWith(
expect(context.axiosMock.post).toHaveBeenCalledWith(
'/api/v1/notifications/read_all'
);
});
@@ -1,5 +1,6 @@
import reportsAPI from '../reports';
import ApiClient from '../ApiClient';
import describeWithAPIMock from './apiSpecHelper';
describe('#Reports API', () => {
it('creates correct instance', () => {
@@ -17,30 +18,14 @@ describe('#Reports API', () => {
expect(reportsAPI).toHaveProperty('getInboxReports');
expect(reportsAPI).toHaveProperty('getTeamReports');
});
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
window.axios = axiosMock;
});
afterEach(() => {
window.axios = originalAxios;
});
describeWithAPIMock('API calls', context => {
it('#getAccountReports', () => {
reportsAPI.getReports({
metric: 'conversations_count',
from: 1621103400,
to: 1621621800,
});
expect(axiosMock.get).toHaveBeenCalledWith('/api/v2/reports', {
expect(context.axiosMock.get).toHaveBeenCalledWith('/api/v2/reports', {
params: {
metric: 'conversations_count',
since: 1621103400,
@@ -53,17 +38,20 @@ describe('#Reports API', () => {
it('#getAccountSummary', () => {
reportsAPI.getSummary(1621103400, 1621621800);
expect(axiosMock.get).toHaveBeenCalledWith('/api/v2/reports/summary', {
params: {
business_hours: undefined,
group_by: undefined,
id: undefined,
since: 1621103400,
timezone_offset: -0,
type: 'account',
until: 1621621800,
},
});
expect(context.axiosMock.get).toHaveBeenCalledWith(
'/api/v2/reports/summary',
{
params: {
business_hours: undefined,
group_by: undefined,
id: undefined,
since: 1621103400,
timezone_offset: -0,
type: 'account',
until: 1621621800,
},
}
);
});
it('#getAgentReports', () => {
@@ -72,48 +60,60 @@ describe('#Reports API', () => {
to: 1621621800,
businessHours: true,
});
expect(axiosMock.get).toHaveBeenCalledWith('/api/v2/reports/agents', {
params: {
since: 1621103400,
until: 1621621800,
business_hours: true,
},
});
expect(context.axiosMock.get).toHaveBeenCalledWith(
'/api/v2/reports/agents',
{
params: {
since: 1621103400,
until: 1621621800,
business_hours: true,
},
}
);
});
it('#getLabelReports', () => {
reportsAPI.getLabelReports({ from: 1621103400, to: 1621621800 });
expect(axiosMock.get).toHaveBeenCalledWith('/api/v2/reports/labels', {
params: {
since: 1621103400,
until: 1621621800,
},
});
expect(context.axiosMock.get).toHaveBeenCalledWith(
'/api/v2/reports/labels',
{
params: {
since: 1621103400,
until: 1621621800,
},
}
);
});
it('#getInboxReports', () => {
reportsAPI.getInboxReports({ from: 1621103400, to: 1621621800 });
expect(axiosMock.get).toHaveBeenCalledWith('/api/v2/reports/inboxes', {
params: {
since: 1621103400,
until: 1621621800,
},
});
expect(context.axiosMock.get).toHaveBeenCalledWith(
'/api/v2/reports/inboxes',
{
params: {
since: 1621103400,
until: 1621621800,
},
}
);
});
it('#getTeamReports', () => {
reportsAPI.getTeamReports({ from: 1621103400, to: 1621621800 });
expect(axiosMock.get).toHaveBeenCalledWith('/api/v2/reports/teams', {
params: {
since: 1621103400,
until: 1621621800,
},
});
expect(context.axiosMock.get).toHaveBeenCalledWith(
'/api/v2/reports/teams',
{
params: {
since: 1621103400,
until: 1621621800,
},
}
);
});
it('#getConversationMetric', () => {
reportsAPI.getConversationMetric('account');
expect(axiosMock.get).toHaveBeenCalledWith(
expect(context.axiosMock.get).toHaveBeenCalledWith(
'/api/v2/reports/conversations',
{
params: {
@@ -1,5 +1,6 @@
import teamsAPI from '../teams';
import ApiClient from '../ApiClient';
import describeWithAPIMock from './apiSpecHelper';
describe('#TeamsAPI', () => {
it('creates correct instance', () => {
@@ -13,33 +14,17 @@ describe('#TeamsAPI', () => {
expect(teamsAPI).toHaveProperty('addAgents');
expect(teamsAPI).toHaveProperty('updateAgents');
});
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
window.axios = axiosMock;
});
afterEach(() => {
window.axios = originalAxios;
});
describeWithAPIMock('API calls', context => {
it('#getAgents', () => {
teamsAPI.getAgents({ teamId: 1 });
expect(axiosMock.get).toHaveBeenCalledWith(
expect(context.axiosMock.get).toHaveBeenCalledWith(
'/api/v1/teams/1/team_members'
);
});
it('#addAgents', () => {
teamsAPI.addAgents({ teamId: 1, agentsList: { user_ids: [1, 10, 21] } });
expect(axiosMock.post).toHaveBeenCalledWith(
expect(context.axiosMock.post).toHaveBeenCalledWith(
'/api/v1/teams/1/team_members',
{
user_ids: { user_ids: [1, 10, 21] },
@@ -53,7 +38,7 @@ describe('#TeamsAPI', () => {
teamId: 1,
agentsList,
});
expect(axiosMock.patch).toHaveBeenCalledWith(
expect(context.axiosMock.patch).toHaveBeenCalledWith(
'/api/v1/teams/1/team_members',
{
user_ids: agentsList,
@@ -81,6 +81,11 @@ $conv-header-height: 2.5rem;
$inbox-thumb-size: 3rem;
// Spinner
$spinkit-spinner-color: $color-white !default;
$spinkit-spinner-margin: 0 0 0 1rem !default;
$spinkit-size: 1rem !default;
// Snackbar default
$woot-snackbar-bg: #323232;
$woot-snackbar-button: #ffeb3b;
@@ -149,11 +149,15 @@
}
.multiselect-wrap--small {
.multiselect__tags,
.multiselect__input {
@apply items-center flex;
}
.multiselect__tags,
.multiselect__input,
.multiselect {
@apply bg-white dark:bg-slate-900 text-slate-800 dark:text-slate-100 rounded-[5px] text-sm min-h-[2.5rem];
@apply bg-white dark:bg-slate-900 text-slate-800 dark:text-slate-100 rounded-[5px] text-sm h-10 min-h-[2.5rem];
}
.multiselect__input {
@@ -161,15 +165,11 @@
}
.multiselect__single {
@apply items-center flex m-0 text-sm max-h-[2.375rem] text-slate-800 dark:text-slate-100 bg-white dark:bg-slate-900 py-3 px-0.5;
@apply items-center flex m-0 text-sm max-h-[2.375rem] text-slate-800 dark:text-slate-100 bg-white dark:bg-slate-900 py-1 px-0.5;
}
.multiselect__placeholder {
@apply m-0 py-2 px-0.5;
}
.multiselect__tag {
@apply py-[6px] my-[1px];
@apply m-0 py-1 px-0.5;
}
.multiselect__select {
@@ -181,6 +181,9 @@
@apply bg-transparent;
}
.multiselect__tags-wrap {
@apply flex-shrink-0;
}
}
.multiselect-wrap--medium {
@@ -9,6 +9,7 @@
@import 'shared/assets/stylesheets/border-radius';
@import 'variables';
@import '~spinkit/scss/spinners/7-three-bounce';
@import 'vue-multiselect/dist/vue-multiselect.min.css';
@import '~shared/assets/stylesheets/ionicons';
@@ -34,26 +34,26 @@
}
&.hollow {
@apply border border-woot-500 dark:border-woot-500 text-woot-500 dark:text-woot-500 hover:bg-woot-50 dark:hover:bg-woot-900;
@apply border border-slate-200 dark:border-slate-600 text-woot-700 dark:text-woot-100 hover:bg-woot-50 dark:hover:bg-woot-900;
&.secondary {
@apply text-slate-700 border-slate-200 dark:border-slate-600 dark:text-slate-100 hover:bg-slate-50 dark:hover:bg-slate-700;
}
&.success {
@apply text-green-700 dark:text-green-400 border-green-100 dark:border-green-600 hover:bg-green-50 dark:hover:bg-green-800;
@apply text-green-700 dark:text-green-100 hover:bg-green-50 dark:hover:bg-green-800;
}
&.alert {
@apply text-red-700 dark:text-red-400 border-red-100 dark:border-red-600 hover:bg-red-50 dark:hover:bg-red-800;
@apply text-red-700 dark:text-red-100 hover:bg-red-50 dark:hover:bg-red-800;
}
&.warning {
@apply text-yellow-600 dark:text-yellow-600 border-yellow-600 dark:border-yellow-700 hover:bg-yellow-50 dark:hover:bg-yellow-800;
@apply text-yellow-700 dark:text-yellow-100 hover:bg-yellow-50 dark:hover:bg-yellow-800;
}
&:hover {
@apply bg-woot-75 dark:bg-woot-800 border-slate-100 dark:border-woot-600 dark:text-woot-400;
@apply bg-slate-75 dark:bg-slate-900 border-slate-100 dark:border-slate-700;
&.secondary {
@apply border-slate-100 dark:border-slate-700 text-slate-800 dark:text-slate-100;
@@ -68,7 +68,7 @@
}
&.warning {
@apply border-slate-100 dark:border-slate-700 text-yellow-700 dark:text-yellow-500;
@apply border-slate-100 dark:border-slate-700 text-yellow-700 dark:text-yellow-700;
}
}
}
@@ -152,10 +152,6 @@
&.is-image {
@apply rounded-lg;
.message__mail-head {
@apply px-4 py-2;
}
}
}
@@ -4,6 +4,10 @@
@apply flex items-center justify-center bg-modal dark:bg-modal z-[9990] h-full left-0 fixed top-0 w-full;
}
.modal--close {
@apply absolute right-2 rtl:right-[unset] rtl:left-2 top-2;
}
.page-top-bar {
@apply px-8 pt-9 pb-0;
@@ -31,7 +31,7 @@
</template>
<script>
import EmojiOrIcon from 'shared/components/EmojiOrIcon.vue';
import EmojiOrIcon from 'shared/components/EmojiOrIcon';
export default {
components: {
@@ -10,8 +10,7 @@
<div
class="flex items-center justify-between py-0 px-4"
:class="{
'pb-3 border-b border-slate-75 dark:border-slate-700':
hasAppliedFiltersOrActiveFolders,
'pb-3 border-b border-slate-75 dark:border-slate-700': hasAppliedFiltersOrActiveFolders,
}"
>
<div class="flex max-w-[85%] justify-center items-center">
@@ -25,7 +24,9 @@
v-if="!hasAppliedFiltersOrActiveFolders"
class="p-1 my-0.5 mx-1 rounded-md capitalize bg-slate-50 dark:bg-slate-800 text-xxs text-slate-600 dark:text-slate-300"
>
{{ $t(`CHAT_LIST.CHAT_STATUS_FILTER_ITEMS.${activeStatus}.TEXT`) }}
{{
this.$t(`CHAT_LIST.CHAT_STATUS_FILTER_ITEMS.${activeStatus}.TEXT`)
}}
</span>
</div>
<div class="flex items-center gap-1">
@@ -192,17 +193,17 @@
<script>
import { mapGetters } from 'vuex';
import ConversationAdvancedFilter from './widgets/conversation/ConversationAdvancedFilter.vue';
import ConversationBasicFilter from './widgets/conversation/ConversationBasicFilter.vue';
import ChatTypeTabs from './widgets/ChatTypeTabs.vue';
import ConversationCard from './widgets/conversation/ConversationCard.vue';
import ConversationAdvancedFilter from './widgets/conversation/ConversationAdvancedFilter';
import ConversationBasicFilter from './widgets/conversation/ConversationBasicFilter';
import ChatTypeTabs from './widgets/ChatTypeTabs';
import ConversationCard from './widgets/conversation/ConversationCard';
import timeMixin from '../mixins/time';
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
import conversationMixin from '../mixins/conversations';
import wootConstants from 'dashboard/constants/globals';
import advancedFilterTypes from './widgets/conversation/advancedFilterItems';
import filterQueryGenerator from '../helper/filterQueryGenerator.js';
import AddCustomViews from 'dashboard/routes/dashboard/customviews/AddCustomViews.vue';
import AddCustomViews from 'dashboard/routes/dashboard/customviews/AddCustomViews';
import DeleteCustomViews from 'dashboard/routes/dashboard/customviews/DeleteCustomViews.vue';
import ConversationBulkActions from './widgets/conversation/conversationBulkActions/Index.vue';
import alertMixin from 'shared/mixins/alertMixin';
@@ -641,8 +642,10 @@ export default {
},
handleKeyEvents(e) {
if (hasPressedAltAndJKey(e)) {
const { allConversations, activeConversationIndex } =
this.getKeyboardListenerParams();
const {
allConversations,
activeConversationIndex,
} = this.getKeyboardListenerParams();
if (activeConversationIndex === -1) {
allConversations[0].click();
}
@@ -13,10 +13,10 @@
/>
<div class="flex flex-row justify-end gap-2 py-2 px-0 w-full">
<woot-button variant="clear" @click.prevent="onClose">
{{ $t('CONVERSATION.CUSTOM_SNOOZE.CANCEL') }}
{{ this.$t('CONVERSATION.CUSTOM_SNOOZE.CANCEL') }}
</woot-button>
<woot-button>
{{ $t('CONVERSATION.CUSTOM_SNOOZE.APPLY') }}
{{ this.$t('CONVERSATION.CUSTOM_SNOOZE.APPLY') }}
</woot-button>
</div>
</form>
@@ -1,6 +1,6 @@
<template>
<div class="text--container">
<woot-button size="small" class="button--text" @click="onCopy">
<woot-button size="small" class=" button--text" @click="onCopy">
{{ $t('COMPONENTS.CODE.BUTTON_TEXT') }}
</woot-button>
<woot-button
+8 -26
View File
@@ -4,19 +4,15 @@
v-if="show"
:class="modalClassName"
transition="modal"
@mousedown="handleMouseDown"
@click="onBackDropClick"
>
<div
:class="modalContainerClassName"
@mouse.stop
@mousedown="event => event.stopPropagation()"
>
<div :class="modalContainerClassName" @click.stop>
<woot-button
v-if="showCloseButton"
color-scheme="secondary"
icon="dismiss"
variant="clear"
class="absolute ltr:right-2 rtl:left-2 top-2 z-10"
class="modal--close"
@click="close"
/>
<slot />
@@ -54,11 +50,6 @@ export default {
default: '',
},
},
data() {
return {
mousedDownOnBackdrop: false,
};
},
computed: {
modalContainerClassName() {
let className =
@@ -75,9 +66,9 @@ export default {
'right-aligned': 'right-aligned',
};
return `modal-mask skip-context-menu ${
modalClassNameMap[this.modalType] || ''
}`;
return `modal-mask skip-context-menu ${modalClassNameMap[
this.modalType
] || ''}`;
},
},
mounted() {
@@ -86,22 +77,13 @@ export default {
this.onClose();
}
});
document.body.addEventListener('mouseup', this.onMouseUp);
},
beforeDestroy() {
document.body.removeEventListener('mouseup', this.onMouseUp);
},
methods: {
handleMouseDown() {
this.mousedDownOnBackdrop = true;
},
close() {
this.onClose();
},
onMouseUp() {
if (this.mousedDownOnBackdrop) {
this.mousedDownOnBackdrop = false;
onBackDropClick() {
if (this.closeOnBackdropClick) {
this.onClose();
}
},
@@ -2,8 +2,7 @@
<div
class="ml-0 mr-0 flex pt-0 pr-4 pb-4 pl-0"
:class="{
'pt-4 border-b border-solid border-slate-50 dark:border-slate-700/30':
showBorder,
'pt-4 border-b border-solid border-slate-50 dark:border-slate-700/30': showBorder,
}"
>
<div class="w-[30%] min-w-0 max-w-[30%] pr-12">
@@ -10,7 +10,7 @@
</template>
<script>
import WootSnackbar from './Snackbar.vue';
import WootSnackbar from './Snackbar';
export default {
components: {
@@ -72,8 +72,10 @@ export default {
const { custom_attributes: subscription } = account;
if (!subscription) return EMPTY_SUBSCRIPTION_INFO;
const { subscription_status: status, subscription_ends_on: endsOn } =
subscription;
const {
subscription_status: status,
subscription_ends_on: endsOn,
} = subscription;
return { status, endsOn: new Date(endsOn) };
},
@@ -1,4 +1,4 @@
import semver from 'semver';
const semver = require('semver');
export const hasAnUpdateAvailable = (latestVersion, currentVersion) => {
if (!semver.valid(latestVersion)) {
@@ -13,7 +13,7 @@
</template>
<script>
import Spinner from 'shared/components/Spinner.vue';
import Spinner from 'shared/components/Spinner';
export default {
components: {
@@ -10,7 +10,7 @@
:is-loading="isLoading"
@click="onCmdResolveConversation"
>
{{ $t('CONVERSATION.HEADER.RESOLVE_ACTION') }}
{{ this.$t('CONVERSATION.HEADER.RESOLVE_ACTION') }}
</woot-button>
<woot-button
v-else-if="isResolved"
@@ -21,7 +21,7 @@
:is-loading="isLoading"
@click="onCmdOpenConversation"
>
{{ $t('CONVERSATION.HEADER.REOPEN_ACTION') }}
{{ this.$t('CONVERSATION.HEADER.REOPEN_ACTION') }}
</woot-button>
<woot-button
v-else-if="showOpenButton"
@@ -31,7 +31,7 @@
:is-loading="isLoading"
@click="onCmdOpenConversation"
>
{{ $t('CONVERSATION.HEADER.OPEN_ACTION') }}
{{ this.$t('CONVERSATION.HEADER.OPEN_ACTION') }}
</woot-button>
<woot-button
v-if="showAdditionalActions"
@@ -57,7 +57,7 @@
icon="snooze"
@click="() => openSnoozeModal()"
>
{{ $t('CONVERSATION.RESOLVE_DROPDOWN.SNOOZE_UNTIL') }}
{{ this.$t('CONVERSATION.RESOLVE_DROPDOWN.SNOOZE_UNTIL') }}
</woot-button>
</woot-dropdown-item>
<woot-dropdown-item v-if="!isPending">
@@ -68,7 +68,7 @@
icon="book-clock"
@click="() => toggleStatus(STATUS_TYPE.PENDING)"
>
{{ $t('CONVERSATION.RESOLVE_DROPDOWN.MARK_PENDING') }}
{{ this.$t('CONVERSATION.RESOLVE_DROPDOWN.MARK_PENDING') }}
</woot-button>
</woot-dropdown-item>
</woot-dropdown-menu>
@@ -90,7 +90,7 @@ import { getUnixTime } from 'date-fns';
import { mapGetters } from 'vuex';
import { mixin as clickaway } from 'vue-clickaway';
import alertMixin from 'shared/mixins/alertMixin';
import CustomSnoozeModal from 'dashboard/components/CustomSnoozeModal.vue';
import CustomSnoozeModal from 'dashboard/components/CustomSnoozeModal';
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
import {
hasPressedAltAndEKey,
@@ -49,11 +49,11 @@
import { mapGetters } from 'vuex';
import { mixin as clickaway } from 'vue-clickaway';
import alertMixin from 'shared/mixins/alertMixin';
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
import WootDropdownHeader from 'shared/components/ui/dropdown/DropdownHeader.vue';
import WootDropdownDivider from 'shared/components/ui/dropdown/DropdownDivider.vue';
import AvailabilityStatusBadge from '../widgets/conversation/AvailabilityStatusBadge.vue';
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem';
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu';
import WootDropdownHeader from 'shared/components/ui/dropdown/DropdownHeader';
import WootDropdownDivider from 'shared/components/ui/dropdown/DropdownDivider';
import AvailabilityStatusBadge from '../widgets/conversation/AvailabilityStatusBadge';
import wootConstants from 'dashboard/constants/globals';
const { AVAILABILITY_STATUS_KEYS } = wootConstants;
@@ -11,6 +11,7 @@
@key-shortcut-modal="toggleKeyShortcutModal"
@open-notification-panel="openNotificationPanel"
/>
<secondary-sidebar
v-if="showSecondarySidebar"
:class="sidebarClassName"
@@ -34,8 +35,8 @@ import adminMixin from '../../mixins/isAdmin';
import { getSidebarItems } from './config/default-sidebar';
import alertMixin from 'shared/mixins/alertMixin';
import PrimarySidebar from './sidebarComponents/Primary.vue';
import SecondarySidebar from './sidebarComponents/Secondary.vue';
import PrimarySidebar from './sidebarComponents/Primary';
import SecondarySidebar from './sidebarComponents/Secondary';
import {
hasPressedAltAndCKey,
hasPressedAltAndRKey,
@@ -113,12 +114,7 @@ export default {
if (!isAvailableForTheUser) {
return false;
}
if (
menuItem.alwaysVisibleOnChatwootInstances &&
!this.isACustomBrandedInstance
) {
return true;
}
if (menuItem.featureFlag) {
return this.isFeatureEnabledonAccount(
this.accountId,
@@ -42,7 +42,6 @@ const primaryMenuItems = accountId => [
key: 'helpcenter',
label: 'HELP_CENTER.TITLE',
featureFlag: FEATURE_FLAGS.HELP_CENTER,
alwaysVisibleOnChatwootInstances: true,
toState: frontendURL(`accounts/${accountId}/portals`),
toStateName: 'default_portal_articles',
roles: ['administrator'],
@@ -34,8 +34,8 @@
<woot-submit-button
:disabled="
$v.accountName.$invalid ||
$v.accountName.$invalid ||
uiFlags.isCreating
$v.accountName.$invalid ||
uiFlags.isCreating
"
:button-text="$t('CREATE_ACCOUNT.FORM.SUBMIT')"
:loading="uiFlags.isCreating"
@@ -16,7 +16,7 @@
</template>
<script>
import { mapGetters } from 'vuex';
import Thumbnail from '../../widgets/Thumbnail.vue';
import Thumbnail from '../../widgets/Thumbnail';
export default {
components: {
@@ -3,8 +3,7 @@
<button
class="text-slate-600 dark:text-slate-100 w-10 h-10 my-2 flex items-center justify-center rounded-lg hover:bg-slate-25 dark:hover:bg-slate-700 dark:hover:text-slate-100 hover:text-slate-600 relative"
:class="{
'bg-woot-50 dark:bg-slate-800 text-woot-500 hover:bg-woot-50':
isNotificationPanelActive,
'bg-woot-50 dark:bg-slate-800 text-woot-500 hover:bg-woot-50': isNotificationPanelActive,
}"
@click="openNotificationPanel"
>
@@ -109,9 +109,9 @@
import { mixin as clickaway } from 'vue-clickaway';
import { mapGetters } from 'vuex';
import Auth from '../../../api/auth';
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
import AvailabilityStatus from 'dashboard/components/layout/AvailabilityStatus.vue';
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem';
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu';
import AvailabilityStatus from 'dashboard/components/layout/AvailabilityStatus';
export default {
components: {
@@ -39,11 +39,11 @@
</div>
</template>
<script>
import Logo from './Logo.vue';
import PrimaryNavItem from './PrimaryNavItem.vue';
import OptionsMenu from './OptionsMenu.vue';
import AgentDetails from './AgentDetails.vue';
import NotificationBell from './NotificationBell.vue';
import Logo from './Logo';
import PrimaryNavItem from './PrimaryNavItem';
import OptionsMenu from './OptionsMenu';
import AgentDetails from './AgentDetails';
import NotificationBell from './NotificationBell';
import wootConstants from 'dashboard/constants/globals';
import { frontendURL } from 'dashboard/helper/URLHelper';
import { ACCOUNT_EVENTS } from '../../../helper/AnalyticsHelper/events';
@@ -9,8 +9,7 @@
class="font-medium h-7 my-1 hover:bg-slate-25 hover:text-bg-50 flex items-center px-2 rounded-md dark:hover:bg-slate-800"
:class="{
'bg-woot-25 dark:bg-slate-800': isActive,
'text-ellipsis overflow-hidden whitespace-nowrap max-w-full':
shouldTruncate,
'text-ellipsis overflow-hidden whitespace-nowrap max-w-full': shouldTruncate,
}"
@click="navigate"
>
@@ -45,8 +44,7 @@
class="text-sm text-slate-700 dark:text-slate-100"
:class="{
'text-woot-500 dark:text-woot-500': isActive,
'text-ellipsis overflow-hidden whitespace-nowrap max-w-full':
shouldTruncate,
'text-ellipsis overflow-hidden whitespace-nowrap max-w-full': shouldTruncate,
}"
>
{{ label }}
@@ -35,8 +35,7 @@
:class="{
'text-slate-300 dark:text-slate-600': isCountZero && !isActiveView,
'text-slate-600 dark:text-slate-50': !isCountZero && !isActiveView,
'bg-woot-75 dark:bg-woot-200 text-woot-600 dark:text-woot-600':
isActiveView,
'bg-woot-75 dark:bg-woot-200 text-woot-600 dark:text-woot-600': isActiveView,
'bg-slate-50 dark:bg-slate-700': !isActiveView,
}"
>
@@ -98,7 +97,7 @@ import {
getInboxWarningIconClass,
} from 'dashboard/helper/inbox';
import SecondaryChildNavItem from './SecondaryChildNavItem.vue';
import SecondaryChildNavItem from './SecondaryChildNavItem';
import {
isOnMentionsView,
isOnUnattendedView,
@@ -53,24 +53,16 @@ export default {
const createdTimeDiff = Date.now() - this.createdAtTimestamp * 1000;
const isBeforeAMonth = createdTimeDiff > DAY_IN_MILLI_SECONDS * 30;
return !isBeforeAMonth
? `${this.$t('CHAT_LIST.CHAT_TIME_STAMP.CREATED.LATEST')} ${
this.createdAtTimeAgo
}`
: `${this.$t(
'CHAT_LIST.CHAT_TIME_STAMP.CREATED.OLDEST'
)} ${this.dateFormat(this.createdAtTimestamp)}`;
? `Created ${this.createdAtTimeAgo}`
: `Created at: ${this.dateFormat(this.createdAtTimestamp)}`;
},
lastActivity() {
const lastActivityTimeDiff =
Date.now() - this.lastActivityTimestamp * 1000;
const isNotActive = lastActivityTimeDiff > DAY_IN_MILLI_SECONDS * 30;
return !isNotActive
? `${this.$t('CHAT_LIST.CHAT_TIME_STAMP.LAST_ACTIVITY.ACTIVE')} ${
this.lastActivityAtTimeAgo
}`
: `${this.$t(
'CHAT_LIST.CHAT_TIME_STAMP.LAST_ACTIVITY.NOT_ACTIVE'
)} ${this.dateFormat(this.lastActivityTimestamp)}`;
? `Last activity ${this.lastActivityAtTimeAgo}`
: `Last activity: ${this.dateFormat(this.lastActivityTimestamp)}`;
},
tooltipText() {
return `${this.createdAt}
@@ -28,8 +28,8 @@
</button>
</template>
<script>
import Spinner from 'shared/components/Spinner.vue';
import EmojiOrIcon from 'shared/components/EmojiOrIcon.vue';
import Spinner from 'shared/components/Spinner';
import EmojiOrIcon from 'shared/components/EmojiOrIcon';
export default {
name: 'WootButton',
@@ -1,6 +1,6 @@
<template>
<div class="animation-container margin-top-1">
<div class="ai-typing--wrap">
<div class="ai-typing--wrap ">
<fluent-icon icon="wand" size="14" class="ai-typing--icon" />
<label>
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.ASSISTANCE_MODAL.AI_WRITING') }}
@@ -1,9 +1,11 @@
<template>
<div class="preview-item__wrap flex overflow-auto max-h-[12.5rem]">
<div
class="preview-item__wrap flex flex-col overflow-auto mt-4 max-h-[12.5rem]"
>
<div
v-for="(attachment, index) in attachments"
:key="attachment.id"
class="preview-item flex items-center p-1 bg-slate-50 dark:bg-slate-800 gap-1 rounded-md w-[15rem] mb-1"
class="preview-item flex p-1 bg-slate-50 dark:bg-slate-800 rounded-md w-[15rem] mb-1"
>
<div class="max-w-[4rem] flex-shrink-0 w-6 flex items-center">
<img
@@ -15,7 +17,7 @@
📄
</span>
</div>
<div class="max-w-[60%] min-w-[50%] overflow-hidden text-ellipsis">
<div class="max-w-[60%] min-w-[50%] overflow-hidden text-ellipsis ml-2">
<span
class="h-4 overflow-hidden text-sm font-medium text-ellipsis whitespace-nowrap"
>
@@ -108,7 +108,7 @@
<script>
import AutomationActionTeamMessageInput from './AutomationActionTeamMessageInput.vue';
import AutomationActionFileInput from './AutomationFileInput.vue';
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor';
export default {
components: {
AutomationActionTeamMessageInput,
@@ -26,7 +26,7 @@
</template>
<script>
import Spinner from 'shared/components/Spinner.vue';
import Spinner from 'shared/components/Spinner';
import alertMixin from 'shared/mixins/alertMixin';
export default {
components: {
@@ -7,7 +7,7 @@
/>
</template>
<script>
import ChannelSelector from '../ChannelSelector.vue';
import ChannelSelector from '../ChannelSelector';
export default {
components: { ChannelSelector },
props: {
@@ -12,7 +12,7 @@
/>
<iframe
v-if="configItem.type === 'frame' && configItem.url"
:id="getFrameId(index)"
:id="`dashboard-app--frame-${index}`"
:src="configItem.url"
@load="() => onIframeLoad(index)"
/>
@@ -21,7 +21,7 @@
</template>
<script>
import LoadingState from 'dashboard/components/widgets/LoadingState.vue';
import LoadingState from 'dashboard/components/widgets/LoadingState';
export default {
components: {
LoadingState,
@@ -39,10 +39,6 @@ export default {
type: Boolean,
default: false,
},
position: {
type: Number,
required: true,
},
},
data() {
return {
@@ -86,14 +82,10 @@ export default {
};
},
methods: {
getFrameId(index) {
return `dashboard-app--frame-${this.position}-${index}`;
},
onIframeLoad(index) {
// A possible alternative is to use ref instead of document.getElementById
// However, when ref is used together with v-for, the ref you get will be
// an array containing the child components mirroring the data source.
const frameElement = document.getElementById(this.getFrameId(index));
const frameElement = document.getElementById(
`dashboard-app--frame-${index}`
);
const eventData = { event: 'appContext', data: this.dashboardAppContext };
frameElement.contentWindow.postMessage(JSON.stringify(eventData), '*');
this.iframeLoading = false;
@@ -1,4 +1,3 @@
<!-- eslint-disable vue/no-mutating-props -->
<template>
<div>
<div
@@ -256,9 +255,7 @@ export default {
value === 'conversation_attribute' ||
value === 'contact_attribute'
) {
// eslint-disable-next-line vue/no-mutating-props
this.value.custom_attribute_type = this.customAttributeType;
// eslint-disable-next-line vue/no-mutating-props
} else this.value.custom_attribute_type = '';
},
immediate: true,
@@ -30,9 +30,9 @@
</template>
<script>
import AddLabel from 'shared/components/ui/dropdown/AddLabel.vue';
import AddLabel from 'shared/components/ui/dropdown/AddLabel';
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
import LabelDropdown from 'shared/components/ui/label/LabelDropdown.vue';
import LabelDropdown from 'shared/components/ui/label/LabelDropdown';
import { mixin as clickaway } from 'vue-clickaway';
import adminMixin from 'dashboard/mixins/isAdmin';
import {
@@ -42,7 +42,7 @@
* Badge - Chat source indication { fb / telegram }
* Username - Username for avatar
*/
import Avatar from './Avatar.vue';
import Avatar from './Avatar';
import { removeEmoji } from 'shared/helpers/emoji';
export default {
@@ -17,7 +17,7 @@
</div>
</template>
<script>
import Thumbnail from './Thumbnail.vue';
import Thumbnail from './Thumbnail';
export default {
components: {

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