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:
June Kelly 2023-05-16 09:18:06 +01:00 committed by Copybot
parent 6c2bc2fe8b
commit 0346e32906
17 changed files with 523 additions and 258 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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: [],
}) })

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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