landed community membership controls
This commit is contained in:
@@ -481,6 +481,139 @@ func CommunityCategory(ctxt ui.AmContext) (string, any) {
|
|||||||
return "framed", "comm_category.jet"
|
return "framed", "comm_category.jet"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func CommunityMembers(ctxt ui.AmContext) (string, any) {
|
||||||
|
comm := ctxt.CurrentCommunity()
|
||||||
|
if !comm.TestPermission("Community.Write", ctxt.EffectiveLevel()) {
|
||||||
|
return "error", ENOACCESS
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the first batch of page variables.
|
||||||
|
hostRole := database.AmRole("Community.Host")
|
||||||
|
ctxt.VarMap().Set("commName", comm.Name)
|
||||||
|
ctxt.VarMap().Set("backLink", fmt.Sprintf("/comm/%s/admin", comm.Alias))
|
||||||
|
ctxt.VarMap().Set("selfLink", fmt.Sprintf("/comm/%s/admin/members", comm.Alias))
|
||||||
|
ctxt.VarMap().Set("roleList", database.AmRoleList("Community.Userlevels"))
|
||||||
|
ctxt.VarMap().Set("hostRole", hostRole)
|
||||||
|
ctxt.SetFrameTitle(fmt.Sprintf("Membership in Community: %s", comm.Name))
|
||||||
|
|
||||||
|
// Get the search parameter values and adjust them.
|
||||||
|
mode := "comm"
|
||||||
|
field := "name"
|
||||||
|
oper := "st"
|
||||||
|
term := ""
|
||||||
|
offset := 0
|
||||||
|
if ctxt.Verb() == "POST" {
|
||||||
|
mode = ctxt.FormField("mode")
|
||||||
|
field = ctxt.FormField("field")
|
||||||
|
oper = ctxt.FormField("oper")
|
||||||
|
term = ctxt.FormField("term")
|
||||||
|
var e1 error
|
||||||
|
offset, e1 = ctxt.FormFieldInt("ofs")
|
||||||
|
if e1 != nil {
|
||||||
|
offset = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
maxPage := ctxt.Globals().MaxSearchPage
|
||||||
|
|
||||||
|
// Adjust the offset based on the page buttons.
|
||||||
|
if ctxt.FormFieldIsSet("prev") {
|
||||||
|
offset = max(0, offset-int(maxPage))
|
||||||
|
} else if ctxt.FormFieldIsSet("next") {
|
||||||
|
offset += int(maxPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the search parameters back to the page variables.
|
||||||
|
ctxt.VarMap().Set("mode", mode)
|
||||||
|
ctxt.VarMap().Set("field", field)
|
||||||
|
ctxt.VarMap().Set("oper", oper)
|
||||||
|
ctxt.VarMap().Set("term", term)
|
||||||
|
ctxt.VarMap().Set("offset", offset)
|
||||||
|
ctxt.VarMap().Set("max", maxPage)
|
||||||
|
|
||||||
|
if ctxt.FormFieldIsSet("update") {
|
||||||
|
// Parse out the list of valid UIDs.
|
||||||
|
uids := util.Map(strings.Split(ctxt.FormField("validUids"), "|"), func(in string) int32 {
|
||||||
|
rc, err := strconv.Atoi(in)
|
||||||
|
if err != nil {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return int32(rc)
|
||||||
|
})
|
||||||
|
for _, uid := range uids {
|
||||||
|
if uid > 0 {
|
||||||
|
// Get old and new access levels from the form.
|
||||||
|
tmp, err := ctxt.FormFieldInt(fmt.Sprintf("old_%d", uid))
|
||||||
|
if err == nil {
|
||||||
|
oldLevel := uint16(tmp)
|
||||||
|
if oldLevel == hostRole.Level() {
|
||||||
|
tmp = int(oldLevel)
|
||||||
|
} else {
|
||||||
|
tmp, err = ctxt.FormFieldInt(fmt.Sprintf("new_%d", uid))
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
newLevel := uint16(tmp)
|
||||||
|
oldLock := ctxt.FormField(fmt.Sprintf("oldlock_%d", uid)) == "1"
|
||||||
|
newLock := ctxt.FormField(fmt.Sprintf("lock_%d", uid)) == "Y"
|
||||||
|
if (oldLevel != newLevel) || (oldLock != newLock) {
|
||||||
|
// Update the level for this user.
|
||||||
|
var u *database.User
|
||||||
|
u, err = database.AmGetUser(ctxt.Ctx(), uid)
|
||||||
|
if err == nil {
|
||||||
|
err = comm.SetMembership(ctxt.Ctx(), u, newLevel, newLock, ctxt.CurrentUserId(), ctxt.RemoteIP())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return "error", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctxt.VarMap().Set("updated", true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate the result list.
|
||||||
|
total := 0
|
||||||
|
var err error
|
||||||
|
var userlist []*database.User
|
||||||
|
switch mode {
|
||||||
|
case "comm":
|
||||||
|
userlist, total, err = comm.ListMembers(ctxt.Ctx(), database.ListMembersFieldNone, database.ListMembersOperNone, "", offset, int(maxPage), false)
|
||||||
|
case "user":
|
||||||
|
userlist, total, err = database.AmSearchUsers(ctxt.Ctx(), SearchUserFieldMap[field], SearchUserOperMap[oper], term, offset, int(maxPage))
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return "error", err
|
||||||
|
}
|
||||||
|
mr := make([]CMData, len(userlist))
|
||||||
|
for i := range userlist {
|
||||||
|
mr[i].User = userlist[i]
|
||||||
|
var mbr bool
|
||||||
|
mbr, mr[i].Lock, mr[i].Level, err = comm.Membership(ctxt.Ctx(), userlist[i])
|
||||||
|
if err != nil {
|
||||||
|
return "error", err
|
||||||
|
}
|
||||||
|
if !mbr {
|
||||||
|
mr[i].Level = 0
|
||||||
|
mr[i].Lock = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the last few variables and return.
|
||||||
|
ctxt.VarMap().Set("resultList", mr)
|
||||||
|
ctxt.VarMap().Set("total", total)
|
||||||
|
ctxt.VarMap().Set("validUids", strings.Join(util.Map(mr, func(cd CMData) string {
|
||||||
|
return fmt.Sprintf("%d", cd.User.Uid)
|
||||||
|
}), "|"))
|
||||||
|
if offset > 0 {
|
||||||
|
ctxt.VarMap().Set("showPrev", true)
|
||||||
|
}
|
||||||
|
if (offset + len(mr)) < total {
|
||||||
|
ctxt.VarMap().Set("showNext", true)
|
||||||
|
}
|
||||||
|
return "framed", "comm_members.jet"
|
||||||
|
}
|
||||||
|
|
||||||
/* CommunityEmailForm displays the form for sending mass mail to the community.
|
/* CommunityEmailForm displays the form for sending mass mail to the community.
|
||||||
* Parameters:
|
* Parameters:
|
||||||
* ctxt - The AmContext for the request.
|
* ctxt - The AmContext for the request.
|
||||||
@@ -503,6 +636,14 @@ func CommunityEmailForm(ctxt ui.AmContext) (string, any) {
|
|||||||
return "framed", "comm_email.jet"
|
return "framed", "comm_email.jet"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* CommunityEmail sends mass mail to the community.
|
||||||
|
* Parameters:
|
||||||
|
* ctxt - The AmContext for the request.
|
||||||
|
* Returns:
|
||||||
|
* Command string dictating what to be rendered.
|
||||||
|
* Data as a parameter for the command string.
|
||||||
|
* Standard Go error status.
|
||||||
|
*/
|
||||||
func CommunityEmail(ctxt ui.AmContext) (string, any) {
|
func CommunityEmail(ctxt ui.AmContext) (string, any) {
|
||||||
comm := ctxt.CurrentCommunity()
|
comm := ctxt.CurrentCommunity()
|
||||||
if !comm.TestPermission("Community.MassMail", ctxt.EffectiveLevel()) {
|
if !comm.TestPermission("Community.MassMail", ctxt.EffectiveLevel()) {
|
||||||
|
|||||||
+2
-1
@@ -214,10 +214,11 @@ func ConferenceAliasAdd(ctxt ui.AmContext) (string, any) {
|
|||||||
return "framed", "conf_aliases.jet"
|
return "framed", "conf_aliases.jet"
|
||||||
}
|
}
|
||||||
|
|
||||||
// CMData is the result data passed to the conference members page.
|
// CMData is the result data passed to the community or conference members page.
|
||||||
type CMData struct {
|
type CMData struct {
|
||||||
User *database.User
|
User *database.User
|
||||||
Level uint16
|
Level uint16
|
||||||
|
Lock bool
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ConferenceMembers shows the conference members and allows their access levels to be adjusted.
|
/* ConferenceMembers shows the conference members and allows their access levels to be adjusted.
|
||||||
|
|||||||
+43
-11
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Amsterdam Web Communities System
|
* Amsterdam Web Communities System
|
||||||
* Copyright (c) 2025 Erbosoft Metaverse Design Solutions, All Rights Reserved
|
* Copyright (c) 2025-2026 Erbosoft Metaverse Design Solutions, All Rights Reserved
|
||||||
*
|
*
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/labstack/gommon/log"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -239,7 +240,11 @@ func (r *CfgRoleList) FindForLevel(level uint16) Role {
|
|||||||
* The specified role.
|
* The specified role.
|
||||||
*/
|
*/
|
||||||
func AmRole(id string) Role {
|
func AmRole(id string) Role {
|
||||||
return securityRoot.roleMap[id]
|
rc, ok := securityRoot.roleMap[id]
|
||||||
|
if !ok {
|
||||||
|
log.Errorf("AmRole('%s') - role not found!", id)
|
||||||
|
}
|
||||||
|
return rc
|
||||||
}
|
}
|
||||||
|
|
||||||
/* AmDefaultRole returns a Role given a default ID.
|
/* AmDefaultRole returns a Role given a default ID.
|
||||||
@@ -249,7 +254,12 @@ func AmRole(id string) Role {
|
|||||||
* The specified role.
|
* The specified role.
|
||||||
*/
|
*/
|
||||||
func AmDefaultRole(id string) Role {
|
func AmDefaultRole(id string) Role {
|
||||||
return securityRoot.defaultsMap[id].roleptr
|
dr, ok := securityRoot.defaultsMap[id]
|
||||||
|
if !ok {
|
||||||
|
log.Errorf("AmDefaultRole('%s') - default role not found!", id)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return dr.roleptr
|
||||||
}
|
}
|
||||||
|
|
||||||
/* AmRoleList returns a RoleList given a list ID.
|
/* AmRoleList returns a RoleList given a list ID.
|
||||||
@@ -259,7 +269,11 @@ func AmDefaultRole(id string) Role {
|
|||||||
* The specified role list.
|
* The specified role list.
|
||||||
*/
|
*/
|
||||||
func AmRoleList(id string) RoleList {
|
func AmRoleList(id string) RoleList {
|
||||||
return securityRoot.listsMap[id]
|
rc, ok := securityRoot.listsMap[id]
|
||||||
|
if !ok {
|
||||||
|
log.Errorf("AmRoleList('%s') - role list not found!", id)
|
||||||
|
}
|
||||||
|
return rc
|
||||||
}
|
}
|
||||||
|
|
||||||
/* AmTestPermission tests a specified access level to see if it satisfies the given permission.
|
/* AmTestPermission tests a specified access level to see if it satisfies the given permission.
|
||||||
@@ -270,12 +284,22 @@ func AmRoleList(id string) RoleList {
|
|||||||
* true if the permission test is satisfied, false if not.
|
* true if the permission test is satisfied, false if not.
|
||||||
*/
|
*/
|
||||||
func AmTestPermission(id string, level uint16) bool {
|
func AmTestPermission(id string, level uint16) bool {
|
||||||
return securityRoot.permsMap[id].level <= level
|
perm, ok := securityRoot.permsMap[id]
|
||||||
|
if !ok {
|
||||||
|
log.Errorf("AmTestPermission('%s') - permission not found!", id)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return perm.level <= level
|
||||||
}
|
}
|
||||||
|
|
||||||
// AmPermissionLevel returns a level value for a permission.
|
// AmPermissionLevel returns a level value for a permission.
|
||||||
func AmPermissionLevel(id string) uint16 {
|
func AmPermissionLevel(id string) uint16 {
|
||||||
return securityRoot.permsMap[id].level
|
perm, ok := securityRoot.permsMap[id]
|
||||||
|
if !ok {
|
||||||
|
log.Errorf("AmPermissionLevel('%s') - permission not found!", id)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return perm.level
|
||||||
}
|
}
|
||||||
|
|
||||||
/* AmCombinePermissionRole combines a permission and a role into a single permission level.
|
/* AmCombinePermissionRole combines a permission and a role into a single permission level.
|
||||||
@@ -286,10 +310,18 @@ func AmPermissionLevel(id string) uint16 {
|
|||||||
* The combined permission level.
|
* The combined permission level.
|
||||||
*/
|
*/
|
||||||
func AmCombinePermissionRole(perm string, role string) uint16 {
|
func AmCombinePermissionRole(perm string, role string) uint16 {
|
||||||
p1 := securityRoot.permsMap[perm].level
|
pperm, ok := securityRoot.permsMap[perm]
|
||||||
p2 := securityRoot.roleMap[role].level
|
if !ok {
|
||||||
if p1 > p2 {
|
log.Errorf("AmCombinePermissionRole('%s', '%s') - permission not found!", perm, role)
|
||||||
return p1
|
return 0
|
||||||
}
|
}
|
||||||
return p2
|
prole, ok := securityRoot.roleMap[role]
|
||||||
|
if !ok {
|
||||||
|
log.Errorf("AmCombinePermissionRole('%s', '%s') - role not found!", perm, role)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if pperm.level > prole.level {
|
||||||
|
return pperm.level
|
||||||
|
}
|
||||||
|
return prole.level
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ _(italicized items can be deferred)_
|
|||||||
- ~~Conferences List honor "hide in list" flag~~
|
- ~~Conferences List honor "hide in list" flag~~
|
||||||
- Community Admin Menu:
|
- Community Admin Menu:
|
||||||
- ~~Set Community Category~~
|
- ~~Set Community Category~~
|
||||||
- Membership Control
|
- ~~Membership Control~~
|
||||||
- ~~E-Mail to All Members~~
|
- ~~E-Mail to All Members~~
|
||||||
- ~~Display Audit Records~~
|
- ~~Display Audit Records~~
|
||||||
- Delete Community
|
- Delete Community
|
||||||
|
|||||||
@@ -124,6 +124,7 @@ func setupEcho() *echo.Echo {
|
|||||||
adminGroup.POST("/logo", ui.AmWrap(EditCommunityLogo))
|
adminGroup.POST("/logo", ui.AmWrap(EditCommunityLogo))
|
||||||
adminGroup.Match(GetAndPost, "/audit", ui.AmWrap(CommunityAudit))
|
adminGroup.Match(GetAndPost, "/audit", ui.AmWrap(CommunityAudit))
|
||||||
adminGroup.GET("/category", ui.AmWrap(CommunityCategory))
|
adminGroup.GET("/category", ui.AmWrap(CommunityCategory))
|
||||||
|
adminGroup.Match(GetAndPost, "/members", ui.AmWrap(CommunityMembers))
|
||||||
adminGroup.GET("/massmail", ui.AmWrap(CommunityEmailForm))
|
adminGroup.GET("/massmail", ui.AmWrap(CommunityEmailForm))
|
||||||
adminGroup.POST("/massmail", ui.AmWrap(CommunityEmail))
|
adminGroup.POST("/massmail", ui.AmWrap(CommunityEmail))
|
||||||
|
|
||||||
@@ -189,6 +190,7 @@ var ampool *util.WorkerPool
|
|||||||
|
|
||||||
// main is Ye Olde Main Function.
|
// main is Ye Olde Main Function.
|
||||||
func main() {
|
func main() {
|
||||||
|
start := time.Now()
|
||||||
// Configure the system.
|
// Configure the system.
|
||||||
config.SetupConfig()
|
config.SetupConfig()
|
||||||
closer, err := database.SetupDb()
|
closer, err := database.SetupDb()
|
||||||
@@ -230,6 +232,9 @@ func main() {
|
|||||||
database.AmStoreAudit(database.AmNewAudit(database.AuditShutdown, 0, myIP.String()))
|
database.AmStoreAudit(database.AmNewAudit(database.AuditShutdown, 0, myIP.String()))
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
stime := time.Since(start)
|
||||||
|
log.Infof("Amsterdam startup sequence completed in %v", stime)
|
||||||
|
|
||||||
// Start server
|
// Start server
|
||||||
go func() {
|
go func() {
|
||||||
if err := e.Start(":1323"); err != nil && err != http.ErrServerClosed {
|
if err := e.Start(":1323"); err != nil && err != http.ErrServerClosed {
|
||||||
|
|||||||
+1
-1
@@ -72,7 +72,7 @@ menudefs:
|
|||||||
permission: "Community.Write"
|
permission: "Community.Write"
|
||||||
disabled: true
|
disabled: true
|
||||||
- text: "Membership Control"
|
- text: "Membership Control"
|
||||||
link: "/TODO/comm/[CID]/admin/members"
|
link: "/comm/[CID]/admin/members"
|
||||||
permission: "Community.ShowAdmin"
|
permission: "Community.ShowAdmin"
|
||||||
- text: "E-Mail to All Members"
|
- text: "E-Mail to All Members"
|
||||||
link: "/comm/[CID]/admin/massmail"
|
link: "/comm/[CID]/admin/massmail"
|
||||||
|
|||||||
@@ -0,0 +1,160 @@
|
|||||||
|
{*
|
||||||
|
* Amsterdam Web Communities System
|
||||||
|
* Copyright (c) 2025-2026 Erbosoft Metaverse Design Solutions, All Rights Reserved
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
*}
|
||||||
|
<div class="p-4">
|
||||||
|
<!-- Page Title -->
|
||||||
|
<div class="mb-6">
|
||||||
|
<div class="flex items-baseline gap-2">
|
||||||
|
<h1 class="text-blue-800 text-4xl font-bold">Set Community Membership:</h1>
|
||||||
|
<span class="text-blue-800 text-2xl">{{ commName }}</span>
|
||||||
|
</div>
|
||||||
|
<hr class="border-2 border-gray-400 w-4/5 mt-2 mb-6">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Upper Links -->
|
||||||
|
<div class="mb-4 flex items-baseline gap-2">
|
||||||
|
<a class="text-blue-700 hover:text-blue-900 text-sm flex items-center gap-2 w-fit" href="{{ selfLink }}">
|
||||||
|
<span>🔃</span>
|
||||||
|
Redisplay Member List
|
||||||
|
</a>
|
||||||
|
|
|
||||||
|
<a class="text-blue-700 hover:text-blue-900 text-sm flex items-center gap-2 w-fit" href="{{ backLink }}">
|
||||||
|
<span>←</span>
|
||||||
|
Return to Community Administration Menu
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Member Find Form -->
|
||||||
|
<div class="max-w-3xl mb-8">
|
||||||
|
<form method="POST" action="{{ selfLink }}">
|
||||||
|
<input type="hidden" name="mode" value="user">
|
||||||
|
<input type="hidden" name="ofs" value="0">
|
||||||
|
|
||||||
|
<div class="bg-gray-50 p-6 rounded-lg">
|
||||||
|
<h2 class="text-xl font-bold text-black mb-4">Find Users:</h2>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="flex items-center gap-2 text-sm">
|
||||||
|
<span class="text-black">Display all users whose</span>
|
||||||
|
<select name="field"
|
||||||
|
class="px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||||
|
<option value="name" {{ if field == "name" }}selected{{ end }}>user name</option>
|
||||||
|
<option value="descr"{{ if field == "descr" }}selected{{ end }}>description</option>
|
||||||
|
<option value="first"{{ if field == "first" }}selected{{ end }}>first name</option>
|
||||||
|
<option value="last"{{ if field == "last" }}selected{{ end }}>last name</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Search Criteria -->
|
||||||
|
<div class="flex items-center gap-2 text-sm flex-wrap">
|
||||||
|
<select name="oper"
|
||||||
|
class="px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||||
|
<option value="st" {{ if oper == "st" }}selected{{ end }}>starts with the string</option>
|
||||||
|
<option value="in" {{ if oper == "in" }}selected{{ end }}>contains the string</option>
|
||||||
|
<option value="re" {{ if oper == "re" }}selected{{ end }}>matches the regular expression</option>
|
||||||
|
</select>
|
||||||
|
<input type="text" name="term" size="32" maxlength="255" value="{{ term }}"
|
||||||
|
placeholder="Enter search term..."
|
||||||
|
class="flex-1 min-w-64 px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button type="submit" name="search"
|
||||||
|
class="bg-blue-600 hover:bg-blue-700 text-white px-8 py-2 rounded font-medium transition-colors">Search</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="border-gray-400 mb-4">
|
||||||
|
<div class="max-w-3xl flex justify-between items-center mb-4">
|
||||||
|
<h2 class="text-xl font-bold text-black mb-4">
|
||||||
|
{{ if mode == "comm" }}
|
||||||
|
Community Members:
|
||||||
|
{{ else if mode == "user" }}
|
||||||
|
Search Results:
|
||||||
|
{{ end }}
|
||||||
|
</h2>
|
||||||
|
{{ if total > 0 }}
|
||||||
|
<div class="text-sm text-black font-bold">(Displaying {{ offset + 1 }}-{{ offset + len(resultList) }} of {{ total }})</div>
|
||||||
|
{{ else }}
|
||||||
|
<div class="text-sm text-black font-bold">(None found)</div>
|
||||||
|
{{ end }}
|
||||||
|
{{ if isset(showPrev) || isset(showNext) }}
|
||||||
|
<div class="flex justify-center gap-4 mt-6">
|
||||||
|
<form method="POST" action="{{ selfLink }}">
|
||||||
|
<input type="hidden" name="mode" value="{{ mode }}">
|
||||||
|
<input type="hidden" name="field" value="{{ field }}">
|
||||||
|
<input type="hidden" name="oper" value="{{ oper }}">
|
||||||
|
<input type="hidden" name="term" value="{{ term }}">
|
||||||
|
<input type="hidden" name="ofs" value="{{ offset }}">
|
||||||
|
{{ if isset(showPrev) }}
|
||||||
|
<button type="submit" name="prev"
|
||||||
|
class="bg-blue-600 hover:bg-blue-700 text-white px-8 py-2 rounded font-medium transition-colors">
|
||||||
|
⏪ Prev
|
||||||
|
</button>
|
||||||
|
{{ end }}
|
||||||
|
{{ if isset(showNext) }}
|
||||||
|
<button type="submit" name="next"
|
||||||
|
class="bg-blue-600 hover:bg-blue-700 text-white px-8 py-2 rounded font-medium transition-colors">
|
||||||
|
Next ⏩
|
||||||
|
</button>
|
||||||
|
{{ end }}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="POST" action="{{ selfLink }}">
|
||||||
|
<input type="hidden" name="mode" value="{{ mode }}">
|
||||||
|
<input type="hidden" name="field" value="{{ field }}">
|
||||||
|
<input type="hidden" name="oper" value="{{ oper }}">
|
||||||
|
<input type="hidden" name="term" value="{{ term }}">
|
||||||
|
<input type="hidden" name="ofs" value="{{ offset }}">
|
||||||
|
<input type="hidden" name="validUids" value="{{ validUids }}">
|
||||||
|
<div class="max-w-2xl bg-gray-50 p-6 rounded-lg">
|
||||||
|
<div class="space-y-4">
|
||||||
|
{{ range _, rx := resultList }}
|
||||||
|
{{ isHost := (rx.Level == hostRole.Level()) }}
|
||||||
|
<div class="flex items-start gap-3">
|
||||||
|
<span class="text-sm pt-0.5 flex-shrink-0">🟣</span>
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="mb-2">
|
||||||
|
<a href="/user/{{ rx.User.Username }}"
|
||||||
|
class="text-blue-700 hover:text-blue-900 font-bold text-base">{{ rx.User.Username }}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input type="hidden" name="old_{{ rx.User.Uid }}" value="{{ rx.Level }}">
|
||||||
|
<select id="new_{{ rx.User.Uid }}" name="new_{{ rx.User.Uid }}" {{ if isHost }}disabled{{ end }}
|
||||||
|
class="flex-1 px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||||
|
{{ if isHost }}
|
||||||
|
<option value="{{ hostRole.Level() }}" selected>{{ hostRole.Name() }}</option>
|
||||||
|
{{ else }}
|
||||||
|
{{ range _, role := roleList.Roles() }}
|
||||||
|
<option value="{{ role.Level() }}" {{ if role.Level() == rx.Level }}selected{{ end }}>{{ role.Name() }}</option>
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
|
</select>
|
||||||
|
<input type="hidden" name="oldlock_{{ rx.User.Uid }}" value="{{ iif(rx.Lock,1,0) }}">
|
||||||
|
<span class="pt-0.5 flex-shrink-0">🔒</span>
|
||||||
|
<input type="checkbox" name="lock_{{ rx.User.Uid }}" value="Y" {{ if rx.Lock }}checked{{ end }} {{ if isHost }}disabled{{ end }}
|
||||||
|
class="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500" />
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
<div class="flex items-start gap-3">
|
||||||
|
<button type="submit" name="update"
|
||||||
|
class="bg-blue-600 hover:bg-blue-700 text-white px-8 py-2 rounded font-medium transition-colors">Update</button>
|
||||||
|
{{ if isset(updated) }}
|
||||||
|
<span class="text-lg text-green font-bold">✅ Updated!</span>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
Reference in New Issue
Block a user