Complete rewrite with clear logic flow - DOM filtering first, then state update with re-render

This commit is contained in:
Mohamed Eltayar 2025-08-29 20:11:57 +03:00
parent 059383f5fb
commit 4e28b9d652
1 changed files with 442 additions and 432 deletions

View File

@ -4,6 +4,7 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) {
var ListRenderer = require('web.ListRenderer');
var core = require('web.core');
var _t = core._t;
var concurrency = require('web.concurrency');
ListRenderer.include({
events: _.extend({}, ListRenderer.prototype.events, {
@ -12,44 +13,57 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) {
'click .oe_clear_search': '_onClearSearchClick'
}),
/**
* @override
*/
init: function () {
this._super.apply(this, arguments);
// Initialize mutex for preventing concurrent operations
this._searchMutex = new concurrency.Mutex();
},
/**
* @override
*/
_renderView: function () {
var self = this;
// Store search value before render
var previousSearchValue = '';
var previousFilteredIds = null;
// Save search state before render
var savedSearchValue = '';
var savedFilteredIds = null;
if (this._search) {
previousSearchValue = this._search.value || '';
previousFilteredIds = this._search.filteredIds;
savedSearchValue = this._search.value || '';
savedFilteredIds = this._search.filteredIds;
}
return this._super.apply(this, arguments).then(function () {
// Check if search box already exists
var existingInput = self.$el.find('.oe_search_input');
// Only add search box to tree views if not exists
// Add search box if needed
if (self.arch && self.arch.tag === 'tree' &&
self.$el && self.$el.hasClass('o_list_view') &&
!existingInput.length) {
!self.$el.find('.oe_search_input').length) {
self._addSearchBox();
self._initSearchState();
}
// Restore previous search state if we had one
if (previousSearchValue) {
// Restore search state if existed
if (savedSearchValue) {
if (!self._search) {
self._initSearchState();
}
self._search.value = previousSearchValue;
self._search.filteredIds = previousFilteredIds;
self._restoreSearchInput();
self._search.value = savedSearchValue;
self._search.filteredIds = savedFilteredIds;
// If we had filtered results, apply filter to new render
if (previousFilteredIds && previousFilteredIds.length > 0) {
self._applySearchFilter(previousFilteredIds);
// Restore UI
self.$('.oe_search_input').val(savedSearchValue);
self.$('.oe_clear_search').show();
// Re-apply filter if we had filtered results
if (savedFilteredIds && savedFilteredIds.length > 0) {
// Use timeout to ensure DOM is ready
setTimeout(function() {
self._applyDOMFilter(savedFilteredIds);
}, 0);
}
}
});
@ -59,7 +73,6 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) {
* Add search box to view
*/
_addSearchBox: function() {
// Check if already exists
if (this.$el.find('.oe_search_container').length) {
return;
}
@ -85,14 +98,14 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) {
originalData: null,
originalDomain: null,
filteredIds: null,
filteredRecords: null,
allFilteredRecords: null, // Store all filtered records
isFiltered: false
};
}
},
/**
* Handle search input event
* Handle search input event (immediate tracking)
*/
_onSearchInput: function(e) {
if (!this._search) {
@ -142,72 +155,421 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) {
},
/**
* Perform search
* Perform search - THE MAIN SEARCH LOGIC
*/
_performSearch: function(value) {
var self = this;
// Clear if empty
if (!value) {
this._clearSearch();
return;
}
// Check prerequisites
if (!this.state || !this.state.model) {
console.error('Missing model information');
return;
}
// Prevent concurrent searches
if (this._search.active) {
return;
}
// Store original data on first search
if (!this._search.originalData && !this._search.isFiltered) {
this._storeOriginalData();
}
// Start search
this._search.active = true;
this._showLoading(true);
// Build domain
var domain = this._buildSearchDomain(value);
// Add base domain if exists
var baseDomain = this._search.originalDomain || this.state.domain || [];
if (baseDomain.length > 0) {
domain = ['&'].concat(baseDomain).concat(domain);
}
// Get fields to read
var fields = this._getReadFields();
// Execute search
this._rpc({
model: this.state.model,
method: 'search_read',
args: [domain],
kwargs: {
fields: fields,
limit: false,
context: this.state.context || {}
// Use mutex to prevent concurrent searches
return this._searchMutex.exec(function() {
// Clear if empty
if (!value) {
return self._clearSearch();
}
}).then(function(results) {
self._search.isFiltered = true;
self._search.filteredRecords = results;
self._displayResults(results);
}).catch(function(error) {
console.error('Search failed:', error);
self._clientSideSearch(value);
}).finally(function() {
self._search.active = false;
self._showLoading(false);
// Check prerequisites
if (!self.state || !self.state.model) {
console.error('Missing model information');
return;
}
// Store original data on first search
if (!self._search.originalData && !self._search.isFiltered) {
self._storeOriginalData();
}
// Show loading
self._showLoading(true);
// Build domain
var domain = self._buildSearchDomain(value);
// Add base domain if exists
var baseDomain = self._search.originalDomain || self.state.domain || [];
if (baseDomain.length > 0) {
domain = ['&'].concat(baseDomain).concat(domain);
}
// Get fields to read
var fields = self._getReadFields();
// Execute search
return self._rpc({
model: self.state.model,
method: 'search_read',
args: [domain],
kwargs: {
fields: fields,
limit: false, // Get all records for accurate count
context: self.state.context || {}
}
}).then(function(results) {
// Process and display results
self._processSearchResults(results);
}).catch(function(error) {
console.error('Search failed:', error);
// Fallback to client-side search
self._clientSideSearch(value);
}).finally(function() {
self._showLoading(false);
});
});
},
/**
* Process search results from server
*/
_processSearchResults: function(records) {
var self = this;
var count = records.length;
// Update search state
this._search.isFiltered = true;
this._search.allFilteredRecords = records;
this._search.filteredIds = records.map(function(r) { return r.id; });
// Show count
this.$('.oe_search_count').text(_t('Found: ') + count + _t(' records')).show();
if (count === 0) {
this._showNoResults();
return;
}
// IMPORTANT: Update the view with filtered results
this._updateViewWithFilteredData(records);
},
/**
* Update view with filtered data - THE KEY METHOD
*/
_updateViewWithFilteredData: function(records) {
var self = this;
var limit = this.state.limit || 80;
var pageRecords = records.slice(0, limit);
// Method 1: Try DOM filtering first (preserves everything)
if (this._tryDOMFiltering(this._search.filteredIds, limit)) {
return; // Success with DOM filtering
}
// Method 2: Update state and trigger re-render
if (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 existing rows
this.$('tbody .o_data_row').remove();
this.$('.oe_no_results').remove();
// Re-render rows using Odoo's method
this._renderRowsForFilteredData();
}
},
/**
* Try DOM filtering first (fastest method)
*/
_tryDOMFiltering: function(filteredIds, limit) {
var $rows = this.$('.o_data_row');
if ($rows.length === 0) {
return false; // No rows to filter
}
var self = this;
var visibleCount = 0;
var foundMatch = false;
$rows.each(function() {
var $row = $(this);
var rowId = self._getRowId($row);
if (rowId && filteredIds.includes(rowId)) {
if (visibleCount < limit) {
$row.show();
visibleCount++;
} else {
$row.hide();
}
foundMatch = true;
} else {
$row.hide();
}
});
// Update pager if we found matches
if (foundMatch) {
this._updatePager(filteredIds.length);
}
return foundMatch;
},
/**
* Render rows for filtered data
*/
_renderRowsForFilteredData: function() {
var self = this;
try {
// Method 1: Use _renderRows if available
if (typeof this._renderRows === 'function') {
var $rows = this._renderRows();
if ($rows && $rows.length) {
this.$('tbody').append($rows);
this._updatePager(this._search.filteredIds.length);
return;
}
}
// Method 2: Use _renderBody
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);
}
// Fallback: Create rows manually
this._createRowsManually();
},
/**
* Create rows manually as last resort
*/
_createRowsManually: function() {
var self = this;
var records = this.state.data.records;
records.forEach(function(record) {
var $row = $('<tr class="o_data_row"></tr>');
$row.data('id', record.id);
// Add cells for each visible column
if (self.columns) {
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) {
value = value[1] || value.display_name || '';
}
var $cell = $('<td class="o_data_cell"></td>');
$cell.text(value);
$cell.attr('data-field', col.attrs.name);
$row.append($cell);
}
});
}
self.$('tbody').append($row);
});
this._updatePager(this._search.filteredIds.length);
},
/**
* Apply DOM filter (used after re-render)
*/
_applyDOMFilter: function(filteredIds) {
var self = this;
var $rows = this.$('.o_data_row');
var limit = this.state.limit || 80;
var visibleCount = 0;
$rows.each(function() {
var $row = $(this);
var rowId = self._getRowId($row);
if (rowId && filteredIds.includes(rowId) && visibleCount < limit) {
$row.show();
visibleCount++;
} else {
$row.hide();
}
});
// Update count display
if (this._search && this._search.filteredIds) {
this.$('.oe_search_count')
.text(_t('Found: ') + this._search.filteredIds.length + _t(' records'))
.show();
}
},
/**
* Get row ID from DOM element
*/
_getRowId: function($row) {
// Method 1: data-id attribute
var id = $row.data('id');
if (id) return parseInt(id);
// Method 2: data-res-id attribute
id = $row.data('res-id');
if (id) return parseInt(id);
// Method 3: 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;
}
// Method 4: dataset
if ($row[0] && $row[0].dataset) {
if ($row[0].dataset.id) return parseInt($row[0].dataset.id);
if ($row[0].dataset.resId) return parseInt($row[0].dataset.resId);
}
return null;
},
/**
* Update pager
*/
_updatePager: function(totalCount) {
if (this.getParent() && this.getParent().pager) {
this.getParent().pager.updateState({
current_min: totalCount > 0 ? 1 : 0,
size: totalCount,
limit: this.state.limit || 80
});
}
},
/**
* Client-side search fallback
*/
_clientSideSearch: function(value) {
var searchLower = value.toLowerCase();
var normalized = this._normalizeArabic(searchLower);
var visible = 0;
var limit = this.state.limit || 80;
var matchedIds = [];
this.$('.o_data_row').each(function() {
var $row = $(this);
var text = $row.text().toLowerCase();
var normalizedText = this._normalizeArabic ? this._normalizeArabic(text) : text;
var match = text.includes(searchLower) || normalizedText.includes(normalized);
if (match && visible < limit) {
$row.show();
visible++;
var rowId = parseInt($row.data('id') || $row.data('res-id'));
if (rowId) matchedIds.push(rowId);
} else {
$row.hide();
}
}.bind(this));
// Update state
this._search.isFiltered = true;
this._search.filteredIds = matchedIds;
// Update count
var msg = visible > 0 ?
_t('Found: ') + visible + _t(' records') :
_t('No records found');
this.$('.oe_search_count').text(msg).show();
if (visible === 0) {
this._showNoResults();
}
this._updatePager(visible);
},
/**
* Clear search
*/
_clearSearch: function() {
var self = this;
// Clear UI
this.$('.oe_search_input').val('');
this.$('.oe_clear_search').hide();
this.$('.oe_search_count').hide();
this.$('.oe_no_results').remove();
// Clear state
this._search.value = '';
this._search.filteredIds = null;
this._search.allFilteredRecords = null;
this._search.isFiltered = false;
// Restore original data
if (this._search.originalData) {
// Restore state
if (this.state) {
if (this.state.data) {
this.state.data.records = this._search.originalData.records;
}
this.state.count = this._search.originalData.count;
this.state.res_ids = this._search.originalData.res_ids;
this.state.domain = this._search.originalDomain || [];
}
// Show all rows with limit
var $rows = this.$('.o_data_row');
var limit = this._search.originalData.limit || 80;
$rows.each(function(index) {
$(this).toggle(index < limit);
});
// Restore pager
this._updatePager(this._search.originalData.count);
// Clear stored data
this._search.originalData = null;
this._search.originalDomain = null;
} else {
// Just show rows with limit
var $rows = this.$('.o_data_row');
var limit = this.state.limit || 80;
$rows.each(function(index) {
$(this).toggle(index < limit);
});
if (this.state) {
this._updatePager(this.state.count || $rows.length);
}
}
},
/**
* Store original data
*/
_storeOriginalData: function() {
if (this.state) {
this._search.originalData = {
records: this.state.data && this.state.data.records ?
this.state.data.records.slice() : [],
count: this.state.count || 0,
res_ids: this.state.res_ids ? this.state.res_ids.slice() : [],
limit: this.state.limit || 80,
offset: this.state.offset || 0
};
this._search.originalDomain = this.state.domain ? this.state.domain.slice() : [];
}
},
/**
* Build search domain
*/
@ -312,359 +674,7 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) {
},
/**
* Display search results
*/
_displayResults: function(records) {
var self = this;
var count = records.length;
// Show count
this.$('.oe_search_count').text(_t('Found: ') + count + _t(' records')).show();
if (count === 0) {
this._showNoResults();
return;
}
// Store filtered IDs
this._search.filteredIds = records.map(function(r) { return r.id; });
// Apply filter to current view
this._applySearchFilter(this._search.filteredIds);
},
/**
* Apply search filter to current rows
*/
_applySearchFilter: function(filteredIds) {
var self = this;
// Method 1: Try DOM filtering first (fastest, preserves input)
var $rows = this.$('.o_data_row');
if ($rows.length > 0) {
var visibleCount = 0;
var limit = this.state.limit || 80;
$rows.each(function() {
var $row = $(this);
var rowId = self._getRowId($row);
if (rowId && filteredIds.includes(rowId) && visibleCount < limit) {
$row.show();
visibleCount++;
} else {
$row.hide();
}
});
// If we have visible rows, we're done
if (visibleCount > 0) {
this._updatePager(filteredIds.length);
return;
}
}
// Method 2: If no rows visible or no rows in DOM, need to render new data
if (this._search.filteredRecords) {
this._renderFilteredRecords(this._search.filteredRecords);
}
},
/**
* Render filtered records when DOM filtering is not enough
*/
_renderFilteredRecords: function(records) {
var self = this;
// Get records for current page
var limit = this.state.limit || 80;
var pageRecords = records.slice(0, limit);
// Update state data
if (this.state && this.state.data) {
// Store original records reference
if (!this._search.originalRecords) {
this._search.originalRecords = this.state.data.records;
}
// Update with filtered records
this.state.data.records = pageRecords;
this.state.count = records.length;
this.state.res_ids = records.map(function(r) { return r.id; });
}
// Remove old rows
this.$('tbody .o_data_row').remove();
this.$('.oe_no_results').remove();
// Try to use Odoo's internal _renderRows (without parameters)
if (typeof this._renderRows === 'function') {
try {
// _renderRows uses state.data.records internally
var $rows = this._renderRows();
if ($rows && $rows.length) {
this.$('tbody').append($rows);
} else {
// Fallback if no rows returned
this._createBasicRows(pageRecords);
}
} catch (err) {
console.warn('_renderRows failed:', err);
this._createBasicRows(pageRecords);
}
} else if (typeof this._renderBody === 'function') {
try {
// Try _renderBody as alternative
var result = this._renderBody();
if (result && typeof result.then === 'function') {
result.then(function() {
// Body rendered
});
}
} catch (err) {
console.warn('_renderBody failed:', err);
this._createBasicRows(pageRecords);
}
} else {
// Direct fallback
this._createBasicRows(pageRecords);
}
// Update pager
this._updatePager(records.length);
},
/**
* Create basic rows as fallback
*/
_createBasicRows: function(records) {
var self = this;
records.forEach(function(record) {
var $row = $('<tr class="o_data_row"></tr>');
$row.data('id', record.id);
// Add cells for each column
if (self.columns) {
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) {
value = value[1] || value.display_name || '';
}
var $cell = $('<td class="o_data_cell"></td>');
$cell.text(value);
$cell.attr('data-field', col.attrs.name);
$row.append($cell);
}
});
}
self.$('tbody').append($row);
});
},
/**
* Update pager with new count
*/
_updatePager: function(totalCount) {
if (this.getParent() && this.getParent().pager) {
this.getParent().pager.updateState({
current_min: totalCount > 0 ? 1 : 0,
size: totalCount,
limit: this.state.limit || 80
});
}
},
/**
* Get row ID from DOM element
*/
_getRowId: function($row) {
// Method 1: data-id attribute
var id = $row.data('id');
if (id) return parseInt(id);
// Method 2: 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;
}
// Method 3: Odoo's internal data
if ($row[0] && $row[0].dataset) {
if ($row[0].dataset.id) return parseInt($row[0].dataset.id);
}
// Method 4: Check for o_res_id class
var classes = $row.attr('class') || '';
var match = classes.match(/o_res_id_(\d+)/);
if (match) return parseInt(match[1]);
return null;
},
/**
* Client-side search fallback
*/
_clientSideSearch: function(value) {
var searchLower = value.toLowerCase();
var normalized = this._normalizeArabic(searchLower);
var visible = 0;
var limit = this.state.limit || 80;
var matchedIds = [];
this.$('.o_data_row').each(function() {
var $row = $(this);
var text = $row.text().toLowerCase();
var match = text.includes(searchLower) || text.includes(normalized);
if (match && visible < limit) {
$row.show();
visible++;
var rowId = parseInt($row.data('id'));
if (rowId) matchedIds.push(rowId);
} else {
$row.hide();
}
});
// Update search state
this._search.isFiltered = true;
this._search.filteredIds = matchedIds;
// Update count
var msg = visible > 0 ?
_t('Found: ') + visible + _t(' records') :
_t('No records found');
this.$('.oe_search_count').text(msg).show();
if (visible === 0) {
this._showNoResults();
}
// Update pager
this._updatePager(visible);
},
/**
* Clear search and restore original view
*/
_clearSearch: function() {
// Clear UI
this.$('.oe_search_input').val('');
this.$('.oe_clear_search').hide();
this.$('.oe_search_count').hide();
this.$('.oe_no_results').remove();
// Clear state
this._search.value = '';
this._search.filteredIds = null;
this._search.filteredRecords = null;
this._search.isFiltered = false;
// Show all rows or restore original
if (this._search.originalData) {
this._restoreOriginalData();
} else {
// Show all rows with pagination
var $rows = this.$('.o_data_row');
var limit = this.state.limit || 80;
$rows.each(function(index) {
$(this).toggle(index < limit);
});
// Restore pager
if (this.state) {
this._updatePager(this.state.count || $rows.length);
}
}
},
/**
* Store original data
*/
_storeOriginalData: function() {
if (this.state) {
this._search.originalData = {
records: this.state.data && this.state.data.records ?
this.state.data.records.slice() : [],
count: this.state.count || 0,
res_ids: this.state.res_ids ? this.state.res_ids.slice() : [],
limit: this.state.limit,
offset: this.state.offset || 0
};
this._search.originalDomain = this.state.domain ? this.state.domain.slice() : [];
}
},
/**
* Restore original data
*/
_restoreOriginalData: function() {
if (!this._search.originalData) return;
var orig = this._search.originalData;
// Restore state
if (this.state) {
if (this.state.data) {
this.state.data.records = orig.records;
}
this.state.count = orig.count;
this.state.res_ids = orig.res_ids;
this.state.limit = orig.limit;
this.state.offset = orig.offset;
this.state.domain = this._search.originalDomain || [];
}
// Show all original rows
var $rows = this.$('.o_data_row');
$rows.show();
// Hide rows beyond limit
var limit = orig.limit || 80;
$rows.each(function(index) {
if (index >= limit) {
$(this).hide();
}
});
// Restore pager
this._updatePager(orig.count);
// Clear stored data
this._search.originalData = null;
this._search.originalDomain = null;
this._search.originalRecords = null;
},
/**
* Restore search input value
*/
_restoreSearchInput: function() {
if (this._search && this._search.value) {
var $input = this.$('.oe_search_input');
if ($input.length && $input.val() !== this._search.value) {
$input.val(this._search.value);
}
this.$('.oe_clear_search').toggle(!!this._search.value);
// Also restore count if we have filtered results
if (this._search.isFiltered && this._search.filteredIds) {
this.$('.oe_search_count')
.text(_t('Found: ') + this._search.filteredIds.length + _t(' records'))
.show();
}
}
},
/**
* Show loading state
* Show loading
*/
_showLoading: function(show) {
this.$('.oe_search_loading').toggle(show);