소스 검색

Merge pull request #15129 from overleaf/mj-jpa-angular-parameters

[web] Explicitly name angular parameters

GitOrigin-RevId: 91beae68989d6c8122132b531a4338b116d87424
Jakob Ackermann 3 년 전
부모
커밋
e959529828
61개의 변경된 파일과 1747개의 추가작업 그리고 1537개의 파일을 삭제
  1. 23 15
      services/web/frontend/js/base.js
  2. 155 150
      services/web/frontend/js/directives/asyncForm.js
  3. 54 48
      services/web/frontend/js/directives/bookmarkableTabset.js
  4. 61 56
      services/web/frontend/js/directives/eventTracking.js
  5. 56 44
      services/web/frontend/js/directives/focus.js
  6. 1 1
      services/web/frontend/js/directives/mathjax.js
  7. 43 40
      services/web/frontend/js/directives/scroll.js
  8. 2 2
      services/web/frontend/js/directives/stopPropagation.js
  9. 19 16
      services/web/frontend/js/directives/videoPlayState.js
  10. 8 4
      services/web/frontend/js/features/editor-navigation-toolbar/controllers/editor-navigation-toolbar-controller.js
  11. 7 10
      services/web/frontend/js/features/file-tree/controllers/file-tree-controller.js
  12. 5 4
      services/web/frontend/js/features/file-view/controllers/file-view-controller.js
  13. 36 31
      services/web/frontend/js/features/outline/controllers/outline-controller.js
  14. 6 4
      services/web/frontend/js/features/share-project-modal/controllers/react-share-project-modal-controller.js
  15. 26 17
      services/web/frontend/js/ide.js
  16. 249 243
      services/web/frontend/js/ide/directives/layout.js
  17. 121 117
      services/web/frontend/js/ide/directives/verticalResizablePanes.js
  18. 6 5
      services/web/frontend/js/ide/editor/controllers/SavingNotificationController.js
  19. 16 7
      services/web/frontend/js/ide/editor/directives/aceEditor.js
  20. 15 12
      services/web/frontend/js/ide/file-tree/controllers/FileTreeController.js
  21. 22 19
      services/web/frontend/js/ide/files/services/files.js
  22. 26 23
      services/web/frontend/js/ide/graphics/services/graphics.js
  23. 2 2
      services/web/frontend/js/ide/history/components/historyEntriesList.js
  24. 2 2
      services/web/frontend/js/ide/history/components/historyEntry.js
  25. 2 2
      services/web/frontend/js/ide/history/components/historyFileEntity.js
  26. 2 2
      services/web/frontend/js/ide/history/components/historyFileTree.js
  27. 1 1
      services/web/frontend/js/ide/history/components/historyLabel.js
  28. 2 2
      services/web/frontend/js/ide/history/components/historyLabelsList.js
  29. 7 4
      services/web/frontend/js/ide/history/controllers/HistoryV2AddLabelModalController.js
  30. 7 4
      services/web/frontend/js/ide/history/controllers/HistoryV2DeleteLabelModalController.js
  31. 5 4
      services/web/frontend/js/ide/history/controllers/HistoryV2FileTreeController.js
  32. 6 4
      services/web/frontend/js/ide/history/controllers/HistoryV2ListController.js
  33. 9 5
      services/web/frontend/js/ide/history/controllers/HistoryV2ToolbarController.js
  34. 97 91
      services/web/frontend/js/ide/metadata/services/metadata.js
  35. 7 4
      services/web/frontend/js/ide/review-panel/controllers/BulkActionsModalController.js
  36. 11 4
      services/web/frontend/js/ide/review-panel/controllers/ReviewPanelController.js
  37. 7 4
      services/web/frontend/js/ide/review-panel/controllers/TrackChangesUpgradeModalController.js
  38. 54 51
      services/web/frontend/js/ide/review-panel/directives/aggregateChangeEntry.js
  39. 41 38
      services/web/frontend/js/ide/review-panel/directives/changeEntry.js
  40. 71 68
      services/web/frontend/js/ide/review-panel/directives/commentEntry.js
  41. 22 19
      services/web/frontend/js/ide/review-panel/directives/reviewPanelCollapseHeight.js
  42. 1 1
      services/web/frontend/js/ide/review-panel/directives/reviewPanelSorted.js
  43. 25 12
      services/web/frontend/js/ide/services/ide.js
  44. 18 14
      services/web/frontend/js/ide/toolbar/EditorLoaderController.js
  45. 13 9
      services/web/frontend/js/ide/toolbar/EditorToolbarController.js
  46. 14 11
      services/web/frontend/js/main.js
  47. 16 10
      services/web/frontend/js/main/account-upgrade-angular.js
  48. 6 5
      services/web/frontend/js/main/annual-upgrade.js
  49. 15 10
      services/web/frontend/js/main/bonus.js
  50. 5 4
      services/web/frontend/js/main/clear-sessions.js
  51. 78 74
      services/web/frontend/js/main/event.js
  52. 7 4
      services/web/frontend/js/main/importing.js
  53. 6 5
      services/web/frontend/js/main/learn.js
  54. 8 5
      services/web/frontend/js/main/scribtex-popup.js
  55. 50 46
      services/web/frontend/js/main/subscription/team-invite-controller.js
  56. 36 32
      services/web/frontend/js/main/system-messages.js
  57. 7 5
      services/web/frontend/js/main/token-access.js
  58. 6 4
      services/web/frontend/js/main/translations.js
  59. 36 27
      services/web/frontend/js/modules/errorCatcher.js
  60. 57 52
      services/web/frontend/js/services/queued-http.js
  61. 31 28
      services/web/frontend/js/services/wait-for.js

+ 23 - 15
services/web/frontend/js/base.js

@@ -37,23 +37,31 @@ const App = angular
     'sessionStorage',
     'sessionStorage',
     'ui.select',
     'ui.select',
   ])
   ])
-  .config(function ($qProvider, $httpProvider, uiSelectConfig) {
-    $qProvider.errorOnUnhandledRejections(false)
-    uiSelectConfig.spinnerClass = 'fa fa-refresh ui-select-spin'
+  .config([
+    '$qProvider',
+    'uiSelectConfig',
+    function ($qProvider, uiSelectConfig) {
+      $qProvider.errorOnUnhandledRejections(false)
+      uiSelectConfig.spinnerClass = 'fa fa-refresh ui-select-spin'
 
 
-    configureMathJax()
-  })
+      configureMathJax()
+    },
+  ])
 
 
-App.run(($rootScope, $templateCache) => {
-  $rootScope.usersEmail = getMeta('ol-usersEmail')
+App.run([
+  '$rootScope',
+  '$templateCache',
+  function ($rootScope, $templateCache) {
+    $rootScope.usersEmail = getMeta('ol-usersEmail')
 
 
-  // UI Select templates are hard-coded and use Glyphicon icons (which we don't import).
-  // The line below simply overrides the hard-coded template with our own, which is
-  // basically the same but using Font Awesome icons.
-  $templateCache.put(
-    'bootstrap/match.tpl.html',
-    '<div class="ui-select-match" ng-hide="$select.open && $select.searchEnabled" ng-disabled="$select.disabled" ng-class="{\'btn-default-focus\':$select.focus}"><span tabindex="-1" class="btn btn-default form-control ui-select-toggle" aria-label="{{ $select.baseTitle }} activate" ng-disabled="$select.disabled" ng-click="$select.activate()" style="outline: 0;"><span ng-show="$select.isEmpty()" class="ui-select-placeholder text-muted">{{$select.placeholder}}</span> <span ng-hide="$select.isEmpty()" class="ui-select-match-text pull-left" ng-class="{\'ui-select-allow-clear\': $select.allowClear && !$select.isEmpty()}" ng-transclude=""></span> <i class="caret pull-right" ng-click="$select.toggle($event)"></i> <a ng-show="$select.allowClear && !$select.isEmpty() && ($select.disabled !== true)" aria-label="{{ $select.baseTitle }} clear" style="margin-right: 10px" ng-click="$select.clear($event)" class="btn btn-xs btn-link pull-right"><i class="fa fa-times" aria-hidden="true"></i></a></span></div>'
-  )
-})
+    // UI Select templates are hard-coded and use Glyphicon icons (which we don't import).
+    // The line below simply overrides the hard-coded template with our own, which is
+    // basically the same but using Font Awesome icons.
+    $templateCache.put(
+      'bootstrap/match.tpl.html',
+      '<div class="ui-select-match" ng-hide="$select.open && $select.searchEnabled" ng-disabled="$select.disabled" ng-class="{\'btn-default-focus\':$select.focus}"><span tabindex="-1" class="btn btn-default form-control ui-select-toggle" aria-label="{{ $select.baseTitle }} activate" ng-disabled="$select.disabled" ng-click="$select.activate()" style="outline: 0;"><span ng-show="$select.isEmpty()" class="ui-select-placeholder text-muted">{{$select.placeholder}}</span> <span ng-hide="$select.isEmpty()" class="ui-select-match-text pull-left" ng-class="{\'ui-select-allow-clear\': $select.allowClear && !$select.isEmpty()}" ng-transclude=""></span> <i class="caret pull-right" ng-click="$select.toggle($event)"></i> <a ng-show="$select.allowClear && !$select.isEmpty() && ($select.disabled !== true)" aria-label="{{ $select.baseTitle }} clear" style="margin-right: 10px" ng-click="$select.clear($event)" class="btn btn-xs btn-link pull-right"><i class="fa fa-times" aria-hidden="true"></i></a></span></div>'
+    )
+  },
+])
 
 
 export default App
 export default App

+ 155 - 150
services/web/frontend/js/directives/asyncForm.js

@@ -1,171 +1,176 @@
 import App from '../base'
 import App from '../base'
 import '../vendor/libs/passfield'
 import '../vendor/libs/passfield'
-App.directive('asyncForm', ($http, validateCaptcha, validateCaptchaV3) => ({
-  controller: [
-    '$scope',
-    '$location',
-    function ($scope, $location) {
-      this.getEmail = () => $scope.email
-      this.getEmailFromQuery = () =>
-        $location.search().email || $location.search().new_email
-      return this
-    },
-  ],
-  link(scope, element, attrs, ctrl) {
-    let response
-    const formName = attrs.asyncForm
-
-    scope[attrs.name].response = response = {}
-    scope[attrs.name].inflight = false
-    scope.email =
-      scope.email ||
-      scope.usersEmail ||
-      ctrl.getEmailFromQuery() ||
-      attrs.newEmail
-
-    const validateCaptchaIfEnabled = function (callback) {
-      scope.$applyAsync(() => {
-        scope[attrs.name].inflight = true
-      })
+App.directive('asyncForm', [
+  '$http',
+  'validateCaptcha',
+  'validateCaptchaV3',
+  ($http, validateCaptcha, validateCaptchaV3) => ({
+    controller: [
+      '$scope',
+      '$location',
+      function ($scope, $location) {
+        this.getEmail = () => $scope.email
+        this.getEmailFromQuery = () =>
+          $location.search().email || $location.search().new_email
+        return this
+      },
+    ],
+    link(scope, element, attrs, ctrl) {
+      let response
+      const formName = attrs.asyncForm
+
+      scope[attrs.name].response = response = {}
+      scope[attrs.name].inflight = false
+      scope.email =
+        scope.email ||
+        scope.usersEmail ||
+        ctrl.getEmailFromQuery() ||
+        attrs.newEmail
+
+      const validateCaptchaIfEnabled = function (callback) {
+        scope.$applyAsync(() => {
+          scope[attrs.name].inflight = true
+        })
 
 
-      if (attrs.captchaActionName) {
-        validateCaptchaV3(attrs.captchaActionName)
+        if (attrs.captchaActionName) {
+          validateCaptchaV3(attrs.captchaActionName)
+        }
+        if (attrs.captcha != null) {
+          validateCaptcha(callback)
+        } else {
+          callback()
+        }
       }
       }
-      if (attrs.captcha != null) {
-        validateCaptcha(callback)
-      } else {
-        callback()
-      }
-    }
 
 
-    const _submitRequest = function (grecaptchaResponse) {
-      const formData = {}
-      for (const data of Array.from(element.serializeArray())) {
-        formData[data.name] = data.value
-      }
+      const _submitRequest = function (grecaptchaResponse) {
+        const formData = {}
+        for (const data of Array.from(element.serializeArray())) {
+          formData[data.name] = data.value
+        }
 
 
-      if (grecaptchaResponse) {
-        formData['g-recaptcha-response'] = grecaptchaResponse
-      }
+        if (grecaptchaResponse) {
+          formData['g-recaptcha-response'] = grecaptchaResponse
+        }
 
 
-      // clear the response object which may be referenced downstream
-      Object.keys(response).forEach(field => delete response[field])
+        // clear the response object which may be referenced downstream
+        Object.keys(response).forEach(field => delete response[field])
 
 
-      // for asyncForm prevent automatic redirect to /login if
-      // authentication fails, we will handle it ourselves
-      const httpRequestFn = _httpRequestFn(element.attr('method'))
-      return httpRequestFn(element.attr('action'), formData, {
-        disableAutoLoginRedirect: true,
-      })
-        .then(function (httpResponse) {
-          const { data, headers } = httpResponse
-          scope[attrs.name].inflight = false
-          response.success = true
-          response.error = false
-
-          const onSuccessHandler = scope[attrs.onSuccess]
-          if (onSuccessHandler) {
-            onSuccessHandler(httpResponse)
-            return
-          }
-
-          if (data.redir) {
-            ga('send', 'event', formName, 'success')
-            return (window.location = data.redir)
-          } else if (data.message) {
-            response.message = data.message
-
-            if (data.message.type === 'error') {
-              response.success = false
-              response.error = true
-              return ga('send', 'event', formName, 'failure', data.message)
-            } else {
-              return ga('send', 'event', formName, 'success')
-            }
-          } else if (scope.$eval(attrs.asyncFormDownloadResponse)) {
-            const blob = new Blob([data], {
-              type: headers('Content-Type'),
-            })
-            location.href = URL.createObjectURL(blob) // Trigger file save
-          }
+        // for asyncForm prevent automatic redirect to /login if
+        // authentication fails, we will handle it ourselves
+        const httpRequestFn = _httpRequestFn(element.attr('method'))
+        return httpRequestFn(element.attr('action'), formData, {
+          disableAutoLoginRedirect: true,
         })
         })
-        .catch(function (httpResponse) {
-          const { data, status } = httpResponse
-          scope[attrs.name].inflight = false
-          response.success = false
-          response.error = true
-          response.status = status
-          response.data = data
-
-          const onErrorHandler = scope[attrs.onError]
-          if (onErrorHandler) {
-            onErrorHandler(httpResponse)
-            return
-          }
-
-          let responseMessage
-          if (data.message && data.message.text) {
-            responseMessage = data.message.text
-          } else {
-            responseMessage = data.message
-          }
-
-          if (status === 400) {
-            // Bad Request
-            response.message = {
-              text:
-                responseMessage ||
-                'Invalid Request. Please correct the data and try again.',
-              type: 'error',
+          .then(function (httpResponse) {
+            const { data, headers } = httpResponse
+            scope[attrs.name].inflight = false
+            response.success = true
+            response.error = false
+
+            const onSuccessHandler = scope[attrs.onSuccess]
+            if (onSuccessHandler) {
+              onSuccessHandler(httpResponse)
+              return
+            }
+
+            if (data.redir) {
+              ga('send', 'event', formName, 'success')
+              return (window.location = data.redir)
+            } else if (data.message) {
+              response.message = data.message
+
+              if (data.message.type === 'error') {
+                response.success = false
+                response.error = true
+                return ga('send', 'event', formName, 'failure', data.message)
+              } else {
+                return ga('send', 'event', formName, 'success')
+              }
+            } else if (scope.$eval(attrs.asyncFormDownloadResponse)) {
+              const blob = new Blob([data], {
+                type: headers('Content-Type'),
+              })
+              location.href = URL.createObjectURL(blob) // Trigger file save
             }
             }
-          } else if (status === 403) {
-            // Forbidden
-            response.message = {
-              text:
-                responseMessage ||
-                'Session error. Please check you have cookies enabled. If the problem persists, try clearing your cache and cookies.',
-              type: 'error',
+          })
+          .catch(function (httpResponse) {
+            const { data, status } = httpResponse
+            scope[attrs.name].inflight = false
+            response.success = false
+            response.error = true
+            response.status = status
+            response.data = data
+
+            const onErrorHandler = scope[attrs.onError]
+            if (onErrorHandler) {
+              onErrorHandler(httpResponse)
+              return
             }
             }
-          } else if (status === 429) {
-            response.message = {
-              text:
-                responseMessage ||
-                'Too many attempts. Please wait for a while and try again.',
-              type: 'error',
+
+            let responseMessage
+            if (data.message && data.message.text) {
+              responseMessage = data.message.text
+            } else {
+              responseMessage = data.message
             }
             }
-          } else {
-            response.message = {
-              text:
-                responseMessage ||
-                'Something went wrong talking to the server :(. Please try again.',
-              type: 'error',
+
+            if (status === 400) {
+              // Bad Request
+              response.message = {
+                text:
+                  responseMessage ||
+                  'Invalid Request. Please correct the data and try again.',
+                type: 'error',
+              }
+            } else if (status === 403) {
+              // Forbidden
+              response.message = {
+                text:
+                  responseMessage ||
+                  'Session error. Please check you have cookies enabled. If the problem persists, try clearing your cache and cookies.',
+                type: 'error',
+              }
+            } else if (status === 429) {
+              response.message = {
+                text:
+                  responseMessage ||
+                  'Too many attempts. Please wait for a while and try again.',
+                type: 'error',
+              }
+            } else {
+              response.message = {
+                text:
+                  responseMessage ||
+                  'Something went wrong talking to the server :(. Please try again.',
+                type: 'error',
+              }
             }
             }
-          }
-          ga('send', 'event', formName, 'failure', data.message)
-        })
-    }
+            ga('send', 'event', formName, 'failure', data.message)
+          })
+      }
 
 
-    const submit = () =>
-      validateCaptchaIfEnabled(response => _submitRequest(response))
+      const submit = () =>
+        validateCaptchaIfEnabled(response => _submitRequest(response))
 
 
-    const _httpRequestFn = (method = 'post') => {
-      const $HTTP_FNS = {
-        post: $http.post,
-        get: $http.get,
+      const _httpRequestFn = (method = 'post') => {
+        const $HTTP_FNS = {
+          post: $http.post,
+          get: $http.get,
+        }
+        return $HTTP_FNS[method.toLowerCase()]
       }
       }
-      return $HTTP_FNS[method.toLowerCase()]
-    }
 
 
-    element.on('submit', function (e) {
-      e.preventDefault()
-      submit()
-    })
+      element.on('submit', function (e) {
+        e.preventDefault()
+        submit()
+      })
 
 
-    if (attrs.autoSubmit) {
-      submit()
-    }
-  },
-}))
+      if (attrs.autoSubmit) {
+        submit()
+      }
+    },
+  }),
+])
 
 
 App.directive('formMessages', () => ({
 App.directive('formMessages', () => ({
   restrict: 'E',
   restrict: 'E',

+ 54 - 48
services/web/frontend/js/directives/bookmarkableTabset.js

@@ -1,58 +1,64 @@
 import _ from 'lodash'
 import _ from 'lodash'
 import App from '../base'
 import App from '../base'
-App.directive('bookmarkableTabset', $location => ({
-  restrict: 'A',
-  require: 'tabset',
-  link(scope, el, attrs, tabset) {
-    const _makeActive = function (hash) {
-      if (hash && hash !== '') {
-        const matchingTab = _.find(
-          tabset.tabs,
-          tab => tab.bookmarkableTabId === hash
-        )
-        if (matchingTab) {
-          matchingTab.select()
-          return el.children()[0].scrollIntoView({ behavior: 'smooth' })
+App.directive('bookmarkableTabset', [
+  '$location',
+  $location => ({
+    restrict: 'A',
+    require: 'tabset',
+    link(scope, el, attrs, tabset) {
+      const _makeActive = function (hash) {
+        if (hash && hash !== '') {
+          const matchingTab = _.find(
+            tabset.tabs,
+            tab => tab.bookmarkableTabId === hash
+          )
+          if (matchingTab) {
+            matchingTab.select()
+            return el.children()[0].scrollIntoView({ behavior: 'smooth' })
+          }
         }
         }
       }
       }
-    }
 
 
-    scope.$applyAsync(function () {
-      // for page load
-      const hash = $location.hash()
-      _makeActive(hash)
-
-      // for links within page to a tab
-      // this needs to be within applyAsync because there could be a link
-      // within a tab to another tab
-      const linksToTabs = document.querySelectorAll('.link-to-tab')
-      const _clickLinkToTab = event => {
-        const hash = event.currentTarget.getAttribute('href').split('#').pop()
+      scope.$applyAsync(function () {
+        // for page load
+        const hash = $location.hash()
         _makeActive(hash)
         _makeActive(hash)
-      }
 
 
-      if (linksToTabs) {
-        Array.from(linksToTabs).map(link =>
-          link.addEventListener('click', _clickLinkToTab)
-        )
-      }
-    })
-  },
-}))
+        // for links within page to a tab
+        // this needs to be within applyAsync because there could be a link
+        // within a tab to another tab
+        const linksToTabs = document.querySelectorAll('.link-to-tab')
+        const _clickLinkToTab = event => {
+          const hash = event.currentTarget.getAttribute('href').split('#').pop()
+          _makeActive(hash)
+        }
 
 
-App.directive('bookmarkableTab', $location => ({
-  restrict: 'A',
-  require: 'tab',
-  link(scope, el, attrs, tab) {
-    const tabScope = el.isolateScope()
-    const tabId = attrs.bookmarkableTab
-    if (tabScope && tabId && tabId !== '') {
-      tabScope.bookmarkableTabId = tabId
-      tabScope.$watch('active', function (isActive, wasActive) {
-        if (isActive && !wasActive && $location.hash() !== tabId) {
-          return $location.hash(tabId)
+        if (linksToTabs) {
+          Array.from(linksToTabs).map(link =>
+            link.addEventListener('click', _clickLinkToTab)
+          )
         }
         }
       })
       })
-    }
-  },
-}))
+    },
+  }),
+])
+
+App.directive('bookmarkableTab', [
+  '$location',
+  $location => ({
+    restrict: 'A',
+    require: 'tab',
+    link(scope, el, attrs, tab) {
+      const tabScope = el.isolateScope()
+      const tabId = attrs.bookmarkableTab
+      if (tabScope && tabId && tabId !== '') {
+        tabScope.bookmarkableTabId = tabId
+        tabScope.$watch('active', function (isActive, wasActive) {
+          if (isActive && !wasActive && $location.hash() !== tabId) {
+            return $location.hash(tabId)
+          }
+        })
+      }
+    },
+  }),
+])

+ 61 - 56
services/web/frontend/js/directives/eventTracking.js

@@ -61,65 +61,70 @@ const isInViewport = function (element) {
   return elBtm > viewportTop && elTop < viewportBtm
   return elBtm > viewportTop && elTop < viewportBtm
 }
 }
 
 
-export default App.directive('eventTracking', eventTracking => ({
-  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
+export default App.directive('eventTracking', [
+  'eventTracking',
+  eventTracking => ({
+    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) {
-      /*
+      const sendEvent = function (scrollEvent) {
+        /*
                       @param {boolean}		scrollEvent		Use to unbind scroll event
                       @param {boolean}		scrollEvent		Use to unbind scroll event
                   */
                   */
-      if (sendMB) {
-        eventTracking[sendMBFunction](scope.eventTracking, segmentation)
+        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 (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)
+      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)
+        )
       }
       }
-      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)
-      )
-    }
-  },
-}))
+    },
+  }),
+])

+ 56 - 44
services/web/frontend/js/directives/focus.js

@@ -10,52 +10,61 @@
  * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  */
  */
 import App from '../base'
 import App from '../base'
-App.directive('focusWhen', $timeout => ({
-  restrict: 'A',
-  link(scope, element, attr) {
-    return scope.$watch(attr.focusWhen, function (value) {
-      if (value) {
-        return $timeout(() => element.focus())
-      }
-    })
-  },
-}))
+App.directive('focusWhen', [
+  '$timeout',
+  $timeout => ({
+    restrict: 'A',
+    link(scope, element, attr) {
+      return scope.$watch(attr.focusWhen, function (value) {
+        if (value) {
+          return $timeout(() => element.focus())
+        }
+      })
+    },
+  }),
+])
 
 
-App.directive('focusOn', $timeout => ({
+App.directive('focusOn', () => ({
   restrict: 'A',
   restrict: 'A',
   link(scope, element, attrs) {
   link(scope, element, attrs) {
     return scope.$on(attrs.focusOn, () => element.focus())
     return scope.$on(attrs.focusOn, () => element.focus())
   },
   },
 }))
 }))
 
 
-App.directive('selectWhen', $timeout => ({
-  restrict: 'A',
-  link(scope, element, attr) {
-    return scope.$watch(attr.selectWhen, function (value) {
-      if (value) {
-        return $timeout(() => element.select())
-      }
-    })
-  },
-}))
+App.directive('selectWhen', [
+  '$timeout',
+  $timeout => ({
+    restrict: 'A',
+    link(scope, element, attr) {
+      return scope.$watch(attr.selectWhen, function (value) {
+        if (value) {
+          return $timeout(() => element.select())
+        }
+      })
+    },
+  }),
+])
 
 
-App.directive('selectOn', $timeout => ({
+App.directive('selectOn', () => ({
   restrict: 'A',
   restrict: 'A',
   link(scope, element, attrs) {
   link(scope, element, attrs) {
     return scope.$on(attrs.selectOn, () => element.select())
     return scope.$on(attrs.selectOn, () => element.select())
   },
   },
 }))
 }))
 
 
-App.directive('selectNameWhen', $timeout => ({
-  restrict: 'A',
-  link(scope, element, attrs) {
-    return scope.$watch(attrs.selectNameWhen, function (value) {
-      if (value) {
-        return $timeout(() => selectName(element))
-      }
-    })
-  },
-}))
+App.directive('selectNameWhen', [
+  '$timeout',
+  $timeout => ({
+    restrict: 'A',
+    link(scope, element, attrs) {
+      return scope.$watch(attrs.selectNameWhen, function (value) {
+        if (value) {
+          return $timeout(() => selectName(element))
+        }
+      })
+    },
+  }),
+])
 
 
 App.directive('selectNameOn', () => ({
 App.directive('selectNameOn', () => ({
   restrict: 'A',
   restrict: 'A',
@@ -64,19 +73,22 @@ App.directive('selectNameOn', () => ({
   },
   },
 }))
 }))
 
 
-App.directive('focus', $timeout => ({
-  scope: {
-    trigger: '@focus',
-  },
+App.directive('focus', [
+  '$timeout',
+  $timeout => ({
+    scope: {
+      trigger: '@focus',
+    },
 
 
-  link(scope, element) {
-    return scope.$watch('trigger', function (value) {
-      if (value === 'true') {
-        return $timeout(() => element[0].focus())
-      }
-    })
-  },
-}))
+    link(scope, element) {
+      return scope.$watch('trigger', function (value) {
+        if (value === 'true') {
+          return $timeout(() => element[0].focus())
+        }
+      })
+    },
+  }),
+])
 
 
 function selectName(element) {
 function selectName(element) {
   // Select up to last '.'. I.e. everything except the file extension
   // Select up to last '.'. I.e. everything except the file extension

+ 1 - 1
services/web/frontend/js/directives/mathjax.js

@@ -2,7 +2,7 @@
 
 
 import App from '../base'
 import App from '../base'
 
 
-export default App.directive('mathjax', function ($compile, $parse) {
+export default App.directive('mathjax', function () {
   return {
   return {
     link(scope, element, attrs) {
     link(scope, element, attrs) {
       if (!(MathJax && MathJax.Hub)) return
       if (!(MathJax && MathJax.Hub)) return

+ 43 - 40
services/web/frontend/js/directives/scroll.js

@@ -11,46 +11,49 @@
  */
  */
 import App from '../base'
 import App from '../base'
 
 
-export default App.directive('updateScrollBottomOn', $timeout => ({
-  restrict: 'A',
-  link(scope, element, attrs, ctrls) {
-    // We keep the offset from the bottom fixed whenever the event fires
-    //
-    // ^   | ^
-    // |   | | scrollTop
-    // |   | v
-    // |   |-----------
-    // |   | ^
-    // |   | |
-    // |   | | clientHeight (viewable area)
-    // |   | |
-    // |   | |
-    // |   | v
-    // |   |-----------
-    // |   | ^
-    // |   | | scrollBottom
-    // v   | v
-    //  \
-    //   scrollHeight
+export default App.directive('updateScrollBottomOn', [
+  '$timeout',
+  $timeout => ({
+    restrict: 'A',
+    link(scope, element, attrs, ctrls) {
+      // We keep the offset from the bottom fixed whenever the event fires
+      //
+      // ^   | ^
+      // |   | | scrollTop
+      // |   | v
+      // |   |-----------
+      // |   | ^
+      // |   | |
+      // |   | | clientHeight (viewable area)
+      // |   | |
+      // |   | |
+      // |   | v
+      // |   |-----------
+      // |   | ^
+      // |   | | scrollBottom
+      // v   | v
+      //  \
+      //   scrollHeight
 
 
-    let scrollBottom = 0
-    element.on(
-      'scroll',
-      e =>
-        (scrollBottom =
-          element[0].scrollHeight -
-          element[0].scrollTop -
-          element[0].clientHeight)
-    )
+      let scrollBottom = 0
+      element.on(
+        'scroll',
+        e =>
+          (scrollBottom =
+            element[0].scrollHeight -
+            element[0].scrollTop -
+            element[0].clientHeight)
+      )
 
 
-    return scope.$on(attrs.updateScrollBottomOn, () =>
-      $timeout(
-        () =>
-          element.scrollTop(
-            element[0].scrollHeight - element[0].clientHeight - scrollBottom
-          ),
-        0
+      return scope.$on(attrs.updateScrollBottomOn, () =>
+        $timeout(
+          () =>
+            element.scrollTop(
+              element[0].scrollHeight - element[0].clientHeight - scrollBottom
+            ),
+          0
+        )
       )
       )
-    )
-  },
-}))
+    },
+  }),
+])

+ 2 - 2
services/web/frontend/js/directives/stopPropagation.js

@@ -9,14 +9,14 @@
  * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  */
  */
 import App from '../base'
 import App from '../base'
-App.directive('stopPropagation', $http => ({
+App.directive('stopPropagation', () => ({
   restrict: 'A',
   restrict: 'A',
   link(scope, element, attrs) {
   link(scope, element, attrs) {
     return element.bind(attrs.stopPropagation, e => e.stopPropagation())
     return element.bind(attrs.stopPropagation, e => e.stopPropagation())
   },
   },
 }))
 }))
 
 
-export default App.directive('preventDefault', $http => ({
+export default App.directive('preventDefault', () => ({
   restrict: 'A',
   restrict: 'A',
   link(scope, element, attrs) {
   link(scope, element, attrs) {
     return element.bind(attrs.preventDefault, e => e.preventDefault())
     return element.bind(attrs.preventDefault, e => e.preventDefault())

+ 19 - 16
services/web/frontend/js/directives/videoPlayState.js

@@ -10,20 +10,23 @@
  */
  */
 import App from '../base'
 import App from '../base'
 
 
-export default App.directive('videoPlayState', $parse => ({
-  restrict: 'A',
-  link(scope, element, attrs) {
-    const videoDOMEl = element[0]
-    return scope.$watch(
-      () => $parse(attrs.videoPlayState)(scope),
-      function (shouldPlay) {
-        if (shouldPlay) {
-          videoDOMEl.currentTime = 0
-          return videoDOMEl.play()
-        } else {
-          return videoDOMEl.pause()
+export default App.directive('videoPlayState', [
+  '$parse',
+  $parse => ({
+    restrict: 'A',
+    link(scope, element, attrs) {
+      const videoDOMEl = element[0]
+      return scope.$watch(
+        () => $parse(attrs.videoPlayState)(scope),
+        function (shouldPlay) {
+          if (shouldPlay) {
+            videoDOMEl.currentTime = 0
+            return videoDOMEl.play()
+          } else {
+            return videoDOMEl.pause()
+          }
         }
         }
-      }
-    )
-  },
-}))
+      )
+    },
+  }),
+])

+ 8 - 4
services/web/frontend/js/features/editor-navigation-toolbar/controllers/editor-navigation-toolbar-controller.js

@@ -3,10 +3,14 @@ import { react2angular } from 'react2angular'
 import EditorNavigationToolbarRoot from '../components/editor-navigation-toolbar-root'
 import EditorNavigationToolbarRoot from '../components/editor-navigation-toolbar-root'
 import { rootContext } from '../../../shared/context/root-context'
 import { rootContext } from '../../../shared/context/root-context'
 
 
-App.controller('EditorNavigationToolbarController', function ($scope, ide) {
-  // wrapper is required to avoid scope problems with `this` inside `EditorManager`
-  $scope.openDoc = (doc, args) => ide.editorManager.openDoc(doc, args)
-})
+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(
 App.component(
   'editorNavigationToolbarRoot',
   'editorNavigationToolbarRoot',

+ 7 - 10
services/web/frontend/js/features/file-tree/controllers/file-tree-controller.js

@@ -5,14 +5,11 @@ import { cloneDeep } from 'lodash'
 import FileTreeRoot from '../components/file-tree-root'
 import FileTreeRoot from '../components/file-tree-root'
 import { rootContext } from '../../../shared/context/root-context'
 import { rootContext } from '../../../shared/context/root-context'
 
 
-App.controller(
-  'ReactFileTreeController',
-  function (
-    $scope,
-    $timeout,
-    ide
-    // eventTracking
-  ) {
+App.controller('ReactFileTreeController', [
+  '$scope',
+  '$timeout',
+  'ide',
+  function ($scope, $timeout, ide) {
     $scope.isConnected = true
     $scope.isConnected = true
 
 
     $scope.$on('project:joined', () => {
     $scope.$on('project:joined', () => {
@@ -108,8 +105,8 @@ App.controller(
     $scope.reindexReferences = () => {
     $scope.reindexReferences = () => {
       ide.$scope.$emit('references:should-reindex', {})
       ide.$scope.$emit('references:should-reindex', {})
     }
     }
-  }
-)
+  },
+])
 
 
 App.component(
 App.component(
   'fileTreeRoot',
   'fileTreeRoot',

+ 5 - 4
services/web/frontend/js/features/file-view/controllers/file-view-controller.js

@@ -5,8 +5,9 @@ import _ from 'lodash'
 import { rootContext } from '../../../shared/context/root-context'
 import { rootContext } from '../../../shared/context/root-context'
 import FileView from '../components/file-view'
 import FileView from '../components/file-view'
 
 
-export default App.controller(
-  'FileViewController',
+export default App.controller('FileViewController', [
+  '$scope',
+  '$rootScope',
   function ($scope, $rootScope) {
   function ($scope, $rootScope) {
     $scope.file = $scope.openFile
     $scope.file = $scope.openFile
 
 
@@ -14,8 +15,8 @@ export default App.controller(
       const oldKeys = $rootScope._references.keys
       const oldKeys = $rootScope._references.keys
       return ($rootScope._references.keys = _.union(oldKeys, newKeys))
       return ($rootScope._references.keys = _.union(oldKeys, newKeys))
     }
     }
-  }
-)
+  },
+])
 
 
 App.component(
 App.component(
   'fileView',
   'fileView',

+ 36 - 31
services/web/frontend/js/features/outline/controllers/outline-controller.js

@@ -3,42 +3,47 @@ import OutlinePane from '../components/outline-pane'
 import { react2angular } from 'react2angular'
 import { react2angular } from 'react2angular'
 import { rootContext } from '../../../shared/context/root-context'
 import { rootContext } from '../../../shared/context/root-context'
 
 
-App.controller('OutlineController', function ($scope, ide, eventTracking) {
-  $scope.isTexFile = false
-  $scope.outline = []
-  $scope.eventTracking = eventTracking
+App.controller('OutlineController', [
+  '$scope',
+  'ide',
+  'eventTracking',
+  function ($scope, ide, eventTracking) {
+    $scope.isTexFile = false
+    $scope.outline = []
+    $scope.eventTracking = eventTracking
+
+    function shouldShowOutline() {
+      return !$scope.editor.newSourceEditor
+    }
 
 
-  function shouldShowOutline() {
-    return !$scope.editor.newSourceEditor
-  }
-
-  $scope.show = shouldShowOutline()
-
-  $scope.$watch('editor.newSourceEditor', function () {
     $scope.show = shouldShowOutline()
     $scope.show = shouldShowOutline()
-  })
-
-  $scope.$on('outline-manager:outline-changed', onOutlineChange)
 
 
-  function onOutlineChange(e, outlineInfo) {
-    $scope.$applyAsync(() => {
-      $scope.isTexFile = outlineInfo.isTexFile
-      $scope.outline = outlineInfo.outline
-      $scope.highlightedLine = outlineInfo.highlightedLine
+    $scope.$watch('editor.newSourceEditor', function () {
+      $scope.show = shouldShowOutline()
     })
     })
-  }
 
 
-  $scope.jumpToLine = (lineNo, syncToPdf) => {
-    ide.outlineManager.jumpToLine(lineNo, syncToPdf)
-    eventTracking.sendMB('outline-jump-to-line')
-  }
-
-  $scope.onToggle = isOpen => {
-    $scope.$applyAsync(() => {
-      $scope.$emit('outline-toggled', isOpen)
-    })
-  }
-})
+    $scope.$on('outline-manager:outline-changed', onOutlineChange)
+
+    function onOutlineChange(e, outlineInfo) {
+      $scope.$applyAsync(() => {
+        $scope.isTexFile = outlineInfo.isTexFile
+        $scope.outline = outlineInfo.outline
+        $scope.highlightedLine = outlineInfo.highlightedLine
+      })
+    }
+
+    $scope.jumpToLine = (lineNo, syncToPdf) => {
+      ide.outlineManager.jumpToLine(lineNo, syncToPdf)
+      eventTracking.sendMB('outline-jump-to-line')
+    }
+
+    $scope.onToggle = isOpen => {
+      $scope.$applyAsync(() => {
+        $scope.$emit('outline-toggled', isOpen)
+      })
+    }
+  },
+])
 
 
 // Wrap React component as Angular component. Only needed for "top-level" component
 // Wrap React component as Angular component. Only needed for "top-level" component
 App.component(
 App.component(

+ 6 - 4
services/web/frontend/js/features/share-project-modal/controllers/react-share-project-modal-controller.js

@@ -14,8 +14,10 @@ App.component(
   )
   )
 )
 )
 
 
-export default App.controller(
-  'ReactShareProjectModalController',
+export default App.controller('ReactShareProjectModalController', [
+  '$scope',
+  'eventTracking',
+  'ide',
   function ($scope, eventTracking, ide) {
   function ($scope, eventTracking, ide) {
     $scope.show = false
     $scope.show = false
 
 
@@ -61,5 +63,5 @@ export default App.controller(
           })
           })
       }
       }
     })
     })
-  }
-)
+  },
+])

+ 26 - 17
services/web/frontend/js/ide.js

@@ -70,8 +70,15 @@ import { reportCM6Perf } from './infrastructure/cm6-performance'
 import { reportAcePerf } from './ide/editor/ace-performance'
 import { reportAcePerf } from './ide/editor/ace-performance'
 import { debugConsole } from '@/utils/debugging'
 import { debugConsole } from '@/utils/debugging'
 
 
-App.controller(
-  'IdeController',
+App.controller('IdeController', [
+  '$scope',
+  '$timeout',
+  'ide',
+  'localStorage',
+  'eventTracking',
+  'metadata',
+  'CobrandingDataService',
+  '$window',
   function (
   function (
     $scope,
     $scope,
     $timeout,
     $timeout,
@@ -79,7 +86,6 @@ App.controller(
     localStorage,
     localStorage,
     eventTracking,
     eventTracking,
     metadata,
     metadata,
-    $q,
     CobrandingDataService,
     CobrandingDataService,
     $window
     $window
   ) {
   ) {
@@ -478,23 +484,26 @@ If the project has been renamed please look in your project list for a new proje
         return $scope.$digest()
         return $scope.$digest()
       }
       }
     })
     })
-  }
-)
+  },
+])
 
 
 cleanupServiceWorker()
 cleanupServiceWorker()
 
 
-angular.module('SharelatexApp').config(function ($provide) {
-  $provide.decorator('$browser', [
-    '$delegate',
-    function ($delegate) {
-      $delegate.onUrlChange = function () {}
-      $delegate.url = function () {
-        return ''
-      }
-      return $delegate
-    },
-  ])
-})
+angular.module('SharelatexApp').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, ['SharelatexApp'])
 export default angular.bootstrap(document.body, ['SharelatexApp'])
 
 

+ 249 - 243
services/web/frontend/js/ide/directives/layout.js

@@ -17,196 +17,201 @@ import _ from 'lodash'
 import '../../vendor/libs/jquery-layout'
 import '../../vendor/libs/jquery-layout'
 import '../../vendor/libs/jquery.ui.touch-punch'
 import '../../vendor/libs/jquery.ui.touch-punch'
 
 
-export default App.directive('layout', ($parse, $compile, ide) => ({
-  compile() {
-    return {
-      pre(scope, element, attrs) {
-        let customTogglerEl, spacingClosed, spacingOpen, state
-        const name = attrs.layout
+export default App.directive('layout', [
+  '$parse',
+  '$compile',
+  'ide',
+  ($parse, $compile, ide) => ({
+    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
+          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.spacingOpen != null) {
+            spacingOpen = parseInt(attrs.spacingOpen, 10)
+          } else {
+            spacingOpen = 7
+          }
 
 
-        if (attrs.spacingClosed != null) {
-          spacingClosed = parseInt(attrs.spacingClosed, 10)
-        } else {
-          spacingClosed = 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),
-          },
-        }
+          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
+          // 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
             }
             }
-            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
+            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
             }
             }
-            // 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'
+          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 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 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)
+          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()
+          // Someone moved the resizer
+          function onInternalResize() {
+            state = layout.readState()
+            scope.$broadcast(`layout:${name}:resize`, state)
+            repositionControls()
+            if (hasCustomToggler) {
+              repositionCustomToggler()
+            }
+            return resetOpenStates()
           }
           }
-          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)
+          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
               }
               }
-              return
             }
             }
-          }
 
 
-          ide.$timeout(() => {
-            layout.resizeAll()
-          })
-        }
+            ide.$timeout(() => {
+              layout.resizeAll()
+            })
+          }
 
 
-        const layout = element.layout(options)
-        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 (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()
+          if (hasCustomToggler) {
+            state = layout.readState()
+            const customTogglerScope = scope.$new()
 
 
-          customTogglerScope.isOpen = true
-          customTogglerScope.isVisible = true
+            customTogglerScope.isOpen = true
+            customTogglerScope.isVisible = true
 
 
-          if (
-            (state[customTogglerPane] != null
-              ? state[customTogglerPane].initClosed
-              : undefined) === true
-          ) {
-            customTogglerScope.isOpen = false
-          }
+            if (
+              (state[customTogglerPane] != null
+                ? state[customTogglerPane].initClosed
+                : undefined) === true
+            ) {
+              customTogglerScope.isOpen = false
+            }
 
 
-          customTogglerScope.tooltipMsgWhenOpen = customTogglerMsgWhenOpen
-          customTogglerScope.tooltipMsgWhenClosed = customTogglerMsgWhenClosed
+            customTogglerScope.tooltipMsgWhenOpen = customTogglerMsgWhenOpen
+            customTogglerScope.tooltipMsgWhenClosed = customTogglerMsgWhenClosed
 
 
-          customTogglerScope.tooltipPlacement =
-            customTogglerPane === 'east' ? 'left' : 'right'
-          customTogglerScope.handleClick = function () {
-            layout.toggle(customTogglerPane)
-            return repositionCustomToggler()
-          }
-          customTogglerEl = $compile(`\
+            customTogglerScope.tooltipPlacement =
+              customTogglerPane === 'east' ? 'left' : 'right'
+            customTogglerScope.handleClick = function () {
+              layout.toggle(customTogglerPane)
+              return repositionCustomToggler()
+            }
+            customTogglerEl = $compile(`\
 <a href \
 <a href \
 ng-show=\"isVisible\" \
 ng-show=\"isVisible\" \
 class=\"custom-toggler ${`custom-toggler-${customTogglerPane}`}\" \
 class=\"custom-toggler ${`custom-toggler-${customTogglerPane}`}\" \
@@ -216,99 +221,100 @@ tooltip-placement=\"{{ tooltipPlacement }}\" \
 ng-click=\"handleClick()\" \
 ng-click=\"handleClick()\" \
 aria-label=\"{{ isOpen ? tooltipMsgWhenOpen : tooltipMsgWhenClosed }}\">\
 aria-label=\"{{ isOpen ? tooltipMsgWhenOpen : tooltipMsgWhenClosed }}\">\
 `)(customTogglerScope)
 `)(customTogglerScope)
-          element.append(customTogglerEl)
-        }
+            element.append(customTogglerEl)
+          }
 
 
-        function onPaneOpen(pane) {
-          if (!hasCustomToggler || pane !== customTogglerPane) {
-            return
+          function onPaneOpen(pane) {
+            if (!hasCustomToggler || pane !== customTogglerPane) {
+              return
+            }
+            return customTogglerEl
+              .scope()
+              .$applyAsync(() => (customTogglerEl.scope().isOpen = true))
           }
           }
-          return customTogglerEl
-            .scope()
-            .$applyAsync(() => (customTogglerEl.scope().isOpen = true))
-        }
 
 
-        function onPaneClose(pane) {
-          if (!hasCustomToggler || pane !== customTogglerPane) {
-            return
+          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)
+            })
           }
           }
-          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())
-        })
+          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)
+          }
 
 
-        // 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)
-        })
+          resetOpenStates()
+          onInternalResize()
 
 
-        if (attrs.openEast != null) {
-          scope.$watch(attrs.openEast, function (value, oldValue) {
-            if (value != null && value !== oldValue) {
+          if (attrs.layoutDisabled != null) {
+            return scope.$watch(attrs.layoutDisabled, function (value) {
               if (value) {
               if (value) {
-                layout.open('east')
+                layout.hide('east')
               } else {
               } else {
-                layout.close('east')
+                layout.show('east')
               }
               }
-              if (hasCustomToggler && customTogglerPane === 'east') {
-                repositionCustomToggler()
-                customTogglerEl.scope().$applyAsync(function () {
-                  customTogglerEl.scope().isOpen = value
+              if (hasCustomToggler) {
+                return customTogglerEl.scope().$applyAsync(function () {
+                  customTogglerEl.scope().isOpen = !value
+                  return (customTogglerEl.scope().isVisible = !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)
-      },
-    }
-  },
-}))
+        post(scope, element, attrs) {
+          const name = attrs.layout
+          const state = element.layout().readState()
+          return scope.$broadcast(`layout:${name}:linked`, state)
+        },
+      }
+    },
+  }),
+])

+ 121 - 117
services/web/frontend/js/ide/directives/verticalResizablePanes.js

@@ -1,129 +1,133 @@
 import App from '../../base'
 import App from '../../base'
 
 
-export default App.directive('verticalResizablePanes', (localStorage, ide) => ({
-  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)
+export default App.directive('verticalResizablePanes', [
+  'localStorage',
+  'ide',
+  (localStorage, ide) => ({
+    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)
+          }
+        })
       }
       }
-    }
 
 
-    function disableResizer() {
-      if (layoutHandle.resizers && layoutHandle.resizers.south) {
-        layoutHandle.resizers.south.addClass(resizerDisabledClass)
+      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%',
+        },
       }
       }
-    }
 
 
-    function handleDragEnd() {
-      manualResizeIncoming = true
-    }
+      const toggledExternally = attrs.verticalResizablePanesToggledExternallyOn
+      const hiddenExternally = attrs.verticalResizablePanesHiddenExternallyOn
+      const hiddenInitially = attrs.verticalResizablePanesHiddenInitially
+      const resizeOn = attrs.verticalResizablePanesResizeOn
+      const resizerDisabledClass = `${layoutOptions.south.resizerClass}-disabled`
 
 
-    function handleResize(paneName, paneEl, paneState) {
-      if (manualResizeIncoming) {
-        storedSize = paneState.size
+      function enableResizer() {
+        if (layoutHandle.resizers && layoutHandle.resizers.south) {
+          layoutHandle.resizers.south.removeClass(resizerDisabledClass)
+        }
       }
       }
-      manualResizeIncoming = false
-    }
-
-    if (toggledExternally) {
-      scope.$on(toggledExternally, (e, open) => {
-        if (open) {
-          enableResizer()
-          layoutHandle.sizePane('south', storedSize ?? defaultSize ?? 'auto')
-        } else {
-          disableResizer()
-          layoutHandle.sizePane('south', 'auto')
+
+      function disableResizer() {
+        if (layoutHandle.resizers && layoutHandle.resizers.south) {
+          layoutHandle.resizers.south.addClass(resizerDisabledClass)
         }
         }
-      })
-    }
-
-    if (hiddenExternally) {
-      ide.$scope.$on(hiddenExternally, (e, open) => {
-        if (open) {
-          layoutHandle.show('south')
-        } else {
-          layoutHandle.hide('south')
+      }
+
+      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 (resizeOn) {
-      ide.$scope.$on(resizeOn, () => {
-        ide.$timeout(() => {
-          layoutHandle.resizeAll()
+      if (hiddenExternally) {
+        ide.$scope.$on(hiddenExternally, (e, open) => {
+          if (open) {
+            layoutHandle.show('south')
+          } else {
+            layoutHandle.hide('south')
+          }
         })
         })
-      })
-    }
-
-    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')
-    }
-  },
-}))
+      }
+
+      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')
+      }
+    },
+  }),
+])

+ 6 - 5
services/web/frontend/js/ide/editor/controllers/SavingNotificationController.js

@@ -14,9 +14,10 @@
 import App from '../../../base'
 import App from '../../../base'
 import Document from '../Document'
 import Document from '../Document'
 
 
-export default App.controller(
-  'SavingNotificationController',
-  function ($scope, $interval, ide) {
+export default App.controller('SavingNotificationController', [
+  '$scope',
+  'ide',
+  function ($scope, ide) {
     let warnAboutUnsavedChanges
     let warnAboutUnsavedChanges
     setInterval(() => pollSavedStatus(), 1000)
     setInterval(() => pollSavedStatus(), 1000)
 
 
@@ -98,5 +99,5 @@ export default App.controller(
         return 'You have unsaved changes. If you leave now they will not be saved.'
         return 'You have unsaved changes. If you leave now they will not be saved.'
       }
       }
     })
     })
-  }
-)
+  },
+])

+ 16 - 7
services/web/frontend/js/ide/editor/directives/aceEditor.js

@@ -54,11 +54,21 @@ if (ace.config._moduleUrl == null) {
   }
   }
 }
 }
 
 
-App.directive(
-  'aceEditor',
+App.directive('aceEditor', [
+  'ide',
+  '$compile',
+  '$rootScope',
+  'eventTracking',
+  'localStorage',
+  '$cacheFactory',
+  'metadata',
+  'graphics',
+  'preamble',
+  'files',
+  '$http',
+  '$q',
   function (
   function (
     ide,
     ide,
-    $timeout,
     $compile,
     $compile,
     $rootScope,
     $rootScope,
     eventTracking,
     eventTracking,
@@ -69,8 +79,7 @@ App.directive(
     preamble,
     preamble,
     files,
     files,
     $http,
     $http,
-    $q,
-    $window
+    $q
   ) {
   ) {
     monkeyPatchSearch($rootScope, $compile)
     monkeyPatchSearch($rootScope, $compile)
 
 
@@ -951,8 +960,8 @@ App.directive(
 </div>\
 </div>\
 `,
 `,
     }
     }
-  }
-)
+  },
+])
 
 
 function monkeyPatchSearch($rootScope, $compile) {
 function monkeyPatchSearch($rootScope, $compile) {
   const searchHtml = `\
   const searchHtml = `\

+ 15 - 12
services/web/frontend/js/ide/file-tree/controllers/FileTreeController.js

@@ -1,15 +1,18 @@
 import App from '../../../base'
 import App from '../../../base'
-App.controller('FileTreeController', function ($scope) {
-  $scope.openNewDocModal = () => {
-    window.dispatchEvent(
-      new CustomEvent('file-tree.start-creating', { detail: { mode: 'doc' } })
-    )
-  }
+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'
+    $scope.orderByFoldersFirst = function (entity) {
+      if ((entity != null ? entity.type : undefined) === 'folder') {
+        return '0'
+      }
+      return '1'
     }
     }
-    return '1'
-  }
-})
+  },
+])

+ 22 - 19
services/web/frontend/js/ide/files/services/files.js

@@ -1,22 +1,25 @@
 import _ from 'lodash'
 import _ from 'lodash'
 import App from '../../../base'
 import App from '../../../base'
 
 
-export default App.factory('files', function (ide) {
-  const Files = {
-    getTeXFiles() {
-      const texFiles = []
-      ide.fileTreeManager.forEachEntity(function (entity, _folder, path) {
-        if (
-          entity.type === 'doc' &&
-          /.*\.(tex|md|txt|tikz)/.test(entity.name)
-        ) {
-          const cloned = _.clone(entity)
-          cloned.path = path
-          texFiles.push(cloned)
-        }
-      })
-      return texFiles
-    },
-  }
-  return Files
-})
+export default App.factory('files', [
+  'ide',
+  function (ide) {
+    const Files = {
+      getTeXFiles() {
+        const texFiles = []
+        ide.fileTreeManager.forEachEntity(function (entity, _folder, path) {
+          if (
+            entity.type === 'doc' &&
+            /.*\.(tex|md|txt|tikz)/.test(entity.name)
+          ) {
+            const cloned = _.clone(entity)
+            cloned.path = path
+            texFiles.push(cloned)
+          }
+        })
+        return texFiles
+      },
+    }
+    return Files
+  },
+])

+ 26 - 23
services/web/frontend/js/ide/graphics/services/graphics.js

@@ -13,30 +13,33 @@ import _ from 'lodash'
  */
  */
 import App from '../../../base'
 import App from '../../../base'
 
 
-export default App.factory('graphics', function (ide) {
-  const Graphics = {
-    getGraphicsFiles() {
-      const graphicsFiles = []
-      ide.fileTreeManager.forEachEntity(function (entity, folder, path) {
-        if (
-          entity.type === 'file' &&
-          __guardMethod__(
-            entity != null ? entity.name : undefined,
-            'match',
-            o => o.match(/.*\.(png|jpg|jpeg|pdf|eps)/i)
-          )
-        ) {
-          const cloned = _.clone(entity)
-          cloned.path = path
-          return graphicsFiles.push(cloned)
-        }
-      })
-      return graphicsFiles
-    },
-  }
+export default App.factory('graphics', [
+  'ide',
+  function (ide) {
+    const Graphics = {
+      getGraphicsFiles() {
+        const graphicsFiles = []
+        ide.fileTreeManager.forEachEntity(function (entity, folder, path) {
+          if (
+            entity.type === 'file' &&
+            __guardMethod__(
+              entity != null ? entity.name : undefined,
+              'match',
+              o => o.match(/.*\.(png|jpg|jpeg|pdf|eps)/i)
+            )
+          ) {
+            const cloned = _.clone(entity)
+            cloned.path = path
+            return graphicsFiles.push(cloned)
+          }
+        })
+        return graphicsFiles
+      },
+    }
 
 
-  return Graphics
-})
+    return Graphics
+  },
+])
 
 
 function __guardMethod__(obj, methodName, transform) {
 function __guardMethod__(obj, methodName, transform) {
   if (
   if (

+ 2 - 2
services/web/frontend/js/ide/history/components/historyEntriesList.js

@@ -10,7 +10,7 @@
  * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  */
  */
 import App from '../../../base'
 import App from '../../../base'
-const historyEntriesListController = function ($scope, $element, $attrs) {
+const historyEntriesListController = function ($scope, $element) {
   const ctrl = this
   const ctrl = this
   ctrl.$entryListViewportEl = null
   ctrl.$entryListViewportEl = null
   ctrl.isDragging = false
   ctrl.isDragging = false
@@ -133,6 +133,6 @@ export default App.component('historyEntriesList', {
     onRangeSelect: '&',
     onRangeSelect: '&',
     onLabelDelete: '&',
     onLabelDelete: '&',
   },
   },
-  controller: historyEntriesListController,
+  controller: ['$scope', '$element', historyEntriesListController],
   templateUrl: 'historyEntriesListTpl',
   templateUrl: 'historyEntriesListTpl',
 })
 })

+ 2 - 2
services/web/frontend/js/ide/history/components/historyEntry.js

@@ -13,7 +13,7 @@ import _ from 'lodash'
 import App from '../../../base'
 import App from '../../../base'
 import ColorManager from '../../colors/ColorManager'
 import ColorManager from '../../colors/ColorManager'
 import displayNameForUser from '../util/displayNameForUser'
 import displayNameForUser from '../util/displayNameForUser'
-const historyEntryController = function ($scope, $element, $attrs) {
+const historyEntryController = function ($scope, $element) {
   const ctrl = this
   const ctrl = this
   // This method (and maybe the one below) will be removed soon. User details data will be
   // This method (and maybe the one below) will be removed soon. User details data will be
   // injected into the history API responses, so we won't need to fetch user data from other
   // injected into the history API responses, so we won't need to fetch user data from other
@@ -120,6 +120,6 @@ export default App.component('historyEntry', {
   require: {
   require: {
     historyEntriesList: '^historyEntriesList',
     historyEntriesList: '^historyEntriesList',
   },
   },
-  controller: historyEntryController,
+  controller: ['$scope', '$element', historyEntryController],
   templateUrl: 'historyEntryTpl',
   templateUrl: 'historyEntryTpl',
 })
 })

+ 2 - 2
services/web/frontend/js/ide/history/components/historyFileEntity.js

@@ -12,7 +12,7 @@
 import App from '../../../base'
 import App from '../../../base'
 import iconTypeFromName from '../../file-tree/util/iconTypeFromName'
 import iconTypeFromName from '../../file-tree/util/iconTypeFromName'
 import fileOperationI18nNames from '../../file-tree/util/fileOperationI18nNames'
 import fileOperationI18nNames from '../../file-tree/util/fileOperationI18nNames'
-const historyFileEntityController = function ($scope, $element, $attrs) {
+const historyFileEntityController = function ($scope) {
   const ctrl = this
   const ctrl = this
   ctrl.hasOperation = false
   ctrl.hasOperation = false
   ctrl.getRenameTooltip = i18nRenamedStr => {
   ctrl.getRenameTooltip = i18nRenamedStr => {
@@ -99,6 +99,6 @@ export default App.component('historyFileEntity', {
   bindings: {
   bindings: {
     fileEntity: '<',
     fileEntity: '<',
   },
   },
-  controller: historyFileEntityController,
+  controller: ['$scope', historyFileEntityController],
   templateUrl: 'historyFileEntityTpl',
   templateUrl: 'historyFileEntityTpl',
 })
 })

+ 2 - 2
services/web/frontend/js/ide/history/components/historyFileTree.js

@@ -8,7 +8,7 @@ import _ from 'lodash'
  * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  */
  */
 import App from '../../../base'
 import App from '../../../base'
-const historyFileTreeController = function ($scope, $element, $attrs) {
+const historyFileTreeController = function ($scope) {
   const ctrl = this
   const ctrl = this
   ctrl.handleEntityClick = file => ctrl.onSelectedFileChange({ file })
   ctrl.handleEntityClick = file => ctrl.onSelectedFileChange({ file })
   ctrl._fileTree = []
   ctrl._fileTree = []
@@ -58,6 +58,6 @@ export default App.component('historyFileTree', {
     onSelectedFileChange: '&',
     onSelectedFileChange: '&',
     isLoading: '<',
     isLoading: '<',
   },
   },
-  controller: historyFileTreeController,
+  controller: ['$scope', historyFileTreeController],
   templateUrl: 'historyFileTreeTpl',
   templateUrl: 'historyFileTreeTpl',
 })
 })

+ 1 - 1
services/web/frontend/js/ide/history/components/historyLabel.js

@@ -10,7 +10,7 @@
  * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  */
  */
 import App from '../../../base'
 import App from '../../../base'
-const historyLabelController = function ($scope, $element, $attrs, $filter) {
+const historyLabelController = function () {
   const ctrl = this
   const ctrl = this
   ctrl.$onInit = () => {
   ctrl.$onInit = () => {
     if (ctrl.showTooltip == null) {
     if (ctrl.showTooltip == null) {

+ 2 - 2
services/web/frontend/js/ide/history/components/historyLabelsList.js

@@ -13,7 +13,7 @@ import _ from 'lodash'
 import App from '../../../base'
 import App from '../../../base'
 import ColorManager from '../../colors/ColorManager'
 import ColorManager from '../../colors/ColorManager'
 import displayNameForUser from '../util/displayNameForUser'
 import displayNameForUser from '../util/displayNameForUser'
-const historyLabelsListController = function ($scope, $element, $attrs) {
+const historyLabelsListController = function ($scope) {
   const ctrl = this
   const ctrl = this
   ctrl.isDragging = false
   ctrl.isDragging = false
   ctrl.versionsWithLabels = []
   ctrl.versionsWithLabels = []
@@ -170,6 +170,6 @@ export default App.component('historyLabelsList', {
     onRangeSelect: '&',
     onRangeSelect: '&',
     onLabelDelete: '&',
     onLabelDelete: '&',
   },
   },
-  controller: historyLabelsListController,
+  controller: ['$scope', historyLabelsListController],
   templateUrl: 'historyLabelsListTpl',
   templateUrl: 'historyLabelsListTpl',
 })
 })

+ 7 - 4
services/web/frontend/js/ide/history/controllers/HistoryV2AddLabelModalController.js

@@ -11,8 +11,11 @@
  */
  */
 import App from '../../../base'
 import App from '../../../base'
 
 
-export default App.controller(
-  'HistoryV2AddLabelModalController',
+export default App.controller('HistoryV2AddLabelModalController', [
+  '$scope',
+  '$modalInstance',
+  'ide',
+  'update',
   function ($scope, $modalInstance, ide, update) {
   function ($scope, $modalInstance, ide, update) {
     $scope.update = update
     $scope.update = update
     $scope.inputs = { labelName: null }
     $scope.inputs = { labelName: null }
@@ -43,5 +46,5 @@ export default App.controller(
           }
           }
         })
         })
     })
     })
-  }
-)
+  },
+])

+ 7 - 4
services/web/frontend/js/ide/history/controllers/HistoryV2DeleteLabelModalController.js

@@ -11,8 +11,11 @@
  */
  */
 import App from '../../../base'
 import App from '../../../base'
 
 
-export default App.controller(
-  'HistoryV2DeleteLabelModalController',
+export default App.controller('HistoryV2DeleteLabelModalController', [
+  '$scope',
+  '$modalInstance',
+  'ide',
+  'labelDetails',
   function ($scope, $modalInstance, ide, labelDetails) {
   function ($scope, $modalInstance, ide, labelDetails) {
     $scope.labelDetails = labelDetails
     $scope.labelDetails = labelDetails
     $scope.state = {
     $scope.state = {
@@ -38,5 +41,5 @@ export default App.controller(
           }
           }
         })
         })
     })
     })
-  }
-)
+  },
+])

+ 5 - 4
services/web/frontend/js/ide/history/controllers/HistoryV2FileTreeController.js

@@ -12,11 +12,12 @@
  */
  */
 import App from '../../../base'
 import App from '../../../base'
 
 
-export default App.controller(
-  'HistoryV2FileTreeController',
+export default App.controller('HistoryV2FileTreeController', [
+  '$scope',
+  'ide',
   function ($scope, ide) {
   function ($scope, ide) {
     $scope.handleFileSelection = file => {
     $scope.handleFileSelection = file => {
       ide.historyManager.selectFile(file)
       ide.historyManager.selectFile(file)
     }
     }
-  }
-)
+  },
+])

+ 6 - 4
services/web/frontend/js/ide/history/controllers/HistoryV2ListController.js

@@ -12,8 +12,10 @@
  */
  */
 import App from '../../../base'
 import App from '../../../base'
 
 
-export default App.controller(
-  'HistoryV2ListController',
+export default App.controller('HistoryV2ListController', [
+  '$scope',
+  '$modal',
+  'ide',
   function ($scope, $modal, ide) {
   function ($scope, $modal, ide) {
     $scope.hoveringOverListSelectors = false
     $scope.hoveringOverListSelectors = false
     $scope.listConfig = { showOnlyLabelled: false }
     $scope.listConfig = { showOnlyLabelled: false }
@@ -50,5 +52,5 @@ export default App.controller(
           },
           },
         },
         },
       }))
       }))
-  }
-)
+  },
+])

+ 9 - 5
services/web/frontend/js/ide/history/controllers/HistoryV2ToolbarController.js

@@ -13,9 +13,13 @@
 import App from '../../../base'
 import App from '../../../base'
 import { debugConsole } from '@/utils/debugging'
 import { debugConsole } from '@/utils/debugging'
 
 
-export default App.controller(
-  'HistoryV2ToolbarController',
-  ($scope, $modal, ide, eventTracking, waitFor) => {
+export default App.controller('HistoryV2ToolbarController', [
+  '$scope',
+  '$modal',
+  'ide',
+  'eventTracking',
+  'waitFor',
+  function ($scope, $modal, ide, eventTracking, waitFor) {
     $scope.currentUpdate = null
     $scope.currentUpdate = null
     $scope.currentLabel = null
     $scope.currentLabel = null
 
 
@@ -120,5 +124,5 @@ export default App.controller(
         })
         })
         .catch(debugConsole.error)
         .catch(debugConsole.error)
     }
     }
-  }
-)
+  },
+])

+ 97 - 91
services/web/frontend/js/ide/metadata/services/metadata.js

@@ -13,111 +13,117 @@ import _ from 'lodash'
  */
  */
 import App from '../../../base'
 import App from '../../../base'
 
 
-export default App.factory('metadata', function ($http, ide) {
-  const debouncer = {} // DocId => Timeout
+export default App.factory('metadata', [
+  '$http',
+  'ide',
+  function ($http, ide) {
+    const debouncer = {} // DocId => Timeout
 
 
-  const state = { documents: {} }
+    const state = { documents: {} }
 
 
-  const metadata = { state }
+    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.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.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.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
+    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
     }
     }
-    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.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.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]
+    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]
-    }
+      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))
-  }
+      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)
-  })
+    window.addEventListener('editor:metadata-outdated', () => {
+      metadata.scheduleLoadDocMetaFromServer(
+        ide.$scope.editor.sharejs_doc.doc_id
+      )
+    })
 
 
-  return metadata
-})
+    return metadata
+  },
+])

+ 7 - 4
services/web/frontend/js/ide/review-panel/controllers/BulkActionsModalController.js

@@ -11,12 +11,15 @@
  */
  */
 import App from '../../../base'
 import App from '../../../base'
 
 
-export default App.controller(
-  'BulkActionsModalController',
+export default App.controller('BulkActionsModalController', [
+  '$scope',
+  '$modalInstance',
+  'isAccept',
+  'nChanges',
   function ($scope, $modalInstance, isAccept, nChanges) {
   function ($scope, $modalInstance, isAccept, nChanges) {
     $scope.isAccept = isAccept
     $scope.isAccept = isAccept
     $scope.nChanges = nChanges
     $scope.nChanges = nChanges
     $scope.cancel = () => $modalInstance.dismiss()
     $scope.cancel = () => $modalInstance.dismiss()
     return ($scope.confirm = () => $modalInstance.close(isAccept))
     return ($scope.confirm = () => $modalInstance.close(isAccept))
-  }
-)
+  },
+])

+ 11 - 4
services/web/frontend/js/ide/review-panel/controllers/ReviewPanelController.js

@@ -20,8 +20,15 @@ import EventEmitter from '../../../utils/EventEmitter'
 import ColorManager from '../../colors/ColorManager'
 import ColorManager from '../../colors/ColorManager'
 import getMeta from '../../../utils/meta'
 import getMeta from '../../../utils/meta'
 
 
-export default App.controller(
-  'ReviewPanelController',
+export default App.controller('ReviewPanelController', [
+  '$scope',
+  '$element',
+  'ide',
+  '$timeout',
+  '$http',
+  '$modal',
+  'eventTracking',
+  'localStorage',
   function (
   function (
     $scope,
     $scope,
     $element,
     $element,
@@ -1412,8 +1419,8 @@ export default App.controller(
 
 
     // Add methods somewhere that React can see them
     // Add methods somewhere that React can see them
     $scope.reviewPanel.saveEdit = $scope.saveEdit
     $scope.reviewPanel.saveEdit = $scope.saveEdit
-  }
-)
+  },
+])
 
 
 // send events to the CodeMirror 6 track changes extension
 // send events to the CodeMirror 6 track changes extension
 const dispatchReviewPanelEvent = (type, payload) => {
 const dispatchReviewPanelEvent = (type, payload) => {

+ 7 - 4
services/web/frontend/js/ide/review-panel/controllers/TrackChangesUpgradeModalController.js

@@ -11,7 +11,10 @@
  */
  */
 import App from '../../../base'
 import App from '../../../base'
 
 
-export default App.controller(
-  'TrackChangesUpgradeModalController',
-  ($scope, $modalInstance) => ($scope.cancel = () => $modalInstance.dismiss())
-)
+export default App.controller('TrackChangesUpgradeModalController', [
+  '$scope',
+  '$modalInstance',
+  function ($scope, $modalInstance) {
+    $scope.cancel = () => $modalInstance.dismiss()
+  },
+])

+ 54 - 51
services/web/frontend/js/ide/review-panel/directives/aggregateChangeEntry.js

@@ -11,59 +11,62 @@
  */
  */
 import App from '../../../base'
 import App from '../../../base'
 
 
-export default App.directive('aggregateChangeEntry', $timeout => ({
-  restrict: 'E',
-  templateUrl: 'aggregateChangeEntryTemplate',
-  scope: {
-    entry: '=',
-    user: '=',
-    permissions: '=',
-    onAccept: '&',
-    onReject: '&',
-    onIndicatorClick: '&',
-    onMouseEnter: '&',
-    onMouseLeave: '&',
-    onBodyClick: '&',
-  },
-  link(scope, element, attrs) {
-    scope.contentLimit = 17
-    scope.isDeletionCollapsed = true
-    scope.isInsertionCollapsed = true
-    scope.deletionNeedsCollapsing = false
-    scope.insertionNeedsCollapsing = false
+export default App.directive('aggregateChangeEntry', [
+  '$timeout',
+  $timeout => ({
+    restrict: 'E',
+    templateUrl: 'aggregateChangeEntryTemplate',
+    scope: {
+      entry: '=',
+      user: '=',
+      permissions: '=',
+      onAccept: '&',
+      onReject: '&',
+      onIndicatorClick: '&',
+      onMouseEnter: '&',
+      onMouseLeave: '&',
+      onBodyClick: '&',
+    },
+    link(scope, element, attrs) {
+      scope.contentLimit = 17
+      scope.isDeletionCollapsed = true
+      scope.isInsertionCollapsed = true
+      scope.deletionNeedsCollapsing = false
+      scope.insertionNeedsCollapsing = false
 
 
-    element.on('click', function (e) {
-      if (
-        $(e.target).is(
-          '.rp-entry, .rp-entry-description, .rp-entry-body, .rp-entry-action-icon i'
-        )
-      ) {
-        return scope.onBodyClick()
-      }
-    })
+      element.on('click', function (e) {
+        if (
+          $(e.target).is(
+            '.rp-entry, .rp-entry-description, .rp-entry-body, .rp-entry-action-icon i'
+          )
+        ) {
+          return scope.onBodyClick()
+        }
+      })
 
 
-    scope.toggleDeletionCollapse = function () {
-      scope.isDeletionCollapsed = !scope.isDeletionCollapsed
-      return $timeout(() => scope.$emit('review-panel:layout'))
-    }
+      scope.toggleDeletionCollapse = function () {
+        scope.isDeletionCollapsed = !scope.isDeletionCollapsed
+        return $timeout(() => scope.$emit('review-panel:layout'))
+      }
 
 
-    scope.toggleInsertionCollapse = function () {
-      scope.isInsertionCollapsed = !scope.isInsertionCollapsed
-      return $timeout(() => scope.$emit('review-panel:layout'))
-    }
+      scope.toggleInsertionCollapse = function () {
+        scope.isInsertionCollapsed = !scope.isInsertionCollapsed
+        return $timeout(() => scope.$emit('review-panel:layout'))
+      }
 
 
-    scope.$watch(
-      'entry.metadata.replaced_content.length',
-      deletionContentLength =>
-        (scope.deletionNeedsCollapsing =
-          deletionContentLength > scope.contentLimit)
-    )
+      scope.$watch(
+        'entry.metadata.replaced_content.length',
+        deletionContentLength =>
+          (scope.deletionNeedsCollapsing =
+            deletionContentLength > scope.contentLimit)
+      )
 
 
-    return scope.$watch(
-      'entry.content.length',
-      insertionContentLength =>
-        (scope.insertionNeedsCollapsing =
-          insertionContentLength > scope.contentLimit)
-    )
-  },
-}))
+      return scope.$watch(
+        'entry.content.length',
+        insertionContentLength =>
+          (scope.insertionNeedsCollapsing =
+            insertionContentLength > scope.contentLimit)
+      )
+    },
+  }),
+])

+ 41 - 38
services/web/frontend/js/ide/review-panel/directives/changeEntry.js

@@ -11,44 +11,47 @@
  */
  */
 import App from '../../../base'
 import App from '../../../base'
 
 
-export default App.directive('changeEntry', $timeout => ({
-  restrict: 'E',
-  templateUrl: 'changeEntryTemplate',
-  scope: {
-    entry: '=',
-    user: '=',
-    permissions: '=',
-    onAccept: '&',
-    onReject: '&',
-    onIndicatorClick: '&',
-    onMouseEnter: '&',
-    onMouseLeave: '&',
-    onBodyClick: '&',
-  },
-  link(scope, element, attrs) {
-    scope.contentLimit = 40
-    scope.isCollapsed = true
-    scope.needsCollapsing = false
+export default App.directive('changeEntry', [
+  '$timeout',
+  $timeout => ({
+    restrict: 'E',
+    templateUrl: 'changeEntryTemplate',
+    scope: {
+      entry: '=',
+      user: '=',
+      permissions: '=',
+      onAccept: '&',
+      onReject: '&',
+      onIndicatorClick: '&',
+      onMouseEnter: '&',
+      onMouseLeave: '&',
+      onBodyClick: '&',
+    },
+    link(scope, element, attrs) {
+      scope.contentLimit = 40
+      scope.isCollapsed = true
+      scope.needsCollapsing = false
 
 
-    element.on('click', function (e) {
-      if (
-        $(e.target).is(
-          '.rp-entry, .rp-entry-description, .rp-entry-body, .rp-entry-action-icon i'
-        )
-      ) {
-        return scope.onBodyClick()
-      }
-    })
+      element.on('click', function (e) {
+        if (
+          $(e.target).is(
+            '.rp-entry, .rp-entry-description, .rp-entry-body, .rp-entry-action-icon i'
+          )
+        ) {
+          return scope.onBodyClick()
+        }
+      })
 
 
-    scope.toggleCollapse = function () {
-      scope.isCollapsed = !scope.isCollapsed
-      return $timeout(() => scope.$emit('review-panel:layout'))
-    }
+      scope.toggleCollapse = function () {
+        scope.isCollapsed = !scope.isCollapsed
+        return $timeout(() => scope.$emit('review-panel:layout'))
+      }
 
 
-    return scope.$watch(
-      'entry.content.length',
-      contentLength =>
-        (scope.needsCollapsing = contentLength > scope.contentLimit)
-    )
-  },
-}))
+      return scope.$watch(
+        'entry.content.length',
+        contentLength =>
+          (scope.needsCollapsing = contentLength > scope.contentLimit)
+      )
+    },
+  }),
+])

+ 71 - 68
services/web/frontend/js/ide/review-panel/directives/commentEntry.js

@@ -11,82 +11,85 @@
  */
  */
 import App from '../../../base'
 import App from '../../../base'
 
 
-export default App.directive('commentEntry', $timeout => ({
-  restrict: 'E',
-  templateUrl: 'commentEntryTemplate',
-  scope: {
-    entry: '=',
-    threads: '=',
-    permissions: '=',
-    onResolve: '&',
-    onReply: '&',
-    onIndicatorClick: '&',
-    onMouseEnter: '&',
-    onMouseLeave: '&',
-    onSaveEdit: '&',
-    onDelete: '&',
-    onBodyClick: '&',
-  },
-  link(scope, element, attrs) {
-    scope.state = { animating: false }
+export default App.directive('commentEntry', [
+  '$timeout',
+  $timeout => ({
+    restrict: 'E',
+    templateUrl: 'commentEntryTemplate',
+    scope: {
+      entry: '=',
+      threads: '=',
+      permissions: '=',
+      onResolve: '&',
+      onReply: '&',
+      onIndicatorClick: '&',
+      onMouseEnter: '&',
+      onMouseLeave: '&',
+      onSaveEdit: '&',
+      onDelete: '&',
+      onBodyClick: '&',
+    },
+    link(scope, element, attrs) {
+      scope.state = { animating: false }
 
 
-    element.on('click', function (e) {
-      if (
-        $(e.target).is(
-          '.rp-entry, .rp-comment-loaded, .rp-comment-content, .rp-comment-reply, .rp-entry-metadata'
-        )
-      ) {
-        return scope.onBodyClick()
-      }
-    })
+      element.on('click', function (e) {
+        if (
+          $(e.target).is(
+            '.rp-entry, .rp-comment-loaded, .rp-comment-content, .rp-comment-reply, .rp-entry-metadata'
+          )
+        ) {
+          return scope.onBodyClick()
+        }
+      })
 
 
-    scope.handleCommentReplyKeyPress = function (ev) {
-      if (ev.keyCode === 13 && !ev.shiftKey && !ev.ctrlKey && !ev.metaKey) {
-        ev.preventDefault()
-        if (scope.entry.replyContent.length > 0) {
-          ev.target.blur()
-          return scope.onReply()
+      scope.handleCommentReplyKeyPress = function (ev) {
+        if (ev.keyCode === 13 && !ev.shiftKey && !ev.ctrlKey && !ev.metaKey) {
+          ev.preventDefault()
+          if (scope.entry.replyContent.length > 0) {
+            ev.target.blur()
+            return scope.onReply()
+          }
         }
         }
       }
       }
-    }
-
-    scope.animateAndCallOnResolve = function () {
-      scope.state.animating = true
-      element.find('.rp-entry').css('top', 0)
-      $timeout(() => scope.onResolve(), 350)
-      return true
-    }
 
 
-    scope.startEditing = function (comment) {
-      comment.editing = true
-      return setTimeout(() => scope.$emit('review-panel:layout'))
-    }
+      scope.animateAndCallOnResolve = function () {
+        scope.state.animating = true
+        element.find('.rp-entry').css('top', 0)
+        $timeout(() => scope.onResolve(), 350)
+        return true
+      }
 
 
-    scope.saveEdit = function (comment) {
-      comment.editing = false
-      return scope.onSaveEdit({ comment })
-    }
+      scope.startEditing = function (comment) {
+        comment.editing = true
+        return setTimeout(() => scope.$emit('review-panel:layout'))
+      }
 
 
-    scope.confirmDelete = function (comment) {
-      comment.deleting = true
-      return setTimeout(() => scope.$emit('review-panel:layout'))
-    }
+      scope.saveEdit = function (comment) {
+        comment.editing = false
+        return scope.onSaveEdit({ comment })
+      }
 
 
-    scope.cancelDelete = function (comment) {
-      comment.deleting = false
-      return setTimeout(() => scope.$emit('review-panel:layout'))
-    }
+      scope.confirmDelete = function (comment) {
+        comment.deleting = true
+        return setTimeout(() => scope.$emit('review-panel:layout'))
+      }
 
 
-    scope.doDelete = function (comment) {
-      comment.deleting = false
-      return scope.onDelete({ comment })
-    }
+      scope.cancelDelete = function (comment) {
+        comment.deleting = false
+        return setTimeout(() => scope.$emit('review-panel:layout'))
+      }
 
 
-    return (scope.saveEditOnEnter = function (ev, comment) {
-      if (ev.keyCode === 13 && !ev.shiftKey && !ev.ctrlKey && !ev.metaKey) {
-        ev.preventDefault()
-        return scope.saveEdit(comment)
+      scope.doDelete = function (comment) {
+        comment.deleting = false
+        return scope.onDelete({ comment })
       }
       }
-    })
-  },
-}))
+
+      return (scope.saveEditOnEnter = function (ev, comment) {
+        if (ev.keyCode === 13 && !ev.shiftKey && !ev.ctrlKey && !ev.metaKey) {
+          ev.preventDefault()
+          return scope.saveEdit(comment)
+        }
+      })
+    },
+  }),
+])

+ 22 - 19
services/web/frontend/js/ide/review-panel/directives/reviewPanelCollapseHeight.js

@@ -10,25 +10,28 @@
  */
  */
 import App from '../../../base'
 import App from '../../../base'
 
 
-export default App.directive('reviewPanelCollapseHeight', $parse => ({
-  restrict: 'A',
-  link(scope, element, attrs) {
-    return scope.$watch(
-      () => $parse(attrs.reviewPanelCollapseHeight)(scope),
-      function (shouldCollapse) {
-        const neededHeight = element.prop('scrollHeight')
-        if (neededHeight > 0) {
-          if (shouldCollapse) {
-            return element.animate({ height: 0 }, 150)
+export default App.directive('reviewPanelCollapseHeight', [
+  '$parse',
+  $parse => ({
+    restrict: 'A',
+    link(scope, element, attrs) {
+      return scope.$watch(
+        () => $parse(attrs.reviewPanelCollapseHeight)(scope),
+        function (shouldCollapse) {
+          const neededHeight = element.prop('scrollHeight')
+          if (neededHeight > 0) {
+            if (shouldCollapse) {
+              return element.animate({ height: 0 }, 150)
+            } else {
+              return element.animate({ height: neededHeight }, 150)
+            }
           } else {
           } else {
-            return element.animate({ height: neededHeight }, 150)
-          }
-        } else {
-          if (shouldCollapse) {
-            return element.height(0)
+            if (shouldCollapse) {
+              return element.height(0)
+            }
           }
           }
         }
         }
-      }
-    )
-  },
-}))
+      )
+    },
+  }),
+])

+ 1 - 1
services/web/frontend/js/ide/review-panel/directives/reviewPanelSorted.js

@@ -16,7 +16,7 @@
 import App from '../../../base'
 import App from '../../../base'
 import { debugConsole } from '@/utils/debugging'
 import { debugConsole } from '@/utils/debugging'
 
 
-export default App.directive('reviewPanelSorted', $timeout => ({
+export default App.directive('reviewPanelSorted', () => ({
   link(scope, element, attrs) {
   link(scope, element, attrs) {
     let previous_focused_entry_index = 0
     let previous_focused_entry_index = 0
 
 

+ 25 - 12
services/web/frontend/js/ide/services/ide.js

@@ -17,8 +17,14 @@ import EditorWatchdogManager from '../connection/EditorWatchdogManager'
 import { debugConsole } from '@/utils/debugging'
 import { debugConsole } from '@/utils/debugging'
 // We create and provide this as service so that we can access the global ide
 // We create and provide this as service so that we can access the global ide
 // from within other parts of the angular app.
 // from within other parts of the angular app.
-App.factory(
-  'ide',
+App.factory('ide', [
+  '$http',
+  'queuedHttp',
+  '$modal',
+  '$q',
+  '$filter',
+  '$timeout',
+  'eventTracking',
   function ($http, queuedHttp, $modal, $q, $filter, $timeout, eventTracking) {
   function ($http, queuedHttp, $modal, $q, $filter, $timeout, eventTracking) {
     const ide = {}
     const ide = {}
     ide.$http = $http
     ide.$http = $http
@@ -132,21 +138,28 @@ App.factory(
       })
       })
 
 
     return ide
     return ide
-  }
-)
+  },
+])
 
 
-App.controller(
-  'GenericMessageModalController',
+App.controller('GenericMessageModalController', [
+  '$scope',
+  '$modalInstance',
+  'title',
+  'message',
   function ($scope, $modalInstance, title, message) {
   function ($scope, $modalInstance, title, message) {
     $scope.title = title
     $scope.title = title
     $scope.message = message
     $scope.message = message
 
 
     return ($scope.done = () => $modalInstance.close())
     return ($scope.done = () => $modalInstance.close())
-  }
-)
+  },
+])
 
 
-App.controller(
-  'OutOfSyncModalController',
+App.controller('OutOfSyncModalController', [
+  '$scope',
+  '$window',
+  'title',
+  'message',
+  'editorContent',
   function ($scope, $window, title, message, editorContent) {
   function ($scope, $window, title, message, editorContent) {
     $scope.title = title
     $scope.title = title
     $scope.message = message
     $scope.message = message
@@ -158,8 +171,8 @@ App.controller(
       // https://github.com/overleaf/issues/issues/3694
       // https://github.com/overleaf/issues/issues/3694
       $window.location.reload()
       $window.location.reload()
     }
     }
-  }
-)
+  },
+])
 
 
 function __guard__(value, transform) {
 function __guard__(value, transform) {
   return typeof value !== 'undefined' && value !== null
   return typeof value !== 'undefined' && value !== null

+ 18 - 14
services/web/frontend/js/ide/toolbar/EditorLoaderController.js

@@ -1,17 +1,21 @@
 import App from '../../base'
 import App from '../../base'
 
 
-App.controller('EditorLoaderController', function ($scope, localStorage) {
-  $scope.$watch('editor.showVisual', function (val) {
-    localStorage(
-      `editor.mode.${$scope.project_id}`,
-      val === true ? 'rich-text' : 'source'
-    )
-  })
+App.controller('EditorLoaderController', [
+  '$scope',
+  'localStorage',
+  function ($scope, localStorage) {
+    $scope.$watch('editor.showVisual', function (val) {
+      localStorage(
+        `editor.mode.${$scope.project_id}`,
+        val === true ? 'rich-text' : 'source'
+      )
+    })
 
 
-  $scope.$watch('editor.newSourceEditor', function (val) {
-    localStorage(
-      `editor.source_editor.${$scope.project_id}`,
-      val === true ? 'cm6' : 'ace'
-    )
-  })
-})
+    $scope.$watch('editor.newSourceEditor', function (val) {
+      localStorage(
+        `editor.source_editor.${$scope.project_id}`,
+        val === true ? 'cm6' : 'ace'
+      )
+    })
+  },
+])

+ 13 - 9
services/web/frontend/js/ide/toolbar/EditorToolbarController.js

@@ -4,16 +4,20 @@ import importOverleafModules from '../../../macros/import-overleaf-module.macro'
 const eModules = importOverleafModules('editorToolbarButtons')
 const eModules = importOverleafModules('editorToolbarButtons')
 const editorToolbarButtons = eModules.map(item => item.import.default)
 const editorToolbarButtons = eModules.map(item => item.import.default)
 
 
-export default App.controller('EditorToolbarController', ($scope, ide) => {
-  const editorButtons = []
+export default App.controller('EditorToolbarController', [
+  '$scope',
+  'ide',
+  function ($scope, ide) {
+    const editorButtons = []
 
 
-  for (const editorToolbarButton of editorToolbarButtons) {
-    const button = editorToolbarButton.button($scope, ide)
+    for (const editorToolbarButton of editorToolbarButtons) {
+      const button = editorToolbarButton.button($scope, ide)
 
 
-    if (editorToolbarButton.source) {
-      editorButtons.push(button)
+      if (editorToolbarButton.source) {
+        editorButtons.push(button)
+      }
     }
     }
-  }
 
 
-  $scope.editorButtons = editorButtons
-})
+    $scope.editorButtons = editorButtons
+  },
+])

+ 14 - 11
services/web/frontend/js/main.js

@@ -41,15 +41,18 @@ import './features/cookie-banner'
 import '../../modules/modules-main'
 import '../../modules/modules-main'
 import './cdn-load-test'
 import './cdn-load-test'
 import { debugConsole } from '@/utils/debugging'
 import { debugConsole } from '@/utils/debugging'
-angular.module('SharelatexApp').config(function ($locationProvider) {
-  try {
-    return $locationProvider.html5Mode({
-      enabled: true,
-      requireBase: false,
-      rewriteLinks: false,
-    })
-  } catch (e) {
-    debugConsole.error("Error while trying to fix '#' links: ", e)
-  }
-})
+angular.module('SharelatexApp').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, ['SharelatexApp'])
 export default angular.bootstrap(document.body, ['SharelatexApp'])

+ 16 - 10
services/web/frontend/js/main/account-upgrade-angular.js

@@ -1,14 +1,20 @@
 import App from '../base'
 import App from '../base'
 import { startFreeTrial, upgradePlan, paywallPrompt } from './account-upgrade'
 import { startFreeTrial, upgradePlan, paywallPrompt } from './account-upgrade'
 
 
-App.controller('FreeTrialModalController', function ($scope) {
-  $scope.buttonClass = 'btn-primary'
-  $scope.startFreeTrial = (source, version) =>
-    startFreeTrial(source, version, $scope)
-  $scope.paywallPrompt = source => paywallPrompt(source)
-})
+App.controller('FreeTrialModalController', [
+  '$scope',
+  function ($scope) {
+    $scope.buttonClass = 'btn-primary'
+    $scope.startFreeTrial = (source, version) =>
+      startFreeTrial(source, version, $scope)
+    $scope.paywallPrompt = source => paywallPrompt(source)
+  },
+])
 
 
-App.controller('UpgradeModalController', function ($scope) {
-  $scope.buttonClass = 'btn-primary'
-  $scope.upgradePlan = source => upgradePlan(source, $scope)
-})
+App.controller('UpgradeModalController', [
+  '$scope',
+  function ($scope) {
+    $scope.buttonClass = 'btn-primary'
+    $scope.upgradePlan = source => upgradePlan(source, $scope)
+  },
+])

+ 6 - 5
services/web/frontend/js/main/annual-upgrade.js

@@ -12,9 +12,10 @@
 import App from '../base'
 import App from '../base'
 import { debugConsole } from '@/utils/debugging'
 import { debugConsole } from '@/utils/debugging'
 
 
-export default App.controller(
-  'AnnualUpgradeController',
-  function ($scope, $http, $modal) {
+export default App.controller('AnnualUpgradeController', [
+  '$scope',
+  '$http',
+  function ($scope, $http) {
     const MESSAGES_URL = '/user/subscription/upgrade-annual'
     const MESSAGES_URL = '/user/subscription/upgrade-annual'
 
 
     $scope.upgradeComplete = false
     $scope.upgradeComplete = false
@@ -43,5 +44,5 @@ export default App.controller(
           debugConsole.error('something went wrong changing plan', err)
           debugConsole.error('something went wrong changing plan', err)
         )
         )
     })
     })
-  }
-)
+  },
+])

+ 15 - 10
services/web/frontend/js/main/bonus.js

@@ -9,17 +9,22 @@
  * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  */
  */
 import App from '../base'
 import App from '../base'
-App.controller(
-  'BonusLinksController',
-  ($scope, $modal) =>
-    ($scope.openLinkToUsModal = () =>
+App.controller('BonusLinksController', [
+  '$scope',
+  '$modal',
+  function ($scope, $modal) {
+    $scope.openLinkToUsModal = () =>
       $modal.open({
       $modal.open({
         templateUrl: 'BonusLinkToUsModal',
         templateUrl: 'BonusLinkToUsModal',
         controller: 'BonusModalController',
         controller: 'BonusModalController',
-      }))
-)
+      })
+  },
+])
 
 
-export default App.controller(
-  'BonusModalController',
-  ($scope, $modalInstance) => ($scope.cancel = () => $modalInstance.dismiss())
-)
+export default App.controller('BonusModalController', [
+  '$scope',
+  '$modalInstance',
+  function ($scope, $modalInstance) {
+    $scope.cancel = () => $modalInstance.dismiss()
+  },
+])

+ 5 - 4
services/web/frontend/js/main/clear-sessions.js

@@ -12,8 +12,9 @@
 import App from '../base'
 import App from '../base'
 import getMeta from '../utils/meta'
 import getMeta from '../utils/meta'
 
 
-export default App.controller(
-  'ClearSessionsController',
+export default App.controller('ClearSessionsController', [
+  '$scope',
+  '$http',
   function ($scope, $http) {
   function ($scope, $http) {
     $scope.state = {
     $scope.state = {
       otherSessions: getMeta('ol-otherSessions'),
       otherSessions: getMeta('ol-otherSessions'),
@@ -34,5 +35,5 @@ export default App.controller(
         })
         })
         .catch(() => ($scope.state.error = true))
         .catch(() => ($scope.state.error = true))
     })
     })
-  }
-)
+  },
+])

+ 78 - 74
services/web/frontend/js/main/event.js

@@ -24,98 +24,102 @@ const CACHE_KEY = 'mbEvents'
 let heartbeatsSent = 0
 let heartbeatsSent = 0
 let nextHeartbeat = new Date()
 let nextHeartbeat = new Date()
 
 
-App.factory('eventTracking', 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)
-    }
+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
-  }
+      return eventCache
+    }
 
 
-  const _eventInCache = function (key) {
-    const curCache = _getEventCache()
-    return curCache[key] || false
-  }
+    const _eventInCache = function (key) {
+      const curCache = _getEventCache()
+      return curCache[key] || false
+    }
 
 
-  const _addEventToCache = function (key) {
-    const curCache = _getEventCache()
-    curCache[key] = true
+    const _addEventToCache = function (key) {
+      const curCache = _getEventCache()
+      curCache[key] = true
 
 
-    return localStorage(CACHE_KEY, curCache)
-  }
+      return localStorage(CACHE_KEY, curCache)
+    }
 
 
-  const _sendEditingSessionHeartbeat = segmentation =>
-    $http({
-      url: `/editingSession/${window.project_id}`,
-      method: 'PUT',
-      data: { segmentation },
-      headers: {
-        'X-CSRF-Token': window.csrfToken,
+    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)
       },
       },
-    })
 
 
-  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)
-      }
-    },
+      sendGAOnce(category, action, label, value) {
+        if (!_eventInCache(action)) {
+          _addEventToCache(action)
+          return this.send(category, action, label, value)
+        }
+      },
 
 
-    editingSessionHeartbeat(segmentationCb = () => {}) {
-      debugConsole.log('[Event] heartbeat trigger')
+      editingSessionHeartbeat(segmentationCb = () => {}) {
+        debugConsole.log('[Event] heartbeat trigger')
 
 
-      // If the next heartbeat is in the future, stop
-      if (nextHeartbeat > new Date()) return
+        // If the next heartbeat is in the future, stop
+        if (nextHeartbeat > new Date()) return
 
 
-      const segmentation = segmentationCb()
+        const segmentation = segmentationCb()
 
 
-      debugConsole.log('[Event] send heartbeat request', segmentation)
-      _sendEditingSessionHeartbeat(segmentation)
+        debugConsole.log('[Event] send heartbeat request', segmentation)
+        _sendEditingSessionHeartbeat(segmentation)
 
 
-      heartbeatsSent++
+        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
+        // 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()
-    },
+        nextHeartbeat = moment().add(backoffSecs, 'seconds').toDate()
+      },
 
 
-    sendMB,
+      sendMB,
 
 
-    sendMBSampled(key, segmentation, rate = 0.01) {
-      if (Math.random() < rate) {
-        this.sendMB(key, segmentation)
-      }
-    },
+      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)
-      }
-    },
+      sendMBOnce(key, segmentation) {
+        if (!_eventInCache(key)) {
+          _addEventToCache(key)
+          this.sendMB(key, segmentation)
+        }
+      },
 
 
-    eventInCache(key) {
-      return _eventInCache(key)
-    },
-  }
-})
+      eventInCache(key) {
+        return _eventInCache(key)
+      },
+    }
+  },
+])
 
 
 export default $('.navbar a').on('click', function (e) {
 export default $('.navbar a').on('click', function (e) {
   const href = $(e.target).attr('href')
   const href = $(e.target).attr('href')

+ 7 - 4
services/web/frontend/js/main/importing.js

@@ -1,6 +1,9 @@
 import App from '../base'
 import App from '../base'
-App.controller(
-  'ImportingController',
+App.controller('ImportingController', [
+  '$interval',
+  '$scope',
+  '$timeout',
+  '$window',
   function ($interval, $scope, $timeout, $window) {
   function ($interval, $scope, $timeout, $window) {
     $interval(function () {
     $interval(function () {
       $scope.state.load_progress += 5
       $scope.state.load_progress += 5
@@ -14,5 +17,5 @@ App.controller(
     $scope.state = {
     $scope.state = {
       load_progress: 20,
       load_progress: 20,
     }
     }
-  }
-)
+  },
+])

+ 6 - 5
services/web/frontend/js/main/learn.js

@@ -2,9 +2,10 @@ import _ from 'lodash'
 import App from '../base'
 import App from '../base'
 import '../directives/mathjax'
 import '../directives/mathjax'
 import '../services/algolia-search'
 import '../services/algolia-search'
-App.controller(
-  'SearchWikiController',
-  function ($scope, algoliaSearch, $modal) {
+App.controller('SearchWikiController', [
+  '$scope',
+  'algoliaSearch',
+  function ($scope, algoliaSearch) {
     $scope.hits = []
     $scope.hits = []
     $scope.hits_total = 0
     $scope.hits_total = 0
     $scope.config_hits_per_page = 20
     $scope.config_hits_per_page = 20
@@ -95,7 +96,7 @@ App.controller(
         }
         }
       )
       )
     }
     }
-  }
-)
+  },
+])
 
 
 export default App.controller('LearnController', function () {})
 export default App.controller('LearnController', function () {})

+ 8 - 5
services/web/frontend/js/main/scribtex-popup.js

@@ -7,8 +7,11 @@
  */
  */
 import App from '../base'
 import App from '../base'
 
 
-export default App.controller('ScribtexPopupController', ($scope, $modal) =>
-  $modal.open({
-    templateUrl: 'scribtexModalTemplate',
-  })
-)
+export default App.controller('ScribtexPopupController', [
+  '$modal',
+  function ($modal) {
+    $modal.open({
+      templateUrl: 'scribtexModalTemplate',
+    })
+  },
+])

+ 50 - 46
services/web/frontend/js/main/subscription/team-invite-controller.js

@@ -13,55 +13,59 @@ import App from '../../base'
 import getMeta from '../../utils/meta'
 import getMeta from '../../utils/meta'
 import { debugConsole } from '@/utils/debugging'
 import { debugConsole } from '@/utils/debugging'
 
 
-export default App.controller('TeamInviteController', function ($scope, $http) {
-  $scope.inflight = false
+export default App.controller('TeamInviteController', [
+  '$scope',
+  '$http',
+  function ($scope, $http) {
+    $scope.inflight = false
 
 
-  const hideJoinSubscription = getMeta('ol-cannot-join-subscription')
-  const hasIndividualRecurlySubscription = getMeta(
-    'ol-hasIndividualRecurlySubscription'
-  )
+    const hideJoinSubscription = getMeta('ol-cannot-join-subscription')
+    const hasIndividualRecurlySubscription = getMeta(
+      'ol-hasIndividualRecurlySubscription'
+    )
 
 
-  if (hideJoinSubscription) {
-    $scope.view = 'restrictedByManagedGroup'
-  } else if (hasIndividualRecurlySubscription) {
-    $scope.view = 'hasIndividualRecurlySubscription'
-  } else {
-    $scope.view = 'teamInvite'
-  }
+    if (hideJoinSubscription) {
+      $scope.view = 'restrictedByManagedGroup'
+    } else if (hasIndividualRecurlySubscription) {
+      $scope.view = 'hasIndividualRecurlySubscription'
+    } else {
+      $scope.view = 'teamInvite'
+    }
 
 
-  $scope.keepPersonalSubscription = () => ($scope.view = 'teamInvite')
+    $scope.keepPersonalSubscription = () => ($scope.view = 'teamInvite')
 
 
-  $scope.cancelPersonalSubscription = function () {
-    $scope.inflight = true
-    const request = $http.post('/user/subscription/cancel', {
-      _csrf: window.csrfToken,
-    })
-    request.then(function () {
-      $scope.inflight = false
-      return ($scope.view = 'teamInvite')
-    })
-    return request.catch(() => {
-      $scope.inflight = false
-      $scope.cancel_error = true
-      debugConsole.error('the request failed')
-    })
-  }
+    $scope.cancelPersonalSubscription = function () {
+      $scope.inflight = true
+      const request = $http.post('/user/subscription/cancel', {
+        _csrf: window.csrfToken,
+      })
+      request.then(function () {
+        $scope.inflight = false
+        return ($scope.view = 'teamInvite')
+      })
+      return request.catch(() => {
+        $scope.inflight = false
+        $scope.cancel_error = true
+        debugConsole.error('the request failed')
+      })
+    }
 
 
-  return ($scope.joinTeam = function () {
-    $scope.inflight = true
-    const inviteToken = getMeta('ol-inviteToken')
-    const request = $http.put(`/subscription/invites/${inviteToken}/`, {
-      _csrf: window.csrfToken,
-    })
-    request.then(function (response) {
-      const { status } = response
-      $scope.inflight = false
-      $scope.view = 'inviteAccepted'
-      if (status !== 200) {
-        // assume request worked
-        return ($scope.requestSent = false)
-      }
+    return ($scope.joinTeam = function () {
+      $scope.inflight = true
+      const inviteToken = getMeta('ol-inviteToken')
+      const request = $http.put(`/subscription/invites/${inviteToken}/`, {
+        _csrf: window.csrfToken,
+      })
+      request.then(function (response) {
+        const { status } = response
+        $scope.inflight = false
+        $scope.view = 'inviteAccepted'
+        if (status !== 200) {
+          // assume request worked
+          return ($scope.requestSent = false)
+        }
+      })
+      return request.catch(() => debugConsole.error('the request failed'))
     })
     })
-    return request.catch(() => debugConsole.error('the request failed'))
-  })
-})
+  },
+])

+ 36 - 32
services/web/frontend/js/main/system-messages.js

@@ -12,38 +12,42 @@
 import App from '../base'
 import App from '../base'
 const MESSAGE_POLL_INTERVAL = 15 * 60 * 1000
 const MESSAGE_POLL_INTERVAL = 15 * 60 * 1000
 // Controller for messages (array)
 // Controller for messages (array)
-App.controller('SystemMessagesController', ($http, $scope) => {
-  $scope.messages = []
-  function pollSystemMessages() {
-    // Ignore polling if tab is hidden or browser is offline
-    if (document.hidden || !navigator.onLine) {
-      return
-    }
+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)
-})
+      $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',
-  function ($scope, $sce) {
+export default App.controller('SystemMessageController', [
+  '$scope',
+  function ($scope) {
     $scope.hidden = $.localStorage(`systemMessage.hide.${$scope.message._id}`)
     $scope.hidden = $.localStorage(`systemMessage.hide.${$scope.message._id}`)
     $scope.protected = $scope.message._id === 'protected'
     $scope.protected = $scope.message._id === 'protected'
     $scope.htmlContent = $scope.message.content
     $scope.htmlContent = $scope.message.content
@@ -55,5 +59,5 @@ export default App.controller(
         return $.localStorage(`systemMessage.hide.${$scope.message._id}`, true)
         return $.localStorage(`systemMessage.hide.${$scope.message._id}`, true)
       }
       }
     })
     })
-  }
-)
+  },
+])

+ 7 - 5
services/web/frontend/js/main/token-access.js

@@ -1,8 +1,10 @@
 import App from '../base'
 import App from '../base'
 import { debugConsole } from '@/utils/debugging'
 import { debugConsole } from '@/utils/debugging'
-App.controller(
-  'TokenAccessPageController',
-  ($scope, $http, $location, localStorage) => {
+App.controller('TokenAccessPageController', [
+  '$scope',
+  '$http',
+  '$location',
+  function ($scope, $http, $location) {
     window.S = $scope
     window.S = $scope
     $scope.mode = 'accessAttempt' // 'accessAttempt' | 'v1Import' | 'requireAccept'
     $scope.mode = 'accessAttempt' // 'accessAttempt' | 'v1Import' | 'requireAccept'
 
 
@@ -86,5 +88,5 @@ App.controller(
         }
         }
       )
       )
     }
     }
-  }
-)
+  },
+])

+ 6 - 4
services/web/frontend/js/main/translations.js

@@ -1,7 +1,9 @@
 import App from '../base'
 import App from '../base'
 
 
-App.controller(
-  'TranslationsPopupController',
+App.controller('TranslationsPopupController', [
+  '$scope',
+  'ipCookie',
+  'localStorage',
   function ($scope, ipCookie, localStorage) {
   function ($scope, ipCookie, localStorage) {
     function getStoredDismissal() {
     function getStoredDismissal() {
       const localStore = localStorage('hide-i18n-notification')
       const localStore = localStorage('hide-i18n-notification')
@@ -28,5 +30,5 @@ App.controller(
       localStorage('hide-i18n-notification', true)
       localStorage('hide-i18n-notification', true)
       $scope.hidei18nNotification = true
       $scope.hidei18nNotification = true
     }
     }
-  }
-)
+  },
+])

+ 36 - 27
services/web/frontend/js/modules/errorCatcher.js

@@ -16,8 +16,8 @@ const UNHANDLED_REJECTION_ERR_MSG = 'Possibly unhandled rejection: canceled'
 
 
 app.config([
 app.config([
   '$provide',
   '$provide',
-  $provide =>
-    $provide.decorator('$exceptionHandler', [
+  function ($provide) {
+    return $provide.decorator('$exceptionHandler', [
       '$log',
       '$log',
       '$delegate',
       '$delegate',
       ($log, $delegate) =>
       ($log, $delegate) =>
@@ -37,39 +37,48 @@ app.config([
 
 
           return $delegate(exception, cause)
           return $delegate(exception, cause)
         },
         },
-    ]),
+    ])
+  },
 ])
 ])
 
 
 // Interceptor to check auth failures in all $http requests
 // Interceptor to check auth failures in all $http requests
 // http://bahmutov.calepin.co/catch-all-errors-in-angular-app.html
 // http://bahmutov.calepin.co/catch-all-errors-in-angular-app.html
 
 
-app.factory('unAuthHttpResponseInterceptor', ($q, $location) => ({
-  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'
-      }
+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)
+      },
     }
     }
-    // pass the response back to the original requester
-    return $q.reject(response)
   },
   },
-}))
+])
 
 
 app.config([
 app.config([
   '$httpProvider',
   '$httpProvider',
-  $httpProvider =>
-    $httpProvider.interceptors.push('unAuthHttpResponseInterceptor'),
+  function ($httpProvider) {
+    return $httpProvider.interceptors.push('unAuthHttpResponseInterceptor')
+  },
 ])
 ])

+ 57 - 52
services/web/frontend/js/services/queued-http.js

@@ -12,66 +12,71 @@
  */
  */
 import App from '../base'
 import App from '../base'
 
 
-export default App.factory('queuedHttp', function ($http, $q) {
-  const pendingRequests = []
-  let inflight = false
+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()
-        })
+    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 = []
+    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)
+      // Adhere to the $http promise conventions
+      promise.then = function (callback, errCallback) {
+        successCallbacks.push(callback)
+        if (errCallback != null) {
+          errorCallbacks.push(errCallback)
+        }
+        return promise
       }
       }
-      return promise
-    }
 
 
-    promise.catch = function (callback) {
-      errorCallbacks.push(callback)
-      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 || [])))
-        )
+      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()
+      pendingRequests.push(doRequest)
+      processPendingRequests()
 
 
-    return promise
-  }
+      return promise
+    }
 
 
-  queuedHttp.post = (url, data) => queuedHttp({ method: 'POST', url, data })
+    queuedHttp.post = (url, data) => queuedHttp({ method: 'POST', url, data })
 
 
-  return queuedHttp
-})
+    return queuedHttp
+  },
+])

+ 31 - 28
services/web/frontend/js/services/wait-for.js

@@ -11,32 +11,35 @@
  */
  */
 import App from '../base'
 import App from '../base'
 
 
-export default App.factory('waitFor', function ($q) {
-  const waitFor = function (testFunction, timeout, pollInterval) {
-    if (pollInterval == null) {
-      pollInterval = 500
-    }
-    const iterationLimit = Math.floor(timeout / pollInterval)
-    let iterations = 0
-    return $q(function (resolve, reject) {
-      let tryIteration
-      return (tryIteration = function () {
-        if (iterations > iterationLimit) {
-          return reject(
-            new Error(
-              `waiting too long, ${JSON.stringify({ timeout, pollInterval })}`
+export default App.factory('waitFor', [
+  '$q',
+  function ($q) {
+    const waitFor = function (testFunction, timeout, pollInterval) {
+      if (pollInterval == null) {
+        pollInterval = 500
+      }
+      const iterationLimit = Math.floor(timeout / pollInterval)
+      let iterations = 0
+      return $q(function (resolve, reject) {
+        let tryIteration
+        return (tryIteration = function () {
+          if (iterations > iterationLimit) {
+            return reject(
+              new Error(
+                `waiting too long, ${JSON.stringify({ timeout, pollInterval })}`
+              )
             )
             )
-          )
-        }
-        iterations += 1
-        const result = testFunction()
-        if (result != null) {
-          return resolve(result)
-        } else {
-          return setTimeout(tryIteration, pollInterval)
-        }
-      })()
-    })
-  }
-  return waitFor
-})
+          }
+          iterations += 1
+          const result = testFunction()
+          if (result != null) {
+            return resolve(result)
+          } else {
+            return setTimeout(tryIteration, pollInterval)
+          }
+        })()
+      })
+    }
+    return waitFor
+  },
+])