تحسين وإصلاح موديول البحث العام في List Views - نسخة محسنة بالكامل
التحسينات والإصلاحات: - إصلاح استخدام reload method بالطريقة الصحيحة لـ Odoo 14 - تحسين معالجة حقول Many2one للبحث الصحيح - إضافة دعم أفضل للحصول على الحقول المرئية من fieldsInfo - تحسين الأداء مع debouncing محسن (300ms) - إضافة معالجة أفضل للأخطاء والحالات الاستثنائية - دعم محسن للغة العربية مع تطبيع شامل للنصوص - إضافة دعم مفاتيح Enter و Escape للتحكم السريع - تحسين واجهة المستخدم مع عرض حالة التحميل - إصلاح مشاكل التزامن والبحث المتكرر - معالجة صحيحة للـ domains المعقدة
This commit is contained in:
parent
7c1890e349
commit
349b144467
|
|
@ -7,9 +7,10 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) {
|
||||||
var _t = core._t;
|
var _t = core._t;
|
||||||
var concurrency = require('web.concurrency');
|
var concurrency = require('web.concurrency');
|
||||||
var rpc = require('web.rpc');
|
var rpc = require('web.rpc');
|
||||||
|
var session = require('web.session');
|
||||||
|
|
||||||
// ================================
|
// ================================
|
||||||
// CORRECT APPROACH: Use ListController for data operations
|
// ListController: Data operations
|
||||||
// ================================
|
// ================================
|
||||||
ListController.include({
|
ListController.include({
|
||||||
|
|
||||||
|
|
@ -18,96 +19,171 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) {
|
||||||
*/
|
*/
|
||||||
init: function () {
|
init: function () {
|
||||||
this._super.apply(this, arguments);
|
this._super.apply(this, arguments);
|
||||||
// Initialize search state in controller (correct place)
|
// Initialize search state
|
||||||
this._customSearchState = {
|
this._customSearchState = {
|
||||||
timer: null,
|
timer: null,
|
||||||
value: '',
|
value: '',
|
||||||
isFiltered: false,
|
isFiltered: false,
|
||||||
filteredCount: 0,
|
filteredCount: 0,
|
||||||
originalDomain: null,
|
originalDomain: null,
|
||||||
searchInProgress: false
|
searchInProgress: false,
|
||||||
|
lastSearchPromise: null
|
||||||
};
|
};
|
||||||
this._searchMutex = new concurrency.Mutex();
|
this._searchMutex = new concurrency.Mutex();
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CORRECT: Handle custom search from controller
|
* @override - Hook into rendering complete
|
||||||
|
*/
|
||||||
|
renderButtons: function () {
|
||||||
|
this._super.apply(this, arguments);
|
||||||
|
// Setup search handler after buttons are rendered
|
||||||
|
this._setupCustomSearchHandler();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup custom search handler
|
||||||
|
*/
|
||||||
|
_setupCustomSearchHandler: function() {
|
||||||
|
var self = this;
|
||||||
|
// Ensure renderer is ready
|
||||||
|
if (this.renderer && this.renderer._customSearchReady) {
|
||||||
|
console.log('Custom search handler already setup');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle custom search from controller
|
||||||
*/
|
*/
|
||||||
_handleCustomSearch: function(value) {
|
_handleCustomSearch: function(value) {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
if (self._customSearchState.searchInProgress) {
|
// Cancel any pending search
|
||||||
console.log('Search already in progress, ignoring');
|
if (self._customSearchState.timer) {
|
||||||
return Promise.resolve();
|
clearTimeout(self._customSearchState.timer);
|
||||||
|
self._customSearchState.timer = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('=== CONTROLLER SEARCH ===');
|
// Debounce search
|
||||||
console.log('Search value:', value);
|
return new Promise(function(resolve) {
|
||||||
|
self._customSearchState.timer = setTimeout(function() {
|
||||||
return this._searchMutex.exec(function() {
|
self._executeCustomSearch(value).then(resolve);
|
||||||
self._customSearchState.searchInProgress = true;
|
}, 300);
|
||||||
|
|
||||||
if (!value || value.length === 0) {
|
|
||||||
console.log('Empty search - clearing filters');
|
|
||||||
return self._clearCustomSearch().finally(function() {
|
|
||||||
self._customSearchState.searchInProgress = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store original domain
|
|
||||||
if (!self._customSearchState.originalDomain && !self._customSearchState.isFiltered) {
|
|
||||||
self._customSearchState.originalDomain = self.model.get(self.handle).domain.slice();
|
|
||||||
console.log('Stored original domain:', self._customSearchState.originalDomain);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build and apply search domain
|
|
||||||
return self._applyCustomSearch(value).finally(function() {
|
|
||||||
self._customSearchState.searchInProgress = false;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CORRECT: Apply search using controller's reload method
|
* Execute the search
|
||||||
|
*/
|
||||||
|
_executeCustomSearch: function(value) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
if (self._customSearchState.searchInProgress) {
|
||||||
|
console.log('Search already in progress, queueing');
|
||||||
|
return self._customSearchState.lastSearchPromise.then(function() {
|
||||||
|
return self._executeCustomSearch(value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('=== EXECUTING CUSTOM SEARCH ===');
|
||||||
|
console.log('Search value:', value);
|
||||||
|
|
||||||
|
var searchPromise = this._searchMutex.exec(function() {
|
||||||
|
self._customSearchState.searchInProgress = true;
|
||||||
|
|
||||||
|
if (!value || value.length === 0) {
|
||||||
|
console.log('Empty search - clearing filters');
|
||||||
|
return self._clearCustomSearch();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store original domain on first search
|
||||||
|
if (!self._customSearchState.originalDomain && !self._customSearchState.isFiltered) {
|
||||||
|
var currentState = self.model.get(self.handle);
|
||||||
|
if (currentState && currentState.domain) {
|
||||||
|
self._customSearchState.originalDomain = JSON.parse(JSON.stringify(currentState.domain));
|
||||||
|
console.log('Stored original domain:', self._customSearchState.originalDomain);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return self._applyCustomSearch(value);
|
||||||
|
}).finally(function() {
|
||||||
|
self._customSearchState.searchInProgress = false;
|
||||||
|
self._customSearchState.lastSearchPromise = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
self._customSearchState.lastSearchPromise = searchPromise;
|
||||||
|
return searchPromise;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply search using proper Odoo 14 method
|
||||||
*/
|
*/
|
||||||
_applyCustomSearch: function(value) {
|
_applyCustomSearch: function(value) {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
// Build search domain
|
// Build search domain
|
||||||
var searchDomain = this._buildCustomSearchDomain(value);
|
var searchDomain = this._buildCustomSearchDomain(value);
|
||||||
if (searchDomain.length === 0) {
|
if (!searchDomain || searchDomain.length === 0) {
|
||||||
console.warn('No searchable fields found');
|
console.warn('No searchable fields found or no valid search domain');
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Combine with original domain
|
// Combine with original domain
|
||||||
var finalDomain = this._combineCustomDomains(searchDomain);
|
var finalDomain = this._combineCustomDomains(searchDomain);
|
||||||
|
|
||||||
console.log('Final search domain:', finalDomain);
|
console.log('Search domain:', searchDomain);
|
||||||
|
console.log('Final domain:', finalDomain);
|
||||||
|
|
||||||
// Step 1: Get count
|
// Show loading state
|
||||||
|
if (self.renderer) {
|
||||||
|
self.renderer.$('.oe_search_loading').show();
|
||||||
|
self.renderer.$('.oe_search_count').hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
// First get the count
|
||||||
return rpc.query({
|
return rpc.query({
|
||||||
model: this.modelName,
|
model: this.modelName,
|
||||||
method: 'search_count',
|
method: 'search_count',
|
||||||
args: [finalDomain],
|
args: [finalDomain],
|
||||||
context: this.model.get(this.handle).getContext()
|
context: session.user_context
|
||||||
}).then(function(count) {
|
}).then(function(count) {
|
||||||
console.log('Search count:', count);
|
console.log('Found records:', count);
|
||||||
|
|
||||||
// Update UI
|
// Update UI with count
|
||||||
self._customSearchState.filteredCount = count;
|
self._customSearchState.filteredCount = count;
|
||||||
self._updateCustomSearchUI(count);
|
self._updateCustomSearchUI(count);
|
||||||
|
|
||||||
// Step 2: CORRECT WAY - Use controller's reload method
|
// Now reload the view with new domain
|
||||||
return self.reload({domain: finalDomain});
|
// CRITICAL FIX: Use proper reload method for Odoo 14
|
||||||
|
var handle = self.handle;
|
||||||
|
var state = self.model.get(handle);
|
||||||
|
|
||||||
|
// Update the domain in the state
|
||||||
|
return self.model.reload(handle, {
|
||||||
|
domain: finalDomain,
|
||||||
|
offset: 0, // Reset to first page
|
||||||
|
limit: state.limit || 80
|
||||||
|
});
|
||||||
}).then(function() {
|
}).then(function() {
|
||||||
|
// Update the view
|
||||||
self._customSearchState.isFiltered = true;
|
self._customSearchState.isFiltered = true;
|
||||||
self._customSearchState.value = value;
|
self._customSearchState.value = value;
|
||||||
|
|
||||||
|
// Trigger update to renderer
|
||||||
|
return self.update({}, {reload: false});
|
||||||
|
}).then(function() {
|
||||||
console.log('Search applied successfully');
|
console.log('Search applied successfully');
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}).catch(function(error) {
|
}).catch(function(error) {
|
||||||
console.error('Search error:', error);
|
console.error('Search error:', error);
|
||||||
|
self._showSearchError();
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
|
}).finally(function() {
|
||||||
|
// Hide loading
|
||||||
|
if (self.renderer) {
|
||||||
|
self.renderer.$('.oe_search_loading').hide();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -119,62 +195,121 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) {
|
||||||
|
|
||||||
console.log('=== CLEARING CUSTOM SEARCH ===');
|
console.log('=== CLEARING CUSTOM SEARCH ===');
|
||||||
|
|
||||||
// Clear UI
|
// Clear UI immediately
|
||||||
this.renderer.$('.oe_search_input').val('');
|
if (this.renderer && this.renderer.$) {
|
||||||
this.renderer.$('.oe_clear_search').hide();
|
this.renderer.$('.oe_search_input').val('');
|
||||||
this.renderer.$('.oe_search_count').hide();
|
this.renderer.$('.oe_clear_search').hide();
|
||||||
|
this.renderer.$('.oe_search_count').hide();
|
||||||
|
this.renderer.$('.oe_search_loading').show();
|
||||||
|
}
|
||||||
|
|
||||||
// Reset state
|
// Reset state
|
||||||
this._customSearchState.value = '';
|
this._customSearchState.value = '';
|
||||||
this._customSearchState.isFiltered = false;
|
this._customSearchState.isFiltered = false;
|
||||||
this._customSearchState.filteredCount = 0;
|
this._customSearchState.filteredCount = 0;
|
||||||
|
|
||||||
// Restore original domain
|
// Get original domain
|
||||||
var originalDomain = this._customSearchState.originalDomain || [];
|
var originalDomain = this._customSearchState.originalDomain || [];
|
||||||
this._customSearchState.originalDomain = null;
|
this._customSearchState.originalDomain = null;
|
||||||
|
|
||||||
// CORRECT: Use controller's reload with original domain
|
console.log('Restoring original domain:', originalDomain);
|
||||||
return this.reload({domain: originalDomain});
|
|
||||||
|
// Reload with original domain
|
||||||
|
var handle = this.handle;
|
||||||
|
var state = this.model.get(handle);
|
||||||
|
|
||||||
|
return this.model.reload(handle, {
|
||||||
|
domain: originalDomain,
|
||||||
|
offset: 0,
|
||||||
|
limit: state.limit || 80
|
||||||
|
}).then(function() {
|
||||||
|
return self.update({}, {reload: false});
|
||||||
|
}).finally(function() {
|
||||||
|
if (self.renderer) {
|
||||||
|
self.renderer.$('.oe_search_loading').hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build search domain
|
* Build search domain - FIXED VERSION
|
||||||
*/
|
*/
|
||||||
_buildCustomSearchDomain: function(value) {
|
_buildCustomSearchDomain: function(value) {
|
||||||
|
if (!value) return [];
|
||||||
|
|
||||||
var fields = this._getCustomSearchableFields();
|
var fields = this._getCustomSearchableFields();
|
||||||
if (fields.length === 0) {
|
if (!fields || fields.length === 0) {
|
||||||
|
console.warn('No searchable fields found');
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
var conditions = [];
|
var conditions = [];
|
||||||
var normalized = this._normalizeArabic(value);
|
var normalized = this._normalizeArabic(value);
|
||||||
|
var searchValues = [value];
|
||||||
|
if (normalized !== value) {
|
||||||
|
searchValues.push(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Building domain for fields:', fields);
|
||||||
|
|
||||||
fields.forEach(function(field) {
|
fields.forEach(function(field) {
|
||||||
try {
|
try {
|
||||||
if (['char', 'text', 'html'].includes(field.type)) {
|
var fieldType = field.type;
|
||||||
conditions.push([field.name, 'ilike', value]);
|
var fieldName = field.name;
|
||||||
if (normalized !== value) {
|
|
||||||
conditions.push([field.name, 'ilike', normalized]);
|
switch(fieldType) {
|
||||||
}
|
case 'char':
|
||||||
} else if (['integer', 'float', 'monetary'].includes(field.type)) {
|
case 'text':
|
||||||
var num = parseFloat(value);
|
case 'html':
|
||||||
if (!isNaN(num)) {
|
searchValues.forEach(function(searchVal) {
|
||||||
conditions.push([field.name, '=', num]);
|
conditions.push([fieldName, 'ilike', searchVal]);
|
||||||
}
|
});
|
||||||
} else if (field.type === 'many2one') {
|
break;
|
||||||
conditions.push([field.name + '.name', 'ilike', value]);
|
|
||||||
if (normalized !== value) {
|
case 'integer':
|
||||||
conditions.push([field.name + '.name', 'ilike', normalized]);
|
case 'float':
|
||||||
}
|
case 'monetary':
|
||||||
} else if (field.type === 'selection') {
|
var numValue = parseFloat(value);
|
||||||
conditions.push([field.name, 'ilike', value]);
|
if (!isNaN(numValue)) {
|
||||||
} else if (field.type === 'boolean') {
|
// For numeric fields, also search as string
|
||||||
var lowerValue = value.toLowerCase();
|
conditions.push([fieldName, '=', numValue]);
|
||||||
if (['true', 'yes', 'نعم', '1'].includes(lowerValue)) {
|
// Also search string representation
|
||||||
conditions.push([field.name, '=', true]);
|
conditions.push([fieldName, 'ilike', value]);
|
||||||
} else if (['false', 'no', 'لا', '0'].includes(lowerValue)) {
|
}
|
||||||
conditions.push([field.name, '=', false]);
|
break;
|
||||||
}
|
|
||||||
|
case 'many2one':
|
||||||
|
// CRITICAL FIX: For many2one, search on display_name
|
||||||
|
searchValues.forEach(function(searchVal) {
|
||||||
|
// Direct search on the field (searches display_name by default)
|
||||||
|
conditions.push([fieldName, 'ilike', searchVal]);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'selection':
|
||||||
|
searchValues.forEach(function(searchVal) {
|
||||||
|
conditions.push([fieldName, 'ilike', searchVal]);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'boolean':
|
||||||
|
var lowerValue = value.toLowerCase().trim();
|
||||||
|
var booleanMappings = {
|
||||||
|
'true': true, 'yes': true, 'نعم': true, '1': true, 'صح': true,
|
||||||
|
'false': false, 'no': false, 'لا': false, '0': false, 'خطأ': false
|
||||||
|
};
|
||||||
|
if (lowerValue in booleanMappings) {
|
||||||
|
conditions.push([fieldName, '=', booleanMappings[lowerValue]]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'date':
|
||||||
|
case 'datetime':
|
||||||
|
// Try to parse as date
|
||||||
|
if (value.match(/^\d{4}-\d{2}-\d{2}/)) {
|
||||||
|
conditions.push([fieldName, 'ilike', value]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Error processing field', field.name, ':', error);
|
console.warn('Error processing field', field.name, ':', error);
|
||||||
|
|
@ -185,28 +320,54 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build proper OR domain
|
||||||
if (conditions.length === 1) {
|
if (conditions.length === 1) {
|
||||||
return conditions;
|
return conditions[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build OR domain
|
// Create OR domain: ['|', ['field1', 'op', 'val'], ['field2', 'op', 'val']]
|
||||||
var domain = [];
|
var orDomain = [];
|
||||||
for (var i = 1; i < conditions.length; i++) {
|
for (var i = 1; i < conditions.length; i++) {
|
||||||
domain.push('|');
|
orDomain.push('|');
|
||||||
}
|
}
|
||||||
return domain.concat(conditions);
|
return orDomain.concat(conditions);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get searchable fields
|
* Get searchable fields - ENHANCED VERSION
|
||||||
*/
|
*/
|
||||||
_getCustomSearchableFields: function() {
|
_getCustomSearchableFields: function() {
|
||||||
var fields = [];
|
var fields = [];
|
||||||
var state = this.model.get(this.handle);
|
var state = this.model.get(this.handle);
|
||||||
var fieldDefs = state.fields || {};
|
|
||||||
|
|
||||||
// Get from renderer columns
|
if (!state) {
|
||||||
if (this.renderer.columns) {
|
console.warn('No state available');
|
||||||
|
return fields;
|
||||||
|
}
|
||||||
|
|
||||||
|
var fieldDefs = state.fields || {};
|
||||||
|
var fieldsInfo = state.fieldsInfo;
|
||||||
|
var viewType = state.viewType || 'list';
|
||||||
|
|
||||||
|
// Method 1: Get from fieldsInfo (most reliable)
|
||||||
|
if (fieldsInfo && fieldsInfo[viewType]) {
|
||||||
|
Object.keys(fieldsInfo[viewType]).forEach(function(fieldName) {
|
||||||
|
var fieldInfo = fieldsInfo[viewType][fieldName];
|
||||||
|
if (fieldInfo && !fieldInfo.invisible && fieldDefs[fieldName]) {
|
||||||
|
var fieldDef = fieldDefs[fieldName];
|
||||||
|
if (fieldDef.store !== false && fieldDef.searchable !== false) {
|
||||||
|
fields.push({
|
||||||
|
name: fieldName,
|
||||||
|
type: fieldDef.type,
|
||||||
|
string: fieldDef.string || fieldName
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method 2: Get from renderer columns if available
|
||||||
|
if (fields.length === 0 && this.renderer && this.renderer.columns) {
|
||||||
this.renderer.columns.forEach(function(col) {
|
this.renderer.columns.forEach(function(col) {
|
||||||
if (!col.invisible && col.attrs && col.attrs.name) {
|
if (!col.invisible && col.attrs && col.attrs.name) {
|
||||||
var fieldName = col.attrs.name;
|
var fieldName = col.attrs.name;
|
||||||
|
|
@ -214,43 +375,53 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) {
|
||||||
if (field && field.store !== false) {
|
if (field && field.store !== false) {
|
||||||
fields.push({
|
fields.push({
|
||||||
name: fieldName,
|
name: fieldName,
|
||||||
type: field.type
|
type: field.type,
|
||||||
|
string: field.string || fieldName
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback fields
|
// Method 3: Fallback to common searchable fields
|
||||||
if (fields.length === 0) {
|
if (fields.length === 0) {
|
||||||
var commonFields = ['name', 'display_name', 'code', 'reference'];
|
var commonFields = ['name', 'display_name', 'code', 'reference', 'ref', 'description'];
|
||||||
commonFields.forEach(function(fname) {
|
commonFields.forEach(function(fname) {
|
||||||
if (fieldDefs[fname] && fieldDefs[fname].store !== false) {
|
if (fieldDefs[fname] && fieldDefs[fname].store !== false) {
|
||||||
fields.push({
|
fields.push({
|
||||||
name: fname,
|
name: fname,
|
||||||
type: fieldDefs[fname].type
|
type: fieldDefs[fname].type,
|
||||||
|
string: fieldDefs[fname].string || fname
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Searchable fields:', fields);
|
console.log('Searchable fields found:', fields);
|
||||||
return fields;
|
return fields;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Combine domains
|
* Combine domains - FIXED VERSION
|
||||||
*/
|
*/
|
||||||
_combineCustomDomains: function(searchDomain) {
|
_combineCustomDomains: function(searchDomain) {
|
||||||
var originalDomain = this._customSearchState.originalDomain || [];
|
var originalDomain = this._customSearchState.originalDomain || [];
|
||||||
|
|
||||||
if (originalDomain.length > 0 && searchDomain.length > 0) {
|
// Handle empty domains
|
||||||
return ['&'].concat(originalDomain).concat(searchDomain);
|
if (!searchDomain || searchDomain.length === 0) {
|
||||||
} else if (searchDomain.length > 0) {
|
|
||||||
return searchDomain;
|
|
||||||
} else {
|
|
||||||
return originalDomain;
|
return originalDomain;
|
||||||
}
|
}
|
||||||
|
if (!originalDomain || originalDomain.length === 0) {
|
||||||
|
return searchDomain;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combine with AND operator
|
||||||
|
// Ensure we're working with arrays
|
||||||
|
var origArray = Array.isArray(originalDomain) ? originalDomain : [originalDomain];
|
||||||
|
var searchArray = Array.isArray(searchDomain) ? searchDomain : [searchDomain];
|
||||||
|
|
||||||
|
// Create combined domain: ['&', original_domain, search_domain]
|
||||||
|
return ['&'].concat(origArray).concat(searchArray);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -258,41 +429,76 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) {
|
||||||
*/
|
*/
|
||||||
_updateCustomSearchUI: function(count) {
|
_updateCustomSearchUI: function(count) {
|
||||||
if (this.renderer && this.renderer.$) {
|
if (this.renderer && this.renderer.$) {
|
||||||
this.renderer.$('.oe_search_count').text(_t('Found: ') + count + _t(' records')).show();
|
var message = _t('Found: ') + count + _t(' records');
|
||||||
|
this.renderer.$('.oe_search_count')
|
||||||
|
.text(message)
|
||||||
|
.show();
|
||||||
|
this.renderer.$('.oe_clear_search').show();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Normalize Arabic text
|
* Show search error
|
||||||
|
*/
|
||||||
|
_showSearchError: function() {
|
||||||
|
if (this.renderer && this.renderer.$) {
|
||||||
|
this.renderer.$('.oe_search_count')
|
||||||
|
.text(_t('Search error occurred'))
|
||||||
|
.addClass('text-danger')
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize Arabic text - ENHANCED VERSION
|
||||||
*/
|
*/
|
||||||
_normalizeArabic: function(text) {
|
_normalizeArabic: function(text) {
|
||||||
if (!text) return '';
|
if (!text) return '';
|
||||||
|
|
||||||
return text
|
return text
|
||||||
|
// Remove Arabic diacritics
|
||||||
.replace(/[\u064B-\u065F]/g, '')
|
.replace(/[\u064B-\u065F]/g, '')
|
||||||
.replace(/[\u0660-\u0669]/g, function(d) {
|
// Convert Arabic-Indic digits to Western digits
|
||||||
|
.replace(/[٠-٩]/g, function(d) {
|
||||||
return String.fromCharCode(d.charCodeAt(0) - 0x0660 + 0x0030);
|
return String.fromCharCode(d.charCodeAt(0) - 0x0660 + 0x0030);
|
||||||
})
|
})
|
||||||
.replace(/[\u06F0-\u06F9]/g, function(d) {
|
// Convert Persian digits to Western digits
|
||||||
|
.replace(/[۰-۹]/g, function(d) {
|
||||||
return String.fromCharCode(d.charCodeAt(0) - 0x06F0 + 0x0030);
|
return String.fromCharCode(d.charCodeAt(0) - 0x06F0 + 0x0030);
|
||||||
})
|
})
|
||||||
.replace(/[\u0622\u0623\u0625\u0627]/g, 'ا')
|
// Normalize Alef variations
|
||||||
.replace(/[\u0629]/g, 'ه')
|
.replace(/[آأإا]/g, 'ا')
|
||||||
.replace(/[\u064A\u0626\u0649]/g, 'ي')
|
// Normalize Teh Marbuta
|
||||||
.replace(/[\u0624\u0648]/g, 'و');
|
.replace(/ة/g, 'ه')
|
||||||
|
// Normalize Yeh variations
|
||||||
|
.replace(/[يئى]/g, 'ي')
|
||||||
|
// Normalize Waw variations
|
||||||
|
.replace(/[ؤو]/g, 'و')
|
||||||
|
// Trim spaces
|
||||||
|
.trim();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ================================
|
// ================================
|
||||||
// ListRenderer: Only for UI (correct approach)
|
// ListRenderer: UI only
|
||||||
// ================================
|
// ================================
|
||||||
ListRenderer.include({
|
ListRenderer.include({
|
||||||
events: _.extend({}, ListRenderer.prototype.events, {
|
events: _.extend({}, ListRenderer.prototype.events, {
|
||||||
'keyup .oe_search_input': '_onCustomSearchKeyUp',
|
'keyup .oe_search_input': '_onCustomSearchKeyUp',
|
||||||
'input .oe_search_input': '_onCustomSearchInput',
|
'input .oe_search_input': '_onCustomSearchInput',
|
||||||
'click .oe_clear_search': '_onCustomClearSearch'
|
'click .oe_clear_search': '_onCustomClearSearch',
|
||||||
|
'keydown .oe_search_input': '_onCustomSearchKeyDown'
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @override
|
||||||
|
*/
|
||||||
|
init: function() {
|
||||||
|
this._super.apply(this, arguments);
|
||||||
|
this._searchTimer = null;
|
||||||
|
this._customSearchReady = false;
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @override
|
* @override
|
||||||
*/
|
*/
|
||||||
|
|
@ -300,62 +506,128 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) {
|
||||||
var self = this;
|
var self = this;
|
||||||
return this._super.apply(this, arguments).then(function (result) {
|
return this._super.apply(this, arguments).then(function (result) {
|
||||||
// Add search box for tree views
|
// Add search box for tree views
|
||||||
if (self.arch && self.arch.tag === 'tree' &&
|
if (self._shouldAddSearchBox()) {
|
||||||
self.$el && self.$el.hasClass('o_list_view')) {
|
|
||||||
self._addCustomSearchBox();
|
self._addCustomSearchBox();
|
||||||
|
self._customSearchReady = true;
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add search box (UI only)
|
* Check if we should add search box
|
||||||
*/
|
*/
|
||||||
_addCustomSearchBox: function() {
|
_shouldAddSearchBox: function() {
|
||||||
if (this.$el.find('.oe_search_container').length) {
|
return this.arch &&
|
||||||
return;
|
this.arch.tag === 'tree' &&
|
||||||
}
|
this.$el &&
|
||||||
|
this.$el.hasClass('o_list_view') &&
|
||||||
var html = '<div class="oe_search_container" style="display: flex; align-items: center; margin: 8px; background: #f8f9fa; padding: 10px; border-radius: 4px;">' +
|
!this.$el.find('.oe_search_container').length;
|
||||||
'<input type="text" class="oe_search_input" placeholder="' + _t('Search across all records...') + '" style="flex: 1; border: 1px solid #ccc; height: 32px; padding: 0 12px; border-radius: 4px;">' +
|
|
||||||
'<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; color: #6c757d; font-size: 0.9em; font-weight: 500;"></span>' +
|
|
||||||
'<span class="oe_search_loading ml-2" style="display: none;"><i class="fa fa-spinner fa-spin"></i></span>' +
|
|
||||||
'</div>';
|
|
||||||
this.$el.prepend($(html));
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle input events (UI only, delegate to controller)
|
* Add search box UI
|
||||||
*/
|
*/
|
||||||
_onCustomSearchInput: function(e) {
|
_addCustomSearchBox: function() {
|
||||||
// Just update UI
|
var html =
|
||||||
this.$('.oe_clear_search').toggle(!!$(e.currentTarget).val().trim());
|
'<div class="oe_search_container" style="display: flex; align-items: center; margin: 8px; background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); padding: 12px; border-radius: 6px; border: 1px solid #dee2e6; box-shadow: 0 1px 3px rgba(0,0,0,0.1);">' +
|
||||||
|
'<input type="text" class="oe_search_input form-control" ' +
|
||||||
|
'placeholder="' + _t('Search in all visible columns...') + '" ' +
|
||||||
|
'style="flex: 1; border: 1px solid #ced4da; height: 36px; padding: 0 12px; border-radius: 4px; font-size: 14px;" ' +
|
||||||
|
'autocomplete="off">' +
|
||||||
|
'<button class="btn btn-secondary oe_clear_search ml-2" style="display: none; min-width: 70px;">' +
|
||||||
|
'<i class="fa fa-times mr-1"></i>' + _t('Clear') +
|
||||||
|
'</button>' +
|
||||||
|
'<span class="oe_search_count badge badge-success ml-2" style="display: none; padding: 6px 10px; font-size: 0.9em;"></span>' +
|
||||||
|
'<span class="oe_search_loading ml-2" style="display: none; color: #007bff;">' +
|
||||||
|
'<i class="fa fa-spinner fa-spin"></i>' +
|
||||||
|
'</span>' +
|
||||||
|
'</div>';
|
||||||
|
|
||||||
|
this.$el.prepend($(html));
|
||||||
|
|
||||||
|
// Focus on search input
|
||||||
|
this.$('.oe_search_input').focus();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle input events
|
||||||
|
*/
|
||||||
|
_onCustomSearchInput: function(e) {
|
||||||
|
var hasValue = !!$(e.currentTarget).val().trim();
|
||||||
|
this.$('.oe_clear_search').toggle(hasValue);
|
||||||
|
|
||||||
|
if (!hasValue) {
|
||||||
|
this.$('.oe_search_count').hide();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle keyup events with debounce
|
||||||
|
*/
|
||||||
_onCustomSearchKeyUp: function(e) {
|
_onCustomSearchKeyUp: function(e) {
|
||||||
var self = this;
|
var self = this;
|
||||||
var value = $(e.currentTarget).val().trim();
|
var value = $(e.currentTarget).val().trim();
|
||||||
|
|
||||||
|
// Ignore special keys
|
||||||
|
if (e.which === 13 || e.which === 27) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Clear previous timer
|
// Clear previous timer
|
||||||
if (this._searchTimer) {
|
if (this._searchTimer) {
|
||||||
clearTimeout(this._searchTimer);
|
clearTimeout(this._searchTimer);
|
||||||
}
|
}
|
||||||
|
|
||||||
// CORRECT: Delegate to controller after delay
|
// Show loading after short delay
|
||||||
this._searchTimer = setTimeout(function() {
|
this._searchTimer = setTimeout(function() {
|
||||||
|
// Delegate to controller
|
||||||
if (self.getParent() && self.getParent()._handleCustomSearch) {
|
if (self.getParent() && self.getParent()._handleCustomSearch) {
|
||||||
self.getParent()._handleCustomSearch(value);
|
self.getParent()._handleCustomSearch(value);
|
||||||
}
|
}
|
||||||
}, 500);
|
}, 500);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle special keys
|
||||||
|
*/
|
||||||
|
_onCustomSearchKeyDown: function(e) {
|
||||||
|
// Enter key - trigger search immediately
|
||||||
|
if (e.which === 13) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (this._searchTimer) {
|
||||||
|
clearTimeout(this._searchTimer);
|
||||||
|
}
|
||||||
|
var value = $(e.currentTarget).val().trim();
|
||||||
|
if (this.getParent() && this.getParent()._handleCustomSearch) {
|
||||||
|
this.getParent()._handleCustomSearch(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Escape key - clear search
|
||||||
|
else if (e.which === 27) {
|
||||||
|
e.preventDefault();
|
||||||
|
this._onCustomClearSearch();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle clear button click
|
||||||
|
*/
|
||||||
_onCustomClearSearch: function() {
|
_onCustomClearSearch: function() {
|
||||||
// CORRECT: Delegate to controller
|
// Clear input
|
||||||
|
this.$('.oe_search_input').val('').focus();
|
||||||
|
this.$('.oe_clear_search').hide();
|
||||||
|
this.$('.oe_search_count').hide();
|
||||||
|
|
||||||
|
// Delegate to controller
|
||||||
if (this.getParent() && this.getParent()._clearCustomSearch) {
|
if (this.getParent() && this.getParent()._clearCustomSearch) {
|
||||||
this.getParent()._clearCustomSearch();
|
this.getParent()._clearCustomSearch();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
ListController: ListController,
|
||||||
|
ListRenderer: ListRenderer
|
||||||
|
};
|
||||||
});
|
});
|
||||||
Loading…
Reference in New Issue