mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-07 20:31:06 -05:00
Replaced application-context
with user-context
(#4246)
* Replaced `application-context` with `user-context` * deleted `user` initialization with `window.user` * fixed tests and storybook GitOrigin-RevId: 0ed4b9070d7c6d370fee2112f310c4bcfea519e7
This commit is contained in:
parent
b7802674d5
commit
9b59c0813c
15 changed files with 104 additions and 89 deletions
|
@ -8,7 +8,7 @@ import InfiniteScroll from './infinite-scroll'
|
|||
import ChatFallbackError from './chat-fallback-error'
|
||||
import Icon from '../../../shared/components/icon'
|
||||
import { useLayoutContext } from '../../../shared/context/layout-context'
|
||||
import { useApplicationContext } from '../../../shared/context/application-context'
|
||||
import { useUserContext } from '../../../shared/context/user-context'
|
||||
import withErrorBoundary from '../../../infrastructure/error-boundary'
|
||||
import { FetchError } from '../../../infrastructure/fetch-json'
|
||||
import { useChatContext } from '../context/chat-context'
|
||||
|
@ -17,8 +17,8 @@ const ChatPane = React.memo(function ChatPane() {
|
|||
const { t } = useTranslation()
|
||||
|
||||
const { chatIsOpen } = useLayoutContext({ chatIsOpen: PropTypes.bool })
|
||||
const { user } = useApplicationContext({
|
||||
user: PropTypes.shape({ id: PropTypes.string.isRequired }),
|
||||
const user = useUserContext({
|
||||
id: PropTypes.string.isRequired,
|
||||
})
|
||||
|
||||
const {
|
||||
|
|
|
@ -9,7 +9,7 @@ import {
|
|||
import PropTypes from 'prop-types'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
|
||||
import { useApplicationContext } from '../../../shared/context/application-context'
|
||||
import { useUserContext } from '../../../shared/context/user-context'
|
||||
import { useProjectContext } from '../../../shared/context/project-context'
|
||||
import { getJSON, postJSON } from '../../../infrastructure/fetch-json'
|
||||
import { appendMessage, prependMessages } from '../utils/message-list-appender'
|
||||
|
@ -117,8 +117,8 @@ ChatContext.Provider.propTypes = {
|
|||
}
|
||||
|
||||
export function ChatProvider({ children }) {
|
||||
const { user } = useApplicationContext({
|
||||
user: PropTypes.shape({ id: PropTypes.string.isRequired }),
|
||||
const user = useUserContext({
|
||||
id: PropTypes.string.isRequired,
|
||||
})
|
||||
const { _id: projectId } = useProjectContext({
|
||||
_id: PropTypes.string.isRequired,
|
||||
|
|
|
@ -4,11 +4,11 @@ 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'
|
||||
import { useApplicationContext } from '../../../shared/context/application-context'
|
||||
import { useUserContext } from '../../../shared/context/user-context'
|
||||
import { useProjectContext } from '../../../shared/context/project-context'
|
||||
|
||||
const applicationContextPropTypes = {
|
||||
user: PropTypes.object,
|
||||
const userContextPropTypes = {
|
||||
id: PropTypes.string,
|
||||
}
|
||||
|
||||
const projectContextPropTypes = {
|
||||
|
@ -21,6 +21,7 @@ const editorContextPropTypes = {
|
|||
isRestrictedTokenMember: PropTypes.bool,
|
||||
renameProject: PropTypes.func.isRequired,
|
||||
isProjectOwner: PropTypes.bool,
|
||||
permissionsLevel: PropTypes.string,
|
||||
}
|
||||
|
||||
const layoutContextPropTypes = {
|
||||
|
@ -45,7 +46,7 @@ const EditorNavigationToolbarRoot = React.memo(
|
|||
openDoc,
|
||||
openShareProjectModal,
|
||||
}) {
|
||||
const { user } = useApplicationContext(applicationContextPropTypes)
|
||||
const user = useUserContext(userContextPropTypes)
|
||||
|
||||
const { name: projectName } = useProjectContext(projectContextPropTypes)
|
||||
|
||||
|
@ -55,6 +56,7 @@ const EditorNavigationToolbarRoot = React.memo(
|
|||
isRestrictedTokenMember,
|
||||
renameProject,
|
||||
isProjectOwner,
|
||||
permissionsLevel,
|
||||
} = useEditorContext(editorContextPropTypes)
|
||||
|
||||
const {
|
||||
|
@ -110,6 +112,12 @@ const EditorNavigationToolbarRoot = React.memo(
|
|||
[openDoc]
|
||||
)
|
||||
|
||||
// the existing angular implementation prevents collaborators from updating a
|
||||
// project's name, but the backend allows that with the following logic:
|
||||
// `const hasRenamePermissions = permissionsLevel === 'owner' || permissionsLevel === 'readAndWrite'`
|
||||
// See https://github.com/overleaf/issues/issues/4492
|
||||
const hasRenamePermissions = permissionsLevel === 'owner'
|
||||
|
||||
// using {display: 'none'} as 1:1 migration from Angular's ng-hide. Using
|
||||
// `loading ? null : <ToolbarHeader/>` causes UI glitches
|
||||
return (
|
||||
|
@ -130,6 +138,7 @@ const EditorNavigationToolbarRoot = React.memo(
|
|||
isAnonymousUser={user == null}
|
||||
projectName={projectName}
|
||||
renameProject={renameProject}
|
||||
hasRenamePermissions={hasRenamePermissions}
|
||||
openShareModal={openShareModal}
|
||||
pdfViewIsOpen={view === 'pdf'}
|
||||
pdfButtonIsVisible={pdfLayout === 'flat'}
|
||||
|
|
|
@ -7,7 +7,7 @@ import Icon from '../../../shared/components/icon'
|
|||
|
||||
function ProjectNameEditableLabel({
|
||||
projectName,
|
||||
userIsAdmin,
|
||||
hasRenamePermissions,
|
||||
onChange,
|
||||
className,
|
||||
}) {
|
||||
|
@ -15,7 +15,7 @@ function ProjectNameEditableLabel({
|
|||
|
||||
const [isRenaming, setIsRenaming] = useState(false)
|
||||
|
||||
const canRename = userIsAdmin && !isRenaming
|
||||
const canRename = hasRenamePermissions && !isRenaming
|
||||
|
||||
const [inputContent, setInputContent] = useState(projectName)
|
||||
|
||||
|
@ -28,8 +28,10 @@ function ProjectNameEditableLabel({
|
|||
}, [isRenaming])
|
||||
|
||||
function startRenaming() {
|
||||
setInputContent(projectName)
|
||||
setIsRenaming(true)
|
||||
if (canRename) {
|
||||
setInputContent(projectName)
|
||||
setIsRenaming(true)
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeyDown(event) {
|
||||
|
@ -84,7 +86,7 @@ function ProjectNameEditableLabel({
|
|||
|
||||
ProjectNameEditableLabel.propTypes = {
|
||||
projectName: PropTypes.string.isRequired,
|
||||
userIsAdmin: PropTypes.bool,
|
||||
hasRenamePermissions: PropTypes.bool,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
className: PropTypes.string,
|
||||
}
|
||||
|
|
|
@ -31,6 +31,7 @@ const ToolbarHeader = React.memo(function ToolbarHeader({
|
|||
isAnonymousUser,
|
||||
projectName,
|
||||
renameProject,
|
||||
hasRenamePermissions,
|
||||
openShareModal,
|
||||
pdfViewIsOpen,
|
||||
pdfButtonIsVisible,
|
||||
|
@ -56,7 +57,7 @@ const ToolbarHeader = React.memo(function ToolbarHeader({
|
|||
<ProjectNameEditableLabel
|
||||
className="toolbar-center"
|
||||
projectName={projectName}
|
||||
userIsAdmin
|
||||
hasRenamePermissions={hasRenamePermissions}
|
||||
onChange={renameProject}
|
||||
/>
|
||||
|
||||
|
@ -108,6 +109,7 @@ ToolbarHeader.propTypes = {
|
|||
isAnonymousUser: PropTypes.bool,
|
||||
projectName: PropTypes.string.isRequired,
|
||||
renameProject: PropTypes.func.isRequired,
|
||||
hasRenamePermissions: PropTypes.bool,
|
||||
openShareModal: PropTypes.func.isRequired,
|
||||
pdfViewIsOpen: PropTypes.bool,
|
||||
pdfButtonIsVisible: PropTypes.bool,
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import { useCallback, useEffect } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { useApplicationContext } from '../../../shared/context/application-context'
|
||||
import { useUserContext } from '../../../shared/context/user-context'
|
||||
import { useFileTreeMutable } from '../contexts/file-tree-mutable'
|
||||
import { useFileTreeSelectable } from '../contexts/file-tree-selectable'
|
||||
import { findInTreeOrThrow } from '../util/find-in-tree'
|
||||
|
||||
export function useFileTreeSocketListener() {
|
||||
const { user } = useApplicationContext({
|
||||
user: PropTypes.shape({ id: PropTypes.string.isRequired }),
|
||||
const user = useUserContext({
|
||||
id: PropTypes.string.isRequired,
|
||||
})
|
||||
const {
|
||||
dispatchRename,
|
||||
|
|
|
@ -3,7 +3,7 @@ import { Trans, useTranslation } from 'react-i18next'
|
|||
import { Button } from 'react-bootstrap'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { useApplicationContext } from '../../../shared/context/application-context'
|
||||
import { useUserContext } from '../../../shared/context/user-context'
|
||||
|
||||
import Icon from '../../../shared/components/icon'
|
||||
import { upgradePlan } from '../../../main/account-upgrade'
|
||||
|
@ -11,8 +11,8 @@ import StartFreeTrialButton from '../../../shared/components/start-free-trial-bu
|
|||
|
||||
export default function AddCollaboratorsUpgrade() {
|
||||
const { t } = useTranslation()
|
||||
const { user } = useApplicationContext({
|
||||
user: PropTypes.shape({ allowedFreeTrial: PropTypes.boolean }),
|
||||
const user = useUserContext({
|
||||
allowedFreeTrial: PropTypes.bool,
|
||||
})
|
||||
|
||||
const [startedFreeTrial, setStartedFreeTrial] = useState(false)
|
||||
|
|
|
@ -1,50 +0,0 @@
|
|||
import { createContext, useContext, useMemo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
export const ApplicationContext = createContext()
|
||||
|
||||
ApplicationContext.Provider.propTypes = {
|
||||
value: PropTypes.shape({
|
||||
user: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
firstName: PropTypes.string,
|
||||
lastName: PropTypes.string,
|
||||
}),
|
||||
gitBridgePublicBaseUrl: PropTypes.string.isRequired,
|
||||
}),
|
||||
}
|
||||
|
||||
export function ApplicationProvider({ children }) {
|
||||
const value = useMemo(() => {
|
||||
const value = {
|
||||
gitBridgePublicBaseUrl: window.gitBridgePublicBaseUrl,
|
||||
}
|
||||
|
||||
if (window.user.id) {
|
||||
value.user = window.user
|
||||
}
|
||||
|
||||
return value
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<ApplicationContext.Provider value={value}>
|
||||
{children}
|
||||
</ApplicationContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
ApplicationProvider.propTypes = {
|
||||
children: PropTypes.any,
|
||||
}
|
||||
|
||||
export function useApplicationContext(propTypes) {
|
||||
const data = useContext(ApplicationContext)
|
||||
PropTypes.checkPropTypes(
|
||||
propTypes,
|
||||
data,
|
||||
'data',
|
||||
'ApplicationContext.Provider'
|
||||
)
|
||||
return data
|
||||
}
|
|
@ -34,6 +34,7 @@ EditorContext.Provider.propTypes = {
|
|||
rootFolder: PropTypes.shape({
|
||||
children: PropTypes.arrayOf(PropTypes.shape({ type: PropTypes.string })),
|
||||
}),
|
||||
permissionsLevel: PropTypes.oneOf(['readOnly', 'readAndWrite', 'owner']),
|
||||
}),
|
||||
}
|
||||
|
||||
|
@ -70,6 +71,15 @@ export function EditorProvider({ children, settings }) {
|
|||
const [loading] = useScopeValue('state.loading')
|
||||
const [projectName, setProjectName] = useScopeValue('project.name')
|
||||
const [rootFolder] = useScopeValue('rootFolder')
|
||||
const [permissionsLevel] = useScopeValue('permissionsLevel')
|
||||
|
||||
useEffect(() => {
|
||||
if (ide?.socket) {
|
||||
ide.socket.on('projectNameUpdated', setProjectName)
|
||||
return () =>
|
||||
ide.socket.removeListener('projectNameUpdated', setProjectName)
|
||||
}
|
||||
}, [ide?.socket, setProjectName])
|
||||
|
||||
const renameProject = useCallback(
|
||||
newName => {
|
||||
|
@ -109,6 +119,7 @@ export function EditorProvider({ children, settings }) {
|
|||
hasPremiumCompile: features?.compileGroup === 'priority',
|
||||
loading,
|
||||
renameProject,
|
||||
permissionsLevel,
|
||||
isProjectOwner: owner?._id === window.user.id,
|
||||
isRestrictedTokenMember: window.isRestrictedTokenMember,
|
||||
rootFolder,
|
||||
|
@ -118,6 +129,7 @@ export function EditorProvider({ children, settings }) {
|
|||
features?.compileGroup,
|
||||
loading,
|
||||
renameProject,
|
||||
permissionsLevel,
|
||||
owner?._id,
|
||||
rootFolder,
|
||||
]
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import PropTypes from 'prop-types'
|
||||
import createSharedContext from 'react2angular-shared-context'
|
||||
|
||||
import { ApplicationProvider } from './application-context'
|
||||
import { UserProvider } from './user-context'
|
||||
import { IdeProvider } from './ide-context'
|
||||
import { EditorProvider } from './editor-context'
|
||||
import { CompileProvider } from './compile-context'
|
||||
|
@ -11,8 +11,8 @@ import { ProjectProvider } from './project-context'
|
|||
|
||||
export function ContextRoot({ children, ide, settings }) {
|
||||
return (
|
||||
<ApplicationProvider>
|
||||
<IdeProvider ide={ide}>
|
||||
<IdeProvider ide={ide}>
|
||||
<UserProvider>
|
||||
<ProjectProvider>
|
||||
<EditorProvider settings={settings}>
|
||||
<CompileProvider>
|
||||
|
@ -22,8 +22,8 @@ export function ContextRoot({ children, ide, settings }) {
|
|||
</CompileProvider>
|
||||
</EditorProvider>
|
||||
</ProjectProvider>
|
||||
</IdeProvider>
|
||||
</ApplicationProvider>
|
||||
</UserProvider>
|
||||
</IdeProvider>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
32
services/web/frontend/js/shared/context/user-context.js
Normal file
32
services/web/frontend/js/shared/context/user-context.js
Normal file
|
@ -0,0 +1,32 @@
|
|||
import { createContext, useContext } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import useScopeValue from './util/scope-value-hook'
|
||||
|
||||
export const UserContext = createContext()
|
||||
|
||||
UserContext.Provider.propTypes = {
|
||||
value: PropTypes.shape({
|
||||
user: PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
allowedFreeTrial: PropTypes.boolean,
|
||||
first_name: PropTypes.string,
|
||||
last_name: PropTypes.string,
|
||||
}),
|
||||
}),
|
||||
}
|
||||
|
||||
export function UserProvider({ children }) {
|
||||
const [user] = useScopeValue('user', true)
|
||||
|
||||
return <UserContext.Provider value={user}>{children}</UserContext.Provider>
|
||||
}
|
||||
|
||||
UserProvider.propTypes = {
|
||||
children: PropTypes.any,
|
||||
}
|
||||
|
||||
export function useUserContext(propTypes) {
|
||||
const data = useContext(UserContext)
|
||||
PropTypes.checkPropTypes(propTypes, data, 'data', 'UserContext.Provider')
|
||||
return data
|
||||
}
|
|
@ -10,6 +10,7 @@ export function setupContext() {
|
|||
if (window._ide) {
|
||||
$scope = {
|
||||
...window._ide.$scope,
|
||||
user: window.user,
|
||||
project: {},
|
||||
$watch: () => {},
|
||||
ui: {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import PreviewLogsPane from '../js/features/preview/components/preview-logs-pane'
|
||||
import { EditorProvider } from '../js/shared/context/editor-context'
|
||||
import { ApplicationProvider } from '../js/shared/context/application-context'
|
||||
import { UserProvider } from '../js/shared/context/user-context'
|
||||
import useFetchMock from './hooks/use-fetch-mock'
|
||||
import { IdeProvider } from '../js/shared/context/ide-context'
|
||||
|
||||
|
@ -24,13 +24,13 @@ export const TimedOutError = args => {
|
|||
}
|
||||
|
||||
return (
|
||||
<ApplicationProvider>
|
||||
<UserProvider>
|
||||
<IdeProvider ide={ide}>
|
||||
<EditorProvider settings={{}}>
|
||||
<PreviewLogsPane {...args} />
|
||||
</EditorProvider>
|
||||
</IdeProvider>
|
||||
</ApplicationProvider>
|
||||
</UserProvider>
|
||||
)
|
||||
}
|
||||
TimedOutError.args = {
|
||||
|
@ -59,13 +59,13 @@ export const TimedOutErrorWithPriorityCompile = args => {
|
|||
}
|
||||
|
||||
return (
|
||||
<ApplicationProvider>
|
||||
<UserProvider>
|
||||
<IdeProvider ide={ide}>
|
||||
<EditorProvider settings={{}}>
|
||||
<PreviewLogsPane {...args} />
|
||||
</EditorProvider>
|
||||
</IdeProvider>
|
||||
</ApplicationProvider>
|
||||
</UserProvider>
|
||||
)
|
||||
}
|
||||
TimedOutErrorWithPriorityCompile.args = {
|
||||
|
|
|
@ -13,7 +13,7 @@ describe('<ProjectNameEditableLabel />', function () {
|
|||
})
|
||||
|
||||
describe('when the name is editable', function () {
|
||||
const editableProps = { ...defaultProps, userIsAdmin: true }
|
||||
const editableProps = { ...defaultProps, hasRenamePermissions: true }
|
||||
|
||||
it('displays an editable input when the edit button is clicked', function () {
|
||||
render(<ProjectNameEditableLabel {...editableProps} />)
|
||||
|
@ -52,11 +52,17 @@ describe('<ProjectNameEditableLabel />', function () {
|
|||
})
|
||||
|
||||
describe('when the name is not editable', function () {
|
||||
const nonEditableProps = { userIsAdmin: false, ...defaultProps }
|
||||
const nonEditableProps = { hasRenamePermissions: false, ...defaultProps }
|
||||
|
||||
it('the edit button is not displayed', function () {
|
||||
render(<ProjectNameEditableLabel {...nonEditableProps} />)
|
||||
expect(screen.queryByRole('button')).to.not.exist
|
||||
})
|
||||
|
||||
it('does not display an editable input when the project name is double clicked', function () {
|
||||
render(<ProjectNameEditableLabel {...nonEditableProps} />)
|
||||
fireEvent.doubleClick(screen.getByText('test-project'))
|
||||
expect(screen.queryByRole('textbox')).to.not.exist
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
import { render } from '@testing-library/react'
|
||||
import sinon from 'sinon'
|
||||
import { ApplicationProvider } from '../../../frontend/js/shared/context/application-context'
|
||||
import { UserProvider } from '../../../frontend/js/shared/context/user-context'
|
||||
import { EditorProvider } from '../../../frontend/js/shared/context/editor-context'
|
||||
import { LayoutProvider } from '../../../frontend/js/shared/context/layout-context'
|
||||
import { ChatProvider } from '../../../frontend/js/features/chat/context/chat-context'
|
||||
|
@ -28,6 +28,7 @@ export function EditorProviders({
|
|||
window.isRestrictedTokenMember = isRestrictedTokenMember
|
||||
|
||||
const $scope = {
|
||||
user: window.user,
|
||||
project: {
|
||||
_id: window.project_id,
|
||||
name: 'project-name',
|
||||
|
@ -52,15 +53,15 @@ export function EditorProviders({
|
|||
window._ide = { $scope, socket }
|
||||
|
||||
return (
|
||||
<ApplicationProvider>
|
||||
<IdeProvider ide={window._ide}>
|
||||
<IdeProvider ide={window._ide}>
|
||||
<UserProvider>
|
||||
<ProjectProvider>
|
||||
<EditorProvider settings={{}}>
|
||||
<LayoutProvider>{children}</LayoutProvider>
|
||||
</EditorProvider>
|
||||
</ProjectProvider>
|
||||
</IdeProvider>
|
||||
</ApplicationProvider>
|
||||
</UserProvider>
|
||||
</IdeProvider>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue