mirror of
https://github.com/overleaf/overleaf.git
synced 2024-10-24 21:12:38 -04: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 { 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" />}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{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" />}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{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" />}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{t('add_comment')}
|
||||||
</AddCommentButton>
|
</AddCommentButton>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -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" />}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{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" />}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{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" />}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{t('add_comment')}
|
||||||
</AddCommentButton>
|
</AddCommentButton>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -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" />}
|
||||||
|
/>
|
||||||
|
{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" />}
|
||||||
|
/>
|
||||||
|
{t('accept')}
|
||||||
</EntryActions.Button>
|
</EntryActions.Button>
|
||||||
</EntryActions>
|
</EntryActions>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -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" />}
|
||||||
|
/>
|
||||||
|
{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" />}
|
||||||
|
/>
|
||||||
|
{t('accept_all')} ({nChanges})
|
||||||
</BulkActions.Button>
|
</BulkActions.Button>
|
||||||
</BulkActions>
|
</BulkActions>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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" />}
|
||||||
|
/>
|
||||||
|
{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" />}
|
||||||
|
/>
|
||||||
|
{t('accept')}
|
||||||
</EntryActions.Button>
|
</EntryActions.Button>
|
||||||
</EntryActions>
|
</EntryActions>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -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" />}
|
||||||
|
/>
|
||||||
|
{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" />}
|
||||||
|
/>
|
||||||
|
{t('reply')}
|
||||||
</EntryActions.Button>
|
</EntryActions.Button>
|
||||||
)}
|
)}
|
||||||
</EntryActions>
|
</EntryActions>
|
||||||
|
|
|
@ -61,7 +61,7 @@ function ResolvedCommentEntry({
|
||||||
<>
|
<>
|
||||||
|
|
||||||
<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')})`}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 && (
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -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;
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue