Merge branch 'master' into sk-server-side-labels-loading

This commit is contained in:
Shane Kilkelly
2017-06-15 08:49:14 +01:00
222 changed files with 34911 additions and 40634 deletions
@@ -176,7 +176,7 @@ module.exports = EditorController =
callback?()
renameProject: (project_id, newName, callback = (err) ->) ->
ProjectDetailsHandler.renameProject project_id, newName, ->
ProjectDetailsHandler.renameProject project_id, newName, (err) ->
if err?
logger.err err:err, project_id:project_id, newName:newName, "error renaming project"
return callback(err)
@@ -25,6 +25,18 @@ module.exports = ErrorController =
else if error instanceof Errors.TooManyRequestsError
logger.warn {err: error, url: req.url}, "too many requests error"
res.sendStatus(429)
else if error instanceof Errors.InvalidNameError
logger.warn {err: error, url: req.url}, "invalid name error"
res.status(400)
res.send(error.message)
else
logger.error err: error, url:req.url, method:req.method, user:user, "error passed to top level next middlewear"
ErrorController.serverError req, res
handleApiError: (error, req, res, next) ->
if error instanceof Errors.NotFoundError
logger.warn {err: error, url: req.url}, "not found error"
res.sendStatus(404)
else
logger.error err: error, url:req.url, method:req.method, user:user, "error passed to top level next middlewear"
res.sendStatus(500)
@@ -5,7 +5,6 @@ NotFoundError = (message) ->
return error
NotFoundError.prototype.__proto__ = Error.prototype
ServiceNotConfiguredError = (message) ->
error = new Error(message)
error.name = "ServiceNotConfiguredError"
@@ -13,7 +12,6 @@ ServiceNotConfiguredError = (message) ->
return error
ServiceNotConfiguredError.prototype.__proto__ = Error.prototype
TooManyRequestsError = (message) ->
error = new Error(message)
error.name = "TooManyRequestsError"
@@ -21,8 +19,15 @@ TooManyRequestsError = (message) ->
return error
TooManyRequestsError.prototype.__proto__ = Error.prototype
InvalidNameError = (message) ->
error = new Error(message)
error.name = "InvalidNameError"
error.__proto__ = InvalidNameError.prototype
return error
InvalidNameError.prototype.__proto__ = Error.prototype
module.exports = Errors =
NotFoundError: NotFoundError
ServiceNotConfiguredError: ServiceNotConfiguredError
TooManyRequestsError: TooManyRequestsError
InvalidNameError: InvalidNameError
@@ -101,7 +101,7 @@ module.exports = ProjectController =
res.send(project_id:project._id)
newProject: (req, res)->
newProject: (req, res, next)->
user_id = AuthenticationController.getLoggedInUserId(req)
projectName = req.body.projectName?.trim()
template = req.body.template
@@ -113,25 +113,17 @@ module.exports = ProjectController =
else
projectCreationHandler.createBasicProject user_id, projectName, cb
], (err, project)->
if err?
logger.error err: err, project: project, user: user_id, name: projectName, templateType: template, "error creating project"
res.sendStatus 500
else
logger.log project: project, user: user_id, name: projectName, templateType: template, "created project"
res.send {project_id:project._id}
return next(err) if err?
logger.log project: project, user: user_id, name: projectName, templateType: template, "created project"
res.send {project_id:project._id}
renameProject: (req, res)->
renameProject: (req, res, next)->
project_id = req.params.Project_id
newName = req.body.newProjectName
if newName.length > 150
return res.sendStatus 400
editorController.renameProject project_id, newName, (err)->
if err?
logger.err err:err, project_id:project_id, newName:newName, "problem renaming project"
res.sendStatus 500
else
res.sendStatus 200
return next(err) if err?
res.sendStatus 200
projectListPage: (req, res, next)->
timer = new metrics.Timer("project-list")
@@ -6,6 +6,7 @@ ObjectId = require('mongoose').Types.ObjectId
Project = require('../../models/Project').Project
Folder = require('../../models/Folder').Folder
ProjectEntityHandler = require('./ProjectEntityHandler')
ProjectDetailsHandler = require('./ProjectDetailsHandler')
User = require('../../models/User').User
fs = require('fs')
Path = require "path"
@@ -15,19 +16,21 @@ module.exports = ProjectCreationHandler =
createBlankProject : (owner_id, projectName, callback = (error, project) ->)->
metrics.inc("project-creation")
logger.log owner_id:owner_id, projectName:projectName, "creating blank project"
rootFolder = new Folder {'name':'rootFolder'}
project = new Project
owner_ref : new ObjectId(owner_id)
name : projectName
if Settings.currentImageName?
project.imageName = Settings.currentImageName
project.rootFolder[0] = rootFolder
User.findById owner_id, "ace.spellCheckLanguage", (err, user)->
project.spellCheckLanguage = user.ace.spellCheckLanguage
project.save (err)->
return callback(err) if err?
callback err, project
ProjectDetailsHandler.validateProjectName projectName, (error) ->
return callback(error) if error?
logger.log owner_id:owner_id, projectName:projectName, "creating blank project"
rootFolder = new Folder {'name':'rootFolder'}
project = new Project
owner_ref : new ObjectId(owner_id)
name : projectName
if Settings.currentImageName?
project.imageName = Settings.currentImageName
project.rootFolder[0] = rootFolder
User.findById owner_id, "ace.spellCheckLanguage", (err, user)->
project.spellCheckLanguage = user.ace.spellCheckLanguage
project.save (err)->
return callback(err) if err?
callback err, project
createBasicProject : (owner_id, projectName, callback = (error, project) ->)->
self = @
@@ -7,8 +7,7 @@ _ = require("underscore")
PublicAccessLevels = require("../Authorization/PublicAccessLevels")
Errors = require("../Errors/Errors")
module.exports =
module.exports = ProjectDetailsHandler =
getDetails: (project_id, callback)->
ProjectGetter.getProject project_id, {name:true, description:true, compiler:true, features:true, owner_ref:true}, (err, project)->
if err?
@@ -39,16 +38,29 @@ module.exports =
callback(err)
renameProject: (project_id, newName, callback = ->)->
logger.log project_id: project_id, newName:newName, "renaming project"
ProjectGetter.getProject project_id, {name:true}, (err, project)->
if err? or !project?
logger.err err:err, project_id:project_id, "error getting project or could not find it todo project rename"
return callback(err)
oldProjectName = project.name
Project.update _id:project_id, {name: newName}, (err, project)=>
if err?
ProjectDetailsHandler.validateProjectName newName, (error) ->
return callback(error) if error?
logger.log project_id: project_id, newName:newName, "renaming project"
ProjectGetter.getProject project_id, {name:true}, (err, project)->
if err? or !project?
logger.err err:err, project_id:project_id, "error getting project or could not find it todo project rename"
return callback(err)
tpdsUpdateSender.moveEntity {project_id:project_id, project_name:oldProjectName, newProjectName:newName}, callback
oldProjectName = project.name
Project.update _id:project_id, {name: newName}, (err, project)=>
if err?
return callback(err)
tpdsUpdateSender.moveEntity {project_id:project_id, project_name:oldProjectName, newProjectName:newName}, callback
MAX_PROJECT_NAME_LENGTH: 150
validateProjectName: (name, callback = (error) ->) ->
if name.length == 0
return callback(new Errors.InvalidNameError("Project name cannot be blank"))
else if name.length > @MAX_PROJECT_NAME_LENGTH
return callback(new Errors.InvalidNameError("Project name is too long"))
else if name.indexOf("/") > -1
return callback(new Errors.InvalidNameError("Project name cannot not contain / characters"))
else
return callback()
setPublicAccessLevel : (project_id, newAccessLevel, callback = ->)->
logger.log project_id: project_id, level: newAccessLevel, "set public access level"
@@ -1,6 +1,5 @@
Project = require('../../models/Project').Project
logger = require('logger-sharelatex')
Project = require("../../models/Project").Project
module.exports =
markAsUpdated : (project_id, callback)->
@@ -64,8 +64,9 @@ module.exports =
if !subscription?
logger.err user_id:user_id, "no subscription found for user"
return callback("no subscription found")
limitReached = subscription.member_ids.length >= subscription.membersLimit
logger.log user_id:user_id, limitReached:limitReached, currentTotal: subscription.member_ids.length, membersLimit: subscription.membersLimit, "checking if subscription members limit has been reached"
currentTotal = (subscription.member_ids or []).length + (subscription.invited_emails or []).length
limitReached = currentTotal >= subscription.membersLimit
logger.log user_id:user_id, limitReached:limitReached, currentTotal: currentTotal, membersLimit: subscription.membersLimit, "checking if subscription members limit has been reached"
callback(err, limitReached, subscription)
getOwnerIdOfProject = (project_id, callback)->
@@ -33,6 +33,14 @@ module.exports =
return res.sendStatus 500
res.send()
removeEmailInviteFromGroup: (req, res)->
adminUserId = AuthenticationController.getLoggedInUserId(req)
email = req.params.email
logger.log {adminUserId, email}, "removing email invite from group subscription"
SubscriptionGroupHandler.removeEmailInviteFromGroup adminUserId, email, (err)->
return next(error) if error?
res.send()
removeSelfFromGroup: (req, res)->
adminUserId = req.query.admin_user_id
userToRemove_id = AuthenticationController.getLoggedInUserId(req)
@@ -1,6 +1,5 @@
async = require("async")
_ = require("underscore")
UserCreator = require("../User/UserCreator")
SubscriptionUpdater = require("./SubscriptionUpdater")
SubscriptionLocator = require("./SubscriptionLocator")
UserLocator = require("../User/UserLocator")
@@ -15,36 +14,40 @@ module.exports = SubscriptionGroupHandler =
addUserToGroup: (adminUserId, newEmail, callback)->
logger.log adminUserId:adminUserId, newEmail:newEmail, "adding user to group"
UserCreator.getUserOrCreateHoldingAccount newEmail, (err, user)->
LimitationsManager.hasGroupMembersLimitReached adminUserId, (err, limitReached, subscription)->
if err?
logger.err err:err, adminUserId:adminUserId, newEmail:newEmail, "error creating user for holding account"
logger.err err:err, adminUserId:adminUserId, newEmail:newEmail, "error checking if limit reached for group plan"
return callback(err)
if !user?
msg = "no user returned whenc reating holidng account or getting user"
logger.err adminUserId:adminUserId, newEmail:newEmail, msg
return callback(msg)
LimitationsManager.hasGroupMembersLimitReached adminUserId, (err, limitReached, subscription)->
if err?
logger.err err:err, adminUserId:adminUserId, newEmail:newEmail, "error checking if limit reached for group plan"
return callback(err)
if limitReached
logger.err adminUserId:adminUserId, newEmail:newEmail, "group subscription limit reached not adding user to group"
return callback(limitReached:limitReached)
SubscriptionUpdater.addUserToGroup adminUserId, user._id, (err)->
if err?
logger.err err:err, "error adding user to group"
return callback(err)
NotificationsBuilder.groupPlan(user, {subscription_id:subscription._id}).read()
userViewModel = buildUserViewModel(user)
callback(err, userViewModel)
if limitReached
logger.err adminUserId:adminUserId, newEmail:newEmail, "group subscription limit reached not adding user to group"
return callback(limitReached:limitReached)
UserLocator.findByEmail newEmail, (err, user)->
return callback(err) if err?
if user?
SubscriptionUpdater.addUserToGroup adminUserId, user._id, (err)->
if err?
logger.err err:err, "error adding user to group"
return callback(err)
NotificationsBuilder.groupPlan(user, {subscription_id:subscription._id}).read()
userViewModel = buildUserViewModel(user)
callback(err, userViewModel)
else
SubscriptionUpdater.addEmailInviteToGroup adminUserId, newEmail, (err) ->
return callback(err) if err?
userViewModel = buildEmailInviteViewModel(newEmail)
callback(err, userViewModel)
removeUserFromGroup: (adminUser_id, userToRemove_id, callback)->
SubscriptionUpdater.removeUserFromGroup adminUser_id, userToRemove_id, callback
removeEmailInviteFromGroup: (adminUser_id, email, callback) ->
SubscriptionUpdater.removeEmailInviteFromGroup adminUser_id, email, callback
getPopulatedListOfMembers: (adminUser_id, callback)->
SubscriptionLocator.getUsersSubscription adminUser_id, (err, subscription)->
users = []
for email in subscription.invited_emails or []
users.push buildEmailInviteViewModel(email)
jobs = _.map subscription.member_ids, (user_id)->
return (cb)->
UserLocator.findById user_id, (err, user)->
@@ -91,7 +94,21 @@ module.exports = SubscriptionGroupHandler =
return callback()
SubscriptionGroupHandler.addUserToGroup subscription?.admin_id, userEmail, callback
convertEmailInvitesToMemberships: (email, user_id, callback = (err) ->) ->
SubscriptionLocator.getGroupsWithEmailInvite email, (err, groups = []) ->
return callback(err) if err?
logger.log {email, user_id, groups}, "found groups to convert from email invite to member"
jobs = []
for group in groups
do (group) ->
jobs.push (cb) ->
SubscriptionUpdater.removeEmailInviteFromGroup group.admin_id, email, (err) ->
return cb(err) if err?
SubscriptionUpdater.addUserToGroup group.admin_id, user_id, (err) ->
return cb(err) if err?
logger.log {group_id: group._id, user_id, email}, "converted email invite to group membership"
return cb()
async.series jobs, callback
buildUserViewModel = (user)->
u =
@@ -101,3 +118,9 @@ buildUserViewModel = (user)->
holdingAccount: user.holdingAccount
_id: user._id
return u
buildEmailInviteViewModel = (email) ->
return {
email: email
holdingAccount: true
}
@@ -30,3 +30,6 @@ module.exports =
getGroupSubscriptionMemberOf: (user_id, callback)->
Subscription.findOne {member_ids: user_id}, {_id:1, planCode:1}, callback
getGroupsWithEmailInvite: (email, callback) ->
Subscription.find { invited_emails: email }, callback
@@ -25,6 +25,7 @@ module.exports =
webRouter.post '/subscription/group/user', AuthenticationController.requireLogin(), SubscriptionGroupController.addUserToGroup
webRouter.get '/subscription/group/export', AuthenticationController.requireLogin(), SubscriptionGroupController.exportGroupCsv
webRouter.delete '/subscription/group/user/:user_id', AuthenticationController.requireLogin(), SubscriptionGroupController.removeUserFromGroup
webRouter.delete '/subscription/group/email/:email', AuthenticationController.requireLogin(), SubscriptionGroupController.removeEmailInviteFromGroup
webRouter.delete '/subscription/group/user', AuthenticationController.requireLogin(), SubscriptionGroupController.removeSelfFromGroup
@@ -35,6 +35,14 @@ module.exports = SubscriptionUpdater =
logger.err err:err, searchOps:searchOps, insertOperation:insertOperation, "error findy and modify add user to group"
return callback(err)
UserFeaturesUpdater.updateFeatures user_id, subscription.planCode, callback
addEmailInviteToGroup: (adminUser_id, email, callback) ->
logger.log {adminUser_id, email}, "adding email into mongo subscription"
searchOps =
admin_id: adminUser_id
insertOperation =
"$addToSet": {invited_emails: email}
Subscription.findAndModify searchOps, insertOperation, callback
removeUserFromGroup: (adminUser_id, user_id, callback)->
searchOps =
@@ -47,6 +55,12 @@ module.exports = SubscriptionUpdater =
return callback(err)
SubscriptionUpdater._setUsersMinimumFeatures user_id, callback
removeEmailInviteFromGroup: (adminUser_id, email, callback)->
Subscription.update {
admin_id: adminUser_id
}, "$pull": {
invited_emails: email
}, callback
_createNewSubscription: (adminUser_id, callback)->
logger.log adminUser_id:adminUser_id, "creating new subscription"
@@ -7,19 +7,22 @@ logger = require("logger-sharelatex")
module.exports = UserHandler =
populateGroupLicenceInvite: (user, callback = ->)->
logger.log user_id:user._id, "populating any potential group licence invites"
licence = SubscriptionDomainHandler.getLicenceUserCanJoin user
if !licence?
return callback()
SubscriptionGroupHandler.convertEmailInvitesToMemberships user.email, user._id, (err) ->
return callback(err) if err?
SubscriptionGroupHandler.isUserPartOfGroup user._id, licence.subscription_id, (err, alreadyPartOfGroup)->
if err?
return callback(err)
else if alreadyPartOfGroup
logger.log user_id:user._id, "user already part of group, not creating notifcation for them"
logger.log user_id:user._id, "populating any potential group licence invites"
licence = SubscriptionDomainHandler.getLicenceUserCanJoin user
if !licence?
return callback()
else
NotificationsBuilder.groupPlan(user, licence).create(callback)
SubscriptionGroupHandler.isUserPartOfGroup user._id, licence.subscription_id, (err, alreadyPartOfGroup)->
if err?
return callback(err)
else if alreadyPartOfGroup
logger.log user_id:user._id, "user already part of group, not creating notifcation for them"
return callback()
else
NotificationsBuilder.groupPlan(user, licence).create(callback)
setupLoginData: (user, callback = ->)->
@populateGroupLicenceInvite user, callback
@@ -1,5 +1,5 @@
version = {
"pdfjs": "1.7.225"
"pdfjs": "1.8.188"
"moment": "2.9.0"
"ace": "1.2.5"
}
@@ -164,11 +164,12 @@ server = require('http').createServer(app)
# process api routes first, if nothing matched fall though and use
# web middlewear + routes
app.use(apiRouter)
app.use(ErrorController.handleApiError)
app.use(webRouter)
app.use(ErrorController.handleError)
router = new Router(webRouter, apiRouter)
app.use ErrorController.handleError
module.exports =
app: app
@@ -6,7 +6,8 @@ ObjectId = Schema.ObjectId
SubscriptionSchema = new Schema
admin_id : {type:ObjectId, ref:'User', index: {unique: true, dropDups: true}}
member_ids : [ type:ObjectId, ref:'User' ]
member_ids : [ type:ObjectId, ref:'User' ]
invited_emails: [ String ]
recurlySubscription_id : String
planCode : {type: String}
groupPlan : {type: Boolean, default: false}
@@ -167,6 +167,8 @@ script(type='text/ng-template', id='cloneProjectModalTemplate')
.modal-header
h3 #{translate("copy_project")}
.modal-body
.alert.alert-danger(ng-show="state.error.message") {{ state.error.message}}
.alert.alert-danger(ng-show="state.error && !state.error.message") #{translate("generic_something_went_wrong")}
form(name="cloneProjectForm", novalidate)
.form-group
label #{translate("new_name")}
@@ -8,20 +8,20 @@
) !{translate("track_changes_is_on")}
a.rp-bulk-actions-btn(
href
ng-if="reviewPanel.selectedEntryIds.length > 1"
ng-if="reviewPanel.nVisibleSelectedChanges > 1"
ng-click="showBulkAcceptDialog();"
)
i.fa.fa-check
|  #{translate("accept_all")}
| ({{ reviewPanel.selectedEntryIds.length }})
| ({{ reviewPanel.nVisibleSelectedChanges }})
a.rp-bulk-actions-btn(
href
ng-if="reviewPanel.selectedEntryIds.length > 1"
ng-if="reviewPanel.nVisibleSelectedChanges > 1"
ng-click="showBulkRejectDialog();"
)
i.fa.fa-times
|  #{translate("reject_all")}
| ({{ reviewPanel.selectedEntryIds.length }})
| ({{ reviewPanel.nVisibleSelectedChanges }})
a.rp-add-comment-btn(
href
ng-if="reviewPanel.entries[editor.open_doc_id]['add-comment'] != null"
@@ -75,8 +75,19 @@
change-entry(
entry="entry"
user="users[entry.metadata.user_id]"
on-reject="rejectChange(entry_id);"
on-accept="acceptChange(entry_id);"
on-reject="rejectChanges(entry.entry_ids);"
on-accept="acceptChanges(entry.entry_ids);"
on-indicator-click="toggleReviewPanel();"
on-body-click="gotoEntry(editor.open_doc_id, entry)"
permissions="permissions"
)
div(ng-if="entry.type === 'aggregate-change'")
aggregate-change-entry(
entry="entry"
user="users[entry.metadata.user_id]"
on-reject="rejectChanges(entry.entry_ids);"
on-accept="acceptChanges(entry.entry_ids);"
on-indicator-click="toggleReviewPanel();"
on-body-click="gotoEntry(editor.open_doc_id, entry)"
permissions="permissions"
@@ -106,7 +117,7 @@
bulk-actions-entry(
on-bulk-accept="showBulkAcceptDialog();"
on-bulk-reject="showBulkRejectDialog();"
n-entries="reviewPanel.selectedEntryIds.length"
n-entries="reviewPanel.nVisibleSelectedChanges"
)
.rp-entry-list(
@@ -142,11 +153,18 @@
change-entry(
entry="entry"
user="users[entry.metadata.user_id]"
on-indicator-click="toggleReviewPanel();"
ng-click="gotoEntry(doc.doc.id, entry)"
permissions="permissions"
)
div(ng-if="entry.type === 'aggregate-change'")
aggregate-change-entry(
entry="entry"
user="users[entry.metadata.user_id]"
ng-click="gotoEntry(editor.open_doc_id, entry)"
permissions="permissions"
)
div(ng-if="entry.type === 'comment'")
comment-entry(
entry="entry"
@@ -154,7 +172,6 @@
on-reply="submitReply(entry, entry_id);"
on-save-edit="saveEdit(entry.thread_id, comment)"
on-delete="deleteComment(entry.thread_id, comment)"
on-indicator-click="toggleReviewPanel();"
ng-click="gotoEntry(doc.doc.id, entry)"
permissions="permissions"
)
@@ -222,6 +239,42 @@ script(type='text/ng-template', id='changeEntryTemplate')
i.fa.fa-check
|  #{translate("accept")}
script(type='text/ng-template', id='aggregateChangeEntryTemplate')
div
.rp-entry-callout.rp-entry-callout-aggregate
.rp-entry-indicator(
ng-class="{ 'rp-entry-indicator-focused': entry.focused }"
ng-click="onIndicatorClick();"
)
i.fa.fa-pencil
.rp-entry.rp-entry-aggregate(
ng-class="{ 'rp-entry-focused': entry.focused }"
)
.rp-entry-body
.rp-entry-action-icon
i.fa.fa-pencil
.rp-entry-details
.rp-entry-description
| #{translate("aggregate_changed")}
del.rp-content-highlight {{ entry.metadata.replaced_content }}
| #{translate("aggregate_to")}
ins.rp-content-highlight {{ entry.content }}
a.rp-collapse-toggle(
href
ng-if="needsCollapsing"
ng-click="toggleCollapse();"
) {{ isCollapsed ? '... (#{translate("show_all")})' : ' (#{translate("show_less")})' }}
.rp-entry-metadata
| {{ entry.metadata.ts | date : 'MMM d, y h:mm a' }} • 
span.rp-entry-user(style="color: hsl({{ user.hue }}, 70%, 40%);") {{ user.name }}
.rp-entry-actions(ng-if="permissions.write")
a.rp-entry-button(href, ng-click="onReject();")
i.fa.fa-times
|  #{translate("reject")}
a.rp-entry-button(href, ng-click="onAccept();")
i.fa.fa-check
|  #{translate("accept")}
script(type='text/ng-template', id='commentEntryTemplate')
.rp-comment-wrapper(
ng-class="{ 'rp-comment-wrapper-resolving': state.animating }"
+10 -2
View File
@@ -98,6 +98,8 @@ script(type='text/ng-template', id='renameProjectModalTemplate')
) ×
h3 #{translate("rename_project")}
.modal-body
.alert.alert-danger(ng-show="state.error.message") {{ state.error.message}}
.alert.alert-danger(ng-show="state.error && !state.error.message") #{translate("generic_something_went_wrong")}
form(name="renameProjectForm", novalidate)
input.form-control(
type="text",
@@ -111,8 +113,10 @@ script(type='text/ng-template', id='renameProjectModalTemplate')
button.btn.btn-default(ng-click="cancel()") #{translate("cancel")}
button.btn.btn-primary(
ng-click="rename()",
ng-disabled="renameProjectForm.$invalid"
) #{translate("rename")}
ng-disabled="renameProjectForm.$invalid || state.inflight"
)
span(ng-show="!state.inflight") #{translate("rename")}
span(ng-show="state.inflight") #{translate("renaming")}...
script(type='text/ng-template', id='cloneProjectModalTemplate')
.modal-header
@@ -123,6 +127,8 @@ script(type='text/ng-template', id='cloneProjectModalTemplate')
) ×
h3 #{translate("copy_project")}
.modal-body
.alert.alert-danger(ng-show="state.error.message") {{ state.error.message}}
.alert.alert-danger(ng-show="state.error && !state.error.message") #{translate("generic_something_went_wrong")}
form(name="cloneProjectForm", novalidate)
.form-group
label #{translate("new_name")}
@@ -155,6 +161,8 @@ script(type='text/ng-template', id='newProjectModalTemplate')
) ×
h3 #{translate("new_project")}
.modal-body
.alert.alert-danger(ng-show="state.error.message") {{ state.error.message}}
.alert.alert-danger(ng-show="state.error && !state.error.message") #{translate("generic_something_went_wrong")}
form(novalidate, name="newProjectForm")
input.form-control(
type="text",
+2
View File
@@ -46,6 +46,7 @@ block content
type='text',
name='first_name',
value=user.first_name
ng-non-bindable
)
.form-group
label(for='lastName').control-label #{translate("last_name")}
@@ -53,6 +54,7 @@ block content
type='text',
name='last_name',
value=user.last_name
ng-non-bindable
)
.actions
button.btn.btn-primary(
@@ -6,6 +6,7 @@ define [
projectName: ide.$scope.project.name + " (Copy)"
$scope.state =
inflight: false
error: false
$modalInstance.opened.then () ->
$timeout () ->
@@ -20,9 +21,16 @@ define [
$scope.clone = () ->
$scope.state.inflight = true
$scope.state.error = false
cloneProject($scope.inputs.projectName)
.then (data) ->
window.location = "/project/#{data.data.project_id}"
.success (data) ->
window.location = "/project/#{data.project_id}"
.error (body, statusCode) ->
$scope.state.inflight = false
if statusCode == 400
$scope.state.error = { message: body }
else
$scope.state.error = true
$scope.cancel = () ->
$modalInstance.dismiss('cancel')
@@ -26,16 +26,10 @@ define [
@$scope.$on "comment:select_line", (e) =>
@selectLineIfNoSelection()
@$scope.$on "change:accept", (e, change_id) =>
@acceptChangeIds([ change_id ])
@$scope.$on "change:reject", (e, change_id) =>
@rejectChangeIds([ change_id ])
@$scope.$on "change:bulk-accept", (e, change_ids) =>
@$scope.$on "changes:accept", (e, change_ids) =>
@acceptChangeIds(change_ids)
@$scope.$on "change:bulk-reject", (e, change_ids) =>
@$scope.$on "changes:reject", (e, change_ids) =>
@rejectChangeIds(change_ids)
@$scope.$on "comment:remove", (e, comment_id) =>
@@ -216,10 +210,66 @@ define [
acceptChangeIds: (change_ids) ->
@rangesTracker.removeChangeIds(change_ids)
@updateAnnotations()
@updateFocus()
rejectChangeIds: (change_ids) ->
changes = @rangesTracker.getChanges(change_ids)
return if changes.length == 0
# When doing bulk rejections, adjacent changes might interact with each other.
# Consider an insertion with an adjacent deletion (which is a common use-case, replacing words):
#
# "foo bar baz" -> "foo quux baz"
#
# The change above will be modeled with two ops, with the insertion going first:
#
# foo quux baz
# |--| -> insertion of "quux", op 1, at position 4
# | -> deletion of "bar", op 2, pushed forward by "quux" to position 8
#
# When rejecting these changes at once, if the insertion is rejected first, we get unexpected
# results. What happens is:
#
# 1) Rejecting the insertion deletes the added word "quux", i.e., it removes 4 chars
# starting from position 4;
#
# "foo quux baz" -> "foo baz"
# |--| -> 4 characters to be removed
#
# 2) Rejecting the deletion adds the deleted word "bar" at position 8 (i.e. it will act as if
# the word "quuux" was still present).
#
# "foo baz" -> "foo bazbar"
# | -> deletion of "bar" is reverted by reinserting "bar" at position 8
#
# While the intended result would be "foo bar baz", what we get is:
#
# "foo bazbar" (note "bar" readded at position 8)
#
# The issue happens because of step 1. To revert the insertion of "quux", 4 characters are deleted
# from position 4. This includes the position where the deletion exists; when that position is
# cleared, the RangesTracker considers that the deletion is gone and stops tracking/updating it.
# As we still hold a reference to it, the code tries to revert it by readding the deleted text, but
# does so at the outdated position (position 8, which was valid when "quux" was present).
#
# To avoid this kind of problem, we need to make sure that reverting operations doesn't affect
# subsequent operations that come after. Reverse sorting the operations based on position will
# achieve it; in the case above, it makes sure that the the deletion is reverted first:
#
# 1) Rejecting the deletion adds the deleted word "bar" at position 8
#
# "foo quux baz" -> "foo quuxbar baz"
# | -> deletion of "bar" is reverted by
# reinserting "bar" at position 8
#
# 2) Rejecting the insertion deletes the added word "quux", i.e., it removes 4 chars
# starting from position 4 and achieves the expected result:
#
# "foo quuxbar baz" -> "foo bar baz"
# |--| -> 4 characters to be removed
changes.sort((a, b) -> b.op.p - a.op.p)
session = @editor.getSession()
for change in changes
if change.op.d?
@@ -239,6 +289,7 @@ define [
session.$fromReject = false
else
throw new Error("unknown change: #{JSON.stringify(change)}")
setTimeout () => @updateFocus()
removeCommentId: (comment_id) ->
@rangesTracker.removeCommentId(comment_id)
@@ -6,22 +6,6 @@ define [
App.factory 'PDFRenderer', ['$q', '$timeout', 'pdfAnnotations', 'pdfTextLayer', 'pdfSpinner', ($q, $timeout, pdfAnnotations, pdfTextLayer, pdfSpinner) ->
# Have a single worker used by all rendering, to avoid reloading
RenderThread = { worker: null, count: 0}
getRenderThread = () ->
if RenderThread.count > 16 # recycle the worker periodically to avoid leaks
RenderThread.readyToDestroy = true
RenderThread = { worker: null, count: 0 }
RenderThread.worker ||= new PDFJS.PDFWorker('pdfjsworker')
RenderThread.count++
return RenderThread
resetWorker = (thread) ->
thread.worker.destroy() if thread.readyToDestroy
# The PDF page renderer
class PDFRenderer
JOB_QUEUE_INTERVAL: 25
PAGE_LOAD_TIMEOUT: 60*1000
@@ -43,8 +27,7 @@ define [
# PDFJS.disableStream
# PDFJS.disableRange
@scale = @options.scale || 1
@thread = getRenderThread()
@pdfjs = PDFJS.getDocument {url: @url, rangeChunkSize: 2*65536, worker: @thread.worker}
@pdfjs = PDFJS.getDocument {url: @url, rangeChunkSize: 2*65536}
@pdfjs.onProgress = @options.progressCallback
@document = $q.when(@pdfjs)
@navigateFn = @options.navigateFn
@@ -353,9 +336,8 @@ define [
destroy: () ->
@shuttingDown = true
@resetState()
@pdfjs.then (document) =>
@pdfjs.then (document) ->
document.cleanup()
document.destroy()
resetWorker(@thread)
]
@@ -5,6 +5,7 @@ define [
"ide/review-panel/directives/reviewPanelSorted"
"ide/review-panel/directives/reviewPanelToggle"
"ide/review-panel/directives/changeEntry"
"ide/review-panel/directives/aggregateChangeEntry"
"ide/review-panel/directives/commentEntry"
"ide/review-panel/directives/addCommentEntry"
"ide/review-panel/directives/bulkActionsEntry"
@@ -26,7 +26,12 @@ define [
resolvedThreadIds: {}
rendererData: {}
loadingThreads: false
selectedEntryIds: []
# All selected changes. If a aggregated change (insertion + deletion) is selection, the two ids
# will be present. The length of this array will differ from the count below (see explanation).
selectedEntryIds: []
# A count of user-facing selected changes. An aggregated change (insertion + deletion) will count
# as only one.
nVisibleSelectedChanges: 0
window.addEventListener "beforeunload", () ->
collapsedStates = {}
@@ -69,20 +74,12 @@ define [
$scope.$apply()
$timeout () ->
$scope.$broadcast "review-panel:layout"
ide.socket.on "accept-change", (doc_id, change_id) ->
if doc_id != $scope.editor.open_doc_id
getChangeTracker(doc_id).removeChangeId(change_id)
else
$scope.$broadcast "change:accept", change_id
updateEntries(doc_id)
$scope.$apply () ->
ide.socket.on "accept-changes", (doc_id, change_ids) ->
if doc_id != $scope.editor.open_doc_id
getChangeTracker(doc_id).removeChangeIds(change_ids)
else
$scope.$broadcast "change:bulk-accept", change_ids
$scope.$broadcast "changes:accept", change_ids
updateEntries(doc_id)
$scope.$apply () ->
@@ -227,28 +224,49 @@ define [
# Assume we'll delete everything until we see it, then we'll remove it from this object
delete_changes = {}
for change_id, change of entries
if change_id != "add-comment"
delete_changes[change_id] = true
for change_id, change of resolvedComments
delete_changes[change_id] = true
for id, change of entries
if id not in [ "add-comment", "bulk-actions" ]
for entry_id in change.entry_ids
delete_changes[entry_id] = true
for id, change of resolvedComments
for entry_id in change.entry_ids
delete_changes[entry_id] = true
potential_aggregate = false
prev_insertion = null
for change in rangesTracker.changes
changed = true
delete delete_changes[change.id]
entries[change.id] ?= {}
# Update in place to avoid a full DOM redraw via angular
metadata = {}
metadata[key] = value for key, value of change.metadata
new_entry = {
type: if change.op.i then "insert" else "delete"
content: change.op.i or change.op.d
offset: change.op.p
metadata: change.metadata
}
for key, value of new_entry
entries[change.id][key] = value
if (
potential_aggregate and
change.op.d and
change.op.p == prev_insertion.op.p + prev_insertion.op.i.length and
change.metadata.user_id == prev_insertion.metadata.user_id
)
# An actual aggregate op.
entries[prev_insertion.id].type = "aggregate-change"
entries[prev_insertion.id].metadata.replaced_content = change.op.d
entries[prev_insertion.id].entry_ids.push change.id
else
entries[change.id] ?= {}
delete delete_changes[change.id]
new_entry = {
type: if change.op.i then "insert" else "delete"
entry_ids: [ change.id ]
content: change.op.i or change.op.d
offset: change.op.p
metadata: change.metadata
}
for key, value of new_entry
entries[change.id][key] = value
if change.op.i
potential_aggregate = true
prev_insertion = change
else
potential_aggregate = false
prev_insertion = null
if !$scope.users[change.metadata.user_id]?
refreshChangeUsers(change.metadata.user_id)
@@ -268,6 +286,7 @@ define [
new_entry = {
type: "comment"
thread_id: comment.op.t
entry_ids: [ comment.id ]
content: comment.op.c
offset: comment.op.p
}
@@ -295,8 +314,10 @@ define [
$scope.$on "editor:focus:changed", (e, selection_offset_start, selection_offset_end, selection) ->
doc_id = $scope.editor.open_doc_id
entries = getDocEntries(doc_id)
# All selected changes will be added to this array.
$scope.reviewPanel.selectedEntryIds = []
# Count of user-visible changes, i.e. an aggregated change will count as one.
$scope.reviewPanel.nVisibleSelectedChanges = 0
delete entries["add-comment"]
delete entries["bulk-actions"]
@@ -313,53 +334,63 @@ define [
}
for id, entry of entries
isChangeEntryAndWithinSelection = false
if entry.type == "comment" and not $scope.reviewPanel.resolvedThreadIds[entry.thread_id]
entry.focused = (entry.offset <= selection_offset_start <= entry.offset + entry.content.length)
else if entry.type == "insert"
isEntryWithinSelection = entry.offset >= selection_offset_start and entry.offset + entry.content.length <= selection_offset_end
isChangeEntryAndWithinSelection = entry.offset >= selection_offset_start and entry.offset + entry.content.length <= selection_offset_end
entry.focused = (entry.offset <= selection_offset_start <= entry.offset + entry.content.length)
$scope.reviewPanel.selectedEntryIds.push id if isEntryWithinSelection
else if entry.type == "delete"
isEntryWithinSelection = selection_offset_start <= entry.offset <= selection_offset_end
isChangeEntryAndWithinSelection = selection_offset_start <= entry.offset <= selection_offset_end
entry.focused = (entry.offset == selection_offset_start)
$scope.reviewPanel.selectedEntryIds.push id if isEntryWithinSelection
else if entry.type == "aggregate-change"
isChangeEntryAndWithinSelection = entry.offset >= selection_offset_start and entry.offset + entry.content.length <= selection_offset_end
entry.focused = (entry.offset <= selection_offset_start <= entry.offset + entry.content.length)
else if entry.type in [ "add-comment", "bulk-actions" ] and selection
entry.focused = true
if isChangeEntryAndWithinSelection
for entry_id in entry.entry_ids
$scope.reviewPanel.selectedEntryIds.push entry_id
$scope.reviewPanel.nVisibleSelectedChanges++
$scope.$broadcast "review-panel:recalculate-screen-positions"
$scope.$broadcast "review-panel:layout"
$scope.acceptChange = (entry_id) ->
$http.post "/project/#{$scope.project_id}/doc/#{$scope.editor.open_doc_id}/changes/#{entry_id}/accept", {_csrf: window.csrfToken}
$scope.$broadcast "change:accept", entry_id
event_tracking.sendMB "rp-change-accepted", { view: if $scope.ui.reviewPanelOpen then $scope.reviewPanel.subView else 'mini' }
$scope.rejectChange = (entry_id) ->
$scope.$broadcast "change:reject", entry_id
event_tracking.sendMB "rp-change-rejected", { view: if $scope.ui.reviewPanelOpen then $scope.reviewPanel.subView else 'mini' }
$scope.acceptChanges = (change_ids) ->
_doAcceptChanges change_ids
event_tracking.sendMB "rp-changes-accepted", { view: if $scope.ui.reviewPanelOpen then $scope.reviewPanel.subView else 'mini' }
$scope.rejectChanges = (change_ids) ->
_doRejectChanges change_ids
event_tracking.sendMB "rp-changes-rejected", { view: if $scope.ui.reviewPanelOpen then $scope.reviewPanel.subView else 'mini' }
_doAcceptChanges = (change_ids) ->
$http.post "/project/#{$scope.project_id}/doc/#{$scope.editor.open_doc_id}/changes/accept", { change_ids, _csrf: window.csrfToken}
$scope.$broadcast "changes:accept", change_ids
_doRejectChanges = (change_ids) ->
$scope.$broadcast "changes:reject", change_ids
bulkAccept = () ->
entry_ids = $scope.reviewPanel.selectedEntryIds.slice()
$http.post "/project/#{$scope.project_id}/doc/#{$scope.editor.open_doc_id}/changes/accept", { change_ids: entry_ids, _csrf: window.csrfToken}
$scope.$broadcast "change:bulk-accept", entry_ids
$scope.reviewPanel.selectedEntryIds = []
_doAcceptChanges $scope.reviewPanel.selectedEntryIds.slice()
event_tracking.sendMB "rp-bulk-accept", {
view: if $scope.ui.reviewPanelOpen then $scope.reviewPanel.subView else 'mini',
nEntries: $scope.reviewPanel.selectedEntryIds.length
nEntries: $scope.reviewPanel.nVisibleSelectedChanges
}
bulkReject = () ->
$scope.$broadcast "change:bulk-reject", $scope.reviewPanel.selectedEntryIds.slice()
$scope.reviewPanel.selectedEntryIds = []
_doRejectChanges $scope.reviewPanel.selectedEntryIds.slice()
event_tracking.sendMB "rp-bulk-reject", {
view: if $scope.ui.reviewPanelOpen then $scope.reviewPanel.subView else 'mini',
nEntries: $scope.reviewPanel.selectedEntryIds.length
nEntries: $scope.reviewPanel.nVisibleSelectedChanges
}
$scope.showBulkAcceptDialog = () ->
showBulkActionsDialog true
$scope.showBulkRejectDialog = () -> showBulkActionsDialog false
$scope.showBulkRejectDialog = () ->
showBulkActionsDialog false
showBulkActionsDialog = (isAccept) ->
$modal.open({
@@ -367,7 +398,7 @@ define [
controller: "BulkActionsModalController"
resolve:
isAccept: () -> isAccept
nChanges: () -> $scope.reviewPanel.selectedEntryIds.length
nChanges: () -> $scope.reviewPanel.nVisibleSelectedChanges
scope: $scope.$new()
}).result.then (isAccept) ->
if isAccept
@@ -0,0 +1,30 @@
define [
"base"
], (App) ->
App.directive "aggregateChangeEntry", ($timeout) ->
restrict: "E"
templateUrl: "aggregateChangeEntryTemplate"
scope:
entry: "="
user: "="
permissions: "="
onAccept: "&"
onReject: "&"
onIndicatorClick: "&"
onBodyClick: "&"
link: (scope, element, attrs) ->
scope.contentLimit = 35
scope.isCollapsed = true
scope.needsCollapsing = false
element.on "click", (e) ->
if $(e.target).is('.rp-entry, .rp-entry-description, .rp-entry-body, .rp-entry-action-icon i')
scope.onBodyClick()
scope.toggleCollapse = () ->
scope.isCollapsed = !scope.isCollapsed
$timeout () ->
scope.$emit "review-panel:layout"
scope.$watch "entry.content.length + entry.metadata.agg_op.content.length", (contentLength) ->
scope.needsCollapsing = contentLength > scope.contentLimit
@@ -63,7 +63,12 @@ define [
# Put the focused entry as close to where it wants to be as possible
focused_entry_top = Math.max(focused_entry.scope.entry.screenPos.y, TOOLBAR_HEIGHT)
focused_entry.$box_el.css(top: focused_entry_top)
focused_entry.$box_el.css(
top: focused_entry_top
# The entry element is invisible by default, to avoid flickering when positioning for
# the first time. Here we make sure it becomes visible after having a "top" value.
visibility: "visible"
)
focused_entry.$indicator_el.css(top: focused_entry_top)
positionLayoutEl(focused_entry.$callout_el, focused_entry.scope.entry.screenPos.y, focused_entry_top)
@@ -73,7 +78,12 @@ define [
height = entry.height
top = Math.max(original_top, previousBottom + PADDING)
previousBottom = top + height
entry.$box_el.css(top: top)
entry.$box_el.css(
top: top
# The entry element is invisible by default, to avoid flickering when positioning for
# the first time. Here we make sure it becomes visible after having a "top" value.
visibility: "visible"
)
entry.$indicator_el.css(top: top)
positionLayoutEl(entry.$callout_el, original_top, top)
sl_console.log "ENTRY", {entry: entry.scope.entry, top}
@@ -89,7 +99,12 @@ define [
bottom = Math.min(original_bottom, previousTop - PADDING)
top = bottom - height
previousTop = top
entry.$box_el.css(top: top)
entry.$box_el.css(
top: top
# The entry element is invisible by default, to avoid flickering when positioning for
# the first time. Here we make sure it becomes visible after having a "top" value.
visibility: "visible"
)
entry.$indicator_el.css(top: top)
positionLayoutEl(entry.$callout_el, original_top, top)
sl_console.log "ENTRY", {entry: entry.scope.entry, top}
@@ -19,12 +19,18 @@ define [
$scope.finishRenaming = () ->
$scope.state.renaming = false
newName = $scope.inputs.name
if !newName? or newName.length == 0 or newName.length > MAX_PROJECT_NAME_LENGTH
return
if $scope.project.name == newName
return
oldName = $scope.project.name
$scope.project.name = newName
settings.saveProjectSettings({name: $scope.project.name})
.error (response, statusCode) ->
$scope.project.name = oldName
if statusCode == 400
ide.showGenericMessageModal("Error renaming project", response)
else
ide.showGenericMessageModal("Error renaming project", "Please try again in a moment")
console.log arguments
ide.socket.on "projectNameUpdated", (name) ->
$scope.$apply () ->
@@ -12,8 +12,7 @@ define [
# End of tracking code.
data._csrf = window.csrfToken
ide.$http.post "/user/settings", data
return ide.$http.post "/user/settings", data
saveProjectSettings: (data) ->
# Tracking code.
@@ -24,9 +23,8 @@ define [
# End of tracking code.
data._csrf = window.csrfToken
ide.$http.post "/project/#{ide.project_id}/settings", data
return ide.$http.post "/project/#{ide.project_id}/settings", data
saveProjectAdminSettings: (data) ->
# Tracking code.
for key in Object.keys(data)
@@ -36,8 +34,6 @@ define [
# End of tracking code.
data._csrf = window.csrfToken
ide.$http.post "/project/#{ide.project_id}/settings/admin", data
return ide.$http.post "/project/#{ide.project_id}/settings/admin", data
}
]
@@ -33,9 +33,13 @@ define [
$scope.removeMembers = () ->
for user in $scope.selectedUsers
do (user) ->
if user.holdingAccount and !user._id?
url = "/subscription/group/email/#{encodeURIComponent(user.email)}"
else
url = "/subscription/group/user/#{user._id}"
queuedHttp({
method: "DELETE",
url: "/subscription/group/user/#{user._id}"
url: url
headers:
"X-Csrf-Token": window.csrfToken
})
@@ -1,9 +1,13 @@
define [
"base"
], (App) ->
App.controller 'RenameProjectModalController', ($scope, $modalInstance, $timeout, projectName) ->
App.controller 'RenameProjectModalController', ($scope, $modalInstance, $timeout, project, queuedHttp) ->
$scope.inputs =
projectName: projectName
projectName: project.name
$scope.state =
inflight: false
error: false
$modalInstance.opened.then () ->
$timeout () ->
@@ -11,7 +15,20 @@ define [
, 200
$scope.rename = () ->
$modalInstance.close($scope.inputs.projectName)
$scope.state.inflight = true
$scope.state.error = false
$scope
.renameProject(project, $scope.inputs.projectName)
.success () ->
$scope.state.inflight = false
$scope.state.error = false
$modalInstance.close()
.error (body, statusCode) ->
$scope.state.inflight = false
if statusCode == 400
$scope.state.error = { message: body }
else
$scope.state.error = true
$scope.cancel = () ->
$modalInstance.dismiss('cancel')
@@ -21,6 +38,7 @@ define [
projectName: project.name + " (Copy)"
$scope.state =
inflight: false
error: false
$modalInstance.opened.then () ->
$timeout () ->
@@ -31,9 +49,16 @@ define [
$scope.state.inflight = true
$scope
.cloneProject(project, $scope.inputs.projectName)
.then (project_id) ->
.success () ->
$scope.state.inflight = false
$modalInstance.close(project_id)
$scope.state.error = false
$modalInstance.close()
.error (body, statusCode) ->
$scope.state.inflight = false
if statusCode == 400
$scope.state.error = { message: body }
else
$scope.state.error = true
$scope.cancel = () ->
$modalInstance.dismiss('cancel')
@@ -43,6 +68,7 @@ define [
projectName: ""
$scope.state =
inflight: false
error: false
$modalInstance.opened.then () ->
$timeout () ->
@@ -51,11 +77,19 @@ define [
$scope.create = () ->
$scope.state.inflight = true
$scope.state.error = false
$scope
.createProject($scope.inputs.projectName, template)
.then (project_id) ->
.success (data) ->
$scope.state.inflight = false
$modalInstance.close(project_id)
$scope.state.error = false
$modalInstance.close(data.project_id)
.error (body, statusCode) ->
$scope.state.inflight = false
if statusCode == 400
$scope.state.error = { message: body }
else
$scope.state.error = true
$scope.cancel = () ->
$modalInstance.dismiss('cancel')
@@ -256,9 +256,7 @@ define [
)
$scope.createProject = (name, template = "none") ->
deferred = $q.defer()
queuedHttp
return queuedHttp
.post("/project/new", {
_csrf: window.csrfToken
projectName: name
@@ -273,13 +271,7 @@ define [
# to the rest of the app
}
$scope.updateVisibleProjects()
deferred.resolve(data.project_id)
)
.error((data, status, headers, config) ->
deferred.reject()
)
return deferred.promise
$scope.openCreateProjectModal = (template = "none") ->
event_tracking.send 'project-list-page-interaction', 'new-project', template
@@ -294,15 +286,13 @@ define [
modalInstance.result.then (project_id) ->
window.location = "/project/#{project_id}"
MAX_PROJECT_NAME_LENGTH = 150
$scope.renameProject = (project, newName) ->
if !newName? or newName.length == 0 or newName.length > MAX_PROJECT_NAME_LENGTH
return
project.name = newName
queuedHttp.post "/project/#{project.id}/rename", {
newProjectName: project.name
return queuedHttp.post "/project/#{project.id}/rename", {
newProjectName: newName,
_csrf: window.csrfToken
}
.success () ->
project.name = newName
$scope.openRenameProjectModal = () ->
project = $scope.getFirstSelectedProject()
@@ -312,16 +302,11 @@ define [
templateUrl: "renameProjectModalTemplate"
controller: "RenameProjectModalController"
resolve:
projectName: () -> project.name
)
modalInstance.result.then(
(newName) ->
$scope.renameProject(project, newName)
project: () -> project
scope: $scope
)
$scope.cloneProject = (project, cloneName) ->
deferred = $q.defer()
event_tracking.send 'project-list-page-interaction', 'project action', 'Clone'
queuedHttp
.post("/project/#{project.id}/clone", {
@@ -337,13 +322,7 @@ define [
# to the rest of the app
}
$scope.updateVisibleProjects()
deferred.resolve(data.project_id)
)
.error((data, status, headers, config) ->
deferred.reject()
)
return deferred.promise
$scope.openCloneProjectModal = () ->
project = $scope.getFirstSelectedProject()
@@ -19,24 +19,29 @@ define [
processPendingRequests()
queuedHttp = (args...) ->
deferred = $q.defer()
promise = deferred.promise
# We can't use Angular's $q.defer promises, because it only passes
# a single argument on error, and $http passes multiple.
promise = {}
successCallbacks = []
errorCallbacks = []
# Adhere to the $http promise conventions
promise.success = (callback) ->
promise.then(callback)
successCallbacks.push callback
return promise
promise.error = (callback) ->
promise.catch(callback)
errorCallbacks.push callback
return promise
doRequest = () ->
$http(args...)
.success (successArgs...) ->
deferred.resolve(successArgs...)
.error (errorArgs...) ->
deferred.reject(errorArgs...)
.success (args...) ->
for cb in successCallbacks
cb(args...)
.error (args...) ->
for cb in errorCallbacks
cb(args...)
pendingRequests.push doRequest
processPendingRequests()
@@ -1 +0,0 @@
* binary
@@ -1,596 +0,0 @@
/* Copyright 2012 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* globals VBArray, PDFJS */
(function compatibilityWrapper() {
'use strict';
// Initializing PDFJS global object here, it case if we need to change/disable
// some PDF.js features, e.g. range requests
if (typeof PDFJS === 'undefined') {
(typeof window !== 'undefined' ? window : this).PDFJS = {};
}
// Checking if the typed arrays are supported
// Support: iOS<6.0 (subarray), IE<10, Android<4.0
(function checkTypedArrayCompatibility() {
if (typeof Uint8Array !== 'undefined') {
// Support: iOS<6.0
if (typeof Uint8Array.prototype.subarray === 'undefined') {
Uint8Array.prototype.subarray = function subarray(start, end) {
return new Uint8Array(this.slice(start, end));
};
Float32Array.prototype.subarray = function subarray(start, end) {
return new Float32Array(this.slice(start, end));
};
}
// Support: Android<4.1
if (typeof Float64Array === 'undefined') {
window.Float64Array = Float32Array;
}
return;
}
function subarray(start, end) {
return new TypedArray(this.slice(start, end));
}
function setArrayOffset(array, offset) {
if (arguments.length < 2) {
offset = 0;
}
for (var i = 0, n = array.length; i < n; ++i, ++offset) {
this[offset] = array[i] & 0xFF;
}
}
function TypedArray(arg1) {
var result, i, n;
if (typeof arg1 === 'number') {
result = [];
for (i = 0; i < arg1; ++i) {
result[i] = 0;
}
} else if ('slice' in arg1) {
result = arg1.slice(0);
} else {
result = [];
for (i = 0, n = arg1.length; i < n; ++i) {
result[i] = arg1[i];
}
}
result.subarray = subarray;
result.buffer = result;
result.byteLength = result.length;
result.set = setArrayOffset;
if (typeof arg1 === 'object' && arg1.buffer) {
result.buffer = arg1.buffer;
}
return result;
}
window.Uint8Array = TypedArray;
window.Int8Array = TypedArray;
// we don't need support for set, byteLength for 32-bit array
// so we can use the TypedArray as well
window.Uint32Array = TypedArray;
window.Int32Array = TypedArray;
window.Uint16Array = TypedArray;
window.Float32Array = TypedArray;
window.Float64Array = TypedArray;
})();
// URL = URL || webkitURL
// Support: Safari<7, Android 4.2+
(function normalizeURLObject() {
if (!window.URL) {
window.URL = window.webkitURL;
}
})();
// Object.defineProperty()?
// Support: Android<4.0, Safari<5.1
(function checkObjectDefinePropertyCompatibility() {
if (typeof Object.defineProperty !== 'undefined') {
var definePropertyPossible = true;
try {
// some browsers (e.g. safari) cannot use defineProperty() on DOM objects
// and thus the native version is not sufficient
Object.defineProperty(new Image(), 'id', { value: 'test' });
// ... another test for android gb browser for non-DOM objects
var Test = function Test() {};
Test.prototype = { get id() { } };
Object.defineProperty(new Test(), 'id',
{ value: '', configurable: true, enumerable: true, writable: false });
} catch (e) {
definePropertyPossible = false;
}
if (definePropertyPossible) {
return;
}
}
Object.defineProperty = function objectDefineProperty(obj, name, def) {
delete obj[name];
if ('get' in def) {
obj.__defineGetter__(name, def['get']);
}
if ('set' in def) {
obj.__defineSetter__(name, def['set']);
}
if ('value' in def) {
obj.__defineSetter__(name, function objectDefinePropertySetter(value) {
this.__defineGetter__(name, function objectDefinePropertyGetter() {
return value;
});
return value;
});
obj[name] = def.value;
}
};
})();
// No XMLHttpRequest#response?
// Support: IE<11, Android <4.0
(function checkXMLHttpRequestResponseCompatibility() {
var xhrPrototype = XMLHttpRequest.prototype;
var xhr = new XMLHttpRequest();
if (!('overrideMimeType' in xhr)) {
// IE10 might have response, but not overrideMimeType
// Support: IE10
Object.defineProperty(xhrPrototype, 'overrideMimeType', {
value: function xmlHttpRequestOverrideMimeType(mimeType) {}
});
}
if ('responseType' in xhr) {
return;
}
// The worker will be using XHR, so we can save time and disable worker.
PDFJS.disableWorker = true;
Object.defineProperty(xhrPrototype, 'responseType', {
get: function xmlHttpRequestGetResponseType() {
return this._responseType || 'text';
},
set: function xmlHttpRequestSetResponseType(value) {
if (value === 'text' || value === 'arraybuffer') {
this._responseType = value;
if (value === 'arraybuffer' &&
typeof this.overrideMimeType === 'function') {
this.overrideMimeType('text/plain; charset=x-user-defined');
}
}
}
});
// Support: IE9
if (typeof VBArray !== 'undefined') {
Object.defineProperty(xhrPrototype, 'response', {
get: function xmlHttpRequestResponseGet() {
if (this.responseType === 'arraybuffer') {
return new Uint8Array(new VBArray(this.responseBody).toArray());
} else {
return this.responseText;
}
}
});
return;
}
Object.defineProperty(xhrPrototype, 'response', {
get: function xmlHttpRequestResponseGet() {
if (this.responseType !== 'arraybuffer') {
return this.responseText;
}
var text = this.responseText;
var i, n = text.length;
var result = new Uint8Array(n);
for (i = 0; i < n; ++i) {
result[i] = text.charCodeAt(i) & 0xFF;
}
return result.buffer;
}
});
})();
// window.btoa (base64 encode function) ?
// Support: IE<10
(function checkWindowBtoaCompatibility() {
if ('btoa' in window) {
return;
}
var digits =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
window.btoa = function windowBtoa(chars) {
var buffer = '';
var i, n;
for (i = 0, n = chars.length; i < n; i += 3) {
var b1 = chars.charCodeAt(i) & 0xFF;
var b2 = chars.charCodeAt(i + 1) & 0xFF;
var b3 = chars.charCodeAt(i + 2) & 0xFF;
var d1 = b1 >> 2, d2 = ((b1 & 3) << 4) | (b2 >> 4);
var d3 = i + 1 < n ? ((b2 & 0xF) << 2) | (b3 >> 6) : 64;
var d4 = i + 2 < n ? (b3 & 0x3F) : 64;
buffer += (digits.charAt(d1) + digits.charAt(d2) +
digits.charAt(d3) + digits.charAt(d4));
}
return buffer;
};
})();
// window.atob (base64 encode function)?
// Support: IE<10
(function checkWindowAtobCompatibility() {
if ('atob' in window) {
return;
}
// https://github.com/davidchambers/Base64.js
var digits =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
window.atob = function (input) {
input = input.replace(/=+$/, '');
if (input.length % 4 === 1) {
throw new Error('bad atob input');
}
for (
// initialize result and counters
var bc = 0, bs, buffer, idx = 0, output = '';
// get next character
buffer = input.charAt(idx++);
// character found in table?
// initialize bit storage and add its ascii value
~buffer && (bs = bc % 4 ? bs * 64 + buffer : buffer,
// and if not first of each 4 characters,
// convert the first 8 bits to one ascii character
bc++ % 4) ? output += String.fromCharCode(255 & bs >> (-2 * bc & 6)) : 0
) {
// try to find character in table (0-63, not found => -1)
buffer = digits.indexOf(buffer);
}
return output;
};
})();
// Function.prototype.bind?
// Support: Android<4.0, iOS<6.0
(function checkFunctionPrototypeBindCompatibility() {
if (typeof Function.prototype.bind !== 'undefined') {
return;
}
Function.prototype.bind = function functionPrototypeBind(obj) {
var fn = this, headArgs = Array.prototype.slice.call(arguments, 1);
var bound = function functionPrototypeBindBound() {
var args = headArgs.concat(Array.prototype.slice.call(arguments));
return fn.apply(obj, args);
};
return bound;
};
})();
// HTMLElement dataset property
// Support: IE<11, Safari<5.1, Android<4.0
(function checkDatasetProperty() {
var div = document.createElement('div');
if ('dataset' in div) {
return; // dataset property exists
}
Object.defineProperty(HTMLElement.prototype, 'dataset', {
get: function() {
if (this._dataset) {
return this._dataset;
}
var dataset = {};
for (var j = 0, jj = this.attributes.length; j < jj; j++) {
var attribute = this.attributes[j];
if (attribute.name.substring(0, 5) !== 'data-') {
continue;
}
var key = attribute.name.substring(5).replace(/\-([a-z])/g,
function(all, ch) {
return ch.toUpperCase();
});
dataset[key] = attribute.value;
}
Object.defineProperty(this, '_dataset', {
value: dataset,
writable: false,
enumerable: false
});
return dataset;
},
enumerable: true
});
})();
// HTMLElement classList property
// Support: IE<10, Android<4.0, iOS<5.0
(function checkClassListProperty() {
var div = document.createElement('div');
if ('classList' in div) {
return; // classList property exists
}
function changeList(element, itemName, add, remove) {
var s = element.className || '';
var list = s.split(/\s+/g);
if (list[0] === '') {
list.shift();
}
var index = list.indexOf(itemName);
if (index < 0 && add) {
list.push(itemName);
}
if (index >= 0 && remove) {
list.splice(index, 1);
}
element.className = list.join(' ');
return (index >= 0);
}
var classListPrototype = {
add: function(name) {
changeList(this.element, name, true, false);
},
contains: function(name) {
return changeList(this.element, name, false, false);
},
remove: function(name) {
changeList(this.element, name, false, true);
},
toggle: function(name) {
changeList(this.element, name, true, true);
}
};
Object.defineProperty(HTMLElement.prototype, 'classList', {
get: function() {
if (this._classList) {
return this._classList;
}
var classList = Object.create(classListPrototype, {
element: {
value: this,
writable: false,
enumerable: true
}
});
Object.defineProperty(this, '_classList', {
value: classList,
writable: false,
enumerable: false
});
return classList;
},
enumerable: true
});
})();
// Check console compatibility
// In older IE versions the console object is not available
// unless console is open.
// Support: IE<10
(function checkConsoleCompatibility() {
if (!('console' in window)) {
window.console = {
log: function() {},
error: function() {},
warn: function() {}
};
} else if (!('bind' in console.log)) {
// native functions in IE9 might not have bind
console.log = (function(fn) {
return function(msg) { return fn(msg); };
})(console.log);
console.error = (function(fn) {
return function(msg) { return fn(msg); };
})(console.error);
console.warn = (function(fn) {
return function(msg) { return fn(msg); };
})(console.warn);
}
})();
// Check onclick compatibility in Opera
// Support: Opera<15
(function checkOnClickCompatibility() {
// workaround for reported Opera bug DSK-354448:
// onclick fires on disabled buttons with opaque content
function ignoreIfTargetDisabled(event) {
if (isDisabled(event.target)) {
event.stopPropagation();
}
}
function isDisabled(node) {
return node.disabled || (node.parentNode && isDisabled(node.parentNode));
}
if (navigator.userAgent.indexOf('Opera') !== -1) {
// use browser detection since we cannot feature-check this bug
document.addEventListener('click', ignoreIfTargetDisabled, true);
}
})();
// Checks if possible to use URL.createObjectURL()
// Support: IE
(function checkOnBlobSupport() {
// sometimes IE loosing the data created with createObjectURL(), see #3977
if (navigator.userAgent.indexOf('Trident') >= 0) {
PDFJS.disableCreateObjectURL = true;
}
})();
// Checks if navigator.language is supported
(function checkNavigatorLanguage() {
if ('language' in navigator) {
return;
}
PDFJS.locale = navigator.userLanguage || 'en-US';
})();
(function checkRangeRequests() {
// Safari has issues with cached range requests see:
// https://github.com/mozilla/pdf.js/issues/3260
// Last tested with version 6.0.4.
// Support: Safari 6.0+
var isSafari = Object.prototype.toString.call(
window.HTMLElement).indexOf('Constructor') > 0;
// Older versions of Android (pre 3.0) has issues with range requests, see:
// https://github.com/mozilla/pdf.js/issues/3381.
// Make sure that we only match webkit-based Android browsers,
// since Firefox/Fennec works as expected.
// Support: Android<3.0
var regex = /Android\s[0-2][^\d]/;
var isOldAndroid = regex.test(navigator.userAgent);
// Range requests are broken in Chrome 39 and 40, https://crbug.com/442318
var isChromeWithRangeBug = /Chrome\/(39|40)\./.test(navigator.userAgent);
if (isSafari || isOldAndroid || isChromeWithRangeBug) {
PDFJS.disableRange = true;
PDFJS.disableStream = true;
}
})();
// Check if the browser supports manipulation of the history.
// Support: IE<10, Android<4.2
(function checkHistoryManipulation() {
// Android 2.x has so buggy pushState support that it was removed in
// Android 3.0 and restored as late as in Android 4.2.
// Support: Android 2.x
if (!history.pushState || navigator.userAgent.indexOf('Android 2.') >= 0) {
PDFJS.disableHistory = true;
}
})();
// Support: IE<11, Chrome<21, Android<4.4, Safari<6
(function checkSetPresenceInImageData() {
// IE < 11 will use window.CanvasPixelArray which lacks set function.
if (window.CanvasPixelArray) {
if (typeof window.CanvasPixelArray.prototype.set !== 'function') {
window.CanvasPixelArray.prototype.set = function(arr) {
for (var i = 0, ii = this.length; i < ii; i++) {
this[i] = arr[i];
}
};
}
} else {
// Old Chrome and Android use an inaccessible CanvasPixelArray prototype.
// Because we cannot feature detect it, we rely on user agent parsing.
var polyfill = false, versionMatch;
if (navigator.userAgent.indexOf('Chrom') >= 0) {
versionMatch = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./);
// Chrome < 21 lacks the set function.
polyfill = versionMatch && parseInt(versionMatch[2]) < 21;
} else if (navigator.userAgent.indexOf('Android') >= 0) {
// Android < 4.4 lacks the set function.
// Android >= 4.4 will contain Chrome in the user agent,
// thus pass the Chrome check above and not reach this block.
polyfill = /Android\s[0-4][^\d]/g.test(navigator.userAgent);
} else if (navigator.userAgent.indexOf('Safari') >= 0) {
versionMatch = navigator.userAgent.
match(/Version\/([0-9]+)\.([0-9]+)\.([0-9]+) Safari\//);
// Safari < 6 lacks the set function.
polyfill = versionMatch && parseInt(versionMatch[1]) < 6;
}
if (polyfill) {
var contextPrototype = window.CanvasRenderingContext2D.prototype;
var createImageData = contextPrototype.createImageData;
contextPrototype.createImageData = function(w, h) {
var imageData = createImageData.call(this, w, h);
imageData.data.set = function(arr) {
for (var i = 0, ii = this.length; i < ii; i++) {
this[i] = arr[i];
}
};
return imageData;
};
// this closure will be kept referenced, so clear its vars
contextPrototype = null;
}
}
})();
// Support: IE<10, Android<4.0, iOS
(function checkRequestAnimationFrame() {
function fakeRequestAnimationFrame(callback) {
window.setTimeout(callback, 20);
}
var isIOS = /(iPad|iPhone|iPod)/g.test(navigator.userAgent);
if (isIOS) {
// requestAnimationFrame on iOS is broken, replacing with fake one.
window.requestAnimationFrame = fakeRequestAnimationFrame;
return;
}
if ('requestAnimationFrame' in window) {
return;
}
window.requestAnimationFrame =
window.mozRequestAnimationFrame ||
window.webkitRequestAnimationFrame ||
fakeRequestAnimationFrame;
})();
(function checkCanvasSizeLimitation() {
var isIOS = /(iPad|iPhone|iPod)/g.test(navigator.userAgent);
var isAndroid = /Android/g.test(navigator.userAgent);
if (isIOS || isAndroid) {
// 5MP
PDFJS.maxCanvasPixels = 5242880;
}
})();
// Disable fullscreen support for certain problematic configurations.
// Support: IE11+ (when embedded).
(function checkFullscreenSupport() {
var isEmbeddedIE = (navigator.userAgent.indexOf('Trident') >= 0 &&
window.parent !== window);
if (isEmbeddedIE) {
PDFJS.disableFullscreen = true;
}
})();
// Provides document.currentScript support
// Support: IE, Chrome<29.
(function checkCurrentScript() {
if ('currentScript' in document) {
return;
}
Object.defineProperty(document, 'currentScript', {
get: function () {
var scripts = document.getElementsByTagName('script');
return scripts[scripts.length - 1];
},
enumerable: true,
configurable: true
});
})();
}).call((typeof window === 'undefined') ? this : window);

Some files were not shown because too many files have changed in this diff Show More