mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-14 20:40:17 -05:00
Merge pull request #20860 from overleaf/td-bs5-history-versions-list
Migrate history versions list to Bootstrap 5 GitOrigin-RevId: 4e006ad353cb11eadaefb2df41d2b8591003c664
This commit is contained in:
parent
8c342dc226
commit
4138f9707a
57 changed files with 1836 additions and 416 deletions
|
@ -4,6 +4,7 @@
|
|||
"function-url-quotes": null,
|
||||
"no-descending-specificity": null,
|
||||
"scss/at-extend-no-missing-placeholder": null,
|
||||
"scss/operator-no-newline-after": null
|
||||
"scss/operator-no-newline-after": null,
|
||||
"property-no-vendor-prefix": [true, { "ignoreProperties": ["mask-image"] }]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,15 @@
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Modal, FormGroup, FormControl } from 'react-bootstrap'
|
||||
import OLForm from '@/features/ui/components/ol/ol-form'
|
||||
import OLFormGroup from '@/features/ui/components/ol/ol-form-group'
|
||||
import ModalError from './modal-error'
|
||||
import AccessibleModal from '../../../../shared/components/accessible-modal'
|
||||
import OLModal, {
|
||||
OLModalBody,
|
||||
OLModalFooter,
|
||||
OLModalHeader,
|
||||
OLModalTitle,
|
||||
} from '@/features/ui/components/ol/ol-modal'
|
||||
import OLButton from '@/features/ui/components/ol/ol-button'
|
||||
import useAsync from '../../../../shared/hooks/use-async'
|
||||
import useAbortController from '../../../../shared/hooks/use-abort-controller'
|
||||
import useAddOrRemoveLabels from '../../hooks/use-add-or-remove-labels'
|
||||
|
@ -11,6 +18,7 @@ import { addLabel } from '../../services/api'
|
|||
import { Label } from '../../services/types/label'
|
||||
import { useRefWithAutoFocus } from '../../../../shared/hooks/use-ref-with-auto-focus'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
import OLFormControl from '@/features/ui/components/ol/ol-form-control'
|
||||
|
||||
type AddLabelModalProps = {
|
||||
show: boolean
|
||||
|
@ -71,51 +79,55 @@ function AddLabelModal({ show, setShow, version }: AddLabelModalProps) {
|
|||
}
|
||||
|
||||
return (
|
||||
<AccessibleModal
|
||||
<OLModal
|
||||
show={show}
|
||||
onExited={handleModalExited}
|
||||
onHide={() => setShow(false)}
|
||||
id="add-history-label"
|
||||
>
|
||||
<Modal.Header>
|
||||
<Modal.Title>{t('history_add_label')}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Modal.Body>
|
||||
<OLModalHeader>
|
||||
<OLModalTitle>{t('history_add_label')}</OLModalTitle>
|
||||
</OLModalHeader>
|
||||
<OLForm onSubmit={handleSubmit}>
|
||||
<OLModalBody>
|
||||
{isError && <ModalError error={responseError} />}
|
||||
<FormGroup>
|
||||
<input
|
||||
<OLFormGroup>
|
||||
<OLFormControl
|
||||
ref={autoFocusedRef}
|
||||
className="form-control"
|
||||
type="text"
|
||||
placeholder={t('history_new_label_name')}
|
||||
required
|
||||
value={comment}
|
||||
onChange={(
|
||||
e: React.ChangeEvent<HTMLInputElement & FormControl>
|
||||
) => setComment(e.target.value)}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setComment(e.target.value)
|
||||
}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
</OLFormGroup>
|
||||
</OLModalBody>
|
||||
<OLModalFooter>
|
||||
<OLButton
|
||||
variant="secondary"
|
||||
disabled={isLoading}
|
||||
onClick={() => setShow(false)}
|
||||
>
|
||||
{t('cancel')}
|
||||
</button>
|
||||
<button
|
||||
</OLButton>
|
||||
<OLButton
|
||||
type="submit"
|
||||
className="btn btn-primary"
|
||||
variant="primary"
|
||||
disabled={isLoading || !comment.length}
|
||||
isLoading={isLoading}
|
||||
bs3Props={{
|
||||
loading: isLoading
|
||||
? t('history_adding_label')
|
||||
: t('history_add_label'),
|
||||
}}
|
||||
>
|
||||
{isLoading ? t('history_adding_label') : t('history_add_label')}
|
||||
</button>
|
||||
</Modal.Footer>
|
||||
</form>
|
||||
</AccessibleModal>
|
||||
{t('history_add_label')}
|
||||
</OLButton>
|
||||
</OLModalFooter>
|
||||
</OLForm>
|
||||
</OLModal>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -8,7 +8,8 @@ import { useUserContext } from '../../../../shared/context/user-context'
|
|||
import useDropdownActiveItem from '../../hooks/use-dropdown-active-item'
|
||||
import { useHistoryContext } from '../../context/history-context'
|
||||
import { useEditorContext } from '../../../../shared/context/editor-context'
|
||||
import { Overlay, Popover } from 'react-bootstrap'
|
||||
import OLPopover from '@/features/ui/components/ol/ol-popover'
|
||||
import OLOverlay from '@/features/ui/components/ol/ol-overlay'
|
||||
import Close from '@/shared/components/close'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
|
@ -173,18 +174,29 @@ function AllHistoryList() {
|
|||
|
||||
if (showHistoryTutorial) {
|
||||
popover = (
|
||||
<Overlay
|
||||
placement="left"
|
||||
<OLOverlay
|
||||
placement="left-start"
|
||||
show={showHistoryTutorial}
|
||||
rootClose
|
||||
onHide={hidePopover}
|
||||
// using scrollerRef to position the popover in the middle of the viewport
|
||||
target={scrollerRef.current ?? undefined}
|
||||
shouldUpdatePosition
|
||||
target={scrollerRef.current}
|
||||
// Only used in Bootstrap 5. In Bootstrap 3 this is done with CSS.
|
||||
popperConfig={{
|
||||
modifiers: [
|
||||
{
|
||||
name: 'offset',
|
||||
options: {
|
||||
offset: [10, 10],
|
||||
},
|
||||
},
|
||||
],
|
||||
}}
|
||||
bs3Props={{ shouldUpdatePosition: true }}
|
||||
>
|
||||
<Popover
|
||||
id="popover-toolbar-overflow"
|
||||
arrowOffsetTop={10}
|
||||
<OLPopover
|
||||
id="popover-react-history-tutorial"
|
||||
bs3Props={{ arrowOffsetTop: 10 }}
|
||||
title={
|
||||
<span>
|
||||
{t('react_history_tutorial_title')}{' '}
|
||||
|
@ -212,23 +224,23 @@ function AllHistoryList() {
|
|||
<a href="https://www.overleaf.com/learn/latex/Using_the_History_feature" />, // eslint-disable-line jsx-a11y/anchor-has-content, react/jsx-key
|
||||
]}
|
||||
/>
|
||||
</Popover>
|
||||
</Overlay>
|
||||
</OLPopover>
|
||||
</OLOverlay>
|
||||
)
|
||||
} else if (showRestorePromo) {
|
||||
popover = (
|
||||
<Overlay
|
||||
placement="left"
|
||||
<OLOverlay
|
||||
placement="left-start"
|
||||
show={showRestorePromo}
|
||||
rootClose
|
||||
onHide={hidePopover}
|
||||
// using scrollerRef to position the popover in the middle of the viewport
|
||||
target={scrollerRef.current ?? undefined}
|
||||
shouldUpdatePosition
|
||||
target={scrollerRef.current}
|
||||
bs3Props={{ shouldUpdatePosition: true }}
|
||||
>
|
||||
<Popover
|
||||
id="popover-toolbar-overflow"
|
||||
arrowOffsetTop={10}
|
||||
<OLPopover
|
||||
id="popover-history-restore-promo"
|
||||
bs3Props={{ arrowOffsetTop: 10 }}
|
||||
title={
|
||||
<span>
|
||||
{t('history_restore_promo_title')}
|
||||
|
@ -255,8 +267,8 @@ function AllHistoryList() {
|
|||
/>,
|
||||
]}
|
||||
/>
|
||||
</Popover>
|
||||
</Overlay>
|
||||
</OLPopover>
|
||||
</OLOverlay>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -330,7 +342,9 @@ function AllHistoryList() {
|
|||
{showNonOwnerPaywall ? <NonOwnerPaywallPrompt /> : null}
|
||||
{updatesLoadingState === 'loadingInitial' ||
|
||||
updatesLoadingState === 'loadingUpdates' ? (
|
||||
<LoadingSpinner />
|
||||
<div className="history-all-versions-loading">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -1,9 +1,15 @@
|
|||
import { useRef, useEffect, ReactNode } from 'react'
|
||||
import { Dropdown } from 'react-bootstrap'
|
||||
import DropdownToggleWithTooltip from '../../../../../shared/components/dropdown/dropdown-toggle-with-tooltip'
|
||||
import DropdownMenuWithRef from '../../../../../shared/components/dropdown/dropdown-menu-with-ref'
|
||||
import React, { useRef, useEffect, ReactNode } from 'react'
|
||||
import { Dropdown as BS3Dropdown } from 'react-bootstrap'
|
||||
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
|
||||
import {
|
||||
Dropdown,
|
||||
DropdownMenu,
|
||||
} from '@/features/ui/components/bootstrap-5/dropdown-menu'
|
||||
import BS3DropdownToggleWithTooltip from '../../../../ui/components/bootstrap-3/dropdown-toggle-with-tooltip'
|
||||
import BS5DropdownToggleWithTooltip from '@/features/ui/components/bootstrap-5/dropdown-toggle-with-tooltip'
|
||||
import DropdownMenuWithRef from '../../../../ui/components/bootstrap-3/dropdown-menu-with-ref'
|
||||
|
||||
type DropdownMenuProps = {
|
||||
type ActionDropdownProps = {
|
||||
id: string
|
||||
children: React.ReactNode
|
||||
parentSelector?: string
|
||||
|
@ -13,7 +19,7 @@ type DropdownMenuProps = {
|
|||
setIsOpened: (isOpened: boolean) => void
|
||||
}
|
||||
|
||||
function ActionsDropdown({
|
||||
function BS3ActionsDropdown({
|
||||
id,
|
||||
children,
|
||||
parentSelector,
|
||||
|
@ -21,7 +27,7 @@ function ActionsDropdown({
|
|||
iconTag,
|
||||
setIsOpened,
|
||||
toolTipDescription,
|
||||
}: DropdownMenuProps) {
|
||||
}: ActionDropdownProps) {
|
||||
const menuRef = useRef<HTMLElement>()
|
||||
|
||||
// handle the placement of the dropdown above or below the toggle button
|
||||
|
@ -47,14 +53,14 @@ function ActionsDropdown({
|
|||
})
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
<BS3Dropdown
|
||||
id={`history-version-dropdown-${id}`}
|
||||
pullRight
|
||||
open={isOpened}
|
||||
onToggle={open => setIsOpened(open)}
|
||||
className="pull-right"
|
||||
>
|
||||
<DropdownToggleWithTooltip
|
||||
<BS3DropdownToggleWithTooltip
|
||||
bsRole="toggle"
|
||||
className="history-version-dropdown-menu-btn"
|
||||
isOpened={isOpened}
|
||||
|
@ -65,7 +71,7 @@ function ActionsDropdown({
|
|||
}}
|
||||
>
|
||||
{iconTag}
|
||||
</DropdownToggleWithTooltip>
|
||||
</BS3DropdownToggleWithTooltip>
|
||||
<DropdownMenuWithRef
|
||||
bsRole="menu"
|
||||
className="history-version-dropdown-menu"
|
||||
|
@ -73,8 +79,49 @@ function ActionsDropdown({
|
|||
>
|
||||
{children}
|
||||
</DropdownMenuWithRef>
|
||||
</BS3Dropdown>
|
||||
)
|
||||
}
|
||||
|
||||
function BS5ActionsDropdown({
|
||||
id,
|
||||
children,
|
||||
isOpened,
|
||||
iconTag,
|
||||
setIsOpened,
|
||||
toolTipDescription,
|
||||
}: Omit<ActionDropdownProps, 'parentSelector'>) {
|
||||
return (
|
||||
<Dropdown
|
||||
align="end"
|
||||
className="float-end"
|
||||
show={isOpened}
|
||||
onToggle={open => setIsOpened(open)}
|
||||
>
|
||||
<BS5DropdownToggleWithTooltip
|
||||
id={`history-version-dropdown-${id}`}
|
||||
className="history-version-dropdown-menu-btn"
|
||||
aria-label={toolTipDescription}
|
||||
toolTipDescription={toolTipDescription}
|
||||
overlayTriggerProps={{ placement: 'bottom' }}
|
||||
tooltipProps={{ hidden: isOpened }}
|
||||
>
|
||||
{iconTag}
|
||||
</BS5DropdownToggleWithTooltip>
|
||||
<DropdownMenu className="history-version-dropdown-menu">
|
||||
{children}
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
)
|
||||
}
|
||||
|
||||
function ActionsDropdown(props: ActionDropdownProps) {
|
||||
return (
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={<BS3ActionsDropdown {...props} />}
|
||||
bs5={<BS5ActionsDropdown {...props} />}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default ActionsDropdown
|
||||
|
|
|
@ -40,10 +40,7 @@ function CompareVersionDropdownContentAllHistory({
|
|||
closeDropdown={closeDropdown}
|
||||
text={t('history_compare_up_to_this_version')}
|
||||
icon={
|
||||
<MaterialIcon
|
||||
type="align_start"
|
||||
className="history-dropdown-icon p-1"
|
||||
/>
|
||||
<MaterialIcon type="align_start" className="history-dropdown-icon" />
|
||||
}
|
||||
/>
|
||||
<CompareDropDownItem
|
||||
|
@ -56,10 +53,7 @@ function CompareVersionDropdownContentAllHistory({
|
|||
closeDropdown={closeDropdown}
|
||||
text={t('history_compare_from_this_version')}
|
||||
icon={
|
||||
<MaterialIcon
|
||||
type="align_end"
|
||||
className="history-dropdown-icon p-1"
|
||||
/>
|
||||
<MaterialIcon type="align_end" className="history-dropdown-icon" />
|
||||
}
|
||||
/>
|
||||
</>
|
||||
|
@ -100,10 +94,7 @@ function CompareVersionDropdownContentLabelsList({
|
|||
closeDropdown={closeDropdownLabels}
|
||||
text={t('history_compare_up_to_this_version')}
|
||||
icon={
|
||||
<MaterialIcon
|
||||
type="align_start"
|
||||
className="history-dropdown-icon p-1"
|
||||
/>
|
||||
<MaterialIcon type="align_start" className="history-dropdown-icon" />
|
||||
}
|
||||
/>
|
||||
<CompareDropDownItem
|
||||
|
@ -116,10 +107,7 @@ function CompareVersionDropdownContentLabelsList({
|
|||
closeDropdown={closeDropdownLabels}
|
||||
text={t('history_compare_from_this_version')}
|
||||
icon={
|
||||
<MaterialIcon
|
||||
type="align_end"
|
||||
className="history-dropdown-icon p-1"
|
||||
/>
|
||||
<MaterialIcon type="align_end" className="history-dropdown-icon" />
|
||||
}
|
||||
/>
|
||||
</>
|
||||
|
|
|
@ -27,7 +27,7 @@ function CompareVersionDropdown({
|
|||
<MaterialIcon
|
||||
type="align_space_even"
|
||||
className="history-dropdown-icon"
|
||||
accessibilityLabel="compare drop down"
|
||||
accessibilityLabel={t('compare')}
|
||||
/>
|
||||
}
|
||||
>
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import ActionsDropdown from './actions-dropdown'
|
||||
import Icon from '../../../../../shared/components/icon'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
|
||||
|
||||
type HistoryDropdownProps = {
|
||||
children: React.ReactNode
|
||||
|
@ -23,7 +25,17 @@ function HistoryDropdown({
|
|||
toolTipDescription={t('more_actions')}
|
||||
setIsOpened={setIsOpened}
|
||||
iconTag={
|
||||
<Icon type="ellipsis-v" accessibilityLabel={t('more_actions')} />
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={
|
||||
<Icon type="ellipsis-v" accessibilityLabel={t('more_actions')} />
|
||||
}
|
||||
bs5={
|
||||
<MaterialIcon
|
||||
type="more_vert"
|
||||
accessibilityLabel={t('more_actions')}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
parentSelector="[data-history-version-list-container]"
|
||||
>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { MenuItem } from 'react-bootstrap'
|
||||
import Icon from '../../../../../../shared/components/icon'
|
||||
import OLDropdownMenuItem from '@/features/ui/components/ol/ol-dropdown-menu-item'
|
||||
import OLTagIcon from '@/features/ui/components/ol/icons/ol-tag-icon'
|
||||
import AddLabelModal from '../../add-label-modal'
|
||||
|
||||
type DownloadProps = {
|
||||
|
@ -26,9 +26,15 @@ function AddLabel({
|
|||
|
||||
return (
|
||||
<>
|
||||
<MenuItem onClick={handleClick} {...props}>
|
||||
<Icon type="tag" fw /> {t('history_label_this_version')}
|
||||
</MenuItem>
|
||||
<OLDropdownMenuItem
|
||||
onClick={handleClick}
|
||||
leadingIcon={<OLTagIcon />}
|
||||
as="button"
|
||||
className="dropdown-item-material-icon-small"
|
||||
{...props}
|
||||
>
|
||||
{t('history_label_this_version')}
|
||||
</OLDropdownMenuItem>
|
||||
<AddLabelModal
|
||||
show={showModal}
|
||||
setShow={setShowModal}
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import Icon from '../../../../../../shared/components/icon'
|
||||
import { useHistoryContext } from '../../../../context/history-context'
|
||||
import { UpdateRange } from '../../../../services/types/update'
|
||||
import { ReactNode } from 'react'
|
||||
import { Button, MenuItem } from 'react-bootstrap'
|
||||
import OLDropdownMenuItem from '@/features/ui/components/ol/ol-dropdown-menu-item'
|
||||
|
||||
type CompareProps = {
|
||||
comparisonRange: UpdateRange
|
||||
|
@ -15,12 +14,12 @@ function CompareDropDownItem({
|
|||
comparisonRange,
|
||||
text,
|
||||
closeDropdown,
|
||||
icon = <Icon type="exchange" fw />,
|
||||
icon,
|
||||
...props
|
||||
}: CompareProps) {
|
||||
const { setSelection } = useHistoryContext()
|
||||
|
||||
const handleCompareVersion = (e: React.MouseEvent<Button>) => {
|
||||
const handleCompareVersion = (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
closeDropdown()
|
||||
|
||||
|
@ -33,10 +32,15 @@ function CompareDropDownItem({
|
|||
}
|
||||
|
||||
return (
|
||||
<MenuItem {...props} onClick={handleCompareVersion}>
|
||||
{icon}
|
||||
<span className="">{text}</span>
|
||||
</MenuItem>
|
||||
<OLDropdownMenuItem
|
||||
{...props}
|
||||
leadingIcon={icon}
|
||||
as="button"
|
||||
onClick={handleCompareVersion}
|
||||
className="dropdown-item-material-icon-small"
|
||||
>
|
||||
{text}
|
||||
</OLDropdownMenuItem>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
import Icon from '../../../../../../shared/components/icon'
|
||||
import { useHistoryContext } from '../../../../context/history-context'
|
||||
import { UpdateRange } from '../../../../services/types/update'
|
||||
import { ReactNode } from 'react'
|
||||
import Tooltip from '../../../../../../shared/components/tooltip'
|
||||
import { Button } from 'react-bootstrap'
|
||||
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
|
||||
import { bsVersion } from '@/features/utils/bootstrap-5'
|
||||
|
||||
type CompareProps = {
|
||||
comparisonRange: UpdateRange
|
||||
icon?: ReactNode
|
||||
icon: ReactNode
|
||||
toolTipDescription?: string
|
||||
closeDropdown: () => void
|
||||
}
|
||||
|
@ -16,12 +15,11 @@ function Compare({
|
|||
comparisonRange,
|
||||
closeDropdown,
|
||||
toolTipDescription,
|
||||
icon = <Icon type="exchange" fw />,
|
||||
...props
|
||||
icon,
|
||||
}: CompareProps) {
|
||||
const { setSelection } = useHistoryContext()
|
||||
|
||||
const handleCompareVersion = (e: React.MouseEvent<Button>) => {
|
||||
const handleCompareVersion = (e: { stopPropagation: () => void }) => {
|
||||
e.stopPropagation()
|
||||
closeDropdown()
|
||||
|
||||
|
@ -34,20 +32,18 @@ function Compare({
|
|||
}
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
<OLTooltip
|
||||
description={toolTipDescription}
|
||||
id="compare-btn"
|
||||
overlayProps={{ placement: 'left' }}
|
||||
>
|
||||
<Button
|
||||
bsStyle={null}
|
||||
className="history-compare-btn"
|
||||
onClick={handleCompareVersion}
|
||||
>
|
||||
<span className="sr-only">{toolTipDescription}</span>
|
||||
<button className="history-compare-btn" onClick={handleCompareVersion}>
|
||||
<span className={bsVersion({ bs3: 'sr-only', bs5: 'visually-hidden' })}>
|
||||
{toolTipDescription}
|
||||
</span>
|
||||
{icon}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</button>
|
||||
</OLTooltip>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import { MenuItem } from 'react-bootstrap'
|
||||
import OLDropdownMenuItem from '@/features/ui/components/ol/ol-dropdown-menu-item'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
|
||||
import Icon from '../../../../../../shared/components/icon'
|
||||
|
||||
type DownloadProps = {
|
||||
|
@ -17,15 +19,21 @@ function Download({
|
|||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
<OLDropdownMenuItem
|
||||
href={`/project/${projectId}/version/${version}/zip`}
|
||||
download={`${projectId}_v${version}.zip`}
|
||||
rel="noreferrer"
|
||||
onClick={closeDropdown}
|
||||
leadingIcon={
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={<Icon type="cloud-download" fw />}
|
||||
bs5={<MaterialIcon type="download" />}
|
||||
/>
|
||||
}
|
||||
{...props}
|
||||
>
|
||||
<Icon type="cloud-download" fw /> {t('history_download_this_version')}
|
||||
</MenuItem>
|
||||
{t('history_download_this_version')}
|
||||
</OLDropdownMenuItem>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
import Icon from '@/shared/components/icon'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { MenuItem } from 'react-bootstrap'
|
||||
import OLDropdownMenuItem from '@/features/ui/components/ol/ol-dropdown-menu-item'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RestoreProjectModal } from '../../../diff-view/modals/restore-project-modal'
|
||||
import { useSplitTestContext } from '@/shared/context/split-test-context'
|
||||
import { useRestoreProject } from '@/features/history/context/hooks/use-restore-project'
|
||||
import withErrorBoundary from '@/infrastructure/error-boundary'
|
||||
import { RestoreProjectErrorModal } from '../../../diff-view/modals/restore-project-error-modal'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
|
||||
|
||||
type RestoreProjectProps = {
|
||||
projectId: string
|
||||
|
@ -44,9 +46,18 @@ const RestoreProject = ({
|
|||
|
||||
return (
|
||||
<>
|
||||
<MenuItem onClick={handleClick}>
|
||||
<Icon type="undo" fw /> {t('restore_project_to_this_version')}
|
||||
</MenuItem>
|
||||
<OLDropdownMenuItem
|
||||
as="button"
|
||||
leadingIcon={
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={<Icon type="undo" fw />}
|
||||
bs5={<MaterialIcon type="undo" />}
|
||||
/>
|
||||
}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{t('restore_project_to_this_version')}
|
||||
</OLDropdownMenuItem>
|
||||
<RestoreProjectModal
|
||||
setShow={setShowModal}
|
||||
show={showModal}
|
||||
|
|
|
@ -35,7 +35,7 @@ function HistoryVersionDetails({
|
|||
// 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 clearfix', {
|
||||
className={classnames('history-version-details', {
|
||||
'history-version-selected':
|
||||
selectionState === 'upperSelected' ||
|
||||
selectionState === 'lowerSelected' ||
|
||||
|
|
|
@ -22,6 +22,7 @@ import { CompareVersionDropdownContentAllHistory } from './dropdown/compare-vers
|
|||
import FileRestoreChange from './file-restore-change'
|
||||
import HistoryResyncChange from './history-resync-change'
|
||||
import ProjectRestoreChange from './project-restore-change'
|
||||
import { bsVersion } from '@/features/utils/bootstrap-5'
|
||||
|
||||
type HistoryVersionProps = {
|
||||
update: LoadedUpdate
|
||||
|
@ -123,7 +124,10 @@ function HistoryVersion({
|
|||
)}
|
||||
|
||||
{selectionState !== 'selected' && !faded ? (
|
||||
<div data-testid="compare-icon-version" className="pull-right">
|
||||
<div
|
||||
data-testid="compare-icon-version"
|
||||
className={bsVersion({ bs3: 'pull-right', bs5: 'float-end' })}
|
||||
>
|
||||
{selectionState !== 'withinSelected' ? (
|
||||
<CompareItems
|
||||
updateRange={updateRange}
|
||||
|
|
|
@ -13,6 +13,7 @@ import { ItemSelectionState } from '../../utils/history-details'
|
|||
import CompareVersionDropdown from './dropdown/compare-version-dropdown'
|
||||
import { CompareVersionDropdownContentLabelsList } from './dropdown/compare-version-dropdown-content'
|
||||
import HistoryDropdownContent from '@/features/history/components/change-list/dropdown/history-dropdown-content'
|
||||
import { bsVersion } from '@/features/utils/bootstrap-5'
|
||||
|
||||
type LabelListItemProps = {
|
||||
version: Version
|
||||
|
@ -96,7 +97,10 @@ function LabelListItem({
|
|||
) : null}
|
||||
</HistoryDropdown>
|
||||
{selectionState !== 'selected' ? (
|
||||
<div data-testid="compare-icon-version" className="pull-right">
|
||||
<div
|
||||
data-testid="compare-icon-version"
|
||||
className={bsVersion({ bs3: 'pull-right', bs5: 'float-end' })}
|
||||
>
|
||||
{selectionState !== 'withinSelected' ? (
|
||||
<CompareItems
|
||||
updateRange={updateRange}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import { Alert } from 'react-bootstrap'
|
||||
import OLNotification from '@/features/ui/components/ol/ol-notification'
|
||||
|
||||
// Using this workaround due to inconsistent and improper error responses from the server
|
||||
type ModalErrorProps = {
|
||||
|
@ -15,10 +15,22 @@ function ModalError({ error }: ModalErrorProps) {
|
|||
const { t } = useTranslation()
|
||||
|
||||
if (error.response?.status === 400 && error.data?.message) {
|
||||
return <Alert bsStyle="danger">{error.data.message}</Alert>
|
||||
return (
|
||||
<OLNotification
|
||||
type="error"
|
||||
content={error.data.message}
|
||||
className="row-spaced-small"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return <Alert bsStyle="danger">{t('generic_something_went_wrong')}</Alert>
|
||||
return (
|
||||
<OLNotification
|
||||
type="error"
|
||||
content={t('generic_something_went_wrong')}
|
||||
className="row-spaced-small"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModalError
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
import { useState } from 'react'
|
||||
import { forwardRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Modal } from 'react-bootstrap'
|
||||
import Icon from '../../../../shared/components/icon'
|
||||
import Tooltip from '../../../../shared/components/tooltip'
|
||||
import AccessibleModal from '../../../../shared/components/accessible-modal'
|
||||
import OLModal, {
|
||||
OLModalBody,
|
||||
OLModalFooter,
|
||||
OLModalHeader,
|
||||
OLModalTitle,
|
||||
} from '@/features/ui/components/ol/ol-modal'
|
||||
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
|
||||
import ModalError from './modal-error'
|
||||
import useAbortController from '../../../../shared/hooks/use-abort-controller'
|
||||
import useAsync from '../../../../shared/hooks/use-async'
|
||||
|
@ -15,118 +18,127 @@ import { LoadedLabel } from '../../services/types/label'
|
|||
import { debugConsole } from '@/utils/debugging'
|
||||
import { formatTimeBasedOnYear } from '@/features/utils/format-date'
|
||||
import { useEditorContext } from '@/shared/context/editor-context'
|
||||
import Tag from '@/shared/components/tag'
|
||||
import OLTag from '@/features/ui/components/ol/ol-tag'
|
||||
import OLButton from '@/features/ui/components/ol/ol-button'
|
||||
import OLTagIcon from '@/features/ui/components/ol/icons/ol-tag-icon'
|
||||
|
||||
type TagProps = {
|
||||
label: LoadedLabel
|
||||
currentUserId: string
|
||||
}
|
||||
|
||||
function ChangeTag({ label, currentUserId, ...props }: TagProps) {
|
||||
const { isProjectOwner } = useEditorContext()
|
||||
const ChangeTag = forwardRef<HTMLElement, TagProps>(
|
||||
({ label, currentUserId, ...props }: TagProps, ref) => {
|
||||
const { isProjectOwner } = useEditorContext()
|
||||
|
||||
const { t } = useTranslation()
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false)
|
||||
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
|
||||
: null
|
||||
const { t } = useTranslation()
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false)
|
||||
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
|
||||
: null
|
||||
|
||||
const showConfirmationModal = (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
setShowDeleteModal(true)
|
||||
}
|
||||
|
||||
const handleModalExited = () => {
|
||||
if (!isSuccess) return
|
||||
|
||||
if (!isPseudoCurrentStateLabel) {
|
||||
removeUpdateLabel(label)
|
||||
const showConfirmationModal = (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
setShowDeleteModal(true)
|
||||
}
|
||||
|
||||
reset()
|
||||
}
|
||||
const handleModalExited = () => {
|
||||
if (!isSuccess) return
|
||||
|
||||
const localDeleteHandler = () => {
|
||||
runAsync(deleteLabel(projectId, label.id, signal))
|
||||
.then(() => setShowDeleteModal(false))
|
||||
.catch(debugConsole.error)
|
||||
}
|
||||
if (!isPseudoCurrentStateLabel) {
|
||||
removeUpdateLabel(label)
|
||||
}
|
||||
|
||||
const responseError = error as unknown as {
|
||||
response: Response
|
||||
data?: {
|
||||
message?: string
|
||||
reset()
|
||||
}
|
||||
}
|
||||
|
||||
const showCloseButton = Boolean(
|
||||
(isOwnedByCurrentUser || isProjectOwner) && !isPseudoCurrentStateLabel
|
||||
)
|
||||
const localDeleteHandler = () => {
|
||||
runAsync(deleteLabel(projectId, label.id, signal))
|
||||
.then(() => setShowDeleteModal(false))
|
||||
.catch(debugConsole.error)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tag
|
||||
prepend={<Icon type="tag" fw />}
|
||||
closeBtnProps={
|
||||
showCloseButton
|
||||
? { 'aria-label': t('delete'), onClick: showConfirmationModal }
|
||||
: undefined
|
||||
}
|
||||
className="history-version-badge"
|
||||
data-testid="history-version-badge"
|
||||
{...props}
|
||||
>
|
||||
{isPseudoCurrentStateLabel
|
||||
? t('history_label_project_current_state')
|
||||
: label.comment}
|
||||
</Tag>
|
||||
{!isPseudoCurrentStateLabel && (
|
||||
<AccessibleModal
|
||||
show={showDeleteModal}
|
||||
onExited={handleModalExited}
|
||||
onHide={() => setShowDeleteModal(false)}
|
||||
id="delete-history-label"
|
||||
const responseError = error as unknown as {
|
||||
response: Response
|
||||
data?: {
|
||||
message?: string
|
||||
}
|
||||
}
|
||||
|
||||
const showCloseButton = Boolean(
|
||||
(isOwnedByCurrentUser || isProjectOwner) && !isPseudoCurrentStateLabel
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<OLTag
|
||||
ref={ref}
|
||||
prepend={<OLTagIcon />}
|
||||
closeBtnProps={
|
||||
showCloseButton
|
||||
? { 'aria-label': t('delete'), onClick: showConfirmationModal }
|
||||
: undefined
|
||||
}
|
||||
className="history-version-badge"
|
||||
data-testid="history-version-badge"
|
||||
{...props}
|
||||
>
|
||||
<Modal.Header>
|
||||
<Modal.Title>{t('history_delete_label')}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
{isError && <ModalError error={responseError} />}
|
||||
<p>
|
||||
{t('history_are_you_sure_delete_label')}
|
||||
<strong>"{label.comment}"</strong>?
|
||||
</p>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
disabled={isLoading}
|
||||
onClick={() => setShowDeleteModal(false)}
|
||||
>
|
||||
{t('cancel')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-danger"
|
||||
disabled={isLoading}
|
||||
onClick={localDeleteHandler}
|
||||
>
|
||||
{isLoading
|
||||
? t('history_deleting_label')
|
||||
: t('history_delete_label')}
|
||||
</button>
|
||||
</Modal.Footer>
|
||||
</AccessibleModal>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
{isPseudoCurrentStateLabel
|
||||
? t('history_label_project_current_state')
|
||||
: label.comment}
|
||||
</OLTag>
|
||||
{!isPseudoCurrentStateLabel && (
|
||||
<OLModal
|
||||
show={showDeleteModal}
|
||||
onExited={handleModalExited}
|
||||
onHide={() => setShowDeleteModal(false)}
|
||||
id="delete-history-label"
|
||||
>
|
||||
<OLModalHeader>
|
||||
<OLModalTitle>{t('history_delete_label')}</OLModalTitle>
|
||||
</OLModalHeader>
|
||||
<OLModalBody>
|
||||
{isError && <ModalError error={responseError} />}
|
||||
<p>
|
||||
{t('history_are_you_sure_delete_label')}
|
||||
<strong>"{label.comment}"</strong>?
|
||||
</p>
|
||||
</OLModalBody>
|
||||
<OLModalFooter>
|
||||
<OLButton
|
||||
variant="secondary"
|
||||
disabled={isLoading}
|
||||
onClick={() => setShowDeleteModal(false)}
|
||||
>
|
||||
{t('cancel')}
|
||||
</OLButton>
|
||||
<OLButton
|
||||
variant="danger"
|
||||
disabled={isLoading}
|
||||
isLoading={isLoading}
|
||||
onClick={localDeleteHandler}
|
||||
bs3Props={{
|
||||
loading: isLoading
|
||||
? t('history_deleting_label')
|
||||
: t('history_delete_label'),
|
||||
}}
|
||||
>
|
||||
{t('history_delete_label')}
|
||||
</OLButton>
|
||||
</OLModalFooter>
|
||||
</OLModal>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
ChangeTag.displayName = 'ChangeTag'
|
||||
|
||||
type LabelBadgesProps = {
|
||||
showTooltip: boolean
|
||||
|
@ -145,13 +157,14 @@ function TagTooltip({ label, currentUserId, showTooltip }: LabelBadgesProps) {
|
|||
? currentLabelData.user_display_name
|
||||
: t('anonymous')
|
||||
|
||||
return showTooltip && !isPseudoCurrentStateLabel ? (
|
||||
<Tooltip
|
||||
return !isPseudoCurrentStateLabel ? (
|
||||
<OLTooltip
|
||||
description={
|
||||
<div className="history-version-label-tooltip">
|
||||
<div className="history-version-label-tooltip-row">
|
||||
<b className="history-version-label-tooltip-row-comment">
|
||||
<Icon type="tag" fw />
|
||||
<OLTagIcon />
|
||||
|
||||
{label.comment}
|
||||
</b>
|
||||
</div>
|
||||
|
@ -165,9 +178,10 @@ function TagTooltip({ label, currentUserId, showTooltip }: LabelBadgesProps) {
|
|||
}
|
||||
id={label.id}
|
||||
overlayProps={{ placement: 'left' }}
|
||||
hidden={!showTooltip}
|
||||
>
|
||||
<ChangeTag label={label} currentUserId={currentUserId} />
|
||||
</Tooltip>
|
||||
</OLTooltip>
|
||||
) : (
|
||||
<ChangeTag label={label} currentUserId={currentUserId} />
|
||||
)
|
||||
|
|
|
@ -1,4 +1,10 @@
|
|||
import { Button, Modal } from 'react-bootstrap'
|
||||
import OLModal, {
|
||||
OLModalBody,
|
||||
OLModalFooter,
|
||||
OLModalHeader,
|
||||
OLModalTitle,
|
||||
} from '@/features/ui/components/ol/ol-modal'
|
||||
import OLButton from '@/features/ui/components/ol/ol-button'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export function RestoreProjectErrorModal({
|
||||
|
@ -9,26 +15,22 @@ export function RestoreProjectErrorModal({
|
|||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<Modal show onHide={resetErrorBoundary}>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>
|
||||
<OLModal show onHide={resetErrorBoundary}>
|
||||
<OLModalHeader closeButton>
|
||||
<OLModalTitle>
|
||||
{t('an_error_occured_while_restoring_project')}
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
</OLModalTitle>
|
||||
</OLModalHeader>
|
||||
<OLModalBody>
|
||||
{t(
|
||||
'there_was_a_problem_restoring_the_project_please_try_again_in_a_few_moments_or_contact_us'
|
||||
)}
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button
|
||||
bsStyle={null}
|
||||
className="btn-secondary"
|
||||
onClick={resetErrorBoundary}
|
||||
>
|
||||
</OLModalBody>
|
||||
<OLModalFooter>
|
||||
<OLButton variant="secondary" onClick={resetErrorBoundary}>
|
||||
{t('close')}
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
</OLButton>
|
||||
</OLModalFooter>
|
||||
</OLModal>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
import AccessibleModal from '@/shared/components/accessible-modal'
|
||||
import OLModal, {
|
||||
OLModalBody,
|
||||
OLModalFooter,
|
||||
OLModalHeader,
|
||||
OLModalTitle,
|
||||
} from '@/features/ui/components/ol/ol-modal'
|
||||
import { formatDate } from '@/utils/dates'
|
||||
import { useCallback } from 'react'
|
||||
import { Button, Modal } from 'react-bootstrap'
|
||||
import OLButton from '@/features/ui/components/ol/ol-button'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type RestoreProjectModalProps = {
|
||||
|
@ -26,35 +31,31 @@ export const RestoreProjectModal = ({
|
|||
}, [setShow])
|
||||
|
||||
return (
|
||||
<AccessibleModal onHide={() => setShow(false)} show={show}>
|
||||
<Modal.Header>
|
||||
<Modal.Title>{t('restore_this_version')}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<OLModal onHide={() => setShow(false)} show={show}>
|
||||
<OLModalHeader>
|
||||
<OLModalTitle>{t('restore_this_version')}</OLModalTitle>
|
||||
</OLModalHeader>
|
||||
<OLModalBody>
|
||||
<p>
|
||||
{t('your_current_project_will_revert_to_the_version_from_time', {
|
||||
timestamp: formatDate(endTimestamp),
|
||||
})}
|
||||
</p>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button
|
||||
className="btn btn-secondary"
|
||||
bsStyle={null}
|
||||
onClick={onCancel}
|
||||
disabled={isRestoring}
|
||||
>
|
||||
</OLModalBody>
|
||||
<OLModalFooter>
|
||||
<OLButton variant="secondary" onClick={onCancel} disabled={isRestoring}>
|
||||
{t('cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
className="btn btn-primary"
|
||||
bsStyle={null}
|
||||
</OLButton>
|
||||
<OLButton
|
||||
variant="primary"
|
||||
onClick={onRestore}
|
||||
disabled={isRestoring}
|
||||
isLoading={isRestoring}
|
||||
bs3Props={{ loading: isRestoring ? t('restoring') : t('restore') }}
|
||||
>
|
||||
{isRestoring ? t('restoring') : t('restore')}
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</AccessibleModal>
|
||||
{t('restore')}
|
||||
</OLButton>
|
||||
</OLModalFooter>
|
||||
</OLModal>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { memo, useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import OlDropdownMenuItem from '@/features/ui/components/ol/ol-dropdown-menu-item'
|
||||
import OLDropdownMenuItem from '@/features/ui/components/ol/ol-dropdown-menu-item'
|
||||
import CloneProjectModal from '../../../../../clone-project-modal/components/clone-project-modal'
|
||||
import useIsMounted from '../../../../../../shared/hooks/use-is-mounted'
|
||||
import { useProjectListContext } from '../../../../context/project-list-context'
|
||||
|
@ -64,9 +64,9 @@ function CopyProjectMenuItem() {
|
|||
|
||||
return (
|
||||
<>
|
||||
<OlDropdownMenuItem onClick={handleOpenModal} as="button" tabIndex={-1}>
|
||||
<OLDropdownMenuItem onClick={handleOpenModal} as="button" tabIndex={-1}>
|
||||
{t('make_a_copy')}
|
||||
</OlDropdownMenuItem>
|
||||
</OLDropdownMenuItem>
|
||||
<CloneProjectModal
|
||||
show={showModal}
|
||||
handleHide={handleCloseModal}
|
||||
|
|
|
@ -2,7 +2,7 @@ import { memo, useCallback, useState } from 'react'
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import { useProjectListContext } from '../../../../context/project-list-context'
|
||||
import useIsMounted from '../../../../../../shared/hooks/use-is-mounted'
|
||||
import OlDropdownMenuItem from '@/features/ui/components/ol/ol-dropdown-menu-item'
|
||||
import OLDropdownMenuItem from '@/features/ui/components/ol/ol-dropdown-menu-item'
|
||||
import RenameProjectModal from '../../../modals/rename-project-modal'
|
||||
|
||||
function RenameProjectMenuItem() {
|
||||
|
@ -34,9 +34,9 @@ function RenameProjectMenuItem() {
|
|||
|
||||
return (
|
||||
<>
|
||||
<OlDropdownMenuItem onClick={handleOpenModal} as="button" tabIndex={-1}>
|
||||
<OLDropdownMenuItem onClick={handleOpenModal} as="button" tabIndex={-1}>
|
||||
{t('rename')}
|
||||
</OlDropdownMenuItem>
|
||||
</OLDropdownMenuItem>
|
||||
<RenameProjectModal
|
||||
handleCloseModal={handleCloseModal}
|
||||
showModal={showModal}
|
||||
|
|
|
@ -2,7 +2,7 @@ import { forwardRef, SyntheticEvent } from 'react'
|
|||
import classnames from 'classnames'
|
||||
import RootCloseWrapper from 'react-overlays/lib/RootCloseWrapper'
|
||||
import { DropdownProps } from 'react-bootstrap'
|
||||
import { MergeAndOverride } from '../../../../../types/utils'
|
||||
import { MergeAndOverride } from '../../../../../../types/utils'
|
||||
|
||||
type DropdownMenuWithRefProps = MergeAndOverride<
|
||||
Pick<DropdownProps, 'bsClass' | 'open' | 'pullRight' | 'onClose'>,
|
|
@ -1,8 +1,8 @@
|
|||
import { forwardRef } from 'react'
|
||||
import Tooltip from '../tooltip'
|
||||
import Tooltip from '../../../../shared/components/tooltip'
|
||||
import classnames from 'classnames'
|
||||
import { DropdownProps } from 'react-bootstrap'
|
||||
import { MergeAndOverride } from '../../../../../types/utils'
|
||||
import { MergeAndOverride } from '../../../../../../types/utils'
|
||||
|
||||
type CustomToggleProps = MergeAndOverride<
|
||||
Pick<DropdownProps, 'bsClass' | 'open'>,
|
|
@ -0,0 +1,52 @@
|
|||
import { ReactNode, forwardRef } from 'react'
|
||||
import { BsPrefixRefForwardingComponent } from 'react-bootstrap-5/helpers'
|
||||
import type { DropdownToggleProps } from '@/features/ui/components/types/dropdown-menu-props'
|
||||
import {
|
||||
DropdownToggle as BS5DropdownToggle,
|
||||
OverlayTrigger,
|
||||
OverlayTriggerProps,
|
||||
Tooltip,
|
||||
} from 'react-bootstrap-5'
|
||||
import type { MergeAndOverride } from '../../../../../../types/utils'
|
||||
|
||||
type DropdownToggleWithTooltipProps = MergeAndOverride<
|
||||
DropdownToggleProps,
|
||||
{
|
||||
children: ReactNode
|
||||
overlayTriggerProps?: Omit<OverlayTriggerProps, 'overlay' | 'children'>
|
||||
toolTipDescription: string
|
||||
tooltipProps?: Omit<React.ComponentProps<typeof Tooltip>, 'children'>
|
||||
'aria-label'?: string
|
||||
}
|
||||
>
|
||||
const DropdownToggleWithTooltip = forwardRef<
|
||||
BsPrefixRefForwardingComponent<'button', DropdownToggleProps>,
|
||||
DropdownToggleWithTooltipProps
|
||||
>(
|
||||
(
|
||||
{
|
||||
children,
|
||||
toolTipDescription,
|
||||
overlayTriggerProps,
|
||||
tooltipProps,
|
||||
id,
|
||||
...toggleProps
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
return (
|
||||
<OverlayTrigger
|
||||
overlay={<Tooltip {...tooltipProps}>{toolTipDescription}</Tooltip>}
|
||||
{...overlayTriggerProps}
|
||||
>
|
||||
<BS5DropdownToggle {...toggleProps} ref={ref}>
|
||||
{children}
|
||||
</BS5DropdownToggle>
|
||||
</OverlayTrigger>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
DropdownToggleWithTooltip.displayName = 'DropdownToggleWithTooltip'
|
||||
|
||||
export default DropdownToggleWithTooltip
|
|
@ -3,6 +3,7 @@ import { Badge, BadgeProps } from 'react-bootstrap-5'
|
|||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
import { MergeAndOverride } from '../../../../../../types/utils'
|
||||
import classnames from 'classnames'
|
||||
import { forwardRef } from 'react'
|
||||
|
||||
type TagProps = MergeAndOverride<
|
||||
BadgeProps,
|
||||
|
@ -13,50 +14,55 @@ type TagProps = MergeAndOverride<
|
|||
}
|
||||
>
|
||||
|
||||
function Tag({
|
||||
prepend,
|
||||
children,
|
||||
contentProps,
|
||||
closeBtnProps,
|
||||
className,
|
||||
...rest
|
||||
}: TagProps) {
|
||||
const { t } = useTranslation()
|
||||
const Tag = forwardRef<HTMLElement, TagProps>(
|
||||
(
|
||||
{ prepend, children, contentProps, closeBtnProps, className, ...rest },
|
||||
ref
|
||||
) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const content = (
|
||||
<>
|
||||
{prepend && <span className="badge-prepend">{prepend}</span>}
|
||||
<span className="badge-content">{children}</span>
|
||||
</>
|
||||
)
|
||||
const content = (
|
||||
<>
|
||||
{prepend && <span className="badge-prepend">{prepend}</span>}
|
||||
<span className="badge-content">{children}</span>
|
||||
</>
|
||||
)
|
||||
|
||||
return (
|
||||
<Badge bg="light" className={classnames('badge-tag', className)} {...rest}>
|
||||
{contentProps?.onClick ? (
|
||||
<button
|
||||
type="button"
|
||||
className="badge-tag-content badge-tag-content-btn"
|
||||
{...contentProps}
|
||||
>
|
||||
{content}
|
||||
</button>
|
||||
) : (
|
||||
<span className="badge-tag-content" {...contentProps}>
|
||||
{content}
|
||||
</span>
|
||||
)}
|
||||
{closeBtnProps && (
|
||||
<button
|
||||
type="button"
|
||||
className="badge-close"
|
||||
aria-label={t('remove_tag', { tagName: children })}
|
||||
{...closeBtnProps}
|
||||
>
|
||||
<MaterialIcon className="badge-close-icon" type="close" />
|
||||
</button>
|
||||
)}
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Badge
|
||||
ref={ref}
|
||||
bg="light"
|
||||
className={classnames('badge-tag', className)}
|
||||
{...rest}
|
||||
>
|
||||
{contentProps?.onClick ? (
|
||||
<button
|
||||
type="button"
|
||||
className="badge-tag-content badge-tag-content-btn"
|
||||
{...contentProps}
|
||||
>
|
||||
{content}
|
||||
</button>
|
||||
) : (
|
||||
<span className="badge-tag-content" {...contentProps}>
|
||||
{content}
|
||||
</span>
|
||||
)}
|
||||
{closeBtnProps && (
|
||||
<button
|
||||
type="button"
|
||||
className="badge-close"
|
||||
aria-label={t('remove_tag', { tagName: children })}
|
||||
{...closeBtnProps}
|
||||
>
|
||||
<MaterialIcon className="badge-close-icon" type="close" />
|
||||
</button>
|
||||
)}
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
Tag.displayName = 'Tag'
|
||||
|
||||
export default Tag
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
import Icon from '@/shared/components/icon'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
|
||||
|
||||
export default function OLTagIcon() {
|
||||
return (
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={<Icon type="tag" fw />}
|
||||
bs5={<MaterialIcon type="sell" />}
|
||||
/>
|
||||
)
|
||||
}
|
|
@ -3,16 +3,26 @@ import { DropdownItem } from '@/features/ui/components/bootstrap-5/dropdown-menu
|
|||
import { DropdownItemProps } from '@/features/ui/components/types/dropdown-menu-props'
|
||||
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
|
||||
|
||||
type OlDropdownMenuItemProps = DropdownItemProps & {
|
||||
type OLDropdownMenuItemProps = DropdownItemProps & {
|
||||
bs3Props?: MenuItemProps
|
||||
}
|
||||
|
||||
function OlDropdownMenuItem(props: OlDropdownMenuItemProps) {
|
||||
function OLDropdownMenuItem(props: OLDropdownMenuItemProps) {
|
||||
const { bs3Props, ...rest } = props
|
||||
|
||||
const bs3MenuItemProps: MenuItemProps = {
|
||||
children: rest.children,
|
||||
children: rest.leadingIcon ? (
|
||||
<>
|
||||
{rest.leadingIcon}
|
||||
|
||||
{rest.children}
|
||||
</>
|
||||
) : (
|
||||
rest.children
|
||||
),
|
||||
onClick: rest.onClick,
|
||||
href: rest.href,
|
||||
download: rest.download,
|
||||
...bs3Props,
|
||||
}
|
||||
|
||||
|
@ -24,4 +34,4 @@ function OlDropdownMenuItem(props: OlDropdownMenuItemProps) {
|
|||
)
|
||||
}
|
||||
|
||||
export default OlDropdownMenuItem
|
||||
export default OLDropdownMenuItem
|
||||
|
|
|
@ -47,6 +47,7 @@ export default function OLModal({ children, ...props }: OLModalProps) {
|
|||
bsSize: bs5Props.size,
|
||||
show: bs5Props.show,
|
||||
onHide: bs5Props.onHide,
|
||||
onExited: bs5Props.onExited,
|
||||
backdrop: bs5Props.backdrop,
|
||||
animation: bs5Props.animation,
|
||||
id: bs5Props.id,
|
||||
|
@ -86,10 +87,15 @@ export function OLModalTitle({ children, ...props }: OLModalTitleProps) {
|
|||
const bs3ModalProps: BS3ModalTitleProps = {
|
||||
componentClass: bs5Props.as,
|
||||
}
|
||||
|
||||
return (
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={<BS3Modal.Title {...bs3ModalProps}>{children}</BS3Modal.Title>}
|
||||
bs5={<BS5Modal.Title {...bs5Props}>{children}</BS5Modal.Title>}
|
||||
bs5={
|
||||
<BS5Modal.Title as="h2" {...bs5Props}>
|
||||
{children}
|
||||
</BS5Modal.Title>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -40,8 +40,9 @@ function OLOverlay(props: OLOverlayProps) {
|
|||
>
|
||||
|
||||
for (const placement of bs3PlacementOptions) {
|
||||
if (placement === bs5Props.placement) {
|
||||
bs3OverlayProps.placement = bs5Props.placement
|
||||
// BS5 has more placement options than BS3, such as "left-start", so these are mapped to "left" etc.
|
||||
if (bs5Props.placement.startsWith(placement)) {
|
||||
bs3OverlayProps.placement = placement
|
||||
break
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ import {
|
|||
} from 'react-bootstrap'
|
||||
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
|
||||
|
||||
type OLPopoverProps = PopoverProps & {
|
||||
type OLPopoverProps = Omit<PopoverProps, 'title'> & {
|
||||
title?: React.ReactNode
|
||||
bs3Props?: BS3PopoverProps
|
||||
}
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
|
||||
import Icon from '@/shared/components/icon'
|
||||
import { Spinner } from 'react-bootstrap-5'
|
||||
import classNames from 'classnames'
|
||||
|
||||
export type OLSpinnerSize = 'sm' | 'lg'
|
||||
|
||||
function OLSpinner({ size = 'sm' }: { size: OLSpinnerSize }) {
|
||||
return (
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={
|
||||
<Icon
|
||||
type="refresh"
|
||||
fw
|
||||
spin
|
||||
className={classNames({ 'fa-2x': size === 'lg' })}
|
||||
/>
|
||||
}
|
||||
bs5={
|
||||
<Spinner
|
||||
size={size === 'sm' ? 'sm' : undefined}
|
||||
animation="border"
|
||||
aria-hidden="true"
|
||||
role="status"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default OLSpinner
|
|
@ -1,12 +1,14 @@
|
|||
import Tag from '@/features/ui/components/bootstrap-5/tag'
|
||||
import BS3Tag from '@/shared/components/tag'
|
||||
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
|
||||
import { forwardRef } from 'react'
|
||||
import { getAriaAndDataProps } from '@/features/utils/bootstrap-5'
|
||||
|
||||
type OLTagProps = React.ComponentProps<typeof Tag> & {
|
||||
bs3Props?: React.ComponentProps<typeof BS3Tag>
|
||||
}
|
||||
|
||||
function OLTag(props: OLTagProps) {
|
||||
const OLTag = forwardRef<HTMLElement, OLTagProps>((props: OLTagProps, ref) => {
|
||||
const { bs3Props, ...rest } = props
|
||||
|
||||
const bs3TagProps: React.ComponentProps<typeof BS3Tag> = {
|
||||
|
@ -14,15 +16,23 @@ function OLTag(props: OLTagProps) {
|
|||
prepend: rest.prepend,
|
||||
closeBtnProps: rest.closeBtnProps,
|
||||
className: rest.className,
|
||||
onClick: rest.onClick,
|
||||
onFocus: rest.onFocus,
|
||||
onBlur: rest.onBlur,
|
||||
onMouseOver: rest.onMouseOver,
|
||||
onMouseOut: rest.onMouseOut,
|
||||
...getAriaAndDataProps(rest),
|
||||
...bs3Props,
|
||||
}
|
||||
|
||||
return (
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={<BS3Tag {...bs3TagProps} />}
|
||||
bs5={<Tag {...rest} />}
|
||||
bs5={<Tag ref={ref} {...rest} />}
|
||||
/>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
OLTag.displayName = 'OLTag'
|
||||
|
||||
export default OLTag
|
||||
|
|
|
@ -42,6 +42,8 @@ export type DropdownItemProps = PropsWithChildren<{
|
|||
role?: string
|
||||
tabIndex?: number
|
||||
target?: string
|
||||
download?: boolean | string
|
||||
rel?: string
|
||||
}>
|
||||
|
||||
export type DropdownToggleProps = PropsWithChildren<{
|
||||
|
@ -53,6 +55,7 @@ export type DropdownToggleProps = PropsWithChildren<{
|
|||
variant?: SplitButtonVariants
|
||||
as?: ElementType
|
||||
size?: 'sm' | 'lg' | undefined
|
||||
'aria-label'?: string
|
||||
}>
|
||||
|
||||
export type DropdownMenuProps = PropsWithChildren<{
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import Icon from './icon'
|
||||
import { useEffect, useState } from 'react'
|
||||
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
|
||||
import { Spinner } from 'react-bootstrap-5'
|
||||
import OLSpinner, {
|
||||
OLSpinnerSize,
|
||||
} from '@/features/ui/components/ol/ol-spinner'
|
||||
import { isBootstrap5 } from '@/features/utils/bootstrap-5'
|
||||
import { setTimeout } from '@/utils/window'
|
||||
import classNames from 'classnames'
|
||||
|
||||
|
@ -10,12 +11,14 @@ function LoadingSpinner({
|
|||
align,
|
||||
delay = 0,
|
||||
loadingText,
|
||||
size,
|
||||
size = 'sm',
|
||||
className,
|
||||
}: {
|
||||
align?: 'left' | 'center'
|
||||
delay?: 0 | 500 // 500 is our standard delay
|
||||
loadingText?: string
|
||||
size?: 'sm'
|
||||
size?: OLSpinnerSize
|
||||
className?: string
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
|
@ -35,32 +38,19 @@ function LoadingSpinner({
|
|||
return null
|
||||
}
|
||||
|
||||
const alignmentClass =
|
||||
align === 'left' ? 'align-items-start' : 'align-items-center'
|
||||
const extraClasses = isBootstrap5()
|
||||
? [
|
||||
'd-inline-flex',
|
||||
align === 'left' ? 'align-items-start' : 'align-items-center',
|
||||
]
|
||||
: null
|
||||
|
||||
return (
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={
|
||||
<div className="loading">
|
||||
<Icon type="refresh" fw spin />
|
||||
|
||||
{loadingText || t('loading')}…
|
||||
</div>
|
||||
}
|
||||
bs5={
|
||||
<div className={classNames(`d-flex ${alignmentClass}`)}>
|
||||
<Spinner
|
||||
animation="border"
|
||||
aria-hidden="true"
|
||||
role="status"
|
||||
className="align-self-center"
|
||||
size={size}
|
||||
/>
|
||||
|
||||
{loadingText || t('loading')}…
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<div className={classNames('loading', className, extraClasses)}>
|
||||
<OLSpinner size={size} />
|
||||
|
||||
{loadingText || t('loading')}…
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -70,14 +60,16 @@ export function FullSizeLoadingSpinner({
|
|||
delay = 0,
|
||||
minHeight,
|
||||
loadingText,
|
||||
size = 'sm',
|
||||
}: {
|
||||
delay?: 0 | 500
|
||||
minHeight?: string
|
||||
loadingText?: string
|
||||
size?: OLSpinnerSize
|
||||
}) {
|
||||
return (
|
||||
<div className="full-size-loading-spinner-container" style={{ minHeight }}>
|
||||
<LoadingSpinner loadingText={loadingText} delay={delay} />
|
||||
<LoadingSpinner size={size} loadingText={loadingText} delay={delay} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -18,7 +18,9 @@ function SystemMessage({ id, children }: SystemMessageProps) {
|
|||
|
||||
return (
|
||||
<li className="system-message">
|
||||
{id !== 'protected' ? <Close onDismiss={() => setHidden(true)} /> : null}
|
||||
{id !== 'protected' ? (
|
||||
<Close onDismiss={() => setHidden(true)} variant="dark" />
|
||||
) : null}
|
||||
{children}
|
||||
</li>
|
||||
)
|
||||
|
|
|
@ -5,6 +5,7 @@ import {
|
|||
DropdownHeader,
|
||||
} from '@/features/ui/components/bootstrap-5/dropdown-menu'
|
||||
import type { Meta } from '@storybook/react'
|
||||
import OLDropdownMenuItem from '@/features/ui/components/ol/ol-dropdown-menu-item'
|
||||
|
||||
type Args = React.ComponentProps<typeof DropdownMenu>
|
||||
|
||||
|
@ -141,42 +142,52 @@ export const Icon = (args: Args) => {
|
|||
return (
|
||||
<DropdownMenu show>
|
||||
<li>
|
||||
<DropdownItem
|
||||
<OLDropdownMenuItem
|
||||
disabled={args.disabled}
|
||||
eventKey="1"
|
||||
href="#/action-1"
|
||||
leadingIcon="view_column_2"
|
||||
>
|
||||
Editor & PDF
|
||||
</DropdownItem>
|
||||
</OLDropdownMenuItem>
|
||||
</li>
|
||||
<li>
|
||||
<DropdownItem
|
||||
<OLDropdownMenuItem
|
||||
active
|
||||
eventKey="2"
|
||||
href="#/action-2"
|
||||
leadingIcon="terminal"
|
||||
>
|
||||
Editor only
|
||||
</DropdownItem>
|
||||
</OLDropdownMenuItem>
|
||||
</li>
|
||||
<li>
|
||||
<DropdownItem
|
||||
eventKey="2"
|
||||
href="#/action-2"
|
||||
<OLDropdownMenuItem
|
||||
eventKey="3"
|
||||
href="#/action-3"
|
||||
leadingIcon="picture_as_pdf"
|
||||
>
|
||||
PDF only
|
||||
</DropdownItem>
|
||||
</OLDropdownMenuItem>
|
||||
</li>
|
||||
<li>
|
||||
<DropdownItem
|
||||
eventKey="2"
|
||||
href="#/action-2"
|
||||
<OLDropdownMenuItem
|
||||
eventKey="4"
|
||||
href="#/action-4"
|
||||
leadingIcon="select_window"
|
||||
>
|
||||
PDF in separate tab
|
||||
</DropdownItem>
|
||||
</OLDropdownMenuItem>
|
||||
</li>
|
||||
<li>
|
||||
<OLDropdownMenuItem
|
||||
eventKey="5"
|
||||
href="#/action-5"
|
||||
leadingIcon="align_space_even"
|
||||
className="dropdown-item-material-icon-small"
|
||||
>
|
||||
Small icon
|
||||
</OLDropdownMenuItem>
|
||||
</li>
|
||||
</DropdownMenu>
|
||||
)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import Icon from '@/shared/components/icon'
|
||||
import OLTagIcon from '@/features/ui/components/ol/icons/ol-tag-icon'
|
||||
import BS3Tag from '@/shared/components/tag'
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
|
||||
|
@ -47,7 +47,7 @@ export const TagPrepend: Story = {
|
|||
render: args => {
|
||||
return (
|
||||
<div className="small">
|
||||
<BS3Tag prepend={<Icon type="tag" fw />} {...args} />
|
||||
<BS3Tag prepend={<OLTagIcon />} {...args} />
|
||||
</div>
|
||||
)
|
||||
},
|
||||
|
@ -58,7 +58,7 @@ export const TagWithCloseButton: Story = {
|
|||
return (
|
||||
<div className="small">
|
||||
<BS3Tag
|
||||
prepend={<Icon type="tag" fw />}
|
||||
prepend={<OLTagIcon />}
|
||||
closeBtnProps={{
|
||||
onClick: () => alert('Close triggered!'),
|
||||
}}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import Icon from '@/shared/components/icon'
|
||||
import OLTagIcon from '@/features/ui/components/ol/icons/ol-tag-icon'
|
||||
import Tag from '@/features/ui/components/bootstrap-5/tag'
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
|
||||
|
@ -41,7 +41,7 @@ export const TagDefault: Story = {
|
|||
|
||||
export const TagPrepend: Story = {
|
||||
render: args => {
|
||||
return <Tag prepend={<Icon type="tag" fw />} {...args} />
|
||||
return <Tag prepend={<OLTagIcon />} {...args} />
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -49,7 +49,7 @@ export const TagWithCloseButton: Story = {
|
|||
render: args => {
|
||||
return (
|
||||
<Tag
|
||||
prepend={<Icon type="tag" fw />}
|
||||
prepend={<OLTagIcon />}
|
||||
closeBtnProps={{
|
||||
onClick: () => alert('Close triggered!'),
|
||||
}}
|
||||
|
@ -63,7 +63,7 @@ export const TagWithContentButtonAndCloseButton: Story = {
|
|||
render: args => {
|
||||
return (
|
||||
<Tag
|
||||
prepend={<Icon type="tag" fw />}
|
||||
prepend={<OLTagIcon />}
|
||||
contentProps={{
|
||||
onClick: () => alert('Content button clicked!'),
|
||||
}}
|
||||
|
|
|
@ -44,9 +44,14 @@ history-root {
|
|||
.doc-container {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.doc-container .loading {
|
||||
margin: 10rem auto auto;
|
||||
}
|
||||
|
||||
.change-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -102,6 +107,7 @@ history-root {
|
|||
}
|
||||
|
||||
.history-version-details {
|
||||
display: flow-root;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
position: relative;
|
||||
|
@ -240,20 +246,15 @@ history-root {
|
|||
}
|
||||
|
||||
.loading {
|
||||
padding-top: 10rem;
|
||||
font-family: @font-family-serif;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
& > .loading {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.history-all-versions-scroller .loading {
|
||||
.history-all-versions-loading {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
padding: @line-height-computed / 2 0;
|
||||
background-color: @gray-lightest;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.history-version-saved-by {
|
||||
|
@ -271,8 +272,11 @@ history-root {
|
|||
|
||||
.history-compare-btn,
|
||||
.history-version-dropdown-menu-btn {
|
||||
.reset-button;
|
||||
|
||||
@size: 30px;
|
||||
padding: 0;
|
||||
border-radius: @btn-border-radius-large;
|
||||
width: @size;
|
||||
height: @size;
|
||||
line-height: 1;
|
||||
|
|
|
@ -45,6 +45,20 @@
|
|||
}
|
||||
}
|
||||
|
||||
@mixin action-button {
|
||||
font-size: 0;
|
||||
line-height: 1;
|
||||
border-radius: 50%;
|
||||
color: var(--content-primary);
|
||||
background-color: transparent;
|
||||
|
||||
&:hover,
|
||||
&:active,
|
||||
&[aria-expanded='true'] {
|
||||
background-color: rgb($neutral-90, 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
@mixin reset-button() {
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
|
@ -104,3 +118,28 @@
|
|||
transparent
|
||||
);
|
||||
}
|
||||
|
||||
@mixin mask-image($gradient) {
|
||||
// mask-image isn't supported without the -webkit prefix on all browsers we support yet
|
||||
-webkit-mask-image: $gradient;
|
||||
mask-image: $gradient;
|
||||
}
|
||||
|
||||
@mixin premium-background {
|
||||
background-image: var(--premium-gradient);
|
||||
}
|
||||
|
||||
@mixin premium-text {
|
||||
@include premium-background;
|
||||
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
color: var(--white); // Fallback
|
||||
background-color: var(--blue-70); // Fallback
|
||||
}
|
||||
|
||||
@mixin dark-bg {
|
||||
--link-color: var(--link-color-dark);
|
||||
--link-hover-color: var(--link-hover-color-dark);
|
||||
--link-visited-color: var(--link-visited-color-dark);
|
||||
}
|
||||
|
|
|
@ -5,6 +5,9 @@
|
|||
--link-color: var(--link-ui);
|
||||
--link-hover-color: var(--link-ui-hover);
|
||||
--link-visited-color: var(--link-ui-visited);
|
||||
--link-color-dark: var(--link-ui-dark);
|
||||
--link-hover-color-dark: var(--link-ui-hover-dark);
|
||||
--link-visited-color-dark: var(--link-ui-visited-dark);
|
||||
}
|
||||
|
||||
a {
|
||||
|
|
|
@ -74,10 +74,6 @@ samp {
|
|||
list-style-image: url('../../../../public/img/fa-check-green.svg');
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: var(--content-disabled) !important;
|
||||
}
|
||||
|
|
|
@ -29,3 +29,4 @@
|
|||
@import 'pagination';
|
||||
@import 'loading-spinner';
|
||||
@import 'error-boundary';
|
||||
@import 'close-button';
|
||||
|
|
|
@ -16,6 +16,10 @@ $max-width: 160px;
|
|||
margin-right: var(--spacing-02);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.material-symbols {
|
||||
font-size: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.badge-close {
|
||||
|
|
|
@ -164,14 +164,14 @@
|
|||
|
||||
.loading-spinner-small {
|
||||
border-width: 0.2em;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
height: 1.25rem;
|
||||
width: 1.25rem;
|
||||
}
|
||||
|
||||
.loading-spinner-large {
|
||||
border-width: 0.2em;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
height: 1.5rem;
|
||||
width: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -187,11 +187,11 @@
|
|||
justify-content: center;
|
||||
|
||||
.icon-small {
|
||||
font-size: 20px;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.icon-large {
|
||||
font-size: 24px;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
// This is our own implementation because the Bootstrap close button requires more customization than is worthwhile
|
||||
.close {
|
||||
@include reset-button;
|
||||
|
||||
color: var(--content-primary);
|
||||
|
||||
&.dark {
|
||||
color: var(--content-primary-dark);
|
||||
}
|
||||
}
|
|
@ -167,3 +167,15 @@
|
|||
.dropdown-item-highlighted {
|
||||
background-color: var(--bg-light-secondary);
|
||||
}
|
||||
|
||||
.dropdown-item-material-icon-small {
|
||||
.material-symbols,
|
||||
&.material-symbols {
|
||||
font-size: var(--bs-body-font-size);
|
||||
|
||||
// Centre the symbol in a 20px-by-20px box
|
||||
width: 20px;
|
||||
line-height: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,17 @@
|
|||
.loading {
|
||||
.spinner-border-sm,
|
||||
.spinner-border {
|
||||
// Ensure the thickness of the spinner is independent of the font size of its container
|
||||
font-size: var(--font-size-03);
|
||||
}
|
||||
|
||||
// Adjust the small spinner to be 25% larger than Bootstrap's default in each dimension
|
||||
.spinner-border-sm {
|
||||
--bs-spinner-width: 1.25rem;
|
||||
--bs-spinner-height: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.full-size-loading-spinner-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
.popover {
|
||||
@include shadow-md;
|
||||
@include dark-bg;
|
||||
|
||||
line-height: var(--line-height-02);
|
||||
}
|
||||
|
|
|
@ -17,9 +17,3 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.system-message .close {
|
||||
@include reset-button;
|
||||
|
||||
color: var(--content-primary-dark);
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
@import 'editor/figure-modal';
|
||||
@import 'editor/review-panel';
|
||||
@import 'editor/chat';
|
||||
@import 'editor/history';
|
||||
@import 'subscription';
|
||||
@import 'editor/pdf';
|
||||
@import 'editor/compile-button';
|
||||
|
|
|
@ -0,0 +1,383 @@
|
|||
history-root {
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
// Adding !important to override the styling of overlays and popovers
|
||||
.history-popover .popover-arrow {
|
||||
top: 20px !important;
|
||||
transform: unset !important;
|
||||
}
|
||||
|
||||
.history-react {
|
||||
--history-change-list-padding: var(--spacing-06);
|
||||
|
||||
display: flex;
|
||||
height: 100%;
|
||||
background-color: var(--bg-light-primary);
|
||||
|
||||
.history-header {
|
||||
@include body-sm;
|
||||
|
||||
height: 40px;
|
||||
background-color: var(--bg-dark-secondary);
|
||||
color: var(--content-primary-dark);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.doc-panel {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.toolbar-container {
|
||||
border-bottom: 1px solid var(--border-divider-dark);
|
||||
padding: 0 var(--spacing-04);
|
||||
}
|
||||
|
||||
.doc-container {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.doc-container .loading {
|
||||
margin: 10rem auto auto;
|
||||
}
|
||||
|
||||
.change-list {
|
||||
@include body-sm;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 320px;
|
||||
border-left: 1px solid var(--border-divider-dark);
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
.toggle-switch-label {
|
||||
flex: 1;
|
||||
|
||||
span {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.history-version-list-container {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.history-all-versions-scroller {
|
||||
overflow-y: auto;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.history-all-versions-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.history-versions-bottom {
|
||||
position: absolute;
|
||||
height: 8em;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.history-toggle-switch-container,
|
||||
.history-version-day,
|
||||
.history-version-details {
|
||||
padding: 0 var(--history-change-list-padding);
|
||||
}
|
||||
|
||||
.history-version-day {
|
||||
background-color: white;
|
||||
position: sticky;
|
||||
z-index: 1;
|
||||
top: 0;
|
||||
display: block;
|
||||
padding-top: var(--spacing-05);
|
||||
padding-bottom: var(--spacing-02);
|
||||
line-height: var(--line-height-02);
|
||||
}
|
||||
|
||||
.history-version-details {
|
||||
display: flow-root;
|
||||
padding-top: var(--spacing-04);
|
||||
padding-bottom: var(--spacing-04);
|
||||
position: relative;
|
||||
|
||||
&.history-version-selectable {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--bg-light-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
&.history-version-selected {
|
||||
background-color: var(--bg-accent-03);
|
||||
border-left: var(--spacing-02) solid var(--green-50);
|
||||
padding-left: calc(
|
||||
var(--history-change-list-padding) - var(--spacing-02)
|
||||
);
|
||||
}
|
||||
|
||||
&.history-version-selected.history-version-selectable:hover {
|
||||
background-color: rgb($green-70, 16%);
|
||||
border-left: var(--spacing-02) solid var(--green-50);
|
||||
}
|
||||
|
||||
&.history-version-within-selected {
|
||||
background-color: var(--bg-light-secondary);
|
||||
border-left: var(--spacing-02) solid var(--green-50);
|
||||
}
|
||||
|
||||
&.history-version-within-selected:hover {
|
||||
background-color: rgb($neutral-90, 8%);
|
||||
}
|
||||
}
|
||||
|
||||
.version-element-within-selected {
|
||||
background-color: var(--bg-light-secondary);
|
||||
border-left: var(--spacing-02) solid var(--green-50);
|
||||
}
|
||||
|
||||
.version-element-selected {
|
||||
background-color: var(--bg-accent-03);
|
||||
border-left: var(--spacing-02) solid var(--green-50);
|
||||
}
|
||||
|
||||
.history-version-metadata-time {
|
||||
display: block;
|
||||
margin-bottom: var(--spacing-02);
|
||||
color: var(--content-primary);
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: initial;
|
||||
}
|
||||
}
|
||||
|
||||
.history-version-metadata-users,
|
||||
.history-version-changes {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.history-version-restore-file {
|
||||
margin-bottom: var(--spacing-04);
|
||||
}
|
||||
|
||||
.history-version-metadata-users {
|
||||
display: inline;
|
||||
vertical-align: bottom;
|
||||
|
||||
> li {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin-right: var(--spacing-04);
|
||||
}
|
||||
}
|
||||
|
||||
.history-version-changes {
|
||||
> li {
|
||||
margin-bottom: var(--spacing-02);
|
||||
}
|
||||
}
|
||||
|
||||
.history-version-user-badge-color {
|
||||
--badge-size: 8px;
|
||||
|
||||
display: inline-block;
|
||||
width: var(--badge-size);
|
||||
height: var(--badge-size);
|
||||
margin-right: var(--spacing-02);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.history-version-user-badge-text {
|
||||
overflow-wrap: anywhere;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.history-version-day,
|
||||
.history-version-change-action,
|
||||
.history-version-metadata-users,
|
||||
.history-version-origin,
|
||||
.history-version-saved-by {
|
||||
color: var(--content-secondary);
|
||||
}
|
||||
|
||||
.history-version-change-action {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.history-version-change-doc {
|
||||
color: var(--content-primary);
|
||||
overflow-wrap: anywhere;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.history-version-divider-container {
|
||||
padding: var(--spacing-03) var(--spacing-04);
|
||||
}
|
||||
|
||||
.history-version-divider {
|
||||
margin: 0;
|
||||
border-color: var(--border-divider);
|
||||
}
|
||||
|
||||
.history-version-badge {
|
||||
margin-bottom: var(--spacing-02);
|
||||
margin-right: var(--spacing-05);
|
||||
height: unset;
|
||||
white-space: normal;
|
||||
overflow-wrap: anywhere;
|
||||
|
||||
.material-symbols {
|
||||
font-size: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.history-version-label {
|
||||
margin-bottom: var(--spacing-02);
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: initial;
|
||||
}
|
||||
}
|
||||
|
||||
.loading {
|
||||
font-family: $font-family-serif;
|
||||
}
|
||||
|
||||
.history-all-versions-loading {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
padding: var(--spacing-05) 0;
|
||||
background-color: var(--bg-light-secondary);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.history-version-saved-by {
|
||||
.history-version-saved-by-label {
|
||||
margin-right: var(--spacing-04);
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown.open {
|
||||
.history-version-dropdown-menu-btn {
|
||||
background-color: rgb(var(--bg-dark-primary) 0.08);
|
||||
box-shadow: initial;
|
||||
}
|
||||
}
|
||||
|
||||
.history-compare-btn,
|
||||
.history-version-dropdown-menu-btn {
|
||||
@include reset-button;
|
||||
@include action-button;
|
||||
|
||||
padding: 0;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.history-loading-panel {
|
||||
padding-top: 10rem;
|
||||
font-family: $font-family-serif;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.history-paywall-prompt {
|
||||
padding: var(--history-change-list-padding);
|
||||
|
||||
.history-feature-list {
|
||||
list-style: none;
|
||||
padding-left: var(--spacing-04);
|
||||
|
||||
li {
|
||||
margin-bottom: var(--spacing-06);
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.history-version-faded .history-version-details {
|
||||
max-height: 6em;
|
||||
|
||||
@include mask-image(linear-gradient(black 35%, transparent));
|
||||
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.history-paywall-heading {
|
||||
@include heading-sm;
|
||||
@include premium-text;
|
||||
|
||||
font-family: inherit;
|
||||
font-weight: 700;
|
||||
margin-top: var(--spacing-08);
|
||||
}
|
||||
|
||||
.history-content {
|
||||
padding: var(--spacing-05);
|
||||
}
|
||||
}
|
||||
|
||||
.history-version-label-tooltip {
|
||||
padding: 6px;
|
||||
text-align: initial;
|
||||
|
||||
.history-version-label-tooltip-row {
|
||||
margin-bottom: var(--spacing-03);
|
||||
|
||||
.history-version-label-tooltip-row-comment {
|
||||
overflow-wrap: anywhere;
|
||||
|
||||
& .material-symbols {
|
||||
font-size: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: initial;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.history-version-dropdown-menu {
|
||||
[role='menuitem'] {
|
||||
padding: var(--spacing-05);
|
||||
color: var(--content-primary);
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: var(--content-primary);
|
||||
background-color: var(--bg-light-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.history-dropdown-icon {
|
||||
color: var(--content-primary);
|
||||
}
|
||||
|
||||
.history-dropdown-icon-inverted {
|
||||
color: var(--neutral-10);
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.history-restore-promo-icon {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.history-error {
|
||||
padding: 16px;
|
||||
}
|
|
@ -328,20 +328,25 @@
|
|||
***************************************/
|
||||
|
||||
.toggle-switch {
|
||||
--toggle-switch-height: 26px;
|
||||
--toggle-switch-padding: var(--spacing-01);
|
||||
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 26px;
|
||||
height: var(--toggle-switch-height);
|
||||
margin-right: var(--spacing-03);
|
||||
border-radius: var(--border-radius-full);
|
||||
background-color: var(--neutral-20);
|
||||
padding: var(--spacing-01);
|
||||
padding: var(--toggle-switch-padding);
|
||||
}
|
||||
|
||||
.toggle-switch-label {
|
||||
display: inline-block;
|
||||
float: left;
|
||||
font-weight: normal;
|
||||
height: 100%;
|
||||
|
||||
// It seems we need to set the height explicitly rather than using 100% to get the button to display correctly in Blink
|
||||
height: calc(var(--toggle-switch-height) - 2 * var(--toggle-switch-padding));
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
|
|
|
@ -657,17 +657,9 @@
|
|||
}
|
||||
|
||||
.dropdown-table-button-toggle {
|
||||
padding: var(--spacing-04);
|
||||
font-size: 0;
|
||||
line-height: 1;
|
||||
border-radius: 50%;
|
||||
color: var(--content-primary);
|
||||
background-color: transparent;
|
||||
@include action-button;
|
||||
|
||||
&:hover,
|
||||
&:active {
|
||||
background-color: rgba($neutral-90, 0.08);
|
||||
}
|
||||
padding: var(--spacing-04);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,3 +5,8 @@
|
|||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,684 @@
|
|||
import '../../../helpers/bootstrap-5'
|
||||
import { useState } from 'react'
|
||||
import ToggleSwitch from '../../../../../frontend/js/features/history/components/change-list/toggle-switch'
|
||||
import ChangeList from '../../../../../frontend/js/features/history/components/change-list/change-list'
|
||||
import {
|
||||
EditorProviders,
|
||||
USER_EMAIL,
|
||||
USER_ID,
|
||||
} from '../../../helpers/editor-providers'
|
||||
import { HistoryProvider } from '../../../../../frontend/js/features/history/context/history-context'
|
||||
import { updates } from '../fixtures/updates'
|
||||
import { labels } from '../fixtures/labels'
|
||||
import {
|
||||
formatTime,
|
||||
relativeDate,
|
||||
} from '../../../../../frontend/js/features/utils/format-date'
|
||||
|
||||
const mountWithEditorProviders = (
|
||||
component: React.ReactNode,
|
||||
scope: Record<string, unknown> = {},
|
||||
props: Record<string, unknown> = {}
|
||||
) => {
|
||||
cy.mount(
|
||||
<EditorProviders scope={scope} {...props}>
|
||||
<HistoryProvider>
|
||||
<div style={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<div className="history-react">{component}</div>
|
||||
</div>
|
||||
</HistoryProvider>
|
||||
</EditorProviders>
|
||||
)
|
||||
}
|
||||
|
||||
describe('change list (Bootstrap 5)', function () {
|
||||
const scope = {
|
||||
ui: { view: 'history', pdfLayout: 'sideBySide', chatOpen: true },
|
||||
}
|
||||
|
||||
const waitForData = () => {
|
||||
cy.wait('@updates')
|
||||
cy.wait('@labels')
|
||||
cy.wait('@diff')
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
cy.intercept('GET', '/project/*/updates*', {
|
||||
body: updates,
|
||||
}).as('updates')
|
||||
cy.intercept('GET', '/project/*/labels', {
|
||||
body: labels,
|
||||
}).as('labels')
|
||||
cy.intercept('GET', '/project/*/filetree/diff*', {
|
||||
body: { diff: [{ pathname: 'main.tex' }, { pathname: 'name.tex' }] },
|
||||
}).as('diff')
|
||||
window.metaAttributesCache.set('ol-inactiveTutorials', [
|
||||
'react-history-buttons-tutorial',
|
||||
])
|
||||
})
|
||||
|
||||
describe('toggle switch', function () {
|
||||
it('renders switch buttons', function () {
|
||||
mountWithEditorProviders(
|
||||
<ToggleSwitch labelsOnly={false} setLabelsOnly={() => {}} />
|
||||
)
|
||||
|
||||
cy.findByLabelText(/all history/i)
|
||||
cy.findByLabelText(/labels/i)
|
||||
})
|
||||
|
||||
it('toggles "all history" and "labels" buttons', function () {
|
||||
function ToggleSwitchWrapped({ labelsOnly }: { labelsOnly: boolean }) {
|
||||
const [labelsOnlyLocal, setLabelsOnlyLocal] = useState(labelsOnly)
|
||||
return (
|
||||
<ToggleSwitch
|
||||
labelsOnly={labelsOnlyLocal}
|
||||
setLabelsOnly={setLabelsOnlyLocal}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
mountWithEditorProviders(<ToggleSwitchWrapped labelsOnly={false} />)
|
||||
|
||||
cy.findByLabelText(/all history/i).as('all-history')
|
||||
cy.findByLabelText(/labels/i).as('labels')
|
||||
cy.get('@all-history').should('be.checked')
|
||||
cy.get('@labels').should('not.be.checked')
|
||||
cy.get('@labels').click({ force: true })
|
||||
cy.get('@all-history').should('not.be.checked')
|
||||
cy.get('@labels').should('be.checked')
|
||||
})
|
||||
})
|
||||
|
||||
describe('tags', function () {
|
||||
it('renders tags', function () {
|
||||
mountWithEditorProviders(<ChangeList />, scope, {
|
||||
user: {
|
||||
id: USER_ID,
|
||||
email: USER_EMAIL,
|
||||
isAdmin: true,
|
||||
},
|
||||
})
|
||||
waitForData()
|
||||
|
||||
cy.findByLabelText(/all history/i).click({ force: true })
|
||||
cy.findAllByTestId('history-version-details').as('details')
|
||||
cy.get('@details').should('have.length', 5)
|
||||
// start with 2nd details entry, as first has no tags
|
||||
cy.get('@details')
|
||||
.eq(1)
|
||||
.within(() => {
|
||||
cy.findAllByTestId('history-version-badge').as('tags')
|
||||
})
|
||||
cy.get('@tags').should('have.length', 2)
|
||||
cy.get('@tags').eq(0).should('contain.text', 'tag-2')
|
||||
cy.get('@tags').eq(1).should('contain.text', 'tag-1')
|
||||
// should have delete buttons
|
||||
cy.get('@tags').each(tag =>
|
||||
cy.wrap(tag).within(() => {
|
||||
cy.findByRole('button', { name: /delete/i })
|
||||
})
|
||||
)
|
||||
// 3rd details entry
|
||||
cy.get('@details')
|
||||
.eq(2)
|
||||
.within(() => {
|
||||
cy.findAllByTestId('history-version-badge').should('have.length', 0)
|
||||
})
|
||||
// 4th details entry
|
||||
cy.get('@details')
|
||||
.eq(3)
|
||||
.within(() => {
|
||||
cy.findAllByTestId('history-version-badge').as('tags')
|
||||
})
|
||||
cy.get('@tags').should('have.length', 2)
|
||||
cy.get('@tags').eq(0).should('contain.text', 'tag-4')
|
||||
cy.get('@tags').eq(1).should('contain.text', 'tag-3')
|
||||
// should not have delete buttons
|
||||
cy.get('@tags').each(tag =>
|
||||
cy.wrap(tag).within(() => {
|
||||
cy.findByRole('button', { name: /delete/i }).should('not.exist')
|
||||
})
|
||||
)
|
||||
cy.findByLabelText(/labels/i).click({ force: true })
|
||||
cy.findAllByTestId('history-version-details').as('details')
|
||||
// first details on labels is always "current version", start testing on second
|
||||
cy.get('@details').should('have.length', 3)
|
||||
cy.get('@details')
|
||||
.eq(1)
|
||||
.within(() => {
|
||||
cy.findAllByTestId('history-version-badge').as('tags')
|
||||
})
|
||||
cy.get('@tags').should('have.length', 2)
|
||||
cy.get('@tags').eq(0).should('contain.text', 'tag-2')
|
||||
cy.get('@tags').eq(1).should('contain.text', 'tag-1')
|
||||
cy.get('@details')
|
||||
.eq(2)
|
||||
.within(() => {
|
||||
cy.findAllByTestId('history-version-badge').as('tags')
|
||||
})
|
||||
cy.get('@tags').should('have.length', 3)
|
||||
cy.get('@tags').eq(0).should('contain.text', 'tag-5')
|
||||
cy.get('@tags').eq(1).should('contain.text', 'tag-4')
|
||||
cy.get('@tags').eq(2).should('contain.text', 'tag-3')
|
||||
})
|
||||
|
||||
it('deletes tag', function () {
|
||||
mountWithEditorProviders(<ChangeList />, scope, {
|
||||
user: {
|
||||
id: USER_ID,
|
||||
email: USER_EMAIL,
|
||||
isAdmin: true,
|
||||
},
|
||||
})
|
||||
waitForData()
|
||||
|
||||
cy.findByLabelText(/all history/i).click({ force: true })
|
||||
|
||||
const labelToDelete = 'tag-2'
|
||||
cy.findAllByTestId('history-version-details').eq(1).as('details')
|
||||
cy.get('@details').within(() => {
|
||||
cy.findAllByTestId('history-version-badge').eq(0).as('tag')
|
||||
})
|
||||
cy.get('@tag').should('contain.text', labelToDelete)
|
||||
cy.get('@tag').within(() => {
|
||||
cy.findByRole('button', { name: /delete/i }).as('delete-btn')
|
||||
})
|
||||
cy.get('@delete-btn').click()
|
||||
cy.findByRole('dialog').as('modal')
|
||||
cy.get('@modal').within(() => {
|
||||
cy.findByRole('heading', { name: /delete label/i })
|
||||
})
|
||||
cy.get('@modal').contains(
|
||||
new RegExp(
|
||||
`are you sure you want to delete the following label "${labelToDelete}"?`,
|
||||
'i'
|
||||
)
|
||||
)
|
||||
cy.get('@modal').within(() => {
|
||||
cy.findByRole('button', { name: /cancel/i }).click()
|
||||
})
|
||||
cy.findByRole('dialog').should('not.exist')
|
||||
cy.get('@delete-btn').click()
|
||||
cy.findByRole('dialog').as('modal')
|
||||
cy.intercept('DELETE', '/project/*/labels/*', {
|
||||
statusCode: 500,
|
||||
}).as('delete')
|
||||
cy.get('@modal').within(() => {
|
||||
cy.findByRole('button', { name: /delete/i }).click()
|
||||
})
|
||||
cy.wait('@delete')
|
||||
cy.get('@modal').within(() => {
|
||||
cy.findByRole('alert').within(() => {
|
||||
cy.contains(/sorry, something went wrong/i)
|
||||
})
|
||||
})
|
||||
cy.findByText(labelToDelete).should('have.length', 1)
|
||||
|
||||
cy.intercept('DELETE', '/project/*/labels/*', {
|
||||
statusCode: 204,
|
||||
}).as('delete')
|
||||
cy.get('@modal').within(() => {
|
||||
cy.findByRole('button', { name: /delete/i }).click()
|
||||
})
|
||||
cy.wait('@delete')
|
||||
cy.findByText(labelToDelete).should('not.exist')
|
||||
})
|
||||
|
||||
it('verifies that selecting the same list item will not trigger a new diff', function () {
|
||||
mountWithEditorProviders(<ChangeList />, scope, {
|
||||
user: {
|
||||
id: USER_ID,
|
||||
email: USER_EMAIL,
|
||||
isAdmin: true,
|
||||
},
|
||||
})
|
||||
waitForData()
|
||||
|
||||
const stub = cy.stub().as('diffStub')
|
||||
cy.intercept('GET', '/project/*/filetree/diff*', stub).as('diff')
|
||||
|
||||
cy.findAllByTestId('history-version-details').eq(2).as('details')
|
||||
cy.get('@details').click() // 1st click
|
||||
cy.wait('@diff')
|
||||
cy.get('@details').click() // 2nd click
|
||||
cy.get('@diffStub').should('have.been.calledOnce')
|
||||
})
|
||||
})
|
||||
|
||||
describe('all history', function () {
|
||||
beforeEach(function () {
|
||||
mountWithEditorProviders(<ChangeList />, scope, {
|
||||
user: {
|
||||
id: USER_ID,
|
||||
email: USER_EMAIL,
|
||||
isAdmin: true,
|
||||
},
|
||||
})
|
||||
waitForData()
|
||||
})
|
||||
|
||||
it('shows grouped versions date', function () {
|
||||
cy.findByText(relativeDate(updates.updates[0].meta.end_ts))
|
||||
cy.findByText(relativeDate(updates.updates[1].meta.end_ts))
|
||||
})
|
||||
|
||||
it('shows the date of the version', function () {
|
||||
cy.findAllByTestId('history-version-details')
|
||||
.eq(1)
|
||||
.within(() => {
|
||||
cy.findByTestId('history-version-metadata-time').should(
|
||||
'have.text',
|
||||
formatTime(updates.updates[0].meta.end_ts, 'Do MMMM, h:mm a')
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('shows change action', function () {
|
||||
cy.findAllByTestId('history-version-details')
|
||||
.eq(1)
|
||||
.within(() => {
|
||||
cy.findByTestId('history-version-change-action').should(
|
||||
'have.text',
|
||||
'Created'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('shows changed document name', function () {
|
||||
cy.findAllByTestId('history-version-details')
|
||||
.eq(2)
|
||||
.within(() => {
|
||||
cy.findByTestId('history-version-change-doc').should(
|
||||
'have.text',
|
||||
updates.updates[2].pathnames[0]
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('shows users', function () {
|
||||
cy.findAllByTestId('history-version-details')
|
||||
.eq(1)
|
||||
.within(() => {
|
||||
cy.findByTestId('history-version-metadata-users')
|
||||
.should('contain.text', 'You')
|
||||
.and('contain.text', updates.updates[1].meta.users[1].first_name)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('labels only', function () {
|
||||
beforeEach(function () {
|
||||
mountWithEditorProviders(<ChangeList />, scope, {
|
||||
user: {
|
||||
id: USER_ID,
|
||||
email: USER_EMAIL,
|
||||
isAdmin: true,
|
||||
},
|
||||
})
|
||||
waitForData()
|
||||
cy.findByLabelText(/labels/i).click({ force: true })
|
||||
})
|
||||
|
||||
it('shows the dropdown menu item for adding new labels', function () {
|
||||
cy.findAllByTestId('history-version-details')
|
||||
.eq(1)
|
||||
.within(() => {
|
||||
cy.findByRole('button', { name: /more actions/i }).click()
|
||||
cy.findByRole('menu').within(() => {
|
||||
cy.findByRole('menuitem', {
|
||||
name: /label this version/i,
|
||||
}).should('exist')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('resets from compare to view mode when switching tabs', function () {
|
||||
cy.findAllByTestId('history-version-details')
|
||||
.eq(1)
|
||||
.within(() => {
|
||||
cy.findByRole('button', {
|
||||
name: /Compare/i,
|
||||
}).click()
|
||||
})
|
||||
cy.findByLabelText(/all history/i).click({ force: true })
|
||||
cy.findAllByTestId('history-version-details').should($versions => {
|
||||
const [selected, ...rest] = Array.from($versions)
|
||||
expect(selected).to.have.attr('data-selected', 'selected')
|
||||
expect(
|
||||
rest.every(version => version.dataset.selected === 'belowSelected')
|
||||
).to.be.true
|
||||
})
|
||||
})
|
||||
it('opens the compare drop down and compares with selected version', function () {
|
||||
cy.findByLabelText(/all history/i).click({ force: true })
|
||||
cy.findAllByTestId('history-version-details')
|
||||
.eq(3)
|
||||
.within(() => {
|
||||
cy.findByRole('button', {
|
||||
name: /compare from this version/i,
|
||||
}).click()
|
||||
})
|
||||
|
||||
cy.findAllByTestId('history-version-details')
|
||||
.eq(1)
|
||||
.within(() => {
|
||||
cy.get('[aria-label="Compare"]').click()
|
||||
cy.findByRole('menu').within(() => {
|
||||
cy.findByRole('menuitem', {
|
||||
name: /compare up to this version/i,
|
||||
}).click()
|
||||
})
|
||||
})
|
||||
|
||||
cy.findAllByTestId('history-version-details').should($versions => {
|
||||
const [
|
||||
aboveSelected,
|
||||
upperSelected,
|
||||
withinSelected,
|
||||
lowerSelected,
|
||||
belowSelected,
|
||||
] = Array.from($versions)
|
||||
expect(aboveSelected).to.have.attr('data-selected', 'aboveSelected')
|
||||
expect(upperSelected).to.have.attr('data-selected', 'upperSelected')
|
||||
expect(withinSelected).to.have.attr('data-selected', 'withinSelected')
|
||||
expect(lowerSelected).to.have.attr('data-selected', 'lowerSelected')
|
||||
expect(belowSelected).to.have.attr('data-selected', 'belowSelected')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('compare mode', function () {
|
||||
beforeEach(function () {
|
||||
mountWithEditorProviders(<ChangeList />, scope, {
|
||||
user: {
|
||||
id: USER_ID,
|
||||
email: USER_EMAIL,
|
||||
isAdmin: true,
|
||||
},
|
||||
})
|
||||
waitForData()
|
||||
})
|
||||
|
||||
it('compares versions', function () {
|
||||
cy.findAllByTestId('history-version-details').should($versions => {
|
||||
const [first, ...rest] = Array.from($versions)
|
||||
expect(first).to.have.attr('data-selected', 'selected')
|
||||
rest.forEach(version =>
|
||||
// Based on the fact that we are selecting first version as we load the page
|
||||
// Every other version will be belowSelected
|
||||
expect(version).to.have.attr('data-selected', 'belowSelected')
|
||||
)
|
||||
})
|
||||
|
||||
cy.intercept('GET', '/project/*/filetree/diff*', {
|
||||
body: { diff: [{ pathname: 'main.tex' }, { pathname: 'name.tex' }] },
|
||||
}).as('compareDiff')
|
||||
|
||||
cy.findAllByTestId('history-version-details')
|
||||
.last()
|
||||
.within(() => {
|
||||
cy.findByTestId('compare-icon-version').click()
|
||||
})
|
||||
cy.wait('@compareDiff')
|
||||
})
|
||||
})
|
||||
|
||||
describe('dropdown', function () {
|
||||
beforeEach(function () {
|
||||
mountWithEditorProviders(<ChangeList />, scope, {
|
||||
user: {
|
||||
id: USER_ID,
|
||||
email: USER_EMAIL,
|
||||
isAdmin: true,
|
||||
},
|
||||
})
|
||||
waitForData()
|
||||
})
|
||||
|
||||
it('adds badge/label', function () {
|
||||
cy.findAllByTestId('history-version-details').eq(1).as('version')
|
||||
cy.get('@version').within(() => {
|
||||
cy.findByRole('button', { name: /more actions/i }).click()
|
||||
cy.findByRole('menu').within(() => {
|
||||
cy.findByRole('menuitem', {
|
||||
name: /label this version/i,
|
||||
}).click()
|
||||
})
|
||||
})
|
||||
cy.intercept('POST', '/project/*/labels', req => {
|
||||
req.reply(200, {
|
||||
id: '64633ee158e9ef7da614c000',
|
||||
comment: req.body.comment,
|
||||
version: req.body.version,
|
||||
user_id: USER_ID,
|
||||
created_at: '2023-05-16T08:29:21.250Z',
|
||||
user_display_name: 'john.doe',
|
||||
})
|
||||
}).as('addLabel')
|
||||
const newLabel = 'my new label'
|
||||
cy.findByRole('dialog').within(() => {
|
||||
cy.findByRole('heading', { name: /add label/i })
|
||||
cy.findByRole('button', { name: /cancel/i })
|
||||
cy.findByRole('button', { name: /add label/i }).should('be.disabled')
|
||||
cy.findByPlaceholderText(/new label name/i).as('input')
|
||||
cy.get('@input').type(newLabel)
|
||||
cy.findByRole('button', { name: /add label/i }).should('be.enabled')
|
||||
cy.get('@input').type('{enter}')
|
||||
})
|
||||
cy.wait('@addLabel')
|
||||
cy.get('@version').within(() => {
|
||||
cy.findAllByTestId('history-version-badge').should($badges => {
|
||||
const includes = Array.from($badges).some(badge =>
|
||||
badge.textContent?.includes(newLabel)
|
||||
)
|
||||
expect(includes).to.be.true
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('downloads version', function () {
|
||||
cy.intercept('GET', '/project/*/version/*/zip', { statusCode: 200 }).as(
|
||||
'download'
|
||||
)
|
||||
cy.findAllByTestId('history-version-details')
|
||||
.eq(0)
|
||||
.within(() => {
|
||||
cy.findByRole('button', { name: /more actions/i }).click()
|
||||
cy.findByRole('menu').within(() => {
|
||||
cy.findByRole('menuitem', {
|
||||
name: /download this version/i,
|
||||
}).click()
|
||||
})
|
||||
})
|
||||
cy.wait('@download')
|
||||
})
|
||||
})
|
||||
|
||||
describe('paywall', function () {
|
||||
const now = Date.now()
|
||||
const oneMinuteAgo = now - 60 * 1000
|
||||
const justOverADayAgo = now - 25 * 60 * 60 * 1000
|
||||
const twoDaysAgo = now - 48 * 60 * 60 * 1000
|
||||
|
||||
const updates = {
|
||||
updates: [
|
||||
{
|
||||
fromV: 3,
|
||||
toV: 4,
|
||||
meta: {
|
||||
users: [
|
||||
{
|
||||
first_name: 'john.doe',
|
||||
last_name: '',
|
||||
email: 'john.doe@test.com',
|
||||
id: '1',
|
||||
},
|
||||
],
|
||||
start_ts: oneMinuteAgo,
|
||||
end_ts: oneMinuteAgo,
|
||||
},
|
||||
labels: [],
|
||||
pathnames: [],
|
||||
project_ops: [{ add: { pathname: 'name.tex' }, atV: 3 }],
|
||||
},
|
||||
{
|
||||
fromV: 1,
|
||||
toV: 3,
|
||||
meta: {
|
||||
users: [
|
||||
{
|
||||
first_name: 'bobby.lapointe',
|
||||
last_name: '',
|
||||
email: 'bobby.lapointe@test.com',
|
||||
id: '2',
|
||||
},
|
||||
],
|
||||
start_ts: justOverADayAgo,
|
||||
end_ts: justOverADayAgo - 10 * 1000,
|
||||
},
|
||||
labels: [],
|
||||
pathnames: ['main.tex'],
|
||||
project_ops: [],
|
||||
},
|
||||
{
|
||||
fromV: 0,
|
||||
toV: 1,
|
||||
meta: {
|
||||
users: [
|
||||
{
|
||||
first_name: 'john.doe',
|
||||
last_name: '',
|
||||
email: 'john.doe@test.com',
|
||||
id: '1',
|
||||
},
|
||||
],
|
||||
start_ts: twoDaysAgo,
|
||||
end_ts: twoDaysAgo,
|
||||
},
|
||||
labels: [
|
||||
{
|
||||
id: 'label1',
|
||||
comment: 'tag-1',
|
||||
version: 0,
|
||||
user_id: USER_ID,
|
||||
created_at: justOverADayAgo,
|
||||
},
|
||||
],
|
||||
pathnames: [],
|
||||
project_ops: [{ add: { pathname: 'main.tex' }, atV: 0 }],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const labels = [
|
||||
{
|
||||
id: 'label1',
|
||||
comment: 'tag-1',
|
||||
version: 0,
|
||||
user_id: USER_ID,
|
||||
created_at: justOverADayAgo,
|
||||
user_display_name: 'john.doe',
|
||||
},
|
||||
]
|
||||
|
||||
const waitForData = () => {
|
||||
cy.wait('@updates')
|
||||
cy.wait('@labels')
|
||||
cy.wait('@diff')
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
cy.intercept('GET', '/project/*/updates*', {
|
||||
body: updates,
|
||||
}).as('updates')
|
||||
cy.intercept('GET', '/project/*/labels', {
|
||||
body: labels,
|
||||
}).as('labels')
|
||||
cy.intercept('GET', '/project/*/filetree/diff*', {
|
||||
body: { diff: [{ pathname: 'main.tex' }, { pathname: 'name.tex' }] },
|
||||
}).as('diff')
|
||||
})
|
||||
|
||||
it('shows non-owner paywall', function () {
|
||||
const scope = {
|
||||
ui: {
|
||||
view: 'history',
|
||||
pdfLayout: 'sideBySide',
|
||||
chatOpen: true,
|
||||
},
|
||||
}
|
||||
|
||||
mountWithEditorProviders(<ChangeList />, scope, {
|
||||
user: {
|
||||
id: USER_ID,
|
||||
email: USER_EMAIL,
|
||||
isAdmin: false,
|
||||
},
|
||||
})
|
||||
|
||||
waitForData()
|
||||
|
||||
cy.get('.history-paywall-prompt').should('have.length', 1)
|
||||
cy.findAllByTestId('history-version').should('have.length', 2)
|
||||
cy.get('.history-paywall-prompt button').should('not.exist')
|
||||
})
|
||||
|
||||
it('shows owner paywall', function () {
|
||||
const scope = {
|
||||
ui: {
|
||||
view: 'history',
|
||||
pdfLayout: 'sideBySide',
|
||||
chatOpen: true,
|
||||
},
|
||||
}
|
||||
|
||||
mountWithEditorProviders(<ChangeList />, scope, {
|
||||
user: {
|
||||
id: USER_ID,
|
||||
email: USER_EMAIL,
|
||||
isAdmin: false,
|
||||
},
|
||||
projectOwner: {
|
||||
_id: USER_ID,
|
||||
email: USER_EMAIL,
|
||||
},
|
||||
})
|
||||
|
||||
waitForData()
|
||||
|
||||
cy.get('.history-paywall-prompt').should('have.length', 1)
|
||||
cy.findAllByTestId('history-version').should('have.length', 2)
|
||||
cy.get('.history-paywall-prompt button').should('have.length', 1)
|
||||
})
|
||||
|
||||
it('shows all labels in free tier', function () {
|
||||
const scope = {
|
||||
ui: {
|
||||
view: 'history',
|
||||
pdfLayout: 'sideBySide',
|
||||
chatOpen: true,
|
||||
},
|
||||
}
|
||||
|
||||
mountWithEditorProviders(<ChangeList />, scope, {
|
||||
user: {
|
||||
id: USER_ID,
|
||||
email: USER_EMAIL,
|
||||
isAdmin: false,
|
||||
},
|
||||
projectOwner: {
|
||||
_id: USER_ID,
|
||||
email: USER_EMAIL,
|
||||
},
|
||||
})
|
||||
|
||||
waitForData()
|
||||
|
||||
cy.findByLabelText(/labels/i).click({ force: true })
|
||||
|
||||
// One pseudo-label for the current state, one for our label
|
||||
cy.get('.history-version-label').should('have.length', 2)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -31,7 +31,7 @@ const mountWithEditorProviders = (
|
|||
)
|
||||
}
|
||||
|
||||
describe('change list', function () {
|
||||
describe('change list (Bootstrap 3)', function () {
|
||||
const scope = {
|
||||
ui: { view: 'history', pdfLayout: 'sideBySide', chatOpen: true },
|
||||
}
|
||||
|
@ -363,7 +363,7 @@ describe('change list', function () {
|
|||
cy.findAllByTestId('history-version-details')
|
||||
.eq(1)
|
||||
.within(() => {
|
||||
cy.findByRole('button', { name: /compare drop down/i }).click()
|
||||
cy.findByRole('button', { name: /compare/i }).click()
|
||||
cy.findByRole('menu').within(() => {
|
||||
cy.findByRole('menuitem', {
|
||||
name: /compare up to this version/i,
|
||||
|
|
Loading…
Reference in a new issue