[WIP] first draft of conference members functionality
This commit is contained in:
@@ -14,6 +14,8 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.erbosoft.com/amy/amsterdam/database"
|
||||
"git.erbosoft.com/amy/amsterdam/ui"
|
||||
@@ -213,6 +215,153 @@ func ConferenceAliasAdd(ctxt ui.AmContext) (string, any, error) {
|
||||
return "framed_template", "conf_aliases.jet", nil
|
||||
}
|
||||
|
||||
// CMData is the result data passed to the conference members page.
|
||||
type CMData struct {
|
||||
User *database.User
|
||||
Level uint16
|
||||
}
|
||||
|
||||
// fieldMap maps field names to search field indexes.
|
||||
var fieldMap = map[string]int{
|
||||
"name": database.SearchUserFieldName,
|
||||
"descr": database.SearchUserFieldDescription,
|
||||
"first": database.SearchUserFieldFirstName,
|
||||
"last": database.SearchUserFieldLastName,
|
||||
}
|
||||
|
||||
// operMap maps operator names to search operator indices.
|
||||
var operMap = map[string]int{
|
||||
"st": database.SearchUserOperPrefix,
|
||||
"in": database.SearchUserOperSubstring,
|
||||
"re": database.SearchUserOperRegex,
|
||||
}
|
||||
|
||||
/* ConferenceMembers shows the conference members and allows their access levels to be adjusted.
|
||||
* 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 ConferenceMembers(ctxt ui.AmContext) (string, any, error) {
|
||||
comm := ctxt.CurrentCommunity()
|
||||
conf := ctxt.GetScratch("currentConference").(*database.Conference)
|
||||
myLevel := ctxt.GetScratch("levelInConference").(uint16)
|
||||
if !conf.TestPermission("Conference.Change", myLevel) {
|
||||
ctxt.SetRC(http.StatusForbidden)
|
||||
return ui.ErrorPage(ctxt, ENOPERM)
|
||||
}
|
||||
|
||||
// Set the first batch of page variables.
|
||||
ctxt.VarMap().Set("commName", comm.Name)
|
||||
ctxt.VarMap().Set("confName", conf.Name)
|
||||
ctxt.VarMap().Set("backLink", fmt.Sprintf("/comm/%s/conf/%s/manage", comm.Alias, ctxt.GetScratch("currentAlias")))
|
||||
ctxt.VarMap().Set("selfLink", fmt.Sprintf("/comm/%s/conf/%s/members", comm.Alias, ctxt.GetScratch("currentAlias")))
|
||||
ctxt.VarMap().Set("roleList", database.AmRoleList("Conference.UserLevels"))
|
||||
ctxt.VarMap().Set("amsterdam_pageTitle", fmt.Sprintf("Membership in Conference: %s", conf.Name))
|
||||
|
||||
// Get the search parameter values and adjust them.
|
||||
mode := ctxt.Parameter("mode")
|
||||
field := ctxt.Parameter("field")
|
||||
oper := ctxt.Parameter("oper")
|
||||
term := ctxt.Parameter("term")
|
||||
offsetStr := ctxt.Parameter("ofs")
|
||||
if mode == "" {
|
||||
mode = "conf"
|
||||
}
|
||||
if field == "" {
|
||||
field = "name"
|
||||
}
|
||||
if oper == "" {
|
||||
oper = "st"
|
||||
}
|
||||
offset := 0
|
||||
if offsetStr != "" {
|
||||
var err error
|
||||
offset, err = strconv.Atoi(offsetStr)
|
||||
if err != nil {
|
||||
offset = 0
|
||||
}
|
||||
}
|
||||
maxPage := ctxt.Globals().MaxSearchPage
|
||||
|
||||
// Adjust the offset based on the page buttons.
|
||||
if ctxt.HasParameter("prev") {
|
||||
offset = max(0, offset-int(maxPage))
|
||||
} else if ctxt.HasParameter("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.HasParameter("update") {
|
||||
// TODO: update the levels
|
||||
}
|
||||
|
||||
// Get the member list for the conference.
|
||||
members, err := conf.Members(ctxt.Ctx())
|
||||
if err != nil {
|
||||
return ui.ErrorPage(ctxt, err)
|
||||
}
|
||||
|
||||
// Generate the result list.
|
||||
total := 0
|
||||
var mr []CMData
|
||||
switch mode {
|
||||
case "conf":
|
||||
total = len(members)
|
||||
if offset > 0 {
|
||||
members = members[offset:]
|
||||
}
|
||||
if len(members) > int(maxPage) {
|
||||
members = members[:maxPage]
|
||||
}
|
||||
mr = make([]CMData, len(members))
|
||||
for i := range members {
|
||||
mr[i].User, _ = database.AmGetUser(ctxt.Ctx(), members[i].Uid)
|
||||
mr[i].Level = members[i].Level
|
||||
}
|
||||
case "comm":
|
||||
ulist, t, err := database.AmSearchCommunityMembers(ctxt.Ctx(), comm, fieldMap[field], operMap[oper], term, offset, int(maxPage))
|
||||
if err != nil {
|
||||
return ui.ErrorPage(ctxt, err)
|
||||
}
|
||||
total = t
|
||||
mr = make([]CMData, len(ulist))
|
||||
for i := range ulist {
|
||||
mr[i].User = ulist[i]
|
||||
mr[i].Level = 0
|
||||
for j := range members {
|
||||
if members[j].Uid == ulist[i].Uid {
|
||||
mr[i].Level = members[j].Level
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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_template", "conf_members.jet", nil
|
||||
}
|
||||
|
||||
/* CreateConferenceForm displays the dialog for creating a new conference.
|
||||
* Parameters:
|
||||
* ctxt - The AmContext for the request.
|
||||
|
||||
@@ -1056,3 +1056,93 @@ func AmSearchCommunities(ctx context.Context, field int, oper int, term string,
|
||||
}
|
||||
return rc, total, nil
|
||||
}
|
||||
|
||||
/* AmSearchCommunityMembers searches for members of a community matching certain criteria.
|
||||
* Parameters:
|
||||
* ctx - Standard Go context value.
|
||||
* c - The community within which to search.
|
||||
* field - A value indicating which field to search:
|
||||
* SearchUserFieldName - The user name.
|
||||
* SearchUserFieldDescription - The user description.
|
||||
* SearchUserFieldFirstName - The user's first name.
|
||||
* SearchUserFieldLastName - The user's last name.
|
||||
* oper - The operation to perform on the search field:
|
||||
* SearchUserOperPrefix - The specified field has the string "term" as a prefix.
|
||||
* SearchUserOperSubstring - The specified field contains the string "term".
|
||||
* SearchUserOperRegex - The specified field matches the regular expression in "term".
|
||||
* term - The search term, as specified above.
|
||||
* offset - Number of users to skip at beginning of list.
|
||||
* max - Maximum number of users to return.
|
||||
* Returns:
|
||||
* Array of User pointers representing the return elements.
|
||||
* The total number of users matching this query (could be greater than max)
|
||||
* Standard Go error status.
|
||||
*/
|
||||
func AmSearchCommunityMembers(ctx context.Context, c *Community, field int, oper int, term string, offset int, max int) ([]*User, int, error) {
|
||||
var queryPortion strings.Builder
|
||||
switch field {
|
||||
case SearchUserFieldName:
|
||||
queryPortion.WriteString("u.username ")
|
||||
case SearchUserFieldDescription:
|
||||
queryPortion.WriteString("u.description ")
|
||||
case SearchUserFieldFirstName:
|
||||
queryPortion.WriteString("c.given_name ")
|
||||
case SearchUserFieldLastName:
|
||||
queryPortion.WriteString("c.family_name ")
|
||||
default:
|
||||
return nil, -1, errors.New("invalid field selector")
|
||||
}
|
||||
switch oper {
|
||||
case SearchUserOperPrefix:
|
||||
queryPortion.WriteString("LIKE '")
|
||||
queryPortion.WriteString(util.SqlEscape(term, true))
|
||||
queryPortion.WriteString("%'")
|
||||
case SearchUserOperSubstring:
|
||||
queryPortion.WriteString("LIKE '%")
|
||||
queryPortion.WriteString(util.SqlEscape(term, true))
|
||||
queryPortion.WriteString("%'")
|
||||
case SearchUserOperRegex:
|
||||
queryPortion.WriteString("REGEXP '")
|
||||
queryPortion.WriteString(util.SqlEscape(term, false))
|
||||
queryPortion.WriteString("'")
|
||||
default:
|
||||
return nil, -1, errors.New("invalid operator selector")
|
||||
}
|
||||
q := queryPortion.String()
|
||||
row := amdb.QueryRowContext(ctx, `SELECT COUNT(*) FROM users u, contacts c, commmember m WHERE u.contactid = c.contactid AND u.uid = m.uid
|
||||
AND m.commid = ? AND u.is_anon = 0 AND `+q, c.Id)
|
||||
var total int
|
||||
err := row.Scan(&total)
|
||||
if err != nil {
|
||||
return nil, -1, err
|
||||
}
|
||||
if total == 0 {
|
||||
return make([]*User, 0), 0, nil
|
||||
}
|
||||
var rs *sql.Rows
|
||||
if offset > 0 {
|
||||
rs, err = amdb.QueryContext(ctx, `SELECT u.uid FROM users u, contacts c, commmember m WHERE u.contactid = c.contactid AND u.uid = m.uid
|
||||
AND m.commid = ? AND u.is_anon = 0 AND `+q+" ORDER BY u.username LIMIT ? OFFSET ?", c.Id, max, offset)
|
||||
} else {
|
||||
rs, err = amdb.QueryContext(ctx, `SELECT u.uid FROM users u, contacts c, commmember m WHERE u.contactid = c.contactid AND u.uid = m.uid
|
||||
AND m.commid = ? AND u.is_anon = 0 AND `+q+" ORDER BY u.username LIMIT ?", c.Id, max)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, total, err
|
||||
}
|
||||
rc := make([]*User, 0, min(max, 10000))
|
||||
for rs.Next() {
|
||||
var uid int32
|
||||
if err = rs.Scan(&uid); err == nil {
|
||||
var u *User
|
||||
u, err = AmGetUser(ctx, uid)
|
||||
if err == nil {
|
||||
rc = append(rc, u)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
log.Errorf("AmSearchCommunityMembers scan error: %v", err)
|
||||
}
|
||||
}
|
||||
return rc, total, nil
|
||||
}
|
||||
|
||||
+57
-14
@@ -14,6 +14,8 @@ import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -44,6 +46,7 @@ type Conference struct {
|
||||
flags *util.OptionSet
|
||||
}
|
||||
|
||||
// ConferenceSettings represents a user's settings within the conference.
|
||||
type ConferenceSettings struct {
|
||||
ConfId int32 `db:"confid"` // conference ID
|
||||
Uid int32 `db:"uid"` // user ID
|
||||
@@ -53,6 +56,13 @@ type ConferenceSettings struct {
|
||||
newflag bool
|
||||
}
|
||||
|
||||
// ConferenceMember represents the membership entries in a conference.
|
||||
type ConferenceMember struct {
|
||||
ConfId int32 `db:"confid"` // conference ID
|
||||
Uid int32 `db:"uid"` // user ID
|
||||
Level uint16 `db:"granted_lvl"` // level granted within the conference
|
||||
}
|
||||
|
||||
// ConferenceProperties represents a property entry for a conference.
|
||||
type ConferenceProperties struct {
|
||||
ConfId int32 `db:"confid"` // conference ID
|
||||
@@ -162,6 +172,7 @@ func (c *Conference) AddAlias(ctx context.Context, alias string, u *User, ipaddr
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveAlias removes an alias from the conference.
|
||||
func (c *Conference) RemoveAlias(ctx context.Context, alias string, u *User, ipaddr string) error {
|
||||
row := amdb.QueryRowContext(ctx, "SELECT COUNT(*) FROM confalias WHERE confid = ?", c.ConfId)
|
||||
aliasCount := 0
|
||||
@@ -200,24 +211,30 @@ func (c *Conference) RemoveAlias(ctx context.Context, alias string, u *User, ipa
|
||||
|
||||
// Hosts returns the list of users that host this conference.
|
||||
func (c *Conference) Hosts(ctx context.Context) ([]*User, error) {
|
||||
rs, err := amdb.QueryContext(ctx, "SELECT uid FROM confmember WHERE confid = ? AND granted_lvl = ?",
|
||||
c.ConfId, AmRole("Conference.Host").Level())
|
||||
var uids []int32
|
||||
err := amdb.SelectContext(ctx, &uids, "SELECT uid FROM confmember WHERE confid = ? AND granted_lvl = ?", c.ConfId, AmRole("Conference.Host").Level())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rc := make([]*User, 0, 5)
|
||||
for rs.Next() {
|
||||
var uid int32
|
||||
if err = rs.Scan(&uid); err == nil {
|
||||
u, err := AmGetUser(ctx, uid)
|
||||
if err == nil {
|
||||
rc = append(rc, u)
|
||||
}
|
||||
rc := make([]*User, 0, len(uids))
|
||||
for _, uid := range uids {
|
||||
u, err := AmGetUser(ctx, uid)
|
||||
if err == nil {
|
||||
rc = append(rc, u)
|
||||
}
|
||||
}
|
||||
slices.SortFunc(rc, func(a, b *User) int {
|
||||
return strings.Compare(strings.ToLower(a.Username), strings.ToLower(b.Username))
|
||||
})
|
||||
return rc, nil
|
||||
}
|
||||
|
||||
// Hosts returns the list of users that host this conference, quietly.
|
||||
func (c *Conference) HostsQ(ctx context.Context) []*User {
|
||||
rc, _ := c.Hosts(ctx)
|
||||
return rc
|
||||
}
|
||||
|
||||
// InCommunity returns true if the specified conference is in the community.
|
||||
func (c *Conference) InCommunity(ctx context.Context, comm *Community) (bool, error) {
|
||||
row := amdb.QueryRowContext(ctx, "SELECT commid FROM commtoconf WHERE commid = ? AND confid = ?", comm.Id, c.ConfId)
|
||||
@@ -274,10 +291,11 @@ func (c *Conference) ContainedBy(ctx context.Context) ([]*Community, error) {
|
||||
return rc, nil
|
||||
}
|
||||
|
||||
// Hosts returns the list of users that host this conference, quietly.
|
||||
func (c *Conference) HostsQ(ctx context.Context) []*User {
|
||||
rc, _ := c.Hosts(ctx)
|
||||
return rc
|
||||
// Members returns all the members of this conference, with their granted user levels.
|
||||
func (c *Conference) Members(ctx context.Context) ([]ConferenceMember, error) {
|
||||
var rc []ConferenceMember
|
||||
err := amdb.SelectContext(ctx, &rc, "SELECT * FROM confmember WHERE confid = ?", c.ConfId)
|
||||
return rc, err
|
||||
}
|
||||
|
||||
// Membership returns a membership flag and granted level for the user in this conference.
|
||||
@@ -294,6 +312,31 @@ func (c *Conference) Membership(ctx context.Context, u *User) (bool, uint16, err
|
||||
return false, 0, err
|
||||
}
|
||||
|
||||
// SetMembership sets the membership level for the given user in this conference.
|
||||
func (c *Conference) SetMembership(ctx context.Context, u *User, level uint16, by *User, ipaddr string) error {
|
||||
if level == 0 {
|
||||
_, err := amdb.ExecContext(ctx, "DELETE FROM confmember WHERE confid = ? AND uid = ?", c.ConfId, u.Uid)
|
||||
return err
|
||||
}
|
||||
row := amdb.QueryRowContext(ctx, "SELECT granted_lvl FROM confmember WHERE confid = ? AND uid = ?", c.ConfId, u.Uid)
|
||||
var oldLevel uint16
|
||||
err := row.Scan(&oldLevel)
|
||||
switch err {
|
||||
case nil:
|
||||
if oldLevel == level {
|
||||
return nil
|
||||
}
|
||||
_, err = amdb.ExecContext(ctx, "UPDATE confmember SET granted_lvl = ? WHERE confid = ? AND uid = ?", level, c.ConfId, u.Uid)
|
||||
case sql.ErrNoRows:
|
||||
_, err = amdb.ExecContext(ctx, "INSERT INTO confmember (confid, uid, granted_lvl) VALUES (?, ?, ?)", c.ConfId, u.Uid, level)
|
||||
}
|
||||
if err != nil {
|
||||
ar := AmNewAudit(AuditConferenceMembership, by.Uid, ipaddr, fmt.Sprintf("conf=%d", c.ConfId), fmt.Sprintf("uid=%d", u.Uid), fmt.Sprintf("level=%d", level))
|
||||
AmStoreAudit(ar)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
/* TestPermission is shorthand that tests if a user has a permission with respect to the conference.
|
||||
* Parameters:
|
||||
* user - The user to be checked.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#
|
||||
# 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
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
@@ -15,7 +15,7 @@ scopes:
|
||||
index: 6
|
||||
roles:
|
||||
- name: "NotInList"
|
||||
display: "Not In List"
|
||||
display: "(not in list)"
|
||||
scope: Global
|
||||
value: "0"
|
||||
- name: "UnrestrictedUser"
|
||||
|
||||
@@ -81,9 +81,9 @@ func setupEcho() *echo.Echo {
|
||||
|
||||
// community group
|
||||
commGroup := e.Group("/comm/:cid", ui.SetCommunity)
|
||||
fn1 := ui.AmWrap(ShowCommunity)
|
||||
commGroup.GET("", fn1)
|
||||
commGroup.GET("/profile", fn1)
|
||||
fn = ui.AmWrap(ShowCommunity)
|
||||
commGroup.GET("", fn)
|
||||
commGroup.GET("/profile", fn)
|
||||
commGroup.GET("/join", ui.AmWrap(JoinCommunity))
|
||||
commGroup.POST("/join", ui.AmWrap(JoinCommunityWithKey))
|
||||
commGroup.GET("/unjoin", ui.AmWrap(UnjoinCommunity))
|
||||
@@ -116,6 +116,9 @@ func setupEcho() *echo.Echo {
|
||||
confGroup.POST("/edit", ui.AmWrap(EditConference))
|
||||
confGroup.GET("/aliases", ui.AmWrap(ConferenceAliasForm))
|
||||
confGroup.POST("/aliases", ui.AmWrap(ConferenceAliasAdd))
|
||||
fn = ui.AmWrap(ConferenceMembers)
|
||||
confGroup.GET("/members", fn)
|
||||
confGroup.POST("/members", fn)
|
||||
confGroup.GET("/hotlist", ui.AmWrap(AddToHotlist))
|
||||
confGroup.GET("/invite", ui.AmWrap(InviteToConference))
|
||||
confGroup.GET("/r/:topic", ui.AmWrap(ReadPosts), ui.SetTopic)
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
{*
|
||||
* 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 Conference Membership:</h1>
|
||||
<span class="text-blue-800 text-2xl">{{ confName }}</span>
|
||||
</div>
|
||||
<hr class="border-2 border-gray-400 w-4/5 mt-2 mb-6">
|
||||
</div>
|
||||
|
||||
<!-- Upper Links -->
|
||||
<div class="mb-4">
|
||||
<a class="text-blue-700 hover:text-blue-900 text-sm font-medium" href="{{ selfLink }}">Redisplay Member List</a> |
|
||||
<a class="text-blue-700 hover:text-blue-900 text-sm font-medium" href="{{ backLink }}">Return to Manage Conference Menu</a>
|
||||
</div>
|
||||
|
||||
<!-- Member Find Form -->
|
||||
<div class="max-w-3xl mb-8">
|
||||
<form method="POST" action="{{ selfLink }}">
|
||||
<input type="hidden" name="mode" value="comm">
|
||||
<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 Members of Community "{{ commName }}":</h2>
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center gap-2 text-sm">
|
||||
<span class="text-black">Display all community members 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="flex justify-between items-center mb-4">
|
||||
<h2 class="text-xl font-bold text-black mb-4">
|
||||
{{ if mode == "conf" }}
|
||||
Conference Members:
|
||||
{{ else if mode == "comm" }}
|
||||
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="bg-gray-50 p-6 rounded-lg">
|
||||
<div class="space-y-4">
|
||||
{{ range _, rx := resultList }}
|
||||
<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 }}"
|
||||
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">
|
||||
{{ range _, role := roleList.Roles() }}
|
||||
<option value="{{ role.Level() }}" {{ if role.Level() == rx.Level }}selected{{ end }}>{{ role.Name() }}</option>
|
||||
{{ end }}
|
||||
</select>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
@@ -139,3 +139,19 @@ func WordRunLengthAfterPrefix(s string, nrunes int) (int, bool) {
|
||||
}
|
||||
return WordRunLength(s[ofs:])
|
||||
}
|
||||
|
||||
/* Map applies a transformation function on all elements of an array of one type, turning it into an
|
||||
* array of another type.
|
||||
* Parameters:
|
||||
* in - The input array to be transformed.
|
||||
* fn - The function to be executed on each element.
|
||||
* Returns:
|
||||
* The array of new elements.
|
||||
*/
|
||||
func Map[A, B any](in []A, fn func(A) B) []B {
|
||||
rc := make([]B, len(in))
|
||||
for i, v := range in {
|
||||
rc[i] = fn(v)
|
||||
}
|
||||
return rc
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user