fix: end Files and composer menu entries that open a dialog with …, and translate Share

The Files tests now look for the new menuAudit keys: the menu entries
moved to them so they can end in "…" while toolbar buttons keep the
plain label.
This commit is contained in:
Linus Rath
2026-11-04 05:32:23 +01:00
parent f92b7d942d
commit fc7b607ff7
8 changed files with 35 additions and 25 deletions
@@ -327,6 +327,8 @@ describe('composer-send-menu', () => {
const schedule = screen.getAllByRole('menuitem', { name: /schedule_send/ })[0];
const plugin = screen.getAllByRole('menuitem', { name: 'Send with Later' })[0];
expect(follows(schedule, plugin)).toBe(true);
// It opens a dialog, and shows the key that opens it from the composer.
expect(schedule).toHaveTextContent('Ctrl + Shift + Enter');
});
it('keeps a plain Send button without plugin entries or scheduled send', () => {
+3 -1
View File
@@ -56,6 +56,7 @@ import { debug } from "@/lib/debug";
import { toast } from "@/stores/toast-store";
import { useContextMenu, isContextMenuKey } from "@/hooks/use-context-menu";
import { ContextMenu, ContextMenuItem, ContextMenuSeparator } from "@/components/ui/context-menu";
import { formatShortcutCombo } from "@/lib/shortcut-keys";
import { sanitizeSignatureHtml, sanitizeSignatureHtmlForDisplay, sanitizeEmailHtml, escapeHtml, sanitizePluginBodyHtml } from "@/lib/email-sanitization";
import { buildReplySubject, buildForwardSubject } from "@/lib/subject-prefix";
import { isFilePreviewable } from "@/lib/file-preview";
@@ -592,6 +593,7 @@ export function EmailComposer({
replyTo
}: EmailComposerProps) {
const t = useTranslations('email_composer');
const tMenu = useTranslations('menuAudit');
const tCommon = useTranslations('common');
const tWindows = useTranslations('pwaWindows');
const tQuote = useTranslations('quote_header');
@@ -5813,7 +5815,7 @@ export function EmailComposer({
<ContextMenu ref={sendMenu.menuRef} isOpen={showSendMenu} position={sendMenu.contextMenu.position} onClose={closeSendMenu}>
{composerClient?.hasDelayedSend() && (
<ContextMenuItem icon={CalendarClock} label={t('schedule_send')} onClick={openScheduleDialog} testId="composer-schedule-send" />
<ContextMenuItem icon={CalendarClock} label={tMenu('schedule_send')} shortcut={formatShortcutCombo('Mod + Shift + Enter')} onClick={openScheduleDialog} testId="composer-schedule-send" />
)}
<ComposerPluginMenuItems
surface="composer-send-menu"
@@ -221,7 +221,7 @@ describe('FileBrowser', () => {
renderBrowser({ resources: [resource(OWN[1])], allFolders: [shared] });
fireEvent.contextMenu(within(tree()).getByText('Documents'));
fireEvent.click(await screen.findByRole('menuitem', { name: 'share' }));
fireEvent.click(await screen.findByRole('menuitem', { name: 'files_share' }));
expect(await screen.findByTestId('share-dialog')).toHaveTextContent('Documents:p1');
});
@@ -230,7 +230,7 @@ describe('FileBrowser', () => {
renderBrowser();
await screen.findAllByText('Team');
fireEvent.contextMenu(within(tree()).getByText('Team'));
expect(screen.queryByRole('menuitem', { name: 'share' })).toBeNull();
expect(screen.queryByRole('menuitem', { name: 'files_share' })).toBeNull();
});
it('hides dot files unless the setting shows them', () => {
@@ -284,7 +284,7 @@ describe('FileBrowser', () => {
it('offers new and upload entries on My files in the sidebar', () => {
renderBrowser();
fireEvent.contextMenu(within(tree()).getByText('my_files'));
for (const name of ['open', 'new_folder', 'new_text_file', 'upload_files', 'upload_folder']) {
for (const name of ['open', 'files_new_folder', 'files_new_text_file', 'files_upload_files', 'files_upload_folder']) {
expect(screen.getByRole('menuitem', { name })).toBeInTheDocument();
}
});
@@ -457,7 +457,7 @@ describe('FileBrowser', () => {
}
render(<Probe />);
const files = groups.find((g) => g.label === 'files');
expect(files?.commands.map((c) => c.label)).toEqual(expect.arrayContaining(['upload_files', 'new_folder', 'trash']));
expect(files?.commands.map((c) => c.label)).toEqual(expect.arrayContaining(['files_upload_files', 'files_new_folder', 'trash']));
fireEvent.keyDown(document.body, { key: '/' });
expect(document.activeElement).toHaveAttribute('role', 'searchbox');
+10 -8
View File
@@ -222,7 +222,9 @@ export function FileBrowser({
onShare,
}: FileBrowserProps) {
const t = useTranslations("files");
const newFileLabel = newFileFormats.length > 0 ? t("new_file") : t("new_text_file");
const tMenu = useTranslations("menuAudit");
// Menu entries that open a dialog end in "…".
const newFileLabel = newFileFormats.length > 0 ? tMenu("files_new_file") : tMenu("files_new_text_file");
const fileNameRules = useMemo(() => client?.getFileNameRules?.() ?? null, [client]);
const validateFileName = useCallback((name: string) => {
const problem = fileNameProblem(name, fileNameRules);
@@ -951,7 +953,7 @@ export function FileBrowser({
)}
{(canShare(resource) || (resource.isDirectory && !resource.isShared)) && <ContextMenuSeparator />}
{canShare(resource) && (
<ContextMenuItem icon={Share2} label={t("share")} onClick={() => { closeMenu(); setShareTargetId(resource.id); }} />
<ContextMenuItem icon={Share2} label={tMenu("files_share")} onClick={() => { closeMenu(); setShareTargetId(resource.id); }} />
)}
{resource.isDirectory && !resource.isShared && (
<ContextMenuItem
@@ -992,10 +994,10 @@ export function FileBrowser({
const newItems = (
<>
<ContextMenuItem icon={Upload} label={t("upload_files")} onClick={() => { closeMenu(); fileInputRef.current?.click(); }} />
<ContextMenuItem icon={FolderUp} label={t("upload_folder")} onClick={() => { closeMenu(); folderInputRef.current?.click(); }} />
<ContextMenuItem icon={Upload} label={tMenu("files_upload_files")} onClick={() => { closeMenu(); fileInputRef.current?.click(); }} />
<ContextMenuItem icon={FolderUp} label={tMenu("files_upload_folder")} onClick={() => { closeMenu(); folderInputRef.current?.click(); }} />
<ContextMenuSeparator />
<ContextMenuItem icon={FolderPlus} label={t("new_folder")} onClick={() => { closeMenu(); setShowNewFolder(true); }} />
<ContextMenuItem icon={FolderPlus} label={tMenu("files_new_folder")} onClick={() => { closeMenu(); setShowNewFolder(true); }} />
<ContextMenuItem icon={FilePlus} label={newFileLabel} onClick={() => { closeMenu(); setShowNewTextFile(true); }} />
</>
);
@@ -1035,9 +1037,9 @@ export function FileBrowser({
const tShell = useTranslations("shell");
const isEmbedded = useIsEmbedded();
useRegisterCommands(tSidebar("files"), [
{ id: "upload-files", label: t("upload_files"), icon: Upload, run: () => fileInputRef.current?.click(), when: () => canCreate },
{ id: "upload-folder", label: t("upload_folder"), icon: FolderUp, run: () => folderInputRef.current?.click(), when: () => canCreate },
{ id: "new-folder", label: t("new_folder"), icon: FolderPlus, run: () => setShowNewFolder(true), when: () => canCreate },
{ id: "upload-files", label: tMenu("files_upload_files"), icon: Upload, run: () => fileInputRef.current?.click(), when: () => canCreate },
{ id: "upload-folder", label: tMenu("files_upload_folder"), icon: FolderUp, run: () => folderInputRef.current?.click(), when: () => canCreate },
{ id: "new-folder", label: tMenu("files_new_folder"), icon: FolderPlus, run: () => setShowNewFolder(true), when: () => canCreate },
{ id: "new-text-file", label: newFileLabel, icon: FilePlus, run: () => setShowNewTextFile(true), when: () => canCreate },
{ id: "trash", label: t("trash"), icon: Trash2, run: () => onNavigate(TRASH_PATH, TRASH_LOCATION), when: () => !inTrash && !accountPickerMode },
{ id: "empty-trash", label: t("empty_trash"), icon: Trash2, run: () => onEmptyTrash?.(), when: () => inTrash && trashCount > 0 && !!onEmptyTrash },
+2 -1
View File
@@ -56,6 +56,7 @@ export function FileDetailsPanel({
iconOptions, getImageUrl, onClose, onOpen, onEdit, onDownload, onShare, onToggleStar, className, pluginFile,
}: FileDetailsPanelProps) {
const t = useTranslations("files");
const tMenu = useTranslations("menuAudit");
const pluginProps = useMemo(() => (pluginFile ? { file: pluginFile } : null), [pluginFile]);
const header = (
@@ -147,7 +148,7 @@ export function FileDetailsPanel({
)}
{canShare && (
<Button size="sm" variant="outline" className="h-8 gap-1.5" onClick={onShare}>
<Share2 className="w-4 h-4" />{t("share")}
<Share2 className="w-4 h-4" />{tMenu("files_share_button")}
</Button>
)}
{resource.isDirectory && !resource.isShared && (
+2 -1
View File
@@ -223,6 +223,7 @@ function FileListRow({ resource, isSelected, isFocused, isTabStop, isDropTarget,
note?: string;
}) {
const t = useTranslations("files");
const tMenu = useTranslations("menuAudit");
const touchProps = useTouchPress(touch, selecting, resource, handlers);
const icon = <ResourceIcon resource={resource} size="sm" options={iconOptions} />;
return (
@@ -299,7 +300,7 @@ function FileListRow({ resource, isSelected, isFocused, isTabStop, isDropTarget,
<RowAction icon={Download} label={t("download")} onClick={() => handlers.onDownload?.(resource)} />
)}
{!touch && handlers.canShare(resource) && (
<RowAction icon={Share2} label={t("share")} onClick={() => handlers.onShare(resource)} />
<RowAction icon={Share2} label={tMenu("files_share_button")} onClick={() => handlers.onShare(resource)} />
)}
<IconButton
data-item-action
+2 -1
View File
@@ -120,6 +120,7 @@ export function FilesHeader({
panelToggle,
}: FilesHeaderProps) {
const t = useTranslations("files");
const tMenu = useTranslations("menuAudit");
const sortMenu = useContextMenu<null>();
const overflowMenu = useContextMenu<Crumb[]>();
const [searchOpen, setSearchOpen] = useState(false);
@@ -159,7 +160,7 @@ export function FilesHeader({
<ToolButton icon={Download} label={t("download")} onClick={selection.onDownload} compact />
)}
{selection.canShare && (
<ToolButton icon={Share2} label={t("share")} onClick={selection.onShare} compact />
<ToolButton icon={Share2} label={tMenu("files_share_button")} onClick={selection.onShare} compact />
)}
{selection.canRename && (
<ToolButton icon={Pencil} label={t("rename")} onClick={selection.onRename} compact />
+10 -9
View File
@@ -125,6 +125,7 @@ export function FilesSidebar({
onDropToTrash,
}: FilesSidebarProps) {
const t = useTranslations("files");
const tMenu = useTranslations("menuAudit");
const newMenu = useContextMenu<null>();
const itemMenu = useContextMenu<MenuTarget>();
const newButtonRef = useRef<HTMLButtonElement>(null);
@@ -274,10 +275,10 @@ export function FilesSidebar({
const createItems = (path: string, id: string | null) => (
<>
<ContextMenuSeparator />
<ContextMenuItem icon={FolderPlus} label={t("new_folder")} onClick={() => run(() => onCreateIn(path, id, "folder"))} />
<ContextMenuItem icon={FilePlus} label={newFileLabel ?? t("new_text_file")} onClick={() => run(() => onCreateIn(path, id, "text"))} />
<ContextMenuItem icon={Upload} label={t("upload_files")} onClick={() => run(() => onCreateIn(path, id, "files"))} />
<ContextMenuItem icon={FolderUp} label={t("upload_folder")} onClick={() => run(() => onCreateIn(path, id, "dir"))} />
<ContextMenuItem icon={FolderPlus} label={tMenu("files_new_folder")} onClick={() => run(() => onCreateIn(path, id, "folder"))} />
<ContextMenuItem icon={FilePlus} label={newFileLabel ?? tMenu("files_new_text_file")} onClick={() => run(() => onCreateIn(path, id, "text"))} />
<ContextMenuItem icon={Upload} label={tMenu("files_upload_files")} onClick={() => run(() => onCreateIn(path, id, "files"))} />
<ContextMenuItem icon={FolderUp} label={tMenu("files_upload_folder")} onClick={() => run(() => onCreateIn(path, id, "dir"))} />
</>
);
@@ -455,11 +456,11 @@ export function FilesSidebar({
</nav>
<ContextMenu ref={newMenu.menuRef} isOpen={newMenu.contextMenu.isOpen} position={newMenu.contextMenu.position} onClose={newMenu.closeContextMenu}>
<ContextMenuItem icon={Upload} label={t("upload_files")} onClick={() => { newMenu.closeContextMenu(); onUploadFiles(); }} />
<ContextMenuItem icon={FolderUp} label={t("upload_folder")} onClick={() => { newMenu.closeContextMenu(); onUploadFolder(); }} />
<ContextMenuItem icon={Upload} label={tMenu("files_upload_files")} onClick={() => { newMenu.closeContextMenu(); onUploadFiles(); }} />
<ContextMenuItem icon={FolderUp} label={tMenu("files_upload_folder")} onClick={() => { newMenu.closeContextMenu(); onUploadFolder(); }} />
<ContextMenuSeparator />
<ContextMenuItem icon={FolderPlus} label={t("new_folder")} onClick={() => { newMenu.closeContextMenu(); onNewFolder(); }} />
<ContextMenuItem icon={FilePlus} label={newFileLabel ?? t("new_text_file")} onClick={() => { newMenu.closeContextMenu(); onNewTextFile(); }} />
<ContextMenuItem icon={FolderPlus} label={tMenu("files_new_folder")} onClick={() => { newMenu.closeContextMenu(); onNewFolder(); }} />
<ContextMenuItem icon={FilePlus} label={newFileLabel ?? tMenu("files_new_text_file")} onClick={() => { newMenu.closeContextMenu(); onNewTextFile(); }} />
</ContextMenu>
<ContextMenu ref={itemMenu.menuRef} isOpen={itemMenu.contextMenu.isOpen} position={itemMenu.contextMenu.position} onClose={itemMenu.closeContextMenu}>
@@ -484,7 +485,7 @@ export function FilesSidebar({
Details act on the list and are left out here (R6). */}
{(!folder.isShared || canShare(folder)) && <ContextMenuSeparator />}
{canShare(folder) && (
<ContextMenuItem icon={Share2} label={t("share")} onClick={() => run(() => onShare(folder))} />
<ContextMenuItem icon={Share2} label={tMenu("files_share")} onClick={() => run(() => onShare(folder))} />
)}
{!folder.isShared && (
<ContextMenuItem