file item's row menu keyed (80 -> 67)
The single row menu shared by the dropdown and the context-menu variants: open,
edit, paste-into, read aloud, extract, run task/agent, cut/copy/duplicate/rename,
compress, copy path, download, pin, chat, create dashboard, delete.
Four strings the scanner cannot see, converted anyway since the file was open:
"Open" sits after a conditional icon expression (`{cond ? <A/> : <B/>}`) rather than
directly after a tag's `>`, which is outside the jsx-text pattern's reach. "Cut" is
three characters and the pattern requires four. "Open in editor"/"(too large)" and
"Compress"/"Compress selection" are both ternaries, which the scanner's own docs
already say it cannot see.
formatDate() called toLocaleDateString(undefined, {...}) — the browser-locale
contradiction D7 describes, same shape as ApiKeys and DavAppPasswords in phase 2.
Replaced with useFormat().date, threaded from the exported FileItem component since
the old helper was module-level and a hook cannot be called from one. Verified the
two locales now genuinely differ: "14 Nov 2023" against "14/11/2023", not "whatever
the browser happens to be set to".
task.name, agent.name and group.category stay untouched — content, not chrome.
Reused files.menu.copyPath, .createDashboardHere and .download from the previous
commit rather than adding duplicates for the same words.
This commit is contained in:
+24
-28
@@ -50,6 +50,8 @@ import type { DirEntry } from '../../../../hooks/useFilesAPI';
|
||||
import type { TaskGroup, TaskSummary } from '../../useTasks';
|
||||
import type { AgentGroup, AgentSummary } from '../../useAgents';
|
||||
import { getFileType } from '../../../FileViewer';
|
||||
import { useTranslation } from '../../../../locales';
|
||||
import { useFormat } from 'i18n';
|
||||
|
||||
export type FileItemProps = {
|
||||
entry: DirEntry;
|
||||
@@ -91,14 +93,6 @@ function formatSize(bytes: number): string {
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
}
|
||||
|
||||
function formatDate(ms: number): string {
|
||||
return new Date(ms).toLocaleDateString(undefined, {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Extensions that reach `getFileType`'s `text` fallback but are not text.
|
||||
*
|
||||
@@ -232,6 +226,7 @@ const MenuItems = ({
|
||||
onRunAgent,
|
||||
onCreateDashboard,
|
||||
}: MenuItemsProps & { ui: MenuPrimitives }) => {
|
||||
const { t } = useTranslation();
|
||||
const isDir = entry.type === 'directory';
|
||||
const fileType = entry.type === 'file' ? getFileType(entry.name) : null;
|
||||
// Same binary guard as the editor. `text` is the FALLBACK type, so without this Read Aloud offered to
|
||||
@@ -268,7 +263,7 @@ const MenuItems = ({
|
||||
puts first. */}
|
||||
<Item onClick={() => onOpen(entry)} className="cursor-pointer">
|
||||
{isDir ? <FolderOpen className="mr-2 h-4 w-4" /> : <ExternalLink className="mr-2 h-4 w-4" />}
|
||||
Open
|
||||
{t('files.menu.open')}
|
||||
</Item>
|
||||
{showEdit && (
|
||||
// The first place `disabled` is used rather than hiding. `/file-browser/read` refuses anything
|
||||
@@ -281,7 +276,7 @@ const MenuItems = ({
|
||||
className={tooBigToEdit ? '' : 'cursor-pointer'}
|
||||
>
|
||||
<Code className="mr-2 h-4 w-4" />
|
||||
{tooBigToEdit ? 'Open in editor (too large)' : 'Open in editor'}
|
||||
{tooBigToEdit ? t('files.menu.openInEditorTooLarge') : t('files.menu.openInEditor')}
|
||||
</Item>
|
||||
)}
|
||||
{/* Paste INTO a folder. The background menu could only paste into the folder already open, so a
|
||||
@@ -289,27 +284,27 @@ const MenuItems = ({
|
||||
{isDir && canPaste && (
|
||||
<Item onClick={() => onPasteInto(entry)} className="cursor-pointer">
|
||||
<ClipboardPaste className="mr-2 h-4 w-4" />
|
||||
Paste into folder
|
||||
{t('files.menu.pasteIntoFolder')}
|
||||
</Item>
|
||||
)}
|
||||
<Separator />
|
||||
{showReadAloud && (
|
||||
<Item onClick={() => onReadAloud(entry)} className="cursor-pointer">
|
||||
<Volume2 className="mr-2 h-4 w-4" />
|
||||
Read Aloud
|
||||
{t('files.menu.readAloud')}
|
||||
</Item>
|
||||
)}
|
||||
{showExtract && (
|
||||
<Item onClick={() => onExtract(entry)} className="cursor-pointer">
|
||||
<FolderArchive className="mr-2 h-4 w-4" />
|
||||
Extract
|
||||
{t('files.menu.extract')}
|
||||
</Item>
|
||||
)}
|
||||
{hasTasks && (
|
||||
<Sub>
|
||||
<SubTrigger className="cursor-pointer">
|
||||
<Play className="mr-2 h-4 w-4" />
|
||||
Run Task
|
||||
{t('files.menu.runTask')}
|
||||
</SubTrigger>
|
||||
<SubContent className="z-[600]">
|
||||
{nestTasks
|
||||
@@ -337,7 +332,7 @@ const MenuItems = ({
|
||||
<Sub>
|
||||
<SubTrigger className="cursor-pointer">
|
||||
<Bot className="mr-2 h-4 w-4" />
|
||||
Run Agent
|
||||
{t('files.menu.runAgent')}
|
||||
</SubTrigger>
|
||||
<SubContent className="z-[600]">
|
||||
{nestAgents
|
||||
@@ -364,58 +359,58 @@ const MenuItems = ({
|
||||
{hasActions && <Separator />}
|
||||
<Item onClick={onCut} className="cursor-pointer">
|
||||
<Scissors className="mr-2 h-4 w-4" />
|
||||
Cut
|
||||
{t('files.menu.cut')}
|
||||
</Item>
|
||||
<Item onClick={onCopy} className="cursor-pointer">
|
||||
<Copy className="mr-2 h-4 w-4" />
|
||||
Copy
|
||||
{t('files.menu.copy')}
|
||||
</Item>
|
||||
{!multiSelected && (
|
||||
<Item onClick={() => onDuplicate(entry)} className="cursor-pointer">
|
||||
<CopyPlus className="mr-2 h-4 w-4" />
|
||||
Duplicate
|
||||
{t('files.menu.duplicate')}
|
||||
</Item>
|
||||
)}
|
||||
{!multiSelected && (
|
||||
<Item onClick={onStartRename} className="cursor-pointer">
|
||||
<Pencil className="mr-2 h-4 w-4" />
|
||||
Rename
|
||||
{t('files.menu.rename')}
|
||||
</Item>
|
||||
)}
|
||||
{/* Compress, the missing half of Extract. `/download` already shelled out to zip but streamed the
|
||||
result away; nothing could make an archive and keep it. */}
|
||||
<Item onClick={() => onCompress(entry)} className="cursor-pointer">
|
||||
<FileArchive className="mr-2 h-4 w-4" />
|
||||
{multiSelected ? 'Compress selection' : 'Compress'}
|
||||
{multiSelected ? t('files.menu.compressSelection') : t('files.menu.compress')}
|
||||
</Item>
|
||||
<Item onClick={() => onCopyPath(entry)} className="cursor-pointer">
|
||||
<ClipboardCopy className="mr-2 h-4 w-4" />
|
||||
Copy path
|
||||
{t('files.menu.copyPath')}
|
||||
</Item>
|
||||
<Item onClick={() => onDownload(entry)} className="cursor-pointer">
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
Download
|
||||
{t('files.menu.download')}
|
||||
</Item>
|
||||
<Separator />
|
||||
{/* Pinning lived only in the widget, which is not where you meet a file worth keeping to hand. */}
|
||||
<Item onClick={() => onTogglePin(entry)} className="cursor-pointer">
|
||||
{isPinned ? <PinOff className="mr-2 h-4 w-4" /> : <Pin className="mr-2 h-4 w-4" />}
|
||||
{isPinned ? 'Unpin' : 'Pin'}
|
||||
{isPinned ? t('files.menu.unpin') : t('files.menu.pin')}
|
||||
</Item>
|
||||
<Item onClick={() => onChat(entry)} className="cursor-pointer">
|
||||
<MessageSquare className="mr-2 h-4 w-4" />
|
||||
Chat...
|
||||
{t('files.menu.chat')}
|
||||
</Item>
|
||||
{isDir && (
|
||||
<Item onClick={() => onCreateDashboard(entry)} className="cursor-pointer">
|
||||
<LayoutGrid className="mr-2 h-4 w-4" />
|
||||
Create Dashboard here
|
||||
{t('files.menu.createDashboardHere')}
|
||||
</Item>
|
||||
)}
|
||||
<Separator />
|
||||
<Item onClick={() => onDelete(entry)} className="text-destructive cursor-pointer">
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
Delete
|
||||
{t('files.menu.delete')}
|
||||
</Item>
|
||||
</>
|
||||
);
|
||||
@@ -593,6 +588,7 @@ export const FileItem = ({
|
||||
isPinned,
|
||||
canPaste,
|
||||
}: FileItemProps) => {
|
||||
const { date } = useFormat();
|
||||
const [renaming, setRenaming] = useState(false);
|
||||
const clickTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const isDir = entry.type === 'directory';
|
||||
@@ -730,7 +726,7 @@ export const FileItem = ({
|
||||
{isDir ? '--' : formatSize(entry.size)}
|
||||
</span>
|
||||
<span className="hidden md:inline text-xs text-duck-dark/50 shrink-0 w-28 text-right">
|
||||
{formatDate(entry.modifiedAt)}
|
||||
{date(entry.modifiedAt, { month: 'short', day: 'numeric', year: 'numeric' })}
|
||||
</span>
|
||||
<div className="file-item-ellipsis opacity-0 group-hover:opacity-100 data-[state=open]:opacity-100 transition-opacity shrink-0">
|
||||
<EllipsisMenu {...menuProps} onEnsureSelected={ensureSelected} />
|
||||
@@ -780,7 +776,7 @@ export const FileItem = ({
|
||||
<span className="text-xs text-duck-dark/50">
|
||||
{isDir ? 'Folder' : formatSize(entry.size)}
|
||||
{' · '}
|
||||
{formatDate(entry.modifiedAt)}
|
||||
{date(entry.modifiedAt, { month: 'short', day: 'numeric', year: 'numeric' })}
|
||||
</span>
|
||||
</div>
|
||||
</ContextMenuTrigger>
|
||||
|
||||
@@ -503,7 +503,25 @@
|
||||
"openFolder": "Open folder",
|
||||
"showLocation": "Show location",
|
||||
"download": "Download",
|
||||
"noResults": "No results found"
|
||||
"noResults": "No results found",
|
||||
"open": "Open",
|
||||
"openInEditor": "Open in editor",
|
||||
"openInEditorTooLarge": "Open in editor (too large)",
|
||||
"pasteIntoFolder": "Paste into folder",
|
||||
"readAloud": "Read Aloud",
|
||||
"extract": "Extract",
|
||||
"runTask": "Run Task",
|
||||
"runAgent": "Run Agent",
|
||||
"cut": "Cut",
|
||||
"copy": "Copy",
|
||||
"duplicate": "Duplicate",
|
||||
"rename": "Rename",
|
||||
"compress": "Compress",
|
||||
"compressSelection": "Compress selection",
|
||||
"unpin": "Unpin",
|
||||
"pin": "Pin",
|
||||
"chat": "Chat...",
|
||||
"delete": "Delete"
|
||||
}
|
||||
},
|
||||
"taskRunner": {
|
||||
|
||||
@@ -503,7 +503,25 @@
|
||||
"openFolder": "Abrir pasta",
|
||||
"showLocation": "Mostrar localização",
|
||||
"download": "Descarregar",
|
||||
"noResults": "Nenhum resultado encontrado"
|
||||
"noResults": "Nenhum resultado encontrado",
|
||||
"open": "Abrir",
|
||||
"openInEditor": "Abrir no editor",
|
||||
"openInEditorTooLarge": "Abrir no editor (demasiado grande)",
|
||||
"pasteIntoFolder": "Colar na pasta",
|
||||
"readAloud": "Ler em voz alta",
|
||||
"extract": "Extrair",
|
||||
"runTask": "Executar tarefa",
|
||||
"runAgent": "Executar agente",
|
||||
"cut": "Cortar",
|
||||
"copy": "Copiar",
|
||||
"duplicate": "Duplicar",
|
||||
"rename": "Renomear",
|
||||
"compress": "Comprimir",
|
||||
"compressSelection": "Comprimir seleção",
|
||||
"unpin": "Remover fixação",
|
||||
"pin": "Fixar",
|
||||
"chat": "Chat...",
|
||||
"delete": "Eliminar"
|
||||
}
|
||||
},
|
||||
"taskRunner": {
|
||||
|
||||
Reference in New Issue
Block a user