# ISettingsProvider and [OrgPolicy]: kits announce their settings, and a setting says whether an organisation may govern it

> A kit with something an admin configures implements `ISettingsProvider` and serves its pages under `[Layout(SettingsFrame)]`. `SettingsArea()` with no list gathers every provider in the app, the app's own included, so adding a kit adds its settings and the app's whole configuration reads as one place. A setting can declare `[OrgPolicy(OrgPolicyMode.X)]` to say whether an organisation's policy may govern it. `osy model` lists every such setting, and `SettingsSection(…, setByOrganization:)` shows "Set by your organization".

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

## Summary        {#summary}
An app's settings come from many places: the people it manages, how long it keeps personal data, the tiers its
customers are on. Each of those is a kit's. With a settings registry, every kit **announces** its sections, and the
app's settings area gathers them all. The app writes no navigation, a kit added means its sections appear, and every
page sits in the app's own frame.

```osy syntax
component MyShellDesign() : WorkspaceShell { render { MyShell { Slot; } } }   // once — the settings area stands in it
```

## Signature      {#signature}
```osy syntax
public interface ISettingsProvider { SettingsLink[] Sections(); }
public List<SettingsLink> AnnouncedSettings();              // every provider's links this reader is Shown, ordered
[Composable] component SettingsArea(SettingsLink[]? sections = null, string title = "Settings", string home = "/settings");
[Layout] component SettingsFrame();                         // SettingsArea() in the app's WorkspaceShell
public int SettingsLink.Order = 100;                        // where it sits in a gathered area

[OrgPolicy(OrgPolicyMode.Default | OrgPolicyMode.Lock | OrgPolicyMode.Range)]   // on a settings field
public interface IOrgPolicySource { bool Sets(string setting); }               // the org layer implements it
public bool SetByOrganization<T>(x => x.Field);             // SetByOrganization<PrivacySetup>(p => p.ReviewPlans)
[Composable] component SettingsSection(…, bool setByOrganization = false);
```

## Description    {#description}

### A kit announces its sections               {#announce}
⭐ **A row of settings needs no page and no provider at all**: mark the row `[Settings("Title", Authorize = …)]` and its
fields `[Setting("Label")]`, and the section is announced and drawn for you — see [[Settings] and [Setting]: a settings section declared on its row, each field drawn by its type](https://osysharp.com/reference/ui/settings-declared/). What
follows is the escape hatch, for sections that hold records and lists.

A class that implements `ISettingsProvider` is **discovered**, never wired. `SettingsArea()` with no list asks
`App.Implementations<ISettingsProvider>()` (see [App.Implementations<T>()](https://osysharp.com/reference/class/app-implementations/)) for every one, the kits' and the
app's own, and shows the links this reader is `Shown`. `Shown` takes the provider's policy as a value
(`Shown = ManagesPeople`), and a class method may name the app's policies exactly as a function does.

They are **grouped and ordered**. A group sits where its lowest `Order` puts it, with ties broken by the group's name.
Within a group, links are ordered by `Order` and then by label. Kits leave gaps (People 10, Organizations 30,
Privacy 60) so an app's own sections can go between them. The app's own provider is how it **adds** a section.

To **reorder or hide** sections, pass a list: `SettingsArea(MyLinks())`. To start from the kits' links, call
`AnnouncedSettings()` from a server body (an `on mount`) and filter or reorder what it returns.

⭐ **Every kit with settings announces them, and the compiler holds it to that.** A package whose own source has a
configuration surface (a settings page, a settings row, an `[OrgPolicy]` field) and no `ISettingsProvider` gets the
warning `SETTINGS_NOT_ANNOUNCED` on its own compile. What counts, and the example a kit copies, are in
[package.osy](https://osysharp.com/reference/project/package/) under "A package with settings joins the app's settings area".

⚑ **Hiding a link is never the security.** Every settings page carries its own `[Authorize]`, and the functions
behind it carry theirs. A reader who types the address of a section they are not shown is refused by the page itself.

### A kit's page sits in the app's frame       {#frame}
A kit serves its settings pages at its own routes, `[Route("/settings/privacy")] [Layout(SettingsFrame)]`. The Ui kit's
`SettingsFrame` is the area inside the app's design of `WorkspaceShell` ([WorkspaceShell, WorkspaceFrame and [Nav] — a kit's pages in your app's shell and navigation](https://osysharp.com/reference/ui/workspace-frame/)) — the same shell every
kit's working page stands in — so an app that designs its shell once has every settings page in it. When the settings
want a frame of their own, **the app takes `SettingsFrame` over** by declaring a `[Layout] component SettingsFrame()`,
which is the rule an app's own `Card` follows over the kit's. A kit's page can be taken over by name in the same way
(see [routes and pages](https://osysharp.com/reference/ui/routing/)).

### A setting may be governed by an organisation   {#org-policy}
`[OrgPolicy(OrgPolicyMode.X)]` on a field of a kit's `[AppSetting]` class, or of a row an admin edits, declares that
an organisation's policy **may** govern it:

| mode | the organisation… | the app… |
|---|---|---|
| `Default` | seeds the value | may change it |
| `Lock` | sets it; the page says "Set by your organization" | may not change it |
| `Range` | bounds a number (a floor, never wider) | chooses within the bounds |

The compiler records the marker and refuses it without one of the three modes, or as `Range` on anything but a
number. `osy model --json` lists every such setting under `orgPolicy`, keyed `Namespace.Type.Field`, with its mode and
the package that declares it. The same mode appears on the field as `orgPolicy`.

**Nothing enforces it yet, and nothing stores an organisation's values.** The seam where an organisation's values will
arrive is `IOrgPolicySource`, which the org layer's package implements. `SetByOrganization<T>(x => x.Field)` asks
every source, and with none in the app it answers `false`. It names the setting by its MEMBER — the field must be
marked `[OrgPolicy]`, and renaming it is a compile error — and the compiler hands the sources the key `osy model`
lists. A key written as a string is refused: it went on compiling after the field was renamed, and quietly stopped
being the organisation's. A page passes the answer to
`SettingsSection(…, setByOrganization: …)` and disables its controls from the same value. A `Range` setting is never
"set by" an organisation, so its page keeps the app's controls.

## Examples       {#examples}

### Two sections, announced and gathered       {#example-gathered}
```osy title="an app's own providers, gathered into SettingsArea() inside the app's own SettingsFrame" test app=ui-settings-registry
[Principal] entity Member { [MaxLength(100)] string Name; }
policy IsMember => IsAuthenticated;

entity Workspace {
  [Required, MaxLength(120)] string Name;
  /// How long a deleted file is kept before it is gone for good.
  [OrgPolicy(OrgPolicyMode.Range)] int KeepDeletedDays = 30;
  security { allow read, update when IsMember; }
}

class WorkspaceSettings : ISettingsProvider {
  public SettingsLink[] Sections() {
    return [new SettingsLink { Label = "Workspace", To = "/settings/workspace", Group = "Workspace", Order = 1,
                               Shown = IsMember, Hint = "Its name, and how long deleted files are kept." }];
  }
}

class BillingSettings : ISettingsProvider {
  public SettingsLink[] Sections() {
    return [new SettingsLink { Label = "Billing", To = "/settings/billing", Group = "Account", Order = 80,
                               Shown = IsMember, Icon = Icons.File }];
  }
}

[Layout]
component SettingsFrame() {
  render {
    Stack(gap: 0, h: "100vh") {
      Row(px: 4, py: 3, label: "Top bar") { Text("Acme", fontWeight: FontWeight.Semibold); }
      SettingsArea() { Outlet; }
    }
  }
}

[Route("/settings/workspace")] [Layout(SettingsFrame)] [Authorize(IsMember)] [Title("Workspace")]
component WorkspacePage() {
  live var spaces = Workspace.ToList();
  render {
    SettingsPage("Workspace") {
      foreach (var w in spaces) {
        SettingsSection("Name") { Field("Workspace name", value: w.Name); }
      }
      SaveBar(saved: "Workspace saved.");
    }
  }
}

[Route("/settings/billing")] [Layout(SettingsFrame)] [Authorize(IsMember)] [Title("Billing")]
component BillingPage() { render { SettingsPage("Billing", description: "Your plan and your invoices."); } }

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

```osy title="every provider is found, in order, and no organisation sets anything yet" run app=ui-settings-registry
[TestFixture]
void Team() { new Member { Name = "Ada" }; }

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

[Test(Team)]
[runas(Ada)]
void Both_sections_are_announced_in_order() {
  Assert.Equal("Workspace, Billing", GatheredLabels());
  Assert.False(SetByOrganization<Workspace>(w => w.KeepDeletedDays));
}

[Test]
void A_reader_who_may_open_neither_is_shown_neither() {
  Assert.Equal("", GatheredLabels());   // anonymous: each provider's own policy says no
}
```

## 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/): `[Settings]` and `[Setting]` — a section declared on its row, with no page code
- [SettingsArea — an app's settings: sections, explicit save and discard, undo, who changed what](https://osysharp.com/reference/ui/settings/): `SettingsPage`, `SettingsSection`, `SaveBar` and `ChangedBy`, the shape every settings page shares
- [App.Implementations<T>()](https://osysharp.com/reference/class/app-implementations/): how a provider is discovered without being wired
- [routes and pages](https://osysharp.com/reference/ui/routing/): a package's pages, and taking one over by name
- [settings a package asks its app for (app settings)](https://osysharp.com/reference/config/app-settings/): `[AppSetting]`, which is developer configuration written in code, unlike the runtime data an admin edits here
