Ultra-defensive code with complete error handling, fallbacks, and client-side search option

This commit is contained in:
Mohamed Eltayar 2025-08-29 16:55:43 +03:00
parent 14d5cdfcea
commit 3ab8990e62
1 changed files with 235 additions and 164 deletions

View File

@ -16,7 +16,7 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) {
var self = this;
return this._super.apply(this, arguments).then(function () {
self.$('.o_list_table').addClass('o_list_table_ungrouped');
if (self.arch.tag == 'tree' && self.$el.hasClass('o_list_view')) {
if (self.arch && self.arch.tag === 'tree' && self.$el && self.$el.hasClass('o_list_view')) {
// Check if the search input already exists
if (!self.$el.find('.oe_search_input').length) {
var search = '<div class="oe_search_container" style="display: flex; align-items: center; margin: 8px; background-color: #f8f9fa; padding: 10px; border-radius: 4px;">' +
@ -32,11 +32,13 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) {
self._initializeSearch();
}
}
}).catch(function(error) {
console.error('Error in _renderView:', error);
return $.when();
});
},
_initializeSearch: function() {
var self = this;
this._searchTimeout = null;
this._currentSearchValue = '';
this._isSearching = false;
@ -53,7 +55,7 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) {
clearTimeout(this._searchTimeout);
}
// Store current value to prevent clearing
// Store current value
this._currentSearchValue = value;
// Add delay to avoid too many requests
@ -70,7 +72,6 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) {
_performSearch: function (searchValue) {
var self = this;
// Keep original value for search, don't lowercase here
var value = (searchValue || '').trim();
// Prevent concurrent searches
@ -86,13 +87,19 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) {
return;
}
// Check if we have required state
if (!this.state || !this.state.model) {
console.error('Missing state or model information');
this._showError(_t('Search not available in this view'));
return;
}
// Show loading indicator
this.$el.find('.oe_search_loading').show();
this.$el.find('.oe_search_count').hide();
this._isSearching = true;
// Store original records if not already stored
// Check different possible locations for records
var currentRecords = this._getCurrentRecords();
if (!this._originalRecords && currentRecords) {
this._originalRecords = currentRecords.slice();
@ -102,93 +109,151 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) {
var model = this.state.model;
var fields = this._getSearchableFields();
if (fields.length === 0) {
if (!fields || fields.length === 0) {
this._isSearching = false;
this.$el.find('.oe_search_loading').hide();
console.warn('No searchable fields found for model:', model);
this._showError(_t('No searchable fields available'));
// Fallback to client-side search if no searchable fields
this._performClientSideSearch(value);
return;
}
// Build search domain
var domain = this._buildSearchDomain(value, fields);
var searchDomain = this._buildSearchDomain(value, fields);
// Add existing domain if any
var baseDomain = this.state.domain || [];
if (baseDomain.length > 0) {
// Combine base domain with search domain
domain = baseDomain.concat(domain);
if (!searchDomain || searchDomain.length === 0) {
this._isSearching = false;
this.$el.find('.oe_search_loading').hide();
this._showError(_t('Unable to build search query'));
return;
}
// Get only visible and stored fields to avoid computed fields issues
var fieldsToRead = this._getFieldsToRead();
// Get base domain
var baseDomain = [];
if (this.state.domain && Array.isArray(this.state.domain)) {
baseDomain = this.state.domain.slice();
}
// Build order string safely
// Combine domains
var finalDomain = baseDomain.concat(searchDomain);
// Get fields to read
var fieldsToRead = this._getFieldsToRead();
if (!fieldsToRead || fieldsToRead.length === 0) {
fieldsToRead = ['id', 'display_name'];
}
// Build order
var orderBy = this._getOrderBy();
console.log('Search query:', value);
console.log('Searching with domain:', JSON.stringify(domain));
console.log('Fields to read:', fieldsToRead);
console.log('Model:', model);
// Clean context - remove restrictions that might limit search
var searchContext = _.extend({}, this.state.context || session.user_context, {
active_test: false, // Include inactive records
bin_size: true // Optimize binary fields
});
// Perform RPC search with specific fields only - NO LIMIT
console.log('=== Search Debug Info ===');
console.log('Model:', model);
console.log('Search value:', value);
console.log('Search domain:', JSON.stringify(finalDomain));
console.log('Fields to read:', fieldsToRead);
console.log('Context:', searchContext);
// Perform RPC search
this._rpc({
model: model,
method: 'search_read',
args: [domain],
args: [finalDomain],
kwargs: {
fields: fieldsToRead, // Only read necessary fields
limit: false, // No limit - get all records
fields: fieldsToRead,
limit: false,
offset: 0,
context: this.state.context || session.user_context,
context: searchContext,
order: orderBy
}
}).then(function(result) {
console.log('Search results:', result.length, 'records found');
console.log('Search completed. Found:', result.length, 'records');
if (result.length > 0) {
console.log('Sample result:', result[0]);
console.log('First result example:', result[0]);
}
self._updateListWithSearchResults(result, value);
}).catch(function(error) {
console.error('Search error:', error);
self._showError(_t('Search failed. Please try again.'));
console.error('Search RPC error:', error);
self._showError(_t('Search failed. Error: ') + (error.message || error.toString()));
// Fallback to client-side search
console.log('Falling back to client-side search...');
self._performClientSideSearch(value);
}).finally(function() {
self._isSearching = false;
self.$el.find('.oe_search_loading').hide();
// Restore search value
self.$el.find('.oe_search_input').val(self._currentSearchValue);
});
},
_performClientSideSearch: function(value) {
// Fallback: Client-side search in visible records
console.log('Performing client-side search for:', value);
var normalizedValue = this._normalizeArabic(value).toLowerCase();
var visibleCount = 0;
var totalCount = 0;
this.$el.find('.o_data_row').each(function() {
var $row = $(this);
totalCount++;
var rowText = $row.text().toLowerCase();
var normalizedRowText = this._normalizeArabic(rowText).toLowerCase();
var isMatch = rowText.indexOf(value.toLowerCase()) > -1 ||
normalizedRowText.indexOf(normalizedValue) > -1;
$row.toggle(isMatch);
if (isMatch) visibleCount++;
}.bind(this));
var $countEl = this.$el.find('.oe_search_count');
if (visibleCount > 0) {
$countEl.text(_t('Found: ') + visibleCount + '/' + totalCount + _t(' visible records')).show();
} else {
$countEl.text(_t('No matches in visible records')).show();
}
},
_getCurrentRecords: function() {
// Try different locations where records might be stored
if (this.state.data && this.state.data.records) {
if (this.state && this.state.data && this.state.data.records) {
return this.state.data.records;
}
if (this.state.records) {
if (this.state && this.state.records) {
return this.state.records;
}
if (this.recordsData) {
return this.recordsData;
}
if (this.records) {
return this.records;
}
return null;
},
_setCurrentRecords: function(records) {
// Set records in the appropriate location
if (this.state.data) {
if (this.state && this.state.data) {
this.state.data.records = records;
} else if (this.state) {
if (!this.state.data) {
this.state.data = {};
}
this.state.data.records = records;
} else if (this.state.records !== undefined) {
this.state.records = records;
} else {
// Create data structure if it doesn't exist
this.state.data = { records: records };
}
},
_getOrderBy: function() {
var orderBy = false;
if (this.state.orderedBy && this.state.orderedBy.length > 0) {
if (this.state && this.state.orderedBy && Array.isArray(this.state.orderedBy) && this.state.orderedBy.length > 0) {
var order = this.state.orderedBy[0];
if (order && order.name) {
orderBy = order.name;
@ -202,34 +267,42 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) {
_getFieldsToRead: function() {
var self = this;
var fields = ['id']; // Always include ID
var fields = ['id'];
// Get only visible, stored fields from columns
// Check if columns exist
if (!this.columns || !Array.isArray(this.columns)) {
console.warn('No columns found in view');
return ['id', 'display_name', 'name'];
}
// Check if state.fields exists
if (!this.state || !this.state.fields) {
console.warn('No fields metadata found');
return ['id', 'display_name', 'name'];
}
// Get visible fields from columns
_.each(this.columns, function(column) {
if (!column.invisible && column.attrs && column.attrs.name) {
if (column && !column.invisible && column.attrs && column.attrs.name) {
var fieldName = column.attrs.name;
var field = self.state.fields[fieldName];
// Only include stored fields or safe field types
if (field) {
// Skip computed fields unless they're stored
// Skip computed non-stored fields
if (field.compute && !field.store) {
console.log('Skipping computed field:', fieldName);
return; // Skip this field
console.log('Skipping computed non-stored field:', fieldName);
return;
}
// Include all other fields
fields.push(fieldName);
}
}
});
// Always try to include display_name if available
// Add essential fields if available
if (self.state.fields.display_name && !fields.includes('display_name')) {
fields.push('display_name');
}
// Always try to include name if available
if (self.state.fields.name && !fields.includes('name')) {
fields.push('name');
}
@ -238,59 +311,60 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) {
},
_buildSearchDomain: function(value, fields) {
if (!value || !fields || fields.length === 0) {
return [];
}
var domain = [];
var orConditions = [];
// Normalize search value for Arabic
// Normalize for Arabic
var normalizedValue = this._normalizeArabic(value);
_.each(fields, function(field) {
// For char, text, html, and selection fields
if (!field || !field.name || !field.type) {
return;
}
// Text fields
if (['char', 'text', 'html', 'selection'].includes(field.type)) {
// Use ilike for case-insensitive search
orConditions.push([field.name, 'ilike', value]);
// Also search with normalized value if different (for Arabic)
if (normalizedValue !== value && normalizedValue) {
if (normalizedValue && normalizedValue !== value) {
orConditions.push([field.name, 'ilike', normalizedValue]);
}
}
// For many2one fields - search in the display_name
// Many2one fields - special handling
else if (field.type === 'many2one') {
// Many2one search needs special handling
// Try searching by ID if value is numeric
// Search by ID if numeric
if (!isNaN(value)) {
orConditions.push([field.name, '=', parseInt(value)]);
}
// Also try text search in the related field's name
orConditions.push([field.name, 'ilike', value]);
// Note: searching text in many2one requires special syntax
// We'll search by ID only for now
}
// For number fields (if value is numeric)
// Number fields
else if (['integer', 'float', 'monetary'].includes(field.type)) {
if (!isNaN(value) && value !== '') {
var numValue = parseFloat(value);
// Exact match for integers
if (field.type === 'integer') {
orConditions.push([field.name, '=', parseInt(value)]);
} else {
// For float/monetary, exact match
orConditions.push([field.name, '=', numValue]);
orConditions.push([field.name, '=', parseFloat(value)]);
}
}
}
// For boolean fields
// Boolean fields
else if (field.type === 'boolean') {
var boolValue = value.toLowerCase();
if (boolValue === 'true' || boolValue === 'yes' || boolValue === '1' || boolValue === 'نعم') {
if (['true', 'yes', '1', 'نعم', 'صح'].includes(boolValue)) {
orConditions.push([field.name, '=', true]);
} else if (boolValue === 'false' || boolValue === 'no' || boolValue === '0' || boolValue === 'لا') {
} else if (['false', 'no', '0', 'لا', 'خطأ'].includes(boolValue)) {
orConditions.push([field.name, '=', false]);
}
}
});
// Create OR domain
// Build OR domain
if (orConditions.length > 0) {
// Add '|' operators for OR condition
for (var i = 1; i < orConditions.length; i++) {
domain.push('|');
}
@ -303,43 +377,49 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) {
_getSearchableFields: function() {
var self = this;
var fields = [];
var addedFields = {}; // Track added fields to avoid duplicates
var addedFields = {};
// Priority 1: Get visible stored fields from columns
_.each(this.columns, function(column) {
if (!column.invisible && column.attrs && column.attrs.name) {
var fieldName = column.attrs.name;
var field = self.state.fields[fieldName];
if (field && !addedFields[fieldName]) {
// Only searchable in stored fields or specific field types
var isSearchable = (
field.searchable !== false &&
(field.store !== false || field.type === 'many2one') &&
(!field.compute || field.store) // Skip non-stored computed fields
);
// Check requirements
if (!this.state || !this.state.fields) {
console.warn('No state.fields available');
return [];
}
// Get fields from columns if available
if (this.columns && Array.isArray(this.columns)) {
_.each(this.columns, function(column) {
if (column && !column.invisible && column.attrs && column.attrs.name) {
var fieldName = column.attrs.name;
var field = self.state.fields[fieldName];
if (isSearchable) {
fields.push({
name: fieldName,
type: field.type,
string: field.string || fieldName
});
addedFields[fieldName] = true;
if (field && !addedFields[fieldName]) {
// Check if searchable
var isSearchable = (
field.searchable !== false &&
(field.store !== false || field.type === 'many2one') &&
(!field.compute || field.store)
);
if (isSearchable) {
fields.push({
name: fieldName,
type: field.type,
string: field.string || fieldName
});
addedFields[fieldName] = true;
}
}
}
}
});
});
}
// Priority 2: Add common searchable fields if not enough fields
// Add common fields if not enough
if (fields.length < 3) {
var commonFields = ['name', 'display_name', 'reference', 'code', 'ref', 'description'];
_.each(commonFields, function(fieldName) {
if (self.state.fields[fieldName] && !addedFields[fieldName]) {
var field = self.state.fields[fieldName];
if (field.searchable !== false &&
field.store !== false &&
(!field.compute || field.store)) {
var field = self.state.fields[fieldName];
if (field && !addedFields[fieldName]) {
if (field.searchable !== false && field.store !== false && (!field.compute || field.store)) {
fields.push({
name: fieldName,
type: field.type,
@ -351,75 +431,73 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) {
});
}
console.log('Searchable fields:', fields);
console.log('Searchable fields found:', fields.length, 'fields');
return fields;
},
_updateListWithSearchResults: function(records, searchValue) {
var self = this;
// Update the count display
if (!records) {
records = [];
}
var count = records.length;
var $countEl = this.$el.find('.oe_search_count');
if (count > 0) {
var message = _t('Found: ') + count + _t(' records');
// Show warning if too many records
if (count > 1000) {
message += ' ' + _t('(Large result set, may affect performance)');
message += ' ' + _t('(Large result set)');
}
$countEl.text(message).show();
// Update the view with new records
this._setCurrentRecords(records);
this._searchMode = true;
// Re-render the body with new records
// Wrap in try-catch and handle both Promise and non-Promise returns
try {
var renderResult = this._renderBody();
if (renderResult && typeof renderResult.then === 'function') {
renderResult.then(function() {
// Restore search input value
self.$el.find('.oe_search_input').val(self._currentSearchValue);
self.$el.find('.oe_clear_search').show();
$countEl.show();
}).catch(function(error) {
console.error('Error rendering search results:', error);
self._showError(_t('Error displaying results'));
});
} else {
// If _renderBody doesn't return a promise, just update UI
self.$el.find('.oe_search_input').val(self._currentSearchValue);
self.$el.find('.oe_clear_search').show();
$countEl.show();
// Try to render if method exists
if (typeof this._renderBody === 'function') {
try {
var renderResult = this._renderBody();
if (renderResult && typeof renderResult.then === 'function') {
renderResult.then(function() {
self._restoreSearchUI();
}).catch(function(error) {
console.error('Render error:', error);
self._restoreSearchUI();
});
} else {
self._restoreSearchUI();
}
} catch (error) {
console.error('Render error:', error);
self._restoreSearchUI();
}
} catch (error) {
console.error('Error calling _renderBody:', error);
self._showError(_t('Error displaying results'));
} else {
console.warn('_renderBody method not found');
self._restoreSearchUI();
}
} else {
$countEl.text(_t('No records found')).show();
this._showNoResults(searchValue);
}
},
_restoreSearchUI: function() {
this.$el.find('.oe_search_input').val(this._currentSearchValue);
this.$el.find('.oe_clear_search').show();
},
_showNoResults: function(searchValue) {
// Hide all data rows
this.$el.find('.o_data_row').hide();
// Remove existing no results message
this.$el.find('.oe_no_results').remove();
// Add no results message
var colspan = this.$el.find('thead tr:first th').length || 1;
var noResultsHtml = '<tr class="oe_no_results">' +
'<td colspan="' + colspan + '" class="text-center text-muted" style="padding: 40px;">' +
'<i class="fa fa-search" style="font-size: 3em; margin-bottom: 10px; display: block; opacity: 0.3;"></i>' +
'<h4>' + _t('No records found') + '</h4>' +
'<p>' + _t('No results for: ') + '<strong>' + _.escape(searchValue) + '</strong></p>' +
'<p class="text-muted">' + _t('Try different keywords or check your filters') + '</p>' +
'<p>' + _t('Search term: ') + '<strong>' + _.escape(searchValue) + '</strong></p>' +
'</td></tr>';
this.$el.find('tbody').append(noResultsHtml);
},
@ -436,69 +514,62 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) {
_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();
this.$el.find('.oe_search_loading').hide();
// Remove no results message
this.$el.find('.oe_no_results').remove();
// Reset search state
this._currentSearchValue = '';
this._searchMode = false;
// If we have original records, restore them
// Restore original records
if (this._originalRecords && this._originalRecords.length > 0) {
this._setCurrentRecords(this._originalRecords);
// Wrap _renderBody in try-catch for safety
try {
var renderResult = this._renderBody();
if (renderResult && typeof renderResult.then === 'function') {
renderResult.then(function() {
self._originalRecords = null;
}).catch(function(error) {
console.error('Error restoring original records:', error);
// Just show all rows as fallback
if (typeof this._renderBody === 'function') {
try {
var renderResult = this._renderBody();
if (renderResult && typeof renderResult.then === 'function') {
renderResult.then(function() {
self._originalRecords = null;
}).catch(function(error) {
console.error('Clear search render error:', error);
self.$el.find('.o_data_row').show();
self._originalRecords = null;
});
} else {
self.$el.find('.o_data_row').show();
self._originalRecords = null;
});
} else {
// If _renderBody doesn't return a promise
}
} catch (error) {
console.error('Clear search error:', error);
self.$el.find('.o_data_row').show();
self._originalRecords = null;
}
} catch (error) {
console.error('Error calling _renderBody:', error);
// Just show all rows as fallback
} else {
self.$el.find('.o_data_row').show();
self._originalRecords = null;
}
} else {
// Just show all rows
this.$el.find('.o_data_row').show();
}
},
_normalizeArabic: function(text) {
if (!text) return text;
if (!text || typeof text !== 'string') return '';
// Normalizing Arabic text by removing common variations
// Don't lowercase here as it's for the search query
return text
.replace(/[\u064B-\u065F]/g, '') // Remove diacritics
.replace(/[\u0660-\u0669]/g, (d) => String.fromCharCode(d.charCodeAt(0) - 0x0660 + 0x0030)) // Arabic-Indic digits
.replace(/[\u06F0-\u06F9]/g, (d) => String.fromCharCode(d.charCodeAt(0) - 0x06F0 + 0x0030)) // Extended Arabic-Indic
.replace(/[\u0622\u0623\u0625\u0627]/g, 'ا') // Normalize Alef
.replace(/[\u0629]/g, 'ه') // Teh Marbuta to Heh
.replace(/[\u064A\u0626\u0649]/g, 'ي') // Normalize Yeh
.replace(/[\u0624\u0648]/g, 'و'); // Normalize Waw
.replace(/[\u0660-\u0669]/g, (d) => String.fromCharCode(d.charCodeAt(0) - 0x0660 + 0x0030))
.replace(/[\u06F0-\u06F9]/g, (d) => String.fromCharCode(d.charCodeAt(0) - 0x06F0 + 0x0030))
.replace(/[\u0622\u0623\u0625\u0627]/g, 'ا')
.replace(/[\u0629]/g, 'ه')
.replace(/[\u064A\u0626\u0649]/g, 'ي')
.replace(/[\u0624\u0648]/g, 'و');
},
destroy: function () {
// Clear timeout if exists
if (this._searchTimeout) {
clearTimeout(this._searchTimeout);
}