0.37.5: overwrite prompt for file upload + auto-reveal uploaded hidden files
The reported "upload doesn't work, file never appears" was two things, both
hit when uploading config files like .env:
1. Single-file upload used overwrite=false and dead-ended on "Already
exists: .env — rename or remove the existing file first." with no way to
replace the file. Now a conflict opens an Overwrite confirmation dialog
(mirroring the copy/paste conflict flow) that retries with overwrite=true.
2. .env (and any dotfile) is hidden, so even a successful upload stayed
invisible unless "Show hidden" was on. After an upload whose name/path
has a dot-segment, "Show hidden" is now auto-enabled so the file shows.
The single-file upload mutation now takes {file, overwrite}; folder upload
(already overwrite=true) also auto-reveals hidden results.
Frontend-only; all 3 images rebuilt+pushed 0.37.5.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
98d756faf6
commit
a40dd0de3e
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
"""Single source of truth for the StackPilot release version."""
|
"""Single source of truth for the StackPilot release version."""
|
||||||
|
|
||||||
APP_VERSION = "0.37.4"
|
APP_VERSION = "0.37.5"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "stackpilot-frontend",
|
"name": "stackpilot-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.37.4",
|
"version": "0.37.5",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ export function Files() {
|
|||||||
const [deleting, setDeleting] = useState<HostPathEntry | null>(null);
|
const [deleting, setDeleting] = useState<HostPathEntry | null>(null);
|
||||||
const [newKind, setNewKind] = useState<"dir" | "file" | null>(null);
|
const [newKind, setNewKind] = useState<"dir" | "file" | null>(null);
|
||||||
const [clip, setClip] = useState<Clipboard | null>(null);
|
const [clip, setClip] = useState<Clipboard | null>(null);
|
||||||
|
const [uploadConflict, setUploadConflict] = useState<File | null>(null);
|
||||||
const [pasteConflict, setPasteConflict] = useState(false);
|
const [pasteConflict, setPasteConflict] = useState(false);
|
||||||
const [progress, setProgress] = useState<{
|
const [progress, setProgress] = useState<{
|
||||||
label: string;
|
label: string;
|
||||||
@@ -131,10 +132,18 @@ export function Files() {
|
|||||||
|
|
||||||
const refresh = () => qc.invalidateQueries({ queryKey: ["files"] });
|
const refresh = () => qc.invalidateQueries({ queryKey: ["files"] });
|
||||||
|
|
||||||
|
// An uploaded dotfile (e.g. .env) is hidden by default — auto-reveal it so
|
||||||
|
// the user can actually see what they just uploaded.
|
||||||
|
const revealIfHidden = (...names: string[]) => {
|
||||||
|
if (!showHidden && names.some((n) => n.split("/").some((seg) => seg.startsWith(".")))) {
|
||||||
|
setShowHidden(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const upload = useMutation({
|
const upload = useMutation({
|
||||||
mutationFn: (file: File) => {
|
mutationFn: ({ file, overwrite }: { file: File; overwrite: boolean }) => {
|
||||||
setProgress({ label: file.name, pct: 0, kind: "upload" });
|
setProgress({ label: file.name, pct: 0, kind: "upload" });
|
||||||
return filesApi.upload(path, file, false, "", host, (pct) =>
|
return filesApi.upload(path, file, overwrite, "", host, (pct) =>
|
||||||
setProgress({
|
setProgress({
|
||||||
label: file.name,
|
label: file.name,
|
||||||
pct,
|
pct,
|
||||||
@@ -145,12 +154,15 @@ export function Files() {
|
|||||||
},
|
},
|
||||||
onSuccess: (r) => {
|
onSuccess: (r) => {
|
||||||
toast.success(`Uploaded ${r.name}`);
|
toast.success(`Uploaded ${r.name}`);
|
||||||
|
setUploadConflict(null);
|
||||||
|
revealIfHidden(r.name);
|
||||||
refresh();
|
refresh();
|
||||||
},
|
},
|
||||||
onError: (e: unknown) => {
|
onError: (e: unknown, vars) => {
|
||||||
const msg = apiErrorMessage(e);
|
const msg = apiErrorMessage(e);
|
||||||
|
// Offer to overwrite instead of dead-ending on "Already exists".
|
||||||
if (msg.startsWith("Already exists")) {
|
if (msg.startsWith("Already exists")) {
|
||||||
toast.error(`${msg} — rename or remove the existing file first.`);
|
setUploadConflict(vars.file);
|
||||||
} else {
|
} else {
|
||||||
toast.error(msg);
|
toast.error(msg);
|
||||||
}
|
}
|
||||||
@@ -201,9 +213,12 @@ export function Files() {
|
|||||||
}
|
}
|
||||||
return { ok, failed };
|
return { ok, failed };
|
||||||
},
|
},
|
||||||
onSuccess: ({ ok, failed }) => {
|
onSuccess: ({ ok, failed }, files) => {
|
||||||
if (failed) toast.warning(`Uploaded ${ok} file(s), ${failed} failed`);
|
if (failed) toast.warning(`Uploaded ${ok} file(s), ${failed} failed`);
|
||||||
else toast.success(`Uploaded ${ok} file(s)`);
|
else toast.success(`Uploaded ${ok} file(s)`);
|
||||||
|
revealIfHidden(
|
||||||
|
...files.map((f) => (f as File & { webkitRelativePath?: string }).webkitRelativePath || f.name),
|
||||||
|
);
|
||||||
refresh();
|
refresh();
|
||||||
},
|
},
|
||||||
onError: (e) => toast.error(apiErrorMessage(e)),
|
onError: (e) => toast.error(apiErrorMessage(e)),
|
||||||
@@ -335,7 +350,7 @@ export function Files() {
|
|||||||
className="hidden"
|
className="hidden"
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const f = e.target.files?.[0];
|
const f = e.target.files?.[0];
|
||||||
if (f) upload.mutate(f);
|
if (f) upload.mutate({ file: f, overwrite: false });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/* webkitdirectory is set imperatively via the callback ref. */}
|
{/* webkitdirectory is set imperatively via the callback ref. */}
|
||||||
@@ -610,6 +625,18 @@ export function Files() {
|
|||||||
onCancel={() => setPasteConflict(false)}
|
onCancel={() => setPasteConflict(false)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{uploadConflict && (
|
||||||
|
<ConfirmDialog
|
||||||
|
title={`“${uploadConflict.name}” already exists here`}
|
||||||
|
message={`Overwrite the existing file at ${path}?`}
|
||||||
|
confirmLabel="Overwrite"
|
||||||
|
danger
|
||||||
|
busy={upload.isPending}
|
||||||
|
onConfirm={() => upload.mutate({ file: uploadConflict, overwrite: true })}
|
||||||
|
onCancel={() => setUploadConflict(null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user