Skip to content

No unsaved-change protection when closing a dirty tab #466

Description

@BorisTyshkevich

Surfaced while implementing #457.

The gap

closeTab (src/ui/tabs.ts:132) drops a tab unconditionally — it refuses only to
close the last one. There is no confirmation for a dirty tab, and no
beforeunload handler anywhere in src/. The only "protect unsaved work" UX in the
app is the linked-saved-query conflict chooser (src/ui/conflict-resolution.ts),
which fires on Save, not on close.

#457's acceptance criteria say a dirty variable tab "must use the same
unsaved-change protection as other editable tabs". Taken literally that is parity,
and parity is what shipped: a variable tab shows the same dirty dot and closes the
same way. But there is nothing to inherit.

Why it matters more for a variable tab

The asymmetry is real, not cosmetic:

  • a dirty query tab's draft was never expected to persist anywhere — closing it
    discards something that only ever lived in that tab;
  • a dirty variable tab's draft is an edit to SQL that IS stored
    (dashboard.variableConfigs[<name>].sql). Closing it silently discards the user's
    only copy of a change to durable configuration, with no prompt and no undo.

The same argument applies to a dirty query tab linked to a saved query
(savedId !== null), which is the other case where the draft diverges from
something persisted.

Goal

Decide and implement one close-time protection for editable tabs, covering both
document kinds rather than bolting one onto variables alone. Options worth weighing:

  • confirm on closing any tab whose draft diverges from committed truth (dirty +
    either savedId !== null or a dashboard-variable doc);
  • confirm only for documents backed by stored content, leaving a never-saved
    scratch tab closing freely (probably the least annoying);
  • a beforeunload guard as well, for whole-page navigation.

Why deferred

Designing a close-guard is new UX for BOTH tab kinds and was not specified by #457,
which is a refactor. Building it there would have meant inventing behaviour under a
criterion that reads as "match what exists".

Metadata

Metadata

Assignees

No one assigned

    Labels

    inboxFiled mid-task; not yet triaged into the roadmap

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions