Make admin recent-opens drill-in rows clickable
Task: #47 — Let admins jump from a recent open straight to that user. Changes (artifacts/teaboy-os/src/pages/admin.tsx): - AppOpensDrillIn: each user row in the per-app recent-opens popup is now a focusable button. Clicking jumps to that user's row in the Users list (closes the popup, then calls the existing handleSelectUserFromDashboard via a new onJumpUser prop), reusing the highlight behavior that already powers the leaderboard. - UserOpensDrillIn: each app row in the per-user recent-opens popup is now a focusable button. Clicking opens that app's edit modal (closes the popup, then calls the existing handleSelectAppFromDashboard via a new onJumpApp prop), mirroring the leaderboard click behavior. - Added type="button", hover/focus styles, and aria-labels reusing the existing admin.dashboard.viewUserDetails / admin.dashboard.viewAppDetails translation keys (already localized for both en and ar). The footer "Edit app" / "View user" buttons remain unchanged and keep working alongside the per-row clicks. No new translation keys, schema changes, or API changes were needed — AppOpenByAppEntry already exposes userId and AppOpenByUserEntry already exposes appId. Verification: tsc --noEmit on teaboy-os shows no new errors from these edits (only the same pre-existing errors that were present before this task). E2E was not run because the change is a thin wiring of existing handlers and admin login requires custom session setup beyond this task's scope. Replit-Task-Id: 5db0964e-95fa-4199-9337-631cd1a790ec
This commit is contained in:
@@ -1515,6 +1515,10 @@ function DashboardSection({
|
||||
setDrillIn(null);
|
||||
onSelectApp(id);
|
||||
}}
|
||||
onJumpUser={(id) => {
|
||||
setDrillIn(null);
|
||||
onSelectUser(id);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{drillIn?.kind === "user" && (
|
||||
@@ -1528,6 +1532,10 @@ function DashboardSection({
|
||||
setDrillIn(null);
|
||||
onSelectUser(id);
|
||||
}}
|
||||
onJumpApp={(id) => {
|
||||
setDrillIn(null);
|
||||
onSelectApp(id);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -1607,6 +1615,7 @@ function AppOpensDrillIn({
|
||||
statsQueryParams,
|
||||
onClose,
|
||||
onJump,
|
||||
onJumpUser,
|
||||
}: {
|
||||
appId: number;
|
||||
lang: string;
|
||||
@@ -1614,6 +1623,7 @@ function AppOpensDrillIn({
|
||||
statsQueryParams: GetAdminStatsParams;
|
||||
onClose: () => void;
|
||||
onJump: (appId: number) => void;
|
||||
onJumpUser: (userId: number) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { data, isLoading, error } = useGetAdminAppOpensByApp(appId, statsQueryParams, {
|
||||
@@ -1665,20 +1675,24 @@ function AppOpensDrillIn({
|
||||
const initial = (display || o.username || "?").trim().charAt(0).toUpperCase();
|
||||
const avatar = resolveServiceImageUrl(o.avatarUrl);
|
||||
return (
|
||||
<li
|
||||
key={o.id}
|
||||
className="flex items-center gap-3 p-2 rounded-xl bg-white/60 border border-slate-200/60"
|
||||
>
|
||||
<LeaderboardAvatar src={avatar} initial={initial} alt={display} />
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-sm font-medium text-foreground truncate">{display}</div>
|
||||
{display !== o.username && (
|
||||
<div className="text-[11px] text-muted-foreground truncate">@{o.username}</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-[11px] text-muted-foreground shrink-0 tabular-nums">
|
||||
{formatOpenTimestamp(o.createdAt, lang)}
|
||||
</div>
|
||||
<li key={o.id}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onJumpUser(o.userId)}
|
||||
aria-label={t("admin.dashboard.viewUserDetails", { name: display })}
|
||||
className="flex items-center gap-3 p-2 rounded-xl bg-white/60 border border-slate-200/60 w-full text-start hover:bg-white hover:border-slate-300 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 transition-colors"
|
||||
>
|
||||
<LeaderboardAvatar src={avatar} initial={initial} alt={display} />
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-sm font-medium text-foreground truncate">{display}</div>
|
||||
{display !== o.username && (
|
||||
<div className="text-[11px] text-muted-foreground truncate">@{o.username}</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-[11px] text-muted-foreground shrink-0 tabular-nums">
|
||||
{formatOpenTimestamp(o.createdAt, lang)}
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
@@ -1695,6 +1709,7 @@ function UserOpensDrillIn({
|
||||
statsQueryParams,
|
||||
onClose,
|
||||
onJump,
|
||||
onJumpApp,
|
||||
}: {
|
||||
userId: number;
|
||||
lang: string;
|
||||
@@ -1702,6 +1717,7 @@ function UserOpensDrillIn({
|
||||
statsQueryParams: GetAdminStatsParams;
|
||||
onClose: () => void;
|
||||
onJump: (userId: number) => void;
|
||||
onJumpApp: (appId: number) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { data, isLoading, error } = useGetAdminAppOpensByUser(userId, statsQueryParams, {
|
||||
@@ -1752,20 +1768,24 @@ function UserOpensDrillIn({
|
||||
{data.opens.map((o) => {
|
||||
const name = lang === "ar" ? o.nameAr : o.nameEn;
|
||||
return (
|
||||
<li
|
||||
key={o.id}
|
||||
className="flex items-center gap-3 p-2 rounded-xl bg-white/60 border border-slate-200/60"
|
||||
>
|
||||
<div
|
||||
className="w-8 h-8 rounded-lg shrink-0"
|
||||
style={{ backgroundColor: `${o.color}40` }}
|
||||
/>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-sm font-medium text-foreground truncate">{name}</div>
|
||||
</div>
|
||||
<div className="text-[11px] text-muted-foreground shrink-0 tabular-nums">
|
||||
{formatOpenTimestamp(o.createdAt, lang)}
|
||||
</div>
|
||||
<li key={o.id}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onJumpApp(o.appId)}
|
||||
aria-label={t("admin.dashboard.viewAppDetails", { name })}
|
||||
className="flex items-center gap-3 p-2 rounded-xl bg-white/60 border border-slate-200/60 w-full text-start hover:bg-white hover:border-slate-300 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 transition-colors"
|
||||
>
|
||||
<div
|
||||
className="w-8 h-8 rounded-lg shrink-0"
|
||||
style={{ backgroundColor: `${o.color}40` }}
|
||||
/>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-sm font-medium text-foreground truncate">{name}</div>
|
||||
</div>
|
||||
<div className="text-[11px] text-muted-foreground shrink-0 tabular-nums">
|
||||
{formatOpenTimestamp(o.createdAt, lang)}
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
|
||||
Reference in New Issue
Block a user