Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -2029,3 +2029,85 @@ describe("EngineGoalsClient — live heartbeat (G#154 AC1)", () => {
expect(refresh).not.toHaveBeenCalled();
});
});

// ── Below-lg: the All | Mine quick scope pill in the title row ─────────────────────────────
// A fast, one-tap ALIAS over the owner facet (settled decision (a): additive, not a
// replacement — the Owner facet inside the Controls menu / FiltersPopover is untouched).
// It lives in the PageBar right slot, left of the Controls trigger. Desktop (lg+) never
// renders it (the host gates it `lg:hidden`; jsdom ignores CSS, so these tests assert the
// wiring, not the media-query hiding).
describe("EngineGoalsClient — below-lg All | Mine scope pill", () => {
const ME = "user-me";

/** The PageBar right slot — where the below-lg compact toolbar lives. */
function rightSlot() {
return within(screen.getByTestId("page-bar-right-slot"));
}
/** The scope pill (a labelled group) within the right slot. */
function pill() {
return within(rightSlot().getByRole("group", { name: "Goal scope" }));
}
/** Open the Controls popover and return a `within` scope over its content — used to read
* back the owner facet's current value (the pill is a pure alias over it). */
function openControls() {
fireEvent.click(rightSlot().getByRole("button", { name: "Controls" }));
return within(screen.getByTestId("goals-controls-menu"));
}

it("renders the All | Mine pill in the right slot, left of the Controls trigger", () => {
renderClient([goal({ status: "open" })], ME);
const p = pill();
expect(p.getByRole("button", { name: "All" })).toBeInTheDocument();
expect(p.getByRole("button", { name: "Mine" })).toBeInTheDocument();
// The pill and the Controls trigger both live in the slot.
expect(rightSlot().getByRole("button", { name: "Controls" })).toBeInTheDocument();
});

it("reflects the store default (owner=currentUserId) as Mine active for a signed-in user", () => {
// The Goals page opens scoped to the signed-in user (useGoalsStore defaults owner to
// currentUserId), so the pill honestly shows Mine active, All inactive.
renderClient([goal({ status: "open" })], ME);
const p = pill();
expect(p.getByRole("button", { name: "Mine" })).toHaveAttribute("aria-pressed", "true");
expect(p.getByRole("button", { name: "All" })).toHaveAttribute("aria-pressed", "false");
});

it("clicking All widens the owner facet to 'all'", () => {
renderClient([goal({ status: "open" })], ME);
fireEvent.click(pill().getByRole("button", { name: "All" }));
// The pill reflects All active / Mine inactive…
expect(pill().getByRole("button", { name: "All" })).toHaveAttribute("aria-pressed", "true");
expect(pill().getByRole("button", { name: "Mine" })).toHaveAttribute("aria-pressed", "false");
// …and the underlying owner facet is the OWNER_ALL sentinel (the pill is a pure alias).
expect((openControls().getByLabelText("Filter by owner") as HTMLSelectElement).value).toBe("all");
});

it("clicking Mine narrows the owner facet to the current user", () => {
renderClient([goal({ status: "open" })], ME);
// First widen to All, then click Mine to narrow back to the signed-in user.
fireEvent.click(pill().getByRole("button", { name: "All" }));
fireEvent.click(pill().getByRole("button", { name: "Mine" }));
expect(pill().getByRole("button", { name: "Mine" })).toHaveAttribute("aria-pressed", "true");
expect(pill().getByRole("button", { name: "All" })).toHaveAttribute("aria-pressed", "false");
expect((openControls().getByLabelText("Filter by owner") as HTMLSelectElement).value).toBe(ME);
});

it("shows NEITHER segment active when a third-party teammate owns the facet", () => {
// A goal owned by someone else surfaces that teammate as an owner option.
renderClient([goal({ status: "open", ownerId: "teammate-2", ownerName: "Dana" })], ME);
// Select the teammate via the owner facet (the pill is only an alias for all/mine).
fireEvent.change(openControls().getByLabelText("Filter by owner"), {
target: { value: "teammate-2" },
});
expect(pill().getByRole("button", { name: "All" })).toHaveAttribute("aria-pressed", "false");
expect(pill().getByRole("button", { name: "Mine" })).toHaveAttribute("aria-pressed", "false");
});

it("does NOT offer Mine when currentUserId is the 'all'/empty sentinel", () => {
// renderClient defaults currentUserId to the "all" sentinel — no real signed-in id.
renderClient([goal({ status: "open" })]);
const p = pill();
expect(p.getByRole("button", { name: "All" })).toBeInTheDocument();
expect(p.queryByRole("button", { name: "Mine" })).not.toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import {
} from "@/components/attachments/use-attachment-upload";
import { GOAL_TYPE_OPTIONS, PRIORITY_OPTIONS, STATUS_FILTERS, type EngineGoalRow, type GoalBucketCounts, type GoalStatusFilter } from "./goals-view";
import { useGoalsStore } from "./use-goals-view";
import { FiltersPopover, GoalsControlsMenu, GoalsFilterBar, SORT_OPTIONS } from "./goals-filter-bar";
import { FiltersPopover, GoalsControlsMenu, GoalsFilterBar, GoalsScopePill, SORT_OPTIONS } from "./goals-filter-bar";
import { GoalsViewSwitcher } from "./goals-view-switcher";
import { GoalsListView, type GoalListRelease } from "./goals-list-view";
import { GoalsKanbanView } from "./goals-kanban-view";
Expand Down Expand Up @@ -440,6 +440,13 @@ export function EngineGoalsClient({
// (view switcher / sort / FiltersPopover / search) renders in the page body below.
const pageBarControls = useMemo(() => (
<div className="flex shrink-0 items-center gap-1.5">
<div className="lg:hidden">
<GoalsScopePill
filters={filters}
setFilter={setFilter}
currentUserId={currentUserId}
/>
</div>
<div className="lg:hidden">
<GoalsControlsMenu
view={view}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
GOAL_TYPE_OPTIONS,
GOAL_VIEWS,
MY_GOALS_LABEL,
OWNER_ALL,
PRIORITY_OPTIONS,
STATUS_FILTERS,
canFilterByMyGoals,
Expand Down Expand Up @@ -257,6 +258,60 @@ export function FiltersPopover({
);
}

/** BELOW-LG ONLY (<1024px). A compact segmented "All | Mine" scope pill for the Goals title
* row — a fast, one-tap ALIAS over the owner facet that already lives in the Controls menu
* (additive, not a replacement). "All" maps to the OWNER_ALL sentinel; "Mine" to the signed-in
* user's id. Neither segment is active when the owner facet is narrowed to some OTHER teammate
* or to Unassigned — the pill never lies about the current scope. "Mine" is only offered when a
* real current-user id exists (`canFilterByMyGoals`). Follows big & simple: single rounded-full
* container, ≥44px tall, #4CD964 active / #8E8E93 idle. Host gates it `lg:hidden`; desktop keeps
* the owner facet inside FiltersPopover untouched. */
export function GoalsScopePill({
filters,
setFilter,
currentUserId,
}: {
filters: GoalFilters;
setFilter: <K extends keyof GoalFilters>(key: K, value: GoalFilters[K]) => void;
currentUserId: string;
}) {
const hasMine = canFilterByMyGoals(currentUserId);
const allActive = filters.owner === OWNER_ALL;
const mineActive = hasMine && filters.owner === currentUserId;

const segment = (active: boolean) =>
`flex h-11 items-center justify-center rounded-full px-4 text-sm font-medium transition-colors ${
active ? "bg-[#4CD964] text-white" : "text-[#8E8E93] hover:bg-[#F5F5F5] hover:text-[#1C1C1E]"
}`;

return (
<div
role="group"
aria-label="Goal scope"
className="inline-flex h-11 shrink-0 items-center overflow-hidden rounded-full border border-gray-200 bg-white"
>
<button
type="button"
aria-pressed={allActive}
onClick={() => setFilter("owner", OWNER_ALL)}
className={segment(allActive)}
>
All
</button>
{hasMine && (
<button
type="button"
aria-pressed={mineActive}
onClick={() => setFilter("owner", currentUserId)}
className={segment(mineActive)}
>
Mine
</button>
)}
</div>
);
}

/** BELOW-LG ONLY (<1024px). A single "Controls" pill that opens one Radix popover grouping
* every goals-toolbar lever — View, Sort, Filter (status + facets), and Search — so the
* scarce mobile title row carries one trigger instead of a scattered cluster. Desktop keeps
Expand Down
Loading