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
3 changes: 3 additions & 0 deletions apps/mobile/src/app/(tabs)/inbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export default function InboxScreen() {
);
const sourceProductFilter = useInboxFilterStore((s) => s.sourceProductFilter);
const statusFilter = useInboxFilterStore((s) => s.statusFilter);
const priorityFilter = useInboxFilterStore((s) => s.priorityFilter);
const suggestedReviewerFilter = useInboxFilterStore(
(s) => s.suggestedReviewerFilter,
);
Expand Down Expand Up @@ -66,6 +67,7 @@ export default function InboxScreen() {
sourceProductFilter,
statusFilter,
suggestedReviewerFilter,
priorityFilter,
defaultStatusFilter: DEFAULT_STATUS_FILTER,
}),
);
Expand All @@ -78,6 +80,7 @@ export default function InboxScreen() {
sourceProductFilter,
statusFilter,
suggestedReviewerFilter,
priorityFilter,
]);

// ── Tinder mode data ──────────────────────────────────────────────────────
Expand Down
3 changes: 3 additions & 0 deletions apps/mobile/src/features/inbox/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ export async function getSignalReports(
if (params?.suggested_reviewers) {
url.searchParams.set("suggested_reviewers", params.suggested_reviewers);
}
if (params?.priority) {
url.searchParams.set("priority", params.priority);
}

const response = await authedFetch(url.toString());

Expand Down
44 changes: 43 additions & 1 deletion apps/mobile/src/features/inbox/components/FilterSheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
type SourceProduct,
useInboxFilterStore,
} from "../stores/inboxFilterStore";
import type { SignalReportStatus } from "../types";
import type { SignalReportPriority, SignalReportStatus } from "../types";
import { inboxStatusLabel } from "../utils";

interface FilterSheetProps {
Expand Down Expand Up @@ -49,6 +49,25 @@ function useStatusDotColors(): Record<string, string> {
};
}

const FILTERABLE_PRIORITIES: SignalReportPriority[] = [
"P0",
"P1",
"P2",
"P3",
"P4",
];

function usePriorityDotColors(): Record<SignalReportPriority, string> {
const themeColors = useThemeColors();
return {
P0: themeColors.status.error,
P1: themeColors.status.warning,
P2: themeColors.status.warning,
P3: themeColors.gray[9],
P4: themeColors.gray[9],
};
}

const SOURCE_PRODUCT_OPTIONS: { value: SourceProduct; label: string }[] = [
{ value: "session_replay", label: "Session replay" },
{ value: "error_tracking", label: "Error tracking" },
Expand Down Expand Up @@ -97,6 +116,7 @@ export function FilterSheet({ visible, onClose }: FilterSheetProps) {
const { bottom, sheetContentTop } = useScreenInsets();
const themeColors = useThemeColors();
const statusDotColors = useStatusDotColors();
const priorityDotColors = usePriorityDotColors();

const sortField = useInboxFilterStore((s) => s.sortField);
const sortDirection = useInboxFilterStore((s) => s.sortDirection);
Expand All @@ -105,10 +125,13 @@ export function FilterSheet({ visible, onClose }: FilterSheetProps) {
const toggleStatus = useInboxFilterStore((s) => s.toggleStatus);
const sourceProductFilter = useInboxFilterStore((s) => s.sourceProductFilter);
const toggleSourceProduct = useInboxFilterStore((s) => s.toggleSourceProduct);
const priorityFilter = useInboxFilterStore((s) => s.priorityFilter);
const togglePriority = useInboxFilterStore((s) => s.togglePriority);
const resetFilters = useInboxFilterStore((s) => s.resetFilters);

const hasActiveFilters =
sourceProductFilter.length > 0 ||
priorityFilter.length > 0 ||
statusFilter.length < FILTERABLE_STATUSES.length;

return (
Expand Down Expand Up @@ -186,6 +209,25 @@ export function FilterSheet({ visible, onClose }: FilterSheetProps) {
))}
</View>

{/* Priority */}
<SectionHeader title="Priority" />
<View className="mb-5">
{FILTERABLE_PRIORITIES.map((priority) => (
<OptionRow
key={priority}
label={priority}
selected={priorityFilter.includes(priority)}
onPress={() => togglePriority(priority)}
left={
<View
className="h-2.5 w-2.5 rounded-full"
style={{ backgroundColor: priorityDotColors[priority] }}
/>
}
/>
))}
</View>

{/* Source */}
<SectionHeader title="Source" />
<View className="mb-5">
Expand Down
3 changes: 3 additions & 0 deletions apps/mobile/src/features/inbox/hooks/useInboxReports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import type {
SuggestedReviewerWriteEntry,
} from "../types";
import {
buildPriorityFilterParam,
buildSignalReportListOrdering,
buildStatusFilterParam,
buildSuggestedReviewerFilterParam,
Expand All @@ -51,6 +52,7 @@ export function useInboxReports(options?: { enabled?: boolean }) {
const suggestedReviewerFilter = useInboxFilterStore(
(s) => s.suggestedReviewerFilter,
);
const priorityFilter = useInboxFilterStore((s) => s.priorityFilter);

const params: SignalReportsQueryParams = {
status: buildStatusFilterParam(statusFilter),
Expand All @@ -63,6 +65,7 @@ export function useInboxReports(options?: { enabled?: boolean }) {
suggestedReviewerFilter.length > 0
? buildSuggestedReviewerFilterParam(suggestedReviewerFilter)
: undefined,
priority: buildPriorityFilterParam(priorityFilter),
};

const query = useQuery<SignalReportsResponse>({
Expand Down
44 changes: 44 additions & 0 deletions apps/mobile/src/features/inbox/stores/inboxFilterStore.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { beforeEach, describe, expect, it } from "vitest";

import { useInboxFilterStore } from "./inboxFilterStore";

const INITIAL_STATE = useInboxFilterStore.getState();

beforeEach(() => {
useInboxFilterStore.setState(INITIAL_STATE, true);
});

describe("inboxFilterStore priority filter", () => {
it("starts empty (no priority filter)", () => {
expect(useInboxFilterStore.getState().priorityFilter).toEqual([]);
});

it("toggles a priority on and off", () => {
const { togglePriority } = useInboxFilterStore.getState();

togglePriority("P0");
expect(useInboxFilterStore.getState().priorityFilter).toEqual(["P0"]);

togglePriority("P0");
expect(useInboxFilterStore.getState().priorityFilter).toEqual([]);
});

it("accumulates multiple priorities", () => {
const { togglePriority } = useInboxFilterStore.getState();

togglePriority("P0");
togglePriority("P2");
expect(useInboxFilterStore.getState().priorityFilter).toEqual(["P0", "P2"]);
});

it("dedupes when set directly", () => {
useInboxFilterStore.getState().setPriorityFilter(["P1", "P1", "P3"]);
expect(useInboxFilterStore.getState().priorityFilter).toEqual(["P1", "P3"]);
});

it("clears the priority filter on reset", () => {
useInboxFilterStore.getState().setPriorityFilter(["P0", "P1"]);
useInboxFilterStore.getState().resetFilters();
expect(useInboxFilterStore.getState().priorityFilter).toEqual([]);
});
});
22 changes: 21 additions & 1 deletion apps/mobile/src/features/inbox/stores/inboxFilterStore.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import AsyncStorage from "@react-native-async-storage/async-storage";
import { create } from "zustand";
import { createJSONStorage, persist } from "zustand/middleware";
import type { SignalReportOrderingField, SignalReportStatus } from "../types";
import type {
SignalReportOrderingField,
SignalReportPriority,
SignalReportStatus,
} from "../types";

type SortField = Extract<
SignalReportOrderingField,
Expand Down Expand Up @@ -34,6 +38,7 @@ interface InboxFilterState {
statusFilter: SignalReportStatus[];
sourceProductFilter: SourceProduct[];
suggestedReviewerFilter: string[];
priorityFilter: SignalReportPriority[];
}

interface InboxFilterActions {
Expand All @@ -43,6 +48,8 @@ interface InboxFilterActions {
toggleSourceProduct: (source: SourceProduct) => void;
toggleSuggestedReviewer: (reviewerUuid: string) => void;
setSuggestedReviewerFilter: (reviewerUuids: string[]) => void;
togglePriority: (priority: SignalReportPriority) => void;
setPriorityFilter: (priorities: SignalReportPriority[]) => void;
resetFilters: () => void;
}

Expand All @@ -56,6 +63,7 @@ export const useInboxFilterStore = create<InboxFilterStore>()(
statusFilter: DEFAULT_STATUS_FILTER,
sourceProductFilter: [],
suggestedReviewerFilter: [],
priorityFilter: [],

setSort: (sortField, sortDirection) => set({ sortField, sortDirection }),
setStatusFilter: (statusFilter) => set({ statusFilter }),
Expand Down Expand Up @@ -88,11 +96,22 @@ export const useInboxFilterStore = create<InboxFilterStore>()(
set({
suggestedReviewerFilter: Array.from(new Set(reviewerUuids)),
}),
togglePriority: (priority) =>
set((state) => {
const current = state.priorityFilter;
const next = current.includes(priority)
? current.filter((p) => p !== priority)
: [...current, priority];
return { priorityFilter: next };
}),
setPriorityFilter: (priorities) =>
set({ priorityFilter: Array.from(new Set(priorities)) }),
resetFilters: () =>
set({
statusFilter: DEFAULT_STATUS_FILTER,
sourceProductFilter: [],
suggestedReviewerFilter: [],
priorityFilter: [],
}),
}),
{
Expand All @@ -104,6 +123,7 @@ export const useInboxFilterStore = create<InboxFilterStore>()(
statusFilter: state.statusFilter,
sourceProductFilter: state.sourceProductFilter,
suggestedReviewerFilter: state.suggestedReviewerFilter,
priorityFilter: state.priorityFilter,
}),
},
),
Expand Down
1 change: 1 addition & 0 deletions apps/mobile/src/features/inbox/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export interface SignalReportsQueryParams {
ordering?: string;
source_product?: string;
suggested_reviewers?: string;
priority?: string;
}

export interface SignalProcessingStateResponse {
Expand Down
40 changes: 39 additions & 1 deletion apps/mobile/src/features/inbox/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {
} from "./types";
import {
buildInboxViewedProperties,
buildPriorityFilterParam,
buildReviewerOptions,
reviewerMatchesAvailable,
toSuggestedReviewerWriteContent,
Expand Down Expand Up @@ -67,6 +68,7 @@ describe("buildInboxViewedProperties", () => {
sourceProductFilter: [],
statusFilter: DEFAULT_STATUS_FILTER,
suggestedReviewerFilter: [],
priorityFilter: [],
defaultStatusFilter: DEFAULT_STATUS_FILTER,
});
expect(props).toMatchObject({
Expand Down Expand Up @@ -116,6 +118,7 @@ describe("buildInboxViewedProperties", () => {
sourceProductFilter: [],
statusFilter: DEFAULT_STATUS_FILTER,
suggestedReviewerFilter: [],
priorityFilter: [],
defaultStatusFilter: DEFAULT_STATUS_FILTER,
});

Expand All @@ -131,11 +134,12 @@ describe("buildInboxViewedProperties", () => {
expect(props.actionability_unknown_count).toBe(1);
});

it("marks filters active when any of status/source/reviewer differs from defaults", () => {
it("marks filters active when any of status/source/reviewer/priority differs from defaults", () => {
const narrowed = buildInboxViewedProperties([], 0, {
sourceProductFilter: [],
statusFilter: ["ready"],
suggestedReviewerFilter: [],
priorityFilter: [],
defaultStatusFilter: DEFAULT_STATUS_FILTER,
});
expect(narrowed.has_active_filters).toBe(true);
Expand All @@ -145,6 +149,7 @@ describe("buildInboxViewedProperties", () => {
sourceProductFilter: ["error_tracking"],
statusFilter: DEFAULT_STATUS_FILTER,
suggestedReviewerFilter: [],
priorityFilter: [],
defaultStatusFilter: DEFAULT_STATUS_FILTER,
});
expect(sourced.has_active_filters).toBe(true);
Expand All @@ -154,16 +159,27 @@ describe("buildInboxViewedProperties", () => {
sourceProductFilter: [],
statusFilter: DEFAULT_STATUS_FILTER,
suggestedReviewerFilter: ["uuid-1"],
priorityFilter: [],
defaultStatusFilter: DEFAULT_STATUS_FILTER,
});
expect(reviewer.has_active_filters).toBe(true);

const prioritized = buildInboxViewedProperties([], 0, {
sourceProductFilter: [],
statusFilter: DEFAULT_STATUS_FILTER,
suggestedReviewerFilter: [],
priorityFilter: ["P0"],
defaultStatusFilter: DEFAULT_STATUS_FILTER,
});
expect(prioritized.has_active_filters).toBe(true);
});

it("treats a reordered default status set as not filtered", () => {
const props = buildInboxViewedProperties([], 0, {
sourceProductFilter: [],
statusFilter: [...DEFAULT_STATUS_FILTER].reverse(),
suggestedReviewerFilter: [],
priorityFilter: [],
defaultStatusFilter: DEFAULT_STATUS_FILTER,
});
expect(props.has_active_filters).toBe(false);
Expand Down Expand Up @@ -229,6 +245,28 @@ describe("reviewerMatchesAvailable", () => {
});
});

describe("buildPriorityFilterParam", () => {
it.each([
{
name: "returns undefined for an empty selection",
input: [],
expected: undefined,
},
{
name: "joins selected priorities with commas",
input: ["P0", "P2"] as const,
expected: "P0,P2",
},
{
name: "dedupes repeated priorities",
input: ["P1", "P1", "P3"] as const,
expected: "P1,P3",
},
])("$name", ({ input, expected }) => {
expect(buildPriorityFilterParam([...input])).toBe(expected);
});
});
Comment thread
Gilbert09 marked this conversation as resolved.

describe("buildReviewerOptions", () => {
it("dedupes by uuid and pins the current user first", () => {
const options = buildReviewerOptions(
Expand Down
Loading
Loading