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": "*"
|
"@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": {
|
"node_modules/@types/aria-query": {
|
||||||
"version": "4.2.2",
|
"version": "4.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-4.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-4.2.2.tgz",
|
||||||
|
@ -12691,15 +12685,6 @@
|
||||||
"integrity": "sha512-0d5Wd09ItQWH1qFbEyQ7oTQ3GZrMfth5JkbN3EvTKLXcHLRDSXeLnlvlOn0wvxVIwK5o2M8JzP/OWz7T3NRsbw==",
|
"integrity": "sha512-0d5Wd09ItQWH1qFbEyQ7oTQ3GZrMfth5JkbN3EvTKLXcHLRDSXeLnlvlOn0wvxVIwK5o2M8JzP/OWz7T3NRsbw==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/@types/long": {
|
||||||
"version": "4.0.1",
|
"version": "4.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz",
|
||||||
|
@ -14733,25 +14718,6 @@
|
||||||
"integrity": "sha512-VJK1SRmXBpjwsB4YOHYSturx48rLKMzHgCqDH2ZDa6ZbMS/N5huoNqyQdK5Fj/xayu3fqbXckn5SeCS1EbMDZg==",
|
"integrity": "sha512-VJK1SRmXBpjwsB4YOHYSturx48rLKMzHgCqDH2ZDa6ZbMS/N5huoNqyQdK5Fj/xayu3fqbXckn5SeCS1EbMDZg==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/ansi-color": {
|
||||||
"version": "0.2.1",
|
"version": "0.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-color/-/ansi-color-0.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-color/-/ansi-color-0.2.1.tgz",
|
||||||
|
@ -15617,17 +15583,6 @@
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"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": {
|
"node_modules/babel-plugin-macros": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz",
|
||||||
|
@ -27997,12 +27952,6 @@
|
||||||
"lodash.keys": "~2.4.1"
|
"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": {
|
"node_modules/lodash.get": {
|
||||||
"version": "4.4.2",
|
"version": "4.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz",
|
||||||
"integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ=="
|
"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": {
|
"node_modules/nise": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/nise/-/nise-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/nise/-/nise-4.1.0.tgz",
|
||||||
|
@ -34807,52 +34744,6 @@
|
||||||
"react-dom": ">=15.0.0"
|
"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": {
|
"node_modules/reactcss": {
|
||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz",
|
||||||
|
@ -36673,12 +36564,6 @@
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"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": {
|
"node_modules/simple-oauth2": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/simple-oauth2/-/simple-oauth2-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/simple-oauth2/-/simple-oauth2-5.0.0.tgz",
|
||||||
|
@ -44616,12 +44501,8 @@
|
||||||
"acorn": "^7.1.1",
|
"acorn": "^7.1.1",
|
||||||
"acorn-walk": "^7.1.1",
|
"acorn-walk": "^7.1.1",
|
||||||
"algoliasearch": "^3.35.1",
|
"algoliasearch": "^3.35.1",
|
||||||
"angular": "~1.8.0",
|
|
||||||
"angular-mocks": "~1.8.0",
|
|
||||||
"angular-sanitize": "~1.8.0",
|
|
||||||
"autoprefixer": "^10.4.16",
|
"autoprefixer": "^10.4.16",
|
||||||
"babel-loader": "^9.1.2",
|
"babel-loader": "^9.1.2",
|
||||||
"babel-plugin-angularjs-annotate": "^0.10.0",
|
|
||||||
"babel-plugin-macros": "^3.1.0",
|
"babel-plugin-macros": "^3.1.0",
|
||||||
"babel-plugin-module-resolver": "^5.0.0",
|
"babel-plugin-module-resolver": "^5.0.0",
|
||||||
"backbone": "^1.3.3",
|
"backbone": "^1.3.3",
|
||||||
|
@ -44698,8 +44579,6 @@
|
||||||
"react-linkify": "^1.0.0-alpha",
|
"react-linkify": "^1.0.0-alpha",
|
||||||
"react-refresh": "^0.14.0",
|
"react-refresh": "^0.14.0",
|
||||||
"react-resizable-panels": "^1.0.3",
|
"react-resizable-panels": "^1.0.3",
|
||||||
"react2angular": "^4.0.6",
|
|
||||||
"react2angular-shared-context": "^1.1.0",
|
|
||||||
"requirejs": "^2.3.6",
|
"requirejs": "^2.3.6",
|
||||||
"resolve-url-loader": "^5.0.0",
|
"resolve-url-loader": "^5.0.0",
|
||||||
"samlp": "^7.0.2",
|
"samlp": "^7.0.2",
|
||||||
|
@ -53003,14 +52882,10 @@
|
||||||
"acorn": "^7.1.1",
|
"acorn": "^7.1.1",
|
||||||
"acorn-walk": "^7.1.1",
|
"acorn-walk": "^7.1.1",
|
||||||
"algoliasearch": "^3.35.1",
|
"algoliasearch": "^3.35.1",
|
||||||
"angular": "~1.8.0",
|
|
||||||
"angular-mocks": "~1.8.0",
|
|
||||||
"angular-sanitize": "~1.8.0",
|
|
||||||
"archiver": "^5.3.0",
|
"archiver": "^5.3.0",
|
||||||
"async": "3.2.2",
|
"async": "3.2.2",
|
||||||
"autoprefixer": "^10.4.16",
|
"autoprefixer": "^10.4.16",
|
||||||
"babel-loader": "^9.1.2",
|
"babel-loader": "^9.1.2",
|
||||||
"babel-plugin-angularjs-annotate": "^0.10.0",
|
|
||||||
"babel-plugin-macros": "^3.1.0",
|
"babel-plugin-macros": "^3.1.0",
|
||||||
"babel-plugin-module-resolver": "^5.0.0",
|
"babel-plugin-module-resolver": "^5.0.0",
|
||||||
"backbone": "^1.3.3",
|
"backbone": "^1.3.3",
|
||||||
|
@ -53152,8 +53027,6 @@
|
||||||
"react-linkify": "^1.0.0-alpha",
|
"react-linkify": "^1.0.0-alpha",
|
||||||
"react-refresh": "^0.14.0",
|
"react-refresh": "^0.14.0",
|
||||||
"react-resizable-panels": "^1.0.3",
|
"react-resizable-panels": "^1.0.3",
|
||||||
"react2angular": "^4.0.6",
|
|
||||||
"react2angular-shared-context": "^1.1.0",
|
|
||||||
"recurly": "^4.0.0",
|
"recurly": "^4.0.0",
|
||||||
"referer-parser": "github:overleaf/nodejs-referer-parser#8b8b103762d05b7be4cfa2f810e1d408be67d7bb",
|
"referer-parser": "github:overleaf/nodejs-referer-parser#8b8b103762d05b7be4cfa2f810e1d408be67d7bb",
|
||||||
"request": "^2.88.2",
|
"request": "^2.88.2",
|
||||||
|
@ -57040,12 +56913,6 @@
|
||||||
"@types/node": "*"
|
"@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": {
|
"@types/aria-query": {
|
||||||
"version": "4.2.2",
|
"version": "4.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-4.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-4.2.2.tgz",
|
||||||
|
@ -57487,15 +57354,6 @@
|
||||||
"integrity": "sha512-0d5Wd09ItQWH1qFbEyQ7oTQ3GZrMfth5JkbN3EvTKLXcHLRDSXeLnlvlOn0wvxVIwK5o2M8JzP/OWz7T3NRsbw==",
|
"integrity": "sha512-0d5Wd09ItQWH1qFbEyQ7oTQ3GZrMfth5JkbN3EvTKLXcHLRDSXeLnlvlOn0wvxVIwK5o2M8JzP/OWz7T3NRsbw==",
|
||||||
"dev": true
|
"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": {
|
"@types/long": {
|
||||||
"version": "4.0.1",
|
"version": "4.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz",
|
"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": {
|
"ansi-color": {
|
||||||
"version": "0.2.1",
|
"version": "0.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-color/-/ansi-color-0.2.1.tgz",
|
"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": {
|
"babel-plugin-macros": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz",
|
"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.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": {
|
"lodash.get": {
|
||||||
"version": "4.4.2",
|
"version": "4.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz",
|
||||||
"integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ=="
|
"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": {
|
"nise": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/nise/-/nise-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/nise/-/nise-4.1.0.tgz",
|
||||||
|
@ -75149,35 +74960,6 @@
|
||||||
"react-lifecycles-compat": "^3.0.4"
|
"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": {
|
"reactcss": {
|
||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz",
|
"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": {
|
"simple-oauth2": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/simple-oauth2/-/simple-oauth2-5.0.0.tgz",
|
"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 &&
|
!showPersonalAccessToken &&
|
||||||
splitTestAssignments['personal-access-token'].variant === 'enabled' // `?personal-access-token=enabled`
|
splitTestAssignments['personal-access-token'].variant === 'enabled' // `?personal-access-token=enabled`
|
||||||
|
|
||||||
const idePageReact = splitTestAssignments['ide-page'].variant === 'react'
|
|
||||||
|
|
||||||
const template =
|
const template =
|
||||||
detachRole === 'detached'
|
detachRole === 'detached'
|
||||||
? // TODO: Create React version of detached page
|
? 'project/ide-react-detached'
|
||||||
'project/editor_detached'
|
: 'project/ide-react'
|
||||||
: idePageReact
|
|
||||||
? 'project/ide-react'
|
|
||||||
: 'project/editor'
|
|
||||||
|
|
||||||
res.render(template, {
|
res.render(template, {
|
||||||
title: project.name,
|
title: project.name,
|
||||||
|
@ -681,7 +676,6 @@ const _ProjectController = {
|
||||||
showUpgradePrompt,
|
showUpgradePrompt,
|
||||||
fixedSizeDocument: true,
|
fixedSizeDocument: true,
|
||||||
useOpenTelemetry: Settings.useOpenTelemetryClient,
|
useOpenTelemetry: Settings.useOpenTelemetryClient,
|
||||||
idePageReact,
|
|
||||||
showPersonalAccessToken,
|
showPersonalAccessToken,
|
||||||
optionalPersonalAccessToken,
|
optionalPersonalAccessToken,
|
||||||
hasTrackChangesFeature: Features.hasFeature('track-changes'),
|
hasTrackChangesFeature: Features.hasFeature('track-changes'),
|
||||||
|
|
|
@ -12,7 +12,6 @@ const {
|
||||||
handleAdminDomainRedirect,
|
handleAdminDomainRedirect,
|
||||||
} = require('../Authorization/AuthorizationMiddleware')
|
} = require('../Authorization/AuthorizationMiddleware')
|
||||||
const ProjectAuditLogHandler = require('../Project/ProjectAuditLogHandler')
|
const ProjectAuditLogHandler = require('../Project/ProjectAuditLogHandler')
|
||||||
const SplitTestHandler = require('../SplitTests/SplitTestHandler')
|
|
||||||
|
|
||||||
const orderedPrivilegeLevels = [
|
const orderedPrivilegeLevels = [
|
||||||
PrivilegeLevels.NONE,
|
PrivilegeLevels.NONE,
|
||||||
|
@ -98,18 +97,7 @@ async function tokenAccessPage(req, res, next) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { variant } = await SplitTestHandler.promises.getAssignment(
|
res.render('project/token/access-react', {
|
||||||
req,
|
|
||||||
res,
|
|
||||||
'token-access-page'
|
|
||||||
)
|
|
||||||
|
|
||||||
const view =
|
|
||||||
variant === 'react'
|
|
||||||
? 'project/token/access-react'
|
|
||||||
: 'project/token/access'
|
|
||||||
|
|
||||||
res.render(view, {
|
|
||||||
postUrl: makePostUrl(token),
|
postUrl: makePostUrl(token),
|
||||||
})
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
@ -72,7 +72,7 @@ html(
|
||||||
|
|
||||||
block head-scripts
|
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)
|
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)
|
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-hasTrackChangesFeature", data-type="boolean" content=hasTrackChangesFeature)
|
||||||
meta(name="ol-inactiveTutorials", data-type="json" content=user.inactiveTutorials)
|
meta(name="ol-inactiveTutorials", data-type="json" content=user.inactiveTutorials)
|
||||||
meta(name="ol-projectTags" data-type="json" content=projectTags)
|
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-loadingText", data-type="string" content=translate("loading"))
|
||||||
meta(name="ol-translationLoadErrorMessage", data-type="string" content=translate("could_not_load_translations"))
|
meta(name="ol-translationLoadErrorMessage", data-type="string" content=translate("could_not_load_translations"))
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
extends ../layout
|
extends ../layout-marketing
|
||||||
|
|
||||||
block entrypointVar
|
block entrypointVar
|
||||||
- entrypoint = 'ide-detached'
|
- entrypoint = 'ide-detached'
|
|
@ -1,4 +1,4 @@
|
||||||
extends ../layout
|
extends ../layout-marketing
|
||||||
|
|
||||||
block vars
|
block vars
|
||||||
- var suppressNavbar = true
|
- 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 ./plans/_mixins
|
||||||
include ../_mixins/bootstrap_js
|
|
||||||
|
block entrypointVar
|
||||||
|
- entrypoint = 'pages/user/subscription/plans-v2/plans-v2-main'
|
||||||
|
|
||||||
block vars
|
block vars
|
||||||
- entrypoint = 'pages/user/subscription/plans-v2/plans-v2-main'
|
|
||||||
- var suppressFooter = true
|
- var suppressFooter = true
|
||||||
- var suppressNavbarRight = true
|
- var suppressNavbarRight = true
|
||||||
- var suppressCookieBanner = true
|
- var suppressCookieBanner = true
|
||||||
|
@ -66,6 +67,3 @@ block content
|
||||||
| #{translate("continue_with_free_plan")}
|
| #{translate("continue_with_free_plan")}
|
||||||
|
|
||||||
!= moduleIncludes("contactModalGeneral-marketing", locals)
|
!= moduleIncludes("contactModalGeneral-marketing", locals)
|
||||||
|
|
||||||
block prepend foot-scripts
|
|
||||||
+bootstrap-js(bootstrapVersion)
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
extends ../layout-marketing
|
extends ../layout-marketing
|
||||||
|
|
||||||
block vars
|
block entrypointVar
|
||||||
- entrypoint = 'pages/user/subscription/plans-v2/plans-v2-main'
|
- entrypoint = 'pages/user/subscription/plans-v2/plans-v2-main'
|
||||||
|
|
||||||
block append meta
|
block append meta
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
extends ../../layout
|
extends ../../layout-marketing
|
||||||
|
|
||||||
block content
|
block content
|
||||||
main.content.content-alt.team-invite#main-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/react", { "runtime": "automatic" }],
|
||||||
"@babel/typescript"
|
"@babel/typescript"
|
||||||
],
|
],
|
||||||
"plugins": ["angularjs-annotate", "macros"],
|
"plugins": ["macros"],
|
||||||
"overrides": [
|
"overrides": [
|
||||||
// treat .cjs files (e.g. libraries symlinked into node_modules) as commonjs
|
// 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 Path = require('path')
|
||||||
const { merge } = require('@overleaf/settings/merge')
|
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
|
let defaultFeatures, siteUrl
|
||||||
|
|
||||||
// Make time interval config easier.
|
// Make time interval config easier.
|
||||||
|
@ -918,9 +891,6 @@ module.exports = {
|
||||||
managedGroupSubscriptionEnrollmentNotification: [],
|
managedGroupSubscriptionEnrollmentNotification: [],
|
||||||
managedGroupEnrollmentInvite: [],
|
managedGroupEnrollmentInvite: [],
|
||||||
ssoCertificateInfo: [],
|
ssoCertificateInfo: [],
|
||||||
// See comment at the definition of these variables.
|
|
||||||
entryPointsIde,
|
|
||||||
entryPointsMain,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
moduleImportSequence: [
|
moduleImportSequence: [
|
||||||
|
@ -935,7 +905,7 @@ module.exports = {
|
||||||
reportOnly: process.env.CSP_REPORT_ONLY === 'true',
|
reportOnly: process.env.CSP_REPORT_ONLY === 'true',
|
||||||
reportPercentage: parseFloat(process.env.CSP_REPORT_PERCENTAGE) || 0,
|
reportPercentage: parseFloat(process.env.CSP_REPORT_PERCENTAGE) || 0,
|
||||||
reportUri: process.env.CSP_REPORT_URI,
|
reportUri: process.env.CSP_REPORT_URI,
|
||||||
exclude: ['app/views/project/editor'],
|
exclude: [],
|
||||||
},
|
},
|
||||||
|
|
||||||
unsupportedBrowsers: {
|
unsupportedBrowsers: {
|
||||||
|
|
|
@ -11,6 +11,7 @@ import { interceptFileUpload } from './upload'
|
||||||
import { interceptProjectListing } from './project-list'
|
import { interceptProjectListing } from './project-list'
|
||||||
import { interceptLinkedFile } from './linked-file'
|
import { interceptLinkedFile } from './linked-file'
|
||||||
import { interceptMathJax } from './mathjax'
|
import { interceptMathJax } from './mathjax'
|
||||||
|
import { interceptMetadata } from './metadata'
|
||||||
|
|
||||||
// eslint-disable-next-line no-unused-vars,@typescript-eslint/no-namespace
|
// eslint-disable-next-line no-unused-vars,@typescript-eslint/no-namespace
|
||||||
declare global {
|
declare global {
|
||||||
|
@ -21,6 +22,7 @@ declare global {
|
||||||
interceptAsync: typeof interceptAsync
|
interceptAsync: typeof interceptAsync
|
||||||
interceptCompile: typeof interceptCompile
|
interceptCompile: typeof interceptCompile
|
||||||
interceptEvents: typeof interceptEvents
|
interceptEvents: typeof interceptEvents
|
||||||
|
interceptMetadata: typeof interceptMetadata
|
||||||
interceptSpelling: typeof interceptSpelling
|
interceptSpelling: typeof interceptSpelling
|
||||||
waitForCompile: typeof waitForCompile
|
waitForCompile: typeof waitForCompile
|
||||||
interceptDeferredCompile: typeof interceptDeferredCompile
|
interceptDeferredCompile: typeof interceptDeferredCompile
|
||||||
|
@ -35,6 +37,7 @@ declare global {
|
||||||
Cypress.Commands.add('interceptAsync', interceptAsync)
|
Cypress.Commands.add('interceptAsync', interceptAsync)
|
||||||
Cypress.Commands.add('interceptCompile', interceptCompile)
|
Cypress.Commands.add('interceptCompile', interceptCompile)
|
||||||
Cypress.Commands.add('interceptEvents', interceptEvents)
|
Cypress.Commands.add('interceptEvents', interceptEvents)
|
||||||
|
Cypress.Commands.add('interceptMetadata', interceptMetadata)
|
||||||
Cypress.Commands.add('interceptSpelling', interceptSpelling)
|
Cypress.Commands.add('interceptSpelling', interceptSpelling)
|
||||||
Cypress.Commands.add('waitForCompile', waitForCompile)
|
Cypress.Commands.add('waitForCompile', waitForCompile)
|
||||||
Cypress.Commands.add('interceptDeferredCompile', interceptDeferredCompile)
|
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 ide = useIdeContext()
|
||||||
|
|
||||||
const [project, setProject] = useScopeValue<ProjectSettings | undefined>(
|
const [project, setProject] = useScopeValue<ProjectSettings | undefined>(
|
||||||
'project',
|
'project'
|
||||||
true
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const setCompiler = useCallback(
|
const setCompiler = useCallback(
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { debugConsole } from '@/utils/debugging'
|
||||||
|
|
||||||
export default function useProjectWideSettings() {
|
export default function useProjectWideSettings() {
|
||||||
// The value will be undefined on mount
|
// The value will be undefined on mount
|
||||||
const [project] = useScopeValue<ProjectSettings | undefined>('project', true)
|
const [project] = useScopeValue<ProjectSettings | undefined>('project')
|
||||||
const saveProjectSettings = useSaveProjectSettings()
|
const saveProjectSettings = useSaveProjectSettings()
|
||||||
|
|
||||||
const setCompiler = useCallback(
|
const setCompiler = useCallback(
|
||||||
|
|
|
@ -6,7 +6,7 @@ export default function useSaveProjectSettings() {
|
||||||
// projectSettings value will be undefined on mount
|
// projectSettings value will be undefined on mount
|
||||||
const [projectSettings, setProjectSettings] = useScopeValue<
|
const [projectSettings, setProjectSettings] = useScopeValue<
|
||||||
ProjectSettings | undefined
|
ProjectSettings | undefined
|
||||||
>('project', true)
|
>('project')
|
||||||
const { _id: projectId } = useProjectContext()
|
const { _id: projectId } = useProjectContext()
|
||||||
|
|
||||||
return async (
|
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
|
disconnect: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const ConnectionContext = createContext<ConnectionContextValue | undefined>(
|
export const ConnectionContext = createContext<
|
||||||
undefined
|
ConnectionContextValue | undefined
|
||||||
)
|
>(undefined)
|
||||||
|
|
||||||
export const ConnectionProvider: FC = ({ children }) => {
|
export const ConnectionProvider: FC = ({ children }) => {
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
|
|
|
@ -87,7 +87,9 @@ export type EditorScopeValue = {
|
||||||
error_state: boolean
|
error_state: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const EditorManagerContext = createContext<EditorManager | undefined>(undefined)
|
export const EditorManagerContext = createContext<EditorManager | undefined>(
|
||||||
|
undefined
|
||||||
|
)
|
||||||
|
|
||||||
export const EditorManagerProvider: FC = ({ children }) => {
|
export const EditorManagerProvider: FC = ({ children }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
|
@ -40,7 +40,7 @@ type IdeReactContextValue = {
|
||||||
projectJoined: boolean
|
projectJoined: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const IdeReactContext = createContext<IdeReactContextValue | undefined>(
|
export const IdeReactContext = createContext<IdeReactContextValue | undefined>(
|
||||||
undefined
|
undefined
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -63,7 +63,7 @@ function populatePdfScope(store: ReactScopeValueStore) {
|
||||||
store.allowNonExistentPath('pdf', true)
|
store.allowNonExistentPath('pdf', true)
|
||||||
}
|
}
|
||||||
|
|
||||||
function createReactScopeValueStore(projectId: string) {
|
export function createReactScopeValueStore(projectId: string) {
|
||||||
const scopeStore = new ReactScopeValueStore()
|
const scopeStore = new ReactScopeValueStore()
|
||||||
|
|
||||||
// Populate the scope value store with default values that will be used by
|
// 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 { getJSON, postJSON } from '@/infrastructure/fetch-json'
|
||||||
import { useOnlineUsersContext } from '@/features/ide-react/context/online-users-context'
|
import { useOnlineUsersContext } from '@/features/ide-react/context/online-users-context'
|
||||||
import { useEditorContext } from '@/shared/context/editor-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 useSocketListener from '@/features/ide-react/hooks/use-socket-listener'
|
||||||
import useEventListener from '@/shared/hooks/use-event-listener'
|
import useEventListener from '@/shared/hooks/use-event-listener'
|
||||||
import { useModalsContext } from '@/features/ide-react/context/modals-context'
|
import { useModalsContext } from '@/features/ide-react/context/modals-context'
|
||||||
|
@ -42,13 +41,12 @@ type MetadataContextValue = {
|
||||||
|
|
||||||
type DocMetadataResponse = { docId: string; meta: DocumentMetadata }
|
type DocMetadataResponse = { docId: string; meta: DocumentMetadata }
|
||||||
|
|
||||||
const MetadataContext = createContext<MetadataContextValue | undefined>(
|
export const MetadataContext = createContext<MetadataContextValue | undefined>(
|
||||||
undefined
|
undefined
|
||||||
)
|
)
|
||||||
|
|
||||||
export const MetadataProvider: FC = ({ children }) => {
|
export const MetadataProvider: FC = ({ children }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const ide = useIdeContext()
|
|
||||||
const { eventEmitter, projectId } = useIdeReactContext()
|
const { eventEmitter, projectId } = useIdeReactContext()
|
||||||
const { socket } = useConnectionContext()
|
const { socket } = useConnectionContext()
|
||||||
const { onlineUsersCount } = useOnlineUsersContext()
|
const { onlineUsersCount } = useOnlineUsersContext()
|
||||||
|
@ -225,10 +223,6 @@ export const MetadataProvider: FC = ({ children }) => {
|
||||||
[documents, getAllLabels, getAllPackages]
|
[documents, getAllLabels, getAllPackages]
|
||||||
)
|
)
|
||||||
|
|
||||||
// Expose metadataManager via ide object because useCodeMirrorScope relies on
|
|
||||||
// it, for now
|
|
||||||
ide.metadataManager = value
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MetadataContext.Provider value={value}>
|
<MetadataContext.Provider value={value}>
|
||||||
{children}
|
{children}
|
||||||
|
|
|
@ -9,7 +9,9 @@ import {
|
||||||
import useScopeValue from '@/shared/hooks/use-scope-value'
|
import useScopeValue from '@/shared/hooks/use-scope-value'
|
||||||
import { DeepReadonly } from '../../../../../types/utils'
|
import { DeepReadonly } from '../../../../../types/utils'
|
||||||
|
|
||||||
const PermissionsContext = createContext<Permissions | undefined>(undefined)
|
export const PermissionsContext = createContext<Permissions | undefined>(
|
||||||
|
undefined
|
||||||
|
)
|
||||||
|
|
||||||
const permissionsMap: DeepReadonly<Record<PermissionsLevel, Permissions>> = {
|
const permissionsMap: DeepReadonly<Record<PermissionsLevel, Permissions>> = {
|
||||||
readOnly: {
|
readOnly: {
|
||||||
|
|
|
@ -40,10 +40,10 @@ export const ReactContextRoot: FC = ({ children }) => {
|
||||||
<PermissionsProvider>
|
<PermissionsProvider>
|
||||||
<ProjectSettingsProvider>
|
<ProjectSettingsProvider>
|
||||||
<LayoutProvider>
|
<LayoutProvider>
|
||||||
<LocalCompileProvider>
|
<EditorManagerProvider>
|
||||||
<DetachCompileProvider>
|
<LocalCompileProvider>
|
||||||
<ChatProvider>
|
<DetachCompileProvider>
|
||||||
<EditorManagerProvider>
|
<ChatProvider>
|
||||||
<FileTreeOpenProvider>
|
<FileTreeOpenProvider>
|
||||||
<OnlineUsersProvider>
|
<OnlineUsersProvider>
|
||||||
<MetadataProvider>
|
<MetadataProvider>
|
||||||
|
@ -53,10 +53,10 @@ export const ReactContextRoot: FC = ({ children }) => {
|
||||||
</MetadataProvider>
|
</MetadataProvider>
|
||||||
</OnlineUsersProvider>
|
</OnlineUsersProvider>
|
||||||
</FileTreeOpenProvider>
|
</FileTreeOpenProvider>
|
||||||
</EditorManagerProvider>
|
</ChatProvider>
|
||||||
</ChatProvider>
|
</DetachCompileProvider>
|
||||||
</DetachCompileProvider>
|
</LocalCompileProvider>
|
||||||
</LocalCompileProvider>
|
</EditorManagerProvider>
|
||||||
</LayoutProvider>
|
</LayoutProvider>
|
||||||
</ProjectSettingsProvider>
|
</ProjectSettingsProvider>
|
||||||
</PermissionsProvider>
|
</PermissionsProvider>
|
||||||
|
|
|
@ -170,10 +170,7 @@ function useReviewPanelState(): ReviewPanel.ReviewPanelState {
|
||||||
ReviewPanel.Value<'commentThreads'>
|
ReviewPanel.Value<'commentThreads'>
|
||||||
>({})
|
>({})
|
||||||
const [entries, setEntries] = useState<ReviewPanel.Value<'entries'>>({})
|
const [entries, setEntries] = useState<ReviewPanel.Value<'entries'>>({})
|
||||||
const [users, setUsers] = useScopeValue<ReviewPanel.Value<'users'>>(
|
const [users, setUsers] = useScopeValue<ReviewPanel.Value<'users'>>('users')
|
||||||
'users',
|
|
||||||
true
|
|
||||||
)
|
|
||||||
const [resolvedComments, setResolvedComments] = useState<
|
const [resolvedComments, setResolvedComments] = useState<
|
||||||
ReviewPanel.Value<'resolvedComments'>
|
ReviewPanel.Value<'resolvedComments'>
|
||||||
>({})
|
>({})
|
||||||
|
@ -532,7 +529,6 @@ function useReviewPanelState(): ReviewPanel.ReviewPanelState {
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return 'anonymous'
|
return 'anonymous'
|
||||||
}
|
}
|
||||||
// FIXME: check this
|
|
||||||
if (project.owner._id === user.id) {
|
if (project.owner._id === user.id) {
|
||||||
return 'member'
|
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 ReactDOM from 'react-dom'
|
||||||
import PdfPreview from './pdf-preview'
|
import PdfPreview from './pdf-preview'
|
||||||
import { ContextRoot } from '../../../shared/context/root-context'
|
|
||||||
import useWaitForI18n from '../../../shared/hooks/use-wait-for-i18n'
|
import useWaitForI18n from '../../../shared/hooks/use-wait-for-i18n'
|
||||||
|
import { ReactContextRoot } from '@/features/ide-react/context/react-context-root'
|
||||||
|
|
||||||
function PdfPreviewDetachedRoot() {
|
function PdfPreviewDetachedRoot() {
|
||||||
const { isReady } = useWaitForI18n()
|
const { isReady } = useWaitForI18n()
|
||||||
|
@ -11,9 +11,9 @@ function PdfPreviewDetachedRoot() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ContextRoot>
|
<ReactContextRoot>
|
||||||
<PdfPreview />
|
<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,
|
useReviewPanelUpdaterFnsContext,
|
||||||
useReviewPanelValueContext,
|
useReviewPanelValueContext,
|
||||||
} from '../../../context/review-panel/review-panel-context'
|
} from '../../../context/review-panel/review-panel-context'
|
||||||
import { useIdeContext } from '@/shared/context/ide-context'
|
|
||||||
import { useEditorContext } from '@/shared/context/editor-context'
|
import { useEditorContext } from '@/shared/context/editor-context'
|
||||||
import { useCodeMirrorViewContext } from '../../codemirror-editor'
|
import { useCodeMirrorViewContext } from '../../codemirror-editor'
|
||||||
import Modal, { useBulkActionsModal } from '../entries/bulk-actions-entry/modal'
|
import Modal, { useBulkActionsModal } from '../entries/bulk-actions-entry/modal'
|
||||||
import getMeta from '../../../../../utils/meta'
|
import getMeta from '../../../../../utils/meta'
|
||||||
import useScopeValue from '../../../../../shared/hooks/use-scope-value'
|
|
||||||
import useScopeEventListener from '@/shared/hooks/use-scope-event-listener'
|
import useScopeEventListener from '@/shared/hooks/use-scope-event-listener'
|
||||||
import { memo, useCallback } from 'react'
|
import { memo, useCallback } from 'react'
|
||||||
import { useLayoutContext } from '@/shared/context/layout-context'
|
import { useLayoutContext } from '@/shared/context/layout-context'
|
||||||
|
@ -29,14 +27,8 @@ function EditorWidgets() {
|
||||||
handleShowBulkRejectDialog,
|
handleShowBulkRejectDialog,
|
||||||
handleConfirmDialog,
|
handleConfirmDialog,
|
||||||
} = useBulkActionsModal()
|
} = useBulkActionsModal()
|
||||||
const { setIsAddingComment, handleSetSubview } =
|
const { setIsAddingComment } = useReviewPanelUpdaterFnsContext()
|
||||||
useReviewPanelUpdaterFnsContext()
|
|
||||||
const { isReactIde } = useIdeContext()
|
|
||||||
const { toggleReviewPanel } = useReviewPanelUpdaterFnsContext()
|
const { toggleReviewPanel } = useReviewPanelUpdaterFnsContext()
|
||||||
const [addNewComment] =
|
|
||||||
useScopeValue<(e: React.MouseEvent<HTMLButtonElement>) => void>(
|
|
||||||
'addNewComment'
|
|
||||||
)
|
|
||||||
const view = useCodeMirrorViewContext()
|
const view = useCodeMirrorViewContext()
|
||||||
const { reviewPanelOpen } = useLayoutContext()
|
const { reviewPanelOpen } = useLayoutContext()
|
||||||
const { isRestrictedTokenMember } = useEditorContext()
|
const { isRestrictedTokenMember } = useEditorContext()
|
||||||
|
@ -55,20 +47,9 @@ function EditorWidgets() {
|
||||||
openDocId && openDocId in entries ? entries[openDocId] : undefined
|
openDocId && openDocId in entries ? entries[openDocId] : undefined
|
||||||
|
|
||||||
const handleAddNewCommentClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
const handleAddNewCommentClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
if (isReactIde) {
|
e.preventDefault()
|
||||||
e.preventDefault()
|
setIsAddingComment(true)
|
||||||
setIsAddingComment(true)
|
toggleReviewPanel()
|
||||||
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(
|
useScopeEventListener(
|
||||||
|
|
|
@ -8,26 +8,14 @@ import { isCurrentFileView } from '../../utils/sub-view'
|
||||||
import { useLayoutContext } from '@/shared/context/layout-context'
|
import { useLayoutContext } from '@/shared/context/layout-context'
|
||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
import { lazy, memo } from 'react'
|
import { lazy, memo } from 'react'
|
||||||
import getMeta from '@/utils/meta'
|
|
||||||
import { SubView } from '../../../../../../types/review-panel/review-panel'
|
import { SubView } from '../../../../../../types/review-panel/review-panel'
|
||||||
|
|
||||||
const isReactIde: boolean = getMeta('ol-idePageReact')
|
|
||||||
|
|
||||||
type ReviewPanelViewProps = {
|
type ReviewPanelViewProps = {
|
||||||
parentDomNode: Element
|
parentDomNode: Element
|
||||||
}
|
}
|
||||||
|
|
||||||
function ReviewPanelView({ parentDomNode }: ReviewPanelViewProps) {
|
function ReviewPanelView({ parentDomNode }: ReviewPanelViewProps) {
|
||||||
const { subView } = useReviewPanelValueContext()
|
return ReactDOM.createPortal(<ReviewPanelContainer />, parentDomNode)
|
||||||
|
|
||||||
return ReactDOM.createPortal(
|
|
||||||
isReactIde ? (
|
|
||||||
<ReviewPanelContainer />
|
|
||||||
) : (
|
|
||||||
<ReviewPanelContent subView={subView} />
|
|
||||||
),
|
|
||||||
parentDomNode
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const ReviewPanelContainer = memo(() => {
|
const ReviewPanelContainer = memo(() => {
|
||||||
|
@ -65,10 +53,9 @@ const ReviewPanelContent = memo<{ subView: SubView }>(({ subView }) => (
|
||||||
))
|
))
|
||||||
ReviewPanelContent.displayName = 'ReviewPanelContent'
|
ReviewPanelContent.displayName = 'ReviewPanelContent'
|
||||||
|
|
||||||
const ReviewPanelProvider = lazy(() =>
|
const ReviewPanelProvider = lazy(
|
||||||
isReactIde
|
() =>
|
||||||
? import('@/features/ide-react/context/review-panel/review-panel-provider')
|
import('@/features/ide-react/context/review-panel/review-panel-provider')
|
||||||
: import('../../context/review-panel/review-panel-provider')
|
|
||||||
)
|
)
|
||||||
|
|
||||||
function ReviewPanel() {
|
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,
|
setMetadata,
|
||||||
setSyntaxValidation,
|
setSyntaxValidation,
|
||||||
} from '../extensions/language'
|
} from '../extensions/language'
|
||||||
import { useIdeContext } from '../../../shared/context/ide-context'
|
|
||||||
import { restoreScrollPosition } from '../extensions/scroll-position'
|
import { restoreScrollPosition } from '../extensions/scroll-position'
|
||||||
import { setEditable } from '../extensions/editable'
|
import { setEditable } from '../extensions/editable'
|
||||||
import { useFileTreeData } from '../../../shared/context/file-tree-data-context'
|
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 { DocumentContainer } from '@/features/ide-react/editor/document-container'
|
||||||
import { useLayoutContext } from '@/shared/context/layout-context'
|
import { useLayoutContext } from '@/shared/context/layout-context'
|
||||||
import { debugConsole } from '@/utils/debugging'
|
import { debugConsole } from '@/utils/debugging'
|
||||||
|
import { useMetadataContext } from '@/features/ide-react/context/metadata-context'
|
||||||
|
|
||||||
function useCodeMirrorScope(view: EditorView) {
|
function useCodeMirrorScope(view: EditorView) {
|
||||||
const ide = useIdeContext()
|
|
||||||
|
|
||||||
const { fileTreeData } = useFileTreeData()
|
const { fileTreeData } = useFileTreeData()
|
||||||
|
|
||||||
const [permissions] = useScopeValue<{ write: boolean }>('permissions')
|
const [permissions] = useScopeValue<{ write: boolean }>('permissions')
|
||||||
|
@ -74,6 +72,8 @@ function useCodeMirrorScope(view: EditorView) {
|
||||||
|
|
||||||
const { reviewPanelOpen, miniReviewPanelVisible } = useLayoutContext()
|
const { reviewPanelOpen, miniReviewPanelVisible } = useLayoutContext()
|
||||||
|
|
||||||
|
const { metadata } = useMetadataContext()
|
||||||
|
|
||||||
const [loadingThreads] = useScopeValue<boolean>('loadingThreads')
|
const [loadingThreads] = useScopeValue<boolean>('loadingThreads')
|
||||||
|
|
||||||
const [currentDoc] = useScopeValue<DocumentContainer | null>(
|
const [currentDoc] = useScopeValue<DocumentContainer | null>(
|
||||||
|
@ -211,22 +211,16 @@ function useCodeMirrorScope(view: EditorView) {
|
||||||
// set the project metadata, mostly for use in autocomplete
|
// set the project metadata, mostly for use in autocomplete
|
||||||
// TODO: read this data from the scope?
|
// TODO: read this data from the scope?
|
||||||
const metadataRef = useRef({
|
const metadataRef = useRef({
|
||||||
documents: ide.metadataManager.metadata.state.documents,
|
documents: metadata.state.documents,
|
||||||
references: references.keys,
|
references: references.keys,
|
||||||
fileTreeData,
|
fileTreeData,
|
||||||
})
|
})
|
||||||
|
|
||||||
// listen to project metadata (docs + packages) updates
|
// listen to project metadata (docs + packages) updates
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const listener = (event: Event) => {
|
metadataRef.current.documents = metadata.state.documents
|
||||||
metadataRef.current.documents = (
|
view.dispatch(setMetadata(metadataRef.current))
|
||||||
event as CustomEvent<Record<string, any>>
|
}, [view, metadata.state.documents])
|
||||||
).detail
|
|
||||||
view.dispatch(setMetadata(metadataRef.current))
|
|
||||||
}
|
|
||||||
window.addEventListener('project:metadata', listener)
|
|
||||||
return () => window.removeEventListener('project:metadata', listener)
|
|
||||||
}, [view])
|
|
||||||
|
|
||||||
// listen to project reference keys updates
|
// listen to project reference keys updates
|
||||||
useEffect(() => {
|
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