From b580399f78e077de750e08d7cdee93ed4f99f60f Mon Sep 17 00:00:00 2001 From: ilkin-overleaf <100852799+ilkin-overleaf@users.noreply.github.com> Date: Thu, 2 Nov 2023 13:18:12 +0200 Subject: [PATCH] Merge pull request #15397 from overleaf/ii-ide-page-prototype-review-panel Init review panel for React IDE page GitOrigin-RevId: fc23201055ae892c5c1d5cb88e472a0bb0cd6c25 --- .../ide-react/context/ide-react-context.tsx | 4 +- .../hooks/use-review-panel-state.ts | 273 ++++++++++++++++++ .../review-panel/review-panel-context.tsx | 43 +++ .../review-panel/types/review-panel-state.ts | 11 + .../review-panel-context-adapter.ts | 47 +++ .../components/codemirror-editor.tsx | 2 +- .../components/review-panel/review-panel.tsx | 42 ++- .../review-panel/review-panel-context.tsx | 17 +- .../stylesheets/app/editor/ide-react.less | 9 + 9 files changed, 440 insertions(+), 8 deletions(-) create mode 100644 services/web/frontend/js/features/ide-react/context/review-panel/hooks/use-review-panel-state.ts create mode 100644 services/web/frontend/js/features/ide-react/context/review-panel/review-panel-context.tsx create mode 100644 services/web/frontend/js/features/ide-react/context/review-panel/types/review-panel-state.ts create mode 100644 services/web/frontend/js/features/ide-react/scope-adapters/review-panel-context-adapter.ts diff --git a/services/web/frontend/js/features/ide-react/context/ide-react-context.tsx b/services/web/frontend/js/features/ide-react/context/ide-react-context.tsx index b80a696efe..6db0473c96 100644 --- a/services/web/frontend/js/features/ide-react/context/ide-react-context.tsx +++ b/services/web/frontend/js/features/ide-react/context/ide-react-context.tsx @@ -9,6 +9,7 @@ import React, { } from 'react' import { ReactScopeValueStore } from '@/features/ide-react/scope-value-store/react-scope-value-store' import populateLayoutScope from '@/features/ide-react/scope-adapters/layout-context-adapter' +import populateReviewPanelScope from '@/features/ide-react/scope-adapters/review-panel-context-adapter' import { IdeProvider } from '@/shared/context/ide-context' import { createIdeEventEmitter, @@ -22,8 +23,8 @@ import { postJSON } from '@/infrastructure/fetch-json' import { EventLog } from '@/features/ide-react/editor/event-log' import { populateSettingsScope } from '@/features/ide-react/scope-adapters/settings-adapter' import { populateOnlineUsersScope } from '@/features/ide-react/context/online-users-context' -import { ReactScopeEventEmitter } from '@/features/ide-react/scope-event-emitter/react-scope-event-emitter' import { populateReferenceScope } from '@/features/ide-react/context/references-context' +import { ReactScopeEventEmitter } from '@/features/ide-react/scope-event-emitter/react-scope-event-emitter' type IdeReactContextValue = { projectId: string @@ -75,6 +76,7 @@ function createReactScopeValueStore() { populateOnlineUsersScope(scopeStore) populateReferenceScope(scopeStore) populateFileTreeScope(scopeStore) + populateReviewPanelScope(scopeStore) scopeStore.allowNonExistentPath('hasLintingError') scopeStore.allowNonExistentPath('loadingThreads') diff --git a/services/web/frontend/js/features/ide-react/context/review-panel/hooks/use-review-panel-state.ts b/services/web/frontend/js/features/ide-react/context/review-panel/hooks/use-review-panel-state.ts new file mode 100644 index 0000000000..eb4947b23d --- /dev/null +++ b/services/web/frontend/js/features/ide-react/context/review-panel/hooks/use-review-panel-state.ts @@ -0,0 +1,273 @@ +import { useState, useMemo, useCallback } from 'react' +import useScopeValue from '../../../../../shared/hooks/use-scope-value' +import { sendMB } from '../../../../../infrastructure/event-tracking' +import { dispatchReviewPanelLayout as handleLayoutChange } from '@/features/source-editor/extensions/changes/change-manager' +import { useProjectContext } from '@/shared/context/project-context' +import { useLayoutContext } from '@/shared/context/layout-context' +import { ReviewPanelStateReactIde } from '../types/review-panel-state' +import * as ReviewPanel from '../types/review-panel-state' +import { + SubView, + ThreadId, +} from '../../../../../../../types/review-panel/review-panel' + +function useReviewPanelState(): ReviewPanelStateReactIde { + const { reviewPanelOpen, setReviewPanelOpen } = useLayoutContext() + const { + features: { trackChangesVisible }, + } = useProjectContext() + + const [subView, setSubView] = useScopeValue>( + 'reviewPanel.subView' + ) + const [loading] = useScopeValue>( + 'reviewPanel.overview.loading' + ) + const [nVisibleSelectedChanges] = useScopeValue< + ReviewPanel.Value<'nVisibleSelectedChanges'> + >('reviewPanel.nVisibleSelectedChanges') + const [collapsed, setCollapsed] = useScopeValue< + ReviewPanel.Value<'collapsed'> + >('reviewPanel.overview.docsCollapsedState') + const [commentThreads] = useScopeValue>( + 'reviewPanel.commentThreads', + true + ) + const [docs] = useScopeValue>('docs') + const [entries] = useScopeValue>( + 'reviewPanel.entries', + true + ) + const [loadingThreads] = + useScopeValue>('loadingThreads') + + const [permissions] = + useScopeValue>('permissions') + const [users] = useScopeValue>('users', true) + const [resolvedComments] = useScopeValue< + ReviewPanel.Value<'resolvedComments'> + >('reviewPanel.resolvedComments', true) + + const [wantTrackChanges] = useScopeValue< + ReviewPanel.Value<'wantTrackChanges'> + >('editor.wantTrackChanges') + const [openDocId] = + useScopeValue>('editor.open_doc_id') + const [shouldCollapse, setShouldCollapse] = useScopeValue< + ReviewPanel.Value<'shouldCollapse'> + >('reviewPanel.fullTCStateCollapsed') + const [lineHeight] = useScopeValue( + 'reviewPanel.rendererData.lineHeight' + ) + + const [toggleTrackChangesForEveryone] = useScopeValue< + ReviewPanel.UpdaterFn<'toggleTrackChangesForEveryone'> + >('toggleTrackChangesForEveryone') + const [toggleTrackChangesForUser] = useScopeValue< + ReviewPanel.UpdaterFn<'toggleTrackChangesForUser'> + >('toggleTrackChangesForUser') + const [toggleTrackChangesForGuests] = useScopeValue< + ReviewPanel.UpdaterFn<'toggleTrackChangesForGuests'> + >('toggleTrackChangesForGuests') + + const [trackChangesState] = useScopeValue< + ReviewPanel.Value<'trackChangesState'> + >('reviewPanel.trackChangesState') + const [trackChangesOnForEveryone] = useScopeValue< + ReviewPanel.Value<'trackChangesOnForEveryone'> + >('reviewPanel.trackChangesOnForEveryone') + const [trackChangesOnForGuests] = useScopeValue< + ReviewPanel.Value<'trackChangesOnForGuests'> + >('reviewPanel.trackChangesOnForGuests') + const [trackChangesForGuestsAvailable] = useScopeValue< + ReviewPanel.Value<'trackChangesForGuestsAvailable'> + >('reviewPanel.trackChangesForGuestsAvailable') + const [resolveComment] = + useScopeValue>('resolveComment') + const [submitNewComment] = + useScopeValue>('submitNewComment') + const [deleteComment] = + useScopeValue>('deleteComment') + const [gotoEntry] = + useScopeValue>('gotoEntry') + const [saveEdit] = + useScopeValue>('saveEdit') + const [submitReplyAngular] = + useScopeValue< + (entry: { thread_id: ThreadId; replyContent: string }) => void + >('submitReply') + + const [formattedProjectMembers] = useScopeValue< + ReviewPanel.Value<'formattedProjectMembers'> + >('reviewPanel.formattedProjectMembers') + + const toggleReviewPanel = useCallback(() => { + if (!trackChangesVisible) { + return + } + setReviewPanelOpen(value => !value) + sendMB('rp-toggle-panel', { + value: reviewPanelOpen, + }) + }, [reviewPanelOpen, setReviewPanelOpen, trackChangesVisible]) + + const [unresolveComment] = + useScopeValue>('unresolveComment') + const [deleteThread] = + useScopeValue>('deleteThread') + const [refreshResolvedCommentsDropdown] = useScopeValue< + ReviewPanel.UpdaterFn<'refreshResolvedCommentsDropdown'> + >('refreshResolvedCommentsDropdown') + const [acceptChanges] = + useScopeValue>('acceptChanges') + const [rejectChanges] = + useScopeValue>('rejectChanges') + const [bulkAcceptActions] = + useScopeValue>( + 'bulkAcceptActions' + ) + const [bulkRejectActions] = + useScopeValue>( + 'bulkRejectActions' + ) + + const handleSetSubview = useCallback( + (subView: SubView) => { + setSubView(subView) + sendMB('rp-subview-change', { subView }) + }, + [setSubView] + ) + + const submitReply = useCallback( + (threadId: ThreadId, replyContent: string) => { + submitReplyAngular({ thread_id: threadId, replyContent }) + }, + [submitReplyAngular] + ) + + const [entryHover, setEntryHover] = useState(false) + const [isAddingComment, setIsAddingComment] = useState(false) + const [navHeight, setNavHeight] = useState(0) + const [toolbarHeight, setToolbarHeight] = useState(0) + const [layoutSuspended, setLayoutSuspended] = useState(false) + + const values = useMemo( + () => ({ + collapsed, + commentThreads, + docs, + entries, + entryHover, + isAddingComment, + loadingThreads, + nVisibleSelectedChanges, + permissions, + users, + resolvedComments, + shouldCollapse, + navHeight, + toolbarHeight, + subView, + wantTrackChanges, + loading, + openDocId, + lineHeight, + trackChangesState, + trackChangesOnForEveryone, + trackChangesOnForGuests, + trackChangesForGuestsAvailable, + formattedProjectMembers, + layoutSuspended, + }), + [ + collapsed, + commentThreads, + docs, + entries, + entryHover, + isAddingComment, + loadingThreads, + nVisibleSelectedChanges, + permissions, + users, + resolvedComments, + shouldCollapse, + navHeight, + toolbarHeight, + subView, + wantTrackChanges, + loading, + openDocId, + lineHeight, + trackChangesState, + trackChangesOnForEveryone, + trackChangesOnForGuests, + trackChangesForGuestsAvailable, + formattedProjectMembers, + layoutSuspended, + ] + ) + + const updaterFns = useMemo( + () => ({ + handleSetSubview, + handleLayoutChange, + gotoEntry, + resolveComment, + submitReply, + acceptChanges, + rejectChanges, + toggleReviewPanel, + bulkAcceptActions, + bulkRejectActions, + saveEdit, + submitNewComment, + deleteComment, + unresolveComment, + refreshResolvedCommentsDropdown, + deleteThread, + toggleTrackChangesForEveryone, + toggleTrackChangesForUser, + toggleTrackChangesForGuests, + setEntryHover, + setCollapsed, + setShouldCollapse, + setIsAddingComment, + setNavHeight, + setToolbarHeight, + setLayoutSuspended, + }), + [ + handleSetSubview, + gotoEntry, + resolveComment, + submitReply, + acceptChanges, + rejectChanges, + toggleReviewPanel, + bulkAcceptActions, + bulkRejectActions, + saveEdit, + submitNewComment, + deleteComment, + unresolveComment, + refreshResolvedCommentsDropdown, + deleteThread, + toggleTrackChangesForEveryone, + toggleTrackChangesForUser, + toggleTrackChangesForGuests, + setCollapsed, + setEntryHover, + setShouldCollapse, + setIsAddingComment, + setNavHeight, + setToolbarHeight, + setLayoutSuspended, + ] + ) + + return { values, updaterFns } +} + +export default useReviewPanelState diff --git a/services/web/frontend/js/features/ide-react/context/review-panel/review-panel-context.tsx b/services/web/frontend/js/features/ide-react/context/review-panel/review-panel-context.tsx new file mode 100644 index 0000000000..3187d93e6f --- /dev/null +++ b/services/web/frontend/js/features/ide-react/context/review-panel/review-panel-context.tsx @@ -0,0 +1,43 @@ +import { useContext, createContext } from 'react' +import useReviewPanelState from '@/features/ide-react/context/review-panel/hooks/use-review-panel-state' +import { ReviewPanelStateReactIde } from '@/features/ide-react/context/review-panel/types/review-panel-state' + +export const ReviewPanelReactIdeValueContext = createContext< + ReviewPanelStateReactIde['values'] | undefined +>(undefined) + +export const ReviewPanelReactIdeUpdaterFnsContext = createContext< + ReviewPanelStateReactIde['updaterFns'] | undefined +>(undefined) + +export const ReviewPanelReactIdeProvider: React.FC = ({ children }) => { + const { values, updaterFns } = useReviewPanelState() + + return ( + + + {children} + + + ) +} + +export function useReviewPanelReactIdeValueContext() { + const context = useContext(ReviewPanelReactIdeValueContext) + if (!context) { + throw new Error( + 'ReviewPanelReactIdeValueContext is only available inside ReviewPanelReactIdeProvider' + ) + } + return context +} + +export function useReviewPanelReactIdeUpdaterFnsContext() { + const context = useContext(ReviewPanelReactIdeUpdaterFnsContext) + if (!context) { + throw new Error( + 'ReviewPanelReactIdeUpdaterFnsContext is only available inside ReviewPanelReactIdeProvider' + ) + } + return context +} diff --git a/services/web/frontend/js/features/ide-react/context/review-panel/types/review-panel-state.ts b/services/web/frontend/js/features/ide-react/context/review-panel/types/review-panel-state.ts new file mode 100644 index 0000000000..db33ee14a1 --- /dev/null +++ b/services/web/frontend/js/features/ide-react/context/review-panel/types/review-panel-state.ts @@ -0,0 +1,11 @@ +import { ReviewPanelState } from '@/features/source-editor/context/review-panel/types/review-panel-state' + +export interface ReviewPanelStateReactIde extends ReviewPanelState {} + +// Getter for values +export type Value = + ReviewPanelStateReactIde['values'][T] + +// Getter for stable functions +export type UpdaterFn = + ReviewPanelStateReactIde['updaterFns'][T] diff --git a/services/web/frontend/js/features/ide-react/scope-adapters/review-panel-context-adapter.ts b/services/web/frontend/js/features/ide-react/scope-adapters/review-panel-context-adapter.ts new file mode 100644 index 0000000000..25c4f9bf29 --- /dev/null +++ b/services/web/frontend/js/features/ide-react/scope-adapters/review-panel-context-adapter.ts @@ -0,0 +1,47 @@ +import { ReactScopeValueStore } from '@/features/ide-react/scope-value-store/react-scope-value-store' + +export default function populateReviewPanelScope(store: ReactScopeValueStore) { + store.set('reviewPanel.overview.docsCollapsedState', {}) + store.set('reviewPanel.subView', 'cur_file') + store.set('reviewPanel.overview.loading', false) + store.set('reviewPanel.nVisibleSelectedChanges', 0) + store.set('reviewPanel.commentThreads', {}) + store.set('docs', undefined) + store.set('reviewPanel.entries', {}) + store.set('loadingThreads', true) + store.set('permissions', { + read: false, + write: false, + admin: false, + comment: false, + }) + store.set('users', {}) + store.set('reviewPanel.resolvedComments', {}) + store.set('editor.wantTrackChanges', false) + store.set('editor.open_doc_id', null) + store.set('reviewPanel.fullTCStateCollapsed', true) + store.set('reviewPanel.rendererData.lineHeight', 0) + store.set('reviewPanel.trackChangesState', {}) + store.set('reviewPanel.trackChangesOnForEveryone', false) + store.set('reviewPanel.trackChangesOnForGuests', false) + store.set('reviewPanel.trackChangesForGuestsAvailable', false) + store.set('reviewPanel.formattedProjectMembers', {}) + store.set('toggleTrackChangesForEveryone', () => {}) + store.set('toggleTrackChangesForUser', () => {}) + store.set('toggleTrackChangesForGuests', () => {}) + store.set('resolveComment', () => {}) + store.set('submitNewComment', async () => {}) + store.set('deleteComment', () => {}) + store.set('gotoEntry', () => {}) + store.set('saveEdit', () => {}) + store.set('toggleReviewPanel', () => {}) + store.set('unresolveComment', () => {}) + store.set('deleteThread', () => {}) + store.set('refreshResolvedCommentsDropdown', async () => {}) + store.set('acceptChanges', () => {}) + store.set('rejectChanges', () => {}) + store.set('bulkAcceptActions', () => {}) + store.set('bulkRejectActions', () => {}) + store.set('submitReply', () => {}) + store.set('addNewComment', () => {}) +} diff --git a/services/web/frontend/js/features/source-editor/components/codemirror-editor.tsx b/services/web/frontend/js/features/source-editor/components/codemirror-editor.tsx index 84d058c349..9ebd55c557 100644 --- a/services/web/frontend/js/features/source-editor/components/codemirror-editor.tsx +++ b/services/web/frontend/js/features/source-editor/components/codemirror-editor.tsx @@ -64,7 +64,7 @@ function CodeMirrorEditor() { - {isReviewPanelReact && !isReactIde && } + {(isReviewPanelReact || isReactIde) && } {sourceEditorComponents.map( ({ import: { default: Component }, path }) => ( diff --git a/services/web/frontend/js/features/source-editor/components/review-panel/review-panel.tsx b/services/web/frontend/js/features/source-editor/components/review-panel/review-panel.tsx index b0994897a7..c3a1a0cb04 100644 --- a/services/web/frontend/js/features/source-editor/components/review-panel/review-panel.tsx +++ b/services/web/frontend/js/features/source-editor/components/review-panel/review-panel.tsx @@ -7,16 +7,22 @@ import { ReviewPanelProvider, useReviewPanelValueContext, } from '../../context/review-panel/review-panel-context' +import { ReviewPanelReactIdeProvider } from '@/features/ide-react/context/review-panel/review-panel-context' import { isCurrentFileView } from '../../utils/sub-view' +import { useLayoutContext } from '@/shared/context/layout-context' +import { useIdeContext } from '@/shared/context/ide-context' +import classnames from 'classnames' type ReviewPanelViewProps = { parentDomNode: Element } function ReviewPanelView({ parentDomNode }: ReviewPanelViewProps) { - const { subView } = useReviewPanelValueContext() + const { subView, loadingThreads } = useReviewPanelValueContext() + const { reviewPanelOpen } = useLayoutContext() + const { isReactIde } = useIdeContext() - return ReactDOM.createPortal( + const content = ( <> {isCurrentFileView(subView) ? ( @@ -24,15 +30,43 @@ function ReviewPanelView({ parentDomNode }: ReviewPanelViewProps) { ) : ( )} - , + + ) + + return ReactDOM.createPortal( + isReactIde ? ( +
+ {content} +
+ ) : ( + content + ), parentDomNode ) } function ReviewPanel() { const view = useCodeMirrorViewContext() + const { isReactIde } = useIdeContext() - return ( + return isReactIde ? ( + + + + ) : ( diff --git a/services/web/frontend/js/features/source-editor/context/review-panel/review-panel-context.tsx b/services/web/frontend/js/features/source-editor/context/review-panel/review-panel-context.tsx index de82abdca6..183d112e6a 100644 --- a/services/web/frontend/js/features/source-editor/context/review-panel/review-panel-context.tsx +++ b/services/web/frontend/js/features/source-editor/context/review-panel/review-panel-context.tsx @@ -1,5 +1,10 @@ import { createContext, useContext } from 'react' import useAngularReviewPanelState from './hooks/use-angular-review-panel-state' +import { + ReviewPanelReactIdeUpdaterFnsContext, + ReviewPanelReactIdeValueContext, +} from '@/features/ide-react/context/review-panel/review-panel-context' +import { useIdeContext } from '@/shared/context/ide-context' import { ReviewPanelState } from './types/review-panel-state' const ReviewPanelValueContext = createContext< @@ -27,7 +32,11 @@ export function ReviewPanelProvider({ children }: ReviewPanelProviderProps) { } export function useReviewPanelValueContext() { - const context = useContext(ReviewPanelValueContext) + const contextAngularIde = useContext(ReviewPanelValueContext) + const contextReactIde = useContext(ReviewPanelReactIdeValueContext) + const { isReactIde } = useIdeContext() + const context = isReactIde ? contextReactIde : contextAngularIde + if (!context) { throw new Error( 'ReviewPanelValueContext is only available inside ReviewPanelProvider' @@ -37,7 +46,11 @@ export function useReviewPanelValueContext() { } export function useReviewPanelUpdaterFnsContext() { - const context = useContext(ReviewPanelUpdaterFnsContext) + const contextAngularIde = useContext(ReviewPanelUpdaterFnsContext) + const contextReactIde = useContext(ReviewPanelReactIdeUpdaterFnsContext) + const { isReactIde } = useIdeContext() + const context = isReactIde ? contextReactIde : contextAngularIde + if (!context) { throw new Error( 'ReviewPanelUpdaterFnsContext is only available inside ReviewPanelProvider' diff --git a/services/web/frontend/stylesheets/app/editor/ide-react.less b/services/web/frontend/stylesheets/app/editor/ide-react.less index 5e63de5f66..0c0e7e151e 100644 --- a/services/web/frontend/stylesheets/app/editor/ide-react.less +++ b/services/web/frontend/stylesheets/app/editor/ide-react.less @@ -12,6 +12,15 @@ position: relative; height: 100%; } + + .review-panel { + height: 100%; + } + + .rp-state-overview { + position: sticky; + top: 0; + } } .ide-react-main {