mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-15 06:33:55 +01:00
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:
@@ -1647,6 +1647,7 @@
|
|||||||
"reference_sync": "",
|
"reference_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": "",
|
||||||
"refresh": "",
|
"refresh": "",
|
||||||
"refresh_page_after_linking_dropbox": "",
|
"refresh_page_after_linking_dropbox": "",
|
||||||
"refresh_page_after_starting_free_trial": "",
|
"refresh_page_after_starting_free_trial": "",
|
||||||
@@ -1781,6 +1782,7 @@
|
|||||||
"search_in_library": "",
|
"search_in_library": "",
|
||||||
"search_in_shared_projects": "",
|
"search_in_shared_projects": "",
|
||||||
"search_in_trashed_projects": "",
|
"search_in_trashed_projects": "",
|
||||||
|
"search_in_trashed_references": "",
|
||||||
"search_in_your_projects": "",
|
"search_in_your_projects": "",
|
||||||
"search_match_case": "",
|
"search_match_case": "",
|
||||||
"search_members": "",
|
"search_members": "",
|
||||||
@@ -2256,7 +2258,6 @@
|
|||||||
"trash_projects": "",
|
"trash_projects": "",
|
||||||
"trashed": "",
|
"trashed": "",
|
||||||
"trashed_projects": "",
|
"trashed_projects": "",
|
||||||
"trashed_references": "",
|
|
||||||
"trashing_projects_wont_affect_collaborators": "",
|
"trashing_projects_wont_affect_collaborators": "",
|
||||||
"trial_last_day": "",
|
"trial_last_day": "",
|
||||||
"trial_remaining_days": "",
|
"trial_remaining_days": "",
|
||||||
|
|||||||
+83
-10
@@ -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 { t } = useTranslation()
|
||||||
const [title, setTitle] = useState(() =>
|
const [title, setTitle] = useState(() =>
|
||||||
activePage === 'library' ? t('library') : t('all_projects')
|
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 { filter, selectedTagId, tags } = useProjectListContext()
|
||||||
const isLibraryEnabled = isSplitTestEnabled('overleaf-library')
|
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>>({
|
const filterTranslations = useRef<Record<Filter, string>>({
|
||||||
all: t('all_projects'),
|
all: t('all_projects'),
|
||||||
owned: t('your_projects'),
|
owned: t('your_projects'),
|
||||||
@@ -76,6 +94,11 @@ function NavigationDropdown({ activePage }: { activePage: ActivePage }) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (isTrashActive) {
|
||||||
|
setTitle(t('trash'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (activePage === 'library') {
|
if (activePage === 'library') {
|
||||||
setTitle(t('library'))
|
setTitle(t('library'))
|
||||||
return
|
return
|
||||||
@@ -94,7 +117,7 @@ function NavigationDropdown({ activePage }: { activePage: ActivePage }) {
|
|||||||
setTitle(tag.name ?? '')
|
setTitle(tag.name ?? '')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [filter, tags, selectedTagId, t, activePage])
|
}, [filter, tags, selectedTagId, t, activePage, isTrashActive])
|
||||||
|
|
||||||
const filterItems = (
|
const filterItems = (
|
||||||
<>
|
<>
|
||||||
@@ -158,7 +181,13 @@ function NavigationDropdown({ activePage }: { activePage: ActivePage }) {
|
|||||||
isLibraryEnabled
|
isLibraryEnabled
|
||||||
? show => {
|
? show => {
|
||||||
if (show) {
|
if (show) {
|
||||||
setView(selectedTagId !== undefined ? 'tags' : 'top')
|
if (selectedTagId !== undefined) {
|
||||||
|
setView('tags')
|
||||||
|
} else if (isTrashActive) {
|
||||||
|
setView('trash')
|
||||||
|
} else {
|
||||||
|
setView('top')
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
setView('top')
|
setView('top')
|
||||||
}
|
}
|
||||||
@@ -205,7 +234,7 @@ function NavigationDropdown({ activePage }: { activePage: ActivePage }) {
|
|||||||
<OLDropdownDivider />
|
<OLDropdownDivider />
|
||||||
<li role="none">
|
<li role="none">
|
||||||
<OLDropdownItem
|
<OLDropdownItem
|
||||||
active={activePage === 'library'}
|
active={activePage === 'library' && !trashActive}
|
||||||
href="/library"
|
href="/library"
|
||||||
leadingIcon={<BookBookmark size={20} />}
|
leadingIcon={<BookBookmark size={20} />}
|
||||||
>
|
>
|
||||||
@@ -213,12 +242,18 @@ function NavigationDropdown({ activePage }: { activePage: ActivePage }) {
|
|||||||
</OLDropdownItem>
|
</OLDropdownItem>
|
||||||
</li>
|
</li>
|
||||||
<li role="none">
|
<li role="none">
|
||||||
<Item
|
<OLDropdownItem
|
||||||
filter="trashed"
|
as="button"
|
||||||
text={t('trash')}
|
tabIndex={-1}
|
||||||
|
trailingIcon="chevron_right"
|
||||||
leadingIcon={<Trash size={20} />}
|
leadingIcon={<Trash size={20} />}
|
||||||
activePage={activePage}
|
onClick={e => {
|
||||||
/>
|
e.stopPropagation()
|
||||||
|
setView('trash')
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('trash')}
|
||||||
|
</OLDropdownItem>
|
||||||
</li>
|
</li>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -241,6 +276,44 @@ function NavigationDropdown({ activePage }: { activePage: ActivePage }) {
|
|||||||
<TagsList />
|
<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>
|
</OLDropdownMenu>
|
||||||
</OLDropdown>
|
</OLDropdown>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import classNames from 'classnames'
|
||||||
import { useProjectListContext } from '../context/project-list-context'
|
import { useProjectListContext } from '../context/project-list-context'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import CurrentPlanWidget from './current-plan-widget/current-plan-widget'
|
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 SortByDropdown from './dropdown/sort-by-dropdown'
|
||||||
import ProjectTools from './table/project-tools/project-tools'
|
import ProjectTools from './table/project-tools/project-tools'
|
||||||
import ProjectListTitle from './title/project-list-title'
|
import ProjectListTitle from './title/project-list-title'
|
||||||
|
import TrashPageTabs from './trash/trash-page-tabs'
|
||||||
import LoadMore from './load-more'
|
import LoadMore from './load-more'
|
||||||
import OLCol from '@/shared/components/ol/ol-col'
|
import OLCol from '@/shared/components/ol/ol-col'
|
||||||
import OLRow from '@/shared/components/ol/ol-row'
|
import OLRow from '@/shared/components/ol/ol-row'
|
||||||
@@ -42,6 +44,7 @@ export function ProjectListDsNav() {
|
|||||||
const isLibraryEnabled = isSplitTestEnabled('overleaf-library')
|
const isLibraryEnabled = isSplitTestEnabled('overleaf-library')
|
||||||
|
|
||||||
const selectedTag = tags.find(tag => tag._id === selectedTagId)
|
const selectedTag = tags.find(tag => tag._id === selectedTagId)
|
||||||
|
const showTrashHeader = isLibraryEnabled && filter === 'trashed'
|
||||||
const showNewProjectButton =
|
const showNewProjectButton =
|
||||||
!isLibraryEnabled ||
|
!isLibraryEnabled ||
|
||||||
(filter !== 'shared' && filter !== 'archived' && filter !== 'trashed')
|
(filter !== 'shared' && filter !== 'archived' && filter !== 'trashed')
|
||||||
@@ -84,7 +87,10 @@ export function ProjectListDsNav() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<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 />
|
<SystemMessages />
|
||||||
<DefaultNavbar
|
<DefaultNavbar
|
||||||
@@ -103,25 +109,38 @@ export function ProjectListDsNav() {
|
|||||||
<UserNotifications />
|
<UserNotifications />
|
||||||
<main aria-labelledby="main-content">
|
<main aria-labelledby="main-content">
|
||||||
<div className="project-list-header-row">
|
<div className="project-list-header-row">
|
||||||
<ProjectListTitle
|
{showTrashHeader ? (
|
||||||
filter={filter}
|
<h1
|
||||||
selectedTag={selectedTag}
|
id="main-content"
|
||||||
selectedTagId={selectedTagId}
|
tabIndex={-1}
|
||||||
className="text-truncate d-none d-md-block"
|
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="project-tools">
|
||||||
<div className="d-none d-md-block">
|
<div className="d-none d-md-block">
|
||||||
{selectedProjects.length === 0 ? (
|
{selectedProjects.length === 0 ? (
|
||||||
<CurrentPlanWidget />
|
showTrashHeader ? null : (
|
||||||
|
<CurrentPlanWidget />
|
||||||
|
)
|
||||||
) : (
|
) : (
|
||||||
<ProjectTools />
|
<ProjectTools />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="d-md-none">
|
<div className="d-md-none">
|
||||||
<CurrentPlanWidget />
|
{!showTrashHeader && <CurrentPlanWidget />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{showTrashHeader && <TrashPageTabs activeTab="projects" />}
|
||||||
<div className="project-ds-nav-project-list">
|
<div className="project-ds-nav-project-list">
|
||||||
<OLRow className="d-none d-md-flex align-items-center">
|
<OLRow className="d-none d-md-flex align-items-center">
|
||||||
<OLCol md={isLibraryEnabled ? 8 : undefined} lg={7}>
|
<OLCol md={isLibraryEnabled ? 8 : undefined} lg={7}>
|
||||||
|
|||||||
+3
-1
@@ -52,7 +52,9 @@ function SidebarDsNav({
|
|||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{isLibraryEnabled && activePage === 'library' && <DsNavOverleafLogo />}
|
{isLibraryEnabled && (activePage === 'library' || isTrashActive) && (
|
||||||
|
<DsNavOverleafLogo />
|
||||||
|
)}
|
||||||
<nav
|
<nav
|
||||||
className="flex-grow flex-shrink"
|
className="flex-grow flex-shrink"
|
||||||
aria-label={t('project_categories_tags')}
|
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 sidebar shows the logo and account/help menus from md upwards, so
|
||||||
// the top navbar is only needed on mobile.
|
// the top navbar is only needed on mobile.
|
||||||
.navbar-default {
|
.navbar-default {
|
||||||
@@ -164,6 +164,19 @@ body {
|
|||||||
border-top: none;
|
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 {
|
.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;
|
min-height: 36px;
|
||||||
|
|
||||||
.project-list-title {
|
.project-list-title {
|
||||||
@include heading-sm;
|
@include heading-md;
|
||||||
|
|
||||||
color: var(--content-secondary-themed);
|
color: var(--content-secondary-themed);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
|||||||
@@ -2143,6 +2143,7 @@
|
|||||||
"reference_sync": "Reference manager sync",
|
"reference_sync": "Reference manager sync",
|
||||||
"references": "References",
|
"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_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": "Refresh",
|
||||||
"refresh_page_after_linking_dropbox": "Please refresh this page after linking your account to Dropbox.",
|
"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.",
|
"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_library": "Search in your library",
|
||||||
"search_in_shared_projects": "Search in projects shared with you",
|
"search_in_shared_projects": "Search in projects shared with you",
|
||||||
"search_in_trashed_projects": "Search in trashed projects",
|
"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_in_your_projects": "Search in your projects",
|
||||||
"search_match_case": "Match case",
|
"search_match_case": "Match case",
|
||||||
"search_members": "Search members",
|
"search_members": "Search members",
|
||||||
@@ -2875,7 +2877,6 @@
|
|||||||
"trash_projects": "Trash Projects",
|
"trash_projects": "Trash Projects",
|
||||||
"trashed": "Trashed",
|
"trashed": "Trashed",
|
||||||
"trashed_projects": "Trashed projects",
|
"trashed_projects": "Trashed projects",
|
||||||
"trashed_references": "Trashed references",
|
|
||||||
"trashing_projects_wont_affect_collaborators": "Trashing projects won’t affect your collaborators.",
|
"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_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",
|
"trial_remaining_days": "__days__ more days on your <b>Overleaf Premium</b> trial",
|
||||||
|
|||||||
Reference in New Issue
Block a user