diff --git a/services/web/app/coffee/Features/History/HistoryController.coffee b/services/web/app/coffee/Features/History/HistoryController.coffee index 189e07b0f5..8b014751f9 100644 --- a/services/web/app/coffee/Features/History/HistoryController.coffee +++ b/services/web/app/coffee/Features/History/HistoryController.coffee @@ -7,6 +7,7 @@ HistoryManager = require "./HistoryManager" ProjectDetailsHandler = require "../Project/ProjectDetailsHandler" ProjectEntityUpdateHandler = require "../Project/ProjectEntityUpdateHandler" RestoreManager = require "./RestoreManager" +ProjectUpdateHandler = require "../Project/ProjectUpdateHandler" module.exports = HistoryController = selectHistoryApi: (req, res, next = (error) ->) -> @@ -143,3 +144,11 @@ module.exports = HistoryController = 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}" 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 diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee index 59c0647c19..ed5e219e3f 100644 --- a/services/web/app/coffee/Features/Project/ProjectController.coffee +++ b/services/web/app/coffee/Features/Project/ProjectController.coffee @@ -184,7 +184,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 @@ -392,6 +392,7 @@ module.exports = ProjectController = id: project._id name: project.name lastUpdated: project.lastUpdated + lastUpdatedBy: project.lastUpdatedBy publicAccessLevel: project.publicAccesLevel accessLevel: accessLevel source: source @@ -430,6 +431,8 @@ module.exports = ProjectController = for project in projects if project.owner_ref? users[project.owner_ref.toString()] = true + if project.lastUpdatedBy? + users[project.lastUpdatedBy] = true jobs = [] for user_id, _ of users @@ -444,6 +447,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()] callback null, projects _buildWarningsList: (v1ProjectData = {}) -> diff --git a/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee b/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee index b7be80cb47..4800de05d4 100644 --- a/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee @@ -114,8 +114,6 @@ module.exports = ProjectEntityUpdateHandler = self = logger.log {project_id, doc_id, modified}, "finished updating doc lines" # path will only be present if the doc is not deleted 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 else callback() diff --git a/services/web/app/coffee/Features/Project/ProjectUpdateHandler.coffee b/services/web/app/coffee/Features/Project/ProjectUpdateHandler.coffee index ba3b3b6295..d89d7c7e89 100644 --- a/services/web/app/coffee/Features/Project/ProjectUpdateHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectUpdateHandler.coffee @@ -2,30 +2,25 @@ Project = require('../../models/Project').Project logger = require('logger-sharelatex') module.exports = - markAsUpdated : (project_id, callback)-> + markAsUpdated : (project_id, user_id, timestamp, callback)-> conditions = {_id:project_id} - update = {lastUpdated:Date.now()} - Project.update conditions, update, {}, (err)-> - if callback? - callback() + update = { + lastUpdated: new Date(timestamp), + lastUpdatedBy: user_id + } + Project.update conditions, update, {}, callback markAsOpened : (project_id, callback)-> conditions = {_id:project_id} update = {lastOpened:Date.now()} - Project.update conditions, update, {}, (err)-> - if callback? - callback() + Project.update conditions, update, {}, callback markAsInactive: (project_id, callback)-> conditions = {_id:project_id} update = {active:false} - Project.update conditions, update, {}, (err)-> - if callback? - callback() + Project.update conditions, update, {}, callback markAsActive: (project_id, callback)-> conditions = {_id:project_id} update = {active:true} - Project.update conditions, update, {}, (err)-> - if callback? - callback() \ No newline at end of file + Project.update conditions, update, {}, callback diff --git a/services/web/app/coffee/models/Project.coffee b/services/web/app/coffee/models/Project.coffee index 00c2f1be52..f4b478d64e 100644 --- a/services/web/app/coffee/models/Project.coffee +++ b/services/web/app/coffee/models/Project.coffee @@ -19,6 +19,7 @@ DeletedFileSchema = new Schema ProjectSchema = new Schema name : {type:String, default:'new project'} lastUpdated : {type:Date, default: () -> new Date()} + lastUpdatedBy : {type:ObjectId, ref: 'User'} lastOpened : {type:Date} active : { type: Boolean, default: true } owner_ref : {type:ObjectId, ref:'User'} diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee index b196c56870..2413632581 100644 --- a/services/web/app/coffee/router.coffee +++ b/services/web/app/coffee/router.coffee @@ -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/restore_file", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, HistoryController.restoreFileFromV2 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.post "/project/:Project_id/labels", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, HistoryController.selectHistoryApi, HistoryController.ensureProjectHistoryEnabled, HistoryController.createLabel diff --git a/services/web/app/views/project/list/item.pug b/services/web/app/views/project/list/item.pug index deb9863d04..5266571da7 100644 --- a/services/web/app/views/project/list/item.pug +++ b/services/web/app/views/project/list/item.pug @@ -1,104 +1,123 @@ -- 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" +td.selectProject + 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) - 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)}}" - 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)" - ) × +td + span.owner {{userDisplayName(project.owner)}} + span(ng-if="isLinkSharingProject(project)") + | + i.fa.fa-link.small( + tooltip=translate("link_sharing") + tooltip-placement="right" + tooltip-append-to-body="true" + ) -.col-xs-2 - span.owner {{ownerName()}} - span(ng-if="isLinkSharingProject(project)") - | - i.fa.fa-link.small( - tooltip=translate("link_sharing") - tooltip-placement="right" - tooltip-append-to-body="true" - ) +td + span.last-modified(tooltip="{{project.lastUpdated | formatDate}}") + | {{project.lastUpdated | fromNowDate}} + span(ng-if='project.lastUpdatedBy') + | + | #{translate('by')} + | + | {{userDisplayName(project.lastUpdatedBy)}} -div(class=lastUpdatedClasses) - if settings.overleaf - span.last-modified(tooltip="{{project.lastUpdated | formatDate}}") {{project.lastUpdated | fromNowDate}} - else - span.last-modified {{project.lastUpdated | formatDate}} - -if settings.overleaf - .hidden-xs.col-sm-3.col-md-2.action-btn-row - div( - ng-if="!project.isTableActionInflight" - ) - button.btn.btn-link.action-btn( - tooltip=translate('copy'), - tooltip-placement="top", - tooltip-append-to-body="true", - ng-click="clone($event)" - ) - i.icon.fa.fa-files-o - button.btn.btn-link.action-btn( - tooltip=translate('download'), - tooltip-placement="top", - tooltip-append-to-body="true", - ng-click="download($event)" - ) - i.icon.fa.fa-cloud-download - button.btn.btn-link.action-btn( - ng-if="!project.archived && isOwner()" - tooltip=translate('archive'), - tooltip-placement="top", - tooltip-append-to-body="true", - ng-click="archiveOrLeave($event)" - ) - i.icon.fa.fa-inbox - button.btn.btn-link.action-btn( - ng-if="!project.archived && !isOwner()" - tooltip=translate('leave'), - tooltip-placement="top", - tooltip-append-to-body="true", - ng-click="archiveOrLeave($event)" - ) - i.icon.fa.fa-sign-out - button.btn.btn-link.action-btn( - ng-if="project.archived" - tooltip=translate('unarchive'), - tooltip-placement="top", - tooltip-append-to-body="true", - ng-click="restore($event)" - ) - i.icon.fa.fa-reply - button.btn.btn-link.action-btn( - ng-if="project.archived && isOwner()" - tooltip=translate('delete_forever'), - tooltip-placement="top", - tooltip-append-to-body="true", - ng-click="deleteProject($event)" - ) - i.icon.fa.fa-trash - div( - ng-if="project.isTableActionInflight" - ) - i.fa.fa-spinner.fa-spin \ No newline at end of file +td.text-right + div( + ng-if="!project.isTableActionInflight && !project.isV1Project" + ) + button.btn.btn-link.action-btn( + tooltip=translate('copy'), + tooltip-placement="top", + tooltip-append-to-body="true", + ng-click="clone($event)" + ) + i.icon.fa.fa-files-o + button.btn.btn-link.action-btn( + tooltip=translate('download'), + tooltip-placement="top", + tooltip-append-to-body="true", + ng-click="download($event)" + ) + i.icon.fa.fa-cloud-download + button.btn.btn-link.action-btn( + ng-if="!project.archived && isOwner()" + tooltip=translate('archive'), + tooltip-placement="top", + tooltip-append-to-body="true", + ng-click="archiveOrLeave($event)" + ) + i.icon.fa.fa-inbox + button.btn.btn-link.action-btn( + ng-if="!project.archived && !isOwner()" + tooltip=translate('leave'), + tooltip-placement="top", + tooltip-append-to-body="true", + ng-click="archiveOrLeave($event)" + ) + i.icon.fa.fa-sign-out + button.btn.btn-link.action-btn( + ng-if="project.archived" + tooltip=translate('unarchive'), + tooltip-placement="top", + tooltip-append-to-body="true", + ng-click="restore($event)" + ) + i.icon.fa.fa-reply + button.btn.btn-link.action-btn( + ng-if="project.archived && isOwner()" + tooltip=translate('delete_forever'), + tooltip-placement="top", + tooltip-append-to-body="true", + ng-click="deleteProject($event)" + ) + i.icon.fa.fa-trash + div( + ng-if="project.isTableActionInflight" + ) + i.fa.fa-spinner.fa-spin diff --git a/services/web/app/views/project/list/project-list.pug b/services/web/app/views/project/list/project-list.pug index cfea4aa6c6..a6971aa49a 100644 --- a/services/web/app/views/project/list/project-list.pug +++ b/services/web/app/views/project/list/project-list.pug @@ -1,185 +1,175 @@ .row - .col-xs-12(ng-cloak) + .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( - placeholder=translate('search_projects')+"…", - aria-label=translate('search_projects')+"…", - autofocus='autofocus', - ng-model="searchText.value", - focus-on='search:clear', - ng-keyup="searchProjects()" - ) - i.fa.fa-search.form-control-feedback-left - i.fa.fa-times.form-control-feedback( - ng-click="clearSearchText()", - style="cursor: pointer;", - ng-show="searchText.value.length > 0" - ) - //- i.fa.fa-remove + 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( + placeholder=translate('search_projects')+"…", + aria-label=translate('search_projects')+"…", + autofocus='autofocus', + ng-model="searchText.value", + focus-on='search:clear', + ng-keyup="searchProjects()" + ) + i.fa.fa-search.form-control-feedback-left + i.fa.fa-times.form-control-feedback( + ng-click="clearSearchText()", + style="cursor: pointer;", + ng-show="searchText.value.length > 0" + ) + //- i.fa.fa-remove - .project-tools(ng-cloak) - .btn-toolbar(ng-show="filter != 'archived'") - .btn-group(ng-hide="selectedProjects.length < 1") - a.btn.btn-default( - href, - tooltip=translate('download'), - tooltip-placement="bottom", - tooltip-append-to-body="true", - ng-click="downloadSelectedProjects()" - ) - i.fa.fa-cloud-download - - var archiveButtonString = settings.overleaf ? translate("archive") : translate("delete") - - var archiveButtonIcon = settings.overleaf ? "fa-inbox" : "fa-trash-o" - a.btn.btn-default( - href, - tooltip=`{{ isArchiveableProjectSelected ? '${archiveButtonString}' : '${translate("leave")}' }}`, - tooltip-placement="bottom", - tooltip-append-to-body="true", - ng-click="openArchiveProjectsModal()" - ) - i.fa(ng-class=`isArchiveableProjectSelected ? '${archiveButtonIcon}' : 'fa-sign-out'`) + .project-tools(ng-cloak) + .btn-toolbar(ng-show="filter != 'archived'") + .btn-group(ng-hide="selectedProjects.length < 1") + a.btn.btn-default( + href, + tooltip=translate('download'), + tooltip-placement="bottom", + tooltip-append-to-body="true", + ng-click="downloadSelectedProjects()" + ) + i.fa.fa-cloud-download + - var archiveButtonString = settings.overleaf ? translate("archive") : translate("delete") + - var archiveButtonIcon = settings.overleaf ? "fa-inbox" : "fa-trash-o" + a.btn.btn-default( + href, + tooltip=`{{ isArchiveableProjectSelected ? '${archiveButtonString}' : '${translate("leave")}' }}`, + tooltip-placement="bottom", + tooltip-append-to-body="true", + ng-click="openArchiveProjectsModal()" + ) + i.fa(ng-class=`isArchiveableProjectSelected ? '${archiveButtonIcon}' : 'fa-sign-out'`) - .btn-group.dropdown(ng-hide="selectedProjects.length < 1", dropdown) - a.btn.btn-default.dropdown-toggle( - href, - data-toggle="dropdown", - dropdown-toggle, - tooltip=translate('add_to_folders'), - tooltip-append-to-body="true", - tooltip-placement="bottom" - ) - i.fa.fa-folder-open-o - | - span.caret - ul.dropdown-menu.dropdown-menu-right.js-tags-dropdown-menu.tags-dropdown-menu( - role="menu" - ng-controller="TagListController" - ) - li.dropdown-header #{translate("add_to_folder")} - li( - ng-repeat="tag in tags | orderBy:'name'", - ng-controller="TagDropdownItemController" - ng-if="!tag.isV1" - ) - a(href="#", ng-click="addOrRemoveProjectsFromTag()", stop-propagation="click") - i.fa( - ng-class="{\ - 'fa-check-square-o': areSelectedProjectsInTag == true,\ - 'fa-square-o': areSelectedProjectsInTag == false,\ - 'fa-minus-square-o': areSelectedProjectsInTag == 'partial'\ - }" - ) - | {{tag.name}} - li.divider - li - a(href, ng-click="openNewTagModal()", stop-propagation="click") #{translate("create_new_folder")} + .btn-group.dropdown(ng-hide="selectedProjects.length < 1", dropdown) + a.btn.btn-default.dropdown-toggle( + href, + data-toggle="dropdown", + dropdown-toggle, + tooltip=translate('add_to_folders'), + tooltip-append-to-body="true", + tooltip-placement="bottom" + ) + i.fa.fa-folder-open-o + | + span.caret + ul.dropdown-menu.dropdown-menu-right.js-tags-dropdown-menu.tags-dropdown-menu( + role="menu" + ng-controller="TagListController" + ) + li.dropdown-header #{translate("add_to_folder")} + li( + ng-repeat="tag in tags | orderBy:'name'", + ng-controller="TagDropdownItemController" + ng-if="!tag.isV1" + ) + a(href="#", ng-click="addOrRemoveProjectsFromTag()", stop-propagation="click") + i.fa( + ng-class="{\ + 'fa-check-square-o': areSelectedProjectsInTag == true,\ + 'fa-square-o': areSelectedProjectsInTag == false,\ + 'fa-minus-square-o': areSelectedProjectsInTag == 'partial'\ + }" + ) + | {{tag.name}} + li.divider + li + a(href, ng-click="openNewTagModal()", stop-propagation="click") #{translate("create_new_folder")} - .btn-group(ng-hide="selectedProjects.length != 1", dropdown).dropdown - a.btn.btn-default.dropdown-toggle( - href, - data-toggle="dropdown", - dropdown-toggle - ) #{translate("more")} - span.caret - ul.dropdown-menu.dropdown-menu-right(role="menu") - li(ng-show="getFirstSelectedProject().accessLevel == 'owner'") - a( - href, - ng-click="openRenameProjectModal()" - ) #{translate("rename")} - li - a( - href, - ng-click="openCloneProjectModal()" - ) #{translate("make_copy")} + .btn-group(ng-hide="selectedProjects.length != 1", dropdown).dropdown + a.btn.btn-default.dropdown-toggle( + href, + data-toggle="dropdown", + dropdown-toggle + ) #{translate("more")} + span.caret + ul.dropdown-menu.dropdown-menu-right(role="menu") + li(ng-show="getFirstSelectedProject().accessLevel == 'owner'") + a( + href, + ng-click="openRenameProjectModal()" + ) #{translate("rename")} + li + a( + href, + ng-click="openCloneProjectModal()" + ) #{translate("make_copy")} - .btn-toolbar(ng-show="filter == 'archived'") - .btn-group(ng-hide="selectedProjects.length < 1") - a.btn.btn-default( - href, - data-original-title="Restore", - data-toggle="tooltip", - data-placement="bottom", - ng-click="restoreSelectedProjects()" - ) #{translate("restore")} + .btn-toolbar(ng-show="filter == 'archived'") + .btn-group(ng-hide="selectedProjects.length < 1") + a.btn.btn-default( + href, + data-original-title="Restore", + data-toggle="tooltip", + data-placement="bottom", + ng-click="restoreSelectedProjects()" + ) #{translate("restore")} - .btn-group(ng-hide="selectedProjects.length < 1") - a.btn.btn-danger( - href, - data-original-title="Delete Forever", - data-toggle="tooltip", - data-placement="bottom", - ng-click="openDeleteProjectsModal()" - ) #{translate("delete_forever")} + .btn-group(ng-hide="selectedProjects.length < 1") + a.btn.btn-danger( + href, + data-original-title="Delete Forever", + data-toggle="tooltip", + data-placement="bottom", + ng-click="openDeleteProjectsModal()" + ) #{translate("delete_forever")} .row.row-spaced - each warning in warnings - .col-xs-12 - .alert.alert-warning(role="alert")= warning + each warning in warnings + .col-xs-12 + .alert.alert-warning(role="alert")= warning - .col-xs-12 - .card.card-thin.project-list-card - ul.list-unstyled.project-list.structured-list( - select-all-list, - ng-if="projects.length > 0", - 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" + .col-xs-12 + .card.card-thin.project-list-card + .table-wrapper(max-height="projectListHeight - 25",) + table.table.table-hover.project-list( + select-all-list, + ng-if="projects.length > 0", + ng-cloak + ) + thead + tr + th.selectProject + 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) - 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')") - .col-xs-2 - 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")} - 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")} - diff --git a/services/web/app/views/project/list/v1-item.pug b/services/web/app/views/project/list/v1-item.pug deleted file mode 100644 index 5a8e37bca0..0000000000 --- a/services/web/app/views/project/list/v1-item.pug +++ /dev/null @@ -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}} \ No newline at end of file diff --git a/services/web/public/coffee/main/project-list/project-list.coffee b/services/web/public/coffee/main/project-list/project-list.coffee index 9a8d7f7a73..0146ff3348 100644 --- a/services/web/public/coffee/main/project-list/project-list.coffee +++ b/services/web/public/coffee/main/project-list/project-list.coffee @@ -13,7 +13,7 @@ define [ $scope.predicate = "lastUpdated" $scope.nUntagged = 0 $scope.reverse = true - $scope.searchText = + $scope.searchText = value : "" $timeout () -> @@ -37,7 +37,7 @@ define [ angular.element($window).bind "resize", () -> recalculateProjectListHeight() $scope.$apply() - + # Allow tags to be accessed on projects as well projectsById = {} for project in $scope.projects @@ -56,7 +56,7 @@ define [ tag.selected = true else tag.selected = false - + $scope.changePredicate = (newPredicate)-> if $scope.predicate == newPredicate $scope.reverse = !$scope.reverse @@ -145,7 +145,7 @@ define [ # We don't want hidden selections project.selected = false - localStorage("project_list", JSON.stringify({ + localStorage("project_list", JSON.stringify({ filter: $scope.filter, selectedTagId: selectedTag?._id })) @@ -461,7 +461,7 @@ define [ resolve: project: () -> project ) - + if storedUIOpts?.filter? if storedUIOpts.filter == "tag" and storedUIOpts.selectedTagId? markTagAsSelected(storedUIOpts.selectedTagId) @@ -485,11 +485,11 @@ define [ $scope.isLinkSharingProject = (project) -> return project.source == 'token' - $scope.ownerName = () -> - if $scope.project.accessLevel == "owner" + $scope.userDisplayName = (user) -> + if user? and user._id == window.user_id return "You" - else if $scope.project.owner? - return [$scope.project.owner.first_name, $scope.project.owner.last_name].filter((n) -> n?).join(" ") + else if user? + return [user.first_name, user.last_name].filter((n) -> n?).join(" ") else return "None" @@ -535,11 +535,11 @@ define [ url: "/project/#{$scope.project.id}?forever=true" headers: "X-CSRF-Token": window.csrfToken - }).then () -> + }).then () -> $scope.project.isTableActionInflight = false $scope._removeProjectFromList $scope.project for tag in $scope.tags $scope._removeProjectIdsFromTagArray(tag, [ $scope.project.id ]) $scope.updateVisibleProjects() - .catch () -> + .catch () -> $scope.project.isTableActionInflight = false diff --git a/services/web/public/stylesheets/app/project-list.less b/services/web/public/stylesheets/app/project-list.less index e5bc8da14c..86a72436c4 100644 --- a/services/web/public/stylesheets/app/project-list.less +++ b/services/web/public/stylesheets/app/project-list.less @@ -62,7 +62,7 @@ .small { color: @sidebar-color; } - } + } .project-list-sidebar when (@is-overleaf) { overflow-x: hidden; @@ -271,7 +271,7 @@ ul.structured-list { li { border-bottom: 1px solid @structured-list-border-color; padding: (@line-height-computed / 4) 0; - + &:last-child { border-bottom: 0 none; } @@ -294,7 +294,7 @@ ul.structured-list { .header when (@is-overleaf = false) { text-transform: uppercase; } - + .select-item, .select-all { position: absolute; left: @line-height-computed; @@ -314,8 +314,38 @@ ul.structured-list { padding: 0 (@line-height-computed / 4); } -ul.project-list { - li { +.table-wrapper { + 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) { font-size: .8rem; } @@ -325,12 +355,13 @@ ul.project-list { .owner when (@is-overleaf = false) { margin-right: 0; } - .projectName { + a.projectName, button.projectName { margin-right: @line-height-computed / 4; padding: 0; vertical-align: inherit; white-space: normal; text-align: left; + color: @structured-list-link-color; } .tag-label { @@ -369,6 +400,7 @@ ul.project-list { .v1-badge { margin-left: -4px; + margin-right: -4px; } .action-btn-row-header, .action-btn-row { @@ -564,7 +596,7 @@ ul.project-list { &:last-child { margin-bottom: 0; } - } + } .announcement-header { .page-header; margin: 0; diff --git a/services/web/public/stylesheets/components/tables.less b/services/web/public/stylesheets/components/tables.less index c41989c04d..30ede6697c 100755 --- a/services/web/public/stylesheets/components/tables.less +++ b/services/web/public/stylesheets/components/tables.less @@ -34,7 +34,7 @@ th { // Bottom align for column headings > thead > tr > th { vertical-align: bottom; - border-bottom: 2px solid @table-border-color; + border-bottom: 1px solid @table-border-color; } // Remove top border from thead by default > caption + thead, diff --git a/services/web/public/stylesheets/core/_common-variables.less b/services/web/public/stylesheets/core/_common-variables.less index 9d7eab3493..41bdbd2d9b 100644 --- a/services/web/public/stylesheets/core/_common-variables.less +++ b/services/web/public/stylesheets/core/_common-variables.less @@ -110,7 +110,7 @@ //## Customizes the `.table` component with basic values, each used across all table variations. //** Padding for `