Merge pull request #35906 from overleaf/dk-library-references-trashed

Soft-delete library references (move to trash)

GitOrigin-RevId: 908a4d41724300c70d56e850cdb519b7a274ad5a
This commit is contained in:
David
2026-09-17 11:41:40 +00:00
committed by Copybot
parent 03cbfbe90f
commit ea989a4788
5 changed files with 117 additions and 3 deletions
@@ -24,6 +24,7 @@
"about_to_archive_projects": "",
"about_to_delete_cert": "",
"about_to_delete_projects": "",
"about_to_delete_reference": "",
"about_to_delete_tag": "",
"about_to_delete_the_following_project": "",
"about_to_delete_the_following_projects": "",
@@ -457,6 +458,7 @@
"delete_message_confirmation": "",
"delete_permanently": "",
"delete_projects": "",
"delete_reference": "",
"delete_row_or_column": "",
"delete_sso_config": "",
"delete_table": "",
@@ -469,6 +471,7 @@
"deleted_by_id": "",
"deleted_by_ip": "",
"deleted_by_on": "",
"deleted_references_kept_in_trash_for_30_days": "",
"deleted_user": "",
"deleting": "",
"demonstrating_git_integration": "",
@@ -1345,6 +1348,7 @@
"no_selection_select_file": "",
"no_symbols_found": "",
"no_thanks_cancel_now": "",
"no_trashed_references": "",
"non_blinking_cursor": "",
"normal": "",
"normally_x_price_per_month": "",
@@ -1635,6 +1639,7 @@
"reference_manager_searched_groups": "",
"reference_manager_unlinked": "",
"reference_managers": "",
"reference_moved_to_trash": "",
"reference_search": "",
"reference_search_setting": "",
"reference_search_settings": "",
@@ -2251,11 +2256,13 @@
"trash_projects": "",
"trashed": "",
"trashed_projects": "",
"trashed_references": "",
"trashing_projects_wont_affect_collaborators": "",
"trial_last_day": "",
"trial_remaining_days": "",
"tried_to_log_in_with_email": "",
"tried_to_register_with_email": "",
"try_a_different_term": "",
"try_a_different_term_or_add_a_new_reference": "",
"try_again": "",
"try_for_free": "",
@@ -2418,6 +2425,7 @@
"view_options": "",
"view_payment_portal": "",
"view_payment_portal_disclaimer": "",
"view_trash": "",
"view_your_invoices": "",
"viewer": "",
"viewing": "",
@@ -15,12 +15,26 @@ import { useProjectListContext } from '@/features/project-list/context/project-l
import importOverleafModules from '../../../../../macros/import-overleaf-module.macro'
import { ActivePage } from '../../util/navigation-state'
function SidebarDsNav({ activePage }: { activePage: ActivePage }) {
function SidebarDsNav({
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 { show: showAddAffiliationWidget } = useAddAffiliation()
const isLibraryEnabled = isSplitTestEnabled('overleaf-library')
const { filter, selectedTagId, selectFilter } = useProjectListContext()
const isTrashActive = selectedTagId === undefined && filter === 'trashed'
const isTrashActive =
activePage === 'library'
? trashActive
: selectedTagId === undefined && filter === 'trashed'
const [dsNavLibraryLinkModule] = importOverleafModules('dsNavLibraryLink')
const DsNavLibraryLink: JSXElementConstructor<{ active?: boolean }> =
dsNavLibraryLinkModule?.import.default
@@ -69,7 +83,9 @@ function SidebarDsNav({ activePage }: { activePage: ActivePage }) {
{isLibraryEnabled && (
<>
{DsNavLibraryLink && (
<DsNavLibraryLink active={activePage === 'library'} />
<DsNavLibraryLink
active={activePage === 'library' && !trashActive}
/>
)}
<button
type="button"
+1
View File
@@ -211,6 +211,7 @@ export interface Meta {
'ol-languages': SpellCheckLanguage[]
'ol-learnedWords': string[]
'ol-legacyEditorThemes': { name: string; dark: boolean }[]
'ol-libraryView': 'library' | 'trashed'
'ol-licenseQuantity'?: number
'ol-loadingText': string
'ol-localIndividualPlans': LocalIndividualPlans
+9
View File
@@ -28,6 +28,7 @@
"about_to_archive_projects": "You are about to archive the following projects:",
"about_to_delete_cert": "You are about to delete the following certificate:",
"about_to_delete_projects": "You are about to delete the following projects:",
"about_to_delete_reference": "You are about to delete the following reference:",
"about_to_delete_tag": "You are about to delete the following tag (any projects in them will not be deleted):",
"about_to_delete_the_following_project": "You are about to delete the following project",
"about_to_delete_the_following_projects": "You are about to delete the following projects",
@@ -605,6 +606,7 @@
"delete_message_confirmation": "Are you sure you want to delete this message? This can’t be undone.",
"delete_permanently": "Delete permanently",
"delete_projects": "Delete Projects",
"delete_reference": "Delete Reference",
"delete_row_or_column": "Delete row or column",
"delete_sso_config": "Delete SSO configuration",
"delete_table": "Delete table",
@@ -617,6 +619,7 @@
"deleted_by_id": "Deleted By ID",
"deleted_by_ip": "Deleted By IP",
"deleted_by_on": "Deleted by __name__ on __date__",
"deleted_references_kept_in_trash_for_30_days": "References you delete will be kept here for 30 days before being permanently removed.",
"deleted_user": "Deleted user",
"deleting": "Deleting",
"demonstrating_git_integration": "Demonstrating Git integration",
@@ -1738,6 +1741,7 @@
"no_selection_select_file": "Currently, no file is selected. Please select a file from the file tree.",
"no_symbols_found": "No symbols found",
"no_thanks_cancel_now": "No thanks, I still want to cancel",
"no_trashed_references": "No references in Trash",
"no_update_email": "No, update email",
"non_blinking_cursor": "Non-blinking cursor",
"non_deletable_entity": "The specified entity may not be deleted",
@@ -2110,6 +2114,7 @@
"reconnect": "Try again",
"reconnecting": "Reconnecting",
"reconnecting_in_x_secs": "Reconnecting in __seconds__ secs",
"recover": "Recover",
"recurly_email_update_needed": "Your billing email address is currently <0>__recurlyEmail__</0>. If needed you can update your billing address to <1>__userEmail__</1>.",
"recurly_email_updated": "Your billing email address was successfully updated",
"redirect_to_editor": "Redirect to editor",
@@ -2129,6 +2134,7 @@
"reference_manager_searched_groups": "__provider__ search groups",
"reference_manager_unlinked": "Reference manager unlinked",
"reference_managers": "Reference managers",
"reference_moved_to_trash": "__citeKey__ moved to Trash",
"reference_search": "Advanced reference search",
"reference_search_info_new": "Find your references easily—search by author, title, year, or journal.",
"reference_search_setting": "Reference search",
@@ -2869,11 +2875,13 @@
"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",
"tried_to_log_in_with_email": "You’ve tried to log in with <b>__email__</b>.",
"tried_to_register_with_email": "You’ve tried to register with <b>__email__</b>, which is already registered with <b>__appName__</b> as an institutional account.",
"try_a_different_term": "Try a different term.",
"try_a_different_term_or_add_a_new_reference": "Try a different term, or add a new reference.",
"try_again": "Please try again",
"try_for_free": "Try for free",
@@ -3064,6 +3072,7 @@
"view_payment_portal_disclaimer": "You might have older invoices prior to February 2026. Please <0>contact support</0> if you need to get access to these.",
"view_pdf": "View PDF",
"view_source": "View Source",
"view_trash": "View Trash",
"view_your_invoices": "View your invoices",
"viewer": "Viewer",
"viewing": "Viewing",
@@ -0,0 +1,80 @@
import { screen, waitFor } from '@testing-library/react'
import { expect } from 'chai'
import fetchMock from 'fetch-mock'
import SidebarDsNav from '../../../../../../frontend/js/features/project-list/components/sidebar/sidebar-ds-nav'
import {
renderWithProjectListContext,
resetProjectListContextFetch,
} from '../../helpers/render-with-context'
import { UserSettingsProvider } from '@/shared/context/user-settings-context'
async function renderSidebar(props: {
activePage: 'library' | 'projects'
trashActive?: boolean
}) {
renderWithProjectListContext(
<UserSettingsProvider>
<SidebarDsNav {...props} />
</UserSettingsProvider>
)
await waitFor(
() => expect(fetchMock.callHistory.called('/api/project')).to.be.true
)
}
describe('<SidebarDsNav />', function () {
beforeEach(function () {
global.localStorage.clear()
window.metaAttributesCache.set('ol-tags', [])
window.metaAttributesCache.set('ol-ExposedSettings', { isOverleaf: true })
window.metaAttributesCache.set('ol-splitTestVariants', {
'overleaf-library': 'enabled',
})
window.metaAttributesCache.set('ol-userSettings', {})
window.metaAttributesCache.set('ol-navbar', {
sessionUser: { email: 'fake@example.com' },
showSubscriptionLink: false,
items: [],
})
})
afterEach(function () {
resetProjectListContextFetch()
window.history.replaceState(null, '', '/project')
})
it('marks trash as the current page and not the library link', async function () {
await renderSidebar({ activePage: 'library', trashActive: true })
expect(
screen.getByRole('button', { name: 'Trash' }).getAttribute('aria-current')
).to.equal('page')
expect(
screen.getByRole('link', { name: 'Library' }).getAttribute('aria-current')
).to.be.null
})
it('marks the library link as the current page on the library view', async function () {
await renderSidebar({ activePage: 'library' })
expect(
screen.getByRole('link', { name: 'Library' }).getAttribute('aria-current')
).to.equal('page')
expect(
screen.getByRole('button', { name: 'Trash' }).getAttribute('aria-current')
).to.be.null
})
it('ignores the project trash filter on the library view', async function () {
window.history.replaceState(null, '', '/project/trashed')
await renderSidebar({ activePage: 'library' })
expect(
screen.getByRole('link', { name: 'Library' }).getAttribute('aria-current')
).to.equal('page')
expect(
screen.getByRole('button', { name: 'Trash' }).getAttribute('aria-current')
).to.be.null
})
})