Merge pull request #1729 from overleaf/ta-show-last-modified

Show LastUpdatedBy on Dashboard

GitOrigin-RevId: 11760e575a46f061d4aa9059a0c1e813b9adb1f9
This commit is contained in:
Timothée Alby 2019-05-21 15:28:13 +02:00 committed by sharelatex
parent 759392047d
commit d86abf49e6
10 changed files with 344 additions and 202 deletions

View file

@ -188,7 +188,7 @@ module.exports = ProjectController =
notifications: (cb)->
NotificationsHandler.getUserNotifications user_id, cb
projects: (cb)->
ProjectGetter.findAllUsersProjects user_id, 'name lastUpdated publicAccesLevel archived owner_ref tokens', cb
ProjectGetter.findAllUsersProjects user_id, 'name lastUpdated lastUpdatedBy publicAccesLevel archived owner_ref tokens', cb
v1Projects: (cb) ->
Modules.hooks.fire "findAllV1Projects", user_id, (error, projects = []) ->
if error? and error instanceof V1ConnectionError
@ -225,7 +225,7 @@ module.exports = ProjectController =
if req.ip != user.lastLoginIp
NotificationsBuilder.ipMatcherAffiliation(user._id, req.ip).create()
ProjectController._injectProjectOwners projects, (error, projects) ->
ProjectController._injectProjectUsers projects, (error, projects) ->
return next(error) if error?
viewModel = {
title:'your_projects'
@ -429,6 +429,7 @@ module.exports = ProjectController =
id: project._id
name: project.name
lastUpdated: project.lastUpdated
lastUpdatedBy: project.lastUpdatedBy
publicAccessLevel: project.publicAccesLevel
accessLevel: accessLevel
source: source
@ -462,11 +463,13 @@ module.exports = ProjectController =
return projectViewModel
_injectProjectOwners: (projects, callback = (error, projects) ->) ->
_injectProjectUsers: (projects, callback = (error, projects) ->) ->
users = {}
for project in projects
if project.owner_ref?
users[project.owner_ref.toString()] = true
if project.lastUpdatedBy?
users[project.lastUpdatedBy.toString()] = true
jobs = []
for user_id, _ of users
@ -481,6 +484,8 @@ module.exports = ProjectController =
for project in projects
if project.owner_ref?
project.owner = users[project.owner_ref.toString()]
if project.lastUpdatedBy?
project.lastUpdatedBy = users[project.lastUpdatedBy.toString()] or null
callback null, projects
_buildWarningsList: (v1ProjectData = {}) ->

View file

@ -81,8 +81,8 @@ block content
include ../translations/translation_message
.project-list-content(event-tracking=settings.overleaf ? "loads_v2_dash" : "", onboard=settings.overleaf ? "true" : "", event-tracking-trigger=settings.overleaf ? "load" : "", event-tracking-mb="true", event-segmentation="{location: 'dash', v2_onboard: true}")
.row.project-list-row(ng-cloak)
.project-list-container(ng-if="projects.length > 0")
.project-list-row(ng-cloak)
.project-list-container.row(ng-if="projects.length > 0")
.project-list-sidebar-wrapper.col-md-2.col-xs-3
aside.project-list-sidebar
include ./list/side-bar
@ -114,7 +114,7 @@ block content
include ./list/notifications
include ./list/project-list
.project-list-empty(ng-if="projects.length === 0")
.project-list-empty.row(ng-if="projects.length === 0")
.col-md-offset-2.col-md-8.col-md-offset-2.col-xs-8
include ./list/empty-project-list

View file

@ -1,42 +1,40 @@
- var titleClasses = settings.overleaf ? "col-xs-6 col-sm-4 col-md-6" : "col-xs-6"
- var lastUpdatedClasses = settings.overleaf ? " col-xs-4 col-sm-3 col-md-2" : "col-xs-4"
div(class=titleClasses)
input.select-item(
select-individual,
type="checkbox",
ng-disabled="shouldDisableCheckbox(project)",
ng-model="project.selected"
stop-propagation="click"
aria-label=translate('select_project') + " '{{ project.name }}'"
)
span
a.projectName(
ng-href="{{projectLink(project)}}"
td.project-list-table-name-cell(ng-if-start="!project.isV1Project")
.project-list-table-name-container
input.project-list-table-select-item(
select-individual,
type="checkbox",
ng-disabled="shouldDisableCheckbox(project)",
ng-model="project.selected"
stop-propagation="click"
) {{project.name}}
span(
ng-controller="TagListController"
aria-label=translate('select_project') + " '{{ project.name }}'"
)
.tag-label(
ng-repeat='tag in project.tags'
span.project-list-table-name
a.project-list-table-name-link(
ng-href="{{projectLink(project)}}"
stop-propagation="click"
) {{project.name}}
span(
ng-controller="TagListController"
)
a.label.label-default.tag-label-name(
href,
ng-click="selectTag(tag)",
.tag-label(
ng-repeat='tag in project.tags'
stop-propagation="click"
)
i.fa.fa-circle(
aria-hidden="true"
ng-style="{ 'color': 'hsl({{ getHueForTagId(tag._id) }}, 70%, 45%)' }"
button.label.label-default.tag-label-name(
ng-click="selectTag(tag)"
aria-label="Select tag {{ tag.name }}"
)
| {{tag.name}}
a.label.label-default.tag-label-remove(
href
ng-click="removeProjectFromTag(project, tag)",
) ×
i.fa.fa-circle(
aria-hidden="true"
ng-style="{ 'color': 'hsl({{ getHueForTagId(tag._id) }}, 70%, 45%)' }"
)
| {{tag.name}}
button.label.label-default.tag-label-remove(
ng-click="removeProjectFromTag(project, tag)"
aria-label="Remove tag {{ tag.name }}"
) ×
.col-xs-2
td.project-list-table-owner-cell
span.owner {{getOwnerName(project)}}
|  
i.fa.fa-question-circle.small(
@ -55,71 +53,73 @@ div(class=titleClasses)
)
span.sr-only #{translate("link_sharing")}
div(class=lastUpdatedClasses)
if settings.overleaf
span.last-modified(tooltip="{{project.lastUpdated | formatDate}}") {{project.lastUpdated | fromNowDate}}
else
span.last-modified {{project.lastUpdated | formatDate}}
td.project-list-table-lastupdated-cell
span.last-modified(tooltip="{{project.lastUpdated | formatDate}}")
| {{project.lastUpdated | fromNowDate}}
span(ng-show='project.lastUpdatedBy')
|
| #{translate('by')}
| {{getUserName(project.lastUpdatedBy)}}
if settings.overleaf
.hidden-xs.col-sm-3.col-md-2.action-btn-row
div(
ng-if="!project.isTableActionInflight"
td.project-list-table-actions-cell(ng-if-end)
div(
ng-if="!project.isTableActionInflight"
)
button.btn.btn-link.action-btn(
aria-label=translate('copy'),
tooltip=translate('copy'),
tooltip-placement="top",
tooltip-append-to-body="true",
ng-click="clone($event)"
)
button.btn.btn-link.action-btn(
aria-label=translate('copy'),
tooltip=translate('copy'),
tooltip-placement="top",
tooltip-append-to-body="true",
ng-click="clone($event)"
)
i.icon.fa.fa-files-o(aria-hidden="true")
button.btn.btn-link.action-btn(
aria-label=translate('download'),
tooltip=translate('download'),
tooltip-placement="top",
tooltip-append-to-body="true",
ng-click="download($event)"
)
i.icon.fa.fa-cloud-download(aria-hidden="true")
button.btn.btn-link.action-btn(
ng-if="!project.archived && isOwner()"
aria-label=translate('archive'),
tooltip=translate('archive'),
tooltip-placement="top",
tooltip-append-to-body="true",
ng-click="archiveOrLeave($event)"
)
i.icon.fa.fa-inbox(aria-hidden="true")
button.btn.btn-link.action-btn(
ng-if="!isOwner()"
aria-label=translate('leave'),
tooltip=translate('leave'),
tooltip-placement="top",
tooltip-append-to-body="true",
ng-click="archiveOrLeave($event)"
)
i.icon.fa.fa-sign-out(aria-hidden="true")
button.btn.btn-link.action-btn(
ng-if="project.archived && isOwner()"
aria-label=translate('unarchive'),
tooltip=translate('unarchive'),
tooltip-placement="top",
tooltip-append-to-body="true",
ng-click="restore($event)"
)
i.icon.fa.fa-reply(aria-hidden="true")
button.btn.btn-link.action-btn(
ng-if="project.archived && isOwner()"
aria-label=translate('delete_forever'),
tooltip=translate('delete_forever'),
tooltip-placement="top",
tooltip-append-to-body="true",
ng-click="deleteProject($event)"
)
i.icon.fa.fa-trash(aria-hidden="true")
div(
ng-if="project.isTableActionInflight"
aria-label=translate('processing')
i.icon.fa.fa-files-o(aria-hidden="true")
button.btn.btn-link.action-btn(
aria-label=translate('download'),
tooltip=translate('download'),
tooltip-placement="top",
tooltip-append-to-body="true",
ng-click="download($event)"
)
i.fa.fa-spinner.fa-spin(aria-hidden="true")
i.icon.fa.fa-cloud-download(aria-hidden="true")
button.btn.btn-link.action-btn(
ng-if="!project.archived && isOwner()"
aria-label=translate('archive'),
tooltip=translate('archive'),
tooltip-placement="top",
tooltip-append-to-body="true",
ng-click="archiveOrLeave($event)"
)
i.icon.fa.fa-inbox(aria-hidden="true")
button.btn.btn-link.action-btn(
ng-if="!isOwner()"
aria-label=translate('leave'),
tooltip=translate('leave'),
tooltip-placement="top",
tooltip-append-to-body="true",
ng-click="archiveOrLeave($event)"
)
i.icon.fa.fa-sign-out(aria-hidden="true")
button.btn.btn-link.action-btn(
ng-if="project.archived && isOwner()"
aria-label=translate('unarchive'),
tooltip=translate('unarchive'),
tooltip-placement="top",
tooltip-append-to-body="true",
ng-click="restore($event)"
)
i.icon.fa.fa-reply(aria-hidden="true")
button.btn.btn-link.action-btn(
ng-if="project.archived && isOwner()"
aria-label=translate('delete_forever'),
tooltip=translate('delete_forever'),
tooltip-placement="top",
tooltip-append-to-body="true",
ng-click="deleteProject($event)"
)
i.icon.fa.fa-trash(aria-hidden="true")
div(
ng-if="project.isTableActionInflight"
aria-label=translate('processing')
)
i.fa.fa-spinner.fa-spin(aria-hidden="true")

View file

@ -22,7 +22,6 @@
ng-click="clearSearchText()"
ng-show="searchText.value.length > 0"
) #{translate('clear_search')}
//- i.fa.fa-remove
.project-tools(ng-cloak)
.btn-toolbar(ng-show="filter != 'archived'")
@ -36,17 +35,15 @@
ng-click="downloadSelectedProjects()"
)
i.fa.fa-cloud-download(aria-hidden="true")
- var archiveButtonString = settings.overleaf ? translate("archive") : translate("delete")
- var archiveButtonIcon = settings.overleaf ? "fa-inbox" : "fa-trash-o"
a.btn.btn-default(
href,
aria-label=`{{ isArchiveableProjectSelected ? '${archiveButtonString}' : '${translate("leave")}' }}`,
tooltip=`{{ isArchiveableProjectSelected ? '${archiveButtonString}' : '${translate("leave")}' }}`,
aria-label=`{{ isArchiveableProjectSelected ? 'translate("archive")' : '${translate("leave")}' }}`,
tooltip=`{{ isArchiveableProjectSelected ? 'translate("archive")' : '${translate("leave")}' }}`,
tooltip-placement="bottom",
tooltip-append-to-body="true",
ng-click="openArchiveProjectsModal()"
)
i.fa(ng-class=`isArchiveableProjectSelected ? '${archiveButtonIcon}' : 'fa-sign-out'` aria-hidden="true")
i.fa(ng-class=`isArchiveableProjectSelected ? 'fa-inbox' : 'fa-sign-out'` aria-hidden="true")
.btn-group.dropdown(ng-hide="selectedProjects.length < 1", dropdown)
a.btn.btn-default.dropdown-toggle(
@ -136,51 +133,41 @@
max-height="projectListHeight - 25",
ng-cloak
)
li.container-fluid
.row
- var titleClasses = settings.overleaf ? " col-xs-6 col-sm-4 col-md-6" : "col-xs-6"
- var lastUpdatedClasses = settings.overleaf ? " col-xs-4 col-sm-3 col-md-2" : "col-xs-4"
div(class=titleClasses)
input.select-all(
select-all,
type="checkbox"
aria-label=translate('select_all_projects')
)
span.header.clickable(ng-click="changePredicate('name')") #{translate("title")}
i.tablesort.fa(ng-class="getSortIconClass('name')" aria-hidden="true")
button.sr-only(ng-click="changePredicate('name')") Sort by #{translate("title")}
.col-xs-2
table.project-list-table
tr.project-list-table-header-row
th.project-list-table-name-cell
.project-list-table-name-container
input.project-list-table-select-item(
select-all,
type="checkbox"
aria-label=translate('select_all_projects')
)
span.header.clickable.project-list-table-name(ng-click="changePredicate('name')") #{translate("title")}
i.tablesort.fa(ng-class="getSortIconClass('name')" aria-hidden="true")
button.sr-only(ng-click="changePredicate('name')") Sort by #{translate("title")}
th.project-list-table-owner-cell
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)
th.project-list-table-lastupdated-cell
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('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:getValueForCurrentPredicate:reverse:comparator",
ng-controller="ProjectListItemController"
)
.row(
ng-if="!project.isV1Project"
th.project-list-table-actions-cell
span.header #{translate("actions")}
tr.project-list-table-row(
ng-repeat="project in visibleProjects | orderBy:getValueForCurrentPredicate:reverse:comparator",
ng-controller="ProjectListItemController"
select-row
)
include ./item
.row(
ng-if="project.isV1Project"
)
include ./v1-item
li(
ng-if="visibleProjects.length == 0",
ng-cloak
)
.row
.col-xs-12.text-centered
small #{translate("no_projects")}
tr(
ng-if="visibleProjects.length == 0",
ng-cloak
)
td(colspan="4").project-list-table-no-projects-cell
span.small #{translate("no_projects")}
div.welcome.text-centered(ng-if="projects.length == 0", ng-cloak)
h2 #{translate("welcome_to_sl")}

View file

@ -1,12 +1,12 @@
.col-xs-6.col-sm-4.col-md-6
.select-item
span.v1-badge(
aria-label=translate("v1_badge")
tooltip-template="'v1ProjectTooltipTemplate'"
tooltip-append-to-body="true"
)
span
if settings.overleaf && settings.overleaf.host
td.project-list-table-name-cell(ng-if-start="project.isV1Project")
.project-list-table-name-container
span.project-list-table-v1-badge-container
span.v1-badge(
aria-label=translate("v1_badge")
tooltip-template="'v1ProjectTooltipTemplate'"
tooltip-append-to-body="true"
)
span.project-list-table-name
if hasFeature('force-import-to-v2')
a.projectName(href='/{{project.id}}') {{project.name}}
else
@ -21,8 +21,11 @@
ng-hide="project.accessLevel == 'owner'"
) {{project.name}}
.col-xs-2
td.project-list-table-owner-cell
span.owner {{ownerName(project)}}
.col-xs-4.col-sm-3.col-md-2
td.project-list-table-lastupdated-cell(
ng-if-end
colspan="2"
)
span.last-modified(tooltip="{{project.lastUpdated | formatDate}}") {{project.lastUpdated | fromNowDate}}

View file

@ -788,6 +788,8 @@ define(['base', 'main/project-list/services/project-list'], function(App) {
$scope.getOwnerName = ProjectListService.getOwnerName
$scope.getUserName = ProjectListService.getUserName
$scope.isOwner = () => window.user_id === $scope.project.owner._id
$scope.$watch('project.selected', function(value) {

View file

@ -8,7 +8,17 @@ define(['base'], App =>
if (project.accessLevel === 'owner') {
return 'You'
} else if (project.owner != null) {
const { first_name, last_name, email } = project.owner
return this.getUserName(project.owner)
} else {
return 'None'
}
},
getUserName(user) {
if (user && user._id === window.user_id) {
return 'You'
} else if (user) {
const { first_name, last_name, email } = user
if (first_name || last_name) {
return [first_name, last_name].filter(n => n != null).join(' ')
} else if (email) {

View file

@ -35,7 +35,6 @@
}
.project-list-content when (@is-overleaf) {
.container-fluid;
margin: 0;
height: 100%;
}
@ -74,6 +73,7 @@
padding-top: @content-margin-vertical;
padding-bottom: @content-margin-vertical;
height: 100%;
margin-left: -(@grid-gutter-width / 2);
}
.project-header {
@ -101,6 +101,141 @@
}
}
.project-list-table {
width: 100%;
table-layout: fixed;
}
.project-list-table-header-row {
border-bottom: 1px solid @structured-list-border-color;
}
.project-list-table-row {
position: relative;
border-bottom: 1px solid @structured-list-border-color;
&:last-child {
border-bottom: 0 none;
}
&:hover {
background-color: @structured-list-hover-color;
}
&:first-child {
border-bottom-color: @structured-header-border-color;
&:hover {
background-color: transparent;
}
}
}
.project-list-table-name-cell,
.project-list-table-owner-cell,
.project-list-table-lastupdated-cell,
.project-list-table-actions-cell,
.project-list-table-no-projects-cell {
padding: (@line-height-computed / 4) 0;
vertical-align: top;
}
.project-list-table-no-projects-cell {
text-align: center;
}
.project-list-table-name-cell {
width: 50%;
padding-right: @line-height-computed / 2;
@media (min-width: @screen-md) {
width: 47%;
}
@media (min-width: @screen-lg) {
width: 50%;
}
}
.project-list-table-name-container {
position: relative;
overflow: hidden;
text-overflow: ellipsis;
}
// Extra specificity needed to override Bootstrap's own specificity.
input.project-list-table-select-item[type="checkbox"] {
position: absolute;
left: @line-height-computed - (@grid-gutter-width / 2);
margin-top: 5px;
}
.project-list-table-v1-badge-container {
position: absolute;
}
.project-list-table-name {
display: inline-block;
padding-left: @line-height-computed * 1.5;
vertical-align: top;
}
.project-list-table-name-link {
padding: 0;
}
.project-list-table-owner-cell {
width: 23%;
padding-right: @line-height-computed / 2;
overflow: hidden;
text-overflow: ellipsis;
@media (min-width: @screen-sm) {
width: 16%;
}
@media (min-width: @screen-md) {
width: 18%;
}
@media (min-width: @screen-lg) {
width: 16%;
}
}
.project-list-table-lastupdated-cell {
width: 27%;
padding-right: @line-height-computed / 2;
overflow: hidden;
text-overflow: ellipsis;
@media (min-width: @screen-sm) {
width: 19%;
}
@media (min-width: @screen-md) {
width: 24%;
}
}
.project-list-table-actions-cell {
display: none;
padding-right: @line-height-computed - (@grid-gutter-width / 2);
text-align: right;
white-space: nowrap;
@media (min-width: @screen-sm) {
display: table-cell;
width: 18%;
}
@media (min-width: @screen-md) {
width: 11%;
}
@media (min-width: @screen-lg) {
width: 10%;
}
}
.action-btn {
padding: 0 0.3em;
margin-left: 0.2em;
}
.first-project {
width: 127px;
text-align: center;
@ -333,53 +468,10 @@ ul.project-list {
text-align: left;
}
.tag-label {
margin-left: @line-height-computed / 4;
position: relative;
display: inline-block;
white-space: nowrap;
top: @tag-top-adjustment;
}
.tag-label-name,
.tag-label-remove {
display: inline-block;
padding: 3px 4px;
border-radius: @tag-border-radius;
background-color: @tag-bg-color;
color: @tag-color;
vertical-align: text-bottom;
&:hover,
&:focus {
color: @tag-color;
background-color: @tag-bg-hover-color;
}
}
.tag-label-name {
padding-right: 2px;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
max-width: @tag-max-width;
overflow: hidden;
text-overflow: ellipsis;
> .fa {
margin-right: 0.3em;
}
}
.tag-label-remove {
padding-left: 2px;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
.v1-badge {
margin-left: -4px;
}
.action-btn-row-header, .action-btn-row {
padding-right: 20px;
text-align: right;
}
.action-btn {
padding: 0 0.3em;
margin-left: 0.2em;
@ -390,6 +482,48 @@ ul.project-list {
}
}
.tag-label {
margin-left: @line-height-computed / 4;
position: relative;
display: inline-block;
white-space: nowrap;
top: @tag-top-adjustment;
}
// Extra specificity needed to override Bootstrap's own specificity.
.label.tag-label-name,
.label.tag-label-remove {
display: inline-block;
padding: 3px 4px;
border-radius: @tag-border-radius;
background-color: @tag-bg-color;
color: @tag-color;
vertical-align: text-bottom;
border-width: 0;
&:hover,
&:focus {
color: @tag-color;
background-color: @tag-bg-hover-color;
outline-width: 0;
}
}
.label.tag-label-name {
padding-right: 2px;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
max-width: @tag-max-width;
overflow: hidden;
text-overflow: ellipsis;
> .fa {
margin-right: 0.3em;
}
}
.label.tag-label-remove {
padding-left: 2px;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
.user_details_auto_complete {
ul>li{

View file

@ -2,7 +2,6 @@
height: 100%;
display: flex;
flex-direction: column;
margin: 0 -15px;
}
.project-list-sidebar {

View file

@ -125,6 +125,7 @@ describe "ProjectController", ->
"../BrandVariations/BrandVariationsHandler": @BrandVariationsHandler
'../Institutions/InstitutionsAPI':
getUserAffiliations: @getUserAffiliations
'../V1/V1Handler': {}
@projectName = "£12321jkj9ujkljds"
@req =
@ -276,7 +277,7 @@ describe "ProjectController", ->
@notifications = [{_id:'1',user_id:'2',templateKey:'3',messageOpts:'4',key:'5'}]
@projects = [
{_id:1, lastUpdated:1, owner_ref: "user-1"},
{_id:2, lastUpdated:2, owner_ref: "user-2"}
{_id:2, lastUpdated:2, owner_ref: "user-2", lastUpdatedBy: "user-1"}
]
@collabertions = [
{_id:5, lastUpdated:5, owner_ref: "user-1"}
@ -350,6 +351,7 @@ describe "ProjectController", ->
@res.render = (pageName, opts)=>
opts.projects[0].owner.should.equal (@users[@projects[0].owner_ref])
opts.projects[1].owner.should.equal (@users[@projects[1].owner_ref])
opts.projects[1].lastUpdatedBy.should.equal (@users[@projects[1].lastUpdatedBy])
done()
@ProjectController.projectListPage @req, @res