From 2bfdae76055a533d11a8d11452d4980b67d758bc Mon Sep 17 00:00:00 2001 From: ilkin-overleaf <100852799+ilkin-overleaf@users.noreply.github.com> Date: Fri, 24 Nov 2023 16:54:58 +0200 Subject: [PATCH] Merge pull request #15909 from overleaf/ii-ide-page-prototype-review-panel-refresh-ranges [web] React ide page refresh ranges GitOrigin-RevId: 7f79b8f63869ee39fef9a101e6dcc56c39af8df7 --- .../hooks/use-review-panel-state.ts | 55 ++++++++++++++++--- .../review-panel-context-adapter.ts | 2 - .../review-panel/types/review-panel-state.ts | 5 +- .../js/shared/hooks/use-persisted-state.ts | 49 +++++++++++++++-- 4 files changed, 94 insertions(+), 17 deletions(-) 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 index 23cc0a4e61..f95093891e 100644 --- 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 @@ -1,5 +1,6 @@ import { useState, useEffect, useMemo, useCallback, useRef } from 'react' import { isEqual, cloneDeep } from 'lodash' +import usePersistedState from '@/shared/hooks/use-persisted-state' import useScopeValue from '../../../../../shared/hooks/use-scope-value' import useSocketListener from '@/features/ide-react/hooks/use-socket-listener' import useAsync from '@/shared/hooks/use-async' @@ -130,9 +131,9 @@ function useReviewPanelState(): ReviewPanelStateReactIde { const [nVisibleSelectedChanges] = useScopeValue< ReviewPanel.Value<'nVisibleSelectedChanges'> >('reviewPanel.nVisibleSelectedChanges') - const [collapsed, setCollapsed] = useScopeValue< + const [collapsed, setCollapsed] = usePersistedState< ReviewPanel.Value<'collapsed'> - >('reviewPanel.overview.docsCollapsedState') + >(`docs_collapsed_state:${projectId}`, {}, false, true) const [commentThreads, setCommentThreads] = useState< ReviewPanel.Value<'commentThreads'> >({}) @@ -273,7 +274,7 @@ function useReviewPanelState(): ReviewPanelStateReactIde { const getChangeTracker = useCallback( (docId: DocId) => { if (!rangesTrackers.current[docId]) { - rangesTrackers.current[docId] = new RangesTracker() as RangesTracker + rangesTrackers.current[docId] = new RangesTracker() rangesTrackers.current[docId].resolvedThreadIds = { ...resolvedThreadIds, } @@ -890,9 +891,47 @@ function useReviewPanelState(): ReviewPanelStateReactIde { [onThreadDeleted, projectId] ) - const [refreshResolvedCommentsDropdown] = useScopeValue< - ReviewPanel.UpdaterFn<'refreshResolvedCommentsDropdown'> - >('refreshResolvedCommentsDropdown') + const refreshRanges = useCallback(() => { + type Doc = { + id: DocId + ranges: { + comments?: unknown[] + changes?: unknown[] + } + } + + return getJSON(`/project/${projectId}/ranges`) + .then(docs => { + setCollapsed(prevState => { + const collapsed = { ...prevState } + docs.forEach(doc => { + if (collapsed[doc.id] == null) { + collapsed[doc.id] = false + } + }) + return collapsed + }) + + docs.forEach(async doc => { + if (doc.id !== currentDocumentId) { + // this is kept up to date in real-time, don't overwrite + const rangesTracker = getChangeTracker(doc.id) + rangesTracker.comments = doc.ranges?.comments ?? [] + rangesTracker.changes = doc.ranges?.changes ?? [] + } + }) + + return Promise.all(docs.map(doc => updateEntries(doc.id))) + }) + .catch(debugConsole.error) + }, [ + currentDocumentId, + getChangeTracker, + projectId, + setCollapsed, + updateEntries, + ]) + const [acceptChanges] = useScopeValue>('acceptChanges') const [rejectChanges] = @@ -1056,7 +1095,7 @@ function useReviewPanelState(): ReviewPanelStateReactIde { submitNewComment, deleteComment, unresolveComment, - refreshResolvedCommentsDropdown, + refreshResolvedCommentsDropdown: refreshRanges, deleteThread, toggleTrackChangesForEveryone, toggleTrackChangesForUser, @@ -1084,7 +1123,7 @@ function useReviewPanelState(): ReviewPanelStateReactIde { submitNewComment, deleteComment, unresolveComment, - refreshResolvedCommentsDropdown, + refreshRanges, deleteThread, toggleTrackChangesForEveryone, toggleTrackChangesForUser, 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 index c520fc7fa8..0d6f2779df 100644 --- 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 @@ -1,7 +1,6 @@ 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.overview.loading', false) store.set('reviewPanel.nVisibleSelectedChanges', 0) store.set('permissions', { @@ -18,7 +17,6 @@ export default function populateReviewPanelScope(store: ReactScopeValueStore) { store.set('deleteComment', () => {}) store.set('gotoEntry', () => {}) store.set('saveEdit', () => {}) - store.set('refreshResolvedCommentsDropdown', async () => {}) store.set('acceptChanges', () => {}) store.set('rejectChanges', () => {}) store.set('bulkAcceptActions', () => {}) diff --git a/services/web/frontend/js/features/source-editor/context/review-panel/types/review-panel-state.ts b/services/web/frontend/js/features/source-editor/context/review-panel/types/review-panel-state.ts index d233bdf65c..519aa2e40a 100644 --- a/services/web/frontend/js/features/source-editor/context/review-panel/types/review-panel-state.ts +++ b/services/web/frontend/js/features/source-editor/context/review-panel/types/review-panel-state.ts @@ -1,6 +1,7 @@ import { CommentId, ReviewPanelCommentThreads, + ReviewPanelDocEntries, ReviewPanelEntries, ReviewPanelUsers, SubView, @@ -72,7 +73,9 @@ export interface ReviewPanelState { ) => void unresolveComment: (threadId: ThreadId) => void deleteThread: (docId: DocId, threadId: ThreadId) => void - refreshResolvedCommentsDropdown: () => Promise + refreshResolvedCommentsDropdown: () => Promise< + void | ReviewPanelDocEntries[] + > submitNewComment: (content: string) => Promise setEntryHover: React.Dispatch>> setIsAddingComment: React.Dispatch< diff --git a/services/web/frontend/js/shared/hooks/use-persisted-state.ts b/services/web/frontend/js/shared/hooks/use-persisted-state.ts index bcd9888a9f..1ff452ce4a 100644 --- a/services/web/frontend/js/shared/hooks/use-persisted-state.ts +++ b/services/web/frontend/js/shared/hooks/use-persisted-state.ts @@ -7,14 +7,51 @@ import { } from 'react' import _ from 'lodash' import localStorage from '../../infrastructure/local-storage' +import { debugConsole } from '@/utils/debugging' + +const safeStringify = (value: unknown) => { + try { + return JSON.stringify(value) + } catch (e) { + debugConsole.error('double stringify exception', e) + return null + } +} + +const safeParse = (value: string) => { + try { + return JSON.parse(value) + } catch (e) { + debugConsole.error('double parse exception', e) + return null + } +} function usePersistedState( key: string, defaultValue?: T, - listen = false + listen = false, + // The option below is for backward compatibility with Angular + // which sometimes stringifies the values twice + doubleStringifyAndParse = false ): [T, Dispatch>] { + const getItem = useCallback( + (key: string) => { + const item = localStorage.getItem(key) + return doubleStringifyAndParse ? safeParse(item) : item + }, + [doubleStringifyAndParse] + ) + const setItem = useCallback( + (key: string, value: unknown) => { + const val = doubleStringifyAndParse ? safeStringify(value) : value + localStorage.setItem(key, val) + }, + [doubleStringifyAndParse] + ) + const [value, setValue] = useState(() => { - return localStorage.getItem(key) ?? defaultValue + return getItem(key) ?? defaultValue }) const updateFunction = useCallback( @@ -27,13 +64,13 @@ function usePersistedState( if (actualNewValue === defaultValue) { localStorage.removeItem(key) } else { - localStorage.setItem(key, actualNewValue) + setItem(key, actualNewValue) } return actualNewValue }) }, - [key, defaultValue] + [key, defaultValue, setItem] ) useEffect(() => { @@ -42,7 +79,7 @@ function usePersistedState( if (event.key === key) { // note: this value is read via getItem rather than from event.newValue // because getItem handles deserializing the JSON that's stored in localStorage. - setValue(localStorage.getItem(key) ?? defaultValue) + setValue(getItem(key) ?? defaultValue) } } @@ -52,7 +89,7 @@ function usePersistedState( window.removeEventListener('storage', listener) } } - }, [key, listen, defaultValue]) + }, [defaultValue, key, listen, getItem]) return [value, updateFunction] }