added dialog for editing community profile

This commit is contained in:
2025-10-17 22:32:28 -06:00
parent 8e80176022
commit 0edba20d1d
10 changed files with 585 additions and 57 deletions
+74
View File
@@ -12,6 +12,7 @@ package main
import ( import (
"errors" "errors"
"fmt"
"net/http" "net/http"
"git.erbosoft.com/amy/amsterdam/database" "git.erbosoft.com/amy/amsterdam/database"
@@ -48,3 +49,76 @@ func CommunityAdminMenu(ctxt ui.AmContext) (string, any, error) {
ctxt.VarMap().Set("amsterdam_pageTitle", menu.Title+" - "+comm.Name) ctxt.VarMap().Set("amsterdam_pageTitle", menu.Title+" - "+comm.Name)
return "framed_template", "menu.jet", nil return "framed_template", "menu.jet", nil
} }
func setupCommunityProfileDialog(dlg *ui.Dialog, comm *database.Community) {
dlg.SetCommunity(comm)
if comm.IsAdmin {
dlg.Field("comtype").Disabled = true
dlg.Field("joinkey").Disabled = true
dlg.Field("membersonly").Disabled = true
dlg.Field("hidemode").Disabled = true
dlg.Field("read_lvl").Disabled = true
dlg.Field("write_lvl").Disabled = true
dlg.Field("create_lvl").Disabled = true
dlg.Field("delete_lvl").Disabled = true
dlg.Field("join_lvl").Disabled = true
}
}
func CommunityProfileForm(ctxt ui.AmContext) (string, any, error) {
err := ctxt.SetCommunityContext(ctxt.URLParam("cid"))
if err != nil {
ctxt.SetRC(http.StatusNotFound)
return ui.ErrorPage(ctxt, err)
}
comm := ctxt.CurrentCommunity()
if !comm.TestPermission("Community.Write", ctxt.EffectiveLevel()) {
ctxt.SetRC(http.StatusForbidden)
return ui.ErrorPage(ctxt, errors.New("you are not permitted to access this page"))
}
var ci *database.ContactInfo
ci, err = comm.ContactInfo()
if err != nil {
return ui.ErrorPage(ctxt, err)
}
flags, err := comm.Flags()
if err != nil {
return ui.ErrorPage(ctxt, err)
}
dlg, err := ui.AmLoadDialog("commprofile")
if err == nil {
setupCommunityProfileDialog(dlg, comm)
dlg.Field("cc").Value = fmt.Sprintf("%d", comm.Id)
dlg.Field("name").Value = comm.Name
dlg.Field("alias").Value = comm.Alias
dlg.Field("synopsis").SetVal(comm.Synopsis)
dlg.Field("rules").SetVal(comm.Rules)
dlg.Field("language").SetVal(comm.Language)
dlg.Field("url").SetVal(ci.URL)
// TODO: set logo URL
dlg.Field("company").SetVal(ci.Company)
dlg.Field("addr1").SetVal(ci.Addr1)
dlg.Field("addr2").SetVal(ci.Addr2)
dlg.Field("loc").SetVal(ci.Locality)
dlg.Field("reg").SetVal(ci.Region)
dlg.Field("pcode").SetVal(ci.PostalCode)
dlg.Field("country").SetVal(ci.Country)
if comm.Public() {
dlg.Field("comtype").Value = "0"
dlg.Field("joinkey").Value = ""
} else {
dlg.Field("comtype").Value = "1"
dlg.Field("joinkey").SetVal(comm.JoinKey)
}
dlg.Field("membersonly").SetChecked(comm.MembersOnly)
dlg.Field("hidemode").Value = comm.HideMode()
dlg.Field("read_lvl").Value = fmt.Sprintf("%d", comm.ReadLevel)
dlg.Field("write_lvl").Value = fmt.Sprintf("%d", comm.WriteLevel)
dlg.Field("create_lvl").Value = fmt.Sprintf("%d", comm.CreateLevel)
dlg.Field("delete_lvl").Value = fmt.Sprintf("%d", comm.DeleteLevel)
dlg.Field("join_lvl").Value = fmt.Sprintf("%d", comm.JoinLevel)
dlg.Field("pic_in_post").SetChecked(flags.Get(database.CommunityFlagPicturesInPosts))
return dlg.Render(ctxt)
}
return ui.ErrorPage(ctxt, err)
}
+139 -1
View File
@@ -47,14 +47,38 @@ type Community struct {
Rules *string `dd:"rules"` Rules *string `dd:"rules"`
JoinKey *string `db:"joinkey"` JoinKey *string `db:"joinkey"`
Alias string `db:"alias"` Alias string `db:"alias"`
flags *util.OptionSet
} }
// CommunityProperties represents a property entry for a community.
type CommunityProperties struct {
Cid int32 `db:"cid"`
Index int32 `db:"ndx"`
Data *string `db:"data"`
}
// Community property indexes defined.
const (
CommunityPropFlags = int32(0) // "flags" user property
)
// Flag values for community property index CommunityPropFlags defined.
const (
CommunityFlagPicturesInPosts = uint(0)
)
// communityCache is the cache for Community objects. // communityCache is the cache for Community objects.
var communityCache *lru.TwoQueueCache = nil var communityCache *lru.TwoQueueCache = nil
// getCommunityMutex is a mutex on AmGetCommunity. // getCommunityMutex is a mutex on AmGetCommunity.
var getCommunityMutex sync.Mutex var getCommunityMutex sync.Mutex
// communityPropCache is the cache for CommunityProperties objects.
var communityPropCache *lru.Cache = nil
// getCommunityPropMutex is a mutex on AmGetCommunityProperty.
var getCommunityPropMutex sync.Mutex
// memberCacheData caches membership information for communities. // memberCacheData caches membership information for communities.
type memberCacheData struct { type memberCacheData struct {
isMember bool isMember bool
@@ -76,7 +100,7 @@ func stuffMembership(cid int32, uid int32, member bool, locked bool, level uint1
memberMutex.Unlock() memberMutex.Unlock()
} }
// init initializes the community cache. // init initializes the caches.
func init() { func init() {
var err error var err error
communityCache, err = lru.New2Q(50) communityCache, err = lru.New2Q(50)
@@ -87,6 +111,10 @@ func init() {
if err != nil { if err != nil {
panic(err) panic(err)
} }
communityPropCache, err = lru.New(100)
if err != nil {
panic(err)
}
} }
// Public returns true if the community is public. // Public returns true if the community is public.
@@ -121,6 +149,16 @@ func (c *Community) LanguageTag() (*language.Tag, error) {
return &t, nil return &t, nil
} }
func (c *Community) HideMode() string {
if c.HideFromSearch {
return "BOTH"
} else if c.HideFromDirectory {
return "DIRECTORY"
} else {
return "NONE"
}
}
/* Membership returns the details of the specified user's membership in the community. /* Membership returns the details of the specified user's membership in the community.
* Parameters: * Parameters:
* u - The user to check the membership of. * u - The user to check the membership of.
@@ -200,6 +238,35 @@ func (c *Community) PermissionLevel(perm string) uint16 {
} }
} }
// GetFlags retrieves the flags from the properties.
func (c *Community) Flags() (*util.OptionSet, error) {
c.Mutex.Lock()
defer c.Mutex.Unlock()
if c.flags == nil {
s, err := AmGetCommunityProperty(c.Id, CommunityPropFlags)
if err != nil {
return nil, err
}
if s == nil {
return nil, fmt.Errorf("missing flags for community %d", c.Id)
}
c.flags = util.OptionSetFromString(*s)
}
return c.flags, nil
}
// SaveFlags writes the flags to the database and stores them.
func (c *Community) SaveFlags(f *util.OptionSet) error {
s := f.AsString()
c.Mutex.Lock()
defer c.Mutex.Unlock()
err := AmSetCommunityProperty(c.Id, CommunityPropFlags, &s)
if err == nil {
c.flags = f
}
return err
}
/* AmGetCommunity returns a reference to the specified community. /* AmGetCommunity returns a reference to the specified community.
* Parameters: * Parameters:
* id - The ID of the community. * id - The ID of the community.
@@ -342,3 +409,74 @@ func AmAutoJoinCommunities(user *User) error {
} }
return err return err
} }
// internalGetProp is a helper used by the property functions.
func internalGetCommProp(cid int32, ndx int32) (*CommunityProperties, error) {
var err error = nil
key := fmt.Sprintf("%d:%d", cid, ndx)
getCommunityPropMutex.Lock()
defer getCommunityPropMutex.Unlock()
rc, ok := communityPropCache.Get(key)
if !ok {
var dbdata []CommunityProperties
err = amdb.Select(&dbdata, "SELECT * from propcomm WHERE cid = ? AND ndx = ?", cid, ndx)
if err != nil {
return nil, err
}
if len(dbdata) == 0 {
return nil, nil
}
if len(dbdata) > 1 {
return nil, fmt.Errorf("AmGetCommunityProperty(%d): too many responses(%d)", cid, len(dbdata))
}
rc = &(dbdata[0])
communityPropCache.Add(key, rc)
}
return rc.(*CommunityProperties), nil
}
/* AmGetCommunityProperty retrieves the value of a user property.
* Parameters:
* cid - The ID of the community to get the property for.
* ndx - The index of the property to retrieve.
* Returns:
* Value of the property string.
* Standard Go error status.
*/
func AmGetCommunityProperty(cid int32, ndx int32) (*string, error) {
p, err := internalGetCommProp(cid, ndx)
if err != nil {
return nil, err
}
return p.Data, nil
}
/* AmSetCommunityProperty sets the value of a community property.
* Parameters:
* cid - The ID of the community to set the property for.
* ndx - The index of the property to set.
* val - The new value of the property.
* Returns:
* Standard Go error status.
*/
func AmSetCommunityProperty(cid int32, ndx int32, val *string) error {
p, err := internalGetCommProp(cid, ndx)
if err != nil {
return err
}
getCommunityPropMutex.Lock()
defer getCommunityPropMutex.Unlock()
if p != nil {
_, err = amdb.Exec("UPDATE propcomm SET data = ? WHERE cid = ? AND ndx = ?", val, cid, ndx)
if err == nil {
p.Data = val
}
} else {
prop := CommunityProperties{Cid: cid, Index: ndx, Data: val}
_, err := amdb.NamedExec("INSERT INTO propcomm (cid, ndx, data) VALUES(:cid, :ndx, :data)", prop)
if err == nil {
communityPropCache.Add(fmt.Sprintf("%d:%d", cid, ndx), prop)
}
}
return err
}
+6
View File
@@ -11,6 +11,7 @@ package database
import ( import (
_ "embed" _ "embed"
"fmt"
"strconv" "strconv"
"strings" "strings"
@@ -184,6 +185,7 @@ type Role interface {
ID() string ID() string
Name() string Name() string
Level() uint16 Level() uint16
LevelStr() string
} }
func (r *CfgRole) ID() string { func (r *CfgRole) ID() string {
@@ -198,6 +200,10 @@ func (r *CfgRole) Level() uint16 {
return r.level return r.level
} }
func (r *CfgRole) LevelStr() string {
return fmt.Sprintf("%d", r.level)
}
// RoleList defines a list of security roles. // RoleList defines a list of security roles.
type RoleList interface { type RoleList interface {
Roles() []Role Roles() []Role
-2
View File
@@ -184,5 +184,3 @@ permissions:
role: "Community.AnyAdmin" role: "Community.AnyAdmin"
- name: "Community.MassMail" - name: "Community.MassMail"
role: "Community.AnyAdmin" role: "Community.AnyAdmin"
- name: "Community.Destroy"
role: "Community.Host"
+1
View File
@@ -66,6 +66,7 @@ func setupEcho() *echo.Echo {
e.GET("/sysadmin", ui.AmWrap(SysAdminMenu)) e.GET("/sysadmin", ui.AmWrap(SysAdminMenu))
e.GET("/comm/:cid/profile", ui.AmWrap(ShowCommunity)) e.GET("/comm/:cid/profile", ui.AmWrap(ShowCommunity))
e.GET("/comm/:cid/admin", ui.AmWrap(CommunityAdminMenu)) e.GET("/comm/:cid/admin", ui.AmWrap(CommunityAdminMenu))
e.GET("/comm/:cid/admin/profile", ui.AmWrap(CommunityProfileForm))
return e return e
} }
+98 -24
View File
@@ -23,17 +23,26 @@ import (
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
// DialogItemChoice holds a dialog item choice (only needed in case of defined dropdowns)
type DialogItemChoice struct {
Id string `yaml:"id"`
Text string `yaml:"text"`
Default bool `yaml:"default,omitempty"`
}
// DialogItem holds the dialog item definition. // DialogItem holds the dialog item definition.
type DialogItem struct { type DialogItem struct {
Type string `yaml:"type"` Type string `yaml:"type"`
Name string `yaml:"name"` Name string `yaml:"name"`
Caption string `yaml:"caption,omitempty"` Caption string `yaml:"caption,omitempty"`
Subcaption string `yaml:"subcaption,omitempty"` Subcaption string `yaml:"subcaption,omitempty"`
Required bool `yaml:"required,omitempty"` Required bool `yaml:"required,omitempty"`
Size int `yaml:"size,omitempty"` Disabled bool `yaml:"disabled,omitempty"`
MaxLength int `yaml:"maxlength,omitempty"` Size int `yaml:"size,omitempty"`
Value string `yaml:"value,omitempty"` MaxLength int `yaml:"maxlength,omitempty"`
Param string `yaml:"param,omitempty"` Value string `yaml:"value,omitempty"`
Param string `yaml:"param,omitempty"`
Choices []DialogItemChoice `yaml:"choices,omitempty"`
AuxData any AuxData any
} }
@@ -44,6 +53,7 @@ type Dialog struct {
Options string `yaml:"options,omitempty"` Options string `yaml:"options,omitempty"`
MenuSelector string `yaml:"menuSelector,omitempty"` MenuSelector string `yaml:"menuSelector,omitempty"`
Title string `yaml:"title"` Title string `yaml:"title"`
Subtitle string `yaml:"subtitle,omitempty"`
Action string `yaml:"action"` Action string `yaml:"action"`
Instructions string `yaml:"instructions,omitempty"` Instructions string `yaml:"instructions,omitempty"`
Fields []DialogItem `yaml:"fields"` Fields []DialogItem `yaml:"fields"`
@@ -82,6 +92,9 @@ func AmLoadDialog(name string) (*Dialog, error) {
if fld.Type == "date" && fld.Param == "" { if fld.Type == "date" && fld.Param == "" {
d.Fields[i].Param = "year:-100" d.Fields[i].Param = "year:-100"
} }
if fld.Type == "dropdown" && len(fld.Choices) == 0 {
return nil, fmt.Errorf("dropdown field %s in dialog %s has no choices", fld.Name, name)
}
} }
return &d, nil return &d, nil
} }
@@ -184,11 +197,29 @@ func (fld *DialogItem) SetVal(p *string) {
} }
} }
// SetInt sets the value of a field to an integer.
func (fld *DialogItem) SetInt(v int) {
fld.Value = fmt.Sprintf("%d", v)
}
// IsEmpty returns true if the field is empty. // IsEmpty returns true if the field is empty.
func (fld *DialogItem) IsEmpty() bool { func (fld *DialogItem) IsEmpty() bool {
return len(fld.Value) == 0 return len(fld.Value) == 0
} }
// SetCommunity alters a dialog's content to reflect the community.
func (d *Dialog) SetCommunity(comm *database.Community) {
d.Title = strings.ReplaceAll(d.Title, "[CNAME]", comm.Name)
d.Subtitle = strings.ReplaceAll(d.Subtitle, "[CNAME]", comm.Name)
d.Action = strings.ReplaceAll(d.Action, "[CID]", comm.Alias)
for i, fld := range d.Fields {
switch fld.Type {
case "userphoto", "communitylogo":
d.Fields[i].Param = strings.ReplaceAll(fld.Param, "[CID]", comm.Alias)
}
}
}
/* Field returns a pointer to a dialog's field, given its name. /* Field returns a pointer to a dialog's field, given its name.
* Parameters: * Parameters:
* name - The name of the field to find. * name - The name of the field to find.
@@ -224,6 +255,46 @@ func (d *Dialog) Render(ctxt AmContext) (string, any, error) {
if d.Fields[i].Value == "" { if d.Fields[i].Value == "" {
d.Fields[i].Value = config.GlobalConfig.Defaults.TimeZone d.Fields[i].Value = config.GlobalConfig.Defaults.TimeZone
} }
case "dropdown":
defv := ""
for _, ch := range fld.Choices {
if ch.Default {
defv = ch.Id
break
}
}
if d.Fields[i].Value == "" {
d.Fields[i].Value = defv
} else {
ok := false
for _, ch := range fld.Choices {
if d.Fields[i].Value == ch.Id {
ok = true
break
}
}
if !ok {
d.Fields[i].Value = defv
}
}
case "rolelist":
rl := database.AmRoleList(fld.Param)
defv := rl.Default().LevelStr()
if d.Fields[i].Value == "" {
d.Fields[i].Value = defv
} else {
ok := false
for _, r := range rl.Roles() {
if d.Fields[i].Value == r.LevelStr() {
ok = true
break
}
if !ok {
d.Fields[i].Value = defv
}
}
}
// TODO: want to do something like dropdown but not sure what yet
} }
} }
if d.MenuSelector != "" && d.MenuSelector != "nochange" { if d.MenuSelector != "" && d.MenuSelector != "nochange" {
@@ -312,7 +383,7 @@ func (d *Dialog) LoadFromForm(ctxt AmContext) {
} }
} }
d.Fields[i].AuxData = dvals d.Fields[i].AuxData = dvals
case "userphoto": case "userphoto", "communitylogo":
d.Fields[i].Value = ctxt.FormField(fmt.Sprintf("%s_data", fld.Name)) d.Fields[i].Value = ctxt.FormField(fmt.Sprintf("%s_data", fld.Name))
default: default:
d.Fields[i].Value = ctxt.FormField(fld.Name) d.Fields[i].Value = ctxt.FormField(fld.Name)
@@ -468,20 +539,23 @@ func validateDateField(fld *DialogItem) error {
// validators maps the field types to validator functions. // validators maps the field types to validator functions.
var validators = map[string]validatorFunc{ var validators = map[string]validatorFunc{
"ams_id": validateAmsIdField, "ams_id": validateAmsIdField,
"button": nilValidator, "button": nilValidator,
"checkbox": nilValidator, "checkbox": nilValidator,
"countrylist": validateCountryField, "communitylogo": nilValidator,
"date": validateDateField, "countrylist": validateCountryField,
"email": validateEmailField, "date": validateDateField,
"header": nilValidator, "dropdown": nilValidator,
"hidden": nilValidator, "email": validateEmailField,
"integer": validateIntegerField, "header": nilValidator,
"localelist": nilValidator, // TODO "hidden": nilValidator,
"password": validateTextField, "integer": validateIntegerField,
"text": validateTextField, "localelist": nilValidator,
"tzlist": nilValidator, // TODO "password": validateTextField,
"userphoto": nilValidator, "rolelist": nilValidator,
"text": validateTextField,
"tzlist": nilValidator,
"userphoto": nilValidator,
} }
/* Validate validates the values in the dialog. /* Validate validates the values in the dialog.
+172
View File
@@ -0,0 +1,172 @@
#
# Amsterdam Web Communities System
# Copyright (c) 2025 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/.
#
name: "community.profile"
formName: "comprofform"
menuSelector: "community"
title: "Edit Community Profile:"
subtitle: "[CNAME]"
action: "/comm/[CID]/admin/profile"
fields:
- type: "hidden"
name: "cc"
value: ""
- type: "header"
name: "header1"
caption: "Basic Information"
- type: "text"
name: "name"
caption: "Community Name"
required: true
size: 32
maxLength: 128
- type: "ams_id"
name: "alias"
caption: "Community Alias"
required: true
size: 32
maxlength: 32
- type: "text"
name: "synopsis"
caption: "Synopsis"
size: 32
maxLength: 255
- type: "text"
name: "rules"
caption: "Rules"
size: 32
maxLength: 255
- type: "localelist"
name: "language"
caption: "Primary Language"
required: true
- type: "text"
name: "url"
caption: "Home Page"
subcaption: "(URL)"
size: 32
maxlength: 255
- type: "communitylogo"
name: "logo"
caption: "Community logo"
param: "/TODO/comm/[CID]/admin/logo"
- type: "header"
name: "header2"
caption: "Location"
- type: "text"
name: "company"
caption: "Company"
size: 32
maxlength: 255
- type: "text"
name: "addr1"
caption: "Address"
size: 32
maxlength: 255
- type: "text"
name: "addr2"
caption: "Address"
subcaption: "(line 2)"
size: 32
maxlength: 255
- type: "text"
name: "loc"
caption: "City"
required: true
size: 32
maxlength: 64
- type: "text"
name: "reg"
caption: "State/Province"
required: true
size: 32
maxlength: 64
- type: "text"
name: "pcode"
caption: "Zip/Postal Code"
required: true
size: 32
maxlength: 64
- type: "countrylist"
name: "country"
caption: "Country"
required: true
- type: "header"
name: "header3"
caption: "Security"
- type: "dropdown"
name: "comtype"
caption: "Community type"
required: true
choices:
- id: "0"
text: "Public"
default: true
- id: "1"
text: "Private"
- type: "text"
name: "joinkey"
caption: "Join Key"
subcaption: "(for private communities)"
size: 32
maxLength: 64
- type: "checkbox"
name: "membersonly"
caption: "Allow only members to access this community"
- type: "dropdown"
name: "hidemode"
caption: "Community visibility"
required: true
choices:
- id: "NONE"
text: "Show in both directory and search"
default: true
- id: "DIRECTORY"
text: "Hide in directory, but not in search"
- id: "BOTH"
text: "Hide in both directory and search"
- type: "rolelist"
name: "read_lvl"
caption: "Security level required to read contents"
required: true
param: "Community.Read"
- type: "rolelist"
name: "write_lvl"
caption: "Security level required to update profile"
required: true
param: "Community.Write"
- type: "rolelist"
name: "create_lvl"
caption: "Security level required to create new subobjects"
required: true
param: "Community.Create"
- type: "rolelist"
name: "delete_lvl"
caption: "Security level required to delete community"
required: true
param: "Community.Delete"
- type: "rolelist"
name: "join_lvl"
caption: "Security level required to join community"
required: true
param: "Community.Join"
- type: "header"
name: "header4"
caption: "Conferencing Options"
- type: "checkbox"
name: "pic_in_post"
caption: "Display user pictures next to posts in conferences"
subcaption: "(by default; user can override)"
- type: "button"
name: "update"
caption: "Update"
param: "blue"
- type: "button"
name: "cancel"
caption: "Cancel"
param: "red"
+5 -5
View File
@@ -61,15 +61,15 @@ menudefs:
community. Use with care and review all changes before applying them to the community. community. Use with care and review all changes before applying them to the community.
items: items:
- text: "Community Profile" - text: "Community Profile"
link: "/TODO/comm/[CID]/admin/profile" link: "/comm/[CID]/admin/profile"
permission: "Community.ShowAdmin" permission: "Community.Write"
- text: "Set Community Category" - text: "Set Community Category"
link: "/TODO/comm/[CID]/admin/category" link: "/TODO/comm/[CID]/admin/category"
permission: "Community.ShowAdmin" permission: "Community.Write"
ifdef: "USECAT" ifdef: "USECAT"
- text: "Set Community Services" - text: "Set Community Services"
link: "/TODO/comm/[CID]/admin/services" link: "/TODO/comm/[CID]/admin/services"
permission: "Community.ShowAdmin" permission: "Community.Write"
disabled: true disabled: true
- text: "Membership Control" - text: "Membership Control"
link: "/TODO/comm/[CID]/admin/members" link: "/TODO/comm/[CID]/admin/members"
@@ -82,5 +82,5 @@ menudefs:
permission: "Community.ShowAdmin" permission: "Community.ShowAdmin"
- text: "Delete Community" - text: "Delete Community"
link: "/TODO/comm/[CID]/admin/delete" link: "/TODO/comm/[CID]/admin/delete"
permission: "Community.Destroy" permission: "Community.Delete"
hazard: true hazard: true
+5
View File
@@ -23,6 +23,7 @@ import (
"time" "time"
"git.erbosoft.com/amy/amsterdam/config" "git.erbosoft.com/amy/amsterdam/config"
"git.erbosoft.com/amy/amsterdam/database"
"git.erbosoft.com/amy/amsterdam/util" "git.erbosoft.com/amy/amsterdam/util"
"github.com/CloudyKit/jet/v6" "github.com/CloudyKit/jet/v6"
"github.com/CloudyKit/jet/v6/loaders/embedfs" "github.com/CloudyKit/jet/v6/loaders/embedfs"
@@ -235,6 +236,10 @@ func SetupTemplates() {
s := a.Get(0).Convert(reflect.TypeFor[string]()).String() s := a.Get(0).Convert(reflect.TypeFor[string]()).String()
return reflect.ValueOf(AmMenu(s)) return reflect.ValueOf(AmMenu(s))
}) })
views.AddGlobalFunc("AmRoleList", func(a jet.Arguments) reflect.Value {
s := a.Get(0).Convert(reflect.TypeFor[string]()).String()
return reflect.ValueOf(database.AmRoleList(s))
})
views.AddGlobalFunc("CapitalizeString", func(a jet.Arguments) reflect.Value { views.AddGlobalFunc("CapitalizeString", func(a jet.Arguments) reflect.Value {
s := a.Get(0).Convert(reflect.TypeFor[string]()).String() s := a.Get(0).Convert(reflect.TypeFor[string]()).String()
return reflect.ValueOf(util.CapitalizeString(s)) return reflect.ValueOf(util.CapitalizeString(s))
+85 -25
View File
@@ -10,6 +10,9 @@
<div class="p-4"> <div class="p-4">
<div class="mb-6"> <div class="mb-6">
<h1 class="text-blue-800 text-4xl font-bold mb-2">{{ amsterdam_dialog.Title }}</h1> <h1 class="text-blue-800 text-4xl font-bold mb-2">{{ amsterdam_dialog.Title }}</h1>
{{ if amsterdam_dialog.Subtitle != "" }}
<span class="text-blue-800 text-2xl font-bold ml-2">{{ amsterdam_dialog.Subtitle }}</span>
{{ end }}
<hr class="border-2 border-gray-400 w-4/5 mb-4"> <hr class="border-2 border-gray-400 w-4/5 mb-4">
</div> </div>
<form name="{{ amsterdam_dialog.FormName }}" method="POST" action="{{ amsterdam_dialog.Action }}" class="max-w-2xl"> <form name="{{ amsterdam_dialog.FormName }}" method="POST" action="{{ amsterdam_dialog.Action }}" class="max-w-2xl">
@@ -58,26 +61,28 @@
{{ range amsterdam_dialog.Fields }} {{ range amsterdam_dialog.Fields }}
{{ if .Type == "text" || .Type == "ams_id" || .Type == "email" }} {{ if .Type == "text" || .Type == "ams_id" || .Type == "email" }}
<div class="flex items-center"> <div class="flex items-center">
<label for="{{ .Name }}" class="w-64 text-right pr-4 text-black text-sm"> <label for="{{ .Name }}"
class="w-64 text-right pr-4 {{ if .Disabled }}text-gray-400{{ else }}text-black{{ end }} text-sm">
{{ .Caption }}{{ if .Subcaption != "" }} {{ .Subcaption }}{{ end }}: {{ .Caption }}{{ if .Subcaption != "" }} {{ .Subcaption }}{{ end }}:
{{ if .Required }}<span class="text-red-600">*</span>{{ end }} {{ if .Required }}<span class="text-red-600">*</span>{{ end }}
</label> </label>
<input type="text" id="{{ .Name }}" name="{{ .Name }}" <input type="text" id="{{ .Name }}" name="{{ .Name }}"
{{ if .Size > 0 }}size="{{ .Size }}"{{ end }} {{ if .Size > 0 }}size="{{ .Size }}"{{ end }}
{{ if .MaxLength > 0 }}maxlength="{{ .MaxLength }}"{{ end }} {{ if .MaxLength > 0 }}maxlength="{{ .MaxLength }}"{{ end }}
value="{{ .Value }}" value="{{ .Value }}" {{ if .Disabled }}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" /> 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" />
</div> </div>
{{ else if .Type == "integer" }} {{ else if .Type == "integer" }}
<div class="flex items-center"> <div class="flex items-center">
<label for="{{ .Name }}" class="w-64 text-right pr-4 text-black text-sm"> <label for="{{ .Name }}"
class="w-64 text-right pr-4 {{ if .Disabled }}text-gray-400{{ else }}text-black{{ end }} text-sm">
{{ .Caption }}{{ if .Subcaption != "" }} {{ .Subcaption }}{{ end }}: {{ .Caption }}{{ if .Subcaption != "" }} {{ .Subcaption }}{{ end }}:
{{ if .Required }}<span class="text-red-600">*</span>{{ end }} {{ if .Required }}<span class="text-red-600">*</span>{{ end }}
</label> </label>
<input type="text" id="{{ .Name }}" name="{{ .Name }}" <input type="text" id="{{ .Name }}" name="{{ .Name }}"
{{ if .Size > 0 }}size="{{ .Size }}"{{ end }} {{ if .Size > 0 }}size="{{ .Size }}"{{ end }}
{{ if .MaxLength > 0 }}maxlength="{{ .MaxLength }}"{{ end }} {{ if .MaxLength > 0 }}maxlength="{{ .MaxLength }}"{{ end }}
value="{{ .Value }}" value="{{ .Value }}" {{ if .Disabled }}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" /> 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" />
{{ vr := .ValueRange() }} {{ vr := .ValueRange() }}
{{ if vr.Low != -1 && vr.High != -1 }} {{ if vr.Low != -1 && vr.High != -1 }}
@@ -86,36 +91,69 @@
</div> </div>
{{ else if .Type == "password" }} {{ else if .Type == "password" }}
<div class="flex items-center"> <div class="flex items-center">
<label for="{{ .Name }}" class="w-64 text-right pr-4 text-black text-sm"> <label for="{{ .Name }}"
class="w-64 text-right pr-4 {{ if .Disabled }}text-gray-400{{ else }}text-black{{ end }} text-sm">
{{ .Caption }}{{ if .Subcaption != "" }} {{ .Subcaption }}{{ end }}: {{ .Caption }}{{ if .Subcaption != "" }} {{ .Subcaption }}{{ end }}:
{{ if .Required }}<span class="text-red-600">*</span>{{ end }} {{ if .Required }}<span class="text-red-600">*</span>{{ end }}
</label> </label>
<input type="password" id="{{ .Name }}" name="{{ .Name }}" <input type="password" id="{{ .Name }}" name="{{ .Name }}"
{{ if .Size > 0 }}size="{{ .Size }}"{{ end }} {{ if .Size > 0 }}size="{{ .Size }}"{{ end }}
{{ if .MaxLength > 0 }}maxlength="{{ .MaxLength }}"{{ end }} {{ if .MaxLength > 0 }}maxlength="{{ .MaxLength }}"{{ end }}
value="{{ .Value }}" value="{{ .Value }}" {{ if .Disabled }}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" /> 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" />
</div> </div>
{{ else if .Type == "checkbox" }} {{ else if .Type == "checkbox" }}
<div class="flex items-center"> <div class="flex items-center">
<div class="w-24 text-right pr-4"> <div class="w-24 text-right pr-4">
<input type="checkbox" id="{{ .Name }}" name="{{ .Name }}" <input type="checkbox" id="{{ .Name }}" name="{{ .Name }}"
value="Y" {{ if .Value != "" }}checked{{ end }} value="Y" {{ if .Value != "" }}checked{{ end }} {{ if .Disabled }}disabled{{ end }}
class="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500" /> class="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500" />
</div> </div>
<label for="{{ .Name }}" class="flex-1 text-black text-sm"> <label for="{{ .Name }}" class="flex-1 {{ if .Disabled }}text-gray-400{{ else }}text-black{{ end }} text-sm">
{{ .Caption }}{{ if .Subcaption != "" }} {{ .Subcaption }}{{ end }} {{ .Caption }}{{ if .Subcaption != "" }} {{ .Subcaption }}{{ end }}
{{ if .Required }}<span class="text-red-600">*</span>{{ end }} {{ if .Required }}<span class="text-red-600">*</span>{{ end }}
</label> </label>
</div> </div>
{{ else if .Type == "countrylist" }} {{ else if .Type == "dropdown" }}
<div class="flex items-center"> <div class="flex items-center">
<label for="{{ .Name }}" class="w-64 text-right pr-4 text-black text-sm"> <label for="{{ .Name }}"
class="w-64 text-right pr-4 {{ if .Disabled }}text-gray-400{{ else }}text-black{{ end }} text-sm">
{{ .Caption }}{{ if .Subcaption != "" }} {{ .Subcaption }}{{ end }} {{ .Caption }}{{ if .Subcaption != "" }} {{ .Subcaption }}{{ end }}
{{ if .Required }}<span class="text-red-600">*</span>{{ end }} {{ if .Required }}<span class="text-red-600">*</span>{{ end }}
</label> </label>
{{ v := .Value }} {{ v := .Value }}
<select id="{{ .Name }}" name="{{ .Name }}" {{ if .Required }}required{{ end }} <select id="{{ .Name }}" name="{{ .Name }}" {{ if .Required }}required{{ end }} {{ if .Disabled }}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">
{{ range .Choices }}
<option value="{{ .Id }}" {{ if .Id == v }}selected{{ end }}>{{ .Text }}</option>
{{ end }}
</select>
</div>
{{ else if .Type == "rolelist" }}
<div class="flex items-center">
<label for="{{ .Name }}"
class="w-64 text-right pr-4 {{ if .Disabled }}text-gray-400{{ else }}text-black{{ end }} text-sm">
{{ .Caption }}{{ if .Subcaption != "" }} {{ .Subcaption }}{{ end }}
{{ if .Required }}<span class="text-red-600">*</span>{{ end }}
</label>
{{ v := .Value }}
{{ rl := AmRoleList(.Param) }}
<select id="{{ .Name }}" name="{{ .Name }}" {{ if .Required }}required{{ end }} {{ if .Disabled }}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">
{{ range rl.Roles() }}
<option value="{{ .LevelStr() }}" {{ if .LevelStr() == v }}selected{{ end }}>{{ .Name() }}</option>
{{ end }}
</select>
</div>
{{ else if .Type == "countrylist" }}
<div class="flex items-center">
<label for="{{ .Name }}"
class="w-64 text-right pr-4 {{ if .Disabled }}text-gray-400{{ else }}text-black{{ end }} text-sm">
{{ .Caption }}{{ if .Subcaption != "" }} {{ .Subcaption }}{{ end }}
{{ if .Required }}<span class="text-red-600">*</span>{{ end }}
</label>
{{ v := .Value }}
<select id="{{ .Name }}" name="{{ .Name }}" {{ if .Required }}required{{ end }} {{ if .Disabled }}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"> 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">
<option value="XX" {{ if v == "XX" }}selected{{ end }}>🏳️ (unknown)</option> <option value="XX" {{ if v == "XX" }}selected{{ end }}>🏳️ (unknown)</option>
{{ range GetCountryList() }} {{ range GetCountryList() }}
@@ -126,12 +164,13 @@
</div> </div>
{{ else if .Type == "localelist" }} {{ else if .Type == "localelist" }}
<div class="flex items-center"> <div class="flex items-center">
<label for="{{ .Name }}" class="w-64 text-right pr-4 text-black text-sm"> <label for="{{ .Name }}"
class="w-64 text-right pr-4 {{ if .Disabled }}text-gray-400{{ else }}text-black{{ end }} text-sm">
{{ .Caption }}{{ if .Subcaption != "" }} {{ .Subcaption }}{{ end }} {{ .Caption }}{{ if .Subcaption != "" }} {{ .Subcaption }}{{ end }}
{{ if .Required }}<span class="text-red-600">*</span>{{ end }} {{ if .Required }}<span class="text-red-600">*</span>{{ end }}
</label> </label>
{{ v := .Value }} {{ v := .Value }}
<select id="{{ .Name }}" name="{{ .Name }}" {{ if .Required }}required{{ end }} <select id="{{ .Name }}" name="{{ .Name }}" {{ if .Required }}required{{ end }} {{ if .Disabled }}disabled{{ end }}
class="flex-1 max-w-md px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"> class="flex-1 max-w-md px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
{{ range GetLanguageList() }} {{ range GetLanguageList() }}
<option value="{{ .Tag }}" {{ if .Tag == v }}selected{{ end }}>{{ .Name }}</option> <option value="{{ .Tag }}" {{ if .Tag == v }}selected{{ end }}>{{ .Name }}</option>
@@ -140,12 +179,13 @@
</div> </div>
{{ else if .Type == "tzlist" }} {{ else if .Type == "tzlist" }}
<div class="flex items-center"> <div class="flex items-center">
<label for="{{ .Name }}" class="w-64 text-right pr-4 text-black text-sm"> <label for="{{ .Name }}"
class="w-64 text-right pr-4 {{ if .Disabled }}text-gray-400{{ else }}text-black{{ end }} text-sm">
{{ .Caption }}{{ if .Subcaption != "" }} {{ .Subcaption }}{{ end }} {{ .Caption }}{{ if .Subcaption != "" }} {{ .Subcaption }}{{ end }}
{{ if .Required }}<span class="text-red-600">*</span>{{ end }} {{ if .Required }}<span class="text-red-600">*</span>{{ end }}
</label> </label>
{{ v := .Value }} {{ v := .Value }}
<select id="{{ .Name }}" name="{{ .Name }}" {{ if .Required }}required{{ end }} <select id="{{ .Name }}" name="{{ .Name }}" {{ if .Required }}required{{ end }} {{ if .Disabled }}disabled{{ end }}
class="flex-1 max-w-md px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"> class="flex-1 max-w-md px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
{{ range GetTimeZoneList() }} {{ range GetTimeZoneList() }}
<option value="{{ . }}" {{ if v == . }}selected{{ end }}>{{ . }}</option> <option value="{{ . }}" {{ if v == . }}selected{{ end }}>{{ . }}</option>
@@ -155,25 +195,27 @@
{{ else if .Type == "date" }} {{ else if .Type == "date" }}
{{ dv := .DateValues() }} {{ dv := .DateValues() }}
<div class="flex items-center"> <div class="flex items-center">
<label class="w-64 text-right pr-4 text-black text-sm"> <label class="w-64 text-right pr-4 {{ if .Disabled }}text-gray-400{{ else }}text-black{{ end }} text-sm">
{{ .Caption }}{{ if .Subcaption != "" }} {{ .Subcaption }}{{ end }} {{ .Caption }}{{ if .Subcaption != "" }} {{ .Subcaption }}{{ end }}
{{ if .Required }}<span class="text-red-600">*</span>{{ end }} {{ if .Required }}<span class="text-red-600">*</span>{{ end }}
</label> </label>
<div class="flex gap-2"> <div class="flex gap-2">
<select name="{{ .Name }}_month" <select name="{{ .Name }}_month" {{ if .Disabled }}disabled{{ end }}
class="px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"> 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="-1" {{ if dv[0] == -1 }}selected{{ end }}>---</option> <option value="-1" {{ if dv[0] == -1 }}selected{{ end }}>---</option>
{{ range i := GetMonthList() }} {{ range i := GetMonthList() }}
<option value="{{ i + 1 }}" {{ if dv[0] == i + 1 }}selected{{ end }}>{{ . }}</option> <option value="{{ i + 1 }}" {{ if dv[0] == i + 1 }}selected{{ end }}>{{ . }}</option>
{{ end }} {{ end }}
</select> </select>
<select name="{{ .Name }}_day" class="px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"> <select name="{{ .Name }}_day" {{ if .Disabled }}disabled{{ end }}
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="-1" {{ if dv[1] == -1 }}selected{{ end }}>---</option> <option value="-1" {{ if dv[1] == -1 }}selected{{ end }}>---</option>
{{ range MakeIntRange(1, 32, 1) }} {{ range MakeIntRange(1, 32, 1) }}
<option value="{{ . }}" {{ if dv[1] == . }}selected{{ end }}>{{ . }}</option> <option value="{{ . }}" {{ if dv[1] == . }}selected{{ end }}>{{ . }}</option>
{{ end }} {{ end }}
</select> </select>
<select name="{{ .Name }}_year" class="px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"> <select name="{{ .Name }}_year" {{ if .Disabled }}disabled{{ end }}
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="-1" {{ if dv[2] == -1 }}selected{{ end }}>---</option> <option value="-1" {{ if dv[2] == -1 }}selected{{ end }}>---</option>
{{ range MakeYearRange(.Param) }} {{ range MakeYearRange(.Param) }}
<option value="{{ . }}" {{ if dv[2] == . }}selected{{ end }}>{{ . }}</option> <option value="{{ . }}" {{ if dv[2] == . }}selected{{ end }}>{{ . }}</option>
@@ -183,13 +225,31 @@
</div> </div>
{{ else if .Type == "userphoto" }} {{ else if .Type == "userphoto" }}
<div class="flex items-start"> <div class="flex items-start">
<label class="w-64 text-right pr-4 text-black text-sm pt-2">{{ .Caption }} <label class="w-64 text-right pr-4 {{ if .Disabled }}text-gray-400{{ else }}text-black{{ end }} text-sm pt-2">{{ .Caption }}
{{ if .Subcaption != "" }} {{ .Subcaption }}{{ end }} (click to change):</label> {{ if .Subcaption != "" }} {{ .Subcaption }}{{ end }} (click to change):</label>
<input type="hidden" name="{{ .Name }}_data" value="{{ .Value }}"/> <input type="hidden" name="{{ .Name }}_data" value="{{ .Value }}"/>
<a href="/profile_photo?tgt={{ target | url }}" {{ if .Disabled }}
class="border-2 border-gray-300 rounded hover:border-blue-500 transition-colors"> <img src="{{ .Value }}" class="w-25 h-25">
<img src="{{ .Value }}" alt="Click to upload photo" class="w-25 h-25"> {{ else }}
</a> <a href="/profile_photo?tgt={{ target | url }}"
class="border-2 border-gray-300 rounded hover:border-blue-500 transition-colors">
<img src="{{ .Value }}" alt="Click to upload photo" class="w-25 h-25">
</a>
{{ end }}
</div>
{{ else if .Type == "communitylogo" }}
<div class="flex items-start">
<label class="w-64 text-right pr-4 {{ if .Disabled }}text-gray-400{{ else }}text-black{{ end }} text-sm pt-2">{{ .Caption }}
{{ if .Subcaption != "" }} {{ .Subcaption }}{{ end }} (click to change):</label>
<input type="hidden" name="{{ .Name }}_data" value="{{ .Value }}"/>
{{ if .Disabled }}
<img src="/img/builtin/default-community.jpg" class="w-28 h-16 rounded">
{{ else }}
<a href="{{ .Param }}"
class="border-2 border-gray-300 rounded hover:border-blue-500 transition-colors">
<img src="/img/builtin/default-community.jpg" alt="Click to upload logo" class="w-28 h-16 rounded">
</a>
{{ end }}
</div> </div>
{{ else if .Type == "header" }} {{ else if .Type == "header" }}
<h2 class="text-lg font-bold text-black mb-4">{{ .Caption }}</h2> <h2 class="text-lg font-bold text-black mb-4">{{ .Caption }}</h2>
@@ -202,7 +262,7 @@
{{ range amsterdam_dialog.Fields }} {{ range amsterdam_dialog.Fields }}
{{ if .Type == "button" }} {{ if .Type == "button" }}
{{ clstmp := "bg-" + .Param + "-600 hover:bg-" + .Param + "-700" }} {{ clstmp := "bg-" + .Param + "-600 hover:bg-" + .Param + "-700" }}
<button type="submit" name="{{ .Name }}" <button type="submit" name="{{ .Name }}" {{ if .Disabled }}disabled{{ end }}
class="{{ clstmp }} text-white px-6 py-2 rounded font-medium transition-colors">{{ .Caption }}</button> class="{{ clstmp }} text-white px-6 py-2 rounded font-medium transition-colors">{{ .Caption }}</button>
{{ end }} {{ end }}
{{ end }} {{ end }}