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__ plan as a <1>member of the group subscription <1>__groupName__ administered by <1>__adminEmail__", "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 anonymously (you will lose edit access) or <1>leave the project.", + "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 page.", "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', + }) + }) })