diff --git a/services/web/frontend/js/features/chat/components/chat-pane.js b/services/web/frontend/js/features/chat/components/chat-pane.js
index a170c5fbc3..215af12117 100644
--- a/services/web/frontend/js/features/chat/components/chat-pane.js
+++ b/services/web/frontend/js/features/chat/components/chat-pane.js
@@ -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 {
diff --git a/services/web/frontend/js/features/chat/context/chat-context.js b/services/web/frontend/js/features/chat/context/chat-context.js
index 78f578fe3e..76d178a50a 100644
--- a/services/web/frontend/js/features/chat/context/chat-context.js
+++ b/services/web/frontend/js/features/chat/context/chat-context.js
@@ -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,
diff --git a/services/web/frontend/js/features/editor-navigation-toolbar/components/editor-navigation-toolbar-root.js b/services/web/frontend/js/features/editor-navigation-toolbar/components/editor-navigation-toolbar-root.js
index aa91caa5be..fb33ddbf29 100644
--- a/services/web/frontend/js/features/editor-navigation-toolbar/components/editor-navigation-toolbar-root.js
+++ b/services/web/frontend/js/features/editor-navigation-toolbar/components/editor-navigation-toolbar-root.js
@@ -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 : ` 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'}
diff --git a/services/web/frontend/js/features/editor-navigation-toolbar/components/project-name-editable-label.js b/services/web/frontend/js/features/editor-navigation-toolbar/components/project-name-editable-label.js
index 24f4e60859..54b457830b 100644
--- a/services/web/frontend/js/features/editor-navigation-toolbar/components/project-name-editable-label.js
+++ b/services/web/frontend/js/features/editor-navigation-toolbar/components/project-name-editable-label.js
@@ -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,
}
diff --git a/services/web/frontend/js/features/editor-navigation-toolbar/components/toolbar-header.js b/services/web/frontend/js/features/editor-navigation-toolbar/components/toolbar-header.js
index c152a2fd6b..fc6f45c453 100644
--- a/services/web/frontend/js/features/editor-navigation-toolbar/components/toolbar-header.js
+++ b/services/web/frontend/js/features/editor-navigation-toolbar/components/toolbar-header.js
@@ -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({
@@ -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,
diff --git a/services/web/frontend/js/features/file-tree/hooks/file-tree-socket-listener.js b/services/web/frontend/js/features/file-tree/hooks/file-tree-socket-listener.js
index db57703300..daf335cf4c 100644
--- a/services/web/frontend/js/features/file-tree/hooks/file-tree-socket-listener.js
+++ b/services/web/frontend/js/features/file-tree/hooks/file-tree-socket-listener.js
@@ -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,
diff --git a/services/web/frontend/js/features/share-project-modal/components/add-collaborators-upgrade.js b/services/web/frontend/js/features/share-project-modal/components/add-collaborators-upgrade.js
index 3f6858cc5f..cd6a96808b 100644
--- a/services/web/frontend/js/features/share-project-modal/components/add-collaborators-upgrade.js
+++ b/services/web/frontend/js/features/share-project-modal/components/add-collaborators-upgrade.js
@@ -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)
diff --git a/services/web/frontend/js/shared/context/application-context.js b/services/web/frontend/js/shared/context/application-context.js
deleted file mode 100644
index 627f8792d7..0000000000
--- a/services/web/frontend/js/shared/context/application-context.js
+++ /dev/null
@@ -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 (
-
- {children}
-
- )
-}
-
-ApplicationProvider.propTypes = {
- children: PropTypes.any,
-}
-
-export function useApplicationContext(propTypes) {
- const data = useContext(ApplicationContext)
- PropTypes.checkPropTypes(
- propTypes,
- data,
- 'data',
- 'ApplicationContext.Provider'
- )
- return data
-}
diff --git a/services/web/frontend/js/shared/context/editor-context.js b/services/web/frontend/js/shared/context/editor-context.js
index c4870806ea..ef11a5a7d6 100644
--- a/services/web/frontend/js/shared/context/editor-context.js
+++ b/services/web/frontend/js/shared/context/editor-context.js
@@ -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,
]
diff --git a/services/web/frontend/js/shared/context/root-context.js b/services/web/frontend/js/shared/context/root-context.js
index e72211b8ef..d62ebd182c 100644
--- a/services/web/frontend/js/shared/context/root-context.js
+++ b/services/web/frontend/js/shared/context/root-context.js
@@ -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 (
-
-
+
+
@@ -22,8 +22,8 @@ export function ContextRoot({ children, ide, settings }) {
-
-
+
+
)
}
diff --git a/services/web/frontend/js/shared/context/user-context.js b/services/web/frontend/js/shared/context/user-context.js
new file mode 100644
index 0000000000..a122078216
--- /dev/null
+++ b/services/web/frontend/js/shared/context/user-context.js
@@ -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 {children}
+}
+
+UserProvider.propTypes = {
+ children: PropTypes.any,
+}
+
+export function useUserContext(propTypes) {
+ const data = useContext(UserContext)
+ PropTypes.checkPropTypes(propTypes, data, 'data', 'UserContext.Provider')
+ return data
+}
diff --git a/services/web/frontend/stories/fixtures/context.js b/services/web/frontend/stories/fixtures/context.js
index abd6c2f494..162599a2c0 100644
--- a/services/web/frontend/stories/fixtures/context.js
+++ b/services/web/frontend/stories/fixtures/context.js
@@ -10,6 +10,7 @@ export function setupContext() {
if (window._ide) {
$scope = {
...window._ide.$scope,
+ user: window.user,
project: {},
$watch: () => {},
ui: {
diff --git a/services/web/frontend/stories/preview-logs-pane.stories.js b/services/web/frontend/stories/preview-logs-pane.stories.js
index 767cc3edff..80551f01d6 100644
--- a/services/web/frontend/stories/preview-logs-pane.stories.js
+++ b/services/web/frontend/stories/preview-logs-pane.stories.js
@@ -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 (
-
+
-
+
)
}
TimedOutError.args = {
@@ -59,13 +59,13 @@ export const TimedOutErrorWithPriorityCompile = args => {
}
return (
-
+
-
+
)
}
TimedOutErrorWithPriorityCompile.args = {
diff --git a/services/web/test/frontend/features/editor-navigation-toolbar/components/project-name-editable-label.test.js b/services/web/test/frontend/features/editor-navigation-toolbar/components/project-name-editable-label.test.js
index d86060302b..d172ac56e9 100644
--- a/services/web/test/frontend/features/editor-navigation-toolbar/components/project-name-editable-label.test.js
+++ b/services/web/test/frontend/features/editor-navigation-toolbar/components/project-name-editable-label.test.js
@@ -13,7 +13,7 @@ describe('', 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()
@@ -52,11 +52,17 @@ describe('', 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()
expect(screen.queryByRole('button')).to.not.exist
})
+
+ it('does not display an editable input when the project name is double clicked', function () {
+ render()
+ fireEvent.doubleClick(screen.getByText('test-project'))
+ expect(screen.queryByRole('textbox')).to.not.exist
+ })
})
})
diff --git a/services/web/test/frontend/helpers/render-with-context.js b/services/web/test/frontend/helpers/render-with-context.js
index 1b04e78285..2e9930be7e 100644
--- a/services/web/test/frontend/helpers/render-with-context.js
+++ b/services/web/test/frontend/helpers/render-with-context.js
@@ -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 (
-
-
+
+
{children}
-
-
+
+
)
}