/** * An element which displays a box to search for an entity (org, user, robot, team). This control * allows for filtering of the entities found and whether to allow selection by e-mail. */ angular.module('quay').directive('entitySearch', function () { var number = 0; var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/entity-search.html', replace: false, transclude: false, restrict: 'C', require: '?ngModel', link: function(scope, element, attr, ctrl) { scope.ngModel = ctrl; }, scope: { 'namespace': '=namespace', 'placeholder': '=placeholder', 'forRepository': '=forRepository', 'skipPermissions': '=skipPermissions', // Default: ['user', 'team', 'robot'] 'allowedEntities': '=allowedEntities', 'currentEntity': '=currentEntity', 'entitySelected': '&entitySelected', 'emailSelected': '&emailSelected', // When set to true, the contents of the control will be cleared as soon // as an entity is selected. 'autoClear': '=autoClear', // Set this property to immediately clear the contents of the control. 'clearValue': '=clearValue', // Whether e-mail addresses are allowed. 'allowEmails': '=allowEmails', 'emailMessage': '@emailMessage', // True if the menu should pull right. 'pullRight': '@pullRight' }, controller: function($rootScope, $scope, $element, Restangular, UserService, ApiService, UtilService, AvatarService, Config) { $scope.lazyLoading = true; $scope.teams = null; $scope.page = {}; $scope.page.robots = null; $scope.isAdmin = false; $scope.isOrganization = false; $scope.includeTeams = true; $scope.includeRobots = true; $scope.includeOrgs = false; $scope.currentEntityInternal = $scope.currentEntity; $scope.createRobotInfo = null; $scope.createTeamInfo = null; $scope.Config = Config; var isSupported = function(kind, opt_array) { return $.inArray(kind, opt_array || $scope.allowedEntities || ['user', 'team', 'robot']) >= 0; }; $scope.lazyLoad = function() { if (!$scope.namespace || !$scope.thisUser || !$scope.lazyLoading) { return; } $scope.isAdmin = UserService.isNamespaceAdmin($scope.namespace); $scope.isOrganization = !!UserService.getOrganization($scope.namespace); // Reset the cached teams and robots. $scope.teams = null; $scope.page.robots = null; // Load the organization's teams (if applicable). if ($scope.isOrganization && isSupported('team')) { // Note: We load the org here again so that we always have the fully up-to-date // teams list. ApiService.getOrganization(null, {'orgname': $scope.namespace}).then(function(resp) { $scope.teams = Object.keys(resp.teams).map(function(key) { return resp.teams[key]; }); }); } // Load the user/organization's robots (if applicable). if ($scope.isAdmin && isSupported('robot')) { ApiService.getRobots($scope.isOrganization ? $scope.namespace : null).then(function(resp) { $scope.page.robots = resp.robots; $scope.lazyLoading = false; }, function() { $scope.lazyLoading = false; }); } else { $scope.lazyLoading = false; } }; $scope.askCreateTeam = function() { $scope.createTeamInfo = { 'namespace': $scope.namespace, 'repository': $scope.forRepository, 'skip_permissions': $scope.skipPermissions }; }; $scope.askCreateRobot = function() { $scope.createRobotInfo = { 'namespace': $scope.namespace, 'repository': $scope.forRepository, 'skip_permissions': $scope.skipPermissions }; }; $scope.handleTeamCreated = function(created) { $scope.setEntity(created.name, 'team', false, created.avatar); $scope.teams.push(created); }; $scope.handleRobotCreated = function(created) { $scope.setEntity(created.name, 'user', true, created.avatar); $scope.page.robots.push(created); }; $scope.setEntity = function(name, kind, is_robot, avatar) { var entity = { 'name': name, 'kind': kind, 'is_robot': is_robot, 'avatar': avatar }; if ($scope.isOrganization) { entity['is_org_member'] = true; } $scope.setEntityInternal(entity, false); }; $scope.clearEntityInternal = function() { $scope.currentEntityInternal = null; $scope.currentEntity = null; $scope.entitySelected({'entity': null}); if ($scope.ngModel) { $scope.ngModel.$setValidity('entity', false); } }; $scope.setEntityInternal = function(entity, updateTypeahead) { // If the entity is an external entity, convert it to a known user via an API call. if (entity.kind == 'external') { var params = { 'username': entity.name }; ApiService.linkExternalUser(null, params).then(function(resp) { $scope.setEntityInternal(resp['entity'], updateTypeahead); }, ApiService.errorDisplay('Could not link external user')); return; } if (updateTypeahead) { $(input).typeahead('val', $scope.autoClear ? '' : entity.name); } else { $(input).val($scope.autoClear ? '' : entity.name); } if (!$scope.autoClear) { $scope.currentEntityInternal = entity; $scope.currentEntity = entity; } $scope.entitySelected({'entity': entity}); if ($scope.ngModel) { $scope.ngModel.$setValidity('entity', !!entity); } }; // Setup the typeahead. var input = $element[0].firstChild.firstChild; (function() { // Create the bloodhound search query system. $rootScope.__entity_search_counter = (($rootScope.__entity_search_counter || 0) + 1); var entitySearchB = new Bloodhound({ name: 'entities' + $rootScope.__entity_search_counter, remote: { url: '/api/v1/entities/%QUERY', replace: function (url, uriEncodedQuery) { var namespace = $scope.namespace || ''; url = url.replace('%QUERY', uriEncodedQuery); url += '?namespace=' + encodeURIComponent(namespace); if ($scope.isOrganization && isSupported('team')) { url += '&includeTeams=true' } if (isSupported('org')) { url += '&includeOrgs=true' } return url; }, filter: function(data) { var datums = []; for (var i = 0; i < data.results.length; ++i) { var entity = data.results[i]; var found = 'user'; if (entity.kind == 'user' || entity.kind == 'external') { found = entity.is_robot ? 'robot' : 'user'; } else if (entity.kind == 'team') { found = 'team'; } else if (entity.kind == 'org') { found = 'org'; } if (!isSupported(found)) { continue; } datums.push({ 'value': entity.name, 'tokens': [entity.name], 'entity': entity }); } return datums; } }, datumTokenizer: function(d) { return Bloodhound.tokenizers.whitespace(d.val); }, queryTokenizer: Bloodhound.tokenizers.whitespace }); entitySearchB.initialize(); // Setup the typeahead. $(input).typeahead({ 'highlight': true, 'hint': false, }, { display: 'value', source: entitySearchB.ttAdapter(), templates: { 'notFound': function(info) { // Only display the empty dialog if the server load has finished. if (info.resultKind == 'remote') { var val = $(input).val(); if (!val) { return null; } if (UtilService.isEmailAddress(val)) { if ($scope.allowEmails) { return '
'; } else { return '