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..830ee7bce 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 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/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/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/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..78143410f 100644 --- a/app/controllers/platform/api/v1/accounts_controller.rb +++ b/app/controllers/platform/api/v1/accounts_controller.rb @@ -3,16 +3,12 @@ class Platform::Api::V1::AccountsController < PlatformController @resource = Account.new(account_params) @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 end def destroy @@ -27,6 +23,14 @@ class Platform::Api::V1::AccountsController < PlatformController end def account_params - params.permit(:name, :locale) + if permitted_params[:enabled_features] + return permitted_params.except(:enabled_features).merge(selected_feature_flags: permitted_params[:enabled_features].map(&:to_sym)) + end + + permitted_params + end + + def permitted_params + params.permit(:name, :locale, enabled_features: [], limits: {}) end 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/javascript/dashboard/components/layout/Sidebar.vue b/app/javascript/dashboard/components/layout/Sidebar.vue index 314b594d2..b919be2cd 100644 --- a/app/javascript/dashboard/components/layout/Sidebar.vue +++ b/app/javascript/dashboard/components/layout/Sidebar.vue @@ -73,14 +73,14 @@ export default { computed: { ...mapGetters({ - currentUser: 'getCurrentUser', - globalConfig: 'globalConfig/get', - isACustomBrandedInstance: 'globalConfig/isACustomBrandedInstance', - isOnChatwootCloud: 'globalConfig/isOnChatwootCloud', - inboxes: 'inboxes/getInboxes', accountId: 'getCurrentAccountId', currentRole: 'getCurrentRole', + currentUser: 'getCurrentUser', + globalConfig: 'globalConfig/get', + inboxes: 'inboxes/getInboxes', + isACustomBrandedInstance: 'globalConfig/isACustomBrandedInstance', isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount', + isOnChatwootCloud: 'globalConfig/isOnChatwootCloud', labels: 'labels/getLabelsOnSidebar', teams: 'teams/getMyTeams', }), diff --git a/app/javascript/dashboard/components/layout/config/sidebarItems/settings.js b/app/javascript/dashboard/components/layout/config/sidebarItems/settings.js index 83a0c2309..768e42ea5 100644 --- a/app/javascript/dashboard/components/layout/config/sidebarItems/settings.js +++ b/app/javascript/dashboard/components/layout/config/sidebarItems/settings.js @@ -1,3 +1,4 @@ +import { FEATURE_FLAGS } from '../../../../featureFlags'; import { frontendURL } from '../../../../helper/URLHelper'; const settings = accountId => ({ @@ -38,12 +39,20 @@ const settings = accountId => ({ 'settings_teams_new', ], menuItems: [ + { + icon: 'briefcase', + label: 'ACCOUNT_SETTINGS', + hasSubMenu: false, + toState: frontendURL(`accounts/${accountId}/settings/general`), + toStateName: 'general_settings_index', + }, { icon: 'people', label: 'AGENTS', hasSubMenu: false, toState: frontendURL(`accounts/${accountId}/settings/agents/list`), toStateName: 'agent_list', + featureFlag: FEATURE_FLAGS.AGENT_MANAGEMENT, }, { icon: 'people-team', @@ -51,6 +60,7 @@ const settings = accountId => ({ hasSubMenu: false, toState: frontendURL(`accounts/${accountId}/settings/teams/list`), toStateName: 'settings_teams_list', + featureFlag: FEATURE_FLAGS.TEAM_MANAGEMENT, }, { icon: 'mail-inbox-all', @@ -58,6 +68,7 @@ const settings = accountId => ({ hasSubMenu: false, toState: frontendURL(`accounts/${accountId}/settings/inboxes/list`), toStateName: 'settings_inbox_list', + featureFlag: FEATURE_FLAGS.INBOX_MANAGEMENT, }, { icon: 'tag', @@ -65,6 +76,7 @@ const settings = accountId => ({ hasSubMenu: false, toState: frontendURL(`accounts/${accountId}/settings/labels/list`), toStateName: 'labels_list', + featureFlag: FEATURE_FLAGS.LABELS, }, { icon: 'code', @@ -74,6 +86,7 @@ const settings = accountId => ({ `accounts/${accountId}/settings/custom-attributes/list` ), toStateName: 'attributes_list', + featureFlag: FEATURE_FLAGS.CUSTOM_ATTRIBUTES, }, { icon: 'automation', @@ -82,6 +95,7 @@ const settings = accountId => ({ hasSubMenu: false, toState: frontendURL(`accounts/${accountId}/settings/automation/list`), toStateName: 'automation_list', + featureFlag: FEATURE_FLAGS.AUTOMATIONS, }, { icon: 'bot', @@ -90,7 +104,7 @@ const settings = accountId => ({ hasSubMenu: false, toState: frontendURL(`accounts/${accountId}/settings/agent-bots`), toStateName: 'agent_bots', - featureFlagKey: 'agent_bots', + featureFlag: FEATURE_FLAGS.AGENT_BOTS, }, { icon: 'flash-settings', @@ -99,7 +113,7 @@ const settings = accountId => ({ toState: frontendURL(`accounts/${accountId}/settings/macros`), toStateName: 'macros_wrapper', beta: true, - featureFlagKey: 'macros', + featureFlag: FEATURE_FLAGS.MACROS, }, { icon: 'chat-multiple', @@ -109,6 +123,7 @@ const settings = accountId => ({ `accounts/${accountId}/settings/canned-response/list` ), toStateName: 'canned_list', + featureFlag: FEATURE_FLAGS.CANNED_RESPONSES, }, { icon: 'flash-on', @@ -116,6 +131,7 @@ const settings = accountId => ({ hasSubMenu: false, toState: frontendURL(`accounts/${accountId}/settings/integrations`), toStateName: 'settings_integrations', + featureFlag: FEATURE_FLAGS.INTEGRATIONS, }, { icon: 'star-emphasis', @@ -123,6 +139,7 @@ const settings = accountId => ({ hasSubMenu: false, toState: frontendURL(`accounts/${accountId}/settings/applications`), toStateName: 'settings_applications', + featureFlag: FEATURE_FLAGS.INTEGRATIONS, }, { icon: 'credit-card-person', @@ -132,13 +149,6 @@ const settings = accountId => ({ toStateName: 'billing_settings_index', showOnlyOnCloud: true, }, - { - icon: 'settings', - label: 'ACCOUNT_SETTINGS', - hasSubMenu: false, - toState: frontendURL(`accounts/${accountId}/settings/general`), - toStateName: 'general_settings_index', - }, ], }); diff --git a/app/javascript/dashboard/components/layout/sidebarComponents/Secondary.vue b/app/javascript/dashboard/components/layout/sidebarComponents/Secondary.vue index e68fcc3a4..1d8ced445 100644 --- a/app/javascript/dashboard/components/layout/sidebarComponents/Secondary.vue +++ b/app/javascript/dashboard/components/layout/sidebarComponents/Secondary.vue @@ -20,6 +20,8 @@ import { frontendURL } from '../../../helper/URLHelper'; import SecondaryNavItem from './SecondaryNavItem.vue'; import AccountContext from './AccountContext.vue'; +import { mapGetters } from 'vuex'; +import { FEATURE_FLAGS } from '../../../featureFlags'; export default { components: { @@ -61,6 +63,9 @@ export default { }, }, computed: { + ...mapGetters({ + isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount', + }), hasSecondaryMenu() { return this.menuConfig.menuItems && this.menuConfig.menuItems.length; }, @@ -89,7 +94,7 @@ export default { icon: 'folder', label: 'INBOXES', hasSubMenu: true, - newLink: true, + newLink: this.showNewLink(FEATURE_FLAGS.INBOX_MANAGEMENT), newLinkTag: 'NEW_INBOX', key: 'inbox', toState: frontendURL(`accounts/${this.accountId}/settings/inboxes/new`), @@ -117,7 +122,7 @@ export default { icon: 'number-symbol', label: 'LABELS', hasSubMenu: true, - newLink: true, + newLink: this.showNewLink(FEATURE_FLAGS.TEAM_MANAGEMENT), newLinkTag: 'NEW_LABEL', key: 'label', toState: frontendURL(`accounts/${this.accountId}/settings/labels`), @@ -141,7 +146,7 @@ export default { label: 'TAGGED_WITH', hasSubMenu: true, key: 'label', - newLink: true, + newLink: this.showNewLink(FEATURE_FLAGS.TEAM_MANAGEMENT), newLinkTag: 'NEW_LABEL', toState: frontendURL(`accounts/${this.accountId}/settings/labels`), toStateName: 'labels_list', @@ -163,7 +168,7 @@ export default { icon: 'people-team', label: 'TEAMS', hasSubMenu: true, - newLink: true, + newLink: this.showNewLink(FEATURE_FLAGS.TEAM_MANAGEMENT), newLinkTag: 'NEW_TEAM', key: 'team', toState: frontendURL(`accounts/${this.accountId}/settings/teams/new`), @@ -238,6 +243,9 @@ export default { toggleAccountModal() { this.$emit('toggle-accounts'); }, + showNewLink(featureFlag) { + return this.isFeatureEnabledonAccount(this.accountId, featureFlag); + }, }, }; diff --git a/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryNavItem.vue b/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryNavItem.vue index a7a6761cf..8661a488f 100644 --- a/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryNavItem.vue +++ b/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryNavItem.vue @@ -123,12 +123,12 @@ export default { return !!this.menuItem.children; }, isMenuItemVisible() { - if (!this.menuItem.featureFlagKey) { + if (!this.menuItem.featureFlag) { return true; } return this.isFeatureEnabledonAccount( this.accountId, - this.menuItem.featureFlagKey + this.menuItem.featureFlag ); }, isInboxConversation() { @@ -217,7 +217,7 @@ export default { } }, showItem(item) { - return this.isAdmin && item.newLink !== undefined; + return this.isAdmin && !!item.newLink; }, onClickOpen() { this.$emit('open'); @@ -321,7 +321,7 @@ export default { .beta { padding-right: var(--space-smaller) !important; padding-left: var(--space-smaller) !important; - margin-left: var(--space-half) !important; + margin-left: var(--space-smaller) !important; display: inline-block; font-size: var(--font-size-micro); font-weight: var(--font-weight-medium); diff --git a/app/javascript/dashboard/components/widgets/AutomationActionInput.vue b/app/javascript/dashboard/components/widgets/AutomationActionInput.vue index 248bb5b35..93a5e3e6c 100644 --- a/app/javascript/dashboard/components/widgets/AutomationActionInput.vue +++ b/app/javascript/dashboard/components/widgets/AutomationActionInput.vue @@ -1,8 +1,5 @@ @@ -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 @@