Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/app/filters.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,25 @@ angular.module('supportAdminApp')
return 'https://connect.topcoder.com//projects/' + id + '/timeline';
};
})

// The resulting link will point to the public member profile,
// use 'tcPrivateProfileLink' instead to link to the private profile.
.filter('tcProfileLink', function () {
return function (id) {
if (id && id !== 'unassigned')
return 'http://www.topcoder.com/tc?module=MemberProfile&cr=' + id;
return 'javascript:;';
};
})

.filter('tcPrivateProfileLink', function () {
return function (handle) {
if (handle)
return 'http://www.topcoder.com/tc?module=LegacyReport&t=profile&ha=' + handle;
return 'javascript:;';
};
})

.filter('tcDirectLink', function () {
return function (id) {
return 'https://www.topcoder.com/direct/projectOverview?formData.projectId=' + id;
Expand Down
178 changes: 107 additions & 71 deletions src/app/work/project.list.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,80 +70,116 @@ module.controller('ProjectListCtrl', [
search: vm.formSearch.search,
status: vm.formSearch.status === ALL ? '' : vm.formSearch.status
}).then(

// For the loaded project objects this function does:
// 1. Queries user email and handles for ids of all projects' copilots
// and owners. The queries are send in batches of size equal to
// MAX_BATCH_SIZE ids per query.
// 2. If 'vm.formSearch.filterByHandleOrEmail' is true, it filters
// loaded project object by handle and emails;
// 3. Redraws the table.

function (projects) {

// Generates the query to search for members with 'usedId' specified
// in 'ownerId' and 'copilotId' field of each project we have found.
// The 'isNaN' check is necessary as 'copilotId' may be equal to
// "unassigned", or some other string values.
var query = projects.reduce(function(query, project) {
if (!isNaN(project.ownerId))
query += 'userId:' + project.ownerId + ' OR ';
if (!isNaN(project.copilotId))
query += 'userId:' + project.copilotId + ' OR ';
return query;
}, '').slice(0,-4);

vm.allProjects = projects;
$scope.projects = vm.allProjects;

// Resolution of User IDs in the found project objects.
membersService.search({
search: query
}).then(
function (users) {

// Uses the found 'users' array to add 'copilotHandle' and
// 'ownerHandle' field to each project object.
var map = users.reduce(function(map, user) {
map[user.userId] = user;
return map;
}, {});
projects.forEach(function(project) {
project.copilot = map[project.copilotId];
project.owner = map[project.ownerId];
});
var MAX_BATCH_SIZE = 100;

// If asked, filters the projects by owner's handle and
// and email (remaining projects will contain
// 'vm.formSearch.handleOrEmail' as a substring
// of their owner's handle or email).
if (vm.filterByHandleOrEmail) {
vm.filterByHandleOrEmail = false;
var key = vm.formSearch.handleOrEmail;
projects = projects.filter(function(project) {
var res = false;
res |= project.owner && project.owner.handle &&
project.owner.handle.includes(key);
res |= project.owner && project.owner.email &&
project.owner.email.includes(key);
res |= project.copilot && project.copilot.handle &&
project.copilot.handle.includes(key);
res |= project.copilot && project.copilot.email &&
project.copilot.email.includes(key);
return res;
});
vm.allProjects = projects;
$scope.projects = projects;
}
$timeout(function () {
vm.formSearch.isLoading = false;
$('.table').trigger('footable_redraw');
}, 500);
// Collects copilot and owner's ids from the loaded project objects.
// The resulting 'queue' array will hold unique user ids to be
// be queried from the MembersService in several batches, and
// the 'users' dictionary will be prepared to collect queiried data.
var queue = [], collect = function (users, userId) {
if (!isNaN(userId) && !users[userId]) {
users[userId] = { email: '', handle: ''};
queue.push(userId);
}
};
var users = projects.reduce(function(users, project) {

// Ids should be trimmed, as in the production data some of them
// may include spaces before and after, which breaks matching of
// ids width handles.
if (project.copilotId) {
project.copilotId = project.copilotId.trim();
}
if (project.ownerId) {
project.ownerId = project.ownerId.trim();
}

},
function (error) {
collect(users, project.copilotId);
collect(users, project.ownerId);
return users;
}, {});

// Even in case of failure with the request to the Member
// Service we still should update the page as the project's
// data have been loaded (but not the user handles).

$timeout(function () {
vm.formSearch.isLoading = false;
$('.table').trigger('footable_redraw');
}, 500);
// Now we send queries to MembersService in batches.
// The MAX_BATCH_SIZE constant sets the maximal batch size
// (i.e. the number of ids in a single query).
var pos = 0, queryNextBatch = function() {
var query = '';
var end = Math.min(pos + MAX_BATCH_SIZE, queue.length);
for (var i = pos; i < end; ++i) {
query += 'userId:' + queue[i] + ' OR ';
}
membersService.search({
search: query.slice(0, -4)
}).then(
function (userObjects) {
pos += MAX_BATCH_SIZE;
userObjects.forEach(function(user) {
users[user.userId] = {
email: user.email,
handle: user.handle
};
});
if (pos < queue.length) {
queryNextBatch();
} else {
finalize();
}
},
function (error) {
finalize();
}
);
};

// Once all necessary data have been queried, this function
// will be used to attach the queried user data to the loaded
// project object, and to redraw the table.
var finalize = function() {

projects.forEach(function(project) {
project.copilot = users[project.copilotId];
project.owner = users[project.ownerId];
});

// If asked, filters the projects by owner's handle and
// and email (remaining projects will contain
// 'vm.formSearch.handleOrEmail' as a substring
// of their owner's handle or email).
if (vm.formSearch.filterByHandleOrEmail) {
vm.formSearch.filterByHandleOrEmail = false;
var key = vm.formSearch.handleOrEmail;
projects = projects.filter(function(project) {
var res = false;
res |= project.owner && project.owner.handle &&
project.owner.handle.includes(key);
res |= project.owner && project.owner.email &&
project.owner.email.includes(key);
res |= project.copilot && project.copilot.handle &&
project.copilot.handle.includes(key);
res |= project.copilot && project.copilot.email &&
project.copilot.email.includes(key);
return res;
});
}
);

vm.allProjects = projects;
$scope.projects = projects;
vm.formSearch.isLoading = false;
$('.table').trigger('footable_redraw');
};

queryNextBatch();
},
function (error) {
// error handling
Expand All @@ -166,9 +202,9 @@ module.controller('ProjectListCtrl', [
* with an additionally activated 'handle or e-mail' filter.
*/
vm.findByHandleOrEmail = function() {
vm.filterByHandleOrEmail = true;
vm.search = '';
vm.status = ALL;
vm.formSearch.filterByHandleOrEmail = true;
vm.formSearch.search = '';
vm.formSearch.status = ALL;
vm.findProjects();
};

Expand Down
4 changes: 2 additions & 2 deletions src/app/work/projects.html
Original file line number Diff line number Diff line change
Expand Up @@ -137,15 +137,15 @@ <h2>Projects</h2>
<td>{{proj.id}}</td>
<td>{{proj.projectType }}</td>
<td>
<a ng-href="{{ proj.ownerId | tcProfileLink }}">
<a ng-href="{{ proj.owner.handle | tcPrivateProfileLink }}">
<!-- In case the owner's member object has not been found,
we show his ID as the fallback. -->
{{proj.owner && proj.owner.handle ?
proj.owner.handle : proj.ownerId}}
</a>
</td>
<td>
<a data-ng-if="proj.copilotId && proj.copilotId !== 'unassigned'" ng-href="{{ proj.copilotId | tcProfileLink}}">
<a data-ng-if="proj.copilotId && proj.copilotId !== 'unassigned'" ng-href="{{ proj.copilot.handle | tcPrivateProfileLink}}">
<!-- In case the copilot's member object has not been
found, we show his ID as the fallback. -->
{{proj.copilot && proj.copilot.handle ?
Expand Down