+
diff --git a/services/web/frontend/js/features/project-list/components/table/cells/action-buttons/download-project-button.tsx b/services/web/frontend/js/features/project-list/components/table/cells/action-buttons/download-project-button.tsx
index 45d7d599c9..678e915ab7 100644
--- a/services/web/frontend/js/features/project-list/components/table/cells/action-buttons/download-project-button.tsx
+++ b/services/web/frontend/js/features/project-list/components/table/cells/action-buttons/download-project-button.tsx
@@ -54,7 +54,7 @@ const DownloadProjectButtonTooltip = memo(
className="action-btn"
icon={
bsVersion({
- bs5: 'cloud_download',
+ bs5: 'download',
bs3: 'cloud-download',
}) as string
}
diff --git a/services/web/frontend/js/features/project-list/components/table/cells/owner-cell.tsx b/services/web/frontend/js/features/project-list/components/table/cells/owner-cell.tsx
index e3cc0d8c41..8f8a3ebdd6 100644
--- a/services/web/frontend/js/features/project-list/components/table/cells/owner-cell.tsx
+++ b/services/web/frontend/js/features/project-list/components/table/cells/owner-cell.tsx
@@ -3,6 +3,8 @@ import Icon from '../../../../../shared/components/icon'
import { getOwnerName } from '../../../util/project'
import { Project } from '../../../../../../../types/project/dashboard/api'
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
+import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
+import MaterialIcon from '@/shared/components/material-icon'
type LinkSharingIconProps = {
prependSpace: boolean
@@ -26,10 +28,21 @@ function LinkSharingIcon({
{/* OverlayTrigger won't fire unless icon is wrapped in a span */}
{prependSpace ? ' ' : ''}
-
+ }
+ bs5={
+
+ }
/>
@@ -48,14 +61,8 @@ export default function OwnerCell({ project }: OwnerCellProps) {
return (
<>
{ownerName === 'You' ? t('you') : ownerName}
- {project.source === 'token' ? (
-
- ) : (
- ''
+ {project.source === 'token' && (
+
)}
>
)
diff --git a/services/web/frontend/js/features/project-list/components/table/project-tools/buttons/archive-projects-button.tsx b/services/web/frontend/js/features/project-list/components/table/project-tools/buttons/archive-projects-button.tsx
index 43779469f0..1a76239ee5 100644
--- a/services/web/frontend/js/features/project-list/components/table/project-tools/buttons/archive-projects-button.tsx
+++ b/services/web/frontend/js/features/project-list/components/table/project-tools/buttons/archive-projects-button.tsx
@@ -1,12 +1,13 @@
import { memo, useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
-import Icon from '../../../../../../shared/components/icon'
-import Tooltip from '../../../../../../shared/components/tooltip'
+import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
+import OLIconButton from '@/features/ui/components/ol/ol-icon-button'
import ArchiveProjectModal from '../../../modals/archive-project-modal'
import useIsMounted from '../../../../../../shared/hooks/use-is-mounted'
import { useProjectListContext } from '../../../../context/project-list-context'
import { archiveProject } from '../../../../util/api'
import { Project } from '../../../../../../../../types/project/dashboard/api'
+import { bsVersion } from '@/features/utils/bootstrap-5'
function ArchiveProjectsButton() {
const { selectedProjects, toggleSelectedProject, updateProjectViewData } =
@@ -40,19 +41,23 @@ function ArchiveProjectsButton() {
return (
<>
-
-
-
+ variant="secondary"
+ accessibilityLabel={text}
+ icon={
+ bsVersion({
+ bs5: 'inbox',
+ bs3: 'inbox',
+ }) as string
+ }
+ />
+
{hasDeletableProjectsSelected && hasLeavableProjectsSelected && (
-
+
)}
{hasDeletableProjectsSelected && !hasLeavableProjectsSelected && (
-
+
)}
-
-
+ variant="secondary"
+ accessibilityLabel={text}
+ icon={
+ bsVersion({
+ bs5: 'download',
+ bs3: 'cloud-download',
+ }) as string
+ }
+ />
+
)
}
diff --git a/services/web/frontend/js/features/project-list/components/table/project-tools/buttons/leave-projects-button.tsx b/services/web/frontend/js/features/project-list/components/table/project-tools/buttons/leave-projects-button.tsx
index a8e65186ff..b9bfcd1f57 100644
--- a/services/web/frontend/js/features/project-list/components/table/project-tools/buttons/leave-projects-button.tsx
+++ b/services/web/frontend/js/features/project-list/components/table/project-tools/buttons/leave-projects-button.tsx
@@ -1,5 +1,5 @@
import { useState } from 'react'
-import { Button } from 'react-bootstrap'
+import OLButton from '@/features/ui/components/ol/ol-button'
import { useTranslation } from 'react-i18next'
import LeaveProjectModal from '../../../modals/leave-project-modal'
import useIsMounted from '../../../../../../shared/hooks/use-is-mounted'
@@ -37,9 +37,9 @@ function LeaveProjectsButton() {
return (
<>
{!hasDeletableProjectsSelected && hasLeavableProjectsSelected && (
-
+
)}
-
- {t('more')}
-
-
-
-
-
-
+
+
+ {t('more')}
+
+
+
+
+
+
+ }
+ bs5={
+
+
+ {t('more')}
+
+
+
+
+
+
+
+
+
+
+ }
+ />
)
}
diff --git a/services/web/frontend/js/features/project-list/components/table/project-tools/buttons/tags-dropdown.tsx b/services/web/frontend/js/features/project-list/components/table/project-tools/buttons/tags-dropdown.tsx
index c129c1582f..3bb86dce78 100644
--- a/services/web/frontend/js/features/project-list/components/table/project-tools/buttons/tags-dropdown.tsx
+++ b/services/web/frontend/js/features/project-list/components/table/project-tools/buttons/tags-dropdown.tsx
@@ -1,6 +1,6 @@
import { sortBy } from 'lodash'
import { memo, useCallback } from 'react'
-import { Button, Dropdown } from 'react-bootstrap'
+import { Button, Dropdown as BS3Dropdown } from 'react-bootstrap'
import { useTranslation } from 'react-i18next'
import ControlledDropdown from '../../../../../../shared/components/controlled-dropdown'
import Icon from '../../../../../../shared/components/icon'
@@ -9,6 +9,15 @@ import { useProjectListContext } from '../../../../context/project-list-context'
import useTag from '../../../../hooks/use-tag'
import { addProjectsToTag, removeProjectsFromTag } from '../../../../util/api'
import { getTagColor } from '../../../../util/tag'
+import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
+import {
+ Dropdown,
+ DropdownDivider,
+ DropdownHeader,
+ DropdownItem,
+ DropdownMenu,
+ DropdownToggle,
+} from '@/features/ui/components/bootstrap-5/dropdown-menu'
function TagsDropdown() {
const {
@@ -84,65 +93,126 @@ function TagsDropdown() {
return (
<>
-
-
-
-
-
-
- {t('add_to_tag')}
-
- {sortBy(tags, tag => tag.name?.toLowerCase()).map(tag => {
- return (
-
+
+
+
+
+
+
+ {t('add_to_tag')}
+
+ {sortBy(tags, tag => tag.name?.toLowerCase()).map(tag => {
+ return (
+
+
+
+ )
+ })}
+
+
- )
- })}
-
-
-
+ }
+ bs5={
+
+
- {t('create_new_tag')}
-
-
-
-
+
+
+
+ {t('add_to_tag')}
+ {sortBy(tags, tag => tag.name?.toLowerCase()).map(
+ (tag, index) => (
+
+
+ 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'
+ ) : (
+
+ )
+ }
+ >
+
+ {tag.name}
+
+
+ )
+ )}
+
+
+
+ {t('create_new_tag')}
+
+
+
+
+ }
+ />
>
)
diff --git a/services/web/frontend/js/features/project-list/components/table/project-tools/buttons/trash-projects-button.tsx b/services/web/frontend/js/features/project-list/components/table/project-tools/buttons/trash-projects-button.tsx
index 959b252288..b12d036938 100644
--- a/services/web/frontend/js/features/project-list/components/table/project-tools/buttons/trash-projects-button.tsx
+++ b/services/web/frontend/js/features/project-list/components/table/project-tools/buttons/trash-projects-button.tsx
@@ -1,12 +1,13 @@
import { memo, useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
-import Icon from '../../../../../../shared/components/icon'
-import Tooltip from '../../../../../../shared/components/tooltip'
+import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
+import OLIconButton from '@/features/ui/components/ol/ol-icon-button'
import TrashProjectModal from '../../../modals/trash-project-modal'
import useIsMounted from '../../../../../../shared/hooks/use-is-mounted'
import { useProjectListContext } from '../../../../context/project-list-context'
import { trashProject } from '../../../../util/api'
import { Project } from '../../../../../../../../types/project/dashboard/api'
+import { bsVersion } from '@/features/utils/bootstrap-5'
function TrashProjectsButton() {
const { selectedProjects, toggleSelectedProject, updateProjectViewData } =
@@ -40,19 +41,23 @@ function TrashProjectsButton() {
return (
<>
-
-
-
+ variant="secondary"
+ accessibilityLabel={text}
+ icon={
+ bsVersion({
+ bs5: 'delete',
+ bs3: 'trash',
+ }) as string
+ }
+ />
+
- {t('unarchive')}
-
+
+ {t('untrash')}
+
)
}
diff --git a/services/web/frontend/js/features/project-list/components/table/project-tools/buttons/untrash-projects-button.tsx b/services/web/frontend/js/features/project-list/components/table/project-tools/buttons/untrash-projects-button.tsx
index ee2081f766..4e61c2439d 100644
--- a/services/web/frontend/js/features/project-list/components/table/project-tools/buttons/untrash-projects-button.tsx
+++ b/services/web/frontend/js/features/project-list/components/table/project-tools/buttons/untrash-projects-button.tsx
@@ -1,5 +1,5 @@
import { memo } from 'react'
-import { Button } from 'react-bootstrap'
+import OLButton from '@/features/ui/components/ol/ol-button'
import { useTranslation } from 'react-i18next'
import { useProjectListContext } from '../../../../context/project-list-context'
import { untrashProject } from '../../../../util/api'
@@ -18,13 +18,9 @@ function UntrashProjectsButton() {
}
return (
-
+
)
}
diff --git a/services/web/frontend/js/features/project-list/components/table/project-tools/menu-items/copy-project-menu-item.tsx b/services/web/frontend/js/features/project-list/components/table/project-tools/menu-items/copy-project-menu-item.tsx
index 934eb2e637..66784772eb 100644
--- a/services/web/frontend/js/features/project-list/components/table/project-tools/menu-items/copy-project-menu-item.tsx
+++ b/services/web/frontend/js/features/project-list/components/table/project-tools/menu-items/copy-project-menu-item.tsx
@@ -1,6 +1,6 @@
import { memo, useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
-import { MenuItem } from 'react-bootstrap'
+import OlDropdownMenuItem from '@/features/ui/components/ol/ol-dropdown-menu-item'
import CloneProjectModal from '../../../../../clone-project-modal/components/clone-project-modal'
import useIsMounted from '../../../../../../shared/hooks/use-is-mounted'
import { useProjectListContext } from '../../../../context/project-list-context'
@@ -64,6 +64,9 @@ function CopyProjectMenuItem() {
return (
<>
+
+ {t('make_a_copy')}
+
-
>
)
}
diff --git a/services/web/frontend/js/features/project-list/components/table/project-tools/menu-items/rename-project-menu-item.tsx b/services/web/frontend/js/features/project-list/components/table/project-tools/menu-items/rename-project-menu-item.tsx
index e5e574cc58..4b80f6b281 100644
--- a/services/web/frontend/js/features/project-list/components/table/project-tools/menu-items/rename-project-menu-item.tsx
+++ b/services/web/frontend/js/features/project-list/components/table/project-tools/menu-items/rename-project-menu-item.tsx
@@ -1,8 +1,8 @@
import { memo, useCallback, useState } from 'react'
-import { MenuItem } from 'react-bootstrap'
import { useTranslation } from 'react-i18next'
import { useProjectListContext } from '../../../../context/project-list-context'
import useIsMounted from '../../../../../../shared/hooks/use-is-mounted'
+import OlDropdownMenuItem from '@/features/ui/components/ol/ol-dropdown-menu-item'
import RenameProjectModal from '../../../modals/rename-project-modal'
function RenameProjectMenuItem() {
@@ -34,7 +34,9 @@ function RenameProjectMenuItem() {
return (
<>
-
+
+ {t('rename')}
+
-
+
+
{filter !== 'archived' && }
{filter !== 'trashed' && }
-
+
-
- {filter === 'trashed' && }
- {filter === 'archived' && }
-
+ {(filter === 'trashed' || filter === 'archived') && (
+
+ {filter === 'trashed' && }
+ {filter === 'archived' && }
+
+ )}
-
- {filter === 'trashed' && (
- <>
-
-
-
- >
- )}
-
+ {filter === 'trashed' && (
+
+
+
+
+
+ )}
{!['archived', 'trashed'].includes(filter) && }
{selectedProjects.length === 1 &&
filter !== 'archived' &&
filter !== 'trashed' && }
-
+
)
}
diff --git a/services/web/frontend/js/features/project-list/components/tags-list.tsx b/services/web/frontend/js/features/project-list/components/tags-list.tsx
index d86dd877e9..8e5333d0fa 100644
--- a/services/web/frontend/js/features/project-list/components/tags-list.tsx
+++ b/services/web/frontend/js/features/project-list/components/tags-list.tsx
@@ -11,10 +11,12 @@ import {
import useTag from '../hooks/use-tag'
import { sortBy } from 'lodash'
import { Tag } from '../../../../../app/src/Features/Tags/types'
+import { DropdownItem } from '@/features/ui/components/bootstrap-5/dropdown-menu'
+import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
type TagsListProps = {
- onTagClick: () => void
- onEditClick: () => void
+ onTagClick?: () => void
+ onEditClick?: () => void
}
function TagsList({ onTagClick, onEditClick }: TagsListProps) {
@@ -32,76 +34,151 @@ function TagsList({ onTagClick, onEditClick }: TagsListProps) {
const handleClick = (e: React.MouseEvent, tag: Tag) => {
handleSelectTag(e, tag._id)
- onTagClick()
+ onTagClick?.()
}
return (
<>
- {sortBy(tags, ['name']).map((tag, index) => (
- handleClick(e as unknown as React.MouseEvent, tag)}
- className="projects-types-menu-item projects-types-menu-tag-item"
- afterNode={
-
- }
- >
-
- {selectedTagId === tag._id ? (
-
- ) : null}
-
+ {sortBy(tags, ['name']).map((tag, index) => (
+
+ handleClick(e as unknown as React.MouseEvent, tag)
+ }
+ className="projects-types-menu-item projects-types-menu-tag-item"
+ afterNode={
+
+ }
+ >
+
+ {selectedTagId === tag._id ? (
+
+ ) : null}
+
+
+
+
+ {tag.name}{' '}
+
+ {' '}
+ ({tag.project_ids?.length})
+
+
+
+
+ ))}
+ {
+ selectTag(UNCATEGORIZED_KEY)
+ onTagClick?.()
}}
>
-
-
-
- {tag.name}{' '}
- ({tag.project_ids?.length})
-
-
-
- ))}
- {
- selectTag(UNCATEGORIZED_KEY)
- onTagClick()
- }}
- >
- {selectedTagId === UNCATEGORIZED_KEY ? (
-
- ) : null}
-
- {t('uncategorized')}
- ({untaggedProjectsCount})
-
-
- {
- openCreateTagModal()
- onTagClick()
- }}
- className="projects-types-menu-item"
- >
-
-
- {t('new_tag')}
-
-
+ {selectedTagId === UNCATEGORIZED_KEY ? (
+
+ ) : null}
+
+ {t('uncategorized')}
+ ({untaggedProjectsCount})
+
+
+ {
+ openCreateTagModal()
+ onTagClick?.()
+ }}
+ className="projects-types-menu-item"
+ >
+
+
+ {t('new_tag')}
+
+
+ >
+ }
+ bs5={
+ <>
+ {sortBy(tags, ['name']).map((tag, index) => (
+
+
+ handleClick(e as unknown as React.MouseEvent, tag)
+ }
+ leadingIcon={
+
+
+
+ }
+ trailingIcon={selectedTagId === tag._id ? 'check' : undefined}
+ active={selectedTagId === tag._id}
+ >
+
+ {tag.name} ({tag.project_ids?.length})
+
+
+ {
+ e.stopPropagation()
+ handleManageTag(e, tag._id)
+ }}
+ aria-label={t('edit_tag')}
+ >
+
+
+
+ ))}
+
+ selectTag(UNCATEGORIZED_KEY)}
+ trailingIcon={
+ selectedTagId === UNCATEGORIZED_KEY ? 'check' : undefined
+ }
+ active={selectedTagId === UNCATEGORIZED_KEY}
+ >
+ {t('uncategorized')} ({untaggedProjectsCount})
+
+
+
+
+ {t('new_tag')}
+
+
+ >
+ }
+ />
>
diff --git a/services/web/frontend/js/features/project-list/components/title/project-list-title.tsx b/services/web/frontend/js/features/project-list/components/title/project-list-title.tsx
index a553f0b681..97b4fdb457 100644
--- a/services/web/frontend/js/features/project-list/components/title/project-list-title.tsx
+++ b/services/web/frontend/js/features/project-list/components/title/project-list-title.tsx
@@ -16,6 +16,7 @@ function ProjectListTitle({
}) {
const { t } = useTranslation()
let message = t('projects')
+
if (selectedTag) {
message = `${selectedTag.name}`
} else if (selectedTagId === UNCATEGORIZED_KEY) {
@@ -39,10 +40,9 @@ function ProjectListTitle({
break
}
}
+
return (
-
- {message}
-
+ {message}
)
}
diff --git a/services/web/frontend/js/features/ui/components/bootstrap-5/bootstrap-version-switcher.tsx b/services/web/frontend/js/features/ui/components/bootstrap-5/bootstrap-version-switcher.tsx
index 8fbbf0b00f..0bdfcca31f 100644
--- a/services/web/frontend/js/features/ui/components/bootstrap-5/bootstrap-version-switcher.tsx
+++ b/services/web/frontend/js/features/ui/components/bootstrap-5/bootstrap-version-switcher.tsx
@@ -1,8 +1,8 @@
import { isBootstrap5 } from '@/features/utils/bootstrap-5'
type BootstrapVersionSwitcherProps = {
- bs3: React.ReactNode
- bs5: React.ReactNode
+ bs3?: React.ReactNode
+ bs5?: React.ReactNode
}
function BootstrapVersionSwitcher({
diff --git a/services/web/frontend/js/features/ui/components/bootstrap-5/button.tsx b/services/web/frontend/js/features/ui/components/bootstrap-5/button.tsx
index 1d51545aaa..0d80f8aab8 100644
--- a/services/web/frontend/js/features/ui/components/bootstrap-5/button.tsx
+++ b/services/web/frontend/js/features/ui/components/bootstrap-5/button.tsx
@@ -1,3 +1,4 @@
+import { forwardRef } from 'react'
import { Button as BS5Button, Spinner } from 'react-bootstrap-5'
import type { ButtonProps } from '@/features/ui/components/types/button-props'
import classNames from 'classnames'
@@ -10,52 +11,71 @@ const sizeClasses = new Map([
['large', 'btn-lg'],
])
-export default function Button({
- children,
- className,
- leadingIcon,
- isLoading = false,
- loadingLabel,
- size = 'default',
- trailingIcon,
- variant = 'primary',
- ...props
-}: ButtonProps) {
- const { t } = useTranslation()
+const Button = forwardRef(
+ (
+ {
+ children,
+ className,
+ leadingIcon,
+ isLoading = false,
+ loadingLabel,
+ size = 'default',
+ trailingIcon,
+ variant = 'primary',
+ ...props
+ },
+ ref
+ ) => {
+ const { t } = useTranslation()
- const sizeClass = sizeClasses.get(size)
- const buttonClassName = classNames('d-inline-grid', sizeClass, className, {
- 'button-loading': isLoading,
- })
- const loadingSpinnerClassName =
- size === 'large' ? 'loading-spinner-large' : 'loading-spinner-small'
- const materialIconClassName = size === 'large' ? 'icon-large' : 'icon-small'
+ const sizeClass = sizeClasses.get(size)
+ const buttonClassName = classNames('d-inline-grid', sizeClass, className, {
+ 'button-loading': isLoading,
+ })
+ const loadingSpinnerClassName =
+ size === 'large' ? 'loading-spinner-large' : 'loading-spinner-small'
+ const materialIconClassName = size === 'large' ? 'icon-large' : 'icon-small'
- return (
-
- {isLoading && (
-
-
-
- {loadingLabel ?? t('loading')}
+ return (
+
+ {isLoading && (
+
+
+
+ {loadingLabel ?? t('loading')}
+
+ )}
+
+ {leadingIcon && (
+
+ )}
+ {children}
+ {trailingIcon && (
+
+ )}
- )}
-
- {leadingIcon && (
-
- )}
- {children}
- {trailingIcon && (
-
- )}
-
-
- )
-}
+
+ )
+ }
+)
+Button.displayName = 'Button'
+
+export default Button
diff --git a/services/web/frontend/js/features/ui/components/bootstrap-5/dropdown-menu.tsx b/services/web/frontend/js/features/ui/components/bootstrap-5/dropdown-menu.tsx
index ff116eac27..e759d8f7f9 100644
--- a/services/web/frontend/js/features/ui/components/bootstrap-5/dropdown-menu.tsx
+++ b/services/web/frontend/js/features/ui/components/bootstrap-5/dropdown-menu.tsx
@@ -16,76 +16,88 @@ import type {
DropdownHeaderProps,
} from '@/features/ui/components/types/dropdown-menu-props'
import MaterialIcon from '@/shared/components/material-icon'
+import { fixedForwardRef } from '@/utils/react'
export function Dropdown({ ...props }: DropdownProps) {
return
}
-export const DropdownItem = forwardRef<
- typeof BS5DropdownItem,
- DropdownItemProps
->(
- (
- { active, children, description, leadingIcon, trailingIcon, ...props },
- ref
- ) => {
- let leadingIconComponent = null
- if (leadingIcon) {
- if (typeof leadingIcon === 'string') {
- leadingIconComponent = (
-
- )
- } else {
- leadingIconComponent = (
-
- {leadingIcon}
-
- )
- }
+function DropdownItem(
+ {
+ active,
+ children,
+ description,
+ leadingIcon,
+ trailingIcon,
+ ...props
+ }: DropdownItemProps,
+ ref: React.ForwardedRef
+) {
+ let leadingIconComponent = null
+ if (leadingIcon) {
+ if (typeof leadingIcon === 'string') {
+ leadingIconComponent = (
+
+ )
+ } else {
+ leadingIconComponent = (
+
+ {leadingIcon}
+
+ )
}
-
- let trailingIconComponent = null
- if (trailingIcon) {
- if (typeof trailingIcon === 'string') {
- const trailingIconType = active ? 'check' : trailingIcon
-
- trailingIconComponent = (
-
- )
- } else {
- trailingIconComponent = (
-
- {trailingIcon}
-
- )
- }
- }
-
- return (
-
- {leadingIconComponent}
- {children}
- {trailingIconComponent}
- {description && (
- {description}
- )}
-
- )
}
-)
-DropdownItem.displayName = 'DropdownItem'
+
+ let trailingIconComponent = null
+ if (trailingIcon) {
+ if (typeof trailingIcon === 'string') {
+ const trailingIconType = active ? 'check' : trailingIcon
+
+ trailingIconComponent = (
+
+ )
+ } else {
+ trailingIconComponent = (
+
+ {trailingIcon}
+
+ )
+ }
+ }
+
+ return (
+
+ {leadingIconComponent}
+ {children}
+ {trailingIconComponent}
+ {description && (
+ {description}
+ )}
+
+ )
+}
+
+function EmptyLeadingIcon() {
+ return
+}
+
+const ForwardReferredDropdownItem = fixedForwardRef(DropdownItem, {
+ EmptyLeadingIcon,
+})
+
+export { ForwardReferredDropdownItem as DropdownItem }
export function DropdownToggle({ ...props }: DropdownToggleProps) {
return
diff --git a/services/web/frontend/js/features/ui/components/bootstrap-5/icon-button.tsx b/services/web/frontend/js/features/ui/components/bootstrap-5/icon-button.tsx
index eb5d1ecafa..198ae9b977 100644
--- a/services/web/frontend/js/features/ui/components/bootstrap-5/icon-button.tsx
+++ b/services/web/frontend/js/features/ui/components/bootstrap-5/icon-button.tsx
@@ -1,28 +1,33 @@
+import { forwardRef } from 'react'
import classNames from 'classnames'
import MaterialIcon from '@/shared/components/material-icon'
import Button from './button'
import type { IconButtonProps } from '@/features/ui/components/types/icon-button-props'
-export default function IconButton({
- accessibilityLabel,
- icon,
- isLoading = false,
- size = 'default',
- ...props
-}: IconButtonProps) {
- const iconButtonClassName = `icon-button-${size}`
- const iconSizeClassName = size === 'large' ? 'icon-large' : 'icon-small'
- const materialIconClassName = classNames(iconSizeClassName, {
- 'button-content-hidden': isLoading,
- })
+const IconButton = forwardRef(
+ (
+ { accessibilityLabel, icon, isLoading = false, size = 'default', ...props },
+ ref
+ ) => {
+ const iconButtonClassName = `icon-button-${size}`
+ const iconSizeClassName = size === 'large' ? 'icon-large' : 'icon-small'
+ const materialIconClassName = classNames(iconSizeClassName, {
+ 'button-content-hidden': isLoading,
+ })
- return (
-
- )
-}
+ return (
+
+ )
+ }
+)
+IconButton.displayName = 'IconButton'
+
+export default IconButton
diff --git a/services/web/frontend/js/features/ui/components/bootstrap-5/tooltip.tsx b/services/web/frontend/js/features/ui/components/bootstrap-5/tooltip.tsx
index a4fd9e2bb9..3e71208d20 100644
--- a/services/web/frontend/js/features/ui/components/bootstrap-5/tooltip.tsx
+++ b/services/web/frontend/js/features/ui/components/bootstrap-5/tooltip.tsx
@@ -1,11 +1,13 @@
import { cloneElement, useEffect, forwardRef } from 'react'
-import { OverlayTrigger, Tooltip as BSTooltip } from 'react-bootstrap-5'
+import {
+ OverlayTrigger,
+ OverlayTriggerProps,
+ Tooltip as BSTooltip,
+ TooltipProps as BSTooltipProps,
+} from 'react-bootstrap-5'
import { callFnsInSequence } from '@/utils/functions'
-type OverlayProps = Omit<
- React.ComponentProps,
- 'overlay' | 'children'
->
+type OverlayProps = Omit
type UpdatingTooltipProps = {
popper: {
@@ -34,7 +36,7 @@ export type TooltipProps = {
description: React.ReactNode
id: string
overlayProps?: OverlayProps
- tooltipProps?: React.ComponentProps
+ tooltipProps?: BSTooltipProps
hidden?: boolean
children: React.ReactElement
}
diff --git a/services/web/frontend/js/features/ui/components/ol/ol-button-group.tsx b/services/web/frontend/js/features/ui/components/ol/ol-button-group.tsx
new file mode 100644
index 0000000000..f4f0ede217
--- /dev/null
+++ b/services/web/frontend/js/features/ui/components/ol/ol-button-group.tsx
@@ -0,0 +1,30 @@
+import { ButtonGroup, ButtonGroupProps } from 'react-bootstrap-5'
+import {
+ ButtonGroup as BS3ButtonGroup,
+ ButtonGroupProps as BS3ButtonGroupProps,
+} from 'react-bootstrap'
+import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
+import { getAriaAndDataProps } from '@/features/utils/bootstrap-5'
+
+type OLButtonGroupProps = ButtonGroupProps & {
+ bs3Props?: Record
+}
+
+function OlButtonGroup({ bs3Props, as, ...rest }: OLButtonGroupProps) {
+ const bs3ButtonGroupProps: BS3ButtonGroupProps = {
+ children: rest.children,
+ className: rest.className,
+ vertical: rest.vertical,
+ ...getAriaAndDataProps(rest),
+ ...bs3Props,
+ }
+
+ return (
+ }
+ bs5={}
+ />
+ )
+}
+
+export default OlButtonGroup
diff --git a/services/web/frontend/js/features/ui/components/ol/ol-button-toolbar.tsx b/services/web/frontend/js/features/ui/components/ol/ol-button-toolbar.tsx
new file mode 100644
index 0000000000..ac1b54593e
--- /dev/null
+++ b/services/web/frontend/js/features/ui/components/ol/ol-button-toolbar.tsx
@@ -0,0 +1,31 @@
+import { ButtonToolbar, ButtonToolbarProps } from 'react-bootstrap-5'
+import {
+ ButtonToolbar as BS3ButtonToolbar,
+ ButtonToolbarProps as BS3ButtonToolbarProps,
+} from 'react-bootstrap'
+import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
+import { getAriaAndDataProps } from '@/features/utils/bootstrap-5'
+
+type OLButtonToolbarProps = ButtonToolbarProps & {
+ bs3Props?: Record
+}
+
+function OlButtonToolbar(props: OLButtonToolbarProps) {
+ const { bs3Props, ...rest } = props
+
+ const bs3ButtonToolbarProps: BS3ButtonToolbarProps = {
+ children: rest.children,
+ className: rest.className,
+ ...getAriaAndDataProps(rest),
+ ...bs3Props,
+ }
+
+ return (
+ }
+ bs5={}
+ />
+ )
+}
+
+export default OlButtonToolbar
diff --git a/services/web/frontend/js/features/ui/components/ol/ol-dropdown-menu-item.tsx b/services/web/frontend/js/features/ui/components/ol/ol-dropdown-menu-item.tsx
new file mode 100644
index 0000000000..ee5114eab2
--- /dev/null
+++ b/services/web/frontend/js/features/ui/components/ol/ol-dropdown-menu-item.tsx
@@ -0,0 +1,27 @@
+import { MenuItem, MenuItemProps } from 'react-bootstrap'
+import { DropdownItem } from '@/features/ui/components/bootstrap-5/dropdown-menu'
+import { DropdownItemProps } from '@/features/ui/components/types/dropdown-menu-props'
+import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
+
+type OlDropdownMenuItemProps = DropdownItemProps & {
+ bs3Props?: MenuItemProps
+}
+
+function OlDropdownMenuItem(props: OlDropdownMenuItemProps) {
+ const { bs3Props, ...rest } = props
+
+ const bs3MenuItemProps: MenuItemProps = {
+ children: rest.children,
+ onClick: rest.onClick,
+ ...bs3Props,
+ }
+
+ return (
+ }
+ bs5={}
+ />
+ )
+}
+
+export default OlDropdownMenuItem
diff --git a/services/web/frontend/js/features/ui/components/ol/ol-icon-button.tsx b/services/web/frontend/js/features/ui/components/ol/ol-icon-button.tsx
index 3d59f0aeeb..5d34b1c2ff 100644
--- a/services/web/frontend/js/features/ui/components/ol/ol-icon-button.tsx
+++ b/services/web/frontend/js/features/ui/components/ol/ol-icon-button.tsx
@@ -1,9 +1,11 @@
+import { forwardRef } from 'react'
import { bs3ButtonProps, BS3ButtonSize } from './ol-button'
import { Button as BS3Button } from 'react-bootstrap'
import type { IconButtonProps } from '@/features/ui/components/types/icon-button-props'
import BootstrapVersionSwitcher from '../bootstrap-5/bootstrap-version-switcher'
import Icon, { IconProps } from '@/shared/components/icon'
import IconButton from '../bootstrap-5/icon-button'
+import { callFnsInSequence } from '@/utils/functions'
export type OLIconButtonProps = IconButtonProps & {
bs3Props?: {
@@ -11,28 +13,44 @@ export type OLIconButtonProps = IconButtonProps & {
fw?: IconProps['fw']
className?: string
bsSize?: BS3ButtonSize
+ onMouseOver?: React.MouseEventHandler
+ onMouseOut?: React.MouseEventHandler
+ onFocus?: React.FocusEventHandler
+ onBlur?: React.FocusEventHandler
}
}
-export default function OLIconButton(props: OLIconButtonProps) {
- const { bs3Props, ...rest } = props
+const OLIconButton = forwardRef(
+ (props, ref) => {
+ const { bs3Props, ...rest } = props
- const { fw, loading, ...bs3Rest } = bs3Props || {}
+ const { fw, loading, ...bs3Rest } = bs3Props || {}
- return (
-
- {loading || (
-
- )}
-
- }
- bs5={}
- />
- )
-}
+ // BS3 OverlayTrigger automatically provides 'onMouseOver', 'onMouseOut', 'onFocus', 'onBlur' event handlers
+ const bs3FinalProps = {
+ 'aria-label': rest.accessibilityLabel,
+ ...bs3ButtonProps(rest),
+ ...bs3Rest,
+ onMouseOver: callFnsInSequence(bs3Props?.onMouseOver, rest.onMouseOver),
+ onMouseOut: callFnsInSequence(bs3Props?.onMouseOut, rest.onMouseOut),
+ onFocus: callFnsInSequence(bs3Props?.onFocus, rest.onFocus),
+ onBlur: callFnsInSequence(bs3Props?.onBlur, rest.onBlur),
+ }
+
+ // BS3 tooltip relies on the 'onMouseOver', 'onMouseOut', 'onFocus', 'onBlur' props
+ // BS5 tooltip relies on the ref
+ return (
+
+ {loading || }
+
+ }
+ bs5={}
+ />
+ )
+ }
+)
+OLIconButton.displayName = 'OLIconButton'
+
+export default OLIconButton
diff --git a/services/web/frontend/js/features/ui/components/types/button-props.ts b/services/web/frontend/js/features/ui/components/types/button-props.ts
index 794d7422d8..5d76cbf4c6 100644
--- a/services/web/frontend/js/features/ui/components/types/button-props.ts
+++ b/services/web/frontend/js/features/ui/components/types/button-props.ts
@@ -1,4 +1,4 @@
-import type { MouseEventHandler, ReactNode } from 'react'
+import type { ReactNode } from 'react'
export type ButtonProps = {
children?: ReactNode
@@ -11,7 +11,11 @@ export type ButtonProps = {
rel?: string
isLoading?: boolean
loadingLabel?: string
- onClick?: MouseEventHandler
+ onClick?: React.MouseEventHandler
+ onMouseOver?: React.MouseEventHandler
+ onMouseOut?: React.MouseEventHandler
+ onFocus?: React.FocusEventHandler
+ onBlur?: React.FocusEventHandler
size?: 'small' | 'default' | 'large'
trailingIcon?: string
type?: 'button' | 'reset' | 'submit'
diff --git a/services/web/frontend/js/features/ui/components/types/dropdown-menu-props.ts b/services/web/frontend/js/features/ui/components/types/dropdown-menu-props.ts
index 6118c5c8ce..1e3fc69aba 100644
--- a/services/web/frontend/js/features/ui/components/types/dropdown-menu-props.ts
+++ b/services/web/frontend/js/features/ui/components/types/dropdown-menu-props.ts
@@ -16,6 +16,7 @@ export type DropdownProps = {
onSelect?: (eventKey: any, event: object) => any
onToggle?: (show: boolean) => void
show?: boolean
+ autoClose?: boolean | 'inside' | 'outside'
}
export type DropdownItemProps = PropsWithChildren<{
@@ -43,6 +44,7 @@ export type DropdownToggleProps = PropsWithChildren<{
id?: string // necessary for assistive technologies
variant?: SplitButtonVariants
as?: ElementType
+ size?: 'sm' | 'lg'
}>
export type DropdownMenuProps = PropsWithChildren<{
@@ -60,4 +62,5 @@ export type DropdownDividerProps = PropsWithChildren<{
export type DropdownHeaderProps = PropsWithChildren<{
as?: ElementType
+ className?: string
}>
diff --git a/services/web/frontend/js/utils/functions.ts b/services/web/frontend/js/utils/functions.ts
index dfc46d8cad..bf189aefa4 100644
--- a/services/web/frontend/js/utils/functions.ts
+++ b/services/web/frontend/js/utils/functions.ts
@@ -1,6 +1,6 @@
export function callFnsInSequence<
- Args,
- Fn extends ((...args: Args[]) => void) | void,
+ Args extends Array,
+ Fn extends ((...args: Args) => void) | void,
>(...fns: Fn[]) {
- return (...args: Args[]) => fns.forEach(fn => fn?.(...args))
+ return (...args: Args) => fns.forEach(fn => fn?.(...args))
}
diff --git a/services/web/frontend/js/utils/react.ts b/services/web/frontend/js/utils/react.ts
new file mode 100644
index 0000000000..c399409d0c
--- /dev/null
+++ b/services/web/frontend/js/utils/react.ts
@@ -0,0 +1,21 @@
+import { forwardRef } from 'react'
+
+export const fixedForwardRef = <
+ T,
+ P = object,
+ A extends Record = Record<
+ string,
+ React.FunctionComponent
+ >,
+>(
+ render: (props: P, ref: React.Ref) => React.ReactElement | null,
+ propsToAttach: A = {} as A
+): ((props: P & React.RefAttributes) => React.ReactElement | null) & A => {
+ const ForwardReferredComponent = forwardRef(render) as any
+
+ for (const i in propsToAttach) {
+ ForwardReferredComponent[i] = propsToAttach[i]
+ }
+
+ return ForwardReferredComponent
+}
diff --git a/services/web/frontend/stories/ui/dropdown-menu.stories.tsx b/services/web/frontend/stories/ui/dropdown-menu.stories.tsx
index 0062b15176..fe7208f653 100644
--- a/services/web/frontend/stories/ui/dropdown-menu.stories.tsx
+++ b/services/web/frontend/stories/ui/dropdown-menu.stories.tsx
@@ -2,6 +2,7 @@ import {
DropdownMenu,
DropdownItem,
DropdownDivider,
+ DropdownHeader,
} from '@/features/ui/components/bootstrap-5/dropdown-menu'
import type { Meta } from '@storybook/react'
@@ -58,6 +59,35 @@ export const Active = (args: Args) => {
)
}
+export const MultipleSelection = (args: Args) => {
+ console.log('DropdownItem.EmptyLeadingIcon', DropdownItem.EmptyLeadingIcon)
+
+ return (
+
+ Header
+
+ }
+ >
+ Example
+
+
+
+
+ Example
+
+
+
+
+ Example
+
+
+
+ )
+}
+
export const Danger = (args: Args) => {
return (
diff --git a/services/web/frontend/stylesheets/app/project-list-react.less b/services/web/frontend/stylesheets/app/project-list-react.less
index 37917cecc6..a49b4cd612 100644
--- a/services/web/frontend/stylesheets/app/project-list-react.less
+++ b/services/web/frontend/stylesheets/app/project-list-react.less
@@ -205,8 +205,6 @@
font-size: @font-size-large;
line-height: 28px;
font-weight: bold;
- overflow: hidden;
- text-overflow: ellipsis;
}
ul.project-list-filters {
diff --git a/services/web/frontend/stylesheets/bootstrap-5/base/bootstrap.scss b/services/web/frontend/stylesheets/bootstrap-5/base/bootstrap.scss
index b32748c842..97fb2f79d1 100644
--- a/services/web/frontend/stylesheets/bootstrap-5/base/bootstrap.scss
+++ b/services/web/frontend/stylesheets/bootstrap-5/base/bootstrap.scss
@@ -31,6 +31,7 @@
@import 'bootstrap-5/scss/forms';
@import 'bootstrap-5/scss/buttons';
@import 'bootstrap-5/scss/dropdown';
+@import 'bootstrap-5/scss/button-group';
@import 'bootstrap-5/scss/badge';
@import 'bootstrap-5/scss/modal';
@import 'bootstrap-5/scss/tooltip';
diff --git a/services/web/frontend/stylesheets/bootstrap-5/components/all.scss b/services/web/frontend/stylesheets/bootstrap-5/components/all.scss
index a525230c56..dfa4110286 100644
--- a/services/web/frontend/stylesheets/bootstrap-5/components/all.scss
+++ b/services/web/frontend/stylesheets/bootstrap-5/components/all.scss
@@ -1,4 +1,5 @@
@import 'button';
+@import 'button-group';
@import 'dropdown-menu';
@import 'split-button';
@import 'notifications';
diff --git a/services/web/frontend/stylesheets/bootstrap-5/components/button-group.scss b/services/web/frontend/stylesheets/bootstrap-5/components/button-group.scss
new file mode 100644
index 0000000000..e0de77102f
--- /dev/null
+++ b/services/web/frontend/stylesheets/bootstrap-5/components/button-group.scss
@@ -0,0 +1,15 @@
+.btn-group {
+ > .btn {
+ &:first-child {
+ padding-left: var(--spacing-05);
+ }
+
+ &:last-child {
+ padding-right: var(--spacing-05);
+ }
+ }
+}
+
+.btn-toolbar {
+ gap: var(--spacing-03);
+}
diff --git a/services/web/frontend/stylesheets/bootstrap-5/components/button.scss b/services/web/frontend/stylesheets/bootstrap-5/components/button.scss
index 812bf430d3..af7f274dd9 100644
--- a/services/web/frontend/stylesheets/bootstrap-5/components/button.scss
+++ b/services/web/frontend/stylesheets/bootstrap-5/components/button.scss
@@ -221,3 +221,19 @@ a.btn:visited {
.copy-button {
text-decoration: none;
}
+
+.btn-reset {
+ @include reset-button;
+}
+
+.btn-transparent {
+ background: none !important;
+ border-radius: 0 !important;
+ color: inherit !important;
+ font-weight: 400;
+
+ &:hover {
+ background: none !important;
+ color: inherit !important;
+ }
+}
diff --git a/services/web/frontend/stylesheets/bootstrap-5/components/dropdown-menu.scss b/services/web/frontend/stylesheets/bootstrap-5/components/dropdown-menu.scss
index af707b494f..1cd660c686 100644
--- a/services/web/frontend/stylesheets/bootstrap-5/components/dropdown-menu.scss
+++ b/services/web/frontend/stylesheets/bootstrap-5/components/dropdown-menu.scss
@@ -104,6 +104,18 @@
}
}
+.dropdown-item-leading-icon,
+.dropdown-item-trailing-icon {
+ .material-symbols {
+ vertical-align: top;
+ }
+}
+
+.dropdown-item-leading-icon-empty {
+ display: inline-block;
+ width: 20px;
+}
+
// description text should look disabled when the dropdown item is disabled
.dropdown-item.disabled .dropdown-item-description,
.dropdown-item[aria-disabled='true'] .dropdown-item-description {
diff --git a/services/web/frontend/stylesheets/bootstrap-5/pages/project-list.scss b/services/web/frontend/stylesheets/bootstrap-5/pages/project-list.scss
index 7eec71a1a6..11cb5c5f9d 100644
--- a/services/web/frontend/stylesheets/bootstrap-5/pages/project-list.scss
+++ b/services/web/frontend/stylesheets/bootstrap-5/pages/project-list.scss
@@ -164,6 +164,65 @@
padding: var(--spacing-08) var(--spacing-06);
}
+ .project-list-header-row {
+ display: flex;
+ align-items: center;
+ margin-bottom: var(--spacing-05);
+ min-height: 36px;
+ }
+
+ .project-list-title {
+ min-width: 0;
+ color: $content-secondary;
+
+ @include heading-sm;
+
+ font-weight: bold;
+ }
+
+ .project-tools {
+ flex-shrink: 0;
+ margin-left: auto;
+ }
+
+ @include media-breakpoint-down(md) {
+ .project-tools {
+ float: left;
+ margin-left: initial;
+ }
+ }
+
+ .projects-toolbar {
+ display: flex;
+ align-items: center;
+
+ .dropdown,
+ .dropdown-toggle {
+ max-width: 100%;
+ }
+
+ .dropdown {
+ min-width: 0;
+ }
+ }
+
+ .projects-sort-dropdown {
+ flex-shrink: 0;
+ margin-left: auto;
+ }
+
+ .project-menu-item-edit-btn {
+ position: absolute;
+ top: 0;
+ right: var(--spacing-09);
+ width: initial;
+ background-color: transparent;
+ }
+
+ .project-menu-item-tag-name {
+ padding-right: var(--spacing-13);
+ }
+
ul.project-list-filters {
margin: var(--spacing-05) calc(-1 * var(--spacing-06));
@@ -609,6 +668,18 @@
}
}
+.current-plan {
+ a.current-plan-label {
+ text-decoration: none;
+ color: $content-secondary;
+ }
+
+ .current-plan-label-icon {
+ vertical-align: text-bottom;
+ color: var(--bg-info-01);
+ }
+}
+
/* stylelint-disable selector-class-pattern */
.project-list-upload-project-modal-uppy-dashboard .uppy-Root {
.uppy-Dashboard-AddFiles-title {
@@ -747,7 +818,7 @@ form.project-search {
margin: 3px; // it's centered, no matching spacing variable
font-weight: bold;
- @include media-breakpoint-down(sm) {
+ @include media-breakpoint-down(md) {
margin: 5px; // it's centered, no matching spacing variable
}
}
@@ -757,7 +828,7 @@ form.project-search {
margin: 3px;
font-weight: bold;
- @include media-breakpoint-down(sm) {
+ @include media-breakpoint-down(md) {
margin: 5px;
}
}
@@ -769,7 +840,7 @@ form.project-search {
font-weight: bold;
}
- @include media-breakpoint-down(sm) {
+ @include media-breakpoint-down(md) {
height: 32px;
width: 32px;
margin: var(--spacing-08);
diff --git a/services/web/locales/en.json b/services/web/locales/en.json
index 030c09e012..c7be9a968c 100644
--- a/services/web/locales/en.json
+++ b/services/web/locales/en.json
@@ -653,6 +653,7 @@
"files_cannot_include_invalid_characters": "File name is empty or contains invalid characters",
"files_selected": "files selected.",
"fill_in_our_quick_survey": "Fill in our quick survey.",
+ "filter_projects": "Filter projects",
"filters": "Filters",
"find_out_more": "Find out More",
"find_out_more_about_institution_login": "Find out more about institutional login",
@@ -1863,6 +1864,7 @@
"sorry_your_token_expired": "Sorry, your token expired",
"sort_by": "Sort by",
"sort_by_x": "Sort by __x__",
+ "sort_projects": "Sort projects",
"source": "Source",
"spell_check": "Spell check",
"sso": "SSO",
@@ -2117,6 +2119,10 @@
"toolbar_insert_table": "Insert Table",
"toolbar_numbered_list": "Numbered List",
"toolbar_redo": "Redo",
+ "toolbar_selected_projects": "Selected projects",
+ "toolbar_selected_projects_management_actions": "Selected projects management actions",
+ "toolbar_selected_projects_remove": "Remove selected projects",
+ "toolbar_selected_projects_restore": "Restore selected projects",
"toolbar_table_insert_size_table": "Insert __size__ table",
"toolbar_table_insert_table_lowercase": "Insert table",
"toolbar_toggle_symbol_palette": "Toggle Symbol Palette",
diff --git a/services/web/test/frontend/features/project-list/components/table/project-tools/project-tools.test.tsx b/services/web/test/frontend/features/project-list/components/table/project-tools/project-tools.test.tsx
index ab8caf15a4..0922dfd699 100644
--- a/services/web/test/frontend/features/project-list/components/table/project-tools/project-tools.test.tsx
+++ b/services/web/test/frontend/features/project-list/components/table/project-tools/project-tools.test.tsx
@@ -17,7 +17,7 @@ describe('', function () {
screen.getByLabelText('Download')
screen.getByLabelText('Archive')
screen.getByLabelText('Trash')
- screen.getByTitle('Tags')
+ screen.getByLabelText('Tags')
screen.getByRole('button', { name: 'Create new tag' })
})
})