mirror of
https://github.com/overleaf/overleaf.git
synced 2024-10-31 21:21:03 -04:00
6ab9fffb49
GitOrigin-RevId: feeae54575cce8b315b4e6bb0df3e17405025855
740 lines
20 KiB
JavaScript
740 lines
20 KiB
JavaScript
/* eslint-disable
|
|
camelcase,
|
|
node/handle-callback-err,
|
|
max-len,
|
|
no-dupe-class-members,
|
|
no-return-assign,
|
|
no-unused-vars,
|
|
*/
|
|
// TODO: This file was created by bulk-decaffeinate.
|
|
// Fix any style issues and re-enable lint.
|
|
/*
|
|
* decaffeinate suggestions:
|
|
* DS101: Remove unnecessary use of Array.from
|
|
* DS102: Remove unnecessary code created because of implicit returns
|
|
* DS103: Rewrite code to no longer use __guard__
|
|
* DS202: Simplify dynamic range loops
|
|
* DS205: Consider reworking code to avoid use of IIFEs
|
|
* DS207: Consider shorter variations of null checks
|
|
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
|
*/
|
|
import './directives/fileEntity'
|
|
import './controllers/FileTreeController'
|
|
import './controllers/FileTreeEntityController'
|
|
import './controllers/FileTreeFolderController'
|
|
import '../../features/file-tree/controllers/file-tree-controller'
|
|
let FileTreeManager
|
|
|
|
export default FileTreeManager = class FileTreeManager {
|
|
constructor(ide, $scope) {
|
|
this.ide = ide
|
|
this.$scope = $scope
|
|
this.$scope.$on('project:joined', () => {
|
|
this.loadRootFolder()
|
|
this.loadDeletedDocs()
|
|
return this.$scope.$emit('file-tree:initialized')
|
|
})
|
|
|
|
this.$scope.$on('entities:multiSelected', (_event, data) => {
|
|
this.$scope.$apply(() => {
|
|
this.$scope.multiSelectedCount = data.count
|
|
})
|
|
})
|
|
|
|
this.$scope.$watch('rootFolder', rootFolder => {
|
|
if (rootFolder != null) {
|
|
return this.recalculateDocList()
|
|
}
|
|
})
|
|
|
|
this._bindToSocketEvents()
|
|
|
|
this.$scope.multiSelectedCount = 0
|
|
|
|
$(document).on('click', () => {
|
|
this.clearMultiSelectedEntities()
|
|
return this.$scope.$digest()
|
|
})
|
|
}
|
|
|
|
_bindToSocketEvents() {
|
|
this.ide.socket.on('reciveNewDoc', (parent_folder_id, doc) => {
|
|
const parent_folder =
|
|
this.findEntityById(parent_folder_id) || this.$scope.rootFolder
|
|
return this.$scope.$apply(() => {
|
|
parent_folder.children.push({
|
|
name: doc.name,
|
|
id: doc._id,
|
|
type: 'doc',
|
|
})
|
|
return this.recalculateDocList()
|
|
})
|
|
})
|
|
|
|
this.ide.socket.on(
|
|
'reciveNewFile',
|
|
(parent_folder_id, file, source, linkedFileData) => {
|
|
const parent_folder =
|
|
this.findEntityById(parent_folder_id) || this.$scope.rootFolder
|
|
return this.$scope.$apply(() => {
|
|
parent_folder.children.push({
|
|
name: file.name,
|
|
id: file._id,
|
|
type: 'file',
|
|
linkedFileData,
|
|
created: file.created,
|
|
})
|
|
return this.recalculateDocList()
|
|
})
|
|
}
|
|
)
|
|
|
|
this.ide.socket.on('reciveNewFolder', (parent_folder_id, folder) => {
|
|
const parent_folder =
|
|
this.findEntityById(parent_folder_id) || this.$scope.rootFolder
|
|
return this.$scope.$apply(() => {
|
|
parent_folder.children.push({
|
|
name: folder.name,
|
|
id: folder._id,
|
|
type: 'folder',
|
|
children: [],
|
|
})
|
|
return this.recalculateDocList()
|
|
})
|
|
})
|
|
|
|
this.ide.socket.on('reciveEntityRename', (entity_id, name) => {
|
|
const entity = this.findEntityById(entity_id)
|
|
if (entity == null) {
|
|
return
|
|
}
|
|
return this.$scope.$apply(() => {
|
|
entity.name = name
|
|
return this.recalculateDocList()
|
|
})
|
|
})
|
|
|
|
this.ide.socket.on('removeEntity', entity_id => {
|
|
const entity = this.findEntityById(entity_id)
|
|
if (entity == null) {
|
|
return
|
|
}
|
|
this.$scope.$apply(() => {
|
|
this._deleteEntityFromScope(entity)
|
|
return this.recalculateDocList()
|
|
})
|
|
return this.$scope.$broadcast('entity:deleted', entity)
|
|
})
|
|
|
|
return this.ide.socket.on('reciveEntityMove', (entity_id, folder_id) => {
|
|
const entity = this.findEntityById(entity_id)
|
|
const folder = this.findEntityById(folder_id)
|
|
return this.$scope.$apply(() => {
|
|
this._moveEntityInScope(entity, folder)
|
|
return this.recalculateDocList()
|
|
})
|
|
})
|
|
}
|
|
|
|
selectEntity(entity) {
|
|
this.selected_entity_id = entity.id // For reselecting after a reconnect
|
|
this.ide.fileTreeManager.forEachEntity(entity => (entity.selected = false))
|
|
return (entity.selected = true)
|
|
}
|
|
|
|
toggleMultiSelectEntity(entity) {
|
|
entity.multiSelected = !entity.multiSelected
|
|
return (this.$scope.multiSelectedCount = this.multiSelectedCount())
|
|
}
|
|
|
|
multiSelectedCount() {
|
|
let count = 0
|
|
this.forEachEntity(function (entity) {
|
|
if (entity.multiSelected) {
|
|
return count++
|
|
}
|
|
})
|
|
return count
|
|
}
|
|
|
|
getMultiSelectedEntities() {
|
|
const entities = []
|
|
this.forEachEntity(function (e) {
|
|
if (e.multiSelected) {
|
|
return entities.push(e)
|
|
}
|
|
})
|
|
return entities
|
|
}
|
|
|
|
getFullCount() {
|
|
const entities = []
|
|
this.forEachEntity(function (e) {
|
|
if (!e.deleted) entities.push(e)
|
|
})
|
|
return entities.length
|
|
}
|
|
|
|
getMultiSelectedEntityChildNodes() {
|
|
// use pathnames with a leading slash to avoid
|
|
// problems with reserved Object properties
|
|
const entities = this.getMultiSelectedEntities()
|
|
const paths = {}
|
|
for (var entity of Array.from(entities)) {
|
|
paths['/' + this.getEntityPath(entity)] = entity
|
|
}
|
|
const prefixes = {}
|
|
for (var path in paths) {
|
|
entity = paths[path]
|
|
const parts = path.split('/')
|
|
if (parts.length <= 2) {
|
|
continue
|
|
} else {
|
|
// Record prefixes a/b/c.tex -> 'a' and 'a/b'
|
|
for (
|
|
let i = 1, end = parts.length - 1, asc = end >= 1;
|
|
asc ? i <= end : i >= end;
|
|
asc ? i++ : i--
|
|
) {
|
|
prefixes['/' + parts.slice(0, i).join('/')] = true
|
|
}
|
|
}
|
|
}
|
|
const child_entities = []
|
|
for (path in paths) {
|
|
// If the path is in the prefixes, then it's a parent folder and
|
|
// should be ignore
|
|
entity = paths[path]
|
|
if (prefixes[path] == null) {
|
|
child_entities.push(entity)
|
|
}
|
|
}
|
|
return child_entities
|
|
}
|
|
|
|
clearMultiSelectedEntities() {
|
|
if (this.$scope.multiSelectedCount === 0) {
|
|
return
|
|
} // Be efficient, this is called a lot on 'click'
|
|
this.forEachEntity(entity => (entity.multiSelected = false))
|
|
return (this.$scope.multiSelectedCount = 0)
|
|
}
|
|
|
|
multiSelectSelectedEntity() {
|
|
const entity = this.findSelectedEntity()
|
|
if (entity) {
|
|
entity.multiSelected = true
|
|
}
|
|
this.$scope.multiSelectedCount = this.multiSelectedCount()
|
|
}
|
|
|
|
existsInFolder(folder_id, name) {
|
|
const folder = this.findEntityById(folder_id)
|
|
if (folder == null) {
|
|
return false
|
|
}
|
|
const entity = this._findEntityByPathInFolder(folder, name)
|
|
return entity != null
|
|
}
|
|
|
|
findSelectedEntity() {
|
|
let selected = null
|
|
this.forEachEntity(function (entity) {
|
|
if (entity.selected) {
|
|
return (selected = entity)
|
|
}
|
|
})
|
|
return selected
|
|
}
|
|
|
|
findEntityById(id, options) {
|
|
if (options == null) {
|
|
options = {}
|
|
}
|
|
if (this.$scope.rootFolder.id === id) {
|
|
return this.$scope.rootFolder
|
|
}
|
|
|
|
let entity = this._findEntityByIdInFolder(this.$scope.rootFolder, id)
|
|
if (entity != null) {
|
|
return entity
|
|
}
|
|
|
|
if (options.includeDeleted) {
|
|
for (entity of Array.from(this.$scope.deletedDocs)) {
|
|
if (entity.id === id) {
|
|
return entity
|
|
}
|
|
}
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
_findEntityByIdInFolder(folder, id) {
|
|
for (const entity of Array.from(folder.children || [])) {
|
|
if (entity.id === id) {
|
|
return entity
|
|
} else if (entity.children != null) {
|
|
const result = this._findEntityByIdInFolder(entity, id)
|
|
if (result != null) {
|
|
return result
|
|
}
|
|
}
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
findEntityByPath(path) {
|
|
return this._findEntityByPathInFolder(this.$scope.rootFolder, path)
|
|
}
|
|
|
|
_findEntityByPathInFolder(folder, path) {
|
|
if (path == null || folder == null) {
|
|
return null
|
|
}
|
|
if (path === '') {
|
|
return folder
|
|
}
|
|
|
|
const parts = path.split('/')
|
|
const name = parts.shift()
|
|
const rest = parts.join('/')
|
|
|
|
if (name === '.') {
|
|
return this._findEntityByPathInFolder(folder, rest)
|
|
}
|
|
|
|
for (const entity of Array.from(folder.children)) {
|
|
if (entity.name === name) {
|
|
if (rest === '') {
|
|
return entity
|
|
} else if (entity.type === 'folder') {
|
|
return this._findEntityByPathInFolder(entity, rest)
|
|
}
|
|
}
|
|
}
|
|
return null
|
|
}
|
|
|
|
forEachEntity(callback) {
|
|
if (callback == null) {
|
|
callback = function (entity, parent_folder, path) {}
|
|
}
|
|
this._forEachEntityInFolder(this.$scope.rootFolder, null, callback)
|
|
|
|
return (() => {
|
|
const result = []
|
|
for (const entity of Array.from(this.$scope.deletedDocs || [])) {
|
|
result.push(callback(entity))
|
|
}
|
|
return result
|
|
})()
|
|
}
|
|
|
|
_forEachEntityInFolder(folder, path, callback) {
|
|
return (() => {
|
|
const result = []
|
|
for (const entity of Array.from(folder.children || [])) {
|
|
var childPath
|
|
if (path != null) {
|
|
childPath = path + '/' + entity.name
|
|
} else {
|
|
childPath = entity.name
|
|
}
|
|
callback(entity, folder, childPath)
|
|
if (entity.children != null) {
|
|
result.push(this._forEachEntityInFolder(entity, childPath, callback))
|
|
} else {
|
|
result.push(undefined)
|
|
}
|
|
}
|
|
return result
|
|
})()
|
|
}
|
|
|
|
getEntityPath(entity) {
|
|
return this._getEntityPathInFolder(this.$scope.rootFolder, entity)
|
|
}
|
|
|
|
_getEntityPathInFolder(folder, entity) {
|
|
for (const child of Array.from(folder.children || [])) {
|
|
if (child === entity) {
|
|
return entity.name
|
|
} else if (child.type === 'folder') {
|
|
const path = this._getEntityPathInFolder(child, entity)
|
|
if (path != null) {
|
|
return child.name + '/' + path
|
|
}
|
|
}
|
|
}
|
|
return null
|
|
}
|
|
|
|
getRootDocDirname() {
|
|
const rootDoc = this.findEntityById(this.$scope.project.rootDoc_id)
|
|
if (rootDoc == null) {
|
|
return
|
|
}
|
|
return this._getEntityDirname(rootDoc)
|
|
}
|
|
|
|
_getEntityDirname(entity) {
|
|
const path = this.getEntityPath(entity)
|
|
if (path == null) {
|
|
return
|
|
}
|
|
return path.split('/').slice(0, -1).join('/')
|
|
}
|
|
|
|
_findParentFolder(entity) {
|
|
const dirname = this._getEntityDirname(entity)
|
|
if (dirname == null) {
|
|
return
|
|
}
|
|
return this.findEntityByPath(dirname)
|
|
}
|
|
|
|
loadRootFolder() {
|
|
return (this.$scope.rootFolder = this._parseFolder(
|
|
__guard__(
|
|
this.$scope != null ? this.$scope.project : undefined,
|
|
x => x.rootFolder[0]
|
|
)
|
|
))
|
|
}
|
|
|
|
_parseFolder(rawFolder) {
|
|
const folder = {
|
|
name: rawFolder.name,
|
|
id: rawFolder._id,
|
|
type: 'folder',
|
|
children: [],
|
|
selected: rawFolder._id === this.selected_entity_id,
|
|
}
|
|
|
|
for (const doc of Array.from(rawFolder.docs || [])) {
|
|
folder.children.push({
|
|
name: doc.name,
|
|
id: doc._id,
|
|
type: 'doc',
|
|
selected: doc._id === this.selected_entity_id,
|
|
})
|
|
}
|
|
|
|
for (const file of Array.from(rawFolder.fileRefs || [])) {
|
|
folder.children.push({
|
|
name: file.name,
|
|
id: file._id,
|
|
type: 'file',
|
|
selected: file._id === this.selected_entity_id,
|
|
linkedFileData: file.linkedFileData,
|
|
created: file.created,
|
|
})
|
|
}
|
|
|
|
for (const childFolder of Array.from(rawFolder.folders || [])) {
|
|
folder.children.push(this._parseFolder(childFolder))
|
|
}
|
|
|
|
return folder
|
|
}
|
|
|
|
loadDeletedDocs() {
|
|
this.$scope.deletedDocs = []
|
|
return Array.from(this.$scope.project.deletedDocs || []).map(doc =>
|
|
this.$scope.deletedDocs.push({
|
|
name: doc.name,
|
|
id: doc._id,
|
|
type: 'doc',
|
|
deleted: true,
|
|
})
|
|
)
|
|
}
|
|
|
|
recalculateDocList() {
|
|
this.$scope.docs = []
|
|
this.forEachEntity((entity, parentFolder, path) => {
|
|
if (entity.type === 'doc' && !entity.deleted) {
|
|
return this.$scope.docs.push({
|
|
doc: entity,
|
|
path,
|
|
})
|
|
}
|
|
})
|
|
// Keep list ordered by folders, then name
|
|
return this.$scope.docs.sort(function (a, b) {
|
|
const aDepth = (a.path.match(/\//g) || []).length
|
|
const bDepth = (b.path.match(/\//g) || []).length
|
|
if (aDepth - bDepth !== 0) {
|
|
return -(aDepth - bDepth) // Deeper path == folder first
|
|
} else if (a.path < b.path) {
|
|
return -1
|
|
} else if (a.path > b.path) {
|
|
return 1
|
|
} else {
|
|
return 0
|
|
}
|
|
})
|
|
}
|
|
|
|
getCurrentFolder() {
|
|
// Return the root folder if nothing is selected
|
|
return (
|
|
this._getCurrentFolder(this.$scope.rootFolder) || this.$scope.rootFolder
|
|
)
|
|
}
|
|
|
|
_getCurrentFolder(startFolder) {
|
|
if (startFolder == null) {
|
|
startFolder = this.$scope.rootFolder
|
|
}
|
|
for (const entity of Array.from(startFolder.children || [])) {
|
|
// The 'current' folder is either the one selected, or
|
|
// the one containing the selected doc/file
|
|
if (entity.selected) {
|
|
if (entity.type === 'folder') {
|
|
return entity
|
|
} else {
|
|
return startFolder
|
|
}
|
|
}
|
|
|
|
if (entity.type === 'folder') {
|
|
const result = this._getCurrentFolder(entity)
|
|
if (result != null) {
|
|
return result
|
|
}
|
|
}
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
projectContainsFolder() {
|
|
for (const entity of Array.from(this.$scope.rootFolder.children)) {
|
|
if (entity.type === 'folder') {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
existsInThisFolder(folder, name) {
|
|
for (const entity of Array.from(
|
|
(folder != null ? folder.children : undefined) || []
|
|
)) {
|
|
if (entity.name === name) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
nameExistsError(message) {
|
|
if (message == null) {
|
|
message = 'already exists'
|
|
}
|
|
const nameExists = this.ide.$q.defer()
|
|
nameExists.reject({ data: message })
|
|
return nameExists.promise
|
|
}
|
|
|
|
createDoc(name, parent_folder) {
|
|
// check if a doc/file/folder already exists with this name
|
|
if (parent_folder == null) {
|
|
parent_folder = this.getCurrentFolder()
|
|
}
|
|
if (this.existsInThisFolder(parent_folder, name)) {
|
|
return this.nameExistsError()
|
|
}
|
|
// We'll wait for the socket.io notification to actually
|
|
// add the doc for us.
|
|
return this.ide.$http.post(`/project/${this.ide.project_id}/doc`, {
|
|
name,
|
|
parent_folder_id: parent_folder != null ? parent_folder.id : undefined,
|
|
_csrf: window.csrfToken,
|
|
})
|
|
}
|
|
|
|
createFolder(name, parent_folder) {
|
|
// check if a doc/file/folder already exists with this name
|
|
if (parent_folder == null) {
|
|
parent_folder = this.getCurrentFolder()
|
|
}
|
|
if (this.existsInThisFolder(parent_folder, name)) {
|
|
return this.nameExistsError()
|
|
}
|
|
// We'll wait for the socket.io notification to actually
|
|
// add the folder for us.
|
|
return this.ide.$http.post(`/project/${this.ide.project_id}/folder`, {
|
|
name,
|
|
parent_folder_id: parent_folder != null ? parent_folder.id : undefined,
|
|
_csrf: window.csrfToken,
|
|
})
|
|
}
|
|
|
|
createLinkedFile(name, parent_folder, provider, data) {
|
|
// check if a doc/file/folder already exists with this name
|
|
if (parent_folder == null) {
|
|
parent_folder = this.getCurrentFolder()
|
|
}
|
|
if (this.existsInThisFolder(parent_folder, name)) {
|
|
return this.nameExistsError()
|
|
}
|
|
// We'll wait for the socket.io notification to actually
|
|
// add the file for us.
|
|
return this.ide.$http.post(
|
|
`/project/${this.ide.project_id}/linked_file`,
|
|
{
|
|
name,
|
|
parent_folder_id: parent_folder != null ? parent_folder.id : undefined,
|
|
provider,
|
|
data,
|
|
_csrf: window.csrfToken,
|
|
},
|
|
{
|
|
disableAutoLoginRedirect: true,
|
|
}
|
|
)
|
|
}
|
|
|
|
refreshLinkedFile(file) {
|
|
const parent_folder = this._findParentFolder(file)
|
|
const provider =
|
|
file.linkedFileData != null ? file.linkedFileData.provider : undefined
|
|
if (provider == null) {
|
|
console.warn(`>> no provider for ${file.name}`, file)
|
|
return
|
|
}
|
|
return this.ide.$http.post(
|
|
`/project/${this.ide.project_id}/linked_file/${file.id}/refresh`,
|
|
{
|
|
_csrf: window.csrfToken,
|
|
},
|
|
{
|
|
disableAutoLoginRedirect: true,
|
|
}
|
|
)
|
|
}
|
|
|
|
renameEntity(entity, name, callback) {
|
|
if (callback == null) {
|
|
callback = function (error) {}
|
|
}
|
|
if (entity.name === name) {
|
|
return
|
|
}
|
|
if (name.length >= 150) {
|
|
return
|
|
}
|
|
// check if a doc/file/folder already exists with this name
|
|
const parent_folder = this.getCurrentFolder()
|
|
if (this.existsInThisFolder(parent_folder, name)) {
|
|
return this.nameExistsError()
|
|
}
|
|
entity.renamingToName = name
|
|
return this.ide.$http
|
|
.post(
|
|
`/project/${this.ide.project_id}/${entity.type}/${entity.id}/rename`,
|
|
{
|
|
name,
|
|
_csrf: window.csrfToken,
|
|
}
|
|
)
|
|
.then(() => (entity.name = name))
|
|
.finally(() => (entity.renamingToName = null))
|
|
}
|
|
|
|
deleteEntity(entity, callback) {
|
|
// We'll wait for the socket.io notification to
|
|
// delete from scope.
|
|
if (callback == null) {
|
|
callback = function (error) {}
|
|
}
|
|
return this.ide.queuedHttp({
|
|
method: 'DELETE',
|
|
url: `/project/${this.ide.project_id}/${entity.type}/${entity.id}`,
|
|
headers: {
|
|
'X-Csrf-Token': window.csrfToken,
|
|
},
|
|
})
|
|
}
|
|
|
|
moveEntity(entity, parent_folder) {
|
|
// Abort move if the folder being moved (entity) has the parent_folder as child
|
|
// since that would break the tree structure.
|
|
if (this._isChildFolder(entity, parent_folder)) {
|
|
return
|
|
}
|
|
// check if a doc/file/folder already exists with this name
|
|
if (this.existsInThisFolder(parent_folder, entity.name)) {
|
|
throw new Error('file exists in this location')
|
|
}
|
|
// Wait for the http response before doing the move
|
|
this.ide.queuedHttp
|
|
.post(
|
|
`/project/${this.ide.project_id}/${entity.type}/${entity.id}/move`,
|
|
{
|
|
folder_id: parent_folder.id,
|
|
_csrf: window.csrfToken,
|
|
}
|
|
)
|
|
.then(() => {
|
|
this._moveEntityInScope(entity, parent_folder)
|
|
})
|
|
}
|
|
|
|
_isChildFolder(parent_folder, child_folder) {
|
|
const parent_path = this.getEntityPath(parent_folder) || '' // null if root folder
|
|
const child_path = this.getEntityPath(child_folder) || '' // null if root folder
|
|
// is parent path the beginning of child path?
|
|
return child_path.slice(0, parent_path.length) === parent_path
|
|
}
|
|
|
|
_deleteEntityFromScope(entity, options) {
|
|
if (options == null) {
|
|
options = { moveToDeleted: true }
|
|
}
|
|
if (entity == null) {
|
|
return
|
|
}
|
|
let parent_folder = null
|
|
this.forEachEntity(function (possible_entity, folder) {
|
|
if (possible_entity === entity) {
|
|
return (parent_folder = folder)
|
|
}
|
|
})
|
|
|
|
if (parent_folder != null) {
|
|
const index = parent_folder.children.indexOf(entity)
|
|
if (index > -1) {
|
|
parent_folder.children.splice(index, 1)
|
|
}
|
|
}
|
|
|
|
if (entity.type !== 'folder' && entity.selected) {
|
|
this.$scope.ui.view = null
|
|
}
|
|
|
|
if (entity.type === 'doc' && options.moveToDeleted) {
|
|
entity.deleted = true
|
|
return this.$scope.deletedDocs.push(entity)
|
|
}
|
|
}
|
|
|
|
_moveEntityInScope(entity, parent_folder) {
|
|
if (Array.from(parent_folder.children).includes(entity)) {
|
|
return
|
|
}
|
|
this._deleteEntityFromScope(entity, { moveToDeleted: false })
|
|
return parent_folder.children.push(entity)
|
|
}
|
|
}
|
|
|
|
function __guard__(value, transform) {
|
|
return typeof value !== 'undefined' && value !== null
|
|
? transform(value)
|
|
: undefined
|
|
}
|