diff --git a/src/app/issues/IssuesFilterForm.test.tsx b/src/app/issues/IssuesFilterForm.test.tsx
new file mode 100644
index 0000000..b2d1418
--- /dev/null
+++ b/src/app/issues/IssuesFilterForm.test.tsx
@@ -0,0 +1,133 @@
+import { render, fireEvent } from "@testing-library/react";
+import { IssuesFilterForm } from "./IssuesFilterForm";
+
+describe("IssuesFilterForm (#701)", () => {
+ beforeEach(() => {
+ jest.useFakeTimers();
+ });
+
+ afterEach(() => {
+ jest.useRealTimers();
+ });
+
+ it("disables default selects and empty inputs when submitted", () => {
+ const { container } = render(
+
+
+
+
+
+
+
+ ,
+ );
+
+ const form = container.querySelector("form")!;
+ const difficultySelect = container.querySelector('select[name="difficulty"]')!;
+ const assetSelect = container.querySelector('select[name="asset"]')!;
+ const minRewardInput = container.querySelector('input[name="minReward"]')!;
+ const maxRewardInput = container.querySelector('input[name="maxReward"]')!;
+ const sortSelect = container.querySelector('select[name="sort"]')!;
+
+ expect(difficultySelect.disabled).toBe(false);
+ expect(assetSelect.disabled).toBe(false);
+ expect(minRewardInput.disabled).toBe(false);
+ expect(maxRewardInput.disabled).toBe(false);
+ expect(sortSelect.disabled).toBe(false);
+
+ fireEvent.submit(form);
+
+ // On submit, default selects and empty inputs should be disabled so GET omits them
+ expect(difficultySelect.disabled).toBe(true);
+ expect(assetSelect.disabled).toBe(true);
+ expect(minRewardInput.disabled).toBe(true);
+ expect(maxRewardInput.disabled).toBe(true);
+ expect(sortSelect.disabled).toBe(true);
+
+ // After timer runs, fields re-enable for interactivity
+ jest.runAllTimers();
+ expect(difficultySelect.disabled).toBe(false);
+ expect(assetSelect.disabled).toBe(false);
+ expect(minRewardInput.disabled).toBe(false);
+ expect(maxRewardInput.disabled).toBe(false);
+ expect(sortSelect.disabled).toBe(false);
+ });
+
+ it("keeps non-default and non-empty inputs enabled on submit", () => {
+ const { container } = render(
+
+
+
+
+
+
+
+
+ ,
+ );
+
+ const form = container.querySelector("form")!;
+ const statusInput = container.querySelector('input[name="status"]')!;
+ const difficultySelect = container.querySelector('select[name="difficulty"]')!;
+ const assetSelect = container.querySelector('select[name="asset"]')!;
+ const minRewardInput = container.querySelector('input[name="minReward"]')!;
+ const maxRewardInput = container.querySelector('input[name="maxReward"]')!;
+ const sortSelect = container.querySelector('select[name="sort"]')!;
+
+ fireEvent.submit(form);
+
+ // Active filters remain enabled
+ expect(statusInput.disabled).toBe(false);
+ expect(difficultySelect.disabled).toBe(false);
+ expect(minRewardInput.disabled).toBe(false);
+ expect(sortSelect.disabled).toBe(false);
+
+ // Defaults and empty inputs are disabled
+ expect(assetSelect.disabled).toBe(true);
+ expect(maxRewardInput.disabled).toBe(true);
+ });
+
+ it("restores disabled elements immediately if submit is prevented", () => {
+ const onSubmit = jest.fn((e: React.FormEvent) => {
+ e.preventDefault();
+ });
+
+ const { container } = render(
+
+
+
+ ,
+ );
+
+ const form = container.querySelector("form")!;
+ const difficultySelect = container.querySelector('select[name="difficulty"]')!;
+ const minRewardInput = container.querySelector('input[name="minReward"]')!;
+
+ fireEvent.submit(form);
+
+ expect(onSubmit).toHaveBeenCalled();
+ // Because preventDefault was called, inputs should be restored immediately
+ expect(difficultySelect.disabled).toBe(false);
+ expect(minRewardInput.disabled).toBe(false);
+ });
+});
diff --git a/src/app/issues/IssuesFilterForm.tsx b/src/app/issues/IssuesFilterForm.tsx
new file mode 100644
index 0000000..2e673ed
--- /dev/null
+++ b/src/app/issues/IssuesFilterForm.tsx
@@ -0,0 +1,66 @@
+"use client";
+
+import type { ComponentPropsWithoutRef } from "react";
+
+const DEFAULT_FILTER_VALUES: Record = {
+ status: "all",
+ difficulty: "all",
+ asset: "all",
+ sort: "default",
+};
+
+/**
+ * Filter form that wraps the GET query submission on `/issues`.
+ *
+ * Disables empty inputs and default-valued select fields right before native
+ * form submission so the browser only appends active filters to the query string.
+ * This keeps the submitted URL clean and canonical, matching `buildIssuesHref` (#701).
+ */
+export function IssuesFilterForm({
+ onSubmit,
+ ...props
+}: ComponentPropsWithoutRef<"form">) {
+ return (
+
+
{visible.length === 0 ? (
filterActive ? (