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 435db7d82..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
@@ -2,463 +2,377 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) {
"use strict";
var ListRenderer = require('web.ListRenderer');
+ var ListController = require('web.ListController');
var core = require('web.core');
var _t = core._t;
var concurrency = require('web.concurrency');
var rpc = require('web.rpc');
+ var session = require('web.session');
- ListRenderer.include({
- events: _.extend({}, ListRenderer.prototype.events, {
- 'keyup .oe_search_input': '_onSearchKeyUp',
- 'input .oe_search_input': '_onSearchInput',
- 'click .oe_clear_search': '_onClearSearchClick'
- }),
+ // ================================
+ // ListController: Data operations
+ // ================================
+ ListController.include({
/**
* @override
*/
init: function () {
this._super.apply(this, arguments);
- // Initialize mutex for preventing concurrent operations
- this._searchMutex = new concurrency.Mutex();
// Initialize search state
- this._initSearchState();
+ this._customSearchState = {
+ timer: null,
+ value: '',
+ isFiltered: false,
+ filteredCount: 0,
+ originalDomain: null,
+ searchInProgress: false,
+ lastSearchPromise: null
+ };
+ this._searchMutex = new concurrency.Mutex();
},
-
+
/**
- * @override - FIXED: Single _renderView method with complete logic
+ * @override - Hook into rendering complete
*/
- _renderView: function () {
+ 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;
-
- // Store current search state to restore after render
- var currentSearchValue = this._search ? this._search.value : '';
- var wasFiltered = this._search && this._search.isFiltered;
- var originalFilteredCount = this._search ? this._search.filteredCount : 0;
-
- return this._super.apply(this, arguments).then(function (result) {
- // Add search box if this is a tree view
- if (self.arch && self.arch.tag === 'tree' &&
- self.$el && self.$el.hasClass('o_list_view')) {
-
- // Add search box if not exists
- if (!self.$el.find('.oe_search_input').length) {
- self._addSearchBox();
- }
-
- // Restore search UI state without re-triggering search
- if (currentSearchValue && wasFiltered) {
- console.log('Restoring search state for:', currentSearchValue);
- self._restoreSearchUIState(currentSearchValue, originalFilteredCount);
- }
-
- // Update search count after Odoo's native rendering if we're in search mode
- if (self._search && self._search.isFiltered && self._search.value) {
- self._updateSearchCount();
- }
- }
-
- return result;
- });
- },
-
- /**
- * Add search box to view
- */
- _addSearchBox: function() {
- if (this.$el.find('.oe_search_container').length) {
+ // Ensure renderer is ready
+ if (this.renderer && this.renderer._customSearchReady) {
+ console.log('Custom search handler already setup');
return;
}
-
- var html = '
' +
- '' +
- '' +
- '' +
- '' +
- '
';
- this.$el.prepend($(html));
},
-
+
/**
- * Initialize search state
+ * Handle custom search from controller
*/
- _initSearchState: function() {
- if (!this._search) {
- this._search = {
- timer: null,
- value: '',
- isFiltered: false,
- filteredCount: 0,
- originalDomain: null,
- lastSearchPromise: null
- };
- this._searchInProgress = false;
- }
- },
-
- /**
- * Restore search UI state after render - SIMPLIFIED
- */
- _restoreSearchUIState: function(searchValue, filteredCount) {
- var $input = this.$('.oe_search_input');
- var $clearBtn = this.$('.oe_clear_search');
- var $count = this.$('.oe_search_count');
-
- // Restore input value
- if ($input.length && searchValue) {
- $input.val(searchValue);
- $clearBtn.show();
-
- // Restore search state
- this._search.value = searchValue;
- this._search.isFiltered = true;
-
- // Show count if available
- if (filteredCount) {
- this._search.filteredCount = filteredCount;
- $count.text(_t('Found: ') + filteredCount + _t(' records')).show();
- } else {
- $count.text(_t('Searching...')).show();
- }
- }
- },
-
- /**
- * Handle search input event
- */
- _onSearchInput: function(e) {
- this._search.value = $(e.currentTarget).val().trim();
- },
-
- /**
- * Handle search input keyup - ENHANCED LOGIC
- */
- _onSearchKeyUp: function(e) {
- var self = this;
- var value = $(e.currentTarget).val().trim();
-
- // Clear previous timer
- if (this._search.timer) {
- clearTimeout(this._search.timer);
- }
-
- // Update UI immediately
- this.$('.oe_clear_search').toggle(!!value);
-
- // Store value
- this._search.value = value;
-
- // ENHANCED: Always trigger search with appropriate delay
- this._search.timer = setTimeout(function() {
- self._performSearch(value);
- }, 500);
- },
-
- /**
- * Handle clear button
- */
- _onClearSearchClick: function() {
- this._clearSearch();
- },
-
- /**
- * CORRECTED: Perform search using PROPER Odoo reload method
- */
- _performSearch: function(value) {
+ _handleCustomSearch: function(value) {
var self = this;
- // Prevent concurrent searches
- if (this._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('=== CORRECTED PERFORM SEARCH ===');
- console.log('Search value:', value);
- console.log('Value length:', value ? value.length : 0);
-
- return this._searchMutex.exec(function() {
- self._searchInProgress = true;
-
- // Handle empty search by clearing filters
- if (!value || value.length === 0) {
- console.log('Empty search - clearing all filters');
- return self._clearSearchInternal().finally(function() {
- self._searchInProgress = false;
- });
- }
-
- // Check prerequisites
- if (!self.state || !self.state.model) {
- console.error('Missing model information');
- self._searchInProgress = false;
- return Promise.resolve();
- }
-
- console.log('Starting CORRECTED search for model:', self.state.model);
-
- // Store original domain only once
- if (!self._search.originalDomain && !self._search.isFiltered) {
- self._storeOriginalDomain();
- }
-
- // Show loading
- self._showLoading(true);
-
- // CORRECTED: Use proper Odoo reload method with domain update
- return self._searchWithReload(value).finally(function() {
- self._showLoading(false);
- self._searchInProgress = false;
- console.log('=== Corrected search completed ===');
- });
+ // Debounce search
+ return new Promise(function(resolve) {
+ self._customSearchState.timer = setTimeout(function() {
+ self._executeCustomSearch(value).then(resolve);
+ }, 300);
});
},
-
+
/**
- * CORRECTED: The PROPER way - Use reload with updated domain
+ * Execute the search
*/
- _searchWithReload: function(value) {
+ _executeCustomSearch: function(value) {
var self = this;
- console.log('=== USING CORRECT RELOAD METHOD ===');
+ 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._buildSearchDomain(value);
- if (searchDomain.length === 0) {
- console.warn('No searchable fields found for value:', value);
+ var searchDomain = this._buildCustomSearchDomain(value);
+ 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._combineDomains(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 accurate count using RPC
+ // 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.state.model,
+ model: this.modelName,
method: 'search_count',
args: [finalDomain],
- context: this.state.context || {}
+ context: session.user_context
}).then(function(count) {
- console.log('Accurate search count from server:', count);
+ console.log('Found records:', count);
- // Store count
- self._search.filteredCount = count;
+ // Update UI with count
+ self._customSearchState.filteredCount = count;
+ self._updateCustomSearchUI(count);
- // Update UI with accurate count
- self.$('.oe_search_count').text(_t('Found: ') + count + _t(' records')).show();
+ // 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);
- // STEP 2: THE CORRECT WAY - Use trigger_up with 'reload' passing domain
- // This is how Odoo's own search works!
- self.trigger_up('reload', {
+ // Update the domain in the state
+ return self.model.reload(handle, {
domain: finalDomain,
- context: self.state.context || {},
- groupBy: self.state.groupedBy || [],
- keepSelection: false
+ offset: 0, // Reset to first page
+ limit: state.limit || 80
});
+ }).then(function() {
+ // Update the view
+ self._customSearchState.isFiltered = true;
+ self._customSearchState.value = value;
- // Update search state
- self._search.isFiltered = true;
-
+ // Trigger update to renderer
+ return self.update({}, {reload: false});
+ }).then(function() {
+ console.log('Search applied successfully');
return Promise.resolve();
}).catch(function(error) {
- console.error('Error getting search count:', error);
-
- // Fallback: still apply reload without count
- self.trigger_up('reload', {
- domain: finalDomain,
- context: self.state.context || {},
- groupBy: self.state.groupedBy || [],
- keepSelection: false
- });
-
- self._search.isFiltered = true;
- self.$('.oe_search_count').text(_t('Search applied')).show();
-
+ console.error('Search error:', error);
+ self._showSearchError();
return Promise.resolve();
+ }).finally(function() {
+ // Hide loading
+ if (self.renderer) {
+ self.renderer.$('.oe_search_loading').hide();
+ }
});
},
-
+
/**
- * CORRECTED: Clear search using proper reload method
+ * Clear custom search
*/
- _clearSearchInternal: function() {
+ _clearCustomSearch: function() {
var self = this;
- console.log('=== CORRECTED CLEAR SEARCH ===');
+ console.log('=== CLEARING CUSTOM SEARCH ===');
// Clear UI immediately
- this.$('.oe_search_input').val('');
- this.$('.oe_clear_search').hide();
- this.$('.oe_search_count').hide();
+ 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();
+ }
- // Clear search state
- this._search.value = '';
- this._search.isFiltered = false;
- this._search.filteredCount = 0;
+ // Reset state
+ this._customSearchState.value = '';
+ this._customSearchState.isFiltered = false;
+ this._customSearchState.filteredCount = 0;
- // Use original domain to clear search
- var originalDomain = this._search.originalDomain || [];
+ // Get original domain
+ var originalDomain = this._customSearchState.originalDomain || [];
+ this._customSearchState.originalDomain = null;
- // CORRECTED: Use proper reload with original domain
- this.trigger_up('reload', {
+ 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,
- context: this.state.context || {},
- groupBy: this.state.groupedBy || [],
- keepSelection: false
+ 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 - FIXED VERSION
+ */
+ _buildCustomSearchDomain: function(value) {
+ if (!value) return [];
- // Clear stored original domain
- this._search.originalDomain = null;
-
- console.log('Search cleared using CORRECTED method');
- return Promise.resolve();
- },
-
- /**
- * Public clear search method
- */
- _clearSearch: function() {
- return this._clearSearchInternal();
- },
-
- /**
- * Store original domain properly
- */
- _storeOriginalDomain: function() {
- if (this.state && this.state.domain && !this._search.originalDomain) {
- // Deep copy the domain to avoid reference issues
- this._search.originalDomain = JSON.parse(JSON.stringify(this.state.domain));
- console.log('Stored original domain:', this._search.originalDomain);
- }
- },
-
- /**
- * ENHANCED: Combine search domain with original domain
- */
- _combineDomains: function(searchDomain) {
- var finalDomain = [];
-
- if (this._search.originalDomain && this._search.originalDomain.length > 0) {
- if (searchDomain.length > 0) {
- // Combine: originalDomain AND searchDomain
- finalDomain = ['&'];
- finalDomain = finalDomain.concat(this._search.originalDomain);
- finalDomain = finalDomain.concat(searchDomain);
- } else {
- finalDomain = this._search.originalDomain.slice();
- }
- } else {
- finalDomain = searchDomain.slice();
- }
-
- return finalDomain;
- },
-
- /**
- * ENHANCED: Update search count after render
- */
- _updateSearchCount: function() {
- // If we have the accurate count from RPC, use it
- if (this._search.filteredCount) {
- this.$('.oe_search_count').text(_t('Found: ') + this._search.filteredCount + _t(' records')).show();
- } else {
- // Fallback: count visible records (just for immediate feedback)
- var visibleCount = this.state && this.state.data ? this.state.data.length : 0;
- if (visibleCount > 0) {
- this.$('.oe_search_count').text(_t('Showing: ') + visibleCount + _t(' records')).show();
- }
- }
- },
-
- /**
- * ENHANCED: Build search domain with better logic
- */
- _buildSearchDomain: function(value) {
- var fields = this._getSearchableFields();
- if (fields.length === 0) {
+ var fields = this._getCustomSearchableFields();
+ if (!fields || fields.length === 0) {
console.warn('No searchable fields found');
return [];
}
- console.log('Searching in fields:', fields.map(f => f.name + ' (' + f.type + ')'));
-
var conditions = [];
var normalized = this._normalizeArabic(value);
+ var searchValues = [value];
+ if (normalized !== value) {
+ searchValues.push(normalized);
+ }
+
+ console.log('Building domain for fields:', fields);
- // Build conditions for each field type
fields.forEach(function(field) {
try {
- if (['char', 'text', 'html'].includes(field.type)) {
- // Text fields: use ilike for partial matching
- conditions.push([field.name, 'ilike', value]);
- if (normalized !== value) {
- conditions.push([field.name, 'ilike', normalized]);
- }
- } else if (['integer', 'float', 'monetary'].includes(field.type)) {
- // Numeric fields: exact match if value is numeric
- var num = parseFloat(value);
- if (!isNaN(num)) {
- conditions.push([field.name, '=', num]);
- }
- } else if (field.type === 'selection') {
- // Selection fields: search in selection values
- conditions.push([field.name, 'ilike', value]);
- } else if (field.type === 'many2one') {
- // Many2one fields: search in related record's name
- conditions.push([field.name + '.name', 'ilike', value]);
- if (normalized !== value) {
- conditions.push([field.name + '.name', 'ilike', normalized]);
- }
- } else if (field.type === 'boolean') {
- // Boolean fields: match true/false/yes/no
- 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);
}
});
- // Build OR domain
if (conditions.length === 0) {
return [];
}
+ // Build proper OR domain
if (conditions.length === 1) {
- return conditions;
+ return conditions[0];
}
- // Add OR operators: |, |, |, condition1, condition2, condition3, condition4
- 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);
},
-
+
/**
- * ENHANCED: Get searchable fields with better logic
+ * Get searchable fields - ENHANCED VERSION
*/
- _getSearchableFields: function() {
+ _getCustomSearchableFields: function() {
var fields = [];
- var fieldDefs = this.state.fields || {};
+ var state = this.model.get(this.handle);
- console.log('Available fields:', Object.keys(fieldDefs));
- console.log('Available columns:', this.columns ? this.columns.length : 0);
+ if (!state) {
+ console.warn('No state available');
+ return fields;
+ }
- // PRIORITY 1: Get from visible columns
- if (this.columns && this.columns.length > 0) {
- this.columns.forEach(function(col) {
+ 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;
var field = fieldDefs[fieldName];
- if (field && field.store !== false && field.searchable !== false) {
+ if (field && field.store !== false) {
fields.push({
name: fieldName,
type: field.type,
@@ -469,11 +383,11 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) {
});
}
- // PRIORITY 2: Add common searchable fields if none found from columns
+ // Method 3: Fallback to common searchable fields
if (fields.length === 0) {
- var commonFields = ['name', 'display_name', 'code', 'reference', 'description', 'note'];
+ var commonFields = ['name', 'display_name', 'code', 'reference', 'ref', 'description'];
commonFields.forEach(function(fname) {
- if (fieldDefs[fname] && fieldDefs[fname].store !== false && fieldDefs[fname].searchable !== false) {
+ if (fieldDefs[fname] && fieldDefs[fname].store !== false) {
fields.push({
name: fname,
type: fieldDefs[fname].type,
@@ -483,57 +397,237 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) {
});
}
- console.log('Final searchable fields:', fields);
+ console.log('Searchable fields found:', fields);
return fields;
},
-
+
/**
- * Show loading indicator
+ * Combine domains - FIXED VERSION
*/
- _showLoading: function(show) {
- this.$('.oe_search_loading').toggle(show);
- if (show) {
- this.$('.oe_search_count').hide();
+ _combineCustomDomains: function(searchDomain) {
+ var originalDomain = this._customSearchState.originalDomain || [];
+
+ // 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);
+ },
+
+ /**
+ * Update search UI
+ */
+ _updateCustomSearchUI: function(count) {
+ if (this.renderer && this.renderer.$) {
+ var message = _t('Found: ') + count + _t(' records');
+ this.renderer.$('.oe_search_count')
+ .text(message)
+ .show();
+ this.renderer.$('.oe_clear_search').show();
}
},
-
+
/**
- * ENHANCED: Normalize Arabic text for better searching
+ * 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 diacritics (تشكيل)
+ // Remove Arabic diacritics
.replace(/[\u064B-\u065F]/g, '')
- // Convert Arabic-Indic digits to European digits
- .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);
})
- // Convert Persian digits to European digits
- .replace(/[\u06F0-\u06F9]/g, function(d) {
+ // Convert Persian digits to Western digits
+ .replace(/[۰-۹]/g, function(d) {
return String.fromCharCode(d.charCodeAt(0) - 0x06F0 + 0x0030);
})
// Normalize Alef variations
- .replace(/[\u0622\u0623\u0625\u0627]/g, 'ا')
+ .replace(/[آأإا]/g, 'ا')
// Normalize Teh Marbuta
- .replace(/[\u0629]/g, 'ه')
+ .replace(/ة/g, 'ه')
// Normalize Yeh variations
- .replace(/[\u064A\u0626\u0649]/g, 'ي')
+ .replace(/[يئى]/g, 'ي')
// Normalize Waw variations
- .replace(/[\u0624\u0648]/g, 'و');
+ .replace(/[ؤو]/g, 'و')
+ // Trim spaces
+ .trim();
+ }
+ });
+
+ // ================================
+ // 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',
+ 'keydown .oe_search_input': '_onCustomSearchKeyDown'
+ }),
+
+ /**
+ * @override
+ */
+ init: function() {
+ this._super.apply(this, arguments);
+ this._searchTimer = null;
+ this._customSearchReady = false;
},
/**
* @override
*/
- destroy: function() {
- if (this._search && this._search.timer) {
- clearTimeout(this._search.timer);
+ _renderView: function () {
+ var self = this;
+ return this._super.apply(this, arguments).then(function (result) {
+ // Add search box for tree views
+ if (self._shouldAddSearchBox()) {
+ self._addCustomSearchBox();
+ self._customSearchReady = true;
+ }
+ return result;
+ });
+ },
+
+ /**
+ * Check if we should add search box
+ */
+ _shouldAddSearchBox: function() {
+ return this.arch &&
+ this.arch.tag === 'tree' &&
+ this.$el &&
+ this.$el.hasClass('o_list_view') &&
+ !this.$el.find('.oe_search_container').length;
+ },
+
+ /**
+ * Add search box UI
+ */
+ _addCustomSearchBox: function() {
+ var html =
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '
';
+
+ 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) {
+ var self = this;
+ var value = $(e.currentTarget).val().trim();
+
+ // Ignore special keys
+ if (e.which === 13 || e.which === 27) {
+ return;
+ }
+
+ // Clear previous timer
+ if (this._searchTimer) {
+ clearTimeout(this._searchTimer);
+ }
+
+ // Show loading after short delay
+ this._searchTimer = setTimeout(function() {
+ // Delegate to controller
+ if (self.getParent() && self.getParent()._handleCustomSearch) {
+ self.getParent()._handleCustomSearch(value);
+ }
+ }, 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() {
+ // 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) {
+ this.getParent()._clearCustomSearch();
}
- this._searchInProgress = false;
- return this._super.apply(this, arguments);
}
});
+ return {
+ ListController: ListController,
+ ListRenderer: ListRenderer
+ };
});
\ No newline at end of file