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,
|
"function-url-quotes": null,
|
||||||
"no-descending-specificity": null,
|
"no-descending-specificity": null,
|
||||||
"scss/at-extend-no-missing-placeholder": 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 { useTranslation } from 'react-i18next'
|
||||||
import { useEffect, useState } from 'react'
|
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 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 useAsync from '../../../../shared/hooks/use-async'
|
||||||
import useAbortController from '../../../../shared/hooks/use-abort-controller'
|
import useAbortController from '../../../../shared/hooks/use-abort-controller'
|
||||||
import useAddOrRemoveLabels from '../../hooks/use-add-or-remove-labels'
|
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 { Label } from '../../services/types/label'
|
||||||
import { useRefWithAutoFocus } from '../../../../shared/hooks/use-ref-with-auto-focus'
|
import { useRefWithAutoFocus } from '../../../../shared/hooks/use-ref-with-auto-focus'
|
||||||
import { debugConsole } from '@/utils/debugging'
|
import { debugConsole } from '@/utils/debugging'
|
||||||
|
import OLFormControl from '@/features/ui/components/ol/ol-form-control'
|
||||||
|
|
||||||
type AddLabelModalProps = {
|
type AddLabelModalProps = {
|
||||||
show: boolean
|
show: boolean
|
||||||
|
@ -71,51 +79,55 @@ function AddLabelModal({ show, setShow, version }: AddLabelModalProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AccessibleModal
|
<OLModal
|
||||||
show={show}
|
show={show}
|
||||||
onExited={handleModalExited}
|
onExited={handleModalExited}
|
||||||
onHide={() => setShow(false)}
|
onHide={() => setShow(false)}
|
||||||
id="add-history-label"
|
id="add-history-label"
|
||||||
>
|
>
|
||||||
<Modal.Header>
|
<OLModalHeader>
|
||||||
<Modal.Title>{t('history_add_label')}</Modal.Title>
|
<OLModalTitle>{t('history_add_label')}</OLModalTitle>
|
||||||
</Modal.Header>
|
</OLModalHeader>
|
||||||
<form onSubmit={handleSubmit}>
|
<OLForm onSubmit={handleSubmit}>
|
||||||
<Modal.Body>
|
<OLModalBody>
|
||||||
{isError && <ModalError error={responseError} />}
|
{isError && <ModalError error={responseError} />}
|
||||||
<FormGroup>
|
<OLFormGroup>
|
||||||
<input
|
<OLFormControl
|
||||||
ref={autoFocusedRef}
|
ref={autoFocusedRef}
|
||||||
className="form-control"
|
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={t('history_new_label_name')}
|
placeholder={t('history_new_label_name')}
|
||||||
required
|
required
|
||||||
value={comment}
|
value={comment}
|
||||||
onChange={(
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
e: React.ChangeEvent<HTMLInputElement & FormControl>
|
setComment(e.target.value)
|
||||||
) => setComment(e.target.value)}
|
}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</OLFormGroup>
|
||||||
</Modal.Body>
|
</OLModalBody>
|
||||||
<Modal.Footer>
|
<OLModalFooter>
|
||||||
<button
|
<OLButton
|
||||||
type="button"
|
variant="secondary"
|
||||||
className="btn btn-secondary"
|
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
onClick={() => setShow(false)}
|
onClick={() => setShow(false)}
|
||||||
>
|
>
|
||||||
{t('cancel')}
|
{t('cancel')}
|
||||||
</button>
|
</OLButton>
|
||||||
<button
|
<OLButton
|
||||||
type="submit"
|
type="submit"
|
||||||
className="btn btn-primary"
|
variant="primary"
|
||||||
disabled={isLoading || !comment.length}
|
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')}
|
{t('history_add_label')}
|
||||||
</button>
|
</OLButton>
|
||||||
</Modal.Footer>
|
</OLModalFooter>
|
||||||
</form>
|
</OLForm>
|
||||||
</AccessibleModal>
|
</OLModal>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,8 @@ import { useUserContext } from '../../../../shared/context/user-context'
|
||||||
import useDropdownActiveItem from '../../hooks/use-dropdown-active-item'
|
import useDropdownActiveItem from '../../hooks/use-dropdown-active-item'
|
||||||
import { useHistoryContext } from '../../context/history-context'
|
import { useHistoryContext } from '../../context/history-context'
|
||||||
import { useEditorContext } from '../../../../shared/context/editor-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 Close from '@/shared/components/close'
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
import MaterialIcon from '@/shared/components/material-icon'
|
import MaterialIcon from '@/shared/components/material-icon'
|
||||||
|
@ -173,18 +174,29 @@ function AllHistoryList() {
|
||||||
|
|
||||||
if (showHistoryTutorial) {
|
if (showHistoryTutorial) {
|
||||||
popover = (
|
popover = (
|
||||||
<Overlay
|
<OLOverlay
|
||||||
placement="left"
|
placement="left-start"
|
||||||
show={showHistoryTutorial}
|
show={showHistoryTutorial}
|
||||||
rootClose
|
rootClose
|
||||||
onHide={hidePopover}
|
onHide={hidePopover}
|
||||||
// using scrollerRef to position the popover in the middle of the viewport
|
// using scrollerRef to position the popover in the middle of the viewport
|
||||||
target={scrollerRef.current ?? undefined}
|
target={scrollerRef.current}
|
||||||
shouldUpdatePosition
|
// 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
|
<OLPopover
|
||||||
id="popover-toolbar-overflow"
|
id="popover-react-history-tutorial"
|
||||||
arrowOffsetTop={10}
|
bs3Props={{ arrowOffsetTop: 10 }}
|
||||||
title={
|
title={
|
||||||
<span>
|
<span>
|
||||||
{t('react_history_tutorial_title')}{' '}
|
{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
|
<a href="https://www.overleaf.com/learn/latex/Using_the_History_feature" />, // eslint-disable-line jsx-a11y/anchor-has-content, react/jsx-key
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</Popover>
|
</OLPopover>
|
||||||
</Overlay>
|
</OLOverlay>
|
||||||
)
|
)
|
||||||
} else if (showRestorePromo) {
|
} else if (showRestorePromo) {
|
||||||
popover = (
|
popover = (
|
||||||
<Overlay
|
<OLOverlay
|
||||||
placement="left"
|
placement="left-start"
|
||||||
show={showRestorePromo}
|
show={showRestorePromo}
|
||||||
rootClose
|
rootClose
|
||||||
onHide={hidePopover}
|
onHide={hidePopover}
|
||||||
// using scrollerRef to position the popover in the middle of the viewport
|
// using scrollerRef to position the popover in the middle of the viewport
|
||||||
target={scrollerRef.current ?? undefined}
|
target={scrollerRef.current}
|
||||||
shouldUpdatePosition
|
bs3Props={{ shouldUpdatePosition: true }}
|
||||||
>
|
>
|
||||||
<Popover
|
<OLPopover
|
||||||
id="popover-toolbar-overflow"
|
id="popover-history-restore-promo"
|
||||||
arrowOffsetTop={10}
|
bs3Props={{ arrowOffsetTop: 10 }}
|
||||||
title={
|
title={
|
||||||
<span>
|
<span>
|
||||||
{t('history_restore_promo_title')}
|
{t('history_restore_promo_title')}
|
||||||
|
@ -255,8 +267,8 @@ function AllHistoryList() {
|
||||||
/>,
|
/>,
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</Popover>
|
</OLPopover>
|
||||||
</Overlay>
|
</OLOverlay>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -330,7 +342,9 @@ function AllHistoryList() {
|
||||||
{showNonOwnerPaywall ? <NonOwnerPaywallPrompt /> : null}
|
{showNonOwnerPaywall ? <NonOwnerPaywallPrompt /> : null}
|
||||||
{updatesLoadingState === 'loadingInitial' ||
|
{updatesLoadingState === 'loadingInitial' ||
|
||||||
updatesLoadingState === 'loadingUpdates' ? (
|
updatesLoadingState === 'loadingUpdates' ? (
|
||||||
<LoadingSpinner />
|
<div className="history-all-versions-loading">
|
||||||
|
<LoadingSpinner />
|
||||||
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,9 +1,15 @@
|
||||||
import { useRef, useEffect, ReactNode } from 'react'
|
import React, { useRef, useEffect, ReactNode } from 'react'
|
||||||
import { Dropdown } from 'react-bootstrap'
|
import { Dropdown as BS3Dropdown } from 'react-bootstrap'
|
||||||
import DropdownToggleWithTooltip from '../../../../../shared/components/dropdown/dropdown-toggle-with-tooltip'
|
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
|
||||||
import DropdownMenuWithRef from '../../../../../shared/components/dropdown/dropdown-menu-with-ref'
|
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
|
id: string
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
parentSelector?: string
|
parentSelector?: string
|
||||||
|
@ -13,7 +19,7 @@ type DropdownMenuProps = {
|
||||||
setIsOpened: (isOpened: boolean) => void
|
setIsOpened: (isOpened: boolean) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
function ActionsDropdown({
|
function BS3ActionsDropdown({
|
||||||
id,
|
id,
|
||||||
children,
|
children,
|
||||||
parentSelector,
|
parentSelector,
|
||||||
|
@ -21,7 +27,7 @@ function ActionsDropdown({
|
||||||
iconTag,
|
iconTag,
|
||||||
setIsOpened,
|
setIsOpened,
|
||||||
toolTipDescription,
|
toolTipDescription,
|
||||||
}: DropdownMenuProps) {
|
}: ActionDropdownProps) {
|
||||||
const menuRef = useRef<HTMLElement>()
|
const menuRef = useRef<HTMLElement>()
|
||||||
|
|
||||||
// handle the placement of the dropdown above or below the toggle button
|
// handle the placement of the dropdown above or below the toggle button
|
||||||
|
@ -47,14 +53,14 @@ function ActionsDropdown({
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dropdown
|
<BS3Dropdown
|
||||||
id={`history-version-dropdown-${id}`}
|
id={`history-version-dropdown-${id}`}
|
||||||
pullRight
|
pullRight
|
||||||
open={isOpened}
|
open={isOpened}
|
||||||
onToggle={open => setIsOpened(open)}
|
onToggle={open => setIsOpened(open)}
|
||||||
className="pull-right"
|
className="pull-right"
|
||||||
>
|
>
|
||||||
<DropdownToggleWithTooltip
|
<BS3DropdownToggleWithTooltip
|
||||||
bsRole="toggle"
|
bsRole="toggle"
|
||||||
className="history-version-dropdown-menu-btn"
|
className="history-version-dropdown-menu-btn"
|
||||||
isOpened={isOpened}
|
isOpened={isOpened}
|
||||||
|
@ -65,7 +71,7 @@ function ActionsDropdown({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{iconTag}
|
{iconTag}
|
||||||
</DropdownToggleWithTooltip>
|
</BS3DropdownToggleWithTooltip>
|
||||||
<DropdownMenuWithRef
|
<DropdownMenuWithRef
|
||||||
bsRole="menu"
|
bsRole="menu"
|
||||||
className="history-version-dropdown-menu"
|
className="history-version-dropdown-menu"
|
||||||
|
@ -73,8 +79,49 @@ function ActionsDropdown({
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</DropdownMenuWithRef>
|
</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>
|
</Dropdown>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ActionsDropdown(props: ActionDropdownProps) {
|
||||||
|
return (
|
||||||
|
<BootstrapVersionSwitcher
|
||||||
|
bs3={<BS3ActionsDropdown {...props} />}
|
||||||
|
bs5={<BS5ActionsDropdown {...props} />}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export default ActionsDropdown
|
export default ActionsDropdown
|
||||||
|
|
|
@ -40,10 +40,7 @@ function CompareVersionDropdownContentAllHistory({
|
||||||
closeDropdown={closeDropdown}
|
closeDropdown={closeDropdown}
|
||||||
text={t('history_compare_up_to_this_version')}
|
text={t('history_compare_up_to_this_version')}
|
||||||
icon={
|
icon={
|
||||||
<MaterialIcon
|
<MaterialIcon type="align_start" className="history-dropdown-icon" />
|
||||||
type="align_start"
|
|
||||||
className="history-dropdown-icon p-1"
|
|
||||||
/>
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<CompareDropDownItem
|
<CompareDropDownItem
|
||||||
|
@ -56,10 +53,7 @@ function CompareVersionDropdownContentAllHistory({
|
||||||
closeDropdown={closeDropdown}
|
closeDropdown={closeDropdown}
|
||||||
text={t('history_compare_from_this_version')}
|
text={t('history_compare_from_this_version')}
|
||||||
icon={
|
icon={
|
||||||
<MaterialIcon
|
<MaterialIcon type="align_end" className="history-dropdown-icon" />
|
||||||
type="align_end"
|
|
||||||
className="history-dropdown-icon p-1"
|
|
||||||
/>
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
@ -100,10 +94,7 @@ function CompareVersionDropdownContentLabelsList({
|
||||||
closeDropdown={closeDropdownLabels}
|
closeDropdown={closeDropdownLabels}
|
||||||
text={t('history_compare_up_to_this_version')}
|
text={t('history_compare_up_to_this_version')}
|
||||||
icon={
|
icon={
|
||||||
<MaterialIcon
|
<MaterialIcon type="align_start" className="history-dropdown-icon" />
|
||||||
type="align_start"
|
|
||||||
className="history-dropdown-icon p-1"
|
|
||||||
/>
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<CompareDropDownItem
|
<CompareDropDownItem
|
||||||
|
@ -116,10 +107,7 @@ function CompareVersionDropdownContentLabelsList({
|
||||||
closeDropdown={closeDropdownLabels}
|
closeDropdown={closeDropdownLabels}
|
||||||
text={t('history_compare_from_this_version')}
|
text={t('history_compare_from_this_version')}
|
||||||
icon={
|
icon={
|
||||||
<MaterialIcon
|
<MaterialIcon type="align_end" className="history-dropdown-icon" />
|
||||||
type="align_end"
|
|
||||||
className="history-dropdown-icon p-1"
|
|
||||||
/>
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -27,7 +27,7 @@ function CompareVersionDropdown({
|
||||||
<MaterialIcon
|
<MaterialIcon
|
||||||
type="align_space_even"
|
type="align_space_even"
|
||||||
className="history-dropdown-icon"
|
className="history-dropdown-icon"
|
||||||
accessibilityLabel="compare drop down"
|
accessibilityLabel={t('compare')}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import ActionsDropdown from './actions-dropdown'
|
import ActionsDropdown from './actions-dropdown'
|
||||||
import Icon from '../../../../../shared/components/icon'
|
import Icon from '../../../../../shared/components/icon'
|
||||||
import { useTranslation } from 'react-i18next'
|
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 = {
|
type HistoryDropdownProps = {
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
|
@ -23,7 +25,17 @@ function HistoryDropdown({
|
||||||
toolTipDescription={t('more_actions')}
|
toolTipDescription={t('more_actions')}
|
||||||
setIsOpened={setIsOpened}
|
setIsOpened={setIsOpened}
|
||||||
iconTag={
|
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]"
|
parentSelector="[data-history-version-list-container]"
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { MenuItem } from 'react-bootstrap'
|
import OLDropdownMenuItem from '@/features/ui/components/ol/ol-dropdown-menu-item'
|
||||||
import Icon from '../../../../../../shared/components/icon'
|
import OLTagIcon from '@/features/ui/components/ol/icons/ol-tag-icon'
|
||||||
import AddLabelModal from '../../add-label-modal'
|
import AddLabelModal from '../../add-label-modal'
|
||||||
|
|
||||||
type DownloadProps = {
|
type DownloadProps = {
|
||||||
|
@ -26,9 +26,15 @@ function AddLabel({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<MenuItem onClick={handleClick} {...props}>
|
<OLDropdownMenuItem
|
||||||
<Icon type="tag" fw /> {t('history_label_this_version')}
|
onClick={handleClick}
|
||||||
</MenuItem>
|
leadingIcon={<OLTagIcon />}
|
||||||
|
as="button"
|
||||||
|
className="dropdown-item-material-icon-small"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{t('history_label_this_version')}
|
||||||
|
</OLDropdownMenuItem>
|
||||||
<AddLabelModal
|
<AddLabelModal
|
||||||
show={showModal}
|
show={showModal}
|
||||||
setShow={setShowModal}
|
setShow={setShowModal}
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
import Icon from '../../../../../../shared/components/icon'
|
|
||||||
import { useHistoryContext } from '../../../../context/history-context'
|
import { useHistoryContext } from '../../../../context/history-context'
|
||||||
import { UpdateRange } from '../../../../services/types/update'
|
import { UpdateRange } from '../../../../services/types/update'
|
||||||
import { ReactNode } from 'react'
|
import { ReactNode } from 'react'
|
||||||
import { Button, MenuItem } from 'react-bootstrap'
|
import OLDropdownMenuItem from '@/features/ui/components/ol/ol-dropdown-menu-item'
|
||||||
|
|
||||||
type CompareProps = {
|
type CompareProps = {
|
||||||
comparisonRange: UpdateRange
|
comparisonRange: UpdateRange
|
||||||
|
@ -15,12 +14,12 @@ function CompareDropDownItem({
|
||||||
comparisonRange,
|
comparisonRange,
|
||||||
text,
|
text,
|
||||||
closeDropdown,
|
closeDropdown,
|
||||||
icon = <Icon type="exchange" fw />,
|
icon,
|
||||||
...props
|
...props
|
||||||
}: CompareProps) {
|
}: CompareProps) {
|
||||||
const { setSelection } = useHistoryContext()
|
const { setSelection } = useHistoryContext()
|
||||||
|
|
||||||
const handleCompareVersion = (e: React.MouseEvent<Button>) => {
|
const handleCompareVersion = (e: React.MouseEvent) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
closeDropdown()
|
closeDropdown()
|
||||||
|
|
||||||
|
@ -33,10 +32,15 @@ function CompareDropDownItem({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MenuItem {...props} onClick={handleCompareVersion}>
|
<OLDropdownMenuItem
|
||||||
{icon}
|
{...props}
|
||||||
<span className="">{text}</span>
|
leadingIcon={icon}
|
||||||
</MenuItem>
|
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 { useHistoryContext } from '../../../../context/history-context'
|
||||||
import { UpdateRange } from '../../../../services/types/update'
|
import { UpdateRange } from '../../../../services/types/update'
|
||||||
import { ReactNode } from 'react'
|
import { ReactNode } from 'react'
|
||||||
import Tooltip from '../../../../../../shared/components/tooltip'
|
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
|
||||||
import { Button } from 'react-bootstrap'
|
import { bsVersion } from '@/features/utils/bootstrap-5'
|
||||||
|
|
||||||
type CompareProps = {
|
type CompareProps = {
|
||||||
comparisonRange: UpdateRange
|
comparisonRange: UpdateRange
|
||||||
icon?: ReactNode
|
icon: ReactNode
|
||||||
toolTipDescription?: string
|
toolTipDescription?: string
|
||||||
closeDropdown: () => void
|
closeDropdown: () => void
|
||||||
}
|
}
|
||||||
|
@ -16,12 +15,11 @@ function Compare({
|
||||||
comparisonRange,
|
comparisonRange,
|
||||||
closeDropdown,
|
closeDropdown,
|
||||||
toolTipDescription,
|
toolTipDescription,
|
||||||
icon = <Icon type="exchange" fw />,
|
icon,
|
||||||
...props
|
|
||||||
}: CompareProps) {
|
}: CompareProps) {
|
||||||
const { setSelection } = useHistoryContext()
|
const { setSelection } = useHistoryContext()
|
||||||
|
|
||||||
const handleCompareVersion = (e: React.MouseEvent<Button>) => {
|
const handleCompareVersion = (e: { stopPropagation: () => void }) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
closeDropdown()
|
closeDropdown()
|
||||||
|
|
||||||
|
@ -34,20 +32,18 @@ function Compare({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<OLTooltip
|
||||||
description={toolTipDescription}
|
description={toolTipDescription}
|
||||||
id="compare-btn"
|
id="compare-btn"
|
||||||
overlayProps={{ placement: 'left' }}
|
overlayProps={{ placement: 'left' }}
|
||||||
>
|
>
|
||||||
<Button
|
<button className="history-compare-btn" onClick={handleCompareVersion}>
|
||||||
bsStyle={null}
|
<span className={bsVersion({ bs3: 'sr-only', bs5: 'visually-hidden' })}>
|
||||||
className="history-compare-btn"
|
{toolTipDescription}
|
||||||
onClick={handleCompareVersion}
|
</span>
|
||||||
>
|
|
||||||
<span className="sr-only">{toolTipDescription}</span>
|
|
||||||
{icon}
|
{icon}
|
||||||
</Button>
|
</button>
|
||||||
</Tooltip>
|
</OLTooltip>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import { useTranslation } from 'react-i18next'
|
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'
|
import Icon from '../../../../../../shared/components/icon'
|
||||||
|
|
||||||
type DownloadProps = {
|
type DownloadProps = {
|
||||||
|
@ -17,15 +19,21 @@ function Download({
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MenuItem
|
<OLDropdownMenuItem
|
||||||
href={`/project/${projectId}/version/${version}/zip`}
|
href={`/project/${projectId}/version/${version}/zip`}
|
||||||
download={`${projectId}_v${version}.zip`}
|
download={`${projectId}_v${version}.zip`}
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
onClick={closeDropdown}
|
onClick={closeDropdown}
|
||||||
|
leadingIcon={
|
||||||
|
<BootstrapVersionSwitcher
|
||||||
|
bs3={<Icon type="cloud-download" fw />}
|
||||||
|
bs5={<MaterialIcon type="download" />}
|
||||||
|
/>
|
||||||
|
}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<Icon type="cloud-download" fw /> {t('history_download_this_version')}
|
{t('history_download_this_version')}
|
||||||
</MenuItem>
|
</OLDropdownMenuItem>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
import Icon from '@/shared/components/icon'
|
import Icon from '@/shared/components/icon'
|
||||||
import { useCallback, useState } from 'react'
|
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 { useTranslation } from 'react-i18next'
|
||||||
import { RestoreProjectModal } from '../../../diff-view/modals/restore-project-modal'
|
import { RestoreProjectModal } from '../../../diff-view/modals/restore-project-modal'
|
||||||
import { useSplitTestContext } from '@/shared/context/split-test-context'
|
import { useSplitTestContext } from '@/shared/context/split-test-context'
|
||||||
import { useRestoreProject } from '@/features/history/context/hooks/use-restore-project'
|
import { useRestoreProject } from '@/features/history/context/hooks/use-restore-project'
|
||||||
import withErrorBoundary from '@/infrastructure/error-boundary'
|
import withErrorBoundary from '@/infrastructure/error-boundary'
|
||||||
import { RestoreProjectErrorModal } from '../../../diff-view/modals/restore-project-error-modal'
|
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 = {
|
type RestoreProjectProps = {
|
||||||
projectId: string
|
projectId: string
|
||||||
|
@ -44,9 +46,18 @@ const RestoreProject = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<MenuItem onClick={handleClick}>
|
<OLDropdownMenuItem
|
||||||
<Icon type="undo" fw /> {t('restore_project_to_this_version')}
|
as="button"
|
||||||
</MenuItem>
|
leadingIcon={
|
||||||
|
<BootstrapVersionSwitcher
|
||||||
|
bs3={<Icon type="undo" fw />}
|
||||||
|
bs5={<MaterialIcon type="undo" />}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
onClick={handleClick}
|
||||||
|
>
|
||||||
|
{t('restore_project_to_this_version')}
|
||||||
|
</OLDropdownMenuItem>
|
||||||
<RestoreProjectModal
|
<RestoreProjectModal
|
||||||
setShow={setShowModal}
|
setShow={setShowModal}
|
||||||
show={showModal}
|
show={showModal}
|
||||||
|
|
|
@ -35,7 +35,7 @@ function HistoryVersionDetails({
|
||||||
// TODO: Sort out accessibility for this
|
// TODO: Sort out accessibility for this
|
||||||
// eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions
|
// eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions
|
||||||
<div
|
<div
|
||||||
className={classnames('history-version-details clearfix', {
|
className={classnames('history-version-details', {
|
||||||
'history-version-selected':
|
'history-version-selected':
|
||||||
selectionState === 'upperSelected' ||
|
selectionState === 'upperSelected' ||
|
||||||
selectionState === 'lowerSelected' ||
|
selectionState === 'lowerSelected' ||
|
||||||
|
|
|
@ -22,6 +22,7 @@ import { CompareVersionDropdownContentAllHistory } from './dropdown/compare-vers
|
||||||
import FileRestoreChange from './file-restore-change'
|
import FileRestoreChange from './file-restore-change'
|
||||||
import HistoryResyncChange from './history-resync-change'
|
import HistoryResyncChange from './history-resync-change'
|
||||||
import ProjectRestoreChange from './project-restore-change'
|
import ProjectRestoreChange from './project-restore-change'
|
||||||
|
import { bsVersion } from '@/features/utils/bootstrap-5'
|
||||||
|
|
||||||
type HistoryVersionProps = {
|
type HistoryVersionProps = {
|
||||||
update: LoadedUpdate
|
update: LoadedUpdate
|
||||||
|
@ -123,7 +124,10 @@ function HistoryVersion({
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{selectionState !== 'selected' && !faded ? (
|
{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' ? (
|
{selectionState !== 'withinSelected' ? (
|
||||||
<CompareItems
|
<CompareItems
|
||||||
updateRange={updateRange}
|
updateRange={updateRange}
|
||||||
|
|
|
@ -13,6 +13,7 @@ import { ItemSelectionState } from '../../utils/history-details'
|
||||||
import CompareVersionDropdown from './dropdown/compare-version-dropdown'
|
import CompareVersionDropdown from './dropdown/compare-version-dropdown'
|
||||||
import { CompareVersionDropdownContentLabelsList } from './dropdown/compare-version-dropdown-content'
|
import { CompareVersionDropdownContentLabelsList } from './dropdown/compare-version-dropdown-content'
|
||||||
import HistoryDropdownContent from '@/features/history/components/change-list/dropdown/history-dropdown-content'
|
import HistoryDropdownContent from '@/features/history/components/change-list/dropdown/history-dropdown-content'
|
||||||
|
import { bsVersion } from '@/features/utils/bootstrap-5'
|
||||||
|
|
||||||
type LabelListItemProps = {
|
type LabelListItemProps = {
|
||||||
version: Version
|
version: Version
|
||||||
|
@ -96,7 +97,10 @@ function LabelListItem({
|
||||||
) : null}
|
) : null}
|
||||||
</HistoryDropdown>
|
</HistoryDropdown>
|
||||||
{selectionState !== 'selected' ? (
|
{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' ? (
|
{selectionState !== 'withinSelected' ? (
|
||||||
<CompareItems
|
<CompareItems
|
||||||
updateRange={updateRange}
|
updateRange={updateRange}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { useTranslation } from 'react-i18next'
|
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
|
// Using this workaround due to inconsistent and improper error responses from the server
|
||||||
type ModalErrorProps = {
|
type ModalErrorProps = {
|
||||||
|
@ -15,10 +15,22 @@ function ModalError({ error }: ModalErrorProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
if (error.response?.status === 400 && error.data?.message) {
|
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
|
export default ModalError
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
import { useState } from 'react'
|
import { forwardRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Modal } from 'react-bootstrap'
|
import OLModal, {
|
||||||
import Icon from '../../../../shared/components/icon'
|
OLModalBody,
|
||||||
import Tooltip from '../../../../shared/components/tooltip'
|
OLModalFooter,
|
||||||
import AccessibleModal from '../../../../shared/components/accessible-modal'
|
OLModalHeader,
|
||||||
|
OLModalTitle,
|
||||||
|
} from '@/features/ui/components/ol/ol-modal'
|
||||||
|
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
|
||||||
import ModalError from './modal-error'
|
import ModalError from './modal-error'
|
||||||
import useAbortController from '../../../../shared/hooks/use-abort-controller'
|
import useAbortController from '../../../../shared/hooks/use-abort-controller'
|
||||||
import useAsync from '../../../../shared/hooks/use-async'
|
import useAsync from '../../../../shared/hooks/use-async'
|
||||||
|
@ -15,118 +18,127 @@ import { LoadedLabel } from '../../services/types/label'
|
||||||
import { debugConsole } from '@/utils/debugging'
|
import { debugConsole } from '@/utils/debugging'
|
||||||
import { formatTimeBasedOnYear } from '@/features/utils/format-date'
|
import { formatTimeBasedOnYear } from '@/features/utils/format-date'
|
||||||
import { useEditorContext } from '@/shared/context/editor-context'
|
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 = {
|
type TagProps = {
|
||||||
label: LoadedLabel
|
label: LoadedLabel
|
||||||
currentUserId: string
|
currentUserId: string
|
||||||
}
|
}
|
||||||
|
|
||||||
function ChangeTag({ label, currentUserId, ...props }: TagProps) {
|
const ChangeTag = forwardRef<HTMLElement, TagProps>(
|
||||||
const { isProjectOwner } = useEditorContext()
|
({ label, currentUserId, ...props }: TagProps, ref) => {
|
||||||
|
const { isProjectOwner } = useEditorContext()
|
||||||
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [showDeleteModal, setShowDeleteModal] = useState(false)
|
const [showDeleteModal, setShowDeleteModal] = useState(false)
|
||||||
const { projectId } = useHistoryContext()
|
const { projectId } = useHistoryContext()
|
||||||
const { signal } = useAbortController()
|
const { signal } = useAbortController()
|
||||||
const { removeUpdateLabel } = useAddOrRemoveLabels()
|
const { removeUpdateLabel } = useAddOrRemoveLabels()
|
||||||
const { isLoading, isSuccess, isError, error, reset, runAsync } = useAsync()
|
const { isLoading, isSuccess, isError, error, reset, runAsync } = useAsync()
|
||||||
const isPseudoCurrentStateLabel = isPseudoLabel(label)
|
const isPseudoCurrentStateLabel = isPseudoLabel(label)
|
||||||
const isOwnedByCurrentUser = !isPseudoCurrentStateLabel
|
const isOwnedByCurrentUser = !isPseudoCurrentStateLabel
|
||||||
? label.user_id === currentUserId
|
? label.user_id === currentUserId
|
||||||
: null
|
: null
|
||||||
|
|
||||||
const showConfirmationModal = (e: React.MouseEvent) => {
|
const showConfirmationModal = (e: React.MouseEvent) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
setShowDeleteModal(true)
|
setShowDeleteModal(true)
|
||||||
}
|
|
||||||
|
|
||||||
const handleModalExited = () => {
|
|
||||||
if (!isSuccess) return
|
|
||||||
|
|
||||||
if (!isPseudoCurrentStateLabel) {
|
|
||||||
removeUpdateLabel(label)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
reset()
|
const handleModalExited = () => {
|
||||||
}
|
if (!isSuccess) return
|
||||||
|
|
||||||
const localDeleteHandler = () => {
|
if (!isPseudoCurrentStateLabel) {
|
||||||
runAsync(deleteLabel(projectId, label.id, signal))
|
removeUpdateLabel(label)
|
||||||
.then(() => setShowDeleteModal(false))
|
}
|
||||||
.catch(debugConsole.error)
|
|
||||||
}
|
|
||||||
|
|
||||||
const responseError = error as unknown as {
|
reset()
|
||||||
response: Response
|
|
||||||
data?: {
|
|
||||||
message?: string
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const showCloseButton = Boolean(
|
const localDeleteHandler = () => {
|
||||||
(isOwnedByCurrentUser || isProjectOwner) && !isPseudoCurrentStateLabel
|
runAsync(deleteLabel(projectId, label.id, signal))
|
||||||
)
|
.then(() => setShowDeleteModal(false))
|
||||||
|
.catch(debugConsole.error)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
const responseError = error as unknown as {
|
||||||
<>
|
response: Response
|
||||||
<Tag
|
data?: {
|
||||||
prepend={<Icon type="tag" fw />}
|
message?: string
|
||||||
closeBtnProps={
|
}
|
||||||
showCloseButton
|
}
|
||||||
? { 'aria-label': t('delete'), onClick: showConfirmationModal }
|
|
||||||
: undefined
|
const showCloseButton = Boolean(
|
||||||
}
|
(isOwnedByCurrentUser || isProjectOwner) && !isPseudoCurrentStateLabel
|
||||||
className="history-version-badge"
|
)
|
||||||
data-testid="history-version-badge"
|
|
||||||
{...props}
|
return (
|
||||||
>
|
<>
|
||||||
{isPseudoCurrentStateLabel
|
<OLTag
|
||||||
? t('history_label_project_current_state')
|
ref={ref}
|
||||||
: label.comment}
|
prepend={<OLTagIcon />}
|
||||||
</Tag>
|
closeBtnProps={
|
||||||
{!isPseudoCurrentStateLabel && (
|
showCloseButton
|
||||||
<AccessibleModal
|
? { 'aria-label': t('delete'), onClick: showConfirmationModal }
|
||||||
show={showDeleteModal}
|
: undefined
|
||||||
onExited={handleModalExited}
|
}
|
||||||
onHide={() => setShowDeleteModal(false)}
|
className="history-version-badge"
|
||||||
id="delete-history-label"
|
data-testid="history-version-badge"
|
||||||
|
{...props}
|
||||||
>
|
>
|
||||||
<Modal.Header>
|
{isPseudoCurrentStateLabel
|
||||||
<Modal.Title>{t('history_delete_label')}</Modal.Title>
|
? t('history_label_project_current_state')
|
||||||
</Modal.Header>
|
: label.comment}
|
||||||
<Modal.Body>
|
</OLTag>
|
||||||
{isError && <ModalError error={responseError} />}
|
{!isPseudoCurrentStateLabel && (
|
||||||
<p>
|
<OLModal
|
||||||
{t('history_are_you_sure_delete_label')}
|
show={showDeleteModal}
|
||||||
<strong>"{label.comment}"</strong>?
|
onExited={handleModalExited}
|
||||||
</p>
|
onHide={() => setShowDeleteModal(false)}
|
||||||
</Modal.Body>
|
id="delete-history-label"
|
||||||
<Modal.Footer>
|
>
|
||||||
<button
|
<OLModalHeader>
|
||||||
type="button"
|
<OLModalTitle>{t('history_delete_label')}</OLModalTitle>
|
||||||
className="btn btn-secondary"
|
</OLModalHeader>
|
||||||
disabled={isLoading}
|
<OLModalBody>
|
||||||
onClick={() => setShowDeleteModal(false)}
|
{isError && <ModalError error={responseError} />}
|
||||||
>
|
<p>
|
||||||
{t('cancel')}
|
{t('history_are_you_sure_delete_label')}
|
||||||
</button>
|
<strong>"{label.comment}"</strong>?
|
||||||
<button
|
</p>
|
||||||
type="button"
|
</OLModalBody>
|
||||||
className="btn btn-danger"
|
<OLModalFooter>
|
||||||
disabled={isLoading}
|
<OLButton
|
||||||
onClick={localDeleteHandler}
|
variant="secondary"
|
||||||
>
|
disabled={isLoading}
|
||||||
{isLoading
|
onClick={() => setShowDeleteModal(false)}
|
||||||
? t('history_deleting_label')
|
>
|
||||||
: t('history_delete_label')}
|
{t('cancel')}
|
||||||
</button>
|
</OLButton>
|
||||||
</Modal.Footer>
|
<OLButton
|
||||||
</AccessibleModal>
|
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 = {
|
type LabelBadgesProps = {
|
||||||
showTooltip: boolean
|
showTooltip: boolean
|
||||||
|
@ -145,13 +157,14 @@ function TagTooltip({ label, currentUserId, showTooltip }: LabelBadgesProps) {
|
||||||
? currentLabelData.user_display_name
|
? currentLabelData.user_display_name
|
||||||
: t('anonymous')
|
: t('anonymous')
|
||||||
|
|
||||||
return showTooltip && !isPseudoCurrentStateLabel ? (
|
return !isPseudoCurrentStateLabel ? (
|
||||||
<Tooltip
|
<OLTooltip
|
||||||
description={
|
description={
|
||||||
<div className="history-version-label-tooltip">
|
<div className="history-version-label-tooltip">
|
||||||
<div className="history-version-label-tooltip-row">
|
<div className="history-version-label-tooltip-row">
|
||||||
<b className="history-version-label-tooltip-row-comment">
|
<b className="history-version-label-tooltip-row-comment">
|
||||||
<Icon type="tag" fw />
|
<OLTagIcon />
|
||||||
|
|
||||||
{label.comment}
|
{label.comment}
|
||||||
</b>
|
</b>
|
||||||
</div>
|
</div>
|
||||||
|
@ -165,9 +178,10 @@ function TagTooltip({ label, currentUserId, showTooltip }: LabelBadgesProps) {
|
||||||
}
|
}
|
||||||
id={label.id}
|
id={label.id}
|
||||||
overlayProps={{ placement: 'left' }}
|
overlayProps={{ placement: 'left' }}
|
||||||
|
hidden={!showTooltip}
|
||||||
>
|
>
|
||||||
<ChangeTag label={label} currentUserId={currentUserId} />
|
<ChangeTag label={label} currentUserId={currentUserId} />
|
||||||
</Tooltip>
|
</OLTooltip>
|
||||||
) : (
|
) : (
|
||||||
<ChangeTag label={label} currentUserId={currentUserId} />
|
<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'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
export function RestoreProjectErrorModal({
|
export function RestoreProjectErrorModal({
|
||||||
|
@ -9,26 +15,22 @@ export function RestoreProjectErrorModal({
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal show onHide={resetErrorBoundary}>
|
<OLModal show onHide={resetErrorBoundary}>
|
||||||
<Modal.Header closeButton>
|
<OLModalHeader closeButton>
|
||||||
<Modal.Title>
|
<OLModalTitle>
|
||||||
{t('an_error_occured_while_restoring_project')}
|
{t('an_error_occured_while_restoring_project')}
|
||||||
</Modal.Title>
|
</OLModalTitle>
|
||||||
</Modal.Header>
|
</OLModalHeader>
|
||||||
<Modal.Body>
|
<OLModalBody>
|
||||||
{t(
|
{t(
|
||||||
'there_was_a_problem_restoring_the_project_please_try_again_in_a_few_moments_or_contact_us'
|
'there_was_a_problem_restoring_the_project_please_try_again_in_a_few_moments_or_contact_us'
|
||||||
)}
|
)}
|
||||||
</Modal.Body>
|
</OLModalBody>
|
||||||
<Modal.Footer>
|
<OLModalFooter>
|
||||||
<Button
|
<OLButton variant="secondary" onClick={resetErrorBoundary}>
|
||||||
bsStyle={null}
|
|
||||||
className="btn-secondary"
|
|
||||||
onClick={resetErrorBoundary}
|
|
||||||
>
|
|
||||||
{t('close')}
|
{t('close')}
|
||||||
</Button>
|
</OLButton>
|
||||||
</Modal.Footer>
|
</OLModalFooter>
|
||||||
</Modal>
|
</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 { formatDate } from '@/utils/dates'
|
||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
import { Button, Modal } from 'react-bootstrap'
|
import OLButton from '@/features/ui/components/ol/ol-button'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
type RestoreProjectModalProps = {
|
type RestoreProjectModalProps = {
|
||||||
|
@ -26,35 +31,31 @@ export const RestoreProjectModal = ({
|
||||||
}, [setShow])
|
}, [setShow])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AccessibleModal onHide={() => setShow(false)} show={show}>
|
<OLModal onHide={() => setShow(false)} show={show}>
|
||||||
<Modal.Header>
|
<OLModalHeader>
|
||||||
<Modal.Title>{t('restore_this_version')}</Modal.Title>
|
<OLModalTitle>{t('restore_this_version')}</OLModalTitle>
|
||||||
</Modal.Header>
|
</OLModalHeader>
|
||||||
<Modal.Body>
|
<OLModalBody>
|
||||||
<p>
|
<p>
|
||||||
{t('your_current_project_will_revert_to_the_version_from_time', {
|
{t('your_current_project_will_revert_to_the_version_from_time', {
|
||||||
timestamp: formatDate(endTimestamp),
|
timestamp: formatDate(endTimestamp),
|
||||||
})}
|
})}
|
||||||
</p>
|
</p>
|
||||||
</Modal.Body>
|
</OLModalBody>
|
||||||
<Modal.Footer>
|
<OLModalFooter>
|
||||||
<Button
|
<OLButton variant="secondary" onClick={onCancel} disabled={isRestoring}>
|
||||||
className="btn btn-secondary"
|
|
||||||
bsStyle={null}
|
|
||||||
onClick={onCancel}
|
|
||||||
disabled={isRestoring}
|
|
||||||
>
|
|
||||||
{t('cancel')}
|
{t('cancel')}
|
||||||
</Button>
|
</OLButton>
|
||||||
<Button
|
<OLButton
|
||||||
className="btn btn-primary"
|
variant="primary"
|
||||||
bsStyle={null}
|
|
||||||
onClick={onRestore}
|
onClick={onRestore}
|
||||||
disabled={isRestoring}
|
disabled={isRestoring}
|
||||||
|
isLoading={isRestoring}
|
||||||
|
bs3Props={{ loading: isRestoring ? t('restoring') : t('restore') }}
|
||||||
>
|
>
|
||||||
{isRestoring ? t('restoring') : t('restore')}
|
{t('restore')}
|
||||||
</Button>
|
</OLButton>
|
||||||
</Modal.Footer>
|
</OLModalFooter>
|
||||||
</AccessibleModal>
|
</OLModal>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { memo, useCallback, useState } from 'react'
|
import { memo, useCallback, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
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 CloneProjectModal from '../../../../../clone-project-modal/components/clone-project-modal'
|
||||||
import useIsMounted from '../../../../../../shared/hooks/use-is-mounted'
|
import useIsMounted from '../../../../../../shared/hooks/use-is-mounted'
|
||||||
import { useProjectListContext } from '../../../../context/project-list-context'
|
import { useProjectListContext } from '../../../../context/project-list-context'
|
||||||
|
@ -64,9 +64,9 @@ function CopyProjectMenuItem() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<OlDropdownMenuItem onClick={handleOpenModal} as="button" tabIndex={-1}>
|
<OLDropdownMenuItem onClick={handleOpenModal} as="button" tabIndex={-1}>
|
||||||
{t('make_a_copy')}
|
{t('make_a_copy')}
|
||||||
</OlDropdownMenuItem>
|
</OLDropdownMenuItem>
|
||||||
<CloneProjectModal
|
<CloneProjectModal
|
||||||
show={showModal}
|
show={showModal}
|
||||||
handleHide={handleCloseModal}
|
handleHide={handleCloseModal}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { memo, useCallback, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useProjectListContext } from '../../../../context/project-list-context'
|
import { useProjectListContext } from '../../../../context/project-list-context'
|
||||||
import useIsMounted from '../../../../../../shared/hooks/use-is-mounted'
|
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'
|
import RenameProjectModal from '../../../modals/rename-project-modal'
|
||||||
|
|
||||||
function RenameProjectMenuItem() {
|
function RenameProjectMenuItem() {
|
||||||
|
@ -34,9 +34,9 @@ function RenameProjectMenuItem() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<OlDropdownMenuItem onClick={handleOpenModal} as="button" tabIndex={-1}>
|
<OLDropdownMenuItem onClick={handleOpenModal} as="button" tabIndex={-1}>
|
||||||
{t('rename')}
|
{t('rename')}
|
||||||
</OlDropdownMenuItem>
|
</OLDropdownMenuItem>
|
||||||
<RenameProjectModal
|
<RenameProjectModal
|
||||||
handleCloseModal={handleCloseModal}
|
handleCloseModal={handleCloseModal}
|
||||||
showModal={showModal}
|
showModal={showModal}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { forwardRef, SyntheticEvent } from 'react'
|
||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
import RootCloseWrapper from 'react-overlays/lib/RootCloseWrapper'
|
import RootCloseWrapper from 'react-overlays/lib/RootCloseWrapper'
|
||||||
import { DropdownProps } from 'react-bootstrap'
|
import { DropdownProps } from 'react-bootstrap'
|
||||||
import { MergeAndOverride } from '../../../../../types/utils'
|
import { MergeAndOverride } from '../../../../../../types/utils'
|
||||||
|
|
||||||
type DropdownMenuWithRefProps = MergeAndOverride<
|
type DropdownMenuWithRefProps = MergeAndOverride<
|
||||||
Pick<DropdownProps, 'bsClass' | 'open' | 'pullRight' | 'onClose'>,
|
Pick<DropdownProps, 'bsClass' | 'open' | 'pullRight' | 'onClose'>,
|
|
@ -1,8 +1,8 @@
|
||||||
import { forwardRef } from 'react'
|
import { forwardRef } from 'react'
|
||||||
import Tooltip from '../tooltip'
|
import Tooltip from '../../../../shared/components/tooltip'
|
||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
import { DropdownProps } from 'react-bootstrap'
|
import { DropdownProps } from 'react-bootstrap'
|
||||||
import { MergeAndOverride } from '../../../../../types/utils'
|
import { MergeAndOverride } from '../../../../../../types/utils'
|
||||||
|
|
||||||
type CustomToggleProps = MergeAndOverride<
|
type CustomToggleProps = MergeAndOverride<
|
||||||
Pick<DropdownProps, 'bsClass' | 'open'>,
|
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 MaterialIcon from '@/shared/components/material-icon'
|
||||||
import { MergeAndOverride } from '../../../../../../types/utils'
|
import { MergeAndOverride } from '../../../../../../types/utils'
|
||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
|
import { forwardRef } from 'react'
|
||||||
|
|
||||||
type TagProps = MergeAndOverride<
|
type TagProps = MergeAndOverride<
|
||||||
BadgeProps,
|
BadgeProps,
|
||||||
|
@ -13,50 +14,55 @@ type TagProps = MergeAndOverride<
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
||||||
function Tag({
|
const Tag = forwardRef<HTMLElement, TagProps>(
|
||||||
prepend,
|
(
|
||||||
children,
|
{ prepend, children, contentProps, closeBtnProps, className, ...rest },
|
||||||
contentProps,
|
ref
|
||||||
closeBtnProps,
|
) => {
|
||||||
className,
|
const { t } = useTranslation()
|
||||||
...rest
|
|
||||||
}: TagProps) {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
const content = (
|
const content = (
|
||||||
<>
|
<>
|
||||||
{prepend && <span className="badge-prepend">{prepend}</span>}
|
{prepend && <span className="badge-prepend">{prepend}</span>}
|
||||||
<span className="badge-content">{children}</span>
|
<span className="badge-content">{children}</span>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Badge bg="light" className={classnames('badge-tag', className)} {...rest}>
|
<Badge
|
||||||
{contentProps?.onClick ? (
|
ref={ref}
|
||||||
<button
|
bg="light"
|
||||||
type="button"
|
className={classnames('badge-tag', className)}
|
||||||
className="badge-tag-content badge-tag-content-btn"
|
{...rest}
|
||||||
{...contentProps}
|
>
|
||||||
>
|
{contentProps?.onClick ? (
|
||||||
{content}
|
<button
|
||||||
</button>
|
type="button"
|
||||||
) : (
|
className="badge-tag-content badge-tag-content-btn"
|
||||||
<span className="badge-tag-content" {...contentProps}>
|
{...contentProps}
|
||||||
{content}
|
>
|
||||||
</span>
|
{content}
|
||||||
)}
|
</button>
|
||||||
{closeBtnProps && (
|
) : (
|
||||||
<button
|
<span className="badge-tag-content" {...contentProps}>
|
||||||
type="button"
|
{content}
|
||||||
className="badge-close"
|
</span>
|
||||||
aria-label={t('remove_tag', { tagName: children })}
|
)}
|
||||||
{...closeBtnProps}
|
{closeBtnProps && (
|
||||||
>
|
<button
|
||||||
<MaterialIcon className="badge-close-icon" type="close" />
|
type="button"
|
||||||
</button>
|
className="badge-close"
|
||||||
)}
|
aria-label={t('remove_tag', { tagName: children })}
|
||||||
</Badge>
|
{...closeBtnProps}
|
||||||
)
|
>
|
||||||
}
|
<MaterialIcon className="badge-close-icon" type="close" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</Badge>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
Tag.displayName = 'Tag'
|
||||||
|
|
||||||
export default 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 { DropdownItemProps } from '@/features/ui/components/types/dropdown-menu-props'
|
||||||
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
|
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
|
||||||
|
|
||||||
type OlDropdownMenuItemProps = DropdownItemProps & {
|
type OLDropdownMenuItemProps = DropdownItemProps & {
|
||||||
bs3Props?: MenuItemProps
|
bs3Props?: MenuItemProps
|
||||||
}
|
}
|
||||||
|
|
||||||
function OlDropdownMenuItem(props: OlDropdownMenuItemProps) {
|
function OLDropdownMenuItem(props: OLDropdownMenuItemProps) {
|
||||||
const { bs3Props, ...rest } = props
|
const { bs3Props, ...rest } = props
|
||||||
|
|
||||||
const bs3MenuItemProps: MenuItemProps = {
|
const bs3MenuItemProps: MenuItemProps = {
|
||||||
children: rest.children,
|
children: rest.leadingIcon ? (
|
||||||
|
<>
|
||||||
|
{rest.leadingIcon}
|
||||||
|
|
||||||
|
{rest.children}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
rest.children
|
||||||
|
),
|
||||||
onClick: rest.onClick,
|
onClick: rest.onClick,
|
||||||
|
href: rest.href,
|
||||||
|
download: rest.download,
|
||||||
...bs3Props,
|
...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,
|
bsSize: bs5Props.size,
|
||||||
show: bs5Props.show,
|
show: bs5Props.show,
|
||||||
onHide: bs5Props.onHide,
|
onHide: bs5Props.onHide,
|
||||||
|
onExited: bs5Props.onExited,
|
||||||
backdrop: bs5Props.backdrop,
|
backdrop: bs5Props.backdrop,
|
||||||
animation: bs5Props.animation,
|
animation: bs5Props.animation,
|
||||||
id: bs5Props.id,
|
id: bs5Props.id,
|
||||||
|
@ -86,10 +87,15 @@ export function OLModalTitle({ children, ...props }: OLModalTitleProps) {
|
||||||
const bs3ModalProps: BS3ModalTitleProps = {
|
const bs3ModalProps: BS3ModalTitleProps = {
|
||||||
componentClass: bs5Props.as,
|
componentClass: bs5Props.as,
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BootstrapVersionSwitcher
|
<BootstrapVersionSwitcher
|
||||||
bs3={<BS3Modal.Title {...bs3ModalProps}>{children}</BS3Modal.Title>}
|
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) {
|
for (const placement of bs3PlacementOptions) {
|
||||||
if (placement === bs5Props.placement) {
|
// BS5 has more placement options than BS3, such as "left-start", so these are mapped to "left" etc.
|
||||||
bs3OverlayProps.placement = bs5Props.placement
|
if (bs5Props.placement.startsWith(placement)) {
|
||||||
|
bs3OverlayProps.placement = placement
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ import {
|
||||||
} from 'react-bootstrap'
|
} from 'react-bootstrap'
|
||||||
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
|
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
|
||||||
|
|
||||||
type OLPopoverProps = PopoverProps & {
|
type OLPopoverProps = Omit<PopoverProps, 'title'> & {
|
||||||
title?: React.ReactNode
|
title?: React.ReactNode
|
||||||
bs3Props?: BS3PopoverProps
|
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 Tag from '@/features/ui/components/bootstrap-5/tag'
|
||||||
import BS3Tag from '@/shared/components/tag'
|
import BS3Tag from '@/shared/components/tag'
|
||||||
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
|
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> & {
|
type OLTagProps = React.ComponentProps<typeof Tag> & {
|
||||||
bs3Props?: React.ComponentProps<typeof BS3Tag>
|
bs3Props?: React.ComponentProps<typeof BS3Tag>
|
||||||
}
|
}
|
||||||
|
|
||||||
function OLTag(props: OLTagProps) {
|
const OLTag = forwardRef<HTMLElement, OLTagProps>((props: OLTagProps, ref) => {
|
||||||
const { bs3Props, ...rest } = props
|
const { bs3Props, ...rest } = props
|
||||||
|
|
||||||
const bs3TagProps: React.ComponentProps<typeof BS3Tag> = {
|
const bs3TagProps: React.ComponentProps<typeof BS3Tag> = {
|
||||||
|
@ -14,15 +16,23 @@ function OLTag(props: OLTagProps) {
|
||||||
prepend: rest.prepend,
|
prepend: rest.prepend,
|
||||||
closeBtnProps: rest.closeBtnProps,
|
closeBtnProps: rest.closeBtnProps,
|
||||||
className: rest.className,
|
className: rest.className,
|
||||||
|
onClick: rest.onClick,
|
||||||
|
onFocus: rest.onFocus,
|
||||||
|
onBlur: rest.onBlur,
|
||||||
|
onMouseOver: rest.onMouseOver,
|
||||||
|
onMouseOut: rest.onMouseOut,
|
||||||
|
...getAriaAndDataProps(rest),
|
||||||
...bs3Props,
|
...bs3Props,
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BootstrapVersionSwitcher
|
<BootstrapVersionSwitcher
|
||||||
bs3={<BS3Tag {...bs3TagProps} />}
|
bs3={<BS3Tag {...bs3TagProps} />}
|
||||||
bs5={<Tag {...rest} />}
|
bs5={<Tag ref={ref} {...rest} />}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
|
||||||
|
OLTag.displayName = 'OLTag'
|
||||||
|
|
||||||
export default OLTag
|
export default OLTag
|
||||||
|
|
|
@ -42,6 +42,8 @@ export type DropdownItemProps = PropsWithChildren<{
|
||||||
role?: string
|
role?: string
|
||||||
tabIndex?: number
|
tabIndex?: number
|
||||||
target?: string
|
target?: string
|
||||||
|
download?: boolean | string
|
||||||
|
rel?: string
|
||||||
}>
|
}>
|
||||||
|
|
||||||
export type DropdownToggleProps = PropsWithChildren<{
|
export type DropdownToggleProps = PropsWithChildren<{
|
||||||
|
@ -53,6 +55,7 @@ export type DropdownToggleProps = PropsWithChildren<{
|
||||||
variant?: SplitButtonVariants
|
variant?: SplitButtonVariants
|
||||||
as?: ElementType
|
as?: ElementType
|
||||||
size?: 'sm' | 'lg' | undefined
|
size?: 'sm' | 'lg' | undefined
|
||||||
|
'aria-label'?: string
|
||||||
}>
|
}>
|
||||||
|
|
||||||
export type DropdownMenuProps = PropsWithChildren<{
|
export type DropdownMenuProps = PropsWithChildren<{
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import Icon from './icon'
|
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
|
import OLSpinner, {
|
||||||
import { Spinner } from 'react-bootstrap-5'
|
OLSpinnerSize,
|
||||||
|
} from '@/features/ui/components/ol/ol-spinner'
|
||||||
|
import { isBootstrap5 } from '@/features/utils/bootstrap-5'
|
||||||
import { setTimeout } from '@/utils/window'
|
import { setTimeout } from '@/utils/window'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
|
|
||||||
|
@ -10,12 +11,14 @@ function LoadingSpinner({
|
||||||
align,
|
align,
|
||||||
delay = 0,
|
delay = 0,
|
||||||
loadingText,
|
loadingText,
|
||||||
size,
|
size = 'sm',
|
||||||
|
className,
|
||||||
}: {
|
}: {
|
||||||
align?: 'left' | 'center'
|
align?: 'left' | 'center'
|
||||||
delay?: 0 | 500 // 500 is our standard delay
|
delay?: 0 | 500 // 500 is our standard delay
|
||||||
loadingText?: string
|
loadingText?: string
|
||||||
size?: 'sm'
|
size?: OLSpinnerSize
|
||||||
|
className?: string
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
@ -35,32 +38,19 @@ function LoadingSpinner({
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const alignmentClass =
|
const extraClasses = isBootstrap5()
|
||||||
align === 'left' ? 'align-items-start' : 'align-items-center'
|
? [
|
||||||
|
'd-inline-flex',
|
||||||
|
align === 'left' ? 'align-items-start' : 'align-items-center',
|
||||||
|
]
|
||||||
|
: null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BootstrapVersionSwitcher
|
<div className={classNames('loading', className, extraClasses)}>
|
||||||
bs3={
|
<OLSpinner size={size} />
|
||||||
<div className="loading">
|
|
||||||
<Icon type="refresh" fw spin />
|
{loadingText || t('loading')}…
|
||||||
|
</div>
|
||||||
{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>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,14 +60,16 @@ export function FullSizeLoadingSpinner({
|
||||||
delay = 0,
|
delay = 0,
|
||||||
minHeight,
|
minHeight,
|
||||||
loadingText,
|
loadingText,
|
||||||
|
size = 'sm',
|
||||||
}: {
|
}: {
|
||||||
delay?: 0 | 500
|
delay?: 0 | 500
|
||||||
minHeight?: string
|
minHeight?: string
|
||||||
loadingText?: string
|
loadingText?: string
|
||||||
|
size?: OLSpinnerSize
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="full-size-loading-spinner-container" style={{ minHeight }}>
|
<div className="full-size-loading-spinner-container" style={{ minHeight }}>
|
||||||
<LoadingSpinner loadingText={loadingText} delay={delay} />
|
<LoadingSpinner size={size} loadingText={loadingText} delay={delay} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,9 @@ function SystemMessage({ id, children }: SystemMessageProps) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li className="system-message">
|
<li className="system-message">
|
||||||
{id !== 'protected' ? <Close onDismiss={() => setHidden(true)} /> : null}
|
{id !== 'protected' ? (
|
||||||
|
<Close onDismiss={() => setHidden(true)} variant="dark" />
|
||||||
|
) : null}
|
||||||
{children}
|
{children}
|
||||||
</li>
|
</li>
|
||||||
)
|
)
|
||||||
|
|
|
@ -5,6 +5,7 @@ import {
|
||||||
DropdownHeader,
|
DropdownHeader,
|
||||||
} from '@/features/ui/components/bootstrap-5/dropdown-menu'
|
} from '@/features/ui/components/bootstrap-5/dropdown-menu'
|
||||||
import type { Meta } from '@storybook/react'
|
import type { Meta } from '@storybook/react'
|
||||||
|
import OLDropdownMenuItem from '@/features/ui/components/ol/ol-dropdown-menu-item'
|
||||||
|
|
||||||
type Args = React.ComponentProps<typeof DropdownMenu>
|
type Args = React.ComponentProps<typeof DropdownMenu>
|
||||||
|
|
||||||
|
@ -141,42 +142,52 @@ export const Icon = (args: Args) => {
|
||||||
return (
|
return (
|
||||||
<DropdownMenu show>
|
<DropdownMenu show>
|
||||||
<li>
|
<li>
|
||||||
<DropdownItem
|
<OLDropdownMenuItem
|
||||||
disabled={args.disabled}
|
disabled={args.disabled}
|
||||||
eventKey="1"
|
eventKey="1"
|
||||||
href="#/action-1"
|
href="#/action-1"
|
||||||
leadingIcon="view_column_2"
|
leadingIcon="view_column_2"
|
||||||
>
|
>
|
||||||
Editor & PDF
|
Editor & PDF
|
||||||
</DropdownItem>
|
</OLDropdownMenuItem>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<DropdownItem
|
<OLDropdownMenuItem
|
||||||
active
|
active
|
||||||
eventKey="2"
|
eventKey="2"
|
||||||
href="#/action-2"
|
href="#/action-2"
|
||||||
leadingIcon="terminal"
|
leadingIcon="terminal"
|
||||||
>
|
>
|
||||||
Editor only
|
Editor only
|
||||||
</DropdownItem>
|
</OLDropdownMenuItem>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<DropdownItem
|
<OLDropdownMenuItem
|
||||||
eventKey="2"
|
eventKey="3"
|
||||||
href="#/action-2"
|
href="#/action-3"
|
||||||
leadingIcon="picture_as_pdf"
|
leadingIcon="picture_as_pdf"
|
||||||
>
|
>
|
||||||
PDF only
|
PDF only
|
||||||
</DropdownItem>
|
</OLDropdownMenuItem>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<DropdownItem
|
<OLDropdownMenuItem
|
||||||
eventKey="2"
|
eventKey="4"
|
||||||
href="#/action-2"
|
href="#/action-4"
|
||||||
leadingIcon="select_window"
|
leadingIcon="select_window"
|
||||||
>
|
>
|
||||||
PDF in separate tab
|
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>
|
</li>
|
||||||
</DropdownMenu>
|
</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 BS3Tag from '@/shared/components/tag'
|
||||||
import type { Meta, StoryObj } from '@storybook/react'
|
import type { Meta, StoryObj } from '@storybook/react'
|
||||||
|
|
||||||
|
@ -47,7 +47,7 @@ export const TagPrepend: Story = {
|
||||||
render: args => {
|
render: args => {
|
||||||
return (
|
return (
|
||||||
<div className="small">
|
<div className="small">
|
||||||
<BS3Tag prepend={<Icon type="tag" fw />} {...args} />
|
<BS3Tag prepend={<OLTagIcon />} {...args} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
@ -58,7 +58,7 @@ export const TagWithCloseButton: Story = {
|
||||||
return (
|
return (
|
||||||
<div className="small">
|
<div className="small">
|
||||||
<BS3Tag
|
<BS3Tag
|
||||||
prepend={<Icon type="tag" fw />}
|
prepend={<OLTagIcon />}
|
||||||
closeBtnProps={{
|
closeBtnProps={{
|
||||||
onClick: () => alert('Close triggered!'),
|
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 Tag from '@/features/ui/components/bootstrap-5/tag'
|
||||||
import type { Meta, StoryObj } from '@storybook/react'
|
import type { Meta, StoryObj } from '@storybook/react'
|
||||||
|
|
||||||
|
@ -41,7 +41,7 @@ export const TagDefault: Story = {
|
||||||
|
|
||||||
export const TagPrepend: Story = {
|
export const TagPrepend: Story = {
|
||||||
render: args => {
|
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 => {
|
render: args => {
|
||||||
return (
|
return (
|
||||||
<Tag
|
<Tag
|
||||||
prepend={<Icon type="tag" fw />}
|
prepend={<OLTagIcon />}
|
||||||
closeBtnProps={{
|
closeBtnProps={{
|
||||||
onClick: () => alert('Close triggered!'),
|
onClick: () => alert('Close triggered!'),
|
||||||
}}
|
}}
|
||||||
|
@ -63,7 +63,7 @@ export const TagWithContentButtonAndCloseButton: Story = {
|
||||||
render: args => {
|
render: args => {
|
||||||
return (
|
return (
|
||||||
<Tag
|
<Tag
|
||||||
prepend={<Icon type="tag" fw />}
|
prepend={<OLTagIcon />}
|
||||||
contentProps={{
|
contentProps={{
|
||||||
onClick: () => alert('Content button clicked!'),
|
onClick: () => alert('Content button clicked!'),
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -44,9 +44,14 @@ history-root {
|
||||||
.doc-container {
|
.doc-container {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
display: flex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.doc-container .loading {
|
||||||
|
margin: 10rem auto auto;
|
||||||
|
}
|
||||||
|
|
||||||
.change-list {
|
.change-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -102,6 +107,7 @@ history-root {
|
||||||
}
|
}
|
||||||
|
|
||||||
.history-version-details {
|
.history-version-details {
|
||||||
|
display: flow-root;
|
||||||
padding-top: 8px;
|
padding-top: 8px;
|
||||||
padding-bottom: 8px;
|
padding-bottom: 8px;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -240,20 +246,15 @@ history-root {
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading {
|
.loading {
|
||||||
padding-top: 10rem;
|
|
||||||
font-family: @font-family-serif;
|
font-family: @font-family-serif;
|
||||||
text-align: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
& > .loading {
|
.history-all-versions-loading {
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.history-all-versions-scroller .loading {
|
|
||||||
position: sticky;
|
position: sticky;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
padding: @line-height-computed / 2 0;
|
padding: @line-height-computed / 2 0;
|
||||||
background-color: @gray-lightest;
|
background-color: @gray-lightest;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.history-version-saved-by {
|
.history-version-saved-by {
|
||||||
|
@ -271,8 +272,11 @@ history-root {
|
||||||
|
|
||||||
.history-compare-btn,
|
.history-compare-btn,
|
||||||
.history-version-dropdown-menu-btn {
|
.history-version-dropdown-menu-btn {
|
||||||
|
.reset-button;
|
||||||
|
|
||||||
@size: 30px;
|
@size: 30px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
border-radius: @btn-border-radius-large;
|
||||||
width: @size;
|
width: @size;
|
||||||
height: @size;
|
height: @size;
|
||||||
line-height: 1;
|
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() {
|
@mixin reset-button() {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
@ -104,3 +118,28 @@
|
||||||
transparent
|
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-color: var(--link-ui);
|
||||||
--link-hover-color: var(--link-ui-hover);
|
--link-hover-color: var(--link-ui-hover);
|
||||||
--link-visited-color: var(--link-ui-visited);
|
--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 {
|
a {
|
||||||
|
|
|
@ -74,10 +74,6 @@ samp {
|
||||||
list-style-image: url('../../../../public/img/fa-check-green.svg');
|
list-style-image: url('../../../../public/img/fa-check-green.svg');
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-center {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-muted {
|
.text-muted {
|
||||||
color: var(--content-disabled) !important;
|
color: var(--content-disabled) !important;
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,3 +29,4 @@
|
||||||
@import 'pagination';
|
@import 'pagination';
|
||||||
@import 'loading-spinner';
|
@import 'loading-spinner';
|
||||||
@import 'error-boundary';
|
@import 'error-boundary';
|
||||||
|
@import 'close-button';
|
||||||
|
|
|
@ -16,6 +16,10 @@ $max-width: 160px;
|
||||||
margin-right: var(--spacing-02);
|
margin-right: var(--spacing-02);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
|
.material-symbols {
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge-close {
|
.badge-close {
|
||||||
|
|
|
@ -164,14 +164,14 @@
|
||||||
|
|
||||||
.loading-spinner-small {
|
.loading-spinner-small {
|
||||||
border-width: 0.2em;
|
border-width: 0.2em;
|
||||||
height: 20px;
|
height: 1.25rem;
|
||||||
width: 20px;
|
width: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading-spinner-large {
|
.loading-spinner-large {
|
||||||
border-width: 0.2em;
|
border-width: 0.2em;
|
||||||
height: 24px;
|
height: 1.5rem;
|
||||||
width: 24px;
|
width: 1.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -187,11 +187,11 @@
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
.icon-small {
|
.icon-small {
|
||||||
font-size: 20px;
|
font-size: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-large {
|
.icon-large {
|
||||||
font-size: 24px;
|
font-size: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.spinner {
|
.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 {
|
.dropdown-item-highlighted {
|
||||||
background-color: var(--bg-light-secondary);
|
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 {
|
.full-size-loading-spinner-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
.popover {
|
.popover {
|
||||||
@include shadow-md;
|
@include shadow-md;
|
||||||
|
@include dark-bg;
|
||||||
|
|
||||||
line-height: var(--line-height-02);
|
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/figure-modal';
|
||||||
@import 'editor/review-panel';
|
@import 'editor/review-panel';
|
||||||
@import 'editor/chat';
|
@import 'editor/chat';
|
||||||
|
@import 'editor/history';
|
||||||
@import 'subscription';
|
@import 'subscription';
|
||||||
@import 'editor/pdf';
|
@import 'editor/pdf';
|
||||||
@import 'editor/compile-button';
|
@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 {
|
||||||
|
--toggle-switch-height: 26px;
|
||||||
|
--toggle-switch-padding: var(--spacing-01);
|
||||||
|
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 26px;
|
height: var(--toggle-switch-height);
|
||||||
margin-right: var(--spacing-03);
|
margin-right: var(--spacing-03);
|
||||||
border-radius: var(--border-radius-full);
|
border-radius: var(--border-radius-full);
|
||||||
background-color: var(--neutral-20);
|
background-color: var(--neutral-20);
|
||||||
padding: var(--spacing-01);
|
padding: var(--toggle-switch-padding);
|
||||||
}
|
}
|
||||||
|
|
||||||
.toggle-switch-label {
|
.toggle-switch-label {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
float: left;
|
float: left;
|
||||||
font-weight: normal;
|
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;
|
text-align: center;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
|
@ -657,17 +657,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-table-button-toggle {
|
.dropdown-table-button-toggle {
|
||||||
padding: var(--spacing-04);
|
@include action-button;
|
||||||
font-size: 0;
|
|
||||||
line-height: 1;
|
|
||||||
border-radius: 50%;
|
|
||||||
color: var(--content-primary);
|
|
||||||
background-color: transparent;
|
|
||||||
|
|
||||||
&:hover,
|
padding: var(--spacing-04);
|
||||||
&:active {
|
|
||||||
background-color: rgba($neutral-90, 0.08);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,3 +5,8 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: 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 = {
|
const scope = {
|
||||||
ui: { view: 'history', pdfLayout: 'sideBySide', chatOpen: true },
|
ui: { view: 'history', pdfLayout: 'sideBySide', chatOpen: true },
|
||||||
}
|
}
|
||||||
|
@ -363,7 +363,7 @@ describe('change list', function () {
|
||||||
cy.findAllByTestId('history-version-details')
|
cy.findAllByTestId('history-version-details')
|
||||||
.eq(1)
|
.eq(1)
|
||||||
.within(() => {
|
.within(() => {
|
||||||
cy.findByRole('button', { name: /compare drop down/i }).click()
|
cy.findByRole('button', { name: /compare/i }).click()
|
||||||
cy.findByRole('menu').within(() => {
|
cy.findByRole('menu').within(() => {
|
||||||
cy.findByRole('menuitem', {
|
cy.findByRole('menuitem', {
|
||||||
name: /compare up to this version/i,
|
name: /compare up to this version/i,
|
||||||
|
|
Loading…
Reference in a new issue