# Save, discard, undo — three ways to show a page's unit of work

> Every edit on a page joins the route's unit of work; how the page OFFERS to keep it is a choice of presentation. `SaveBar` rises at the foot while there are changes, `SaveActions` puts Save and Discard inline beside the record's name, and `AutoSave` keeps the changes by itself a moment later with an Undo while they wait. All three drive the same unit of work, so switching is one line.

<!-- id: ui-saving · area: ui · stability: stable · html: https://osysharp.com/reference/ui/saving/ -->

## Summary        {#summary}

A page that edits rows already has a unit of work — the route's — and every field bound to a row joins it. What the
page still has to decide is how to OFFER to keep the changes. The UI kit ships three answers, each a control you drop
in, each reading and driving that same unit of work:

| control | what the person sees | reach for it when |
|---|---|---|
| `SaveBar` | a bar rises at the foot while there are changes: **Undo · Discard · Save** | a page of many edits a person reviews before keeping — settings, terms, a long form |
| `SaveActions` | **● Edited · Undo · Discard · Save** inline, beside the record's name or in a drawer's head | a record edited in place, a side panel, a dense console |
| `AutoSave` | **Saving in 2 s · Undo**, then **All changes saved** — there is no Save | a note, a CRM record, anything edited in passing |

```osy title="the same edit, kept three ways" test app=ui-saving
entity Contact {
  [Required, MaxLength(120)] string Name;
  [MaxLength(400)] string? Note;
  security { allow read, create, update when IsAnonymous || IsAuthenticated; }
}

// Inline: Save and Discard beside the name, only while there is something to keep.
[Route("/contacts/{id}")] [Render(CSR)] [AllowAnonymous]
component ContactPage(Guid id) {
  live var contact = Contact.FirstOrDefault(c => c.Id == id);
  render {
    if (contact != null) {
      Stack(gap: 4, p: 6) {
        PageHead(contact.Name) { SaveActions(saved: "Contact saved."); }
        TextAreaField("Note", value: contact.Note, rows: 4);
      }
    }
  }
}

// Kept by itself: no Save at all, and an Undo while the change waits.
[Route("/contacts/{id}/jot")] [Render(CSR)] [AllowAnonymous]
component ContactJot(Guid id) {
  live var contact = Contact.FirstOrDefault(c => c.Id == id);
  render {
    if (contact != null) {
      Stack(gap: 4, p: 6) {
        PageHead(contact.Name) { AutoSave(after: 2); }
        TextAreaField("Note", value: contact.Note, rows: 4);
      }
    }
  }
}
```

## Signature      {#signature}

```osy syntax
SaveBar(Action onSave = null, string saved = "Saved.", string blockedBy = "", Action onSaved = null)
SaveActions(string saved = "Saved", string blockedBy = "", Action onSaved = null)
AutoSave(int after = 2, string saved = "All changes saved")
```

## Description    {#description}

### What do all three have in common?   {#common}

They read `Navigation.IsDirty` to know whether there is anything to keep, and they act through `UnitOfWork`:
`Commit()` to keep, `Discard()` to drop every change, `Undo()` to step back one step. The platform takes a step for
every action the person runs and every field they type into, named by it ("Undo artwork"); a
`UnitOfWork.Mark("Rename the status")` names a step itself and groups the ones after it ([[ui-page-unit-of-work#undo]]).
Nothing about how the page EDITS changes between them — the fields bind rows exactly as before — so moving a page
from one to another is the one line that names the control.

A save the model's own rules would refuse is HELD, not sent: the edits stay, every field in the way is shown, and the
control says how many ("1 field needs attention."). A save the SERVER refuses is never silent either — its reason is
shown where the control is ("Not saved — …") — but what that commit carried is gone, so the page shows the stored row
again ([[ui-page-unit-of-work#held-save]]).

And the edits are not lost on the way out: while the page is dirty, leaving it asks first — Keep editing, Save and
leave, or Discard and leave — in the kit shells' own look, and from the platform for any other way out
([LeavePrompt — asking before a page with unsaved edits is left](https://osysharp.com/reference/ui/leave-prompt/), [[ui-page-unit-of-work#leaving]]).

### Which one fits my page?   {#choose}

Ask what a person expects to happen when they look away.

- **They expect nothing to be kept until they say so** — finance, settings, anything with consequences: `SaveBar`.
  The bar appears only when there is something to keep, says so in words, and carries Mod+S and Mod+Z.
- **They are working in one record and want to keep it right there** — a drawer, an inspector, a record's head:
  `SaveActions`. It takes no room at all while there is nothing to keep, then shows the same three verbs inline.
- **They expect it to be kept, as a document editor keeps a document** — `AutoSave`. Once the person PAUSES for
  `after` seconds (two by default) the change is kept; every further edit starts the count again
  (`UnitOfWork.EditCount()`), so nothing is kept mid-word. Until then it says so and can be taken back.

### What does Undo mean once something saves itself?   {#auto-undo}

`AutoSave`'s Undo is honest about time: while the change is waiting it steps back to the last savepoint (or, with
none, drops the changes not yet kept). Once the change is kept it is kept — the window to change your mind is the
one before. That is why the waiting state is visible and counts down rather than happening silently.

### How do I hold a save back until the page is complete?   {#blocked}

`SaveBar` and `SaveActions` take `blockedBy:` — the sentence saying why the page cannot be kept as it stands. While
it is not empty the control says it and Save does nothing (Mod+S included). Compute it from the page's own values so
it changes as the person types.

### How do I drive them in a test?   {#test}

```osy title="each kind, asserted on the row it keeps" test app=ui-saving-test
entity Note {
  [Required, MaxLength(60)] string Title;
  security { allow read, create, update when IsAnonymous || IsAuthenticated; }
}

[Route("/notes/{id}")] [Render(CSR)] [AllowAnonymous]
component NotePage(Guid id) {
  live var note = Note.FirstOrDefault(n => n.Id == id);
  render {
    if (note != null) {
      Stack(gap: 3, p: 6) {
        PageHead("Note") { SaveActions(saved: "Kept."); }
        Field("Title", value: note.Title);
      }
    }
  }
}

[Test]
void an_inline_save_keeps_the_edit() {
  var n = new Note { Title = "Draft" };
  UnitOfWork.Commit();
  Ui.Visit("/notes/" + n.Id);
  Ui.Fill("Title", "Final");
  Assert.Visible("Edited");
  Ui.Click("Save");
  Assert.Visible("Kept.");
  Assert.Equal("Final", Note.Single().Title);
}
```

For `AutoSave`, make the edit, `Assert.Visible("Saving in")`, then `Ui.Tick()` once per second of `after` — the
control's clock runs on `on every`, which a test fires on demand rather than waiting for it.

## See also       {#see-also}

- [A page's unit of work — what the platform does for an editing page, end to end](https://osysharp.com/reference/ui/page-unit-of-work/) — the whole journey: automatic undo, a held save, leaving, an act that waits
- [LeavePrompt — asking before a page with unsaved edits is left](https://osysharp.com/reference/ui/leave-prompt/) — what happens when somebody leaves the page before keeping its edits
- [SettingsArea — an app's settings: sections, explicit save and discard, undo, who changed what](https://osysharp.com/reference/ui/settings/) — the settings area, whose pages use `SaveBar`
- [Dock](https://osysharp.com/reference/ui/dock/) — a docked drawer, where `SaveActions` belongs
- [Arrangements — five ways to build a whole app, and which look wears each](https://osysharp.com/reference/ui/arrangements/) — which save presentation each archetype chose, and why
- [creating & saving data](https://osysharp.com/reference/ui/data-mutation/) — how an edit joins the unit of work in the first place
