diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json
index 65f8c170e2..ac468efe00 100644
--- a/services/web/frontend/extracted-translations.json
+++ b/services/web/frontend/extracted-translations.json
@@ -1641,6 +1641,7 @@
"session_error": "",
"session_expired_redirecting_to_login": "",
"sessions": "",
+ "set_as_main_document": "",
"set_color": "",
"set_column_width": "",
"set_up_single_sign_on": "",
@@ -2260,6 +2261,7 @@
"you_are_on_x_plan_as_member_of_group_subscription_y_administered_by_z": "",
"you_are_using_your_organization_email_x_would_like_you_to_take_action": "",
"you_can_also_choose_to_view_anonymously_or_leave_the_project": "",
+ "you_can_also_right_click_a_file_to_set_it_as_main": "",
"you_can_buy_this_plan_but_not_as_a_trial": "",
"you_can_manage_your_reference_manager_integrations_from_your_account_settings_page": "",
"you_can_now_enable_sso": "",
diff --git a/services/web/frontend/js/features/file-tree/components/file-tree-item/file-tree-item-menu-items.tsx b/services/web/frontend/js/features/file-tree/components/file-tree-item/file-tree-item-menu-items.tsx
index 0bc02dfe99..3ffdbe7642 100644
--- a/services/web/frontend/js/features/file-tree/components/file-tree-item/file-tree-item-menu-items.tsx
+++ b/services/web/frontend/js/features/file-tree/components/file-tree-item/file-tree-item-menu-items.tsx
@@ -23,6 +23,8 @@ function FileTreeItemMenuItems() {
startUploadingDocOrFile,
downloadPath,
selectedFileName,
+ canSetRootDocId,
+ setRootDocId,
} = useFileTreeActionable()
const { project } = useProjectContext()
@@ -63,10 +65,23 @@ function FileTreeItemMenuItems() {
) : null}
+ {canSetRootDocId ? (
+ <>
+
+
+
+ {t('set_as_main_document')}
+
+
+ >
+ ) : null}
{canDelete ? (
-
- {t('delete')}
-
+ <>
+
+
+ {t('delete')}
+
+ >
) : null}
{canCreate ? (
<>
diff --git a/services/web/frontend/js/features/file-tree/contexts/file-tree-actionable.tsx b/services/web/frontend/js/features/file-tree/contexts/file-tree-actionable.tsx
index b9688edc65..5fcabe1653 100644
--- a/services/web/frontend/js/features/file-tree/contexts/file-tree-actionable.tsx
+++ b/services/web/frontend/js/features/file-tree/contexts/file-tree-actionable.tsx
@@ -19,6 +19,7 @@ import {
NewDocEntity,
NewLinkedFileEntity,
NewEntity,
+ syncRootDocId,
} from '../util/sync-mutation'
import { findInTree, findInTreeOrThrow } from '../util/find-in-tree'
import { isNameUniqueInFolder } from '../util/is-name-unique-in-folder'
@@ -39,6 +40,7 @@ import { useReferencesContext } from '@/features/ide-react/context/references-co
import { usePermissionsContext } from '@/features/ide-react/context/permissions-context'
import { FileTreeEntity } from '@ol-types/file-tree-entity'
import { Doc } from '@ol-types/doc'
+import { isValidTeXFile } from '@/main/is-valid-tex-file'
type DroppedFile = File & {
relativePath?: string
@@ -87,6 +89,8 @@ const FileTreeActionableContext = createContext<
droppedFiles: { files: File[]; targetFolderId: string } | null
setDroppedFiles: (value: DroppedFiles | null) => void
downloadPath?: string
+ canSetRootDocId: boolean
+ setRootDocId: () => Promise
}
| undefined
>(undefined)
@@ -230,11 +234,13 @@ function fileTreeActionableReducer(state: State, action: Action) {
export const FileTreeActionableProvider: FC = ({
children,
}) => {
- const { projectId } = useProjectContext()
+ const { projectId, project, updateProject } = useProjectContext()
const { fileTreeReadOnly } = useFileTreeData()
const { indexAllReferences } = useReferencesContext()
const { write } = usePermissionsContext()
+ const rootDocId = project?.rootDocId
+
const [state, dispatch] = useReducer(
fileTreeReadOnly
? fileTreeActionableReadOnlyReducer
@@ -522,6 +528,34 @@ export const FileTreeActionableProvider: FC = ({
}
}, [fileTreeData, projectId, selectedEntityIds])
+ const canSetRootDocId = useMemo(() => {
+ // must have write permission on the project
+ if (!write) {
+ return false
+ }
+
+ // must be only one file selected
+ if (!selectedFileName) {
+ return false
+ }
+
+ // must not already be the root doc
+ if (rootDocId && selectedEntityIds.has(rootDocId)) {
+ return false
+ }
+
+ // must have a valid root doc extension
+ return isValidTeXFile(selectedFileName)
+ }, [rootDocId, selectedEntityIds, selectedFileName, write])
+
+ const setRootDocId = useCallback(async () => {
+ const [selectedEntityId] = selectedEntityIds
+
+ await syncRootDocId(projectId, selectedEntityId)
+
+ updateProject({ rootDocId: selectedEntityId })
+ }, [projectId, selectedEntityIds, updateProject])
+
const value = useMemo(
() => ({
canDelete: write && selectedEntityIds.size > 0 && !isRootFolderSelected,
@@ -549,6 +583,8 @@ export const FileTreeActionableProvider: FC = ({
droppedFiles,
setDroppedFiles,
downloadPath,
+ canSetRootDocId,
+ setRootDocId,
}),
[
cancel,
@@ -573,6 +609,8 @@ export const FileTreeActionableProvider: FC = ({
startUploadingDocOrFile,
state,
write,
+ canSetRootDocId,
+ setRootDocId,
]
)
diff --git a/services/web/frontend/js/features/file-tree/util/sync-mutation.ts b/services/web/frontend/js/features/file-tree/util/sync-mutation.ts
index 1c10ef4961..6bacb6eb72 100644
--- a/services/web/frontend/js/features/file-tree/util/sync-mutation.ts
+++ b/services/web/frontend/js/features/file-tree/util/sync-mutation.ts
@@ -88,3 +88,9 @@ export function syncCreateEntity(
function getEntityPathName(entityType: string) {
return entityType === 'fileRef' ? 'file' : entityType
}
+
+export function syncRootDocId(projectId: string, rootDocId: string) {
+ return postJSON(`/project/${projectId}/settings`, {
+ body: { rootDocId },
+ })
+}
diff --git a/services/web/frontend/js/features/ide-redesign/components/settings/compiler-settings/root-document-setting.tsx b/services/web/frontend/js/features/ide-redesign/components/settings/compiler-settings/root-document-setting.tsx
index ec168cbb14..0a8307c25c 100644
--- a/services/web/frontend/js/features/ide-redesign/components/settings/compiler-settings/root-document-setting.tsx
+++ b/services/web/frontend/js/features/ide-redesign/components/settings/compiler-settings/root-document-setting.tsx
@@ -45,7 +45,7 @@ export default function RootDocumentSetting() {
__date__.",
"the_original_text_has_changed": "The original text has changed, so this suggestion can’t be applied",
"the_overleaf_color_scheme": "The __appName__ color scheme",
- "the_primary_file_for_compiling_your_project": "The primary file for compiling your project",
+ "the_primary_file_for_compiling_your_project": "The primary file for compiling your project.",
"the_project_that_contains_this_file_is_not_shared_with_you": "The project that contains this file is not shared with you",
"the_requested_conversion_job_was_not_found": "The link to open this content on Overleaf specified a conversion job that could not be found. It’s possible that the job has expired and needs to be run again. If this keeps happening for links on a particular site, please report this to them.",
"the_requested_publisher_was_not_found": "The link to open this content on Overleaf specified a publisher that could not be found. If this keeps happening for links on a particular site, please report this to them.",
@@ -2820,6 +2821,7 @@
"you_are_on_x_plan_as_member_of_group_subscription_y_administered_by_z": "You are on our <0>__planName__0> plan as a <1>member1> of the group subscription <1>__groupName__1> administered by <1>__adminEmail__1>",
"you_are_using_your_organization_email_x_would_like_you_to_take_action": "Because you are using your organization email on Overleaf, __companyName__ would like you to take one of the following actions.",
"you_can_also_choose_to_view_anonymously_or_leave_the_project": "You can also choose to <0>view anonymously0> (you will lose edit access) or <1>leave the project1>.",
+ "you_can_also_right_click_a_file_to_set_it_as_main": "You can also right-click a file to set it as main.",
"you_can_buy_this_plan_but_not_as_a_trial": "You can buy this plan but not as a trial, as you’ve completed a trial before.",
"you_can_manage_your_reference_manager_integrations_from_your_account_settings_page": "You can manage your reference manager integrations from your <0>account settings page0>.",
"you_can_now_enable_sso": "You can now enable SSO on your group settings page.",
diff --git a/services/web/locales/zh-CN.json b/services/web/locales/zh-CN.json
index f2daf457ef..cfd656b99e 100644
--- a/services/web/locales/zh-CN.json
+++ b/services/web/locales/zh-CN.json
@@ -2192,7 +2192,7 @@
"the_next_payment_will_be_collected_on": "下一笔付款将于__date__收取。",
"the_original_text_has_changed": "原文本已发生改变,因此此建议无法应用",
"the_overleaf_color_scheme": "__appName__ 配色方案",
- "the_primary_file_for_compiling_your_project": "编译项目的主要文件",
+ "the_primary_file_for_compiling_your_project": "编译项目的主要文件。",
"the_project_that_contains_this_file_is_not_shared_with_you": "包含此文件的项目未与您共享",
"the_requested_conversion_job_was_not_found": "在Overleaf打开此内容的链接指定了找不到的转换作业。作业可能已过期,需要重新运行。如果某个网站的链接经常出现这种情况,请向他们报告。",
"the_requested_publisher_was_not_found": "在Overleaf打开此内容的链接指定了找不到的发布者。如果某个网站的链接经常出现这种情况,请向他们报告。",
diff --git a/services/web/test/frontend/features/file-tree/flows/context-menu.spec.tsx b/services/web/test/frontend/features/file-tree/flows/context-menu.spec.tsx
index 11eb1a97b6..102aca7ecb 100644
--- a/services/web/test/frontend/features/file-tree/flows/context-menu.spec.tsx
+++ b/services/web/test/frontend/features/file-tree/flows/context-menu.spec.tsx
@@ -111,4 +111,66 @@ describe('FileTree Context Menu Flow', function () {
cy.findAllByRole('button', { name: 'main.tex' }).trigger('contextmenu')
cy.findByRole('menu').should('not.exist')
})
+
+ it('shows "set main document" item when appropriate', function () {
+ const rootFolder = [
+ {
+ _id: 'root-folder-id',
+ name: 'rootFolder',
+ docs: [
+ { _id: 'main-doc', name: 'main.tex' },
+ { _id: 'other-doc', name: 'other.tex' },
+ ],
+ folders: [],
+ fileRefs: [],
+ },
+ ]
+
+ cy.mount(
+
+
+
+ )
+
+ cy.findByRole('menu').should('not.exist')
+
+ // main.tex is already the main document
+ cy.findByRole('button', { name: 'main.tex' }).trigger('contextmenu')
+ cy.findByRole('menu')
+ .findByRole('menuitem', { name: 'Set as main document' })
+ .should('not.exist')
+
+ // set other.tex as the main document
+ cy.findByRole('button', { name: 'other.tex' }).click({ force: true })
+ cy.findByRole('button', { name: 'other.tex' }).trigger('contextmenu')
+
+ cy.intercept('POST', '/project/123abc/settings', { statusCode: 204 }).as(
+ 'update-settings'
+ )
+
+ cy.findByRole('menu')
+ .findByRole('menuitem', { name: 'Set as main document' })
+ .click()
+
+ cy.wait('@update-settings')
+ .its('request.body.rootDocId')
+ .should('eq', 'other-doc')
+
+ // main.tex is now not the main document
+ cy.findByRole('button', { name: 'main.tex' }).trigger('contextmenu')
+ cy.findByRole('menu').findByRole('menuitem', {
+ name: 'Set as main document',
+ })
+ })
})