diff --git a/package-lock.json b/package-lock.json index 0ba5e13ce3..eb8542685b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12243,12 +12243,6 @@ "@types/node": "*" } }, - "node_modules/@types/angular": { - "version": "1.8.4", - "resolved": "https://registry.npmjs.org/@types/angular/-/angular-1.8.4.tgz", - "integrity": "sha512-wPS/ncJWhyxJsndsW1B6Ta8D4mi97x1yItSu+rkLDytU3oRIh2CFAjMuJceYwFAh9+DIohndWM0QBA9OU2Hv0g==", - "dev": true - }, "node_modules/@types/aria-query": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-4.2.2.tgz", @@ -12691,15 +12685,6 @@ "integrity": "sha512-0d5Wd09ItQWH1qFbEyQ7oTQ3GZrMfth5JkbN3EvTKLXcHLRDSXeLnlvlOn0wvxVIwK5o2M8JzP/OWz7T3NRsbw==", "dev": true }, - "node_modules/@types/lodash.frompairs": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@types/lodash.frompairs/-/lodash.frompairs-4.0.6.tgz", - "integrity": "sha512-rwCUf4NMKhXpiVjL/RXP8YOk+rd02/J4tACADEgaMXRVnzDbSSlBMKFZoX/ARmHVLg3Qc98Um4PErGv8FbxU7w==", - "dev": true, - "dependencies": { - "@types/lodash": "*" - } - }, "node_modules/@types/long": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz", @@ -14733,25 +14718,6 @@ "integrity": "sha512-VJK1SRmXBpjwsB4YOHYSturx48rLKMzHgCqDH2ZDa6ZbMS/N5huoNqyQdK5Fj/xayu3fqbXckn5SeCS1EbMDZg==", "dev": true }, - "node_modules/angular": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/angular/-/angular-1.8.3.tgz", - "integrity": "sha512-5qjkWIQQVsHj4Sb5TcEs4WZWpFeVFHXwxEBHUhrny41D8UrBAd6T/6nPPAsLngJCReIOqi95W3mxdveveutpZw==", - "deprecated": "For the actively supported Angular, see https://www.npmjs.com/package/@angular/core. AngularJS support has officially ended. For extended AngularJS support options, see https://goo.gle/angularjs-path-forward.", - "dev": true - }, - "node_modules/angular-mocks": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/angular-mocks/-/angular-mocks-1.8.2.tgz", - "integrity": "sha512-I5L3P0l21HPdVsP4A4qWmENt4ePjjbkDFdAzOaM7QiibFySbt14DptPbt2IjeG4vFBr4vSLbhIz8Fk03DISl8Q==", - "dev": true - }, - "node_modules/angular-sanitize": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/angular-sanitize/-/angular-sanitize-1.8.2.tgz", - "integrity": "sha512-OB6Goa+QN3byf5asQ7XRl7DKZejm/F/ZOqa9z1skqYVOWA2hoBxoCmt9E7+i7T/TbxZP5zYzKxNZVVJNu860Hg==", - "dev": true - }, "node_modules/ansi-color": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/ansi-color/-/ansi-color-0.2.1.tgz", @@ -15617,17 +15583,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/babel-plugin-angularjs-annotate": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/babel-plugin-angularjs-annotate/-/babel-plugin-angularjs-annotate-0.10.0.tgz", - "integrity": "sha512-NPE7FOAxcLPCUR/kNkrhHIjoScR3RyIlRH3yRn79j8EZWtpILVnCOdA9yKfsOmRh6BHnLHKl8ZAThc+YDd/QwQ==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.0.0", - "@babel/types": "^7.2.0", - "simple-is": "~0.2.0" - } - }, "node_modules/babel-plugin-macros": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", @@ -27997,12 +27952,6 @@ "lodash.keys": "~2.4.1" } }, - "node_modules/lodash.frompairs": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.frompairs/-/lodash.frompairs-4.0.1.tgz", - "integrity": "sha1-vE5SB/onV8E25XNhTpZkUGsrG9I=", - "dev": true - }, "node_modules/lodash.get": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", @@ -30011,18 +29960,6 @@ "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==" }, - "node_modules/ngcomponent": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ngcomponent/-/ngcomponent-4.1.0.tgz", - "integrity": "sha512-cGL3iVoqMWTpCfaIwgRKhdaGqiy2Z+CCG0cVfjlBvdqE8saj8xap9B4OTf+qwObxLVZmDTJPDgx3bN6Q/lZ7BQ==", - "dev": true, - "dependencies": { - "@types/angular": "^1.6.39", - "@types/lodash": "^4.14.85", - "angular": ">=1.5.0", - "lodash": "^4.17.4" - } - }, "node_modules/nise": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/nise/-/nise-4.1.0.tgz", @@ -34807,52 +34744,6 @@ "react-dom": ">=15.0.0" } }, - "node_modules/react2angular": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/react2angular/-/react2angular-4.0.6.tgz", - "integrity": "sha512-MDl2WRoTyu7Gyh4+FAIlmsM2mxIa/DjSz6G/d90L1tK8ZRubqVEayKF6IPyAruC5DMhGDVJ7tlAIcu/gMNDjXg==", - "dev": true, - "dependencies": { - "@types/lodash.frompairs": "^4.0.5", - "angular": ">=1.5", - "lodash.frompairs": "^4.0.1", - "ngcomponent": "^4.1.0" - }, - "peerDependencies": { - "@types/angular": ">=1.5", - "@types/prop-types": ">=15", - "@types/react": ">=16", - "@types/react-dom": ">=16", - "prop-types": ">=15", - "react": ">=15", - "react-dom": ">=15" - } - }, - "node_modules/react2angular-shared-context": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/react2angular-shared-context/-/react2angular-shared-context-1.1.2.tgz", - "integrity": "sha512-0zrxBjmBs+et5zYNknx/jvrJCzGz6KbF8BHfzXHTl9ms6iMsbmmXkZiQQksVT1Og5wnkmVq9nlLVfWYJLSXF0w==", - "dev": true, - "dependencies": { - "uuid": "^8.3.2" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "react": "^16.0.0 || ^17", - "react-dom": "^16.0.0 || ^17" - } - }, - "node_modules/react2angular-shared-context/node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true, - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/reactcss": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz", @@ -36673,12 +36564,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/simple-is": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/simple-is/-/simple-is-0.2.0.tgz", - "integrity": "sha1-Krt1qt453rXMgVzhDmGRFkhQuvA=", - "dev": true - }, "node_modules/simple-oauth2": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/simple-oauth2/-/simple-oauth2-5.0.0.tgz", @@ -44616,12 +44501,8 @@ "acorn": "^7.1.1", "acorn-walk": "^7.1.1", "algoliasearch": "^3.35.1", - "angular": "~1.8.0", - "angular-mocks": "~1.8.0", - "angular-sanitize": "~1.8.0", "autoprefixer": "^10.4.16", "babel-loader": "^9.1.2", - "babel-plugin-angularjs-annotate": "^0.10.0", "babel-plugin-macros": "^3.1.0", "babel-plugin-module-resolver": "^5.0.0", "backbone": "^1.3.3", @@ -44698,8 +44579,6 @@ "react-linkify": "^1.0.0-alpha", "react-refresh": "^0.14.0", "react-resizable-panels": "^1.0.3", - "react2angular": "^4.0.6", - "react2angular-shared-context": "^1.1.0", "requirejs": "^2.3.6", "resolve-url-loader": "^5.0.0", "samlp": "^7.0.2", @@ -53003,14 +52882,10 @@ "acorn": "^7.1.1", "acorn-walk": "^7.1.1", "algoliasearch": "^3.35.1", - "angular": "~1.8.0", - "angular-mocks": "~1.8.0", - "angular-sanitize": "~1.8.0", "archiver": "^5.3.0", "async": "3.2.2", "autoprefixer": "^10.4.16", "babel-loader": "^9.1.2", - "babel-plugin-angularjs-annotate": "^0.10.0", "babel-plugin-macros": "^3.1.0", "babel-plugin-module-resolver": "^5.0.0", "backbone": "^1.3.3", @@ -53152,8 +53027,6 @@ "react-linkify": "^1.0.0-alpha", "react-refresh": "^0.14.0", "react-resizable-panels": "^1.0.3", - "react2angular": "^4.0.6", - "react2angular-shared-context": "^1.1.0", "recurly": "^4.0.0", "referer-parser": "github:overleaf/nodejs-referer-parser#8b8b103762d05b7be4cfa2f810e1d408be67d7bb", "request": "^2.88.2", @@ -57040,12 +56913,6 @@ "@types/node": "*" } }, - "@types/angular": { - "version": "1.8.4", - "resolved": "https://registry.npmjs.org/@types/angular/-/angular-1.8.4.tgz", - "integrity": "sha512-wPS/ncJWhyxJsndsW1B6Ta8D4mi97x1yItSu+rkLDytU3oRIh2CFAjMuJceYwFAh9+DIohndWM0QBA9OU2Hv0g==", - "dev": true - }, "@types/aria-query": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-4.2.2.tgz", @@ -57487,15 +57354,6 @@ "integrity": "sha512-0d5Wd09ItQWH1qFbEyQ7oTQ3GZrMfth5JkbN3EvTKLXcHLRDSXeLnlvlOn0wvxVIwK5o2M8JzP/OWz7T3NRsbw==", "dev": true }, - "@types/lodash.frompairs": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@types/lodash.frompairs/-/lodash.frompairs-4.0.6.tgz", - "integrity": "sha512-rwCUf4NMKhXpiVjL/RXP8YOk+rd02/J4tACADEgaMXRVnzDbSSlBMKFZoX/ARmHVLg3Qc98Um4PErGv8FbxU7w==", - "dev": true, - "requires": { - "@types/lodash": "*" - } - }, "@types/long": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz", @@ -59081,24 +58939,6 @@ } } }, - "angular": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/angular/-/angular-1.8.3.tgz", - "integrity": "sha512-5qjkWIQQVsHj4Sb5TcEs4WZWpFeVFHXwxEBHUhrny41D8UrBAd6T/6nPPAsLngJCReIOqi95W3mxdveveutpZw==", - "dev": true - }, - "angular-mocks": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/angular-mocks/-/angular-mocks-1.8.2.tgz", - "integrity": "sha512-I5L3P0l21HPdVsP4A4qWmENt4ePjjbkDFdAzOaM7QiibFySbt14DptPbt2IjeG4vFBr4vSLbhIz8Fk03DISl8Q==", - "dev": true - }, - "angular-sanitize": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/angular-sanitize/-/angular-sanitize-1.8.2.tgz", - "integrity": "sha512-OB6Goa+QN3byf5asQ7XRl7DKZejm/F/ZOqa9z1skqYVOWA2hoBxoCmt9E7+i7T/TbxZP5zYzKxNZVVJNu860Hg==", - "dev": true - }, "ansi-color": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/ansi-color/-/ansi-color-0.2.1.tgz", @@ -59740,17 +59580,6 @@ } } }, - "babel-plugin-angularjs-annotate": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/babel-plugin-angularjs-annotate/-/babel-plugin-angularjs-annotate-0.10.0.tgz", - "integrity": "sha512-NPE7FOAxcLPCUR/kNkrhHIjoScR3RyIlRH3yRn79j8EZWtpILVnCOdA9yKfsOmRh6BHnLHKl8ZAThc+YDd/QwQ==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "@babel/types": "^7.2.0", - "simple-is": "~0.2.0" - } - }, "babel-plugin-macros": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", @@ -70105,12 +69934,6 @@ "lodash.keys": "~2.4.1" } }, - "lodash.frompairs": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.frompairs/-/lodash.frompairs-4.0.1.tgz", - "integrity": "sha1-vE5SB/onV8E25XNhTpZkUGsrG9I=", - "dev": true - }, "lodash.get": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", @@ -71596,18 +71419,6 @@ "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==" }, - "ngcomponent": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ngcomponent/-/ngcomponent-4.1.0.tgz", - "integrity": "sha512-cGL3iVoqMWTpCfaIwgRKhdaGqiy2Z+CCG0cVfjlBvdqE8saj8xap9B4OTf+qwObxLVZmDTJPDgx3bN6Q/lZ7BQ==", - "dev": true, - "requires": { - "@types/angular": "^1.6.39", - "@types/lodash": "^4.14.85", - "angular": ">=1.5.0", - "lodash": "^4.17.4" - } - }, "nise": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/nise/-/nise-4.1.0.tgz", @@ -75149,35 +74960,6 @@ "react-lifecycles-compat": "^3.0.4" } }, - "react2angular": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/react2angular/-/react2angular-4.0.6.tgz", - "integrity": "sha512-MDl2WRoTyu7Gyh4+FAIlmsM2mxIa/DjSz6G/d90L1tK8ZRubqVEayKF6IPyAruC5DMhGDVJ7tlAIcu/gMNDjXg==", - "dev": true, - "requires": { - "@types/lodash.frompairs": "^4.0.5", - "angular": ">=1.5", - "lodash.frompairs": "^4.0.1", - "ngcomponent": "^4.1.0" - } - }, - "react2angular-shared-context": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/react2angular-shared-context/-/react2angular-shared-context-1.1.2.tgz", - "integrity": "sha512-0zrxBjmBs+et5zYNknx/jvrJCzGz6KbF8BHfzXHTl9ms6iMsbmmXkZiQQksVT1Og5wnkmVq9nlLVfWYJLSXF0w==", - "dev": true, - "requires": { - "uuid": "^8.3.2" - }, - "dependencies": { - "uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true - } - } - }, "reactcss": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz", @@ -76578,12 +76360,6 @@ } } }, - "simple-is": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/simple-is/-/simple-is-0.2.0.tgz", - "integrity": "sha1-Krt1qt453rXMgVzhDmGRFkhQuvA=", - "dev": true - }, "simple-oauth2": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/simple-oauth2/-/simple-oauth2-5.0.0.tgz", diff --git a/patches/ngcomponent+4.1.0.patch b/patches/ngcomponent+4.1.0.patch deleted file mode 100644 index d8bc68175e..0000000000 --- a/patches/ngcomponent+4.1.0.patch +++ /dev/null @@ -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') diff --git a/patches/react2angular+4.0.6.patch b/patches/react2angular+4.0.6.patch deleted file mode 100644 index afcf627f67..0000000000 --- a/patches/react2angular+4.0.6.patch +++ /dev/null @@ -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' diff --git a/services/web/app/src/Features/Project/ProjectController.js b/services/web/app/src/Features/Project/ProjectController.js index 0ce87ec293..4c8841d94f 100644 --- a/services/web/app/src/Features/Project/ProjectController.js +++ b/services/web/app/src/Features/Project/ProjectController.js @@ -603,15 +603,10 @@ const _ProjectController = { !showPersonalAccessToken && splitTestAssignments['personal-access-token'].variant === 'enabled' // `?personal-access-token=enabled` - const idePageReact = splitTestAssignments['ide-page'].variant === 'react' - const template = detachRole === 'detached' - ? // TODO: Create React version of detached page - 'project/editor_detached' - : idePageReact - ? 'project/ide-react' - : 'project/editor' + ? 'project/ide-react-detached' + : 'project/ide-react' res.render(template, { title: project.name, @@ -681,7 +676,6 @@ const _ProjectController = { showUpgradePrompt, fixedSizeDocument: true, useOpenTelemetry: Settings.useOpenTelemetryClient, - idePageReact, showPersonalAccessToken, optionalPersonalAccessToken, hasTrackChangesFeature: Features.hasFeature('track-changes'), diff --git a/services/web/app/src/Features/TokenAccess/TokenAccessController.js b/services/web/app/src/Features/TokenAccess/TokenAccessController.js index e9d9b30077..8174ca797e 100644 --- a/services/web/app/src/Features/TokenAccess/TokenAccessController.js +++ b/services/web/app/src/Features/TokenAccess/TokenAccessController.js @@ -12,7 +12,6 @@ const { handleAdminDomainRedirect, } = require('../Authorization/AuthorizationMiddleware') const ProjectAuditLogHandler = require('../Project/ProjectAuditLogHandler') -const SplitTestHandler = require('../SplitTests/SplitTestHandler') const orderedPrivilegeLevels = [ PrivilegeLevels.NONE, @@ -98,18 +97,7 @@ async function tokenAccessPage(req, res, next) { } } - const { variant } = await SplitTestHandler.promises.getAssignment( - req, - res, - 'token-access-page' - ) - - const view = - variant === 'react' - ? 'project/token/access-react' - : 'project/token/access' - - res.render(view, { + res.render('project/token/access-react', { postUrl: makePostUrl(token), }) } catch (err) { diff --git a/services/web/app/views/layout-base.pug b/services/web/app/views/layout-base.pug index 83d6e4cf4e..e33ea57f7a 100644 --- a/services/web/app/views/layout-base.pug +++ b/services/web/app/views/layout-base.pug @@ -72,7 +72,7 @@ html( block head-scripts - body(ng-csp=(cspEnabled ? "no-unsafe-eval" : false) class=(showThinFooter ? 'thin-footer' : undefined)) + body(class=(showThinFooter ? 'thin-footer' : undefined)) if(settings.recaptcha && settings.recaptcha.siteKeyV3) script(type="text/javascript", nonce=scriptNonce, src="https://www.recaptcha.net/recaptcha/api.js?render="+settings.recaptcha.siteKeyV3, defer=deferScripts) diff --git a/services/web/app/views/layout.pug b/services/web/app/views/layout.pug deleted file mode 100644 index ff4481c87b..0000000000 --- a/services/web/app/views/layout.pug +++ /dev/null @@ -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) diff --git a/services/web/app/views/layout/footer.pug b/services/web/app/views/layout/footer.pug deleted file mode 100644 index f43d44385c..0000000000 --- a/services/web/app/views/layout/footer.pug +++ /dev/null @@ -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} diff --git a/services/web/app/views/layout/navbar.pug b/services/web/app/views/layout/navbar.pug deleted file mode 100644 index 5a325ca048..0000000000 --- a/services/web/app/views/layout/navbar.pug +++ /dev/null @@ -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')} diff --git a/services/web/app/views/project/editor.pug b/services/web/app/views/project/editor.pug deleted file mode 100644 index 4cc2bd0175..0000000000 --- a/services/web/app/views/project/editor.pug +++ /dev/null @@ -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') diff --git a/services/web/app/views/project/editor/editor-pane.pug b/services/web/app/views/project/editor/editor-pane.pug deleted file mode 100644 index 0d86e44678..0000000000 --- a/services/web/app/views/project/editor/editor-pane.pug +++ /dev/null @@ -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) diff --git a/services/web/app/views/project/editor/editor.pug b/services/web/app/views/project/editor/editor.pug deleted file mode 100644 index 7d69c244a8..0000000000 --- a/services/web/app/views/project/editor/editor.pug +++ /dev/null @@ -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')} - diff --git a/services/web/app/views/project/editor/file-tree-history-react.pug b/services/web/app/views/project/editor/file-tree-history-react.pug deleted file mode 100644 index a5788dbb45..0000000000 --- a/services/web/app/views/project/editor/file-tree-history-react.pug +++ /dev/null @@ -1,3 +0,0 @@ -aside.editor-sidebar.full-size.history-file-tree#history-file-tree( - ng-show="ui.view == 'history'" -) diff --git a/services/web/app/views/project/editor/file-tree-react.pug b/services/web/app/views/project/editor/file-tree-react.pug deleted file mode 100644 index 8895a86161..0000000000 --- a/services/web/app/views/project/editor/file-tree-react.pug +++ /dev/null @@ -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 - ) diff --git a/services/web/app/views/project/editor/file-view.pug b/services/web/app/views/project/editor/file-view.pug deleted file mode 100644 index e3f7f0b3c4..0000000000 --- a/services/web/app/views/project/editor/file-view.pug +++ /dev/null @@ -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' - ) diff --git a/services/web/app/views/project/editor/header-react.pug b/services/web/app/views/project/editor/header-react.pug deleted file mode 100644 index c1ab7fb6bc..0000000000 --- a/services/web/app/views/project/editor/header-react.pug +++ /dev/null @@ -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" - ) diff --git a/services/web/app/views/project/editor/left-menu-react.pug b/services/web/app/views/project/editor/left-menu-react.pug deleted file mode 100644 index 814220ff0f..0000000000 --- a/services/web/app/views/project/editor/left-menu-react.pug +++ /dev/null @@ -1 +0,0 @@ -editor-left-menu() diff --git a/services/web/app/views/project/editor/main.pug b/services/web/app/views/project/editor/main.pug deleted file mode 100644 index 69715bfbdf..0000000000 --- a/services/web/app/views/project/editor/main.pug +++ /dev/null @@ -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() diff --git a/services/web/app/views/project/editor/meta.pug b/services/web/app/views/project/editor/meta.pug index 77e8ad42aa..f161fd81a4 100644 --- a/services/web/app/views/project/editor/meta.pug +++ b/services/web/app/views/project/editor/meta.pug @@ -35,7 +35,6 @@ meta(name="ol-optionalPersonalAccessToken", data-type="boolean" content=optional meta(name="ol-hasTrackChangesFeature", data-type="boolean" content=hasTrackChangesFeature) meta(name="ol-inactiveTutorials", data-type="json" content=user.inactiveTutorials) meta(name="ol-projectTags" data-type="json" content=projectTags) -meta(name="ol-idePageReact", data-type="boolean" content=idePageReact) meta(name="ol-loadingText", data-type="string" content=translate("loading")) meta(name="ol-translationLoadErrorMessage", data-type="string" content=translate("could_not_load_translations")) diff --git a/services/web/app/views/project/editor_detached.pug b/services/web/app/views/project/ide-react-detached.pug similarity index 91% rename from services/web/app/views/project/editor_detached.pug rename to services/web/app/views/project/ide-react-detached.pug index 874082fa01..88b48da5b7 100644 --- a/services/web/app/views/project/editor_detached.pug +++ b/services/web/app/views/project/ide-react-detached.pug @@ -1,4 +1,4 @@ -extends ../layout +extends ../layout-marketing block entrypointVar - entrypoint = 'ide-detached' diff --git a/services/web/app/views/project/ide-react.pug b/services/web/app/views/project/ide-react.pug index 83cd038d5b..3071e0ef5d 100644 --- a/services/web/app/views/project/ide-react.pug +++ b/services/web/app/views/project/ide-react.pug @@ -1,4 +1,4 @@ -extends ../layout +extends ../layout-marketing block vars - var suppressNavbar = true diff --git a/services/web/app/views/project/token/access.pug b/services/web/app/views/project/token/access.pug deleted file mode 100644 index cad37498cf..0000000000 --- a/services/web/app/views/project/token/access.pug +++ /dev/null @@ -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); - }); diff --git a/services/web/app/views/source-editor/source-editor.pug b/services/web/app/views/source-editor/source-editor.pug deleted file mode 100644 index 9aa186fba2..0000000000 --- a/services/web/app/views/source-editor/source-editor.pug +++ /dev/null @@ -1,3 +0,0 @@ -source-editor.review-panel-react#editor( - ng-show="!!editor.sharejs_doc && !editor.opening && multiSelectedCount === 0 && !editor.error_state" -) diff --git a/services/web/app/views/subscriptions/interstitial-payment.pug b/services/web/app/views/subscriptions/interstitial-payment.pug index 8cebac6831..1ccf5c31f3 100644 --- a/services/web/app/views/subscriptions/interstitial-payment.pug +++ b/services/web/app/views/subscriptions/interstitial-payment.pug @@ -1,10 +1,11 @@ -extends ../layout +extends ../layout-marketing include ./plans/_mixins -include ../_mixins/bootstrap_js + +block entrypointVar + - entrypoint = 'pages/user/subscription/plans-v2/plans-v2-main' block vars - - entrypoint = 'pages/user/subscription/plans-v2/plans-v2-main' - var suppressFooter = true - var suppressNavbarRight = true - var suppressCookieBanner = true @@ -66,6 +67,3 @@ block content | #{translate("continue_with_free_plan")} != moduleIncludes("contactModalGeneral-marketing", locals) - -block prepend foot-scripts - +bootstrap-js(bootstrapVersion) diff --git a/services/web/app/views/subscriptions/plans.pug b/services/web/app/views/subscriptions/plans.pug index 032bae4181..9ba135a0f3 100644 --- a/services/web/app/views/subscriptions/plans.pug +++ b/services/web/app/views/subscriptions/plans.pug @@ -1,6 +1,6 @@ extends ../layout-marketing -block vars +block entrypointVar - entrypoint = 'pages/user/subscription/plans-v2/plans-v2-main' block append meta diff --git a/services/web/app/views/subscriptions/team/invite_logged_out.pug b/services/web/app/views/subscriptions/team/invite_logged_out.pug index ddf24b3d9d..bfd2a8e949 100644 --- a/services/web/app/views/subscriptions/team/invite_logged_out.pug +++ b/services/web/app/views/subscriptions/team/invite_logged_out.pug @@ -1,4 +1,4 @@ -extends ../../layout +extends ../../layout-marketing block content main.content.content-alt.team-invite#main-content diff --git a/services/web/app/views/view_templates/bonus_templates.pug b/services/web/app/views/view_templates/bonus_templates.pug deleted file mode 100644 index 58b4fc4973..0000000000 --- a/services/web/app/views/view_templates/bonus_templates.pug +++ /dev/null @@ -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 diff --git a/services/web/babel.config.json b/services/web/babel.config.json index 0f2fe30012..26fc60f388 100644 --- a/services/web/babel.config.json +++ b/services/web/babel.config.json @@ -27,7 +27,7 @@ ["@babel/react", { "runtime": "automatic" }], "@babel/typescript" ], - "plugins": ["angularjs-annotate", "macros"], + "plugins": ["macros"], "overrides": [ // treat .cjs files (e.g. libraries symlinked into node_modules) as commonjs { diff --git a/services/web/config/settings.defaults.js b/services/web/config/settings.defaults.js index 700567ca2f..11a2cbdb70 100644 --- a/services/web/config/settings.defaults.js +++ b/services/web/config/settings.defaults.js @@ -1,33 +1,6 @@ -const fs = require('fs') const Path = require('path') const { merge } = require('@overleaf/settings/merge') -// Automatically detect module imports that are included in this version of the application (SaaS, Server-CE, Server Pro). -// E.g. during a Server-CE build, we will not find imports for proprietary modules. -// -// Restart webpack after adding/removing modules. -const MODULES_PATH = Path.join(__dirname, '../modules') -const entryPointsIde = [] -const entryPointsMain = [] -fs.readdirSync(MODULES_PATH).forEach(module => { - const entryPathIde = Path.join( - MODULES_PATH, - module, - '/frontend/js/ide/index.js' - ) - if (fs.existsSync(entryPathIde)) { - entryPointsIde.push(entryPathIde) - } - const entryPathMain = Path.join( - MODULES_PATH, - module, - '/frontend/js/main/index.js' - ) - if (fs.existsSync(entryPathMain)) { - entryPointsMain.push(entryPathMain) - } -}) - let defaultFeatures, siteUrl // Make time interval config easier. @@ -918,9 +891,6 @@ module.exports = { managedGroupSubscriptionEnrollmentNotification: [], managedGroupEnrollmentInvite: [], ssoCertificateInfo: [], - // See comment at the definition of these variables. - entryPointsIde, - entryPointsMain, }, moduleImportSequence: [ @@ -935,7 +905,7 @@ module.exports = { reportOnly: process.env.CSP_REPORT_ONLY === 'true', reportPercentage: parseFloat(process.env.CSP_REPORT_PERCENTAGE) || 0, reportUri: process.env.CSP_REPORT_URI, - exclude: ['app/views/project/editor'], + exclude: [], }, unsupportedBrowsers: { diff --git a/services/web/cypress/support/shared/commands/index.ts b/services/web/cypress/support/shared/commands/index.ts index f9b3295547..7d12eb1bbb 100644 --- a/services/web/cypress/support/shared/commands/index.ts +++ b/services/web/cypress/support/shared/commands/index.ts @@ -11,6 +11,7 @@ import { interceptFileUpload } from './upload' import { interceptProjectListing } from './project-list' import { interceptLinkedFile } from './linked-file' import { interceptMathJax } from './mathjax' +import { interceptMetadata } from './metadata' // eslint-disable-next-line no-unused-vars,@typescript-eslint/no-namespace declare global { @@ -21,6 +22,7 @@ declare global { interceptAsync: typeof interceptAsync interceptCompile: typeof interceptCompile interceptEvents: typeof interceptEvents + interceptMetadata: typeof interceptMetadata interceptSpelling: typeof interceptSpelling waitForCompile: typeof waitForCompile interceptDeferredCompile: typeof interceptDeferredCompile @@ -35,6 +37,7 @@ declare global { Cypress.Commands.add('interceptAsync', interceptAsync) Cypress.Commands.add('interceptCompile', interceptCompile) Cypress.Commands.add('interceptEvents', interceptEvents) +Cypress.Commands.add('interceptMetadata', interceptMetadata) Cypress.Commands.add('interceptSpelling', interceptSpelling) Cypress.Commands.add('waitForCompile', waitForCompile) Cypress.Commands.add('interceptDeferredCompile', interceptDeferredCompile) diff --git a/services/web/cypress/support/shared/commands/metadata.ts b/services/web/cypress/support/shared/commands/metadata.ts new file mode 100644 index 0000000000..7548c409bc --- /dev/null +++ b/services/web/cypress/support/shared/commands/metadata.ts @@ -0,0 +1,3 @@ +export const interceptMetadata = () => { + cy.intercept('POST', '/project/*/doc/*/metadata', {}) +} diff --git a/services/web/frontend/js/base.js b/services/web/frontend/js/base.js deleted file mode 100644 index c72058819a..0000000000 --- a/services/web/frontend/js/base.js +++ /dev/null @@ -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', - '
{{$select.placeholder}}
' - ) - }, -]) - -export default App diff --git a/services/web/frontend/js/directives/eventTracking.js b/services/web/frontend/js/directives/eventTracking.js deleted file mode 100644 index 51d076292b..0000000000 --- a/services/web/frontend/js/directives/eventTracking.js +++ /dev/null @@ -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) - ) - } - }, - } - }, -]) diff --git a/services/web/frontend/js/features/editor-left-menu/controllers/editor-left-menu-controller.js b/services/web/frontend/js/features/editor-left-menu/controllers/editor-left-menu-controller.js deleted file mode 100644 index 64afa8ca66..0000000000 --- a/services/web/frontend/js/features/editor-left-menu/controllers/editor-left-menu-controller.js +++ /dev/null @@ -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))) diff --git a/services/web/frontend/js/features/editor-left-menu/hooks/use-project-wide-settings-socket-listener.tsx b/services/web/frontend/js/features/editor-left-menu/hooks/use-project-wide-settings-socket-listener.tsx index 8cfba364ae..5b517c53b3 100644 --- a/services/web/frontend/js/features/editor-left-menu/hooks/use-project-wide-settings-socket-listener.tsx +++ b/services/web/frontend/js/features/editor-left-menu/hooks/use-project-wide-settings-socket-listener.tsx @@ -7,8 +7,7 @@ export default function useProjectWideSettingsSocketListener() { const ide = useIdeContext() const [project, setProject] = useScopeValue( - 'project', - true + 'project' ) const setCompiler = useCallback( diff --git a/services/web/frontend/js/features/editor-left-menu/hooks/use-project-wide-settings.tsx b/services/web/frontend/js/features/editor-left-menu/hooks/use-project-wide-settings.tsx index c5960fc21e..3e569f2757 100644 --- a/services/web/frontend/js/features/editor-left-menu/hooks/use-project-wide-settings.tsx +++ b/services/web/frontend/js/features/editor-left-menu/hooks/use-project-wide-settings.tsx @@ -8,7 +8,7 @@ import { debugConsole } from '@/utils/debugging' export default function useProjectWideSettings() { // The value will be undefined on mount - const [project] = useScopeValue('project', true) + const [project] = useScopeValue('project') const saveProjectSettings = useSaveProjectSettings() const setCompiler = useCallback( diff --git a/services/web/frontend/js/features/editor-left-menu/hooks/use-save-project-settings.tsx b/services/web/frontend/js/features/editor-left-menu/hooks/use-save-project-settings.tsx index e4f41271b4..07a20a10fa 100644 --- a/services/web/frontend/js/features/editor-left-menu/hooks/use-save-project-settings.tsx +++ b/services/web/frontend/js/features/editor-left-menu/hooks/use-save-project-settings.tsx @@ -6,7 +6,7 @@ export default function useSaveProjectSettings() { // projectSettings value will be undefined on mount const [projectSettings, setProjectSettings] = useScopeValue< ProjectSettings | undefined - >('project', true) + >('project') const { _id: projectId } = useProjectContext() return async ( diff --git a/services/web/frontend/js/features/editor-navigation-toolbar/controllers/editor-navigation-toolbar-controller.js b/services/web/frontend/js/features/editor-navigation-toolbar/controllers/editor-navigation-toolbar-controller.js deleted file mode 100644 index c538c7efca..0000000000 --- a/services/web/frontend/js/features/editor-navigation-toolbar/controllers/editor-navigation-toolbar-controller.js +++ /dev/null @@ -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', - ]) -) diff --git a/services/web/frontend/js/features/file-tree/controllers/file-tree-controller.js b/services/web/frontend/js/features/file-tree/controllers/file-tree-controller.js deleted file mode 100644 index 31757d09b1..0000000000 --- a/services/web/frontend/js/features/file-tree/controllers/file-tree-controller.js +++ /dev/null @@ -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', - ]) -) diff --git a/services/web/frontend/js/features/file-view/controllers/file-view-controller.js b/services/web/frontend/js/features/file-view/controllers/file-view-controller.js deleted file mode 100644 index 301fab5bd3..0000000000 --- a/services/web/frontend/js/features/file-view/controllers/file-view-controller.js +++ /dev/null @@ -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']) -) diff --git a/services/web/frontend/js/features/history/controllers/history-controller.js b/services/web/frontend/js/features/history/controllers/history-controller.js deleted file mode 100644 index 4441ee5f34..0000000000 --- a/services/web/frontend/js/features/history/controllers/history-controller.js +++ /dev/null @@ -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))) diff --git a/services/web/frontend/js/features/ide-react/context/connection-context.tsx b/services/web/frontend/js/features/ide-react/context/connection-context.tsx index 7ea8ff4d99..5213abf0d1 100644 --- a/services/web/frontend/js/features/ide-react/context/connection-context.tsx +++ b/services/web/frontend/js/features/ide-react/context/connection-context.tsx @@ -27,9 +27,9 @@ type ConnectionContextValue = { disconnect: () => void } -const ConnectionContext = createContext( - undefined -) +export const ConnectionContext = createContext< + ConnectionContextValue | undefined +>(undefined) export const ConnectionProvider: FC = ({ children }) => { const location = useLocation() diff --git a/services/web/frontend/js/features/ide-react/context/editor-manager-context.tsx b/services/web/frontend/js/features/ide-react/context/editor-manager-context.tsx index f90e7c1961..064b8fae06 100644 --- a/services/web/frontend/js/features/ide-react/context/editor-manager-context.tsx +++ b/services/web/frontend/js/features/ide-react/context/editor-manager-context.tsx @@ -87,7 +87,9 @@ export type EditorScopeValue = { error_state: boolean } -const EditorManagerContext = createContext(undefined) +export const EditorManagerContext = createContext( + undefined +) export const EditorManagerProvider: FC = ({ children }) => { const { t } = useTranslation() diff --git a/services/web/frontend/js/features/ide-react/context/ide-react-context.tsx b/services/web/frontend/js/features/ide-react/context/ide-react-context.tsx index 5b3b3843e0..56500bc1c4 100644 --- a/services/web/frontend/js/features/ide-react/context/ide-react-context.tsx +++ b/services/web/frontend/js/features/ide-react/context/ide-react-context.tsx @@ -40,7 +40,7 @@ type IdeReactContextValue = { projectJoined: boolean } -const IdeReactContext = createContext( +export const IdeReactContext = createContext( undefined ) @@ -63,7 +63,7 @@ function populatePdfScope(store: ReactScopeValueStore) { store.allowNonExistentPath('pdf', true) } -function createReactScopeValueStore(projectId: string) { +export function createReactScopeValueStore(projectId: string) { const scopeStore = new ReactScopeValueStore() // Populate the scope value store with default values that will be used by diff --git a/services/web/frontend/js/features/ide-react/context/metadata-context.tsx b/services/web/frontend/js/features/ide-react/context/metadata-context.tsx index 6a5303ea0d..7118f5a1f1 100644 --- a/services/web/frontend/js/features/ide-react/context/metadata-context.tsx +++ b/services/web/frontend/js/features/ide-react/context/metadata-context.tsx @@ -15,7 +15,6 @@ import _ from 'lodash' import { getJSON, postJSON } from '@/infrastructure/fetch-json' import { useOnlineUsersContext } from '@/features/ide-react/context/online-users-context' import { useEditorContext } from '@/shared/context/editor-context' -import { useIdeContext } from '@/shared/context/ide-context' import useSocketListener from '@/features/ide-react/hooks/use-socket-listener' import useEventListener from '@/shared/hooks/use-event-listener' import { useModalsContext } from '@/features/ide-react/context/modals-context' @@ -42,13 +41,12 @@ type MetadataContextValue = { type DocMetadataResponse = { docId: string; meta: DocumentMetadata } -const MetadataContext = createContext( +export const MetadataContext = createContext( undefined ) export const MetadataProvider: FC = ({ children }) => { const { t } = useTranslation() - const ide = useIdeContext() const { eventEmitter, projectId } = useIdeReactContext() const { socket } = useConnectionContext() const { onlineUsersCount } = useOnlineUsersContext() @@ -225,10 +223,6 @@ export const MetadataProvider: FC = ({ children }) => { [documents, getAllLabels, getAllPackages] ) - // Expose metadataManager via ide object because useCodeMirrorScope relies on - // it, for now - ide.metadataManager = value - return ( {children} diff --git a/services/web/frontend/js/features/ide-react/context/permissions-context.tsx b/services/web/frontend/js/features/ide-react/context/permissions-context.tsx index 65eaf65953..aa6fb1b432 100644 --- a/services/web/frontend/js/features/ide-react/context/permissions-context.tsx +++ b/services/web/frontend/js/features/ide-react/context/permissions-context.tsx @@ -9,7 +9,9 @@ import { import useScopeValue from '@/shared/hooks/use-scope-value' import { DeepReadonly } from '../../../../../types/utils' -const PermissionsContext = createContext(undefined) +export const PermissionsContext = createContext( + undefined +) const permissionsMap: DeepReadonly> = { readOnly: { diff --git a/services/web/frontend/js/features/ide-react/context/react-context-root.tsx b/services/web/frontend/js/features/ide-react/context/react-context-root.tsx index 200d3429d6..523072f8ec 100644 --- a/services/web/frontend/js/features/ide-react/context/react-context-root.tsx +++ b/services/web/frontend/js/features/ide-react/context/react-context-root.tsx @@ -40,10 +40,10 @@ export const ReactContextRoot: FC = ({ children }) => { - - - - + + + + @@ -53,10 +53,10 @@ export const ReactContextRoot: FC = ({ children }) => { - - - - + + + + diff --git a/services/web/frontend/js/features/ide-react/context/review-panel/hooks/use-review-panel-state.ts b/services/web/frontend/js/features/ide-react/context/review-panel/hooks/use-review-panel-state.ts index 6de9411847..3fdfb8c624 100644 --- a/services/web/frontend/js/features/ide-react/context/review-panel/hooks/use-review-panel-state.ts +++ b/services/web/frontend/js/features/ide-react/context/review-panel/hooks/use-review-panel-state.ts @@ -170,10 +170,7 @@ function useReviewPanelState(): ReviewPanel.ReviewPanelState { ReviewPanel.Value<'commentThreads'> >({}) const [entries, setEntries] = useState>({}) - const [users, setUsers] = useScopeValue>( - 'users', - true - ) + const [users, setUsers] = useScopeValue>('users') const [resolvedComments, setResolvedComments] = useState< ReviewPanel.Value<'resolvedComments'> >({}) @@ -532,7 +529,6 @@ function useReviewPanelState(): ReviewPanel.ReviewPanelState { if (!user) { return 'anonymous' } - // FIXME: check this if (project.owner._id === user.id) { return 'member' } diff --git a/services/web/frontend/js/features/ide-react/scope-event-emitter/angular-scope-event-emitter.ts b/services/web/frontend/js/features/ide-react/scope-event-emitter/angular-scope-event-emitter.ts deleted file mode 100644 index 75173949bf..0000000000 --- a/services/web/frontend/js/features/ide-react/scope-event-emitter/angular-scope-event-emitter.ts +++ /dev/null @@ -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) - } -} diff --git a/services/web/frontend/js/features/ide-react/scope-value-store/angular-scope-value-store.ts b/services/web/frontend/js/features/ide-react/scope-value-store/angular-scope-value-store.ts deleted file mode 100644 index 237be90e58..0000000000 --- a/services/web/frontend/js/features/ide-react/scope-value-store/angular-scope-value-store.ts +++ /dev/null @@ -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( - path: string, - callback: (newValue: T) => void, - deep: boolean - ): () => void { - return this.$scope.$watch( - path, - (newValue: T) => callback(deep ? _.cloneDeep(newValue) : newValue), - deep - ) - } -} diff --git a/services/web/frontend/js/features/outline/controllers/outline-controller.js b/services/web/frontend/js/features/outline/controllers/outline-controller.js deleted file mode 100644 index dc37385af8..0000000000 --- a/services/web/frontend/js/features/outline/controllers/outline-controller.js +++ /dev/null @@ -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)) -) diff --git a/services/web/frontend/js/features/pdf-preview/components/pdf-preview-detached-root.jsx b/services/web/frontend/js/features/pdf-preview/components/pdf-preview-detached-root.jsx index cc64f58e6a..e3dc967fb2 100644 --- a/services/web/frontend/js/features/pdf-preview/components/pdf-preview-detached-root.jsx +++ b/services/web/frontend/js/features/pdf-preview/components/pdf-preview-detached-root.jsx @@ -1,7 +1,7 @@ import ReactDOM from 'react-dom' import PdfPreview from './pdf-preview' -import { ContextRoot } from '../../../shared/context/root-context' import useWaitForI18n from '../../../shared/hooks/use-wait-for-i18n' +import { ReactContextRoot } from '@/features/ide-react/context/react-context-root' function PdfPreviewDetachedRoot() { const { isReady } = useWaitForI18n() @@ -11,9 +11,9 @@ function PdfPreviewDetachedRoot() { } return ( - + - + ) } diff --git a/services/web/frontend/js/features/pdf-preview/controllers/pdf-preview-controller.js b/services/web/frontend/js/features/pdf-preview/controllers/pdf-preview-controller.js deleted file mode 100644 index e3d2fae4b1..0000000000 --- a/services/web/frontend/js/features/pdf-preview/controllers/pdf-preview-controller.js +++ /dev/null @@ -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), []) -) diff --git a/services/web/frontend/js/features/share-project-modal/controllers/react-share-project-modal-controller.js b/services/web/frontend/js/features/share-project-modal/controllers/react-share-project-modal-controller.js deleted file mode 100644 index cda460db1b..0000000000 --- a/services/web/frontend/js/features/share-project-modal/controllers/react-share-project-modal-controller.js +++ /dev/null @@ -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) - }) - } - }) - }, -]) diff --git a/services/web/frontend/js/features/source-editor/components/review-panel/editor-widgets/editor-widgets.tsx b/services/web/frontend/js/features/source-editor/components/review-panel/editor-widgets/editor-widgets.tsx index 025bb9d63e..e3837a8862 100644 --- a/services/web/frontend/js/features/source-editor/components/review-panel/editor-widgets/editor-widgets.tsx +++ b/services/web/frontend/js/features/source-editor/components/review-panel/editor-widgets/editor-widgets.tsx @@ -8,12 +8,10 @@ import { useReviewPanelUpdaterFnsContext, useReviewPanelValueContext, } from '../../../context/review-panel/review-panel-context' -import { useIdeContext } from '@/shared/context/ide-context' import { useEditorContext } from '@/shared/context/editor-context' import { useCodeMirrorViewContext } from '../../codemirror-editor' import Modal, { useBulkActionsModal } from '../entries/bulk-actions-entry/modal' import getMeta from '../../../../../utils/meta' -import useScopeValue from '../../../../../shared/hooks/use-scope-value' import useScopeEventListener from '@/shared/hooks/use-scope-event-listener' import { memo, useCallback } from 'react' import { useLayoutContext } from '@/shared/context/layout-context' @@ -29,14 +27,8 @@ function EditorWidgets() { handleShowBulkRejectDialog, handleConfirmDialog, } = useBulkActionsModal() - const { setIsAddingComment, handleSetSubview } = - useReviewPanelUpdaterFnsContext() - const { isReactIde } = useIdeContext() + const { setIsAddingComment } = useReviewPanelUpdaterFnsContext() const { toggleReviewPanel } = useReviewPanelUpdaterFnsContext() - const [addNewComment] = - useScopeValue<(e: React.MouseEvent) => void>( - 'addNewComment' - ) const view = useCodeMirrorViewContext() const { reviewPanelOpen } = useLayoutContext() const { isRestrictedTokenMember } = useEditorContext() @@ -55,20 +47,9 @@ function EditorWidgets() { openDocId && openDocId in entries ? entries[openDocId] : undefined const handleAddNewCommentClick = (e: React.MouseEvent) => { - if (isReactIde) { - e.preventDefault() - setIsAddingComment(true) - toggleReviewPanel() - return - } - - addNewComment(e) - setTimeout(() => { - // Re-render the comment box in order to add autofocus every time - handleSetSubview('cur_file') - setIsAddingComment(false) - setIsAddingComment(true) - }, 0) + e.preventDefault() + setIsAddingComment(true) + toggleReviewPanel() } useScopeEventListener( diff --git a/services/web/frontend/js/features/source-editor/components/review-panel/review-panel.tsx b/services/web/frontend/js/features/source-editor/components/review-panel/review-panel.tsx index dc92cf5f93..72d94a51be 100644 --- a/services/web/frontend/js/features/source-editor/components/review-panel/review-panel.tsx +++ b/services/web/frontend/js/features/source-editor/components/review-panel/review-panel.tsx @@ -8,26 +8,14 @@ import { isCurrentFileView } from '../../utils/sub-view' import { useLayoutContext } from '@/shared/context/layout-context' import classnames from 'classnames' import { lazy, memo } from 'react' -import getMeta from '@/utils/meta' import { SubView } from '../../../../../../types/review-panel/review-panel' -const isReactIde: boolean = getMeta('ol-idePageReact') - type ReviewPanelViewProps = { parentDomNode: Element } function ReviewPanelView({ parentDomNode }: ReviewPanelViewProps) { - const { subView } = useReviewPanelValueContext() - - return ReactDOM.createPortal( - isReactIde ? ( - - ) : ( - - ), - parentDomNode - ) + return ReactDOM.createPortal(, parentDomNode) } const ReviewPanelContainer = memo(() => { @@ -65,10 +53,9 @@ const ReviewPanelContent = memo<{ subView: SubView }>(({ subView }) => ( )) ReviewPanelContent.displayName = 'ReviewPanelContent' -const ReviewPanelProvider = lazy(() => - isReactIde - ? import('@/features/ide-react/context/review-panel/review-panel-provider') - : import('../../context/review-panel/review-panel-provider') +const ReviewPanelProvider = lazy( + () => + import('@/features/ide-react/context/review-panel/review-panel-provider') ) function ReviewPanel() { diff --git a/services/web/frontend/js/features/source-editor/context/review-panel/hooks/use-angular-review-panel-state.ts b/services/web/frontend/js/features/source-editor/context/review-panel/hooks/use-angular-review-panel-state.ts deleted file mode 100644 index ebbc1469eb..0000000000 --- a/services/web/frontend/js/features/source-editor/context/review-panel/hooks/use-angular-review-panel-state.ts +++ /dev/null @@ -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.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.commentThreads', - true - ) - const [entries] = useScopeValue>( - 'reviewPanel.entries', - true - ) - const [loadingThreads] = - useScopeValue>('loadingThreads') - - const [permissions] = - useScopeValue>('permissions') - const [users] = useScopeValue>('users', true) - const [resolvedComments] = useScopeValue< - ReviewPanel.Value<'resolvedComments'> - >('reviewPanel.resolvedComments', true) - - const [wantTrackChanges] = useScopeValue< - ReviewPanel.Value<'wantTrackChanges'> - >('editor.wantTrackChanges') - const [openDocId] = - useScopeValue>('editor.open_doc_id') - const [shouldCollapse, setShouldCollapse] = useScopeValue< - ReviewPanel.Value<'shouldCollapse'> - >('reviewPanel.fullTCStateCollapsed') - const [lineHeight] = useScopeValue( - '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>('resolveComment') - const [submitNewComment] = - useScopeValue>('submitNewComment') - const [deleteComment] = - useScopeValue>('deleteComment') - const [gotoEntry] = - useScopeValue>('gotoEntry') - const [saveEdit] = - useScopeValue>('saveEdit') - const [submitReplyAngular] = - useScopeValue< - (entry: { thread_id: ThreadId; replyContent: string }) => void - >('submitReply') - - const [formattedProjectMembers] = useScopeValue< - ReviewPanel.Value<'formattedProjectMembers'> - >('reviewPanel.formattedProjectMembers') - - const [toggleReviewPanel] = - useScopeValue>( - 'toggleReviewPanel' - ) - const [unresolveComment] = - useScopeValue>('unresolveComment') - - const [deleteThreadAngular] = - useScopeValue< - ( - _: unknown, - ...args: [...Parameters>] - ) => ReturnType> - >('deleteThread') - const deleteThread = useCallback( - (docId: DocId, threadId: ThreadId) => { - deleteThreadAngular(undefined, docId, threadId) - }, - [deleteThreadAngular] - ) - - const [refreshResolvedCommentsDropdown] = useScopeValue< - ReviewPanel.UpdaterFn<'refreshResolvedCommentsDropdown'> - >('refreshResolvedCommentsDropdown') - const [acceptChanges] = - useScopeValue>('acceptChanges') - const [rejectChanges] = - useScopeValue>('rejectChanges') - const [bulkAcceptActions] = - useScopeValue>( - 'bulkAcceptActions' - ) - const [bulkRejectActions] = - useScopeValue>( - '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( - () => ({ - 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( - () => ({ - 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 diff --git a/services/web/frontend/js/features/source-editor/context/review-panel/review-panel-provider.tsx b/services/web/frontend/js/features/source-editor/context/review-panel/review-panel-provider.tsx deleted file mode 100644 index 7003dd2fef..0000000000 --- a/services/web/frontend/js/features/source-editor/context/review-panel/review-panel-provider.tsx +++ /dev/null @@ -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 ( - - - {children} - - - ) -} - -export default ReviewPanelProvider diff --git a/services/web/frontend/js/features/source-editor/controllers/grammarly-advert-controller.js b/services/web/frontend/js/features/source-editor/controllers/grammarly-advert-controller.js deleted file mode 100644 index 9e27cf2ec0..0000000000 --- a/services/web/frontend/js/features/source-editor/controllers/grammarly-advert-controller.js +++ /dev/null @@ -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)) -) diff --git a/services/web/frontend/js/features/source-editor/controllers/source-editor-controller.ts b/services/web/frontend/js/features/source-editor/controllers/source-editor-controller.ts deleted file mode 100644 index 67e9a5debf..0000000000 --- a/services/web/frontend/js/features/source-editor/controllers/source-editor-controller.ts +++ /dev/null @@ -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), [])) diff --git a/services/web/frontend/js/features/source-editor/hooks/use-codemirror-scope.ts b/services/web/frontend/js/features/source-editor/hooks/use-codemirror-scope.ts index 5eea17b8f4..277659a1f3 100644 --- a/services/web/frontend/js/features/source-editor/hooks/use-codemirror-scope.ts +++ b/services/web/frontend/js/features/source-editor/hooks/use-codemirror-scope.ts @@ -26,7 +26,6 @@ import { setMetadata, setSyntaxValidation, } from '../extensions/language' -import { useIdeContext } from '../../../shared/context/ide-context' import { restoreScrollPosition } from '../extensions/scroll-position' import { setEditable } from '../extensions/editable' import { useFileTreeData } from '../../../shared/context/file-tree-data-context' @@ -59,10 +58,9 @@ import grammarlyExtensionPresent from '@/shared/utils/grammarly' import { DocumentContainer } from '@/features/ide-react/editor/document-container' import { useLayoutContext } from '@/shared/context/layout-context' import { debugConsole } from '@/utils/debugging' +import { useMetadataContext } from '@/features/ide-react/context/metadata-context' function useCodeMirrorScope(view: EditorView) { - const ide = useIdeContext() - const { fileTreeData } = useFileTreeData() const [permissions] = useScopeValue<{ write: boolean }>('permissions') @@ -74,6 +72,8 @@ function useCodeMirrorScope(view: EditorView) { const { reviewPanelOpen, miniReviewPanelVisible } = useLayoutContext() + const { metadata } = useMetadataContext() + const [loadingThreads] = useScopeValue('loadingThreads') const [currentDoc] = useScopeValue( @@ -211,22 +211,16 @@ function useCodeMirrorScope(view: EditorView) { // set the project metadata, mostly for use in autocomplete // TODO: read this data from the scope? const metadataRef = useRef({ - documents: ide.metadataManager.metadata.state.documents, + documents: metadata.state.documents, references: references.keys, fileTreeData, }) // listen to project metadata (docs + packages) updates useEffect(() => { - const listener = (event: Event) => { - metadataRef.current.documents = ( - event as CustomEvent> - ).detail - view.dispatch(setMetadata(metadataRef.current)) - } - window.addEventListener('project:metadata', listener) - return () => window.removeEventListener('project:metadata', listener) - }, [view]) + metadataRef.current.documents = metadata.state.documents + view.dispatch(setMetadata(metadataRef.current)) + }, [view, metadata.state.documents]) // listen to project reference keys updates useEffect(() => { diff --git a/services/web/frontend/js/features/source-editor/ide/index.js b/services/web/frontend/js/features/source-editor/ide/index.js deleted file mode 100644 index 4e62de4c8d..0000000000 --- a/services/web/frontend/js/features/source-editor/ide/index.js +++ /dev/null @@ -1 +0,0 @@ -import '../controllers/source-editor-controller' diff --git a/services/web/frontend/js/ide.js b/services/web/frontend/js/ide.js deleted file mode 100644 index 0114740368..0000000000 --- a/services/web/frontend/js/ide.js +++ /dev/null @@ -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 -} diff --git a/services/web/frontend/js/ide/LoadingManager.js b/services/web/frontend/js/ide/LoadingManager.js deleted file mode 100644 index f63c1eeddb..0000000000 --- a/services/web/frontend/js/ide/LoadingManager.js +++ /dev/null @@ -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 diff --git a/services/web/frontend/js/ide/binary-files/BinaryFilesManager.js b/services/web/frontend/js/ide/binary-files/BinaryFilesManager.js deleted file mode 100644 index 0c0afe627b..0000000000 --- a/services/web/frontend/js/ide/binary-files/BinaryFilesManager.js +++ /dev/null @@ -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 - ) - } -} diff --git a/services/web/frontend/js/ide/chat/index.js b/services/web/frontend/js/ide/chat/index.js deleted file mode 100644 index c8671f72eb..0000000000 --- a/services/web/frontend/js/ide/chat/index.js +++ /dev/null @@ -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))) diff --git a/services/web/frontend/js/ide/cobranding/CobrandingDataService.js b/services/web/frontend/js/ide/cobranding/CobrandingDataService.js deleted file mode 100644 index 50d5dab70f..0000000000 --- a/services/web/frontend/js/ide/cobranding/CobrandingDataService.js +++ /dev/null @@ -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, - } -}) diff --git a/services/web/frontend/js/ide/connection/ConnectionManager.js b/services/web/frontend/js/ide/connection/ConnectionManager.js deleted file mode 100644 index e22c7190d9..0000000000 --- a/services/web/frontend/js/ide/connection/ConnectionManager.js +++ /dev/null @@ -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 connection problems guide 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 connection problems guide 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 -})() diff --git a/services/web/frontend/js/ide/connection/EditorWatchdogManager.js b/services/web/frontend/js/ide/connection/EditorWatchdogManager.js deleted file mode 100644 index 206c84167d..0000000000 --- a/services/web/frontend/js/ide/connection/EditorWatchdogManager.js +++ /dev/null @@ -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) - } -} diff --git a/services/web/frontend/js/ide/directives/SafePath.js b/services/web/frontend/js/ide/directives/SafePath.js deleted file mode 100644 index 4d26383687..0000000000 --- a/services/web/frontend/js/ide/directives/SafePath.js +++ /dev/null @@ -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 - }, -} diff --git a/services/web/frontend/js/ide/directives/layout.js b/services/web/frontend/js/ide/directives/layout.js deleted file mode 100644 index 5db9928021..0000000000 --- a/services/web/frontend/js/ide/directives/layout.js +++ /dev/null @@ -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(`\ -\ -`)(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) - }, - } - }, - } - }, -]) diff --git a/services/web/frontend/js/ide/directives/verticalResizablePanes.js b/services/web/frontend/js/ide/directives/verticalResizablePanes.js deleted file mode 100644 index b6fe36867d..0000000000 --- a/services/web/frontend/js/ide/directives/verticalResizablePanes.js +++ /dev/null @@ -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') - } - }, - } - }, -]) diff --git a/services/web/frontend/js/ide/editor/Document.js b/services/web/frontend/js/ide/editor/Document.js deleted file mode 100644 index 1d8e4a5044..0000000000 --- a/services/web/frontend/js/ide/editor/Document.js +++ /dev/null @@ -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 -} diff --git a/services/web/frontend/js/ide/editor/EditorManager.js b/services/web/frontend/js/ide/editor/EditorManager.js deleted file mode 100644 index 2f3abb7b24..0000000000 --- a/services/web/frontend/js/ide/editor/EditorManager.js +++ /dev/null @@ -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.
Please see this help guide for more information", - 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 -})() diff --git a/services/web/frontend/js/ide/editor/EditorShareJsCodec.js b/services/web/frontend/js/ide/editor/EditorShareJsCodec.js deleted file mode 100644 index dd2885fa60..0000000000 --- a/services/web/frontend/js/ide/editor/EditorShareJsCodec.js +++ /dev/null @@ -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 } - }, -} diff --git a/services/web/frontend/js/ide/editor/ShareJsDoc.js b/services/web/frontend/js/ide/editor/ShareJsDoc.js deleted file mode 100644 index 9e47980dc4..0000000000 --- a/services/web/frontend/js/ide/editor/ShareJsDoc.js +++ /dev/null @@ -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 -} diff --git a/services/web/frontend/js/ide/editor/controllers/CompileButton.js b/services/web/frontend/js/ide/editor/controllers/CompileButton.js deleted file mode 100644 index abaa460ad5..0000000000 --- a/services/web/frontend/js/ide/editor/controllers/CompileButton.js +++ /dev/null @@ -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)) -) diff --git a/services/web/frontend/js/ide/editor/controllers/SavingNotificationController.js b/services/web/frontend/js/ide/editor/controllers/SavingNotificationController.js deleted file mode 100644 index c1b03b4224..0000000000 --- a/services/web/frontend/js/ide/editor/controllers/SavingNotificationController.js +++ /dev/null @@ -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.' - } - }) - }, -]) diff --git a/services/web/frontend/js/ide/editor/controllers/SwitchToPDFButton.js b/services/web/frontend/js/ide/editor/controllers/SwitchToPDFButton.js deleted file mode 100644 index c4b4cd0e18..0000000000 --- a/services/web/frontend/js/ide/editor/controllers/SwitchToPDFButton.js +++ /dev/null @@ -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)) -) diff --git a/services/web/frontend/js/ide/editor/directives/formattingButtons.js b/services/web/frontend/js/ide/editor/directives/formattingButtons.js deleted file mode 100644 index 71bbc32991..0000000000 --- a/services/web/frontend/js/ide/editor/directives/formattingButtons.js +++ /dev/null @@ -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', - } -}) diff --git a/services/web/frontend/js/ide/editor/directives/toggleSwitch.js b/services/web/frontend/js/ide/editor/directives/toggleSwitch.js deleted file mode 100644 index ee4335d74b..0000000000 --- a/services/web/frontend/js/ide/editor/directives/toggleSwitch.js +++ /dev/null @@ -1,38 +0,0 @@ -import App from '../../../base' - -export default App.directive('toggleSwitch', function () { - return { - restrict: 'E', - scope: { - description: '@', - labelFalse: '@', - labelTrue: '@', - ngModel: '=', - }, - template: `\ -
- {{description}} - - - - - - -
\ -`, - } -}) diff --git a/services/web/frontend/js/ide/file-tree/FileTreeManager.js b/services/web/frontend/js/ide/file-tree/FileTreeManager.js deleted file mode 100644 index 16ff846d78..0000000000 --- a/services/web/frontend/js/ide/file-tree/FileTreeManager.js +++ /dev/null @@ -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 -} diff --git a/services/web/frontend/js/ide/file-tree/controllers/FileTreeController.js b/services/web/frontend/js/ide/file-tree/controllers/FileTreeController.js deleted file mode 100644 index 51c588d2c2..0000000000 --- a/services/web/frontend/js/ide/file-tree/controllers/FileTreeController.js +++ /dev/null @@ -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' - } - }, -]) diff --git a/services/web/frontend/js/ide/file-view/index.js b/services/web/frontend/js/ide/file-view/index.js deleted file mode 100644 index b9502cf8eb..0000000000 --- a/services/web/frontend/js/ide/file-view/index.js +++ /dev/null @@ -1 +0,0 @@ -import '../../features/file-view/controllers/file-view-controller' diff --git a/services/web/frontend/js/ide/metadata/MetadataManager.js b/services/web/frontend/js/ide/metadata/MetadataManager.js deleted file mode 100644 index 3d5c01a268..0000000000 --- a/services/web/frontend/js/ide/metadata/MetadataManager.js +++ /dev/null @@ -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() - } -} diff --git a/services/web/frontend/js/ide/metadata/services/metadata.js b/services/web/frontend/js/ide/metadata/services/metadata.js deleted file mode 100644 index 4cf05fa007..0000000000 --- a/services/web/frontend/js/ide/metadata/services/metadata.js +++ /dev/null @@ -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 - }, -]) diff --git a/services/web/frontend/js/ide/online-users/OnlineUsersManager.js b/services/web/frontend/js/ide/online-users/OnlineUsersManager.js deleted file mode 100644 index 250da63bc0..0000000000 --- a/services/web/frontend/js/ide/online-users/OnlineUsersManager.js +++ /dev/null @@ -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 -})() diff --git a/services/web/frontend/js/ide/permissions/PermissionsManager.js b/services/web/frontend/js/ide/permissions/PermissionsManager.js deleted file mode 100644 index 1cd88600c9..0000000000 --- a/services/web/frontend/js/ide/permissions/PermissionsManager.js +++ /dev/null @@ -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) - } - }) - } -} diff --git a/services/web/frontend/js/ide/references/ReferencesManager.js b/services/web/frontend/js/ide/references/ReferencesManager.js deleted file mode 100644 index 3ad2cde92a..0000000000 --- a/services/web/frontend/js/ide/references/ReferencesManager.js +++ /dev/null @@ -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 -} diff --git a/services/web/frontend/js/ide/review-panel/ReviewPanelManager.js b/services/web/frontend/js/ide/review-panel/ReviewPanelManager.js deleted file mode 100644 index 268bb74243..0000000000 --- a/services/web/frontend/js/ide/review-panel/ReviewPanelManager.js +++ /dev/null @@ -1 +0,0 @@ -import './controllers/ReviewPanelController' diff --git a/services/web/frontend/js/ide/review-panel/controllers/ReviewPanelController.js b/services/web/frontend/js/ide/review-panel/controllers/ReviewPanelController.js deleted file mode 100644 index dcc4853e9b..0000000000 --- a/services/web/frontend/js/ide/review-panel/controllers/ReviewPanelController.js +++ /dev/null @@ -1,1349 +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: - * DS101: Remove unnecessary use of Array.from - * 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 RangesTracker from '@overleaf/ranges-tracker' -import App from '../../../base' -import EventEmitter from '../../../utils/EventEmitter' -import ColorManager from '../../colors/ColorManager' -import getMeta from '../../../utils/meta' - -export default App.controller('ReviewPanelController', [ - '$scope', - '$element', - 'ide', - '$timeout', - '$http', - '$modal', - 'eventTracking', - 'localStorage', - function ( - $scope, - $element, - ide, - $timeout, - $http, - $modal, - eventTracking, - localStorage - ) { - let UserTCSyncState - const $reviewPanelEl = $element.find('#review-panel') - - const UserTypes = { - MEMBER: 'member', // Invited, listed in project.members - GUEST: 'guest', // Not invited, but logged in so has a user_id - ANONYMOUS: 'anonymous', // No user_id - } - - const currentUserType = function () { - if ((ide.$scope.user != null ? ide.$scope.user.id : undefined) == null) { - return UserTypes.ANONYMOUS - } else { - const user_id = ide.$scope.user.id - const { project } = ide.$scope - if ( - (project.owner != null ? project.owner.id : undefined) === user_id - ) { - return UserTypes.MEMBER - } - for (const member of Array.from(project.members)) { - if (member._id === user_id) { - return UserTypes.MEMBER - } - } - return UserTypes.GUEST - } - } - - $scope.SubViews = { - CUR_FILE: 'cur_file', - OVERVIEW: 'overview', - } - - $scope.UserTCSyncState = UserTCSyncState = { - SYNCED: 'synced', - PENDING: 'pending', - } - - ide.$scope.reviewPanel = { - trackChangesState: {}, - trackChangesOnForEveryone: false, - trackChangesOnForGuests: false, - trackChangesForGuestsAvailable: false, - entries: {}, - resolvedComments: {}, - hasEntries: false, - subView: $scope.SubViews.CUR_FILE, - openSubView: $scope.SubViews.CUR_FILE, - overview: { - loading: false, - docsCollapsedState: - JSON.parse( - localStorage(`docs_collapsed_state:${$scope.project_id}`) - ) || {}, - }, - dropdown: { - loading: false, - }, - commentThreads: {}, - resolvedThreadIds: {}, - layoutToLeft: false, - rendererData: {}, - formattedProjectMembers: {}, - fullTCStateCollapsed: true, - // All selected changes. If a aggregated change (insertion + deletion) is selection, the two ids - // will be present. The length of this array will differ from the count below (see explanation). - selectedEntryIds: [], - // A count of user-facing selected changes. An aggregated change (insertion + deletion) will count - // as only one. - nVisibleSelectedChanges: 0, - entryHover: false, - } - - ide.$scope.loadingThreads = true - - window.addEventListener('beforeunload', function () { - const collapsedStates = {} - for (const doc in ide.$scope.reviewPanel.overview.docsCollapsedState) { - const state = ide.$scope.reviewPanel.overview.docsCollapsedState[doc] - if (state) { - collapsedStates[doc] = state - } - } - const valToStore = - Object.keys(collapsedStates).length > 0 - ? JSON.stringify(collapsedStates) - : null - return localStorage( - `docs_collapsed_state:${$scope.project_id}`, - valToStore - ) - }) - - $scope.$on('layout:pdf:linked', (event, state) => - ide.$scope.$broadcast('review-panel:layout') - ) - - $scope.$on('layout:pdf:resize', (event, state) => { - ide.$scope.reviewPanel.layoutToLeft = - state.east?.size < 220 || state.east?.initClosed - ide.$scope.$broadcast('review-panel:layout', false) - }) - - $scope.$on('review-panel:sizes', (e, sizes) => { - $scope.$broadcast('editor:set-scroll-size', sizes) - dispatchReviewPanelEvent('sizes', sizes) - }) - - $scope.$watch('project.features.trackChangesVisible', function (visible) { - if (visible == null) { - return - } - if (!visible) { - return ($scope.ui.reviewPanelOpen = false) - } - }) - - $scope.$watch('project.members', function (members) { - ide.$scope.reviewPanel.formattedProjectMembers = {} - if (($scope.project != null ? $scope.project.owner : undefined) != null) { - ide.$scope.reviewPanel.formattedProjectMembers[ - $scope.project.owner._id - ] = formatUser($scope.project.owner) - } - if ( - ($scope.project != null ? $scope.project.members : undefined) != null - ) { - return (() => { - const result = [] - for (const member of Array.from(members)) { - if (member.privileges === 'readAndWrite') { - if ( - ide.$scope.reviewPanel.trackChangesState[member._id] == null - ) { - // An added member will have track changes enabled if track changes is on for everyone - _setUserTCState( - member._id, - ide.$scope.reviewPanel.trackChangesOnForEveryone, - true - ) - } - result.push( - (ide.$scope.reviewPanel.formattedProjectMembers[member._id] = - formatUser(member)) - ) - } else { - result.push(undefined) - } - } - return result - })() - } - }) - - $scope.commentState = { - adding: false, - content: '', - } - - ide.$scope.users = $scope.users = {} - - ide.$scope.reviewPanelEventsBridge = new EventEmitter() - - ide.socket.on('new-comment', function (thread_id, comment) { - const thread = getThread(thread_id) - delete thread.submitting - thread.messages.push(formatComment(comment)) - $scope.$apply() - return $timeout(() => ide.$scope.$broadcast('review-panel:layout')) - }) - - ide.socket.on('accept-changes', function (doc_id, change_ids) { - if (doc_id !== $scope.editor.open_doc_id) { - getChangeTracker(doc_id).removeChangeIds(change_ids) - } else { - $scope.$broadcast('changes:accept', change_ids) - dispatchReviewPanelEvent('changes:accept', change_ids) - } - updateEntries(doc_id) - return $scope.$apply(function () {}) - }) - - ide.socket.on('resolve-thread', (thread_id, user) => - _onCommentResolved(thread_id, user) - ) - - ide.socket.on('reopen-thread', thread_id => _onCommentReopened(thread_id)) - - ide.socket.on('delete-thread', function (thread_id) { - _onThreadDeleted(thread_id) - return $scope.$apply(function () {}) - }) - - ide.socket.on('edit-message', function (thread_id, message_id, content) { - _onCommentEdited(thread_id, message_id, content) - return $scope.$apply(function () {}) - }) - - ide.socket.on('delete-message', function (thread_id, message_id) { - _onCommentDeleted(thread_id, message_id) - return $scope.$apply(function () {}) - }) - - const rangesTrackers = {} - - const getDocEntries = function (doc_id) { - if (ide.$scope.reviewPanel.entries[doc_id] == null) { - ide.$scope.reviewPanel.entries[doc_id] = {} - } - return ide.$scope.reviewPanel.entries[doc_id] - } - - const getDocResolvedComments = function (doc_id) { - if (ide.$scope.reviewPanel.resolvedComments[doc_id] == null) { - ide.$scope.reviewPanel.resolvedComments[doc_id] = {} - } - return ide.$scope.reviewPanel.resolvedComments[doc_id] - } - - function getThread(thread_id) { - if (ide.$scope.reviewPanel.commentThreads[thread_id] == null) { - ide.$scope.reviewPanel.commentThreads[thread_id] = { messages: [] } - } - return ide.$scope.reviewPanel.commentThreads[thread_id] - } - - function getChangeTracker(doc_id) { - if (rangesTrackers[doc_id] == null) { - rangesTrackers[doc_id] = new RangesTracker() - rangesTrackers[doc_id].resolvedThreadIds = - ide.$scope.reviewPanel.resolvedThreadIds - } - return rangesTrackers[doc_id] - } - - $scope.$watch( - '!ui.reviewPanelOpen && reviewPanel.hasEntries', - function (open, prevVal) { - if (open == null) { - return - } - $scope.ui.miniReviewPanelVisible = open - if (open !== prevVal) { - return $timeout(() => $scope.$broadcast('review-panel:toggle')) - } - } - ) - - $scope.$watch('ui.reviewPanelOpen', function (open) { - if (open == null) { - return - } - if (!open) { - // Always show current file when not open, but save current state - ide.$scope.reviewPanel.openSubView = ide.$scope.reviewPanel.subView - ide.$scope.reviewPanel.subView = $scope.SubViews.CUR_FILE - } else { - // Reset back to what we had when previously open - ide.$scope.reviewPanel.subView = ide.$scope.reviewPanel.openSubView - } - return $timeout(function () { - $scope.$broadcast('review-panel:toggle') - return ide.$scope.$broadcast('review-panel:layout', false) - }) - }) - - $scope.$watch('reviewPanel.subView', function (view, oldView) { - if (view == null) { - return - } - if (view === $scope.SubViews.OVERVIEW) { - return refreshOverviewPanel() - } else if (oldView === $scope.SubViews.OVERVIEW) { - dispatchReviewPanelEvent('overview-closed', view) - } - }) - - $scope.$watch('editor.sharejs_doc', function (doc, old_doc) { - if (doc == null) { - return - } - // The open doc range tracker is kept up to date in real-time so - // replace any outdated info with this - rangesTrackers[doc.doc_id] = doc.ranges - rangesTrackers[doc.doc_id].resolvedThreadIds = - ide.$scope.reviewPanel.resolvedThreadIds - ide.$scope.reviewPanel.rangesTracker = rangesTrackers[doc.doc_id] - if (old_doc != null) { - old_doc.off('flipped_pending_to_inflight') - } - doc.on('flipped_pending_to_inflight', () => regenerateTrackChangesId(doc)) - return regenerateTrackChangesId(doc) - }) - - $scope.$watch( - function () { - const entries = - ide.$scope.reviewPanel.entries[$scope.editor.open_doc_id] || {} - const permEntries = {} - for (const entry in entries) { - const entryData = entries[entry] - if (!['add-comment', 'bulk-actions'].includes(entry)) { - permEntries[entry] = entryData - } - } - return Object.keys(permEntries).length - }, - nEntries => - (ide.$scope.reviewPanel.hasEntries = - nEntries > 0 && $scope.project.features.trackChangesVisible) - ) - - function regenerateTrackChangesId(doc) { - const old_id = getChangeTracker(doc.doc_id).getIdSeed() - const new_id = RangesTracker.generateIdSeed() - getChangeTracker(doc.doc_id).setIdSeed(new_id) - return doc.setTrackChangesIdSeeds({ pending: new_id, inflight: old_id }) - } - - const refreshRanges = () => - $http - .get(`/project/${$scope.project_id}/ranges`) - .then(function (response) { - const docs = response.data - return (() => { - const result = [] - for (const doc of Array.from(docs)) { - if ( - ide.$scope.reviewPanel.overview.docsCollapsedState[doc.id] == - null - ) { - ide.$scope.reviewPanel.overview.docsCollapsedState[doc.id] = - false - } - if (doc.id !== $scope.editor.open_doc_id) { - // this is kept up to date in real-time, don't overwrite - const rangesTracker = getChangeTracker(doc.id) - rangesTracker.comments = - (doc.ranges != null ? doc.ranges.comments : undefined) || [] - rangesTracker.changes = - (doc.ranges != null ? doc.ranges.changes : undefined) || [] - } - result.push(updateEntries(doc.id)) - } - return result - })() - }) - - function refreshOverviewPanel() { - ide.$scope.reviewPanel.overview.loading = true - return refreshRanges() - .then(() => (ide.$scope.reviewPanel.overview.loading = false)) - .catch(() => (ide.$scope.reviewPanel.overview.loading = false)) - } - - ide.$scope.refreshResolvedCommentsDropdown = function () { - ide.$scope.reviewPanel.dropdown.loading = true - const q = refreshRanges() - q.then(() => (ide.$scope.reviewPanel.dropdown.loading = false)) - q.catch(() => (ide.$scope.reviewPanel.dropdown.loading = false)) - return q - } - - async function updateEntries(doc_id) { - const rangesTracker = getChangeTracker(doc_id) - const entries = getDocEntries(doc_id) - const resolvedComments = getDocResolvedComments(doc_id) - - let changed = false - - // Assume we'll delete everything until we see it, then we'll remove it from this object - const delete_changes = {} - for (const id in entries) { - const change = entries[id] - if (!['add-comment', 'bulk-actions'].includes(id)) { - for (const entry_id of Array.from(change.entry_ids)) { - delete_changes[entry_id] = true - } - } - } - for (const id in resolvedComments) { - const change = resolvedComments[id] - for (const entry_id of Array.from(change.entry_ids)) { - delete_changes[entry_id] = true - } - } - - let potential_aggregate = false - let prev_insertion = null - - for (const change of Array.from(rangesTracker.changes)) { - changed = true - - if ( - potential_aggregate && - change.op.d && - change.op.p === prev_insertion.op.p + prev_insertion.op.i.length && - change.metadata.user_id === prev_insertion.metadata.user_id - ) { - // An actual aggregate op. - entries[prev_insertion.id].type = 'aggregate-change' - entries[prev_insertion.id].metadata.replaced_content = change.op.d - entries[prev_insertion.id].entry_ids.push(change.id) - } else { - if (entries[change.id] == null) { - entries[change.id] = {} - } - delete delete_changes[change.id] - const new_entry = { - type: change.op.i ? 'insert' : 'delete', - entry_ids: [change.id], - content: change.op.i || change.op.d, - offset: change.op.p, - metadata: change.metadata, - } - for (const key in new_entry) { - entries[change.id][key] = new_entry[key] - } - } - - if (change.op.i) { - potential_aggregate = true - prev_insertion = change - } else { - potential_aggregate = false - prev_insertion = null - } - - if ($scope.users[change.metadata.user_id] == null) { - if (!window.isRestrictedTokenMember) { - refreshChangeUsers(change.metadata.user_id) - } - } - } - - if (!window.isRestrictedTokenMember) { - if (rangesTracker.comments.length > 0) { - await ensureThreadsAreLoaded() - } else if (ide.$scope.loadingThreads === true) { - // ensure that tracked changes are highlighted even if no comments are loaded - ide.$scope.loadingThreads = false - dispatchReviewPanelEvent('loaded_threads') - } - } - - if (!_loadingThreadsInProgress) { - for (const comment of Array.from(rangesTracker.comments)) { - let new_comment - changed = true - delete delete_changes[comment.id] - if (ide.$scope.reviewPanel.resolvedThreadIds[comment.op.t]) { - new_comment = - resolvedComments[comment.id] != null - ? resolvedComments[comment.id] - : (resolvedComments[comment.id] = {}) - delete entries[comment.id] - } else { - new_comment = - entries[comment.id] != null - ? entries[comment.id] - : (entries[comment.id] = {}) - delete resolvedComments[comment.id] - } - const new_entry = { - type: 'comment', - thread_id: comment.op.t, - entry_ids: [comment.id], - content: comment.op.c, - offset: comment.op.p, - } - for (const key in new_entry) { - new_comment[key] = new_entry[key] - } - } - } - - for (const change_id in delete_changes) { - const _ = delete_changes[change_id] - changed = true - delete entries[change_id] - delete resolvedComments[change_id] - } - - if (changed) { - // TODO: unused? - $scope.$broadcast('entries:changed') - } - - return entries - } - - $scope.$on('editor:track-changes:changed', async function () { - const doc_id = $scope.editor.open_doc_id - const entries = await updateEntries(doc_id) - - $scope.$broadcast('review-panel:recalculate-screen-positions') - dispatchReviewPanelEvent('recalculate-screen-positions', { - entries, - updateType: 'trackedChangesChange', - }) - - // Ensure that watchers, such as the React-based review panel component, - // are informed of the changes to entries - ide.$scope.$apply() - - return ide.$scope.$broadcast('review-panel:layout') - }) - - $scope.$on('editor:track-changes:visibility_changed', () => - $timeout(() => ide.$scope.$broadcast('review-panel:layout', false)) - ) - - $scope.$on( - 'editor:focus:changed', - function ( - e, - selection_offset_start, - selection_offset_end, - selection, - updateType = null - ) { - const doc_id = $scope.editor.open_doc_id - const entries = getDocEntries(doc_id) - // All selected changes will be added to this array. - ide.$scope.reviewPanel.selectedEntryIds = [] - // Count of user-visible changes, i.e. an aggregated change will count as one. - ide.$scope.reviewPanel.nVisibleSelectedChanges = 0 - - const offset = selection_offset_start - const length = selection_offset_end - selection_offset_start - - // Recreate the add comment and bulk actions entries only when - // necessary. This is to avoid the UI thinking that these entries have - // changed and getting into an infinite loop. - if (selection) { - const existingAddComment = entries['add-comment'] - if ( - !existingAddComment || - existingAddComment.offset !== offset || - existingAddComment.length !== length - ) { - entries['add-comment'] = { - type: 'add-comment', - offset, - length, - } - } - const existingBulkActions = entries['bulk-actions'] - if ( - !existingBulkActions || - existingBulkActions.offset !== offset || - existingBulkActions.length !== length - ) { - entries['bulk-actions'] = { - type: 'bulk-actions', - offset, - length, - } - } - } else { - delete entries['add-comment'] - delete entries['bulk-actions'] - } - - for (const id in entries) { - const entry = entries[id] - let isChangeEntryAndWithinSelection = false - if ( - entry.type === 'comment' && - !ide.$scope.reviewPanel.resolvedThreadIds[entry.thread_id] - ) { - entry.focused = - entry.offset <= selection_offset_start && - selection_offset_start <= entry.offset + entry.content.length - } else if (entry.type === 'insert') { - isChangeEntryAndWithinSelection = - entry.offset >= selection_offset_start && - entry.offset + entry.content.length <= selection_offset_end - entry.focused = - entry.offset <= selection_offset_start && - selection_offset_start <= entry.offset + entry.content.length - } else if (entry.type === 'delete') { - isChangeEntryAndWithinSelection = - selection_offset_start <= entry.offset && - entry.offset <= selection_offset_end - entry.focused = entry.offset === selection_offset_start - } else if (entry.type === 'aggregate-change') { - isChangeEntryAndWithinSelection = - entry.offset >= selection_offset_start && - entry.offset + entry.content.length <= selection_offset_end - entry.focused = - entry.offset <= selection_offset_start && - selection_offset_start <= entry.offset + entry.content.length - } else if ( - ['add-comment', 'bulk-actions'].includes(entry.type) && - selection - ) { - entry.focused = true - } - - if (isChangeEntryAndWithinSelection) { - for (const entry_id of Array.from(entry.entry_ids)) { - ide.$scope.reviewPanel.selectedEntryIds.push(entry_id) - } - ide.$scope.reviewPanel.nVisibleSelectedChanges++ - } - } - - $scope.$broadcast('review-panel:recalculate-screen-positions') - - dispatchReviewPanelEvent('recalculate-screen-positions', { - entries, - updateType, - }) - - // Ensure that watchers, such as the React-based review panel component, - // are informed of the changes to entries - ide.$scope.$apply() - - return ide.$scope.$broadcast('review-panel:layout') - } - ) - - ide.$scope.acceptChanges = function (change_ids) { - _doAcceptChanges(change_ids) - eventTracking.sendMB('rp-changes-accepted', { - view: $scope.ui.reviewPanelOpen - ? ide.$scope.reviewPanel.subView - : 'mini', - }) - } - - ide.$scope.rejectChanges = function (change_ids) { - _doRejectChanges(change_ids) - eventTracking.sendMB('rp-changes-rejected', { - view: $scope.ui.reviewPanelOpen - ? ide.$scope.reviewPanel.subView - : 'mini', - }) - } - - // The next two functions control a class on the review panel that affects - // the overflow-y rule on the panel. This is necessary so that an entry in - // the review panel is visible when hovering over its indicator when the - // review panel is minimized. See issue #8057. - $scope.mouseEnterIndicator = function () { - ide.$scope.reviewPanel.entryHover = true - } - - $scope.mouseLeaveIndicator = function () { - ide.$scope.reviewPanel.entryHover = false - } - - function _doAcceptChanges(change_ids) { - $http.post( - `/project/${$scope.project_id}/doc/${$scope.editor.open_doc_id}/changes/accept`, - { change_ids, _csrf: window.csrfToken } - ) - $scope.$broadcast('changes:accept', change_ids) - dispatchReviewPanelEvent('changes:accept', change_ids) - } - - const _doRejectChanges = change_ids => { - $scope.$broadcast('changes:reject', change_ids) - dispatchReviewPanelEvent('changes:reject', change_ids) - } - - ide.$scope.bulkAcceptActions = function () { - _doAcceptChanges(ide.$scope.reviewPanel.selectedEntryIds.slice()) - eventTracking.sendMB('rp-bulk-accept', { - view: $scope.ui.reviewPanelOpen - ? ide.$scope.reviewPanel.subView - : 'mini', - nEntries: ide.$scope.reviewPanel.nVisibleSelectedChanges, - }) - } - - ide.$scope.bulkRejectActions = function () { - _doRejectChanges(ide.$scope.reviewPanel.selectedEntryIds.slice()) - eventTracking.sendMB('rp-bulk-reject', { - view: $scope.ui.reviewPanelOpen - ? ide.$scope.reviewPanel.subView - : 'mini', - nEntries: ide.$scope.reviewPanel.nVisibleSelectedChanges, - }) - } - - ide.$scope.addNewComment = function (e) { - e.preventDefault() - ide.$scope.$broadcast('comment:start_adding') - return $scope.toggleReviewPanel() - } - - $scope.addNewCommentFromKbdShortcut = function () { - if (!$scope.project.features.trackChangesVisible) { - return - } - $scope.$broadcast('comment:select_line') - dispatchReviewPanelEvent('comment:select_line') - - if (!$scope.ui.reviewPanelOpen) { - $scope.toggleReviewPanel() - } - return $timeout(function () { - ide.$scope.$broadcast('review-panel:layout') - ide.$scope.$broadcast('comment:start_adding') - }) - } - - $scope.startNewComment = function () { - $scope.$broadcast('comment:select_line') - dispatchReviewPanelEvent('comment:select_line') - return $timeout(() => ide.$scope.$broadcast('review-panel:layout')) - } - - ide.$scope.submitNewComment = function (content) { - if (content == null || content === '') { - return - } - const doc_id = $scope.editor.open_doc_id - const entries = getDocEntries(doc_id) - if (entries['add-comment'] == null) { - return - } - const { offset, length } = entries['add-comment'] - const thread_id = RangesTracker.generateId() - const thread = getThread(thread_id) - thread.submitting = true - - const emitCommentAdd = () => { - $scope.$broadcast('comment:add', thread_id, offset, length) - dispatchReviewPanelEvent('comment:add', { - threadId: thread_id, - offset, - length, - }) - - // TODO: unused? - $scope.$broadcast('editor:clearSelection') - $timeout(() => ide.$scope.$broadcast('review-panel:layout')) - eventTracking.sendMB('rp-new-comment', { size: content.length }) - } - - return $http - .post(`/project/${$scope.project_id}/thread/${thread_id}/messages`, { - content, - _csrf: window.csrfToken, - }) - .then(() => { - emitCommentAdd() - }) - .catch(() => { - ide.showGenericMessageModal( - 'Error submitting comment', - 'Sorry, there was a problem submitting your comment' - ) - throw Error('Error submitting comment') - }) - } - - $scope.cancelNewComment = entry => - $timeout(() => ide.$scope.$broadcast('review-panel:layout')) - - $scope.startReply = function (entry) { - entry.replying = true - return $timeout(() => ide.$scope.$broadcast('review-panel:layout')) - } - - ide.$scope.submitReply = function (entry, entry_id) { - const { thread_id } = entry - const content = entry.replyContent - $http - .post(`/project/${$scope.project_id}/thread/${thread_id}/messages`, { - content, - _csrf: window.csrfToken, - }) - .catch(() => - ide.showGenericMessageModal( - 'Error submitting comment', - 'Sorry, there was a problem submitting your comment' - ) - ) - - const trackingMetadata = { - view: $scope.ui.reviewPanelOpen - ? ide.$scope.reviewPanel.subView - : 'mini', - size: entry.replyContent.length, - thread: thread_id, - } - - const thread = getThread(thread_id) - thread.submitting = true - entry.replyContent = '' - entry.replying = false - $timeout(() => ide.$scope.$broadcast('review-panel:layout')) - eventTracking.sendMB('rp-comment-reply', trackingMetadata) - } - - $scope.cancelReply = function (entry) { - entry.replying = false - entry.replyContent = '' - return ide.$scope.$broadcast('review-panel:layout') - } - - ide.$scope.resolveComment = function (doc_id, entry_id) { - const entry = getDocEntries(doc_id)[entry_id] - entry.focused = false - $http.post( - `/project/${$scope.project_id}/doc/${doc_id}/thread/${entry.thread_id}/resolve`, - { _csrf: window.csrfToken } - ) - _onCommentResolved(entry.thread_id, ide.$scope.user) - eventTracking.sendMB('rp-comment-resolve', { - view: $scope.ui.reviewPanelOpen - ? ide.$scope.reviewPanel.subView - : 'mini', - }) - } - - ide.$scope.unresolveComment = function (doc_id, thread_id) { - _onCommentReopened(thread_id) - $http.post( - `/project/${$scope.project_id}/doc/${doc_id}/thread/${thread_id}/reopen`, - { - _csrf: window.csrfToken, - } - ) - eventTracking.sendMB('rp-comment-reopen') - } - - function _onCommentResolved(thread_id, user) { - const thread = getThread(thread_id) - if (thread == null) { - return - } - thread.resolved = true - thread.resolved_by_user = formatUser(user) - thread.resolved_at = new Date().toISOString() - ide.$scope.reviewPanel.resolvedThreadIds[thread_id] = true - $scope.$broadcast('comment:resolve_threads', [thread_id]) - dispatchReviewPanelEvent('comment:resolve_threads', [thread_id]) - } - - function _onCommentReopened(thread_id) { - const thread = getThread(thread_id) - if (thread == null) { - return - } - delete thread.resolved - delete thread.resolved_by_user - delete thread.resolved_at - delete ide.$scope.reviewPanel.resolvedThreadIds[thread_id] - $scope.$broadcast('comment:unresolve_thread', thread_id) - dispatchReviewPanelEvent('comment:unresolve_thread', thread_id) - } - - function _onThreadDeleted(thread_id) { - delete ide.$scope.reviewPanel.resolvedThreadIds[thread_id] - delete ide.$scope.reviewPanel.commentThreads[thread_id] - $scope.$broadcast('comment:remove', thread_id) - dispatchReviewPanelEvent('comment:remove', thread_id) - } - - function _onCommentEdited(thread_id, comment_id, content) { - const thread = getThread(thread_id) - if (thread == null) { - return - } - for (const message of Array.from(thread.messages)) { - if (message.id === comment_id) { - message.content = content - } - } - return updateEntries() - } - - function _onCommentDeleted(thread_id, comment_id) { - const thread = getThread(thread_id) - if (thread == null) { - return - } - thread.messages = thread.messages.filter(m => m.id !== comment_id) - return updateEntries() - } - - ide.$scope.deleteThread = function (entry_id, doc_id, thread_id) { - _onThreadDeleted(thread_id) - $http({ - method: 'DELETE', - url: `/project/${$scope.project_id}/doc/${doc_id}/thread/${thread_id}`, - headers: { - 'X-CSRF-Token': window.csrfToken, - }, - }) - eventTracking.sendMB('rp-comment-delete') - } - - ide.$scope.saveEdit = function (thread_id, comment_id, content) { - $http.post( - `/project/${$scope.project_id}/thread/${thread_id}/messages/${comment_id}/edit`, - { - content, - _csrf: window.csrfToken, - } - ) - return $timeout(() => ide.$scope.$broadcast('review-panel:layout')) - } - - ide.$scope.deleteComment = function (thread_id, comment_id) { - _onCommentDeleted(thread_id, comment_id) - $http({ - method: 'DELETE', - url: `/project/${$scope.project_id}/thread/${thread_id}/messages/${comment_id}`, - headers: { - 'X-CSRF-Token': window.csrfToken, - }, - }) - return $timeout(() => ide.$scope.$broadcast('review-panel:layout')) - } - - $scope.setSubView = function (subView) { - ide.$scope.reviewPanel.subView = subView - eventTracking.sendMB('rp-subview-change', { subView }) - } - - ide.$scope.gotoEntry = (doc_id, entry_offset) => - ide.editorManager.openDocId(doc_id, { gotoOffset: entry_offset }) - - const _setUserTCState = function (userId, newValue, isLocal) { - if (isLocal == null) { - isLocal = false - } - if (ide.$scope.reviewPanel.trackChangesState[userId] == null) { - ide.$scope.reviewPanel.trackChangesState[userId] = {} - } - const state = ide.$scope.reviewPanel.trackChangesState[userId] - - if ( - state.syncState == null || - state.syncState === UserTCSyncState.SYNCED - ) { - state.value = newValue - state.syncState = UserTCSyncState.SYNCED - } else if ( - state.syncState === UserTCSyncState.PENDING && - state.value === newValue - ) { - state.syncState = UserTCSyncState.SYNCED - } else if (isLocal) { - state.value = newValue - state.syncState = UserTCSyncState.PENDING - } - if (userId === ide.$scope.user.id) { - return ($scope.editor.wantTrackChanges = newValue) - } - } - - const _setEveryoneTCState = function (newValue, isLocal) { - if (isLocal == null) { - isLocal = false - } - ide.$scope.reviewPanel.trackChangesOnForEveryone = newValue - const { project } = $scope - for (const member of Array.from(project.members)) { - _setUserTCState(member._id, newValue, isLocal) - } - _setGuestsTCState(newValue, isLocal) - return _setUserTCState(project.owner._id, newValue, isLocal) - } - - function _setGuestsTCState(newValue, isLocal) { - if (isLocal == null) { - isLocal = false - } - ide.$scope.reviewPanel.trackChangesOnForGuests = newValue - if ( - currentUserType() === UserTypes.GUEST || - currentUserType() === UserTypes.ANONYMOUS - ) { - return ($scope.editor.wantTrackChanges = newValue) - } - } - - const applyClientTrackChangesStateToServer = function () { - ide.$scope.reviewPanel.trackChangesState = { - ...ide.$scope.reviewPanel.trackChangesState, - } - const data = {} - if (ide.$scope.reviewPanel.trackChangesOnForEveryone) { - data.on = true - } else { - data.on_for = {} - for (const userId in ide.$scope.reviewPanel.trackChangesState) { - const userState = ide.$scope.reviewPanel.trackChangesState[userId] - data.on_for[userId] = userState.value - } - if (ide.$scope.reviewPanel.trackChangesOnForGuests) { - data.on_for_guests = true - } - } - data._csrf = window.csrfToken - return $http.post(`/project/${$scope.project_id}/track_changes`, data) - } - - const applyTrackChangesStateToClient = function (state) { - if (typeof state === 'boolean') { - _setEveryoneTCState(state) - return _setGuestsTCState(state) - } else { - const { project } = $scope - ide.$scope.reviewPanel.trackChangesOnForEveryone = false - _setGuestsTCState(state.__guests__ === true) - for (const member of Array.from(project.members)) { - _setUserTCState( - member._id, - state[member._id] != null ? state[member._id] : false - ) - } - return _setUserTCState( - $scope.project.owner._id, - state[$scope.project.owner._id] != null - ? state[$scope.project.owner._id] - : false - ) - } - } - - ide.$scope.toggleTrackChangesForEveryone = function (onForEveryone) { - _setEveryoneTCState(onForEveryone, true) - _setGuestsTCState(onForEveryone, true) - return applyClientTrackChangesStateToServer() - } - - ide.$scope.toggleTrackChangesForGuests = function (onForGuests) { - _setGuestsTCState(onForGuests, true) - return applyClientTrackChangesStateToServer() - } - - ide.$scope.toggleTrackChangesForUser = function (onForUser, userId) { - _setUserTCState(userId, onForUser, true) - return applyClientTrackChangesStateToServer() - } - - ide.socket.on('toggle-track-changes', state => - $scope.$apply(() => applyTrackChangesStateToClient(state)) - ) - - $scope.toggleTrackChangesFromKbdShortcut = function () { - if ( - !( - $scope.project.features.trackChangesVisible && - $scope.project.features.trackChanges - ) - ) { - return - } - return $scope.toggleTrackChangesForUser( - !ide.$scope.reviewPanel.trackChangesState[ide.$scope.user.id].value, - ide.$scope.user.id - ) - } - - const setGuestFeatureBasedOnProjectAccessLevel = projectPublicAccessLevel => - (ide.$scope.reviewPanel.trackChangesForGuestsAvailable = - projectPublicAccessLevel === 'tokenBased') - - const onToggleTrackChangesForGuestsAvailability = function (available) { - // If the feature is no longer available we need to turn off the guest flag - if (available) { - return - } - if (!ide.$scope.reviewPanel.trackChangesOnForGuests) { - return - } // Already turned off - if (ide.$scope.reviewPanel.trackChangesOnForEveryone) { - return - } // Overrides guest setting - return $scope.toggleTrackChangesForGuests(false) - } - - $scope.$watch( - 'project.publicAccesLevel', - setGuestFeatureBasedOnProjectAccessLevel - ) - - $scope.$watch( - 'reviewPanel.trackChangesForGuestsAvailable', - function (available) { - if (available != null) { - return onToggleTrackChangesForGuestsAvailability(available) - } - } - ) - - let _inited = false - ide.$scope.$on('project:joined', function () { - if (_inited) { - return - } - const { project } = ide.$scope - if (project.features.trackChanges) { - applyTrackChangesStateToClient(project.trackChangesState) - } else { - applyTrackChangesStateToClient(false) - } - setGuestFeatureBasedOnProjectAccessLevel(project.publicAccesLevel) - return (_inited = true) - }) - - let _refreshingRangeUsers = false - const _refreshedForUserIds = {} - function refreshChangeUsers(refresh_for_user_id) { - if (refresh_for_user_id != null) { - if (_refreshedForUserIds[refresh_for_user_id] != null) { - // We've already tried to refresh to get this user id, so stop it looping - return - } - _refreshedForUserIds[refresh_for_user_id] = true - } - - // Only do one refresh at once - if (_refreshingRangeUsers) { - return - } - _refreshingRangeUsers = true - - return $http - .get(`/project/${$scope.project_id}/changes/users`) - .then(function (response) { - const users = response.data - _refreshingRangeUsers = false - ide.$scope.users = $scope.users = {} - // Always include ourself, since if we submit an op, we might need to display info - // about it locally before it has been flushed through the server - if ( - (ide.$scope.user != null ? ide.$scope.user.id : undefined) != null - ) { - $scope.users[ide.$scope.user.id] = formatUser(ide.$scope.user) - } - return (() => { - const result = [] - for (const user of Array.from(users)) { - if (user.id != null) { - result.push(($scope.users[user.id] = formatUser(user))) - } else { - result.push(undefined) - } - } - return result - })() - }) - .catch(() => (_refreshingRangeUsers = false)) - } - - let _threadsLoadedOnce = false - let _loadingThreadsInProgress = false - async function ensureThreadsAreLoaded() { - if (_threadsLoadedOnce) { - // We get any updates in real time so only need to load them once. - return - } - _threadsLoadedOnce = true - _loadingThreadsInProgress = true - ide.$scope.loadingThreads = true - return $http - .get(`/project/${$scope.project_id}/threads`) - .then(async function (response) { - const threads = response.data - ide.$scope.loadingThreads = false - _loadingThreadsInProgress = false - for (const thread_id in ide.$scope.reviewPanel.resolvedThreadIds) { - delete ide.$scope.reviewPanel.resolvedThreadIds[thread_id] - } - for (const thread_id in threads) { - const thread = threads[thread_id] - for (const comment of Array.from(thread.messages)) { - formatComment(comment) - } - if (thread.resolved_by_user != null) { - thread.resolved_by_user = formatUser(thread.resolved_by_user) - ide.$scope.reviewPanel.resolvedThreadIds[thread_id] = true - $scope.$broadcast('comment:resolve_threads', [thread_id]) - } - } - ide.$scope.reviewPanel.commentThreads = threads - // Update entries so that the view has up-to-date information about - // the entries when handling the loaded_threads events, which avoids - // thrashing - await updateEntries($scope.editor.open_doc_id) - - dispatchReviewPanelEvent('loaded_threads') - return $timeout(() => ide.$scope.$broadcast('review-panel:layout')) - }) - } - - function formatComment(comment) { - comment.user = formatUser(comment.user) - comment.timestamp = new Date(comment.timestamp) - return comment - } - - function formatUser(user) { - let isSelf, name - const id = - (user != null ? user._id : undefined) || - (user != null ? user.id : undefined) - - if (id == null) { - return { - email: null, - name: 'Anonymous', - isSelf: false, - hue: ColorManager.ANONYMOUS_HUE, - avatar_text: 'A', - } - } - if (id === window.user_id) { - name = 'You' - isSelf = true - } else { - name = [user.first_name, user.last_name] - .filter(n => n != null && n !== '') - .join(' ') - if (name === '') { - name = - (user.email != null ? user.email.split('@')[0] : undefined) || - 'Unknown' - } - isSelf = false - } - return { - id, - email: user.email, - name, - isSelf, - hue: ColorManager.getHueForUserId(id), - avatar_text: [user.first_name, user.last_name] - .filter(n => n != null) - .map(n => n[0]) - .join(''), - } - } - - // listen for events from the CodeMirror 6 track changes extension - window.addEventListener('editor:event', event => { - const { type, payload } = event.detail - - switch (type) { - case 'line-height': { - ide.$scope.reviewPanel.rendererData.lineHeight = payload - ide.$scope.$broadcast('review-panel:layout') - break - } - - case 'track-changes:changed': { - $scope.$broadcast('editor:track-changes:changed') - break - } - - case 'track-changes:visibility_changed': { - $scope.$broadcast('editor:track-changes:visibility_changed') - break - } - - case 'focus:changed': { - const { from, to, empty, updateType } = payload - $scope.$broadcast( - 'editor:focus:changed', - from, - to, - !empty, - updateType - ) - break - } - - case 'add-new-comment': { - $scope.addNewCommentFromKbdShortcut() - break - } - - case 'toggle-track-changes': { - $scope.toggleTrackChangesFromKbdShortcut() - break - } - - case 'toggle-review-panel': { - $scope.toggleReviewPanel() - break - } - } - }) - - // Add methods somewhere that React can see them - $scope.reviewPanel.saveEdit = $scope.saveEdit - }, -]) - -// send events to the CodeMirror 6 track changes extension -const dispatchReviewPanelEvent = (type, payload) => { - window.dispatchEvent( - new CustomEvent('review-panel:event', { - detail: { type, payload }, - }) - ) -} diff --git a/services/web/frontend/js/ide/services/ide.js b/services/web/frontend/js/ide/services/ide.js deleted file mode 100644 index d0d3ee1f01..0000000000 --- a/services/web/frontend/js/ide/services/ide.js +++ /dev/null @@ -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 -} diff --git a/services/web/frontend/js/ide/toolbar/EditorLoaderController.js b/services/web/frontend/js/ide/toolbar/EditorLoaderController.js deleted file mode 100644 index 1164e22157..0000000000 --- a/services/web/frontend/js/ide/toolbar/EditorLoaderController.js +++ /dev/null @@ -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') - }) - }, -]) diff --git a/services/web/frontend/js/ide/toolbar/index.js b/services/web/frontend/js/ide/toolbar/index.js deleted file mode 100644 index fd9e505cdb..0000000000 --- a/services/web/frontend/js/ide/toolbar/index.js +++ /dev/null @@ -1 +0,0 @@ -import './EditorLoaderController' diff --git a/services/web/frontend/js/libraries.js b/services/web/frontend/js/libraries.js deleted file mode 100644 index e231bb2dd1..0000000000 --- a/services/web/frontend/js/libraries.js +++ /dev/null @@ -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' diff --git a/services/web/frontend/js/main.js b/services/web/frontend/js/main.js deleted file mode 100644 index 1472a803ef..0000000000 --- a/services/web/frontend/js/main.js +++ /dev/null @@ -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']) diff --git a/services/web/frontend/js/main/event.js b/services/web/frontend/js/main/event.js deleted file mode 100644 index b3e64cf7c3..0000000000 --- a/services/web/frontend/js/main/event.js +++ /dev/null @@ -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) - } -}) diff --git a/services/web/frontend/js/main/importing.js b/services/web/frontend/js/main/importing.js deleted file mode 100644 index 6469321709..0000000000 --- a/services/web/frontend/js/main/importing.js +++ /dev/null @@ -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, - } - }, -]) diff --git a/services/web/frontend/js/main/system-messages.js b/services/web/frontend/js/main/system-messages.js deleted file mode 100644 index 9fe2e25d94..0000000000 --- a/services/web/frontend/js/main/system-messages.js +++ /dev/null @@ -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) - } - }) - }, -]) diff --git a/services/web/frontend/js/main/token-access.js b/services/web/frontend/js/main/token-access.js deleted file mode 100644 index 68cb1075ab..0000000000 --- a/services/web/frontend/js/main/token-access.js +++ /dev/null @@ -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' - } - ) - } - }, -]) diff --git a/services/web/frontend/js/modules/errorCatcher.js b/services/web/frontend/js/modules/errorCatcher.js deleted file mode 100644 index 0a7b388770..0000000000 --- a/services/web/frontend/js/modules/errorCatcher.js +++ /dev/null @@ -1,84 +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 - * DS207: Consider shorter variations of null checks - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ -import { captureException } from '../infrastructure/error-reporter' - -const app = angular.module('ErrorCatcher', []) -const UNHANDLED_REJECTION_ERR_MSG = 'Possibly unhandled rejection: canceled' - -app.config([ - '$provide', - function ($provide) { - return $provide.decorator('$exceptionHandler', [ - '$log', - '$delegate', - ($log, $delegate) => - function (exception, cause) { - if ( - exception === UNHANDLED_REJECTION_ERR_MSG && - cause === undefined - ) { - return - } - - captureException(exception, { - tags: { - handler: 'angular-exception-handler', - }, - }) - - return $delegate(exception, cause) - }, - ]) - }, -]) - -// Interceptor to check auth failures in all $http requests -// http://bahmutov.calepin.co/catch-all-errors-in-angular-app.html - -app.factory('unAuthHttpResponseInterceptor', [ - '$q', - function ($q) { - return { - responseError(response) { - // redirect any unauthorised or forbidden responses back to /login - // - // set disableAutoLoginRedirect:true in the http request config - // to disable this behaviour - if ( - [401, 403].includes(response.status) && - !(response.config != null - ? response.config.disableAutoLoginRedirect - : undefined) - ) { - // for /project urls set the ?redir parameter to come back here - // otherwise just go to the login page - if (window.location.pathname.match(/^\/project/)) { - window.location = `/login?redir=${encodeURI( - window.location.pathname - )}` - } else { - window.location = '/login' - } - } - // pass the response back to the original requester - return $q.reject(response) - }, - } - }, -]) - -app.config([ - '$httpProvider', - function ($httpProvider) { - return $httpProvider.interceptors.push('unAuthHttpResponseInterceptor') - }, -]) diff --git a/services/web/frontend/js/modules/localStorage.js b/services/web/frontend/js/modules/localStorage.js deleted file mode 100644 index 4b41f732b8..0000000000 --- a/services/web/frontend/js/modules/localStorage.js +++ /dev/null @@ -1,19 +0,0 @@ -import { debugConsole } from '@/utils/debugging' - -angular.module('localStorage', []).value('localStorage', localStorage) - -/* - localStorage can throw browser exceptions, for example if it is full - We don't use localStorage for anything critical, on in that case just - fail gracefully. -*/ -function localStorage(...args) { - try { - return $.localStorage(...args) - } catch (e) { - debugConsole.error('localStorage exception', e) - return null - } -} - -export default localStorage diff --git a/services/web/frontend/js/modules/recursionHelper.js b/services/web/frontend/js/modules/recursionHelper.js deleted file mode 100644 index f19caefed0..0000000000 --- a/services/web/frontend/js/modules/recursionHelper.js +++ /dev/null @@ -1,78 +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 - */ -// -// * An Angular service which helps with creating recursive directives. -// * @author Mark Lagendijk -// * @license MIT -// -// From: https://github.com/marklagendijk/angular-recursion -/* 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 - */ -// -// * An Angular service which helps with creating recursive directives. -// * @author Mark Lagendijk -// * @license MIT -// -// From: https://github.com/marklagendijk/angular-recursion -angular.module('RecursionHelper', []).factory('RecursionHelper', [ - '$compile', - function ($compile) { - /* - Manually compiles the element, fixing the recursion loop. - @param element - @param [link] A post-link function, or an object with function(s) registered via pre and post properties. - @returns An object containing the linking functions. - */ - return { - compile(element, link) { - // Normalize the link parameter - if (angular.isFunction(link)) { - link = { post: link } - } - - // Break the recursion loop by removing the contents - const contents = element.contents().remove() - let compiledContents - return { - pre: link && link.pre ? link.pre : null, - - /* - Compiles and re-adds the contents - */ - post(scope, element) { - // Compile the contents - if (!compiledContents) { - compiledContents = $compile(contents) - } - - // Re-add the compiled contents to the element - compiledContents(scope, function (clone) { - element.append(clone) - }) - - // Call the post-linking function, if any - if (link && link.post) { - link.post.apply(null, arguments) - } - }, - } - }, - } - }, -]) diff --git a/services/web/frontend/js/modules/sessionStorage.js b/services/web/frontend/js/modules/sessionStorage.js deleted file mode 100644 index 94a363dbce..0000000000 --- a/services/web/frontend/js/modules/sessionStorage.js +++ /dev/null @@ -1,19 +0,0 @@ -import { debugConsole } from '@/utils/debugging' - -angular.module('sessionStorage', []).value('sessionStorage', sessionStorage) - -/* - sessionStorage can throw browser exceptions, for example if it is full - We don't use sessionStorage for anything critical, on in that case just - fail gracefully. -*/ -function sessionStorage(...args) { - try { - return $.sessionStorage(...args) - } catch (e) { - debugConsole.error('sessionStorage exception', e) - return null - } -} - -export default sessionStorage diff --git a/services/web/frontend/js/services/algolia-search.js b/services/web/frontend/js/services/algolia-search.js deleted file mode 100644 index 6eb1c92ab6..0000000000 --- a/services/web/frontend/js/services/algolia-search.js +++ /dev/null @@ -1,20 +0,0 @@ -import _ from 'lodash' -import App from '../base' -import AlgoliaSearch from 'algoliasearch' -import getMeta from '../utils/meta' - -export default App.factory('algoliaSearch', function () { - let wikiIdx - const algoliaConfig = getMeta('ol-algolia') - const wikiIndex = _.get(algoliaConfig, 'indexes.wiki') - if (wikiIndex) { - const client = AlgoliaSearch(algoliaConfig.appId, algoliaConfig.apiKey) - wikiIdx = client.initIndex(wikiIndex) - } - - const service = { - searchWiki: wikiIdx ? wikiIdx.search.bind(wikiIdx) : null, - } - - return service -}) diff --git a/services/web/frontend/js/services/queued-http.js b/services/web/frontend/js/services/queued-http.js deleted file mode 100644 index 5bc3172870..0000000000 --- a/services/web/frontend/js/services/queued-http.js +++ /dev/null @@ -1,82 +0,0 @@ -/* eslint-disable - max-len, -*/ -// 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' - -export default App.factory('queuedHttp', [ - '$http', - function ($http) { - const pendingRequests = [] - let inflight = false - - function processPendingRequests() { - if (inflight) { - return - } - const doRequest = pendingRequests.shift() - if (doRequest != null) { - inflight = true - return doRequest() - .then(function () { - inflight = false - return processPendingRequests() - }) - .catch(function () { - inflight = false - return processPendingRequests() - }) - } - } - - const queuedHttp = function (...args) { - // We can't use Angular's $q.defer promises, because it only passes - // a single argument on error, and $http passes multiple. - const promise = {} - const successCallbacks = [] - const errorCallbacks = [] - - // Adhere to the $http promise conventions - promise.then = function (callback, errCallback) { - successCallbacks.push(callback) - if (errCallback != null) { - errorCallbacks.push(errCallback) - } - return promise - } - - promise.catch = function (callback) { - errorCallbacks.push(callback) - return promise - } - - const doRequest = () => - $http(...Array.from(args || [])) - .then((...args) => - Array.from(successCallbacks).map(fn => - fn(...Array.from(args || [])) - ) - ) - .catch((...args) => - Array.from(errorCallbacks).map(fn => fn(...Array.from(args || []))) - ) - - pendingRequests.push(doRequest) - processPendingRequests() - - return promise - } - - queuedHttp.post = (url, data) => queuedHttp({ method: 'POST', url, data }) - - return queuedHttp - }, -]) diff --git a/services/web/frontend/js/shared/context/controllers/root-context-controller.js b/services/web/frontend/js/shared/context/controllers/root-context-controller.js deleted file mode 100644 index 19d98e92eb..0000000000 --- a/services/web/frontend/js/shared/context/controllers/root-context-controller.js +++ /dev/null @@ -1,8 +0,0 @@ -import App from '../../../base' -import { react2angular } from 'react2angular' -import { rootContext } from '../root-context' - -App.component( - 'sharedContextReact', - react2angular(rootContext.component, [], ['ide']) -) diff --git a/services/web/frontend/js/shared/context/ide-angular-provider.tsx b/services/web/frontend/js/shared/context/ide-angular-provider.tsx deleted file mode 100644 index f0780f135b..0000000000 --- a/services/web/frontend/js/shared/context/ide-angular-provider.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { FC, useState } from 'react' -import { AngularScopeValueStore } from '@/features/ide-react/scope-value-store/angular-scope-value-store' -import { AngularScopeEventEmitter } from '@/features/ide-react/scope-event-emitter/angular-scope-event-emitter' -import { Ide, IdeProvider } from '@/shared/context/ide-context' -import { getMockIde } from '@/shared/context/mock/mock-ide' - -export const IdeAngularProvider: FC<{ ide?: Ide }> = ({ ide, children }) => { - const [ideValue] = useState(() => ide || getMockIde()) - const [scopeStore] = useState( - () => new AngularScopeValueStore(ideValue.$scope) - ) - const [scopeEventEmitter] = useState( - () => new AngularScopeEventEmitter(ideValue.$scope) - ) - - return ( - - {children} - - ) -} diff --git a/services/web/frontend/js/shared/context/ide-context.tsx b/services/web/frontend/js/shared/context/ide-context.tsx index 14b4b3db5e..ea7da8e358 100644 --- a/services/web/frontend/js/shared/context/ide-context.tsx +++ b/services/web/frontend/js/shared/context/ide-context.tsx @@ -1,22 +1,18 @@ import { createContext, FC, useContext, useEffect, useMemo } from 'react' import { ScopeValueStore } from '../../../../types/ide/scope-value-store' -import { Scope } from '../../../../types/angular/scope' -import getMeta from '@/utils/meta' import { ScopeEventEmitter } from '../../../../types/ide/scope-event-emitter' export type Ide = { [key: string]: any // TODO: define the rest of the `ide` and `$scope` properties - $scope: Scope + $scope: Record } type IdeContextValue = Ide & { - isReactIde: boolean scopeStore: ScopeValueStore scopeEventEmitter: ScopeEventEmitter } -const IdeContext = createContext(undefined) -const isReactIde: boolean = getMeta('ol-idePageReact') +export const IdeContext = createContext(undefined) export const IdeProvider: FC<{ ide: Ide @@ -45,7 +41,6 @@ export const IdeProvider: FC<{ const value = useMemo(() => { return { ...ide, - isReactIde, scopeStore, scopeEventEmitter, } diff --git a/services/web/frontend/js/shared/context/local-compile-context.tsx b/services/web/frontend/js/shared/context/local-compile-context.tsx index 8586ba0fc3..0848217f75 100644 --- a/services/web/frontend/js/shared/context/local-compile-context.tsx +++ b/services/web/frontend/js/shared/context/local-compile-context.tsx @@ -35,6 +35,7 @@ import { useFileTreeData } from '@/shared/context/file-tree-data-context' import { useFileTreePathContext } from '@/features/file-tree/contexts/file-tree-path' import { useUserSettingsContext } from '@/shared/context/user-settings-context' import { useFeatureFlag } from '@/shared/context/split-test-context' +import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context' type PdfFile = Record @@ -106,6 +107,7 @@ export const LocalCompileProvider: FC = ({ children }) => { const ide = useIdeContext() const { hasPremiumCompile, isProjectOwner } = useEditorContext() + const { openDocId } = useEditorManagerContext() const { _id: projectId, rootDocId } = useProjectContext() @@ -574,13 +576,13 @@ export const LocalCompileProvider: FC = ({ children }) => { const result = findEntityByPath(entry.file) if (result && result.type === 'doc') { - ide.editorManager.openDocId(result.entity._id, { + openDocId(result.entity._id, { gotoLine: entry.line ?? undefined, gotoColumn: entry.column ?? undefined, }) } }, - [findEntityByPath, ide.editorManager] + [findEntityByPath, openDocId] ) // clear the cache then run a compile, triggered by a menu item diff --git a/services/web/frontend/js/shared/context/project-context.tsx b/services/web/frontend/js/shared/context/project-context.tsx index 58f5398906..e473955f5d 100644 --- a/services/web/frontend/js/shared/context/project-context.tsx +++ b/services/web/frontend/js/shared/context/project-context.tsx @@ -62,7 +62,7 @@ const projectFallback = { } export const ProjectProvider: FC = ({ children }) => { - const [project] = useScopeValue('project', true) + const [project] = useScopeValue('project') const { _id, diff --git a/services/web/frontend/js/shared/context/root-context.tsx b/services/web/frontend/js/shared/context/root-context.tsx deleted file mode 100644 index 52b8bf7904..0000000000 --- a/services/web/frontend/js/shared/context/root-context.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { FC } from 'react' -import createSharedContext from 'react2angular-shared-context' -import { UserProvider } from './user-context' -import { IdeAngularProvider } from './ide-angular-provider' -import { EditorProvider } from './editor-context' -import { LocalCompileProvider } from './local-compile-context' -import { DetachCompileProvider } from './detach-compile-context' -import { LayoutProvider } from './layout-context' -import { DetachProvider } from './detach-context' -import { ChatProvider } from '@/features/chat/context/chat-context' -import { ProjectProvider } from './project-context' -import { SplitTestProvider } from './split-test-context' -import { FileTreeDataProvider } from './file-tree-data-context' -import { ProjectSettingsProvider } from '@/features/editor-left-menu/context/project-settings-context' -import { FileTreePathProvider } from '@/features/file-tree/contexts/file-tree-path' -import { UserSettingsProvider } from '@/shared/context/user-settings-context' -import { OutlineProvider } from '@/features/ide-react/context/outline-context' -import { Ide } from '@/shared/context/ide-context' - -export const ContextRoot: FC<{ ide?: Ide }> = ({ children, ide }) => { - return ( - - - - - - - - - - - - - - - {children} - - - - - - - - - - - - - - - ) -} - -export const rootContext = createSharedContext(ContextRoot) diff --git a/services/web/frontend/js/shared/hooks/use-scope-value.ts b/services/web/frontend/js/shared/hooks/use-scope-value.ts index 38a716a1eb..594a71f92a 100644 --- a/services/web/frontend/js/shared/hooks/use-scope-value.ts +++ b/services/web/frontend/js/shared/hooks/use-scope-value.ts @@ -17,24 +17,19 @@ import { useIdeContext } from '../context/ide-context' * returned as undefined when there is nothing in the scope store for that path. */ export default function useScopeValue( - path: string, // dot '.' path of a property in the Angular scope - deep = false + path: string // dot '.' path of a property in the Angular scope ): [T, Dispatch>] { const { scopeStore } = useIdeContext() const [value, setValue] = useState(() => scopeStore.get(path)) useEffect(() => { - return scopeStore.watch( - path, - (newValue: T) => { - // NOTE: this is deliberately wrapped in a function, - // to avoid calling setValue directly with a value that's a function - setValue(() => newValue) - }, - deep - ) - }, [path, scopeStore, deep]) + return scopeStore.watch(path, (newValue: T) => { + // NOTE: this is deliberately wrapped in a function, + // to avoid calling setValue directly with a value that's a function + setValue(() => newValue) + }) + }, [path, scopeStore]) const scopeSetter = useCallback( (newValue: SetStateAction) => { diff --git a/services/web/frontend/js/utils/EventEmitter.js b/services/web/frontend/js/utils/EventEmitter.js index 84e8c2fc2c..3ed66cdc41 100644 --- a/services/web/frontend/js/utils/EventEmitter.js +++ b/services/web/frontend/js/utils/EventEmitter.js @@ -7,6 +7,10 @@ // Remove a listener for the foo event with the bar namespace: .off 'foo.bar' export default class EventEmitter { + constructor() { + this.events = {} + } + on(event, callback) { if (!this.events) { this.events = {} diff --git a/services/web/frontend/js/vendor/libs/jquery-layout.js b/services/web/frontend/js/vendor/libs/jquery-layout.js deleted file mode 100644 index 43ecc886c1..0000000000 --- a/services/web/frontend/js/vendor/libs/jquery-layout.js +++ /dev/null @@ -1,8503 +0,0 @@ -/*! jQuery UI - v1.11.4 - 2016-02-10 -* http://jqueryui.com -* Includes: core.js, widget.js, mouse.js, draggable.js, droppable.js -* Copyright jQuery Foundation and other contributors; Licensed MIT */ - -/*! - * jQuery UI Core 1.11.4 - * http://jqueryui.com - * - * Copyright jQuery Foundation and other contributors - * Released under the MIT license. - * http://jquery.org/license - * - * http://api.jqueryui.com/category/ui-core/ - */ - - -// $.ui might exist from components with no dependencies, e.g., $.ui.position -$.ui = $.ui || {}; - -$.extend( $.ui, { - version: "1.11.4", - - keyCode: { - BACKSPACE: 8, - COMMA: 188, - DELETE: 46, - DOWN: 40, - END: 35, - ENTER: 13, - ESCAPE: 27, - HOME: 36, - LEFT: 37, - PAGE_DOWN: 34, - PAGE_UP: 33, - PERIOD: 190, - RIGHT: 39, - SPACE: 32, - TAB: 9, - UP: 38 - } -}); - -// plugins -$.fn.extend({ - scrollParent: function( includeHidden ) { - var position = this.css( "position" ), - excludeStaticParent = position === "absolute", - overflowRegex = includeHidden ? /(auto|scroll|hidden)/ : /(auto|scroll)/, - scrollParent = this.parents().filter( function() { - var parent = $( this ); - if ( excludeStaticParent && parent.css( "position" ) === "static" ) { - return false; - } - return overflowRegex.test( parent.css( "overflow" ) + parent.css( "overflow-y" ) + parent.css( "overflow-x" ) ); - }).eq( 0 ); - - return position === "fixed" || !scrollParent.length ? $( this[ 0 ].ownerDocument || document ) : scrollParent; - }, - - uniqueId: (function() { - var uuid = 0; - - return function() { - return this.each(function() { - if ( !this.id ) { - this.id = "ui-id-" + ( ++uuid ); - } - }); - }; - })(), - - removeUniqueId: function() { - return this.each(function() { - if ( /^ui-id-\d+$/.test( this.id ) ) { - $( this ).removeAttr( "id" ); - } - }); - } -}); - -// selectors -function focusable( element, isTabIndexNotNaN ) { - var map, mapName, img, - nodeName = element.nodeName.toLowerCase(); - if ( "area" === nodeName ) { - map = element.parentNode; - mapName = map.name; - if ( !element.href || !mapName || map.nodeName.toLowerCase() !== "map" ) { - return false; - } - img = $( "img[usemap='#" + mapName + "']" )[ 0 ]; - return !!img && visible( img ); - } - return ( /^(input|select|textarea|button|object)$/.test( nodeName ) ? - !element.disabled : - "a" === nodeName ? - element.href || isTabIndexNotNaN : - isTabIndexNotNaN) && - // the element and all of its ancestors must be visible - visible( element ); -} - -function visible( element ) { - return $.expr.filters.visible( element ) && - !$( element ).parents().addBack().filter(function() { - return $.css( this, "visibility" ) === "hidden"; - }).length; -} - -$.extend( $.expr[ ":" ], { - data: $.expr.createPseudo ? - $.expr.createPseudo(function( dataName ) { - return function( elem ) { - return !!$.data( elem, dataName ); - }; - }) : - // support: jQuery <1.8 - function( elem, i, match ) { - return !!$.data( elem, match[ 3 ] ); - }, - - focusable: function( element ) { - return focusable( element, !isNaN( $.attr( element, "tabindex" ) ) ); - }, - - tabbable: function( element ) { - var tabIndex = $.attr( element, "tabindex" ), - isTabIndexNaN = isNaN( tabIndex ); - return ( isTabIndexNaN || tabIndex >= 0 ) && focusable( element, !isTabIndexNaN ); - } -}); - -// support: jQuery <1.8 -if ( !$( "" ).outerWidth( 1 ).jquery ) { - $.each( [ "Width", "Height" ], function( i, name ) { - var side = name === "Width" ? [ "Left", "Right" ] : [ "Top", "Bottom" ], - type = name.toLowerCase(), - orig = { - innerWidth: $.fn.innerWidth, - innerHeight: $.fn.innerHeight, - outerWidth: $.fn.outerWidth, - outerHeight: $.fn.outerHeight - }; - - function reduce( elem, size, border, margin ) { - $.each( side, function() { - size -= parseFloat( $.css( elem, "padding" + this ) ) || 0; - if ( border ) { - size -= parseFloat( $.css( elem, "border" + this + "Width" ) ) || 0; - } - if ( margin ) { - size -= parseFloat( $.css( elem, "margin" + this ) ) || 0; - } - }); - return size; - } - - $.fn[ "inner" + name ] = function( size ) { - if ( size === undefined ) { - return orig[ "inner" + name ].call( this ); - } - - return this.each(function() { - $( this ).css( type, reduce( this, size ) + "px" ); - }); - }; - - $.fn[ "outer" + name] = function( size, margin ) { - if ( typeof size !== "number" ) { - return orig[ "outer" + name ].call( this, size ); - } - - return this.each(function() { - $( this).css( type, reduce( this, size, true, margin ) + "px" ); - }); - }; - }); -} - -// support: jQuery <1.8 -if ( !$.fn.addBack ) { - $.fn.addBack = function( selector ) { - return this.add( selector == null ? - this.prevObject : this.prevObject.filter( selector ) - ); - }; -} - -// support: jQuery 1.6.1, 1.6.2 (http://bugs.jquery.com/ticket/9413) -if ( $( "" ).data( "a-b", "a" ).removeData( "a-b" ).data( "a-b" ) ) { - $.fn.removeData = (function( removeData ) { - return function( key ) { - if ( arguments.length ) { - return removeData.call( this, $.camelCase( key ) ); - } else { - return removeData.call( this ); - } - }; - })( $.fn.removeData ); -} - -// deprecated -$.ui.ie = !!/msie [\w.]+/.exec( navigator.userAgent.toLowerCase() ); - -$.fn.extend({ - focus: (function( orig ) { - return function( delay, fn ) { - return typeof delay === "number" ? - this.each(function() { - var elem = this; - setTimeout(function() { - $( elem ).focus(); - if ( fn ) { - fn.call( elem ); - } - }, delay ); - }) : - orig.apply( this, arguments ); - }; - })( $.fn.focus ), - - disableSelection: (function() { - var eventType = "onselectstart" in document.createElement( "div" ) ? - "selectstart" : - "mousedown"; - - return function() { - return this.bind( eventType + ".ui-disableSelection", function( event ) { - event.preventDefault(); - }); - }; - })(), - - enableSelection: function() { - return this.unbind( ".ui-disableSelection" ); - }, - - zIndex: function( zIndex ) { - if ( zIndex !== undefined ) { - return this.css( "zIndex", zIndex ); - } - - if ( this.length ) { - var elem = $( this[ 0 ] ), position, value; - while ( elem.length && elem[ 0 ] !== document ) { - // Ignore z-index if position is set to a value where z-index is ignored by the browser - // This makes behavior of this function consistent across browsers - // WebKit always returns auto if the element is positioned - position = elem.css( "position" ); - if ( position === "absolute" || position === "relative" || position === "fixed" ) { - // IE returns 0 when zIndex is not specified - // other browsers return a string - // we ignore the case of nested elements with an explicit value of 0 - //
- value = parseInt( elem.css( "zIndex" ), 10 ); - if ( !isNaN( value ) && value !== 0 ) { - return value; - } - } - elem = elem.parent(); - } - } - - return 0; - } -}); - -// $.ui.plugin is deprecated. Use $.widget() extensions instead. -$.ui.plugin = { - add: function( module, option, set ) { - var i, - proto = $.ui[ module ].prototype; - for ( i in set ) { - proto.plugins[ i ] = proto.plugins[ i ] || []; - proto.plugins[ i ].push( [ option, set[ i ] ] ); - } - }, - call: function( instance, name, args, allowDisconnected ) { - var i, - set = instance.plugins[ name ]; - - if ( !set ) { - return; - } - - if ( !allowDisconnected && ( !instance.element[ 0 ].parentNode || instance.element[ 0 ].parentNode.nodeType === 11 ) ) { - return; - } - - for ( i = 0; i < set.length; i++ ) { - if ( instance.options[ set[ i ][ 0 ] ] ) { - set[ i ][ 1 ].apply( instance.element, args ); - } - } - } -}; - - -/*! - * jQuery UI Widget 1.11.4 - * http://jqueryui.com - * - * Copyright jQuery Foundation and other contributors - * Released under the MIT license. - * http://jquery.org/license - * - * http://api.jqueryui.com/jQuery.widget/ - */ - - -var widget_uuid = 0, - widget_slice = Array.prototype.slice; - -$.cleanData = (function( orig ) { - return function( elems ) { - var events, elem, i; - for ( i = 0; (elem = elems[i]) != null; i++ ) { - try { - - // Only trigger remove when necessary to save time - events = $._data( elem, "events" ); - if ( events && events.remove ) { - $( elem ).triggerHandler( "remove" ); - } - - // http://bugs.jquery.com/ticket/8235 - } catch ( e ) {} - } - orig( elems ); - }; -})( $.cleanData ); - -$.widget = function( name, base, prototype ) { - var fullName, existingConstructor, constructor, basePrototype, - // proxiedPrototype allows the provided prototype to remain unmodified - // so that it can be used as a mixin for multiple widgets (#8876) - proxiedPrototype = {}, - namespace = name.split( "." )[ 0 ]; - - name = name.split( "." )[ 1 ]; - fullName = namespace + "-" + name; - - if ( !prototype ) { - prototype = base; - base = $.Widget; - } - - // create selector for plugin - $.expr[ ":" ][ fullName.toLowerCase() ] = function( elem ) { - return !!$.data( elem, fullName ); - }; - - $[ namespace ] = $[ namespace ] || {}; - existingConstructor = $[ namespace ][ name ]; - constructor = $[ namespace ][ name ] = function( options, element ) { - // allow instantiation without "new" keyword - if ( !this._createWidget ) { - return new constructor( options, element ); - } - - // allow instantiation without initializing for simple inheritance - // must use "new" keyword (the code above always passes args) - if ( arguments.length ) { - this._createWidget( options, element ); - } - }; - // extend with the existing constructor to carry over any static properties - $.extend( constructor, existingConstructor, { - version: prototype.version, - // copy the object used to create the prototype in case we need to - // redefine the widget later - _proto: $.extend( {}, prototype ), - // track widgets that inherit from this widget in case this widget is - // redefined after a widget inherits from it - _childConstructors: [] - }); - - basePrototype = new base(); - // we need to make the options hash a property directly on the new instance - // otherwise we'll modify the options hash on the prototype that we're - // inheriting from - basePrototype.options = $.widget.extend( {}, basePrototype.options ); - $.each( prototype, function( prop, value ) { - if ( !$.isFunction( value ) ) { - proxiedPrototype[ prop ] = value; - return; - } - proxiedPrototype[ prop ] = (function() { - var _super = function() { - return base.prototype[ prop ].apply( this, arguments ); - }, - _superApply = function( args ) { - return base.prototype[ prop ].apply( this, args ); - }; - return function() { - var __super = this._super, - __superApply = this._superApply, - returnValue; - - this._super = _super; - this._superApply = _superApply; - - returnValue = value.apply( this, arguments ); - - this._super = __super; - this._superApply = __superApply; - - return returnValue; - }; - })(); - }); - constructor.prototype = $.widget.extend( basePrototype, { - // TODO: remove support for widgetEventPrefix - // always use the name + a colon as the prefix, e.g., draggable:start - // don't prefix for widgets that aren't DOM-based - widgetEventPrefix: existingConstructor ? (basePrototype.widgetEventPrefix || name) : name - }, proxiedPrototype, { - constructor: constructor, - namespace: namespace, - widgetName: name, - widgetFullName: fullName - }); - - // If this widget is being redefined then we need to find all widgets that - // are inheriting from it and redefine all of them so that they inherit from - // the new version of this widget. We're essentially trying to replace one - // level in the prototype chain. - if ( existingConstructor ) { - $.each( existingConstructor._childConstructors, function( i, child ) { - var childPrototype = child.prototype; - - // redefine the child widget using the same prototype that was - // originally used, but inherit from the new version of the base - $.widget( childPrototype.namespace + "." + childPrototype.widgetName, constructor, child._proto ); - }); - // remove the list of existing child constructors from the old constructor - // so the old child constructors can be garbage collected - delete existingConstructor._childConstructors; - } else { - base._childConstructors.push( constructor ); - } - - $.widget.bridge( name, constructor ); - - return constructor; -}; - -$.widget.extend = function( target ) { - var input = widget_slice.call( arguments, 1 ), - inputIndex = 0, - inputLength = input.length, - key, - value; - for ( ; inputIndex < inputLength; inputIndex++ ) { - for ( key in input[ inputIndex ] ) { - value = input[ inputIndex ][ key ]; - if ( input[ inputIndex ].hasOwnProperty( key ) && value !== undefined ) { - // Clone objects - if ( $.isPlainObject( value ) ) { - target[ key ] = $.isPlainObject( target[ key ] ) ? - $.widget.extend( {}, target[ key ], value ) : - // Don't extend strings, arrays, etc. with objects - $.widget.extend( {}, value ); - // Copy everything else by reference - } else { - target[ key ] = value; - } - } - } - } - return target; -}; - -$.widget.bridge = function( name, object ) { - var fullName = object.prototype.widgetFullName || name; - $.fn[ name ] = function( options ) { - var isMethodCall = typeof options === "string", - args = widget_slice.call( arguments, 1 ), - returnValue = this; - - if ( isMethodCall ) { - this.each(function() { - var methodValue, - instance = $.data( this, fullName ); - if ( options === "instance" ) { - returnValue = instance; - return false; - } - if ( !instance ) { - return $.error( "cannot call methods on " + name + " prior to initialization; " + - "attempted to call method '" + options + "'" ); - } - if ( !$.isFunction( instance[options] ) || options.charAt( 0 ) === "_" ) { - return $.error( "no such method '" + options + "' for " + name + " widget instance" ); - } - methodValue = instance[ options ].apply( instance, args ); - if ( methodValue !== instance && methodValue !== undefined ) { - returnValue = methodValue && methodValue.jquery ? - returnValue.pushStack( methodValue.get() ) : - methodValue; - return false; - } - }); - } else { - - // Allow multiple hashes to be passed on init - if ( args.length ) { - options = $.widget.extend.apply( null, [ options ].concat(args) ); - } - - this.each(function() { - var instance = $.data( this, fullName ); - if ( instance ) { - instance.option( options || {} ); - if ( instance._init ) { - instance._init(); - } - } else { - $.data( this, fullName, new object( options, this ) ); - } - }); - } - - return returnValue; - }; -}; - -$.Widget = function( /* options, element */ ) {}; -$.Widget._childConstructors = []; - -$.Widget.prototype = { - widgetName: "widget", - widgetEventPrefix: "", - defaultElement: "
", - options: { - disabled: false, - - // callbacks - create: null - }, - _createWidget: function( options, element ) { - element = $( element || this.defaultElement || this )[ 0 ]; - this.element = $( element ); - this.uuid = widget_uuid++; - this.eventNamespace = "." + this.widgetName + this.uuid; - - this.bindings = $(); - this.hoverable = $(); - this.focusable = $(); - - if ( element !== this ) { - $.data( element, this.widgetFullName, this ); - this._on( true, this.element, { - remove: function( event ) { - if ( event.target === element ) { - this.destroy(); - } - } - }); - this.document = $( element.style ? - // element within the document - element.ownerDocument : - // element is window or document - element.document || element ); - this.window = $( this.document[0].defaultView || this.document[0].parentWindow ); - } - - this.options = $.widget.extend( {}, - this.options, - this._getCreateOptions(), - options ); - - this._create(); - this._trigger( "create", null, this._getCreateEventData() ); - this._init(); - }, - _getCreateOptions: $.noop, - _getCreateEventData: $.noop, - _create: $.noop, - _init: $.noop, - - destroy: function() { - this._destroy(); - // we can probably remove the unbind calls in 2.0 - // all event bindings should go through this._on() - this.element - .unbind( this.eventNamespace ) - .removeData( this.widgetFullName ) - // support: jquery <1.6.3 - // http://bugs.jquery.com/ticket/9413 - .removeData( $.camelCase( this.widgetFullName ) ); - this.widget() - .unbind( this.eventNamespace ) - .removeAttr( "aria-disabled" ) - .removeClass( - this.widgetFullName + "-disabled " + - "ui-state-disabled" ); - - // clean up events and states - this.bindings.unbind( this.eventNamespace ); - this.hoverable.removeClass( "ui-state-hover" ); - this.focusable.removeClass( "ui-state-focus" ); - }, - _destroy: $.noop, - - widget: function() { - return this.element; - }, - - option: function( key, value ) { - var options = key, - parts, - curOption, - i; - - if ( arguments.length === 0 ) { - // don't return a reference to the internal hash - return $.widget.extend( {}, this.options ); - } - - if ( typeof key === "string" ) { - // handle nested keys, e.g., "foo.bar" => { foo: { bar: ___ } } - options = {}; - parts = key.split( "." ); - key = parts.shift(); - if ( parts.length ) { - curOption = options[ key ] = $.widget.extend( {}, this.options[ key ] ); - for ( i = 0; i < parts.length - 1; i++ ) { - curOption[ parts[ i ] ] = curOption[ parts[ i ] ] || {}; - curOption = curOption[ parts[ i ] ]; - } - key = parts.pop(); - if ( arguments.length === 1 ) { - return curOption[ key ] === undefined ? null : curOption[ key ]; - } - curOption[ key ] = value; - } else { - if ( arguments.length === 1 ) { - return this.options[ key ] === undefined ? null : this.options[ key ]; - } - options[ key ] = value; - } - } - - this._setOptions( options ); - - return this; - }, - _setOptions: function( options ) { - var key; - - for ( key in options ) { - this._setOption( key, options[ key ] ); - } - - return this; - }, - _setOption: function( key, value ) { - this.options[ key ] = value; - - if ( key === "disabled" ) { - this.widget() - .toggleClass( this.widgetFullName + "-disabled", !!value ); - - // If the widget is becoming disabled, then nothing is interactive - if ( value ) { - this.hoverable.removeClass( "ui-state-hover" ); - this.focusable.removeClass( "ui-state-focus" ); - } - } - - return this; - }, - - enable: function() { - return this._setOptions({ disabled: false }); - }, - disable: function() { - return this._setOptions({ disabled: true }); - }, - - _on: function( suppressDisabledCheck, element, handlers ) { - var delegateElement, - instance = this; - - // no suppressDisabledCheck flag, shuffle arguments - if ( typeof suppressDisabledCheck !== "boolean" ) { - handlers = element; - element = suppressDisabledCheck; - suppressDisabledCheck = false; - } - - // no element argument, shuffle and use this.element - if ( !handlers ) { - handlers = element; - element = this.element; - delegateElement = this.widget(); - } else { - element = delegateElement = $( element ); - this.bindings = this.bindings.add( element ); - } - - $.each( handlers, function( event, handler ) { - function handlerProxy() { - // allow widgets to customize the disabled handling - // - disabled as an array instead of boolean - // - disabled class as method for disabling individual parts - if ( !suppressDisabledCheck && - ( instance.options.disabled === true || - $( this ).hasClass( "ui-state-disabled" ) ) ) { - return; - } - return ( typeof handler === "string" ? instance[ handler ] : handler ) - .apply( instance, arguments ); - } - - // copy the guid so direct unbinding works - if ( typeof handler !== "string" ) { - handlerProxy.guid = handler.guid = - handler.guid || handlerProxy.guid || $.guid++; - } - - var match = event.match( /^([\w:-]*)\s*(.*)$/ ), - eventName = match[1] + instance.eventNamespace, - selector = match[2]; - if ( selector ) { - delegateElement.delegate( selector, eventName, handlerProxy ); - } else { - element.bind( eventName, handlerProxy ); - } - }); - }, - - _off: function( element, eventName ) { - eventName = (eventName || "").split( " " ).join( this.eventNamespace + " " ) + - this.eventNamespace; - element.unbind( eventName ).undelegate( eventName ); - - // Clear the stack to avoid memory leaks (#10056) - this.bindings = $( this.bindings.not( element ).get() ); - this.focusable = $( this.focusable.not( element ).get() ); - this.hoverable = $( this.hoverable.not( element ).get() ); - }, - - _delay: function( handler, delay ) { - function handlerProxy() { - return ( typeof handler === "string" ? instance[ handler ] : handler ) - .apply( instance, arguments ); - } - var instance = this; - return setTimeout( handlerProxy, delay || 0 ); - }, - - _hoverable: function( element ) { - this.hoverable = this.hoverable.add( element ); - this._on( element, { - mouseenter: function( event ) { - $( event.currentTarget ).addClass( "ui-state-hover" ); - }, - mouseleave: function( event ) { - $( event.currentTarget ).removeClass( "ui-state-hover" ); - } - }); - }, - - _focusable: function( element ) { - this.focusable = this.focusable.add( element ); - this._on( element, { - focusin: function( event ) { - $( event.currentTarget ).addClass( "ui-state-focus" ); - }, - focusout: function( event ) { - $( event.currentTarget ).removeClass( "ui-state-focus" ); - } - }); - }, - - _trigger: function( type, event, data ) { - var prop, orig, - callback = this.options[ type ]; - - data = data || {}; - event = $.Event( event ); - event.type = ( type === this.widgetEventPrefix ? - type : - this.widgetEventPrefix + type ).toLowerCase(); - // the original event may come from any element - // so we need to reset the target on the new event - event.target = this.element[ 0 ]; - - // copy original event properties over to the new event - orig = event.originalEvent; - if ( orig ) { - for ( prop in orig ) { - if ( !( prop in event ) ) { - event[ prop ] = orig[ prop ]; - } - } - } - - this.element.trigger( event, data ); - return !( $.isFunction( callback ) && - callback.apply( this.element[0], [ event ].concat( data ) ) === false || - event.isDefaultPrevented() ); - } -}; - -$.each( { show: "fadeIn", hide: "fadeOut" }, function( method, defaultEffect ) { - $.Widget.prototype[ "_" + method ] = function( element, options, callback ) { - if ( typeof options === "string" ) { - options = { effect: options }; - } - var hasOptions, - effectName = !options ? - method : - options === true || typeof options === "number" ? - defaultEffect : - options.effect || defaultEffect; - options = options || {}; - if ( typeof options === "number" ) { - options = { duration: options }; - } - hasOptions = !$.isEmptyObject( options ); - options.complete = callback; - if ( options.delay ) { - element.delay( options.delay ); - } - if ( hasOptions && $.effects && $.effects.effect[ effectName ] ) { - element[ method ]( options ); - } else if ( effectName !== method && element[ effectName ] ) { - element[ effectName ]( options.duration, options.easing, callback ); - } else { - element.queue(function( next ) { - $( this )[ method ](); - if ( callback ) { - callback.call( element[ 0 ] ); - } - next(); - }); - } - }; -}); - -var widget = $.widget; - - -/*! - * jQuery UI Mouse 1.11.4 - * http://jqueryui.com - * - * Copyright jQuery Foundation and other contributors - * Released under the MIT license. - * http://jquery.org/license - * - * http://api.jqueryui.com/mouse/ - */ - - -var mouseHandled = false; -$( document ).mouseup( function() { - mouseHandled = false; -}); - -var mouse = $.widget("ui.mouse", { - version: "1.11.4", - options: { - cancel: "input,textarea,button,select,option", - distance: 1, - delay: 0 - }, - _mouseInit: function() { - var that = this; - - this.element - .bind("mousedown." + this.widgetName, function(event) { - return that._mouseDown(event); - }) - .bind("click." + this.widgetName, function(event) { - if (true === $.data(event.target, that.widgetName + ".preventClickEvent")) { - $.removeData(event.target, that.widgetName + ".preventClickEvent"); - event.stopImmediatePropagation(); - return false; - } - }); - - this.started = false; - }, - - // TODO: make sure destroying one instance of mouse doesn't mess with - // other instances of mouse - _mouseDestroy: function() { - this.element.unbind("." + this.widgetName); - if ( this._mouseMoveDelegate ) { - this.document - .unbind("mousemove." + this.widgetName, this._mouseMoveDelegate) - .unbind("mouseup." + this.widgetName, this._mouseUpDelegate); - } - }, - - _mouseDown: function(event) { - // don't let more than one widget handle mouseStart - if ( mouseHandled ) { - return; - } - - this._mouseMoved = false; - - // we may have missed mouseup (out of window) - (this._mouseStarted && this._mouseUp(event)); - - this._mouseDownEvent = event; - - var that = this, - btnIsLeft = (event.which === 1), - // event.target.nodeName works around a bug in IE 8 with - // disabled inputs (#7620) - elIsCancel = (typeof this.options.cancel === "string" && event.target.nodeName ? $(event.target).closest(this.options.cancel).length : false); - if (!btnIsLeft || elIsCancel || !this._mouseCapture(event)) { - return true; - } - - this.mouseDelayMet = !this.options.delay; - if (!this.mouseDelayMet) { - this._mouseDelayTimer = setTimeout(function() { - that.mouseDelayMet = true; - }, this.options.delay); - } - - if (this._mouseDistanceMet(event) && this._mouseDelayMet(event)) { - this._mouseStarted = (this._mouseStart(event) !== false); - if (!this._mouseStarted) { - event.preventDefault(); - return true; - } - } - - // Click event may never have fired (Gecko & Opera) - if (true === $.data(event.target, this.widgetName + ".preventClickEvent")) { - $.removeData(event.target, this.widgetName + ".preventClickEvent"); - } - - // these delegates are required to keep context - this._mouseMoveDelegate = function(event) { - return that._mouseMove(event); - }; - this._mouseUpDelegate = function(event) { - return that._mouseUp(event); - }; - - this.document - .bind( "mousemove." + this.widgetName, this._mouseMoveDelegate ) - .bind( "mouseup." + this.widgetName, this._mouseUpDelegate ); - - event.preventDefault(); - - mouseHandled = true; - return true; - }, - - _mouseMove: function(event) { - // Only check for mouseups outside the document if you've moved inside the document - // at least once. This prevents the firing of mouseup in the case of IE<9, which will - // fire a mousemove event if content is placed under the cursor. See #7778 - // Support: IE <9 - if ( this._mouseMoved ) { - // IE mouseup check - mouseup happened when mouse was out of window - if ($.ui.ie && ( !document.documentMode || document.documentMode < 9 ) && !event.button) { - return this._mouseUp(event); - - // Iframe mouseup check - mouseup occurred in another document - } else if ( !event.which ) { - return this._mouseUp( event ); - } - } - - if ( event.which || event.button ) { - this._mouseMoved = true; - } - - if (this._mouseStarted) { - this._mouseDrag(event); - return event.preventDefault(); - } - - if (this._mouseDistanceMet(event) && this._mouseDelayMet(event)) { - this._mouseStarted = - (this._mouseStart(this._mouseDownEvent, event) !== false); - (this._mouseStarted ? this._mouseDrag(event) : this._mouseUp(event)); - } - - return !this._mouseStarted; - }, - - _mouseUp: function(event) { - this.document - .unbind( "mousemove." + this.widgetName, this._mouseMoveDelegate ) - .unbind( "mouseup." + this.widgetName, this._mouseUpDelegate ); - - if (this._mouseStarted) { - this._mouseStarted = false; - - if (event.target === this._mouseDownEvent.target) { - $.data(event.target, this.widgetName + ".preventClickEvent", true); - } - - this._mouseStop(event); - } - - mouseHandled = false; - return false; - }, - - _mouseDistanceMet: function(event) { - return (Math.max( - Math.abs(this._mouseDownEvent.pageX - event.pageX), - Math.abs(this._mouseDownEvent.pageY - event.pageY) - ) >= this.options.distance - ); - }, - - _mouseDelayMet: function(/* event */) { - return this.mouseDelayMet; - }, - - // These are placeholder methods, to be overriden by extending plugin - _mouseStart: function(/* event */) {}, - _mouseDrag: function(/* event */) {}, - _mouseStop: function(/* event */) {}, - _mouseCapture: function(/* event */) { return true; } -}); - - -/*! - * jQuery UI Draggable 1.11.4 - * http://jqueryui.com - * - * Copyright jQuery Foundation and other contributors - * Released under the MIT license. - * http://jquery.org/license - * - * http://api.jqueryui.com/draggable/ - */ - - -$.widget("ui.draggable", $.ui.mouse, { - version: "1.11.4", - widgetEventPrefix: "drag", - options: { - addClasses: true, - appendTo: "parent", - axis: false, - connectToSortable: false, - containment: false, - cursor: "auto", - cursorAt: false, - grid: false, - handle: false, - helper: "original", - iframeFix: false, - opacity: false, - refreshPositions: false, - revert: false, - revertDuration: 500, - scope: "default", - scroll: true, - scrollSensitivity: 20, - scrollSpeed: 20, - snap: false, - snapMode: "both", - snapTolerance: 20, - stack: false, - zIndex: false, - - // callbacks - drag: null, - start: null, - stop: null - }, - _create: function() { - - if ( this.options.helper === "original" ) { - this._setPositionRelative(); - } - if (this.options.addClasses){ - this.element.addClass("ui-draggable"); - } - if (this.options.disabled){ - this.element.addClass("ui-draggable-disabled"); - } - this._setHandleClassName(); - - this._mouseInit(); - }, - - _setOption: function( key, value ) { - this._super( key, value ); - if ( key === "handle" ) { - this._removeHandleClassName(); - this._setHandleClassName(); - } - }, - - _destroy: function() { - if ( ( this.helper || this.element ).is( ".ui-draggable-dragging" ) ) { - this.destroyOnClear = true; - return; - } - this.element.removeClass( "ui-draggable ui-draggable-dragging ui-draggable-disabled" ); - this._removeHandleClassName(); - this._mouseDestroy(); - }, - - _mouseCapture: function(event) { - var o = this.options; - - this._blurActiveElement( event ); - - // among others, prevent a drag on a resizable-handle - if (this.helper || o.disabled || $(event.target).closest(".ui-resizable-handle").length > 0) { - return false; - } - - //Quit if we're not on a valid handle - this.handle = this._getHandle(event); - if (!this.handle) { - return false; - } - - this._blockFrames( o.iframeFix === true ? "iframe" : o.iframeFix ); - - return true; - - }, - - _blockFrames: function( selector ) { - this.iframeBlocks = this.document.find( selector ).map(function() { - var iframe = $( this ); - - return $( "
" ) - .css( "position", "absolute" ) - .appendTo( iframe.parent() ) - .outerWidth( iframe.outerWidth() ) - .outerHeight( iframe.outerHeight() ) - .offset( iframe.offset() )[ 0 ]; - }); - }, - - _unblockFrames: function() { - if ( this.iframeBlocks ) { - this.iframeBlocks.remove(); - delete this.iframeBlocks; - } - }, - - _blurActiveElement: function( event ) { - var document = this.document[ 0 ]; - - // Only need to blur if the event occurred on the draggable itself, see #10527 - if ( !this.handleElement.is( event.target ) ) { - return; - } - - // support: IE9 - // IE9 throws an "Unspecified error" accessing document.activeElement from an