diff --git a/services/web/app/views/project/editor/left-menu.pug b/services/web/app/views/project/editor/left-menu.pug index 8abffea540..628b26b279 100644 --- a/services/web/app/views/project/editor/left-menu.pug +++ b/services/web/app/views/project/editor/left-menu.pug @@ -30,7 +30,7 @@ aside#left-menu.full-size( i.fa.fa-file-pdf-o.fa-2x br | PDF - + span(ng-show="!anonymous") h4 #{translate("actions")} ul.list-unstyled.nav @@ -50,14 +50,21 @@ aside#left-menu.full-size( ) != moduleIncludes("editorLeftMenu:actions", locals) - li(ng-controller="WordCountController") - a(href, ng-if="pdf.url" ,ng-click="openWordCountModal()") + li(ng-controller="WordCountModalController") + a(href, ng-if="pdf.url", ng-click="openWordCountModal()") i.fa.fa-fw.fa-eye span    #{translate("word_count")} - a.link-disabled(href, ng-if="!pdf.url" , tooltip=translate('please_compile_pdf_before_word_count')) + a.link-disabled(href, ng-if="!pdf.url", tooltip=translate('please_compile_pdf_before_word_count')) i.fa.fa-fw.fa-eye span.link-disabled    #{translate("word_count")} + word-count-modal( + clsi-server-id="clsiServerId" + handle-hide="handleHide" + project-id="projectId" + show="show" + ) + if (moduleIncludesAvailable("editorLeftMenu:sync")) div(ng-show="!anonymous") h4() #{translate("sync")} @@ -234,14 +241,43 @@ aside#left-menu.full-size( ng-cloak ) +script(type='text/ng-template', id='cloneProjectModalTemplate') + .modal-header + h3 #{translate("copy_project")} + .modal-body + .alert.alert-danger(ng-show="state.error.message") {{ state.error.message}} + .alert.alert-danger(ng-show="state.error && !state.error.message") #{translate("generic_something_went_wrong")} + form(name="cloneProjectForm", novalidate) + .form-group + label #{translate("new_name")} + input.form-control( + type="text", + placeholder="New Project Name", + required, + ng-model="inputs.projectName", + on-enter="clone()", + focus-on="open" + ) + .modal-footer + button.btn.btn-default( + ng-disabled="state.inflight" + ng-click="cancel()" + ) #{translate("cancel")} + button.btn.btn-primary( + ng-disabled="cloneProjectForm.$invalid || state.inflight" + ng-click="clone()" + ) + span(ng-hide="state.inflight") #{translate("copy")} + span(ng-show="state.inflight") #{translate("copying")}… + script(type='text/ng-template', id='wordCountModalTemplate') .modal-header h3 #{translate("word_count")} .modal-body div(ng-if="status.loading") .loading(ng-show="!status.error && status.loading") - i.fa.fa-refresh.fa-spin.fa-fw - span   #{translate("loading")}… + i.fa.fa-refresh.fa-spin.fa-fw + span   #{translate("loading")}… div.pdf-disabled( ng-if="!pdf.url" tooltip=translate('please_compile_pdf_before_word_count') @@ -257,16 +293,16 @@ script(type='text/ng-template', id='wordCountModalTemplate') .col-xs-4 .pull-right #{translate("total_words")} : .col-xs-6 {{data.textWords}} - .row + .row .col-xs-4 .pull-right #{translate("headers")} : .col-xs-6 {{data.headers}} - .row - .col-xs-4 + .row + .col-xs-4 .pull-right #{translate("math_inline")} : .col-xs-6 {{data.mathInline}} - .row - .col-xs-4 + .row + .col-xs-4 .pull-right #{translate("math_display")} : .col-xs-6 {{data.mathDisplay}} .modal-footer diff --git a/services/web/frontend/extracted-translation-keys.json b/services/web/frontend/extracted-translation-keys.json index 6a6dff3194..07d4e21c64 100644 --- a/services/web/frontend/extracted-translation-keys.json +++ b/services/web/frontend/extracted-translation-keys.json @@ -112,5 +112,12 @@ "duplicate_file", "error", "invalid_file_name", - "ok" + "ok", + "refresh", + "word_count", + "total_words", + "headers", + "math_inline", + "math_display", + "done" ] 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 new file mode 100644 index 0000000000..7f7c36dd31 --- /dev/null +++ b/services/web/frontend/js/features/word-count-modal/components/word-count-modal-content.js @@ -0,0 +1,94 @@ +import React from 'react' +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' + +function WordCountModalContent({ data, error, handleHide, loading }) { + const { t } = useTranslation() + + return ( + <> + + {t('word_count')} + + + + {loading && !error && ( +
+   {t('loading')}… +
+ )} + + {error && ( + {t('generic_something_went_wrong')} + )} + + {data && ( + + {data.messages && ( + + + +

{data.messages}

+
+ +
+ )} + + + +
{t('total_words')}:
+ + {data.textWords} +
+ + + +
{t('headers')}:
+ + {data.headers} +
+ + + +
{t('math_inline')}:
+ + {data.mathInline} +
+ + + +
{t('math_display')}:
+ + {data.mathDisplay} +
+
+ )} +
+ + + + + + ) +} + +WordCountModalContent.propTypes = { + 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 + }) +} + +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 new file mode 100644 index 0000000000..20b0079516 --- /dev/null +++ b/services/web/frontend/js/features/word-count-modal/components/word-count-modal.js @@ -0,0 +1,70 @@ +import React, { useCallback, useEffect, useState } from 'react' +import { Modal } from 'react-bootstrap' +import PropTypes from 'prop-types' +import WordCountModalContent from './word-count-modal-content' + +function WordCountModal({ handleHide, show, projectId }) { + const [loading, setLoading] = useState(true) + const [error, setError] = useState(false) + const [data, setData] = useState() + const [abortController, setAbortController] = useState(new AbortController()) + + useEffect(() => { + if (!show) { + return + } + + setData(undefined) + setError(false) + setLoading(true) + + const _abortController = new AbortController() + setAbortController(_abortController) + + fetch(`/project/${projectId}/wordcount`, { + signal: _abortController.signal + }) + .then(async response => { + if (response.ok) { + const { texcount } = await response.json() + setData(texcount) + } else { + setError(true) + } + }) + .catch(() => { + setError(true) + }) + .finally(() => { + setLoading(false) + }) + + return () => { + _abortController.abort() + } + }, [show, projectId]) + + const abortAndHide = useCallback(() => { + abortController.abort() + handleHide() + }, [abortController, handleHide]) + + return ( + + + + ) +} + +WordCountModal.propTypes = { + handleHide: PropTypes.func.isRequired, + projectId: PropTypes.string.isRequired, + show: PropTypes.bool.isRequired +} + +export default WordCountModal diff --git a/services/web/frontend/js/features/word-count-modal/controllers/word-count-modal-controller.js b/services/web/frontend/js/features/word-count-modal/controllers/word-count-modal-controller.js new file mode 100644 index 0000000000..a98a58f996 --- /dev/null +++ b/services/web/frontend/js/features/word-count-modal/controllers/word-count-modal-controller.js @@ -0,0 +1,27 @@ +import App from '../../../base' +import { react2angular } from 'react2angular' + +import WordCountModal from '../components/word-count-modal' + +App.component('wordCountModal', react2angular(WordCountModal)) + +export default App.controller('WordCountModalController', function( + $scope, + ide +) { + $scope.show = false + $scope.projectId = ide.project_id + + $scope.handleHide = () => { + $scope.$applyAsync(() => { + $scope.show = false + }) + } + + $scope.openWordCountModal = () => { + $scope.$applyAsync(() => { + $scope.projectId = ide.project_id + $scope.show = true + }) + } +}) diff --git a/services/web/frontend/js/ide/wordcount/controllers/WordCountController.js b/services/web/frontend/js/ide/wordcount/controllers/WordCountController.js deleted file mode 100644 index 563e80884f..0000000000 --- a/services/web/frontend/js/ide/wordcount/controllers/WordCountController.js +++ /dev/null @@ -1,21 +0,0 @@ -/* 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 App from '../../../base' - -export default App.controller( - 'WordCountController', - ($scope, $modal) => - ($scope.openWordCountModal = () => - $modal.open({ - templateUrl: 'wordCountModalTemplate', - controller: 'WordCountModalController' - })) -) diff --git a/services/web/frontend/js/ide/wordcount/controllers/WordCountModalController.js b/services/web/frontend/js/ide/wordcount/controllers/WordCountModalController.js deleted file mode 100644 index 72603054a3..0000000000 --- a/services/web/frontend/js/ide/wordcount/controllers/WordCountModalController.js +++ /dev/null @@ -1,38 +0,0 @@ -/* 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 App from '../../../base' - -export default App.controller('WordCountModalController', function( - $scope, - $modalInstance, - ide, - $http -) { - $scope.status = { loading: true } - - const opts = { - url: `/project/${ide.project_id}/wordcount`, - method: 'GET', - params: { - clsiserverid: ide.clsiServerId - } - } - $http(opts) - .then(function(response) { - const { data } = response - $scope.status.loading = false - return ($scope.data = data.texcount) - }) - .catch(() => ($scope.status.error = true)) - - return ($scope.cancel = () => $modalInstance.dismiss('cancel')) -}) diff --git a/services/web/frontend/js/ide/wordcount/index.js b/services/web/frontend/js/ide/wordcount/index.js index 7d8f27a843..0a6113a914 100644 --- a/services/web/frontend/js/ide/wordcount/index.js +++ b/services/web/frontend/js/ide/wordcount/index.js @@ -1,4 +1 @@ -// TODO: This file was created by bulk-decaffeinate. -// Fix any style issues and re-enable lint. -import './controllers/WordCountController' -import './controllers/WordCountModalController' +import '../../features/word-count-modal/controllers/word-count-modal-controller' diff --git a/services/web/frontend/stories/word-count-modal.stories.js b/services/web/frontend/stories/word-count-modal.stories.js new file mode 100644 index 0000000000..20b6781cdc --- /dev/null +++ b/services/web/frontend/stories/word-count-modal.stories.js @@ -0,0 +1,77 @@ +import React from 'react' + +import WordCountModalContent from '../js/features/word-count-modal/components/word-count-modal-content' + +// NOTE: WordCountModalContent is wrapped in modal classes, without modal behaviours + +export const Loading = args => ( +
+
+ +
+
+) +Loading.args = { + loading: true, + error: false +} + +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 + } +} + +export default { + title: 'Word Count Modal', + component: WordCountModalContent, + argTypes: { + handleHide: { action: 'handleHide' } + } +} 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 new file mode 100644 index 0000000000..e0b8095b1e --- /dev/null +++ b/services/web/test/frontend/features/word-count-modal/components/word-count-modal.test.js @@ -0,0 +1,74 @@ +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 { expect } from 'chai' + +const handleHide = () => { + // closed +} + +describe('', function() { + it('renders the translated modal title', async function() { + render() + + await screen.findByText('Word Count') + + expect(screen.queryByText(/Loading/)).to.not.exist + }) + + it('renders a loading message when loading', async function() { + render() + + await screen.findByText('Loading') + }) + + it('renders an error message and hides loading message on error', async function() { + render() + + await screen.findByText('Sorry, something went wrong') + + expect(screen.queryByText(/Loading/)).to.not.exist + }) + + it('displays messages', async function() { + render( + + ) + + await screen.findByText('This is a test') + }) + + it('displays counts data', async function() { + render( + + ) + + await screen.findByText((content, element) => + element.textContent.trim().match(/^Total Words\s*:\s*100$/) + ) + await screen.findByText((content, element) => + element.textContent.trim().match(/^Math Display\s*:\s*200$/) + ) + await screen.findByText((content, element) => + element.textContent.trim().match(/^Math Inline\s*:\s*300$/) + ) + await screen.findByText((content, element) => + element.textContent.trim().match(/^Headers\s*:\s*400$/) + ) + }) +})