mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge pull request #87 from sharelatex/as-fetch-ol-projects
Show V1 (OL) projects in project list
This commit is contained in:
commit
51ec0ab4c7
10 changed files with 172 additions and 44 deletions
|
@ -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
|
||||
|
|
40
services/web/app/views/project/list/item.pug
Normal file
40
services/web/app/views/project/list/item.pug
Normal file
|
@ -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}}
|
|
@ -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
|
||||
|
|
|
@ -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="#",
|
||||
|
|
10
services/web/app/views/project/list/v1-item.pug
Normal file
10
services/web/app/views/project/list/v1-item.pug
Normal file
|
@ -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}}
|
|
@ -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
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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;
|
||||
|
|
10
services/web/public/stylesheets/app/v1-badge.less
Normal file
10
services/web/public/stylesheets/app/v1-badge.less
Normal file
|
@ -0,0 +1,10 @@
|
|||
.v1-badge {
|
||||
&:extend(.label);
|
||||
&:extend(.label-default);
|
||||
vertical-align: 11%;
|
||||
padding: 1px 3px;
|
||||
margin: 0 6px;
|
||||
&:before {
|
||||
content: "V1";
|
||||
}
|
||||
}
|
|
@ -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)=>
|
||||
|
|
Loading…
Reference in a new issue