feat(android): AdminRequests — show requester username
Audit v2 #18. AdminRequestsScreen was showing only the request's display name, not which user asked for it. Now fetches the admin users list in parallel with the requests list, builds a userId → username map, and renders "Requested by <username>" beneath each row. Falls back to an 8-char userId prefix when the users list fetch fails (e.g., admin without users-list access on some future server-side ACL change). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -81,6 +81,7 @@ fun AdminRequestsScreen(
|
|||||||
)
|
)
|
||||||
is AdminRequestsUiState.Success -> RequestList(
|
is AdminRequestsUiState.Success -> RequestList(
|
||||||
rows = s.rows,
|
rows = s.rows,
|
||||||
|
usernames = s.usernames,
|
||||||
onApprove = viewModel::approve,
|
onApprove = viewModel::approve,
|
||||||
onReject = viewModel::reject,
|
onReject = viewModel::reject,
|
||||||
)
|
)
|
||||||
@@ -92,12 +93,20 @@ fun AdminRequestsScreen(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun RequestList(
|
private fun RequestList(
|
||||||
rows: List<RequestRef>,
|
rows: List<RequestRef>,
|
||||||
|
usernames: Map<String, String>,
|
||||||
onApprove: (String) -> Unit,
|
onApprove: (String) -> Unit,
|
||||||
onReject: (String) -> Unit,
|
onReject: (String) -> Unit,
|
||||||
) {
|
) {
|
||||||
LazyColumn(modifier = Modifier.fillMaxSize()) {
|
LazyColumn(modifier = Modifier.fillMaxSize()) {
|
||||||
items(items = rows, key = { it.id }) { req ->
|
items(items = rows, key = { it.id }) { req ->
|
||||||
AdminRequestRow(req = req, onApprove = { onApprove(req.id) }, onReject = { onReject(req.id) })
|
AdminRequestRow(
|
||||||
|
req = req,
|
||||||
|
requester = usernames[req.userId]
|
||||||
|
?: req.userId.takeIf { it.isNotEmpty() }?.take(8)
|
||||||
|
?: "unknown",
|
||||||
|
onApprove = { onApprove(req.id) },
|
||||||
|
onReject = { onReject(req.id) },
|
||||||
|
)
|
||||||
HorizontalDivider()
|
HorizontalDivider()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -106,6 +115,7 @@ private fun RequestList(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun AdminRequestRow(
|
private fun AdminRequestRow(
|
||||||
req: RequestRef,
|
req: RequestRef,
|
||||||
|
requester: String,
|
||||||
onApprove: () -> Unit,
|
onApprove: () -> Unit,
|
||||||
onReject: () -> Unit,
|
onReject: () -> Unit,
|
||||||
) {
|
) {
|
||||||
@@ -122,6 +132,13 @@ private fun AdminRequestRow(
|
|||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
)
|
)
|
||||||
|
Text(
|
||||||
|
text = "Requested by $requester",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||||
AssistChip(
|
AssistChip(
|
||||||
onClick = {},
|
onClick = {},
|
||||||
|
|||||||
+19
-4
@@ -3,10 +3,13 @@ package com.fabledsword.minstrel.admin.ui
|
|||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.fabledsword.minstrel.admin.data.AdminRequestsRepository
|
import com.fabledsword.minstrel.admin.data.AdminRequestsRepository
|
||||||
|
import com.fabledsword.minstrel.admin.data.AdminUsersRepository
|
||||||
import com.fabledsword.minstrel.events.EventsStream
|
import com.fabledsword.minstrel.events.EventsStream
|
||||||
import com.fabledsword.minstrel.models.RequestRef
|
import com.fabledsword.minstrel.models.RequestRef
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
@@ -19,13 +22,17 @@ private val RELEVANT_EVENT_KINDS = setOf("request.status_changed")
|
|||||||
sealed interface AdminRequestsUiState {
|
sealed interface AdminRequestsUiState {
|
||||||
data object Loading : AdminRequestsUiState
|
data object Loading : AdminRequestsUiState
|
||||||
data object Empty : AdminRequestsUiState
|
data object Empty : AdminRequestsUiState
|
||||||
data class Success(val rows: List<RequestRef>) : AdminRequestsUiState
|
data class Success(
|
||||||
|
val rows: List<RequestRef>,
|
||||||
|
val usernames: Map<String, String>,
|
||||||
|
) : AdminRequestsUiState
|
||||||
data class Error(val message: String) : AdminRequestsUiState
|
data class Error(val message: String) : AdminRequestsUiState
|
||||||
}
|
}
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class AdminRequestsViewModel @Inject constructor(
|
class AdminRequestsViewModel @Inject constructor(
|
||||||
private val repository: AdminRequestsRepository,
|
private val repository: AdminRequestsRepository,
|
||||||
|
private val usersRepository: AdminUsersRepository,
|
||||||
private val eventsStream: EventsStream,
|
private val eventsStream: EventsStream,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
@@ -44,11 +51,19 @@ class AdminRequestsViewModel @Inject constructor(
|
|||||||
fun refresh(): Job = viewModelScope.launch {
|
fun refresh(): Job = viewModelScope.launch {
|
||||||
internal.value = AdminRequestsUiState.Loading
|
internal.value = AdminRequestsUiState.Loading
|
||||||
try {
|
try {
|
||||||
val rows = repository.list()
|
val (rows, usernames) = coroutineScope {
|
||||||
|
val requestsJob = async { repository.list() }
|
||||||
|
val usernamesJob = async {
|
||||||
|
runCatching {
|
||||||
|
usersRepository.list().associate { it.id to it.username }
|
||||||
|
}.getOrDefault(emptyMap())
|
||||||
|
}
|
||||||
|
requestsJob.await() to usernamesJob.await()
|
||||||
|
}
|
||||||
internal.value = if (rows.isEmpty()) {
|
internal.value = if (rows.isEmpty()) {
|
||||||
AdminRequestsUiState.Empty
|
AdminRequestsUiState.Empty
|
||||||
} else {
|
} else {
|
||||||
AdminRequestsUiState.Success(rows)
|
AdminRequestsUiState.Success(rows = rows, usernames = usernames)
|
||||||
}
|
}
|
||||||
} catch (
|
} catch (
|
||||||
@Suppress("TooGenericExceptionCaught") e: Throwable,
|
@Suppress("TooGenericExceptionCaught") e: Throwable,
|
||||||
@@ -75,7 +90,7 @@ class AdminRequestsViewModel @Inject constructor(
|
|||||||
internal.value = if (filtered.isEmpty()) {
|
internal.value = if (filtered.isEmpty()) {
|
||||||
AdminRequestsUiState.Empty
|
AdminRequestsUiState.Empty
|
||||||
} else {
|
} else {
|
||||||
AdminRequestsUiState.Success(filtered)
|
AdminRequestsUiState.Success(rows = filtered, usernames = before.usernames)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
|||||||
Reference in New Issue
Block a user