mirror of
https://github.com/overleaf/overleaf.git
synced 2024-09-16 02:52:31 -04:00
Refactor WordCountModalController (#4747)
GitOrigin-RevId: d32d84a96743cd104f7d5fcd6ec66fc2c0b61c45
This commit is contained in:
parent
618cf99548
commit
1d55af6e75
9 changed files with 157 additions and 206 deletions
|
@ -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"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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,
|
|
||||||
}),
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
|
@ -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 }
|
||||||
|
}
|
|
@ -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' },
|
|
||||||
},
|
|
||||||
}
|
|
|
@ -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')
|
||||||
|
|
||||||
|
const project = {
|
||||||
|
_id: 'project-id',
|
||||||
|
name: 'A Project',
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{ delay: mockResponseDelay }
|
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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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$/)
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue