Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) | `{}` |
Expand Down
2 changes: 2 additions & 0 deletions internal/server/setting.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -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", "")
Expand Down
3 changes: 3 additions & 0 deletions internal/server/setting_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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"
}
Expand All @@ -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)
})
Expand Down
1 change: 1 addition & 0 deletions setting.json.example
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"customize": {
"disableKillCount": false,
"enabled": false,
"hideMapFilters": false,
"headerSubtitle": "",
"headerTitle": "",
"pageTitle": "",
Expand Down
1 change: 1 addition & 0 deletions ui/src/data/apiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export interface CustomizeConfig {
websiteLogo?: string;
websiteLogoSize?: string;
disableKillCount?: boolean;
hideMapFilters?: boolean;
headerTitle?: string;
headerSubtitle?: string;
pageTitle?: string;
Expand Down
21 changes: 21 additions & 0 deletions ui/src/hooks/__tests__/useCustomize.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => (
<CustomizeProvider>
<TestConsumer onConfig={() => {}} />
</CustomizeProvider>
));

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";
Expand Down
12 changes: 7 additions & 5 deletions ui/src/hooks/useCustomize.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
5 changes: 4 additions & 1 deletion ui/src/pages/recording-selector/RecordingSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -440,7 +443,7 @@ export function RecordingSelector(): JSX.Element {
</div>

{/* Map filter dropdown */}
<Show when={uniqueMaps().length > 1}>
<Show when={showMapFilters() && uniqueMaps().length > 1}>
<MapFilterDropdown
uniqueMaps={uniqueMaps()}
mapFilter={mapFilter()}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,36 @@ describe("RecordingSelector", () => {
});
});

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<typeof vi.fn>)(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 () => {
Expand Down
Loading