From ff7de70a61050a587efe0ed2275bd330ab841e58 Mon Sep 17 00:00:00 2001 From: claude Date: Wed, 17 Jun 2026 20:35:50 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20per-file=20convert=20=E2=80=94=20Duplica?= =?UTF-8?q?teNameError=20+=20first-click=20no-op?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two bugs: 1. Converting when output already exists threw DuplicateNameError (400). Now overwrites existing doc via setDocument instead of failing. 2. Right-clicking an unselected file left contextMenuEntityId null, so the first click on Convert silently did nothing. Added contextMenuEntityId to FileTreeMainContext, set it on right-click and on the … button click; FileTreeItemMenuItems now uses it for the convert hooks rather than relying on selectedEntityIds. Co-Authored-By: Claude Sonnet 4.6 --- .../Downloads/ProjectDownloadsController.mjs | 34 ++++++++++++++----- .../file-tree-item/file-tree-item-inner.tsx | 4 ++- .../file-tree-item-menu-items.tsx | 27 +++++++++++---- .../file-tree-item/file-tree-item-menu.tsx | 4 ++- .../file-tree/contexts/file-tree-main.tsx | 7 ++++ 5 files changed, 58 insertions(+), 18 deletions(-) diff --git a/services/web/app/src/Features/Downloads/ProjectDownloadsController.mjs b/services/web/app/src/Features/Downloads/ProjectDownloadsController.mjs index 42b8ea9cbd..44dae22ee6 100644 --- a/services/web/app/src/Features/Downloads/ProjectDownloadsController.mjs +++ b/services/web/app/src/Features/Downloads/ProjectDownloadsController.mjs @@ -232,14 +232,30 @@ async function convertDocInProject(req, res) { await rm(tmpDir, { recursive: true, force: true }) } - const { doc } = await ProjectEntityUpdateHandler.promises.addDoc( - projectId, - parentFolderId, - outputName, - convertedContent.split('\n'), - userId, - 'convert' - ) + const existingDoc = folder.docs?.find(d => d.name === outputName) + let resultDocId, resultName + if (existingDoc) { + await DocumentUpdaterHandler.promises.setDocument( + projectId, + existingDoc._id.toString(), + userId, + convertedContent.split('\n'), + 'convert' + ) + resultDocId = existingDoc._id + resultName = existingDoc.name + } else { + const { doc } = await ProjectEntityUpdateHandler.promises.addDoc( + projectId, + parentFolderId, + outputName, + convertedContent.split('\n'), + userId, + 'convert' + ) + resultDocId = doc._id + resultName = doc.name + } ProjectAuditLogHandler.addEntryInBackground( projectId, @@ -248,7 +264,7 @@ async function convertDocInProject(req, res) { req.ip ) - res.json({ docId: doc._id, name: doc.name }) + res.json({ docId: resultDocId, name: resultName }) } export default { diff --git a/services/web/frontend/js/features/file-tree/components/file-tree-item/file-tree-item-inner.tsx b/services/web/frontend/js/features/file-tree/components/file-tree-item/file-tree-item-inner.tsx index 668a773323..e5a226e0b4 100644 --- a/services/web/frontend/js/features/file-tree/components/file-tree-item/file-tree-item-inner.tsx +++ b/services/web/frontend/js/features/file-tree/components/file-tree-item/file-tree-item-inner.tsx @@ -30,7 +30,8 @@ function FileTreeItemInner({ onClick?: () => void }) { const { fileTreeReadOnly } = useFileTreeData() - const { setContextMenuCoords } = useFileTreeMainContext() + const { setContextMenuCoords, setContextMenuEntityId } = + useFileTreeMainContext() const { isRenaming } = useFileTreeActionable() const { selectedEntityIds } = useFileTreeSelectable() @@ -73,6 +74,7 @@ function FileTreeItemInner({ ev.preventDefault() + setContextMenuEntityId(id) setContextMenuCoords({ top: ev.pageY, left: ev.pageX, 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 885d7bbd20..c6a8517ae4 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 @@ -10,6 +10,7 @@ import { import { useFileTreeActionable } from '../../contexts/file-tree-actionable' import { useFileTreeSelectable } from '../../contexts/file-tree-selectable' import { useFileTreeData } from '@/shared/context/file-tree-data-context' +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' @@ -37,22 +38,34 @@ function FileTreeItemMenuItems() { const { fileTreeData } = useFileTreeData() const { selectedEntityIds } = useFileTreeSelectable() + const { contextMenuEntityId } = useFileTreeMainContext() const selectedEntityId = selectedEntityIds.size === 1 ? Array.from(selectedEntityIds)[0] : null - const selectedEntityType = selectedEntityId - ? findInTree(fileTreeData, selectedEntityId)?.type + + // Use context-menu-target entity for convert; falls back to selection + const convertEntityId = contextMenuEntityId ?? selectedEntityId + const convertEntity = convertEntityId + ? findInTree(fileTreeData, convertEntityId) : null - const isDoc = selectedEntityType === 'doc' + const isConvertableDoc = convertEntity?.type === 'doc' + const convertEntityName = convertEntity?.entity.name ?? null + const enablePandocConversions = getMeta('ol-ExposedSettings')?.enablePandocConversions const canConvertToTypst = - enablePandocConversions && canRename && isDoc && selectedFileName?.endsWith('.tex') + enablePandocConversions && + canRename && + isConvertableDoc && + convertEntityName?.endsWith('.tex') const canConvertToLatex = - enablePandocConversions && canRename && isDoc && selectedFileName?.endsWith('.typ') + enablePandocConversions && + canRename && + isConvertableDoc && + convertEntityName?.endsWith('.typ') - const { convert: convertToTypst } = useConvertDoc('typst', selectedEntityId) - const { convert: convertToLatex } = useConvertDoc('latex', selectedEntityId) + const { convert: convertToTypst } = useConvertDoc('typst', convertEntityId) + const { convert: convertToLatex } = useConvertDoc('latex', convertEntityId) const downloadWithAnalytics = useCallback(() => { // we are only interested in downloads of bib files WRT analytics, for the purposes of promoting the tpr integrations diff --git a/services/web/frontend/js/features/file-tree/components/file-tree-item/file-tree-item-menu.tsx b/services/web/frontend/js/features/file-tree/components/file-tree-item/file-tree-item-menu.tsx index 36b54b8e69..f5efcff356 100644 --- a/services/web/frontend/js/features/file-tree/components/file-tree-item/file-tree-item-menu.tsx +++ b/services/web/frontend/js/features/file-tree/components/file-tree-item/file-tree-item-menu.tsx @@ -5,7 +5,8 @@ import MaterialIcon from '@/shared/components/material-icon' function FileTreeItemMenu({ id, name }: { id: string; name: string }) { const { t } = useTranslation() - const { contextMenuCoords, setContextMenuCoords } = useFileTreeMainContext() + const { contextMenuCoords, setContextMenuCoords, setContextMenuEntityId } = + useFileTreeMainContext() const menuButtonRef = useRef(null) const isMenuOpen = Boolean(contextMenuCoords) @@ -13,6 +14,7 @@ function FileTreeItemMenu({ id, name }: { id: string; name: string }) { function handleClick(event: React.MouseEvent) { event.stopPropagation() if (!contextMenuCoords && menuButtonRef.current) { + setContextMenuEntityId(id) const target = menuButtonRef.current.getBoundingClientRect() setContextMenuCoords({ top: target.top + target.height / 2, diff --git a/services/web/frontend/js/features/file-tree/contexts/file-tree-main.tsx b/services/web/frontend/js/features/file-tree/contexts/file-tree-main.tsx index d8cceb31c3..d372de09a7 100644 --- a/services/web/frontend/js/features/file-tree/contexts/file-tree-main.tsx +++ b/services/web/frontend/js/features/file-tree/contexts/file-tree-main.tsx @@ -9,6 +9,8 @@ const FileTreeMainContext = createContext< setStartedFreeTrial: (value: boolean) => void contextMenuCoords: ContextMenuCoords | null setContextMenuCoords: (value: ContextMenuCoords | null) => void + contextMenuEntityId: string | null + setContextMenuEntityId: (value: string | null) => void } | undefined >(undefined) @@ -39,6 +41,9 @@ export const FileTreeMainProvider: FC< }) => { const [contextMenuCoords, setContextMenuCoords] = useState(null) + const [contextMenuEntityId, setContextMenuEntityId] = useState( + null + ) return ( {children}