From 349b144467bd6ff30986b22a81ba1ae07a415c1e Mon Sep 17 00:00:00 2001 From: Mohamed Eltayar <152964073+maltayyar2@users.noreply.github.com> Date: Sat, 30 Aug 2025 14:32:03 +0300 Subject: [PATCH] =?UTF-8?q?=D8=AA=D8=AD=D8=B3=D9=8A=D9=86=20=D9=88=D8=A5?= =?UTF-8?q?=D8=B5=D9=84=D8=A7=D8=AD=20=D9=85=D9=88=D8=AF=D9=8A=D9=88=D9=84?= =?UTF-8?q?=20=D8=A7=D9=84=D8=A8=D8=AD=D8=AB=20=D8=A7=D9=84=D8=B9=D8=A7?= =?UTF-8?q?=D9=85=20=D9=81=D9=8A=20List=20Views=20-=20=D9=86=D8=B3=D8=AE?= =?UTF-8?q?=D8=A9=20=D9=85=D8=AD=D8=B3=D9=86=D8=A9=20=D8=A8=D8=A7=D9=84?= =?UTF-8?q?=D9=83=D8=A7=D9=85=D9=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit التحسينات والإصلاحات: - إصلاح استخدام reload method بالطريقة الصحيحة لـ Odoo 14 - تحسين معالجة حقول Many2one للبحث الصحيح - إضافة دعم أفضل للحصول على الحقول المرئية من fieldsInfo - تحسين الأداء مع debouncing محسن (300ms) - إضافة معالجة أفضل للأخطاء والحالات الاستثنائية - دعم محسن للغة العربية مع تطبيع شامل للنصوص - إضافة دعم مفاتيح Enter و Escape للتحكم السريع - تحسين واجهة المستخدم مع عرض حالة التحميل - إصلاح مشاكل التزامن والبحث المتكرر - معالجة صحيحة للـ domains المعقدة --- .../static/src/js/list_search.js | 520 +++++++++++++----- 1 file changed, 396 insertions(+), 124 deletions(-) diff --git a/odex25_base/fims_general_search_tree_view/static/src/js/list_search.js b/odex25_base/fims_general_search_tree_view/static/src/js/list_search.js index baa72ebf0..188528e9c 100644 --- a/odex25_base/fims_general_search_tree_view/static/src/js/list_search.js +++ b/odex25_base/fims_general_search_tree_view/static/src/js/list_search.js @@ -7,9 +7,10 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) { var _t = core._t; var concurrency = require('web.concurrency'); var rpc = require('web.rpc'); + var session = require('web.session'); // ================================ - // CORRECT APPROACH: Use ListController for data operations + // ListController: Data operations // ================================ ListController.include({ @@ -18,96 +19,171 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) { */ init: function () { this._super.apply(this, arguments); - // Initialize search state in controller (correct place) + // Initialize search state this._customSearchState = { timer: null, value: '', isFiltered: false, filteredCount: 0, originalDomain: null, - searchInProgress: false + searchInProgress: false, + lastSearchPromise: null }; 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) { var self = this; - if (self._customSearchState.searchInProgress) { - console.log('Search already in progress, ignoring'); - return Promise.resolve(); + // Cancel any pending search + if (self._customSearchState.timer) { + clearTimeout(self._customSearchState.timer); + self._customSearchState.timer = null; } - console.log('=== CONTROLLER SEARCH ==='); - console.log('Search value:', value); - - return this._searchMutex.exec(function() { - self._customSearchState.searchInProgress = true; - - 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; - }); + // Debounce search + return new Promise(function(resolve) { + self._customSearchState.timer = setTimeout(function() { + self._executeCustomSearch(value).then(resolve); + }, 300); }); }, /** - * 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) { var self = this; // Build search domain var searchDomain = this._buildCustomSearchDomain(value); - if (searchDomain.length === 0) { - console.warn('No searchable fields found'); + if (!searchDomain || searchDomain.length === 0) { + console.warn('No searchable fields found or no valid search domain'); return Promise.resolve(); } // Combine with original domain 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({ model: this.modelName, method: 'search_count', args: [finalDomain], - context: this.model.get(this.handle).getContext() + context: session.user_context }).then(function(count) { - console.log('Search count:', count); + console.log('Found records:', count); - // Update UI + // Update UI with count self._customSearchState.filteredCount = count; self._updateCustomSearchUI(count); - // Step 2: CORRECT WAY - Use controller's reload method - return self.reload({domain: finalDomain}); + // Now reload the view with new domain + // 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() { + // Update the view self._customSearchState.isFiltered = true; self._customSearchState.value = value; + + // Trigger update to renderer + return self.update({}, {reload: false}); + }).then(function() { console.log('Search applied successfully'); return Promise.resolve(); }).catch(function(error) { console.error('Search error:', error); + self._showSearchError(); 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 ==='); - // Clear UI - this.renderer.$('.oe_search_input').val(''); - this.renderer.$('.oe_clear_search').hide(); - this.renderer.$('.oe_search_count').hide(); + // Clear UI immediately + if (this.renderer && this.renderer.$) { + this.renderer.$('.oe_search_input').val(''); + this.renderer.$('.oe_clear_search').hide(); + this.renderer.$('.oe_search_count').hide(); + this.renderer.$('.oe_search_loading').show(); + } // Reset state this._customSearchState.value = ''; this._customSearchState.isFiltered = false; this._customSearchState.filteredCount = 0; - // Restore original domain + // Get original domain var originalDomain = this._customSearchState.originalDomain || []; this._customSearchState.originalDomain = null; - // CORRECT: Use controller's reload with original domain - return this.reload({domain: originalDomain}); + console.log('Restoring original 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) { + if (!value) return []; + var fields = this._getCustomSearchableFields(); - if (fields.length === 0) { + if (!fields || fields.length === 0) { + console.warn('No searchable fields found'); return []; } var conditions = []; 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) { try { - if (['char', 'text', 'html'].includes(field.type)) { - conditions.push([field.name, 'ilike', value]); - if (normalized !== value) { - conditions.push([field.name, 'ilike', normalized]); - } - } else if (['integer', 'float', 'monetary'].includes(field.type)) { - var num = parseFloat(value); - if (!isNaN(num)) { - conditions.push([field.name, '=', num]); - } - } else if (field.type === 'many2one') { - conditions.push([field.name + '.name', 'ilike', value]); - if (normalized !== value) { - conditions.push([field.name + '.name', 'ilike', normalized]); - } - } else if (field.type === 'selection') { - conditions.push([field.name, 'ilike', value]); - } else if (field.type === 'boolean') { - var lowerValue = value.toLowerCase(); - if (['true', 'yes', 'نعم', '1'].includes(lowerValue)) { - conditions.push([field.name, '=', true]); - } else if (['false', 'no', 'لا', '0'].includes(lowerValue)) { - conditions.push([field.name, '=', false]); - } + var fieldType = field.type; + var fieldName = field.name; + + switch(fieldType) { + case 'char': + case 'text': + case 'html': + searchValues.forEach(function(searchVal) { + conditions.push([fieldName, 'ilike', searchVal]); + }); + break; + + case 'integer': + case 'float': + case 'monetary': + var numValue = parseFloat(value); + if (!isNaN(numValue)) { + // For numeric fields, also search as string + conditions.push([fieldName, '=', numValue]); + // Also search string representation + conditions.push([fieldName, 'ilike', value]); + } + 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) { console.warn('Error processing field', field.name, ':', error); @@ -185,28 +320,54 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) { return []; } + // Build proper OR domain if (conditions.length === 1) { - return conditions; + return conditions[0]; } - // Build OR domain - var domain = []; + // Create OR domain: ['|', ['field1', 'op', 'val'], ['field2', 'op', 'val']] + var orDomain = []; 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() { var fields = []; var state = this.model.get(this.handle); - var fieldDefs = state.fields || {}; - // Get from renderer columns - if (this.renderer.columns) { + if (!state) { + 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) { if (!col.invisible && col.attrs && 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) { fields.push({ 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) { - var commonFields = ['name', 'display_name', 'code', 'reference']; + var commonFields = ['name', 'display_name', 'code', 'reference', 'ref', 'description']; commonFields.forEach(function(fname) { if (fieldDefs[fname] && fieldDefs[fname].store !== false) { fields.push({ 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; }, /** - * Combine domains + * Combine domains - FIXED VERSION */ _combineCustomDomains: function(searchDomain) { var originalDomain = this._customSearchState.originalDomain || []; - if (originalDomain.length > 0 && searchDomain.length > 0) { - return ['&'].concat(originalDomain).concat(searchDomain); - } else if (searchDomain.length > 0) { - return searchDomain; - } else { + // Handle empty domains + if (!searchDomain || searchDomain.length === 0) { 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) { 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) { if (!text) return ''; return text + // Remove Arabic diacritics .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); }) - .replace(/[\u06F0-\u06F9]/g, function(d) { + // Convert Persian digits to Western digits + .replace(/[۰-۹]/g, function(d) { return 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, 'و'); + // Normalize Alef variations + .replace(/[آأإا]/g, 'ا') + // Normalize Teh Marbuta + .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({ events: _.extend({}, ListRenderer.prototype.events, { 'keyup .oe_search_input': '_onCustomSearchKeyUp', '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 */ @@ -300,62 +506,128 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) { var self = this; return this._super.apply(this, arguments).then(function (result) { // Add search box for tree views - if (self.arch && self.arch.tag === 'tree' && - self.$el && self.$el.hasClass('o_list_view')) { + if (self._shouldAddSearchBox()) { self._addCustomSearchBox(); + self._customSearchReady = true; } return result; }); }, /** - * Add search box (UI only) + * Check if we should add search box */ - _addCustomSearchBox: function() { - if (this.$el.find('.oe_search_container').length) { - return; - } - - var html = '