dropdown-demo
A project's LEAD, chosen from a dropdown — the sample that proves M121's `Slot(item)` renders, and that a dropdown bound to an ENTITY-typed property round-trips the selection back into the data.
4 source files2 test files


Get it
// not one of the apps the toolchain ships: every file // of it is below.
The app
app.osy8 lines
// A project's LEAD, chosen from a dropdown — the sample that proves M121's `Slot(item)` renders, and that a // dropdown bound to an ENTITY-typed property round-trips the selection back into the data. app DropdownDemo { use Osysharp.Ui; model "model/**/*.osy"; tests "tests/**/*.test.osy"; }
model/auth.osy70 lines
// Auth, on the `demo/auth-demo` shape — the smallest real login, because this demo needs WRITES and the deny-all // endpoint gate correctly refuses an anonymous one. That is not incidental to what is being demonstrated: choosing // a lead WRITES `Project.Lead`, so a version of this demo that opened with no login could not have proven the // binding at all. [Role] enum AppRole { Authenticator, Member } [RoleGrant] entity RoleGrant { User Grantee; AppRole Level; security { } } policy IsAuthenticator => RoleGrant.Any(g => g.Grantee == user && g.Level == AppRole.Authenticator); [Principal] entity User { [MaxLength(200), Unique] string Email; [MaxLength(200)] string PasswordHash; [MaxLength(80)] string Name; [MaxLength(120)] string Title; [MaxLength(4)] string Initials; security { deny read PasswordHash when !IsAuthenticator; allow read when IsAuthenticator; allow create when IsAuthenticator; allow update when IsAuthenticator; allow read when IsAuthenticated; } } [AuthMethod] string Signup(string email, string password) { if (password.Length < 8) { return ""; } // hashing is not a length check: "" and "a" hash perfectly well var u = new User { Email = email, PasswordHash = Security.HashPassword(password), Name = email, Title = "Founder", Initials = "ME", }; if (!User.Any(x => x.Email == "[email protected]")) { new User { Email = "[email protected]", Name = "Ada Lovelace", Title = "Principal engineer", Initials = "AL", PasswordHash = Security.HashPassword("demo-password") }; new User { Email = "[email protected]", Name = "Grace Hopper", Title = "Engineering manager", Initials = "GH", PasswordHash = Security.HashPassword("demo-password") }; new User { Email = "[email protected]", Name = "Alan Turing", Title = "Research lead", Initials = "AT", PasswordHash = Security.HashPassword("demo-password") }; new User { Email = "[email protected]", Name = "Katherine Johnson", Title = "Staff engineer", Initials = "KJ", PasswordHash = Security.HashPassword("demo-password") }; } return Security.IssueJwt(u.Id, u.Email); } [AuthMethod] string Login(string email, string password) { var u = User.Where(x => x.Email == email).FirstOrDefault(); if (u == null) { Security.VerifyPassword(password); return ""; } if (Security.VerifyPassword(password, u.PasswordHash)) { return Security.IssueJwt(u.Id, u.Email); } return ""; } // Audit redaction is EXPLICIT. The entity-change trail records every changed property's old AND new value verbatim // into a durable, exportable log, and the `deny read` on the credential does not cover it — that governs READERS, // and the audit writer is not one. Named here, the trail records THAT the field changed and never its value. app.Audit = new AuditConfig { Redact = new AuditRedaction { Properties = [User.PasswordHash] }, }; app.AuthBootstrap = new AuthBootstrap { Role = AppRole.Authenticator, Login = Login, Signup = Signup, LoginPage = LoginPage, };
model/home.osy132 lines
// THE POINT OF THIS DEMO, in one line: `Dropdown(value: p.Lead, options: people)` — a generic dropdown over // ENTITIES, whose caller decides what a row looks like, bound two-way to an entity-typed property. theme Studio { Colors { Bg = Modes.Of(light: "#F6F7F9", dark: "#0E1116"); OnBg = Modes.Of(light: "#16181D", dark: "#E6EAF0"); Border = Modes.Of(light: "#DDE1E6", dark: "#2A2F37"); BorderStrong = Modes.Of(light: "#C3C9D0", dark: "#3A414D"); Surface = Modes.Of(light: "#FFFFFF", dark: "#171B21"); OnSurface = Modes.Of(light: "#1A1F26", dark: "#E6EAF0"); Muted = Modes.Of(light: "#EFF1F5", dark: "#232833"); Subtle = Modes.Of(light: "#6B7280", dark: "#8A94A6"); Accent = "#4F46E5"; OnAccent = "#FFFFFF"; } Radius { Control = "8px"; Card = "14px"; Pill = "999px"; } FontSize { Caption = "13px"; Body = "15px"; Section = "17px"; Display = "28px"; } FontWeight { Medium = "600"; } Length { Avatar = "28px"; Field = "300px"; } } entity Project { [MaxLength(80)] string Name; User? Lead; security { allow read, create, update when IsAuthenticated; } } [Composable] component Avatar(string initials) { render { Box(w: Length.Avatar, h: Length.Avatar, rounded: Radius.Pill, bg: Colors.Accent, color: Colors.OnAccent) { Row(align: Align.Center, justify: Justify.Center, h: "100%") { Text(initials, fontSize: FontSize.Caption, fontWeight: FontWeight.Medium); } } } } [Title("Dropdowns")] [Route("/")] component Home() { // A team grows, so the lead is FOUND: the twenty whose name matches what was typed (all of them, while nothing is). string leadSearch = ""; live var people = User.Where(u => u.Name.Contains(leadSearch)).OrderBy(u => u.Name).Take(20); live var project = Project.FirstOrDefault(); on mount { SeedDemo(); } action Save() { UnitOfWork.Commit(); } // ⚑ WHAT THE PAGE LOOKS LIKE WHILE `project` AND ITS PEOPLE ARE STILL ON THE WAY. Without this it paints nothing // until the read answers, and a blank screen is indistinguishable from one that hung. It draws // the SHAPE, never the data — a skeleton cannot read the members it stands in for. skeleton { Stack(gap: 6, p: 8, minH: "100vh", bg: Colors.Bg) { Skeleton(shape: SkeletonShape.HalfLine); Skeleton(shape: SkeletonShape.Control); Skeleton(shape: SkeletonShape.Control); Skeleton(shape: SkeletonShape.Block); } } render { Stack(gap: 6, p: 8, minH: "100vh", bg: Colors.Bg, color: Colors.OnBg) { if (project == null) { Text("Seeding…", color: Colors.Subtle); } else { Stack(gap: 1) { Text(project.Name, fontSize: FontSize.Display, fontWeight: FontWeight.Medium); Row(gap: 2, align: Align.Center) { Text("Lead:", color: Colors.Subtle, fontSize: FontSize.Body); if (project.Lead == null) { Text("nobody yet", color: Colors.Subtle, fontSize: FontSize.Body); } else { Text(project.Lead.Name, fontSize: FontSize.Body, fontWeight: FontWeight.Medium); } } } Stack(gap: 2) { Text("Project lead", color: Colors.Subtle, fontSize: FontSize.Caption, fontWeight: FontWeight.Medium); SearchField("Find a lead", value: leadSearch); Dropdown(value: project.Lead, options: people, placeholder: "Choose a lead…", label: "Project lead") { d => Avatar(d.Initials); Stack(gap: 0) { Text(d.Name, fontSize: FontSize.Body); Text(d.Title, fontSize: FontSize.Caption, color: Colors.Subtle); } } Row(w: Length.Field) { Button("Save", onPress: Save); } } } } } } [Title("Sign in")] [Route("/login")] [AllowAnonymous] [Render(CSR)] component LoginPage() { string email = "[email protected]"; string password = "demo-password"; string problem = ""; action DoLogin() { Session.SignIn(Login(email, password)); } // `Signup` answers "" when it refuses, and `Session.SignIn("")` stores nothing — so without this branch the // button would accept the click and do nothing at all, which is the one thing a refusal must never look like. action DoSignup() { var ticket = Signup(email, password); if (ticket == "") { problem = "Pick a password of at least 8 characters."; } else { Session.SignIn(ticket); } } render { Stack(gap: 4, p: 8, minH: "100vh", bg: Colors.Bg, color: Colors.OnBg) { Text("Dropdown demo", fontSize: FontSize.Display, fontWeight: FontWeight.Medium); Text("Sign up with anything — it is a local throwaway app.", color: Colors.Subtle, fontSize: FontSize.Body); Stack(gap: 2, w: Length.Field) { Field("Email", value: email, placeholder: "[email protected]"); Field("Password", value: password, type: "password", placeholder: "password"); if (problem != "") { Text(problem, fontSize: FontSize.Caption, color: Colors.Subtle); } Row(gap: 2) { Button("Create account", onPress: DoSignup); Button("Sign in", onPress: DoLogin); } } } } }
model/seed.osy8 lines
// The project to lead. Written by a SIGNED-IN member on first visit — `Project` grants create to any authenticated // caller, which is exactly who this is. void SeedDemo() { if (Project.Any()) { return; } new Project { Name = "Apollo" }; UnitOfWork.Commit(); }