mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-07 20:31:06 -05:00
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:
parent
ea59a98386
commit
e514e97305
21 changed files with 933 additions and 41 deletions
20
package-lock.json
generated
20
package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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": "",
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1 +1,7 @@
|
|||
export type SubView = 'cur_file' | 'overview'
|
||||
|
||||
function Nav() {
|
||||
return <div style={{ background: '#bbb', padding: '10px' }}>Nav</div>
|
||||
}
|
||||
|
||||
export default Nav
|
||||
|
|
|
@ -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')}
|
||||
|
||||
<span className="rp-resolved-comment-context-file">
|
||||
{thread.docName}
|
||||
</span>
|
||||
<p className="rp-resolved-comment-context-quote">
|
||||
<span>{content}</span>
|
||||
</p>
|
||||
{needsCollapsing && (
|
||||
<>
|
||||
|
||||
<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}:
|
||||
</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}:
|
||||
</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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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<
|
||||
|
|
|
@ -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']>
|
||||
>
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,6 +291,7 @@
|
|||
left: -(2 * @rp-entry-arrow-width + 2);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
&::after {
|
||||
.triangle(
|
||||
left,
|
||||
|
@ -284,6 +304,7 @@
|
|||
content: '';
|
||||
}
|
||||
}
|
||||
|
||||
.rp-state-current-file-mini.rp-layout-left & {
|
||||
left: auto;
|
||||
right: @review-off-width + @rp-entry-arrow-width;
|
||||
|
@ -295,6 +316,7 @@
|
|||
left: -(@review-off-width + @rp-entry-arrow-width);
|
||||
right: -(2 * @rp-entry-arrow-width + 2);
|
||||
}
|
||||
|
||||
&::after {
|
||||
.triangle(
|
||||
right,
|
||||
|
@ -306,6 +328,7 @@
|
|||
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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -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(),
|
||||
|
|
6
services/web/types/review-panel.ts
Normal file
6
services/web/types/review-panel.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
export interface ReviewPanelPermissions {
|
||||
read: boolean
|
||||
write: boolean
|
||||
admin: boolean
|
||||
comment: boolean
|
||||
}
|
Loading…
Reference in a new issue