diff --git a/apps/mobile/src/features/pull-requests/PullRequestListFilters.tsx b/apps/mobile/src/features/pull-requests/PullRequestListFilters.tsx index be9b6ccff0e7..cffa039c864f 100644 --- a/apps/mobile/src/features/pull-requests/PullRequestListFilters.tsx +++ b/apps/mobile/src/features/pull-requests/PullRequestListFilters.tsx @@ -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"; @@ -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>, selected: string) { return Object.entries(options).map(([value, title]) => ({ @@ -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 ( + 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) => ( + + )} + + ); + } return ( [environment.id, environment.label]), - ]), - props.environmentId ?? "", - )} + actions={environmentActions} onSelect={select} /> ) : null} @@ -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} /> [login, login]), - ]), - author ?? "", - )} + actions={authorActions} onSelect={select} /> diff --git a/apps/mobile/src/features/pull-requests/PullRequestToolbar.android.tsx b/apps/mobile/src/features/pull-requests/PullRequestToolbar.android.tsx index 9b320fb26e7b..e13e60eb6958 100644 --- a/apps/mobile/src/features/pull-requests/PullRequestToolbar.android.tsx +++ b/apps/mobile/src/features/pull-requests/PullRequestToolbar.android.tsx @@ -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, @@ -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; }) { @@ -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", @@ -126,8 +118,34 @@ export function PullRequestToolbar(props: { screen: "SettingsContent", params: { screen: "Settings" }, }), + overflowOnly: true, }, ] + : [] + } + trailing={ + <> + {!searching ? ( + + ) : null} + {props.filters} + {!searching && !props.sidebar ? ( + + navigation.navigate("SettingsSheet", { + screen: "SettingsContent", + params: { screen: "Settings" }, + }) + } + /> + ) : null} + } /> ); diff --git a/apps/mobile/src/features/pull-requests/PullRequestsScreen.tsx b/apps/mobile/src/features/pull-requests/PullRequestsScreen.tsx index 226d13cd40d9..a85486e1fdd0 100644 --- a/apps/mobile/src/features/pull-requests/PullRequestsScreen.tsx +++ b/apps/mobile/src/features/pull-requests/PullRequestsScreen.tsx @@ -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"; @@ -61,6 +63,7 @@ export function PullRequestsScreen( props: { readonly sidebar?: boolean; readonly selectedPullRequestKey?: string | null; + readonly onRequestVisibility?: () => void; } = {}, ) { const navigation = useNavigation(); @@ -210,7 +213,37 @@ export function PullRequestsScreen( ? entries.findIndex((entry) => pullRequestKey(entry) === initialAnchor.key) : -1; - return ( + const filters = ( + ({ + 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 = ( - ({ - 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 ? ( {result.data.errors @@ -373,4 +373,15 @@ export function PullRequestsScreen( /> ); + if (Platform.OS !== "android") return content; + return ( + <> + + {content} + + ); } diff --git a/apps/mobile/src/features/workspace/AndroidWorkspaceSidebar.android.tsx b/apps/mobile/src/features/workspace/AndroidWorkspaceSidebar.android.tsx index f59d31037f6e..2121044223ef 100644 --- a/apps/mobile/src/features/workspace/AndroidWorkspaceSidebar.android.tsx +++ b/apps/mobile/src/features/workspace/AndroidWorkspaceSidebar.android.tsx @@ -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: { @@ -17,10 +15,11 @@ export function AndroidWorkspaceSidebar(props: { ) : ( - - - - + ); diff --git a/apps/mobile/src/features/workspace/WorkspaceRootScreen.android.tsx b/apps/mobile/src/features/workspace/WorkspaceRootScreen.android.tsx index 367ee3acbff8..264ac94b361b 100644 --- a/apps/mobile/src/features/workspace/WorkspaceRootScreen.android.tsx +++ b/apps/mobile/src/features/workspace/WorkspaceRootScreen.android.tsx @@ -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"; @@ -53,12 +51,7 @@ export function WorkspaceRootScreen() { {workspace.state.selectedTab === "threads" ? ( ) : ( - <> - - - - - + )}