mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-14 14:13:43 +01:00
[web] open comments and changes via url (#36546)
* [web] open a comment from a URL * [web] add "View comment" button to digest email * [web] open a tracked change from a URL * [web] add "View suggestion" button to digest email * [web] open the review panel from an open=review-panel URL * [web] drop the digest email CTA and collapse the deep links to t=review * [web] add an Open project button to the section overflow row and plain-text view links * [web] validate the comment and change deep-link ids as object ids * [web] collapse the deep-link resolvers into a single DeepLink component * fix: scrolling into view when below the fold * fix: address claude review * fix: style adjustments * fix: updating types to accomodate accepted/rejected changes --------- Co-authored-by: Jimmy Domagala-Tang <jimmy.domagala-tang@overleaf.com> GitOrigin-RevId: dab535591a93e1c3849a6b2a0dcc3e3934354385
This commit is contained in:
committed by
Copybot
co-authored by
Jimmy Domagala-Tang
parent
5dc8c2b0c3
commit
44910202f3
@@ -0,0 +1,153 @@
|
||||
import {
|
||||
createContext,
|
||||
FC,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { ThreadId } from '../../../../../types/review-panel/review-panel'
|
||||
import { showCommentNotFoundToast } from '@/features/ide-react/components/deep-link-toasts'
|
||||
|
||||
const DEEP_LINK_PARAMS = ['doc', 'comment', 'change', 'open'] as const
|
||||
|
||||
// the `open` param names either an editor panel or a settings tab, so it
|
||||
// resolves into one of two fields and the other stays null
|
||||
type DeepLinkPanel = 'review-panel'
|
||||
type DeepLinkSettings = 'project-notifications'
|
||||
|
||||
type DeepLinkParams = {
|
||||
docId: string | null
|
||||
threadId: ThreadId | null
|
||||
changeId: string | null
|
||||
panel: DeepLinkPanel | null
|
||||
settings: DeepLinkSettings | null
|
||||
}
|
||||
|
||||
type DeepLinkContextValue = {
|
||||
deepLinkedDocId: string | null
|
||||
deepLinkedThreadId: ThreadId | null
|
||||
deepLinkedChangeId: string | null
|
||||
deepLinkedPanel: DeepLinkPanel | null
|
||||
deepLinkedSettings: DeepLinkSettings | null
|
||||
clearDeepLinkedThread: () => void
|
||||
clearDeepLinkedChange: () => void
|
||||
reportDeepLinkedThreadNotFound: () => void
|
||||
}
|
||||
|
||||
const DeepLinkContext = createContext<DeepLinkContextValue | undefined>(
|
||||
undefined
|
||||
)
|
||||
|
||||
function readPanel(value: string | null): DeepLinkPanel | null {
|
||||
if (value === 'review-panel') {
|
||||
return value
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function readSettings(value: string | null): DeepLinkSettings | null {
|
||||
if (value === 'project-notifications') {
|
||||
return value
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function readDeepLinkParams(): DeepLinkParams {
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
const open = params.get('open')
|
||||
|
||||
return {
|
||||
docId: params.get('doc'),
|
||||
threadId: params.get('comment') as ThreadId | null,
|
||||
changeId: params.get('change'),
|
||||
panel: readPanel(open),
|
||||
settings: readSettings(open),
|
||||
}
|
||||
}
|
||||
|
||||
function stripDeepLinkParams() {
|
||||
const url = new URL(window.location.href)
|
||||
let changed = false
|
||||
|
||||
for (const param of DEEP_LINK_PARAMS) {
|
||||
if (url.searchParams.has(param)) {
|
||||
url.searchParams.delete(param)
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
window.history.replaceState(window.history.state, '', url.toString())
|
||||
}
|
||||
}
|
||||
|
||||
export const DeepLinkProvider: FC<React.PropsWithChildren> = ({ children }) => {
|
||||
const paramsRef = useRef<DeepLinkParams | null>(null)
|
||||
if (paramsRef.current === null) {
|
||||
paramsRef.current = readDeepLinkParams()
|
||||
}
|
||||
|
||||
const { docId, panel, settings } = paramsRef.current
|
||||
const [threadId, setThreadId] = useState(paramsRef.current.threadId)
|
||||
const [changeId, setChangeId] = useState(paramsRef.current.changeId)
|
||||
|
||||
useEffect(() => {
|
||||
stripDeepLinkParams()
|
||||
}, [])
|
||||
|
||||
const clearDeepLinkedThread = useCallback(() => setThreadId(null), [])
|
||||
const clearDeepLinkedChange = useCallback(() => setChangeId(null), [])
|
||||
|
||||
// several places give up on the comment at different points, so keep the
|
||||
// toast and the clear together in one call rather than pairing them by hand
|
||||
const reportDeepLinkedThreadNotFound = useCallback(() => {
|
||||
showCommentNotFoundToast()
|
||||
setThreadId(null)
|
||||
}, [])
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
deepLinkedDocId: docId,
|
||||
deepLinkedThreadId: threadId,
|
||||
deepLinkedChangeId: changeId,
|
||||
deepLinkedPanel: panel,
|
||||
deepLinkedSettings: settings,
|
||||
clearDeepLinkedThread,
|
||||
clearDeepLinkedChange,
|
||||
reportDeepLinkedThreadNotFound,
|
||||
}),
|
||||
[
|
||||
docId,
|
||||
threadId,
|
||||
changeId,
|
||||
panel,
|
||||
settings,
|
||||
clearDeepLinkedThread,
|
||||
clearDeepLinkedChange,
|
||||
reportDeepLinkedThreadNotFound,
|
||||
]
|
||||
)
|
||||
|
||||
return (
|
||||
<DeepLinkContext.Provider value={value}>
|
||||
{children}
|
||||
</DeepLinkContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useDeepLinkContext(): DeepLinkContextValue {
|
||||
const context = useContext(DeepLinkContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
'useDeepLinkContext is only available inside DeepLinkProvider'
|
||||
)
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
@@ -39,6 +39,7 @@ import { convertFileRefToBinaryFile } from '@/features/ide-react/util/file-view'
|
||||
import { useEditorOpenDocContext } from '@/features/ide-react/context/editor-open-doc-context'
|
||||
import { useEditorPropertiesContext } from '@/features/ide-react/context/editor-properties-context'
|
||||
import { showConnectionRestoredToast } from '@/features/ide-react/components/connection-restored-toast'
|
||||
import { useDeepLinkContext } from '@/features/ide-react/context/deep-link-context'
|
||||
|
||||
export interface GotoOffsetOptions {
|
||||
gotoOffset: number
|
||||
@@ -119,6 +120,12 @@ export const EditorManagerProvider: FC<React.PropsWithChildren> = ({
|
||||
wantTrackChangesRef.current = wantTrackChanges
|
||||
}, [wantTrackChanges])
|
||||
|
||||
const {
|
||||
deepLinkedDocId,
|
||||
deepLinkedThreadId,
|
||||
reportDeepLinkedThreadNotFound,
|
||||
} = useDeepLinkContext()
|
||||
|
||||
const { fileTreeData } = useFileTreeData()
|
||||
const fileTreeDataRef = useRef(fileTreeData)
|
||||
useEffect(() => {
|
||||
@@ -527,13 +534,34 @@ export const EditorManagerProvider: FC<React.PropsWithChildren> = ({
|
||||
|
||||
const openInitialDoc = useCallback(
|
||||
async (fallbackDocId?: string) => {
|
||||
if (deepLinkedDocId) {
|
||||
const deepLinkedDoc = findDocEntityById(fileTreeData, deepLinkedDocId)
|
||||
if (deepLinkedDoc) {
|
||||
return await openDoc(deepLinkedDoc)
|
||||
}
|
||||
|
||||
// the deep link points at a doc that is no longer in the project, so
|
||||
// any comment it referenced is unreachable
|
||||
if (deepLinkedThreadId) {
|
||||
reportDeepLinkedThreadNotFound()
|
||||
}
|
||||
}
|
||||
|
||||
const docId =
|
||||
customLocalStorage.getItem(currentDocumentIdStorageKey) || fallbackDocId
|
||||
if (docId) {
|
||||
return await openDocWithId(docId)
|
||||
}
|
||||
},
|
||||
[currentDocumentIdStorageKey, openDocWithId]
|
||||
[
|
||||
currentDocumentIdStorageKey,
|
||||
deepLinkedDocId,
|
||||
deepLinkedThreadId,
|
||||
fileTreeData,
|
||||
openDoc,
|
||||
openDocWithId,
|
||||
reportDeepLinkedThreadNotFound,
|
||||
]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { ElementType, FC, PropsWithChildren } from 'react'
|
||||
import { ChatProvider } from '@/features/chat/context/chat-context'
|
||||
import { ChangesUsersProvider } from '@/shared/context/changes-users-context'
|
||||
import { ConnectionProvider } from './connection-context'
|
||||
import { DeepLinkProvider } from '@/features/ide-react/context/deep-link-context'
|
||||
import { DetachCompileProvider } from '@/shared/context/detach-compile-context'
|
||||
import { DetachProvider } from '@/shared/context/detach-context'
|
||||
import { EditorManagerProvider } from '@/features/ide-react/context/editor-manager-context'
|
||||
@@ -50,6 +51,7 @@ export const ReactContextRoot: FC<
|
||||
ChatProvider,
|
||||
ChangesUsersProvider,
|
||||
ConnectionProvider,
|
||||
DeepLinkProvider,
|
||||
DetachCompileProvider,
|
||||
DetachProvider,
|
||||
EditorManagerProvider,
|
||||
@@ -98,78 +100,80 @@ export const ReactContextRoot: FC<
|
||||
)
|
||||
|
||||
return (
|
||||
<Providers.SplitTestProvider>
|
||||
<Providers.ModalsContextProvider>
|
||||
<Providers.ConnectionProvider>
|
||||
<Providers.ProjectProvider>
|
||||
<Providers.UserSettingsProvider>
|
||||
<Providers.IdeReactProvider>
|
||||
<Providers.UserProvider>
|
||||
<Providers.SnapshotProvider>
|
||||
<Providers.DetachProvider>
|
||||
<Providers.EditorOpenDocProvider>
|
||||
<Providers.EditorViewProvider>
|
||||
<Providers.EditorPropertiesProvider>
|
||||
<Providers.EditorProvider>
|
||||
<Providers.TutorialProvider>
|
||||
<Providers.FileTreeDataProvider>
|
||||
<Providers.FileTreePathProvider>
|
||||
<Providers.UserFeaturesProvider>
|
||||
<Providers.PermissionsProvider>
|
||||
<Providers.RailProvider>
|
||||
<Providers.LayoutProvider>
|
||||
<Providers.ProjectSettingsProvider>
|
||||
<Providers.EditorManagerProvider>
|
||||
<Providers.UnsavedDocsProvider>
|
||||
<Providers.ReferencesProvider>
|
||||
<Providers.LocalCompileProvider>
|
||||
<Providers.DetachCompileProvider>
|
||||
<Providers.ChatProvider>
|
||||
<Providers.FileTreeOpenProvider>
|
||||
<Providers.TabsProvider>
|
||||
<Providers.OnlineUsersProvider>
|
||||
<Providers.MetadataProvider>
|
||||
<Providers.OutlineProvider>
|
||||
<Providers.CommandRegistryProvider>
|
||||
<Providers.EditorSelectionProvider>
|
||||
<Providers.ChangesUsersProvider>
|
||||
{
|
||||
childrenWrappedWithDynamicProviders
|
||||
}
|
||||
</Providers.ChangesUsersProvider>
|
||||
</Providers.EditorSelectionProvider>
|
||||
</Providers.CommandRegistryProvider>
|
||||
</Providers.OutlineProvider>
|
||||
</Providers.MetadataProvider>
|
||||
</Providers.OnlineUsersProvider>
|
||||
</Providers.TabsProvider>
|
||||
</Providers.FileTreeOpenProvider>
|
||||
</Providers.ChatProvider>
|
||||
</Providers.DetachCompileProvider>
|
||||
</Providers.LocalCompileProvider>
|
||||
</Providers.ReferencesProvider>
|
||||
</Providers.UnsavedDocsProvider>
|
||||
</Providers.EditorManagerProvider>
|
||||
</Providers.ProjectSettingsProvider>
|
||||
</Providers.LayoutProvider>
|
||||
</Providers.RailProvider>
|
||||
</Providers.PermissionsProvider>
|
||||
</Providers.UserFeaturesProvider>
|
||||
</Providers.FileTreePathProvider>
|
||||
</Providers.FileTreeDataProvider>
|
||||
</Providers.TutorialProvider>
|
||||
</Providers.EditorProvider>
|
||||
</Providers.EditorPropertiesProvider>
|
||||
</Providers.EditorViewProvider>
|
||||
</Providers.EditorOpenDocProvider>
|
||||
</Providers.DetachProvider>
|
||||
</Providers.SnapshotProvider>
|
||||
</Providers.UserProvider>
|
||||
</Providers.IdeReactProvider>
|
||||
</Providers.UserSettingsProvider>
|
||||
</Providers.ProjectProvider>
|
||||
</Providers.ConnectionProvider>
|
||||
</Providers.ModalsContextProvider>
|
||||
</Providers.SplitTestProvider>
|
||||
<Providers.DeepLinkProvider>
|
||||
<Providers.SplitTestProvider>
|
||||
<Providers.ModalsContextProvider>
|
||||
<Providers.ConnectionProvider>
|
||||
<Providers.ProjectProvider>
|
||||
<Providers.UserSettingsProvider>
|
||||
<Providers.IdeReactProvider>
|
||||
<Providers.UserProvider>
|
||||
<Providers.SnapshotProvider>
|
||||
<Providers.DetachProvider>
|
||||
<Providers.EditorOpenDocProvider>
|
||||
<Providers.EditorViewProvider>
|
||||
<Providers.EditorPropertiesProvider>
|
||||
<Providers.EditorProvider>
|
||||
<Providers.TutorialProvider>
|
||||
<Providers.FileTreeDataProvider>
|
||||
<Providers.FileTreePathProvider>
|
||||
<Providers.UserFeaturesProvider>
|
||||
<Providers.PermissionsProvider>
|
||||
<Providers.RailProvider>
|
||||
<Providers.LayoutProvider>
|
||||
<Providers.ProjectSettingsProvider>
|
||||
<Providers.EditorManagerProvider>
|
||||
<Providers.UnsavedDocsProvider>
|
||||
<Providers.ReferencesProvider>
|
||||
<Providers.LocalCompileProvider>
|
||||
<Providers.DetachCompileProvider>
|
||||
<Providers.ChatProvider>
|
||||
<Providers.FileTreeOpenProvider>
|
||||
<Providers.TabsProvider>
|
||||
<Providers.OnlineUsersProvider>
|
||||
<Providers.MetadataProvider>
|
||||
<Providers.OutlineProvider>
|
||||
<Providers.CommandRegistryProvider>
|
||||
<Providers.EditorSelectionProvider>
|
||||
<Providers.ChangesUsersProvider>
|
||||
{
|
||||
childrenWrappedWithDynamicProviders
|
||||
}
|
||||
</Providers.ChangesUsersProvider>
|
||||
</Providers.EditorSelectionProvider>
|
||||
</Providers.CommandRegistryProvider>
|
||||
</Providers.OutlineProvider>
|
||||
</Providers.MetadataProvider>
|
||||
</Providers.OnlineUsersProvider>
|
||||
</Providers.TabsProvider>
|
||||
</Providers.FileTreeOpenProvider>
|
||||
</Providers.ChatProvider>
|
||||
</Providers.DetachCompileProvider>
|
||||
</Providers.LocalCompileProvider>
|
||||
</Providers.ReferencesProvider>
|
||||
</Providers.UnsavedDocsProvider>
|
||||
</Providers.EditorManagerProvider>
|
||||
</Providers.ProjectSettingsProvider>
|
||||
</Providers.LayoutProvider>
|
||||
</Providers.RailProvider>
|
||||
</Providers.PermissionsProvider>
|
||||
</Providers.UserFeaturesProvider>
|
||||
</Providers.FileTreePathProvider>
|
||||
</Providers.FileTreeDataProvider>
|
||||
</Providers.TutorialProvider>
|
||||
</Providers.EditorProvider>
|
||||
</Providers.EditorPropertiesProvider>
|
||||
</Providers.EditorViewProvider>
|
||||
</Providers.EditorOpenDocProvider>
|
||||
</Providers.DetachProvider>
|
||||
</Providers.SnapshotProvider>
|
||||
</Providers.UserProvider>
|
||||
</Providers.IdeReactProvider>
|
||||
</Providers.UserSettingsProvider>
|
||||
</Providers.ProjectProvider>
|
||||
</Providers.ConnectionProvider>
|
||||
</Providers.ModalsContextProvider>
|
||||
</Providers.SplitTestProvider>
|
||||
</Providers.DeepLinkProvider>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user