Files
metabuilder/qml/qt6/UserManagement.qml
johndoe6345789 d9ca84628b feat(a11y): deep keyboard accessibility pass across all QML components
Second-pass a11y work across all 12 component groups. Every interactive
element now has activeFocusOnTab, Keys.onReturnPressed/SpacePressed, and
context-aware Accessible.name/description bindings.

Highlights:
- Dialogs: keyboard handlers with enabled-guard on confirm buttons
- CDropdownMenu: full keyboard nav (Up/Down/Enter/Escape)
- CLoginForm: explicit KeyNavigation.tab chain (username→password→submit)
- CNotificationBell: dynamic "3 notifications"/"No notifications" name
- CJobProgressBar: Accessible.minimumValue/maximumValue/currentValue
- CExecutionStatusDot: "Execution status: Running/Passed/Failed" binding
- CKeyboardShortcuts: invisible Repeater exposes all shortcuts to a11y tree
- CDataTable rows: "Row N of M" descriptions
- Canvas elements: Accessible.Canvas role + keyboard zoom (+/- keys)
- DropdownExpandedList: focus-highlight extended to :activeFocus
- Dynamic names reflect loading state (e.g. "Signing in, please wait")

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 20:53:53 +00:00

278 lines
8.3 KiB
QML

import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QmlComponents 1.0
import "qmllib/dbal"
import "qmllib/MetaBuilder"
import "qmllib/MetaBuilder/UserManagementDBAL.js" as UDBAL
Rectangle {
id: root
color: Theme.background
objectName: "view_user_management"
Accessible.role: Accessible.Pane
Accessible.name: "User Management"
DBALProvider { id: dbal }
property bool useLiveData: dbal.connected
property var users: UDBAL.loadJson(
Qt.resolvedUrl(
"qmllib/MetaBuilder/data/"
+ "users-mock.json"
)
)
property string searchText: ""
property string activeRoleFilter: "all"
property int nextUid: 5
property bool createDialogOpen: false
property bool editDialogOpen: false
property bool deleteDialogOpen: false
property int editIndex: -1
property int deleteIndex: -1
readonly property var roles: [
"user", "admin", "god", "supergod"
]
function loadUsers() {
UDBAL.loadUsers(dbal,
function(parsed) {
users = parsed
nextUid = parsed.length + 1
}
)
}
onUseLiveDataChanged: {
if (useLiveData) loadUsers()
}
Component.onCompleted: { loadUsers() }
function createUser(userData) {
if (useLiveData) {
dbal.create("user", userData,
function(r, e) {
if (!e) loadUsers()
else {
users = UDBAL
.createUserLocally(
users, nextUid,
userData
)
nextUid++
}
createDialogOpen = false
}
)
} else {
users = UDBAL.createUserLocally(
users, nextUid, userData
)
nextUid++
createDialogOpen = false
}
}
function saveEdit(userData) {
if (editIndex < 0) return
if (useLiveData) {
dbal.update("user",
users[editIndex].uid, userData,
function(r, e) {
if (!e) loadUsers()
else users = UDBAL
.saveEditLocally(
users, editIndex,
userData
)
editDialogOpen = false
}
)
} else {
users = UDBAL.saveEditLocally(
users, editIndex, userData
)
editDialogOpen = false
}
}
function confirmDelete() {
if (deleteIndex < 0) return
if (useLiveData) {
dbal.remove("user",
users[deleteIndex].uid,
function(r, e) {
if (!e) loadUsers()
else users = UDBAL
.deleteLocally(
users, deleteIndex
)
deleteDialogOpen = false
deleteIndex = -1
}
)
} else {
users = UDBAL.deleteLocally(
users, deleteIndex
)
deleteDialogOpen = false
deleteIndex = -1
}
}
ColumnLayout {
anchors.fill: parent
anchors.margins: 20; spacing: 16
FlexRow {
Layout.fillWidth: true; spacing: 12
CText {
variant: "h3"
text: "User Management"
}
Item { Layout.fillWidth: true }
CButton {
text: "Create User"
variant: "primary"
activeFocusOnTab: true
Accessible.role: Accessible.Button
Accessible.name: "Create new user"
Keys.onReturnPressed:
createDialogOpen = true
onClicked: createDialogOpen = true
}
}
UserStatsBar {
Layout.fillWidth: true
totalUsers: users.length
adminCount: UDBAL.countByRole(
users, "admin"
)
godCount: UDBAL.countByRole(
users, "god"
)
superGodCount: UDBAL.countByRole(
users, "supergod"
)
}
UserSearchFilter {
Layout.fillWidth: true
searchText: root.searchText
activeRoleFilter:
root.activeRoleFilter
onSearchChanged: function(t) {
root.searchText = t
}
onRoleFilterChanged: function(r) {
root.activeRoleFilter = r
}
}
UserTable {
Layout.fillWidth: true
Layout.fillHeight: true
users: UDBAL.filteredUsers(
root.users, root.searchText,
root.activeRoleFilter
)
allUsers: root.users
onEditClicked: function(uid) {
var idx = UDBAL.findUserIndex(
root.users, uid
)
if (idx < 0) return
var u = root.users[idx]
editFormDialog.formUsername =
u.username
editFormDialog.formEmail =
u.email
editFormDialog.formPassword = ""
editFormDialog.formRole = u.role
editFormDialog.formActive =
u.status === "active"
editIndex = idx
editDialogOpen = true
}
onDeleteClicked: function(uid) {
deleteIndex = UDBAL.findUserIndex(
root.users, uid
)
deleteDialogOpen = true
}
}
}
UserFormDialog {
visible: createDialogOpen
isEdit: false; roles: root.roles
onAccepted: createUser({
username: formUsername,
email: formEmail,
role: formRole,
status: formActive
? "active" : "inactive"
})
onCancelled: createDialogOpen = false
}
UserFormDialog {
id: editFormDialog
visible: editDialogOpen
isEdit: true; roles: root.roles
onAccepted: saveEdit({
username: formUsername,
email: formEmail,
role: formRole,
status: formActive
? "active" : "inactive"
})
onCancelled: editDialogOpen = false
}
CDialog {
visible: deleteDialogOpen
title: "Delete User"
ColumnLayout {
spacing: 16; width: 380
CAlert {
Layout.fillWidth: true
severity: "error"
text: deleteIndex >= 0
&& deleteIndex < users.length
? "Are you sure you want to"
+ " delete \""
+ users[deleteIndex]
.username
+ "\"? This action cannot"
+ " be undone."
: "Confirm deletion?"
}
FlexRow {
Layout.fillWidth: true
spacing: 8
Item { Layout.fillWidth: true }
CButton {
text: "Cancel"
variant: "ghost"
activeFocusOnTab: true
Accessible.role: Accessible.Button
Accessible.name: "Cancel deletion"
Keys.onReturnPressed: {
deleteDialogOpen = false
deleteIndex = -1
}
onClicked: {
deleteDialogOpen = false
deleteIndex = -1
}
}
CButton {
text: "Delete"
variant: "danger"
activeFocusOnTab: true
Accessible.role: Accessible.Button
Accessible.name:
"Confirm delete user"
Keys.onReturnPressed:
confirmDelete()
onClicked: confirmDelete()
}
}
}
}
}