- {I18n.t("task_bots.settings.leads.start_conversation")}
+ {I18n.t('task_bots.settings.leads.start_conversation')}
@@ -324,8 +337,8 @@ function LeadsSettings ({
name="routing"
type="radio"
value="close"
- defaultChecked={state.routing === "close"}
- //value={state.routing}
+ defaultChecked={state.routing === 'close'}
+ // value={state.routing}
onChange={handleRadioChange}
label={I18n.t('task_bots.settings.leads.close')}
/>
@@ -352,7 +365,7 @@ function LeadsSettings ({
{
getTasks()
}, [])
+ useEffect(()=> {
+ setValue('tasks_rules', items)
+ }, [JSON.stringify(items)])
+
+ function addItem (item) {
+ setItems(items.concat(item))
+ }
+
+ function updateItem (name, item, i) {
+ setItems(items.map((o, index) => index !== i
+ ? o : { ...o, [name]: item }
+ )
+ )
+ }
+
+ function addEmptyItem () {
+ addItem({})
+ }
+
+ function deleteItem (i) {
+ setItems(items.filter((item, index) => index !== i))
+ }
+
+ return (
+
+ )
+}
+
+function TaskSelector ({
+ app,
+ tasks,
+ item,
+ index,
+ value,
+ deleteRule,
+ updateRule
+}) {
+
+ const [selected, setSelected] = React.useState(item.trigger)
+
useEffect(() => {
- setValue('trigger', selected)
+ // setValue(selected, index)
+ updateRule('trigger', selected, index)
}, [selected])
function handleChange (e) {
setSelected(e.value)
}
- const selectedAgent = tasks.find((o) => o.id === selected)
+ const selectedTask = tasks.find((o) => o.id === selected)
let defaultValue = null
- if (selectedAgent) {
- defaultValue = { label: selectedAgent.title, value: selectedAgent.id }
+ if (selectedTask) {
+ defaultValue = { label: selectedTask.title, value: selectedTask.id }
}
- const options = [{label: 'none', value: null}].concat(
+ const options = [{ label: 'none', value: null }].concat(
tasks.map((o) => ({ label: o.title, value: o.id }))
)
return (
-
-
+
+
+
+
+
+
+
+
)
}
+function RuleSelector ({ app, update, data }) {
+ const [predicates, setPredicates] = React.useState(data)
+
+ React.useEffect(() => {
+ update(predicates)
+ }, [predicates])
+
+ function updatePredicates (data) {
+ setPredicates(data)
+ }
+
+ function deletePredicate (data) {
+ setPredicates(data)
+ }
+
+ function addPredicate (data) {
+ const pendingPredicate = {
+ attribute: data.name,
+ comparison: null,
+ type: data.type,
+ value: data.value
+ }
+ setPredicates(predicates.concat(pendingPredicate))
+ }
+
+ function displayName (o) {
+ return o.attribute.split('_').join(' ')
+ }
+
+ function getTextForPredicate (o) {
+ if (o.type === 'match') {
+ return `Match ${o.value === 'and' ? 'all' : 'any'} criteria`
+ } else {
+ return `${displayName(o)} ${o.comparison ? o.comparison : ''} ${
+ o.value ? o.value : ''
+ }`
+ }
+ }
+
+ return
+ {
+ predicates.map((o, i) => {
+ return
+ {
+ // debugger
+ // }}
+ deletePredicate={(items) => {
+ deletePredicate(items)
+ }}
+ />
+
+
+ })
+ }
+
+
{
+ addPredicate(predicate)
+ }}
+ />
+
+}
+
export default SettingsForm
diff --git a/app/models/app_user.rb b/app/models/app_user.rb
index 99fc1152c..f2896d98a 100644
--- a/app/models/app_user.rb
+++ b/app/models/app_user.rb
@@ -266,16 +266,15 @@ def register_visit(options)
end
def register_in_crm
- # refactor this query
- app.app_packages
- .joins(:app_package_integrations)
- .tagged_with("crm").each do |pkg|
- pkg.app_package_integrations.each do |integration|
- profile = self.external_profiles.find_or_create_by(
- provider: pkg.name.downcase
- )
- profile.sync
- end
+ crm_tags = app.app_packages.tagged_with("crm").pluck(:id)
+ integrations = app.app_package_integrations.includes(:app_package)
+ .where(app_package: crm_tags)
+
+ integrations.each do |integration|
+ profile = self.external_profiles.find_or_create_by(
+ provider: pkg.name.downcase
+ )
+ profile.sync
end
end
diff --git a/app/services/action_trigger_factory.rb b/app/services/action_trigger_factory.rb
index 9fff78aae..9f6d30fe3 100644
--- a/app/services/action_trigger_factory.rb
+++ b/app/services/action_trigger_factory.rb
@@ -287,17 +287,30 @@ def self.infer_for(app:, user:)
subject
end
- def self.find_configured_bot_for_user(app: , user:)
- if user.is_a?(Lead)
- id = app.lead_tasks_settings['trigger']
- return if id.blank?
- return app.bot_tasks.find(id)
- elsif(user.is_a?(AppUser))
- id = app.user_tasks_settings['trigger']
- return if id.blank?
- return app.bot_tasks.find(id)
- end
- nil
+ def self.find_configured_bot_for_user(app:, user:)
+ settings_namespace = user.is_a?(Lead) ? :lead_tasks_settings : :user_tasks_settings
+
+ return if !app.send(settings_namespace)['override_with_task']
+ return if app.send(settings_namespace)['task_rules'].empty?
+
+ return find_by_segment(
+ app: app,
+ user: user,
+ rules: app.send(settings_namespace)['task_rules']
+ )
+ end
+
+ def self.find_by_segment(app: , user:, rules:)
+ rules.find{|o|
+ comparator = SegmentComparator.new(
+ user: user,
+ predicates: o['predicates'] || []
+ )
+ if comparator.compare && trigger = app.bot_tasks.find(o['trigger'])
+ return trigger
+ end
+ nil
+ }
end
diff --git a/config/application.rb b/config/application.rb
index f3ee939b5..e8c278c57 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -40,6 +40,8 @@ class Application < Rails::Application
end
end
+ config.middleware.use BatchLoader::Middleware
+
URLcrypt::key = [ENV['SECRET_KEY_BASE']].pack('H*')
locales = %w[af sq ar eu bg be ca hr cs da nl en eo et fo fi fr gl de el iw hu is ga it ja ko lv lt mk mt no pl pt ro ru gd sr sr sk sl es sv tr uk]
diff --git a/config/environments/test.rb b/config/environments/test.rb
index 4f6017ed2..a08888993 100644
--- a/config/environments/test.rb
+++ b/config/environments/test.rb
@@ -7,7 +7,7 @@
# test suite. You never need to work with it otherwise. Remember that
# your test database is "scratch space" for the test suite and is wiped
# and recreated between test runs. Don't rely on the data there!
- config.cache_classes = ENV['CI'].present?
+ config.cache_classes = false #ENV['CI'].present?
ENV['HOST'] = 'http://localhost:5002'
ENV['WS'] = 'ws://localhost:5002'
diff --git a/config/puma.rb b/config/puma.rb
index ff86cf58f..b07081824 100644
--- a/config/puma.rb
+++ b/config/puma.rb
@@ -33,5 +33,9 @@
#
# preload_app!
+on_worker_boot do
+ ActiveRecord::Base.establish_connection if defined?(ActiveRecord)
+end
+
# Allow puma to be restarted by `rails restart` command.
plugin :tmp_restart
diff --git a/cypress.json b/cypress.json
index 18d99a4c1..1d7f4be6c 100644
--- a/cypress.json
+++ b/cypress.json
@@ -4,7 +4,7 @@
"baseUrl": "http://localhost:5002",
"blacklistHosts": null,
"chromeWebSecurity": true,
- "defaultCommandTimeout": 30000,
+ "defaultCommandTimeout": 10000,
"env": {},
"execTimeout": 60000,
"fileServerFolder": "",
diff --git a/package.json b/package.json
index e4c54f7ff..a23f6dc56 100644
--- a/package.json
+++ b/package.json
@@ -98,7 +98,7 @@
"rails-cy": "bundle exec rails server -e test -p 5002",
"cypress:open": "./node_modules/.bin/cypress open",
"cypress:run": "./node_modules/.bin/cypress run",
- "cypress:ci": "./node_modules/.bin/cypress run --headless --browser chrome --record --key 0998a03f-f6fe-4acf-8444-b81533c950cd",
+ "cypress:ci": "./node_modules/.bin/cypress run --headless --browser chrome --record --key 0998a03f-f6fe-4acf-8444-b81533c950cd --parallel --ci-build-id $TRAVIS_BUILD_ID",
"dev": "./bin/webpack-dev-server",
"build": "bundle exec rake assets:precompile"
}
diff --git a/spec/cypress/app_commands/clean.rb b/spec/cypress/app_commands/clean.rb
index a0c945fcf..c46d731f2 100644
--- a/spec/cypress/app_commands/clean.rb
+++ b/spec/cypress/app_commands/clean.rb
@@ -1,24 +1,16 @@
# frozen_string_literal: true
-ActiveRecord::Base.connection_pool.with_connection do
+#require 'database_cleaner/active_record'
+#require 'database_cleaner/redis'
- if defined?(DatabaseCleaner)
- # cleaning the database using database_cleaner
- DatabaseCleaner.strategy = :truncation
- DatabaseCleaner.clean
- DatabaseCleaner.strategy = :truncation
- DatabaseCleaner.clean
- # see https://github.com/bmabey/database_cleaner/issues/99
- #begin
- # ActiveRecord::Base.connection.send :rollback_transaction_records, true
- #rescue
- #end
- else
- logger.warn 'add database_cleaner or update clean_db'
- #Post.delete_all if defined?(Post)
- end
+DatabaseCleaner.strategy = :truncation
- Rails.logger.info 'APPCLEANED' # used by log_fail.rb
- # code
+#DatabaseCleaner[:redis].strategy = :truncation, { only: ["test:*"] }
+# then, whenever you need to clean the DB
+begin
+ DatabaseCleaner.clean
+rescue => e
+ sleep 2
+ DatabaseCleaner.clean_with :deletion
end
\ No newline at end of file
diff --git a/spec/cypress/app_commands/start_conversation.rb b/spec/cypress/app_commands/start_conversation_command.rb
similarity index 100%
rename from spec/cypress/app_commands/start_conversation.rb
rename to spec/cypress/app_commands/start_conversation_command.rb
diff --git a/spec/cypress/integration/messenger/conversations_spec.js b/spec/cypress/integration/messenger/conversations_spec.js
index 84ae3c349..68246f9ce 100644
--- a/spec/cypress/integration/messenger/conversations_spec.js
+++ b/spec/cypress/integration/messenger/conversations_spec.js
@@ -1,11 +1,10 @@
describe('Conversation Spec', function () {
+
describe('run previous', function () {
- beforeEach(() => {
+ it('run previous conversations', function () {
cy.appScenario('start_conversation_from_agent')
- })
- it('run previous conversations', function () {
openMessenger(($body) => {
expect($body.html()).to.contain('Start a conversation')
cy.appEval('App.last.app_users.size').then((res) => {
@@ -29,11 +28,8 @@ describe('Conversation Spec', function () {
})
describe('basic', function () {
- beforeEach(() => {
- cy.appScenario('basic')
- })
-
it('start_conversation', function () {
+ cy.appScenario('basic')
openMessenger(($body, appKey) => {
expect($body.html()).to.contain('Start a conversation')
@@ -50,7 +46,7 @@ describe('Conversation Spec', function () {
.type('oeoe \n').then(() => {
cy.wrap($body).contains('oeoe')
- cy.app('start_conversation', { text: '11111', app_key: appKey, rules: [] })
+ cy.app('start_conversation_command', { text: '11111', app_key: appKey, rules: [] })
cy.wrap($body).contains('was assigned to this conversation')
cy.wrap($body).contains('11111')
diff --git a/spec/cypress/integration/messenger/task_bot_spec.js b/spec/cypress/integration/messenger/task_bot_spec.js
index 10963c3f6..01b868797 100644
--- a/spec/cypress/integration/messenger/task_bot_spec.js
+++ b/spec/cypress/integration/messenger/task_bot_spec.js
@@ -130,7 +130,7 @@ describe('Task bot Spec', function () {
cy.wrap($body).contains('will reply as soon as they can.')
- // cy.wrap($body).contains('oeoe')
+ cy.wrap($body).contains('oeoe')
cy.wrap($body).contains('Are you an existing')
cy.wrap($body).contains("I'm a customer").click()
cy.wrap($body).contains("You replied Yes, I'm a customer")
@@ -171,8 +171,9 @@ describe('Task bot Spec', function () {
cy.wrap($body)
.xpath('/html/body/div/div/div/div[2]/div/div/div/div[2]/div/div/textarea')
- .type('oeoe \n')
- cy.wrap($body).contains('four')
+ .type('oeoe \n').then(()=> {
+ cy.wrap($body).contains('four')
+ })
})
})
})
@@ -189,37 +190,94 @@ describe('Task bot Spec', function () {
cy.appEval('Redis.current.del("app_user:1:trigger_locked")')
})
- it('sessionless enter bot on new conversation', function () {
+ it('sessionless enter bot on new conversation with predicates', function () {
cy.appEval('App.last').then((results) => {
const appKey = results.key
- cy.appEval(`App.find_by(key: '${appKey}').update(lead_tasks_settings: {trigger: 1} )`)
-
- cy.app('bot_task_command', {
- app_key: appKey
- }).then((res) => {
- cy.appEval(`BotTask.find(${res.id}).update(state: 'disabled' )`)
-
- cy.log(res)
+ cy.appEval(`App.find_by(key: '${appKey}').update(lead_tasks_settings: {
+ override_with_task: true,
+ task_rules: [
+ { trigger: 1,
+ predicates: [
+ {
+ "attribute": "lang",
+ "comparison": "eq",
+ "type": "string",
+ "value": "en"
+ }]
+ }
+ ]
+ })`).then(()=>{
+ cy.app('bot_task_command', {
+ app_key: appKey
+ }).then((res) => {
+ cy.appEval(`BotTask.find(${res.id}).update(state: 'disabled' )`)
+
+ cy.log(res)
+
+ helpers.openMessenger('?sessionless=true&lang=en', ($body, appKey) => {
+ expect($body.html()).to.contain('Start a conversation')
+ cy.wrap($body)
+ .xpath('/html/body/div/div/div/div[2]/div/div[1]/div/div/div[2]/a[1]')
+ .click()
+ .then(() => {
+ cy.wrap($body)
+ .xpath('/html/body/div/div/div/div[2]/div/div/div/div[2]/div/div/textarea')
+ .should('be.enabled').then(() => {
+ cy.wrap($body)
+ .xpath('/html/body/div/div/div/div[2]/div/div/div/div[2]/div/div/textarea')
+ .type('oeoe \n').then(() => {
+ cy.wrap($body).contains('oeoe')
+ cy.wrap($body).contains('one')
+ cy.wrap($body).contains('two')
+ cy.wrap($body).contains('tree')
+ })
+ })
+ })
+ })
+ })
+ })
- helpers.openMessenger('?sessionless=true', ($body, appKey) => {
- expect($body.html()).to.contain('Start a conversation')
- cy.wrap($body)
- .xpath('/html/body/div/div/div/div[2]/div/div[1]/div/div/div[2]/a[1]')
- .click()
- .then(() => {
- cy.wrap($body)
- .xpath('/html/body/div/div/div/div[2]/div/div/div/div[2]/div/div/textarea')
- .should('be.enabled').then(() => {
- cy.wrap($body)
- .xpath('/html/body/div/div/div/div[2]/div/div/div/div[2]/div/div/textarea')
- .type('oeoe \n').then(() => {
- cy.wrap($body).contains('oeoe')
- cy.wrap($body).contains('one')
- cy.wrap($body).contains('two')
- cy.wrap($body).contains('tree')
- })
- })
- })
+ })
+ })
+
+ it('sessionless enter bot on new conversation with empty predicates', function () {
+ cy.appEval('App.last').then((results) => {
+ const appKey = results.key
+ cy.appEval(`App.find_by(key: '${appKey}').update(lead_tasks_settings: {
+ override_with_task: true,
+ task_rules: [
+ { trigger: 1,
+ predicates: []
+ }
+ ]
+ })`).then(() => {
+ cy.app('bot_task_command', {
+ app_key: appKey
+ }).then((res) => {
+ cy.appEval(`BotTask.find(${res.id}).update(state: 'disabled' )`)
+
+ cy.log(res)
+
+ helpers.openMessenger('?sessionless=true&lang=en', ($body, appKey) => {
+ expect($body.html()).to.contain('Start a conversation')
+ cy.wrap($body)
+ .xpath('/html/body/div/div/div/div[2]/div/div[1]/div/div/div[2]/a[1]')
+ .click()
+ .then(() => {
+ cy.wrap($body)
+ .xpath('/html/body/div/div/div/div[2]/div/div/div/div[2]/div/div/textarea')
+ .should('be.enabled').then(() => {
+ cy.wrap($body)
+ .xpath('/html/body/div/div/div/div[2]/div/div/div/div[2]/div/div/textarea')
+ .type('oeoe \n').then(() => {
+ cy.wrap($body).contains('oeoe')
+ cy.wrap($body).contains('one')
+ cy.wrap($body).contains('two')
+ cy.wrap($body).contains('tree')
+ })
+ })
+ })
+ })
})
})
})
@@ -227,9 +285,7 @@ describe('Task bot Spec', function () {
it('sessionless default bot new conversation', function () {
cy.appEval('App.last').then((results) => {
- const appKey = results.key
- //cy.appEval(`App.find_by(key: '${appKey}').update(lead_tasks_settings: {trigger: 1} )`)
-
+ const appKey = results.key
cy.app('bot_task_command', {
app_key: appKey
}).then((res) => {
@@ -262,35 +318,55 @@ describe('Task bot Spec', function () {
it('user session default bot new conversation', function () {
cy.appEval('App.last').then((results) => {
const appKey = results.key
- cy.appEval(`App.find_by(key: '${appKey}').update(user_tasks_settings: {trigger: 1} )`)
-
- cy.app('bot_task_command', {
- app_key: appKey
- }).then((res) => {
- cy.appEval(`BotTask.find(${res.id}).update(state: 'disabled' )`)
- cy.log(res)
-
- helpers.openMessenger('', ($body, appKey) => {
- expect($body.html()).to.contain('Start a conversation')
- cy.wrap($body)
- .xpath('/html/body/div/div/div/div[2]/div/div[1]/div/div/div[2]/a[1]')
- .click()
- .then(() => {
- cy.wrap($body)
- .xpath('/html/body/div/div/div/div[2]/div/div/div/div[2]/div/div/textarea')
- .should('be.enabled').then(() => {
- cy.wrap($body)
- .xpath('/html/body/div/div/div/div[2]/div/div/div/div[2]/div/div/textarea')
- .type('oeoe \n').then(() => {
- cy.wrap($body).contains('oeoe')
-
- cy.wrap($body).contains('one')
- cy.wrap($body).contains('two')
- cy.wrap($body).contains('tree')
- })
- })
- })
+ cy.appEval(`App.find_by(key: '${appKey}').update(user_tasks_settings: {
+ override_with_task: true,
+ task_rules: [
+ { trigger: 1,
+ predicates: [
+ {
+ "attribute": "lang",
+ "comparison": "eq",
+ "type": "string",
+ "value": "en"
+ },
+ {
+ "attribute": "num_devices",
+ "comparison": "eq",
+ "type": "integer",
+ "value": 2
+ }]
+ }
+ ]
+ })`).then(() => {
+ cy.app('bot_task_command', {
+ app_key: appKey
+ }).then((res) => {
+ cy.appEval(`BotTask.find(${res.id}).update(state: 'disabled' )`)
+
+ cy.log(res)
+
+ helpers.openMessenger('', ($body, appKey) => {
+ expect($body.html()).to.contain('Start a conversation')
+ cy.wrap($body)
+ .xpath('/html/body/div/div/div/div[2]/div/div[1]/div/div/div[2]/a[1]')
+ .click()
+ .then(() => {
+ cy.wrap($body)
+ .xpath('/html/body/div/div/div/div[2]/div/div/div/div[2]/div/div/textarea')
+ .should('be.enabled').then(() => {
+ cy.wrap($body)
+ .xpath('/html/body/div/div/div/div[2]/div/div/div/div[2]/div/div/textarea')
+ .type('oeoe \n').then(() => {
+ cy.wrap($body).contains('oeoe')
+
+ cy.wrap($body).contains('one')
+ cy.wrap($body).contains('two')
+ cy.wrap($body).contains('tree')
+ })
+ })
+ })
+ })
})
})
})
@@ -299,35 +375,49 @@ describe('Task bot Spec', function () {
it('user session default bot new conversation', function () {
cy.appEval('App.last').then((results) => {
const appKey = results.key
- cy.appEval(`App.find_by(key: '${appKey}').update(user_tasks_settings: {trigger: 1} )`)
-
- cy.app('bot_task_command', {
- app_key: appKey
- }).then((res) => {
- cy.appEval(`BotTask.find(${res.id}).update(state: 'disabled' )`)
- cy.log(res)
-
- helpers.openMessenger('', ($body, appKey) => {
- expect($body.html()).to.contain('Start a conversation')
- cy.wrap($body)
- .xpath('/html/body/div/div/div/div[2]/div/div[1]/div/div/div[2]/a[1]')
- .click()
- .then(() => {
- cy.wrap($body)
- .xpath('/html/body/div/div/div/div[2]/div/div/div/div[2]/div/div/textarea')
- .should('be.enabled').then(() => {
- cy.wrap($body)
- .xpath('/html/body/div/div/div/div[2]/div/div/div/div[2]/div/div/textarea')
- .type('oeoe \n').then(() => {
- cy.wrap($body).contains('oeoe')
-
- cy.wrap($body).contains('one')
- cy.wrap($body).contains('two')
- cy.wrap($body).contains('tree')
- })
- })
- })
+ cy.appEval(`App.find_by(key: '${appKey}').update(user_tasks_settings: {
+ override_with_task: true,
+ task_rules: [
+ { trigger: 1,
+ predicates: [
+ {
+ "attribute": "lang",
+ "comparison": "eq",
+ "type": "string",
+ "value": "en"
+ }]
+ }
+ ]
+ })`).then(() => {
+ cy.app('bot_task_command', {
+ app_key: appKey
+ }).then((res) => {
+ cy.appEval(`BotTask.find(${res.id}).update(state: 'disabled' )`)
+
+ cy.log(res)
+
+ helpers.openMessenger('', ($body, appKey) => {
+ expect($body.html()).to.contain('Start a conversation')
+ cy.wrap($body)
+ .xpath('/html/body/div/div/div/div[2]/div/div[1]/div/div/div[2]/a[1]')
+ .click()
+ .then(() => {
+ cy.wrap($body)
+ .xpath('/html/body/div/div/div/div[2]/div/div/div/div[2]/div/div/textarea')
+ .should('be.enabled').then(() => {
+ cy.wrap($body)
+ .xpath('/html/body/div/div/div/div[2]/div/div/div/div[2]/div/div/textarea')
+ .type('oeoe \n').then(() => {
+ cy.wrap($body).contains('oeoe')
+
+ cy.wrap($body).contains('one')
+ cy.wrap($body).contains('two')
+ cy.wrap($body).contains('tree')
+ })
+ })
+ })
+ })
})
})
})
diff --git a/spec/cypress/screenshots/messenger/conversations_spec.js/Conversation Spec -- basic -- start_conversation (failed).png b/spec/cypress/screenshots/messenger/conversations_spec.js/Conversation Spec -- basic -- start_conversation (failed).png
new file mode 100644
index 000000000..882bb9b88
Binary files /dev/null and b/spec/cypress/screenshots/messenger/conversations_spec.js/Conversation Spec -- basic -- start_conversation (failed).png differ
diff --git a/spec/cypress/support/on-rails.js b/spec/cypress/support/on-rails.js
index ec3929bad..787a2ed70 100644
--- a/spec/cypress/support/on-rails.js
+++ b/spec/cypress/support/on-rails.js
@@ -45,7 +45,13 @@ Cypress.on('fail', (err, runnable) => {
// allow app to generate additional logging data
Cypress.$.ajax({
url: '/__cypress__/command',
- data: JSON.stringify({name: 'log_fail', options: {error_message: err.message, runnable_full_title: runnable.fullTitle() }}),
+ data: JSON.stringify({
+ name: 'log_fail',
+ options: {
+ error_message: err.message,
+ runnable_full_title: runnable.fullTitle()
+ }
+ }),
async: false,
method: 'POST'
});