feat: set-as-main for .typ/.qmd; fix compiler filter; fix ZIP import compiler
Build and Deploy Verso / deploy (push) Successful in 14m56s
Build and Deploy Verso / deploy (push) Successful in 14m56s
Set as main document (context menu):
- canSetRootDocId used selectedEntityIds so .typ/.qmd files couldn't be
set as main via right-click on an unselected file.
Now computed locally from contextMenuEntityId (same pattern as convert)
using isValidTeXFile which already covers .typ, .qmd, .tex etc.
Compiler filter (editor settings):
- docs?.find(id) could return undefined due to ID format mismatch,
causing all engines to show as available for non-LaTeX projects.
Added findInTree fallback so the root doc name is always resolved.
ZIP import compiler:
- Projects created from ZIP always got defaultLatexCompiler ('quarto')
regardless of content.
- findRootDocFileFromDirectory now also searches for .typ and .qmd root
files after finding no .tex file.
- ProjectUploadManager now infers the compiler from the root doc
extension and sets it on the project after import.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -51,24 +51,20 @@ async function setRootDocAutomatically(projectId) {
|
||||
}
|
||||
|
||||
async function findRootDocFileFromDirectory(directoryPath) {
|
||||
const unsortedFiles = await globby(['**/*.{tex,Rtex,Rnw}'], {
|
||||
// First try LaTeX files (look for \documentclass)
|
||||
const unsortedTexFiles = await globby(['**/*.{tex,Rtex,Rnw,ltx}'], {
|
||||
cwd: directoryPath,
|
||||
followSymlinkedDirectories: false,
|
||||
onlyFiles: true,
|
||||
case: false,
|
||||
})
|
||||
|
||||
// the search order is such that we prefer files closer to the project root, then
|
||||
// we go by file size in ascending order, because people often have a main
|
||||
// file that just includes a bunch of other files; then we go by name, in
|
||||
// order to be deterministic
|
||||
|
||||
const files = await _sortFileList(unsortedFiles, directoryPath)
|
||||
let firstFileInRootFolder
|
||||
const texFiles = await _sortFileList(unsortedTexFiles, directoryPath)
|
||||
let firstTexInRootFolder
|
||||
let doc = null
|
||||
|
||||
while (files.length > 0 && doc == null) {
|
||||
const file = files.shift()
|
||||
while (texFiles.length > 0 && doc == null) {
|
||||
const file = texFiles.shift()
|
||||
const content = await fs.promises.readFile(
|
||||
Path.join(directoryPath, file),
|
||||
'utf8'
|
||||
@@ -77,18 +73,52 @@ async function findRootDocFileFromDirectory(directoryPath) {
|
||||
if (DocumentHelper.contentHasDocumentclass(normalizedContent)) {
|
||||
doc = { path: file, content: normalizedContent }
|
||||
}
|
||||
|
||||
if (!firstFileInRootFolder && !file.includes('/')) {
|
||||
firstFileInRootFolder = { path: file, content: normalizedContent }
|
||||
if (!firstTexInRootFolder && !file.includes('/')) {
|
||||
firstTexInRootFolder = { path: file, content: normalizedContent }
|
||||
}
|
||||
}
|
||||
|
||||
// if no doc was found, use the first file in the root folder as the main doc
|
||||
if (!doc && firstFileInRootFolder) {
|
||||
doc = firstFileInRootFolder
|
||||
if (!doc && firstTexInRootFolder) {
|
||||
doc = firstTexInRootFolder
|
||||
}
|
||||
|
||||
return { path: doc?.path, content: doc?.content }
|
||||
if (doc) return { path: doc.path, content: doc.content }
|
||||
|
||||
// Then try Typst files
|
||||
const typFiles = await globby(['*.typ'], {
|
||||
cwd: directoryPath,
|
||||
followSymlinkedDirectories: false,
|
||||
onlyFiles: true,
|
||||
case: false,
|
||||
})
|
||||
if (typFiles.length > 0) {
|
||||
typFiles.sort()
|
||||
const file = typFiles[0]
|
||||
const content = await fs.promises.readFile(
|
||||
Path.join(directoryPath, file),
|
||||
'utf8'
|
||||
)
|
||||
return { path: file, content }
|
||||
}
|
||||
|
||||
// Then try Quarto/R Markdown files
|
||||
const qmdFiles = await globby(['*.{qmd,Rmd,rmd}'], {
|
||||
cwd: directoryPath,
|
||||
followSymlinkedDirectories: false,
|
||||
onlyFiles: true,
|
||||
case: false,
|
||||
})
|
||||
if (qmdFiles.length > 0) {
|
||||
qmdFiles.sort()
|
||||
const file = qmdFiles[0]
|
||||
const content = await fs.promises.readFile(
|
||||
Path.join(directoryPath, file),
|
||||
'utf8'
|
||||
)
|
||||
return { path: file, content }
|
||||
}
|
||||
|
||||
return { path: undefined, content: undefined }
|
||||
}
|
||||
|
||||
async function setRootDocFromName(projectId, rootDocName) {
|
||||
|
||||
@@ -14,10 +14,21 @@ import ProjectRootDocManager from '../Project/ProjectRootDocManager.mjs'
|
||||
import ProjectDetailsHandler from '../Project/ProjectDetailsHandler.mjs'
|
||||
import ProjectDeleter from '../Project/ProjectDeleter.mjs'
|
||||
import { DeletedProjectReasons } from '../Project/DeletedProjectReasons.mjs'
|
||||
import ProjectOptionsHandler from '../Project/ProjectOptionsHandler.mjs'
|
||||
import TpdsProjectFlusher from '../ThirdPartyDataStore/TpdsProjectFlusher.mjs'
|
||||
import logger from '@overleaf/logger'
|
||||
import OError from '@overleaf/o-error'
|
||||
|
||||
const COMPILER_BY_EXTENSION = {
|
||||
tex: 'pdflatex',
|
||||
rtex: 'pdflatex',
|
||||
ltx: 'pdflatex',
|
||||
rnw: 'pdflatex',
|
||||
typ: 'typst',
|
||||
qmd: 'quarto',
|
||||
rmd: 'quarto',
|
||||
}
|
||||
|
||||
export default {
|
||||
createProjectFromZipArchive: callbackify(createProjectFromZipArchive),
|
||||
createProjectFromZipArchiveWithName: callbackify(
|
||||
@@ -52,6 +63,11 @@ async function createProjectFromZipArchive(ownerId, defaultName, zipPath) {
|
||||
project._id,
|
||||
path
|
||||
)
|
||||
const ext = path.split('.').pop()?.toLowerCase()
|
||||
const compiler = ext ? COMPILER_BY_EXTENSION[ext] : null
|
||||
if (compiler) {
|
||||
await ProjectOptionsHandler.promises.setCompiler(project._id, compiler)
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// no need to wait for the cleanup here
|
||||
|
||||
+21
-6
@@ -14,6 +14,8 @@ import { useFileTreeMainContext } from '../../contexts/file-tree-main'
|
||||
import { findInTree } from '../../util/find-in-tree'
|
||||
import useConvertDoc from '@/features/ide-react/hooks/use-convert-doc'
|
||||
import getMeta from '@/utils/meta'
|
||||
import { isValidTeXFile } from '@/main/is-valid-tex-file'
|
||||
import { syncRootDocId } from '../../util/sync-mutation'
|
||||
|
||||
function FileTreeItemMenuItems() {
|
||||
const { t } = useTranslation()
|
||||
@@ -29,12 +31,11 @@ function FileTreeItemMenuItems() {
|
||||
startUploadingDocOrFile,
|
||||
downloadPath,
|
||||
selectedFileName,
|
||||
canSetRootDocId,
|
||||
setRootDocId,
|
||||
} = useFileTreeActionable()
|
||||
|
||||
const { project } = useProjectContext()
|
||||
const { project, projectId, updateProject } = useProjectContext()
|
||||
const projectOwner = project?.owner?._id
|
||||
const rootDocId = project?.rootDocId
|
||||
|
||||
const { fileTreeData, fileTreeReadOnly } = useFileTreeData()
|
||||
const { selectedEntityIds } = useFileTreeSelectable()
|
||||
@@ -42,7 +43,7 @@ function FileTreeItemMenuItems() {
|
||||
const selectedEntityId =
|
||||
selectedEntityIds.size === 1 ? Array.from(selectedEntityIds)[0] : null
|
||||
|
||||
// Use context-menu-target entity for convert; falls back to selection
|
||||
// Use context-menu-target entity for convert/set-as-main; falls back to selection
|
||||
const convertEntityId = contextMenuEntityId ?? selectedEntityId
|
||||
const convertEntity = convertEntityId
|
||||
? findInTree(fileTreeData, convertEntityId)
|
||||
@@ -64,6 +65,20 @@ function FileTreeItemMenuItems() {
|
||||
isConvertableDoc &&
|
||||
convertEntityName?.endsWith('.typ')
|
||||
|
||||
const canShowSetAsMain =
|
||||
!fileTreeReadOnly &&
|
||||
isConvertableDoc &&
|
||||
!!convertEntityId &&
|
||||
convertEntityId !== rootDocId &&
|
||||
!!convertEntityName &&
|
||||
isValidTeXFile(convertEntityName)
|
||||
|
||||
const handleSetAsMain = useCallback(async () => {
|
||||
if (!convertEntityId) return
|
||||
await syncRootDocId(projectId, convertEntityId)
|
||||
updateProject({ rootDocId: convertEntityId })
|
||||
}, [convertEntityId, projectId, updateProject])
|
||||
|
||||
const { convert: convertToTypst } = useConvertDoc('typst', convertEntityId)
|
||||
const { convert: convertToLatex } = useConvertDoc('latex', convertEntityId)
|
||||
|
||||
@@ -102,11 +117,11 @@ function FileTreeItemMenuItems() {
|
||||
</DropdownItem>
|
||||
</li>
|
||||
) : null}
|
||||
{canSetRootDocId ? (
|
||||
{canShowSetAsMain ? (
|
||||
<>
|
||||
<DropdownDivider />
|
||||
<li role="none">
|
||||
<DropdownItem onClick={setRootDocId}>
|
||||
<DropdownItem onClick={handleSetAsMain}>
|
||||
{t('set_as_main_document')}
|
||||
</DropdownItem>
|
||||
</li>
|
||||
|
||||
+14
-6
@@ -7,6 +7,7 @@ import { usePermissionsContext } from '@/features/ide-react/context/permissions-
|
||||
import { ProjectCompiler } from '@ol-types/project-settings'
|
||||
import { useSetCompilationSettingWithEvent } from '@/features/editor-left-menu/hooks/use-set-compilation-setting'
|
||||
import { useFileTreeData } from '@/shared/context/file-tree-data-context'
|
||||
import { findInTree } from '@/features/file-tree/util/find-in-tree'
|
||||
|
||||
// Which compiler engines make sense for a given root-file extension. CLSI
|
||||
// dispatches the real engine from this extension (.qmd → Quarto, .typ → Typst,
|
||||
@@ -34,7 +35,7 @@ export default function CompilerSetting() {
|
||||
const [compilerOptions] = useState(() => getCompilerOptions())
|
||||
const { t } = useTranslation()
|
||||
const { write } = usePermissionsContext()
|
||||
const { docs } = useFileTreeData()
|
||||
const { docs, fileTreeData } = useFileTreeData()
|
||||
const changeCompiler = useSetCompilationSettingWithEvent(
|
||||
'compiler',
|
||||
setCompiler
|
||||
@@ -43,14 +44,21 @@ export default function CompilerSetting() {
|
||||
// Disable the engines that don't apply to the current root file's extension.
|
||||
// The currently-selected engine is always left enabled so it keeps showing.
|
||||
const options = useMemo(() => {
|
||||
const rootDoc = rootDocId
|
||||
? docs?.find(doc => doc.doc.id === rootDocId)
|
||||
: undefined
|
||||
const extension = rootDoc?.doc.name.split('.').pop()?.toLowerCase()
|
||||
let rootDocName: string | undefined
|
||||
if (rootDocId) {
|
||||
const fromDocs = docs?.find(doc => doc.doc.id === rootDocId)
|
||||
if (fromDocs) {
|
||||
rootDocName = fromDocs.doc.name
|
||||
} else if (fileTreeData) {
|
||||
// Fallback: look up directly in tree in case of ID format mismatch
|
||||
const found = findInTree(fileTreeData, rootDocId)
|
||||
if (found?.type === 'doc') rootDocName = found.entity.name
|
||||
}
|
||||
}
|
||||
const extension = rootDocName?.split('.').pop()?.toLowerCase()
|
||||
const allowed = extension ? ENGINES_BY_EXTENSION[extension] : undefined
|
||||
|
||||
if (!allowed) {
|
||||
// Unknown / no root file: don't restrict anything.
|
||||
return compilerOptions
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user