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 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 {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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({
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,
]

View file

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

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) {
$scope = {
...window._ide.$scope,
user: window.user,
project: {},
$watch: () => {},
ui: {

View file

@ -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 = {

View file

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

View file

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