first commit
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useCodeMirrorViewContext } from '@/features/source-editor/components/codemirror-editor'
|
||||
import {
|
||||
Change,
|
||||
CommentOperation,
|
||||
EditOperation,
|
||||
} from '../../../../../types/change'
|
||||
import { DecorationSet, EditorView } from '@codemirror/view'
|
||||
import { EditorSelection } from '@codemirror/state'
|
||||
import _ from 'lodash'
|
||||
import useReviewPanelLayout from './use-review-panel-layout'
|
||||
|
||||
const useMoreCommments = (
|
||||
changes: Change<EditOperation>[],
|
||||
comments: Change<CommentOperation>[],
|
||||
newComments: DecorationSet
|
||||
): {
|
||||
onEntriesPositioned: () => void
|
||||
onMoreCommentsAboveClick: null | (() => void)
|
||||
onMoreCommentsBelowClick: null | (() => void)
|
||||
} => {
|
||||
const view = useCodeMirrorViewContext()
|
||||
const { showPanel, mini } = useReviewPanelLayout()
|
||||
const reviewPanelOpen = showPanel && !mini
|
||||
|
||||
const [positionAbove, setPositionAbove] = useState<number | null>(null)
|
||||
const [positionBelow, setPositionBelow] = useState<number | null>(null)
|
||||
|
||||
const updateEntryPositions = useMemo(
|
||||
() =>
|
||||
_.debounce(
|
||||
() =>
|
||||
view.requestMeasure({
|
||||
key: 'review-panel-more-comments',
|
||||
read(view) {
|
||||
const container = view.scrollDOM
|
||||
|
||||
if (!container || !reviewPanelOpen) {
|
||||
return { positionAbove: null, positionBelow: null }
|
||||
}
|
||||
|
||||
const containerTop = container.scrollTop
|
||||
const containerBottom = containerTop + container.clientHeight
|
||||
|
||||
// First check for any entries in view by looking for the actual rendered entries
|
||||
for (const entryElt of container.querySelectorAll<HTMLElement>(
|
||||
'.review-panel-entry'
|
||||
)) {
|
||||
const entryTop = entryElt?.offsetTop ?? 0
|
||||
const entryBottom = entryTop + (entryElt?.offsetHeight ?? 0)
|
||||
|
||||
if (entryBottom > containerTop && entryTop < containerBottom) {
|
||||
// Some part of the entry is in view
|
||||
return { positionAbove: null, positionBelow: null }
|
||||
}
|
||||
}
|
||||
|
||||
// Find the max and min positions in the visible part of the viewport
|
||||
const visibleFrom = view.lineBlockAtHeight(containerTop).from
|
||||
const visibleTo = view.lineBlockAtHeight(containerBottom).to
|
||||
|
||||
// Then go through the positions to find the first entry above and below the visible viewport.
|
||||
// We can't use the rendered entries for this because only the entries that are in the viewport (or
|
||||
// have been in the viewport during the current page view session) are actually rendered.
|
||||
let firstEntryAbove: number | null = null
|
||||
let firstEntryBelow: number | null = null
|
||||
|
||||
const updateFirstEntryAboveBelowPositions = (
|
||||
position: number
|
||||
) => {
|
||||
if (visibleFrom === null || position < visibleFrom) {
|
||||
firstEntryAbove = Math.max(firstEntryAbove ?? 0, position)
|
||||
}
|
||||
|
||||
if (visibleTo === null || position > visibleTo) {
|
||||
firstEntryBelow = Math.min(
|
||||
firstEntryBelow ?? Number.MAX_VALUE,
|
||||
position
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
for (const entry of [...changes, ...comments]) {
|
||||
updateFirstEntryAboveBelowPositions(entry.op.p)
|
||||
}
|
||||
|
||||
const cursor = newComments.iter()
|
||||
while (cursor.value) {
|
||||
updateFirstEntryAboveBelowPositions(cursor.from)
|
||||
cursor.next()
|
||||
}
|
||||
|
||||
return {
|
||||
positionAbove: firstEntryAbove,
|
||||
positionBelow: firstEntryBelow,
|
||||
}
|
||||
},
|
||||
write({ positionAbove, positionBelow }) {
|
||||
setPositionAbove(positionAbove)
|
||||
setPositionBelow(positionBelow)
|
||||
},
|
||||
}),
|
||||
200
|
||||
),
|
||||
[changes, comments, newComments, view, reviewPanelOpen]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const scrollerElt = document.getElementsByClassName('cm-scroller')[0]
|
||||
if (scrollerElt) {
|
||||
scrollerElt.addEventListener('scroll', updateEntryPositions)
|
||||
return () => {
|
||||
scrollerElt.removeEventListener('scroll', updateEntryPositions)
|
||||
}
|
||||
}
|
||||
}, [updateEntryPositions])
|
||||
|
||||
const onMoreCommentsClick = useCallback(
|
||||
(position: number) => {
|
||||
view.dispatch({
|
||||
effects: EditorView.scrollIntoView(position, {
|
||||
y: 'center',
|
||||
}),
|
||||
selection: EditorSelection.cursor(position),
|
||||
})
|
||||
},
|
||||
[view]
|
||||
)
|
||||
|
||||
const onMoreCommentsAboveClick = useCallback(() => {
|
||||
if (positionAbove !== null) {
|
||||
onMoreCommentsClick(positionAbove)
|
||||
}
|
||||
}, [positionAbove, onMoreCommentsClick])
|
||||
|
||||
const onMoreCommentsBelowClick = useCallback(() => {
|
||||
if (positionBelow !== null) {
|
||||
onMoreCommentsClick(positionBelow)
|
||||
}
|
||||
}, [positionBelow, onMoreCommentsClick])
|
||||
|
||||
return {
|
||||
onEntriesPositioned: updateEntryPositions,
|
||||
onMoreCommentsAboveClick:
|
||||
positionAbove !== null ? onMoreCommentsAboveClick : null,
|
||||
onMoreCommentsBelowClick:
|
||||
positionBelow !== null ? onMoreCommentsBelowClick : null,
|
||||
}
|
||||
}
|
||||
|
||||
export default useMoreCommments
|
||||
@@ -0,0 +1,22 @@
|
||||
import { useCallback } from 'react'
|
||||
import { DocId } from '../../../../../types/project-settings'
|
||||
import { useProjectContext } from '../../../shared/context/project-context'
|
||||
import usePersistedState from '../../../shared/hooks/use-persisted-state'
|
||||
|
||||
export default function useOverviewFileCollapsed(docId: DocId) {
|
||||
const { _id: projectId } = useProjectContext()
|
||||
const [collapsedDocs, setCollapsedDocs] = usePersistedState<
|
||||
Record<DocId, boolean>
|
||||
>(`docs_collapsed_state:${projectId}`, {}, false, true)
|
||||
|
||||
const toggleCollapsed = useCallback(() => {
|
||||
setCollapsedDocs((collapsedDocs: Record<DocId, boolean>) => {
|
||||
return {
|
||||
...collapsedDocs,
|
||||
[docId]: !collapsedDocs[docId],
|
||||
}
|
||||
})
|
||||
}, [docId, setCollapsedDocs])
|
||||
|
||||
return { collapsed: collapsedDocs[docId], toggleCollapsed }
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { Ranges } from '../context/ranges-context'
|
||||
import { useProjectContext } from '@/shared/context/project-context'
|
||||
import { getJSON } from '@/infrastructure/fetch-json'
|
||||
import useSocketListener from '@/features/ide-react/hooks/use-socket-listener'
|
||||
import { useConnectionContext } from '@/features/ide-react/context/connection-context'
|
||||
|
||||
export default function useProjectRanges() {
|
||||
const { _id: projectId } = useProjectContext()
|
||||
const [error, setError] = useState<Error>()
|
||||
const [projectRanges, setProjectRanges] = useState<Map<string, Ranges>>()
|
||||
const [loading, setLoading] = useState(true)
|
||||
const { socket } = useConnectionContext()
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true)
|
||||
getJSON<{ id: string; ranges: Ranges }[]>(`/project/${projectId}/ranges`)
|
||||
.then(data => {
|
||||
setProjectRanges(
|
||||
new Map(
|
||||
data.map(item => [
|
||||
item.id,
|
||||
{
|
||||
docId: item.id,
|
||||
changes: item.ranges.changes ?? [],
|
||||
comments: item.ranges.comments ?? [],
|
||||
},
|
||||
])
|
||||
)
|
||||
)
|
||||
})
|
||||
.catch(error => setError(error))
|
||||
.finally(() => setLoading(false))
|
||||
}, [projectId])
|
||||
|
||||
useSocketListener(
|
||||
socket,
|
||||
'accept-changes',
|
||||
useCallback((docId: string, entryIds: string[]) => {
|
||||
setProjectRanges(prevProjectRanges => {
|
||||
if (!prevProjectRanges) {
|
||||
return prevProjectRanges
|
||||
}
|
||||
|
||||
const ranges = prevProjectRanges.get(docId)
|
||||
if (!ranges) {
|
||||
return prevProjectRanges
|
||||
}
|
||||
const updatedProjectRanges = new Map(prevProjectRanges)
|
||||
|
||||
updatedProjectRanges.set(docId, {
|
||||
...ranges,
|
||||
changes: ranges.changes.filter(
|
||||
change => !entryIds.includes(change.id)
|
||||
),
|
||||
})
|
||||
|
||||
return updatedProjectRanges
|
||||
})
|
||||
}, [])
|
||||
)
|
||||
|
||||
return { projectRanges, error, loading }
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { useLayoutContext } from '@/shared/context/layout-context'
|
||||
import { useRangesContext } from '../context/ranges-context'
|
||||
import { useThreadsContext } from '@/features/review-panel-new/context/threads-context'
|
||||
import { hasActiveRange } from '@/features/review-panel-new/utils/has-active-range'
|
||||
import { useRailContext } from '@/features/ide-redesign/contexts/rail-context'
|
||||
import { useIsNewEditorEnabled } from '@/features/ide-redesign/utils/new-editor-utils'
|
||||
|
||||
export default function useReviewPanelLayout(): {
|
||||
showPanel: boolean
|
||||
showHeader: boolean
|
||||
mini: boolean
|
||||
} {
|
||||
const ranges = useRangesContext()
|
||||
const threads = useThreadsContext()
|
||||
const { selectedTab: selectedRailTab, isOpen: railIsOpen } = useRailContext()
|
||||
const { reviewPanelOpen: reviewPanelOpenOldEditor } = useLayoutContext()
|
||||
|
||||
const newEditor = useIsNewEditorEnabled()
|
||||
|
||||
const reviewPanelOpen = newEditor
|
||||
? selectedRailTab === 'review-panel' && railIsOpen
|
||||
: reviewPanelOpenOldEditor
|
||||
|
||||
const hasCommentOrChange = hasActiveRange(ranges, threads)
|
||||
const showPanel = reviewPanelOpen || !!hasCommentOrChange
|
||||
const mini = !reviewPanelOpen
|
||||
const showHeader = showPanel && !mini
|
||||
|
||||
return { showPanel, showHeader, mini }
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { CSSProperties, useCallback, useEffect, useState } from 'react'
|
||||
import { useCodeMirrorViewContext } from '@/features/source-editor/components/codemirror-context'
|
||||
import getMeta from '@/utils/meta'
|
||||
|
||||
export const useReviewPanelStyles = (mini: boolean) => {
|
||||
const view = useCodeMirrorViewContext()
|
||||
|
||||
const [styles, setStyles] = useState<CSSProperties>({
|
||||
'--review-panel-header-height': getMeta('ol-isReviewerRoleEnabled')
|
||||
? '36px'
|
||||
: '69px',
|
||||
} as CSSProperties)
|
||||
|
||||
const updateScrollDomVariables = useCallback((element: HTMLDivElement) => {
|
||||
const { top, bottom } = element.getBoundingClientRect()
|
||||
|
||||
setStyles(value => ({
|
||||
...value,
|
||||
'--review-panel-top': `${top}px`,
|
||||
'--review-panel-bottom': `${bottom}px`,
|
||||
}))
|
||||
}, [])
|
||||
|
||||
const updateContentDomVariables = useCallback((element: HTMLDivElement) => {
|
||||
const { height } = element.getBoundingClientRect()
|
||||
|
||||
setStyles(value => ({
|
||||
...value,
|
||||
'--review-panel-height': `${height}px`,
|
||||
}))
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if ('ResizeObserver' in window) {
|
||||
const scrollDomObserver = new window.ResizeObserver(entries =>
|
||||
updateScrollDomVariables(entries[0]?.target as HTMLDivElement)
|
||||
)
|
||||
scrollDomObserver.observe(view.scrollDOM)
|
||||
|
||||
const contentDomObserver = new window.ResizeObserver(entries =>
|
||||
updateContentDomVariables(entries[0]?.target as HTMLDivElement)
|
||||
)
|
||||
contentDomObserver.observe(view.contentDOM)
|
||||
|
||||
return () => {
|
||||
scrollDomObserver.disconnect()
|
||||
contentDomObserver.disconnect()
|
||||
}
|
||||
}
|
||||
}, [view, updateScrollDomVariables, updateContentDomVariables])
|
||||
|
||||
return styles
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { useCallback, useState, Dispatch, SetStateAction } from 'react'
|
||||
|
||||
export default function useSubmittableTextInput(
|
||||
handleSubmit: (
|
||||
content: string,
|
||||
setContent: Dispatch<SetStateAction<string>>
|
||||
) => void
|
||||
) {
|
||||
const [content, setContent] = useState('')
|
||||
|
||||
const handleKeyPress = useCallback(
|
||||
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey && !e.ctrlKey && !e.metaKey) {
|
||||
e.preventDefault()
|
||||
if (content.trim().length > 0) {
|
||||
handleSubmit(content, setContent)
|
||||
}
|
||||
}
|
||||
},
|
||||
[content, handleSubmit]
|
||||
)
|
||||
|
||||
const handleChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setContent(e.target.value)
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
return { handleChange, handleKeyPress, content }
|
||||
}
|
||||
Reference in New Issue
Block a user