diff --git a/services/web/frontend/js/features/project-list/components/add-affiliation.tsx b/services/web/frontend/js/features/project-list/components/add-affiliation.tsx
index 151ecf9548..de4a130bfc 100644
--- a/services/web/frontend/js/features/project-list/components/add-affiliation.tsx
+++ b/services/web/frontend/js/features/project-list/components/add-affiliation.tsx
@@ -1,8 +1,8 @@
import { useTranslation } from 'react-i18next'
-import { Button } from 'react-bootstrap'
import { useProjectListContext } from '../context/project-list-context'
import getMeta from '../../../utils/meta'
import classNames from 'classnames'
+import OLButton from '@/features/ui/components/ol/ol-button'
export function useAddAffiliation() {
const { totalProjectsCount } = useProjectListContext()
@@ -26,18 +26,14 @@ function AddAffiliation({ className }: AddAffiliationProps) {
return null
}
- const classes = classNames('text-centered', 'add-affiliation', className)
+ const classes = classNames('text-center', 'add-affiliation', className)
return (
{t('are_you_affiliated_with_an_institution')}
-
+
)
}
diff --git a/services/web/frontend/js/features/project-list/components/color-picker/color-picker.tsx b/services/web/frontend/js/features/project-list/components/color-picker/color-picker.tsx
index 4160e2fece..ab0a373b2e 100644
--- a/services/web/frontend/js/features/project-list/components/color-picker/color-picker.tsx
+++ b/services/web/frontend/js/features/project-list/components/color-picker/color-picker.tsx
@@ -1,9 +1,9 @@
-import Icon from '../../../../shared/components/icon'
import useSelectColor from '../../hooks/use-select-color'
import { SketchPicker } from 'react-color'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
-import Tooltip from '../../../../shared/components/tooltip'
+import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
+import MaterialIcon from '@/shared/components/material-icon'
const PRESET_COLORS: ReadonlyArray<{ color: string; name: string }> = [
{ color: '#A7B1C2', name: 'Grey' },
@@ -48,7 +48,7 @@ function ColorPickerItem({ color, name }: ColorPickerItemProps) {
{t('select_color', { name })}
{!pickingCustomColor && color === selectedColor && (
-
+
)}
)
@@ -95,19 +95,25 @@ function MoreButton() {
tabIndex={0}
onKeyDown={handleKeyDown}
>
-
- {isCustomColorSelected ? (
-
- ) : showCustomPicker ? (
-
- ) : (
-
- )}
-
+
+ {isCustomColorSelected ? (
+
+ ) : showCustomPicker ? (
+
+ ) : (
+
+ )}
+
+
{showCustomPicker && (
<>
diff --git a/services/web/frontend/js/features/project-list/components/modals/create-tag-modal.tsx b/services/web/frontend/js/features/project-list/components/modals/create-tag-modal.tsx
index bd906687f4..cf2eb62e97 100644
--- a/services/web/frontend/js/features/project-list/components/modals/create-tag-modal.tsx
+++ b/services/web/frontend/js/features/project-list/components/modals/create-tag-modal.tsx
@@ -1,8 +1,6 @@
import { useCallback, useEffect, useState } from 'react'
-import { Button, ControlLabel, Form, FormGroup, Modal } from 'react-bootstrap'
import { useTranslation } from 'react-i18next'
import { Tag } from '../../../../../../app/src/Features/Tags/types'
-import AccessibleModal from '../../../../shared/components/accessible-modal'
import useAsync from '../../../../shared/hooks/use-async'
import { useProjectListContext } from '../../context/project-list-context'
import { useRefWithAutoFocus } from '../../../../shared/hooks/use-ref-with-auto-focus'
@@ -11,6 +9,18 @@ import { createTag } from '../../util/api'
import { MAX_TAG_LENGTH } from '../../util/tag'
import { ColorPicker } from '../color-picker/color-picker'
import { debugConsole } from '@/utils/debugging'
+import OLModal, {
+ OLModalBody,
+ OLModalFooter,
+ OLModalHeader,
+ OLModalTitle,
+} from '@/features/ui/components/ol/ol-modal'
+import OLFormGroup from '@/features/ui/components/ol/ol-form-group'
+import OLFormLabel from '@/features/ui/components/ol/ol-form-label'
+import OLButton from '@/features/ui/components/ol/ol-button'
+import OLNotification from '@/features/ui/components/ol/ol-notification'
+import OLFormControl from '@/features/ui/components/ol/ol-form-control'
+import OLForm from '@/features/ui/components/ol/ol-form'
type CreateTagModalProps = {
id: string
@@ -30,7 +40,7 @@ export default function CreateTagModal({
const { tags } = useProjectListContext()
const { selectedColor } = useSelectColor()
const { t } = useTranslation()
- const { isError, runAsync, status } = useAsync()
+ const { isLoading, isError, runAsync, status } = useAsync()
const { autoFocusedRef } = useRefWithAutoFocus()
const [tagName, setTagName] = useState()
@@ -69,60 +79,63 @@ export default function CreateTagModal({
}
return (
-
-
- {t('create_new_tag')}
-
+
+
+ {t('create_new_tag')}
+
-
-
-
-
-
+
+
{validationError && (
-
- {validationError}
-
+
)}
{isError && (
-
-
- {t('generic_something_went_wrong')}
-
-
+
)}
-
-
-
-
+ {t('create')}
+
+
+
)
}
diff --git a/services/web/frontend/js/features/project-list/components/modals/delete-tag-modal.tsx b/services/web/frontend/js/features/project-list/components/modals/delete-tag-modal.tsx
index 0fa2e8c4eb..b66b2a5b8d 100644
--- a/services/web/frontend/js/features/project-list/components/modals/delete-tag-modal.tsx
+++ b/services/web/frontend/js/features/project-list/components/modals/delete-tag-modal.tsx
@@ -1,11 +1,17 @@
import { useCallback } from 'react'
-import { Button, Modal } from 'react-bootstrap'
import { useTranslation } from 'react-i18next'
import { Tag } from '../../../../../../app/src/Features/Tags/types'
-import AccessibleModal from '../../../../shared/components/accessible-modal'
import useAsync from '../../../../shared/hooks/use-async'
import { deleteTag } from '../../util/api'
import { debugConsole } from '@/utils/debugging'
+import OLModal, {
+ OLModalBody,
+ OLModalFooter,
+ OLModalHeader,
+ OLModalTitle,
+} from '@/features/ui/components/ol/ol-modal'
+import OLButton from '@/features/ui/components/ol/ol-button'
+import OLNotification from '@/features/ui/components/ol/ol-notification'
type DeleteTagModalProps = {
id: string
@@ -39,42 +45,40 @@ export default function DeleteTagModal({
}
return (
-
-
- {t('delete_tag')}
-
+
+
+ {t('delete_tag')}
+
-
+
{t('about_to_delete_tag')}
-
-
-
{isError && (
-
-
- {t('generic_something_went_wrong')}
-
-
+
)}
-
-
-
-
+ {t('delete')}
+
+
+
)
}
diff --git a/services/web/frontend/js/features/project-list/components/modals/edit-tag-modal.tsx b/services/web/frontend/js/features/project-list/components/modals/edit-tag-modal.tsx
index 31593d1df2..621f3ed8d8 100644
--- a/services/web/frontend/js/features/project-list/components/modals/edit-tag-modal.tsx
+++ b/services/web/frontend/js/features/project-list/components/modals/edit-tag-modal.tsx
@@ -1,8 +1,6 @@
import { useCallback, useEffect, useState } from 'react'
-import { Button, ControlLabel, Form, FormGroup, Modal } from 'react-bootstrap'
import { useTranslation } from 'react-i18next'
import { Tag } from '../../../../../../app/src/Features/Tags/types'
-import AccessibleModal from '../../../../shared/components/accessible-modal'
import useAsync from '../../../../shared/hooks/use-async'
import { useProjectListContext } from '../../context/project-list-context'
import { useRefWithAutoFocus } from '../../../../shared/hooks/use-ref-with-auto-focus'
@@ -11,6 +9,17 @@ import { editTag } from '../../util/api'
import { getTagColor, MAX_TAG_LENGTH } from '../../util/tag'
import { ColorPicker } from '../color-picker/color-picker'
import { debugConsole } from '@/utils/debugging'
+import OLModal, {
+ OLModalBody,
+ OLModalFooter,
+ OLModalHeader,
+ OLModalTitle,
+} from '@/features/ui/components/ol/ol-modal'
+import OLForm from '@/features/ui/components/ol/ol-form'
+import OLFormGroup from '@/features/ui/components/ol/ol-form-group'
+import OLFormLabel from '@/features/ui/components/ol/ol-form-label'
+import OLButton from '@/features/ui/components/ol/ol-button'
+import OLNotification from '@/features/ui/components/ol/ol-notification'
type EditTagModalProps = {
id: string
@@ -77,14 +86,14 @@ export function EditTagModal({ id, tag, onEdit, onClose }: EditTagModalProps) {
}
return (
-
-
- {t('edit_tag')}
-
+
+
+ {t('edit_tag')}
+
-
-
-
-
-
+
+
{validationError && (
-
- {validationError}
-
+
)}
{isError && (
-
-
- {t('generic_something_went_wrong')}
-
-
+
)}
-
-
-
-
+ {t('save')}
+
+
+
)
}
diff --git a/services/web/frontend/js/features/project-list/components/sidebar/sidebar-filters.tsx b/services/web/frontend/js/features/project-list/components/sidebar/sidebar-filters.tsx
index 8dc982f0ce..325456f6cb 100644
--- a/services/web/frontend/js/features/project-list/components/sidebar/sidebar-filters.tsx
+++ b/services/web/frontend/js/features/project-list/components/sidebar/sidebar-filters.tsx
@@ -1,5 +1,4 @@
import { useTranslation } from 'react-i18next'
-import { Button } from 'react-bootstrap'
import {
Filter,
useProjectListContext,
@@ -19,9 +18,9 @@ export function SidebarFilter({ filter, text }: SidebarFilterProps) {
{isActive => (
-
)}
diff --git a/services/web/frontend/js/features/project-list/components/sidebar/tags-list.tsx b/services/web/frontend/js/features/project-list/components/sidebar/tags-list.tsx
index 084e102417..6e868a9204 100644
--- a/services/web/frontend/js/features/project-list/components/sidebar/tags-list.tsx
+++ b/services/web/frontend/js/features/project-list/components/sidebar/tags-list.tsx
@@ -1,7 +1,5 @@
import { sortBy } from 'lodash'
-import { Button } from 'react-bootstrap'
import { useTranslation } from 'react-i18next'
-import Icon from '../../../../shared/components/icon'
import MaterialIcon from '../../../../shared/components/material-icon'
import {
UNCATEGORIZED_KEY,
@@ -9,6 +7,13 @@ import {
} from '../../context/project-list-context'
import useTag from '../../hooks/use-tag'
import { getTagColor } from '../../util/tag'
+import {
+ Dropdown,
+ DropdownItem,
+ DropdownMenu,
+ DropdownToggle,
+} from '@/features/ui/components/bootstrap-5/dropdown-menu'
+import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
export default function TagsList() {
const { t } = useTranslation()
@@ -35,14 +40,10 @@ export default function TagsList() {
{t('organize_projects')}
-
-
+
+
{t('new_tag')}
-
+
{sortBy(tags, tag => tag.name?.toLowerCase()).map(tag => {
return (
@@ -50,19 +51,19 @@ export default function TagsList() {
className={`tag ${selectedTagId === tag._id ? 'active' : ''}`}
key={tag._id}
>
-
handleSelectTag(e as unknown as React.MouseEvent, tag._id)
}
- bsStyle={null}
>
-
+
{tag.name}{' '}
@@ -70,36 +71,66 @@ export default function TagsList() {
({projectsPerTag[tag._id].length})
-
-
-
-
-
-
- -
- handleEditTag(e, tag._id)}
- className="tag-action"
+
+
+
+
+
+
+ handleEditTag(e, tag._id)}
+ >
+ {t('edit')}
+
+ handleDeleteTag(e, tag._id)}
+ >
+ {t('delete')}
+
+
+
+ }
+ bs3={
+
+
- {t('edit')}
-
-
- -
- handleDeleteTag(e, tag._id)}
- className="tag-action"
- >
- {t('delete')}
-
-
-
-
+
+
+
+ -
+ handleEditTag(e, tag._id)}
+ className="tag-action"
+ >
+ {t('edit')}
+
+
+ -
+ handleDeleteTag(e, tag._id)}
+ className="tag-action"
+ >
+ {t('delete')}
+
+
+
+
+ }
+ />
)
})}
@@ -109,16 +140,16 @@ export default function TagsList() {
selectedTagId === UNCATEGORIZED_KEY ? 'active' : ''
}`}
>
- selectTag(UNCATEGORIZED_KEY)}
- bsStyle={null}
>
{t('uncategorized')}{' '}
({untaggedProjectsCount})
-
+
)}
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 6c9a483c06..9b413cb71c 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
@@ -12,6 +12,7 @@ export type DropdownProps = {
| { xxl: 'start' | 'end' }
as?: ElementType
children: ReactNode
+ className?: string
onSelect?: (eventKey: any, event: object) => any
onToggle?: (show: boolean) => void
show?: boolean
diff --git a/services/web/frontend/stylesheets/app/project-list-react.less b/services/web/frontend/stylesheets/app/project-list-react.less
index db2ae13872..285711284e 100644
--- a/services/web/frontend/stylesheets/app/project-list-react.less
+++ b/services/web/frontend/stylesheets/app/project-list-react.less
@@ -941,26 +941,29 @@
.color-picker-more {
color: @gray-dark;
- margin: 6px 0 0 7px;
+ margin: 3px;
+ font-weight: bold;
@media (max-width: @screen-xs-max) {
- margin: 8px 0 0 9px;
+ margin: 5px;
}
}
.color-picker-more-open {
color: @gray-dark;
- margin: 5px 0 0 5px;
+ margin: 3px;
+ font-weight: bold;
@media (max-width: @screen-xs-max) {
- margin: 7px 0 0 7px;
+ margin: 5px;
}
}
}
.color-picker-item-icon {
- margin: 6px 0 0 6px;
+ margin: 4px;
color: @white;
+ font-weight: bold;
}
@media (max-width: @screen-xs-max) {
@@ -969,7 +972,7 @@
margin: 24px 24px;
.color-picker-item-icon {
- margin: 8px 0 0 8px;
+ margin: 6px;
color: @white;
}
}
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 b07e61e410..9d0a8ac0f2 100644
--- a/services/web/frontend/stylesheets/bootstrap-5/components/dropdown-menu.scss
+++ b/services/web/frontend/stylesheets/bootstrap-5/components/dropdown-menu.scss
@@ -4,6 +4,7 @@
.dropdown-header {
@include body-xs;
+
padding: var(--spacing-05) var(--spacing-06) var(--spacing-02)
var(--spacing-04);
}
@@ -12,16 +13,20 @@
@include shadow-md;
min-width: 240px;
+
+ &.sm {
+ min-width: 160px;
+ }
}
.dropdown-item {
@include body-sm;
--bs-dropdown-item-border-radius: var(--border-radius-base);
+
display: grid;
grid-auto-flow: column;
- justify-content: start;
- align-content: center;
+ place-content: center start;
min-height: 44px; // a minimum height of 44px to be accessible for touch screens
position: relative;
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 23af3a2e9a..600c02fc12 100644
--- a/services/web/frontend/stylesheets/bootstrap-5/pages/project-list.scss
+++ b/services/web/frontend/stylesheets/bootstrap-5/pages/project-list.scss
@@ -1,11 +1,13 @@
.project-list-empty-col {
display: flex;
height: 100%;
- flex-direction: column;
+ flex-flow: column nowrap;
flex-wrap: nowrap;
+
.row:first-child {
flex-grow: 1; /* fill vertical space so notifications are pushed to bottom */
}
+
.card-body {
// h2 + .card-thin top padding
padding-bottom: calc(var(--line-height-03) + var(--line-height-03) / 2);
@@ -47,8 +49,7 @@
.project-list-sidebar-react {
flex-grow: 1;
- padding-left: var(--spacing-06);
- padding-right: var(--spacing-06);
+ padding: var(--spacing-08) var(--spacing-06);
-ms-overflow-style: -ms-autohiding-scrollbar;
padding-top: var(--spacing-08);
padding-bottom: var(--spacing-08);
@@ -61,6 +62,7 @@
button {
white-space: normal;
word-wrap: anywhere;
+
// prevents buttons from expanding sidebar width
}
@@ -81,7 +83,8 @@
.welcome-new-wrapper {
.welcome-title {
- @include heading-xl();
+ @include heading-xl;
+
margin-top: var(--spacing-08);
}
@@ -160,6 +163,196 @@
padding: var(--spacing-08) var(--spacing-06);
}
+ ul.project-list-filters {
+ margin: var(--spacing-05) calc(-1 * var(--spacing-06));
+
+ .subdued {
+ color: var(--content-disabled);
+ }
+
+ > li {
+ cursor: pointer;
+ position: relative;
+
+ > button {
+ width: 100%;
+ font-weight: normal;
+ text-align: left;
+ color: var(--white);
+ background-color: transparent;
+ border-radius: unset;
+ border: none;
+ border-bottom: solid 1px transparent;
+ padding: var(--spacing-03) var(--spacing-06);
+
+ &:hover {
+ background-color: var(--neutral-70);
+ text-decoration: none;
+ }
+
+ &:focus {
+ text-decoration: none;
+ outline: none;
+ }
+ }
+
+ &.separator {
+ padding: var(--spacing-03) var(--spacing-06);
+ cursor: auto;
+ }
+ }
+
+ > li.active {
+ border-radius: 0;
+
+ > button {
+ background-color: var(--neutral-90);
+ font-weight: 700;
+ color: var(--white);
+
+ .subdued {
+ color: var(--white);
+ }
+ }
+ }
+
+ h2 {
+ font-size: var(--font-size-02);
+ margin-bottom: var(--spacing-00);
+ color: var(--content-disabled);
+ text-transform: uppercase;
+ padding: var(--spacing-03) var(--spacing-00);
+ }
+
+ > li.tag {
+ &.active {
+ .tag-menu > button {
+ color: var(--white);
+ border-color: var(--white);
+
+ &:hover {
+ background-color: var(--neutral-90);
+ }
+ }
+ }
+
+ &.untagged {
+ button.tag-name {
+ span.name {
+ font-style: italic;
+ padding-left: 0;
+ }
+ }
+ }
+
+ &:hover {
+ &:not(.active) {
+ background-color: var(--neutral-70);
+ }
+
+ .tag-menu {
+ display: block;
+ }
+ }
+
+ &:not(.active) {
+ .tag-menu > a:hover {
+ background-color: var(--neutral-90);
+ }
+ }
+
+ button.tag-name {
+ position: relative;
+ padding: var(--spacing-03) var(--spacing-09) var(--spacing-03)
+ var(--spacing-06);
+ display: flex;
+ align-items: center;
+ word-wrap: anywhere;
+
+ .tag-list-icon {
+ vertical-align: sub;
+ font-weight: bold;
+ }
+
+ span.name {
+ padding-left: 0.5em;
+ line-height: 1.4;
+ }
+ }
+ }
+
+ .tag-menu {
+ button.dropdown-toggle {
+ border: 1px solid var(--white);
+ border-radius: var(--border-radius-base);
+ background-color: transparent;
+ color: var(--white);
+ display: block;
+ width: 16px;
+ height: 16px;
+ position: relative;
+ padding: var(--spacing-01) var(--spacing-03);
+
+ &::after {
+ position: absolute;
+ top: 4px;
+ left: -2px;
+ }
+ }
+
+ display: none;
+ width: auto;
+ position: absolute;
+ top: 50%;
+ margin-top: -8px; // Half the element height.
+ right: 4px;
+
+ &.open {
+ display: block;
+ }
+
+ button.tag-action {
+ border-radius: unset;
+ width: 100%;
+ background-color: transparent;
+ border-color: transparent;
+ color: var(--neutral-70);
+ text-align: left;
+ font-weight: normal;
+
+ &:hover {
+ color: var(--white);
+ background-color: var(--bg-accent-01);
+ }
+
+ &:active {
+ outline: none;
+ }
+ }
+ }
+ }
+}
+
+.add-affiliation-mobile-wrapper {
+ padding: var(--spacing-07) 0;
+}
+
+.add-affiliation {
+ .progress {
+ height: var(--spacing-05);
+ margin-bottom: var(--spacing-03);
+ }
+
+ p {
+ margin-bottom: var(--spacing-03);
+ }
+
+ &.is-mobile p {
+ @include body-xs;
+
+ white-space: normal;
+ }
+
.project-dash-table {
width: 100%;
table-layout: fixed;
@@ -231,7 +424,8 @@
.dash-cell-date-owner {
font-size: $font-size-sm;
- @include text-truncate();
+
+ @include text-truncate;
}
@include media-breakpoint-up(sm) {
@@ -417,7 +611,7 @@
background-color: var(--bg-dark-tertiary);
border-color: transparent;
color: var(--neutral-20);
- box-shadow: 2px 4px 6px rgba(0, 0, 0, 0.25);
+ box-shadow: 2px 4px 6px rgb(0 0 0 / 25%);
border-radius: var(--border-radius-base);
@include media-breakpoint-up(md) {
@@ -426,8 +620,8 @@
button.close {
@extend .text-white;
+
padding: 0;
- -webkit-appearance: none;
}
}
@@ -474,3 +668,84 @@ form.project-search {
.project-search-clear-btn {
@include reset-button;
}
+
+.color-picker-item {
+ height: 28px;
+ width: 28px;
+ cursor: pointer;
+ position: relative;
+ outline: none;
+ border-radius: var(--border-radius-base);
+ margin: 0 var(--spacing-06) 0 0;
+ display: inline-block;
+ vertical-align: middle;
+
+ &:focus-visible {
+ box-shadow:
+ 0 0 0 2px var(--white),
+ 0 0 0 3px var(--blue-50),
+ 0 0 0 5px var(--blue-30);
+ }
+
+ &.more-button {
+ border: 1px solid var(--neutral-70);
+
+ .color-picker-more {
+ color: var(--neutral-70);
+ margin: 3px; // it's centered, no matching spacing variable
+ font-weight: bold;
+
+ @include media-breakpoint-down(sm) {
+ margin: 5px; // it's centered, no matching spacing variable
+ }
+ }
+
+ .color-picker-more-open {
+ color: var(--neutral-70);
+ margin: 3px;
+ font-weight: bold;
+
+ @include media-breakpoint-down(sm) {
+ margin: 5px;
+ }
+ }
+ }
+
+ .color-picker-item-icon {
+ margin: 3px; // it's centered, no matching spacing variable
+ color: var(--white);
+ font-weight: bold;
+ }
+
+ @include media-breakpoint-down(sm) {
+ height: 32px;
+ width: 32px;
+ margin: var(--spacing-08);
+
+ .color-picker-item-icon {
+ margin: 5px; // it's centered, no matching spacing variable
+ color: var(--white);
+ }
+ }
+}
+
+.color-picker-more-wrapper {
+ position: relative;
+ display: inline-block;
+
+ .custom-picker {
+ position: absolute;
+ user-select: none;
+ z-index: 1;
+
+ @include media-breakpoint-down(sm) {
+ top: 56px;
+ left: 24px;
+ }
+ }
+
+ .popover-backdrop {
+ position: fixed;
+ inset: 0;
+ }
+}