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 { TaskGroup, TaskSummary } from '../../useTasks';
|
||||||
import type { AgentGroup, AgentSummary } from '../../useAgents';
|
import type { AgentGroup, AgentSummary } from '../../useAgents';
|
||||||
import { getFileType } from '../../../FileViewer';
|
import { getFileType } from '../../../FileViewer';
|
||||||
|
import { useTranslation } from '../../../../locales';
|
||||||
|
import { useFormat } from 'i18n';
|
||||||
|
|
||||||
export type FileItemProps = {
|
export type FileItemProps = {
|
||||||
entry: DirEntry;
|
entry: DirEntry;
|
||||||
@@ -91,14 +93,6 @@ function formatSize(bytes: number): string {
|
|||||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
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.
|
* Extensions that reach `getFileType`'s `text` fallback but are not text.
|
||||||
*
|
*
|
||||||
@@ -232,6 +226,7 @@ const MenuItems = ({
|
|||||||
onRunAgent,
|
onRunAgent,
|
||||||
onCreateDashboard,
|
onCreateDashboard,
|
||||||
}: MenuItemsProps & { ui: MenuPrimitives }) => {
|
}: MenuItemsProps & { ui: MenuPrimitives }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const isDir = entry.type === 'directory';
|
const isDir = entry.type === 'directory';
|
||||||
const fileType = entry.type === 'file' ? getFileType(entry.name) : null;
|
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
|
// 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. */}
|
puts first. */}
|
||||||
<Item onClick={() => onOpen(entry)} className="cursor-pointer">
|
<Item onClick={() => onOpen(entry)} className="cursor-pointer">
|
||||||
{isDir ? <FolderOpen className="mr-2 h-4 w-4" /> : <ExternalLink className="mr-2 h-4 w-4" />}
|
{isDir ? <FolderOpen className="mr-2 h-4 w-4" /> : <ExternalLink className="mr-2 h-4 w-4" />}
|
||||||
Open
|
{t('files.menu.open')}
|
||||||
</Item>
|
</Item>
|
||||||
{showEdit && (
|
{showEdit && (
|
||||||
// The first place `disabled` is used rather than hiding. `/file-browser/read` refuses anything
|
// The first place `disabled` is used rather than hiding. `/file-browser/read` refuses anything
|
||||||
@@ -281,7 +276,7 @@ const MenuItems = ({
|
|||||||
className={tooBigToEdit ? '' : 'cursor-pointer'}
|
className={tooBigToEdit ? '' : 'cursor-pointer'}
|
||||||
>
|
>
|
||||||
<Code className="mr-2 h-4 w-4" />
|
<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>
|
</Item>
|
||||||
)}
|
)}
|
||||||
{/* Paste INTO a folder. The background menu could only paste into the folder already open, so a
|
{/* 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 && (
|
{isDir && canPaste && (
|
||||||
<Item onClick={() => onPasteInto(entry)} className="cursor-pointer">
|
<Item onClick={() => onPasteInto(entry)} className="cursor-pointer">
|
||||||
<ClipboardPaste className="mr-2 h-4 w-4" />
|
<ClipboardPaste className="mr-2 h-4 w-4" />
|
||||||
Paste into folder
|
{t('files.menu.pasteIntoFolder')}
|
||||||
</Item>
|
</Item>
|
||||||
)}
|
)}
|
||||||
<Separator />
|
<Separator />
|
||||||
{showReadAloud && (
|
{showReadAloud && (
|
||||||
<Item onClick={() => onReadAloud(entry)} className="cursor-pointer">
|
<Item onClick={() => onReadAloud(entry)} className="cursor-pointer">
|
||||||
<Volume2 className="mr-2 h-4 w-4" />
|
<Volume2 className="mr-2 h-4 w-4" />
|
||||||
Read Aloud
|
{t('files.menu.readAloud')}
|
||||||
</Item>
|
</Item>
|
||||||
)}
|
)}
|
||||||
{showExtract && (
|
{showExtract && (
|
||||||
<Item onClick={() => onExtract(entry)} className="cursor-pointer">
|
<Item onClick={() => onExtract(entry)} className="cursor-pointer">
|
||||||
<FolderArchive className="mr-2 h-4 w-4" />
|
<FolderArchive className="mr-2 h-4 w-4" />
|
||||||
Extract
|
{t('files.menu.extract')}
|
||||||
</Item>
|
</Item>
|
||||||
)}
|
)}
|
||||||
{hasTasks && (
|
{hasTasks && (
|
||||||
<Sub>
|
<Sub>
|
||||||
<SubTrigger className="cursor-pointer">
|
<SubTrigger className="cursor-pointer">
|
||||||
<Play className="mr-2 h-4 w-4" />
|
<Play className="mr-2 h-4 w-4" />
|
||||||
Run Task
|
{t('files.menu.runTask')}
|
||||||
</SubTrigger>
|
</SubTrigger>
|
||||||
<SubContent className="z-[600]">
|
<SubContent className="z-[600]">
|
||||||
{nestTasks
|
{nestTasks
|
||||||
@@ -337,7 +332,7 @@ const MenuItems = ({
|
|||||||
<Sub>
|
<Sub>
|
||||||
<SubTrigger className="cursor-pointer">
|
<SubTrigger className="cursor-pointer">
|
||||||
<Bot className="mr-2 h-4 w-4" />
|
<Bot className="mr-2 h-4 w-4" />
|
||||||
Run Agent
|
{t('files.menu.runAgent')}
|
||||||
</SubTrigger>
|
</SubTrigger>
|
||||||
<SubContent className="z-[600]">
|
<SubContent className="z-[600]">
|
||||||
{nestAgents
|
{nestAgents
|
||||||
@@ -364,58 +359,58 @@ const MenuItems = ({
|
|||||||
{hasActions && <Separator />}
|
{hasActions && <Separator />}
|
||||||
<Item onClick={onCut} className="cursor-pointer">
|
<Item onClick={onCut} className="cursor-pointer">
|
||||||
<Scissors className="mr-2 h-4 w-4" />
|
<Scissors className="mr-2 h-4 w-4" />
|
||||||
Cut
|
{t('files.menu.cut')}
|
||||||
</Item>
|
</Item>
|
||||||
<Item onClick={onCopy} className="cursor-pointer">
|
<Item onClick={onCopy} className="cursor-pointer">
|
||||||
<Copy className="mr-2 h-4 w-4" />
|
<Copy className="mr-2 h-4 w-4" />
|
||||||
Copy
|
{t('files.menu.copy')}
|
||||||
</Item>
|
</Item>
|
||||||
{!multiSelected && (
|
{!multiSelected && (
|
||||||
<Item onClick={() => onDuplicate(entry)} className="cursor-pointer">
|
<Item onClick={() => onDuplicate(entry)} className="cursor-pointer">
|
||||||
<CopyPlus className="mr-2 h-4 w-4" />
|
<CopyPlus className="mr-2 h-4 w-4" />
|
||||||
Duplicate
|
{t('files.menu.duplicate')}
|
||||||
</Item>
|
</Item>
|
||||||
)}
|
)}
|
||||||
{!multiSelected && (
|
{!multiSelected && (
|
||||||
<Item onClick={onStartRename} className="cursor-pointer">
|
<Item onClick={onStartRename} className="cursor-pointer">
|
||||||
<Pencil className="mr-2 h-4 w-4" />
|
<Pencil className="mr-2 h-4 w-4" />
|
||||||
Rename
|
{t('files.menu.rename')}
|
||||||
</Item>
|
</Item>
|
||||||
)}
|
)}
|
||||||
{/* Compress, the missing half of Extract. `/download` already shelled out to zip but streamed the
|
{/* 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. */}
|
result away; nothing could make an archive and keep it. */}
|
||||||
<Item onClick={() => onCompress(entry)} className="cursor-pointer">
|
<Item onClick={() => onCompress(entry)} className="cursor-pointer">
|
||||||
<FileArchive className="mr-2 h-4 w-4" />
|
<FileArchive className="mr-2 h-4 w-4" />
|
||||||
{multiSelected ? 'Compress selection' : 'Compress'}
|
{multiSelected ? t('files.menu.compressSelection') : t('files.menu.compress')}
|
||||||
</Item>
|
</Item>
|
||||||
<Item onClick={() => onCopyPath(entry)} className="cursor-pointer">
|
<Item onClick={() => onCopyPath(entry)} 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')}
|
||||||
</Item>
|
</Item>
|
||||||
<Item onClick={() => onDownload(entry)} className="cursor-pointer">
|
<Item onClick={() => onDownload(entry)} className="cursor-pointer">
|
||||||
<Download className="mr-2 h-4 w-4" />
|
<Download className="mr-2 h-4 w-4" />
|
||||||
Download
|
{t('files.menu.download')}
|
||||||
</Item>
|
</Item>
|
||||||
<Separator />
|
<Separator />
|
||||||
{/* Pinning lived only in the widget, which is not where you meet a file worth keeping to hand. */}
|
{/* 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">
|
<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 ? <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>
|
||||||
<Item onClick={() => onChat(entry)} className="cursor-pointer">
|
<Item onClick={() => onChat(entry)} className="cursor-pointer">
|
||||||
<MessageSquare className="mr-2 h-4 w-4" />
|
<MessageSquare className="mr-2 h-4 w-4" />
|
||||||
Chat...
|
{t('files.menu.chat')}
|
||||||
</Item>
|
</Item>
|
||||||
{isDir && (
|
{isDir && (
|
||||||
<Item onClick={() => onCreateDashboard(entry)} className="cursor-pointer">
|
<Item onClick={() => onCreateDashboard(entry)} 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')}
|
||||||
</Item>
|
</Item>
|
||||||
)}
|
)}
|
||||||
<Separator />
|
<Separator />
|
||||||
<Item onClick={() => onDelete(entry)} className="text-destructive cursor-pointer">
|
<Item onClick={() => onDelete(entry)} className="text-destructive cursor-pointer">
|
||||||
<Trash2 className="mr-2 h-4 w-4" />
|
<Trash2 className="mr-2 h-4 w-4" />
|
||||||
Delete
|
{t('files.menu.delete')}
|
||||||
</Item>
|
</Item>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -593,6 +588,7 @@ export const FileItem = ({
|
|||||||
isPinned,
|
isPinned,
|
||||||
canPaste,
|
canPaste,
|
||||||
}: FileItemProps) => {
|
}: FileItemProps) => {
|
||||||
|
const { date } = useFormat();
|
||||||
const [renaming, setRenaming] = useState(false);
|
const [renaming, setRenaming] = useState(false);
|
||||||
const clickTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const clickTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
const isDir = entry.type === 'directory';
|
const isDir = entry.type === 'directory';
|
||||||
@@ -730,7 +726,7 @@ export const FileItem = ({
|
|||||||
{isDir ? '--' : formatSize(entry.size)}
|
{isDir ? '--' : formatSize(entry.size)}
|
||||||
</span>
|
</span>
|
||||||
<span className="hidden md:inline text-xs text-duck-dark/50 shrink-0 w-28 text-right">
|
<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>
|
</span>
|
||||||
<div className="file-item-ellipsis opacity-0 group-hover:opacity-100 data-[state=open]:opacity-100 transition-opacity shrink-0">
|
<div className="file-item-ellipsis opacity-0 group-hover:opacity-100 data-[state=open]:opacity-100 transition-opacity shrink-0">
|
||||||
<EllipsisMenu {...menuProps} onEnsureSelected={ensureSelected} />
|
<EllipsisMenu {...menuProps} onEnsureSelected={ensureSelected} />
|
||||||
@@ -780,7 +776,7 @@ export const FileItem = ({
|
|||||||
<span className="text-xs text-duck-dark/50">
|
<span className="text-xs text-duck-dark/50">
|
||||||
{isDir ? 'Folder' : formatSize(entry.size)}
|
{isDir ? 'Folder' : formatSize(entry.size)}
|
||||||
{' · '}
|
{' · '}
|
||||||
{formatDate(entry.modifiedAt)}
|
{date(entry.modifiedAt, { month: 'short', day: 'numeric', year: 'numeric' })}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</ContextMenuTrigger>
|
</ContextMenuTrigger>
|
||||||
|
|||||||
@@ -503,7 +503,25 @@
|
|||||||
"openFolder": "Open folder",
|
"openFolder": "Open folder",
|
||||||
"showLocation": "Show location",
|
"showLocation": "Show location",
|
||||||
"download": "Download",
|
"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": {
|
"taskRunner": {
|
||||||
|
|||||||
@@ -503,7 +503,25 @@
|
|||||||
"openFolder": "Abrir pasta",
|
"openFolder": "Abrir pasta",
|
||||||
"showLocation": "Mostrar localização",
|
"showLocation": "Mostrar localização",
|
||||||
"download": "Descarregar",
|
"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": {
|
"taskRunner": {
|
||||||
|
|||||||
Reference in New Issue
Block a user