From f4cdfaa607c12a899430c8252ddde89fa7b95e91 Mon Sep 17 00:00:00 2001 From: David <33458145+davidmcpowell@users.noreply.github.com> Date: Thu, 30 Jul 2026 13:06:08 +0100 Subject: [PATCH] Merge pull request #36037 from overleaf/dp-library-trash-navigation-2 Add trash navigation with Projects/References tabs- #36001 GitOrigin-RevId: 675ad078f117c5acd8744024c751209c615a7c2e --- .../web/frontend/extracted-translations.json | 3 +- .../dropdown/navigation-dropdown.tsx | 93 +++++++++++++++++-- .../components/project-list-ds-nav.tsx | 37 ++++++-- .../components/sidebar/sidebar-ds-nav.tsx | 4 +- .../components/trash/trash-page-tabs.tsx | 33 +++++++ .../pages/project-list-ds-nav.scss | 48 +++++++++- .../stylesheets/pages/project-list.scss | 2 +- services/web/locales/en.json | 3 +- 8 files changed, 199 insertions(+), 24 deletions(-) create mode 100644 services/web/frontend/js/features/project-list/components/trash/trash-page-tabs.tsx diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index f5db4c6f68..2592c8b4b9 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -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": "", diff --git a/services/web/frontend/js/features/project-list/components/dropdown/navigation-dropdown.tsx b/services/web/frontend/js/features/project-list/components/dropdown/navigation-dropdown.tsx index 7445e908a1..10b40bf7b9 100644 --- a/services/web/frontend/js/features/project-list/components/dropdown/navigation-dropdown.tsx +++ b/services/web/frontend/js/features/project-list/components/dropdown/navigation-dropdown.tsx @@ -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>({ 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 }) {
  • } > @@ -213,12 +242,18 @@ function NavigationDropdown({ activePage }: { activePage: ActivePage }) {
  • - } - activePage={activePage} - /> + onClick={e => { + e.stopPropagation() + setView('trash') + }} + > + {t('trash')} +
  • )} @@ -241,6 +276,44 @@ function NavigationDropdown({ activePage }: { activePage: ActivePage }) { )} + {isLibraryEnabled && view === 'trash' && ( + <> +
  • + } + aria-label={t('back')} + onClick={e => { + e.stopPropagation() + setView('top') + }} + > + {t('trash')} + +
  • +
  • + } + > + {t('projects')} + +
  • +
  • + } + > + {t('references')} + +
  • + + )} ) diff --git a/services/web/frontend/js/features/project-list/components/project-list-ds-nav.tsx b/services/web/frontend/js/features/project-list/components/project-list-ds-nav.tsx index 1db53fe0d1..3564a48b96 100644 --- a/services/web/frontend/js/features/project-list/components/project-list-ds-nav.tsx +++ b/services/web/frontend/js/features/project-list/components/project-list-ds-nav.tsx @@ -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 (
    - + {showTrashHeader ? ( +

    + {t('trash')} +

    + ) : ( + + )}
    {selectedProjects.length === 0 ? ( - + showTrashHeader ? null : ( + + ) ) : ( )}
    - + {!showTrashHeader && }
    + {showTrashHeader && }
    diff --git a/services/web/frontend/js/features/project-list/components/sidebar/sidebar-ds-nav.tsx b/services/web/frontend/js/features/project-list/components/sidebar/sidebar-ds-nav.tsx index 4e59a60619..6b09a4e157 100644 --- a/services/web/frontend/js/features/project-list/components/sidebar/sidebar-ds-nav.tsx +++ b/services/web/frontend/js/features/project-list/components/sidebar/sidebar-ds-nav.tsx @@ -52,7 +52,9 @@ function SidebarDsNav({ }, })} > - {isLibraryEnabled && activePage === 'library' && } + {isLibraryEnabled && (activePage === 'library' || isTrashActive) && ( + + )} + ) +} + +export default TrashPageTabs diff --git a/services/web/frontend/stylesheets/pages/project-list-ds-nav.scss b/services/web/frontend/stylesheets/pages/project-list-ds-nav.scss index 8ddcad13e3..f0fc4257ee 100644 --- a/services/web/frontend/stylesheets/pages/project-list-ds-nav.scss +++ b/services/web/frontend/stylesheets/pages/project-list-ds-nav.scss @@ -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; + } + } +} diff --git a/services/web/frontend/stylesheets/pages/project-list.scss b/services/web/frontend/stylesheets/pages/project-list.scss index b412eaaaa4..766b4a46ba 100644 --- a/services/web/frontend/stylesheets/pages/project-list.scss +++ b/services/web/frontend/stylesheets/pages/project-list.scss @@ -142,7 +142,7 @@ min-height: 36px; .project-list-title { - @include heading-sm; + @include heading-md; color: var(--content-secondary-themed); font-weight: bold; diff --git a/services/web/locales/en.json b/services/web/locales/en.json index 6ed0dee0a9..49576b4e9f 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -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 Overleaf Premium trial", "trial_remaining_days": "__days__ more days on your Overleaf Premium trial",