From c8f139cced2458ef42326d9ea0d88e4bee8a55ea Mon Sep 17 00:00:00 2001 From: Alf Eaton <75253002+aeaton-overleaf@users.noreply.github.com> Date: Fri, 5 Mar 2021 13:00:21 +0000 Subject: [PATCH] Merge pull request #3707 from overleaf/ae-refactor-word-count-modal Refactor "Word Count" modal GitOrigin-RevId: 00561b5b3f8f161238321c440ecde67cd42ece1c --- .../components/word-count-modal-content.js | 29 ++-- .../components/word-count-modal.js | 38 ++---- .../js/features/word-count-modal/utils/api.js | 10 ++ .../word-count-modal-content.stories.js | 45 ++++++ .../stories/word-count-modal.stories.js | 129 +++++++++--------- services/web/package.json | 1 + .../components/word-count-modal.test.js | 86 +++++++----- 7 files changed, 207 insertions(+), 131 deletions(-) create mode 100644 services/web/frontend/js/features/word-count-modal/utils/api.js create mode 100644 services/web/frontend/stories/word-count-modal-content.stories.js diff --git a/services/web/frontend/js/features/word-count-modal/components/word-count-modal-content.js b/services/web/frontend/js/features/word-count-modal/components/word-count-modal-content.js index 7f7c36dd31..e3cf73c93c 100644 --- a/services/web/frontend/js/features/word-count-modal/components/word-count-modal-content.js +++ b/services/web/frontend/js/features/word-count-modal/components/word-count-modal-content.js @@ -3,12 +3,25 @@ import { Row, Col, Modal, Grid, Alert, Button } from 'react-bootstrap' import PropTypes from 'prop-types' import { useTranslation } from 'react-i18next' import Icon from '../../../shared/components/icon' +import AccessibleModal from '../../../shared/components/accessible-modal' -function WordCountModalContent({ data, error, handleHide, loading }) { +export default function WordCountModalContent({ + animation = true, + show, + data, + error, + handleHide, + loading +}) { const { t } = useTranslation() return ( - <> + {t('word_count')} @@ -16,7 +29,7 @@ function WordCountModalContent({ data, error, handleHide, loading }) { {loading && !error && (
-   {t('loading')}… +   {t('loading')}…
)} @@ -70,11 +83,13 @@ function WordCountModalContent({ data, error, handleHide, loading }) { - +
) } WordCountModalContent.propTypes = { + animation: PropTypes.bool, + show: PropTypes.bool.isRequired, handleHide: PropTypes.func.isRequired, loading: PropTypes.bool.isRequired, error: PropTypes.bool, @@ -86,9 +101,3 @@ WordCountModalContent.propTypes = { textWords: PropTypes.number }) } - -function Loading() { - return -} - -export default WordCountModalContent diff --git a/services/web/frontend/js/features/word-count-modal/components/word-count-modal.js b/services/web/frontend/js/features/word-count-modal/components/word-count-modal.js index dac9291de4..573ea3d284 100644 --- a/services/web/frontend/js/features/word-count-modal/components/word-count-modal.js +++ b/services/web/frontend/js/features/word-count-modal/components/word-count-modal.js @@ -1,7 +1,8 @@ import React, { useCallback, useEffect, useState } from 'react' -import { Modal } from 'react-bootstrap' import PropTypes from 'prop-types' +import AbortController from 'abort-controller' import WordCountModalContent from './word-count-modal-content' +import { fetchWordCount } from '../utils/api' function WordCountModal({ clsiServerId, handleHide, projectId, show }) { const [loading, setLoading] = useState(true) @@ -21,25 +22,17 @@ function WordCountModal({ clsiServerId, handleHide, projectId, show }) { const _abortController = new AbortController() setAbortController(_abortController) - let query = '' - if (clsiServerId) { - query = `?clsiserverid=${clsiServerId}` - } - - fetch(`/project/${projectId}/wordcount${query}`, { + fetchWordCount(projectId, clsiServerId, { signal: _abortController.signal }) - .then(async response => { - if (response.ok) { - const { texcount } = await response.json() - setData(texcount) - } else { + .then(data => { + setData(data.texcount) + }) + .catch(error => { + if (error.cause?.name !== 'AbortError') { setError(true) } }) - .catch(() => { - setError(true) - }) .finally(() => { setLoading(false) }) @@ -55,14 +48,13 @@ function WordCountModal({ clsiServerId, handleHide, projectId, show }) { }, [abortController, handleHide]) return ( - - - + ) } diff --git a/services/web/frontend/js/features/word-count-modal/utils/api.js b/services/web/frontend/js/features/word-count-modal/utils/api.js new file mode 100644 index 0000000000..54818f0338 --- /dev/null +++ b/services/web/frontend/js/features/word-count-modal/utils/api.js @@ -0,0 +1,10 @@ +import { getJSON } from '../../../infrastructure/fetch-json' + +export function fetchWordCount(projectId, clsiServerId, options) { + let query = '' + if (clsiServerId) { + query = `?clsiserverid=${clsiServerId}` + } + + return getJSON(`/project/${projectId}/wordcount${query}`, options) +} diff --git a/services/web/frontend/stories/word-count-modal-content.stories.js b/services/web/frontend/stories/word-count-modal-content.stories.js new file mode 100644 index 0000000000..8d4cfc5f8c --- /dev/null +++ b/services/web/frontend/stories/word-count-modal-content.stories.js @@ -0,0 +1,45 @@ +import React from 'react' + +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 +} + +export const Loading = args => { + return +} + +export const LoadingError = args => { + return +} + +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 +} + +export default { + title: 'Word Count Modal / Content', + component: WordCountModalContent, + args: { + animation: false, + show: true, + error: false, + loading: false + }, + argTypes: { + handleHide: { action: 'hide' } + } +} diff --git a/services/web/frontend/stories/word-count-modal.stories.js b/services/web/frontend/stories/word-count-modal.stories.js index 20b6781cdc..703a830477 100644 --- a/services/web/frontend/stories/word-count-modal.stories.js +++ b/services/web/frontend/stories/word-count-modal.stories.js @@ -1,77 +1,76 @@ import React from 'react' +import fetchMock from 'fetch-mock' +import PropTypes from 'prop-types' -import WordCountModalContent from '../js/features/word-count-modal/components/word-count-modal-content' +import WordCountModal from '../js/features/word-count-modal/components/word-count-modal' -// NOTE: WordCountModalContent is wrapped in modal classes, without modal behaviours +export const Interactive = ({ + mockResponse = 200, + mockResponseDelay = 500, + ...args +}) => { + fetchMock.restore().get( + 'express:/project/:projectId/wordcount', + () => { + switch (mockResponse) { + case 400: + return { status: 400, body: 'The project id is not valid' } -export const Loading = args => ( -
-
- -
-
-) -Loading.args = { - loading: true, - error: false + case 200: + return { + texcount: { + headers: 4, + mathDisplay: 40, + mathInline: 400, + textWords: 4000 + } + } + + default: + return mockResponse + } + }, + { delay: mockResponseDelay } + ) + + return } - -export const LoadingError = args => ( -
-
- -
-
-) -LoadingError.args = { - loading: false, - error: true -} - -export const Loaded = args => ( -
-
- -
-
-) -Loaded.args = { - loading: false, - error: false, - data: { - headers: 4, - mathDisplay: 40, - mathInline: 400, - textWords: 4000 - } -} - -export const Messages = args => ( -
-
- -
-
-) -Messages.args = { - loading: false, - error: false, - data: { - 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'), - headers: 4, - mathDisplay: 40, - mathInline: 400, - textWords: 4000 - } +Interactive.propTypes = { + mockResponse: PropTypes.number, + mockResponseDelay: PropTypes.number } export default { title: 'Word Count Modal', - component: WordCountModalContent, + component: WordCountModal, + args: { + clsiServerId: 'server-id', + projectId: 'project-id', + show: true + }, argTypes: { - handleHide: { action: 'handleHide' } + 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 + } + } } } diff --git a/services/web/package.json b/services/web/package.json index 56caa3ddb7..81ee45a749 100644 --- a/services/web/package.json +++ b/services/web/package.json @@ -165,6 +165,7 @@ "@storybook/react": "^6.1.10", "@testing-library/dom": "^7.29.4", "@testing-library/react": "^11.2.3", + "abort-controller": "^3.0.0", "acorn": "^7.1.1", "acorn-walk": "^7.1.1", "angular-mocks": "~1.8.0", diff --git a/services/web/test/frontend/features/word-count-modal/components/word-count-modal.test.js b/services/web/test/frontend/features/word-count-modal/components/word-count-modal.test.js index e0b8095b1e..1a658b8db5 100644 --- a/services/web/test/frontend/features/word-count-modal/components/word-count-modal.test.js +++ b/services/web/test/frontend/features/word-count-modal/components/word-count-modal.test.js @@ -1,29 +1,43 @@ import React from 'react' -import { render, screen } from '@testing-library/react' -import WordCountModalContent from '../../../../../frontend/js/features/word-count-modal/components/word-count-modal-content' +import { render, screen, cleanup } from '@testing-library/react' +import WordCountModal from '../../../../../frontend/js/features/word-count-modal/components/word-count-modal' import { expect } from 'chai' +import sinon from 'sinon' +import fetchMock from 'fetch-mock' -const handleHide = () => { - // closed -} +describe('', function() { + afterEach(function() { + fetchMock.reset() + cleanup() + }) + + const modalProps = { + projectId: 'project-1', + clsiServerId: 'clsi-server-1', + show: true, + handleHide: sinon.stub() + } -describe('', function() { it('renders the translated modal title', async function() { - render() + render() await screen.findByText('Word Count') - - expect(screen.queryByText(/Loading/)).to.not.exist }) it('renders a loading message when loading', async function() { - render() + fetchMock.get('express:/project/:projectId/wordcount', () => { + return { status: 200, body: { texcount: {} } } + }) - await screen.findByText('Loading') + render() + + await screen.findByText('Loading…') }) it('renders an error message and hides loading message on error', async function() { - render() + fetchMock.get('express:/project/:projectId/wordcount', 500) + + render() await screen.findByText('Sorry, something went wrong') @@ -31,32 +45,38 @@ describe('', function() { }) it('displays messages', async function() { - render( - - ) + fetchMock.get('express:/project/:projectId/wordcount', () => { + return { + status: 200, + body: { + texcount: { + messages: 'This is a test' + } + } + } + }) + + render() await screen.findByText('This is a test') }) it('displays counts data', async function() { - render( - - ) + fetchMock.get('express:/project/:projectId/wordcount', () => { + return { + status: 200, + body: { + texcount: { + textWords: 100, + mathDisplay: 200, + mathInline: 300, + headers: 400 + } + } + } + }) + + render() await screen.findByText((content, element) => element.textContent.trim().match(/^Total Words\s*:\s*100$/)