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:
Miguel Serrano 2021-06-25 10:14:07 +02:00 committed by Copybot
parent b7802674d5
commit 9b59c0813c
15 changed files with 104 additions and 89 deletions

View file

@ -8,7 +8,7 @@ import InfiniteScroll from './infinite-scroll'
import ChatFallbackError from './chat-fallback-error' import ChatFallbackError from './chat-fallback-error'
import Icon from '../../../shared/components/icon' import Icon from '../../../shared/components/icon'
import { useLayoutContext } from '../../../shared/context/layout-context' 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 withErrorBoundary from '../../../infrastructure/error-boundary'
import { FetchError } from '../../../infrastructure/fetch-json' import { FetchError } from '../../../infrastructure/fetch-json'
import { useChatContext } from '../context/chat-context' import { useChatContext } from '../context/chat-context'
@ -17,8 +17,8 @@ const ChatPane = React.memo(function ChatPane() {
const { t } = useTranslation() const { t } = useTranslation()
const { chatIsOpen } = useLayoutContext({ chatIsOpen: PropTypes.bool }) const { chatIsOpen } = useLayoutContext({ chatIsOpen: PropTypes.bool })
const { user } = useApplicationContext({ const user = useUserContext({
user: PropTypes.shape({ id: PropTypes.string.isRequired }), id: PropTypes.string.isRequired,
}) })
const { const {

View file

@ -9,7 +9,7 @@ import {
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { v4 as uuid } from 'uuid' 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 { useProjectContext } from '../../../shared/context/project-context'
import { getJSON, postJSON } from '../../../infrastructure/fetch-json' import { getJSON, postJSON } from '../../../infrastructure/fetch-json'
import { appendMessage, prependMessages } from '../utils/message-list-appender' import { appendMessage, prependMessages } from '../utils/message-list-appender'
@ -117,8 +117,8 @@ ChatContext.Provider.propTypes = {
} }
export function ChatProvider({ children }) { export function ChatProvider({ children }) {
const { user } = useApplicationContext({ const user = useUserContext({
user: PropTypes.shape({ id: PropTypes.string.isRequired }), id: PropTypes.string.isRequired,
}) })
const { _id: projectId } = useProjectContext({ const { _id: projectId } = useProjectContext({
_id: PropTypes.string.isRequired, _id: PropTypes.string.isRequired,

View file

@ -4,11 +4,11 @@ import ToolbarHeader from './toolbar-header'
import { useEditorContext } from '../../../shared/context/editor-context' import { useEditorContext } from '../../../shared/context/editor-context'
import { useChatContext } from '../../chat/context/chat-context' import { useChatContext } from '../../chat/context/chat-context'
import { useLayoutContext } from '../../../shared/context/layout-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' import { useProjectContext } from '../../../shared/context/project-context'
const applicationContextPropTypes = { const userContextPropTypes = {
user: PropTypes.object, id: PropTypes.string,
} }
const projectContextPropTypes = { const projectContextPropTypes = {
@ -21,6 +21,7 @@ const editorContextPropTypes = {
isRestrictedTokenMember: PropTypes.bool, isRestrictedTokenMember: PropTypes.bool,
renameProject: PropTypes.func.isRequired, renameProject: PropTypes.func.isRequired,
isProjectOwner: PropTypes.bool, isProjectOwner: PropTypes.bool,
permissionsLevel: PropTypes.string,
} }
const layoutContextPropTypes = { const layoutContextPropTypes = {
@ -45,7 +46,7 @@ const EditorNavigationToolbarRoot = React.memo(
openDoc, openDoc,
openShareProjectModal, openShareProjectModal,
}) { }) {
const { user } = useApplicationContext(applicationContextPropTypes) const user = useUserContext(userContextPropTypes)
const { name: projectName } = useProjectContext(projectContextPropTypes) const { name: projectName } = useProjectContext(projectContextPropTypes)
@ -55,6 +56,7 @@ const EditorNavigationToolbarRoot = React.memo(
isRestrictedTokenMember, isRestrictedTokenMember,
renameProject, renameProject,
isProjectOwner, isProjectOwner,
permissionsLevel,
} = useEditorContext(editorContextPropTypes) } = useEditorContext(editorContextPropTypes)
const { const {
@ -110,6 +112,12 @@ const EditorNavigationToolbarRoot = React.memo(
[openDoc] [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 // 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 (
@ -130,6 +138,7 @@ const EditorNavigationToolbarRoot = React.memo(
isAnonymousUser={user == null} isAnonymousUser={user == null}
projectName={projectName} projectName={projectName}
renameProject={renameProject} renameProject={renameProject}
hasRenamePermissions={hasRenamePermissions}
openShareModal={openShareModal} openShareModal={openShareModal}
pdfViewIsOpen={view === 'pdf'} pdfViewIsOpen={view === 'pdf'}
pdfButtonIsVisible={pdfLayout === 'flat'} pdfButtonIsVisible={pdfLayout === 'flat'}

View file

@ -7,7 +7,7 @@ import Icon from '../../../shared/components/icon'
function ProjectNameEditableLabel({ function ProjectNameEditableLabel({
projectName, projectName,
userIsAdmin, hasRenamePermissions,
onChange, onChange,
className, className,
}) { }) {
@ -15,7 +15,7 @@ function ProjectNameEditableLabel({
const [isRenaming, setIsRenaming] = useState(false) const [isRenaming, setIsRenaming] = useState(false)
const canRename = userIsAdmin && !isRenaming const canRename = hasRenamePermissions && !isRenaming
const [inputContent, setInputContent] = useState(projectName) const [inputContent, setInputContent] = useState(projectName)
@ -28,9 +28,11 @@ function ProjectNameEditableLabel({
}, [isRenaming]) }, [isRenaming])
function startRenaming() { function startRenaming() {
if (canRename) {
setInputContent(projectName) setInputContent(projectName)
setIsRenaming(true) setIsRenaming(true)
} }
}
function handleKeyDown(event) { function handleKeyDown(event) {
if (event.key === 'Enter') { if (event.key === 'Enter') {
@ -84,7 +86,7 @@ function ProjectNameEditableLabel({
ProjectNameEditableLabel.propTypes = { ProjectNameEditableLabel.propTypes = {
projectName: PropTypes.string.isRequired, projectName: PropTypes.string.isRequired,
userIsAdmin: PropTypes.bool, hasRenamePermissions: PropTypes.bool,
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
className: PropTypes.string, className: PropTypes.string,
} }

View file

@ -31,6 +31,7 @@ const ToolbarHeader = React.memo(function ToolbarHeader({
isAnonymousUser, isAnonymousUser,
projectName, projectName,
renameProject, renameProject,
hasRenamePermissions,
openShareModal, openShareModal,
pdfViewIsOpen, pdfViewIsOpen,
pdfButtonIsVisible, pdfButtonIsVisible,
@ -56,7 +57,7 @@ const ToolbarHeader = React.memo(function ToolbarHeader({
<ProjectNameEditableLabel <ProjectNameEditableLabel
className="toolbar-center" className="toolbar-center"
projectName={projectName} projectName={projectName}
userIsAdmin hasRenamePermissions={hasRenamePermissions}
onChange={renameProject} onChange={renameProject}
/> />
@ -108,6 +109,7 @@ ToolbarHeader.propTypes = {
isAnonymousUser: PropTypes.bool, isAnonymousUser: PropTypes.bool,
projectName: PropTypes.string.isRequired, projectName: PropTypes.string.isRequired,
renameProject: PropTypes.func.isRequired, renameProject: PropTypes.func.isRequired,
hasRenamePermissions: PropTypes.bool,
openShareModal: PropTypes.func.isRequired, openShareModal: PropTypes.func.isRequired,
pdfViewIsOpen: PropTypes.bool, pdfViewIsOpen: PropTypes.bool,
pdfButtonIsVisible: PropTypes.bool, pdfButtonIsVisible: PropTypes.bool,

View file

@ -1,14 +1,14 @@
import { useCallback, useEffect } from 'react' import { useCallback, useEffect } from 'react'
import PropTypes from 'prop-types' 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 { useFileTreeMutable } from '../contexts/file-tree-mutable'
import { useFileTreeSelectable } from '../contexts/file-tree-selectable' import { useFileTreeSelectable } from '../contexts/file-tree-selectable'
import { findInTreeOrThrow } from '../util/find-in-tree' import { findInTreeOrThrow } from '../util/find-in-tree'
export function useFileTreeSocketListener() { export function useFileTreeSocketListener() {
const { user } = useApplicationContext({ const user = useUserContext({
user: PropTypes.shape({ id: PropTypes.string.isRequired }), id: PropTypes.string.isRequired,
}) })
const { const {
dispatchRename, dispatchRename,

View file

@ -3,7 +3,7 @@ import { Trans, useTranslation } from 'react-i18next'
import { Button } from 'react-bootstrap' import { Button } from 'react-bootstrap'
import PropTypes from 'prop-types' 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 Icon from '../../../shared/components/icon'
import { upgradePlan } from '../../../main/account-upgrade' import { upgradePlan } from '../../../main/account-upgrade'
@ -11,8 +11,8 @@ import StartFreeTrialButton from '../../../shared/components/start-free-trial-bu
export default function AddCollaboratorsUpgrade() { export default function AddCollaboratorsUpgrade() {
const { t } = useTranslation() const { t } = useTranslation()
const { user } = useApplicationContext({ const user = useUserContext({
user: PropTypes.shape({ allowedFreeTrial: PropTypes.boolean }), allowedFreeTrial: PropTypes.bool,
}) })
const [startedFreeTrial, setStartedFreeTrial] = useState(false) const [startedFreeTrial, setStartedFreeTrial] = useState(false)

View file

@ -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
}

View file

@ -34,6 +34,7 @@ EditorContext.Provider.propTypes = {
rootFolder: PropTypes.shape({ rootFolder: PropTypes.shape({
children: PropTypes.arrayOf(PropTypes.shape({ type: PropTypes.string })), 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 [loading] = useScopeValue('state.loading')
const [projectName, setProjectName] = useScopeValue('project.name') const [projectName, setProjectName] = useScopeValue('project.name')
const [rootFolder] = useScopeValue('rootFolder') 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( const renameProject = useCallback(
newName => { newName => {
@ -109,6 +119,7 @@ export function EditorProvider({ children, settings }) {
hasPremiumCompile: features?.compileGroup === 'priority', hasPremiumCompile: features?.compileGroup === 'priority',
loading, loading,
renameProject, renameProject,
permissionsLevel,
isProjectOwner: owner?._id === window.user.id, isProjectOwner: owner?._id === window.user.id,
isRestrictedTokenMember: window.isRestrictedTokenMember, isRestrictedTokenMember: window.isRestrictedTokenMember,
rootFolder, rootFolder,
@ -118,6 +129,7 @@ export function EditorProvider({ children, settings }) {
features?.compileGroup, features?.compileGroup,
loading, loading,
renameProject, renameProject,
permissionsLevel,
owner?._id, owner?._id,
rootFolder, rootFolder,
] ]

View file

@ -1,7 +1,7 @@
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import createSharedContext from 'react2angular-shared-context' import createSharedContext from 'react2angular-shared-context'
import { ApplicationProvider } from './application-context' import { UserProvider } from './user-context'
import { IdeProvider } from './ide-context' import { IdeProvider } from './ide-context'
import { EditorProvider } from './editor-context' import { EditorProvider } from './editor-context'
import { CompileProvider } from './compile-context' import { CompileProvider } from './compile-context'
@ -11,8 +11,8 @@ import { ProjectProvider } from './project-context'
export function ContextRoot({ children, ide, settings }) { export function ContextRoot({ children, ide, settings }) {
return ( return (
<ApplicationProvider>
<IdeProvider ide={ide}> <IdeProvider ide={ide}>
<UserProvider>
<ProjectProvider> <ProjectProvider>
<EditorProvider settings={settings}> <EditorProvider settings={settings}>
<CompileProvider> <CompileProvider>
@ -22,8 +22,8 @@ export function ContextRoot({ children, ide, settings }) {
</CompileProvider> </CompileProvider>
</EditorProvider> </EditorProvider>
</ProjectProvider> </ProjectProvider>
</UserProvider>
</IdeProvider> </IdeProvider>
</ApplicationProvider>
) )
} }

View 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
}

View file

@ -10,6 +10,7 @@ export function setupContext() {
if (window._ide) { if (window._ide) {
$scope = { $scope = {
...window._ide.$scope, ...window._ide.$scope,
user: window.user,
project: {}, project: {},
$watch: () => {}, $watch: () => {},
ui: { ui: {

View file

@ -1,6 +1,6 @@
import PreviewLogsPane from '../js/features/preview/components/preview-logs-pane' import PreviewLogsPane from '../js/features/preview/components/preview-logs-pane'
import { EditorProvider } from '../js/shared/context/editor-context' 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 useFetchMock from './hooks/use-fetch-mock'
import { IdeProvider } from '../js/shared/context/ide-context' import { IdeProvider } from '../js/shared/context/ide-context'
@ -24,13 +24,13 @@ export const TimedOutError = args => {
} }
return ( return (
<ApplicationProvider> <UserProvider>
<IdeProvider ide={ide}> <IdeProvider ide={ide}>
<EditorProvider settings={{}}> <EditorProvider settings={{}}>
<PreviewLogsPane {...args} /> <PreviewLogsPane {...args} />
</EditorProvider> </EditorProvider>
</IdeProvider> </IdeProvider>
</ApplicationProvider> </UserProvider>
) )
} }
TimedOutError.args = { TimedOutError.args = {
@ -59,13 +59,13 @@ export const TimedOutErrorWithPriorityCompile = args => {
} }
return ( return (
<ApplicationProvider> <UserProvider>
<IdeProvider ide={ide}> <IdeProvider ide={ide}>
<EditorProvider settings={{}}> <EditorProvider settings={{}}>
<PreviewLogsPane {...args} /> <PreviewLogsPane {...args} />
</EditorProvider> </EditorProvider>
</IdeProvider> </IdeProvider>
</ApplicationProvider> </UserProvider>
) )
} }
TimedOutErrorWithPriorityCompile.args = { TimedOutErrorWithPriorityCompile.args = {

View file

@ -13,7 +13,7 @@ describe('<ProjectNameEditableLabel />', function () {
}) })
describe('when the name is editable', 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 () { it('displays an editable input when the edit button is clicked', function () {
render(<ProjectNameEditableLabel {...editableProps} />) render(<ProjectNameEditableLabel {...editableProps} />)
@ -52,11 +52,17 @@ describe('<ProjectNameEditableLabel />', function () {
}) })
describe('when the name is not editable', 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 () { it('the edit button is not displayed', function () {
render(<ProjectNameEditableLabel {...nonEditableProps} />) render(<ProjectNameEditableLabel {...nonEditableProps} />)
expect(screen.queryByRole('button')).to.not.exist 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
})
}) })
}) })

View file

@ -3,7 +3,7 @@
import { render } from '@testing-library/react' import { render } from '@testing-library/react'
import sinon from 'sinon' 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 { EditorProvider } from '../../../frontend/js/shared/context/editor-context'
import { LayoutProvider } from '../../../frontend/js/shared/context/layout-context' import { LayoutProvider } from '../../../frontend/js/shared/context/layout-context'
import { ChatProvider } from '../../../frontend/js/features/chat/context/chat-context' import { ChatProvider } from '../../../frontend/js/features/chat/context/chat-context'
@ -28,6 +28,7 @@ export function EditorProviders({
window.isRestrictedTokenMember = isRestrictedTokenMember window.isRestrictedTokenMember = isRestrictedTokenMember
const $scope = { const $scope = {
user: window.user,
project: { project: {
_id: window.project_id, _id: window.project_id,
name: 'project-name', name: 'project-name',
@ -52,15 +53,15 @@ export function EditorProviders({
window._ide = { $scope, socket } window._ide = { $scope, socket }
return ( return (
<ApplicationProvider>
<IdeProvider ide={window._ide}> <IdeProvider ide={window._ide}>
<UserProvider>
<ProjectProvider> <ProjectProvider>
<EditorProvider settings={{}}> <EditorProvider settings={{}}>
<LayoutProvider>{children}</LayoutProvider> <LayoutProvider>{children}</LayoutProvider>
</EditorProvider> </EditorProvider>
</ProjectProvider> </ProjectProvider>
</UserProvider>
</IdeProvider> </IdeProvider>
</ApplicationProvider>
) )
} }