Merge pull request #13438 from overleaf/ii-review-panel-toolbar-shell

[web] Create review panel toolbar shell

GitOrigin-RevId: 561fb18e1239c9b96b52944716a83cf3b8606677
This commit is contained in:
ilkin-overleaf 2023-06-14 11:29:24 +03:00 committed by Copybot
parent ea59a98386
commit e514e97305
21 changed files with 933 additions and 41 deletions

20
package-lock.json generated
View file

@ -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",

View file

@ -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": "",

View file

@ -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 (
<div className={classnames(...reviewPanelClasses, classNames)} {...rest}>
<div
className={classnames(...reviewPanelClasses, classNames)}
{...rest}
data-testid="review-panel"
>
<Toolbar />
{children}
<Nav />
</div>
)
}

View file

@ -3,8 +3,34 @@ import Container from './container'
function CurrentFileContainer() {
return (
<Container>
<em>ReviewPanelCurrentFileContainer</em>
<div className="review-panel-tools">Tools</div>
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.
</Container>
)
}

View file

@ -1 +1,7 @@
export type SubView = 'cur_file' | 'overview'
function Nav() {
return <div style={{ background: '#bbb', padding: '10px' }}>Nav</div>
}
export default Nav

View file

@ -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 (
<a target="blank" rel="noreferrer noopener" href={decoratedHref} key={key}>
{decoratedText}
</a>
)
}
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 (
<div className="rp-resolved-comment">
<div>
<div className="rp-resolved-comment-context">
{t('quoted_text_in')}
&nbsp;
<span className="rp-resolved-comment-context-file">
{thread.docName}
</span>
<p className="rp-resolved-comment-context-quote">
<span>{content}</span>
</p>
{needsCollapsing && (
<>
&nbsp;
<button
className="rp-collapse-toggle btn-inline-link"
onClick={() => setIsCollapsed(value => !value)}
>
{isCollapsed ? `… (${t('show_all')})` : ` (${t('show_less')})`}
</button>
</>
)}
</div>
{thread.messages.map((comment, index) => {
const showUser =
index === 0 ||
comment.user.id !== thread.messages[index - 1].user.id
return (
<div className="rp-comment" key={comment.id}>
<p className="rp-comment-content">
{showUser && (
<span
className="rp-entry-user"
style={{ color: `hsl(${comment.user.hue}, 70%, 40%)` }}
>
{comment.user.name}:&nbsp;
</span>
)}
<Linkify componentDecorator={LinkDecorator}>
{comment.content}
</Linkify>
</p>
<div className="rp-entry-metadata">
{formatTime(comment.timestamp, 'MMM d, y h:mm a')}
</div>
</div>
)
})}
<div className="rp-comment rp-comment-resolver">
<p className="rp-comment-resolver-content">
<span
className="rp-entry-user"
style={{ color: `hsl(${thread.resolved_by_user.hue}, 70%, 40%)` }}
>
{thread.resolved_by_user.name}:&nbsp;
</span>
{t('mark_as_resolved')}.
</p>
<div className="rp-entry-metadata">
{formatTime(thread.resolved_at, 'MMM d, y h:mm a')}
</div>
</div>
</div>
{permissions.comment && permissions.write && (
<div className="rp-entry-actions">
<button className="rp-entry-button" onClick={handleUnresolve}>
{t('reopen')}
</button>
<button className="rp-entry-button" onClick={handleDelete}>
{t('delete')}
</button>
</div>
)}
</div>
)
}
export default ResolvedCommentEntry

View file

@ -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 (
<div className="resolved-comments">
<div
aria-hidden="true"
className={classnames('resolved-comments-backdrop', {
'resolved-comments-backdrop-visible': isOpen,
})}
onClick={() => setIsOpen(false)}
/>
<Tooltip
id="resolved-comments-toggle"
description={t('resolved_comments')}
overlayProps={{ container: document.body, placement: 'bottom' }}
>
<button
className="resolved-comments-toggle"
onClick={() => setIsOpen(value => !value)}
aria-label={t('resolved_comments')}
>
<Icon type="inbox" />
</button>
</Tooltip>
<div
className={classnames('resolved-comments-dropdown', {
'resolved-comments-dropdown-open': isOpen,
})}
>
{isLoading ? (
<div className="rp-loading">
<Icon type="spinner" spin />
</div>
) : (
<ResolvedCommentsScroller
resolvedComments={[
{
resolved_at: 12345,
entryId: '123',
docName: 'demo name',
content: 'demo content',
messages: [
{
id: '123',
user: {
id: '123',
hue: 'abcde',
name: 'demo name',
},
content: 'demo content',
timestamp: '12345',
},
],
resolved_by_user: {
name: 'demo',
hue: 'abcde',
},
},
]}
/>
)}
</div>
</div>
)
}
export default ResolvedCommentsDropdown

View file

@ -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 (
<div className="resolved-comments-scroller">
{sortedResolvedComments.map(comment => (
<ResolvedCommentEntry key={comment.entryId} thread={comment} />
))}
{!resolvedComments.length && (
<div className="rp-loading">{t('no_resolved_threads')}</div>
)}
</div>
)
}
export default ResolvedCommentsScroller

View file

@ -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<HTMLUListElement | null>(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 (
<>
<span className="review-panel-toolbar-label">
{wantTrackChanges && (
<span className="review-panel-toolbar-icon-on">
<Icon type="circle" />
</span>
)}
<button
className="review-panel-toolbar-collapse-button"
onClick={() => setShouldCollapse(value => !value)}
>
{wantTrackChanges ? (
// eslint-disable-next-line react/jsx-key
<Trans i18nKey="track_changes_is_on" components={[<strong />]} />
) : (
// eslint-disable-next-line react/jsx-key
<Trans i18nKey="track_changes_is_off" components={[<strong />]} />
)}
<span
className={classnames('rp-tc-state-collapse', {
'rp-tc-state-collapse-on': shouldCollapse,
})}
>
<Icon type="angle-down" />
</span>
</button>
</span>
<ul
className="rp-tc-state"
ref={containerRef}
data-testid="review-panel-track-changes-menu"
>
<li className="rp-tc-state-item rp-tc-state-item-everyone">
<Tooltip
description={t('tc_switch_everyone_tip')}
id="track-changes-switch-everyone"
overlayProps={{
container: document.body,
placement: 'left',
delay: 1000,
}}
>
<span className="rp-tc-state-item-name">{t('tc_everyone')}</span>
</Tooltip>
<TrackChangesToggle
id="track-changes-everyone"
description={t('track_changes_for_everyone')}
handleToggle={() =>
toggleTrackChangesForEveryone(!trackChangesOnForEveryone)
}
value={trackChangesOnForEveryone}
disabled={!project.features.trackChanges || !permissions.write}
/>
</li>
{Object.values(formattedProjectMembers).map(member => (
<li className="rp-tc-state-item" key={member.id}>
<Tooltip
description={t('tc_switch_user_tip')}
id="track-changes-switch-user"
overlayProps={{
container: document.body,
placement: 'left',
delay: 1000,
}}
>
<span
className={classnames('rp-tc-state-item-name', {
'rp-tc-state-item-name-disabled': trackChangesOnForEveryone,
})}
>
{member.name}
</span>
</Tooltip>
<TrackChangesToggle
id={`track-changes-user-toggle-${member.id}`}
description={t('track_changes_for_x', { name: member.name })}
handleToggle={() =>
toggleTrackChangesForUser(
!trackChangesState[member.id].value,
member.id
)
}
value={trackChangesState[member.id].value}
disabled={
trackChangesOnForEveryone ||
!project.features.trackChanges ||
!permissions.write
}
/>
</li>
))}
<li className="rp-tc-state-separator" />
<li className="rp-tc-state-item">
<Tooltip
description={t('tc_switch_guests_tip')}
id="track-changes-switch-guests"
overlayProps={{
container: document.body,
placement: 'left',
delay: 1000,
}}
>
<span
className={classnames('rp-tc-state-item-name', {
'rp-tc-state-item-name-disabled': trackChangesOnForEveryone,
})}
>
{t('tc_guests')}
</span>
</Tooltip>
<TrackChangesToggle
id="track-changes-guests-toggle"
description="Track changes for guests"
handleToggle={() =>
toggleTrackChangesForGuests(!trackChangesOnForGuests)
}
value={trackChangesOnForGuests}
disabled={
trackChangesOnForEveryone ||
!project.features.trackChanges ||
!permissions.write ||
!trackChangesForGuestsAvailable
}
/>
</li>
</ul>
</>
)
}
export default ToggleMenu

View file

@ -0,0 +1,13 @@
import ResolvedCommentsDropdown from './resolved-comments-dropdown'
import ToggleMenu from './toggle-menu'
function Toolbar() {
return (
<div className="review-panel-toolbar">
<ResolvedCommentsDropdown />
<ToggleMenu />
</div>
)
}
export default Toolbar

View file

@ -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 (
<div className="input-switch">
<input
id={`input-switch-${id}`}
disabled={disabled}
type="checkbox"
className="input-switch-hidden-input"
onChange={handleToggle}
checked={value}
/>
<label htmlFor={`input-switch-${id}`} className="input-switch-btn">
<span className="sr-only">{description}</span>
</label>
</div>
)
}
export default TrackChangesToggle

View file

@ -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.Value<'subView'>>(
'reviewPanel.subView'
)
const [collapsed, setCollapsed] = useScopeValue<
ReviewPanel.Value<'collapsed'>
>('reviewPanel.overview.docsCollapsedState')
const [permissions] =
useScopeValue<ReviewPanel.Value<'permissions'>>('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<ReviewPanelState['values']>(
() => ({
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<ReviewPanelState['updaterFns']>(
() => ({
setSubView,
setCollapsed,
setShouldCollapse,
}),
[setSubView, setCollapsed, setShouldCollapse]
)
return { values, updaterFns }
}
export default useAngularReviewPanelState

View file

@ -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.Value<'subView'>>(
'reviewPanel.subView'
)
const [collapsed, setCollapsed] = useScopeValue<
ReviewPanel.Value<'collapsed'>
>('reviewPanel.overview.docsCollapsedState')
const values = useMemo<ReviewPanelState['values']>(
() => ({
subView,
collapsed,
}),
[subView, collapsed]
)
const updaterFns = useMemo<ReviewPanelState['updaterFns']>(
() => ({
setSubView,
setCollapsed,
}),
[setSubView, setCollapsed]
)
return { values, updaterFns }
}
export default useAngularReviewPanelState

View file

@ -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<

View file

@ -1,9 +1,27 @@
import { SubView } from '../../../components/review-panel/nav'
import { ReviewPanelPermissions } from '../../../../../../../types/review-panel'
export interface ReviewPanelState {
values: {
collapsed: Record<string, boolean>
subView: SubView
permissions: ReviewPanelPermissions
shouldCollapse: boolean
wantTrackChanges: boolean
toggleTrackChangesForEveryone: (isOn: boolean) => unknown
toggleTrackChangesForUser: (isOn: boolean, memberId: string) => unknown
toggleTrackChangesForGuests: (isOn: boolean) => unknown
trackChangesState: Record<string, { value: boolean; syncState: string }>
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<ReviewPanelState['values']['subView']>
>
setShouldCollapse: React.Dispatch<
React.SetStateAction<ReviewPanelState['values']['shouldCollapse']>
>
}
}

View file

@ -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;
}
}

View file

@ -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 <strong>off</strong>",
"track_changes_is_on": "Track changes is <strong>on</strong>",
"tracked_change_added": "Added",

View file

@ -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",

View file

@ -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 <div style={{ width: 785, height: 785 }} {...props} />
}
describe('<ReviewPanel />', 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(
<Container className="rp-size-expanded">
<EditorProviders scope={newScope}>
<CodeMirrorEditor />
</EditorProviders>
</Container>
)
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(
<Container className="rp-size-expanded">
<EditorProviders scope={scope}>
<CodeMirrorEditor />
</EditorProviders>
</Container>
)
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')
})
})
})
})

View file

@ -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(),

View file

@ -0,0 +1,6 @@
export interface ReviewPanelPermissions {
read: boolean
write: boolean
admin: boolean
comment: boolean
}