ソースを参照

[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
Antoine Clausse 1 年間 前
コミット
9e745ed7ae
50 ファイル変更553 行追加1823 行削除
  1. 0 9
      services/web/app/src/Features/Project/ProjectListController.mjs
  2. 0 1
      services/web/app/views/project/list-react.pug
  3. 0 1
      services/web/frontend/extracted-translations.json
  4. 3 24
      services/web/frontend/js/features/project-list/components/current-plan-widget/commons-plan.tsx
  5. 4 27
      services/web/frontend/js/features/project-list/components/current-plan-widget/free-plan.tsx
  6. 3 24
      services/web/frontend/js/features/project-list/components/current-plan-widget/group-plan.tsx
  7. 3 21
      services/web/frontend/js/features/project-list/components/current-plan-widget/individual-plan.tsx
  8. 1 258
      services/web/frontend/js/features/project-list/components/dropdown/actions-dropdown.tsx
  9. 40 111
      services/web/frontend/js/features/project-list/components/dropdown/projects-dropdown.tsx
  10. 43 112
      services/web/frontend/js/features/project-list/components/dropdown/sort-by-dropdown.tsx
  11. 0 3
      services/web/frontend/js/features/project-list/components/modals/create-tag-modal.tsx
  12. 0 3
      services/web/frontend/js/features/project-list/components/modals/delete-tag-modal.tsx
  13. 0 3
      services/web/frontend/js/features/project-list/components/modals/edit-tag-modal.tsx
  14. 1 12
      services/web/frontend/js/features/project-list/components/modals/manage-tag-modal.tsx
  15. 102 214
      services/web/frontend/js/features/project-list/components/new-project-button.tsx
  16. 0 3
      services/web/frontend/js/features/project-list/components/new-project-button/modal-content-new-project-form.tsx
  17. 0 16
      services/web/frontend/js/features/project-list/components/notifications/groups/affiliation/reconfirm-affiliation.tsx
  18. 0 8
      services/web/frontend/js/features/project-list/components/notifications/groups/common.tsx
  19. 13 72
      services/web/frontend/js/features/project-list/components/project-list-root.tsx
  20. 4 19
      services/web/frontend/js/features/project-list/components/search-form.tsx
  21. 1 6
      services/web/frontend/js/features/project-list/components/sidebar/sidebar.tsx
  22. 25 60
      services/web/frontend/js/features/project-list/components/sidebar/tags-list.tsx
  23. 1 4
      services/web/frontend/js/features/project-list/components/sort/with-content.tsx
  24. 1 8
      services/web/frontend/js/features/project-list/components/survey-widget.tsx
  25. 1 3
      services/web/frontend/js/features/project-list/components/table/cells/action-buttons/archive-project-button.tsx
  26. 1 9
      services/web/frontend/js/features/project-list/components/table/cells/action-buttons/compile-and-download-project-pdf-button.tsx
  27. 1 3
      services/web/frontend/js/features/project-list/components/table/cells/action-buttons/copy-project-button.tsx
  28. 1 3
      services/web/frontend/js/features/project-list/components/table/cells/action-buttons/delete-project-button.tsx
  29. 1 3
      services/web/frontend/js/features/project-list/components/table/cells/action-buttons/download-project-button.tsx
  30. 1 3
      services/web/frontend/js/features/project-list/components/table/cells/action-buttons/leave-project-button.tsx
  31. 1 3
      services/web/frontend/js/features/project-list/components/table/cells/action-buttons/trash-project-button.tsx
  32. 1 3
      services/web/frontend/js/features/project-list/components/table/cells/action-buttons/unarchive-project-button.tsx
  33. 1 3
      services/web/frontend/js/features/project-list/components/table/cells/action-buttons/untrash-project-button.tsx
  34. 18 73
      services/web/frontend/js/features/project-list/components/table/cells/inline-tags.tsx
  35. 4 17
      services/web/frontend/js/features/project-list/components/table/cells/owner-cell.tsx
  36. 0 1
      services/web/frontend/js/features/project-list/components/table/project-checkbox.tsx
  37. 9 58
      services/web/frontend/js/features/project-list/components/table/project-list-table-row.tsx
  38. 10 65
      services/web/frontend/js/features/project-list/components/table/project-list-table.tsx
  39. 1 2
      services/web/frontend/js/features/project-list/components/table/project-tools/buttons/archive-projects-button.tsx
  40. 1 2
      services/web/frontend/js/features/project-list/components/table/project-tools/buttons/download-projects-button.tsx
  41. 13 37
      services/web/frontend/js/features/project-list/components/table/project-tools/buttons/project-tools-more-dropdown-button.tsx
  42. 53 135
      services/web/frontend/js/features/project-list/components/table/project-tools/buttons/tags-dropdown.tsx
  43. 1 2
      services/web/frontend/js/features/project-list/components/table/project-tools/buttons/trash-projects-button.tsx
  44. 52 136
      services/web/frontend/js/features/project-list/components/tags-list.tsx
  45. 74 198
      services/web/frontend/js/features/project-list/components/welcome-message-new/welcome-message-create-new-project-dropdown.tsx
  46. 1 1
      services/web/test/frontend/features/project-list/components/new-project-button.test.tsx
  47. 34 23
      services/web/test/frontend/features/project-list/components/notifications.test.tsx
  48. 11 13
      services/web/test/frontend/features/project-list/components/sidebar/tags-list.test.tsx
  49. 16 7
      services/web/test/frontend/features/project-list/components/table/project-tools/project-tools.test.tsx
  50. 1 1
      services/web/test/frontend/features/project-list/components/welcome-message.test.tsx

+ 0 - 9
services/web/app/src/Features/Project/ProjectListController.mjs

@@ -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,

+ 0 - 1
services/web/app/views/project/list-react.pug

@@ -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)

+ 0 - 1
services/web/frontend/extracted-translations.json

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

+ 3 - 24
services/web/frontend/js/features/project-list/components/current-plan-widget/commons-plan.tsx

@@ -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>
     </>

+ 4 - 27
services/web/frontend/js/features/project-list/components/current-plan-widget/free-plan.tsx

@@ -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"

+ 3 - 24
services/web/frontend/js/features/project-list/components/current-plan-widget/group-plan.tsx

@@ -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>
     </>

+ 3 - 21
services/web/frontend/js/features/project-list/components/current-plan-widget/individual-plan.tsx

@@ -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>
     </>

+ 1 - 258
services/web/frontend/js/features/project-list/components/dropdown/actions-dropdown.tsx

@@ -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

+ 40 - 111
services/web/frontend/js/features/project-list/components/dropdown/projects-dropdown.tsx

@@ -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>
   )
 }
 

+ 43 - 112
services/web/frontend/js/features/project-list/components/dropdown/sort-by-dropdown.tsx

@@ -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>
   )
 }
 

+ 0 - 3
services/web/frontend/js/features/project-list/components/modals/create-tag-modal.tsx

@@ -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>

+ 0 - 3
services/web/frontend/js/features/project-list/components/modals/delete-tag-modal.tsx

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

+ 0 - 3
services/web/frontend/js/features/project-list/components/modals/edit-tag-modal.tsx

@@ -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>

+ 1 - 12
services/web/frontend/js/features/project-list/components/modals/manage-tag-modal.tsx

@@ -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>
   )

+ 102 - 214
services/web/frontend/js/features/project-list/components/new-project-button.tsx

@@ -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) =>
+              {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: 'upload_project',
-                    dropdownMenuEvent: 'upload-project',
+                    modalVariant: 'import_from_github',
+                    dropdownMenuEvent: 'import-from-github',
                   })
                 }
-              >
-                {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"
-            >
-              {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}`}>
+              />
+            )}
+          </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)} />
     </>
   )

+ 0 - 3
services/web/frontend/js/features/project-list/components/new-project-button/modal-content-new-project-form.tsx

@@ -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>

+ 0 - 16
services/web/frontend/js/features/project-list/components/notifications/groups/affiliation/reconfirm-affiliation.tsx

@@ -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}

+ 0 - 8
services/web/frontend/js/features/project-list/components/notifications/groups/common.tsx

@@ -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)}

+ 13 - 72
services/web/frontend/js/features/project-list/components/project-list-root.tsx

@@ -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')}

+ 4 - 19
services/web/frontend/js/features/project-list/components/search-form.tsx

@@ -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>
               )
             }

+ 1 - 6
services/web/frontend/js/features/project-list/components/sidebar/sidebar.tsx

@@ -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` }),

+ 25 - 60
services/web/frontend/js/features/project-list/components/sidebar/tags-list.tsx

@@ -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>
         )
       })}

+ 1 - 4
services/web/frontend/js/features/project-list/components/sort/with-content.tsx

@@ -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 })
     }
 

+ 1 - 8
services/web/frontend/js/features/project-list/components/survey-widget.tsx

@@ -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

+ 1 - 3
services/web/frontend/js/features/project-list/components/table/cells/action-buttons/archive-project-button.tsx

@@ -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>

+ 1 - 9
services/web/frontend/js/features/project-list/components/table/cells/action-buttons/compile-and-download-project-pdf-button.tsx

@@ -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>

+ 1 - 3
services/web/frontend/js/features/project-list/components/table/cells/action-buttons/copy-project-button.tsx

@@ -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>

+ 1 - 3
services/web/frontend/js/features/project-list/components/table/cells/action-buttons/delete-project-button.tsx

@@ -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>

+ 1 - 3
services/web/frontend/js/features/project-list/components/table/cells/action-buttons/download-project-button.tsx

@@ -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>

+ 1 - 3
services/web/frontend/js/features/project-list/components/table/cells/action-buttons/leave-project-button.tsx

@@ -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>

+ 1 - 3
services/web/frontend/js/features/project-list/components/table/cells/action-buttons/trash-project-button.tsx

@@ -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>

+ 1 - 3
services/web/frontend/js/features/project-list/components/table/cells/action-buttons/unarchive-project-button.tsx

@@ -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>

+ 1 - 3
services/web/frontend/js/features/project-list/components/table/cells/action-buttons/untrash-project-button.tsx

@@ -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>

+ 18 - 73
services/web/frontend/js/features/project-list/components/table/cells/inline-tags.tsx

@@ -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>
-      }
-      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>
+    <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>
   )
 }
 

+ 4 - 17
services/web/frontend/js/features/project-list/components/table/cells/owner-cell.tsx

@@ -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>

+ 0 - 1
services/web/frontend/js/features/project-list/components/table/project-checkbox.tsx

@@ -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' }}
       />
     )
   }

+ 9 - 58
services/web/frontend/js/features/project-list/components/table/project-list-table-row.tsx

@@ -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>

+ 10 - 65
services/web/frontend/js/features/project-list/components/table/project-list-table.tsx

@@ -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')}>

+ 1 - 2
services/web/frontend/js/features/project-list/components/table/project-tools/buttons/archive-projects-button.tsx

@@ -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

+ 1 - 2
services/web/frontend/js/features/project-list/components/table/project-tools/buttons/download-projects-button.tsx

@@ -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>
   )

+ 13 - 37
services/web/frontend/js/features/project-list/components/table/project-tools/buttons/project-tools-more-dropdown-button.tsx

@@ -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>
   )
 }
 

+ 53 - 135
services/web/frontend/js/features/project-list/components/table/project-tools/buttons/tags-dropdown.tsx

@@ -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')}
-            >
-              <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"
+      <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}
             >
-              <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" />
     </>
   )

+ 1 - 2
services/web/frontend/js/features/project-list/components/table/project-tools/buttons/trash-projects-button.tsx

@@ -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

+ 52 - 136
services/web/frontend/js/features/project-list/components/tags-list.tsx

@@ -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" />
     </>

+ 74 - 198
services/web/frontend/js/features/project-list/components/welcome-message-new/welcome-message-create-new-project-dropdown.tsx

@@ -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">
-              <DropdownItem
-                as="button"
-                onClick={e =>
-                  handleDropdownItemClick(e, 'blank_project', 'blank-project')
-                }
-                tabIndex={-1}
-              >
-                {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">
+            {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, 'upload_project', 'upload-project')
-                }
-                tabIndex={-1}
+                key={`portal-template-${index}`}
+                onClick={e => handlePortalTemplateClick(e, portalTemplate.name)}
+                href={`${portalTemplate.url}#templates`}
               >
-                {t('upload_project')}
+                {portalTemplate.name}
               </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>
   )
 }
 

+ 1 - 1
services/web/test/frontend/features/project-list/components/new-project-button.test.tsx

@@ -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'

+ 34 - 23
services/web/test/frontend/features/project-list/components/notifications.test.tsx

@@ -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'
         )

+ 11 - 13
services/web/test/frontend/features/project-list/components/sidebar/tags-list.test.tsx

@@ -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 () {

+ 16 - 7
services/web/test/frontend/features/project-list/components/table/project-tools/project-tools.test.tsx

@@ -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
   })
 })

+ 1 - 1
services/web/test/frontend/features/project-list/components/welcome-message.test.tsx

@@ -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',
     })