Merge pull request #900 from sharelatex/ja-show-last-modified

Record and show last modified by user for projects
This commit is contained in:
James Allen 2018-09-13 13:19:25 +01:00 committed by GitHub
commit 078575b236
17 changed files with 410 additions and 344 deletions

View file

@ -7,6 +7,7 @@ HistoryManager = require "./HistoryManager"
ProjectDetailsHandler = require "../Project/ProjectDetailsHandler" ProjectDetailsHandler = require "../Project/ProjectDetailsHandler"
ProjectEntityUpdateHandler = require "../Project/ProjectEntityUpdateHandler" ProjectEntityUpdateHandler = require "../Project/ProjectEntityUpdateHandler"
RestoreManager = require "./RestoreManager" RestoreManager = require "./RestoreManager"
ProjectUpdateHandler = require "../Project/ProjectUpdateHandler"
module.exports = HistoryController = module.exports = HistoryController =
selectHistoryApi: (req, res, next = (error) ->) -> selectHistoryApi: (req, res, next = (error) ->) ->
@ -143,3 +144,11 @@ module.exports = HistoryController =
error = new Error("history api responded with non-success code: #{response.statusCode}") error = new Error("history api responded with non-success code: #{response.statusCode}")
logger.error err: error, "project-history api responded with non-success code: #{response.statusCode}" logger.error err: error, "project-history api responded with non-success code: #{response.statusCode}"
callback(error) callback(error)
setLastUpdated: (req, res, next) ->
{project_id} = req.params
{user_id, timestamp} = req.body
logger.log {project_id, user_id, timestamp}, 'updating last updated date'
ProjectUpdateHandler.markAsUpdated project_id, user_id, timestamp, (error) ->
return next(error) if error?
res.sendStatus 200

View file

@ -184,7 +184,7 @@ module.exports = ProjectController =
notifications: (cb)-> notifications: (cb)->
NotificationsHandler.getUserNotifications user_id, cb NotificationsHandler.getUserNotifications user_id, cb
projects: (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) -> v1Projects: (cb) ->
Modules.hooks.fire "findAllV1Projects", user_id, (error, projects = []) -> Modules.hooks.fire "findAllV1Projects", user_id, (error, projects = []) ->
if error? and error instanceof V1ConnectionError if error? and error instanceof V1ConnectionError
@ -392,6 +392,7 @@ module.exports = ProjectController =
id: project._id id: project._id
name: project.name name: project.name
lastUpdated: project.lastUpdated lastUpdated: project.lastUpdated
lastUpdatedBy: project.lastUpdatedBy
publicAccessLevel: project.publicAccesLevel publicAccessLevel: project.publicAccesLevel
accessLevel: accessLevel accessLevel: accessLevel
source: source source: source
@ -430,6 +431,8 @@ module.exports = ProjectController =
for project in projects for project in projects
if project.owner_ref? if project.owner_ref?
users[project.owner_ref.toString()] = true users[project.owner_ref.toString()] = true
if project.lastUpdatedBy?
users[project.lastUpdatedBy] = true
jobs = [] jobs = []
for user_id, _ of users for user_id, _ of users
@ -444,6 +447,8 @@ module.exports = ProjectController =
for project in projects for project in projects
if project.owner_ref? if project.owner_ref?
project.owner = users[project.owner_ref.toString()] project.owner = users[project.owner_ref.toString()]
if project.lastUpdatedBy?
project.lastUpdatedBy = users[project.lastUpdatedBy.toString()]
callback null, projects callback null, projects
_buildWarningsList: (v1ProjectData = {}) -> _buildWarningsList: (v1ProjectData = {}) ->

View file

@ -114,8 +114,6 @@ module.exports = ProjectEntityUpdateHandler = self =
logger.log {project_id, doc_id, modified}, "finished updating doc lines" logger.log {project_id, doc_id, modified}, "finished updating doc lines"
# path will only be present if the doc is not deleted # path will only be present if the doc is not deleted
if modified && !isDeletedDoc if modified && !isDeletedDoc
# Don't need to block for marking as updated
ProjectUpdateHandler.markAsUpdated project_id
TpdsUpdateSender.addDoc {project_id:project_id, path:path.fileSystem, doc_id:doc_id, project_name:project.name, rev:rev}, callback TpdsUpdateSender.addDoc {project_id:project_id, path:path.fileSystem, doc_id:doc_id, project_name:project.name, rev:rev}, callback
else else
callback() callback()

View file

@ -2,30 +2,25 @@ Project = require('../../models/Project').Project
logger = require('logger-sharelatex') logger = require('logger-sharelatex')
module.exports = module.exports =
markAsUpdated : (project_id, callback)-> markAsUpdated : (project_id, user_id, timestamp, callback)->
conditions = {_id:project_id} conditions = {_id:project_id}
update = {lastUpdated:Date.now()} update = {
Project.update conditions, update, {}, (err)-> lastUpdated: new Date(timestamp),
if callback? lastUpdatedBy: user_id
callback() }
Project.update conditions, update, {}, callback
markAsOpened : (project_id, callback)-> markAsOpened : (project_id, callback)->
conditions = {_id:project_id} conditions = {_id:project_id}
update = {lastOpened:Date.now()} update = {lastOpened:Date.now()}
Project.update conditions, update, {}, (err)-> Project.update conditions, update, {}, callback
if callback?
callback()
markAsInactive: (project_id, callback)-> markAsInactive: (project_id, callback)->
conditions = {_id:project_id} conditions = {_id:project_id}
update = {active:false} update = {active:false}
Project.update conditions, update, {}, (err)-> Project.update conditions, update, {}, callback
if callback?
callback()
markAsActive: (project_id, callback)-> markAsActive: (project_id, callback)->
conditions = {_id:project_id} conditions = {_id:project_id}
update = {active:true} update = {active:true}
Project.update conditions, update, {}, (err)-> Project.update conditions, update, {}, callback
if callback?
callback()

View file

@ -19,6 +19,7 @@ DeletedFileSchema = new Schema
ProjectSchema = new Schema ProjectSchema = new Schema
name : {type:String, default:'new project'} name : {type:String, default:'new project'}
lastUpdated : {type:Date, default: () -> new Date()} lastUpdated : {type:Date, default: () -> new Date()}
lastUpdatedBy : {type:ObjectId, ref: 'User'}
lastOpened : {type:Date} lastOpened : {type:Date}
active : { type: Boolean, default: true } active : { type: Boolean, default: true }
owner_ref : {type:ObjectId, ref:'User'} owner_ref : {type:ObjectId, ref:'User'}

View file

@ -238,6 +238,7 @@ module.exports = class Router
webRouter.post '/project/:project_id/doc/:doc_id/restore', AuthorizationMiddlewear.ensureUserCanWriteProjectContent, HistoryController.restoreDocFromDeletedDoc webRouter.post '/project/:project_id/doc/:doc_id/restore', AuthorizationMiddlewear.ensureUserCanWriteProjectContent, HistoryController.restoreDocFromDeletedDoc
webRouter.post "/project/:project_id/restore_file", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, HistoryController.restoreFileFromV2 webRouter.post "/project/:project_id/restore_file", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, HistoryController.restoreFileFromV2
privateApiRouter.post "/project/:Project_id/history/resync", AuthenticationController.httpAuth, HistoryController.resyncProjectHistory privateApiRouter.post "/project/:Project_id/history/resync", AuthenticationController.httpAuth, HistoryController.resyncProjectHistory
privateApiRouter.post "/project/:project_id/last_updated", AuthenticationController.httpAuth, HistoryController.setLastUpdated
webRouter.get "/project/:Project_id/labels", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.selectHistoryApi, HistoryController.ensureProjectHistoryEnabled, HistoryController.getLabels webRouter.get "/project/:Project_id/labels", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.selectHistoryApi, HistoryController.ensureProjectHistoryEnabled, HistoryController.getLabels
webRouter.post "/project/:Project_id/labels", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, HistoryController.selectHistoryApi, HistoryController.ensureProjectHistoryEnabled, HistoryController.createLabel webRouter.post "/project/:Project_id/labels", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, HistoryController.selectHistoryApi, HistoryController.ensureProjectHistoryEnabled, HistoryController.createLabel

View file

@ -1,104 +1,123 @@
- var titleClasses = settings.overleaf ? "col-xs-6 col-sm-4 col-md-6" : "col-xs-6" td.selectProject
- var lastUpdatedClasses = settings.overleaf ? " col-xs-4 col-sm-3 col-md-2" : "col-xs-4" input.select-item(
ng-if="!project.isV1Project",
select-individual,
type="checkbox",
ng-disabled="shouldDisableCheckbox(project)",
ng-model="project.selected"
stop-propagation="click"
aria-label=translate('select_project') + " '{{ project.name }}'"
)
span.v1-badge(
ng-if="project.isV1Project",
aria-label=translate("v1_badge")
tooltip-template="'v1ProjectTooltipTemplate'"
tooltip-append-to-body="true"
)
td.projectName
span(ng-if="project.isV1Project")
if settings.overleaf && settings.overleaf.host
button.btn.btn-link.projectName(
ng-click="openV1ImportModal(project)"
stop-propagation="click"
ng-show="project.accessLevel == 'owner'"
) {{project.name}}
a.projectName(
href=settings.overleaf.host + "/{{project.id}}"
target="_blank"
ng-hide="project.accessLevel == 'owner'"
) {{project.name}}
span(ng-if="!project.isV1Project")
a.projectName(
ng-href="{{projectLink(project)}}"
stop-propagation="click"
) {{project.name}}
span(
ng-controller="TagListController"
)
.tag-label(
ng-repeat='tag in project.tags'
stop-propagation="click"
)
a.label.label-default.tag-label-name(
href,
ng-click="selectTag(tag)"
) {{tag.name}}
a.label.label-default.tag-label-remove(
href
ng-click="removeProjectFromTag(project, tag)"
) ×
div(class=titleClasses) td
input.select-item( span.owner {{userDisplayName(project.owner)}}
select-individual, span(ng-if="isLinkSharingProject(project)")
type="checkbox", |  
ng-disabled="shouldDisableCheckbox(project)", i.fa.fa-link.small(
ng-model="project.selected" tooltip=translate("link_sharing")
stop-propagation="click" tooltip-placement="right"
aria-label=translate('select_project') + " '{{ project.name }}'" tooltip-append-to-body="true"
) )
span
a.projectName(
ng-href="{{projectLink(project)}}"
stop-propagation="click"
) {{project.name}}
span(
ng-controller="TagListController"
)
.tag-label(
ng-repeat='tag in project.tags'
stop-propagation="click"
)
a.label.label-default.tag-label-name(
href,
ng-click="selectTag(tag)"
) {{tag.name}}
a.label.label-default.tag-label-remove(
href
ng-click="removeProjectFromTag(project, tag)"
) ×
.col-xs-2 td
span.owner {{ownerName()}} span.last-modified(tooltip="{{project.lastUpdated | formatDate}}")
span(ng-if="isLinkSharingProject(project)") | {{project.lastUpdated | fromNowDate}}
|   span(ng-if='project.lastUpdatedBy')
i.fa.fa-link.small( |
tooltip=translate("link_sharing") | #{translate('by')}
tooltip-placement="right" |
tooltip-append-to-body="true" | {{userDisplayName(project.lastUpdatedBy)}}
)
div(class=lastUpdatedClasses) td.text-right
if settings.overleaf div(
span.last-modified(tooltip="{{project.lastUpdated | formatDate}}") {{project.lastUpdated | fromNowDate}} ng-if="!project.isTableActionInflight && !project.isV1Project"
else )
span.last-modified {{project.lastUpdated | formatDate}} button.btn.btn-link.action-btn(
tooltip=translate('copy'),
if settings.overleaf tooltip-placement="top",
.hidden-xs.col-sm-3.col-md-2.action-btn-row tooltip-append-to-body="true",
div( ng-click="clone($event)"
ng-if="!project.isTableActionInflight" )
) i.icon.fa.fa-files-o
button.btn.btn-link.action-btn( button.btn.btn-link.action-btn(
tooltip=translate('copy'), tooltip=translate('download'),
tooltip-placement="top", tooltip-placement="top",
tooltip-append-to-body="true", tooltip-append-to-body="true",
ng-click="clone($event)" ng-click="download($event)"
) )
i.icon.fa.fa-files-o i.icon.fa.fa-cloud-download
button.btn.btn-link.action-btn( button.btn.btn-link.action-btn(
tooltip=translate('download'), ng-if="!project.archived && isOwner()"
tooltip-placement="top", tooltip=translate('archive'),
tooltip-append-to-body="true", tooltip-placement="top",
ng-click="download($event)" tooltip-append-to-body="true",
) ng-click="archiveOrLeave($event)"
i.icon.fa.fa-cloud-download )
button.btn.btn-link.action-btn( i.icon.fa.fa-inbox
ng-if="!project.archived && isOwner()" button.btn.btn-link.action-btn(
tooltip=translate('archive'), ng-if="!project.archived && !isOwner()"
tooltip-placement="top", tooltip=translate('leave'),
tooltip-append-to-body="true", tooltip-placement="top",
ng-click="archiveOrLeave($event)" tooltip-append-to-body="true",
) ng-click="archiveOrLeave($event)"
i.icon.fa.fa-inbox )
button.btn.btn-link.action-btn( i.icon.fa.fa-sign-out
ng-if="!project.archived && !isOwner()" button.btn.btn-link.action-btn(
tooltip=translate('leave'), ng-if="project.archived"
tooltip-placement="top", tooltip=translate('unarchive'),
tooltip-append-to-body="true", tooltip-placement="top",
ng-click="archiveOrLeave($event)" tooltip-append-to-body="true",
) ng-click="restore($event)"
i.icon.fa.fa-sign-out )
button.btn.btn-link.action-btn( i.icon.fa.fa-reply
ng-if="project.archived" button.btn.btn-link.action-btn(
tooltip=translate('unarchive'), ng-if="project.archived && isOwner()"
tooltip-placement="top", tooltip=translate('delete_forever'),
tooltip-append-to-body="true", tooltip-placement="top",
ng-click="restore($event)" tooltip-append-to-body="true",
) ng-click="deleteProject($event)"
i.icon.fa.fa-reply )
button.btn.btn-link.action-btn( i.icon.fa.fa-trash
ng-if="project.archived && isOwner()" div(
tooltip=translate('delete_forever'), ng-if="project.isTableActionInflight"
tooltip-placement="top", )
tooltip-append-to-body="true", i.fa.fa-spinner.fa-spin
ng-click="deleteProject($event)"
)
i.icon.fa.fa-trash
div(
ng-if="project.isTableActionInflight"
)
i.fa.fa-spinner.fa-spin

View file

@ -1,185 +1,175 @@
.row .row
.col-xs-12(ng-cloak) .col-xs-12(ng-cloak)
form.project-search.form-horizontal(role="form") form.project-search.form-horizontal(role="form")
.form-group.has-feedback.has-feedback-left.col-md-7.col-xs-12 .form-group.has-feedback.has-feedback-left.col-md-7.col-xs-12
input.form-control.col-md-7.col-xs-12( input.form-control.col-md-7.col-xs-12(
placeholder=translate('search_projects')+"…", placeholder=translate('search_projects')+"…",
aria-label=translate('search_projects')+"…", aria-label=translate('search_projects')+"…",
autofocus='autofocus', autofocus='autofocus',
ng-model="searchText.value", ng-model="searchText.value",
focus-on='search:clear', focus-on='search:clear',
ng-keyup="searchProjects()" ng-keyup="searchProjects()"
) )
i.fa.fa-search.form-control-feedback-left i.fa.fa-search.form-control-feedback-left
i.fa.fa-times.form-control-feedback( i.fa.fa-times.form-control-feedback(
ng-click="clearSearchText()", ng-click="clearSearchText()",
style="cursor: pointer;", style="cursor: pointer;",
ng-show="searchText.value.length > 0" ng-show="searchText.value.length > 0"
) )
//- i.fa.fa-remove //- i.fa.fa-remove
.project-tools(ng-cloak) .project-tools(ng-cloak)
.btn-toolbar(ng-show="filter != 'archived'") .btn-toolbar(ng-show="filter != 'archived'")
.btn-group(ng-hide="selectedProjects.length < 1") .btn-group(ng-hide="selectedProjects.length < 1")
a.btn.btn-default( a.btn.btn-default(
href, href,
tooltip=translate('download'), tooltip=translate('download'),
tooltip-placement="bottom", tooltip-placement="bottom",
tooltip-append-to-body="true", tooltip-append-to-body="true",
ng-click="downloadSelectedProjects()" ng-click="downloadSelectedProjects()"
) )
i.fa.fa-cloud-download i.fa.fa-cloud-download
- var archiveButtonString = settings.overleaf ? translate("archive") : translate("delete") - var archiveButtonString = settings.overleaf ? translate("archive") : translate("delete")
- var archiveButtonIcon = settings.overleaf ? "fa-inbox" : "fa-trash-o" - var archiveButtonIcon = settings.overleaf ? "fa-inbox" : "fa-trash-o"
a.btn.btn-default( a.btn.btn-default(
href, href,
tooltip=`{{ isArchiveableProjectSelected ? '${archiveButtonString}' : '${translate("leave")}' }}`, tooltip=`{{ isArchiveableProjectSelected ? '${archiveButtonString}' : '${translate("leave")}' }}`,
tooltip-placement="bottom", tooltip-placement="bottom",
tooltip-append-to-body="true", tooltip-append-to-body="true",
ng-click="openArchiveProjectsModal()" ng-click="openArchiveProjectsModal()"
) )
i.fa(ng-class=`isArchiveableProjectSelected ? '${archiveButtonIcon}' : 'fa-sign-out'`) i.fa(ng-class=`isArchiveableProjectSelected ? '${archiveButtonIcon}' : 'fa-sign-out'`)
.btn-group.dropdown(ng-hide="selectedProjects.length < 1", dropdown) .btn-group.dropdown(ng-hide="selectedProjects.length < 1", dropdown)
a.btn.btn-default.dropdown-toggle( a.btn.btn-default.dropdown-toggle(
href, href,
data-toggle="dropdown", data-toggle="dropdown",
dropdown-toggle, dropdown-toggle,
tooltip=translate('add_to_folders'), tooltip=translate('add_to_folders'),
tooltip-append-to-body="true", tooltip-append-to-body="true",
tooltip-placement="bottom" tooltip-placement="bottom"
) )
i.fa.fa-folder-open-o i.fa.fa-folder-open-o
| |
span.caret span.caret
ul.dropdown-menu.dropdown-menu-right.js-tags-dropdown-menu.tags-dropdown-menu( ul.dropdown-menu.dropdown-menu-right.js-tags-dropdown-menu.tags-dropdown-menu(
role="menu" role="menu"
ng-controller="TagListController" ng-controller="TagListController"
) )
li.dropdown-header #{translate("add_to_folder")} li.dropdown-header #{translate("add_to_folder")}
li( li(
ng-repeat="tag in tags | orderBy:'name'", ng-repeat="tag in tags | orderBy:'name'",
ng-controller="TagDropdownItemController" ng-controller="TagDropdownItemController"
ng-if="!tag.isV1" ng-if="!tag.isV1"
) )
a(href="#", ng-click="addOrRemoveProjectsFromTag()", stop-propagation="click") a(href="#", ng-click="addOrRemoveProjectsFromTag()", stop-propagation="click")
i.fa( i.fa(
ng-class="{\ ng-class="{\
'fa-check-square-o': areSelectedProjectsInTag == true,\ 'fa-check-square-o': areSelectedProjectsInTag == true,\
'fa-square-o': areSelectedProjectsInTag == false,\ 'fa-square-o': areSelectedProjectsInTag == false,\
'fa-minus-square-o': areSelectedProjectsInTag == 'partial'\ 'fa-minus-square-o': areSelectedProjectsInTag == 'partial'\
}" }"
) )
| {{tag.name}} | {{tag.name}}
li.divider li.divider
li li
a(href, ng-click="openNewTagModal()", stop-propagation="click") #{translate("create_new_folder")} a(href, ng-click="openNewTagModal()", stop-propagation="click") #{translate("create_new_folder")}
.btn-group(ng-hide="selectedProjects.length != 1", dropdown).dropdown .btn-group(ng-hide="selectedProjects.length != 1", dropdown).dropdown
a.btn.btn-default.dropdown-toggle( a.btn.btn-default.dropdown-toggle(
href, href,
data-toggle="dropdown", data-toggle="dropdown",
dropdown-toggle dropdown-toggle
) #{translate("more")} ) #{translate("more")}
span.caret span.caret
ul.dropdown-menu.dropdown-menu-right(role="menu") ul.dropdown-menu.dropdown-menu-right(role="menu")
li(ng-show="getFirstSelectedProject().accessLevel == 'owner'") li(ng-show="getFirstSelectedProject().accessLevel == 'owner'")
a( a(
href, href,
ng-click="openRenameProjectModal()" ng-click="openRenameProjectModal()"
) #{translate("rename")} ) #{translate("rename")}
li li
a( a(
href, href,
ng-click="openCloneProjectModal()" ng-click="openCloneProjectModal()"
) #{translate("make_copy")} ) #{translate("make_copy")}
.btn-toolbar(ng-show="filter == 'archived'") .btn-toolbar(ng-show="filter == 'archived'")
.btn-group(ng-hide="selectedProjects.length < 1") .btn-group(ng-hide="selectedProjects.length < 1")
a.btn.btn-default( a.btn.btn-default(
href, href,
data-original-title="Restore", data-original-title="Restore",
data-toggle="tooltip", data-toggle="tooltip",
data-placement="bottom", data-placement="bottom",
ng-click="restoreSelectedProjects()" ng-click="restoreSelectedProjects()"
) #{translate("restore")} ) #{translate("restore")}
.btn-group(ng-hide="selectedProjects.length < 1") .btn-group(ng-hide="selectedProjects.length < 1")
a.btn.btn-danger( a.btn.btn-danger(
href, href,
data-original-title="Delete Forever", data-original-title="Delete Forever",
data-toggle="tooltip", data-toggle="tooltip",
data-placement="bottom", data-placement="bottom",
ng-click="openDeleteProjectsModal()" ng-click="openDeleteProjectsModal()"
) #{translate("delete_forever")} ) #{translate("delete_forever")}
.row.row-spaced .row.row-spaced
each warning in warnings each warning in warnings
.col-xs-12 .col-xs-12
.alert.alert-warning(role="alert")= warning .alert.alert-warning(role="alert")= warning
.col-xs-12 .col-xs-12
.card.card-thin.project-list-card .card.card-thin.project-list-card
ul.list-unstyled.project-list.structured-list( .table-wrapper(max-height="projectListHeight - 25",)
select-all-list, table.table.table-hover.project-list(
ng-if="projects.length > 0", select-all-list,
max-height="projectListHeight - 25", ng-if="projects.length > 0",
ng-cloak ng-cloak
) )
li.container-fluid thead
.row tr
- var titleClasses = settings.overleaf ? " col-xs-6 col-sm-4 col-md-6" : "col-xs-6" th.selectProject
- var lastUpdatedClasses = settings.overleaf ? " col-xs-4 col-sm-3 col-md-2" : "col-xs-4" input.select-all(
select-all,
type="checkbox"
aria-label=translate('select_all_projects')
)
th.projectName
span.header.clickable(ng-click="changePredicate('name')") #{translate("title")}
i.tablesort.fa(ng-class="getSortIconClass('name')")
th
span.header.clickable(ng-click="changePredicate('accessLevel')") #{translate("owner")}
i.tablesort.fa(ng-class="getSortIconClass('accessLevel')")
th
span.header.clickable(ng-click="changePredicate('lastUpdated')") #{translate("last_modified")}
i.tablesort.fa(ng-class="getSortIconClass('lastUpdated')")
if settings.overleaf
th.text-right
span.header #{translate("actions")}
tbody
tr.project_entry(
ng-repeat="project in visibleProjects | orderBy:predicate:reverse",
ng-controller="ProjectListItemController"
)
include ./item
tr(
ng-if="visibleProjects.length == 0",
ng-cloak
)
td(colspan=5).text-center
small #{translate("no_projects")}
div(class=titleClasses) div.welcome.text-centered(ng-if="projects.length == 0", ng-cloak)
input.select-all( h2 #{translate("welcome_to_sl")}
select-all, p #{translate("new_to_latex_look_at")}
type="checkbox" a(href="/templates") #{translate("templates").toLowerCase()}
aria-label=translate('select_all_projects') | #{translate("or")}
) a(href="/learn") #{translate("latex_help_guide")}
span.header.clickable(ng-click="changePredicate('name')") #{translate("title")} | ,
i.tablesort.fa(ng-class="getSortIconClass('name')") br
.col-xs-2 | #{translate("or_create_project_left")}
span.header.clickable(ng-click="changePredicate('accessLevel')") #{translate("owner")}
i.tablesort.fa(ng-class="getSortIconClass('accessLevel')")
div(class=lastUpdatedClasses)
span.header.clickable(ng-click="changePredicate('lastUpdated')") #{translate("last_modified")}
i.tablesort.fa(ng-class="getSortIconClass('lastUpdated')")
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-controller="ProjectListItemController"
)
.row(
ng-if="!project.isV1Project"
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")}
div.welcome.text-centered(ng-if="projects.length == 0", ng-cloak)
h2 #{translate("welcome_to_sl")}
p #{translate("new_to_latex_look_at")}
a(href="/templates") #{translate("templates").toLowerCase()}
| #{translate("or")}
a(href="/learn") #{translate("latex_help_guide")}
| ,
br
| #{translate("or_create_project_left")}

View file

@ -1,25 +0,0 @@
.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
button.btn.btn-link.projectName(
ng-click="openV1ImportModal(project)"
stop-propagation="click"
ng-show="project.accessLevel == 'owner'"
) {{project.name}}
a.projectName(
href=settings.overleaf.host + "/{{project.id}}"
target="_blank"
ng-hide="project.accessLevel == 'owner'"
) {{project.name}}
.col-xs-2
span.owner {{ownerName()}}
.col-xs-4.col-sm-3.col-md-2
span.last-modified(tooltip="{{project.lastUpdated | formatDate}}") {{project.lastUpdated | fromNowDate}}

View file

@ -13,7 +13,7 @@ define [
$scope.predicate = "lastUpdated" $scope.predicate = "lastUpdated"
$scope.nUntagged = 0 $scope.nUntagged = 0
$scope.reverse = true $scope.reverse = true
$scope.searchText = $scope.searchText =
value : "" value : ""
$timeout () -> $timeout () ->
@ -37,7 +37,7 @@ define [
angular.element($window).bind "resize", () -> angular.element($window).bind "resize", () ->
recalculateProjectListHeight() recalculateProjectListHeight()
$scope.$apply() $scope.$apply()
# Allow tags to be accessed on projects as well # Allow tags to be accessed on projects as well
projectsById = {} projectsById = {}
for project in $scope.projects for project in $scope.projects
@ -56,7 +56,7 @@ define [
tag.selected = true tag.selected = true
else else
tag.selected = false tag.selected = false
$scope.changePredicate = (newPredicate)-> $scope.changePredicate = (newPredicate)->
if $scope.predicate == newPredicate if $scope.predicate == newPredicate
$scope.reverse = !$scope.reverse $scope.reverse = !$scope.reverse
@ -145,7 +145,7 @@ define [
# We don't want hidden selections # We don't want hidden selections
project.selected = false project.selected = false
localStorage("project_list", JSON.stringify({ localStorage("project_list", JSON.stringify({
filter: $scope.filter, filter: $scope.filter,
selectedTagId: selectedTag?._id selectedTagId: selectedTag?._id
})) }))
@ -461,7 +461,7 @@ define [
resolve: resolve:
project: () -> project project: () -> project
) )
if storedUIOpts?.filter? if storedUIOpts?.filter?
if storedUIOpts.filter == "tag" and storedUIOpts.selectedTagId? if storedUIOpts.filter == "tag" and storedUIOpts.selectedTagId?
markTagAsSelected(storedUIOpts.selectedTagId) markTagAsSelected(storedUIOpts.selectedTagId)
@ -485,11 +485,11 @@ define [
$scope.isLinkSharingProject = (project) -> $scope.isLinkSharingProject = (project) ->
return project.source == 'token' return project.source == 'token'
$scope.ownerName = () -> $scope.userDisplayName = (user) ->
if $scope.project.accessLevel == "owner" if user? and user._id == window.user_id
return "You" return "You"
else if $scope.project.owner? else if user?
return [$scope.project.owner.first_name, $scope.project.owner.last_name].filter((n) -> n?).join(" ") return [user.first_name, user.last_name].filter((n) -> n?).join(" ")
else else
return "None" return "None"
@ -535,11 +535,11 @@ define [
url: "/project/#{$scope.project.id}?forever=true" url: "/project/#{$scope.project.id}?forever=true"
headers: headers:
"X-CSRF-Token": window.csrfToken "X-CSRF-Token": window.csrfToken
}).then () -> }).then () ->
$scope.project.isTableActionInflight = false $scope.project.isTableActionInflight = false
$scope._removeProjectFromList $scope.project $scope._removeProjectFromList $scope.project
for tag in $scope.tags for tag in $scope.tags
$scope._removeProjectIdsFromTagArray(tag, [ $scope.project.id ]) $scope._removeProjectIdsFromTagArray(tag, [ $scope.project.id ])
$scope.updateVisibleProjects() $scope.updateVisibleProjects()
.catch () -> .catch () ->
$scope.project.isTableActionInflight = false $scope.project.isTableActionInflight = false

View file

@ -62,7 +62,7 @@
.small { .small {
color: @sidebar-color; color: @sidebar-color;
} }
} }
.project-list-sidebar when (@is-overleaf) { .project-list-sidebar when (@is-overleaf) {
overflow-x: hidden; overflow-x: hidden;
@ -271,7 +271,7 @@ ul.structured-list {
li { li {
border-bottom: 1px solid @structured-list-border-color; border-bottom: 1px solid @structured-list-border-color;
padding: (@line-height-computed / 4) 0; padding: (@line-height-computed / 4) 0;
&:last-child { &:last-child {
border-bottom: 0 none; border-bottom: 0 none;
} }
@ -294,7 +294,7 @@ ul.structured-list {
.header when (@is-overleaf = false) { .header when (@is-overleaf = false) {
text-transform: uppercase; text-transform: uppercase;
} }
.select-item, .select-all { .select-item, .select-all {
position: absolute; position: absolute;
left: @line-height-computed; left: @line-height-computed;
@ -314,8 +314,38 @@ ul.structured-list {
padding: 0 (@line-height-computed / 4); padding: 0 (@line-height-computed / 4);
} }
ul.project-list { .table-wrapper {
li { overflow: scroll;
}
table.project-list {
margin: 0;
width: 100%;
th when (@is-overleaf = true) {
font-weight: 600;
}
th when (@is-overleaf = false) {
text-transform: uppercase;
font-weight: normal;
}
// thead > tr > th {
// padding-top: @line-height-computed / 8;
// }
td {
@media (min-width: @screen-md-min) {
white-space: nowrap;
}
&.projectName {
white-space: normal;
width: 50%;
}
&.selectProject {
width: 1%;
}
.last-modified when (@is-overleaf = false) { .last-modified when (@is-overleaf = false) {
font-size: .8rem; font-size: .8rem;
} }
@ -325,12 +355,13 @@ ul.project-list {
.owner when (@is-overleaf = false) { .owner when (@is-overleaf = false) {
margin-right: 0; margin-right: 0;
} }
.projectName { a.projectName, button.projectName {
margin-right: @line-height-computed / 4; margin-right: @line-height-computed / 4;
padding: 0; padding: 0;
vertical-align: inherit; vertical-align: inherit;
white-space: normal; white-space: normal;
text-align: left; text-align: left;
color: @structured-list-link-color;
} }
.tag-label { .tag-label {
@ -369,6 +400,7 @@ ul.project-list {
.v1-badge { .v1-badge {
margin-left: -4px; margin-left: -4px;
margin-right: -4px;
} }
.action-btn-row-header, .action-btn-row { .action-btn-row-header, .action-btn-row {
@ -564,7 +596,7 @@ ul.project-list {
&:last-child { &:last-child {
margin-bottom: 0; margin-bottom: 0;
} }
} }
.announcement-header { .announcement-header {
.page-header; .page-header;
margin: 0; margin: 0;

View file

@ -34,7 +34,7 @@ th {
// Bottom align for column headings // Bottom align for column headings
> thead > tr > th { > thead > tr > th {
vertical-align: bottom; vertical-align: bottom;
border-bottom: 2px solid @table-border-color; border-bottom: 1px solid @table-border-color;
} }
// Remove top border from thead by default // Remove top border from thead by default
> caption + thead, > caption + thead,

View file

@ -110,7 +110,7 @@
//## Customizes the `.table` component with basic values, each used across all table variations. //## Customizes the `.table` component with basic values, each used across all table variations.
//** Padding for `<th>`s and `<td>`s. //** Padding for `<th>`s and `<td>`s.
@table-cell-padding: 8px; @table-cell-padding: @line-height-computed / 4;
//** Padding for cells in `.table-condensed`. //** Padding for cells in `.table-condensed`.
@table-condensed-cell-padding: 5px; @table-condensed-cell-padding: 5px;

View file

@ -0,0 +1,42 @@
expect = require("chai").expect
async = require("async")
User = require "./helpers/User"
request = require "./helpers/request"
settings = require "settings-sharelatex"
Project = require("../../../app/js/models/Project").Project
markAsUpdated = (project_id, user_id, timestamp, callback) ->
request.post {
url: "/project/#{project_id}/last_updated"
json: {
user_id,
timestamp
}
auth:
user: settings.apis.web.user
pass: settings.apis.web.pass
sendImmediately: true
jar: false
}, callback
describe "ProjectLastUpdated", ->
before (done) ->
@timeout(90000)
@owner = new User()
@timestamp = Date.now()
@user_id = "abcdef1234567890abcdef12"
async.series [
(cb) => @owner.login cb
(cb) => @owner.createProject "private-project", (error, @project_id) => cb(error)
], done
describe "with user_id and timestamp", ->
it 'should update the project', (done) ->
markAsUpdated @project_id, @user_id, @timestamp, (error, response, body) =>
return done(error) if error?
expect(response.statusCode).to.equal 200
Project.findOne _id: @project_id, (error, project) =>
return done(error) if error?
expect(project.lastUpdated.getTime()).to.equal @timestamp
expect(project.lastUpdatedBy.toString()).to.equal @user_id
done()

View file

@ -23,6 +23,7 @@ describe "HistoryController", ->
"../Project/ProjectDetailsHandler": @ProjectDetailsHandler = {} "../Project/ProjectDetailsHandler": @ProjectDetailsHandler = {}
"../Project/ProjectEntityUpdateHandler": @ProjectEntityUpdateHandler = {} "../Project/ProjectEntityUpdateHandler": @ProjectEntityUpdateHandler = {}
"./RestoreManager": @RestoreManager = {} "./RestoreManager": @RestoreManager = {}
"../Project/ProjectUpdateHandler": @ProjectUpdateHandler = {}
@settings.apis = @settings.apis =
trackchanges: trackchanges:
enabled: false enabled: false

View file

@ -190,11 +190,6 @@ describe 'ProjectEntityUpdateHandler', ->
.calledWith(project_id, doc_id, @docLines, @version, @ranges) .calledWith(project_id, doc_id, @docLines, @version, @ranges)
.should.equal true .should.equal true
it "should mark the project as updated", ->
@ProjectUpdater.markAsUpdated
.calledWith(project_id)
.should.equal true
it "should send the doc the to the TPDS", -> it "should send the doc the to the TPDS", ->
@TpdsUpdateSender.addDoc @TpdsUpdateSender.addDoc
.calledWith({ .calledWith({

View file

@ -16,12 +16,15 @@ describe 'ProjectUpdateHandler', ->
describe 'marking a project as recently updated', -> describe 'marking a project as recently updated', ->
it 'should send an update to mongo', (done)-> it 'should send an update to mongo', (done)->
project_id = "project_id" project_id = "project_id"
@handler.markAsUpdated project_id, (err)=> user_id = "mock_user_id"
args = @ProjectModel.update.args[0] timestamp = Date.now()
args[0]._id.should.equal project_id @handler.markAsUpdated project_id, user_id, timestamp, (err)=>
date = args[1].lastUpdated+"" @ProjectModel.update.calledWith({
now = Date.now()+"" _id: project_id,
date.substring(0,5).should.equal now.substring(0,5) }, {
lastUpdated: new Date(timestamp),
lastUpdatedBy: user_id
}).should.equal true
done() done()
describe "markAsOpened", -> describe "markAsOpened", ->