<commit_before>/*
 * Axelor Business Solutions
 *
 * Copyright (C) 2005-2016 Axelor (<http://axelor.com>).
 *
 * This program is free software: you can redistribute it and/or  modify
 * it under the terms of the GNU Affero General Public License, version 3,
 * as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
(function() {

"use strict";

var ui = angular.module('axelor.ui');

ui.controller('SearchViewCtrl', SearchViewCtrl);

SearchViewCtrl.$inject = ['$scope', '$element', '$http', 'DataSource', 'ViewService', 'MenuService'];
function SearchViewCtrl($scope, $element, $http, DataSource, ViewService, MenuService) {
	
	var view = $scope._views.search || {};
	
	$scope._dataSource = DataSource.create('multi-search');
	
	$scope.applyLater(function(){
		if (view.deferred)
			view.deferred.resolve($scope);
	}, 0);
	
	function fixFields(fields) {
		_.each(fields, function(field){
			if (field.type == 'reference') {
				field.type = 'MANY_TO_ONE';
				field.canNew = false;
				field.canEdit = false;
			}
			
			if (field.type)
				field.type = field.type.toUpperCase();
			else
				field.type = 'STRING';
		});
		return fields;
	}
	
	$scope.show = function(viewPromise) {
		if (!viewPromise) {
			viewPromise = $scope.loadView('search', view.name);
			viewPromise.success(function(fields, schema){
				$scope.initView(schema);
			});
		}
		
		$scope.onShow(viewPromise);
	};

	$scope.onShow = function() {
		
	};
	
	$scope.initView = function(schema) {
		
		var params = $scope._viewParams;
		
		$scope._searchFields = fixFields(schema.searchFields);
		$scope._resultFields = fixFields(schema.resultFields);

		$scope._searchView = schema;
		$scope._showSingle = params.params && params.params.showSingle;
		$scope._forceEdit = params.params && params.params.forceEdit;
		$scope._hideActions = params.params && params.params.hideActions;
		
		$scope.updateRoute();
		
		if (params.options && params.options.mode == "search") {
			$scope.setRouteOptions(params.options);
		}
		
	};
	
	$scope.getRouteOptions = function() {
		var args = [],
			query = $scope._routeSearch;

		return {
			mode: 'search',
			args: args,
			query: query
		};
	};

	$scope._routeSearch = null;
	var onNewCalled = false;
	$scope.setRouteOptions = function(options) {
		var opts = options || {},
			fields = $scope._searchFields || [],
			search = opts.search,
			record = {};
		
		var changed = !angular.equals($scope._routeSearch, search);
		
		$scope._routeSearch = search;
		
		if (!onNewCalled && _.isEmpty(search)) {
			onNewCalled = true;
			scopes.form.$broadcast('on:new');
		}
		if (!search || _.isEmpty(search) || !changed) {
			return $scope.updateRoute();
		}
		
		_.each(fields, function(field) {
			var value = search[field.name];
			if (value === undefined) {
				return;
			}
			if (field.target) {
				if (value) {
					record[field.name] = {id: +value};
				}
			} else {
				record[field.name] = value;
			}
		});
		
		if (search.objects) {
			scopes.toolbar.editRecord({
				objectSelect: search.objects
			});
		}
		
		scopes.form.editSearch(record, fields);
		
		function _doSearch() {
			var promise = $scope.doSearch();
			if (promise && promise.then && $scope._showSingle) {
				promise.then(function () {
					var items = scopes.grid.getItems();
					if (items && items.length === 1) {
						scopes.grid.selection = [0];
						scopes.grid.onEdit();
					}
				});
			}
		}
		
		var promise = scopes.toolbar._viewPromise;
		if (promise && promise.then) {
			promise.then(function() {
				$scope.$timeout(_doSearch);
			});
		} else {
			_doSearch();
		}
		
	};
	
	var scopes = {};
	$scope._register = function(key, scope) {
		scopes[key] = scope;
	};
	
	$scope.doSearch = function() {
		var params = _.extend({}, scopes.form.record),
			empty =  _.chain(params).values().compact().isEmpty().value();
		if (empty)
			return $scope.doClear();

		var selected = (scopes.toolbar.record || {}).objectSelect;
		
		_.extend(params,{
			__name: view.name,
			__selected: _.isEmpty(selected) ? null : selected.split(/,\s*/)
		});

		var promise = $http.post('ws/search', {
			limit: view.limit || 80,
			data: params
		});
		
		return promise.then(function(response){
			var res = response.data,
				records = res.data || [];
			
			// slickgrid expects unique `id` so generate them and store original one
			_.each(records, function(rec, i){
				rec._id = rec.id;
				rec.id = i + 1;
			});
			
			scopes.grid.setItems(records);
			
			if (_.isEmpty(records)) {
				axelor.notify.info(_t("No records found."));
			}
		});
		
	};
	
	$scope.doClear = function(all) {
		scopes.form.edit(null);
		scopes.form.$broadcast('on:new');
		scopes.grid.setItems([]);
		if (all) {
			scopes.toolbar.edit(null);
			scopes.toolbar.doReset();
		}
	};
	
	$scope.doAction = function() {
		
		var action = scopes.toolbar.getMenuAction();
		if (!action) {
			return;
		}
	
		var grid = scopes.grid,
			index = _.first(grid.selection),
			record = grid.getItem(index);
		
		action = action.action;
		record = _.extend({
			_action: action
		}, record);
		
		record.id = record._id;
		
		MenuService.action(action).success(function(result){

			if (!result.data) {
				return;
			}
			
			var view = result.data[0].view;
			var tab = view;

			tab.action = _.uniqueId('$act');
			tab.viewType = 'form';
			
			tab.context = _.extend({}, tab.context, {
				_ref : record
			});
			
			$scope.openTab(tab);
		});
	};
}

ui.controller('SearchFormCtrl', SearchFormCtrl);

SearchFormCtrl.$inject = ['$scope', '$element', 'ViewService'];
function SearchFormCtrl($scope, $element, ViewService) {
	
	ui.FormViewCtrl.call(this, $scope, $element);
	$scope._register('form', $scope);
	$scope.setEditable();
	
	// prevent requesting defaults
	$scope.defaultValues = {};
	
	$scope.$watch('_searchView', function(schema) {
		if (!schema) return;
		var form = {
			title: 'Search',
			type: 'form',
			cols: 1,
			items: [{
				type: 'panel',
				title: schema.title,
				items: schema.searchFields
			}]
		};

		var meta = { fields: schema.searchFields };
		ViewService.process(meta, schema.searchForm);
		
		function process(item) {
			if (item.items || item.pages) {
				return _.each(item.items || item.pages, process);
			}
			switch (item.widget) {
			case 'ManyToOne':
			case 'OneToOne':
			case 'SuggestBox':
				item.canNew = false;
				item.canEdit = false;
				break;
			case 'OneToMany':
			case 'ManyToMany':
			case 'MasterDetail':
				item.hidden = true;
			}
		}
		
		if (schema.searchForm && schema.searchForm.items) {
			_.each(schema.searchForm.items, process);
		}
		
		$scope.fields = meta.fields;
		$scope.schema = schema.searchForm || form;
		$scope.schema.loaded = true;
	});
	
	var model = null;
	var getContext = $scope.getContext;
	
	$scope.getContext = function() {
		var view = $scope._searchView || {};
		if (model === null && view.selects) {
			model = (_.first(view.selects) || {}).model;
		}
		
		var ctx = getContext.apply(this, arguments) || {};
		ctx._model = model;
		return ctx;
	};
	
	$scope.editSearch = function (record, fields) {
		$scope.editRecord(record);
		setTimeout(function () {
			_.each(fields, function (field) {
				if (!field.target || !$scope.record) return;
				var item = $element.find('[x-field=' + field.name + ']');
				var itemScope = item.data('$scope');
				var value = itemScope.getValue();
				if (value && itemScope && !itemScope.text && itemScope.select) {
					itemScope.select(value);
				}
			});
		});
	};
}

ui.controller('SearchGridCtrl', SearchGridCtrl);

SearchGridCtrl.$inject = ['$scope', '$element', 'ViewService', '$interpolate'];
function SearchGridCtrl($scope, $element, ViewService, $interpolate) {
	
	ui.GridViewCtrl.call(this, $scope, $element);
	$scope._register('grid', $scope);
	
	var viewTitles = {};

	$scope.$watch('_searchView', function(schema) {
		if (!schema) return;
		var view = {
			title: 'Search',
			type: 'grid',
			editIcon: true,
			items: []
		};
		
		var objItem = _.findWhere(schema.resultFields, {name: 'object'});
		if (!objItem) {
			view.items.push(objItem = {});
		}
		
		objItem = _.extend(objItem, { name : '_modelTitle', title: _t('Object') });
		view.items = view.items.concat(schema.resultFields);

		if (+(objItem.width) === 0) {
			objItem.hidden = true;
		}

		var meta = { fields: schema.resultFields };
		ViewService.process(meta);

		_.each(schema.selects, function (select) {
			viewTitles[select.model] = select.viewTitle;
		});

		_.each(view.items, function (item) {
			if (item.width) {
				objItem.width = objItem.width || 220;
			}
		});

		$scope.fields = meta.fields;
		$scope.schema = view;
		$scope.schema.loaded = true;
	});
	
	$scope.onEdit = function(force) {

		var index = _.first(this.selection),
			records = this.getItems(),
			record = this.getItem(index),
			ids, domain, views;

		ids = _.chain(records).filter(function(rec){
				return rec._model == record._model;
			}).pluck('_id').value();

		domain = "self.id IN (" + ids.join(',') + ")";
		
		views = _.map(['form', 'grid'], function(type){
			var view = { type : type };
			var name = record["_" + type];
			if (name) view.name = name;
			return view;
		});
		
		if (force === undefined) {
			force = $scope._forceEdit;
		}
		
		var title = viewTitles[record._model];
		if (title) {
			title = $interpolate(title)(record);
		}
		
		var tab = {
			action: _.uniqueId('$act'),
			model: record._model,
			title: title || record._modelTitle,
			forceTitle: true,
			domain: domain,
			recordId: record._id,
			forceEdit: force,
			viewType: 'form',
			views: views
		};
		
		this.openTab(tab);
	};

	$scope.onSort = function(event, args) {
		var grid = args.grid;
		var data = grid.getData();
		var sortCols = args.sortCols;
		
		var types = {};
		
		_.each($scope.fields, function (field) {
			types[field.name] = field.type;
		});

		data.sort(function(dataRow1, dataRow2) {
			for (var i = 0, l = sortCols.length; i < l; i++) {
				var name = sortCols[i].sortCol.field;
				var sign = sortCols[i].sortAsc ? 1 : -1;
				var value1 = dataRow1[name], value2 = dataRow2[name];
		
				switch (types[name]) {
				case "integer":
				case "long":
					value1 = value1 || 0;
					value2 = value2 || 0;
					break;
				default:
					value1 = value1 || "";
					value2 = value2 || "";
				}

				var result = (value1 == value2 ? 0 : (value1 > value2 ? 1 : -1)) * sign;
				if (result) {
					return result;
				}
			}
			return 0;
		});
		
		grid.invalidate();
		grid.render();
	};
}

ui.controller('SearchToolbarCtrl', SearchToolbarCtrl);

SearchToolbarCtrl.$inject = ['$scope', '$element', '$http'];
function SearchToolbarCtrl($scope, $element, $http) {

	ui.FormViewCtrl.call(this, $scope, $element);
	$scope._register('toolbar', $scope);
	$scope.setEditable();
	
	var menus = {};
	
	function fetch(key, parent, request, response) {
		
		if (menus[key]) {
			return response(menus[key]);
		}

		var promise = $http.get('ws/search/menu', {
			params: {
				parent: parent
			}
		});
		promise.then(function(res){
			var data = res.data.data;
			data = _.map(data, function(item){
				return {
					value: item.name,
					action: item.action,
					label: item.title
				};
			});
			menus[key] = data;
			response(data);
		});
	}
	
	$scope.fetchRootMenus = function(request, response) {
		fetch('menuRoot', null, request, response);
	};
	
	$scope.fetchSubMenus = function(request, response) {
		fetch('menuSub', $scope.record.menuRoot, request, response);
	};
	
	$scope.fetchItemMenus = function(request, response) {
		fetch('menuItem', $scope.record.menuSub, request, response);
	};
	
	$scope.resetSelector = function(a, b) {
		_.each(arguments, function(name){
			if (_.isString(name)) {
				$scope.record[name] = null;
				menus[name] = null;
			}
		});
	};
	
	$scope.getMenuAction = function() {
		return _.find(menus.menuItem, function(item){
			return item.value === $scope.record.menuItem;
		});
	};

	$scope.$watch('_searchView', function(schema) {
		
		if (!schema) {
			return;
		}

		var selected = [];

		$scope.fields = {
			'objectSelect' : {
				type : 'string',
				placeholder: _t('Search Objects'),
				multiple : true,
				selectionList : _.map(schema.selects, function(x) {
					if (x.selected) {
						selected.push(x.model);
					}
					return {
						value : x.model,
						title : x.title
					};
				})
			},
			'menuRoot' : {
				type : 'string',
				placeholder: _t('Action Category'),
				widget: 'select-query',
				attrs: {
					query: 'fetchRootMenus',
					'ng-change': 'resetSelector("menuSub", "menuItem")'
				}
			},
			'menuSub' : {
				placeholder: _t('Action Sub-Category'),
				widget: 'select-query',
				attrs: {
					query: 'fetchSubMenus',
					'ng-change': 'resetSelector($event, "menuItem")'
				}
			},
			'menuItem' : {
				placeholder: _t('Action'),
				widget: 'select-query',
				attrs: {
					query: 'fetchItemMenus'
				}
			}
		};

		var items1 = [{
			name : 'objectSelect',
			showTitle : false,
			colSpan: 8
		}, {
			type : 'button',
			title : _t('Search'),
			colSpan: 2,
			attrs: {
				'ng-click': 'doSearch()'
			}
		}, {
			type : 'button',
			title : _t('Clear'),
			colSpan: 2,
			attrs: {
				'ng-click': 'doClear(true)'
			}
		}];
		
		var items2 = [{
			name : 'menuRoot',
			showTitle : false,
			colSpan: 3
		}, {
			name : 'menuSub',
			showTitle : false,
			colSpan: 3
		}, {
			name : 'menuItem',
			showTitle : false,
			colSpan: 4
		}, {
			type : 'button',
			title : _t('Go'),
			colSpan: 2,
			attrs: {
				'ng-click': 'doAction()'
			}
		}];
		
		var item1 = {
			type: "panel",
			colSpan: $scope._hideActions ? 12 : 6,
			items: items1
		};
		
		var item2 = {
			type: "panel",
			colSpan: 6,
			items: items2
		};

		schema = {
			type : 'form',
			items: [{
				type: 'panel',
				items: $scope._hideActions ? [item1] : [item1, item2]
			}]
		};

		$scope.schema = schema;
		$scope.schema.loaded = true;

		$scope.doReset = function () {
			var record = $scope.record || {};
			if (selected.length > 0 && _.isEmpty(record.objectSelect)) {
				record.objectSelect = selected.join(', ');
				$scope.edit(record);
			}
		};
		
		$scope.$timeout($scope.doReset);
	});
}

angular.module('axelor.ui').directive('uiViewSearch', function(){
	return {
		controller: SearchViewCtrl,
		link: function(scope, element, attrs, ctrl) {
			
			element.on('keypress', '.search-view-form form:first', function(event){
				if (event.keyCode == 13 && $(event.target).is('input')){
					scope.doSearch();
				}
			});
			
			var grid = element.children('.search-view-grid');
			element.on('adjustSize', function(){
				if (!element.is(':visible'))
					return;
				grid.height(element.height() - grid.position().top);
			});
		}
	};
});

// ActionSelector (TODO: re-use search view toolbar)

ActionSelectorCtrl.$inject = ['$scope', '$element', '$attrs', '$http', 'MenuService'];
function ActionSelectorCtrl($scope, $element, $attrs, $http, MenuService) {

	ui.FormViewCtrl.call(this, $scope, $element);
	var menus = {},
		category = $attrs.category;
	
	function fetch(key, request, response, params) {
		
		if (menus[key]) {
			return response(menus[key]);
		}

		var promise = $http.get('ws/search/menu', {
			params: params
		});
		promise.then(function(res){
			var data = res.data.data;
			data = _.map(data, function(item){
				return {
					value: item.name,
					action: item.action,
					label: item.title
				};
			});
			menus[key] = data;
			response(data);
		});
	}
	
	$scope.fetchRootMenus = function(request, response) {
		fetch('$menuRoot', request, response, {
			parent: '',
			category: category
		});
	};
	
	$scope.fetchSubMenus = function(request, response) {
		if (!$scope.record.$menuRoot) return;
		fetch('$menuSub', request, response, {
			parent: $scope.record.$menuRoot
		});
	};
	
	$scope.fetchItemMenus = function(request, response) {
		if (!$scope.record.$menuSub) return;
		fetch('$menuItem', request, response, {
			parent: $scope.record.$menuSub
		});
	};
	
	$scope.resetSelector = function(a, b) {
		_.each(arguments, function(name){
			if (_.isString(name)) {
				$scope.record[name] = null;
				menus[name] = null;
			}
		});
	};
	
	$scope.getMenuAction = function() {
		return _.find(menus.$menuItem, function(item){
			return item.value === $scope.record.$menuItem;
		});
	};
	
	$scope.doAction = function() {
		
		var action = $scope.getMenuAction();
		if (!action) {
			return;
		}
	
		var context = $scope.$parent.getContext(),
			record;

		action = action.action;
		record = {
			id : context.id,
			_action: action,
			_model: context._model
		};

		MenuService.action(action).success(function(result){

			if (!result.data) {
				return;
			}
			
			var view = result.data[0].view;
			var tab = view;

			tab.action = _.uniqueId('$act');
			tab.viewType = 'form';
			
			tab.context = _.extend({}, tab.context, {
				_ref : record
			});
			
			$scope.openTab(tab);
		});
	};

	$scope.fields = {
		'$menuRoot' : {
			type : 'string',
			placeholder: _t('Action Category'),
			widget: 'select-query',
			attrs: {
				query: 'fetchRootMenus',
				'ng-change': 'resetSelector("$menuSub", "$menuItem")'
			}
		},
		'$menuSub' : {
			placeholder: _t('Action Sub-Category'),
			type: 'select-query',
			attrs: {
				query: 'fetchSubMenus',
				'ng-change': 'resetSelector($event, "$menuItem")'
			}
		},
		'$menuItem' : {
			placeholder: _t('Action'),
			widget: 'select-query',
			attrs: {
				query: 'fetchItemMenus'
			}
		}
	};

	$scope.schema = {
		cols : 4,
		colWidths : '30%,30%,30%,10%',
		type : 'form',
		items : [ {
			name : '$menuRoot',
			showTitle : false
		}, {
			name : '$menuSub',
			showTitle : false
		}, {
			name : '$menuItem',
			showTitle : false
		}, {
			type : 'button',
			title : _t('Go'),
			attrs: {
				'ng-click': 'doAction()'
			}
		} ]
	};
}

angular.module('axelor.ui').directive('uiActionSelector', function(){
	return {
		scope: true,
		controller: ActionSelectorCtrl,
		template: '<div ui-view-form x-handler="this"></div>'
	};
});

})();
<commit_msg>Add support for onLoad actions on multi-object search<commit_after>/*
 * Axelor Business Solutions
 *
 * Copyright (C) 2005-2016 Axelor (<http://axelor.com>).
 *
 * This program is free software: you can redistribute it and/or  modify
 * it under the terms of the GNU Affero General Public License, version 3,
 * as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
(function() {

"use strict";

var ui = angular.module('axelor.ui');

ui.controller('SearchViewCtrl', SearchViewCtrl);

SearchViewCtrl.$inject = ['$scope', '$element', '$http', 'DataSource', 'ViewService', 'MenuService'];
function SearchViewCtrl($scope, $element, $http, DataSource, ViewService, MenuService) {
	
	var view = $scope._views.search || {};
	
	$scope._dataSource = DataSource.create('multi-search');
	
	$scope.applyLater(function(){
		if (view.deferred)
			view.deferred.resolve($scope);
	}, 0);
	
	function fixFields(fields) {
		_.each(fields, function(field){
			if (field.type == 'reference') {
				field.type = 'MANY_TO_ONE';
				field.canNew = false;
				field.canEdit = false;
			}
			
			if (field.type)
				field.type = field.type.toUpperCase();
			else
				field.type = 'STRING';
		});
		return fields;
	}
	
	$scope.show = function(viewPromise) {
		if (!viewPromise) {
			viewPromise = $scope.loadView('search', view.name);
			viewPromise.success(function(fields, schema){
				$scope.initView(schema);
			});
		}
		
		$scope.onShow(viewPromise);
	};

	$scope.onShow = function() {
		
	};
	
	$scope.initView = function(schema) {
		
		var params = $scope._viewParams;
		
		$scope._searchFields = fixFields(schema.searchFields);
		$scope._resultFields = fixFields(schema.resultFields);

		$scope._searchView = schema;
		$scope._showSingle = params.params && params.params.showSingle;
		$scope._forceEdit = params.params && params.params.forceEdit;
		$scope._hideActions = params.params && params.params.hideActions;
		
		$scope.updateRoute();
		
		if (params.options && params.options.mode == "search") {
			$scope.setRouteOptions(params.options);
		}
		
	};
	
	$scope.getRouteOptions = function() {
		var args = [],
			query = $scope._routeSearch;

		return {
			mode: 'search',
			args: args,
			query: query
		};
	};

	$scope._routeSearch = null;
	var onNewCalled = false;
	$scope.setRouteOptions = function(options) {
		var opts = options || {},
			fields = $scope._searchFields || [],
			search = opts.search,
			record = {};
		
		var changed = !angular.equals($scope._routeSearch, search);
		
		$scope._routeSearch = search;
		
		if (!onNewCalled && _.isEmpty(search)) {
			onNewCalled = true;
			scopes.form.$broadcast('on:new');
		}
		if (!search || _.isEmpty(search) || !changed) {
			return $scope.updateRoute();
		}
		
		_.each(fields, function(field) {
			var value = search[field.name];
			if (value === undefined) {
				return;
			}
			if (field.target) {
				if (value) {
					record[field.name] = {id: +value};
				}
			} else {
				record[field.name] = value;
			}
		});
		
		if (search.objects) {
			scopes.toolbar.editRecord({
				objectSelect: search.objects
			});
		}
		
		scopes.form.editSearch(record, fields);
		
		function _doSearch() {
			var promise = $scope.doSearch();
			if (promise && promise.then && $scope._showSingle) {
				promise.then(function () {
					var items = scopes.grid.getItems();
					if (items && items.length === 1) {
						scopes.grid.selection = [0];
						scopes.grid.onEdit();
					}
				});
			}
		}
		
		var promise = scopes.toolbar._viewPromise;
		if (promise && promise.then) {
			promise.then(function() {
				$scope.$timeout(_doSearch);
			});
		} else {
			_doSearch();
		}
		
	};
	
	var scopes = {};
	$scope._register = function(key, scope) {
		scopes[key] = scope;
	};
	
	$scope.doSearch = function() {
		var params = _.extend({}, scopes.form.record),
			empty =  _.chain(params).values().compact().isEmpty().value();
		if (empty)
			return $scope.doClear();

		var selected = (scopes.toolbar.record || {}).objectSelect;
		
		_.extend(params,{
			__name: view.name,
			__selected: _.isEmpty(selected) ? null : selected.split(/,\s*/)
		});

		var promise = $http.post('ws/search', {
			limit: view.limit || 80,
			data: params
		});
		
		return promise.then(function(response){
			var res = response.data,
				records = res.data || [];
			
			// slickgrid expects unique `id` so generate them and store original one
			_.each(records, function(rec, i){
				rec._id = rec.id;
				rec.id = i + 1;
			});
			
			scopes.grid.setItems(records);
			
			if (scopes.form.$events.onLoad) {
				scopes.form.record._count = records.length;
				scopes.form.record._countByModels = _.countBy(records, function(rec) {
					return rec._model;
				});
				scopes.form.$events.onLoad();
			}
			
			if (_.isEmpty(records)) {
				axelor.notify.info(_t("No records found."));
			}
		});
		
	};
	
	$scope.doClear = function(all) {
		scopes.form.edit(null);
		scopes.form.$broadcast('on:new');
		scopes.grid.setItems([]);
		if (all) {
			scopes.toolbar.edit(null);
			scopes.toolbar.doReset();
		}
	};
	
	$scope.doAction = function() {
		
		var action = scopes.toolbar.getMenuAction();
		if (!action) {
			return;
		}
	
		var grid = scopes.grid,
			index = _.first(grid.selection),
			record = grid.getItem(index);
		
		action = action.action;
		record = _.extend({
			_action: action
		}, record);
		
		record.id = record._id;
		
		MenuService.action(action).success(function(result){

			if (!result.data) {
				return;
			}
			
			var view = result.data[0].view;
			var tab = view;

			tab.action = _.uniqueId('$act');
			tab.viewType = 'form';
			
			tab.context = _.extend({}, tab.context, {
				_ref : record
			});
			
			$scope.openTab(tab);
		});
	};
}

ui.controller('SearchFormCtrl', SearchFormCtrl);

SearchFormCtrl.$inject = ['$scope', '$element', 'ViewService'];
function SearchFormCtrl($scope, $element, ViewService) {
	
	ui.FormViewCtrl.call(this, $scope, $element);
	$scope._register('form', $scope);
	$scope.setEditable();
	
	// prevent requesting defaults
	$scope.defaultValues = {};
	
	$scope.$watch('_searchView', function(schema) {
		if (!schema) return;
		var form = {
			title: 'Search',
			type: 'form',
			cols: 1,
			items: [{
				type: 'panel',
				title: schema.title,
				items: schema.searchFields
			}]
		};

		var meta = { fields: schema.searchFields };
		ViewService.process(meta, schema.searchForm);
		
		function process(item) {
			if (item.items || item.pages) {
				return _.each(item.items || item.pages, process);
			}
			switch (item.widget) {
			case 'ManyToOne':
			case 'OneToOne':
			case 'SuggestBox':
				item.canNew = false;
				item.canEdit = false;
				break;
			case 'OneToMany':
			case 'ManyToMany':
			case 'MasterDetail':
				item.hidden = true;
			}
		}
		
		if (schema.searchForm && schema.searchForm.items) {
			_.each(schema.searchForm.items, process);
		}
		
		$scope.fields = meta.fields;
		$scope.schema = schema.searchForm || form;
		$scope.schema.loaded = true;
	});
	
	var model = null;
	var getContext = $scope.getContext;
	
	$scope.getContext = function() {
		var view = $scope._searchView || {};
		if (model === null && view.selects) {
			model = (_.first(view.selects) || {}).model;
		}
		
		var ctx = getContext.apply(this, arguments) || {};
		ctx._model = model;
		return ctx;
	};
	
	$scope.editSearch = function (record, fields) {
		$scope.editRecord(record);
		setTimeout(function () {
			_.each(fields, function (field) {
				if (!field.target || !$scope.record) return;
				var item = $element.find('[x-field=' + field.name + ']');
				var itemScope = item.data('$scope');
				var value = itemScope.getValue();
				if (value && itemScope && !itemScope.text && itemScope.select) {
					itemScope.select(value);
				}
			});
		});
	};
}

ui.controller('SearchGridCtrl', SearchGridCtrl);

SearchGridCtrl.$inject = ['$scope', '$element', 'ViewService', '$interpolate'];
function SearchGridCtrl($scope, $element, ViewService, $interpolate) {
	
	ui.GridViewCtrl.call(this, $scope, $element);
	$scope._register('grid', $scope);
	
	var viewTitles = {};

	$scope.$watch('_searchView', function(schema) {
		if (!schema) return;
		var view = {
			title: 'Search',
			type: 'grid',
			editIcon: true,
			items: []
		};
		
		var objItem = _.findWhere(schema.resultFields, {name: 'object'});
		if (!objItem) {
			view.items.push(objItem = {});
		}
		
		objItem = _.extend(objItem, { name : '_modelTitle', title: _t('Object') });
		view.items = view.items.concat(schema.resultFields);

		if (+(objItem.width) === 0) {
			objItem.hidden = true;
		}

		var meta = { fields: schema.resultFields };
		ViewService.process(meta);

		_.each(schema.selects, function (select) {
			viewTitles[select.model] = select.viewTitle;
		});

		_.each(view.items, function (item) {
			if (item.width) {
				objItem.width = objItem.width || 220;
			}
		});

		$scope.fields = meta.fields;
		$scope.schema = view;
		$scope.schema.loaded = true;
	});
	
	$scope.onEdit = function(force) {

		var index = _.first(this.selection),
			records = this.getItems(),
			record = this.getItem(index),
			ids, domain, views;

		ids = _.chain(records).filter(function(rec){
				return rec._model == record._model;
			}).pluck('_id').value();

		domain = "self.id IN (" + ids.join(',') + ")";
		
		views = _.map(['form', 'grid'], function(type){
			var view = { type : type };
			var name = record["_" + type];
			if (name) view.name = name;
			return view;
		});
		
		if (force === undefined) {
			force = $scope._forceEdit;
		}
		
		var title = viewTitles[record._model];
		if (title) {
			title = $interpolate(title)(record);
		}
		
		var tab = {
			action: _.uniqueId('$act'),
			model: record._model,
			title: title || record._modelTitle,
			forceTitle: true,
			domain: domain,
			recordId: record._id,
			forceEdit: force,
			viewType: 'form',
			views: views
		};
		
		this.openTab(tab);
	};

	$scope.onSort = function(event, args) {
		var grid = args.grid;
		var data = grid.getData();
		var sortCols = args.sortCols;
		
		var types = {};
		
		_.each($scope.fields, function (field) {
			types[field.name] = field.type;
		});

		data.sort(function(dataRow1, dataRow2) {
			for (var i = 0, l = sortCols.length; i < l; i++) {
				var name = sortCols[i].sortCol.field;
				var sign = sortCols[i].sortAsc ? 1 : -1;
				var value1 = dataRow1[name], value2 = dataRow2[name];
		
				switch (types[name]) {
				case "integer":
				case "long":
					value1 = value1 || 0;
					value2 = value2 || 0;
					break;
				default:
					value1 = value1 || "";
					value2 = value2 || "";
				}

				var result = (value1 == value2 ? 0 : (value1 > value2 ? 1 : -1)) * sign;
				if (result) {
					return result;
				}
			}
			return 0;
		});
		
		grid.invalidate();
		grid.render();
	};
}

ui.controller('SearchToolbarCtrl', SearchToolbarCtrl);

SearchToolbarCtrl.$inject = ['$scope', '$element', '$http'];
function SearchToolbarCtrl($scope, $element, $http) {

	ui.FormViewCtrl.call(this, $scope, $element);
	$scope._register('toolbar', $scope);
	$scope.setEditable();
	
	var menus = {};
	
	function fetch(key, parent, request, response) {
		
		if (menus[key]) {
			return response(menus[key]);
		}

		var promise = $http.get('ws/search/menu', {
			params: {
				parent: parent
			}
		});
		promise.then(function(res){
			var data = res.data.data;
			data = _.map(data, function(item){
				return {
					value: item.name,
					action: item.action,
					label: item.title
				};
			});
			menus[key] = data;
			response(data);
		});
	}
	
	$scope.fetchRootMenus = function(request, response) {
		fetch('menuRoot', null, request, response);
	};
	
	$scope.fetchSubMenus = function(request, response) {
		fetch('menuSub', $scope.record.menuRoot, request, response);
	};
	
	$scope.fetchItemMenus = function(request, response) {
		fetch('menuItem', $scope.record.menuSub, request, response);
	};
	
	$scope.resetSelector = function(a, b) {
		_.each(arguments, function(name){
			if (_.isString(name)) {
				$scope.record[name] = null;
				menus[name] = null;
			}
		});
	};
	
	$scope.getMenuAction = function() {
		return _.find(menus.menuItem, function(item){
			return item.value === $scope.record.menuItem;
		});
	};

	$scope.$watch('_searchView', function(schema) {
		
		if (!schema) {
			return;
		}

		var selected = [];

		$scope.fields = {
			'objectSelect' : {
				type : 'string',
				placeholder: _t('Search Objects'),
				multiple : true,
				selectionList : _.map(schema.selects, function(x) {
					if (x.selected) {
						selected.push(x.model);
					}
					return {
						value : x.model,
						title : x.title
					};
				})
			},
			'menuRoot' : {
				type : 'string',
				placeholder: _t('Action Category'),
				widget: 'select-query',
				attrs: {
					query: 'fetchRootMenus',
					'ng-change': 'resetSelector("menuSub", "menuItem")'
				}
			},
			'menuSub' : {
				placeholder: _t('Action Sub-Category'),
				widget: 'select-query',
				attrs: {
					query: 'fetchSubMenus',
					'ng-change': 'resetSelector($event, "menuItem")'
				}
			},
			'menuItem' : {
				placeholder: _t('Action'),
				widget: 'select-query',
				attrs: {
					query: 'fetchItemMenus'
				}
			}
		};

		var items1 = [{
			name : 'objectSelect',
			showTitle : false,
			colSpan: 8
		}, {
			type : 'button',
			title : _t('Search'),
			colSpan: 2,
			attrs: {
				'ng-click': 'doSearch()'
			}
		}, {
			type : 'button',
			title : _t('Clear'),
			colSpan: 2,
			attrs: {
				'ng-click': 'doClear(true)'
			}
		}];
		
		var items2 = [{
			name : 'menuRoot',
			showTitle : false,
			colSpan: 3
		}, {
			name : 'menuSub',
			showTitle : false,
			colSpan: 3
		}, {
			name : 'menuItem',
			showTitle : false,
			colSpan: 4
		}, {
			type : 'button',
			title : _t('Go'),
			colSpan: 2,
			attrs: {
				'ng-click': 'doAction()'
			}
		}];
		
		var item1 = {
			type: "panel",
			colSpan: $scope._hideActions ? 12 : 6,
			items: items1
		};
		
		var item2 = {
			type: "panel",
			colSpan: 6,
			items: items2
		};

		schema = {
			type : 'form',
			items: [{
				type: 'panel',
				items: $scope._hideActions ? [item1] : [item1, item2]
			}]
		};

		$scope.schema = schema;
		$scope.schema.loaded = true;

		$scope.doReset = function () {
			var record = $scope.record || {};
			if (selected.length > 0 && _.isEmpty(record.objectSelect)) {
				record.objectSelect = selected.join(', ');
				$scope.edit(record);
			}
		};
		
		$scope.$timeout($scope.doReset);
	});
}

angular.module('axelor.ui').directive('uiViewSearch', function(){
	return {
		controller: SearchViewCtrl,
		link: function(scope, element, attrs, ctrl) {
			
			element.on('keypress', '.search-view-form form:first', function(event){
				if (event.keyCode == 13 && $(event.target).is('input')){
					scope.doSearch();
				}
			});
			
			var grid = element.children('.search-view-grid');
			element.on('adjustSize', function(){
				if (!element.is(':visible'))
					return;
				grid.height(element.height() - grid.position().top);
			});
		}
	};
});

// ActionSelector (TODO: re-use search view toolbar)

ActionSelectorCtrl.$inject = ['$scope', '$element', '$attrs', '$http', 'MenuService'];
function ActionSelectorCtrl($scope, $element, $attrs, $http, MenuService) {

	ui.FormViewCtrl.call(this, $scope, $element);
	var menus = {},
		category = $attrs.category;
	
	function fetch(key, request, response, params) {
		
		if (menus[key]) {
			return response(menus[key]);
		}

		var promise = $http.get('ws/search/menu', {
			params: params
		});
		promise.then(function(res){
			var data = res.data.data;
			data = _.map(data, function(item){
				return {
					value: item.name,
					action: item.action,
					label: item.title
				};
			});
			menus[key] = data;
			response(data);
		});
	}
	
	$scope.fetchRootMenus = function(request, response) {
		fetch('$menuRoot', request, response, {
			parent: '',
			category: category
		});
	};
	
	$scope.fetchSubMenus = function(request, response) {
		if (!$scope.record.$menuRoot) return;
		fetch('$menuSub', request, response, {
			parent: $scope.record.$menuRoot
		});
	};
	
	$scope.fetchItemMenus = function(request, response) {
		if (!$scope.record.$menuSub) return;
		fetch('$menuItem', request, response, {
			parent: $scope.record.$menuSub
		});
	};
	
	$scope.resetSelector = function(a, b) {
		_.each(arguments, function(name){
			if (_.isString(name)) {
				$scope.record[name] = null;
				menus[name] = null;
			}
		});
	};
	
	$scope.getMenuAction = function() {
		return _.find(menus.$menuItem, function(item){
			return item.value === $scope.record.$menuItem;
		});
	};
	
	$scope.doAction = function() {
		
		var action = $scope.getMenuAction();
		if (!action) {
			return;
		}
	
		var context = $scope.$parent.getContext(),
			record;

		action = action.action;
		record = {
			id : context.id,
			_action: action,
			_model: context._model
		};

		MenuService.action(action).success(function(result){

			if (!result.data) {
				return;
			}
			
			var view = result.data[0].view;
			var tab = view;

			tab.action = _.uniqueId('$act');
			tab.viewType = 'form';
			
			tab.context = _.extend({}, tab.context, {
				_ref : record
			});
			
			$scope.openTab(tab);
		});
	};

	$scope.fields = {
		'$menuRoot' : {
			type : 'string',
			placeholder: _t('Action Category'),
			widget: 'select-query',
			attrs: {
				query: 'fetchRootMenus',
				'ng-change': 'resetSelector("$menuSub", "$menuItem")'
			}
		},
		'$menuSub' : {
			placeholder: _t('Action Sub-Category'),
			type: 'select-query',
			attrs: {
				query: 'fetchSubMenus',
				'ng-change': 'resetSelector($event, "$menuItem")'
			}
		},
		'$menuItem' : {
			placeholder: _t('Action'),
			widget: 'select-query',
			attrs: {
				query: 'fetchItemMenus'
			}
		}
	};

	$scope.schema = {
		cols : 4,
		colWidths : '30%,30%,30%,10%',
		type : 'form',
		items : [ {
			name : '$menuRoot',
			showTitle : false
		}, {
			name : '$menuSub',
			showTitle : false
		}, {
			name : '$menuItem',
			showTitle : false
		}, {
			type : 'button',
			title : _t('Go'),
			attrs: {
				'ng-click': 'doAction()'
			}
		} ]
	};
}

angular.module('axelor.ui').directive('uiActionSelector', function(){
	return {
		scope: true,
		controller: ActionSelectorCtrl,
		template: '<div ui-view-form x-handler="this"></div>'
	};
});

})();

{'fraction_non_alphanumeric': 0.1743937714579222, 'fraction_numerical': 0.0043794436484550295, 'mean_word_length': 3.064608284803868, 'pattern_counts': {'":': 4, '<': 13, '<?xml version=': 0, '>': 15, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2973415', 'n_tokens_mistral': 15398, 'n_tokens_neox': 12560, 'n_words': 2603}