diff --git a/.codeclimate.yml b/.codeclimate.yml index 974681a09..d8b8d985b 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -54,3 +54,5 @@ exclude_patterns: - 'app/javascript/widget/i18n/index.js' - 'app/javascript/survey/i18n/index.js' - 'app/javascript/shared/constants/locales.js' + - 'app/javascript/dashboard/helper/specs/macrosFixtures.js' + - 'app/javascript/dashboard/routes/dashboard/settings/macros/constants.js' diff --git a/.env.example b/.env.example index 12262576e..62b08a7b1 100644 --- a/.env.example +++ b/.env.example @@ -34,6 +34,11 @@ REDIS_SENTINELS= # You can find list of master using "SENTINEL masters" command REDIS_SENTINEL_MASTER_NAME= +# By default Chatwoot will pass REDIS_PASSWORD as the password value for sentinels +# Use the following environment variable to customize passwords for sentinels. +# Use empty string if sentinels are configured with out passwords +# REDIS_SENTINEL_PASSWORD= + # Redis premium breakage in heroku fix # enable the following configuration # ref: https://github.com/chatwoot/chatwoot/issues/2420 @@ -51,7 +56,7 @@ RAILS_MAX_THREADS=5 # The email from which all outgoing emails are sent # could user either `email@yourdomain.com` or `BrandName ` -MAILER_SENDER_EMAIL="Chatwoot " +MAILER_SENDER_EMAIL=Chatwoot #SMTP domain key is set up for HELO checking SMTP_DOMAIN=chatwoot.com diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index d9409af96..26b053b5f 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -6,6 +6,7 @@ labels: 'Bug' assignees: '' --- + **Describe the bug** A clear and concise description of what the bug is. @@ -16,11 +17,11 @@ Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' -4. See error +4. See the error **Expected behavior** -A clear and concise description of what you expected to happen. +Share a clear and concise description of what you expected to happen. **Screenshots** @@ -28,27 +29,50 @@ If applicable, add screenshots to help explain your problem. **Browser logs** -Share the browser logs to debug the issue further +Share the browser logs to debug the issue further. **Server logs** -Share the server logs to debug the issue further +Share the server logs to debug the issue further. **Environment** -Describe whether you are using Chatwoot Cloud (app.chatwoot.com) or a self hosted installation of Chatwoot. If you are using a self hosted installation of Chatwoot describe the type of deployment (Docker/Linux VM installation/Heroku) +Describe whether you are using Chatwoot Cloud (app.chatwoot.com) or a self-hosted installation of Chatwoot. If you are using a self-hosted installation of Chatwoot, describe the type of deployment (Docker/Linux VM installation/Heroku/Kubernetes/Other). -**Desktop (please complete the following information):** - - OS: [e.g. iOS] - - Browser [e.g. chrome, safari] +- [ ] app.chatwoot.com (Chatwoot Cloud) +- [ ] Self-hosted +- - [ ] Linux VM +- - [ ] Docker +- - [ ] Kubernetes +- - [ ] Heroku +- - [ ] Other (Please specify) + + +**Desktop (please complete the following information)** (If applicable) + - OS: [e.g. Linux, Windows, MacOS] + - Browser [e.g. chrome, firefox, safari] - Version [e.g. 22] -**Smartphone (please complete the following information):** - - Device: [e.g. iPhone6] +**Smartphone (please complete the following information)** (If applicable) + - Device: [e.g. iPhone6, Pixel7] - OS: [e.g. iOS8.1] - - Browser [e.g. stock browser, safari] + - Browser [e.g. stock browser, firefox, safari] - Version [e.g. 22] +**Docker** (If applicable) + +Please share the output of the following. +- `docker version` +- `docker info` +- `docker-compose version` + +**Cloud Provider** (If applicable) +- [ ] AWS +- [ ] GCP +- [ ] Azure +- [ ] DigitalOcean +- [ ] Others + **Additional context** Add any other context about the problem here. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 4ce1b084c..cd4d83b38 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -2,8 +2,7 @@ ## Description -Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. - +Please include a summary of the change and issue(s) fixed. Also, mention relevant motivation, context, and any dependencies that this change requires. Fixes # (issue) ## Type of change @@ -12,18 +11,18 @@ Please delete options that are not relevant. - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) -- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) +- [ ] Breaking change (fix or feature that would cause existing functionality not to work as expected) - [ ] This change requires a documentation update ## How Has This Been Tested? -Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration +Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration. ## Checklist: - [ ] My code follows the style guidelines of this project -- [ ] I have performed a self-review of my own code +- [ ] I have performed a self-review of my code - [ ] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [ ] My changes generate no new warnings diff --git a/.gitignore b/.gitignore index 11a8c50e3..fc77a7b55 100644 --- a/.gitignore +++ b/.gitignore @@ -60,3 +60,5 @@ test/cypress/videos/* /config/master.key /config/*.enc + +.vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/Gemfile.lock b/Gemfile.lock index 5b14d5b5e..686c3a37b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -427,14 +427,14 @@ GEM netrc (0.11.0) newrelic_rpm (8.9.0) nio4r (2.5.8) - nokogiri (1.13.7) + nokogiri (1.13.9) mini_portile2 (~> 2.8.0) racc (~> 1.4) - nokogiri (1.13.7-arm64-darwin) + nokogiri (1.13.9-arm64-darwin) racc (~> 1.4) - nokogiri (1.13.7-x86_64-darwin) + nokogiri (1.13.9-x86_64-darwin) racc (~> 1.4) - nokogiri (1.13.7-x86_64-linux) + nokogiri (1.13.9-x86_64-linux) racc (~> 1.4) oauth (0.5.10) orm_adapter (0.5.0) @@ -808,4 +808,4 @@ RUBY VERSION ruby 3.0.4p208 BUNDLED WITH - 2.3.18 + 2.3.16 diff --git a/app/builders/messages/instagram/message_builder.rb b/app/builders/messages/instagram/message_builder.rb index 3b8ead18c..1be960d21 100644 --- a/app/builders/messages/instagram/message_builder.rb +++ b/app/builders/messages/instagram/message_builder.rb @@ -72,6 +72,7 @@ class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder def build_message return if @outgoing_echo && already_sent_from_chatwoot? + return if message_content.blank? && all_unsupported_files? @message = conversation.messages.create!(message_params) @@ -117,6 +118,13 @@ class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder cw_message.present? end + def all_unsupported_files? + return if attachments.empty? + + attachments_type = attachments.pluck(:type).uniq.first + unsupported_file_type?(attachments_type) + end + ### Sample response # { # "object": "instagram", diff --git a/app/builders/messages/message_builder.rb b/app/builders/messages/message_builder.rb index e9bf0802b..69ed786ce 100644 --- a/app/builders/messages/message_builder.rb +++ b/app/builders/messages/message_builder.rb @@ -35,7 +35,13 @@ class Messages::MessageBuilder file: uploaded_attachment ) - attachment.file_type = file_type(uploaded_attachment&.content_type) if uploaded_attachment.is_a?(ActionDispatch::Http::UploadedFile) + attachment.file_type = if uploaded_attachment.is_a?(String) + file_type_by_signed_id( + uploaded_attachment + ) + else + file_type(uploaded_attachment&.content_type) + end end end diff --git a/app/builders/messages/messenger/message_builder.rb b/app/builders/messages/messenger/message_builder.rb index 3c406f1e0..d3b5bf6b9 100644 --- a/app/builders/messages/messenger/message_builder.rb +++ b/app/builders/messages/messenger/message_builder.rb @@ -2,7 +2,8 @@ class Messages::Messenger::MessageBuilder include ::FileTypeHelper def process_attachment(attachment) - return if attachment['type'].to_sym == :template + # This check handles very rare case if there are multiple files to attach with only one usupported file + return if unsupported_file_type?(attachment['type']) attachment_obj = @message.attachments.new(attachment_params(attachment).except(:remote_file_url)) attachment_obj.save! @@ -80,4 +81,10 @@ class Messages::Messenger::MessageBuilder ChatwootExceptionTracker.new(e, account: @inbox.account).capture_exception {} end + + private + + def unsupported_file_type?(attachment_type) + [:template, :unsupported_type].include? attachment_type.to_sym + end end diff --git a/app/controllers/api/v1/accounts/articles_controller.rb b/app/controllers/api/v1/accounts/articles_controller.rb index d650b19d9..a7891ffd4 100644 --- a/app/controllers/api/v1/accounts/articles_controller.rb +++ b/app/controllers/api/v1/accounts/articles_controller.rb @@ -5,9 +5,10 @@ class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController before_action :set_current_page, only: [:index] def index - @articles_count = @portal.articles.count - @articles = @portal.articles - @articles = @articles.search(list_params) if list_params.present? + @portal_articles = @portal.articles + @all_articles = @portal_articles.search(list_params) + @articles_count = @all_articles.count + @articles = @all_articles.page(@current_page) end def create @@ -37,7 +38,7 @@ class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController end def portal - @portal ||= Current.account.portals.find_by(slug: params[:portal_id]) + @portal ||= Current.account.portals.find_by!(slug: params[:portal_id]) end def article_params diff --git a/app/controllers/api/v1/accounts/categories_controller.rb b/app/controllers/api/v1/accounts/categories_controller.rb index bc18b61b8..e28e601d5 100644 --- a/app/controllers/api/v1/accounts/categories_controller.rb +++ b/app/controllers/api/v1/accounts/categories_controller.rb @@ -5,6 +5,7 @@ class Api::V1::Accounts::CategoriesController < Api::V1::Accounts::BaseControlle before_action :set_current_page, only: [:index] def index + @current_locale = params[:locale] @categories = @portal.categories.search(params) end diff --git a/app/controllers/api/v1/accounts/macros_controller.rb b/app/controllers/api/v1/accounts/macros_controller.rb index e7946a54e..a13d74995 100644 --- a/app/controllers/api/v1/accounts/macros_controller.rb +++ b/app/controllers/api/v1/accounts/macros_controller.rb @@ -1,6 +1,6 @@ class Api::V1::Accounts::MacrosController < Api::V1::Accounts::BaseController - before_action :check_authorization before_action :fetch_macro, only: [:show, :update, :destroy, :execute] + before_action :check_authorization, only: [:show, :update, :destroy, :execute] def index @macros = Macro.with_visibility(current_user, params) @@ -14,6 +14,8 @@ class Api::V1::Accounts::MacrosController < Api::V1::Accounts::BaseController render json: { error: @macro.errors.messages }, status: :unprocessable_entity and return unless @macro.valid? @macro.save! + process_attachments + @macro end def show @@ -25,10 +27,21 @@ class Api::V1::Accounts::MacrosController < Api::V1::Accounts::BaseController head :ok end + def attach_file + file_blob = ActiveStorage::Blob.create_and_upload!( + key: nil, + io: params[:attachment].tempfile, + filename: params[:attachment].original_filename, + content_type: params[:attachment].content_type + ) + render json: { blob_key: file_blob.key, blob_id: file_blob.id } + end + def update ActiveRecord::Base.transaction do @macro.update!(macros_with_user) @macro.set_visibility(current_user, permitted_params) + process_attachments @macro.save! rescue StandardError => e Rails.logger.error e @@ -42,6 +55,19 @@ class Api::V1::Accounts::MacrosController < Api::V1::Accounts::BaseController head :ok end + private + + def process_attachments + actions = @macro.actions.filter_map { |k, _v| k if k['action_name'] == 'send_attachment' } + return if actions.blank? + + actions.each do |action| + blob_id = action['action_params'] + blob = ActiveStorage::Blob.find_by(id: blob_id) + @macro.files.attach(blob) + end + end + def permitted_params params.permit( :name, :account_id, :visibility, @@ -56,4 +82,8 @@ class Api::V1::Accounts::MacrosController < Api::V1::Accounts::BaseController def fetch_macro @macro = Current.account.macros.find_by(id: params[:id]) end + + def check_authorization + authorize(@macro) if @macro.present? + end end diff --git a/app/controllers/api/v1/accounts/portals_controller.rb b/app/controllers/api/v1/accounts/portals_controller.rb index 6ac000b58..62a98a872 100644 --- a/app/controllers/api/v1/accounts/portals_controller.rb +++ b/app/controllers/api/v1/accounts/portals_controller.rb @@ -14,7 +14,10 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController @portal.members << agents end - def show; end + def show + @all_articles = @portal.articles + @articles = @all_articles.search(locale: params[:locale]) + end def create @portal = Current.account.portals.build(portal_params) diff --git a/app/controllers/api/v1/widget/base_controller.rb b/app/controllers/api/v1/widget/base_controller.rb index 9ec702e6c..229cf27e9 100644 --- a/app/controllers/api/v1/widget/base_controller.rb +++ b/app/controllers/api/v1/widget/base_controller.rb @@ -50,7 +50,9 @@ class Api::V1::Widget::BaseController < ApplicationController end def contact_name - params[:contact][:name] || contact_email.split('@')[0] if contact_email.present? + return if @contact.email.present? || @contact.phone_number.present? || @contact.identifier.present? + + permitted_params.dig(:contact, :name) || (contact_email.split('@')[0] if contact_email.present?) end def contact_phone_number diff --git a/app/controllers/api/v1/widget/conversations_controller.rb b/app/controllers/api/v1/widget/conversations_controller.rb index 83206de90..9b8c4da81 100644 --- a/app/controllers/api/v1/widget/conversations_controller.rb +++ b/app/controllers/api/v1/widget/conversations_controller.rb @@ -17,7 +17,8 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController @contact = ContactIdentifyAction.new( contact: @contact, params: { email: contact_email, phone_number: contact_phone_number, name: contact_name }, - retain_original_contact_name: true + retain_original_contact_name: true, + discard_invalid_attrs: true ).perform end diff --git a/app/controllers/platform/api/v1/accounts_controller.rb b/app/controllers/platform/api/v1/accounts_controller.rb index 9c8f60f38..2873cc22c 100644 --- a/app/controllers/platform/api/v1/accounts_controller.rb +++ b/app/controllers/platform/api/v1/accounts_controller.rb @@ -1,18 +1,17 @@ class Platform::Api::V1::AccountsController < PlatformController def create @resource = Account.new(account_params) + update_resource_features @resource.save! @platform_app.platform_app_permissibles.find_or_create_by(permissible: @resource) - render json: @resource end - def show - render json: @resource - end + def show; end def update - @resource.update!(account_params) - render json: @resource + @resource.assign_attributes(account_params) + update_resource_features + @resource.save! end def destroy @@ -27,6 +26,18 @@ class Platform::Api::V1::AccountsController < PlatformController end def account_params - params.permit(:name, :locale) + permitted_params.except(:features) + end + + def update_resource_features + return if permitted_params[:features].blank? + + permitted_params[:features].each do |key, value| + value.present? ? @resource.enable_features(key) : @resource.disable_features(key) + end + end + + def permitted_params + params.permit(:name, :locale, :domain, :support_email, :status, features: {}, limits: {}, custom_attributes: {}) end end diff --git a/app/controllers/public/api/v1/inboxes/contacts_controller.rb b/app/controllers/public/api/v1/inboxes/contacts_controller.rb index 1fde3051e..b3ebd4ade 100644 --- a/app/controllers/public/api/v1/inboxes/contacts_controller.rb +++ b/app/controllers/public/api/v1/inboxes/contacts_controller.rb @@ -7,7 +7,7 @@ class Public::Api::V1::Inboxes::ContactsController < Public::Api::V1::InboxesCon @contact_inbox = ::ContactInboxWithContactBuilder.new( source_id: source_id, inbox: @inbox_channel.inbox, - contact_attributes: permitted_params.except(:identifier, :identifier_hash) + contact_attributes: permitted_params.except(:identifier_hash) ).perform end diff --git a/app/controllers/public/api/v1/inboxes_controller.rb b/app/controllers/public/api/v1/inboxes_controller.rb index a57e72e40..65fad57b1 100644 --- a/app/controllers/public/api/v1/inboxes_controller.rb +++ b/app/controllers/public/api/v1/inboxes_controller.rb @@ -3,9 +3,15 @@ class Public::Api::V1::InboxesController < PublicController before_action :set_contact_inbox before_action :set_conversation + def show + @inbox_channel = ::Channel::Api.find_by!(identifier: params[:id]) + end + private def set_inbox_channel + return if params[:inbox_id].blank? + @inbox_channel = ::Channel::Api.find_by!(identifier: params[:inbox_id]) end diff --git a/app/controllers/public/api/v1/portals/articles_controller.rb b/app/controllers/public/api/v1/portals/articles_controller.rb index 0dfa2ea9b..ae0fb2b6a 100644 --- a/app/controllers/public/api/v1/portals/articles_controller.rb +++ b/app/controllers/public/api/v1/portals/articles_controller.rb @@ -1,7 +1,7 @@ class Public::Api::V1::Portals::ArticlesController < PublicController before_action :ensure_custom_domain_request, only: [:show, :index] before_action :portal - before_action :set_category + before_action :set_category, except: [:index] before_action :set_article, only: [:show] layout 'portal' @@ -20,7 +20,7 @@ class Public::Api::V1::Portals::ArticlesController < PublicController end def set_category - @category = @portal.categories.find_by!(slug: params[:category_slug]) + @category = @portal.categories.find_by!(slug: params[:category_slug]) if params[:category_slug].present? end def portal diff --git a/app/helpers/file_type_helper.rb b/app/helpers/file_type_helper.rb index db3f6249d..03b807aad 100644 --- a/app/helpers/file_type_helper.rb +++ b/app/helpers/file_type_helper.rb @@ -8,6 +8,12 @@ module FileTypeHelper :file end + # Used in case of DIRECT_UPLOADS_ENABLED=true + def file_type_by_signed_id(signed_id) + blob = ActiveStorage::Blob.find_signed(signed_id) + file_type(blob&.content_type) + end + def image_file?(content_type) [ 'image/jpeg', diff --git a/app/javascript/dashboard/api/helpCenter/categories.js b/app/javascript/dashboard/api/helpCenter/categories.js index b6eae3f32..01658497e 100644 --- a/app/javascript/dashboard/api/helpCenter/categories.js +++ b/app/javascript/dashboard/api/helpCenter/categories.js @@ -7,8 +7,8 @@ class CategoriesAPI extends PortalsAPI { super('categories', { accountScoped: true }); } - get({ portalSlug }) { - return axios.get(`${this.url}/${portalSlug}/categories`); + get({ portalSlug, locale }) { + return axios.get(`${this.url}/${portalSlug}/categories?locale=${locale}`); } create({ portalSlug, categoryObj }) { diff --git a/app/javascript/dashboard/api/helpCenter/portals.js b/app/javascript/dashboard/api/helpCenter/portals.js index 520453540..28220b0b5 100644 --- a/app/javascript/dashboard/api/helpCenter/portals.js +++ b/app/javascript/dashboard/api/helpCenter/portals.js @@ -6,6 +6,10 @@ class PortalsAPI extends ApiClient { super('portals', { accountScoped: true }); } + getPortal({ portalSlug, locale }) { + return axios.get(`${this.url}/${portalSlug}?locale=${locale}`); + } + updatePortal({ portalSlug, portalObj }) { return axios.patch(`${this.url}/${portalSlug}`, portalObj); } diff --git a/app/javascript/dashboard/api/inbox/conversation.js b/app/javascript/dashboard/api/inbox/conversation.js index 9f6b8baf1..22548499d 100644 --- a/app/javascript/dashboard/api/inbox/conversation.js +++ b/app/javascript/dashboard/api/inbox/conversation.js @@ -105,6 +105,16 @@ class ConversationApi extends ApiClient { custom_attributes: customAttributes, }); } + + fetchParticipants(conversationId) { + return axios.get(`${this.url}/${conversationId}/participants`); + } + + updateParticipants({ conversationId, userIds }) { + return axios.patch(`${this.url}/${conversationId}/participants`, { + user_ids: userIds, + }); + } } export default new ConversationApi(); diff --git a/app/javascript/dashboard/components/index.js b/app/javascript/dashboard/components/index.js index 5e58aa049..f5ce5d856 100644 --- a/app/javascript/dashboard/components/index.js +++ b/app/javascript/dashboard/components/index.js @@ -5,9 +5,12 @@ import Button from './ui/WootButton'; import Code from './Code'; import ColorPicker from './widgets/ColorPicker'; import ConfirmDeleteModal from './widgets/modal/ConfirmDeleteModal.vue'; +import ConfirmModal from './widgets/modal/ConfirmationModal.vue'; +import ContextMenu from './ui/ContextMenu.vue'; import DeleteModal from './widgets/modal/DeleteModal.vue'; import DropdownItem from 'shared/components/ui/dropdown/DropdownItem'; import DropdownMenu from 'shared/components/ui/dropdown/DropdownMenu'; +import FeatureToggle from './widgets/FeatureToggle'; import HorizontalBar from './widgets/chart/HorizontalBarChart'; import Input from './widgets/forms/Input.vue'; import Label from './ui/Label'; @@ -21,8 +24,6 @@ import SubmitButton from './buttons/FormSubmitButton'; import Tabs from './ui/Tabs/Tabs'; import TabsItem from './ui/Tabs/TabsItem'; import Thumbnail from './widgets/Thumbnail.vue'; -import ConfirmModal from './widgets/modal/ConfirmationModal.vue'; -import ContextMenu from './ui/ContextMenu.vue'; const WootUIKit = { AvatarUploader, @@ -31,9 +32,12 @@ const WootUIKit = { Code, ColorPicker, ConfirmDeleteModal, + ConfirmModal, + ContextMenu, DeleteModal, DropdownItem, DropdownMenu, + FeatureToggle, HorizontalBar, Input, Label, @@ -47,8 +51,6 @@ const WootUIKit = { Tabs, TabsItem, Thumbnail, - ConfirmModal, - ContextMenu, install(Vue) { const keys = Object.keys(this); keys.pop(); // remove 'install' from keys diff --git a/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryChildNavItem.vue b/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryChildNavItem.vue index de10dda4a..2dd23544f 100644 --- a/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryChildNavItem.vue +++ b/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryChildNavItem.vue @@ -26,7 +26,7 @@ :class="{ 'text-truncate': shouldTruncate }" > {{ label }} - + {{ childItemCount }} @@ -76,7 +76,7 @@ export default { type: String, default: '', }, - isHelpCenterSidebar: { + showChildCount: { type: Boolean, default: false, }, @@ -127,11 +127,16 @@ $label-badge-size: var(--space-slab); color: var(--w-500); border-color: var(--w-25); } + &.is-active .count-view { + background: var(--w-75); + color: var(--w-500); + } } .menu-label { flex-grow: 1; - line-height: var(--space-two); + display: inline-flex; + align-items: center; } .inbox-icon { @@ -175,10 +180,6 @@ $label-badge-size: var(--space-slab); font-weight: var(--font-weight-bold); margin-left: var(--space-smaller); padding: var(--space-zero) var(--space-smaller); - - &.is-active { - background: var(--w-50); - color: var(--w-500); - } + line-height: var(--font-size-small); } diff --git a/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryNavItem.vue b/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryNavItem.vue index 8661a488f..644258f50 100644 --- a/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryNavItem.vue +++ b/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryNavItem.vue @@ -4,16 +4,15 @@ {{ $t(`SIDEBAR.${menuItem.label}`) }} - {{ $t(`SIDEBAR.${menuItem.label}`) }} - + {{ `${menuItem.count}` }} -
  • +
  • -

    - {{ $t('SIDEBAR.HELP_CENTER.CATEGORY_EMPTY_MESSAGE') }} -

    @@ -104,14 +96,6 @@ export default { type: Object, default: () => ({}), }, - isHelpCenterSidebar: { - type: Boolean, - default: false, - }, - isCategoryEmpty: { - type: Boolean, - default: false, - }, }, computed: { ...mapGetters({ @@ -161,8 +145,8 @@ export default { this.menuItem.toStateName === 'settings_applications' ); }, - isArticlesView() { - return this.$store.state.route.name === this.menuItem.toStateName; + isCurrentRoute() { + return this.$store.state.route.name.includes(this.menuItem.toStateName); }, computedClass() { @@ -181,12 +165,11 @@ export default { } return ' '; } - if (this.isHelpCenterSidebar) { - if (this.isArticlesView) { - return 'is-active'; - } - return ' '; + + if (this.isCurrentRoute) { + return 'is-active'; } + return ''; }, }, @@ -222,6 +205,9 @@ export default { onClickOpen() { this.$emit('open'); }, + showChildCount(count) { + return Number.isInteger(count); + }, }, }; @@ -277,6 +263,11 @@ export default { color: var(--w-500); border-color: var(--w-25); } + + &.is-active .count-view { + background: var(--w-75); + color: var(--w-600); + } } .secondary-menu--icon { @@ -306,15 +297,12 @@ export default { 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); +.sidebar-item .menu-item--new { + padding: var(--space-small) 0; - &:hover { - color: var(--w-500); + .button { + display: inline-flex; + color: var(--s-500); } } @@ -340,11 +328,6 @@ export default { font-weight: var(--font-weight-bold); margin-left: var(--space-smaller); padding: var(--space-zero) var(--space-smaller); - - &.is-active { - background: var(--w-50); - color: var(--w-500); - } } .submenu-icons { @@ -356,10 +339,4 @@ export default { margin-left: var(--space-small); } } - -.empty-text { - color: var(--s-500); - font-size: var(--font-size-small); - margin: var(--space-smaller); -} diff --git a/app/javascript/dashboard/components/ui/TimeAgo.vue b/app/javascript/dashboard/components/ui/TimeAgo.vue index c262a8aa0..c796ed0f6 100644 --- a/app/javascript/dashboard/components/ui/TimeAgo.vue +++ b/app/javascript/dashboard/components/ui/TimeAgo.vue @@ -1,17 +1,15 @@ diff --git a/app/javascript/dashboard/components/widgets/FilterInput/Index.vue b/app/javascript/dashboard/components/widgets/FilterInput/Index.vue index 46c64fc53..b3dd67dc3 100644 --- a/app/javascript/dashboard/components/widgets/FilterInput/Index.vue +++ b/app/javascript/dashboard/components/widgets/FilterInput/Index.vue @@ -32,6 +32,7 @@ v-for="attribute in filterAttributes" :key="attribute.key" :value="attribute.key" + :disabled="attribute.disabled" > {{ attribute.name }} @@ -173,6 +174,10 @@ export default { type: Array, default: () => [], }, + customAttributeType: { + type: String, + default: '', + }, }, computed: { attributeKey: { diff --git a/app/javascript/dashboard/components/widgets/TableFooter.vue b/app/javascript/dashboard/components/widgets/TableFooter.vue index c89b37b01..e28099bff 100644 --- a/app/javascript/dashboard/components/widgets/TableFooter.vue +++ b/app/javascript/dashboard/components/widgets/TableFooter.vue @@ -83,75 +83,71 @@ export default { }, pageSize: { type: Number, - default: 15, + default: 25, }, totalCount: { type: Number, default: 0, }, - onPageChange: { - type: Function, - default: () => {}, - }, }, computed: { isFooterVisible() { return this.totalCount && !(this.firstIndex > this.totalCount); }, firstIndex() { - const firstIndex = this.pageSize * (this.currentPage - 1) + 1; - return firstIndex; + return this.pageSize * (this.currentPage - 1) + 1; }, lastIndex() { - const index = Math.min(this.totalCount, this.pageSize * this.currentPage); - return index; + return Math.min(this.totalCount, this.pageSize * this.currentPage); }, searchButtonClass() { return this.searchQuery !== '' ? 'show' : ''; }, hasLastPage() { - const isDisabled = - this.currentPage === Math.ceil(this.totalCount / this.pageSize); - return isDisabled; + return !!Math.ceil(this.totalCount / this.pageSize); }, hasFirstPage() { - const isDisabled = this.currentPage === 1; - return isDisabled; + return this.currentPage === 1; }, hasNextPage() { - const isDisabled = - this.currentPage === Math.ceil(this.totalCount / this.pageSize); - return isDisabled; + return this.currentPage === Math.ceil(this.totalCount / this.pageSize); }, hasPrevPage() { - const isDisabled = this.currentPage === 1; - return isDisabled; + return this.currentPage === 1; }, }, methods: { onNextPage() { - if (this.hasNextPage) return; + if (this.hasNextPage) { + return; + } const newPage = this.currentPage + 1; this.onPageChange(newPage); }, onPrevPage() { - if (this.hasPrevPage) return; - + if (this.hasPrevPage) { + return; + } const newPage = this.currentPage - 1; this.onPageChange(newPage); }, onFirstPage() { - if (this.hasFirstPage) return; - + if (this.hasFirstPage) { + return; + } const newPage = 1; this.onPageChange(newPage); }, onLastPage() { - if (this.hasLastPage) return; - + if (this.hasLastPage) { + return; + } const newPage = Math.ceil(this.totalCount / this.pageSize); this.onPageChange(newPage); }, + onPageChange(page) { + this.$emit('page-change', page); + }, }, }; diff --git a/app/javascript/dashboard/components/widgets/Thumbnail.spec.js b/app/javascript/dashboard/components/widgets/Thumbnail.spec.js index c10cd3d16..1ea5e6cc6 100644 --- a/app/javascript/dashboard/components/widgets/Thumbnail.spec.js +++ b/app/javascript/dashboard/components/widgets/Thumbnail.spec.js @@ -2,49 +2,47 @@ import { mount } from '@vue/test-utils'; import Avatar from './Avatar.vue'; import Thumbnail from './Thumbnail.vue'; -describe(`when there are NO errors loading the thumbnail`, () => { - it(`should render the agent thumbnail`, () => { +describe('Thumbnail.vue', () => { + it('should render the agent thumbnail if valid image is passed', () => { const wrapper = mount(Thumbnail, { propsData: { src: 'https://some_valid_url.com', }, data() { return { + hasImageLoaded: true, imgError: false, }; }, }); - expect(wrapper.find('#image').exists()).toBe(true); + expect(wrapper.find('.user-thumbnail').exists()).toBe(true); const avatarComponent = wrapper.findComponent(Avatar); - expect(avatarComponent.exists()).toBe(false); + expect(avatarComponent.isVisible()).toBe(false); }); -}); -describe(`when there ARE errors loading the thumbnail`, () => { - it(`should render the agent avatar`, () => { + it('should render the avatar component if invalid image is passed', () => { const wrapper = mount(Thumbnail, { propsData: { src: 'https://some_invalid_url.com', }, data() { return { + hasImageLoaded: true, imgError: true, }; }, }); expect(wrapper.find('#image').exists()).toBe(false); const avatarComponent = wrapper.findComponent(Avatar); - expect(avatarComponent.exists()).toBe(true); + expect(avatarComponent.isVisible()).toBe(true); }); -}); -describe(`when Avatar shows`, () => { - it(`initials shold correspond to username`, () => { + it('should the initial of the name if no image is passed', () => { const wrapper = mount(Avatar, { propsData: { username: 'Angie Rojas', }, }); - expect(wrapper.find('span').text()).toBe('AR'); + expect(wrapper.find('div').text()).toBe('AR'); }); }); diff --git a/app/javascript/dashboard/components/widgets/Thumbnail.vue b/app/javascript/dashboard/components/widgets/Thumbnail.vue index 4bfc729de..c827135df 100644 --- a/app/javascript/dashboard/components/widgets/Thumbnail.vue +++ b/app/javascript/dashboard/components/widgets/Thumbnail.vue @@ -1,74 +1,29 @@ + + + + diff --git a/app/javascript/dashboard/components/widgets/conversation/contextMenu/Index.vue b/app/javascript/dashboard/components/widgets/conversation/contextMenu/Index.vue index 4a1713165..a8a5f3cb3 100644 --- a/app/javascript/dashboard/components/widgets/conversation/contextMenu/Index.vue +++ b/app/javascript/dashboard/components/widgets/conversation/contextMenu/Index.vue @@ -17,7 +17,10 @@ @click="snoozeConversation(option.snoozedUntil)" /> - + - + - + - + @@ -50,7 +50,6 @@ export default { padding: var(--space-smaller); border-radius: var(--border-radius-small); overflow: hidden; - .menu-label { margin: 0; font-size: var(--font-size-mini); diff --git a/app/javascript/dashboard/components/widgets/conversation/contextMenu/menuItemWithSubmenu.vue b/app/javascript/dashboard/components/widgets/conversation/contextMenu/menuItemWithSubmenu.vue index 08922bc37..04870ccc1 100644 --- a/app/javascript/dashboard/components/widgets/conversation/contextMenu/menuItemWithSubmenu.vue +++ b/app/javascript/dashboard/components/widgets/conversation/contextMenu/menuItemWithSubmenu.vue @@ -1,11 +1,14 @@ @@ -60,12 +60,12 @@ export default { }, pageSize: { type: Number, - default: 15, + default: 25, }, }, methods: { onPageChange(page) { - this.$emit('on-page-change', page); + this.$emit('page-change', page); }, }, }; diff --git a/app/javascript/dashboard/routes/dashboard/helpcenter/components/HelpCenterLayout.vue b/app/javascript/dashboard/routes/dashboard/helpcenter/components/HelpCenterLayout.vue index 562d6bd07..b4626e108 100644 --- a/app/javascript/dashboard/routes/dashboard/helpcenter/components/HelpCenterLayout.vue +++ b/app/javascript/dashboard/routes/dashboard/helpcenter/components/HelpCenterLayout.vue @@ -2,6 +2,7 @@
    + ({ id: category.id, @@ -216,6 +225,13 @@ export default { return this.selectedPortal ? this.selectedPortal.name : ''; }, }, + + watch: { + '$route.params.portalSlug'() { + this.fetchPortalsAndItsCategories(); + }, + }, + mounted() { window.addEventListener('resize', this.handleResize); this.handleResize(); @@ -232,7 +248,7 @@ export default { }, updated() { const slug = this.$route.params.portalSlug; - if (slug) { + if (slug !== this.lastActivePortalSlug) { this.lastActivePortalSlug = slug; this.updateUISettings({ last_active_portal_slug: slug, @@ -251,12 +267,14 @@ export default { toggleSidebar() { this.isSidebarOpen = !this.isSidebarOpen; }, - fetchPortalsAndItsCategories() { - this.$store.dispatch('portals/index').then(() => { - this.$store.dispatch('categories/index', { - portalSlug: this.selectedPortalSlug, - }); - }); + async fetchPortalsAndItsCategories() { + await this.$store.dispatch('portals/index'); + const selectedPortalParam = { + portalSlug: this.selectedPortalSlug, + locale: this.selectedLocaleInPortal, + }; + this.$store.dispatch('portals/show', selectedPortalParam); + this.$store.dispatch('categories/index', selectedPortalParam); this.$store.dispatch('agents/get'); }, toggleKeyShortcutModal() { @@ -277,12 +295,15 @@ export default { closePortalPopover() { this.showPortalPopover = false; }, - onClickOpenAddCatogoryModal() { + onClickOpenAddCategoryModal() { this.showAddCategoryModal = true; }, onClickCloseAddCategoryModal() { this.showAddCategoryModal = false; }, + toggleAccountModal() { + this.showAccountModal = !this.showAccountModal; + }, }, }; diff --git a/app/javascript/dashboard/routes/dashboard/helpcenter/components/PortalListItem.vue b/app/javascript/dashboard/routes/dashboard/helpcenter/components/PortalListItem.vue index 0354ad22b..045bd77cd 100644 --- a/app/javascript/dashboard/routes/dashboard/helpcenter/components/PortalListItem.vue +++ b/app/javascript/dashboard/routes/dashboard/helpcenter/components/PortalListItem.vue @@ -376,7 +376,6 @@ export default { } } .portal-title { - color: var(--s-900); margin-bottom: 0; } .portal-count { @@ -389,14 +388,17 @@ export default { } } .portal-locales { - margin-top: var(--space-medium); - margin-bottom: var(--space-small); + margin-bottom: var(--space-large); .locale-title { color: var(--s-800); font-weight: var(--font-weight-medium); margin-bottom: var(--space-small); } } + + .portal--heading { + margin-bottom: var(--space-normal); + } } .portal-settings--icon { padding: var(--space-smaller); diff --git a/app/javascript/dashboard/routes/dashboard/helpcenter/components/PortalSettingsBasicForm.vue b/app/javascript/dashboard/routes/dashboard/helpcenter/components/PortalSettingsBasicForm.vue index 796d029d6..e13c60061 100644 --- a/app/javascript/dashboard/routes/dashboard/helpcenter/components/PortalSettingsBasicForm.vue +++ b/app/javascript/dashboard/routes/dashboard/helpcenter/components/PortalSettingsBasicForm.vue @@ -35,11 +35,12 @@
    @@ -51,15 +52,18 @@ :label="$t('HELP_CENTER.PORTAL.ADD.SLUG.LABEL')" :placeholder="$t('HELP_CENTER.PORTAL.ADD.SLUG.PLACEHOLDER')" :help-text="domainHelpText" - @input="$v.slug.$touch" + @blur="$v.slug.$touch" />
    @@ -77,8 +81,8 @@ diff --git a/app/javascript/dashboard/routes/dashboard/settings/automation/EditAutomationRule.vue b/app/javascript/dashboard/routes/dashboard/settings/automation/EditAutomationRule.vue index 668b44d51..7230c6a27 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/automation/EditAutomationRule.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/automation/EditAutomationRule.vue @@ -2,7 +2,7 @@