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 type { UseFileBrowserAppType } from '../useFileBrowserApp';
import { NewEntryDialog } from './NewEntryDialog'; import { NewEntryDialog } from './NewEntryDialog';
import { FileGrid } from './FileGrid'; import { FileGrid } from './FileGrid';
import { useTranslation } from '../../../../locales';
type FileViewContainerProps = { type FileViewContainerProps = {
fileBrowserManager: UseFileBrowserAppType; fileBrowserManager: UseFileBrowserAppType;
}; };
export const FileViewContainer = ({ fileBrowserManager }: FileViewContainerProps) => { export const FileViewContainer = ({ fileBrowserManager }: FileViewContainerProps) => {
const { t } = useTranslation();
const { const {
searchQuery, searchQuery,
searching, searching,
@@ -85,11 +87,11 @@ export const FileViewContainer = ({ fileBrowserManager }: FileViewContainerProps
<ContextMenuContent className="z-[600]"> <ContextMenuContent className="z-[600]">
<ContextMenuItem onClick={handleCopyCurrentPath} className="cursor-pointer"> <ContextMenuItem onClick={handleCopyCurrentPath} className="cursor-pointer">
<ClipboardCopy className="mr-2 h-4 w-4" /> <ClipboardCopy className="mr-2 h-4 w-4" />
Copy path {t('files.menu.copyPath')}
</ContextMenuItem> </ContextMenuItem>
<ContextMenuItem onClick={handleChatHere} className="cursor-pointer"> <ContextMenuItem onClick={handleChatHere} className="cursor-pointer">
<MessageSquare className="mr-2 h-4 w-4" /> <MessageSquare className="mr-2 h-4 w-4" />
Chat about this {t('files.menu.chatAboutThis')}
</ContextMenuItem> </ContextMenuItem>
{/* Gated on the clipboard, as both toolbars already were. Ungated it fell through to the {/* 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 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 && ( {clipboard !== null && (
<ContextMenuItem onClick={handlePaste} className="cursor-pointer"> <ContextMenuItem onClick={handlePaste} className="cursor-pointer">
<ClipboardPaste className="mr-2 h-4 w-4" /> <ClipboardPaste className="mr-2 h-4 w-4" />
Paste {t('files.menu.paste')}
</ContextMenuItem> </ContextMenuItem>
)} )}
{/* Both open the same dialog. It validates as you type — against the names already on screen — {/* 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. */} rejected request. The server still refuses to clobber; this is the courtesy, that is the lock. */}
<ContextMenuItem onClick={() => setNewEntry('file')} className="cursor-pointer"> <ContextMenuItem onClick={() => setNewEntry('file')} className="cursor-pointer">
<FilePlus className="mr-2 h-4 w-4" /> <FilePlus className="mr-2 h-4 w-4" />
New file {t('files.menu.newFile')}
</ContextMenuItem> </ContextMenuItem>
<ContextMenuItem onClick={() => setNewEntry('folder')} className="cursor-pointer"> <ContextMenuItem onClick={() => setNewEntry('folder')} className="cursor-pointer">
<FolderPlus className="mr-2 h-4 w-4" /> <FolderPlus className="mr-2 h-4 w-4" />
New folder {t('files.menu.newFolder')}
</ContextMenuItem> </ContextMenuItem>
<ContextMenuItem onClick={() => fileInputRef.current?.click()} className="cursor-pointer"> <ContextMenuItem onClick={() => fileInputRef.current?.click()} className="cursor-pointer">
<Upload className="mr-2 h-4 w-4" /> <Upload className="mr-2 h-4 w-4" />
Upload files {t('files.menu.uploadFiles')}
</ContextMenuItem> </ContextMenuItem>
<ContextMenuItem onClick={() => folderInputRef.current?.click()} className="cursor-pointer"> <ContextMenuItem onClick={() => folderInputRef.current?.click()} className="cursor-pointer">
<FolderUp className="mr-2 h-4 w-4" /> <FolderUp className="mr-2 h-4 w-4" />
Upload folder {t('files.menu.uploadFolder')}
</ContextMenuItem> </ContextMenuItem>
{/* Was toolbar-only, and the toolbar hides it under `md:` — so on a phone there was no way {/* Was toolbar-only, and the toolbar hides it under `md:` — so on a phone there was no way
to clone at all. */} to clone at all. */}
<ContextMenuItem onClick={() => setNewEntry('clone')} className="cursor-pointer"> <ContextMenuItem onClick={() => setNewEntry('clone')} className="cursor-pointer">
<GitBranch className="mr-2 h-4 w-4" /> <GitBranch className="mr-2 h-4 w-4" />
Clone repository {t('files.menu.cloneRepository')}
</ContextMenuItem> </ContextMenuItem>
<ContextMenuSeparator /> <ContextMenuSeparator />
<ContextMenuItem onClick={() => setShowHidden(!showHidden)} className="cursor-pointer"> <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 ? <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> </ContextMenuItem>
<ContextMenuSeparator /> <ContextMenuSeparator />
<ContextMenuItem onClick={handleCreateDashboardHere} className="cursor-pointer"> <ContextMenuItem onClick={handleCreateDashboardHere} className="cursor-pointer">
<LayoutGrid className="mr-2 h-4 w-4" /> <LayoutGrid className="mr-2 h-4 w-4" />
Create Dashboard here {t('files.menu.createDashboardHere')}
</ContextMenuItem> </ContextMenuItem>
<ContextMenuItem onClick={() => setShowVideoDownload(true)} className="cursor-pointer"> <ContextMenuItem onClick={() => setShowVideoDownload(true)} className="cursor-pointer">
<Download className="mr-2 h-4 w-4" /> <Download className="mr-2 h-4 w-4" />
Download video {t('files.menu.downloadVideo')}
</ContextMenuItem> </ContextMenuItem>
<ContextMenuItem onClick={() => setShowDictate(true)} className="cursor-pointer"> <ContextMenuItem onClick={() => setShowDictate(true)} className="cursor-pointer">
<Mic className="mr-2 h-4 w-4" /> <Mic className="mr-2 h-4 w-4" />
Dictate {t('files.menu.dictate')}
</ContextMenuItem> </ContextMenuItem>
</ContextMenuContent> </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="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"> <div className="flex flex-col items-center gap-2 text-duck-teal">
<Upload className="h-8 w-8" /> <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>
</div> </div>
)} )}
@@ -213,23 +215,23 @@ export const FileViewContainer = ({ fileBrowserManager }: FileViewContainerProps
{isDir && ( {isDir && (
<ContextMenuItem onClick={() => handleSearchResultClick(entry)} className="cursor-pointer"> <ContextMenuItem onClick={() => handleSearchResultClick(entry)} className="cursor-pointer">
<FolderOpen className="mr-2 h-4 w-4" /> <FolderOpen className="mr-2 h-4 w-4" />
Open folder {t('files.menu.openFolder')}
</ContextMenuItem> </ContextMenuItem>
)} )}
<ContextMenuItem onClick={() => handleShowLocation(entry)} className="cursor-pointer"> <ContextMenuItem onClick={() => handleShowLocation(entry)} className="cursor-pointer">
<FolderSearch className="mr-2 h-4 w-4" /> <FolderSearch className="mr-2 h-4 w-4" />
Show location {t('files.menu.showLocation')}
</ContextMenuItem> </ContextMenuItem>
{absPath && ( {absPath && (
<ContextMenuItem onClick={() => handleCopyAbsPath(absPath)} className="cursor-pointer"> <ContextMenuItem onClick={() => handleCopyAbsPath(absPath)} className="cursor-pointer">
<ClipboardCopy className="mr-2 h-4 w-4" /> <ClipboardCopy className="mr-2 h-4 w-4" />
Copy path {t('files.menu.copyPath')}
</ContextMenuItem> </ContextMenuItem>
)} )}
{absPath && !isDir && ( {absPath && !isDir && (
<ContextMenuItem onClick={() => handleDownloadPath(absPath)} className="cursor-pointer"> <ContextMenuItem onClick={() => handleDownloadPath(absPath)} className="cursor-pointer">
<Download className="mr-2 h-4 w-4" /> <Download className="mr-2 h-4 w-4" />
Download {t('files.menu.download')}
</ContextMenuItem> </ContextMenuItem>
)} )}
</ContextMenuContent> </ContextMenuContent>
@@ -238,7 +240,9 @@ export const FileViewContainer = ({ fileBrowserManager }: FileViewContainerProps
})} })}
</div> </div>
) : searchResults ? ( ) : 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} ) : null}
</div> </div>
</ContextMenuTrigger> </ContextMenuTrigger>
@@ -484,7 +484,27 @@
"duplicated": "Duplicated “{{name}}”", "duplicated": "Duplicated “{{name}}”",
"pastedInto": "Pasted into “{{name}}”", "pastedInto": "Pasted into “{{name}}”",
"unpinned": "Unpinned “{{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": { "taskRunner": {
"stop": "Stop", "stop": "Stop",
@@ -484,7 +484,27 @@
"duplicated": "“{{name}}” duplicado", "duplicated": "“{{name}}” duplicado",
"pastedInto": "Colado em “{{name}}”", "pastedInto": "Colado em “{{name}}”",
"unpinned": "“{{name}}” removido dos fixados", "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": { "taskRunner": {
"stop": "Parar", "stop": "Parar",