Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c85eff59eb | ||
|
|
1134871cbc | ||
|
|
749e4726ff | ||
|
|
dfbcfaa525 | ||
|
|
4a43b3611c | ||
|
|
95c2915723 | ||
|
|
960de3e545 |
@@ -21,6 +21,7 @@ class Api::V1::Accounts::SearchController < Api::V1::Accounts::BaseController
|
||||
SearchService.new(
|
||||
current_user: Current.user,
|
||||
current_account: Current.account,
|
||||
current_account_user: Current.account_user,
|
||||
search_type: search_type,
|
||||
params: params
|
||||
).perform
|
||||
|
||||
@@ -3,14 +3,14 @@ import { useVuelidate } from '@vuelidate/core';
|
||||
import { required, minLength } from '@vuelidate/validators';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
import WootSubmitButton from '../../../../components/buttons/FormSubmitButton.vue';
|
||||
import Modal from '../../../../components/Modal.vue';
|
||||
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
|
||||
|
||||
export default {
|
||||
name: 'AddCanned',
|
||||
components: {
|
||||
NextButton,
|
||||
WootSubmitButton,
|
||||
Modal,
|
||||
WootMessageEditor,
|
||||
},
|
||||
@@ -118,23 +118,18 @@ export default {
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row justify-end w-full gap-2 px-0 py-2">
|
||||
<NextButton
|
||||
faded
|
||||
slate
|
||||
type="reset"
|
||||
:label="$t('CANNED_MGMT.ADD.CANCEL_BUTTON_TEXT')"
|
||||
@click.prevent="onClose"
|
||||
/>
|
||||
<NextButton
|
||||
type="submit"
|
||||
:label="$t('CANNED_MGMT.ADD.FORM.SUBMIT')"
|
||||
<WootSubmitButton
|
||||
:disabled="
|
||||
v$.content.$invalid ||
|
||||
v$.shortCode.$invalid ||
|
||||
addCanned.showLoading
|
||||
"
|
||||
:is-loading="addCanned.showLoading"
|
||||
:button-text="$t('CANNED_MGMT.ADD.FORM.SUBMIT')"
|
||||
:loading="addCanned.showLoading"
|
||||
/>
|
||||
<button class="button clear" @click.prevent="onClose">
|
||||
{{ $t('CANNED_MGMT.ADD.CANCEL_BUTTON_TEXT') }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -4,12 +4,12 @@ import { useVuelidate } from '@vuelidate/core';
|
||||
import { required, minLength } from '@vuelidate/validators';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
import WootSubmitButton from '../../../../components/buttons/FormSubmitButton.vue';
|
||||
import Modal from '../../../../components/Modal.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NextButton,
|
||||
WootSubmitButton,
|
||||
Modal,
|
||||
WootMessageEditor,
|
||||
},
|
||||
@@ -122,23 +122,18 @@ export default {
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row justify-end w-full gap-2 px-0 py-2">
|
||||
<NextButton
|
||||
faded
|
||||
slate
|
||||
type="reset"
|
||||
:label="$t('CANNED_MGMT.EDIT.CANCEL_BUTTON_TEXT')"
|
||||
@click.prevent="onClose"
|
||||
/>
|
||||
<NextButton
|
||||
type="submit"
|
||||
:label="$t('CANNED_MGMT.EDIT.FORM.SUBMIT')"
|
||||
<WootSubmitButton
|
||||
:disabled="
|
||||
v$.content.$invalid ||
|
||||
v$.shortCode.$invalid ||
|
||||
editCanned.showLoading
|
||||
"
|
||||
:is-loading="editCanned.showLoading"
|
||||
:button-text="$t('CANNED_MGMT.EDIT.FORM.SUBMIT')"
|
||||
:loading="editCanned.showLoading"
|
||||
/>
|
||||
<button class="button clear" @click.prevent="onClose">
|
||||
{{ $t('CANNED_MGMT.EDIT.CANCEL_BUTTON_TEXT') }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -7,8 +7,6 @@ import { computed, onMounted, ref, defineOptions } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useStoreGetters, useStore } from 'dashboard/composables/store';
|
||||
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'CannedResponseSettings',
|
||||
});
|
||||
@@ -126,11 +124,13 @@ const tableHeaders = computed(() => {
|
||||
feature-name="canned_responses"
|
||||
>
|
||||
<template #actions>
|
||||
<Button
|
||||
icon="i-lucide-circle-plus"
|
||||
:label="$t('CANNED_MGMT.HEADER_BTN_TXT')"
|
||||
<woot-button
|
||||
class="button nice rounded-md"
|
||||
icon="add-circle"
|
||||
@click="openAddPopup"
|
||||
/>
|
||||
>
|
||||
{{ $t('CANNED_MGMT.HEADER_BTN_TXT') }}
|
||||
</woot-button>
|
||||
</template>
|
||||
</BaseSettingsHeader>
|
||||
|
||||
@@ -188,20 +188,21 @@ const tableHeaders = computed(() => {
|
||||
{{ cannedItem.content }}
|
||||
</td>
|
||||
<td class="py-4 flex justify-end gap-1">
|
||||
<Button
|
||||
<woot-button
|
||||
v-tooltip.top="$t('CANNED_MGMT.EDIT.BUTTON_TEXT')"
|
||||
icon="i-lucide-pen"
|
||||
slate
|
||||
xs
|
||||
faded
|
||||
variant="smooth"
|
||||
size="tiny"
|
||||
color-scheme="secondary"
|
||||
icon="edit"
|
||||
@click="openEditPopup(cannedItem)"
|
||||
/>
|
||||
<Button
|
||||
<woot-button
|
||||
v-tooltip.top="$t('CANNED_MGMT.DELETE.BUTTON_TEXT')"
|
||||
icon="i-lucide-trash-2"
|
||||
xs
|
||||
ruby
|
||||
faded
|
||||
variant="smooth"
|
||||
color-scheme="alert"
|
||||
size="tiny"
|
||||
icon="dismiss-circle"
|
||||
class-names="grey-btn"
|
||||
:is-loading="loading[cannedItem.id]"
|
||||
@click="openDeletePopup(cannedItem, index)"
|
||||
/>
|
||||
|
||||
@@ -5,7 +5,6 @@ import BaseSettingsHeader from '../components/BaseSettingsHeader.vue';
|
||||
import CustomRoleModal from './component/CustomRoleModal.vue';
|
||||
import CustomRoleTableBody from './component/CustomRoleTableBody.vue';
|
||||
import CustomRolePaywall from './component/CustomRolePaywall.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useStore, useMapGetter } from 'dashboard/composables/store';
|
||||
@@ -135,12 +134,14 @@ const confirmDeletion = () => {
|
||||
feature-name="canned_responses"
|
||||
>
|
||||
<template #actions>
|
||||
<Button
|
||||
icon="i-lucide-circle-plus"
|
||||
:label="$t('CUSTOM_ROLE.HEADER_BTN_TXT')"
|
||||
<woot-button
|
||||
class="rounded-md button nice"
|
||||
icon="add-circle"
|
||||
:disabled="isBehindAPaywall"
|
||||
@click="openAddModal"
|
||||
/>
|
||||
>
|
||||
{{ $t('CUSTOM_ROLE.HEADER_BTN_TXT') }}
|
||||
</woot-button>
|
||||
</template>
|
||||
</BaseSettingsHeader>
|
||||
</template>
|
||||
|
||||
+11
-14
@@ -12,7 +12,7 @@ import {
|
||||
CONVERSATION_PARTICIPATING_PERMISSIONS,
|
||||
} from 'dashboard/constants/permissions.js';
|
||||
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import WootSubmitButton from 'dashboard/components/buttons/FormSubmitButton.vue';
|
||||
|
||||
const props = defineProps({
|
||||
mode: {
|
||||
@@ -160,12 +160,13 @@ const isSubmitDisabled = computed(
|
||||
/>
|
||||
<form class="flex flex-col w-full" @submit.prevent="handleCustomRole">
|
||||
<div class="w-full">
|
||||
<label :class="{ error: v$.name.$error }">
|
||||
<label>
|
||||
{{ $t('CUSTOM_ROLE.FORM.NAME.LABEL') }}
|
||||
<input
|
||||
ref="nameInput"
|
||||
v-model.trim="name"
|
||||
type="text"
|
||||
:class="{ error: v$.name.$error }"
|
||||
:placeholder="$t('CUSTOM_ROLE.FORM.NAME.PLACEHOLDER')"
|
||||
@blur="v$.name.$touch"
|
||||
/>
|
||||
@@ -173,12 +174,13 @@ const isSubmitDisabled = computed(
|
||||
</div>
|
||||
|
||||
<div class="w-full">
|
||||
<label :class="{ error: v$.description.$error }">
|
||||
<label>
|
||||
{{ $t('CUSTOM_ROLE.FORM.DESCRIPTION.LABEL') }}
|
||||
|
||||
<textarea
|
||||
v-model="description"
|
||||
:rows="3"
|
||||
:class="{ error: v$.description.$error }"
|
||||
:placeholder="$t('CUSTOM_ROLE.FORM.DESCRIPTION.PLACEHOLDER')"
|
||||
@blur="v$.description.$touch"
|
||||
/>
|
||||
@@ -211,19 +213,14 @@ const isSubmitDisabled = computed(
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row justify-end w-full gap-2 px-0 py-2">
|
||||
<Button
|
||||
faded
|
||||
slate
|
||||
type="reset"
|
||||
:label="$t('CUSTOM_ROLE.FORM.CANCEL_BUTTON_TEXT')"
|
||||
@click.prevent="emit('close')"
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
:label="submitButtonText"
|
||||
<WootSubmitButton
|
||||
:disabled="isSubmitDisabled"
|
||||
:is-loading="addCustomRole.showLoading"
|
||||
:button-text="submitButtonText"
|
||||
:loading="addCustomRole.showLoading"
|
||||
/>
|
||||
<button class="button clear" @click.prevent="emit('close')">
|
||||
{{ $t('CUSTOM_ROLE.FORM.CANCEL_BUTTON_TEXT') }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
+12
-12
@@ -2,8 +2,6 @@
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { getI18nKey } from 'dashboard/routes/dashboard/settings/helper/settingsHelper';
|
||||
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
defineProps({
|
||||
roles: {
|
||||
type: Array,
|
||||
@@ -46,20 +44,22 @@ const getFormattedPermissions = role => {
|
||||
{{ getFormattedPermissions(customRole) }}
|
||||
</td>
|
||||
<td class="flex justify-end gap-1 py-4">
|
||||
<Button
|
||||
<woot-button
|
||||
v-tooltip.top="$t('CUSTOM_ROLE.EDIT.BUTTON_TEXT')"
|
||||
icon="i-lucide-pen"
|
||||
slate
|
||||
xs
|
||||
faded
|
||||
variant="smooth"
|
||||
size="tiny"
|
||||
color-scheme="secondary"
|
||||
class-names="grey-btn"
|
||||
icon="edit"
|
||||
@click="emit('edit', customRole)"
|
||||
/>
|
||||
<Button
|
||||
<woot-button
|
||||
v-tooltip.top="$t('CUSTOM_ROLE.DELETE.BUTTON_TEXT')"
|
||||
icon="i-lucide-trash-2"
|
||||
xs
|
||||
ruby
|
||||
faded
|
||||
variant="smooth"
|
||||
color-scheme="alert"
|
||||
size="tiny"
|
||||
icon="dismiss-circle"
|
||||
class-names="grey-btn"
|
||||
:is-loading="loading[customRole.id]"
|
||||
@click="emit('delete', customRole)"
|
||||
/>
|
||||
|
||||
@@ -3,13 +3,11 @@ import { mapGetters } from 'vuex';
|
||||
import { convertSecondsToTimeUnit } from '@chatwoot/utils';
|
||||
import validations from './validations';
|
||||
import SlaTimeInput from './SlaTimeInput.vue';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SlaTimeInput,
|
||||
NextButton,
|
||||
},
|
||||
props: {
|
||||
selectedResponse: {
|
||||
@@ -207,19 +205,19 @@ export default {
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end w-full gap-2 mt-8">
|
||||
<NextButton
|
||||
faded
|
||||
slate
|
||||
type="reset"
|
||||
:label="$t('SLA.FORM.CANCEL')"
|
||||
<woot-button
|
||||
class="px-4 rounded-xl button clear outline-woot-200/50 outline"
|
||||
@click.prevent="onClose"
|
||||
/>
|
||||
<NextButton
|
||||
type="submit"
|
||||
:label="submitLabel"
|
||||
:disabled="isSubmitDisabled"
|
||||
>
|
||||
{{ $t('SLA.FORM.CANCEL') }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
:is-disabled="isSubmitDisabled"
|
||||
class="px-4 rounded-xl"
|
||||
:is-loading="uiFlags.isUpdating"
|
||||
/>
|
||||
>
|
||||
{{ submitLabel }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script setup>
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import BaseSettingsHeader from '../../components/BaseSettingsHeader.vue';
|
||||
|
||||
defineProps({
|
||||
@@ -20,11 +19,14 @@ defineEmits(['add']);
|
||||
feature-name="sla"
|
||||
>
|
||||
<template v-if="showActions" #actions>
|
||||
<Button
|
||||
:label="$t('SLA.ADD_ACTION')"
|
||||
icon="i-lucide-circle-plus"
|
||||
<woot-button
|
||||
color-scheme="primary"
|
||||
icon="plus-sign"
|
||||
class="rounded-md"
|
||||
@click="$emit('add')"
|
||||
/>
|
||||
>
|
||||
{{ $t('SLA.ADD_ACTION') }}
|
||||
</woot-button>
|
||||
</template>
|
||||
</BaseSettingsHeader>
|
||||
</template>
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import BaseSettingsListItem from '../../components/BaseSettingsListItem.vue';
|
||||
import SLAResponseTime from './SLAResponseTime.vue';
|
||||
import SLABusinessHoursLabel from './SLABusinessHoursLabel.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
defineProps({
|
||||
slaName: {
|
||||
@@ -57,12 +56,13 @@ const emit = defineEmits(['delete']);
|
||||
</div>
|
||||
</template>
|
||||
<template #actions>
|
||||
<Button
|
||||
<woot-button
|
||||
v-tooltip.top="$t('SLA.FORM.DELETE')"
|
||||
faded
|
||||
ruby
|
||||
xs
|
||||
icon="i-lucide-trash-2"
|
||||
variant="smooth"
|
||||
color-scheme="alert"
|
||||
size="tiny"
|
||||
icon="delete"
|
||||
class-names="grey-btn"
|
||||
:is-loading="isLoading"
|
||||
@click="emit('delete')"
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
class SearchService
|
||||
pattr_initialize [:current_user!, :current_account!, :params!, :search_type!]
|
||||
pattr_initialize [:current_user!, :current_account!, :current_account_user!, :params!, :search_type!]
|
||||
|
||||
def perform
|
||||
case search_type
|
||||
@@ -25,13 +25,16 @@ class SearchService
|
||||
end
|
||||
|
||||
def filter_conversations
|
||||
@conversations = current_account.conversations.where(inbox_id: accessable_inbox_ids)
|
||||
.joins('INNER JOIN contacts ON conversations.contact_id = contacts.id')
|
||||
.where("cast(conversations.display_id as text) ILIKE :search OR contacts.name ILIKE :search OR contacts.email
|
||||
ILIKE :search OR contacts.phone_number ILIKE :search OR contacts.identifier ILIKE :search", search: "%#{search_query}%")
|
||||
.order('conversations.created_at DESC')
|
||||
.page(params[:page])
|
||||
.per(15)
|
||||
query = current_account.conversations
|
||||
# admins can see all conversations anyway, so no need to add the heavy IN clause
|
||||
query = query.where(inbox_id: accessable_inbox_ids) unless @current_account_user.administrator?
|
||||
|
||||
@conversations = query.joins('INNER JOIN contacts ON conversations.contact_id = contacts.id')
|
||||
.where("cast(conversations.display_id as text) ILIKE :search OR contacts.name ILIKE :search OR contacts.email
|
||||
ILIKE :search OR contacts.phone_number ILIKE :search OR contacts.identifier ILIKE :search", search: "%#{search_query}%")
|
||||
.order('conversations.created_at DESC')
|
||||
.page(params[:page])
|
||||
.per(15)
|
||||
end
|
||||
|
||||
def filter_messages
|
||||
@@ -47,16 +50,7 @@ class SearchService
|
||||
|
||||
if search_query.present?
|
||||
# Use the @@ operator with to_tsquery for better GIN index utilization
|
||||
# Convert search query to tsquery format with prefix matching
|
||||
|
||||
# Use this if we wanna match splitting the words
|
||||
# split_query = search_query.split.map { |term| "#{term} | #{term}:*" }.join(' & ')
|
||||
|
||||
# This will do entire sentence matching using phrase distance operator
|
||||
tsquery = search_query.split.join(' <-> ')
|
||||
|
||||
# Apply the text search using the GIN index
|
||||
base_query.where('content @@ to_tsquery(?)', tsquery)
|
||||
base_query.where('content @@ to_tsquery(?)', search_query)
|
||||
.reorder('created_at DESC')
|
||||
.page(params[:page])
|
||||
.per(15)
|
||||
@@ -76,12 +70,14 @@ class SearchService
|
||||
end
|
||||
|
||||
def message_base_query
|
||||
current_account.messages.where(inbox_id: accessable_inbox_ids)
|
||||
.where('created_at >= ?', 3.months.ago)
|
||||
query = current_account.messages
|
||||
# admins can see all conversations anyway, so no need to add the heavy IN clause
|
||||
query = query.where(inbox_id: accessable_inbox_ids) unless @current_account_user.administrator?
|
||||
query.where('created_at >= ?', 3.months.ago)
|
||||
end
|
||||
|
||||
def use_gin_search
|
||||
current_account.feature_enabled?('search_with_gin')
|
||||
search_query.split.size == 1
|
||||
end
|
||||
|
||||
def filter_contacts
|
||||
|
||||
@@ -3,11 +3,11 @@ require Rails.root.join('lib/redis/config')
|
||||
schedule_file = 'config/schedule.yml'
|
||||
|
||||
Sidekiq.configure_client do |config|
|
||||
config.redis = Redis::Config.sidekiq
|
||||
config.redis = Redis::Config.app
|
||||
end
|
||||
|
||||
Sidekiq.configure_server do |config|
|
||||
config.redis = Redis::Config.sidekiq
|
||||
config.redis = Redis::Config.app
|
||||
|
||||
# skip the default start stop logging
|
||||
if Rails.env.production?
|
||||
|
||||
@@ -5,18 +5,10 @@ module Redis::Config
|
||||
config
|
||||
end
|
||||
|
||||
def sidekiq
|
||||
sidekiq_config
|
||||
end
|
||||
|
||||
def config
|
||||
@config ||= sentinel? ? sentinel_config : base_config
|
||||
end
|
||||
|
||||
def sidekiq_config
|
||||
@sidekiq_config ||= sentinel? ? sentinel_config : base_sidekiq_config
|
||||
end
|
||||
|
||||
def base_config
|
||||
{
|
||||
url: ENV.fetch('REDIS_URL', 'redis://127.0.0.1:6379'),
|
||||
@@ -27,16 +19,6 @@ module Redis::Config
|
||||
}
|
||||
end
|
||||
|
||||
def base_sidekiq_config
|
||||
{
|
||||
url: ENV.fetch('SIDEKIQ_REDIS_URL', ENV.fetch('REDIS_URL', 'redis://127.0.0.1:6379')),
|
||||
password: ENV.fetch('SIDEKIQ_REDIS_PASSWORD', ENV.fetch('REDIS_PASSWORD', nil)).presence,
|
||||
ssl_params: { verify_mode: Chatwoot.redis_ssl_verify_mode },
|
||||
reconnect_attempts: 2,
|
||||
timeout: 1
|
||||
}
|
||||
end
|
||||
|
||||
def sentinel?
|
||||
ENV.fetch('REDIS_SENTINELS', nil).presence
|
||||
end
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe SearchService do
|
||||
subject(:search) { described_class.new(current_user: user, current_account: account, params: params, search_type: search_type) }
|
||||
subject(:search) do
|
||||
described_class.new(current_user: user, current_account: account, current_account_user: user.account_users.find_by(account: account),
|
||||
params: params, search_type: search_type)
|
||||
end
|
||||
|
||||
let(:search_type) { 'all' }
|
||||
let!(:account) { create(:account) }
|
||||
@@ -26,25 +29,33 @@ describe SearchService do
|
||||
|
||||
it 'returns all for all' do
|
||||
search_type = 'all'
|
||||
search = described_class.new(current_user: user, current_account: account, params: params, search_type: search_type)
|
||||
search = described_class.new(current_user: user, current_account: account,
|
||||
current_account_user: user.account_users.find_by(account: account),
|
||||
params: params, search_type: search_type)
|
||||
expect(search.perform.keys).to match_array(%i[contacts messages conversations])
|
||||
end
|
||||
|
||||
it 'returns contacts for contacts' do
|
||||
search_type = 'Contact'
|
||||
search = described_class.new(current_user: user, current_account: account, params: params, search_type: search_type)
|
||||
search = described_class.new(current_user: user, current_account: account,
|
||||
current_account_user: user.account_users.find_by(account: account),
|
||||
params: params, search_type: search_type)
|
||||
expect(search.perform.keys).to match_array(%i[contacts])
|
||||
end
|
||||
|
||||
it 'returns messages for messages' do
|
||||
search_type = 'Message'
|
||||
search = described_class.new(current_user: user, current_account: account, params: params, search_type: search_type)
|
||||
search = described_class.new(current_user: user, current_account: account,
|
||||
current_account_user: user.account_users.find_by(account: account),
|
||||
params: params, search_type: search_type)
|
||||
expect(search.perform.keys).to match_array(%i[messages])
|
||||
end
|
||||
|
||||
it 'returns conversations for conversations' do
|
||||
search_type = 'Conversation'
|
||||
search = described_class.new(current_user: user, current_account: account, params: params, search_type: search_type)
|
||||
search = described_class.new(current_user: user, current_account: account,
|
||||
current_account_user: user.account_users.find_by(account: account),
|
||||
params: params, search_type: search_type)
|
||||
expect(search.perform.keys).to match_array(%i[conversations])
|
||||
end
|
||||
end
|
||||
@@ -61,7 +72,8 @@ describe SearchService do
|
||||
harry4 = create(:contact, identifier: 'Potter1235', account_id: account.id, last_activity_at: 2.minutes.ago)
|
||||
|
||||
params = { q: 'Potter ' }
|
||||
search = described_class.new(current_user: user, current_account: account, params: params, search_type: 'Contact')
|
||||
search = described_class.new(current_user: user, current_account: account,
|
||||
current_account_user: user.account_users.find_by(account: account), params: params, search_type: 'Contact')
|
||||
expect(search.perform[:contacts].map(&:id)).to eq([harry4.id, harry3.id, harry2.id, harry.id])
|
||||
end
|
||||
end
|
||||
@@ -75,17 +87,19 @@ describe SearchService do
|
||||
# random messsage in inbox with out access
|
||||
create(:message, account: account, inbox: create(:inbox, account: account), content: 'Harry Potter is a wizard')
|
||||
params = { q: 'Harry' }
|
||||
search = described_class.new(current_user: user, current_account: account, params: params, search_type: 'Message')
|
||||
search = described_class.new(current_user: user, current_account: account,
|
||||
current_account_user: user.account_users.find_by(account: account), params: params, search_type: 'Message')
|
||||
expect(search.perform[:messages].map(&:id)).to eq([message2.id, message.id])
|
||||
end
|
||||
|
||||
context 'with feature flag for search type' do
|
||||
let(:params) { { q: 'Harry' } }
|
||||
context 'with different search methods' do
|
||||
let(:search_type) { 'Message' }
|
||||
|
||||
it 'uses LIKE search when search_with_gin feature is disabled' do
|
||||
allow(account).to receive(:feature_enabled?).with('search_with_gin').and_return(false)
|
||||
search_service = described_class.new(current_user: user, current_account: account, params: params, search_type: search_type)
|
||||
it 'uses LIKE search for multi-word queries' do
|
||||
params = { q: 'Harry Potter' }
|
||||
search_service = described_class.new(current_user: user, current_account: account,
|
||||
current_account_user: user.account_users.find_by(account: account),
|
||||
params: params, search_type: search_type)
|
||||
|
||||
expect(search_service).to receive(:filter_messages_with_like).and_call_original
|
||||
expect(search_service).not_to receive(:filter_messages_with_gin)
|
||||
@@ -93,9 +107,11 @@ describe SearchService do
|
||||
search_service.perform
|
||||
end
|
||||
|
||||
it 'uses GIN search when search_with_gin feature is enabled' do
|
||||
allow(account).to receive(:feature_enabled?).with('search_with_gin').and_return(true)
|
||||
search_service = described_class.new(current_user: user, current_account: account, params: params, search_type: search_type)
|
||||
it 'uses GIN search for single-word queries' do
|
||||
params = { q: 'Harry' }
|
||||
search_service = described_class.new(current_user: user, current_account: account,
|
||||
current_account_user: user.account_users.find_by(account: account),
|
||||
params: params, search_type: search_type)
|
||||
|
||||
expect(search_service).to receive(:filter_messages_with_gin).and_call_original
|
||||
expect(search_service).not_to receive(:filter_messages_with_like)
|
||||
@@ -103,24 +119,39 @@ describe SearchService do
|
||||
search_service.perform
|
||||
end
|
||||
|
||||
it 'returns same results regardless of search type' do
|
||||
it 'returns same results for single word regardless of search method' do
|
||||
# Create test messages
|
||||
message3 = create(:message, account: account, inbox: inbox, content: 'Harry is a wizard apprentice')
|
||||
params = { q: 'Harry' }
|
||||
|
||||
# Test with GIN search
|
||||
allow(account).to receive(:feature_enabled?).with('search_with_gin').and_return(true)
|
||||
gin_search = described_class.new(current_user: user, current_account: account, params: params, search_type: search_type)
|
||||
# Test with GIN search (single word)
|
||||
gin_search = described_class.new(current_user: user, current_account: account,
|
||||
current_account_user: user.account_users.find_by(account: account),
|
||||
params: params, search_type: search_type)
|
||||
allow(gin_search).to receive(:use_gin_search).and_return(true)
|
||||
gin_results = gin_search.perform[:messages].map(&:id)
|
||||
|
||||
# Test with LIKE search
|
||||
allow(account).to receive(:feature_enabled?).with('search_with_gin').and_return(false)
|
||||
like_search = described_class.new(current_user: user, current_account: account, params: params, search_type: search_type)
|
||||
# Test with LIKE search (forcing LIKE for comparison)
|
||||
like_search = described_class.new(current_user: user, current_account: account,
|
||||
current_account_user: user.account_users.find_by(account: account),
|
||||
params: params, search_type: search_type)
|
||||
allow(like_search).to receive(:use_gin_search).and_return(false)
|
||||
like_results = like_search.perform[:messages].map(&:id)
|
||||
|
||||
# Both search types should return the same messages
|
||||
expect(gin_results).to match_array(like_results)
|
||||
expect(gin_results).to include(message.id, message2.id, message3.id)
|
||||
end
|
||||
|
||||
it 'handles empty search queries' do
|
||||
params = { q: '' }
|
||||
search_service = described_class.new(current_user: user, current_account: account,
|
||||
current_account_user: user.account_users.find_by(account: account),
|
||||
params: params, search_type: search_type)
|
||||
|
||||
results = search_service.perform[:messages]
|
||||
expect(results).to be_a(ActiveRecord::Relation)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -131,7 +162,8 @@ describe SearchService do
|
||||
create(:conversation, contact: random, inbox: inbox, account: account)
|
||||
conv2 = create(:conversation, contact: harry, inbox: inbox, account: account)
|
||||
params = { q: 'Harry' }
|
||||
search = described_class.new(current_user: user, current_account: account, params: params, search_type: 'Conversation')
|
||||
search = described_class.new(current_user: user, current_account: account,
|
||||
current_account_user: user.account_users.find_by(account: account), params: params, search_type: 'Conversation')
|
||||
expect(search.perform[:conversations].map(&:id)).to eq([conv2.id, conversation.id])
|
||||
end
|
||||
|
||||
@@ -139,28 +171,86 @@ describe SearchService do
|
||||
random = create(:contact, account_id: account.id, name: 'random', email: 'random@random.test', identifier: 'random')
|
||||
new_converstion = create(:conversation, contact: random, inbox: inbox, account: account)
|
||||
params = { q: new_converstion.display_id }
|
||||
search = described_class.new(current_user: user, current_account: account, params: params, search_type: 'Conversation')
|
||||
search = described_class.new(current_user: user, current_account: account,
|
||||
current_account_user: user.account_users.find_by(account: account), params: params, search_type: 'Conversation')
|
||||
expect(search.perform[:conversations].map(&:id)).to include new_converstion.id
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#use_gin_search' do
|
||||
let(:params) { { q: 'test' } }
|
||||
|
||||
it 'checks if the account has the search_with_gin feature enabled' do
|
||||
expect(account).to receive(:feature_enabled?).with('search_with_gin')
|
||||
search.send(:use_gin_search)
|
||||
end
|
||||
|
||||
it 'returns true when search_with_gin feature is enabled' do
|
||||
allow(account).to receive(:feature_enabled?).with('search_with_gin').and_return(true)
|
||||
it 'returns true for single-word queries' do
|
||||
params = { q: 'test' }
|
||||
search = described_class.new(current_user: user, current_account: account, current_account_user: user.account_users.find_by(account: account),
|
||||
params: params, search_type: 'Message')
|
||||
expect(search.send(:use_gin_search)).to be true
|
||||
end
|
||||
|
||||
it 'returns false when search_with_gin feature is disabled' do
|
||||
allow(account).to receive(:feature_enabled?).with('search_with_gin').and_return(false)
|
||||
it 'returns false for multi-word queries' do
|
||||
params = { q: 'test query' }
|
||||
search = described_class.new(current_user: user, current_account: account, current_account_user: user.account_users.find_by(account: account),
|
||||
params: params, search_type: 'Message')
|
||||
expect(search.send(:use_gin_search)).to be false
|
||||
end
|
||||
|
||||
it 'returns false for empty queries' do
|
||||
params = { q: '' }
|
||||
search = described_class.new(current_user: user, current_account: account, current_account_user: user.account_users.find_by(account: account),
|
||||
params: params, search_type: 'Message')
|
||||
expect(search.send(:use_gin_search)).to be false
|
||||
end
|
||||
|
||||
it 'returns false for nil queries' do
|
||||
params = { q: nil }
|
||||
search = described_class.new(current_user: user, current_account: account, current_account_user: user.account_users.find_by(account: account),
|
||||
params: params, search_type: 'Message')
|
||||
expect(search.send(:use_gin_search)).to be false
|
||||
end
|
||||
end
|
||||
|
||||
describe '#accessable_inbox_ids' do
|
||||
it 'returns inbox ids assigned to the user' do
|
||||
search = described_class.new(current_user: user, current_account: account, current_account_user: user.account_users.find_by(account: account),
|
||||
params: {}, search_type: 'Message')
|
||||
expect(search.send(:accessable_inbox_ids)).to eq([inbox.id])
|
||||
end
|
||||
end
|
||||
|
||||
describe '#message_base_query' do
|
||||
it 'returns messages from the last 3 months' do
|
||||
old_message = create(:message, account: account, inbox: inbox, content: 'old message', created_at: 4.months.ago)
|
||||
recent_message = create(:message, account: account, inbox: inbox, content: 'recent message', created_at: 2.months.ago)
|
||||
|
||||
search = described_class.new(current_user: user, current_account: account, current_account_user: user.account_users.find_by(account: account),
|
||||
params: {}, search_type: 'Message')
|
||||
base_query = search.send(:message_base_query)
|
||||
|
||||
expect(base_query).to include(recent_message)
|
||||
expect(base_query).not_to include(old_message)
|
||||
end
|
||||
|
||||
it 'filters by inbox_id for non-administrators' do
|
||||
other_inbox = create(:inbox, account: account)
|
||||
other_inbox_message = create(:message, account: account, inbox: other_inbox, content: 'other inbox message')
|
||||
|
||||
search = described_class.new(current_user: user, current_account: account, current_account_user: user.account_users.find_by(account: account),
|
||||
params: {}, search_type: 'Message')
|
||||
base_query = search.send(:message_base_query)
|
||||
|
||||
expect(base_query).not_to include(other_inbox_message)
|
||||
end
|
||||
|
||||
it 'does not filter by inbox_id for administrators' do
|
||||
# Create a new user with administrator role
|
||||
admin_user = create(:user, account: account, role: :administrator)
|
||||
other_inbox = create(:inbox, account: account)
|
||||
other_inbox_message = create(:message, account: account, inbox: other_inbox, content: 'other inbox message')
|
||||
|
||||
search = described_class.new(current_user: admin_user, current_account: account,
|
||||
current_account_user: admin_user.account_users.find_by(account: account), params: {}, search_type: 'Message')
|
||||
base_query = search.send(:message_base_query)
|
||||
|
||||
expect(base_query).to include(other_inbox_message)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user