diff --git a/README.md b/README.md index 23d4d476..41133d95 100644 --- a/README.md +++ b/README.md @@ -161,6 +161,7 @@ For a full theming guide with example themes and an AI prompt to generate your o | `customize.websiteLogo` | `OCAP_CUSTOMIZE_WEBSITELOGO` | URL to your website logo | `""` | | `customize.websiteLogoSize` | `OCAP_CUSTOMIZE_WEBSITELOGOSIZE` | Logo size | `32px` | | `customize.disableKillCount` | `OCAP_CUSTOMIZE_DISABLEKILLCOUNT` | Hide kill counts in the UI | `false` | +| `customize.hideMapFilters` | `OCAP_CUSTOMIZE_HIDEMAPFILTERS` | Hide the map filter dropdown on the recording list | `false` | | `customize.headerTitle` | `OCAP_CUSTOMIZE_HEADERTITLE` | Custom header title | `""` | | `customize.headerSubtitle` | `OCAP_CUSTOMIZE_HEADERSUBTITLE` | Custom header subtitle | `""` | | `customize.cssOverrides` | `OCAP_CUSTOMIZE_CSSOVERRIDES` | CSS variable overrides (JSON object, see below) | `{}` | diff --git a/internal/server/setting.go b/internal/server/setting.go index 676a2ae1..b39214f9 100644 --- a/internal/server/setting.go +++ b/internal/server/setting.go @@ -47,6 +47,7 @@ type Customize struct { WebsiteLogo string `json:"websiteLogo" yaml:"websiteLogo"` WebsiteLogoSize string `json:"websiteLogoSize" yaml:"websiteLogoSize"` DisableKillCount bool `json:"disableKillCount" yaml:"disableKillCount"` + HideMapFilters bool `json:"hideMapFilters" yaml:"hideMapFilters"` HeaderTitle string `json:"headerTitle" yaml:"headerTitle"` HeaderSubtitle string `json:"headerSubtitle" yaml:"headerSubtitle"` PageTitle string `json:"pageTitle" yaml:"pageTitle"` @@ -109,6 +110,7 @@ func NewSetting() (setting Setting, err error) { viper.SetDefault("customize.websiteLogo", "") viper.SetDefault("customize.websiteLogoSize", "32px") viper.SetDefault("customize.disableKillCount", false) + viper.SetDefault("customize.hideMapFilters", false) viper.SetDefault("customize.headerTitle", "") viper.SetDefault("customize.headerSubtitle", "") viper.SetDefault("customize.pageTitle", "") diff --git a/internal/server/setting_test.go b/internal/server/setting_test.go index dbf479d9..2a4fd92f 100644 --- a/internal/server/setting_test.go +++ b/internal/server/setting_test.go @@ -146,6 +146,7 @@ func TestNewSetting_ConfigFile(t *testing.T) { assert.Empty(t, setting.Customize.WebsiteURL) assert.Empty(t, setting.Customize.WebsiteLogo) assert.False(t, setting.Customize.DisableKillCount) + assert.False(t, setting.Customize.HideMapFilters) }) t.Run("customize values from config", func(t *testing.T) { @@ -158,6 +159,7 @@ func TestNewSetting_ConfigFile(t *testing.T) { "websiteLogo": "/logo.png", "websiteLogoSize": "64px", "disableKillCount": true, + "hideMapFilters": true, "headerTitle": "My Community", "headerSubtitle": "After Action Reviews" } @@ -175,6 +177,7 @@ func TestNewSetting_ConfigFile(t *testing.T) { assert.Equal(t, "/logo.png", setting.Customize.WebsiteLogo) assert.Equal(t, "64px", setting.Customize.WebsiteLogoSize) assert.True(t, setting.Customize.DisableKillCount) + assert.True(t, setting.Customize.HideMapFilters) assert.Equal(t, "My Community", setting.Customize.HeaderTitle) assert.Equal(t, "After Action Reviews", setting.Customize.HeaderSubtitle) }) diff --git a/setting.json.example b/setting.json.example index 9b64c136..f8a1b900 100644 --- a/setting.json.example +++ b/setting.json.example @@ -14,6 +14,7 @@ "customize": { "disableKillCount": false, "enabled": false, + "hideMapFilters": false, "headerSubtitle": "", "headerTitle": "", "pageTitle": "", diff --git a/ui/src/data/apiClient.ts b/ui/src/data/apiClient.ts index 03434901..fb626566 100644 --- a/ui/src/data/apiClient.ts +++ b/ui/src/data/apiClient.ts @@ -20,6 +20,7 @@ export interface CustomizeConfig { websiteLogo?: string; websiteLogoSize?: string; disableKillCount?: boolean; + hideMapFilters?: boolean; headerTitle?: string; headerSubtitle?: string; pageTitle?: string; diff --git a/ui/src/hooks/__tests__/useCustomize.test.tsx b/ui/src/hooks/__tests__/useCustomize.test.tsx index fc86958f..3363dbdb 100644 --- a/ui/src/hooks/__tests__/useCustomize.test.tsx +++ b/ui/src/hooks/__tests__/useCustomize.test.tsx @@ -149,6 +149,27 @@ describe("useCustomize", () => { expect(parsed.websiteURL).toBeUndefined(); }); + it("honors hideMapFilters even when customize is not enabled", async () => { + mockGetCustomize.mockResolvedValue({ + enabled: false, + hideMapFilters: true, + websiteURL: "https://should-not-appear.com", + }); + + const { getByTestId } = render(() => ( + + {}} /> + + )); + + await vi.waitFor(() => { + const parsed = JSON.parse(getByTestId("config").textContent || "{}") as CustomizeConfig; + expect(parsed.hideMapFilters).toBe(true); + }); + const parsed = JSON.parse(getByTestId("config").textContent || "{}") as CustomizeConfig; + expect(parsed.websiteURL).toBeUndefined(); + }); + it("applies pageTitle to document.title and restores on unmount", async () => { const originalTitle = document.title; document.title = "OCAP2"; diff --git a/ui/src/hooks/useCustomize.tsx b/ui/src/hooks/useCustomize.tsx index 8bbe692f..f62a8940 100644 --- a/ui/src/hooks/useCustomize.tsx +++ b/ui/src/hooks/useCustomize.tsx @@ -24,11 +24,13 @@ export function CustomizeProvider(props: { const data = await api.getCustomize(); if (!mounted) return; if (!data.enabled) { - // disableKillCount is a privacy toggle, not a branding option, so - // honor it even when customize itself is not enabled. - if (data.disableKillCount) { - setConfig({ disableKillCount: true }); - } + // disableKillCount is a privacy toggle and hideMapFilters is a layout + // preference — neither is a branding option, so honor them even when + // customize itself is not enabled. + const fallback: CustomizeConfig = {}; + if (data.disableKillCount) fallback.disableKillCount = true; + if (data.hideMapFilters) fallback.hideMapFilters = true; + if (Object.keys(fallback).length > 0) setConfig(fallback); return; } setConfig(data); diff --git a/ui/src/pages/recording-selector/RecordingSelector.tsx b/ui/src/pages/recording-selector/RecordingSelector.tsx index d3e79fbe..9333888d 100644 --- a/ui/src/pages/recording-selector/RecordingSelector.tsx +++ b/ui/src/pages/recording-selector/RecordingSelector.tsx @@ -49,6 +49,9 @@ export function RecordingSelector(): JSX.Element { const worldDisplayName = (systemName: string) => worldNames().get(systemName) ?? systemName; + // Map filters are shown unless the instance opts out via customize.hideMapFilters + const showMapFilters = (): boolean => !customize().hideMapFilters; + let searchRef: HTMLInputElement | undefined; let scrollRef: HTMLDivElement | undefined; @@ -440,7 +443,7 @@ export function RecordingSelector(): JSX.Element { {/* Map filter dropdown */} - 1}> + 1}> { }); }); + it("shows the map filter dropdown by default", async () => { + const { findByTestId, queryByTestId } = renderPage(); + await findByTestId("recording-1"); + + expect(queryByTestId("map-filter-dropdown-trigger")).not.toBeNull(); + }); + + it("hides the map filter dropdown when customize.hideMapFilters is set", async () => { + const recordingsFetch = globalThis.fetch; + globalThis.fetch = vi.fn().mockImplementation((input: RequestInfo | URL) => { + const url = typeof input === "string" ? input : input.toString(); + if (url.includes("/customize")) { + return Promise.resolve({ + ok: true, + json: () => Promise.resolve({ enabled: true, hideMapFilters: true }), + } as Response); + } + return (recordingsFetch as ReturnType)(input); + }); + + const { findByTestId, queryByTestId } = renderPage(); + await findByTestId("recording-1"); + + await vi.waitFor(() => { + expect(queryByTestId("map-filter-dropdown-trigger")).toBeNull(); + }); + // Other filters are unaffected + expect(queryByTestId("search-input")).not.toBeNull(); + }); + // ── Clear filters ── it("shows clear button when filter is active and clears on click", async () => {