Skip to content
Open
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
105 changes: 70 additions & 35 deletions apps/mobile/src/features/pull-requests/PullRequestListFilters.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { sortProjectsByGroupOrder } from "@t3tools/client-runtime/state/project-grouping";
import { useMemo } from "react";
import { Platform, ScrollView } from "react-native";
import { AndroidAnchoredMenu } from "../../components/ui/AndroidAnchoredMenu";
import { AndroidHeaderIconButton } from "../../components/ui/AndroidScreenHeader";
import type { MenuProps } from "../../components/ui/Menu.types";
import { ListFilterMenu } from "../workspace/ListFilterMenu";
import { useProjects, useThreadShells } from "../../state/entities";
Expand All @@ -24,12 +26,7 @@ const SORTS = {
created: "Newest created",
oldest: "Oldest updated",
} as const;
// On Android the row sits under ScreenContent's 28dp corner. A 12dp inset keeps the
// 16dp chip radius concentric with it (28 - 12 = 16).
const ROW_STYLE =
Platform.OS === "android"
? { paddingHorizontal: 12, paddingTop: 12, paddingBottom: 8, gap: 8 }
: { paddingHorizontal: 16, paddingTop: 2, paddingBottom: 10, gap: 8 };
const ROW_STYLE = { paddingHorizontal: 16, paddingTop: 2, paddingBottom: 10, gap: 8 };
const id = (field: string, value: string) => JSON.stringify([field, value]);
function choices(field: string, options: Readonly<Record<string, string>>, selected: string) {
return Object.entries(options).map(([value, title]) => ({
Expand Down Expand Up @@ -227,6 +224,70 @@ export function PullRequestListFilters(props: {
const authors = [
...new Set([...(author && author !== "me" ? [author] : []), ...props.authors]),
].slice(0, 60);
const environmentActions = choices(
"environment",
Object.fromEntries([
["", "All environments"],
...props.environments.map((environment) => [environment.id, environment.label]),
]),
props.environmentId ?? "",
);
const labelActions: MenuProps["actions"] = [
{
id: id("labels", ""),
title: "All labels",
state: selectedLabels.length ? "off" : "on",
},
...labels.map((label) => ({
id: id("label", label),
title: label,
state: selectedLabelSet.has(label) ? ("on" as const) : ("off" as const),
})),
];
const authorActions = choices(
"author",
Object.fromEntries([
["", "All authors"],
["me", "Me"],
...authors.map((login) => [login, login]),
]),
author ?? "",
);
if (Platform.OS === "android") {
const active =
count > 0 ||
value.sort !== "updated" ||
(props.environments.length > 1 && props.environmentId !== null);
return (
<AndroidAnchoredMenu
title="Filter pull requests"
actions={[
...(props.environments.length > 1
? [{ id: "environments", title: "Environment", subactions: environmentActions }]
: []),
{ id: "states", title: "State", subactions: choices("state", STATES, value.state) },
{
id: "involvement",
title: "Involvement",
subactions: choices("involvement", INVOLVEMENT, value.involvement),
},
{ id: "labels", title: "Labels", subactions: labelActions },
{ id: "authors", title: "Author", subactions: authorActions },
...more,
]}
onPressAction={({ nativeEvent }) => select(nativeEvent.event)}
>
{(open) => (
<AndroidHeaderIconButton
accessibilityLabel="Filter pull requests"
icon={active ? "line.3.horizontal.decrease.circle.fill" : "line.3.horizontal.decrease"}
selected={active}
onPress={open}
/>
)}
</AndroidAnchoredMenu>
);
}
return (
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={ROW_STYLE}>
<ListFilterMenu
Expand All @@ -244,14 +305,7 @@ export function PullRequestListFilters(props: {
?.label ?? "All environments"
}
accessibilityLabel="Choose pull request environment"
actions={choices(
"environment",
Object.fromEntries([
["", "All environments"],
...props.environments.map((environment) => [environment.id, environment.label]),
]),
props.environmentId ?? "",
)}
actions={environmentActions}
onSelect={select}
/>
) : null}
Expand All @@ -273,33 +327,14 @@ export function PullRequestListFilters(props: {
label={selectedLabels.length ? `Labels ${selectedLabels.length}` : "Labels"}
active={selectedLabels.length > 0}
accessibilityLabel="Filter by label"
actions={[
{
id: id("labels", ""),
title: "All labels",
state: selectedLabels.length ? "off" : "on",
},
...labels.map((label) => ({
id: id("label", label),
title: label,
state: selectedLabelSet.has(label) ? ("on" as const) : ("off" as const),
})),
]}
actions={labelActions}
onSelect={select}
/>
<ListFilterMenu
label={author === "me" ? "By you" : (author ?? "Author")}
active={Boolean(author)}
accessibilityLabel="Filter by author"
actions={choices(
"author",
Object.fromEntries([
["", "All authors"],
["me", "Me"],
...authors.map((login) => [login, login]),
]),
author ?? "",
)}
actions={authorActions}
onSelect={select}
/>
</ScrollView>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useNavigation, useNavigationState } from "@react-navigation/native";
import { useCallback, useEffect, useRef } from "react";
import { useCallback, useEffect, useRef, type ReactNode } from "react";
import { BackHandler, Keyboard, View, type TextInputInstance } from "react-native";
import {
AndroidHeaderIconButton,
Expand All @@ -14,6 +14,7 @@ import { useWorkspaceListNavigation } from "../workspace/useAndroidWorkspaceNavi

/** PR search shares the thread list's native Android header placement. */
export function PullRequestToolbar(props: {
readonly filters: ReactNode;
readonly sidebar?: boolean;
readonly onRequestVisibility?: () => void;
}) {
Expand Down Expand Up @@ -101,22 +102,13 @@ export function PullRequestToolbar(props: {
) : undefined
}
actions={
searching
? []
: [
...(props.sidebar
? [
{
accessibilityLabel: "Show threads",
icon: "text.bubble",
onPress: showThreads,
} as const,
]
: []),
!searching && props.sidebar
? [
{
accessibilityLabel: "Search pull requests",
icon: "magnifyingglass",
onPress: openSearch,
accessibilityLabel: "Show threads",
icon: "text.bubble",
onPress: showThreads,
overflowOnly: true,
},
{
accessibilityLabel: "Open settings",
Expand All @@ -126,8 +118,34 @@ export function PullRequestToolbar(props: {
screen: "SettingsContent",
params: { screen: "Settings" },
}),
overflowOnly: true,
},
]
: []
}
trailing={
<>
{!searching ? (
<AndroidHeaderIconButton
accessibilityLabel="Search pull requests"
icon="magnifyingglass"
onPress={openSearch}
/>
) : null}
{props.filters}
{!searching && !props.sidebar ? (
<AndroidHeaderIconButton
accessibilityLabel="Open settings"
icon="gearshape"
onPress={() =>
navigation.navigate("SettingsSheet", {
screen: "SettingsContent",
params: { screen: "Settings" },
})
}
/>
) : null}
</>
}
/>
);
Expand Down
81 changes: 46 additions & 35 deletions apps/mobile/src/features/pull-requests/PullRequestsScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,14 @@ import { useSafeAreaInsets } from "react-native-safe-area-context";

import { AppText as Text } from "../../components/ui/AppText";
import { EmptyState } from "../../components/ui/EmptyState";
import { ScreenContent } from "../../components/ui/ScreenContent";
import { NativePrimaryColumnContext } from "../../native/v5-workspace-context";
import { useWorkspaceState } from "../../state/workspace";
import { useProjects } from "../../state/entities";
import { environmentServerConfigsAtom } from "../../state/server";
import { PullRequestListRow } from "./PullRequestListRow";
import { PullRequestListFilters } from "./PullRequestListFilters";
import { PullRequestToolbar } from "./PullRequestToolbar.android";
import { usePullRequestList } from "./usePullRequestList";
import { usePullRequestListControls } from "./usePullRequestListControls";
import { usePullRequestListActions } from "./usePullRequestListActions";
Expand Down Expand Up @@ -61,6 +63,7 @@ export function PullRequestsScreen(
props: {
readonly sidebar?: boolean;
readonly selectedPullRequestKey?: string | null;
readonly onRequestVisibility?: () => void;
} = {},
) {
const navigation = useNavigation();
Expand Down Expand Up @@ -210,7 +213,37 @@ export function PullRequestsScreen(
? entries.findIndex((entry) => pullRequestKey(entry) === initialAnchor.key)
: -1;

return (
const filters = (
<PullRequestListFilters
environments={environments.map((item) => ({
id: String(item.environmentId),
label: item.environmentLabel,
}))}
environmentId={environmentId}
onEnvironmentChange={listControls.setEnvironmentId}
value={
typedDraft ? { ...controls, filters: { ...controls.filters, draft: typedDraft } } : controls
}
onChange={(next) => {
const update = applyPullRequestDraftFilterChange(
typedQuery,
controls.filters,
next.filters,
);
listControls.setControls(environmentId, { ...next, filters: update.filters });
if (update.query !== typedQuery)
workspace.dispatch({ type: "query", tab: "pullRequests", query: update.query });
void listRef.current?.scrollToOffset({
offset: Platform.OS === "ios" ? -insets.top : 0,
animated: false,
});
}}
authors={knownFacets.environmentId === environmentId ? knownFacets.authors : facets.authors}
labels={knownFacets.environmentId === environmentId ? knownFacets.labels : facets.labels}
projects={projects}
/>
);
const content = (
<View className={sidebar ? "flex-1 bg-drawer" : "flex-1 bg-screen"}>
<LegendList
ref={listRef}
Expand Down Expand Up @@ -272,40 +305,7 @@ export function PullRequestsScreen(
renderItem={renderPullRequest}
ListHeaderComponent={
<>
<PullRequestListFilters
environments={environments.map((item) => ({
id: String(item.environmentId),
label: item.environmentLabel,
}))}
environmentId={environmentId}
onEnvironmentChange={listControls.setEnvironmentId}
value={
typedDraft
? { ...controls, filters: { ...controls.filters, draft: typedDraft } }
: controls
}
onChange={(next) => {
const update = applyPullRequestDraftFilterChange(
typedQuery,
controls.filters,
next.filters,
);
listControls.setControls(environmentId, { ...next, filters: update.filters });
if (update.query !== typedQuery)
workspace.dispatch({ type: "query", tab: "pullRequests", query: update.query });
void listRef.current?.scrollToOffset({
offset: Platform.OS === "ios" ? -insets.top : 0,
animated: false,
});
}}
authors={
knownFacets.environmentId === environmentId ? knownFacets.authors : facets.authors
}
labels={
knownFacets.environmentId === environmentId ? knownFacets.labels : facets.labels
}
projects={projects}
/>
{Platform.OS !== "android" ? filters : null}
{result.data?.errors.length ? (
<Text className="px-4 py-3 text-sm text-foreground-muted">
{result.data.errors
Expand Down Expand Up @@ -373,4 +373,15 @@ export function PullRequestsScreen(
/>
</View>
);
if (Platform.OS !== "android") return content;
return (
<>
<PullRequestToolbar
sidebar={sidebar}
filters={filters}
onRequestVisibility={props.onRequestVisibility}
/>
<ScreenContent insetHorizontal={sidebar}>{content}</ScreenContent>
</>
);
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { useMemo, type ReactNode } from "react";
import { View } from "react-native";
import { ScreenContent } from "../../components/ui/ScreenContent";
import { PullRequestsScreen } from "../pull-requests/PullRequestsScreen";
import { PullRequestToolbar } from "../pull-requests/PullRequestToolbar.android";
import { WorkspaceTabsContext, useWorkspaceTabs } from "./WorkspaceTabsContext";

export function AndroidWorkspaceSidebar(props: {
Expand All @@ -17,10 +15,11 @@ export function AndroidWorkspaceSidebar(props: {
) : (
<WorkspaceTabsContext value={context}>
<View className="flex-1 bg-header">
<PullRequestToolbar sidebar onRequestVisibility={props.onRequestVisibility} />
<ScreenContent insetHorizontal>
<PullRequestsScreen sidebar selectedPullRequestKey={props.selectedPullRequestKey} />
</ScreenContent>
<PullRequestsScreen
sidebar
selectedPullRequestKey={props.selectedPullRequestKey}
onRequestVisibility={props.onRequestVisibility}
/>
</View>
</WorkspaceTabsContext>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,9 @@ import { SafeAreaInsetsContext, useSafeAreaInsets } from "react-native-safe-area
import { NativeStackScreenOptions } from "../../native/StackHeader";
import { AndroidScreenHeader } from "../../components/ui/AndroidScreenHeader";
import { EmptyState } from "../../components/ui/EmptyState";
import { ScreenContent } from "../../components/ui/ScreenContent";
import { useAndroidControlSizing } from "../../components/ui/useAndroidControlSizing";
import { HomeRouteScreen } from "../home/HomeRouteScreen";
import { PullRequestsScreen } from "../pull-requests/PullRequestsScreen";
import { PullRequestToolbar } from "../pull-requests/PullRequestToolbar.android";
import { useAdaptiveWorkspaceLayout } from "../layout/AdaptiveWorkspaceLayout";
import { AndroidWorkspaceNavigation } from "./AndroidWorkspaceNavigation";
import { WorkspaceTabsContext, useWorkspaceTabs } from "./WorkspaceTabsContext";
Expand Down Expand Up @@ -53,12 +51,7 @@ export function WorkspaceRootScreen() {
{workspace.state.selectedTab === "threads" ? (
<HomeRouteScreen />
) : (
<>
<PullRequestToolbar />
<ScreenContent>
<PullRequestsScreen />
</ScreenContent>
</>
<PullRequestsScreen />
)}
</View>
</SafeAreaInsetsContext>
Expand Down
Loading