Jelajahi Sumber

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

[web] Explicitly name angular parameters

GitOrigin-RevId: 91beae68989d6c8122132b531a4338b116d87424
Jakob Ackermann 3 tahun lalu
induk
melakukan
e959529828
61 mengubah file dengan 1747 tambahan dan 1537 penghapusan
  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',
     '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

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

@@ -1,171 +1,176 @@
 import App from '../base'
 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', () => ({
   restrict: 'E',

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

@@ -1,58 +1,64 @@
 import _ from 'lodash'
 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)
-      }
 
-      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
 }
 
-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
                   */
-      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
  */
 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',
   link(scope, element, attrs) {
     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',
   link(scope, element, attrs) {
     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', () => ({
   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) {
   // 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'
 
-export default App.directive('mathjax', function ($compile, $parse) {
+export default App.directive('mathjax', function () {
   return {
     link(scope, element, attrs) {
       if (!(MathJax && MathJax.Hub)) return

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

@@ -11,46 +11,49 @@
  */
 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
  */
 import App from '../base'
-App.directive('stopPropagation', $http => ({
+App.directive('stopPropagation', () => ({
   restrict: 'A',
   link(scope, element, attrs) {
     return element.bind(attrs.stopPropagation, e => e.stopPropagation())
   },
 }))
 
-export default App.directive('preventDefault', $http => ({
+export default App.directive('preventDefault', () => ({
   restrict: 'A',
   link(scope, element, attrs) {
     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'
 
-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 { 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(
   '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 { 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.$on('project:joined', () => {
@@ -108,8 +105,8 @@ App.controller(
     $scope.reindexReferences = () => {
       ide.$scope.$emit('references:should-reindex', {})
     }
-  }
-)
+  },
+])
 
 App.component(
   '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 FileView from '../components/file-view'
 
-export default App.controller(
-  'FileViewController',
+export default App.controller('FileViewController', [
+  '$scope',
+  '$rootScope',
   function ($scope, $rootScope) {
     $scope.file = $scope.openFile
 
@@ -14,8 +15,8 @@ export default App.controller(
       const oldKeys = $rootScope._references.keys
       return ($rootScope._references.keys = _.union(oldKeys, newKeys))
     }
-  }
-)
+  },
+])
 
 App.component(
   '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 { 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.$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
 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) {
     $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 { debugConsole } from '@/utils/debugging'
 
-App.controller(
-  'IdeController',
+App.controller('IdeController', [
+  '$scope',
+  '$timeout',
+  'ide',
+  'localStorage',
+  'eventTracking',
+  'metadata',
+  'CobrandingDataService',
+  '$window',
   function (
     $scope,
     $timeout,
@@ -79,7 +86,6 @@ App.controller(
     localStorage,
     eventTracking,
     metadata,
-    $q,
     CobrandingDataService,
     $window
   ) {
@@ -478,23 +484,26 @@ If the project has been renamed please look in your project list for a new proje
         return $scope.$digest()
       }
     })
-  }
-)
+  },
+])
 
 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'])
 

+ 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.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 \
 ng-show=\"isVisible\" \
 class=\"custom-toggler ${`custom-toggler-${customTogglerPane}`}\" \
@@ -216,99 +221,100 @@ tooltip-placement=\"{{ tooltipPlacement }}\" \
 ng-click=\"handleClick()\" \
 aria-label=\"{{ isOpen ? tooltipMsgWhenOpen : tooltipMsgWhenClosed }}\">\
 `)(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) {
-                layout.open('east')
+                layout.hide('east')
               } 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'
 
-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 Document from '../Document'
 
-export default App.controller(
-  'SavingNotificationController',
-  function ($scope, $interval, ide) {
+export default App.controller('SavingNotificationController', [
+  '$scope',
+  'ide',
+  function ($scope, ide) {
     let warnAboutUnsavedChanges
     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.'
       }
     })
-  }
-)
+  },
+])

+ 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 (
     ide,
-    $timeout,
     $compile,
     $rootScope,
     eventTracking,
@@ -69,8 +79,7 @@ App.directive(
     preamble,
     files,
     $http,
-    $q,
-    $window
+    $q
   ) {
     monkeyPatchSearch($rootScope, $compile)
 
@@ -951,8 +960,8 @@ App.directive(
 </div>\
 `,
     }
-  }
-)
+  },
+])
 
 function monkeyPatchSearch($rootScope, $compile) {
   const searchHtml = `\

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

@@ -1,15 +1,18 @@
 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 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'
 
-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) {
   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
  */
 import App from '../../../base'
-const historyEntriesListController = function ($scope, $element, $attrs) {
+const historyEntriesListController = function ($scope, $element) {
   const ctrl = this
   ctrl.$entryListViewportEl = null
   ctrl.isDragging = false
@@ -133,6 +133,6 @@ export default App.component('historyEntriesList', {
     onRangeSelect: '&',
     onLabelDelete: '&',
   },
-  controller: historyEntriesListController,
+  controller: ['$scope', '$element', historyEntriesListController],
   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 ColorManager from '../../colors/ColorManager'
 import displayNameForUser from '../util/displayNameForUser'
-const historyEntryController = function ($scope, $element, $attrs) {
+const historyEntryController = function ($scope, $element) {
   const ctrl = this
   // 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
@@ -120,6 +120,6 @@ export default App.component('historyEntry', {
   require: {
     historyEntriesList: '^historyEntriesList',
   },
-  controller: historyEntryController,
+  controller: ['$scope', '$element', historyEntryController],
   templateUrl: 'historyEntryTpl',
 })

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

@@ -12,7 +12,7 @@
 import App from '../../../base'
 import iconTypeFromName from '../../file-tree/util/iconTypeFromName'
 import fileOperationI18nNames from '../../file-tree/util/fileOperationI18nNames'
-const historyFileEntityController = function ($scope, $element, $attrs) {
+const historyFileEntityController = function ($scope) {
   const ctrl = this
   ctrl.hasOperation = false
   ctrl.getRenameTooltip = i18nRenamedStr => {
@@ -99,6 +99,6 @@ export default App.component('historyFileEntity', {
   bindings: {
     fileEntity: '<',
   },
-  controller: historyFileEntityController,
+  controller: ['$scope', historyFileEntityController],
   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
  */
 import App from '../../../base'
-const historyFileTreeController = function ($scope, $element, $attrs) {
+const historyFileTreeController = function ($scope) {
   const ctrl = this
   ctrl.handleEntityClick = file => ctrl.onSelectedFileChange({ file })
   ctrl._fileTree = []
@@ -58,6 +58,6 @@ export default App.component('historyFileTree', {
     onSelectedFileChange: '&',
     isLoading: '<',
   },
-  controller: historyFileTreeController,
+  controller: ['$scope', historyFileTreeController],
   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
  */
 import App from '../../../base'
-const historyLabelController = function ($scope, $element, $attrs, $filter) {
+const historyLabelController = function () {
   const ctrl = this
   ctrl.$onInit = () => {
     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 ColorManager from '../../colors/ColorManager'
 import displayNameForUser from '../util/displayNameForUser'
-const historyLabelsListController = function ($scope, $element, $attrs) {
+const historyLabelsListController = function ($scope) {
   const ctrl = this
   ctrl.isDragging = false
   ctrl.versionsWithLabels = []
@@ -170,6 +170,6 @@ export default App.component('historyLabelsList', {
     onRangeSelect: '&',
     onLabelDelete: '&',
   },
-  controller: historyLabelsListController,
+  controller: ['$scope', historyLabelsListController],
   templateUrl: 'historyLabelsListTpl',
 })

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

@@ -11,8 +11,11 @@
  */
 import App from '../../../base'
 
-export default App.controller(
-  'HistoryV2AddLabelModalController',
+export default App.controller('HistoryV2AddLabelModalController', [
+  '$scope',
+  '$modalInstance',
+  'ide',
+  'update',
   function ($scope, $modalInstance, ide, update) {
     $scope.update = update
     $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'
 
-export default App.controller(
-  'HistoryV2DeleteLabelModalController',
+export default App.controller('HistoryV2DeleteLabelModalController', [
+  '$scope',
+  '$modalInstance',
+  'ide',
+  'labelDetails',
   function ($scope, $modalInstance, ide, labelDetails) {
     $scope.labelDetails = labelDetails
     $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'
 
-export default App.controller(
-  'HistoryV2FileTreeController',
+export default App.controller('HistoryV2FileTreeController', [
+  '$scope',
+  'ide',
   function ($scope, ide) {
     $scope.handleFileSelection = file => {
       ide.historyManager.selectFile(file)
     }
-  }
-)
+  },
+])

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

@@ -12,8 +12,10 @@
  */
 import App from '../../../base'
 
-export default App.controller(
-  'HistoryV2ListController',
+export default App.controller('HistoryV2ListController', [
+  '$scope',
+  '$modal',
+  'ide',
   function ($scope, $modal, ide) {
     $scope.hoveringOverListSelectors = 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 { 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.currentLabel = null
 
@@ -120,5 +124,5 @@ export default App.controller(
         })
         .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'
 
-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'
 
-export default App.controller(
-  'BulkActionsModalController',
+export default App.controller('BulkActionsModalController', [
+  '$scope',
+  '$modalInstance',
+  'isAccept',
+  'nChanges',
   function ($scope, $modalInstance, isAccept, nChanges) {
     $scope.isAccept = isAccept
     $scope.nChanges = nChanges
     $scope.cancel = () => $modalInstance.dismiss()
     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 getMeta from '../../../utils/meta'
 
-export default App.controller(
-  'ReviewPanelController',
+export default App.controller('ReviewPanelController', [
+  '$scope',
+  '$element',
+  'ide',
+  '$timeout',
+  '$http',
+  '$modal',
+  'eventTracking',
+  'localStorage',
   function (
     $scope,
     $element,
@@ -1412,8 +1419,8 @@ export default App.controller(
 
     // Add methods somewhere that React can see them
     $scope.reviewPanel.saveEdit = $scope.saveEdit
-  }
-)
+  },
+])
 
 // send events to the CodeMirror 6 track changes extension
 const dispatchReviewPanelEvent = (type, payload) => {

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

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

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

@@ -1,17 +1,21 @@
 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 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 './cdn-load-test'
 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'])

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

@@ -1,14 +1,20 @@
 import App from '../base'
 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 { 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'
 
     $scope.upgradeComplete = false
@@ -43,5 +44,5 @@ export default App.controller(
           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
  */
 import App from '../base'
-App.controller(
-  'BonusLinksController',
-  ($scope, $modal) =>
-    ($scope.openLinkToUsModal = () =>
+App.controller('BonusLinksController', [
+  '$scope',
+  '$modal',
+  function ($scope, $modal) {
+    $scope.openLinkToUsModal = () =>
       $modal.open({
         templateUrl: 'BonusLinkToUsModal',
         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 getMeta from '../utils/meta'
 
-export default App.controller(
-  'ClearSessionsController',
+export default App.controller('ClearSessionsController', [
+  '$scope',
+  '$http',
   function ($scope, $http) {
     $scope.state = {
       otherSessions: getMeta('ol-otherSessions'),
@@ -34,5 +35,5 @@ export default App.controller(
         })
         .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 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) {
   const href = $(e.target).attr('href')

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

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

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

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

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

@@ -7,8 +7,11 @@
  */
 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 { 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'
 const MESSAGE_POLL_INTERVAL = 15 * 60 * 1000
 // 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.protected = $scope.message._id === 'protected'
     $scope.htmlContent = $scope.message.content
@@ -55,5 +59,5 @@ export default App.controller(
         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 { debugConsole } from '@/utils/debugging'
-App.controller(
-  'TokenAccessPageController',
-  ($scope, $http, $location, localStorage) => {
+App.controller('TokenAccessPageController', [
+  '$scope',
+  '$http',
+  '$location',
+  function ($scope, $http, $location) {
     window.S = $scope
     $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'
 
-App.controller(
-  'TranslationsPopupController',
+App.controller('TranslationsPopupController', [
+  '$scope',
+  'ipCookie',
+  'localStorage',
   function ($scope, ipCookie, localStorage) {
     function getStoredDismissal() {
       const localStore = localStorage('hide-i18n-notification')
@@ -28,5 +30,5 @@ App.controller(
       localStorage('hide-i18n-notification', 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([
   '$provide',
-  $provide =>
-    $provide.decorator('$exceptionHandler', [
+  function ($provide) {
+    return $provide.decorator('$exceptionHandler', [
       '$log',
       '$delegate',
       ($log, $delegate) =>
@@ -37,39 +37,48 @@ app.config([
 
           return $delegate(exception, cause)
         },
-    ]),
+    ])
+  },
 ])
 
 // Interceptor to check auth failures in all $http requests
 // http://bahmutov.calepin.co/catch-all-errors-in-angular-app.html
 
-app.factory('unAuthHttpResponseInterceptor', ($q, $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([
   '$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'
 
-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'
 
-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
+  },
+])