🚀 [ENHANCE] Add Many2Many/One2Many search support to fims_general_search_tree_view

- Add relational_fields support for FieldOne2Many and FieldMany2Many
- Implement dedicated search functionality for form embedded lists
- Add proper DOM detection for relational field contexts
- Maintain backward compatibility with existing ListRenderer functionality
- Support Arabic search with normalization for both contexts
- Add specialized styling for relational field search boxes
This commit is contained in:
Mohamed Eltayar 2025-08-30 20:15:05 +03:00
parent aff17a0043
commit 9a9b45764b
1 changed files with 303 additions and 1 deletions

View File

@ -3,12 +3,17 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) {
var ListRenderer = require('web.ListRenderer');
var ListController = require('web.ListController');
var relational_fields = require('web.relational_fields');
var core = require('web.core');
var _t = core._t;
var concurrency = require('web.concurrency');
var rpc = require('web.rpc');
var session = require('web.session');
// Get relational field classes
var FieldOne2Many = relational_fields.FieldOne2Many;
var FieldMany2Many = relational_fields.FieldMany2Many;
ListController.include({
init: function () {
this._super.apply(this, arguments);
@ -512,8 +517,305 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) {
}
});
// ============= NEW: Relational Fields Support =============
// Mixin for common relational field functionality
var RelationalFieldMixin = {
_searchState: null,
_searchMutex: null,
init: function() {
this._super.apply(this, arguments);
this._initRelationalSearch();
},
_initRelationalSearch: function() {
this._searchState = {
timer: null,
value: '',
isFiltered: false,
filteredCount: 0,
originalRecords: null,
allRecords: null,
lastSearchValue: ''
};
this._searchMutex = new concurrency.Mutex();
},
_renderView: function() {
var self = this;
return this._super.apply(this, arguments).then(function(result) {
if (self._shouldAddRelationalSearchBox()) {
self._addRelationalSearchBox();
self._storeOriginalRecords();
}
return result;
});
},
_shouldAddRelationalSearchBox: function() {
return this.renderer &&
this.renderer.$el &&
this.renderer.$el.find('.o_list_view').length > 0 &&
!this.renderer.$el.find('.oe_relational_search_container').length &&
this.value &&
this.value.data &&
this.value.data.length > 0;
},
_storeOriginalRecords: function() {
if (this.value && this.value.data && !this._searchState.originalRecords) {
this._searchState.originalRecords = JSON.parse(JSON.stringify(this.value.data));
this._searchState.allRecords = JSON.parse(JSON.stringify(this.value.data));
}
},
_addRelationalSearchBox: function() {
var self = this;
var savedValue = this._searchState.value || '';
var savedCount = this._searchState.filteredCount || 0;
var isFiltered = this._searchState.isFiltered || false;
var countText = isFiltered ?
_t('عدد السجلات: ') + savedCount : '';
var html =
'<div class="oe_relational_search_container d-flex align-items-center mb-2" style="padding: 8px; background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); border: 1px solid #dee2e6; border-radius: 6px; gap: 8px;">' +
'<input type="text" class="oe_relational_search_input form-control flex-grow-1" ' +
'placeholder="' + _.escape(_t('البحث في السجلات المرتبطة...')) + '" ' +
'value="' + _.escape(savedValue) + '" ' +
'autocomplete="off" style="height: 32px; border-radius: 4px;">' +
'<button class="btn btn-secondary oe_relational_clear_search" style="display: ' +
(savedValue ? 'inline-block' : 'none') + '; height: 32px; min-width: 60px;">' +
'<i class="fa fa-times mr-1"></i>' + _t('مسح') +
'</button>' +
'<span class="oe_relational_search_count badge badge-success" style="display: ' +
(isFiltered ? 'inline-block' : 'none') + '; font-size: 0.9em;">' +
countText +
'</span>' +
'<span class="oe_relational_search_loading" style="display: none; color: #007bff;">' +
'<i class="fa fa-spinner fa-spin"></i>' +
'</span>' +
'</div>';
var $listContainer = this.renderer.$el.find('.o_list_view').first();
$listContainer.prepend($(html));
// Bind events
this.renderer.$el.find('.oe_relational_search_input').on('keyup', function(e) {
self._onRelationalSearchKeyUp(e);
});
this.renderer.$el.find('.oe_relational_search_input').on('input', function(e) {
self._onRelationalSearchInput(e);
});
this.renderer.$el.find('.oe_relational_clear_search').on('click', function(e) {
self._onRelationalClearSearch(e);
});
this.renderer.$el.find('.oe_relational_search_input').on('keydown', function(e) {
self._onRelationalSearchKeyDown(e);
});
},
_onRelationalSearchInput: function(e) {
var currentValue = $(e.currentTarget).val();
var hasValue = !!currentValue.trim();
this.renderer.$el.find('.oe_relational_clear_search').toggle(hasValue);
if (!hasValue) {
this.renderer.$el.find('.oe_relational_search_count').hide();
}
},
_onRelationalSearchKeyUp: function(e) {
var self = this;
var value = $(e.currentTarget).val().trim();
var ignoreKeys = [13, 27, 16, 17, 18, 91, 93, 37, 38, 39, 40,
33, 34, 35, 36, 9, 20, 144, 145,
112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123];
if (ignoreKeys.indexOf(e.which) !== -1) return;
if (value === this._searchState.lastSearchValue) return;
this._searchState.lastSearchValue = value;
if (this._searchState.timer) {
clearTimeout(this._searchState.timer);
}
this._searchState.timer = setTimeout(function() {
self._executeRelationalSearch(value);
}, 300);
},
_onRelationalSearchKeyDown: function(e) {
if (e.which === 13) {
e.preventDefault();
if (this._searchState.timer) {
clearTimeout(this._searchState.timer);
}
var value = $(e.currentTarget).val().trim();
this._executeRelationalSearch(value);
} else if (e.which === 27) {
e.preventDefault();
this._onRelationalClearSearch();
}
},
_onRelationalClearSearch: function() {
this.renderer.$el.find('.oe_relational_search_input').val('').focus();
this.renderer.$el.find('.oe_relational_clear_search').hide();
this.renderer.$el.find('.oe_relational_search_count').hide();
this._searchState.lastSearchValue = '';
this._clearRelationalSearch();
},
_executeRelationalSearch: function(value) {
var self = this;
return this._searchMutex.exec(function() {
if (!value || value.length === 0) {
return self._clearRelationalSearch();
}
return self._applyRelationalSearch(value);
});
},
_applyRelationalSearch: function(value) {
var self = this;
if (!this._searchState.allRecords || this._searchState.allRecords.length === 0) {
return Promise.resolve();
}
var normalizedSearch = this._normalizeText(value.toLowerCase());
var searchValues = [value.toLowerCase(), normalizedSearch].filter(function(v, i, arr) {
return arr.indexOf(v) === i; // Remove duplicates
});
var filteredRecords = this._searchState.allRecords.filter(function(record) {
return self._recordMatchesSearch(record, searchValues);
});
this._searchState.filteredCount = filteredRecords.length;
this._searchState.isFiltered = true;
this._searchState.value = value;
// Update the UI
this._updateRelationalSearchUI(filteredRecords.length);
// Update the field's value with filtered data
var newValue = _.clone(this.value);
newValue.data = filteredRecords;
newValue.count = filteredRecords.length;
// Re-render with filtered data
return this._setValue(newValue, {forceChange: true}).then(function() {
return Promise.resolve();
});
},
_recordMatchesSearch: function(record, searchValues) {
var self = this;
var recordData = record.data || {};
// Get searchable fields from the record
var searchableValues = [];
// Add all text-like fields
Object.keys(recordData).forEach(function(fieldName) {
var value = recordData[fieldName];
if (value !== null && value !== undefined) {
if (typeof value === 'string') {
searchableValues.push(value.toLowerCase());
searchableValues.push(self._normalizeText(value.toLowerCase()));
} else if (typeof value === 'number') {
searchableValues.push(value.toString());
} else if (Array.isArray(value) && value.length === 2) {
// Many2one field format [id, display_name]
if (typeof value[1] === 'string') {
searchableValues.push(value[1].toLowerCase());
searchableValues.push(self._normalizeText(value[1].toLowerCase()));
}
}
}
});
// Check if any search value matches any record value
return searchValues.some(function(searchValue) {
return searchableValues.some(function(recordValue) {
return recordValue.indexOf(searchValue) !== -1;
});
});
},
_clearRelationalSearch: function() {
if (!this._searchState.originalRecords) {
return Promise.resolve();
}
this._searchState.value = '';
this._searchState.isFiltered = false;
this._searchState.filteredCount = 0;
this._searchState.lastSearchValue = '';
// Restore original records
var newValue = _.clone(this.value);
newValue.data = JSON.parse(JSON.stringify(this._searchState.originalRecords));
newValue.count = this._searchState.originalRecords.length;
// Hide search UI elements
if (this.renderer && this.renderer.$el) {
this.renderer.$el.find('.oe_relational_search_count').hide();
this.renderer.$el.find('.oe_relational_search_loading').hide();
}
// Re-render with original data
return this._setValue(newValue, {forceChange: true});
},
_updateRelationalSearchUI: function(count) {
if (this.renderer && this.renderer.$el) {
this.renderer.$el.find('.oe_relational_search_count')
.text(_t('عدد السجلات: ') + count)
.removeClass('text-danger')
.show();
this.renderer.$el.find('.oe_relational_clear_search').show();
}
},
_normalizeText: function(text) {
if (!text) return '';
return text
.replace(/[\u064B-\u065F]/g, '') // Remove Arabic diacritics
.replace(/[٠-٩]/g, function(d) { // Convert Arabic-Indic digits
return String.fromCharCode(d.charCodeAt(0) - 0x0660 + 0x0030);
})
.replace(/[۰-۹]/g, function(d) { // Convert Extended Arabic-Indic digits
return String.fromCharCode(d.charCodeAt(0) - 0x06F0 + 0x0030);
})
.replace(/[آأإا]/g, 'ا') // Normalize Alef variations
.replace(/ة/g, 'ه') // Normalize Taa Marbouta
.replace(/[يئى]/g, 'ي') // Normalize Yaa variations
.trim();
}
};
// Apply the mixin to FieldOne2Many
FieldOne2Many.include(RelationalFieldMixin);
// Apply the mixin to FieldMany2Many
FieldMany2Many.include(RelationalFieldMixin);
return {
ListController: ListController,
ListRenderer: ListRenderer
ListRenderer: ListRenderer,
FieldOne2Many: FieldOne2Many,
FieldMany2Many: FieldMany2Many
};
});