Add multi-select machine filters

Replace machine dropdowns with multi-select chips and pass machine lists in queries.

Signed-off-by: codex@lucy.xalior.com
This commit is contained in:
2026-01-11 13:04:41 +00:00
parent 2f93ed1774
commit 1e8925e631
7 changed files with 193 additions and 65 deletions

View File

@@ -9,7 +9,7 @@ const querySchema = z.object({
year: z.coerce.number().int().optional(),
sort: z.enum(["year_desc", "year_asc", "title", "entry_id_desc"]).optional(),
dLanguageId: z.string().trim().length(2).optional(),
dMachinetypeId: z.coerce.number().int().positive().optional(),
dMachinetypeId: z.string().optional(),
filetypeId: z.coerce.number().int().positive().optional(),
schemetypeId: z.string().trim().length(2).optional(),
sourcetypeId: z.string().trim().length(1).optional(),
@@ -17,6 +17,15 @@ const querySchema = z.object({
isDemo: z.coerce.boolean().optional(),
});
function parseIdList(value: string | undefined) {
if (!value) return undefined;
const ids = value
.split(",")
.map((id) => Number(id.trim()))
.filter((id) => Number.isFinite(id) && id > 0);
return ids.length ? ids : undefined;
}
export async function GET(req: NextRequest) {
const { searchParams } = new URL(req.url);
const parsed = querySchema.safeParse({
@@ -39,7 +48,8 @@ export async function GET(req: NextRequest) {
headers: { "content-type": "application/json" },
});
}
const data = await searchReleases(parsed.data);
const dMachinetypeId = parseIdList(parsed.data.dMachinetypeId);
const data = await searchReleases({ ...parsed.data, dMachinetypeId });
return new Response(JSON.stringify(data), {
headers: { "content-type": "application/json" },
});

View File

@@ -12,12 +12,21 @@ const querySchema = z.object({
.trim()
.length(2, "languageId must be a 2-char code")
.optional(),
machinetypeId: z.coerce.number().int().positive().optional(),
machinetypeId: z.string().optional(),
sort: z.enum(["title", "id_desc"]).optional(),
scope: z.enum(["title", "title_aliases", "title_aliases_origins"]).optional(),
facets: z.coerce.boolean().optional(),
});
function parseIdList(value: string | undefined) {
if (!value) return undefined;
const ids = value
.split(",")
.map((id) => Number(id.trim()))
.filter((id) => Number.isFinite(id) && id > 0);
return ids.length ? ids : undefined;
}
export async function GET(req: NextRequest) {
const { searchParams } = new URL(req.url);
const parsed = querySchema.safeParse({
@@ -37,9 +46,11 @@ export async function GET(req: NextRequest) {
{ status: 400, headers: { "content-type": "application/json" } }
);
}
const data = await searchEntries(parsed.data);
const machinetypeId = parseIdList(parsed.data.machinetypeId);
const searchParamsParsed = { ...parsed.data, machinetypeId };
const data = await searchEntries(searchParamsParsed);
const body = parsed.data.facets
? { ...data, facets: await getEntryFacets(parsed.data) }
? { ...data, facets: await getEntryFacets(searchParamsParsed) }
: data;
return new Response(JSON.stringify(body), {
headers: { "content-type": "application/json" },

View File

@@ -52,11 +52,21 @@ export default function EntriesExplorer({
page: number;
genreId: string | number | "";
languageId: string | "";
machinetypeId: string | number | "";
machinetypeId: string;
sort: "title" | "id_desc";
scope?: SearchScope;
};
}) {
const preferredMachineIds = [27, 26, 8, 9];
const parseMachineIds = (value?: string) => {
if (!value) return preferredMachineIds.slice();
const ids = value
.split(",")
.map((id) => Number(id.trim()))
.filter((id) => Number.isFinite(id) && id > 0);
return ids.length ? ids : preferredMachineIds.slice();
};
const router = useRouter();
const pathname = usePathname();
@@ -72,17 +82,20 @@ export default function EntriesExplorer({
initialUrlState?.genreId === "" ? "" : initialUrlState?.genreId ? Number(initialUrlState.genreId) : ""
);
const [languageId, setLanguageId] = useState<string | "">(initialUrlState?.languageId ?? "");
const [machinetypeId, setMachinetypeId] = useState<number | "">(
initialUrlState?.machinetypeId === "" ? "" : initialUrlState?.machinetypeId ? Number(initialUrlState.machinetypeId) : ""
);
const [machinetypeIds, setMachinetypeIds] = useState<number[]>(parseMachineIds(initialUrlState?.machinetypeId));
const [sort, setSort] = useState<"title" | "id_desc">(initialUrlState?.sort ?? "id_desc");
const [scope, setScope] = useState<SearchScope>(initialUrlState?.scope ?? "title");
const [facets, setFacets] = useState<EntryFacets | null>(initialFacets ?? null);
const preferredMachineIds = [27, 26, 8, 9];
const preferredMachineNames = useMemo(() => {
if (!machines.length) return preferredMachineIds.map((id) => `#${id}`);
return preferredMachineIds.map((id) => machines.find((m) => m.id === id)?.name ?? `#${id}`);
}, [machines]);
const orderedMachines = useMemo(() => {
const seen = new Set(preferredMachineIds);
const preferred = preferredMachineIds.map((id) => machines.find((m) => m.id === id)).filter(Boolean) as { id: number; name: string }[];
const rest = machines.filter((m) => !seen.has(m.id));
return [...preferred, ...rest];
}, [machines]);
const pageSize = 20;
const totalPages = useMemo(() => (data ? Math.max(1, Math.ceil(data.total / data.pageSize)) : 1), [data]);
@@ -97,14 +110,14 @@ export default function EntriesExplorer({
const name = languages.find((l) => l.id === languageId)?.name ?? languageId;
chips.push(`lang: ${name}`);
}
if (machinetypeId !== "") {
const name = machines.find((m) => m.id === Number(machinetypeId))?.name ?? `#${machinetypeId}`;
chips.push(`machine: ${name}`);
if (machinetypeIds.length > 0) {
const names = machinetypeIds.map((id) => machines.find((m) => m.id === id)?.name ?? `#${id}`);
chips.push(`machine: ${names.join(", ")}`);
}
if (scope === "title_aliases") chips.push("scope: titles + aliases");
if (scope === "title_aliases_origins") chips.push("scope: titles + aliases + origins");
return chips;
}, [appliedQ, genreId, languageId, machinetypeId, scope, genres, languages, machines]);
}, [appliedQ, genreId, languageId, machinetypeIds, scope, genres, languages, machines]);
function updateUrl(nextPage = page) {
const params = new URLSearchParams();
@@ -112,7 +125,7 @@ export default function EntriesExplorer({
params.set("page", String(nextPage));
if (genreId !== "") params.set("genreId", String(genreId));
if (languageId !== "") params.set("languageId", String(languageId));
if (machinetypeId !== "") params.set("machinetypeId", String(machinetypeId));
if (machinetypeIds.length > 0) params.set("machinetypeId", machinetypeIds.join(","));
if (sort) params.set("sort", sort);
if (scope !== "title") params.set("scope", scope);
const qs = params.toString();
@@ -128,7 +141,7 @@ export default function EntriesExplorer({
params.set("pageSize", String(pageSize));
if (genreId !== "") params.set("genreId", String(genreId));
if (languageId !== "") params.set("languageId", String(languageId));
if (machinetypeId !== "") params.set("machinetypeId", String(machinetypeId));
if (machinetypeIds.length > 0) params.set("machinetypeId", machinetypeIds.join(","));
if (sort) params.set("sort", sort);
if (scope !== "title") params.set("scope", scope);
if (withFacets) params.set("facets", "true");
@@ -165,8 +178,7 @@ export default function EntriesExplorer({
(initialUrlState?.q ?? "") === appliedQ &&
(initialUrlState?.genreId === "" ? "" : Number(initialUrlState?.genreId ?? "")) === (genreId === "" ? "" : Number(genreId)) &&
(initialUrlState?.languageId ?? "") === (languageId ?? "") &&
(initialUrlState?.machinetypeId === "" ? "" : Number(initialUrlState?.machinetypeId ?? "")) ===
(machinetypeId === "" ? "" : Number(machinetypeId)) &&
parseMachineIds(initialUrlState?.machinetypeId).join(",") === machinetypeIds.join(",") &&
sort === (initialUrlState?.sort ?? "id_desc") &&
(initialUrlState?.scope ?? "title") === scope
) {
@@ -176,7 +188,7 @@ export default function EntriesExplorer({
updateUrl(page);
fetchData(appliedQ, page, true);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [page, genreId, languageId, machinetypeId, sort, scope, appliedQ]);
}, [page, genreId, languageId, machinetypeIds, sort, scope, appliedQ]);
// Load filter lists on mount only if not provided by server
useEffect(() => {
@@ -207,7 +219,7 @@ export default function EntriesExplorer({
setAppliedQ("");
setGenreId("");
setLanguageId("");
setMachinetypeId("");
setMachinetypeIds(preferredMachineIds.slice());
setSort("id_desc");
setScope("title");
setPage(1);
@@ -219,11 +231,11 @@ export default function EntriesExplorer({
params.set("page", String(Math.max(1, (data?.page ?? 1) - 1)));
if (genreId !== "") params.set("genreId", String(genreId));
if (languageId !== "") params.set("languageId", String(languageId));
if (machinetypeId !== "") params.set("machinetypeId", String(machinetypeId));
if (machinetypeIds.length > 0) params.set("machinetypeId", machinetypeIds.join(","));
if (sort) params.set("sort", sort);
if (scope !== "title") params.set("scope", scope);
return `/zxdb/entries?${params.toString()}`;
}, [appliedQ, data?.page, genreId, languageId, machinetypeId, sort, scope]);
}, [appliedQ, data?.page, genreId, languageId, machinetypeIds, sort, scope]);
const nextHref = useMemo(() => {
const params = new URLSearchParams();
@@ -231,11 +243,11 @@ export default function EntriesExplorer({
params.set("page", String(Math.max(1, (data?.page ?? 1) + 1)));
if (genreId !== "") params.set("genreId", String(genreId));
if (languageId !== "") params.set("languageId", String(languageId));
if (machinetypeId !== "") params.set("machinetypeId", String(machinetypeId));
if (machinetypeIds.length > 0) params.set("machinetypeId", machinetypeIds.join(","));
if (sort) params.set("sort", sort);
if (scope !== "title") params.set("scope", scope);
return `/zxdb/entries?${params.toString()}`;
}, [appliedQ, data?.page, genreId, languageId, machinetypeId, sort, scope]);
}, [appliedQ, data?.page, genreId, languageId, machinetypeIds, sort, scope]);
return (
<div>
@@ -303,15 +315,34 @@ export default function EntriesExplorer({
</div>
<div>
<label className="form-label small text-secondary">Machine</label>
<select className="form-select" value={machinetypeId} onChange={(e) => { setMachinetypeId(e.target.value === "" ? "" : Number(e.target.value)); setPage(1); }}>
<option value="">All machines</option>
{machines.map((m) => (
<option key={m.id} value={m.id}>{m.name}</option>
))}
</select>
{machinetypeId === "" && (
<div className="form-text">Preferred: {preferredMachineNames.join(", ")}</div>
)}
<div className="d-flex flex-wrap gap-2">
{orderedMachines.map((m) => {
const active = machinetypeIds.includes(m.id);
return (
<button
key={m.id}
type="button"
className={`btn btn-sm ${active ? "btn-primary" : "btn-outline-secondary"}`}
onClick={() => {
setMachinetypeIds((current) => {
const next = new Set(current);
if (next.has(m.id)) {
next.delete(m.id);
} else {
next.add(m.id);
}
const order = orderedMachines.map((item) => item.id);
return order.filter((id) => next.has(id));
});
setPage(1);
}}
>
{m.name}
</button>
);
})}
</div>
<div className="form-text">Preferred: {preferredMachineNames.join(", ")}</div>
</div>
<div>
<label className="form-label small text-secondary">Sort</label>

View File

@@ -7,12 +7,24 @@ export const metadata = {
export const dynamic = "force-dynamic";
function parseIdList(value: string | string[] | undefined) {
if (!value) return undefined;
const raw = Array.isArray(value) ? value.join(",") : value;
const ids = raw
.split(",")
.map((id) => Number(id.trim()))
.filter((id) => Number.isFinite(id) && id > 0);
return ids.length ? ids : undefined;
}
export default async function Page({ searchParams }: { searchParams: Promise<{ [key: string]: string | string[] | undefined }> }) {
const sp = await searchParams;
const page = Math.max(1, Number(Array.isArray(sp.page) ? sp.page[0] : sp.page) || 1);
const genreId = (Array.isArray(sp.genreId) ? sp.genreId[0] : sp.genreId) ?? "";
const languageId = (Array.isArray(sp.languageId) ? sp.languageId[0] : sp.languageId) ?? "";
const machinetypeId = (Array.isArray(sp.machinetypeId) ? sp.machinetypeId[0] : sp.machinetypeId) ?? "";
const preferredMachineIds = [27, 26, 8, 9];
const machinetypeIds = parseIdList(sp.machinetypeId) ?? preferredMachineIds;
const machinetypeId = machinetypeIds.join(",");
const sort = ((Array.isArray(sp.sort) ? sp.sort[0] : sp.sort) ?? "id_desc") as "title" | "id_desc";
const q = (Array.isArray(sp.q) ? sp.q[0] : sp.q) ?? "";
const scope = ((Array.isArray(sp.scope) ? sp.scope[0] : sp.scope) ?? "title") as
@@ -29,7 +41,7 @@ export default async function Page({ searchParams }: { searchParams: Promise<{ [
scope,
genreId: genreId ? Number(genreId) : undefined,
languageId: languageId || undefined,
machinetypeId: machinetypeId ? Number(machinetypeId) : undefined,
machinetypeId: machinetypeIds,
}),
listGenres(),
listLanguages(),
@@ -40,7 +52,7 @@ export default async function Page({ searchParams }: { searchParams: Promise<{ [
scope,
genreId: genreId ? Number(genreId) : undefined,
languageId: languageId || undefined,
machinetypeId: machinetypeId ? Number(machinetypeId) : undefined,
machinetypeId: machinetypeIds,
}),
]);

View File

@@ -51,6 +51,16 @@ export default function ReleasesExplorer({
casetypes: { id: string; name: string }[];
};
}) {
const preferredMachineIds = [27, 26, 8, 9];
const parseMachineIds = (value?: string) => {
if (!value) return preferredMachineIds.slice();
const ids = value
.split(",")
.map((id) => Number(id.trim()))
.filter((id) => Number.isFinite(id) && id > 0);
return ids.length ? ids : preferredMachineIds.slice();
};
const router = useRouter();
const pathname = usePathname();
@@ -64,7 +74,7 @@ export default function ReleasesExplorer({
// Download-based filters and their option lists
const [dLanguageId, setDLanguageId] = useState<string>(initialUrlState?.dLanguageId ?? "");
const [dMachinetypeId, setDMachinetypeId] = useState<string>(initialUrlState?.dMachinetypeId ?? "");
const [dMachinetypeIds, setDMachinetypeIds] = useState<number[]>(parseMachineIds(initialUrlState?.dMachinetypeId));
const [filetypeId, setFiletypeId] = useState<string>(initialUrlState?.filetypeId ?? "");
const [schemetypeId, setSchemetypeId] = useState<string>(initialUrlState?.schemetypeId ?? "");
const [sourcetypeId, setSourcetypeId] = useState<string>(initialUrlState?.sourcetypeId ?? "");
@@ -78,11 +88,16 @@ export default function ReleasesExplorer({
const [sources, setSources] = useState<{ id: string; name: string }[]>(initialLists?.sourcetypes ?? []);
const [cases, setCases] = useState<{ id: string; name: string }[]>(initialLists?.casetypes ?? []);
const initialLoad = useRef(true);
const preferredMachineIds = [27, 26, 8, 9];
const preferredMachineNames = useMemo(() => {
if (!machines.length) return preferredMachineIds.map((id) => `#${id}`);
return preferredMachineIds.map((id) => machines.find((m) => m.id === id)?.name ?? `#${id}`);
}, [machines]);
const orderedMachines = useMemo(() => {
const seen = new Set(preferredMachineIds);
const preferred = preferredMachineIds.map((id) => machines.find((m) => m.id === id)).filter(Boolean) as { id: number; name: string }[];
const rest = machines.filter((m) => !seen.has(m.id));
return [...preferred, ...rest];
}, [machines]);
const pageSize = 20;
const totalPages = useMemo(() => (data ? Math.max(1, Math.ceil(data.total / data.pageSize)) : 1), [data]);
@@ -94,7 +109,7 @@ export default function ReleasesExplorer({
if (year) params.set("year", year);
if (sort) params.set("sort", sort);
if (dLanguageId) params.set("dLanguageId", dLanguageId);
if (dMachinetypeId) params.set("dMachinetypeId", dMachinetypeId);
if (dMachinetypeIds.length > 0) params.set("dMachinetypeId", dMachinetypeIds.join(","));
if (filetypeId) params.set("filetypeId", filetypeId);
if (schemetypeId) params.set("schemetypeId", schemetypeId);
if (sourcetypeId) params.set("sourcetypeId", sourcetypeId);
@@ -114,7 +129,7 @@ export default function ReleasesExplorer({
if (year) params.set("year", String(Number(year)));
if (sort) params.set("sort", sort);
if (dLanguageId) params.set("dLanguageId", dLanguageId);
if (dMachinetypeId) params.set("dMachinetypeId", dMachinetypeId);
if (dMachinetypeIds.length > 0) params.set("dMachinetypeId", dMachinetypeIds.join(","));
if (filetypeId) params.set("filetypeId", filetypeId);
if (schemetypeId) params.set("schemetypeId", schemetypeId);
if (sourcetypeId) params.set("sourcetypeId", sourcetypeId);
@@ -148,7 +163,7 @@ export default function ReleasesExplorer({
(initialUrlState?.year ?? "") === (year ?? "") &&
sort === (initialUrlState?.sort ?? "year_desc") &&
(initialUrlState?.dLanguageId ?? "") === dLanguageId &&
(initialUrlState?.dMachinetypeId ?? "") === dMachinetypeId &&
parseMachineIds(initialUrlState?.dMachinetypeId).join(",") === dMachinetypeIds.join(",") &&
(initialUrlState?.filetypeId ?? "") === filetypeId &&
(initialUrlState?.schemetypeId ?? "") === schemetypeId &&
(initialUrlState?.sourcetypeId ?? "") === sourcetypeId &&
@@ -168,7 +183,7 @@ export default function ReleasesExplorer({
}
updateUrl(page);
fetchData(appliedQ, page);
}, [page, year, sort, dLanguageId, dMachinetypeId, filetypeId, schemetypeId, sourcetypeId, casetypeId, isDemo, appliedQ]);
}, [page, year, sort, dLanguageId, dMachinetypeIds, filetypeId, schemetypeId, sourcetypeId, casetypeId, isDemo, appliedQ]);
function onSubmit(e: React.FormEvent) {
e.preventDefault();
@@ -209,14 +224,14 @@ export default function ReleasesExplorer({
if (year) params.set("year", year);
if (sort) params.set("sort", sort);
if (dLanguageId) params.set("dLanguageId", dLanguageId);
if (dMachinetypeId) params.set("dMachinetypeId", dMachinetypeId);
if (dMachinetypeIds.length > 0) params.set("dMachinetypeId", dMachinetypeIds.join(","));
if (filetypeId) params.set("filetypeId", filetypeId);
if (schemetypeId) params.set("schemetypeId", schemetypeId);
if (sourcetypeId) params.set("sourcetypeId", sourcetypeId);
if (casetypeId) params.set("casetypeId", casetypeId);
if (isDemo) params.set("isDemo", "1");
return `/zxdb/releases?${params.toString()}`;
}, [appliedQ, data?.page, year, sort, dLanguageId, dMachinetypeId, filetypeId, schemetypeId, sourcetypeId, casetypeId, isDemo]);
}, [appliedQ, data?.page, year, sort, dLanguageId, dMachinetypeIds, filetypeId, schemetypeId, sourcetypeId, casetypeId, isDemo]);
const nextHref = useMemo(() => {
const params = new URLSearchParams();
@@ -225,14 +240,14 @@ export default function ReleasesExplorer({
if (year) params.set("year", year);
if (sort) params.set("sort", sort);
if (dLanguageId) params.set("dLanguageId", dLanguageId);
if (dMachinetypeId) params.set("dMachinetypeId", dMachinetypeId);
if (dMachinetypeIds.length > 0) params.set("dMachinetypeId", dMachinetypeIds.join(","));
if (filetypeId) params.set("filetypeId", filetypeId);
if (schemetypeId) params.set("schemetypeId", schemetypeId);
if (sourcetypeId) params.set("sourcetypeId", sourcetypeId);
if (casetypeId) params.set("casetypeId", casetypeId);
if (isDemo) params.set("isDemo", "1");
return `/zxdb/releases?${params.toString()}`;
}, [appliedQ, data?.page, year, sort, dLanguageId, dMachinetypeId, filetypeId, schemetypeId, sourcetypeId, casetypeId, isDemo]);
}, [appliedQ, data?.page, year, sort, dLanguageId, dMachinetypeIds, filetypeId, schemetypeId, sourcetypeId, casetypeId, isDemo]);
return (
<div>
@@ -291,15 +306,34 @@ export default function ReleasesExplorer({
</div>
<div>
<label className="form-label small text-secondary">DL Machine</label>
<select className="form-select" value={dMachinetypeId} onChange={(e) => { setDMachinetypeId(e.target.value); setPage(1); }}>
<option value="">All machines</option>
{machines.map((m) => (
<option key={m.id} value={m.id}>{m.name}</option>
))}
</select>
{dMachinetypeId === "" && (
<div className="form-text">Preferred: {preferredMachineNames.join(", ")}</div>
)}
<div className="d-flex flex-wrap gap-2">
{orderedMachines.map((m) => {
const active = dMachinetypeIds.includes(m.id);
return (
<button
key={m.id}
type="button"
className={`btn btn-sm ${active ? "btn-primary" : "btn-outline-secondary"}`}
onClick={() => {
setDMachinetypeIds((current) => {
const next = new Set(current);
if (next.has(m.id)) {
next.delete(m.id);
} else {
next.add(m.id);
}
const order = orderedMachines.map((item) => item.id);
return order.filter((id) => next.has(id));
});
setPage(1);
}}
>
{m.name}
</button>
);
})}
</div>
<div className="form-text">Preferred: {preferredMachineNames.join(", ")}</div>
</div>
<div>
<label className="form-label small text-secondary">File type</label>

View File

@@ -7,6 +7,16 @@ export const metadata = {
export const dynamic = "force-dynamic";
function parseIdList(value: string | string[] | undefined) {
if (!value) return undefined;
const raw = Array.isArray(value) ? value.join(",") : value;
const ids = raw
.split(",")
.map((id) => Number(id.trim()))
.filter((id) => Number.isFinite(id) && id > 0);
return ids.length ? ids : undefined;
}
export default async function Page({ searchParams }: { searchParams: Promise<{ [key: string]: string | string[] | undefined }> }) {
const sp = await searchParams;
const hasParams = Object.values(sp).some((value) => value !== undefined);
@@ -16,8 +26,9 @@ export default async function Page({ searchParams }: { searchParams: Promise<{ [
const year = yearStr ? Number(yearStr) : undefined;
const sort = ((Array.isArray(sp.sort) ? sp.sort[0] : sp.sort) ?? "year_desc") as "year_desc" | "year_asc" | "title" | "entry_id_desc";
const dLanguageId = (Array.isArray(sp.dLanguageId) ? sp.dLanguageId[0] : sp.dLanguageId) ?? "";
const dMachinetypeIdStr = (Array.isArray(sp.dMachinetypeId) ? sp.dMachinetypeId[0] : sp.dMachinetypeId) ?? "";
const dMachinetypeId = dMachinetypeIdStr ? Number(dMachinetypeIdStr) : undefined;
const preferredMachineIds = [27, 26, 8, 9];
const dMachinetypeIds = parseIdList(sp.dMachinetypeId) ?? preferredMachineIds;
const dMachinetypeIdStr = dMachinetypeIds.join(",");
const filetypeIdStr = (Array.isArray(sp.filetypeId) ? sp.filetypeId[0] : sp.filetypeId) ?? "";
const filetypeId = filetypeIdStr ? Number(filetypeIdStr) : undefined;
const schemetypeId = (Array.isArray(sp.schemetypeId) ? sp.schemetypeId[0] : sp.schemetypeId) ?? "";
@@ -27,7 +38,7 @@ export default async function Page({ searchParams }: { searchParams: Promise<{ [
const isDemo = isDemoStr ? (isDemoStr === "true" || isDemoStr === "1") : undefined;
const [initial, langs, machines, filetypes, schemes, sources, cases] = await Promise.all([
searchReleases({ page, pageSize: 20, q, year, sort, dLanguageId: dLanguageId || undefined, dMachinetypeId, filetypeId, schemetypeId: schemetypeId || undefined, sourcetypeId: sourcetypeId || undefined, casetypeId: casetypeId || undefined, isDemo }),
searchReleases({ page, pageSize: 20, q, year, sort, dLanguageId: dLanguageId || undefined, dMachinetypeId: dMachinetypeIds, filetypeId, schemetypeId: schemetypeId || undefined, sourcetypeId: sourcetypeId || undefined, casetypeId: casetypeId || undefined, isDemo }),
listLanguages(),
listMachinetypes(),
listFiletypes(),