diff --git a/services/web/.stylelintrc.json b/services/web/.stylelintrc.json index a601a209e2..09ff0b393f 100644 --- a/services/web/.stylelintrc.json +++ b/services/web/.stylelintrc.json @@ -4,6 +4,7 @@ "function-url-quotes": null, "no-descending-specificity": null, "scss/at-extend-no-missing-placeholder": null, - "scss/operator-no-newline-after": null + "scss/operator-no-newline-after": null, + "property-no-vendor-prefix": [true, { "ignoreProperties": ["mask-image"] }] } } diff --git a/services/web/frontend/js/features/history/components/change-list/add-label-modal.tsx b/services/web/frontend/js/features/history/components/change-list/add-label-modal.tsx index 92cc4f35de..8658dae246 100644 --- a/services/web/frontend/js/features/history/components/change-list/add-label-modal.tsx +++ b/services/web/frontend/js/features/history/components/change-list/add-label-modal.tsx @@ -1,8 +1,15 @@ import { useTranslation } from 'react-i18next' import { useEffect, useState } from 'react' -import { Modal, FormGroup, FormControl } from 'react-bootstrap' +import OLForm from '@/features/ui/components/ol/ol-form' +import OLFormGroup from '@/features/ui/components/ol/ol-form-group' import ModalError from './modal-error' -import AccessibleModal from '../../../../shared/components/accessible-modal' +import OLModal, { + OLModalBody, + OLModalFooter, + OLModalHeader, + OLModalTitle, +} from '@/features/ui/components/ol/ol-modal' +import OLButton from '@/features/ui/components/ol/ol-button' import useAsync from '../../../../shared/hooks/use-async' import useAbortController from '../../../../shared/hooks/use-abort-controller' import useAddOrRemoveLabels from '../../hooks/use-add-or-remove-labels' @@ -11,6 +18,7 @@ import { addLabel } from '../../services/api' import { Label } from '../../services/types/label' import { useRefWithAutoFocus } from '../../../../shared/hooks/use-ref-with-auto-focus' import { debugConsole } from '@/utils/debugging' +import OLFormControl from '@/features/ui/components/ol/ol-form-control' type AddLabelModalProps = { show: boolean @@ -71,51 +79,55 @@ function AddLabelModal({ show, setShow, version }: AddLabelModalProps) { } return ( - setShow(false)} id="add-history-label" > - - {t('history_add_label')} - -
- + + {t('history_add_label')} + + + {isError && } - - + - ) => setComment(e.target.value)} + onChange={(e: React.ChangeEvent) => + setComment(e.target.value) + } /> - - - - - - -
-
+ {t('history_add_label')} + + + + ) } diff --git a/services/web/frontend/js/features/history/components/change-list/all-history-list.tsx b/services/web/frontend/js/features/history/components/change-list/all-history-list.tsx index 2c0abd1a2f..2442b0206c 100644 --- a/services/web/frontend/js/features/history/components/change-list/all-history-list.tsx +++ b/services/web/frontend/js/features/history/components/change-list/all-history-list.tsx @@ -8,7 +8,8 @@ import { useUserContext } from '../../../../shared/context/user-context' import useDropdownActiveItem from '../../hooks/use-dropdown-active-item' import { useHistoryContext } from '../../context/history-context' import { useEditorContext } from '../../../../shared/context/editor-context' -import { Overlay, Popover } from 'react-bootstrap' +import OLPopover from '@/features/ui/components/ol/ol-popover' +import OLOverlay from '@/features/ui/components/ol/ol-overlay' import Close from '@/shared/components/close' import { Trans, useTranslation } from 'react-i18next' import MaterialIcon from '@/shared/components/material-icon' @@ -173,18 +174,29 @@ function AllHistoryList() { if (showHistoryTutorial) { popover = ( - - {t('react_history_tutorial_title')}{' '} @@ -212,23 +224,23 @@ function AllHistoryList() { , // eslint-disable-line jsx-a11y/anchor-has-content, react/jsx-key ]} /> - - + + ) } else if (showRestorePromo) { popover = ( - - {t('history_restore_promo_title')} @@ -255,8 +267,8 @@ function AllHistoryList() { />, ]} /> - - + + ) } @@ -330,7 +342,9 @@ function AllHistoryList() { {showNonOwnerPaywall ? : null} {updatesLoadingState === 'loadingInitial' || updatesLoadingState === 'loadingUpdates' ? ( - +
+ +
) : null} ) diff --git a/services/web/frontend/js/features/history/components/change-list/dropdown/actions-dropdown.tsx b/services/web/frontend/js/features/history/components/change-list/dropdown/actions-dropdown.tsx index e59a7b57ae..3bbab1479d 100644 --- a/services/web/frontend/js/features/history/components/change-list/dropdown/actions-dropdown.tsx +++ b/services/web/frontend/js/features/history/components/change-list/dropdown/actions-dropdown.tsx @@ -1,9 +1,15 @@ -import { useRef, useEffect, ReactNode } from 'react' -import { Dropdown } from 'react-bootstrap' -import DropdownToggleWithTooltip from '../../../../../shared/components/dropdown/dropdown-toggle-with-tooltip' -import DropdownMenuWithRef from '../../../../../shared/components/dropdown/dropdown-menu-with-ref' +import React, { useRef, useEffect, ReactNode } from 'react' +import { Dropdown as BS3Dropdown } from 'react-bootstrap' +import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher' +import { + Dropdown, + DropdownMenu, +} from '@/features/ui/components/bootstrap-5/dropdown-menu' +import BS3DropdownToggleWithTooltip from '../../../../ui/components/bootstrap-3/dropdown-toggle-with-tooltip' +import BS5DropdownToggleWithTooltip from '@/features/ui/components/bootstrap-5/dropdown-toggle-with-tooltip' +import DropdownMenuWithRef from '../../../../ui/components/bootstrap-3/dropdown-menu-with-ref' -type DropdownMenuProps = { +type ActionDropdownProps = { id: string children: React.ReactNode parentSelector?: string @@ -13,7 +19,7 @@ type DropdownMenuProps = { setIsOpened: (isOpened: boolean) => void } -function ActionsDropdown({ +function BS3ActionsDropdown({ id, children, parentSelector, @@ -21,7 +27,7 @@ function ActionsDropdown({ iconTag, setIsOpened, toolTipDescription, -}: DropdownMenuProps) { +}: ActionDropdownProps) { const menuRef = useRef() // handle the placement of the dropdown above or below the toggle button @@ -47,14 +53,14 @@ function ActionsDropdown({ }) return ( - setIsOpened(open)} className="pull-right" > - {iconTag} - + {children} + + ) +} + +function BS5ActionsDropdown({ + id, + children, + isOpened, + iconTag, + setIsOpened, + toolTipDescription, +}: Omit) { + return ( + setIsOpened(open)} + > + + {iconTag} + + + {children} + ) } +function ActionsDropdown(props: ActionDropdownProps) { + return ( + } + bs5={} + /> + ) +} + export default ActionsDropdown diff --git a/services/web/frontend/js/features/history/components/change-list/dropdown/compare-version-dropdown-content.tsx b/services/web/frontend/js/features/history/components/change-list/dropdown/compare-version-dropdown-content.tsx index 30aba06693..6e5f809720 100644 --- a/services/web/frontend/js/features/history/components/change-list/dropdown/compare-version-dropdown-content.tsx +++ b/services/web/frontend/js/features/history/components/change-list/dropdown/compare-version-dropdown-content.tsx @@ -40,10 +40,7 @@ function CompareVersionDropdownContentAllHistory({ closeDropdown={closeDropdown} text={t('history_compare_up_to_this_version')} icon={ - + } /> + } /> @@ -100,10 +94,7 @@ function CompareVersionDropdownContentLabelsList({ closeDropdown={closeDropdownLabels} text={t('history_compare_up_to_this_version')} icon={ - + } /> + } /> diff --git a/services/web/frontend/js/features/history/components/change-list/dropdown/compare-version-dropdown.tsx b/services/web/frontend/js/features/history/components/change-list/dropdown/compare-version-dropdown.tsx index f06aa7aefe..6cf2d54f1f 100644 --- a/services/web/frontend/js/features/history/components/change-list/dropdown/compare-version-dropdown.tsx +++ b/services/web/frontend/js/features/history/components/change-list/dropdown/compare-version-dropdown.tsx @@ -27,7 +27,7 @@ function CompareVersionDropdown({ } > diff --git a/services/web/frontend/js/features/history/components/change-list/dropdown/history-dropdown.tsx b/services/web/frontend/js/features/history/components/change-list/dropdown/history-dropdown.tsx index 419af3355b..8b6fde8bef 100644 --- a/services/web/frontend/js/features/history/components/change-list/dropdown/history-dropdown.tsx +++ b/services/web/frontend/js/features/history/components/change-list/dropdown/history-dropdown.tsx @@ -1,6 +1,8 @@ import ActionsDropdown from './actions-dropdown' import Icon from '../../../../../shared/components/icon' import { useTranslation } from 'react-i18next' +import MaterialIcon from '@/shared/components/material-icon' +import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher' type HistoryDropdownProps = { children: React.ReactNode @@ -23,7 +25,17 @@ function HistoryDropdown({ toolTipDescription={t('more_actions')} setIsOpened={setIsOpened} iconTag={ - + + } + bs5={ + + } + /> } parentSelector="[data-history-version-list-container]" > diff --git a/services/web/frontend/js/features/history/components/change-list/dropdown/menu-item/add-label.tsx b/services/web/frontend/js/features/history/components/change-list/dropdown/menu-item/add-label.tsx index 8bc4f9228d..882bb9a439 100644 --- a/services/web/frontend/js/features/history/components/change-list/dropdown/menu-item/add-label.tsx +++ b/services/web/frontend/js/features/history/components/change-list/dropdown/menu-item/add-label.tsx @@ -1,7 +1,7 @@ import { useState } from 'react' import { useTranslation } from 'react-i18next' -import { MenuItem } from 'react-bootstrap' -import Icon from '../../../../../../shared/components/icon' +import OLDropdownMenuItem from '@/features/ui/components/ol/ol-dropdown-menu-item' +import OLTagIcon from '@/features/ui/components/ol/icons/ol-tag-icon' import AddLabelModal from '../../add-label-modal' type DownloadProps = { @@ -26,9 +26,15 @@ function AddLabel({ return ( <> - - {t('history_label_this_version')} - + } + as="button" + className="dropdown-item-material-icon-small" + {...props} + > + {t('history_label_this_version')} + , + icon, ...props }: CompareProps) { const { setSelection } = useHistoryContext() - const handleCompareVersion = (e: React.MouseEvent - + + ) } diff --git a/services/web/frontend/js/features/history/components/change-list/dropdown/menu-item/download.tsx b/services/web/frontend/js/features/history/components/change-list/dropdown/menu-item/download.tsx index 70f79eff8b..fc3e1a2d5f 100644 --- a/services/web/frontend/js/features/history/components/change-list/dropdown/menu-item/download.tsx +++ b/services/web/frontend/js/features/history/components/change-list/dropdown/menu-item/download.tsx @@ -1,5 +1,7 @@ import { useTranslation } from 'react-i18next' -import { MenuItem } from 'react-bootstrap' +import OLDropdownMenuItem from '@/features/ui/components/ol/ol-dropdown-menu-item' +import MaterialIcon from '@/shared/components/material-icon' +import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher' import Icon from '../../../../../../shared/components/icon' type DownloadProps = { @@ -17,15 +19,21 @@ function Download({ const { t } = useTranslation() return ( - } + bs5={} + /> + } {...props} > - {t('history_download_this_version')} - + {t('history_download_this_version')} + ) } diff --git a/services/web/frontend/js/features/history/components/change-list/dropdown/menu-item/restore-project.tsx b/services/web/frontend/js/features/history/components/change-list/dropdown/menu-item/restore-project.tsx index 411a7a52c3..dad0f47cd0 100644 --- a/services/web/frontend/js/features/history/components/change-list/dropdown/menu-item/restore-project.tsx +++ b/services/web/frontend/js/features/history/components/change-list/dropdown/menu-item/restore-project.tsx @@ -1,12 +1,14 @@ import Icon from '@/shared/components/icon' import { useCallback, useState } from 'react' -import { MenuItem } from 'react-bootstrap' +import OLDropdownMenuItem from '@/features/ui/components/ol/ol-dropdown-menu-item' import { useTranslation } from 'react-i18next' import { RestoreProjectModal } from '../../../diff-view/modals/restore-project-modal' import { useSplitTestContext } from '@/shared/context/split-test-context' import { useRestoreProject } from '@/features/history/context/hooks/use-restore-project' import withErrorBoundary from '@/infrastructure/error-boundary' import { RestoreProjectErrorModal } from '../../../diff-view/modals/restore-project-error-modal' +import MaterialIcon from '@/shared/components/material-icon' +import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher' type RestoreProjectProps = { projectId: string @@ -44,9 +46,18 @@ const RestoreProject = ({ return ( <> - - {t('restore_project_to_this_version')} - + } + bs5={} + /> + } + onClick={handleClick} + > + {t('restore_project_to_this_version')} + +
{selectionState !== 'withinSelected' ? ( {selectionState !== 'selected' ? ( -
+
{selectionState !== 'withinSelected' ? ( {error.data.message} + return ( + + ) } - return {t('generic_something_went_wrong')} + return ( + + ) } export default ModalError diff --git a/services/web/frontend/js/features/history/components/change-list/tag-tooltip.tsx b/services/web/frontend/js/features/history/components/change-list/tag-tooltip.tsx index afe9c8fc16..19b9c547ce 100644 --- a/services/web/frontend/js/features/history/components/change-list/tag-tooltip.tsx +++ b/services/web/frontend/js/features/history/components/change-list/tag-tooltip.tsx @@ -1,9 +1,12 @@ -import { useState } from 'react' +import { forwardRef, useState } from 'react' import { useTranslation } from 'react-i18next' -import { Modal } from 'react-bootstrap' -import Icon from '../../../../shared/components/icon' -import Tooltip from '../../../../shared/components/tooltip' -import AccessibleModal from '../../../../shared/components/accessible-modal' +import OLModal, { + OLModalBody, + OLModalFooter, + OLModalHeader, + OLModalTitle, +} from '@/features/ui/components/ol/ol-modal' +import OLTooltip from '@/features/ui/components/ol/ol-tooltip' import ModalError from './modal-error' import useAbortController from '../../../../shared/hooks/use-abort-controller' import useAsync from '../../../../shared/hooks/use-async' @@ -15,118 +18,127 @@ import { LoadedLabel } from '../../services/types/label' import { debugConsole } from '@/utils/debugging' import { formatTimeBasedOnYear } from '@/features/utils/format-date' import { useEditorContext } from '@/shared/context/editor-context' -import Tag from '@/shared/components/tag' +import OLTag from '@/features/ui/components/ol/ol-tag' +import OLButton from '@/features/ui/components/ol/ol-button' +import OLTagIcon from '@/features/ui/components/ol/icons/ol-tag-icon' type TagProps = { label: LoadedLabel currentUserId: string } -function ChangeTag({ label, currentUserId, ...props }: TagProps) { - const { isProjectOwner } = useEditorContext() +const ChangeTag = forwardRef( + ({ label, currentUserId, ...props }: TagProps, ref) => { + const { isProjectOwner } = useEditorContext() - const { t } = useTranslation() - const [showDeleteModal, setShowDeleteModal] = useState(false) - const { projectId } = useHistoryContext() - const { signal } = useAbortController() - const { removeUpdateLabel } = useAddOrRemoveLabels() - const { isLoading, isSuccess, isError, error, reset, runAsync } = useAsync() - const isPseudoCurrentStateLabel = isPseudoLabel(label) - const isOwnedByCurrentUser = !isPseudoCurrentStateLabel - ? label.user_id === currentUserId - : null + const { t } = useTranslation() + const [showDeleteModal, setShowDeleteModal] = useState(false) + const { projectId } = useHistoryContext() + const { signal } = useAbortController() + const { removeUpdateLabel } = useAddOrRemoveLabels() + const { isLoading, isSuccess, isError, error, reset, runAsync } = useAsync() + const isPseudoCurrentStateLabel = isPseudoLabel(label) + const isOwnedByCurrentUser = !isPseudoCurrentStateLabel + ? label.user_id === currentUserId + : null - const showConfirmationModal = (e: React.MouseEvent) => { - e.stopPropagation() - setShowDeleteModal(true) - } - - const handleModalExited = () => { - if (!isSuccess) return - - if (!isPseudoCurrentStateLabel) { - removeUpdateLabel(label) + const showConfirmationModal = (e: React.MouseEvent) => { + e.stopPropagation() + setShowDeleteModal(true) } - reset() - } + const handleModalExited = () => { + if (!isSuccess) return - const localDeleteHandler = () => { - runAsync(deleteLabel(projectId, label.id, signal)) - .then(() => setShowDeleteModal(false)) - .catch(debugConsole.error) - } + if (!isPseudoCurrentStateLabel) { + removeUpdateLabel(label) + } - const responseError = error as unknown as { - response: Response - data?: { - message?: string + reset() } - } - const showCloseButton = Boolean( - (isOwnedByCurrentUser || isProjectOwner) && !isPseudoCurrentStateLabel - ) + const localDeleteHandler = () => { + runAsync(deleteLabel(projectId, label.id, signal)) + .then(() => setShowDeleteModal(false)) + .catch(debugConsole.error) + } - return ( - <> - } - closeBtnProps={ - showCloseButton - ? { 'aria-label': t('delete'), onClick: showConfirmationModal } - : undefined - } - className="history-version-badge" - data-testid="history-version-badge" - {...props} - > - {isPseudoCurrentStateLabel - ? t('history_label_project_current_state') - : label.comment} - - {!isPseudoCurrentStateLabel && ( - setShowDeleteModal(false)} - id="delete-history-label" + const responseError = error as unknown as { + response: Response + data?: { + message?: string + } + } + + const showCloseButton = Boolean( + (isOwnedByCurrentUser || isProjectOwner) && !isPseudoCurrentStateLabel + ) + + return ( + <> + } + closeBtnProps={ + showCloseButton + ? { 'aria-label': t('delete'), onClick: showConfirmationModal } + : undefined + } + className="history-version-badge" + data-testid="history-version-badge" + {...props} > - - {t('history_delete_label')} - - - {isError && } -

- {t('history_are_you_sure_delete_label')}  - "{label.comment}"? -

-
- - - - -
- )} - - ) -} + {isPseudoCurrentStateLabel + ? t('history_label_project_current_state') + : label.comment} + + {!isPseudoCurrentStateLabel && ( + setShowDeleteModal(false)} + id="delete-history-label" + > + + {t('history_delete_label')} + + + {isError && } +

+ {t('history_are_you_sure_delete_label')}  + "{label.comment}"? +

+
+ + setShowDeleteModal(false)} + > + {t('cancel')} + + + {t('history_delete_label')} + + +
+ )} + + ) + } +) + +ChangeTag.displayName = 'ChangeTag' type LabelBadgesProps = { showTooltip: boolean @@ -145,13 +157,14 @@ function TagTooltip({ label, currentUserId, showTooltip }: LabelBadgesProps) { ? currentLabelData.user_display_name : t('anonymous') - return showTooltip && !isPseudoCurrentStateLabel ? ( -
- + +   {label.comment}
@@ -165,9 +178,10 @@ function TagTooltip({ label, currentUserId, showTooltip }: LabelBadgesProps) { } id={label.id} overlayProps={{ placement: 'left' }} + hidden={!showTooltip} > -
+ ) : ( ) diff --git a/services/web/frontend/js/features/history/components/diff-view/modals/restore-project-error-modal.tsx b/services/web/frontend/js/features/history/components/diff-view/modals/restore-project-error-modal.tsx index c7a91fd96e..d03124a7b6 100644 --- a/services/web/frontend/js/features/history/components/diff-view/modals/restore-project-error-modal.tsx +++ b/services/web/frontend/js/features/history/components/diff-view/modals/restore-project-error-modal.tsx @@ -1,4 +1,10 @@ -import { Button, Modal } from 'react-bootstrap' +import OLModal, { + OLModalBody, + OLModalFooter, + OLModalHeader, + OLModalTitle, +} from '@/features/ui/components/ol/ol-modal' +import OLButton from '@/features/ui/components/ol/ol-button' import { useTranslation } from 'react-i18next' export function RestoreProjectErrorModal({ @@ -9,26 +15,22 @@ export function RestoreProjectErrorModal({ const { t } = useTranslation() return ( - - - + + + {t('an_error_occured_while_restoring_project')} - - - + + + {t( 'there_was_a_problem_restoring_the_project_please_try_again_in_a_few_moments_or_contact_us' )} - - - - - + + + ) } diff --git a/services/web/frontend/js/features/history/components/diff-view/modals/restore-project-modal.tsx b/services/web/frontend/js/features/history/components/diff-view/modals/restore-project-modal.tsx index 1a59ec2a61..b9eb1ea088 100644 --- a/services/web/frontend/js/features/history/components/diff-view/modals/restore-project-modal.tsx +++ b/services/web/frontend/js/features/history/components/diff-view/modals/restore-project-modal.tsx @@ -1,7 +1,12 @@ -import AccessibleModal from '@/shared/components/accessible-modal' +import OLModal, { + OLModalBody, + OLModalFooter, + OLModalHeader, + OLModalTitle, +} from '@/features/ui/components/ol/ol-modal' import { formatDate } from '@/utils/dates' import { useCallback } from 'react' -import { Button, Modal } from 'react-bootstrap' +import OLButton from '@/features/ui/components/ol/ol-button' import { useTranslation } from 'react-i18next' type RestoreProjectModalProps = { @@ -26,35 +31,31 @@ export const RestoreProjectModal = ({ }, [setShow]) return ( - setShow(false)} show={show}> - - {t('restore_this_version')} - - + setShow(false)} show={show}> + + {t('restore_this_version')} + +

{t('your_current_project_will_revert_to_the_version_from_time', { timestamp: formatDate(endTimestamp), })}

-
- - - - -
+ {t('restore')} + + + ) } diff --git a/services/web/frontend/js/features/project-list/components/table/project-tools/menu-items/copy-project-menu-item.tsx b/services/web/frontend/js/features/project-list/components/table/project-tools/menu-items/copy-project-menu-item.tsx index 66784772eb..37a662bb27 100644 --- a/services/web/frontend/js/features/project-list/components/table/project-tools/menu-items/copy-project-menu-item.tsx +++ b/services/web/frontend/js/features/project-list/components/table/project-tools/menu-items/copy-project-menu-item.tsx @@ -1,6 +1,6 @@ import { memo, useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' -import OlDropdownMenuItem from '@/features/ui/components/ol/ol-dropdown-menu-item' +import OLDropdownMenuItem from '@/features/ui/components/ol/ol-dropdown-menu-item' import CloneProjectModal from '../../../../../clone-project-modal/components/clone-project-modal' import useIsMounted from '../../../../../../shared/hooks/use-is-mounted' import { useProjectListContext } from '../../../../context/project-list-context' @@ -64,9 +64,9 @@ function CopyProjectMenuItem() { return ( <> - + {t('make_a_copy')} - + - + {t('rename')} - + , diff --git a/services/web/frontend/js/shared/components/dropdown/dropdown-toggle-with-tooltip.tsx b/services/web/frontend/js/features/ui/components/bootstrap-3/dropdown-toggle-with-tooltip.tsx similarity index 89% rename from services/web/frontend/js/shared/components/dropdown/dropdown-toggle-with-tooltip.tsx rename to services/web/frontend/js/features/ui/components/bootstrap-3/dropdown-toggle-with-tooltip.tsx index f71d4fd6fa..9f0dedf622 100644 --- a/services/web/frontend/js/shared/components/dropdown/dropdown-toggle-with-tooltip.tsx +++ b/services/web/frontend/js/features/ui/components/bootstrap-3/dropdown-toggle-with-tooltip.tsx @@ -1,8 +1,8 @@ import { forwardRef } from 'react' -import Tooltip from '../tooltip' +import Tooltip from '../../../../shared/components/tooltip' import classnames from 'classnames' import { DropdownProps } from 'react-bootstrap' -import { MergeAndOverride } from '../../../../../types/utils' +import { MergeAndOverride } from '../../../../../../types/utils' type CustomToggleProps = MergeAndOverride< Pick, diff --git a/services/web/frontend/js/features/ui/components/bootstrap-5/dropdown-toggle-with-tooltip.tsx b/services/web/frontend/js/features/ui/components/bootstrap-5/dropdown-toggle-with-tooltip.tsx new file mode 100644 index 0000000000..f2ba81fbad --- /dev/null +++ b/services/web/frontend/js/features/ui/components/bootstrap-5/dropdown-toggle-with-tooltip.tsx @@ -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 + toolTipDescription: string + tooltipProps?: Omit, 'children'> + 'aria-label'?: string + } +> +const DropdownToggleWithTooltip = forwardRef< + BsPrefixRefForwardingComponent<'button', DropdownToggleProps>, + DropdownToggleWithTooltipProps +>( + ( + { + children, + toolTipDescription, + overlayTriggerProps, + tooltipProps, + id, + ...toggleProps + }, + ref + ) => { + return ( + {toolTipDescription}} + {...overlayTriggerProps} + > + + {children} + + + ) + } +) + +DropdownToggleWithTooltip.displayName = 'DropdownToggleWithTooltip' + +export default DropdownToggleWithTooltip diff --git a/services/web/frontend/js/features/ui/components/bootstrap-5/tag.tsx b/services/web/frontend/js/features/ui/components/bootstrap-5/tag.tsx index 4663591f40..8fede7f50d 100644 --- a/services/web/frontend/js/features/ui/components/bootstrap-5/tag.tsx +++ b/services/web/frontend/js/features/ui/components/bootstrap-5/tag.tsx @@ -3,6 +3,7 @@ import { Badge, BadgeProps } from 'react-bootstrap-5' import MaterialIcon from '@/shared/components/material-icon' import { MergeAndOverride } from '../../../../../../types/utils' import classnames from 'classnames' +import { forwardRef } from 'react' type TagProps = MergeAndOverride< BadgeProps, @@ -13,50 +14,55 @@ type TagProps = MergeAndOverride< } > -function Tag({ - prepend, - children, - contentProps, - closeBtnProps, - className, - ...rest -}: TagProps) { - const { t } = useTranslation() +const Tag = forwardRef( + ( + { prepend, children, contentProps, closeBtnProps, className, ...rest }, + ref + ) => { + const { t } = useTranslation() - const content = ( - <> - {prepend && {prepend}} - {children} - - ) + const content = ( + <> + {prepend && {prepend}} + {children} + + ) - return ( - - {contentProps?.onClick ? ( - - ) : ( - - {content} - - )} - {closeBtnProps && ( - - )} - - ) -} + return ( + + {contentProps?.onClick ? ( + + ) : ( + + {content} + + )} + {closeBtnProps && ( + + )} + + ) + } +) + +Tag.displayName = 'Tag' export default Tag diff --git a/services/web/frontend/js/features/ui/components/ol/icons/ol-tag-icon.tsx b/services/web/frontend/js/features/ui/components/ol/icons/ol-tag-icon.tsx new file mode 100644 index 0000000000..8e81387bbe --- /dev/null +++ b/services/web/frontend/js/features/ui/components/ol/icons/ol-tag-icon.tsx @@ -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 ( + } + bs5={} + /> + ) +} diff --git a/services/web/frontend/js/features/ui/components/ol/ol-dropdown-menu-item.tsx b/services/web/frontend/js/features/ui/components/ol/ol-dropdown-menu-item.tsx index ee5114eab2..3b68faf57e 100644 --- a/services/web/frontend/js/features/ui/components/ol/ol-dropdown-menu-item.tsx +++ b/services/web/frontend/js/features/ui/components/ol/ol-dropdown-menu-item.tsx @@ -3,16 +3,26 @@ import { DropdownItem } from '@/features/ui/components/bootstrap-5/dropdown-menu import { DropdownItemProps } from '@/features/ui/components/types/dropdown-menu-props' import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher' -type OlDropdownMenuItemProps = DropdownItemProps & { +type OLDropdownMenuItemProps = DropdownItemProps & { bs3Props?: MenuItemProps } -function OlDropdownMenuItem(props: OlDropdownMenuItemProps) { +function OLDropdownMenuItem(props: OLDropdownMenuItemProps) { const { bs3Props, ...rest } = props const bs3MenuItemProps: MenuItemProps = { - children: rest.children, + children: rest.leadingIcon ? ( + <> + {rest.leadingIcon} +   + {rest.children} + + ) : ( + rest.children + ), onClick: rest.onClick, + href: rest.href, + download: rest.download, ...bs3Props, } @@ -24,4 +34,4 @@ function OlDropdownMenuItem(props: OlDropdownMenuItemProps) { ) } -export default OlDropdownMenuItem +export default OLDropdownMenuItem diff --git a/services/web/frontend/js/features/ui/components/ol/ol-modal.tsx b/services/web/frontend/js/features/ui/components/ol/ol-modal.tsx index fb25cc5628..43dcddcf87 100644 --- a/services/web/frontend/js/features/ui/components/ol/ol-modal.tsx +++ b/services/web/frontend/js/features/ui/components/ol/ol-modal.tsx @@ -47,6 +47,7 @@ export default function OLModal({ children, ...props }: OLModalProps) { bsSize: bs5Props.size, show: bs5Props.show, onHide: bs5Props.onHide, + onExited: bs5Props.onExited, backdrop: bs5Props.backdrop, animation: bs5Props.animation, id: bs5Props.id, @@ -86,10 +87,15 @@ export function OLModalTitle({ children, ...props }: OLModalTitleProps) { const bs3ModalProps: BS3ModalTitleProps = { componentClass: bs5Props.as, } + return ( {children}} - bs5={{children}} + bs5={ + + {children} + + } /> ) } diff --git a/services/web/frontend/js/features/ui/components/ol/ol-overlay.tsx b/services/web/frontend/js/features/ui/components/ol/ol-overlay.tsx index 3fa2d3e34a..40b7872bc8 100644 --- a/services/web/frontend/js/features/ui/components/ol/ol-overlay.tsx +++ b/services/web/frontend/js/features/ui/components/ol/ol-overlay.tsx @@ -40,8 +40,9 @@ function OLOverlay(props: OLOverlayProps) { > for (const placement of bs3PlacementOptions) { - if (placement === bs5Props.placement) { - bs3OverlayProps.placement = bs5Props.placement + // BS5 has more placement options than BS3, such as "left-start", so these are mapped to "left" etc. + if (bs5Props.placement.startsWith(placement)) { + bs3OverlayProps.placement = placement break } } diff --git a/services/web/frontend/js/features/ui/components/ol/ol-popover.tsx b/services/web/frontend/js/features/ui/components/ol/ol-popover.tsx index 7e6d1feeca..fbf74d3225 100644 --- a/services/web/frontend/js/features/ui/components/ol/ol-popover.tsx +++ b/services/web/frontend/js/features/ui/components/ol/ol-popover.tsx @@ -6,7 +6,7 @@ import { } from 'react-bootstrap' import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher' -type OLPopoverProps = PopoverProps & { +type OLPopoverProps = Omit & { title?: React.ReactNode bs3Props?: BS3PopoverProps } diff --git a/services/web/frontend/js/features/ui/components/ol/ol-spinner.tsx b/services/web/frontend/js/features/ui/components/ol/ol-spinner.tsx new file mode 100644 index 0000000000..615f5acd47 --- /dev/null +++ b/services/web/frontend/js/features/ui/components/ol/ol-spinner.tsx @@ -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 ( + + } + bs5={ +
- } - bs5={ -
-
- } - /> +
+ +   + {loadingText || t('loading')}… +
) } @@ -70,14 +60,16 @@ export function FullSizeLoadingSpinner({ delay = 0, minHeight, loadingText, + size = 'sm', }: { delay?: 0 | 500 minHeight?: string loadingText?: string + size?: OLSpinnerSize }) { return (
- +
) } diff --git a/services/web/frontend/js/shared/components/system-message.tsx b/services/web/frontend/js/shared/components/system-message.tsx index 8a59c42921..09fe0983d9 100644 --- a/services/web/frontend/js/shared/components/system-message.tsx +++ b/services/web/frontend/js/shared/components/system-message.tsx @@ -18,7 +18,9 @@ function SystemMessage({ id, children }: SystemMessageProps) { return (
  • - {id !== 'protected' ? setHidden(true)} /> : null} + {id !== 'protected' ? ( + setHidden(true)} variant="dark" /> + ) : null} {children}
  • ) diff --git a/services/web/frontend/stories/ui/dropdown-menu.stories.tsx b/services/web/frontend/stories/ui/dropdown-menu.stories.tsx index 6766b6905d..f3d9003f98 100644 --- a/services/web/frontend/stories/ui/dropdown-menu.stories.tsx +++ b/services/web/frontend/stories/ui/dropdown-menu.stories.tsx @@ -5,6 +5,7 @@ import { DropdownHeader, } from '@/features/ui/components/bootstrap-5/dropdown-menu' import type { Meta } from '@storybook/react' +import OLDropdownMenuItem from '@/features/ui/components/ol/ol-dropdown-menu-item' type Args = React.ComponentProps @@ -141,42 +142,52 @@ export const Icon = (args: Args) => { return (
  • - Editor & PDF - +
  • - Editor only - +
  • - PDF only - +
  • - PDF in separate tab - + +
  • +
  • + + Small icon +
  • ) diff --git a/services/web/frontend/stories/ui/tag-bs3.stories.tsx b/services/web/frontend/stories/ui/tag-bs3.stories.tsx index 5f7a661755..8281f05650 100644 --- a/services/web/frontend/stories/ui/tag-bs3.stories.tsx +++ b/services/web/frontend/stories/ui/tag-bs3.stories.tsx @@ -1,4 +1,4 @@ -import Icon from '@/shared/components/icon' +import OLTagIcon from '@/features/ui/components/ol/icons/ol-tag-icon' import BS3Tag from '@/shared/components/tag' import type { Meta, StoryObj } from '@storybook/react' @@ -47,7 +47,7 @@ export const TagPrepend: Story = { render: args => { return (
    - } {...args} /> + } {...args} />
    ) }, @@ -58,7 +58,7 @@ export const TagWithCloseButton: Story = { return (
    } + prepend={} closeBtnProps={{ onClick: () => alert('Close triggered!'), }} diff --git a/services/web/frontend/stories/ui/tag-bs5.stories.tsx b/services/web/frontend/stories/ui/tag-bs5.stories.tsx index 1bcd160896..e4f09c033d 100644 --- a/services/web/frontend/stories/ui/tag-bs5.stories.tsx +++ b/services/web/frontend/stories/ui/tag-bs5.stories.tsx @@ -1,4 +1,4 @@ -import Icon from '@/shared/components/icon' +import OLTagIcon from '@/features/ui/components/ol/icons/ol-tag-icon' import Tag from '@/features/ui/components/bootstrap-5/tag' import type { Meta, StoryObj } from '@storybook/react' @@ -41,7 +41,7 @@ export const TagDefault: Story = { export const TagPrepend: Story = { render: args => { - return } {...args} /> + return } {...args} /> }, } @@ -49,7 +49,7 @@ export const TagWithCloseButton: Story = { render: args => { return ( } + prepend={} closeBtnProps={{ onClick: () => alert('Close triggered!'), }} @@ -63,7 +63,7 @@ export const TagWithContentButtonAndCloseButton: Story = { render: args => { return ( } + prepend={} contentProps={{ onClick: () => alert('Content button clicked!'), }} diff --git a/services/web/frontend/stylesheets/app/editor/history-react.less b/services/web/frontend/stylesheets/app/editor/history-react.less index f000c3b6b6..0917155254 100644 --- a/services/web/frontend/stylesheets/app/editor/history-react.less +++ b/services/web/frontend/stylesheets/app/editor/history-react.less @@ -44,9 +44,14 @@ history-root { .doc-container { flex: 1; overflow-y: auto; + display: flex; } } + .doc-container .loading { + margin: 10rem auto auto; + } + .change-list { display: flex; flex-direction: column; @@ -102,6 +107,7 @@ history-root { } .history-version-details { + display: flow-root; padding-top: 8px; padding-bottom: 8px; position: relative; @@ -240,20 +246,15 @@ history-root { } .loading { - padding-top: 10rem; font-family: @font-family-serif; - text-align: center; } - & > .loading { - flex: 1; - } - - .history-all-versions-scroller .loading { + .history-all-versions-loading { position: sticky; bottom: 0; padding: @line-height-computed / 2 0; background-color: @gray-lightest; + text-align: center; } .history-version-saved-by { @@ -271,8 +272,11 @@ history-root { .history-compare-btn, .history-version-dropdown-menu-btn { + .reset-button; + @size: 30px; padding: 0; + border-radius: @btn-border-radius-large; width: @size; height: @size; line-height: 1; diff --git a/services/web/frontend/stylesheets/bootstrap-5/abstracts/mixins.scss b/services/web/frontend/stylesheets/bootstrap-5/abstracts/mixins.scss index 9747dd7027..af899105eb 100644 --- a/services/web/frontend/stylesheets/bootstrap-5/abstracts/mixins.scss +++ b/services/web/frontend/stylesheets/bootstrap-5/abstracts/mixins.scss @@ -45,6 +45,20 @@ } } +@mixin action-button { + font-size: 0; + line-height: 1; + border-radius: 50%; + color: var(--content-primary); + background-color: transparent; + + &:hover, + &:active, + &[aria-expanded='true'] { + background-color: rgb($neutral-90, 0.08); + } +} + @mixin reset-button() { padding: 0; cursor: pointer; @@ -104,3 +118,28 @@ transparent ); } + +@mixin mask-image($gradient) { + // mask-image isn't supported without the -webkit prefix on all browsers we support yet + -webkit-mask-image: $gradient; + mask-image: $gradient; +} + +@mixin premium-background { + background-image: var(--premium-gradient); +} + +@mixin premium-text { + @include premium-background; + + background-clip: text; + -webkit-text-fill-color: transparent; + color: var(--white); // Fallback + background-color: var(--blue-70); // Fallback +} + +@mixin dark-bg { + --link-color: var(--link-color-dark); + --link-hover-color: var(--link-hover-color-dark); + --link-visited-color: var(--link-visited-color-dark); +} diff --git a/services/web/frontend/stylesheets/bootstrap-5/base/links.scss b/services/web/frontend/stylesheets/bootstrap-5/base/links.scss index c24d974366..db53d4d8f7 100644 --- a/services/web/frontend/stylesheets/bootstrap-5/base/links.scss +++ b/services/web/frontend/stylesheets/bootstrap-5/base/links.scss @@ -5,6 +5,9 @@ --link-color: var(--link-ui); --link-hover-color: var(--link-ui-hover); --link-visited-color: var(--link-ui-visited); + --link-color-dark: var(--link-ui-dark); + --link-hover-color-dark: var(--link-ui-hover-dark); + --link-visited-color-dark: var(--link-ui-visited-dark); } a { diff --git a/services/web/frontend/stylesheets/bootstrap-5/base/typography.scss b/services/web/frontend/stylesheets/bootstrap-5/base/typography.scss index fb434edb3f..5b2adf6587 100644 --- a/services/web/frontend/stylesheets/bootstrap-5/base/typography.scss +++ b/services/web/frontend/stylesheets/bootstrap-5/base/typography.scss @@ -74,10 +74,6 @@ samp { list-style-image: url('../../../../public/img/fa-check-green.svg'); } -.text-center { - text-align: center; -} - .text-muted { color: var(--content-disabled) !important; } diff --git a/services/web/frontend/stylesheets/bootstrap-5/components/all.scss b/services/web/frontend/stylesheets/bootstrap-5/components/all.scss index 9b4c5b4d9e..2e4824a2e1 100644 --- a/services/web/frontend/stylesheets/bootstrap-5/components/all.scss +++ b/services/web/frontend/stylesheets/bootstrap-5/components/all.scss @@ -29,3 +29,4 @@ @import 'pagination'; @import 'loading-spinner'; @import 'error-boundary'; +@import 'close-button'; diff --git a/services/web/frontend/stylesheets/bootstrap-5/components/badge.scss b/services/web/frontend/stylesheets/bootstrap-5/components/badge.scss index 0c1178dd50..1b7534c125 100644 --- a/services/web/frontend/stylesheets/bootstrap-5/components/badge.scss +++ b/services/web/frontend/stylesheets/bootstrap-5/components/badge.scss @@ -16,6 +16,10 @@ $max-width: 160px; margin-right: var(--spacing-02); display: flex; align-items: center; + + .material-symbols { + font-size: inherit; + } } .badge-close { diff --git a/services/web/frontend/stylesheets/bootstrap-5/components/button.scss b/services/web/frontend/stylesheets/bootstrap-5/components/button.scss index 19cf06865a..286a46b8f8 100644 --- a/services/web/frontend/stylesheets/bootstrap-5/components/button.scss +++ b/services/web/frontend/stylesheets/bootstrap-5/components/button.scss @@ -164,14 +164,14 @@ .loading-spinner-small { border-width: 0.2em; - height: 20px; - width: 20px; + height: 1.25rem; + width: 1.25rem; } .loading-spinner-large { border-width: 0.2em; - height: 24px; - width: 24px; + height: 1.5rem; + width: 1.5rem; } } @@ -187,11 +187,11 @@ justify-content: center; .icon-small { - font-size: 20px; + font-size: 1.25rem; } .icon-large { - font-size: 24px; + font-size: 1.5rem; } .spinner { diff --git a/services/web/frontend/stylesheets/bootstrap-5/components/close-button.scss b/services/web/frontend/stylesheets/bootstrap-5/components/close-button.scss new file mode 100644 index 0000000000..bb3e78f3f7 --- /dev/null +++ b/services/web/frontend/stylesheets/bootstrap-5/components/close-button.scss @@ -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); + } +} diff --git a/services/web/frontend/stylesheets/bootstrap-5/components/dropdown-menu.scss b/services/web/frontend/stylesheets/bootstrap-5/components/dropdown-menu.scss index d8b30248de..3dc5777795 100644 --- a/services/web/frontend/stylesheets/bootstrap-5/components/dropdown-menu.scss +++ b/services/web/frontend/stylesheets/bootstrap-5/components/dropdown-menu.scss @@ -167,3 +167,15 @@ .dropdown-item-highlighted { background-color: var(--bg-light-secondary); } + +.dropdown-item-material-icon-small { + .material-symbols, + &.material-symbols { + font-size: var(--bs-body-font-size); + + // Centre the symbol in a 20px-by-20px box + width: 20px; + line-height: 20px; + text-align: center; + } +} diff --git a/services/web/frontend/stylesheets/bootstrap-5/components/loading-spinner.scss b/services/web/frontend/stylesheets/bootstrap-5/components/loading-spinner.scss index a92aae35d3..3c92ce6c35 100644 --- a/services/web/frontend/stylesheets/bootstrap-5/components/loading-spinner.scss +++ b/services/web/frontend/stylesheets/bootstrap-5/components/loading-spinner.scss @@ -1,3 +1,17 @@ +.loading { + .spinner-border-sm, + .spinner-border { + // Ensure the thickness of the spinner is independent of the font size of its container + font-size: var(--font-size-03); + } + + // Adjust the small spinner to be 25% larger than Bootstrap's default in each dimension + .spinner-border-sm { + --bs-spinner-width: 1.25rem; + --bs-spinner-height: 1.25rem; + } +} + .full-size-loading-spinner-container { width: 100%; height: 100%; diff --git a/services/web/frontend/stylesheets/bootstrap-5/components/popover.scss b/services/web/frontend/stylesheets/bootstrap-5/components/popover.scss index d1bacede62..af951e9510 100644 --- a/services/web/frontend/stylesheets/bootstrap-5/components/popover.scss +++ b/services/web/frontend/stylesheets/bootstrap-5/components/popover.scss @@ -1,5 +1,6 @@ .popover { @include shadow-md; + @include dark-bg; line-height: var(--line-height-02); } diff --git a/services/web/frontend/stylesheets/bootstrap-5/components/system-messages.scss b/services/web/frontend/stylesheets/bootstrap-5/components/system-messages.scss index f244107521..13455447d7 100644 --- a/services/web/frontend/stylesheets/bootstrap-5/components/system-messages.scss +++ b/services/web/frontend/stylesheets/bootstrap-5/components/system-messages.scss @@ -17,9 +17,3 @@ } } } - -.system-message .close { - @include reset-button; - - color: var(--content-primary-dark); -} diff --git a/services/web/frontend/stylesheets/bootstrap-5/pages/all.scss b/services/web/frontend/stylesheets/bootstrap-5/pages/all.scss index bfd18c084f..f3f7e0e844 100644 --- a/services/web/frontend/stylesheets/bootstrap-5/pages/all.scss +++ b/services/web/frontend/stylesheets/bootstrap-5/pages/all.scss @@ -15,6 +15,7 @@ @import 'editor/figure-modal'; @import 'editor/review-panel'; @import 'editor/chat'; +@import 'editor/history'; @import 'subscription'; @import 'editor/pdf'; @import 'editor/compile-button'; diff --git a/services/web/frontend/stylesheets/bootstrap-5/pages/editor/history.scss b/services/web/frontend/stylesheets/bootstrap-5/pages/editor/history.scss new file mode 100644 index 0000000000..bfb986767e --- /dev/null +++ b/services/web/frontend/stylesheets/bootstrap-5/pages/editor/history.scss @@ -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; +} diff --git a/services/web/frontend/stylesheets/bootstrap-5/pages/editor/toolbar.scss b/services/web/frontend/stylesheets/bootstrap-5/pages/editor/toolbar.scss index f0ed2e720c..2eb210dce8 100644 --- a/services/web/frontend/stylesheets/bootstrap-5/pages/editor/toolbar.scss +++ b/services/web/frontend/stylesheets/bootstrap-5/pages/editor/toolbar.scss @@ -328,20 +328,25 @@ ***************************************/ .toggle-switch { + --toggle-switch-height: 26px; + --toggle-switch-padding: var(--spacing-01); + display: inline-flex; align-items: center; - height: 26px; + height: var(--toggle-switch-height); margin-right: var(--spacing-03); border-radius: var(--border-radius-full); background-color: var(--neutral-20); - padding: var(--spacing-01); + padding: var(--toggle-switch-padding); } .toggle-switch-label { display: inline-block; float: left; font-weight: normal; - height: 100%; + + // It seems we need to set the height explicitly rather than using 100% to get the button to display correctly in Blink + height: calc(var(--toggle-switch-height) - 2 * var(--toggle-switch-padding)); text-align: center; margin: 0; cursor: pointer; diff --git a/services/web/frontend/stylesheets/bootstrap-5/pages/project-list.scss b/services/web/frontend/stylesheets/bootstrap-5/pages/project-list.scss index 9514c64047..ca949ef486 100644 --- a/services/web/frontend/stylesheets/bootstrap-5/pages/project-list.scss +++ b/services/web/frontend/stylesheets/bootstrap-5/pages/project-list.scss @@ -657,17 +657,9 @@ } .dropdown-table-button-toggle { - padding: var(--spacing-04); - font-size: 0; - line-height: 1; - border-radius: 50%; - color: var(--content-primary); - background-color: transparent; + @include action-button; - &:hover, - &:active { - background-color: rgba($neutral-90, 0.08); - } + padding: var(--spacing-04); } } } diff --git a/services/web/frontend/stylesheets/components/loading-spinner.less b/services/web/frontend/stylesheets/components/loading-spinner.less index a92aae35d3..7640caaa7c 100644 --- a/services/web/frontend/stylesheets/components/loading-spinner.less +++ b/services/web/frontend/stylesheets/components/loading-spinner.less @@ -5,3 +5,8 @@ align-items: center; justify-content: center; } + +.loading { + display: inline-flex; + align-items: center; +} diff --git a/services/web/test/frontend/features/history/components/change-list-bs5.spec.tsx b/services/web/test/frontend/features/history/components/change-list-bs5.spec.tsx new file mode 100644 index 0000000000..c950bb41ad --- /dev/null +++ b/services/web/test/frontend/features/history/components/change-list-bs5.spec.tsx @@ -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 = {}, + props: Record = {} +) => { + cy.mount( + + +
    +
    {component}
    +
    +
    +
    + ) +} + +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( + {}} /> + ) + + 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 ( + + ) + } + + mountWithEditorProviders() + + 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(, 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(, 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(, 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(, 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(, 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(, 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(, 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(, 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(, 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(, 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) + }) + }) +}) diff --git a/services/web/test/frontend/features/history/components/change-list.spec.tsx b/services/web/test/frontend/features/history/components/change-list.spec.tsx index 9087c1a616..051a5b63bf 100644 --- a/services/web/test/frontend/features/history/components/change-list.spec.tsx +++ b/services/web/test/frontend/features/history/components/change-list.spec.tsx @@ -31,7 +31,7 @@ const mountWithEditorProviders = ( ) } -describe('change list', function () { +describe('change list (Bootstrap 3)', function () { const scope = { ui: { view: 'history', pdfLayout: 'sideBySide', chatOpen: true }, } @@ -363,7 +363,7 @@ describe('change list', function () { cy.findAllByTestId('history-version-details') .eq(1) .within(() => { - cy.findByRole('button', { name: /compare drop down/i }).click() + cy.findByRole('button', { name: /compare/i }).click() cy.findByRole('menu').within(() => { cy.findByRole('menuitem', { name: /compare up to this version/i,