mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge pull request #579 from sharelatex/sk-linked-files-from-project
Linked Files from Project
This commit is contained in:
commit
d3ae276091
20 changed files with 703 additions and 43 deletions
|
@ -5,7 +5,8 @@ logger = require 'logger-sharelatex'
|
|||
|
||||
module.exports = LinkedFilesController = {
|
||||
Agents: {
|
||||
url: require('./UrlAgent')
|
||||
url: require('./UrlAgent'),
|
||||
project_file: require('./ProjectFileAgent')
|
||||
}
|
||||
|
||||
createLinkedFile: (req, res, next) ->
|
||||
|
@ -22,11 +23,17 @@ module.exports = LinkedFilesController = {
|
|||
|
||||
linkedFileData = Agent.sanitizeData(data)
|
||||
linkedFileData.provider = provider
|
||||
Agent.writeIncomingFileToDisk project_id, linkedFileData, user_id, (error, fsPath) ->
|
||||
if error?
|
||||
logger.error {err: error, project_id, name, linkedFileData, parent_folder_id, user_id}, 'error writing linked file to disk'
|
||||
return Agent.handleError(error, req, res, next)
|
||||
EditorController.upsertFile project_id, parent_folder_id, name, fsPath, linkedFileData, "upload", user_id, (error) ->
|
||||
return next(error) if error?
|
||||
res.send(204) # created
|
||||
}
|
||||
Agent.checkAuth project_id, linkedFileData, user_id, (err, allowed) ->
|
||||
return Agent.handleError(err, req, res, next) if err?
|
||||
return res.sendStatus(403) if !allowed
|
||||
Agent.decorateLinkedFileData linkedFileData, (err, newLinkedFileData) ->
|
||||
return Agent.handleError(err) if err?
|
||||
linkedFileData = newLinkedFileData
|
||||
Agent.writeIncomingFileToDisk project_id, linkedFileData, user_id, (error, fsPath) ->
|
||||
if error?
|
||||
logger.error {err: error, project_id, name, linkedFileData, parent_folder_id, user_id}, 'error writing linked file to disk'
|
||||
return Agent.handleError(error, req, res, next)
|
||||
EditorController.upsertFile project_id, parent_folder_id, name, fsPath, linkedFileData, "upload", user_id, (error, file) ->
|
||||
return next(error) if error?
|
||||
res.json(new_file_id: file._id) # created
|
||||
}
|
||||
|
|
|
@ -0,0 +1,127 @@
|
|||
FileWriter = require('../../infrastructure/FileWriter')
|
||||
AuthorizationManager = require('../Authorization/AuthorizationManager')
|
||||
ProjectLocator = require('../Project/ProjectLocator')
|
||||
ProjectGetter = require('../Project/ProjectGetter')
|
||||
DocstoreManager = require('../Docstore/DocstoreManager')
|
||||
FileStoreHandler = require('../FileStore/FileStoreHandler')
|
||||
FileWriter = require('../../infrastructure/FileWriter')
|
||||
_ = require "underscore"
|
||||
Settings = require 'settings-sharelatex'
|
||||
|
||||
|
||||
AccessDeniedError = (message) ->
|
||||
error = new Error(message)
|
||||
error.name = 'AccessDenied'
|
||||
error.__proto__ = AccessDeniedError.prototype
|
||||
return error
|
||||
AccessDeniedError.prototype.__proto__ = Error.prototype
|
||||
|
||||
|
||||
BadEntityTypeError = (message) ->
|
||||
error = new Error(message)
|
||||
error.name = 'BadEntityType'
|
||||
error.__proto__ = BadEntityTypeError.prototype
|
||||
return error
|
||||
BadEntityTypeError.prototype.__proto__ = Error.prototype
|
||||
|
||||
|
||||
BadDataError = (message) ->
|
||||
error = new Error(message)
|
||||
error.name = 'BadData'
|
||||
error.__proto__ = BadDataError.prototype
|
||||
return error
|
||||
BadDataError.prototype.__proto__ = Error.prototype
|
||||
|
||||
|
||||
ProjectNotFoundError = (message) ->
|
||||
error = new Error(message)
|
||||
error.name = 'ProjectNotFound'
|
||||
error.__proto__ = ProjectNotFoundError.prototype
|
||||
return error
|
||||
ProjectNotFoundError.prototype.__proto__ = Error.prototype
|
||||
|
||||
|
||||
SourceFileNotFoundError = (message) ->
|
||||
error = new Error(message)
|
||||
error.name = 'BadData'
|
||||
error.__proto__ = SourceFileNotFoundError.prototype
|
||||
return error
|
||||
SourceFileNotFoundError.prototype.__proto__ = Error.prototype
|
||||
|
||||
|
||||
module.exports = ProjectFileAgent =
|
||||
|
||||
sanitizeData: (data) ->
|
||||
return _.pick(
|
||||
data,
|
||||
'source_project_id',
|
||||
'source_entity_path'
|
||||
)
|
||||
|
||||
_validate: (data) ->
|
||||
return (
|
||||
data.source_project_id? &&
|
||||
data.source_entity_path?
|
||||
)
|
||||
|
||||
decorateLinkedFileData: (data, callback = (err, newData) ->) ->
|
||||
callback = _.once(callback)
|
||||
{ source_project_id } = data
|
||||
return callback(new BadDataError()) if !source_project_id?
|
||||
ProjectGetter.getProject source_project_id, (err, project) ->
|
||||
return callback(err) if err?
|
||||
return callback(new ProjectNotFoundError()) if !project?
|
||||
callback(err, _.extend(data, {source_project_display_name: project.name}))
|
||||
|
||||
checkAuth: (project_id, data, current_user_id, callback = (error, allowed)->) ->
|
||||
callback = _.once(callback)
|
||||
if !ProjectFileAgent._validate(data)
|
||||
return callback(new BadDataError())
|
||||
{source_project_id, source_entity_path} = data
|
||||
AuthorizationManager.canUserReadProject current_user_id, source_project_id, null, (err, canRead) ->
|
||||
return callback(err) if err?
|
||||
callback(null, canRead)
|
||||
|
||||
writeIncomingFileToDisk:
|
||||
(project_id, data, current_user_id, callback = (error, fsPath) ->) ->
|
||||
callback = _.once(callback)
|
||||
if !ProjectFileAgent._validate(data)
|
||||
return callback(new BadDataError())
|
||||
{source_project_id, source_entity_path} = data
|
||||
ProjectLocator.findElementByPath {
|
||||
project_id: source_project_id,
|
||||
path: source_entity_path
|
||||
}, (err, entity, type) ->
|
||||
if err?
|
||||
if err.toString().match(/^not found.*/)
|
||||
err = new SourceFileNotFoundError()
|
||||
return callback(err)
|
||||
ProjectFileAgent._writeEntityToDisk source_project_id, entity._id, type, callback
|
||||
|
||||
_writeEntityToDisk: (project_id, entity_id, type, callback=(err, location)->) ->
|
||||
callback = _.once(callback)
|
||||
if type == 'doc'
|
||||
DocstoreManager.getDoc project_id, entity_id, (err, lines) ->
|
||||
return callback(err) if err?
|
||||
FileWriter.writeLinesToDisk entity_id, lines, callback
|
||||
else if type == 'file'
|
||||
FileStoreHandler.getFileStream project_id, entity_id, null, (err, fileStream) ->
|
||||
return callback(err) if err?
|
||||
FileWriter.writeStreamToDisk entity_id, fileStream, callback
|
||||
else
|
||||
callback(new BadEntityTypeError())
|
||||
|
||||
handleError: (error, req, res, next) ->
|
||||
if error instanceof AccessDeniedError
|
||||
res.status(403).send("You do not have access to this project")
|
||||
else if error instanceof BadDataError
|
||||
res.status(400).send("The submitted data is not valid")
|
||||
else if error instanceof BadEntityTypeError
|
||||
res.status(400).send("The file is the wrong type")
|
||||
else if error instanceof SourceFileNotFoundError
|
||||
res.status(404).send("Source file not found")
|
||||
else if error instanceof ProjectNotFoundError
|
||||
res.status(404).send("Project not found")
|
||||
else
|
||||
next(error)
|
||||
next()
|
|
@ -27,6 +27,12 @@ module.exports = UrlAgent = {
|
|||
url: @._prependHttpIfNeeded(data.url)
|
||||
}
|
||||
|
||||
decorateLinkedFileData: (data, callback = (err, newData) ->) ->
|
||||
return callback(null, data)
|
||||
|
||||
checkAuth: (project_id, data, current_user_id, callback = (error, allowed)->) ->
|
||||
callback(null, true)
|
||||
|
||||
writeIncomingFileToDisk: (project_id, data, current_user_id, callback = (error, fsPath) ->) ->
|
||||
callback = _.once(callback)
|
||||
url = data.url
|
||||
|
@ -65,4 +71,4 @@ module.exports = UrlAgent = {
|
|||
if !Settings.apis?.linkedUrlProxy?.url?
|
||||
throw new Error('no linked url proxy configured')
|
||||
return "#{Settings.apis.linkedUrlProxy.url}?url=#{encodeURIComponent(url)}"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ Sources = require "../Authorization/Sources"
|
|||
TokenAccessHandler = require '../TokenAccess/TokenAccessHandler'
|
||||
CollaboratorsHandler = require '../Collaborators/CollaboratorsHandler'
|
||||
Modules = require '../../infrastructure/Modules'
|
||||
ProjectEntityHandler = require './ProjectEntityHandler'
|
||||
crypto = require 'crypto'
|
||||
|
||||
module.exports = ProjectController =
|
||||
|
@ -138,6 +139,33 @@ module.exports = ProjectController =
|
|||
return next(err) if err?
|
||||
res.sendStatus 200
|
||||
|
||||
userProjectsJson: (req, res, next) ->
|
||||
user_id = AuthenticationController.getLoggedInUserId(req)
|
||||
ProjectGetter.findAllUsersProjects user_id,
|
||||
'name lastUpdated publicAccesLevel archived owner_ref tokens', (err, projects) ->
|
||||
return next(err) if err?
|
||||
projects = ProjectController._buildProjectList(projects)
|
||||
.filter((p) -> !p.archived)
|
||||
.filter((p) -> !p.isV1Project)
|
||||
.map((p) -> {_id: p.id, name: p.name, accessLevel: p.accessLevel})
|
||||
|
||||
res.json({projects: projects})
|
||||
|
||||
projectEntitiesJson: (req, res, next) ->
|
||||
user_id = AuthenticationController.getLoggedInUserId(req)
|
||||
project_id = req.params.Project_id
|
||||
ProjectGetter.getProject project_id, (err, project) ->
|
||||
return next(err) if err?
|
||||
ProjectEntityHandler.getAllEntitiesFromProject project, (err, docs, files) ->
|
||||
return next(err) if err?
|
||||
entities = docs.concat(files)
|
||||
.sort (a, b) -> a.path > b.path # Sort by path ascending
|
||||
.map (e) -> {
|
||||
path: e.path,
|
||||
type: if e.doc? then 'doc' else 'file'
|
||||
}
|
||||
res.json({project_id: project_id, entities: entities})
|
||||
|
||||
projectListPage: (req, res, next)->
|
||||
timer = new metrics.Timer("project-list")
|
||||
user_id = AuthenticationController.getLoggedInUserId(req)
|
||||
|
@ -313,6 +341,7 @@ module.exports = ProjectController =
|
|||
maxDocLength: Settings.max_doc_length
|
||||
useV2History: !!project.overleaf?.history?.display
|
||||
showRichText: req.query?.rt == 'true'
|
||||
showTestControls: req.query?.tc == 'true' || user.isAdmin
|
||||
showPublishModal: req.query?.pm == 'true'
|
||||
timer.done()
|
||||
|
||||
|
|
|
@ -6,16 +6,31 @@ Settings = require 'settings-sharelatex'
|
|||
request = require 'request'
|
||||
|
||||
module.exports = FileWriter =
|
||||
|
||||
_ensureDumpFolderExists: (callback=(error)->) ->
|
||||
fs.mkdir Settings.path.dumpFolder, (error) ->
|
||||
if error? and error.code != 'EEXIST'
|
||||
# Ignore error about already existing
|
||||
return callback(error)
|
||||
callback(null)
|
||||
|
||||
writeLinesToDisk: (identifier, lines, callback = (error, fsPath)->) ->
|
||||
callback = _.once(callback)
|
||||
fsPath = "#{Settings.path.dumpFolder}/#{identifier}_#{uuid.v4()}"
|
||||
FileWriter._ensureDumpFolderExists (error) ->
|
||||
return callback(error) if error?
|
||||
fs.writeFile fsPath, lines.join('\n'), (error) ->
|
||||
return callback(error) if error?
|
||||
callback(null, fsPath)
|
||||
|
||||
writeStreamToDisk: (identifier, stream, callback = (error, fsPath) ->) ->
|
||||
callback = _.once(callback)
|
||||
fsPath = "#{Settings.path.dumpFolder}/#{identifier}_#{uuid.v4()}"
|
||||
|
||||
stream.pause()
|
||||
fs.mkdir Settings.path.dumpFolder, (error) ->
|
||||
FileWriter._ensureDumpFolderExists (error) ->
|
||||
return callback(error) if error?
|
||||
stream.resume()
|
||||
if error? and error.code != 'EEXIST'
|
||||
# Ignore error about already existing
|
||||
return callback(error)
|
||||
|
||||
writeStream = fs.createWriteStream(fsPath)
|
||||
stream.pipe(writeStream)
|
||||
|
@ -39,4 +54,4 @@ module.exports = FileWriter =
|
|||
else
|
||||
err = new Error("bad response from url: #{response.statusCode}")
|
||||
logger.err {err, identifier, url}, err.message
|
||||
callback(err)
|
||||
callback(err)
|
||||
|
|
|
@ -119,6 +119,11 @@ module.exports = class Router
|
|||
webRouter.get '/user/personal_info', AuthenticationController.requireLogin(), UserInfoController.getLoggedInUsersPersonalInfo
|
||||
privateApiRouter.get '/user/:user_id/personal_info', AuthenticationController.httpAuth, UserInfoController.getPersonalInfo
|
||||
|
||||
webRouter.get '/user/projects', AuthenticationController.requireLogin(), ProjectController.userProjectsJson
|
||||
webRouter.get '/project/:Project_id/entities', AuthenticationController.requireLogin(),
|
||||
AuthorizationMiddlewear.ensureUserCanReadProject,
|
||||
ProjectController.projectEntitiesJson
|
||||
|
||||
webRouter.get '/project', AuthenticationController.requireLogin(), ProjectController.projectListPage
|
||||
webRouter.post '/project/new', AuthenticationController.requireLogin(), ProjectController.newProject
|
||||
|
||||
|
|
|
@ -47,7 +47,18 @@ div.binary-file.full-size(
|
|||
|
|
||||
| at {{ openFile.created | formatDate:'h:mm a' }} {{ openFile.created | relativeDate }}
|
||||
|
||||
span(ng-if="openFile.linkedFileData.provider == 'url'")
|
||||
div(ng-if="openFile.linkedFileData.provider == 'project_file'")
|
||||
p
|
||||
i.fa.fa-fw.fa-external-link-square.fa-rotate-180.linked-file-icon
|
||||
| Imported from
|
||||
|
|
||||
a(ng-href='/project/{{openFile.linkedFileData.source_project_id}}' target="_blank")
|
||||
| {{ openFile.linkedFileData.source_project_display_name }}
|
||||
| /{{ openFile.linkedFileData.source_entity_path.slice(1) }},
|
||||
|
|
||||
| at {{ openFile.created | formatDate:'h:mm a' }} {{ openFile.created | relativeDate }}
|
||||
|
||||
span(ng-if="openFile.linkedFileData.provider == 'url' || openFile.linkedFileData.provider == 'project_file'")
|
||||
button.btn.btn-success(
|
||||
href, ng-click="refreshFile(openFile)",
|
||||
ng-disabled="refreshing"
|
||||
|
@ -63,3 +74,7 @@ div.binary-file.full-size(
|
|||
i.fa.fa-fw.fa-download
|
||||
|
|
||||
| #{translate("download")}
|
||||
div(ng-if="refreshError")
|
||||
br
|
||||
.alert.alert-danger.col-md-6.col-md-offset-3
|
||||
| Error: {{ refreshError}}
|
||||
|
|
|
@ -342,6 +342,77 @@ script(type='text/ng-template', id='newDocModalTemplate')
|
|||
span(ng-show="state.inflight") #{translate("creating")}...
|
||||
|
||||
|
||||
// Project Linked Files Modal
|
||||
script(type='text/ng-template', id='projectLinkedFileModalTemplate')
|
||||
.modal-header
|
||||
h3 New file from Project
|
||||
|
||||
.modal-body
|
||||
div
|
||||
div.alert.alert-danger(ng-if="state.error") Error, something went wrong!
|
||||
div
|
||||
form
|
||||
.form-controls
|
||||
label(for="project-select") Select a Project
|
||||
span(ng-show="state.inFlight.projects")
|
||||
|
|
||||
i.fa.fa-spinner.fa-spin
|
||||
select.form-control(
|
||||
name="project-select"
|
||||
ng-model="data.selectedProjectId"
|
||||
ng-disabled="!shouldEnableProjectSelect()"
|
||||
)
|
||||
option(value="" disabled selected) - Please Select a Project
|
||||
option(
|
||||
ng-repeat="project in data.projects"
|
||||
value="{{ project._id }}"
|
||||
) {{ project.name }}
|
||||
|
||||
br
|
||||
.form-controls
|
||||
label(for="project-entity-select") Select a File
|
||||
span(ng-show="state.inFlight.entities")
|
||||
|
|
||||
i.fa.fa-spinner.fa-spin
|
||||
select.form-control(
|
||||
name="project-entity-select"
|
||||
ng-model="data.selectedProjectEntity"
|
||||
ng-disabled="!shouldEnableProjectEntitySelect()"
|
||||
)
|
||||
option(value="" disabled selected) - Please Select a File
|
||||
option(
|
||||
ng-repeat="projectEntity in data.projectEntities"
|
||||
value="{{ projectEntity.path }}"
|
||||
) {{ projectEntity.path.slice(1) }}
|
||||
br
|
||||
|
||||
.form-controls
|
||||
label(for="name") File Name In This Project
|
||||
input.form-control(
|
||||
type="text"
|
||||
placeholder="example.tex"
|
||||
required
|
||||
ng-model="data.name"
|
||||
name="name"
|
||||
)
|
||||
br
|
||||
|
||||
.modal-footer
|
||||
span(ng-show="state.inFlight.create")
|
||||
i.fa.fa-spinner.fa-spin
|
||||
|
|
||||
button.btn.btn-default(
|
||||
ng-disabled="state.inflight"
|
||||
ng-click="cancel()"
|
||||
) #{translate("cancel")}
|
||||
button.btn.btn-primary(
|
||||
ng-disabled="!shouldEnableCreateButton()"
|
||||
ng-click="create()"
|
||||
)
|
||||
span(ng-hide="state.inflight") #{translate("create")}
|
||||
span(ng-show="state.inflight") #{translate("creating")}...
|
||||
|
||||
|
||||
script(type='text/ng-template', id='linkedFileModalTemplate')
|
||||
.modal-header
|
||||
h3 New file from URL
|
||||
|
|
|
@ -62,6 +62,23 @@ aside#left-menu.full-size(
|
|||
!= moduleIncludes("editorLeftMenu:editing_services", locals)
|
||||
|
||||
|
||||
if showTestControls
|
||||
h4 Test Controls
|
||||
ul.list-unstyled.nav(ng-controller="TestControlsController")
|
||||
li
|
||||
a(href="#" ng-click="richText()")
|
||||
i.fa.fa-exclamation.fa-fw
|
||||
| Rich Text
|
||||
li
|
||||
a(href="#" ng-click="openProjectLinkedFileModal()")
|
||||
i.fa.fa-exclamation.fa-fw
|
||||
| Project-Linked-File Modal
|
||||
li
|
||||
a(href="#" ng-click="openLinkedFileModal()")
|
||||
i.fa.fa-exclamation.fa-fw
|
||||
| URL-Linked-File Modal
|
||||
|
||||
|
||||
h4(ng-show="!anonymous") #{translate("settings")}
|
||||
form.settings(ng-controller="SettingsController", ng-show="!anonymous")
|
||||
.containter-fluid
|
||||
|
@ -179,6 +196,7 @@ aside#left-menu.full-size(
|
|||
option(value="pdfjs") #{translate("built_in")}
|
||||
option(value="native") #{translate("native")}
|
||||
|
||||
|
||||
h4 #{translate("hotkeys")}
|
||||
ul.list-unstyled.nav
|
||||
li(ng-controller="HotkeysController")
|
||||
|
|
|
@ -17,6 +17,7 @@ services:
|
|||
PROJECT_HISTORY_ENABLED: 'true'
|
||||
ENABLED_LINKED_FILE_TYPES: 'url'
|
||||
LINKED_URL_PROXY: 'http://localhost:6543'
|
||||
ENABLED_LINKED_FILE_TYPES: 'url,project_file'
|
||||
SHARELATEX_CONFIG: /app/test/acceptance/config/settings.test.coffee
|
||||
depends_on:
|
||||
- redis
|
||||
|
|
|
@ -18,6 +18,7 @@ define [
|
|||
"ide/chat/index"
|
||||
"ide/clone/index"
|
||||
"ide/hotkeys/index"
|
||||
"ide/test-controls/index"
|
||||
"ide/wordcount/index"
|
||||
"ide/directives/layout"
|
||||
"ide/directives/validFile"
|
||||
|
@ -34,6 +35,7 @@ define [
|
|||
"directives/videoPlayState"
|
||||
"services/queued-http"
|
||||
"services/validateCaptcha"
|
||||
"services/wait-for"
|
||||
"filters/formatDate"
|
||||
"main/event"
|
||||
"main/account-upgrade"
|
||||
|
@ -54,7 +56,7 @@ define [
|
|||
SafariScrollPatcher
|
||||
) ->
|
||||
|
||||
App.controller "IdeController", ($scope, $timeout, ide, localStorage, sixpack, event_tracking, metadata) ->
|
||||
App.controller "IdeController", ($scope, $timeout, ide, localStorage, sixpack, event_tracking, metadata, $q) ->
|
||||
# Don't freak out if we're already in an apply callback
|
||||
$scope.$originalApply = $scope.$apply
|
||||
$scope.$apply = (fn = () ->) ->
|
||||
|
|
|
@ -2,7 +2,7 @@ define [
|
|||
"base"
|
||||
"moment"
|
||||
], (App, moment) ->
|
||||
App.controller "BinaryFileController", ["$scope", "$rootScope", "$http", "$timeout", "$element", "ide", ($scope, $rootScope, $http, $timeout, $element, ide) ->
|
||||
App.controller "BinaryFileController", ["$scope", "$rootScope", "$http", "$timeout", "$element", "ide", "waitFor", ($scope, $rootScope, $http, $timeout, $element, ide, waitFor) ->
|
||||
|
||||
TWO_MEGABYTES = 2 * 1024 * 1024
|
||||
|
||||
|
@ -31,6 +31,7 @@ define [
|
|||
data: null
|
||||
|
||||
$scope.refreshing = false
|
||||
$scope.refreshError = null
|
||||
|
||||
MAX_URL_LENGTH = 60
|
||||
FRONT_OF_URL_LENGTH = 35
|
||||
|
@ -48,9 +49,27 @@ define [
|
|||
|
||||
$scope.refreshFile = (file) ->
|
||||
$scope.refreshing = true
|
||||
$scope.refreshError = null
|
||||
ide.fileTreeManager.refreshLinkedFile(file)
|
||||
.then () ->
|
||||
loadTextFileFilePreview()
|
||||
.then (response) ->
|
||||
{ data } = response
|
||||
{ new_file_id } = data
|
||||
$timeout(
|
||||
() ->
|
||||
waitFor(
|
||||
() ->
|
||||
ide.fileTreeManager.findEntityById(new_file_id)
|
||||
5000
|
||||
)
|
||||
.then (newFile) ->
|
||||
ide.binaryFilesManager.openFile(newFile)
|
||||
.catch (err) ->
|
||||
console.warn(err)
|
||||
, 0
|
||||
)
|
||||
$scope.refreshError = null
|
||||
.catch (response) ->
|
||||
$scope.refreshError = response.data
|
||||
.finally () ->
|
||||
$scope.refreshing = false
|
||||
|
||||
|
@ -86,11 +105,9 @@ define [
|
|||
# show dots when payload is closs to cutoff
|
||||
if data.length >= (TWO_MEGABYTES - 200)
|
||||
$scope.textPreview.shouldShowDots = true
|
||||
try
|
||||
# remove last partial line
|
||||
data = data.replace(/\n.*$/, '')
|
||||
finally
|
||||
$scope.textPreview.data = data
|
||||
data = data?.replace?(/\n.*$/, '')
|
||||
$scope.textPreview.data = data
|
||||
$timeout(setHeight, 0)
|
||||
.catch (error) ->
|
||||
console.error(error)
|
||||
|
|
|
@ -43,6 +43,19 @@ define [
|
|||
}
|
||||
)
|
||||
|
||||
$scope.openProjectLinkedFileModal = window.openProjectLinkedFileModal = () ->
|
||||
unless 'project_file' in window.data.enabledLinkedFileTypes
|
||||
console.warn("Project linked files are not enabled")
|
||||
return
|
||||
$modal.open(
|
||||
templateUrl: "projectLinkedFileModalTemplate"
|
||||
controller: "ProjectLinkedFileModalController"
|
||||
scope: $scope
|
||||
resolve: {
|
||||
parent_folder: () -> ide.fileTreeManager.getCurrentFolder()
|
||||
}
|
||||
)
|
||||
|
||||
$scope.orderByFoldersFirst = (entity) ->
|
||||
return '0' if entity?.type == "folder"
|
||||
return '1'
|
||||
|
@ -201,6 +214,117 @@ define [
|
|||
$modalInstance.dismiss('cancel')
|
||||
]
|
||||
|
||||
App.controller "ProjectLinkedFileModalController", [
|
||||
"$scope", "ide", "$modalInstance", "$timeout", "parent_folder",
|
||||
($scope, ide, $modalInstance, $timeout, parent_folder) ->
|
||||
$scope.data =
|
||||
projects: null # or []
|
||||
selectedProjectId: null
|
||||
projectEntities: null # or []
|
||||
selectedProjectEntity: null
|
||||
name: null
|
||||
$scope.state =
|
||||
inFlight:
|
||||
projects: false
|
||||
entities: false
|
||||
create: false
|
||||
error: false
|
||||
|
||||
$scope.$watch 'data.selectedProjectId', (newVal, oldVal) ->
|
||||
return if !newVal
|
||||
$scope.data.selectedProjectEntity = null
|
||||
$scope.getProjectEntities($scope.data.selectedProjectId)
|
||||
|
||||
# auto-set filename based on selected file
|
||||
$scope.$watch 'data.selectedProjectEntity', (newVal, oldVal) ->
|
||||
return if !newVal
|
||||
fileName = newVal.split('/').reverse()[0]
|
||||
if fileName
|
||||
$scope.data.name = fileName
|
||||
|
||||
_setInFlight = (type) ->
|
||||
$scope.state.inFlight[type] = true
|
||||
|
||||
_reset = (opts) ->
|
||||
isError = opts.err == true
|
||||
inFlight = $scope.state.inFlight
|
||||
inFlight.projects = inFlight.entities = inFlight.create = false
|
||||
$scope.state.error = isError
|
||||
|
||||
$scope.shouldEnableProjectSelect = () ->
|
||||
{ state, data } = $scope
|
||||
return !state.inFlight.projects && data.projects
|
||||
|
||||
$scope.shouldEnableProjectEntitySelect = () ->
|
||||
{ state, data } = $scope
|
||||
return !state.inFlight.projects && !state.inFlight.entities && data.projects && data.selectedProjectId
|
||||
|
||||
$scope.shouldEnableCreateButton = () ->
|
||||
state = $scope.state
|
||||
data = $scope.data
|
||||
return !state.inFlight.projects &&
|
||||
!state.inFlight.entities &&
|
||||
data.projects &&
|
||||
data.selectedProjectId &&
|
||||
data.projectEntities &&
|
||||
data.selectedProjectEntity &&
|
||||
data.name
|
||||
|
||||
$scope.getUserProjects = () ->
|
||||
_setInFlight('projects')
|
||||
ide.$http.get("/user/projects", {
|
||||
_csrf: window.csrfToken
|
||||
})
|
||||
.then (resp) ->
|
||||
$scope.data.projectEntities = null
|
||||
$scope.data.projects = resp.data.projects.filter (p) ->
|
||||
p._id != ide.project_id
|
||||
_reset(err: false)
|
||||
.catch (err) ->
|
||||
_reset(err: true)
|
||||
|
||||
$scope.getProjectEntities = (project_id) =>
|
||||
_setInFlight('entities')
|
||||
ide.$http.get("/project/#{project_id}/entities", {
|
||||
_csrf: window.csrfToken
|
||||
})
|
||||
.then (resp) ->
|
||||
if $scope.data.selectedProjectId == resp.data.project_id
|
||||
$scope.data.projectEntities = resp.data.entities
|
||||
_reset(err: false)
|
||||
.catch (err) ->
|
||||
_reset(err: true)
|
||||
|
||||
$scope.init = () ->
|
||||
$scope.getUserProjects()
|
||||
$timeout($scope.init, 0)
|
||||
|
||||
$scope.create = () ->
|
||||
projectId = $scope.data.selectedProjectId
|
||||
path = $scope.data.selectedProjectEntity
|
||||
name = $scope.data.name
|
||||
if !name || !path || !projectId
|
||||
_reset(err: true)
|
||||
return
|
||||
_setInFlight('create')
|
||||
ide.fileTreeManager
|
||||
.createLinkedFile(name, parent_folder, 'project_file', {
|
||||
source_project_id: projectId,
|
||||
source_entity_path: path
|
||||
})
|
||||
.then () ->
|
||||
_reset(err: false)
|
||||
$modalInstance.close()
|
||||
.catch (response)->
|
||||
{ data } = response
|
||||
_reset(err: true)
|
||||
|
||||
$scope.cancel = () ->
|
||||
$modalInstance.dismiss('cancel')
|
||||
|
||||
]
|
||||
|
||||
# TODO: rename all this to UrlLinkedFilModalController
|
||||
App.controller "LinkedFileModalController", [
|
||||
"$scope", "ide", "$modalInstance", "$timeout", "parent_folder",
|
||||
($scope, ide, $modalInstance, $timeout, parent_folder) ->
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
define [
|
||||
"base"
|
||||
], (App) ->
|
||||
App.controller "HistoryV2DiffController", ($scope, ide, event_tracking) ->
|
||||
App.controller "HistoryV2DiffController", ($scope, ide, event_tracking, waitFor) ->
|
||||
$scope.restoreState =
|
||||
inflight: false
|
||||
error: false
|
||||
|
@ -24,17 +24,16 @@ define [
|
|||
$scope.restoreState.inflight = false
|
||||
|
||||
openEntity = (data) ->
|
||||
iterations = 0
|
||||
{id, type} = data
|
||||
do tryOpen = () ->
|
||||
if iterations > 5
|
||||
return
|
||||
iterations += 1
|
||||
entity = ide.fileTreeManager.findEntityById(id)
|
||||
if entity? and type == 'doc'
|
||||
ide.editorManager.openDoc(entity)
|
||||
else if entity? and type == 'file'
|
||||
ide.binaryFilesManager.openFile(entity)
|
||||
else
|
||||
setTimeout(tryOpen, 500)
|
||||
|
||||
waitFor(
|
||||
() ->
|
||||
ide.fileTreeManager.findEntityById(id)
|
||||
3000
|
||||
)
|
||||
.then (entity) ->
|
||||
if type == 'doc'
|
||||
ide.editorManager.openDoc(entity)
|
||||
else if type == 'file'
|
||||
ide.binaryFilesManager.openFile(entity)
|
||||
.catch (err) ->
|
||||
console.warn(err)
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
define [
|
||||
"base"
|
||||
"ace/ace"
|
||||
], (App) ->
|
||||
App.controller "TestControlsController", ($scope) ->
|
||||
|
||||
$scope.openProjectLinkedFileModal = () ->
|
||||
window.openProjectLinkedFileModal()
|
||||
|
||||
$scope.openLinkedFileModal = () ->
|
||||
window.openLinkedFileModal()
|
||||
|
||||
$scope.richText = () ->
|
||||
current = window.location.toString()
|
||||
target = "#{current}#{if window.location.search then '&' else '?'}rt=true"
|
||||
window.location.href = target
|
|
@ -0,0 +1,3 @@
|
|||
define [
|
||||
"ide/test-controls/controllers/TestControlsController"
|
||||
], () ->
|
20
services/web/public/coffee/services/wait-for.coffee
Normal file
20
services/web/public/coffee/services/wait-for.coffee
Normal file
|
@ -0,0 +1,20 @@
|
|||
define [
|
||||
"base"
|
||||
], (App) ->
|
||||
App.factory "waitFor", ($q) ->
|
||||
waitFor = (testFunction, timeout, pollInterval=500) ->
|
||||
iterationLimit = Math.floor(timeout / pollInterval)
|
||||
iterations = 0
|
||||
$q(
|
||||
(resolve, reject) ->
|
||||
do tryIteration = () ->
|
||||
if iterations > iterationLimit
|
||||
return reject(new Error("waiting too long, #{JSON.stringify({timeout, pollInterval})}"))
|
||||
iterations += 1
|
||||
result = testFunction()
|
||||
if result?
|
||||
resolve(result)
|
||||
else
|
||||
setTimeout(tryIteration, pollInterval)
|
||||
)
|
||||
return waitFor
|
|
@ -27,6 +27,122 @@ describe "LinkedFiles", ->
|
|||
@owner.login ->
|
||||
mkdirp Settings.path.dumpFolder, done
|
||||
|
||||
describe "creating a project linked file", ->
|
||||
before (done) ->
|
||||
@source_doc_name = 'test.txt'
|
||||
async.series [
|
||||
(cb) =>
|
||||
@owner.createProject 'plf-test-one', {template: 'blank'}, (error, project_id) =>
|
||||
@project_one_id = project_id
|
||||
cb(error)
|
||||
(cb) =>
|
||||
@owner.getProject @project_one_id, (error, project) =>
|
||||
@project_one = project
|
||||
@project_one_root_folder_id = project.rootFolder[0]._id.toString()
|
||||
cb(error)
|
||||
(cb) =>
|
||||
@owner.createProject 'plf-test-two', {template: 'blank'}, (error, project_id) =>
|
||||
@project_two_id = project_id
|
||||
cb(error)
|
||||
(cb) =>
|
||||
@owner.getProject @project_two_id, (error, project) =>
|
||||
@project_two = project
|
||||
@project_two_root_folder_id = project.rootFolder[0]._id.toString()
|
||||
cb(error)
|
||||
(cb) =>
|
||||
@owner.createDocInProject @project_two_id,
|
||||
@project_two_root_folder_id,
|
||||
@source_doc_name,
|
||||
(error, doc_id) =>
|
||||
@source_doc_id = doc_id
|
||||
cb(error)
|
||||
(cb) =>
|
||||
@owner.createDocInProject @project_two_id,
|
||||
@project_two_root_folder_id,
|
||||
'some-harmless-doc.txt',
|
||||
(error, doc_id) =>
|
||||
cb(error)
|
||||
], done
|
||||
|
||||
it 'should produce a list of the users projects', (done) ->
|
||||
@owner.request.get {
|
||||
url: "/user/projects",
|
||||
json: true
|
||||
}, (err, response, body) =>
|
||||
expect(err).to.not.exist
|
||||
expect(body).to.deep.equal {
|
||||
projects: [
|
||||
{ _id: @project_one_id, name: 'plf-test-one', accessLevel: 'owner' },
|
||||
{ _id: @project_two_id, name: 'plf-test-two', accessLevel: 'owner' }
|
||||
]
|
||||
}
|
||||
done()
|
||||
|
||||
it 'should produce a list of entities in the project', (done) ->
|
||||
@owner.request.get {
|
||||
url: "/project/#{@project_two_id}/entities",
|
||||
json: true
|
||||
}, (err, response, body) =>
|
||||
expect(err).to.not.exist
|
||||
expect(body).to.deep.equal {
|
||||
project_id: @project_two_id,
|
||||
entities: [
|
||||
{ path: '/main.tex', type: 'doc' },
|
||||
{ path: '/some-harmless-doc.txt', type: 'doc' },
|
||||
{ path: '/test.txt', type: 'doc' }
|
||||
]
|
||||
}
|
||||
done()
|
||||
|
||||
|
||||
it 'should import a file from the source project', (done) ->
|
||||
@owner.request.post {
|
||||
url: "/project/#{@project_one_id}/linked_file",
|
||||
json:
|
||||
name: 'test-link.txt',
|
||||
parent_folder_id: @project_one_root_folder_id,
|
||||
provider: 'project_file',
|
||||
data:
|
||||
source_project_id: @project_two_id,
|
||||
source_entity_path: "/#{@source_doc_name}",
|
||||
}, (error, response, body) =>
|
||||
new_file_id = body.new_file_id
|
||||
@existing_file_id = new_file_id
|
||||
expect(new_file_id).to.exist
|
||||
@owner.getProject @project_one_id, (error, project) =>
|
||||
return done(error) if error?
|
||||
firstFile = project.rootFolder[0].fileRefs[0]
|
||||
expect(firstFile._id.toString()).to.equal(new_file_id.toString())
|
||||
expect(firstFile.linkedFileData).to.deep.equal {
|
||||
provider: 'project_file',
|
||||
source_project_id: @project_two_id,
|
||||
source_entity_path: "/#{@source_doc_name}",
|
||||
source_project_display_name: "plf-test-two"
|
||||
}
|
||||
expect(firstFile.name).to.equal('test-link.txt')
|
||||
done()
|
||||
|
||||
it 'should refresh the file', (done) ->
|
||||
@owner.request.post {
|
||||
url: "/project/#{@project_one_id}/linked_file",
|
||||
json:
|
||||
name: 'test-link.txt',
|
||||
parent_folder_id: @project_one_root_folder_id,
|
||||
provider: 'project_file',
|
||||
data:
|
||||
source_project_id: @project_two_id,
|
||||
source_entity_path: "/#{@source_doc_name}",
|
||||
}, (error, response, body) =>
|
||||
new_file_id = body.new_file_id
|
||||
expect(new_file_id).to.exist
|
||||
expect(new_file_id).to.not.equal @existing_file_id
|
||||
@owner.getProject @project_one_id, (error, project) =>
|
||||
return done(error) if error?
|
||||
firstFile = project.rootFolder[0].fileRefs[0]
|
||||
expect(firstFile._id.toString()).to.equal(new_file_id.toString())
|
||||
expect(firstFile.name).to.equal('test-link.txt')
|
||||
done()
|
||||
|
||||
describe "creating a URL based linked file", ->
|
||||
before (done) ->
|
||||
@owner.createProject "url-linked-files-project", {template: "blank"}, (error, project_id) =>
|
||||
|
@ -50,7 +166,7 @@ describe "LinkedFiles", ->
|
|||
name: 'url-test-file-1'
|
||||
}, (error, response, body) =>
|
||||
throw error if error?
|
||||
expect(response.statusCode).to.equal 204
|
||||
expect(response.statusCode).to.equal 200
|
||||
@owner.getProject @project_id, (error, project) =>
|
||||
throw error if error?
|
||||
file = project.rootFolder[0].fileRefs[0]
|
||||
|
@ -76,7 +192,7 @@ describe "LinkedFiles", ->
|
|||
name: 'url-test-file-2'
|
||||
}, (error, response, body) =>
|
||||
throw error if error?
|
||||
expect(response.statusCode).to.equal 204
|
||||
expect(response.statusCode).to.equal 200
|
||||
@owner.request.post {
|
||||
url: "/project/#{@project_id}/linked_file",
|
||||
json:
|
||||
|
@ -88,7 +204,7 @@ describe "LinkedFiles", ->
|
|||
name: 'url-test-file-2'
|
||||
}, (error, response, body) =>
|
||||
throw error if error?
|
||||
expect(response.statusCode).to.equal 204
|
||||
expect(response.statusCode).to.equal 200
|
||||
@owner.getProject @project_id, (error, project) =>
|
||||
throw error if error?
|
||||
file = project.rootFolder[0].fileRefs[1]
|
||||
|
@ -168,7 +284,7 @@ describe "LinkedFiles", ->
|
|||
name: 'url-test-file-6'
|
||||
}, (error, response, body) =>
|
||||
throw error if error?
|
||||
expect(response.statusCode).to.equal 204
|
||||
expect(response.statusCode).to.equal 200
|
||||
@owner.getProject @project_id, (error, project) =>
|
||||
throw error if error?
|
||||
file = _.find project.rootFolder[0].fileRefs, (file) ->
|
||||
|
|
|
@ -143,6 +143,18 @@ class User
|
|||
return callback(err)
|
||||
callback(null)
|
||||
|
||||
createDocInProject: (project_id, parent_folder_id, name, callback=(error, doc_id)->) ->
|
||||
@getCsrfToken (error) =>
|
||||
return callback(error) if error?
|
||||
@request.post {
|
||||
url: "/project/#{project_id}/doc",
|
||||
json: {
|
||||
name: name,
|
||||
parent_folder_id: parent_folder_id
|
||||
}
|
||||
}, (error, response, body) =>
|
||||
callback(null, body._id)
|
||||
|
||||
addUserToProject: (project_id, user, privileges, callback = (error, user) ->) ->
|
||||
if privileges == 'readAndWrite'
|
||||
updateOp = {$addToSet: {collaberator_refs: user._id.toString()}}
|
||||
|
|
|
@ -67,6 +67,7 @@ describe "ProjectController", ->
|
|||
protectTokens: sinon.stub()
|
||||
@CollaboratorsHandler =
|
||||
userIsTokenMember: sinon.stub().callsArgWith(2, null, false)
|
||||
@ProjectEntityHandler = {}
|
||||
@Modules =
|
||||
hooks:
|
||||
fire: sinon.stub()
|
||||
|
@ -98,6 +99,7 @@ describe "ProjectController", ->
|
|||
"../TokenAccess/TokenAccessHandler": @TokenAccessHandler
|
||||
"../Collaborators/CollaboratorsHandler": @CollaboratorsHandler
|
||||
"../../infrastructure/Modules": @Modules
|
||||
"./ProjectEntityHandler": @ProjectEntityHandler
|
||||
|
||||
@projectName = "£12321jkj9ujkljds"
|
||||
@req =
|
||||
|
@ -520,7 +522,62 @@ describe "ProjectController", ->
|
|||
@ProjectUpdateHandler.markAsOpened.calledWith(@project_id).should.equal true
|
||||
done()
|
||||
@ProjectController.loadEditor @req, @res
|
||||
|
||||
|
||||
describe 'userProjectsJson', ->
|
||||
beforeEach (done) ->
|
||||
projects = [
|
||||
{archived: true, id: 'a', name: 'A', accessLevel: 'a', somethingElse: 1}
|
||||
{archived: false, id: 'b', name: 'B', accessLevel: 'b', somethingElse: 1}
|
||||
{archived: false, id: 'c', name: 'C', accessLevel: 'c', somethingElse: 1}
|
||||
{archived: false, id: 'd', name: 'D', accessLevel: 'd', somethingElse: 1}
|
||||
]
|
||||
@ProjectGetter.findAllUsersProjects = sinon.stub().callsArgWith(2, null, [])
|
||||
@ProjectController._buildProjectList = sinon.stub().returns(projects)
|
||||
@AuthenticationController.getLoggedInUserId = sinon.stub().returns 'abc'
|
||||
done()
|
||||
|
||||
it 'should produce a list of projects', (done) ->
|
||||
@res.json = (data) =>
|
||||
expect(data).to.deep.equal {
|
||||
projects: [
|
||||
{_id: 'b', name: 'B', accessLevel: 'b'},
|
||||
{_id: 'c', name: 'C', accessLevel: 'c'},
|
||||
{_id: 'd', name: 'D', accessLevel: 'd'}
|
||||
]
|
||||
}
|
||||
done()
|
||||
@ProjectController.userProjectsJson @req, @res, @next
|
||||
|
||||
describe 'projectEntitiesJson', ->
|
||||
beforeEach () ->
|
||||
@AuthenticationController.getLoggedInUserId = sinon.stub().returns 'abc'
|
||||
@req.params = {Project_id: 'abcd'}
|
||||
@project = { _id: 'abcd' }
|
||||
@docs = [
|
||||
{path: '/things/b.txt', doc: true},
|
||||
{path: '/main.tex', doc: true}
|
||||
]
|
||||
@files = [
|
||||
{path: '/things/a.txt'}
|
||||
]
|
||||
@ProjectGetter.getProject = sinon.stub().callsArgWith(1, null, @project)
|
||||
@ProjectEntityHandler.getAllEntitiesFromProject = sinon.stub().callsArgWith(1, null, @docs, @files)
|
||||
|
||||
it 'should produce a list of entities', (done) ->
|
||||
@res.json = (data) =>
|
||||
expect(data).to.deep.equal {
|
||||
project_id: 'abcd',
|
||||
entities: [
|
||||
{path: '/main.tex', type: 'doc'},
|
||||
{path: '/things/a.txt', type: 'file'},
|
||||
{path: '/things/b.txt', type: 'doc'}
|
||||
]
|
||||
}
|
||||
expect(@ProjectGetter.getProject.callCount).to.equal 1
|
||||
expect(@ProjectEntityHandler.getAllEntitiesFromProject.callCount).to.equal 1
|
||||
done()
|
||||
@ProjectController.projectEntitiesJson @req, @res, @next
|
||||
|
||||
describe '_isInPercentageRollout', ->
|
||||
before ->
|
||||
@ids = [
|
||||
|
|
Loading…
Reference in a new issue