Преглед изворни кода

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

Shane Kilkelly пре 9 година
родитељ
комит
9607174b64
100 измењених фајлова са 517 додато и 829 уклоњено
  1. 1 1
      services/web/app/coffee/Features/Editor/EditorController.coffee
  2. 12 0
      services/web/app/coffee/Features/Errors/ErrorController.coffee
  3. 7 2
      services/web/app/coffee/Features/Errors/Errors.coffee
  4. 7 15
      services/web/app/coffee/Features/Project/ProjectController.coffee
  5. 16 13
      services/web/app/coffee/Features/Project/ProjectCreationHandler.coffee
  6. 23 11
      services/web/app/coffee/Features/Project/ProjectDetailsHandler.coffee
  7. 0 1
      services/web/app/coffee/Features/Project/ProjectUpdateHandler.coffee
  8. 3 2
      services/web/app/coffee/Features/Subscription/LimitationsManager.coffee
  9. 8 0
      services/web/app/coffee/Features/Subscription/SubscriptionGroupController.coffee
  10. 46 23
      services/web/app/coffee/Features/Subscription/SubscriptionGroupHandler.coffee
  11. 3 0
      services/web/app/coffee/Features/Subscription/SubscriptionLocator.coffee
  12. 1 0
      services/web/app/coffee/Features/Subscription/SubscriptionRouter.coffee
  13. 14 0
      services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee
  14. 14 11
      services/web/app/coffee/Features/User/UserHandler.coffee
  15. 1 1
      services/web/app/coffee/infrastructure/PackageVersions.coffee
  16. 2 1
      services/web/app/coffee/infrastructure/Server.coffee
  17. 2 1
      services/web/app/coffee/models/Subscription.coffee
  18. 2 0
      services/web/app/views/project/editor/left-menu.pug
  19. 62 9
      services/web/app/views/project/editor/review-panel.pug
  20. 10 2
      services/web/app/views/project/list/modals.pug
  21. 2 0
      services/web/app/views/user/settings.pug
  22. 10 2
      services/web/public/coffee/ide/clone/controllers/CloneProjectModalController.coffee
  23. 59 8
      services/web/public/coffee/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.coffee
  24. 2 20
      services/web/public/coffee/ide/pdfng/directives/pdfRenderer.coffee
  25. 1 0
      services/web/public/coffee/ide/review-panel/ReviewPanelManager.coffee
  26. 84 53
      services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee
  27. 30 0
      services/web/public/coffee/ide/review-panel/directives/aggregateChangeEntry.coffee
  28. 18 3
      services/web/public/coffee/ide/review-panel/directives/reviewPanelSorted.coffee
  29. 8 2
      services/web/public/coffee/ide/settings/controllers/ProjectNameController.coffee
  30. 3 7
      services/web/public/coffee/ide/settings/services/settings.coffee
  31. 5 1
      services/web/public/coffee/main/group-members.coffee
  32. 41 7
      services/web/public/coffee/main/project-list/modal-controllers.coffee
  33. 7 28
      services/web/public/coffee/main/project-list/project-list.coffee
  34. 13 8
      services/web/public/coffee/services/queued-http.coffee
  35. 0 1
      services/web/public/js/libs/pdfjs-1.6.210p2/bcmaps/.gitattributes
  36. 0 596
      services/web/public/js/libs/pdfjs-1.6.210p2/compatibility.js
  37. 0 0
      services/web/public/js/libs/pdfjs-1.8.188/bcmaps/78-EUC-H.bcmap
  38. 0 0
      services/web/public/js/libs/pdfjs-1.8.188/bcmaps/78-EUC-V.bcmap
  39. 0 0
      services/web/public/js/libs/pdfjs-1.8.188/bcmaps/78-H.bcmap
  40. 0 0
      services/web/public/js/libs/pdfjs-1.8.188/bcmaps/78-RKSJ-H.bcmap
  41. 0 0
      services/web/public/js/libs/pdfjs-1.8.188/bcmaps/78-RKSJ-V.bcmap
  42. 0 0
      services/web/public/js/libs/pdfjs-1.8.188/bcmaps/78-V.bcmap
  43. 0 0
      services/web/public/js/libs/pdfjs-1.8.188/bcmaps/78ms-RKSJ-H.bcmap
  44. 0 0
      services/web/public/js/libs/pdfjs-1.8.188/bcmaps/78ms-RKSJ-V.bcmap
  45. 0 0
      services/web/public/js/libs/pdfjs-1.8.188/bcmaps/83pv-RKSJ-H.bcmap
  46. 0 0
      services/web/public/js/libs/pdfjs-1.8.188/bcmaps/90ms-RKSJ-H.bcmap
  47. 0 0
      services/web/public/js/libs/pdfjs-1.8.188/bcmaps/90ms-RKSJ-V.bcmap
  48. 0 0
      services/web/public/js/libs/pdfjs-1.8.188/bcmaps/90msp-RKSJ-H.bcmap
  49. 0 0
      services/web/public/js/libs/pdfjs-1.8.188/bcmaps/90msp-RKSJ-V.bcmap
  50. 0 0
      services/web/public/js/libs/pdfjs-1.8.188/bcmaps/90pv-RKSJ-H.bcmap
  51. 0 0
      services/web/public/js/libs/pdfjs-1.8.188/bcmaps/90pv-RKSJ-V.bcmap
  52. 0 0
      services/web/public/js/libs/pdfjs-1.8.188/bcmaps/Add-H.bcmap
  53. 0 0
      services/web/public/js/libs/pdfjs-1.8.188/bcmaps/Add-RKSJ-H.bcmap
  54. 0 0
      services/web/public/js/libs/pdfjs-1.8.188/bcmaps/Add-RKSJ-V.bcmap
  55. 0 0
      services/web/public/js/libs/pdfjs-1.8.188/bcmaps/Add-V.bcmap
  56. 0 0
      services/web/public/js/libs/pdfjs-1.8.188/bcmaps/Adobe-CNS1-0.bcmap
  57. 0 0
      services/web/public/js/libs/pdfjs-1.8.188/bcmaps/Adobe-CNS1-1.bcmap
  58. 0 0
      services/web/public/js/libs/pdfjs-1.8.188/bcmaps/Adobe-CNS1-2.bcmap
  59. 0 0
      services/web/public/js/libs/pdfjs-1.8.188/bcmaps/Adobe-CNS1-3.bcmap
  60. 0 0
      services/web/public/js/libs/pdfjs-1.8.188/bcmaps/Adobe-CNS1-4.bcmap
  61. 0 0
      services/web/public/js/libs/pdfjs-1.8.188/bcmaps/Adobe-CNS1-5.bcmap
  62. 0 0
      services/web/public/js/libs/pdfjs-1.8.188/bcmaps/Adobe-CNS1-6.bcmap
  63. 0 0
      services/web/public/js/libs/pdfjs-1.8.188/bcmaps/Adobe-CNS1-UCS2.bcmap
  64. 0 0
      services/web/public/js/libs/pdfjs-1.8.188/bcmaps/Adobe-GB1-0.bcmap
  65. 0 0
      services/web/public/js/libs/pdfjs-1.8.188/bcmaps/Adobe-GB1-1.bcmap
  66. 0 0
      services/web/public/js/libs/pdfjs-1.8.188/bcmaps/Adobe-GB1-2.bcmap
  67. 0 0
      services/web/public/js/libs/pdfjs-1.8.188/bcmaps/Adobe-GB1-3.bcmap
  68. 0 0
      services/web/public/js/libs/pdfjs-1.8.188/bcmaps/Adobe-GB1-4.bcmap
  69. 0 0
      services/web/public/js/libs/pdfjs-1.8.188/bcmaps/Adobe-GB1-5.bcmap
  70. 0 0
      services/web/public/js/libs/pdfjs-1.8.188/bcmaps/Adobe-GB1-UCS2.bcmap
  71. 0 0
      services/web/public/js/libs/pdfjs-1.8.188/bcmaps/Adobe-Japan1-0.bcmap
  72. 0 0
      services/web/public/js/libs/pdfjs-1.8.188/bcmaps/Adobe-Japan1-1.bcmap
  73. 0 0
      services/web/public/js/libs/pdfjs-1.8.188/bcmaps/Adobe-Japan1-2.bcmap
  74. 0 0
      services/web/public/js/libs/pdfjs-1.8.188/bcmaps/Adobe-Japan1-3.bcmap
  75. 0 0
      services/web/public/js/libs/pdfjs-1.8.188/bcmaps/Adobe-Japan1-4.bcmap
  76. 0 0
      services/web/public/js/libs/pdfjs-1.8.188/bcmaps/Adobe-Japan1-5.bcmap
  77. 0 0
      services/web/public/js/libs/pdfjs-1.8.188/bcmaps/Adobe-Japan1-6.bcmap
  78. 0 0
      services/web/public/js/libs/pdfjs-1.8.188/bcmaps/Adobe-Japan1-UCS2.bcmap
  79. 0 0
      services/web/public/js/libs/pdfjs-1.8.188/bcmaps/Adobe-Korea1-0.bcmap
  80. 0 0
      services/web/public/js/libs/pdfjs-1.8.188/bcmaps/Adobe-Korea1-1.bcmap
  81. 0 0
      services/web/public/js/libs/pdfjs-1.8.188/bcmaps/Adobe-Korea1-2.bcmap
  82. 0 0
      services/web/public/js/libs/pdfjs-1.8.188/bcmaps/Adobe-Korea1-UCS2.bcmap
  83. 0 0
      services/web/public/js/libs/pdfjs-1.8.188/bcmaps/B5-H.bcmap
  84. 0 0
      services/web/public/js/libs/pdfjs-1.8.188/bcmaps/B5-V.bcmap
  85. 0 0
      services/web/public/js/libs/pdfjs-1.8.188/bcmaps/B5pc-H.bcmap
  86. 0 0
      services/web/public/js/libs/pdfjs-1.8.188/bcmaps/B5pc-V.bcmap
  87. 0 0
      services/web/public/js/libs/pdfjs-1.8.188/bcmaps/CNS-EUC-H.bcmap
  88. 0 0
      services/web/public/js/libs/pdfjs-1.8.188/bcmaps/CNS-EUC-V.bcmap
  89. 0 0
      services/web/public/js/libs/pdfjs-1.8.188/bcmaps/CNS1-H.bcmap
  90. 0 0
      services/web/public/js/libs/pdfjs-1.8.188/bcmaps/CNS1-V.bcmap
  91. 0 0
      services/web/public/js/libs/pdfjs-1.8.188/bcmaps/CNS2-H.bcmap
  92. 0 0
      services/web/public/js/libs/pdfjs-1.8.188/bcmaps/CNS2-V.bcmap
  93. 0 0
      services/web/public/js/libs/pdfjs-1.8.188/bcmaps/ETHK-B5-H.bcmap
  94. 0 0
      services/web/public/js/libs/pdfjs-1.8.188/bcmaps/ETHK-B5-V.bcmap
  95. 0 0
      services/web/public/js/libs/pdfjs-1.8.188/bcmaps/ETen-B5-H.bcmap
  96. 0 0
      services/web/public/js/libs/pdfjs-1.8.188/bcmaps/ETen-B5-V.bcmap
  97. 0 0
      services/web/public/js/libs/pdfjs-1.8.188/bcmaps/ETenms-B5-H.bcmap
  98. 0 0
      services/web/public/js/libs/pdfjs-1.8.188/bcmaps/ETenms-B5-V.bcmap
  99. 0 0
      services/web/public/js/libs/pdfjs-1.8.188/bcmaps/EUC-H.bcmap
  100. 0 0
      services/web/public/js/libs/pdfjs-1.8.188/bcmaps/EUC-V.bcmap

+ 1 - 1
services/web/app/coffee/Features/Editor/EditorController.coffee

@@ -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)

+ 12 - 0
services/web/app/coffee/Features/Errors/ErrorController.coffee

@@ -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)

+ 7 - 2
services/web/app/coffee/Features/Errors/Errors.coffee

@@ -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

+ 7 - 15
services/web/app/coffee/Features/Project/ProjectController.coffee

@@ -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")

+ 16 - 13
services/web/app/coffee/Features/Project/ProjectCreationHandler.coffee

@@ -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 = @

+ 23 - 11
services/web/app/coffee/Features/Project/ProjectDetailsHandler.coffee

@@ -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"

+ 0 - 1
services/web/app/coffee/Features/Project/ProjectUpdateHandler.coffee

@@ -1,6 +1,5 @@
 Project = require('../../models/Project').Project
 logger = require('logger-sharelatex')
-Project = require("../../models/Project").Project
 
 module.exports = 
 	markAsUpdated : (project_id, callback)->

+ 3 - 2
services/web/app/coffee/Features/Subscription/LimitationsManager.coffee

@@ -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)->

+ 8 - 0
services/web/app/coffee/Features/Subscription/SubscriptionGroupController.coffee

@@ -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)

+ 46 - 23
services/web/app/coffee/Features/Subscription/SubscriptionGroupHandler.coffee

@@ -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
+	}

+ 3 - 0
services/web/app/coffee/Features/Subscription/SubscriptionLocator.coffee

@@ -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

+ 1 - 0
services/web/app/coffee/Features/Subscription/SubscriptionRouter.coffee

@@ -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
 
 

+ 14 - 0
services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee

@@ -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"

+ 14 - 11
services/web/app/coffee/Features/User/UserHandler.coffee

@@ -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 - 1
services/web/app/coffee/infrastructure/PackageVersions.coffee

@@ -1,5 +1,5 @@
 version = {
-	"pdfjs": "1.7.225"
+	"pdfjs": "1.8.188"
 	"moment": "2.9.0"
 	"ace": "1.2.5"
 }

+ 2 - 1
services/web/app/coffee/infrastructure/Server.coffee

@@ -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

+ 2 - 1
services/web/app/coffee/models/Subscription.coffee

@@ -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}

+ 2 - 0
services/web/app/views/project/editor/left-menu.pug

@@ -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")}

+ 62 - 9
services/web/app/views/project/editor/review-panel.pug

@@ -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
services/web/app/views/project/list/modals.pug

@@ -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 - 0
services/web/app/views/user/settings.pug

@@ -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(

+ 10 - 2
services/web/public/coffee/ide/clone/controllers/CloneProjectModalController.coffee

@@ -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')

+ 59 - 8
services/web/public/coffee/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.coffee

@@ -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)

+ 2 - 20
services/web/public/coffee/ide/pdfng/directives/pdfRenderer.coffee

@@ -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)
 
 		]

+ 1 - 0
services/web/public/coffee/ide/review-panel/ReviewPanelManager.coffee

@@ -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"

+ 84 - 53
services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee

@@ -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

+ 30 - 0
services/web/public/coffee/ide/review-panel/directives/aggregateChangeEntry.coffee

@@ -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

+ 18 - 3
services/web/public/coffee/ide/review-panel/directives/reviewPanelSorted.coffee

@@ -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}

+ 8 - 2
services/web/public/coffee/ide/settings/controllers/ProjectNameController.coffee

@@ -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 () ->

+ 3 - 7
services/web/public/coffee/ide/settings/services/settings.coffee

@@ -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
 		}
 	]

+ 5 - 1
services/web/public/coffee/main/group-members.coffee

@@ -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
 					})

+ 41 - 7
services/web/public/coffee/main/project-list/modal-controllers.coffee

@@ -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
+					$scope.state.error = false
+					$modalInstance.close(data.project_id)
+				.error (body, statusCode) ->
 					$scope.state.inflight = false
-					$modalInstance.close(project_id)
+					if statusCode == 400
+						$scope.state.error = { message: body }
+					else
+						$scope.state.error = true
 
 		$scope.cancel = () ->
 			$modalInstance.dismiss('cancel')

+ 7 - 28
services/web/public/coffee/main/project-list/project-list.coffee

@@ -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,14 +271,8 @@ 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
 			modalInstance = $modal.open(
@@ -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()

+ 13 - 8
services/web/public/coffee/services/queued-http.coffee

@@ -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()

+ 0 - 1
services/web/public/js/libs/pdfjs-1.6.210p2/bcmaps/.gitattributes

@@ -1 +0,0 @@
-* binary

+ 0 - 596
services/web/public/js/libs/pdfjs-1.6.210p2/compatibility.js

@@ -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);

+ 0 - 0
services/web/public/js/libs/pdfjs-1.6.210p2/bcmaps/78-EUC-H.bcmap → services/web/public/js/libs/pdfjs-1.8.188/bcmaps/78-EUC-H.bcmap


+ 0 - 0
services/web/public/js/libs/pdfjs-1.6.210p2/bcmaps/78-EUC-V.bcmap → services/web/public/js/libs/pdfjs-1.8.188/bcmaps/78-EUC-V.bcmap


+ 0 - 0
services/web/public/js/libs/pdfjs-1.6.210p2/bcmaps/78-H.bcmap → services/web/public/js/libs/pdfjs-1.8.188/bcmaps/78-H.bcmap


+ 0 - 0
services/web/public/js/libs/pdfjs-1.6.210p2/bcmaps/78-RKSJ-H.bcmap → services/web/public/js/libs/pdfjs-1.8.188/bcmaps/78-RKSJ-H.bcmap


+ 0 - 0
services/web/public/js/libs/pdfjs-1.6.210p2/bcmaps/78-RKSJ-V.bcmap → services/web/public/js/libs/pdfjs-1.8.188/bcmaps/78-RKSJ-V.bcmap


+ 0 - 0
services/web/public/js/libs/pdfjs-1.6.210p2/bcmaps/78-V.bcmap → services/web/public/js/libs/pdfjs-1.8.188/bcmaps/78-V.bcmap


+ 0 - 0
services/web/public/js/libs/pdfjs-1.6.210p2/bcmaps/78ms-RKSJ-H.bcmap → services/web/public/js/libs/pdfjs-1.8.188/bcmaps/78ms-RKSJ-H.bcmap


+ 0 - 0
services/web/public/js/libs/pdfjs-1.6.210p2/bcmaps/78ms-RKSJ-V.bcmap → services/web/public/js/libs/pdfjs-1.8.188/bcmaps/78ms-RKSJ-V.bcmap


+ 0 - 0
services/web/public/js/libs/pdfjs-1.6.210p2/bcmaps/83pv-RKSJ-H.bcmap → services/web/public/js/libs/pdfjs-1.8.188/bcmaps/83pv-RKSJ-H.bcmap


+ 0 - 0
services/web/public/js/libs/pdfjs-1.6.210p2/bcmaps/90ms-RKSJ-H.bcmap → services/web/public/js/libs/pdfjs-1.8.188/bcmaps/90ms-RKSJ-H.bcmap


+ 0 - 0
services/web/public/js/libs/pdfjs-1.6.210p2/bcmaps/90ms-RKSJ-V.bcmap → services/web/public/js/libs/pdfjs-1.8.188/bcmaps/90ms-RKSJ-V.bcmap


+ 0 - 0
services/web/public/js/libs/pdfjs-1.6.210p2/bcmaps/90msp-RKSJ-H.bcmap → services/web/public/js/libs/pdfjs-1.8.188/bcmaps/90msp-RKSJ-H.bcmap


+ 0 - 0
services/web/public/js/libs/pdfjs-1.6.210p2/bcmaps/90msp-RKSJ-V.bcmap → services/web/public/js/libs/pdfjs-1.8.188/bcmaps/90msp-RKSJ-V.bcmap


+ 0 - 0
services/web/public/js/libs/pdfjs-1.6.210p2/bcmaps/90pv-RKSJ-H.bcmap → services/web/public/js/libs/pdfjs-1.8.188/bcmaps/90pv-RKSJ-H.bcmap


+ 0 - 0
services/web/public/js/libs/pdfjs-1.6.210p2/bcmaps/90pv-RKSJ-V.bcmap → services/web/public/js/libs/pdfjs-1.8.188/bcmaps/90pv-RKSJ-V.bcmap


+ 0 - 0
services/web/public/js/libs/pdfjs-1.6.210p2/bcmaps/Add-H.bcmap → services/web/public/js/libs/pdfjs-1.8.188/bcmaps/Add-H.bcmap


+ 0 - 0
services/web/public/js/libs/pdfjs-1.6.210p2/bcmaps/Add-RKSJ-H.bcmap → services/web/public/js/libs/pdfjs-1.8.188/bcmaps/Add-RKSJ-H.bcmap


+ 0 - 0
services/web/public/js/libs/pdfjs-1.6.210p2/bcmaps/Add-RKSJ-V.bcmap → services/web/public/js/libs/pdfjs-1.8.188/bcmaps/Add-RKSJ-V.bcmap


+ 0 - 0
services/web/public/js/libs/pdfjs-1.6.210p2/bcmaps/Add-V.bcmap → services/web/public/js/libs/pdfjs-1.8.188/bcmaps/Add-V.bcmap


+ 0 - 0
services/web/public/js/libs/pdfjs-1.6.210p2/bcmaps/Adobe-CNS1-0.bcmap → services/web/public/js/libs/pdfjs-1.8.188/bcmaps/Adobe-CNS1-0.bcmap


+ 0 - 0
services/web/public/js/libs/pdfjs-1.6.210p2/bcmaps/Adobe-CNS1-1.bcmap → services/web/public/js/libs/pdfjs-1.8.188/bcmaps/Adobe-CNS1-1.bcmap


+ 0 - 0
services/web/public/js/libs/pdfjs-1.6.210p2/bcmaps/Adobe-CNS1-2.bcmap → services/web/public/js/libs/pdfjs-1.8.188/bcmaps/Adobe-CNS1-2.bcmap


+ 0 - 0
services/web/public/js/libs/pdfjs-1.6.210p2/bcmaps/Adobe-CNS1-3.bcmap → services/web/public/js/libs/pdfjs-1.8.188/bcmaps/Adobe-CNS1-3.bcmap


+ 0 - 0
services/web/public/js/libs/pdfjs-1.6.210p2/bcmaps/Adobe-CNS1-4.bcmap → services/web/public/js/libs/pdfjs-1.8.188/bcmaps/Adobe-CNS1-4.bcmap


+ 0 - 0
services/web/public/js/libs/pdfjs-1.6.210p2/bcmaps/Adobe-CNS1-5.bcmap → services/web/public/js/libs/pdfjs-1.8.188/bcmaps/Adobe-CNS1-5.bcmap


+ 0 - 0
services/web/public/js/libs/pdfjs-1.6.210p2/bcmaps/Adobe-CNS1-6.bcmap → services/web/public/js/libs/pdfjs-1.8.188/bcmaps/Adobe-CNS1-6.bcmap


+ 0 - 0
services/web/public/js/libs/pdfjs-1.6.210p2/bcmaps/Adobe-CNS1-UCS2.bcmap → services/web/public/js/libs/pdfjs-1.8.188/bcmaps/Adobe-CNS1-UCS2.bcmap


+ 0 - 0
services/web/public/js/libs/pdfjs-1.6.210p2/bcmaps/Adobe-GB1-0.bcmap → services/web/public/js/libs/pdfjs-1.8.188/bcmaps/Adobe-GB1-0.bcmap


+ 0 - 0
services/web/public/js/libs/pdfjs-1.6.210p2/bcmaps/Adobe-GB1-1.bcmap → services/web/public/js/libs/pdfjs-1.8.188/bcmaps/Adobe-GB1-1.bcmap


+ 0 - 0
services/web/public/js/libs/pdfjs-1.6.210p2/bcmaps/Adobe-GB1-2.bcmap → services/web/public/js/libs/pdfjs-1.8.188/bcmaps/Adobe-GB1-2.bcmap


+ 0 - 0
services/web/public/js/libs/pdfjs-1.6.210p2/bcmaps/Adobe-GB1-3.bcmap → services/web/public/js/libs/pdfjs-1.8.188/bcmaps/Adobe-GB1-3.bcmap


+ 0 - 0
services/web/public/js/libs/pdfjs-1.6.210p2/bcmaps/Adobe-GB1-4.bcmap → services/web/public/js/libs/pdfjs-1.8.188/bcmaps/Adobe-GB1-4.bcmap


+ 0 - 0
services/web/public/js/libs/pdfjs-1.6.210p2/bcmaps/Adobe-GB1-5.bcmap → services/web/public/js/libs/pdfjs-1.8.188/bcmaps/Adobe-GB1-5.bcmap


+ 0 - 0
services/web/public/js/libs/pdfjs-1.6.210p2/bcmaps/Adobe-GB1-UCS2.bcmap → services/web/public/js/libs/pdfjs-1.8.188/bcmaps/Adobe-GB1-UCS2.bcmap


+ 0 - 0
services/web/public/js/libs/pdfjs-1.6.210p2/bcmaps/Adobe-Japan1-0.bcmap → services/web/public/js/libs/pdfjs-1.8.188/bcmaps/Adobe-Japan1-0.bcmap


+ 0 - 0
services/web/public/js/libs/pdfjs-1.6.210p2/bcmaps/Adobe-Japan1-1.bcmap → services/web/public/js/libs/pdfjs-1.8.188/bcmaps/Adobe-Japan1-1.bcmap


+ 0 - 0
services/web/public/js/libs/pdfjs-1.6.210p2/bcmaps/Adobe-Japan1-2.bcmap → services/web/public/js/libs/pdfjs-1.8.188/bcmaps/Adobe-Japan1-2.bcmap


+ 0 - 0
services/web/public/js/libs/pdfjs-1.6.210p2/bcmaps/Adobe-Japan1-3.bcmap → services/web/public/js/libs/pdfjs-1.8.188/bcmaps/Adobe-Japan1-3.bcmap


+ 0 - 0
services/web/public/js/libs/pdfjs-1.6.210p2/bcmaps/Adobe-Japan1-4.bcmap → services/web/public/js/libs/pdfjs-1.8.188/bcmaps/Adobe-Japan1-4.bcmap


+ 0 - 0
services/web/public/js/libs/pdfjs-1.6.210p2/bcmaps/Adobe-Japan1-5.bcmap → services/web/public/js/libs/pdfjs-1.8.188/bcmaps/Adobe-Japan1-5.bcmap


+ 0 - 0
services/web/public/js/libs/pdfjs-1.6.210p2/bcmaps/Adobe-Japan1-6.bcmap → services/web/public/js/libs/pdfjs-1.8.188/bcmaps/Adobe-Japan1-6.bcmap


+ 0 - 0
services/web/public/js/libs/pdfjs-1.6.210p2/bcmaps/Adobe-Japan1-UCS2.bcmap → services/web/public/js/libs/pdfjs-1.8.188/bcmaps/Adobe-Japan1-UCS2.bcmap


+ 0 - 0
services/web/public/js/libs/pdfjs-1.6.210p2/bcmaps/Adobe-Korea1-0.bcmap → services/web/public/js/libs/pdfjs-1.8.188/bcmaps/Adobe-Korea1-0.bcmap


+ 0 - 0
services/web/public/js/libs/pdfjs-1.6.210p2/bcmaps/Adobe-Korea1-1.bcmap → services/web/public/js/libs/pdfjs-1.8.188/bcmaps/Adobe-Korea1-1.bcmap


+ 0 - 0
services/web/public/js/libs/pdfjs-1.6.210p2/bcmaps/Adobe-Korea1-2.bcmap → services/web/public/js/libs/pdfjs-1.8.188/bcmaps/Adobe-Korea1-2.bcmap


+ 0 - 0
services/web/public/js/libs/pdfjs-1.6.210p2/bcmaps/Adobe-Korea1-UCS2.bcmap → services/web/public/js/libs/pdfjs-1.8.188/bcmaps/Adobe-Korea1-UCS2.bcmap


+ 0 - 0
services/web/public/js/libs/pdfjs-1.6.210p2/bcmaps/B5-H.bcmap → services/web/public/js/libs/pdfjs-1.8.188/bcmaps/B5-H.bcmap


+ 0 - 0
services/web/public/js/libs/pdfjs-1.6.210p2/bcmaps/B5-V.bcmap → services/web/public/js/libs/pdfjs-1.8.188/bcmaps/B5-V.bcmap


+ 0 - 0
services/web/public/js/libs/pdfjs-1.6.210p2/bcmaps/B5pc-H.bcmap → services/web/public/js/libs/pdfjs-1.8.188/bcmaps/B5pc-H.bcmap


+ 0 - 0
services/web/public/js/libs/pdfjs-1.6.210p2/bcmaps/B5pc-V.bcmap → services/web/public/js/libs/pdfjs-1.8.188/bcmaps/B5pc-V.bcmap


+ 0 - 0
services/web/public/js/libs/pdfjs-1.6.210p2/bcmaps/CNS-EUC-H.bcmap → services/web/public/js/libs/pdfjs-1.8.188/bcmaps/CNS-EUC-H.bcmap


+ 0 - 0
services/web/public/js/libs/pdfjs-1.6.210p2/bcmaps/CNS-EUC-V.bcmap → services/web/public/js/libs/pdfjs-1.8.188/bcmaps/CNS-EUC-V.bcmap


+ 0 - 0
services/web/public/js/libs/pdfjs-1.6.210p2/bcmaps/CNS1-H.bcmap → services/web/public/js/libs/pdfjs-1.8.188/bcmaps/CNS1-H.bcmap


+ 0 - 0
services/web/public/js/libs/pdfjs-1.6.210p2/bcmaps/CNS1-V.bcmap → services/web/public/js/libs/pdfjs-1.8.188/bcmaps/CNS1-V.bcmap


+ 0 - 0
services/web/public/js/libs/pdfjs-1.6.210p2/bcmaps/CNS2-H.bcmap → services/web/public/js/libs/pdfjs-1.8.188/bcmaps/CNS2-H.bcmap


+ 0 - 0
services/web/public/js/libs/pdfjs-1.6.210p2/bcmaps/CNS2-V.bcmap → services/web/public/js/libs/pdfjs-1.8.188/bcmaps/CNS2-V.bcmap


+ 0 - 0
services/web/public/js/libs/pdfjs-1.6.210p2/bcmaps/ETHK-B5-H.bcmap → services/web/public/js/libs/pdfjs-1.8.188/bcmaps/ETHK-B5-H.bcmap


+ 0 - 0
services/web/public/js/libs/pdfjs-1.6.210p2/bcmaps/ETHK-B5-V.bcmap → services/web/public/js/libs/pdfjs-1.8.188/bcmaps/ETHK-B5-V.bcmap


+ 0 - 0
services/web/public/js/libs/pdfjs-1.6.210p2/bcmaps/ETen-B5-H.bcmap → services/web/public/js/libs/pdfjs-1.8.188/bcmaps/ETen-B5-H.bcmap


+ 0 - 0
services/web/public/js/libs/pdfjs-1.6.210p2/bcmaps/ETen-B5-V.bcmap → services/web/public/js/libs/pdfjs-1.8.188/bcmaps/ETen-B5-V.bcmap


+ 0 - 0
services/web/public/js/libs/pdfjs-1.6.210p2/bcmaps/ETenms-B5-H.bcmap → services/web/public/js/libs/pdfjs-1.8.188/bcmaps/ETenms-B5-H.bcmap


+ 0 - 0
services/web/public/js/libs/pdfjs-1.6.210p2/bcmaps/ETenms-B5-V.bcmap → services/web/public/js/libs/pdfjs-1.8.188/bcmaps/ETenms-B5-V.bcmap


+ 0 - 0
services/web/public/js/libs/pdfjs-1.6.210p2/bcmaps/EUC-H.bcmap → services/web/public/js/libs/pdfjs-1.8.188/bcmaps/EUC-H.bcmap


+ 0 - 0
services/web/public/js/libs/pdfjs-1.6.210p2/bcmaps/EUC-V.bcmap → services/web/public/js/libs/pdfjs-1.8.188/bcmaps/EUC-V.bcmap


Неке датотеке нису приказане због велике количине промена