Public: anyone can read this; the crew can change it.
An app's members and custom domains speak the home page's language #9914
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/9855-an-app-s-members-and-custom-domains-spea → main 2 commits fork
3 files
changed +35
−29
src/web/pages/settings.ts +16 −29
| @@ -902,8 +902,9 @@ function appMembersView( | ||
| 902 | 902 | access: AppAccess, |
| 903 | 903 | named: readonly string[], |
| 904 | 904 | ): Html { |
| 905 | return card( | |
| 906 | html`<form method="post" action="${base}/settings/app/members"> | |
| 905 | return html`<section class="settings-block" id="members"> | |
| 906 | <h2>${icon("users")} Who may enter</h2> | |
| 907 | <form method="post" action="${base}/settings/app/members"> | |
| 907 | 908 | <div class="field"> |
| 908 | 909 | <label for="f-members">People</label> |
| 909 | 910 | <textarea |
| @@ -924,12 +925,8 @@ ${named.join("\n")}</textarea | ||
| 924 | 925 | </p> |
| 925 | 926 | </div> |
| 926 | 927 | <div class="form-actions">${btn("Save people", { variant: "primary" })}</div> |
| 927 | </form>`, | |
| 928 | { | |
| 929 | head: html`${icon("users")} | |
| 930 | <h2>Who may enter</h2>`, | |
| 931 | }, | |
| 932 | ); | |
| 928 | </form> | |
| 929 | </section>`; | |
| 933 | 930 | } |
| 934 | 931 | |
| 935 | 932 | function appDomainsView( |
| @@ -970,14 +967,13 @@ function appDomainsView( | ||
| 970 | 967 | </div> |
| 971 | 968 | </li>`, |
| 972 | 969 | ); |
| 973 | return card( | |
| 974 | html`<div class="card-body"> | |
| 975 | <p class="muted small"> | |
| 970 | return html`<section class="settings-block" id="domains"> | |
| 971 | <h2>${icon("globe")} Domains</h2> | |
| 972 | <p class="muted small mb"> | |
| 976 | 973 | Serve the app at a name of your own — |
| 977 | 974 | <span class="mono">app.example.com</span>, or an apex. Add it, publish |
| 978 | 975 | the two records it shows, and press Check now. |
| 979 | 976 | </p> |
| 980 | </div> | |
| 981 | 977 | ${when( |
| 982 | 978 | rows.length > 0, |
| 983 | 979 | () => |
| @@ -986,19 +982,15 @@ function appDomainsView( | ||
| 986 | 982 | </ul>`, |
| 987 | 983 | )} |
| 988 | 984 | ${domains === undefined |
| 989 | ? html`<div class="card-body"> | |
| 990 | <p class="muted small"> | |
| 985 | ? html`<p class="muted small"> | |
| 991 | 986 | Nothing is serving apps on this forge, so a domain would have |
| 992 | 987 | nowhere to point. |
| 988 | </p>` | |
| 989 | : html`<p class="muted small mb"> | |
| 990 | The forge serves the app there as soon as the record is | |
| 991 | verified. The app itself learns its new address at its next | |
| 992 | deploy. | |
| 993 | 993 | </p> |
| 994 | </div>` | |
| 995 | : html`<div class="card-body"> | |
| 996 | <p class="muted small"> | |
| 997 | The forge serves the app there as soon as the record is | |
| 998 | verified. The app itself learns its new address at its next | |
| 999 | deploy. | |
| 1000 | </p> | |
| 1001 | </div> | |
| 1002 | 994 | <form |
| 1003 | 995 | class="edit-row" |
| 1004 | 996 | method="post" |
| @@ -1015,13 +1007,8 @@ function appDomainsView( | ||
| 1015 | 1007 | required |
| 1016 | 1008 | /> |
| 1017 | 1009 | ${btn("Add", { variant: "primary", size: "sm", icon: "plus" })} |
| 1018 | </form>`}`, | |
| 1019 | { | |
| 1020 | head: html`${icon("globe")} | |
| 1021 | <h2>Domains</h2>`, | |
| 1022 | pad: false, | |
| 1023 | }, | |
| 1024 | ); | |
| 1010 | </form>`} | |
| 1011 | </section>`; | |
| 1025 | 1012 | } |
| 1026 | 1013 | |
| 1027 | 1014 | /** The records as a person copies them into a zone: type, name, value. */ |
test/app-members.test.ts +3
| @@ -433,6 +433,9 @@ describe("a private app names the people who may enter it", () => { | ||
| 433 | 433 | }) |
| 434 | 434 | ).text(); |
| 435 | 435 | expect(page).toContain("Who may enter"); |
| 436 | const members = page.slice(page.indexOf('id="members"'), page.indexOf("</section>", page.indexOf('id="members"'))); | |
| 437 | expect(members).toContain('action="/alice/game/settings/app/members"'); | |
| 438 | expect(members).not.toContain('class="card'); | |
| 436 | 439 | expect(page).toContain("are named under Who may enter (carol)"); |
| 437 | 440 | // The form saves the whole list. |
| 438 | 441 | const saved = await fetch( |
test/settings-app-domains.test.ts +16
| @@ -160,3 +160,19 @@ test("a writer who is not an admin is refused, as is a stranger who cannot read | ||
| 160 | 160 | expect(hidden.status).toBe(404); |
| 161 | 161 | expect(rows()).toEqual([]); |
| 162 | 162 | }); |
| 163 | ||
| 164 | test("the domains section is headings over rows, not a card, and says what to publish", async () => { | |
| 165 | await post("founder", DOMAINS, { host: HOST }); | |
| 166 | const page = await ( | |
| 167 | await fetch(`${servers.url}/acme/shop/settings/app`, { headers: { cookie: `${cookies["founder"]}; forge_mode=dev` } }) | |
| 168 | ).text(); | |
| 169 | const start = page.indexOf('id="domains"'); | |
| 170 | expect(start).toBeGreaterThan(0); | |
| 171 | const section = page.slice(start, page.indexOf("</section>", start)); | |
| 172 | expect(section).not.toContain('class="card'); | |
| 173 | expect(section).toContain("waiting for DNS"); | |
| 174 | expect(section).toContain(PROOF); | |
| 175 | expect(section).toContain(`${DOMAINS}/${HOST}/check`); | |
| 176 | expect(section).toContain(encodeURIComponent(`${DOMAINS}/${HOST}/delete`)); | |
| 177 | await post("founder", `${DOMAINS}/${HOST}/delete`); | |
| 178 | }); |