Sfoglia il codice sorgente

Convert utility functions to TypeScript (#22658)

* Convert event-tracking to TypeScript

* Convert local-storage to TypeScript

* Convert mapSeries to TypeScript

* Convert SessionStorage to TypeScript

* Convert account-upgrade to TypeScript

* Convert isValidTeXFile to TypeScript

* Convert date functions to TypeScript

* Convert EventEmitter to TypeScript

* Convert isNetworkError to TypeScript

* Convert webpack-public-path to TypeScript

* Convert displayNameForUser to TypeScript

GitOrigin-RevId: 79c5a2d1101fcd520f3116f0f4af29d974189d94
Alf Eaton 1 anno fa
parent
commit
2fbb4615f9
52 ha cambiato i file con 203 aggiunte e 210 eliminazioni
  1. 1 1
      services/web/frontend/js/features/editor-left-menu/components/settings/settings-document.tsx
  2. 1 2
      services/web/frontend/js/features/history/components/change-list/owner-paywall-prompt.tsx
  3. 22 0
      services/web/frontend/js/features/history/utils/display-name-for-user.ts
  4. 1 1
      services/web/frontend/js/features/history/utils/highlights-from-diff-response.ts
  5. 1 1
      services/web/frontend/js/features/ide-react/context/outline-context.tsx
  6. 1 1
      services/web/frontend/js/features/pdf-preview/util/pdf-caching-transport.js
  7. 1 1
      services/web/frontend/js/features/share-project-modal/components/add-collaborators-upgrade.tsx
  8. 1 1
      services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/access-levels-changed.tsx
  9. 1 1
      services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/add-collaborators-upgrade.tsx
  10. 1 1
      services/web/frontend/js/features/source-editor/components/editor-switch.tsx
  11. 1 1
      services/web/frontend/js/features/source-editor/components/review-panel/upgrade-track-changes-modal.tsx
  12. 2 2
      services/web/frontend/js/features/source-editor/hooks/use-codemirror-scope.ts
  13. 0 24
      services/web/frontend/js/ide/history/util/displayNameForUser.js
  14. 33 11
      services/web/frontend/js/infrastructure/event-tracking.ts
  15. 13 8
      services/web/frontend/js/infrastructure/local-storage.ts
  16. 0 16
      services/web/frontend/js/infrastructure/promise.js
  17. 13 0
      services/web/frontend/js/infrastructure/promise.ts
  18. 13 8
      services/web/frontend/js/infrastructure/session-storage.ts
  19. 0 49
      services/web/frontend/js/main/account-upgrade.js
  20. 29 0
      services/web/frontend/js/main/account-upgrade.ts
  21. 1 3
      services/web/frontend/js/main/is-valid-tex-file.ts
  22. 1 1
      services/web/frontend/js/pages/project-list.tsx
  23. 1 1
      services/web/frontend/js/pages/sharing-updates.tsx
  24. 1 1
      services/web/frontend/js/pages/token-access.tsx
  25. 1 1
      services/web/frontend/js/pages/user/settings.jsx
  26. 1 1
      services/web/frontend/js/pages/user/subscription/base.js
  27. 2 2
      services/web/frontend/js/shared/components/start-free-trial-button.tsx
  28. 17 7
      services/web/frontend/js/utils/EventEmitter.ts
  29. 2 2
      services/web/frontend/js/utils/dates.ts
  30. 2 2
      services/web/frontend/js/utils/is-network-error.ts
  31. 0 0
      services/web/frontend/js/utils/webpack-public-path.ts
  32. 1 1
      services/web/frontend/stories/settings/leavers-survey-alert.stories.tsx
  33. 1 1
      services/web/test/frontend/components/pdf-preview/pdf-preview.spec.tsx
  34. 7 8
      services/web/test/frontend/features/editor-left-menu/components/settings/settings-document.test.tsx
  35. 1 1
      services/web/test/frontend/features/editor-navigation-toolbar/components/layout-dropdown-button.test.jsx
  36. 3 14
      services/web/test/frontend/features/history/utils/display-name-for-user.test.ts
  37. 1 1
      services/web/test/frontend/features/project-list/components/current-plan-widget.test.tsx
  38. 1 1
      services/web/test/frontend/features/project-list/components/notifications.test.tsx
  39. 1 1
      services/web/test/frontend/features/project-list/components/project-list-root.test.tsx
  40. 1 1
      services/web/test/frontend/features/project-list/components/project-search.test.tsx
  41. 1 1
      services/web/test/frontend/features/project-list/components/table/cells/action-buttons/compile-and-download-project-pdf-button.test.tsx
  42. 1 1
      services/web/test/frontend/features/project-list/components/table/projects-action-modal.test.tsx
  43. 2 2
      services/web/test/frontend/features/settings/components/leavers-survey-alert.test.tsx
  44. 1 1
      services/web/test/frontend/features/settings/components/linking/integration-widget.test.tsx
  45. 1 1
      services/web/test/frontend/features/settings/components/root.test.tsx
  46. 1 1
      services/web/test/frontend/features/settings/context/user-email-context.test.tsx
  47. 1 1
      services/web/test/frontend/features/subscription/components/dashboard/states/active/active.test.tsx
  48. 1 1
      services/web/test/frontend/infrastructure/local-storage.test.js
  49. 1 1
      services/web/test/frontend/shared/components/notification.test.tsx
  50. 1 1
      services/web/test/frontend/shared/hooks/use-persisted-state.test.tsx
  51. 9 20
      services/web/test/frontend/utils/EventEmitterTests.js
  52. 2 0
      services/web/types/window.ts

+ 1 - 1
services/web/frontend/js/features/editor-left-menu/components/settings/settings-document.tsx

@@ -1,6 +1,6 @@
 import { useMemo } from 'react'
 import { useTranslation } from 'react-i18next'
-import isValidTeXFile from '../../../../main/is-valid-tex-file'
+import { isValidTeXFile } from '../../../../main/is-valid-tex-file'
 import { useEditorContext } from '../../../../shared/context/editor-context'
 import { useProjectSettingsContext } from '../../context/project-settings-context'
 import SettingsMenuSelect from './settings-menu-select'

+ 1 - 2
services/web/frontend/js/features/history/components/change-list/owner-paywall-prompt.tsx

@@ -3,7 +3,6 @@ import Icon from '../../../../shared/components/icon'
 import { useCallback, useEffect, useState } from 'react'
 import * as eventTracking from '../../../../infrastructure/event-tracking'
 import StartFreeTrialButton from '../../../../shared/components/start-free-trial-button'
-import { paywallPrompt } from '../../../../main/account-upgrade'
 import { useFeatureFlag } from '@/shared/context/split-test-context'
 
 function FeatureItem({ text }: { text: string }) {
@@ -22,7 +21,7 @@ export function OwnerPaywallPrompt() {
 
   useEffect(() => {
     eventTracking.send('subscription-funnel', 'editor-click-feature', 'history')
-    paywallPrompt('history')
+    eventTracking.sendMB('paywall-prompt', { 'paywall-type': 'history' })
   }, [])
 
   const handleFreeTrialClick = useCallback(() => {

+ 22 - 0
services/web/frontend/js/features/history/utils/display-name-for-user.ts

@@ -0,0 +1,22 @@
+import { User } from '@/features/history/services/types/shared'
+import getMeta from '@/utils/meta'
+import { formatUserName } from '@/features/history/utils/history-details'
+
+export default function displayNameForUser(
+  user:
+    | (User & {
+        name?: string
+      })
+    | null
+) {
+  if (user == null) {
+    return 'Anonymous'
+  }
+  if (user.id === getMeta('ol-user').id) {
+    return 'you'
+  }
+  if (user.name != null) {
+    return user.name
+  }
+  return formatUserName(user)
+}

+ 1 - 1
services/web/frontend/js/features/history/utils/highlights-from-diff-response.ts

@@ -1,8 +1,8 @@
-import displayNameForUser from '../../../ide/history/util/displayNameForUser'
 import moment from 'moment/moment'
 import ColorManager from '../../../ide/colors/ColorManager'
 import { DocDiffChunk, Highlight } from '../services/types/doc'
 import { TFunction } from 'i18next'
+import displayNameForUser from './display-name-for-user'
 
 export function highlightsFromDiffResponse(
   chunks: DocDiffChunk[],

+ 1 - 1
services/web/frontend/js/features/ide-react/context/outline-context.tsx

@@ -12,7 +12,7 @@ import useScopeEventEmitter from '@/shared/hooks/use-scope-event-emitter'
 import useEventListener from '@/shared/hooks/use-event-listener'
 import * as eventTracking from '@/infrastructure/event-tracking'
 import useScopeValue from '@/shared/hooks/use-scope-value'
-import isValidTeXFile from '@/main/is-valid-tex-file'
+import { isValidTeXFile } from '@/main/is-valid-tex-file'
 import localStorage from '@/infrastructure/local-storage'
 import { useProjectContext } from '@/shared/context/project-context'
 

+ 1 - 1
services/web/frontend/js/features/pdf-preview/util/pdf-caching-transport.js

@@ -9,7 +9,7 @@ import {
   prefetchLargeEnabled,
   trackPdfDownloadEnabled,
 } from './pdf-caching-flags'
-import { isNetworkError } from '@/utils/isNetworkError'
+import { isNetworkError } from '@/utils/is-network-error'
 import { debugConsole } from '@/utils/debugging'
 import { PDFJS } from './pdf-js'
 

+ 1 - 1
services/web/frontend/js/features/share-project-modal/components/add-collaborators-upgrade.tsx

@@ -1,7 +1,7 @@
 import { useState } from 'react'
 import { useTranslation } from 'react-i18next'
 import { useUserContext } from '../../../shared/context/user-context'
-import { upgradePlan } from '../../../main/account-upgrade'
+import { upgradePlan } from '@/main/account-upgrade'
 import StartFreeTrialButton from '../../../shared/components/start-free-trial-button'
 import Icon from '../../../shared/components/icon'
 import { useFeatureFlag } from '../../../shared/context/split-test-context'

+ 1 - 1
services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/access-levels-changed.tsx

@@ -1,6 +1,6 @@
 import { useTranslation } from 'react-i18next'
 import Notification from '@/shared/components/notification'
-import { upgradePlan } from '../../../../main/account-upgrade'
+import { upgradePlan } from '@/main/account-upgrade'
 import { useProjectContext } from '@/shared/context/project-context'
 import { useUserContext } from '@/shared/context/user-context'
 import { sendMB } from '@/infrastructure/event-tracking'

+ 1 - 1
services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/add-collaborators-upgrade.tsx

@@ -1,6 +1,6 @@
 import { useTranslation } from 'react-i18next'
 import Notification from '@/shared/components/notification'
-import { upgradePlan } from '../../../../main/account-upgrade'
+import { upgradePlan } from '@/main/account-upgrade'
 import { linkSharingEnforcementDate } from '../../utils/link-sharing'
 import { useProjectContext } from '@/shared/context/project-context'
 import { useUserContext } from '@/shared/context/user-context'

+ 1 - 1
services/web/frontend/js/features/source-editor/components/editor-switch.tsx

@@ -3,7 +3,7 @@ import useScopeValue from '@/shared/hooks/use-scope-value'
 import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
 import useTutorial from '@/shared/hooks/promotions/use-tutorial'
 import { sendMB } from '../../../infrastructure/event-tracking'
-import isValidTeXFile from '../../../main/is-valid-tex-file'
+import { isValidTeXFile } from '../../../main/is-valid-tex-file'
 import { useTranslation } from 'react-i18next'
 import {
   EditorSwitchBeginnerTooltip,

+ 1 - 1
services/web/frontend/js/features/source-editor/components/review-panel/upgrade-track-changes-modal.tsx

@@ -2,7 +2,7 @@ import { useTranslation } from 'react-i18next'
 import Icon from '../../../../shared/components/icon'
 import { useProjectContext } from '../../../../shared/context/project-context'
 import { useUserContext } from '../../../../shared/context/user-context'
-import { startFreeTrial, upgradePlan } from '../../../../main/account-upgrade'
+import { startFreeTrial, upgradePlan } from '@/main/account-upgrade'
 import { memo } from 'react'
 import { useFeatureFlag } from '@/shared/context/split-test-context'
 import OLModal, {

+ 2 - 2
services/web/frontend/js/features/source-editor/hooks/use-codemirror-scope.ts

@@ -47,7 +47,7 @@ import { setVisual } from '../extensions/visual/visual'
 import { useFileTreePathContext } from '@/features/file-tree/contexts/file-tree-path'
 import { useUserSettingsContext } from '@/shared/context/user-settings-context'
 import { setDocName } from '@/features/source-editor/extensions/doc-name'
-import isValidTexFile from '@/main/is-valid-tex-file'
+import { isValidTeXFile } from '@/main/is-valid-tex-file'
 import { captureException } from '@/infrastructure/error-reporter'
 import grammarlyExtensionPresent from '@/shared/utils/grammarly'
 import { DocumentContainer } from '@/features/ide-react/editor/document-container'
@@ -288,7 +288,7 @@ function useCodeMirrorScope(view: EditorView) {
 
   const { previewByPath } = useFileTreePathContext()
 
-  const showVisual = visual && isValidTexFile(docName)
+  const showVisual = visual && isValidTeXFile(docName)
 
   const visualRef = useRef({
     previewByPath,

+ 0 - 24
services/web/frontend/js/ide/history/util/displayNameForUser.js

@@ -1,24 +0,0 @@
-import getMeta from '@/utils/meta'
-
-export default function displayNameForUser(user) {
-  if (user == null) {
-    return 'Anonymous'
-  }
-  if (user.id === getMeta('ol-user').id) {
-    return 'you'
-  }
-  if (user.name != null) {
-    return user.name
-  }
-  let name = [user.first_name, user.last_name]
-    .filter(n => n != null)
-    .join(' ')
-    .trim()
-  if (name === '') {
-    name = user.email.split('@')[0]
-  }
-  if (name == null || name === '') {
-    return '?'
-  }
-  return name
-}

+ 33 - 11
services/web/frontend/js/infrastructure/event-tracking.js → services/web/frontend/js/infrastructure/event-tracking.ts

@@ -1,25 +1,40 @@
-import sessionStorage from '../infrastructure/session-storage'
+import sessionStorage from './session-storage'
 import getMeta from '@/utils/meta'
 
+type Segmentation = Record<
+  string,
+  string | number | boolean | undefined | unknown | any // TODO: RecurlyError
+>
+
 const CACHE_KEY = 'mbEvents'
 
-function alreadySent(key) {
+function alreadySent(key: string) {
   const eventCache = sessionStorage.getItem(CACHE_KEY) || {}
   return !!eventCache[key]
 }
-function markAsSent(key) {
+function markAsSent(key: string) {
   const eventCache = sessionStorage.getItem(CACHE_KEY) || {}
   eventCache[key] = true
   sessionStorage.setItem(CACHE_KEY, eventCache)
 }
 
-export function send(category, action, label, value) {
+export function send(
+  category: string,
+  action: string,
+  label?: string,
+  value?: string
+) {
   if (typeof window.ga === 'function') {
     window.ga('send', 'event', category, action, label, value)
   }
 }
 
-export function sendOnce(category, action, label, value) {
+export function sendOnce(
+  category: string,
+  action: string,
+  label: string,
+  value: string
+) {
   if (alreadySent(category)) return
   if (typeof window.ga !== 'function') return
 
@@ -27,7 +42,7 @@ export function sendOnce(category, action, label, value) {
   markAsSent(category)
 }
 
-export function sendMB(key, segmentation = {}) {
+export function sendMB(key: string, segmentation: Segmentation = {}) {
   if (!segmentation.page) {
     segmentation.page = window.location.pathname
   }
@@ -40,21 +55,28 @@ export function sendMB(key, segmentation = {}) {
   }
 }
 
-export function sendMBOnce(key, segmentation = {}) {
+export function sendMBOnce(key: string, segmentation: Segmentation = {}) {
   if (alreadySent(key)) return
   sendMB(key, segmentation)
   markAsSent(key)
 }
 
-export function sendMBSampled(key, body = {}, rate = 0.01) {
+export function sendMBSampled(
+  key: string,
+  segmentation: Segmentation = {},
+  rate = 0.01
+) {
   if (Math.random() < rate) {
-    sendMB(key, body)
+    sendMB(key, segmentation)
   }
 }
 
 const sentOncePerPageLoad = new Set()
 
-export function sendMBOncePerPageLoad(key, segmentation = {}) {
+export function sendMBOncePerPageLoad(
+  key: string,
+  segmentation: Segmentation = {}
+) {
   if (sentOncePerPageLoad.has(key)) return
   sendMB(key, segmentation)
   sentOncePerPageLoad.add(key)
@@ -66,7 +88,7 @@ export function sendMBOncePerPageLoad(key, segmentation = {}) {
 // @screen-sm: 768px;
 export const isSmallDevice = window.screen.width < 768
 
-function sendBeacon(key, data) {
+function sendBeacon(key: string, data: Segmentation) {
   if (!navigator || !navigator.sendBeacon) return
   if (!getMeta('ol-ExposedSettings').isOverleaf) return
 

+ 13 - 8
services/web/frontend/js/infrastructure/local-storage.js → services/web/frontend/js/infrastructure/local-storage.ts

@@ -12,7 +12,11 @@ import { debugConsole } from '@/utils/debugging'
  * @param {string?} key Key passed to the localStorage function (if any)
  * @param {any?} value Value passed to the localStorage function (if any)
  */
-const callSafe = function (fn, key, value) {
+const callSafe = function (
+  fn: (...args: any) => any,
+  key?: string,
+  value?: any
+) {
   try {
     return fn(key, value)
   } catch (e) {
@@ -21,11 +25,12 @@ const callSafe = function (fn, key, value) {
   }
 }
 
-const getItem = function (key) {
-  return JSON.parse(localStorage.getItem(key))
+const getItem = function (key: string) {
+  const value = localStorage.getItem(key)
+  return value === null ? null : JSON.parse(value)
 }
 
-const setItem = function (key, value) {
+const setItem = function (key: string, value: any) {
   localStorage.setItem(key, JSON.stringify(value))
 }
 
@@ -33,15 +38,15 @@ const clear = function () {
   localStorage.clear()
 }
 
-const removeItem = function (key) {
+const removeItem = function (key: string) {
   return localStorage.removeItem(key)
 }
 
 const customLocalStorage = {
-  getItem: key => callSafe(getItem, key),
-  setItem: (key, value) => callSafe(setItem, key, value),
+  getItem: (key: string) => callSafe(getItem, key),
+  setItem: (key: string, value: any) => callSafe(setItem, key, value),
   clear: () => callSafe(clear),
-  removeItem: key => callSafe(removeItem, key),
+  removeItem: (key: string) => callSafe(removeItem, key),
 }
 
 export default customLocalStorage

+ 0 - 16
services/web/frontend/js/infrastructure/promise.js

@@ -1,16 +0,0 @@
-/**
- * run `fn` in serie for all values, and resolve with an array of the results
- * inspired by https://stackoverflow.com/a/50506360/1314820
- * @template T the input array's item type
- * @template V the `fn` function's return type
- * @param {T[]} values
- * @param {(item: T) => Promise<V>} fn
- * @returns {V[]}
- */
-export function mapSeries(values, fn) {
-  return values.reduce(async (promiseChain, value) => {
-    const chainResults = await promiseChain
-    const currentResult = await fn(value)
-    return [...chainResults, currentResult]
-  }, Promise.resolve([]))
-}

+ 13 - 0
services/web/frontend/js/infrastructure/promise.ts

@@ -0,0 +1,13 @@
+/**
+ * run `fn` in series for all values, and resolve with an array of the results
+ */
+export const mapSeries = async <T = any, V = any>(
+  values: T[],
+  fn: (item: T) => Promise<V>
+) => {
+  const output: V[] = []
+  for (const value of values) {
+    output.push(await fn(value))
+  }
+  return output
+}

+ 13 - 8
services/web/frontend/js/infrastructure/session-storage.js → services/web/frontend/js/infrastructure/session-storage.ts

@@ -12,7 +12,11 @@ import { debugConsole } from '@/utils/debugging'
  * @param {string?} key Key passed to the sessionStorage function (if any)
  * @param {any?} value Value passed to the sessionStorage function (if any)
  */
-const callSafe = function (fn, key, value) {
+const callSafe = function (
+  fn: (...args: any) => any,
+  key?: string,
+  value?: any
+) {
   try {
     return fn(key, value)
   } catch (e) {
@@ -21,11 +25,12 @@ const callSafe = function (fn, key, value) {
   }
 }
 
-const getItem = function (key) {
-  return JSON.parse(sessionStorage.getItem(key))
+const getItem = function (key: string) {
+  const value = sessionStorage.getItem(key)
+  return value === null ? null : JSON.parse(value)
 }
 
-const setItem = function (key, value) {
+const setItem = function (key: string, value: any) {
   sessionStorage.setItem(key, JSON.stringify(value))
 }
 
@@ -33,15 +38,15 @@ const clear = function () {
   sessionStorage.clear()
 }
 
-const removeItem = function (key) {
+const removeItem = function (key: string) {
   return sessionStorage.removeItem(key)
 }
 
 const customSessionStorage = {
-  getItem: key => callSafe(getItem, key),
-  setItem: (key, value) => callSafe(setItem, key, value),
+  getItem: (key: string) => callSafe(getItem, key),
+  setItem: (key: string, value: any) => callSafe(setItem, key, value),
   clear: () => callSafe(clear),
-  removeItem: key => callSafe(removeItem, key),
+  removeItem: (key: string) => callSafe(removeItem, key),
 }
 
 export default customSessionStorage

+ 0 - 49
services/web/frontend/js/main/account-upgrade.js

@@ -1,49 +0,0 @@
-import * as eventTracking from '../infrastructure/event-tracking'
-
-function startFreeTrial(source, version, $scope, variant) {
-  const eventSegmentation = { 'paywall-type': source }
-  if (variant) {
-    eventSegmentation.variant = variant
-  }
-
-  eventTracking.send('subscription-funnel', 'upgraded-free-trial', source)
-  eventTracking.sendMB('paywall-click', eventSegmentation)
-
-  const searchParams = new URLSearchParams({
-    itm_campaign: source,
-  })
-
-  if (version) {
-    searchParams.set('itm_content', version)
-  }
-
-  if ($scope) {
-    $scope.startedFreeTrial = true
-  }
-
-  window.open(`/user/subscription/choose-your-plan?${searchParams.toString()}`)
-}
-
-function upgradePlan(source, $scope) {
-  const w = window.open()
-  const go = function () {
-    if (typeof ga === 'function') {
-      ga('send', 'event', 'subscription-funnel', 'upgraded-plan', source)
-    }
-    const url = '/user/subscription'
-
-    if ($scope) {
-      $scope.startedFreeTrial = true
-    }
-
-    w.location = url
-  }
-
-  go()
-}
-
-function paywallPrompt(source) {
-  eventTracking.sendMB('paywall-prompt', { 'paywall-type': source })
-}
-
-export { startFreeTrial, upgradePlan, paywallPrompt }

+ 29 - 0
services/web/frontend/js/main/account-upgrade.ts

@@ -0,0 +1,29 @@
+import * as eventTracking from '../infrastructure/event-tracking'
+
+export function startFreeTrial(source: string, variant?: string) {
+  const eventSegmentation: Record<string, string> = { 'paywall-type': source }
+  if (variant) {
+    eventSegmentation.variant = variant
+  }
+
+  eventTracking.send('subscription-funnel', 'upgraded-free-trial', source)
+  eventTracking.sendMB('paywall-click', eventSegmentation)
+
+  const searchParams = new URLSearchParams({
+    itm_campaign: source,
+  })
+
+  window.open(`/user/subscription/choose-your-plan?${searchParams.toString()}`)
+}
+
+export function upgradePlan(source: string) {
+  const openedWindow = window.open()
+
+  if (typeof window.ga === 'function') {
+    window.ga('send', 'event', 'subscription-funnel', 'upgraded-plan', source)
+  }
+
+  if (openedWindow) {
+    openedWindow.location = '/user/subscription'
+  }
+}

+ 1 - 3
services/web/frontend/js/main/is-valid-tex-file.js → services/web/frontend/js/main/is-valid-tex-file.ts

@@ -1,6 +1,6 @@
 import getMeta from '@/utils/meta'
 
-function isValidTeXFile(filename) {
+export const isValidTeXFile = (filename: string) => {
   const validTeXFileRegExp = new RegExp(
     `\\.(${getMeta('ol-ExposedSettings').validRootDocExtensions.join('|')})$`,
     'i'
@@ -8,5 +8,3 @@ function isValidTeXFile(filename) {
 
   return validTeXFileRegExp.test(filename)
 }
-
-export default isValidTeXFile

+ 1 - 1
services/web/frontend/js/pages/project-list.tsx

@@ -1,5 +1,5 @@
 import './../utils/meta'
-import './../utils/webpack-public-path'
+import '../utils/webpack-public-path'
 import './../infrastructure/error-reporter'
 import '@/i18n'
 import '../features/event-tracking'

+ 1 - 1
services/web/frontend/js/pages/sharing-updates.tsx

@@ -1,5 +1,5 @@
 import './../utils/meta'
-import './../utils/webpack-public-path'
+import '../utils/webpack-public-path'
 import './../infrastructure/error-reporter'
 import '@/i18n'
 import ReactDOM from 'react-dom'

+ 1 - 1
services/web/frontend/js/pages/token-access.tsx

@@ -1,5 +1,5 @@
 import './../utils/meta'
-import './../utils/webpack-public-path'
+import '../utils/webpack-public-path'
 import './../infrastructure/error-reporter'
 import '@/i18n'
 import ReactDOM from 'react-dom'

+ 1 - 1
services/web/frontend/js/pages/user/settings.jsx

@@ -1,6 +1,6 @@
 import '../../marketing'
 import './../../utils/meta'
-import './../../utils/webpack-public-path'
+import '../../utils/webpack-public-path'
 import './../../infrastructure/error-reporter'
 import '@/i18n'
 import '../../features/settings/components/root'

+ 1 - 1
services/web/frontend/js/pages/user/subscription/base.js

@@ -1,5 +1,5 @@
 import './../../../utils/meta'
-import './../../../utils/webpack-public-path'
+import '../../../utils/webpack-public-path'
 import './../../../infrastructure/error-reporter'
 import '@/i18n'
 import '../../../features/event-tracking'

+ 2 - 2
services/web/frontend/js/shared/components/start-free-trial-button.tsx

@@ -1,6 +1,6 @@
 import { MouseEventHandler, useCallback, useEffect } from 'react'
 import { useTranslation } from 'react-i18next'
-import { startFreeTrial } from '../../main/account-upgrade'
+import { startFreeTrial } from '@/main/account-upgrade'
 import * as eventTracking from '../../infrastructure/event-tracking'
 import OLButton from '@/features/ui/components/ol/ol-button'
 
@@ -41,7 +41,7 @@ export default function StartFreeTrialButton({
         handleClick(event)
       }
 
-      startFreeTrial(source, null, null, variant)
+      startFreeTrial(source, variant)
     },
     [handleClick, source, variant]
   )

+ 17 - 7
services/web/frontend/js/utils/EventEmitter.js → services/web/frontend/js/utils/EventEmitter.ts

@@ -7,11 +7,19 @@
 // Remove a listener for the foo event with the bar namespace: .off 'foo.bar'
 
 export default class EventEmitter {
+  events: Record<
+    string,
+    {
+      callback: (...args: any[]) => void
+      namespace: string
+    }[]
+  >
+
   constructor() {
     this.events = {}
   }
 
-  on(event, callback) {
+  on(event: string, callback: (...args: any[]) => void) {
     if (!this.events) {
       this.events = {}
     }
@@ -26,7 +34,7 @@ export default class EventEmitter {
     })
   }
 
-  off(event, cb) {
+  off(event?: string, callback?: (...args: any[]) => void) {
     if (!this.events) {
       this.events = {}
     }
@@ -36,8 +44,10 @@ export default class EventEmitter {
       if (!this.events[event]) {
         this.events[event] = []
       }
-      if (cb) {
-        this.events[event] = this.events[event].filter(e => e.callback !== cb)
+      if (callback) {
+        this.events[event] = this.events[event].filter(
+          e => e.callback !== callback
+        )
       } else if (!namespace) {
         // Clear all listeners for event
         delete this.events[event]
@@ -53,7 +63,7 @@ export default class EventEmitter {
     }
   }
 
-  trigger(event, ...args) {
+  trigger(event: string, ...args: any[]) {
     if (!this.events) {
       this.events = {}
     }
@@ -62,7 +72,7 @@ export default class EventEmitter {
     }
   }
 
-  emit(...args) {
-    this.trigger(...args)
+  emit(event: string, ...args: any[]) {
+    this.trigger(event, ...args)
   }
 }

+ 2 - 2
services/web/frontend/js/utils/dates.js → services/web/frontend/js/utils/dates.ts

@@ -1,6 +1,6 @@
 import moment from 'moment'
 
-export function formatDate(date, format) {
+export function formatDate(date: moment.MomentInput, format?: string) {
   if (!date) return 'N/A'
   if (format == null) {
     format = 'Do MMM YYYY, h:mm a'
@@ -8,6 +8,6 @@ export function formatDate(date, format) {
   return moment(date).format(format)
 }
 
-export function fromNowDate(date) {
+export function fromNowDate(date: moment.MomentInput | string) {
   return moment(date).fromNow()
 }

+ 2 - 2
services/web/frontend/js/utils/isNetworkError.js → services/web/frontend/js/utils/is-network-error.ts

@@ -68,6 +68,6 @@ const NETWORK_ERRORS = [
   '요청한 시간이 초과되었습니다.',
 ]
 
-export function isNetworkError(err) {
-  return NETWORK_ERRORS.includes(err?.message)
+export function isNetworkError(err?: Error) {
+  return err && NETWORK_ERRORS.includes(err.message)
 }

+ 0 - 0
services/web/frontend/js/utils/webpack-public-path.js → services/web/frontend/js/utils/webpack-public-path.ts


+ 1 - 1
services/web/frontend/stories/settings/leavers-survey-alert.stories.tsx

@@ -1,7 +1,7 @@
 import EmailsSection from '../../js/features/settings/components/emails-section'
 import { UserEmailsProvider } from '../../js/features/settings/context/user-email-context'
 import { LeaversSurveyAlert } from '../../js/features/settings/components/leavers-survey-alert'
-import localStorage from '../../js/infrastructure/local-storage'
+import localStorage from '@/infrastructure/local-storage'
 import { bsVersionDecorator } from '../../../.storybook/utils/with-bootstrap-switcher'
 
 export const SurveyAlert = () => {

+ 1 - 1
services/web/test/frontend/components/pdf-preview/pdf-preview.spec.tsx

@@ -1,5 +1,5 @@
 import '../../helpers/bootstrap-3'
-import localStorage from '../../../../frontend/js/infrastructure/local-storage'
+import localStorage from '@/infrastructure/local-storage'
 import PdfPreview from '../../../../frontend/js/features/pdf-preview/components/pdf-preview'
 import { EditorProviders } from '../../helpers/editor-providers'
 import { mockScope } from './scope'

+ 7 - 8
services/web/test/frontend/features/editor-left-menu/components/settings/settings-document.test.tsx

@@ -1,17 +1,13 @@
 import { screen, within } from '@testing-library/dom'
 import { expect } from 'chai'
-import sinon from 'sinon'
 import fetchMock from 'fetch-mock'
 import SettingsDocument from '../../../../../../frontend/js/features/editor-left-menu/components/settings/settings-document'
-import * as isValidTeXFileModule from '../../../../../../frontend/js/main/is-valid-tex-file'
 import { Folder } from '../../../../../../types/folder'
 import { EditorLeftMenuProvider } from '@/features/editor-left-menu/components/editor-left-menu-context'
 import { render } from '@testing-library/react'
 import { EditorProviders } from '../../../../helpers/editor-providers'
 
 describe('<SettingsDocument />', function () {
-  let isValidTeXFileStub: sinon.SinonStub
-
   const rootFolder: Folder = {
     _id: 'root-folder-id',
     name: 'rootFolder',
@@ -25,15 +21,18 @@ describe('<SettingsDocument />', function () {
     folders: [],
   }
 
+  let originalSettings: typeof window.metaAttributesCache
+
   beforeEach(function () {
-    isValidTeXFileStub = sinon
-      .stub(isValidTeXFileModule, 'default')
-      .returns(true)
+    originalSettings = window.metaAttributesCache.get('ol-ExposedSettings')
+    window.metaAttributesCache.set('ol-ExposedSettings', {
+      validRootDocExtensions: ['tex'],
+    })
   })
 
   afterEach(function () {
     fetchMock.reset()
-    isValidTeXFileStub.restore()
+    window.metaAttributesCache.set('ol-ExposedSettings', originalSettings)
   })
 
   it('shows correct menu', async function () {

+ 1 - 1
services/web/test/frontend/features/editor-navigation-toolbar/components/layout-dropdown-button.test.jsx

@@ -4,7 +4,7 @@ import { expect } from 'chai'
 import { fireEvent, screen } from '@testing-library/react'
 import LayoutDropdownButton from '../../../../../frontend/js/features/editor-navigation-toolbar/components/layout-dropdown-button'
 import { renderWithEditorContext } from '../../../helpers/render-with-context'
-import * as eventTracking from '../../../../../frontend/js/infrastructure/event-tracking'
+import * as eventTracking from '@/infrastructure/event-tracking'
 
 describe('<LayoutDropdownButton />', function () {
   let openStub

+ 3 - 14
services/web/test/frontend/ide/history/util/displayNameForUserTests.js → services/web/test/frontend/features/history/utils/display-name-for-user.test.ts

@@ -1,19 +1,8 @@
-/* eslint-disable
-    no-return-assign,
-*/
-// TODO: This file was created by bulk-decaffeinate.
-// Fix any style issues and re-enable lint.
-/*
- * decaffeinate suggestions:
- * DS102: Remove unnecessary code created because of implicit returns
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
 import { expect } from 'chai'
+import displayNameForUser from '@/features/history/utils/display-name-for-user'
 
-import displayNameForUser from '../../../../../frontend/js/ide/history/util/displayNameForUser'
-
-export default describe('displayNameForUser', function () {
-  const currentUsersId = 42
+describe('displayNameForUser', function () {
+  const currentUsersId = 'user-a'
   beforeEach(function () {
     window.metaAttributesCache.set('ol-user', { id: currentUsersId })
   })

+ 1 - 1
services/web/test/frontend/features/project-list/components/current-plan-widget.test.tsx

@@ -7,7 +7,7 @@ import {
   IndividualPlanSubscription,
 } from '../../../../../types/project/dashboard/subscription'
 import { DeepReadonly } from '../../../../../types/utils'
-import * as eventTracking from '../../../../../frontend/js/infrastructure/event-tracking'
+import * as eventTracking from '@/infrastructure/event-tracking'
 import CurrentPlanWidget from '../../../../../frontend/js/features/project-list/components/current-plan-widget/current-plan-widget'
 
 describe('<CurrentPlanWidget />', function () {

+ 1 - 1
services/web/test/frontend/features/project-list/components/notifications.test.tsx

@@ -36,7 +36,7 @@ import { DeepPartial } from '../../../../../types/utils'
 import { Project } from '../../../../../types/project/dashboard/api'
 import GroupsAndEnterpriseBanner from '../../../../../frontend/js/features/project-list/components/notifications/groups-and-enterprise-banner'
 import GroupSsoSetupSuccess from '../../../../../frontend/js/features/project-list/components/notifications/groups/group-sso-setup-success'
-import localStorage from '../../../../../frontend/js/infrastructure/local-storage'
+import localStorage from '@/infrastructure/local-storage'
 import * as useLocationModule from '../../../../../frontend/js/shared/hooks/use-location'
 import {
   commonsSubscription,

+ 1 - 1
services/web/test/frontend/features/project-list/components/project-list-root.test.tsx

@@ -4,7 +4,7 @@ import fetchMock from 'fetch-mock'
 import sinon from 'sinon'
 import ProjectListRoot from '../../../../../frontend/js/features/project-list/components/project-list-root'
 import { renderWithProjectListContext } from '../helpers/render-with-context'
-import * as eventTracking from '../../../../../frontend/js/infrastructure/event-tracking'
+import * as eventTracking from '@/infrastructure/event-tracking'
 import {
   projectsData,
   owner,

+ 1 - 1
services/web/test/frontend/features/project-list/components/project-search.test.tsx

@@ -2,7 +2,7 @@ import sinon from 'sinon'
 import { render, screen, fireEvent } from '@testing-library/react'
 import { expect } from 'chai'
 import SearchForm from '../../../../../frontend/js/features/project-list/components/search-form'
-import * as eventTracking from '../../../../../frontend/js/infrastructure/event-tracking'
+import * as eventTracking from '@/infrastructure/event-tracking'
 import fetchMock from 'fetch-mock'
 import { Filter } from '../../../../../frontend/js/features/project-list/context/project-list-context'
 import { Tag } from '../../../../../app/src/Features/Tags/types'

+ 1 - 1
services/web/test/frontend/features/project-list/components/table/cells/action-buttons/compile-and-download-project-pdf-button.test.tsx

@@ -5,7 +5,7 @@ import { projectsData } from '../../../../fixtures/projects-data'
 import * as useLocationModule from '../../../../../../../../frontend/js/shared/hooks/use-location'
 import { CompileAndDownloadProjectPDFButtonTooltip } from '../../../../../../../../frontend/js/features/project-list/components/table/cells/action-buttons/compile-and-download-project-pdf-button'
 import fetchMock from 'fetch-mock'
-import * as eventTracking from '../../../../../../../../frontend/js/infrastructure/event-tracking'
+import * as eventTracking from '@/infrastructure/event-tracking'
 
 describe('<CompileAndDownloadProjectPDFButton />', function () {
   let assignStub: sinon.SinonStub

+ 1 - 1
services/web/test/frontend/features/project-list/components/table/projects-action-modal.test.tsx

@@ -7,7 +7,7 @@ import {
   resetProjectListContextFetch,
   renderWithProjectListContext,
 } from '../../helpers/render-with-context'
-import * as eventTracking from '../../../../../../frontend/js/infrastructure/event-tracking'
+import * as eventTracking from '@/infrastructure/event-tracking'
 
 describe('<ProjectsActionModal />', function () {
   const actionHandler = sinon.stub().resolves({})

+ 2 - 2
services/web/test/frontend/features/settings/components/leavers-survey-alert.test.tsx

@@ -3,8 +3,8 @@ import sinon from 'sinon'
 import { fireEvent, screen, render } from '@testing-library/react'
 import { UserEmailsProvider } from '../../../../../frontend/js/features/settings/context/user-email-context'
 import { LeaversSurveyAlert } from '../../../../../frontend/js/features/settings/components/leavers-survey-alert'
-import * as eventTracking from '../../../../../frontend/js/infrastructure/event-tracking'
-import localStorage from '../../../../../frontend/js/infrastructure/local-storage'
+import * as eventTracking from '@/infrastructure/event-tracking'
+import localStorage from '@/infrastructure/local-storage'
 import fetchMock from 'fetch-mock'
 
 function renderWithProvider() {

+ 1 - 1
services/web/test/frontend/features/settings/components/linking/integration-widget.test.tsx

@@ -2,7 +2,7 @@ import { expect } from 'chai'
 import sinon from 'sinon'
 import { screen, fireEvent, render, waitFor } from '@testing-library/react'
 import { IntegrationLinkingWidget } from '../../../../../../frontend/js/features/settings/components/linking/integration-widget'
-import * as eventTracking from '../../../../../../frontend/js/infrastructure/event-tracking'
+import * as eventTracking from '@/infrastructure/event-tracking'
 
 describe('<IntegrationLinkingWidgetTest/>', function () {
   const defaultProps = {

+ 1 - 1
services/web/test/frontend/features/settings/components/root.test.tsx

@@ -1,7 +1,7 @@
 import { expect } from 'chai'
 import sinon from 'sinon'
 import { screen, render, waitFor } from '@testing-library/react'
-import * as eventTracking from '../../../../../frontend/js/infrastructure/event-tracking'
+import * as eventTracking from '@/infrastructure/event-tracking'
 import SettingsPageRoot from '../../../../../frontend/js/features/settings/components/root'
 import getMeta from '@/utils/meta'
 

+ 1 - 1
services/web/test/frontend/features/settings/context/user-email-context.test.tsx

@@ -14,7 +14,7 @@ import {
   fakeUsersData,
   unconfirmedCommonsUserData,
 } from '../fixtures/test-user-email-data'
-import localStorage from '../../../../../frontend/js/infrastructure/local-storage'
+import localStorage from '@/infrastructure/local-storage'
 
 const renderUserEmailsContext = () =>
   renderHook(() => useUserEmailsContext(), {

+ 1 - 1
services/web/test/frontend/features/subscription/components/dashboard/states/active/active.test.tsx

@@ -1,6 +1,6 @@
 import { expect } from 'chai'
 import { fireEvent, screen, waitFor } from '@testing-library/react'
-import * as eventTracking from '../../../../../../../../frontend/js/infrastructure/event-tracking'
+import * as eventTracking from '@/infrastructure/event-tracking'
 import { RecurlySubscription } from '../../../../../../../../types/subscription/dashboard/subscription'
 import {
   annualActiveSubscription,

+ 1 - 1
services/web/test/frontend/infrastructure/local-storage.test.js

@@ -1,7 +1,7 @@
 import { expect } from 'chai'
 import sinon from 'sinon'
 
-import customLocalStorage from '../../../frontend/js/infrastructure/local-storage'
+import customLocalStorage from '@/infrastructure/local-storage'
 import { debugConsole } from '@/utils/debugging'
 
 describe('localStorage', function () {

+ 1 - 1
services/web/test/frontend/shared/components/notification.test.tsx

@@ -1,7 +1,7 @@
 import { expect } from 'chai'
 import { screen, render } from '@testing-library/react'
 import Notification from '../../../../frontend/js/shared/components/notification'
-import * as eventTracking from '../../../../frontend/js/infrastructure/event-tracking'
+import * as eventTracking from '@/infrastructure/event-tracking'
 import sinon from 'sinon'
 
 describe('<Notification />', function () {

+ 1 - 1
services/web/test/frontend/shared/hooks/use-persisted-state.test.tsx

@@ -3,7 +3,7 @@ import { expect } from 'chai'
 import { useEffect } from 'react'
 import { render, screen } from '@testing-library/react'
 import usePersistedState from '../../../../frontend/js/shared/hooks/use-persisted-state'
-import localStorage from '../../../../frontend/js/infrastructure/local-storage'
+import localStorage from '@/infrastructure/local-storage'
 
 describe('usePersistedState', function () {
   beforeEach(function () {

+ 9 - 20
services/web/test/frontend/utils/EventEmitterTests.js

@@ -1,22 +1,11 @@
-/* eslint-disable
-    max-len,
-    no-return-assign,
-*/
-// TODO: This file was created by bulk-decaffeinate.
-// Fix any style issues and re-enable lint.
-/*
- * decaffeinate suggestions:
- * DS102: Remove unnecessary code created because of implicit returns
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
 import { expect } from 'chai'
 import sinon from 'sinon'
 
-import EventEmitter from '../../../frontend/js/utils/EventEmitter'
+import EventEmitter from '@/utils/EventEmitter'
 
-export default describe('EventEmitter', function () {
+describe('EventEmitter', function () {
   beforeEach(function () {
-    return (this.eventEmitter = new EventEmitter())
+    this.eventEmitter = new EventEmitter()
   })
 
   it('calls listeners', function () {
@@ -28,7 +17,7 @@ export default describe('EventEmitter', function () {
     this.eventEmitter.trigger('foo')
 
     expect(cb1).to.have.been.called
-    return expect(cb2).to.not.have.been.called
+    expect(cb2).to.not.have.been.called
   })
 
   it('calls multiple listeners', function () {
@@ -40,7 +29,7 @@ export default describe('EventEmitter', function () {
     this.eventEmitter.trigger('foo')
 
     expect(cb1).to.have.been.called
-    return expect(cb2).to.have.been.called
+    expect(cb2).to.have.been.called
   })
 
   it('calls listeners with namespace', function () {
@@ -52,7 +41,7 @@ export default describe('EventEmitter', function () {
     this.eventEmitter.trigger('foo')
 
     expect(cb1).to.have.been.called
-    return expect(cb2).to.have.been.called
+    expect(cb2).to.have.been.called
   })
 
   it('removes listeners', function () {
@@ -62,7 +51,7 @@ export default describe('EventEmitter', function () {
 
     this.eventEmitter.trigger('foo')
 
-    return expect(cb).to.not.have.been.called
+    expect(cb).to.not.have.been.called
   })
 
   it('removes namespaced listeners', function () {
@@ -72,7 +61,7 @@ export default describe('EventEmitter', function () {
 
     this.eventEmitter.trigger('foo')
 
-    return expect(cb).to.not.have.been.called
+    expect(cb).to.not.have.been.called
   })
 
   it('does not remove unnamespaced listeners if off called with namespace', function () {
@@ -85,6 +74,6 @@ export default describe('EventEmitter', function () {
     this.eventEmitter.trigger('foo')
 
     expect(cb1).to.have.been.called
-    return expect(cb2).to.not.have.been.called
+    expect(cb2).to.not.have.been.called
   })
 })

+ 2 - 0
services/web/types/window.ts

@@ -37,5 +37,7 @@ declare global {
         store: ScopeValueStore
       }
     }
+    ga?: (...args: any) => void
+    gtag?: (...args: any) => void
   }
 }