Merge pull request #4412 from expsa/eltayar

🔍 [odex25_base] تحسين موديول البحث العام - البحث في جميع السجلات (Server-side)
This commit is contained in:
Mohamed Eltayar 2025-08-29 15:36:55 +03:00 committed by GitHub
commit 777d8e130e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 290 additions and 38 deletions

View File

@ -6,20 +6,40 @@
#
###############################################################################
{
'name': 'General Search in Tree/List View',
'category': 'Report',
'summary': 'General Search in Tree/List View',
'version': '14.0.1.0',
'name': 'General Search in Tree/List View - All Records',
'category': 'Tools',
'summary': 'Search across all records in tree/list views, not just visible ones',
'version': '14.0.2.0',
'license': 'OPL-1',
'description': """User allow to search for all fields value in tree view.""",
'depends': ['base'],
'description': """
Enhanced General Search in Tree/List View
==========================================
This module adds a powerful search functionality to all tree/list views that:
* Searches across ALL records in the database, not just visible ones
* Supports Arabic text normalization and search
* Searches in all visible fields automatically
* Shows count of found records
* Uses server-side filtering for better performance
* Includes a delay mechanism to avoid excessive server requests
Features:
---------
* Real-time search with 500ms debounce
* Arabic text support with normalization
* Search in multiple field types (char, text, many2one, numbers)
* Clear button to reset search
* Found records counter
* Maintains original view filters
""",
'depends': ['base', 'web'],
'author': "Fortutech IMS Pvt. Ltd.",
'website': "http://www.fortutechims.com",
'data': [
'views/assets.xml',
],
"installable": True,
"application": True,
"application": False,
"auto_install": False,
"images": ['static/description/banner.png'],
}

View File

@ -9,4 +9,58 @@
width: 100%;
overflow-x: auto;
padding: 12px;
}
}
/* Search container styles */
.oe_search_container {
background-color: #f8f9fa;
padding: 10px;
border-radius: 4px;
margin-bottom: 10px;
}
.oe_search_input {
transition: all 0.3s ease;
}
.oe_search_input:focus {
border-color: #007bff !important;
box-shadow: 0 0 0 0.2rem rgba(0,123,255,.25);
outline: none;
}
.oe_clear_search {
min-width: 70px;
}
.oe_search_count {
color: #6c757d;
font-size: 0.9em;
font-weight: 500;
}
/* Loading indicator */
.oe_search_loading {
display: inline-block;
margin-left: 10px;
}
.oe_search_loading:after {
content: " ";
display: inline-block;
width: 16px;
height: 16px;
border-radius: 50%;
border: 2px solid #007bff;
border-color: #007bff transparent #007bff transparent;
animation: oe-search-spin 1.2s linear infinite;
}
@keyframes oe-search-spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}

View File

@ -1,12 +1,16 @@
odoo.define('fims_row_no_header_fix_tree_view.list_search', function (require) {
odoo.define('fims_general_search_tree_view.list_search', function (require) {
"use strict";
var ListRenderer = require('web.ListRenderer');
var core = require('web.core');
var _t = core._t;
ListRenderer.include({
events: _.extend({
'keyup .oe_search_input': '_onKeyUp'
'keyup .oe_search_input': '_onKeyUp',
'change .oe_search_input': '_onSearchChange'
}, ListRenderer.prototype.events),
_renderView: function () {
var self = this;
return this._super.apply(this, arguments).then(function () {
@ -14,43 +18,217 @@ odoo.define('fims_row_no_header_fix_tree_view.list_search', function (require) {
if (self.arch.tag == 'tree' && self.$el.hasClass('o_list_view')) {
// Check if the search input already exists
if (!self.$el.find('.oe_search_input').length) {
var search = '<input type="text" class="oe_search_input mt-2 ml-2 pl-3" placeholder="Search...">';
var search = '<div class="oe_search_container" style="display: flex; align-items: center; margin: 8px;">' +
'<input type="text" class="oe_search_input" placeholder="' + _t('Search in all records...') + '" style="flex: 1;">' +
'<button class="btn btn-sm btn-secondary oe_clear_search ml-2" style="display: none;">' + _t('Clear') + '</button>' +
'<span class="oe_search_count ml-2" style="display: none;"></span>' +
'</div>';
self.$el.find('table').addClass('oe_table_search');
var $search = $(search).css('border', '1px solid #ccc')
.css('width', '99%')
.css('height', '28px')
var $search = $(search);
$search.find('.oe_search_input').css({
'border': '1px solid #ccc',
'height': '32px',
'padding': '0 12px',
'border-radius': '4px'
});
$search.find('.oe_clear_search').on('click', function() {
self._clearSearch();
});
self.$el.prepend($search);
// Store original domain for restoration
self._originalDomain = self.state.domain || [];
self._searchTimeout = null;
}
}
});
},
_onKeyUp: function (event) {
var value = $(event.currentTarget).val().toLowerCase();
var count_row = 0;
var $el = $(this.$el)
$(".oe_table_search tr:not(:first)").filter(function() {
$(this).toggle(arabicCaseInsensitiveSearch($(this).text(),value))
count_row = arabicCaseInsensitiveSearch($(this).text(),value) ? count_row+1 : count_row
var self = this;
var value = $(event.currentTarget).val();
// Clear previous timeout
if (this._searchTimeout) {
clearTimeout(this._searchTimeout);
}
// Add delay to avoid too many requests
this._searchTimeout = setTimeout(function() {
self._performSearch(value);
}, 500); // 500ms delay
},
_onSearchChange: function (event) {
var value = $(event.currentTarget).val();
this._performSearch(value);
},
_performSearch: function (searchValue) {
var self = this;
var value = searchValue.toLowerCase().trim();
if (!value) {
this._clearSearch();
return;
}
// Show/hide clear button
this.$el.find('.oe_clear_search').toggle(!!value);
// Build search domain
var searchDomain = this._buildSearchDomain(value);
// Combine with original domain
var newDomain = this._originalDomain.concat(searchDomain);
// Update the domain and reload
this.trigger_up('reload', {
domain: newDomain,
offset: 0,
limit: this.state.limit,
keepChanges: false,
reload: true,
callback: function() {
self._updateSearchCount(value);
}
});
},
_buildSearchDomain: function(value) {
var self = this;
var domain = [];
var searchableFields = this._getSearchableFields();
if (searchableFields.length === 0) {
return [];
}
// Normalize search value for Arabic
var normalizedValue = this._normalizeArabic(value);
// Build OR domain for all searchable fields
var orConditions = [];
_.each(searchableFields, function(field) {
// For char, text, and name fields
if (['char', 'text', 'html'].includes(field.type)) {
orConditions.push([field.name, 'ilike', value]);
// Also search with normalized value if different
if (normalizedValue !== value) {
orConditions.push([field.name, 'ilike', normalizedValue]);
}
}
// For many2one fields (search in display_name)
else if (field.type === 'many2one') {
orConditions.push([field.name + '.display_name', 'ilike', value]);
}
// For number fields (if value is numeric)
else if (['integer', 'float', 'monetary'].includes(field.type) && !isNaN(value)) {
orConditions.push([field.name, '=', parseFloat(value)]);
}
// For selection fields
else if (field.type === 'selection') {
orConditions.push([field.name, 'ilike', value]);
}
});
// Create OR domain
if (orConditions.length > 0) {
// Add '|' operators for OR condition
for (var i = 1; i < orConditions.length; i++) {
domain.push('|');
}
domain = domain.concat(orConditions);
}
return domain;
},
_getSearchableFields: function() {
var self = this;
var fields = [];
// Get visible fields from the list view
_.each(this.columns, function(column) {
if (!column.invisible && column.attrs.name) {
var fieldName = column.attrs.name;
var field = self.state.fields[fieldName];
if (field && field.searchable !== false) {
fields.push({
name: fieldName,
type: field.type,
string: field.string
});
}
}
});
// If no visible fields, get all char/text fields
if (fields.length === 0) {
_.each(this.state.fields, function(field, fieldName) {
if (['char', 'text', 'many2one'].includes(field.type) && field.searchable !== false) {
fields.push({
name: fieldName,
type: field.type,
string: field.string
});
}
});
}
return fields;
},
_clearSearch: function() {
var self = this;
// Clear input
this.$el.find('.oe_search_input').val('');
this.$el.find('.oe_clear_search').hide();
this.$el.find('.oe_search_count').hide();
// Restore original domain
this.trigger_up('reload', {
domain: this._originalDomain,
offset: 0,
limit: this.state.limit,
keepChanges: false,
reload: true
});
},
_updateSearchCount: function(searchValue) {
var count = this.state.count || 0;
var $countEl = this.$el.find('.oe_search_count');
if (searchValue && count >= 0) {
$countEl.text(_t('Found: ') + count + _t(' records')).show();
} else {
$countEl.hide();
}
},
_normalizeArabic: function(text) {
if (!text) return text;
// Normalizing Arabic text by removing common variations
return text
.replace(/[\u064B-\u065F]/g, '') // Remove diacritics
.replace(/[\u0660-\u0669]/g, (d) => String.fromCharCode(d.charCodeAt(0) - 0x0660 + 0x0030)) // Convert Arabic-Indic digits
.replace(/[\u06F0-\u06F9]/g, (d) => String.fromCharCode(d.charCodeAt(0) - 0x06F0 + 0x0030)) // Extended Arabic-Indic digits
.replace(/[\u0622\u0623\u0625\u0627]/g, 'ا') // Normalize Alef forms
.replace(/[\u0629]/g, 'ه') // Normalize Teh Marbuta to Heh
.replace(/[\u064A\u0626\u0649]/g, 'ي') // Normalize Yeh forms
.replace(/[\u0624\u0648]/g, 'و'); // Normalize Waw variants
},
destroy: function () {
// Clear timeout if exists
if (this._searchTimeout) {
clearTimeout(this._searchTimeout);
}
return this._super.apply(this, arguments);
}
});
function normalizeArabic(text) {
// Normalizing Arabic text by removing common variations
return text
.replace(/[\u064B-\u065F]/g, '') // Remove diacritics
.replace(/[\u0660-\u0669]/g, (d) => String.fromCharCode(d.charCodeAt(0) - 0x0660 + 0x0030)) // Convert Arabic-Indic digits to Latin digits
.replace(/[\u06F0-\u06F9]/g, (d) => String.fromCharCode(d.charCodeAt(0) - 0x06F0 + 0x0030)) // Convert Extended Arabic-Indic digits to Latin digits
.replace(/[\u0622\u0623\u0625\u0627]/g, 'ا') // Normalize different forms of Alef
.replace(/[\u0629]/g, 'ه') // Normalize Teh Marbuta to Heh
.replace(/[\u064A\u0626\u0649]/g, 'ي') // Normalize different forms of Yeh
.replace(/[\u0624\u0648]/g, 'و'); // Normalize Waw and its variants
}
function arabicCaseInsensitiveSearch(text, searchTerm) {
const normalizedText = normalizeArabic(text).toLowerCase();
const normalizedSearchTerm = normalizeArabic(searchTerm).toLowerCase();
return normalizedText.indexOf(normalizedSearchTerm) > -1;
}
});