mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-12 21:23:45 +01:00
* [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
154 lines
3.8 KiB
TypeScript
154 lines
3.8 KiB
TypeScript
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
|
|
}
|