feat(users): tag-style group selector with chips for user edit/create

This commit is contained in:
Claus Lohmar 2026-06-15 06:15:01 +00:00
parent 19f1cf2c2b
commit 8eca6d0ed4
4 changed files with 130 additions and 64 deletions

View file

@ -149,5 +149,13 @@ templ adminStyles() {
.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)}
/* Tags */
.tag-selector{border:1px solid var(--border);border-radius:8px;padding:8px;min-height:40px}
.tag-chips{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:8px;min-height:24px}
.tag-pool{display:flex;flex-wrap:wrap;gap:6px}
.tag-chip{display:inline-flex;align-items:center;gap:4px;padding:4px 10px;border-radius:99px;font-size:0.78rem;cursor:pointer;background:var(--surface2);color:var(--text2);transition:all 0.1s;user-select:none}
.tag-chip:hover{background:var(--surface3);color:var(--text)}
.tag-chip.selected{background:var(--blue);color:#fff}.tag-chip.selected .tag-x{display:inline}
.tag-x{display:none;font-weight:700;margin-left:2px;line-height:1}
</style>
}

File diff suppressed because one or more lines are too long

View file

@ -84,14 +84,14 @@ templ CreateUserForm(groups []string) {
if len(groups) > 0 {
<div class="form-group">
<label>Groups</label>
<div class="checkbox-list">
for _, g := range groups {
<label class="checkbox-item">
<input type="checkbox" name="group" value={ g }/>
<span>{ g }</span>
</label>
}
<div class="tag-selector">
<div class="tag-pool">
for _, g := range groups {
<div class="tag-chip" onclick="toggleTag(this)" data-value={ g }>{ g }</div>
}
</div>
</div>
<input type="hidden" name="group" id="group-input-create"/>
</div>
}
<button type="submit" class="btn" style="width:100%;margin-top:8px">Create User</button>
@ -195,15 +195,43 @@ templ EditUserForm(u UserRow, groups []string) {
if len(groups) > 0 {
<div class="form-group">
<label>Groups</label>
<div class="checkbox-list">
for _, g := range groups {
<label class="checkbox-item">
<input type="checkbox" name="group" value={ g } if hasGroup(u.Groups, g) { checked }/>
<span>{ g }</span>
</label>
}
<div class="tag-selector" id="group-tags">
<div class="tag-chips" id="selected-groups">
for _, g := range groups {
if hasGroup(u.Groups, g) {
<div class="tag-chip selected" onclick="toggleTag(this)" data-value={ g }>{ g }<span class="tag-x">&times;</span></div>
}
}
</div>
<div class="tag-pool">
for _, g := range groups {
if !hasGroup(u.Groups, g) {
<div class="tag-chip" onclick="toggleTag(this)" data-value={ g }>{ g }</div>
}
}
</div>
</div>
<input type="hidden" name="group" id="group-input"/>
</div>
<script type="text/javascript">
function toggleTag(el) {
el.classList.toggle('selected');
if (el.classList.contains('selected')) {
document.getElementById('selected-groups').appendChild(el);
} else {
document.getElementById('selected-groups').appendChild(el);
el.classList.add('selected');
}
updateGroupInput();
}
function updateGroupInput() {
var vals = [];
document.querySelectorAll('#selected-groups .tag-chip.selected').forEach(function(c){
vals.push(c.getAttribute('data-value'));
});
document.getElementById('group-input').value = vals.join(',');
}
</script>
}
<div style="display:flex;gap:8px;margin-top:12px">
<button type="submit" class="btn" style="flex:1">Save</button>

View file

@ -199,43 +199,43 @@ func CreateUserForm(groups []string) templ.Component {
return templ_7745c5c3_Err
}
if len(groups) > 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<div class=\"form-group\"><label>Groups</label><div class=\"checkbox-list\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<div class=\"form-group\"><label>Groups</label><div class=\"tag-selector\"><div class=\"tag-pool\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, g := range groups {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<label class=\"checkbox-item\"><input type=\"checkbox\" name=\"group\" value=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<div class=\"tag-chip\" onclick=\"toggleTag(this)\" data-value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.ResolveAttributeValue(g)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `core/admin/templates/user-dashboard.templ`, Line: 90, Col: 53}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `core/admin/templates/user-dashboard.templ`, Line: 90, Col: 70}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var10)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\"> <span>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(g)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `core/admin/templates/user-dashboard.templ`, Line: 91, Col: 17}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `core/admin/templates/user-dashboard.templ`, Line: 90, Col: 76}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</span></label>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</div></div>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</div></div><input type=\"hidden\" name=\"group\" id=\"group-input-create\"></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -484,71 +484,101 @@ func EditUserForm(u UserRow, groups []string) templ.Component {
return templ_7745c5c3_Err
}
if len(groups) > 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "<div class=\"form-group\"><label>Groups</label><div class=\"checkbox-list\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "<div class=\"form-group\"><label>Groups</label><div class=\"tag-selector\" id=\"group-tags\"><div class=\"tag-chips\" id=\"selected-groups\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, g := range groups {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "<label class=\"checkbox-item\"><input type=\"checkbox\" name=\"group\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var21 string
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.ResolveAttributeValue(g)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `core/admin/templates/user-dashboard.templ`, Line: 201, Col: 53}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var21)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if hasGroup(u.Groups, g) {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, " checked")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "<div class=\"tag-chip selected\" onclick=\"toggleTag(this)\" data-value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var21 string
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.ResolveAttributeValue(g)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `core/admin/templates/user-dashboard.templ`, Line: 202, Col: 80}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var21)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var22 string
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(g)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `core/admin/templates/user-dashboard.templ`, Line: 202, Col: 86}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "<span class=\"tag-x\">&times;</span></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "> <span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var22 string
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(g)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `core/admin/templates/user-dashboard.templ`, Line: 202, Col: 17}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "</span></label>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "</div><div class=\"tag-pool\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, g := range groups {
if !hasGroup(u.Groups, g) {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "<div class=\"tag-chip\" onclick=\"toggleTag(this)\" data-value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var23 string
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.ResolveAttributeValue(g)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `core/admin/templates/user-dashboard.templ`, Line: 209, Col: 71}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var23)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var24 string
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(g)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `core/admin/templates/user-dashboard.templ`, Line: 209, Col: 77}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "</div></div>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "</div></div><input type=\"hidden\" name=\"group\" id=\"group-input\"></div><script type=\"text/javascript\">\n\t\t\t\t\tfunction toggleTag(el) {\n\t\t\t\t\t\tel.classList.toggle('selected');\n\t\t\t\t\t\tif (el.classList.contains('selected')) {\n\t\t\t\t\t\t\tdocument.getElementById('selected-groups').appendChild(el);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tdocument.getElementById('selected-groups').appendChild(el);\n\t\t\t\t\t\t\tel.classList.add('selected');\n\t\t\t\t\t\t}\n\t\t\t\t\t\tupdateGroupInput();\n\t\t\t\t\t}\n\t\t\t\t\tfunction updateGroupInput() {\n\t\t\t\t\t\tvar vals = [];\n\t\t\t\t\t\tdocument.querySelectorAll('#selected-groups .tag-chip.selected').forEach(function(c){\n\t\t\t\t\t\t\tvals.push(c.getAttribute('data-value'));\n\t\t\t\t\t\t});\n\t\t\t\t\t\tdocument.getElementById('group-input').value = vals.join(',');\n\t\t\t\t\t}\n\t\t\t\t</script>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "<div style=\"display:flex;gap:8px;margin-top:12px\"><button type=\"submit\" class=\"btn\" style=\"flex:1\">Save</button> <button type=\"button\" class=\"btn danger\" hx-delete=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "<div style=\"display:flex;gap:8px;margin-top:12px\"><button type=\"submit\" class=\"btn\" style=\"flex:1\">Save</button> <button type=\"button\" class=\"btn danger\" hx-delete=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var23 string
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.ResolveAttributeValue("/admin/api/users/" + u.Username)
var templ_7745c5c3_Var25 string
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.ResolveAttributeValue("/admin/api/users/" + u.Username)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `core/admin/templates/user-dashboard.templ`, Line: 211, Col: 49}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `core/admin/templates/user-dashboard.templ`, Line: 239, Col: 49}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var23)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var25)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "\" hx-target=\"#user-list-body\" hx-swap=\"innerHTML\" hx-on::after-request=\"htmx.trigger('#user-detail', 'click')\">Delete</button></div></form></div>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "\" hx-target=\"#user-list-body\" hx-swap=\"innerHTML\" hx-on::after-request=\"htmx.trigger('#user-detail', 'click')\">Delete</button></div></form></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}