diff --git a/app/src/main/java/com/runicgateway/app/ui/navigation/Menu.kt b/app/src/main/java/com/runicgateway/app/ui/navigation/Menu.kt index a8f7c8d..a3bb18e 100644 --- a/app/src/main/java/com/runicgateway/app/ui/navigation/Menu.kt +++ b/app/src/main/java/com/runicgateway/app/ui/navigation/Menu.kt @@ -196,8 +196,21 @@ val APP_MENU: List = listOf( // Staff operations (§1, M10) — revealed for staff roles; the backend re-checks every call. MenuEntry(Routes.ADMIN_DASHBOARD, R.string.menu_admin_dashboard, MenuAccess.STAFF), MenuEntry(Routes.ADMIN_CONTENT, R.string.menu_admin_content, MenuAccess.STAFF), - MenuEntry(Routes.ADMIN_MODERATION, R.string.menu_admin_moderation, MenuAccess.MODERATOR), - MenuEntry(Routes.ADMIN_SUPPORT, R.string.menu_admin_support, MenuAccess.MODERATOR), + // Moderation and the support queue act on the shard (`/admin/shard/*`, module-uo's + // admin mount), so they need the module installed like the player rows do. On a + // Rust-only site they once showed a UO form whose every action failed (M20). + MenuEntry( + Routes.ADMIN_MODERATION, + R.string.menu_admin_moderation, + MenuAccess.MODERATOR, + capability = Capability.SHARD, + ), + MenuEntry( + Routes.ADMIN_SUPPORT, + R.string.menu_admin_support, + MenuAccess.MODERATOR, + capability = Capability.SHARD, + ), ) /** diff --git a/app/src/test/java/com/runicgateway/app/ui/navigation/MenuCapabilityGatingTest.kt b/app/src/test/java/com/runicgateway/app/ui/navigation/MenuCapabilityGatingTest.kt index 99df01a..6e42726 100644 --- a/app/src/test/java/com/runicgateway/app/ui/navigation/MenuCapabilityGatingTest.kt +++ b/app/src/test/java/com/runicgateway/app/ui/navigation/MenuCapabilityGatingTest.kt @@ -168,6 +168,26 @@ class MenuCapabilityGatingTest { } } + @Test fun moderationAndSupportNeedTheShardModule() { + // Both call `/admin/shard/*`, module-uo's admin mount. Before M20 they + // carried no capability, so a moderator on a Rust-only site saw a UO form + // whose every action failed. + val staffRows = APP_MENU.filter { it.route == Routes.ADMIN_MODERATION || it.route == Routes.ADMIN_SUPPORT } + assertEquals(2, staffRows.size) + assertTrue(staffRows.all { it.capability == Capability.SHARD }) + + val moderator = signedIn(Role.MODERATOR) + val rustOnly = visibleEntries(APP_MENU, moderator, null, serving(Capability.RUST)).map { it.route } + assertFalse(Routes.ADMIN_MODERATION in rustOnly) + assertFalse(Routes.ADMIN_SUPPORT in rustOnly) + // …while the core staff rows stay. + assertTrue(Routes.ADMIN_DASHBOARD in rustOnly) + + val uo = visibleEntries(APP_MENU, moderator, null, serving(Capability.SHARD)).map { it.route } + assertTrue(Routes.ADMIN_MODERATION in uo) + assertTrue(Routes.ADMIN_SUPPORT in uo) + } + @Test fun bothEventRowsDeclareCoresCapabilityAndNoFeature() { // Events are core's. A `feature` on one of them would gate a core screen on // a module's visibility config, which is the coupling this separation exists