Refactor the entity-search directive/control to make its interface much cleaner and to add support for ng-model validity checking

This commit is contained in:
Joseph Schorr 2014-07-18 13:45:08 -04:00
parent df7b8d651c
commit 3865e3b1b7
8 changed files with 234 additions and 155 deletions

View file

@ -999,7 +999,14 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
{
'id': 'quay_notification',
'title': 'Quay.io notification',
'icon': 'quay-icon'
'icon': 'quay-icon',
'fields': [
{
'name': 'target',
'type': 'entity',
'title': 'Recipient'
}
]
},
{
'id': 'email',
@ -3232,48 +3239,72 @@ quayApp.directive('entitySearch', function () {
replace: false,
transclude: false,
restrict: 'C',
require: '?ngModel',
link: function(scope, element, attr, ctrl) {
scope.ngModel = ctrl;
},
scope: {
'namespace': '=namespace',
'inputTitle': '=inputTitle',
'entitySelected': '=entitySelected',
'includeTeams': '=includeTeams',
'isOrganization': '=isOrganization',
'isPersistent': '=isPersistent',
'placeholder': '=placeholder',
// Default: ['user', 'team', 'robot']
'allowedEntities': '=allowedEntities',
'currentEntity': '=currentEntity',
'clearNow': '=clearNow',
'filter': '=filter',
'entitySelected': '&entitySelected',
// 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',
},
controller: function($scope, $element, Restangular, UserService, ApiService) {
controller: function($rootScope, $scope, $element, Restangular, UserService, ApiService) {
$scope.lazyLoading = true;
$scope.teams = null;
$scope.robots = null;
$scope.isAdmin = false;
$scope.isOrganization = false;
$scope.includeTeams = true;
$scope.includeRobots = true;
$scope.currentEntityInternal = $scope.currentEntity;
var isSupported = function(kind, opt_array) {
return $.inArray(kind, opt_array || $scope.allowedEntities || ['user', 'team', 'robot']) >= 0;
};
$scope.lazyLoad = function() {
if (!$scope.namespace || !$scope.lazyLoading) { return; }
// Determine whether we can admin this namespace.
$scope.isAdmin = UserService.isNamespaceAdmin($scope.namespace);
// If the scope is an organization and we are not part of it, then nothing more we can do.
if (!$scope.isAdmin && $scope.isOrganization && !UserService.getOrganization($scope.namespace)) {
$scope.teams = null;
$scope.robots = null;
$scope.lazyLoading = false;
return;
}
if ($scope.isOrganization && $scope.includeTeams) {
// Reset the cached teams and robots.
$scope.teams = null;
$scope.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 = resp.teams;
});
}
ApiService.getRobots($scope.isOrganization ? $scope.namespace : null).then(function(resp) {
$scope.robots = resp.robots;
// Load the user/organization's robots (if applicable).
if ($scope.isAdmin && isSupported('robot')) {
ApiService.getRobots($scope.isOrganization ? $scope.namespace : null).then(function(resp) {
$scope.robots = resp.robots;
$scope.lazyLoading = false;
}, function() {
$scope.lazyLoading = false;
});
} else {
$scope.lazyLoading = false;
}, function() {
$scope.lazyLoading = false;
});
}
};
$scope.createTeam = function() {
@ -3321,7 +3352,7 @@ quayApp.directive('entitySearch', function () {
'is_robot': is_robot
};
if ($scope.is_organization) {
if ($scope.isOrganization) {
entity['is_org_member'] = true;
}
@ -3331,146 +3362,184 @@ quayApp.directive('entitySearch', function () {
$scope.clearEntityInternal = function() {
$scope.currentEntityInternal = null;
$scope.currentEntity = null;
if ($scope.entitySelected) {
$scope.entitySelected(null);
$scope.entitySelected({'entity': null});
if ($scope.ngModel) {
$scope.ngModel.$setValidity('entity', false);
}
};
$scope.setEntityInternal = function(entity, updateTypeahead) {
if (updateTypeahead) {
$(input).typeahead('val', $scope.isPersistent ? entity.name : '');
$(input).typeahead('val', $scope.autoClear ? '' : entity.name);
} else {
$(input).val($scope.isPersistent ? entity.name : '');
$(input).val($scope.autoClear ? '' : entity.name);
}
if ($scope.isPersistent) {
if (!$scope.autoClear) {
$scope.currentEntityInternal = entity;
$scope.currentEntity = entity;
}
if ($scope.entitySelected) {
$scope.entitySelected(entity);
$scope.entitySelected({'entity': entity});
if ($scope.ngModel) {
$scope.ngModel.$setValidity('entity', !!entity);
}
};
number++;
// 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'
}
return url;
},
filter: function(data) {
var datums = [];
for (var i = 0; i < data.results.length; ++i) {
var entity = data.results[i];
var entitySearchB = new Bloodhound({
name: 'entities' + number,
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.includeTeams) {
url += '&includeTeams=true'
}
return url;
},
filter: function(data) {
var datums = [];
for (var i = 0; i < data.results.length; ++i) {
var entity = data.results[i];
if ($scope.filter) {
var allowed = $scope.filter;
var found = 'user';
if (entity.kind == 'user') {
found = entity.is_robot ? 'robot' : 'user';
} else if (entity.kind == 'team') {
found = 'team';
}
if (allowed.indexOf(found)) {
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
}, {
source: entitySearchB.ttAdapter(),
templates: {
'empty': 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 (val.indexOf('@') > 0) {
return '<div class="tt-empty">A Quay.io username (not an e-mail address) must be specified</div>';
}
var classes = [];
if (isSupported('user')) { classes.push('users'); }
if ($scope.isAdmin && isSupported('robot')) { classes.push('robot accounts'); }
if ($scope.isOrganization && isSupported('team')) { classes.push('teams'); }
if (classes.length > 1) {
classes[classes.length - 1] = 'or ' + classes[classes.length - 1];
}
var class_string = '';
for (var i = 0; i < classes.length; ++i) {
if (i > 0) {
if (i == classes.length - 1) {
class_string += ' or ';
} else {
class_string += ', ';
}
}
class_string += classes[i];
}
return '<div class="tt-empty">No matching Quay.io ' + class_string + ' found</div>';
}
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();
return null;
},
'suggestion': function (datum) {
template = '<div class="entity-mini-listing">';
if (datum.entity.kind == 'user' && !datum.entity.is_robot) {
template += '<i class="fa fa-user fa-lg"></i>';
} else if (datum.entity.kind == 'user' && datum.entity.is_robot) {
template += '<i class="fa fa-wrench fa-lg"></i>';
} else if (datum.entity.kind == 'team') {
template += '<i class="fa fa-group fa-lg"></i>';
}
template += '<span class="name">' + datum.value + '</span>';
var counter = 0;
var input = $element[0].firstChild.firstChild;
$(input).typeahead({
'highlight': true
}, {
source: entitySearchB.ttAdapter(),
templates: {
'empty': 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 (datum.entity.is_org_member === false && datum.entity.kind == 'user') {
template += '<i class="fa fa-exclamation-triangle" title="User is outside the organization"></i>';
}
if (val.indexOf('@') > 0) {
return '<div class="tt-empty">A Quay.io username (not an e-mail address) must be specified</div>';
}
template += '</div>';
return template;
}}
});
var robots = $scope.isOrganization ? ', robot accounts' : '';
var teams = ($scope.includeTeams && $scope.isOrganization) ? ' or teams' : '';
return '<div class="tt-empty">No matching Quay.io users' + robots + teams + ' found</div>';
}
return null;
},
'suggestion': function (datum) {
template = '<div class="entity-mini-listing">';
if (datum.entity.kind == 'user' && !datum.entity.is_robot) {
template += '<i class="fa fa-user fa-lg"></i>';
} else if (datum.entity.kind == 'user' && datum.entity.is_robot) {
template += '<i class="fa fa-wrench fa-lg"></i>';
} else if (datum.entity.kind == 'team') {
template += '<i class="fa fa-group fa-lg"></i>';
}
template += '<span class="name">' + datum.value + '</span>';
if (datum.entity.is_org_member === false && datum.entity.kind == 'user') {
template += '<i class="fa fa-exclamation-triangle" title="User is outside the organization"></i>';
}
template += '</div>';
return template;
}}
});
$(input).on('input', function(e) {
$scope.$apply(function() {
if ($scope.isPersistent) {
$(input).on('input', function(e) {
$scope.$apply(function() {
$scope.clearEntityInternal();
}
});
});
});
$(input).on('typeahead:selected', function(e, datum) {
$scope.$apply(function() {
$scope.setEntityInternal(datum.entity, true);
$(input).on('typeahead:selected', function(e, datum) {
$scope.$apply(function() {
$scope.setEntityInternal(datum.entity, true);
});
});
});
})();
$scope.$watch('clearValue', function() {
if (!input) { return; }
$scope.$watch('clearNow', function() {
$(input).typeahead('val', '');
$scope.clearEntityInternal();
});
$scope.$watch('inputTitle', function(title) {
$scope.$watch('placeholder', function(title) {
input.setAttribute('placeholder', title);
});
$scope.$watch('allowedEntities', function(allowed) {
if (!allowed) { return; }
$scope.includeTeams = isSupported('team', allowed);
$scope.includeRobots = isSupported('robot', allowed);
});
$scope.$watch('namespace', function(namespace) {
if (!namespace) { return; }
$scope.isAdmin = UserService.isNamespaceAdmin(namespace);
$scope.isOrganization = !!UserService.getOrganization(namespace);
});
$scope.$watch('currentEntity', function(entity) {
if ($scope.currentEntityInternal != entity) {
if (entity) {