diff --git a/services/web/app/views/project/list/item.pug b/services/web/app/views/project/list/item.pug index fd3440ca7d..b860594a8c 100644 --- a/services/web/app/views/project/list/item.pug +++ b/services/web/app/views/project/list/item.pug @@ -32,7 +32,7 @@ div(class=titleClasses) ) × .col-xs-2 - span.owner {{ownerName()}} + span.owner {{getOwnerName(project)}} |   i.fa.fa-question-circle.small( ng-if="hasGenericOwnerName()" diff --git a/services/web/app/views/project/list/project-list.pug b/services/web/app/views/project/list/project-list.pug index 9ad6596157..7514a5737a 100644 --- a/services/web/app/views/project/list/project-list.pug +++ b/services/web/app/views/project/list/project-list.pug @@ -1,8 +1,5 @@ .row .col-xs-12(ng-cloak) - - - form.project-search.form-horizontal(role="form") .form-group.has-feedback.has-feedback-left.col-md-7.col-xs-12 input.form-control.col-md-7.col-xs-12( @@ -154,18 +151,18 @@ i.tablesort.fa(ng-class="getSortIconClass('name')" aria-hidden="true") button.sr-only(ng-click="changePredicate('name')") Sort by #{translate("title")} .col-xs-2 - span.header.clickable(ng-click="changePredicate('accessLevel')") #{translate("owner")} - i.tablesort.fa(ng-class="getSortIconClass('accessLevel')" aria-hidden="true") - button.sr-only(ng-click="changePredicate('accessLevel')") Sort by #{translate("owner")} + span.header.clickable(ng-click="changePredicate('ownerName')") #{translate("owner")} + i.tablesort.fa(ng-class="getSortIconClass('ownerName')" aria-hidden="true") + button.sr-only(ng-click="changePredicate('ownerName')") Sort by #{translate("owner")} div(class=lastUpdatedClasses) span.header.clickable(ng-click="changePredicate('lastUpdated')") #{translate("last_modified")} i.tablesort.fa(ng-class="getSortIconClass('lastUpdated')" aria-hidden="true") - button.sr-only(ng-click="changePredicate('accessLevel')") Sort by #{translate("last_modified")} + button.sr-only(ng-click="changePredicate('lastUpdated')") Sort by #{translate("last_modified")} if settings.overleaf .hidden-xs.col-sm-3.col-md-2.action-btn-row-header span.header #{translate("actions")} li.project_entry.container-fluid( - ng-repeat="project in visibleProjects | orderBy:predicate:reverse", + ng-repeat="project in visibleProjects | orderBy:getValueForCurrentPredicate:reverse:comparator", ng-controller="ProjectListItemController" ) .row( diff --git a/services/web/app/views/project/list/v1-item.pug b/services/web/app/views/project/list/v1-item.pug index caccdfb2ac..e7b0179f50 100644 --- a/services/web/app/views/project/list/v1-item.pug +++ b/services/web/app/views/project/list/v1-item.pug @@ -22,7 +22,7 @@ ) {{project.name}} .col-xs-2 - span.owner {{ownerName()}} + span.owner {{ownerName(project)}} .col-xs-4.col-sm-3.col-md-2 span.last-modified(tooltip="{{project.lastUpdated | formatDate}}") {{project.lastUpdated | fromNowDate}} \ No newline at end of file diff --git a/services/web/public/src/main/project-list/project-list.js b/services/web/public/src/main/project-list/project-list.js index 50a242b1dc..e27ec90003 100644 --- a/services/web/public/src/main/project-list/project-list.js +++ b/services/web/public/src/main/project-list/project-list.js @@ -12,7 +12,7 @@ * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -define(['base'], function(App) { +define(['base', 'main/project-list/services/project-list'], function(App) { App.controller('ProjectPageController', function( $scope, $modal, @@ -21,7 +21,8 @@ define(['base'], function(App) { queuedHttp, event_tracking, $timeout, - localStorage + localStorage, + ProjectListService ) { $scope.projects = window.data.projects $scope.tags = window.data.tags @@ -34,6 +35,10 @@ define(['base'], function(App) { $scope.nUntagged = 0 $scope.reverse = true $scope.searchText = { value: '' } + $scope.$watch('predicate', function(newValue) { + $scope.comparator = + newValue === 'ownerName' ? ownerNameComparator : defaultComparator + }) $scope.shouldShowSurveyLink = false const surveyStartDate = new Date(2019, 3, 2) @@ -89,6 +94,67 @@ define(['base'], function(App) { return ($scope.projectListHeight = height) } + function defaultComparator(v1, v2) { + var result = 0 + var type1 = v1.type + var type2 = v2.type + + if ($scope.predicate === 'ownerName') { + return + } + + if (type1 === type2) { + var value1 = v1.value + var value2 = v2.value + + if (type1 === 'string') { + // Compare strings case-insensitively + value1 = value1.toLowerCase() + value2 = value2.toLowerCase() + } else if (type1 === 'object') { + // For basic objects, use the position of the object + // in the collection instead of the value + if (angular.isObject(value1)) value1 = v1.index + if (angular.isObject(value2)) value2 = v2.index + } + + if (value1 !== value2) { + result = value1 < value2 ? -1 : 1 + } + } else { + result = type1 < type2 ? -1 : 1 + } + + return result + } + + function ownerNameComparator(v1, v2) { + if ($scope.predicate !== 'ownerName') { + return + } + if (v1.value === 'You') { + if (v2.value === 'You') { + return v1.index < v2.index ? -1 : 1 + } else { + return 1 + } + } else if (v1.value === 'An Overleaf v1 User' || v1.value === 'None') { + if (v2.value === 'An Overleaf v1 User' || v2.value === 'None') { + return v1.index < v2.index ? -1 : 1 + } else { + return -1 + } + } else { + if (v2.value === 'You') { + return -1 + } else if (v2.value === 'An Overleaf v1 User' || v2.value === 'None') { + return 1 + } else { + return v1.value > v2.value ? -1 : 1 + } + } + } + angular.element($window).bind('resize', function() { recalculateProjectListHeight() return $scope.$apply() @@ -599,6 +665,14 @@ define(['base'], function(App) { } } + $scope.getValueForCurrentPredicate = function(project) { + if ($scope.predicate === 'ownerName') { + return ProjectListService.getOwnerName(project) + } else { + return project[$scope.predicate] + } + } + $scope.openDeleteProjectsModal = function() { const modalInstance = $modal.open({ templateUrl: 'deleteProjectsModalTemplate', @@ -713,7 +787,8 @@ define(['base'], function(App) { return App.controller('ProjectListItemController', function( $scope, $modal, - queuedHttp + queuedHttp, + ProjectListService ) { $scope.shouldDisableCheckbox = project => $scope.filter === 'archived' && project.accessLevel !== 'owner' @@ -741,22 +816,7 @@ define(['base'], function(App) { return !first_name && !last_name && !email } - $scope.ownerName = function() { - if ($scope.project.accessLevel === 'owner') { - return 'You' - } else if ($scope.project.owner != null) { - const { first_name, last_name, email } = $scope.project.owner - if (first_name || last_name) { - return [first_name, last_name].filter(n => n != null).join(' ') - } else if (email) { - return email - } else { - return 'An Overleaf v1 User' - } - } else { - return 'None' - } - } + $scope.getOwnerName = ProjectListService.getOwnerName $scope.isOwner = () => window.user_id === $scope.project.owner._id diff --git a/services/web/public/src/main/project-list/services/project-list.js b/services/web/public/src/main/project-list/services/project-list.js new file mode 100644 index 0000000000..146a2c14a6 --- /dev/null +++ b/services/web/public/src/main/project-list/services/project-list.js @@ -0,0 +1,24 @@ +/* eslint-disable + camelcase +*/ +define(['base'], App => + App.service('ProjectListService', [ + () => ({ + getOwnerName(project) { + if (project.accessLevel === 'owner') { + return 'You' + } else if (project.owner != null) { + const { first_name, last_name, email } = project.owner + if (first_name || last_name) { + return [first_name, last_name].filter(n => n != null).join(' ') + } else if (email) { + return email + } else { + return 'An Overleaf v1 User' + } + } else { + return 'None' + } + } + }) + ]))