feat(groups): app permission checkboxes in create group form

This commit is contained in:
Claus Lohmar 2026-06-15 05:39:32 +00:00
parent 9df43d8423
commit 46474981f0
7 changed files with 84 additions and 9 deletions

View file

@ -10,6 +10,7 @@ type Group struct {
ID int `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Apps string `json:"apps"`
UserCount int `json:"user_count"`
}
@ -24,7 +25,7 @@ func NewGroupStore(db *sql.DB) *GroupStore {
func (s *GroupStore) List() ([]Group, error) {
rows, err := s.db.Query(`
SELECT g.id, g.name, g.description,
SELECT g.id, g.name, g.description, g.apps,
(SELECT COUNT(*) FROM users WHERE ',' || groups || ',' LIKE '%,' || g.name || ',%') as user_count
FROM groups g ORDER BY g.name
`)
@ -36,7 +37,7 @@ func (s *GroupStore) List() ([]Group, error) {
var groups []Group
for rows.Next() {
var g Group
if err := rows.Scan(&g.ID, &g.Name, &g.Description, &g.UserCount); err != nil {
if err := rows.Scan(&g.ID, &g.Name, &g.Description, &g.Apps, &g.UserCount); err != nil {
return nil, err
}
groups = append(groups, g)
@ -46,7 +47,7 @@ func (s *GroupStore) List() ([]Group, error) {
func (s *GroupStore) GetByName(name string) (*Group, error) {
var g Group
err := s.db.QueryRow(`SELECT id, name, description FROM groups WHERE name = ?`, name).Scan(&g.ID, &g.Name, &g.Description)
err := s.db.QueryRow(`SELECT id, name, description, apps FROM groups WHERE name = ?`, name).Scan(&g.ID, &g.Name, &g.Description, &g.Apps)
if err == sql.ErrNoRows {
return nil, nil
}
@ -56,8 +57,8 @@ func (s *GroupStore) GetByName(name string) (*Group, error) {
return &g, nil
}
func (s *GroupStore) Create(name, description string) error {
_, err := s.db.Exec(`INSERT INTO groups (name, description) VALUES (?, ?)`, name, description)
func (s *GroupStore) Create(name, description, apps string) error {
_, err := s.db.Exec(`INSERT INTO groups (name, description, apps) VALUES (?, ?, ?)`, name, description, apps)
if err != nil {
return fmt.Errorf("create group %s: %w", name, err)
}

View file

@ -69,11 +69,27 @@ templ CreateGroupForm() {
<label>Description</label>
<input type="text" name="description" class="input" placeholder="Team description"/>
</div>
<div class="form-group">
<label>Permitted Apps</label>
<div class="checkbox-list" id="group-apps">
for _, a := range allApps() {
<label class="checkbox-item">
<input type="checkbox" name="app" value={ a }/>
<span>{ a }</span>
</label>
}
</div>
</div>
<button type="submit" class="btn" style="width:100%;margin-top:8px">Create Group</button>
</form>
</div>
}
func allApps() []string {
return []string{"Admin Panel", "Files", "Mail", "Calendar", "Office", "Contacts", "Tasks", "Chat"}
}
type GroupRow struct {
Name string
UserCount int

View file

@ -192,7 +192,43 @@ func CreateGroupForm() templ.Component {
templ_7745c5c3_Var10 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<div class=\"detail-card\"><div class=\"section-header\"><h3>New Group</h3><button class=\"btn-sm\" hx-get=\"/admin/groups/cancel-form\" hx-target=\"#group-detail\" hx-swap=\"innerHTML\">Cancel</button></div><form hx-post=\"/admin/groups\" hx-target=\"#group-list-body\" hx-swap=\"innerHTML\" hx-on::after-request=\"htmx.trigger('#group-detail', 'click')\"><div class=\"form-group\"><label>Group Name *</label> <input type=\"text\" name=\"name\" class=\"input\" required placeholder=\"e.g. engineering\"></div><div class=\"form-group\"><label>Description</label> <input type=\"text\" name=\"description\" class=\"input\" placeholder=\"Team description\"></div><button type=\"submit\" class=\"btn\" style=\"width:100%;margin-top:8px\">Create Group</button></form></div>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<div class=\"detail-card\"><div class=\"section-header\"><h3>New Group</h3><button class=\"btn-sm\" hx-get=\"/admin/groups/cancel-form\" hx-target=\"#group-detail\" hx-swap=\"innerHTML\">Cancel</button></div><form hx-post=\"/admin/groups\" hx-target=\"#group-list-body\" hx-swap=\"innerHTML\" hx-on::after-request=\"htmx.trigger('#group-detail', 'click')\"><div class=\"form-group\"><label>Group Name *</label> <input type=\"text\" name=\"name\" class=\"input\" required placeholder=\"e.g. engineering\"></div><div class=\"form-group\"><label>Description</label> <input type=\"text\" name=\"description\" class=\"input\" placeholder=\"Team description\"></div><div class=\"form-group\"><label>Permitted Apps</label><div class=\"checkbox-list\" id=\"group-apps\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, a := range allApps() {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<label class=\"checkbox-item\"><input type=\"checkbox\" name=\"app\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.ResolveAttributeValue(a)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `core/admin/templates/groups.templ`, Line: 77, Col: 50}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var11)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\"> <span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(a)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `core/admin/templates/groups.templ`, Line: 78, Col: 16}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</span></label>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</div></div><button type=\"submit\" class=\"btn\" style=\"width:100%;margin-top:8px\">Create Group</button></form></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -200,6 +236,10 @@ func CreateGroupForm() templ.Component {
})
}
func allApps() []string {
return []string{"Admin Panel", "Files", "Mail", "Calendar", "Office", "Contacts", "Tasks", "Chat"}
}
type GroupRow struct {
Name string
UserCount int

View file

@ -143,5 +143,9 @@ templ adminStyles() {
.group-row-left{display:flex;align-items:center;gap:10px}
.group-icon{width:32px;height:32px;border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:0.8rem;font-weight:700;color:#fff}
.group-row-right{display:flex;align-items:center;gap:8px}
.checkbox-list{display:grid;grid-template-columns:1fr 1fr;gap:4px;max-height:120px;overflow-y:auto}
.checkbox-item{display:flex;align-items:center;gap:6px;padding:6px 8px;border-radius:6px;cursor:pointer;font-size:0.85rem}
.checkbox-item:hover{background:var(--surface2)}
.checkbox-item input[type=checkbox]{accent-color:var(--blue)}
</style>
}

File diff suppressed because one or more lines are too long

View file

@ -332,7 +332,16 @@ func (h *Handler) createGroup(w http.ResponseWriter, r *http.Request) {
http.Error(w, "name required", http.StatusBadRequest)
return
}
if err := h.groupStore.Create(name, desc); err != nil {
// Collect checked apps
apps := r.Form["app"]
appsStr := ""
for i, a := range apps {
if i > 0 {
appsStr += ","
}
appsStr += a
}
if err := h.groupStore.Create(name, desc, appsStr); err != nil {
http.Error(w, err.Error(), http.StatusConflict)
return
}

View file

@ -67,10 +67,15 @@ func (d *Database) ensureTable(name string) error {
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE NOT NULL,
description TEXT NOT NULL DEFAULT '',
apps TEXT NOT NULL DEFAULT '',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`)
return err
if err != nil {
return err
}
d.DB.Exec(`ALTER TABLE groups ADD COLUMN apps TEXT NOT NULL DEFAULT ''`)
return nil
case "users":
// Create table if it doesn't exist