diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json
index 293df8843f..8a0dc6128e 100644
--- a/services/web/frontend/extracted-translations.json
+++ b/services/web/frontend/extracted-translations.json
@@ -96,6 +96,7 @@
"add_ons": "",
"add_or_remove_project_from_tag": "",
"add_reference": "",
+ "add_references": "",
"add_role_and_department": "",
"add_to_dictionary": "",
"add_to_tag": "",
@@ -1157,6 +1158,7 @@
"manage_template": "",
"manage_users_subtext": "",
"manage_your_ai_assist_add_on": "",
+ "manage_your_references_across_projects": "",
"managed": "",
"managed_user_accounts": "",
"managed_user_invite_has_been_sent_to_email": "",
@@ -1910,6 +1912,7 @@
"start_typing_find_your_company": "",
"start_typing_find_your_organization": "",
"start_typing_find_your_university": "",
+ "start_your_collection_of_most_used_references": "",
"stop": "",
"stop_compile": "",
"stop_on_first_error": "",
diff --git a/services/web/frontend/js/features/project-list/components/dropdown/mobile-page-switcher-items.tsx b/services/web/frontend/js/features/project-list/components/dropdown/mobile-page-switcher-items.tsx
new file mode 100644
index 0000000000..959822ae64
--- /dev/null
+++ b/services/web/frontend/js/features/project-list/components/dropdown/mobile-page-switcher-items.tsx
@@ -0,0 +1,42 @@
+import { useTranslation } from 'react-i18next'
+import { DropdownItem } from '@/shared/components/dropdown/dropdown-menu'
+
+type Props = {
+ activePage: 'library' | 'projects'
+ onProjectsClick?: () => void
+}
+
+function MobilePageSwitcherItems({ activePage, onProjectsClick }: Props) {
+ const { t } = useTranslation()
+
+ return (
+ <>
+
+
+ {t('library')}
+
+
+
+ {onProjectsClick ? (
+ {
+ e.stopPropagation()
+ onProjectsClick()
+ }}
+ >
+ {t('projects')}
+
+ ) : (
+
+ {t('projects')}
+
+ )}
+
+ >
+ )
+}
+
+export default MobilePageSwitcherItems
diff --git a/services/web/frontend/js/features/project-list/components/dropdown/projects-dropdown.tsx b/services/web/frontend/js/features/project-list/components/dropdown/projects-dropdown.tsx
index 273c50ced6..571eb1dab3 100644
--- a/services/web/frontend/js/features/project-list/components/dropdown/projects-dropdown.tsx
+++ b/services/web/frontend/js/features/project-list/components/dropdown/projects-dropdown.tsx
@@ -12,8 +12,11 @@ import {
DropdownMenu,
DropdownToggle,
} from '@/shared/components/dropdown/dropdown-menu'
+import MaterialIcon from '@/shared/components/material-icon'
+import { isSplitTestEnabled } from '@/utils/splitTestUtils'
import ProjectsFilterMenu from '../projects-filter-menu'
import TagsList from '../tags-list'
+import MobilePageSwitcherItems from './mobile-page-switcher-items'
type ItemProps = {
filter: Filter
@@ -48,7 +51,9 @@ export function Item({ filter, text, onClick }: ItemProps) {
function ProjectsDropdown() {
const { t } = useTranslation()
const [title, setTitle] = useState(() => t('all_projects'))
+ const [view, setView] = useState<'top' | 'submenu'>('submenu')
const { filter, selectedTagId, tags } = useProjectListContext()
+ const isLibraryEnabled = isSplitTestEnabled('overleaf-library')
const filterTranslations = useRef>({
all: t('all_projects'),
owned: t('your_projects'),
@@ -73,8 +78,40 @@ function ProjectsDropdown() {
}
}, [filter, tags, selectedTagId, t])
+ const submenuItems = (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {t('tags')}:
+
+ >
+ )
+
return (
-
+ {
+ if (!show) {
+ setView('submenu')
+ }
+ }
+ : undefined
+ }
+ >
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {t('tags')}:
-
+ {!isLibraryEnabled && submenuItems}
+ {isLibraryEnabled && view === 'submenu' && (
+ <>
+
+ }
+ aria-label={t('back')}
+ onClick={e => {
+ e.stopPropagation()
+ setView('top')
+ }}
+ >
+ {t('projects')}
+
+
+ {submenuItems}
+ >
+ )}
+ {isLibraryEnabled && view === 'top' && (
+ setView('submenu')}
+ />
+ )}
)
diff --git a/services/web/frontend/js/features/project-list/components/new-project-button.tsx b/services/web/frontend/js/features/project-list/components/new-project-button.tsx
index 86731f1df7..6f34d3049e 100644
--- a/services/web/frontend/js/features/project-list/components/new-project-button.tsx
+++ b/services/web/frontend/js/features/project-list/components/new-project-button.tsx
@@ -21,6 +21,8 @@ import { useSendProjectListMB } from '@/features/project-list/components/project
import type { PortalTemplate } from '../../../../../types/portal-template'
import { useFeatureFlag } from '@/shared/context/split-test-context'
import MaterialIcon from '@/shared/components/material-icon'
+import { useProjectListContext } from '@/features/project-list/context/project-list-context'
+import { isSplitTestEnabled } from '@/utils/splitTestUtils'
type SendTrackingEvent = {
dropdownMenu: string
@@ -65,6 +67,12 @@ function NewProjectButton({
const markdownImportEnabled =
useFeatureFlag('import-markdown') &&
getMeta('ol-ExposedSettings').enablePandocConversions
+ const { selectedTagId, tags } = useProjectListContext()
+ const isLibraryEnabled = isSplitTestEnabled('overleaf-library')
+ const initialTags =
+ isLibraryEnabled && selectedTagId
+ ? tags.filter(tag => tag._id === selectedTagId)
+ : []
const sendTrackingEvent = useCallback(
({
dropdownMenu,
@@ -310,7 +318,11 @@ function NewProjectButton({
) : null}
- setModal(null)} />
+ setModal(null)}
+ initialTags={initialTags}
+ />
>
)
}
diff --git a/services/web/frontend/js/features/project-list/components/new-project-button/blank-project-modal.tsx b/services/web/frontend/js/features/project-list/components/new-project-button/blank-project-modal.tsx
index 5943bf7baf..dd1ae3ca0e 100644
--- a/services/web/frontend/js/features/project-list/components/new-project-button/blank-project-modal.tsx
+++ b/services/web/frontend/js/features/project-list/components/new-project-button/blank-project-modal.tsx
@@ -1,11 +1,13 @@
import ModalContentNewProjectForm from './modal-content-new-project-form'
import { OLModal } from '@/shared/components/ol/ol-modal'
+import { Tag } from '../../../../../../app/src/Features/Tags/types'
type BlankProjectModalProps = {
onHide: () => void
+ initialTags?: Tag[]
}
-function BlankProjectModal({ onHide }: BlankProjectModalProps) {
+function BlankProjectModal({ onHide, initialTags }: BlankProjectModalProps) {
return (
-
+
)
}
diff --git a/services/web/frontend/js/features/project-list/components/new-project-button/example-project-modal.tsx b/services/web/frontend/js/features/project-list/components/new-project-button/example-project-modal.tsx
index aacd9c05d2..3989883b6b 100644
--- a/services/web/frontend/js/features/project-list/components/new-project-button/example-project-modal.tsx
+++ b/services/web/frontend/js/features/project-list/components/new-project-button/example-project-modal.tsx
@@ -1,11 +1,16 @@
import { OLModal } from '@/shared/components/ol/ol-modal'
import ModalContentNewProjectForm from './modal-content-new-project-form'
+import { Tag } from '../../../../../../app/src/Features/Tags/types'
type ExampleProjectModalProps = {
onHide: () => void
+ initialTags?: Tag[]
}
-function ExampleProjectModal({ onHide }: ExampleProjectModalProps) {
+function ExampleProjectModal({
+ onHide,
+ initialTags,
+}: ExampleProjectModalProps) {
return (
-
+
)
}
diff --git a/services/web/frontend/js/features/project-list/components/new-project-button/modal-content-new-project-form.tsx b/services/web/frontend/js/features/project-list/components/new-project-button/modal-content-new-project-form.tsx
index 048016c3bc..db191eacb6 100644
--- a/services/web/frontend/js/features/project-list/components/new-project-button/modal-content-new-project-form.tsx
+++ b/services/web/frontend/js/features/project-list/components/new-project-button/modal-content-new-project-form.tsx
@@ -1,4 +1,4 @@
-import React, { useState } from 'react'
+import React, { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import useAsync from '../../../../shared/hooks/use-async'
import {
@@ -19,6 +19,10 @@ import OLButton from '@/shared/components/ol/ol-button'
import OLForm from '@/shared/components/ol/ol-form'
import OLFormLabel from '@/shared/components/ol/ol-form-label'
import OLFormGroup from '@/shared/components/ol/ol-form-group'
+import { CloneProjectTag } from '@/features/clone-project-modal/components/clone-project-tag'
+import { addProjectsToTag } from '@/features/project-list/util/api'
+import { captureException } from '@/infrastructure/error-reporter'
+import { Tag } from '../../../../../../app/src/Features/Tags/types'
type NewProjectData = {
project_id: string
@@ -34,16 +38,26 @@ type NewProjectData = {
type Props = {
onCancel: () => void
template?: string
+ initialTags?: Tag[]
}
-function ModalContentNewProjectForm({ onCancel, template = 'none' }: Props) {
+function ModalContentNewProjectForm({
+ onCancel,
+ template = 'none',
+ initialTags = [],
+}: Props) {
const { t } = useTranslation()
const { autoFocusedRef } = useRefWithAutoFocus()
const [projectName, setProjectName] = useState('')
+ const [projectTags, setProjectTags] = useState(initialTags)
const [redirecting, setRedirecting] = useState(false)
const { isLoading, isError, error, runAsync } = useAsync()
const location = useLocation()
+ const removeTag = useCallback((tag: Tag) => {
+ setProjectTags(value => value.filter(item => item._id !== tag._id))
+ }, [])
+
const createNewProject = () => {
runAsync(
postJSON('/project/new', {
@@ -53,10 +67,17 @@ function ModalContentNewProjectForm({ onCancel, template = 'none' }: Props) {
},
})
)
- .then(data => {
+ .then(async data => {
if (data.project_id) {
// prevents clicking on create again between async load of next page and pending state being finished
setRedirecting(true)
+ for (const tag of projectTags) {
+ try {
+ await addProjectsToTag(tag._id, [data.project_id])
+ } catch (err) {
+ captureException(err as Error)
+ }
+ }
location.assign(`/project/${data.project_id}`)
}
})
@@ -97,6 +118,21 @@ function ModalContentNewProjectForm({ onCancel, template = 'none' }: Props) {
value={projectName}
/>
+
+ {projectTags.length > 0 && (
+
+ {t('tags')}:
+
+ {projectTags.map(tag => (
+
+ ))}
+
+
+ )}
diff --git a/services/web/frontend/js/features/project-list/components/new-project-button/new-project-button-modal.tsx b/services/web/frontend/js/features/project-list/components/new-project-button/new-project-button-modal.tsx
index bfac2356c1..6f9f273a3a 100644
--- a/services/web/frontend/js/features/project-list/components/new-project-button/new-project-button-modal.tsx
+++ b/services/web/frontend/js/features/project-list/components/new-project-button/new-project-button-modal.tsx
@@ -5,6 +5,7 @@ import { JSXElementConstructor, lazy, Suspense, useCallback } from 'react'
import { Nullable } from '../../../../../../types/utils'
import { FullSizeLoadingSpinner } from '@/shared/components/loading-spinner'
import { useLocation } from '@/shared/hooks/use-location'
+import { Tag } from '../../../../../../app/src/Features/Tags/types'
const UploadProjectModal = lazy(() => import('./upload-project-modal'))
const ImportDocumentModal = lazy(() => import('./import-document-modal'))
@@ -20,9 +21,14 @@ export type NewProjectButtonModalVariant =
type NewProjectButtonModalProps = {
modal: Nullable
onHide: () => void
+ initialTags?: Tag[]
}
-function NewProjectButtonModal({ modal, onHide }: NewProjectButtonModalProps) {
+function NewProjectButtonModal({
+ modal,
+ onHide,
+ initialTags,
+}: NewProjectButtonModalProps) {
const [importProjectFromGithubModalWrapper] = importOverleafModules(
'importProjectFromGithubModalWrapper'
)
@@ -45,9 +51,9 @@ function NewProjectButtonModal({ modal, onHide }: NewProjectButtonModalProps) {
switch (modal) {
case 'blank_project':
- return
+ return
case 'example_project':
- return
+ return
case 'upload_project':
return (
}>
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 b0d7e02f06..a558bdd6ac 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
@@ -42,6 +42,9 @@ export function ProjectListDsNav() {
const isLibraryEnabled = isSplitTestEnabled('overleaf-library')
const selectedTag = tags.find(tag => tag._id === selectedTagId)
+ const showNewProjectButton =
+ !isLibraryEnabled ||
+ (filter !== 'shared' && filter !== 'archived' && filter !== 'trashed')
const tableTopArea = (
@@ -54,10 +57,12 @@ export function ProjectListDsNav() {
selectedTag={selectedTag}
className="overflow-hidden flex-grow-1"
/>
-
+ {showNewProjectButton && (
+
+ )}
>
) : (
<>
@@ -127,7 +132,7 @@ export function ProjectListDsNav() {
selectedTag={selectedTag}
/>
- {isLibraryEnabled && (
+ {isLibraryEnabled && showNewProjectButton && (
- {!isLibraryEnabled && (
-
- )}
+
diff --git a/services/web/frontend/js/shared/components/sidebar/ds-nav-page-switcher.tsx b/services/web/frontend/js/shared/components/sidebar/ds-nav-page-switcher.tsx
index 30b60cc3bf..2623dc9863 100644
--- a/services/web/frontend/js/shared/components/sidebar/ds-nav-page-switcher.tsx
+++ b/services/web/frontend/js/shared/components/sidebar/ds-nav-page-switcher.tsx
@@ -1,6 +1,7 @@
import { useTranslation } from 'react-i18next'
import { BookBookmark, Folder } from '@phosphor-icons/react'
import { useActiveOverallTheme } from '@/shared/hooks/use-active-overall-theme'
+import getMeta from '@/utils/meta'
import overleafLogo from '@/shared/svgs/overleaf-a-ds-solution-mallard.svg'
import overleafLogoDark from '@/shared/svgs/overleaf-a-ds-solution-mallard-dark.svg'
@@ -18,20 +19,23 @@ export function DsNavPageSwitcher({
onProjectsClick?: React.MouseEventHandler
}) {
const { t } = useTranslation()
+ const appName = getMeta('ol-ExposedSettings')?.appName ?? 'Overleaf'
const activeOverallTheme = useActiveOverallTheme()
return (
<>
{showLogo && (
)}
li > button {
+ padding-left: calc(var(--spacing-05) + 24px + var(--spacing-05));
+ }
+
+ .dropdown-header {
+ padding-left: calc(var(--spacing-06) + 24px + var(--spacing-05));
+ }
+
+ > li.tag button.tag-name {
+ padding-left: calc(var(--spacing-06) + 24px + var(--spacing-05));
+ }
}
}
}
diff --git a/services/web/locales/en.json b/services/web/locales/en.json
index bda2e42504..04103df886 100644
--- a/services/web/locales/en.json
+++ b/services/web/locales/en.json
@@ -113,6 +113,7 @@
"add_ons_for_any_plan_subheading": "Buy add-ons for any Overleaf plan (including the free plan) to unlock additional features.",
"add_or_remove_project_from_tag": "Add or remove project from tag __tagName__",
"add_reference": "Add reference",
+ "add_references": "Add references",
"add_role_and_department": "Add role and department",
"add_to_dictionary": "Add to dictionary",
"add_to_tag": "Add to tag",
@@ -1519,6 +1520,7 @@
"manage_template": "Manage template",
"manage_users_subtext": "Add or remove members and assign roles",
"manage_your_ai_assist_add_on": "Manage your AI Assist add-on",
+ "manage_your_references_across_projects": "Manage your references across projects.",
"managed": "Managed",
"managed_user_accounts": "Managed user accounts",
"managed_user_accounts_explanation": "Provides tighter management of user access and deletion and allows organizations to keep control of projects when someone leaves the organization",
@@ -2477,6 +2479,7 @@
"start_typing_find_your_company": " Start typing to find your company",
"start_typing_find_your_organization": "Start typing to find your organization",
"start_typing_find_your_university": "Start typing to find your university",
+ "start_your_collection_of_most_used_references": "Start your collection of most-used references in the Library, so you can easily add them to any project.",
"state": "State",
"status_checks": "Status Checks",
"still_have_questions": "Still have questions?",
diff --git a/services/web/test/frontend/features/project-list/components/new-project-button/modal-content-new-project-form.test.tsx b/services/web/test/frontend/features/project-list/components/new-project-button/modal-content-new-project-form.test.tsx
index e1c97bb914..252877e772 100644
--- a/services/web/test/frontend/features/project-list/components/new-project-button/modal-content-new-project-form.test.tsx
+++ b/services/web/test/frontend/features/project-list/components/new-project-button/modal-content-new-project-form.test.tsx
@@ -4,6 +4,7 @@ import fetchMock from 'fetch-mock'
import sinon from 'sinon'
import ModalContentNewProjectForm from '../../../../../../frontend/js/features/project-list/components/new-project-button/modal-content-new-project-form'
import { location } from '@/shared/components/location'
+import * as projectListApi from '@/features/project-list/util/api'
describe('', function () {
beforeEach(function () {
@@ -51,6 +52,45 @@ describe('', function () {
sinon.assert.calledWith(assignStub, `/project/${projectId}`)
})
+ it('redirects even when adding tags fails', async function () {
+ const projectId = 'ab123'
+
+ fetchMock.post('/project/new', {
+ status: 200,
+ body: {
+ project_id: projectId,
+ },
+ })
+
+ const addProjectsToTagStub = this.locationWrapperSandbox
+ .stub(projectListApi, 'addProjectsToTag')
+ .rejects(new Error('tag add failed'))
+
+ render(
+ {}}
+ initialTags={[{ _id: 'tag-1', user_id: 'user-1', name: 'Tag 1' }]}
+ />
+ )
+
+ fireEvent.change(screen.getByLabelText('Project name'), {
+ target: { value: 'Test Name' },
+ })
+
+ fireEvent.click(
+ screen.getByRole('button', {
+ name: 'Create',
+ })
+ )
+
+ const assignStub = this.locationWrapperStub.assign
+ await waitFor(() => {
+ sinon.assert.calledOnce(assignStub)
+ })
+ sinon.assert.calledWith(assignStub, `/project/${projectId}`)
+ sinon.assert.calledWith(addProjectsToTagStub, 'tag-1', [projectId])
+ })
+
it('shows error when project name contains "/"', async function () {
const errorMessage = 'Project name cannot contain / characters'