diff --git a/.circleci/config.yml b/.circleci/config.yml
index 804c63857..24119bc75 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -218,6 +218,49 @@ jobs:
source ~/.rvm/scripts/rvm
bundle install
+ # Install and configure OpenSearch
+ - run:
+ name: Install OpenSearch
+ command: |
+ # Download and install OpenSearch 2.11.0 (compatible with Elasticsearch 7.x clients)
+ wget https://artifacts.opensearch.org/releases/bundle/opensearch/2.11.0/opensearch-2.11.0-linux-x64.tar.gz
+ tar -xzf opensearch-2.11.0-linux-x64.tar.gz
+ sudo mv opensearch-2.11.0 /opt/opensearch
+
+ - run:
+ name: Configure and Start OpenSearch
+ command: |
+ # Configure OpenSearch for single-node testing
+ cat > /opt/opensearch/config/opensearch.yml \<< EOF
+ cluster.name: chatwoot-test
+ node.name: node-1
+ network.host: 0.0.0.0
+ http.port: 9200
+ discovery.type: single-node
+ plugins.security.disabled: true
+ EOF
+
+ # Set ownership and permissions
+ sudo chown -R $USER:$USER /opt/opensearch
+
+ # Start OpenSearch in background
+ /opt/opensearch/bin/opensearch -d -p /tmp/opensearch.pid
+
+ - run:
+ name: Wait for OpenSearch to be ready
+ command: |
+ echo "Waiting for OpenSearch to start..."
+ for i in {1..30}; do
+ if curl -s http://localhost:9200/_cluster/health | grep -q '"status"'; then
+ echo "OpenSearch is ready!"
+ exit 0
+ fi
+ echo "Waiting... ($i/30)"
+ sleep 2
+ done
+ echo "OpenSearch failed to start"
+ exit 1
+
# Configure environment and database
- run:
name: Database Setup and Configure Environment Variables
@@ -234,6 +277,7 @@ jobs:
sed -i -e '/POSTGRES_USERNAME/ s/=.*/=chatwoot/' .env
sed -i -e "/POSTGRES_PASSWORD/ s/=.*/=$pg_pass/" .env
echo -en "\nINSTALLATION_ENV=circleci" >> ".env"
+ echo -en "\nOPENSEARCH_URL=http://localhost:9200" >> ".env"
# Database setup
- run:
diff --git a/app/controllers/api/v1/accounts/search_controller.rb b/app/controllers/api/v1/accounts/search_controller.rb
index 13e3a6a6c..7ee25e02e 100644
--- a/app/controllers/api/v1/accounts/search_controller.rb
+++ b/app/controllers/api/v1/accounts/search_controller.rb
@@ -28,5 +28,7 @@ class Api::V1::Accounts::SearchController < Api::V1::Accounts::BaseController
search_type: search_type,
params: params
).perform
+ rescue ArgumentError => e
+ render json: { error: e.message }, status: :unprocessable_entity
end
end
diff --git a/app/javascript/dashboard/api/search.js b/app/javascript/dashboard/api/search.js
index d533c2f28..10214f3f5 100644
--- a/app/javascript/dashboard/api/search.js
+++ b/app/javascript/dashboard/api/search.js
@@ -14,38 +14,48 @@ class SearchAPI extends ApiClient {
});
}
- contacts({ q, page = 1 }) {
+ contacts({ q, page = 1, since, until }) {
return axios.get(`${this.url}/contacts`, {
params: {
q,
page: page,
+ since,
+ until,
},
});
}
- conversations({ q, page = 1 }) {
+ conversations({ q, page = 1, since, until }) {
return axios.get(`${this.url}/conversations`, {
params: {
q,
page: page,
+ since,
+ until,
},
});
}
- messages({ q, page = 1 }) {
+ messages({ q, page = 1, since, until, from, inboxId }) {
return axios.get(`${this.url}/messages`, {
params: {
q,
page: page,
+ since,
+ until,
+ from,
+ inbox_id: inboxId,
},
});
}
- articles({ q, page = 1 }) {
+ articles({ q, page = 1, since, until }) {
return axios.get(`${this.url}/articles`, {
params: {
q,
page: page,
+ since,
+ until,
},
});
}
diff --git a/app/javascript/dashboard/api/specs/search.spec.js b/app/javascript/dashboard/api/specs/search.spec.js
new file mode 100644
index 000000000..251ea760e
--- /dev/null
+++ b/app/javascript/dashboard/api/specs/search.spec.js
@@ -0,0 +1,134 @@
+import searchAPI from '../search';
+import ApiClient from '../ApiClient';
+
+describe('#SearchAPI', () => {
+ it('creates correct instance', () => {
+ expect(searchAPI).toBeInstanceOf(ApiClient);
+ expect(searchAPI).toHaveProperty('get');
+ expect(searchAPI).toHaveProperty('contacts');
+ expect(searchAPI).toHaveProperty('conversations');
+ expect(searchAPI).toHaveProperty('messages');
+ expect(searchAPI).toHaveProperty('articles');
+ });
+
+ describe('API calls', () => {
+ const originalAxios = window.axios;
+ const axiosMock = {
+ get: vi.fn(() => Promise.resolve()),
+ };
+
+ beforeEach(() => {
+ window.axios = axiosMock;
+ });
+
+ afterEach(() => {
+ window.axios = originalAxios;
+ vi.clearAllMocks();
+ });
+
+ it('#get', () => {
+ searchAPI.get({ q: 'test query' });
+ expect(axiosMock.get).toHaveBeenCalledWith('/api/v1/search', {
+ params: { q: 'test query' },
+ });
+ });
+
+ it('#contacts', () => {
+ searchAPI.contacts({ q: 'test', page: 1 });
+ expect(axiosMock.get).toHaveBeenCalledWith('/api/v1/search/contacts', {
+ params: { q: 'test', page: 1, since: undefined, until: undefined },
+ });
+ });
+
+ it('#contacts with date filters', () => {
+ searchAPI.contacts({
+ q: 'test',
+ page: 2,
+ since: 1700000000,
+ until: 1732000000,
+ });
+ expect(axiosMock.get).toHaveBeenCalledWith('/api/v1/search/contacts', {
+ params: { q: 'test', page: 2, since: 1700000000, until: 1732000000 },
+ });
+ });
+
+ it('#conversations', () => {
+ searchAPI.conversations({ q: 'test', page: 1 });
+ expect(axiosMock.get).toHaveBeenCalledWith(
+ '/api/v1/search/conversations',
+ {
+ params: { q: 'test', page: 1, since: undefined, until: undefined },
+ }
+ );
+ });
+
+ it('#conversations with date filters', () => {
+ searchAPI.conversations({
+ q: 'test',
+ page: 1,
+ since: 1700000000,
+ until: 1732000000,
+ });
+ expect(axiosMock.get).toHaveBeenCalledWith(
+ '/api/v1/search/conversations',
+ {
+ params: { q: 'test', page: 1, since: 1700000000, until: 1732000000 },
+ }
+ );
+ });
+
+ it('#messages', () => {
+ searchAPI.messages({ q: 'test', page: 1 });
+ expect(axiosMock.get).toHaveBeenCalledWith('/api/v1/search/messages', {
+ params: {
+ q: 'test',
+ page: 1,
+ since: undefined,
+ until: undefined,
+ from: undefined,
+ inbox_id: undefined,
+ },
+ });
+ });
+
+ it('#messages with all filters', () => {
+ searchAPI.messages({
+ q: 'test',
+ page: 1,
+ since: 1700000000,
+ until: 1732000000,
+ from: 'contact:42',
+ inboxId: 10,
+ });
+ expect(axiosMock.get).toHaveBeenCalledWith('/api/v1/search/messages', {
+ params: {
+ q: 'test',
+ page: 1,
+ since: 1700000000,
+ until: 1732000000,
+ from: 'contact:42',
+ inbox_id: 10,
+ },
+ });
+ });
+
+ it('#articles', () => {
+ searchAPI.articles({ q: 'test', page: 1 });
+ expect(axiosMock.get).toHaveBeenCalledWith('/api/v1/search/articles', {
+ params: { q: 'test', page: 1, since: undefined, until: undefined },
+ });
+ });
+
+ it('#articles with date filters', () => {
+ searchAPI.articles({
+ q: 'test',
+ page: 2,
+ since: 1700000000,
+ until: 1732000000,
+ });
+ expect(axiosMock.get).toHaveBeenCalledWith('/api/v1/search/articles', {
+ params: { q: 'test', page: 2, since: 1700000000, until: 1732000000 },
+ });
+ });
+ });
+});
diff --git a/app/javascript/dashboard/components-next/CardLayout.vue b/app/javascript/dashboard/components-next/CardLayout.vue
index 462402167..166f5ea4c 100644
--- a/app/javascript/dashboard/components-next/CardLayout.vue
+++ b/app/javascript/dashboard/components-next/CardLayout.vue
@@ -19,7 +19,7 @@ const handleClick = () => {
{{ section.title }}
- {{ category }} -
+ {{ truncatedContent }} +- {{ truncatedContent }} -
-- - {{ email }} - - ⢠- {{ phone }} -
-{{ $t('SEARCH.EMPTY_STATE', { item: titleCase, query }) }}
diff --git a/app/javascript/dashboard/modules/search/components/SearchTabs.vue b/app/javascript/dashboard/modules/search/components/SearchTabs.vue index 9260796ef..86e40df57 100644 --- a/app/javascript/dashboard/modules/search/components/SearchTabs.vue +++ b/app/javascript/dashboard/modules/search/components/SearchTabs.vue @@ -1,5 +1,8 @@ -