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 && (
+ <>
+
+
setIsCollapsed(value => !value)}
+ >
+ {isCollapsed ? `… (${t('show_all')})` : ` (${t('show_less')})`}
+
+ >
+ )}
+
+ {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 && (
+
+
+ {t('reopen')}
+
+
+ {t('delete')}
+
+
+ )}
+
+ )
+}
+
+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 (
+
+
setIsOpen(false)}
+ />
+
+
+
+
+ {isLoading ? (
+
+
+
+ ) : (
+
+ )}
+
+
+ )
+}
+
+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 && (
+
+
+
+ )}
+
+ setShouldCollapse(value => !value)}
+ >
+ {wantTrackChanges ? (
+ // eslint-disable-next-line react/jsx-key
+ ]} />
+ ) : (
+ // eslint-disable-next-line react/jsx-key
+ ]} />
+ )}
+
+
+
+
+
+
+
+
+
+ {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 (
+
+
+
+ {description}
+
+
+ )
+}
+
+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
+}