From 771a39f2674a0ce7a9c3f7238fe4d2b9959d2a52 Mon Sep 17 00:00:00 2001 From: Alf Eaton <75253002+aeaton-overleaf@users.noreply.github.com> Date: Thu, 10 Dec 2020 11:13:59 +0000 Subject: [PATCH] Merge pull request #3441 from overleaf/3803-hotkeys-modal Migrate hotkeys modal to React GitOrigin-RevId: 78399d3d62771cd296bdc2f4f8b1083263d31551 --- services/web/app/views/project/editor.pug | 2 - .../web/app/views/project/editor/hotkeys.pug | 113 ----------- .../app/views/project/editor/left-menu.pug | 8 +- .../frontend/extracted-translation-keys.json | 7 + .../components/hotkeys-modal-content.js | 177 ++++++++++++++++++ .../hotkeys-modal/components/hotkeys-modal.js | 26 +++ .../controllers/hotkeys-modal-controller.js | 25 +++ .../hotkeys/controllers/HotkeysController.js | 47 ----- services/web/frontend/js/ide/hotkeys/index.js | 2 +- .../frontend/stories/hotkeys-modal.stories.js | 24 +++ .../components/hotkeys-modal.test.js | 61 ++++++ 11 files changed, 328 insertions(+), 164 deletions(-) delete mode 100644 services/web/app/views/project/editor/hotkeys.pug create mode 100644 services/web/frontend/js/features/hotkeys-modal/components/hotkeys-modal-content.js create mode 100644 services/web/frontend/js/features/hotkeys-modal/components/hotkeys-modal.js create mode 100644 services/web/frontend/js/features/hotkeys-modal/controllers/hotkeys-modal-controller.js delete mode 100644 services/web/frontend/js/ide/hotkeys/controllers/HotkeysController.js create mode 100644 services/web/frontend/stories/hotkeys-modal.stories.js create mode 100644 services/web/test/frontend/features/hotkeys-modal/components/hotkeys-modal.test.js diff --git a/services/web/app/views/project/editor.pug b/services/web/app/views/project/editor.pug index 473825b426..2b09ad54e6 100644 --- a/services/web/app/views/project/editor.pug +++ b/services/web/app/views/project/editor.pug @@ -110,8 +110,6 @@ block content else include ./editor/chat - include ./editor/hotkeys - script(type="text/ng-template", id="genericMessageModalTemplate") .modal-header button.close( diff --git a/services/web/app/views/project/editor/hotkeys.pug b/services/web/app/views/project/editor/hotkeys.pug deleted file mode 100644 index 798548f5c4..0000000000 --- a/services/web/app/views/project/editor/hotkeys.pug +++ /dev/null @@ -1,113 +0,0 @@ -script(type="text/ng-template", id="hotkeysModalTemplate") - .modal-header - button.close( - type="button" - data-dismiss="modal" - ng-click="cancel()" - aria-label="Close" - ) - span(aria-hidden="true") × - h3 #{translate("hotkeys")} - .modal-body.modal-hotkeys - h3 #{translate("common")} - .row - .col-xs-4 - .hotkey - span.combination {{ctrl}} + F - span.description Find (and replace) - .hotkey - span.combination {{ctrl}} + Enter - span.description Compile - .col-xs-4 - .hotkey - span.combination {{ctrl}} + Z - span.description Undo - .col-xs-4 - .hotkey - span.combination {{ctrl}} + Y - span.description Redo - h3 #{translate("navigation")} - .row - .col-xs-4 - .hotkey - span.combination {{ctrl}} + Home - span.description Beginning of document - .col-xs-4 - .hotkey - span.combination {{ctrl}} + End - span.description End of document - .col-xs-4 - .hotkey - span.combination {{ctrl}} + L - span.description Go To Line - h3 #{translate("editing")} - .row - .col-xs-4 - .hotkey - span.combination {{ctrl}} + / - span.description Toggle Comment - .hotkey - span.combination {{ctrl}} + D - span.description Delete Current Line - .hotkey - span.combination {{ctrl}} + A - span.description Select All - .col-xs-4 - .hotkey - span.combination Ctrl + U - span.description To Uppercase - .hotkey - span.combination Ctrl + Shift + U - span.description To Lowercase - .hotkey - span.combination Tab - span.description Indent Selection - .col-xs-4 - .hotkey - span.combination {{ctrl}} + B - span.description Bold text - .hotkey - span.combination {{ctrl}} + I - span.description Italic Text - - h3 #{translate("autocomplete")} - .row - .col-xs-4 - .hotkey - span.combination Ctrl + Space - span.description Autocomplete Menu - .col-xs-4 - .hotkey - span.combination Tab / Up / Down - span.description Select Candidate - .col-xs-4 - .hotkey - span.combination Enter - span.description Insert Candidate - - h3 !{translate("autocomplete_references")} - .row - .col-xs-4 - .hotkey - span.combination Ctrl + Space - span.description Search References - - h3(ng-if="trackChangesVisible") #{translate("review")} - .row(ng-if="trackChangesVisible") - .col-xs-4 - .hotkey - span.combination {{ctrl}} + J - span.description Toggle review panel - .col-xs-4 - .hotkey - span.combination {{ctrl}} + Shift + A - span.description Toggle track changes - .col-xs-4 - .hotkey - span.combination {{ctrl}} + Shift + C - span.description Add a comment - - .modal-footer - button.btn.btn-default( - ng-click="cancel()" - ) #{translate("ok")} diff --git a/services/web/app/views/project/editor/left-menu.pug b/services/web/app/views/project/editor/left-menu.pug index a3715ba7bd..020bd33091 100644 --- a/services/web/app/views/project/editor/left-menu.pug +++ b/services/web/app/views/project/editor/left-menu.pug @@ -201,10 +201,16 @@ aside#left-menu.full-size( h4 #{translate("help")} ul.list-unstyled.nav - li(ng-controller="HotkeysController") + li(ng-controller="HotkeysModalController") a(ng-click="openHotkeysModal()") i.fa.fa-keyboard-o.fa-fw |    #{translate("show_hotkeys")} + + hotkeys-modal( + handle-hide="handleHide" + show="show" + track-changes-visible="trackChangesVisible" + ) if showSupport li a(href='/learn', target="_blank") diff --git a/services/web/frontend/extracted-translation-keys.json b/services/web/frontend/extracted-translation-keys.json index fefc81fc46..3d453847e2 100644 --- a/services/web/frontend/extracted-translation-keys.json +++ b/services/web/frontend/extracted-translation-keys.json @@ -2,10 +2,13 @@ "auto_compile", "autocompile_disabled_reason", "autocompile_disabled", + "autocomplete", + "autocomplete_references", "clear_cached_files", "clsi_maintenance", "clsi_unavailable", "collapse", + "common", "compile_error_description", "compile_error_entry_description", "compile_mode", @@ -16,6 +19,7 @@ "download_file", "download_pdf", "duplicate_paths_found", + "editing", "expand", "fast", "file_outline", @@ -27,6 +31,7 @@ "full_screen", "go_to_error_location", "hide_outline", + "hotkeys", "ignore_validation_errors", "latex_error", "learn_how_to_make_documents_compile_quickly", @@ -34,6 +39,7 @@ "log_entry_description", "log_hint_extra_info", "main_file_not_found", + "navigation", "n_errors_plural", "n_errors", "n_warnings_plural", @@ -58,6 +64,7 @@ "raw_logs", "recompile_from_scratch", "recompile", + "review", "run_syntax_check_now", "send_first_message", "server_error", diff --git a/services/web/frontend/js/features/hotkeys-modal/components/hotkeys-modal-content.js b/services/web/frontend/js/features/hotkeys-modal/components/hotkeys-modal-content.js new file mode 100644 index 0000000000..7b93bcd6af --- /dev/null +++ b/services/web/frontend/js/features/hotkeys-modal/components/hotkeys-modal-content.js @@ -0,0 +1,177 @@ +import React from 'react' +import { Button, Modal, Row, Col } from 'react-bootstrap' +import PropTypes from 'prop-types' +import { Trans, useTranslation } from 'react-i18next' + +function HotkeysModalContent({ + handleHide, + isMac = false, + trackChangesVisible = false +}) { + const { t } = useTranslation() + + const ctrl = isMac ? 'Cmd' : 'Ctrl' + + return ( + <> + + {t('hotkeys')} + + + +

{t('common')}

+ + + + + + + + + + + + + + +

{t('navigation')}

+ + + + + + + + + + + + + +

{t('editing')}

+ + + + + + + + + + + + + + + + + + + + +

{t('autocomplete')}

+ + + + + + + + + + + + + +

+ }} + /> +

+ + + + + + + + {trackChangesVisible && ( + <> +

{t('review')}

+ + + + + + + + + + + + + + )} +
+ + + + + + ) +} + +HotkeysModalContent.propTypes = { + isMac: PropTypes.bool, + handleHide: PropTypes.func.isRequired, + trackChangesVisible: PropTypes.bool +} + +function Hotkey({ combination, description }) { + return ( +
+ {combination} + {description} +
+ ) +} +Hotkey.propTypes = { + combination: PropTypes.string.isRequired, + description: PropTypes.string.isRequired +} + +export default HotkeysModalContent diff --git a/services/web/frontend/js/features/hotkeys-modal/components/hotkeys-modal.js b/services/web/frontend/js/features/hotkeys-modal/components/hotkeys-modal.js new file mode 100644 index 0000000000..881e7b060a --- /dev/null +++ b/services/web/frontend/js/features/hotkeys-modal/components/hotkeys-modal.js @@ -0,0 +1,26 @@ +import React from 'react' +import { Modal } from 'react-bootstrap' +import PropTypes from 'prop-types' +import HotkeysModalContent from './hotkeys-modal-content' + +function HotkeysModal({ handleHide, show, trackChangesVisible = false }) { + const isMac = /Mac/i.test(navigator.platform) + + return ( + + + + ) +} + +HotkeysModal.propTypes = { + handleHide: PropTypes.func.isRequired, + show: PropTypes.bool.isRequired, + trackChangesVisible: PropTypes.bool +} + +export default HotkeysModal diff --git a/services/web/frontend/js/features/hotkeys-modal/controllers/hotkeys-modal-controller.js b/services/web/frontend/js/features/hotkeys-modal/controllers/hotkeys-modal-controller.js new file mode 100644 index 0000000000..d28c4dc186 --- /dev/null +++ b/services/web/frontend/js/features/hotkeys-modal/controllers/hotkeys-modal-controller.js @@ -0,0 +1,25 @@ +import App from '../../../base' +import { react2angular } from 'react2angular' + +import HotkeysModal from '../components/hotkeys-modal' + +App.component('hotkeysModal', react2angular(HotkeysModal)) + +export default App.controller('HotkeysModalController', function($scope) { + $scope.show = false + + $scope.handleHide = () => { + $scope.$applyAsync(() => { + $scope.show = false + }) + } + + $scope.openHotkeysModal = () => { + $scope.trackChangesVisible = + $scope.project && $scope.project.features.trackChangesVisible + + $scope.$applyAsync(() => { + $scope.show = true + }) + } +}) diff --git a/services/web/frontend/js/ide/hotkeys/controllers/HotkeysController.js b/services/web/frontend/js/ide/hotkeys/controllers/HotkeysController.js deleted file mode 100644 index 7a576ca557..0000000000 --- a/services/web/frontend/js/ide/hotkeys/controllers/HotkeysController.js +++ /dev/null @@ -1,47 +0,0 @@ -/* eslint-disable - camelcase, - 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' -import 'ace/ace' -App.controller( - 'HotkeysController', - ($scope, $modal, eventTracking) => - ($scope.openHotkeysModal = function() { - eventTracking.sendMB('ide-open-hotkeys-modal') - - return $modal.open({ - templateUrl: 'hotkeysModalTemplate', - controller: 'HotkeysModalController', - size: 'lg', - resolve: { - trackChangesVisible() { - return $scope.project.features.trackChangesVisible - } - } - }) - }) -) - -export default App.controller('HotkeysModalController', function( - $scope, - $modalInstance, - trackChangesVisible -) { - $scope.trackChangesVisible = trackChangesVisible - if (ace.require('ace/lib/useragent').isMac) { - $scope.ctrl = 'Cmd' - } else { - $scope.ctrl = 'Ctrl' - } - - return ($scope.cancel = () => $modalInstance.dismiss()) -}) diff --git a/services/web/frontend/js/ide/hotkeys/index.js b/services/web/frontend/js/ide/hotkeys/index.js index 612c60e78b..25e1a33db1 100644 --- a/services/web/frontend/js/ide/hotkeys/index.js +++ b/services/web/frontend/js/ide/hotkeys/index.js @@ -3,4 +3,4 @@ // TODO: This file was created by bulk-decaffeinate. // Fix any style issues and re-enable lint. import './BackspaceHighjack' -import './controllers/HotkeysController' +import '../../features/hotkeys-modal/controllers/hotkeys-modal-controller' diff --git a/services/web/frontend/stories/hotkeys-modal.stories.js b/services/web/frontend/stories/hotkeys-modal.stories.js new file mode 100644 index 0000000000..e7fdf1e505 --- /dev/null +++ b/services/web/frontend/stories/hotkeys-modal.stories.js @@ -0,0 +1,24 @@ +import React from 'react' + +import HotkeysModalContent from '../js/features/hotkeys-modal/components/hotkeys-modal-content' + +// NOTE: HotkeysModalContent is wrapped in modal classes, without modal behaviours +export const Basic = args => ( +
+
+ +
+
+) + +export default { + title: 'Hotkeys Modal', + component: HotkeysModalContent, + args: { + isMac: true, + trackChangesVisible: true + }, + argTypes: { + handleHide: { action: 'handleHide' } + } +} diff --git a/services/web/test/frontend/features/hotkeys-modal/components/hotkeys-modal.test.js b/services/web/test/frontend/features/hotkeys-modal/components/hotkeys-modal.test.js new file mode 100644 index 0000000000..1065d0ff7a --- /dev/null +++ b/services/web/test/frontend/features/hotkeys-modal/components/hotkeys-modal.test.js @@ -0,0 +1,61 @@ +import React from 'react' +import { render, screen } from '@testing-library/react' +import HotkeysModalContent from '../../../../../frontend/js/features/hotkeys-modal/components/hotkeys-modal-content' +import { expect } from 'chai' + +const handleHide = () => { + // closed +} + +describe('', function() { + it('renders the translated modal title', function() { + const { container } = render( + + ) + + expect(container.querySelector('.modal-title').textContent).to.equal( + 'Hotkeys' + ) + }) + + it('renders translated heading with embedded code', function() { + const { container } = render( + + ) + + const results = container.querySelectorAll('h3 code') + expect(results).to.have.length(1) + }) + + it('renders the hotkey descriptions', function() { + const { container } = render( + + ) + + const hotkeys = container.querySelectorAll('[data-test-selector="hotkey"]') + expect(hotkeys).to.have.length(19) + }) + + it('renders extra hotkey descriptions when Track Changes is enabled', function() { + const { container } = render( + + ) + + const hotkeys = container.querySelectorAll('[data-test-selector="hotkey"]') + expect(hotkeys).to.have.length(22) + }) + + it('uses Ctrl for non-macOS', function() { + render() + + screen.getAllByText(/Ctrl/) + expect(screen.queryByText(/Cmd/)).to.not.exist + }) + + it('uses Cmd for macOS', function() { + render() + + screen.getAllByText(/Cmd/) + expect(screen.queryByText(/Ctrl/)).to.not.exist + }) +})