first commit
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
export const buildName = (user?: {
|
||||
first_name?: string
|
||||
last_name?: string
|
||||
email?: string
|
||||
}) => {
|
||||
const name = [user?.first_name, user?.last_name].filter(Boolean).join(' ')
|
||||
|
||||
if (name) {
|
||||
return name
|
||||
}
|
||||
if (user?.email) {
|
||||
return user.email.split('@')[0]
|
||||
}
|
||||
return 'Unknown'
|
||||
}
|
@@ -0,0 +1,15 @@
|
||||
import {
|
||||
Change,
|
||||
DeleteOperation,
|
||||
InsertOperation,
|
||||
} from '../../../../../types/change'
|
||||
|
||||
export const canAggregate = (
|
||||
deletion: Change<DeleteOperation>,
|
||||
insertion: Change<InsertOperation>
|
||||
) =>
|
||||
deletion.metadata?.user_id &&
|
||||
// same user
|
||||
deletion.metadata?.user_id === insertion.metadata?.user_id &&
|
||||
// same position
|
||||
deletion.op.p === insertion.op.p + insertion.op.i.length
|
@@ -0,0 +1,44 @@
|
||||
import { SelectionRange } from '@codemirror/state'
|
||||
import { Ranges } from '@/features/review-panel-new/context/ranges-context'
|
||||
import { isDeleteChange, isInsertChange } from '@/utils/operations'
|
||||
import { canAggregate } from './can-aggregate'
|
||||
import { Change, EditOperation } from '../../../../../types/change'
|
||||
|
||||
export function numberOfChangesInSelection(
|
||||
ranges: Ranges | undefined,
|
||||
selection: SelectionRange
|
||||
) {
|
||||
if (!ranges) {
|
||||
return 0
|
||||
}
|
||||
|
||||
let count = 0
|
||||
let precedingChange: Change<EditOperation> | null = null
|
||||
|
||||
for (const change of ranges.changes) {
|
||||
if (
|
||||
precedingChange &&
|
||||
isInsertChange(precedingChange) &&
|
||||
isDeleteChange(change) &&
|
||||
canAggregate(change, precedingChange)
|
||||
) {
|
||||
// only count once for the aggregated change
|
||||
continue
|
||||
} else if (
|
||||
isInsertChange(change) &&
|
||||
change.op.p >= selection.from &&
|
||||
change.op.p + change.op.i.length <= selection.to
|
||||
) {
|
||||
count++
|
||||
} else if (
|
||||
isDeleteChange(change) &&
|
||||
selection.from <= change.op.p &&
|
||||
change.op.p <= selection.to
|
||||
) {
|
||||
count++
|
||||
}
|
||||
precedingChange = change
|
||||
}
|
||||
|
||||
return count
|
||||
}
|
@@ -0,0 +1,25 @@
|
||||
import { Ranges } from '@/features/review-panel-new/context/ranges-context'
|
||||
import { Threads } from '@/features/review-panel-new/context/threads-context'
|
||||
|
||||
export const hasActiveRange = (
|
||||
ranges: Ranges | undefined,
|
||||
threads: Threads | undefined
|
||||
): boolean | undefined => {
|
||||
if (!ranges || !threads) {
|
||||
// data isn't loaded yet
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (ranges.changes.length > 0) {
|
||||
// at least one tracked change
|
||||
return true
|
||||
}
|
||||
|
||||
for (const thread of Object.values(threads)) {
|
||||
if (!thread.resolved) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
@@ -0,0 +1,10 @@
|
||||
import { AnyOperation } from '../../../../../types/change'
|
||||
import { SelectionRange } from '@codemirror/state'
|
||||
import { visibleTextLength } from '@/utils/operations'
|
||||
|
||||
export const isSelectionWithinOp = (
|
||||
op: AnyOperation,
|
||||
range: SelectionRange
|
||||
): boolean => {
|
||||
return range.to >= op.p && range.from <= op.p + visibleTextLength(op)
|
||||
}
|
@@ -0,0 +1,92 @@
|
||||
import getMeta from '@/utils/meta'
|
||||
import { debounce } from 'lodash'
|
||||
|
||||
export const OFFSET_FOR_ENTRIES_ABOVE = 70
|
||||
const COLLAPSED_HEADER_HEIGHT = getMeta('ol-isReviewerRoleEnabled') ? 42 : 75
|
||||
const GAP_BETWEEN_ENTRIES = 4
|
||||
|
||||
export const positionItems = debounce(
|
||||
(
|
||||
element: HTMLDivElement,
|
||||
previousFocusedItemIndex: number | undefined,
|
||||
docId: string
|
||||
) => {
|
||||
const items = Array.from(
|
||||
element.querySelectorAll<HTMLDivElement>('.review-panel-entry')
|
||||
)
|
||||
|
||||
items.sort((a, b) => Number(a.dataset.pos) - Number(b.dataset.pos))
|
||||
|
||||
if (!items.length) {
|
||||
return
|
||||
}
|
||||
|
||||
let activeItemIndex = items.findIndex(item =>
|
||||
item.classList.contains('review-panel-entry-selected')
|
||||
)
|
||||
|
||||
if (activeItemIndex === -1) {
|
||||
// if entry was not selected manually
|
||||
// check if there is an entry in selection and use that as the focused item
|
||||
activeItemIndex = items.findIndex(item =>
|
||||
item.classList.contains('review-panel-entry-highlighted')
|
||||
)
|
||||
}
|
||||
|
||||
if (activeItemIndex === -1) {
|
||||
activeItemIndex = previousFocusedItemIndex || 0
|
||||
}
|
||||
|
||||
const activeItem = items[activeItemIndex]
|
||||
if (!activeItem) {
|
||||
return
|
||||
}
|
||||
|
||||
const activeItemTop = getTopPosition(activeItem, activeItemIndex === 0)
|
||||
|
||||
activeItem.style.top = `${activeItemTop}px`
|
||||
activeItem.style.visibility = 'visible'
|
||||
const focusedItemRect = activeItem.getBoundingClientRect()
|
||||
|
||||
// above the active item
|
||||
let topLimit = activeItemTop
|
||||
for (let i = activeItemIndex - 1; i >= 0; i--) {
|
||||
const item = items[i]
|
||||
const rect = item.getBoundingClientRect()
|
||||
let top = getTopPosition(item, i === 0)
|
||||
const bottom = top + rect.height
|
||||
if (bottom > topLimit) {
|
||||
top = topLimit - rect.height - GAP_BETWEEN_ENTRIES
|
||||
}
|
||||
item.style.top = `${top}px`
|
||||
item.style.visibility = 'visible'
|
||||
topLimit = top
|
||||
}
|
||||
|
||||
// below the active item
|
||||
let bottomLimit = activeItemTop + focusedItemRect.height
|
||||
for (let i = activeItemIndex + 1; i < items.length; i++) {
|
||||
const item = items[i]
|
||||
const rect = item.getBoundingClientRect()
|
||||
let top = getTopPosition(item, false)
|
||||
if (top < bottomLimit) {
|
||||
top = bottomLimit + GAP_BETWEEN_ENTRIES
|
||||
}
|
||||
item.style.top = `${top}px`
|
||||
item.style.visibility = 'visible'
|
||||
bottomLimit = top + rect.height
|
||||
}
|
||||
|
||||
return {
|
||||
docId,
|
||||
activeItemIndex,
|
||||
}
|
||||
},
|
||||
100,
|
||||
{ leading: false, trailing: true, maxWait: 1000 }
|
||||
)
|
||||
|
||||
function getTopPosition(item: HTMLDivElement, isFirstEntry: boolean) {
|
||||
const offset = isFirstEntry ? 0 : OFFSET_FOR_ENTRIES_ABOVE
|
||||
return Math.max(COLLAPSED_HEADER_HEIGHT + offset, Number(item.dataset.top))
|
||||
}
|
Reference in New Issue
Block a user