Эх сурвалжийг харах

[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 жил өмнө
parent
commit
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', {
   res.render('project/list-react', {
     title: 'your_projects',
     title: 'your_projects',
     usersBestSubscription,
     usersBestSubscription,

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

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

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

@@ -764,7 +764,6 @@
   "join_project": "",
   "join_project": "",
   "join_team_explanation": "",
   "join_team_explanation": "",
   "joined_team": "",
   "joined_team": "",
-  "joining": "",
   "justify": "",
   "justify": "",
   "kb_suggestions_enquiry": "",
   "kb_suggestions_enquiry": "",
   "keep_current_plan": "",
   "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 { CommonsPlanSubscription } from '../../../../../../types/project/dashboard/subscription'
 import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
 import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
 import MaterialIcon from '@/shared/components/material-icon'
 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<
 type CommonsPlanProps = Pick<
   CommonsPlanSubscription,
   CommonsPlanSubscription,
@@ -23,14 +20,7 @@ function CommonsPlan({
 
 
   return (
   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
       <OLTooltip
         description={t('commons_plan_tooltip', {
         description={t('commons_plan_tooltip', {
           plan: plan.name,
           plan: plan.name,
@@ -41,21 +31,10 @@ function CommonsPlan({
       >
       >
         <a
         <a
           href={featuresPageURL}
           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;
           {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>
         </a>
       </OLTooltip>
       </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 { useTranslation, Trans } from 'react-i18next'
 import OLButton from '@/features/ui/components/ol/ol-button'
 import OLButton from '@/features/ui/components/ol/ol-button'
 import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
 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 MaterialIcon from '@/shared/components/material-icon'
 import { FreePlanSubscription } from '../../../../../../types/project/dashboard/subscription'
 import { FreePlanSubscription } from '../../../../../../types/project/dashboard/subscription'
 import * as eventTracking from '../../../../infrastructure/event-tracking'
 import * as eventTracking from '../../../../infrastructure/event-tracking'
-import { bsVersion } from '@/features/utils/bootstrap-5'
-import classnames from 'classnames'
 
 
 type FreePlanProps = Pick<FreePlanSubscription, 'featuresPageURL'>
 type FreePlanProps = Pick<FreePlanSubscription, 'featuresPageURL'>
 
 
@@ -27,14 +24,7 @@ function FreePlan({ featuresPageURL }: FreePlanProps) {
 
 
   return (
   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
       <OLTooltip
         description={t('free_plan_tooltip')}
         description={t('free_plan_tooltip')}
         id="free-plan"
         id="free-plan"
@@ -42,26 +32,13 @@ function FreePlan({ featuresPageURL }: FreePlanProps) {
       >
       >
         <a
         <a
           href={featuresPageURL}
           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;
           {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>
         </a>
       </OLTooltip>{' '}
       </OLTooltip>{' '}
-      <span
-        className={bsVersion({
-          bs5: 'd-none d-md-inline-block',
-          bs3: 'hidden-xs',
-        })}
-      >
+      <span className="d-none d-md-inline-block">
         <OLButton
         <OLButton
           variant="primary"
           variant="primary"
           href="/user/subscription/plans"
           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 { useTranslation, Trans } from 'react-i18next'
 import { GroupPlanSubscription } from '../../../../../../types/project/dashboard/subscription'
 import { GroupPlanSubscription } from '../../../../../../types/project/dashboard/subscription'
 import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
 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 MaterialIcon from '@/shared/components/material-icon'
-import { bsVersion } from '@/features/utils/bootstrap-5'
-import classnames from 'classnames'
 
 
 type GroupPlanProps = Pick<
 type GroupPlanProps = Pick<
   GroupPlanSubscription,
   GroupPlanSubscription,
@@ -37,14 +34,7 @@ function GroupPlan({
 
 
   return (
   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
       <OLTooltip
         description={
         description={
           subscription.teamName != null
           subscription.teamName != null
@@ -59,21 +49,10 @@ function GroupPlan({
       >
       >
         <a
         <a
           href={featuresPageURL}
           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;
           {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>
         </a>
       </OLTooltip>
       </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 { useTranslation, Trans } from 'react-i18next'
 import { IndividualPlanSubscription } from '../../../../../../types/project/dashboard/subscription'
 import { IndividualPlanSubscription } from '../../../../../../types/project/dashboard/subscription'
 import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
 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 MaterialIcon from '@/shared/components/material-icon'
-import { bsVersion } from '@/features/utils/bootstrap-5'
-import classnames from 'classnames'
 
 
 type IndividualPlanProps = Pick<
 type IndividualPlanProps = Pick<
   IndividualPlanSubscription,
   IndividualPlanSubscription,
@@ -36,14 +33,7 @@ function IndividualPlan({
 
 
   return (
   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
       <OLTooltip
         description={t('plan_tooltip', { plan: plan.name })}
         description={t('plan_tooltip', { plan: plan.name })}
         id="individual-plan"
         id="individual-plan"
@@ -51,18 +41,10 @@ function IndividualPlan({
       >
       >
         <a
         <a
           href={featuresPageURL}
           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;
           {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>
         </a>
       </OLTooltip>
       </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 { useTranslation } from 'react-i18next'
-import { Dropdown as BS3Dropdown } from 'react-bootstrap'
 import { Spinner } from 'react-bootstrap-5'
 import { Spinner } from 'react-bootstrap-5'
 import {
 import {
   Dropdown,
   Dropdown,
@@ -8,8 +6,6 @@ import {
   DropdownMenu,
   DropdownMenu,
   DropdownToggle,
   DropdownToggle,
 } from '@/features/ui/components/bootstrap-5/dropdown-menu'
 } 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 CopyProjectButton from '../table/cells/action-buttons/copy-project-button'
 import DownloadProjectButton from '../table/cells/action-buttons/download-project-button'
 import DownloadProjectButton from '../table/cells/action-buttons/download-project-button'
 import ArchiveProjectButton from '../table/cells/action-buttons/archive-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 CompileAndDownloadProjectPDFButton from '../table/cells/action-buttons/compile-and-download-project-pdf-button'
 import RenameProjectButton from '../table/cells/action-buttons/rename-project-button'
 import RenameProjectButton from '../table/cells/action-buttons/rename-project-button'
 import MaterialIcon from '@/shared/components/material-icon'
 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 = {
 type ActionDropdownProps = {
   project: Project
   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()
   const { t } = useTranslation()
 
 
   return (
   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
 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 { useState, useEffect, useRef } from 'react'
 import { useTranslation } from 'react-i18next'
 import { useTranslation } from 'react-i18next'
-import {
-  Dropdown as BS3Dropdown,
-  MenuItem as BS3MenuItem,
-} from 'react-bootstrap'
-import Icon from '../../../../shared/components/icon'
 import {
 import {
   Filter,
   Filter,
   UNCATEGORIZED_KEY,
   UNCATEGORIZED_KEY,
@@ -19,8 +14,6 @@ import {
 } from '@/features/ui/components/bootstrap-5/dropdown-menu'
 } from '@/features/ui/components/bootstrap-5/dropdown-menu'
 import ProjectsFilterMenu from '../projects-filter-menu'
 import ProjectsFilterMenu from '../projects-filter-menu'
 import TagsList from '../tags-list'
 import TagsList from '../tags-list'
-import MenuItemButton from './menu-item-button'
-import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
 
 
 type ItemProps = {
 type ItemProps = {
   filter: Filter
   filter: Filter
@@ -38,30 +31,15 @@ export function Item({ filter, text, onClick }: ItemProps) {
   return (
   return (
     <ProjectsFilterMenu filter={filter}>
     <ProjectsFilterMenu filter={filter}>
       {isActive => (
       {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>
     </ProjectsFilterMenu>
   )
   )
@@ -70,7 +48,6 @@ export function Item({ filter, text, onClick }: ItemProps) {
 function ProjectsDropdown() {
 function ProjectsDropdown() {
   const { t } = useTranslation()
   const { t } = useTranslation()
   const [title, setTitle] = useState(() => t('all_projects'))
   const [title, setTitle] = useState(() => t('all_projects'))
-  const [isOpened, setIsOpened] = useState(false)
   const { filter, selectedTagId, tags } = useProjectListContext()
   const { filter, selectedTagId, tags } = useProjectListContext()
   const filterTranslations = useRef<Record<Filter, string>>({
   const filterTranslations = useRef<Record<Filter, string>>({
     all: t('all_projects'),
     all: t('all_projects'),
@@ -79,7 +56,6 @@ function ProjectsDropdown() {
     archived: t('archived_projects'),
     archived: t('archived_projects'),
     trashed: t('trashed_projects'),
     trashed: t('trashed_projects'),
   })
   })
-  const handleClose = () => setIsOpened(false)
 
 
   useEffect(() => {
   useEffect(() => {
     if (selectedTagId === undefined) {
     if (selectedTagId === undefined) {
@@ -98,84 +74,37 @@ function ProjectsDropdown() {
   }, [filter, tags, selectedTagId, t])
   }, [filter, tags, selectedTagId, t])
 
 
   return (
   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 { useState, useEffect, useRef } from 'react'
 import { useTranslation } from 'react-i18next'
 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 useSort from '../../hooks/use-sort'
 import withContent, { SortBtnProps } from '../sort/with-content'
 import withContent, { SortBtnProps } from '../sort/with-content'
 import { useProjectListContext } from '../../context/project-list-context'
 import { useProjectListContext } from '../../context/project-list-context'
 import { Sort } from '../../../../../../types/project/dashboard/api'
 import { Sort } from '../../../../../../types/project/dashboard/api'
-import MenuItemButton from './menu-item-button'
 import {
 import {
   Dropdown,
   Dropdown,
   DropdownHeader,
   DropdownHeader,
@@ -17,31 +11,17 @@ import {
   DropdownMenu,
   DropdownMenu,
   DropdownToggle,
   DropdownToggle,
 } from '@/features/ui/components/bootstrap-5/dropdown-menu'
 } 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) {
 function Item({ onClick, text, iconType, screenReaderText }: SortBtnProps) {
   return (
   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() {
 function SortByDropdown() {
   const { t } = useTranslation()
   const { t } = useTranslation()
   const [title, setTitle] = useState(() => t('last_modified'))
   const [title, setTitle] = useState(() => t('last_modified'))
-  const [isOpened, setIsOpened] = useState(false)
   const { sort } = useProjectListContext()
   const { sort } = useProjectListContext()
   const { handleSort } = useSort()
   const { handleSort } = useSort()
   const sortByTranslations = useRef<Record<Sort['by'], string>>({
   const sortByTranslations = useRef<Record<Sort['by'], string>>({
@@ -60,11 +39,6 @@ function SortByDropdown() {
   })
   })
 
 
   const handleClick = (by: Sort['by']) => {
   const handleClick = (by: Sort['by']) => {
-    setTitle(sortByTranslations.current[by])
-    setIsOpened(false)
-    handleSort(by)
-  }
-  const handleClickBS5 = (by: Sort['by']) => {
     setTitle(sortByTranslations.current[by])
     setTitle(sortByTranslations.current[by])
     handleSort(by)
     handleSort(by)
   }
   }
@@ -74,84 +48,41 @@ function SortByDropdown() {
   }, [sort.by])
   }, [sort.by])
 
 
   return (
   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
             status === 'pending' || !tagName?.length || !!validationError
           }
           }
           isLoading={isLoading}
           isLoading={isLoading}
-          bs3Props={{
-            loading: isLoading ? `${t('creating')}…` : t('create'),
-          }}
         >
         >
           {t('create')}
           {t('create')}
         </OLButton>
         </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"
           variant="danger"
           disabled={isLoading}
           disabled={isLoading}
           isLoading={isLoading}
           isLoading={isLoading}
-          bs3Props={{
-            loading: isLoading ? `${t('deleting')}…` : t('delete'),
-          }}
         >
         >
           {t('delete')}
           {t('delete')}
         </OLButton>
         </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
             !!validationError
           }
           }
           isLoading={isLoading}
           isLoading={isLoading}
-          bs3Props={{
-            loading: isLoading ? `${t('saving')}…` : t('save'),
-          }}
         >
         >
           {t('save')}
           {t('save')}
         </OLButton>
         </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 OLFormGroup from '@/features/ui/components/ol/ol-form-group'
 import OLFormLabel from '@/features/ui/components/ol/ol-form-label'
 import OLFormLabel from '@/features/ui/components/ol/ol-form-label'
 import OLButton from '@/features/ui/components/ol/ol-button'
 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 Notification from '@/shared/components/notification'
-import { bsVersion } from '@/features/utils/bootstrap-5'
 
 
 type ManageTagModalProps = {
 type ManageTagModalProps = {
   id: string
   id: string
@@ -125,12 +123,9 @@ export function ManageTagModal({
         <OLButton
         <OLButton
           variant="danger"
           variant="danger"
           onClick={() => runDeleteTag(tag._id)}
           onClick={() => runDeleteTag(tag._id)}
-          className={bsVersion({ bs3: 'pull-left', bs5: 'me-auto' })}
+          className="me-auto"
           disabled={isDeleteLoading || isUpdateLoading}
           disabled={isDeleteLoading || isUpdateLoading}
           isLoading={isDeleteLoading}
           isLoading={isDeleteLoading}
-          bs3Props={{
-            loading: isDeleteLoading ? `${t('deleting')}…` : t('delete_tag'),
-          }}
         >
         >
           {t('delete_tag')}
           {t('delete_tag')}
         </OLButton>
         </OLButton>
@@ -151,15 +146,9 @@ export function ManageTagModal({
               (newTagName === tag?.name && selectedColor === getTagColor(tag))
               (newTagName === tag?.name && selectedColor === getTagColor(tag))
           )}
           )}
           isLoading={isUpdateLoading}
           isLoading={isUpdateLoading}
-          bs3Props={{
-            loading: isUpdateLoading
-              ? `${t('saving')}…`
-              : t('save_or_cancel-save'),
-          }}
         >
         >
           {t('save_or_cancel-save')}
           {t('save_or_cancel-save')}
         </OLButton>
         </OLButton>
-        <BootstrapVersionSwitcher bs3={<div className="clearfix" />} />
       </OLModalFooter>
       </OLModalFooter>
     </OLModal>
     </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 { type JSXElementConstructor, useCallback, useState } from 'react'
-import {
-  Dropdown as BS3Dropdown,
-  MenuItem as BS3MenuItem,
-} from 'react-bootstrap'
 import { useTranslation } from 'react-i18next'
 import { useTranslation } from 'react-i18next'
-import ControlledDropdown from '../../../shared/components/controlled-dropdown'
 import getMeta from '../../../utils/meta'
 import getMeta from '../../../utils/meta'
 import NewProjectButtonModal, {
 import NewProjectButtonModal, {
   NewProjectButtonModalVariant,
   NewProjectButtonModalVariant,
@@ -13,7 +8,6 @@ import AddAffiliation, { useAddAffiliation } from './add-affiliation'
 import { Nullable } from '../../../../../types/utils'
 import { Nullable } from '../../../../../types/utils'
 import { sendMB } from '../../../infrastructure/event-tracking'
 import { sendMB } from '../../../infrastructure/event-tracking'
 import importOverleafModules from '../../../../macros/import-overleaf-module.macro'
 import importOverleafModules from '../../../../macros/import-overleaf-module.macro'
-import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
 import {
 import {
   Dropdown,
   Dropdown,
   DropdownDivider,
   DropdownDivider,
@@ -151,225 +145,119 @@ function NewProjectButton({
 
 
   return (
   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, {
                   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
                   <DropdownItem
-                    href={templateLink.url}
+                    key={`portal-template-${index}`}
+                    href={`${portalTemplate.url}#templates`}
                     onClick={e =>
                     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>
                   </DropdownItem>
                 </li>
                 </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)} />
       <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}
           onClick={createNewProject}
           disabled={projectName === '' || isLoading}
           disabled={projectName === '' || isLoading}
           isLoading={isLoading}
           isLoading={isLoading}
-          bs3Props={{
-            loading: isLoading ? `${t('creating')}…` : t('create'),
-          }}
         >
         >
           {t('create')}
           {t('create')}
         </OLButton>
         </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 { useState, useEffect } from 'react'
 import { useTranslation, Trans } from 'react-i18next'
 import { useTranslation, Trans } from 'react-i18next'
-import Icon from '../../../../../../shared/components/icon'
 import getMeta from '../../../../../../utils/meta'
 import getMeta from '../../../../../../utils/meta'
 import useAsync from '../../../../../../shared/hooks/use-async'
 import useAsync from '../../../../../../shared/hooks/use-async'
 import {
 import {
@@ -88,13 +87,6 @@ function ReconfirmAffiliation({
             className="btn-inline-link"
             className="btn-inline-link"
             disabled={isLoading}
             disabled={isLoading}
             isLoading={isLoading}
             isLoading={isLoading}
-            bs3Props={{
-              loading: isLoading ? (
-                <>
-                  <Icon type="refresh" spin fw /> {t('sending')}&hellip;
-                </>
-              ) : null,
-            }}
           >
           >
             {t('resend_confirmation_email')}
             {t('resend_confirmation_email')}
           </OLButton>
           </OLButton>
@@ -143,14 +135,6 @@ function ReconfirmAffiliation({
       action={
       action={
         <OLButton
         <OLButton
           variant="secondary"
           variant="secondary"
-          bs3Props={{
-            loading:
-              isLoading || isPending ? (
-                <>
-                  <Icon type="refresh" spin fw /> {t('sending')}&hellip;
-                </>
-              ) : null,
-          }}
           isLoading={isLoading || isPending}
           isLoading={isLoading || isPending}
           disabled={isLoading || isPending}
           disabled={isLoading || isPending}
           onClick={handleRequestReconfirmation}
           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 { useTranslation, Trans } from 'react-i18next'
 import Notification from '../notification'
 import Notification from '../notification'
-import Icon from '../../../../../shared/components/icon'
 import getMeta from '../../../../../utils/meta'
 import getMeta from '../../../../../utils/meta'
 import useAsyncDismiss from '../hooks/useAsyncDismiss'
 import useAsyncDismiss from '../hooks/useAsyncDismiss'
 import useAsync from '../../../../../shared/hooks/use-async'
 import useAsync from '../../../../../shared/hooks/use-async'
@@ -96,13 +95,6 @@ function CommonNotification({ notification }: CommonNotificationProps) {
             ) : (
             ) : (
               <OLButton
               <OLButton
                 variant="secondary"
                 variant="secondary"
-                bs3Props={{
-                  loading: isLoading ? (
-                    <>
-                      <Icon type="spinner" spin /> {t('joining')}&hellip;
-                    </>
-                  ) : null,
-                }}
                 isLoading={isLoading}
                 isLoading={isLoading}
                 disabled={isLoading}
                 disabled={isLoading}
                 onClick={() => handleAcceptInvite(notification)}
                 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 { GenericErrorBoundaryFallback } from '../../../shared/components/generic-error-boundary-fallback'
 import { SplitTestProvider } from '@/shared/context/split-test-context'
 import { SplitTestProvider } from '@/shared/context/split-test-context'
 import OLCol from '@/features/ui/components/ol/ol-col'
 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 Notification from '@/shared/components/notification'
 import OLRow from '@/features/ui/components/ol/ol-row'
 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'
 import { TableContainer } from '@/features/ui/components/bootstrap-5/table'
 
 
 function ProjectListRoot() {
 function ProjectListRoot() {
@@ -78,13 +75,7 @@ function ProjectListPageContent() {
   const { t } = useTranslation()
   const { t } = useTranslation()
 
 
   const tableTopArea = (
   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">
       <div className="clearfix">
         <NewProjectButton
         <NewProjectButton
           id="new-project-button-projects-table"
           id="new-project-button-projects-table"
@@ -110,12 +101,7 @@ function ProjectListPageContent() {
     <>
     <>
       <SystemMessages />
       <SystemMessages />
 
 
-      <div
-        className={classnames(
-          'project-list-wrapper',
-          bsVersion({ bs3: 'clearfix container mx-0 px-0' })
-        )}
-      >
+      <div className="project-list-wrapper">
         {totalProjectsCount > 0 ? (
         {totalProjectsCount > 0 ? (
           <>
           <>
             <Sidebar />
             <Sidebar />
@@ -131,43 +117,22 @@ function ProjectListPageContent() {
                   filter={filter}
                   filter={filter}
                   selectedTag={selectedTag}
                   selectedTag={selectedTag}
                   selectedTagId={selectedTagId}
                   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="project-tools">
-                  <div
-                    className={bsVersion({
-                      bs5: 'd-none d-md-block',
-                      bs3: 'hidden-xs',
-                    })}
-                  >
+                  <div className="d-none d-md-block">
                     {selectedProjects.length === 0 ? (
                     {selectedProjects.length === 0 ? (
                       <CurrentPlanWidget />
                       <CurrentPlanWidget />
                     ) : (
                     ) : (
                       <ProjectTools />
                       <ProjectTools />
                     )}
                     )}
                   </div>
                   </div>
-                  <div
-                    className={bsVersion({
-                      bs5: 'd-md-none',
-                      bs3: 'visible-xs',
-                    })}
-                  >
+                  <div className="d-md-none">
                     <CurrentPlanWidget />
                     <CurrentPlanWidget />
                   </div>
                   </div>
                 </div>
                 </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}>
                 <OLCol lg={7}>
                   <SearchForm
                   <SearchForm
                     inputValue={searchText}
                     inputValue={searchText}
@@ -177,20 +142,10 @@ function ProjectListPageContent() {
                   />
                   />
                 </OLCol>
                 </OLCol>
               </OLRow>
               </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 />
                 <SurveyWidget />
               </div>
               </div>
-              <div
-                className={classnames(
-                  'mt-1',
-                  bsVersion({ bs5: 'd-md-none', bs3: 'visible-xs' })
-                )}
-              >
+              <div className="mt-1 d-md-none">
                 <div
                 <div
                   role="toolbar"
                   role="toolbar"
                   className="projects-toolbar"
                   className="projects-toolbar"
@@ -202,20 +157,10 @@ function ProjectListPageContent() {
               </div>
               </div>
               <OLRow className="row-spaced">
               <OLRow className="row-spaced">
                 <OLCol>
                 <OLCol>
-                  <BootstrapVersionSwitcher
-                    bs3={
-                      <div className="card project-list-card">
-                        {tableTopArea}
-                        <ProjectListTable />
-                      </div>
-                    }
-                    bs5={
-                      <TableContainer bordered>
-                        {tableTopArea}
-                        <ProjectListTable />
-                      </TableContainer>
-                    }
-                  />
+                  <TableContainer bordered>
+                    {tableTopArea}
+                    <ProjectListTable />
+                  </TableContainer>
                 </OLCol>
                 </OLCol>
               </OLRow>
               </OLRow>
               <OLRow className="row-spaced">
               <OLRow className="row-spaced">
@@ -253,11 +198,7 @@ function DashApiError() {
   const { t } = useTranslation()
   const { t } = useTranslation()
   return (
   return (
     <OLRow className="row-spaced">
     <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">
         <div className="notification-list">
           <Notification
           <Notification
             content={t('generic_something_went_wrong')}
             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 { useTranslation } from 'react-i18next'
 import { FormControl } from 'react-bootstrap'
 import { FormControl } from 'react-bootstrap'
-import Icon from '../../../shared/components/icon'
 import * as eventTracking from '../../../infrastructure/event-tracking'
 import * as eventTracking from '../../../infrastructure/event-tracking'
 import classnames from 'classnames'
 import classnames from 'classnames'
 import { Tag } from '../../../../../app/src/Features/Tags/types'
 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 OLCol from '@/features/ui/components/ol/ol-col'
 import OLFormControl from '@/features/ui/components/ol/ol-form-control'
 import OLFormControl from '@/features/ui/components/ol/ol-form-control'
 import MaterialIcon from '@/shared/components/material-icon'
 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 = {
 type SearchFormOwnProps = {
   inputValue: string
   inputValue: string
@@ -79,12 +76,9 @@ function SearchForm({
       className={classnames('project-search', className)}
       className={classnames('project-search', className)}
       role="search"
       role="search"
       onSubmit={e => e.preventDefault()}
       onSubmit={e => e.preventDefault()}
-      bs3Props={{ horizontal: true }}
       {...props}
       {...props}
     >
     >
-      <OLFormGroup
-        className={bsVersion({ bs3: 'has-feedback has-feedback-left' })}
-      >
+      <OLFormGroup>
         <OLCol>
         <OLCol>
           <OLFormControl
           <OLFormControl
             type="text"
             type="text"
@@ -92,25 +86,16 @@ function SearchForm({
             onChange={handleChange}
             onChange={handleChange}
             placeholder={placeholder}
             placeholder={placeholder}
             aria-label={placeholder}
             aria-label={placeholder}
-            prepend={BootstrapVersionSwitcher({
-              bs3: <Icon type="search" />,
-              bs5: <MaterialIcon type="search" />,
-            })}
+            prepend={<MaterialIcon type="search" />}
             append={
             append={
               inputValue.length > 0 && (
               inputValue.length > 0 && (
                 <button
                 <button
                   type="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')}
                   aria-label={t('clear_search')}
                   onClick={handleClear}
                   onClick={handleClear}
                 >
                 >
-                  <BootstrapVersionSwitcher
-                    bs3={<Icon type="times" />}
-                    bs5={<MaterialIcon type="clear" />}
-                  />
+                  <MaterialIcon type="clear" />
                 </button>
                 </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 AddAffiliation, { useAddAffiliation } from '../add-affiliation'
 import SurveyWidget from '../survey-widget'
 import SurveyWidget from '../survey-widget'
 import { usePersistedResize } from '../../../../shared/hooks/use-resize'
 import { usePersistedResize } from '../../../../shared/hooks/use-resize'
-import { bsVersion } from '@/features/utils/bootstrap-5'
-import classnames from 'classnames'
 
 
 function Sidebar() {
 function Sidebar() {
   const { show: showAddAffiliationWidget } = useAddAffiliation()
   const { show: showAddAffiliationWidget } = useAddAffiliation()
@@ -14,10 +12,7 @@ function Sidebar() {
 
 
   return (
   return (
     <div
     <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({
       {...getTargetProps({
         style: {
         style: {
           ...(mousePos?.x && { flexBasis: `${mousePos.x}px` }),
           ...(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,
   DropdownMenu,
   DropdownToggle,
   DropdownToggle,
 } from '@/features/ui/components/bootstrap-5/dropdown-menu'
 } from '@/features/ui/components/bootstrap-5/dropdown-menu'
-import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
 
 
 export default function TagsList() {
 export default function TagsList() {
   const { t } = useTranslation()
   const { t } = useTranslation()
@@ -76,65 +75,31 @@ export default function TagsList() {
                 </span>
                 </span>
               </span>
               </span>
             </button>
             </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>
           </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 { useTranslation } from 'react-i18next'
 import { Sort } from '../../../../../../types/project/dashboard/api'
 import { Sort } from '../../../../../../types/project/dashboard/api'
-import { bsVersion } from '@/features/utils/bootstrap-5'
 
 
 type SortBtnOwnProps = {
 type SortBtnOwnProps = {
   column: string
   column: string
@@ -28,9 +27,7 @@ function withContent<T extends SortBtnOwnProps>(
 
 
     if (column === sort.by) {
     if (column === sort.by) {
       iconType =
       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 })
       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 getMeta from '../../../utils/meta'
 import { useCallback } from 'react'
 import { useCallback } from 'react'
 import Close from '@/shared/components/close'
 import Close from '@/shared/components/close'
-import { bsVersion } from '@/features/utils/bootstrap-5'
 
 
 export default function SurveyWidget() {
 export default function SurveyWidget() {
   const survey = getMeta('ol-survey')
   const survey = getMeta('ol-survey')
@@ -22,13 +21,7 @@ export default function SurveyWidget() {
   return (
   return (
     <div className="user-notifications">
     <div className="user-notifications">
       <div className="notification-entry">
       <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">
           <div className="notification-body">
             {survey.preText}&nbsp;
             {survey.preText}&nbsp;
             <a
             <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 { archiveProject } from '../../../../util/api'
 import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
 import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
 import OLIconButton from '@/features/ui/components/ol/ol-icon-button'
 import OLIconButton from '@/features/ui/components/ol/ol-icon-button'
-import { bsVersion } from '@/features/utils/bootstrap-5'
 
 
 type ArchiveProjectButtonProps = {
 type ArchiveProjectButtonProps = {
   project: Project
   project: Project
@@ -78,8 +77,7 @@ const ArchiveProjectButtonTooltip = memo(function ArchiveProjectButtonTooltip({
               variant="link"
               variant="link"
               accessibilityLabel={text}
               accessibilityLabel={text}
               className="action-btn"
               className="action-btn"
-              icon={bsVersion({ bs5: 'inbox', bs3: 'inbox' })}
-              bs3Props={{ fw: true }}
+              icon="inbox"
             />
             />
           </span>
           </span>
         </OLTooltip>
         </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 { useTranslation } from 'react-i18next'
 import { memo, useCallback, useState } from 'react'
 import { memo, useCallback, useState } from 'react'
 import { Project } from '../../../../../../../../types/project/dashboard/api'
 import { Project } from '../../../../../../../../types/project/dashboard/api'
-import Icon from '../../../../../../shared/components/icon'
 import * as eventTracking from '../../../../../../infrastructure/event-tracking'
 import * as eventTracking from '../../../../../../infrastructure/event-tracking'
 import { useLocation } from '../../../../../../shared/hooks/use-location'
 import { useLocation } from '../../../../../../shared/hooks/use-location'
 import useAbortController from '../../../../../../shared/hooks/use-abort-controller'
 import useAbortController from '../../../../../../shared/hooks/use-abort-controller'
@@ -15,7 +14,6 @@ import OLModal, {
   OLModalHeader,
   OLModalHeader,
   OLModalTitle,
   OLModalTitle,
 } from '@/features/ui/components/ol/ol-modal'
 } from '@/features/ui/components/ol/ol-modal'
-import { bsVersion } from '@/features/utils/bootstrap-5'
 import OLIconButton from '@/features/ui/components/ol/ol-icon-button'
 import OLIconButton from '@/features/ui/components/ol/ol-icon-button'
 
 
 type CompileAndDownloadProjectPDFButtonProps = {
 type CompileAndDownloadProjectPDFButtonProps = {
@@ -159,13 +157,7 @@ const CompileAndDownloadProjectPDFButtonTooltip = memo(
                 loadingLabel={text}
                 loadingLabel={text}
                 isLoading={pendingCompile}
                 isLoading={pendingCompile}
                 className="action-btn"
                 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>
             </span>
           </OLTooltip>
           </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 { isSmallDevice } from '../../../../../../infrastructure/event-tracking'
 import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
 import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
 import OLIconButton from '@/features/ui/components/ol/ol-icon-button'
 import OLIconButton from '@/features/ui/components/ol/ol-icon-button'
-import { bsVersion } from '@/features/utils/bootstrap-5'
 
 
 type CopyButtonProps = {
 type CopyButtonProps = {
   project: Project
   project: Project
@@ -111,8 +110,7 @@ const CopyProjectButtonTooltip = memo(function CopyProjectButtonTooltip({
               variant="link"
               variant="link"
               accessibilityLabel={text}
               accessibilityLabel={text}
               className="action-btn"
               className="action-btn"
-              icon={bsVersion({ bs5: 'file_copy', bs3: 'files-o' })}
-              bs3Props={{ fw: true }}
+              icon="file_copy"
             />
             />
           </span>
           </span>
         </OLTooltip>
         </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 getMeta from '@/utils/meta'
 import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
 import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
 import OLIconButton from '@/features/ui/components/ol/ol-icon-button'
 import OLIconButton from '@/features/ui/components/ol/ol-icon-button'
-import { bsVersion } from '@/features/utils/bootstrap-5'
 
 
 type DeleteProjectButtonProps = {
 type DeleteProjectButtonProps = {
   project: Project
   project: Project
@@ -76,8 +75,7 @@ const DeleteProjectButtonTooltip = memo(function DeleteProjectButtonTooltip({
               variant="link"
               variant="link"
               accessibilityLabel={text}
               accessibilityLabel={text}
               className="action-btn"
               className="action-btn"
-              icon={bsVersion({ bs5: 'block', bs3: 'ban' })}
-              bs3Props={{ fw: true }}
+              icon="block"
             />
             />
           </span>
           </span>
         </OLTooltip>
         </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 { isSmallDevice } from '../../../../../../infrastructure/event-tracking'
 import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
 import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
 import OLIconButton from '@/features/ui/components/ol/ol-icon-button'
 import OLIconButton from '@/features/ui/components/ol/ol-icon-button'
-import { bsVersion } from '@/features/utils/bootstrap-5'
 
 
 type DownloadProjectButtonProps = {
 type DownloadProjectButtonProps = {
   project: Project
   project: Project
@@ -52,8 +51,7 @@ const DownloadProjectButtonTooltip = memo(
                 variant="link"
                 variant="link"
                 accessibilityLabel={text}
                 accessibilityLabel={text}
                 className="action-btn"
                 className="action-btn"
-                icon={bsVersion({ bs5: 'download', bs3: 'cloud-download' })}
-                bs3Props={{ fw: true }}
+                icon="download"
               />
               />
             </span>
             </span>
           </OLTooltip>
           </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 getMeta from '@/utils/meta'
 import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
 import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
 import OLIconButton from '@/features/ui/components/ol/ol-icon-button'
 import OLIconButton from '@/features/ui/components/ol/ol-icon-button'
-import { bsVersion } from '@/features/utils/bootstrap-5'
 
 
 type LeaveProjectButtonProps = {
 type LeaveProjectButtonProps = {
   project: Project
   project: Project
@@ -75,8 +74,7 @@ const LeaveProjectButtonTooltip = memo(function LeaveProjectButtonTooltip({
               variant="link"
               variant="link"
               accessibilityLabel={text}
               accessibilityLabel={text}
               className="action-btn"
               className="action-btn"
-              icon={bsVersion({ bs5: 'logout', bs3: 'sign-out' })}
-              bs3Props={{ fw: true }}
+              icon="logout"
             />
             />
           </span>
           </span>
         </OLTooltip>
         </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 { trashProject } from '../../../../util/api'
 import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
 import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
 import OLIconButton from '@/features/ui/components/ol/ol-icon-button'
 import OLIconButton from '@/features/ui/components/ol/ol-icon-button'
-import { bsVersion } from '@/features/utils/bootstrap-5'
 
 
 type TrashProjectButtonProps = {
 type TrashProjectButtonProps = {
   project: Project
   project: Project
@@ -75,8 +74,7 @@ const TrashProjectButtonTooltip = memo(function TrashProjectButtonTooltip({
               variant="link"
               variant="link"
               accessibilityLabel={text}
               accessibilityLabel={text}
               className="action-btn"
               className="action-btn"
-              icon={bsVersion({ bs5: 'delete', bs3: 'trash' })}
-              bs3Props={{ fw: true }}
+              icon="delete"
             />
             />
           </span>
           </span>
         </OLTooltip>
         </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 { unarchiveProject } from '../../../../util/api'
 import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
 import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
 import OLIconButton from '@/features/ui/components/ol/ol-icon-button'
 import OLIconButton from '@/features/ui/components/ol/ol-icon-button'
-import { bsVersion } from '@/features/utils/bootstrap-5'
 
 
 type UnarchiveProjectButtonProps = {
 type UnarchiveProjectButtonProps = {
   project: Project
   project: Project
@@ -54,8 +53,7 @@ const UnarchiveProjectButtonTooltip = memo(
                 variant="link"
                 variant="link"
                 accessibilityLabel={text}
                 accessibilityLabel={text}
                 className="action-btn"
                 className="action-btn"
-                icon={bsVersion({ bs5: 'restore_page', bs3: 'reply' })}
-                bs3Props={{ fw: true }}
+                icon="restore_page"
               />
               />
             </span>
             </span>
           </OLTooltip>
           </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 { untrashProject } from '../../../../util/api'
 import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
 import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
 import OLIconButton from '@/features/ui/components/ol/ol-icon-button'
 import OLIconButton from '@/features/ui/components/ol/ol-icon-button'
-import { bsVersion } from '@/features/utils/bootstrap-5'
 
 
 type UntrashProjectButtonProps = {
 type UntrashProjectButtonProps = {
   project: Project
   project: Project
@@ -53,8 +52,7 @@ const UntrashProjectButtonTooltip = memo(function UntrashProjectButtonTooltip({
               variant="link"
               variant="link"
               accessibilityLabel={text}
               accessibilityLabel={text}
               className="action-btn"
               className="action-btn"
-              icon={bsVersion({ bs5: 'restore_page', bs3: 'reply' })}
-              bs3Props={{ fw: true }}
+              icon="restore_page"
             />
             />
           </span>
           </span>
         </OLTooltip>
         </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 { useTranslation } from 'react-i18next'
 import { Tag as TagType } from '../../../../../../../app/src/Features/Tags/types'
 import { Tag as TagType } from '../../../../../../../app/src/Features/Tags/types'
-import Icon from '../../../../../shared/components/icon'
 import { useProjectListContext } from '../../../context/project-list-context'
 import { useProjectListContext } from '../../../context/project-list-context'
 import { removeProjectFromTag } from '../../../util/api'
 import { removeProjectFromTag } from '../../../util/api'
-import classnames from 'classnames'
 import { getTagColor } from '../../../util/tag'
 import { getTagColor } from '../../../util/tag'
 import Tag from '@/features/ui/components/bootstrap-5/tag'
 import Tag from '@/features/ui/components/bootstrap-5/tag'
-import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
 
 
 type InlineTagsProps = {
 type InlineTagsProps = {
   projectId: string
   projectId: string
@@ -36,17 +33,6 @@ type InlineTagProps = {
 function InlineTag({ tag, projectId }: InlineTagProps) {
 function InlineTag({ tag, projectId }: InlineTagProps) {
   const { t } = useTranslation()
   const { t } = useTranslation()
   const { selectTag, removeProjectFromTagInView } = useProjectListContext()
   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(
   const handleRemoveTag = useCallback(
     async (tagId: string, projectId: string) => {
     async (tagId: string, projectId: string) => {
@@ -55,66 +41,25 @@ function InlineTag({ tag, projectId }: InlineTagProps) {
     },
     },
     [removeProjectFromTagInView]
     [removeProjectFromTagInView]
   )
   )
-  const handleCloseMouseOver = () => setClassNames('tag-label-close-hover')
-  const handleCloseMouseOut = () => setClassNames('')
-
   return (
   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 { useTranslation } from 'react-i18next'
-import Icon from '../../../../../shared/components/icon'
 import { getOwnerName } from '../../../util/project'
 import { getOwnerName } from '../../../util/project'
 import { Project } from '../../../../../../../types/project/dashboard/api'
 import { Project } from '../../../../../../../types/project/dashboard/api'
 import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
 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 MaterialIcon from '@/shared/components/material-icon'
 
 
 type LinkSharingIconProps = {
 type LinkSharingIconProps = {
@@ -28,21 +26,10 @@ function LinkSharingIcon({
       {/* OverlayTrigger won't fire unless icon is wrapped in a span */}
       {/* OverlayTrigger won't fire unless icon is wrapped in a span */}
       <span className={className}>
       <span className={className}>
         {prependSpace ? ' ' : ''}
         {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>
       </span>
     </OLTooltip>
     </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)}
         checked={selectedProjectIds.has(projectId)}
         aria-label={t('select_project', { project: projectName })}
         aria-label={t('select_project', { project: projectName })}
         data-project-id={projectId}
         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 { Project } from '../../../../../../types/project/dashboard/api'
 import { ProjectCheckbox } from './project-checkbox'
 import { ProjectCheckbox } from './project-checkbox'
 import { ProjectListOwnerName } from '@/features/project-list/components/table/project-list-owner-name'
 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 = {
 type ProjectListTableRowProps = {
   project: Project
   project: Project
@@ -19,79 +17,32 @@ function ProjectListTableRow({ project, selected }: ProjectListTableRowProps) {
   const ownerName = getOwnerName(project)
   const ownerName = getOwnerName(project)
 
 
   return (
   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} />
         <ProjectCheckbox projectId={project.id} projectName={project.name} />
       </td>
       </td>
       <td className="dash-cell-name">
       <td className="dash-cell-name">
         <a href={`/project/${project.id}`}>{project.name}</a>{' '}
         <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>
-      <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} />
         <LastUpdatedCell project={project} />
         {ownerName ? <ProjectListOwnerName ownerName={ownerName} /> : null}
         {ownerName ? <ProjectListOwnerName ownerName={ownerName} /> : null}
       </td>
       </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} />
         <OwnerCell project={project} />
       </td>
       </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} />
         <LastUpdatedCell project={project} />
       </td>
       </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} />
         <InlineTags projectId={project.id} />
       </td>
       </td>
       <td className="dash-cell-actions">
       <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} />
           <ActionsCell project={project} />
         </div>
         </div>
-        <div className={bsVersion({ bs5: 'd-md-none', bs3: 'visible-xs' })}>
+        <div className="d-md-none">
           <ActionsDropdown project={project} />
           <ActionsDropdown project={project} />
         </div>
         </div>
       </td>
       </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 { useCallback, useRef, useEffect } from 'react'
 import { useTranslation } from 'react-i18next'
 import { useTranslation } from 'react-i18next'
-import Icon from '../../../../shared/components/icon'
 import ProjectListTableRow from './project-list-table-row'
 import ProjectListTableRow from './project-list-table-row'
 import { useProjectListContext } from '../../context/project-list-context'
 import { useProjectListContext } from '../../context/project-list-context'
 import useSort from '../../hooks/use-sort'
 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 OLTable from '@/features/ui/components/ol/ol-table'
 import OLFormCheckbox from '@/features/ui/components/ol/ol-form-checkbox'
 import OLFormCheckbox from '@/features/ui/components/ol/ol-form-checkbox'
 import MaterialIcon from '@/shared/components/material-icon'
 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) {
 function SortBtn({ onClick, text, iconType, screenReaderText }: SortBtnProps) {
   return (
   return (
     <button
     <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}
       onClick={onClick}
       aria-label={screenReaderText}
       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>
     </button>
   )
   )
 }
 }
@@ -66,26 +51,11 @@ function ProjectListTable() {
 
 
   return (
   return (
     <OLTable className="project-dash-table" container={false} hover>
     <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>
         <tr>
           <th
           <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')}
             aria-label={t('select_projects')}
           >
           >
             <OLFormCheckbox
             <OLFormCheckbox
@@ -97,10 +67,6 @@ function ProjectListTable() {
               }
               }
               disabled={visibleProjects.length === 0}
               disabled={visibleProjects.length === 0}
               aria-label={t('select_all_projects')}
               aria-label={t('select_all_projects')}
-              bs3Props={{
-                bsClass: 'dash-cell-checkbox-wrapper',
-                inputRef: undefined,
-              }}
               inputRef={checkAllRef}
               inputRef={checkAllRef}
             />
             />
           </th>
           </th>
@@ -123,22 +89,13 @@ function ProjectListTable() {
             />
             />
           </th>
           </th>
           <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')}
             aria-label={t('date_and_owner')}
           >
           >
             {t('date_and_owner')}
             {t('date_and_owner')}
           </th>
           </th>
           <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-label={t('owner')}
             aria-sort={
             aria-sort={
               sort.by === 'owner'
               sort.by === 'owner'
@@ -156,13 +113,7 @@ function ProjectListTable() {
             />
             />
           </th>
           </th>
           <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-label={t('last_modified')}
             aria-sort={
             aria-sort={
               sort.by === 'lastUpdated'
               sort.by === 'lastUpdated'
@@ -179,13 +130,7 @@ function ProjectListTable() {
               onClick={() => handleSort('lastUpdated')}
               onClick={() => handleSort('lastUpdated')}
             />
             />
           </th>
           </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')}
             {t('tags')}
           </th>
           </th>
           <th className="dash-cell-actions" aria-label={t('actions')}>
           <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 { useProjectListContext } from '../../../../context/project-list-context'
 import { archiveProject } from '../../../../util/api'
 import { archiveProject } from '../../../../util/api'
 import { Project } from '../../../../../../../../types/project/dashboard/api'
 import { Project } from '../../../../../../../../types/project/dashboard/api'
-import { bsVersion } from '@/features/utils/bootstrap-5'
 
 
 function ArchiveProjectsButton() {
 function ArchiveProjectsButton() {
   const { selectedProjects, toggleSelectedProject, updateProjectViewData } =
   const { selectedProjects, toggleSelectedProject, updateProjectViewData } =
@@ -50,7 +49,7 @@ function ArchiveProjectsButton() {
           onClick={handleOpenModal}
           onClick={handleOpenModal}
           variant="secondary"
           variant="secondary"
           accessibilityLabel={text}
           accessibilityLabel={text}
-          icon={bsVersion({ bs5: 'inbox', bs3: 'inbox' })}
+          icon="inbox"
         />
         />
       </OLTooltip>
       </OLTooltip>
       <ArchiveProjectModal
       <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 { useProjectListContext } from '../../../../context/project-list-context'
 import { useLocation } from '../../../../../../shared/hooks/use-location'
 import { useLocation } from '../../../../../../shared/hooks/use-location'
 import { isSmallDevice } from '../../../../../../infrastructure/event-tracking'
 import { isSmallDevice } from '../../../../../../infrastructure/event-tracking'
-import { bsVersion } from '@/features/utils/bootstrap-5'
 
 
 function DownloadProjectsButton() {
 function DownloadProjectsButton() {
   const { selectedProjects, selectOrUnselectAllProjects } =
   const { selectedProjects, selectOrUnselectAllProjects } =
@@ -39,7 +38,7 @@ function DownloadProjectsButton() {
         onClick={handleDownloadProjects}
         onClick={handleDownloadProjects}
         variant="secondary"
         variant="secondary"
         accessibilityLabel={text}
         accessibilityLabel={text}
-        icon={bsVersion({ bs5: 'download', bs3: 'cloud-download' })}
+        icon="download"
       />
       />
     </OLTooltip>
     </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 { memo } from 'react'
-import { Dropdown as BS3Dropdown } from 'react-bootstrap'
 import { useTranslation } from 'react-i18next'
 import { useTranslation } from 'react-i18next'
-import ControlledDropdown from '../../../../../../shared/components/controlled-dropdown'
 import CopyProjectMenuItem from '../menu-items/copy-project-menu-item'
 import CopyProjectMenuItem from '../menu-items/copy-project-menu-item'
 import RenameProjectMenuItem from '../menu-items/rename-project-menu-item'
 import RenameProjectMenuItem from '../menu-items/rename-project-menu-item'
 import {
 import {
@@ -9,46 +7,24 @@ import {
   DropdownMenu,
   DropdownMenu,
   DropdownToggle,
   DropdownToggle,
 } from '@/features/ui/components/bootstrap-5/dropdown-menu'
 } from '@/features/ui/components/bootstrap-5/dropdown-menu'
-import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
 
 
 function ProjectToolsMoreDropdownButton() {
 function ProjectToolsMoreDropdownButton() {
   const { t } = useTranslation()
   const { t } = useTranslation()
 
 
   return (
   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 { sortBy } from 'lodash'
 import { memo, useCallback } from 'react'
 import { memo, useCallback } from 'react'
-import { Button, Dropdown as BS3Dropdown } from 'react-bootstrap'
 import { useTranslation } from 'react-i18next'
 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 MaterialIcon from '../../../../../../shared/components/material-icon'
 import { useProjectListContext } from '../../../../context/project-list-context'
 import { useProjectListContext } from '../../../../context/project-list-context'
 import useTag from '../../../../hooks/use-tag'
 import useTag from '../../../../hooks/use-tag'
 import { addProjectsToTag, removeProjectsFromTag } from '../../../../util/api'
 import { addProjectsToTag, removeProjectsFromTag } from '../../../../util/api'
 import { getTagColor } from '../../../../util/tag'
 import { getTagColor } from '../../../../util/tag'
-import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
 import {
 import {
   Dropdown,
   Dropdown,
   DropdownDivider,
   DropdownDivider,
@@ -79,140 +75,62 @@ function TagsDropdown() {
     [selectedProjects]
     [selectedProjects]
   )
   )
 
 
-  const containsSomeSelectedProjects = useCallback(
-    tag => {
-      for (const project of selectedProjects) {
-        if (tag.project_ids?.includes(project.id)) {
-          return true
-        }
-      }
-      return false
-    },
-    [selectedProjects]
-  )
-
   return (
   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" />
       <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 { useProjectListContext } from '../../../../context/project-list-context'
 import { trashProject } from '../../../../util/api'
 import { trashProject } from '../../../../util/api'
 import { Project } from '../../../../../../../../types/project/dashboard/api'
 import { Project } from '../../../../../../../../types/project/dashboard/api'
-import { bsVersion } from '@/features/utils/bootstrap-5'
 
 
 function TrashProjectsButton() {
 function TrashProjectsButton() {
   const { selectedProjects, toggleSelectedProject, updateProjectViewData } =
   const { selectedProjects, toggleSelectedProject, updateProjectViewData } =
@@ -50,7 +49,7 @@ function TrashProjectsButton() {
           onClick={handleOpenModal}
           onClick={handleOpenModal}
           variant="secondary"
           variant="secondary"
           accessibilityLabel={text}
           accessibilityLabel={text}
-          icon={bsVersion({ bs5: 'delete', bs3: 'trash' })}
+          icon="delete"
         />
         />
       </OLTooltip>
       </OLTooltip>
       <TrashProjectModal
       <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 { useTranslation } from 'react-i18next'
 import MaterialIcon from '../../../shared/components/material-icon'
 import MaterialIcon from '../../../shared/components/material-icon'
 import { getTagColor } from '../util/tag'
 import { getTagColor } from '../util/tag'
-import MenuItemButton from './dropdown/menu-item-button'
-import Icon from '../../../shared/components/icon'
 import {
 import {
   UNCATEGORIZED_KEY,
   UNCATEGORIZED_KEY,
   useProjectListContext,
   useProjectListContext,
@@ -12,7 +9,6 @@ import useTag from '../hooks/use-tag'
 import { sortBy } from 'lodash'
 import { sortBy } from 'lodash'
 import { Tag } from '../../../../../app/src/Features/Tags/types'
 import { Tag } from '../../../../../app/src/Features/Tags/types'
 import { DropdownItem } from '@/features/ui/components/bootstrap-5/dropdown-menu'
 import { DropdownItem } from '@/features/ui/components/bootstrap-5/dropdown-menu'
-import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
 
 
 type TagsListProps = {
 type TagsListProps = {
   onTagClick?: () => void
   onTagClick?: () => void
@@ -39,143 +35,63 @@ function TagsList({ onTagClick, onEditClick }: TagsListProps) {
 
 
   return (
   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>
                 </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>
               </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 />
       <CreateTagModal id="create-tag-modal-dropdown" disableCustomColor />
       <ManageTagModal id="manage-tag-modal-dropdown" />
       <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 { useTranslation } from 'react-i18next'
 import { sendMB } from '../../../../infrastructure/event-tracking'
 import { sendMB } from '../../../../infrastructure/event-tracking'
 import getMeta from '../../../../utils/meta'
 import getMeta from '../../../../utils/meta'
@@ -11,7 +11,6 @@ import {
   DropdownMenu,
   DropdownMenu,
   DropdownToggle,
   DropdownToggle,
 } from '@/features/ui/components/bootstrap-5/dropdown-menu'
 } from '@/features/ui/components/bootstrap-5/dropdown-menu'
-import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
 
 
 const CustomDropdownToggle = forwardRef<
 const CustomDropdownToggle = forwardRef<
   HTMLButtonElement,
   HTMLButtonElement,
@@ -57,36 +56,11 @@ type WelcomeMessageCreateNewProjectDropdownProps = {
 function WelcomeMessageCreateNewProjectDropdown({
 function WelcomeMessageCreateNewProjectDropdown({
   setActiveModal,
   setActiveModal,
 }: WelcomeMessageCreateNewProjectDropdownProps) {
 }: WelcomeMessageCreateNewProjectDropdownProps) {
-  const [showDropdown, setShowDropdown] = useState(false)
   const { t } = useTranslation()
   const { t } = useTranslation()
   const portalTemplates = getMeta('ol-portalTemplates') || []
   const portalTemplates = getMeta('ol-portalTemplates') || []
 
 
   const { isOverleaf } = getMeta('ol-ExposedSettings')
   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(
   const handleDropdownItemClick = useCallback(
     (
     (
       e: React.MouseEvent,
       e: React.MouseEvent,
@@ -96,15 +70,13 @@ function WelcomeMessageCreateNewProjectDropdown({
       // prevent firing the main dropdown onClick event
       // prevent firing the main dropdown onClick event
       e.stopPropagation()
       e.stopPropagation()
 
 
-      setShowDropdown(false)
-
       sendMB('welcome-page-create-first-project-click', {
       sendMB('welcome-page-create-first-project-click', {
         dropdownOpen: true,
         dropdownOpen: true,
         dropdownMenu: dropdownMenuEvent,
         dropdownMenu: dropdownMenuEvent,
       })
       })
       setActiveModal(modalVariant)
       setActiveModal(modalVariant)
     },
     },
-    [setActiveModal, setShowDropdown]
+    [setActiveModal]
   )
   )
 
 
   const handlePortalTemplateClick = useCallback(
   const handlePortalTemplateClick = useCallback(
@@ -112,187 +84,91 @@ function WelcomeMessageCreateNewProjectDropdown({
       // prevent firing the main dropdown onClick event
       // prevent firing the main dropdown onClick event
       e.stopPropagation()
       e.stopPropagation()
 
 
-      setShowDropdown(false)
-
       sendMB('welcome-page-create-first-project-click', {
       sendMB('welcome-page-create-first-project-click', {
         dropdownMenu: 'institution-template',
         dropdownMenu: 'institution-template',
         dropdownOpen: true,
         dropdownOpen: true,
         institutionTemplateName,
         institutionTemplateName,
       })
       })
     },
     },
-    [setShowDropdown]
+    []
   )
   )
 
 
   return (
   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
               <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>
               </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
       // dynamic menu based on portalTemplates
       const affiliationTemplate = screen.getByRole('menuitem', {
       const affiliationTemplate = screen.getByRole('menuitem', {
-        name: 'Affiliation 1',
+        name: 'Affiliation 1 Template',
       })
       })
       expect(affiliationTemplate.getAttribute('href')).to.equal(
       expect(affiliationTemplate.getAttribute('href')).to.equal(
         '/edu/test-new-template#templates'
         '/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 { expect } from 'chai'
-import sinon from 'sinon'
+import sinon, { SinonStub } from 'sinon'
 import {
 import {
   fireEvent,
   fireEvent,
   render,
   render,
@@ -46,6 +46,7 @@ import {
   individualSubscription,
   individualSubscription,
 } from '../fixtures/user-subscriptions'
 } from '../fixtures/user-subscriptions'
 import getMeta from '@/utils/meta'
 import getMeta from '@/utils/meta'
+import * as bootstrapUtils from '@/features/utils/bootstrap-5'
 
 
 const renderWithinProjectListProvider = (Component: React.ComponentType) => {
 const renderWithinProjectListProvider = (Component: React.ComponentType) => {
   render(<Component />, {
   render(<Component />, {
@@ -65,6 +66,16 @@ describe('<UserNotifications />', function () {
     appName: 'Overleaf',
     appName: 'Overleaf',
   }
   }
 
 
+  let isBootstrap5Stub: SinonStub
+
+  before(function () {
+    isBootstrap5Stub = sinon.stub(bootstrapUtils, 'isBootstrap5').returns(true)
+  })
+
+  after(function () {
+    isBootstrap5Stub.restore()
+  })
+
   beforeEach(function () {
   beforeEach(function () {
     fetchMock.reset()
     fetchMock.reset()
 
 
@@ -127,15 +138,15 @@ describe('<UserNotifications />', function () {
 
 
       expect(joinBtn.disabled).to.be.true
       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
       expect(acceptMock.called()).to.be.true
       screen.getByText(/joined/i)
       screen.getByText(/joined/i)
       expect(screen.queryByRole('button', { name: /join project/i })).to.be.null
       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(
       expect(openProject.getAttribute('href')).to.equal(
         `/project/${notificationProjectInvite.messageOpts.projectId}`
         `/project/${notificationProjectInvite.messageOpts.projectId}`
       )
       )
@@ -173,12 +184,12 @@ describe('<UserNotifications />', function () {
       fireEvent.click(joinBtn)
       fireEvent.click(joinBtn)
 
 
       await waitForElementToBeRemoved(() =>
       await waitForElementToBeRemoved(() =>
-        screen.getByRole('button', { name: /joining/i })
+        screen.getByRole('button', { name: /loading/i })
       )
       )
 
 
       expect(fetchMock.called()).to.be.true
       expect(fetchMock.called()).to.be.true
       screen.getByRole('button', { name: /join project/i })
       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 () {
     it('shows WFH2020', async function () {
@@ -197,7 +208,7 @@ describe('<UserNotifications />', function () {
       screen.getByRole('alert')
       screen.getByRole('alert')
       screen.getByText(/your free WFH2020 upgrade came to an end on/i)
       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(
       expect(viewLink.getAttribute('href')).to.equal(
         'https://www.overleaf.com/events/wfh2020'
         'https://www.overleaf.com/events/wfh2020'
       )
       )
@@ -236,7 +247,7 @@ describe('<UserNotifications />', function () {
       expect(findOutMore.getAttribute('href')).to.equal(
       expect(findOutMore.getAttribute('href')).to.equal(
         'https://www.overleaf.com/learn/how-to/Institutional_Login'
         '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(
       expect(linkAccount.getAttribute('href')).to.equal(
         `${exposedSettings.samlInitPath}?university_id=${notificationIPMatchedAffiliation.messageOpts.institutionId}&auto=/project`
         `${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
         /add an institutional email address to claim your features/i
       )
       )
 
 
-      const addAffiliation = screen.getByRole('link', {
+      const addAffiliation = screen.getByRole('button', {
         name: /add affiliation/i,
         name: /add affiliation/i,
       })
       })
       expect(addAffiliation.getAttribute('href')).to.equal(`/user/settings`)
       expect(addAffiliation.getAttribute('href')).to.equal(`/user/settings`)
@@ -297,7 +308,7 @@ describe('<UserNotifications />', function () {
       screen.getByText(/file limit/i)
       screen.getByText(/file limit/i)
       screen.getByText(/You can't add more files to the project or sync it/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,
         name: /Open project/i,
       })
       })
       expect(accountSettings.getAttribute('href')).to.equal('/project/123')
       expect(accountSettings.getAttribute('href')).to.equal('/project/123')
@@ -487,7 +498,7 @@ describe('<UserNotifications />', function () {
         '/learn/how-to/Institutional_Login'
         '/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(
       expect(action.getAttribute('href')).to.equal(
         `${exposedSettings.samlInitPath}?university_id=${notificationsInstitution.institutionId}&auto=/project&email=${notificationsInstitution.email}`
         `${exposedSettings.samlInitPath}?university_id=${notificationsInstitution.institutionId}&auto=/project&email=${notificationsInstitution.email}`
       )
       )
@@ -552,7 +563,7 @@ describe('<UserNotifications />', function () {
       screen.getByRole('alert')
       screen.getByRole('alert')
       screen.getByText(/which is already registered with/i)
       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(
       expect(action.getAttribute('href')).to.equal(
         '/learn/how-to/Institutional_Login'
         '/learn/how-to/Institutional_Login'
       )
       )
@@ -735,7 +746,7 @@ describe('<UserNotifications />', function () {
         screen.getByRole('button', { name: /confirm affiliation/i })
         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)
       screen.getByText(/check your email inbox to confirm/i)
       expect(screen.queryByRole('button', { name: /confirm affiliation/i })).to
       expect(screen.queryByRole('button', { name: /confirm affiliation/i })).to
         .be.null
         .be.null
@@ -745,7 +756,7 @@ describe('<UserNotifications />', function () {
       fireEvent.click(
       fireEvent.click(
         screen.getByRole('button', { name: /resend confirmation email/i })
         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)
       expect(sendReconfirmationMock.calls()).to.have.lengthOf(2)
     })
     })
 
 
@@ -814,7 +825,7 @@ describe('<UserNotifications />', function () {
       renderWithinProjectListProvider(GroupsAndEnterpriseBanner)
       renderWithinProjectListProvider(GroupsAndEnterpriseBanner)
       await fetchMock.flush(true)
       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 () {
     it('shows the banner for users that have dismissed the previous banners', async function () {
@@ -824,7 +835,7 @@ describe('<UserNotifications />', function () {
       renderWithinProjectListProvider(GroupsAndEnterpriseBanner)
       renderWithinProjectListProvider(GroupsAndEnterpriseBanner)
       await fetchMock.flush(true)
       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
         .null
     })
     })
 
 
@@ -840,7 +851,7 @@ describe('<UserNotifications />', function () {
       renderWithinProjectListProvider(GroupsAndEnterpriseBanner)
       renderWithinProjectListProvider(GroupsAndEnterpriseBanner)
       await fetchMock.flush(true)
       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
         .null
     })
     })
 
 
@@ -856,7 +867,7 @@ describe('<UserNotifications />', function () {
       renderWithinProjectListProvider(GroupsAndEnterpriseBanner)
       renderWithinProjectListProvider(GroupsAndEnterpriseBanner)
       await fetchMock.flush(true)
       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 () {
     describe('users that are not in group and are not affiliated', function () {
@@ -897,7 +908,7 @@ describe('<UserNotifications />', function () {
         screen.getByText(
         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.'
           '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`)
         expect(link.getAttribute('href')).to.equal(`/for/contact-sales-2`)
       })
       })
@@ -914,7 +925,7 @@ describe('<UserNotifications />', function () {
         screen.getByText(
         screen.getByText(
           'Why do Fortune 500 companies and top research institutions trust Overleaf to streamline their collaboration? Get in touch to learn more.'
           '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`)
         expect(link.getAttribute('href')).to.equal(`/for/contact-sales-4`)
       })
       })
@@ -941,7 +952,7 @@ describe('<UserNotifications />', function () {
       })
       })
       it('shows the banner', function () {
       it('shows the banner', function () {
         renderWithinProjectListProvider(UserNotifications)
         renderWithinProjectListProvider(UserNotifications)
-        const ctaLink = screen.getByRole('link', {
+        const ctaLink = screen.getByRole('button', {
           name: 'Get Writefull Premium',
           name: 'Get Writefull Premium',
         })
         })
         expect(ctaLink.getAttribute('href')).to.equal(
         expect(ctaLink.getAttribute('href')).to.equal(
@@ -951,7 +962,7 @@ describe('<UserNotifications />', function () {
 
 
       it('dismisses the banner when the close button is clicked', function () {
       it('dismisses the banner when the close button is clicked', function () {
         renderWithinProjectListProvider(UserNotifications)
         renderWithinProjectListProvider(UserNotifications)
-        screen.getByRole('link', { name: /Writefull/ })
+        screen.getByRole('button', { name: /Writefull/ })
         const WritefullPromoBanner = screen.getByTestId(
         const WritefullPromoBanner = screen.getByTestId(
           'writefull-premium-promo-banner'
           '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 () {
   describe('Edit modal', function () {
     beforeEach(async function () {
     beforeEach(async function () {
       const tag1Button = screen.getByText('Tag 1')
       const tag1Button = screen.getByText('Tag 1')
-
-      const editButton = within(
+      const dropdownToggle = within(
         tag1Button.closest('li') as HTMLElement
         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 () {
     it('modal is open', async function () {
@@ -250,15 +248,15 @@ describe('<TagsList />', function () {
 
 
   describe('Delete modal', function () {
   describe('Delete modal', function () {
     beforeEach(async 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
         tag1Button.closest('li') as HTMLElement
-      ).getByRole('button', {
+      ).getByTestId('tag-dropdown-toggle')
+      await fireEvent.click(dropdownToggle)
+      const deleteMenuItem = await screen.findByRole('menuitem', {
         name: 'Delete',
         name: 'Delete',
       })
       })
-
-      await fireEvent.click(deleteButton)
+      await fireEvent.click(deleteMenuItem)
     })
     })
 
 
     it('modal is open', async function () {
     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()
     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 />)
     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')
     screen.getByText('Institution Templates')
 
 
     // dynamic menu based on portalTemplates
     // dynamic menu based on portalTemplates
-    const affiliationTemplate = screen.getByRole('link', {
+    const affiliationTemplate = screen.getByRole('menuitem', {
       name: 'Affiliation 1',
       name: 'Affiliation 1',
     })
     })