From d55e46d3c00c20589c521cf3dddf10a39d5ce9b5 Mon Sep 17 00:00:00 2001 From: Miguel Serrano Date: Tue, 23 Feb 2021 11:17:41 +0100 Subject: [PATCH] Merge pull request #3642 from overleaf/msm-history-track-changes-toggles [ReactNavToolbar] Track changes and History toggle buttons. GitOrigin-RevId: a67a9a488c0960dba3f3d374cde4db0080ed2952 --- services/web/app/views/project/editor.pug | 13 +--- .../app/views/project/editor/header-react.pug | 5 +- .../web/frontend/extracted-translations.json | 1 + .../js/features/chat/components/chat-pane.js | 7 +- .../js/features/chat/context/chat-context.js | 27 ++++++-- .../editor-navigation-toolbar-root.js | 69 +++++++++++++++---- .../components/history-toggle-button.js | 28 ++++++++ .../components/toolbar-header.js | 39 +++++++++-- .../components/track-changes-toggle-button.js | 34 +++++++++ .../editor-navigation-toolbar-controller.js | 13 ++-- .../controllers/root-context-controller.js | 37 +--------- .../js/shared/context/editor-context.js | 51 +++----------- .../js/shared/context/layout-context.js | 59 ++++++++++++++++ .../js/shared/context/root-context.js | 28 ++------ .../shared/context/util/scope-value-hook.js | 38 ++++++++++ .../frontend/helpers/render-with-context.js | 13 ++-- 16 files changed, 312 insertions(+), 150 deletions(-) create mode 100644 services/web/frontend/js/features/editor-navigation-toolbar/components/history-toggle-button.js create mode 100644 services/web/frontend/js/features/editor-navigation-toolbar/components/track-changes-toggle-button.js create mode 100644 services/web/frontend/js/shared/context/layout-context.js create mode 100644 services/web/frontend/js/shared/context/util/scope-value-hook.js diff --git a/services/web/app/views/project/editor.pug b/services/web/app/views/project/editor.pug index 834a6a4fc3..62fd40479c 100644 --- a/services/web/app/views/project/editor.pug +++ b/services/web/app/views/project/editor.pug @@ -8,16 +8,7 @@ block vars block content .editor(ng-controller="IdeController").full-size //- required by react2angular-shared-context, must be rendered as a top level component - div( - ng-controller="ReactRootContextController" - ) - shared-context-react( - editor-loading="editorLoading" - chat-is-open-angular="chatIsOpenAngular" - set-chat-is-open-angular="setChatIsOpenAngular" - open-doc="openDoc" - online-users-array="onlineUsersArray" - ) + shared-context-react() .loading-screen(ng-if="state.loading") .loading-screen-brand-container .loading-screen-brand( @@ -123,7 +114,7 @@ block content aside.chat( ng-controller="ReactChatController" ) - chat(reset-unread-messages="resetUnreadMessages" chat-is-open="chatIsOpen") + chat() script(type="text/ng-template", id="genericMessageModalTemplate") .modal-header diff --git a/services/web/app/views/project/editor/header-react.pug b/services/web/app/views/project/editor/header-react.pug index 9760222246..1ff99d2d93 100644 --- a/services/web/app/views/project/editor/header-react.pug +++ b/services/web/app/views/project/editor/header-react.pug @@ -1,3 +1,6 @@ div(ng-controller="EditorNavigationToolbarController") - editor-navigation-toolbar-root(on-show-left-menu-click="onShowLeftMenuClick") \ No newline at end of file + editor-navigation-toolbar-root( + open-doc="openDoc" + online-users-array="onlineUsersArray" + ) \ No newline at end of file diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index f288d2c84d..cdf57f3f8a 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -54,6 +54,7 @@ "go_to_error_location": "", "headers": "", "hide_outline": "", + "history": "", "hotkeys": "", "ignore_validation_errors": "", "invalid_file_name": "", diff --git a/services/web/frontend/js/features/chat/components/chat-pane.js b/services/web/frontend/js/features/chat/components/chat-pane.js index ad529dda24..51bd7d2526 100644 --- a/services/web/frontend/js/features/chat/components/chat-pane.js +++ b/services/web/frontend/js/features/chat/components/chat-pane.js @@ -1,19 +1,18 @@ import React, { useEffect, useState } from 'react' +import PropTypes from 'prop-types' import MessageList from './message-list' import MessageInput from './message-input' import InfiniteScroll from './infinite-scroll' import Icon from '../../../shared/components/icon' import { useTranslation } from 'react-i18next' -import { useEditorContext } from '../../../shared/context/editor-context' +import { useLayoutContext } from '../../../shared/context/layout-context' import withErrorBoundary from '../../../infrastructure/error-boundary' import { useChatContext } from '../context/chat-context' function ChatPane() { const { t } = useTranslation() - const { - ui: { chatIsOpen } - } = useEditorContext() + const { chatIsOpen } = useLayoutContext({ chatIsOpen: PropTypes.bool }) const { userId, 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 30f173f595..1211658077 100644 --- a/services/web/frontend/js/features/chat/context/chat-context.js +++ b/services/web/frontend/js/features/chat/context/chat-context.js @@ -10,20 +10,37 @@ import { useApplicationContext } from '../../../shared/context/application-conte import { useEditorContext } from '../../../shared/context/editor-context' import { ChatStore } from '../store/chat-store' import useBrowserWindow from '../../../infrastructure/browser-window-hook' +import { useLayoutContext } from '../../../shared/context/layout-context' export const ChatContext = createContext() +ChatContext.Provider.propTypes = { + value: PropTypes.shape({ + userId: PropTypes.string.isRequired, + atEnd: PropTypes.bool, + loading: PropTypes.bool, + messages: PropTypes.array.isRequired, + unreadMessageCount: PropTypes.number.isRequired, + resetUnreadMessageCount: PropTypes.func.isRequired, + loadMoreMessages: PropTypes.func.isRequired, + sendMessage: PropTypes.func.isRequired + }).isRequired +} + export function ChatProvider({ children }) { const { hasFocus: windowHasFocus, flashTitle, stopFlashingTitle } = useBrowserWindow() - const { user } = useApplicationContext() - const { - projectId, - ui: { chatIsOpen } - } = useEditorContext() + const { user } = useApplicationContext({ + user: PropTypes.shape({ id: PropTypes.string.isRequired }.isRequired) + }) + const { projectId } = useEditorContext({ + projectId: PropTypes.string.isRequired + }) + + const { chatIsOpen } = useLayoutContext({ chatIsOpen: PropTypes.bool }) const [unreadMessageCount, setUnreadMessageCount] = useState(0) function resetUnreadMessageCount() { 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 a44e1f5e99..0844ee34d1 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 @@ -3,23 +3,61 @@ import PropTypes from 'prop-types' import ToolbarHeader from './toolbar-header' import { useEditorContext } from '../../../shared/context/editor-context' import { useChatContext } from '../../chat/context/chat-context' +import { useLayoutContext } from '../../../shared/context/layout-context' + +const editorContextPropTypes = { + cobranding: PropTypes.object, + loading: PropTypes.bool, + isRestrictedTokenMember: PropTypes.bool +} + +const layoutContextPropTypes = { + chatIsOpen: PropTypes.bool, + setChatIsOpen: PropTypes.func.isRequired, + reviewPanelOpen: PropTypes.bool, + setReviewPanelOpen: PropTypes.func.isRequired, + view: PropTypes.string, + setView: PropTypes.func.isRequired, + setLeftMenuShown: PropTypes.func.isRequired +} + +function EditorNavigationToolbarRoot({ onlineUsersArray, openDoc }) { + const { cobranding, loading, isRestrictedTokenMember } = useEditorContext( + editorContextPropTypes + ) -function EditorNavigationToolbarRoot({ onShowLeftMenuClick }) { const { - cobranding, - loading, - ui, - onlineUsersArray, - openDoc - } = useEditorContext() + chatIsOpen, + setChatIsOpen, + reviewPanelOpen, + setReviewPanelOpen, + view, + setView, + setLeftMenuShown + } = useLayoutContext(layoutContextPropTypes) + const { resetUnreadMessageCount, unreadMessageCount } = useChatContext() const toggleChatOpen = useCallback(() => { - if (!ui.chatIsOpen) { + if (!chatIsOpen) { resetUnreadMessageCount() } - ui.toggleChatOpen() - }, [ui, resetUnreadMessageCount]) + setChatIsOpen(value => !value) + }, [chatIsOpen, setChatIsOpen, resetUnreadMessageCount]) + + const toggleReviewPanelOpen = useCallback( + () => setReviewPanelOpen(value => !value), + [setReviewPanelOpen] + ) + + const toggleHistoryOpen = useCallback(() => { + setView(view === 'history' ? 'editor' : 'history') + }, [view, setView]) + + const onShowLeftMenuClick = useCallback( + () => setLeftMenuShown(value => !value), + [setLeftMenuShown] + ) function goToUser(user) { if (user.doc && typeof user.row === 'number') { @@ -34,16 +72,23 @@ function EditorNavigationToolbarRoot({ onShowLeftMenuClick }) { style={loading ? { display: 'none' } : {}} cobranding={cobranding} onShowLeftMenuClick={onShowLeftMenuClick} - chatIsOpen={ui.chatIsOpen} + chatIsOpen={chatIsOpen} unreadMessageCount={unreadMessageCount} toggleChatOpen={toggleChatOpen} + reviewPanelOpen={reviewPanelOpen} + toggleReviewPanelOpen={toggleReviewPanelOpen} + historyIsOpen={view === 'history'} + toggleHistoryOpen={toggleHistoryOpen} onlineUsers={onlineUsersArray} goToUser={goToUser} + isRestrictedTokenMember={isRestrictedTokenMember} /> ) } EditorNavigationToolbarRoot.propTypes = { - onShowLeftMenuClick: PropTypes.func.isRequired + onlineUsersArray: PropTypes.array.isRequired, + openDoc: PropTypes.func.isRequired } + export default EditorNavigationToolbarRoot diff --git a/services/web/frontend/js/features/editor-navigation-toolbar/components/history-toggle-button.js b/services/web/frontend/js/features/editor-navigation-toolbar/components/history-toggle-button.js new file mode 100644 index 0000000000..6bc79ccc33 --- /dev/null +++ b/services/web/frontend/js/features/editor-navigation-toolbar/components/history-toggle-button.js @@ -0,0 +1,28 @@ +import React from 'react' +import PropTypes from 'prop-types' +import classNames from 'classnames' +import { useTranslation } from 'react-i18next' +import Icon from '../../../shared/components/icon' + +function HistoryToggleButton({ historyIsOpen, onClick }) { + const { t } = useTranslation() + + const classes = classNames('btn', 'btn-full-height', { + active: historyIsOpen + }) + + return ( + // eslint-disable-next-line jsx-a11y/anchor-is-valid + + +

{t('history')}

+
+ ) +} + +HistoryToggleButton.propTypes = { + historyIsOpen: PropTypes.bool, + onClick: PropTypes.func.isRequired +} + +export default HistoryToggleButton 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 99ab4650c9..f08c7601dc 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,15 +5,22 @@ 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 TrackChangesToggleButton from './track-changes-toggle-button' +import HistoryToggleButton from './history-toggle-button' function ToolbarHeader({ cobranding, onShowLeftMenuClick, chatIsOpen, toggleChatOpen, + reviewPanelOpen, + toggleReviewPanelOpen, + historyIsOpen, + toggleHistoryOpen, unreadMessageCount, onlineUsers, - goToUser + goToUser, + isRestrictedTokenMember }) { return (
@@ -24,11 +31,24 @@ function ToolbarHeader({
- + {!isRestrictedTokenMember && ( + <> + + + + + )}
) @@ -39,9 +59,14 @@ ToolbarHeader.propTypes = { cobranding: PropTypes.object, chatIsOpen: PropTypes.bool, toggleChatOpen: PropTypes.func.isRequired, + reviewPanelOpen: PropTypes.bool, + toggleReviewPanelOpen: PropTypes.func.isRequired, + historyIsOpen: PropTypes.bool, + toggleHistoryOpen: PropTypes.func.isRequired, unreadMessageCount: PropTypes.number.isRequired, onlineUsers: PropTypes.array.isRequired, - goToUser: PropTypes.func.isRequired + goToUser: PropTypes.func.isRequired, + isRestrictedTokenMember: PropTypes.bool } export default ToolbarHeader diff --git a/services/web/frontend/js/features/editor-navigation-toolbar/components/track-changes-toggle-button.js b/services/web/frontend/js/features/editor-navigation-toolbar/components/track-changes-toggle-button.js new file mode 100644 index 0000000000..edd36f471f --- /dev/null +++ b/services/web/frontend/js/features/editor-navigation-toolbar/components/track-changes-toggle-button.js @@ -0,0 +1,34 @@ +import React from 'react' +import PropTypes from 'prop-types' +import classNames from 'classnames' +import { useTranslation } from 'react-i18next' + +function TrackChangesToggleButton({ trackChangesIsOpen, disabled, onClick }) { + const { t } = useTranslation() + const classes = classNames('btn', 'btn-full-height', { + active: trackChangesIsOpen && !disabled, + disabled: disabled + }) + + return ( + // eslint-disable-next-line jsx-a11y/anchor-is-valid + + +

{t('review')}

+
+ ) +} + +TrackChangesToggleButton.propTypes = { + trackChangesIsOpen: PropTypes.bool, + disabled: PropTypes.bool, + onClick: PropTypes.func.isRequired +} + +export default TrackChangesToggleButton diff --git a/services/web/frontend/js/features/editor-navigation-toolbar/controllers/editor-navigation-toolbar-controller.js b/services/web/frontend/js/features/editor-navigation-toolbar/controllers/editor-navigation-toolbar-controller.js index bc385b60ab..31a674faaf 100644 --- a/services/web/frontend/js/features/editor-navigation-toolbar/controllers/editor-navigation-toolbar-controller.js +++ b/services/web/frontend/js/features/editor-navigation-toolbar/controllers/editor-navigation-toolbar-controller.js @@ -4,15 +4,18 @@ import EditorNavigationToolbarRoot from '../components/editor-navigation-toolbar import { rootContext } from '../../../shared/context/root-context' App.controller('EditorNavigationToolbarController', function($scope, ide) { - $scope.onShowLeftMenuClick = () => - ide.$scope.$applyAsync(() => { - ide.$scope.ui.leftMenuShown = !ide.$scope.ui.leftMenuShown - }) + // wrapper is required to avoid scope problems with `this` inside `EditorManager` + $scope.openDoc = (doc, args) => ide.editorManager.openDoc(doc, args) }) App.component( 'editorNavigationToolbarRoot', react2angular(rootContext.use(EditorNavigationToolbarRoot), [ - 'onShowLeftMenuClick' + 'openDoc', + + // `$scope.onlineUsersArray` is already populated by `OnlineUsersManager`, which also creates + // a new array instance every time the list of online users change (which should refresh the + // value passed to React as a prop, triggering a re-render) + 'onlineUsersArray' ]) ) diff --git a/services/web/frontend/js/shared/context/controllers/root-context-controller.js b/services/web/frontend/js/shared/context/controllers/root-context-controller.js index 90b17f4b19..19d98e92eb 100644 --- a/services/web/frontend/js/shared/context/controllers/root-context-controller.js +++ b/services/web/frontend/js/shared/context/controllers/root-context-controller.js @@ -2,42 +2,7 @@ import App from '../../../base' import { react2angular } from 'react2angular' import { rootContext } from '../root-context' -App.controller('ReactRootContextController', function($scope, ide) { - $scope.editorLoading = !!ide.$scope.state.loading - ide.$scope.$watch('state.loading', editorLoading => { - $scope.editorLoading = editorLoading - }) - - $scope.setChatIsOpenAngular = value => { - ide.$scope.$applyAsync(() => { - ide.$scope.ui.chatOpen = value - }) - } - - // we need to pass `$scope.ui.chatOpen` to Angular while both React and Angular - // Navigation Toolbars exist in the codebase. Once the Angular version is removed, - // the React Navigation Toolbar will be the only source of truth for the open/closed state, - // but `setChatIsOpenAngular` will still need to exist since Angular is responsible of the - // global layout - $scope.chatIsOpenAngular = ide.$scope.ui.chatOpen - ide.$scope.$watch('ui.chatOpen', value => { - $scope.chatIsOpenAngular = value - }) - - // wrapper is required to avoid scope problems with `this` inside `EditorManager` - $scope.openDoc = (doc, args) => ide.editorManager.openDoc(doc, args) -}) - App.component( 'sharedContextReact', - react2angular(rootContext.component, [ - 'editorLoading', - 'setChatIsOpenAngular', - 'chatIsOpenAngular', - 'openDoc', - // `$scope.onlineUsersArray` is already populated by `OnlineUsersManager`, which also creates - // a new array instance every time the list of online users change (which should refresh the - // value passed to React as a prop, triggering a re-render) - 'onlineUsersArray' - ]) + react2angular(rootContext.component, [], ['ide']) ) diff --git a/services/web/frontend/js/shared/context/editor-context.js b/services/web/frontend/js/shared/context/editor-context.js index 988a5a7f2d..4a41ad0cb9 100644 --- a/services/web/frontend/js/shared/context/editor-context.js +++ b/services/web/frontend/js/shared/context/editor-context.js @@ -1,6 +1,6 @@ -import React, { createContext, useCallback, useContext, useEffect } from 'react' +import React, { createContext, useContext } from 'react' import PropTypes from 'prop-types' -import usePersistedState from '../../infrastructure/persisted-state-hook' +import useScopeValue from './util/scope-value-hook' export const EditorContext = createContext() @@ -13,18 +13,12 @@ EditorContext.Provider.propTypes = { }), loading: PropTypes.bool, projectId: PropTypes.string.isRequired, - isProjectOwner: PropTypes.bool + isProjectOwner: PropTypes.bool, + isRestrictedTokenMember: PropTypes.bool }) } -export function EditorProvider({ - children, - loading, - chatIsOpenAngular, - setChatIsOpenAngular, - openDoc, - onlineUsersArray -}) { +export function EditorProvider({ children, $scope }) { const cobranding = window.brandVariation ? { logoImgUrl: window.brandVariation.logo_url, @@ -38,39 +32,14 @@ export function EditorProvider({ ? window._ide.$scope.project.owner._id : null - const [chatIsOpen, setChatIsOpen] = usePersistedState( - 'editor.ui.chat.open', - false - ) - - const toggleChatOpen = useCallback(() => { - setChatIsOpen(!chatIsOpen) - setChatIsOpenAngular(!chatIsOpen) - }, [chatIsOpen, setChatIsOpenAngular, setChatIsOpen]) - - // updates React's `chatIsOpen` state when the chat is opened by Angular. - // In order to prevent race conditions with `toggleChatOpen` it's not a 1:1 binding: - // Angular forces the React state to `true`, but can only set it to `false` when - // the React state is explicitly `true`. - useEffect(() => { - if (chatIsOpenAngular) { - setChatIsOpen(true) - } else if (chatIsOpen) { - setChatIsOpen(false) - } - }, [chatIsOpenAngular, chatIsOpen, setChatIsOpen]) + const [loading] = useScopeValue('state.loading', $scope) const editorContextValue = { cobranding, loading, projectId: window.project_id, isProjectOwner: ownerId === window.user.id, - openDoc, - onlineUsersArray, - ui: { - chatIsOpen, - toggleChatOpen - } + isRestrictedTokenMember: window.isRestrictedTokenMember } return ( @@ -82,11 +51,7 @@ export function EditorProvider({ EditorProvider.propTypes = { children: PropTypes.any, - loading: PropTypes.bool, - chatIsOpenAngular: PropTypes.bool, - setChatIsOpenAngular: PropTypes.func.isRequired, - openDoc: PropTypes.func.isRequired, - onlineUsersArray: PropTypes.array.isRequired + $scope: 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 new file mode 100644 index 0000000000..29e33cc4c5 --- /dev/null +++ b/services/web/frontend/js/shared/context/layout-context.js @@ -0,0 +1,59 @@ +import React, { createContext, useContext } from 'react' +import PropTypes from 'prop-types' +import useScopeValue from './util/scope-value-hook' + +export const LayoutContext = createContext() + +LayoutContext.Provider.propTypes = { + value: PropTypes.shape({ + view: PropTypes.string, + setView: PropTypes.func.isRequired, + chatIsOpen: PropTypes.bool, + setChatIsOpen: PropTypes.func.isRequired, + reviewPanelOpen: PropTypes.bool, + setReviewPanelOpen: PropTypes.func.isRequired, + leftMenuShown: PropTypes.bool, + setLeftMenuShown: PropTypes.func.isRequired + }).isRequired +} + +export function LayoutProvider({ children, $scope }) { + const [view, setView] = useScopeValue('ui.view', $scope) + const [chatIsOpen, setChatIsOpen] = useScopeValue('ui.chatOpen', $scope) + const [reviewPanelOpen, setReviewPanelOpen] = useScopeValue( + 'ui.reviewPanelOpen', + $scope + ) + const [leftMenuShown, setLeftMenuShown] = useScopeValue( + 'ui.leftMenuShown', + $scope + ) + + const layoutContextValue = { + view, + setView, + chatIsOpen, + setChatIsOpen, + reviewPanelOpen, + setReviewPanelOpen, + leftMenuShown, + setLeftMenuShown + } + + return ( + + {children} + + ) +} + +LayoutProvider.propTypes = { + children: PropTypes.any, + $scope: PropTypes.any.isRequired +} + +export function useLayoutContext(propTypes) { + const data = useContext(LayoutContext) + PropTypes.checkPropTypes(propTypes, data, 'data', 'LayoutContext.Provider') + return data +} diff --git a/services/web/frontend/js/shared/context/root-context.js b/services/web/frontend/js/shared/context/root-context.js index ae91ba783b..892d40297f 100644 --- a/services/web/frontend/js/shared/context/root-context.js +++ b/services/web/frontend/js/shared/context/root-context.js @@ -4,25 +4,15 @@ import { ApplicationProvider } from './application-context' import { EditorProvider } from './editor-context' import createSharedContext from 'react2angular-shared-context' import { ChatProvider } from '../../features/chat/context/chat-context' +import { LayoutProvider } from './layout-context' -export function ContextRoot({ - children, - editorLoading, - chatIsOpenAngular, - setChatIsOpenAngular, - openDoc, - onlineUsersArray -}) { +export function ContextRoot({ children, ide }) { return ( - - {children} + + + {children} + ) @@ -30,11 +20,7 @@ export function ContextRoot({ ContextRoot.propTypes = { children: PropTypes.any, - editorLoading: PropTypes.bool, - chatIsOpenAngular: PropTypes.bool, - setChatIsOpenAngular: PropTypes.func.isRequired, - openDoc: PropTypes.func.isRequired, - onlineUsersArray: PropTypes.array.isRequired + ide: PropTypes.any.isRequired } export const rootContext = createSharedContext(ContextRoot) diff --git a/services/web/frontend/js/shared/context/util/scope-value-hook.js b/services/web/frontend/js/shared/context/util/scope-value-hook.js new file mode 100644 index 0000000000..4c233ab6d1 --- /dev/null +++ b/services/web/frontend/js/shared/context/util/scope-value-hook.js @@ -0,0 +1,38 @@ +import { useCallback, useEffect, useState } from 'react' +import _ from 'lodash' + +/** + * Binds a property in an Angular scope making it accessible in a React + * component. The interface is compatible with React.useState(), including + * the option of passing a function to the setter. + * + * @param {string} path - dot '.' path of a property in `sourceScope`. + * @param {object} $scope - Angular $scope containing the value to bind. + * @returns {[any, function]} - Binded value and setter function tuple. + */ +export default function useScopeValue(path, $scope, deep = false) { + const [value, setValue] = useState(() => _.get($scope, path)) + + useEffect(() => { + return $scope.$watch( + path, + newValue => { + setValue(newValue) + }, + deep + ) + }, [path, $scope, deep]) + + const scopeSetter = useCallback( + newValue => { + setValue(val => { + const actualNewValue = _.isFunction(newValue) ? newValue(val) : newValue + $scope.$applyAsync(() => _.set($scope, path, actualNewValue)) + return actualNewValue + }) + }, + [path, $scope] + ) + + return [value, scopeSetter] +} diff --git a/services/web/test/frontend/helpers/render-with-context.js b/services/web/test/frontend/helpers/render-with-context.js index a551deab75..ad7901bbc4 100644 --- a/services/web/test/frontend/helpers/render-with-context.js +++ b/services/web/test/frontend/helpers/render-with-context.js @@ -4,6 +4,7 @@ import { ApplicationProvider } from '../../../frontend/js/shared/context/applica import { EditorProvider } from '../../../frontend/js/shared/context/editor-context' import sinon from 'sinon' import { ChatProvider } from '../../../frontend/js/features/chat/context/chat-context' +import { LayoutProvider } from '../../../frontend/js/shared/context/layout-context' export function renderWithEditorContext( children, @@ -17,7 +18,11 @@ export function renderWithEditorContext( owner: { _id: '124abd' } - } + }, + ui: { + chatOpen: true + }, + $watch: () => {} }, socket: { on: sinon.stub(), @@ -27,19 +32,17 @@ export function renderWithEditorContext( return render( {}} - setChatIsOpenAngular={() => {}} openDoc={() => {}} onlineUsersArray={[]} + $scope={window._ide.$scope} > - {children} + {children} ) } export function renderWithChatContext(children, { user, projectId } = {}) { - global.localStorage.setItem('editor.ui.chat.open', true) return renderWithEditorContext({children}, { user, projectId