# SettingsArea — an app's settings: sections, explicit save and discard, undo, who changed what

> The kit's settings vocabulary: `SettingsArea` lays out an app's settings — its sections in a grouped nav, the current one lit, each section's page beside it (on a phone, the nav first and each section a screen of its own); `SettingsPage` and `SettingsSection` give every page the same shape; `SaveBar` saves or discards a page's edits as one explicit act, with Undo back to the last savepoint; and `ChangedBy` says who last changed something, and when.

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

## Summary        {#summary}
Settings are pages people change rarely and must be able to trust: where each thing lives, that nothing is stored
until they say so, that a slip can be taken back, and who changed it last. The kit gives each of those a component, so
every settings page of an app reads the same.

```osy syntax
SettingsArea(SettingsSections(), title: "Settings", home: "/settings") { Outlet; }
```

## Signature      {#signature}
```osy syntax
public sealed class SettingsLink {
  string Label; string To; string Group = ""; Icons Icon = Icons.Gear; string Hint = ""; bool Shown = true;
  int Order = 100;
}
[Composable] component SettingsArea(SettingsLink[]? sections = null, string title = "Settings", string home = "/settings");
[Composable] component SettingsPage(string title, string description = "");
[Composable] component SettingsSection(string title, string description = "", bool locked = false,
                                       string lockedBecause = "", string changed = "", bool setByOrganization = false);
[Composable] component SaveBar(Action onSave = null, string saved = "Saved.", string blockedBy = "", Action onSaved = null);
[Composable] component SettingRow(string hint = "", bool setByOrganization = false);   // one setting: its control, its line
[Composable] component SettingsGrid();                       // short settings side by side
string ChangedBy(string by, DateTime? at, DateTime now);     // "Changed by Ada Lindqvist · 3 min ago"
string TimeAgo(DateTime at, DateTime now);                   // "just now", "4 min ago", "yesterday", "12 Mar"
```

## Description    {#description}

### The area and its sections                 {#area}
Put `SettingsArea` in a `[Layout]` and give it the sections, each a `SettingsLink` to the page that holds it. Links
with the same `Group` are listed together under it; the one being looked at is lit. `Shown` takes a policy as a value
(`Shown = IsAdmin`), so a person sees only the sections they may open — and each page still carries its own
`[Authorize]`, because hiding a link is not refusing a page. On a phone, `home` is the list of sections with each
one's `Hint`, and every section is a screen of its own with a way back. In a test the nav is a named region,
`"<title> sections"`: `Ui.Click("Delivery", within: "Settings sections")`.

With **no list**, `SettingsArea()` gathers the sections every kit and the app itself announce (`ISettingsProvider`),
grouped and ordered, and shows "Nothing to set up" when there are none. That is the usual form once kits ship their
own settings pages. See [ISettingsProvider and [OrgPolicy]: kits announce their settings, and a setting says whether an organisation may govern it](https://osysharp.com/reference/ui/settings-registry/).

### A page and its sections                   {#page}
`SettingsPage(title, description)` is a page's heading and a line saying what it is for, then its sections in a
readable column. `SettingsSection` puts what a group of settings is on the left and its controls on the right, stacked
on a phone; it is a named region, so a test says `within: "Zones"`.

- **`locked`** shows the controls and says they are not this reader's to change (`lockedBecause`). Give each control
  `disabled:` from the same policy — the rows are refused by their own `security { }` rules either way.
- **`changed`** is the "who changed this, and when" line — `ChangedBy(…)`, fed from the row's history: with
  `using Osysharp.Observability;`, `<Entity>.Changes(id)` answers newest first, so its first entry is the last change.

### One setting, the way every section draws it   {#row}
`SettingRow(hint:, setByOrganization:)` holds one control (its slot), the line under it saying what it does, and
"Set by your organization" when the reader's organisation sets that one setting while the rest of its section is the
app's. `SettingsGrid { … }` puts short settings — numbers, durations, choices, codes — side by side, one column on a
phone. A `[Settings]` section is drawn from exactly these ([[Settings] and [Setting]: a settings section declared on its row, each field drawn by its type](https://osysharp.com/reference/ui/settings-declared/)), so a page built by hand with them
reads the same.

### Nothing is stored until Save              {#save}
The page edits rows directly — `Field("Name", value: z.Name)`, `new DeliveryZone { … }`, `row.Delete()` — and none of it
is stored: it lives in the page's unit of work. `SaveBar` appears only while there is something unsaved
(`Navigation.IsDirty`), says so, and offers three things:

- **Save** (Mod+S) commits every change at once. **`blockedBy` holds it back:** the sentence saying why the page
  cannot be saved as it stands. While it is not empty the bar says it and Save does nothing, so compute it from the
  page's own values and it follows the person as they type (`blockedBy: Refusal()`). `onSave` runs as Save is pressed,
  before the commit — for a derived value. ⚠ It is fired, not awaited (a component's callback never is), so it
  **cannot** stop the save: a sentence it throws is lost. A check belongs in `blockedBy`.
- **A save the server refuses says why.** A refused commit gives the page's edits back, so nothing is left unsaved —
  and the bar says "Not saved — " and the reason in its place, rather than closing with the reason inside it. `onSaved`
  runs once a save has landed, to re-read what the page shows about it (who changed it).
- **Discard** drops every unsaved change.
- **Undo** (Mod+Z) goes back to the last savepoint. Make one before each edit with `UnitOfWork.Mark("Add a zone")`; its
  label is the button's ("Undo add a zone").

Leaving the page through the app shell's rail with changes still unsaved asks first — Keep editing, Save and leave,
or Discard and leave ([LeavePrompt — asking before a page with unsaved edits is left](https://osysharp.com/reference/ui/leave-prompt/)); Save and leave keeps them exactly as the bar's Save would.

Once a save lands, the bar says `saved` in its place. In a test: `Ui.Click("Save")`, `Ui.Click("Discard")`, and
`Assert.Visible("Unsaved changes")` while there are some.

## Examples       {#examples}

### A bakery's delivery settings              {#example-delivery}
```osy title="a settings area with two sections, a page that edits rows behind a SaveBar, and undo per edit" test app=ui-settings
entity DeliveryZone {
  [Required("Name the zone."), MaxLength(80)] string Name;
  decimal Fee = 0;
  bool Active = true;
  security { allow read, create, update when IsAuthenticated; }
}

[Layout]
component SettingsLayout() {
  render {
    SettingsArea([
      new SettingsLink { Label = "Delivery", To = "/settings/delivery", Group = "Orders", Icon = Icons.MapPin,
                         Hint = "Where you deliver, and what it costs." },
      new SettingsLink { Label = "Opening hours", To = "/settings/hours", Group = "Shop", Icon = Icons.Clock,
                         Hint = "When the counter is open." },
    ], title: "Settings", home: "/settings") { Outlet; }
  }
}

[Route("/settings")] [Layout(SettingsLayout)] [Title("Settings")]
component SettingsHome() {
  render { SettingsPage("Settings", description: "How the bakery takes orders."); }
}

[Route("/settings/delivery")] [Layout(SettingsLayout)] [Title("Delivery")]
component DeliverySettings() {
  live var zones = DeliveryZone.OrderBy(z => z.Name).ToList();

  action AddZone() {
    UnitOfWork.Mark("Add a zone");
    new DeliveryZone { Name = "New zone" };
  }

  render {
    SettingsPage("Delivery", description: "Where the van goes, and what a customer pays for it.") {
      SettingsSection("Zones", description: "A zone that is off is kept, and offered to nobody.") {
        foreach (var z in zones) {
          Row(gap: 3, align: Align.End, label: z.Name) {
            Stack(grow: 1) { Field("Name", value: z.Name); }
            Field("Fee", value: z.Fee);
            Switch("Delivering", value: z.Active);
          }
        }
        Row { Button("Add a zone", onPress: AddZone, tone: Tone.Ghost, size: Size.Sm); }
      }
      // Held back, with the reason on the bar, while a zone has no name.
      SaveBar(saved: "Delivery saved.", blockedBy: zones.Any(z => z.Name.Trim() == "") ? "Name every zone." : "");
    }
  }
}

[Route("/")] [Title("Bakery")]
component Home() { render { TextLink("Settings", to: "/settings"); } }
```

`Apps/harbour`'s Settings (statuses, SLA policies, business hours, tiers, routing, fields and forms, saved replies,
shared views, people) is the whole vocabulary in a working app.

## See also       {#see-also}
- [[Settings] and [Setting]: a settings section declared on its row, each field drawn by its type](https://osysharp.com/reference/ui/settings-declared/): a row of settings declared with `[Settings]` and `[Setting]` — drawn with no page code
- [ISettingsProvider and [OrgPolicy]: kits announce their settings, and a setting says whether an organisation may govern it](https://osysharp.com/reference/ui/settings-registry/): kits announce their sections, `SettingsArea()` gathers them, and `[OrgPolicy]` marks what an organisation may govern
- [Save, discard, undo — three ways to show a page's unit of work](https://osysharp.com/reference/ui/saving/) — `SaveBar` beside `SaveActions` and `AutoSave`, the other two ways to offer a save
- [LeavePrompt — asking before a page with unsaved edits is left](https://osysharp.com/reference/ui/leave-prompt/) — the question asked when a page with unsaved changes is left
- [creating & saving data](https://osysharp.com/reference/ui/data-mutation/) — how a page's edits live in its unit of work until a commit
- [App shells](https://osysharp.com/reference/ui/shell/) — the app shell a settings area sits inside
- [routes and pages](https://osysharp.com/reference/ui/routing/) — layouts, `Outlet` and `[Route]`
- [The UserManagement kit — the user manager: who is in the app, what each may do, and who changed it](https://osysharp.com/reference/security/people-kit/) — the people page a settings area usually starts with
