file view container's context menus keyed (97 -> 80)

The folder right-click menu and the search-results menu: copy path, chat about this,
paste, new file/folder, upload, clone, hidden-files toggle, create dashboard,
download video, dictate, open folder, show location, and the drop-to-upload overlay.

"Hide hidden files" / "Show hidden files" was a ternary inside JSX, which the
scanner's own docs say it cannot see — found by reading the file rather than by the
tool. Two keys, one per branch, since English and Portuguese verbs differ by more
than a negation.

files.menu.* is a new sub-namespace under the existing files.* group rather than a
flat name, since a bare "download" would otherwise collide with nothing today but
reads as ambiguous next to files.downloadFailed.
This commit is contained in:
2026-08-23 16:13:05 +00:00
parent 07241440c9
commit 3565cc6ac6
3 changed files with 64 additions and 20 deletions
@@ -29,12 +29,14 @@ import {
import type { UseFileBrowserAppType } from '../useFileBrowserApp';
import { NewEntryDialog } from './NewEntryDialog';
import { FileGrid } from './FileGrid';
import { useTranslation } from '../../../../locales';
type FileViewContainerProps = {
fileBrowserManager: UseFileBrowserAppType;
};
export const FileViewContainer = ({ fileBrowserManager }: FileViewContainerProps) => {
const { t } = useTranslation();
const {
searchQuery,
searching,
@@ -85,11 +87,11 @@ export const FileViewContainer = ({ fileBrowserManager }: FileViewContainerProps
<ContextMenuContent className="z-[600]">
<ContextMenuItem onClick={handleCopyCurrentPath} className="cursor-pointer">
<ClipboardCopy className="mr-2 h-4 w-4" />
Copy path
{t('files.menu.copyPath')}
</ContextMenuItem>
<ContextMenuItem onClick={handleChatHere} className="cursor-pointer">
<MessageSquare className="mr-2 h-4 w-4" />
Chat about this
{t('files.menu.chatAboutThis')}
</ContextMenuItem>
{/* Gated on the clipboard, as both toolbars already were. Ungated it fell through to the
OS clipboard path, which on an insecure origin toasts an error and otherwise silently
@@ -97,7 +99,7 @@ export const FileViewContainer = ({ fileBrowserManager }: FileViewContainerProps
{clipboard !== null && (
<ContextMenuItem onClick={handlePaste} className="cursor-pointer">
<ClipboardPaste className="mr-2 h-4 w-4" />
Paste
{t('files.menu.paste')}
</ContextMenuItem>
)}
{/* Both open the same dialog. It validates as you type — against the names already on screen —
@@ -105,43 +107,43 @@ export const FileViewContainer = ({ fileBrowserManager }: FileViewContainerProps
rejected request. The server still refuses to clobber; this is the courtesy, that is the lock. */}
<ContextMenuItem onClick={() => setNewEntry('file')} className="cursor-pointer">
<FilePlus className="mr-2 h-4 w-4" />
New file
{t('files.menu.newFile')}
</ContextMenuItem>
<ContextMenuItem onClick={() => setNewEntry('folder')} className="cursor-pointer">
<FolderPlus className="mr-2 h-4 w-4" />
New folder
{t('files.menu.newFolder')}
</ContextMenuItem>
<ContextMenuItem onClick={() => fileInputRef.current?.click()} className="cursor-pointer">
<Upload className="mr-2 h-4 w-4" />
Upload files
{t('files.menu.uploadFiles')}
</ContextMenuItem>
<ContextMenuItem onClick={() => folderInputRef.current?.click()} className="cursor-pointer">
<FolderUp className="mr-2 h-4 w-4" />
Upload folder
{t('files.menu.uploadFolder')}
</ContextMenuItem>
{/* Was toolbar-only, and the toolbar hides it under `md:` — so on a phone there was no way
to clone at all. */}
<ContextMenuItem onClick={() => setNewEntry('clone')} className="cursor-pointer">
<GitBranch className="mr-2 h-4 w-4" />
Clone repository
{t('files.menu.cloneRepository')}
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem onClick={() => setShowHidden(!showHidden)} className="cursor-pointer">
{showHidden ? <EyeOff className="mr-2 h-4 w-4" /> : <Eye className="mr-2 h-4 w-4" />}
{showHidden ? 'Hide hidden files' : 'Show hidden files'}
{showHidden ? t('files.menu.hideHiddenFiles') : t('files.menu.showHiddenFiles')}
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem onClick={handleCreateDashboardHere} className="cursor-pointer">
<LayoutGrid className="mr-2 h-4 w-4" />
Create Dashboard here
{t('files.menu.createDashboardHere')}
</ContextMenuItem>
<ContextMenuItem onClick={() => setShowVideoDownload(true)} className="cursor-pointer">
<Download className="mr-2 h-4 w-4" />
Download video
{t('files.menu.downloadVideo')}
</ContextMenuItem>
<ContextMenuItem onClick={() => setShowDictate(true)} className="cursor-pointer">
<Mic className="mr-2 h-4 w-4" />
Dictate
{t('files.menu.dictate')}
</ContextMenuItem>
</ContextMenuContent>
);
@@ -158,7 +160,7 @@ export const FileViewContainer = ({ fileBrowserManager }: FileViewContainerProps
<div className="absolute inset-0 z-50 flex items-center justify-center bg-duck-teal/10 border-2 border-dashed border-duck-teal rounded-lg m-2 pointer-events-none">
<div className="flex flex-col items-center gap-2 text-duck-teal">
<Upload className="h-8 w-8" />
<span className="text-sm font-medium">Drop files to upload</span>
<span className="text-sm font-medium">{t('files.menu.dropToUpload')}</span>
</div>
</div>
)}
@@ -213,23 +215,23 @@ export const FileViewContainer = ({ fileBrowserManager }: FileViewContainerProps
{isDir && (
<ContextMenuItem onClick={() => handleSearchResultClick(entry)} className="cursor-pointer">
<FolderOpen className="mr-2 h-4 w-4" />
Open folder
{t('files.menu.openFolder')}
</ContextMenuItem>
)}
<ContextMenuItem onClick={() => handleShowLocation(entry)} className="cursor-pointer">
<FolderSearch className="mr-2 h-4 w-4" />
Show location
{t('files.menu.showLocation')}
</ContextMenuItem>
{absPath && (
<ContextMenuItem onClick={() => handleCopyAbsPath(absPath)} className="cursor-pointer">
<ClipboardCopy className="mr-2 h-4 w-4" />
Copy path
{t('files.menu.copyPath')}
</ContextMenuItem>
)}
{absPath && !isDir && (
<ContextMenuItem onClick={() => handleDownloadPath(absPath)} className="cursor-pointer">
<Download className="mr-2 h-4 w-4" />
Download
{t('files.menu.download')}
</ContextMenuItem>
)}
</ContextMenuContent>
@@ -238,7 +240,9 @@ export const FileViewContainer = ({ fileBrowserManager }: FileViewContainerProps
})}
</div>
) : searchResults ? (
<div className="flex items-center justify-center py-20 text-duck-dark/40 text-sm">No results found</div>
<div className="flex items-center justify-center py-20 text-duck-dark/40 text-sm">
{t('files.menu.noResults')}
</div>
) : null}
</div>
</ContextMenuTrigger>
@@ -484,7 +484,27 @@
"duplicated": "Duplicated “{{name}}”",
"pastedInto": "Pasted into “{{name}}”",
"unpinned": "Unpinned “{{name}}”",
"pinned": "Pinned “{{name}}”"
"pinned": "Pinned “{{name}}”",
"menu": {
"copyPath": "Copy path",
"chatAboutThis": "Chat about this",
"paste": "Paste",
"newFile": "New file",
"newFolder": "New folder",
"uploadFiles": "Upload files",
"uploadFolder": "Upload folder",
"cloneRepository": "Clone repository",
"hideHiddenFiles": "Hide hidden files",
"showHiddenFiles": "Show hidden files",
"createDashboardHere": "Create Dashboard here",
"downloadVideo": "Download video",
"dictate": "Dictate",
"dropToUpload": "Drop files to upload",
"openFolder": "Open folder",
"showLocation": "Show location",
"download": "Download",
"noResults": "No results found"
}
},
"taskRunner": {
"stop": "Stop",
@@ -484,7 +484,27 @@
"duplicated": "“{{name}}” duplicado",
"pastedInto": "Colado em “{{name}}”",
"unpinned": "“{{name}}” removido dos fixados",
"pinned": "“{{name}}” fixado"
"pinned": "“{{name}}” fixado",
"menu": {
"copyPath": "Copiar caminho",
"chatAboutThis": "Conversar sobre isto",
"paste": "Colar",
"newFile": "Novo ficheiro",
"newFolder": "Nova pasta",
"uploadFiles": "Carregar ficheiros",
"uploadFolder": "Carregar pasta",
"cloneRepository": "Clonar repositório",
"hideHiddenFiles": "Ocultar ficheiros ocultos",
"showHiddenFiles": "Mostrar ficheiros ocultos",
"createDashboardHere": "Criar painel aqui",
"downloadVideo": "Descarregar vídeo",
"dictate": "Ditar",
"dropToUpload": "Largue ficheiros para carregar",
"openFolder": "Abrir pasta",
"showLocation": "Mostrar localização",
"download": "Descarregar",
"noResults": "Nenhum resultado encontrado"
}
},
"taskRunner": {
"stop": "Parar",