From 05f07d82e83808347754500a3805f1cad4d8aec3 Mon Sep 17 00:00:00 2001 From: swithek <52840391+swithek@users.noreply.github.com> Date: Sat, 12 Sep 2026 17:56:00 +0200 Subject: [PATCH] hide app section in settings when all apps are disabled --- .../settings/BaseModal.nuxt.test.ts | 24 +++++++++++++++ web/app/components/settings/BaseModal.vue | 21 ++++++++----- .../api/useCapabilitiesAPI.nuxt.test.ts | 30 +++++++++++++++++++ web/app/composables/api/useCapabilitiesAPI.ts | 4 +++ 4 files changed, 71 insertions(+), 8 deletions(-) diff --git a/web/app/components/settings/BaseModal.nuxt.test.ts b/web/app/components/settings/BaseModal.nuxt.test.ts index 384e58c7..2c10a052 100644 --- a/web/app/components/settings/BaseModal.nuxt.test.ts +++ b/web/app/components/settings/BaseModal.nuxt.test.ts @@ -16,6 +16,7 @@ import { mockAuthOrganization, seedAuthAccounts, seedAuthSession, + seedCapabilities, t, teleportedButton, } from "../test-helpers" @@ -112,6 +113,29 @@ describe("", { concurrent: false }, () => { ]) }) + it.for([{ app: "github" }, { app: "slack" }] as { + app: "github" | "slack" + }[])( + "keeps the apps section while only $app is available", + async ({ app }, { expect }) => { + seedCapabilities({ github: app === "github", slack: app === "slack" }) + + await mountModal() + + expect(dialogText()).toContain(t("settings.apps.title")) + }, + ) + + it("drops the apps section on a deployment with no apps", async ({ + expect, + }) => { + seedCapabilities({ github: false, slack: false }) + + await mountModal() + + expect(dialogText()).not.toContain(t("settings.apps.title")) + }) + it("gives the deep-linkable sections their anchors", async ({ expect }) => { await mountModal() diff --git a/web/app/components/settings/BaseModal.vue b/web/app/components/settings/BaseModal.vue index d5a38998..3c81c799 100644 --- a/web/app/components/settings/BaseModal.vue +++ b/web/app/components/settings/BaseModal.vue @@ -20,6 +20,7 @@ const emit = defineEmits<{ }>() const { t } = useI18n({ useScope: "global" }) +const { isAnyAppEnabled } = useCapabilitiesAPI() const openAction = ref< | null | "email-change" @@ -35,7 +36,7 @@ const openAction = ref< const actionOpts = ref( null, ) -const sections = [ +const sections = computed(() => [ { title: t("settings.profile.title"), // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- eslint's ts program resolves .vue imports as error typed, vue-tsc accepts this @@ -47,12 +48,16 @@ const sections = [ component: WorkspaceSection, id: ORG_SECTION_ID, }, - { - title: t("settings.apps.title"), - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- eslint's ts program resolves .vue imports as error typed, vue-tsc accepts this - component: AppsSection, - id: APP_SECTION_ID, // for scrolling into view when opened - }, + ...(isAnyAppEnabled.value + ? [ + { + title: t("settings.apps.title"), + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- eslint's ts program resolves .vue imports as error typed, vue-tsc accepts this + component: AppsSection, + id: APP_SECTION_ID, // for scrolling into view when opened + }, + ] + : []), { title: t("settings.mcp.title"), // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- eslint's ts program resolves .vue imports as error typed, vue-tsc accepts this @@ -64,7 +69,7 @@ const sections = [ component: DataSourceSection, id: DATA_SOURCE_SECTION_ID, }, -] +]) whenever(open, async (val) => { if (val === "org-members") { diff --git a/web/app/composables/api/useCapabilitiesAPI.nuxt.test.ts b/web/app/composables/api/useCapabilitiesAPI.nuxt.test.ts index daae707a..33c07a77 100644 --- a/web/app/composables/api/useCapabilitiesAPI.nuxt.test.ts +++ b/web/app/composables/api/useCapabilitiesAPI.nuxt.test.ts @@ -98,6 +98,36 @@ describe("useCapabilitiesAPI", { concurrent: false }, () => { }) }) + describe("isAnyAppEnabled", () => { + const cc: Record = { + "both apps are available": { github: true, slack: true }, + "only github is available": { github: true, slack: false }, + "only slack is available": { github: false, slack: true }, + } + + Object.entries(cc).forEach(([name, c]) => { + it(`reports enabled when ${name}`, async ({ expect }) => { + mockEndpoint("GET", "/api/capabilities", () => stubCapabilities(c)) + const api = makeCapabilitiesAPI() + + await api.fetchCapabilities.refresh() + + expect(api.isAnyAppEnabled.value).toBe(true) + }) + }) + + it("reports disabled when neither app is available", async ({ expect }) => { + mockEndpoint("GET", "/api/capabilities", () => + stubCapabilities({ github: false, slack: false }), + ) + const api = makeCapabilitiesAPI() + + await api.fetchCapabilities.refresh() + + expect(api.isAnyAppEnabled.value).toBe(false) + }) + }) + describe("isAssistantEnabled", () => { const cc: Record = { "runs a full-strength model": { diff --git a/web/app/composables/api/useCapabilitiesAPI.ts b/web/app/composables/api/useCapabilitiesAPI.ts index 3c3bfd8d..4cb5d0c2 100644 --- a/web/app/composables/api/useCapabilitiesAPI.ts +++ b/web/app/composables/api/useCapabilitiesAPI.ts @@ -28,6 +28,9 @@ export default function () { const isGithubEnabled = enabled((c) => c.github) const isSlackEnabled = enabled((c) => c.slack) + // github and slack are the only connectable apps, so both being off + // leaves nothing for the apps settings section to show + const isAnyAppEnabled = enabled((c) => c.github || c.slack) const isChangeDetectionEnabled = enabled((c) => c.changeDetection) const isSearchEnabled = enabled((c) => c.search) @@ -47,6 +50,7 @@ export default function () { fetchCapabilities, isGithubEnabled, isSlackEnabled, + isAnyAppEnabled, isChangeDetectionEnabled, isSearchEnabled, isAssistantEnabled,