# [Settings] and [Setting]: a settings section declared on its row, each field drawn by its type

> Mark the row staff configure `[Settings("Title", Authorize = Policy)]` and each field on it `[Setting("Label")]`, and the settings area draws the whole section with no page code: each field by its type (a switch, a dropdown, a number field bounded by its `[Range]`, a duration field, a time-zone picker, a text field), Save and Discard, who changed each group, and "Set by your organization" for an `[OrgPolicy(OrgPolicyMode.Lock)]` field the organisation sets. A page of your own stays the escape hatch for records and lists.

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

## Summary        {#summary}
Most of what staff configure is a single row of switches, choices, numbers, durations and short text. Declare it on
the row, and the settings area does the rest: it lists the section to whoever `Authorize` admits, serves its page,
draws each field by its type, saves only on Save, says who last changed each group, and locks a field the
organisation sets. The kit writes no page, no provider and no Save.

```osy syntax
[Settings("Notifications", Authorize = IsAdmin)]
entity NotificationSettings {
  [Setting("Tell the whole team")] bool TellEveryone = true;
  [Setting("Remind after")] TimeSpan RemindAfter = TimeSpan.FromHours(4);
}
```

## Signature      {#signature}
```osy syntax
[Settings(string title,                     // the section's name in the settings area, and the page's heading
          Authorize = <policy>,             // who may open it — the page's [Authorize] and the link's Shown (required)
          Route = "/settings/…",            // default: "/settings/" + the title, lower-cased and dashed
          Group = "…", Order = 100, Icon = Icons.Gear,   // where the area lists it (as a SettingsLink)
          Hint = "…",                       // the line under it in the area's index
          Description = "…",                // the line under the page's heading
          Saved = "… saved.",               // what the Save bar says once a save lands
          Owner = Person)]                  // a PERSONAL section — the field saying whose row it is; each reader
                                            // edits their own, made for them the first time (see below)
[SettingsGroup(string title, string description)]   // a group of fields, in the order the entity declares them
entity Row {
  [Setting(string label, Group = "…", Hint = "…", Order = 0)] T Field = default;
}

// What the section is made of — public, to use elsewhere or to take over by name:
[Composable] component RowFields(Row row);  // the groups alone (a desk places them); saved by whatever holds the row
[Composable] component RowEditor();         // the page's body — the row, its groups and the Save bar
[Route(Route)] component RowPage();          // the page, in [Layout(SettingsFrame)], gated by Authorize

public bool SetByOrganization<T>(x => x.Field);   // is this [OrgPolicy] field set by the reader's organisation?
```

## Description    {#description}

### Which control does each field get?   {#types}
A field's TYPE chooses its row, the way a block's parameter types choose its editor:

| the field | its row |
|---|---|
| `bool` | a switch, with the hint under it |
| an enum | a dropdown of the enum's members, by their `[Label]` |
| `int` | a number field — `[Range(min, max)]` (or `[Min]`/`[Max]`) bounds it and its steppers |
| `decimal` | a decimal number field, bounded the same way |
| `TimeSpan` | a duration field: it reads "4 hours", and opens to a number and a unit (minutes, hours, days) |
| `TimeZoneInfo` | a time-zone picker (`TimeZonePicker`) over the app's declared zones, each read as its place and region — "Stockholm — Europe". A name that is not a time zone cannot be saved, because it cannot be typed |
| `string` | a text field; past 200 characters (`[MaxLength]`) a text area |

Anything else — a date, a reference to another row, a list — is refused by name: a row cannot hold it, and a page of
your own (`SettingsPage`, `SettingsSection`) is the escape hatch. A nullable field is refused too: a setting always
has a value, so give it a default.

Short fields (numbers, durations, choices, codes up to 40 characters) that follow each other sit side by side in a
grid (`SettingsGrid`); a switch takes its own line. Every row is a `SettingRow` inside a `SettingsSection`, so a
declared section reads exactly like a page built by hand.

### How are the fields grouped?   {#groups}
Each `[SettingsGroup("Sales", "…")]` on the entity is a section of the page — its title and its line on the left,
its fields on the right — in the order the entity declares them. A field joins one with `Group = "Sales"`; a field
naming no group is in a section named after the page. Within a group, fields go by `Order` and then as declared.

### When is it saved, and what holds a Save back?   {#save}
Nothing is saved until Save, from the page's `SaveBar`, which offers Discard and Undo as on every settings page. While
a value cannot be saved the bar says why and Save does nothing: a required text left empty, a text past its
`[MaxLength]`, a number outside its `[Range]`. A rule only the server holds — an `invariant` — refuses the Save
there, and the bar says "Not saved — " and the rule's message.

The row is made with every field's default the first time the page is opened, so every stored field of a settings row
needs a default (or `?`): the compiler refuses one that has neither, naming it.

### Who changed it?   {#changed}
Under each group: "Changed by Ada Lindqvist · 3 min ago" — the latest change a person made to that group's fields.
It comes from the app's change trail, so it appears when the APP keeps one (`use Osysharp.Observability;`); without a
trail there is nobody to name, and the line is left out rather than guessed. The row being made with its defaults is
not a change.

### What does the organisation set?   {#org-policy}
A field marked `[OrgPolicy(OrgPolicyMode.Lock)]` is asked about on its own: when the reader's organisation sets it, its
control is disabled and the row says "Set by your organization" — or, when the organisation sets every field of a
group, the group says it once. See [[ui-settings-registry#org-policy]] for the modes and the seam.

In code of your own, ask by the field — `SetByOrganization<ReviewSettings>(s => s.PublishAtOnce)`. The field must be
marked `[OrgPolicy]`, and a renamed field is a compile error. The string form is refused: a key written as text goes
on compiling after the field is renamed, and quietly stops being the organisation's.

### A section each person keeps for themselves   {#personal}
Most settings are the app's — one row, changed by whoever `Authorize` admits. A person's own preferences (what is mailed
to them, how dense their list is) are one row EACH: name the field saying whose row it is with `Owner = Person`, and the
section finds the reader's own row — by whose it is, never by what the rules let them read, so an admin who may read
everybody's still opens their own — and makes it, with every default and the reader as its owner, the first time they
open the page. The field is a stored, non-nullable reference to the app's principal (`[Required] Principal Person;`),
needs no default, and is never a `[Setting]`; the entity's rules say who may read and change a row
(`allow read, create, update when … where Person == user`).

```osy syntax
[Settings("Notifications", Authorize = IsMember, Owner = Person, Route = "/settings/notifications")]
entity NotificationPreferences {
  [Required] Principal Person;
  [Setting("Mail me")] NotificationMail Mail = NotificationMail.MentionsAtOnce;
  security { allow read, create, update when IsMember where Person == user; }
}
```

### What do I write instead of a page?   {#escape-hatch}
Nothing, for a row of settings. For records and lists — saved replies, queues with members, mailboxes to connect —
write the page yourself under `[Layout(SettingsFrame)]` and announce it with an `ISettingsProvider`
([ISettingsProvider and [OrgPolicy]: kits announce their settings, and a setting says whether an organisation may govern it](https://osysharp.com/reference/ui/settings-registry/)). To change one declared section's page, declare a component of the same name (`RowPage`,
`RowEditor`) in the app; to place the fields somewhere else, call `RowFields(row)` and save from there.

## Examples       {#examples}

### A section with no page code   {#example-declared}
```osy title="the row, its groups and its fields — the settings area draws the rest" test app=ui-settings-declared
[Principal] entity Member { [MaxLength(100)] string Name; }
policy IsMember => IsAuthenticated;

enum DigestDay { [Label("Monday")] Monday, [Label("Friday")] Friday }

[Settings("Notifications", Authorize = IsMember, Group = "Workspace", Order = 2, Icon = Icons.Bell,
          Hint = "When people hear about new work.", Saved = "Notifications saved.")]
[SettingsGroup("New work", "Who hears about something new.")]
[SettingsGroup("Digest", "The weekly summary.")]
entity NotificationSettings {
  [Setting("Tell the whole team", Group = "New work", Hint = "Off, only the person it is assigned to hears.")]
  bool TellEveryone = true;
  [Setting("Remind after", Group = "New work")]
  TimeSpan RemindAfter = TimeSpan.FromHours(4);
  [Setting("Send on", Group = "Digest")]
  DigestDay DigestOn = DigestDay.Monday;
  [Setting("Items in a digest", Group = "Digest"), Range(5, 50)]
  int DigestSize = 20;
  [Setting("Lock after", Group = "Digest")] [OrgPolicy(OrgPolicyMode.Lock)]
  bool LockAfterSending = false;
  [Setting("Time zone", Group = "Digest", Hint = "The digest goes out at 07:00 here.")]
  TimeZoneInfo SendIn = TimeZoneInfo.FindSystemTimeZoneById("Europe/London");
  security { allow read, create, update when IsMember; }
}

string Announced() {
  return string.Join(", ", AnnouncedSettings().Select(l => l.Label + " " + l.To));
}
```

```osy title="announced with no provider written, and the organisation asked by the field" run app=ui-settings-declared
[TestFixture]
void Team() { new Member { Name = "Ada" }; }

principal Ada => Member.Single(m => m.Name == "Ada");

[Test(Team)]
[runas(Ada)]
void The_section_is_announced_and_nothing_is_set_by_an_organisation() {
  Assert.Equal("Notifications /settings/notifications", Announced());
  Assert.False(SetByOrganization<NotificationSettings>(s => s.LockAfterSending));
}
```

## See also       {#see-also}
- [ISettingsProvider and [OrgPolicy]: kits announce their settings, and a setting says whether an organisation may govern it](https://osysharp.com/reference/ui/settings-registry/): `ISettingsProvider` for a page of your own, and `[OrgPolicy]`'s modes
- [SettingsArea — an app's settings: sections, explicit save and discard, undo, who changed what](https://osysharp.com/reference/ui/settings/): `SettingsPage`, `SettingsSection`, `SaveBar`, the pieces a declared section is drawn with
- [package.osy](https://osysharp.com/reference/project/package/): a kit's settings join the app's settings area — the rule, and the warning that keeps it
- [Changes](https://osysharp.com/reference/entity/changes/): `<Entity>.Changes(id)`, where "Changed by" comes from
- [settings a package asks its app for (app settings)](https://osysharp.com/reference/config/app-settings/): `[AppSetting]`, code configuration the app sets in `app.osy`, not on a settings page
