Merge pull request #12933 from overleaf/ii-history-react-select-latest-version-when-all-labels-are-removed

[web] Fix history entries and labels selection issues

GitOrigin-RevId: 73b14ba15ab4f0d9ff5946b1159ae7d5912582dd
This commit is contained in:
Miguel Serrano 2023-05-08 10:07:45 +02:00 committed by Copybot
parent 385e91652a
commit 92ade70601
9 changed files with 141 additions and 54 deletions

View file

@ -1,15 +1,10 @@
import usePersistedState from '../../../../shared/hooks/use-persisted-state'
import ToggleSwitch from './toggle-switch' import ToggleSwitch from './toggle-switch'
import AllHistoryList from './all-history-list' import AllHistoryList from './all-history-list'
import LabelsList from './labels-list' import LabelsList from './labels-list'
import { useHistoryContext } from '../../context/history-context' import { useHistoryContext } from '../../context/history-context'
function ChangeList() { function ChangeList() {
const { projectId, error } = useHistoryContext() const { error, labelsOnly, setLabelsOnly } = useHistoryContext()
const [labelsOnly, setLabelsOnly] = usePersistedState(
`history.userPrefs.showOnlyLabels.${projectId}`,
false
)
return ( return (
<aside className="change-list"> <aside className="change-list">

View file

@ -2,6 +2,7 @@ import { useTranslation } from 'react-i18next'
import { MenuItem, MenuItemProps } from 'react-bootstrap' import { MenuItem, MenuItemProps } from 'react-bootstrap'
import Icon from '../../../../../../shared/components/icon' import Icon from '../../../../../../shared/components/icon'
import { useHistoryContext } from '../../../../context/history-context' import { useHistoryContext } from '../../../../context/history-context'
import { computeUpdateRange } from '../../../../utils/range'
import { UpdateRange } from '../../../../services/types/update' import { UpdateRange } from '../../../../services/types/update'
type CompareProps = { type CompareProps = {
@ -19,37 +20,24 @@ function Compare({
const { t } = useTranslation() const { t } = useTranslation()
const { selection, setSelection } = useHistoryContext() const { selection, setSelection } = useHistoryContext()
function compare() { const handleCompareVersion = (e: React.MouseEvent<MenuItemProps>) => {
e.stopPropagation()
const { updateRange } = selection const { updateRange } = selection
if (!updateRange) { if (updateRange) {
return const range = computeUpdateRange(
} updateRange,
const fromVersion = Math.min(fromV, updateRange.fromV) fromV,
const toVersion = Math.max(toV, updateRange.toV) toV,
const fromVTimestamp = Math.min( updateMetaEndTimestamp
updateMetaEndTimestamp,
updateRange.fromVTimestamp
)
const toVTimestamp = Math.max(
updateMetaEndTimestamp,
updateRange.toVTimestamp
) )
setSelection({ setSelection({
updateRange: { updateRange: range,
fromV: fromVersion,
toV: toVersion,
fromVTimestamp,
toVTimestamp,
},
comparing: true, comparing: true,
files: [], files: [],
}) })
} }
const handleCompareVersion = (e: React.MouseEvent<MenuItemProps>) => {
e.stopPropagation()
compare()
} }
return ( return (

View file

@ -1,3 +1,4 @@
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import HistoryVersionDetails from './history-version-details' import HistoryVersionDetails from './history-version-details'
import TagTooltip from './tag-tooltip' import TagTooltip from './tag-tooltip'
@ -6,25 +7,18 @@ import LabelDropdown from './dropdown/label-dropdown'
import { useHistoryContext } from '../../context/history-context' 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 { isPseudoLabel } from '../../utils/label' import { getVersionWithLabels, isPseudoLabel } from '../../utils/label'
import { formatTime, isoToUnix } from '../../../utils/format-date' import { formatTime, isoToUnix } from '../../../utils/format-date'
import { groupBy, orderBy } from 'lodash'
import { LoadedLabel } from '../../services/types/label'
function LabelsList() { function LabelsList() {
const { t } = useTranslation() const { t } = useTranslation()
const { labels, projectId, selection } = useHistoryContext() const { labels, projectId, selection } = useHistoryContext()
const { id: currentUserId } = useUserContext() const { id: currentUserId } = useUserContext()
let versionWithLabels: { version: number; labels: LoadedLabel[] }[] = [] const versionWithLabels = useMemo(
if (labels) { () => getVersionWithLabels(labels),
const groupedLabelsHash = groupBy(labels, 'version') [labels]
versionWithLabels = Object.keys(groupedLabelsHash).map(key => ({ )
version: parseInt(key, 10),
labels: groupedLabelsHash[key],
}))
versionWithLabels = orderBy(versionWithLabels, ['version'], ['desc'])
}
return ( return (
<> <>

View file

@ -1,5 +1,7 @@
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useHistoryContext } from '../../context/history-context' import { useHistoryContext } from '../../context/history-context'
import { getUpdateForVersion } from '../../utils/history-details'
import { computeUpdateRange } from '../../utils/range'
type ToggleSwitchProps = { type ToggleSwitchProps = {
labelsOnly: boolean labelsOnly: boolean
@ -10,13 +12,48 @@ type ToggleSwitchProps = {
function ToggleSwitch({ labelsOnly, setLabelsOnly }: ToggleSwitchProps) { function ToggleSwitch({ labelsOnly, setLabelsOnly }: ToggleSwitchProps) {
const { t } = useTranslation() const { t } = useTranslation()
const { resetSelection, selection } = useHistoryContext() const { selection, setSelection, resetSelection, updatesInfo } =
useHistoryContext()
const handleChange = (isLabelsOnly: boolean) => { const handleChange = (isLabelsOnly: boolean) => {
let isSelectionReset = false
// using the switch toggle should reset the selection when in `compare` mode
if (selection.comparing) { if (selection.comparing) {
isSelectionReset = true
resetSelection() resetSelection()
} }
// in labels only mode the `fromV` is equal to `toV` value
// switching to all history mode and triggering immediate comparison with
// an older version would cause a bug if the computation below is skipped
if (!isLabelsOnly && !isSelectionReset) {
const update = selection.updateRange?.toV
? getUpdateForVersion(selection.updateRange.toV, updatesInfo.updates)
: null
const { updateRange } = selection
if (
updateRange &&
update &&
(update.fromV !== updateRange.fromV || update.toV !== updateRange.toV)
) {
const range = computeUpdateRange(
updateRange,
update.fromV,
update.toV,
update.meta.end_ts
)
setSelection({
updateRange: range,
comparing: false,
files: [],
})
}
}
setLabelsOnly(isLabelsOnly) setLabelsOnly(isLabelsOnly)
} }

View file

@ -16,6 +16,7 @@ import { renamePathnameKey } from '../utils/file-tree'
import { isFileRenamed } from '../utils/file-diff' import { isFileRenamed } from '../utils/file-diff'
import { loadLabels } from '../utils/label' import { loadLabels } from '../utils/label'
import { autoSelectFile } from '../utils/auto-select-file' import { autoSelectFile } from '../utils/auto-select-file'
import usePersistedState from '../../../shared/hooks/use-persisted-state'
import ColorManager from '../../../ide/colors/ColorManager' import ColorManager from '../../../ide/colors/ColorManager'
import moment from 'moment' import moment from 'moment'
import { cloneDeep } from 'lodash' import { cloneDeep } from 'lodash'
@ -86,6 +87,10 @@ function useHistory() {
nextBeforeTimestamp: undefined, nextBeforeTimestamp: undefined,
}) })
const [labels, setLabels] = useState<HistoryContextValue['labels']>(null) const [labels, setLabels] = useState<HistoryContextValue['labels']>(null)
const [labelsOnly, setLabelsOnly] = usePersistedState(
`history.userPrefs.showOnlyLabels.${projectId}`,
false
)
const [loadingState, setLoadingState] = const [loadingState, setLoadingState] =
useState<HistoryContextValue['loadingState']>('loadingInitial') useState<HistoryContextValue['loadingState']>('loadingInitial')
const [error, setError] = useState<HistoryContextValue['error']>(null) const [error, setError] = useState<HistoryContextValue['error']>(null)
@ -269,6 +274,8 @@ function useHistory() {
setUpdatesInfo, setUpdatesInfo,
labels, labels,
setLabels, setLabels,
labelsOnly,
setLabelsOnly,
userHasFullFeature, userHasFullFeature,
currentUserIsOwner, currentUserIsOwner,
projectId, projectId,
@ -286,6 +293,8 @@ function useHistory() {
setUpdatesInfo, setUpdatesInfo,
labels, labels,
setLabels, setLabels,
labelsOnly,
setLabelsOnly,
userHasFullFeature, userHasFullFeature,
currentUserIsOwner, currentUserIsOwner,
projectId, projectId,

View file

@ -31,6 +31,8 @@ export type HistoryContextValue = {
setError: React.Dispatch<React.SetStateAction<HistoryContextValue['error']>> setError: React.Dispatch<React.SetStateAction<HistoryContextValue['error']>>
labels: Nullable<LoadedLabel[]> labels: Nullable<LoadedLabel[]>
setLabels: React.Dispatch<React.SetStateAction<HistoryContextValue['labels']>> setLabels: React.Dispatch<React.SetStateAction<HistoryContextValue['labels']>>
labelsOnly: boolean
setLabelsOnly: React.Dispatch<React.SetStateAction<boolean>>
projectId: string projectId: string
selection: Selection selection: Selection
setSelection: React.Dispatch< setSelection: React.Dispatch<

View file

@ -1,9 +1,16 @@
import { useHistoryContext } from '../context/history-context' import { useHistoryContext } from '../context/history-context'
import { isLabel, loadLabels } from '../utils/label' import { getVersionWithLabels, isLabel, loadLabels } from '../utils/label'
import { Label } from '../services/types/label' import { Label } from '../services/types/label'
function useAddOrRemoveLabels() { function useAddOrRemoveLabels() {
const { updatesInfo, setUpdatesInfo, labels, setLabels } = useHistoryContext() const {
updatesInfo,
setUpdatesInfo,
labels,
setLabels,
selection,
resetSelection,
} = useHistoryContext()
const addOrRemoveLabel = ( const addOrRemoveLabel = (
label: Label, label: Label,
@ -25,7 +32,10 @@ function useAddOrRemoveLabels() {
if (labels) { if (labels) {
const nonPseudoLabels = labels.filter(isLabel) const nonPseudoLabels = labels.filter(isLabel)
const processedNonPseudoLabels = labelsHandler(nonPseudoLabels) const processedNonPseudoLabels = labelsHandler(nonPseudoLabels)
setLabels(loadLabels(processedNonPseudoLabels, tempUpdates[0].toV)) const newLabels = loadLabels(processedNonPseudoLabels, tempUpdates[0].toV)
setLabels(newLabels)
return newLabels
} }
} }
@ -37,7 +47,20 @@ function useAddOrRemoveLabels() {
const removeUpdateLabel = (label: Label) => { const removeUpdateLabel = (label: Label) => {
const labelHandler = (labels: Label[]) => const labelHandler = (labels: Label[]) =>
labels.filter(({ id }) => id !== label.id) labels.filter(({ id }) => id !== label.id)
addOrRemoveLabel(label, labelHandler) const newLabels = addOrRemoveLabel(label, labelHandler)
// removing all labels from current selection should reset the selection
if (newLabels) {
const versionWithLabels = getVersionWithLabels(newLabels)
// build an Array<number> of available versions
const versions = versionWithLabels.map(v => v.version)
const selectedVersion = selection.updateRange?.toV
// check whether the versions array has a version matching the current selection
if (selectedVersion && !versions.includes(selectedVersion)) {
resetSelection()
}
}
} }
return { addUpdateLabel, removeUpdateLabel } return { addUpdateLabel, removeUpdateLabel }

View file

@ -1,4 +1,4 @@
import { orderBy } from 'lodash' import { orderBy, groupBy } from 'lodash'
import { import {
LoadedLabel, LoadedLabel,
Label, Label,
@ -63,4 +63,17 @@ export const loadLabels = (
return labelsWithPseudoLabelIfNeeded return labelsWithPseudoLabelIfNeeded
} }
export const updateLabels = () => {} export const getVersionWithLabels = (labels: Nullable<LoadedLabel[]>) => {
let versionWithLabels: { version: number; labels: LoadedLabel[] }[] = []
if (labels) {
const groupedLabelsHash = groupBy(labels, 'version')
versionWithLabels = Object.keys(groupedLabelsHash).map(key => ({
version: parseInt(key, 10),
labels: groupedLabelsHash[key],
}))
versionWithLabels = orderBy(versionWithLabels, ['version'], ['desc'])
}
return versionWithLabels
}

View file

@ -0,0 +1,26 @@
import { UpdateRange } from '../services/types/update'
export const computeUpdateRange = (
updateRange: UpdateRange,
fromV: number,
toV: number,
updateMetaEndTimestamp: number
) => {
const fromVersion = Math.min(fromV, updateRange.fromV)
const toVersion = Math.max(toV, updateRange.toV)
const fromVTimestamp = Math.min(
updateMetaEndTimestamp,
updateRange.fromVTimestamp
)
const toVTimestamp = Math.max(
updateMetaEndTimestamp,
updateRange.toVTimestamp
)
return {
fromV: fromVersion,
toV: toVersion,
fromVTimestamp,
toVTimestamp,
}
}