Merge pull request #19027 from overleaf/ii-bs5-projects-list-table

[web] BS5 projects table migration

GitOrigin-RevId: 237bd8113c68d7fd1b66712f7361eb956b1e10e7
This commit is contained in:
ilkin-overleaf 2024-07-10 16:34:19 +03:00 committed by Copybot
parent bac35566ff
commit c3ed95bc48
64 changed files with 1301 additions and 540 deletions

View file

@ -1224,7 +1224,6 @@
"shortcut_to_open_advanced_reference_search": "",
"show_all": "",
"show_all_projects": "",
"show_all_uppercase": "",
"show_document_preamble": "",
"show_hotkeys": "",
"show_in_code": "",
@ -1232,7 +1231,6 @@
"show_less": "",
"show_local_file_contents": "",
"show_outline": "",
"show_x_more": "",
"show_x_more_projects": "",
"showing_1_result": "",
"showing_1_result_of_total": "",

View file

@ -2,16 +2,20 @@
import PropTypes from 'prop-types'
import { useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import {
Modal,
Alert,
Button,
ControlLabel,
FormControl,
FormGroup,
} from 'react-bootstrap'
import { postJSON } from '../../../infrastructure/fetch-json'
import { CloneProjectTag } from './clone-project-tag'
import {
OLModalBody,
OLModalFooter,
OLModalHeader,
OLModalTitle,
} from '@/features/ui/components/ol/ol-modal'
import Notification from '@/shared/components/notification'
import OLForm from '@/features/ui/components/ol/ol-form'
import OLFormGroup from '@/features/ui/components/ol/ol-form-group'
import OLFormControl from '@/features/ui/components/ol/ol-form-control'
import OLFormLabel from '@/features/ui/components/ol/ol-form-label'
import OLButton from '@/features/ui/components/ol/ol-button'
export default function CloneProjectModalContent({
handleHide,
@ -77,19 +81,15 @@ export default function CloneProjectModalContent({
return (
<>
<Modal.Header closeButton>
<Modal.Title>{t('copy_project')}</Modal.Title>
</Modal.Header>
<OLModalHeader closeButton>
<OLModalTitle>{t('copy_project')}</OLModalTitle>
</OLModalHeader>
<Modal.Body>
<form id="clone-project-form" onSubmit={handleSubmit}>
<FormGroup>
<ControlLabel htmlFor="clone-project-form-name">
{t('new_name')}
</ControlLabel>
<FormControl
id="clone-project-form-name"
<OLModalBody>
<OLForm id="clone-project-form" onSubmit={handleSubmit}>
<OLFormGroup controlId="clone-project-form-name">
<OLFormLabel>{t('new_name')}</OLFormLabel>
<OLFormControl
type="text"
placeholder="New Project Name"
required
@ -97,13 +97,14 @@ export default function CloneProjectModalContent({
onChange={event => setClonedProjectName(event.target.value)}
autoFocus
/>
</FormGroup>
</OLFormGroup>
{clonedProjectTags.length > 0 && (
<FormGroup className="clone-project-tag">
<ControlLabel htmlFor="clone-project-tags-list">
{t('tags')}:{' '}
</ControlLabel>
<OLFormGroup
controlId="clone-project-tags-list"
className="clone-project-tag mb-3"
>
<OLFormLabel>{t('tags')}: </OLFormLabel>
<div role="listbox" id="clone-project-tags-list">
{clonedProjectTags.map(tag => (
<CloneProjectTag
@ -113,37 +114,31 @@ export default function CloneProjectModalContent({
/>
))}
</div>
</FormGroup>
</OLFormGroup>
)}
</form>
</OLForm>
{error && (
<Alert bsStyle="danger">
{error.length ? error : t('generic_something_went_wrong')}
</Alert>
<Notification
content={error.length ? error : t('generic_something_went_wrong')}
type="error"
/>
)}
</Modal.Body>
</OLModalBody>
<Modal.Footer>
<Button
type="button"
bsStyle={null}
className="btn-secondary"
disabled={inFlight}
onClick={handleHide}
>
<OLModalFooter>
<OLButton variant="secondary" disabled={inFlight} onClick={handleHide}>
{t('cancel')}
</Button>
<Button
</OLButton>
<OLButton
variant="primary"
disabled={inFlight || !valid}
form="clone-project-form"
type="submit"
bsStyle="primary"
disabled={inFlight || !valid}
>
{inFlight ? <>{t('copying')}</> : t('copy')}
</Button>
</Modal.Footer>
</OLButton>
</OLModalFooter>
</>
)
}

View file

@ -1,7 +1,7 @@
import React, { memo, useCallback, useState } from 'react'
import PropTypes from 'prop-types'
import CloneProjectModalContent from './clone-project-modal-content'
import AccessibleModal from '../../../shared/components/accessible-modal'
import OLModal from '@/features/ui/components/ol/ol-modal'
function CloneProjectModal({
show,
@ -20,7 +20,7 @@ function CloneProjectModal({
}, [handleHide, inFlight])
return (
<AccessibleModal
<OLModal
animation
show={show}
onHide={onHide}
@ -38,7 +38,7 @@ function CloneProjectModal({
projectName={projectName}
projectTags={projectTags}
/>
</AccessibleModal>
</OLModal>
)
}

View file

@ -1,5 +1,13 @@
import { useState, useCallback } from 'react'
import { Dropdown } from 'react-bootstrap'
import { useTranslation } from 'react-i18next'
import { Dropdown as BS3Dropdown } from 'react-bootstrap'
import { Spinner } from 'react-bootstrap-5'
import {
Dropdown,
DropdownItem,
DropdownMenu,
DropdownToggle,
} from '@/features/ui/components/bootstrap-5/dropdown-menu'
import MenuItemButton from './menu-item-button'
import Icon from '../../../../shared/components/icon'
import CopyProjectButton from '../table/cells/action-buttons/copy-project-button'
@ -13,10 +21,12 @@ import DeleteProjectButton from '../table/cells/action-buttons/delete-project-bu
import { Project } from '../../../../../../types/project/dashboard/api'
import CompileAndDownloadProjectPDFButton from '../table/cells/action-buttons/compile-and-download-project-pdf-button'
import RenameProjectButton from '../table/cells/action-buttons/rename-project-button'
import MaterialIcon from '@/shared/components/material-icon'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
type ActionButtonProps = {
project: Project
onClick: () => void // eslint-disable-line react/no-unused-prop-types
onClick: <T extends React.MouseEvent>(e?: T, fn?: (e?: T) => void) => void
}
function CopyProjectButtonMenuItem({ project, onClick }: ActionButtonProps) {
@ -24,7 +34,7 @@ function CopyProjectButtonMenuItem({ project, onClick }: ActionButtonProps) {
<CopyProjectButton project={project}>
{(text, handleOpenModal) => (
<MenuItemButton
onClick={() => handleOpenModal(onClick)}
onClick={e => handleOpenModal(e, onClick)}
className="projects-action-menu-item"
>
<Icon type="files-o" className="menu-item-button-icon" />{' '}
@ -43,7 +53,7 @@ function CompileAndDownloadProjectPDFButtonMenuItem({
<CompileAndDownloadProjectPDFButton project={project}>
{(text, pendingCompile, downloadProject) => (
<MenuItemButton
onClick={() => downloadProject(onClick)}
onClick={e => downloadProject(e, onClick)}
className="projects-action-menu-item"
>
{pendingCompile ? (
@ -219,7 +229,7 @@ type ActionDropdownProps = {
project: Project
}
function ActionsDropdown({ project }: ActionDropdownProps) {
export function BS3ActionsDropdown({ project }: ActionDropdownProps) {
const [isOpened, setIsOpened] = useState(false)
const handleClose = useCallback(() => {
@ -227,16 +237,16 @@ function ActionsDropdown({ project }: ActionDropdownProps) {
}, [setIsOpened])
return (
<Dropdown
<BS3Dropdown
id={`project-actions-dropdown-${project.id}`}
pullRight
open={isOpened}
onToggle={open => setIsOpened(open)}
>
<Dropdown.Toggle noCaret className="btn-transparent">
<BS3Dropdown.Toggle noCaret className="btn-transparent">
<Icon type="ellipsis-h" fw />
</Dropdown.Toggle>
<Dropdown.Menu className="projects-dropdown-menu text-left">
</BS3Dropdown.Toggle>
<BS3Dropdown.Menu className="projects-dropdown-menu text-left">
<RenameProjectButtonMenuItem project={project} onClick={handleClose} />
<CopyProjectButtonMenuItem project={project} onClick={handleClose} />
<DownloadProjectButtonMenuItem
@ -256,9 +266,181 @@ function ActionsDropdown({ project }: ActionDropdownProps) {
<UntrashProjectButtonMenuItem project={project} onClick={handleClose} />
<LeaveProjectButtonMenuItem project={project} onClick={handleClose} />
<DeleteProjectButtonMenuItem project={project} onClick={handleClose} />
</Dropdown.Menu>
</BS3Dropdown.Menu>
</BS3Dropdown>
)
}
function BS5ActionsDropdown({ project }: ActionDropdownProps) {
const { t } = useTranslation()
return (
<Dropdown align="end">
<DropdownToggle
id={`project-actions-dropdown-toggle-btn-${project.id}`}
bsPrefix="dropdown-table-button-toggle"
>
<MaterialIcon type="more_vert" accessibilityLabel={t('actions')} />
</DropdownToggle>
<DropdownMenu flip={false}>
<RenameProjectButton project={project}>
{(text, handleOpenModal) => (
<li role="none">
<DropdownItem
as="button"
tabIndex={-1}
onClick={handleOpenModal}
leadingIcon="edit"
>
{text}
</DropdownItem>
</li>
)}
</RenameProjectButton>
<CopyProjectButton project={project}>
{(text, handleOpenModal) => (
<li role="none">
<DropdownItem
as="button"
tabIndex={-1}
onClick={handleOpenModal}
leadingIcon="file_copy"
>
{text}
</DropdownItem>
</li>
)}
</CopyProjectButton>
<DownloadProjectButton project={project}>
{(text, downloadProject) => (
<li role="none">
<DropdownItem
as="button"
tabIndex={-1}
onClick={downloadProject}
leadingIcon="cloud_download"
>
{text}
</DropdownItem>
</li>
)}
</DownloadProjectButton>
<CompileAndDownloadProjectPDFButton project={project}>
{(text, pendingCompile, downloadProject) => (
<li role="none">
<DropdownItem
as="button"
tabIndex={-1}
onClick={e => {
e.stopPropagation()
downloadProject()
}}
leadingIcon={
pendingCompile ? (
<Spinner
animation="border"
aria-hidden="true"
as="span"
className="dropdown-item-leading-icon spinner"
size="sm"
role="status"
/>
) : (
'picture_as_pdf'
)
}
>
{text}
</DropdownItem>
</li>
)}
</CompileAndDownloadProjectPDFButton>
<ArchiveProjectButton project={project}>
{(text, handleOpenModal) => (
<li role="none">
<DropdownItem
as="button"
tabIndex={-1}
onClick={handleOpenModal}
leadingIcon="inbox"
>
{text}
</DropdownItem>
</li>
)}
</ArchiveProjectButton>
<TrashProjectButton project={project}>
{(text, handleOpenModal) => (
<li role="none">
<DropdownItem
as="button"
tabIndex={-1}
onClick={handleOpenModal}
leadingIcon="delete"
>
{text}
</DropdownItem>
</li>
)}
</TrashProjectButton>
<UnarchiveProjectButton project={project}>
{(text, unarchiveProject) => (
<li role="none">
<DropdownItem
as="button"
tabIndex={-1}
onClick={unarchiveProject}
leadingIcon="restore_page"
>
{text}
</DropdownItem>
</li>
)}
</UnarchiveProjectButton>
<UntrashProjectButton project={project}>
{(text, untrashProject) => (
<li role="none">
<DropdownItem
as="button"
tabIndex={-1}
onClick={untrashProject}
leadingIcon="restore_page"
>
{text}
</DropdownItem>
</li>
)}
</UntrashProjectButton>
<LeaveProjectButton project={project}>
{text => (
<li role="none">
<DropdownItem as="button" tabIndex={-1} leadingIcon="logout">
{text}
</DropdownItem>
</li>
)}
</LeaveProjectButton>
<DeleteProjectButton project={project}>
{text => (
<li role="none">
<DropdownItem as="button" tabIndex={-1} leadingIcon="block">
{text}
</DropdownItem>
</li>
)}
</DeleteProjectButton>
</DropdownMenu>
</Dropdown>
)
}
function ActionsDropdown({ project }: ActionDropdownProps) {
return (
<BootstrapVersionSwitcher
bs3={<BS3ActionsDropdown project={project} />}
bs5={<BS5ActionsDropdown project={project} />}
/>
)
}
export default ActionsDropdown

View file

@ -2,7 +2,7 @@ import { ReactNode } from 'react'
type MenuItemButtonProps = {
children: ReactNode
onClick?: (...args: unknown[]) => void
onClick?: (e?: React.MouseEvent) => void
className?: string
afterNode?: React.ReactNode
}

View file

@ -1,6 +1,6 @@
import { Button } from 'react-bootstrap'
import { useTranslation } from 'react-i18next'
import { useProjectListContext } from '../context/project-list-context'
import OLButton from '@/features/ui/components/ol/ol-button'
export default function LoadMore() {
const {
@ -13,16 +13,17 @@ export default function LoadMore() {
const { t } = useTranslation()
return (
<div className="text-centered">
<div className="text-center">
{hiddenProjectsCount > 0 ? (
<Button
bsStyle={null}
className="project-list-load-more-button btn-secondary-info btn-secondary"
onClick={() => loadMoreProjects()}
aria-label={t('show_x_more_projects', { x: loadMoreCount })}
>
{t('show_x_more', { x: loadMoreCount })}
</Button>
<>
<OLButton
variant="secondary"
className="project-list-load-more-button"
onClick={() => loadMoreProjects()}
>
{t('show_x_more_projects', { x: loadMoreCount })}
</OLButton>
</>
) : null}
<p>
{hiddenProjectsCount > 0 ? (
@ -33,15 +34,13 @@ export default function LoadMore() {
n: visibleProjects.length + hiddenProjectsCount,
})}
</span>{' '}
<button
type="button"
<OLButton
variant="link"
onClick={() => showAllProjects()}
style={{ padding: 0 }}
className="btn-link"
aria-label={t('show_all_projects')}
className="btn-inline-link"
>
{t('show_all_uppercase')}
</button>
{t('show_all_projects')}
</OLButton>
</>
) : (
<span aria-live="polite">

View file

@ -1,5 +1,4 @@
import { memo, useEffect, useState } from 'react'
import { Modal } from 'react-bootstrap'
import { useTranslation } from 'react-i18next'
import { Project } from '../../../../../../types/project/dashboard/api'
import { getUserFacingMessage } from '../../../../infrastructure/fetch-json'
@ -12,6 +11,7 @@ import OLModal, {
OLModalBody,
OLModalFooter,
OLModalHeader,
OLModalTitle,
} from '@/features/ui/components/ol/ol-modal'
type ProjectsActionModalProps = {
@ -80,7 +80,7 @@ function ProjectsActionModal({
backdrop="static"
>
<OLModalHeader closeButton>
<Modal.Title>{title}</Modal.Title>
<OLModalTitle>{title}</OLModalTitle>
</OLModalHeader>
<OLModalBody>
{children}

View file

@ -8,7 +8,7 @@ type ProjectsToDisplayProps = {
function ProjectsList({ projects, projectsToDisplay }: ProjectsToDisplayProps) {
return (
<ul className="projects-action-list">
<ul>
{projectsToDisplay.map(project => (
<li
key={`projects-action-list-${project.id}`}

View file

@ -1,13 +1,5 @@
import { memo, useCallback, useEffect, useMemo, useState } from 'react'
import {
Button,
ControlLabel,
FormControl,
FormGroup,
Modal,
} from 'react-bootstrap'
import { useTranslation } from 'react-i18next'
import AccessibleModal from '../../../../shared/components/accessible-modal'
import * as eventTracking from '../../../../infrastructure/event-tracking'
import { Project } from '../../../../../../types/project/dashboard/api'
import { renameProject } from '../../util/api'
@ -17,6 +9,17 @@ import { getUserFacingMessage } from '../../../../infrastructure/fetch-json'
import { debugConsole } from '@/utils/debugging'
import { isSmallDevice } from '../../../../infrastructure/event-tracking'
import Notification from '@/shared/components/notification'
import OLModal, {
OLModalBody,
OLModalFooter,
OLModalHeader,
OLModalTitle,
} from '@/features/ui/components/ol/ol-modal'
import OLButton from '@/features/ui/components/ol/ol-button'
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 OLFormControl from '@/features/ui/components/ol/ol-form-control'
type RenameProjectModalProps = {
handleCloseModal: () => void
@ -82,24 +85,22 @@ function RenameProjectModal({
]
)
const handleOnChange = (
event: React.ChangeEvent<HTMLFormElement & FormControl>
) => {
const handleOnChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setNewProjectName(event.target.value)
}
return (
<AccessibleModal
<OLModal
animation
show={showModal}
onHide={handleCloseModal}
id="rename-project-modal"
backdrop="static"
>
<Modal.Header closeButton>
<Modal.Title>{t('rename_project')}</Modal.Title>
</Modal.Header>
<Modal.Body>
<OLModalHeader closeButton>
<OLModalTitle>{t('rename_project')}</OLModalTitle>
</OLModalHeader>
<OLModalBody>
{isError && (
<div className="notification-list">
<Notification
@ -108,41 +109,33 @@ function RenameProjectModal({
/>
</div>
)}
<form id="rename-project-form" onSubmit={handleSubmit}>
<FormGroup>
<ControlLabel htmlFor="rename-project-form-name">
{t('new_name')}
</ControlLabel>
<FormControl
id="rename-project-form-name"
<OLForm id="rename-project-form" onSubmit={handleSubmit}>
<OLFormGroup controlId="rename-project-form-name">
<OLFormLabel>{t('new_name')}</OLFormLabel>
<OLFormControl
type="text"
placeholder={t('project_name')}
required
value={newProjectName}
onChange={handleOnChange}
/>
</FormGroup>
</form>
</Modal.Body>
<Modal.Footer>
<Button
bsStyle={null}
className="btn-secondary"
onClick={handleCloseModal}
>
</OLFormGroup>
</OLForm>
</OLModalBody>
<OLModalFooter>
<OLButton variant="secondary" onClick={handleCloseModal}>
{t('cancel')}
</Button>
<Button
form="rename-project-form"
bsStyle="primary"
disabled={isLoading || !isValid}
</OLButton>
<OLButton
variant="primary"
type="submit"
form="rename-project-form"
disabled={isLoading || !isValid}
>
{t('rename')}
</Button>
</Modal.Footer>
</AccessibleModal>
</OLButton>
</OLModalFooter>
</OLModal>
)
}

View file

@ -4,7 +4,6 @@ import {
} from '../context/project-list-context'
import { ColorPickerProvider } from '../context/color-picker-context'
import * as eventTracking from '../../../infrastructure/event-tracking'
import { Col, Row } from 'react-bootstrap'
import { useTranslation } from 'react-i18next'
import useWaitForI18n from '../../../shared/hooks/use-wait-for-i18n'
import CurrentPlanWidget from './current-plan-widget/current-plan-widget'
@ -30,6 +29,9 @@ import OLCol from '@/features/ui/components/ol/ol-col'
import { bsVersion } from '@/features/utils/bootstrap-5'
import classnames from 'classnames'
import Notification from '@/shared/components/notification'
import OLRow from '@/features/ui/components/ol/ol-row'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
import { TableContainer } from '@/features/ui/components/bootstrap-5/table'
function ProjectListRoot() {
const { isReady } = useWaitForI18n()
@ -75,6 +77,32 @@ function ProjectListPageContent() {
const { t } = useTranslation()
const tableTopArea = (
<div
className={classnames(
'pt-2',
'pb-3',
bsVersion({ bs5: 'd-md-none', bs3: 'visible-xs' })
)}
>
<div className="clearfix">
<NewProjectButton
id="new-project-button-projects-table"
className="pull-left me-2"
showAddAffiliationWidget
/>
<SearchForm
inputValue={searchText}
setInputValue={setSearchText}
filter={filter}
selectedTag={selectedTag}
className="overflow-hidden"
formGroupProps={{ className: 'mb-0' }}
/>
</div>
</div>
)
return isLoading ? (
<div className="loading-container">
<LoadingBranded loadProgress={loadProgress} label={t('loading')} />
@ -94,41 +122,62 @@ function ProjectListPageContent() {
<Sidebar />
<div className="project-list-main-react">
{error ? <DashApiError /> : ''}
<Row>
<Col xs={12}>
<OLRow>
<OLCol>
<UserNotifications />
</Col>
</Row>
</OLCol>
</OLRow>
<div className="project-list-header-row">
<ProjectListTitle
filter={filter}
selectedTag={selectedTag}
selectedTagId={selectedTagId}
className="hidden-xs text-truncate"
className={classnames(
'text-truncate',
bsVersion({
bs5: 'd-none d-md-block',
bs3: 'hidden-xs',
})
)}
/>
<div className="project-tools">
<div className="hidden-xs">
<div
className={bsVersion({
bs5: 'd-none d-md-block',
bs3: 'hidden-xs',
})}
>
{selectedProjects.length === 0 ? (
<CurrentPlanWidget />
) : (
<ProjectTools />
)}
</div>
<div className="visible-xs">
<div
className={bsVersion({
bs5: 'd-md-none',
bs3: 'visible-xs',
})}
>
<CurrentPlanWidget />
</div>
</div>
</div>
<Row className="hidden-xs">
<Col md={7}>
<OLRow
className={bsVersion({
bs5: 'd-none d-md-block',
bs3: 'hidden-xs',
})}
>
<OLCol lg={7}>
<SearchForm
inputValue={searchText}
setInputValue={setSearchText}
filter={filter}
selectedTag={selectedTag}
/>
</Col>
</Row>
</OLCol>
</OLRow>
<div
className={classnames(
'project-list-sidebar-survey-wrapper',
@ -137,60 +186,59 @@ function ProjectListPageContent() {
>
<SurveyWidget />
</div>
<div className="visible-xs mt-1">
<div
className={classnames(
'mt-1',
bsVersion({ bs5: 'd-md-none', bs3: 'visible-xs' })
)}
>
<div role="toolbar" className="projects-toolbar">
<ProjectsDropdown />
<SortByDropdown />
</div>
</div>
<Row className="row-spaced">
<Col xs={12}>
<div className="card project-list-card">
<div className="visible-xs pt-2 pb-3">
<div className="clearfix">
<NewProjectButton
id="new-project-button-projects-table"
className="pull-left me-2"
showAddAffiliationWidget
/>
<SearchForm
inputValue={searchText}
setInputValue={setSearchText}
filter={filter}
selectedTag={selectedTag}
className="overflow-hidden"
formGroupProps={{ className: 'mb-0' }}
/>
<OLRow className="row-spaced">
<OLCol>
<BootstrapVersionSwitcher
bs3={
<div className="card project-list-card">
{tableTopArea}
<ProjectListTable />
</div>
</div>
<ProjectListTable />
</div>
</Col>
</Row>
<Row className="row-spaced">
<Col xs={12}>
}
bs5={
<TableContainer bordered>
{tableTopArea}
<ProjectListTable />
</TableContainer>
}
/>
</OLCol>
</OLRow>
<OLRow className="row-spaced">
<OLCol>
<LoadMore />
</Col>
</Row>
</OLCol>
</OLRow>
</div>
</>
) : (
<div className="project-list-welcome-wrapper">
{error ? <DashApiError /> : ''}
<Row className="row-spaced mx-0">
<OLRow className="row-spaced mx-0">
<OLCol
md={{ span: 10, offset: 1 }}
lg={{ span: 8, offset: 2 }}
className="project-list-empty-col"
>
<Row>
<OLRow>
<OLCol>
<UserNotifications />
</OLCol>
</Row>
</OLRow>
<WelcomeMessage />
</OLCol>
</Row>
</OLRow>
</div>
)}
</div>
@ -201,7 +249,7 @@ function ProjectListPageContent() {
function DashApiError() {
const { t } = useTranslation()
return (
<Row className="row-spaced">
<OLRow className="row-spaced">
<OLCol
xs={{ span: 8, offset: 2 }}
bs3Props={{ xs: 8, xsOffset: 2 }}
@ -214,7 +262,7 @@ function DashApiError() {
/>
</div>
</OLCol>
</Row>
</OLRow>
)
}

View file

@ -1,5 +1,6 @@
import { useTranslation } from 'react-i18next'
import { Sort } from '../../../../../../types/project/dashboard/api'
import { bsVersion } from '@/features/utils/bootstrap-5'
type SortBtnOwnProps = {
column: string
@ -26,7 +27,10 @@ function withContent<T extends SortBtnOwnProps>(
let screenReaderText = t('sort_by_x', { x: text })
if (column === sort.by) {
iconType = sort.order === 'asc' ? 'caret-up' : 'caret-down'
iconType =
sort.order === 'asc'
? bsVersion({ bs5: 'arrow_upward_alt', bs3: 'caret-up' })
: bsVersion({ bs5: 'arrow_downward_alt', bs3: 'caret-down' })
screenReaderText = t('reverse_x_sort_order', { x: text })
}

View file

@ -1,12 +1,13 @@
import { useTranslation } from 'react-i18next'
import { Project } from '../../../../../../../../types/project/dashboard/api'
import { memo, useCallback, useState } from 'react'
import Icon from '../../../../../../shared/components/icon'
import Tooltip from '../../../../../../shared/components/tooltip'
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 OLTooltip from '@/features/ui/components/ol/ol-tooltip'
import OLIconButton from '@/features/ui/components/ol/ol-icon-button'
import { bsVersion } from '@/features/utils/bootstrap-5'
type ArchiveProjectButtonProps = {
project: Project
@ -65,20 +66,28 @@ const ArchiveProjectButtonTooltip = memo(function ArchiveProjectButtonTooltip({
return (
<ArchiveProjectButton project={project}>
{(text, handleOpenModal) => (
<Tooltip
<OLTooltip
key={`tooltip-archive-project-${project.id}`}
id={`archive-project-${project.id}`}
description={text}
overlayProps={{ placement: 'top', trigger: ['hover', 'focus'] }}
>
<button
className="btn btn-link action-btn"
aria-label={text}
onClick={handleOpenModal}
>
<Icon type="inbox" fw />
</button>
</Tooltip>
<span>
<OLIconButton
onClick={handleOpenModal}
variant="link"
accessibilityLabel={text}
className="action-btn"
icon={
bsVersion({
bs5: 'inbox',
bs3: 'inbox',
}) as string
}
bs3Props={{ fw: true }}
/>
</span>
</OLTooltip>
)}
</ArchiveProjectButton>
)

View file

@ -2,21 +2,31 @@ import { useTranslation } from 'react-i18next'
import { memo, useCallback, useState } from 'react'
import { Project } from '../../../../../../../../types/project/dashboard/api'
import Icon from '../../../../../../shared/components/icon'
import Tooltip from '../../../../../../shared/components/tooltip'
import * as eventTracking from '../../../../../../infrastructure/event-tracking'
import { useLocation } from '../../../../../../shared/hooks/use-location'
import useAbortController from '../../../../../../shared/hooks/use-abort-controller'
import { postJSON } from '../../../../../../infrastructure/fetch-json'
import AccessibleModal from '../../../../../../shared/components/accessible-modal'
import { Button, Modal } from 'react-bootstrap'
import { isSmallDevice } from '../../../../../../infrastructure/event-tracking'
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
import OLButton from '@/features/ui/components/ol/ol-button'
import OLModal, {
OLModalBody,
OLModalFooter,
OLModalHeader,
OLModalTitle,
} from '@/features/ui/components/ol/ol-modal'
import { bsVersion } from '@/features/utils/bootstrap-5'
import OLIconButton from '@/features/ui/components/ol/ol-icon-button'
type CompileAndDownloadProjectPDFButtonProps = {
project: Project
children: (
text: string,
pendingDownload: boolean,
downloadProject: (fn: () => void) => void
downloadProject: <T extends React.MouseEvent>(
e?: T,
fn?: (e?: T) => void
) => void
) => React.ReactElement
}
@ -31,7 +41,7 @@ function CompileAndDownloadProjectPDFButton({
const [pendingCompile, setPendingCompile] = useState(false)
const downloadProject = useCallback(
onDone => {
<T extends React.MouseEvent>(e?: T, onDone?: (e?: T) => void) => {
setPendingCompile(pendingCompile => {
if (pendingCompile) return true
eventTracking.sendMB('project-list-page-interaction', {
@ -72,7 +82,7 @@ function CompileAndDownloadProjectPDFButton({
location.assign(
`/download/project/${project.id}/build/${outputFile.build}/output/output.pdf?${params}`
)
onDone()
onDone?.(e)
} else {
setShowErrorModal(true)
}
@ -111,19 +121,19 @@ function CompileErrorModal({
const { t } = useTranslation()
return (
<>
<AccessibleModal show onHide={handleClose}>
<Modal.Header closeButton>
<Modal.Title>
<OLModal show onHide={handleClose}>
<OLModalHeader closeButton>
<OLModalTitle>
{project.name}: {t('pdf_unavailable_for_download')}
</Modal.Title>
</Modal.Header>
<Modal.Body>{t('generic_linked_file_compile_error')}</Modal.Body>
<Modal.Footer>
<a href={`/project/${project.id}`}>
<Button bsStyle="primary">{t('open_project')}</Button>
</a>
</Modal.Footer>
</AccessibleModal>
</OLModalTitle>
</OLModalHeader>
<OLModalBody>{t('generic_linked_file_compile_error')}</OLModalBody>
<OLModalFooter>
<OLButton variant="primary" href={`/project/${project.id}`}>
{t('open_project')}
</OLButton>
</OLModalFooter>
</OLModal>
</>
)
}
@ -135,24 +145,35 @@ const CompileAndDownloadProjectPDFButtonTooltip = memo(
return (
<CompileAndDownloadProjectPDFButton project={project}>
{(text, pendingCompile, compileAndDownloadProject) => (
<Tooltip
<OLTooltip
key={`tooltip-compile-and-download-project-${project.id}`}
id={`compile-and-download-project-${project.id}`}
description={text}
overlayProps={{ placement: 'top', trigger: ['hover', 'focus'] }}
>
<button
className="btn btn-link action-btn"
aria-label={text}
onClick={() => compileAndDownloadProject(() => {})}
>
{pendingCompile ? (
<Icon type="spinner" spin />
) : (
<Icon type="file-pdf-o" />
)}
</button>
</Tooltip>
<span>
<OLIconButton
onClick={compileAndDownloadProject}
variant="link"
accessibilityLabel={text}
loadingLabel={text}
isLoading={pendingCompile}
className="action-btn"
icon={
bsVersion({
bs5: 'picture_as_pdf',
bs3: 'file-pdf-o',
}) as string
}
bs3Props={{
fw: true,
loading: pendingCompile ? (
<Icon type="spinner" fw accessibilityLabel={text} spin />
) : null,
}}
/>
</span>
</OLTooltip>
)}
</CompileAndDownloadProjectPDFButton>
)

View file

@ -1,7 +1,5 @@
import { memo, useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Icon from '../../../../../../shared/components/icon'
import Tooltip from '../../../../../../shared/components/tooltip'
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'
@ -12,14 +10,18 @@ import {
} from '../../../../../../../../types/project/dashboard/api'
import { useProjectTags } from '@/features/project-list/hooks/use-project-tags'
import { isSmallDevice } from '../../../../../../infrastructure/event-tracking'
type HandleOpenModal = (fn?: () => void) => void
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
import OLIconButton from '@/features/ui/components/ol/ol-icon-button'
import { bsVersion } from '@/features/utils/bootstrap-5'
type CopyButtonProps = {
project: Project
children: (
text: string,
handleOpenModal: HandleOpenModal
handleOpenModal: <T extends React.MouseEvent>(
e?: T,
fn?: (e?: T) => void
) => void
) => React.ReactElement
}
@ -37,9 +39,9 @@ function CopyProjectButton({ project, children }: CopyButtonProps) {
const projectTags = useProjectTags(project.id)
const handleOpenModal = useCallback(
(onOpen?: Parameters<HandleOpenModal>[0]) => {
<T extends React.MouseEvent>(e?: T, onOpen?: (e?: T) => void) => {
setShowModal(true)
onOpen?.()
onOpen?.(e)
},
[]
)
@ -97,20 +99,28 @@ const CopyProjectButtonTooltip = memo(function CopyProjectButtonTooltip({
return (
<CopyProjectButton project={project}>
{(text, handleOpenModal) => (
<Tooltip
<OLTooltip
key={`tooltip-copy-project-${project.id}`}
id={`copy-project-${project.id}`}
description={text}
overlayProps={{ placement: 'top', trigger: ['hover', 'focus'] }}
>
<button
className="btn btn-link action-btn"
aria-label={text}
onClick={() => handleOpenModal()}
>
<Icon type="files-o" fw />
</button>
</Tooltip>
<span>
<OLIconButton
onClick={handleOpenModal}
variant="link"
accessibilityLabel={text}
className="action-btn"
icon={
bsVersion({
bs5: 'file_copy',
bs3: 'files-o',
}) as string
}
bs3Props={{ fw: true }}
/>
</span>
</OLTooltip>
)}
</CopyProjectButton>
)

View file

@ -1,13 +1,14 @@
import { memo, useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Project } from '../../../../../../../../types/project/dashboard/api'
import Icon from '../../../../../../shared/components/icon'
import Tooltip from '../../../../../../shared/components/tooltip'
import DeleteProjectModal from '../../../modals/delete-project-modal'
import useIsMounted from '../../../../../../shared/hooks/use-is-mounted'
import { deleteProject } from '../../../../util/api'
import { useProjectListContext } from '../../../../context/project-list-context'
import getMeta from '@/utils/meta'
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
import OLIconButton from '@/features/ui/components/ol/ol-icon-button'
import { bsVersion } from '@/features/utils/bootstrap-5'
type DeleteProjectButtonProps = {
project: Project
@ -63,20 +64,28 @@ const DeleteProjectButtonTooltip = memo(function DeleteProjectButtonTooltip({
return (
<DeleteProjectButton project={project}>
{(text, handleOpenModal) => (
<Tooltip
<OLTooltip
key={`tooltip-delete-project-${project.id}`}
id={`delete-project-${project.id}`}
description={text}
overlayProps={{ placement: 'top', trigger: ['hover', 'focus'] }}
>
<button
className="btn btn-link action-btn"
aria-label={text}
onClick={handleOpenModal}
>
<Icon type="ban" fw />
</button>
</Tooltip>
<span>
<OLIconButton
onClick={handleOpenModal}
variant="link"
accessibilityLabel={text}
className="action-btn"
icon={
bsVersion({
bs5: 'block',
bs3: 'ban',
}) as string
}
bs3Props={{ fw: true }}
/>
</span>
</OLTooltip>
)}
</DeleteProjectButton>
)

View file

@ -1,11 +1,12 @@
import { useTranslation } from 'react-i18next'
import { memo, useCallback } from 'react'
import { Project } from '../../../../../../../../types/project/dashboard/api'
import Icon from '../../../../../../shared/components/icon'
import Tooltip from '../../../../../../shared/components/tooltip'
import * as eventTracking from '../../../../../../infrastructure/event-tracking'
import { useLocation } from '../../../../../../shared/hooks/use-location'
import { isSmallDevice } from '../../../../../../infrastructure/event-tracking'
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
import OLIconButton from '@/features/ui/components/ol/ol-icon-button'
import { bsVersion } from '@/features/utils/bootstrap-5'
type DownloadProjectButtonProps = {
project: Project
@ -39,20 +40,28 @@ const DownloadProjectButtonTooltip = memo(
return (
<DownloadProjectButton project={project}>
{(text, downloadProject) => (
<Tooltip
<OLTooltip
key={`tooltip-download-project-${project.id}`}
id={`download-project-${project.id}`}
description={text}
overlayProps={{ placement: 'top', trigger: ['hover', 'focus'] }}
>
<button
className="btn btn-link action-btn"
aria-label={text}
onClick={downloadProject}
>
<Icon type="cloud-download" fw />
</button>
</Tooltip>
<span>
<OLIconButton
onClick={downloadProject}
variant="link"
accessibilityLabel={text}
className="action-btn"
icon={
bsVersion({
bs5: 'cloud_download',
bs3: 'cloud-download',
}) as string
}
bs3Props={{ fw: true }}
/>
</span>
</OLTooltip>
)}
</DownloadProjectButton>
)

View file

@ -1,13 +1,14 @@
import { memo, useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Icon from '../../../../../../shared/components/icon'
import Tooltip from '../../../../../../shared/components/tooltip'
import LeaveProjectModal from '../../../modals/leave-project-modal'
import { useProjectListContext } from '../../../../context/project-list-context'
import useIsMounted from '../../../../../../shared/hooks/use-is-mounted'
import { leaveProject } from '../../../../util/api'
import { Project } from '../../../../../../../../types/project/dashboard/api'
import getMeta from '@/utils/meta'
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
import OLIconButton from '@/features/ui/components/ol/ol-icon-button'
import { bsVersion } from '@/features/utils/bootstrap-5'
type LeaveProjectButtonProps = {
project: Project
@ -62,20 +63,28 @@ const LeaveProjectButtonTooltip = memo(function LeaveProjectButtonTooltip({
return (
<LeaveProjectButton project={project}>
{(text, handleOpenModal) => (
<Tooltip
<OLTooltip
key={`tooltip-leave-project-${project.id}`}
id={`leave-project-${project.id}`}
description={text}
overlayProps={{ placement: 'top', trigger: ['hover', 'focus'] }}
>
<button
className="btn btn-link action-btn"
aria-label={text}
onClick={handleOpenModal}
>
<Icon type="sign-out" fw />
</button>
</Tooltip>
<span>
<OLIconButton
onClick={handleOpenModal}
variant="link"
accessibilityLabel={text}
className="action-btn"
icon={
bsVersion({
bs5: 'logout',
bs3: 'sign-out',
}) as string
}
bs3Props={{ fw: true }}
/>
</span>
</OLTooltip>
)}
</LeaveProjectButton>
)

View file

@ -1,12 +1,13 @@
import { useTranslation } from 'react-i18next'
import { memo, useCallback, useState } from 'react'
import { Project } from '../../../../../../../../types/project/dashboard/api'
import Icon from '../../../../../../shared/components/icon'
import Tooltip from '../../../../../../shared/components/tooltip'
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 OLTooltip from '@/features/ui/components/ol/ol-tooltip'
import OLIconButton from '@/features/ui/components/ol/ol-icon-button'
import { bsVersion } from '@/features/utils/bootstrap-5'
type TrashProjectButtonProps = {
project: Project
@ -62,20 +63,28 @@ const TrashProjectButtonTooltip = memo(function TrashProjectButtonTooltip({
return (
<TrashProjectButton project={project}>
{(text, handleOpenModal) => (
<Tooltip
<OLTooltip
key={`tooltip-trash-project-${project.id}`}
id={`trash-project-${project.id}`}
description={text}
overlayProps={{ placement: 'top', trigger: ['hover', 'focus'] }}
>
<button
className="btn btn-link action-btn"
aria-label={text}
onClick={handleOpenModal}
>
<Icon type="trash" fw />
</button>
</Tooltip>
<span>
<OLIconButton
onClick={handleOpenModal}
variant="link"
accessibilityLabel={text}
className="action-btn"
icon={
bsVersion({
bs5: 'delete',
bs3: 'trash',
}) as string
}
bs3Props={{ fw: true }}
/>
</span>
</OLTooltip>
)}
</TrashProjectButton>
)

View file

@ -1,10 +1,11 @@
import { useTranslation } from 'react-i18next'
import { memo, useCallback } from 'react'
import { Project } from '../../../../../../../../types/project/dashboard/api'
import Icon from '../../../../../../shared/components/icon'
import Tooltip from '../../../../../../shared/components/tooltip'
import { useProjectListContext } from '../../../../context/project-list-context'
import { unarchiveProject } from '../../../../util/api'
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
import OLIconButton from '@/features/ui/components/ol/ol-icon-button'
import { bsVersion } from '@/features/utils/bootstrap-5'
type UnarchiveProjectButtonProps = {
project: Project
@ -41,20 +42,28 @@ const UnarchiveProjectButtonTooltip = memo(
return (
<UnarchiveProjectButton project={project}>
{(text, handleUnarchiveProject) => (
<Tooltip
<OLTooltip
key={`tooltip-unarchive-project-${project.id}`}
id={`unarchive-project-${project.id}`}
description={text}
overlayProps={{ placement: 'top', trigger: ['hover', 'focus'] }}
>
<button
className="btn btn-link action-btn"
aria-label={text}
onClick={handleUnarchiveProject}
>
<Icon type="reply" fw />
</button>
</Tooltip>
<span>
<OLIconButton
onClick={handleUnarchiveProject}
variant="link"
accessibilityLabel={text}
className="action-btn"
icon={
bsVersion({
bs5: 'restore_page',
bs3: 'reply',
}) as string
}
bs3Props={{ fw: true }}
/>
</span>
</OLTooltip>
)}
</UnarchiveProjectButton>
)

View file

@ -1,10 +1,11 @@
import { useTranslation } from 'react-i18next'
import { memo, useCallback } from 'react'
import { Project } from '../../../../../../../../types/project/dashboard/api'
import Icon from '../../../../../../shared/components/icon'
import Tooltip from '../../../../../../shared/components/tooltip'
import { useProjectListContext } from '../../../../context/project-list-context'
import { untrashProject } from '../../../../util/api'
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
import OLIconButton from '@/features/ui/components/ol/ol-icon-button'
import { bsVersion } from '@/features/utils/bootstrap-5'
type UntrashProjectButtonProps = {
project: Project
@ -40,20 +41,28 @@ const UntrashProjectButtonTooltip = memo(function UntrashProjectButtonTooltip({
return (
<UntrashProjectButton project={project}>
{(text, handleUntrashProject) => (
<Tooltip
<OLTooltip
key={`tooltip-untrash-project-${project.id}`}
id={`untrash-project-${project.id}`}
description={text}
overlayProps={{ placement: 'top', trigger: ['hover', 'focus'] }}
>
<button
className="btn btn-link action-btn"
aria-label={text}
onClick={handleUntrashProject}
>
<Icon type="reply" fw />
</button>
</Tooltip>
<span>
<OLIconButton
onClick={handleUntrashProject}
variant="link"
accessibilityLabel={text}
className="action-btn"
icon={
bsVersion({
bs5: 'restore_page',
bs3: 'reply',
}) as string
}
bs3Props={{ fw: true }}
/>
</span>
</OLTooltip>
)}
</UntrashProjectButton>
)

View file

@ -1,7 +1,7 @@
import { formatDate, fromNowDate } from '../../../../../utils/dates'
import { Project } from '../../../../../../../types/project/dashboard/api'
import Tooltip from '../../../../../shared/components/tooltip'
import { LastUpdatedBy } from '@/features/project-list/components/table/cells/last-updated-by'
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
type LastUpdatedCellProps = {
project: Project
@ -12,7 +12,7 @@ export default function LastUpdatedCell({ project }: LastUpdatedCellProps) {
const tooltipText = formatDate(project.lastUpdated)
return (
<Tooltip
<OLTooltip
key={`tooltip-last-updated-${project.id}`}
id={`tooltip-last-updated-${project.id}`}
description={tooltipText}
@ -28,6 +28,6 @@ export default function LastUpdatedCell({ project }: LastUpdatedCellProps) {
) : (
<span>{lastUpdatedDate}</span>
)}
</Tooltip>
</OLTooltip>
)
}

View file

@ -1,8 +1,8 @@
import { useTranslation } from 'react-i18next'
import Icon from '../../../../../shared/components/icon'
import Tooltip from '../../../../../shared/components/tooltip'
import { getOwnerName } from '../../../util/project'
import { Project } from '../../../../../../../types/project/dashboard/api'
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
type LinkSharingIconProps = {
prependSpace: boolean
@ -17,7 +17,7 @@ function LinkSharingIcon({
}: LinkSharingIconProps) {
const { t } = useTranslation()
return (
<Tooltip
<OLTooltip
key={`tooltip-link-sharing-${project.id}`}
id={`tooltip-link-sharing-${project.id}`}
description={t('link_sharing')}
@ -32,7 +32,7 @@ function LinkSharingIcon({
accessibilityLabel={t('link_sharing')}
/>
</span>
</Tooltip>
</OLTooltip>
)
}

View file

@ -1,26 +1,32 @@
import { memo, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { useProjectListContext } from '@/features/project-list/context/project-list-context'
import OLFormCheckbox from '@/features/ui/components/ol/ol-form-checkbox'
export const ProjectCheckbox = memo<{ projectId: string }>(({ projectId }) => {
const { selectedProjectIds, toggleSelectedProject } = useProjectListContext()
export const ProjectCheckbox = memo<{ projectId: string; projectName: string }>(
({ projectId, projectName }) => {
const { t } = useTranslation()
const { selectedProjectIds, toggleSelectedProject } =
useProjectListContext()
const handleCheckboxChange = useCallback(
event => {
toggleSelectedProject(projectId, event.target.checked)
},
[projectId, toggleSelectedProject]
)
const handleCheckboxChange = useCallback(
event => {
toggleSelectedProject(projectId, event.target.checked)
},
[projectId, toggleSelectedProject]
)
return (
<input
type="checkbox"
id={`select-project-${projectId}`}
autoComplete="off"
checked={selectedProjectIds.has(projectId)}
onChange={handleCheckboxChange}
data-project-id={projectId}
/>
)
})
return (
<OLFormCheckbox
autoComplete="off"
onChange={handleCheckboxChange}
checked={selectedProjectIds.has(projectId)}
aria-label={t('select_project', { project: projectName })}
data-project-id={projectId}
bs3Props={{ bsClass: 'dash-cell-checkbox-wrapper' }}
/>
)
}
)
ProjectCheckbox.displayName = 'ProjectCheckbox'

View file

@ -1,5 +1,4 @@
import { memo } from 'react'
import { useTranslation } from 'react-i18next'
import InlineTags from './cells/inline-tags'
import OwnerCell from './cells/owner-cell'
import LastUpdatedCell from './cells/last-updated-cell'
@ -9,45 +8,90 @@ import { getOwnerName } from '../../util/project'
import { Project } from '../../../../../../types/project/dashboard/api'
import { ProjectCheckbox } from './project-checkbox'
import { ProjectListOwnerName } from '@/features/project-list/components/table/project-list-owner-name'
import { bsVersion } from '@/features/utils/bootstrap-5'
import classnames from 'classnames'
type ProjectListTableRowProps = {
project: Project
selected: boolean
}
function ProjectListTableRow({ project }: ProjectListTableRowProps) {
const { t } = useTranslation()
function ProjectListTableRow({ project, selected }: ProjectListTableRowProps) {
const ownerName = getOwnerName(project)
return (
<tr>
<td className="dash-cell-checkbox hidden-xs">
<ProjectCheckbox projectId={project.id} />
<label htmlFor={`select-project-${project.id}`} className="sr-only">
{t('select_project', { project: project.name })}
</label>
<tr className={selected ? bsVersion({ bs5: 'table-active' }) : undefined}>
<td
className={classnames(
'dash-cell-checkbox',
bsVersion({
bs5: 'd-none d-md-table-cell',
bs3: 'hidden-xs',
})
)}
>
<ProjectCheckbox projectId={project.id} projectName={project.name} />
</td>
<td className="dash-cell-name">
<a href={`/project/${project.id}`}>{project.name}</a>{' '}
<InlineTags className="hidden-xs" projectId={project.id} />
<InlineTags
className={bsVersion({
bs5: 'd-none d-md-inline',
bs3: 'hidden-xs',
})}
projectId={project.id}
/>
</td>
<td className="dash-cell-date-owner visible-xs pb-0">
<td
className={classnames(
'dash-cell-date-owner',
'pb-0',
bsVersion({ bs5: 'd-md-none', bs3: 'visible-xs' })
)}
>
<LastUpdatedCell project={project} />
{ownerName ? <ProjectListOwnerName ownerName={ownerName} /> : null}
</td>
<td className="dash-cell-owner hidden-xs">
<td
className={classnames(
'dash-cell-owner',
bsVersion({
bs5: 'd-none d-md-table-cell',
bs3: 'hidden-xs',
})
)}
>
<OwnerCell project={project} />
</td>
<td className="dash-cell-date hidden-xs">
<td
className={classnames(
'dash-cell-date',
bsVersion({
bs5: 'd-none d-md-table-cell',
bs3: 'hidden-xs',
})
)}
>
<LastUpdatedCell project={project} />
</td>
<td className="dash-cell-tag visible-xs pt-0">
<td
className={classnames(
'dash-cell-tag',
'pt-0',
bsVersion({ bs5: 'd-md-none', bs3: 'visible-xs' })
)}
>
<InlineTags projectId={project.id} />
</td>
<td className="dash-cell-actions">
<div className="hidden-xs">
<div
className={bsVersion({
bs5: 'd-none d-md-block',
bs3: 'hidden-xs',
})}
>
<ActionsCell project={project} />
</div>
<div className="visible-xs">
<div className={bsVersion({ bs5: 'd-md-none', bs3: 'visible-xs' })}>
<ActionsDropdown project={project} />
</div>
</td>

View file

@ -1,21 +1,37 @@
import { useCallback } from 'react'
import { useCallback, useRef, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import Icon from '../../../../shared/components/icon'
import ProjectListTableRow from './project-list-table-row'
import { useProjectListContext } from '../../context/project-list-context'
import useSort from '../../hooks/use-sort'
import withContent, { SortBtnProps } from '../sort/with-content'
import { Project } from '../../../../../../types/project/dashboard/api'
import OLTable from '@/features/ui/components/ol/ol-table'
import OLFormCheckbox from '@/features/ui/components/ol/ol-form-checkbox'
import MaterialIcon from '@/shared/components/material-icon'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
import { bsVersion } from '@/features/utils/bootstrap-5'
import classnames from 'classnames'
function SortBtn({ onClick, text, iconType, screenReaderText }: SortBtnProps) {
return (
<button
className="btn-link table-header-sort-btn hidden-xs"
className={classnames(
'table-header-sort-btn',
bsVersion({
bs5: 'd-none d-md-inline-block',
bs3: 'hidden-xs',
})
)}
onClick={onClick}
aria-label={screenReaderText}
>
<span className="tablesort-text">{text}</span>
{iconType && <Icon type={iconType} />}
<span className={bsVersion({ bs3: 'tablesort-text' })}>{text}</span>
{iconType && (
<BootstrapVersionSwitcher
bs3={<Icon type={iconType} />}
bs5={<MaterialIcon type={iconType} />}
/>
)}
</button>
)
}
@ -31,6 +47,7 @@ function ProjectListTable() {
selectOrUnselectAllProjects,
} = useProjectListContext()
const { handleSort } = useSort()
const checkAllRef = useRef<HTMLInputElement>()
const handleAllProjectsCheckboxChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
@ -39,18 +56,39 @@ function ProjectListTable() {
[selectOrUnselectAllProjects]
)
useEffect(() => {
if (checkAllRef.current) {
checkAllRef.current.indeterminate =
selectedProjects.length > 0 &&
selectedProjects.length !== visibleProjects.length
}
}, [selectedProjects, visibleProjects])
return (
<table className="project-dash-table">
<caption className="sr-only">{t('projects_list')}</caption>
<thead className="sr-only-xs">
<OLTable className="project-dash-table" container={false} hover>
<caption
className={bsVersion({ bs5: 'visually-hidden', bs3: 'sr-only' })}
>
{t('projects_list')}
</caption>
<thead
className={bsVersion({
bs5: 'visually-hidden-max-md',
bs3: 'sr-only-xs',
})}
>
<tr>
<th
className="dash-cell-checkbox hidden-xs"
className={classnames(
'dash-cell-checkbox',
bsVersion({
bs5: 'd-none d-md-table-cell',
bs3: 'hidden-xs',
})
)}
aria-label={t('select_projects')}
>
<input
type="checkbox"
id="project-list-table-select-all"
<OLFormCheckbox
autoComplete="off"
onChange={handleAllProjectsCheckboxChange}
checked={
@ -58,10 +96,13 @@ function ProjectListTable() {
visibleProjects.length !== 0
}
disabled={visibleProjects.length === 0}
aria-label={t('select_all_projects')}
bs3Props={{
bsClass: 'dash-cell-checkbox-wrapper',
inputRef: undefined,
}}
inputRef={checkAllRef}
/>
<label htmlFor="project-list-table-select-all" className="sr-only">
{t('select_all_projects')}
</label>
</th>
<th
className="dash-cell-name"
@ -82,13 +123,22 @@ function ProjectListTable() {
/>
</th>
<th
className="dash-cell-date-owner visible-xs"
className={classnames(
'dash-cell-date-owner',
bsVersion({ bs5: 'd-md-none', bs3: 'visible-xs' })
)}
aria-label={t('date_and_owner')}
>
{t('date_and_owner')}
</th>
<th
className="dash-cell-owner hidden-xs"
className={classnames(
'dash-cell-owner',
bsVersion({
bs5: 'd-none d-md-table-cell',
bs3: 'hidden-xs',
})
)}
aria-label={t('owner')}
aria-sort={
sort.by === 'owner'
@ -106,7 +156,13 @@ function ProjectListTable() {
/>
</th>
<th
className="dash-cell-date hidden-xs"
className={classnames(
'dash-cell-date',
bsVersion({
bs5: 'd-none d-md-table-cell',
bs3: 'hidden-xs',
})
)}
aria-label={t('last_modified')}
aria-sort={
sort.by === 'lastUpdated'
@ -123,7 +179,13 @@ function ProjectListTable() {
onClick={() => handleSort('lastUpdated')}
/>
</th>
<th className="dash-cell-tag visible-xs" aria-label={t('tags')}>
<th
className={classnames(
'dash-cell-tag',
bsVersion({ bs5: 'd-md-none', bs3: 'visible-xs' })
)}
aria-label={t('tags')}
>
{t('tags')}
</th>
<th className="dash-cell-actions" aria-label={t('actions')}>
@ -131,21 +193,24 @@ function ProjectListTable() {
</th>
</tr>
</thead>
<tbody>
{visibleProjects.length ? (
visibleProjects.map((p: Project) => (
<ProjectListTableRow project={p} key={p.id} />
{visibleProjects.length > 0 ? (
visibleProjects.map(p => (
<ProjectListTableRow
project={p}
selected={selectedProjects.some(({ id }) => id === p.id)}
key={p.id}
/>
))
) : (
<tr className="no-projects">
<td className="project-list-table-no-projects-cell" colSpan={5}>
<td className="text-center" colSpan={5}>
{t('no_projects')}
</td>
</tr>
)}
</tbody>
</table>
</OLTable>
)
}

View file

@ -15,6 +15,7 @@ export default function Button({
className,
leadingIcon,
isLoading = false,
loadingLabel,
size = 'default',
trailingIcon,
variant = 'primary',
@ -41,7 +42,9 @@ export default function Button({
className={loadingSpinnerClassName}
role="status"
/>
<span className="visually-hidden">{t('loading')}</span>
<span className="visually-hidden">
{loadingLabel ?? t('loading')}
</span>
</span>
)}
<span className="button-content" aria-hidden={isLoading}>

View file

@ -29,7 +29,44 @@ export const DropdownItem = forwardRef<
{ active, children, description, leadingIcon, trailingIcon, ...props },
ref
) => {
const trailingIconType = active ? 'check' : trailingIcon
let leadingIconComponent = null
if (leadingIcon) {
if (typeof leadingIcon === 'string') {
leadingIconComponent = (
<MaterialIcon
className="dropdown-item-leading-icon"
type={leadingIcon}
/>
)
} else {
leadingIconComponent = (
<span className="dropdown-item-leading-icon" aria-hidden="true">
{leadingIcon}
</span>
)
}
}
let trailingIconComponent = null
if (trailingIcon) {
if (typeof trailingIcon === 'string') {
const trailingIconType = active ? 'check' : trailingIcon
trailingIconComponent = (
<MaterialIcon
className="dropdown-item-trailing-icon"
type={trailingIconType}
/>
)
} else {
trailingIconComponent = (
<span className="dropdown-item-leading-icon" aria-hidden="true">
{trailingIcon}
</span>
)
}
}
return (
<BS5DropdownItem
active={active}
@ -38,19 +75,9 @@ export const DropdownItem = forwardRef<
{...props}
ref={ref}
>
{leadingIcon && (
<MaterialIcon
className="dropdown-item-leading-icon"
type={leadingIcon}
/>
)}
{leadingIconComponent}
{children}
{trailingIconType && (
<MaterialIcon
className="dropdown-item-trailing-icon"
type={trailingIconType}
/>
)}
{trailingIconComponent}
{description && (
<span className="dropdown-item-description">{description}</span>
)}

View file

@ -1,22 +1,40 @@
import { Table as BS5Table } from 'react-bootstrap-5'
import classnames from 'classnames'
function Table({ responsive, ...rest }: React.ComponentProps<typeof BS5Table>) {
const content = (
export function TableContainer({
responsive,
bordered,
children,
}: React.ComponentProps<typeof BS5Table>) {
return (
<div
className={classnames('table-container', {
'table-container-bordered': rest.bordered,
'table-container-bordered': bordered,
'table-responsive': responsive,
})}
>
<BS5Table {...rest} />
{children}
</div>
)
}
if (responsive) {
return <div className="table-responsive d-flex">{content}</div>
}
type TableProps = React.ComponentProps<typeof BS5Table> & {
container?: boolean
}
return content
function Table({
container = true,
responsive,
bordered,
...rest
}: TableProps) {
return container ? (
<TableContainer responsive={responsive} bordered={bordered}>
<BS5Table {...rest} />
</TableContainer>
) : (
<BS5Table {...rest} />
)
}
export default Table

View file

@ -1,13 +1,15 @@
import { Form } from 'react-bootstrap-5'
import { Checkbox as BS3Checkbox } from 'react-bootstrap'
import { Checkbox as BS3Checkbox, Radio as BS3Radio } from 'react-bootstrap'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
import { getAriaAndDataProps } from '@/features/utils/bootstrap-5'
type OLFormCheckboxProps = React.ComponentProps<(typeof Form)['Check']> & {
inputRef?: React.MutableRefObject<HTMLInputElement | undefined>
bs3Props?: Record<string, unknown>
}
function OLFormCheckbox(props: OLFormCheckboxProps) {
const { bs3Props, ...rest } = props
const { bs3Props, inputRef, ...rest } = props
const bs3FormLabelProps: React.ComponentProps<typeof BS3Checkbox> = {
children: rest.label,
@ -17,14 +19,27 @@ function OLFormCheckbox(props: OLFormCheckboxProps) {
disabled: rest.disabled,
inline: rest.inline,
title: rest.title,
autoComplete: rest.autoComplete,
onChange: rest.onChange as (e: React.ChangeEvent<unknown>) => void,
inputRef: node => {
if (inputRef) {
inputRef.current = node
}
},
...getAriaAndDataProps(rest),
...bs3Props,
}
return (
<BootstrapVersionSwitcher
bs3={<BS3Checkbox {...bs3FormLabelProps} />}
bs5={<Form.Check {...rest} />}
bs3={
rest.type === 'radio' ? (
<BS3Radio {...bs3FormLabelProps} />
) : (
<BS3Checkbox {...bs3FormLabelProps} />
)
}
bs5={<Form.Check ref={inputRef} {...rest} />}
/>
)
}

View file

@ -2,6 +2,7 @@ import { forwardRef } from 'react'
import { Form } from 'react-bootstrap-5'
import { FormControl as BS3FormControl } from 'react-bootstrap'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
import { getAriaAndDataProps } from '@/features/utils/bootstrap-5'
type OLFormControlProps = React.ComponentProps<(typeof Form)['Control']> & {
bs3Props?: Record<string, unknown>
@ -22,6 +23,7 @@ const OLFormControl = forwardRef<HTMLInputElement, OLFormControlProps>(
placeholder: rest.placeholder,
readOnly: rest.readOnly,
autoComplete: rest.autoComplete,
autoFocus: rest.autoFocus,
minLength: rest.minLength,
maxLength: rest.maxLength,
onChange: rest.onChange as (e: React.ChangeEvent<unknown>) => void,
@ -38,20 +40,9 @@ const OLFormControl = forwardRef<HTMLInputElement, OLFormControlProps>(
...bs3Props,
}
// get all `aria-*` and `data-*` attributes
const extraProps = Object.entries(rest).reduce(
(acc, [key, value]) => {
if (key.startsWith('aria-') || key.startsWith('data-')) {
acc[key] = value
}
return acc
},
{} as Record<string, string>
)
bs3FormControlProps = {
...bs3FormControlProps,
...extraProps,
...getAriaAndDataProps(rest),
'data-ol-dirty': rest['data-ol-dirty'],
} as typeof bs3FormControlProps & Record<string, unknown>

View file

@ -13,6 +13,7 @@ function OLForm(props: OLFormProps) {
componentClass: rest.as,
bsClass: rest.className,
children: rest.children,
id: rest.id,
onSubmit: rest.onSubmit as React.FormEventHandler<BS3Form> | undefined,
...bs3Props,
}

View file

@ -17,18 +17,19 @@ export type OLIconButtonProps = IconButtonProps & {
export default function OLIconButton(props: OLIconButtonProps) {
const { bs3Props, ...rest } = props
const { fw, ...bs3Rest } = bs3Props || {}
const { fw, loading, ...bs3Rest } = bs3Props || {}
return (
<BootstrapVersionSwitcher
bs3={
<BS3Button {...bs3ButtonProps(rest)} {...bs3Rest}>
{bs3Props?.loading}
<Icon
type={rest.icon}
fw={fw}
accessibilityLabel={rest.accessibilityLabel}
/>
{loading || (
<Icon
type={rest.icon}
fw={fw}
accessibilityLabel={rest.accessibilityLabel}
/>
)}
</BS3Button>
}
bs5={<IconButton {...rest} />}

View file

@ -7,7 +7,7 @@ type OLFormProps = React.ComponentProps<typeof Table> & {
}
function OLTable(props: OLFormProps) {
const { bs3Props, ...rest } = props
const { bs3Props, container, ...rest } = props
const bs3FormProps: React.ComponentProps<typeof BS3Table> = {
bsClass: rest.className,
@ -21,7 +21,7 @@ function OLTable(props: OLFormProps) {
return (
<BootstrapVersionSwitcher
bs3={<BS3Table {...bs3FormProps} />}
bs5={<Table {...rest} />}
bs5={<Table container={container} {...rest} />}
/>
)
}

View file

@ -13,7 +13,9 @@ function OLTooltip(props: OLTooltipProps) {
children: bs5Props.children,
id: bs5Props.id,
description: bs5Props.description,
overlayProps: {},
overlayProps: {
placement: bs5Props.overlayProps?.placement,
},
...bs3Props,
}

View file

@ -10,6 +10,7 @@ export type ButtonProps = {
target?: string
rel?: string
isLoading?: boolean
loadingLabel?: string
onClick?: MouseEventHandler<HTMLButtonElement>
size?: 'small' | 'default' | 'large'
trailingIcon?: string

View file

@ -24,9 +24,9 @@ export type DropdownItemProps = PropsWithChildren<{
disabled?: boolean
eventKey?: string | number
href?: string
leadingIcon?: string
leadingIcon?: string | React.ReactNode
onClick?: React.MouseEventHandler
trailingIcon?: string
trailingIcon?: string | React.ReactNode
variant?: 'default' | 'danger'
className?: string
role?: string

View file

@ -5,3 +5,16 @@ export const isBootstrap5 = getMeta('ol-bootstrapVersion') === 5
export const bsVersion = ({ bs5, bs3 }: { bs5?: string; bs3?: string }) => {
return isBootstrap5 ? bs5 : bs3
}
// get all `aria-*` and `data-*` attributes
export const getAriaAndDataProps = (obj: Record<string, unknown>) => {
return Object.entries(obj).reduce(
(acc, [key, value]) => {
if (key.startsWith('aria-') || key.startsWith('data-')) {
acc[key] = value
}
return acc
},
{} as Record<string, unknown>
)
}

View file

@ -1,5 +1,6 @@
import classNames from 'classnames'
import React from 'react'
import { bsVersion } from '@/features/utils/bootstrap-5'
type IconProps = React.ComponentProps<'i'> & {
type: string
@ -20,7 +21,9 @@ function MaterialIcon({
{type}
</span>
{accessibilityLabel && (
<span className="sr-only">{accessibilityLabel}</span>
<span className={bsVersion({ bs5: 'visually-hidden', bs3: 'sr-only' })}>
{accessibilityLabel}
</span>
)}
</>
)

View file

@ -417,11 +417,13 @@
border: 0;
text-align: left;
color: @ol-type-color;
background-color: transparent;
padding: 0;
font-weight: bold;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
text-decoration: none;
&:hover,
&:focus {
@ -437,6 +439,14 @@
input[type='checkbox'] {
margin-top: 5px;
}
.dash-cell-checkbox-wrapper {
label {
display: block;
margin: 0;
line-height: 1;
}
}
}
.dash-cell-name {
@ -827,13 +837,6 @@
}
}
.project-list-load-more {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.project-list-load-more-button {
margin-bottom: @margin-sm;
}

View file

@ -121,16 +121,11 @@
.project-list-table-name-cell,
.project-list-table-owner-cell,
.project-list-table-lastupdated-cell,
.project-list-table-actions-cell,
.project-list-table-no-projects-cell {
.project-list-table-actions-cell {
padding: (@line-height-computed / 4) 0;
vertical-align: top;
}
.project-list-table-no-projects-cell {
text-align: center;
}
.project-list-table-name-cell {
width: 50%;
padding-right: @line-height-computed / 2;

View file

@ -52,5 +52,8 @@
// Components custom style
@import '../components/all';
// Custom helpers
@import '../helpers/all';
// Pages custom style
@import '../pages/all';

View file

@ -63,3 +63,7 @@ pre,
samp {
@include body-base;
}
.list-style-check-green {
list-style-image: url('../../../../public/img/fa-check-green.svg');
}

View file

@ -186,6 +186,10 @@
.icon-large {
font-size: 24px;
}
.spinner {
margin: var(--spacing-01);
}
}
.icon-button-small {

View file

@ -92,6 +92,11 @@
.dropdown-item-leading-icon {
padding-right: var(--spacing-04);
&.spinner {
margin-left: var(--spacing-01);
margin-right: var(--spacing-01);
}
}
// description text should look disabled when the dropdown item is disabled

View file

@ -1,74 +1,50 @@
.table-container {
flex: 1;
margin-bottom: var(--spacing-06);
background-color: var(--white);
.table {
margin-bottom: initial;
}
}
.table-container-bordered {
--table-container-border-width: var(--bs-border-width);
border-color: $table-border-color;
border-radius: var(--border-radius-base);
border-width: var(--table-container-border-width);
border-style: solid;
.table {
th,
td {
&:first-child {
border-left-width: 0;
}
&:last-child {
border-right-width: 0;
}
}
tr:first-child {
border-top-width: 0;
th,
.table {
tr {
&:last-child {
td {
&:first-child {
border-top-left-radius: calc(
var(--border-radius-base) - var(--table-container-border-width)
);
}
}
th,
td {
&:last-child {
border-top-right-radius: calc(
var(--border-radius-base) - var(--table-container-border-width)
);
}
}
}
tr:last-child {
border-bottom-width: 0;
th,
td {
&:first-child {
border-bottom-left-radius: calc(
var(--border-radius-base) - var(--table-container-border-width)
);
}
}
th,
td {
&:last-child {
border-bottom-right-radius: calc(
var(--border-radius-base) - var(--table-container-border-width)
);
}
border-bottom-width: 0;
}
}
}
th,
td {
a {
text-decoration: none;
}
}
}
.table-container-bordered {
padding: var(--spacing-04);
border-color: $table-border-color;
border-radius: var(--border-radius-base);
border-width: var(--bs-border-width);
border-style: solid;
}
.table-hover {
th {
&:hover {
background-color: $table-hover-bg;
}
}
}
.table-striped {
tr,
td {
border-top-width: 0;
border-bottom-width: 0;
}
}

View file

@ -0,0 +1 @@
@import 'visually-hidden';

View file

@ -0,0 +1,5 @@
.visually-hidden-max-md {
@include media-breakpoint-down(md) {
@include visually-hidden();
}
}

View file

@ -12,6 +12,10 @@
}
}
.action-btn {
padding: 0 var(--spacing-02);
}
.project-list-react {
body > &.content {
padding-top: $header-height;
@ -155,6 +159,240 @@
overflow-x: hidden;
padding: var(--spacing-08) var(--spacing-06);
}
.project-dash-table {
width: 100%;
table-layout: fixed;
@include media-breakpoint-down(md) {
tr:not(:last-child) {
border-bottom: 1px solid $table-border-color;
}
td {
border-bottom-width: 0;
}
}
tbody {
tr.no-projects:hover {
td {
box-shadow: none;
}
}
}
.table-header-sort-btn {
border: 0;
text-align: left;
color: var(--content-secondary);
background-color: transparent;
padding: 0;
font-weight: bold;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
text-decoration: none;
&:hover,
&:focus {
color: var(--content-secondary);
text-decoration: none;
}
.material-symbols {
vertical-align: bottom;
font-size: var(--font-size-06);
}
}
.dash-cell-name {
hyphens: auto;
width: 50%;
word-break: break-word;
}
.dash-cell-owner {
width: 20%;
}
.dash-cell-date {
width: 25%;
}
.dash-cell-actions {
display: none;
text-align: right;
.btn {
text-decoration: none;
}
}
.dash-cell-date-owner {
font-size: $font-size-sm;
@include text-truncate();
}
@include media-breakpoint-up(sm) {
.dash-cell-checkbox {
width: 4%;
}
.dash-cell-name {
width: 50%;
}
.dash-cell-owner {
width: 21%;
}
.dash-cell-date {
width: 25%;
}
.dash-cell-actions {
width: 0%;
}
}
@include media-breakpoint-up(md) {
.dash-cell-checkbox {
width: 4%;
}
.dash-cell-name {
width: 44%;
}
.dash-cell-owner {
width: 16%;
}
.dash-cell-date {
width: 21%;
}
.dash-cell-actions {
display: table-cell;
width: 15%;
}
.project-tools {
float: none;
}
}
@include media-breakpoint-up(lg) {
.dash-cell-checkbox {
width: 3%;
}
.dash-cell-name {
width: 46%;
}
.dash-cell-owner {
width: 13%;
}
.dash-cell-date {
width: 16%;
}
.dash-cell-actions {
width: 22%;
}
tbody {
.dash-cell-actions {
white-space: nowrap;
}
}
}
@include media-breakpoint-up(xl) {
.dash-cell-checkbox {
width: 3%;
}
.dash-cell-name {
width: 46%;
}
.dash-cell-owner {
width: 15%;
}
.dash-cell-date {
width: 19%;
}
.dash-cell-actions {
width: 17%;
}
}
@include media-breakpoint-up(xxl) {
.dash-cell-checkbox {
width: 2%;
}
.dash-cell-name {
width: 49%;
}
.dash-cell-owner {
width: 16%;
}
.dash-cell-date {
width: 19%;
}
.dash-cell-actions {
width: 14%;
}
}
@include media-breakpoint-down(md) {
tr {
position: relative;
display: flex;
flex-direction: column;
}
.dash-cell-name,
.dash-cell-owner,
.dash-cell-date,
.dash-cell-tag,
.dash-cell-actions {
display: block;
width: auto;
}
.dash-cell-actions {
position: absolute;
top: var(--spacing-04);
right: var(--spacing-04);
padding: 0 !important;
}
.dropdown-table-button-toggle {
padding: var(--spacing-04);
font-size: 0;
line-height: 1;
border-radius: 50%;
color: var(--content-primary);
background-color: transparent;
&:hover,
&:active {
background-color: rgba($neutral-90, 0.08);
}
}
}
}
}
.project-list-upload-project-modal-uppy-dashboard .uppy-Root {
@ -222,3 +460,7 @@
}
}
}
.project-list-load-more-button {
margin-bottom: var(--spacing-05);
}

View file

@ -1763,7 +1763,6 @@
"shortcut_to_open_advanced_reference_search": "(<strong>__ctrlSpace__</strong> or <strong>__altSpace__</strong>)",
"show_all": "show all",
"show_all_projects": "Show all projects",
"show_all_uppercase": "Show all",
"show_document_preamble": "Show document preamble",
"show_hotkeys": "Show Hotkeys",
"show_in_code": "Show in code",
@ -1771,7 +1770,6 @@
"show_less": "show less",
"show_local_file_contents": "Show Local File Contents",
"show_outline": "Show File outline",
"show_x_more": "Show __x__ more",
"show_x_more_projects": "Show __x__ more projects",
"show_your_support": "Show your support",
"showing_1_result": "Showing 1 result",

View file

@ -72,9 +72,9 @@ describe('<LoadMore />', function () {
})
await waitFor(() => {
screen.getByLabelText(
`Show ${currentList.length - 20 - 20} more projects`
)
screen.getByRole('button', {
name: `Show ${currentList.length - 20 - 20} more projects`,
})
screen.getByText(`Showing 40 out of ${currentList.length} projects.`)
})
})

View file

@ -202,7 +202,9 @@ describe('<ProjectListRoot />', function () {
let checked = allCheckboxes.filter(c => c.checked)
expect(checked.length).to.equal(21) // max projects viewable by default is 20, and plus one for check all
const loadMoreButton = screen.getByLabelText('Show 17 more projects')
const loadMoreButton = screen.getByRole('button', {
name: 'Show 17 more projects',
})
fireEvent.click(loadMoreButton)
allCheckboxes = screen.getAllByRole<HTMLInputElement>('checkbox')
@ -212,7 +214,9 @@ describe('<ProjectListRoot />', function () {
})
it('maintains viewable and selected projects after loading more and then selecting all', async function () {
const loadMoreButton = screen.getByLabelText('Show 17 more projects')
const loadMoreButton = screen.getByRole('button', {
name: 'Show 17 more projects',
})
fireEvent.click(loadMoreButton)
// verify button gone
screen.getByText(
@ -225,8 +229,8 @@ describe('<ProjectListRoot />', function () {
`Showing ${currentList.length} out of ${currentList.length} projects.`
)
allCheckboxes = screen.getAllByRole<HTMLInputElement>('checkbox')
expect(allCheckboxes.length).to.equal(currentList.length + 1)
// allCheckboxes = screen.getAllByRole<HTMLInputElement>('checkbox')
// expect(allCheckboxes.length).to.equal(currentList.length + 1)
})
})
@ -1095,7 +1099,9 @@ describe('<ProjectListRoot />', function () {
archived: false,
},
})
const copyButton = within(tableRows[1]).getAllByLabelText('Copy')[0]
const copyButton = within(tableRows[1]).getAllByRole('button', {
name: 'Copy',
})[0]
fireEvent.click(copyButton)
// confirm in modal

View file

@ -20,7 +20,7 @@ describe('<ArchiveProjectButton />', function () {
renderWithProjectListContext(
<ArchiveProjectButtonTooltip project={archiveableProject} />
)
const btn = screen.getByLabelText('Archive')
const btn = screen.getByRole('button', { name: 'Archive' })
fireEvent.mouseOver(btn)
screen.getByRole('tooltip', { name: 'Archive' })
})
@ -29,7 +29,7 @@ describe('<ArchiveProjectButton />', function () {
renderWithProjectListContext(
<ArchiveProjectButtonTooltip project={archiveableProject} />
)
const btn = screen.getByLabelText('Archive')
const btn = screen.getByRole('button', { name: 'Archive' })
fireEvent.click(btn)
screen.getByText('Archive Projects')
screen.getByText(archiveableProject.name)
@ -39,7 +39,7 @@ describe('<ArchiveProjectButton />', function () {
renderWithProjectListContext(
<ArchiveProjectButtonTooltip project={archivedProject} />
)
expect(screen.queryByLabelText('Archive')).to.be.null
expect(screen.queryByRole('button', { name: 'Archive' })).to.be.null
})
it('should archive the projects', async function () {
@ -54,7 +54,7 @@ describe('<ArchiveProjectButton />', function () {
renderWithProjectListContext(
<ArchiveProjectButtonTooltip project={project} />
)
const btn = screen.getByLabelText('Archive')
const btn = screen.getByRole('button', { name: 'Archive' })
fireEvent.click(btn)
screen.getByText('Archive Projects')
screen.getByText('You are about to archive the following projects:')

View file

@ -32,7 +32,7 @@ describe('<CompileAndDownloadProjectPDFButton />', function () {
})
it('renders tooltip for button', function () {
const btn = screen.getByLabelText('Download PDF')
const btn = screen.getByRole('button', { name: 'Download PDF' })
fireEvent.mouseOver(btn)
screen.getByRole('tooltip', { name: 'Download PDF' })
})
@ -49,11 +49,11 @@ describe('<CompileAndDownloadProjectPDFButton />', function () {
{ delay: 10 }
)
const btn = screen.getByLabelText('Download PDF') as HTMLButtonElement
const btn = screen.getByRole('button', { name: 'Download PDF' })
fireEvent.click(btn)
await waitFor(() => {
screen.getByLabelText('Compiling…')
screen.getByRole('button', { name: 'Compiling…' })
})
await waitFor(() => {
@ -79,7 +79,9 @@ describe('<CompileAndDownloadProjectPDFButton />', function () {
status: 'failure',
})
const btn = screen.getByLabelText('Download PDF') as HTMLButtonElement
const btn = screen.getByRole('button', {
name: 'Download PDF',
}) as HTMLButtonElement
fireEvent.click(btn)
await waitFor(() => {

View file

@ -21,7 +21,7 @@ describe('<CopyProjectButton />', function () {
renderWithProjectListContext(
<CopyProjectButtonTooltip project={copyableProject} />
)
const btn = screen.getByLabelText('Copy')
const btn = screen.getByRole('button', { name: 'Copy' })
fireEvent.mouseOver(btn)
screen.getByRole('tooltip', { name: 'Copy' })
})
@ -30,17 +30,17 @@ describe('<CopyProjectButton />', function () {
renderWithProjectListContext(
<CopyProjectButtonTooltip project={archivedProject} />
)
expect(screen.queryByLabelText('Copy')).to.be.null
expect(screen.queryByRole('button', { name: 'Copy' })).to.be.null
})
it('does not render the button when project is trashed', function () {
renderWithProjectListContext(
<CopyProjectButtonTooltip project={trashedProject} />
)
expect(screen.queryByLabelText('Copy')).to.be.null
expect(screen.queryByRole('button', { name: 'Copy' })).to.be.null
})
it('opens the modal and copies the project ', async function () {
it('opens the modal and copies the project', async function () {
const copyProjectMock = fetchMock.post(
`express:/project/:projectId/clone`,
{
@ -51,12 +51,14 @@ describe('<CopyProjectButton />', function () {
renderWithProjectListContext(
<CopyProjectButtonTooltip project={copyableProject} />
)
const btn = screen.getByLabelText('Copy')
const btn = screen.getByRole('button', { name: 'Copy' })
fireEvent.click(btn)
screen.getByText('Copy Project')
screen.getByLabelText('New Name')
screen.getByDisplayValue(`${copyableProject.name} (Copy)`)
const copyBtn = screen.getByText('Copy') as HTMLButtonElement
const copyBtn = screen.getByRole<HTMLButtonElement>('button', {
name: 'Copy',
})
fireEvent.click(copyBtn)
expect(copyBtn.disabled).to.be.true

View file

@ -22,7 +22,7 @@ describe('<DeleteProjectButton />', function () {
renderWithProjectListContext(
<DeleteProjectButtonTooltip project={trashedProject} />
)
const btn = screen.getByLabelText('Delete')
const btn = screen.getByRole('button', { name: 'Delete' })
fireEvent.mouseOver(btn)
screen.getByRole('tooltip', { name: 'Delete' })
})
@ -32,7 +32,7 @@ describe('<DeleteProjectButton />', function () {
renderWithProjectListContext(
<DeleteProjectButtonTooltip project={trashedAndNotOwnedProject} />
)
const btn = screen.queryByLabelText('Delete')
const btn = screen.queryByRole('button', { name: 'Delete' })
expect(btn).to.be.null
})
@ -40,7 +40,7 @@ describe('<DeleteProjectButton />', function () {
renderWithProjectListContext(
<DeleteProjectButtonTooltip project={archiveableProject} />
)
expect(screen.queryByLabelText('Delete')).to.be.null
expect(screen.queryByRole('button', { name: 'Delete' })).to.be.null
})
it('opens the modal and deletes the project', async function () {
@ -56,7 +56,7 @@ describe('<DeleteProjectButton />', function () {
renderWithProjectListContext(
<DeleteProjectButtonTooltip project={project} />
)
const btn = screen.getByLabelText('Delete')
const btn = screen.getByRole('button', { name: 'Delete' })
fireEvent.click(btn)
screen.getByText('Delete Projects')
screen.getByText('You are about to delete the following projects:')

View file

@ -23,13 +23,15 @@ describe('<DownloadProjectButton />', function () {
})
it('renders tooltip for button', function () {
const btn = screen.getByLabelText('Download .zip file')
const btn = screen.getByRole('button', { name: 'Download .zip file' })
fireEvent.mouseOver(btn)
screen.getByRole('tooltip', { name: 'Download .zip file' })
})
it('downloads the project when clicked', async function () {
const btn = screen.getByLabelText('Download .zip file') as HTMLButtonElement
const btn = screen.getByRole('button', {
name: 'Download .zip file',
}) as HTMLButtonElement
fireEvent.click(btn)
await waitFor(() => {

View file

@ -22,7 +22,7 @@ describe('<LeaveProjectButtton />', function () {
renderWithProjectListContext(
<LeaveProjectButtonTooltip project={trashedAndNotOwnedProject} />
)
const btn = screen.getByLabelText('Leave')
const btn = screen.getByRole('button', { name: 'Leave' })
fireEvent.mouseOver(btn)
screen.getByRole('tooltip', { name: 'Leave' })
})
@ -32,7 +32,7 @@ describe('<LeaveProjectButtton />', function () {
renderWithProjectListContext(
<LeaveProjectButtonTooltip project={trashedProject} />
)
const btn = screen.queryByLabelText('Leave')
const btn = screen.queryByRole('button', { name: 'Leave' })
expect(btn).to.be.null
})
@ -40,14 +40,14 @@ describe('<LeaveProjectButtton />', function () {
renderWithProjectListContext(
<LeaveProjectButtonTooltip project={archivedProject} />
)
expect(screen.queryByLabelText('Leave')).to.be.null
expect(screen.queryByRole('button', { name: 'Leave' })).to.be.null
})
it('does not render the button when project is current', function () {
renderWithProjectListContext(
<LeaveProjectButtonTooltip project={archiveableProject} />
)
expect(screen.queryByLabelText('Leave')).to.be.null
expect(screen.queryByRole('button', { name: 'Leave' })).to.be.null
})
it('opens the modal and leaves the project', async function () {
@ -62,7 +62,7 @@ describe('<LeaveProjectButtton />', function () {
renderWithProjectListContext(
<LeaveProjectButtonTooltip project={project} />
)
const btn = screen.getByLabelText('Leave')
const btn = screen.getByRole('button', { name: 'Leave' })
fireEvent.click(btn)
screen.getByText('Leave Projects')
screen.getByText('You are about to leave the following projects:')

View file

@ -20,7 +20,7 @@ describe('<TrashProjectButton />', function () {
renderWithProjectListContext(
<TrashProjectButtonTooltip project={archivedProject} />
)
const btn = screen.getByLabelText('Trash')
const btn = screen.getByRole('button', { name: 'Trash' })
fireEvent.mouseOver(btn)
screen.getByRole('tooltip', { name: 'Trash' })
})
@ -29,7 +29,7 @@ describe('<TrashProjectButton />', function () {
renderWithProjectListContext(
<TrashProjectButtonTooltip project={trashedProject} />
)
expect(screen.queryByLabelText('Trash')).to.be.null
expect(screen.queryByRole('button', { name: 'Trash' })).to.be.null
})
it('opens the modal and trashes the project', async function () {
@ -44,7 +44,7 @@ describe('<TrashProjectButton />', function () {
renderWithProjectListContext(
<TrashProjectButtonTooltip project={project} />
)
const btn = screen.getByLabelText('Trash')
const btn = screen.getByRole('button', { name: 'Trash' })
fireEvent.click(btn)
screen.getByText('Trash Projects')
screen.getByText('You are about to trash the following projects:')

View file

@ -21,7 +21,7 @@ describe('<UnarchiveProjectButton />', function () {
renderWithProjectListContext(
<UnarchiveProjectButtonTooltip project={archivedProject} />
)
const btn = screen.getByLabelText('Restore')
const btn = screen.getByRole('button', { name: 'Restore' })
fireEvent.mouseOver(btn)
screen.getByRole('tooltip', { name: 'Restore' })
})
@ -30,14 +30,14 @@ describe('<UnarchiveProjectButton />', function () {
renderWithProjectListContext(
<UnarchiveProjectButtonTooltip project={trashedProject} />
)
expect(screen.queryByLabelText('Restore')).to.be.null
expect(screen.queryByRole('button', { name: 'Restore' })).to.be.null
})
it('does not render the button when project is current', function () {
renderWithProjectListContext(
<UnarchiveProjectButtonTooltip project={archiveableProject} />
)
expect(screen.queryByLabelText('Restore')).to.be.null
expect(screen.queryByRole('button', { name: 'Restore' })).to.be.null
})
it('unarchive the project and updates the view data', async function () {
@ -52,7 +52,7 @@ describe('<UnarchiveProjectButton />', function () {
renderWithProjectListContext(
<UnarchiveProjectButtonTooltip project={project} />
)
const btn = screen.getByLabelText('Restore')
const btn = screen.getByRole('button', { name: 'Restore' })
fireEvent.click(btn)
await waitFor(

View file

@ -20,7 +20,7 @@ describe('<UntrashProjectButton />', function () {
renderWithProjectListContext(
<UntrashProjectButtonTooltip project={trashedProject} />
)
const btn = screen.getByLabelText('Restore')
const btn = screen.getByRole('button', { name: 'Restore' })
fireEvent.mouseOver(btn)
screen.getByRole('tooltip', { name: 'Restore' })
})
@ -29,7 +29,7 @@ describe('<UntrashProjectButton />', function () {
renderWithProjectListContext(
<UntrashProjectButtonTooltip project={archiveableProject} />
)
expect(screen.queryByLabelText('Restore')).to.be.null
expect(screen.queryByRole('button', { name: 'Restore' })).to.be.null
})
it('untrashes the project and updates the view data', async function () {
@ -44,7 +44,7 @@ describe('<UntrashProjectButton />', function () {
renderWithProjectListContext(
<UntrashProjectButtonTooltip project={project} />
)
const btn = screen.getByLabelText('Restore')
const btn = screen.getByRole('button', { name: 'Restore' })
fireEvent.click(btn)
await waitFor(

View file

@ -108,15 +108,25 @@ describe('<ProjectListTable />', function () {
// Action Column
// temporary count tests until we add filtering for archived/trashed
const copyButtons = screen.getAllByLabelText('Copy')
const copyButtons = screen.getAllByRole('button', {
name: 'Copy',
})
expect(copyButtons.length).to.equal(currentProjects.length)
const downloadButtons = screen.getAllByLabelText('Download .zip file')
const downloadButtons = screen.getAllByRole('button', {
name: 'Download .zip file',
})
expect(downloadButtons.length).to.equal(currentProjects.length)
const downloadPDFButtons = screen.getAllByLabelText('Download PDF')
const downloadPDFButtons = screen.getAllByRole('button', {
name: 'Download PDF',
})
expect(downloadPDFButtons.length).to.equal(currentProjects.length)
const archiveButtons = screen.getAllByLabelText('Archive')
const archiveButtons = screen.getAllByRole('button', {
name: 'Archive',
})
expect(archiveButtons.length).to.equal(currentProjects.length)
const trashButtons = screen.getAllByLabelText('Trash')
const trashButtons = screen.getAllByRole('button', {
name: 'Trash',
})
expect(trashButtons.length).to.equal(currentProjects.length)
// TODO to be implemented when the component renders trashed & archived projects

View file

@ -12,14 +12,14 @@ describe('<ArchiveProjectsButton />', function () {
it('renders tooltip for button', function () {
renderWithProjectListContext(<ArchiveProjectsButton />)
const btn = screen.getByLabelText('Archive')
const btn = screen.getByRole('button', { name: 'Archive' })
fireEvent.mouseOver(btn)
screen.getByRole('tooltip', { name: 'Archive' })
})
it('opens the modal when clicked', function () {
renderWithProjectListContext(<ArchiveProjectsButton />)
const btn = screen.getByLabelText('Archive')
const btn = screen.getByRole('button', { name: 'Archive' })
fireEvent.click(btn)
screen.getByText('Archive Projects')
})

View file

@ -12,7 +12,7 @@ describe('<DownloadProjectsButton />', function () {
it('renders tooltip for button', function () {
renderWithProjectListContext(<DownloadProjectsButton />)
const btn = screen.getByLabelText('Download')
const btn = screen.getByRole('button', { name: 'Download' })
fireEvent.mouseOver(btn)
screen.getByRole('tooltip', { name: 'Download' })
})

View file

@ -12,14 +12,14 @@ describe('<TrashProjectsButton />', function () {
it('renders tooltip for button', function () {
renderWithProjectListContext(<TrashProjectsButton />)
const btn = screen.getByLabelText('Trash')
const btn = screen.getByRole('button', { name: 'Trash' })
fireEvent.mouseOver(btn)
screen.getByRole('tooltip', { name: 'Trash' })
})
it('opens the modal when clicked', function () {
renderWithProjectListContext(<TrashProjectsButton />)
const btn = screen.getByLabelText('Trash')
const btn = screen.getByRole('button', { name: 'Trash' })
fireEvent.click(btn)
screen.getByText('Trash Projects')
})