From 0e71084600c723564d9f007272320e205dc3d5c3 Mon Sep 17 00:00:00 2001 From: ilkin-overleaf <100852799+ilkin-overleaf@users.noreply.github.com> Date: Thu, 22 Aug 2024 15:14:22 +0300 Subject: [PATCH] Merge pull request #19840 from overleaf/ii-bs5-project-tools [web] BS5 project tools GitOrigin-RevId: 3181c62985b6db4051292b484f53178a0736fa75 --- server-ce/test/project-list.spec.ts | 4 +- .../web/frontend/extracted-translations.json | 6 + .../current-plan-widget/commons-plan.tsx | 38 +++- .../current-plan-widget/free-plan.tsx | 57 +++-- .../current-plan-widget/group-plan.tsx | 38 +++- .../current-plan-widget/individual-plan.tsx | 35 ++- .../components/dropdown/actions-dropdown.tsx | 2 +- .../components/dropdown/projects-dropdown.tsx | 159 +++++++++---- .../components/dropdown/sort-by-dropdown.tsx | 156 +++++++++---- .../components/modals/create-tag-modal.tsx | 6 +- .../components/modals/delete-tag-modal.tsx | 4 +- .../components/modals/edit-tag-modal.tsx | 8 +- .../components/modals/manage-tag-modal.tsx | 134 +++++------ .../components/project-list-root.tsx | 6 +- .../download-project-button.tsx | 2 +- .../components/table/cells/owner-cell.tsx | 31 ++- .../buttons/archive-projects-button.tsx | 25 ++- .../buttons/delete-leave-projects-button.tsx | 6 +- .../buttons/delete-projects-button.tsx | 6 +- .../buttons/download-projects-button.tsx | 25 ++- .../buttons/leave-projects-button.tsx | 6 +- .../project-tools-more-dropdown-button.tsx | 55 +++-- .../project-tools/buttons/tags-dropdown.tsx | 178 ++++++++++----- .../buttons/trash-projects-button.tsx | 25 ++- .../buttons/unarchive-projects-button.tsx | 12 +- .../buttons/untrash-projects-button.tsx | 10 +- .../menu-items/copy-project-menu-item.tsx | 6 +- .../menu-items/rename-project-menu-item.tsx | 6 +- .../table/project-tools/project-tools.tsx | 42 ++-- .../project-list/components/tags-list.tsx | 209 ++++++++++++------ .../components/title/project-list-title.tsx | 6 +- .../bootstrap-version-switcher.tsx | 4 +- .../ui/components/bootstrap-5/button.tsx | 110 +++++---- .../components/bootstrap-5/dropdown-menu.tsx | 138 ++++++------ .../ui/components/bootstrap-5/icon-button.tsx | 49 ++-- .../ui/components/bootstrap-5/tooltip.tsx | 14 +- .../ui/components/ol/ol-button-group.tsx | 30 +++ .../ui/components/ol/ol-button-toolbar.tsx | 31 +++ .../components/ol/ol-dropdown-menu-item.tsx | 27 +++ .../ui/components/ol/ol-icon-button.tsx | 58 +++-- .../ui/components/types/button-props.ts | 8 +- .../components/types/dropdown-menu-props.ts | 3 + services/web/frontend/js/utils/functions.ts | 6 +- services/web/frontend/js/utils/react.ts | 21 ++ .../stories/ui/dropdown-menu.stories.tsx | 30 +++ .../stylesheets/app/project-list-react.less | 2 - .../bootstrap-5/base/bootstrap.scss | 1 + .../bootstrap-5/components/all.scss | 1 + .../bootstrap-5/components/button-group.scss | 15 ++ .../bootstrap-5/components/button.scss | 16 ++ .../bootstrap-5/components/dropdown-menu.scss | 12 + .../bootstrap-5/pages/project-list.scss | 77 ++++++- services/web/locales/en.json | 6 + .../project-tools/project-tools.test.tsx | 2 +- 54 files changed, 1379 insertions(+), 585 deletions(-) create mode 100644 services/web/frontend/js/features/ui/components/ol/ol-button-group.tsx create mode 100644 services/web/frontend/js/features/ui/components/ol/ol-button-toolbar.tsx create mode 100644 services/web/frontend/js/features/ui/components/ol/ol-dropdown-menu-item.tsx create mode 100644 services/web/frontend/js/utils/react.ts create mode 100644 services/web/frontend/stylesheets/bootstrap-5/components/button-group.scss diff --git a/server-ce/test/project-list.spec.ts b/server-ce/test/project-list.spec.ts index a78bf241f7..9ee9ac9ca0 100644 --- a/server-ce/test/project-list.spec.ts +++ b/server-ce/test/project-list.spec.ts @@ -41,7 +41,7 @@ describe('Project List', () => { cy.visit('/project') findProjectRow(projectName).within(() => - cy.contains(`Download .zip file`).click() + cy.findByRole('button', { name: 'Download .zip file' }).click() ) cy.task('readFileInZip', { @@ -55,7 +55,7 @@ describe('Project List', () => { cy.visit('/project') findProjectRow(projectName).within(() => - cy.contains(`Download PDF`).click() + cy.findByRole('button', { name: 'Download PDF' }).click() ) const pdfName = projectName.replaceAll('-', '_') diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index 35f78c3d6f..bf77bdb9f6 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -446,6 +446,7 @@ "files_cannot_include_invalid_characters": "", "files_selected": "", "fill_in_our_quick_survey": "", + "filter_projects": "", "find_out_more": "", "find_out_more_about_institution_login": "", "find_out_more_about_the_file_outline": "", @@ -1298,6 +1299,7 @@ "sorry_your_table_cant_be_displayed_at_the_moment": "", "sort_by": "", "sort_by_x": "", + "sort_projects": "", "source": "", "spell_check": "", "sso": "", @@ -1499,6 +1501,10 @@ "toolbar_insert_table": "", "toolbar_numbered_list": "", "toolbar_redo": "", + "toolbar_selected_projects": "", + "toolbar_selected_projects_management_actions": "", + "toolbar_selected_projects_remove": "", + "toolbar_selected_projects_restore": "", "toolbar_table_insert_size_table": "", "toolbar_table_insert_table_lowercase": "", "toolbar_toggle_symbol_palette": "", diff --git a/services/web/frontend/js/features/project-list/components/current-plan-widget/commons-plan.tsx b/services/web/frontend/js/features/project-list/components/current-plan-widget/commons-plan.tsx index da4d2f1947..8eae9d1066 100644 --- a/services/web/frontend/js/features/project-list/components/current-plan-widget/commons-plan.tsx +++ b/services/web/frontend/js/features/project-list/components/current-plan-widget/commons-plan.tsx @@ -1,6 +1,10 @@ import { useTranslation, Trans } from 'react-i18next' import { CommonsPlanSubscription } from '../../../../../../types/project/dashboard/subscription' -import Tooltip from '../../../../shared/components/tooltip' +import OLTooltip from '@/features/ui/components/ol/ol-tooltip' +import MaterialIcon from '@/shared/components/material-icon' +import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher' +import { bsVersion } from '@/features/utils/bootstrap-5' +import classnames from 'classnames' type CommonsPlanProps = Pick< CommonsPlanSubscription, @@ -19,8 +23,15 @@ function CommonsPlan({ return ( <> - {currentPlanLabel} - + {currentPlanLabel} + + - - {currentPlanLabel} + + {currentPlanLabel}  + } + bs5={ + + } + /> - + ) } diff --git a/services/web/frontend/js/features/project-list/components/current-plan-widget/free-plan.tsx b/services/web/frontend/js/features/project-list/components/current-plan-widget/free-plan.tsx index 07a63f4fbf..9b50254f2b 100644 --- a/services/web/frontend/js/features/project-list/components/current-plan-widget/free-plan.tsx +++ b/services/web/frontend/js/features/project-list/components/current-plan-widget/free-plan.tsx @@ -1,8 +1,12 @@ import { useTranslation, Trans } from 'react-i18next' -import { Button } from 'react-bootstrap' +import OLButton from '@/features/ui/components/ol/ol-button' +import OLTooltip from '@/features/ui/components/ol/ol-tooltip' +import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher' +import MaterialIcon from '@/shared/components/material-icon' import { FreePlanSubscription } from '../../../../../../types/project/dashboard/subscription' -import Tooltip from '../../../../shared/components/tooltip' import * as eventTracking from '../../../../infrastructure/event-tracking' +import { bsVersion } from '@/features/utils/bootstrap-5' +import classnames from 'classnames' type FreePlanProps = Pick @@ -23,24 +27,49 @@ function FreePlan({ featuresPageURL }: FreePlanProps) { return ( <> - {currentPlanLabel} - + {currentPlanLabel} + + - - {currentPlanLabel} + + {currentPlanLabel}  + } + bs5={ + + } + /> - {' '} - + + {t('upgrade')} + + ) } diff --git a/services/web/frontend/js/features/project-list/components/current-plan-widget/group-plan.tsx b/services/web/frontend/js/features/project-list/components/current-plan-widget/group-plan.tsx index 9198c277d8..b5a01c5d28 100644 --- a/services/web/frontend/js/features/project-list/components/current-plan-widget/group-plan.tsx +++ b/services/web/frontend/js/features/project-list/components/current-plan-widget/group-plan.tsx @@ -1,6 +1,10 @@ import { useTranslation, Trans } from 'react-i18next' import { GroupPlanSubscription } from '../../../../../../types/project/dashboard/subscription' -import Tooltip from '../../../../shared/components/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 { bsVersion } from '@/features/utils/bootstrap-5' +import classnames from 'classnames' type GroupPlanProps = Pick< GroupPlanSubscription, @@ -33,8 +37,15 @@ function GroupPlan({ return ( <> - {currentPlanLabel} - + {currentPlanLabel} + + - - {currentPlanLabel} + + {currentPlanLabel}  + } + bs5={ + + } + /> - + ) } diff --git a/services/web/frontend/js/features/project-list/components/current-plan-widget/individual-plan.tsx b/services/web/frontend/js/features/project-list/components/current-plan-widget/individual-plan.tsx index 9aea683aab..cb542e645d 100644 --- a/services/web/frontend/js/features/project-list/components/current-plan-widget/individual-plan.tsx +++ b/services/web/frontend/js/features/project-list/components/current-plan-widget/individual-plan.tsx @@ -1,6 +1,10 @@ import { useTranslation, Trans } from 'react-i18next' import { IndividualPlanSubscription } from '../../../../../../types/project/dashboard/subscription' -import Tooltip from '../../../../shared/components/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 { bsVersion } from '@/features/utils/bootstrap-5' +import classnames from 'classnames' type IndividualPlanProps = Pick< IndividualPlanSubscription, @@ -32,16 +36,35 @@ function IndividualPlan({ return ( <> - {currentPlanLabel} - + {currentPlanLabel} + + - - {currentPlanLabel} + + {currentPlanLabel}  + } + bs5={ + + } + /> - + ) } diff --git a/services/web/frontend/js/features/project-list/components/dropdown/actions-dropdown.tsx b/services/web/frontend/js/features/project-list/components/dropdown/actions-dropdown.tsx index 854bf5a2b4..d9d7f6ba2e 100644 --- a/services/web/frontend/js/features/project-list/components/dropdown/actions-dropdown.tsx +++ b/services/web/frontend/js/features/project-list/components/dropdown/actions-dropdown.tsx @@ -318,7 +318,7 @@ function BS5ActionsDropdown({ project }: ActionDropdownProps) { as="button" tabIndex={-1} onClick={downloadProject} - leadingIcon="cloud_download" + leadingIcon="download" > {text} diff --git a/services/web/frontend/js/features/project-list/components/dropdown/projects-dropdown.tsx b/services/web/frontend/js/features/project-list/components/dropdown/projects-dropdown.tsx index 6da86e69cb..d2ce1181ac 100644 --- a/services/web/frontend/js/features/project-list/components/dropdown/projects-dropdown.tsx +++ b/services/web/frontend/js/features/project-list/components/dropdown/projects-dropdown.tsx @@ -1,41 +1,67 @@ import { useState, useEffect, useRef } from 'react' import { useTranslation } from 'react-i18next' -import { Dropdown, MenuItem } from 'react-bootstrap' +import { + Dropdown as BS3Dropdown, + MenuItem as BS3MenuItem, +} from 'react-bootstrap' import Icon from '../../../../shared/components/icon' import { Filter, UNCATEGORIZED_KEY, useProjectListContext, } from '../../context/project-list-context' +import { + Dropdown, + DropdownHeader, + DropdownItem, + DropdownMenu, + DropdownToggle, +} from '@/features/ui/components/bootstrap-5/dropdown-menu' import ProjectsFilterMenu from '../projects-filter-menu' import TagsList from '../tags-list' import MenuItemButton from './menu-item-button' +import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher' type ItemProps = { filter: Filter text: string - onClick: () => void + onClick?: () => void } export function Item({ filter, text, onClick }: ItemProps) { const { selectFilter } = useProjectListContext() const handleClick = () => { selectFilter(filter) - onClick() + onClick?.() } return ( {isActive => ( - - {isActive ? ( - - ) : null} - {text} - + + {isActive ? ( + + ) : null} + {text} + + } + bs5={ + + {text} + + } + /> )} ) @@ -72,37 +98,84 @@ function ProjectsDropdown() { }, [filter, tags, selectedTagId, t]) return ( - setIsOpened(open)} - > - - {title} - - - - - - - - - {t('tags')}: - - - + setIsOpened(open)} + > + + {title} + + + + + + + + + {t('tags')}: + + + + } + bs5={ + + + + {title} + + + +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • + + {t('tags')}: + + +
    +
    + } + /> ) } diff --git a/services/web/frontend/js/features/project-list/components/dropdown/sort-by-dropdown.tsx b/services/web/frontend/js/features/project-list/components/dropdown/sort-by-dropdown.tsx index dea7ad8b97..d7fd703edc 100644 --- a/services/web/frontend/js/features/project-list/components/dropdown/sort-by-dropdown.tsx +++ b/services/web/frontend/js/features/project-list/components/dropdown/sort-by-dropdown.tsx @@ -1,22 +1,47 @@ import { useState, useEffect, useRef } from 'react' import { useTranslation } from 'react-i18next' -import { Dropdown, MenuItem } from 'react-bootstrap' +import { + Dropdown as BS3Dropdown, + MenuItem as BS3MenuItem, +} from 'react-bootstrap' import Icon from '../../../../shared/components/icon' import useSort from '../../hooks/use-sort' import withContent, { SortBtnProps } from '../sort/with-content' import { useProjectListContext } from '../../context/project-list-context' import { Sort } from '../../../../../../types/project/dashboard/api' import MenuItemButton from './menu-item-button' +import { + Dropdown, + DropdownHeader, + DropdownItem, + DropdownMenu, + DropdownToggle, +} from '@/features/ui/components/bootstrap-5/dropdown-menu' +import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher' function Item({ onClick, text, iconType, screenReaderText }: SortBtnProps) { return ( - - {iconType ? ( - - ) : null} - {text} - {screenReaderText} - + + {iconType ? ( + + ) : null} + {text} + {screenReaderText} + + } + bs5={ + + {text} + + } + /> ) } @@ -39,45 +64,94 @@ function SortByDropdown() { setIsOpened(false) handleSort(by) } + const handleClickBS5 = (by: Sort['by']) => { + setTitle(sortByTranslations.current[by]) + handleSort(by) + } useEffect(() => { setTitle(sortByTranslations.current[sort.by]) }, [sort.by]) return ( - setIsOpened(open)} - > - - {title} - - - - {t('sort_by')}: - handleClick('title')} - /> - handleClick('owner')} - /> - handleClick('lastUpdated')} - /> - - + setIsOpened(open)} + > + + {title} + + + + {t('sort_by')}: + handleClick('title')} + /> + handleClick('owner')} + /> + handleClick('lastUpdated')} + /> + + + } + bs5={ + + + + {title} + + + + + {t('sort_by')}: + + handleClickBS5('title')} + /> + handleClickBS5('owner')} + /> + handleClickBS5('lastUpdated')} + /> + + + } + /> ) } 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 cf2eb62e97..99d3018560 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 @@ -18,9 +18,9 @@ import OLModal, { 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' +import Notification from '@/shared/components/notification' type CreateTagModalProps = { id: string @@ -104,10 +104,10 @@ export default function CreateTagModal({ {validationError && ( - + )} {isError && ( - 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 b66b2a5b8d..89f1d2db5e 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 @@ -11,7 +11,7 @@ import OLModal, { 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' +import Notification from '@/shared/components/notification' type DeleteTagModalProps = { id: string @@ -56,7 +56,7 @@ export default function DeleteTagModal({
  • {tag.name}
  • {isError && ( - 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 5d878a7b3a..cc59d31173 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 @@ -19,7 +19,7 @@ import OLForm from '@/features/ui/components/ol/ol-form' import OLFormGroup from '@/features/ui/components/ol/ol-form-group' import OLFormLabel from '@/features/ui/components/ol/ol-form-label' import OLButton from '@/features/ui/components/ol/ol-button' -import OLNotification from '@/features/ui/components/ol/ol-notification' +import Notification from '@/shared/components/notification' type EditTagModalProps = { id: string @@ -113,12 +113,12 @@ export function EditTagModal({ id, tag, onEdit, onClose }: EditTagModalProps) { {validationError && ( - + )} {isError && ( - )} diff --git a/services/web/frontend/js/features/project-list/components/modals/manage-tag-modal.tsx b/services/web/frontend/js/features/project-list/components/modals/manage-tag-modal.tsx index 26a2fe683a..777ed46c3e 100644 --- a/services/web/frontend/js/features/project-list/components/modals/manage-tag-modal.tsx +++ b/services/web/frontend/js/features/project-list/components/modals/manage-tag-modal.tsx @@ -1,7 +1,5 @@ import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' -import { Button, ControlLabel, Form, FormGroup, Modal } from 'react-bootstrap' -import AccessibleModal from '../../../../shared/components/accessible-modal' import useAsync from '../../../../shared/hooks/use-async' import { useRefWithAutoFocus } from '../../../../shared/hooks/use-ref-with-auto-focus' import useSelectColor from '../../hooks/use-select-color' @@ -10,6 +8,19 @@ import { Tag } from '../../../../../../app/src/Features/Tags/types' import { getTagColor } 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 BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher' +import Notification from '@/shared/components/notification' +import { bsVersion } from '@/features/utils/bootstrap-5' type ManageTagModalProps = { id: string @@ -78,14 +89,14 @@ export function ManageTagModal({ } return ( - - - {t('edit_tag')} - + + + {t('edit_tag')} + - -
    - + + + setNewTagName(e.target.value)} /> - - -
    -
    - - -
    -
    - -
    - - -
    + + {(isDeleteError || isRenameError) && ( -
    - - {t('generic_something_went_wrong')} - -
    + )} -
    -
    + + + + runDeleteTag(tag._id)} + className={bsVersion({ bs3: 'pull-left', bs5: 'me-auto' })} + disabled={isDeleteLoading || isUpdateLoading} + isLoading={isDeleteLoading} + bs3Props={{ + loading: isDeleteLoading ? `${t('deleting')}…` : t('delete_tag'), + }} + > + {t('delete_tag')} + + + {t('save_or_cancel-cancel')} + + runUpdateTag(tag._id)} + disabled={Boolean( + isUpdateLoading || + isDeleteLoading || + !newTagName?.length || + (newTagName === tag?.name && selectedColor === getTagColor(tag)) + )} + isLoading={isUpdateLoading} + bs3Props={{ + loading: isUpdateLoading + ? `${t('saving')}…` + : t('save_or_cancel-save'), + }} + > + {t('save_or_cancel-save')} + + } /> + + ) } diff --git a/services/web/frontend/js/features/project-list/components/project-list-root.tsx b/services/web/frontend/js/features/project-list/components/project-list-root.tsx index 28a160edd7..d56646b717 100644 --- a/services/web/frontend/js/features/project-list/components/project-list-root.tsx +++ b/services/web/frontend/js/features/project-list/components/project-list-root.tsx @@ -191,7 +191,11 @@ function ProjectListPageContent() { bsVersion({ bs5: 'd-md-none', bs3: 'visible-xs' }) )} > -
    +
    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 ( +
  • + +
  • + ) + })} +
  • +
  • - ) - })} -
  • -
  • - -
  • -
    -
    + + + + {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')} + - {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')} + + {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 && ( - - + ) + } +) +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 = ( - - ) - } +function DropdownItem( + { + active, + children, + description, + leadingIcon, + trailingIcon, + ...props + }: DropdownItemProps, + ref: React.ForwardedRef +) { + let leadingIconComponent = null + if (leadingIcon) { + if (typeof leadingIcon === 'string') { + leadingIconComponent = ( + + ) + } else { + leadingIconComponent = ( + + ) } - - let trailingIconComponent = null - if (trailingIcon) { - if (typeof trailingIcon === 'string') { - const trailingIconType = active ? 'check' : trailingIcon - - trailingIconComponent = ( - - ) - } else { - trailingIconComponent = ( - - ) - } - } - - 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 = ( + + ) + } + } + + 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' }) }) })