Skip to content

Commit c8c6f83

Browse files
author
SSMG4
committed
fix: FTP download crash, recent connections pre-fill, dialog aria warnings, connection dialog UX
server/index.js: - Fix FTP download: replace plain object with proper Writable stream so basic-ftp can call .once() without crashing RecentConnectionsDialog: - Replace onConnect (which sent anonymous credentials) with onPrefill that opens ConnectionDialog pre-filled — user now enters password manually ConnectionDialog: - Add prefill prop for pre-populating from recent connections - Password show/hide eye icon (appears only after first character typed) - Anonymous shown as placeholder, not pre-filled value - Save button between Cancel and Connect — saves directly to savedConnections Index.tsx: - Wire connectionPrefill state and onPrefill handler - Wire onSave handler to persist connection without navigating away Dialogs (aria fix): - Add DialogDescription to BookmarksDialog, RecentConnectionsDialog, SavedConnectionsDialog, CreateFolderDialog, CreateFileDialog, FileProperties — fixes "Missing Description for DialogContent" warning
1 parent 4b11e30 commit c8c6f83

10 files changed

Lines changed: 110 additions & 33 deletions

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -302,4 +302,3 @@ WebFTP is licensed under the **GNU General Public License v3.0**.
302302
See the [LICENSE](LICENSE) file for the full license text.
303303

304304
You are free to use, modify, and distribute this software under the terms of the GPL-3.0. Any derivative work must also be distributed under the same license.
305-

server/index.js

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -222,12 +222,20 @@ app.post('/api/download', async (req, res) => {
222222
res.setHeader('Content-Type', 'application/octet-stream');
223223

224224
if (session.type === 'ftp') {
225+
const { Writable } = await import('stream');
225226
const chunks = [];
226-
const writable = {
227-
write(chunk) { chunks.push(chunk); },
228-
end() { res.end(Buffer.concat(chunks)); },
229-
};
230-
await session.client.downloadTo(writable, remotePath);
227+
const writable = new Writable({
228+
write(chunk, _encoding, callback) {
229+
chunks.push(chunk);
230+
callback();
231+
},
232+
});
233+
await new Promise((resolve, reject) => {
234+
writable.on('finish', resolve);
235+
writable.on('error', reject);
236+
session.client.downloadTo(writable, remotePath).then(resolve).catch(reject);
237+
});
238+
res.end(Buffer.concat(chunks));
231239
return;
232240
}
233241

src/components/BookmarksDialog.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { ScrollArea } from '@/components/ui/scroll-area';
77
import {
88
Dialog,
99
DialogContent,
10+
DialogDescription,
1011
DialogHeader,
1112
DialogTitle,
1213
} from '@/components/ui/dialog';
@@ -48,6 +49,9 @@ export const BookmarksDialog = ({ open, onOpenChange, onNavigate }: BookmarksDia
4849
<Bookmark className="h-5 w-5" />
4950
Bookmarks
5051
</DialogTitle>
52+
<DialogDescription>
53+
Your saved folder bookmarks. Right-click any folder to add one.
54+
</DialogDescription>
5155
</DialogHeader>
5256

5357
<ScrollArea className="h-[400px]">

src/components/ConnectionDialog.tsx

Lines changed: 49 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
99
import { Switch } from '@/components/ui/switch';
1010
import { ConnectOptions, Protocol } from '@/types/ftp';
1111
import { useAuth } from '@/contexts/AuthContext';
12-
import { Lock, ChevronDown } from 'lucide-react';
12+
import { Lock, ChevronDown, Eye, EyeOff, Save } from 'lucide-react';
1313

1414
interface ConnectionDialogProps {
1515
open: boolean;
1616
onOpenChange: (open: boolean) => void;
1717
onConnect: (options: ConnectOptions) => void;
18+
onSave?: (options: ConnectOptions) => void;
1819
isConnecting: boolean;
20+
prefill?: Partial<ConnectOptions>;
1921
}
2022

2123
const DEFAULT_PORTS: Record<Protocol, number> = {
@@ -33,12 +35,14 @@ export const ConnectionDialog = ({
3335
open,
3436
onOpenChange,
3537
onConnect,
38+
onSave,
3639
isConnecting,
40+
prefill,
3741
}: ConnectionDialogProps) => {
3842
const [formData, setFormData] = useState<ConnectOptions>({
3943
host: '',
4044
port: 21,
41-
username: 'anonymous',
45+
username: '',
4246
password: '',
4347
protocol: 'ftp',
4448
});
@@ -48,6 +52,7 @@ export const ConnectionDialog = ({
4852
const [showSftpMore, setShowSftpMore] = useState(false);
4953
const [showSmbMore, setShowSmbMore] = useState(false);
5054
const [showWebdavMore, setShowWebdavMore] = useState(false);
55+
const [showPassword, setShowPassword] = useState(false);
5156
const [keyFile, setKeyFile] = useState<File | null>(null);
5257
const { user } = useAuth();
5358

@@ -71,10 +76,22 @@ export const ConnectionDialog = ({
7176
setKeyFile(null);
7277
}, [formData.protocol]);
7378

74-
// Reset on open
79+
// Reset on open, applying any prefill from recent connections
7580
useEffect(() => {
7681
if (open) {
77-
setFormData(prev => ({ ...prev, protocol: 'ftp', port: 21, username: 'anonymous' }));
82+
setFormData({
83+
host: prefill?.host ?? '',
84+
port: prefill?.port ?? 21,
85+
username: prefill?.username ?? '',
86+
password: '',
87+
protocol: prefill?.protocol ?? 'ftp',
88+
});
89+
setShowPassword(false);
90+
setShowFtpMore(false);
91+
setShowSftpMore(false);
92+
setShowSmbMore(false);
93+
setShowWebdavMore(false);
94+
setKeyFile(null);
7895
}
7996
}, [open]);
8097

@@ -134,12 +151,24 @@ export const ConnectionDialog = ({
134151
</div>
135152
<div className="grid gap-2">
136153
<Label htmlFor="password">Password</Label>
137-
<Input
138-
id="password"
139-
type="password"
140-
value={formData.password}
141-
onChange={(e) => setFormData(prev => ({ ...prev, password: e.target.value }))}
142-
/>
154+
<div className="relative">
155+
<Input
156+
id="password"
157+
type={showPassword ? 'text' : 'password'}
158+
value={formData.password}
159+
onChange={(e) => setFormData(prev => ({ ...prev, password: e.target.value }))}
160+
className={formData.password ? 'pr-10' : ''}
161+
/>
162+
{formData.password && (
163+
<button
164+
type="button"
165+
onClick={() => setShowPassword(v => !v)}
166+
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
167+
>
168+
{showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
169+
</button>
170+
)}
171+
</div>
143172
</div>
144173
</>
145174
);
@@ -462,6 +491,16 @@ export const ConnectionDialog = ({
462491
<Button type="button" variant="outline" onClick={() => onOpenChange(false)} disabled={isConnecting}>
463492
Cancel
464493
</Button>
494+
{onSave && (
495+
<Button
496+
type="button"
497+
onClick={() => onSave(formData)}
498+
disabled={!formData.host || isConnecting}
499+
>
500+
<Save className="h-4 w-4 mr-2" />
501+
Save
502+
</Button>
503+
)}
465504
<Button type="submit" disabled={isConnecting}>
466505
{isConnecting ? 'Connecting...' : 'Connect'}
467506
</Button>

src/components/CreateFileDialog.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
DialogContent,
66
DialogHeader,
77
DialogTitle,
8+
DialogDescription,
89
DialogFooter,
910
} from '@/components/ui/dialog';
1011
import { Input } from '@/components/ui/input';
@@ -37,6 +38,7 @@ export const CreateFileDialog = ({ open, onOpenChange, onCreateFile }: CreateFil
3738
<FileText className="h-5 w-5 text-muted-foreground" />
3839
Create New File
3940
</DialogTitle>
41+
<DialogDescription>Enter a name for the new file.</DialogDescription>
4042
</DialogHeader>
4143
<form onSubmit={handleSubmit}>
4244
<div className="space-y-4 py-4">

src/components/CreateFolderDialog.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
DialogContent,
66
DialogHeader,
77
DialogTitle,
8+
DialogDescription,
89
DialogFooter,
910
} from '@/components/ui/dialog';
1011
import { Input } from '@/components/ui/input';
@@ -37,6 +38,7 @@ export const CreateFolderDialog = ({ open, onOpenChange, onCreateFolder }: Creat
3738
<FolderPlus className="h-5 w-5 text-accent" />
3839
Create New Folder
3940
</DialogTitle>
41+
<DialogDescription>Enter a name for the new folder.</DialogDescription>
4042
</DialogHeader>
4143
<form onSubmit={handleSubmit}>
4244
<div className="space-y-4 py-4">

src/components/FileProperties.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
DialogContent,
99
DialogHeader,
1010
DialogTitle,
11+
DialogDescription,
1112
} from '@/components/ui/dialog';
1213
import { File, Folder } from 'lucide-react';
1314

@@ -36,6 +37,7 @@ export const FileProperties = ({
3637
)}
3738
Properties
3839
</DialogTitle>
40+
<DialogDescription>Details for {file.name}</DialogDescription>
3941
</DialogHeader>
4042

4143
<div className="space-y-4">

src/components/RecentConnectionsDialog.tsx

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { ScrollArea } from '@/components/ui/scroll-area';
77
import {
88
Dialog,
99
DialogContent,
10+
DialogDescription,
1011
DialogHeader,
1112
DialogTitle,
1213
} from '@/components/ui/dialog';
@@ -23,7 +24,8 @@ interface RecentConnection {
2324
interface RecentConnectionsDialogProps {
2425
open: boolean;
2526
onOpenChange: (open: boolean) => void;
26-
onConnect: (options: ConnectOptions) => void;
27+
// Called with a partial ConnectOptions so the ConnectionDialog opens pre-filled
28+
onPrefill: (partial: Partial<ConnectOptions>) => void;
2729
}
2830

2931
function getStorageKey(userId: string | undefined): string {
@@ -53,13 +55,12 @@ function formatTimestamp(timestamp: number): string {
5355
return `${diffDays} days ago`;
5456
}
5557

56-
export const RecentConnectionsDialog = ({ open, onOpenChange, onConnect }: RecentConnectionsDialogProps) => {
58+
export const RecentConnectionsDialog = ({ open, onOpenChange, onPrefill }: RecentConnectionsDialogProps) => {
5759
const { user } = useAuth();
5860
const [recentConnections, setRecentConnections] = useState<RecentConnection[]>(() =>
5961
loadRecent(user?.id)
6062
);
6163

62-
// Refresh when dialog opens
6364
const handleOpenChange = (v: boolean) => {
6465
if (v) setRecentConnections(loadRecent(user?.id));
6566
onOpenChange(v);
@@ -73,22 +74,23 @@ export const RecentConnectionsDialog = ({ open, onOpenChange, onConnect }: Recen
7374
};
7475

7576
const handleClearAll = () => {
76-
const key = getStorageKey(user?.id);
77-
localStorage.removeItem(key);
77+
localStorage.removeItem(getStorageKey(user?.id));
7878
setRecentConnections([]);
7979
};
8080

81-
// Reconnect with minimal options — the user will need to re-enter password
82-
// (we deliberately don't store passwords in recent connections)
83-
const handleConnect = (conn: RecentConnection) => {
84-
onConnect({
81+
// Open ConnectionDialog pre-filled with host and protocol so user can enter password
82+
const handleSelect = (conn: RecentConnection) => {
83+
onOpenChange(false);
84+
onPrefill({
8585
host: conn.host,
86-
port: 21,
87-
username: 'anonymous',
88-
password: '',
86+
port: conn.protocol === 'sftp' || conn.protocol === 'ssh' || conn.protocol === 'scp' ? 22
87+
: conn.protocol === 'smb' ? 445
88+
: conn.protocol === 'webdav' ? 443
89+
: 21,
8990
protocol: (conn.protocol as ConnectOptions['protocol']) || 'ftp',
91+
username: '',
92+
password: '',
9093
});
91-
onOpenChange(false);
9294
};
9395

9496
return (
@@ -106,6 +108,9 @@ export const RecentConnectionsDialog = ({ open, onOpenChange, onConnect }: Recen
106108
</Button>
107109
)}
108110
</div>
111+
<DialogDescription>
112+
Select a recent connection to open the connection form pre-filled.
113+
</DialogDescription>
109114
</DialogHeader>
110115

111116
<ScrollArea className="h-[400px]">
@@ -119,9 +124,10 @@ export const RecentConnectionsDialog = ({ open, onOpenChange, onConnect }: Recen
119124
recentConnections.map(conn => (
120125
<div
121126
key={conn.id}
122-
className="flex items-center justify-between p-3 border border-border rounded-lg hover:border-primary transition-colors group"
127+
className="flex items-center justify-between p-3 border border-border rounded-lg hover:border-primary transition-colors group cursor-pointer"
128+
onClick={() => handleSelect(conn)}
123129
>
124-
<div className="flex-1 cursor-pointer min-w-0" onClick={() => handleConnect(conn)}>
130+
<div className="flex-1 min-w-0">
125131
<p className="font-medium truncate">{conn.host}</p>
126132
<p className="text-sm text-muted-foreground">
127133
{(conn.protocol || 'FTP').toUpperCase()}{formatTimestamp(conn.timestamp)}
@@ -131,7 +137,7 @@ export const RecentConnectionsDialog = ({ open, onOpenChange, onConnect }: Recen
131137
size="sm"
132138
variant="ghost"
133139
className="opacity-0 group-hover:opacity-100 transition-opacity shrink-0"
134-
onClick={() => handleDelete(conn.id)}
140+
onClick={(e) => { e.stopPropagation(); handleDelete(conn.id); }}
135141
>
136142
<Trash2 className="h-4 w-4" />
137143
</Button>

src/components/SavedConnectionsDialog.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import {
44
Dialog,
55
DialogContent,
6+
DialogDescription,
67
DialogHeader,
78
DialogTitle,
89
} from '@/components/ui/dialog';
@@ -26,6 +27,9 @@ export const SavedConnectionsDialog = ({ open, onOpenChange, onConnect }: SavedC
2627
<DialogContent className="max-w-2xl">
2728
<DialogHeader>
2829
<DialogTitle>Saved Connections</DialogTitle>
30+
<DialogDescription>
31+
Your saved server connections. Click one to connect.
32+
</DialogDescription>
2933
</DialogHeader>
3034
<SavedConnections onConnect={handleConnect} />
3135
</DialogContent>

src/pages/Index.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ const Index = () => {
105105

106106
// View state
107107
const [connectionDialogOpen, setConnectionDialogOpen] = useState(false);
108+
const [connectionPrefill, setConnectionPrefill] = useState<Partial<ConnectOptions> | undefined>(undefined);
108109
const [selectedFile, setSelectedFile] = useState<FtpEntry>();
109110
const [dragActive, setDragActive] = useState(false);
110111
const [editingFile, setEditingFile] = useState<{ path: string; name: string; content: string } | null>(null);
@@ -506,7 +507,14 @@ const Index = () => {
506507
open={connectionDialogOpen}
507508
onOpenChange={setConnectionDialogOpen}
508509
onConnect={handleConnect}
510+
onSave={(options) => {
511+
const stored = JSON.parse(localStorage.getItem('savedConnections') || '[]');
512+
const entry = { ...options, id: Date.now().toString(), name: options.displayName || options.host };
513+
localStorage.setItem('savedConnections', JSON.stringify([...stored, entry]));
514+
toast({ title: 'Connection saved', description: `${entry.name} saved to connections.` });
515+
}}
509516
isConnecting={isConnecting}
517+
prefill={connectionPrefill}
510518
/>
511519

512520
{/* File Editor */}
@@ -540,7 +548,10 @@ const Index = () => {
540548
<RecentConnectionsDialog
541549
open={recentConnectionsOpen}
542550
onOpenChange={setRecentConnectionsOpen}
543-
onConnect={handleConnect}
551+
onPrefill={(partial) => {
552+
setConnectionPrefill(partial);
553+
setConnectionDialogOpen(true);
554+
}}
544555
/>
545556

546557
{/* Saved Connections */}

0 commit comments

Comments
 (0)