[web] Tear down Bootstrap 5 project dashboard feature flag (#21820)

* Remove `BootstrapVersionSwitcher`

* Remove `bsVersion`

* Remove `bootstrap-5-project-dashboard` from code

* Fix frontend test

* Fixup classname `"me-auto"`

Co-authored-by: Tim Down <tim.down@overleaf.com>

* Rename `handleClickBS5` to `handleClick`

Co-authored-by: Tim Down <tim.down@overleaf.com>

* Remove `bs3Props` from `project-list/components`

* Remove translation: `joining`

* Set `isBootstrap5()` to true in test, and update

---------

Co-authored-by: Tim Down <tim.down@overleaf.com>
GitOrigin-RevId: 94005bc8636c9235253758b8ac18a732ee1dbd77
This commit is contained in:
Antoine Clausse
2024-11-20 14:03:25 +01:00
committed by Copybot
parent b27b2808f2
commit 9e745ed7ae
50 changed files with 555 additions and 1825 deletions

View File

@@ -440,15 +440,6 @@ async function projectListPage(req, res, next) {
)
}
// Get the user's assignment for this page's Bootstrap 5 split test, which
// populates splitTestVariants with a value for the split test name and allows
// Pug to read it
await SplitTestHandler.promises.getAssignment(
req,
res,
'bootstrap-5-project-dashboard'
)
res.render('project/list-react', {
title: 'your_projects',
usersBestSubscription,

View File

@@ -6,7 +6,6 @@ block entrypointVar
block vars
- var suppressNavContentLinks = true
- bootstrap5PageStatus = 'enabled' // One of 'disabled', 'enabled', and 'queryStringOnly'
- bootstrap5PageSplitTest = 'bootstrap-5-project-dashboard'
block append meta
meta(name="ol-usersBestSubscription" data-type="json" content=usersBestSubscription)

View File

@@ -764,7 +764,6 @@
"join_project": "",
"join_team_explanation": "",
"joined_team": "",
"joining": "",
"justify": "",
"kb_suggestions_enquiry": "",
"keep_current_plan": "",

View File

@@ -2,9 +2,6 @@ import { useTranslation, Trans } from 'react-i18next'
import { CommonsPlanSubscription } from '../../../../../../types/project/dashboard/subscription'
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
import MaterialIcon from '@/shared/components/material-icon'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
import { bsVersion } from '@/features/utils/bootstrap-5'
import classnames from 'classnames'
type CommonsPlanProps = Pick<
CommonsPlanSubscription,
@@ -23,14 +20,7 @@ function CommonsPlan({
return (
<>
<span
className={classnames(
'current-plan-label',
bsVersion({ bs5: 'd-md-none', bs3: 'visible-xs' })
)}
>
{currentPlanLabel}
</span>
<span className="current-plan-label d-md-none">{currentPlanLabel}</span>
<OLTooltip
description={t('commons_plan_tooltip', {
plan: plan.name,
@@ -41,21 +31,10 @@ function CommonsPlan({
>
<a
href={featuresPageURL}
className={classnames(
'current-plan-label',
bsVersion({
bs5: 'd-none d-md-inline-block',
bs3: 'hidden-xs',
})
)}
className="current-plan-label d-none d-md-inline-block"
>
{currentPlanLabel}&nbsp;
<BootstrapVersionSwitcher
bs3={<span className="info-badge" />}
bs5={
<MaterialIcon type="info" className="current-plan-label-icon" />
}
/>
<MaterialIcon type="info" className="current-plan-label-icon" />
</a>
</OLTooltip>
</>

View File

@@ -1,12 +1,9 @@
import { useTranslation, Trans } from 'react-i18next'
import OLButton from '@/features/ui/components/ol/ol-button'
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
import MaterialIcon from '@/shared/components/material-icon'
import { FreePlanSubscription } from '../../../../../../types/project/dashboard/subscription'
import * as eventTracking from '../../../../infrastructure/event-tracking'
import { bsVersion } from '@/features/utils/bootstrap-5'
import classnames from 'classnames'
type FreePlanProps = Pick<FreePlanSubscription, 'featuresPageURL'>
@@ -27,14 +24,7 @@ function FreePlan({ featuresPageURL }: FreePlanProps) {
return (
<>
<span
className={classnames(
'current-plan-label',
bsVersion({ bs5: 'd-md-none', bs3: 'visible-xs' })
)}
>
{currentPlanLabel}
</span>
<span className="current-plan-label d-md-none">{currentPlanLabel}</span>
<OLTooltip
description={t('free_plan_tooltip')}
id="free-plan"
@@ -42,26 +32,13 @@ function FreePlan({ featuresPageURL }: FreePlanProps) {
>
<a
href={featuresPageURL}
className={classnames(
'current-plan-label',
bsVersion({ bs5: 'd-none d-md-inline-block', bs3: 'hidden-xs' })
)}
className="current-plan-label d-none d-md-inline-block"
>
{currentPlanLabel}&nbsp;
<BootstrapVersionSwitcher
bs3={<span className="info-badge" />}
bs5={
<MaterialIcon type="info" className="current-plan-label-icon" />
}
/>
<MaterialIcon type="info" className="current-plan-label-icon" />
</a>
</OLTooltip>{' '}
<span
className={bsVersion({
bs5: 'd-none d-md-inline-block',
bs3: 'hidden-xs',
})}
>
<span className="d-none d-md-inline-block">
<OLButton
variant="primary"
href="/user/subscription/plans"

View File

@@ -1,10 +1,7 @@
import { useTranslation, Trans } from 'react-i18next'
import { GroupPlanSubscription } from '../../../../../../types/project/dashboard/subscription'
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
import MaterialIcon from '@/shared/components/material-icon'
import { bsVersion } from '@/features/utils/bootstrap-5'
import classnames from 'classnames'
type GroupPlanProps = Pick<
GroupPlanSubscription,
@@ -37,14 +34,7 @@ function GroupPlan({
return (
<>
<span
className={classnames(
'current-plan-label',
bsVersion({ bs5: 'd-md-none', bs3: 'visible-xs' })
)}
>
{currentPlanLabel}
</span>
<span className="current-plan-label d-md-none">{currentPlanLabel}</span>
<OLTooltip
description={
subscription.teamName != null
@@ -59,21 +49,10 @@ function GroupPlan({
>
<a
href={featuresPageURL}
className={classnames(
'current-plan-label',
bsVersion({
bs5: 'd-none d-md-inline-block',
bs3: 'hidden-xs',
})
)}
className="current-plan-label d-none d-md-inline-block"
>
{currentPlanLabel}&nbsp;
<BootstrapVersionSwitcher
bs3={<span className="info-badge" />}
bs5={
<MaterialIcon type="info" className="current-plan-label-icon" />
}
/>
<MaterialIcon type="info" className="current-plan-label-icon" />
</a>
</OLTooltip>
</>

View File

@@ -1,10 +1,7 @@
import { useTranslation, Trans } from 'react-i18next'
import { IndividualPlanSubscription } from '../../../../../../types/project/dashboard/subscription'
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
import MaterialIcon from '@/shared/components/material-icon'
import { bsVersion } from '@/features/utils/bootstrap-5'
import classnames from 'classnames'
type IndividualPlanProps = Pick<
IndividualPlanSubscription,
@@ -36,14 +33,7 @@ function IndividualPlan({
return (
<>
<span
className={classnames(
'current-plan-label',
bsVersion({ bs5: 'd-md-none', bs3: 'visible-xs' })
)}
>
{currentPlanLabel}
</span>
<span className="current-plan-label d-md-none">{currentPlanLabel}</span>
<OLTooltip
description={t('plan_tooltip', { plan: plan.name })}
id="individual-plan"
@@ -51,18 +41,10 @@ function IndividualPlan({
>
<a
href={featuresPageURL}
className={classnames(
'current-plan-label',
bsVersion({ bs5: 'd-none d-md-inline-block', bs3: 'hidden-xs' })
)}
className="current-plan-label d-none d-md-inline-block"
>
{currentPlanLabel}&nbsp;
<BootstrapVersionSwitcher
bs3={<span className="info-badge" />}
bs5={
<MaterialIcon type="info" className="current-plan-label-icon" />
}
/>
<MaterialIcon type="info" className="current-plan-label-icon" />
</a>
</OLTooltip>
</>

View File

@@ -1,6 +1,4 @@
import { useState, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { Dropdown as BS3Dropdown } from 'react-bootstrap'
import { Spinner } from 'react-bootstrap-5'
import {
Dropdown,
@@ -8,8 +6,6 @@ import {
DropdownMenu,
DropdownToggle,
} from '@/features/ui/components/bootstrap-5/dropdown-menu'
import MenuItemButton from './menu-item-button'
import Icon from '../../../../shared/components/icon'
import CopyProjectButton from '../table/cells/action-buttons/copy-project-button'
import DownloadProjectButton from '../table/cells/action-buttons/download-project-button'
import ArchiveProjectButton from '../table/cells/action-buttons/archive-project-button'
@@ -22,256 +18,12 @@ import { Project } from '../../../../../../types/project/dashboard/api'
import CompileAndDownloadProjectPDFButton from '../table/cells/action-buttons/compile-and-download-project-pdf-button'
import RenameProjectButton from '../table/cells/action-buttons/rename-project-button'
import MaterialIcon from '@/shared/components/material-icon'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
type ActionButtonProps = {
project: Project
onClick: <T extends React.MouseEvent>(e?: T, fn?: (e?: T) => void) => void
}
function CopyProjectButtonMenuItem({ project, onClick }: ActionButtonProps) {
return (
<CopyProjectButton project={project}>
{(text, handleOpenModal) => (
<MenuItemButton
onClick={e => handleOpenModal(e, onClick)}
className="projects-action-menu-item"
>
<Icon type="files-o" className="menu-item-button-icon" />{' '}
<span className="menu-item-button-text">{text}</span>
</MenuItemButton>
)}
</CopyProjectButton>
)
}
function CompileAndDownloadProjectPDFButtonMenuItem({
project,
onClick,
}: ActionButtonProps) {
return (
<CompileAndDownloadProjectPDFButton project={project}>
{(text, pendingCompile, downloadProject) => (
<MenuItemButton
onClick={e => downloadProject(e, onClick)}
className="projects-action-menu-item"
>
{pendingCompile ? (
<Icon type="spinner" spin className="menu-item-button-icon" />
) : (
<Icon type="file-pdf-o" className="menu-item-button-icon" />
)}{' '}
<span className="menu-item-button-text">{text}</span>
</MenuItemButton>
)}
</CompileAndDownloadProjectPDFButton>
)
}
function DownloadProjectButtonMenuItem({
project,
onClick,
}: ActionButtonProps) {
const handleClick = (downloadProject: () => void) => {
downloadProject()
onClick()
}
return (
<DownloadProjectButton project={project}>
{(text, downloadProject) => (
<MenuItemButton
onClick={() => handleClick(downloadProject)}
className="projects-action-menu-item"
>
<Icon type="cloud-download" className="menu-item-button-icon" />{' '}
<span className="menu-item-button-text">{text}</span>
</MenuItemButton>
)}
</DownloadProjectButton>
)
}
function ArchiveProjectButtonMenuItem({ project, onClick }: ActionButtonProps) {
const handleClick = (handleOpenModal: () => void) => {
handleOpenModal()
onClick()
}
return (
<ArchiveProjectButton project={project}>
{(text, handleOpenModal) => (
<MenuItemButton
onClick={() => handleClick(handleOpenModal)}
className="projects-action-menu-item"
>
<Icon type="inbox" className="menu-item-button-icon" />{' '}
<span className="menu-item-button-text">{text}</span>
</MenuItemButton>
)}
</ArchiveProjectButton>
)
}
function TrashProjectButtonMenuItem({ project, onClick }: ActionButtonProps) {
const handleClick = (handleOpenModal: () => void) => {
handleOpenModal()
onClick()
}
return (
<TrashProjectButton project={project}>
{(text, handleOpenModal) => (
<MenuItemButton
onClick={() => handleClick(handleOpenModal)}
className="projects-action-menu-item"
>
<Icon type="trash" className="menu-item-button-icon" />{' '}
<span className="menu-item-button-text">{text}</span>
</MenuItemButton>
)}
</TrashProjectButton>
)
}
function UnarchiveProjectButtonMenuItem({
project,
onClick,
}: ActionButtonProps) {
const handleClick = (unarchiveProject: () => Promise<void>) => {
unarchiveProject()
onClick()
}
return (
<UnarchiveProjectButton project={project}>
{(text, unarchiveProject) => (
<MenuItemButton
onClick={() => handleClick(unarchiveProject)}
className="projects-action-menu-item"
>
<Icon type="reply" className="menu-item-button-icon" />{' '}
<span className="menu-item-button-text">{text}</span>
</MenuItemButton>
)}
</UnarchiveProjectButton>
)
}
function UntrashProjectButtonMenuItem({ project, onClick }: ActionButtonProps) {
const handleClick = (untrashProject: () => Promise<void>) => {
untrashProject()
onClick()
}
return (
<UntrashProjectButton project={project}>
{(text, untrashProject) => (
<MenuItemButton
onClick={() => handleClick(untrashProject)}
className="projects-action-menu-item"
>
<Icon type="reply" className="menu-item-button-icon" />{' '}
<span className="menu-item-button-text">{text}</span>
</MenuItemButton>
)}
</UntrashProjectButton>
)
}
function LeaveProjectButtonMenuItem({ project, onClick }: ActionButtonProps) {
return (
<LeaveProjectButton project={project}>
{text => (
<MenuItemButton onClick={onClick} className="projects-action-menu-item">
<Icon type="sign-out" className="menu-item-button-icon" />{' '}
<span className="menu-item-button-text">{text}</span>
</MenuItemButton>
)}
</LeaveProjectButton>
)
}
function DeleteProjectButtonMenuItem({ project, onClick }: ActionButtonProps) {
return (
<DeleteProjectButton project={project}>
{text => (
<MenuItemButton onClick={onClick} className="projects-action-menu-item">
<Icon type="ban" className="menu-item-button-icon" />{' '}
<span className="menu-item-button-text">{text}</span>
</MenuItemButton>
)}
</DeleteProjectButton>
)
}
function RenameProjectButtonMenuItem({ project, onClick }: ActionButtonProps) {
const handleClick = (handleOpenModal: () => void) => {
handleOpenModal()
onClick()
}
return (
<RenameProjectButton project={project}>
{(text, handleOpenModal) => (
<MenuItemButton
onClick={() => handleClick(handleOpenModal)}
className="projects-action-menu-item"
>
<Icon type="pencil" className="menu-item-button-icon" />{' '}
<span className="menu-item-button-text">{text}</span>
</MenuItemButton>
)}
</RenameProjectButton>
)
}
type ActionDropdownProps = {
project: Project
}
export function BS3ActionsDropdown({ project }: ActionDropdownProps) {
const [isOpened, setIsOpened] = useState(false)
const handleClose = useCallback(() => {
setIsOpened(false)
}, [setIsOpened])
return (
<BS3Dropdown
id={`project-actions-dropdown-${project.id}`}
pullRight
open={isOpened}
onToggle={open => setIsOpened(open)}
>
<BS3Dropdown.Toggle noCaret className="btn-transparent">
<Icon type="ellipsis-h" fw />
</BS3Dropdown.Toggle>
<BS3Dropdown.Menu className="projects-dropdown-menu text-left">
<RenameProjectButtonMenuItem project={project} onClick={handleClose} />
<CopyProjectButtonMenuItem project={project} onClick={handleClose} />
<DownloadProjectButtonMenuItem
project={project}
onClick={handleClose}
/>
<CompileAndDownloadProjectPDFButtonMenuItem
project={project}
onClick={handleClose}
/>
<ArchiveProjectButtonMenuItem project={project} onClick={handleClose} />
<TrashProjectButtonMenuItem project={project} onClick={handleClose} />
<UnarchiveProjectButtonMenuItem
project={project}
onClick={handleClose}
/>
<UntrashProjectButtonMenuItem project={project} onClick={handleClose} />
<LeaveProjectButtonMenuItem project={project} onClick={handleClose} />
<DeleteProjectButtonMenuItem project={project} onClick={handleClose} />
</BS3Dropdown.Menu>
</BS3Dropdown>
)
}
function BS5ActionsDropdown({ project }: ActionDropdownProps) {
function ActionsDropdown({ project }: ActionDropdownProps) {
const { t } = useTranslation()
return (
@@ -434,13 +186,4 @@ function BS5ActionsDropdown({ project }: ActionDropdownProps) {
)
}
function ActionsDropdown({ project }: ActionDropdownProps) {
return (
<BootstrapVersionSwitcher
bs3={<BS3ActionsDropdown project={project} />}
bs5={<BS5ActionsDropdown project={project} />}
/>
)
}
export default ActionsDropdown

View File

@@ -1,10 +1,5 @@
import { useState, useEffect, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import {
Dropdown as BS3Dropdown,
MenuItem as BS3MenuItem,
} from 'react-bootstrap'
import Icon from '../../../../shared/components/icon'
import {
Filter,
UNCATEGORIZED_KEY,
@@ -19,8 +14,6 @@ import {
} from '@/features/ui/components/bootstrap-5/dropdown-menu'
import ProjectsFilterMenu from '../projects-filter-menu'
import TagsList from '../tags-list'
import MenuItemButton from './menu-item-button'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
type ItemProps = {
filter: Filter
@@ -38,30 +31,15 @@ export function Item({ filter, text, onClick }: ItemProps) {
return (
<ProjectsFilterMenu filter={filter}>
{isActive => (
<BootstrapVersionSwitcher
bs3={
<MenuItemButton
onClick={handleClick}
className="projects-types-menu-item"
>
{isActive ? (
<Icon type="check" className="menu-item-button-icon" />
) : null}
<span className="menu-item-button-text">{text}</span>
</MenuItemButton>
}
bs5={
<DropdownItem
as="button"
tabIndex={-1}
onClick={handleClick}
trailingIcon={isActive ? 'check' : undefined}
active={isActive}
>
{text}
</DropdownItem>
}
/>
<DropdownItem
as="button"
tabIndex={-1}
onClick={handleClick}
trailingIcon={isActive ? 'check' : undefined}
active={isActive}
>
{text}
</DropdownItem>
)}
</ProjectsFilterMenu>
)
@@ -70,7 +48,6 @@ export function Item({ filter, text, onClick }: ItemProps) {
function ProjectsDropdown() {
const { t } = useTranslation()
const [title, setTitle] = useState(() => t('all_projects'))
const [isOpened, setIsOpened] = useState(false)
const { filter, selectedTagId, tags } = useProjectListContext()
const filterTranslations = useRef<Record<Filter, string>>({
all: t('all_projects'),
@@ -79,7 +56,6 @@ function ProjectsDropdown() {
archived: t('archived_projects'),
trashed: t('trashed_projects'),
})
const handleClose = () => setIsOpened(false)
useEffect(() => {
if (selectedTagId === undefined) {
@@ -98,84 +74,37 @@ function ProjectsDropdown() {
}, [filter, tags, selectedTagId, t])
return (
<BootstrapVersionSwitcher
bs3={
<BS3Dropdown
id="projects-types-dropdown"
open={isOpened}
onToggle={open => setIsOpened(open)}
>
<BS3Dropdown.Toggle
bsSize="large"
noCaret
className="ps-0 btn-transparent"
>
<span className="text-truncate me-1">{title}</span>
<Icon type="angle-down" />
</BS3Dropdown.Toggle>
<BS3Dropdown.Menu className="projects-dropdown-menu">
<Item filter="all" text={t('all_projects')} onClick={handleClose} />
<Item
filter="owned"
text={t('your_projects')}
onClick={handleClose}
/>
<Item
filter="shared"
text={t('shared_with_you')}
onClick={handleClose}
/>
<Item
filter="archived"
text={t('archived_projects')}
onClick={handleClose}
/>
<Item
filter="trashed"
text={t('trashed_projects')}
onClick={handleClose}
/>
<BS3MenuItem header>{t('tags')}:</BS3MenuItem>
<TagsList onTagClick={handleClose} onEditClick={handleClose} />
</BS3Dropdown.Menu>
</BS3Dropdown>
}
bs5={
<Dropdown>
<DropdownToggle
id="projects-types-dropdown-toggle-btn"
className="ps-0 mb-0 btn-transparent h3"
size="lg"
aria-label={t('filter_projects')}
>
<span className="text-truncate" aria-hidden>
{title}
</span>
</DropdownToggle>
<DropdownMenu flip={false}>
<li role="none">
<Item filter="all" text={t('all_projects')} />
</li>
<li role="none">
<Item filter="owned" text={t('your_projects')} />
</li>
<li role="none">
<Item filter="shared" text={t('shared_with_you')} />
</li>
<li role="none">
<Item filter="archived" text={t('archived_projects')} />
</li>
<li role="none">
<Item filter="trashed" text={t('trashed_projects')} />
</li>
<DropdownHeader className="text-uppercase">
{t('tags')}:
</DropdownHeader>
<TagsList />
</DropdownMenu>
</Dropdown>
}
/>
<Dropdown>
<DropdownToggle
id="projects-types-dropdown-toggle-btn"
className="ps-0 mb-0 btn-transparent h3"
size="lg"
aria-label={t('filter_projects')}
>
<span className="text-truncate" aria-hidden>
{title}
</span>
</DropdownToggle>
<DropdownMenu flip={false}>
<li role="none">
<Item filter="all" text={t('all_projects')} />
</li>
<li role="none">
<Item filter="owned" text={t('your_projects')} />
</li>
<li role="none">
<Item filter="shared" text={t('shared_with_you')} />
</li>
<li role="none">
<Item filter="archived" text={t('archived_projects')} />
</li>
<li role="none">
<Item filter="trashed" text={t('trashed_projects')} />
</li>
<DropdownHeader className="text-uppercase">{t('tags')}:</DropdownHeader>
<TagsList />
</DropdownMenu>
</Dropdown>
)
}

View File

@@ -1,15 +1,9 @@
import { useState, useEffect, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import {
Dropdown as BS3Dropdown,
MenuItem as BS3MenuItem,
} from 'react-bootstrap'
import Icon from '../../../../shared/components/icon'
import useSort from '../../hooks/use-sort'
import withContent, { SortBtnProps } from '../sort/with-content'
import { useProjectListContext } from '../../context/project-list-context'
import { Sort } from '../../../../../../types/project/dashboard/api'
import MenuItemButton from './menu-item-button'
import {
Dropdown,
DropdownHeader,
@@ -17,31 +11,17 @@ import {
DropdownMenu,
DropdownToggle,
} from '@/features/ui/components/bootstrap-5/dropdown-menu'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
function Item({ onClick, text, iconType, screenReaderText }: SortBtnProps) {
return (
<BootstrapVersionSwitcher
bs3={
<MenuItemButton onClick={onClick} className="projects-sort-menu-item">
{iconType ? (
<Icon type={iconType} className="menu-item-button-icon" />
) : null}
<span className="menu-item-button-text">{text}</span>
<span className="sr-only">{screenReaderText}</span>
</MenuItemButton>
}
bs5={
<DropdownItem
as="button"
tabIndex={-1}
onClick={onClick}
trailingIcon={iconType}
>
{text}
</DropdownItem>
}
/>
<DropdownItem
as="button"
tabIndex={-1}
onClick={onClick}
trailingIcon={iconType}
>
{text}
</DropdownItem>
)
}
@@ -50,7 +30,6 @@ const ItemWithContent = withContent(Item)
function SortByDropdown() {
const { t } = useTranslation()
const [title, setTitle] = useState(() => t('last_modified'))
const [isOpened, setIsOpened] = useState(false)
const { sort } = useProjectListContext()
const { handleSort } = useSort()
const sortByTranslations = useRef<Record<Sort['by'], string>>({
@@ -60,11 +39,6 @@ function SortByDropdown() {
})
const handleClick = (by: Sort['by']) => {
setTitle(sortByTranslations.current[by])
setIsOpened(false)
handleSort(by)
}
const handleClickBS5 = (by: Sort['by']) => {
setTitle(sortByTranslations.current[by])
handleSort(by)
}
@@ -74,84 +48,41 @@ function SortByDropdown() {
}, [sort.by])
return (
<BootstrapVersionSwitcher
bs3={
<BS3Dropdown
id="projects-sort-dropdown"
className="projects-sort-dropdown"
pullRight
open={isOpened}
onToggle={open => setIsOpened(open)}
>
<BS3Dropdown.Toggle
bsSize="small"
noCaret
className="pe-0 btn-transparent"
>
<span className="text-truncate me-1">{title}</span>
<Icon type="angle-down" />
</BS3Dropdown.Toggle>
<BS3Dropdown.Menu className="projects-dropdown-menu">
<BS3MenuItem header>{t('sort_by')}:</BS3MenuItem>
<ItemWithContent
column="title"
text={t('title')}
sort={sort}
onClick={() => handleClick('title')}
/>
<ItemWithContent
column="owner"
text={t('owner')}
sort={sort}
onClick={() => handleClick('owner')}
/>
<ItemWithContent
column="lastUpdated"
text={t('last_modified')}
sort={sort}
onClick={() => handleClick('lastUpdated')}
/>
</BS3Dropdown.Menu>
</BS3Dropdown>
}
bs5={
<Dropdown className="projects-sort-dropdown" align="end">
<DropdownToggle
id="projects-sort-dropdown"
className="pe-0 mb-0 btn-transparent"
size="sm"
aria-label={t('sort_projects')}
>
<span className="text-truncate" aria-hidden>
{title}
</span>
</DropdownToggle>
<DropdownMenu flip={false}>
<DropdownHeader className="text-uppercase">
{t('sort_by')}:
</DropdownHeader>
<ItemWithContent
column="title"
text={t('title')}
sort={sort}
onClick={() => handleClickBS5('title')}
/>
<ItemWithContent
column="owner"
text={t('owner')}
sort={sort}
onClick={() => handleClickBS5('owner')}
/>
<ItemWithContent
column="lastUpdated"
text={t('last_modified')}
sort={sort}
onClick={() => handleClickBS5('lastUpdated')}
/>
</DropdownMenu>
</Dropdown>
}
/>
<Dropdown className="projects-sort-dropdown" align="end">
<DropdownToggle
id="projects-sort-dropdown"
className="pe-0 mb-0 btn-transparent"
size="sm"
aria-label={t('sort_projects')}
>
<span className="text-truncate" aria-hidden>
{title}
</span>
</DropdownToggle>
<DropdownMenu flip={false}>
<DropdownHeader className="text-uppercase">
{t('sort_by')}:
</DropdownHeader>
<ItemWithContent
column="title"
text={t('title')}
sort={sort}
onClick={() => handleClick('title')}
/>
<ItemWithContent
column="owner"
text={t('owner')}
sort={sort}
onClick={() => handleClick('owner')}
/>
<ItemWithContent
column="lastUpdated"
text={t('last_modified')}
sort={sort}
onClick={() => handleClick('lastUpdated')}
/>
</DropdownMenu>
</Dropdown>
)
}

View File

@@ -129,9 +129,6 @@ export default function CreateTagModal({
status === 'pending' || !tagName?.length || !!validationError
}
isLoading={isLoading}
bs3Props={{
loading: isLoading ? `${t('creating')}` : t('create'),
}}
>
{t('create')}
</OLButton>

View File

@@ -72,9 +72,6 @@ export default function DeleteTagModal({
variant="danger"
disabled={isLoading}
isLoading={isLoading}
bs3Props={{
loading: isLoading ? `${t('deleting')}` : t('delete'),
}}
>
{t('delete')}
</OLButton>

View File

@@ -138,9 +138,6 @@ export function EditTagModal({ id, tag, onEdit, onClose }: EditTagModalProps) {
!!validationError
}
isLoading={isLoading}
bs3Props={{
loading: isLoading ? `${t('saving')}` : t('save'),
}}
>
{t('save')}
</OLButton>

View File

@@ -18,9 +18,7 @@ import OLForm from '@/features/ui/components/ol/ol-form'
import OLFormGroup from '@/features/ui/components/ol/ol-form-group'
import OLFormLabel from '@/features/ui/components/ol/ol-form-label'
import OLButton from '@/features/ui/components/ol/ol-button'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
import Notification from '@/shared/components/notification'
import { bsVersion } from '@/features/utils/bootstrap-5'
type ManageTagModalProps = {
id: string
@@ -125,12 +123,9 @@ export function ManageTagModal({
<OLButton
variant="danger"
onClick={() => runDeleteTag(tag._id)}
className={bsVersion({ bs3: 'pull-left', bs5: 'me-auto' })}
className="me-auto"
disabled={isDeleteLoading || isUpdateLoading}
isLoading={isDeleteLoading}
bs3Props={{
loading: isDeleteLoading ? `${t('deleting')}` : t('delete_tag'),
}}
>
{t('delete_tag')}
</OLButton>
@@ -151,15 +146,9 @@ export function ManageTagModal({
(newTagName === tag?.name && selectedColor === getTagColor(tag))
)}
isLoading={isUpdateLoading}
bs3Props={{
loading: isUpdateLoading
? `${t('saving')}`
: t('save_or_cancel-save'),
}}
>
{t('save_or_cancel-save')}
</OLButton>
<BootstrapVersionSwitcher bs3={<div className="clearfix" />} />
</OLModalFooter>
</OLModal>
)

View File

@@ -1,10 +1,5 @@
import { type JSXElementConstructor, useCallback, useState } from 'react'
import {
Dropdown as BS3Dropdown,
MenuItem as BS3MenuItem,
} from 'react-bootstrap'
import { useTranslation } from 'react-i18next'
import ControlledDropdown from '../../../shared/components/controlled-dropdown'
import getMeta from '../../../utils/meta'
import NewProjectButtonModal, {
NewProjectButtonModalVariant,
@@ -13,7 +8,6 @@ import AddAffiliation, { useAddAffiliation } from './add-affiliation'
import { Nullable } from '../../../../../types/utils'
import { sendMB } from '../../../infrastructure/event-tracking'
import importOverleafModules from '../../../../macros/import-overleaf-module.macro'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
import {
Dropdown,
DropdownDivider,
@@ -151,225 +145,119 @@ function NewProjectButton({
return (
<>
<BootstrapVersionSwitcher
bs3={
<ControlledDropdown id={id} className={className}>
<BS3Dropdown.Toggle
noCaret
className="new-project-button"
bsStyle="primary"
<Dropdown className={className} onSelect={handleMainButtonClick}>
<DropdownToggle
id={id}
className="new-project-button"
variant="primary"
>
{buttonText || t('new_project')}
</DropdownToggle>
<DropdownMenu>
<li role="none">
<DropdownItem
onClick={e =>
handleModalMenuClick(e, {
modalVariant: 'blank_project',
dropdownMenuEvent: 'blank-project',
})
}
>
{buttonText || t('new_project')}
</BS3Dropdown.Toggle>
<BS3Dropdown.Menu>
<BS3MenuItem
onClick={(e: React.MouseEvent) =>
handleModalMenuClick(e, {
modalVariant: 'blank_project',
dropdownMenuEvent: 'blank-project',
})
}
>
{t('blank_project')}
</BS3MenuItem>
<BS3MenuItem
onClick={(e: React.MouseEvent) =>
handleModalMenuClick(e, {
modalVariant: 'example_project',
dropdownMenuEvent: 'example-project',
})
}
>
{t('example_project')}
</BS3MenuItem>
<BS3MenuItem
onClick={(e: React.MouseEvent) =>
handleModalMenuClick(e, {
modalVariant: 'upload_project',
dropdownMenuEvent: 'upload-project',
})
}
>
{t('upload_project')}
</BS3MenuItem>
{ImportProjectFromGithubMenu && (
<ImportProjectFromGithubMenu
onClick={e =>
handleModalMenuClick(e, {
modalVariant: 'import_from_github',
dropdownMenuEvent: 'import-from-github',
})
}
/>
)}
{portalTemplates.length > 0 ? (
<>
<BS3MenuItem divider />
<div aria-hidden="true" className="dropdown-header">
{`${t('institution')} ${t('templates')}`}
</div>
{portalTemplates.map((portalTemplate, index) => (
<BS3MenuItem
key={`portal-template-${index}`}
href={`${portalTemplate.url}#templates`}
onClick={(e: React.MouseEvent) =>
handlePortalTemplateClick(e, portalTemplate.name)
}
>
{portalTemplate.name}
</BS3MenuItem>
))}
</>
) : null}
{templateLinks && templateLinks.length > 0 && (
<>
<BS3MenuItem divider />
<div aria-hidden="true" className="dropdown-header">
{t('templates')}
</div>
</>
)}
{templateLinks?.map((templateLink, index) => (
<BS3MenuItem
key={`new-project-button-template-${index}`}
href={templateLink.url}
onClick={(e: React.MouseEvent) =>
handleStaticTemplateClick(e, templateLink.trackingKey)
}
>
{templateLink.name === 'view_all'
? t('view_all')
: templateLink.name}
</BS3MenuItem>
))}
{showAddAffiliationWidget && enableAddAffiliationWidget ? (
<>
<BS3MenuItem divider />
<li className="add-affiliation-mobile-wrapper">
<AddAffiliation className="is-mobile" />
</li>
</>
) : null}
</BS3Dropdown.Menu>
</ControlledDropdown>
}
bs5={
<Dropdown className={className} onSelect={handleMainButtonClick}>
<DropdownToggle
id={id}
className="new-project-button"
variant="primary"
{t('blank_project')}
</DropdownItem>
</li>
<li role="none">
<DropdownItem
onClick={e =>
handleModalMenuClick(e, {
modalVariant: 'example_project',
dropdownMenuEvent: 'example-project',
})
}
>
{buttonText || t('new_project')}
</DropdownToggle>
<DropdownMenu>
<li role="none">
<DropdownItem
onClick={e =>
handleModalMenuClick(e, {
modalVariant: 'blank_project',
dropdownMenuEvent: 'blank-project',
})
}
>
{t('blank_project')}
</DropdownItem>
</li>
<li role="none">
<DropdownItem
onClick={e =>
handleModalMenuClick(e, {
modalVariant: 'example_project',
dropdownMenuEvent: 'example-project',
})
}
>
{t('example_project')}
</DropdownItem>
</li>
<li role="none">
<DropdownItem
onClick={e =>
handleModalMenuClick(e, {
modalVariant: 'upload_project',
dropdownMenuEvent: 'upload-project',
})
}
>
{t('upload_project')}
</DropdownItem>
</li>
<li role="none">
{ImportProjectFromGithubMenu && (
<ImportProjectFromGithubMenu
onClick={e =>
handleModalMenuClick(e, {
modalVariant: 'import_from_github',
dropdownMenuEvent: 'import-from-github',
})
}
/>
)}
</li>
{portalTemplates.length > 0 ? (
<>
<DropdownDivider />
<DropdownHeader aria-hidden="true">
{`${t('institution')} ${t('templates')}`}
</DropdownHeader>
{portalTemplates.map((portalTemplate, index) => (
<li role="none" key={`portal-template-${index}`}>
<DropdownItem
key={`portal-template-${index}`}
href={`${portalTemplate.url}#templates`}
onClick={e =>
handlePortalTemplateClick(e, portalTemplate.name)
}
aria-label={`${portalTemplate.name} ${t('template')}`}
>
{portalTemplate.name}
</DropdownItem>
</li>
))}
</>
) : null}
{templateLinks && templateLinks.length > 0 && (
<>
<DropdownDivider />
<DropdownHeader aria-hidden="true">
{t('templates')}
</DropdownHeader>
</>
)}
{templateLinks?.map((templateLink, index) => (
<li role="none" key={`new-project-button-template-${index}`}>
{t('example_project')}
</DropdownItem>
</li>
<li role="none">
<DropdownItem
onClick={e =>
handleModalMenuClick(e, {
modalVariant: 'upload_project',
dropdownMenuEvent: 'upload-project',
})
}
>
{t('upload_project')}
</DropdownItem>
</li>
<li role="none">
{ImportProjectFromGithubMenu && (
<ImportProjectFromGithubMenu
onClick={e =>
handleModalMenuClick(e, {
modalVariant: 'import_from_github',
dropdownMenuEvent: 'import-from-github',
})
}
/>
)}
</li>
{portalTemplates.length > 0 ? (
<>
<DropdownDivider />
<DropdownHeader aria-hidden="true">
{`${t('institution')} ${t('templates')}`}
</DropdownHeader>
{portalTemplates.map((portalTemplate, index) => (
<li role="none" key={`portal-template-${index}`}>
<DropdownItem
href={templateLink.url}
key={`portal-template-${index}`}
href={`${portalTemplate.url}#templates`}
onClick={e =>
handleStaticTemplateClick(e, templateLink.trackingKey)
handlePortalTemplateClick(e, portalTemplate.name)
}
aria-label={`${templateLink.name} ${t('template')}`}
aria-label={`${portalTemplate.name} ${t('template')}`}
>
{templateLink.name === 'view_all'
? t('view_all')
: templateLink.name}
{portalTemplate.name}
</DropdownItem>
</li>
))}
{showAddAffiliationWidget && enableAddAffiliationWidget ? (
<>
<DropdownDivider />
<li className="add-affiliation-mobile-wrapper">
<AddAffiliation className="is-mobile" />
</li>
</>
) : null}
</DropdownMenu>
</Dropdown>
}
/>
</>
) : null}
{templateLinks && templateLinks.length > 0 && (
<>
<DropdownDivider />
<DropdownHeader aria-hidden="true">
{t('templates')}
</DropdownHeader>
</>
)}
{templateLinks?.map((templateLink, index) => (
<li role="none" key={`new-project-button-template-${index}`}>
<DropdownItem
href={templateLink.url}
onClick={e =>
handleStaticTemplateClick(e, templateLink.trackingKey)
}
aria-label={`${templateLink.name} ${t('template')}`}
>
{templateLink.name === 'view_all'
? t('view_all')
: templateLink.name}
</DropdownItem>
</li>
))}
{showAddAffiliationWidget && enableAddAffiliationWidget ? (
<>
<DropdownDivider />
<li className="add-affiliation-mobile-wrapper">
<AddAffiliation className="is-mobile" />
</li>
</>
) : null}
</DropdownMenu>
</Dropdown>
<NewProjectButtonModal modal={modal} onHide={() => setModal(null)} />
</>
)

View File

@@ -102,9 +102,6 @@ function ModalContentNewProjectForm({ onCancel, template = 'none' }: Props) {
onClick={createNewProject}
disabled={projectName === '' || isLoading}
isLoading={isLoading}
bs3Props={{
loading: isLoading ? `${t('creating')}` : t('create'),
}}
>
{t('create')}
</OLButton>

View File

@@ -1,6 +1,5 @@
import { useState, useEffect } from 'react'
import { useTranslation, Trans } from 'react-i18next'
import Icon from '../../../../../../shared/components/icon'
import getMeta from '../../../../../../utils/meta'
import useAsync from '../../../../../../shared/hooks/use-async'
import {
@@ -88,13 +87,6 @@ function ReconfirmAffiliation({
className="btn-inline-link"
disabled={isLoading}
isLoading={isLoading}
bs3Props={{
loading: isLoading ? (
<>
<Icon type="refresh" spin fw /> {t('sending')}&hellip;
</>
) : null,
}}
>
{t('resend_confirmation_email')}
</OLButton>
@@ -143,14 +135,6 @@ function ReconfirmAffiliation({
action={
<OLButton
variant="secondary"
bs3Props={{
loading:
isLoading || isPending ? (
<>
<Icon type="refresh" spin fw /> {t('sending')}&hellip;
</>
) : null,
}}
isLoading={isLoading || isPending}
disabled={isLoading || isPending}
onClick={handleRequestReconfirmation}

View File

@@ -1,6 +1,5 @@
import { useTranslation, Trans } from 'react-i18next'
import Notification from '../notification'
import Icon from '../../../../../shared/components/icon'
import getMeta from '../../../../../utils/meta'
import useAsyncDismiss from '../hooks/useAsyncDismiss'
import useAsync from '../../../../../shared/hooks/use-async'
@@ -96,13 +95,6 @@ function CommonNotification({ notification }: CommonNotificationProps) {
) : (
<OLButton
variant="secondary"
bs3Props={{
loading: isLoading ? (
<>
<Icon type="spinner" spin /> {t('joining')}&hellip;
</>
) : null,
}}
isLoading={isLoading}
disabled={isLoading}
onClick={() => handleAcceptInvite(notification)}

View File

@@ -26,11 +26,8 @@ import withErrorBoundary from '../../../infrastructure/error-boundary'
import { GenericErrorBoundaryFallback } from '../../../shared/components/generic-error-boundary-fallback'
import { SplitTestProvider } from '@/shared/context/split-test-context'
import OLCol from '@/features/ui/components/ol/ol-col'
import { bsVersion } from '@/features/utils/bootstrap-5'
import classnames from 'classnames'
import Notification from '@/shared/components/notification'
import OLRow from '@/features/ui/components/ol/ol-row'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
import { TableContainer } from '@/features/ui/components/bootstrap-5/table'
function ProjectListRoot() {
@@ -78,13 +75,7 @@ function ProjectListPageContent() {
const { t } = useTranslation()
const tableTopArea = (
<div
className={classnames(
'pt-2',
'pb-3',
bsVersion({ bs5: 'd-md-none', bs3: 'visible-xs' })
)}
>
<div className="pt-2 pb-3 d-md-none">
<div className="clearfix">
<NewProjectButton
id="new-project-button-projects-table"
@@ -110,12 +101,7 @@ function ProjectListPageContent() {
<>
<SystemMessages />
<div
className={classnames(
'project-list-wrapper',
bsVersion({ bs3: 'clearfix container mx-0 px-0' })
)}
>
<div className="project-list-wrapper">
{totalProjectsCount > 0 ? (
<>
<Sidebar />
@@ -131,43 +117,22 @@ function ProjectListPageContent() {
filter={filter}
selectedTag={selectedTag}
selectedTagId={selectedTagId}
className={classnames(
'text-truncate',
bsVersion({
bs5: 'd-none d-md-block',
bs3: 'hidden-xs',
})
)}
className="text-truncate d-none d-md-block"
/>
<div className="project-tools">
<div
className={bsVersion({
bs5: 'd-none d-md-block',
bs3: 'hidden-xs',
})}
>
<div className="d-none d-md-block">
{selectedProjects.length === 0 ? (
<CurrentPlanWidget />
) : (
<ProjectTools />
)}
</div>
<div
className={bsVersion({
bs5: 'd-md-none',
bs3: 'visible-xs',
})}
>
<div className="d-md-none">
<CurrentPlanWidget />
</div>
</div>
</div>
<OLRow
className={bsVersion({
bs5: 'd-none d-md-block',
bs3: 'hidden-xs',
})}
>
<OLRow className="d-none d-md-block">
<OLCol lg={7}>
<SearchForm
inputValue={searchText}
@@ -177,20 +142,10 @@ function ProjectListPageContent() {
/>
</OLCol>
</OLRow>
<div
className={classnames(
'project-list-sidebar-survey-wrapper',
bsVersion({ bs5: 'd-md-none', bs3: 'visible-xs' })
)}
>
<div className="project-list-sidebar-survey-wrapper d-md-none">
<SurveyWidget />
</div>
<div
className={classnames(
'mt-1',
bsVersion({ bs5: 'd-md-none', bs3: 'visible-xs' })
)}
>
<div className="mt-1 d-md-none">
<div
role="toolbar"
className="projects-toolbar"
@@ -202,20 +157,10 @@ function ProjectListPageContent() {
</div>
<OLRow className="row-spaced">
<OLCol>
<BootstrapVersionSwitcher
bs3={
<div className="card project-list-card">
{tableTopArea}
<ProjectListTable />
</div>
}
bs5={
<TableContainer bordered>
{tableTopArea}
<ProjectListTable />
</TableContainer>
}
/>
<TableContainer bordered>
{tableTopArea}
<ProjectListTable />
</TableContainer>
</OLCol>
</OLRow>
<OLRow className="row-spaced">
@@ -253,11 +198,7 @@ function DashApiError() {
const { t } = useTranslation()
return (
<OLRow className="row-spaced">
<OLCol
xs={{ span: 8, offset: 2 }}
bs3Props={{ xs: 8, xsOffset: 2 }}
aria-live="polite"
>
<OLCol xs={{ span: 8, offset: 2 }} aria-live="polite">
<div className="notification-list">
<Notification
content={t('generic_something_went_wrong')}

View File

@@ -1,6 +1,5 @@
import { useTranslation } from 'react-i18next'
import { FormControl } from 'react-bootstrap'
import Icon from '../../../shared/components/icon'
import * as eventTracking from '../../../infrastructure/event-tracking'
import classnames from 'classnames'
import { Tag } from '../../../../../app/src/Features/Tags/types'
@@ -12,8 +11,6 @@ import OLFormGroup from '@/features/ui/components/ol/ol-form-group'
import OLCol from '@/features/ui/components/ol/ol-col'
import OLFormControl from '@/features/ui/components/ol/ol-form-control'
import MaterialIcon from '@/shared/components/material-icon'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
import { bsVersion } from '@/features/utils/bootstrap-5'
type SearchFormOwnProps = {
inputValue: string
@@ -79,12 +76,9 @@ function SearchForm({
className={classnames('project-search', className)}
role="search"
onSubmit={e => e.preventDefault()}
bs3Props={{ horizontal: true }}
{...props}
>
<OLFormGroup
className={bsVersion({ bs3: 'has-feedback has-feedback-left' })}
>
<OLFormGroup>
<OLCol>
<OLFormControl
type="text"
@@ -92,25 +86,16 @@ function SearchForm({
onChange={handleChange}
placeholder={placeholder}
aria-label={placeholder}
prepend={BootstrapVersionSwitcher({
bs3: <Icon type="search" />,
bs5: <MaterialIcon type="search" />,
})}
prepend={<MaterialIcon type="search" />}
append={
inputValue.length > 0 && (
<button
type="button"
className={bsVersion({
bs3: 'project-search-clear-btn btn-link',
bs5: 'project-search-clear-btn',
})}
className="project-search-clear-btn"
aria-label={t('clear_search')}
onClick={handleClear}
>
<BootstrapVersionSwitcher
bs3={<Icon type="times" />}
bs5={<MaterialIcon type="clear" />}
/>
<MaterialIcon type="clear" />
</button>
)
}

View File

@@ -3,8 +3,6 @@ import SidebarFilters from './sidebar-filters'
import AddAffiliation, { useAddAffiliation } from '../add-affiliation'
import SurveyWidget from '../survey-widget'
import { usePersistedResize } from '../../../../shared/hooks/use-resize'
import { bsVersion } from '@/features/utils/bootstrap-5'
import classnames from 'classnames'
function Sidebar() {
const { show: showAddAffiliationWidget } = useAddAffiliation()
@@ -14,10 +12,7 @@ function Sidebar() {
return (
<div
className={classnames(
'project-list-sidebar-wrapper-react',
bsVersion({ bs5: 'd-none d-md-block', bs3: 'hidden-xs' })
)}
className="project-list-sidebar-wrapper-react d-none d-md-block"
{...getTargetProps({
style: {
...(mousePos?.x && { flexBasis: `${mousePos.x}px` }),

View File

@@ -13,7 +13,6 @@ import {
DropdownMenu,
DropdownToggle,
} from '@/features/ui/components/bootstrap-5/dropdown-menu'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
export default function TagsList() {
const { t } = useTranslation()
@@ -76,65 +75,31 @@ export default function TagsList() {
</span>
</span>
</button>
<BootstrapVersionSwitcher
bs5={
<Dropdown align="end" className="tag-menu">
<DropdownToggle id={`${tag._id}-dropdown-toggle`}>
<span className="caret" />
</DropdownToggle>
<DropdownMenu className="dropdown-menu-sm-width">
<DropdownItem
as="li"
className="tag-action"
onClick={e => handleEditTag(e, tag._id)}
>
{t('edit')}
</DropdownItem>
<DropdownItem
as="li"
className="tag-action"
onClick={e => handleDeleteTag(e, tag._id)}
>
{t('delete')}
</DropdownItem>
</DropdownMenu>
</Dropdown>
}
bs3={
<span className="dropdown tag-menu">
<button
type="button"
className="dropdown-toggle"
data-toggle="dropdown"
dropdown-toggle=""
aria-haspopup="true"
aria-expanded="false"
>
<span className="caret" />
</button>
<ul className="dropdown-menu dropdown-menu-right" role="menu">
<li>
<button
type="button"
onClick={e => handleEditTag(e, tag._id)}
className="tag-action"
>
{t('edit')}
</button>
</li>
<li>
<button
type="button"
onClick={e => handleDeleteTag(e, tag._id)}
className="tag-action"
>
{t('delete')}
</button>
</li>
</ul>
</span>
}
/>
<Dropdown align="end" className="tag-menu">
<DropdownToggle
id={`${tag._id}-dropdown-toggle`}
data-testid="tag-dropdown-toggle"
>
<span className="caret" />
</DropdownToggle>
<DropdownMenu className="dropdown-menu-sm-width">
<DropdownItem
as="li"
className="tag-action"
onClick={e => handleEditTag(e, tag._id)}
>
{t('edit')}
</DropdownItem>
<DropdownItem
as="li"
className="tag-action"
onClick={e => handleDeleteTag(e, tag._id)}
>
{t('delete')}
</DropdownItem>
</DropdownMenu>
</Dropdown>
</li>
)
})}

View File

@@ -1,6 +1,5 @@
import { useTranslation } from 'react-i18next'
import { Sort } from '../../../../../../types/project/dashboard/api'
import { bsVersion } from '@/features/utils/bootstrap-5'
type SortBtnOwnProps = {
column: string
@@ -28,9 +27,7 @@ function withContent<T extends SortBtnOwnProps>(
if (column === sort.by) {
iconType =
sort.order === 'asc'
? bsVersion({ bs5: 'arrow_upward_alt', bs3: 'caret-up' })
: bsVersion({ bs5: 'arrow_downward_alt', bs3: 'caret-down' })
sort.order === 'asc' ? 'arrow_upward_alt' : 'arrow_downward_alt'
screenReaderText = t('reverse_x_sort_order', { x: text })
}

View File

@@ -2,7 +2,6 @@ import usePersistedState from '../../../shared/hooks/use-persisted-state'
import getMeta from '../../../utils/meta'
import { useCallback } from 'react'
import Close from '@/shared/components/close'
import { bsVersion } from '@/features/utils/bootstrap-5'
export default function SurveyWidget() {
const survey = getMeta('ol-survey')
@@ -22,13 +21,7 @@ export default function SurveyWidget() {
return (
<div className="user-notifications">
<div className="notification-entry">
<div
role="alert"
className={bsVersion({
bs3: 'alert alert-info-alt',
bs5: 'survey-notification',
})}
>
<div role="alert" className="survey-notification">
<div className="notification-body">
{survey.preText}&nbsp;
<a

View File

@@ -7,7 +7,6 @@ import { useProjectListContext } from '../../../../context/project-list-context'
import { archiveProject } from '../../../../util/api'
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
import OLIconButton from '@/features/ui/components/ol/ol-icon-button'
import { bsVersion } from '@/features/utils/bootstrap-5'
type ArchiveProjectButtonProps = {
project: Project
@@ -78,8 +77,7 @@ const ArchiveProjectButtonTooltip = memo(function ArchiveProjectButtonTooltip({
variant="link"
accessibilityLabel={text}
className="action-btn"
icon={bsVersion({ bs5: 'inbox', bs3: 'inbox' })}
bs3Props={{ fw: true }}
icon="inbox"
/>
</span>
</OLTooltip>

View File

@@ -1,7 +1,6 @@
import { useTranslation } from 'react-i18next'
import { memo, useCallback, useState } from 'react'
import { Project } from '../../../../../../../../types/project/dashboard/api'
import Icon from '../../../../../../shared/components/icon'
import * as eventTracking from '../../../../../../infrastructure/event-tracking'
import { useLocation } from '../../../../../../shared/hooks/use-location'
import useAbortController from '../../../../../../shared/hooks/use-abort-controller'
@@ -15,7 +14,6 @@ import OLModal, {
OLModalHeader,
OLModalTitle,
} from '@/features/ui/components/ol/ol-modal'
import { bsVersion } from '@/features/utils/bootstrap-5'
import OLIconButton from '@/features/ui/components/ol/ol-icon-button'
type CompileAndDownloadProjectPDFButtonProps = {
@@ -159,13 +157,7 @@ const CompileAndDownloadProjectPDFButtonTooltip = memo(
loadingLabel={text}
isLoading={pendingCompile}
className="action-btn"
icon={bsVersion({ bs5: 'picture_as_pdf', bs3: 'file-pdf-o' })}
bs3Props={{
fw: true,
loading: pendingCompile ? (
<Icon type="spinner" fw accessibilityLabel={text} spin />
) : null,
}}
icon="picture_as_pdf"
/>
</span>
</OLTooltip>

View File

@@ -12,7 +12,6 @@ import { useProjectTags } from '@/features/project-list/hooks/use-project-tags'
import { isSmallDevice } from '../../../../../../infrastructure/event-tracking'
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
import OLIconButton from '@/features/ui/components/ol/ol-icon-button'
import { bsVersion } from '@/features/utils/bootstrap-5'
type CopyButtonProps = {
project: Project
@@ -111,8 +110,7 @@ const CopyProjectButtonTooltip = memo(function CopyProjectButtonTooltip({
variant="link"
accessibilityLabel={text}
className="action-btn"
icon={bsVersion({ bs5: 'file_copy', bs3: 'files-o' })}
bs3Props={{ fw: true }}
icon="file_copy"
/>
</span>
</OLTooltip>

View File

@@ -8,7 +8,6 @@ import { useProjectListContext } from '../../../../context/project-list-context'
import getMeta from '@/utils/meta'
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
import OLIconButton from '@/features/ui/components/ol/ol-icon-button'
import { bsVersion } from '@/features/utils/bootstrap-5'
type DeleteProjectButtonProps = {
project: Project
@@ -76,8 +75,7 @@ const DeleteProjectButtonTooltip = memo(function DeleteProjectButtonTooltip({
variant="link"
accessibilityLabel={text}
className="action-btn"
icon={bsVersion({ bs5: 'block', bs3: 'ban' })}
bs3Props={{ fw: true }}
icon="block"
/>
</span>
</OLTooltip>

View File

@@ -6,7 +6,6 @@ import { useLocation } from '../../../../../../shared/hooks/use-location'
import { isSmallDevice } from '../../../../../../infrastructure/event-tracking'
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
import OLIconButton from '@/features/ui/components/ol/ol-icon-button'
import { bsVersion } from '@/features/utils/bootstrap-5'
type DownloadProjectButtonProps = {
project: Project
@@ -52,8 +51,7 @@ const DownloadProjectButtonTooltip = memo(
variant="link"
accessibilityLabel={text}
className="action-btn"
icon={bsVersion({ bs5: 'download', bs3: 'cloud-download' })}
bs3Props={{ fw: true }}
icon="download"
/>
</span>
</OLTooltip>

View File

@@ -8,7 +8,6 @@ import { Project } from '../../../../../../../../types/project/dashboard/api'
import getMeta from '@/utils/meta'
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
import OLIconButton from '@/features/ui/components/ol/ol-icon-button'
import { bsVersion } from '@/features/utils/bootstrap-5'
type LeaveProjectButtonProps = {
project: Project
@@ -75,8 +74,7 @@ const LeaveProjectButtonTooltip = memo(function LeaveProjectButtonTooltip({
variant="link"
accessibilityLabel={text}
className="action-btn"
icon={bsVersion({ bs5: 'logout', bs3: 'sign-out' })}
bs3Props={{ fw: true }}
icon="logout"
/>
</span>
</OLTooltip>

View File

@@ -7,7 +7,6 @@ import { useProjectListContext } from '../../../../context/project-list-context'
import { trashProject } from '../../../../util/api'
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
import OLIconButton from '@/features/ui/components/ol/ol-icon-button'
import { bsVersion } from '@/features/utils/bootstrap-5'
type TrashProjectButtonProps = {
project: Project
@@ -75,8 +74,7 @@ const TrashProjectButtonTooltip = memo(function TrashProjectButtonTooltip({
variant="link"
accessibilityLabel={text}
className="action-btn"
icon={bsVersion({ bs5: 'delete', bs3: 'trash' })}
bs3Props={{ fw: true }}
icon="delete"
/>
</span>
</OLTooltip>

View File

@@ -5,7 +5,6 @@ import { useProjectListContext } from '../../../../context/project-list-context'
import { unarchiveProject } from '../../../../util/api'
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
import OLIconButton from '@/features/ui/components/ol/ol-icon-button'
import { bsVersion } from '@/features/utils/bootstrap-5'
type UnarchiveProjectButtonProps = {
project: Project
@@ -54,8 +53,7 @@ const UnarchiveProjectButtonTooltip = memo(
variant="link"
accessibilityLabel={text}
className="action-btn"
icon={bsVersion({ bs5: 'restore_page', bs3: 'reply' })}
bs3Props={{ fw: true }}
icon="restore_page"
/>
</span>
</OLTooltip>

View File

@@ -5,7 +5,6 @@ import { useProjectListContext } from '../../../../context/project-list-context'
import { untrashProject } from '../../../../util/api'
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
import OLIconButton from '@/features/ui/components/ol/ol-icon-button'
import { bsVersion } from '@/features/utils/bootstrap-5'
type UntrashProjectButtonProps = {
project: Project
@@ -53,8 +52,7 @@ const UntrashProjectButtonTooltip = memo(function UntrashProjectButtonTooltip({
variant="link"
accessibilityLabel={text}
className="action-btn"
icon={bsVersion({ bs5: 'restore_page', bs3: 'reply' })}
bs3Props={{ fw: true }}
icon="restore_page"
/>
</span>
</OLTooltip>

View File

@@ -1,13 +1,10 @@
import { useCallback, useState, useRef } from 'react'
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { Tag as TagType } from '../../../../../../../app/src/Features/Tags/types'
import Icon from '../../../../../shared/components/icon'
import { useProjectListContext } from '../../../context/project-list-context'
import { removeProjectFromTag } from '../../../util/api'
import classnames from 'classnames'
import { getTagColor } from '../../../util/tag'
import Tag from '@/features/ui/components/bootstrap-5/tag'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
type InlineTagsProps = {
projectId: string
@@ -36,17 +33,6 @@ type InlineTagProps = {
function InlineTag({ tag, projectId }: InlineTagProps) {
const { t } = useTranslation()
const { selectTag, removeProjectFromTagInView } = useProjectListContext()
const [classNames, setClassNames] = useState('')
const tagLabelRef = useRef(null)
const tagBtnRef = useRef<HTMLButtonElement>(null)
const handleLabelClick = (e: React.MouseEvent) => {
// trigger the click on the button only when the event
// is triggered from the wrapper element
if (e.target === tagLabelRef.current) {
tagBtnRef.current?.click()
}
}
const handleRemoveTag = useCallback(
async (tagId: string, projectId: string) => {
@@ -55,66 +41,25 @@ function InlineTag({ tag, projectId }: InlineTagProps) {
},
[removeProjectFromTagInView]
)
const handleCloseMouseOver = () => setClassNames('tag-label-close-hover')
const handleCloseMouseOut = () => setClassNames('')
return (
<BootstrapVersionSwitcher
bs3={
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
<div
className={classnames('tag-label', classNames)}
onClick={handleLabelClick}
ref={tagLabelRef}
>
<button
className="label label-default tag-label-name"
aria-label={t('select_tag', { tagName: tag.name })}
ref={tagBtnRef}
onClick={() => selectTag(tag._id)}
>
<span
style={{
color: getTagColor(tag),
}}
>
<Icon type="circle" aria-hidden="true" />
</span>{' '}
{tag.name}
</button>
{/* eslint-disable-next-line jsx-a11y/mouse-events-have-key-events */}
<button
className="label label-default tag-label-remove"
aria-label={t('remove_tag', { tagName: tag.name })}
onClick={() => handleRemoveTag(tag._id, projectId)}
onMouseOver={handleCloseMouseOver}
onMouseOut={handleCloseMouseOut}
>
<span aria-hidden="true">×</span>
</button>
</div>
<Tag
prepend={
<i
className="badge-tag-circle"
style={{ backgroundColor: getTagColor(tag) }}
/>
}
bs5={
<Tag
prepend={
<i
className="badge-tag-circle"
style={{ backgroundColor: getTagColor(tag) }}
/>
}
contentProps={{
'aria-label': t('select_tag', { tagName: tag.name }),
onClick: () => selectTag(tag._id),
}}
closeBtnProps={{
onClick: () => handleRemoveTag(tag._id, projectId),
}}
className="ms-2"
>
{tag.name}
</Tag>
}
/>
contentProps={{
'aria-label': t('select_tag', { tagName: tag.name }),
onClick: () => selectTag(tag._id),
}}
closeBtnProps={{
onClick: () => handleRemoveTag(tag._id, projectId),
}}
className="ms-2"
>
{tag.name}
</Tag>
)
}

View File

@@ -1,9 +1,7 @@
import { useTranslation } from 'react-i18next'
import Icon from '../../../../../shared/components/icon'
import { getOwnerName } from '../../../util/project'
import { Project } from '../../../../../../../types/project/dashboard/api'
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
import MaterialIcon from '@/shared/components/material-icon'
type LinkSharingIconProps = {
@@ -28,21 +26,10 @@ function LinkSharingIcon({
{/* OverlayTrigger won't fire unless icon is wrapped in a span */}
<span className={className}>
{prependSpace ? ' ' : ''}
<BootstrapVersionSwitcher
bs3={
<Icon
type="link"
className="small"
accessibilityLabel={t('link_sharing')}
/>
}
bs5={
<MaterialIcon
type="link"
className="align-text-bottom"
accessibilityLabel={t('link_sharing')}
/>
}
<MaterialIcon
type="link"
className="align-text-bottom"
accessibilityLabel={t('link_sharing')}
/>
</span>
</OLTooltip>

View File

@@ -23,7 +23,6 @@ export const ProjectCheckbox = memo<{ projectId: string; projectName: string }>(
checked={selectedProjectIds.has(projectId)}
aria-label={t('select_project', { project: projectName })}
data-project-id={projectId}
bs3Props={{ bsClass: 'dash-cell-checkbox-wrapper' }}
/>
)
}

View File

@@ -8,8 +8,6 @@ import { getOwnerName } from '../../util/project'
import { Project } from '../../../../../../types/project/dashboard/api'
import { ProjectCheckbox } from './project-checkbox'
import { ProjectListOwnerName } from '@/features/project-list/components/table/project-list-owner-name'
import { bsVersion } from '@/features/utils/bootstrap-5'
import classnames from 'classnames'
type ProjectListTableRowProps = {
project: Project
@@ -19,79 +17,32 @@ function ProjectListTableRow({ project, selected }: ProjectListTableRowProps) {
const ownerName = getOwnerName(project)
return (
<tr className={selected ? bsVersion({ bs5: 'table-active' }) : undefined}>
<td
className={classnames(
'dash-cell-checkbox',
bsVersion({
bs5: 'd-none d-md-table-cell',
bs3: 'hidden-xs',
})
)}
>
<tr className={selected ? 'table-active' : undefined}>
<td className="dash-cell-checkbox d-none d-md-table-cell">
<ProjectCheckbox projectId={project.id} projectName={project.name} />
</td>
<td className="dash-cell-name">
<a href={`/project/${project.id}`}>{project.name}</a>{' '}
<InlineTags
className={bsVersion({
bs5: 'd-none d-md-inline',
bs3: 'hidden-xs',
})}
projectId={project.id}
/>
<InlineTags className="d-none d-md-inline" projectId={project.id} />
</td>
<td
className={classnames(
'dash-cell-date-owner',
'pb-0',
bsVersion({ bs5: 'd-md-none', bs3: 'visible-xs' })
)}
>
<td className="dash-cell-date-owner pb-0 d-md-none">
<LastUpdatedCell project={project} />
{ownerName ? <ProjectListOwnerName ownerName={ownerName} /> : null}
</td>
<td
className={classnames(
'dash-cell-owner',
bsVersion({
bs5: 'd-none d-md-table-cell',
bs3: 'hidden-xs',
})
)}
>
<td className="dash-cell-owner d-none d-md-table-cell">
<OwnerCell project={project} />
</td>
<td
className={classnames(
'dash-cell-date',
bsVersion({
bs5: 'd-none d-md-table-cell',
bs3: 'hidden-xs',
})
)}
>
<td className="dash-cell-date d-none d-md-table-cell">
<LastUpdatedCell project={project} />
</td>
<td
className={classnames(
'dash-cell-tag',
'pt-0',
bsVersion({ bs5: 'd-md-none', bs3: 'visible-xs' })
)}
>
<td className="dash-cell-tag pt-0 d-md-none">
<InlineTags projectId={project.id} />
</td>
<td className="dash-cell-actions">
<div
className={bsVersion({
bs5: 'd-none d-md-block',
bs3: 'hidden-xs',
})}
>
<div className="d-none d-md-block">
<ActionsCell project={project} />
</div>
<div className={bsVersion({ bs5: 'd-md-none', bs3: 'visible-xs' })}>
<div className="d-md-none">
<ActionsDropdown project={project} />
</div>
</td>

View File

@@ -1,6 +1,5 @@
import { useCallback, useRef, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import Icon from '../../../../shared/components/icon'
import ProjectListTableRow from './project-list-table-row'
import { useProjectListContext } from '../../context/project-list-context'
import useSort from '../../hooks/use-sort'
@@ -8,30 +7,16 @@ import withContent, { SortBtnProps } from '../sort/with-content'
import OLTable from '@/features/ui/components/ol/ol-table'
import OLFormCheckbox from '@/features/ui/components/ol/ol-form-checkbox'
import MaterialIcon from '@/shared/components/material-icon'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
import { bsVersion } from '@/features/utils/bootstrap-5'
import classnames from 'classnames'
function SortBtn({ onClick, text, iconType, screenReaderText }: SortBtnProps) {
return (
<button
className={classnames(
'table-header-sort-btn',
bsVersion({
bs5: 'd-none d-md-inline-block',
bs3: 'hidden-xs',
})
)}
className="table-header-sort-btn d-none d-md-inline-block"
onClick={onClick}
aria-label={screenReaderText}
>
<span className={bsVersion({ bs3: 'tablesort-text' })}>{text}</span>
{iconType && (
<BootstrapVersionSwitcher
bs3={<Icon type={iconType} />}
bs5={<MaterialIcon type={iconType} />}
/>
)}
<span>{text}</span>
{iconType && <MaterialIcon type={iconType} />}
</button>
)
}
@@ -66,26 +51,11 @@ function ProjectListTable() {
return (
<OLTable className="project-dash-table" container={false} hover>
<caption
className={bsVersion({ bs5: 'visually-hidden', bs3: 'sr-only' })}
>
{t('projects_list')}
</caption>
<thead
className={bsVersion({
bs5: 'visually-hidden-max-md',
bs3: 'sr-only-xs',
})}
>
<caption className="visually-hidden">{t('projects_list')}</caption>
<thead className="visually-hidden-max-md">
<tr>
<th
className={classnames(
'dash-cell-checkbox',
bsVersion({
bs5: 'd-none d-md-table-cell',
bs3: 'hidden-xs',
})
)}
className="dash-cell-checkbox d-none d-md-table-cell"
aria-label={t('select_projects')}
>
<OLFormCheckbox
@@ -97,10 +67,6 @@ function ProjectListTable() {
}
disabled={visibleProjects.length === 0}
aria-label={t('select_all_projects')}
bs3Props={{
bsClass: 'dash-cell-checkbox-wrapper',
inputRef: undefined,
}}
inputRef={checkAllRef}
/>
</th>
@@ -123,22 +89,13 @@ function ProjectListTable() {
/>
</th>
<th
className={classnames(
'dash-cell-date-owner',
bsVersion({ bs5: 'd-md-none', bs3: 'visible-xs' })
)}
className="dash-cell-date-owner d-md-none"
aria-label={t('date_and_owner')}
>
{t('date_and_owner')}
</th>
<th
className={classnames(
'dash-cell-owner',
bsVersion({
bs5: 'd-none d-md-table-cell',
bs3: 'hidden-xs',
})
)}
className="dash-cell-owner d-none d-md-table-cell"
aria-label={t('owner')}
aria-sort={
sort.by === 'owner'
@@ -156,13 +113,7 @@ function ProjectListTable() {
/>
</th>
<th
className={classnames(
'dash-cell-date',
bsVersion({
bs5: 'd-none d-md-table-cell',
bs3: 'hidden-xs',
})
)}
className="dash-cell-date d-none d-md-table-cell"
aria-label={t('last_modified')}
aria-sort={
sort.by === 'lastUpdated'
@@ -179,13 +130,7 @@ function ProjectListTable() {
onClick={() => handleSort('lastUpdated')}
/>
</th>
<th
className={classnames(
'dash-cell-tag',
bsVersion({ bs5: 'd-md-none', bs3: 'visible-xs' })
)}
aria-label={t('tags')}
>
<th className="dash-cell-tag d-md-none" aria-label={t('tags')}>
{t('tags')}
</th>
<th className="dash-cell-actions" aria-label={t('actions')}>

View File

@@ -7,7 +7,6 @@ import useIsMounted from '../../../../../../shared/hooks/use-is-mounted'
import { useProjectListContext } from '../../../../context/project-list-context'
import { archiveProject } from '../../../../util/api'
import { Project } from '../../../../../../../../types/project/dashboard/api'
import { bsVersion } from '@/features/utils/bootstrap-5'
function ArchiveProjectsButton() {
const { selectedProjects, toggleSelectedProject, updateProjectViewData } =
@@ -50,7 +49,7 @@ function ArchiveProjectsButton() {
onClick={handleOpenModal}
variant="secondary"
accessibilityLabel={text}
icon={bsVersion({ bs5: 'inbox', bs3: 'inbox' })}
icon="inbox"
/>
</OLTooltip>
<ArchiveProjectModal

View File

@@ -6,7 +6,6 @@ import * as eventTracking from '../../../../../../infrastructure/event-tracking'
import { useProjectListContext } from '../../../../context/project-list-context'
import { useLocation } from '../../../../../../shared/hooks/use-location'
import { isSmallDevice } from '../../../../../../infrastructure/event-tracking'
import { bsVersion } from '@/features/utils/bootstrap-5'
function DownloadProjectsButton() {
const { selectedProjects, selectOrUnselectAllProjects } =
@@ -39,7 +38,7 @@ function DownloadProjectsButton() {
onClick={handleDownloadProjects}
variant="secondary"
accessibilityLabel={text}
icon={bsVersion({ bs5: 'download', bs3: 'cloud-download' })}
icon="download"
/>
</OLTooltip>
)

View File

@@ -1,7 +1,5 @@
import { memo } from 'react'
import { Dropdown as BS3Dropdown } from 'react-bootstrap'
import { useTranslation } from 'react-i18next'
import ControlledDropdown from '../../../../../../shared/components/controlled-dropdown'
import CopyProjectMenuItem from '../menu-items/copy-project-menu-item'
import RenameProjectMenuItem from '../menu-items/rename-project-menu-item'
import {
@@ -9,46 +7,24 @@ import {
DropdownMenu,
DropdownToggle,
} from '@/features/ui/components/bootstrap-5/dropdown-menu'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
function ProjectToolsMoreDropdownButton() {
const { t } = useTranslation()
return (
<BootstrapVersionSwitcher
bs3={
<ControlledDropdown id="project-tools-more-dropdown">
<BS3Dropdown.Toggle bsStyle={null} className="btn-secondary">
{t('more')}
</BS3Dropdown.Toggle>
<BS3Dropdown.Menu
className="dropdown-menu-right"
data-testid="project-tools-more-dropdown-menu"
>
<RenameProjectMenuItem />
<CopyProjectMenuItem />
</BS3Dropdown.Menu>
</ControlledDropdown>
}
bs5={
<Dropdown align="end">
<DropdownToggle id="project-tools-more-dropdown" variant="secondary">
{t('more')}
</DropdownToggle>
<DropdownMenu
flip={false}
data-testid="project-tools-more-dropdown-menu"
>
<li role="none">
<RenameProjectMenuItem />
</li>
<li role="none">
<CopyProjectMenuItem />
</li>
</DropdownMenu>
</Dropdown>
}
/>
<Dropdown align="end">
<DropdownToggle id="project-tools-more-dropdown" variant="secondary">
{t('more')}
</DropdownToggle>
<DropdownMenu flip={false} data-testid="project-tools-more-dropdown-menu">
<li role="none">
<RenameProjectMenuItem />
</li>
<li role="none">
<CopyProjectMenuItem />
</li>
</DropdownMenu>
</Dropdown>
)
}

View File

@@ -1,15 +1,11 @@
import { sortBy } from 'lodash'
import { memo, useCallback } from 'react'
import { Button, Dropdown as BS3Dropdown } from 'react-bootstrap'
import { useTranslation } from 'react-i18next'
import ControlledDropdown from '../../../../../../shared/components/controlled-dropdown'
import Icon from '../../../../../../shared/components/icon'
import MaterialIcon from '../../../../../../shared/components/material-icon'
import { useProjectListContext } from '../../../../context/project-list-context'
import useTag from '../../../../hooks/use-tag'
import { addProjectsToTag, removeProjectsFromTag } from '../../../../util/api'
import { getTagColor } from '../../../../util/tag'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
import {
Dropdown,
DropdownDivider,
@@ -79,140 +75,62 @@ function TagsDropdown() {
[selectedProjects]
)
const containsSomeSelectedProjects = useCallback(
tag => {
for (const project of selectedProjects) {
if (tag.project_ids?.includes(project.id)) {
return true
}
}
return false
},
[selectedProjects]
)
return (
<>
<BootstrapVersionSwitcher
bs3={
<ControlledDropdown id="tags">
<BS3Dropdown.Toggle
bsStyle={null}
className="btn-secondary"
aria-label={t('tags')}
<Dropdown align="end" autoClose="outside">
<DropdownToggle
id="project-tools-more-dropdown"
variant="secondary"
aria-label={t('tags')}
>
<MaterialIcon type="label" className="align-text-top" />
</DropdownToggle>
<DropdownMenu
flip={false}
data-testid="project-tools-more-dropdown-menu"
>
<DropdownHeader>{t('add_to_tag')}</DropdownHeader>
{sortBy(tags, tag => tag.name?.toLowerCase()).map((tag, index) => (
<li role="none" key={tag._id}>
<DropdownItem
onClick={e =>
containsAllSelectedProjects(tag)
? handleRemoveTagFromSelectedProjects(e, tag._id)
: handleAddTagToSelectedProjects(e, tag._id)
}
aria-label={t('add_or_remove_project_from_tag', {
tagName: tag.name,
})}
as="button"
tabIndex={-1}
leadingIcon={
containsAllSelectedProjects(tag) ? (
'check'
) : (
<DropdownItem.EmptyLeadingIcon />
)
}
>
<span
className="badge-tag-circle align-self-center ms-0"
style={{ backgroundColor: getTagColor(tag) }}
/>
<span className="text-truncate">{tag.name}</span>
</DropdownItem>
</li>
))}
<DropdownDivider />
<li role="none">
<DropdownItem
onClick={handleOpenCreateTagModal}
as="button"
tabIndex={-1}
>
<MaterialIcon type="label" style={{ verticalAlign: 'sub' }} />
</BS3Dropdown.Toggle>
<BS3Dropdown.Menu className="dropdown-menu-right">
<li className="dropdown-header" role="heading" aria-level={3}>
{t('add_to_tag')}
</li>
{sortBy(tags, tag => tag.name?.toLowerCase()).map(tag => {
return (
<li key={tag._id}>
<Button
className="tag-dropdown-button"
onClick={e =>
containsAllSelectedProjects(tag)
? handleRemoveTagFromSelectedProjects(e, tag._id)
: handleAddTagToSelectedProjects(e, tag._id)
}
aria-label={t('add_or_remove_project_from_tag', {
tagName: tag.name,
})}
>
<Icon
type={
containsAllSelectedProjects(tag)
? 'check-square-o'
: containsSomeSelectedProjects(tag)
? 'minus-square-o'
: 'square-o'
}
className="tag-checkbox"
/>{' '}
<span
className="tag-dot"
style={{
backgroundColor: getTagColor(tag),
}}
/>{' '}
{tag.name}
</Button>
</li>
)
})}
<li className="divider" />
<li>
<Button
className="tag-dropdown-button"
onClick={handleOpenCreateTagModal}
>
{t('create_new_tag')}
</Button>
</li>
</BS3Dropdown.Menu>
</ControlledDropdown>
}
bs5={
<Dropdown align="end" autoClose="outside">
<DropdownToggle
id="project-tools-more-dropdown"
variant="secondary"
aria-label={t('tags')}
>
<MaterialIcon type="label" className="align-text-top" />
</DropdownToggle>
<DropdownMenu
flip={false}
data-testid="project-tools-more-dropdown-menu"
>
<DropdownHeader>{t('add_to_tag')}</DropdownHeader>
{sortBy(tags, tag => tag.name?.toLowerCase()).map(
(tag, index) => (
<li role="none" key={tag._id}>
<DropdownItem
onClick={e =>
containsAllSelectedProjects(tag)
? handleRemoveTagFromSelectedProjects(e, tag._id)
: handleAddTagToSelectedProjects(e, tag._id)
}
aria-label={t('add_or_remove_project_from_tag', {
tagName: tag.name,
})}
as="button"
tabIndex={-1}
leadingIcon={
containsAllSelectedProjects(tag) ? (
'check'
) : (
<DropdownItem.EmptyLeadingIcon />
)
}
>
<span
className="badge-tag-circle align-self-center ms-0"
style={{ backgroundColor: getTagColor(tag) }}
/>
<span className="text-truncate">{tag.name}</span>
</DropdownItem>
</li>
)
)}
<DropdownDivider />
<li role="none">
<DropdownItem
onClick={handleOpenCreateTagModal}
as="button"
tabIndex={-1}
>
{t('create_new_tag')}
</DropdownItem>
</li>
</DropdownMenu>
</Dropdown>
}
/>
{t('create_new_tag')}
</DropdownItem>
</li>
</DropdownMenu>
</Dropdown>
<CreateTagModal id="toolbar-create-tag-modal" />
</>
)

View File

@@ -7,7 +7,6 @@ import useIsMounted from '../../../../../../shared/hooks/use-is-mounted'
import { useProjectListContext } from '../../../../context/project-list-context'
import { trashProject } from '../../../../util/api'
import { Project } from '../../../../../../../../types/project/dashboard/api'
import { bsVersion } from '@/features/utils/bootstrap-5'
function TrashProjectsButton() {
const { selectedProjects, toggleSelectedProject, updateProjectViewData } =
@@ -50,7 +49,7 @@ function TrashProjectsButton() {
onClick={handleOpenModal}
variant="secondary"
accessibilityLabel={text}
icon={bsVersion({ bs5: 'delete', bs3: 'trash' })}
icon="delete"
/>
</OLTooltip>
<TrashProjectModal

View File

@@ -1,9 +1,6 @@
import { Button } from 'react-bootstrap'
import { useTranslation } from 'react-i18next'
import MaterialIcon from '../../../shared/components/material-icon'
import { getTagColor } from '../util/tag'
import MenuItemButton from './dropdown/menu-item-button'
import Icon from '../../../shared/components/icon'
import {
UNCATEGORIZED_KEY,
useProjectListContext,
@@ -12,7 +9,6 @@ import useTag from '../hooks/use-tag'
import { sortBy } from 'lodash'
import { Tag } from '../../../../../app/src/Features/Tags/types'
import { DropdownItem } from '@/features/ui/components/bootstrap-5/dropdown-menu'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
type TagsListProps = {
onTagClick?: () => void
@@ -39,143 +35,63 @@ function TagsList({ onTagClick, onEditClick }: TagsListProps) {
return (
<>
<BootstrapVersionSwitcher
bs3={
<>
{sortBy(tags, ['name']).map((tag, index) => (
<MenuItemButton
key={index}
onClick={e =>
handleClick(e as unknown as React.MouseEvent, tag)
}
className="projects-types-menu-item projects-types-menu-tag-item"
afterNode={
<Button
onClick={e => {
e.stopPropagation()
handleManageTag(e, tag._id)
onEditClick?.()
}}
className="btn-transparent edit-btn me-2"
bsStyle={null}
>
<Icon type="pencil" fw />
</Button>
}
>
<span className="tag-item menu-item-button-text">
{selectedTagId === tag._id ? (
<Icon type="check" className="menu-item-button-icon" />
) : null}
<span
className="me-2"
style={{
color: getTagColor(tag),
}}
>
<MaterialIcon
type="label"
style={{ verticalAlign: 'sub' }}
/>
</span>
<span className="tag-name-and-size">
<span className="tag-name">{tag.name}</span>
<span className="subdued">({tag.project_ids?.length})</span>
</span>
<>
{sortBy(tags, ['name']).map((tag, index) => (
<li role="none" className="position-relative" key={index}>
<DropdownItem
as="button"
tabIndex={-1}
onClick={e => handleClick(e as unknown as React.MouseEvent, tag)}
leadingIcon={
<span style={{ color: getTagColor(tag) }}>
<MaterialIcon type="label" className="align-text-top" />
</span>
</MenuItemButton>
))}
<MenuItemButton
className="untagged projects-types-menu-item"
onClick={() => {
selectTag(UNCATEGORIZED_KEY)
onTagClick?.()
}}
}
trailingIcon={selectedTagId === tag._id ? 'check' : undefined}
active={selectedTagId === tag._id}
>
{selectedTagId === UNCATEGORIZED_KEY ? (
<Icon type="check" className="menu-item-button-icon" />
) : null}
<span className="tag-item menu-item-button-text">
{t('uncategorized')}&nbsp;
<span className="subdued">({untaggedProjectsCount})</span>
<span className="project-menu-item-tag-name text-truncate">
{tag.name}&nbsp;({tag.project_ids?.length})
</span>
</MenuItemButton>
<MenuItemButton
onClick={() => {
openCreateTagModal()
onTagClick?.()
</DropdownItem>
<DropdownItem
as="button"
tabIndex={-1}
className="project-menu-item-edit-btn"
onClick={e => {
e.stopPropagation()
handleManageTag(e, tag._id)
}}
className="projects-types-menu-item"
aria-label={t('edit_tag')}
>
<span className="tag-item menu-item-button-text">
<Icon type="plus" className="me-2" />
<span>{t('new_tag')}</span>
</span>
</MenuItemButton>
</>
}
bs5={
<>
{sortBy(tags, ['name']).map((tag, index) => (
<li role="none" className="position-relative" key={index}>
<DropdownItem
as="button"
tabIndex={-1}
onClick={e =>
handleClick(e as unknown as React.MouseEvent, tag)
}
leadingIcon={
<span style={{ color: getTagColor(tag) }}>
<MaterialIcon type="label" className="align-text-top" />
</span>
}
trailingIcon={selectedTagId === tag._id ? 'check' : undefined}
active={selectedTagId === tag._id}
>
<span className="project-menu-item-tag-name text-truncate">
{tag.name}&nbsp;({tag.project_ids?.length})
</span>
</DropdownItem>
<DropdownItem
as="button"
tabIndex={-1}
className="project-menu-item-edit-btn"
onClick={e => {
e.stopPropagation()
handleManageTag(e, tag._id)
}}
aria-label={t('edit_tag')}
>
<MaterialIcon type="edit" className="align-text-top" />
</DropdownItem>
</li>
))}
<li role="none">
<DropdownItem
as="button"
tabIndex={-1}
onClick={() => selectTag(UNCATEGORIZED_KEY)}
trailingIcon={
selectedTagId === UNCATEGORIZED_KEY ? 'check' : undefined
}
active={selectedTagId === UNCATEGORIZED_KEY}
>
{t('uncategorized')}&nbsp;({untaggedProjectsCount})
</DropdownItem>
</li>
<li role="none">
<DropdownItem
as="button"
tabIndex={-1}
onClick={openCreateTagModal}
leadingIcon="add"
>
{t('new_tag')}
</DropdownItem>
</li>
</>
}
/>
<MaterialIcon type="edit" className="align-text-top" />
</DropdownItem>
</li>
))}
<li role="none">
<DropdownItem
as="button"
tabIndex={-1}
onClick={() => selectTag(UNCATEGORIZED_KEY)}
trailingIcon={
selectedTagId === UNCATEGORIZED_KEY ? 'check' : undefined
}
active={selectedTagId === UNCATEGORIZED_KEY}
>
{t('uncategorized')}&nbsp;({untaggedProjectsCount})
</DropdownItem>
</li>
<li role="none">
<DropdownItem
as="button"
tabIndex={-1}
onClick={openCreateTagModal}
leadingIcon="add"
>
{t('new_tag')}
</DropdownItem>
</li>
</>
<CreateTagModal id="create-tag-modal-dropdown" disableCustomColor />
<ManageTagModal id="manage-tag-modal-dropdown" />
</>

View File

@@ -1,4 +1,4 @@
import { useCallback, useState, forwardRef } from 'react'
import { useCallback, forwardRef } from 'react'
import { useTranslation } from 'react-i18next'
import { sendMB } from '../../../../infrastructure/event-tracking'
import getMeta from '../../../../utils/meta'
@@ -11,7 +11,6 @@ import {
DropdownMenu,
DropdownToggle,
} from '@/features/ui/components/bootstrap-5/dropdown-menu'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
const CustomDropdownToggle = forwardRef<
HTMLButtonElement,
@@ -57,36 +56,11 @@ type WelcomeMessageCreateNewProjectDropdownProps = {
function WelcomeMessageCreateNewProjectDropdown({
setActiveModal,
}: WelcomeMessageCreateNewProjectDropdownProps) {
const [showDropdown, setShowDropdown] = useState(false)
const { t } = useTranslation()
const portalTemplates = getMeta('ol-portalTemplates') || []
const { isOverleaf } = getMeta('ol-ExposedSettings')
const handleClick = useCallback(() => {
sendMB('welcome-page-create-first-project-click', {
dropdownMenu: 'main-button',
dropdownOpen: showDropdown,
})
// toggle the dropdown
setShowDropdown(!showDropdown)
}, [setShowDropdown, showDropdown])
const handleKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLDivElement>) => {
if (e.code === 'Enter') {
handleClick()
} else if (e.code === 'Space') {
handleClick()
// prevent page down when pressing space
e.preventDefault()
}
},
[handleClick]
)
const handleDropdownItemClick = useCallback(
(
e: React.MouseEvent,
@@ -96,15 +70,13 @@ function WelcomeMessageCreateNewProjectDropdown({
// prevent firing the main dropdown onClick event
e.stopPropagation()
setShowDropdown(false)
sendMB('welcome-page-create-first-project-click', {
dropdownOpen: true,
dropdownMenu: dropdownMenuEvent,
})
setActiveModal(modalVariant)
},
[setActiveModal, setShowDropdown]
[setActiveModal]
)
const handlePortalTemplateClick = useCallback(
@@ -112,187 +84,91 @@ function WelcomeMessageCreateNewProjectDropdown({
// prevent firing the main dropdown onClick event
e.stopPropagation()
setShowDropdown(false)
sendMB('welcome-page-create-first-project-click', {
dropdownMenu: 'institution-template',
dropdownOpen: true,
institutionTemplateName,
})
},
[setShowDropdown]
[]
)
return (
<BootstrapVersionSwitcher
bs3={
<div className="welcome-message-card-item">
<div
role="button"
tabIndex={0}
className="card welcome-message-card"
onClick={handleClick}
onKeyDown={handleKeyDown}
<Dropdown>
<DropdownToggle
as={CustomDropdownToggle}
id="create-new-project-dropdown-toggle-btn"
/>
<DropdownMenu flip={false} className="create-new-project-dropdown">
<li role="none">
<DropdownItem
as="button"
onClick={e =>
handleDropdownItemClick(e, 'blank_project', 'blank-project')
}
tabIndex={-1}
>
<p>{t('create_a_new_project')}</p>
<img
className="welcome-message-card-img"
src="/img/welcome-page/create-a-new-project.svg"
aria-hidden="true"
alt=""
/>
{showDropdown && (
<div className="card create-new-project-dropdown">
<button
onClick={e =>
handleDropdownItemClick(e, 'blank_project', 'blank-project')
}
>
{t('blank_project')}
</button>
<button
onClick={e =>
handleDropdownItemClick(
e,
'example_project',
'example-project'
)
}
>
{t('example_project')}
</button>
<button
onClick={e =>
handleDropdownItemClick(
e,
'upload_project',
'upload-project'
)
}
>
{t('upload_project')}
</button>
{isOverleaf && (
<button
onClick={e =>
handleDropdownItemClick(
e,
'import_from_github',
'import-from-github'
)
}
>
{t('import_from_github')}
</button>
)}
{portalTemplates.length > 0 ? (
<>
<hr />
<div className="dropdown-header">
{t('institution_templates')}
</div>
{portalTemplates?.map((portalTemplate, index) => (
<a
key={`portal-template-${index}`}
href={`${portalTemplate.url}#templates`}
onClick={e =>
handlePortalTemplateClick(e, portalTemplate.name)
}
>
{portalTemplate.name}
</a>
))}
</>
) : null}
</div>
)}
</div>
</div>
}
bs5={
<Dropdown className="welcome-message-card-item">
<DropdownToggle
as={CustomDropdownToggle}
id="create-new-project-dropdown-toggle-btn"
/>
<DropdownMenu flip={false} className="create-new-project-dropdown">
<li role="none">
{t('blank_project')}
</DropdownItem>
</li>
<li role="none">
<DropdownItem
as="button"
onClick={e =>
handleDropdownItemClick(e, 'example_project', 'example-project')
}
tabIndex={-1}
>
{t('example_project')}
</DropdownItem>
</li>
<li role="none">
<DropdownItem
as="button"
onClick={e =>
handleDropdownItemClick(e, 'upload_project', 'upload-project')
}
tabIndex={-1}
>
{t('upload_project')}
</DropdownItem>
</li>
{isOverleaf && (
<li role="none">
<DropdownItem
as="button"
onClick={e =>
handleDropdownItemClick(
e,
'import_from_github',
'import-from-github'
)
}
tabIndex={-1}
>
{t('import_from_github')}
</DropdownItem>
</li>
)}
{(portalTemplates?.length ?? 0) > 0 ? (
<>
<DropdownDivider />
<DropdownHeader aria-hidden="true">
{t('institution_templates')}
</DropdownHeader>
{portalTemplates?.map((portalTemplate, index) => (
<DropdownItem
as="button"
onClick={e =>
handleDropdownItemClick(e, 'blank_project', 'blank-project')
}
tabIndex={-1}
key={`portal-template-${index}`}
onClick={e => handlePortalTemplateClick(e, portalTemplate.name)}
href={`${portalTemplate.url}#templates`}
>
{t('blank_project')}
{portalTemplate.name}
</DropdownItem>
</li>
<li role="none">
<DropdownItem
as="button"
onClick={e =>
handleDropdownItemClick(
e,
'example_project',
'example-project'
)
}
tabIndex={-1}
>
{t('example_project')}
</DropdownItem>
</li>
<li role="none">
<DropdownItem
as="button"
onClick={e =>
handleDropdownItemClick(e, 'upload_project', 'upload-project')
}
tabIndex={-1}
>
{t('upload_project')}
</DropdownItem>
</li>
{isOverleaf && (
<li role="none">
<DropdownItem
as="button"
onClick={e =>
handleDropdownItemClick(
e,
'import_from_github',
'import-from-github'
)
}
tabIndex={-1}
>
{t('import_from_github')}
</DropdownItem>
</li>
)}
{(portalTemplates?.length ?? 0) > 0 ? (
<>
<DropdownDivider />
<DropdownHeader aria-hidden="true">
{t('institution_templates')}
</DropdownHeader>
{portalTemplates?.map((portalTemplate, index) => (
<DropdownItem
key={`portal-template-${index}`}
onClick={e =>
handlePortalTemplateClick(e, portalTemplate.name)
}
href={`${portalTemplate.url}#templates`}
>
{portalTemplate.name}
</DropdownItem>
))}
</>
) : null}
</DropdownMenu>
</Dropdown>
}
/>
))}
</>
) : null}
</DropdownMenu>
</Dropdown>
)
}

View File

@@ -117,7 +117,7 @@ describe('<NewProjectButton />', function () {
// dynamic menu based on portalTemplates
const affiliationTemplate = screen.getByRole('menuitem', {
name: 'Affiliation 1',
name: 'Affiliation 1 Template',
})
expect(affiliationTemplate.getAttribute('href')).to.equal(
'/edu/test-new-template#templates'

View File

@@ -1,5 +1,5 @@
import { expect } from 'chai'
import sinon from 'sinon'
import sinon, { SinonStub } from 'sinon'
import {
fireEvent,
render,
@@ -46,6 +46,7 @@ import {
individualSubscription,
} from '../fixtures/user-subscriptions'
import getMeta from '@/utils/meta'
import * as bootstrapUtils from '@/features/utils/bootstrap-5'
const renderWithinProjectListProvider = (Component: React.ComponentType) => {
render(<Component />, {
@@ -65,6 +66,16 @@ describe('<UserNotifications />', function () {
appName: 'Overleaf',
}
let isBootstrap5Stub: SinonStub
before(function () {
isBootstrap5Stub = sinon.stub(bootstrapUtils, 'isBootstrap5').returns(true)
})
after(function () {
isBootstrap5Stub.restore()
})
beforeEach(function () {
fetchMock.reset()
@@ -127,15 +138,15 @@ describe('<UserNotifications />', function () {
expect(joinBtn.disabled).to.be.true
await waitForElementToBeRemoved(() =>
screen.getByRole('button', { name: /joining/i })
)
screen.debug()
await waitForElementToBeRemoved(() => screen.getByText('Loading'))
expect(acceptMock.called()).to.be.true
screen.getByText(/joined/i)
expect(screen.queryByRole('button', { name: /join project/i })).to.be.null
const openProject = screen.getByRole('link', { name: /open project/i })
const openProject = screen.getByRole('button', { name: /open project/i })
expect(openProject.getAttribute('href')).to.equal(
`/project/${notificationProjectInvite.messageOpts.projectId}`
)
@@ -173,12 +184,12 @@ describe('<UserNotifications />', function () {
fireEvent.click(joinBtn)
await waitForElementToBeRemoved(() =>
screen.getByRole('button', { name: /joining/i })
screen.getByRole('button', { name: /loading/i })
)
expect(fetchMock.called()).to.be.true
screen.getByRole('button', { name: /join project/i })
expect(screen.queryByRole('link', { name: /open project/i })).to.be.null
expect(screen.queryByRole('button', { name: /open project/i })).to.be.null
})
it('shows WFH2020', async function () {
@@ -197,7 +208,7 @@ describe('<UserNotifications />', function () {
screen.getByRole('alert')
screen.getByText(/your free WFH2020 upgrade came to an end on/i)
const viewLink = screen.getByRole('link', { name: /view/i })
const viewLink = screen.getByRole('button', { name: /view/i })
expect(viewLink.getAttribute('href')).to.equal(
'https://www.overleaf.com/events/wfh2020'
)
@@ -236,7 +247,7 @@ describe('<UserNotifications />', function () {
expect(findOutMore.getAttribute('href')).to.equal(
'https://www.overleaf.com/learn/how-to/Institutional_Login'
)
const linkAccount = screen.getByRole('link', { name: /link account/i })
const linkAccount = screen.getByRole('button', { name: /link account/i })
expect(linkAccount.getAttribute('href')).to.equal(
`${exposedSettings.samlInitPath}?university_id=${notificationIPMatchedAffiliation.messageOpts.institutionId}&auto=/project`
)
@@ -271,7 +282,7 @@ describe('<UserNotifications />', function () {
/add an institutional email address to claim your features/i
)
const addAffiliation = screen.getByRole('link', {
const addAffiliation = screen.getByRole('button', {
name: /add affiliation/i,
})
expect(addAffiliation.getAttribute('href')).to.equal(`/user/settings`)
@@ -297,7 +308,7 @@ describe('<UserNotifications />', function () {
screen.getByText(/file limit/i)
screen.getByText(/You can't add more files to the project or sync it/i)
const accountSettings = screen.getByRole('link', {
const accountSettings = screen.getByRole('button', {
name: /Open project/i,
})
expect(accountSettings.getAttribute('href')).to.equal('/project/123')
@@ -487,7 +498,7 @@ describe('<UserNotifications />', function () {
'/learn/how-to/Institutional_Login'
)
const action = screen.getByRole('link', { name: /link account/i })
const action = screen.getByRole('button', { name: /link account/i })
expect(action.getAttribute('href')).to.equal(
`${exposedSettings.samlInitPath}?university_id=${notificationsInstitution.institutionId}&auto=/project&email=${notificationsInstitution.email}`
)
@@ -552,7 +563,7 @@ describe('<UserNotifications />', function () {
screen.getByRole('alert')
screen.getByText(/which is already registered with/i)
const action = screen.getByRole('link', { name: /find out more/i })
const action = screen.getByRole('button', { name: /find out more/i })
expect(action.getAttribute('href')).to.equal(
'/learn/how-to/Institutional_Login'
)
@@ -735,7 +746,7 @@ describe('<UserNotifications />', function () {
screen.getByRole('button', { name: /confirm affiliation/i })
)
await waitForElementToBeRemoved(() => screen.getByText(/sending/i))
await waitForElementToBeRemoved(() => screen.getByText(/loading/i))
screen.getByText(/check your email inbox to confirm/i)
expect(screen.queryByRole('button', { name: /confirm affiliation/i })).to
.be.null
@@ -745,7 +756,7 @@ describe('<UserNotifications />', function () {
fireEvent.click(
screen.getByRole('button', { name: /resend confirmation email/i })
)
await waitForElementToBeRemoved(() => screen.getByText(/sending/i))
await waitForElementToBeRemoved(() => screen.getByText('Loading'))
expect(sendReconfirmationMock.calls()).to.have.lengthOf(2)
})
@@ -814,7 +825,7 @@ describe('<UserNotifications />', function () {
renderWithinProjectListProvider(GroupsAndEnterpriseBanner)
await fetchMock.flush(true)
expect(screen.queryByRole('link', { name: 'Contact Sales' })).to.be.null
expect(screen.queryByRole('button', { name: 'Contact Sales' })).to.be.null
})
it('shows the banner for users that have dismissed the previous banners', async function () {
@@ -824,7 +835,7 @@ describe('<UserNotifications />', function () {
renderWithinProjectListProvider(GroupsAndEnterpriseBanner)
await fetchMock.flush(true)
expect(screen.queryByRole('link', { name: 'Contact Sales' })).to.not.be
expect(screen.queryByRole('button', { name: 'Contact Sales' })).to.not.be
.null
})
@@ -840,7 +851,7 @@ describe('<UserNotifications />', function () {
renderWithinProjectListProvider(GroupsAndEnterpriseBanner)
await fetchMock.flush(true)
expect(screen.queryByRole('link', { name: 'Contact Sales' })).to.not.be
expect(screen.queryByRole('button', { name: 'Contact Sales' })).to.not.be
.null
})
@@ -856,7 +867,7 @@ describe('<UserNotifications />', function () {
renderWithinProjectListProvider(GroupsAndEnterpriseBanner)
await fetchMock.flush(true)
expect(screen.queryByRole('link', { name: 'Contact Sales' })).to.be.null
expect(screen.queryByRole('button', { name: 'Contact Sales' })).to.be.null
})
describe('users that are not in group and are not affiliated', function () {
@@ -897,7 +908,7 @@ describe('<UserNotifications />', function () {
screen.getByText(
'Overleaf On-Premises: Does your company want to keep its data within its firewall? Overleaf offers Server Pro, an on-premises solution for companies. Get in touch to learn more.'
)
const link = screen.getByRole('link', { name: 'Contact Sales' })
const link = screen.getByRole('button', { name: 'Contact Sales' })
expect(link.getAttribute('href')).to.equal(`/for/contact-sales-2`)
})
@@ -914,7 +925,7 @@ describe('<UserNotifications />', function () {
screen.getByText(
'Why do Fortune 500 companies and top research institutions trust Overleaf to streamline their collaboration? Get in touch to learn more.'
)
const link = screen.getByRole('link', { name: 'Contact Sales' })
const link = screen.getByRole('button', { name: 'Contact Sales' })
expect(link.getAttribute('href')).to.equal(`/for/contact-sales-4`)
})
@@ -941,7 +952,7 @@ describe('<UserNotifications />', function () {
})
it('shows the banner', function () {
renderWithinProjectListProvider(UserNotifications)
const ctaLink = screen.getByRole('link', {
const ctaLink = screen.getByRole('button', {
name: 'Get Writefull Premium',
})
expect(ctaLink.getAttribute('href')).to.equal(
@@ -951,7 +962,7 @@ describe('<UserNotifications />', function () {
it('dismisses the banner when the close button is clicked', function () {
renderWithinProjectListProvider(UserNotifications)
screen.getByRole('link', { name: /Writefull/ })
screen.getByRole('button', { name: /Writefull/ })
const WritefullPromoBanner = screen.getByTestId(
'writefull-premium-promo-banner'
)

View File

@@ -158,14 +158,12 @@ describe('<TagsList />', function () {
describe('Edit modal', function () {
beforeEach(async function () {
const tag1Button = screen.getByText('Tag 1')
const editButton = within(
const dropdownToggle = within(
tag1Button.closest('li') as HTMLElement
).getByRole('button', {
name: 'Edit',
})
await fireEvent.click(editButton)
).getByTestId('tag-dropdown-toggle')
await fireEvent.click(dropdownToggle)
const editMenuItem = await screen.findByRole('menuitem', { name: 'Edit' })
await fireEvent.click(editMenuItem)
})
it('modal is open', async function () {
@@ -250,15 +248,15 @@ describe('<TagsList />', function () {
describe('Delete modal', function () {
beforeEach(async function () {
const tag1Button = screen.getByText('Another tag')
const deleteButton = within(
const tag1Button = screen.getByText('Tag 1')
const dropdownToggle = within(
tag1Button.closest('li') as HTMLElement
).getByRole('button', {
).getByTestId('tag-dropdown-toggle')
await fireEvent.click(dropdownToggle)
const deleteMenuItem = await screen.findByRole('menuitem', {
name: 'Delete',
})
await fireEvent.click(deleteButton)
await fireEvent.click(deleteMenuItem)
})
it('modal is open', async function () {

View File

@@ -11,13 +11,22 @@ describe('<ProjectTools />', function () {
resetProjectListContextFetch()
})
it('renders the project tools for the all projects filter', function () {
it('renders the project tools for the all projects filter', async function () {
renderWithProjectListContext(<ProjectTools />)
expect(screen.getAllByRole('button').length).to.equal(5)
screen.getByLabelText('Download')
screen.getByLabelText('Archive')
screen.getByLabelText('Trash')
screen.getByLabelText('Tags')
screen.getByRole('button', { name: 'Create new tag' })
const initialButtons = screen.getAllByRole('button')
expect(initialButtons).to.have.length(4)
expect(screen.getByLabelText('Download')).to.exist
expect(screen.getByLabelText('Archive')).to.exist
expect(screen.getByLabelText('Trash')).to.exist
expect(screen.getByLabelText('Tags')).to.exist
expect(screen.queryByText('Create new tag')).to.not.exist
screen.getByLabelText('Tags').click()
const createTagButton = await screen.findByText('Create new tag')
expect(createTagButton).to.exist
})
})

View File

@@ -61,7 +61,7 @@ describe('<WelcomeMessage />', function () {
screen.getByText('Institution Templates')
// dynamic menu based on portalTemplates
const affiliationTemplate = screen.getByRole('link', {
const affiliationTemplate = screen.getByRole('menuitem', {
name: 'Affiliation 1',
})