Allow using mode shorthands to filter map pool search results

This commit is contained in:
Kalle
2026-06-01 17:12:21 +03:00
parent 082e1a268f
commit 0cf88e80e5
2 changed files with 108 additions and 1 deletions

View File

@@ -0,0 +1,53 @@
import { describe, expect, test } from "vitest";
import { mapSearchFilter } from "./BracketMapListDialog";
const contains = (string: string, substring: string) =>
string.toLowerCase().includes(substring.toLowerCase());
const filter = (textValue: string, inputValue: string) =>
mapSearchFilter(textValue, inputValue, contains);
describe("mapSearchFilter", () => {
test("matches everything for empty input", () => {
expect(filter("SZ Hagglefish Market", "")).toBe(true);
expect(filter("TC Crableg Capital", " ")).toBe(true);
});
test("matches against stage name when no mode prefix is given", () => {
expect(filter("SZ Crableg Capital", "crab")).toBe(true);
expect(filter("TC Crableg Capital", "crab")).toBe(true);
expect(filter("SZ Hagglefish Market", "crab")).toBe(false);
});
test("mode prefix alone matches all maps of that mode", () => {
expect(filter("SZ Hagglefish Market", "sz")).toBe(true);
expect(filter("SZ Crableg Capital", "sz")).toBe(true);
expect(filter("TC Crableg Capital", "sz")).toBe(false);
});
test("mode prefix is case-insensitive", () => {
expect(filter("SZ Hagglefish Market", "Sz")).toBe(true);
expect(filter("SZ Hagglefish Market", "SZ")).toBe(true);
});
test("trailing whitespace after a mode prefix still matches all of that mode", () => {
expect(filter("SZ Hagglefish Market", "sz ")).toBe(true);
expect(filter("TC Hagglefish Market", "sz ")).toBe(false);
});
test("mode prefix combined with a query filters within that mode", () => {
expect(filter("SZ Crableg Capital", "sz crab")).toBe(true);
expect(filter("SZ Hagglefish Market", "sz crab")).toBe(false);
expect(filter("TC Crableg Capital", "sz crab")).toBe(false);
});
test("query after mode prefix is matched against the stage name", () => {
expect(filter("RM Museum d'Alfonsino", "rm museum")).toBe(true);
expect(filter("RM Museum d'Alfonsino", "rm market")).toBe(false);
});
test("non-mode first word falls back to stage name search", () => {
expect(filter("SZ Hagglefish Market", "hagg")).toBe(true);
expect(filter("SZ Hagglefish Market", "market")).toBe(true);
});
});

View File

@@ -8,6 +8,7 @@ import {
Unlink,
} from "lucide-react";
import * as React from "react";
import { useFilter } from "react-aria-components";
import { useTranslation } from "react-i18next";
import { type FetcherWithComponents, Link, useFetcher } from "react-router";
import { SendouDialog } from "~/components/elements/Dialog";
@@ -1072,6 +1073,7 @@ function MapListRow({
onMapChange: (map: NonNullable<TournamentRoundMaps["list"]>[number]) => void;
}) {
const { t } = useTranslation(["common", "game-misc"]);
const { contains } = useFilter({ sensitivity: "base" });
const tournament = useTournament();
const items = modesShort.flatMap((mode) => {
@@ -1118,13 +1120,20 @@ function MapListRow({
search={{
placeholder: t("common:forms.stageSearch.search.placeholder"),
}}
filter={(textValue, inputValue) =>
mapSearchFilter(textValue, inputValue, contains)
}
className={styles.mapRowSelect}
popoverClassName={styles.mapRowSelectPopover}
>
{(group) => (
<SendouSelectItemSection key={group.key} heading={group.modeLabel}>
{group.maps.map((m) => (
<SendouSelectItem key={m.id} id={m.id} textValue={m.name}>
<SendouSelectItem
key={m.id}
id={m.id}
textValue={`${m.mode} ${m.name}`}
>
<div className={styles.mapSelectItem}>
<ModeImage mode={m.mode} size={20} />
<StageImage
@@ -1143,6 +1152,51 @@ function MapListRow({
);
}
/**
* Filter for the map search input that supports an optional game mode prefix.
*
* - `"sz"` matches every Splat Zones map
* - `"sz crab"` matches Splat Zones maps whose stage name contains "crab"
* - input without a recognized mode prefix matches against the stage name as before
*
* Expects `textValue` to be formatted as `"${modeShort} ${stageName}"`.
*/
export function mapSearchFilter(
textValue: string,
inputValue: string,
contains: (string: string, substring: string) => boolean,
) {
const separatorIndex = textValue.indexOf(" ");
const mode = textValue.slice(0, separatorIndex);
const stageName = textValue.slice(separatorIndex + 1);
const trimmedInput = inputValue.trim();
const firstSpaceIndex = trimmedInput.indexOf(" ");
const firstWord =
firstSpaceIndex === -1
? trimmedInput
: trimmedInput.slice(0, firstSpaceIndex);
const matchedMode = modesShort.find(
(m) => m.toLowerCase() === firstWord.toLowerCase(),
);
if (!matchedMode) {
return contains(stageName, trimmedInput);
}
if (mode !== matchedMode) return false;
const restQuery =
firstSpaceIndex === -1
? ""
: trimmedInput.slice(firstSpaceIndex + 1).trim();
if (restQuery === "") return true;
return contains(stageName, restQuery);
}
function MysteryRow({
number,
isCounterpicks,