mirror of
https://github.com/overleaf/overleaf.git
synced 2024-09-16 02:52:31 -04:00
Merge pull request #12773 from overleaf/ii-history-react-changes-list-dropdown
[web] Actions dropdown history migration GitOrigin-RevId: 6b7055501c5eb1529b1794db92bb9f5f3faa6648
This commit is contained in:
parent
28f90c8a87
commit
0895e33235
27 changed files with 788 additions and 186 deletions
|
@ -354,15 +354,21 @@
|
|||
"hide_document_preamble": "",
|
||||
"hide_outline": "",
|
||||
"history": "",
|
||||
"history_add_label": "",
|
||||
"history_adding_label": "",
|
||||
"history_are_you_sure_delete_label": "",
|
||||
"history_compare_with_this_version": "",
|
||||
"history_delete_label": "",
|
||||
"history_deleting_label": "",
|
||||
"history_download_this_version": "",
|
||||
"history_entry_origin_dropbox": "",
|
||||
"history_entry_origin_git": "",
|
||||
"history_entry_origin_github": "",
|
||||
"history_entry_origin_upload": "",
|
||||
"history_label_created_by": "",
|
||||
"history_label_project_current_state": "",
|
||||
"history_label_this_version": "",
|
||||
"history_new_label_name": "",
|
||||
"history_view_a11y_description": "",
|
||||
"history_view_all": "",
|
||||
"history_view_labels": "",
|
||||
|
@ -512,6 +518,7 @@
|
|||
"menu": "",
|
||||
"month": "",
|
||||
"more": "",
|
||||
"more_actions": "",
|
||||
"n_items": "",
|
||||
"n_items_plural": "",
|
||||
"n_more_updates_above": "",
|
||||
|
|
|
@ -0,0 +1,108 @@
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import { useState } from 'react'
|
||||
import { Modal, FormGroup, FormControl } from 'react-bootstrap'
|
||||
import ModalError from './modal-error'
|
||||
import AccessibleModal from '../../../../shared/components/accessible-modal'
|
||||
import useAsync from '../../../../shared/hooks/use-async'
|
||||
import useAbortController from '../../../../shared/hooks/use-abort-controller'
|
||||
import useAddOrRemoveLabels from '../../hooks/use-add-or-remove-labels'
|
||||
import { useHistoryContext } from '../../context/history-context'
|
||||
import { addLabel } from '../../services/api'
|
||||
import { Label } from '../../services/types/label'
|
||||
|
||||
type AddLabelModalProps = {
|
||||
show: boolean
|
||||
setShow: React.Dispatch<React.SetStateAction<boolean>>
|
||||
version: number
|
||||
}
|
||||
|
||||
function AddLabelModal({ show, setShow, version }: AddLabelModalProps) {
|
||||
const { t } = useTranslation()
|
||||
const [comment, setComment] = useState('')
|
||||
const {
|
||||
isLoading,
|
||||
isSuccess,
|
||||
isError,
|
||||
error,
|
||||
data: label,
|
||||
reset,
|
||||
runAsync,
|
||||
} = useAsync<Label>()
|
||||
const { projectId } = useHistoryContext()
|
||||
const { signal } = useAbortController()
|
||||
const { addUpdateLabel } = useAddOrRemoveLabels()
|
||||
|
||||
const handleModalExited = () => {
|
||||
setComment('')
|
||||
|
||||
if (!isSuccess || !label) return
|
||||
|
||||
addUpdateLabel(label)
|
||||
|
||||
reset()
|
||||
|
||||
// TODO
|
||||
// _handleHistoryUIStateChange()
|
||||
}
|
||||
|
||||
const handleAddLabel = () => {
|
||||
runAsync(addLabel(projectId, { comment, version }, signal))
|
||||
.then(() => setShow(false))
|
||||
.catch(console.error)
|
||||
}
|
||||
|
||||
const responseError = error as unknown as {
|
||||
response: Response
|
||||
data?: {
|
||||
message?: string
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<AccessibleModal
|
||||
show={show}
|
||||
onExited={handleModalExited}
|
||||
onHide={() => setShow(false)}
|
||||
id="add-history-label"
|
||||
>
|
||||
<Modal.Header>
|
||||
<Modal.Title>{t('history_add_label')}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
{isError && <ModalError error={responseError} />}
|
||||
<FormGroup>
|
||||
<FormControl
|
||||
type="text"
|
||||
placeholder={t('history_new_label_name')}
|
||||
required
|
||||
value={comment}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement & FormControl>) =>
|
||||
setComment(e.target.value)
|
||||
}
|
||||
autoFocus // eslint-disable-line jsx-a11y/no-autofocus
|
||||
/>
|
||||
</FormGroup>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
disabled={isLoading}
|
||||
onClick={() => setShow(false)}
|
||||
>
|
||||
{t('cancel')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary"
|
||||
disabled={isLoading || !comment.length}
|
||||
onClick={handleAddLabel}
|
||||
>
|
||||
{isLoading ? t('history_adding_label') : t('history_add_label')}
|
||||
</button>
|
||||
</Modal.Footer>
|
||||
</AccessibleModal>
|
||||
)
|
||||
}
|
||||
|
||||
export default AddLabelModal
|
|
@ -0,0 +1,43 @@
|
|||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Dropdown } from 'react-bootstrap'
|
||||
import DropdownToggleWithTooltip from '../../../../../shared/components/dropdown/dropdown-toggle-with-tooltip'
|
||||
import Icon from '../../../../../shared/components/icon'
|
||||
|
||||
type DropdownMenuProps = {
|
||||
id: string
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
function ActionsDropdown({ id, children }: DropdownMenuProps) {
|
||||
const { t } = useTranslation()
|
||||
const [isOpened, setIsOpened] = useState(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dropdown
|
||||
id={`history-version-dropdown-${id}`}
|
||||
pullRight
|
||||
open={isOpened}
|
||||
onToggle={open => setIsOpened(open)}
|
||||
>
|
||||
<DropdownToggleWithTooltip
|
||||
bsRole="toggle"
|
||||
className="history-version-dropdown-menu-btn"
|
||||
tooltipProps={{
|
||||
id,
|
||||
description: t('more_actions'),
|
||||
overlayProps: { placement: 'bottom', trigger: ['hover'] },
|
||||
}}
|
||||
>
|
||||
<Icon type="ellipsis-v" accessibilityLabel={t('more_actions')} />
|
||||
</DropdownToggleWithTooltip>
|
||||
<Dropdown.Menu className="history-version-dropdown-menu">
|
||||
{children}
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ActionsDropdown
|
|
@ -0,0 +1,40 @@
|
|||
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}>
|
||||
<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,37 @@
|
|||
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}>
|
||||
<Download projectId={projectId} version={version} />
|
||||
{!isComparing && !isSelected && (
|
||||
<Compare
|
||||
projectId={projectId}
|
||||
fromV={version}
|
||||
toV={version}
|
||||
updateMetaEndTimestamp={updateMetaEndTimestamp}
|
||||
/>
|
||||
)}
|
||||
</ActionsDropdown>
|
||||
)
|
||||
}
|
||||
|
||||
export default LabelDropdown
|
|
@ -0,0 +1,30 @@
|
|||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { MenuItem } from 'react-bootstrap'
|
||||
import Icon from '../../../../../../shared/components/icon'
|
||||
import AddLabelModal from '../../add-label-modal'
|
||||
|
||||
type DownloadProps = {
|
||||
projectId: string
|
||||
version: number
|
||||
}
|
||||
|
||||
function AddLabel({ version, projectId, ...props }: DownloadProps) {
|
||||
const { t } = useTranslation()
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
<MenuItem onClick={() => setShowModal(true)} {...props}>
|
||||
<Icon type="tag" fw /> {t('history_label_this_version')}
|
||||
</MenuItem>
|
||||
<AddLabelModal
|
||||
show={showModal}
|
||||
setShow={setShowModal}
|
||||
version={version}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default AddLabel
|
|
@ -0,0 +1,63 @@
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import { MenuItem, MenuItemProps } from 'react-bootstrap'
|
||||
import Icon from '../../../../../../shared/components/icon'
|
||||
import { useHistoryContext } from '../../../../context/history-context'
|
||||
import { UpdateRange } from '../../../../services/types/update'
|
||||
|
||||
type CompareProps = {
|
||||
projectId: string
|
||||
updateMetaEndTimestamp: number
|
||||
} & Pick<UpdateRange, 'fromV' | 'toV'>
|
||||
|
||||
function Compare({
|
||||
projectId,
|
||||
fromV,
|
||||
toV,
|
||||
updateMetaEndTimestamp,
|
||||
...props
|
||||
}: CompareProps) {
|
||||
const { t } = useTranslation()
|
||||
const { selection, setSelection } = useHistoryContext()
|
||||
|
||||
function compare() {
|
||||
const { updateRange } = selection
|
||||
if (!updateRange) {
|
||||
return
|
||||
}
|
||||
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
|
||||
)
|
||||
|
||||
setSelection({
|
||||
updateRange: {
|
||||
fromV: fromVersion,
|
||||
toV: toVersion,
|
||||
fromVTimestamp,
|
||||
toVTimestamp,
|
||||
},
|
||||
comparing: true,
|
||||
files: [],
|
||||
pathname: null,
|
||||
})
|
||||
}
|
||||
|
||||
const handleCompareVersion = (e: React.MouseEvent<MenuItemProps>) => {
|
||||
e.stopPropagation()
|
||||
compare()
|
||||
}
|
||||
|
||||
return (
|
||||
<MenuItem onClick={handleCompareVersion} {...props}>
|
||||
<Icon type="exchange" fw /> {t('history_compare_with_this_version')}
|
||||
</MenuItem>
|
||||
)
|
||||
}
|
||||
|
||||
export default Compare
|
|
@ -0,0 +1,33 @@
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import { MenuItem, MenuItemProps } from 'react-bootstrap'
|
||||
import Icon from '../../../../../../shared/components/icon'
|
||||
import * as location from '../../../../../../shared/components/location'
|
||||
|
||||
type DownloadProps = {
|
||||
projectId: string
|
||||
version: number
|
||||
}
|
||||
|
||||
function Download({ version, projectId, ...props }: DownloadProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const handleDownloadVersion = (e: React.MouseEvent<MenuItemProps>) => {
|
||||
e.preventDefault()
|
||||
const event = e as typeof e & { target: HTMLAnchorElement }
|
||||
location.assign(event.target.href)
|
||||
}
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
href={`/project/${projectId}/version/${version}/zip`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
onClick={handleDownloadVersion}
|
||||
{...props}
|
||||
>
|
||||
<Icon type="cloud-download" fw /> {t('history_download_this_version')}
|
||||
</MenuItem>
|
||||
)
|
||||
}
|
||||
|
||||
export default Download
|
|
@ -0,0 +1,47 @@
|
|||
import { useHistoryContext } from '../../context/history-context'
|
||||
import classnames from 'classnames'
|
||||
import { UpdateRange } from '../../services/types/update'
|
||||
|
||||
type HistoryVersionDetailsProps = {
|
||||
children: React.ReactNode
|
||||
selected: boolean
|
||||
} & UpdateRange
|
||||
|
||||
function HistoryVersionDetails({
|
||||
children,
|
||||
selected,
|
||||
fromV,
|
||||
toV,
|
||||
fromVTimestamp,
|
||||
toVTimestamp,
|
||||
}: HistoryVersionDetailsProps) {
|
||||
const { setSelection } = useHistoryContext()
|
||||
|
||||
const handleSelect = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
const target = e.target as HTMLElement
|
||||
if (!target.closest('.dropdown') && e.currentTarget.contains(target)) {
|
||||
setSelection({
|
||||
updateRange: { fromV, toV, fromVTimestamp, toVTimestamp },
|
||||
comparing: false,
|
||||
files: [],
|
||||
pathname: null,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
// TODO: Sort out accessibility for this
|
||||
// eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions
|
||||
<div
|
||||
className={classnames('history-version-details', {
|
||||
'history-version-selected': selected,
|
||||
})}
|
||||
data-testid="history-version-details"
|
||||
onClick={handleSelect}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default HistoryVersionDetails
|
|
@ -1,14 +1,15 @@
|
|||
import HistoryVersionDetails from './history-version-details'
|
||||
import TagTooltip from './tag-tooltip'
|
||||
import Changes from './changes'
|
||||
import MetadataUsersList from './metadata-users-list'
|
||||
import Origin from './origin'
|
||||
import HistoryVersionDropdown from './dropdown/history-version-dropdown'
|
||||
import { useUserContext } from '../../../../shared/context/user-context'
|
||||
import { useHistoryContext } from '../../context/history-context'
|
||||
import { isUpdateSelected } from '../../utils/history-details'
|
||||
import { relativeDate, formatTime } from '../../../utils/format-date'
|
||||
import { orderBy } from 'lodash'
|
||||
import { LoadedUpdate } from '../../services/types/update'
|
||||
import { useHistoryContext } from '../../context/history-context'
|
||||
import classNames from 'classnames'
|
||||
import { updateIsSelected } from '../../utils/history-details'
|
||||
|
||||
type HistoryEntryProps = {
|
||||
update: LoadedUpdate
|
||||
|
@ -16,94 +17,62 @@ type HistoryEntryProps = {
|
|||
|
||||
function HistoryVersion({ update }: HistoryEntryProps) {
|
||||
const { id: currentUserId } = useUserContext()
|
||||
const { projectId, selection } = useHistoryContext()
|
||||
|
||||
const orderedLabels = orderBy(update.labels, ['created_at'], ['desc'])
|
||||
|
||||
const { selection, setSelection } = useHistoryContext()
|
||||
|
||||
const selected = updateIsSelected({
|
||||
const selected = isUpdateSelected({
|
||||
fromV: update.fromV,
|
||||
toV: update.toV,
|
||||
selection,
|
||||
})
|
||||
|
||||
function compare() {
|
||||
const { updateRange } = selection
|
||||
if (!updateRange) {
|
||||
return
|
||||
}
|
||||
const fromV = Math.min(update.fromV, updateRange.fromV)
|
||||
const toV = Math.max(update.toV, updateRange.toV)
|
||||
const fromVTimestamp = Math.min(
|
||||
update.meta.end_ts,
|
||||
updateRange.fromVTimestamp
|
||||
)
|
||||
const toVTimestamp = Math.max(update.meta.end_ts, updateRange.toVTimestamp)
|
||||
|
||||
setSelection({
|
||||
updateRange: { fromV, toV, fromVTimestamp, toVTimestamp },
|
||||
comparing: true,
|
||||
files: [],
|
||||
pathname: null,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classNames({ 'history-version-selected': selected })}>
|
||||
<div>
|
||||
{update.meta.first_in_day && (
|
||||
<time className="history-version-day">
|
||||
{relativeDate(update.meta.end_ts)}
|
||||
</time>
|
||||
)}
|
||||
{/* TODO: Sort out accessibility for this */}
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */}
|
||||
<div
|
||||
className="history-version-details"
|
||||
data-testid="history-version-details"
|
||||
onClick={() =>
|
||||
setSelection({
|
||||
updateRange: {
|
||||
fromV: update.fromV,
|
||||
toV: update.toV,
|
||||
fromVTimestamp: update.meta.end_ts,
|
||||
toVTimestamp: update.meta.end_ts,
|
||||
},
|
||||
comparing: false,
|
||||
files: [],
|
||||
pathname: null,
|
||||
})
|
||||
}
|
||||
<HistoryVersionDetails
|
||||
fromV={update.fromV}
|
||||
toV={update.toV}
|
||||
fromVTimestamp={update.meta.end_ts}
|
||||
toVTimestamp={update.meta.end_ts}
|
||||
selected={selected}
|
||||
>
|
||||
<time className="history-version-metadata-time">
|
||||
<b>{formatTime(update.meta.end_ts, 'Do MMMM, h:mm a')}</b>
|
||||
</time>
|
||||
{orderedLabels.map(label => (
|
||||
<TagTooltip
|
||||
key={label.id}
|
||||
showTooltip
|
||||
currentUserId={currentUserId}
|
||||
label={label}
|
||||
<div className="history-version-main-details">
|
||||
<time className="history-version-metadata-time">
|
||||
<b>{formatTime(update.meta.end_ts, 'Do MMMM, h:mm a')}</b>
|
||||
</time>
|
||||
{orderedLabels.map(label => (
|
||||
<TagTooltip
|
||||
key={label.id}
|
||||
showTooltip
|
||||
currentUserId={currentUserId}
|
||||
label={label}
|
||||
/>
|
||||
))}
|
||||
<Changes
|
||||
pathnames={update.pathnames}
|
||||
projectOps={update.project_ops}
|
||||
/>
|
||||
))}
|
||||
<Changes pathnames={update.pathnames} projectOps={update.project_ops} />
|
||||
<MetadataUsersList
|
||||
users={update.meta.users}
|
||||
origin={update.meta.origin}
|
||||
currentUserId={currentUserId}
|
||||
<MetadataUsersList
|
||||
users={update.meta.users}
|
||||
origin={update.meta.origin}
|
||||
currentUserId={currentUserId}
|
||||
/>
|
||||
<Origin origin={update.meta.origin} />
|
||||
</div>
|
||||
<HistoryVersionDropdown
|
||||
id={`${update.fromV}_${update.toV}`}
|
||||
projectId={projectId}
|
||||
isComparing={selection.comparing}
|
||||
isSelected={selected}
|
||||
fromV={update.fromV}
|
||||
toV={update.toV}
|
||||
updateMetaEndTimestamp={update.meta.end_ts}
|
||||
/>
|
||||
<Origin origin={update.meta.origin} />
|
||||
{selection.comparing ? null : (
|
||||
<div>
|
||||
<button
|
||||
onClick={event => {
|
||||
event.stopPropagation()
|
||||
compare()
|
||||
}}
|
||||
>
|
||||
Compare
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</HistoryVersionDetails>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import { Fragment } from 'react'
|
||||
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 { isUpdateSelected } from '../../utils/history-details'
|
||||
import { isPseudoLabel } from '../../utils/label'
|
||||
import { formatTime } from '../../../utils/format-date'
|
||||
import { groupBy, orderBy } from 'lodash'
|
||||
|
@ -11,7 +13,7 @@ import { LoadedLabel } from '../../services/types/label'
|
|||
|
||||
function LabelsList() {
|
||||
const { t } = useTranslation()
|
||||
const { labels } = useHistoryContext()
|
||||
const { updatesInfo, labels, projectId, selection } = useHistoryContext()
|
||||
const { id: currentUserId } = useUserContext()
|
||||
|
||||
let versionWithLabels: { version: number; labels: LoadedLabel[] }[] = []
|
||||
|
@ -26,40 +28,67 @@ function LabelsList() {
|
|||
|
||||
return (
|
||||
<>
|
||||
{versionWithLabels.map(({ version, labels }) => (
|
||||
<div
|
||||
key={version}
|
||||
className="history-version-details"
|
||||
data-testid="history-version-details"
|
||||
>
|
||||
{labels.map(label => (
|
||||
<Fragment key={label.id}>
|
||||
<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,
|
||||
}}
|
||||
{versionWithLabels.map(({ version, labels }) => {
|
||||
const selected = isUpdateSelected({
|
||||
fromV: version,
|
||||
toV: version,
|
||||
selection,
|
||||
})
|
||||
|
||||
const update = updatesInfo.updates.find(update => {
|
||||
return update.labels.some(label => label.version === version)
|
||||
})
|
||||
|
||||
if (!update) return null
|
||||
|
||||
return (
|
||||
<HistoryVersionDetails
|
||||
key={version}
|
||||
fromV={version}
|
||||
toV={version}
|
||||
fromVTimestamp={update.meta.end_ts}
|
||||
toVTimestamp={update.meta.end_ts}
|
||||
selected={selected}
|
||||
>
|
||||
<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>
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
))}
|
||||
</div>
|
||||
<LabelDropdown
|
||||
id={version.toString()}
|
||||
projectId={projectId}
|
||||
version={version}
|
||||
updateMetaEndTimestamp={update.meta.end_ts}
|
||||
isComparing={selection.comparing}
|
||||
isSelected={selected}
|
||||
/>
|
||||
</HistoryVersionDetails>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import { Alert } from 'react-bootstrap'
|
||||
|
||||
// Using this workaround due to inconsistent and improper error responses from the server
|
||||
type ModalErrorProps = {
|
||||
error: {
|
||||
response: Response
|
||||
data?: {
|
||||
message?: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function ModalError({ error }: ModalErrorProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (error.response.status === 400 && error?.data?.message) {
|
||||
return <Alert bsStyle="danger">{error.data.message}</Alert>
|
||||
}
|
||||
|
||||
return <Alert bsStyle="danger">{t('generic_something_went_wrong')}</Alert>
|
||||
}
|
||||
|
||||
export default ModalError
|
|
@ -1,14 +1,17 @@
|
|||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Alert, Modal } from 'react-bootstrap'
|
||||
import { useHistoryContext } from '../../context/history-context'
|
||||
import { Modal } from 'react-bootstrap'
|
||||
import Icon from '../../../../shared/components/icon'
|
||||
import Tooltip from '../../../../shared/components/tooltip'
|
||||
import Badge from '../../../../shared/components/badge'
|
||||
import AccessibleModal from '../../../../shared/components/accessible-modal'
|
||||
import ModalError from './modal-error'
|
||||
import useAbortController from '../../../../shared/hooks/use-abort-controller'
|
||||
import useAsync from '../../../../shared/hooks/use-async'
|
||||
import { deleteJSON } from '../../../../infrastructure/fetch-json'
|
||||
import { isLabel, isPseudoLabel, loadLabels } from '../../utils/label'
|
||||
import useAddOrRemoveLabels from '../../hooks/use-add-or-remove-labels'
|
||||
import { useHistoryContext } from '../../context/history-context'
|
||||
import { deleteLabel } from '../../services/api'
|
||||
import { isPseudoLabel } from '../../utils/label'
|
||||
import { formatDate } from '../../../../utils/dates'
|
||||
import { LoadedLabel } from '../../services/types/label'
|
||||
|
||||
|
@ -20,9 +23,10 @@ type TagProps = {
|
|||
function Tag({ label, currentUserId, ...props }: TagProps) {
|
||||
const { t } = useTranslation()
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false)
|
||||
const { projectId, updatesInfo, setUpdatesInfo, labels, setLabels } =
|
||||
useHistoryContext()
|
||||
const { isLoading, isSuccess, isError, error, runAsync } = useAsync()
|
||||
const { projectId } = useHistoryContext()
|
||||
const { signal } = useAbortController()
|
||||
const { removeUpdateLabel } = useAddOrRemoveLabels()
|
||||
const { isLoading, isSuccess, isError, error, reset, runAsync } = useAsync()
|
||||
const isPseudoCurrentStateLabel = isPseudoLabel(label)
|
||||
const isOwnedByCurrentUser = !isPseudoCurrentStateLabel
|
||||
? label.user_id === currentUserId
|
||||
|
@ -36,35 +40,18 @@ function Tag({ label, currentUserId, ...props }: TagProps) {
|
|||
const handleModalExited = () => {
|
||||
if (!isSuccess) return
|
||||
|
||||
const tempUpdates = [...updatesInfo.updates]
|
||||
for (const [i, update] of tempUpdates.entries()) {
|
||||
if (update.toV === label.version) {
|
||||
tempUpdates[i] = {
|
||||
...update,
|
||||
labels: update.labels.filter(({ id }) => id !== label.id),
|
||||
}
|
||||
break
|
||||
}
|
||||
if (!isPseudoCurrentStateLabel) {
|
||||
removeUpdateLabel(label)
|
||||
}
|
||||
|
||||
setUpdatesInfo({ ...updatesInfo, updates: tempUpdates })
|
||||
|
||||
if (labels) {
|
||||
const nonPseudoLabels = labels.filter(isLabel)
|
||||
const filteredLabels = nonPseudoLabels.filter(({ id }) => id !== label.id)
|
||||
setLabels(loadLabels(filteredLabels, tempUpdates[0].toV))
|
||||
}
|
||||
|
||||
setShowDeleteModal(false)
|
||||
reset()
|
||||
|
||||
// TODO _handleHistoryUIStateChange
|
||||
}
|
||||
|
||||
const localDeleteHandler = () => {
|
||||
runAsync(deleteJSON(`/project/${projectId}/labels/${label.id}`))
|
||||
.then(() => {
|
||||
setShowDeleteModal(false)
|
||||
})
|
||||
runAsync(deleteLabel(projectId, label.id, signal))
|
||||
.then(() => setShowDeleteModal(false))
|
||||
.catch(console.error)
|
||||
}
|
||||
|
||||
|
@ -103,16 +90,7 @@ function Tag({ label, currentUserId, ...props }: TagProps) {
|
|||
<Modal.Title>{t('history_delete_label')}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
{isError ? (
|
||||
responseError.response.status === 400 &&
|
||||
responseError?.data?.message ? (
|
||||
<Alert bsStyle="danger">{responseError.data.message}</Alert>
|
||||
) : (
|
||||
<Alert bsStyle="danger">
|
||||
{t('generic_something_went_wrong')}
|
||||
</Alert>
|
||||
)
|
||||
) : null}
|
||||
{isError && <ModalError error={responseError} />}
|
||||
<p>
|
||||
{t('history_are_you_sure_delete_label')}
|
||||
<strong>"{label.comment}"</strong>?
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import { useHistoryContext } from '../../context/history-context'
|
||||
|
||||
type ToggleSwitchProps = {
|
||||
labelsOnly: boolean
|
||||
|
@ -9,6 +10,15 @@ type ToggleSwitchProps = {
|
|||
|
||||
function ToggleSwitch({ labelsOnly, setLabelsOnly }: ToggleSwitchProps) {
|
||||
const { t } = useTranslation()
|
||||
const { resetSelection, selection } = useHistoryContext()
|
||||
|
||||
const handleChange = (isLabelsOnly: boolean) => {
|
||||
if (selection.comparing) {
|
||||
resetSelection()
|
||||
}
|
||||
|
||||
setLabelsOnly(isLabelsOnly)
|
||||
}
|
||||
|
||||
return (
|
||||
<fieldset className="toggle-switch">
|
||||
|
@ -17,7 +27,7 @@ function ToggleSwitch({ labelsOnly, setLabelsOnly }: ToggleSwitchProps) {
|
|||
type="radio"
|
||||
name="labels-only-toggle-switch"
|
||||
checked={!labelsOnly}
|
||||
onChange={() => setLabelsOnly(false)}
|
||||
onChange={() => handleChange(false)}
|
||||
className="toggle-switch-input"
|
||||
id="toggle-switch-all-history"
|
||||
/>
|
||||
|
@ -31,7 +41,7 @@ function ToggleSwitch({ labelsOnly, setLabelsOnly }: ToggleSwitchProps) {
|
|||
type="radio"
|
||||
name="labels-only-toggle-switch"
|
||||
checked={labelsOnly}
|
||||
onChange={() => setLabelsOnly(true)}
|
||||
onChange={() => handleChange(true)}
|
||||
className="toggle-switch-input"
|
||||
id="toggle-switch-labels"
|
||||
/>
|
||||
|
|
|
@ -57,6 +57,13 @@ function limitUpdates(
|
|||
})
|
||||
}
|
||||
|
||||
const selectionInitialState: Selection = {
|
||||
updateRange: null,
|
||||
comparing: false,
|
||||
files: [],
|
||||
pathname: null,
|
||||
}
|
||||
|
||||
function useHistory() {
|
||||
const { view } = useLayoutContext()
|
||||
const user = useUserContext()
|
||||
|
@ -65,12 +72,7 @@ function useHistory() {
|
|||
const projectId = project._id
|
||||
const projectOwnerId = project.owner?._id
|
||||
|
||||
const [selection, setSelection] = useState<Selection>({
|
||||
updateRange: null,
|
||||
comparing: false,
|
||||
files: [],
|
||||
pathname: null,
|
||||
})
|
||||
const [selection, setSelection] = useState<Selection>(selectionInitialState)
|
||||
|
||||
const [updatesInfo, setUpdatesInfo] = useState<
|
||||
HistoryContextValue['updatesInfo']
|
||||
|
@ -183,6 +185,10 @@ function useHistory() {
|
|||
updatesInfo,
|
||||
])
|
||||
|
||||
const resetSelection = useCallback(() => {
|
||||
setSelection(selectionInitialState)
|
||||
}, [])
|
||||
|
||||
// Initial load when the History tab is active
|
||||
const initialFetch = useRef(false)
|
||||
useEffect(() => {
|
||||
|
@ -250,6 +256,7 @@ function useHistory() {
|
|||
selection,
|
||||
setSelection,
|
||||
fetchNextBatchOfUpdates,
|
||||
resetSelection,
|
||||
}),
|
||||
[
|
||||
error,
|
||||
|
@ -263,6 +270,7 @@ function useHistory() {
|
|||
selection,
|
||||
setSelection,
|
||||
fetchNextBatchOfUpdates,
|
||||
resetSelection,
|
||||
]
|
||||
)
|
||||
|
||||
|
|
|
@ -20,6 +20,9 @@ export type HistoryContextValue = {
|
|||
setLabels: React.Dispatch<React.SetStateAction<HistoryContextValue['labels']>>
|
||||
projectId: string
|
||||
selection: Selection
|
||||
setSelection: (selection: Selection) => void
|
||||
setSelection: React.Dispatch<
|
||||
React.SetStateAction<HistoryContextValue['selection']>
|
||||
>
|
||||
fetchNextBatchOfUpdates: () => void
|
||||
resetSelection: () => void
|
||||
}
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
import { useHistoryContext } from '../context/history-context'
|
||||
import { isLabel, loadLabels } from '../utils/label'
|
||||
import { Label } from '../services/types/label'
|
||||
|
||||
function useAddOrRemoveLabels() {
|
||||
const { updatesInfo, setUpdatesInfo, labels, setLabels } = useHistoryContext()
|
||||
|
||||
const addOrRemoveLabel = (
|
||||
label: Label,
|
||||
labelsHandler: (label: Label[]) => Label[]
|
||||
) => {
|
||||
const tempUpdates = [...updatesInfo.updates]
|
||||
for (const [i, update] of tempUpdates.entries()) {
|
||||
if (update.toV === label.version) {
|
||||
tempUpdates[i] = {
|
||||
...update,
|
||||
labels: labelsHandler(update.labels),
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
setUpdatesInfo({ ...updatesInfo, updates: tempUpdates })
|
||||
|
||||
if (labels) {
|
||||
const nonPseudoLabels = labels.filter(isLabel)
|
||||
const processedNonPseudoLabels = labelsHandler(nonPseudoLabels)
|
||||
setLabels(loadLabels(processedNonPseudoLabels, tempUpdates[0].toV))
|
||||
}
|
||||
}
|
||||
|
||||
const addUpdateLabel = (label: Label) => {
|
||||
const labelHandler = (labels: Label[]) => labels.concat(label)
|
||||
addOrRemoveLabel(label, labelHandler)
|
||||
}
|
||||
|
||||
const removeUpdateLabel = (label: Label) => {
|
||||
const labelHandler = (labels: Label[]) =>
|
||||
labels.filter(({ id }) => id !== label.id)
|
||||
addOrRemoveLabel(label, labelHandler)
|
||||
}
|
||||
|
||||
return { addUpdateLabel, removeUpdateLabel }
|
||||
}
|
||||
|
||||
export default useAddOrRemoveLabels
|
|
@ -1,4 +1,8 @@
|
|||
import { getJSON } from '../../../infrastructure/fetch-json'
|
||||
import {
|
||||
deleteJSON,
|
||||
getJSON,
|
||||
postJSON,
|
||||
} from '../../../infrastructure/fetch-json'
|
||||
import { FileDiff } from './types/file'
|
||||
import { FetchUpdatesResponse } from './types/update'
|
||||
import { Label } from './types/label'
|
||||
|
@ -25,6 +29,22 @@ export function fetchLabels(projectId: string) {
|
|||
return getJSON<Label[]>(labelsURL)
|
||||
}
|
||||
|
||||
export function addLabel(
|
||||
projectId: string,
|
||||
body: { comment: string; version: number },
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
return postJSON(`/project/${projectId}/labels`, { body, signal })
|
||||
}
|
||||
|
||||
export function deleteLabel(
|
||||
projectId: string,
|
||||
labelId: string,
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
return deleteJSON(`/project/${projectId}/labels/${labelId}`, { signal })
|
||||
}
|
||||
|
||||
export function diffFiles(projectId: string, fromV: number, toV: number) {
|
||||
const queryParams: Record<string, string> = {
|
||||
from: fromV.toString(),
|
||||
|
|
|
@ -1,22 +1,24 @@
|
|||
import { Nullable } from '../../../../../../types/utils'
|
||||
|
||||
interface UpdateLabel {
|
||||
interface LabelBase {
|
||||
id: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
interface UpdateLabel extends LabelBase {
|
||||
comment: string
|
||||
version: number
|
||||
user_id: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export interface Label extends UpdateLabel {
|
||||
user_display_name: string
|
||||
}
|
||||
|
||||
export interface PseudoCurrentStateLabel {
|
||||
export interface PseudoCurrentStateLabel extends LabelBase {
|
||||
id: '1'
|
||||
isPseudoCurrentStateLabel: true
|
||||
version: Nullable<number>
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export type LoadedLabel = Label | PseudoCurrentStateLabel
|
||||
|
|
|
@ -43,14 +43,15 @@ type UpdateIsSelectedArg = {
|
|||
selection: Selection
|
||||
}
|
||||
|
||||
export const updateIsSelected = ({
|
||||
export const isUpdateSelected = ({
|
||||
fromV,
|
||||
toV,
|
||||
selection,
|
||||
}: UpdateIsSelectedArg) => {
|
||||
return (
|
||||
selection.updateRange &&
|
||||
fromV >= selection.updateRange.fromV &&
|
||||
toV <= selection.updateRange.toV
|
||||
)
|
||||
if (selection.updateRange) {
|
||||
return (
|
||||
fromV >= selection.updateRange.fromV && toV <= selection.updateRange.toV
|
||||
)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -62,3 +62,5 @@ export const loadLabels = (
|
|||
|
||||
return labelsWithPseudoLabelIfNeeded
|
||||
}
|
||||
|
||||
export const updateLabels = () => {}
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
import { forwardRef } from 'react'
|
||||
import Tooltip from '../tooltip'
|
||||
import classnames from 'classnames'
|
||||
import { DropdownProps } from 'react-bootstrap'
|
||||
import { MergeAndOverride } from '../../../../../types/utils'
|
||||
|
||||
type CustomToggleProps = MergeAndOverride<
|
||||
Pick<DropdownProps, 'bsClass'>,
|
||||
{
|
||||
children: React.ReactNode
|
||||
bsRole: 'toggle'
|
||||
className?: string
|
||||
tooltipProps: Omit<React.ComponentProps<typeof Tooltip>, 'children'>
|
||||
}
|
||||
>
|
||||
|
||||
const DropdownToggleWithTooltip = forwardRef<
|
||||
HTMLButtonElement,
|
||||
CustomToggleProps
|
||||
>(function (props, ref) {
|
||||
const {
|
||||
tooltipProps,
|
||||
children,
|
||||
bsClass,
|
||||
className,
|
||||
bsRole: _bsRole,
|
||||
...rest
|
||||
} = props
|
||||
|
||||
return (
|
||||
<Tooltip {...tooltipProps}>
|
||||
<button
|
||||
type="button"
|
||||
ref={ref}
|
||||
className={classnames(bsClass, 'btn', className)}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
</Tooltip>
|
||||
)
|
||||
})
|
||||
DropdownToggleWithTooltip.displayName = 'DropdownToggleWithTooltip'
|
||||
|
||||
export default DropdownToggleWithTooltip
|
|
@ -1,14 +1,18 @@
|
|||
import { useState } from 'react'
|
||||
import ToggleSwitchComponent from '../../js/features/history/components/change-list/toggle-switch'
|
||||
import { ScopeDecorator } from '../decorators/scope'
|
||||
import { HistoryProvider } from '../../js/features/history/context/history-context'
|
||||
|
||||
export const LabelsOnlyToggleSwitch = () => {
|
||||
const [labelsOnly, setLabelsOnly] = useState(false)
|
||||
|
||||
return (
|
||||
<ToggleSwitchComponent
|
||||
labelsOnly={labelsOnly}
|
||||
setLabelsOnly={setLabelsOnly}
|
||||
/>
|
||||
<HistoryProvider>
|
||||
<ToggleSwitchComponent
|
||||
labelsOnly={labelsOnly}
|
||||
setLabelsOnly={setLabelsOnly}
|
||||
/>
|
||||
</HistoryProvider>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -28,6 +32,7 @@ export default {
|
|||
},
|
||||
},
|
||||
decorators: [
|
||||
ScopeDecorator,
|
||||
(Story: React.ComponentType) => (
|
||||
<div className="history-react">
|
||||
<div className="change-list">
|
||||
|
|
|
@ -83,6 +83,7 @@ history-root {
|
|||
|
||||
.history-version-day {
|
||||
position: sticky;
|
||||
z-index: 1;
|
||||
top: 0;
|
||||
display: block;
|
||||
padding-top: 12px;
|
||||
|
@ -92,6 +93,8 @@ history-root {
|
|||
}
|
||||
|
||||
.history-version-details {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
|
||||
|
@ -169,13 +172,9 @@ history-root {
|
|||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.history-version-saved-by {
|
||||
.history-version-label {
|
||||
margin-bottom: 4px;
|
||||
|
||||
&-label {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: initial;
|
||||
}
|
||||
|
@ -198,6 +197,40 @@ history-root {
|
|||
background-color: @gray-lightest;
|
||||
}
|
||||
|
||||
.history-version-saved-by {
|
||||
&-label {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown.open {
|
||||
.history-version-dropdown-menu-btn {
|
||||
background-color: rgba(@neutral-90, 0.08);
|
||||
box-shadow: initial;
|
||||
}
|
||||
}
|
||||
|
||||
.history-version-dropdown-menu-btn {
|
||||
@size: 30px;
|
||||
padding: 0;
|
||||
width: @size;
|
||||
height: @size;
|
||||
font-size: @font-size-small;
|
||||
color: @neutral-90;
|
||||
background-color: transparent;
|
||||
|
||||
&:hover,
|
||||
&:active {
|
||||
z-index: initial;
|
||||
background-color: rgba(@neutral-90, 0.08);
|
||||
box-shadow: initial;
|
||||
}
|
||||
}
|
||||
|
||||
.history-version-main-details {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.history-loading-panel {
|
||||
padding-top: 10rem;
|
||||
font-family: @font-family-serif;
|
||||
|
@ -242,6 +275,19 @@ history-root {
|
|||
}
|
||||
}
|
||||
|
||||
.history-version-dropdown-menu {
|
||||
[role='menuitem'] {
|
||||
padding: 10px 12px;
|
||||
color: @neutral-90;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: @neutral-90;
|
||||
background-color: @neutral-10;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.document-diff-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
width: @size;
|
||||
margin-left: 4px;
|
||||
margin-right: -@padding;
|
||||
font-size: @size;
|
||||
font-size: 20px;
|
||||
|
||||
&:hover {
|
||||
background-color: @neutral-40;
|
||||
|
|
|
@ -642,8 +642,10 @@
|
|||
"history_add_label": "Add label",
|
||||
"history_adding_label": "Adding label",
|
||||
"history_are_you_sure_delete_label": "Are you sure you want to delete the following label",
|
||||
"history_compare_with_this_version": "Compare with this version",
|
||||
"history_delete_label": "Delete label",
|
||||
"history_deleting_label": "Deleting label",
|
||||
"history_download_this_version": "Download this version",
|
||||
"history_entry_origin_dropbox": "via Dropbox",
|
||||
"history_entry_origin_git": "via Git",
|
||||
"history_entry_origin_github": "via GitHub",
|
||||
|
@ -911,6 +913,7 @@
|
|||
"month": "month",
|
||||
"monthly": "Monthly",
|
||||
"more": "More",
|
||||
"more_actions": "More actions",
|
||||
"more_info": "More Info",
|
||||
"more_project_collaborators": "<0>More</0> project <0>collaborators</0>",
|
||||
"more_than_one_kind_of_snippet_was_requested": "The link to open this content on Overleaf included some invalid parameters. If this keeps happening for links on a particular site, please report this to them.",
|
||||
|
|
|
@ -6,14 +6,15 @@ import { HistoryProvider } from '../../../../../frontend/js/features/history/con
|
|||
import { updates } from '../fixtures/updates'
|
||||
import { labels } from '../fixtures/labels'
|
||||
|
||||
const mountChangeList = (scope: Record<string, unknown> = {}) => {
|
||||
const mountWithEditorProviders = (
|
||||
component: React.ReactNode,
|
||||
scope: Record<string, unknown> = {}
|
||||
) => {
|
||||
cy.mount(
|
||||
<EditorProviders scope={scope}>
|
||||
<HistoryProvider>
|
||||
<div style={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<div className="history-react">
|
||||
<ChangeList />
|
||||
</div>
|
||||
<div className="history-react">{component}</div>
|
||||
</div>
|
||||
</HistoryProvider>
|
||||
</EditorProviders>
|
||||
|
@ -23,7 +24,9 @@ const mountChangeList = (scope: Record<string, unknown> = {}) => {
|
|||
describe('change list', function () {
|
||||
describe('toggle switch', function () {
|
||||
it('renders switch buttons', function () {
|
||||
cy.mount(<ToggleSwitch labelsOnly={false} setLabelsOnly={() => {}} />)
|
||||
mountWithEditorProviders(
|
||||
<ToggleSwitch labelsOnly={false} setLabelsOnly={() => {}} />
|
||||
)
|
||||
|
||||
cy.findByLabelText(/all history/i)
|
||||
cy.findByLabelText(/labels/i)
|
||||
|
@ -40,7 +43,7 @@ describe('change list', function () {
|
|||
)
|
||||
}
|
||||
|
||||
cy.mount(<ToggleSwitchWrapped labelsOnly={false} />)
|
||||
mountWithEditorProviders(<ToggleSwitchWrapped labelsOnly={false} />)
|
||||
|
||||
cy.findByLabelText(/all history/i).as('all-history')
|
||||
cy.findByLabelText(/labels/i).as('labels')
|
||||
|
@ -76,7 +79,7 @@ describe('change list', function () {
|
|||
})
|
||||
|
||||
it('renders tags', function () {
|
||||
mountChangeList(scope)
|
||||
mountWithEditorProviders(<ChangeList />, scope)
|
||||
waitForData()
|
||||
|
||||
cy.findByLabelText(/all history/i).click({ force: true })
|
||||
|
@ -141,7 +144,7 @@ describe('change list', function () {
|
|||
})
|
||||
|
||||
it('deletes tag', function () {
|
||||
mountChangeList(scope)
|
||||
mountWithEditorProviders(<ChangeList />, scope)
|
||||
waitForData()
|
||||
|
||||
cy.findByLabelText(/all history/i).click({ force: true })
|
||||
|
|
Loading…
Reference in a new issue