Merge pull request #36037 from overleaf/dp-library-trash-navigation-2

Add trash navigation with Projects/References tabs- #36001

GitOrigin-RevId: 675ad078f117c5acd8744024c751209c615a7c2e
This commit is contained in:
David
2026-09-17 11:41:52 +00:00
committed by Copybot
parent 281b5fe25c
commit f4cdfaa607
8 changed files with 199 additions and 24 deletions
@@ -1647,6 +1647,7 @@
"reference_sync": "",
"references": "",
"references_from_these_libraries_will_be_included_in_your_reference_search_results": "",
"references_permanently_deleted_after_30_days": "",
"refresh": "",
"refresh_page_after_linking_dropbox": "",
"refresh_page_after_starting_free_trial": "",
@@ -1781,6 +1782,7 @@
"search_in_library": "",
"search_in_shared_projects": "",
"search_in_trashed_projects": "",
"search_in_trashed_references": "",
"search_in_your_projects": "",
"search_match_case": "",
"search_members": "",
@@ -2256,7 +2258,6 @@
"trash_projects": "",
"trashed": "",
"trashed_projects": "",
"trashed_references": "",
"trashing_projects_wont_affect_collaborators": "",
"trial_last_day": "",
"trial_remaining_days": "",
@@ -59,14 +59,32 @@ export function Item({
)
}
function NavigationDropdown({ activePage }: { activePage: ActivePage }) {
function NavigationDropdown({
activePage,
trashActive = false,
}: {
activePage: ActivePage
/**
* Whether the trash is active. This is only relevant when the active page is
* "library" as which project page is active is determined by the selected
* filter.
*/
trashActive?: boolean
}) {
const { t } = useTranslation()
const [title, setTitle] = useState(() =>
activePage === 'library' ? t('library') : t('all_projects')
)
const [view, setView] = useState<'top' | 'tags'>('top')
const [view, setView] = useState<'top' | 'tags' | 'trash'>('top')
const { filter, selectedTagId, tags } = useProjectListContext()
const isLibraryEnabled = isSplitTestEnabled('overleaf-library')
const projectsTrashActive =
activePage === 'projects' &&
selectedTagId === undefined &&
filter === 'trashed'
const referencesTrashActive = activePage === 'library' && trashActive
const isTrashActive = projectsTrashActive || referencesTrashActive
const filterTranslations = useRef<Record<Filter, string>>({
all: t('all_projects'),
owned: t('your_projects'),
@@ -76,6 +94,11 @@ function NavigationDropdown({ activePage }: { activePage: ActivePage }) {
})
useEffect(() => {
if (isTrashActive) {
setTitle(t('trash'))
return
}
if (activePage === 'library') {
setTitle(t('library'))
return
@@ -94,7 +117,7 @@ function NavigationDropdown({ activePage }: { activePage: ActivePage }) {
setTitle(tag.name ?? '')
}
}
}, [filter, tags, selectedTagId, t, activePage])
}, [filter, tags, selectedTagId, t, activePage, isTrashActive])
const filterItems = (
<>
@@ -158,7 +181,13 @@ function NavigationDropdown({ activePage }: { activePage: ActivePage }) {
isLibraryEnabled
? show => {
if (show) {
setView(selectedTagId !== undefined ? 'tags' : 'top')
if (selectedTagId !== undefined) {
setView('tags')
} else if (isTrashActive) {
setView('trash')
} else {
setView('top')
}
} else {
setView('top')
}
@@ -205,7 +234,7 @@ function NavigationDropdown({ activePage }: { activePage: ActivePage }) {
<OLDropdownDivider />
<li role="none">
<OLDropdownItem
active={activePage === 'library'}
active={activePage === 'library' && !trashActive}
href="/library"
leadingIcon={<BookBookmark size={20} />}
>
@@ -213,12 +242,18 @@ function NavigationDropdown({ activePage }: { activePage: ActivePage }) {
</OLDropdownItem>
</li>
<li role="none">
<Item
filter="trashed"
text={t('trash')}
<OLDropdownItem
as="button"
tabIndex={-1}
trailingIcon="chevron_right"
leadingIcon={<Trash size={20} />}
activePage={activePage}
/>
onClick={e => {
e.stopPropagation()
setView('trash')
}}
>
{t('trash')}
</OLDropdownItem>
</li>
</>
)}
@@ -241,6 +276,44 @@ function NavigationDropdown({ activePage }: { activePage: ActivePage }) {
<TagsList />
</>
)}
{isLibraryEnabled && view === 'trash' && (
<>
<li role="none">
<OLDropdownItem
as="button"
tabIndex={-1}
leadingIcon={<MaterialIcon type="chevron_left" />}
aria-label={t('back')}
onClick={e => {
e.stopPropagation()
setView('top')
}}
>
{t('trash')}
</OLDropdownItem>
</li>
<li role="none">
<OLDropdownItem
href="/project/trashed"
active={projectsTrashActive}
trailingIcon={projectsTrashActive ? 'check' : undefined}
leadingIcon={<Folder size={20} />}
>
{t('projects')}
</OLDropdownItem>
</li>
<li role="none">
<OLDropdownItem
href="/library/trashed"
active={referencesTrashActive}
trailingIcon={referencesTrashActive ? 'check' : undefined}
leadingIcon={<BookBookmark size={20} />}
>
{t('references')}
</OLDropdownItem>
</li>
</>
)}
</OLDropdownMenu>
</OLDropdown>
)
@@ -1,3 +1,4 @@
import classNames from 'classnames'
import { useProjectListContext } from '../context/project-list-context'
import { useTranslation } from 'react-i18next'
import CurrentPlanWidget from './current-plan-widget/current-plan-widget'
@@ -9,6 +10,7 @@ import NavigationDropdown from './dropdown/navigation-dropdown'
import SortByDropdown from './dropdown/sort-by-dropdown'
import ProjectTools from './table/project-tools/project-tools'
import ProjectListTitle from './title/project-list-title'
import TrashPageTabs from './trash/trash-page-tabs'
import LoadMore from './load-more'
import OLCol from '@/shared/components/ol/ol-col'
import OLRow from '@/shared/components/ol/ol-row'
@@ -42,6 +44,7 @@ export function ProjectListDsNav() {
const isLibraryEnabled = isSplitTestEnabled('overleaf-library')
const selectedTag = tags.find(tag => tag._id === selectedTagId)
const showTrashHeader = isLibraryEnabled && filter === 'trashed'
const showNewProjectButton =
!isLibraryEnabled ||
(filter !== 'shared' && filter !== 'archived' && filter !== 'trashed')
@@ -84,7 +87,10 @@ export function ProjectListDsNav() {
return (
<div
className={`project-ds-nav-page website-redesign${isLibraryEnabled ? ' library-enabled' : ''}`}
className={classNames('project-ds-nav-page', 'website-redesign', {
'library-enabled': isLibraryEnabled,
'ds-nav-hides-top-navbar': showTrashHeader,
})}
>
<SystemMessages />
<DefaultNavbar
@@ -103,25 +109,38 @@ export function ProjectListDsNav() {
<UserNotifications />
<main aria-labelledby="main-content">
<div className="project-list-header-row">
<ProjectListTitle
filter={filter}
selectedTag={selectedTag}
selectedTagId={selectedTagId}
className="text-truncate d-none d-md-block"
/>
{showTrashHeader ? (
<h1
id="main-content"
tabIndex={-1}
className="project-list-title text-truncate d-none d-md-block mb-0"
>
{t('trash')}
</h1>
) : (
<ProjectListTitle
filter={filter}
selectedTag={selectedTag}
selectedTagId={selectedTagId}
className="text-truncate d-none d-md-block"
/>
)}
<div className="project-tools">
<div className="d-none d-md-block">
{selectedProjects.length === 0 ? (
<CurrentPlanWidget />
showTrashHeader ? null : (
<CurrentPlanWidget />
)
) : (
<ProjectTools />
)}
</div>
<div className="d-md-none">
<CurrentPlanWidget />
{!showTrashHeader && <CurrentPlanWidget />}
</div>
</div>
</div>
{showTrashHeader && <TrashPageTabs activeTab="projects" />}
<div className="project-ds-nav-project-list">
<OLRow className="d-none d-md-flex align-items-center">
<OLCol md={isLibraryEnabled ? 8 : undefined} lg={7}>
@@ -52,7 +52,9 @@ function SidebarDsNav({
},
})}
>
{isLibraryEnabled && activePage === 'library' && <DsNavOverleafLogo />}
{isLibraryEnabled && (activePage === 'library' || isTrashActive) && (
<DsNavOverleafLogo />
)}
<nav
className="flex-grow flex-shrink"
aria-label={t('project_categories_tags')}
@@ -0,0 +1,33 @@
import { useTranslation } from 'react-i18next'
import classnames from 'classnames'
export type TrashTab = 'projects' | 'references'
function TrashPageTabs({ activeTab }: { activeTab: TrashTab }) {
const { t } = useTranslation()
const tabs: { key: TrashTab; label: string; href: string }[] = [
{ key: 'projects', label: t('projects'), href: '/project/trashed' },
{ key: 'references', label: t('references'), href: '/library/trashed' },
]
return (
<nav className="trash-page-tabs" aria-label={t('trash')}>
{tabs.map(tab => {
const active = tab.key === activeTab
return (
<a
key={tab.key}
href={tab.href}
className={classnames('trash-page-tab', { active })}
aria-current={active ? 'page' : undefined}
>
{tab.label}
</a>
)
})}
</nav>
)
}
export default TrashPageTabs
@@ -147,7 +147,7 @@ body {
}
}
&.library-page {
&.ds-nav-hides-top-navbar {
// The sidebar shows the logo and account/help menus from md upwards, so
// the top navbar is only needed on mobile.
.navbar-default {
@@ -164,6 +164,19 @@ body {
border-top: none;
}
}
.project-list-header-row {
@include media-breakpoint-up(md) {
margin-top: var(--spacing-08);
}
// On mobile the trash page has no heading or plan widget in this row
// (both are desktop-only), so collapse it to avoid an empty gap above
// the navigation dropdown.
@include media-breakpoint-down(md) {
display: none;
}
}
}
.navbar-nav > li > .dropdown-toggle::after {
@@ -559,3 +572,36 @@ body {
}
}
}
.trash-page-tabs {
display: flex;
gap: var(--spacing-06);
margin-bottom: var(--spacing-06);
border-bottom: 1px solid var(--border-divider-themed);
// On mobile the Projects/References switch lives in the navigation dropdown
// submenu instead of these tabs.
@include media-breakpoint-down(md) {
display: none;
}
.trash-page-tab {
padding: var(--spacing-03) var(--spacing-02);
margin-bottom: -1px;
border-bottom: 2px solid transparent;
color: var(--content-secondary-themed);
font-size: var(--font-size-03);
text-decoration: none;
&:hover {
color: var(--content-primary-themed);
text-decoration: none;
}
&.active {
border-bottom-color: var(--green-50);
color: var(--content-primary-themed);
font-weight: 600;
}
}
}
@@ -142,7 +142,7 @@
min-height: 36px;
.project-list-title {
@include heading-sm;
@include heading-md;
color: var(--content-secondary-themed);
font-weight: bold;
+2 -1
View File
@@ -2143,6 +2143,7 @@
"reference_sync": "Reference manager sync",
"references": "References",
"references_from_these_libraries_will_be_included_in_your_reference_search_results": "References from these libraries will be included in your reference search results.",
"references_permanently_deleted_after_30_days": "References are permanently deleted after 30 days.",
"refresh": "Refresh",
"refresh_page_after_linking_dropbox": "Please refresh this page after linking your account to Dropbox.",
"refresh_page_after_starting_free_trial": "Please refresh this page after starting your free trial.",
@@ -2315,6 +2316,7 @@
"search_in_library": "Search in your library",
"search_in_shared_projects": "Search in projects shared with you",
"search_in_trashed_projects": "Search in trashed projects",
"search_in_trashed_references": "Search in trashed references",
"search_in_your_projects": "Search in your projects",
"search_match_case": "Match case",
"search_members": "Search members",
@@ -2875,7 +2877,6 @@
"trash_projects": "Trash Projects",
"trashed": "Trashed",
"trashed_projects": "Trashed projects",
"trashed_references": "Trashed references",
"trashing_projects_wont_affect_collaborators": "Trashing projects won’t affect your collaborators.",
"trial_last_day": "This is the last day of your <b>Overleaf Premium</b> trial",
"trial_remaining_days": "__days__ more days on your <b>Overleaf Premium</b> trial",