Bläddra i källkod

Refactor WordCountModalController (#4747)

GitOrigin-RevId: d32d84a96743cd104f7d5fcd6ec66fc2c0b61c45
Alf Eaton 4 år sedan
förälder
incheckning
1d55af6e75

+ 0 - 2
services/web/app/views/project/editor/left-menu.pug

@@ -60,9 +60,7 @@ aside#left-menu.full-size(
 					span.link-disabled    #{translate("word_count")}
 
 				word-count-modal(
-					clsi-server-id="clsiServerId"
 					handle-hide="handleHide"
-					project-id="projectId"
 					show="show"
 				)
 

+ 11 - 28
services/web/frontend/js/features/word-count-modal/components/word-count-modal-content.js

@@ -1,26 +1,20 @@
-import { Row, Col, Modal, Grid, Alert, Button } from 'react-bootstrap'
 import PropTypes from 'prop-types'
 import { useTranslation } from 'react-i18next'
+import { Alert, Button, Modal, Row, Col, Grid } from 'react-bootstrap'
+import { useIdeContext } from '../../../shared/context/ide-context'
+import { useProjectContext } from '../../../shared/context/project-context'
+import { useWordCount } from '../hooks/use-word-count'
 import Icon from '../../../shared/components/icon'
-import AccessibleModal from '../../../shared/components/accessible-modal'
 
-export default function WordCountModalContent({
-  animation = true,
-  show,
-  data,
-  error,
-  handleHide,
-  loading,
-}) {
+// NOTE: this component is only mounted when the modal is open
+export default function WordCountModalContent({ handleHide }) {
+  const { _id: projectId } = useProjectContext()
+  const { clsiServerId } = useIdeContext()
   const { t } = useTranslation()
+  const { data, error, loading } = useWordCount(projectId, clsiServerId)
 
   return (
-    <AccessibleModal
-      animation={animation}
-      show={show}
-      onHide={handleHide}
-      id="clone-project-modal"
-    >
+    <>
       <Modal.Header closeButton>
         <Modal.Title>{t('word_count')}</Modal.Title>
       </Modal.Header>
@@ -82,21 +76,10 @@ export default function WordCountModalContent({
       <Modal.Footer>
         <Button onClick={handleHide}>{t('done')}</Button>
       </Modal.Footer>
-    </AccessibleModal>
+    </>
   )
 }
 
 WordCountModalContent.propTypes = {
-  animation: PropTypes.bool,
-  show: PropTypes.bool.isRequired,
   handleHide: PropTypes.func.isRequired,
-  loading: PropTypes.bool.isRequired,
-  error: PropTypes.bool,
-  data: PropTypes.shape({
-    messages: PropTypes.string,
-    headers: PropTypes.number,
-    mathDisplay: PropTypes.number,
-    mathInline: PropTypes.number,
-    textWords: PropTypes.number,
-  }),
 }

+ 17 - 41
services/web/frontend/js/features/word-count-modal/components/word-count-modal.js

@@ -1,52 +1,28 @@
-import { useEffect, useState } from 'react'
+import React from 'react'
 import PropTypes from 'prop-types'
 import WordCountModalContent from './word-count-modal-content'
-import { fetchWordCount } from '../utils/api'
-
-function WordCountModal({ clsiServerId, handleHide, projectId, show }) {
-  const [loading, setLoading] = useState(true)
-  const [error, setError] = useState(false)
-  const [data, setData] = useState()
-
-  useEffect(() => {
-    if (!show) {
-      return
-    }
-
-    setData(undefined)
-    setError(false)
-    setLoading(true)
-
-    fetchWordCount(projectId, clsiServerId)
-      .then(data => {
-        setData(data.texcount)
-      })
-      .catch(error => {
-        if (error.cause?.name !== 'AbortError') {
-          setError(true)
-        }
-      })
-      .finally(() => {
-        setLoading(false)
-      })
-  }, [show, projectId, clsiServerId])
+import AccessibleModal from '../../../shared/components/accessible-modal'
+import withErrorBoundary from '../../../infrastructure/error-boundary'
 
+const WordCountModal = React.memo(function WordCountModal({
+  show,
+  handleHide,
+}) {
   return (
-    <WordCountModalContent
-      data={data}
-      error={error}
+    <AccessibleModal
+      animation
       show={show}
-      handleHide={handleHide}
-      loading={loading}
-    />
+      onHide={handleHide}
+      id="clone-project-modal"
+    >
+      <WordCountModalContent handleHide={handleHide} />
+    </AccessibleModal>
   )
-}
+})
 
 WordCountModal.propTypes = {
-  clsiServerId: PropTypes.string,
+  show: PropTypes.bool,
   handleHide: PropTypes.func.isRequired,
-  projectId: PropTypes.string.isRequired,
-  show: PropTypes.bool.isRequired,
 }
 
-export default WordCountModal
+export default withErrorBoundary(WordCountModal)

+ 20 - 20
services/web/frontend/js/features/word-count-modal/controllers/word-count-modal-controller.js

@@ -1,28 +1,28 @@
 import App from '../../../base'
 import { react2angular } from 'react2angular'
-
 import WordCountModal from '../components/word-count-modal'
+import { rootContext } from '../../../shared/context/root-context'
 
-App.component('wordCountModal', react2angular(WordCountModal))
-
-export default App.controller(
-  'WordCountModalController',
-  function ($scope, ide) {
-    $scope.show = false
-    $scope.projectId = ide.project_id
+export default App.controller('WordCountModalController', function ($scope) {
+  $scope.show = false
 
-    $scope.handleHide = () => {
-      $scope.$applyAsync(() => {
-        $scope.show = false
-      })
-    }
+  $scope.handleHide = () => {
+    $scope.$applyAsync(() => {
+      $scope.show = false
+    })
+  }
 
-    $scope.openWordCountModal = () => {
-      $scope.$applyAsync(() => {
-        $scope.clsiServerId = ide.clsiServerId
-        $scope.projectId = ide.project_id
-        $scope.show = true
-      })
-    }
+  $scope.openWordCountModal = () => {
+    $scope.$applyAsync(() => {
+      $scope.show = true
+    })
   }
+})
+
+App.component(
+  'wordCountModal',
+  react2angular(
+    rootContext.use(WordCountModal),
+    Object.keys(WordCountModal.propTypes)
+  )
 )

+ 26 - 0
services/web/frontend/js/features/word-count-modal/hooks/use-word-count.js

@@ -0,0 +1,26 @@
+import useAbortController from '../../../shared/hooks/use-abort-controller'
+import { fetchWordCount } from '../utils/api'
+import { useEffect, useState } from 'react'
+
+export function useWordCount(projectId, clsiServerId) {
+  const [loading, setLoading] = useState(true)
+  const [error, setError] = useState(false)
+  const [data, setData] = useState()
+
+  const { signal } = useAbortController()
+
+  useEffect(() => {
+    fetchWordCount(projectId, clsiServerId, { signal })
+      .then(data => {
+        setData(data.texcount)
+      })
+      .catch(() => {
+        setError(true)
+      })
+      .finally(() => {
+        setLoading(false)
+      })
+  }, [signal, clsiServerId, projectId])
+
+  return { data, error, loading }
+}

+ 0 - 43
services/web/frontend/stories/word-count-modal-content.stories.js

@@ -1,43 +0,0 @@
-import WordCountModalContent from '../js/features/word-count-modal/components/word-count-modal-content'
-
-export const Basic = args => {
-  const data = {
-    headers: 4,
-    mathDisplay: 40,
-    mathInline: 400,
-    textWords: 4000,
-  }
-
-  return <WordCountModalContent {...args} data={data} />
-}
-
-export const Loading = args => {
-  return <WordCountModalContent {...args} loading />
-}
-
-export const LoadingError = args => {
-  return <WordCountModalContent {...args} error />
-}
-
-export const Messages = args => {
-  const messages = [
-    'Lorem ipsum dolor sit amet.',
-    'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.',
-  ].join('\n')
-
-  return <WordCountModalContent {...args} data={{ messages }} />
-}
-
-export default {
-  title: 'Modals / Word Count / Content',
-  component: WordCountModalContent,
-  args: {
-    animation: false,
-    show: true,
-    error: false,
-    loading: false,
-  },
-  argTypes: {
-    handleHide: { action: 'hide' },
-  },
-}

+ 44 - 57
services/web/frontend/stories/word-count-modal.stories.js

@@ -1,77 +1,64 @@
-import PropTypes from 'prop-types'
-
-import WordCountModal from '../js/features/word-count-modal/components/word-count-modal'
 import useFetchMock from './hooks/use-fetch-mock'
+import { withContextRoot } from './utils/with-context-root'
+import WordCountModal from '../js/features/word-count-modal/components/word-count-modal'
+
+const counts = {
+  headers: 4,
+  mathDisplay: 40,
+  mathInline: 400,
+  textWords: 4000,
+}
+
+const messages = [
+  'Lorem ipsum dolor sit amet.',
+  'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.',
+].join('\n')
 
-export const Interactive = ({
-  mockResponse = 200,
-  mockResponseDelay = 500,
-  ...args
-}) => {
+const project = {
+  _id: 'project-id',
+  name: 'A Project',
+}
+
+export const WordCount = args => {
   useFetchMock(fetchMock => {
     fetchMock.get(
       'express:/project/:projectId/wordcount',
-      () => {
-        switch (mockResponse) {
-          case 400:
-            return { status: 400, body: 'The project id is not valid' }
+      { status: 200, body: { texcount: counts } },
+      { delay: 500 }
+    )
+  })
 
-          case 200:
-            return {
-              texcount: {
-                headers: 4,
-                mathDisplay: 40,
-                mathInline: 400,
-                textWords: 4000,
-              },
-            }
+  return withContextRoot(<WordCountModal {...args} />, { project })
+}
 
-          default:
-            return mockResponse
-        }
-      },
-      { delay: mockResponseDelay }
+export const WordCountWithMessages = args => {
+  useFetchMock(fetchMock => {
+    fetchMock.get(
+      'express:/project/:projectId/wordcount',
+      { status: 200, body: { texcount: { ...counts, messages } } },
+      { delay: 500 }
     )
   })
 
-  return <WordCountModal {...args} />
+  return withContextRoot(<WordCountModal {...args} />, { project })
 }
-Interactive.propTypes = {
-  mockResponse: PropTypes.number,
-  mockResponseDelay: PropTypes.number,
+
+export const ErrorResponse = args => {
+  useFetchMock(fetchMock => {
+    fetchMock.get(
+      'express:/project/:projectId/wordcount',
+      { status: 500 },
+      { delay: 500 }
+    )
+  })
+
+  return withContextRoot(<WordCountModal {...args} />, { project })
 }
 
 export default {
   title: 'Modals / Word Count',
   component: WordCountModal,
   args: {
-    clsiServerId: 'server-id',
-    projectId: 'project-id',
     show: true,
   },
-  argTypes: {
-    handleHide: { action: 'handleHide' },
-    mockResponse: {
-      name: 'Mock Response Status',
-      type: { name: 'number', required: false },
-      description: 'The status code that should be returned by the mock server',
-      defaultValue: 200,
-      control: {
-        type: 'radio',
-        options: [200, 500, 400],
-      },
-    },
-    mockResponseDelay: {
-      name: 'Mock Response Delay',
-      type: { name: 'number', required: false },
-      description: 'The delay before returning a response from the mock server',
-      defaultValue: 500,
-      control: {
-        type: 'range',
-        min: 0,
-        max: 2500,
-        step: 250,
-      },
-    },
-  },
 }

+ 34 - 11
services/web/test/frontend/features/word-count-modal/components/word-count-modal.test.js

@@ -1,24 +1,27 @@
-import { render, screen, cleanup } from '@testing-library/react'
-import WordCountModal from '../../../../../frontend/js/features/word-count-modal/components/word-count-modal'
+import { screen } from '@testing-library/react'
 import { expect } from 'chai'
 import sinon from 'sinon'
 import fetchMock from 'fetch-mock'
+import { renderWithEditorContext } from '../../../helpers/render-with-context'
+import WordCountModal from '../../../../../frontend/js/features/word-count-modal/components/word-count-modal'
 
 describe('<WordCountModal />', function () {
   afterEach(function () {
     fetchMock.reset()
-    cleanup()
   })
 
-  const modalProps = {
+  const contextProps = {
     projectId: 'project-1',
     clsiServerId: 'clsi-server-1',
-    show: true,
-    handleHide: sinon.stub(),
   }
 
   it('renders the translated modal title', async function () {
-    render(<WordCountModal {...modalProps} />)
+    const handleHide = sinon.stub()
+
+    renderWithEditorContext(
+      <WordCountModal show handleHide={handleHide} />,
+      contextProps
+    )
 
     await screen.findByText('Word Count')
   })
@@ -28,7 +31,12 @@ describe('<WordCountModal />', function () {
       return { status: 200, body: { texcount: { messages: 'This is a test' } } }
     })
 
-    render(<WordCountModal {...modalProps} />)
+    const handleHide = sinon.stub()
+
+    renderWithEditorContext(
+      <WordCountModal show handleHide={handleHide} />,
+      contextProps
+    )
 
     await screen.findByText('Loading…')
 
@@ -38,7 +46,12 @@ describe('<WordCountModal />', function () {
   it('renders an error message and hides loading message on error', async function () {
     fetchMock.get('express:/project/:projectId/wordcount', 500)
 
-    render(<WordCountModal {...modalProps} />)
+    const handleHide = sinon.stub()
+
+    renderWithEditorContext(
+      <WordCountModal show handleHide={handleHide} />,
+      contextProps
+    )
 
     await screen.findByText('Sorry, something went wrong')
 
@@ -57,7 +70,12 @@ describe('<WordCountModal />', function () {
       }
     })
 
-    render(<WordCountModal {...modalProps} />)
+    const handleHide = sinon.stub()
+
+    renderWithEditorContext(
+      <WordCountModal show handleHide={handleHide} />,
+      contextProps
+    )
 
     await screen.findByText('This is a test')
   })
@@ -77,7 +95,12 @@ describe('<WordCountModal />', function () {
       }
     })
 
-    render(<WordCountModal {...modalProps} />)
+    const handleHide = sinon.stub()
+
+    renderWithEditorContext(
+      <WordCountModal show handleHide={handleHide} />,
+      contextProps
+    )
 
     await screen.findByText((content, element) =>
       element.textContent.trim().match(/^Total Words\s*:\s*100$/)

+ 2 - 1
services/web/test/frontend/helpers/render-with-context.js

@@ -20,6 +20,7 @@ export function EditorProviders({
     removeListener: sinon.stub(),
   },
   isRestrictedTokenMember = false,
+  clsiServerId = '1234',
   scope,
   children,
 }) {
@@ -51,7 +52,7 @@ export function EditorProviders({
     ...scope,
   }
 
-  window._ide = { $scope, socket }
+  window._ide = { $scope, socket, clsiServerId }
 
   return (
     <SplitTestProvider>