Refactor WordCountModalController (#4747)

GitOrigin-RevId: d32d84a96743cd104f7d5fcd6ec66fc2c0b61c45
This commit is contained in:
Alf Eaton 2021-09-14 09:54:08 +01:00 committed by Copybot
parent 618cf99548
commit 1d55af6e75
9 changed files with 157 additions and 206 deletions

View file

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

View file

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

View file

@ -1,52 +1,28 @@
import { useEffect, useState } from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import WordCountModalContent from './word-count-modal-content' import WordCountModalContent from './word-count-modal-content'
import { fetchWordCount } from '../utils/api' import AccessibleModal from '../../../shared/components/accessible-modal'
import withErrorBoundary from '../../../infrastructure/error-boundary'
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])
const WordCountModal = React.memo(function WordCountModal({
show,
handleHide,
}) {
return ( return (
<WordCountModalContent <AccessibleModal
data={data} animation
error={error}
show={show} show={show}
handleHide={handleHide} onHide={handleHide}
loading={loading} id="clone-project-modal"
/> >
<WordCountModalContent handleHide={handleHide} />
</AccessibleModal>
) )
} })
WordCountModal.propTypes = { WordCountModal.propTypes = {
clsiServerId: PropTypes.string, show: PropTypes.bool,
handleHide: PropTypes.func.isRequired, handleHide: PropTypes.func.isRequired,
projectId: PropTypes.string.isRequired,
show: PropTypes.bool.isRequired,
} }
export default WordCountModal export default withErrorBoundary(WordCountModal)

View file

@ -1,15 +1,10 @@
import App from '../../../base' import App from '../../../base'
import { react2angular } from 'react2angular' import { react2angular } from 'react2angular'
import WordCountModal from '../components/word-count-modal' 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) {
export default App.controller(
'WordCountModalController',
function ($scope, ide) {
$scope.show = false $scope.show = false
$scope.projectId = ide.project_id
$scope.handleHide = () => { $scope.handleHide = () => {
$scope.$applyAsync(() => { $scope.$applyAsync(() => {
@ -19,10 +14,15 @@ export default App.controller(
$scope.openWordCountModal = () => { $scope.openWordCountModal = () => {
$scope.$applyAsync(() => { $scope.$applyAsync(() => {
$scope.clsiServerId = ide.clsiServerId
$scope.projectId = ide.project_id
$scope.show = true $scope.show = true
}) })
} }
} })
App.component(
'wordCountModal',
react2angular(
rootContext.use(WordCountModal),
Object.keys(WordCountModal.propTypes)
)
) )

View file

@ -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 }
}

View file

@ -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' },
},
}

View file

@ -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 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'
export const Interactive = ({ const counts = {
mockResponse = 200,
mockResponseDelay = 500,
...args
}) => {
useFetchMock(fetchMock => {
fetchMock.get(
'express:/project/:projectId/wordcount',
() => {
switch (mockResponse) {
case 400:
return { status: 400, body: 'The project id is not valid' }
case 200:
return {
texcount: {
headers: 4, headers: 4,
mathDisplay: 40, mathDisplay: 40,
mathInline: 400, mathInline: 400,
textWords: 4000, textWords: 4000,
}, }
}
default: const messages = [
return mockResponse 'Lorem ipsum dolor sit amet.',
} 'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.',
}, ].join('\n')
{ delay: mockResponseDelay }
const project = {
_id: 'project-id',
name: 'A Project',
}
export const WordCount = args => {
useFetchMock(fetchMock => {
fetchMock.get(
'express:/project/:projectId/wordcount',
{ status: 200, body: { texcount: counts } },
{ delay: 500 }
) )
}) })
return <WordCountModal {...args} /> return withContextRoot(<WordCountModal {...args} />, { project })
} }
Interactive.propTypes = {
mockResponse: PropTypes.number, export const WordCountWithMessages = args => {
mockResponseDelay: PropTypes.number, useFetchMock(fetchMock => {
fetchMock.get(
'express:/project/:projectId/wordcount',
{ status: 200, body: { texcount: { ...counts, messages } } },
{ delay: 500 }
)
})
return withContextRoot(<WordCountModal {...args} />, { project })
}
export const ErrorResponse = args => {
useFetchMock(fetchMock => {
fetchMock.get(
'express:/project/:projectId/wordcount',
{ status: 500 },
{ delay: 500 }
)
})
return withContextRoot(<WordCountModal {...args} />, { project })
} }
export default { export default {
title: 'Modals / Word Count', title: 'Modals / Word Count',
component: WordCountModal, component: WordCountModal,
args: { args: {
clsiServerId: 'server-id',
projectId: 'project-id',
show: true, 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,
},
},
},
} }

View file

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

View file

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