mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge pull request #3632 from overleaf/msm-navbar-collaborator-widget
[ReactNavToolbar] Collaborators widget GitOrigin-RevId: 65f2484962591103f02eb7624a974d0806b1abf0
This commit is contained in:
parent
77c35e3715
commit
d78644e02c
10 changed files with 208 additions and 12 deletions
|
@ -15,6 +15,8 @@ block content
|
||||||
editor-loading="editorLoading"
|
editor-loading="editorLoading"
|
||||||
chat-is-open-angular="chatIsOpenAngular"
|
chat-is-open-angular="chatIsOpenAngular"
|
||||||
set-chat-is-open-angular="setChatIsOpenAngular"
|
set-chat-is-open-angular="setChatIsOpenAngular"
|
||||||
|
open-doc="openDoc"
|
||||||
|
online-users-array="onlineUsersArray"
|
||||||
)
|
)
|
||||||
.loading-screen(ng-if="state.loading")
|
.loading-screen(ng-if="state.loading")
|
||||||
.loading-screen-brand-container
|
.loading-screen-brand-container
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
"compile_mode": "",
|
"compile_mode": "",
|
||||||
"compile_terminated_by_user": "",
|
"compile_terminated_by_user": "",
|
||||||
"compiling": "",
|
"compiling": "",
|
||||||
|
"connected_users": "",
|
||||||
"conflicting_paths_found": "",
|
"conflicting_paths_found": "",
|
||||||
"copy": "",
|
"copy": "",
|
||||||
"copy_project": "",
|
"copy_project": "",
|
||||||
|
|
|
@ -5,7 +5,13 @@ import { useEditorContext } from '../../../shared/context/editor-context'
|
||||||
import { useChatContext } from '../../chat/context/chat-context'
|
import { useChatContext } from '../../chat/context/chat-context'
|
||||||
|
|
||||||
function EditorNavigationToolbarRoot({ onShowLeftMenuClick }) {
|
function EditorNavigationToolbarRoot({ onShowLeftMenuClick }) {
|
||||||
const { cobranding, loading, ui } = useEditorContext()
|
const {
|
||||||
|
cobranding,
|
||||||
|
loading,
|
||||||
|
ui,
|
||||||
|
onlineUsersArray,
|
||||||
|
openDoc
|
||||||
|
} = useEditorContext()
|
||||||
const { resetUnreadMessageCount, unreadMessageCount } = useChatContext()
|
const { resetUnreadMessageCount, unreadMessageCount } = useChatContext()
|
||||||
|
|
||||||
const toggleChatOpen = useCallback(() => {
|
const toggleChatOpen = useCallback(() => {
|
||||||
|
@ -15,6 +21,12 @@ function EditorNavigationToolbarRoot({ onShowLeftMenuClick }) {
|
||||||
ui.toggleChatOpen()
|
ui.toggleChatOpen()
|
||||||
}, [ui, resetUnreadMessageCount])
|
}, [ui, resetUnreadMessageCount])
|
||||||
|
|
||||||
|
function goToUser(user) {
|
||||||
|
if (user.doc && typeof user.row === 'number') {
|
||||||
|
openDoc(user.doc, { gotoLine: user.row + 1 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// using {display: 'none'} as 1:1 migration from Angular's ng-hide. Using
|
// using {display: 'none'} as 1:1 migration from Angular's ng-hide. Using
|
||||||
// `loading ? null : <ToolbarHeader/>` causes UI glitches
|
// `loading ? null : <ToolbarHeader/>` causes UI glitches
|
||||||
return (
|
return (
|
||||||
|
@ -25,6 +37,8 @@ function EditorNavigationToolbarRoot({ onShowLeftMenuClick }) {
|
||||||
chatIsOpen={ui.chatIsOpen}
|
chatIsOpen={ui.chatIsOpen}
|
||||||
unreadMessageCount={unreadMessageCount}
|
unreadMessageCount={unreadMessageCount}
|
||||||
toggleChatOpen={toggleChatOpen}
|
toggleChatOpen={toggleChatOpen}
|
||||||
|
onlineUsers={onlineUsersArray}
|
||||||
|
goToUser={goToUser}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,120 @@
|
||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { Dropdown, MenuItem, OverlayTrigger, Tooltip } from 'react-bootstrap'
|
||||||
|
import Icon from '../../../shared/components/icon'
|
||||||
|
import ColorManager from '../../../ide/colors/ColorManager'
|
||||||
|
|
||||||
|
function OnlineUsersWidget({ onlineUsers, goToUser }) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const shouldDisplayDropdown = onlineUsers.length >= 4
|
||||||
|
|
||||||
|
if (shouldDisplayDropdown) {
|
||||||
|
return (
|
||||||
|
<Dropdown id="online-users" className="online-users" pullRight>
|
||||||
|
<DropDownToggleButton
|
||||||
|
bsRole="toggle"
|
||||||
|
onlineUserCount={onlineUsers.length}
|
||||||
|
/>
|
||||||
|
<Dropdown.Menu>
|
||||||
|
<MenuItem header>{t('connected_users')}</MenuItem>
|
||||||
|
{onlineUsers.map(user => (
|
||||||
|
<MenuItem
|
||||||
|
as="button"
|
||||||
|
key={user.user_id}
|
||||||
|
eventKey={user}
|
||||||
|
onSelect={goToUser}
|
||||||
|
>
|
||||||
|
<UserIcon user={user} onClick={goToUser} showName />
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Dropdown.Menu>
|
||||||
|
</Dropdown>
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<div className="online-users">
|
||||||
|
{onlineUsers.map(user => (
|
||||||
|
<OverlayTrigger
|
||||||
|
key={user.user_id}
|
||||||
|
placement="bottom"
|
||||||
|
trigger={['hover', 'focus']}
|
||||||
|
overlay={<Tooltip id="tooltip-online-user">{user.name}</Tooltip>}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
{/* OverlayTrigger won't fire unless UserIcon is wrapped in a span */}
|
||||||
|
<UserIcon user={user} onClick={goToUser} />
|
||||||
|
</span>
|
||||||
|
</OverlayTrigger>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
OnlineUsersWidget.propTypes = {
|
||||||
|
onlineUsers: PropTypes.arrayOf(
|
||||||
|
PropTypes.shape({
|
||||||
|
user_id: PropTypes.string.isRequired,
|
||||||
|
name: PropTypes.string.isRequired
|
||||||
|
})
|
||||||
|
).isRequired,
|
||||||
|
goToUser: PropTypes.func.isRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
function UserIcon({ user, showName, onClick }) {
|
||||||
|
const backgroundColor = `hsl(${ColorManager.getHueForUserId(
|
||||||
|
user.user_id
|
||||||
|
)}, 70%, 50%)`
|
||||||
|
|
||||||
|
function handleOnClick() {
|
||||||
|
onClick(user)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
|
||||||
|
<span onClick={handleOnClick}>
|
||||||
|
<span className="online-user" style={{ backgroundColor }}>
|
||||||
|
{user.name.slice(0, 1)}
|
||||||
|
</span>
|
||||||
|
{showName && user.name}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
UserIcon.propTypes = {
|
||||||
|
user: PropTypes.shape({
|
||||||
|
user_id: PropTypes.string.isRequired,
|
||||||
|
name: PropTypes.string.isRequired
|
||||||
|
}),
|
||||||
|
showName: PropTypes.bool,
|
||||||
|
onClick: PropTypes.func.isRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
const DropDownToggleButton = React.forwardRef((props, ref) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
return (
|
||||||
|
<OverlayTrigger
|
||||||
|
placement="left"
|
||||||
|
overlay={
|
||||||
|
<Tooltip id="tooltip-connected-users">{t('connected_users')}</Tooltip>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="btn online-user online-user-multi"
|
||||||
|
onClick={props.onClick} // required by Bootstrap Dropdown to trigger an opening
|
||||||
|
>
|
||||||
|
<strong>{props.onlineUserCount}</strong>
|
||||||
|
<Icon type="users" modifier="fw" />
|
||||||
|
</button>
|
||||||
|
</OverlayTrigger>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
DropDownToggleButton.propTypes = {
|
||||||
|
onlineUserCount: PropTypes.number.isRequired,
|
||||||
|
onClick: PropTypes.func
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OnlineUsersWidget
|
|
@ -4,13 +4,16 @@ import MenuButton from './menu-button'
|
||||||
import CobrandingLogo from './cobranding-logo'
|
import CobrandingLogo from './cobranding-logo'
|
||||||
import BackToProjectsButton from './back-to-projects-button'
|
import BackToProjectsButton from './back-to-projects-button'
|
||||||
import ChatToggleButton from './chat-toggle-button'
|
import ChatToggleButton from './chat-toggle-button'
|
||||||
|
import OnlineUsersWidget from './online-users-widget'
|
||||||
|
|
||||||
function ToolbarHeader({
|
function ToolbarHeader({
|
||||||
cobranding,
|
cobranding,
|
||||||
onShowLeftMenuClick,
|
onShowLeftMenuClick,
|
||||||
chatIsOpen,
|
chatIsOpen,
|
||||||
toggleChatOpen,
|
toggleChatOpen,
|
||||||
unreadMessageCount
|
unreadMessageCount,
|
||||||
|
onlineUsers,
|
||||||
|
goToUser
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<header className="toolbar toolbar-header toolbar-with-labels">
|
<header className="toolbar toolbar-header toolbar-with-labels">
|
||||||
|
@ -20,6 +23,7 @@ function ToolbarHeader({
|
||||||
<BackToProjectsButton />
|
<BackToProjectsButton />
|
||||||
</div>
|
</div>
|
||||||
<div className="toolbar-right">
|
<div className="toolbar-right">
|
||||||
|
<OnlineUsersWidget onlineUsers={onlineUsers} goToUser={goToUser} />
|
||||||
<ChatToggleButton
|
<ChatToggleButton
|
||||||
chatIsOpen={chatIsOpen}
|
chatIsOpen={chatIsOpen}
|
||||||
onClick={toggleChatOpen}
|
onClick={toggleChatOpen}
|
||||||
|
@ -35,7 +39,9 @@ ToolbarHeader.propTypes = {
|
||||||
cobranding: PropTypes.object,
|
cobranding: PropTypes.object,
|
||||||
chatIsOpen: PropTypes.bool,
|
chatIsOpen: PropTypes.bool,
|
||||||
toggleChatOpen: PropTypes.func.isRequired,
|
toggleChatOpen: PropTypes.func.isRequired,
|
||||||
unreadMessageCount: PropTypes.number.isRequired
|
unreadMessageCount: PropTypes.number.isRequired,
|
||||||
|
onlineUsers: PropTypes.array.isRequired,
|
||||||
|
goToUser: PropTypes.func.isRequired
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ToolbarHeader
|
export default ToolbarHeader
|
||||||
|
|
|
@ -23,6 +23,9 @@ App.controller('ReactRootContextController', function($scope, ide) {
|
||||||
ide.$scope.$watch('ui.chatOpen', value => {
|
ide.$scope.$watch('ui.chatOpen', value => {
|
||||||
$scope.chatIsOpenAngular = 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(
|
App.component(
|
||||||
|
@ -30,6 +33,11 @@ App.component(
|
||||||
react2angular(rootContext.component, [
|
react2angular(rootContext.component, [
|
||||||
'editorLoading',
|
'editorLoading',
|
||||||
'setChatIsOpenAngular',
|
'setChatIsOpenAngular',
|
||||||
'chatIsOpenAngular'
|
'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'
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
|
|
|
@ -21,7 +21,9 @@ export function EditorProvider({
|
||||||
children,
|
children,
|
||||||
loading,
|
loading,
|
||||||
chatIsOpenAngular,
|
chatIsOpenAngular,
|
||||||
setChatIsOpenAngular
|
setChatIsOpenAngular,
|
||||||
|
openDoc,
|
||||||
|
onlineUsersArray
|
||||||
}) {
|
}) {
|
||||||
const cobranding = window.brandVariation
|
const cobranding = window.brandVariation
|
||||||
? {
|
? {
|
||||||
|
@ -63,6 +65,8 @@ export function EditorProvider({
|
||||||
loading,
|
loading,
|
||||||
projectId: window.project_id,
|
projectId: window.project_id,
|
||||||
isProjectOwner: ownerId === window.user.id,
|
isProjectOwner: ownerId === window.user.id,
|
||||||
|
openDoc,
|
||||||
|
onlineUsersArray,
|
||||||
ui: {
|
ui: {
|
||||||
chatIsOpen,
|
chatIsOpen,
|
||||||
toggleChatOpen
|
toggleChatOpen
|
||||||
|
@ -80,7 +84,9 @@ EditorProvider.propTypes = {
|
||||||
children: PropTypes.any,
|
children: PropTypes.any,
|
||||||
loading: PropTypes.bool,
|
loading: PropTypes.bool,
|
||||||
chatIsOpenAngular: PropTypes.bool,
|
chatIsOpenAngular: PropTypes.bool,
|
||||||
setChatIsOpenAngular: PropTypes.func.isRequired
|
setChatIsOpenAngular: PropTypes.func.isRequired,
|
||||||
|
openDoc: PropTypes.func.isRequired,
|
||||||
|
onlineUsersArray: PropTypes.array.isRequired
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useEditorContext(propTypes) {
|
export function useEditorContext(propTypes) {
|
||||||
|
|
|
@ -9,7 +9,9 @@ export function ContextRoot({
|
||||||
children,
|
children,
|
||||||
editorLoading,
|
editorLoading,
|
||||||
chatIsOpenAngular,
|
chatIsOpenAngular,
|
||||||
setChatIsOpenAngular
|
setChatIsOpenAngular,
|
||||||
|
openDoc,
|
||||||
|
onlineUsersArray
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<ApplicationProvider>
|
<ApplicationProvider>
|
||||||
|
@ -17,6 +19,8 @@ export function ContextRoot({
|
||||||
loading={editorLoading}
|
loading={editorLoading}
|
||||||
chatIsOpenAngular={chatIsOpenAngular}
|
chatIsOpenAngular={chatIsOpenAngular}
|
||||||
setChatIsOpenAngular={setChatIsOpenAngular}
|
setChatIsOpenAngular={setChatIsOpenAngular}
|
||||||
|
openDoc={openDoc}
|
||||||
|
onlineUsersArray={onlineUsersArray}
|
||||||
>
|
>
|
||||||
<ChatProvider>{children}</ChatProvider>
|
<ChatProvider>{children}</ChatProvider>
|
||||||
</EditorProvider>
|
</EditorProvider>
|
||||||
|
@ -28,7 +32,9 @@ ContextRoot.propTypes = {
|
||||||
children: PropTypes.any,
|
children: PropTypes.any,
|
||||||
editorLoading: PropTypes.bool,
|
editorLoading: PropTypes.bool,
|
||||||
chatIsOpenAngular: PropTypes.bool,
|
chatIsOpenAngular: PropTypes.bool,
|
||||||
setChatIsOpenAngular: PropTypes.func.isRequired
|
setChatIsOpenAngular: PropTypes.func.isRequired,
|
||||||
|
openDoc: PropTypes.func.isRequired,
|
||||||
|
onlineUsersArray: PropTypes.array.isRequired
|
||||||
}
|
}
|
||||||
|
|
||||||
export const rootContext = createSharedContext(ContextRoot)
|
export const rootContext = createSharedContext(ContextRoot)
|
||||||
|
|
|
@ -1,10 +1,38 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import ToolbarHeader from '../js/features/editor-navigation-toolbar/components/toolbar-header'
|
import ToolbarHeader from '../js/features/editor-navigation-toolbar/components/toolbar-header'
|
||||||
|
|
||||||
export const Default = () => {
|
// required by ColorManager
|
||||||
return <ToolbarHeader />
|
window.user = { id: 42 }
|
||||||
|
|
||||||
|
export const UpToThreeConnectedUsers = args => {
|
||||||
|
return <ToolbarHeader {...args} />
|
||||||
|
}
|
||||||
|
UpToThreeConnectedUsers.args = {
|
||||||
|
onlineUsers: ['a', 'c', 'd'].map(c => ({
|
||||||
|
user_id: c,
|
||||||
|
name: `${c}_user name`
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ManyConnectedUsers = args => {
|
||||||
|
return <ToolbarHeader {...args} />
|
||||||
|
}
|
||||||
|
ManyConnectedUsers.args = {
|
||||||
|
onlineUsers: ['a', 'c', 'd', 'e', 'f'].map(c => ({
|
||||||
|
user_id: c,
|
||||||
|
name: `${c}_user name`
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: 'EditorNavigationToolbar'
|
title: 'EditorNavigationToolbar',
|
||||||
|
component: ToolbarHeader,
|
||||||
|
argTypes: {
|
||||||
|
goToUser: { action: 'goToUser' }
|
||||||
|
},
|
||||||
|
args: {
|
||||||
|
onlineUsers: [{ user_id: 'abc', name: 'overleaf' }],
|
||||||
|
goToUser: () => {},
|
||||||
|
onShowLeftMenuClick: () => {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,12 @@ export function renderWithEditorContext(
|
||||||
}
|
}
|
||||||
return render(
|
return render(
|
||||||
<ApplicationProvider>
|
<ApplicationProvider>
|
||||||
<EditorProvider setChatIsOpen={() => {}} setChatIsOpenAngular={() => {}}>
|
<EditorProvider
|
||||||
|
setChatIsOpen={() => {}}
|
||||||
|
setChatIsOpenAngular={() => {}}
|
||||||
|
openDoc={() => {}}
|
||||||
|
onlineUsersArray={[]}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</EditorProvider>
|
</EditorProvider>
|
||||||
</ApplicationProvider>
|
</ApplicationProvider>
|
||||||
|
|
Loading…
Reference in a new issue