mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Remove Angular (#17534)
GitOrigin-RevId: 7a0d45e17d9905fa75569e2d19ca59caa4a41565
This commit is contained in:
parent
b3d33fe813
commit
c24ace801b
149 changed files with 665 additions and 21004 deletions
224
package-lock.json
generated
224
package-lock.json
generated
|
@ -12243,12 +12243,6 @@
|
|||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/angular": {
|
||||
"version": "1.8.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/angular/-/angular-1.8.4.tgz",
|
||||
"integrity": "sha512-wPS/ncJWhyxJsndsW1B6Ta8D4mi97x1yItSu+rkLDytU3oRIh2CFAjMuJceYwFAh9+DIohndWM0QBA9OU2Hv0g==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/aria-query": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-4.2.2.tgz",
|
||||
|
@ -12691,15 +12685,6 @@
|
|||
"integrity": "sha512-0d5Wd09ItQWH1qFbEyQ7oTQ3GZrMfth5JkbN3EvTKLXcHLRDSXeLnlvlOn0wvxVIwK5o2M8JzP/OWz7T3NRsbw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/lodash.frompairs": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash.frompairs/-/lodash.frompairs-4.0.6.tgz",
|
||||
"integrity": "sha512-rwCUf4NMKhXpiVjL/RXP8YOk+rd02/J4tACADEgaMXRVnzDbSSlBMKFZoX/ARmHVLg3Qc98Um4PErGv8FbxU7w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/lodash": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/long": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz",
|
||||
|
@ -14733,25 +14718,6 @@
|
|||
"integrity": "sha512-VJK1SRmXBpjwsB4YOHYSturx48rLKMzHgCqDH2ZDa6ZbMS/N5huoNqyQdK5Fj/xayu3fqbXckn5SeCS1EbMDZg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/angular": {
|
||||
"version": "1.8.3",
|
||||
"resolved": "https://registry.npmjs.org/angular/-/angular-1.8.3.tgz",
|
||||
"integrity": "sha512-5qjkWIQQVsHj4Sb5TcEs4WZWpFeVFHXwxEBHUhrny41D8UrBAd6T/6nPPAsLngJCReIOqi95W3mxdveveutpZw==",
|
||||
"deprecated": "For the actively supported Angular, see https://www.npmjs.com/package/@angular/core. AngularJS support has officially ended. For extended AngularJS support options, see https://goo.gle/angularjs-path-forward.",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/angular-mocks": {
|
||||
"version": "1.8.2",
|
||||
"resolved": "https://registry.npmjs.org/angular-mocks/-/angular-mocks-1.8.2.tgz",
|
||||
"integrity": "sha512-I5L3P0l21HPdVsP4A4qWmENt4ePjjbkDFdAzOaM7QiibFySbt14DptPbt2IjeG4vFBr4vSLbhIz8Fk03DISl8Q==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/angular-sanitize": {
|
||||
"version": "1.8.2",
|
||||
"resolved": "https://registry.npmjs.org/angular-sanitize/-/angular-sanitize-1.8.2.tgz",
|
||||
"integrity": "sha512-OB6Goa+QN3byf5asQ7XRl7DKZejm/F/ZOqa9z1skqYVOWA2hoBxoCmt9E7+i7T/TbxZP5zYzKxNZVVJNu860Hg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/ansi-color": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-color/-/ansi-color-0.2.1.tgz",
|
||||
|
@ -15617,17 +15583,6 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-plugin-angularjs-annotate": {
|
||||
"version": "0.10.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-angularjs-annotate/-/babel-plugin-angularjs-annotate-0.10.0.tgz",
|
||||
"integrity": "sha512-NPE7FOAxcLPCUR/kNkrhHIjoScR3RyIlRH3yRn79j8EZWtpILVnCOdA9yKfsOmRh6BHnLHKl8ZAThc+YDd/QwQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.0.0",
|
||||
"@babel/types": "^7.2.0",
|
||||
"simple-is": "~0.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-plugin-macros": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz",
|
||||
|
@ -27997,12 +27952,6 @@
|
|||
"lodash.keys": "~2.4.1"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash.frompairs": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.frompairs/-/lodash.frompairs-4.0.1.tgz",
|
||||
"integrity": "sha1-vE5SB/onV8E25XNhTpZkUGsrG9I=",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/lodash.get": {
|
||||
"version": "4.4.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
|
||||
|
@ -30011,18 +29960,6 @@
|
|||
"resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz",
|
||||
"integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ=="
|
||||
},
|
||||
"node_modules/ngcomponent": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ngcomponent/-/ngcomponent-4.1.0.tgz",
|
||||
"integrity": "sha512-cGL3iVoqMWTpCfaIwgRKhdaGqiy2Z+CCG0cVfjlBvdqE8saj8xap9B4OTf+qwObxLVZmDTJPDgx3bN6Q/lZ7BQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/angular": "^1.6.39",
|
||||
"@types/lodash": "^4.14.85",
|
||||
"angular": ">=1.5.0",
|
||||
"lodash": "^4.17.4"
|
||||
}
|
||||
},
|
||||
"node_modules/nise": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/nise/-/nise-4.1.0.tgz",
|
||||
|
@ -34807,52 +34744,6 @@
|
|||
"react-dom": ">=15.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react2angular": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/react2angular/-/react2angular-4.0.6.tgz",
|
||||
"integrity": "sha512-MDl2WRoTyu7Gyh4+FAIlmsM2mxIa/DjSz6G/d90L1tK8ZRubqVEayKF6IPyAruC5DMhGDVJ7tlAIcu/gMNDjXg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/lodash.frompairs": "^4.0.5",
|
||||
"angular": ">=1.5",
|
||||
"lodash.frompairs": "^4.0.1",
|
||||
"ngcomponent": "^4.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/angular": ">=1.5",
|
||||
"@types/prop-types": ">=15",
|
||||
"@types/react": ">=16",
|
||||
"@types/react-dom": ">=16",
|
||||
"prop-types": ">=15",
|
||||
"react": ">=15",
|
||||
"react-dom": ">=15"
|
||||
}
|
||||
},
|
||||
"node_modules/react2angular-shared-context": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/react2angular-shared-context/-/react2angular-shared-context-1.1.2.tgz",
|
||||
"integrity": "sha512-0zrxBjmBs+et5zYNknx/jvrJCzGz6KbF8BHfzXHTl9ms6iMsbmmXkZiQQksVT1Og5wnkmVq9nlLVfWYJLSXF0w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.0.0 || ^17",
|
||||
"react-dom": "^16.0.0 || ^17"
|
||||
}
|
||||
},
|
||||
"node_modules/react2angular-shared-context/node_modules/uuid": {
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/reactcss": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz",
|
||||
|
@ -36673,12 +36564,6 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/simple-is": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/simple-is/-/simple-is-0.2.0.tgz",
|
||||
"integrity": "sha1-Krt1qt453rXMgVzhDmGRFkhQuvA=",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/simple-oauth2": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/simple-oauth2/-/simple-oauth2-5.0.0.tgz",
|
||||
|
@ -44616,12 +44501,8 @@
|
|||
"acorn": "^7.1.1",
|
||||
"acorn-walk": "^7.1.1",
|
||||
"algoliasearch": "^3.35.1",
|
||||
"angular": "~1.8.0",
|
||||
"angular-mocks": "~1.8.0",
|
||||
"angular-sanitize": "~1.8.0",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"babel-loader": "^9.1.2",
|
||||
"babel-plugin-angularjs-annotate": "^0.10.0",
|
||||
"babel-plugin-macros": "^3.1.0",
|
||||
"babel-plugin-module-resolver": "^5.0.0",
|
||||
"backbone": "^1.3.3",
|
||||
|
@ -44698,8 +44579,6 @@
|
|||
"react-linkify": "^1.0.0-alpha",
|
||||
"react-refresh": "^0.14.0",
|
||||
"react-resizable-panels": "^1.0.3",
|
||||
"react2angular": "^4.0.6",
|
||||
"react2angular-shared-context": "^1.1.0",
|
||||
"requirejs": "^2.3.6",
|
||||
"resolve-url-loader": "^5.0.0",
|
||||
"samlp": "^7.0.2",
|
||||
|
@ -53003,14 +52882,10 @@
|
|||
"acorn": "^7.1.1",
|
||||
"acorn-walk": "^7.1.1",
|
||||
"algoliasearch": "^3.35.1",
|
||||
"angular": "~1.8.0",
|
||||
"angular-mocks": "~1.8.0",
|
||||
"angular-sanitize": "~1.8.0",
|
||||
"archiver": "^5.3.0",
|
||||
"async": "3.2.2",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"babel-loader": "^9.1.2",
|
||||
"babel-plugin-angularjs-annotate": "^0.10.0",
|
||||
"babel-plugin-macros": "^3.1.0",
|
||||
"babel-plugin-module-resolver": "^5.0.0",
|
||||
"backbone": "^1.3.3",
|
||||
|
@ -53152,8 +53027,6 @@
|
|||
"react-linkify": "^1.0.0-alpha",
|
||||
"react-refresh": "^0.14.0",
|
||||
"react-resizable-panels": "^1.0.3",
|
||||
"react2angular": "^4.0.6",
|
||||
"react2angular-shared-context": "^1.1.0",
|
||||
"recurly": "^4.0.0",
|
||||
"referer-parser": "github:overleaf/nodejs-referer-parser#8b8b103762d05b7be4cfa2f810e1d408be67d7bb",
|
||||
"request": "^2.88.2",
|
||||
|
@ -57040,12 +56913,6 @@
|
|||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/angular": {
|
||||
"version": "1.8.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/angular/-/angular-1.8.4.tgz",
|
||||
"integrity": "sha512-wPS/ncJWhyxJsndsW1B6Ta8D4mi97x1yItSu+rkLDytU3oRIh2CFAjMuJceYwFAh9+DIohndWM0QBA9OU2Hv0g==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/aria-query": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-4.2.2.tgz",
|
||||
|
@ -57487,15 +57354,6 @@
|
|||
"integrity": "sha512-0d5Wd09ItQWH1qFbEyQ7oTQ3GZrMfth5JkbN3EvTKLXcHLRDSXeLnlvlOn0wvxVIwK5o2M8JzP/OWz7T3NRsbw==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/lodash.frompairs": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash.frompairs/-/lodash.frompairs-4.0.6.tgz",
|
||||
"integrity": "sha512-rwCUf4NMKhXpiVjL/RXP8YOk+rd02/J4tACADEgaMXRVnzDbSSlBMKFZoX/ARmHVLg3Qc98Um4PErGv8FbxU7w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/lodash": "*"
|
||||
}
|
||||
},
|
||||
"@types/long": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz",
|
||||
|
@ -59081,24 +58939,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"angular": {
|
||||
"version": "1.8.3",
|
||||
"resolved": "https://registry.npmjs.org/angular/-/angular-1.8.3.tgz",
|
||||
"integrity": "sha512-5qjkWIQQVsHj4Sb5TcEs4WZWpFeVFHXwxEBHUhrny41D8UrBAd6T/6nPPAsLngJCReIOqi95W3mxdveveutpZw==",
|
||||
"dev": true
|
||||
},
|
||||
"angular-mocks": {
|
||||
"version": "1.8.2",
|
||||
"resolved": "https://registry.npmjs.org/angular-mocks/-/angular-mocks-1.8.2.tgz",
|
||||
"integrity": "sha512-I5L3P0l21HPdVsP4A4qWmENt4ePjjbkDFdAzOaM7QiibFySbt14DptPbt2IjeG4vFBr4vSLbhIz8Fk03DISl8Q==",
|
||||
"dev": true
|
||||
},
|
||||
"angular-sanitize": {
|
||||
"version": "1.8.2",
|
||||
"resolved": "https://registry.npmjs.org/angular-sanitize/-/angular-sanitize-1.8.2.tgz",
|
||||
"integrity": "sha512-OB6Goa+QN3byf5asQ7XRl7DKZejm/F/ZOqa9z1skqYVOWA2hoBxoCmt9E7+i7T/TbxZP5zYzKxNZVVJNu860Hg==",
|
||||
"dev": true
|
||||
},
|
||||
"ansi-color": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-color/-/ansi-color-0.2.1.tgz",
|
||||
|
@ -59740,17 +59580,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"babel-plugin-angularjs-annotate": {
|
||||
"version": "0.10.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-angularjs-annotate/-/babel-plugin-angularjs-annotate-0.10.0.tgz",
|
||||
"integrity": "sha512-NPE7FOAxcLPCUR/kNkrhHIjoScR3RyIlRH3yRn79j8EZWtpILVnCOdA9yKfsOmRh6BHnLHKl8ZAThc+YDd/QwQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/code-frame": "^7.0.0",
|
||||
"@babel/types": "^7.2.0",
|
||||
"simple-is": "~0.2.0"
|
||||
}
|
||||
},
|
||||
"babel-plugin-macros": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz",
|
||||
|
@ -70105,12 +69934,6 @@
|
|||
"lodash.keys": "~2.4.1"
|
||||
}
|
||||
},
|
||||
"lodash.frompairs": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.frompairs/-/lodash.frompairs-4.0.1.tgz",
|
||||
"integrity": "sha1-vE5SB/onV8E25XNhTpZkUGsrG9I=",
|
||||
"dev": true
|
||||
},
|
||||
"lodash.get": {
|
||||
"version": "4.4.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
|
||||
|
@ -71596,18 +71419,6 @@
|
|||
"resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz",
|
||||
"integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ=="
|
||||
},
|
||||
"ngcomponent": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ngcomponent/-/ngcomponent-4.1.0.tgz",
|
||||
"integrity": "sha512-cGL3iVoqMWTpCfaIwgRKhdaGqiy2Z+CCG0cVfjlBvdqE8saj8xap9B4OTf+qwObxLVZmDTJPDgx3bN6Q/lZ7BQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/angular": "^1.6.39",
|
||||
"@types/lodash": "^4.14.85",
|
||||
"angular": ">=1.5.0",
|
||||
"lodash": "^4.17.4"
|
||||
}
|
||||
},
|
||||
"nise": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/nise/-/nise-4.1.0.tgz",
|
||||
|
@ -75149,35 +74960,6 @@
|
|||
"react-lifecycles-compat": "^3.0.4"
|
||||
}
|
||||
},
|
||||
"react2angular": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/react2angular/-/react2angular-4.0.6.tgz",
|
||||
"integrity": "sha512-MDl2WRoTyu7Gyh4+FAIlmsM2mxIa/DjSz6G/d90L1tK8ZRubqVEayKF6IPyAruC5DMhGDVJ7tlAIcu/gMNDjXg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/lodash.frompairs": "^4.0.5",
|
||||
"angular": ">=1.5",
|
||||
"lodash.frompairs": "^4.0.1",
|
||||
"ngcomponent": "^4.1.0"
|
||||
}
|
||||
},
|
||||
"react2angular-shared-context": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/react2angular-shared-context/-/react2angular-shared-context-1.1.2.tgz",
|
||||
"integrity": "sha512-0zrxBjmBs+et5zYNknx/jvrJCzGz6KbF8BHfzXHTl9ms6iMsbmmXkZiQQksVT1Og5wnkmVq9nlLVfWYJLSXF0w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"uuid": {
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"reactcss": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz",
|
||||
|
@ -76578,12 +76360,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"simple-is": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/simple-is/-/simple-is-0.2.0.tgz",
|
||||
"integrity": "sha1-Krt1qt453rXMgVzhDmGRFkhQuvA=",
|
||||
"dev": true
|
||||
},
|
||||
"simple-oauth2": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/simple-oauth2/-/simple-oauth2-5.0.0.tgz",
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
diff --git a/node_modules/ngcomponent/index.ts b/node_modules/ngcomponent/index.ts
|
||||
index 5fe33c5..8e1c6fc 100644
|
||||
--- a/node_modules/ngcomponent/index.ts
|
||||
+++ b/node_modules/ngcomponent/index.ts
|
||||
@@ -1,3 +1,4 @@
|
||||
+// @ts-nocheck
|
||||
import { IChangesObject } from 'angular'
|
||||
import assign = require('lodash/assign')
|
||||
import mapValues = require('lodash/mapValues')
|
|
@ -1,9 +0,0 @@
|
|||
diff --git a/node_modules/react2angular/index.tsx b/node_modules/react2angular/index.tsx
|
||||
index 5cee831..a07e040 100644
|
||||
--- a/node_modules/react2angular/index.tsx
|
||||
+++ b/node_modules/react2angular/index.tsx
|
||||
@@ -1,3 +1,4 @@
|
||||
+// @ts-nocheck
|
||||
import { IAugmentedJQuery, IComponentOptions } from 'angular'
|
||||
import fromPairs = require('lodash.frompairs')
|
||||
import NgComponent from 'ngcomponent'
|
|
@ -603,15 +603,10 @@ const _ProjectController = {
|
|||
!showPersonalAccessToken &&
|
||||
splitTestAssignments['personal-access-token'].variant === 'enabled' // `?personal-access-token=enabled`
|
||||
|
||||
const idePageReact = splitTestAssignments['ide-page'].variant === 'react'
|
||||
|
||||
const template =
|
||||
detachRole === 'detached'
|
||||
? // TODO: Create React version of detached page
|
||||
'project/editor_detached'
|
||||
: idePageReact
|
||||
? 'project/ide-react'
|
||||
: 'project/editor'
|
||||
? 'project/ide-react-detached'
|
||||
: 'project/ide-react'
|
||||
|
||||
res.render(template, {
|
||||
title: project.name,
|
||||
|
@ -681,7 +676,6 @@ const _ProjectController = {
|
|||
showUpgradePrompt,
|
||||
fixedSizeDocument: true,
|
||||
useOpenTelemetry: Settings.useOpenTelemetryClient,
|
||||
idePageReact,
|
||||
showPersonalAccessToken,
|
||||
optionalPersonalAccessToken,
|
||||
hasTrackChangesFeature: Features.hasFeature('track-changes'),
|
||||
|
|
|
@ -12,7 +12,6 @@ const {
|
|||
handleAdminDomainRedirect,
|
||||
} = require('../Authorization/AuthorizationMiddleware')
|
||||
const ProjectAuditLogHandler = require('../Project/ProjectAuditLogHandler')
|
||||
const SplitTestHandler = require('../SplitTests/SplitTestHandler')
|
||||
|
||||
const orderedPrivilegeLevels = [
|
||||
PrivilegeLevels.NONE,
|
||||
|
@ -98,18 +97,7 @@ async function tokenAccessPage(req, res, next) {
|
|||
}
|
||||
}
|
||||
|
||||
const { variant } = await SplitTestHandler.promises.getAssignment(
|
||||
req,
|
||||
res,
|
||||
'token-access-page'
|
||||
)
|
||||
|
||||
const view =
|
||||
variant === 'react'
|
||||
? 'project/token/access-react'
|
||||
: 'project/token/access'
|
||||
|
||||
res.render(view, {
|
||||
res.render('project/token/access-react', {
|
||||
postUrl: makePostUrl(token),
|
||||
})
|
||||
} catch (err) {
|
||||
|
|
|
@ -72,7 +72,7 @@ html(
|
|||
|
||||
block head-scripts
|
||||
|
||||
body(ng-csp=(cspEnabled ? "no-unsafe-eval" : false) class=(showThinFooter ? 'thin-footer' : undefined))
|
||||
body(class=(showThinFooter ? 'thin-footer' : undefined))
|
||||
if(settings.recaptcha && settings.recaptcha.siteKeyV3)
|
||||
script(type="text/javascript", nonce=scriptNonce, src="https://www.recaptcha.net/recaptcha/api.js?render="+settings.recaptcha.siteKeyV3, defer=deferScripts)
|
||||
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
extends ./layout-base
|
||||
|
||||
block entrypointVar
|
||||
- entrypoint = 'main'
|
||||
|
||||
block body
|
||||
if (typeof(suppressNavbar) == "undefined")
|
||||
include layout/navbar
|
||||
|
||||
block content
|
||||
|
||||
if (typeof(suppressFooter) == "undefined")
|
||||
if showThinFooter
|
||||
include layout/footer
|
||||
else
|
||||
include layout/fat-footer
|
||||
|
||||
if (typeof(suppressCookieBanner) == 'undefined')
|
||||
include _cookie_banner
|
||||
|
||||
!= moduleIncludes("contactModal", locals)
|
|
@ -1,42 +0,0 @@
|
|||
|
||||
|
||||
footer.site-footer
|
||||
- var showLanguagePicker = Object.keys(settings.i18n.subdomainLang).length > 1
|
||||
- var hasCustomLeftNav = nav.left_footer && nav.left_footer.length > 0
|
||||
.site-footer-content.hidden-print
|
||||
.row
|
||||
ul.col-md-9
|
||||
if hasFeature('saas')
|
||||
li © #{new Date().getFullYear()} Overleaf
|
||||
else if !settings.nav.hide_powered_by
|
||||
li
|
||||
//- year of Server Pro release, static
|
||||
| © 2024
|
||||
|
|
||||
a(href='https://www.overleaf.com/for/enterprises') Powered by Overleaf
|
||||
|
||||
if showLanguagePicker || hasCustomLeftNav
|
||||
li
|
||||
strong.text-muted |
|
||||
|
||||
if showLanguagePicker
|
||||
include language-picker
|
||||
|
||||
if showLanguagePicker && hasCustomLeftNav
|
||||
li
|
||||
strong.text-muted |
|
||||
|
||||
each item in nav.left_footer
|
||||
li
|
||||
if item.url
|
||||
a(href=item.url, class=item.class) !{translate(item.text)}
|
||||
else
|
||||
| !{item.text}
|
||||
|
||||
ul.col-md-3.text-right
|
||||
each item in nav.right_footer
|
||||
li
|
||||
if item.url
|
||||
a(href=item.url, class=item.class, aria-label=item.label) !{item.text}
|
||||
else
|
||||
| !{item.text}
|
|
@ -1,147 +0,0 @@
|
|||
nav.navbar.navbar-default.navbar-main
|
||||
.container-fluid
|
||||
.navbar-header
|
||||
if (typeof(suppressNavbarRight) == "undefined")
|
||||
button.navbar-toggle(ng-init="navCollapsed = true", ng-click="navCollapsed = !navCollapsed", ng-class="{active: !navCollapsed}", aria-label="Toggle " + translate('navigation'))
|
||||
i.fa.fa-bars(aria-hidden="true")
|
||||
if settings.nav.custom_logo
|
||||
a(href='/', aria-label=settings.appName, style='background-image:url("'+settings.nav.custom_logo+'")').navbar-brand
|
||||
else if (nav.title)
|
||||
a(href='/', aria-label=settings.appName, ng-non-bindable).navbar-title #{nav.title}
|
||||
else
|
||||
a(href='/', aria-label=settings.appName).navbar-brand
|
||||
|
||||
- var canDisplayAdminMenu = hasAdminAccess()
|
||||
- var canDisplayAdminRedirect = canRedirectToAdminDomain()
|
||||
- var canDisplaySplitTestMenu = hasFeature('saas') && (canDisplayAdminMenu || (getSessionUser() && getSessionUser().staffAccess && (getSessionUser().staffAccess.splitTestMetrics || getSessionUser().staffAccess.splitTestManagement)))
|
||||
- var canDisplaySurveyMenu = hasFeature('saas') && canDisplayAdminMenu
|
||||
|
||||
if (typeof(suppressNavbarRight) == "undefined")
|
||||
.navbar-collapse.collapse(collapse="navCollapsed")
|
||||
ul.nav.navbar-nav.navbar-right
|
||||
if (canDisplayAdminMenu || canDisplayAdminRedirect || canDisplaySplitTestMenu)
|
||||
li.dropdown(class="subdued", dropdown)
|
||||
a.dropdown-toggle(href, dropdown-toggle)
|
||||
| Admin
|
||||
b.caret
|
||||
ul.dropdown-menu
|
||||
if canDisplayAdminMenu
|
||||
li
|
||||
a(href="/admin") Manage Site
|
||||
li
|
||||
a(href="/admin/user") Manage Users
|
||||
li
|
||||
a(href="/admin/project") Project URL Lookup
|
||||
li
|
||||
a(href="/admin/saml/logs") SAML logs
|
||||
if canDisplayAdminRedirect
|
||||
li
|
||||
a(href=settings.adminUrl) Switch to Admin
|
||||
if canDisplaySplitTestMenu
|
||||
li
|
||||
a(href="/admin/split-test") Manage Feature Flags
|
||||
if canDisplaySurveyMenu
|
||||
li
|
||||
a(href="/admin/survey") Manage Surveys
|
||||
|
||||
// loop over header_extras
|
||||
each item in nav.header_extras
|
||||
-
|
||||
if ((item.only_when_logged_in && getSessionUser())
|
||||
|| (item.only_when_logged_out && (!getSessionUser()))
|
||||
|| (!item.only_when_logged_out && !item.only_when_logged_in && !item.only_content_pages)
|
||||
|| (item.only_content_pages && (typeof(suppressNavContentLinks) == "undefined" || !suppressNavContentLinks))
|
||||
){
|
||||
var showNavItem = true
|
||||
} else {
|
||||
var showNavItem = false
|
||||
}
|
||||
|
||||
if showNavItem
|
||||
if item.dropdown
|
||||
li.dropdown(class=item.class, dropdown)
|
||||
a.dropdown-toggle(href, dropdown-toggle)
|
||||
| !{translate(item.text)}
|
||||
b.caret
|
||||
ul.dropdown-menu
|
||||
each child in item.dropdown
|
||||
if child.divider
|
||||
li.divider
|
||||
else if child.isContactUs
|
||||
li
|
||||
a(ng-controller="ContactModal" ng-click="contactUsModal()" href)
|
||||
span(event-tracking="menu-clicked-contact" event-tracking-mb="true" event-tracking-trigger="click")
|
||||
| #{translate("contact_us")}
|
||||
else
|
||||
li
|
||||
if child.url
|
||||
a(
|
||||
href=child.url,
|
||||
class=child.class,
|
||||
event-tracking=child.event
|
||||
event-tracking-mb="true"
|
||||
event-tracking-trigger="click"
|
||||
event-segmentation=child.eventSegmentation
|
||||
) !{translate(child.text)}
|
||||
else
|
||||
| !{translate(child.text)}
|
||||
else
|
||||
li(class=item.class)
|
||||
if item.url
|
||||
a(
|
||||
href=item.url,
|
||||
class=item.class,
|
||||
event-tracking=item.event
|
||||
event-tracking-mb="true"
|
||||
event-tracking-trigger="click"
|
||||
) !{translate(item.text)}
|
||||
else
|
||||
| !{translate(item.text)}
|
||||
|
||||
// logged out
|
||||
if !getSessionUser()
|
||||
// register link
|
||||
if hasFeature('registration-page')
|
||||
li
|
||||
a(
|
||||
href="/register"
|
||||
event-tracking="menu-clicked-register"
|
||||
event-tracking-action="clicked"
|
||||
event-tracking-trigger="click"
|
||||
event-tracking-mb="true"
|
||||
event-segmentation={ page: currentUrl }
|
||||
) #{translate('register')}
|
||||
|
||||
// login link
|
||||
li
|
||||
a(
|
||||
href="/login"
|
||||
event-tracking="menu-clicked-login"
|
||||
event-tracking-action="clicked"
|
||||
event-tracking-trigger="click"
|
||||
event-tracking-mb="true"
|
||||
event-segmentation={ page: currentUrl }
|
||||
).text-capitalize #{translate('log_in')}
|
||||
|
||||
// projects link and account menu
|
||||
if getSessionUser()
|
||||
li
|
||||
a(href="/project") #{translate('Projects')}
|
||||
li.dropdown(dropdown)
|
||||
a.dropdown-toggle(href, dropdown-toggle)
|
||||
| #{translate('Account')}
|
||||
b.caret
|
||||
ul.dropdown-menu
|
||||
li
|
||||
div.subdued {{ usersEmail }}
|
||||
li.divider.hidden-xs.hidden-sm
|
||||
li
|
||||
a(href="/user/settings") #{translate('Account Settings')}
|
||||
if nav.showSubscriptionLink
|
||||
li
|
||||
a(href="/user/subscription") #{translate('subscription')}
|
||||
li.divider.hidden-xs.hidden-sm
|
||||
li
|
||||
form(method="POST" action="/logout")
|
||||
input(name='_csrf', type='hidden', value=csrfToken)
|
||||
button.btn-link.text-left.dropdown-menu-button #{translate('log_out')}
|
|
@ -1,124 +0,0 @@
|
|||
extends ../layout
|
||||
|
||||
block vars
|
||||
- var suppressNavbar = true
|
||||
- var suppressFooter = true
|
||||
- var suppressSkipToContent = true
|
||||
- var suppressCookieBanner = true
|
||||
- metadata.robotsNoindexNofollow = true
|
||||
|
||||
block entrypointVar
|
||||
- entrypoint = 'ide'
|
||||
|
||||
block content
|
||||
.editor(ng-controller="IdeController").full-size
|
||||
//- required by react2angular-shared-context, must be rendered as a top level component
|
||||
shared-context-react()
|
||||
.loading-screen(ng-if="state.loading")
|
||||
.loading-screen-brand-container
|
||||
.loading-screen-brand(
|
||||
style="height: 20%;"
|
||||
ng-style="{ 'height': state.load_progress + '%' }"
|
||||
)
|
||||
h3.loading-screen-label(ng-if="!state.error") #{translate("loading")}
|
||||
span.loading-screen-ellip .
|
||||
span.loading-screen-ellip .
|
||||
span.loading-screen-ellip .
|
||||
p.loading-screen-error(ng-if="state.error").ng-cloak
|
||||
span(ng-bind-html="state.error")
|
||||
|
||||
.global-alerts(ng-cloak ng-hide="editor.error_state")
|
||||
.alert.alert-danger.small(ng-if="connection.forced_disconnect")
|
||||
strong #{translate("disconnected")}
|
||||
|
||||
.alert.alert-warning.small(ng-if="connection.reconnection_countdown")
|
||||
strong #{translate("lost_connection")}.
|
||||
| #{translate("reconnecting_in_x_secs", {seconds:"{{ connection.reconnection_countdown }}"})}.
|
||||
a#try-reconnect-now-button.alert-link-as-btn.pull-right(href, ng-click="tryReconnectNow()") #{translate("try_now")}
|
||||
|
||||
.alert.alert-warning.small(ng-if="connection.reconnecting && connection.stillReconnecting")
|
||||
strong #{translate("reconnecting")}…
|
||||
|
||||
.alert.alert-warning.small(ng-if="sync_tex_error")
|
||||
strong #{translate("synctex_failed")}.
|
||||
a#synctex-more-info-button.alert-link-as-btn.pull-right(
|
||||
href="/learn/how-to/SyncTeX_Errors"
|
||||
target="_blank"
|
||||
) #{translate("more_info")}
|
||||
|
||||
.alert.alert-warning.small(ng-if="connection.inactive_disconnect")
|
||||
strong #{translate("editor_disconected_click_to_reconnect")}
|
||||
|
||||
.alert.alert-warning.small(ng-if="connection.debug") {{ connection.state }}
|
||||
|
||||
.div(ng-controller="SavingNotificationController")
|
||||
.alert.alert-warning.small(ng-repeat="(doc_id, state) in docSavingStatus" ng-if="state.unsavedSeconds > 8") #{translate("saving_notification_with_seconds", {docname:"{{ state.doc.name }}", seconds:"{{ state.unsavedSeconds }}"})}
|
||||
|
||||
.div(ng-controller="SystemMessagesController")
|
||||
.alert.alert-warning.system-message(
|
||||
ng-repeat="message in messages"
|
||||
ng-controller="SystemMessageController"
|
||||
ng-hide="hidden"
|
||||
)
|
||||
button(ng-hide="protected" ng-click="hide()").close.pull-right
|
||||
span(aria-hidden="true") ×
|
||||
span.sr-only #{translate("close")}
|
||||
.system-message-content
|
||||
| {{htmlContent}}
|
||||
|
||||
if hasFeature('saas')
|
||||
legacy-editor-warning(delay=10000)
|
||||
|
||||
include ./editor/main
|
||||
|
||||
script(type="text/ng-template" id="genericMessageModalTemplate")
|
||||
.modal-header
|
||||
button.close(
|
||||
type="button"
|
||||
data-dismiss="modal"
|
||||
ng-click="done()"
|
||||
aria-label="Close"
|
||||
)
|
||||
span(aria-hidden="true") ×
|
||||
h3 {{ title }}
|
||||
.modal-body(ng-bind-html="message")
|
||||
.modal-footer
|
||||
button.btn.btn-info(ng-click="done()") #{translate("ok")}
|
||||
|
||||
script(type="text/ng-template" id="outOfSyncModalTemplate")
|
||||
.modal-header
|
||||
button.close(
|
||||
type="button"
|
||||
data-dismiss="modal"
|
||||
ng-click="done()"
|
||||
aria-label="Close"
|
||||
)
|
||||
span(aria-hidden="true") ×
|
||||
h3 {{ title }}
|
||||
.modal-body(ng-bind-html="message")
|
||||
|
||||
.modal-body
|
||||
button.btn.btn-info(
|
||||
ng-init="showFileContents = false"
|
||||
ng-click="showFileContents = !showFileContents"
|
||||
)
|
||||
| {{showFileContents ? "Hide" : "Show"}} Local File Contents
|
||||
.text-preview(ng-show="showFileContents")
|
||||
textarea.scroll-container(readonly="readonly" rows="{{editorContentRows}}")
|
||||
| {{editorContent}}
|
||||
|
||||
.modal-footer
|
||||
button.btn.btn-info(ng-click="done()") #{translate("reload_editor")}
|
||||
|
||||
script(type="text/ng-template" id="lockEditorModalTemplate")
|
||||
.modal-header
|
||||
h3 {{ title }}
|
||||
.modal-body(ng-bind-html="message")
|
||||
|
||||
block append meta
|
||||
include ./editor/meta
|
||||
|
||||
block prepend foot-scripts
|
||||
each file in (useOpenTelemetry ? entrypointScripts("tracing") : [])
|
||||
script(type="text/javascript", nonce=scriptNonce, src=file)
|
||||
script(type="text/javascript", nonce=scriptNonce, src=(wsUrl || '/socket.io') + '/socket.io.js')
|
|
@ -1,52 +0,0 @@
|
|||
.ui-layout-center(
|
||||
ng-controller="ReviewPanelController",
|
||||
ng-class="{\
|
||||
'rp-state-current-file': (reviewPanel.subView === SubViews.CUR_FILE),\
|
||||
'rp-state-current-file-expanded': (reviewPanel.subView === SubViews.CUR_FILE && ui.reviewPanelOpen),\
|
||||
'rp-state-current-file-mini': (reviewPanel.subView === SubViews.CUR_FILE && !ui.reviewPanelOpen),\
|
||||
'rp-state-overview': (reviewPanel.subView === SubViews.OVERVIEW),\
|
||||
'rp-size-mini': ui.miniReviewPanelVisible,\
|
||||
'rp-size-expanded': ui.reviewPanelOpen,\
|
||||
'rp-layout-left': reviewPanel.layoutToLeft,\
|
||||
'rp-loading-threads': loadingThreads,\
|
||||
}"
|
||||
)
|
||||
|
||||
.multi-selection-ongoing(
|
||||
ng-show="editor.multiSelectedCount > 0"
|
||||
)
|
||||
.multi-selection-message
|
||||
h4 {{ editor.multiSelectedCount }} #{translate('files_selected')}
|
||||
|
||||
include ./file-view
|
||||
|
||||
.editor-container.full-size(
|
||||
ng-show="ui.view == 'editor' && editor.multiSelectedCount === 0"
|
||||
vertical-resizable-panes="south-pane-resizer"
|
||||
vertical-resizable-panes-hidden-externally-on="south-pane-toggled"
|
||||
vertical-resizable-panes-hidden-initially="true"
|
||||
vertical-resizable-panes-default-size="250"
|
||||
vertical-resizable-panes-min-size="250"
|
||||
vertical-resizable-panes-max-size="336"
|
||||
vertical-resizable-panes-resize-on="layout:flat-screen:toggle,south-pane-toggled"
|
||||
)
|
||||
.div(vertical-resizable-top)
|
||||
|
||||
.loading-panel(
|
||||
ng-show="(!editor.sharejs_doc || editor.opening) && !editor.error_state",
|
||||
)
|
||||
span(ng-show="editor.open_doc_id")
|
||||
i.fa.fa-spin.fa-refresh
|
||||
| #{translate("loading")}…
|
||||
span(ng-show="!editor.open_doc_id")
|
||||
i.fa.fa-arrow-left
|
||||
| #{translate("open_a_file_on_the_left")}
|
||||
|
||||
div(ng-controller="EditorLoaderController")
|
||||
|
||||
include ../../source-editor/source-editor
|
||||
|
||||
if moduleIncludesAvailable('editor:symbol-palette')
|
||||
.div(vertical-resizable-bottom)
|
||||
if moduleIncludesAvailable('editor:symbol-palette')
|
||||
!= moduleIncludes('editor:symbol-palette', locals)
|
|
@ -1,53 +0,0 @@
|
|||
div.full-size(
|
||||
ng-show="ui.view == 'editor' || ui.view === 'file'"
|
||||
layout="pdf"
|
||||
open-east="ui.pdfOpen"
|
||||
mask-iframes-on-resize="true"
|
||||
resize-on="layout:main:resize"
|
||||
resize-proportionally="true"
|
||||
initial-size-east="'50%'"
|
||||
minimum-restore-size-east="300"
|
||||
allow-overflow-on="'center'"
|
||||
custom-toggler-pane="east"
|
||||
custom-toggler-msg-when-open=translate("tooltip_hide_pdf")
|
||||
custom-toggler-msg-when-closed=translate("tooltip_show_pdf")
|
||||
)
|
||||
include ./editor-pane
|
||||
|
||||
.ui-layout-east
|
||||
div(ng-if="ui.pdfLayout == 'sideBySide'")
|
||||
pdf-preview()
|
||||
|
||||
.ui-layout-resizer-controls.synctex-controls(
|
||||
ng-show="settings.pdfViewer !== 'native'"
|
||||
)
|
||||
pdf-synctex-controls()
|
||||
|
||||
div.full-size(
|
||||
ng-if="ui.pdfLayout == 'flat'"
|
||||
ng-show="ui.view == 'pdf'"
|
||||
)
|
||||
pdf-preview()
|
||||
|
||||
// fallback, shown when no file/view is selected
|
||||
div.full-size.no-file-selection(
|
||||
ng-if="!ui.view"
|
||||
)
|
||||
.no-file-selection-message(
|
||||
ng-if="rootFolder.children && rootFolder.children.length > 0"
|
||||
)
|
||||
h3
|
||||
| #{translate('no_selection_select_file')}
|
||||
.no-file-selection-message(
|
||||
ng-if="rootFolder.children && rootFolder.children.length === 0"
|
||||
)
|
||||
h3
|
||||
| #{translate('no_selection_create_new_file')}
|
||||
div(
|
||||
ng-controller="FileTreeController"
|
||||
)
|
||||
button.btn.btn-primary(
|
||||
ng-click="openNewDocModal()"
|
||||
)
|
||||
| #{translate('new_file')}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
aside.editor-sidebar.full-size.history-file-tree#history-file-tree(
|
||||
ng-show="ui.view == 'history'"
|
||||
)
|
|
@ -1,27 +0,0 @@
|
|||
aside.editor-sidebar.full-size(
|
||||
ng-show="ui.view != 'history'"
|
||||
vertical-resizable-panes="outline-resizer"
|
||||
vertical-resizable-panes-toggled-externally-on="outline-toggled"
|
||||
vertical-resizable-panes-default-size="350"
|
||||
vertical-resizable-panes-min-size="32"
|
||||
vertical-resizable-panes-max-size="'75%'"
|
||||
vertical-resizable-panes-resize-on="left-pane-resize-all"
|
||||
)
|
||||
|
||||
div(
|
||||
ng-controller="ReactFileTreeController"
|
||||
vertical-resizable-top
|
||||
)
|
||||
file-tree-root(
|
||||
on-select="onSelect"
|
||||
on-init="onInit"
|
||||
is-connected="isConnected"
|
||||
ref-providers="refProviders"
|
||||
reindex-references="reindexReferences"
|
||||
set-ref-provider-enabled="setRefProviderEnabled"
|
||||
set-started-free-trial="setStartedFreeTrial"
|
||||
)
|
||||
|
||||
outline-container(
|
||||
vertical-resizable-bottom
|
||||
)
|
|
@ -1,9 +0,0 @@
|
|||
div(
|
||||
ng-controller="FileViewController"
|
||||
ng-show="ui.view == 'file'"
|
||||
ng-if="openFile && editor.multiSelectedCount === 0"
|
||||
)
|
||||
file-view(
|
||||
file='file'
|
||||
store-references-keys='storeReferencesKeys'
|
||||
)
|
|
@ -1,12 +0,0 @@
|
|||
div(ng-controller="ReactShareProjectModalController")
|
||||
share-project-modal(
|
||||
handle-hide="handleHide"
|
||||
show="show"
|
||||
)
|
||||
|
||||
div(ng-controller="EditorNavigationToolbarController")
|
||||
editor-navigation-toolbar-root(
|
||||
open-doc="openDoc"
|
||||
online-users-array="onlineUsersArray"
|
||||
open-share-project-modal="openShareProjectModal"
|
||||
)
|
|
@ -1 +0,0 @@
|
|||
editor-left-menu()
|
|
@ -1,39 +0,0 @@
|
|||
include ./left-menu-react
|
||||
|
||||
#chat-wrapper.full-size(
|
||||
layout="chat",
|
||||
spacing-open="{{ui.chatResizerSizeOpen}}",
|
||||
spacing-closed="{{ui.chatResizerSizeClosed}}",
|
||||
ng-hide="state.loading",
|
||||
ng-cloak
|
||||
)
|
||||
.ui-layout-center
|
||||
include ./header-react
|
||||
|
||||
main#ide-body(
|
||||
ng-cloak,
|
||||
role="main",
|
||||
layout="main",
|
||||
ng-hide="state.loading",
|
||||
resize-on="layout:chat:resize,history:toggle,layout:flat-screen:toggle,south-pane-toggled",
|
||||
minimum-restore-size-west="130"
|
||||
custom-toggler-pane="west"
|
||||
custom-toggler-msg-when-open=translate("tooltip_hide_filetree")
|
||||
custom-toggler-msg-when-closed=translate("tooltip_show_filetree")
|
||||
tabindex="0"
|
||||
initial-size-east="250"
|
||||
init-closed-east="true"
|
||||
open-east="ui.chatOpen"
|
||||
)
|
||||
.ui-layout-west
|
||||
include ./file-tree-react
|
||||
include ./file-tree-history-react
|
||||
|
||||
.ui-layout-center
|
||||
include ./editor
|
||||
history-root()
|
||||
|
||||
if !isRestrictedTokenMember
|
||||
.ui-layout-east
|
||||
aside.chat
|
||||
chat()
|
|
@ -35,7 +35,6 @@ meta(name="ol-optionalPersonalAccessToken", data-type="boolean" content=optional
|
|||
meta(name="ol-hasTrackChangesFeature", data-type="boolean" content=hasTrackChangesFeature)
|
||||
meta(name="ol-inactiveTutorials", data-type="json" content=user.inactiveTutorials)
|
||||
meta(name="ol-projectTags" data-type="json" content=projectTags)
|
||||
meta(name="ol-idePageReact", data-type="boolean" content=idePageReact)
|
||||
meta(name="ol-loadingText", data-type="string" content=translate("loading"))
|
||||
meta(name="ol-translationLoadErrorMessage", data-type="string" content=translate("could_not_load_translations"))
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
extends ../layout
|
||||
extends ../layout-marketing
|
||||
|
||||
block entrypointVar
|
||||
- entrypoint = 'ide-detached'
|
|
@ -1,4 +1,4 @@
|
|||
extends ../layout
|
||||
extends ../layout-marketing
|
||||
|
||||
block vars
|
||||
- var suppressNavbar = true
|
||||
|
|
|
@ -1,127 +0,0 @@
|
|||
extends ../../layout
|
||||
|
||||
block vars
|
||||
- var suppressFooter = true
|
||||
- var suppressCookieBanner = true
|
||||
- var suppressSkipToContent = true
|
||||
|
||||
block content
|
||||
|
||||
script(type="template", id="overleaf-token-access-data")!= StringHelper.stringifyJsonForScript({ postUrl: postUrl, csrfToken: csrfToken})
|
||||
|
||||
div(
|
||||
ng-controller="TokenAccessPageController",
|
||||
ng-init="post()"
|
||||
)
|
||||
.editor.full-size
|
||||
div
|
||||
|
|
||||
a(href="/project", style="font-size: 2rem; margin-left: 1rem; color: #ddd;")
|
||||
i.fa.fa-arrow-left
|
||||
|
||||
.loading-screen(
|
||||
ng-show="mode == 'accessAttempt'"
|
||||
)
|
||||
.loading-screen-brand-container
|
||||
.loading-screen-brand()
|
||||
|
||||
h3.loading-screen-label.text-center
|
||||
| #{translate('join_project')}
|
||||
span(ng-show="accessInFlight == true")
|
||||
span.loading-screen-ellip .
|
||||
span.loading-screen-ellip .
|
||||
span.loading-screen-ellip .
|
||||
|
||||
|
||||
.global-alerts.text-center(ng-cloak)
|
||||
div(ng-show="accessError", ng-cloak)
|
||||
br
|
||||
div(ng-switch="accessError", ng-cloak)
|
||||
div(ng-switch-when="not_found")
|
||||
h4(aria-live="assertive")
|
||||
| Project not found
|
||||
|
||||
div(ng-switch-default)
|
||||
.alert.alert-danger(aria-live="assertive") #{translate('token_access_failure')}
|
||||
p
|
||||
a(href="/") #{translate('home')}
|
||||
|
||||
.loading-screen(
|
||||
ng-show="mode == 'v1Import'"
|
||||
)
|
||||
.container
|
||||
.row
|
||||
.col-sm-8.col-sm-offset-2
|
||||
h1.text-center
|
||||
span(ng-if="v1ImportData.status != 'mustLogin'") Overleaf v1 Project
|
||||
span(ng-if="v1ImportData.status == 'mustLogin'") Please Log In
|
||||
img.v2-import__img(
|
||||
src="/img/v1-import/v2-editor.png"
|
||||
alt="The new V2 editor."
|
||||
)
|
||||
|
||||
div(ng-if="v1ImportData.status == 'cannotImport'")
|
||||
h2.text-center
|
||||
| Cannot Access Overleaf v1 Project
|
||||
p.text-center.row-spaced-small
|
||||
| Please contact the project owner or
|
||||
|
|
||||
a(href="/contact") contact support
|
||||
|
|
||||
| for assistance.
|
||||
|
||||
div(ng-if="v1ImportData.status == 'mustLogin'")
|
||||
p.text-center.row-spaced-small
|
||||
| You will need to log in to access this project.
|
||||
|
||||
.row-spaced.text-center
|
||||
a.btn.btn-primary(
|
||||
href="/login?redir={{ currentPath() }}"
|
||||
) Log In To Access Project
|
||||
|
||||
div(ng-if="v1ImportData.status == 'canDownloadZip'")
|
||||
p.text-center.row-spaced.small
|
||||
| #[strong() {{ getProjectName() }}] has not yet been moved into
|
||||
| the new version of Overleaf. This project was created
|
||||
| anonymously and therefore cannot be automatically imported.
|
||||
| Please download a zip file of the project and upload that to
|
||||
| continue editing it. If you would like to delete this project
|
||||
| after you have made a copy, please contact support.
|
||||
|
||||
.row-spaced.text-center
|
||||
a.btn.btn-primary(ng-href="{{ buildZipDownloadPath(v1ImportData.projectId) }}")
|
||||
| Download project zip file
|
||||
|
||||
.loading-screen(
|
||||
ng-show="mode == 'requireAccept'"
|
||||
)
|
||||
.container
|
||||
.row
|
||||
.col-md-8.col-md-offset-2
|
||||
.card
|
||||
.page-header.text-centered
|
||||
h1 #{translate("invited_to_join")}
|
||||
br
|
||||
em {{ getProjectName() }}
|
||||
.row.text-center
|
||||
.col-md-12
|
||||
p
|
||||
if user
|
||||
| #{translate("accepting_invite_as")}
|
||||
|
|
||||
em #{user.email}
|
||||
.row.text-center
|
||||
.col-md-12
|
||||
button.btn.btn-lg.btn-primary(
|
||||
type='submit'
|
||||
ng-click="postConfirmedByUser()"
|
||||
) #{translate("join_project")}
|
||||
|
||||
|
||||
block append foot-scripts
|
||||
script(type="text/javascript", nonce=scriptNonce).
|
||||
$(document).ready(function () {
|
||||
setTimeout(function() {
|
||||
$('.loading-screen-brand').css('height', '20%')
|
||||
}, 500);
|
||||
});
|
|
@ -1,3 +0,0 @@
|
|||
source-editor.review-panel-react#editor(
|
||||
ng-show="!!editor.sharejs_doc && !editor.opening && multiSelectedCount === 0 && !editor.error_state"
|
||||
)
|
|
@ -1,10 +1,11 @@
|
|||
extends ../layout
|
||||
extends ../layout-marketing
|
||||
|
||||
include ./plans/_mixins
|
||||
include ../_mixins/bootstrap_js
|
||||
|
||||
block entrypointVar
|
||||
- entrypoint = 'pages/user/subscription/plans-v2/plans-v2-main'
|
||||
|
||||
block vars
|
||||
- entrypoint = 'pages/user/subscription/plans-v2/plans-v2-main'
|
||||
- var suppressFooter = true
|
||||
- var suppressNavbarRight = true
|
||||
- var suppressCookieBanner = true
|
||||
|
@ -66,6 +67,3 @@ block content
|
|||
| #{translate("continue_with_free_plan")}
|
||||
|
||||
!= moduleIncludes("contactModalGeneral-marketing", locals)
|
||||
|
||||
block prepend foot-scripts
|
||||
+bootstrap-js(bootstrapVersion)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
extends ../layout-marketing
|
||||
|
||||
block vars
|
||||
block entrypointVar
|
||||
- entrypoint = 'pages/user/subscription/plans-v2/plans-v2-main'
|
||||
|
||||
block append meta
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
extends ../../layout
|
||||
extends ../../layout-marketing
|
||||
|
||||
block content
|
||||
main.content.content-alt.team-invite#main-content
|
||||
|
|
|
@ -1,47 +0,0 @@
|
|||
script(type="text/ng-template", id="BonusLinkToUsModal")
|
||||
.modal-header
|
||||
button.close(
|
||||
type="button"
|
||||
data-dismiss="modal"
|
||||
ng-click="cancel()"
|
||||
aria-label="Close"
|
||||
)
|
||||
span(aria-hidden="true") ×
|
||||
h3 Dropbox link
|
||||
.modal-body.modal-body-share
|
||||
|
||||
div(ng-show="dbState.gotLinkStatus")
|
||||
div(ng-hide="dbState.userIsLinkedToDropbox || !dbState.hasDropboxFeature")
|
||||
|
||||
span(ng-hide="dbState.startedLinkProcess") Your account is not linked to dropbox
|
||||
|
|
||||
a(ng-click="linkToDropbox()").btn.btn-info Update Dropbox Settings
|
||||
|
||||
p.small.text-center(ng-show="dbState.startedLinkProcess")
|
||||
| Please refresh this page after starting your free trial.
|
||||
|
||||
|
||||
div(ng-show="dbState.hasDropboxFeature && dbState.userIsLinkedToDropbox")
|
||||
progressbar.progress-striped.active(value='dbState.percentageLeftTillNextPoll', type="info")
|
||||
span
|
||||
strong {{dbState.minsTillNextPoll}} minutes
|
||||
span until dropbox is next checked for changes.
|
||||
|
||||
div.text-center(ng-hide="dbState.hasDropboxFeature")
|
||||
p You need to upgrade your account to link to dropbox.
|
||||
p
|
||||
a.btn(ng-click="startFreeTrial('dropbox')", ng-class="buttonClass") Start Free Trial
|
||||
p.small(ng-show="startedFreeTrial")
|
||||
| Please refresh this page after starting your free trial.
|
||||
|
||||
div(ng-hide="dbState.gotLinkStatus")
|
||||
span.small checking dropbox status
|
||||
i.fa.fa-refresh.fa-spin(aria-hidden="true")
|
||||
|
||||
|
||||
|
||||
.modal-footer()
|
||||
button.btn.btn-default(
|
||||
ng-click="cancel()",
|
||||
)
|
||||
span Dismiss
|
|
@ -27,7 +27,7 @@
|
|||
["@babel/react", { "runtime": "automatic" }],
|
||||
"@babel/typescript"
|
||||
],
|
||||
"plugins": ["angularjs-annotate", "macros"],
|
||||
"plugins": ["macros"],
|
||||
"overrides": [
|
||||
// treat .cjs files (e.g. libraries symlinked into node_modules) as commonjs
|
||||
{
|
||||
|
|
|
@ -1,33 +1,6 @@
|
|||
const fs = require('fs')
|
||||
const Path = require('path')
|
||||
const { merge } = require('@overleaf/settings/merge')
|
||||
|
||||
// Automatically detect module imports that are included in this version of the application (SaaS, Server-CE, Server Pro).
|
||||
// E.g. during a Server-CE build, we will not find imports for proprietary modules.
|
||||
//
|
||||
// Restart webpack after adding/removing modules.
|
||||
const MODULES_PATH = Path.join(__dirname, '../modules')
|
||||
const entryPointsIde = []
|
||||
const entryPointsMain = []
|
||||
fs.readdirSync(MODULES_PATH).forEach(module => {
|
||||
const entryPathIde = Path.join(
|
||||
MODULES_PATH,
|
||||
module,
|
||||
'/frontend/js/ide/index.js'
|
||||
)
|
||||
if (fs.existsSync(entryPathIde)) {
|
||||
entryPointsIde.push(entryPathIde)
|
||||
}
|
||||
const entryPathMain = Path.join(
|
||||
MODULES_PATH,
|
||||
module,
|
||||
'/frontend/js/main/index.js'
|
||||
)
|
||||
if (fs.existsSync(entryPathMain)) {
|
||||
entryPointsMain.push(entryPathMain)
|
||||
}
|
||||
})
|
||||
|
||||
let defaultFeatures, siteUrl
|
||||
|
||||
// Make time interval config easier.
|
||||
|
@ -918,9 +891,6 @@ module.exports = {
|
|||
managedGroupSubscriptionEnrollmentNotification: [],
|
||||
managedGroupEnrollmentInvite: [],
|
||||
ssoCertificateInfo: [],
|
||||
// See comment at the definition of these variables.
|
||||
entryPointsIde,
|
||||
entryPointsMain,
|
||||
},
|
||||
|
||||
moduleImportSequence: [
|
||||
|
@ -935,7 +905,7 @@ module.exports = {
|
|||
reportOnly: process.env.CSP_REPORT_ONLY === 'true',
|
||||
reportPercentage: parseFloat(process.env.CSP_REPORT_PERCENTAGE) || 0,
|
||||
reportUri: process.env.CSP_REPORT_URI,
|
||||
exclude: ['app/views/project/editor'],
|
||||
exclude: [],
|
||||
},
|
||||
|
||||
unsupportedBrowsers: {
|
||||
|
|
|
@ -11,6 +11,7 @@ import { interceptFileUpload } from './upload'
|
|||
import { interceptProjectListing } from './project-list'
|
||||
import { interceptLinkedFile } from './linked-file'
|
||||
import { interceptMathJax } from './mathjax'
|
||||
import { interceptMetadata } from './metadata'
|
||||
|
||||
// eslint-disable-next-line no-unused-vars,@typescript-eslint/no-namespace
|
||||
declare global {
|
||||
|
@ -21,6 +22,7 @@ declare global {
|
|||
interceptAsync: typeof interceptAsync
|
||||
interceptCompile: typeof interceptCompile
|
||||
interceptEvents: typeof interceptEvents
|
||||
interceptMetadata: typeof interceptMetadata
|
||||
interceptSpelling: typeof interceptSpelling
|
||||
waitForCompile: typeof waitForCompile
|
||||
interceptDeferredCompile: typeof interceptDeferredCompile
|
||||
|
@ -35,6 +37,7 @@ declare global {
|
|||
Cypress.Commands.add('interceptAsync', interceptAsync)
|
||||
Cypress.Commands.add('interceptCompile', interceptCompile)
|
||||
Cypress.Commands.add('interceptEvents', interceptEvents)
|
||||
Cypress.Commands.add('interceptMetadata', interceptMetadata)
|
||||
Cypress.Commands.add('interceptSpelling', interceptSpelling)
|
||||
Cypress.Commands.add('waitForCompile', waitForCompile)
|
||||
Cypress.Commands.add('interceptDeferredCompile', interceptDeferredCompile)
|
||||
|
|
3
services/web/cypress/support/shared/commands/metadata.ts
Normal file
3
services/web/cypress/support/shared/commands/metadata.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export const interceptMetadata = () => {
|
||||
cy.intercept('POST', '/project/*/doc/*/metadata', {})
|
||||
}
|
|
@ -1,61 +0,0 @@
|
|||
/* eslint-disable
|
||||
camelcase,
|
||||
max-len,
|
||||
no-useless-escape,
|
||||
*/
|
||||
// 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__
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
|
||||
import './utils/webpack-public-path'
|
||||
import './libraries'
|
||||
import './infrastructure/error-reporter'
|
||||
import './modules/recursionHelper'
|
||||
import './modules/errorCatcher'
|
||||
import './modules/localStorage'
|
||||
import './modules/sessionStorage'
|
||||
import getMeta from './utils/meta'
|
||||
|
||||
const App = angular
|
||||
.module('OverleafApp', [
|
||||
'ui.bootstrap',
|
||||
'RecursionHelper',
|
||||
'ngSanitize',
|
||||
'ErrorCatcher',
|
||||
'localStorage',
|
||||
'sessionStorage',
|
||||
'ui.select',
|
||||
])
|
||||
.config([
|
||||
'$qProvider',
|
||||
'uiSelectConfig',
|
||||
function ($qProvider, uiSelectConfig) {
|
||||
$qProvider.errorOnUnhandledRejections(false)
|
||||
uiSelectConfig.spinnerClass = 'fa fa-refresh ui-select-spin'
|
||||
},
|
||||
])
|
||||
|
||||
App.run([
|
||||
'$rootScope',
|
||||
'$templateCache',
|
||||
function ($rootScope, $templateCache) {
|
||||
$rootScope.usersEmail = getMeta('ol-usersEmail')
|
||||
|
||||
// UI Select templates are hard-coded and use Glyphicon icons (which we don't import).
|
||||
// The line below simply overrides the hard-coded template with our own, which is
|
||||
// basically the same but using Font Awesome icons.
|
||||
$templateCache.put(
|
||||
'bootstrap/match.tpl.html',
|
||||
'<div class="ui-select-match" ng-hide="$select.open && $select.searchEnabled" ng-disabled="$select.disabled" ng-class="{\'btn-default-focus\':$select.focus}"><span tabindex="-1" class="btn btn-default form-control ui-select-toggle" aria-label="{{ $select.baseTitle }} activate" ng-disabled="$select.disabled" ng-click="$select.activate()" style="outline: 0;"><span ng-show="$select.isEmpty()" class="ui-select-placeholder text-muted">{{$select.placeholder}}</span> <span ng-hide="$select.isEmpty()" class="ui-select-match-text pull-left" ng-class="{\'ui-select-allow-clear\': $select.allowClear && !$select.isEmpty()}" ng-transclude=""></span> <i class="caret pull-right" ng-click="$select.toggle($event)"></i> <a ng-show="$select.allowClear && !$select.isEmpty() && ($select.disabled !== true)" aria-label="{{ $select.baseTitle }} clear" style="margin-right: 10px" ng-click="$select.clear($event)" class="btn btn-xs btn-link pull-right"><i class="fa fa-times" aria-hidden="true"></i></a></span></div>'
|
||||
)
|
||||
},
|
||||
])
|
||||
|
||||
export default App
|
|
@ -1,134 +0,0 @@
|
|||
import _ from 'lodash'
|
||||
/* eslint-disable
|
||||
camelcase,
|
||||
max-len,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
// For sending event data to metabase and google analytics
|
||||
// ---
|
||||
// by default,
|
||||
// event not sent to MB.
|
||||
// for MB, add event-tracking-mb='true'
|
||||
// by default, event sent to MB via sendMB
|
||||
// event not sent to GA.
|
||||
// for GA, add event-tracking-ga attribute, where the value is the GA category
|
||||
// Either GA or MB can use the attribute event-tracking-send-once='true' to
|
||||
// send event just once
|
||||
// MB will use the key and GA will use the action to determine if the event
|
||||
// has been sent
|
||||
// event-tracking-trigger attribute is required to send event
|
||||
|
||||
/* eslint-disable
|
||||
camelcase,
|
||||
max-len,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
// For sending event data to metabase and google analytics
|
||||
// ---
|
||||
// by default,
|
||||
// event not sent to MB.
|
||||
// for MB, add event-tracking-mb='true'
|
||||
// by default, event sent to MB via sendMB
|
||||
// event not sent to GA.
|
||||
// for GA, add event-tracking-ga attribute, where the value is the GA category
|
||||
// Either GA or MB can use the attribute event-tracking-send-once='true' to
|
||||
// send event just once
|
||||
// MB will use the key and GA will use the action to determine if the event
|
||||
// has been sent
|
||||
// event-tracking-trigger attribute is required to send event
|
||||
|
||||
import App from '../base'
|
||||
|
||||
const isInViewport = function (element) {
|
||||
const elTop = element.offset().top
|
||||
const elBtm = elTop + element.outerHeight()
|
||||
|
||||
const viewportTop = $(window).scrollTop()
|
||||
const viewportBtm = viewportTop + $(window).height()
|
||||
|
||||
return elBtm > viewportTop && elTop < viewportBtm
|
||||
}
|
||||
|
||||
export default App.directive('eventTracking', [
|
||||
'eventTracking',
|
||||
function (eventTracking) {
|
||||
return {
|
||||
scope: {
|
||||
eventTracking: '@',
|
||||
eventSegmentation: '=?',
|
||||
},
|
||||
link(scope, element, attrs) {
|
||||
const sendGA = attrs.eventTrackingGa || false
|
||||
const sendMB = attrs.eventTrackingMb || false
|
||||
const sendMBFunction = attrs.eventTrackingSendOnce
|
||||
? 'sendMBOnce'
|
||||
: 'sendMB'
|
||||
const sendGAFunction = attrs.eventTrackingSendOnce
|
||||
? 'sendGAOnce'
|
||||
: 'send'
|
||||
const segmentation = scope.eventSegmentation || {}
|
||||
segmentation.page = window.location.pathname
|
||||
|
||||
const sendEvent = function (scrollEvent) {
|
||||
/*
|
||||
@param {boolean} scrollEvent Use to unbind scroll event
|
||||
*/
|
||||
if (sendMB) {
|
||||
eventTracking[sendMBFunction](scope.eventTracking, segmentation)
|
||||
}
|
||||
if (sendGA) {
|
||||
eventTracking[sendGAFunction](
|
||||
attrs.eventTrackingGa,
|
||||
attrs.eventTrackingAction || scope.eventTracking,
|
||||
attrs.eventTrackingLabel || ''
|
||||
)
|
||||
}
|
||||
if (scrollEvent) {
|
||||
return $(window).unbind('resize scroll')
|
||||
}
|
||||
}
|
||||
|
||||
if (attrs.eventTrackingTrigger === 'load') {
|
||||
return sendEvent()
|
||||
} else if (attrs.eventTrackingTrigger === 'click') {
|
||||
return element.on('click', e => sendEvent())
|
||||
} else if (attrs.eventTrackingTrigger === 'hover') {
|
||||
let timer = null
|
||||
let timeoutAmt = 500
|
||||
if (attrs.eventHoverAmt) {
|
||||
timeoutAmt = parseInt(attrs.eventHoverAmt, 10)
|
||||
}
|
||||
return element
|
||||
.on('mouseenter', function () {
|
||||
timer = setTimeout(() => sendEvent(), timeoutAmt)
|
||||
})
|
||||
.on('mouseleave', () => clearTimeout(timer))
|
||||
} else if (
|
||||
attrs.eventTrackingTrigger === 'scroll' &&
|
||||
!eventTracking.eventInCache(scope.eventTracking)
|
||||
) {
|
||||
$(window).on(
|
||||
'resize scroll',
|
||||
_.throttle(() => {
|
||||
if (isInViewport(element)) {
|
||||
sendEvent(true)
|
||||
}
|
||||
}, 500)
|
||||
)
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
])
|
|
@ -1,6 +0,0 @@
|
|||
import App from '../../../base'
|
||||
import { react2angular } from 'react2angular'
|
||||
import { rootContext } from '../../../shared/context/root-context'
|
||||
import EditorLeftMenu from '../components/editor-left-menu'
|
||||
|
||||
App.component('editorLeftMenu', react2angular(rootContext.use(EditorLeftMenu)))
|
|
@ -7,8 +7,7 @@ export default function useProjectWideSettingsSocketListener() {
|
|||
const ide = useIdeContext()
|
||||
|
||||
const [project, setProject] = useScopeValue<ProjectSettings | undefined>(
|
||||
'project',
|
||||
true
|
||||
'project'
|
||||
)
|
||||
|
||||
const setCompiler = useCallback(
|
||||
|
|
|
@ -8,7 +8,7 @@ import { debugConsole } from '@/utils/debugging'
|
|||
|
||||
export default function useProjectWideSettings() {
|
||||
// The value will be undefined on mount
|
||||
const [project] = useScopeValue<ProjectSettings | undefined>('project', true)
|
||||
const [project] = useScopeValue<ProjectSettings | undefined>('project')
|
||||
const saveProjectSettings = useSaveProjectSettings()
|
||||
|
||||
const setCompiler = useCallback(
|
||||
|
|
|
@ -6,7 +6,7 @@ export default function useSaveProjectSettings() {
|
|||
// projectSettings value will be undefined on mount
|
||||
const [projectSettings, setProjectSettings] = useScopeValue<
|
||||
ProjectSettings | undefined
|
||||
>('project', true)
|
||||
>('project')
|
||||
const { _id: projectId } = useProjectContext()
|
||||
|
||||
return async (
|
||||
|
|
|
@ -1,30 +0,0 @@
|
|||
import App from '../../../base'
|
||||
import { react2angular } from 'react2angular'
|
||||
import EditorNavigationToolbarRoot from '../components/editor-navigation-toolbar-root'
|
||||
import { rootContext } from '../../../shared/context/root-context'
|
||||
|
||||
App.controller('EditorNavigationToolbarController', [
|
||||
'$scope',
|
||||
'ide',
|
||||
function ($scope, ide) {
|
||||
// wrapper is required to avoid scope problems with `this` inside `EditorManager`
|
||||
$scope.openDoc = (doc, args) => ide.editorManager.openDoc(doc, args)
|
||||
},
|
||||
])
|
||||
|
||||
App.component(
|
||||
'editorNavigationToolbarRoot',
|
||||
react2angular(rootContext.use(EditorNavigationToolbarRoot), [
|
||||
'openDoc',
|
||||
|
||||
// `$scope.onlineUsersArray` is already populated by `OnlineUsersManager`, which also creates
|
||||
// a new array instance every time the list of online users change (which should refresh the
|
||||
// value passed to React as a prop, triggering a re-render)
|
||||
'onlineUsersArray',
|
||||
|
||||
// We're still including ShareController as part fo the React navigation toolbar. The reason is
|
||||
// the coupling between ShareController's $scope and Angular's ShareProjectModal. Once ShareProjectModal
|
||||
// is fully ported to React we should be able to repli
|
||||
'openShareProjectModal',
|
||||
])
|
||||
)
|
|
@ -1,123 +0,0 @@
|
|||
import App from '../../../base'
|
||||
import { react2angular } from 'react2angular'
|
||||
import { cloneDeep } from 'lodash'
|
||||
|
||||
import FileTreeRoot from '../components/file-tree-root'
|
||||
import { rootContext } from '../../../shared/context/root-context'
|
||||
|
||||
App.controller('ReactFileTreeController', [
|
||||
'$scope',
|
||||
'$timeout',
|
||||
'ide',
|
||||
function ($scope, $timeout, ide) {
|
||||
$scope.isConnected = true
|
||||
|
||||
$scope.$on('project:joined', () => {
|
||||
$scope.$emit('file-tree:initialized')
|
||||
})
|
||||
|
||||
$scope.$watch('editor.open_doc_id', openDocId => {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('editor.openDoc', { detail: openDocId })
|
||||
)
|
||||
})
|
||||
|
||||
$scope.$on('file-tree.reselectDoc', (ev, docId) => {
|
||||
window.dispatchEvent(new CustomEvent('editor.openDoc', { detail: docId }))
|
||||
})
|
||||
|
||||
// Set isConnected to true if:
|
||||
// - connection state is 'ready', OR
|
||||
// - connection state is 'waitingCountdown' and reconnection_countdown is null
|
||||
// The added complexity is needed because in Firefox on page reload the
|
||||
// connection state goes into 'waitingCountdown' before being hidden and we
|
||||
// don't want to show a disconnect UI.
|
||||
function updateIsConnected() {
|
||||
if ($scope.connection) {
|
||||
const isReady = $scope.connection.state === 'ready'
|
||||
const willStartCountdown =
|
||||
$scope.connection.state === 'waitingCountdown' &&
|
||||
$scope.connection.reconnection_countdown === null
|
||||
$scope.isConnected = isReady || willStartCountdown
|
||||
} else {
|
||||
$scope.isConnected = false
|
||||
}
|
||||
}
|
||||
|
||||
$scope.$watch('connection.state', updateIsConnected)
|
||||
$scope.$watch('connection.reconnection_countdown', updateIsConnected)
|
||||
|
||||
$scope.onInit = () => {
|
||||
// HACK: resize the vertical pane on init after a 0ms timeout. We do not
|
||||
// understand why this is necessary but without this the resized handle is
|
||||
// stuck at the bottom. The vertical resize will soon be migrated to React
|
||||
// so we accept to live with this hack for now.
|
||||
$timeout(() => {
|
||||
$scope.$emit('left-pane-resize-all')
|
||||
})
|
||||
}
|
||||
|
||||
$scope.onSelect = selectedEntities => {
|
||||
if (selectedEntities.length === 1) {
|
||||
const selectedEntity = selectedEntities[0]
|
||||
const type =
|
||||
selectedEntity.type === 'fileRef' ? 'file' : selectedEntity.type
|
||||
$scope.$emit('entity:selected', {
|
||||
...selectedEntity.entity,
|
||||
id: selectedEntity.entity._id,
|
||||
type,
|
||||
})
|
||||
|
||||
// in the react implementation there is no such concept as "1
|
||||
// multi-selected entity" so here we pass a count of 0
|
||||
$scope.$emit('entities:multiSelected', { count: 0 })
|
||||
} else if (selectedEntities.length > 1) {
|
||||
$scope.$emit('entities:multiSelected', {
|
||||
count: selectedEntities.length,
|
||||
})
|
||||
} else {
|
||||
$scope.$emit('entity:no-selection')
|
||||
}
|
||||
}
|
||||
|
||||
$scope.refProviders = ide.$scope.user.refProviders || {}
|
||||
|
||||
ide.$scope.$watch(
|
||||
'user.refProviders',
|
||||
refProviders => {
|
||||
$scope.refProviders = cloneDeep(refProviders)
|
||||
},
|
||||
true
|
||||
)
|
||||
|
||||
$scope.setRefProviderEnabled = (provider, value = true) => {
|
||||
ide.$scope.$applyAsync(() => {
|
||||
ide.$scope.user.refProviders[provider] = value
|
||||
})
|
||||
}
|
||||
|
||||
$scope.setStartedFreeTrial = started => {
|
||||
$scope.$applyAsync(() => {
|
||||
$scope.startedFreeTrial = started
|
||||
})
|
||||
}
|
||||
|
||||
$scope.reindexReferences = () => {
|
||||
ide.$scope.$emit('references:should-reindex', {})
|
||||
}
|
||||
},
|
||||
])
|
||||
|
||||
App.component(
|
||||
'fileTreeRoot',
|
||||
react2angular(rootContext.use(FileTreeRoot), [
|
||||
'onSelect',
|
||||
'onDelete',
|
||||
'onInit',
|
||||
'isConnected',
|
||||
'setRefProviderEnabled',
|
||||
'setStartedFreeTrial',
|
||||
'reindexReferences',
|
||||
'refProviders',
|
||||
])
|
||||
)
|
|
@ -1,24 +0,0 @@
|
|||
import App from '../../../base'
|
||||
import { react2angular } from 'react2angular'
|
||||
import _ from 'lodash'
|
||||
|
||||
import { rootContext } from '../../../shared/context/root-context'
|
||||
import FileView from '../components/file-view'
|
||||
|
||||
export default App.controller('FileViewController', [
|
||||
'$scope',
|
||||
'$rootScope',
|
||||
function ($scope, $rootScope) {
|
||||
$scope.file = $scope.openFile
|
||||
|
||||
$scope.storeReferencesKeys = newKeys => {
|
||||
const oldKeys = $rootScope._references.keys
|
||||
return ($rootScope._references.keys = _.union(oldKeys, newKeys))
|
||||
}
|
||||
},
|
||||
])
|
||||
|
||||
App.component(
|
||||
'fileView',
|
||||
react2angular(rootContext.use(FileView), ['storeReferencesKeys', 'file'])
|
||||
)
|
|
@ -1,6 +0,0 @@
|
|||
import App from '../../../base'
|
||||
import { react2angular } from 'react2angular'
|
||||
import HistoryRoot from '../components/history-root'
|
||||
import { rootContext } from '../../../shared/context/root-context'
|
||||
|
||||
App.component('historyRoot', react2angular(rootContext.use(HistoryRoot)))
|
|
@ -27,9 +27,9 @@ type ConnectionContextValue = {
|
|||
disconnect: () => void
|
||||
}
|
||||
|
||||
const ConnectionContext = createContext<ConnectionContextValue | undefined>(
|
||||
undefined
|
||||
)
|
||||
export const ConnectionContext = createContext<
|
||||
ConnectionContextValue | undefined
|
||||
>(undefined)
|
||||
|
||||
export const ConnectionProvider: FC = ({ children }) => {
|
||||
const location = useLocation()
|
||||
|
|
|
@ -87,7 +87,9 @@ export type EditorScopeValue = {
|
|||
error_state: boolean
|
||||
}
|
||||
|
||||
const EditorManagerContext = createContext<EditorManager | undefined>(undefined)
|
||||
export const EditorManagerContext = createContext<EditorManager | undefined>(
|
||||
undefined
|
||||
)
|
||||
|
||||
export const EditorManagerProvider: FC = ({ children }) => {
|
||||
const { t } = useTranslation()
|
||||
|
|
|
@ -40,7 +40,7 @@ type IdeReactContextValue = {
|
|||
projectJoined: boolean
|
||||
}
|
||||
|
||||
const IdeReactContext = createContext<IdeReactContextValue | undefined>(
|
||||
export const IdeReactContext = createContext<IdeReactContextValue | undefined>(
|
||||
undefined
|
||||
)
|
||||
|
||||
|
@ -63,7 +63,7 @@ function populatePdfScope(store: ReactScopeValueStore) {
|
|||
store.allowNonExistentPath('pdf', true)
|
||||
}
|
||||
|
||||
function createReactScopeValueStore(projectId: string) {
|
||||
export function createReactScopeValueStore(projectId: string) {
|
||||
const scopeStore = new ReactScopeValueStore()
|
||||
|
||||
// Populate the scope value store with default values that will be used by
|
||||
|
|
|
@ -15,7 +15,6 @@ import _ from 'lodash'
|
|||
import { getJSON, postJSON } from '@/infrastructure/fetch-json'
|
||||
import { useOnlineUsersContext } from '@/features/ide-react/context/online-users-context'
|
||||
import { useEditorContext } from '@/shared/context/editor-context'
|
||||
import { useIdeContext } from '@/shared/context/ide-context'
|
||||
import useSocketListener from '@/features/ide-react/hooks/use-socket-listener'
|
||||
import useEventListener from '@/shared/hooks/use-event-listener'
|
||||
import { useModalsContext } from '@/features/ide-react/context/modals-context'
|
||||
|
@ -42,13 +41,12 @@ type MetadataContextValue = {
|
|||
|
||||
type DocMetadataResponse = { docId: string; meta: DocumentMetadata }
|
||||
|
||||
const MetadataContext = createContext<MetadataContextValue | undefined>(
|
||||
export const MetadataContext = createContext<MetadataContextValue | undefined>(
|
||||
undefined
|
||||
)
|
||||
|
||||
export const MetadataProvider: FC = ({ children }) => {
|
||||
const { t } = useTranslation()
|
||||
const ide = useIdeContext()
|
||||
const { eventEmitter, projectId } = useIdeReactContext()
|
||||
const { socket } = useConnectionContext()
|
||||
const { onlineUsersCount } = useOnlineUsersContext()
|
||||
|
@ -225,10 +223,6 @@ export const MetadataProvider: FC = ({ children }) => {
|
|||
[documents, getAllLabels, getAllPackages]
|
||||
)
|
||||
|
||||
// Expose metadataManager via ide object because useCodeMirrorScope relies on
|
||||
// it, for now
|
||||
ide.metadataManager = value
|
||||
|
||||
return (
|
||||
<MetadataContext.Provider value={value}>
|
||||
{children}
|
||||
|
|
|
@ -9,7 +9,9 @@ import {
|
|||
import useScopeValue from '@/shared/hooks/use-scope-value'
|
||||
import { DeepReadonly } from '../../../../../types/utils'
|
||||
|
||||
const PermissionsContext = createContext<Permissions | undefined>(undefined)
|
||||
export const PermissionsContext = createContext<Permissions | undefined>(
|
||||
undefined
|
||||
)
|
||||
|
||||
const permissionsMap: DeepReadonly<Record<PermissionsLevel, Permissions>> = {
|
||||
readOnly: {
|
||||
|
|
|
@ -40,10 +40,10 @@ export const ReactContextRoot: FC = ({ children }) => {
|
|||
<PermissionsProvider>
|
||||
<ProjectSettingsProvider>
|
||||
<LayoutProvider>
|
||||
<EditorManagerProvider>
|
||||
<LocalCompileProvider>
|
||||
<DetachCompileProvider>
|
||||
<ChatProvider>
|
||||
<EditorManagerProvider>
|
||||
<FileTreeOpenProvider>
|
||||
<OnlineUsersProvider>
|
||||
<MetadataProvider>
|
||||
|
@ -53,10 +53,10 @@ export const ReactContextRoot: FC = ({ children }) => {
|
|||
</MetadataProvider>
|
||||
</OnlineUsersProvider>
|
||||
</FileTreeOpenProvider>
|
||||
</EditorManagerProvider>
|
||||
</ChatProvider>
|
||||
</DetachCompileProvider>
|
||||
</LocalCompileProvider>
|
||||
</EditorManagerProvider>
|
||||
</LayoutProvider>
|
||||
</ProjectSettingsProvider>
|
||||
</PermissionsProvider>
|
||||
|
|
|
@ -170,10 +170,7 @@ function useReviewPanelState(): ReviewPanel.ReviewPanelState {
|
|||
ReviewPanel.Value<'commentThreads'>
|
||||
>({})
|
||||
const [entries, setEntries] = useState<ReviewPanel.Value<'entries'>>({})
|
||||
const [users, setUsers] = useScopeValue<ReviewPanel.Value<'users'>>(
|
||||
'users',
|
||||
true
|
||||
)
|
||||
const [users, setUsers] = useScopeValue<ReviewPanel.Value<'users'>>('users')
|
||||
const [resolvedComments, setResolvedComments] = useState<
|
||||
ReviewPanel.Value<'resolvedComments'>
|
||||
>({})
|
||||
|
@ -532,7 +529,6 @@ function useReviewPanelState(): ReviewPanel.ReviewPanelState {
|
|||
if (!user) {
|
||||
return 'anonymous'
|
||||
}
|
||||
// FIXME: check this
|
||||
if (project.owner._id === user.id) {
|
||||
return 'member'
|
||||
}
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
import {
|
||||
ScopeEventEmitter,
|
||||
ScopeEventName,
|
||||
} from '../../../../../types/ide/scope-event-emitter'
|
||||
import { Scope } from '../../../../../types/angular/scope'
|
||||
|
||||
export class AngularScopeEventEmitter implements ScopeEventEmitter {
|
||||
// eslint-disable-next-line no-useless-constructor
|
||||
constructor(readonly $scope: Scope) {}
|
||||
|
||||
emit(eventName: ScopeEventName, broadcast: boolean, ...detail: unknown[]) {
|
||||
if (broadcast) {
|
||||
this.$scope.$broadcast(eventName, ...detail)
|
||||
} else {
|
||||
this.$scope.$emit(eventName, ...detail)
|
||||
}
|
||||
}
|
||||
|
||||
on(eventName: ScopeEventName, listener: (...args: unknown[]) => void) {
|
||||
return this.$scope.$on(eventName, listener)
|
||||
}
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
import { ScopeValueStore } from '../../../../../types/ide/scope-value-store'
|
||||
import { Scope } from '../../../../../types/angular/scope'
|
||||
import _ from 'lodash'
|
||||
|
||||
export class AngularScopeValueStore implements ScopeValueStore {
|
||||
// eslint-disable-next-line no-useless-constructor
|
||||
constructor(readonly $scope: Scope) {}
|
||||
|
||||
get(path: string) {
|
||||
return _.get(this.$scope, path)
|
||||
}
|
||||
|
||||
set(path: string, value: unknown): void {
|
||||
this.$scope.$applyAsync(() => _.set(this.$scope, path, value))
|
||||
}
|
||||
|
||||
watch<T>(
|
||||
path: string,
|
||||
callback: (newValue: T) => void,
|
||||
deep: boolean
|
||||
): () => void {
|
||||
return this.$scope.$watch(
|
||||
path,
|
||||
(newValue: T) => callback(deep ? _.cloneDeep(newValue) : newValue),
|
||||
deep
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
import App from '@/base'
|
||||
import { react2angular } from 'react2angular'
|
||||
import { rootContext } from '@/shared/context/root-context'
|
||||
import { OutlineContainer } from '@/features/outline/components/outline-container'
|
||||
|
||||
App.component(
|
||||
'outlineContainer',
|
||||
react2angular(rootContext.use(OutlineContainer))
|
||||
)
|
|
@ -1,7 +1,7 @@
|
|||
import ReactDOM from 'react-dom'
|
||||
import PdfPreview from './pdf-preview'
|
||||
import { ContextRoot } from '../../../shared/context/root-context'
|
||||
import useWaitForI18n from '../../../shared/hooks/use-wait-for-i18n'
|
||||
import { ReactContextRoot } from '@/features/ide-react/context/react-context-root'
|
||||
|
||||
function PdfPreviewDetachedRoot() {
|
||||
const { isReady } = useWaitForI18n()
|
||||
|
@ -11,9 +11,9 @@ function PdfPreviewDetachedRoot() {
|
|||
}
|
||||
|
||||
return (
|
||||
<ContextRoot>
|
||||
<ReactContextRoot>
|
||||
<PdfPreview />
|
||||
</ContextRoot>
|
||||
</ReactContextRoot>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
import App from '../../../base'
|
||||
import { react2angular } from 'react2angular'
|
||||
|
||||
import PdfPreview from '../components/pdf-preview'
|
||||
import { rootContext } from '../../../shared/context/root-context'
|
||||
import {
|
||||
DefaultSynctexControl,
|
||||
DetacherSynctexControl,
|
||||
} from '../components/detach-synctex-control'
|
||||
|
||||
App.component('pdfPreview', react2angular(rootContext.use(PdfPreview), []))
|
||||
App.component(
|
||||
'pdfSynctexControls',
|
||||
react2angular(rootContext.use(DefaultSynctexControl), [])
|
||||
)
|
||||
App.component(
|
||||
'detacherSynctexControl',
|
||||
react2angular(rootContext.use(DetacherSynctexControl), [])
|
||||
)
|
|
@ -1,68 +0,0 @@
|
|||
import App from '../../../base'
|
||||
import { react2angular } from 'react2angular'
|
||||
|
||||
import ShareProjectModal from '../components/share-project-modal'
|
||||
import { rootContext } from '../../../shared/context/root-context'
|
||||
import { listProjectInvites, listProjectMembers } from '../utils/api'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
|
||||
App.component(
|
||||
'shareProjectModal',
|
||||
react2angular(rootContext.use(ShareProjectModal), [
|
||||
'animation',
|
||||
'handleHide',
|
||||
'show',
|
||||
])
|
||||
)
|
||||
|
||||
export default App.controller('ReactShareProjectModalController', [
|
||||
'$scope',
|
||||
'eventTracking',
|
||||
'ide',
|
||||
function ($scope, eventTracking, ide) {
|
||||
$scope.show = false
|
||||
|
||||
$scope.handleHide = () => {
|
||||
$scope.$applyAsync(() => {
|
||||
$scope.show = false
|
||||
})
|
||||
}
|
||||
|
||||
$scope.openShareProjectModal = () => {
|
||||
eventTracking.sendMBOnce('ide-open-share-modal-once')
|
||||
$scope.$applyAsync(() => {
|
||||
$scope.show = true
|
||||
})
|
||||
}
|
||||
|
||||
ide.socket.on('project:membership:changed', data => {
|
||||
if (data.members) {
|
||||
listProjectMembers($scope.project._id)
|
||||
.then(({ members }) => {
|
||||
if (members) {
|
||||
$scope.$applyAsync(() => {
|
||||
$scope.project.members = members
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
debugConsole.error('Error fetching members for project', err)
|
||||
})
|
||||
}
|
||||
|
||||
if (data.invites) {
|
||||
listProjectInvites($scope.project._id)
|
||||
.then(({ invites }) => {
|
||||
if (invites) {
|
||||
$scope.$applyAsync(() => {
|
||||
$scope.project.invites = invites
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
debugConsole.error('Error fetching invites for project', err)
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
])
|
|
@ -8,12 +8,10 @@ import {
|
|||
useReviewPanelUpdaterFnsContext,
|
||||
useReviewPanelValueContext,
|
||||
} from '../../../context/review-panel/review-panel-context'
|
||||
import { useIdeContext } from '@/shared/context/ide-context'
|
||||
import { useEditorContext } from '@/shared/context/editor-context'
|
||||
import { useCodeMirrorViewContext } from '../../codemirror-editor'
|
||||
import Modal, { useBulkActionsModal } from '../entries/bulk-actions-entry/modal'
|
||||
import getMeta from '../../../../../utils/meta'
|
||||
import useScopeValue from '../../../../../shared/hooks/use-scope-value'
|
||||
import useScopeEventListener from '@/shared/hooks/use-scope-event-listener'
|
||||
import { memo, useCallback } from 'react'
|
||||
import { useLayoutContext } from '@/shared/context/layout-context'
|
||||
|
@ -29,14 +27,8 @@ function EditorWidgets() {
|
|||
handleShowBulkRejectDialog,
|
||||
handleConfirmDialog,
|
||||
} = useBulkActionsModal()
|
||||
const { setIsAddingComment, handleSetSubview } =
|
||||
useReviewPanelUpdaterFnsContext()
|
||||
const { isReactIde } = useIdeContext()
|
||||
const { setIsAddingComment } = useReviewPanelUpdaterFnsContext()
|
||||
const { toggleReviewPanel } = useReviewPanelUpdaterFnsContext()
|
||||
const [addNewComment] =
|
||||
useScopeValue<(e: React.MouseEvent<HTMLButtonElement>) => void>(
|
||||
'addNewComment'
|
||||
)
|
||||
const view = useCodeMirrorViewContext()
|
||||
const { reviewPanelOpen } = useLayoutContext()
|
||||
const { isRestrictedTokenMember } = useEditorContext()
|
||||
|
@ -55,20 +47,9 @@ function EditorWidgets() {
|
|||
openDocId && openDocId in entries ? entries[openDocId] : undefined
|
||||
|
||||
const handleAddNewCommentClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
if (isReactIde) {
|
||||
e.preventDefault()
|
||||
setIsAddingComment(true)
|
||||
toggleReviewPanel()
|
||||
return
|
||||
}
|
||||
|
||||
addNewComment(e)
|
||||
setTimeout(() => {
|
||||
// Re-render the comment box in order to add autofocus every time
|
||||
handleSetSubview('cur_file')
|
||||
setIsAddingComment(false)
|
||||
setIsAddingComment(true)
|
||||
}, 0)
|
||||
}
|
||||
|
||||
useScopeEventListener(
|
||||
|
|
|
@ -8,26 +8,14 @@ import { isCurrentFileView } from '../../utils/sub-view'
|
|||
import { useLayoutContext } from '@/shared/context/layout-context'
|
||||
import classnames from 'classnames'
|
||||
import { lazy, memo } from 'react'
|
||||
import getMeta from '@/utils/meta'
|
||||
import { SubView } from '../../../../../../types/review-panel/review-panel'
|
||||
|
||||
const isReactIde: boolean = getMeta('ol-idePageReact')
|
||||
|
||||
type ReviewPanelViewProps = {
|
||||
parentDomNode: Element
|
||||
}
|
||||
|
||||
function ReviewPanelView({ parentDomNode }: ReviewPanelViewProps) {
|
||||
const { subView } = useReviewPanelValueContext()
|
||||
|
||||
return ReactDOM.createPortal(
|
||||
isReactIde ? (
|
||||
<ReviewPanelContainer />
|
||||
) : (
|
||||
<ReviewPanelContent subView={subView} />
|
||||
),
|
||||
parentDomNode
|
||||
)
|
||||
return ReactDOM.createPortal(<ReviewPanelContainer />, parentDomNode)
|
||||
}
|
||||
|
||||
const ReviewPanelContainer = memo(() => {
|
||||
|
@ -65,10 +53,9 @@ const ReviewPanelContent = memo<{ subView: SubView }>(({ subView }) => (
|
|||
))
|
||||
ReviewPanelContent.displayName = 'ReviewPanelContent'
|
||||
|
||||
const ReviewPanelProvider = lazy(() =>
|
||||
isReactIde
|
||||
? import('@/features/ide-react/context/review-panel/review-panel-provider')
|
||||
: import('../../context/review-panel/review-panel-provider')
|
||||
const ReviewPanelProvider = lazy(
|
||||
() =>
|
||||
import('@/features/ide-react/context/review-panel/review-panel-provider')
|
||||
)
|
||||
|
||||
function ReviewPanel() {
|
||||
|
|
|
@ -1,275 +0,0 @@
|
|||
import { useState, useMemo, useCallback } from 'react'
|
||||
import useScopeValue from '../../../../../shared/hooks/use-scope-value'
|
||||
import useLayoutToLeft from '@/features/ide-react/context/review-panel/hooks/useLayoutToLeft'
|
||||
import { sendMB } from '../../../../../infrastructure/event-tracking'
|
||||
import type * as ReviewPanel from '../types/review-panel-state'
|
||||
import {
|
||||
SubView,
|
||||
ThreadId,
|
||||
} from '../../../../../../../types/review-panel/review-panel'
|
||||
import { DocId } from '../../../../../../../types/project-settings'
|
||||
import { dispatchReviewPanelLayout as handleLayoutChange } from '../../../extensions/changes/change-manager'
|
||||
|
||||
function useAngularReviewPanelState(): ReviewPanel.ReviewPanelState {
|
||||
const [subView, setSubView] = useScopeValue<ReviewPanel.Value<'subView'>>(
|
||||
'reviewPanel.subView'
|
||||
)
|
||||
const [isOverviewLoading] = useScopeValue<
|
||||
ReviewPanel.Value<'isOverviewLoading'>
|
||||
>('reviewPanel.overview.loading')
|
||||
const [nVisibleSelectedChanges] = useScopeValue<
|
||||
ReviewPanel.Value<'nVisibleSelectedChanges'>
|
||||
>('reviewPanel.nVisibleSelectedChanges')
|
||||
const [collapsed, setCollapsed] = useScopeValue<
|
||||
ReviewPanel.Value<'collapsed'>
|
||||
>('reviewPanel.overview.docsCollapsedState')
|
||||
const [commentThreads] = useScopeValue<ReviewPanel.Value<'commentThreads'>>(
|
||||
'reviewPanel.commentThreads',
|
||||
true
|
||||
)
|
||||
const [entries] = useScopeValue<ReviewPanel.Value<'entries'>>(
|
||||
'reviewPanel.entries',
|
||||
true
|
||||
)
|
||||
const [loadingThreads] =
|
||||
useScopeValue<ReviewPanel.Value<'loadingThreads'>>('loadingThreads')
|
||||
|
||||
const [permissions] =
|
||||
useScopeValue<ReviewPanel.Value<'permissions'>>('permissions')
|
||||
const [users] = useScopeValue<ReviewPanel.Value<'users'>>('users', true)
|
||||
const [resolvedComments] = useScopeValue<
|
||||
ReviewPanel.Value<'resolvedComments'>
|
||||
>('reviewPanel.resolvedComments', true)
|
||||
|
||||
const [wantTrackChanges] = useScopeValue<
|
||||
ReviewPanel.Value<'wantTrackChanges'>
|
||||
>('editor.wantTrackChanges')
|
||||
const [openDocId] =
|
||||
useScopeValue<ReviewPanel.Value<'openDocId'>>('editor.open_doc_id')
|
||||
const [shouldCollapse, setShouldCollapse] = useScopeValue<
|
||||
ReviewPanel.Value<'shouldCollapse'>
|
||||
>('reviewPanel.fullTCStateCollapsed')
|
||||
const [lineHeight] = useScopeValue<number>(
|
||||
'reviewPanel.rendererData.lineHeight'
|
||||
)
|
||||
|
||||
const [toggleTrackChangesForEveryone] = useScopeValue<
|
||||
ReviewPanel.UpdaterFn<'toggleTrackChangesForEveryone'>
|
||||
>('toggleTrackChangesForEveryone')
|
||||
const [toggleTrackChangesForUser] = useScopeValue<
|
||||
ReviewPanel.UpdaterFn<'toggleTrackChangesForUser'>
|
||||
>('toggleTrackChangesForUser')
|
||||
const [toggleTrackChangesForGuests] = useScopeValue<
|
||||
ReviewPanel.UpdaterFn<'toggleTrackChangesForGuests'>
|
||||
>('toggleTrackChangesForGuests')
|
||||
|
||||
const [trackChangesState] = useScopeValue<
|
||||
ReviewPanel.Value<'trackChangesState'>
|
||||
>('reviewPanel.trackChangesState')
|
||||
const [trackChangesOnForEveryone] = useScopeValue<
|
||||
ReviewPanel.Value<'trackChangesOnForEveryone'>
|
||||
>('reviewPanel.trackChangesOnForEveryone')
|
||||
const [trackChangesOnForGuests] = useScopeValue<
|
||||
ReviewPanel.Value<'trackChangesOnForGuests'>
|
||||
>('reviewPanel.trackChangesOnForGuests')
|
||||
const [trackChangesForGuestsAvailable] = useScopeValue<
|
||||
ReviewPanel.Value<'trackChangesForGuestsAvailable'>
|
||||
>('reviewPanel.trackChangesForGuestsAvailable')
|
||||
const [resolveComment] =
|
||||
useScopeValue<ReviewPanel.UpdaterFn<'resolveComment'>>('resolveComment')
|
||||
const [submitNewComment] =
|
||||
useScopeValue<ReviewPanel.UpdaterFn<'submitNewComment'>>('submitNewComment')
|
||||
const [deleteComment] =
|
||||
useScopeValue<ReviewPanel.UpdaterFn<'deleteComment'>>('deleteComment')
|
||||
const [gotoEntry] =
|
||||
useScopeValue<ReviewPanel.UpdaterFn<'gotoEntry'>>('gotoEntry')
|
||||
const [saveEdit] =
|
||||
useScopeValue<ReviewPanel.UpdaterFn<'saveEdit'>>('saveEdit')
|
||||
const [submitReplyAngular] =
|
||||
useScopeValue<
|
||||
(entry: { thread_id: ThreadId; replyContent: string }) => void
|
||||
>('submitReply')
|
||||
|
||||
const [formattedProjectMembers] = useScopeValue<
|
||||
ReviewPanel.Value<'formattedProjectMembers'>
|
||||
>('reviewPanel.formattedProjectMembers')
|
||||
|
||||
const [toggleReviewPanel] =
|
||||
useScopeValue<ReviewPanel.UpdaterFn<'toggleReviewPanel'>>(
|
||||
'toggleReviewPanel'
|
||||
)
|
||||
const [unresolveComment] =
|
||||
useScopeValue<ReviewPanel.UpdaterFn<'unresolveComment'>>('unresolveComment')
|
||||
|
||||
const [deleteThreadAngular] =
|
||||
useScopeValue<
|
||||
(
|
||||
_: unknown,
|
||||
...args: [...Parameters<ReviewPanel.UpdaterFn<'deleteThread'>>]
|
||||
) => ReturnType<ReviewPanel.UpdaterFn<'deleteThread'>>
|
||||
>('deleteThread')
|
||||
const deleteThread = useCallback(
|
||||
(docId: DocId, threadId: ThreadId) => {
|
||||
deleteThreadAngular(undefined, docId, threadId)
|
||||
},
|
||||
[deleteThreadAngular]
|
||||
)
|
||||
|
||||
const [refreshResolvedCommentsDropdown] = useScopeValue<
|
||||
ReviewPanel.UpdaterFn<'refreshResolvedCommentsDropdown'>
|
||||
>('refreshResolvedCommentsDropdown')
|
||||
const [acceptChanges] =
|
||||
useScopeValue<ReviewPanel.UpdaterFn<'acceptChanges'>>('acceptChanges')
|
||||
const [rejectChanges] =
|
||||
useScopeValue<ReviewPanel.UpdaterFn<'rejectChanges'>>('rejectChanges')
|
||||
const [bulkAcceptActions] =
|
||||
useScopeValue<ReviewPanel.UpdaterFn<'bulkAcceptActions'>>(
|
||||
'bulkAcceptActions'
|
||||
)
|
||||
const [bulkRejectActions] =
|
||||
useScopeValue<ReviewPanel.UpdaterFn<'bulkRejectActions'>>(
|
||||
'bulkRejectActions'
|
||||
)
|
||||
|
||||
const layoutToLeft = useLayoutToLeft('#editor')
|
||||
|
||||
const handleSetSubview = useCallback(
|
||||
(subView: SubView) => {
|
||||
setSubView(subView)
|
||||
sendMB('rp-subview-change', { subView })
|
||||
},
|
||||
[setSubView]
|
||||
)
|
||||
|
||||
const submitReply = useCallback(
|
||||
(threadId: ThreadId, replyContent: string) => {
|
||||
submitReplyAngular({ thread_id: threadId, replyContent })
|
||||
},
|
||||
[submitReplyAngular]
|
||||
)
|
||||
|
||||
const [isAddingComment, setIsAddingComment] = useState(false)
|
||||
const [navHeight, setNavHeight] = useState(0)
|
||||
const [toolbarHeight, setToolbarHeight] = useState(0)
|
||||
const [layoutSuspended, setLayoutSuspended] = useState(false)
|
||||
const [unsavedComment, setUnsavedComment] = useState('')
|
||||
|
||||
const values = useMemo<ReviewPanel.ReviewPanelState['values']>(
|
||||
() => ({
|
||||
collapsed,
|
||||
commentThreads,
|
||||
entries,
|
||||
isAddingComment,
|
||||
loadingThreads,
|
||||
nVisibleSelectedChanges,
|
||||
permissions,
|
||||
users,
|
||||
resolvedComments,
|
||||
shouldCollapse,
|
||||
navHeight,
|
||||
toolbarHeight,
|
||||
subView,
|
||||
wantTrackChanges,
|
||||
isOverviewLoading,
|
||||
openDocId,
|
||||
lineHeight,
|
||||
trackChangesState,
|
||||
trackChangesOnForEveryone,
|
||||
trackChangesOnForGuests,
|
||||
trackChangesForGuestsAvailable,
|
||||
formattedProjectMembers,
|
||||
layoutSuspended,
|
||||
unsavedComment,
|
||||
layoutToLeft,
|
||||
}),
|
||||
[
|
||||
collapsed,
|
||||
commentThreads,
|
||||
entries,
|
||||
isAddingComment,
|
||||
loadingThreads,
|
||||
nVisibleSelectedChanges,
|
||||
permissions,
|
||||
users,
|
||||
resolvedComments,
|
||||
shouldCollapse,
|
||||
navHeight,
|
||||
toolbarHeight,
|
||||
subView,
|
||||
wantTrackChanges,
|
||||
isOverviewLoading,
|
||||
openDocId,
|
||||
lineHeight,
|
||||
trackChangesState,
|
||||
trackChangesOnForEveryone,
|
||||
trackChangesOnForGuests,
|
||||
trackChangesForGuestsAvailable,
|
||||
formattedProjectMembers,
|
||||
layoutSuspended,
|
||||
unsavedComment,
|
||||
layoutToLeft,
|
||||
]
|
||||
)
|
||||
|
||||
const updaterFns = useMemo<ReviewPanel.ReviewPanelState['updaterFns']>(
|
||||
() => ({
|
||||
handleSetSubview,
|
||||
handleLayoutChange,
|
||||
gotoEntry,
|
||||
resolveComment,
|
||||
submitReply,
|
||||
acceptChanges,
|
||||
rejectChanges,
|
||||
toggleReviewPanel,
|
||||
bulkAcceptActions,
|
||||
bulkRejectActions,
|
||||
saveEdit,
|
||||
submitNewComment,
|
||||
deleteComment,
|
||||
unresolveComment,
|
||||
refreshResolvedCommentsDropdown,
|
||||
deleteThread,
|
||||
toggleTrackChangesForEveryone,
|
||||
toggleTrackChangesForUser,
|
||||
toggleTrackChangesForGuests,
|
||||
setCollapsed,
|
||||
setShouldCollapse,
|
||||
setIsAddingComment,
|
||||
setNavHeight,
|
||||
setToolbarHeight,
|
||||
setLayoutSuspended,
|
||||
setUnsavedComment,
|
||||
}),
|
||||
[
|
||||
handleSetSubview,
|
||||
gotoEntry,
|
||||
resolveComment,
|
||||
submitReply,
|
||||
acceptChanges,
|
||||
rejectChanges,
|
||||
toggleReviewPanel,
|
||||
bulkAcceptActions,
|
||||
bulkRejectActions,
|
||||
saveEdit,
|
||||
submitNewComment,
|
||||
deleteComment,
|
||||
unresolveComment,
|
||||
refreshResolvedCommentsDropdown,
|
||||
deleteThread,
|
||||
toggleTrackChangesForEveryone,
|
||||
toggleTrackChangesForUser,
|
||||
toggleTrackChangesForGuests,
|
||||
setCollapsed,
|
||||
setShouldCollapse,
|
||||
setIsAddingComment,
|
||||
setNavHeight,
|
||||
setToolbarHeight,
|
||||
setLayoutSuspended,
|
||||
setUnsavedComment,
|
||||
]
|
||||
)
|
||||
|
||||
return { values, updaterFns }
|
||||
}
|
||||
|
||||
export default useAngularReviewPanelState
|
|
@ -1,20 +0,0 @@
|
|||
import useAngularReviewPanelState from '@/features/source-editor/context/review-panel/hooks/use-angular-review-panel-state'
|
||||
import { FC } from 'react'
|
||||
import {
|
||||
ReviewPanelUpdaterFnsContext,
|
||||
ReviewPanelValueContext,
|
||||
} from './review-panel-context'
|
||||
|
||||
const ReviewPanelProvider: FC = ({ children }) => {
|
||||
const { values, updaterFns } = useAngularReviewPanelState()
|
||||
|
||||
return (
|
||||
<ReviewPanelValueContext.Provider value={values}>
|
||||
<ReviewPanelUpdaterFnsContext.Provider value={updaterFns}>
|
||||
{children}
|
||||
</ReviewPanelUpdaterFnsContext.Provider>
|
||||
</ReviewPanelValueContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export default ReviewPanelProvider
|
|
@ -1,9 +0,0 @@
|
|||
import App from '../../../base'
|
||||
import { react2angular } from 'react2angular'
|
||||
import { rootContext } from '../../../shared/context/root-context'
|
||||
import GrammarlyAdvert from '../components/grammarly-advert'
|
||||
|
||||
App.component(
|
||||
'grammarlyAdvert',
|
||||
react2angular(rootContext.use(GrammarlyAdvert))
|
||||
)
|
|
@ -1,6 +0,0 @@
|
|||
import { react2angular } from 'react2angular'
|
||||
import SourceEditor from '../components/source-editor'
|
||||
import App from '../../../base'
|
||||
import { rootContext } from '../../../shared/context/root-context'
|
||||
|
||||
App.component('sourceEditor', react2angular(rootContext.use(SourceEditor), []))
|
|
@ -26,7 +26,6 @@ import {
|
|||
setMetadata,
|
||||
setSyntaxValidation,
|
||||
} from '../extensions/language'
|
||||
import { useIdeContext } from '../../../shared/context/ide-context'
|
||||
import { restoreScrollPosition } from '../extensions/scroll-position'
|
||||
import { setEditable } from '../extensions/editable'
|
||||
import { useFileTreeData } from '../../../shared/context/file-tree-data-context'
|
||||
|
@ -59,10 +58,9 @@ import grammarlyExtensionPresent from '@/shared/utils/grammarly'
|
|||
import { DocumentContainer } from '@/features/ide-react/editor/document-container'
|
||||
import { useLayoutContext } from '@/shared/context/layout-context'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
import { useMetadataContext } from '@/features/ide-react/context/metadata-context'
|
||||
|
||||
function useCodeMirrorScope(view: EditorView) {
|
||||
const ide = useIdeContext()
|
||||
|
||||
const { fileTreeData } = useFileTreeData()
|
||||
|
||||
const [permissions] = useScopeValue<{ write: boolean }>('permissions')
|
||||
|
@ -74,6 +72,8 @@ function useCodeMirrorScope(view: EditorView) {
|
|||
|
||||
const { reviewPanelOpen, miniReviewPanelVisible } = useLayoutContext()
|
||||
|
||||
const { metadata } = useMetadataContext()
|
||||
|
||||
const [loadingThreads] = useScopeValue<boolean>('loadingThreads')
|
||||
|
||||
const [currentDoc] = useScopeValue<DocumentContainer | null>(
|
||||
|
@ -211,22 +211,16 @@ function useCodeMirrorScope(view: EditorView) {
|
|||
// set the project metadata, mostly for use in autocomplete
|
||||
// TODO: read this data from the scope?
|
||||
const metadataRef = useRef({
|
||||
documents: ide.metadataManager.metadata.state.documents,
|
||||
documents: metadata.state.documents,
|
||||
references: references.keys,
|
||||
fileTreeData,
|
||||
})
|
||||
|
||||
// listen to project metadata (docs + packages) updates
|
||||
useEffect(() => {
|
||||
const listener = (event: Event) => {
|
||||
metadataRef.current.documents = (
|
||||
event as CustomEvent<Record<string, any>>
|
||||
).detail
|
||||
metadataRef.current.documents = metadata.state.documents
|
||||
view.dispatch(setMetadata(metadataRef.current))
|
||||
}
|
||||
window.addEventListener('project:metadata', listener)
|
||||
return () => window.removeEventListener('project:metadata', listener)
|
||||
}, [view])
|
||||
}, [view, metadata.state.documents])
|
||||
|
||||
// listen to project reference keys updates
|
||||
useEffect(() => {
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
import '../controllers/source-editor-controller'
|
|
@ -1,422 +0,0 @@
|
|||
/* eslint-disable
|
||||
camelcase,
|
||||
max-len,
|
||||
no-cond-assign,
|
||||
no-return-assign,
|
||||
no-unused-vars,
|
||||
no-useless-escape,
|
||||
*/
|
||||
// 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__
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
import App from './base'
|
||||
import FileTreeManager from './ide/file-tree/FileTreeManager'
|
||||
import LoadingManager from './ide/LoadingManager'
|
||||
import ConnectionManager from './ide/connection/ConnectionManager'
|
||||
import EditorManager from './ide/editor/EditorManager'
|
||||
import OnlineUsersManager from './ide/online-users/OnlineUsersManager'
|
||||
import PermissionsManager from './ide/permissions/PermissionsManager'
|
||||
import BinaryFilesManager from './ide/binary-files/BinaryFilesManager'
|
||||
import ReferencesManager from './ide/references/ReferencesManager'
|
||||
import MetadataManager from './ide/metadata/MetadataManager'
|
||||
import './ide/review-panel/ReviewPanelManager'
|
||||
import './ide/cobranding/CobrandingDataService'
|
||||
import './ide/chat/index'
|
||||
import './ide/file-view/index'
|
||||
import './ide/toolbar/index'
|
||||
import './ide/directives/layout'
|
||||
import './ide/directives/verticalResizablePanes'
|
||||
import './ide/services/ide'
|
||||
import './services/queued-http' // used in FileTreeManager
|
||||
import './main/event' // used in various controllers
|
||||
import './main/system-messages' // used in project/editor
|
||||
import '../../modules/modules-ide'
|
||||
import './features/source-editor/ide'
|
||||
import './shared/context/controllers/root-context-controller'
|
||||
import './features/editor-navigation-toolbar/controllers/editor-navigation-toolbar-controller'
|
||||
import './features/pdf-preview/controllers/pdf-preview-controller'
|
||||
import './features/share-project-modal/controllers/react-share-project-modal-controller'
|
||||
import './features/source-editor/controllers/grammarly-advert-controller'
|
||||
import './features/history/controllers/history-controller'
|
||||
import './features/editor-left-menu/controllers/editor-left-menu-controller'
|
||||
import './features/outline/controllers/outline-controller'
|
||||
import { cleanupServiceWorker } from './utils/service-worker-cleanup'
|
||||
import { reportCM6Perf } from './infrastructure/cm6-performance'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
|
||||
App.controller('IdeController', [
|
||||
'$scope',
|
||||
'$timeout',
|
||||
'ide',
|
||||
'localStorage',
|
||||
'eventTracking',
|
||||
'metadata',
|
||||
'CobrandingDataService',
|
||||
'$window',
|
||||
function (
|
||||
$scope,
|
||||
$timeout,
|
||||
ide,
|
||||
localStorage,
|
||||
eventTracking,
|
||||
metadata,
|
||||
CobrandingDataService,
|
||||
$window
|
||||
) {
|
||||
// Don't freak out if we're already in an apply callback
|
||||
let err, pdfLayout, userAgent
|
||||
$scope.$originalApply = $scope.$apply
|
||||
$scope.$apply = function (fn) {
|
||||
if (fn == null) {
|
||||
fn = function () {}
|
||||
}
|
||||
const phase = this.$root.$$phase
|
||||
if (phase === '$apply' || phase === '$digest') {
|
||||
return fn()
|
||||
} else {
|
||||
return this.$originalApply(fn)
|
||||
}
|
||||
}
|
||||
|
||||
$scope.state = {
|
||||
loading: true,
|
||||
load_progress: 40,
|
||||
error: null,
|
||||
}
|
||||
$scope.ui = {
|
||||
leftMenuShown: false,
|
||||
view: 'editor',
|
||||
chatOpen: false,
|
||||
pdfLayout: 'sideBySide',
|
||||
pdfHidden: false,
|
||||
pdfWidth: 0,
|
||||
reviewPanelOpen: localStorage(`ui.reviewPanelOpen.${window.project_id}`),
|
||||
miniReviewPanelVisible: false,
|
||||
chatResizerSizeOpen: 7,
|
||||
chatResizerSizeClosed: 0,
|
||||
}
|
||||
$scope.user = window.user
|
||||
|
||||
$scope.settings = window.userSettings
|
||||
$scope.anonymous = window.anonymous
|
||||
$scope.isTokenMember = window.isTokenMember
|
||||
$scope.isRestrictedTokenMember = window.isRestrictedTokenMember
|
||||
|
||||
$scope.cobranding = {
|
||||
isProjectCobranded: CobrandingDataService.isProjectCobranded(),
|
||||
logoImgUrl: CobrandingDataService.getLogoImgUrl(),
|
||||
submitBtnHtml: CobrandingDataService.getSubmitBtnHtml(),
|
||||
brandVariationName: CobrandingDataService.getBrandVariationName(),
|
||||
brandVariationHomeUrl: CobrandingDataService.getBrandVariationHomeUrl(),
|
||||
}
|
||||
|
||||
$scope.chat = {}
|
||||
|
||||
ide.toggleReviewPanel = $scope.toggleReviewPanel = function () {
|
||||
$scope.$applyAsync(() => {
|
||||
if (!$scope.project.features.trackChangesVisible) {
|
||||
return
|
||||
}
|
||||
$scope.ui.reviewPanelOpen = !$scope.ui.reviewPanelOpen
|
||||
eventTracking.sendMB('rp-toggle-panel', {
|
||||
value: $scope.ui.reviewPanelOpen,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
$scope.$watch('ui.reviewPanelOpen', function (value) {
|
||||
if (value != null) {
|
||||
return localStorage(`ui.reviewPanelOpen.${window.project_id}`, value)
|
||||
}
|
||||
})
|
||||
|
||||
$scope.$on('layout:pdf:resize', function (_, layoutState) {
|
||||
$scope.ui.pdfHidden = layoutState.east.initClosed
|
||||
return ($scope.ui.pdfWidth = layoutState.east.size)
|
||||
})
|
||||
|
||||
$scope.$watch('ui.view', function (newView, oldView) {
|
||||
if (newView !== oldView) {
|
||||
$scope.$broadcast('layout:flat-screen:toggle')
|
||||
}
|
||||
if (newView != null && newView !== 'editor' && newView !== 'pdf') {
|
||||
eventTracking.sendMBOnce(`ide-open-view-${newView}-once`)
|
||||
}
|
||||
})
|
||||
|
||||
$scope.$watch('ui.chatOpen', function (isOpen) {
|
||||
if (isOpen) {
|
||||
eventTracking.sendMBOnce('ide-open-chat-once')
|
||||
}
|
||||
})
|
||||
|
||||
$scope.$watch('ui.leftMenuShown', function (isOpen) {
|
||||
if (isOpen) {
|
||||
eventTracking.sendMBOnce('ide-open-left-menu-once')
|
||||
}
|
||||
})
|
||||
|
||||
$scope.trackHover = feature => {
|
||||
eventTracking.sendMBOnce(`ide-hover-${feature}-once`)
|
||||
}
|
||||
// End of tracking code.
|
||||
|
||||
window._ide = ide
|
||||
|
||||
ide.validFileRegex = '^[^*/]*$' // Don't allow * and /
|
||||
|
||||
ide.project_id = $scope.project_id = window.project_id
|
||||
ide.$scope = $scope
|
||||
|
||||
ide.referencesSearchManager = new ReferencesManager(ide, $scope)
|
||||
ide.loadingManager = new LoadingManager($scope)
|
||||
ide.connectionManager = new ConnectionManager(ide, $scope)
|
||||
ide.fileTreeManager = new FileTreeManager(ide, $scope)
|
||||
ide.editorManager = new EditorManager(
|
||||
ide,
|
||||
$scope,
|
||||
localStorage,
|
||||
eventTracking
|
||||
)
|
||||
ide.onlineUsersManager = new OnlineUsersManager(ide, $scope)
|
||||
ide.permissionsManager = new PermissionsManager(ide, $scope)
|
||||
ide.binaryFilesManager = new BinaryFilesManager(ide, $scope)
|
||||
ide.metadataManager = new MetadataManager(ide, $scope, metadata)
|
||||
|
||||
let inited = false
|
||||
$scope.$on('project:joined', function () {
|
||||
if (inited) {
|
||||
return
|
||||
}
|
||||
inited = true
|
||||
if (
|
||||
__guard__(
|
||||
$scope != null ? $scope.project : undefined,
|
||||
x => x.deletedByExternalDataSource
|
||||
)
|
||||
) {
|
||||
ide.showGenericMessageModal(
|
||||
'Project Renamed or Deleted',
|
||||
`\
|
||||
This project has either been renamed or deleted by an external data source such as Dropbox.
|
||||
We don't want to delete your data on Overleaf, so this project still contains your history and collaborators.
|
||||
If the project has been renamed please look in your project list for a new project under the new name.\
|
||||
`
|
||||
)
|
||||
}
|
||||
return $timeout(function () {
|
||||
if ($scope.permissions.write) {
|
||||
let _labelsInitialLoadDone
|
||||
ide.metadataManager.loadProjectMetaFromServer()
|
||||
return (_labelsInitialLoadDone = true)
|
||||
}
|
||||
}, 200)
|
||||
})
|
||||
|
||||
// Count the first 'doc:opened' as a sign that the ide is loaded
|
||||
// and broadcast a message. This is a good event to listen for
|
||||
// if you want to wait until the ide is fully loaded and initialized
|
||||
let _loaded = false
|
||||
$scope.$on('doc:opened', function () {
|
||||
if (_loaded) {
|
||||
return
|
||||
}
|
||||
$scope.$broadcast('ide:loaded')
|
||||
return (_loaded = true)
|
||||
})
|
||||
|
||||
ide.editingSessionHeartbeat = () => {
|
||||
eventTracking.editingSessionHeartbeat(() => {
|
||||
const editorType = ide.editorManager.getEditorType()
|
||||
|
||||
const segmentation = {
|
||||
editorType,
|
||||
}
|
||||
|
||||
if (editorType === 'cm6' || editorType === 'cm6-rich-text') {
|
||||
const cm6PerfData = reportCM6Perf()
|
||||
|
||||
// Ignore if no typing has happened
|
||||
if (cm6PerfData.numberOfEntries > 0) {
|
||||
const perfProps = [
|
||||
'Max',
|
||||
'Mean',
|
||||
'Median',
|
||||
'NinetyFifthPercentile',
|
||||
'DocLength',
|
||||
'NumberOfEntries',
|
||||
'MaxUserEventsBetweenDomUpdates',
|
||||
'Grammarly',
|
||||
'SessionLength',
|
||||
'Memory',
|
||||
'Lags',
|
||||
'NonLags',
|
||||
'LongestLag',
|
||||
'MeanLagsPerMeasure',
|
||||
'MeanKeypressesPerMeasure',
|
||||
'MeanKeypressPaint',
|
||||
'LongTasks',
|
||||
'Release',
|
||||
]
|
||||
|
||||
for (const prop of perfProps) {
|
||||
const perfValue =
|
||||
cm6PerfData[prop.charAt(0).toLowerCase() + prop.slice(1)]
|
||||
if (perfValue !== null) {
|
||||
segmentation['cm6Perf' + prop] = perfValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return segmentation
|
||||
})
|
||||
}
|
||||
|
||||
$scope.$on('cursor:editor:update', () => {
|
||||
ide.editingSessionHeartbeat()
|
||||
})
|
||||
$scope.$on('scroll:editor:update', () => {
|
||||
ide.editingSessionHeartbeat()
|
||||
})
|
||||
|
||||
angular.element($window).on('click', ide.editingSessionHeartbeat)
|
||||
|
||||
$scope.$on('$destroy', () =>
|
||||
angular.element($window).off('click', ide.editingSessionHeartbeat)
|
||||
)
|
||||
|
||||
const DARK_THEMES = [
|
||||
'ambiance',
|
||||
'chaos',
|
||||
'clouds_midnight',
|
||||
'cobalt',
|
||||
'idle_fingers',
|
||||
'merbivore',
|
||||
'merbivore_soft',
|
||||
'mono_industrial',
|
||||
'monokai',
|
||||
'pastel_on_dark',
|
||||
'solarized_dark',
|
||||
'terminal',
|
||||
'tomorrow_night',
|
||||
'tomorrow_night_blue',
|
||||
'tomorrow_night_bright',
|
||||
'tomorrow_night_eighties',
|
||||
'twilight',
|
||||
'vibrant_ink',
|
||||
]
|
||||
$scope.darkTheme = false
|
||||
// Listen for settings change from React
|
||||
window.addEventListener('settings:change', event => {
|
||||
$scope.darkTheme = DARK_THEMES.includes(event.detail.editorTheme)
|
||||
})
|
||||
|
||||
ide.localStorage = localStorage
|
||||
|
||||
$scope.switchToFlatLayout = function (view) {
|
||||
$scope.ui.pdfLayout = 'flat'
|
||||
$scope.ui.view = view
|
||||
return ide.localStorage('pdf.layout', 'flat')
|
||||
}
|
||||
|
||||
$scope.switchToSideBySideLayout = function (view) {
|
||||
$scope.ui.pdfLayout = 'sideBySide'
|
||||
$scope.ui.view = view
|
||||
return localStorage('pdf.layout', 'split')
|
||||
}
|
||||
|
||||
if ((pdfLayout = localStorage('pdf.layout'))) {
|
||||
if (pdfLayout === 'split') {
|
||||
$scope.switchToSideBySideLayout()
|
||||
}
|
||||
if (pdfLayout === 'flat') {
|
||||
$scope.switchToFlatLayout()
|
||||
}
|
||||
} else {
|
||||
$scope.switchToSideBySideLayout()
|
||||
}
|
||||
|
||||
// Update ui.pdfOpen when the layout changes.
|
||||
// The east pane should open when the layout changes from "Editor only" or "PDF only" to "Editor & PDF".
|
||||
$scope.$watch('ui.pdfLayout', value => {
|
||||
$scope.ui.pdfOpen = value === 'sideBySide'
|
||||
})
|
||||
|
||||
// Update ui.pdfLayout when the east pane is toggled.
|
||||
// The layout should be set to "Editor & PDF" (sideBySide) when the east pane is opened, and "Editor only" (flat) when the east pane is closed.
|
||||
$scope.$watch('ui.pdfOpen', value => {
|
||||
$scope.ui.pdfLayout = value ? 'sideBySide' : 'flat'
|
||||
if (value) {
|
||||
window.dispatchEvent(new CustomEvent('ui:pdf-open'))
|
||||
}
|
||||
})
|
||||
|
||||
$scope.handleKeyDown = () => {
|
||||
// unused?
|
||||
}
|
||||
|
||||
// User can append ?ft=somefeature to url to activate a feature toggle
|
||||
ide.featureToggle = __guard__(
|
||||
__guard__(
|
||||
typeof location !== 'undefined' && location !== null
|
||||
? location.search
|
||||
: undefined,
|
||||
x1 => x1.match(/^\?ft=(\w+)$/)
|
||||
),
|
||||
x => x[1]
|
||||
)
|
||||
|
||||
// Listen for editor:lint event from CM6 linter
|
||||
window.addEventListener('editor:lint', event => {
|
||||
$scope.hasLintingError = event.detail.hasLintingError
|
||||
})
|
||||
|
||||
ide.socket.on('project:access:revoked', () => {
|
||||
ide.showGenericMessageModal(
|
||||
'Removed From Project',
|
||||
'You have been removed from this project, and will no longer have access to it. You will be redirected to your project dashboard momentarily.'
|
||||
)
|
||||
})
|
||||
|
||||
return ide.socket.on('project:publicAccessLevel:changed', data => {
|
||||
if (data.newAccessLevel != null) {
|
||||
ide.$scope.project.publicAccesLevel = data.newAccessLevel
|
||||
return $scope.$digest()
|
||||
}
|
||||
})
|
||||
},
|
||||
])
|
||||
|
||||
cleanupServiceWorker()
|
||||
|
||||
angular.module('OverleafApp').config([
|
||||
'$provide',
|
||||
function ($provide) {
|
||||
$provide.decorator('$browser', [
|
||||
'$delegate',
|
||||
function ($delegate) {
|
||||
$delegate.onUrlChange = function () {}
|
||||
$delegate.url = function () {
|
||||
return ''
|
||||
}
|
||||
return $delegate
|
||||
},
|
||||
])
|
||||
},
|
||||
])
|
||||
|
||||
export default angular.bootstrap(document.body, ['OverleafApp'])
|
||||
|
||||
function __guard__(value, transform) {
|
||||
return typeof value !== 'undefined' && value !== null
|
||||
? transform(value)
|
||||
: undefined
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
import i18n from '../i18n'
|
||||
|
||||
// Control the editor loading screen. We want to show the loading screen until
|
||||
// both the websocket connection has been established (so that the editor is in
|
||||
// the correct state) and the translations have been loaded (so we don't see a
|
||||
// flash of untranslated text).
|
||||
class LoadingManager {
|
||||
constructor($scope) {
|
||||
this.$scope = $scope
|
||||
|
||||
const socketPromise = new Promise(resolve => {
|
||||
this.resolveSocketPromise = resolve
|
||||
})
|
||||
|
||||
Promise.all([socketPromise, i18n])
|
||||
.then(() => {
|
||||
this.$scope.$apply(() => {
|
||||
this.$scope.state.load_progress = 100
|
||||
this.$scope.state.loading = false
|
||||
this.$scope.$emit('editor:loaded')
|
||||
})
|
||||
})
|
||||
// Note: this will only catch errors in from i18n setup. ConnectionManager
|
||||
// handles errors for the socket connection
|
||||
.catch(() => {
|
||||
this.$scope.$apply(() => {
|
||||
this.$scope.state.error = 'Could not load translations.'
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
socketLoaded() {
|
||||
this.resolveSocketPromise()
|
||||
}
|
||||
}
|
||||
|
||||
export default LoadingManager
|
|
@ -1,73 +0,0 @@
|
|||
/* eslint-disable
|
||||
max-len,
|
||||
no-unused-vars,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
let BinaryFilesManager
|
||||
|
||||
export default BinaryFilesManager = class BinaryFilesManager {
|
||||
constructor(ide, $scope) {
|
||||
this.ide = ide
|
||||
this.$scope = $scope
|
||||
this.$scope.$on('entity:selected', (event, entity) => {
|
||||
if (this.$scope.ui.view !== 'track-changes' && entity.type === 'file') {
|
||||
return this.openFile(entity)
|
||||
} else if (entity.type === 'doc') {
|
||||
return this.closeFile()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
openFile(file) {
|
||||
if (this.$scope.ui.view === 'editor') {
|
||||
// store position before switching to binary view
|
||||
this.$scope.$broadcast('store-doc-position')
|
||||
}
|
||||
|
||||
this.ide.fileTreeManager.selectEntity(file)
|
||||
if (this.$scope.ui.view !== 'history') {
|
||||
this.$scope.ui.view = 'file'
|
||||
}
|
||||
this.$scope.openFile = null
|
||||
this.$scope.$apply()
|
||||
return window.setTimeout(
|
||||
() => {
|
||||
this.$scope.openFile = file
|
||||
|
||||
this.$scope.$apply()
|
||||
|
||||
this.$scope.$broadcast('file-view:file-opened')
|
||||
window.dispatchEvent(new Event('file-view:file-opened'))
|
||||
},
|
||||
0,
|
||||
this
|
||||
)
|
||||
}
|
||||
|
||||
openFileWithId(id) {
|
||||
const entity = this.ide.fileTreeManager.findEntityById(id)
|
||||
if (entity?.type === 'file') {
|
||||
this.openFile(entity)
|
||||
}
|
||||
}
|
||||
|
||||
closeFile() {
|
||||
return window.setTimeout(
|
||||
() => {
|
||||
this.$scope.openFile = null
|
||||
if (this.$scope.ui.view !== 'history') {
|
||||
this.$scope.ui.view = 'editor'
|
||||
}
|
||||
this.$scope.$apply()
|
||||
},
|
||||
0,
|
||||
this
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
import App from '../../base'
|
||||
import { rootContext } from '../../shared/context/root-context'
|
||||
import ChatPane from '../../features/chat/components/chat-pane'
|
||||
import { react2angular } from 'react2angular'
|
||||
|
||||
App.component('chat', react2angular(rootContext.use(ChatPane)))
|
|
@ -1,59 +0,0 @@
|
|||
/* eslint-disable
|
||||
camelcase,
|
||||
max-len,
|
||||
no-return-assign,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
import App from '../../base'
|
||||
const _cobrandingData = window.brandVariation
|
||||
|
||||
export default App.factory('CobrandingDataService', function () {
|
||||
const isProjectCobranded = () => _cobrandingData != null
|
||||
|
||||
const getLogoImgUrl = () =>
|
||||
_cobrandingData != null ? _cobrandingData.logo_url : undefined
|
||||
|
||||
const getSubmitBtnHtml = () =>
|
||||
_cobrandingData != null ? _cobrandingData.submit_button_html : undefined
|
||||
|
||||
const getBrandVariationName = () =>
|
||||
_cobrandingData != null ? _cobrandingData.name : undefined
|
||||
|
||||
const getBrandVariationHomeUrl = () =>
|
||||
_cobrandingData != null ? _cobrandingData.home_url : undefined
|
||||
|
||||
const getPublishGuideHtml = () =>
|
||||
_cobrandingData != null ? _cobrandingData.publish_guide_html : undefined
|
||||
|
||||
const getPartner = () =>
|
||||
_cobrandingData != null ? _cobrandingData.partner : undefined
|
||||
|
||||
const hasBrandedMenu = () =>
|
||||
_cobrandingData != null ? _cobrandingData.branded_menu : undefined
|
||||
|
||||
const getBrandId = () =>
|
||||
_cobrandingData != null ? _cobrandingData.brand_id : undefined
|
||||
|
||||
const getBrandVariationId = () =>
|
||||
_cobrandingData != null ? _cobrandingData.id : undefined
|
||||
|
||||
return {
|
||||
isProjectCobranded,
|
||||
getLogoImgUrl,
|
||||
getSubmitBtnHtml,
|
||||
getBrandVariationName,
|
||||
getBrandVariationHomeUrl,
|
||||
getPublishGuideHtml,
|
||||
getPartner,
|
||||
hasBrandedMenu,
|
||||
getBrandId,
|
||||
getBrandVariationId,
|
||||
}
|
||||
})
|
|
@ -1,630 +0,0 @@
|
|||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* DS206: Consider reworking classes to avoid initClass
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
|
||||
import SocketIoShim from './SocketIoShim'
|
||||
import getMeta from '../../utils/meta'
|
||||
import { debugConsole, debugging } from '@/utils/debugging'
|
||||
|
||||
let ConnectionManager
|
||||
const ONEHOUR = 1000 * 60 * 60
|
||||
|
||||
export default ConnectionManager = (function () {
|
||||
ConnectionManager = class ConnectionManager {
|
||||
static initClass() {
|
||||
this.prototype.disconnectAfterMs = ONEHOUR * 24
|
||||
|
||||
this.prototype.lastUserAction = new Date()
|
||||
|
||||
this.prototype.MIN_RETRY_INTERVAL = 1000 // ms, rate limit on reconnects for user clicking "try now"
|
||||
this.prototype.BACKGROUND_RETRY_INTERVAL = 5 * 1000
|
||||
|
||||
this.prototype.RECONNECT_GRACEFULLY_RETRY_INTERVAL = 5000 // ms
|
||||
this.prototype.MAX_RECONNECT_GRACEFULLY_INTERVAL = 45 * 1000
|
||||
}
|
||||
|
||||
constructor(ide, $scope) {
|
||||
this.ide = ide
|
||||
this.$scope = $scope
|
||||
if (typeof window.io !== 'object') {
|
||||
this.switchToWsFallbackIfPossible()
|
||||
debugConsole.error(
|
||||
'Socket.io javascript not loaded. Please check that the real-time service is running and accessible.'
|
||||
)
|
||||
this.ide.socket = SocketIoShim.stub()
|
||||
this.$scope.$apply(() => {
|
||||
return (this.$scope.state.error =
|
||||
'Could not connect to websocket server :(')
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
setInterval(() => {
|
||||
return this.disconnectIfInactive()
|
||||
}, ONEHOUR)
|
||||
|
||||
// trigger a reconnect immediately if network comes back online
|
||||
window.addEventListener('online', () => {
|
||||
debugConsole.log('[online] browser notified online')
|
||||
if (!this.connected) {
|
||||
return this.tryReconnectWithRateLimit({ force: true })
|
||||
}
|
||||
})
|
||||
|
||||
this.userIsLeavingPage = false
|
||||
window.addEventListener('beforeunload', () => {
|
||||
this.userIsLeavingPage = true
|
||||
}) // Don't return true or it will show a pop up
|
||||
|
||||
this.connected = false
|
||||
this.userIsInactive = false
|
||||
this.gracefullyReconnecting = false
|
||||
this.shuttingDown = false
|
||||
|
||||
this.$scope.connection = {
|
||||
debug: debugging,
|
||||
reconnecting: false,
|
||||
stillReconnecting: false,
|
||||
// If we need to force everyone to reload the editor
|
||||
forced_disconnect: false,
|
||||
inactive_disconnect: false,
|
||||
jobId: 0,
|
||||
}
|
||||
|
||||
this.$scope.tryReconnectNow = () => {
|
||||
// user manually requested reconnection via "Try now" button
|
||||
return this.tryReconnectWithRateLimit({ force: true })
|
||||
}
|
||||
|
||||
this.$scope.$on('cursor:editor:update', () => {
|
||||
this.lastUserAction = new Date() // time of last edit
|
||||
if (!this.connected) {
|
||||
// user is editing, try to reconnect
|
||||
return this.tryReconnectWithRateLimit()
|
||||
}
|
||||
})
|
||||
|
||||
document.querySelector('body').addEventListener('click', e => {
|
||||
if (
|
||||
!this.shuttingDown &&
|
||||
!this.connected &&
|
||||
e.target.id !== 'try-reconnect-now-button'
|
||||
) {
|
||||
// user is editing, try to reconnect
|
||||
return this.tryReconnectWithRateLimit()
|
||||
}
|
||||
})
|
||||
|
||||
// initial connection attempt
|
||||
this.updateConnectionManagerState('connecting')
|
||||
const parsedURL = new URL(
|
||||
getMeta('ol-wsUrl') || '/socket.io',
|
||||
window.origin
|
||||
)
|
||||
const query = new URLSearchParams({
|
||||
projectId: getMeta('ol-project_id'),
|
||||
}).toString()
|
||||
this.ide.socket = SocketIoShim.connect(parsedURL.origin, {
|
||||
resource: parsedURL.pathname.slice(1),
|
||||
reconnect: false,
|
||||
'connect timeout': 30 * 1000,
|
||||
'force new connection': true,
|
||||
query,
|
||||
})
|
||||
|
||||
// handle network-level websocket errors (e.g. failed dns lookups)
|
||||
|
||||
let connectionAttempt = 1
|
||||
const connectionErrorHandler = err => {
|
||||
if (
|
||||
window.wsRetryHandshake &&
|
||||
connectionAttempt++ < window.wsRetryHandshake
|
||||
) {
|
||||
return setTimeout(
|
||||
() => this.ide.socket.socket.connect(),
|
||||
// add jitter to spread reconnects
|
||||
connectionAttempt * (1 + Math.random()) * 1000
|
||||
)
|
||||
}
|
||||
this.updateConnectionManagerState('error')
|
||||
debugConsole.log('socket.io error', err)
|
||||
if (!this.switchToWsFallbackIfPossible()) {
|
||||
this.connected = false
|
||||
return this.$scope.$apply(() => {
|
||||
return (this.$scope.state.error =
|
||||
"Unable to connect, please view the <u><a href='/learn/Kb/Connection_problems'>connection problems guide</a></u> to fix the issue.")
|
||||
})
|
||||
}
|
||||
}
|
||||
this.ide.socket.on('error', connectionErrorHandler)
|
||||
|
||||
// The "connect" event is the first event we get back. It only
|
||||
// indicates that the websocket is connected, we still need to
|
||||
// pass authentication to join a project.
|
||||
|
||||
this.ide.socket.on('connect', () => {
|
||||
// state should be 'connecting'...
|
||||
// remove connection error handler when connected, avoid unwanted fallbacks
|
||||
this.ide.socket.removeListener('error', connectionErrorHandler)
|
||||
debugConsole.log('[socket.io connect] Connected')
|
||||
this.updateConnectionManagerState('authenticating')
|
||||
})
|
||||
|
||||
// The next event we should get is an authentication response
|
||||
// from the server, either "joinProjectResponse" or "connectionRejected".
|
||||
|
||||
this.ide.socket.on(
|
||||
'joinProjectResponse',
|
||||
({ publicId, project, permissionsLevel, protocolVersion }) => {
|
||||
this.ide.socket.publicId = publicId
|
||||
debugConsole.log('[socket.io bootstrap] ready for joinDoc')
|
||||
this.connected = true
|
||||
this.gracefullyReconnecting = false
|
||||
this.ide.pushEvent('connected')
|
||||
this.ide.pushEvent('joinProjectResponse')
|
||||
this.updateConnectionManagerState('joining')
|
||||
|
||||
this.$scope.$apply(() => {
|
||||
if (this.$scope.state.loading) {
|
||||
this.$scope.state.load_progress = 70
|
||||
}
|
||||
})
|
||||
|
||||
this.handleJoinProjectResponse({
|
||||
project,
|
||||
permissionsLevel,
|
||||
protocolVersion,
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
this.ide.socket.on('connectionRejected', err => {
|
||||
// state should be 'authenticating'...
|
||||
debugConsole.log(
|
||||
'[socket.io connectionRejected] session not valid or other connection error'
|
||||
)
|
||||
// real-time sends a 'retry' message if the process was shutting down
|
||||
// real-time sends TooManyRequests if joinProject was rate-limited.
|
||||
if (err?.message === 'retry' || err?.code === 'TooManyRequests') {
|
||||
return this.tryReconnectWithRateLimit()
|
||||
}
|
||||
if (err?.code === 'ProjectNotFound') {
|
||||
// A stale browser tab tried to join a deleted project.
|
||||
// Reloading the page will render a 404.
|
||||
this.ide
|
||||
.showGenericMessageModal(
|
||||
'Project has been deleted',
|
||||
'This project has been deleted by the owner.'
|
||||
)
|
||||
.result.then(() => location.reload(true))
|
||||
return
|
||||
}
|
||||
// we have failed authentication, usually due to an invalid session cookie
|
||||
return this.reportConnectionError(err)
|
||||
})
|
||||
|
||||
// Alternatively the attempt to connect can fail completely, so
|
||||
// we never get into the "connect" state.
|
||||
|
||||
this.ide.socket.on('connect_failed', () => {
|
||||
this.updateConnectionManagerState('error')
|
||||
this.connected = false
|
||||
return this.$scope.$apply(() => {
|
||||
return (this.$scope.state.error =
|
||||
"Unable to connect, please view the <u><a href='/learn/Kb/Connection_problems'>connection problems guide</a></u> to fix the issue.")
|
||||
})
|
||||
})
|
||||
|
||||
// We can get a "disconnect" event at any point after the
|
||||
// "connect" event.
|
||||
|
||||
this.ide.socket.on('disconnect', () => {
|
||||
debugConsole.log('[socket.io disconnect] Disconnected')
|
||||
this.connected = false
|
||||
this.ide.pushEvent('disconnected')
|
||||
|
||||
if (!this.$scope.connection.state.match(/^waiting/)) {
|
||||
if (
|
||||
!this.$scope.connection.forced_disconnect &&
|
||||
!this.userIsInactive &&
|
||||
!this.shuttingDown
|
||||
) {
|
||||
this.startAutoReconnectCountdown()
|
||||
} else {
|
||||
this.updateConnectionManagerState('inactive')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Site administrators can send the forceDisconnect event to all users
|
||||
|
||||
this.ide.socket.on('forceDisconnect', (message, delay = 10) => {
|
||||
this.updateConnectionManagerState('inactive')
|
||||
this.shuttingDown = true // prevent reconnection attempts
|
||||
this.$scope.$apply(() => {
|
||||
this.$scope.permissions = { ...this.$scope.permissions, write: false }
|
||||
return (this.$scope.connection.forced_disconnect = true)
|
||||
})
|
||||
// flush changes before disconnecting
|
||||
this.ide.$scope.$broadcast('flush-changes')
|
||||
window.setTimeout(() => this.ide.socket.disconnect(), 1000 * delay)
|
||||
this.ide.showLockEditorMessageModal(
|
||||
'Please wait',
|
||||
`\
|
||||
We're performing maintenance on Overleaf and you need to wait a moment.
|
||||
Sorry for any inconvenience.
|
||||
The editor will refresh automatically in ${delay} seconds.\
|
||||
`
|
||||
)
|
||||
return setTimeout(() => location.reload(), delay * 1000)
|
||||
})
|
||||
|
||||
this.ide.socket.on('reconnectGracefully', () => {
|
||||
debugConsole.log('Reconnect gracefully')
|
||||
this.reconnectGracefully()
|
||||
})
|
||||
}
|
||||
|
||||
switchToWsFallbackIfPossible() {
|
||||
const search = new URLSearchParams(window.location.search)
|
||||
if (getMeta('ol-wsUrl') && search.get('ws') !== 'fallback') {
|
||||
// if we tried to boot from a custom real-time backend and failed,
|
||||
// try reloading and falling back to the siteUrl
|
||||
search.set('ws', 'fallback')
|
||||
window.location.search = search.toString()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
updateConnectionManagerState(state) {
|
||||
this.$scope.$apply(() => {
|
||||
this.$scope.connection.jobId += 1
|
||||
const jobId = this.$scope.connection.jobId
|
||||
debugConsole.log(
|
||||
`[updateConnectionManagerState ${jobId}] from ${this.$scope.connection.state} to ${state}`
|
||||
)
|
||||
this.$scope.connection.state = state
|
||||
|
||||
this.$scope.connection.reconnecting = false
|
||||
this.$scope.connection.stillReconnecting = false
|
||||
this.$scope.connection.inactive_disconnect = false
|
||||
this.$scope.connection.joining = false
|
||||
this.$scope.connection.reconnection_countdown = null
|
||||
|
||||
if (state === 'connecting') {
|
||||
// initial connection
|
||||
} else if (state === 'reconnecting') {
|
||||
// reconnection after a connection has failed
|
||||
this.stopReconnectCountdownTimer()
|
||||
this.$scope.connection.reconnecting = true
|
||||
// if reconnecting takes more than 1s (it doesn't, usually) show the
|
||||
// 'reconnecting...' warning
|
||||
setTimeout(() => {
|
||||
if (
|
||||
this.$scope.connection.reconnecting &&
|
||||
this.$scope.connection.jobId === jobId
|
||||
) {
|
||||
this.$scope.connection.stillReconnecting = true
|
||||
}
|
||||
}, 1000)
|
||||
} else if (state === 'reconnectFailed') {
|
||||
// reconnect attempt failed
|
||||
} else if (state === 'authenticating') {
|
||||
// socket connection has been established, trying to authenticate
|
||||
} else if (state === 'joining') {
|
||||
// authenticated, joining project
|
||||
this.$scope.connection.joining = true
|
||||
} else if (state === 'ready') {
|
||||
// project has been joined
|
||||
} else if (state === 'waitingCountdown') {
|
||||
// disconnected and waiting to reconnect via the countdown timer
|
||||
this.stopReconnectCountdownTimer()
|
||||
} else if (state === 'waitingGracefully') {
|
||||
// disconnected and waiting to reconnect gracefully
|
||||
this.stopReconnectCountdownTimer()
|
||||
} else if (state === 'inactive') {
|
||||
// disconnected and not trying to reconnect (inactive)
|
||||
} else if (state === 'error') {
|
||||
// something is wrong
|
||||
} else {
|
||||
debugConsole.log(
|
||||
`[WARN] [updateConnectionManagerState ${jobId}] got unrecognised state ${state}`
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
expectConnectionManagerState(state, jobId) {
|
||||
if (
|
||||
this.$scope.connection.state === state &&
|
||||
(!jobId || jobId === this.$scope.connection.jobId)
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
debugConsole.log(
|
||||
`[WARN] [state mismatch] expected state ${state}${
|
||||
jobId ? '/' + jobId : ''
|
||||
} when in ${this.$scope.connection.state}/${
|
||||
this.$scope.connection.jobId
|
||||
}`
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
// Error reporting, which can reload the page if appropriate
|
||||
|
||||
reportConnectionError(err) {
|
||||
debugConsole.log('[socket.io] reporting connection error')
|
||||
this.updateConnectionManagerState('error')
|
||||
if (
|
||||
(err != null ? err.message : undefined) === 'not authorized' ||
|
||||
(err != null ? err.message : undefined) === 'invalid session'
|
||||
) {
|
||||
return (window.location = `/login?redir=${encodeURI(
|
||||
window.location.pathname
|
||||
)}`)
|
||||
} else {
|
||||
this.ide.socket.disconnect()
|
||||
return this.ide.showGenericMessageModal(
|
||||
'Something went wrong connecting',
|
||||
`\
|
||||
Something went wrong connecting to your project. Please refresh if this continues to happen.\
|
||||
`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
handleJoinProjectResponse({ project, permissionsLevel, protocolVersion }) {
|
||||
if (
|
||||
this.$scope.protocolVersion != null &&
|
||||
this.$scope.protocolVersion !== protocolVersion
|
||||
) {
|
||||
location.reload(true)
|
||||
}
|
||||
|
||||
this.$scope.$apply(() => {
|
||||
this.updateConnectionManagerState('ready')
|
||||
this.$scope.protocolVersion = protocolVersion
|
||||
const defaultProjectAttributes = { rootDoc_id: null }
|
||||
this.$scope.project = { ...defaultProjectAttributes, ...project }
|
||||
this.$scope.permissionsLevel = permissionsLevel
|
||||
this.ide.loadingManager.socketLoaded()
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('project:joined', { detail: this.$scope.project })
|
||||
)
|
||||
this.$scope.$broadcast('project:joined')
|
||||
})
|
||||
}
|
||||
|
||||
reconnectImmediately() {
|
||||
this.disconnect()
|
||||
return this.tryReconnect()
|
||||
}
|
||||
|
||||
disconnect(options) {
|
||||
if (options && options.permanent) {
|
||||
debugConsole.log('[disconnect] shutting down ConnectionManager')
|
||||
this.updateConnectionManagerState('inactive')
|
||||
this.shuttingDown = true // prevent reconnection attempts
|
||||
} else if (this.ide.socket.socket && !this.ide.socket.socket.connected) {
|
||||
debugConsole.log(
|
||||
'[socket.io] skipping disconnect because socket.io has not connected'
|
||||
)
|
||||
return
|
||||
}
|
||||
debugConsole.log('[socket.io] disconnecting client')
|
||||
return this.ide.socket.disconnect()
|
||||
}
|
||||
|
||||
startAutoReconnectCountdown() {
|
||||
this.updateConnectionManagerState('waitingCountdown')
|
||||
const connectionId = this.$scope.connection.jobId
|
||||
let countdown
|
||||
debugConsole.log('[ConnectionManager] starting autoreconnect countdown')
|
||||
const twoMinutes = 2 * 60 * 1000
|
||||
if (
|
||||
this.lastUserAction != null &&
|
||||
new Date() - this.lastUserAction > twoMinutes
|
||||
) {
|
||||
// between 1 minute and 3 minutes
|
||||
countdown = 60 + Math.floor(Math.random() * 120)
|
||||
} else {
|
||||
countdown = 3 + Math.floor(Math.random() * 7)
|
||||
}
|
||||
|
||||
if (this.userIsLeavingPage) {
|
||||
// user will have pressed refresh or back etc
|
||||
return
|
||||
}
|
||||
|
||||
this.$scope.$apply(() => {
|
||||
this.$scope.connection.reconnecting = false
|
||||
this.$scope.connection.stillReconnecting = false
|
||||
this.$scope.connection.joining = false
|
||||
this.$scope.connection.reconnection_countdown = countdown
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
if (!this.connected && !this.countdownTimeoutId) {
|
||||
this.countdownTimeoutId = setTimeout(
|
||||
() => this.decreaseCountdown(connectionId),
|
||||
1000
|
||||
)
|
||||
}
|
||||
}, 200)
|
||||
}
|
||||
|
||||
stopReconnectCountdownTimer() {
|
||||
// clear timeout and set to null so we know there is no countdown running
|
||||
if (this.countdownTimeoutId != null) {
|
||||
debugConsole.log(
|
||||
'[ConnectionManager] cancelling existing reconnect timer'
|
||||
)
|
||||
clearTimeout(this.countdownTimeoutId)
|
||||
this.countdownTimeoutId = null
|
||||
}
|
||||
}
|
||||
|
||||
decreaseCountdown(connectionId) {
|
||||
this.countdownTimeoutId = null
|
||||
if (this.$scope.connection.reconnection_countdown == null) {
|
||||
return
|
||||
}
|
||||
if (
|
||||
!this.expectConnectionManagerState('waitingCountdown', connectionId)
|
||||
) {
|
||||
debugConsole.log(
|
||||
`[ConnectionManager] Aborting stale countdown ${connectionId}`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
debugConsole.log(
|
||||
'[ConnectionManager] decreasing countdown',
|
||||
this.$scope.connection.reconnection_countdown
|
||||
)
|
||||
this.$scope.$apply(() => {
|
||||
this.$scope.connection.reconnection_countdown--
|
||||
})
|
||||
|
||||
if (this.$scope.connection.reconnection_countdown <= 0) {
|
||||
this.$scope.connection.reconnecting = false
|
||||
this.$scope.$apply(() => {
|
||||
this.tryReconnect()
|
||||
})
|
||||
} else {
|
||||
this.countdownTimeoutId = setTimeout(
|
||||
() => this.decreaseCountdown(connectionId),
|
||||
1000
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
tryReconnect() {
|
||||
debugConsole.log('[ConnectionManager] tryReconnect')
|
||||
if (
|
||||
this.connected ||
|
||||
this.shuttingDown ||
|
||||
this.$scope.connection.reconnecting
|
||||
) {
|
||||
return
|
||||
}
|
||||
this.updateConnectionManagerState('reconnecting')
|
||||
debugConsole.log('[ConnectionManager] Starting new connection')
|
||||
|
||||
const removeHandler = () => {
|
||||
this.ide.socket.removeListener('error', handleFailure)
|
||||
this.ide.socket.removeListener('connect', handleSuccess)
|
||||
}
|
||||
const handleFailure = () => {
|
||||
debugConsole.log('[ConnectionManager] tryReconnect: failed')
|
||||
removeHandler()
|
||||
this.updateConnectionManagerState('reconnectFailed')
|
||||
this.tryReconnectWithRateLimit({ force: true })
|
||||
}
|
||||
const handleSuccess = () => {
|
||||
debugConsole.log('[ConnectionManager] tryReconnect: success')
|
||||
removeHandler()
|
||||
}
|
||||
this.ide.socket.on('error', handleFailure)
|
||||
this.ide.socket.on('connect', handleSuccess)
|
||||
|
||||
// use socket.io connect() here to make a single attempt, the
|
||||
// reconnect() method makes multiple attempts
|
||||
this.ide.socket.socket.connect()
|
||||
// record the time of the last attempt to connect
|
||||
this.lastConnectionAttempt = new Date()
|
||||
}
|
||||
|
||||
tryReconnectWithRateLimit(options) {
|
||||
// bail out if the reconnect is already in progress
|
||||
if (this.$scope.connection.reconnecting || this.connected) {
|
||||
return
|
||||
}
|
||||
// bail out if we are going to reconnect soon anyway
|
||||
const reconnectingSoon =
|
||||
this.$scope.connection.reconnection_countdown != null &&
|
||||
this.$scope.connection.reconnection_countdown <= 5
|
||||
const clickedTryNow = options != null ? options.force : undefined // user requested reconnection
|
||||
if (reconnectingSoon && !clickedTryNow) {
|
||||
return
|
||||
}
|
||||
// bail out if we tried reconnecting recently
|
||||
const allowedInterval = clickedTryNow
|
||||
? this.MIN_RETRY_INTERVAL
|
||||
: this.BACKGROUND_RETRY_INTERVAL
|
||||
if (
|
||||
this.lastConnectionAttempt != null &&
|
||||
new Date() - this.lastConnectionAttempt < allowedInterval
|
||||
) {
|
||||
if (this.$scope.connection.state !== 'waitingCountdown') {
|
||||
this.startAutoReconnectCountdown()
|
||||
}
|
||||
return
|
||||
}
|
||||
this.tryReconnect()
|
||||
}
|
||||
|
||||
disconnectIfInactive() {
|
||||
this.userIsInactive =
|
||||
new Date() - this.lastUserAction > this.disconnectAfterMs
|
||||
if (this.userIsInactive && this.connected) {
|
||||
this.disconnect()
|
||||
return this.$scope.$apply(() => {
|
||||
return (this.$scope.connection.inactive_disconnect = true)
|
||||
}) // 5 minutes
|
||||
}
|
||||
}
|
||||
|
||||
reconnectGracefully(force) {
|
||||
if (this.reconnectGracefullyStarted == null) {
|
||||
this.reconnectGracefullyStarted = new Date()
|
||||
} else {
|
||||
if (!force) {
|
||||
debugConsole.log(
|
||||
'[reconnectGracefully] reconnection is already in process, so skipping'
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
const userIsInactive =
|
||||
new Date() - this.lastUserAction >
|
||||
this.RECONNECT_GRACEFULLY_RETRY_INTERVAL
|
||||
const maxIntervalReached =
|
||||
new Date() - this.reconnectGracefullyStarted >
|
||||
this.MAX_RECONNECT_GRACEFULLY_INTERVAL
|
||||
if (userIsInactive || maxIntervalReached) {
|
||||
debugConsole.log(
|
||||
"[reconnectGracefully] User didn't do anything for last 5 seconds, reconnecting"
|
||||
)
|
||||
this._reconnectGracefullyNow()
|
||||
} else {
|
||||
debugConsole.log(
|
||||
'[reconnectGracefully] User is working, will try again in 5 seconds'
|
||||
)
|
||||
this.updateConnectionManagerState('waitingGracefully')
|
||||
setTimeout(() => {
|
||||
this.reconnectGracefully(true)
|
||||
}, this.RECONNECT_GRACEFULLY_RETRY_INTERVAL)
|
||||
}
|
||||
}
|
||||
|
||||
_reconnectGracefullyNow() {
|
||||
this.gracefullyReconnecting = true
|
||||
this.reconnectGracefullyStarted = null
|
||||
// Clear cookie so we don't go to the same backend server
|
||||
$.cookie('SERVERID', '', { expires: -1, path: '/' })
|
||||
return this.reconnectImmediately()
|
||||
}
|
||||
}
|
||||
ConnectionManager.initClass()
|
||||
return ConnectionManager
|
||||
})()
|
|
@ -1,236 +0,0 @@
|
|||
/*
|
||||
|
||||
EditorWatchdogManager is used for end-to-end checks of edits.
|
||||
|
||||
|
||||
The editor UI is backed by Ace and CodeMirrors, which in turn are connected
|
||||
to ShareJs documents in the frontend.
|
||||
Edits propagate from the editor to ShareJs and are send through socket.io
|
||||
and real-time to document-updater.
|
||||
In document-updater edits are integrated into the document history and
|
||||
a confirmation/rejection is sent back to the frontend.
|
||||
|
||||
Along the way things can get lost.
|
||||
We have certain safe-guards in place, but are still getting occasional
|
||||
reports of lost edits.
|
||||
|
||||
EditorWatchdogManager is implementing the basis for end-to-end checks on
|
||||
two levels:
|
||||
|
||||
- local/ShareJsDoc: edits that pass-by a ShareJs document shall get
|
||||
acknowledged eventually.
|
||||
- global: any edits made in the editor shall get acknowledged eventually,
|
||||
independent for which ShareJs document (potentially none) sees it.
|
||||
|
||||
How does this work?
|
||||
===================
|
||||
|
||||
The global check is using a global EditorWatchdogManager that is available
|
||||
via the angular factory 'ide'.
|
||||
Local/ShareJsDoc level checks will connect to the global instance.
|
||||
|
||||
Each EditorWatchdogManager keeps track of the oldest un-acknowledged edit.
|
||||
When ever a ShareJs document receives an acknowledgement event, a local
|
||||
EditorWatchdogManager will see it and also notify the global instance about
|
||||
it.
|
||||
The next edit cycle will clear the oldest un-acknowledged timestamp in case
|
||||
a new ack has arrived, otherwise it will bark loud! via the timeout handler.
|
||||
|
||||
Scenarios
|
||||
=========
|
||||
|
||||
- User opens the CodeMirror editor
|
||||
- attach global check to new CM instance
|
||||
- detach Ace from the local EditorWatchdogManager
|
||||
- when the frontend attaches the CM instance to ShareJs, we also
|
||||
attach it to the local EditorWatchdogManager
|
||||
- the internal attach process writes the document content to the editor,
|
||||
which in turn emits 'change' events. These event need to be excluded
|
||||
from the watchdog. EditorWatchdogManager.ignoreEditsFor takes care
|
||||
of that.
|
||||
- User opens the Ace editor (again)
|
||||
- (attach global check to the Ace editor, only one copy of Ace is around)
|
||||
- detach local EditorWatchdogManager from CM
|
||||
- likewise with CM, attach Ace to the local EditorWatchdogManager
|
||||
- User makes an edit
|
||||
- the editor will emit a 'change' event
|
||||
- the global EditorWatchdogManager will process it first
|
||||
- the local EditorWatchdogManager will process it next
|
||||
- Document-updater confirms an edit
|
||||
- the local EditorWatchdogManager will process it first, it passes it on to
|
||||
- the global EditorWatchdogManager will process it next
|
||||
|
||||
Time
|
||||
====
|
||||
|
||||
The delay between edits and acks is measured using a monotonic clock:
|
||||
`performance.now()`.
|
||||
It is agnostic to system clock changes in either direction and timezone
|
||||
changes do not affect it as well.
|
||||
Roughly speaking, it is initialized with `0` when the `window` context is
|
||||
created, before our JS app boots.
|
||||
As per canIUse.com and MDN `performance.now()` is available to all supported
|
||||
Browsers, including IE11.
|
||||
See also: https://caniuse.com/?search=performance.now
|
||||
See also: https://developer.mozilla.org/en-US/docs/Web/API/Performance/now
|
||||
*/
|
||||
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
|
||||
// TIMEOUT specifies the timeout for edits into a single ShareJsDoc.
|
||||
const TIMEOUT = 60 * 1000
|
||||
// GLOBAL_TIMEOUT specifies the timeout for edits into any ShareJSDoc.
|
||||
const GLOBAL_TIMEOUT = TIMEOUT
|
||||
// REPORT_EVERY specifies how often we send events/report errors.
|
||||
const REPORT_EVERY = 60 * 1000
|
||||
|
||||
const SCOPE_LOCAL = 'ShareJsDoc'
|
||||
const SCOPE_GLOBAL = 'global'
|
||||
|
||||
class Reporter {
|
||||
constructor(onTimeoutHandler) {
|
||||
this._onTimeoutHandler = onTimeoutHandler
|
||||
this._lastReport = undefined
|
||||
this._queue = []
|
||||
}
|
||||
|
||||
_getMetaPreferLocal() {
|
||||
for (const meta of this._queue) {
|
||||
if (meta.scope === SCOPE_LOCAL) {
|
||||
return meta
|
||||
}
|
||||
}
|
||||
return this._queue.pop()
|
||||
}
|
||||
|
||||
onTimeout(meta) {
|
||||
// Collect all 'meta's for this update.
|
||||
// global arrive before local ones, but we are eager to report local ones.
|
||||
this._queue.push(meta)
|
||||
|
||||
setTimeout(() => {
|
||||
// Another handler processed the 'meta' entry already
|
||||
if (!this._queue.length) return
|
||||
|
||||
const maybeLocalMeta = this._getMetaPreferLocal()
|
||||
|
||||
// Discard other, newly arrived 'meta's
|
||||
this._queue.length = 0
|
||||
|
||||
const now = Date.now()
|
||||
// Do not flood the server with losing-edits events
|
||||
const reportedRecently = now - this._lastReport < REPORT_EVERY
|
||||
if (!reportedRecently) {
|
||||
this._lastReport = now
|
||||
this._onTimeoutHandler(maybeLocalMeta)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default class EditorWatchdogManager {
|
||||
constructor({ parent, onTimeoutHandler }) {
|
||||
this.scope = parent ? SCOPE_LOCAL : SCOPE_GLOBAL
|
||||
this.timeout = parent ? TIMEOUT : GLOBAL_TIMEOUT
|
||||
this.parent = parent
|
||||
if (parent) {
|
||||
this.reporter = parent.reporter
|
||||
} else {
|
||||
this.reporter = new Reporter(onTimeoutHandler)
|
||||
}
|
||||
|
||||
this.lastAck = null
|
||||
this.lastUnackedEdit = null
|
||||
}
|
||||
|
||||
onAck() {
|
||||
this.lastAck = performance.now()
|
||||
|
||||
// bubble up to globalEditorWatchdogManager
|
||||
if (this.parent) this.parent.onAck()
|
||||
}
|
||||
|
||||
onEdit() {
|
||||
// Use timestamps to track the high-water mark of unacked edits
|
||||
const now = performance.now()
|
||||
|
||||
// Discard the last unacked edit if there are now newer acks
|
||||
if (this.lastAck > this.lastUnackedEdit) {
|
||||
this.lastUnackedEdit = null
|
||||
}
|
||||
// Start tracking for this keypress if we aren't already tracking an
|
||||
// unacked edit
|
||||
if (!this.lastUnackedEdit) {
|
||||
this.lastUnackedEdit = now
|
||||
}
|
||||
|
||||
// Report an error if the last tracked edit hasn't been cleared by an
|
||||
// ack from the server after a long time
|
||||
const delay = now - this.lastUnackedEdit
|
||||
if (delay > this.timeout) {
|
||||
const timeOrigin = Date.now() - now
|
||||
const scope = this.scope
|
||||
const lastAck = new Date(this.lastAck ? timeOrigin + this.lastAck : 0)
|
||||
const lastUnackedEdit = new Date(timeOrigin + this.lastUnackedEdit)
|
||||
const meta = { scope, delay, lastAck, lastUnackedEdit }
|
||||
this._log('timedOut', meta)
|
||||
this.reporter.onTimeout(meta)
|
||||
}
|
||||
}
|
||||
|
||||
attachToEditor(editorName, editor) {
|
||||
let onChange
|
||||
if (editorName === 'CM6') {
|
||||
// Code Mirror 6
|
||||
this._log('attach to editor', editorName)
|
||||
onChange = (_editor, changeDescription) => {
|
||||
if (changeDescription.origin === 'remote') return
|
||||
if (!(changeDescription.removed || changeDescription.inserted)) return
|
||||
this.onEdit()
|
||||
}
|
||||
editor.on('change', onChange)
|
||||
const detachFromEditor = () => {
|
||||
this._log('detach from editor', editorName)
|
||||
editor.off('change', onChange)
|
||||
}
|
||||
return detachFromEditor
|
||||
}
|
||||
if (editorName === 'CM') {
|
||||
// CM is passing the CM instance as first parameter, then the change.
|
||||
onChange = (editor, change) => {
|
||||
// Ignore remote changes.
|
||||
if (change.origin === 'remote') return
|
||||
|
||||
// sharejs only looks at DEL or INSERT change events.
|
||||
// NOTE: Keep in sync with sharejs.
|
||||
if (!(change.removed || change.text)) return
|
||||
|
||||
this.onEdit()
|
||||
}
|
||||
} else {
|
||||
// ACE is passing the change object as first parameter.
|
||||
onChange = change => {
|
||||
// Ignore remote changes.
|
||||
if (change.origin === 'remote') return
|
||||
|
||||
// sharejs only looks at DEL or INSERT change events.
|
||||
// NOTE: Keep in sync with sharejs.
|
||||
if (!(change.action === 'remove' || change.action === 'insert')) return
|
||||
|
||||
this.onEdit()
|
||||
}
|
||||
}
|
||||
this._log('attach to editor', editorName)
|
||||
editor.on('change', onChange)
|
||||
|
||||
const detachFromEditor = () => {
|
||||
this._log('detach from editor', editorName)
|
||||
editor.off('change', onChange)
|
||||
}
|
||||
return detachFromEditor
|
||||
}
|
||||
|
||||
_log() {
|
||||
debugConsole.log(`[EditorWatchdogManager] ${this.scope}:`, ...arguments)
|
||||
}
|
||||
}
|
|
@ -1,99 +0,0 @@
|
|||
/* eslint-disable
|
||||
max-len,
|
||||
no-return-assign,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
// This file is shared between the frontend and server code of web, so that
|
||||
// filename validation is the same in both implementations.
|
||||
// The logic in all copies must be kept in sync:
|
||||
// app/src/Features/Project/SafePath.js
|
||||
// frontend/js/ide/directives/SafePath.js
|
||||
// frontend/js/features/file-tree/util/safe-path.js
|
||||
|
||||
let SafePath
|
||||
// eslint-disable-next-line prefer-regex-literals
|
||||
const BADCHAR_RX = new RegExp(
|
||||
`\
|
||||
[\
|
||||
\\/\
|
||||
\\\\\
|
||||
\\*\
|
||||
\\u0000-\\u001F\
|
||||
\\u007F\
|
||||
\\u0080-\\u009F\
|
||||
\\uD800-\\uDFFF\
|
||||
]\
|
||||
`,
|
||||
'g'
|
||||
)
|
||||
|
||||
// eslint-disable-next-line prefer-regex-literals
|
||||
const BADFILE_RX = new RegExp(
|
||||
`\
|
||||
(^\\.$)\
|
||||
|(^\\.\\.$)\
|
||||
|(^\\s+)\
|
||||
|(\\s+$)\
|
||||
`,
|
||||
'g'
|
||||
)
|
||||
|
||||
// Put a block on filenames which match javascript property names, as they
|
||||
// can cause exceptions where the code puts filenames into a hash. This is a
|
||||
// temporary workaround until the code in other places is made safe against
|
||||
// property names.
|
||||
//
|
||||
// The list of property names is taken from
|
||||
// ['prototype'].concat(Object.getOwnPropertyNames(Object.prototype))
|
||||
// eslint-disable-next-line prefer-regex-literals
|
||||
const BLOCKEDFILE_RX = new RegExp(`\
|
||||
^(\
|
||||
prototype\
|
||||
|constructor\
|
||||
|toString\
|
||||
|toLocaleString\
|
||||
|valueOf\
|
||||
|hasOwnProperty\
|
||||
|isPrototypeOf\
|
||||
|propertyIsEnumerable\
|
||||
|__defineGetter__\
|
||||
|__lookupGetter__\
|
||||
|__defineSetter__\
|
||||
|__lookupSetter__\
|
||||
|__proto__\
|
||||
)$\
|
||||
`)
|
||||
|
||||
const MAX_PATH = 1024 // Maximum path length, in characters. This is fairly arbitrary.
|
||||
|
||||
export default SafePath = {
|
||||
clean(filename) {
|
||||
filename = filename.replace(BADCHAR_RX, '_')
|
||||
// for BADFILE_RX replace any matches with an equal number of underscores
|
||||
filename = filename.replace(BADFILE_RX, match =>
|
||||
new Array(match.length + 1).join('_')
|
||||
)
|
||||
// replace blocked filenames 'prototype' with '@prototype'
|
||||
filename = filename.replace(BLOCKEDFILE_RX, '@$1')
|
||||
return filename
|
||||
},
|
||||
|
||||
isCleanFilename(filename) {
|
||||
return (
|
||||
SafePath.isAllowedLength(filename) &&
|
||||
!filename.match(BADCHAR_RX) &&
|
||||
!filename.match(BADFILE_RX)
|
||||
)
|
||||
},
|
||||
|
||||
isAllowedLength(pathname) {
|
||||
return pathname.length > 0 && pathname.length <= MAX_PATH
|
||||
},
|
||||
}
|
|
@ -1,323 +0,0 @@
|
|||
/* eslint-disable
|
||||
max-len,
|
||||
no-return-assign,
|
||||
no-useless-escape,
|
||||
*/
|
||||
// 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
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
import App from '../../base'
|
||||
import _ from 'lodash'
|
||||
import '../../vendor/libs/jquery-layout'
|
||||
import '../../vendor/libs/jquery.ui.touch-punch'
|
||||
|
||||
export default App.directive('layout', [
|
||||
'$parse',
|
||||
'$compile',
|
||||
'ide',
|
||||
function ($parse, $compile, ide) {
|
||||
return {
|
||||
compile() {
|
||||
return {
|
||||
pre(scope, element, attrs) {
|
||||
let customTogglerEl, spacingClosed, spacingOpen, state
|
||||
const name = attrs.layout
|
||||
|
||||
const { customTogglerPane } = attrs
|
||||
const { customTogglerMsgWhenOpen } = attrs
|
||||
const { customTogglerMsgWhenClosed } = attrs
|
||||
const hasCustomToggler =
|
||||
customTogglerPane != null &&
|
||||
customTogglerMsgWhenOpen != null &&
|
||||
customTogglerMsgWhenClosed != null
|
||||
|
||||
if (attrs.spacingOpen != null) {
|
||||
spacingOpen = parseInt(attrs.spacingOpen, 10)
|
||||
} else {
|
||||
spacingOpen = 7
|
||||
}
|
||||
|
||||
if (attrs.spacingClosed != null) {
|
||||
spacingClosed = parseInt(attrs.spacingClosed, 10)
|
||||
} else {
|
||||
spacingClosed = 7
|
||||
}
|
||||
|
||||
const options = {
|
||||
spacing_open: spacingOpen,
|
||||
spacing_closed: spacingClosed,
|
||||
slidable: false,
|
||||
enableCursorHotkey: false,
|
||||
onopen: pane => {
|
||||
return onPaneOpen(pane)
|
||||
},
|
||||
onclose: pane => {
|
||||
return onPaneClose(pane)
|
||||
},
|
||||
onresize: () => {
|
||||
return onInternalResize()
|
||||
},
|
||||
maskIframesOnResize: scope.$eval(
|
||||
attrs.maskIframesOnResize || 'false'
|
||||
),
|
||||
east: {
|
||||
size: scope.$eval(attrs.initialSizeEast),
|
||||
initClosed: scope.$eval(attrs.initClosedEast),
|
||||
},
|
||||
west: {
|
||||
size: scope.$eval(attrs.initialSizeWest),
|
||||
initClosed: scope.$eval(attrs.initClosedWest),
|
||||
},
|
||||
}
|
||||
|
||||
// Restore previously recorded state
|
||||
if ((state = ide.localStorage(`layout.${name}`)) != null) {
|
||||
if (state.east != null) {
|
||||
if (
|
||||
attrs.minimumRestoreSizeEast == null ||
|
||||
(state.east.size >= attrs.minimumRestoreSizeEast &&
|
||||
!state.east.initClosed)
|
||||
) {
|
||||
options.east = state.east
|
||||
}
|
||||
options.east.initClosed = state.east.initClosed
|
||||
}
|
||||
if (state.west != null) {
|
||||
if (
|
||||
attrs.minimumRestoreSizeWest == null ||
|
||||
(state.west.size >= attrs.minimumRestoreSizeWest &&
|
||||
!state.west.initClosed)
|
||||
) {
|
||||
options.west = state.west
|
||||
}
|
||||
// NOTE: disabled so that the file tree re-opens on page load
|
||||
// options.west.initClosed = state.west.initClosed
|
||||
}
|
||||
}
|
||||
|
||||
options.east.resizerCursor = 'ew-resize'
|
||||
options.west.resizerCursor = 'ew-resize'
|
||||
|
||||
function repositionControls() {
|
||||
state = layout.readState()
|
||||
if (state.east != null) {
|
||||
const controls = element.find('> .ui-layout-resizer-controls')
|
||||
if (state.east.initClosed) {
|
||||
return controls.hide()
|
||||
} else {
|
||||
controls.show()
|
||||
return controls.css({
|
||||
right: state.east.size,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function repositionCustomToggler() {
|
||||
if (customTogglerEl == null) {
|
||||
return
|
||||
}
|
||||
state = layout.readState()
|
||||
const positionAnchor =
|
||||
customTogglerPane === 'east' ? 'right' : 'left'
|
||||
const paneState = state[customTogglerPane]
|
||||
if (paneState != null) {
|
||||
return customTogglerEl.css(
|
||||
positionAnchor,
|
||||
paneState.initClosed ? 0 : paneState.size
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function resetOpenStates() {
|
||||
state = layout.readState()
|
||||
if (attrs.openEast != null && state.east != null) {
|
||||
const openEast = $parse(attrs.openEast)
|
||||
return openEast.assign(scope, !state.east.initClosed)
|
||||
}
|
||||
}
|
||||
|
||||
// Someone moved the resizer
|
||||
function onInternalResize() {
|
||||
state = layout.readState()
|
||||
scope.$broadcast(`layout:${name}:resize`, state)
|
||||
repositionControls()
|
||||
if (hasCustomToggler) {
|
||||
repositionCustomToggler()
|
||||
}
|
||||
return resetOpenStates()
|
||||
}
|
||||
|
||||
let oldWidth = element.width()
|
||||
// Something resized our parent element
|
||||
const onExternalResize = function () {
|
||||
if (
|
||||
attrs.resizeProportionally != null &&
|
||||
scope.$eval(attrs.resizeProportionally)
|
||||
) {
|
||||
const eastState = layout.readState().east
|
||||
if (eastState != null) {
|
||||
const currentWidth = element.width()
|
||||
if (currentWidth > 0) {
|
||||
const newInternalWidth =
|
||||
(eastState.size / oldWidth) * currentWidth
|
||||
oldWidth = currentWidth
|
||||
layout.sizePane('east', newInternalWidth)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ide.$timeout(() => {
|
||||
layout.resizeAll()
|
||||
})
|
||||
}
|
||||
|
||||
const layout = element.layout(options)
|
||||
layout.resizeAll()
|
||||
|
||||
if (attrs.resizeOn != null) {
|
||||
for (const event of Array.from(attrs.resizeOn.split(','))) {
|
||||
scope.$on(event, () => onExternalResize())
|
||||
}
|
||||
}
|
||||
|
||||
if (hasCustomToggler) {
|
||||
state = layout.readState()
|
||||
const customTogglerScope = scope.$new()
|
||||
|
||||
customTogglerScope.isOpen = true
|
||||
customTogglerScope.isVisible = true
|
||||
|
||||
if (
|
||||
(state[customTogglerPane] != null
|
||||
? state[customTogglerPane].initClosed
|
||||
: undefined) === true
|
||||
) {
|
||||
customTogglerScope.isOpen = false
|
||||
}
|
||||
|
||||
customTogglerScope.tooltipMsgWhenOpen = customTogglerMsgWhenOpen
|
||||
customTogglerScope.tooltipMsgWhenClosed =
|
||||
customTogglerMsgWhenClosed
|
||||
|
||||
customTogglerScope.tooltipPlacement =
|
||||
customTogglerPane === 'east' ? 'left' : 'right'
|
||||
customTogglerScope.handleClick = function () {
|
||||
layout.toggle(customTogglerPane)
|
||||
return repositionCustomToggler()
|
||||
}
|
||||
customTogglerEl = $compile(`\
|
||||
<a href \
|
||||
ng-show=\"isVisible\" \
|
||||
class=\"custom-toggler ${`custom-toggler-${customTogglerPane}`}\" \
|
||||
ng-class=\"isOpen ? 'custom-toggler-open' : 'custom-toggler-closed'\" \
|
||||
tooltip=\"{{ isOpen ? tooltipMsgWhenOpen : tooltipMsgWhenClosed }}\" \
|
||||
tooltip-placement=\"{{ tooltipPlacement }}\" \
|
||||
ng-click=\"handleClick()\" \
|
||||
aria-label=\"{{ isOpen ? tooltipMsgWhenOpen : tooltipMsgWhenClosed }}\">\
|
||||
`)(customTogglerScope)
|
||||
element.append(customTogglerEl)
|
||||
}
|
||||
|
||||
function onPaneOpen(pane) {
|
||||
if (!hasCustomToggler || pane !== customTogglerPane) {
|
||||
return
|
||||
}
|
||||
return customTogglerEl
|
||||
.scope()
|
||||
.$applyAsync(() => (customTogglerEl.scope().isOpen = true))
|
||||
}
|
||||
|
||||
function onPaneClose(pane) {
|
||||
if (!hasCustomToggler || pane !== customTogglerPane) {
|
||||
return
|
||||
}
|
||||
return customTogglerEl
|
||||
.scope()
|
||||
.$applyAsync(() => (customTogglerEl.scope().isOpen = false))
|
||||
}
|
||||
|
||||
// Ensure editor resizes after loading. This is to handle the case where
|
||||
// the window has been resized while the editor is loading
|
||||
scope.$on('editor:loaded', () => {
|
||||
ide.$timeout(() => layout.resizeAll())
|
||||
})
|
||||
|
||||
// Save state when exiting
|
||||
$(window).unload(() => {
|
||||
// Save only the state properties for the current layout, ignoring sublayouts inside it.
|
||||
// If we save sublayouts state (`children`), the layout library will use it when
|
||||
// initializing. This raises errors when the sublayout elements aren't available (due to
|
||||
// being loaded at init or just not existing for the current project/user).
|
||||
const stateToSave = _.mapValues(layout.readState(), pane =>
|
||||
_.omit(pane, 'children')
|
||||
)
|
||||
ide.localStorage(`layout.${name}`, stateToSave)
|
||||
})
|
||||
|
||||
if (attrs.openEast != null) {
|
||||
scope.$watch(attrs.openEast, function (value, oldValue) {
|
||||
if (value != null && value !== oldValue) {
|
||||
if (value) {
|
||||
layout.open('east')
|
||||
} else {
|
||||
layout.close('east')
|
||||
}
|
||||
if (hasCustomToggler && customTogglerPane === 'east') {
|
||||
repositionCustomToggler()
|
||||
customTogglerEl.scope().$applyAsync(function () {
|
||||
customTogglerEl.scope().isOpen = value
|
||||
})
|
||||
}
|
||||
}
|
||||
return setTimeout(() => scope.$digest(), 0)
|
||||
})
|
||||
}
|
||||
|
||||
if (attrs.allowOverflowOn != null) {
|
||||
const overflowPane = scope.$eval(attrs.allowOverflowOn)
|
||||
const overflowPaneEl = layout.panes[overflowPane]
|
||||
// Set the panel as overflowing (gives it higher z-index and sets overflow rules)
|
||||
layout.allowOverflow(overflowPane)
|
||||
// Read the given z-index value and increment it, so that it's higher than synctex controls.
|
||||
const overflowPaneZVal = overflowPaneEl.zIndex()
|
||||
overflowPaneEl.css('z-index', overflowPaneZVal + 1)
|
||||
}
|
||||
|
||||
resetOpenStates()
|
||||
onInternalResize()
|
||||
|
||||
if (attrs.layoutDisabled != null) {
|
||||
return scope.$watch(attrs.layoutDisabled, function (value) {
|
||||
if (value) {
|
||||
layout.hide('east')
|
||||
} else {
|
||||
layout.show('east')
|
||||
}
|
||||
if (hasCustomToggler) {
|
||||
return customTogglerEl.scope().$applyAsync(function () {
|
||||
customTogglerEl.scope().isOpen = !value
|
||||
return (customTogglerEl.scope().isVisible = !value)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
post(scope, element, attrs) {
|
||||
const name = attrs.layout
|
||||
const state = element.layout().readState()
|
||||
return scope.$broadcast(`layout:${name}:linked`, state)
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
])
|
|
@ -1,139 +0,0 @@
|
|||
import App from '../../base'
|
||||
|
||||
export default App.directive('verticalResizablePanes', [
|
||||
'localStorage',
|
||||
'ide',
|
||||
function (localStorage, ide) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
link(scope, element, attrs) {
|
||||
const name = attrs.verticalResizablePanes
|
||||
const minSize = scope.$eval(attrs.verticalResizablePanesMinSize)
|
||||
const maxSize = scope.$eval(attrs.verticalResizablePanesMaxSize)
|
||||
const defaultSize = scope.$eval(attrs.verticalResizablePanesDefaultSize)
|
||||
let storedSize = null
|
||||
let manualResizeIncoming = false
|
||||
|
||||
if (name) {
|
||||
const storageKey = `vertical-resizable:${name}:south-size`
|
||||
storedSize = localStorage(storageKey)
|
||||
$(window).unload(() => {
|
||||
if (storedSize) {
|
||||
localStorage(storageKey, storedSize)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const layoutOptions = {
|
||||
center: {
|
||||
paneSelector: '[vertical-resizable-top]',
|
||||
paneClass: 'vertical-resizable-top',
|
||||
size: 'auto',
|
||||
},
|
||||
south: {
|
||||
paneSelector: '[vertical-resizable-bottom]',
|
||||
paneClass: 'vertical-resizable-bottom',
|
||||
resizerClass: 'vertical-resizable-resizer',
|
||||
resizerCursor: 'ns-resize',
|
||||
size: 'auto',
|
||||
resizable: true,
|
||||
closable: false,
|
||||
slidable: false,
|
||||
spacing_open: 6,
|
||||
spacing_closed: 6,
|
||||
maxSize: '75%',
|
||||
},
|
||||
}
|
||||
|
||||
const toggledExternally =
|
||||
attrs.verticalResizablePanesToggledExternallyOn
|
||||
const hiddenExternally = attrs.verticalResizablePanesHiddenExternallyOn
|
||||
const hiddenInitially = attrs.verticalResizablePanesHiddenInitially
|
||||
const resizeOn = attrs.verticalResizablePanesResizeOn
|
||||
const resizerDisabledClass = `${layoutOptions.south.resizerClass}-disabled`
|
||||
|
||||
function enableResizer() {
|
||||
if (layoutHandle.resizers && layoutHandle.resizers.south) {
|
||||
layoutHandle.resizers.south.removeClass(resizerDisabledClass)
|
||||
}
|
||||
}
|
||||
|
||||
function disableResizer() {
|
||||
if (layoutHandle.resizers && layoutHandle.resizers.south) {
|
||||
layoutHandle.resizers.south.addClass(resizerDisabledClass)
|
||||
}
|
||||
}
|
||||
|
||||
function handleDragEnd() {
|
||||
manualResizeIncoming = true
|
||||
}
|
||||
|
||||
function handleResize(paneName, paneEl, paneState) {
|
||||
if (manualResizeIncoming) {
|
||||
storedSize = paneState.size
|
||||
}
|
||||
manualResizeIncoming = false
|
||||
}
|
||||
|
||||
if (toggledExternally) {
|
||||
scope.$on(toggledExternally, (e, open) => {
|
||||
if (open) {
|
||||
enableResizer()
|
||||
layoutHandle.sizePane(
|
||||
'south',
|
||||
storedSize ?? defaultSize ?? 'auto'
|
||||
)
|
||||
} else {
|
||||
disableResizer()
|
||||
layoutHandle.sizePane('south', 'auto')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (hiddenExternally) {
|
||||
ide.$scope.$on(hiddenExternally, (e, open) => {
|
||||
if (open) {
|
||||
layoutHandle.show('south')
|
||||
} else {
|
||||
layoutHandle.hide('south')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (resizeOn) {
|
||||
ide.$scope.$on(resizeOn, () => {
|
||||
ide.$timeout(() => {
|
||||
layoutHandle.resizeAll()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
if (maxSize) {
|
||||
layoutOptions.south.maxSize = maxSize
|
||||
}
|
||||
|
||||
if (minSize) {
|
||||
layoutOptions.south.minSize = minSize
|
||||
}
|
||||
|
||||
if (defaultSize) {
|
||||
layoutOptions.south.size = defaultSize
|
||||
}
|
||||
|
||||
// The `drag` event fires only when the user manually resizes the panes; the `resize` event fires even when
|
||||
// the layout library internally resizes itself. In order to get explicit user-initiated resizes, we need to
|
||||
// listen to `drag` events. However, when the `drag` event fires, the panes aren't yet finished sizing so we
|
||||
// get the pane size *before* the resize happens. We do get the correct size in the next `resize` event.
|
||||
// The solution to work around this is to set up a flag in `drag` events which tells the next `resize` event
|
||||
// that it was user-initiated (therefore, storing the value).
|
||||
layoutOptions.south.ondrag_end = handleDragEnd
|
||||
layoutOptions.south.onresize = handleResize
|
||||
|
||||
const layoutHandle = element.layout(layoutOptions)
|
||||
if (hiddenInitially === 'true') {
|
||||
layoutHandle.hide('south')
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
])
|
|
@ -1,781 +0,0 @@
|
|||
/* eslint-disable
|
||||
camelcase,
|
||||
n/handle-callback-err,
|
||||
max-len,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS001: Remove Babel/TypeScript constructor workaround
|
||||
* DS101: Remove unnecessary use of Array.from
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* DS103: Rewrite code to no longer use __guard__
|
||||
* DS205: Consider reworking code to avoid use of IIFEs
|
||||
* DS206: Consider reworking classes to avoid initClass
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
import RangesTracker from '@overleaf/ranges-tracker'
|
||||
import EventEmitter from '../../utils/EventEmitter'
|
||||
import ShareJsDoc from './ShareJsDoc'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
let Document
|
||||
|
||||
export default Document = (function () {
|
||||
Document = class Document extends EventEmitter {
|
||||
static initClass() {
|
||||
this.prototype.MAX_PENDING_OP_SIZE = 64
|
||||
}
|
||||
|
||||
static getDocument(ide, doc_id) {
|
||||
if (!this.openDocs) {
|
||||
this.openDocs = {}
|
||||
}
|
||||
// Try to clean up existing docs before reopening them. If the doc has no
|
||||
// buffered ops then it will be deleted by _cleanup() and a new instance
|
||||
// of the document created below. This prevents us trying to follow the
|
||||
// joinDoc:existing code path on an existing doc that doesn't have any
|
||||
// local changes and getting an error if its version is too old.
|
||||
if (this.openDocs[doc_id]) {
|
||||
debugConsole.log(
|
||||
`[getDocument] Cleaning up existing document instance for ${doc_id}`
|
||||
)
|
||||
this.openDocs[doc_id]._cleanUp()
|
||||
}
|
||||
if (this.openDocs[doc_id] == null) {
|
||||
debugConsole.log(
|
||||
`[getDocument] Creating new document instance for ${doc_id}`
|
||||
)
|
||||
this.openDocs[doc_id] = new Document(ide, doc_id)
|
||||
} else {
|
||||
debugConsole.log(
|
||||
`[getDocument] Returning existing document instance for ${doc_id}`
|
||||
)
|
||||
}
|
||||
return this.openDocs[doc_id]
|
||||
}
|
||||
|
||||
static hasUnsavedChanges() {
|
||||
const object = this.openDocs || {}
|
||||
for (const doc_id in object) {
|
||||
const doc = object[doc_id]
|
||||
if (doc.hasBufferedOps()) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
static flushAll() {
|
||||
return (() => {
|
||||
const result = []
|
||||
for (const doc_id in this.openDocs) {
|
||||
const doc = this.openDocs[doc_id]
|
||||
result.push(doc.flush())
|
||||
}
|
||||
return result
|
||||
})()
|
||||
}
|
||||
|
||||
constructor(ide, doc_id) {
|
||||
super()
|
||||
this.ide = ide
|
||||
this.doc_id = doc_id
|
||||
this.connected = this.ide.socket.socket.connected
|
||||
this.joined = false
|
||||
this.wantToBeJoined = false
|
||||
this._checkCM6Consistency = () => this._checkConsistency(this.cm6)
|
||||
this._bindToEditorEvents()
|
||||
this._bindToSocketEvents()
|
||||
}
|
||||
|
||||
editorType() {
|
||||
if (this.cm6) {
|
||||
return 'cm6'
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
attachToCM6(cm6) {
|
||||
this.cm6 = cm6
|
||||
if (this.doc != null) {
|
||||
this.doc.attachToCM6(this.cm6)
|
||||
}
|
||||
if (this.cm6 != null) {
|
||||
this.cm6.on('change', this._checkCM6Consistency)
|
||||
}
|
||||
return this.ide.$scope.$emit('document:opened', this.doc)
|
||||
}
|
||||
|
||||
detachFromCM6() {
|
||||
if (this.doc != null) {
|
||||
this.doc.detachFromCM6()
|
||||
}
|
||||
if (this.cm6 != null) {
|
||||
this.cm6.off('change', this._checkCM6Consistency)
|
||||
}
|
||||
delete this.cm6
|
||||
this.clearChaosMonkey()
|
||||
return this.ide.$scope.$emit('document:closed', this.doc)
|
||||
}
|
||||
|
||||
submitOp(...args) {
|
||||
return this.doc != null
|
||||
? this.doc.submitOp(...Array.from(args || []))
|
||||
: undefined
|
||||
}
|
||||
|
||||
_checkConsistency(editor) {
|
||||
// We've been seeing a lot of errors when I think there shouldn't be
|
||||
// any, which may be related to this check happening before the change is
|
||||
// applied. If we use a timeout, hopefully we can reduce this.
|
||||
return setTimeout(() => {
|
||||
const editorValue = editor != null ? editor.getValue() : undefined
|
||||
const sharejsValue =
|
||||
this.doc != null ? this.doc.getSnapshot() : undefined
|
||||
if (editorValue !== sharejsValue) {
|
||||
return this._onError(
|
||||
new Error('Editor text does not match server text'),
|
||||
{},
|
||||
editorValue
|
||||
)
|
||||
}
|
||||
}, 0)
|
||||
}
|
||||
|
||||
getSnapshot() {
|
||||
return this.doc != null ? this.doc.getSnapshot() : undefined
|
||||
}
|
||||
|
||||
getType() {
|
||||
return this.doc != null ? this.doc.getType() : undefined
|
||||
}
|
||||
|
||||
getInflightOp() {
|
||||
return this.doc != null ? this.doc.getInflightOp() : undefined
|
||||
}
|
||||
|
||||
getPendingOp() {
|
||||
return this.doc != null ? this.doc.getPendingOp() : undefined
|
||||
}
|
||||
|
||||
getRecentAck() {
|
||||
return this.doc != null ? this.doc.getRecentAck() : undefined
|
||||
}
|
||||
|
||||
getOpSize(op) {
|
||||
return this.doc != null ? this.doc.getOpSize(op) : undefined
|
||||
}
|
||||
|
||||
hasBufferedOps() {
|
||||
return this.doc != null ? this.doc.hasBufferedOps() : undefined
|
||||
}
|
||||
|
||||
setTrackingChanges(track_changes) {
|
||||
return (this.doc.track_changes = track_changes)
|
||||
}
|
||||
|
||||
getTrackingChanges() {
|
||||
return !!this.doc.track_changes
|
||||
}
|
||||
|
||||
setTrackChangesIdSeeds(id_seeds) {
|
||||
return (this.doc.track_changes_id_seeds = id_seeds)
|
||||
}
|
||||
|
||||
_bindToSocketEvents() {
|
||||
this._onUpdateAppliedHandler = update => this._onUpdateApplied(update)
|
||||
this.ide.socket.on('otUpdateApplied', this._onUpdateAppliedHandler)
|
||||
this._onErrorHandler = (error, message) => {
|
||||
// 'otUpdateError' are emitted per doc socket.io room, hence we can be
|
||||
// sure that message.doc_id exists.
|
||||
if (message.doc_id !== this.doc_id) {
|
||||
// This error is for another doc. Do not action it. We could open
|
||||
// a modal that has the wrong context on it.
|
||||
return
|
||||
}
|
||||
this._onError(error, message)
|
||||
}
|
||||
this.ide.socket.on('otUpdateError', this._onErrorHandler)
|
||||
this._onDisconnectHandler = error => this._onDisconnect(error)
|
||||
return this.ide.socket.on('disconnect', this._onDisconnectHandler)
|
||||
}
|
||||
|
||||
_bindToEditorEvents() {
|
||||
const onReconnectHandler = update => {
|
||||
return this._onReconnect(update)
|
||||
}
|
||||
return (this._unsubscribeReconnectHandler = this.ide.$scope.$on(
|
||||
'project:joined',
|
||||
onReconnectHandler
|
||||
))
|
||||
}
|
||||
|
||||
_unBindFromEditorEvents() {
|
||||
return this._unsubscribeReconnectHandler()
|
||||
}
|
||||
|
||||
_unBindFromSocketEvents() {
|
||||
this.ide.socket.removeListener(
|
||||
'otUpdateApplied',
|
||||
this._onUpdateAppliedHandler
|
||||
)
|
||||
this.ide.socket.removeListener('otUpdateError', this._onErrorHandler)
|
||||
return this.ide.socket.removeListener(
|
||||
'disconnect',
|
||||
this._onDisconnectHandler
|
||||
)
|
||||
}
|
||||
|
||||
leaveAndCleanUp(cb) {
|
||||
return this.leave(error => {
|
||||
this._cleanUp()
|
||||
if (cb) cb(error)
|
||||
})
|
||||
}
|
||||
|
||||
join(callback) {
|
||||
if (callback == null) {
|
||||
callback = function () {}
|
||||
}
|
||||
this.wantToBeJoined = true
|
||||
this._cancelLeave()
|
||||
if (this.connected) {
|
||||
return this._joinDoc(callback)
|
||||
} else {
|
||||
if (!this._joinCallbacks) {
|
||||
this._joinCallbacks = []
|
||||
}
|
||||
return this._joinCallbacks.push(callback)
|
||||
}
|
||||
}
|
||||
|
||||
leave(callback) {
|
||||
if (callback == null) {
|
||||
callback = function () {}
|
||||
}
|
||||
this.flush() // force an immediate flush when leaving document
|
||||
this.wantToBeJoined = false
|
||||
this._cancelJoin()
|
||||
if (this.doc != null && this.doc.hasBufferedOps()) {
|
||||
debugConsole.log(
|
||||
'[leave] Doc has buffered ops, pushing callback for later'
|
||||
)
|
||||
if (!this._leaveCallbacks) {
|
||||
this._leaveCallbacks = []
|
||||
}
|
||||
return this._leaveCallbacks.push(callback)
|
||||
} else if (!this.connected) {
|
||||
debugConsole.log('[leave] Not connected, returning now')
|
||||
return callback()
|
||||
} else {
|
||||
debugConsole.log('[leave] Leaving now')
|
||||
return this._leaveDoc(callback)
|
||||
}
|
||||
}
|
||||
|
||||
flush() {
|
||||
return this.doc != null ? this.doc.flushPendingOps() : undefined
|
||||
}
|
||||
|
||||
chaosMonkey(line, char) {
|
||||
if (line == null) {
|
||||
line = 0
|
||||
}
|
||||
if (char == null) {
|
||||
char = 'a'
|
||||
}
|
||||
const orig = char
|
||||
let copy = null
|
||||
let pos = 0
|
||||
const timer = () => {
|
||||
if (copy == null || !copy.length) {
|
||||
copy = orig.slice() + ' ' + new Date() + '\n'
|
||||
line += Math.random() > 0.1 ? 1 : -2
|
||||
if (line < 0) {
|
||||
line = 0
|
||||
}
|
||||
pos = 0
|
||||
}
|
||||
char = copy[0]
|
||||
copy = copy.slice(1)
|
||||
if (this.cm6) {
|
||||
this.cm6.view.dispatch({
|
||||
changes: {
|
||||
from: Math.min(pos, this.cm6.view.state.doc.length),
|
||||
insert: char,
|
||||
},
|
||||
})
|
||||
}
|
||||
pos += 1
|
||||
return (this._cm = setTimeout(
|
||||
timer,
|
||||
100 + (Math.random() < 0.1 ? 1000 : 0)
|
||||
))
|
||||
}
|
||||
return (this._cm = timer())
|
||||
}
|
||||
|
||||
clearChaosMonkey() {
|
||||
const timer = this._cm
|
||||
if (timer) {
|
||||
delete this._cm
|
||||
return clearTimeout(timer)
|
||||
}
|
||||
}
|
||||
|
||||
pollSavedStatus() {
|
||||
// returns false if doc has ops waiting to be acknowledged or
|
||||
// sent that haven't changed since the last time we checked.
|
||||
// Otherwise returns true.
|
||||
let saved
|
||||
const inflightOp = this.getInflightOp()
|
||||
const pendingOp = this.getPendingOp()
|
||||
const recentAck = this.getRecentAck()
|
||||
const pendingOpSize = pendingOp != null && this.getOpSize(pendingOp)
|
||||
if (inflightOp == null && pendingOp == null) {
|
||||
// there's nothing going on, this is ok.
|
||||
saved = true
|
||||
debugConsole.log('[pollSavedStatus] no inflight or pending ops')
|
||||
} else if (inflightOp != null && inflightOp === this.oldInflightOp) {
|
||||
// The same inflight op has been sitting unacked since we
|
||||
// last checked, this is bad.
|
||||
saved = false
|
||||
debugConsole.log('[pollSavedStatus] inflight op is same as before')
|
||||
} else if (
|
||||
pendingOp != null &&
|
||||
recentAck &&
|
||||
pendingOpSize < this.MAX_PENDING_OP_SIZE
|
||||
) {
|
||||
// There is an op waiting to go to server but it is small and
|
||||
// within the flushDelay, this is ok for now.
|
||||
saved = true
|
||||
debugConsole.log(
|
||||
'[pollSavedStatus] pending op (small with recent ack) assume ok',
|
||||
pendingOp,
|
||||
pendingOpSize
|
||||
)
|
||||
} else {
|
||||
// In any other situation, assume the document is unsaved.
|
||||
saved = false
|
||||
debugConsole.log(
|
||||
`[pollSavedStatus] assuming not saved (inflightOp?: ${
|
||||
inflightOp != null
|
||||
}, pendingOp?: ${pendingOp != null})`
|
||||
)
|
||||
}
|
||||
|
||||
this.oldInflightOp = inflightOp
|
||||
return saved
|
||||
}
|
||||
|
||||
_cancelLeave() {
|
||||
if (this._leaveCallbacks != null) {
|
||||
return delete this._leaveCallbacks
|
||||
}
|
||||
}
|
||||
|
||||
_cancelJoin() {
|
||||
if (this._joinCallbacks != null) {
|
||||
return delete this._joinCallbacks
|
||||
}
|
||||
}
|
||||
|
||||
_onUpdateApplied(update) {
|
||||
this.ide.pushEvent('received-update', {
|
||||
doc_id: this.doc_id,
|
||||
remote_doc_id: update != null ? update.doc : undefined,
|
||||
wantToBeJoined: this.wantToBeJoined,
|
||||
update,
|
||||
hasDoc: this.doc != null,
|
||||
})
|
||||
|
||||
if (
|
||||
window.disconnectOnAck != null &&
|
||||
Math.random() < window.disconnectOnAck
|
||||
) {
|
||||
debugConsole.log('Disconnecting on ack', update)
|
||||
window._ide.socket.socket.disconnect()
|
||||
// Pretend we never received the ack
|
||||
return
|
||||
}
|
||||
|
||||
if (window.dropAcks != null && Math.random() < window.dropAcks) {
|
||||
if (update.op == null) {
|
||||
// Only drop our own acks, not collaborator updates
|
||||
debugConsole.log('Simulating a lost ack', update)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
(update != null ? update.doc : undefined) === this.doc_id &&
|
||||
this.doc != null
|
||||
) {
|
||||
this.ide.pushEvent('received-update:processing', {
|
||||
update,
|
||||
})
|
||||
// FIXME: change this back to processUpdateFromServer when redis fixed
|
||||
this.doc.processUpdateFromServerInOrder(update)
|
||||
|
||||
if (!this.wantToBeJoined) {
|
||||
return this.leave()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_onDisconnect() {
|
||||
debugConsole.log('[onDisconnect] disconnecting')
|
||||
this.connected = false
|
||||
this.joined = false
|
||||
return this.doc != null
|
||||
? this.doc.updateConnectionState('disconnected')
|
||||
: undefined
|
||||
}
|
||||
|
||||
_onReconnect() {
|
||||
debugConsole.log('[onReconnect] reconnected (joined project)')
|
||||
this.ide.pushEvent('reconnected:afterJoinProject')
|
||||
|
||||
this.connected = true
|
||||
if (
|
||||
this.wantToBeJoined ||
|
||||
(this.doc != null ? this.doc.hasBufferedOps() : undefined)
|
||||
) {
|
||||
debugConsole.log(
|
||||
`[onReconnect] Rejoining (wantToBeJoined: ${
|
||||
this.wantToBeJoined
|
||||
} OR hasBufferedOps: ${
|
||||
this.doc != null ? this.doc.hasBufferedOps() : undefined
|
||||
})`
|
||||
)
|
||||
return this._joinDoc(error => {
|
||||
if (error != null) {
|
||||
return this._onError(error)
|
||||
}
|
||||
this.doc.updateConnectionState('ok')
|
||||
this.doc.flushPendingOps()
|
||||
return this._callJoinCallbacks()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
_callJoinCallbacks() {
|
||||
for (const callback of Array.from(this._joinCallbacks || [])) {
|
||||
callback()
|
||||
}
|
||||
return delete this._joinCallbacks
|
||||
}
|
||||
|
||||
_joinDoc(callback) {
|
||||
if (callback == null) {
|
||||
callback = function () {}
|
||||
}
|
||||
if (this.doc != null) {
|
||||
this.ide.pushEvent('joinDoc:existing', {
|
||||
doc_id: this.doc_id,
|
||||
version: this.doc.getVersion(),
|
||||
})
|
||||
return this.ide.socket.emit(
|
||||
'joinDoc',
|
||||
this.doc_id,
|
||||
this.doc.getVersion(),
|
||||
{ encodeRanges: true },
|
||||
(error, docLines, version, updates, ranges) => {
|
||||
if (error != null) {
|
||||
return callback(error)
|
||||
}
|
||||
this.joined = true
|
||||
this.doc.catchUp(updates)
|
||||
this._decodeRanges(ranges)
|
||||
this._catchUpRanges(
|
||||
ranges != null ? ranges.changes : undefined,
|
||||
ranges != null ? ranges.comments : undefined
|
||||
)
|
||||
return callback()
|
||||
}
|
||||
)
|
||||
} else {
|
||||
this.ide.pushEvent('joinDoc:new', {
|
||||
doc_id: this.doc_id,
|
||||
})
|
||||
return this.ide.socket.emit(
|
||||
'joinDoc',
|
||||
this.doc_id,
|
||||
{ encodeRanges: true },
|
||||
(error, docLines, version, updates, ranges) => {
|
||||
if (error != null) {
|
||||
return callback(error)
|
||||
}
|
||||
this.joined = true
|
||||
this.ide.pushEvent('joinDoc:inited', {
|
||||
doc_id: this.doc_id,
|
||||
version,
|
||||
})
|
||||
this.doc = new ShareJsDoc(
|
||||
this.doc_id,
|
||||
docLines,
|
||||
version,
|
||||
this.ide.socket,
|
||||
this.ide.globalEditorWatchdogManager
|
||||
)
|
||||
this._decodeRanges(ranges)
|
||||
this.ranges = new RangesTracker(
|
||||
ranges != null ? ranges.changes : undefined,
|
||||
ranges != null ? ranges.comments : undefined
|
||||
)
|
||||
this._bindToShareJsDocEvents()
|
||||
return callback()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
_decodeRanges(ranges) {
|
||||
const decodeFromWebsockets = text => decodeURIComponent(escape(text))
|
||||
try {
|
||||
for (const change of Array.from(ranges.changes || [])) {
|
||||
if (change.op.i != null) {
|
||||
change.op.i = decodeFromWebsockets(change.op.i)
|
||||
}
|
||||
if (change.op.d != null) {
|
||||
change.op.d = decodeFromWebsockets(change.op.d)
|
||||
}
|
||||
}
|
||||
return (() => {
|
||||
const result = []
|
||||
for (const comment of Array.from(ranges.comments || [])) {
|
||||
if (comment.op.c != null) {
|
||||
result.push((comment.op.c = decodeFromWebsockets(comment.op.c)))
|
||||
} else {
|
||||
result.push(undefined)
|
||||
}
|
||||
}
|
||||
return result
|
||||
})()
|
||||
} catch (err) {
|
||||
debugConsole.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
_leaveDoc(callback) {
|
||||
if (callback == null) {
|
||||
callback = function () {}
|
||||
}
|
||||
this.ide.pushEvent('leaveDoc', {
|
||||
doc_id: this.doc_id,
|
||||
})
|
||||
debugConsole.log('[_leaveDoc] Sending leaveDoc request')
|
||||
return this.ide.socket.emit('leaveDoc', this.doc_id, error => {
|
||||
if (error != null) {
|
||||
return callback(error)
|
||||
}
|
||||
this.joined = false
|
||||
for (callback of Array.from(this._leaveCallbacks || [])) {
|
||||
debugConsole.log('[_leaveDoc] Calling buffered callback', callback)
|
||||
callback(error)
|
||||
}
|
||||
delete this._leaveCallbacks
|
||||
return callback(error)
|
||||
})
|
||||
}
|
||||
|
||||
_cleanUp() {
|
||||
// if we arrive here from _onError the pending and inflight ops will have been cleared
|
||||
if (this.hasBufferedOps()) {
|
||||
debugConsole.log(
|
||||
`[_cleanUp] Document (${this.doc_id}) has buffered ops, refusing to remove from openDocs`
|
||||
)
|
||||
return // return immediately, do not unbind from events
|
||||
} else if (Document.openDocs[this.doc_id] === this) {
|
||||
debugConsole.log(
|
||||
`[_cleanUp] Removing self (${this.doc_id}) from in openDocs`
|
||||
)
|
||||
delete Document.openDocs[this.doc_id]
|
||||
} else {
|
||||
// It's possible that this instance has error, and the doc has been reloaded.
|
||||
// This creates a new instance in Document.openDoc with the same id. We shouldn't
|
||||
// clear it because it's not this instance.
|
||||
debugConsole.log(
|
||||
`[_cleanUp] New instance of (${this.doc_id}) created. Not removing`
|
||||
)
|
||||
}
|
||||
this._unBindFromEditorEvents()
|
||||
return this._unBindFromSocketEvents()
|
||||
}
|
||||
|
||||
_bindToShareJsDocEvents() {
|
||||
this.doc.on('error', (error, meta) => this._onError(error, meta))
|
||||
this.doc.on('externalUpdate', update => {
|
||||
this.ide.pushEvent('externalUpdate', { doc_id: this.doc_id })
|
||||
return this.trigger('externalUpdate', update)
|
||||
})
|
||||
this.doc.on('remoteop', (...args) => {
|
||||
this.ide.pushEvent('remoteop', { doc_id: this.doc_id })
|
||||
return this.trigger('remoteop', ...Array.from(args))
|
||||
})
|
||||
this.doc.on('op:sent', op => {
|
||||
this.ide.pushEvent('op:sent', {
|
||||
doc_id: this.doc_id,
|
||||
op,
|
||||
})
|
||||
return this.trigger('op:sent')
|
||||
})
|
||||
this.doc.on('op:acknowledged', op => {
|
||||
this.ide.pushEvent('op:acknowledged', {
|
||||
doc_id: this.doc_id,
|
||||
op,
|
||||
})
|
||||
this.ide.$scope.$emit('ide:opAcknowledged', {
|
||||
doc_id: this.doc_id,
|
||||
op,
|
||||
})
|
||||
return this.trigger('op:acknowledged')
|
||||
})
|
||||
this.doc.on('op:timeout', op => {
|
||||
this.ide.pushEvent('op:timeout', {
|
||||
doc_id: this.doc_id,
|
||||
op,
|
||||
})
|
||||
this.trigger('op:timeout')
|
||||
return this._onError(new Error('op timed out'))
|
||||
})
|
||||
this.doc.on('flush', (inflightOp, pendingOp, version) => {
|
||||
return this.ide.pushEvent('flush', {
|
||||
doc_id: this.doc_id,
|
||||
inflightOp,
|
||||
pendingOp,
|
||||
v: version,
|
||||
})
|
||||
})
|
||||
|
||||
let docChangedTimeout
|
||||
this.doc.on('change', (ops, oldSnapshot, msg) => {
|
||||
this._applyOpsToRanges(ops, oldSnapshot, msg)
|
||||
if (docChangedTimeout) {
|
||||
window.clearTimeout(docChangedTimeout)
|
||||
}
|
||||
docChangedTimeout = window.setTimeout(() => {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('doc:changed', { detail: { id: this.doc_id } })
|
||||
)
|
||||
this.ide.$scope.$emit('doc:changed', { doc_id: this.doc_id })
|
||||
}, 50)
|
||||
})
|
||||
|
||||
this.doc.on('flipped_pending_to_inflight', () => {
|
||||
return this.trigger('flipped_pending_to_inflight')
|
||||
})
|
||||
|
||||
let docSavedTimeout
|
||||
this.doc.on('saved', () => {
|
||||
if (docSavedTimeout) {
|
||||
window.clearTimeout(docSavedTimeout)
|
||||
}
|
||||
docSavedTimeout = window.setTimeout(() => {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('doc:saved', { detail: { id: this.doc_id } })
|
||||
)
|
||||
this.ide.$scope.$emit('doc:saved', { doc_id: this.doc_id })
|
||||
}, 50)
|
||||
})
|
||||
}
|
||||
|
||||
_onError(error, meta, editorContent) {
|
||||
if (meta == null) {
|
||||
meta = {}
|
||||
}
|
||||
meta.doc_id = this.doc_id
|
||||
debugConsole.log('ShareJS error', error, meta)
|
||||
if (error.message === 'no project_id found on client') {
|
||||
debugConsole.log('ignoring error, will wait to join project')
|
||||
return
|
||||
}
|
||||
if (this.doc != null) {
|
||||
this.doc.clearInflightAndPendingOps()
|
||||
}
|
||||
this.trigger('error', error, meta, editorContent)
|
||||
// The clean up should run after the error is triggered because the error triggers a
|
||||
// disconnect. If we run the clean up first, we remove our event handlers and miss
|
||||
// the disconnect event, which means we try to leaveDoc when the connection comes back.
|
||||
// This could intefere with the new connection of a new instance of this document.
|
||||
return this._cleanUp()
|
||||
}
|
||||
|
||||
_applyOpsToRanges(ops, oldSnapshot, msg) {
|
||||
let old_id_seed
|
||||
if (ops == null) {
|
||||
ops = []
|
||||
}
|
||||
let track_changes_as = null
|
||||
const remote_op = msg != null
|
||||
if (__guard__(msg != null ? msg.meta : undefined, x => x.tc) != null) {
|
||||
old_id_seed = this.ranges.getIdSeed()
|
||||
this.ranges.setIdSeed(msg.meta.tc)
|
||||
}
|
||||
if (remote_op && (msg.meta != null ? msg.meta.tc : undefined)) {
|
||||
track_changes_as = msg.meta.user_id
|
||||
} else if (!remote_op && this.track_changes_as != null) {
|
||||
;({ track_changes_as } = this)
|
||||
}
|
||||
this.ranges.track_changes = track_changes_as != null
|
||||
for (const op of this._filterOps(ops)) {
|
||||
this.ranges.applyOp(op, { user_id: track_changes_as })
|
||||
}
|
||||
if (old_id_seed != null) {
|
||||
this.ranges.setIdSeed(old_id_seed)
|
||||
}
|
||||
if (remote_op) {
|
||||
// With remote ops, the editor hasn't been updated when we receive this op,
|
||||
// so defer updating track changes until it has
|
||||
return setTimeout(() => this.emit('ranges:dirty'))
|
||||
} else {
|
||||
return this.emit('ranges:dirty')
|
||||
}
|
||||
}
|
||||
|
||||
_catchUpRanges(changes, comments) {
|
||||
// We've just been given the current server's ranges, but need to apply any local ops we have.
|
||||
// Reset to the server state then apply our local ops again.
|
||||
if (changes == null) {
|
||||
changes = []
|
||||
}
|
||||
if (comments == null) {
|
||||
comments = []
|
||||
}
|
||||
this.emit('ranges:clear')
|
||||
this.ranges.changes = changes
|
||||
this.ranges.comments = comments
|
||||
this.ranges.track_changes = this.doc.track_changes
|
||||
for (const op of this._filterOps(this.doc.getInflightOp() || [])) {
|
||||
this.ranges.setIdSeed(this.doc.track_changes_id_seeds.inflight)
|
||||
this.ranges.applyOp(op, { user_id: this.track_changes_as })
|
||||
}
|
||||
for (const op of this._filterOps(this.doc.getPendingOp() || [])) {
|
||||
this.ranges.setIdSeed(this.doc.track_changes_id_seeds.pending)
|
||||
this.ranges.applyOp(op, { user_id: this.track_changes_as })
|
||||
}
|
||||
return this.emit('ranges:redraw')
|
||||
}
|
||||
|
||||
_filterOps(ops) {
|
||||
// Read-only token users can't see/edit comment, so we filter out comment
|
||||
// ops to avoid highlighting comment ranges.
|
||||
if (window.isRestrictedTokenMember) {
|
||||
return ops.filter(op => op.c == null)
|
||||
} else {
|
||||
return ops
|
||||
}
|
||||
}
|
||||
}
|
||||
Document.initClass()
|
||||
return Document
|
||||
})()
|
||||
|
||||
function __guard__(value, transform) {
|
||||
return typeof value !== 'undefined' && value !== null
|
||||
? transform(value)
|
||||
: undefined
|
||||
}
|
|
@ -1,510 +0,0 @@
|
|||
import _ from 'lodash'
|
||||
/* eslint-disable
|
||||
camelcase,
|
||||
n/handle-callback-err,
|
||||
max-len,
|
||||
no-return-assign,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* DS206: Consider reworking classes to avoid initClass
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
import Document from './Document'
|
||||
import './directives/formattingButtons'
|
||||
import './directives/toggleSwitch'
|
||||
import './controllers/SavingNotificationController'
|
||||
import './controllers/CompileButton'
|
||||
import './controllers/SwitchToPDFButton'
|
||||
import '../metadata/services/metadata'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
import customLocalStorage from '@/infrastructure/local-storage'
|
||||
|
||||
let EditorManager
|
||||
|
||||
export default EditorManager = (function () {
|
||||
EditorManager = class EditorManager {
|
||||
static initClass() {
|
||||
this.prototype._syncTimeout = null
|
||||
}
|
||||
|
||||
constructor(ide, $scope, localStorage, eventTracking) {
|
||||
this.ide = ide
|
||||
this.editorOpenDocEpoch = 0 // track pending document loads
|
||||
this.$scope = $scope
|
||||
this.localStorage = localStorage
|
||||
this.$scope.editor = {
|
||||
sharejs_doc: null,
|
||||
open_doc_id: null,
|
||||
open_doc_name: null,
|
||||
opening: true,
|
||||
trackChanges: false,
|
||||
wantTrackChanges: false,
|
||||
docTooLongErrorShown: false,
|
||||
showVisual: this.showVisual(),
|
||||
showSymbolPalette: false,
|
||||
toggleSymbolPalette: () => {
|
||||
const newValue = !this.$scope.editor.showSymbolPalette
|
||||
this.$scope.editor.showSymbolPalette = newValue
|
||||
ide.$scope.$emit('south-pane-toggled', newValue)
|
||||
eventTracking.sendMB(
|
||||
newValue ? 'symbol-palette-show' : 'symbol-palette-hide'
|
||||
)
|
||||
},
|
||||
insertSymbol: symbol => {
|
||||
ide.$scope.$emit('editor:replace-selection', symbol.command)
|
||||
eventTracking.sendMB('symbol-palette-insert')
|
||||
},
|
||||
multiSelectedCount: 0,
|
||||
}
|
||||
|
||||
window.addEventListener('editor:insert-symbol', event => {
|
||||
this.$scope.editor.insertSymbol(event.detail)
|
||||
})
|
||||
|
||||
this.$scope.$on('entity:selected', (event, entity) => {
|
||||
if (this.$scope.ui.view !== 'history' && entity.type === 'doc') {
|
||||
return this.openDoc(entity)
|
||||
}
|
||||
})
|
||||
|
||||
this.$scope.$on('entity:no-selection', () => {
|
||||
this.$scope.$apply(() => {
|
||||
this.$scope.ui.view = null
|
||||
})
|
||||
})
|
||||
|
||||
this.$scope.$on('entity:deleted', (event, entity) => {
|
||||
if (this.$scope.editor.open_doc_id === entity.id) {
|
||||
if (!this.$scope.project.rootDoc_id) {
|
||||
this.$scope.ui.view = null
|
||||
return
|
||||
}
|
||||
const doc = this.ide.fileTreeManager.findEntityById(
|
||||
this.$scope.project.rootDoc_id
|
||||
)
|
||||
if (doc == null) {
|
||||
this.$scope.ui.view = null
|
||||
return
|
||||
}
|
||||
return this.openDoc(doc)
|
||||
}
|
||||
})
|
||||
|
||||
let initialized = false
|
||||
this.$scope.$on('file-tree:initialized', () => {
|
||||
if (!initialized) {
|
||||
initialized = true
|
||||
return this.autoOpenDoc()
|
||||
}
|
||||
})
|
||||
|
||||
this.$scope.$on('flush-changes', () => {
|
||||
return Document.flushAll()
|
||||
})
|
||||
|
||||
// event dispatched by pdf preview
|
||||
window.addEventListener('flush-changes', () => {
|
||||
Document.flushAll()
|
||||
})
|
||||
|
||||
window.addEventListener('blur', () => {
|
||||
// The browser may put the tab into sleep as it looses focus.
|
||||
// Flushing the documents should help with keeping the documents in
|
||||
// sync: we can use any new version of the doc that the server may
|
||||
// present us. There should be no need to insert local changes into
|
||||
// the doc history as the user comes back.
|
||||
debugConsole.log('[EditorManager] forcing flush onblur')
|
||||
Document.flushAll()
|
||||
})
|
||||
|
||||
this.$scope.$watch('editor.wantTrackChanges', value => {
|
||||
if (value == null) {
|
||||
return
|
||||
}
|
||||
return this._syncTrackChangesState(this.$scope.editor.sharejs_doc)
|
||||
})
|
||||
|
||||
window.addEventListener('editor:open-doc', event => {
|
||||
const { doc, ...options } = event.detail
|
||||
this.openDoc(doc, options)
|
||||
})
|
||||
|
||||
window.addEventListener('editor:open-file', event => {
|
||||
const { name, ...options } = event.detail
|
||||
for (const extension of ['', '.tex']) {
|
||||
const path = `${name}${extension}`
|
||||
const doc = ide.fileTreeManager.findEntityByPath(path)
|
||||
if (doc) {
|
||||
this.openDoc(doc, options)
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
getEditorType() {
|
||||
if (!this.$scope.editor.sharejs_doc) {
|
||||
return null
|
||||
}
|
||||
|
||||
let editorType = this.$scope.editor.sharejs_doc.editorType()
|
||||
|
||||
if (editorType === 'cm6' && this.$scope.editor.showVisual) {
|
||||
editorType = 'cm6-rich-text'
|
||||
}
|
||||
|
||||
return editorType
|
||||
}
|
||||
|
||||
showVisual() {
|
||||
const editorModeKey = `editor.mode.${this.$scope.project_id}`
|
||||
const editorModeVal = this.localStorage(editorModeKey)
|
||||
|
||||
if (editorModeVal) {
|
||||
// clean up the old key
|
||||
customLocalStorage.removeItem(editorModeKey)
|
||||
}
|
||||
|
||||
const lastUsedMode = this.localStorage(`editor.lastUsedMode`)
|
||||
if (lastUsedMode) {
|
||||
return lastUsedMode === 'visual'
|
||||
} else {
|
||||
return editorModeVal === 'rich-text'
|
||||
}
|
||||
}
|
||||
|
||||
autoOpenDoc() {
|
||||
const open_doc_id =
|
||||
this.ide.localStorage(`doc.open_id.${this.$scope.project_id}`) ||
|
||||
this.$scope.project.rootDoc_id
|
||||
if (open_doc_id == null) {
|
||||
return
|
||||
}
|
||||
const doc = this.ide.fileTreeManager.findEntityById(open_doc_id)
|
||||
if (doc == null) {
|
||||
return
|
||||
}
|
||||
return this.openDoc(doc)
|
||||
}
|
||||
|
||||
openDocId(doc_id, options) {
|
||||
if (options == null) {
|
||||
options = {}
|
||||
}
|
||||
const doc = this.ide.fileTreeManager.findEntityById(doc_id)
|
||||
if (doc == null) {
|
||||
return
|
||||
}
|
||||
return this.openDoc(doc, options)
|
||||
}
|
||||
|
||||
jumpToLine(options) {
|
||||
return this.$scope.$broadcast(
|
||||
'editor:gotoLine',
|
||||
options.gotoLine,
|
||||
options.gotoColumn,
|
||||
options.syncToPdf
|
||||
)
|
||||
}
|
||||
|
||||
openDoc(doc, options) {
|
||||
if (options == null) {
|
||||
options = {}
|
||||
}
|
||||
debugConsole.log(`[openDoc] Opening ${doc.id}`)
|
||||
if (this.$scope.ui.view === 'editor') {
|
||||
// store position of previous doc before switching docs
|
||||
this.$scope.$broadcast('store-doc-position')
|
||||
}
|
||||
this.$scope.ui.view = 'editor'
|
||||
|
||||
const done = isNewDoc => {
|
||||
const eventName = 'doc:after-opened'
|
||||
this.$scope.$broadcast(eventName, { isNewDoc })
|
||||
window.dispatchEvent(new CustomEvent(eventName, { detail: isNewDoc }))
|
||||
if (options.gotoLine != null) {
|
||||
// allow Ace to display document before moving, delay until next tick
|
||||
// added delay to make this happen later that gotoStoredPosition in
|
||||
// CursorPositionManager
|
||||
setTimeout(() => this.jumpToLine(options))
|
||||
// when opening a doc in CM6, jump to the line again after a stored scroll position has been restored
|
||||
if (isNewDoc) {
|
||||
window.addEventListener(
|
||||
'editor:scroll-position-restored',
|
||||
() => this.jumpToLine(options),
|
||||
{ once: true }
|
||||
)
|
||||
}
|
||||
} else if (options.gotoOffset != null) {
|
||||
setTimeout(() => {
|
||||
this.$scope.$broadcast('editor:gotoOffset', options.gotoOffset)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// If we already have the document open we can return at this point.
|
||||
// Note: only use forceReopen:true to override this when the document is
|
||||
// is out of sync and needs to be reloaded from the server.
|
||||
if (doc.id === this.$scope.editor.open_doc_id && !options.forceReopen) {
|
||||
// automatically update the file tree whenever the file is opened
|
||||
this.ide.fileTreeManager.selectEntity(doc)
|
||||
this.$scope.$broadcast('file-tree.reselectDoc', doc.id)
|
||||
this.$scope.$apply(() => {
|
||||
return done(false)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
this.$scope.$applyAsync(() => {
|
||||
// We're now either opening a new document or reloading a broken one.
|
||||
this.$scope.editor.open_doc_id = doc.id
|
||||
this.$scope.editor.open_doc_name = doc.name
|
||||
|
||||
this.ide.localStorage(`doc.open_id.${this.$scope.project_id}`, doc.id)
|
||||
this.ide.fileTreeManager.selectEntity(doc)
|
||||
|
||||
this.$scope.editor.opening = true
|
||||
return this._openNewDocument(doc, (error, sharejs_doc) => {
|
||||
if (error && error.message === 'another document was loaded') {
|
||||
debugConsole.log(
|
||||
`[openDoc] another document was loaded while ${doc.id} was loading`
|
||||
)
|
||||
return
|
||||
}
|
||||
if (error != null) {
|
||||
this.ide.showGenericMessageModal(
|
||||
'Error opening document',
|
||||
'Sorry, something went wrong opening this document. Please try again.'
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
this._syncTrackChangesState(sharejs_doc)
|
||||
|
||||
this.$scope.$broadcast('doc:opened')
|
||||
|
||||
return this.$scope.$applyAsync(() => {
|
||||
this.$scope.editor.opening = false
|
||||
this.$scope.editor.sharejs_doc = sharejs_doc
|
||||
return done(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
_openNewDocument(doc, callback) {
|
||||
// Leave the current document
|
||||
// - when we are opening a different new one, to avoid race conditions
|
||||
// between leaving and joining the same document
|
||||
// - when the current one has pending ops that need flushing, to avoid
|
||||
// race conditions from cleanup
|
||||
const current_sharejs_doc = this.$scope.editor.sharejs_doc
|
||||
const currentDocId = current_sharejs_doc && current_sharejs_doc.doc_id
|
||||
const hasBufferedOps =
|
||||
current_sharejs_doc && current_sharejs_doc.hasBufferedOps()
|
||||
const changingDoc = current_sharejs_doc && currentDocId !== doc.id
|
||||
if (changingDoc || hasBufferedOps) {
|
||||
debugConsole.log('[_openNewDocument] Leaving existing open doc...')
|
||||
|
||||
// Do not trigger any UI changes from remote operations
|
||||
this._unbindFromDocumentEvents(current_sharejs_doc)
|
||||
// Keep listening for out-of-sync and similar errors.
|
||||
this._attachErrorHandlerToDocument(doc, current_sharejs_doc)
|
||||
|
||||
// Teardown the Document -> ShareJsDoc -> sharejs doc
|
||||
// By the time this completes, the Document instance is no longer
|
||||
// registered in Document.openDocs and _doOpenNewDocument can start
|
||||
// from scratch -- read: no corrupted internal state.
|
||||
const editorOpenDocEpoch = ++this.editorOpenDocEpoch
|
||||
current_sharejs_doc.leaveAndCleanUp(error => {
|
||||
if (error) {
|
||||
debugConsole.log(
|
||||
`[_openNewDocument] error leaving doc ${currentDocId}`,
|
||||
error
|
||||
)
|
||||
return callback(error)
|
||||
}
|
||||
if (this.editorOpenDocEpoch !== editorOpenDocEpoch) {
|
||||
debugConsole.log(
|
||||
`[openNewDocument] editorOpenDocEpoch mismatch ${this.editorOpenDocEpoch} vs ${editorOpenDocEpoch}`
|
||||
)
|
||||
return callback(new Error('another document was loaded'))
|
||||
}
|
||||
this._doOpenNewDocument(doc, callback)
|
||||
})
|
||||
} else {
|
||||
this._doOpenNewDocument(doc, callback)
|
||||
}
|
||||
}
|
||||
|
||||
_doOpenNewDocument(doc, callback) {
|
||||
if (callback == null) {
|
||||
callback = function () {}
|
||||
}
|
||||
debugConsole.log('[_doOpenNewDocument] Opening...')
|
||||
const new_sharejs_doc = Document.getDocument(this.ide, doc.id)
|
||||
const editorOpenDocEpoch = ++this.editorOpenDocEpoch
|
||||
return new_sharejs_doc.join(error => {
|
||||
if (error != null) {
|
||||
debugConsole.log(
|
||||
`[_doOpenNewDocument] error joining doc ${doc.id}`,
|
||||
error
|
||||
)
|
||||
return callback(error)
|
||||
}
|
||||
if (this.editorOpenDocEpoch !== editorOpenDocEpoch) {
|
||||
debugConsole.log(
|
||||
`[openNewDocument] editorOpenDocEpoch mismatch ${this.editorOpenDocEpoch} vs ${editorOpenDocEpoch}`
|
||||
)
|
||||
new_sharejs_doc.leaveAndCleanUp()
|
||||
return callback(new Error('another document was loaded'))
|
||||
}
|
||||
this._bindToDocumentEvents(doc, new_sharejs_doc)
|
||||
return callback(null, new_sharejs_doc)
|
||||
})
|
||||
}
|
||||
|
||||
_attachErrorHandlerToDocument(doc, sharejs_doc) {
|
||||
sharejs_doc.on('error', (error, meta, editorContent) => {
|
||||
let message
|
||||
if ((error != null ? error.message : undefined) != null) {
|
||||
;({ message } = error)
|
||||
} else if (typeof error === 'string') {
|
||||
message = error
|
||||
} else {
|
||||
message = ''
|
||||
}
|
||||
if (/maxDocLength/.test(message)) {
|
||||
this.$scope.docTooLongErrorShown = true
|
||||
this.openDoc(doc, { forceReopen: true })
|
||||
const genericMessageModal = this.ide.showGenericMessageModal(
|
||||
'Document Too Long',
|
||||
'Sorry, this file is too long to be edited manually. Please upload it directly.'
|
||||
)
|
||||
genericMessageModal.result.finally(() => {
|
||||
this.$scope.docTooLongErrorShown = false
|
||||
})
|
||||
} else if (/too many comments or tracked changes/.test(message)) {
|
||||
this.ide.showGenericMessageModal(
|
||||
'Too many comments or tracked changes',
|
||||
'Sorry, this file has too many comments or tracked changes. Please try accepting or rejecting some existing changes, or resolving and deleting some comments.'
|
||||
)
|
||||
} else if (!this.$scope.docTooLongErrorShown) {
|
||||
// Do not allow this doc to open another error modal.
|
||||
sharejs_doc.off('error')
|
||||
|
||||
// Preserve the sharejs contents before the teardown.
|
||||
editorContent =
|
||||
typeof editorContent === 'string'
|
||||
? editorContent
|
||||
: sharejs_doc.doc._doc.snapshot
|
||||
|
||||
// Tear down the ShareJsDoc.
|
||||
if (sharejs_doc.doc) sharejs_doc.doc.clearInflightAndPendingOps()
|
||||
|
||||
// Do not re-join after re-connecting.
|
||||
sharejs_doc.leaveAndCleanUp()
|
||||
this.ide.connectionManager.disconnect({ permanent: true })
|
||||
this.ide.reportError(error, meta)
|
||||
|
||||
// Tell the user about the error state.
|
||||
this.$scope.editor.error_state = true
|
||||
this.ide.showOutOfSyncModal(
|
||||
'Out of sync',
|
||||
"Sorry, this file has gone out of sync and we need to do a full refresh. <br> <a target='_blank' rel='noopener noreferrer' href='/learn/Kb/Editor_out_of_sync_problems'>Please see this help guide for more information</a>",
|
||||
editorContent
|
||||
)
|
||||
// Do not forceReopen the document.
|
||||
return
|
||||
}
|
||||
const removeHandler = this.$scope.$on('project:joined', () => {
|
||||
this.openDoc(doc, { forceReopen: true })
|
||||
removeHandler()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
_bindToDocumentEvents(doc, sharejs_doc) {
|
||||
this._attachErrorHandlerToDocument(doc, sharejs_doc)
|
||||
|
||||
return sharejs_doc.on('externalUpdate', update => {
|
||||
if (this._ignoreExternalUpdates) {
|
||||
return
|
||||
}
|
||||
if (
|
||||
_.property(['meta', 'type'])(update) === 'external' &&
|
||||
_.property(['meta', 'source'])(update) === 'git-bridge'
|
||||
) {
|
||||
return
|
||||
}
|
||||
if (update?.meta?.source === 'file-revert') {
|
||||
return
|
||||
}
|
||||
return this.ide.showGenericMessageModal(
|
||||
'Document Updated Externally',
|
||||
'This document was just updated externally. Any recent changes you have made may have been overwritten. To see previous versions please look in the history.'
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
_unbindFromDocumentEvents(document) {
|
||||
return document.off()
|
||||
}
|
||||
|
||||
getCurrentDocValue() {
|
||||
return this.$scope.editor.sharejs_doc != null
|
||||
? this.$scope.editor.sharejs_doc.getSnapshot()
|
||||
: undefined
|
||||
}
|
||||
|
||||
getCurrentDocId() {
|
||||
return this.$scope.editor.open_doc_id
|
||||
}
|
||||
|
||||
startIgnoringExternalUpdates() {
|
||||
return (this._ignoreExternalUpdates = true)
|
||||
}
|
||||
|
||||
stopIgnoringExternalUpdates() {
|
||||
return (this._ignoreExternalUpdates = false)
|
||||
}
|
||||
|
||||
_syncTrackChangesState(doc) {
|
||||
let tryToggle
|
||||
if (doc == null) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this._syncTimeout != null) {
|
||||
clearTimeout(this._syncTimeout)
|
||||
this._syncTimeout = null
|
||||
}
|
||||
|
||||
const want = this.$scope.editor.wantTrackChanges
|
||||
const have = doc.getTrackingChanges()
|
||||
if (want === have) {
|
||||
this.$scope.editor.trackChanges = want
|
||||
return
|
||||
}
|
||||
|
||||
return (tryToggle = () => {
|
||||
const saved = doc.getInflightOp() == null && doc.getPendingOp() == null
|
||||
if (saved) {
|
||||
doc.setTrackingChanges(want)
|
||||
return this.$scope.$apply(() => {
|
||||
return (this.$scope.editor.trackChanges = want)
|
||||
})
|
||||
} else {
|
||||
return (this._syncTimeout = setTimeout(tryToggle, 100))
|
||||
}
|
||||
})()
|
||||
}
|
||||
}
|
||||
EditorManager.initClass()
|
||||
return EditorManager
|
||||
})()
|
|
@ -1,50 +0,0 @@
|
|||
/* eslint-disable
|
||||
max-len,
|
||||
no-return-assign,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
let EditorShareJsCodec
|
||||
|
||||
export default EditorShareJsCodec = {
|
||||
rangeToShareJs(range, lines) {
|
||||
let offset = 0
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i]
|
||||
offset += i < range.row ? line.length : range.column
|
||||
}
|
||||
offset += range.row // Include newlines
|
||||
return offset
|
||||
},
|
||||
|
||||
changeToShareJs(delta, lines) {
|
||||
const offset = EditorShareJsCodec.rangeToShareJs(delta.start, lines)
|
||||
|
||||
const text = delta.lines.join('\n')
|
||||
switch (delta.action) {
|
||||
case 'insert':
|
||||
return { i: text, p: offset }
|
||||
case 'remove':
|
||||
return { d: text, p: offset }
|
||||
default:
|
||||
throw new Error(`unknown action: ${delta.action}`)
|
||||
}
|
||||
},
|
||||
|
||||
shareJsOffsetToRowColumn(offset, lines) {
|
||||
let row = 0
|
||||
for (row = 0; row < lines.length; row++) {
|
||||
const line = lines[row]
|
||||
if (offset <= line.length) {
|
||||
break
|
||||
}
|
||||
offset -= lines[row].length + 1
|
||||
} // + 1 for newline char
|
||||
return { row, column: offset }
|
||||
},
|
||||
}
|
|
@ -1,471 +0,0 @@
|
|||
/* eslint-disable
|
||||
camelcase,
|
||||
max-len,
|
||||
no-unused-vars,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS001: Remove Babel/TypeScript constructor workaround
|
||||
* DS101: Remove unnecessary use of Array.from
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* DS103: Rewrite code to no longer use __guard__
|
||||
* DS205: Consider reworking code to avoid use of IIFEs
|
||||
* DS206: Consider reworking classes to avoid initClass
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
import EventEmitter from '../../utils/EventEmitter'
|
||||
import ShareJs from '../../vendor/libs/sharejs'
|
||||
import EditorWatchdogManager from '../connection/EditorWatchdogManager'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
import { recordDocumentFirstChangeEvent } from '@/features/event-tracking/document-first-change-event'
|
||||
|
||||
let ShareJsDoc
|
||||
const SINGLE_USER_FLUSH_DELAY = 2000 // ms
|
||||
const MULTI_USER_FLUSH_DELAY = 500 // ms
|
||||
|
||||
export default ShareJsDoc = (function () {
|
||||
ShareJsDoc = class ShareJsDoc extends EventEmitter {
|
||||
static initClass() {
|
||||
this.prototype.INFLIGHT_OP_TIMEOUT = 5000 // Retry sending ops after 5 seconds without an ack
|
||||
this.prototype.WAIT_FOR_CONNECTION_TIMEOUT = 500
|
||||
|
||||
this.prototype.FATAL_OP_TIMEOUT = 30000
|
||||
}
|
||||
|
||||
constructor(
|
||||
doc_id,
|
||||
docLines,
|
||||
version,
|
||||
socket,
|
||||
globalEditorWatchdogManager
|
||||
) {
|
||||
super()
|
||||
// Dencode any binary bits of data
|
||||
// See http://ecmanaut.blogspot.co.uk/2006/07/encoding-decoding-utf8-in-javascript.html
|
||||
this.doc_id = doc_id
|
||||
this.socket = socket
|
||||
this.type = 'text'
|
||||
docLines = Array.from(docLines).map(line =>
|
||||
decodeURIComponent(escape(line))
|
||||
)
|
||||
const snapshot = docLines.join('\n')
|
||||
this.track_changes = false
|
||||
|
||||
this.connection = {
|
||||
send: update => {
|
||||
this._startInflightOpTimeout(update)
|
||||
if (
|
||||
window.disconnectOnUpdate != null &&
|
||||
Math.random() < window.disconnectOnUpdate
|
||||
) {
|
||||
debugConsole.log('Disconnecting on update', update)
|
||||
window._ide.socket.socket.disconnect()
|
||||
}
|
||||
if (
|
||||
window.dropUpdates != null &&
|
||||
Math.random() < window.dropUpdates
|
||||
) {
|
||||
debugConsole.log('Simulating a lost update', update)
|
||||
return
|
||||
}
|
||||
if (this.track_changes) {
|
||||
if (update.meta == null) {
|
||||
update.meta = {}
|
||||
}
|
||||
update.meta.tc = this.track_changes_id_seeds.inflight
|
||||
}
|
||||
return this.socket.emit(
|
||||
'applyOtUpdate',
|
||||
this.doc_id,
|
||||
update,
|
||||
error => {
|
||||
if (error != null) {
|
||||
return this._handleError(error)
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
state: 'ok',
|
||||
id: this.socket.publicId,
|
||||
}
|
||||
|
||||
this._doc = new ShareJs.Doc(this.connection, this.doc_id, {
|
||||
type: this.type,
|
||||
})
|
||||
this._doc.setFlushDelay(SINGLE_USER_FLUSH_DELAY)
|
||||
this._doc.on('change', (...args) => {
|
||||
return this.trigger('change', ...Array.from(args))
|
||||
})
|
||||
this.EditorWatchdogManager = new EditorWatchdogManager({
|
||||
parent: globalEditorWatchdogManager,
|
||||
})
|
||||
this._doc.on('acknowledge', () => {
|
||||
this.lastAcked = new Date() // note time of last ack from server for an op we sent
|
||||
this.EditorWatchdogManager.onAck() // keep track of last ack globally
|
||||
return this.trigger('acknowledge')
|
||||
})
|
||||
this._doc.on('remoteop', (...args) => {
|
||||
// As soon as we're working with a collaborator, start sending
|
||||
// ops more frequently for low latency.
|
||||
this._doc.setFlushDelay(MULTI_USER_FLUSH_DELAY)
|
||||
return this.trigger('remoteop', ...Array.from(args))
|
||||
})
|
||||
this._doc.on('flipped_pending_to_inflight', () => {
|
||||
return this.trigger('flipped_pending_to_inflight')
|
||||
})
|
||||
this._doc.on('saved', () => {
|
||||
return this.trigger('saved')
|
||||
})
|
||||
this._doc.on('error', e => {
|
||||
return this._handleError(e)
|
||||
})
|
||||
|
||||
this._bindToDocChanges(this._doc)
|
||||
|
||||
this.processUpdateFromServer({
|
||||
open: true,
|
||||
v: version,
|
||||
snapshot,
|
||||
})
|
||||
this._removeCarriageReturnCharFromShareJsDoc()
|
||||
}
|
||||
|
||||
_removeCarriageReturnCharFromShareJsDoc() {
|
||||
const doc = this._doc
|
||||
if (doc.snapshot.indexOf('\r') === -1) {
|
||||
return
|
||||
}
|
||||
window._ide.pushEvent('remove-carriage-return-char', {
|
||||
doc_id: this.doc_id,
|
||||
})
|
||||
let nextPos
|
||||
while ((nextPos = doc.snapshot.indexOf('\r')) !== -1) {
|
||||
debugConsole.log('[ShareJsDoc] remove-carriage-return-char', nextPos)
|
||||
doc.del(nextPos, 1)
|
||||
}
|
||||
}
|
||||
|
||||
submitOp(...args) {
|
||||
return this._doc.submitOp(...Array.from(args || []))
|
||||
}
|
||||
|
||||
// The following code puts out of order messages into a queue
|
||||
// so that they can be processed in order. This is a workaround
|
||||
// for messages being delayed by redis cluster.
|
||||
// FIXME: REMOVE THIS WHEN REDIS PUBSUB IS SENDING MESSAGES IN ORDER
|
||||
_isAheadOfExpectedVersion(message) {
|
||||
return this._doc.version > 0 && message.v > this._doc.version
|
||||
}
|
||||
|
||||
_pushOntoQueue(message) {
|
||||
debugConsole.log(`[processUpdate] push onto queue ${message.v}`)
|
||||
// set a timer so that we never leave messages in the queue indefinitely
|
||||
if (!this.queuedMessageTimer) {
|
||||
this.queuedMessageTimer = setTimeout(() => {
|
||||
debugConsole.log(
|
||||
`[processUpdate] queue timeout fired for ${message.v}`
|
||||
)
|
||||
// force the message to be processed after the timeout,
|
||||
// it will cause an error if the missing update has not arrived
|
||||
this.processUpdateFromServer(message)
|
||||
}, this.INFLIGHT_OP_TIMEOUT)
|
||||
}
|
||||
this.queuedMessages.push(message)
|
||||
// keep the queue in order, lowest version first
|
||||
this.queuedMessages.sort(function (a, b) {
|
||||
return a.v - b.v
|
||||
})
|
||||
}
|
||||
|
||||
_clearQueue() {
|
||||
this.queuedMessages = []
|
||||
}
|
||||
|
||||
_processQueue() {
|
||||
if (this.queuedMessages.length > 0) {
|
||||
const nextAvailableVersion = this.queuedMessages[0].v
|
||||
if (nextAvailableVersion > this._doc.version) {
|
||||
// there are updates we still can't apply yet
|
||||
} else {
|
||||
// there's a version we can accept on the queue, apply it
|
||||
debugConsole.log(
|
||||
`[processUpdate] taken from queue ${nextAvailableVersion}`
|
||||
)
|
||||
this.processUpdateFromServerInOrder(this.queuedMessages.shift())
|
||||
// clear the pending timer if the queue has now been cleared
|
||||
if (this.queuedMessages.length === 0 && this.queuedMessageTimer) {
|
||||
debugConsole.log('[processUpdate] queue is empty, cleared timeout')
|
||||
clearTimeout(this.queuedMessageTimer)
|
||||
this.queuedMessageTimer = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: This is the new method which reorders incoming updates if needed
|
||||
// called from Document.js
|
||||
processUpdateFromServerInOrder(message) {
|
||||
// Create an array to hold queued messages
|
||||
if (!this.queuedMessages) {
|
||||
this.queuedMessages = []
|
||||
}
|
||||
// Is this update ahead of the next expected update?
|
||||
// If so, put it on a queue to be handled later.
|
||||
if (this._isAheadOfExpectedVersion(message)) {
|
||||
this._pushOntoQueue(message)
|
||||
return // defer processing this update for now
|
||||
}
|
||||
const error = this.processUpdateFromServer(message)
|
||||
if (
|
||||
error instanceof Error &&
|
||||
error.message === 'Invalid version from server'
|
||||
) {
|
||||
// if there was an error, abandon the queued updates ahead of this one
|
||||
this._clearQueue()
|
||||
return
|
||||
}
|
||||
// Do we have any messages queued up?
|
||||
// find the next message if available
|
||||
this._processQueue()
|
||||
}
|
||||
|
||||
// FIXME: This is the original method. Switch back to this when redis
|
||||
// issues are resolved.
|
||||
processUpdateFromServer(message) {
|
||||
try {
|
||||
this._doc._onMessage(message)
|
||||
} catch (error) {
|
||||
// Version mismatches are thrown as errors
|
||||
debugConsole.error(error)
|
||||
this._handleError(error)
|
||||
return error // return the error for queue handling
|
||||
}
|
||||
|
||||
if (
|
||||
__guard__(message != null ? message.meta : undefined, x => x.type) ===
|
||||
'external'
|
||||
) {
|
||||
return this.trigger('externalUpdate', message)
|
||||
}
|
||||
}
|
||||
|
||||
catchUp(updates) {
|
||||
return (() => {
|
||||
const result = []
|
||||
for (let i = 0; i < updates.length; i++) {
|
||||
const update = updates[i]
|
||||
update.v = this._doc.version
|
||||
update.doc = this.doc_id
|
||||
result.push(this.processUpdateFromServer(update))
|
||||
}
|
||||
return result
|
||||
})()
|
||||
}
|
||||
|
||||
getSnapshot() {
|
||||
return this._doc.snapshot
|
||||
}
|
||||
|
||||
getVersion() {
|
||||
return this._doc.version
|
||||
}
|
||||
|
||||
getType() {
|
||||
return this.type
|
||||
}
|
||||
|
||||
clearInflightAndPendingOps() {
|
||||
this._clearFatalTimeoutTimer()
|
||||
this._doc.inflightOp = null
|
||||
this._doc.inflightCallbacks = []
|
||||
this._doc.pendingOp = null
|
||||
return (this._doc.pendingCallbacks = [])
|
||||
}
|
||||
|
||||
flushPendingOps() {
|
||||
// This will flush any ops that are pending.
|
||||
// If there is an inflight op it will do nothing.
|
||||
return this._doc.flush()
|
||||
}
|
||||
|
||||
updateConnectionState(state) {
|
||||
debugConsole.log(`[updateConnectionState] Setting state to ${state}`)
|
||||
this.connection.state = state
|
||||
this.connection.id = this.socket.publicId
|
||||
this._doc.autoOpen = false
|
||||
this._doc._connectionStateChanged(state)
|
||||
return (this.lastAcked = null) // reset the last ack time when connection changes
|
||||
}
|
||||
|
||||
hasBufferedOps() {
|
||||
return this._doc.inflightOp != null || this._doc.pendingOp != null
|
||||
}
|
||||
|
||||
getInflightOp() {
|
||||
return this._doc.inflightOp
|
||||
}
|
||||
|
||||
getPendingOp() {
|
||||
return this._doc.pendingOp
|
||||
}
|
||||
|
||||
getRecentAck() {
|
||||
// check if we have received an ack recently (within a factor of two of the single user flush delay)
|
||||
return (
|
||||
this.lastAcked != null &&
|
||||
new Date() - this.lastAcked < 2 * SINGLE_USER_FLUSH_DELAY
|
||||
)
|
||||
}
|
||||
|
||||
getOpSize(op) {
|
||||
// compute size of an op from its components
|
||||
// (total number of characters inserted and deleted)
|
||||
let size = 0
|
||||
for (const component of Array.from(op || [])) {
|
||||
if ((component != null ? component.i : undefined) != null) {
|
||||
size += component.i.length
|
||||
}
|
||||
if ((component != null ? component.d : undefined) != null) {
|
||||
size += component.d.length
|
||||
}
|
||||
}
|
||||
return size
|
||||
}
|
||||
|
||||
_attachEditorWatchdogManager(editorName, editor) {
|
||||
// end-to-end check for edits -> acks, for this very ShareJsdoc
|
||||
// This will catch a broken connection and missing UX-blocker for the
|
||||
// user, allowing them to keep editing.
|
||||
this._detachEditorWatchdogManager =
|
||||
this.EditorWatchdogManager.attachToEditor(editorName, editor)
|
||||
}
|
||||
|
||||
_attachToEditor(editorName, editor, attachToShareJs) {
|
||||
this._attachEditorWatchdogManager(editorName, editor)
|
||||
|
||||
attachToShareJs()
|
||||
}
|
||||
|
||||
_maybeDetachEditorWatchdogManager() {
|
||||
// a failed attach attempt may lead to a missing cleanup handler
|
||||
if (this._detachEditorWatchdogManager) {
|
||||
this._detachEditorWatchdogManager()
|
||||
delete this._detachEditorWatchdogManager
|
||||
}
|
||||
}
|
||||
|
||||
attachToCM6(cm6) {
|
||||
this._attachToEditor('CM6', cm6, () => {
|
||||
cm6.attachShareJs(this._doc, window.maxDocLength)
|
||||
})
|
||||
}
|
||||
|
||||
detachFromCM6() {
|
||||
this._maybeDetachEditorWatchdogManager()
|
||||
if (this._doc.detach_cm6) {
|
||||
this._doc.detach_cm6()
|
||||
}
|
||||
}
|
||||
|
||||
_startInflightOpTimeout(update) {
|
||||
this._startFatalTimeoutTimer(update)
|
||||
const retryOp = () => {
|
||||
// Only send the update again if inflightOp is still populated
|
||||
// This can be cleared when hard reloading the document in which
|
||||
// case we don't want to keep trying to send it.
|
||||
debugConsole.log('[inflightOpTimeout] Trying op again')
|
||||
if (this._doc.inflightOp != null) {
|
||||
// When there is a socket.io disconnect, @_doc.inflightSubmittedIds
|
||||
// is updated with the socket.io client id of the current op in flight
|
||||
// (meta.source of the op).
|
||||
// @connection.id is the client id of the current socket.io session.
|
||||
// So we need both depending on whether the op was submitted before
|
||||
// one or more disconnects, or if it was submitted during the current session.
|
||||
update.dupIfSource = [
|
||||
this.connection.id,
|
||||
...Array.from(this._doc.inflightSubmittedIds),
|
||||
]
|
||||
|
||||
// We must be joined to a project for applyOtUpdate to work on the real-time
|
||||
// service, so don't send an op if we're not. Connection state is set to 'ok'
|
||||
// when we've joined the project
|
||||
if (this.connection.state !== 'ok') {
|
||||
let timer
|
||||
debugConsole.log(
|
||||
'[inflightOpTimeout] Not connected, retrying in 0.5s'
|
||||
)
|
||||
return (timer = setTimeout(
|
||||
retryOp,
|
||||
this.WAIT_FOR_CONNECTION_TIMEOUT
|
||||
))
|
||||
} else {
|
||||
debugConsole.log('[inflightOpTimeout] Sending')
|
||||
return this.connection.send(update)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const timer = setTimeout(retryOp, this.INFLIGHT_OP_TIMEOUT)
|
||||
return this._doc.inflightCallbacks.push(() => {
|
||||
this._clearFatalTimeoutTimer()
|
||||
return clearTimeout(timer)
|
||||
}) // 30 seconds
|
||||
}
|
||||
|
||||
_startFatalTimeoutTimer(update) {
|
||||
// If an op doesn't get acked within FATAL_OP_TIMEOUT, something has
|
||||
// gone unrecoverably wrong (the op will have been retried multiple times)
|
||||
if (this._timeoutTimer != null) {
|
||||
return
|
||||
}
|
||||
return (this._timeoutTimer = setTimeout(() => {
|
||||
this._clearFatalTimeoutTimer()
|
||||
return this.trigger('op:timeout', update)
|
||||
}, this.FATAL_OP_TIMEOUT))
|
||||
}
|
||||
|
||||
_clearFatalTimeoutTimer() {
|
||||
if (this._timeoutTimer == null) {
|
||||
return
|
||||
}
|
||||
clearTimeout(this._timeoutTimer)
|
||||
return (this._timeoutTimer = null)
|
||||
}
|
||||
|
||||
_handleError(error, meta) {
|
||||
if (meta == null) {
|
||||
meta = {}
|
||||
}
|
||||
return this.trigger('error', error, meta)
|
||||
}
|
||||
|
||||
_bindToDocChanges(doc) {
|
||||
const { submitOp } = doc
|
||||
doc.submitOp = (...args) => {
|
||||
recordDocumentFirstChangeEvent()
|
||||
this.trigger('op:sent', ...Array.from(args))
|
||||
doc.pendingCallbacks.push(() => {
|
||||
return this.trigger('op:acknowledged', ...Array.from(args))
|
||||
})
|
||||
return submitOp.apply(doc, args)
|
||||
}
|
||||
|
||||
const { flush } = doc
|
||||
return (doc.flush = (...args) => {
|
||||
this.trigger('flush', doc.inflightOp, doc.pendingOp, doc.version)
|
||||
return flush.apply(doc, args)
|
||||
})
|
||||
}
|
||||
}
|
||||
ShareJsDoc.initClass()
|
||||
return ShareJsDoc
|
||||
})()
|
||||
|
||||
function __guard__(value, transform) {
|
||||
return typeof value !== 'undefined' && value !== null
|
||||
? transform(value)
|
||||
: undefined
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
import App from '../../../base'
|
||||
import { react2angular } from 'react2angular'
|
||||
import { rootContext } from '../../../shared/context/root-context'
|
||||
import DetachCompileButtonWrapper from '../../../features/pdf-preview/components/detach-compile-button-wrapper'
|
||||
|
||||
App.component(
|
||||
'editorCompileButton',
|
||||
react2angular(rootContext.use(DetachCompileButtonWrapper))
|
||||
)
|
|
@ -1,103 +0,0 @@
|
|||
/* eslint-disable
|
||||
camelcase,
|
||||
max-len,
|
||||
no-return-assign,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
import App from '../../../base'
|
||||
import Document from '../Document'
|
||||
|
||||
export default App.controller('SavingNotificationController', [
|
||||
'$scope',
|
||||
'ide',
|
||||
function ($scope, ide) {
|
||||
let warnAboutUnsavedChanges
|
||||
setInterval(() => pollSavedStatus(), 1000)
|
||||
|
||||
$(window).bind('beforeunload', () => {
|
||||
return warnAboutUnsavedChanges()
|
||||
})
|
||||
|
||||
let lockEditorModal = null // modal showing "connection lost"
|
||||
let originalPermissionsLevel
|
||||
const MAX_UNSAVED_SECONDS = 15 // lock the editor after this time if unsaved
|
||||
|
||||
$scope.docSavingStatus = {}
|
||||
function pollSavedStatus() {
|
||||
let t
|
||||
const oldStatus = $scope.docSavingStatus
|
||||
const oldUnsavedCount = $scope.docSavingStatusCount
|
||||
const newStatus = {}
|
||||
let newUnsavedCount = 0
|
||||
let maxUnsavedSeconds = 0
|
||||
|
||||
for (const doc_id in Document.openDocs) {
|
||||
const doc = Document.openDocs[doc_id]
|
||||
const saving = doc.pollSavedStatus()
|
||||
if (!saving) {
|
||||
newUnsavedCount++
|
||||
if (oldStatus[doc_id] != null) {
|
||||
newStatus[doc_id] = oldStatus[doc_id]
|
||||
t = newStatus[doc_id].unsavedSeconds += 1
|
||||
if (t > maxUnsavedSeconds) {
|
||||
maxUnsavedSeconds = t
|
||||
}
|
||||
} else {
|
||||
newStatus[doc_id] = {
|
||||
unsavedSeconds: 0,
|
||||
doc: ide.fileTreeManager.findEntityById(doc_id),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (newUnsavedCount > 0 && t > MAX_UNSAVED_SECONDS && !lockEditorModal) {
|
||||
lockEditorModal = ide.showLockEditorMessageModal(
|
||||
'Connection lost',
|
||||
'Sorry, the connection to the server is down.'
|
||||
)
|
||||
|
||||
// put editor in readOnly mode
|
||||
originalPermissionsLevel = ide.$scope.permissionsLevel
|
||||
ide.$scope.permissionsLevel = 'readOnly'
|
||||
|
||||
lockEditorModal.result.finally(() => {
|
||||
lockEditorModal = null // unset the modal if connection comes back
|
||||
// restore original permissions
|
||||
ide.$scope.permissionsLevel = originalPermissionsLevel
|
||||
})
|
||||
}
|
||||
|
||||
if (lockEditorModal && newUnsavedCount === 0) {
|
||||
lockEditorModal.dismiss('connection back up')
|
||||
// restore original permissions if they were changed
|
||||
if (originalPermissionsLevel) {
|
||||
ide.$scope.permissionsLevel = originalPermissionsLevel
|
||||
}
|
||||
}
|
||||
|
||||
// for performance, only update the display if the old or new
|
||||
// counts of unsaved files are nonzeror. If both old and new
|
||||
// unsaved counts are zero then we know we are in a good state
|
||||
// and don't need to do anything to the UI.
|
||||
if (newUnsavedCount || oldUnsavedCount) {
|
||||
$scope.docSavingStatus = newStatus
|
||||
$scope.docSavingStatusCount = newUnsavedCount
|
||||
return $scope.$apply()
|
||||
}
|
||||
}
|
||||
|
||||
return (warnAboutUnsavedChanges = function () {
|
||||
if (Document.hasUnsavedChanges()) {
|
||||
return 'You have unsaved changes. If you leave now they will not be saved.'
|
||||
}
|
||||
})
|
||||
},
|
||||
])
|
|
@ -1,9 +0,0 @@
|
|||
import App from '../../../base'
|
||||
import { react2angular } from 'react2angular'
|
||||
import { rootContext } from '../../../shared/context/root-context'
|
||||
import SwitchToPDFButton from '../../../features/source-editor/components/switch-to-pdf-button'
|
||||
|
||||
App.component(
|
||||
'switchToPdfButton',
|
||||
react2angular(rootContext.use(SwitchToPDFButton))
|
||||
)
|
|
@ -1,19 +0,0 @@
|
|||
import App from '../../../base'
|
||||
|
||||
export default App.directive('formattingButtons', function () {
|
||||
return {
|
||||
scope: {
|
||||
buttons: '=',
|
||||
opening: '=',
|
||||
isFullscreenEditor: '=',
|
||||
},
|
||||
|
||||
link(scope, element, attrs) {
|
||||
scope.showMore = false
|
||||
scope.shownButtons = scope.buttons
|
||||
scope.overflowedButtons = []
|
||||
},
|
||||
|
||||
templateUrl: 'formattingButtonsTpl',
|
||||
}
|
||||
})
|
|
@ -1,38 +0,0 @@
|
|||
import App from '../../../base'
|
||||
|
||||
export default App.directive('toggleSwitch', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
description: '@',
|
||||
labelFalse: '@',
|
||||
labelTrue: '@',
|
||||
ngModel: '=',
|
||||
},
|
||||
template: `\
|
||||
<fieldset class="toggle-switch">
|
||||
<legend class="sr-only">{{description}}</legend>
|
||||
|
||||
<input
|
||||
type="radio"
|
||||
name="toggle-switch-{{$id}}"
|
||||
class="toggle-switch-input"
|
||||
id="toggle-switch-false-{{$id}}"
|
||||
ng-value="false"
|
||||
ng-model="ngModel"
|
||||
>
|
||||
<label for="toggle-switch-false-{{$id}}" class="toggle-switch-label"><span>{{labelFalse}}</span></label>
|
||||
|
||||
<input
|
||||
type="radio"
|
||||
class="toggle-switch-input"
|
||||
name="toggle-switch-{{$id}}"
|
||||
id="toggle-switch-true-{{$id}}"
|
||||
ng-value="true"
|
||||
ng-model="ngModel"
|
||||
>
|
||||
<label for="toggle-switch-true-{{$id}}" class="toggle-switch-label"><span>{{labelTrue}}</span></label>
|
||||
</fieldset>\
|
||||
`,
|
||||
}
|
||||
})
|
|
@ -1,740 +0,0 @@
|
|||
/* eslint-disable
|
||||
camelcase,
|
||||
n/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 './controllers/FileTreeController'
|
||||
import '../../features/file-tree/controllers/file-tree-controller'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
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.editor.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()
|
||||
setTimeout(() => this.$scope.$digest(), 0)
|
||||
})
|
||||
}
|
||||
|
||||
_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)
|
||||
}
|
||||
|
||||
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 (const entity of Array.from(entities)) {
|
||||
paths['/' + this.getEntityPath(entity)] = entity
|
||||
}
|
||||
const prefixes = {}
|
||||
for (const path in paths) {
|
||||
const 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 (const path in paths) {
|
||||
// If the path is in the prefixes, then it's a parent folder and
|
||||
// should be ignore
|
||||
const 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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
getPreviewByPath(path) {
|
||||
for (const suffix of [
|
||||
'',
|
||||
'.png',
|
||||
'.jpg',
|
||||
'.jpeg',
|
||||
'.pdf',
|
||||
'.PNG',
|
||||
'.JPG',
|
||||
'.JPEG',
|
||||
'.PDF',
|
||||
]) {
|
||||
const entity = this.findEntityByPath(path + suffix)
|
||||
|
||||
if (entity) {
|
||||
return {
|
||||
url: `/project/${this.$scope.project._id}/file/${entity.id}`,
|
||||
extension: entity.name.split('.').pop(),
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
_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 () {}
|
||||
}
|
||||
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 || [])) {
|
||||
let 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) {
|
||||
debugConsole.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 () {}
|
||||
}
|
||||
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 () {}
|
||||
}
|
||||
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
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
import App from '../../../base'
|
||||
App.controller('FileTreeController', [
|
||||
'$scope',
|
||||
function ($scope) {
|
||||
$scope.openNewDocModal = () => {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('file-tree.start-creating', { detail: { mode: 'doc' } })
|
||||
)
|
||||
}
|
||||
|
||||
$scope.orderByFoldersFirst = function (entity) {
|
||||
if ((entity != null ? entity.type : undefined) === 'folder') {
|
||||
return '0'
|
||||
}
|
||||
return '1'
|
||||
}
|
||||
},
|
||||
])
|
|
@ -1 +0,0 @@
|
|||
import '../../features/file-view/controllers/file-view-controller'
|
|
@ -1,28 +0,0 @@
|
|||
/* eslint-disable
|
||||
max-len,
|
||||
no-unused-vars,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
let MetadataManager
|
||||
|
||||
export default MetadataManager = class MetadataManager {
|
||||
constructor(ide, $scope, metadata) {
|
||||
this.ide = ide
|
||||
this.$scope = $scope
|
||||
this.metadata = metadata
|
||||
this.ide.socket.on('broadcastDocMeta', data => {
|
||||
return this.metadata.onBroadcastDocMeta(data)
|
||||
})
|
||||
this.$scope.$on('entity:deleted', this.metadata.onEntityDeleted)
|
||||
}
|
||||
|
||||
loadProjectMetaFromServer() {
|
||||
return this.metadata.loadProjectMetaFromServer()
|
||||
}
|
||||
}
|
|
@ -1,129 +0,0 @@
|
|||
import _ from 'lodash'
|
||||
/* eslint-disable
|
||||
no-return-assign,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* 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 App from '../../../base'
|
||||
|
||||
export default App.factory('metadata', [
|
||||
'$http',
|
||||
'ide',
|
||||
function ($http, ide) {
|
||||
const debouncer = {} // DocId => Timeout
|
||||
|
||||
const state = { documents: {} }
|
||||
|
||||
const metadata = { state }
|
||||
|
||||
metadata.onBroadcastDocMeta = function (data) {
|
||||
if (data.docId != null && data.meta != null) {
|
||||
state.documents[data.docId] = data.meta
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('project:metadata', { detail: state.documents })
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
metadata.onEntityDeleted = function (e, entity) {
|
||||
if (entity.type === 'doc') {
|
||||
return delete state.documents[entity.id]
|
||||
}
|
||||
}
|
||||
|
||||
metadata.getAllLabels = () =>
|
||||
_.flattenDeep(
|
||||
(() => {
|
||||
const result = []
|
||||
for (const docId in state.documents) {
|
||||
const meta = state.documents[docId]
|
||||
result.push(meta.labels)
|
||||
}
|
||||
return result
|
||||
})()
|
||||
)
|
||||
|
||||
metadata.getAllPackages = function () {
|
||||
const packageCommandMapping = {}
|
||||
for (const _docId in state.documents) {
|
||||
const meta = state.documents[_docId]
|
||||
for (const packageName in meta.packages) {
|
||||
const commandSnippets = meta.packages[packageName]
|
||||
packageCommandMapping[packageName] = commandSnippets
|
||||
}
|
||||
}
|
||||
return packageCommandMapping
|
||||
}
|
||||
|
||||
metadata.loadProjectMetaFromServer = () =>
|
||||
$http
|
||||
.get(`/project/${window.project_id}/metadata`)
|
||||
.then(function (response) {
|
||||
const { data } = response
|
||||
if (data.projectMeta) {
|
||||
return (() => {
|
||||
const result = []
|
||||
for (const docId in data.projectMeta) {
|
||||
const docMeta = data.projectMeta[docId]
|
||||
result.push((state.documents[docId] = docMeta))
|
||||
}
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('project:metadata', { detail: state.documents })
|
||||
)
|
||||
return result
|
||||
})()
|
||||
}
|
||||
})
|
||||
|
||||
metadata.loadDocMetaFromServer = docId =>
|
||||
$http
|
||||
.post(`/project/${window.project_id}/doc/${docId}/metadata`, {
|
||||
// Don't broadcast metadata when there are no other users in the
|
||||
// project.
|
||||
broadcast: ide.$scope.onlineUsersCount > 0,
|
||||
_csrf: window.csrfToken,
|
||||
})
|
||||
.then(function (response) {
|
||||
const { data } = response
|
||||
// handle the POST response like a broadcast event when there are no
|
||||
// other users in the project.
|
||||
metadata.onBroadcastDocMeta(data)
|
||||
})
|
||||
|
||||
metadata.scheduleLoadDocMetaFromServer = function (docId) {
|
||||
if (ide.$scope.permissionsLevel === 'readOnly') {
|
||||
// The POST request is blocked for users without write permission.
|
||||
// The user will not be able to consume the meta data for edits anyways.
|
||||
return
|
||||
}
|
||||
// De-bounce loading labels with a timeout
|
||||
const existingTimeout = debouncer[docId]
|
||||
|
||||
if (existingTimeout != null) {
|
||||
clearTimeout(existingTimeout)
|
||||
delete debouncer[docId]
|
||||
}
|
||||
|
||||
return (debouncer[docId] = setTimeout(() => {
|
||||
// TODO: wait for the document to be saved?
|
||||
metadata.loadDocMetaFromServer(docId)
|
||||
return delete debouncer[docId]
|
||||
}, 2000))
|
||||
}
|
||||
|
||||
window.addEventListener('editor:metadata-outdated', () => {
|
||||
metadata.scheduleLoadDocMetaFromServer(
|
||||
ide.$scope.editor.sharejs_doc.doc_id
|
||||
)
|
||||
})
|
||||
|
||||
return metadata
|
||||
},
|
||||
])
|
|
@ -1,176 +0,0 @@
|
|||
/* eslint-disable
|
||||
camelcase,
|
||||
n/handle-callback-err,
|
||||
max-len,
|
||||
no-return-assign,
|
||||
*/
|
||||
// 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
|
||||
* DS206: Consider reworking classes to avoid initClass
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
import ColorManager from '../colors/ColorManager'
|
||||
|
||||
let OnlineUsersManager
|
||||
|
||||
export default OnlineUsersManager = (function () {
|
||||
OnlineUsersManager = class OnlineUsersManager {
|
||||
static initClass() {
|
||||
this.prototype.cursorUpdateInterval = 500
|
||||
}
|
||||
|
||||
constructor(ide, $scope) {
|
||||
this.ide = ide
|
||||
this.$scope = $scope
|
||||
this.$scope.onlineUsers = {}
|
||||
this.$scope.onlineUserCursorHighlights = {}
|
||||
this.$scope.onlineUsersArray = []
|
||||
this.$scope.onlineUsersCount = 0
|
||||
|
||||
this.$scope.$on('cursor:editor:update', (event, position) => {
|
||||
return this.sendCursorPositionUpdate(position)
|
||||
})
|
||||
|
||||
this.$scope.$on('project:joined', () => {
|
||||
return this.ide.socket.emit(
|
||||
'clientTracking.getConnectedUsers',
|
||||
(error, connectedUsers) => {
|
||||
this.$scope.onlineUsers = {}
|
||||
for (const user of Array.from(connectedUsers || [])) {
|
||||
if (user.client_id === this.ide.socket.publicId) {
|
||||
// Don't store myself
|
||||
continue
|
||||
}
|
||||
// Store data in the same format returned by clientTracking.clientUpdated
|
||||
|
||||
this.$scope.onlineUsers[user.client_id] = {
|
||||
id: user.client_id,
|
||||
user_id: user.user_id,
|
||||
email: user.email,
|
||||
name: `${user.first_name} ${user.last_name}`,
|
||||
doc_id:
|
||||
user.cursorData != null ? user.cursorData.doc_id : undefined,
|
||||
row: user.cursorData != null ? user.cursorData.row : undefined,
|
||||
column:
|
||||
user.cursorData != null ? user.cursorData.column : undefined,
|
||||
}
|
||||
}
|
||||
return this.refreshOnlineUsers()
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
this.ide.socket.on('clientTracking.clientUpdated', client => {
|
||||
if (client.id !== this.ide.socket.publicId) {
|
||||
// Check it's not me!
|
||||
return this.$scope.$apply(() => {
|
||||
this.$scope.onlineUsers[client.id] = client
|
||||
return this.refreshOnlineUsers()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
this.ide.socket.on('clientTracking.clientDisconnected', client_id => {
|
||||
return this.$scope.$apply(() => {
|
||||
delete this.$scope.onlineUsers[client_id]
|
||||
return this.refreshOnlineUsers()
|
||||
})
|
||||
})
|
||||
|
||||
this.$scope.getHueForUserId = user_id => {
|
||||
return ColorManager.getHueForUserId(user_id)
|
||||
}
|
||||
}
|
||||
|
||||
refreshOnlineUsers() {
|
||||
this.$scope.onlineUsersArray = []
|
||||
|
||||
for (const client_id in this.$scope.onlineUsers) {
|
||||
const user = this.$scope.onlineUsers[client_id]
|
||||
if (user.doc_id != null) {
|
||||
user.doc = this.ide.fileTreeManager.findEntityById(user.doc_id)
|
||||
}
|
||||
|
||||
// If the user's name is empty use their email as display name
|
||||
// Otherwise they're probably an anonymous user
|
||||
if (user.name === null || user.name.trim().length === 0) {
|
||||
if (user.email) {
|
||||
user.name = user.email.trim()
|
||||
} else if (user.user_id === 'anonymous-user') {
|
||||
user.name = 'Anonymous'
|
||||
}
|
||||
}
|
||||
|
||||
user.initial = user.name != null ? user.name[0] : undefined
|
||||
if (!user.initial || user.initial === ' ') {
|
||||
user.initial = '?'
|
||||
}
|
||||
|
||||
this.$scope.onlineUsersArray.push(user)
|
||||
}
|
||||
|
||||
// keep a count of the other online users
|
||||
this.$scope.onlineUsersCount = this.$scope.onlineUsersArray.length
|
||||
|
||||
this.$scope.onlineUserCursorHighlights = {}
|
||||
for (const client_id in this.$scope.onlineUsers) {
|
||||
const client = this.$scope.onlineUsers[client_id]
|
||||
const { doc_id } = client
|
||||
if (doc_id == null || client.row == null || client.column == null) {
|
||||
continue
|
||||
}
|
||||
if (!this.$scope.onlineUserCursorHighlights[doc_id]) {
|
||||
this.$scope.onlineUserCursorHighlights[doc_id] = []
|
||||
}
|
||||
this.$scope.onlineUserCursorHighlights[doc_id].push({
|
||||
label: client.name,
|
||||
cursor: {
|
||||
row: client.row,
|
||||
column: client.column,
|
||||
},
|
||||
hue: ColorManager.getHueForUserId(client.user_id),
|
||||
})
|
||||
}
|
||||
|
||||
if (this.$scope.onlineUsersArray.length > 0) {
|
||||
delete this.cursorUpdateTimeout
|
||||
return (this.cursorUpdateInterval = 500)
|
||||
} else {
|
||||
delete this.cursorUpdateTimeout
|
||||
return (this.cursorUpdateInterval = 60 * 1000 * 5)
|
||||
}
|
||||
}
|
||||
|
||||
sendCursorPositionUpdate(position) {
|
||||
if (position != null) {
|
||||
this.$scope.currentPosition = position // keep track of the latest position
|
||||
}
|
||||
if (this.cursorUpdateTimeout == null) {
|
||||
return (this.cursorUpdateTimeout = setTimeout(() => {
|
||||
const doc_id = this.$scope.editor.open_doc_id
|
||||
// always send the latest position to other clients
|
||||
this.ide.socket.emit('clientTracking.updatePosition', {
|
||||
row:
|
||||
this.$scope.currentPosition != null
|
||||
? this.$scope.currentPosition.row
|
||||
: undefined,
|
||||
column:
|
||||
this.$scope.currentPosition != null
|
||||
? this.$scope.currentPosition.column
|
||||
: undefined,
|
||||
doc_id,
|
||||
})
|
||||
|
||||
return delete this.cursorUpdateTimeout
|
||||
}, this.cursorUpdateInterval))
|
||||
}
|
||||
}
|
||||
}
|
||||
OnlineUsersManager.initClass()
|
||||
return OnlineUsersManager
|
||||
})()
|
|
@ -1,49 +0,0 @@
|
|||
/* eslint-disable
|
||||
no-return-assign,
|
||||
no-unused-vars,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
let PermissionsManager
|
||||
|
||||
export default PermissionsManager = class PermissionsManager {
|
||||
constructor(ide, $scope) {
|
||||
this.ide = ide
|
||||
this.$scope = $scope
|
||||
this.$scope.permissions = {
|
||||
read: false,
|
||||
write: false,
|
||||
admin: false,
|
||||
comment: false,
|
||||
}
|
||||
this.$scope.$watch('permissionsLevel', permissionsLevel => {
|
||||
if (permissionsLevel != null) {
|
||||
if (permissionsLevel === 'readOnly') {
|
||||
this.$scope.permissions.read = true
|
||||
this.$scope.permissions.write = false
|
||||
this.$scope.permissions.admin = false
|
||||
this.$scope.permissions.comment = true
|
||||
} else if (permissionsLevel === 'readAndWrite') {
|
||||
this.$scope.permissions.read = true
|
||||
this.$scope.permissions.write = true
|
||||
this.$scope.permissions.comment = true
|
||||
} else if (permissionsLevel === 'owner') {
|
||||
this.$scope.permissions.read = true
|
||||
this.$scope.permissions.write = true
|
||||
this.$scope.permissions.admin = true
|
||||
this.$scope.permissions.comment = true
|
||||
}
|
||||
}
|
||||
|
||||
if (this.$scope.anonymous) {
|
||||
return (this.$scope.permissions.comment = false)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,110 +0,0 @@
|
|||
import _ from 'lodash'
|
||||
/* eslint-disable
|
||||
max-len,
|
||||
no-return-assign,
|
||||
no-unused-vars,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* DS103: Rewrite code to no longer use __guard__
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
import CryptoJSSHA1 from 'crypto-js/sha1'
|
||||
let ReferencesManager
|
||||
|
||||
export default ReferencesManager = class ReferencesManager {
|
||||
constructor(ide, $scope) {
|
||||
this.ide = ide
|
||||
this.$scope = $scope
|
||||
this.$scope.$root._references = this.state = { keys: [] }
|
||||
this.existingIndexHash = {}
|
||||
|
||||
this.$scope.$on('document:closed', (e, doc) => {
|
||||
let entity
|
||||
if (doc.doc_id) {
|
||||
entity = this.ide.fileTreeManager.findEntityById(doc.doc_id)
|
||||
}
|
||||
if (
|
||||
__guard__(entity != null ? entity.name : undefined, x =>
|
||||
x.match(/.*\.bib$/)
|
||||
)
|
||||
) {
|
||||
return this.indexReferencesIfDocModified(doc, true)
|
||||
}
|
||||
})
|
||||
|
||||
this.$scope.$on('references:should-reindex', (e, data) => {
|
||||
return this.indexAllReferences(true)
|
||||
})
|
||||
|
||||
// When we join the project:
|
||||
// index all references files
|
||||
// and don't broadcast to all clients
|
||||
this.inited = false
|
||||
this.$scope.$on('project:joined', e => {
|
||||
// We only need to grab the references when the editor first loads,
|
||||
// not on every reconnect
|
||||
if (!this.inited) {
|
||||
this.inited = true
|
||||
this.ide.socket.on('references:keys:updated', (keys, allDocs) =>
|
||||
this._storeReferencesKeys(keys, allDocs)
|
||||
)
|
||||
this.indexAllReferences(false)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
_storeReferencesKeys(newKeys, replaceExistingKeys) {
|
||||
const oldKeys = this.$scope.$root._references.keys
|
||||
const keys = replaceExistingKeys ? newKeys : _.union(oldKeys, newKeys)
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('project:references', {
|
||||
detail: keys,
|
||||
})
|
||||
)
|
||||
return (this.$scope.$root._references.keys = keys)
|
||||
}
|
||||
|
||||
indexReferencesIfDocModified(doc, shouldBroadcast) {
|
||||
// avoid reindexing references if the bib file has not changed since the
|
||||
// last time they were indexed
|
||||
const docId = doc.doc_id
|
||||
const snapshot = doc._doc.snapshot
|
||||
const now = Date.now()
|
||||
const sha1 = CryptoJSSHA1(
|
||||
'blob ' + snapshot.length + '\x00' + snapshot
|
||||
).toString()
|
||||
const CACHE_LIFETIME = 6 * 3600 * 1000 // allow reindexing every 6 hours
|
||||
const cacheEntry = this.existingIndexHash[docId]
|
||||
const isCached =
|
||||
cacheEntry &&
|
||||
cacheEntry.timestamp > now - CACHE_LIFETIME &&
|
||||
cacheEntry.hash === sha1
|
||||
if (!isCached) {
|
||||
this.indexAllReferences(shouldBroadcast)
|
||||
this.existingIndexHash[docId] = { hash: sha1, timestamp: now }
|
||||
}
|
||||
}
|
||||
|
||||
indexAllReferences(shouldBroadcast) {
|
||||
const opts = {
|
||||
shouldBroadcast,
|
||||
_csrf: window.csrfToken,
|
||||
}
|
||||
return this.ide.$http
|
||||
.post(`/project/${this.$scope.project_id}/references/indexAll`, opts)
|
||||
.then(response => {
|
||||
return this._storeReferencesKeys(response.data.keys, true)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function __guard__(value, transform) {
|
||||
return typeof value !== 'undefined' && value !== null
|
||||
? transform(value)
|
||||
: undefined
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
import './controllers/ReviewPanelController'
|
File diff suppressed because it is too large
Load diff
|
@ -1,181 +0,0 @@
|
|||
/* eslint-disable
|
||||
max-len,
|
||||
no-return-assign,
|
||||
*/
|
||||
// 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__
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
import App from '../../base'
|
||||
import EditorWatchdogManager from '../connection/EditorWatchdogManager'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
// We create and provide this as service so that we can access the global ide
|
||||
// from within other parts of the angular app.
|
||||
App.factory('ide', [
|
||||
'$http',
|
||||
'queuedHttp',
|
||||
'$modal',
|
||||
'$q',
|
||||
'$filter',
|
||||
'$timeout',
|
||||
'eventTracking',
|
||||
function ($http, queuedHttp, $modal, $q, $filter, $timeout, eventTracking) {
|
||||
const ide = {}
|
||||
ide.$http = $http
|
||||
ide.queuedHttp = queuedHttp
|
||||
ide.$q = $q
|
||||
ide.$filter = $filter
|
||||
ide.$timeout = $timeout
|
||||
ide.globalEditorWatchdogManager = new EditorWatchdogManager({
|
||||
onTimeoutHandler: meta => {
|
||||
eventTracking.sendMB('losing-edits', meta)
|
||||
// clone the meta object, reportError adds additional fields into it
|
||||
ide.reportError('losing-edits', Object.assign({}, meta))
|
||||
},
|
||||
})
|
||||
|
||||
this.recentEvents = []
|
||||
ide.pushEvent = (type, meta) => {
|
||||
if (meta == null) {
|
||||
meta = {}
|
||||
}
|
||||
debugConsole.log('event', type, meta)
|
||||
this.recentEvents.push({ type, meta, date: new Date() })
|
||||
if (this.recentEvents.length > 100) {
|
||||
return this.recentEvents.shift()
|
||||
}
|
||||
}
|
||||
|
||||
ide.reportError = (error, meta) => {
|
||||
if (meta == null) {
|
||||
meta = {}
|
||||
}
|
||||
meta.user_id = window.user_id
|
||||
meta.project_id = window.project_id
|
||||
meta.client_id = __guard__(
|
||||
ide.socket != null ? ide.socket.socket : undefined,
|
||||
x => x.sessionid
|
||||
)
|
||||
meta.transport = __guard__(
|
||||
__guard__(
|
||||
ide.socket != null ? ide.socket.socket : undefined,
|
||||
x2 => x2.transport
|
||||
),
|
||||
x1 => x1.name
|
||||
)
|
||||
meta.client_now = new Date()
|
||||
const errorObj = {}
|
||||
if (typeof error === 'object') {
|
||||
for (const key of Array.from(Object.getOwnPropertyNames(error))) {
|
||||
errorObj[key] = error[key]
|
||||
}
|
||||
} else if (typeof error === 'string') {
|
||||
errorObj.message = error
|
||||
}
|
||||
return $http.post('/error/client', {
|
||||
error: errorObj,
|
||||
meta,
|
||||
_csrf: window.csrfToken,
|
||||
})
|
||||
}
|
||||
|
||||
ide.showGenericMessageModal = (title, message) =>
|
||||
$modal.open({
|
||||
templateUrl: 'genericMessageModalTemplate',
|
||||
controller: 'GenericMessageModalController',
|
||||
resolve: {
|
||||
title() {
|
||||
return title
|
||||
},
|
||||
message() {
|
||||
return message
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
ide.showOutOfSyncModal = (title, message, editorContent) =>
|
||||
$modal.open({
|
||||
templateUrl: 'outOfSyncModalTemplate',
|
||||
controller: 'OutOfSyncModalController',
|
||||
backdrop: false, // not dismissable by clicking background
|
||||
keyboard: false, // prevent dismiss via keyboard
|
||||
resolve: {
|
||||
title() {
|
||||
return title
|
||||
},
|
||||
message() {
|
||||
return message
|
||||
},
|
||||
editorContent() {
|
||||
return editorContent
|
||||
},
|
||||
},
|
||||
windowClass: 'out-of-sync-modal',
|
||||
})
|
||||
|
||||
ide.showLockEditorMessageModal = (title, message) =>
|
||||
// modal to block the editor when connection is down
|
||||
$modal.open({
|
||||
templateUrl: 'lockEditorModalTemplate',
|
||||
controller: 'GenericMessageModalController',
|
||||
backdrop: false, // not dismissable by clicking background
|
||||
keyboard: false, // prevent dismiss via keyboard
|
||||
resolve: {
|
||||
title() {
|
||||
return title
|
||||
},
|
||||
message() {
|
||||
return message
|
||||
},
|
||||
},
|
||||
windowClass: 'lock-editor-modal',
|
||||
})
|
||||
|
||||
return ide
|
||||
},
|
||||
])
|
||||
|
||||
App.controller('GenericMessageModalController', [
|
||||
'$scope',
|
||||
'$modalInstance',
|
||||
'title',
|
||||
'message',
|
||||
function ($scope, $modalInstance, title, message) {
|
||||
$scope.title = title
|
||||
$scope.message = message
|
||||
|
||||
return ($scope.done = () => $modalInstance.close())
|
||||
},
|
||||
])
|
||||
|
||||
App.controller('OutOfSyncModalController', [
|
||||
'$scope',
|
||||
'$window',
|
||||
'title',
|
||||
'message',
|
||||
'editorContent',
|
||||
function ($scope, $window, title, message, editorContent) {
|
||||
$scope.title = title
|
||||
$scope.message = message
|
||||
$scope.editorContent = editorContent
|
||||
$scope.editorContentRows = editorContent.split('\n').length
|
||||
|
||||
$scope.done = () => {
|
||||
// Reload the page to avoid staying in an inconsistent state.
|
||||
// https://github.com/overleaf/issues/issues/3694
|
||||
$window.location.reload()
|
||||
}
|
||||
},
|
||||
])
|
||||
|
||||
function __guard__(value, transform) {
|
||||
return typeof value !== 'undefined' && value !== null
|
||||
? transform(value)
|
||||
: undefined
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
import App from '../../base'
|
||||
|
||||
App.controller('EditorLoaderController', [
|
||||
'$scope',
|
||||
'localStorage',
|
||||
function ($scope, localStorage) {
|
||||
$scope.$watch('editor.showVisual', function (val) {
|
||||
localStorage(`editor.lastUsedMode`, val === true ? 'visual' : 'code')
|
||||
})
|
||||
},
|
||||
])
|
|
@ -1 +0,0 @@
|
|||
import './EditorLoaderController'
|
|
@ -1,13 +0,0 @@
|
|||
import 'jquery'
|
||||
import 'angular'
|
||||
import 'angular-sanitize'
|
||||
import 'lodash'
|
||||
import './vendor/libs/ui-bootstrap'
|
||||
import './vendor/libs/jquery.storage'
|
||||
import './vendor/libs/select/select'
|
||||
|
||||
// CSS
|
||||
import 'angular/angular-csp.css'
|
||||
|
||||
// Rewrite meta elements
|
||||
import './utils/meta'
|
|
@ -1,32 +0,0 @@
|
|||
/* eslint-disable
|
||||
max-len,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
import './main/token-access' // used in project/token/access
|
||||
import './main/event' // used in various controllers
|
||||
import './main/system-messages' // used in project/editor
|
||||
import './directives/eventTracking' // used in lots of places
|
||||
import './features/cookie-banner'
|
||||
import '../../modules/modules-main'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
angular.module('OverleafApp').config([
|
||||
'$locationProvider',
|
||||
function ($locationProvider) {
|
||||
try {
|
||||
return $locationProvider.html5Mode({
|
||||
enabled: true,
|
||||
requireBase: false,
|
||||
rewriteLinks: false,
|
||||
})
|
||||
} catch (e) {
|
||||
debugConsole.error("Error while trying to fix '#' links: ", e)
|
||||
}
|
||||
},
|
||||
])
|
||||
export default angular.bootstrap(document.body, ['OverleafApp'])
|
|
@ -1,129 +0,0 @@
|
|||
/* eslint-disable
|
||||
camelcase,
|
||||
max-len,
|
||||
no-return-assign,
|
||||
no-unused-vars,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
import moment from 'moment'
|
||||
import App from '../base'
|
||||
import '../modules/localStorage'
|
||||
import { sendMB } from '../infrastructure/event-tracking'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
const CACHE_KEY = 'mbEvents'
|
||||
|
||||
// keep track of how many heartbeats we've sent so we can calculate how
|
||||
// long wait until the next one
|
||||
let heartbeatsSent = 0
|
||||
let nextHeartbeat = new Date()
|
||||
|
||||
App.factory('eventTracking', [
|
||||
'$http',
|
||||
'localStorage',
|
||||
function ($http, localStorage) {
|
||||
const _getEventCache = function () {
|
||||
let eventCache = localStorage(CACHE_KEY)
|
||||
|
||||
// Initialize as an empy object if the event cache is still empty.
|
||||
if (eventCache == null) {
|
||||
eventCache = {}
|
||||
localStorage(CACHE_KEY, eventCache)
|
||||
}
|
||||
|
||||
return eventCache
|
||||
}
|
||||
|
||||
const _eventInCache = function (key) {
|
||||
const curCache = _getEventCache()
|
||||
return curCache[key] || false
|
||||
}
|
||||
|
||||
const _addEventToCache = function (key) {
|
||||
const curCache = _getEventCache()
|
||||
curCache[key] = true
|
||||
|
||||
return localStorage(CACHE_KEY, curCache)
|
||||
}
|
||||
|
||||
const _sendEditingSessionHeartbeat = segmentation =>
|
||||
$http({
|
||||
url: `/editingSession/${window.project_id}`,
|
||||
method: 'PUT',
|
||||
data: { segmentation },
|
||||
headers: {
|
||||
'X-CSRF-Token': window.csrfToken,
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
send(category, action, label, value) {
|
||||
return ga('send', 'event', category, action, label, value)
|
||||
},
|
||||
|
||||
sendGAOnce(category, action, label, value) {
|
||||
if (!_eventInCache(action)) {
|
||||
_addEventToCache(action)
|
||||
return this.send(category, action, label, value)
|
||||
}
|
||||
},
|
||||
|
||||
editingSessionHeartbeat(segmentationCb = () => {}) {
|
||||
debugConsole.log('[Event] heartbeat trigger')
|
||||
|
||||
// If the next heartbeat is in the future, stop
|
||||
if (nextHeartbeat > new Date()) return
|
||||
|
||||
const segmentation = segmentationCb()
|
||||
|
||||
debugConsole.log('[Event] send heartbeat request', segmentation)
|
||||
_sendEditingSessionHeartbeat(segmentation)
|
||||
|
||||
heartbeatsSent++
|
||||
|
||||
// send two first heartbeats at 0 and 30s then increase the backoff time
|
||||
// 1min per call until we reach 5 min
|
||||
const backoffSecs =
|
||||
heartbeatsSent <= 2
|
||||
? 30
|
||||
: heartbeatsSent <= 6
|
||||
? (heartbeatsSent - 2) * 60
|
||||
: 300
|
||||
|
||||
nextHeartbeat = moment().add(backoffSecs, 'seconds').toDate()
|
||||
},
|
||||
|
||||
sendMB,
|
||||
|
||||
sendMBSampled(key, segmentation, rate = 0.01) {
|
||||
if (Math.random() < rate) {
|
||||
this.sendMB(key, segmentation)
|
||||
}
|
||||
},
|
||||
|
||||
sendMBOnce(key, segmentation) {
|
||||
if (!_eventInCache(key)) {
|
||||
_addEventToCache(key)
|
||||
this.sendMB(key, segmentation)
|
||||
}
|
||||
},
|
||||
|
||||
eventInCache(key) {
|
||||
return _eventInCache(key)
|
||||
},
|
||||
}
|
||||
},
|
||||
])
|
||||
|
||||
export default $('.navbar a').on('click', function (e) {
|
||||
const href = $(e.target).attr('href')
|
||||
if (href != null) {
|
||||
return ga('send', 'event', 'navigation', 'top menu bar', href)
|
||||
}
|
||||
})
|
|
@ -1,21 +0,0 @@
|
|||
import App from '../base'
|
||||
App.controller('ImportingController', [
|
||||
'$interval',
|
||||
'$scope',
|
||||
'$timeout',
|
||||
'$window',
|
||||
function ($interval, $scope, $timeout, $window) {
|
||||
$interval(function () {
|
||||
$scope.state.load_progress += 5
|
||||
if ($scope.state.load_progress > 100) {
|
||||
$scope.state.load_progress = 20
|
||||
}
|
||||
}, 500)
|
||||
$timeout(function () {
|
||||
$window.location.reload()
|
||||
}, 5000)
|
||||
$scope.state = {
|
||||
load_progress: 20,
|
||||
}
|
||||
},
|
||||
])
|
|
@ -1,63 +0,0 @@
|
|||
/* eslint-disable
|
||||
max-len,
|
||||
no-return-assign,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
import App from '../base'
|
||||
const MESSAGE_POLL_INTERVAL = 15 * 60 * 1000
|
||||
// Controller for messages (array)
|
||||
App.controller('SystemMessagesController', [
|
||||
'$http',
|
||||
'$scope',
|
||||
function ($http, $scope) {
|
||||
$scope.messages = []
|
||||
function pollSystemMessages() {
|
||||
// Ignore polling if tab is hidden or browser is offline
|
||||
if (document.hidden || !navigator.onLine) {
|
||||
return
|
||||
}
|
||||
|
||||
$http
|
||||
.get('/system/messages')
|
||||
.then(response => {
|
||||
// Ignore if content-type is anything but JSON, prevents a bug where
|
||||
// the user logs out in another tab, then a 302 redirect was returned,
|
||||
// which is transparently resolved by the browser to the login (HTML)
|
||||
// page.
|
||||
// This then caused an Angular error where it was attempting to loop
|
||||
// through the HTML as a string
|
||||
if (response.headers('content-type').includes('json')) {
|
||||
$scope.messages = response.data
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// ignore errors
|
||||
})
|
||||
}
|
||||
pollSystemMessages()
|
||||
setInterval(pollSystemMessages, MESSAGE_POLL_INTERVAL)
|
||||
},
|
||||
])
|
||||
|
||||
export default App.controller('SystemMessageController', [
|
||||
'$scope',
|
||||
function ($scope) {
|
||||
$scope.hidden = $.localStorage(`systemMessage.hide.${$scope.message._id}`)
|
||||
$scope.protected = $scope.message._id === 'protected'
|
||||
$scope.htmlContent = $scope.message.content
|
||||
|
||||
return ($scope.hide = function () {
|
||||
if (!$scope.protected) {
|
||||
// do not allow protected messages to be hidden
|
||||
$scope.hidden = true
|
||||
return $.localStorage(`systemMessage.hide.${$scope.message._id}`, true)
|
||||
}
|
||||
})
|
||||
},
|
||||
])
|
|
@ -1,92 +0,0 @@
|
|||
import App from '../base'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
App.controller('TokenAccessPageController', [
|
||||
'$scope',
|
||||
'$http',
|
||||
'$location',
|
||||
function ($scope, $http, $location) {
|
||||
window.S = $scope
|
||||
$scope.mode = 'accessAttempt' // 'accessAttempt' | 'v1Import' | 'requireAccept'
|
||||
|
||||
$scope.v1ImportData = null
|
||||
$scope.requireAccept = null
|
||||
|
||||
$scope.accessInFlight = false
|
||||
$scope.accessSuccess = false
|
||||
$scope.accessError = false
|
||||
|
||||
$scope.currentPath = () => {
|
||||
return $location.path()
|
||||
}
|
||||
|
||||
$scope.buildZipDownloadPath = projectId => {
|
||||
return `/overleaf/project/${projectId}/download/zip`
|
||||
}
|
||||
|
||||
$scope.getProjectName = () => {
|
||||
if ($scope.v1ImportData?.name) {
|
||||
return $scope.v1ImportData.name
|
||||
} else if ($scope.requireAccept?.projectName) {
|
||||
return $scope.requireAccept.projectName
|
||||
} else {
|
||||
return 'This project'
|
||||
}
|
||||
}
|
||||
|
||||
$scope.postConfirmedByUser = () => {
|
||||
$scope.post(true)
|
||||
}
|
||||
|
||||
$scope.post = (confirmedByUser = false) => {
|
||||
$scope.mode = 'accessAttempt'
|
||||
const textData = $('#overleaf-token-access-data').text()
|
||||
const parsedData = JSON.parse(textData)
|
||||
const { postUrl, csrfToken } = parsedData
|
||||
$scope.accessInFlight = true
|
||||
$http({
|
||||
method: 'POST',
|
||||
url: postUrl,
|
||||
data: {
|
||||
_csrf: csrfToken,
|
||||
confirmedByUser,
|
||||
tokenHashPrefix: window.location.hash,
|
||||
},
|
||||
}).then(
|
||||
function successCallback(response) {
|
||||
$scope.accessInFlight = false
|
||||
$scope.accessError = false
|
||||
const { data } = response
|
||||
if (data.redirect) {
|
||||
const redirect = response.data.redirect
|
||||
if (!redirect) {
|
||||
debugConsole.warn(
|
||||
'no redirect supplied in success response data',
|
||||
response
|
||||
)
|
||||
$scope.accessError = true
|
||||
return
|
||||
}
|
||||
window.location.replace(redirect)
|
||||
} else if (data.v1Import) {
|
||||
$scope.mode = 'v1Import'
|
||||
$scope.v1ImportData = data.v1Import
|
||||
} else if (data.requireAccept) {
|
||||
$scope.mode = 'requireAccept'
|
||||
$scope.requireAccept = data.requireAccept
|
||||
} else {
|
||||
debugConsole.warn(
|
||||
'invalid data from server in success response',
|
||||
response
|
||||
)
|
||||
$scope.accessError = true
|
||||
}
|
||||
},
|
||||
function errorCallback(response) {
|
||||
debugConsole.warn('error response from server', response)
|
||||
$scope.accessInFlight = false
|
||||
$scope.accessError = response.status === 404 ? 'not_found' : 'error'
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
])
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue