| 1 | {{define "content"}} |
| 2 | {{$viewer := .Content.Viewer}} |
| 3 | {{$grantable := .Content.Grantable}} |
| 4 | <h1>Members</h1> |
| 5 | |
| 6 | <table> |
| 7 | <thead><tr><th>Member</th><th>Role</th><th>Status</th><th>Actions</th></tr></thead> |
| 8 | <tbody> |
| 9 | {{range .Content.Members}} |
| 10 | <tr> |
| 11 | <td>{{.Email}}{{if .Name}} ({{.Name}}){{end}}</td> |
| 12 | <td>{{.Role.Title}}</td> |
| 13 | <td>{{if .Active}}Active{{else}}Removed{{end}}</td> |
| 14 | <td> |
| 15 | {{if mayActOn $viewer .}} |
| 16 | {{/* The role selector is built from Grantable, never from the three |
| 17 | role constants. An Admin may grant only Member — idear refuses a |
| 18 | grant that is not strictly below the granter's own rank — so a |
| 19 | selector offering Admin to an Admin would 403 on every submit, |
| 20 | and one offering Owner would 403 for everybody including the |
| 21 | Owner. Hiding a control is never the enforcement; the store |
| 22 | refuses either way. */}} |
| 23 | {{if $grantable}} |
| 24 | <form method="post" action="/members/{{.ID}}/role"> |
| 25 | <select name="role" aria-label="Role"> |
| 26 | {{range $grantable}}<option value="{{.}}">{{.Title}}</option>{{end}} |
| 27 | </select> |
| 28 | <button type="submit">Set role</button> |
| 29 | </form> |
| 30 | {{end}} |
| 31 | {{if .Active}} |
| 32 | <form method="post" action="/members/{{.ID}}/remove"><button type="submit">Remove</button></form> |
| 33 | {{else}} |
| 34 | <form method="post" action="/members/{{.ID}}/restore"><button type="submit">Restore</button></form> |
| 35 | {{end}} |
| 36 | {{end}} |
| 37 | </td> |
| 38 | </tr> |
| 39 | {{end}} |
| 40 | </tbody> |
| 41 | </table> |
| 42 | |
| 43 | {{if $grantable}} |
| 44 | <h2>Invite someone</h2> |
| 45 | <form method="post" action="/members/invitations"> |
| 46 | <label>Email <input type="email" name="email" required></label> |
| 47 | <select name="role" aria-label="Role"> |
| 48 | {{range $grantable}}<option value="{{.}}">{{.Title}}</option>{{end}} |
| 49 | </select> |
| 50 | <button type="submit">Invite</button> |
| 51 | </form> |
| 52 | {{end}} |
| 53 | |
| 54 | {{with .Content.Invitations}} |
| 55 | <h2>Pending invitations</h2> |
| 56 | <ul> |
| 57 | {{range .}} |
| 58 | <li>{{.Email}} — {{.Role.Title}} |
| 59 | <form method="post" action="/members/invitations/{{.ID}}/revoke"><button type="submit">Revoke</button></form> |
| 60 | </li> |
| 61 | {{end}} |
| 62 | </ul> |
| 63 | {{end}} |
| 64 | |
| 65 | {{if .Content.IsOwner}} |
| 66 | <h2>Transfer ownership</h2> |
| 67 | {{/* The only path to Owner. idear demotes the outgoing Owner and |
| 68 | promotes the incoming one in one transaction, so the instance is |
| 69 | never left with two Owners or none — and role=owner is refused on |
| 70 | every other route, for every actor. */}} |
| 71 | <form method="post" action="/members/transfer"> |
| 72 | <select name="member" aria-label="New owner"> |
| 73 | {{range .Content.Members}}{{if and .Active (ne .ID $viewer.ID)}}<option value="{{.ID}}">{{.Email}}</option>{{end}}{{end}} |
| 74 | </select> |
| 75 | <button type="submit">Transfer ownership</button> |
| 76 | </form> |
| 77 | {{end}} |
| 78 | {{end}} |
| 79 | |