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"
|
||||
chat-is-open-angular="chatIsOpenAngular"
|
||||
set-chat-is-open-angular="setChatIsOpenAngular"
|
||||
open-doc="openDoc"
|
||||
online-users-array="onlineUsersArray"
|
||||
)
|
||||
.loading-screen(ng-if="state.loading")
|
||||
.loading-screen-brand-container
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
"compile_mode": "",
|
||||
"compile_terminated_by_user": "",
|
||||
"compiling": "",
|
||||
"connected_users": "",
|
||||
"conflicting_paths_found": "",
|
||||
"copy": "",
|
||||
"copy_project": "",
|
||||
|
|
|
@ -5,7 +5,13 @@ import { useEditorContext } from '../../../shared/context/editor-context'
|
|||
import { useChatContext } from '../../chat/context/chat-context'
|
||||
|
||||
function EditorNavigationToolbarRoot({ onShowLeftMenuClick }) {
|
||||
const { cobranding, loading, ui } = useEditorContext()
|
||||
const {
|
||||
cobranding,
|
||||
loading,
|
||||
ui,
|
||||
onlineUsersArray,
|
||||
openDoc
|
||||
} = useEditorContext()
|
||||
const { resetUnreadMessageCount, unreadMessageCount } = useChatContext()
|
||||
|
||||
const toggleChatOpen = useCallback(() => {
|
||||
|
@ -15,6 +21,12 @@ function EditorNavigationToolbarRoot({ onShowLeftMenuClick }) {
|
|||
ui.toggleChatOpen()
|
||||
}, [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
|
||||
// `loading ? null : <ToolbarHeader/>` causes UI glitches
|
||||
return (
|
||||
|
@ -25,6 +37,8 @@ function EditorNavigationToolbarRoot({ onShowLeftMenuClick }) {
|
|||
chatIsOpen={ui.chatIsOpen}
|
||||
unreadMessageCount={unreadMessageCount}
|
||||
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 BackToProjectsButton from './back-to-projects-button'
|
||||
import ChatToggleButton from './chat-toggle-button'
|
||||
import OnlineUsersWidget from './online-users-widget'
|
||||
|
||||
function ToolbarHeader({
|
||||
cobranding,
|
||||
onShowLeftMenuClick,
|
||||
chatIsOpen,
|
||||
toggleChatOpen,
|
||||
unreadMessageCount
|
||||
unreadMessageCount,
|
||||
onlineUsers,
|
||||
goToUser
|
||||
}) {
|
||||
return (
|
||||
<header className="toolbar toolbar-header toolbar-with-labels">
|
||||
|
@ -20,6 +23,7 @@ function ToolbarHeader({
|
|||
<BackToProjectsButton />
|
||||
</div>
|
||||
<div className="toolbar-right">
|
||||
<OnlineUsersWidget onlineUsers={onlineUsers} goToUser={goToUser} />
|
||||
<ChatToggleButton
|
||||
chatIsOpen={chatIsOpen}
|
||||
onClick={toggleChatOpen}
|
||||
|
@ -35,7 +39,9 @@ ToolbarHeader.propTypes = {
|
|||
cobranding: PropTypes.object,
|
||||
chatIsOpen: PropTypes.bool,
|
||||
toggleChatOpen: PropTypes.func.isRequired,
|
||||
unreadMessageCount: PropTypes.number.isRequired
|
||||
unreadMessageCount: PropTypes.number.isRequired,
|
||||
onlineUsers: PropTypes.array.isRequired,
|
||||
goToUser: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
export default ToolbarHeader
|
||||
|
|
|
@ -23,6 +23,9 @@ App.controller('ReactRootContextController', function($scope, ide) {
|
|||
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(
|
||||
|
@ -30,6 +33,11 @@ App.component(
|
|||
react2angular(rootContext.component, [
|
||||
'editorLoading',
|
||||
'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,
|
||||
loading,
|
||||
chatIsOpenAngular,
|
||||
setChatIsOpenAngular
|
||||
setChatIsOpenAngular,
|
||||
openDoc,
|
||||
onlineUsersArray
|
||||
}) {
|
||||
const cobranding = window.brandVariation
|
||||
? {
|
||||
|
@ -63,6 +65,8 @@ export function EditorProvider({
|
|||
loading,
|
||||
projectId: window.project_id,
|
||||
isProjectOwner: ownerId === window.user.id,
|
||||
openDoc,
|
||||
onlineUsersArray,
|
||||
ui: {
|
||||
chatIsOpen,
|
||||
toggleChatOpen
|
||||
|
@ -80,7 +84,9 @@ EditorProvider.propTypes = {
|
|||
children: PropTypes.any,
|
||||
loading: 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) {
|
||||
|
|
|
@ -9,7 +9,9 @@ export function ContextRoot({
|
|||
children,
|
||||
editorLoading,
|
||||
chatIsOpenAngular,
|
||||
setChatIsOpenAngular
|
||||
setChatIsOpenAngular,
|
||||
openDoc,
|
||||
onlineUsersArray
|
||||
}) {
|
||||
return (
|
||||
<ApplicationProvider>
|
||||
|
@ -17,6 +19,8 @@ export function ContextRoot({
|
|||
loading={editorLoading}
|
||||
chatIsOpenAngular={chatIsOpenAngular}
|
||||
setChatIsOpenAngular={setChatIsOpenAngular}
|
||||
openDoc={openDoc}
|
||||
onlineUsersArray={onlineUsersArray}
|
||||
>
|
||||
<ChatProvider>{children}</ChatProvider>
|
||||
</EditorProvider>
|
||||
|
@ -28,7 +32,9 @@ ContextRoot.propTypes = {
|
|||
children: PropTypes.any,
|
||||
editorLoading: 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)
|
||||
|
|
|
@ -1,10 +1,38 @@
|
|||
import React from 'react'
|
||||
import ToolbarHeader from '../js/features/editor-navigation-toolbar/components/toolbar-header'
|
||||
|
||||
export const Default = () => {
|
||||
return <ToolbarHeader />
|
||||
// required by ColorManager
|
||||
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 {
|
||||
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(
|
||||
<ApplicationProvider>
|
||||
<EditorProvider setChatIsOpen={() => {}} setChatIsOpenAngular={() => {}}>
|
||||
<EditorProvider
|
||||
setChatIsOpen={() => {}}
|
||||
setChatIsOpenAngular={() => {}}
|
||||
openDoc={() => {}}
|
||||
onlineUsersArray={[]}
|
||||
>
|
||||
{children}
|
||||
</EditorProvider>
|
||||
</ApplicationProvider>
|
||||
|
|
Loading…
Reference in a new issue