first commit

This commit is contained in:
2025-04-24 13:11:28 +08:00
commit ff9c54d5e4
5960 changed files with 834111 additions and 0 deletions

View File

@@ -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'
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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))
}