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
46 changes: 39 additions & 7 deletions apps/mobile/src/features/home/HomeHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,12 @@ import {
} from "./home-list-filter-menu";
import {
hasCustomHomeListOptions,
OWNERSHIP_FILTER_LABELS,
OWNERSHIP_FILTERS,
OWNERSHIP_RELATION_LABELS,
OWNERSHIP_RELATIONS,
type OwnershipFilter,
type OwnershipRelation,
PROJECT_SORT_OPTIONS,
THREAD_SORT_OPTIONS,
} from "./home-list-options";
Expand Down Expand Up @@ -58,6 +63,7 @@ export function HomeHeader(props: {
readonly selectedEnvironmentIds: readonly EnvironmentId[];
readonly selectedProjectKey: string | null;
readonly ownershipFilter: OwnershipFilter;
readonly ownershipRelation: OwnershipRelation;
/**
* Hide settled from the main Threads inbox. Recency/none default on;
* project grouping defaults off at the call site.
Expand All @@ -72,6 +78,7 @@ export function HomeHeader(props: {
readonly onToggleEnvironment: (environmentId: EnvironmentId) => void;
readonly onProjectChange: (projectKey: string | null) => void;
readonly onOwnershipFilterChange: (filter: OwnershipFilter) => void;
readonly onOwnershipRelationChange: (relation: OwnershipRelation) => void;
readonly onHideSettledThreadsChange: (hide: boolean) => void;
readonly onProjectSortOrderChange: (sortOrder: HomeProjectSortOrder) => void;
readonly onThreadSortOrderChange: (sortOrder: SidebarThreadSortOrder) => void;
Expand Down Expand Up @@ -110,6 +117,7 @@ function AndroidHomeHeader(props: HomeHeaderProps) {
const hasCustomListOptions =
props.selectedEnvironmentIds.length > 0 ||
props.ownershipFilter !== "any" ||
props.ownershipRelation !== "both" ||
props.selectedProjectKey !== null ||
(props.listMode === "threads" &&
props.hideSettledThreads !== defaultHideSettledForGrouping(props.threadGrouping)) ||
Expand All @@ -118,6 +126,7 @@ function AndroidHomeHeader(props: HomeHeaderProps) {
hasCustomHomeListOptions({
selectedEnvironmentIds: props.selectedEnvironmentIds,
ownershipFilter: props.ownershipFilter,
ownershipRelation: props.ownershipRelation,
listMode: props.listMode,
threadGrouping: props.threadGrouping,
projectSortOrder: props.projectSortOrder,
Expand Down Expand Up @@ -147,15 +156,25 @@ function AndroidHomeHeader(props: HomeHeaderProps) {
{
id: "ownership",
title: "Ownership",
subactions: [
{ id: "ownership:any", title: "Anyone" },
{ id: "ownership:mine", title: "Mine" },
{ id: "ownership:theirs", title: "Theirs" },
].map((action) => ({
...action,
state: checkedMenuState(action.id === `ownership:${props.ownershipFilter}`),
subactions: OWNERSHIP_FILTERS.map((value) => ({
id: `ownership:${value}`,
title: OWNERSHIP_FILTER_LABELS[value],
state: checkedMenuState(value === props.ownershipFilter),
})),
},
...(props.ownershipFilter === "mine" || props.ownershipFilter === "theirs"
? ([
{
id: "ownership-relation",
title: props.ownershipFilter === "mine" ? "Mine includes" : "Theirs includes",
subactions: OWNERSHIP_RELATIONS.map((value) => ({
id: `ownership-relation:${value}`,
title: OWNERSHIP_RELATION_LABELS[value],
state: checkedMenuState(value === props.ownershipRelation),
})),
},
] satisfies MenuAction[])
: []),
...(props.projects.length === 0 || props.listMode === "board"
? []
: ([
Expand Down Expand Up @@ -223,6 +242,7 @@ function AndroidHomeHeader(props: HomeHeaderProps) {
props.hideSettledThreads,
props.listMode,
props.ownershipFilter,
props.ownershipRelation,
props.projectSortOrder,
props.projects,
props.selectedEnvironmentIds,
Expand Down Expand Up @@ -250,6 +270,14 @@ function AndroidHomeHeader(props: HomeHeaderProps) {
return;
}

if (id.startsWith("ownership-relation:")) {
const relation = id.slice("ownership-relation:".length);
if (relation === "created" || relation === "participated" || relation === "both") {
props.onOwnershipRelationChange(relation);
}
return;
}

if (id.startsWith("ownership:")) {
const ownership = id.slice("ownership:".length);
if (ownership === "any" || ownership === "mine" || ownership === "theirs") {
Expand Down Expand Up @@ -421,6 +449,7 @@ function IosHomeHeader(props: HomeHeaderProps) {
const hasCustomListOptions =
props.selectedEnvironmentIds.length > 0 ||
props.ownershipFilter !== "any" ||
props.ownershipRelation !== "both" ||
props.selectedProjectKey !== null ||
(props.listMode === "threads" &&
props.hideSettledThreads !== defaultHideSettledForGrouping(props.threadGrouping)) ||
Expand All @@ -429,6 +458,7 @@ function IosHomeHeader(props: HomeHeaderProps) {
hasCustomHomeListOptions({
selectedEnvironmentIds: props.selectedEnvironmentIds,
ownershipFilter: props.ownershipFilter,
ownershipRelation: props.ownershipRelation,
listMode: props.listMode,
threadGrouping: props.threadGrouping,
projectSortOrder: props.projectSortOrder,
Expand All @@ -446,12 +476,14 @@ function IosHomeHeader(props: HomeHeaderProps) {
selectedEnvironmentIds: props.selectedEnvironmentIds,
selectedProjectKey: props.selectedProjectKey,
ownershipFilter: props.ownershipFilter,
ownershipRelation: props.ownershipRelation,
projectSortOrder: props.projectSortOrder,
threadSortOrder: props.threadSortOrder,
onClearEnvironments: props.onClearEnvironments,
onToggleEnvironment: props.onToggleEnvironment,
onProjectChange: props.onProjectChange,
onOwnershipFilterChange: props.onOwnershipFilterChange,
onOwnershipRelationChange: props.onOwnershipRelationChange,
onProjectSortOrderChange: props.onProjectSortOrderChange,
onThreadSortOrderChange: props.onThreadSortOrderChange,
listOrganization,
Expand Down
11 changes: 10 additions & 1 deletion apps/mobile/src/features/home/HomeRouteScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ export function HomeRouteScreen() {
toggleSelectedEnvironmentId,
clearSelectedEnvironments,
setOwnershipFilter,
setOwnershipRelation,
setListMode,
setThreadGrouping,
setProjectSortOrder,
Expand All @@ -91,9 +92,15 @@ export function HomeRouteScreen() {
(participant) => participant.personId,
),
mode: listOptions.ownershipFilter,
relation: listOptions.ownershipRelation,
}),
),
[claimPersonIdByEnvironment, listOptions.ownershipFilter, threads],
[
claimPersonIdByEnvironment,
listOptions.ownershipFilter,
listOptions.ownershipRelation,
threads,
],
);
const preferencesResult = useAtomValue(mobilePreferencesAtom);
const savePreferences = useAtomSet(updateMobilePreferencesAtom);
Expand Down Expand Up @@ -175,6 +182,7 @@ export function HomeRouteScreen() {
selectedEnvironmentIds={selectedEnvironmentIds}
selectedProjectKey={selectedProjectKey}
ownershipFilter={listOptions.ownershipFilter}
ownershipRelation={listOptions.ownershipRelation}
hideSettledThreads={hideSettledThreads}
projectSortOrder={listOptions.projectSortOrder}
threadSortOrder={listOptions.threadSortOrder}
Expand All @@ -184,6 +192,7 @@ export function HomeRouteScreen() {
onToggleEnvironment={toggleSelectedEnvironmentId}
onProjectChange={setSelectedProjectKey}
onOwnershipFilterChange={setOwnershipFilter}
onOwnershipRelationChange={setOwnershipRelation}
onHideSettledThreadsChange={setHideSettledThreads}
onOpenSettings={() => navigation.navigate("SettingsSheet", { screen: "Settings" })}
onProjectSortOrderChange={setProjectSortOrder}
Expand Down
132 changes: 81 additions & 51 deletions apps/mobile/src/features/home/home-list-filter-menu.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,42 @@ import { describe, expect, it, vi } from "vite-plus/test";

import { buildHomeListFilterMenu } from "./home-list-filter-menu";

function baseProps(
overrides: Partial<Parameters<typeof buildHomeListFilterMenu>[0]> = {},
): Parameters<typeof buildHomeListFilterMenu>[0] {
return {
environments: [],
projects: [],
selectedEnvironmentIds: [],
selectedProjectKey: null,
ownershipFilter: "any",
ownershipRelation: "both",
projectSortOrder: "updated_at",
threadSortOrder: "updated_at",
onClearEnvironments: vi.fn(),
onToggleEnvironment: vi.fn(),
onProjectChange: vi.fn(),
onOwnershipFilterChange: vi.fn(),
onOwnershipRelationChange: vi.fn(),
onProjectSortOrderChange: vi.fn(),
onThreadSortOrderChange: vi.fn(),
...overrides,
};
}

describe("buildHomeListFilterMenu", () => {
it("adds a project scope submenu that selects and clears the same scope as the chips", () => {
const onProjectChange = vi.fn();
const menu = buildHomeListFilterMenu({
environments: [],
projects: [
{ key: "environment-1:project-1", label: "Codething" },
{ key: "environment-1:project-2", label: "Website" },
],
selectedEnvironmentIds: [],
selectedProjectKey: "environment-1:project-1",
ownershipFilter: "any",
projectSortOrder: "updated_at",
threadSortOrder: "updated_at",
onClearEnvironments: vi.fn(),
onToggleEnvironment: vi.fn(),
onProjectChange,
onOwnershipFilterChange: vi.fn(),
onProjectSortOrderChange: vi.fn(),
onThreadSortOrderChange: vi.fn(),
});
const menu = buildHomeListFilterMenu(
baseProps({
projects: [
{ key: "environment-1:project-1", label: "Codething" },
{ key: "environment-1:project-2", label: "Website" },
],
selectedProjectKey: "environment-1:project-1",
onProjectChange,
}),
);

const projectMenu = menu.items.find(
(item) => item.type === "submenu" && item.title === "Project",
Expand All @@ -47,24 +62,17 @@ describe("buildHomeListFilterMenu", () => {
it("supports multi-select environment toggles", () => {
const onToggleEnvironment = vi.fn();
const onClearEnvironments = vi.fn();
const menu = buildHomeListFilterMenu({
environments: [
{ environmentId: "env-1" as never, label: "Smart" },
{ environmentId: "env-2" as never, label: "t3vm" },
],
projects: [],
selectedEnvironmentIds: ["env-1" as never],
selectedProjectKey: null,
ownershipFilter: "any",
projectSortOrder: "updated_at",
threadSortOrder: "updated_at",
onClearEnvironments,
onToggleEnvironment,
onProjectChange: vi.fn(),
onOwnershipFilterChange: vi.fn(),
onProjectSortOrderChange: vi.fn(),
onThreadSortOrderChange: vi.fn(),
});
const menu = buildHomeListFilterMenu(
baseProps({
environments: [
{ environmentId: "env-1" as never, label: "Smart" },
{ environmentId: "env-2" as never, label: "t3vm" },
],
selectedEnvironmentIds: ["env-1" as never],
onClearEnvironments,
onToggleEnvironment,
}),
);

const environmentMenu = menu.items.find(
(item) => item.type === "submenu" && item.title === "Environment",
Expand All @@ -86,21 +94,12 @@ describe("buildHomeListFilterMenu", () => {

it("offers Anyone, Mine, and Theirs ownership filters", () => {
const onOwnershipFilterChange = vi.fn();
const menu = buildHomeListFilterMenu({
environments: [],
projects: [],
selectedEnvironmentIds: [],
selectedProjectKey: null,
ownershipFilter: "mine",
projectSortOrder: "updated_at",
threadSortOrder: "updated_at",
onClearEnvironments: vi.fn(),
onToggleEnvironment: vi.fn(),
onProjectChange: vi.fn(),
onOwnershipFilterChange,
onProjectSortOrderChange: vi.fn(),
onThreadSortOrderChange: vi.fn(),
});
const menu = buildHomeListFilterMenu(
baseProps({
ownershipFilter: "mine",
onOwnershipFilterChange,
}),
);

const ownershipMenu = menu.items.find(
(item) => item.type === "submenu" && item.title === "Ownership",
Expand All @@ -117,4 +116,35 @@ describe("buildHomeListFilterMenu", () => {
ownershipMenu.items[2]?.onPress();
expect(onOwnershipFilterChange).toHaveBeenCalledWith("theirs");
});

it("offers created / participated / both sub-filters when Mine or Theirs is selected", () => {
const onOwnershipRelationChange = vi.fn();
const menu = buildHomeListFilterMenu(
baseProps({
ownershipFilter: "mine",
ownershipRelation: "created",
onOwnershipRelationChange,
}),
);

const relationMenu = menu.items.find(
(item) => item.type === "submenu" && item.title === "Mine includes",
);
expect(relationMenu).toMatchObject({
type: "submenu",
items: [
{ title: "Created or participated", state: "off" },
{ title: "Created", state: "on" },
{ title: "Participated", state: "off" },
],
});
if (relationMenu?.type !== "submenu") throw new Error("Expected relation submenu");
relationMenu.items[2]?.onPress();
expect(onOwnershipRelationChange).toHaveBeenCalledWith("participated");

const anyoneMenu = buildHomeListFilterMenu(baseProps({ ownershipFilter: "any" }));
expect(
anyoneMenu.items.some((item) => item.type === "submenu" && item.title === "Mine includes"),
).toBe(false);
});
});
Loading
Loading