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

View file

@ -69,11 +69,27 @@ templ CreateGroupForm() {
<label>Description</label> <label>Description</label>
<input type="text" name="description" class="input" placeholder="Team description"/> <input type="text" name="description" class="input" placeholder="Team description"/>
</div> </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> <button type="submit" class="btn" style="width:100%;margin-top:8px">Create Group</button>
</form> </form>
</div> </div>
} }
func allApps() []string {
return []string{"Admin Panel", "Files", "Mail", "Calendar", "Office", "Contacts", "Tasks", "Chat"}
}
type GroupRow struct { type GroupRow struct {
Name string Name string
UserCount int UserCount int

View file

@ -192,7 +192,43 @@ func CreateGroupForm() templ.Component {
templ_7745c5c3_Var10 = templ.NopComponent templ_7745c5c3_Var10 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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 { type GroupRow struct {
Name string Name string
UserCount int UserCount int

View file

@ -143,5 +143,9 @@ templ adminStyles() {
.group-row-left{display:flex;align-items:center;gap:10px} .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-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} .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> </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) http.Error(w, "name required", http.StatusBadRequest)
return 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) http.Error(w, err.Error(), http.StatusConflict)
return return
} }

View file

@ -67,10 +67,15 @@ func (d *Database) ensureTable(name string) error {
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE NOT NULL, name TEXT UNIQUE NOT NULL,
description TEXT NOT NULL DEFAULT '', description TEXT NOT NULL DEFAULT '',
apps TEXT NOT NULL DEFAULT '',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP 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": case "users":
// Create table if it doesn't exist // Create table if it doesn't exist