Merge pull request #21240 from overleaf/ii-bs5-review-panel-old

[web] BS5 review panel old

GitOrigin-RevId: da018b8f2946afb21ab63da0003453e20781f04c
This commit is contained in:
ilkin-overleaf 2024-10-23 16:21:00 +03:00 committed by Copybot
parent 92a23b7e9d
commit 5c3d9117c5
25 changed files with 1594 additions and 430 deletions

View file

@ -16,6 +16,8 @@ import useScopeEventListener from '@/shared/hooks/use-scope-event-listener'
import { memo, useCallback } from 'react' import { memo, useCallback } from 'react'
import { useLayoutContext } from '@/shared/context/layout-context' import { useLayoutContext } from '@/shared/context/layout-context'
import classnames from 'classnames' import classnames from 'classnames'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
import MaterialIcon from '@/shared/components/material-icon'
function EditorWidgets() { function EditorWidgets() {
const { t } = useTranslation() const { t } = useTranslation()
@ -71,10 +73,20 @@ function EditorWidgets() {
{nChanges > 1 && permissions.write && ( {nChanges > 1 && permissions.write && (
<> <>
<BulkActions.Button onClick={handleShowBulkAcceptDialog}> <BulkActions.Button onClick={handleShowBulkAcceptDialog}>
<Icon type="check" /> {t('accept_all')} ({nChanges}) <BootstrapVersionSwitcher
bs3={<Icon type="check" />}
bs5={<MaterialIcon type="check" />}
/>
&nbsp;
{t('accept_all')} ({nChanges})
</BulkActions.Button> </BulkActions.Button>
<BulkActions.Button onClick={handleShowBulkRejectDialog}> <BulkActions.Button onClick={handleShowBulkRejectDialog}>
<Icon type="times" /> {t('reject_all')} ({nChanges}) <BootstrapVersionSwitcher
bs3={<Icon type="times" />}
bs5={<MaterialIcon type="close" />}
/>
&nbsp;
{t('reject_all')} ({nChanges})
</BulkActions.Button> </BulkActions.Button>
</> </>
)} )}
@ -83,7 +95,12 @@ function EditorWidgets() {
!isRestrictedTokenMember && !isRestrictedTokenMember &&
currentDocEntries?.['add-comment'] && ( currentDocEntries?.['add-comment'] && (
<AddCommentButton onClick={handleAddNewCommentClick}> <AddCommentButton onClick={handleAddNewCommentClick}>
<Icon type="comment" /> {t('add_comment')} <BootstrapVersionSwitcher
bs3={<Icon type="comment" />}
bs5={<MaterialIcon type="mode_comment" />}
/>
&nbsp;
{t('add_comment')}
</AddCommentButton> </AddCommentButton>
)} )}
</div> </div>

View file

@ -11,6 +11,9 @@ import {
useReviewPanelValueContext, useReviewPanelValueContext,
} from '../../../context/review-panel/review-panel-context' } from '../../../context/review-panel/review-panel-context'
import classnames from 'classnames' import classnames from 'classnames'
import MaterialIcon from '@/shared/components/material-icon'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
import LoadingSpinner from '@/shared/components/loading-spinner'
function AddCommentEntry() { function AddCommentEntry() {
const { t } = useTranslation() const { t } = useTranslation()
@ -116,9 +119,7 @@ function AddCommentEntry() {
<> <>
<div className="rp-new-comment"> <div className="rp-new-comment">
{isSubmitting ? ( {isSubmitting ? (
<div className="rp-loading"> <LoadingSpinner className="d-flex justify-content-center" />
<Icon type="spinner" spin />
</div>
) : ( ) : (
<AutoExpandingTextArea <AutoExpandingTextArea
className="rp-comment-input" className="rp-comment-input"
@ -137,19 +138,34 @@ function AddCommentEntry() {
className="rp-entry-button-cancel" className="rp-entry-button-cancel"
onClick={handleCancelNewComment} onClick={handleCancelNewComment}
> >
<Icon type="times" /> {t('cancel')} <BootstrapVersionSwitcher
bs3={<Icon type="times" />}
bs5={<MaterialIcon type="close" />}
/>
&nbsp;
{t('cancel')}
</EntryActions.Button> </EntryActions.Button>
<EntryActions.Button <EntryActions.Button
onClick={handleSubmitNewComment} onClick={handleSubmitNewComment}
disabled={isSubmitting || !content.length} disabled={isSubmitting || !content.length}
> >
<Icon type="comment" /> {t('comment')} <BootstrapVersionSwitcher
bs3={<Icon type="comment" />}
bs5={<MaterialIcon type="mode_comment" />}
/>
&nbsp;
{t('comment')}
</EntryActions.Button> </EntryActions.Button>
</EntryActions> </EntryActions>
</> </>
) : ( ) : (
<AddCommentButton onClick={handleStartNewComment}> <AddCommentButton onClick={handleStartNewComment}>
<Icon type="comment" /> {t('add_comment')} <BootstrapVersionSwitcher
bs3={<Icon type="comment" />}
bs5={<MaterialIcon type="mode_comment" />}
/>
&nbsp;
{t('add_comment')}
</AddCommentButton> </AddCommentButton>
)} )}
</div> </div>

View file

@ -12,6 +12,8 @@ import { BaseChangeEntryProps } from '../types/base-change-entry-props'
import useIndicatorHover from '../hooks/use-indicator-hover' import useIndicatorHover from '../hooks/use-indicator-hover'
import EntryIndicator from './entry-indicator' import EntryIndicator from './entry-indicator'
import { useEntryClick } from '@/features/source-editor/components/review-panel/hooks/use-entry-click' import { useEntryClick } from '@/features/source-editor/components/review-panel/hooks/use-entry-click'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
import MaterialIcon from '@/shared/components/material-icon'
interface AggregateChangeEntryProps extends BaseChangeEntryProps { interface AggregateChangeEntryProps extends BaseChangeEntryProps {
replacedContent: string replacedContent: string
@ -80,7 +82,10 @@ function AggregateChangeEntry({
onMouseEnter={handleIndicatorMouseEnter} onMouseEnter={handleIndicatorMouseEnter}
onClick={handleIndicatorClick} onClick={handleIndicatorClick}
> >
<Icon type="pencil" /> <BootstrapVersionSwitcher
bs3={<Icon type="pencil" />}
bs5={<MaterialIcon type="edit" />}
/>
</EntryIndicator> </EntryIndicator>
<div <div
className={classnames('rp-entry', 'rp-entry-aggregate', { className={classnames('rp-entry', 'rp-entry-aggregate', {
@ -89,7 +94,10 @@ function AggregateChangeEntry({
> >
<div className="rp-entry-body"> <div className="rp-entry-body">
<div className="rp-entry-action-icon"> <div className="rp-entry-action-icon">
<Icon type="pencil" /> <BootstrapVersionSwitcher
bs3={<Icon type="pencil" />}
bs5={<MaterialIcon type="edit" />}
/>
</div> </div>
<div className="rp-entry-details"> <div className="rp-entry-details">
<div className="rp-entry-description"> <div className="rp-entry-description">
@ -97,7 +105,7 @@ function AggregateChangeEntry({
<del className="rp-content-highlight">{deletionContent}</del> <del className="rp-content-highlight">{deletionContent}</del>
{deletionNeedsCollapsing && ( {deletionNeedsCollapsing && (
<button <button
className="rp-collapse-toggle btn-inline-link" className="rp-collapse-toggle"
onClick={handleDeletionToggleCollapse} onClick={handleDeletionToggleCollapse}
> >
{isDeletionCollapsed {isDeletionCollapsed
@ -109,7 +117,7 @@ function AggregateChangeEntry({
<ins className="rp-content-highlight">{insertionContent}</ins> <ins className="rp-content-highlight">{insertionContent}</ins>
{insertionNeedsCollapsing && ( {insertionNeedsCollapsing && (
<button <button
className="rp-collapse-toggle btn-inline-link" className="rp-collapse-toggle"
onClick={handleInsertionToggleCollapse} onClick={handleInsertionToggleCollapse}
> >
{isInsertionCollapsed {isInsertionCollapsed
@ -139,10 +147,18 @@ function AggregateChangeEntry({
{permissions.write && ( {permissions.write && (
<EntryActions> <EntryActions>
<EntryActions.Button onClick={() => rejectChanges(entryIds)}> <EntryActions.Button onClick={() => rejectChanges(entryIds)}>
<Icon type="times" /> {t('reject')} <BootstrapVersionSwitcher
bs3={<Icon type="times" />}
bs5={<MaterialIcon type="close" />}
/>
&nbsp;{t('reject')}
</EntryActions.Button> </EntryActions.Button>
<EntryActions.Button onClick={() => acceptChanges(entryIds)}> <EntryActions.Button onClick={() => acceptChanges(entryIds)}>
<Icon type="check" /> {t('accept')} <BootstrapVersionSwitcher
bs3={<Icon type="check" />}
bs5={<MaterialIcon type="check" />}
/>
&nbsp;{t('accept')}
</EntryActions.Button> </EntryActions.Button>
</EntryActions> </EntryActions>
)} )}

View file

@ -5,6 +5,8 @@ import Icon from '../../../../../../shared/components/icon'
import BulkActions from './bulk-actions' import BulkActions from './bulk-actions'
import Modal, { useBulkActionsModal } from './modal' import Modal, { useBulkActionsModal } from './modal'
import { ReviewPanelBulkActionsEntry } from '../../../../../../../../types/review-panel/entry' import { ReviewPanelBulkActionsEntry } from '../../../../../../../../types/review-panel/entry'
import MaterialIcon from '@/shared/components/material-icon'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
type BulkActionsEntryProps = { type BulkActionsEntryProps = {
entryId: ReviewPanelBulkActionsEntry['type'] entryId: ReviewPanelBulkActionsEntry['type']
@ -30,10 +32,18 @@ function BulkActionsEntry({ entryId, nChanges }: BulkActionsEntryProps) {
<EntryCallout className="rp-entry-callout-bulk-actions" /> <EntryCallout className="rp-entry-callout-bulk-actions" />
<BulkActions className="rp-entry"> <BulkActions className="rp-entry">
<BulkActions.Button onClick={handleShowBulkRejectDialog}> <BulkActions.Button onClick={handleShowBulkRejectDialog}>
<Icon type="times" /> {t('reject_all')} ({nChanges}) <BootstrapVersionSwitcher
bs3={<Icon type="times" />}
bs5={<MaterialIcon type="close" />}
/>
&nbsp;{t('reject_all')} ({nChanges})
</BulkActions.Button> </BulkActions.Button>
<BulkActions.Button onClick={handleShowBulkAcceptDialog}> <BulkActions.Button onClick={handleShowBulkAcceptDialog}>
<Icon type="check" /> {t('accept_all')} ({nChanges}) <BootstrapVersionSwitcher
bs3={<Icon type="check" />}
bs5={<MaterialIcon type="check" />}
/>
&nbsp;{t('accept_all')} ({nChanges})
</BulkActions.Button> </BulkActions.Button>
</BulkActions> </BulkActions>
</> </>

View file

@ -1,8 +1,13 @@
import { useState, useCallback } from 'react' import { useState, useCallback } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Button, Modal as BootstrapModal } from 'react-bootstrap'
import AccessibleModal from '../../../../../../shared/components/accessible-modal'
import { useReviewPanelUpdaterFnsContext } from '../../../../context/review-panel/review-panel-context' import { useReviewPanelUpdaterFnsContext } from '../../../../context/review-panel/review-panel-context'
import OLModal, {
OLModalBody,
OLModalFooter,
OLModalHeader,
OLModalTitle,
} from '@/features/ui/components/ol/ol-modal'
import OLButton from '@/features/ui/components/ol/ol-button'
type BulkActionsModalProps = { type BulkActionsModalProps = {
show: boolean show: boolean
@ -22,30 +27,28 @@ function Modal({
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<AccessibleModal show={show} onHide={() => setShow(false)}> <OLModal show={show} onHide={() => setShow(false)}>
<BootstrapModal.Header closeButton> <OLModalHeader closeButton>
<h3>{isAccept ? t('accept_all') : t('reject_all')}</h3> <OLModalTitle>
</BootstrapModal.Header> {isAccept ? t('accept_all') : t('reject_all')}
<BootstrapModal.Body> </OLModalTitle>
</OLModalHeader>
<OLModalBody>
<p> <p>
{isAccept {isAccept
? t('bulk_accept_confirm', { nChanges }) ? t('bulk_accept_confirm', { nChanges })
: t('bulk_reject_confirm', { nChanges })} : t('bulk_reject_confirm', { nChanges })}
</p> </p>
</BootstrapModal.Body> </OLModalBody>
<BootstrapModal.Footer> <OLModalFooter>
<Button <OLButton variant="secondary" onClick={() => setShow(false)}>
bsStyle={null}
className="btn-secondary"
onClick={() => setShow(false)}
>
{t('cancel')} {t('cancel')}
</Button> </OLButton>
<Button bsStyle={null} className="btn-primary" onClick={onConfirm}> <OLButton variant="primary" onClick={onConfirm}>
{t('ok')} {t('ok')}
</Button> </OLButton>
</BootstrapModal.Footer> </OLModalFooter>
</AccessibleModal> </OLModal>
) )
} }

View file

@ -13,6 +13,8 @@ import comparePropsWithShallowArrayCompare from '../utils/compare-props-with-sha
import useIndicatorHover from '../hooks/use-indicator-hover' import useIndicatorHover from '../hooks/use-indicator-hover'
import EntryIndicator from './entry-indicator' import EntryIndicator from './entry-indicator'
import { useEntryClick } from '@/features/source-editor/components/review-panel/hooks/use-entry-click' import { useEntryClick } from '@/features/source-editor/components/review-panel/hooks/use-entry-click'
import MaterialIcon from '@/shared/components/material-icon'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
interface ChangeEntryProps extends BaseChangeEntryProps { interface ChangeEntryProps extends BaseChangeEntryProps {
type: ReviewPanelChangeEntry['type'] type: ReviewPanelChangeEntry['type']
@ -71,7 +73,14 @@ function ChangeEntry({
onMouseEnter={handleIndicatorMouseEnter} onMouseEnter={handleIndicatorMouseEnter}
onClick={handleIndicatorClick} onClick={handleIndicatorClick}
> >
{isInsert ? <Icon type="pencil" /> : <i className="rp-icon-delete" />} {isInsert ? (
<BootstrapVersionSwitcher
bs3={<Icon type="pencil" />}
bs5={<MaterialIcon type="edit" />}
/>
) : (
<i className="rp-icon-delete" />
)}
</EntryIndicator> </EntryIndicator>
<div <div
className={classnames('rp-entry', `rp-entry-${type}`, { className={classnames('rp-entry', `rp-entry-${type}`, {
@ -81,7 +90,10 @@ function ChangeEntry({
<div className="rp-entry-body"> <div className="rp-entry-body">
<div className="rp-entry-action-icon"> <div className="rp-entry-action-icon">
{isInsert ? ( {isInsert ? (
<Icon type="pencil" /> <BootstrapVersionSwitcher
bs3={<Icon type="pencil" />}
bs5={<MaterialIcon type="edit" />}
/>
) : ( ) : (
<i className="rp-icon-delete" /> <i className="rp-icon-delete" />
)} )}
@ -106,7 +118,7 @@ function ChangeEntry({
)} )}
{needsCollapsing && ( {needsCollapsing && (
<button <button
className="rp-collapse-toggle btn-inline-link" className="rp-collapse-toggle"
onClick={handleToggleCollapse} onClick={handleToggleCollapse}
> >
{isCollapsed {isCollapsed
@ -137,10 +149,18 @@ function ChangeEntry({
{permissions.write && ( {permissions.write && (
<EntryActions> <EntryActions>
<EntryActions.Button onClick={() => rejectChanges(entryIds)}> <EntryActions.Button onClick={() => rejectChanges(entryIds)}>
<Icon type="times" /> {t('reject')} <BootstrapVersionSwitcher
bs3={<Icon type="times" />}
bs5={<MaterialIcon type="close" />}
/>
&nbsp;{t('reject')}
</EntryActions.Button> </EntryActions.Button>
<EntryActions.Button onClick={() => acceptChanges(entryIds)}> <EntryActions.Button onClick={() => acceptChanges(entryIds)}>
<Icon type="check" /> {t('accept')} <BootstrapVersionSwitcher
bs3={<Icon type="check" />}
bs5={<MaterialIcon type="check" />}
/>
&nbsp;{t('accept')}
</EntryActions.Button> </EntryActions.Button>
</EntryActions> </EntryActions>
)} )}

View file

@ -16,6 +16,9 @@ import { ReviewPanelCommentEntry } from '../../../../../../../types/review-panel
import useIndicatorHover from '../hooks/use-indicator-hover' import useIndicatorHover from '../hooks/use-indicator-hover'
import EntryIndicator from './entry-indicator' import EntryIndicator from './entry-indicator'
import { useEntryClick } from '@/features/source-editor/components/review-panel/hooks/use-entry-click' import { useEntryClick } from '@/features/source-editor/components/review-panel/hooks/use-entry-click'
import MaterialIcon from '@/shared/components/material-icon'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
import LoadingSpinner from '@/shared/components/loading-spinner'
type CommentEntryProps = { type CommentEntryProps = {
docId: DocId docId: DocId
@ -120,7 +123,10 @@ function CommentEntry({
onMouseEnter={handleIndicatorMouseEnter} onMouseEnter={handleIndicatorMouseEnter}
onClick={handleIndicatorClick} onClick={handleIndicatorClick}
> >
<Icon type="comment" /> <BootstrapVersionSwitcher
bs3={<Icon type="comment" />}
bs5={<MaterialIcon type="mode_comment" />}
/>
</EntryIndicator> </EntryIndicator>
<div <div
className={classnames('rp-entry', 'rp-entry-comment', { className={classnames('rp-entry', 'rp-entry-comment', {
@ -130,7 +136,7 @@ function CommentEntry({
ref={entryDivRef} ref={entryDivRef}
> >
{!submitting && (!thread || thread.messages.length === 0) && ( {!submitting && (!thread || thread.messages.length === 0) && (
<div className="rp-loading">{t('no_comments')}</div> <div className="text-center p-1">{t('no_comments')}</div>
)} )}
<div className="rp-comment-loaded"> <div className="rp-comment-loaded">
{thread.messages.map(comment => ( {thread.messages.map(comment => (
@ -143,9 +149,7 @@ function CommentEntry({
))} ))}
</div> </div>
{submitting && ( {submitting && (
<div className="rp-loading"> <LoadingSpinner className="d-flex justify-content-center" />
<Icon type="spinner" spin />
</div>
)} )}
{permissions.comment && ( {permissions.comment && (
<div className="rp-comment-reply"> <div className="rp-comment-reply">
@ -163,7 +167,11 @@ function CommentEntry({
<EntryActions> <EntryActions>
{permissions.comment && permissions.write && ( {permissions.comment && permissions.write && (
<EntryActions.Button onClick={handleAnimateAndCallOnResolve}> <EntryActions.Button onClick={handleAnimateAndCallOnResolve}>
<Icon type="inbox" /> {t('resolve')} <BootstrapVersionSwitcher
bs3={<Icon type="inbox" />}
bs5={<MaterialIcon type="inbox" />}
/>
&nbsp;{t('resolve')}
</EntryActions.Button> </EntryActions.Button>
)} )}
{permissions.comment && ( {permissions.comment && (
@ -171,7 +179,11 @@ function CommentEntry({
onClick={handleOnReply} onClick={handleOnReply}
disabled={!replyContent.length} disabled={!replyContent.length}
> >
<Icon type="reply" /> {t('reply')} <BootstrapVersionSwitcher
bs3={<Icon type="reply" />}
bs5={<MaterialIcon type="reply" />}
/>
&nbsp;{t('reply')}
</EntryActions.Button> </EntryActions.Button>
)} )}
</EntryActions> </EntryActions>

View file

@ -61,7 +61,7 @@ function ResolvedCommentEntry({
<> <>
&nbsp; &nbsp;
<button <button
className="rp-collapse-toggle btn-inline-link" className="rp-collapse-toggle"
onClick={() => setIsCollapsed(value => !value)} onClick={() => setIsCollapsed(value => !value)}
> >
{isCollapsed ? `… (${t('show_all')})` : ` (${t('show_less')})`} {isCollapsed ? `… (${t('show_all')})` : ` (${t('show_less')})`}

View file

@ -8,6 +8,8 @@ import {
import { isCurrentFileView, isOverviewView } from '../../utils/sub-view' import { isCurrentFileView, isOverviewView } from '../../utils/sub-view'
import { useCallback } from 'react' import { useCallback } from 'react'
import { useResizeObserver } from '../../../../shared/hooks/use-resize-observer' import { useResizeObserver } from '../../../../shared/hooks/use-resize-observer'
import MaterialIcon from '@/shared/components/material-icon'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
function Nav() { function Nav() {
const { t } = useTranslation() const { t } = useTranslation()
@ -40,7 +42,10 @@ function Nav() {
})} })}
onClick={() => handleSetSubview('cur_file')} onClick={() => handleSetSubview('cur_file')}
> >
<Icon type="file-text-o" /> <BootstrapVersionSwitcher
bs3={<Icon type="file-text-o" />}
bs5={<MaterialIcon type="description" className="align-middle" />}
/>
<span className="rp-nav-label">{t('current_file')}</span> <span className="rp-nav-label">{t('current_file')}</span>
</button> </button>
<button <button
@ -55,7 +60,10 @@ function Nav() {
})} })}
onClick={() => handleSetSubview('overview')} onClick={() => handleSetSubview('overview')}
> >
<Icon type="list" /> <BootstrapVersionSwitcher
bs3={<Icon type="list" />}
bs5={<MaterialIcon type="list" className="align-middle" />}
/>
<span className="rp-nav-label">{t('overview')}</span> <span className="rp-nav-label">{t('overview')}</span>
</button> </button>
</div> </div>

View file

@ -2,11 +2,11 @@ import Container from './container'
import Toggler from './toggler' import Toggler from './toggler'
import Toolbar from './toolbar/toolbar' import Toolbar from './toolbar/toolbar'
import Nav from './nav' import Nav from './nav'
import Icon from '../../../../shared/components/icon'
import OverviewFile from './overview-file' import OverviewFile from './overview-file'
import { useReviewPanelValueContext } from '../../context/review-panel/review-panel-context' import { useReviewPanelValueContext } from '../../context/review-panel/review-panel-context'
import { useFileTreeData } from '@/shared/context/file-tree-data-context' import { useFileTreeData } from '@/shared/context/file-tree-data-context'
import { memo } from 'react' import { memo } from 'react'
import LoadingSpinner from '@/shared/components/loading-spinner'
function OverviewContainer() { function OverviewContainer() {
const { isOverviewLoading } = useReviewPanelValueContext() const { isOverviewLoading } = useReviewPanelValueContext()
@ -24,9 +24,7 @@ function OverviewContainer() {
aria-labelledby="review-panel-tab-overview" aria-labelledby="review-panel-tab-overview"
> >
{isOverviewLoading ? ( {isOverviewLoading ? (
<div className="rp-loading"> <LoadingSpinner className="d-flex justify-content-center my-2" />
<Icon type="spinner" spin />
</div>
) : ( ) : (
docs?.map(doc => ( docs?.map(doc => (
<OverviewFile <OverviewFile

View file

@ -8,6 +8,8 @@ import classnames from 'classnames'
import { ReviewPanelDocEntries } from '../../../../../../types/review-panel/review-panel' import { ReviewPanelDocEntries } from '../../../../../../types/review-panel/review-panel'
import { MainDocument } from '../../../../../../types/project-settings' import { MainDocument } from '../../../../../../types/project-settings'
import OverviewFileEntries from '@/features/source-editor/components/review-panel/entries/overview-file-entries' import OverviewFileEntries from '@/features/source-editor/components/review-panel/entries/overview-file-entries'
import MaterialIcon from '@/shared/components/material-icon'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
type OverviewFileProps = { type OverviewFileProps = {
docId: MainDocument['doc']['id'] docId: MainDocument['doc']['id']
@ -45,7 +47,10 @@ function OverviewFile({ docId, docPath }: OverviewFileProps) {
'rp-overview-file-header-collapse-on': docCollapsed, 'rp-overview-file-header-collapse-on': docCollapsed,
})} })}
> >
<Icon type="angle-down" /> <BootstrapVersionSwitcher
bs3={<Icon type="angle-down" />}
bs5={<MaterialIcon type="expand_more" />}
/>
</span> </span>
{docPath} {docPath}
{docCollapsed && ( {docCollapsed && (

View file

@ -1,4 +1,4 @@
import { useEffect, useRef } from 'react' import { useEffect, useLayoutEffect, useRef, useCallback } from 'react'
import { useLayoutContext } from '../../../../shared/context/layout-context' import { useLayoutContext } from '../../../../shared/context/layout-context'
import { import {
ReviewPanelEntry, ReviewPanelEntry,
@ -6,7 +6,6 @@ import {
} from '../../../../../../types/review-panel/entry' } from '../../../../../../types/review-panel/entry'
import { debugConsole } from '../../../../utils/debugging' import { debugConsole } from '../../../../utils/debugging'
import { useReviewPanelValueContext } from '../../context/review-panel/review-panel-context' import { useReviewPanelValueContext } from '../../context/review-panel/review-panel-context'
import useEventListener from '../../../../shared/hooks/use-event-listener'
import { ReviewPanelDocEntries } from '../../../../../../types/review-panel/review-panel' import { ReviewPanelDocEntries } from '../../../../../../types/review-panel/review-panel'
import { dispatchReviewPanelLayout } from '../../extensions/changes/change-manager' import { dispatchReviewPanelLayout } from '../../extensions/changes/change-manager'
import { isEqual } from 'lodash' import { isEqual } from 'lodash'
@ -214,255 +213,269 @@ function PositionedEntries({
previousLayoutInfoRef.current = initialLayoutInfo previousLayoutInfoRef.current = initialLayoutInfo
} }
const layout = (animate = true) => { const layout = useCallback(
const container = containerRef.current (animate = true) => {
if (!container) { const container = containerRef.current
return if (!container) {
} return
const padding = reviewPanelOpen ? 8 : 4
const toolbarPaddedHeight = reviewPanelOpen ? toolbarHeight + 6 : 0
const navPaddedHeight = reviewPanelOpen ? navHeight + 4 : 0
// Create a list of entry views, typing together DOM elements and model.
// No measuring or style change is done at this point.
const entryViews: EntryView[] = []
// TODO: Look into tying the entry to the DOM element without going via a DOM data attribute
for (const wrapper of container.querySelectorAll<HTMLElement>(
'.rp-entry-wrapper'
)) {
const entryId = wrapper.dataset.entryId as
| EntryView['entryId']
| undefined
if (!entryId) {
throw new Error('Could not find an entry ID')
} }
const entry = entries.find(value => value[0] === entryId)?.[1] const padding = reviewPanelOpen ? 8 : 4
if (!entry) { const toolbarPaddedHeight = reviewPanelOpen ? toolbarHeight + 6 : 0
throw new Error(`Could not find an entry for ID ${entryId}`) const navPaddedHeight = reviewPanelOpen ? navHeight + 4 : 0
// Create a list of entry views, typing together DOM elements and model.
// No measuring or style change is done at this point.
const entryViews: EntryView[] = []
// TODO: Look into tying the entry to the DOM element without going via a DOM data attribute
for (const wrapper of container.querySelectorAll<HTMLElement>(
'.rp-entry-wrapper'
)) {
const entryId = wrapper.dataset.entryId as
| EntryView['entryId']
| undefined
if (!entryId) {
throw new Error('Could not find an entry ID')
}
const entry = entries.find(value => value[0] === entryId)?.[1]
if (!entry) {
throw new Error(`Could not find an entry for ID ${entryId}`)
}
const indicator = wrapper.querySelector<HTMLElement>(
'.rp-entry-indicator'
)
const box = wrapper.querySelector<HTMLElement>('.rp-entry')
const callout = wrapper.querySelector<HTMLElement>('.rp-entry-callout')
const layoutElement = reviewPanelOpen ? box : indicator
if (box && callout && layoutElement) {
const previousPositions =
previousLayoutInfoRef.current?.positions.find(
pos => pos.entryId === entryId
)?.positions
const hasScreenPos = Boolean(entry.screenPos)
entryViews.push({
entryId,
wrapper,
indicator,
box,
callout,
layout: layoutElement,
hasScreenPos,
height: 0,
entry,
previousPositions,
})
} else {
debugConsole.log(
'Entry wrapper is missing indicator, box or callout, so ignoring',
wrapper
)
}
} }
const indicator = wrapper.querySelector<HTMLElement>( if (entryViews.length === 0) {
'.rp-entry-indicator' resetLayout()
return
}
entryViews.sort((a, b) => a.entry.offset - b.entry.offset)
// Do the DOM interaction in three phases:
//
// - Apply the `display` property to all elements whose visibility has
// changed. This needs to happen first in order to measure heights.
// - Measure the height of each entry
// - Move each entry without animation to their original position
// relative to the editor content
// - Re-enable animation and position each entry
//
// The idea is to batch DOM reads and writes to avoid layout thrashing. In
// this case, the best we can do is a write phase, a read phase then a
// final write phase.
// See https://web.dev/avoid-large-complex-layouts-and-layout-thrashing/
// First, update display for each entry that needs it
hideOrShowEntries(entryViews)
// Next, measure the height of each entry
for (const entryView of entryViews) {
if (entryView.hasScreenPos) {
entryView.height = entryView.layout.offsetHeight
}
}
// Calculate positions for all positioned entries, starting by calculating
// which entry to put in its desired position and anchor everything else
// around. If there is an explicitly focused entry, use that.
let focusedEntryIndex = entryViews.findIndex(view => view.entry.focused)
if (focusedEntryIndex === -1) {
// There is no explicitly focused entry, so use the focused entry from the
// previous layout. This will be the first entry in the list if there was
// no previous layout.
focusedEntryIndex = Math.min(
previousLayoutInfoRef.current.focusedEntryIndex,
entryViews.length - 1
)
// If the entry from the previous layout has no screen position, fall back
// to the first entry in the list that does.
if (!entryViews[focusedEntryIndex].hasScreenPos) {
focusedEntryIndex = entryViews.findIndex(view => view.hasScreenPos)
}
}
// If there is no entry with a screen position, bail out
if (focusedEntryIndex === -1) {
return
}
const focusedEntryView = entryViews[focusedEntryIndex]
// If the focused entry has no screenPos, we can't position other
// entryViews relative to it, so we position all other entryViews as
// though the focused entry is at the top and the rest follow it
const entryViewsAfter = focusedEntryView.hasScreenPos
? entryViews.slice(focusedEntryIndex + 1)
: [...entryViews]
const entryViewsBefore = focusedEntryView.hasScreenPos
? entryViews.slice(0, focusedEntryIndex).reverse() // Work through backwards, starting with the one just above
: []
debugConsole.log('focusedEntryIndex', focusedEntryIndex)
let lastEntryBottom = 0
let firstEntryTop = 0
// Put the focused entry as close as possible to where it wants to be
if (focusedEntryView.hasScreenPos) {
const focusedEntryScreenPos = focusedEntryView.entry.screenPos
const entryTop = Math.max(focusedEntryScreenPos.y, toolbarPaddedHeight)
updateEntryPositions(focusedEntryView, entryTop, lineHeight)
lastEntryBottom = entryTop + focusedEntryView.height
firstEntryTop = entryTop
}
// Calculate positions for entries that are below the focused entry
calculateEntryViewPositions(
entryViewsAfter,
lineHeight,
(originalTop: number, height: number) => {
const top = Math.max(originalTop, lastEntryBottom + padding)
lastEntryBottom = top + height
return top
}
) )
const box = wrapper.querySelector<HTMLElement>('.rp-entry')
const callout = wrapper.querySelector<HTMLElement>('.rp-entry-callout')
const layoutElement = reviewPanelOpen ? box : indicator
if (box && callout && layoutElement) { // Calculate positions for entries that are above the focused entry
const previousPositions = previousLayoutInfoRef.current?.positions.find( calculateEntryViewPositions(
pos => pos.entryId === entryId entryViewsBefore,
)?.positions lineHeight,
const hasScreenPos = Boolean(entry.screenPos) (originalTop: number, height: number) => {
entryViews.push({ const originalBottom = originalTop + height
entryId, const bottom = Math.min(originalBottom, firstEntryTop - padding)
wrapper, const top = bottom - height
indicator, firstEntryTop = top
box, return top
callout, }
layout: layoutElement, )
hasScreenPos,
height: 0, // Calculate the new top overflow
entry, const overflowTop = Math.max(0, toolbarPaddedHeight - firstEntryTop)
previousPositions,
// Check whether the positions of any entry have changed since the last
// layout
const positions = entryViews.map(
(entryView): EntryPositions => ({
entryId: entryView.entryId,
positions: entryView.positions,
}) })
} else { )
debugConsole.log(
'Entry wrapper is missing indicator, box or callout, so ignoring', const positionsChanged = !positionsEqual(
wrapper previousLayoutInfoRef.current.positions,
positions
)
// Check whether the top overflow or review panel height have changed
const overflowTopChanged =
overflowTop !== previousLayoutInfoRef.current.overflowTop
const height = lastEntryBottom + navPaddedHeight
const heightChanged = height !== previousLayoutInfoRef.current.height
const isMoveRequired = positionsChanged || overflowTopChanged
// Move entries into their initial positions, if animating, avoiding
// animation until the final animated move
if (animate && isMoveRequired) {
container.classList.add('no-animate')
moveEntriesToInitialPosition(entryViews, overflowTop)
}
// Inform the editor of the new top overflow and/or height if either has
// changed
if (overflowTopChanged || heightChanged) {
window.dispatchEvent(
new CustomEvent('review-panel:event', {
detail: {
type: 'sizes',
payload: {
overflowTop,
height,
},
},
})
) )
} }
}
if (entryViews.length === 0) { // Do the final move
resetLayout() if (isMoveRequired) {
return if (animate) {
} container.classList.remove('no-animate')
moveEntriesToFinalPositions(entryViews, overflowTop, false)
} else {
container.classList.add('no-animate')
moveEntriesToFinalPositions(entryViews, overflowTop, true)
entryViews.sort((a, b) => a.entry.offset - b.entry.offset) // Force reflow now to ensure that entries are moved without animation
// eslint-disable-next-line no-void
void container.offsetHeight
// Do the DOM interaction in three phases: container.classList.remove('no-animate')
// }
// - Apply the `display` property to all elements whose visibility has }
// changed. This needs to happen first in order to measure heights.
// - Measure the height of each entry
// - Move each entry without animation to their original position
// relative to the editor content
// - Re-enable animation and position each entry
//
// The idea is to batch DOM reads and writes to avoid layout thrashing. In
// this case, the best we can do is a write phase, a read phase then a
// final write phase.
// See https://web.dev/avoid-large-complex-layouts-and-layout-thrashing/
// First, update display for each entry that needs it previousLayoutInfoRef.current = {
hideOrShowEntries(entryViews) positions,
focusedEntryIndex,
height,
overflowTop,
}
},
[entries, lineHeight, navHeight, reviewPanelOpen, toolbarHeight]
)
// Next, measure the height of each entry useLayoutEffect(() => {
for (const entryView of entryViews) { const callback = (event: Event) => {
if (entryView.hasScreenPos) { const e = event as CustomEvent
entryView.height = entryView.layout.offsetHeight
if (!layoutSuspended) {
// Clear previous positions if forcing a layout
if (e.detail.force) {
previousLayoutInfoRef.current = initialLayoutInfo
}
layout(e.detail.animate)
} }
} }
// Calculate positions for all positioned entries, starting by calculating window.addEventListener('review-panel:layout', callback)
// which entry to put in its desired position and anchor everything else
// around. If there is an explicitly focused entry, use that. return () => {
let focusedEntryIndex = entryViews.findIndex(view => view.entry.focused) window.removeEventListener('review-panel:layout', callback)
if (focusedEntryIndex === -1) {
// There is no explicitly focused entry, so use the focused entry from the
// previous layout. This will be the first entry in the list if there was
// no previous layout.
focusedEntryIndex = Math.min(
previousLayoutInfoRef.current.focusedEntryIndex,
entryViews.length - 1
)
// If the entry from the previous layout has no screen position, fall back
// to the first entry in the list that does.
if (!entryViews[focusedEntryIndex].hasScreenPos) {
focusedEntryIndex = entryViews.findIndex(view => view.hasScreenPos)
}
} }
}, [layoutSuspended, layout])
// If there is no entry with a screen position, bail out
if (focusedEntryIndex === -1) {
return
}
const focusedEntryView = entryViews[focusedEntryIndex]
// If the focused entry has no screenPos, we can't position other
// entryViews relative to it, so we position all other entryViews as
// though the focused entry is at the top and the rest follow it
const entryViewsAfter = focusedEntryView.hasScreenPos
? entryViews.slice(focusedEntryIndex + 1)
: [...entryViews]
const entryViewsBefore = focusedEntryView.hasScreenPos
? entryViews.slice(0, focusedEntryIndex).reverse() // Work through backwards, starting with the one just above
: []
debugConsole.log('focusedEntryIndex', focusedEntryIndex)
let lastEntryBottom = 0
let firstEntryTop = 0
// Put the focused entry as close as possible to where it wants to be
if (focusedEntryView.hasScreenPos) {
const focusedEntryScreenPos = focusedEntryView.entry.screenPos
const entryTop = Math.max(focusedEntryScreenPos.y, toolbarPaddedHeight)
updateEntryPositions(focusedEntryView, entryTop, lineHeight)
lastEntryBottom = entryTop + focusedEntryView.height
firstEntryTop = entryTop
}
// Calculate positions for entries that are below the focused entry
calculateEntryViewPositions(
entryViewsAfter,
lineHeight,
(originalTop: number, height: number) => {
const top = Math.max(originalTop, lastEntryBottom + padding)
lastEntryBottom = top + height
return top
}
)
// Calculate positions for entries that are above the focused entry
calculateEntryViewPositions(
entryViewsBefore,
lineHeight,
(originalTop: number, height: number) => {
const originalBottom = originalTop + height
const bottom = Math.min(originalBottom, firstEntryTop - padding)
const top = bottom - height
firstEntryTop = top
return top
}
)
// Calculate the new top overflow
const overflowTop = Math.max(0, toolbarPaddedHeight - firstEntryTop)
// Check whether the positions of any entry have changed since the last
// layout
const positions = entryViews.map(
(entryView): EntryPositions => ({
entryId: entryView.entryId,
positions: entryView.positions,
})
)
const positionsChanged = !positionsEqual(
previousLayoutInfoRef.current.positions,
positions
)
// Check whether the top overflow or review panel height have changed
const overflowTopChanged =
overflowTop !== previousLayoutInfoRef.current.overflowTop
const height = lastEntryBottom + navPaddedHeight
const heightChanged = height !== previousLayoutInfoRef.current.height
const isMoveRequired = positionsChanged || overflowTopChanged
// Move entries into their initial positions, if animating, avoiding
// animation until the final animated move
if (animate && isMoveRequired) {
container.classList.add('no-animate')
moveEntriesToInitialPosition(entryViews, overflowTop)
}
// Inform the editor of the new top overflow and/or height if either has
// changed
if (overflowTopChanged || heightChanged) {
window.dispatchEvent(
new CustomEvent('review-panel:event', {
detail: {
type: 'sizes',
payload: {
overflowTop,
height,
},
},
})
)
}
// Do the final move
if (isMoveRequired) {
if (animate) {
container.classList.remove('no-animate')
moveEntriesToFinalPositions(entryViews, overflowTop, false)
} else {
container.classList.add('no-animate')
moveEntriesToFinalPositions(entryViews, overflowTop, true)
// Force reflow now to ensure that entries are moved without animation
// eslint-disable-next-line no-void
void container.offsetHeight
container.classList.remove('no-animate')
}
}
previousLayoutInfoRef.current = {
positions,
focusedEntryIndex,
height,
overflowTop,
}
}
useEventListener('review-panel:layout', (event: CustomEvent) => {
if (!layoutSuspended) {
// Clear previous positions if forcing a layout
if (event.detail.force) {
previousLayoutInfoRef.current = initialLayoutInfo
}
layout(event.detail.animate)
}
})
// Layout on first render. This is necessary to ensure layout happens when // Layout on first render. This is necessary to ensure layout happens when
// switching from overview to current file view // switching from overview to current file view

View file

@ -1,5 +1,8 @@
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useReviewPanelUpdaterFnsContext } from '../../context/review-panel/review-panel-context' import { useReviewPanelUpdaterFnsContext } from '../../context/review-panel/review-panel-context'
import Icon from '@/shared/components/icon'
import MaterialIcon from '@/shared/components/material-icon'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
function Toggler() { function Toggler() {
const { t } = useTranslation() const { t } = useTranslation()
@ -18,6 +21,12 @@ function Toggler() {
onClick={handleTogglerClick} onClick={handleTogglerClick}
> >
<span className="sr-only">{t('hotkey_toggle_review_panel')}</span> <span className="sr-only">{t('hotkey_toggle_review_panel')}</span>
<span className="review-panel-toggler-icon">
<BootstrapVersionSwitcher
bs3={<Icon type="chevron-left" />}
bs5={<MaterialIcon type="chevron_left" />}
/>
</span>
</button> </button>
) )
} }

View file

@ -1,7 +1,6 @@
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useState, useMemo, useCallback } from 'react' import { useState, useMemo, useCallback } from 'react'
import Icon from '../../../../../shared/components/icon' import Icon from '../../../../../shared/components/icon'
import Tooltip from '../../../../../shared/components/tooltip'
import ResolvedCommentsScroller from './resolved-comments-scroller' import ResolvedCommentsScroller from './resolved-comments-scroller'
import classnames from 'classnames' import classnames from 'classnames'
import { import {
@ -16,6 +15,10 @@ import { ReviewPanelResolvedCommentThread } from '../../../../../../../types/rev
import { DocId } from '../../../../../../../types/project-settings' import { DocId } from '../../../../../../../types/project-settings'
import { ReviewPanelEntry } from '../../../../../../../types/review-panel/entry' import { ReviewPanelEntry } from '../../../../../../../types/review-panel/entry'
import { useFileTreeData } from '@/shared/context/file-tree-data-context' import { useFileTreeData } from '@/shared/context/file-tree-data-context'
import LoadingSpinner from '@/shared/components/loading-spinner'
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
import MaterialIcon from '@/shared/components/material-icon'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
export interface FilteredResolvedComments export interface FilteredResolvedComments
extends ReviewPanelResolvedCommentThread { extends ReviewPanelResolvedCommentThread {
@ -97,7 +100,7 @@ function ResolvedCommentsDropdown() {
onClick={() => setIsOpen(false)} onClick={() => setIsOpen(false)}
/> />
<Tooltip <OLTooltip
id="resolved-comments-toggle" id="resolved-comments-toggle"
description={t('resolved_comments')} description={t('resolved_comments')}
overlayProps={{ container: document.body, placement: 'bottom' }} overlayProps={{ container: document.body, placement: 'bottom' }}
@ -107,9 +110,12 @@ function ResolvedCommentsDropdown() {
onClick={handleResolvedCommentsClick} onClick={handleResolvedCommentsClick}
aria-label={t('resolved_comments')} aria-label={t('resolved_comments')}
> >
<Icon type="inbox" /> <BootstrapVersionSwitcher
bs3={<Icon type="inbox" />}
bs5={<MaterialIcon type="inbox" />}
/>
</button> </button>
</Tooltip> </OLTooltip>
<div <div
className={classnames('resolved-comments-dropdown', { className={classnames('resolved-comments-dropdown', {
@ -117,9 +123,7 @@ function ResolvedCommentsDropdown() {
})} })}
> >
{isLoading ? ( {isLoading ? (
<div className="rp-loading"> <LoadingSpinner className="d-flex justify-content-center my-2" />
<Icon type="spinner" spin />
</div>
) : isOpen ? ( ) : isOpen ? (
<ResolvedCommentsScroller <ResolvedCommentsScroller
resolvedComments={filteredResolvedComments} resolvedComments={filteredResolvedComments}

View file

@ -31,7 +31,7 @@ function ResolvedCommentsScroller({
/> />
))} ))}
{!resolvedComments.length && ( {!resolvedComments.length && (
<div className="rp-loading">{t('no_resolved_threads')}</div> <div className="text-center p-1">{t('no_resolved_threads')}</div>
)} )}
</div> </div>
) )

View file

@ -10,6 +10,8 @@ import {
} from '../../../context/review-panel/review-panel-context' } from '../../../context/review-panel/review-panel-context'
import { send, sendMB } from '../../../../../infrastructure/event-tracking' import { send, sendMB } from '../../../../../infrastructure/event-tracking'
import classnames from 'classnames' import classnames from 'classnames'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
import MaterialIcon from '@/shared/components/material-icon'
const sendAnalytics = () => { const sendAnalytics = () => {
send('subscription-funnel', 'editor-click-feature', 'real-time-track-changes') send('subscription-funnel', 'editor-click-feature', 'real-time-track-changes')
@ -39,7 +41,7 @@ function ToggleMenu() {
<span className="review-panel-toolbar-label"> <span className="review-panel-toolbar-label">
{wantTrackChanges && ( {wantTrackChanges && (
<span className="review-panel-toolbar-icon-on"> <span className="review-panel-toolbar-icon-on">
<Icon type="circle" /> <span className="track-changes-indicator-circle" />
</span> </span>
)} )}
@ -47,13 +49,18 @@ function ToggleMenu() {
className="review-panel-toolbar-collapse-button" className="review-panel-toolbar-collapse-button"
onClick={handleToggleFullTCStateCollapse} onClick={handleToggleFullTCStateCollapse}
> >
{wantTrackChanges ? <TrackChangesOn /> : <TrackChangesOff />} <span>
{wantTrackChanges ? <TrackChangesOn /> : <TrackChangesOff />}
</span>
<span <span
className={classnames('rp-tc-state-collapse', { className={classnames('rp-tc-state-collapse', {
'rp-tc-state-collapse-on': shouldCollapse, 'rp-tc-state-collapse-on': shouldCollapse,
})} })}
> >
<Icon type="angle-down" /> <BootstrapVersionSwitcher
bs3={<Icon type="angle-down" />}
bs5={<MaterialIcon type="expand_more" />}
/>
</span> </span>
</button> </button>
</span> </span>

View file

@ -1,5 +1,4 @@
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Tooltip from '@/shared/components/tooltip'
import TrackChangesToggle from '@/features/source-editor/components/review-panel/toolbar/track-changes-toggle' import TrackChangesToggle from '@/features/source-editor/components/review-panel/toolbar/track-changes-toggle'
import { import {
useReviewPanelUpdaterFnsContext, useReviewPanelUpdaterFnsContext,
@ -7,6 +6,7 @@ import {
} from '@/features/source-editor/context/review-panel/review-panel-context' } from '@/features/source-editor/context/review-panel/review-panel-context'
import { useProjectContext } from '@/shared/context/project-context' import { useProjectContext } from '@/shared/context/project-context'
import classnames from 'classnames' import classnames from 'classnames'
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
function TrackChangesMenu() { function TrackChangesMenu() {
const { t } = useTranslation() const { t } = useTranslation()
@ -28,7 +28,7 @@ function TrackChangesMenu() {
return ( return (
<ul className="rp-tc-state" data-testid="review-panel-track-changes-menu"> <ul className="rp-tc-state" data-testid="review-panel-track-changes-menu">
<li className="rp-tc-state-item rp-tc-state-item-everyone"> <li className="rp-tc-state-item rp-tc-state-item-everyone">
<Tooltip <OLTooltip
description={t('tc_switch_everyone_tip')} description={t('tc_switch_everyone_tip')}
id="track-changes-switch-everyone" id="track-changes-switch-everyone"
overlayProps={{ overlayProps={{
@ -38,7 +38,7 @@ function TrackChangesMenu() {
}} }}
> >
<span className="rp-tc-state-item-name">{t('tc_everyone')}</span> <span className="rp-tc-state-item-name">{t('tc_everyone')}</span>
</Tooltip> </OLTooltip>
<TrackChangesToggle <TrackChangesToggle
id="track-changes-everyone" id="track-changes-everyone"
@ -52,7 +52,7 @@ function TrackChangesMenu() {
</li> </li>
{Object.values(formattedProjectMembers).map(member => ( {Object.values(formattedProjectMembers).map(member => (
<li className="rp-tc-state-item" key={member.id}> <li className="rp-tc-state-item" key={member.id}>
<Tooltip <OLTooltip
description={t('tc_switch_user_tip')} description={t('tc_switch_user_tip')}
id="track-changes-switch-user" id="track-changes-switch-user"
overlayProps={{ overlayProps={{
@ -68,7 +68,7 @@ function TrackChangesMenu() {
> >
{member.name} {member.name}
</span> </span>
</Tooltip> </OLTooltip>
<TrackChangesToggle <TrackChangesToggle
id={`track-changes-user-toggle-${member.id}`} id={`track-changes-user-toggle-${member.id}`}
@ -90,7 +90,7 @@ function TrackChangesMenu() {
))} ))}
<li className="rp-tc-state-separator" /> <li className="rp-tc-state-separator" />
<li className="rp-tc-state-item"> <li className="rp-tc-state-item">
<Tooltip <OLTooltip
description={t('tc_switch_guests_tip')} description={t('tc_switch_guests_tip')}
id="track-changes-switch-guests" id="track-changes-switch-guests"
overlayProps={{ overlayProps={{
@ -106,7 +106,7 @@ function TrackChangesMenu() {
> >
{t('tc_guests')} {t('tc_guests')}
</span> </span>
</Tooltip> </OLTooltip>
<TrackChangesToggle <TrackChangesToggle
id="track-changes-guests-toggle" id="track-changes-guests-toggle"

View file

@ -1,12 +1,21 @@
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Row, Col, Button, Modal } from 'react-bootstrap'
import AccessibleModal from '../../../../shared/components/accessible-modal'
import Icon from '../../../../shared/components/icon' import Icon from '../../../../shared/components/icon'
import { useProjectContext } from '../../../../shared/context/project-context' import { useProjectContext } from '../../../../shared/context/project-context'
import { useUserContext } from '../../../../shared/context/user-context' import { useUserContext } from '../../../../shared/context/user-context'
import { startFreeTrial, upgradePlan } from '../../../../main/account-upgrade' import { startFreeTrial, upgradePlan } from '../../../../main/account-upgrade'
import { memo } from 'react' import { memo } from 'react'
import { useFeatureFlag } from '@/shared/context/split-test-context' import { useFeatureFlag } from '@/shared/context/split-test-context'
import OLModal, {
OLModalBody,
OLModalFooter,
OLModalHeader,
OLModalTitle,
} from '@/features/ui/components/ol/ol-modal'
import OLButton from '@/features/ui/components/ol/ol-button'
import OLRow from '@/features/ui/components/ol/ol-row'
import OLCol from '@/features/ui/components/ol/ol-col'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
import MaterialIcon from '@/shared/components/material-icon'
type UpgradeTrackChangesModalProps = { type UpgradeTrackChangesModalProps = {
show: boolean show: boolean
@ -24,11 +33,11 @@ function UpgradeTrackChangesModal({
const hasNewPaywallCta = useFeatureFlag('paywall-cta') const hasNewPaywallCta = useFeatureFlag('paywall-cta')
return ( return (
<AccessibleModal show={show} onHide={() => setShow(false)}> <OLModal show={show} onHide={() => setShow(false)}>
<Modal.Header closeButton> <OLModalHeader closeButton>
<h3>{t('upgrade_to_track_changes')}</h3> <OLModalTitle>{t('upgrade_to_track_changes')}</OLModalTitle>
</Modal.Header> </OLModalHeader>
<Modal.Body> <OLModalBody>
<div className="teaser-video-container"> <div className="teaser-video-container">
{/* eslint-disable-next-line jsx-a11y/media-has-caption */} {/* eslint-disable-next-line jsx-a11y/media-has-caption */}
<video className="teaser-video" autoPlay loop> <video className="teaser-video" autoPlay loop>
@ -45,8 +54,8 @@ function UpgradeTrackChangesModal({
<h4 className="teaser-title"> <h4 className="teaser-title">
{t('see_changes_in_your_documents_live')} {t('see_changes_in_your_documents_live')}
</h4> </h4>
<Row> <OLRow>
<Col md={10} mdOffset={1}> <OLCol lg={{ span: 10, offset: 1 }}>
<ul className="list-unstyled"> <ul className="list-unstyled">
{[ {[
t('track_any_change_in_real_time'), t('track_any_change_in_real_time'),
@ -54,36 +63,43 @@ function UpgradeTrackChangesModal({
t('accept_or_reject_each_changes_individually'), t('accept_or_reject_each_changes_individually'),
].map(translation => ( ].map(translation => (
<li key={translation}> <li key={translation}>
<Icon type="check" /> {translation} <BootstrapVersionSwitcher
bs3={<Icon type="check" />}
bs5={
<MaterialIcon
type="check"
className="align-text-bottom"
/>
}
/>
&nbsp;{translation}
</li> </li>
))} ))}
</ul> </ul>
</Col> </OLCol>
</Row> </OLRow>
<p className="small"> <p className="small">
{t('already_subscribed_try_refreshing_the_page')} {t('already_subscribed_try_refreshing_the_page')}
</p> </p>
{project.owner && ( {project.owner && (
<Row className="text-center"> <div className="text-center">
{project.owner._id === user.id ? ( {project.owner._id === user.id ? (
user.allowedFreeTrial ? ( user.allowedFreeTrial ? (
<Button <OLButton
bsStyle={null} variant="primary"
className="btn-primary"
onClick={() => startFreeTrial('track-changes')} onClick={() => startFreeTrial('track-changes')}
> >
{hasNewPaywallCta {hasNewPaywallCta
? t('get_track_changes') ? t('get_track_changes')
: t('try_it_for_free')} : t('try_it_for_free')}
</Button> </OLButton>
) : ( ) : (
<Button <OLButton
bsStyle={null} variant="primary"
className="btn-primary"
onClick={() => upgradePlan('project-sharing')} onClick={() => upgradePlan('project-sharing')}
> >
{t('upgrade')} {t('upgrade')}
</Button> </OLButton>
) )
) : ( ) : (
<p> <p>
@ -94,19 +110,15 @@ function UpgradeTrackChangesModal({
</strong> </strong>
</p> </p>
)} )}
</Row> </div>
)} )}
</Modal.Body> </OLModalBody>
<Modal.Footer> <OLModalFooter>
<Button <OLButton variant="secondary" onClick={() => setShow(false)}>
bsStyle={null}
className="btn-secondary"
onClick={() => setShow(false)}
>
{t('close')} {t('close')}
</Button> </OLButton>
</Modal.Footer> </OLModalFooter>
</AccessibleModal> </OLModal>
) )
} }

View file

@ -39,10 +39,7 @@ function LoadingSpinner({
} }
const extraClasses = isBootstrap5() const extraClasses = isBootstrap5()
? [ ? [align === 'left' ? 'align-items-start' : 'align-items-center']
'd-inline-flex',
align === 'left' ? 'align-items-start' : 'align-items-center',
]
: null : null
return ( return (

View file

@ -9,10 +9,8 @@
@rp-border-grey: #d9d9d9; @rp-border-grey: #d9d9d9;
@rp-green: #2c8e30; @rp-green: #2c8e30;
@rp-green-on-dark: rgba(37, 107, 41, 0.5);
@rp-red: #c5060b; @rp-red: #c5060b;
@rp-yellow: #f3b111; @rp-yellow: #f3b111;
@rp-yellow-on-dark: rgba(194, 93, 11, 0.5);
@rp-grey: #aaaaaa; @rp-grey: #aaaaaa;
@rp-type-blue: #6b7797; @rp-type-blue: #6b7797;
@ -24,8 +22,6 @@
@review-panel-width: 230px; @review-panel-width: 230px;
@review-off-width: 22px; @review-off-width: 22px;
@rp-toolbar-height: 32px;
@rp-entry-animation-speed: 0.3s; @rp-entry-animation-speed: 0.3s;
.rp-button() { .rp-button() {
@ -64,12 +60,6 @@
} }
} }
.loading-panel {
.rp-size-expanded & {
right: @review-panel-width;
}
}
.review-panel-toolbar { .review-panel-toolbar {
display: none; display: none;
@ -105,10 +95,6 @@
margin-right: 5px; margin-right: 5px;
} }
.review-panel-toolbar-spinner {
margin-left: 5px;
}
.rp-tc-state { .rp-tc-state {
position: absolute; position: absolute;
top: 100%; top: 100%;
@ -173,10 +159,6 @@
} }
} }
.rp-entry-list-inner {
position: relative;
}
.rp-entry-indicator { .rp-entry-indicator {
display: none; display: none;
z-index: 2; // above .review-panel-toggler z-index: 2; // above .review-panel-toggler
@ -752,11 +734,6 @@
} }
} }
.rp-loading {
text-align: center;
padding: 5px;
}
.rp-nav { .rp-nav {
display: none; display: none;
flex-shrink: 0; flex-shrink: 0;
@ -806,18 +783,6 @@
font-size: @rp-base-font-size; font-size: @rp-base-font-size;
} }
#editor {
.rp-size-mini & {
right: @review-off-width;
}
.rp-size-expanded & {
right: @review-panel-width;
left: 0px;
width: auto;
}
}
.review-icon { .review-icon {
display: inline-block; display: inline-block;
background: url('../../../../public/img/ol-icons/review-icon-dark-theme.svg') background: url('../../../../public/img/ol-icons/review-icon-dark-theme.svg')
@ -919,6 +884,7 @@ button when (@is-overleaf-light = true) {
} }
.rp-collapse-toggle { .rp-collapse-toggle {
.btn-inline-link;
color: @rp-type-blue; color: @rp-type-blue;
font-weight: @rp-semibold-weight; font-weight: @rp-semibold-weight;
@ -979,8 +945,8 @@ button when (@is-overleaf-light = true) {
} }
.rp-size-expanded & { .rp-size-expanded & {
&::after { .review-panel-toggler-icon .fa {
content: '\f105'; transform: scale(0.7) rotate(180deg);
} }
} }
@ -990,20 +956,16 @@ button when (@is-overleaf-light = true) {
background-color: #fff; background-color: #fff;
} }
&::after { .review-panel-toggler-icon {
content: '\f104';
font-family: FontAwesome;
line-height: 1;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-size: 16px;
display: block;
position: sticky; position: sticky;
top: 50%; top: 50%;
bottom: 50%; bottom: 50%;
width: 100%; width: 100%;
text-align: center;
margin-top: -0.5em; .fa {
font-size: 14px;
transform: scale(0.7);
}
} }
} }
@ -1134,40 +1096,10 @@ button when (@is-overleaf-light = true) {
} }
} }
.review-panel-indicator { .track-changes-indicator-circle {
display: none; display: inline-block;
} width: 10px;
height: 10px;
.review-panel-mini { border-radius: 100%;
width: 22px !important; background-color: @ol-green;
overflow: visible !important;
.rp-entry {
height: 12px;
}
.rp-entry-indicator {
position: absolute;
left: -6px;
right: 0;
display: flex;
width: 20px;
height: 20px;
}
.rp-entry-content {
display: none;
background: white;
border: 1px solid @rp-border-grey;
width: 200px;
}
.rp-entry:hover {
.rp-entry-content {
display: initial;
position: absolute;
left: -206px;
top: 0;
}
}
} }

View file

@ -143,3 +143,33 @@
--link-hover-color: var(--link-hover-color-dark); --link-hover-color: var(--link-hover-color-dark);
--link-visited-color: var(--link-visited-color-dark); --link-visited-color: var(--link-visited-color-dark);
} }
@mixin triangle($direction, $width, $height, $color) {
position: absolute;
border-color: transparent;
border-style: solid;
width: 0;
height: 0;
@if $direction == top {
border-width: 0 calc(#{$width} / 2) #{$height} calc(#{$width} / 2);
border-bottom-color: $color;
border-left-color: transparent;
border-right-color: transparent;
} @else if $direction == bottom {
border-width: #{$height} calc(#{$width} / 2) 0 calc(#{$width} / 2);
border-top-color: $color;
border-left-color: transparent;
border-right-color: transparent;
} @else if $direction == right {
border-width: calc(#{$height} / 2) 0 calc(#{$height} / 2) #{$width};
border-left-color: $color;
border-top-color: transparent;
border-bottom-color: transparent;
} @else if $direction == left {
border-width: calc(#{$height} / 2) #{$width} calc(#{$height} / 2) 0;
border-right-color: $color;
border-top-color: transparent;
border-bottom-color: transparent;
}
}

View file

@ -1,4 +1,6 @@
.loading { .loading {
display: inline-flex;
.spinner-border-sm, .spinner-border-sm,
.spinner-border { .spinner-border {
// Ensure the thickness of the spinner is independent of the font size of its container // Ensure the thickness of the spinner is independent of the font size of its container

View file

@ -294,8 +294,8 @@ $editor-toggler-bg-dark-color: color.adjust(
} }
.teaser-video-container { .teaser-video-container {
margin: calc(var(--spacing-07) * -1) calc(var(--spacing-07) * -1) margin: calc(var(--bs-modal-padding) * -1) calc(var(--bs-modal-padding) * -1)
var(--spacing-02) calc(var(--spacing-07) * -1); var(--spacing-02) calc(var(--bs-modal-padding) * -1);
overflow: hidden; overflow: hidden;
} }

View file

@ -84,3 +84,11 @@
position: absolute; position: absolute;
z-index: 1; z-index: 1;
} }
.d-flex {
display: flex !important;
}
.justify-content-center {
justify-content: center !important;
}