Merge branch 'release/2.0.0'
This commit is contained in:
commit
a7623041bf
@ -7,7 +7,7 @@ defaults: &defaults
|
|||||||
working_directory: ~/build
|
working_directory: ~/build
|
||||||
docker:
|
docker:
|
||||||
# specify the version you desire here
|
# specify the version you desire here
|
||||||
- image: cimg/ruby:3.0.2-node
|
- image: cimg/ruby:3.0.2-browsers
|
||||||
|
|
||||||
# Specify service dependencies here if necessary
|
# Specify service dependencies here if necessary
|
||||||
# CircleCI maintains a library of pre-built images
|
# CircleCI maintains a library of pre-built images
|
||||||
@ -77,6 +77,18 @@ jobs:
|
|||||||
paths:
|
paths:
|
||||||
- cc-test-reporter
|
- cc-test-reporter
|
||||||
|
|
||||||
|
# verify swagger specification
|
||||||
|
- run:
|
||||||
|
name: Verify swagger API specification
|
||||||
|
command: |
|
||||||
|
bundle exec rake swagger:build
|
||||||
|
if [[ `git status swagger/swagger.json --porcelain` ]]
|
||||||
|
then
|
||||||
|
echo "ERROR: The swagger.json file is not in sync with the yaml specification. Run 'rake swagger:build' and commit 'swagger/swagger.json'."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
curl -L https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/5.3.0/openapi-generator-cli-5.3.0.jar > ~/tmp/openapi-generator-cli-5.3.0.jar
|
||||||
|
java -jar ~/tmp/openapi-generator-cli-5.3.0.jar validate -i swagger/swagger.json
|
||||||
# Database setup
|
# Database setup
|
||||||
- run: yarn install --check-files
|
- run: yarn install --check-files
|
||||||
- run: bundle exec rake db:create
|
- run: bundle exec rake db:create
|
||||||
|
|||||||
@ -40,3 +40,7 @@ exclude_patterns:
|
|||||||
- "app/javascript/dashboard/i18n/locale"
|
- "app/javascript/dashboard/i18n/locale"
|
||||||
- "**/*.stories.js"
|
- "**/*.stories.js"
|
||||||
- "stories/"
|
- "stories/"
|
||||||
|
- "app/javascript/dashboard/components/widgets/conversation/advancedFilterItems/index.js"
|
||||||
|
- "app/javascript/shared/constants/countries.js"
|
||||||
|
- "app/javascript/dashboard/components/widgets/conversation/advancedFilterItems/languages.js"
|
||||||
|
- "app/javascript/dashboard/routes/dashboard/contacts/contactFilterItems/index.js"
|
||||||
|
|||||||
@ -57,6 +57,9 @@ SMTP_AUTHENTICATION=
|
|||||||
SMTP_ENABLE_STARTTLS_AUTO=true
|
SMTP_ENABLE_STARTTLS_AUTO=true
|
||||||
# Can be: 'none', 'peer', 'client_once', 'fail_if_no_peer_cert', see http://api.rubyonrails.org/classes/ActionMailer/Base.html
|
# Can be: 'none', 'peer', 'client_once', 'fail_if_no_peer_cert', see http://api.rubyonrails.org/classes/ActionMailer/Base.html
|
||||||
SMTP_OPENSSL_VERIFY_MODE=peer
|
SMTP_OPENSSL_VERIFY_MODE=peer
|
||||||
|
# Comment out the following environment variables if required by your SMTP server
|
||||||
|
# SMTP_TLS=
|
||||||
|
# SMTP_SSL=
|
||||||
|
|
||||||
# Mail Incoming
|
# Mail Incoming
|
||||||
# This is the domain set for the reply emails when conversation continuity is enabled
|
# This is the domain set for the reply emails when conversation continuity is enabled
|
||||||
|
|||||||
@ -28,6 +28,9 @@ module.exports = {
|
|||||||
}],
|
}],
|
||||||
'vue/html-self-closing': 'off',
|
'vue/html-self-closing': 'off',
|
||||||
"vue/no-v-html": 'off',
|
"vue/no-v-html": 'off',
|
||||||
|
'vue/singleline-html-element-content-newline': 'warn',
|
||||||
|
'vue/require-default-prop': 'warn',
|
||||||
|
'vue/require-prop-types': 'warn',
|
||||||
'import/extensions': ['off']
|
'import/extensions': ['off']
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|||||||
5
.husky/pre-commit
Executable file
5
.husky/pre-commit
Executable file
@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
. "$(dirname "$0")/_/husky.sh"
|
||||||
|
npm run eslint
|
||||||
|
bundle exec rubocop -a
|
||||||
|
git add
|
||||||
4
.husky/pre-push
Executable file
4
.husky/pre-push
Executable file
@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
. "$(dirname "$0")/_/husky.sh"
|
||||||
|
|
||||||
|
sh bin/validate_push
|
||||||
@ -57,7 +57,7 @@ Metrics/BlockLength:
|
|||||||
- db/schema.rb
|
- db/schema.rb
|
||||||
Metrics/ModuleLength:
|
Metrics/ModuleLength:
|
||||||
Exclude:
|
Exclude:
|
||||||
- lib/woot_message_seeder.rb
|
- lib/seeders/message_seeder.rb
|
||||||
Rails/ApplicationController:
|
Rails/ApplicationController:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'app/controllers/api/v1/widget/messages_controller.rb'
|
- 'app/controllers/api/v1/widget/messages_controller.rb'
|
||||||
|
|||||||
128
CODE_OF_CONDUCT.md
Normal file
128
CODE_OF_CONDUCT.md
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
|
||||||
|
We as members, contributors, and leaders pledge to make participation in our
|
||||||
|
community a harassment-free experience for everyone, regardless of age, body
|
||||||
|
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||||
|
identity and expression, level of experience, education, socio-economic status,
|
||||||
|
nationality, personal appearance, race, religion, or sexual identity
|
||||||
|
and orientation.
|
||||||
|
|
||||||
|
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||||
|
diverse, inclusive, and healthy community.
|
||||||
|
|
||||||
|
## Our Standards
|
||||||
|
|
||||||
|
Examples of behavior that contributes to a positive environment for our
|
||||||
|
community include:
|
||||||
|
|
||||||
|
* Demonstrating empathy and kindness toward other people
|
||||||
|
* Being respectful of differing opinions, viewpoints, and experiences
|
||||||
|
* Giving and gracefully accepting constructive feedback
|
||||||
|
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||||
|
and learning from the experience
|
||||||
|
* Focusing on what is best not just for us as individuals, but for the
|
||||||
|
overall community
|
||||||
|
|
||||||
|
Examples of unacceptable behavior include:
|
||||||
|
|
||||||
|
* The use of sexualized language or imagery, and sexual attention or
|
||||||
|
advances of any kind
|
||||||
|
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||||
|
* Public or private harassment
|
||||||
|
* Publishing others' private information, such as a physical or email
|
||||||
|
address, without their explicit permission
|
||||||
|
* Other conduct which could reasonably be considered inappropriate in a
|
||||||
|
professional setting
|
||||||
|
|
||||||
|
## Enforcement Responsibilities
|
||||||
|
|
||||||
|
Community leaders are responsible for clarifying and enforcing our standards of
|
||||||
|
acceptable behavior and will take appropriate and fair corrective action in
|
||||||
|
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||||
|
or harmful.
|
||||||
|
|
||||||
|
Community leaders have the right and responsibility to remove, edit, or reject
|
||||||
|
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||||
|
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||||
|
decisions when appropriate.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies within all community spaces, and also applies when
|
||||||
|
an individual is officially representing the community in public spaces.
|
||||||
|
Examples of representing our community include using an official e-mail address,
|
||||||
|
posting via an official social media account, or acting as an appointed
|
||||||
|
representative at an online or offline event.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
|
reported to the community leaders responsible for enforcement at
|
||||||
|
hello@chatwoot.com.
|
||||||
|
All complaints will be reviewed and investigated promptly and fairly.
|
||||||
|
|
||||||
|
All community leaders are obligated to respect the privacy and security of the
|
||||||
|
reporter of any incident.
|
||||||
|
|
||||||
|
## Enforcement Guidelines
|
||||||
|
|
||||||
|
Community leaders will follow these Community Impact Guidelines in determining
|
||||||
|
the consequences for any action they deem in violation of this Code of Conduct:
|
||||||
|
|
||||||
|
### 1. Correction
|
||||||
|
|
||||||
|
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||||
|
unprofessional or unwelcome in the community.
|
||||||
|
|
||||||
|
**Consequence**: A private, written warning from community leaders, providing
|
||||||
|
clarity around the nature of the violation and an explanation of why the
|
||||||
|
behavior was inappropriate. A public apology may be requested.
|
||||||
|
|
||||||
|
### 2. Warning
|
||||||
|
|
||||||
|
**Community Impact**: A violation through a single incident or series
|
||||||
|
of actions.
|
||||||
|
|
||||||
|
**Consequence**: A warning with consequences for continued behavior. No
|
||||||
|
interaction with the people involved, including unsolicited interaction with
|
||||||
|
those enforcing the Code of Conduct, for a specified period of time. This
|
||||||
|
includes avoiding interactions in community spaces as well as external channels
|
||||||
|
like social media. Violating these terms may lead to a temporary or
|
||||||
|
permanent ban.
|
||||||
|
|
||||||
|
### 3. Temporary Ban
|
||||||
|
|
||||||
|
**Community Impact**: A serious violation of community standards, including
|
||||||
|
sustained inappropriate behavior.
|
||||||
|
|
||||||
|
**Consequence**: A temporary ban from any sort of interaction or public
|
||||||
|
communication with the community for a specified period of time. No public or
|
||||||
|
private interaction with the people involved, including unsolicited interaction
|
||||||
|
with those enforcing the Code of Conduct, is allowed during this period.
|
||||||
|
Violating these terms may lead to a permanent ban.
|
||||||
|
|
||||||
|
### 4. Permanent Ban
|
||||||
|
|
||||||
|
**Community Impact**: Demonstrating a pattern of violation of community
|
||||||
|
standards, including sustained inappropriate behavior, harassment of an
|
||||||
|
individual, or aggression toward or disparagement of classes of individuals.
|
||||||
|
|
||||||
|
**Consequence**: A permanent ban from any sort of public interaction within
|
||||||
|
the community.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||||
|
version 2.0, available at
|
||||||
|
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||||
|
|
||||||
|
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||||
|
enforcement ladder](https://github.com/mozilla/diversity).
|
||||||
|
|
||||||
|
[homepage]: https://www.contributor-covenant.org
|
||||||
|
|
||||||
|
For answers to common questions about this code of conduct, see the FAQ at
|
||||||
|
https://www.contributor-covenant.org/faq. Translations are available at
|
||||||
|
https://www.contributor-covenant.org/translations.
|
||||||
5
CONTRIBUTING.md
Normal file
5
CONTRIBUTING.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# Contributing to Chatwoot
|
||||||
|
|
||||||
|
Thanks for taking the time to contribute! :tada::+1:
|
||||||
|
|
||||||
|
Please refer to our [Contributing Guide](https://www.chatwoot.com/docs/contributing-guide) for detailed instructions.
|
||||||
8
LICENSE
8
LICENSE
@ -1,7 +1,11 @@
|
|||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2017-2021 Chatwoot Inc.
|
Copyright (c) 2017-2021 Chatwoot Inc.
|
||||||
|
|
||||||
|
Portions of this software are licensed as follows:
|
||||||
|
|
||||||
|
* All content that resides under the "enterprise/" directory of this repository, if that directory exists, is licensed under the license defined in "enterprise/LICENSE".
|
||||||
|
* All third party components incorporated into the Chatwoot Software are licensed under the original license provided by the owner of the applicable component.
|
||||||
|
* Content outside of the above mentioned directories or restrictions above is available under the "MIT Expat" license as defined below.
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
in the Software without restriction, including without limitation the rights
|
in the Software without restriction, including without limitation the rights
|
||||||
|
|||||||
16
README.md
16
README.md
@ -6,7 +6,10 @@
|
|||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://heroku.com/deploy?template=https://github.com/chatwoot/chatwoot/tree/master" alt="Deploy to Heroku">
|
<a href="https://heroku.com/deploy?template=https://github.com/chatwoot/chatwoot/tree/master" alt="Deploy to Heroku">
|
||||||
<img alt="Deploy" src="https://www.herokucdn.com/deploy/button.svg"/>
|
<img width="150" alt="Deploy" src="https://www.herokucdn.com/deploy/button.svg"/>
|
||||||
|
</a>
|
||||||
|
<a href="https://marketplace.digitalocean.com/apps/chatwoot?refcode=f2238426a2a8" alt="Deploy to DigitalOcean">
|
||||||
|
<img width="200" alt="Deploy to DO" src="https://www.deploytodo.com/do-btn-blue.svg"/>
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@ -81,9 +84,18 @@ Deploying Chatwoot to Heroku is a breeze. It's as simple as clicking this button
|
|||||||
|
|
||||||
Follow this [link](https://www.chatwoot.com/docs/environment-variables) to understand setting the correct environment variables for the app to work with all the features. There might be breakages if you do not set the relevant environment variables.
|
Follow this [link](https://www.chatwoot.com/docs/environment-variables) to understand setting the correct environment variables for the app to work with all the features. There might be breakages if you do not set the relevant environment variables.
|
||||||
|
|
||||||
|
|
||||||
|
### DigitalOcean 1-Click Kubernetes deployment
|
||||||
|
|
||||||
|
Chatwoot now supports 1-Click deployment to DigitalOcean as a kubernetes app.
|
||||||
|
|
||||||
|
<a href="https://marketplace.digitalocean.com/apps/chatwoot?refcode=f2238426a2a8" alt="Deploy to DigitalOcean">
|
||||||
|
<img width="200" alt="Deploy to DO" src="https://www.deploytodo.com/do-btn-blue.svg"/>
|
||||||
|
</a>
|
||||||
|
|
||||||
### Other deployment options
|
### Other deployment options
|
||||||
|
|
||||||
Please follow [deployment architecture guide](https://www.chatwoot.com/docs/deployment/architecture) to deploy with Docker or Caprover.
|
For other supported options, checkout our [deployment page](https://chatwoot.com/deploy).
|
||||||
|
|
||||||
## Security
|
## Security
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,7 @@ class ContactInboxBuilder
|
|||||||
def perform
|
def perform
|
||||||
@contact = Contact.find(contact_id)
|
@contact = Contact.find(contact_id)
|
||||||
@inbox = @contact.account.inboxes.find(inbox_id)
|
@inbox = @contact.account.inboxes.find(inbox_id)
|
||||||
return unless ['Channel::TwilioSms', 'Channel::Email', 'Channel::Api'].include? @inbox.channel_type
|
return unless ['Channel::TwilioSms', 'Channel::Email', 'Channel::Api', 'Channel::Whatsapp'].include? @inbox.channel_type
|
||||||
|
|
||||||
source_id = @source_id || generate_source_id
|
source_id = @source_id || generate_source_id
|
||||||
create_contact_inbox(source_id) if source_id.present?
|
create_contact_inbox(source_id) if source_id.present?
|
||||||
@ -14,12 +14,20 @@ class ContactInboxBuilder
|
|||||||
|
|
||||||
def generate_source_id
|
def generate_source_id
|
||||||
return twilio_source_id if @inbox.channel_type == 'Channel::TwilioSms'
|
return twilio_source_id if @inbox.channel_type == 'Channel::TwilioSms'
|
||||||
|
return wa_source_id if @inbox.channel_type == 'Channel::Whatsapp'
|
||||||
return @contact.email if @inbox.channel_type == 'Channel::Email'
|
return @contact.email if @inbox.channel_type == 'Channel::Email'
|
||||||
return SecureRandom.uuid if @inbox.channel_type == 'Channel::Api'
|
return SecureRandom.uuid if @inbox.channel_type == 'Channel::Api'
|
||||||
|
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def wa_source_id
|
||||||
|
return unless @contact.phone_number
|
||||||
|
|
||||||
|
# whatsapp doesn't want the + in e164 format
|
||||||
|
"#{@contact.phone_number}.delete('+')"
|
||||||
|
end
|
||||||
|
|
||||||
def twilio_source_id
|
def twilio_source_id
|
||||||
return unless @contact.phone_number
|
return unless @contact.phone_number
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,7 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
|
|||||||
before_action :fetch_agent, except: [:create, :index]
|
before_action :fetch_agent, except: [:create, :index]
|
||||||
before_action :check_authorization
|
before_action :check_authorization
|
||||||
before_action :find_user, only: [:create]
|
before_action :find_user, only: [:create]
|
||||||
|
before_action :validate_limit, only: [:create]
|
||||||
before_action :create_user, only: [:create]
|
before_action :create_user, only: [:create]
|
||||||
before_action :save_account_user, only: [:create]
|
before_action :save_account_user, only: [:create]
|
||||||
|
|
||||||
@ -69,4 +70,8 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
|
|||||||
def agents
|
def agents
|
||||||
@agents ||= Current.account.users.order_by_full_name.includes({ avatar_attachment: [:blob] })
|
@agents ||= Current.account.users.order_by_full_name.includes({ avatar_attachment: [:blob] })
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def validate_limit
|
||||||
|
render_payment_required('Account limit exceeded. Upgrade to a higher plan') if agents.count >= Current.account.usage_limits[:agents]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -74,7 +74,7 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def long_lived_token(omniauth_token)
|
def long_lived_token(omniauth_token)
|
||||||
koala = Koala::Facebook::OAuth.new(ENV['FB_APP_ID'], ENV['FB_APP_SECRET'])
|
koala = Koala::Facebook::OAuth.new(GlobalConfigService.load('FB_APP_ID', ''), GlobalConfigService.load('FB_APP_SECRET', ''))
|
||||||
koala.exchange_access_token_info(omniauth_token)['access_token']
|
koala.exchange_access_token_info(omniauth_token)['access_token']
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Rails.logger.info e
|
Rails.logger.info e
|
||||||
|
|||||||
@ -33,7 +33,7 @@ class Api::V1::Accounts::CannedResponsesController < Api::V1::Accounts::BaseCont
|
|||||||
|
|
||||||
def canned_responses
|
def canned_responses
|
||||||
if params[:search]
|
if params[:search]
|
||||||
Current.account.canned_responses.where('short_code ILIKE ?', "#{params[:search]}%")
|
Current.account.canned_responses.where('short_code ILIKE :search OR content ILIKE :search', search: "%#{params[:search]}%")
|
||||||
else
|
else
|
||||||
Current.account.canned_responses
|
Current.account.canned_responses
|
||||||
end
|
end
|
||||||
|
|||||||
@ -11,9 +11,9 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
|||||||
RESULTS_PER_PAGE = 15
|
RESULTS_PER_PAGE = 15
|
||||||
|
|
||||||
before_action :check_authorization
|
before_action :check_authorization
|
||||||
before_action :set_current_page, only: [:index, :active, :search]
|
before_action :set_current_page, only: [:index, :active, :search, :filter]
|
||||||
before_action :fetch_contact, only: [:show, :update, :destroy, :contactable_inboxes, :destroy_custom_attributes]
|
before_action :fetch_contact, only: [:show, :update, :destroy, :contactable_inboxes, :destroy_custom_attributes]
|
||||||
before_action :set_include_contact_inboxes, only: [:index, :search]
|
before_action :set_include_contact_inboxes, only: [:index, :search, :filter]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@contacts_count = resolved_contacts.count
|
@contacts_count = resolved_contacts.count
|
||||||
@ -81,11 +81,6 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
|||||||
def update
|
def update
|
||||||
@contact.assign_attributes(contact_update_params)
|
@contact.assign_attributes(contact_update_params)
|
||||||
@contact.save!
|
@contact.save!
|
||||||
rescue ActiveRecord::RecordInvalid => e
|
|
||||||
render json: {
|
|
||||||
message: e.record.errors.full_messages.join(', '),
|
|
||||||
contact: Current.account.contacts.find_by(email: contact_params[:email])
|
|
||||||
}, status: :unprocessable_entity
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
|
|||||||
@ -74,9 +74,10 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
|||||||
end
|
end
|
||||||
|
|
||||||
def update_last_seen
|
def update_last_seen
|
||||||
@conversation.agent_last_seen_at = DateTime.now.utc
|
# rubocop:disable Rails/SkipsModelValidations
|
||||||
@conversation.assignee_last_seen_at = DateTime.now.utc if assignee?
|
@conversation.update_column(:agent_last_seen_at, DateTime.now.utc)
|
||||||
@conversation.save!
|
@conversation.update_column(:assignee_last_seen_at, DateTime.now.utc) if assignee?
|
||||||
|
# rubocop:enable Rails/SkipsModelValidations
|
||||||
end
|
end
|
||||||
|
|
||||||
def custom_attributes
|
def custom_attributes
|
||||||
|
|||||||
@ -39,7 +39,7 @@ class Api::V1::Accounts::CustomAttributeDefinitionsController < Api::V1::Account
|
|||||||
:attribute_display_type,
|
:attribute_display_type,
|
||||||
:attribute_key,
|
:attribute_key,
|
||||||
:attribute_model,
|
:attribute_model,
|
||||||
:default_value
|
attribute_values: []
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
||||||
|
include Api::V1::InboxesHelper
|
||||||
before_action :fetch_inbox, except: [:index, :create]
|
before_action :fetch_inbox, except: [:index, :create]
|
||||||
before_action :fetch_agent_bot, only: [:set_agent_bot]
|
before_action :fetch_agent_bot, only: [:set_agent_bot]
|
||||||
# we are already handling the authorization in fetch inbox
|
# we are already handling the authorization in fetch inbox
|
||||||
@ -41,12 +42,13 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
|||||||
def update
|
def update
|
||||||
@inbox.update(permitted_params.except(:channel))
|
@inbox.update(permitted_params.except(:channel))
|
||||||
@inbox.update_working_hours(params.permit(working_hours: Inbox::OFFISABLE_ATTRS)[:working_hours]) if params[:working_hours]
|
@inbox.update_working_hours(params.permit(working_hours: Inbox::OFFISABLE_ATTRS)[:working_hours]) if params[:working_hours]
|
||||||
|
|
||||||
channel_attributes = get_channel_attributes(@inbox.channel_type)
|
channel_attributes = get_channel_attributes(@inbox.channel_type)
|
||||||
|
|
||||||
# Inbox update doesn't necessarily need channel attributes
|
# Inbox update doesn't necessarily need channel attributes
|
||||||
return if permitted_params(channel_attributes)[:channel].blank?
|
return if permitted_params(channel_attributes)[:channel].blank?
|
||||||
|
|
||||||
|
validate_email_channel(channel_attributes) if @inbox.inbox_type == 'Email'
|
||||||
|
|
||||||
@inbox.channel.update!(permitted_params(channel_attributes)[:channel])
|
@inbox.channel.update!(permitted_params(channel_attributes)[:channel])
|
||||||
update_channel_feature_flags
|
update_channel_feature_flags
|
||||||
end
|
end
|
||||||
|
|||||||
@ -13,6 +13,11 @@ class Api::V1::ProfilesController < Api::BaseController
|
|||||||
@user.update!(profile_params)
|
@user.update!(profile_params)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def avatar
|
||||||
|
@user.avatar.attachment.destroy! if @user.avatar.attached?
|
||||||
|
head :ok
|
||||||
|
end
|
||||||
|
|
||||||
def availability
|
def availability
|
||||||
@user.account_users.find_by!(account_id: availability_params[:account_id]).update!(availability: availability_params[:availability])
|
@user.account_users.find_by!(account_id: availability_params[:account_id]).update!(availability: availability_params[:availability])
|
||||||
end
|
end
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
class Api::V1::Widget::ContactsController < Api::V1::Widget::BaseController
|
class Api::V1::Widget::ContactsController < Api::V1::Widget::BaseController
|
||||||
before_action :process_hmac
|
before_action :process_hmac, only: [:update]
|
||||||
|
|
||||||
def show; end
|
def show; end
|
||||||
|
|
||||||
@ -8,7 +8,7 @@ class Api::V1::Widget::ContactsController < Api::V1::Widget::BaseController
|
|||||||
contact: @contact,
|
contact: @contact,
|
||||||
params: permitted_params.to_h.deep_symbolize_keys
|
params: permitted_params.to_h.deep_symbolize_keys
|
||||||
)
|
)
|
||||||
render json: contact_identify_action.perform
|
@contact = contact_identify_action.perform
|
||||||
end
|
end
|
||||||
|
|
||||||
# TODO : clean up this with proper routes delete contacts/custom_attributes
|
# TODO : clean up this with proper routes delete contacts/custom_attributes
|
||||||
@ -21,13 +21,22 @@ class Api::V1::Widget::ContactsController < Api::V1::Widget::BaseController
|
|||||||
private
|
private
|
||||||
|
|
||||||
def process_hmac
|
def process_hmac
|
||||||
return if params[:identifier_hash].blank? && !@web_widget.hmac_mandatory
|
return unless should_verify_hmac?
|
||||||
|
|
||||||
render json: { error: 'HMAC failed: Invalid Identifier Hash Provided' }, status: :unauthorized unless valid_hmac?
|
render json: { error: 'HMAC failed: Invalid Identifier Hash Provided' }, status: :unauthorized unless valid_hmac?
|
||||||
|
|
||||||
@contact_inbox.update(hmac_verified: true)
|
@contact_inbox.update(hmac_verified: true)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def should_verify_hmac?
|
||||||
|
return false if params[:identifier_hash].blank? && !@web_widget.hmac_mandatory
|
||||||
|
|
||||||
|
# Taking an extra caution that the hmac is triggered whenever identifier is present
|
||||||
|
return false if params[:custom_attributes].present? && params[:identifier].blank?
|
||||||
|
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
def valid_hmac?
|
def valid_hmac?
|
||||||
params[:identifier_hash] == OpenSSL::HMAC.hexdigest(
|
params[:identifier_hash] == OpenSSL::HMAC.hexdigest(
|
||||||
'sha256',
|
'sha256',
|
||||||
|
|||||||
@ -31,13 +31,18 @@ module RequestExceptionHandler
|
|||||||
render json: { error: message }, status: :unprocessable_entity
|
render json: { error: message }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def render_payment_required(message)
|
||||||
|
render json: { error: message }, status: :payment_required
|
||||||
|
end
|
||||||
|
|
||||||
def render_internal_server_error(message)
|
def render_internal_server_error(message)
|
||||||
render json: { error: message }, status: :internal_server_error
|
render json: { error: message }, status: :internal_server_error
|
||||||
end
|
end
|
||||||
|
|
||||||
def render_record_invalid(exception)
|
def render_record_invalid(exception)
|
||||||
render json: {
|
render json: {
|
||||||
message: exception.record.errors.full_messages.join(', ')
|
message: exception.record.errors.full_messages.join(', '),
|
||||||
|
attributes: exception.record.errors.attribute_names
|
||||||
}, status: :unprocessable_entity
|
}, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@ -26,14 +26,17 @@ class DashboardController < ActionController::Base
|
|||||||
'API_CHANNEL_THUMBNAIL',
|
'API_CHANNEL_THUMBNAIL',
|
||||||
'ANALYTICS_TOKEN',
|
'ANALYTICS_TOKEN',
|
||||||
'ANALYTICS_HOST'
|
'ANALYTICS_HOST'
|
||||||
).merge(
|
).merge(app_config)
|
||||||
APP_VERSION: Chatwoot.config[:version],
|
|
||||||
VAPID_PUBLIC_KEY: VapidService.public_key,
|
|
||||||
ENABLE_ACCOUNT_SIGNUP: GlobalConfigService.load('ENABLE_ACCOUNT_SIGNUP', 'false')
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def ensure_installation_onboarding
|
def ensure_installation_onboarding
|
||||||
redirect_to '/installation/onboarding' if ::Redis::Alfred.get(::Redis::Alfred::CHATWOOT_INSTALLATION_ONBOARDING)
|
redirect_to '/installation/onboarding' if ::Redis::Alfred.get(::Redis::Alfred::CHATWOOT_INSTALLATION_ONBOARDING)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def app_config
|
||||||
|
{ APP_VERSION: Chatwoot.config[:version],
|
||||||
|
VAPID_PUBLIC_KEY: VapidService.public_key,
|
||||||
|
ENABLE_ACCOUNT_SIGNUP: GlobalConfigService.load('ENABLE_ACCOUNT_SIGNUP', 'false'),
|
||||||
|
FB_APP_ID: GlobalConfigService.load('FB_APP_ID', '') }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -28,10 +28,7 @@ class DeviseOverrides::ConfirmationsController < Devise::ConfirmationsController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def create_reset_token_link(user)
|
def create_reset_token_link(user)
|
||||||
raw, enc = Devise.token_generator.generate(user.class, :reset_password_token)
|
token = user.send(:set_reset_password_token)
|
||||||
user.reset_password_token = enc
|
"/app/auth/password/edit?config=default&redirect_url=&reset_password_token=#{token}"
|
||||||
user.reset_password_sent_at = Time.now.utc
|
|
||||||
user.save(validate: false)
|
|
||||||
"/app/auth/password/edit?config=default&redirect_url=&reset_password_token=#{raw}"
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -25,6 +25,6 @@ class Webhooks::InstagramController < ApplicationController
|
|||||||
private
|
private
|
||||||
|
|
||||||
def valid_instagram_token?(token)
|
def valid_instagram_token?(token)
|
||||||
token == ENV['IG_VERIFY_TOKEN']
|
token == GlobalConfigService.load('IG_VERIFY_TOKEN', '')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -1,5 +1,30 @@
|
|||||||
class ConversationDrop < BaseDrop
|
class ConversationDrop < BaseDrop
|
||||||
|
include MessageFormatHelper
|
||||||
|
|
||||||
def display_id
|
def display_id
|
||||||
@obj.try(:display_id)
|
@obj.try(:display_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def contact_name
|
||||||
|
@obj.try(:contact).name.capitalize || 'Customer'
|
||||||
|
end
|
||||||
|
|
||||||
|
def recent_messages
|
||||||
|
@obj.try(:recent_messages).map do |message|
|
||||||
|
{
|
||||||
|
'sender' => message_sender_name(message.sender),
|
||||||
|
'content' => render_message_content(transform_user_mention_content(message.content)),
|
||||||
|
'attachments' => message.attachments.map(&:file_url)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def message_sender_name(sender)
|
||||||
|
return 'Bot' if sender.blank?
|
||||||
|
return contact_name if sender.instance_of?(Contact)
|
||||||
|
|
||||||
|
sender&.available_name || sender&.name
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -1,2 +1,5 @@
|
|||||||
class InboxDrop < BaseDrop
|
class InboxDrop < BaseDrop
|
||||||
|
def name
|
||||||
|
@obj.try(:name)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -6,7 +6,7 @@ class MessageDrop < BaseDrop
|
|||||||
end
|
end
|
||||||
|
|
||||||
def text_content
|
def text_content
|
||||||
content = @obj.try(:content)
|
content = @obj.try(:content) || ''
|
||||||
transform_user_mention_content content
|
render_message_content(transform_user_mention_content(content))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -70,7 +70,12 @@ class ConversationFinder
|
|||||||
end
|
end
|
||||||
|
|
||||||
def find_all_conversations
|
def find_all_conversations
|
||||||
@conversations = current_account.conversations.where(inbox_id: @inbox_ids)
|
if params[:conversation_type] == 'mention'
|
||||||
|
conversation_ids = current_account.mentions.where(user: current_user).pluck(:conversation_id)
|
||||||
|
@conversations = current_account.conversations.where(id: conversation_ids)
|
||||||
|
else
|
||||||
|
@conversations = current_account.conversations.where(inbox_id: @inbox_ids)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def filter_by_assignee_type
|
def filter_by_assignee_type
|
||||||
@ -123,6 +128,10 @@ class ConversationFinder
|
|||||||
@conversations = @conversations.includes(
|
@conversations = @conversations.includes(
|
||||||
:taggings, :inbox, { assignee: { avatar_attachment: [:blob] } }, { contact: { avatar_attachment: [:blob] } }, :team, :contact_inbox
|
:taggings, :inbox, { assignee: { avatar_attachment: [:blob] } }, { contact: { avatar_attachment: [:blob] } }, :team, :contact_inbox
|
||||||
)
|
)
|
||||||
@conversations.latest.page(current_page)
|
if params[:conversation_type] == 'mention'
|
||||||
|
@conversations.page(current_page)
|
||||||
|
else
|
||||||
|
@conversations.latest.page(current_page)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
33
app/helpers/api/v1/inboxes_helper.rb
Normal file
33
app/helpers/api/v1/inboxes_helper.rb
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
module Api::V1::InboxesHelper
|
||||||
|
def validate_email_channel(attributes)
|
||||||
|
channel_data = permitted_params(attributes)[:channel]
|
||||||
|
|
||||||
|
validate_imap(channel_data)
|
||||||
|
validate_smtp(channel_data)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def validate_imap(channel_data)
|
||||||
|
return unless channel_data.key?('imap_enabled') && channel_data[:imap_enabled]
|
||||||
|
|
||||||
|
Mail.defaults do
|
||||||
|
retriever_method :imap, { address: channel_data[:imap_address],
|
||||||
|
port: channel_data[:imap_port],
|
||||||
|
user_name: channel_data[:imap_email],
|
||||||
|
password: channel_data[:imap_password],
|
||||||
|
enable_ssl: channel_data[:imap_enable_ssl] }
|
||||||
|
end
|
||||||
|
|
||||||
|
Mail.connection do # rubocop:disable:block
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_smtp(channel_data)
|
||||||
|
return unless channel_data.key?('smtp_enabled') && channel_data[:smtp_enabled]
|
||||||
|
|
||||||
|
smtp = Net::SMTP.start(channel_data[:smtp_address], channel_data[:smtp_port], channel_data[:smtp_domain], channel_data[:smtp_email],
|
||||||
|
channel_data[:smtp_password], :login)
|
||||||
|
smtp.finish unless smtp&.nil?
|
||||||
|
end
|
||||||
|
end
|
||||||
@ -1,6 +1,13 @@
|
|||||||
module MessageFormatHelper
|
module MessageFormatHelper
|
||||||
include RegexHelper
|
include RegexHelper
|
||||||
|
|
||||||
def transform_user_mention_content(message_content)
|
def transform_user_mention_content(message_content)
|
||||||
message_content.gsub(MENTION_REGEX, '\1')
|
message_content.gsub(MENTION_REGEX, '\1')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def render_message_content(message_content)
|
||||||
|
# rubocop:disable Rails/OutputSafety
|
||||||
|
CommonMarker.render_html(message_content).html_safe
|
||||||
|
# rubocop:enable Rails/OutputSafety
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -166,4 +166,8 @@ export default {
|
|||||||
profile: { ...availabilityData },
|
profile: { ...availabilityData },
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
deleteAvatar() {
|
||||||
|
return axios.delete(endPoints('deleteAvatar').url);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -53,6 +53,11 @@ class ContactAPI extends ApiClient {
|
|||||||
return axios.get(requestURL);
|
return axios.get(requestURL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
filter(page = 1, sortAttr = 'name', queryPayload) {
|
||||||
|
let requestURL = `${this.url}/filter?${buildContactParams(page, sortAttr)}`;
|
||||||
|
return axios.post(requestURL, queryPayload);
|
||||||
|
}
|
||||||
|
|
||||||
importContacts(file) {
|
importContacts(file) {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('import_file', file);
|
formData.append('import_file', file);
|
||||||
|
|||||||
@ -36,6 +36,10 @@ const endPoints = {
|
|||||||
},
|
},
|
||||||
params: { omniauth_token: '' },
|
params: { omniauth_token: '' },
|
||||||
},
|
},
|
||||||
|
|
||||||
|
deleteAvatar: {
|
||||||
|
url: '/api/v1/profile/avatar',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default page => {
|
export default page => {
|
||||||
|
|||||||
@ -6,7 +6,15 @@ class ConversationApi extends ApiClient {
|
|||||||
super('conversations', { accountScoped: true });
|
super('conversations', { accountScoped: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
get({ inboxId, status, assigneeType, page, labels, teamId }) {
|
get({
|
||||||
|
inboxId,
|
||||||
|
status,
|
||||||
|
assigneeType,
|
||||||
|
page,
|
||||||
|
labels,
|
||||||
|
teamId,
|
||||||
|
conversationType,
|
||||||
|
}) {
|
||||||
return axios.get(this.url, {
|
return axios.get(this.url, {
|
||||||
params: {
|
params: {
|
||||||
inbox_id: inboxId,
|
inbox_id: inboxId,
|
||||||
@ -15,6 +23,15 @@ class ConversationApi extends ApiClient {
|
|||||||
assignee_type: assigneeType,
|
assignee_type: assigneeType,
|
||||||
page,
|
page,
|
||||||
labels,
|
labels,
|
||||||
|
conversation_type: conversationType,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
filter(payload) {
|
||||||
|
return axios.post(`${this.url}/filter`, payload.queryData, {
|
||||||
|
params: {
|
||||||
|
page: payload.page,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -54,7 +71,7 @@ class ConversationApi extends ApiClient {
|
|||||||
toggleTyping({ conversationId, status, isPrivate }) {
|
toggleTyping({ conversationId, status, isPrivate }) {
|
||||||
return axios.post(`${this.url}/${conversationId}/toggle_typing_status`, {
|
return axios.post(`${this.url}/${conversationId}/toggle_typing_status`, {
|
||||||
typing_status: status,
|
typing_status: status,
|
||||||
is_private: isPrivate
|
is_private: isPrivate,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,7 +83,7 @@ class ConversationApi extends ApiClient {
|
|||||||
return axios.post(`${this.url}/${conversationId}/unmute`);
|
return axios.post(`${this.url}/${conversationId}/unmute`);
|
||||||
}
|
}
|
||||||
|
|
||||||
meta({ inboxId, status, assigneeType, labels, teamId }) {
|
meta({ inboxId, status, assigneeType, labels, teamId, conversationType }) {
|
||||||
return axios.get(`${this.url}/meta`, {
|
return axios.get(`${this.url}/meta`, {
|
||||||
params: {
|
params: {
|
||||||
inbox_id: inboxId,
|
inbox_id: inboxId,
|
||||||
@ -74,6 +91,7 @@ class ConversationApi extends ApiClient {
|
|||||||
assignee_type: assigneeType,
|
assignee_type: assigneeType,
|
||||||
labels,
|
labels,
|
||||||
team_id: teamId,
|
team_id: teamId,
|
||||||
|
conversation_type: conversationType,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,6 +11,7 @@ describe('#ContactsAPI', () => {
|
|||||||
expect(contactAPI).toHaveProperty('update');
|
expect(contactAPI).toHaveProperty('update');
|
||||||
expect(contactAPI).toHaveProperty('delete');
|
expect(contactAPI).toHaveProperty('delete');
|
||||||
expect(contactAPI).toHaveProperty('getConversations');
|
expect(contactAPI).toHaveProperty('getConversations');
|
||||||
|
expect(contactAPI).toHaveProperty('filter');
|
||||||
});
|
});
|
||||||
|
|
||||||
describeWithAPIMock('API calls', context => {
|
describeWithAPIMock('API calls', context => {
|
||||||
@ -81,6 +82,24 @@ describe('#ContactsAPI', () => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('#filter', () => {
|
||||||
|
const queryPayload = {
|
||||||
|
payload: [
|
||||||
|
{
|
||||||
|
attribute_key: 'email',
|
||||||
|
filter_operator: 'contains',
|
||||||
|
values: ['fayaz'],
|
||||||
|
query_operator: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
contactAPI.filter(1, 'name', queryPayload);
|
||||||
|
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
||||||
|
'/api/v1/contacts/filter?include_contact_inboxes=false&page=1&sort=name',
|
||||||
|
queryPayload
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -19,6 +19,7 @@ describe('#ConversationAPI', () => {
|
|||||||
expect(conversationAPI).toHaveProperty('unmute');
|
expect(conversationAPI).toHaveProperty('unmute');
|
||||||
expect(conversationAPI).toHaveProperty('meta');
|
expect(conversationAPI).toHaveProperty('meta');
|
||||||
expect(conversationAPI).toHaveProperty('sendEmailTranscript');
|
expect(conversationAPI).toHaveProperty('sendEmailTranscript');
|
||||||
|
expect(conversationAPI).toHaveProperty('filter');
|
||||||
});
|
});
|
||||||
|
|
||||||
describeWithAPIMock('API calls', context => {
|
describeWithAPIMock('API calls', context => {
|
||||||
@ -173,5 +174,41 @@ describe('#ConversationAPI', () => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('#filter', () => {
|
||||||
|
const payload = {
|
||||||
|
page: 1,
|
||||||
|
queryData: {
|
||||||
|
payload: [
|
||||||
|
{
|
||||||
|
attribute_key: 'status',
|
||||||
|
filter_operator: 'equal_to',
|
||||||
|
values: ['pending', 'resolved'],
|
||||||
|
query_operator: 'and',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
attribute_key: 'assignee',
|
||||||
|
filter_operator: 'equal_to',
|
||||||
|
values: [3],
|
||||||
|
query_operator: 'and',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
attribute_key: 'id',
|
||||||
|
filter_operator: 'equal_to',
|
||||||
|
values: ['This is a test'],
|
||||||
|
query_operator: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
conversationAPI.filter(payload);
|
||||||
|
expect(
|
||||||
|
context.axiosMock.post
|
||||||
|
).toHaveBeenCalledWith(
|
||||||
|
'/api/v1/conversations/filter',
|
||||||
|
payload.queryData,
|
||||||
|
{ params: { page: payload.page } }
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
/* Enter and leave animations can use different */
|
/* Enter and leave animations can use different */
|
||||||
/* durations and timing functions. */
|
/* durations and timing functions. */
|
||||||
.slide-fade-enter-active {
|
.slide-fade-enter-active {
|
||||||
@ -9,7 +8,8 @@
|
|||||||
transition: all .3s $ease-out-cubic;
|
transition: all .3s $ease-out-cubic;
|
||||||
}
|
}
|
||||||
|
|
||||||
.slide-fade-enter, .slide-fade-leave-to {
|
.slide-fade-enter,
|
||||||
|
.slide-fade-leave-to {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateX(10px);
|
transform: translateX(10px);
|
||||||
}
|
}
|
||||||
@ -22,22 +22,33 @@
|
|||||||
transform: translateX($space-medium);
|
transform: translateX($space-medium);
|
||||||
}
|
}
|
||||||
|
|
||||||
.conversations-list-enter-active, .conversations-list-leave-active {
|
.conversations-list-enter-active,
|
||||||
|
.conversations-list-leave-active {
|
||||||
transition: all .25s $ease-out-cubic;
|
transition: all .25s $ease-out-cubic;
|
||||||
}
|
}
|
||||||
|
|
||||||
.conversations-list-enter, .conversations-list-leave-to /* .conversations-list-leave-active for <2.1.8 */ {
|
.conversations-list-enter,
|
||||||
|
.conversations-list-leave-to {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateX($space-medium);
|
transform: translateX($space-medium);
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-list-enter-active, .menu-list-leave-active {
|
.menu-list-enter-active,
|
||||||
transition: all .2s $ease-out-cubic;
|
.menu-list-leave-active {
|
||||||
|
transition: opacity .3s $ease-out-cubic,
|
||||||
|
transform .2s $ease-out-cubic;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-list-enter, .menu-list-leave-to /* .conversations-list-leave-active for <2.1.8 */ {
|
|
||||||
|
.menu-list-leave-to {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateX($space-medium);
|
position: absolute;
|
||||||
|
transform: translateX($space-small);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-list-enter {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-$space-small);
|
||||||
}
|
}
|
||||||
|
|
||||||
.slide-up-enter-active {
|
.slide-up-enter-active {
|
||||||
@ -48,8 +59,8 @@
|
|||||||
transition: all .3s $ease-out-cubic;
|
transition: all .3s $ease-out-cubic;
|
||||||
}
|
}
|
||||||
|
|
||||||
.slide-up-enter, .slide-up-leave-to
|
.slide-up-enter,
|
||||||
/* .slide-fade-leave-active for <2.1.8 */ {
|
.slide-up-leave-to {
|
||||||
transform: translateY(-$space-medium);
|
transform: translateY(-$space-medium);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
@ -60,10 +71,10 @@
|
|||||||
transition: transform 0.25s $ease-in-cubic, opacity 0.15s $ease-in-cubic;
|
transition: transform 0.25s $ease-in-cubic, opacity 0.15s $ease-in-cubic;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-slide-enter, .menu-slide-leave-to
|
.menu-slide-enter,
|
||||||
/* .slide-fade-leave-active for <2.1.8 */ {
|
.menu-slide-leave-to {
|
||||||
transform: translateY($space-small);
|
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
transform: translateY($space-small);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -75,10 +86,10 @@
|
|||||||
transition: all .1s $ease-out-sine;
|
transition: all .1s $ease-out-sine;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toast-fade-enter, .toast-fade-leave-to
|
.toast-fade-enter,
|
||||||
/* .toast-fade-leave-active for <2.1.8 */ {
|
.toast-fade-leave-to {
|
||||||
transform: translateY(-$space-small);
|
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
transform: translateY(-$space-small);
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-fade-enter-active {
|
.modal-fade-enter-active {
|
||||||
@ -89,8 +100,8 @@
|
|||||||
transition: all .1s $ease-out-sine;
|
transition: all .1s $ease-out-sine;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-fade-enter, .modal-fade-leave-to
|
.modal-fade-enter,
|
||||||
/* .slide-fade-leave-active for <2.1.8 */ {
|
.modal-fade-leave-to {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -49,7 +49,21 @@ code {
|
|||||||
.cursor-pointer {
|
.cursor-pointer {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove when grid gutters are fixed
|
// remove when grid gutters are fixed
|
||||||
.columns.with-right-space {
|
.columns.with-right-space {
|
||||||
padding-right: var(--space-normal);
|
padding-right: var(--space-normal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
border-radius: var(--border-radius-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.padding-right-small {
|
||||||
|
padding-right: var(--space-one);
|
||||||
|
}
|
||||||
|
|
||||||
|
.margin-right-small {
|
||||||
|
margin-right: var(--space-small);
|
||||||
|
}
|
||||||
|
|||||||
@ -219,9 +219,9 @@ $badge-background: $primary-color;
|
|||||||
$badge-color: $white;
|
$badge-color: $white;
|
||||||
$badge-color-alt: $black;
|
$badge-color-alt: $black;
|
||||||
$badge-palette: $foundation-palette;
|
$badge-palette: $foundation-palette;
|
||||||
$badge-padding: 0.3em;
|
$badge-padding: var(--space-smaller);
|
||||||
$badge-minwidth: 2.1em;
|
$badge-minwidth: 2.1em;
|
||||||
$badge-font-size: 0.6rem;
|
$badge-font-size: var(--font-size-nano);
|
||||||
|
|
||||||
// 10. Breadcrumbs
|
// 10. Breadcrumbs
|
||||||
// ---------------
|
// ---------------
|
||||||
@ -400,7 +400,7 @@ $mediaobject-image-width-stacked: 100%;
|
|||||||
|
|
||||||
$menu-margin: 0;
|
$menu-margin: 0;
|
||||||
$menu-margin-nested: $space-medium;
|
$menu-margin-nested: $space-medium;
|
||||||
$menu-item-padding: $space-one;
|
$menu-item-padding: $space-slab;
|
||||||
$menu-item-color-active: $white;
|
$menu-item-color-active: $white;
|
||||||
$menu-item-background-active: $color-background;
|
$menu-item-background-active: $color-background;
|
||||||
$menu-icon-spacing: 0.25rem;
|
$menu-icon-spacing: 0.25rem;
|
||||||
|
|||||||
@ -6,6 +6,10 @@
|
|||||||
margin-right: var(--space-smaller);
|
margin-right: var(--space-smaller);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.margin-left-minus-slab {
|
||||||
|
margin-left: var(--space-minus-slab);
|
||||||
|
}
|
||||||
|
|
||||||
.fs-small {
|
.fs-small {
|
||||||
font-size: var(--font-size-small);
|
font-size: var(--font-size-small);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -44,11 +44,14 @@ $woot-logo-padding: $space-large $space-two;
|
|||||||
$color-woot: #1f93ff;
|
$color-woot: #1f93ff;
|
||||||
$color-gray: #6e6f73;
|
$color-gray: #6e6f73;
|
||||||
$color-light-gray: #999a9b;
|
$color-light-gray: #999a9b;
|
||||||
$color-border: #e0e6ed;
|
|
||||||
$color-border-light: #f0f4f5;
|
$color-border: var(--s-75);
|
||||||
$color-border-dark: #cad0d4;
|
$color-border-light: var(--s-50);
|
||||||
$color-background: #f4f6fb;
|
$color-border-dark: var(--s-100);
|
||||||
$color-background-light: #f9fafc;
|
|
||||||
|
$color-background: var(--s-50);
|
||||||
|
$color-background-light: var(--s-25);
|
||||||
|
|
||||||
$color-white: #fff;
|
$color-white: #fff;
|
||||||
$color-body: #3c4858;
|
$color-body: #3c4858;
|
||||||
$color-heading: #1f2d3d;
|
$color-heading: #1f2d3d;
|
||||||
|
|||||||
@ -2,8 +2,9 @@
|
|||||||
@include elegant-card;
|
@include elegant-card;
|
||||||
@include border-light;
|
@include border-light;
|
||||||
box-sizing: content-box;
|
box-sizing: content-box;
|
||||||
|
padding: var(--space-small);
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
z-index: 999;
|
z-index: var(--z-index-very-high);
|
||||||
|
|
||||||
&.dropdown-pane--open {
|
&.dropdown-pane--open {
|
||||||
display: block;
|
display: block;
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
@include background-white;
|
@include background-white;
|
||||||
@include flex;
|
@include flex;
|
||||||
@include flex-align($x: justify, $y: middle);
|
@include flex-align($x: justify, $y: middle);
|
||||||
@include border-normal-bottom;
|
border-bottom: 1px solid var(--s-50);
|
||||||
height: $header-height;
|
height: $header-height;
|
||||||
min-height: $header-height;
|
min-height: $header-height;
|
||||||
|
|
||||||
|
|||||||
@ -25,6 +25,7 @@ $default-button-height: 4.0rem;
|
|||||||
// @TODDO - Remove after moving all buttons to woot-button
|
// @TODDO - Remove after moving all buttons to woot-button
|
||||||
.icon+.button__content {
|
.icon+.button__content {
|
||||||
padding-left: var(--space-small);
|
padding-left: var(--space-small);
|
||||||
|
width: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.expanded {
|
&.expanded {
|
||||||
@ -103,7 +104,6 @@ $default-button-height: 4.0rem;
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -85,11 +85,6 @@
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
width: 27rem;
|
width: 27rem;
|
||||||
|
|
||||||
.small-icon {
|
|
||||||
font-size: $font-size-mini;
|
|
||||||
vertical-align: top;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.conversation--meta {
|
.conversation--meta {
|
||||||
|
|||||||
@ -93,7 +93,7 @@
|
|||||||
|
|
||||||
.status--filter {
|
.status--filter {
|
||||||
@include padding($zero null $zero $space-normal);
|
@include padding($zero null $zero $space-normal);
|
||||||
@include margin($space-smaller $space-slab $zero $zero);
|
@include margin($zero);
|
||||||
background-color: $color-background-light;
|
background-color: $color-background-light;
|
||||||
border: 1px solid $color-border;
|
border: 1px solid $color-border;
|
||||||
float: right;
|
float: right;
|
||||||
|
|||||||
@ -19,7 +19,7 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: $font-size-big;
|
font-size: $font-size-big;
|
||||||
line-height: $space-normal;
|
line-height: $space-normal;
|
||||||
padding: $space-normal $space-two;
|
padding: $space-normal;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: $space-micro;
|
right: $space-micro;
|
||||||
top: $space-micro;
|
top: $space-micro;
|
||||||
@ -29,7 +29,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.page-top-bar {
|
.page-top-bar {
|
||||||
@include padding($space-large $space-large $zero);
|
@include padding($space-large $space-large $zero);
|
||||||
|
|
||||||
@ -48,13 +47,16 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
width: 60rem;
|
width: 60rem;
|
||||||
|
|
||||||
|
&.medium {
|
||||||
|
max-width: 80%;
|
||||||
|
width: 90rem;
|
||||||
|
}
|
||||||
|
|
||||||
.content-box {
|
.content-box {
|
||||||
@include padding($zero);
|
@include padding($zero);
|
||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
color: $color-heading;
|
color: $color-heading;
|
||||||
font-size: $font-size-medium;
|
font-size: $font-size-medium;
|
||||||
@ -89,15 +91,19 @@
|
|||||||
button {
|
button {
|
||||||
font-size: $font-size-small;
|
font-size: $font-size-small;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.justify-content-end {
|
||||||
|
justify-content: end;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.delete-item {
|
.delete-item {
|
||||||
@include padding($space-large);
|
@include padding($space-large);
|
||||||
|
|
||||||
button {
|
button {
|
||||||
@include margin($zero);
|
@include margin($zero);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-enter,
|
.modal-enter,
|
||||||
|
|||||||
@ -6,12 +6,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
@include border-normal-right;
|
|
||||||
@include background-white;
|
|
||||||
@include full-height;
|
|
||||||
@include margin(0);
|
|
||||||
@include space-between-column;
|
|
||||||
width: $nav-bar-width;
|
|
||||||
z-index: 1024 - 1;
|
z-index: 1024 - 1;
|
||||||
|
|
||||||
//logo
|
//logo
|
||||||
@ -22,26 +16,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-nav {
|
|
||||||
a {
|
|
||||||
border-radius: $space-smaller;
|
|
||||||
color: $color-gray;
|
|
||||||
font-size: $font-size-default;
|
|
||||||
font-weight: $font-weight-medium;
|
|
||||||
|
|
||||||
.wrap,
|
|
||||||
.child-icon {
|
|
||||||
&:hover {
|
|
||||||
color: $color-woot;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.active a .wrap {
|
|
||||||
color: $color-woot;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.nested {
|
.nested {
|
||||||
a {
|
a {
|
||||||
font-size: $font-size-small;
|
font-size: $font-size-small;
|
||||||
@ -83,34 +57,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-nav {
|
|
||||||
@include flex-weight(1);
|
|
||||||
@include scroll-on-hover;
|
|
||||||
padding: 0 $space-medium - $space-one;
|
|
||||||
|
|
||||||
a {
|
|
||||||
&::before {
|
|
||||||
margin-right: $space-slab;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-title {
|
|
||||||
color: $color-gray;
|
|
||||||
font-size: $font-size-medium;
|
|
||||||
margin-top: $space-medium;
|
|
||||||
|
|
||||||
>span {
|
|
||||||
margin-left: $space-one;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-title+ul>li>a {
|
|
||||||
@include padding($space-micro null);
|
|
||||||
color: $medium-gray;
|
|
||||||
line-height: $global-lineheight;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hamburger--menu {
|
.hamburger--menu {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: none;
|
display: none;
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="cw-accordion">
|
<div class="cw-accordion">
|
||||||
<button class="cw-accordion--title" @click="$emit('click')">
|
<button class="cw-accordion--title drag-handle" @click="$emit('click')">
|
||||||
<div class="cw-accordion--title-wrap">
|
<div class="cw-accordion--title-wrap">
|
||||||
<emoji-or-icon class="icon-or-emoji" :icon="icon" :emoji="emoji" />
|
<emoji-or-icon class="icon-or-emoji" :icon="icon" :emoji="emoji" />
|
||||||
<h5>
|
<h5>
|
||||||
@ -10,8 +10,8 @@
|
|||||||
<div class="button-icon--wrap">
|
<div class="button-icon--wrap">
|
||||||
<slot name="button" />
|
<slot name="button" />
|
||||||
<div class="chevron-icon__wrap">
|
<div class="chevron-icon__wrap">
|
||||||
<i v-if="isOpen" class="ion-minus chevron-icon"></i>
|
<fluent-icon v-if="isOpen" size="24" icon="subtract" type="solid" />
|
||||||
<i v-else class="ion-plus chevron-icon"></i>
|
<fluent-icon v-else size="24" icon="add" type="solid" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
@ -66,10 +66,10 @@ export default {
|
|||||||
}
|
}
|
||||||
.cw-accordion--title {
|
.cw-accordion--title {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: var(--b-50);
|
background: var(--s-50);
|
||||||
border-bottom: 1px solid var(--color-border);
|
border-bottom: 1px solid var(--s-100);
|
||||||
border-top: 1px solid var(--color-border);
|
border-top: 1px solid var(--s-100);
|
||||||
cursor: pointer;
|
cursor: grab;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|||||||
@ -1,14 +1,39 @@
|
|||||||
<template>
|
l<template>
|
||||||
<div class="conversations-list-wrap">
|
<div class="conversations-list-wrap">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
<div class="chat-list__top">
|
<div class="chat-list__top" :class="{ filter__applied: hasAppliedFilters }">
|
||||||
<h1 class="page-title text-truncate" :title="pageTitle">
|
<h1 class="page-title text-truncate" :title="pageTitle">
|
||||||
{{ pageTitle }}
|
{{ pageTitle }}
|
||||||
</h1>
|
</h1>
|
||||||
<chat-filter @statusFilterChange="updateStatusType" />
|
|
||||||
|
<div class="filter--actions">
|
||||||
|
<chat-filter
|
||||||
|
v-if="!hasAppliedFilters"
|
||||||
|
@statusFilterChange="updateStatusType"
|
||||||
|
/>
|
||||||
|
<woot-button
|
||||||
|
v-else
|
||||||
|
variant="clear"
|
||||||
|
color-scheme="danger"
|
||||||
|
class="btn-clear-filters"
|
||||||
|
@click="resetAndFetchData"
|
||||||
|
>
|
||||||
|
{{ $t('FILTER.CLEAR_BUTTON_LABEL') }}
|
||||||
|
</woot-button>
|
||||||
|
<woot-button
|
||||||
|
v-tooltip.top-end="$t('FILTER.TOOLTIP_LABEL')"
|
||||||
|
variant="clear"
|
||||||
|
color-scheme="secondary"
|
||||||
|
class="btn-filter"
|
||||||
|
@click="onToggleAdvanceFiltersModal"
|
||||||
|
>
|
||||||
|
<fluent-icon icon="filter" />
|
||||||
|
</woot-button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<chat-type-tabs
|
<chat-type-tabs
|
||||||
|
v-if="!hasAppliedFilters"
|
||||||
:items="assigneeTabItems"
|
:items="assigneeTabItems"
|
||||||
:active-tab="activeAssigneeTab"
|
:active-tab="activeAssigneeTab"
|
||||||
class="tab--chat-type"
|
class="tab--chat-type"
|
||||||
@ -26,6 +51,7 @@
|
|||||||
:active-label="label"
|
:active-label="label"
|
||||||
:team-id="teamId"
|
:team-id="teamId"
|
||||||
:chat="chat"
|
:chat="chat"
|
||||||
|
:conversation-type="conversationType"
|
||||||
:show-assignee="showAssigneeInConversationCard"
|
:show-assignee="showAssigneeInConversationCard"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -37,7 +63,7 @@
|
|||||||
v-if="!hasCurrentPageEndReached && !chatListLoading"
|
v-if="!hasCurrentPageEndReached && !chatListLoading"
|
||||||
variant="clear"
|
variant="clear"
|
||||||
size="expanded"
|
size="expanded"
|
||||||
@click="fetchConversations"
|
@click="loadMoreConversations"
|
||||||
>
|
>
|
||||||
{{ $t('CHAT_LIST.LOAD_MORE_CONVERSATIONS') }}
|
{{ $t('CHAT_LIST.LOAD_MORE_CONVERSATIONS') }}
|
||||||
</woot-button>
|
</woot-button>
|
||||||
@ -53,6 +79,18 @@
|
|||||||
{{ $t('CHAT_LIST.EOF') }}
|
{{ $t('CHAT_LIST.EOF') }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<woot-modal
|
||||||
|
:show.sync="showAdvancedFilters"
|
||||||
|
:on-close="onToggleAdvanceFiltersModal"
|
||||||
|
size="medium"
|
||||||
|
>
|
||||||
|
<conversation-advanced-filter
|
||||||
|
v-if="showAdvancedFilters"
|
||||||
|
:filter-types="advancedFilterTypes"
|
||||||
|
:on-close="onToggleAdvanceFiltersModal"
|
||||||
|
@applyFilter="onApplyFilter"
|
||||||
|
/>
|
||||||
|
</woot-modal>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -60,12 +98,16 @@
|
|||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
|
|
||||||
import ChatFilter from './widgets/conversation/ChatFilter';
|
import ChatFilter from './widgets/conversation/ChatFilter';
|
||||||
|
import ConversationAdvancedFilter from './widgets/conversation/ConversationAdvancedFilter';
|
||||||
import ChatTypeTabs from './widgets/ChatTypeTabs';
|
import ChatTypeTabs from './widgets/ChatTypeTabs';
|
||||||
import ConversationCard from './widgets/conversation/ConversationCard';
|
import ConversationCard from './widgets/conversation/ConversationCard';
|
||||||
import timeMixin from '../mixins/time';
|
import timeMixin from '../mixins/time';
|
||||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||||
import conversationMixin from '../mixins/conversations';
|
import conversationMixin from '../mixins/conversations';
|
||||||
import wootConstants from '../constants';
|
import wootConstants from '../constants';
|
||||||
|
import advancedFilterTypes from './widgets/conversation/advancedFilterItems';
|
||||||
|
import filterQueryGenerator from '../helper/filterQueryGenerator.js';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
hasPressedAltAndJKey,
|
hasPressedAltAndJKey,
|
||||||
hasPressedAltAndKKey,
|
hasPressedAltAndKKey,
|
||||||
@ -76,6 +118,7 @@ export default {
|
|||||||
ChatTypeTabs,
|
ChatTypeTabs,
|
||||||
ConversationCard,
|
ConversationCard,
|
||||||
ChatFilter,
|
ChatFilter,
|
||||||
|
ConversationAdvancedFilter,
|
||||||
},
|
},
|
||||||
mixins: [timeMixin, conversationMixin, eventListenerMixins],
|
mixins: [timeMixin, conversationMixin, eventListenerMixins],
|
||||||
props: {
|
props: {
|
||||||
@ -91,11 +134,20 @@ export default {
|
|||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
|
conversationType: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
activeAssigneeTab: wootConstants.ASSIGNEE_TYPE.ME,
|
activeAssigneeTab: wootConstants.ASSIGNEE_TYPE.ME,
|
||||||
activeStatus: wootConstants.STATUS_TYPE.OPEN,
|
activeStatus: wootConstants.STATUS_TYPE.OPEN,
|
||||||
|
showAdvancedFilters: false,
|
||||||
|
advancedFilterTypes: advancedFilterTypes.map(filter => ({
|
||||||
|
...filter,
|
||||||
|
attributeName: this.$t(`FILTER.ATTRIBUTES.${filter.attributeI18nKey}`),
|
||||||
|
})),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -109,7 +161,11 @@ export default {
|
|||||||
currentUserID: 'getCurrentUserID',
|
currentUserID: 'getCurrentUserID',
|
||||||
activeInbox: 'getSelectedInbox',
|
activeInbox: 'getSelectedInbox',
|
||||||
conversationStats: 'conversationStats/getStats',
|
conversationStats: 'conversationStats/getStats',
|
||||||
|
appliedFilters: 'getAppliedConversationFilters',
|
||||||
}),
|
}),
|
||||||
|
hasAppliedFilters() {
|
||||||
|
return this.appliedFilters.length;
|
||||||
|
},
|
||||||
assigneeTabItems() {
|
assigneeTabItems() {
|
||||||
return this.$t('CHAT_LIST.ASSIGNEE_TYPE_TABS').map(item => {
|
return this.$t('CHAT_LIST.ASSIGNEE_TYPE_TABS').map(item => {
|
||||||
const count = this.conversationStats[item.COUNT_KEY] || 0;
|
const count = this.conversationStats[item.COUNT_KEY] || 0;
|
||||||
@ -131,9 +187,17 @@ export default {
|
|||||||
this.activeAssigneeTab
|
this.activeAssigneeTab
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
currentPageFilterKey() {
|
||||||
|
return this.hasAppliedFilters ? 'appliedFilters' : this.activeAssigneeTab;
|
||||||
|
},
|
||||||
|
currentFiltersPage() {
|
||||||
|
return this.$store.getters['conversationPage/getCurrentPageFilter'](
|
||||||
|
this.currentPageFilterKey
|
||||||
|
);
|
||||||
|
},
|
||||||
hasCurrentPageEndReached() {
|
hasCurrentPageEndReached() {
|
||||||
return this.$store.getters['conversationPage/getHasEndReached'](
|
return this.$store.getters['conversationPage/getHasEndReached'](
|
||||||
this.activeAssigneeTab
|
this.currentPageFilterKey
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
conversationFilters() {
|
conversationFilters() {
|
||||||
@ -144,6 +208,9 @@ export default {
|
|||||||
page: this.currentPage + 1,
|
page: this.currentPage + 1,
|
||||||
labels: this.label ? [this.label] : undefined,
|
labels: this.label ? [this.label] : undefined,
|
||||||
teamId: this.teamId ? this.teamId : undefined,
|
teamId: this.teamId ? this.teamId : undefined,
|
||||||
|
conversationType: this.conversationType
|
||||||
|
? this.conversationType
|
||||||
|
: undefined,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
pageTitle() {
|
pageTitle() {
|
||||||
@ -156,17 +223,24 @@ export default {
|
|||||||
if (this.label) {
|
if (this.label) {
|
||||||
return `#${this.label}`;
|
return `#${this.label}`;
|
||||||
}
|
}
|
||||||
|
if (this.conversationType === 'mention') {
|
||||||
|
return this.$t('CHAT_LIST.MENTION_HEADING');
|
||||||
|
}
|
||||||
return this.$t('CHAT_LIST.TAB_HEADING');
|
return this.$t('CHAT_LIST.TAB_HEADING');
|
||||||
},
|
},
|
||||||
conversationList() {
|
conversationList() {
|
||||||
let conversationList = [];
|
let conversationList = [];
|
||||||
const filters = this.conversationFilters;
|
if (!this.hasAppliedFilters) {
|
||||||
if (this.activeAssigneeTab === 'me') {
|
const filters = this.conversationFilters;
|
||||||
conversationList = [...this.mineChatsList(filters)];
|
if (this.activeAssigneeTab === 'me') {
|
||||||
} else if (this.activeAssigneeTab === 'unassigned') {
|
conversationList = [...this.mineChatsList(filters)];
|
||||||
conversationList = [...this.unAssignedChatsList(filters)];
|
} else if (this.activeAssigneeTab === 'unassigned') {
|
||||||
|
conversationList = [...this.unAssignedChatsList(filters)];
|
||||||
|
} else {
|
||||||
|
conversationList = [...this.allChatList(filters)];
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
conversationList = [...this.allChatList(filters)];
|
conversationList = [...this.chatLists];
|
||||||
}
|
}
|
||||||
|
|
||||||
return conversationList;
|
return conversationList;
|
||||||
@ -188,6 +262,9 @@ export default {
|
|||||||
label() {
|
label() {
|
||||||
this.resetAndFetchData();
|
this.resetAndFetchData();
|
||||||
},
|
},
|
||||||
|
conversationType() {
|
||||||
|
this.resetAndFetchData();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.$store.dispatch('setChatFilter', this.activeStatus);
|
this.$store.dispatch('setChatFilter', this.activeStatus);
|
||||||
@ -198,6 +275,17 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
onApplyFilter(payload) {
|
||||||
|
if (this.$route.name !== 'home') {
|
||||||
|
this.$router.push({ name: 'home' });
|
||||||
|
}
|
||||||
|
this.$store.dispatch('conversationPage/reset');
|
||||||
|
this.$store.dispatch('emptyAllConversations');
|
||||||
|
this.fetchFilteredConversations(payload);
|
||||||
|
},
|
||||||
|
onToggleAdvanceFiltersModal() {
|
||||||
|
this.showAdvancedFilters = !this.showAdvancedFilters;
|
||||||
|
},
|
||||||
getKeyboardListenerParams() {
|
getKeyboardListenerParams() {
|
||||||
const allConversations = this.$refs.activeConversation.querySelectorAll(
|
const allConversations = this.$refs.activeConversation.querySelectorAll(
|
||||||
'div.conversations-list div.conversation'
|
'div.conversations-list div.conversation'
|
||||||
@ -245,6 +333,7 @@ export default {
|
|||||||
resetAndFetchData() {
|
resetAndFetchData() {
|
||||||
this.$store.dispatch('conversationPage/reset');
|
this.$store.dispatch('conversationPage/reset');
|
||||||
this.$store.dispatch('emptyAllConversations');
|
this.$store.dispatch('emptyAllConversations');
|
||||||
|
this.$store.dispatch('clearConversationFilters');
|
||||||
this.fetchConversations();
|
this.fetchConversations();
|
||||||
},
|
},
|
||||||
fetchConversations() {
|
fetchConversations() {
|
||||||
@ -252,6 +341,23 @@ export default {
|
|||||||
.dispatch('fetchAllConversations', this.conversationFilters)
|
.dispatch('fetchAllConversations', this.conversationFilters)
|
||||||
.then(() => this.$emit('conversation-load'));
|
.then(() => this.$emit('conversation-load'));
|
||||||
},
|
},
|
||||||
|
loadMoreConversations() {
|
||||||
|
if (!this.hasAppliedFilters) {
|
||||||
|
this.fetchConversations();
|
||||||
|
} else {
|
||||||
|
this.fetchFilteredConversations(this.appliedFilters);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fetchFilteredConversations(payload) {
|
||||||
|
let page = this.currentFiltersPage + 1;
|
||||||
|
this.$store
|
||||||
|
.dispatch('fetchFilteredConversations', {
|
||||||
|
queryData: filterQueryGenerator(payload),
|
||||||
|
page,
|
||||||
|
})
|
||||||
|
.then(() => this.$emit('conversation-load'));
|
||||||
|
this.showAdvancedFilters = false;
|
||||||
|
},
|
||||||
updateAssigneeTab(selectedTab) {
|
updateAssigneeTab(selectedTab) {
|
||||||
if (this.activeAssigneeTab !== selectedTab) {
|
if (this.activeAssigneeTab !== selectedTab) {
|
||||||
bus.$emit('clearSearchInput');
|
bus.$emit('clearSearchInput');
|
||||||
@ -273,6 +379,7 @@ export default {
|
|||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@import '~dashboard/assets/scss/woot';
|
@import '~dashboard/assets/scss/woot';
|
||||||
|
|
||||||
.spinner {
|
.spinner {
|
||||||
margin-top: var(--space-normal);
|
margin-top: var(--space-normal);
|
||||||
margin-bottom: var(--space-normal);
|
margin-bottom: var(--space-normal);
|
||||||
@ -295,4 +402,23 @@ export default {
|
|||||||
flex-basis: 46rem;
|
flex-basis: 46rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.filter--actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
.btn-filter {
|
||||||
|
cursor: pointer;
|
||||||
|
i {
|
||||||
|
font-size: var(--font-size-two);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.btn-clear-filters {
|
||||||
|
color: var(--r-500);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter__applied {
|
||||||
|
padding: var(--space-slab) 0 !important;
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -2,77 +2,113 @@
|
|||||||
<div class="custom-attribute">
|
<div class="custom-attribute">
|
||||||
<div class="title-wrap">
|
<div class="title-wrap">
|
||||||
<h4 class="text-block-title title error">
|
<h4 class="text-block-title title error">
|
||||||
<span class="attribute-name" :class="{ error: $v.editedValue.$error }">
|
<div v-if="isAttributeTypeCheckbox" class="checkbox-wrap">
|
||||||
{{ label }}
|
<input
|
||||||
</span>
|
v-model="editedValue"
|
||||||
|
class="checkbox"
|
||||||
|
type="checkbox"
|
||||||
|
@change="onUpdate"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="name-button__wrap">
|
||||||
|
<span
|
||||||
|
class="attribute-name"
|
||||||
|
:class="{ error: $v.editedValue.$error }"
|
||||||
|
>
|
||||||
|
{{ label }}
|
||||||
|
</span>
|
||||||
|
<woot-button
|
||||||
|
v-if="showActions"
|
||||||
|
v-tooltip.left="$t('CUSTOM_ATTRIBUTES.ACTIONS.DELETE')"
|
||||||
|
variant="link"
|
||||||
|
size="medium"
|
||||||
|
color-scheme="secondary"
|
||||||
|
icon="delete"
|
||||||
|
class-names="delete-button"
|
||||||
|
@click="onDelete"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="isEditing">
|
<div v-if="notAttributeTypeCheckboxAndList">
|
||||||
<div class="input-group small">
|
<div v-show="isEditing">
|
||||||
<input
|
<div class="input-group small">
|
||||||
ref="inputfield"
|
<input
|
||||||
v-model="editedValue"
|
ref="inputfield"
|
||||||
:type="inputType"
|
v-model="editedValue"
|
||||||
class="input-group-field"
|
:type="inputType"
|
||||||
autofocus="true"
|
class="input-group-field"
|
||||||
:class="{ error: $v.editedValue.$error }"
|
autofocus="true"
|
||||||
@blur="$v.editedValue.$touch"
|
:class="{ error: $v.editedValue.$error }"
|
||||||
@keyup.enter="onUpdate"
|
@blur="$v.editedValue.$touch"
|
||||||
/>
|
@keyup.enter="onUpdate"
|
||||||
<div class="input-group-button">
|
/>
|
||||||
<woot-button size="small" icon="ion-checkmark" @click="onUpdate" />
|
<div class="input-group-button">
|
||||||
|
<woot-button size="small" icon="checkmark" @click="onUpdate" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span v-if="shouldShowErrorMessage" class="error-message">
|
||||||
|
{{ errorMessage }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-show="!isEditing"
|
||||||
|
class="value--view"
|
||||||
|
:class="{ 'is-editable': showActions }"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
v-if="isAttributeTypeLink"
|
||||||
|
:href="value"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="value"
|
||||||
|
>
|
||||||
|
{{ value || '---' }}
|
||||||
|
</a>
|
||||||
|
<p v-else class="value">
|
||||||
|
{{ displayValue || '---' }}
|
||||||
|
</p>
|
||||||
|
<div class="action-buttons__wrap">
|
||||||
|
<woot-button
|
||||||
|
v-if="showActions"
|
||||||
|
v-tooltip="$t('CUSTOM_ATTRIBUTES.ACTIONS.COPY')"
|
||||||
|
variant="link"
|
||||||
|
size="small"
|
||||||
|
color-scheme="secondary"
|
||||||
|
icon="clipboard"
|
||||||
|
class-names="edit-button"
|
||||||
|
@click="onCopy"
|
||||||
|
/>
|
||||||
|
<woot-button
|
||||||
|
v-if="showActions"
|
||||||
|
v-tooltip.right="$t('CUSTOM_ATTRIBUTES.ACTIONS.EDIT')"
|
||||||
|
variant="link"
|
||||||
|
size="small"
|
||||||
|
color-scheme="secondary"
|
||||||
|
icon="edit"
|
||||||
|
class-names="edit-button"
|
||||||
|
@click="onEdit"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span v-if="shouldShowErrorMessage" class="error-message">
|
|
||||||
{{ errorMessage }}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div v-if="isAttributeTypeList">
|
||||||
v-show="!isEditing"
|
<multiselect-dropdown
|
||||||
class="value--view"
|
:options="listOptions"
|
||||||
:class="{ 'is-editable': showActions }"
|
:selected-item="selectedItem"
|
||||||
>
|
:has-thumbnail="false"
|
||||||
<a
|
:multiselector-placeholder="
|
||||||
v-if="isAttributeTypeLink"
|
$t('CUSTOM_ATTRIBUTES.FORM.ATTRIBUTE_TYPE.LIST.PLACEHOLDER')
|
||||||
:href="value"
|
"
|
||||||
target="_blank"
|
:no-search-result="
|
||||||
rel="noopener noreferrer"
|
$t('CUSTOM_ATTRIBUTES.FORM.ATTRIBUTE_TYPE.LIST.NO_RESULT')
|
||||||
class="value"
|
"
|
||||||
>
|
:input-placeholder="
|
||||||
{{ value || '---' }}
|
$t(
|
||||||
</a>
|
'CUSTOM_ATTRIBUTES.FORM.ATTRIBUTE_TYPE.LIST.SEARCH_INPUT_PLACEHOLDER'
|
||||||
<p v-else class="value">
|
)
|
||||||
{{ formattedValue || '---' }}
|
"
|
||||||
</p>
|
@click="onUpdateListValue"
|
||||||
<woot-button
|
|
||||||
v-if="showActions"
|
|
||||||
v-tooltip="$t('CUSTOM_ATTRIBUTES.ACTIONS.COPY')"
|
|
||||||
variant="link"
|
|
||||||
size="small"
|
|
||||||
color-scheme="secondary"
|
|
||||||
icon="ion-clipboard"
|
|
||||||
class-names="edit-button"
|
|
||||||
@click="onCopy"
|
|
||||||
/>
|
|
||||||
<woot-button
|
|
||||||
v-if="showActions"
|
|
||||||
v-tooltip="$t('CUSTOM_ATTRIBUTES.ACTIONS.EDIT')"
|
|
||||||
variant="link"
|
|
||||||
size="small"
|
|
||||||
color-scheme="secondary"
|
|
||||||
icon="ion-compose"
|
|
||||||
class-names="edit-button"
|
|
||||||
@click="onEdit"
|
|
||||||
/>
|
|
||||||
<woot-button
|
|
||||||
v-if="showActions"
|
|
||||||
v-tooltip="$t('CUSTOM_ATTRIBUTES.ACTIONS.DELETE')"
|
|
||||||
variant="link"
|
|
||||||
size="small"
|
|
||||||
color-scheme="secondary"
|
|
||||||
icon="ion-trash-a"
|
|
||||||
class-names="edit-button"
|
|
||||||
@click="onDelete"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -82,13 +118,18 @@
|
|||||||
import format from 'date-fns/format';
|
import format from 'date-fns/format';
|
||||||
import { required, url } from 'vuelidate/lib/validators';
|
import { required, url } from 'vuelidate/lib/validators';
|
||||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||||
|
import MultiselectDropdown from 'shared/components/ui/MultiselectDropdown.vue';
|
||||||
|
|
||||||
const DATE_FORMAT = 'yyyy-MM-dd';
|
const DATE_FORMAT = 'yyyy-MM-dd';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
components: {
|
||||||
|
MultiselectDropdown,
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
label: { type: String, required: true },
|
label: { type: String, required: true },
|
||||||
value: { type: [String, Number], default: '' },
|
values: { type: Array, default: () => [] },
|
||||||
|
value: { type: [String, Number, Boolean], default: '' },
|
||||||
showActions: { type: Boolean, default: false },
|
showActions: { type: Boolean, default: false },
|
||||||
attributeType: { type: String, default: 'text' },
|
attributeType: { type: String, default: 'text' },
|
||||||
attributeKey: { type: String, required: true },
|
attributeKey: { type: String, required: true },
|
||||||
@ -97,10 +138,7 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
isEditing: false,
|
isEditing: false,
|
||||||
editedValue:
|
editedValue: null,
|
||||||
this.attributeType === 'date'
|
|
||||||
? format(new Date(this.value || new Date()), DATE_FORMAT)
|
|
||||||
: this.value,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
validations() {
|
validations() {
|
||||||
@ -115,9 +153,40 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
formattedValue() {
|
||||||
|
if (this.isAttributeTypeDate) {
|
||||||
|
return format(new Date(this.value || new Date()), DATE_FORMAT);
|
||||||
|
}
|
||||||
|
if (this.isAttributeTypeCheckbox) {
|
||||||
|
return this.value === 'false' ? false : this.value;
|
||||||
|
}
|
||||||
|
return this.value;
|
||||||
|
},
|
||||||
|
listOptions() {
|
||||||
|
return this.values.map((value, index) => ({
|
||||||
|
id: index + 1,
|
||||||
|
name: value,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
selectedItem() {
|
||||||
|
const id = this.values.indexOf(this.editedValue) + 1;
|
||||||
|
return { id, name: this.editedValue };
|
||||||
|
},
|
||||||
|
isAttributeTypeCheckbox() {
|
||||||
|
return this.attributeType === 'checkbox';
|
||||||
|
},
|
||||||
|
isAttributeTypeList() {
|
||||||
|
return this.attributeType === 'list';
|
||||||
|
},
|
||||||
isAttributeTypeLink() {
|
isAttributeTypeLink() {
|
||||||
return this.attributeType === 'link';
|
return this.attributeType === 'link';
|
||||||
},
|
},
|
||||||
|
isAttributeTypeDate() {
|
||||||
|
return this.attributeType === 'date';
|
||||||
|
},
|
||||||
|
notAttributeTypeCheckboxAndList() {
|
||||||
|
return !this.isAttributeTypeCheckbox && !this.isAttributeTypeList;
|
||||||
|
},
|
||||||
inputType() {
|
inputType() {
|
||||||
return this.isAttributeTypeLink ? 'url' : this.attributeType;
|
return this.isAttributeTypeLink ? 'url' : this.attributeType;
|
||||||
},
|
},
|
||||||
@ -130,7 +199,7 @@ export default {
|
|||||||
}
|
}
|
||||||
return this.$t('CUSTOM_ATTRIBUTES.VALIDATIONS.REQUIRED');
|
return this.$t('CUSTOM_ATTRIBUTES.VALIDATIONS.REQUIRED');
|
||||||
},
|
},
|
||||||
formattedValue() {
|
displayValue() {
|
||||||
if (this.attributeType === 'date') {
|
if (this.attributeType === 'date') {
|
||||||
return format(new Date(this.editedValue), 'dd-MM-yyyy');
|
return format(new Date(this.editedValue), 'dd-MM-yyyy');
|
||||||
}
|
}
|
||||||
@ -138,6 +207,7 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
this.editedValue = this.formattedValue;
|
||||||
bus.$on(BUS_EVENTS.FOCUS_CUSTOM_ATTRIBUTE, focusAttributeKey => {
|
bus.$on(BUS_EVENTS.FOCUS_CUSTOM_ATTRIBUTE, focusAttributeKey => {
|
||||||
if (this.attributeKey === focusAttributeKey) {
|
if (this.attributeKey === focusAttributeKey) {
|
||||||
this.onEdit();
|
this.onEdit();
|
||||||
@ -156,6 +226,12 @@ export default {
|
|||||||
this.focusInput();
|
this.focusInput();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
onUpdateListValue(value) {
|
||||||
|
if (value) {
|
||||||
|
this.editedValue = value.name;
|
||||||
|
this.onUpdate();
|
||||||
|
}
|
||||||
|
},
|
||||||
onUpdate() {
|
onUpdate() {
|
||||||
const updatedValue =
|
const updatedValue =
|
||||||
this.attributeType === 'date'
|
this.attributeType === 'date'
|
||||||
@ -194,8 +270,23 @@ export default {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.checkbox-wrap {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.checkbox {
|
||||||
|
margin: 0 var(--space-small) 0 0;
|
||||||
|
}
|
||||||
|
.name-button__wrap {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
.attribute-name {
|
.attribute-name {
|
||||||
|
width: 100%;
|
||||||
&.error {
|
&.error {
|
||||||
color: var(--r-400);
|
color: var(--r-400);
|
||||||
}
|
}
|
||||||
@ -206,22 +297,34 @@ export default {
|
|||||||
.edit-button {
|
.edit-button {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
.delete-button {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
width: var(--space-normal);
|
||||||
|
}
|
||||||
.value--view {
|
.value--view {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
&.is-editable:hover {
|
&.is-editable:hover {
|
||||||
.value {
|
.value {
|
||||||
background: var(--color-background);
|
background: var(--color-background);
|
||||||
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
.edit-button {
|
.edit-button {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.action-buttons__wrap {
|
||||||
|
display: flex;
|
||||||
|
max-width: var(--space-larger);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.value {
|
.value {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
min-width: var(--space-mega);
|
min-width: var(--space-mega);
|
||||||
border-radius: var(--border-radius-small);
|
border-radius: var(--border-radius-small);
|
||||||
|
margin-bottom: 0;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
padding: var(--space-micro) var(--space-smaller);
|
padding: var(--space-micro) var(--space-smaller);
|
||||||
}
|
}
|
||||||
@ -235,4 +338,17 @@ export default {
|
|||||||
margin-top: -1.6rem;
|
margin-top: -1.6rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::v-deep {
|
||||||
|
.selector-wrap {
|
||||||
|
margin: 0;
|
||||||
|
top: var(--space-smaller);
|
||||||
|
.selector-name {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.name {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -7,7 +7,9 @@
|
|||||||
@click="onBackDropClick"
|
@click="onBackDropClick"
|
||||||
>
|
>
|
||||||
<div :class="modalContainerClassName" @click.stop>
|
<div :class="modalContainerClassName" @click.stop>
|
||||||
<i class="ion-android-close modal--close" @click="close"></i>
|
<button class="modal--close" @click="close">
|
||||||
|
<fluent-icon icon="dismiss" />
|
||||||
|
</button>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -34,6 +36,10 @@ export default {
|
|||||||
type: String,
|
type: String,
|
||||||
default: 'centered',
|
default: 'centered',
|
||||||
},
|
},
|
||||||
|
size: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
modalContainerClassName() {
|
modalContainerClassName() {
|
||||||
@ -41,7 +47,8 @@ export default {
|
|||||||
if (this.fullWidth) {
|
if (this.fullWidth) {
|
||||||
return `${className} modal-container--full-width`;
|
return `${className} modal-container--full-width`;
|
||||||
}
|
}
|
||||||
return className;
|
|
||||||
|
return `${className} ${this.size}`;
|
||||||
},
|
},
|
||||||
modalClassName() {
|
modalClassName() {
|
||||||
const modalClassNameMap = {
|
const modalClassNameMap = {
|
||||||
|
|||||||
@ -2,29 +2,21 @@
|
|||||||
<transition name="network-notification-fade" tag="div">
|
<transition name="network-notification-fade" tag="div">
|
||||||
<div v-show="showNotification" class="ui-notification-container">
|
<div v-show="showNotification" class="ui-notification-container">
|
||||||
<div class="ui-notification">
|
<div class="ui-notification">
|
||||||
<svg
|
<fluent-icon icon="wifi-off" />
|
||||||
class="ui-notification-icon"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M18.364 5.636a9 9 0 010 12.728m0 0l-2.829-2.829m2.829 2.829L21 21M15.536 8.464a5 5 0 010 7.072m0 0l-2.829-2.829m-4.243 2.829a4.978 4.978 0 01-1.414-2.83m-1.414 5.658a9 9 0 01-2.167-9.238m7.824 2.167a1 1 0 111.414 1.414m-1.414-1.414L3 3m8.293 8.293l1.414 1.414"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<p class="ui-notification-text">
|
<p class="ui-notification-text">
|
||||||
{{ $t('NETWORK.NOTIFICATION.TEXT') }}
|
{{ $t('NETWORK.NOTIFICATION.TEXT') }}
|
||||||
</p>
|
</p>
|
||||||
<button class="ui-refresh-button" @click="refreshPage">
|
<woot-button variant="clear" size="small" @click="refreshPage">
|
||||||
{{ $t('NETWORK.BUTTON.REFRESH') }}
|
{{ $t('NETWORK.BUTTON.REFRESH') }}
|
||||||
</button>
|
</woot-button>
|
||||||
<button class="ui-close-button" @click="closeNotification">
|
<woot-button
|
||||||
<i class="ui-close-icon icon ion-ios-close-outline" />
|
variant="clear"
|
||||||
</button>
|
size="small"
|
||||||
|
color-scheme="secondary"
|
||||||
|
icon="dismiss-circle"
|
||||||
|
@click="closeNotification"
|
||||||
|
>
|
||||||
|
</woot-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
@ -72,52 +64,24 @@ export default {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
right: var(--space-normal);
|
right: var(--space-normal);
|
||||||
top: var(--space-normal);
|
top: var(--space-normal);
|
||||||
width: 100%;
|
z-index: var(--z-index-very-high);
|
||||||
z-index: 9999;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ui-notification {
|
.ui-notification {
|
||||||
@include shadow;
|
|
||||||
align-items: center;
|
|
||||||
background-color: var(--white);
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
border-radius: var(--space-one);
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
max-width: 40rem;
|
align-items: center;
|
||||||
min-height: 3rem;
|
width: 100%;
|
||||||
|
|
||||||
|
background-color: var(--y-100);
|
||||||
|
border-radius: var(--border-radius-medium);
|
||||||
|
box-shadow: var(--shadow-large);
|
||||||
|
|
||||||
min-width: 24rem;
|
min-width: 24rem;
|
||||||
padding: var(--space-normal) var(--space-two);
|
padding: var(--space-normal);
|
||||||
text-align: left;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ui-notification-text {
|
.ui-notification-text {
|
||||||
margin: 0;
|
margin: 0 var(--space-small);
|
||||||
}
|
|
||||||
|
|
||||||
.ui-refresh-button {
|
|
||||||
color: var(--color-woot);
|
|
||||||
font-size: var(--font-size-small);
|
|
||||||
font-weight: bold;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.ui-notification-icon {
|
|
||||||
color: var(--b-600);
|
|
||||||
width: var(--font-size-mega);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ui-close-icon {
|
|
||||||
color: var(--b-600);
|
|
||||||
font-size: var(--font-size-large);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ui-close-button {
|
|
||||||
&:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,18 +1,28 @@
|
|||||||
<template>
|
<template>
|
||||||
<i class="ion-android-menu hamburger--menu" @click="onMenuItemClick" />
|
<button @click="onMenuItemClick">
|
||||||
|
<fluent-icon class="hamburger--menu" icon="list" />
|
||||||
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
methods: {
|
methods: {
|
||||||
onMenuItemClick() {
|
onMenuItemClick() {
|
||||||
bus.$emit('sidemenu_icon_click');
|
bus.$emit(BUS_EVENTS.TOGGLE_SIDEMENU);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style scoped lang="scss">
|
||||||
.ion-android-menu {
|
.hamburger--menu {
|
||||||
font-size: var(--font-size-big);
|
cursor: pointer;
|
||||||
|
display: none;
|
||||||
|
margin-right: var(--space-normal);
|
||||||
|
|
||||||
|
@media screen and (max-width: 1200px) {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,13 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="ui-snackbar">
|
<div class="ui-snackbar">
|
||||||
<div class="ui-snackbar-text">{{ message }}</div>
|
<div class="ui-snackbar-text">
|
||||||
|
{{ message }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
message: String,
|
message: String,
|
||||||
@ -22,9 +23,7 @@ export default {
|
|||||||
toggleAfterTimeout: false,
|
toggleAfterTimeout: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
methods: {
|
mounted() {},
|
||||||
},
|
methods: {},
|
||||||
mounted() {
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -29,12 +29,18 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
bus.$on('newToastMessage', message => {
|
bus.$on('newToastMessage', this.onNewToastMessage);
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
bus.$off('newToastMessage', this.onNewToastMessage);
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onNewToastMessage(message) {
|
||||||
this.snackMessages.push({ key: new Date().getTime(), message });
|
this.snackMessages.push({ key: new Date().getTime(), message });
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
this.snackMessages.splice(0, 1);
|
this.snackMessages.splice(0, 1);
|
||||||
}, this.duration);
|
}, this.duration);
|
||||||
});
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<button :type="type" class="button nice" :class="variant" @click="onClick">
|
<button :type="type" class="button nice" :class="variant" @click="onClick">
|
||||||
<i
|
<fluent-icon
|
||||||
v-if="!isLoading && icon"
|
v-if="!isLoading && icon"
|
||||||
class="icon"
|
class="icon"
|
||||||
:class="buttonIconClass + ' ' + icon"
|
:class="buttonIconClass"
|
||||||
|
:icon="icon"
|
||||||
/>
|
/>
|
||||||
<spinner v-if="isLoading" />
|
<spinner v-if="isLoading" />
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
:class="computedClass"
|
:class="computedClass"
|
||||||
@click="onClick"
|
@click="onClick"
|
||||||
>
|
>
|
||||||
<i v-if="!!iconClass" :class="iconClass" class="icon" />
|
<fluent-icon v-if="!!iconClass" :icon="iconClass" class="icon" />
|
||||||
<span>{{ buttonText }}</span>
|
<span>{{ buttonText }}</span>
|
||||||
<spinner v-if="loading" />
|
<spinner v-if="loading" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -5,8 +5,9 @@
|
|||||||
v-if="isOpen"
|
v-if="isOpen"
|
||||||
class-names="resolve"
|
class-names="resolve"
|
||||||
color-scheme="success"
|
color-scheme="success"
|
||||||
icon="ion-checkmark"
|
icon="checkmark"
|
||||||
emoji="✅"
|
emoji="✅"
|
||||||
|
icon-size="16"
|
||||||
:is-loading="isLoading"
|
:is-loading="isLoading"
|
||||||
@click="onCmdResolveConversation"
|
@click="onCmdResolveConversation"
|
||||||
>
|
>
|
||||||
@ -16,8 +17,9 @@
|
|||||||
v-else-if="isResolved"
|
v-else-if="isResolved"
|
||||||
class-names="resolve"
|
class-names="resolve"
|
||||||
color-scheme="warning"
|
color-scheme="warning"
|
||||||
icon="ion-refresh"
|
icon="arrow-redo"
|
||||||
emoji="👀"
|
emoji="👀"
|
||||||
|
icon-size="16"
|
||||||
:is-loading="isLoading"
|
:is-loading="isLoading"
|
||||||
@click="onCmdOpenConversation"
|
@click="onCmdOpenConversation"
|
||||||
>
|
>
|
||||||
@ -27,7 +29,8 @@
|
|||||||
v-else-if="showOpenButton"
|
v-else-if="showOpenButton"
|
||||||
class-names="resolve"
|
class-names="resolve"
|
||||||
color-scheme="primary"
|
color-scheme="primary"
|
||||||
icon="ion-person"
|
icon="person"
|
||||||
|
icon-size="16"
|
||||||
:is-loading="isLoading"
|
:is-loading="isLoading"
|
||||||
@click="onCmdOpenConversation"
|
@click="onCmdOpenConversation"
|
||||||
>
|
>
|
||||||
@ -38,7 +41,8 @@
|
|||||||
ref="arrowDownButton"
|
ref="arrowDownButton"
|
||||||
:color-scheme="buttonClass"
|
:color-scheme="buttonClass"
|
||||||
:disabled="isLoading"
|
:disabled="isLoading"
|
||||||
icon="ion-arrow-down-b"
|
icon="chevron-down"
|
||||||
|
icon-size="16"
|
||||||
emoji="🔽"
|
emoji="🔽"
|
||||||
@click="openDropdown"
|
@click="openDropdown"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1,66 +1,41 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="status">
|
<woot-dropdown-menu>
|
||||||
<div class="status-view">
|
<woot-dropdown-header :title="$t('SIDEBAR.SET_AVAILABILITY_TITLE')" />
|
||||||
<availability-status-badge :status="currentUserAvailability" />
|
<woot-dropdown-item
|
||||||
<div class="status-view--title">
|
v-for="status in availabilityStatuses"
|
||||||
{{ availabilityDisplayLabel }}
|
:key="status.value"
|
||||||
</div>
|
class="status-items"
|
||||||
</div>
|
>
|
||||||
|
|
||||||
<div class="status-change">
|
|
||||||
<transition name="menu-slide">
|
|
||||||
<div
|
|
||||||
v-if="isStatusMenuOpened"
|
|
||||||
v-on-clickaway="closeStatusMenu"
|
|
||||||
class="dropdown-pane dropdowm--top"
|
|
||||||
>
|
|
||||||
<woot-dropdown-menu>
|
|
||||||
<woot-dropdown-item
|
|
||||||
v-for="status in availabilityStatuses"
|
|
||||||
:key="status.value"
|
|
||||||
class="status-items"
|
|
||||||
>
|
|
||||||
<woot-button
|
|
||||||
variant="clear"
|
|
||||||
size="small"
|
|
||||||
color-scheme="secondary"
|
|
||||||
class-names="status-change--dropdown-button"
|
|
||||||
:is-disabled="status.disabled"
|
|
||||||
@click="
|
|
||||||
changeAvailabilityStatus(status.value, currentAccountId)
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<availability-status-badge :status="status.value" />
|
|
||||||
{{ status.label }}
|
|
||||||
</woot-button>
|
|
||||||
</woot-dropdown-item>
|
|
||||||
</woot-dropdown-menu>
|
|
||||||
</div>
|
|
||||||
</transition>
|
|
||||||
|
|
||||||
<woot-button
|
<woot-button
|
||||||
variant="clear"
|
size="small"
|
||||||
color-scheme="secondary"
|
:color-scheme="status.disabled ? '' : 'secondary'"
|
||||||
class-names="status-change--change-button link"
|
:variant="status.disabled ? 'smooth' : 'clear'"
|
||||||
@click="openStatusMenu"
|
class-names="status-change--dropdown-button"
|
||||||
|
@click="changeAvailabilityStatus(status.value)"
|
||||||
>
|
>
|
||||||
{{ $t('SIDEBAR_ITEMS.CHANGE_AVAILABILITY_STATUS') }}
|
<availability-status-badge :status="status.value" />
|
||||||
|
{{ status.label }}
|
||||||
</woot-button>
|
</woot-button>
|
||||||
</div>
|
</woot-dropdown-item>
|
||||||
</div>
|
<woot-dropdown-divider />
|
||||||
|
</woot-dropdown-menu>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import { mixin as clickaway } from 'vue-clickaway';
|
import { mixin as clickaway } from 'vue-clickaway';
|
||||||
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
|
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem';
|
||||||
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
|
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 AvailabilityStatusBadge from '../widgets/conversation/AvailabilityStatusBadge';
|
||||||
|
|
||||||
const AVAILABILITY_STATUS_KEYS = ['online', 'busy', 'offline'];
|
const AVAILABILITY_STATUS_KEYS = ['online', 'busy', 'offline'];
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
WootDropdownHeader,
|
||||||
|
WootDropdownDivider,
|
||||||
WootDropdownMenu,
|
WootDropdownMenu,
|
||||||
WootDropdownItem,
|
WootDropdownItem,
|
||||||
AvailabilityStatusBadge,
|
AvailabilityStatusBadge,
|
||||||
@ -100,8 +75,7 @@ export default {
|
|||||||
label: statusLabel,
|
label: statusLabel,
|
||||||
value: AVAILABILITY_STATUS_KEYS[index],
|
value: AVAILABILITY_STATUS_KEYS[index],
|
||||||
disabled:
|
disabled:
|
||||||
this.currentUserAvailability ===
|
this.currentUserAvailability === AVAILABILITY_STATUS_KEYS[index],
|
||||||
AVAILABILITY_STATUS_KEYS[index],
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -114,7 +88,8 @@ export default {
|
|||||||
closeStatusMenu() {
|
closeStatusMenu() {
|
||||||
this.isStatusMenuOpened = false;
|
this.isStatusMenuOpened = false;
|
||||||
},
|
},
|
||||||
changeAvailabilityStatus(availability, accountId) {
|
changeAvailabilityStatus(availability) {
|
||||||
|
const accountId = this.currentAccountId;
|
||||||
if (this.isUpdating) {
|
if (this.isUpdating) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,77 +1,37 @@
|
|||||||
<template>
|
<template>
|
||||||
<aside class="sidebar animated shrink columns">
|
<aside class="woot-sidebar">
|
||||||
<div class="logo">
|
<primary-sidebar
|
||||||
<router-link :to="dashboardPath" replace>
|
:logo-source="globalConfig.logo"
|
||||||
<img :src="globalConfig.logo" :alt="globalConfig.installationName" />
|
:installation-name="globalConfig.installationName"
|
||||||
</router-link>
|
:account-id="accountId"
|
||||||
</div>
|
:menu-items="primaryMenuItems"
|
||||||
|
:active-menu-item="activePrimaryMenu.key"
|
||||||
<div class="main-nav">
|
@toggle-accounts="toggleAccountModal"
|
||||||
<transition-group name="menu-list" tag="ul" class="menu vertical">
|
@key-shortcut-modal="toggleKeyShortcutModal"
|
||||||
<sidebar-item
|
/>
|
||||||
v-for="item in accessibleMenuItems"
|
<secondary-sidebar
|
||||||
:key="item.toState"
|
:account-id="accountId"
|
||||||
:menu-item="item"
|
:inboxes="inboxes"
|
||||||
/>
|
:labels="labels"
|
||||||
<sidebar-item
|
:teams="teams"
|
||||||
v-if="shouldShowTeams"
|
:menu-config="activeSecondaryMenu"
|
||||||
:key="teamSection.toState"
|
:current-role="currentRole"
|
||||||
:menu-item="teamSection"
|
@add-label="showAddLabelPopup"
|
||||||
/>
|
/>
|
||||||
<sidebar-item
|
|
||||||
v-if="shouldShowSidebarItem"
|
|
||||||
:key="inboxSection.toState"
|
|
||||||
:menu-item="inboxSection"
|
|
||||||
/>
|
|
||||||
<sidebar-item
|
|
||||||
v-if="shouldShowSidebarItem"
|
|
||||||
:key="labelSection.toState"
|
|
||||||
:menu-item="labelSection"
|
|
||||||
@add-label="showAddLabelPopup"
|
|
||||||
/>
|
|
||||||
<sidebar-item
|
|
||||||
v-if="showShowContactSideMenu"
|
|
||||||
:key="contactLabelSection.key"
|
|
||||||
:menu-item="contactLabelSection"
|
|
||||||
@add-label="showAddLabelPopup"
|
|
||||||
/>
|
|
||||||
</transition-group>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bottom-nav">
|
|
||||||
<availability-status />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bottom-nav app-context-menu" @click="toggleOptions">
|
|
||||||
<agent-details @show-options="toggleOptions" />
|
|
||||||
<notification-bell />
|
|
||||||
<span class="current-user--options icon ion-android-more-vertical" />
|
|
||||||
<options-menu
|
|
||||||
:show="showOptionsMenu"
|
|
||||||
@toggle-accounts="toggleAccountModal"
|
|
||||||
@show-support-chat-window="toggleSupportChatWindow"
|
|
||||||
@key-shortcut-modal="toggleKeyShortcutModal"
|
|
||||||
@close="toggleOptions"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<woot-key-shortcut-modal
|
<woot-key-shortcut-modal
|
||||||
v-if="showShortcutModal"
|
v-if="showShortcutModal"
|
||||||
@close="closeKeyShortcutModal"
|
@close="closeKeyShortcutModal"
|
||||||
@clickaway="closeKeyShortcutModal"
|
@clickaway="closeKeyShortcutModal"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<account-selector
|
<account-selector
|
||||||
:show-account-modal="showAccountModal"
|
:show-account-modal="showAccountModal"
|
||||||
@close-account-modal="toggleAccountModal"
|
@close-account-modal="toggleAccountModal"
|
||||||
@show-create-account-modal="openCreateAccountModal"
|
@show-create-account-modal="openCreateAccountModal"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<add-account-modal
|
<add-account-modal
|
||||||
:show="showCreateAccountModal"
|
:show="showCreateAccountModal"
|
||||||
@close-account-create-modal="closeCreateAccountModal"
|
@close-account-create-modal="closeCreateAccountModal"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<woot-modal :show.sync="showAddLabelModal" :on-close="hideAddLabelPopup">
|
<woot-modal :show.sync="showAddLabelModal" :on-close="hideAddLabelPopup">
|
||||||
<add-label-modal @close="hideAddLabelPopup" />
|
<add-label-modal @close="hideAddLabelPopup" />
|
||||||
</woot-modal>
|
</woot-modal>
|
||||||
@ -82,17 +42,14 @@
|
|||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
|
|
||||||
import adminMixin from '../../mixins/isAdmin';
|
import adminMixin from '../../mixins/isAdmin';
|
||||||
import SidebarItem from './SidebarItem';
|
import { getSidebarItems } from './config/default-sidebar';
|
||||||
import AvailabilityStatus from './AvailabilityStatus';
|
|
||||||
import { frontendURL } from '../../helper/URLHelper';
|
|
||||||
import { getSidebarItems } from '../../i18n/default-sidebar';
|
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
import NotificationBell from './sidebarComponents/NotificationBell';
|
|
||||||
import AgentDetails from './sidebarComponents/AgentDetails.vue';
|
|
||||||
import OptionsMenu from './sidebarComponents/OptionsMenu.vue';
|
|
||||||
import AccountSelector from './sidebarComponents/AccountSelector.vue';
|
import AccountSelector from './sidebarComponents/AccountSelector.vue';
|
||||||
import AddAccountModal from './sidebarComponents/AddAccountModal.vue';
|
import AddAccountModal from './sidebarComponents/AddAccountModal.vue';
|
||||||
import AddLabelModal from '../../routes/dashboard/settings/labels/AddLabel';
|
import AddLabelModal from '../../routes/dashboard/settings/labels/AddLabel';
|
||||||
|
import PrimarySidebar from './sidebarComponents/Primary';
|
||||||
|
import SecondarySidebar from './sidebarComponents/Secondary';
|
||||||
import WootKeyShortcutModal from 'components/widgets/modal/WootKeyShortcutModal';
|
import WootKeyShortcutModal from 'components/widgets/modal/WootKeyShortcutModal';
|
||||||
import {
|
import {
|
||||||
hasPressedAltAndCKey,
|
hasPressedAltAndCKey,
|
||||||
@ -107,14 +64,11 @@ import router from '../../routes';
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
AgentDetails,
|
|
||||||
SidebarItem,
|
|
||||||
AvailabilityStatus,
|
|
||||||
NotificationBell,
|
|
||||||
OptionsMenu,
|
|
||||||
AccountSelector,
|
AccountSelector,
|
||||||
AddAccountModal,
|
AddAccountModal,
|
||||||
AddLabelModal,
|
AddLabelModal,
|
||||||
|
PrimarySidebar,
|
||||||
|
SecondarySidebar,
|
||||||
WootKeyShortcutModal,
|
WootKeyShortcutModal,
|
||||||
},
|
},
|
||||||
mixins: [adminMixin, alertMixin, eventListenerMixins],
|
mixins: [adminMixin, alertMixin, eventListenerMixins],
|
||||||
@ -135,129 +89,34 @@ export default {
|
|||||||
inboxes: 'inboxes/getInboxes',
|
inboxes: 'inboxes/getInboxes',
|
||||||
accountId: 'getCurrentAccountId',
|
accountId: 'getCurrentAccountId',
|
||||||
currentRole: 'getCurrentRole',
|
currentRole: 'getCurrentRole',
|
||||||
accountLabels: 'labels/getLabelsOnSidebar',
|
labels: 'labels/getLabelsOnSidebar',
|
||||||
teams: 'teams/getMyTeams',
|
teams: 'teams/getMyTeams',
|
||||||
}),
|
}),
|
||||||
|
sideMenuConfig() {
|
||||||
sidemenuItems() {
|
|
||||||
return getSidebarItems(this.accountId);
|
return getSidebarItems(this.accountId);
|
||||||
},
|
},
|
||||||
accessibleMenuItems() {
|
primaryMenuItems() {
|
||||||
// get all keys in menuGroup
|
const menuItems = this.sideMenuConfig.primaryMenu;
|
||||||
const groupKey = Object.keys(this.sidemenuItems);
|
return menuItems.filter(menuItem =>
|
||||||
|
menuItem.roles.includes(this.currentRole)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
activeSecondaryMenu() {
|
||||||
|
const { secondaryMenu } = this.sideMenuConfig;
|
||||||
|
const { name: currentRoute } = this.$route;
|
||||||
|
|
||||||
let menuItems = [];
|
const activeSecondaryMenu =
|
||||||
// Iterate over menuGroup to find the correct group
|
secondaryMenu.find(menuItem =>
|
||||||
for (let i = 0; i < groupKey.length; i += 1) {
|
menuItem.routes.includes(currentRoute)
|
||||||
const groupItem = this.sidemenuItems[groupKey[i]];
|
) || {};
|
||||||
// Check if current route is included
|
return activeSecondaryMenu;
|
||||||
const isRouteIncluded = groupItem.routes.includes(this.currentRoute);
|
|
||||||
if (isRouteIncluded) {
|
|
||||||
menuItems = Object.values(groupItem.menuItems);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.filterMenuItemsByRole(menuItems);
|
|
||||||
},
|
},
|
||||||
currentRoute() {
|
activePrimaryMenu() {
|
||||||
return this.$store.state.route.name;
|
const activePrimaryMenu =
|
||||||
},
|
this.primaryMenuItems.find(
|
||||||
shouldShowSidebarItem() {
|
menuItem => menuItem.key === this.activeSecondaryMenu.parentNav
|
||||||
return this.sidemenuItems.common.routes.includes(this.currentRoute);
|
) || {};
|
||||||
},
|
return activePrimaryMenu;
|
||||||
showShowContactSideMenu() {
|
|
||||||
return this.sidemenuItems.contacts.routes.includes(this.currentRoute);
|
|
||||||
},
|
|
||||||
shouldShowTeams() {
|
|
||||||
return this.shouldShowSidebarItem && this.teams.length;
|
|
||||||
},
|
|
||||||
inboxSection() {
|
|
||||||
return {
|
|
||||||
icon: 'ion-folder',
|
|
||||||
label: 'INBOXES',
|
|
||||||
hasSubMenu: true,
|
|
||||||
newLink: true,
|
|
||||||
key: 'inbox',
|
|
||||||
cssClass: 'menu-title align-justify',
|
|
||||||
toState: frontendURL(`accounts/${this.accountId}/settings/inboxes`),
|
|
||||||
toStateName: 'settings_inbox_list',
|
|
||||||
newLinkRouteName: 'settings_inbox_new',
|
|
||||||
children: this.inboxes.map(inbox => ({
|
|
||||||
id: inbox.id,
|
|
||||||
label: inbox.name,
|
|
||||||
toState: frontendURL(`accounts/${this.accountId}/inbox/${inbox.id}`),
|
|
||||||
type: inbox.channel_type,
|
|
||||||
phoneNumber: inbox.phone_number,
|
|
||||||
})),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
labelSection() {
|
|
||||||
return {
|
|
||||||
icon: 'ion-pound',
|
|
||||||
label: 'LABELS',
|
|
||||||
hasSubMenu: true,
|
|
||||||
newLink: true,
|
|
||||||
key: 'label',
|
|
||||||
cssClass: 'menu-title align-justify',
|
|
||||||
toState: frontendURL(`accounts/${this.accountId}/settings/labels`),
|
|
||||||
toStateName: 'labels_list',
|
|
||||||
showModalForNewItem: true,
|
|
||||||
modalName: 'AddLabel',
|
|
||||||
children: this.accountLabels.map(label => ({
|
|
||||||
id: label.id,
|
|
||||||
label: label.title,
|
|
||||||
color: label.color,
|
|
||||||
truncateLabel: true,
|
|
||||||
toState: frontendURL(
|
|
||||||
`accounts/${this.accountId}/label/${label.title}`
|
|
||||||
),
|
|
||||||
})),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
contactLabelSection() {
|
|
||||||
return {
|
|
||||||
icon: 'ion-pound',
|
|
||||||
label: 'TAGGED_WITH',
|
|
||||||
hasSubMenu: true,
|
|
||||||
key: 'label',
|
|
||||||
newLink: false,
|
|
||||||
cssClass: 'menu-title align-justify',
|
|
||||||
toState: frontendURL(`accounts/${this.accountId}/settings/labels`),
|
|
||||||
toStateName: 'labels_list',
|
|
||||||
showModalForNewItem: true,
|
|
||||||
modalName: 'AddLabel',
|
|
||||||
children: this.accountLabels.map(label => ({
|
|
||||||
id: label.id,
|
|
||||||
label: label.title,
|
|
||||||
color: label.color,
|
|
||||||
truncateLabel: true,
|
|
||||||
toState: frontendURL(
|
|
||||||
`accounts/${this.accountId}/labels/${label.title}/contacts`
|
|
||||||
),
|
|
||||||
})),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
teamSection() {
|
|
||||||
return {
|
|
||||||
icon: 'ion-ios-people',
|
|
||||||
label: 'TEAMS',
|
|
||||||
hasSubMenu: true,
|
|
||||||
newLink: true,
|
|
||||||
key: 'team',
|
|
||||||
cssClass: 'menu-title align-justify teams-sidebar-menu',
|
|
||||||
toState: frontendURL(`accounts/${this.accountId}/settings/teams`),
|
|
||||||
toStateName: 'teams_list',
|
|
||||||
newLinkRouteName: 'settings_teams_new',
|
|
||||||
children: this.teams.map(team => ({
|
|
||||||
id: team.id,
|
|
||||||
label: team.name,
|
|
||||||
truncateLabel: true,
|
|
||||||
toState: frontendURL(`accounts/${this.accountId}/team/${team.id}`),
|
|
||||||
})),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
dashboardPath() {
|
|
||||||
return frontendURL(`accounts/${this.accountId}/dashboard`);
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
@ -302,25 +161,11 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
isCurrentRouteSameAsNavigation(routeName) {
|
isCurrentRouteSameAsNavigation(routeName) {
|
||||||
return router.currentRoute && router.currentRoute.name === routeName;
|
return this.$route.name === routeName;
|
||||||
},
|
},
|
||||||
toggleSupportChatWindow() {
|
toggleSupportChatWindow() {
|
||||||
window.$chatwoot.toggle();
|
window.$chatwoot.toggle();
|
||||||
},
|
},
|
||||||
filterMenuItemsByRole(menuItems) {
|
|
||||||
if (!this.currentRole) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return menuItems.filter(
|
|
||||||
menuItem =>
|
|
||||||
window.roleWiseRoutes[this.currentRole].indexOf(
|
|
||||||
menuItem.toStateName
|
|
||||||
) > -1
|
|
||||||
);
|
|
||||||
},
|
|
||||||
toggleOptions() {
|
|
||||||
this.showOptionsMenu = !this.showOptionsMenu;
|
|
||||||
},
|
|
||||||
toggleAccountModal() {
|
toggleAccountModal() {
|
||||||
this.showAccountModal = !this.showAccountModal;
|
this.showAccountModal = !this.showAccountModal;
|
||||||
},
|
},
|
||||||
@ -341,6 +186,23 @@ export default {
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.woot-sidebar {
|
||||||
|
background: var(--white);
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondary-menu {
|
||||||
|
background: var(--white);
|
||||||
|
border-right: 1px solid var(--s-50);
|
||||||
|
height: 100vh;
|
||||||
|
width: 19rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
overflow: auto;
|
||||||
|
padding: var(--space-small);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import '~dashboard/assets/scss/variables';
|
@import '~dashboard/assets/scss/variables';
|
||||||
|
|
||||||
@ -354,11 +216,11 @@ export default {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: $space-small $space-large;
|
padding: $space-small $space-large;
|
||||||
|
|
||||||
.ion-ios-checkmark {
|
.selected--account {
|
||||||
font-size: $font-size-big;
|
margin-top: -$space-smaller;
|
||||||
|
|
||||||
& + .account--details {
|
& + .account--details {
|
||||||
padding-left: $space-normal;
|
padding-left: $space-normal - $space-micro;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -405,7 +267,7 @@ export default {
|
|||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.teams-sidebar-menu + .nested.vertical.menu {
|
.secondary-menu .nested.vertical.menu {
|
||||||
padding-left: calc(var(--space-medium) - var(--space-one));
|
margin-left: var(--space-small);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,154 +0,0 @@
|
|||||||
<template>
|
|
||||||
<router-link
|
|
||||||
:to="menuItem.toState"
|
|
||||||
tag="li"
|
|
||||||
active-class="active"
|
|
||||||
:class="computedClass"
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
class="sub-menu-title"
|
|
||||||
:class="getMenuItemClass"
|
|
||||||
data-tooltip
|
|
||||||
aria-haspopup="true"
|
|
||||||
:title="menuItem.toolTip"
|
|
||||||
>
|
|
||||||
<div class="wrap">
|
|
||||||
<i :class="menuItem.icon" />
|
|
||||||
{{ $t(`SIDEBAR.${menuItem.label}`) }}
|
|
||||||
</div>
|
|
||||||
<span
|
|
||||||
v-if="showItem(menuItem)"
|
|
||||||
class="child-icon ion-android-add-circle"
|
|
||||||
@click.prevent="newLinkClick(menuItem)"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
<ul v-if="menuItem.hasSubMenu" class="nested vertical menu">
|
|
||||||
<router-link
|
|
||||||
v-for="child in menuItem.children"
|
|
||||||
:key="child.id"
|
|
||||||
active-class="active flex-container"
|
|
||||||
tag="li"
|
|
||||||
:to="child.toState"
|
|
||||||
>
|
|
||||||
<a href="#" :class="computedChildClass(child)">
|
|
||||||
<div class="wrap">
|
|
||||||
<i
|
|
||||||
v-if="menuItem.key === 'inbox'"
|
|
||||||
class="inbox-icon"
|
|
||||||
:class="computedInboxClass(child)"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
v-if="child.color"
|
|
||||||
class="label-color--display"
|
|
||||||
:style="{ backgroundColor: child.color }"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
:title="computedChildTitle(child)"
|
|
||||||
:class="computedChildClass(child)"
|
|
||||||
>
|
|
||||||
{{ child.label }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</router-link>
|
|
||||||
</ul>
|
|
||||||
</router-link>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { mapGetters } from 'vuex';
|
|
||||||
|
|
||||||
import router from '../../routes';
|
|
||||||
import adminMixin from '../../mixins/isAdmin';
|
|
||||||
import { getInboxClassByType } from 'dashboard/helper/inbox';
|
|
||||||
export default {
|
|
||||||
mixins: [adminMixin],
|
|
||||||
props: {
|
|
||||||
menuItem: {
|
|
||||||
type: Object,
|
|
||||||
default() {
|
|
||||||
return {};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
...mapGetters({
|
|
||||||
activeInbox: 'getSelectedInbox',
|
|
||||||
}),
|
|
||||||
getMenuItemClass() {
|
|
||||||
return this.menuItem.cssClass
|
|
||||||
? `side-menu ${this.menuItem.cssClass}`
|
|
||||||
: 'side-menu';
|
|
||||||
},
|
|
||||||
computedClass() {
|
|
||||||
// If active Inbox is present
|
|
||||||
// donot highlight conversations
|
|
||||||
if (this.activeInbox) return ' ';
|
|
||||||
|
|
||||||
if (
|
|
||||||
this.$store.state.route.name === 'inbox_conversation' &&
|
|
||||||
this.menuItem.toStateName === 'home'
|
|
||||||
) {
|
|
||||||
return 'active';
|
|
||||||
}
|
|
||||||
return ' ';
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
computedInboxClass(child) {
|
|
||||||
const { type, phoneNumber } = child;
|
|
||||||
const classByType = getInboxClassByType(type, phoneNumber);
|
|
||||||
return classByType;
|
|
||||||
},
|
|
||||||
computedChildClass(child) {
|
|
||||||
if (!child.truncateLabel) return '';
|
|
||||||
return 'text-truncate';
|
|
||||||
},
|
|
||||||
computedChildTitle(child) {
|
|
||||||
if (!child.truncateLabel) return false;
|
|
||||||
return child.label;
|
|
||||||
},
|
|
||||||
newLinkClick(item) {
|
|
||||||
if (item.newLinkRouteName) {
|
|
||||||
router.push({ name: item.newLinkRouteName, params: { page: 'new' } });
|
|
||||||
} else if (item.showModalForNewItem) {
|
|
||||||
if (item.modalName === 'AddLabel') {
|
|
||||||
this.$emit('add-label');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
showItem(item) {
|
|
||||||
return this.isAdmin && item.newLink !== undefined;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import '~dashboard/assets/scss/variables';
|
|
||||||
|
|
||||||
.sub-menu-title {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wrap {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.label-color--display {
|
|
||||||
border-radius: $space-smaller;
|
|
||||||
height: $space-normal;
|
|
||||||
margin-right: $space-small;
|
|
||||||
min-width: $space-normal;
|
|
||||||
width: $space-normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inbox-icon {
|
|
||||||
position: relative;
|
|
||||||
top: -1px;
|
|
||||||
&.ion-ios-email {
|
|
||||||
font-size: var(--font-size-medium);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
import conversations from './sidebarItems/conversations';
|
||||||
|
import contacts from './sidebarItems/contacts';
|
||||||
|
import reports from './sidebarItems/reports';
|
||||||
|
import campaigns from './sidebarItems/campaigns';
|
||||||
|
import settings from './sidebarItems/settings';
|
||||||
|
import notifications from './sidebarItems/notifications';
|
||||||
|
import primaryMenu from './sidebarItems/primaryMenu';
|
||||||
|
|
||||||
|
export const getSidebarItems = accountId => ({
|
||||||
|
primaryMenu: primaryMenu(accountId),
|
||||||
|
secondaryMenu: [
|
||||||
|
conversations(accountId),
|
||||||
|
contacts(accountId),
|
||||||
|
reports(accountId),
|
||||||
|
campaigns(accountId),
|
||||||
|
settings(accountId),
|
||||||
|
notifications(accountId),
|
||||||
|
],
|
||||||
|
});
|
||||||
@ -1,30 +1,26 @@
|
|||||||
import { frontendURL } from '../../helper/URLHelper';
|
import { frontendURL } from '../../../../helper/URLHelper';
|
||||||
|
|
||||||
const campaigns = accountId => ({
|
const campaigns = accountId => ({
|
||||||
|
parentNav: 'campaigns',
|
||||||
routes: ['settings_account_campaigns', 'one_off'],
|
routes: ['settings_account_campaigns', 'one_off'],
|
||||||
menuItems: {
|
menuItems: [
|
||||||
back: {
|
{
|
||||||
icon: 'ion-ios-arrow-back',
|
icon: 'arrow-swap',
|
||||||
label: 'HOME',
|
|
||||||
hasSubMenu: false,
|
|
||||||
toStateName: 'home',
|
|
||||||
toState: frontendURL(`accounts/${accountId}/dashboard`),
|
|
||||||
},
|
|
||||||
ongoingCampaigns: {
|
|
||||||
icon: 'ion-arrow-swap',
|
|
||||||
label: 'ONGOING',
|
label: 'ONGOING',
|
||||||
|
key: 'ongoingCampaigns',
|
||||||
hasSubMenu: false,
|
hasSubMenu: false,
|
||||||
toState: frontendURL(`accounts/${accountId}/campaigns/ongoing`),
|
toState: frontendURL(`accounts/${accountId}/campaigns/ongoing`),
|
||||||
toStateName: 'settings_account_campaigns',
|
toStateName: 'settings_account_campaigns',
|
||||||
},
|
},
|
||||||
onOffCampaigns: {
|
{
|
||||||
icon: 'ion-radio-waves',
|
key: 'oneOffCampaigns',
|
||||||
|
icon: 'sound-source',
|
||||||
label: 'ONE_OFF',
|
label: 'ONE_OFF',
|
||||||
hasSubMenu: false,
|
hasSubMenu: false,
|
||||||
toState: frontendURL(`accounts/${accountId}/campaigns/one_off`),
|
toState: frontendURL(`accounts/${accountId}/campaigns/one_off`),
|
||||||
toStateName: 'one_off',
|
toStateName: 'one_off',
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
export default campaigns;
|
export default campaigns;
|
||||||
@ -1,27 +1,21 @@
|
|||||||
import { frontendURL } from '../../helper/URLHelper';
|
import { frontendURL } from '../../../../helper/URLHelper';
|
||||||
|
|
||||||
const contacts = accountId => ({
|
const contacts = accountId => ({
|
||||||
|
parentNav: 'contacts',
|
||||||
routes: [
|
routes: [
|
||||||
'contacts_dashboard',
|
'contacts_dashboard',
|
||||||
'contact_profile_dashboard',
|
'contact_profile_dashboard',
|
||||||
'contacts_labels_dashboard',
|
'contacts_labels_dashboard',
|
||||||
],
|
],
|
||||||
menuItems: {
|
menuItems: [
|
||||||
back: {
|
{
|
||||||
icon: 'ion-ios-arrow-back',
|
icon: 'contact-card-group',
|
||||||
label: 'HOME',
|
|
||||||
hasSubMenu: false,
|
|
||||||
toStateName: 'home',
|
|
||||||
toState: frontendURL(`accounts/${accountId}/dashboard`),
|
|
||||||
},
|
|
||||||
contacts: {
|
|
||||||
icon: 'ion-person',
|
|
||||||
label: 'ALL_CONTACTS',
|
label: 'ALL_CONTACTS',
|
||||||
hasSubMenu: false,
|
hasSubMenu: false,
|
||||||
toState: frontendURL(`accounts/${accountId}/contacts`),
|
toState: frontendURL(`accounts/${accountId}/contacts`),
|
||||||
toStateName: 'contacts_dashboard',
|
toStateName: 'contacts_dashboard',
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
export default contacts;
|
export default contacts;
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
import { frontendURL } from '../../../../helper/URLHelper';
|
||||||
|
|
||||||
|
const conversations = accountId => ({
|
||||||
|
parentNav: 'conversations',
|
||||||
|
routes: [
|
||||||
|
'home',
|
||||||
|
'inbox_dashboard',
|
||||||
|
'inbox_conversation',
|
||||||
|
'conversation_through_inbox',
|
||||||
|
'notifications_dashboard',
|
||||||
|
'label_conversations',
|
||||||
|
'conversations_through_label',
|
||||||
|
'team_conversations',
|
||||||
|
'conversations_through_team',
|
||||||
|
'conversation_mentions',
|
||||||
|
'conversation_through_mentions',
|
||||||
|
],
|
||||||
|
menuItems: [
|
||||||
|
{
|
||||||
|
icon: 'chat',
|
||||||
|
label: 'ALL_CONVERSATIONS',
|
||||||
|
key: 'conversations',
|
||||||
|
toState: frontendURL(`accounts/${accountId}/dashboard`),
|
||||||
|
toolTip: 'Conversation from all subscribed inboxes',
|
||||||
|
toStateName: 'home',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'mention',
|
||||||
|
label: 'MENTIONED_CONVERSATIONS',
|
||||||
|
key: 'conversation_mentions',
|
||||||
|
toState: frontendURL(`accounts/${accountId}/mentions/conversations`),
|
||||||
|
toStateName: 'conversation_mentions',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
export default conversations;
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
const notifications = () => ({
|
||||||
|
parentNav: 'notifications',
|
||||||
|
routes: ['notifications_index'],
|
||||||
|
menuItems: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
export default notifications;
|
||||||
@ -0,0 +1,46 @@
|
|||||||
|
import { frontendURL } from '../../../../helper/URLHelper';
|
||||||
|
|
||||||
|
const primaryMenuItems = accountId => [
|
||||||
|
{
|
||||||
|
icon: 'chat',
|
||||||
|
key: 'conversations',
|
||||||
|
label: 'CONVERSATIONS',
|
||||||
|
toState: frontendURL(`accounts/${accountId}/dashboard`),
|
||||||
|
toStateName: 'home',
|
||||||
|
roles: ['administrator', 'agent'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'book-contacts',
|
||||||
|
key: 'contacts',
|
||||||
|
label: 'CONTACTS',
|
||||||
|
toState: frontendURL(`accounts/${accountId}/contacts`),
|
||||||
|
toStateName: 'contacts_dashboard',
|
||||||
|
roles: ['administrator', 'agent'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'arrow-trending-lines',
|
||||||
|
key: 'reports',
|
||||||
|
label: 'REPORTS',
|
||||||
|
toState: frontendURL(`accounts/${accountId}/reports`),
|
||||||
|
toStateName: 'settings_account_reports',
|
||||||
|
roles: ['administrator'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'megaphone',
|
||||||
|
key: 'campaigns',
|
||||||
|
label: 'CAMPAIGNS',
|
||||||
|
toState: frontendURL(`accounts/${accountId}/campaigns`),
|
||||||
|
toStateName: 'settings_account_campaigns',
|
||||||
|
roles: ['administrator'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'settings',
|
||||||
|
key: 'settings',
|
||||||
|
label: 'SETTINGS',
|
||||||
|
toState: frontendURL(`accounts/${accountId}/settings`),
|
||||||
|
toStateName: 'settings_home',
|
||||||
|
roles: ['administrator', 'agent'],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default primaryMenuItems;
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
const profileSettings = () => ({
|
||||||
|
parentNav: 'profileSettings',
|
||||||
|
routes: ['profile_settings_index'],
|
||||||
|
menuItems: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
export default profileSettings;
|
||||||
@ -1,6 +1,7 @@
|
|||||||
import { frontendURL } from '../../helper/URLHelper';
|
import { frontendURL } from '../../../../helper/URLHelper';
|
||||||
|
|
||||||
const reports = accountId => ({
|
const reports = accountId => ({
|
||||||
|
parentNav: 'reports',
|
||||||
routes: [
|
routes: [
|
||||||
'settings_account_reports',
|
'settings_account_reports',
|
||||||
'csat_reports',
|
'csat_reports',
|
||||||
@ -9,57 +10,50 @@ const reports = accountId => ({
|
|||||||
'inbox_reports',
|
'inbox_reports',
|
||||||
'team_reports',
|
'team_reports',
|
||||||
],
|
],
|
||||||
menuItems: {
|
menuItems: [
|
||||||
back: {
|
{
|
||||||
icon: 'ion-ios-arrow-back',
|
icon: 'arrow-trending-lines',
|
||||||
label: 'HOME',
|
|
||||||
hasSubMenu: false,
|
|
||||||
toStateName: 'home',
|
|
||||||
toState: frontendURL(`accounts/${accountId}/dashboard`),
|
|
||||||
},
|
|
||||||
reportOverview: {
|
|
||||||
icon: 'ion-arrow-graph-up-right',
|
|
||||||
label: 'REPORTS_OVERVIEW',
|
label: 'REPORTS_OVERVIEW',
|
||||||
hasSubMenu: false,
|
hasSubMenu: false,
|
||||||
toState: frontendURL(`accounts/${accountId}/reports/overview`),
|
toState: frontendURL(`accounts/${accountId}/reports/overview`),
|
||||||
toStateName: 'settings_account_reports',
|
toStateName: 'settings_account_reports',
|
||||||
},
|
},
|
||||||
csatReports: {
|
{
|
||||||
icon: 'ion-happy',
|
icon: 'emoji',
|
||||||
label: 'CSAT',
|
label: 'CSAT',
|
||||||
hasSubMenu: false,
|
hasSubMenu: false,
|
||||||
toState: frontendURL(`accounts/${accountId}/reports/csat`),
|
toState: frontendURL(`accounts/${accountId}/reports/csat`),
|
||||||
toStateName: 'csat_reports',
|
toStateName: 'csat_reports',
|
||||||
},
|
},
|
||||||
agentReports: {
|
{
|
||||||
icon: 'ion-person-stalker',
|
icon: 'people',
|
||||||
label: 'REPORTS_AGENT',
|
label: 'REPORTS_AGENT',
|
||||||
hasSubMenu: false,
|
hasSubMenu: false,
|
||||||
toState: frontendURL(`accounts/${accountId}/reports/agent`),
|
toState: frontendURL(`accounts/${accountId}/reports/agent`),
|
||||||
toStateName: 'agent_reports',
|
toStateName: 'agent_reports',
|
||||||
},
|
},
|
||||||
labelReports: {
|
{
|
||||||
icon: 'ion-pricetags',
|
icon: 'tag',
|
||||||
label: 'REPORTS_LABEL',
|
label: 'REPORTS_LABEL',
|
||||||
hasSubMenu: false,
|
hasSubMenu: false,
|
||||||
toState: frontendURL(`accounts/${accountId}/reports/label`),
|
toState: frontendURL(`accounts/${accountId}/reports/label`),
|
||||||
toStateName: 'label_reports',
|
toStateName: 'label_reports',
|
||||||
},
|
},
|
||||||
inboxReports: {
|
{
|
||||||
icon: 'ion-archive',
|
icon: 'mail-inbox-all',
|
||||||
label: 'REPORTS_INBOX',
|
label: 'REPORTS_INBOX',
|
||||||
hasSubMenu: false,
|
hasSubMenu: false,
|
||||||
toState: frontendURL(`accounts/${accountId}/reports/inboxes`),
|
toState: frontendURL(`accounts/${accountId}/reports/inboxes`),
|
||||||
toStateName: 'inbox_reports',
|
toStateName: 'inbox_reports',
|
||||||
},
|
},
|
||||||
teamReports: {
|
{
|
||||||
icon: 'ion-ios-people',
|
icon: 'people-team',
|
||||||
label: 'REPORTS_TEAM',
|
label: 'REPORTS_TEAM',
|
||||||
hasSubMenu: false,
|
hasSubMenu: false,
|
||||||
toState: frontendURL(`accounts/${accountId}/reports/teams`),
|
toState: frontendURL(`accounts/${accountId}/reports/teams`),
|
||||||
toStateName: 'team_reports',
|
toStateName: 'team_reports',
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
export default reports;
|
export default reports;
|
||||||
@ -1,6 +1,7 @@
|
|||||||
import { frontendURL } from '../../helper/URLHelper';
|
import { frontendURL } from '../../../../helper/URLHelper';
|
||||||
|
|
||||||
const settings = accountId => ({
|
const settings = accountId => ({
|
||||||
|
parentNav: 'settings',
|
||||||
routes: [
|
routes: [
|
||||||
'agent_list',
|
'agent_list',
|
||||||
'canned_list',
|
'canned_list',
|
||||||
@ -28,45 +29,39 @@ const settings = accountId => ({
|
|||||||
'settings_teams_edit',
|
'settings_teams_edit',
|
||||||
'settings_teams_edit_members',
|
'settings_teams_edit_members',
|
||||||
'settings_teams_edit_finish',
|
'settings_teams_edit_finish',
|
||||||
|
'automation_list',
|
||||||
],
|
],
|
||||||
menuItems: {
|
menuItems: [
|
||||||
back: {
|
{
|
||||||
icon: 'ion-ios-arrow-back',
|
icon: 'people',
|
||||||
label: 'HOME',
|
|
||||||
hasSubMenu: false,
|
|
||||||
toStateName: 'home',
|
|
||||||
toState: frontendURL(`accounts/${accountId}/dashboard`),
|
|
||||||
},
|
|
||||||
agents: {
|
|
||||||
icon: 'ion-person-stalker',
|
|
||||||
label: 'AGENTS',
|
label: 'AGENTS',
|
||||||
hasSubMenu: false,
|
hasSubMenu: false,
|
||||||
toState: frontendURL(`accounts/${accountId}/settings/agents/list`),
|
toState: frontendURL(`accounts/${accountId}/settings/agents/list`),
|
||||||
toStateName: 'agent_list',
|
toStateName: 'agent_list',
|
||||||
},
|
},
|
||||||
teams: {
|
{
|
||||||
icon: 'ion-ios-people',
|
icon: 'people-team',
|
||||||
label: 'TEAMS',
|
label: 'TEAMS',
|
||||||
hasSubMenu: false,
|
hasSubMenu: false,
|
||||||
toState: frontendURL(`accounts/${accountId}/settings/teams/list`),
|
toState: frontendURL(`accounts/${accountId}/settings/teams/list`),
|
||||||
toStateName: 'settings_teams_list',
|
toStateName: 'settings_teams_list',
|
||||||
},
|
},
|
||||||
inboxes: {
|
{
|
||||||
icon: 'ion-archive',
|
icon: 'mail-inbox-all',
|
||||||
label: 'INBOXES',
|
label: 'INBOXES',
|
||||||
hasSubMenu: false,
|
hasSubMenu: false,
|
||||||
toState: frontendURL(`accounts/${accountId}/settings/inboxes/list`),
|
toState: frontendURL(`accounts/${accountId}/settings/inboxes/list`),
|
||||||
toStateName: 'settings_inbox_list',
|
toStateName: 'settings_inbox_list',
|
||||||
},
|
},
|
||||||
labels: {
|
{
|
||||||
icon: 'ion-pricetags',
|
icon: 'tag',
|
||||||
label: 'LABELS',
|
label: 'LABELS',
|
||||||
hasSubMenu: false,
|
hasSubMenu: false,
|
||||||
toState: frontendURL(`accounts/${accountId}/settings/labels/list`),
|
toState: frontendURL(`accounts/${accountId}/settings/labels/list`),
|
||||||
toStateName: 'labels_list',
|
toStateName: 'labels_list',
|
||||||
},
|
},
|
||||||
attributes: {
|
{
|
||||||
icon: 'ion-code',
|
icon: 'code',
|
||||||
label: 'CUSTOM_ATTRIBUTES',
|
label: 'CUSTOM_ATTRIBUTES',
|
||||||
hasSubMenu: false,
|
hasSubMenu: false,
|
||||||
toState: frontendURL(
|
toState: frontendURL(
|
||||||
@ -74,8 +69,15 @@ const settings = accountId => ({
|
|||||||
),
|
),
|
||||||
toStateName: 'attributes_list',
|
toStateName: 'attributes_list',
|
||||||
},
|
},
|
||||||
cannedResponses: {
|
{
|
||||||
icon: 'ion-chatbox-working',
|
icon: 'autocorrect',
|
||||||
|
label: 'AUTOMATION',
|
||||||
|
hasSubMenu: false,
|
||||||
|
toState: frontendURL(`accounts/${accountId}/settings/automation/list`),
|
||||||
|
toStateName: 'automation_list',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'chat-multiple',
|
||||||
label: 'CANNED_RESPONSES',
|
label: 'CANNED_RESPONSES',
|
||||||
hasSubMenu: false,
|
hasSubMenu: false,
|
||||||
toState: frontendURL(
|
toState: frontendURL(
|
||||||
@ -83,28 +85,28 @@ const settings = accountId => ({
|
|||||||
),
|
),
|
||||||
toStateName: 'canned_list',
|
toStateName: 'canned_list',
|
||||||
},
|
},
|
||||||
settings_integrations: {
|
{
|
||||||
icon: 'ion-flash',
|
icon: 'flash-on',
|
||||||
label: 'INTEGRATIONS',
|
label: 'INTEGRATIONS',
|
||||||
hasSubMenu: false,
|
hasSubMenu: false,
|
||||||
toState: frontendURL(`accounts/${accountId}/settings/integrations`),
|
toState: frontendURL(`accounts/${accountId}/settings/integrations`),
|
||||||
toStateName: 'settings_integrations',
|
toStateName: 'settings_integrations',
|
||||||
},
|
},
|
||||||
settings_applications: {
|
{
|
||||||
icon: 'ion-asterisk',
|
icon: 'star-emphasis',
|
||||||
label: 'APPLICATIONS',
|
label: 'APPLICATIONS',
|
||||||
hasSubMenu: false,
|
hasSubMenu: false,
|
||||||
toState: frontendURL(`accounts/${accountId}/settings/applications`),
|
toState: frontendURL(`accounts/${accountId}/settings/applications`),
|
||||||
toStateName: 'settings_applications',
|
toStateName: 'settings_applications',
|
||||||
},
|
},
|
||||||
general_settings_index: {
|
{
|
||||||
icon: 'ion-gear-a',
|
icon: 'settings',
|
||||||
label: 'ACCOUNT_SETTINGS',
|
label: 'ACCOUNT_SETTINGS',
|
||||||
hasSubMenu: false,
|
hasSubMenu: false,
|
||||||
toState: frontendURL(`accounts/${accountId}/settings/general`),
|
toState: frontendURL(`accounts/${accountId}/settings/general`),
|
||||||
toStateName: 'general_settings_index',
|
toStateName: 'general_settings_index',
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
export default settings;
|
export default settings;
|
||||||
@ -14,7 +14,13 @@
|
|||||||
class="account-selector"
|
class="account-selector"
|
||||||
>
|
>
|
||||||
<a :href="`/app/accounts/${account.id}/dashboard`">
|
<a :href="`/app/accounts/${account.id}/dashboard`">
|
||||||
<i v-if="account.id === accountId" class="ion ion-ios-checkmark" />
|
<fluent-icon
|
||||||
|
v-if="account.id === accountId"
|
||||||
|
class="selected--account"
|
||||||
|
icon="checkmark-circle"
|
||||||
|
type="solid"
|
||||||
|
size="24"
|
||||||
|
/>
|
||||||
<label :for="account.name" class="account--details">
|
<label :for="account.name" class="account--details">
|
||||||
<div class="account--name">{{ account.name }}</div>
|
<div class="account--name">{{ account.name }}</div>
|
||||||
<div class="account--role">{{ account.role }}</div>
|
<div class="account--role">{{ account.role }}</div>
|
||||||
|
|||||||
@ -12,7 +12,7 @@
|
|||||||
<div v-if="!hasAccounts" class="alert-wrap">
|
<div v-if="!hasAccounts" class="alert-wrap">
|
||||||
<div class="callout alert">
|
<div class="callout alert">
|
||||||
<div class="icon-wrap">
|
<div class="icon-wrap">
|
||||||
<i class="ion-alert-circled"></i>
|
<fluent-icon icon="warning" />
|
||||||
</div>
|
</div>
|
||||||
{{ $t('CREATE_ACCOUNT.NO_ACCOUNT_WARNING') }}
|
{{ $t('CREATE_ACCOUNT.NO_ACCOUNT_WARNING') }}
|
||||||
</div>
|
</div>
|
||||||
@ -102,18 +102,19 @@ export default {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss">
|
<style lang="scss" scoped>
|
||||||
.alert-wrap {
|
.alert-wrap {
|
||||||
margin: var(--space-zero) var(--space-large);
|
font-size: var(--font-size-small);
|
||||||
margin-top: var(--space-medium);
|
margin: var(--space-medium) var(--space-large) var(--space-zero);
|
||||||
|
|
||||||
.callout {
|
.callout {
|
||||||
|
align-items: center;
|
||||||
|
border-radius: var(--border-radius-normal);
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-wrap {
|
.icon-wrap {
|
||||||
font-size: var(--font-size-big);
|
|
||||||
margin-left: var(--space-smaller);
|
margin-left: var(--space-smaller);
|
||||||
margin-right: var(--space-slab);
|
margin-right: var(--space-slab);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,18 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="current-user--row">
|
<woot-button
|
||||||
|
v-tooltip.right="$t(`SIDEBAR.PROFILE_SETTINGS`)"
|
||||||
|
variant="link"
|
||||||
|
class="current-user"
|
||||||
|
@click="handleClick"
|
||||||
|
>
|
||||||
<thumbnail
|
<thumbnail
|
||||||
:src="currentUser.avatar_url"
|
:src="currentUser.avatar_url"
|
||||||
:username="currentUserAvailableName"
|
:username="currentUser.name"
|
||||||
|
:status="statusOfAgent"
|
||||||
|
should-show-status-always
|
||||||
|
size="32px"
|
||||||
/>
|
/>
|
||||||
<div class="current-user--data">
|
</woot-button>
|
||||||
<h3 class="current-user--name text-truncate">
|
|
||||||
{{ currentUserAvailableName }}
|
|
||||||
</h3>
|
|
||||||
<h5 v-if="currentRole" class="current-user--role">
|
|
||||||
{{ $t(`AGENT_MGMT.AGENT_TYPES.${currentRole.toUpperCase()}`) }}
|
|
||||||
</h5>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
@ -25,39 +25,25 @@ export default {
|
|||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
currentUser: 'getCurrentUser',
|
currentUser: 'getCurrentUser',
|
||||||
currentRole: 'getCurrentRole',
|
currentUserAvailability: 'getCurrentUserAvailability',
|
||||||
}),
|
}),
|
||||||
currentUserAvailableName() {
|
statusOfAgent() {
|
||||||
return this.currentUser.name;
|
return this.currentUserAvailability || 'offline';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleClick() {
|
||||||
|
this.$emit('toggle-menu');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.current-user--row {
|
.current-user {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
border-radius: 50%;
|
||||||
|
border: 2px solid var(--white);
|
||||||
.current-user--data {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
.current-user--name {
|
|
||||||
font-size: var(--font-size-small);
|
|
||||||
font-weight: var(--font-weight-medium);
|
|
||||||
margin-bottom: var(--space-micro);
|
|
||||||
margin-left: var(--space-one);
|
|
||||||
max-width: 12rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.current-user--role {
|
|
||||||
color: var(--color-gray);
|
|
||||||
font-size: var(--font-size-mini);
|
|
||||||
margin-bottom: var(--zero);
|
|
||||||
margin-left: var(--space-one);
|
|
||||||
text-transform: capitalize;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -0,0 +1,46 @@
|
|||||||
|
<template>
|
||||||
|
<div class="logo">
|
||||||
|
<router-link :to="dashboardPath" replace>
|
||||||
|
<img :src="source" :alt="name" />
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import { frontendURL } from 'dashboard/helper/URLHelper';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
source: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
accountId: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
dashboardPath() {
|
||||||
|
return frontendURL(`accounts/${this.accountId}/dashboard`);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
$logo-size: 32px;
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
padding: var(--space-normal);
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: $logo-size;
|
||||||
|
height: $logo-size;
|
||||||
|
object-fit: cover;
|
||||||
|
object-position: left center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -1,11 +1,19 @@
|
|||||||
<template>
|
<template>
|
||||||
<span class="notifications icon ion-ios-bell" @click.stop="showNotification">
|
<div class="notifications-link">
|
||||||
<span v-if="unreadCount" class="unread-badge">{{ unreadCount }}</span>
|
<primary-nav-item
|
||||||
</span>
|
name="NOTIFICATIONS"
|
||||||
|
icon="alert"
|
||||||
|
:to="`/app/accounts/${accountId}/notifications`"
|
||||||
|
:count="unreadCount"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
|
import PrimaryNavItem from './PrimaryNavItem';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
components: { PrimaryNavItem },
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
accountId: 'getCurrentAccountId',
|
accountId: 'getCurrentAccountId',
|
||||||
@ -13,40 +21,20 @@ export default {
|
|||||||
}),
|
}),
|
||||||
unreadCount() {
|
unreadCount() {
|
||||||
if (!this.notificationMetadata.unreadCount) {
|
if (!this.notificationMetadata.unreadCount) {
|
||||||
return 0;
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.notificationMetadata.unreadCount < 100
|
return this.notificationMetadata.unreadCount < 100
|
||||||
? this.notificationMetadata.unreadCount
|
? `${this.notificationMetadata.unreadCount}`
|
||||||
: '99+';
|
: '99+';
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {},
|
||||||
showNotification() {
|
|
||||||
this.$router.push(`/app/accounts/${this.accountId}/notifications`);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.notifications {
|
.notifications-link {
|
||||||
font-size: var(--font-size-big);
|
margin-bottom: var(--space-small);
|
||||||
margin-bottom: auto;
|
|
||||||
margin-left: auto;
|
|
||||||
margin-top: auto;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
.unread-badge {
|
|
||||||
background: var(--r-300);
|
|
||||||
border-radius: var(--space-small);
|
|
||||||
color: var(--white);
|
|
||||||
font-size: var(--font-size-micro);
|
|
||||||
font-weight: var(--font-weight-black);
|
|
||||||
left: var(--space-slab);
|
|
||||||
padding: 0 var(--space-smaller);
|
|
||||||
position: absolute;
|
|
||||||
top: var(--space-smaller);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -2,15 +2,19 @@
|
|||||||
<transition name="menu-slide">
|
<transition name="menu-slide">
|
||||||
<div
|
<div
|
||||||
v-if="show"
|
v-if="show"
|
||||||
v-on-clickaway="() => $emit('close')"
|
v-on-clickaway="onClickAway"
|
||||||
class="dropdown-pane dropdowm--top"
|
class="dropdown-pane"
|
||||||
|
:class="{ 'dropdown-pane--open': show }"
|
||||||
>
|
>
|
||||||
|
<availability-status />
|
||||||
|
<li class="divider" />
|
||||||
<woot-dropdown-menu>
|
<woot-dropdown-menu>
|
||||||
<woot-dropdown-item v-if="showChangeAccountOption">
|
<woot-dropdown-item v-if="showChangeAccountOption">
|
||||||
<woot-button
|
<woot-button
|
||||||
variant="clear"
|
variant="clear"
|
||||||
|
color-scheme="secondary"
|
||||||
size="small"
|
size="small"
|
||||||
class=" change-accounts--button"
|
icon="arrow-swap"
|
||||||
@click="$emit('toggle-accounts')"
|
@click="$emit('toggle-accounts')"
|
||||||
>
|
>
|
||||||
{{ $t('SIDEBAR_ITEMS.CHANGE_ACCOUNTS') }}
|
{{ $t('SIDEBAR_ITEMS.CHANGE_ACCOUNTS') }}
|
||||||
@ -19,36 +23,50 @@
|
|||||||
<woot-dropdown-item v-if="globalConfig.chatwootInboxToken">
|
<woot-dropdown-item v-if="globalConfig.chatwootInboxToken">
|
||||||
<woot-button
|
<woot-button
|
||||||
variant="clear"
|
variant="clear"
|
||||||
|
color-scheme="secondary"
|
||||||
size="small"
|
size="small"
|
||||||
class=" change-accounts--button"
|
icon="chat-help"
|
||||||
@click="$emit('show-support-chat-window')"
|
@click="$emit('show-support-chat-window')"
|
||||||
>
|
>
|
||||||
Contact Support
|
{{ $t('SIDEBAR_ITEMS.CONTACT_SUPPORT') }}
|
||||||
</woot-button>
|
</woot-button>
|
||||||
</woot-dropdown-item>
|
</woot-dropdown-item>
|
||||||
<woot-dropdown-item>
|
<woot-dropdown-item>
|
||||||
<woot-button
|
<woot-button
|
||||||
variant="clear"
|
variant="clear"
|
||||||
|
color-scheme="secondary"
|
||||||
size="small"
|
size="small"
|
||||||
class=" change-accounts--button"
|
icon="keyboard"
|
||||||
@click="$emit('key-shortcut-modal')"
|
@click="handleKeyboardHelpClick"
|
||||||
>
|
>
|
||||||
{{ $t('SIDEBAR_ITEMS.KEYBOARD_SHORTCUTS') }}
|
{{ $t('SIDEBAR_ITEMS.KEYBOARD_SHORTCUTS') }}
|
||||||
</woot-button>
|
</woot-button>
|
||||||
</woot-dropdown-item>
|
</woot-dropdown-item>
|
||||||
<woot-dropdown-item>
|
<woot-dropdown-item>
|
||||||
<router-link
|
<router-link
|
||||||
|
v-slot="{ href, isActive, navigate }"
|
||||||
:to="`/app/accounts/${accountId}/profile/settings`"
|
:to="`/app/accounts/${accountId}/profile/settings`"
|
||||||
class="button clear small change-accounts--button"
|
custom
|
||||||
>
|
>
|
||||||
{{ $t('SIDEBAR_ITEMS.PROFILE_SETTINGS') }}
|
<a
|
||||||
|
:href="href"
|
||||||
|
class="button small clear secondary"
|
||||||
|
:class="{ 'is-active': isActive }"
|
||||||
|
@click="e => handleProfileSettingClick(e, navigate)"
|
||||||
|
>
|
||||||
|
<fluent-icon icon="person" class="icon icon--font" />
|
||||||
|
<span class="button__content">
|
||||||
|
{{ $t('SIDEBAR_ITEMS.PROFILE_SETTINGS') }}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
</router-link>
|
</router-link>
|
||||||
</woot-dropdown-item>
|
</woot-dropdown-item>
|
||||||
<woot-dropdown-item>
|
<woot-dropdown-item>
|
||||||
<woot-button
|
<woot-button
|
||||||
variant="clear"
|
variant="clear"
|
||||||
|
color-scheme="secondary"
|
||||||
size="small"
|
size="small"
|
||||||
class=" change-accounts--button"
|
icon="power"
|
||||||
@click="logout"
|
@click="logout"
|
||||||
>
|
>
|
||||||
{{ $t('SIDEBAR_ITEMS.LOGOUT') }}
|
{{ $t('SIDEBAR_ITEMS.LOGOUT') }}
|
||||||
@ -63,13 +81,15 @@
|
|||||||
import { mixin as clickaway } from 'vue-clickaway';
|
import { mixin as clickaway } from 'vue-clickaway';
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import Auth from '../../../api/auth';
|
import Auth from '../../../api/auth';
|
||||||
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
|
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem';
|
||||||
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
|
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu';
|
||||||
|
import AvailabilityStatus from 'dashboard/components/layout/AvailabilityStatus';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
WootDropdownMenu,
|
WootDropdownMenu,
|
||||||
WootDropdownItem,
|
WootDropdownItem,
|
||||||
|
AvailabilityStatus,
|
||||||
},
|
},
|
||||||
mixins: [clickaway],
|
mixins: [clickaway],
|
||||||
props: {
|
props: {
|
||||||
@ -88,18 +108,34 @@ export default {
|
|||||||
if (this.globalConfig.createNewAccountFromDashboard) {
|
if (this.globalConfig.createNewAccountFromDashboard) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return this.currentUser.accounts.length > 1;
|
|
||||||
|
const { accounts = [] } = this.currentUser;
|
||||||
|
return accounts.length > 1;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
handleProfileSettingClick(e, navigate) {
|
||||||
|
this.$emit('close');
|
||||||
|
navigate(e);
|
||||||
|
},
|
||||||
|
handleKeyboardHelpClick() {
|
||||||
|
this.$emit('key-shortcut-modal');
|
||||||
|
this.$emit('close');
|
||||||
|
},
|
||||||
logout() {
|
logout() {
|
||||||
Auth.logout();
|
Auth.logout();
|
||||||
},
|
},
|
||||||
|
onClickAway() {
|
||||||
|
if (this.show) this.$emit('close');
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.dropdown-pane {
|
.dropdown-pane {
|
||||||
right: 0;
|
left: var(--space-slab);
|
||||||
|
bottom: var(--space-larger);
|
||||||
|
min-width: 16.8rem;
|
||||||
|
z-index: var(--z-index-much-higher);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -0,0 +1,112 @@
|
|||||||
|
<template>
|
||||||
|
<div class="primary--sidebar">
|
||||||
|
<logo
|
||||||
|
:source="logoSource"
|
||||||
|
:name="installationName"
|
||||||
|
:account-id="accountId"
|
||||||
|
/>
|
||||||
|
<nav class="menu vertical">
|
||||||
|
<primary-nav-item
|
||||||
|
v-for="menuItem in menuItems"
|
||||||
|
:key="menuItem.toState"
|
||||||
|
:icon="menuItem.icon"
|
||||||
|
:name="menuItem.label"
|
||||||
|
:to="menuItem.toState"
|
||||||
|
:is-child-menu-active="menuItem.key === activeMenuItem"
|
||||||
|
/>
|
||||||
|
</nav>
|
||||||
|
<div class="menu vertical user-menu">
|
||||||
|
<notification-bell />
|
||||||
|
<agent-details @toggle-menu="toggleOptions" />
|
||||||
|
<options-menu
|
||||||
|
:show="showOptionsMenu"
|
||||||
|
@toggle-accounts="toggleAccountModal"
|
||||||
|
@show-support-chat-window="toggleSupportChatWindow"
|
||||||
|
@key-shortcut-modal="$emit('key-shortcut-modal')"
|
||||||
|
@close="toggleOptions"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import Logo from './Logo';
|
||||||
|
import PrimaryNavItem from './PrimaryNavItem';
|
||||||
|
import OptionsMenu from './OptionsMenu';
|
||||||
|
import AgentDetails from './AgentDetails';
|
||||||
|
import NotificationBell from './NotificationBell';
|
||||||
|
|
||||||
|
import { frontendURL } from 'dashboard/helper/URLHelper';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Logo,
|
||||||
|
PrimaryNavItem,
|
||||||
|
OptionsMenu,
|
||||||
|
AgentDetails,
|
||||||
|
NotificationBell,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
logoSource: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
installationName: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
accountId: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
menuItems: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
activeMenuItem: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
showOptionsMenu: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
frontendURL,
|
||||||
|
toggleOptions() {
|
||||||
|
this.showOptionsMenu = !this.showOptionsMenu;
|
||||||
|
},
|
||||||
|
toggleAccountModal() {
|
||||||
|
this.$emit('toggle-accounts');
|
||||||
|
},
|
||||||
|
toggleSupportChatWindow() {
|
||||||
|
window.$chatwoot.toggle();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.primary--sidebar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: var(--space-jumbo);
|
||||||
|
border-right: 1px solid var(--s-50);
|
||||||
|
box-sizing: content-box;
|
||||||
|
height: 100vh;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu {
|
||||||
|
align-items: center;
|
||||||
|
margin-top: var(--space-medium);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-menu {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-grow: 1;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-bottom: var(--space-normal);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,78 @@
|
|||||||
|
<template>
|
||||||
|
<router-link v-slot="{ href, isActive, navigate }" :to="to" custom>
|
||||||
|
<a
|
||||||
|
v-tooltip.right="$t(`SIDEBAR.${name}`)"
|
||||||
|
:href="href"
|
||||||
|
class="button clear button--only-icon menu-item"
|
||||||
|
:class="{ 'is-active': isActive || isChildMenuActive }"
|
||||||
|
@click="navigate"
|
||||||
|
>
|
||||||
|
<fluent-icon :icon="icon" />
|
||||||
|
<span class="show-for-sr">{{ name }}</span>
|
||||||
|
<span v-if="count" class="badge warning">{{ count }}</span>
|
||||||
|
</a>
|
||||||
|
</router-link>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
to: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
count: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
isChildMenuActive: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.button {
|
||||||
|
margin: var(--space-small) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item {
|
||||||
|
display: inline-flex;
|
||||||
|
position: relative;
|
||||||
|
border-radius: var(--border-radius-large);
|
||||||
|
border: 1px solid transparent;
|
||||||
|
color: var(--s-600);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--w-25);
|
||||||
|
color: var(--s-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: var(--w-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-active {
|
||||||
|
background: var(--w-50);
|
||||||
|
color: var(--w-500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
font-size: var(--font-size-default);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
position: absolute;
|
||||||
|
right: var(--space-minus-smaller);
|
||||||
|
top: var(--space-minus-smaller);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,170 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="hasSecondaryMenu" class="main-nav secondary-menu">
|
||||||
|
<transition-group name="menu-list" tag="ul" class="menu vertical">
|
||||||
|
<secondary-nav-item
|
||||||
|
v-for="menuItem in accessibleMenuItems"
|
||||||
|
:key="menuItem.toState"
|
||||||
|
:menu-item="menuItem"
|
||||||
|
/>
|
||||||
|
<secondary-nav-item
|
||||||
|
v-for="menuItem in additionalSecondaryMenuItems[menuConfig.parentNav]"
|
||||||
|
:key="menuItem.key"
|
||||||
|
:menu-item="menuItem"
|
||||||
|
@add-label="showAddLabelPopup"
|
||||||
|
/>
|
||||||
|
</transition-group>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import { frontendURL } from '../../../helper/URLHelper';
|
||||||
|
import SecondaryNavItem from './SecondaryNavItem.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
SecondaryNavItem,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
accountId: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
labels: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
inboxes: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
teams: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
menuConfig: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {},
|
||||||
|
},
|
||||||
|
currentRole: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
hasSecondaryMenu() {
|
||||||
|
return this.menuConfig.menuItems && this.menuConfig.menuItems.length;
|
||||||
|
},
|
||||||
|
accessibleMenuItems() {
|
||||||
|
if (!this.currentRole) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return this.menuConfig.menuItems.filter(
|
||||||
|
menuItem =>
|
||||||
|
window.roleWiseRoutes[this.currentRole].indexOf(
|
||||||
|
menuItem.toStateName
|
||||||
|
) > -1
|
||||||
|
);
|
||||||
|
},
|
||||||
|
inboxSection() {
|
||||||
|
return {
|
||||||
|
icon: 'folder',
|
||||||
|
label: 'INBOXES',
|
||||||
|
hasSubMenu: true,
|
||||||
|
newLink: true,
|
||||||
|
newLinkTag: 'NEW_INBOX',
|
||||||
|
key: 'inbox',
|
||||||
|
toState: frontendURL(`accounts/${this.accountId}/settings/inboxes/new`),
|
||||||
|
toStateName: 'settings_inbox_new',
|
||||||
|
newLinkRouteName: 'settings_inbox_new',
|
||||||
|
children: this.inboxes.map(inbox => ({
|
||||||
|
id: inbox.id,
|
||||||
|
label: inbox.name,
|
||||||
|
truncateLabel: true,
|
||||||
|
toState: frontendURL(`accounts/${this.accountId}/inbox/${inbox.id}`),
|
||||||
|
type: inbox.channel_type,
|
||||||
|
phoneNumber: inbox.phone_number,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
labelSection() {
|
||||||
|
return {
|
||||||
|
icon: 'number-symbol',
|
||||||
|
label: 'LABELS',
|
||||||
|
hasSubMenu: true,
|
||||||
|
newLink: true,
|
||||||
|
newLinkTag: 'NEW_LABEL',
|
||||||
|
key: 'label',
|
||||||
|
toState: frontendURL(`accounts/${this.accountId}/settings/labels`),
|
||||||
|
toStateName: 'labels_list',
|
||||||
|
showModalForNewItem: true,
|
||||||
|
modalName: 'AddLabel',
|
||||||
|
children: this.labels.map(label => ({
|
||||||
|
id: label.id,
|
||||||
|
label: label.title,
|
||||||
|
color: label.color,
|
||||||
|
truncateLabel: true,
|
||||||
|
toState: frontendURL(
|
||||||
|
`accounts/${this.accountId}/label/${label.title}`
|
||||||
|
),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
contactLabelSection() {
|
||||||
|
return {
|
||||||
|
icon: 'number-symbol',
|
||||||
|
label: 'TAGGED_WITH',
|
||||||
|
hasSubMenu: true,
|
||||||
|
key: 'label',
|
||||||
|
newLink: true,
|
||||||
|
newLinkTag: 'NEW_LABEL',
|
||||||
|
toState: frontendURL(`accounts/${this.accountId}/settings/labels`),
|
||||||
|
toStateName: 'labels_list',
|
||||||
|
showModalForNewItem: true,
|
||||||
|
modalName: 'AddLabel',
|
||||||
|
children: this.labels.map(label => ({
|
||||||
|
id: label.id,
|
||||||
|
label: label.title,
|
||||||
|
color: label.color,
|
||||||
|
truncateLabel: true,
|
||||||
|
toState: frontendURL(
|
||||||
|
`accounts/${this.accountId}/labels/${label.title}/contacts`
|
||||||
|
),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
teamSection() {
|
||||||
|
return {
|
||||||
|
icon: 'people-team',
|
||||||
|
label: 'TEAMS',
|
||||||
|
hasSubMenu: true,
|
||||||
|
newLink: true,
|
||||||
|
newLinkTag: 'NEW_TEAM',
|
||||||
|
key: 'team',
|
||||||
|
toState: frontendURL(`accounts/${this.accountId}/settings/teams/new`),
|
||||||
|
toStateName: 'settings_teams_new',
|
||||||
|
newLinkRouteName: 'settings_teams_new',
|
||||||
|
children: this.teams.map(team => ({
|
||||||
|
id: team.id,
|
||||||
|
label: team.name,
|
||||||
|
truncateLabel: true,
|
||||||
|
toState: frontendURL(`accounts/${this.accountId}/team/${team.id}`),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
additionalSecondaryMenuItems() {
|
||||||
|
let conversationMenuItems = [this.inboxSection, this.labelSection];
|
||||||
|
if (this.teams.length) {
|
||||||
|
conversationMenuItems = [this.teamSection, ...conversationMenuItems];
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
conversations: conversationMenuItems,
|
||||||
|
contacts: [this.contactLabelSection],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
showAddLabelPopup() {
|
||||||
|
this.$emit('add-label');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@ -0,0 +1,145 @@
|
|||||||
|
<template>
|
||||||
|
<router-link
|
||||||
|
v-slot="{ href, isActive, navigate }"
|
||||||
|
:to="to"
|
||||||
|
custom
|
||||||
|
active-class="active"
|
||||||
|
>
|
||||||
|
<li :class="{ active: isActive }">
|
||||||
|
<a
|
||||||
|
:href="href"
|
||||||
|
class="button clear menu-item text-truncate"
|
||||||
|
:class="{ 'is-active': isActive, 'text-truncate': shouldTruncate }"
|
||||||
|
@click="navigate"
|
||||||
|
>
|
||||||
|
<span v-if="icon" class="badge--icon">
|
||||||
|
<fluent-icon class="inbox-icon" :icon="icon" size="12" />
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="labelColor"
|
||||||
|
class="badge--label"
|
||||||
|
:style="{ backgroundColor: labelColor }"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
:title="menuTitle"
|
||||||
|
class="menu-label button__content"
|
||||||
|
:class="{ 'text-truncate': shouldTruncate }"
|
||||||
|
>
|
||||||
|
{{ label }}
|
||||||
|
</span>
|
||||||
|
<span v-if="count" class="badge" :class="{ secondary: !isActive }">
|
||||||
|
{{ count }}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</router-link>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
to: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
labelColor: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
shouldTruncate: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
count: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
showIcon() {
|
||||||
|
return { 'text-truncate': this.shouldTruncate };
|
||||||
|
},
|
||||||
|
menuTitle() {
|
||||||
|
return this.shouldTruncate ? this.label : '';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
$badge-size: var(--space-normal);
|
||||||
|
$label-badge-size: var(--space-slab);
|
||||||
|
|
||||||
|
.button {
|
||||||
|
margin: var(--space-small) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item {
|
||||||
|
display: inline-flex;
|
||||||
|
color: var(--s-600);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
width: 100%;
|
||||||
|
height: var(--space-medium);
|
||||||
|
padding: var(--space-smaller) var(--space-smaller);
|
||||||
|
margin: var(--space-smaller) 0;
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--s-25);
|
||||||
|
color: var(--s-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: var(--w-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-active {
|
||||||
|
background: var(--w-25);
|
||||||
|
color: var(--w-500);
|
||||||
|
border-color: var(--w-25);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-label {
|
||||||
|
flex-grow: 1;
|
||||||
|
line-height: var(--space-two);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inbox-icon {
|
||||||
|
font-size: var(--font-size-nano);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge--label,
|
||||||
|
.badge--icon {
|
||||||
|
display: inline-flex;
|
||||||
|
border-radius: var(--border-radius-small);
|
||||||
|
margin-right: var(--space-smaller);
|
||||||
|
background: var(--s-100);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge--icon {
|
||||||
|
align-items: center;
|
||||||
|
height: $badge-size;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: $badge-size;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge--label {
|
||||||
|
height: $label-badge-size;
|
||||||
|
min-width: $label-badge-size;
|
||||||
|
margin-left: var(--space-smaller);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge.secondary {
|
||||||
|
min-width: unset;
|
||||||
|
background: var(--s-75);
|
||||||
|
color: var(--s-600);
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,190 @@
|
|||||||
|
<template>
|
||||||
|
<li class="sidebar-item">
|
||||||
|
<span v-if="hasSubMenu" class="secondary-menu--title fs-small">
|
||||||
|
{{ $t(`SIDEBAR.${menuItem.label}`) }}
|
||||||
|
</span>
|
||||||
|
<router-link
|
||||||
|
v-else
|
||||||
|
class="secondary-menu--title secondary-menu--link fs-small"
|
||||||
|
:class="computedClass"
|
||||||
|
:to="menuItem.toState"
|
||||||
|
>
|
||||||
|
<fluent-icon
|
||||||
|
:icon="menuItem.icon"
|
||||||
|
class="secondary-menu--icon"
|
||||||
|
size="14"
|
||||||
|
/>
|
||||||
|
{{ $t(`SIDEBAR.${menuItem.label}`) }}
|
||||||
|
</router-link>
|
||||||
|
|
||||||
|
<ul v-if="hasSubMenu" class="nested vertical menu">
|
||||||
|
<secondary-child-nav-item
|
||||||
|
v-for="child in menuItem.children"
|
||||||
|
:key="child.id"
|
||||||
|
:to="child.toState"
|
||||||
|
:label="child.label"
|
||||||
|
:label-color="child.color"
|
||||||
|
:should-truncate="child.truncateLabel"
|
||||||
|
:icon="computedInboxClass(child)"
|
||||||
|
/>
|
||||||
|
<router-link
|
||||||
|
v-if="showItem(menuItem)"
|
||||||
|
v-slot="{ href, isActive, navigate }"
|
||||||
|
:to="menuItem.toState"
|
||||||
|
custom
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
:href="href"
|
||||||
|
class="button small clear menu-item--new secondary"
|
||||||
|
:class="{ 'is-active': isActive }"
|
||||||
|
@click="e => newLinkClick(e, navigate)"
|
||||||
|
>
|
||||||
|
<fluent-icon icon="add" />
|
||||||
|
<span class="button__content">
|
||||||
|
{{ $t(`SIDEBAR.${menuItem.newLinkTag}`) }}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</router-link>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
|
||||||
|
import adminMixin from '../../../mixins/isAdmin';
|
||||||
|
import { getInboxClassByType } from 'dashboard/helper/inbox';
|
||||||
|
|
||||||
|
import SecondaryChildNavItem from './SecondaryChildNavItem';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: { SecondaryChildNavItem },
|
||||||
|
mixins: [adminMixin],
|
||||||
|
props: {
|
||||||
|
menuItem: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters({ activeInbox: 'getSelectedInbox' }),
|
||||||
|
hasSubMenu() {
|
||||||
|
return !!this.menuItem.children;
|
||||||
|
},
|
||||||
|
computedClass() {
|
||||||
|
// If active Inbox is present
|
||||||
|
// donot highlight conversations
|
||||||
|
if (this.activeInbox) return ' ';
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.$store.state.route.name === 'inbox_conversation' &&
|
||||||
|
this.menuItem.toStateName === 'home'
|
||||||
|
) {
|
||||||
|
return 'is-active';
|
||||||
|
}
|
||||||
|
return ' ';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
computedInboxClass(child) {
|
||||||
|
const { type, phoneNumber } = child;
|
||||||
|
if (!type) return '';
|
||||||
|
const classByType = getInboxClassByType(type, phoneNumber);
|
||||||
|
return classByType;
|
||||||
|
},
|
||||||
|
newLinkClick(e, navigate) {
|
||||||
|
if (this.menuItem.newLinkRouteName) {
|
||||||
|
navigate(e);
|
||||||
|
} else if (this.menuItem.showModalForNewItem) {
|
||||||
|
if (this.menuItem.modalName === 'AddLabel') {
|
||||||
|
e.preventDefault();
|
||||||
|
this.$emit('add-label');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
showItem(item) {
|
||||||
|
return this.isAdmin && item.newLink !== undefined;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.sidebar-item {
|
||||||
|
margin: var(--space-smaller) 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondary-menu--title {
|
||||||
|
color: var(--s-600);
|
||||||
|
display: flex;
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
line-height: var(--space-two);
|
||||||
|
margin: var(--space-small) 0;
|
||||||
|
padding: 0 var(--space-small);
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondary-menu--link {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin: 0;
|
||||||
|
padding: var(--space-small);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
border-radius: var(--border-radius-normal);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--s-25);
|
||||||
|
color: var(--s-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: var(--w-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.router-link-exact-active,
|
||||||
|
&.is-active {
|
||||||
|
background: var(--w-25);
|
||||||
|
color: var(--w-500);
|
||||||
|
border-color: var(--w-25);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondary-menu--icon {
|
||||||
|
margin-right: var(--space-smaller);
|
||||||
|
min-width: var(--space-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-menu-link {
|
||||||
|
color: var(--s-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrap {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-color--display {
|
||||||
|
border-radius: var(--space-smaller);
|
||||||
|
height: var(--space-normal);
|
||||||
|
margin-right: var(--space-small);
|
||||||
|
min-width: var(--space-normal);
|
||||||
|
width: var(--space-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inbox-icon {
|
||||||
|
position: relative;
|
||||||
|
top: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-item .button.menu-item--new {
|
||||||
|
display: inline-flex;
|
||||||
|
height: var(--space-medium);
|
||||||
|
margin: var(--space-smaller) 0;
|
||||||
|
padding: var(--space-smaller);
|
||||||
|
color: var(--s-500);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--w-500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -7,10 +7,12 @@ import i18n from 'dashboard/i18n';
|
|||||||
|
|
||||||
import WootModal from 'dashboard/components/Modal';
|
import WootModal from 'dashboard/components/Modal';
|
||||||
import WootModalHeader from 'dashboard/components/ModalHeader';
|
import WootModalHeader from 'dashboard/components/ModalHeader';
|
||||||
|
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon';
|
||||||
|
|
||||||
const localVue = createLocalVue();
|
const localVue = createLocalVue();
|
||||||
localVue.component('woot-modal', WootModal);
|
localVue.component('woot-modal', WootModal);
|
||||||
localVue.component('woot-modal-header', WootModalHeader);
|
localVue.component('woot-modal-header', WootModalHeader);
|
||||||
|
localVue.component('fluent-icon', FluentIcon);
|
||||||
|
|
||||||
localVue.use(Vuex);
|
localVue.use(Vuex);
|
||||||
localVue.use(VueI18n);
|
localVue.use(VueI18n);
|
||||||
@ -84,7 +86,7 @@ describe('accountSelctor', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('first account item is checked', () => {
|
it('first account item is checked', () => {
|
||||||
const accountFirstItem = accountSelector.find('.account-selector .ion');
|
const accountFirstItem = accountSelector.find('.account-selector svg');
|
||||||
expect(accountFirstItem.exists()).toBe(true);
|
expect(accountFirstItem.exists()).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -2,13 +2,20 @@ import AgentDetails from '../AgentDetails';
|
|||||||
import { createLocalVue, shallowMount } from '@vue/test-utils';
|
import { createLocalVue, shallowMount } from '@vue/test-utils';
|
||||||
import Vuex from 'vuex';
|
import Vuex from 'vuex';
|
||||||
import VueI18n from 'vue-i18n';
|
import VueI18n from 'vue-i18n';
|
||||||
|
import VTooltip from 'v-tooltip';
|
||||||
|
|
||||||
import i18n from 'dashboard/i18n';
|
import i18n from 'dashboard/i18n';
|
||||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail';
|
import Thumbnail from 'dashboard/components/widgets/Thumbnail';
|
||||||
|
import WootButton from 'dashboard/components/ui/WootButton';
|
||||||
const localVue = createLocalVue();
|
const localVue = createLocalVue();
|
||||||
localVue.use(Vuex);
|
localVue.use(Vuex);
|
||||||
localVue.use(VueI18n);
|
localVue.use(VueI18n);
|
||||||
localVue.component('thumbnail', Thumbnail);
|
localVue.component('thumbnail', Thumbnail);
|
||||||
|
localVue.component('woot-button', WootButton);
|
||||||
|
localVue.component('woot-button', WootButton);
|
||||||
|
localVue.use(VTooltip, {
|
||||||
|
defaultHtml: false,
|
||||||
|
});
|
||||||
|
|
||||||
const i18nConfig = new VueI18n({
|
const i18nConfig = new VueI18n({
|
||||||
locale: 'en',
|
locale: 'en',
|
||||||
@ -16,7 +23,11 @@ const i18nConfig = new VueI18n({
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('agentDetails', () => {
|
describe('agentDetails', () => {
|
||||||
const currentUser = { name: 'Neymar Junior', avatar_url: '' };
|
const currentUser = {
|
||||||
|
name: 'Neymar Junior',
|
||||||
|
avatar_url: '',
|
||||||
|
availability_status: 'online',
|
||||||
|
};
|
||||||
const currentRole = 'agent';
|
const currentRole = 'agent';
|
||||||
let store = null;
|
let store = null;
|
||||||
let actions = null;
|
let actions = null;
|
||||||
@ -31,6 +42,7 @@ describe('agentDetails', () => {
|
|||||||
getters: {
|
getters: {
|
||||||
getCurrentUser: () => currentUser,
|
getCurrentUser: () => currentUser,
|
||||||
getCurrentRole: () => currentRole,
|
getCurrentRole: () => currentRole,
|
||||||
|
getCurrentUserAvailability: () => currentUser.availability_status,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -47,14 +59,8 @@ describe('agentDetails', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows the agent name', () => {
|
it(' the agent status', () => {
|
||||||
const agentTitle = agentDetails.find('.current-user--name');
|
expect(agentDetails.find('thumbnail-stub').vm.status).toBe('online');
|
||||||
expect(agentTitle.text()).toBe('Neymar Junior');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('shows the agent role', () => {
|
|
||||||
const agentTitle = agentDetails.find('.current-user--role');
|
|
||||||
expect(agentTitle.text()).toBe('Agent');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('agent thumbnail exists', () => {
|
it('agent thumbnail exists', () => {
|
||||||
|
|||||||
@ -50,8 +50,9 @@ describe('notificationBell', () => {
|
|||||||
localVue,
|
localVue,
|
||||||
i18n: i18nConfig,
|
i18n: i18nConfig,
|
||||||
});
|
});
|
||||||
const statusViewTitle = notificationBell.find('.unread-badge');
|
|
||||||
expect(statusViewTitle.text()).toBe('19');
|
const statusViewTitle = notificationBell.find('primary-nav-item-stub');
|
||||||
|
expect(statusViewTitle.vm.count).toBe('19');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('it should return unread count 99+ ', async () => {
|
it('it should return unread count 99+ ', async () => {
|
||||||
@ -61,7 +62,7 @@ describe('notificationBell', () => {
|
|||||||
localVue,
|
localVue,
|
||||||
i18n: i18nConfig,
|
i18n: i18nConfig,
|
||||||
});
|
});
|
||||||
const statusViewTitle = notificationBell.find('.unread-badge');
|
const statusViewTitle = notificationBell.find('primary-nav-item-stub');
|
||||||
expect(statusViewTitle.text()).toBe('99+');
|
expect(statusViewTitle.vm.count).toBe('99+');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,15 +1,23 @@
|
|||||||
import AvailabilityStatus from '../AvailabilityStatus';
|
import AvailabilityStatus from '../AvailabilityStatus.vue';
|
||||||
import { createLocalVue, mount } from '@vue/test-utils';
|
import { createLocalVue, mount } from '@vue/test-utils';
|
||||||
import Vuex from 'vuex';
|
import Vuex from 'vuex';
|
||||||
import VueI18n from 'vue-i18n';
|
import VueI18n from 'vue-i18n';
|
||||||
|
|
||||||
import WootButton from 'dashboard/components/ui/WootButton';
|
import WootButton from 'dashboard/components/ui/WootButton';
|
||||||
|
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 i18n from 'dashboard/i18n';
|
import i18n from 'dashboard/i18n';
|
||||||
|
|
||||||
const localVue = createLocalVue();
|
const localVue = createLocalVue();
|
||||||
localVue.use(Vuex);
|
localVue.use(Vuex);
|
||||||
localVue.use(VueI18n);
|
localVue.use(VueI18n);
|
||||||
localVue.component('woot-button', WootButton);
|
localVue.component('woot-button', WootButton);
|
||||||
|
localVue.component('woot-dropdown-header', WootDropdownHeader);
|
||||||
|
localVue.component('woot-dropdown-menu', WootDropdownMenu);
|
||||||
|
localVue.component('woot-dropdown-divider', WootDropdownDivider);
|
||||||
|
localVue.component('woot-dropdown-item', WootDropdownItem);
|
||||||
|
|
||||||
const i18nConfig = new VueI18n({
|
const i18nConfig = new VueI18n({
|
||||||
locale: 'en',
|
locale: 'en',
|
||||||
@ -52,29 +60,11 @@ describe('AvailabilityStatus', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows current user status', () => {
|
|
||||||
const statusViewTitle = availabilityStatus.find('.status-view--title');
|
|
||||||
|
|
||||||
expect(statusViewTitle.text()).toBe('Online');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('opens the menu when user clicks "change"', async () => {
|
|
||||||
expect(availabilityStatus.find('.dropdown-pane').exists()).toBe(false);
|
|
||||||
|
|
||||||
await availabilityStatus
|
|
||||||
.find('.status-change--change-button')
|
|
||||||
.trigger('click');
|
|
||||||
|
|
||||||
expect(availabilityStatus.find('.dropdown-pane').exists()).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('dispatches an action when user changes status', async () => {
|
it('dispatches an action when user changes status', async () => {
|
||||||
await availabilityStatus
|
await availabilityStatus;
|
||||||
.find('.status-change--change-button')
|
availabilityStatus
|
||||||
.trigger('click');
|
.findAll('.status-change--dropdown-button')
|
||||||
|
.at(2)
|
||||||
await availabilityStatus
|
|
||||||
.find('.status-change li:last-child button')
|
|
||||||
.trigger('click');
|
.trigger('click');
|
||||||
|
|
||||||
expect(actions.updateAvailability).toBeCalledWith(
|
expect(actions.updateAvailability).toBeCalledWith(
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import { mount } from '@vue/test-utils';
|
import { shallowMount } from '@vue/test-utils';
|
||||||
import SidemenuIcon from '../SidemenuIcon';
|
import SidemenuIcon from '../SidemenuIcon';
|
||||||
|
|
||||||
describe('SidemenuIcon', () => {
|
describe('SidemenuIcon', () => {
|
||||||
test('matches snapshot', () => {
|
test('matches snapshot', () => {
|
||||||
const wrapper = mount(SidemenuIcon);
|
const wrapper = shallowMount(SidemenuIcon);
|
||||||
expect(wrapper.vm).toBeTruthy();
|
expect(wrapper.vm).toBeTruthy();
|
||||||
expect(wrapper.element).toMatchSnapshot();
|
expect(wrapper.element).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,7 +1,10 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`SidemenuIcon matches snapshot 1`] = `
|
exports[`SidemenuIcon matches snapshot 1`] = `
|
||||||
<i
|
<button>
|
||||||
class="ion-android-menu hamburger--menu"
|
<fluent-icon
|
||||||
/>
|
class="hamburger--menu"
|
||||||
|
icon="list"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@ -1,13 +1,23 @@
|
|||||||
<template>
|
<template>
|
||||||
<div :class="labelClass" :style="labelStyle" :title="description">
|
<div :class="labelClass" :style="labelStyle" :title="description">
|
||||||
<i v-if="icon" class="label--icon" :class="icon" @click="onClick" />
|
<button v-if="icon" class="label-action--button" @click="onClick">
|
||||||
|
<fluent-icon :icon="icon" size="12" class="label--icon" />
|
||||||
|
</button>
|
||||||
<span v-if="!href">{{ title }}</span>
|
<span v-if="!href">{{ title }}</span>
|
||||||
<a v-else :href="href" :style="anchorStyle">{{ title }}</a>
|
<a v-else :href="href" :style="anchorStyle">{{ title }}</a>
|
||||||
<i v-if="showClose" class="close--icon ion-close" @click="onClick" />
|
<button
|
||||||
|
v-if="showClose"
|
||||||
|
class="label-action--button"
|
||||||
|
:style="{ color: textColor }"
|
||||||
|
@click="onClick"
|
||||||
|
>
|
||||||
|
<fluent-icon icon="dismiss" size="12" class="close--icon" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import { getContrastingTextColor } from '@chatwoot/utils';
|
import { getContrastingTextColor } from '@chatwoot/utils';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
title: {
|
title: {
|
||||||
@ -79,6 +89,8 @@ export default {
|
|||||||
@import '~dashboard/assets/scss/variables';
|
@import '~dashboard/assets/scss/variables';
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
font-weight: var(--font-weight-medium);
|
font-weight: var(--font-weight-medium);
|
||||||
margin-right: var(--space-smaller);
|
margin-right: var(--space-smaller);
|
||||||
margin-bottom: var(--space-smaller);
|
margin-bottom: var(--space-smaller);
|
||||||
@ -89,11 +101,12 @@ export default {
|
|||||||
|
|
||||||
.label--icon {
|
.label--icon {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
margin-right: var(--space-smaller);
|
||||||
}
|
}
|
||||||
.label--icon,
|
|
||||||
.close--icon {
|
.close--icon {
|
||||||
font-size: var(--font-size-micro);
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
margin-left: var(--space-smaller);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.small .label--icon,
|
&.small .label--icon,
|
||||||
@ -150,4 +163,8 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.label-action--button {
|
||||||
|
margin-bottom: var(--space-minus-micro);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,7 +1,13 @@
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<label class="switch" :class="classObject">
|
<label class="switch" :class="classObject">
|
||||||
<input class="switch-input" :name="name" :id="id" :disabled="disabled" v-model="value" type="checkbox">
|
<input
|
||||||
|
:id="id"
|
||||||
|
v-model="value"
|
||||||
|
class="switch-input"
|
||||||
|
:name="name"
|
||||||
|
:disabled="disabled"
|
||||||
|
type="checkbox"
|
||||||
|
/>
|
||||||
<div class="switch-paddle" :for="name">
|
<div class="switch-paddle" :for="name">
|
||||||
<span class="show-for-sr">on off</span>
|
<span class="show-for-sr">on off</span>
|
||||||
</div>
|
</div>
|
||||||
@ -24,12 +30,6 @@ export default {
|
|||||||
value: null,
|
value: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
beforeMount() {
|
|
||||||
this.value = this.checked;
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.$emit('input', this.value = !!this.checked);
|
|
||||||
},
|
|
||||||
computed: {
|
computed: {
|
||||||
classObject() {
|
classObject() {
|
||||||
const { type, size, value } = this;
|
const { type, size, value } = this;
|
||||||
@ -45,5 +45,11 @@ export default {
|
|||||||
this.$emit('input', val);
|
this.$emit('input', val);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
beforeMount() {
|
||||||
|
this.value = this.checked;
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$emit('input', (this.value = !!this.checked));
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -14,7 +14,7 @@
|
|||||||
<h3>
|
<h3>
|
||||||
{{ item.title }}
|
{{ item.title }}
|
||||||
<span v-if="isOver(item)" class="completed">
|
<span v-if="isOver(item)" class="completed">
|
||||||
<i class="ion-checkmark"></i>
|
<fluent-icon icon="checkmark" />
|
||||||
</span>
|
</span>
|
||||||
</h3>
|
</h3>
|
||||||
<span class="step">
|
<span class="step">
|
||||||
|
|||||||
@ -11,6 +11,7 @@
|
|||||||
class="icon"
|
class="icon"
|
||||||
:emoji="emoji"
|
:emoji="emoji"
|
||||||
:icon="icon"
|
:icon="icon"
|
||||||
|
:icon-size="iconSize"
|
||||||
/>
|
/>
|
||||||
<span v-if="$slots.default" class="button__content"><slot></slot></span>
|
<span v-if="$slots.default" class="button__content"><slot></slot></span>
|
||||||
</button>
|
</button>
|
||||||
@ -35,6 +36,10 @@ export default {
|
|||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
|
iconSize: {
|
||||||
|
type: [Number, String],
|
||||||
|
default: 20,
|
||||||
|
},
|
||||||
emoji: {
|
emoji: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
|
|||||||
@ -24,12 +24,11 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="remove-file-wrap">
|
<div class="remove-file-wrap">
|
||||||
<button
|
<woot-button
|
||||||
class="remove--attachment"
|
class="remove--attachment clear secondary"
|
||||||
|
icon="dismiss"
|
||||||
@click="() => onRemoveAttachment(index)"
|
@click="() => onRemoveAttachment(index)"
|
||||||
>
|
/>
|
||||||
<i class="ion-android-close"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -22,7 +22,7 @@ export default {
|
|||||||
},
|
},
|
||||||
color: {
|
color: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '#1f93ff',
|
default: '',
|
||||||
},
|
},
|
||||||
customStyle: {
|
customStyle: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@ -43,15 +43,21 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
style() {
|
style() {
|
||||||
return {
|
let style = {
|
||||||
width: `${this.size}px`,
|
width: `${this.size}px`,
|
||||||
height: `${this.size}px`,
|
height: `${this.size}px`,
|
||||||
borderRadius: this.rounded ? '50%' : 0,
|
borderRadius: this.rounded ? '50%' : 0,
|
||||||
lineHeight: `${this.size + Math.floor(this.size / 20)}px`,
|
lineHeight: `${this.size + Math.floor(this.size / 20)}px`,
|
||||||
backgroundColor: this.backgroundColor,
|
|
||||||
fontSize: `${Math.floor(this.size / 2.5)}px`,
|
fontSize: `${Math.floor(this.size / 2.5)}px`,
|
||||||
color: this.color,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (this.backgroundColor) {
|
||||||
|
style = { ...style, backgroundColor: this.backgroundColor };
|
||||||
|
}
|
||||||
|
if (this.color) {
|
||||||
|
style = { ...style, color: this.color };
|
||||||
|
}
|
||||||
|
return style;
|
||||||
},
|
},
|
||||||
userInitial() {
|
userInitial() {
|
||||||
return this.initials || this.initial(this.username);
|
return this.initials || this.initial(this.username);
|
||||||
@ -81,6 +87,7 @@ export default {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
background-image: linear-gradient(to top, #4481eb 0%, #04befe 100%);
|
background-image: linear-gradient(to top, var(--w-100) 0%, var(--w-75) 100%);
|
||||||
|
color: var(--w-600);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<span class="back-button ion-ios-arrow-left" @click.capture="goBack">
|
<button class="back-button" @click.capture="goBack">
|
||||||
|
<fluent-icon icon="chevron-left" />
|
||||||
{{ buttonLabel || $t('GENERAL_SETTINGS.BACK') }}
|
{{ buttonLabel || $t('GENERAL_SETTINGS.BACK') }}
|
||||||
</span>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import router from '../../routes/index';
|
import router from '../../routes/index';
|
||||||
@ -28,3 +29,4 @@ export default {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
<style scoped></style>
|
||||||
|
|||||||
@ -1,7 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="row empty-state">
|
<div class="row empty-state">
|
||||||
<h3 class="title">{{ title }}</h3>
|
<h3 class="title">
|
||||||
<p class="message">{{ message }}</p>
|
{{ title }}
|
||||||
|
</h3>
|
||||||
|
<p class="message">
|
||||||
|
{{ message }}
|
||||||
|
</p>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
263
app/javascript/dashboard/components/widgets/FilterInput.vue
Normal file
263
app/javascript/dashboard/components/widgets/FilterInput.vue
Normal file
@ -0,0 +1,263 @@
|
|||||||
|
<template>
|
||||||
|
<div class="filters">
|
||||||
|
<div class="filter">
|
||||||
|
<div class="filter-inputs">
|
||||||
|
<select
|
||||||
|
v-model="attributeKey"
|
||||||
|
class="filter__question"
|
||||||
|
@change="resetFilter()"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
v-for="attribute in filterAttributes"
|
||||||
|
:key="attribute.key"
|
||||||
|
:value="attribute.key"
|
||||||
|
>
|
||||||
|
{{ attribute.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select v-model="filterOperator" class="filter__operator">
|
||||||
|
<option
|
||||||
|
v-for="(operator, o) in operators"
|
||||||
|
:key="o"
|
||||||
|
:value="operator.value"
|
||||||
|
>
|
||||||
|
{{ $t(`FILTER.OPERATOR_LABELS.${operator.value}`) }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<div v-if="showUserInput" class="filter__answer--wrap">
|
||||||
|
<div
|
||||||
|
v-if="inputType === 'multi_select'"
|
||||||
|
class="multiselect-wrap--small"
|
||||||
|
>
|
||||||
|
<multiselect
|
||||||
|
v-model="values"
|
||||||
|
track-by="id"
|
||||||
|
label="name"
|
||||||
|
:placeholder="'Select'"
|
||||||
|
:multiple="true"
|
||||||
|
selected-label
|
||||||
|
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
|
||||||
|
deselect-label=""
|
||||||
|
:max-height="160"
|
||||||
|
:options="dropdownValues"
|
||||||
|
:allow-empty="false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="inputType === 'search_select'"
|
||||||
|
class="multiselect-wrap--small"
|
||||||
|
>
|
||||||
|
<multiselect
|
||||||
|
v-model="values"
|
||||||
|
track-by="id"
|
||||||
|
label="name"
|
||||||
|
:placeholder="'Select'"
|
||||||
|
selected-label
|
||||||
|
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
|
||||||
|
deselect-label=""
|
||||||
|
:max-height="160"
|
||||||
|
:options="dropdownValues"
|
||||||
|
:allow-empty="false"
|
||||||
|
:option-height="104"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
v-else
|
||||||
|
v-model="values"
|
||||||
|
type="text"
|
||||||
|
class="answer--text-input"
|
||||||
|
placeholder="Enter value"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<woot-button
|
||||||
|
icon="dismiss"
|
||||||
|
variant="clear"
|
||||||
|
color-scheme="secondary"
|
||||||
|
@click="removeFilter"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p v-if="v.values.$dirty && v.values.$error" class="filter-error">
|
||||||
|
{{ $t('FILTER.EMPTY_VALUE_ERROR') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="showQueryOperator" class="filter__join-operator">
|
||||||
|
<hr class="operator__line" />
|
||||||
|
<select v-model="query_operator" class="operator__select">
|
||||||
|
<option value="and">
|
||||||
|
{{ $t('FILTER.QUERY_DROPDOWN_LABELS.AND') }}
|
||||||
|
</option>
|
||||||
|
<option value="or">
|
||||||
|
{{ $t('FILTER.QUERY_DROPDOWN_LABELS.OR') }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
default: () => null,
|
||||||
|
},
|
||||||
|
filterAttributes: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
inputType: {
|
||||||
|
type: String,
|
||||||
|
default: 'plain_text',
|
||||||
|
},
|
||||||
|
operators: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
dropdownValues: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
showQueryOperator: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
v: {
|
||||||
|
type: Object,
|
||||||
|
default: () => null,
|
||||||
|
},
|
||||||
|
showUserInput: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
attributeKey: {
|
||||||
|
get() {
|
||||||
|
if (!this.value) return null;
|
||||||
|
return this.value.attribute_key;
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
const payload = this.value || {};
|
||||||
|
this.$emit('input', { ...payload, attribute_key: value });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
filterOperator: {
|
||||||
|
get() {
|
||||||
|
if (!this.value) return null;
|
||||||
|
return this.value.filter_operator;
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
const payload = this.value || {};
|
||||||
|
this.$emit('input', { ...payload, filter_operator: value });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
values: {
|
||||||
|
get() {
|
||||||
|
if (!this.value) return null;
|
||||||
|
return this.value.values;
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
const payload = this.value || {};
|
||||||
|
this.$emit('input', { ...payload, values: value });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
query_operator: {
|
||||||
|
get() {
|
||||||
|
if (!this.value) return null;
|
||||||
|
return this.value.query_operator;
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
const payload = this.value || {};
|
||||||
|
this.$emit('input', { ...payload, query_operator: value });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
removeFilter() {
|
||||||
|
this.$emit('removeFilter');
|
||||||
|
},
|
||||||
|
resetFilter() {
|
||||||
|
this.$emit('resetFilter');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.filter {
|
||||||
|
background: var(--color-background);
|
||||||
|
padding: var(--space-small);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--border-radius-medium);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-inputs {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-error {
|
||||||
|
color: var(--r-500);
|
||||||
|
display: block;
|
||||||
|
margin: var(--space-smaller) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter__question,
|
||||||
|
.filter__operator {
|
||||||
|
margin-bottom: var(--space-zero);
|
||||||
|
margin-right: var(--space-smaller);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter__question {
|
||||||
|
max-width: 30%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter__operator {
|
||||||
|
max-width: 20%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter__answer--wrap {
|
||||||
|
margin-right: var(--space-smaller);
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
|
input {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.filter__answer {
|
||||||
|
&.answer--text-input {
|
||||||
|
margin-bottom: var(--space-zero);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter__join-operator-wrap {
|
||||||
|
position: relative;
|
||||||
|
z-index: var(--z-index-twenty);
|
||||||
|
margin: var(--space-zero);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter__join-operator {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
|
margin: var(--space-one) var(--space-zero);
|
||||||
|
|
||||||
|
.operator__line {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.operator__select {
|
||||||
|
position: relative;
|
||||||
|
width: auto;
|
||||||
|
margin-bottom: var(--space-zero) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect {
|
||||||
|
margin-bottom: var(--space-zero);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -1,38 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="inbox">
|
|
||||||
<i :class="icon" />
|
|
||||||
<span>{{ inbox.name }}</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<script>
|
|
||||||
import { INBOX_TYPES } from 'shared/mixins/inboxMixin';
|
|
||||||
export default {
|
|
||||||
props: {
|
|
||||||
inbox: {
|
|
||||||
type: Object,
|
|
||||||
default: () => {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
icon() {
|
|
||||||
if (this.inbox.channel_type === INBOX_TYPES.WEB) {
|
|
||||||
return 'icon ion-earth';
|
|
||||||
}
|
|
||||||
return 'icon ion-android-textsms';
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.inbox {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
.icon {
|
|
||||||
margin-right: var(--space-micro);
|
|
||||||
min-width: var(--space-normal);
|
|
||||||
position: relative;
|
|
||||||
top: -1px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="inbox-item">
|
|
||||||
<img src="~dashboard/assets/images/no_page_image.png" alt="No Page Image" />
|
|
||||||
<div class="item--details columns">
|
|
||||||
<h4 class="item--name">
|
|
||||||
{{ inbox.label }}
|
|
||||||
</h4>
|
|
||||||
<p class="item--sub">
|
|
||||||
Facebook
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<!-- <span class="ion-chevron-right arrow"></span> -->
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<script>
|
|
||||||
/* eslint no-console: 0 */
|
|
||||||
// import WootSwitch from '../ui/Switch';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
props: ['inbox'],
|
|
||||||
created() {},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user