/** * 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.requiresLazyLoading = true; $scope.isLazyLoading = false; $scope.userRequestedLazyLoading = false; $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; }; var resetCache = function() { $scope.requiresLazyLoading = true; $scope.teams = null; $scope.page.robots = null; }; $scope.lazyLoad = function() { $scope.userRequestedLazyLoading = true; $scope.checkLazyLoad(); }; $scope.checkLazyLoad = function() { if (!$scope.namespace || !$scope.thisUser || !$scope.requiresLazyLoading || $scope.isLazyLoading || !$scope.userRequestedLazyLoading) { return; } $scope.isLazyLoading = true; $scope.isAdmin = UserService.isNamespaceAdmin($scope.namespace); $scope.isOrganization = !!UserService.getOrganization($scope.namespace); // Reset the cached teams and robots, just to be sure. $scope.teams = null; $scope.page.robots = null; var requiredOperations = 0; var operationComplete = function() { requiredOperations--; if (requiredOperations <= 0) { $scope.isLazyLoading = false; $scope.requiresLazyLoading = false; } }; // Load the organization's teams (if applicable). if ($scope.isOrganization && isSupported('team')) { requiredOperations++; // Note: We load the org here directly 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]; }); operationComplete(); }, operationComplete); } // Load the user/organization's robots (if applicable). if ($scope.isAdmin && isSupported('robot')) { requiredOperations++; var params = { 'token': false, 'limit': 20 }; ApiService.getRobots($scope.isOrganization ? $scope.namespace : null, null, params).then(function(resp) { $scope.page.robots = resp.robots; operationComplete(); }, operationComplete); } if (requiredOperations == 0) { operationComplete(); } }; $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); if (created.new_team) { $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 (query_url, uriEncodedQuery) { $scope.lazyLoad(); var namespace = $scope.namespace || ''; var url = UtilService.getRestUrl(query_url.replace('%QUERY', uriEncodedQuery)); url.setQueryParameter('namespace', namespace); if ($scope.isOrganization && isSupported('team')) { url.setQueryParameter('includeTeams', true); } if (isSupported('org')) { url.setQueryParameter('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 '