mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge pull request #21240 from overleaf/ii-bs5-review-panel-old
[web] BS5 review panel old GitOrigin-RevId: da018b8f2946afb21ab63da0003453e20781f04c
This commit is contained in:
parent
92a23b7e9d
commit
5c3d9117c5
25 changed files with 1594 additions and 430 deletions
|
@ -16,6 +16,8 @@ import useScopeEventListener from '@/shared/hooks/use-scope-event-listener'
|
|||
import { memo, useCallback } from 'react'
|
||||
import { useLayoutContext } from '@/shared/context/layout-context'
|
||||
import classnames from 'classnames'
|
||||
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
|
||||
function EditorWidgets() {
|
||||
const { t } = useTranslation()
|
||||
|
@ -71,10 +73,20 @@ function EditorWidgets() {
|
|||
{nChanges > 1 && permissions.write && (
|
||||
<>
|
||||
<BulkActions.Button onClick={handleShowBulkAcceptDialog}>
|
||||
<Icon type="check" /> {t('accept_all')} ({nChanges})
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={<Icon type="check" />}
|
||||
bs5={<MaterialIcon type="check" />}
|
||||
/>
|
||||
|
||||
{t('accept_all')} ({nChanges})
|
||||
</BulkActions.Button>
|
||||
<BulkActions.Button onClick={handleShowBulkRejectDialog}>
|
||||
<Icon type="times" /> {t('reject_all')} ({nChanges})
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={<Icon type="times" />}
|
||||
bs5={<MaterialIcon type="close" />}
|
||||
/>
|
||||
|
||||
{t('reject_all')} ({nChanges})
|
||||
</BulkActions.Button>
|
||||
</>
|
||||
)}
|
||||
|
@ -83,7 +95,12 @@ function EditorWidgets() {
|
|||
!isRestrictedTokenMember &&
|
||||
currentDocEntries?.['add-comment'] && (
|
||||
<AddCommentButton onClick={handleAddNewCommentClick}>
|
||||
<Icon type="comment" /> {t('add_comment')}
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={<Icon type="comment" />}
|
||||
bs5={<MaterialIcon type="mode_comment" />}
|
||||
/>
|
||||
|
||||
{t('add_comment')}
|
||||
</AddCommentButton>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -11,6 +11,9 @@ import {
|
|||
useReviewPanelValueContext,
|
||||
} from '../../../context/review-panel/review-panel-context'
|
||||
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() {
|
||||
const { t } = useTranslation()
|
||||
|
@ -116,9 +119,7 @@ function AddCommentEntry() {
|
|||
<>
|
||||
<div className="rp-new-comment">
|
||||
{isSubmitting ? (
|
||||
<div className="rp-loading">
|
||||
<Icon type="spinner" spin />
|
||||
</div>
|
||||
<LoadingSpinner className="d-flex justify-content-center" />
|
||||
) : (
|
||||
<AutoExpandingTextArea
|
||||
className="rp-comment-input"
|
||||
|
@ -137,19 +138,34 @@ function AddCommentEntry() {
|
|||
className="rp-entry-button-cancel"
|
||||
onClick={handleCancelNewComment}
|
||||
>
|
||||
<Icon type="times" /> {t('cancel')}
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={<Icon type="times" />}
|
||||
bs5={<MaterialIcon type="close" />}
|
||||
/>
|
||||
|
||||
{t('cancel')}
|
||||
</EntryActions.Button>
|
||||
<EntryActions.Button
|
||||
onClick={handleSubmitNewComment}
|
||||
disabled={isSubmitting || !content.length}
|
||||
>
|
||||
<Icon type="comment" /> {t('comment')}
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={<Icon type="comment" />}
|
||||
bs5={<MaterialIcon type="mode_comment" />}
|
||||
/>
|
||||
|
||||
{t('comment')}
|
||||
</EntryActions.Button>
|
||||
</EntryActions>
|
||||
</>
|
||||
) : (
|
||||
<AddCommentButton onClick={handleStartNewComment}>
|
||||
<Icon type="comment" /> {t('add_comment')}
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={<Icon type="comment" />}
|
||||
bs5={<MaterialIcon type="mode_comment" />}
|
||||
/>
|
||||
|
||||
{t('add_comment')}
|
||||
</AddCommentButton>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -12,6 +12,8 @@ import { BaseChangeEntryProps } from '../types/base-change-entry-props'
|
|||
import useIndicatorHover from '../hooks/use-indicator-hover'
|
||||
import EntryIndicator from './entry-indicator'
|
||||
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 {
|
||||
replacedContent: string
|
||||
|
@ -80,7 +82,10 @@ function AggregateChangeEntry({
|
|||
onMouseEnter={handleIndicatorMouseEnter}
|
||||
onClick={handleIndicatorClick}
|
||||
>
|
||||
<Icon type="pencil" />
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={<Icon type="pencil" />}
|
||||
bs5={<MaterialIcon type="edit" />}
|
||||
/>
|
||||
</EntryIndicator>
|
||||
<div
|
||||
className={classnames('rp-entry', 'rp-entry-aggregate', {
|
||||
|
@ -89,7 +94,10 @@ function AggregateChangeEntry({
|
|||
>
|
||||
<div className="rp-entry-body">
|
||||
<div className="rp-entry-action-icon">
|
||||
<Icon type="pencil" />
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={<Icon type="pencil" />}
|
||||
bs5={<MaterialIcon type="edit" />}
|
||||
/>
|
||||
</div>
|
||||
<div className="rp-entry-details">
|
||||
<div className="rp-entry-description">
|
||||
|
@ -97,7 +105,7 @@ function AggregateChangeEntry({
|
|||
<del className="rp-content-highlight">{deletionContent}</del>
|
||||
{deletionNeedsCollapsing && (
|
||||
<button
|
||||
className="rp-collapse-toggle btn-inline-link"
|
||||
className="rp-collapse-toggle"
|
||||
onClick={handleDeletionToggleCollapse}
|
||||
>
|
||||
{isDeletionCollapsed
|
||||
|
@ -109,7 +117,7 @@ function AggregateChangeEntry({
|
|||
<ins className="rp-content-highlight">{insertionContent}</ins>
|
||||
{insertionNeedsCollapsing && (
|
||||
<button
|
||||
className="rp-collapse-toggle btn-inline-link"
|
||||
className="rp-collapse-toggle"
|
||||
onClick={handleInsertionToggleCollapse}
|
||||
>
|
||||
{isInsertionCollapsed
|
||||
|
@ -139,10 +147,18 @@ function AggregateChangeEntry({
|
|||
{permissions.write && (
|
||||
<EntryActions>
|
||||
<EntryActions.Button onClick={() => rejectChanges(entryIds)}>
|
||||
<Icon type="times" /> {t('reject')}
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={<Icon type="times" />}
|
||||
bs5={<MaterialIcon type="close" />}
|
||||
/>
|
||||
{t('reject')}
|
||||
</EntryActions.Button>
|
||||
<EntryActions.Button onClick={() => acceptChanges(entryIds)}>
|
||||
<Icon type="check" /> {t('accept')}
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={<Icon type="check" />}
|
||||
bs5={<MaterialIcon type="check" />}
|
||||
/>
|
||||
{t('accept')}
|
||||
</EntryActions.Button>
|
||||
</EntryActions>
|
||||
)}
|
||||
|
|
|
@ -5,6 +5,8 @@ import Icon from '../../../../../../shared/components/icon'
|
|||
import BulkActions from './bulk-actions'
|
||||
import Modal, { useBulkActionsModal } from './modal'
|
||||
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 = {
|
||||
entryId: ReviewPanelBulkActionsEntry['type']
|
||||
|
@ -30,10 +32,18 @@ function BulkActionsEntry({ entryId, nChanges }: BulkActionsEntryProps) {
|
|||
<EntryCallout className="rp-entry-callout-bulk-actions" />
|
||||
<BulkActions className="rp-entry">
|
||||
<BulkActions.Button onClick={handleShowBulkRejectDialog}>
|
||||
<Icon type="times" /> {t('reject_all')} ({nChanges})
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={<Icon type="times" />}
|
||||
bs5={<MaterialIcon type="close" />}
|
||||
/>
|
||||
{t('reject_all')} ({nChanges})
|
||||
</BulkActions.Button>
|
||||
<BulkActions.Button onClick={handleShowBulkAcceptDialog}>
|
||||
<Icon type="check" /> {t('accept_all')} ({nChanges})
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={<Icon type="check" />}
|
||||
bs5={<MaterialIcon type="check" />}
|
||||
/>
|
||||
{t('accept_all')} ({nChanges})
|
||||
</BulkActions.Button>
|
||||
</BulkActions>
|
||||
</>
|
||||
|
|
|
@ -1,8 +1,13 @@
|
|||
import { useState, useCallback } from 'react'
|
||||
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 OLModal, {
|
||||
OLModalBody,
|
||||
OLModalFooter,
|
||||
OLModalHeader,
|
||||
OLModalTitle,
|
||||
} from '@/features/ui/components/ol/ol-modal'
|
||||
import OLButton from '@/features/ui/components/ol/ol-button'
|
||||
|
||||
type BulkActionsModalProps = {
|
||||
show: boolean
|
||||
|
@ -22,30 +27,28 @@ function Modal({
|
|||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<AccessibleModal show={show} onHide={() => setShow(false)}>
|
||||
<BootstrapModal.Header closeButton>
|
||||
<h3>{isAccept ? t('accept_all') : t('reject_all')}</h3>
|
||||
</BootstrapModal.Header>
|
||||
<BootstrapModal.Body>
|
||||
<OLModal show={show} onHide={() => setShow(false)}>
|
||||
<OLModalHeader closeButton>
|
||||
<OLModalTitle>
|
||||
{isAccept ? t('accept_all') : t('reject_all')}
|
||||
</OLModalTitle>
|
||||
</OLModalHeader>
|
||||
<OLModalBody>
|
||||
<p>
|
||||
{isAccept
|
||||
? t('bulk_accept_confirm', { nChanges })
|
||||
: t('bulk_reject_confirm', { nChanges })}
|
||||
</p>
|
||||
</BootstrapModal.Body>
|
||||
<BootstrapModal.Footer>
|
||||
<Button
|
||||
bsStyle={null}
|
||||
className="btn-secondary"
|
||||
onClick={() => setShow(false)}
|
||||
>
|
||||
</OLModalBody>
|
||||
<OLModalFooter>
|
||||
<OLButton variant="secondary" onClick={() => setShow(false)}>
|
||||
{t('cancel')}
|
||||
</Button>
|
||||
<Button bsStyle={null} className="btn-primary" onClick={onConfirm}>
|
||||
</OLButton>
|
||||
<OLButton variant="primary" onClick={onConfirm}>
|
||||
{t('ok')}
|
||||
</Button>
|
||||
</BootstrapModal.Footer>
|
||||
</AccessibleModal>
|
||||
</OLButton>
|
||||
</OLModalFooter>
|
||||
</OLModal>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -13,6 +13,8 @@ import comparePropsWithShallowArrayCompare from '../utils/compare-props-with-sha
|
|||
import useIndicatorHover from '../hooks/use-indicator-hover'
|
||||
import EntryIndicator from './entry-indicator'
|
||||
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 {
|
||||
type: ReviewPanelChangeEntry['type']
|
||||
|
@ -71,7 +73,14 @@ function ChangeEntry({
|
|||
onMouseEnter={handleIndicatorMouseEnter}
|
||||
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>
|
||||
<div
|
||||
className={classnames('rp-entry', `rp-entry-${type}`, {
|
||||
|
@ -81,7 +90,10 @@ function ChangeEntry({
|
|||
<div className="rp-entry-body">
|
||||
<div className="rp-entry-action-icon">
|
||||
{isInsert ? (
|
||||
<Icon type="pencil" />
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={<Icon type="pencil" />}
|
||||
bs5={<MaterialIcon type="edit" />}
|
||||
/>
|
||||
) : (
|
||||
<i className="rp-icon-delete" />
|
||||
)}
|
||||
|
@ -106,7 +118,7 @@ function ChangeEntry({
|
|||
)}
|
||||
{needsCollapsing && (
|
||||
<button
|
||||
className="rp-collapse-toggle btn-inline-link"
|
||||
className="rp-collapse-toggle"
|
||||
onClick={handleToggleCollapse}
|
||||
>
|
||||
{isCollapsed
|
||||
|
@ -137,10 +149,18 @@ function ChangeEntry({
|
|||
{permissions.write && (
|
||||
<EntryActions>
|
||||
<EntryActions.Button onClick={() => rejectChanges(entryIds)}>
|
||||
<Icon type="times" /> {t('reject')}
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={<Icon type="times" />}
|
||||
bs5={<MaterialIcon type="close" />}
|
||||
/>
|
||||
{t('reject')}
|
||||
</EntryActions.Button>
|
||||
<EntryActions.Button onClick={() => acceptChanges(entryIds)}>
|
||||
<Icon type="check" /> {t('accept')}
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={<Icon type="check" />}
|
||||
bs5={<MaterialIcon type="check" />}
|
||||
/>
|
||||
{t('accept')}
|
||||
</EntryActions.Button>
|
||||
</EntryActions>
|
||||
)}
|
||||
|
|
|
@ -16,6 +16,9 @@ import { ReviewPanelCommentEntry } from '../../../../../../../types/review-panel
|
|||
import useIndicatorHover from '../hooks/use-indicator-hover'
|
||||
import EntryIndicator from './entry-indicator'
|
||||
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 = {
|
||||
docId: DocId
|
||||
|
@ -120,7 +123,10 @@ function CommentEntry({
|
|||
onMouseEnter={handleIndicatorMouseEnter}
|
||||
onClick={handleIndicatorClick}
|
||||
>
|
||||
<Icon type="comment" />
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={<Icon type="comment" />}
|
||||
bs5={<MaterialIcon type="mode_comment" />}
|
||||
/>
|
||||
</EntryIndicator>
|
||||
<div
|
||||
className={classnames('rp-entry', 'rp-entry-comment', {
|
||||
|
@ -130,7 +136,7 @@ function CommentEntry({
|
|||
ref={entryDivRef}
|
||||
>
|
||||
{!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">
|
||||
{thread.messages.map(comment => (
|
||||
|
@ -143,9 +149,7 @@ function CommentEntry({
|
|||
))}
|
||||
</div>
|
||||
{submitting && (
|
||||
<div className="rp-loading">
|
||||
<Icon type="spinner" spin />
|
||||
</div>
|
||||
<LoadingSpinner className="d-flex justify-content-center" />
|
||||
)}
|
||||
{permissions.comment && (
|
||||
<div className="rp-comment-reply">
|
||||
|
@ -163,7 +167,11 @@ function CommentEntry({
|
|||
<EntryActions>
|
||||
{permissions.comment && permissions.write && (
|
||||
<EntryActions.Button onClick={handleAnimateAndCallOnResolve}>
|
||||
<Icon type="inbox" /> {t('resolve')}
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={<Icon type="inbox" />}
|
||||
bs5={<MaterialIcon type="inbox" />}
|
||||
/>
|
||||
{t('resolve')}
|
||||
</EntryActions.Button>
|
||||
)}
|
||||
{permissions.comment && (
|
||||
|
@ -171,7 +179,11 @@ function CommentEntry({
|
|||
onClick={handleOnReply}
|
||||
disabled={!replyContent.length}
|
||||
>
|
||||
<Icon type="reply" /> {t('reply')}
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={<Icon type="reply" />}
|
||||
bs5={<MaterialIcon type="reply" />}
|
||||
/>
|
||||
{t('reply')}
|
||||
</EntryActions.Button>
|
||||
)}
|
||||
</EntryActions>
|
||||
|
|
|
@ -61,7 +61,7 @@ function ResolvedCommentEntry({
|
|||
<>
|
||||
|
||||
<button
|
||||
className="rp-collapse-toggle btn-inline-link"
|
||||
className="rp-collapse-toggle"
|
||||
onClick={() => setIsCollapsed(value => !value)}
|
||||
>
|
||||
{isCollapsed ? `… (${t('show_all')})` : ` (${t('show_less')})`}
|
||||
|
|
|
@ -8,6 +8,8 @@ import {
|
|||
import { isCurrentFileView, isOverviewView } from '../../utils/sub-view'
|
||||
import { useCallback } from 'react'
|
||||
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() {
|
||||
const { t } = useTranslation()
|
||||
|
@ -40,7 +42,10 @@ function Nav() {
|
|||
})}
|
||||
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>
|
||||
</button>
|
||||
<button
|
||||
|
@ -55,7 +60,10 @@ function Nav() {
|
|||
})}
|
||||
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>
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
@ -2,11 +2,11 @@ import Container from './container'
|
|||
import Toggler from './toggler'
|
||||
import Toolbar from './toolbar/toolbar'
|
||||
import Nav from './nav'
|
||||
import Icon from '../../../../shared/components/icon'
|
||||
import OverviewFile from './overview-file'
|
||||
import { useReviewPanelValueContext } from '../../context/review-panel/review-panel-context'
|
||||
import { useFileTreeData } from '@/shared/context/file-tree-data-context'
|
||||
import { memo } from 'react'
|
||||
import LoadingSpinner from '@/shared/components/loading-spinner'
|
||||
|
||||
function OverviewContainer() {
|
||||
const { isOverviewLoading } = useReviewPanelValueContext()
|
||||
|
@ -24,9 +24,7 @@ function OverviewContainer() {
|
|||
aria-labelledby="review-panel-tab-overview"
|
||||
>
|
||||
{isOverviewLoading ? (
|
||||
<div className="rp-loading">
|
||||
<Icon type="spinner" spin />
|
||||
</div>
|
||||
<LoadingSpinner className="d-flex justify-content-center my-2" />
|
||||
) : (
|
||||
docs?.map(doc => (
|
||||
<OverviewFile
|
||||
|
|
|
@ -8,6 +8,8 @@ import classnames from 'classnames'
|
|||
import { ReviewPanelDocEntries } from '../../../../../../types/review-panel/review-panel'
|
||||
import { MainDocument } from '../../../../../../types/project-settings'
|
||||
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 = {
|
||||
docId: MainDocument['doc']['id']
|
||||
|
@ -45,7 +47,10 @@ function OverviewFile({ docId, docPath }: OverviewFileProps) {
|
|||
'rp-overview-file-header-collapse-on': docCollapsed,
|
||||
})}
|
||||
>
|
||||
<Icon type="angle-down" />
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={<Icon type="angle-down" />}
|
||||
bs5={<MaterialIcon type="expand_more" />}
|
||||
/>
|
||||
</span>
|
||||
{docPath}
|
||||
{docCollapsed && (
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { useEffect, useRef } from 'react'
|
||||
import { useEffect, useLayoutEffect, useRef, useCallback } from 'react'
|
||||
import { useLayoutContext } from '../../../../shared/context/layout-context'
|
||||
import {
|
||||
ReviewPanelEntry,
|
||||
|
@ -6,7 +6,6 @@ import {
|
|||
} from '../../../../../../types/review-panel/entry'
|
||||
import { debugConsole } from '../../../../utils/debugging'
|
||||
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 { dispatchReviewPanelLayout } from '../../extensions/changes/change-manager'
|
||||
import { isEqual } from 'lodash'
|
||||
|
@ -214,255 +213,269 @@ function PositionedEntries({
|
|||
previousLayoutInfoRef.current = initialLayoutInfo
|
||||
}
|
||||
|
||||
const layout = (animate = true) => {
|
||||
const container = containerRef.current
|
||||
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 layout = useCallback(
|
||||
(animate = true) => {
|
||||
const container = containerRef.current
|
||||
if (!container) {
|
||||
return
|
||||
}
|
||||
|
||||
const entry = entries.find(value => value[0] === entryId)?.[1]
|
||||
if (!entry) {
|
||||
throw new Error(`Could not find an entry for ID ${entryId}`)
|
||||
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]
|
||||
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>(
|
||||
'.rp-entry-indicator'
|
||||
if (entryViews.length === 0) {
|
||||
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) {
|
||||
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,
|
||||
// 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,
|
||||
})
|
||||
} else {
|
||||
debugConsole.log(
|
||||
'Entry wrapper is missing indicator, box or callout, so ignoring',
|
||||
wrapper
|
||||
)
|
||||
|
||||
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,
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (entryViews.length === 0) {
|
||||
resetLayout()
|
||||
return
|
||||
}
|
||||
// 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)
|
||||
|
||||
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:
|
||||
//
|
||||
// - 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/
|
||||
container.classList.remove('no-animate')
|
||||
}
|
||||
}
|
||||
|
||||
// First, update display for each entry that needs it
|
||||
hideOrShowEntries(entryViews)
|
||||
previousLayoutInfoRef.current = {
|
||||
positions,
|
||||
focusedEntryIndex,
|
||||
height,
|
||||
overflowTop,
|
||||
}
|
||||
},
|
||||
[entries, lineHeight, navHeight, reviewPanelOpen, toolbarHeight]
|
||||
)
|
||||
|
||||
// Next, measure the height of each entry
|
||||
for (const entryView of entryViews) {
|
||||
if (entryView.hasScreenPos) {
|
||||
entryView.height = entryView.layout.offsetHeight
|
||||
useLayoutEffect(() => {
|
||||
const callback = (event: Event) => {
|
||||
const e = event as CustomEvent
|
||||
|
||||
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
|
||||
// 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)
|
||||
}
|
||||
window.addEventListener('review-panel:layout', callback)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('review-panel:layout', callback)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
})
|
||||
}, [layoutSuspended, layout])
|
||||
|
||||
// Layout on first render. This is necessary to ensure layout happens when
|
||||
// switching from overview to current file view
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
import { useTranslation } from 'react-i18next'
|
||||
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() {
|
||||
const { t } = useTranslation()
|
||||
|
@ -18,6 +21,12 @@ function Toggler() {
|
|||
onClick={handleTogglerClick}
|
||||
>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import { useState, useMemo, useCallback } from 'react'
|
||||
import Icon from '../../../../../shared/components/icon'
|
||||
import Tooltip from '../../../../../shared/components/tooltip'
|
||||
import ResolvedCommentsScroller from './resolved-comments-scroller'
|
||||
import classnames from 'classnames'
|
||||
import {
|
||||
|
@ -16,6 +15,10 @@ import { ReviewPanelResolvedCommentThread } from '../../../../../../../types/rev
|
|||
import { DocId } from '../../../../../../../types/project-settings'
|
||||
import { ReviewPanelEntry } from '../../../../../../../types/review-panel/entry'
|
||||
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
|
||||
extends ReviewPanelResolvedCommentThread {
|
||||
|
@ -97,7 +100,7 @@ function ResolvedCommentsDropdown() {
|
|||
onClick={() => setIsOpen(false)}
|
||||
/>
|
||||
|
||||
<Tooltip
|
||||
<OLTooltip
|
||||
id="resolved-comments-toggle"
|
||||
description={t('resolved_comments')}
|
||||
overlayProps={{ container: document.body, placement: 'bottom' }}
|
||||
|
@ -107,9 +110,12 @@ function ResolvedCommentsDropdown() {
|
|||
onClick={handleResolvedCommentsClick}
|
||||
aria-label={t('resolved_comments')}
|
||||
>
|
||||
<Icon type="inbox" />
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={<Icon type="inbox" />}
|
||||
bs5={<MaterialIcon type="inbox" />}
|
||||
/>
|
||||
</button>
|
||||
</Tooltip>
|
||||
</OLTooltip>
|
||||
|
||||
<div
|
||||
className={classnames('resolved-comments-dropdown', {
|
||||
|
@ -117,9 +123,7 @@ function ResolvedCommentsDropdown() {
|
|||
})}
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="rp-loading">
|
||||
<Icon type="spinner" spin />
|
||||
</div>
|
||||
<LoadingSpinner className="d-flex justify-content-center my-2" />
|
||||
) : isOpen ? (
|
||||
<ResolvedCommentsScroller
|
||||
resolvedComments={filteredResolvedComments}
|
||||
|
|
|
@ -31,7 +31,7 @@ function ResolvedCommentsScroller({
|
|||
/>
|
||||
))}
|
||||
{!resolvedComments.length && (
|
||||
<div className="rp-loading">{t('no_resolved_threads')}</div>
|
||||
<div className="text-center p-1">{t('no_resolved_threads')}</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -10,6 +10,8 @@ import {
|
|||
} from '../../../context/review-panel/review-panel-context'
|
||||
import { send, sendMB } from '../../../../../infrastructure/event-tracking'
|
||||
import classnames from 'classnames'
|
||||
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
|
||||
const sendAnalytics = () => {
|
||||
send('subscription-funnel', 'editor-click-feature', 'real-time-track-changes')
|
||||
|
@ -39,7 +41,7 @@ function ToggleMenu() {
|
|||
<span className="review-panel-toolbar-label">
|
||||
{wantTrackChanges && (
|
||||
<span className="review-panel-toolbar-icon-on">
|
||||
<Icon type="circle" />
|
||||
<span className="track-changes-indicator-circle" />
|
||||
</span>
|
||||
)}
|
||||
|
||||
|
@ -47,13 +49,18 @@ function ToggleMenu() {
|
|||
className="review-panel-toolbar-collapse-button"
|
||||
onClick={handleToggleFullTCStateCollapse}
|
||||
>
|
||||
{wantTrackChanges ? <TrackChangesOn /> : <TrackChangesOff />}
|
||||
<span>
|
||||
{wantTrackChanges ? <TrackChangesOn /> : <TrackChangesOff />}
|
||||
</span>
|
||||
<span
|
||||
className={classnames('rp-tc-state-collapse', {
|
||||
'rp-tc-state-collapse-on': shouldCollapse,
|
||||
})}
|
||||
>
|
||||
<Icon type="angle-down" />
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={<Icon type="angle-down" />}
|
||||
bs5={<MaterialIcon type="expand_more" />}
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
</span>
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
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 {
|
||||
useReviewPanelUpdaterFnsContext,
|
||||
|
@ -7,6 +6,7 @@ import {
|
|||
} from '@/features/source-editor/context/review-panel/review-panel-context'
|
||||
import { useProjectContext } from '@/shared/context/project-context'
|
||||
import classnames from 'classnames'
|
||||
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
|
||||
|
||||
function TrackChangesMenu() {
|
||||
const { t } = useTranslation()
|
||||
|
@ -28,7 +28,7 @@ function TrackChangesMenu() {
|
|||
return (
|
||||
<ul className="rp-tc-state" data-testid="review-panel-track-changes-menu">
|
||||
<li className="rp-tc-state-item rp-tc-state-item-everyone">
|
||||
<Tooltip
|
||||
<OLTooltip
|
||||
description={t('tc_switch_everyone_tip')}
|
||||
id="track-changes-switch-everyone"
|
||||
overlayProps={{
|
||||
|
@ -38,7 +38,7 @@ function TrackChangesMenu() {
|
|||
}}
|
||||
>
|
||||
<span className="rp-tc-state-item-name">{t('tc_everyone')}</span>
|
||||
</Tooltip>
|
||||
</OLTooltip>
|
||||
|
||||
<TrackChangesToggle
|
||||
id="track-changes-everyone"
|
||||
|
@ -52,7 +52,7 @@ function TrackChangesMenu() {
|
|||
</li>
|
||||
{Object.values(formattedProjectMembers).map(member => (
|
||||
<li className="rp-tc-state-item" key={member.id}>
|
||||
<Tooltip
|
||||
<OLTooltip
|
||||
description={t('tc_switch_user_tip')}
|
||||
id="track-changes-switch-user"
|
||||
overlayProps={{
|
||||
|
@ -68,7 +68,7 @@ function TrackChangesMenu() {
|
|||
>
|
||||
{member.name}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</OLTooltip>
|
||||
|
||||
<TrackChangesToggle
|
||||
id={`track-changes-user-toggle-${member.id}`}
|
||||
|
@ -90,7 +90,7 @@ function TrackChangesMenu() {
|
|||
))}
|
||||
<li className="rp-tc-state-separator" />
|
||||
<li className="rp-tc-state-item">
|
||||
<Tooltip
|
||||
<OLTooltip
|
||||
description={t('tc_switch_guests_tip')}
|
||||
id="track-changes-switch-guests"
|
||||
overlayProps={{
|
||||
|
@ -106,7 +106,7 @@ function TrackChangesMenu() {
|
|||
>
|
||||
{t('tc_guests')}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</OLTooltip>
|
||||
|
||||
<TrackChangesToggle
|
||||
id="track-changes-guests-toggle"
|
||||
|
|
|
@ -1,12 +1,21 @@
|
|||
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 { useProjectContext } from '../../../../shared/context/project-context'
|
||||
import { useUserContext } from '../../../../shared/context/user-context'
|
||||
import { startFreeTrial, upgradePlan } from '../../../../main/account-upgrade'
|
||||
import { memo } from 'react'
|
||||
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 = {
|
||||
show: boolean
|
||||
|
@ -24,11 +33,11 @@ function UpgradeTrackChangesModal({
|
|||
const hasNewPaywallCta = useFeatureFlag('paywall-cta')
|
||||
|
||||
return (
|
||||
<AccessibleModal show={show} onHide={() => setShow(false)}>
|
||||
<Modal.Header closeButton>
|
||||
<h3>{t('upgrade_to_track_changes')}</h3>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<OLModal show={show} onHide={() => setShow(false)}>
|
||||
<OLModalHeader closeButton>
|
||||
<OLModalTitle>{t('upgrade_to_track_changes')}</OLModalTitle>
|
||||
</OLModalHeader>
|
||||
<OLModalBody>
|
||||
<div className="teaser-video-container">
|
||||
{/* eslint-disable-next-line jsx-a11y/media-has-caption */}
|
||||
<video className="teaser-video" autoPlay loop>
|
||||
|
@ -45,8 +54,8 @@ function UpgradeTrackChangesModal({
|
|||
<h4 className="teaser-title">
|
||||
{t('see_changes_in_your_documents_live')}
|
||||
</h4>
|
||||
<Row>
|
||||
<Col md={10} mdOffset={1}>
|
||||
<OLRow>
|
||||
<OLCol lg={{ span: 10, offset: 1 }}>
|
||||
<ul className="list-unstyled">
|
||||
{[
|
||||
t('track_any_change_in_real_time'),
|
||||
|
@ -54,36 +63,43 @@ function UpgradeTrackChangesModal({
|
|||
t('accept_or_reject_each_changes_individually'),
|
||||
].map(translation => (
|
||||
<li key={translation}>
|
||||
<Icon type="check" /> {translation}
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={<Icon type="check" />}
|
||||
bs5={
|
||||
<MaterialIcon
|
||||
type="check"
|
||||
className="align-text-bottom"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{translation}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Col>
|
||||
</Row>
|
||||
</OLCol>
|
||||
</OLRow>
|
||||
<p className="small">
|
||||
{t('already_subscribed_try_refreshing_the_page')}
|
||||
</p>
|
||||
{project.owner && (
|
||||
<Row className="text-center">
|
||||
<div className="text-center">
|
||||
{project.owner._id === user.id ? (
|
||||
user.allowedFreeTrial ? (
|
||||
<Button
|
||||
bsStyle={null}
|
||||
className="btn-primary"
|
||||
<OLButton
|
||||
variant="primary"
|
||||
onClick={() => startFreeTrial('track-changes')}
|
||||
>
|
||||
{hasNewPaywallCta
|
||||
? t('get_track_changes')
|
||||
: t('try_it_for_free')}
|
||||
</Button>
|
||||
</OLButton>
|
||||
) : (
|
||||
<Button
|
||||
bsStyle={null}
|
||||
className="btn-primary"
|
||||
<OLButton
|
||||
variant="primary"
|
||||
onClick={() => upgradePlan('project-sharing')}
|
||||
>
|
||||
{t('upgrade')}
|
||||
</Button>
|
||||
</OLButton>
|
||||
)
|
||||
) : (
|
||||
<p>
|
||||
|
@ -94,19 +110,15 @@ function UpgradeTrackChangesModal({
|
|||
</strong>
|
||||
</p>
|
||||
)}
|
||||
</Row>
|
||||
</div>
|
||||
)}
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button
|
||||
bsStyle={null}
|
||||
className="btn-secondary"
|
||||
onClick={() => setShow(false)}
|
||||
>
|
||||
</OLModalBody>
|
||||
<OLModalFooter>
|
||||
<OLButton variant="secondary" onClick={() => setShow(false)}>
|
||||
{t('close')}
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</AccessibleModal>
|
||||
</OLButton>
|
||||
</OLModalFooter>
|
||||
</OLModal>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -39,10 +39,7 @@ function LoadingSpinner({
|
|||
}
|
||||
|
||||
const extraClasses = isBootstrap5()
|
||||
? [
|
||||
'd-inline-flex',
|
||||
align === 'left' ? 'align-items-start' : 'align-items-center',
|
||||
]
|
||||
? [align === 'left' ? 'align-items-start' : 'align-items-center']
|
||||
: null
|
||||
|
||||
return (
|
||||
|
|
|
@ -9,10 +9,8 @@
|
|||
@rp-border-grey: #d9d9d9;
|
||||
|
||||
@rp-green: #2c8e30;
|
||||
@rp-green-on-dark: rgba(37, 107, 41, 0.5);
|
||||
@rp-red: #c5060b;
|
||||
@rp-yellow: #f3b111;
|
||||
@rp-yellow-on-dark: rgba(194, 93, 11, 0.5);
|
||||
@rp-grey: #aaaaaa;
|
||||
|
||||
@rp-type-blue: #6b7797;
|
||||
|
@ -24,8 +22,6 @@
|
|||
@review-panel-width: 230px;
|
||||
@review-off-width: 22px;
|
||||
|
||||
@rp-toolbar-height: 32px;
|
||||
|
||||
@rp-entry-animation-speed: 0.3s;
|
||||
|
||||
.rp-button() {
|
||||
|
@ -64,12 +60,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.loading-panel {
|
||||
.rp-size-expanded & {
|
||||
right: @review-panel-width;
|
||||
}
|
||||
}
|
||||
|
||||
.review-panel-toolbar {
|
||||
display: none;
|
||||
|
||||
|
@ -105,10 +95,6 @@
|
|||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.review-panel-toolbar-spinner {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.rp-tc-state {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
|
@ -173,10 +159,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.rp-entry-list-inner {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.rp-entry-indicator {
|
||||
display: none;
|
||||
z-index: 2; // above .review-panel-toggler
|
||||
|
@ -752,11 +734,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.rp-loading {
|
||||
text-align: center;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.rp-nav {
|
||||
display: none;
|
||||
flex-shrink: 0;
|
||||
|
@ -806,18 +783,6 @@
|
|||
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 {
|
||||
display: inline-block;
|
||||
background: url('../../../../public/img/ol-icons/review-icon-dark-theme.svg')
|
||||
|
@ -919,6 +884,7 @@ button when (@is-overleaf-light = true) {
|
|||
}
|
||||
|
||||
.rp-collapse-toggle {
|
||||
.btn-inline-link;
|
||||
color: @rp-type-blue;
|
||||
font-weight: @rp-semibold-weight;
|
||||
|
||||
|
@ -979,8 +945,8 @@ button when (@is-overleaf-light = true) {
|
|||
}
|
||||
|
||||
.rp-size-expanded & {
|
||||
&::after {
|
||||
content: '\f105';
|
||||
.review-panel-toggler-icon .fa {
|
||||
transform: scale(0.7) rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -990,20 +956,16 @@ button when (@is-overleaf-light = true) {
|
|||
background-color: #fff;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '\f104';
|
||||
font-family: FontAwesome;
|
||||
line-height: 1;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
font-size: 16px;
|
||||
display: block;
|
||||
.review-panel-toggler-icon {
|
||||
position: sticky;
|
||||
top: 50%;
|
||||
bottom: 50%;
|
||||
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 {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.review-panel-mini {
|
||||
width: 22px !important;
|
||||
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;
|
||||
}
|
||||
}
|
||||
.track-changes-indicator-circle {
|
||||
display: inline-block;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 100%;
|
||||
background-color: @ol-green;
|
||||
}
|
||||
|
|
|
@ -143,3 +143,33 @@
|
|||
--link-hover-color: var(--link-hover-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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
.loading {
|
||||
display: inline-flex;
|
||||
|
||||
.spinner-border-sm,
|
||||
.spinner-border {
|
||||
// Ensure the thickness of the spinner is independent of the font size of its container
|
||||
|
|
|
@ -294,8 +294,8 @@ $editor-toggler-bg-dark-color: color.adjust(
|
|||
}
|
||||
|
||||
.teaser-video-container {
|
||||
margin: calc(var(--spacing-07) * -1) calc(var(--spacing-07) * -1)
|
||||
var(--spacing-02) calc(var(--spacing-07) * -1);
|
||||
margin: calc(var(--bs-modal-padding) * -1) calc(var(--bs-modal-padding) * -1)
|
||||
var(--spacing-02) calc(var(--bs-modal-padding) * -1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -84,3 +84,11 @@
|
|||
position: absolute;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.d-flex {
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
.justify-content-center {
|
||||
justify-content: center !important;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue