Fix: Enhanced General Search filtering in Tree/List Views

Merge pull request #4426 from expsa/eltayar

Enhanced General Search functionality in Tree/List Views:
- Fixed DOM filtering logic to ensure list view updates with filtered records  
- Enhanced row ID detection with 7 different methods for better compatibility
- Added multiple rendering fallback methods for different Odoo versions
- Improved search result processing and synchronization between counter and list
- Enhanced Arabic text support and client-side search fallback
This commit is contained in:
Mohamed Eltayar 2025-08-29 20:47:28 +03:00 committed by GitHub
commit 4333b0460e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 252 additions and 167 deletions

View File

@ -30,8 +30,9 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) {
_renderView: function () {
var self = this;
// IMPORTANT: Don't reset search state, just preserve it
var hasActiveSearch = this._search && this._search.value;
// Store search state before render
var searchValue = this._search ? this._search.value : '';
var hasActiveSearch = searchValue && searchValue.length > 0;
return this._super.apply(this, arguments).then(function () {
// Add search box if needed
@ -43,15 +44,16 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) {
self._addSearchBox();
}
// Always restore search state after render
// Always restore search UI
self._restoreSearchUI();
// If we have an active search, apply the filter
if (hasActiveSearch && self._search.filteredIds) {
// Apply filter after DOM is ready
// If we had an active search, re-apply it after a short delay
if (hasActiveSearch) {
setTimeout(function() {
self._reapplySearchFilter();
}, 0);
if (self._search && self._search.value) {
self._performSearch(self._search.value);
}
}, 100);
}
}
});
@ -115,21 +117,6 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) {
}
},
/**
* Reapply search filter after re-render
*/
_reapplySearchFilter: function() {
if (!this._search || !this._search.filteredIds) return;
// If we have all filtered records, update the view
if (this._search.allFilteredRecords) {
this._updateViewWithFilteredData(this._search.allFilteredRecords, false);
} else {
// Just apply DOM filter
this._applyDOMFilter(this._search.filteredIds);
}
},
/**
* Handle search input event (immediate tracking)
*/
@ -263,213 +250,292 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) {
return;
}
// Update the view with filtered results
this._updateViewWithFilteredData(records, true);
// Update the view with filtered results - CRITICAL FIX
this._updateViewWithFilteredData(records);
},
/**
* Update view with filtered data - THE KEY METHOD
* Update view with filtered data - COMPLETELY REWRITTEN
*/
_updateViewWithFilteredData: function(records, updateState) {
_updateViewWithFilteredData: function(records) {
var self = this;
var limit = this.state.limit || 80;
var pageRecords = records.slice(0, limit);
// Method 1: Try DOM filtering first (fastest, preserves input)
var domFilterSuccess = this._tryDOMFiltering(this._search.filteredIds, limit);
// Method 1: Try to filter existing DOM rows first
var domFilterWorked = this._filterExistingRows();
if (domFilterSuccess) {
// DOM filtering worked, just update pager
if (domFilterWorked) {
// Update pager with correct count
this._updatePager(records.length);
return;
}
// Method 2: Need to update data and re-render
// This happens when DOM doesn't have the records we need
if (updateState && this.state && this.state.data) {
// Update state with filtered data
this.state.data.records = pageRecords;
this.state.count = records.length;
this.state.res_ids = this._search.filteredIds;
// Clear and re-render
this._clearTableBody();
this._renderFilteredRows();
}
// Method 2: Update the actual data and re-render
this._updateStateAndRerender(records);
},
/**
* Try DOM filtering first (fastest method)
* Filter existing rows in DOM - ENHANCED METHOD
*/
_tryDOMFiltering: function(filteredIds, limit) {
var $rows = this.$('.o_data_row');
if ($rows.length === 0) {
return false; // No rows to filter
_filterExistingRows: function() {
if (!this._search.filteredIds || this._search.filteredIds.length === 0) {
return false;
}
var self = this;
var $allRows = this.$('.o_data_row');
var limit = this.state.limit || 80;
var visibleCount = 0;
var foundMatch = false;
var foundAnyMatch = false;
$rows.each(function() {
console.log('Filtering rows. Total rows:', $allRows.length, 'Filtered IDs:', this._search.filteredIds);
// First pass: hide all rows
$allRows.hide();
// Second pass: show matching rows up to limit
$allRows.each(function(index) {
var $row = $(this);
var rowId = self._getRowId($row);
if (rowId && filteredIds.includes(rowId)) {
console.log('Row', index, 'ID:', rowId, 'Should show:', self._search.filteredIds.includes(rowId));
if (rowId && self._search.filteredIds.includes(rowId)) {
if (visibleCount < limit) {
$row.show();
visibleCount++;
foundMatch = true;
} else {
$row.hide();
foundAnyMatch = true;
console.log('Showing row', index, 'with ID', rowId);
}
} else {
$row.hide();
}
});
return foundMatch;
},
/**
* Clear table body
*/
_clearTableBody: function() {
this.$('tbody .o_data_row').remove();
this.$('.oe_no_results').remove();
},
/**
* Render filtered rows
*/
_renderFilteredRows: function() {
var self = this;
console.log('DOM filtering result - Found matches:', foundAnyMatch, 'Visible count:', visibleCount);
try {
// Try Odoo's internal render methods
if (typeof this._renderRows === 'function') {
var $rows = this._renderRows();
if ($rows && $rows.length) {
this.$('tbody').append($rows);
this._updatePager(this._search.filteredIds.length);
return;
}
}
if (typeof this._renderBody === 'function') {
var result = this._renderBody();
if (result && typeof result.then === 'function') {
result.then(function() {
self._updatePager(self._search.filteredIds.length);
});
} else {
this._updatePager(this._search.filteredIds.length);
}
return;
}
} catch (err) {
console.warn('Render failed, using fallback:', err);
// If no matches found in DOM, we need to re-render
return foundAnyMatch;
},
/**
* Update state and re-render - ENHANCED METHOD
*/
_updateStateAndRerender: function(records) {
var self = this;
var limit = this.state.limit || 80;
var pageRecords = records.slice(0, limit);
console.log('Re-rendering with filtered data. Total records:', records.length, 'Page records:', pageRecords.length);
// Update state
if (this.state && this.state.data) {
this.state.data.records = pageRecords;
this.state.count = records.length;
this.state.res_ids = this._search.filteredIds;
}
// Fallback: Create rows manually
this._createRowsManually();
// Clear existing rows
this.$('tbody .o_data_row').remove();
this.$('.oe_no_results').remove();
// Try different rendering approaches
this._tryMultipleRenderMethods(pageRecords);
// Update pager
this._updatePager(records.length);
},
/**
* Create rows manually as last resort
* Try multiple rendering methods - NEW ENHANCED METHOD
*/
_createRowsManually: function() {
_tryMultipleRenderMethods: function(records) {
var self = this;
var records = this.state.data.records;
var renderSuccess = false;
if (!records || records.length === 0) return;
// Method 1: Try Odoo's _renderRows if available
if (!renderSuccess && typeof this._renderRows === 'function') {
try {
var $rows = this._renderRows();
if ($rows && $rows.length > 0) {
this.$('tbody').append($rows);
renderSuccess = true;
console.log('Render method 1 succeeded');
}
} catch (err) {
console.warn('Render method 1 failed:', err);
}
}
records.forEach(function(record) {
// Method 2: Try _renderBody if available
if (!renderSuccess && typeof this._renderBody === 'function') {
try {
var result = this._renderBody();
if (result) {
if (typeof result.then === 'function') {
result.then(function() {
console.log('Render method 2 succeeded (async)');
});
} else {
console.log('Render method 2 succeeded (sync)');
}
renderSuccess = true;
}
} catch (err) {
console.warn('Render method 2 failed:', err);
}
}
// Method 3: Manual row creation (fallback)
if (!renderSuccess) {
console.log('Using manual row creation fallback');
this._createRowsManually(records);
renderSuccess = true;
}
return renderSuccess;
},
/**
* Create rows manually - ENHANCED FALLBACK
*/
_createRowsManually: function(records) {
var self = this;
if (!records || records.length === 0) {
console.log('No records to create rows for');
return;
}
console.log('Creating', records.length, 'rows manually');
records.forEach(function(record, index) {
var $row = $('<tr class="o_data_row"></tr>');
$row.data('id', record.id);
$row.attr('data-id', record.id);
// Add cells for each visible column
if (self.columns) {
// Set multiple ID attributes for better compatibility
$row.attr('data-id', record.id);
$row.attr('data-res-id', record.id);
$row.data('id', record.id);
$row.data('res-id', record.id);
// Store record data
$row.data('record', record);
console.log('Creating row for record ID:', record.id);
// Add cells based on visible columns
if (self.columns && self.columns.length > 0) {
self.columns.forEach(function(col) {
if (!col.invisible && col.attrs && col.attrs.name) {
var value = record[col.attrs.name] || '';
// Handle many2one fields
if (typeof value === 'object' && value) {
if (Array.isArray(value) && value.length >= 2) {
value = value[1];
} else if (value.display_name) {
value = value.display_name;
} else {
value = '';
}
}
var $cell = $('<td class="o_data_cell"></td>');
$cell.text(value);
$cell.attr('data-field', col.attrs.name);
var $cell = self._createCellForField(record, col);
$row.append($cell);
}
});
} else {
// Fallback: create basic cells
var cellValue = record.display_name || record.name || record.id;
var $cell = $('<td class="o_data_cell"></td>').text(cellValue);
$row.append($cell);
}
self.$('tbody').append($row);
});
this._updatePager(this._search.filteredIds.length);
console.log('Manual row creation completed');
},
/**
* Apply DOM filter (used after re-render)
* Create cell for specific field - NEW HELPER METHOD
*/
_applyDOMFilter: function(filteredIds) {
var self = this;
var $rows = this.$('.o_data_row');
var limit = this.state.limit || 80;
var visibleCount = 0;
_createCellForField: function(record, col) {
var fieldName = col.attrs.name;
var value = record[fieldName];
var displayValue = '';
$rows.each(function() {
var $row = $(this);
var rowId = self._getRowId($row);
if (rowId && filteredIds.includes(rowId) && visibleCount < limit) {
$row.show();
visibleCount++;
// Handle different field types
if (value !== null && value !== undefined) {
if (typeof value === 'object' && value) {
// Many2one field
if (Array.isArray(value) && value.length >= 2) {
displayValue = value[1];
} else if (value.display_name) {
displayValue = value.display_name;
} else {
displayValue = String(value);
}
} else {
$row.hide();
displayValue = String(value);
}
});
}
var $cell = $('<td class="o_data_cell"></td>');
$cell.text(displayValue);
$cell.attr('data-field', fieldName);
// Add field-specific classes if needed
if (col.attrs.class) {
$cell.addClass(col.attrs.class);
}
return $cell;
},
/**
* Get row ID from DOM element
* Get row ID from DOM element - ENHANCED METHOD
*/
_getRowId: function($row) {
// Method 1: data-id attribute
var id = $row.attr('data-id');
if (id) return parseInt(id);
if (id && !isNaN(parseInt(id))) {
return parseInt(id);
}
// Method 2: jQuery data
id = $row.data('id');
if (id) return parseInt(id);
// Method 3: data-res-id attribute
// Method 2: data-res-id attribute
id = $row.attr('data-res-id');
if (id) return parseInt(id);
if (id && !isNaN(parseInt(id))) {
return parseInt(id);
}
// Method 3: jQuery data id
id = $row.data('id');
if (id && !isNaN(parseInt(id))) {
return parseInt(id);
}
// Method 4: jQuery data res-id
id = $row.data('res-id');
if (id) return parseInt(id);
// Method 5: Check for record object
var record = $row.data('record');
if (record) {
if (record.res_id) return record.res_id;
if (record.id) return record.id;
if (record.data && record.data.id) return record.data.id;
if (id && !isNaN(parseInt(id))) {
return parseInt(id);
}
// Method 5: Check for record object in data
var record = $row.data('record');
if (record) {
if (record.res_id && !isNaN(parseInt(record.res_id))) {
return parseInt(record.res_id);
}
if (record.id && !isNaN(parseInt(record.id))) {
return parseInt(record.id);
}
if (record.data && record.data.id && !isNaN(parseInt(record.data.id))) {
return parseInt(record.data.id);
}
}
// Method 6: Look for hidden inputs with record ID
var $hiddenInput = $row.find('input[name="id"]');
if ($hiddenInput.length) {
id = $hiddenInput.val();
if (id && !isNaN(parseInt(id))) {
return parseInt(id);
}
}
// Method 7: Check if row has any reference to record ID in classes
var classes = $row.attr('class') || '';
var match = classes.match(/o_data_row_(\d+)/);
if (match) {
return parseInt(match[1]);
}
console.warn('Could not determine row ID for row:', $row[0]);
return null;
},
@ -487,7 +553,7 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) {
},
/**
* Client-side search fallback
* Client-side search fallback - ENHANCED
*/
_clientSideSearch: function(value) {
var self = this;
@ -497,17 +563,25 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) {
var limit = this.state.limit || 80;
var matchedIds = [];
console.log('Using client-side search fallback');
this.$('.o_data_row').each(function() {
var $row = $(this);
var text = $row.text().toLowerCase();
var normalizedText = self._normalizeArabic(text);
var match = text.includes(searchLower) || normalizedText.includes(normalized);
if (match && visible < limit) {
$row.show();
visible++;
var rowId = self._getRowId($row);
if (rowId) matchedIds.push(rowId);
if (match) {
if (visible < limit) {
$row.show();
visible++;
var rowId = self._getRowId($row);
if (rowId) {
matchedIds.push(rowId);
}
} else {
$row.hide();
}
} else {
$row.hide();
}
@ -528,12 +602,16 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) {
}
this._updatePager(visible);
console.log('Client-side search completed. Visible:', visible, 'IDs:', matchedIds);
},
/**
* Clear search
* Clear search - ENHANCED
*/
_clearSearch: function() {
console.log('Clearing search');
// Clear UI
this.$('.oe_search_input').val('');
this.$('.oe_clear_search').hide();
@ -574,18 +652,23 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) {
this._search.originalData = null;
this._search.originalDomain = null;
} else {
// Just show rows with limit
// Just show all rows with limit
var $rows = this.$('.o_data_row');
var limit = this.state.limit || 80;
var limit = this.state ? (this.state.limit || 80) : 80;
$rows.show(); // Show all first
$rows.each(function(index) {
$(this).toggle(index < limit);
if (index >= limit) {
$(this).hide();
}
});
if (this.state) {
this._updatePager(this.state.count || $rows.length);
}
}
console.log('Search cleared');
},
/**
@ -602,6 +685,8 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) {
offset: this.state.offset || 0
};
this._search.originalDomain = this.state.domain ? this.state.domain.slice() : [];
console.log('Stored original data:', this._search.originalData);
}
},