From a555f0d309004b38b581ca8573a985a5d5ac1f96 Mon Sep 17 00:00:00 2001 From: Miguel Serrano Date: Wed, 10 Mar 2021 13:19:54 +0100 Subject: [PATCH] [ReactNavToolbar] Project name + pdf and share project buttons (#3709) * Added project name, pdf toggle and share project buttons to navigation toolbar * Added PropTypes check to `useChatContext()` * React context updates for project name/rename, pdf view and share moda * Hide PDF button when pdfLayout != 'flat' GitOrigin-RevId: 3f4a1b072259df7148d3417cd22116702bdd79ac --- .../app/views/project/editor/header-react.pug | 13 +-- .../web/frontend/extracted-translations.json | 1 + .../js/features/chat/context/chat-context.js | 6 +- .../editor-navigation-toolbar-root.js | 54 ++++++++++-- .../components/pdf-toggle-button.js | 36 ++++++++ .../components/project-name-editable-label.js | 82 +++++++++++++++++++ .../components/share-project-button.js | 22 +++++ .../components/toolbar-header.js | 33 +++++++- .../editor-navigation-toolbar-controller.js | 7 +- .../js/infrastructure/browser-window-hook.js | 14 +++- .../controllers/root-context-controller.js | 2 +- .../js/shared/context/editor-context.js | 67 +++++++++++++-- .../js/shared/context/layout-context.js | 8 +- .../js/shared/context/root-context.js | 7 +- .../editor-navigation-toolbar.stories.js | 13 ++- .../frontend/helpers/render-with-context.js | 10 +-- 16 files changed, 328 insertions(+), 47 deletions(-) create mode 100644 services/web/frontend/js/features/editor-navigation-toolbar/components/pdf-toggle-button.js create mode 100644 services/web/frontend/js/features/editor-navigation-toolbar/components/project-name-editable-label.js create mode 100644 services/web/frontend/js/features/editor-navigation-toolbar/components/share-project-button.js diff --git a/services/web/app/views/project/editor/header-react.pug b/services/web/app/views/project/editor/header-react.pug index 1ff99d2d93..480934432a 100644 --- a/services/web/app/views/project/editor/header-react.pug +++ b/services/web/app/views/project/editor/header-react.pug @@ -1,6 +1,7 @@ -div(ng-controller="EditorNavigationToolbarController") - - editor-navigation-toolbar-root( - open-doc="openDoc" - online-users-array="onlineUsersArray" - ) \ No newline at end of file +div(ng-controller="ShareController") + div(ng-controller="EditorNavigationToolbarController") + editor-navigation-toolbar-root( + open-doc="openDoc" + online-users-array="onlineUsersArray" + open-share-project-modal="openShareProjectModal" + ) \ No newline at end of file diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index cdf57f3f8a..8767999001 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -109,6 +109,7 @@ "run_syntax_check_now": "", "send_first_message": "", "server_error": "", + "share": "", "show_outline": "", "something_went_wrong_rendering_pdf": "", "somthing_went_wrong_compiling": "", diff --git a/services/web/frontend/js/features/chat/context/chat-context.js b/services/web/frontend/js/features/chat/context/chat-context.js index 1211658077..c5fe86dd38 100644 --- a/services/web/frontend/js/features/chat/context/chat-context.js +++ b/services/web/frontend/js/features/chat/context/chat-context.js @@ -113,6 +113,8 @@ ChatProvider.propTypes = { children: PropTypes.any } -export function useChatContext() { - return useContext(ChatContext) +export function useChatContext(propTypes) { + const data = useContext(ChatContext) + PropTypes.checkPropTypes(propTypes, data, 'data', 'ChatContext.Provider') + return data } diff --git a/services/web/frontend/js/features/editor-navigation-toolbar/components/editor-navigation-toolbar-root.js b/services/web/frontend/js/features/editor-navigation-toolbar/components/editor-navigation-toolbar-root.js index 0844ee34d1..dd910cefaa 100644 --- a/services/web/frontend/js/features/editor-navigation-toolbar/components/editor-navigation-toolbar-root.js +++ b/services/web/frontend/js/features/editor-navigation-toolbar/components/editor-navigation-toolbar-root.js @@ -8,7 +8,10 @@ import { useLayoutContext } from '../../../shared/context/layout-context' const editorContextPropTypes = { cobranding: PropTypes.object, loading: PropTypes.bool, - isRestrictedTokenMember: PropTypes.bool + isRestrictedTokenMember: PropTypes.bool, + projectName: PropTypes.string.isRequired, + renameProject: PropTypes.func.isRequired, + isProjectOwner: PropTypes.bool } const layoutContextPropTypes = { @@ -18,13 +21,28 @@ const layoutContextPropTypes = { setReviewPanelOpen: PropTypes.func.isRequired, view: PropTypes.string, setView: PropTypes.func.isRequired, - setLeftMenuShown: PropTypes.func.isRequired + setLeftMenuShown: PropTypes.func.isRequired, + pdfLayout: PropTypes.string.isRequired } -function EditorNavigationToolbarRoot({ onlineUsersArray, openDoc }) { - const { cobranding, loading, isRestrictedTokenMember } = useEditorContext( - editorContextPropTypes - ) +const chatContextPropTypes = { + resetUnreadMessageCount: PropTypes.func.isRequired, + unreadMessageCount: PropTypes.number.isRequired +} + +function EditorNavigationToolbarRoot({ + onlineUsersArray, + openDoc, + openShareProjectModal +}) { + const { + cobranding, + loading, + isRestrictedTokenMember, + projectName, + renameProject, + isProjectOwner + } = useEditorContext(editorContextPropTypes) const { chatIsOpen, @@ -33,10 +51,13 @@ function EditorNavigationToolbarRoot({ onlineUsersArray, openDoc }) { setReviewPanelOpen, view, setView, - setLeftMenuShown + setLeftMenuShown, + pdfLayout } = useLayoutContext(layoutContextPropTypes) - const { resetUnreadMessageCount, unreadMessageCount } = useChatContext() + const { resetUnreadMessageCount, unreadMessageCount } = useChatContext( + chatContextPropTypes + ) const toggleChatOpen = useCallback(() => { if (!chatIsOpen) { @@ -54,6 +75,14 @@ function EditorNavigationToolbarRoot({ onlineUsersArray, openDoc }) { setView(view === 'history' ? 'editor' : 'history') }, [view, setView]) + const togglePdfView = useCallback(() => { + setView(view === 'pdf' ? 'editor' : 'pdf') + }, [view, setView]) + + const openShareModal = useCallback(() => { + openShareProjectModal(isProjectOwner) + }, [openShareProjectModal, isProjectOwner]) + const onShowLeftMenuClick = useCallback( () => setLeftMenuShown(value => !value), [setLeftMenuShown] @@ -82,13 +111,20 @@ function EditorNavigationToolbarRoot({ onlineUsersArray, openDoc }) { onlineUsers={onlineUsersArray} goToUser={goToUser} isRestrictedTokenMember={isRestrictedTokenMember} + projectName={projectName} + renameProject={renameProject} + openShareModal={openShareModal} + pdfViewIsOpen={view === 'pdf'} + pdfButtonIsVisible={pdfLayout === 'flat'} + togglePdfView={togglePdfView} /> ) } EditorNavigationToolbarRoot.propTypes = { onlineUsersArray: PropTypes.array.isRequired, - openDoc: PropTypes.func.isRequired + openDoc: PropTypes.func.isRequired, + openShareProjectModal: PropTypes.func.isRequired } export default EditorNavigationToolbarRoot diff --git a/services/web/frontend/js/features/editor-navigation-toolbar/components/pdf-toggle-button.js b/services/web/frontend/js/features/editor-navigation-toolbar/components/pdf-toggle-button.js new file mode 100644 index 0000000000..449738b583 --- /dev/null +++ b/services/web/frontend/js/features/editor-navigation-toolbar/components/pdf-toggle-button.js @@ -0,0 +1,36 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { OverlayTrigger, Tooltip } from 'react-bootstrap' +import classNames from 'classnames' +import Icon from '../../../shared/components/icon' + +function PdfToggleButton({ onClick, pdfViewIsOpen }) { + const classes = classNames( + 'btn', + 'btn-full-height', + 'btn-full-height-no-border', + { + active: pdfViewIsOpen + } + ) + + return ( + PDF} + > + {/* eslint-disable-next-line jsx-a11y/anchor-is-valid,jsx-a11y/click-events-have-key-events,jsx-a11y/interactive-supports-focus */} + + + + + ) +} + +PdfToggleButton.propTypes = { + onClick: PropTypes.func.isRequired, + pdfViewIsOpen: PropTypes.bool +} + +export default PdfToggleButton diff --git a/services/web/frontend/js/features/editor-navigation-toolbar/components/project-name-editable-label.js b/services/web/frontend/js/features/editor-navigation-toolbar/components/project-name-editable-label.js new file mode 100644 index 0000000000..e1dab5b491 --- /dev/null +++ b/services/web/frontend/js/features/editor-navigation-toolbar/components/project-name-editable-label.js @@ -0,0 +1,82 @@ +import React, { useEffect, useState, useRef } from 'react' +import PropTypes from 'prop-types' +import classNames from 'classnames' +import Icon from '../../../shared/components/icon' + +function ProjectNameEditableLabel({ + projectName, + userIsAdmin, + onChange, + className +}) { + const [isRenaming, setIsRenaming] = useState(false) + + const canRename = userIsAdmin && !isRenaming + + const [inputContent, setInputContent] = useState(projectName) + + const inputRef = useRef(null) + + useEffect(() => { + if (isRenaming) { + inputRef.current.select() + } + }, [isRenaming]) + + function startRenaming() { + setInputContent(projectName) + setIsRenaming(true) + } + + function handleKeyDown(event) { + if (event.key === 'Enter') { + event.preventDefault() + setIsRenaming(false) + onChange(event.target.value) + } + } + + function handleOnChange(event) { + setInputContent(event.target.value) + } + + function handleBlur() { + setIsRenaming(false) + } + + return ( +
+ {!isRenaming && ( + + {projectName} + + )} + {isRenaming && ( + + )} + {canRename && ( + // eslint-disable-next-line jsx-a11y/anchor-is-valid, jsx-a11y/click-events-have-key-events, jsx-a11y/interactive-supports-focus + + + + )} +
+ ) +} + +ProjectNameEditableLabel.propTypes = { + projectName: PropTypes.string.isRequired, + userIsAdmin: PropTypes.bool, + onChange: PropTypes.func.isRequired, + className: PropTypes.string +} + +export default ProjectNameEditableLabel diff --git a/services/web/frontend/js/features/editor-navigation-toolbar/components/share-project-button.js b/services/web/frontend/js/features/editor-navigation-toolbar/components/share-project-button.js new file mode 100644 index 0000000000..d9e4dc5501 --- /dev/null +++ b/services/web/frontend/js/features/editor-navigation-toolbar/components/share-project-button.js @@ -0,0 +1,22 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { useTranslation } from 'react-i18next' +import Icon from '../../../shared/components/icon' + +function ShareProjectButton({ onClick }) { + const { t } = useTranslation() + + return ( + // eslint-disable-next-line jsx-a11y/anchor-is-valid,jsx-a11y/click-events-have-key-events,jsx-a11y/interactive-supports-focus + + +

{t('share')}

+
+ ) +} + +ShareProjectButton.propTypes = { + onClick: PropTypes.func.isRequired +} + +export default ShareProjectButton diff --git a/services/web/frontend/js/features/editor-navigation-toolbar/components/toolbar-header.js b/services/web/frontend/js/features/editor-navigation-toolbar/components/toolbar-header.js index f08c7601dc..9c255d8997 100644 --- a/services/web/frontend/js/features/editor-navigation-toolbar/components/toolbar-header.js +++ b/services/web/frontend/js/features/editor-navigation-toolbar/components/toolbar-header.js @@ -5,8 +5,11 @@ import CobrandingLogo from './cobranding-logo' import BackToProjectsButton from './back-to-projects-button' import ChatToggleButton from './chat-toggle-button' import OnlineUsersWidget from './online-users-widget' +import ProjectNameEditableLabel from './project-name-editable-label' import TrackChangesToggleButton from './track-changes-toggle-button' import HistoryToggleButton from './history-toggle-button' +import ShareProjectButton from './share-project-button' +import PdfToggleButton from './pdf-toggle-button' function ToolbarHeader({ cobranding, @@ -20,7 +23,13 @@ function ToolbarHeader({ unreadMessageCount, onlineUsers, goToUser, - isRestrictedTokenMember + isRestrictedTokenMember, + projectName, + renameProject, + openShareModal, + pdfViewIsOpen, + pdfButtonIsVisible, + togglePdfView }) { return (
@@ -29,8 +38,22 @@ function ToolbarHeader({ {cobranding ? : null} + {pdfButtonIsVisible && ( + + )} + +
+ {!isRestrictedTokenMember && ( <> { + setProjectName(oldName => { + if (oldName !== newName) { + settings.saveProjectSettings({ name: newName }).catch(response => { + setProjectName(oldName) + const { data, status } = response + if (status === 400) { + return ide.showGenericMessageModal('Error renaming project', data) + } else { + return ide.showGenericMessageModal( + 'Error renaming project', + 'Please try again in a moment' + ) + } + }) + } + return newName + }) + }, + [settings, ide, setProjectName] + ) + + const { setTitle } = useBrowserWindow() + setTitle( + `${projectName ? projectName + ' - ' : ''}Online LaTeX Editor ${appName}` + ) const editorContextValue = { cobranding, loading, projectId: window.project_id, + projectName: projectName || '', // initially might be empty in Angular + renameProject, isProjectOwner: ownerId === window.user.id, isRestrictedTokenMember: window.isRestrictedTokenMember } return ( - - {children} - + <> + + {children} + + ) } EditorProvider.propTypes = { children: PropTypes.any, - $scope: PropTypes.any.isRequired + ide: PropTypes.any.isRequired, + settings: PropTypes.any.isRequired } export function useEditorContext(propTypes) { diff --git a/services/web/frontend/js/shared/context/layout-context.js b/services/web/frontend/js/shared/context/layout-context.js index 29e33cc4c5..2b421aa4a4 100644 --- a/services/web/frontend/js/shared/context/layout-context.js +++ b/services/web/frontend/js/shared/context/layout-context.js @@ -13,7 +13,8 @@ LayoutContext.Provider.propTypes = { reviewPanelOpen: PropTypes.bool, setReviewPanelOpen: PropTypes.func.isRequired, leftMenuShown: PropTypes.bool, - setLeftMenuShown: PropTypes.func.isRequired + setLeftMenuShown: PropTypes.func.isRequired, + pdfLayout: PropTypes.oneOf(['sideBySide', 'flat', 'split']).isRequired }).isRequired } @@ -29,6 +30,8 @@ export function LayoutProvider({ children, $scope }) { $scope ) + const [pdfLayout] = useScopeValue('ui.pdfLayout', $scope) + const layoutContextValue = { view, setView, @@ -37,7 +40,8 @@ export function LayoutProvider({ children, $scope }) { reviewPanelOpen, setReviewPanelOpen, leftMenuShown, - setLeftMenuShown + setLeftMenuShown, + pdfLayout } return ( diff --git a/services/web/frontend/js/shared/context/root-context.js b/services/web/frontend/js/shared/context/root-context.js index 892d40297f..b1fde159a1 100644 --- a/services/web/frontend/js/shared/context/root-context.js +++ b/services/web/frontend/js/shared/context/root-context.js @@ -6,10 +6,10 @@ import createSharedContext from 'react2angular-shared-context' import { ChatProvider } from '../../features/chat/context/chat-context' import { LayoutProvider } from './layout-context' -export function ContextRoot({ children, ide }) { +export function ContextRoot({ children, ide, settings }) { return ( - + {children} @@ -20,7 +20,8 @@ export function ContextRoot({ children, ide }) { ContextRoot.propTypes = { children: PropTypes.any, - ide: PropTypes.any.isRequired + ide: PropTypes.any.isRequired, + settings: PropTypes.any.isRequired } export const rootContext = createSharedContext(ContextRoot) diff --git a/services/web/frontend/stories/editor-navigation-toolbar.stories.js b/services/web/frontend/stories/editor-navigation-toolbar.stories.js index 4e6296c3a0..230c4cc690 100644 --- a/services/web/frontend/stories/editor-navigation-toolbar.stories.js +++ b/services/web/frontend/stories/editor-navigation-toolbar.stories.js @@ -29,11 +29,18 @@ export default { title: 'EditorNavigationToolbar', component: ToolbarHeader, argTypes: { - goToUser: { action: 'goToUser' } + goToUser: { action: 'goToUser' }, + renameProject: { action: 'renameProject' }, + toggleHistoryOpen: { action: 'toggleHistoryOpen' }, + toggleReviewPanelOpen: { action: 'toggleReviewPanelOpen' }, + toggleChatOpen: { action: 'toggleChatOpen' }, + togglePdfView: { action: 'togglePdfView' }, + openShareModal: { action: 'openShareModal' }, + onShowLeftMenuClick: { action: 'onShowLeftMenuClick' } }, args: { + projectName: 'Overleaf Project', onlineUsers: [{ user_id: 'abc', name: 'overleaf' }], - goToUser: () => {}, - onShowLeftMenuClick: () => {} + unreadMessageCount: 0 } } diff --git a/services/web/test/frontend/helpers/render-with-context.js b/services/web/test/frontend/helpers/render-with-context.js index ad7901bbc4..c89483d123 100644 --- a/services/web/test/frontend/helpers/render-with-context.js +++ b/services/web/test/frontend/helpers/render-with-context.js @@ -11,6 +11,7 @@ export function renderWithEditorContext( { user = { id: '123abd' }, projectId = 'project123' } = {} ) { window.user = user || window.user + window.ExposedSettings.appName = 'test' window.project_id = projectId != null ? projectId : window.project_id window._ide = { $scope: { @@ -20,7 +21,8 @@ export function renderWithEditorContext( } }, ui: { - chatOpen: true + chatOpen: true, + pdfLayout: 'flat' }, $watch: () => {} }, @@ -31,11 +33,7 @@ export function renderWithEditorContext( } return render( - {}} - onlineUsersArray={[]} - $scope={window._ide.$scope} - > + {children}