diff --git a/.codeclimate.yml b/.codeclimate.yml deleted file mode 100644 index c68251f32..000000000 --- a/.codeclimate.yml +++ /dev/null @@ -1,62 +0,0 @@ -version: '2' -plugins: - rubocop: - enabled: false - channel: rubocop-0-73 - eslint: - enabled: false - csslint: - enabled: true - scss-lint: - enabled: true - brakeman: - enabled: false -checks: - similar-code: - enabled: false - method-count: - enabled: true - config: - threshold: 32 - file-lines: - enabled: true - config: - threshold: 300 - method-lines: - config: - threshold: 50 -exclude_patterns: - - 'spec/' - - '**/specs/**/**' - - '**/spec/**/**' - - 'db/*' - - 'bin/**/*' - - 'db/**/*' - - 'config/**/*' - - 'public/**/*' - - 'vendor/**/*' - - 'node_modules/**/*' - - 'lib/tasks/auto_annotate_models.rake' - - 'app/test-matchers.js' - - 'docs/*' - - '**/*.md' - - '**/*.yml' - - 'app/javascript/dashboard/i18n/locale' - - '**/*.stories.js' - - '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' - - 'app/javascript/dashboard/routes/dashboard/settings/automation/constants.js' - - 'app/javascript/dashboard/components/widgets/FilterInput/FilterOperatorTypes.js' - - 'app/javascript/dashboard/routes/dashboard/settings/reports/constants.js' - - 'app/javascript/dashboard/store/captain/storeFactory.js' - - 'app/javascript/dashboard/i18n/index.js' - - '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' - - '**/fixtures/**' - - '**/*/fixtures.js' diff --git a/.github/workflows/auto-assign-pr.yml b/.github/workflows/auto-assign-pr.yml new file mode 100644 index 000000000..98df89707 --- /dev/null +++ b/.github/workflows/auto-assign-pr.yml @@ -0,0 +1,28 @@ +name: Auto-assign PR to Author + +on: + pull_request: + types: [opened] + +jobs: + auto-assign: + runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: + - name: Auto-assign PR to author + uses: actions/github-script@v7 + with: + script: | + const { owner, repo } = context.repo; + const pull_number = context.payload.pull_request.number; + const author = context.payload.pull_request.user.login; + + await github.rest.issues.addAssignees({ + owner, + repo, + issue_number: pull_number, + assignees: [author] + }); + + console.log(`Assigned PR #${pull_number} to ${author}`); \ No newline at end of file diff --git a/.qlty/.gitignore b/.qlty/.gitignore new file mode 100644 index 000000000..30366188d --- /dev/null +++ b/.qlty/.gitignore @@ -0,0 +1,7 @@ +* +!configs +!configs/** +!hooks +!hooks/** +!qlty.toml +!.gitignore diff --git a/.qlty/configs/.hadolint.yaml b/.qlty/configs/.hadolint.yaml new file mode 100644 index 000000000..8f7e23e45 --- /dev/null +++ b/.qlty/configs/.hadolint.yaml @@ -0,0 +1,2 @@ +ignored: + - DL3008 diff --git a/.qlty/configs/.shellcheckrc b/.qlty/configs/.shellcheckrc new file mode 100644 index 000000000..6a38d9281 --- /dev/null +++ b/.qlty/configs/.shellcheckrc @@ -0,0 +1 @@ +source-path=SCRIPTDIR \ No newline at end of file diff --git a/.qlty/configs/.yamllint.yaml b/.qlty/configs/.yamllint.yaml new file mode 100644 index 000000000..d22fa7799 --- /dev/null +++ b/.qlty/configs/.yamllint.yaml @@ -0,0 +1,8 @@ +rules: + document-start: disable + quoted-strings: + required: only-when-needed + extra-allowed: ["{|}"] + key-duplicates: {} + octal-values: + forbid-implicit-octal: true diff --git a/.qlty/qlty.toml b/.qlty/qlty.toml new file mode 100644 index 000000000..57981a5f7 --- /dev/null +++ b/.qlty/qlty.toml @@ -0,0 +1,84 @@ +# This file was automatically generated by `qlty init`. +# You can modify it to suit your needs. +# We recommend you to commit this file to your repository. +# +# This configuration is used by both Qlty CLI and Qlty Cloud. +# +# Qlty CLI -- Code quality toolkit for developers +# Qlty Cloud -- Fully automated Code Health Platform +# +# Try Qlty Cloud: https://qlty.sh +# +# For a guide to configuration, visit https://qlty.sh/d/config +# Or for a full reference, visit https://qlty.sh/d/qlty-toml +config_version = "0" + +exclude_patterns = [ + "*_min.*", + "*-min.*", + "*.min.*", + "**/.yarn/**", + "**/*.d.ts", + "**/assets/**", + "**/bower_components/**", + "**/build/**", + "**/cache/**", + "**/config/**", + "**/db/**", + "**/deps/**", + "**/dist/**", + "**/extern/**", + "**/external/**", + "**/generated/**", + "**/Godeps/**", + "**/gradlew/**", + "**/mvnw/**", + "**/node_modules/**", + "**/protos/**", + "**/seed/**", + "**/target/**", + "**/templates/**", + "**/testdata/**", + "**/vendor/**", "spec/", "**/specs/**/**", "**/spec/**/**", "db/*", "bin/**/*", "db/**/*", "config/**/*", "public/**/*", "vendor/**/*", "node_modules/**/*", "lib/tasks/auto_annotate_models.rake", "app/test-matchers.js", "docs/*", "**/*.md", "**/*.yml", "app/javascript/dashboard/i18n/locale", "**/*.stories.js", "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", "app/javascript/dashboard/routes/dashboard/settings/automation/constants.js", "app/javascript/dashboard/components/widgets/FilterInput/FilterOperatorTypes.js", "app/javascript/dashboard/routes/dashboard/settings/reports/constants.js", "app/javascript/dashboard/store/captain/storeFactory.js", "app/javascript/dashboard/i18n/index.js", "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", "**/fixtures/**", "**/*/fixtures.js", +] + +test_patterns = [ + "**/test/**", + "**/spec/**", + "**/*.test.*", + "**/*.spec.*", + "**/*_test.*", + "**/*_spec.*", + "**/test_*.*", + "**/spec_*.*", +] + +[smells] +mode = "comment" + +[smells.boolean_logic] +threshold = 4 + +[smells.file_complexity] +threshold = 66 +enabled = true + +[smells.return_statements] +threshold = 4 + +[smells.nested_control_flow] +threshold = 4 + +[smells.function_parameters] +threshold = 4 + +[smells.function_complexity] +threshold = 5 + +[smells.duplication] +enabled = true +threshold = 20 + +[[source]] +name = "default" +default = true diff --git a/.rubocop.yml b/.rubocop.yml index 12e756af6..e30a71ee9 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -283,7 +283,7 @@ Rails/RedundantActiveRecordAllMethod: Enabled: false Layout/TrailingEmptyLines: - Enabled: false + Enabled: true Style/SafeNavigationChainLength: Enabled: false diff --git a/app/controllers/api/v1/accounts/campaigns_controller.rb b/app/controllers/api/v1/accounts/campaigns_controller.rb index b1a132246..2a1650e53 100644 --- a/app/controllers/api/v1/accounts/campaigns_controller.rb +++ b/app/controllers/api/v1/accounts/campaigns_controller.rb @@ -29,6 +29,6 @@ class Api::V1::Accounts::CampaignsController < Api::V1::Accounts::BaseController def campaign_params params.require(:campaign).permit(:title, :description, :message, :enabled, :trigger_only_during_business_hours, :inbox_id, :sender_id, - :scheduled_at, audience: [:type, :id], trigger_rules: {}) + :scheduled_at, audience: [:type, :id], trigger_rules: {}, template_params: {}) end end diff --git a/app/controllers/api/v1/accounts/integrations/notion_controller.rb b/app/controllers/api/v1/accounts/integrations/notion_controller.rb index dff6ccece..ecf6bae6e 100644 --- a/app/controllers/api/v1/accounts/integrations/notion_controller.rb +++ b/app/controllers/api/v1/accounts/integrations/notion_controller.rb @@ -11,4 +11,4 @@ class Api::V1::Accounts::Integrations::NotionController < Api::V1::Accounts::Bas def fetch_hook @hook = Integrations::Hook.where(account: Current.account).find_by(app_id: 'notion') end -end \ No newline at end of file +end diff --git a/app/controllers/api/v1/accounts/notion/authorizations_controller.rb b/app/controllers/api/v1/accounts/notion/authorizations_controller.rb index bb9b2f858..3e0f6586a 100644 --- a/app/controllers/api/v1/accounts/notion/authorizations_controller.rb +++ b/app/controllers/api/v1/accounts/notion/authorizations_controller.rb @@ -18,4 +18,4 @@ class Api::V1::Accounts::Notion::AuthorizationsController < Api::V1::Accounts::O render json: { success: false }, status: :unprocessable_entity end end -end \ No newline at end of file +end diff --git a/app/controllers/api/v1/accounts/whatsapp/authorizations_controller.rb b/app/controllers/api/v1/accounts/whatsapp/authorizations_controller.rb new file mode 100644 index 000000000..e7a1f3fa6 --- /dev/null +++ b/app/controllers/api/v1/accounts/whatsapp/authorizations_controller.rb @@ -0,0 +1,64 @@ +class Api::V1::Accounts::Whatsapp::AuthorizationsController < Api::V1::Accounts::BaseController + before_action :validate_feature_enabled! + + # POST /api/v1/accounts/:account_id/whatsapp/authorization + # Handles the embedded signup callback data from the Facebook SDK + def create + validate_embedded_signup_params! + channel = process_embedded_signup + render_success_response(channel.inbox) + rescue StandardError => e + render_error_response(e) + end + + private + + def process_embedded_signup + service = Whatsapp::EmbeddedSignupService.new( + account: Current.account, + code: params[:code], + business_id: params[:business_id], + waba_id: params[:waba_id], + phone_number_id: params[:phone_number_id] + ) + service.perform + end + + def render_success_response(inbox) + render json: { + success: true, + id: inbox.id, + name: inbox.name, + channel_type: 'whatsapp' + } + end + + def render_error_response(error) + Rails.logger.error "[WHATSAPP AUTHORIZATION] Embedded signup error: #{error.message}" + Rails.logger.error error.backtrace.join("\n") + render json: { + success: false, + error: error.message + }, status: :unprocessable_entity + end + + def validate_feature_enabled! + return if Current.account.feature_whatsapp_embedded_signup? + + render json: { + success: false, + error: 'WhatsApp embedded signup is not enabled for this account' + }, status: :forbidden + end + + def validate_embedded_signup_params! + missing_params = [] + missing_params << 'code' if params[:code].blank? + missing_params << 'business_id' if params[:business_id].blank? + missing_params << 'waba_id' if params[:waba_id].blank? + + return if missing_params.empty? + + raise ArgumentError, "Required parameters are missing: #{missing_params.join(', ')}" + end +end diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index 6a4ce2461..4a2df5ee5 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -67,6 +67,8 @@ class DashboardController < ActionController::Base FB_APP_ID: GlobalConfigService.load('FB_APP_ID', ''), INSTAGRAM_APP_ID: GlobalConfigService.load('INSTAGRAM_APP_ID', ''), FACEBOOK_API_VERSION: GlobalConfigService.load('FACEBOOK_API_VERSION', 'v17.0'), + WHATSAPP_APP_ID: GlobalConfigService.load('WHATSAPP_APP_ID', ''), + WHATSAPP_CONFIGURATION_ID: GlobalConfigService.load('WHATSAPP_CONFIGURATION_ID', ''), IS_ENTERPRISE: ChatwootApp.enterprise?, AZURE_APP_ID: GlobalConfigService.load('AZURE_APP_ID', ''), GIT_SHA: GIT_HASH diff --git a/app/controllers/notion/callbacks_controller.rb b/app/controllers/notion/callbacks_controller.rb index 94030fc8e..22dcf6d30 100644 --- a/app/controllers/notion/callbacks_controller.rb +++ b/app/controllers/notion/callbacks_controller.rb @@ -33,4 +33,4 @@ class Notion::CallbacksController < OauthCallbackController def notion_redirect_uri "#{ENV.fetch('FRONTEND_URL', nil)}/app/accounts/#{account.id}/settings/integrations/notion" end -end \ No newline at end of file +end diff --git a/app/controllers/super_admin/app_configs_controller.rb b/app/controllers/super_admin/app_configs_controller.rb index 771f9f28c..5cf158b98 100644 --- a/app/controllers/super_admin/app_configs_controller.rb +++ b/app/controllers/super_admin/app_configs_controller.rb @@ -39,8 +39,10 @@ class SuperAdmin::AppConfigsController < SuperAdmin::ApplicationController 'email' => ['MAILER_INBOUND_EMAIL_DOMAIN'], 'linear' => %w[LINEAR_CLIENT_ID LINEAR_CLIENT_SECRET], 'slack' => %w[SLACK_CLIENT_ID SLACK_CLIENT_SECRET], + 'instagram' => %w[INSTAGRAM_APP_ID INSTAGRAM_APP_SECRET INSTAGRAM_VERIFY_TOKEN INSTAGRAM_API_VERSION ENABLE_INSTAGRAM_CHANNEL_HUMAN_AGENT], + 'whatsapp_embedded' => %w[WHATSAPP_APP_ID WHATSAPP_APP_SECRET WHATSAPP_CONFIGURATION_ID WHATSAPP_API_VERSION], 'notion' => %w[NOTION_CLIENT_ID NOTION_CLIENT_SECRET], - 'instagram' => %w[INSTAGRAM_APP_ID INSTAGRAM_APP_SECRET INSTAGRAM_VERIFY_TOKEN INSTAGRAM_API_VERSION ENABLE_INSTAGRAM_CHANNEL_HUMAN_AGENT] + 'google' => %w[GOOGLE_OAUTH_CLIENT_ID GOOGLE_OAUTH_CLIENT_SECRET GOOGLE_OAUTH_REDIRECT_URI] } @allowed_configs = mapping.fetch(@config, %w[ENABLE_ACCOUNT_SIGNUP FIREBASE_PROJECT_ID FIREBASE_CREDENTIALS]) diff --git a/app/controllers/super_admin/application_controller.rb b/app/controllers/super_admin/application_controller.rb index 775fb34fc..5b04fbb44 100644 --- a/app/controllers/super_admin/application_controller.rb +++ b/app/controllers/super_admin/application_controller.rb @@ -7,8 +7,9 @@ class SuperAdmin::ApplicationController < Administrate::ApplicationController include ActionView::Helpers::TagHelper include ActionView::Context + include SuperAdmin::NavigationHelper - helper_method :render_vue_component + helper_method :render_vue_component, :settings_open?, :settings_pages # authenticiation done via devise : SuperAdmin Model before_action :authenticate_super_admin! diff --git a/enterprise/app/helpers/super_admin/features.yml b/app/helpers/super_admin/features.yml similarity index 85% rename from enterprise/app/helpers/super_admin/features.yml rename to app/helpers/super_admin/features.yml index e86f66832..f49004e79 100644 --- a/enterprise/app/helpers/super_admin/features.yml +++ b/app/helpers/super_admin/features.yml @@ -1,5 +1,7 @@ # TODO: Move this values to features.yml itself # No need to replicate the same values in two places + +# ------- Premium Features ------- # captain: name: 'Captain' description: 'Enable AI-powered conversations with your customers.' @@ -32,6 +34,15 @@ disable_branding: enabled: <%= (ChatwootHub.pricing_plan != 'community') %> icon: 'icon-sailbot-fill' enterprise: true + +# ------- Product Features ------- # +help_center: + name: 'Help Center' + description: 'Allow agents to create help center articles and publish them in a portal.' + enabled: true + icon: 'icon-book-2-line' + +# ------- Communication Channels ------- # live_chat: name: 'Live Chat' description: 'Improve your customer experience using a live chat on your website.' @@ -42,6 +53,12 @@ email: description: 'Manage your email customer interactions from Chatwoot.' enabled: true icon: 'icon-mail-send-fill' + config_key: 'email' +sms: + name: 'SMS' + description: 'Manage your SMS customer interactions from Chatwoot.' + enabled: true + icon: 'icon-message-line' messenger: name: 'Messenger' description: 'Stay connected with your customers on Facebook & Instagram.' @@ -69,22 +86,22 @@ line: description: 'Manage your Line customer interactions from Chatwoot.' enabled: true icon: 'icon-line-line' -sms: - name: 'SMS' - description: 'Manage your SMS customer interactions from Chatwoot.' + +# ------- OAuth & Authentication ------- # +google: + name: 'Google' + description: 'Configuration for setting up Google OAuth Integration' enabled: true - icon: 'icon-message-line' -help_center: - name: 'Help Center' - description: 'Allow agents to create help center articles and publish them in a portal.' - enabled: true - icon: 'icon-book-2-line' + icon: 'icon-google' + config_key: 'google' microsoft: name: 'Microsoft' description: 'Configuration for setting up Microsoft Email' enabled: true icon: 'icon-microsoft' config_key: 'microsoft' + +# ------- Third-party Integrations ------- # linear: name: 'Linear' description: 'Configuration for setting up Linear Integration' @@ -103,6 +120,12 @@ slack: enabled: true icon: 'icon-slack' config_key: 'slack' +whatsapp_embedded: + name: 'WhatsApp Embedded' + description: 'Configuration for setting up WhatsApp Embedded Integration' + enabled: true + icon: 'icon-whatsapp-line' + config_key: 'whatsapp_embedded' shopify: name: 'Shopify' description: 'Configuration for setting up Shopify Integration' diff --git a/enterprise/app/helpers/super_admin/features_helper.rb b/app/helpers/super_admin/features_helper.rb similarity index 78% rename from enterprise/app/helpers/super_admin/features_helper.rb rename to app/helpers/super_admin/features_helper.rb index 2fbcd1715..475ad6d25 100644 --- a/enterprise/app/helpers/super_admin/features_helper.rb +++ b/app/helpers/super_admin/features_helper.rb @@ -1,6 +1,6 @@ module SuperAdmin::FeaturesHelper def self.available_features - YAML.load(ERB.new(Rails.root.join('enterprise/app/helpers/super_admin/features.yml').read).result).with_indifferent_access + YAML.load(ERB.new(Rails.root.join('app/helpers/super_admin/features.yml').read).result).with_indifferent_access end def self.plan_details diff --git a/app/helpers/super_admin/navigation_helper.rb b/app/helpers/super_admin/navigation_helper.rb new file mode 100644 index 000000000..5fca3fa76 --- /dev/null +++ b/app/helpers/super_admin/navigation_helper.rb @@ -0,0 +1,16 @@ +module SuperAdmin::NavigationHelper + def settings_open? + params[:controller].in? %w[super_admin/settings super_admin/app_configs] + end + + def settings_pages + features = SuperAdmin::FeaturesHelper.available_features.select do |_feature, attrs| + attrs['config_key'].present? && attrs['enabled'] + end + + # Add general at the beginning + general_feature = [['general', { 'config_key' => 'general', 'name' => 'General' }]] + + general_feature + features.to_a + end +end diff --git a/app/javascript/dashboard/api/channel/whatsappChannel.js b/app/javascript/dashboard/api/channel/whatsappChannel.js new file mode 100644 index 000000000..e1003b123 --- /dev/null +++ b/app/javascript/dashboard/api/channel/whatsappChannel.js @@ -0,0 +1,14 @@ +/* global axios */ +import ApiClient from '../ApiClient'; + +class WhatsappChannel extends ApiClient { + constructor() { + super('whatsapp', { accountScoped: true }); + } + + createEmbeddedSignup(params) { + return axios.post(`${this.baseUrl()}/whatsapp/authorization`, params); + } +} + +export default new WhatsappChannel(); diff --git a/app/javascript/dashboard/components-next/Campaigns/EmptyState/WhatsAppCampaignEmptyState.vue b/app/javascript/dashboard/components-next/Campaigns/EmptyState/WhatsAppCampaignEmptyState.vue new file mode 100644 index 000000000..ab01acb4a --- /dev/null +++ b/app/javascript/dashboard/components-next/Campaigns/EmptyState/WhatsAppCampaignEmptyState.vue @@ -0,0 +1,37 @@ + + + diff --git a/app/javascript/dashboard/components-next/Campaigns/Pages/CampaignPage/WhatsAppCampaign/WhatsAppCampaignDialog.vue b/app/javascript/dashboard/components-next/Campaigns/Pages/CampaignPage/WhatsAppCampaign/WhatsAppCampaignDialog.vue new file mode 100644 index 000000000..12a789fee --- /dev/null +++ b/app/javascript/dashboard/components-next/Campaigns/Pages/CampaignPage/WhatsAppCampaign/WhatsAppCampaignDialog.vue @@ -0,0 +1,48 @@ + + + diff --git a/app/javascript/dashboard/components-next/Campaigns/Pages/CampaignPage/WhatsAppCampaign/WhatsAppCampaignForm.vue b/app/javascript/dashboard/components-next/Campaigns/Pages/CampaignPage/WhatsAppCampaign/WhatsAppCampaignForm.vue new file mode 100644 index 000000000..df76ae901 --- /dev/null +++ b/app/javascript/dashboard/components-next/Campaigns/Pages/CampaignPage/WhatsAppCampaign/WhatsAppCampaignForm.vue @@ -0,0 +1,357 @@ + + + diff --git a/app/javascript/dashboard/components-next/Contacts/ContactsForm/ContactsForm.vue b/app/javascript/dashboard/components-next/Contacts/ContactsForm/ContactsForm.vue index cd8c767b8..e77a0c10d 100644 --- a/app/javascript/dashboard/components-next/Contacts/ContactsForm/ContactsForm.vue +++ b/app/javascript/dashboard/components-next/Contacts/ContactsForm/ContactsForm.vue @@ -218,10 +218,13 @@ const resetForm = () => { Object.assign(state, defaultState); }; -watch(() => props.contactData, prepareStateBasedOnProps, { - immediate: true, - deep: true, -}); +watch( + () => props.contactData?.id, + id => { + if (id) prepareStateBasedOnProps(); + }, + { immediate: true } +); // Expose state to parent component for avatar upload defineExpose({ diff --git a/app/javascript/dashboard/components-next/breadcrumb/Breadcrumb.vue b/app/javascript/dashboard/components-next/breadcrumb/Breadcrumb.vue index 8ca325607..8a1b26e90 100644 --- a/app/javascript/dashboard/components-next/breadcrumb/Breadcrumb.vue +++ b/app/javascript/dashboard/components-next/breadcrumb/Breadcrumb.vue @@ -15,8 +15,8 @@ const emit = defineEmits(['click']); const { t } = useI18n(); -const onClick = event => { - emit('click', event); +const onClick = (item, index) => { + emit('click', item, index); }; @@ -24,22 +24,25 @@ const onClick = event => { diff --git a/app/javascript/dashboard/components-next/captain/SettingsPageLayout.vue b/app/javascript/dashboard/components-next/captain/SettingsPageLayout.vue new file mode 100644 index 000000000..4a48794b3 --- /dev/null +++ b/app/javascript/dashboard/components-next/captain/SettingsPageLayout.vue @@ -0,0 +1,91 @@ + + + diff --git a/app/javascript/dashboard/components-next/captain/assistant/ResponseCard.vue b/app/javascript/dashboard/components-next/captain/assistant/ResponseCard.vue index 7879411c8..a924ca228 100644 --- a/app/javascript/dashboard/components-next/captain/assistant/ResponseCard.vue +++ b/app/javascript/dashboard/components-next/captain/assistant/ResponseCard.vue @@ -55,6 +55,10 @@ const props = defineProps({ type: Boolean, default: false, }, + showMenu: { + type: Boolean, + default: true, + }, }); const emit = defineEmits(['action', 'navigate', 'select', 'hover']); @@ -130,7 +134,7 @@ const handleDocumentableClick = () => { {{ question }} -
+
+import { reactive, computed, watch } from 'vue'; +import { useI18n } from 'vue-i18n'; +import { useVuelidate } from '@vuelidate/core'; +import { required, minLength } from '@vuelidate/validators'; + +import Input from 'dashboard/components-next/input/Input.vue'; +import Button from 'dashboard/components-next/button/Button.vue'; +import Editor from 'dashboard/components-next/Editor/Editor.vue'; + +const props = defineProps({ + assistant: { + type: Object, + default: () => ({}), + }, +}); + +const emit = defineEmits(['submit']); + +const { t } = useI18n(); + +const initialState = { + name: '', + description: '', + productName: '', + features: { + conversationFaqs: false, + memories: false, + }, +}; + +const state = reactive({ ...initialState }); + +const validationRules = { + name: { required, minLength: minLength(1) }, + description: { required, minLength: minLength(1) }, + productName: { required, minLength: minLength(1) }, +}; + +const v$ = useVuelidate(validationRules, state); + +const getErrorMessage = field => { + return v$.value[field].$error ? v$.value[field].$errors[0].$message : ''; +}; + +const formErrors = computed(() => ({ + name: getErrorMessage('name'), + description: getErrorMessage('description'), + productName: getErrorMessage('productName'), +})); + +const updateStateFromAssistant = assistant => { + const { config = {} } = assistant; + state.name = assistant.name; + state.description = assistant.description; + state.productName = config.product_name; + state.features = { + conversationFaqs: config.feature_faq || false, + memories: config.feature_memory || false, + }; +}; + +const handleBasicInfoUpdate = async () => { + const result = await Promise.all([ + v$.value.name.$validate(), + v$.value.description.$validate(), + v$.value.productName.$validate(), + ]).then(results => results.every(Boolean)); + if (!result) return; + + const payload = { + name: state.name, + description: state.description, + config: { + ...props.assistant.config, + product_name: state.productName, + feature_faq: state.features.conversationFaqs, + feature_memory: state.features.memories, + }, + }; + + emit('submit', payload); +}; + +watch( + () => props.assistant, + newAssistant => { + if (newAssistant) updateStateFromAssistant(newAssistant); + }, + { immediate: true } +); + + + diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/assistant/settings/AssistantControlItems.vue b/app/javascript/dashboard/components-next/captain/pageComponents/assistant/settings/AssistantControlItems.vue new file mode 100644 index 000000000..2bbede899 --- /dev/null +++ b/app/javascript/dashboard/components-next/captain/pageComponents/assistant/settings/AssistantControlItems.vue @@ -0,0 +1,43 @@ + + + diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/assistant/settings/AssistantSystemSettingsForm.vue b/app/javascript/dashboard/components-next/captain/pageComponents/assistant/settings/AssistantSystemSettingsForm.vue new file mode 100644 index 000000000..b86bdacb4 --- /dev/null +++ b/app/javascript/dashboard/components-next/captain/pageComponents/assistant/settings/AssistantSystemSettingsForm.vue @@ -0,0 +1,125 @@ + + + diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/settings/SettingsHeader.story.vue b/app/javascript/dashboard/components-next/captain/pageComponents/settings/SettingsHeader.story.vue new file mode 100644 index 000000000..654e5ab16 --- /dev/null +++ b/app/javascript/dashboard/components-next/captain/pageComponents/settings/SettingsHeader.story.vue @@ -0,0 +1,28 @@ + + + diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/settings/SettingsHeader.vue b/app/javascript/dashboard/components-next/captain/pageComponents/settings/SettingsHeader.vue new file mode 100644 index 000000000..dc6b7dda9 --- /dev/null +++ b/app/javascript/dashboard/components-next/captain/pageComponents/settings/SettingsHeader.vue @@ -0,0 +1,19 @@ + + + diff --git a/app/javascript/dashboard/components-next/sidebar/Sidebar.vue b/app/javascript/dashboard/components-next/sidebar/Sidebar.vue index 171f4a4d8..12f15ce4b 100644 --- a/app/javascript/dashboard/components-next/sidebar/Sidebar.vue +++ b/app/javascript/dashboard/components-next/sidebar/Sidebar.vue @@ -331,6 +331,11 @@ const menuItems = computed(() => { label: t('SIDEBAR.SMS'), to: accountScopedRoute('campaigns_sms_index'), }, + { + name: 'WhatsApp', + label: t('SIDEBAR.WHATSAPP'), + to: accountScopedRoute('campaigns_whatsapp_index'), + }, ], }, { diff --git a/app/javascript/dashboard/components/widgets/mentions/MentionBox.vue b/app/javascript/dashboard/components/widgets/mentions/MentionBox.vue index 894ab3045..999ec758d 100644 --- a/app/javascript/dashboard/components/widgets/mentions/MentionBox.vue +++ b/app/javascript/dashboard/components/widgets/mentions/MentionBox.vue @@ -1,5 +1,5 @@ + + diff --git a/app/javascript/dashboard/routes/dashboard/captain/assistants/Edit.vue b/app/javascript/dashboard/routes/dashboard/captain/assistants/Edit.vue index 8b64e2663..2b2506934 100644 --- a/app/javascript/dashboard/routes/dashboard/captain/assistants/Edit.vue +++ b/app/javascript/dashboard/routes/dashboard/captain/assistants/Edit.vue @@ -5,10 +5,13 @@ import { useStore } from 'dashboard/composables/store'; import { useMapGetter } from 'dashboard/composables/store'; import { useAlert } from 'dashboard/composables'; import { useI18n } from 'vue-i18n'; +import { FEATURE_FLAGS } from 'dashboard/featureFlags'; import PageLayout from 'dashboard/components-next/captain/PageLayout.vue'; import EditAssistantForm from '../../../../components-next/captain/pageComponents/assistant/EditAssistantForm.vue'; import AssistantPlayground from 'dashboard/components-next/captain/assistant/AssistantPlayground.vue'; +import AssistantSettings from 'dashboard/routes/dashboard/captain/assistants/settings/Settings.vue'; + const route = useRoute(); const store = useStore(); const { t } = useI18n(); @@ -19,6 +22,16 @@ const assistant = computed(() => store.getters['captainAssistants/getRecord'](Number(assistantId)) ); +const isFeatureEnabledonAccount = useMapGetter( + 'accounts/isFeatureEnabledonAccount' +); +const currentAccountId = useMapGetter('getCurrentAccountId'); + +const isCaptainV2Enabled = isFeatureEnabledonAccount.value( + currentAccountId.value, + FEATURE_FLAGS.CAPTAIN_V2 +); + const isAssistantAvailable = computed(() => !!assistant.value?.id); const handleSubmit = async updatedAssistant => { @@ -36,14 +49,16 @@ const handleSubmit = async updatedAssistant => { }; onMounted(() => { - if (!isAssistantAvailable.value) { + if (!isAssistantAvailable.value || !isCaptainV2Enabled) { store.dispatch('captainAssistants/show', assistantId); } });