mirror of
https://github.com/overleaf/overleaf.git
synced 2025-04-10 22:07:19 +00:00
Merge pull request #3642 from overleaf/msm-history-track-changes-toggles
[ReactNavToolbar] Track changes and History toggle buttons. GitOrigin-RevId: a67a9a488c0960dba3f3d374cde4db0080ed2952
This commit is contained in:
parent
e5c49ea19a
commit
d55e46d3c0
16 changed files with 312 additions and 150 deletions
|
@ -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
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
div(ng-controller="EditorNavigationToolbarController")
|
||||
|
||||
editor-navigation-toolbar-root(on-show-left-menu-click="onShowLeftMenuClick")
|
||||
editor-navigation-toolbar-root(
|
||||
open-doc="openDoc"
|
||||
online-users-array="onlineUsersArray"
|
||||
)
|
|
@ -54,6 +54,7 @@
|
|||
"go_to_error_location": "",
|
||||
"headers": "",
|
||||
"hide_outline": "",
|
||||
"history": "",
|
||||
"hotkeys": "",
|
||||
"ignore_validation_errors": "",
|
||||
"invalid_file_name": "",
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
<a role="button" className={classes} href="#" onClick={onClick}>
|
||||
<Icon type="fw" modifier="history" />
|
||||
<p className="toolbar-label">{t('history')}</p>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
HistoryToggleButton.propTypes = {
|
||||
historyIsOpen: PropTypes.bool,
|
||||
onClick: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
export default HistoryToggleButton
|
|
@ -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 (
|
||||
<header className="toolbar toolbar-header toolbar-with-labels">
|
||||
|
@ -24,11 +31,24 @@ function ToolbarHeader({
|
|||
</div>
|
||||
<div className="toolbar-right">
|
||||
<OnlineUsersWidget onlineUsers={onlineUsers} goToUser={goToUser} />
|
||||
<ChatToggleButton
|
||||
chatIsOpen={chatIsOpen}
|
||||
onClick={toggleChatOpen}
|
||||
unreadMessageCount={unreadMessageCount}
|
||||
/>
|
||||
{!isRestrictedTokenMember && (
|
||||
<>
|
||||
<TrackChangesToggleButton
|
||||
onClick={toggleReviewPanelOpen}
|
||||
disabled={historyIsOpen}
|
||||
trackChangesIsOpen={reviewPanelOpen}
|
||||
/>
|
||||
<HistoryToggleButton
|
||||
historyIsOpen={historyIsOpen}
|
||||
onClick={toggleHistoryOpen}
|
||||
/>
|
||||
<ChatToggleButton
|
||||
chatIsOpen={chatIsOpen}
|
||||
onClick={toggleChatOpen}
|
||||
unreadMessageCount={unreadMessageCount}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
|
@ -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
|
||||
|
|
|
@ -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
|
||||
<a
|
||||
role="button"
|
||||
disabled={disabled}
|
||||
className={classes}
|
||||
href="#"
|
||||
onClick={onClick}
|
||||
>
|
||||
<i className="review-icon" />
|
||||
<p className="toolbar-label">{t('review')}</p>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
TrackChangesToggleButton.propTypes = {
|
||||
trackChangesIsOpen: PropTypes.bool,
|
||||
disabled: PropTypes.bool,
|
||||
onClick: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
export default TrackChangesToggleButton
|
|
@ -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'
|
||||
])
|
||||
)
|
||||
|
|
|
@ -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'])
|
||||
)
|
||||
|
|
|
@ -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) {
|
||||
|
|
59
services/web/frontend/js/shared/context/layout-context.js
Normal file
59
services/web/frontend/js/shared/context/layout-context.js
Normal file
|
@ -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 (
|
||||
<LayoutContext.Provider value={layoutContextValue}>
|
||||
{children}
|
||||
</LayoutContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
|
@ -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 (
|
||||
<ApplicationProvider>
|
||||
<EditorProvider
|
||||
loading={editorLoading}
|
||||
chatIsOpenAngular={chatIsOpenAngular}
|
||||
setChatIsOpenAngular={setChatIsOpenAngular}
|
||||
openDoc={openDoc}
|
||||
onlineUsersArray={onlineUsersArray}
|
||||
>
|
||||
<ChatProvider>{children}</ChatProvider>
|
||||
<EditorProvider $scope={ide.$scope}>
|
||||
<LayoutProvider $scope={ide.$scope}>
|
||||
<ChatProvider>{children}</ChatProvider>
|
||||
</LayoutProvider>
|
||||
</EditorProvider>
|
||||
</ApplicationProvider>
|
||||
)
|
||||
|
@ -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)
|
||||
|
|
|
@ -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]
|
||||
}
|
|
@ -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(
|
||||
<ApplicationProvider>
|
||||
<EditorProvider
|
||||
setChatIsOpen={() => {}}
|
||||
setChatIsOpenAngular={() => {}}
|
||||
openDoc={() => {}}
|
||||
onlineUsersArray={[]}
|
||||
$scope={window._ide.$scope}
|
||||
>
|
||||
{children}
|
||||
<LayoutProvider $scope={window._ide.$scope}>{children}</LayoutProvider>
|
||||
</EditorProvider>
|
||||
</ApplicationProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export function renderWithChatContext(children, { user, projectId } = {}) {
|
||||
global.localStorage.setItem('editor.ui.chat.open', true)
|
||||
return renderWithEditorContext(<ChatProvider>{children}</ChatProvider>, {
|
||||
user,
|
||||
projectId
|
||||
|
|
Loading…
Add table
Reference in a new issue