Skip to content

fix(project): prompt for a file name on Save As in browsers without the save picker#458

Merged
giswqs merged 2 commits into
mainfrom
fix/save-as-prompt-browser
Jun 18, 2026
Merged

fix(project): prompt for a file name on Save As in browsers without the save picker#458
giswqs merged 2 commits into
mainfrom
fix/save-as-prompt-browser

Conversation

@giswqs

@giswqs giswqs commented Jun 18, 2026

Copy link
Copy Markdown
Member

Summary

  • Fixes Issue: Project - Save as function does not work as expected #448. In Firefox and Safari, which lack the File System Access API (window.showSaveFilePicker), both Save and Save As fell back to an anchor download under a fixed default name, so users could never name the project file.
  • Adds a "Save project as" dialog that prompts for a file name in that browser-download fallback. It appears for Save As (and a first Save with no established name); later in-place Saves reuse the chosen name silently. Chromium browsers (native picker) and the Tauri desktop app (native save dialog) are unaffected.
  • A typed name without a recognized extension gets .geolibre.json appended automatically.

Test plan

  • Full build / typecheck passes (npm run typecheck)
  • i18n catalog test passes
  • Verified in a real browser with window.showSaveFilePicker removed: Save As opens the name dialog prefilled with Untitled Project.geolibre.json; entering my-custom-map downloads my-custom-map.geolibre.json, which parses as a valid project file

Summary by CodeRabbit

  • New Features
    • Added a controlled “Save project as” dialog to let users choose a custom project filename during saving.
    • Improved cross-environment saving by introducing a browser “Save As” fallback when file system saving isn’t available.
  • Bug Fixes
    • Prevented overlapping save actions that could interfere with the filename prompt and save results.
  • Documentation
    • Added new i18n strings for the “Save project as” dialog UI (title, description, label, placeholder).

…he save picker

Firefox and Safari lack the File System Access API, so Save and Save As both fell
back to an anchor download under a fixed default name, leaving users unable to name
the project file. Prompt for a name in that fallback so Save As (and a first Save)
honor the user's choice.
@netlify

netlify Bot commented Jun 18, 2026

Copy link
Copy Markdown

Deploy Preview for geolibre-app ready!

Name Link
🔨 Latest commit e69756e
🔍 Latest deploy log https://app.netlify.com/projects/geolibre-app/deploys/6a334a35c1c3240008ac5491
😎 Deploy Preview https://deploy-preview-458--geolibre-app.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: e5b86842-4cd2-42c1-8c76-fe2d3dd66cef

📥 Commits

Reviewing files that changed from the base of the PR and between 866ee27 and e69756e.

📒 Files selected for processing (2)
  • apps/geolibre-desktop/src/components/layout/toolbar/ProjectFileDialogs.tsx
  • apps/geolibre-desktop/src/hooks/useProjectFileActions.ts

📝 Walkthrough

Walkthrough

Adds a "Save project as" naming dialog for browsers that lack the File System Access API (showSaveFilePicker). A new browserSaveFallsBackToDownload() helper detects the fallback condition. The useProjectFileActions hook gains a promise-based prompt flow with filename sanitization and concurrency safety, and ProjectFileDialogs renders the new dialog wired to that hook state.

Changes

Save As Naming Flow

Layer / File(s) Summary
Runtime environment detection
apps/geolibre-desktop/src/lib/tauri-io.ts
Adds browserSaveFallsBackToDownload(), which returns true only in browser environments where window.showSaveFilePicker is absent, and false under Tauri or when window is unavailable.
Hook types, state, and prompt lifecycle
apps/geolibre-desktop/src/hooks/useProjectFileActions.ts
Exports SaveNamePrompt interface and ensureProjectFileName() sanitizer. Adds saveNamePrompt/saveNameInput React state and isSavingRef concurrency guard. Implements askSaveName(), submitSaveNamePrompt, and cancelSaveNamePrompt to manage the prompt promise lifecycle.
Save project logic with conditional prompt
apps/geolibre-desktop/src/hooks/useProjectFileActions.ts
Updates saveProject to conditionally await the filename prompt when the runtime falls back to download mode. Wraps save logic in a concurrency-safe guard and threads the sanitized filename into saveProjectFile. Extends hook's returned API to expose prompt state and handlers.
Dialog UI and i18n
apps/geolibre-desktop/src/components/layout/toolbar/ProjectFileDialogs.tsx, apps/geolibre-desktop/src/i18n/locales/en.json
Renders a controlled Save As Dialog gated on saveNamePrompt !== null with an autofocus filename input, cancel and save buttons, and four new English translation keys for title, description, field label, and placeholder.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant ProjectFileDialogs
  participant useProjectFileActions
  participant browserSaveFallsBackToDownload
  participant saveProjectFile

  User->>ProjectFileDialogs: clicks "Save As"
  ProjectFileDialogs->>useProjectFileActions: saveProject()
  useProjectFileActions->>browserSaveFallsBackToDownload: check runtime
  browserSaveFallsBackToDownload-->>useProjectFileActions: true (no showSaveFilePicker)
  useProjectFileActions->>useProjectFileActions: askSaveName() opens prompt
  ProjectFileDialogs->>User: displays filename input dialog
  User->>ProjectFileDialogs: enters name, clicks Save
  ProjectFileDialogs->>useProjectFileActions: submitSaveNamePrompt(name)
  useProjectFileActions->>useProjectFileActions: ensureProjectFileName(name)
  useProjectFileActions->>saveProjectFile: call with sanitized filename
  saveProjectFile-->>User: file downloaded with chosen name
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • opengeos/GeoLibre#372: Refactors the same useProjectFileActions and ProjectFileDialogs files to structure the project file dialog system, providing the foundation that this PR builds upon for the new "Save project as" naming flow.

Poem

🐇 Hop, hop! A dialog springs to life,
No more default names causing strife.
The browser checks: no picker found?
A prompt appears, filename-bound!
Type your name, then click to save —
The rabbit dug a well-named cave. 🗂️

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding a file name prompt for Save As in browsers without the native save picker API.
Linked Issues check ✅ Passed The PR implementation directly addresses issue #448 by introducing a Save As dialog that allows users to specify custom file names in Firefox and Safari, which lack File System Access API support.
Out of Scope Changes check ✅ Passed All changes are directly related to the objective of implementing the Save As prompt functionality for browsers without native file picker support.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/save-as-prompt-browser

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread apps/geolibre-desktop/src/hooks/useProjectFileActions.ts
Comment thread apps/geolibre-desktop/src/hooks/useProjectFileActions.ts
@github-actions

Copy link
Copy Markdown
Contributor

Code review

Reviewed the diff for the "Save As" name-prompt feature (Firefox/Safari fallback). The overall approach is sound: the new browserSaveFallsBackToDownload() guard correctly identifies the affected environments, the dialog flow mirrors the existing envStripPrompt pattern, and the save-path logic handles all four cases (first save / save-as × Tauri+Chromium / Firefox+Safari) correctly. i18n keys are in place and the build passes.

Bugs

# Finding Confidence
B1 Concurrent Save As calls strand the first promise. askSaveName overwrites saveNamePrompt state unconditionally. If two saveProject invocations reach askSaveName simultaneously (e.g. keyboard shortcut fires while the dialog is still being mounted), resolve from the first call is silently dropped and that async chain hangs indefinitely. Same exposure exists for envStripPrompt. An isSavingRef guard or early-return check in saveProject would close this. Medium

Quality

# Finding Confidence
Q1 Silent blank-input fallback. Submitting the name dialog with only whitespace triggers the !trimmed branch in ensureProjectFileName, downloading a file called "Untitled Project.geolibre.json" with no explanation. The field should either block submission or visually indicate the fallback before the user submits. Low
Q2 Plain .json accepted as a recognized extension. ensureProjectFileName allows data.json to pass through unchanged. The file re-opens in GeoLibre (it accepts .json), but it is not identifiable as a GeoLibre project at a glance. Restricting the no-append case to .geolibre and .geolibre.json would match the stated intent. Low
Q3 saveNameInput not cleared on dialog close. Both cancelSaveNamePrompt and submitSaveNamePrompt leave saveNameInput holding the previous value. Currently harmless because React 18 batches the two setState calls in askSaveName, but the implicit assumption is fragile. Resetting to "" on close makes the invariant explicit. Low
Q4 Stale component docstring. ProjectFileDialogs JSDoc (line 18) still lists only the three older dialogs; the new save-name dialog should be mentioned. Trivial

Security

Nothing found. browserSafeFileName (called inside saveProjectFileBrowser) strips path separators before the value reaches the download anchor, so user-typed path traversal sequences like ../../../evil are reduced to their leaf name.

Performance

Nothing found.

CLAUDE.md

No violations. i18n strings are in en.json under the toolbar.item namespace, the dialog follows the existing shadcn UI conventions, and no external hosts requiring CSP changes are introduced.

- Serialize saves with an in-flight ref guard so a second save started while a
  prompt dialog is open cannot clobber the pending prompt and strand the first
  call's unresolved promise.
- Disable the Save button in the name dialog when the input is blank, so a
  whitespace-only name can no longer silently fall back to the default.
- Reset saveNameInput to "" when the name dialog closes, making the reset
  explicit rather than relying on the next open to overwrite it.
- Mention the save-name dialog in the ProjectFileDialogs docstring.
@giswqs giswqs merged commit d949203 into main Jun 18, 2026
22 of 23 checks passed
@giswqs giswqs deleted the fix/save-as-prompt-browser branch June 18, 2026 01:35
function ensureProjectFileName(name: string): string {
const trimmed = name.trim();
if (!trimmed) return `${DEFAULT_PROJECT_NAME}.geolibre.json`;
return /\.(geolibre\.json|geolibre|json)$/i.test(trimmed)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The regex accepts a bare .json extension as sufficient, so a user who types report.json gets a file downloaded as report.json — valid JSON that re-imports fine, but the OS and the file-open dialog won't recognise it as a GeoLibre project (the picker only surfaces .geolibre / .geolibre.json by name). The placeholder already guides users toward the double-extension; tightening the regex to match would make the sanitisation enforce the same intent:

Suggested change
return /\.(geolibre\.json|geolibre|json)$/i.test(trimmed)
return /\.(geolibre\.json|geolibre)$/i.test(trimmed)
? trimmed
: `${trimmed}.geolibre.json`;

If .json should intentionally stay accepted (e.g. to round-trip files the open-dialog also accepts as .json), a brief note in the JSDoc would document that deliberately.

* @param name - The raw file name the user typed.
* @returns A sanitized file name ending in a project extension.
*/
function ensureProjectFileName(name: string): string {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor nits grouped:

No unit tests for ensureProjectFileName. The function has four distinct branches (blank input, .geolibre.json, .geolibre, no recognised extension). Other small utilities in this repo (e.g. the string-list helpers) are covered by tests/*.test.ts; a handful of assertions here would be low-cost and prevent accidental regex regressions.

setSaveNameInput on the public return surface. It is only consumed by ProjectFileDialogs for its controlled <Input>, which is fine. But exposing the raw setter means external callers can mutate saveNameInput while a saveNamePrompt promise is pending — the resolved value would silently diverge from what the user typed. Consider keeping the setter internal and wiring the onChange callback through a dedicated onSaveNameChange prop, matching how the URL-open input is handled.

@github-actions

Copy link
Copy Markdown
Contributor

Code review

Reviewed useProjectFileActions.ts, ProjectFileDialogs.tsx, tauri-io.ts, and en.json. The three-way branching across Tauri / Chromium (showSaveFilePicker) / Firefox+Safari is correct; the serialised-save guard (isSavingRef) properly covers both the pre-existing env-strip prompt and the new save-name prompt; dialog open/close lifecycle (double-resolve via optional-chaining + idempotent Promise resolution) follows the same safe pattern already used for envStripPrompt.

Bugs

  • None confirmed.

Quality (medium confidence)

# Finding Location
1 ensureProjectFileName passes bare .json — a user who types report.json gets a file that re-imports fine but won't be recognised as a GeoLibre project by the OS or the file-open picker (which filters on .geolibre/.geolibre.json). The placeholder already steers users away; tightening the regex would enforce the same intent. Suggestion included in inline comment. useProjectFileActions.ts line 51
2 No unit tests for ensureProjectFileName — four distinct branches (blank, .geolibre.json, .geolibre, no extension), none exercised. The test suite covers comparable utility functions; a handful of assertions would be cheap insurance. useProjectFileActions.ts line 48
3 setSaveNameInput on the public return object — the raw state setter is exposed, so callers could mutate the input while a saveNamePrompt promise is pending. Only ProjectFileDialogs uses it today (controlled <Input>), so impact is low, but a narrower onSaveNameChange wrapper would match how the URL-open input is handled and close the footgun. useProjectFileActions.ts line 391

Security

Nothing found. browserSafeFileName in tauri-io.ts strips path separators before link.download, neutralising any traversal in user-entered filenames; browser download attributes can't traverse paths regardless.

Performance

Nothing found. browserSaveFallsBackToDownload() is a cheap synchronous feature-detect called once per save operation.

CLAUDE.md

All new user-facing strings use t() and are added to en.json; i18next.d.ts types keys via typeof en so no manual type update is needed. ✅

giswqs added a commit that referenced this pull request Jun 18, 2026
#458 made Save prompt for a file name (via a dialog) in browsers without the
File System Access picker — which these specs delete in addInitScript. The
save-and-reopen specs clicked Project → Save and waited for a download that
never fired, because the name-prompt dialog now intercepts the save, so both
tests hit the 60s timeout (failing on main too). Accept the pre-filled default
name and confirm the dialog so the download proceeds.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Issue: Project - Save as function does not work as expected

1 participant