From e514e973057dc51196d98b02e37bd004ae8da307 Mon Sep 17 00:00:00 2001 From: ilkin-overleaf <100852799+ilkin-overleaf@users.noreply.github.com> Date: Wed, 14 Jun 2023 11:29:24 +0300 Subject: [PATCH] Merge pull request #13438 from overleaf/ii-review-panel-toolbar-shell [web] Create review panel toolbar shell GitOrigin-RevId: 561fb18e1239c9b96b52944716a83cf3b8606677 --- package-lock.json | 20 ++ .../web/frontend/extracted-translations.json | 16 ++ .../components/review-panel/container.tsx | 10 +- .../review-panel/current-file-container.tsx | 30 ++- .../components/review-panel/nav.tsx | 6 + .../toolbar/resolved-comment-entry.tsx | 142 ++++++++++++++ .../toolbar/resolved-comments-dropdown.tsx | 81 ++++++++ .../toolbar/resolved-comments-scroller.tsx | 54 +++++ .../review-panel/toolbar/toggle-menu.tsx | 184 ++++++++++++++++++ .../review-panel/toolbar/toolbar.tsx | 13 ++ .../toolbar/track-changes-toggle.tsx | 33 ++++ .../hooks/use-angular-review-panel-state.ts | 95 +++++++++ .../hooks/use-angular-review-panel.ts | 33 ---- .../review-panel/review-panel-context.tsx | 2 +- .../review-panel/types/review-panel-state.ts | 21 ++ .../stylesheets/app/editor/review-panel.less | 86 +++++++- services/web/locales/en.json | 2 + services/web/package.json | 1 + .../review-panel/review-panel.spec.tsx | 121 ++++++++++++ .../source-editor/helpers/mock-scope.ts | 18 ++ services/web/types/review-panel.ts | 6 + 21 files changed, 933 insertions(+), 41 deletions(-) create mode 100644 services/web/frontend/js/features/source-editor/components/review-panel/toolbar/resolved-comment-entry.tsx create mode 100644 services/web/frontend/js/features/source-editor/components/review-panel/toolbar/resolved-comments-dropdown.tsx create mode 100644 services/web/frontend/js/features/source-editor/components/review-panel/toolbar/resolved-comments-scroller.tsx create mode 100644 services/web/frontend/js/features/source-editor/components/review-panel/toolbar/toggle-menu.tsx create mode 100644 services/web/frontend/js/features/source-editor/components/review-panel/toolbar/toolbar.tsx create mode 100644 services/web/frontend/js/features/source-editor/components/review-panel/toolbar/track-changes-toggle.tsx create mode 100644 services/web/frontend/js/features/source-editor/context/review-panel/hooks/use-angular-review-panel-state.ts delete mode 100644 services/web/frontend/js/features/source-editor/context/review-panel/hooks/use-angular-review-panel.ts create mode 100644 services/web/test/frontend/features/review-panel/review-panel.spec.tsx create mode 100644 services/web/types/review-panel.ts diff --git a/package-lock.json b/package-lock.json index 053454b947..edab058774 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12079,6 +12079,15 @@ "@types/react": "*" } }, + "node_modules/@types/react-linkify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/react-linkify/-/react-linkify-1.0.0.tgz", + "integrity": "sha512-2NKXPQGaHNfh/dCqkVC55k1tAhQyNoNZa31J50nIneMVwHqUI00FAP+Lyp8e0BarPf84kn4GRVAhtWX9XJBzSQ==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/react/node_modules/csstype": { "version": "3.0.10", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.10.tgz", @@ -41617,6 +41626,7 @@ "@types/react-bootstrap": "^0.32.29", "@types/react-color": "^3.0.6", "@types/react-dom": "^17.0.13", + "@types/react-linkify": "^1.0.0", "@types/recurly__recurly-js": "^4.22.0", "@types/sinon-chai": "^3.2.8", "@types/uuid": "^8.3.4", @@ -50211,6 +50221,7 @@ "@types/react-bootstrap": "^0.32.29", "@types/react-color": "^3.0.6", "@types/react-dom": "^17.0.13", + "@types/react-linkify": "1.0.0", "@types/recurly__recurly-js": "^4.22.0", "@types/sinon-chai": "^3.2.8", "@types/uuid": "^8.3.4", @@ -53922,6 +53933,15 @@ "@types/react": "*" } }, + "@types/react-linkify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/react-linkify/-/react-linkify-1.0.0.tgz", + "integrity": "sha512-2NKXPQGaHNfh/dCqkVC55k1tAhQyNoNZa31J50nIneMVwHqUI00FAP+Lyp8e0BarPf84kn4GRVAhtWX9XJBzSQ==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, "@types/reactcss": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/@types/reactcss/-/reactcss-1.2.6.tgz", diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index 7ddf995c85..c4da12d1b9 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -567,6 +567,7 @@ "manage_sessions": "", "manage_subscription": "", "managers_management": "", + "mark_as_resolved": "", "math_display": "", "math_inline": "", "maximum_files_uploaded_together": "", @@ -624,6 +625,7 @@ "no_pdf_error_title": "", "no_preview_available": "", "no_projects": "", + "no_resolved_threads": "", "no_search_results": "", "no_symbols_found": "", "no_thanks_cancel_now": "", @@ -739,6 +741,7 @@ "publishing": "", "pull_github_changes_into_sharelatex": "", "push_sharelatex_changes_to_github": "", + "quoted_text_in": "", "raw_logs": "", "raw_logs_description": "", "reactivate_subscription": "", @@ -777,6 +780,7 @@ "removing": "", "rename": "", "rename_project": "", + "reopen": "", "replace_figure": "", "replace_from_another_project": "", "replace_from_computer": "", @@ -787,6 +791,7 @@ "resend": "", "resend_confirmation_email": "", "resending_confirmation_email": "", + "resolved_comments": "", "restore_file": "", "restoring": "", "reverse_x_sort_order": "", @@ -865,12 +870,14 @@ "share_with_your_collabs": "", "shared_with_you": "", "sharelatex_beta_program": "", + "show_all": "", "show_all_projects": "", "show_all_uppercase": "", "show_document_preamble": "", "show_hotkeys": "", "show_in_code": "", "show_in_pdf": "", + "show_less": "", "show_outline": "", "show_x_more": "", "show_x_more_projects": "", @@ -927,6 +934,11 @@ "tag_name_is_already_used": "", "tags": "", "take_short_survey": "", + "tc_everyone": "", + "tc_guests": "", + "tc_switch_everyone_tip": "", + "tc_switch_guests_tip": "", + "tc_switch_user_tip": "", "template_approved_by_publisher": "", "template_description": "", "template_title_taken_from_project_title": "", @@ -990,6 +1002,10 @@ "total_with_subtotal_and_tax": "", "total_words": "", "track_changes": "", + "track_changes_for_everyone": "", + "track_changes_for_x": "", + "track_changes_is_off": "", + "track_changes_is_on": "", "trash": "", "trash_projects": "", "trashed": "", diff --git a/services/web/frontend/js/features/source-editor/components/review-panel/container.tsx b/services/web/frontend/js/features/source-editor/components/review-panel/container.tsx index 48dd244bd6..4b49f1c05c 100644 --- a/services/web/frontend/js/features/source-editor/components/review-panel/container.tsx +++ b/services/web/frontend/js/features/source-editor/components/review-panel/container.tsx @@ -1,3 +1,5 @@ +import Toolbar from './toolbar/toolbar' +import Nav from './nav' import classnames from 'classnames' const reviewPanelClasses = ['ol-cm-review-panel'] @@ -10,8 +12,14 @@ type ContainerProps = { function Container({ children, classNames, ...rest }: ContainerProps) { return ( -
+
+ {children} +
) } diff --git a/services/web/frontend/js/features/source-editor/components/review-panel/current-file-container.tsx b/services/web/frontend/js/features/source-editor/components/review-panel/current-file-container.tsx index 92447b34ef..c1b4f08143 100644 --- a/services/web/frontend/js/features/source-editor/components/review-panel/current-file-container.tsx +++ b/services/web/frontend/js/features/source-editor/components/review-panel/current-file-container.tsx @@ -3,8 +3,34 @@ import Container from './container' function CurrentFileContainer() { return ( - ReviewPanelCurrentFileContainer -
Tools
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod + tempor incididunt ut labore et dolore magna aliqua. Et malesuada fames ac + turpis egestas integer eget aliquet nibh. Et leo duis ut diam quam nulla + porttitor massa id. Risus quis varius quam quisque id diam vel quam + elementum. Nibh venenatis cras sed felis. Sit amet commodo nulla facilisi + nullam vehicula ipsum a arcu. Dui ut ornare lectus sit amet est placerat + in. Aliquam ultrices sagittis orci a. Leo a diam sollicitudin tempor id eu + nisl nunc mi. Quis ipsum suspendisse ultrices gravida dictum fusce. Ut + etiam sit amet nisl purus in mollis nunc sed. Rhoncus est pellentesque + elit ullamcorper dignissim cras. Faucibus turpis in eu mi bibendum. Proin + libero nunc consequat interdum. Ac placerat vestibulum lectus mauris + ultrices eros in cursus turpis. Ac felis donec et odio. Nullam ac tortor + vitae purus faucibus. Consectetur lorem donec massa sapien faucibus et + molestie. Praesent elementum facilisis leo vel fringilla est ullamcorper + eget nulla. Adipiscing vitae proin sagittis nisl rhoncus mattis rhoncus + urna. Cursus metus aliquam eleifend mi in nulla posuere sollicitudin + aliquam. Eget nullam non nisi est sit amet facilisis magna. Donec + adipiscing tristique risus nec feugiat in fermentum posuere. Gravida + rutrum quisque non tellus orci ac auctor augue. Euismod in pellentesque + massa placerat duis ultricies lacus. Pellentesque diam volutpat commodo + sed egestas. Tempus iaculis urna id volutpat lacus laoreet. Lorem ipsum + dolor sit amet consectetur. Tincidunt id aliquet risus feugiat in ante + metus. Risus ultricies tristique nulla aliquet enim tortor at auctor urna. + Purus in mollis nunc sed. In ante metus dictum at. Magna eget est lorem + ipsum dolor sit. Fusce id velit ut tortor pretium viverra. Augue neque + gravida in fermentum et sollicitudin ac. Et malesuada fames ac turpis. + Felis bibendum ut tristique et egestas quis ipsum suspendisse ultrices. + Varius vel pharetra vel turpis nunc eget.
) } diff --git a/services/web/frontend/js/features/source-editor/components/review-panel/nav.tsx b/services/web/frontend/js/features/source-editor/components/review-panel/nav.tsx index b6d90ff8d6..4762551b6c 100644 --- a/services/web/frontend/js/features/source-editor/components/review-panel/nav.tsx +++ b/services/web/frontend/js/features/source-editor/components/review-panel/nav.tsx @@ -1 +1,7 @@ export type SubView = 'cur_file' | 'overview' + +function Nav() { + return
Nav
+} + +export default Nav diff --git a/services/web/frontend/js/features/source-editor/components/review-panel/toolbar/resolved-comment-entry.tsx b/services/web/frontend/js/features/source-editor/components/review-panel/toolbar/resolved-comment-entry.tsx new file mode 100644 index 0000000000..32411a86c9 --- /dev/null +++ b/services/web/frontend/js/features/source-editor/components/review-panel/toolbar/resolved-comment-entry.tsx @@ -0,0 +1,142 @@ +import { useTranslation } from 'react-i18next' +import { useState } from 'react' +import Linkify from 'react-linkify' +import { formatTime } from '../../../../utils/format-date' +import { useReviewPanelValueContext } from '../../../context/review-panel/review-panel-context' + +function LinkDecorator( + decoratedHref: string, + decoratedText: string, + key: number +) { + return ( + + {decoratedText} + + ) +} + +type ResolvedCommentEntryProps = { + thread: { + resolved_at: number + entryId: string + docName: string + content: string + messages: Array<{ + id: string + user: { + id: string + hue: string + name: string + } + content: string + timestamp: string + }> + resolved_by_user: { + name: string + hue: string + } + } // TODO extract type + contentLimit?: number +} + +function ResolvedCommentEntry({ + thread, + contentLimit = 40, +}: ResolvedCommentEntryProps) { + const { t } = useTranslation() + const { permissions } = useReviewPanelValueContext() + const [isCollapsed, setIsCollapsed] = useState(false) + const needsCollapsing = thread.content.length > contentLimit + const content = isCollapsed + ? thread.content.substring(0, contentLimit) + : thread.content + + const handleUnresolve = () => { + // TODO unresolve comment + } + + const handleDelete = () => { + // TODO delete thread + } + + return ( +
+
+
+ {t('quoted_text_in')} +   + + {thread.docName} + +

+ {content} +

+ {needsCollapsing && ( + <> +   + + + )} +
+ {thread.messages.map((comment, index) => { + const showUser = + index === 0 || + comment.user.id !== thread.messages[index - 1].user.id + + return ( +
+

+ {showUser && ( + + {comment.user.name}:  + + )} + + {comment.content} + +

+
+ {formatTime(comment.timestamp, 'MMM d, y h:mm a')} +
+
+ ) + })} +
+

+ + {thread.resolved_by_user.name}:  + + {t('mark_as_resolved')}. +

+
+ {formatTime(thread.resolved_at, 'MMM d, y h:mm a')} +
+
+
+ {permissions.comment && permissions.write && ( +
+ + +
+ )} +
+ ) +} + +export default ResolvedCommentEntry diff --git a/services/web/frontend/js/features/source-editor/components/review-panel/toolbar/resolved-comments-dropdown.tsx b/services/web/frontend/js/features/source-editor/components/review-panel/toolbar/resolved-comments-dropdown.tsx new file mode 100644 index 0000000000..2f8fe9ebb2 --- /dev/null +++ b/services/web/frontend/js/features/source-editor/components/review-panel/toolbar/resolved-comments-dropdown.tsx @@ -0,0 +1,81 @@ +import { useTranslation } from 'react-i18next' +import { useState } from 'react' +import Icon from '../../../../../shared/components/icon' +import Tooltip from '../../../../../shared/components/tooltip' +import ResolvedCommentsScroller from './resolved-comments-scroller' +import classnames from 'classnames' + +function ResolvedCommentsDropdown() { + const { t } = useTranslation() + const [isOpen, setIsOpen] = useState(false) + // TODO setIsLoading + // eslint-disable-next-line no-unused-vars + const [isLoading, setIsLoading] = useState(false) + + return ( +
+ + ) +} + +export default ResolvedCommentsDropdown diff --git a/services/web/frontend/js/features/source-editor/components/review-panel/toolbar/resolved-comments-scroller.tsx b/services/web/frontend/js/features/source-editor/components/review-panel/toolbar/resolved-comments-scroller.tsx new file mode 100644 index 0000000000..d5ab010750 --- /dev/null +++ b/services/web/frontend/js/features/source-editor/components/review-panel/toolbar/resolved-comments-scroller.tsx @@ -0,0 +1,54 @@ +import { useTranslation } from 'react-i18next' +import { useMemo } from 'react' +import ResolvedCommentEntry from './resolved-comment-entry' +import moment from 'moment' + +type ResolvedCommentsScrollerProps = { + resolvedComments: Array<{ + resolved_at: number + entryId: string + docName: string + content: string + messages: Array<{ + id: string + user: { + id: string + hue: string + name: string + } + content: string + timestamp: string + }> + resolved_by_user: { + name: string + hue: string + } + }> // TODO extract type +} + +function ResolvedCommentsScroller({ + resolvedComments, +}: ResolvedCommentsScrollerProps) { + const { t } = useTranslation() + + // TODO remove momentjs + const sortedResolvedComments = useMemo(() => { + return [...resolvedComments].sort( + (a, b) => + moment(b.resolved_at).valueOf() - moment(a.resolved_at).valueOf() + ) + }, [resolvedComments]) + + return ( +
+ {sortedResolvedComments.map(comment => ( + + ))} + {!resolvedComments.length && ( +
{t('no_resolved_threads')}
+ )} +
+ ) +} + +export default ResolvedCommentsScroller diff --git a/services/web/frontend/js/features/source-editor/components/review-panel/toolbar/toggle-menu.tsx b/services/web/frontend/js/features/source-editor/components/review-panel/toolbar/toggle-menu.tsx new file mode 100644 index 0000000000..d5e4ad726f --- /dev/null +++ b/services/web/frontend/js/features/source-editor/components/review-panel/toolbar/toggle-menu.tsx @@ -0,0 +1,184 @@ +import { useEffect, useRef } from 'react' +import { Trans, useTranslation } from 'react-i18next' +import Tooltip from '../../../../../shared/components/tooltip' +import Icon from '../../../../../shared/components/icon' +import TrackChangesToggle from './track-changes-toggle' +import { useProjectContext } from '../../../../../shared/context/project-context' +import { + useReviewPanelUpdaterFnsContext, + useReviewPanelValueContext, +} from '../../../context/review-panel/review-panel-context' +import classnames from 'classnames' + +function ToggleMenu() { + const { t } = useTranslation() + const project = useProjectContext() + const { setShouldCollapse } = useReviewPanelUpdaterFnsContext() + const { + permissions, + wantTrackChanges, + shouldCollapse, + toggleTrackChangesForEveryone, + toggleTrackChangesForUser, + toggleTrackChangesForGuests, + trackChangesState, + trackChangesOnForEveryone, + trackChangesOnForGuests, + trackChangesForGuestsAvailable, + formattedProjectMembers, + } = useReviewPanelValueContext() + + const containerRef = useRef(null) + + useEffect(() => { + if (containerRef.current) { + const neededHeight = containerRef.current.scrollHeight + + if (neededHeight > 0) { + const height = shouldCollapse ? 0 : neededHeight + containerRef.current.style.height = `${height}px` + } else { + if (shouldCollapse) { + containerRef.current.style.height = '0' + } + } + } + }, [shouldCollapse]) + + return ( + <> + + {wantTrackChanges && ( + + + + )} + + + + +
    +
  • + + {t('tc_everyone')} + + + + toggleTrackChangesForEveryone(!trackChangesOnForEveryone) + } + value={trackChangesOnForEveryone} + disabled={!project.features.trackChanges || !permissions.write} + /> +
  • + {Object.values(formattedProjectMembers).map(member => ( +
  • + + + {member.name} + + + + + toggleTrackChangesForUser( + !trackChangesState[member.id].value, + member.id + ) + } + value={trackChangesState[member.id].value} + disabled={ + trackChangesOnForEveryone || + !project.features.trackChanges || + !permissions.write + } + /> +
  • + ))} +
  • +
  • + + + {t('tc_guests')} + + + + + toggleTrackChangesForGuests(!trackChangesOnForGuests) + } + value={trackChangesOnForGuests} + disabled={ + trackChangesOnForEveryone || + !project.features.trackChanges || + !permissions.write || + !trackChangesForGuestsAvailable + } + /> +
  • +
+ + ) +} + +export default ToggleMenu diff --git a/services/web/frontend/js/features/source-editor/components/review-panel/toolbar/toolbar.tsx b/services/web/frontend/js/features/source-editor/components/review-panel/toolbar/toolbar.tsx new file mode 100644 index 0000000000..13a8dd30ab --- /dev/null +++ b/services/web/frontend/js/features/source-editor/components/review-panel/toolbar/toolbar.tsx @@ -0,0 +1,13 @@ +import ResolvedCommentsDropdown from './resolved-comments-dropdown' +import ToggleMenu from './toggle-menu' + +function Toolbar() { + return ( +
+ + +
+ ) +} + +export default Toolbar diff --git a/services/web/frontend/js/features/source-editor/components/review-panel/toolbar/track-changes-toggle.tsx b/services/web/frontend/js/features/source-editor/components/review-panel/toolbar/track-changes-toggle.tsx new file mode 100644 index 0000000000..b06d761292 --- /dev/null +++ b/services/web/frontend/js/features/source-editor/components/review-panel/toolbar/track-changes-toggle.tsx @@ -0,0 +1,33 @@ +type TrackChangesToggleProps = { + id: string + description: string + disabled: boolean + handleToggle: () => void + value: boolean +} + +function TrackChangesToggle({ + id, + description, + disabled, + handleToggle, + value, +}: TrackChangesToggleProps) { + return ( +
+ + +
+ ) +} + +export default TrackChangesToggle diff --git a/services/web/frontend/js/features/source-editor/context/review-panel/hooks/use-angular-review-panel-state.ts b/services/web/frontend/js/features/source-editor/context/review-panel/hooks/use-angular-review-panel-state.ts new file mode 100644 index 0000000000..092cd4c41d --- /dev/null +++ b/services/web/frontend/js/features/source-editor/context/review-panel/hooks/use-angular-review-panel-state.ts @@ -0,0 +1,95 @@ +import { useMemo } from 'react' +import useScopeValue from '../../../../../shared/hooks/use-scope-value' +import { ReviewPanelState } from '../types/review-panel-state' +import * as ReviewPanel from '../types/review-panel-state' + +function useAngularReviewPanelState(): ReviewPanelState { + const [subView, setSubView] = useScopeValue>( + 'reviewPanel.subView' + ) + const [collapsed, setCollapsed] = useScopeValue< + ReviewPanel.Value<'collapsed'> + >('reviewPanel.overview.docsCollapsedState') + const [permissions] = + useScopeValue>('permissions') + + const [wantTrackChanges] = useScopeValue< + ReviewPanel.Value<'wantTrackChanges'> + >('editor.wantTrackChanges') + const [shouldCollapse, setShouldCollapse] = useScopeValue< + ReviewPanel.Value<'shouldCollapse'> + >('reviewPanel.fullTCStateCollapsed') + + const [toggleTrackChangesForEveryone] = useScopeValue< + ReviewPanel.Value<'toggleTrackChangesForEveryone'> + >('toggleTrackChangesForEveryone') + const [toggleTrackChangesForUser] = useScopeValue< + ReviewPanel.Value<'toggleTrackChangesForUser'> + >('toggleTrackChangesForUser') + const [toggleTrackChangesForGuests] = useScopeValue< + ReviewPanel.Value<'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 [formattedProjectMembers] = useScopeValue< + ReviewPanel.Value<'formattedProjectMembers'> + >('reviewPanel.formattedProjectMembers') + + const values = useMemo( + () => ({ + collapsed, + permissions, + subView, + shouldCollapse, + wantTrackChanges, + toggleTrackChangesForEveryone, + toggleTrackChangesForUser, + toggleTrackChangesForGuests, + trackChangesState, + trackChangesOnForEveryone, + trackChangesOnForGuests, + trackChangesForGuestsAvailable, + formattedProjectMembers, + }), + [ + collapsed, + permissions, + subView, + shouldCollapse, + wantTrackChanges, + toggleTrackChangesForEveryone, + toggleTrackChangesForUser, + toggleTrackChangesForGuests, + trackChangesState, + trackChangesOnForEveryone, + trackChangesOnForGuests, + trackChangesForGuestsAvailable, + formattedProjectMembers, + ] + ) + + const updaterFns = useMemo( + () => ({ + setSubView, + setCollapsed, + setShouldCollapse, + }), + [setSubView, setCollapsed, setShouldCollapse] + ) + + return { values, updaterFns } +} + +export default useAngularReviewPanelState diff --git a/services/web/frontend/js/features/source-editor/context/review-panel/hooks/use-angular-review-panel.ts b/services/web/frontend/js/features/source-editor/context/review-panel/hooks/use-angular-review-panel.ts deleted file mode 100644 index 44091df1c7..0000000000 --- a/services/web/frontend/js/features/source-editor/context/review-panel/hooks/use-angular-review-panel.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { useMemo } from 'react' -import useScopeValue from '../../../../../shared/hooks/use-scope-value' -import { ReviewPanelState } from '../types/review-panel-state' -import * as ReviewPanel from '../types/review-panel-state' - -function useAngularReviewPanelState(): ReviewPanelState { - const [subView, setSubView] = useScopeValue>( - 'reviewPanel.subView' - ) - const [collapsed, setCollapsed] = useScopeValue< - ReviewPanel.Value<'collapsed'> - >('reviewPanel.overview.docsCollapsedState') - - const values = useMemo( - () => ({ - subView, - collapsed, - }), - [subView, collapsed] - ) - - const updaterFns = useMemo( - () => ({ - setSubView, - setCollapsed, - }), - [setSubView, setCollapsed] - ) - - return { values, updaterFns } -} - -export default useAngularReviewPanelState 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 51858caf47..de82abdca6 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,5 @@ import { createContext, useContext } from 'react' -import useAngularReviewPanelState from './hooks/use-angular-review-panel' +import useAngularReviewPanelState from './hooks/use-angular-review-panel-state' import { ReviewPanelState } from './types/review-panel-state' const ReviewPanelValueContext = createContext< 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 85bc486c6d..bd0428aaba 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,9 +1,27 @@ import { SubView } from '../../../components/review-panel/nav' +import { ReviewPanelPermissions } from '../../../../../../../types/review-panel' export interface ReviewPanelState { values: { collapsed: Record subView: SubView + permissions: ReviewPanelPermissions + shouldCollapse: boolean + wantTrackChanges: boolean + toggleTrackChangesForEveryone: (isOn: boolean) => unknown + toggleTrackChangesForUser: (isOn: boolean, memberId: string) => unknown + toggleTrackChangesForGuests: (isOn: boolean) => unknown + trackChangesState: Record + trackChangesOnForEveryone: boolean + trackChangesOnForGuests: boolean + trackChangesForGuestsAvailable: boolean + formattedProjectMembers: Record< + string, + { + id: string + name: string + } + > } updaterFns: { setCollapsed: React.Dispatch< @@ -12,6 +30,9 @@ export interface ReviewPanelState { setSubView: React.Dispatch< React.SetStateAction > + setShouldCollapse: React.Dispatch< + React.SetStateAction + > } } diff --git a/services/web/frontend/stylesheets/app/editor/review-panel.less b/services/web/frontend/stylesheets/app/editor/review-panel.less index fc864ca0d4..8403971066 100644 --- a/services/web/frontend/stylesheets/app/editor/review-panel.less +++ b/services/web/frontend/stylesheets/app/editor/review-panel.less @@ -66,6 +66,7 @@ #review-panel { display: block; + .rp-size-expanded & { display: flex; flex-direction: column; @@ -73,6 +74,7 @@ overflow: visible; border-left-width: 1px; } + .rp-size-mini & { width: @review-off-width; z-index: 6; @@ -98,6 +100,7 @@ .review-panel-toolbar { display: none; + .rp-size-expanded & { display: flex; align-items: center; @@ -117,22 +120,27 @@ flex-basis: 32px; flex-shrink: 0; } + .review-panel-toolbar-label { cursor: pointer; text-align: right; flex-grow: 1; } + .review-panel-toolbar-icon-on { margin-right: 5px; color: @ol-green; } + .review-panel-toolbar-label-disabled { cursor: auto; margin-right: 5px; } + .review-panel-toolbar-spinner { margin-left: 5px; } + .rp-tc-state { position: absolute; top: 100%; @@ -146,24 +154,30 @@ background-color: @rp-bg-dim-blue; text-align: left; } + .rp-tc-state-collapse { .rp-collapse-arrow; margin-left: 5px; } + .rp-tc-state-item { display: flex; align-items: center; padding: 3px 0; + &:last-of-type { padding-bottom: 5px; } } + .rp-tc-state-separator { border-bottom: 1px solid @rp-border-grey; } + .rp-tc-state-item-everyone { border-bottom: 1px solid @rp-border-grey; } + .rp-tc-state-item-name { flex-grow: 1; overflow: hidden; @@ -171,6 +185,7 @@ white-space: nowrap; font-weight: @rp-semibold-weight; } + .rp-tc-state-item-name-disabled { opacity: 0.35; } @@ -216,9 +231,11 @@ .rp-size-mini & { display: block; } + .rp-size-mini &-add-comment { display: none; } + position: absolute; left: 2px; right: 2px; @@ -228,6 +245,7 @@ color: #fff; cursor: pointer; transition: top @rp-entry-animation-speed, left 0.1s, right 0.1s; + .no-animate & { transition: none; } @@ -257,6 +275,7 @@ position: absolute; width: @review-panel-width; } + .rp-state-current-file-mini & { display: none; left: @review-off-width + @rp-entry-arrow-width; @@ -272,11 +291,12 @@ left: -(2 * @rp-entry-arrow-width + 2); z-index: -1; } + &::after { .triangle( left, @rp-entry-arrow-width, - @rp-entry-arrow-width * 1.5, + @rp-entry-arrow-width * 1.5, inherit ); top: (@review-off-width / 2) - @rp-entry-arrow-width; @@ -284,6 +304,7 @@ content: ''; } } + .rp-state-current-file-mini.rp-layout-left & { left: auto; right: @review-off-width + @rp-entry-arrow-width; @@ -295,17 +316,19 @@ left: -(@review-off-width + @rp-entry-arrow-width); right: -(2 * @rp-entry-arrow-width + 2); } + &::after { .triangle( right, @rp-entry-arrow-width, - @rp-entry-arrow-width * 1.5, + @rp-entry-arrow-width * 1.5, inherit ); right: -(@rp-entry-ribbon-width + @rp-entry-arrow-width); left: auto; } } + .rp-state-current-file-expanded & { visibility: hidden; left: 5px; @@ -325,15 +348,18 @@ right: 5px; } } + &-bulk-actions { right: auto; } } + .rp-state-overview & { border-radius: 0; border-bottom: solid 1px @rp-border-grey; cursor: pointer; } + .resolved-comments-dropdown & { position: static; margin-bottom: 5px; @@ -343,6 +369,7 @@ border-radius: 3px; background-color: #fff; transition: top @rp-entry-animation-speed, left 0.1s, right 0.1s; + .no-animate & { transition: none; } @@ -395,11 +422,13 @@ border-left-width: 0; } } + .rp-entry-body { display: flex; align-items: center; padding: 4px 5px; } + .rp-entry-action-icon { font-size: @rp-icon-large-size; padding: 0 3px; @@ -426,10 +455,12 @@ .rp-entry-metadata { font-size: @rp-small-font-size; } + .rp-entry-user { font-weight: @rp-semibold-weight; font-style: normal; } + .rp-comment-actions { a { color: @rp-type-blue; @@ -453,6 +484,7 @@ display: none; } } + .rp-entry-button { .rp-button(); flex: 1 1 50%; @@ -468,6 +500,7 @@ &:first-child { border-bottom-left-radius: 3px; } + &:last-child { border-bottom-right-radius: 0; } @@ -494,6 +527,7 @@ } } } + .rp-comment-content { margin: 0; color: @rp-type-darkgrey; @@ -504,6 +538,7 @@ .rp-comment-resolver { color: @rp-type-blue; } + .rp-comment-resolver-content { font-style: italic; margin: 0; @@ -533,10 +568,12 @@ .rp-bulk-actions-btn { border-radius: 0; + &:first-child { border-top-left-radius: 3px; border-bottom-left-radius: 3px; } + &:last-child { border-top-right-radius: 3px; border-bottom-right-radius: 3px; @@ -570,6 +607,7 @@ font-size: 0.8em; text-decoration: line-through; font-weight: @rp-semibold-weight; + &::before { content: 'Ab'; } @@ -581,10 +619,12 @@ background-color: #fff; margin-bottom: 5px; } + .rp-resolved-comment-context { background-color: lighten(@rp-yellow, 35%); padding: 4px 5px; } + .rp-resolved-comment-context-file { font-weight: @rp-semibold-weight; } @@ -597,10 +637,12 @@ .rp-entry-callout { transition: top @rp-entry-animation-speed, height @rp-entry-animation-speed; + .rp-state-current-file & { position: absolute; border-top: 1px solid grey; border-right: 1px dashed grey; + &::after { content: ''; position: absolute; @@ -610,14 +652,18 @@ border-bottom: 1px solid grey; } } + .rp-state-current-file-expanded & { width: 3px; + &::after { width: 3px; } } + .rp-state-current-file-mini & { width: 1px; + &::after { width: 1px; } @@ -630,6 +676,7 @@ .rp-state-current-file &-inverted { border-top: none; border-bottom: 1px solid grey; + &::after { top: 0px; bottom: -1px; @@ -640,6 +687,7 @@ .rp-state-current-file &-insert { border-color: @rp-green; + &::after { border-color: @rp-green; } @@ -647,6 +695,7 @@ .rp-state-current-file &-delete { border-color: @rp-red; + &::after { border-color: @rp-red; } @@ -654,6 +703,7 @@ .rp-state-current-file &-comment { border-color: @rp-yellow; + &::after { border-color: @rp-yellow; } @@ -679,10 +729,12 @@ font-weight: normal; font-size: 0.9em; } + .rp-overview-file-header-collapse { .rp-collapse-arrow; float: left; } + .rp-overview-file-entries { overflow: hidden; } @@ -704,6 +756,7 @@ .rp-nav { display: none; flex-shrink: 0; + .rp-size-expanded & { display: flex; } @@ -716,6 +769,7 @@ position: absolute; bottom: 0; } + width: 100%; font-size: @rp-icon-large-size; text-align: center; @@ -723,6 +777,7 @@ border-top: solid 1px @rp-border-grey; z-index: 2; } + .rp-nav-item { display: block; color: lighten(@rp-type-blue, 25%); @@ -741,6 +796,7 @@ border-top: solid 3px @rp-highlight-blue; } } + .rp-nav-label { display: block; font-size: @rp-base-font-size; @@ -749,8 +805,10 @@ #editor { .rp-size-mini & { right: @review-off-width; + .ace-editor-body { overflow: visible; + .ace_scrollbar-v { right: -@review-off-width; } @@ -766,6 +824,7 @@ .rp-state-current-file-expanded & { .ace-editor-body { overflow: visible; + .ace_scrollbar-v { right: -@review-panel-width; } @@ -785,6 +844,7 @@ .rp-unsupported-msg-wrapper { display: none; + .rp-size-expanded.rp-unsupported & { display: block; } @@ -812,18 +872,22 @@ .track-changes-marker-callout { border-radius: 0; position: absolute; + .rp-state-overview &, .rp-loading-threads & { display: none; } } + .track-changes-added-marker-callout { border-bottom: 1px dashed @rp-green; z-index: 1; } + .track-changes-comment-marker-callout { border-bottom: 1px dashed @rp-yellow; } + .track-changes-deleted-marker-callout { border-bottom: 1px dashed @rp-red; } @@ -831,9 +895,11 @@ .track-changes-marker { border-radius: 0; position: absolute; + .rp-loading-threads & { display: none; } + z-index: 6; // Appear above text selection } @@ -841,10 +907,12 @@ background-color: @rp-yellow; opacity: 0.3; } + .track-changes-added-marker { background-color: @rp-green; opacity: 0.3; } + .track-changes-deleted-marker { border-left: 2px dotted @rp-red; margin-left: -1px; @@ -854,6 +922,7 @@ .track-changes-comment-marker { background-color: @rp-yellow-on-dark; } + .track-changes-added-marker { background-color: @rp-green-on-dark; } @@ -975,8 +1044,8 @@ button when (@is-overleaf-light = true) { content: ''; .triangle( top, - @rp-entry-arrow-width * 3, - @rp-entry-arrow-width * 1.5, + @rp-entry-arrow-width * 3, + @rp-entry-arrow-width * 1.5, @rp-bg-blue ); top: -@rp-entry-ribbon-width * 2; @@ -988,6 +1057,7 @@ button when (@is-overleaf-light = true) { display: flex; } } + .resolved-comments-scroller { flex: 0 0 auto; // Can't use 100% in the flex-basis key here, IE won't account for padding. width: 100%; // We need to set the width explicitly, as flex-basis won't work. @@ -1021,11 +1091,13 @@ button when (@is-overleaf-light = true) { .rp-size-mini & { right: @review-off-width; } + .rp-size-expanded &, .rp-unsupported & { display: none; } } + .rp-track-changes-indicator { display: block; padding: 5px 10px; @@ -1103,6 +1175,7 @@ button when (@is-overleaf-light = true) { margin-top: -0.5em; } } + // Helper class for elements which aren't treated as flex-items by IE10, e.g: // * inline items; // * unknown elements (elements which aren't standard DOM elements, such as custom element directives) @@ -1220,4 +1293,9 @@ button when (@is-overleaf-light = true) { } } } + + .rp-tc-state { + height: 0; + transition: height 150ms; + } } diff --git a/services/web/locales/en.json b/services/web/locales/en.json index 72935816a1..0385287732 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -1638,6 +1638,8 @@ "tr": "Turkish", "track_any_change_in_real_time": "Track any change, in real-time", "track_changes": "Track changes", + "track_changes_for_everyone": "Track changes for everyone", + "track_changes_for_x": "Track changes for __name__", "track_changes_is_off": "Track changes is off", "track_changes_is_on": "Track changes is on", "tracked_change_added": "Added", diff --git a/services/web/package.json b/services/web/package.json index 0cd9674e7f..6d1a13a110 100644 --- a/services/web/package.json +++ b/services/web/package.json @@ -272,6 +272,7 @@ "@types/react-bootstrap": "^0.32.29", "@types/react-color": "^3.0.6", "@types/react-dom": "^17.0.13", + "@types/react-linkify": "^1.0.0", "@types/recurly__recurly-js": "^4.22.0", "@types/sinon-chai": "^3.2.8", "@types/uuid": "^8.3.4", diff --git a/services/web/test/frontend/features/review-panel/review-panel.spec.tsx b/services/web/test/frontend/features/review-panel/review-panel.spec.tsx new file mode 100644 index 0000000000..190b70920d --- /dev/null +++ b/services/web/test/frontend/features/review-panel/review-panel.spec.tsx @@ -0,0 +1,121 @@ +import CodeMirrorEditor from '../../../../frontend/js/features/source-editor/components/codemirror-editor' +import { EditorProviders } from '../../helpers/editor-providers' +import { mockScope } from '../source-editor/helpers/mock-scope' + +type ContainerProps = { + children: React.ReactNode + className?: string +} + +function Container(props: ContainerProps) { + return
+} + +describe('', function () { + beforeEach(function () { + window.metaAttributesCache.set('ol-isReviewPanelReact', true) + window.metaAttributesCache.set('ol-preventCompileOnLoad', true) + + cy.interceptEvents() + cy.interceptSpelling() + + const scope = mockScope('') + scope.editor.showVisual = true + + // Making a shallow copy, otherwise cannot spy on an object declared outside the test file + const newScope = { ...scope } + cy.spy(newScope, 'toggleTrackChangesForEveryone').as( + 'toggleTrackChangesForEveryone' + ) + + cy.mount( + + + + + + ) + + cy.findByTestId('review-panel').as('review-panel') + }) + + describe('toolbar', function () { + describe('resolved comments dropdown', function () { + it('renders dropdown button', function () { + cy.findByRole('button', { name: /resolved comments/i }) + }) + + // eslint-disable-next-line mocha/no-skipped-tests + it.skip('opens dropdown', function () {}) + + // eslint-disable-next-line mocha/no-skipped-tests + it.skip('renders list of resolved comments', function () {}) + + // eslint-disable-next-line mocha/no-skipped-tests + it.skip('reopens resolved comment', function () {}) + + // eslint-disable-next-line mocha/no-skipped-tests + it.skip('deletes resolved comment', function () {}) + }) + + describe('track changes toggle menu', function () { + it('renders track changes toolbar', function () { + cy.get('@review-panel').within(() => { + cy.findByRole('button', { name: /track changes is (on|off)$/i }) + }) + }) + + it('opens/closes toggle menu', function () { + cy.get('@review-panel').within(() => { + cy.findByTestId('review-panel-track-changes-menu').as('menu') + cy.get('@menu').should('have.css', 'height', '1px') + cy.findByRole('button', { name: /track changes is/i }).click() + // verify the menu is expanded + cy.get('@menu') + .then($el => { + const height = window + .getComputedStyle($el[0]) + .getPropertyValue('height') + return parseFloat(height) + }) + .should('be.gt', 1) + cy.findByRole('button', { name: /track changes is/i }).click() + cy.get('@menu').should('have.css', 'height', '1px') + }) + }) + + it('toggles the "everyone" track changes switch', function () { + cy.get('@review-panel').within(() => { + cy.findByRole('button', { name: /track changes is off/i }).click() + cy.findByLabelText(/track changes for everyone/i).click({ + force: true, + }) + cy.get('@toggleTrackChangesForEveryone').should('be.calledOnce') + }) + }) + + it('renders track changes with "on" state', function () { + const scope = mockScope('') + scope.editor.showVisual = true + scope.editor.wantTrackChanges = true + + cy.mount( + + + + + + ) + + cy.findByTestId('review-panel').within(() => { + cy.findByRole('button', { name: /track changes is on/i }).click() + }) + }) + + it('renders a disabled guests switch', function () { + cy.findByRole('button', { name: /track changes is off/i }).click() + cy.findByLabelText(/track changes for guests/i).should('be.disabled') + }) + }) + }) +}) diff --git a/services/web/test/frontend/features/source-editor/helpers/mock-scope.ts b/services/web/test/frontend/features/source-editor/helpers/mock-scope.ts index f9e4d1b3ff..341714df9e 100644 --- a/services/web/test/frontend/features/source-editor/helpers/mock-scope.ts +++ b/services/web/test/frontend/features/source-editor/helpers/mock-scope.ts @@ -1,5 +1,6 @@ import { docId, mockDoc } from './mock-doc' import { Folder } from '../../../../../types/folder' + export const rootFolderId = '012345678901234567890123' export const figuresFolderId = '123456789012345678901234' export const figureId = '234567890123456789012345' @@ -55,6 +56,7 @@ export const mockScope = (content?: string) => { open_doc_name: 'test.tex', open_doc_id: docId, showVisual: false, + wantTrackChanges: false, }, pdf: { logEntryAnnotations: {}, @@ -64,7 +66,23 @@ export const mockScope = (content?: string) => { name: 'Test Project', spellCheckLanguage: 'en', rootFolder: [] as Folder[], + features: { + trackChanges: true, + }, }, + permissions: { + comment: true, + write: true, + }, + reviewPanel: { + subView: 'cur_file', + formattedProjectMembers: {}, + fullTCStateCollapsed: true, + }, + ui: { + reviewPanelOpen: true, + }, + toggleTrackChangesForEveryone() {}, onlineUserCursorHighlights: {}, permissionsLevel: 'owner', $on: cy.stub(), diff --git a/services/web/types/review-panel.ts b/services/web/types/review-panel.ts new file mode 100644 index 0000000000..f768b477c4 --- /dev/null +++ b/services/web/types/review-panel.ts @@ -0,0 +1,6 @@ +export interface ReviewPanelPermissions { + read: boolean + write: boolean + admin: boolean + comment: boolean +}