Merge pull request #20617 from overleaf/dk-review-indicator-hover

Improve "mini" view popovers for new review panel

GitOrigin-RevId: ff5fc0af70bd9660d5cc17437b25824ef4c9a704
This commit is contained in:
David 2024-10-02 12:58:46 +01:00 committed by Copybot
parent 46198cd780
commit 56d396a6cf
6 changed files with 204 additions and 136 deletions

View file

@ -23,146 +23,172 @@ export const ReviewPanelChange = memo<{
editable?: boolean
docId: string
hoverRanges?: boolean
}>(({ change, aggregate, top, docId, hoverRanges, editable = true }) => {
const { t } = useTranslation()
const { acceptChanges, rejectChanges } = useRangesActionsContext()
const permissions = usePermissionsContext()
const changesUsers = useChangesUsersContext()
hovered?: boolean
onEnter?: () => void
onLeave?: () => void
}>(
({
change,
aggregate,
top,
docId,
hoverRanges,
editable = true,
hovered,
onEnter,
onLeave,
}) => {
const { t } = useTranslation()
const { acceptChanges, rejectChanges } = useRangesActionsContext()
const permissions = usePermissionsContext()
const changesUsers = useChangesUsersContext()
if (!changesUsers) {
// if users are not loaded yet, do not show "Unknown" user
return null
}
if (!changesUsers) {
// if users are not loaded yet, do not show "Unknown" user
return null
}
return (
<ReviewPanelEntry
className={classnames('review-panel-entry-change', {
'review-panel-entry-insert': 'i' in change.op,
'review-panel-entry-delete': 'd' in change.op,
// TODO: aggregate
})}
top={top}
op={change.op}
position={change.op.p}
docId={docId}
hoverRanges={hoverRanges}
>
<div className="review-panel-entry-indicator">
<MaterialIcon type="edit" className="review-panel-entry-icon" />
</div>
return (
<ReviewPanelEntry
className={classnames('review-panel-entry-change', {
'review-panel-entry-insert': 'i' in change.op,
'review-panel-entry-delete': 'd' in change.op,
'review-panel-entry-hover': hovered,
// TODO: aggregate
})}
top={top}
op={change.op}
position={change.op.p}
docId={docId}
hoverRanges={hoverRanges}
>
<div
className="review-panel-entry-indicator"
onMouseEnter={onEnter}
onMouseLeave={onLeave}
>
<MaterialIcon type="edit" className="review-panel-entry-icon" />
</div>
<div className="review-panel-entry-content">
<div className="review-panel-entry-header">
<div>
<div className="review-panel-entry-user">
<ReviewPanelChangeUser change={change} />
</div>
<div className="review-panel-entry-time">
{formatTimeBasedOnYear(change.metadata?.ts)}
<div
className="review-panel-entry-content"
onMouseEnter={onEnter}
onMouseLeave={onLeave}
>
<div className="review-panel-entry-header">
<div>
<div className="review-panel-entry-user">
<ReviewPanelChangeUser change={change} />
</div>
<div className="review-panel-entry-time">
{formatTimeBasedOnYear(change.metadata?.ts)}
</div>
</div>
{editable && permissions.write && (
<div className="review-panel-entry-actions">
<Tooltip
id="accept-change"
overlayProps={{ placement: 'bottom' }}
description={t('accept_change')}
tooltipProps={{ className: 'review-panel-tooltip' }}
>
<Button
onClick={() =>
aggregate
? acceptChanges(change.id, aggregate.id)
: acceptChanges(change.id)
}
bsStyle={null}
>
<MaterialIcon
type="check"
className="review-panel-entry-actions-icon"
accessibilityLabel={t('accept_change')}
/>
</Button>
</Tooltip>
<Tooltip
id="reject-change"
description={t('reject_change')}
overlayProps={{ placement: 'bottom' }}
tooltipProps={{ className: 'review-panel-tooltip' }}
>
<Button
bsStyle={null}
onClick={() =>
aggregate
? rejectChanges(change.id, aggregate.id)
: rejectChanges(change.id)
}
>
<MaterialIcon
className="review-panel-entry-actions-icon"
accessibilityLabel={t('reject_change')}
type="close"
/>
</Button>
</Tooltip>
</div>
)}
</div>
{editable && permissions.write && (
<div className="review-panel-entry-actions">
<Tooltip
id="accept-change"
overlayProps={{ placement: 'bottom' }}
description={t('accept_change')}
>
<Button
onClick={() =>
aggregate
? acceptChanges(change.id, aggregate.id)
: acceptChanges(change.id)
}
bsStyle={null}
>
<div className="review-panel-change-body">
{'i' in change.op && (
<>
{aggregate ? (
<MaterialIcon
type="check"
className="review-panel-entry-actions-icon"
accessibilityLabel={t('accept_change')}
className="review-panel-entry-icon review-panel-entry-icon-changed"
type="edit"
/>
</Button>
</Tooltip>
<Tooltip
id="reject-change"
description={t('reject_change')}
overlayProps={{ placement: 'bottom' }}
>
<Button
bsStyle={null}
onClick={() =>
aggregate
? rejectChanges(change.id, aggregate.id)
: rejectChanges(change.id)
}
>
) : (
<MaterialIcon
className="review-panel-entry-actions-icon"
accessibilityLabel={t('reject_change')}
type="close"
className="review-panel-entry-icon review-panel-entry-icon-accept"
type="add_circle"
/>
</Button>
</Tooltip>
</div>
)}
</div>
)}
<div className="review-panel-change-body">
{'i' in change.op && (
<>
{aggregate ? (
<MaterialIcon
className="review-panel-entry-icon review-panel-entry-icon-changed"
type="edit"
/>
) : (
<MaterialIcon
className="review-panel-entry-icon review-panel-entry-icon-accept"
type="add_circle"
/>
)}
{aggregate ? (
<span>
{t('aggregate_changed')}:{' '}
<del className="review-panel-content-highlight">
{aggregate.op.d}
</del>{' '}
{t('aggregate_to')}{' '}
<ins className="review-panel-content-highlight">
{change.op.i}
</ins>
</span>
) : (
<span>
{t('tracked_change_added')}:&nbsp;
<ins className="review-panel-content-highlight">
{change.op.i}
</ins>
</span>
)}
</>
)}
{'d' in change.op && (
<>
<MaterialIcon
className="review-panel-entry-icon review-panel-entry-icon-reject"
type="delete"
/>
{aggregate ? (
<span>
{t('aggregate_changed')}:{' '}
{t('tracked_change_deleted')}:&nbsp;
<del className="review-panel-content-highlight">
{aggregate.op.d}
</del>{' '}
{t('aggregate_to')}{' '}
<ins className="review-panel-content-highlight">
{change.op.i}
</ins>
{change.op.d}
</del>
</span>
) : (
<span>
{t('tracked_change_added')}:&nbsp;
<ins className="review-panel-content-highlight">
{change.op.i}
</ins>
</span>
)}
</>
)}
{'d' in change.op && (
<>
<MaterialIcon
className="review-panel-entry-icon review-panel-entry-icon-reject"
type="delete"
/>
<span>
{t('tracked_change_deleted')}:&nbsp;
<del className="review-panel-content-highlight">
{change.op.d}
</del>
</span>
</>
)}
</>
)}
</div>
</div>
</div>
</ReviewPanelEntry>
)
})
</ReviewPanelEntry>
)
}
)
ReviewPanelChange.displayName = 'ReviewPanelChange'

View file

@ -14,7 +14,9 @@ import useSubmittableTextInput from '../hooks/use-submittable-text-input'
export const ReviewPanelCommentContent = memo<{
comment: Change<CommentOperation>
isResolved: boolean
}>(({ comment, isResolved }) => {
onLeave?: () => void
onEnter?: () => void
}>(({ comment, isResolved, onLeave, onEnter }) => {
const { t } = useTranslation()
const [submitting, setSubmitting] = useState(false)
const [error, setError] = useState<Error>()
@ -47,7 +49,11 @@ export const ReviewPanelCommentContent = memo<{
}
return (
<div className="review-panel-entry-content">
<div
className="review-panel-entry-content"
onMouseEnter={onEnter}
onMouseLeave={onLeave}
>
{thread.messages.map((message, i) => {
const isReply = i !== 0

View file

@ -11,7 +11,10 @@ export const ReviewPanelComment = memo<{
docId: string
top?: number
hoverRanges?: boolean
}>(({ comment, top, docId, hoverRanges }) => {
onEnter?: () => void
onLeave?: () => void
hovered?: boolean
}>(({ comment, top, hovered, onEnter, onLeave, docId, hoverRanges }) => {
const threads = useThreadsContext()
const thread = threads?.[comment.op.t]
@ -23,6 +26,7 @@ export const ReviewPanelComment = memo<{
<ReviewPanelEntry
className={classnames('review-panel-entry-comment', {
'review-panel-entry-loaded': !!threads?.[comment.op.t],
'review-panel-entry-hover': hovered,
})}
docId={docId}
top={top}
@ -30,10 +34,19 @@ export const ReviewPanelComment = memo<{
position={comment.op.p}
hoverRanges={hoverRanges}
>
<div className="review-panel-entry-indicator">
<div
className="review-panel-entry-indicator"
onMouseEnter={onEnter}
onMouseLeave={onLeave}
>
<MaterialIcon type="comment" className="review-panel-entry-icon" />
</div>
<ReviewPanelCommentContent comment={comment} isResolved={false} />
<ReviewPanelCommentContent
comment={comment}
isResolved={false}
onLeave={onLeave}
onEnter={onEnter}
/>
</ReviewPanelEntry>
)
})

View file

@ -43,6 +43,20 @@ const ReviewPanelCurrentFile: FC = () => {
const ranges = useRangesContext()
const threads = useThreadsContext()
const state = useCodeMirrorStateContext()
const [hoveredEntry, setHoveredEntry] = useState<string | null>(null)
const hoverTimeout = useRef<number>(0)
const handleEntryEnter = useCallback((id: string) => {
clearTimeout(hoverTimeout.current)
setHoveredEntry(id)
}, [])
const handleEntryLeave = useCallback((id: string) => {
clearTimeout(hoverTimeout.current)
hoverTimeout.current = window.setTimeout(() => {
setHoveredEntry(null)
}, 100)
}, [])
const [aggregatedRanges, setAggregatedRanges] = useState<AggregatedRanges>()
@ -298,6 +312,9 @@ const ReviewPanelCurrentFile: FC = () => {
change={change}
top={positions.get(change.id)}
aggregate={aggregatedRanges.aggregates.get(change.id)}
hovered={hoveredEntry === change.id}
onEnter={() => handleEntryEnter(change.id)}
onLeave={() => handleEntryLeave(change.id)}
/>
)
)}
@ -310,6 +327,9 @@ const ReviewPanelCurrentFile: FC = () => {
key={comment.id}
comment={comment}
top={positions.get(comment.id)}
hovered={hoveredEntry === comment.id}
onEnter={() => handleEntryEnter(comment.id)}
onLeave={() => handleEntryLeave(comment.id)}
/>
)
)}

View file

@ -106,6 +106,7 @@ export const ReviewPanelMessage: FC<{
id="resolve-thread"
overlayProps={{ placement: 'bottom' }}
description={t('resolve_comment')}
tooltipProps={{ className: 'review-panel-tooltip' }}
>
<Button onClick={onResolve} bsStyle={null}>
<MaterialIcon

View file

@ -554,15 +554,14 @@
margin-left: 0;
background-color: transparent;
border: none;
width: 100%;
}
.review-panel-entry-indicator {
position: absolute;
left: 0;
top: 7px;
top: 0;
display: flex;
width: 16px;
height: 16px;
color: @content-secondary;
cursor: pointer;
}
@ -576,7 +575,7 @@
padding: @spacing-02;
}
.review-panel-entry:hover {
.review-panel-entry-hover {
.review-panel-entry-content {
display: initial;
position: absolute;
@ -599,3 +598,6 @@
align-items: center;
gap: 2px;
}
.review-panel-tooltip {
pointer-events: none; // this is to prevent mouseLeave event from firing when hovering over the tooltip
}