mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
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:
parent
46198cd780
commit
56d396a6cf
6 changed files with 204 additions and 136 deletions
|
@ -23,146 +23,172 @@ export const ReviewPanelChange = memo<{
|
||||||
editable?: boolean
|
editable?: boolean
|
||||||
docId: string
|
docId: string
|
||||||
hoverRanges?: boolean
|
hoverRanges?: boolean
|
||||||
}>(({ change, aggregate, top, docId, hoverRanges, editable = true }) => {
|
hovered?: boolean
|
||||||
const { t } = useTranslation()
|
onEnter?: () => void
|
||||||
const { acceptChanges, rejectChanges } = useRangesActionsContext()
|
onLeave?: () => void
|
||||||
const permissions = usePermissionsContext()
|
}>(
|
||||||
const changesUsers = useChangesUsersContext()
|
({
|
||||||
|
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 (!changesUsers) {
|
||||||
// if users are not loaded yet, do not show "Unknown" user
|
// if users are not loaded yet, do not show "Unknown" user
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReviewPanelEntry
|
<ReviewPanelEntry
|
||||||
className={classnames('review-panel-entry-change', {
|
className={classnames('review-panel-entry-change', {
|
||||||
'review-panel-entry-insert': 'i' in change.op,
|
'review-panel-entry-insert': 'i' in change.op,
|
||||||
'review-panel-entry-delete': 'd' in change.op,
|
'review-panel-entry-delete': 'd' in change.op,
|
||||||
// TODO: aggregate
|
'review-panel-entry-hover': hovered,
|
||||||
})}
|
// TODO: aggregate
|
||||||
top={top}
|
})}
|
||||||
op={change.op}
|
top={top}
|
||||||
position={change.op.p}
|
op={change.op}
|
||||||
docId={docId}
|
position={change.op.p}
|
||||||
hoverRanges={hoverRanges}
|
docId={docId}
|
||||||
>
|
hoverRanges={hoverRanges}
|
||||||
<div className="review-panel-entry-indicator">
|
>
|
||||||
<MaterialIcon type="edit" className="review-panel-entry-icon" />
|
<div
|
||||||
</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
|
||||||
<div className="review-panel-entry-header">
|
className="review-panel-entry-content"
|
||||||
<div>
|
onMouseEnter={onEnter}
|
||||||
<div className="review-panel-entry-user">
|
onMouseLeave={onLeave}
|
||||||
<ReviewPanelChangeUser change={change} />
|
>
|
||||||
</div>
|
<div className="review-panel-entry-header">
|
||||||
<div className="review-panel-entry-time">
|
<div>
|
||||||
{formatTimeBasedOnYear(change.metadata?.ts)}
|
<div className="review-panel-entry-user">
|
||||||
|
<ReviewPanelChangeUser change={change} />
|
||||||
|
</div>
|
||||||
|
<div className="review-panel-entry-time">
|
||||||
|
{formatTimeBasedOnYear(change.metadata?.ts)}
|
||||||
|
</div>
|
||||||
</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>
|
</div>
|
||||||
{editable && permissions.write && (
|
|
||||||
<div className="review-panel-entry-actions">
|
<div className="review-panel-change-body">
|
||||||
<Tooltip
|
{'i' in change.op && (
|
||||||
id="accept-change"
|
<>
|
||||||
overlayProps={{ placement: 'bottom' }}
|
{aggregate ? (
|
||||||
description={t('accept_change')}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
onClick={() =>
|
|
||||||
aggregate
|
|
||||||
? acceptChanges(change.id, aggregate.id)
|
|
||||||
: acceptChanges(change.id)
|
|
||||||
}
|
|
||||||
bsStyle={null}
|
|
||||||
>
|
|
||||||
<MaterialIcon
|
<MaterialIcon
|
||||||
type="check"
|
className="review-panel-entry-icon review-panel-entry-icon-changed"
|
||||||
className="review-panel-entry-actions-icon"
|
type="edit"
|
||||||
accessibilityLabel={t('accept_change')}
|
|
||||||
/>
|
/>
|
||||||
</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
|
<MaterialIcon
|
||||||
className="review-panel-entry-actions-icon"
|
className="review-panel-entry-icon review-panel-entry-icon-accept"
|
||||||
accessibilityLabel={t('reject_change')}
|
type="add_circle"
|
||||||
type="close"
|
|
||||||
/>
|
/>
|
||||||
</Button>
|
)}
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="review-panel-change-body">
|
{aggregate ? (
|
||||||
{'i' in change.op && (
|
<span>
|
||||||
<>
|
{t('aggregate_changed')}:{' '}
|
||||||
{aggregate ? (
|
<del className="review-panel-content-highlight">
|
||||||
<MaterialIcon
|
{aggregate.op.d}
|
||||||
className="review-panel-entry-icon review-panel-entry-icon-changed"
|
</del>{' '}
|
||||||
type="edit"
|
{t('aggregate_to')}{' '}
|
||||||
/>
|
<ins className="review-panel-content-highlight">
|
||||||
) : (
|
{change.op.i}
|
||||||
<MaterialIcon
|
</ins>
|
||||||
className="review-panel-entry-icon review-panel-entry-icon-accept"
|
</span>
|
||||||
type="add_circle"
|
) : (
|
||||||
/>
|
<span>
|
||||||
)}
|
{t('tracked_change_added')}:
|
||||||
|
<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>
|
<span>
|
||||||
{t('aggregate_changed')}:{' '}
|
{t('tracked_change_deleted')}:
|
||||||
<del className="review-panel-content-highlight">
|
<del className="review-panel-content-highlight">
|
||||||
{aggregate.op.d}
|
{change.op.d}
|
||||||
</del>{' '}
|
</del>
|
||||||
{t('aggregate_to')}{' '}
|
|
||||||
<ins className="review-panel-content-highlight">
|
|
||||||
{change.op.i}
|
|
||||||
</ins>
|
|
||||||
</span>
|
</span>
|
||||||
) : (
|
</>
|
||||||
<span>
|
)}
|
||||||
{t('tracked_change_added')}:
|
</div>
|
||||||
<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')}:
|
|
||||||
<del className="review-panel-content-highlight">
|
|
||||||
{change.op.d}
|
|
||||||
</del>
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</ReviewPanelEntry>
|
||||||
</ReviewPanelEntry>
|
)
|
||||||
)
|
}
|
||||||
})
|
)
|
||||||
ReviewPanelChange.displayName = 'ReviewPanelChange'
|
ReviewPanelChange.displayName = 'ReviewPanelChange'
|
||||||
|
|
|
@ -14,7 +14,9 @@ import useSubmittableTextInput from '../hooks/use-submittable-text-input'
|
||||||
export const ReviewPanelCommentContent = memo<{
|
export const ReviewPanelCommentContent = memo<{
|
||||||
comment: Change<CommentOperation>
|
comment: Change<CommentOperation>
|
||||||
isResolved: boolean
|
isResolved: boolean
|
||||||
}>(({ comment, isResolved }) => {
|
onLeave?: () => void
|
||||||
|
onEnter?: () => void
|
||||||
|
}>(({ comment, isResolved, onLeave, onEnter }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [submitting, setSubmitting] = useState(false)
|
const [submitting, setSubmitting] = useState(false)
|
||||||
const [error, setError] = useState<Error>()
|
const [error, setError] = useState<Error>()
|
||||||
|
@ -47,7 +49,11 @@ export const ReviewPanelCommentContent = memo<{
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="review-panel-entry-content">
|
<div
|
||||||
|
className="review-panel-entry-content"
|
||||||
|
onMouseEnter={onEnter}
|
||||||
|
onMouseLeave={onLeave}
|
||||||
|
>
|
||||||
{thread.messages.map((message, i) => {
|
{thread.messages.map((message, i) => {
|
||||||
const isReply = i !== 0
|
const isReply = i !== 0
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,10 @@ export const ReviewPanelComment = memo<{
|
||||||
docId: string
|
docId: string
|
||||||
top?: number
|
top?: number
|
||||||
hoverRanges?: boolean
|
hoverRanges?: boolean
|
||||||
}>(({ comment, top, docId, hoverRanges }) => {
|
onEnter?: () => void
|
||||||
|
onLeave?: () => void
|
||||||
|
hovered?: boolean
|
||||||
|
}>(({ comment, top, hovered, onEnter, onLeave, docId, hoverRanges }) => {
|
||||||
const threads = useThreadsContext()
|
const threads = useThreadsContext()
|
||||||
|
|
||||||
const thread = threads?.[comment.op.t]
|
const thread = threads?.[comment.op.t]
|
||||||
|
@ -23,6 +26,7 @@ export const ReviewPanelComment = memo<{
|
||||||
<ReviewPanelEntry
|
<ReviewPanelEntry
|
||||||
className={classnames('review-panel-entry-comment', {
|
className={classnames('review-panel-entry-comment', {
|
||||||
'review-panel-entry-loaded': !!threads?.[comment.op.t],
|
'review-panel-entry-loaded': !!threads?.[comment.op.t],
|
||||||
|
'review-panel-entry-hover': hovered,
|
||||||
})}
|
})}
|
||||||
docId={docId}
|
docId={docId}
|
||||||
top={top}
|
top={top}
|
||||||
|
@ -30,10 +34,19 @@ export const ReviewPanelComment = memo<{
|
||||||
position={comment.op.p}
|
position={comment.op.p}
|
||||||
hoverRanges={hoverRanges}
|
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" />
|
<MaterialIcon type="comment" className="review-panel-entry-icon" />
|
||||||
</div>
|
</div>
|
||||||
<ReviewPanelCommentContent comment={comment} isResolved={false} />
|
<ReviewPanelCommentContent
|
||||||
|
comment={comment}
|
||||||
|
isResolved={false}
|
||||||
|
onLeave={onLeave}
|
||||||
|
onEnter={onEnter}
|
||||||
|
/>
|
||||||
</ReviewPanelEntry>
|
</ReviewPanelEntry>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
@ -43,6 +43,20 @@ const ReviewPanelCurrentFile: FC = () => {
|
||||||
const ranges = useRangesContext()
|
const ranges = useRangesContext()
|
||||||
const threads = useThreadsContext()
|
const threads = useThreadsContext()
|
||||||
const state = useCodeMirrorStateContext()
|
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>()
|
const [aggregatedRanges, setAggregatedRanges] = useState<AggregatedRanges>()
|
||||||
|
|
||||||
|
@ -298,6 +312,9 @@ const ReviewPanelCurrentFile: FC = () => {
|
||||||
change={change}
|
change={change}
|
||||||
top={positions.get(change.id)}
|
top={positions.get(change.id)}
|
||||||
aggregate={aggregatedRanges.aggregates.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}
|
key={comment.id}
|
||||||
comment={comment}
|
comment={comment}
|
||||||
top={positions.get(comment.id)}
|
top={positions.get(comment.id)}
|
||||||
|
hovered={hoveredEntry === comment.id}
|
||||||
|
onEnter={() => handleEntryEnter(comment.id)}
|
||||||
|
onLeave={() => handleEntryLeave(comment.id)}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -106,6 +106,7 @@ export const ReviewPanelMessage: FC<{
|
||||||
id="resolve-thread"
|
id="resolve-thread"
|
||||||
overlayProps={{ placement: 'bottom' }}
|
overlayProps={{ placement: 'bottom' }}
|
||||||
description={t('resolve_comment')}
|
description={t('resolve_comment')}
|
||||||
|
tooltipProps={{ className: 'review-panel-tooltip' }}
|
||||||
>
|
>
|
||||||
<Button onClick={onResolve} bsStyle={null}>
|
<Button onClick={onResolve} bsStyle={null}>
|
||||||
<MaterialIcon
|
<MaterialIcon
|
||||||
|
|
|
@ -554,15 +554,14 @@
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.review-panel-entry-indicator {
|
.review-panel-entry-indicator {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
top: 7px;
|
top: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
color: @content-secondary;
|
color: @content-secondary;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
@ -576,7 +575,7 @@
|
||||||
padding: @spacing-02;
|
padding: @spacing-02;
|
||||||
}
|
}
|
||||||
|
|
||||||
.review-panel-entry:hover {
|
.review-panel-entry-hover {
|
||||||
.review-panel-entry-content {
|
.review-panel-entry-content {
|
||||||
display: initial;
|
display: initial;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -599,3 +598,6 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
}
|
}
|
||||||
|
.review-panel-tooltip {
|
||||||
|
pointer-events: none; // this is to prevent mouseLeave event from firing when hovering over the tooltip
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue