diff --git a/client/res/templates/record/search.tpl b/client/res/templates/record/search.tpl index c7c3d4243b..ef49655dcd 100644 --- a/client/res/templates/record/search.tpl +++ b/client/res/templates/record/search.tpl @@ -65,3 +65,11 @@ {{/each}} + +
diff --git a/client/src/views/email/fields/email-address-varchar.js b/client/src/views/email/fields/email-address-varchar.js index 669e306527..a46c0790a8 100644 --- a/client/src/views/email/fields/email-address-varchar.js +++ b/client/src/views/email/fields/email-address-varchar.js @@ -204,6 +204,12 @@ define( if (this.mode == 'search' && this.getAcl().check('Email', 'create')) { EmailAddress.prototype.initSearchAutocomplete.call(this); } + + if (this.mode == 'search') { + this.$input.on('input', function () { + this.trigger('change'); + }.bind(this)); + } }, checkEmailAddressInString: function (string) { diff --git a/client/src/views/email/fields/email-address.js b/client/src/views/email/fields/email-address.js index 9058c37b48..d45c71ca64 100644 --- a/client/src/views/email/fields/email-address.js +++ b/client/src/views/email/fields/email-address.js @@ -45,6 +45,12 @@ define('views/email/fields/email-address', ['views/fields/base'], function (Dep) if (this.mode == 'search' && this.getAcl().check('Email', 'create')) { this.initSearchAutocomplete(); } + + if (this.mode == 'search') { + this.$input.on('input', function () { + this.trigger('change'); + }.bind(this)); + } }, initSearchAutocomplete: function () { diff --git a/client/src/views/email/fields/from-address-varchar.js b/client/src/views/email/fields/from-address-varchar.js index 147d34b7e0..ea318437dc 100644 --- a/client/src/views/email/fields/from-address-varchar.js +++ b/client/src/views/email/fields/from-address-varchar.js @@ -92,6 +92,12 @@ define( if (this.mode == 'edit' && this.getAcl().check('Email', 'create')) { EmailAddress.prototype.initSearchAutocomplete.call(this); } + + if (this.mode == 'search') { + this.$input.on('input', function () { + this.trigger('change'); + }.bind(this)); + } }, getAutocompleteMaxCount: function () { diff --git a/client/src/views/fields/array.js b/client/src/views/fields/array.js index 5a5db702da..941b5491c5 100644 --- a/client/src/views/fields/array.js +++ b/client/src/views/fields/array.js @@ -357,6 +357,14 @@ define('views/fields/array', ['views/fields/base', 'lib!Selectize'], function (D var type = this.$el.find('select.search-type').val(); this.handleSearchType(type); + + this.$el.find('select.search-type').on('change', function () { + this.trigger('change'); + }.bind(this)); + + this.$element.on('change', function () { + this.trigger('change'); + }.bind(this)); }, fetchFromDom: function () { diff --git a/client/src/views/fields/bool.js b/client/src/views/fields/bool.js index 14e9c9b4dc..69cc79e256 100644 --- a/client/src/views/fields/bool.js +++ b/client/src/views/fields/bool.js @@ -42,12 +42,24 @@ define('views/fields/bool', 'views/fields/base', function (Dep) { validations: [], + initialSearchIsNotIdle: true, + data: function () { var data = Dep.prototype.data.call(this); data.valueIsSet = this.model.has(this.name); return data; }, + afterRender: function () { + Dep.prototype.afterRender.call(this); + + if (this.mode == 'search') { + this.$element.on('change', function () { + this.trigger('change'); + }.bind(this)); + } + }, + fetch: function () { var value = this.$element.get(0).checked; var data = {}; diff --git a/client/src/views/fields/date.js b/client/src/views/fields/date.js index dd96cb0004..a7adee8647 100644 --- a/client/src/views/fields/date.js +++ b/client/src/views/fields/date.js @@ -44,17 +44,25 @@ define('views/fields/date', 'views/fields/base', function (Dep) { validations: ['required', 'date', 'after', 'before'], - searchTypeList: ['lastSevenDays', 'ever', 'isEmpty', 'currentMonth', 'lastMonth', 'nextMonth', 'currentQuarter', 'lastQuarter', 'currentYear', 'lastYear', 'today', 'past', 'future', 'lastXDays', 'nextXDays', 'olderThanXDays', 'afterXDays', 'on', 'after', 'before', 'between'], + searchTypeList: [ + 'lastSevenDays', 'ever', 'isEmpty', 'currentMonth', 'lastMonth', 'nextMonth', 'currentQuarter', + 'lastQuarter', 'currentYear', 'lastYear', 'today', 'past', 'future', 'lastXDays', 'nextXDays', + 'olderThanXDays', 'afterXDays', 'on', 'after', 'before', 'between', + ], + + initialSearchIsNotIdle: true, setup: function () { Dep.prototype.setup.call(this); if (this.getConfig().get('fiscalYearShift')) { this.searchTypeList = Espo.Utils.clone(this.searchTypeList); + if (this.getConfig().get('fiscalYearShift') % 3 != 0) { this.searchTypeList.push('currentFiscalQuarter'); this.searchTypeList.push('lastFiscalQuarter'); } + this.searchTypeList.push('currentFiscalYear'); this.searchTypeList.push('lastFiscalYear'); } @@ -199,10 +207,19 @@ define('views/fields/date', 'views/fields/base', function (Dep) { if (this.mode == 'search') { var $elAdd = this.$el.find('input.additional'); + $elAdd.datepicker(options); $elAdd.parent().find('button.date-picker-btn').on('click', function (e) { $elAdd.datepicker('show'); }); + + this.$el.find('select.search-type').on('change', function () { + this.trigger('change'); + }.bind(this)); + + $elAdd.on('change', function () { + this.trigger('change'); + }.bind(this)); } this.$element.parent().find('button.date-picker-btn').on('click', function (e) { diff --git a/client/src/views/fields/enum.js b/client/src/views/fields/enum.js index 945fdc40c0..8c903a6be2 100644 --- a/client/src/views/fields/enum.js +++ b/client/src/views/fields/enum.js @@ -258,6 +258,14 @@ define('views/fields/enum', ['views/fields/base', 'lib!Selectize'], function (De }); this.$el.find('.selectize-dropdown-content').addClass('small'); + + this.$el.find('select.search-type').on('change', function () { + this.trigger('change'); + }.bind(this)); + + this.$element.on('change', function () { + this.trigger('change'); + }.bind(this)); } }, diff --git a/client/src/views/fields/int.js b/client/src/views/fields/int.js index fa73cb8557..b17e0e1513 100644 --- a/client/src/views/fields/int.js +++ b/client/src/views/fields/int.js @@ -69,6 +69,18 @@ define('views/fields/int', 'views/fields/base', function (Dep) { if (this.mode == 'search') { var $searchType = this.$el.find('select.search-type'); this.handleSearchType($searchType.val()); + + this.$el.find('select.search-type').on('change', function () { + this.trigger('change'); + }.bind(this)); + + this.$element.on('input', function () { + this.trigger('change'); + }.bind(this)); + + this.$el.find('input.additional').on('input', function () { + this.trigger('change'); + }.bind(this)); } }, diff --git a/client/src/views/fields/link-multiple.js b/client/src/views/fields/link-multiple.js index 1a746a9b71..6a8b65fdc3 100644 --- a/client/src/views/fields/link-multiple.js +++ b/client/src/views/fields/link-multiple.js @@ -26,7 +26,7 @@ * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. ************************************************************************/ -Espo.define('views/fields/link-multiple', 'views/fields/base', function (Dep) { +define('views/fields/link-multiple', 'views/fields/base', function (Dep) { return Dep.extend({ @@ -272,6 +272,10 @@ Espo.define('views/fields/link-multiple', 'views/fields/base', function (Dep) { if (this.mode == 'search') { var type = this.$el.find('select.search-type').val(); this.handleSearchType(type); + + this.$el.find('select.search-type').on('change', function () { + this.trigger('change'); + }.bind(this)); } } }, diff --git a/client/src/views/fields/link-parent.js b/client/src/views/fields/link-parent.js index dad904dc9a..e56817b218 100644 --- a/client/src/views/fields/link-parent.js +++ b/client/src/views/fields/link-parent.js @@ -318,6 +318,10 @@ define('views/fields/link-parent', 'views/fields/base', function (Dep) { if (this.mode == 'search') { var type = this.$el.find('select.search-type').val(); this.handleSearchType(type); + + this.$el.find('select.search-type').on('change', function () { + this.trigger('change'); + }.bind(this)); } }, diff --git a/client/src/views/fields/link.js b/client/src/views/fields/link.js index c79ae2677b..5b4c384fd3 100644 --- a/client/src/views/fields/link.js +++ b/client/src/views/fields/link.js @@ -358,6 +358,10 @@ define('views/fields/link', 'views/fields/base', function (Dep) { this.once('remove', function () { $elementOneOf.autocomplete('dispose'); }, this); + + this.$el.find('select.search-type').on('change', function () { + this.trigger('change'); + }.bind(this)); } } diff --git a/client/src/views/fields/text.js b/client/src/views/fields/text.js index cd632adcfd..cea234a164 100644 --- a/client/src/views/fields/text.js +++ b/client/src/views/fields/text.js @@ -26,7 +26,7 @@ * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. ************************************************************************/ -Espo.define('views/fields/text', 'views/fields/base', function (Dep) { +define('views/fields/text', 'views/fields/base', function (Dep) { return Dep.extend({ @@ -217,6 +217,14 @@ Espo.define('views/fields/text', 'views/fields/base', function (Dep) { if (this.mode == 'search') { var type = this.$el.find('select.search-type').val(); this.handleSearchType(type); + + this.$el.find('select.search-type').on('change', function () { + this.trigger('change'); + }.bind(this)); + + this.$element.on('input', function () { + this.trigger('change'); + }.bind(this)); } if (this.mode === 'edit' && !this.autoHeightDisabled) { diff --git a/client/src/views/fields/varchar.js b/client/src/views/fields/varchar.js index 67e7ccdba9..2769d71f53 100644 --- a/client/src/views/fields/varchar.js +++ b/client/src/views/fields/varchar.js @@ -38,7 +38,10 @@ define('views/fields/varchar', 'views/fields/base', function (Dep) { searchTemplate: 'fields/varchar/search', - searchTypeList: ['startsWith', 'contains', 'equals', 'endsWith', 'like', 'notContains', 'notEquals', 'notLike', 'isEmpty', 'isNotEmpty'], + searchTypeList: [ + 'startsWith', 'contains', 'equals', 'endsWith', 'like', 'notContains', + 'notEquals', 'notLike', 'isEmpty', 'isNotEmpty', + ], setup: function () { this.setupOptions(); @@ -157,6 +160,16 @@ define('views/fields/varchar', 'views/fields/base', function (Dep) { this.$element.autocomplete('dispose'); }, this); } + + if (this.mode === 'search') { + this.$el.find('select.search-type').on('change', function () { + this.trigger('change'); + }.bind(this)); + + this.$element.on('input', function () { + this.trigger('change'); + }.bind(this)); + } }, fetch: function () { diff --git a/client/src/views/record/search.js b/client/src/views/record/search.js index b121f8f172..6b262ac497 100644 --- a/client/src/views/record/search.js +++ b/client/src/views/record/search.js @@ -52,9 +52,15 @@ define('views/record/search', 'view', function (Dep) { textFilterDisabled: false, + toShowApplyFiltersButton: false, + + toShowResetFiltersText: false, + + isSearchedWithAdvancedFilter: false, + viewModeIconClassMap: { list: 'fas fa-align-justify', - kanban: 'fas fa-align-left fa-rotate-90' + kanban: 'fas fa-align-left fa-rotate-90', }, data: function () { @@ -74,6 +80,8 @@ define('views/record/search', 'view', function (Dep) { viewModeDataList: this.viewModeDataList || [], hasViewModeSwitcher: this.viewModeList && this.viewModeList.length > 1, isWide: this.options.isWide, + toShowApplyFiltersButton: this.toShowApplyFiltersButton, + toShowResetFiltersText: this.toShowResetFiltersText, }; }, @@ -170,6 +178,10 @@ define('views/record/search', 'view', function (Dep) { this.loadSearchData(); + if (this.hasAdvancedFilter()) { + this.isSearchedWithAdvancedFilter = true; + } + if (this.presetName) { var hasPresetListed = false; @@ -277,57 +289,37 @@ define('views/record/search', 'view', function (Dep) { 'keypress input[data-name="textFilter"]': function (e) { if (e.keyCode == 13) { this.search(); + + this.hideApplyFiltersButton(); } }, 'focus input[data-name="textFilter"]': function (e) { e.currentTarget.select(); }, + 'click .advanced-filters-apply-container a[data-action="applyFilters"]': function (e) { + this.search(); + + this.hideApplyFiltersButton(); + }, 'click button[data-action="search"]': function (e) { this.search(); + + this.hideApplyFiltersButton(); }, 'click a[data-action="addFilter"]': function (e) { var $target = $(e.currentTarget); var name = $target.data('name'); - this.advanced[name] = {}; $target.closest('li').addClass('hidden'); - this.presetName = this.primary; - - this.createFilter(name, {}, function (view) { - view.populateDefaults(); - this.fetch(); - this.updateSearch(); - }.bind(this)); - this.updateAddFilterButton(); - this.handleLeftDropdownVisibility(); - - this.manageLabels(); - this.controlResetButtonVisibility(); + this.addFilter(name); }, 'click .advanced-filters a.remove-filter': function (e) { var $target = $(e.currentTarget); + var name = $target.data('name'); - this.$el.find('ul.filter-list li[data-name="' + name + '"]').removeClass('hidden'); - - var container = this.getView('filter-' + name).$el.closest('div.filter'); - this.clearView('filter-' + name); - container.remove(); - - delete this.advanced[name]; - - - this.presetName = this.primary; - - this.updateAddFilterButton(); - - this.fetch(); - this.updateSearch(); - - this.manageLabels(); - this.handleLeftDropdownVisibility(); - this.controlResetButtonVisibility(); + this.removeFilter(name); }, 'click button[data-action="reset"]': function (e) { this.resetFilters(); @@ -337,11 +329,13 @@ define('views/record/search', 'view', function (Dep) { }, 'click a[data-action="selectPreset"]': function (e) { var presetName = $(e.currentTarget).data('name') || null; + this.selectPreset(presetName); }, 'click .dropdown-menu a[data-action="savePreset"]': function (e) { this.createView('savePreset', 'views/modals/save-filters', {}, function (view) { view.render(); + this.listenToOnce(view, 'save', function (name) { this.savePreset(name); view.close(); @@ -376,6 +370,59 @@ define('views/record/search', 'view', function (Dep) { } }, + removeFilter: function (name) { + this.$el.find('ul.filter-list li[data-name="' + name + '"]').removeClass('hidden'); + + var container = this.getView('filter-' + name).$el.closest('div.filter'); + this.clearView('filter-' + name); + container.remove(); + + delete this.advanced[name]; + + this.presetName = this.primary; + + this.updateAddFilterButton(); + + this.fetch(); + this.updateSearch(); + + this.manageLabels(); + this.handleLeftDropdownVisibility(); + this.controlResetButtonVisibility(); + + if (this.isSearchedWithAdvancedFilter) { + this.showResetFiltersButton(); + } else { + if (!this.hasAdvancedFilter()) { + this.hideApplyFiltersButton(); + } + } + }, + + addFilter: function (name) { + this.advanced[name] = {}; + + this.presetName = this.primary; + + this.createFilter(name, {}, function (view) { + view.populateDefaults(); + this.fetch(); + this.updateSearch(); + + if (view.getView('field').initialSearchIsNotIdle) { + this.showApplyFiltersButton(); + } + }.bind(this)); + + this.updateAddFilterButton(); + this.handleLeftDropdownVisibility(); + + this.manageLabels(); + this.controlResetButtonVisibility(); + + + }, + refresh: function () { this.notify('Loading...'); this.collection.abortLastFetch(); @@ -391,6 +438,7 @@ define('views/record/search', 'view', function (Dep) { this.presetName = presetName; var advanced = this.getPresetData(); + this.primary = this.getPrimaryFilterName(); var isPreset = !(this.primary === this.presetName); @@ -406,6 +454,7 @@ define('views/record/search', 'view', function (Dep) { this.createFilters(function () { this.render(); }.bind(this)); + this.updateCollection(); }, @@ -422,7 +471,10 @@ define('views/record/search', 'view', function (Dep) { this.collection.resetOrderToDefault(); this.textFilter = ''; + this.selectPreset(this.presetName, true); + + this.hideApplyFiltersButton(); }, savePreset: function (name) { @@ -462,23 +514,29 @@ define('views/record/search', 'view', function (Dep) { removePreset: function (id) { var presetFilters = this.getPreferences().get('presetFilters') || {}; + if (!(this.scope in presetFilters)) { presetFilters[this.scope] = []; } var list; + list = presetFilters[this.scope]; + list.forEach(function (item, i) { if (item.id == id) { list.splice(i, 1); + return; } }, this); list = this.presetFilterList; + list.forEach(function (item, i) { if (item.id == id) { list.splice(i, 1); + return; } }, this); @@ -500,6 +558,7 @@ define('views/record/search', 'view', function (Dep) { updateAddFilterButton: function () { var $ul = this.$el.find('ul.filter-list'); + if ($ul.children().not('.hidden').not('.dropdown-header').length == 0) { this.$el.find('button.add-filter-button').addClass('disabled'); } else { @@ -512,6 +571,7 @@ define('views/record/search', 'view', function (Dep) { this.$filtersButton = this.$el.find('.search-row button.filters-button'); this.$leftDropdown = this.$el.find('div.search-row div.left-dropdown'); this.$resetButton = this.$el.find('[data-action="reset"]'); + this.$applyFiltersContainer = this.$el.find('.advanced-filters-apply-container'); this.updateAddFilterButton(); @@ -622,6 +682,7 @@ define('views/record/search', 'view', function (Dep) { if (primary) { var label = this.translate(primary, 'presetFilters', this.entityType); var style = this.getPrimaryFilterStyle(); + filterLabel = label; filterStyle = style; } @@ -656,6 +717,12 @@ define('views/record/search', 'view', function (Dep) { this.updateSearch(); this.updateCollection(); this.controlResetButtonVisibility(); + + this.isSearchedWithAdvancedFilter = this.hasAdvancedFilter(); + }, + + hasAdvancedFilter: function () { + return Object.keys(this.advanced).length > 0; }, getFilterDataList: function () { @@ -664,7 +731,7 @@ define('views/record/search', 'view', function (Dep) { for (var field in this.advanced) { arr.push({ key: 'filter-' + field, - name: field + name: field, }); } @@ -685,28 +752,35 @@ define('views/record/search', 'view', function (Dep) { getPresetFilterList: function () { var arr = []; + this.presetFilterList.forEach(function (item) { if (typeof item == 'string') { item = {name: item}; } + arr.push(item); }, this); + return arr; }, getPresetData: function () { var data = {}; + this.getPresetFilterList().forEach(function (item) { if (item.name == this.presetName) { data = Espo.Utils.clone(item.data || {}); + return; } }, this); + return data; }, getPrimaryFilterName: function () { var primaryFilterName = null; + this.getPresetFilterList().forEach(function (item) { if (item.name == this.presetName) { if (!('data' in item)) { @@ -714,25 +788,31 @@ define('views/record/search', 'view', function (Dep) { } else if (item.primary) { primaryFilterName = item.primary; } + return; } }, this); + return primaryFilterName; }, getPrimaryFilterStyle: function () { var style = null; + this.getPresetFilterList().forEach(function (item) { if (item.name == this.primary) { style = item.style || 'default'; + return; } }, this); + return style; }, loadSearchData: function () { var searchData = this.searchManager.get(); + this.textFilter = searchData.textFilter; if ('presetName' in searchData) { @@ -740,22 +820,27 @@ define('views/record/search', 'view', function (Dep) { } var primaryIsSet = false; + if ('primary' in searchData) { this.primary = searchData.primary; + if (!this.presetName) { this.presetName = this.primary; } + primaryIsSet = true; } if (this.presetName) { this.advanced = _.extend(Espo.Utils.clone(this.getPresetData()), searchData.advanced); + if (!primaryIsSet) { this.primary = this.getPrimaryFilterName(); } } else { this.advanced = Espo.Utils.clone(searchData.advanced); } + this.bool = searchData.bool; }, @@ -763,26 +848,49 @@ define('views/record/search', 'view', function (Dep) { params = params || {}; var rendered = false; + if (this.isRendered()) { rendered = true; - this.$advancedFiltersPanel.append(''); + + this.$advancedFiltersPanel.append( + '' + ); } this.createView('filter-' + name, 'views/search/filter', { name: name, model: this.model, params: params, - el: this.options.el + ' .filter[data-name="' + name + '"]' + el: this.options.el + ' .filter[data-name="' + name + '"]', }, function (view) { if (typeof callback === 'function') { view.once('after:render', function () { callback(view); }); } + if (rendered && !noRender) { view.render(); } - }.bind(this)); + + this.listenTo(view, 'change', function () { + var toShowApply = this.isSearchedWithAdvancedFilter; + + if (!toShowApply) { + var data = view.getView('field').fetchSearch(); + + if (data) { + toShowApply = true; + } + } + + if (!toShowApply) { + return; + } + + this.showApplyFiltersButton(); + }, this); + }); }, fetch: function () { @@ -799,7 +907,9 @@ define('views/record/search', 'view', function (Dep) { var data = {}; var method = 'fetch'; var view = this.getView('filter-' + field).getView('field'); + this.advanced[field] = view.fetchSearch(); + view.searchParams = this.advanced[field]; } }, @@ -810,7 +920,7 @@ define('views/record/search', 'view', function (Dep) { advanced: this.advanced, bool: this.bool, presetName: this.presetName, - primary: this.primary + primary: this.primary, }); }, @@ -819,15 +929,44 @@ define('views/record/search', 'view', function (Dep) { for (var i in this.moreFieldList) { var field = this.moreFieldList[i]; + var o = { name: field, checked: (field in this.advanced), }; + defs.push(o); } return defs; }, + showResetFiltersButton: function () { + this.toShowApplyFiltersButton = true; + this.toShowResetFiltersText = true; + + this.$applyFiltersContainer.removeClass('hidden'); + + this.$applyFiltersContainer.find('.text-apply').addClass('hidden'); + this.$applyFiltersContainer.find('.text-reset').removeClass('hidden'); + }, + + showApplyFiltersButton: function () { + this.toShowApplyFiltersButton = true; + this.toShowResetFiltersText = false; + + this.$applyFiltersContainer.removeClass('hidden'); + + this.$applyFiltersContainer.find('.text-reset').addClass('hidden'); + this.$applyFiltersContainer.find('.text-apply').removeClass('hidden'); + }, + + hideApplyFiltersButton: function () { + this.toShowApplyFiltersButton = false; + this.toShowResetFiltersText = false; + + this.$applyFiltersContainer.addClass('hidden'); + }, + }); }); diff --git a/client/src/views/search/filter.js b/client/src/views/search/filter.js index 2cea138b7c..b30ae0251f 100644 --- a/client/src/views/search/filter.js +++ b/client/src/views/search/filter.js @@ -26,7 +26,7 @@ * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. ************************************************************************/ -Espo.define('views/search/filter', 'view', function (Dep) { +define('views/search/filter', 'view', function (Dep) { return Dep.extend({ @@ -36,7 +36,7 @@ Espo.define('views/search/filter', 'view', function (Dep) { return { name: this.name, scope: this.model.name, - notRemovable: this.options.notRemovable + notRemovable: this.options.notRemovable, }; }, @@ -55,16 +55,27 @@ Espo.define('views/search/filter', 'view', function (Dep) { name: name, }, searchParams: this.options.params, + }, function (view) { + this.listenTo(view, 'change', function () { + this.trigger('change'); + }, this); }); } }, populateDefaults: function () { var view = this.getView('field'); - if (!view) return; - if (!('populateSearchDefaults' in view)) return; + + if (!view) { + return; + } + + if (!('populateSearchDefaults' in view)) { + return; + } + view.populateSearchDefaults(); - } + }, }); }); diff --git a/frontend/less/espo/custom.less b/frontend/less/espo/custom.less index 4163bf22bf..5b8e19bc12 100644 --- a/frontend/less/espo/custom.less +++ b/frontend/less/espo/custom.less @@ -2481,6 +2481,10 @@ table.table-admin-panel { } } +.advanced-filters-apply-container { + margin-bottom: 12px; +} + @media screen and (min-width: @screen-sm-min) { #global-search-panel { margin-top: 5px;