Add per-role badge colors

Role badges rendered gray for everything except admin, with no way to tell
roles apart. Add an optional color per role, matching how statuses and types
carry one: new roles.color column (migration 7d27_roles_color), color threaded
through the role API and the user serializer, and a ColorSwatchPicker in the
role editor. Badges use the role's color with contrast-aware text and fall
back to the old admin/gray classes when unset.
This commit is contained in:
cproudlock
2026-07-20 10:05:58 -04:00
parent 0b247ed96f
commit a7ff882e21
5 changed files with 119 additions and 4 deletions

View File

@@ -34,7 +34,8 @@
v-for="role in user.roles"
:key="role.roleid"
class="badge"
:class="role.rolename === 'admin' ? 'badge-primary' : 'badge-secondary'"
:class="roleBadgeClass(role)"
:style="roleBadgeStyle(role)"
>
{{ role.rolename }}
</span>
@@ -83,7 +84,7 @@
<tbody>
<tr v-for="role in roles" :key="role.roleid">
<td>
<span class="badge" :class="role.rolename === 'admin' ? 'badge-primary' : 'badge-secondary'">
<span class="badge" :class="roleBadgeClass(role)" :style="roleBadgeStyle(role)">
{{ role.rolename }}
</span>
</td>
@@ -214,6 +215,11 @@
</div>
</div>
<div class="form-group">
<label>Badge color</label>
<ColorSwatchPicker v-model="roleForm.color" />
</div>
<div class="form-group" v-if="!editingRole?.isadmin">
<label>Permissions</label>
<p class="text-muted" style="font-size: 0.85rem; margin: 0 0 0.5rem 0">
@@ -289,6 +295,9 @@ import { ref, reactive, onMounted } from 'vue'
import { usersApi } from '../../api'
import { useAuthStore } from '../../stores/auth'
import { apiError } from '../../utils/apiError'
import ColorSwatchPicker from '@/components/ColorSwatchPicker.vue'
const DEFAULT_ROLE_COLOR = '#0d6efd'
const authStore = useAuthStore()
const currentUserId = authStore.user?.userid
@@ -323,9 +332,32 @@ const userForm = reactive({
const roleForm = reactive({
rolename: '',
description: '',
color: DEFAULT_ROLE_COLOR,
permissions: []
})
// Role badge: use the role's own color when set (contrast-aware text), else
// fall back to the legacy admin=primary / other=gray classes.
function isLightColor(color) {
if (!color) return false
const hex = color.replace('#', '')
if (hex.length !== 6) return false
const r = parseInt(hex.slice(0, 2), 16)
const g = parseInt(hex.slice(2, 4), 16)
const b = parseInt(hex.slice(4, 6), 16)
return (r * 299 + g * 587 + b * 114) / 1000 > 155
}
function roleBadgeClass(role) {
if (role.color) return ''
return role.rolename === 'admin' ? 'badge-primary' : 'badge-secondary'
}
function roleBadgeStyle(role) {
if (!role.color) return null
return { background: role.color, color: isLightColor(role.color) ? '#000' : '#fff' }
}
async function loadData() {
loading.value = true
try {
@@ -459,6 +491,7 @@ function editRole(role) {
editingRole.value = role
roleForm.rolename = role.rolename
roleForm.description = role.description || ''
roleForm.color = role.color || DEFAULT_ROLE_COLOR
roleForm.permissions = role.permissions ? [...role.permissions] : []
}
@@ -467,6 +500,7 @@ function closeRoleModal() {
editingRole.value = null
roleForm.rolename = ''
roleForm.description = ''
roleForm.color = DEFAULT_ROLE_COLOR
roleForm.permissions = []
}
@@ -476,6 +510,7 @@ async function saveRole() {
try {
const data = {
description: roleForm.description,
color: roleForm.color,
permissions: roleForm.permissions
}