diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee index ff20664c43..bbff14d2a1 100644 --- a/services/web/app/coffee/Features/Project/ProjectController.coffee +++ b/services/web/app/coffee/Features/Project/ProjectController.coffee @@ -24,6 +24,7 @@ AnalyticsManager = require "../Analytics/AnalyticsManager" Sources = require "../Authorization/Sources" TokenAccessHandler = require '../TokenAccess/TokenAccessHandler' CollaboratorsHandler = require '../Collaborators/CollaboratorsHandler' +Modules = require '../../infrastructure/Modules' crypto = require 'crypto' module.exports = ProjectController = @@ -148,6 +149,11 @@ module.exports = ProjectController = NotificationsHandler.getUserNotifications user_id, cb projects: (cb)-> ProjectGetter.findAllUsersProjects user_id, 'name lastUpdated publicAccesLevel archived owner_ref tokens', cb + v1Projects: (cb) -> + Modules.hooks.fire "findAllV1Projects", user_id, (error, projects = []) -> + if error? and error.message == 'No V1 connection' + return cb(null, projects: [], tags: [], noConnection: true) + return cb(error, projects[0]) # hooks.fire returns an array of results, only need first hasSubscription: (cb)-> LimitationsManager.userHasSubscriptionOrIsGroupMember currentUser, cb user: (cb) -> @@ -157,11 +163,12 @@ module.exports = ProjectController = logger.err err:err, "error getting data for project list page" return next(err) logger.log results:results, user_id:user_id, "rendering project list" - tags = results.tags[0] + v1Tags = results.v1Projects?.tags or [] + tags = results.tags[0].concat(v1Tags) notifications = require("underscore").map results.notifications, (notification)-> notification.html = req.i18n.translate(notification.templateKey, notification.messageOpts) return notification - projects = ProjectController._buildProjectList results.projects + projects = ProjectController._buildProjectList results.projects, results.v1Projects?.projects user = results.user ProjectController._injectProjectOwners projects, (error, projects) -> return next(error) if error? @@ -173,6 +180,8 @@ module.exports = ProjectController = notifications: notifications or [] user: user hasSubscription: results.hasSubscription[0] + isShowingV1Projects: results.v1Projects? + noV1Connection: results.v1Projects?.noConnection } if Settings?.algolia?.app_id? and Settings?.algolia?.read_only_api_key? @@ -390,7 +399,7 @@ module.exports = ProjectController = showLinkSharingOnboarding: showLinkSharingOnboarding timer.done() - _buildProjectList: (allProjects)-> + _buildProjectList: (allProjects, v1Projects = [])-> {owned, readAndWrite, readOnly, tokenReadAndWrite, tokenReadOnly} = allProjects projects = [] for project in owned @@ -400,6 +409,8 @@ module.exports = ProjectController = projects.push ProjectController._buildProjectViewModel(project, "readWrite", Sources.INVITE) for project in readOnly projects.push ProjectController._buildProjectViewModel(project, "readOnly", Sources.INVITE) + for project in v1Projects + projects.push ProjectController._buildV1ProjectViewModel(project) # Token-access # Only add these projects if they're not already present, this gives us cascading access # from 'owner' => 'token-read-only' @@ -424,9 +435,20 @@ module.exports = ProjectController = archived: !!project.archived owner_ref: project.owner_ref tokens: project.tokens + isV1Project: false } return model + _buildV1ProjectViewModel: (project) -> + { + id: project.id + name: project.title + lastUpdated: new Date(project.updated_at * 1000) # Convert from epoch + accessLevel: "readOnly", + archived: project.removed || project.archived + isV1Project: true + } + _injectProjectOwners: (projects, callback = (error, projects) ->) -> users = {} for project in projects diff --git a/services/web/app/views/project/list/item.pug b/services/web/app/views/project/list/item.pug new file mode 100644 index 0000000000..f246ad34d4 --- /dev/null +++ b/services/web/app/views/project/list/item.pug @@ -0,0 +1,40 @@ +.col-xs-6 + input.select-item( + select-individual, + type="checkbox", + 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)" + ) × + +.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" + ) +.col-xs-4 + span.last-modified {{project.lastUpdated | formatDate}} \ No newline at end of file diff --git a/services/web/app/views/project/list/project-list.pug b/services/web/app/views/project/list/project-list.pug index aba8ed08be..0db4f49400 100644 --- a/services/web/app/views/project/list/project-list.pug +++ b/services/web/app/views/project/list/project-list.pug @@ -114,6 +114,10 @@ ) #{translate("delete_forever")} .row.row-spaced + if noV1Connection + .col-xs-12 + .alert.alert-warning No V1 Connection + .col-xs-12 .card.card-thin.project-list-card ul.list-unstyled.project-list.structured-list( @@ -142,47 +146,15 @@ ng-repeat="project in visibleProjects | orderBy:predicate:reverse", ng-controller="ProjectListItemController" ) - .row(select-row) - .col-xs-6 - input.select-item( - select-individual, - type="checkbox", - 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)" - ) × - - .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" - ) - .col-xs-4 - span.last-modified {{project.lastUpdated | formatDate}} + .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 diff --git a/services/web/app/views/project/list/side-bar.pug b/services/web/app/views/project/list/side-bar.pug index fe053f0019..9fceb4cc86 100644 --- a/services/web/app/views/project/list/side-bar.pug +++ b/services/web/app/views/project/list/side-bar.pug @@ -45,6 +45,9 @@ a(href) #{translate("shared_with_you")} li(ng-class="{active: (filter == 'archived')}", ng-click="filterProjects('archived')") a(href) #{translate("deleted_projects")} + if isShowingV1Projects + li(ng-class="{active: (filter == 'v1')}", ng-click="filterProjects('v1')") + a(href) #{translate("v1_projects")} li.separator h2 #{translate("folders")} li.tag( @@ -62,6 +65,11 @@ ) span.name {{tag.name}} span.subdued ({{tag.project_ids.length}}) + span.v1-badge( + ng-if="tag.isV1", + ng-cloak, + aria-label=translate("v1_badge") + ) span.dropdown.tag-menu(dropdown) a.dropdown-toggle( href="#", diff --git a/services/web/app/views/project/list/v1-item.pug b/services/web/app/views/project/list/v1-item.pug new file mode 100644 index 0000000000..5631788aeb --- /dev/null +++ b/services/web/app/views/project/list/v1-item.pug @@ -0,0 +1,10 @@ +.col-xs-8 + span.v1-badge(aria-label=translate("v1_badge")) + span + a.projectName( + href=settings.overleaf.host + "/{{project.id}}" + stop-propagation="click" + ) {{project.name}} + +.col-xs-4 + span.last-modified {{project.lastUpdated | formatDate}} \ 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 8c39df060f..1d81504c74 100644 --- a/services/web/public/coffee/main/project-list/project-list.coffee +++ b/services/web/public/coffee/main/project-list/project-list.coffee @@ -116,6 +116,10 @@ define [ if $scope.filter == "shared" and project.accessLevel == "owner" visible = false + # Hide projects from V1 if we only want to see shared projects + if $scope.filter == "shared" and project.isV1Project + visible = false + # Hide projects we don't own if we only want to see owned projects if $scope.filter == "owned" and project.accessLevel != "owner" visible = false @@ -129,6 +133,9 @@ define [ if project.archived visible = false + if $scope.filter == "v1" and !project.isV1Project + visible = false + if visible $scope.visibleProjects.push project else diff --git a/services/web/public/stylesheets/_style_includes.less b/services/web/public/stylesheets/_style_includes.less index a5c7589ef0..54539a4cb4 100644 --- a/services/web/public/stylesheets/_style_includes.less +++ b/services/web/public/stylesheets/_style_includes.less @@ -78,6 +78,7 @@ @import "app/invite.less"; @import "app/review-features-page.less"; @import "app/error-pages.less"; +@import "app/v1-badge.less"; @import "../js/libs/pdfListView/TextLayer.css"; @import "../js/libs/pdfListView/AnnotationsLayer.css"; diff --git a/services/web/public/stylesheets/app/project-list.less b/services/web/public/stylesheets/app/project-list.less index 1568915296..1ca170ff89 100644 --- a/services/web/public/stylesheets/app/project-list.less +++ b/services/web/public/stylesheets/app/project-list.less @@ -366,6 +366,11 @@ ul.project-list { border-top-left-radius: 0; border-bottom-left-radius: 0; } + + .v1-badge { + margin-right: 9px; + margin-left: 7px; + } } i.tablesort { padding-left: 8px; diff --git a/services/web/public/stylesheets/app/v1-badge.less b/services/web/public/stylesheets/app/v1-badge.less new file mode 100644 index 0000000000..ced9d04c81 --- /dev/null +++ b/services/web/public/stylesheets/app/v1-badge.less @@ -0,0 +1,10 @@ +.v1-badge { + &:extend(.label); + &:extend(.label-default); + vertical-align: 11%; + padding: 1px 3px; + margin: 0 6px; + &:before { + content: "V1"; + } +} \ No newline at end of file diff --git a/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee b/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee index 498090f790..35176acd4b 100644 --- a/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee @@ -66,6 +66,9 @@ describe "ProjectController", -> protectTokens: sinon.stub() @CollaboratorsHandler = userIsTokenMember: sinon.stub().callsArgWith(2, null, false) + @Modules = + hooks: + fire: sinon.stub() @ProjectController = SandboxedModule.require modulePath, requires: "settings-sharelatex":@settings "logger-sharelatex": @@ -93,6 +96,7 @@ describe "ProjectController", -> "../Analytics/AnalyticsManager": @AnalyticsManager "../TokenAccess/TokenAccessHandler": @TokenAccessHandler "../Collaborators/CollaboratorsHandler": @CollaboratorsHandler + "../../infrastructure/Modules": @Modules @projectName = "£12321jkj9ujkljds" @req = @@ -263,6 +267,7 @@ describe "ProjectController", -> @TagsHandler.getAllTags.callsArgWith(1, null, @tags, {}) @NotificationsHandler.getUserNotifications = sinon.stub().callsArgWith(1, null, @notifications, {}) @ProjectGetter.findAllUsersProjects.callsArgWith(2, null, @allProjects) + @Modules.hooks.fire.withArgs('findAllV1Projects', @user._id).yields(undefined) # Without integration module hook, cb returns undefined it "should render the project/list page", (done)-> @res.render = (pageName, opts)=> @@ -295,6 +300,53 @@ describe "ProjectController", -> done() @ProjectController.projectListPage @req, @res + describe 'with overleaf-integration-web-module hook', -> + beforeEach -> + @V1Response = + projects: [ + { id: '123mockV1Id', title: 'mock title', updated_at: 1509616411, removed: false, archived: false } + { id: '456mockV1Id', title: 'mock title 2', updated_at: 1509616411, removed: true, archived: false } + ], + tags: [ + { name: 'mock tag', project_ids: ['123mockV1Id'] } + ] + @Modules.hooks.fire.withArgs('findAllV1Projects', @user._id).yields(null, [@V1Response]) # Need to wrap response in array, as multiple hooks could fire + + it 'should include V1 projects', (done) -> + @res.render = (pageName, opts) => + opts.projects.length.should.equal ( + @projects.length + + @collabertions.length + + @readOnly.length + + @tokenReadAndWrite.length + + @tokenReadOnly.length + + @V1Response.projects.length + ) + opts.projects.forEach (p) -> + # Check properties correctly mapped from V1 + expect(p).to.have.property 'id' + expect(p).to.have.property 'name' + expect(p).to.have.property 'lastUpdated' + expect(p).to.have.property 'accessLevel' + expect(p).to.have.property 'archived' + done() + @ProjectController.projectListPage @req, @res + + it 'should include V1 tags', (done) -> + @res.render = (pageName, opts) => + opts.tags.length.should.equal (@tags.length + @V1Response.tags.length) + opts.tags.forEach (t) -> + expect(t).to.have.property 'name' + expect(t).to.have.property 'project_ids' + done() + @ProjectController.projectListPage @req, @res + + it 'should have isShowingV1Projects flag', (done) -> + @res.render = (pageName, opts) => + opts.isShowingV1Projects.should.equal true + done() + @ProjectController.projectListPage @req, @res + describe "projectListPage with duplicate projects", -> beforeEach -> @@ -338,6 +390,7 @@ describe "ProjectController", -> @TagsHandler.getAllTags.callsArgWith(1, null, @tags, {}) @NotificationsHandler.getUserNotifications = sinon.stub().callsArgWith(1, null, @notifications, {}) @ProjectGetter.findAllUsersProjects.callsArgWith(2, null, @allProjects) + @Modules.hooks.fire.withArgs('findAllV1Projects', @user._id).yields(undefined) # Without integration module hook, cb returns undefined it "should render the project/list page", (done)-> @res.render = (pageName, opts)=>