mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-29 05:03:33 -05:00
Merge pull request #12836 from overleaf/td-history-performance
History migration: Improve performance of selecting or comparing versions GitOrigin-RevId: 2a18a93c246fb94ed8d8b9770449be83364177ea
This commit is contained in:
parent
6c2bc2fe8b
commit
0346e32906
17 changed files with 523 additions and 258 deletions
|
@ -1,20 +1,35 @@
|
||||||
import { Fragment, useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { useHistoryContext } from '../../context/history-context'
|
|
||||||
import HistoryVersion from './history-version'
|
import HistoryVersion from './history-version'
|
||||||
import LoadingSpinner from '../../../../shared/components/loading-spinner'
|
import LoadingSpinner from '../../../../shared/components/loading-spinner'
|
||||||
import { OwnerPaywallPrompt } from './owner-paywall-prompt'
|
import { OwnerPaywallPrompt } from './owner-paywall-prompt'
|
||||||
import { NonOwnerPaywallPrompt } from './non-owner-paywall-prompt'
|
import { NonOwnerPaywallPrompt } from './non-owner-paywall-prompt'
|
||||||
import { relativeDate } from '../../../utils/format-date'
|
import { isVersionSelected } from '../../utils/history-details'
|
||||||
|
import { useUserContext } from '../../../../shared/context/user-context'
|
||||||
|
import useDropdownActiveItem from '../../hooks/use-dropdown-active-item'
|
||||||
|
import { useHistoryContext } from '../../context/history-context'
|
||||||
|
|
||||||
function AllHistoryList() {
|
function AllHistoryList() {
|
||||||
const { updatesInfo, fetchNextBatchOfUpdates, currentUserIsOwner } =
|
const { id: currentUserId } = useUserContext()
|
||||||
useHistoryContext()
|
const {
|
||||||
const updatesLoadingState = updatesInfo.loadingState
|
projectId,
|
||||||
const { visibleUpdateCount, updates, atEnd } = updatesInfo
|
updatesInfo,
|
||||||
|
fetchNextBatchOfUpdates,
|
||||||
|
currentUserIsOwner,
|
||||||
|
selection,
|
||||||
|
setSelection,
|
||||||
|
} = useHistoryContext()
|
||||||
|
const {
|
||||||
|
visibleUpdateCount,
|
||||||
|
updates,
|
||||||
|
atEnd,
|
||||||
|
loadingState: updatesLoadingState,
|
||||||
|
} = updatesInfo
|
||||||
const scrollerRef = useRef<HTMLDivElement>(null)
|
const scrollerRef = useRef<HTMLDivElement>(null)
|
||||||
const bottomRef = useRef<HTMLDivElement>(null)
|
const bottomRef = useRef<HTMLDivElement>(null)
|
||||||
const intersectionObserverRef = useRef<IntersectionObserver | null>(null)
|
const intersectionObserverRef = useRef<IntersectionObserver | null>(null)
|
||||||
const [bottomVisible, setBottomVisible] = useState(false)
|
const [bottomVisible, setBottomVisible] = useState(false)
|
||||||
|
const { activeDropdownItem, setActiveDropdownItem, closeDropdownForItem } =
|
||||||
|
useDropdownActiveItem()
|
||||||
const showPaywall =
|
const showPaywall =
|
||||||
updatesLoadingState === 'ready' && updatesInfo.freeHistoryLimitHit
|
updatesLoadingState === 'ready' && updatesInfo.freeHistoryLimitHit
|
||||||
const showOwnerPaywall = showPaywall && currentUserIsOwner
|
const showOwnerPaywall = showPaywall && currentUserIsOwner
|
||||||
|
@ -73,25 +88,36 @@ function AllHistoryList() {
|
||||||
<div ref={scrollerRef} className="history-all-versions-scroller">
|
<div ref={scrollerRef} className="history-all-versions-scroller">
|
||||||
<div className="history-all-versions-container">
|
<div className="history-all-versions-container">
|
||||||
<div ref={bottomRef} className="history-versions-bottom" />
|
<div ref={bottomRef} className="history-versions-bottom" />
|
||||||
{visibleUpdates.map((update, index) => (
|
{visibleUpdates.map((update, index) => {
|
||||||
<Fragment key={`${update.fromV}_${update.toV}`}>
|
const selected = isVersionSelected(
|
||||||
{update.meta.first_in_day && index > 0 && (
|
selection,
|
||||||
<hr className="history-version-divider" />
|
update.fromV,
|
||||||
)}
|
update.toV
|
||||||
{update.meta.first_in_day && (
|
)
|
||||||
<time className="history-version-day">
|
const dropdownActive = update === activeDropdownItem.item
|
||||||
{relativeDate(update.meta.end_ts)}
|
const showDivider = Boolean(update.meta.first_in_day && index > 0)
|
||||||
</time>
|
const faded =
|
||||||
)}
|
updatesInfo.freeHistoryLimitHit &&
|
||||||
|
index === visibleUpdates.length - 1
|
||||||
|
|
||||||
|
return (
|
||||||
<HistoryVersion
|
<HistoryVersion
|
||||||
|
key={`${update.fromV}_${update.toV}`}
|
||||||
update={update}
|
update={update}
|
||||||
faded={
|
faded={faded}
|
||||||
updatesInfo.freeHistoryLimitHit &&
|
showDivider={showDivider}
|
||||||
index === visibleUpdates.length - 1
|
setSelection={setSelection}
|
||||||
}
|
selected={selected}
|
||||||
|
currentUserId={currentUserId}
|
||||||
|
comparing={selection.comparing}
|
||||||
|
projectId={projectId}
|
||||||
|
setActiveDropdownItem={setActiveDropdownItem}
|
||||||
|
closeDropdownForItem={closeDropdownForItem}
|
||||||
|
dropdownOpen={activeDropdownItem.isOpened && dropdownActive}
|
||||||
|
dropdownActive={dropdownActive}
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
)
|
||||||
))}
|
})}
|
||||||
</div>
|
</div>
|
||||||
{showOwnerPaywall ? <OwnerPaywallPrompt /> : null}
|
{showOwnerPaywall ? <OwnerPaywallPrompt /> : null}
|
||||||
{showNonOwnerPaywall ? <NonOwnerPaywallPrompt /> : null}
|
{showNonOwnerPaywall ? <NonOwnerPaywallPrompt /> : null}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { useState, useRef, useEffect } from 'react'
|
import { useRef, useEffect } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Dropdown } from 'react-bootstrap'
|
import { Dropdown } from 'react-bootstrap'
|
||||||
import DropdownToggleWithTooltip from '../../../../../shared/components/dropdown/dropdown-toggle-with-tooltip'
|
import DropdownToggleWithTooltip from '../../../../../shared/components/dropdown/dropdown-toggle-with-tooltip'
|
||||||
|
@ -9,11 +9,18 @@ type DropdownMenuProps = {
|
||||||
id: string
|
id: string
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
parentSelector?: string
|
parentSelector?: string
|
||||||
|
isOpened: boolean
|
||||||
|
setIsOpened: (isOpened: boolean) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
function ActionsDropdown({ id, children, parentSelector }: DropdownMenuProps) {
|
function ActionsDropdown({
|
||||||
|
id,
|
||||||
|
children,
|
||||||
|
parentSelector,
|
||||||
|
isOpened,
|
||||||
|
setIsOpened,
|
||||||
|
}: DropdownMenuProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [isOpened, setIsOpened] = useState(false)
|
|
||||||
const menuRef = useRef<HTMLElement>()
|
const menuRef = useRef<HTMLElement>()
|
||||||
|
|
||||||
// handle the placement of the dropdown above or below the toggle button
|
// handle the placement of the dropdown above or below the toggle button
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
import ActionsDropdown from './actions-dropdown'
|
||||||
|
|
||||||
|
type HistoryDropdownProps = {
|
||||||
|
children: React.ReactNode
|
||||||
|
id: string
|
||||||
|
isOpened: boolean
|
||||||
|
setIsOpened: (isOpened: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
function HistoryDropdown({
|
||||||
|
children,
|
||||||
|
id,
|
||||||
|
isOpened,
|
||||||
|
setIsOpened,
|
||||||
|
}: HistoryDropdownProps) {
|
||||||
|
return (
|
||||||
|
<ActionsDropdown id={id} isOpened={isOpened} setIsOpened={setIsOpened}>
|
||||||
|
{children}
|
||||||
|
</ActionsDropdown>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default HistoryDropdown
|
|
@ -1,43 +0,0 @@
|
||||||
import ActionsDropdown from './actions-dropdown'
|
|
||||||
import AddLabel from './menu-item/add-label'
|
|
||||||
import Download from './menu-item/download'
|
|
||||||
import Compare from './menu-item/compare'
|
|
||||||
import { UpdateRange } from '../../../services/types/update'
|
|
||||||
|
|
||||||
type HistoryVersionDropdownProps = {
|
|
||||||
id: string
|
|
||||||
projectId: string
|
|
||||||
isComparing: boolean
|
|
||||||
isSelected: boolean
|
|
||||||
updateMetaEndTimestamp: number
|
|
||||||
} & Pick<UpdateRange, 'fromV' | 'toV'>
|
|
||||||
|
|
||||||
function HistoryVersionDropdown({
|
|
||||||
id,
|
|
||||||
projectId,
|
|
||||||
isComparing,
|
|
||||||
isSelected,
|
|
||||||
fromV,
|
|
||||||
toV,
|
|
||||||
updateMetaEndTimestamp,
|
|
||||||
}: HistoryVersionDropdownProps) {
|
|
||||||
return (
|
|
||||||
<ActionsDropdown
|
|
||||||
id={id}
|
|
||||||
parentSelector="[data-history-version-list-container]"
|
|
||||||
>
|
|
||||||
<AddLabel projectId={projectId} version={toV} />
|
|
||||||
<Download projectId={projectId} version={toV} />
|
|
||||||
{!isComparing && !isSelected && (
|
|
||||||
<Compare
|
|
||||||
projectId={projectId}
|
|
||||||
fromV={fromV}
|
|
||||||
toV={toV}
|
|
||||||
updateMetaEndTimestamp={updateMetaEndTimestamp}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</ActionsDropdown>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default HistoryVersionDropdown
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
import Download from './menu-item/download'
|
||||||
|
import Compare from './menu-item/compare'
|
||||||
|
import { Version } from '../../../services/types/update'
|
||||||
|
import { ActiveDropdown } from '../../../hooks/use-dropdown-active-item'
|
||||||
|
import { useCallback } from 'react'
|
||||||
|
|
||||||
|
type LabelDropdownContentProps = {
|
||||||
|
projectId: string
|
||||||
|
version: Version
|
||||||
|
updateMetaEndTimestamp: number
|
||||||
|
selected: boolean
|
||||||
|
comparing: boolean
|
||||||
|
closeDropdownForItem: ActiveDropdown['closeDropdownForItem']
|
||||||
|
}
|
||||||
|
|
||||||
|
function LabelDropdownContent({
|
||||||
|
projectId,
|
||||||
|
version,
|
||||||
|
updateMetaEndTimestamp,
|
||||||
|
selected,
|
||||||
|
comparing,
|
||||||
|
closeDropdownForItem,
|
||||||
|
}: LabelDropdownContentProps) {
|
||||||
|
const closeDropdown = useCallback(() => {
|
||||||
|
closeDropdownForItem(version)
|
||||||
|
}, [closeDropdownForItem, version])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Download
|
||||||
|
projectId={projectId}
|
||||||
|
version={version}
|
||||||
|
closeDropdown={closeDropdown}
|
||||||
|
/>
|
||||||
|
{!comparing && !selected && (
|
||||||
|
<Compare
|
||||||
|
projectId={projectId}
|
||||||
|
fromV={version}
|
||||||
|
toV={version}
|
||||||
|
updateMetaEndTimestamp={updateMetaEndTimestamp}
|
||||||
|
closeDropdown={closeDropdown}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LabelDropdownContent
|
|
@ -1,40 +0,0 @@
|
||||||
import ActionsDropdown from './actions-dropdown'
|
|
||||||
import Download from './menu-item/download'
|
|
||||||
import Compare from './menu-item/compare'
|
|
||||||
|
|
||||||
type LabelDropdownProps = {
|
|
||||||
id: string
|
|
||||||
projectId: string
|
|
||||||
isComparing: boolean
|
|
||||||
isSelected: boolean
|
|
||||||
version: number
|
|
||||||
updateMetaEndTimestamp: number
|
|
||||||
}
|
|
||||||
|
|
||||||
function LabelDropdown({
|
|
||||||
id,
|
|
||||||
projectId,
|
|
||||||
isComparing,
|
|
||||||
isSelected,
|
|
||||||
version,
|
|
||||||
updateMetaEndTimestamp,
|
|
||||||
}: LabelDropdownProps) {
|
|
||||||
return (
|
|
||||||
<ActionsDropdown
|
|
||||||
id={id}
|
|
||||||
parentSelector="[data-history-version-list-container]"
|
|
||||||
>
|
|
||||||
<Download projectId={projectId} version={version} />
|
|
||||||
{!isComparing && !isSelected && (
|
|
||||||
<Compare
|
|
||||||
projectId={projectId}
|
|
||||||
fromV={version}
|
|
||||||
toV={version}
|
|
||||||
updateMetaEndTimestamp={updateMetaEndTimestamp}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</ActionsDropdown>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default LabelDropdown
|
|
|
@ -7,15 +7,26 @@ import AddLabelModal from '../../add-label-modal'
|
||||||
type DownloadProps = {
|
type DownloadProps = {
|
||||||
projectId: string
|
projectId: string
|
||||||
version: number
|
version: number
|
||||||
|
closeDropdown: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
function AddLabel({ version, projectId, ...props }: DownloadProps) {
|
function AddLabel({
|
||||||
|
version,
|
||||||
|
projectId,
|
||||||
|
closeDropdown,
|
||||||
|
...props
|
||||||
|
}: DownloadProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [showModal, setShowModal] = useState(false)
|
const [showModal, setShowModal] = useState(false)
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
closeDropdown()
|
||||||
|
setShowModal(true)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<MenuItem onClick={() => setShowModal(true)} {...props}>
|
<MenuItem onClick={handleClick} {...props}>
|
||||||
<Icon type="tag" fw /> {t('history_label_this_version')}
|
<Icon type="tag" fw /> {t('history_label_this_version')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<AddLabelModal
|
<AddLabelModal
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { UpdateRange } from '../../../../services/types/update'
|
||||||
type CompareProps = {
|
type CompareProps = {
|
||||||
projectId: string
|
projectId: string
|
||||||
updateMetaEndTimestamp: number
|
updateMetaEndTimestamp: number
|
||||||
|
closeDropdown: () => void
|
||||||
} & Pick<UpdateRange, 'fromV' | 'toV'>
|
} & Pick<UpdateRange, 'fromV' | 'toV'>
|
||||||
|
|
||||||
function Compare({
|
function Compare({
|
||||||
|
@ -15,29 +16,36 @@ function Compare({
|
||||||
fromV,
|
fromV,
|
||||||
toV,
|
toV,
|
||||||
updateMetaEndTimestamp,
|
updateMetaEndTimestamp,
|
||||||
|
closeDropdown,
|
||||||
...props
|
...props
|
||||||
}: CompareProps) {
|
}: CompareProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { selection, setSelection } = useHistoryContext()
|
const { setSelection } = useHistoryContext()
|
||||||
|
|
||||||
const handleCompareVersion = (e: React.MouseEvent<MenuItemProps>) => {
|
const handleCompareVersion = (e: React.MouseEvent<MenuItemProps>) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
|
closeDropdown()
|
||||||
|
|
||||||
const { updateRange } = selection
|
setSelection(prevSelection => {
|
||||||
if (updateRange) {
|
const { updateRange } = prevSelection
|
||||||
const range = computeUpdateRange(
|
|
||||||
updateRange,
|
|
||||||
fromV,
|
|
||||||
toV,
|
|
||||||
updateMetaEndTimestamp
|
|
||||||
)
|
|
||||||
|
|
||||||
setSelection({
|
if (updateRange) {
|
||||||
updateRange: range,
|
const range = computeUpdateRange(
|
||||||
comparing: true,
|
updateRange,
|
||||||
files: [],
|
fromV,
|
||||||
})
|
toV,
|
||||||
}
|
updateMetaEndTimestamp
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
updateRange: range,
|
||||||
|
comparing: true,
|
||||||
|
files: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return prevSelection
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -6,13 +6,20 @@ import * as location from '../../../../../../shared/components/location'
|
||||||
type DownloadProps = {
|
type DownloadProps = {
|
||||||
projectId: string
|
projectId: string
|
||||||
version: number
|
version: number
|
||||||
|
closeDropdown: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
function Download({ version, projectId, ...props }: DownloadProps) {
|
function Download({
|
||||||
|
version,
|
||||||
|
projectId,
|
||||||
|
closeDropdown,
|
||||||
|
...props
|
||||||
|
}: DownloadProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const handleDownloadVersion = (e: React.MouseEvent<MenuItemProps>) => {
|
const handleDownloadVersion = (e: React.MouseEvent<MenuItemProps>) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
closeDropdown()
|
||||||
const event = e as typeof e & { target: HTMLAnchorElement }
|
const event = e as typeof e & { target: HTMLAnchorElement }
|
||||||
location.assign(event.target.href)
|
location.assign(event.target.href)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
import AddLabel from './menu-item/add-label'
|
||||||
|
import Download from './menu-item/download'
|
||||||
|
import Compare from './menu-item/compare'
|
||||||
|
import { LoadedUpdate } from '../../../services/types/update'
|
||||||
|
import { useCallback } from 'react'
|
||||||
|
import { ActiveDropdown } from '../../../hooks/use-dropdown-active-item'
|
||||||
|
|
||||||
|
type VersionDropdownContentProps = {
|
||||||
|
projectId: string
|
||||||
|
update: LoadedUpdate
|
||||||
|
selected: boolean
|
||||||
|
comparing: boolean
|
||||||
|
closeDropdownForItem: ActiveDropdown['closeDropdownForItem']
|
||||||
|
}
|
||||||
|
|
||||||
|
function VersionDropdownContent({
|
||||||
|
projectId,
|
||||||
|
update,
|
||||||
|
selected,
|
||||||
|
comparing,
|
||||||
|
closeDropdownForItem,
|
||||||
|
}: VersionDropdownContentProps) {
|
||||||
|
const closeDropdown = useCallback(() => {
|
||||||
|
closeDropdownForItem(update)
|
||||||
|
}, [closeDropdownForItem, update])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AddLabel
|
||||||
|
projectId={projectId}
|
||||||
|
version={update.toV}
|
||||||
|
closeDropdown={closeDropdown}
|
||||||
|
/>
|
||||||
|
<Download
|
||||||
|
projectId={projectId}
|
||||||
|
version={update.toV}
|
||||||
|
closeDropdown={closeDropdown}
|
||||||
|
/>
|
||||||
|
{!comparing && !selected && (
|
||||||
|
<Compare
|
||||||
|
projectId={projectId}
|
||||||
|
fromV={update.fromV}
|
||||||
|
toV={update.toV}
|
||||||
|
updateMetaEndTimestamp={update.meta.end_ts}
|
||||||
|
closeDropdown={closeDropdown}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default VersionDropdownContent
|
|
@ -1,29 +1,28 @@
|
||||||
import { useHistoryContext } from '../../context/history-context'
|
|
||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
|
import { HistoryContextValue } from '../../context/types/history-context-value'
|
||||||
import { UpdateRange } from '../../services/types/update'
|
import { UpdateRange } from '../../services/types/update'
|
||||||
|
import { ReactNode, MouseEvent } from 'react'
|
||||||
|
|
||||||
type HistoryVersionDetailsProps = {
|
type HistoryVersionDetailsProps = {
|
||||||
children: React.ReactNode
|
children: ReactNode
|
||||||
|
updateRange: UpdateRange
|
||||||
selected: boolean
|
selected: boolean
|
||||||
selectable: boolean
|
selectable: boolean
|
||||||
} & UpdateRange
|
setSelection: HistoryContextValue['setSelection']
|
||||||
|
}
|
||||||
|
|
||||||
function HistoryVersionDetails({
|
function HistoryVersionDetails({
|
||||||
children,
|
children,
|
||||||
selected,
|
selected,
|
||||||
|
updateRange,
|
||||||
selectable,
|
selectable,
|
||||||
fromV,
|
setSelection,
|
||||||
toV,
|
|
||||||
fromVTimestamp,
|
|
||||||
toVTimestamp,
|
|
||||||
}: HistoryVersionDetailsProps) {
|
}: HistoryVersionDetailsProps) {
|
||||||
const { setSelection } = useHistoryContext()
|
const handleSelect = (e: MouseEvent<HTMLDivElement>) => {
|
||||||
|
|
||||||
const handleSelect = (e: React.MouseEvent<HTMLDivElement>) => {
|
|
||||||
const target = e.target as HTMLElement
|
const target = e.target as HTMLElement
|
||||||
if (!target.closest('.dropdown') && e.currentTarget.contains(target)) {
|
if (!target.closest('.dropdown') && e.currentTarget.contains(target)) {
|
||||||
setSelection({
|
setSelection({
|
||||||
updateRange: { fromV, toV, fromVTimestamp, toVTimestamp },
|
updateRange,
|
||||||
comparing: false,
|
comparing: false,
|
||||||
files: [],
|
files: [],
|
||||||
})
|
})
|
||||||
|
|
|
@ -3,79 +3,115 @@ import TagTooltip from './tag-tooltip'
|
||||||
import Changes from './changes'
|
import Changes from './changes'
|
||||||
import MetadataUsersList from './metadata-users-list'
|
import MetadataUsersList from './metadata-users-list'
|
||||||
import Origin from './origin'
|
import Origin from './origin'
|
||||||
import HistoryVersionDropdown from './dropdown/history-version-dropdown'
|
import HistoryDropdown from './dropdown/history-dropdown'
|
||||||
import { useUserContext } from '../../../../shared/context/user-context'
|
import { formatTime, relativeDate } from '../../../utils/format-date'
|
||||||
import { useHistoryContext } from '../../context/history-context'
|
|
||||||
import { isVersionSelected } from '../../utils/history-details'
|
|
||||||
import { formatTime } from '../../../utils/format-date'
|
|
||||||
import { orderBy } from 'lodash'
|
import { orderBy } from 'lodash'
|
||||||
import { LoadedUpdate } from '../../services/types/update'
|
import { LoadedUpdate } from '../../services/types/update'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
|
import { updateRangeForUpdate } from '../../utils/history-details'
|
||||||
|
import { ActiveDropdown } from '../../hooks/use-dropdown-active-item'
|
||||||
|
import { memo } from 'react'
|
||||||
|
import { HistoryContextValue } from '../../context/types/history-context-value'
|
||||||
|
import VersionDropdownContent from './dropdown/version-dropdown-content'
|
||||||
|
|
||||||
type HistoryEntryProps = {
|
type HistoryVersionProps = {
|
||||||
update: LoadedUpdate
|
update: LoadedUpdate
|
||||||
|
currentUserId: string
|
||||||
|
projectId: string
|
||||||
|
comparing: boolean
|
||||||
faded: boolean
|
faded: boolean
|
||||||
|
showDivider: boolean
|
||||||
|
selected: boolean
|
||||||
|
setSelection: HistoryContextValue['setSelection']
|
||||||
|
dropdownOpen: boolean
|
||||||
|
dropdownActive: boolean
|
||||||
|
setActiveDropdownItem: ActiveDropdown['setActiveDropdownItem']
|
||||||
|
closeDropdownForItem: ActiveDropdown['closeDropdownForItem']
|
||||||
}
|
}
|
||||||
|
|
||||||
function HistoryVersion({ update, faded }: HistoryEntryProps) {
|
function HistoryVersion({
|
||||||
const { id: currentUserId } = useUserContext()
|
update,
|
||||||
const { projectId, selection } = useHistoryContext()
|
currentUserId,
|
||||||
|
projectId,
|
||||||
|
comparing,
|
||||||
|
faded,
|
||||||
|
showDivider,
|
||||||
|
selected,
|
||||||
|
setSelection,
|
||||||
|
dropdownOpen,
|
||||||
|
dropdownActive,
|
||||||
|
setActiveDropdownItem,
|
||||||
|
closeDropdownForItem,
|
||||||
|
}: HistoryVersionProps) {
|
||||||
const orderedLabels = orderBy(update.labels, ['created_at'], ['desc'])
|
const orderedLabels = orderBy(update.labels, ['created_at'], ['desc'])
|
||||||
const selected = isVersionSelected(selection, update.fromV, update.toV)
|
const selectable = !faded && (comparing || !selected)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<>
|
||||||
data-testid="history-version"
|
{showDivider ? <hr className="history-version-divider" /> : null}
|
||||||
className={classNames({
|
{update.meta.first_in_day ? (
|
||||||
'history-version-faded': faded,
|
<time className="history-version-day">
|
||||||
})}
|
{relativeDate(update.meta.end_ts)}
|
||||||
>
|
</time>
|
||||||
<HistoryVersionDetails
|
) : null}
|
||||||
fromV={update.fromV}
|
<div
|
||||||
toV={update.toV}
|
data-testid="history-version"
|
||||||
fromVTimestamp={update.meta.end_ts}
|
className={classNames({
|
||||||
toVTimestamp={update.meta.end_ts}
|
'history-version-faded': faded,
|
||||||
selected={selected}
|
})}
|
||||||
selectable={!faded && (selection.comparing || !selected)}
|
|
||||||
>
|
>
|
||||||
<div className="history-version-main-details">
|
<HistoryVersionDetails
|
||||||
<time className="history-version-metadata-time">
|
selected={selected}
|
||||||
<b>{formatTime(update.meta.end_ts, 'Do MMMM, h:mm a')}</b>
|
setSelection={setSelection}
|
||||||
</time>
|
updateRange={updateRangeForUpdate(update)}
|
||||||
{orderedLabels.map(label => (
|
selectable={selectable}
|
||||||
<TagTooltip
|
>
|
||||||
key={label.id}
|
<div className="history-version-main-details">
|
||||||
showTooltip
|
<time className="history-version-metadata-time">
|
||||||
currentUserId={currentUserId}
|
<b>{formatTime(update.meta.end_ts, 'Do MMMM, h:mm a')}</b>
|
||||||
label={label}
|
</time>
|
||||||
|
{orderedLabels.map(label => (
|
||||||
|
<TagTooltip
|
||||||
|
key={label.id}
|
||||||
|
showTooltip
|
||||||
|
currentUserId={currentUserId}
|
||||||
|
label={label}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<Changes
|
||||||
|
pathnames={update.pathnames}
|
||||||
|
projectOps={update.project_ops}
|
||||||
/>
|
/>
|
||||||
))}
|
<MetadataUsersList
|
||||||
<Changes
|
users={update.meta.users}
|
||||||
pathnames={update.pathnames}
|
origin={update.meta.origin}
|
||||||
projectOps={update.project_ops}
|
currentUserId={currentUserId}
|
||||||
/>
|
/>
|
||||||
<MetadataUsersList
|
<Origin origin={update.meta.origin} />
|
||||||
users={update.meta.users}
|
</div>
|
||||||
origin={update.meta.origin}
|
{faded ? null : (
|
||||||
currentUserId={currentUserId}
|
<HistoryDropdown
|
||||||
/>
|
id={`${update.fromV}_${update.toV}`}
|
||||||
<Origin origin={update.meta.origin} />
|
isOpened={dropdownOpen}
|
||||||
</div>
|
setIsOpened={(isOpened: boolean) =>
|
||||||
{faded ? null : (
|
setActiveDropdownItem({ item: update, isOpened })
|
||||||
<HistoryVersionDropdown
|
}
|
||||||
id={`${update.fromV}_${update.toV}`}
|
>
|
||||||
projectId={projectId}
|
{dropdownActive ? (
|
||||||
isComparing={selection.comparing}
|
<VersionDropdownContent
|
||||||
isSelected={selected}
|
comparing={comparing}
|
||||||
fromV={update.fromV}
|
selected={selected}
|
||||||
toV={update.toV}
|
update={update}
|
||||||
updateMetaEndTimestamp={update.meta.end_ts}
|
projectId={projectId}
|
||||||
/>
|
closeDropdownForItem={closeDropdownForItem}
|
||||||
)}
|
/>
|
||||||
</HistoryVersionDetails>
|
) : null}
|
||||||
</div>
|
</HistoryDropdown>
|
||||||
|
)}
|
||||||
|
</HistoryVersionDetails>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default HistoryVersion
|
export default memo(HistoryVersion)
|
||||||
|
|
|
@ -0,0 +1,121 @@
|
||||||
|
import { memo, useCallback } from 'react'
|
||||||
|
import { UpdateRange, Version } from '../../services/types/update'
|
||||||
|
import TagTooltip from './tag-tooltip'
|
||||||
|
import { formatTime, isoToUnix } from '../../../utils/format-date'
|
||||||
|
import { isPseudoLabel } from '../../utils/label'
|
||||||
|
import UserNameWithColoredBadge from './user-name-with-colored-badge'
|
||||||
|
import HistoryDropdown from './dropdown/history-dropdown'
|
||||||
|
import HistoryVersionDetails from './history-version-details'
|
||||||
|
import { LoadedLabel } from '../../services/types/label'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { ActiveDropdown } from '../../hooks/use-dropdown-active-item'
|
||||||
|
import { HistoryContextValue } from '../../context/types/history-context-value'
|
||||||
|
import LabelDropdownContent from './dropdown/label-dropdown-content'
|
||||||
|
|
||||||
|
type LabelListItemProps = {
|
||||||
|
version: Version
|
||||||
|
labels: LoadedLabel[]
|
||||||
|
currentUserId: string
|
||||||
|
projectId: string
|
||||||
|
comparing: boolean
|
||||||
|
selected: boolean
|
||||||
|
selectable: boolean
|
||||||
|
setSelection: HistoryContextValue['setSelection']
|
||||||
|
dropdownOpen: boolean
|
||||||
|
dropdownActive: boolean
|
||||||
|
setActiveDropdownItem: ActiveDropdown['setActiveDropdownItem']
|
||||||
|
closeDropdownForItem: ActiveDropdown['closeDropdownForItem']
|
||||||
|
}
|
||||||
|
|
||||||
|
function LabelListItem({
|
||||||
|
version,
|
||||||
|
labels,
|
||||||
|
currentUserId,
|
||||||
|
projectId,
|
||||||
|
comparing,
|
||||||
|
selected,
|
||||||
|
selectable,
|
||||||
|
setSelection,
|
||||||
|
dropdownOpen,
|
||||||
|
dropdownActive,
|
||||||
|
setActiveDropdownItem,
|
||||||
|
closeDropdownForItem,
|
||||||
|
}: LabelListItemProps) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
// first label
|
||||||
|
const fromVTimestamp = isoToUnix(labels[labels.length - 1].created_at)
|
||||||
|
// most recent label
|
||||||
|
const toVTimestamp = isoToUnix(labels[0].created_at)
|
||||||
|
|
||||||
|
const updateRange: UpdateRange = {
|
||||||
|
fromV: version,
|
||||||
|
toV: version,
|
||||||
|
fromVTimestamp,
|
||||||
|
toVTimestamp,
|
||||||
|
}
|
||||||
|
|
||||||
|
const setIsOpened = useCallback(
|
||||||
|
(isOpened: boolean) => {
|
||||||
|
setActiveDropdownItem({ item: version, isOpened })
|
||||||
|
},
|
||||||
|
[setActiveDropdownItem, version]
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HistoryVersionDetails
|
||||||
|
key={version}
|
||||||
|
updateRange={updateRange}
|
||||||
|
selected={selected}
|
||||||
|
selectable={selectable}
|
||||||
|
setSelection={setSelection}
|
||||||
|
>
|
||||||
|
<div className="history-version-main-details">
|
||||||
|
{labels.map(label => (
|
||||||
|
<div key={label.id} className="history-version-label">
|
||||||
|
<TagTooltip
|
||||||
|
showTooltip={false}
|
||||||
|
currentUserId={currentUserId}
|
||||||
|
label={label}
|
||||||
|
/>
|
||||||
|
<time className="history-version-metadata-time">
|
||||||
|
{formatTime(label.created_at, 'Do MMMM, h:mm a')}
|
||||||
|
</time>
|
||||||
|
{!isPseudoLabel(label) && (
|
||||||
|
<div className="history-version-saved-by">
|
||||||
|
<span className="history-version-saved-by-label">
|
||||||
|
{t('saved_by')}
|
||||||
|
</span>
|
||||||
|
<UserNameWithColoredBadge
|
||||||
|
user={{
|
||||||
|
id: label.user_id,
|
||||||
|
displayName: label.user_display_name,
|
||||||
|
}}
|
||||||
|
currentUserId={currentUserId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<HistoryDropdown
|
||||||
|
id={version.toString()}
|
||||||
|
isOpened={dropdownOpen}
|
||||||
|
setIsOpened={setIsOpened}
|
||||||
|
>
|
||||||
|
{dropdownActive ? (
|
||||||
|
<LabelDropdownContent
|
||||||
|
comparing={comparing}
|
||||||
|
selected={selected}
|
||||||
|
version={version}
|
||||||
|
updateMetaEndTimestamp={toVTimestamp}
|
||||||
|
projectId={projectId}
|
||||||
|
closeDropdownForItem={closeDropdownForItem}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</HistoryDropdown>
|
||||||
|
</HistoryVersionDetails>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(LabelListItem)
|
|
@ -1,20 +1,17 @@
|
||||||
import { useMemo } from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import HistoryVersionDetails from './history-version-details'
|
|
||||||
import TagTooltip from './tag-tooltip'
|
|
||||||
import UserNameWithColoredBadge from './user-name-with-colored-badge'
|
|
||||||
import LabelDropdown from './dropdown/label-dropdown'
|
|
||||||
import { useHistoryContext } from '../../context/history-context'
|
|
||||||
import { useUserContext } from '../../../../shared/context/user-context'
|
import { useUserContext } from '../../../../shared/context/user-context'
|
||||||
import { isVersionSelected } from '../../utils/history-details'
|
import { isVersionSelected } from '../../utils/history-details'
|
||||||
import { getVersionWithLabels, isPseudoLabel } from '../../utils/label'
|
import { useMemo } from 'react'
|
||||||
import { formatTime, isoToUnix } from '../../../utils/format-date'
|
|
||||||
import { Version } from '../../services/types/update'
|
import { Version } from '../../services/types/update'
|
||||||
|
import LabelListItem from './label-list-item'
|
||||||
|
import useDropdownActiveItem from '../../hooks/use-dropdown-active-item'
|
||||||
|
import { getVersionWithLabels } from '../../utils/label'
|
||||||
|
import { useHistoryContext } from '../../context/history-context'
|
||||||
|
|
||||||
function LabelsList() {
|
function LabelsList() {
|
||||||
const { t } = useTranslation()
|
|
||||||
const { labels, projectId, selection } = useHistoryContext()
|
|
||||||
const { id: currentUserId } = useUserContext()
|
const { id: currentUserId } = useUserContext()
|
||||||
|
const { projectId, labels, selection, setSelection } = useHistoryContext()
|
||||||
|
const { activeDropdownItem, setActiveDropdownItem, closeDropdownForItem } =
|
||||||
|
useDropdownActiveItem()
|
||||||
|
|
||||||
const versionWithLabels = useMemo(
|
const versionWithLabels = useMemo(
|
||||||
() => getVersionWithLabels(labels),
|
() => getVersionWithLabels(labels),
|
||||||
|
@ -33,59 +30,24 @@ function LabelsList() {
|
||||||
<>
|
<>
|
||||||
{versionWithLabels.map(({ version, labels }) => {
|
{versionWithLabels.map(({ version, labels }) => {
|
||||||
const selected = selectedVersions.has(version)
|
const selected = selectedVersions.has(version)
|
||||||
|
const dropdownActive = version === activeDropdownItem.item
|
||||||
// first label
|
|
||||||
const fromVTimestamp = isoToUnix(labels[labels.length - 1].created_at)
|
|
||||||
// most recent label
|
|
||||||
const toVTimestamp = isoToUnix(labels[0].created_at)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HistoryVersionDetails
|
<LabelListItem
|
||||||
key={version}
|
key={version}
|
||||||
fromV={version}
|
labels={labels}
|
||||||
toV={version}
|
version={version}
|
||||||
fromVTimestamp={fromVTimestamp}
|
currentUserId={currentUserId}
|
||||||
toVTimestamp={toVTimestamp}
|
projectId={projectId}
|
||||||
|
comparing={selection.comparing}
|
||||||
selected={selected}
|
selected={selected}
|
||||||
selectable={!(singleVersionSelected && selected)}
|
selectable={!(singleVersionSelected && selected)}
|
||||||
>
|
setSelection={setSelection}
|
||||||
<div className="history-version-main-details">
|
dropdownOpen={activeDropdownItem.isOpened && dropdownActive}
|
||||||
{labels.map(label => (
|
dropdownActive={dropdownActive}
|
||||||
<div key={label.id} className="history-version-label">
|
setActiveDropdownItem={setActiveDropdownItem}
|
||||||
<TagTooltip
|
closeDropdownForItem={closeDropdownForItem}
|
||||||
showTooltip={false}
|
/>
|
||||||
currentUserId={currentUserId}
|
|
||||||
label={label}
|
|
||||||
/>
|
|
||||||
<time className="history-version-metadata-time">
|
|
||||||
{formatTime(label.created_at, 'Do MMMM, h:mm a')}
|
|
||||||
</time>
|
|
||||||
{!isPseudoLabel(label) && (
|
|
||||||
<div className="history-version-saved-by">
|
|
||||||
<span className="history-version-saved-by-label">
|
|
||||||
{t('saved_by')}
|
|
||||||
</span>
|
|
||||||
<UserNameWithColoredBadge
|
|
||||||
user={{
|
|
||||||
id: label.user_id,
|
|
||||||
displayName: label.user_display_name,
|
|
||||||
}}
|
|
||||||
currentUserId={currentUserId}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<LabelDropdown
|
|
||||||
id={version.toString()}
|
|
||||||
projectId={projectId}
|
|
||||||
version={version}
|
|
||||||
updateMetaEndTimestamp={toVTimestamp}
|
|
||||||
isComparing={selection.comparing}
|
|
||||||
isSelected={selected}
|
|
||||||
/>
|
|
||||||
</HistoryVersionDetails>
|
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -11,7 +11,7 @@ const fileTreeContainer = document.getElementById('history-file-tree')
|
||||||
function Main() {
|
function Main() {
|
||||||
const { updatesInfo, error } = useHistoryContext()
|
const { updatesInfo, error } = useHistoryContext()
|
||||||
|
|
||||||
let content = null
|
let content
|
||||||
if (updatesInfo.loadingState === 'loadingInitial') {
|
if (updatesInfo.loadingState === 'loadingInitial') {
|
||||||
content = <LoadingSpinner />
|
content = <LoadingSpinner />
|
||||||
} else if (error) {
|
} else if (error) {
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { Dispatch, SetStateAction, useCallback, useState } from 'react'
|
||||||
|
import { LoadedUpdate, Version } from '../services/types/update'
|
||||||
|
|
||||||
|
type DropdownItem = LoadedUpdate | Version
|
||||||
|
|
||||||
|
export type ActiveDropdownValue = {
|
||||||
|
item: DropdownItem | null
|
||||||
|
isOpened: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ActiveDropdown = {
|
||||||
|
activeDropdownItem: ActiveDropdownValue
|
||||||
|
setActiveDropdownItem: Dispatch<SetStateAction<ActiveDropdownValue>>
|
||||||
|
closeDropdownForItem: (item: DropdownItem) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
function useDropdownActiveItem(): ActiveDropdown {
|
||||||
|
const [activeDropdownItem, setActiveDropdownItem] =
|
||||||
|
useState<ActiveDropdownValue>({
|
||||||
|
item: null,
|
||||||
|
isOpened: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const closeDropdownForItem = useCallback(
|
||||||
|
(item: DropdownItem) => setActiveDropdownItem({ item, isOpened: false }),
|
||||||
|
[setActiveDropdownItem]
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
activeDropdownItem,
|
||||||
|
setActiveDropdownItem,
|
||||||
|
closeDropdownForItem,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useDropdownActiveItem
|
|
@ -71,3 +71,15 @@ export function isVersionSelected(
|
||||||
|
|
||||||
export const getUpdateForVersion = (version: number, updates: LoadedUpdate[]) =>
|
export const getUpdateForVersion = (version: number, updates: LoadedUpdate[]) =>
|
||||||
updates.find(update => update.toV === version)
|
updates.find(update => update.toV === version)
|
||||||
|
|
||||||
|
export const updateRangeForUpdate = (update: LoadedUpdate) => {
|
||||||
|
const { fromV, toV, meta } = update
|
||||||
|
const fromVTimestamp = meta.end_ts
|
||||||
|
|
||||||
|
return {
|
||||||
|
fromV,
|
||||||
|
toV,
|
||||||
|
fromVTimestamp,
|
||||||
|
toVTimestamp: fromVTimestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue