Task #96: Show dependency counts in admin app/service/user lists
Goal: make the admin delete dialog show its dependency warning on
the FIRST click (matching the existing Groups UX), instead of
needing a 409 round-trip from the DELETE endpoint to populate the
warning.
Changes:
- lib/api-spec/openapi.yaml: added optional count fields to App
(groupCount, restrictionCount, openCount), Service (orderCount),
and UserProfile (noteCount, orderCount, conversationCount,
messageCount).
- artifacts/api-server/src/routes/apps.ts: GET /admin/apps now
batch-loads groupCount/restrictionCount/openCount via grouped
COUNT queries on group_apps, app_permissions, and app_opens.
- artifacts/api-server/src/routes/services.ts: GET /services now
batch-loads orderCount from service_orders.
- artifacts/api-server/src/routes/users.ts: GET /users now batch-
loads noteCount/orderCount/conversationCount/messageCount from
notes, service_orders, conversations.created_by, and
messages.sender_id.
- artifacts/tx-os/src/pages/admin.tsx: Apps, Services, and Users
delete buttons pre-populate their conflict-state from the row's
count fields so the DeletionWarningDialog shows the warning on
the first click. The lazy 409 fallback still works as a safety
net for any caller without counts.
- replit.md: documented the new admin-panel delete UX.
Tests:
- Added artifacts/api-server/tests/list-dependency-counts.test.mjs
with 6 tests verifying each list endpoint exposes the new count
fields with the expected non-zero values when dependents exist
AND zero values when they do not (apps, services, users).
- Existing artifacts/api-server/tests/delete-force-warnings.test.mjs
(9 tests) still passes — DELETE behavior unchanged.
- Verified end-to-end with a Playwright browser test: clicking the
service delete icon ONCE opens the confirmation modal with the
dependency warning ("orders", count >= 1) immediately.
Notes / drift:
- Count fields were added to the shared App/Service/UserProfile
schemas (not list-only sub-schemas) so the same shape is returned
from list and detail endpoints. Code review flagged this as
acceptable but broader than strictly necessary; left as is to
keep the API consistent.
Out of scope (already pre-existing on main, tracked as follow-ups):
- artifacts/api-server/tests/apps-open.test.mjs has a syntax error.
- artifacts/api-server/tests/app-permissions-unique.test.mjs has
two failing assertions about the composite primary key.
- artifacts/api-server/src/routes/executive-meetings.ts has 3 pre-
existing typecheck errors around the font-settings scope column.
- The `test` workflow exits with ECONNREFUSED when the api-server
isn't already up — needs a readiness check.
Replit-Task-Id: bd14fe73-9961-431b-ab5a-ab70f116e8c7
This commit is contained in:
@@ -122,7 +122,56 @@ router.get("/admin/apps", requireAdmin, async (_req, res): Promise<void> => {
|
||||
.select()
|
||||
.from(appsTable)
|
||||
.orderBy(asc(appsTable.sortOrder), asc(appsTable.nameEn));
|
||||
res.json(allApps);
|
||||
|
||||
if (allApps.length === 0) {
|
||||
res.json([]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Compute dependency counts so the admin delete dialog can warn before
|
||||
// the first click (mirrors the GET /groups behavior). The lazy 409
|
||||
// fallback in DELETE /apps/:id remains as a safety net.
|
||||
const appIds = allApps.map((a) => a.id);
|
||||
|
||||
const groupRows = await db
|
||||
.select({
|
||||
appId: groupAppsTable.appId,
|
||||
count: sql<number>`count(*)::int`,
|
||||
})
|
||||
.from(groupAppsTable)
|
||||
.where(inArray(groupAppsTable.appId, appIds))
|
||||
.groupBy(groupAppsTable.appId);
|
||||
const restrictionRows = await db
|
||||
.select({
|
||||
appId: appPermissionsTable.appId,
|
||||
count: sql<number>`count(*)::int`,
|
||||
})
|
||||
.from(appPermissionsTable)
|
||||
.where(inArray(appPermissionsTable.appId, appIds))
|
||||
.groupBy(appPermissionsTable.appId);
|
||||
const openRows = await db
|
||||
.select({
|
||||
appId: appOpensTable.appId,
|
||||
count: sql<number>`count(*)::int`,
|
||||
})
|
||||
.from(appOpensTable)
|
||||
.where(inArray(appOpensTable.appId, appIds))
|
||||
.groupBy(appOpensTable.appId);
|
||||
|
||||
const groupMap = new Map(groupRows.map((r) => [r.appId, r.count]));
|
||||
const restrictionMap = new Map(
|
||||
restrictionRows.map((r) => [r.appId, r.count]),
|
||||
);
|
||||
const openMap = new Map(openRows.map((r) => [r.appId, r.count]));
|
||||
|
||||
res.json(
|
||||
allApps.map((a) => ({
|
||||
...a,
|
||||
groupCount: groupMap.get(a.id) ?? 0,
|
||||
restrictionCount: restrictionMap.get(a.id) ?? 0,
|
||||
openCount: openMap.get(a.id) ?? 0,
|
||||
})),
|
||||
);
|
||||
});
|
||||
|
||||
router.put("/me/app-order", requireAuth, async (req, res): Promise<void> => {
|
||||
|
||||
Reference in New Issue
Block a user