Task #183: clickable dependency counts on admin Apps/Services/Users
Turned the inline dependency-count badges on each admin row into
focusable, keyboard-accessible buttons that open a drill-in modal
listing the actual rows behind the count.
Backend (artifacts/api-server/src/routes):
- apps.ts: GET /admin/apps/:id/dependents/{groups,restrictions,opens}
- services.ts: GET /admin/services/:id/dependents/orders
- users.ts: GET /admin/users/:id/dependents/{notes,orders,conversations,messages}
All paginated (limit default 50, max 200; offset) returning
{items, totalCount, limit, offset, nextOffset}. Restrictions joins
the role names that include each required permission.
OpenAPI:
- Added 8 path operations + 16 schemas (Item + Page) and re-ran
`pnpm --filter @workspace/api-spec run codegen`.
Frontend (artifacts/tx-os/src/pages/admin.tsx):
- New DependencyDrillIn component reuses DrillInShell (Escape +
backdrop close, RTL/LTR safe) and the existing LoadMoreSection
pagination pattern from AppOpensDrillIn.
- Each count part in Apps, Services, and Users panels is now a
<button> with a unique data-testid (e.g. app-counts-groups-213,
user-counts-messages-5) and an aria-label that reads
"View {count}".
- AdminPage owns dependencyTarget state for Apps/Services counts;
UsersPanel owns its own (it already encapsulates user list).
Translations:
- Added admin.dependents.* (titles, subtitles, empty/error/load
labels, conversation/order/message helper strings, order status
enum) to en.json and ar.json.
Verification: - tx-os typecheck clean; api-server has only the pre-existing
executive-meetings.ts errors (untouched).
- e2e tested via runTest: login as admin, opened Apps panel,
drilled into groups + opens (Load more grew 50→100), drilled into
Tea orders, drilled into user 5 conversations and messages,
switched to Arabic/RTL and re-opened the Groups drill-in to
confirm Arabic rendering with no raw i18n keys.
Replit-Task-Id: fe96a05b-325f-4e1f-901b-3a2235fb24b5
This commit is contained in:
@@ -912,6 +912,167 @@ export interface AdminAppOpensByUser {
|
||||
opens: AppOpenByUserEntry[];
|
||||
}
|
||||
|
||||
export interface AppDependentGroupItem {
|
||||
id: number;
|
||||
name: string;
|
||||
/** @nullable */
|
||||
descriptionAr: string | null;
|
||||
/** @nullable */
|
||||
descriptionEn: string | null;
|
||||
}
|
||||
|
||||
export interface AppDependentGroupsPage {
|
||||
items: AppDependentGroupItem[];
|
||||
totalCount: number;
|
||||
limit: number;
|
||||
offset: number;
|
||||
/** @nullable */
|
||||
nextOffset: number | null;
|
||||
}
|
||||
|
||||
export interface AppDependentRestrictionItem {
|
||||
id: number;
|
||||
name: string;
|
||||
/** @nullable */
|
||||
descriptionAr: string | null;
|
||||
/** @nullable */
|
||||
descriptionEn: string | null;
|
||||
/** Names of roles currently holding this permission. */
|
||||
roles: string[];
|
||||
}
|
||||
|
||||
export interface AppDependentRestrictionsPage {
|
||||
items: AppDependentRestrictionItem[];
|
||||
totalCount: number;
|
||||
limit: number;
|
||||
offset: number;
|
||||
/** @nullable */
|
||||
nextOffset: number | null;
|
||||
}
|
||||
|
||||
export interface AppDependentOpenItem {
|
||||
id: number;
|
||||
createdAt: string;
|
||||
userId: number;
|
||||
username: string;
|
||||
/** @nullable */
|
||||
displayNameAr: string | null;
|
||||
/** @nullable */
|
||||
displayNameEn: string | null;
|
||||
/** @nullable */
|
||||
avatarUrl: string | null;
|
||||
}
|
||||
|
||||
export interface AppDependentOpensPage {
|
||||
items: AppDependentOpenItem[];
|
||||
totalCount: number;
|
||||
limit: number;
|
||||
offset: number;
|
||||
/** @nullable */
|
||||
nextOffset: number | null;
|
||||
}
|
||||
|
||||
export interface ServiceDependentOrderItem {
|
||||
id: number;
|
||||
status: string;
|
||||
/** @nullable */
|
||||
notes: string | null;
|
||||
createdAt: string;
|
||||
userId: number;
|
||||
username: string;
|
||||
/** @nullable */
|
||||
displayNameAr: string | null;
|
||||
/** @nullable */
|
||||
displayNameEn: string | null;
|
||||
}
|
||||
|
||||
export interface ServiceDependentOrdersPage {
|
||||
items: ServiceDependentOrderItem[];
|
||||
totalCount: number;
|
||||
limit: number;
|
||||
offset: number;
|
||||
/** @nullable */
|
||||
nextOffset: number | null;
|
||||
}
|
||||
|
||||
export interface UserDependentNoteItem {
|
||||
id: number;
|
||||
title: string;
|
||||
isPinned: boolean;
|
||||
isArchived: boolean;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface UserDependentNotesPage {
|
||||
items: UserDependentNoteItem[];
|
||||
totalCount: number;
|
||||
limit: number;
|
||||
offset: number;
|
||||
/** @nullable */
|
||||
nextOffset: number | null;
|
||||
}
|
||||
|
||||
export interface UserDependentOrderItem {
|
||||
id: number;
|
||||
status: string;
|
||||
/** @nullable */
|
||||
notes: string | null;
|
||||
createdAt: string;
|
||||
serviceId: number;
|
||||
serviceNameAr: string;
|
||||
serviceNameEn: string;
|
||||
}
|
||||
|
||||
export interface UserDependentOrdersPage {
|
||||
items: UserDependentOrderItem[];
|
||||
totalCount: number;
|
||||
limit: number;
|
||||
offset: number;
|
||||
/** @nullable */
|
||||
nextOffset: number | null;
|
||||
}
|
||||
|
||||
export interface UserDependentConversationItem {
|
||||
id: number;
|
||||
/** @nullable */
|
||||
nameAr: string | null;
|
||||
/** @nullable */
|
||||
nameEn: string | null;
|
||||
isGroup: boolean;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface UserDependentConversationsPage {
|
||||
items: UserDependentConversationItem[];
|
||||
totalCount: number;
|
||||
limit: number;
|
||||
offset: number;
|
||||
/** @nullable */
|
||||
nextOffset: number | null;
|
||||
}
|
||||
|
||||
export interface UserDependentMessageItem {
|
||||
id: number;
|
||||
content: string;
|
||||
kind: string;
|
||||
createdAt: string;
|
||||
conversationId: number;
|
||||
/** @nullable */
|
||||
conversationNameAr: string | null;
|
||||
/** @nullable */
|
||||
conversationNameEn: string | null;
|
||||
conversationIsGroup: boolean;
|
||||
}
|
||||
|
||||
export interface UserDependentMessagesPage {
|
||||
items: UserDependentMessageItem[];
|
||||
totalCount: number;
|
||||
limit: number;
|
||||
offset: number;
|
||||
/** @nullable */
|
||||
nextOffset: number | null;
|
||||
}
|
||||
|
||||
export interface AuditLogActor {
|
||||
id: number;
|
||||
username: string;
|
||||
@@ -1327,3 +1488,99 @@ Combines with `targetId` and the other filters.
|
||||
*/
|
||||
actorUserId?: number;
|
||||
};
|
||||
|
||||
export type GetAdminAppDependentGroupsParams = {
|
||||
/**
|
||||
* @minimum 1
|
||||
* @maximum 200
|
||||
*/
|
||||
limit?: number;
|
||||
/**
|
||||
* @minimum 0
|
||||
*/
|
||||
offset?: number;
|
||||
};
|
||||
|
||||
export type GetAdminAppDependentRestrictionsParams = {
|
||||
/**
|
||||
* @minimum 1
|
||||
* @maximum 200
|
||||
*/
|
||||
limit?: number;
|
||||
/**
|
||||
* @minimum 0
|
||||
*/
|
||||
offset?: number;
|
||||
};
|
||||
|
||||
export type GetAdminAppDependentOpensParams = {
|
||||
/**
|
||||
* @minimum 1
|
||||
* @maximum 200
|
||||
*/
|
||||
limit?: number;
|
||||
/**
|
||||
* @minimum 0
|
||||
*/
|
||||
offset?: number;
|
||||
};
|
||||
|
||||
export type GetAdminServiceDependentOrdersParams = {
|
||||
/**
|
||||
* @minimum 1
|
||||
* @maximum 200
|
||||
*/
|
||||
limit?: number;
|
||||
/**
|
||||
* @minimum 0
|
||||
*/
|
||||
offset?: number;
|
||||
};
|
||||
|
||||
export type GetAdminUserDependentNotesParams = {
|
||||
/**
|
||||
* @minimum 1
|
||||
* @maximum 200
|
||||
*/
|
||||
limit?: number;
|
||||
/**
|
||||
* @minimum 0
|
||||
*/
|
||||
offset?: number;
|
||||
};
|
||||
|
||||
export type GetAdminUserDependentOrdersParams = {
|
||||
/**
|
||||
* @minimum 1
|
||||
* @maximum 200
|
||||
*/
|
||||
limit?: number;
|
||||
/**
|
||||
* @minimum 0
|
||||
*/
|
||||
offset?: number;
|
||||
};
|
||||
|
||||
export type GetAdminUserDependentConversationsParams = {
|
||||
/**
|
||||
* @minimum 1
|
||||
* @maximum 200
|
||||
*/
|
||||
limit?: number;
|
||||
/**
|
||||
* @minimum 0
|
||||
*/
|
||||
offset?: number;
|
||||
};
|
||||
|
||||
export type GetAdminUserDependentMessagesParams = {
|
||||
/**
|
||||
* @minimum 1
|
||||
* @maximum 200
|
||||
*/
|
||||
limit?: number;
|
||||
/**
|
||||
* @minimum 0
|
||||
*/
|
||||
offset?: number;
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user