| ... | @@ -1,5 +1,5 @@ | ... | @@ -1,5 +1,5 @@ |
| 1 | import { MaterialListRow } from "../../components/MaterialListRow"; | 1 | import { MaterialListRow } from "../../components/MaterialListRow"; |
| 2 | import { NativeHeaderToolbar, NativeStackScreenOptions } from "../../native/StackHeader"; | 2 | import { ScreenHeader } from "../../components/ScreenHeader"; |
| 3 | import { | 3 | import { |
| 4 | StackActions, | 4 | StackActions, |
| 5 | useIsFocused, | 5 | useIsFocused, |
| ... | @@ -8,11 +8,10 @@ import { | ... | @@ -8,11 +8,10 @@ import { |
| 8 | } from "@react-navigation/native"; | 8 | } from "@react-navigation/native"; |
| 9 | import { SymbolView } from "../../components/AppSymbol"; | 9 | import { SymbolView } from "../../components/AppSymbol"; |
| 10 | import type { EnvironmentProject } from "@t3tools/client-runtime/state/shell"; | 10 | import type { EnvironmentProject } from "@t3tools/client-runtime/state/shell"; |
| 11 | import { useEffect, useRef } from "react"; | 11 | import { useEffect, useRef, useState } from "react"; |
| 12 | import { ActivityIndicator, Alert, Platform, Pressable, View } from "react-native"; | 12 | import { ActivityIndicator, Alert, Platform, Pressable, View } from "react-native"; |
| 13 | import { useSafeAreaInsets } from "react-native-safe-area-context"; | 13 | import { useSafeAreaInsets } from "react-native-safe-area-context"; |
| 14 | import { cn } from "../../lib/cn"; | 14 | import { cn } from "../../lib/cn"; |
| 15 | import { AndroidScreenHeader } from "../../components/AndroidScreenHeader"; | | |
| 16 | import { MaterialScreenContent } from "../../components/MaterialScreenContent"; | 15 | import { MaterialScreenContent } from "../../components/MaterialScreenContent"; |
| 17 | import { MaterialButton } from "../../components/MaterialButton"; | 16 | import { MaterialButton } from "../../components/MaterialButton"; |
| 18 | import { ScreenScrollView as ScrollView } from "../../components/ScreenScrollView"; | 17 | import { ScreenScrollView as ScrollView } from "../../components/ScreenScrollView"; |
| ... | @@ -24,7 +23,7 @@ import { useWorkspaceState } from "../../state/workspace"; | ... | @@ -24,7 +23,7 @@ import { useWorkspaceState } from "../../state/workspace"; |
| 24 | import { useAdaptiveWorkspaceLayout } from "../layout/AdaptiveWorkspaceLayout"; | 23 | import { useAdaptiveWorkspaceLayout } from "../layout/AdaptiveWorkspaceLayout"; |
| 25 | import { useIncomingShare } from "../sharing/IncomingShareProvider"; | 24 | import { useIncomingShare } from "../sharing/IncomingShareProvider"; |
| 26 | import { useNewTaskFlow } from "./new-task-flow-provider"; | 25 | import { useNewTaskFlow } from "./new-task-flow-provider"; |
| 27 | import { getProjectScopeSelectionTarget } from "./new-task-project-selection"; | 26 | import { filterProjectScopes, getProjectScopeSelectionTarget } from "./new-task-project-selection"; |
| 28 | | 27 | |
| 29 | type NewTaskRouteParams = { | 28 | type NewTaskRouteParams = { |
| 30 | readonly incomingShareId?: string | string[]; | 29 | readonly incomingShareId?: string | string[]; |
| ... | @@ -85,13 +84,58 @@ function deriveProjectEmptyState(catalogState: WorkspaceState): { | ... | @@ -85,13 +84,58 @@ function deriveProjectEmptyState(catalogState: WorkspaceState): { |
| 85 | }; | 84 | }; |
| 86 | } | 85 | } |
| 87 | | 86 | |
| | 87 | function NewTaskHeader(props: { |
| | 88 | readonly title: string; |
| | 89 | readonly subtitle: string | null; |
| | 90 | readonly canAddProject: boolean; |
| | 91 | readonly searchText: string; |
| | 92 | readonly onSearchTextChange: (text: string) => void; |
| | 93 | }) { |
| | 94 | const navigation = useNavigation(); |
| | 95 | const { layout } = useAdaptiveWorkspaceLayout(); |
| | 96 | return ( |
| | 97 | <ScreenHeader |
| | 98 | title={props.title} |
| | 99 | subtitle={props.subtitle ?? undefined} |
| | 100 | sidebar={false} |
| | 101 | hideBottomBorder |
| | 102 | onBack={() => navigation.goBack()} |
| | 103 | actions={[ |
| | 104 | ...(Platform.OS === "ios" && layout.usesSplitView |
| | 105 | ? [ |
| | 106 | { |
| | 107 | accessibilityLabel: "Close new task", |
| | 108 | icon: "xmark" as const, |
| | 109 | onPress: () => navigation.goBack(), |
| | 110 | }, |
| | 111 | ] |
| | 112 | : []), |
| | 113 | ...(props.canAddProject |
| | 114 | ? [ |
| | 115 | { |
| | 116 | accessibilityLabel: "Add project", |
| | 117 | icon: "plus" as const, |
| | 118 | onPress: () => navigation.dispatch(StackActions.push("AddProject")), |
| | 119 | }, |
| | 120 | ] |
| | 121 | : []), |
| | 122 | ]} |
| | 123 | search={{ |
| | 124 | value: props.searchText, |
| | 125 | onChangeText: props.onSearchTextChange, |
| | 126 | placeholder: "Search projects", |
| | 127 | }} |
| | 128 | /> |
| | 129 | ); |
| | 130 | } |
| | 131 | |
| 88 | export function NewTaskRouteScreen({ route }: StaticScreenProps<NewTaskRouteParams | undefined>) { | 132 | export function NewTaskRouteScreen({ route }: StaticScreenProps<NewTaskRouteParams | undefined>) { |
| 89 | const projects = useProjects(); | 133 | const projects = useProjects(); |
| | 134 | const [searchText, setSearchText] = useState(""); |
| 90 | const { projectScopes, selectedEnvironmentId, setProject } = useNewTaskFlow(); | 135 | const { projectScopes, selectedEnvironmentId, setProject } = useNewTaskFlow(); |
| 91 | const { state: catalogState } = useWorkspaceState(); | 136 | const { state: catalogState } = useWorkspaceState(); |
| 92 | const navigation = useNavigation(); | 137 | const navigation = useNavigation(); |
| 93 | const isFocused = useIsFocused(); | 138 | const isFocused = useIsFocused(); |
| 94 | const { layout } = useAdaptiveWorkspaceLayout(); | | |
| 95 | const insets = useSafeAreaInsets(); | 139 | const insets = useSafeAreaInsets(); |
| 96 | const { getShare, releaseShareReservation } = useIncomingShare(); | 140 | const { getShare, releaseShareReservation } = useIncomingShare(); |
| 97 | const routeShareId = Array.isArray(route.params?.incomingShareId) | 141 | const routeShareId = Array.isArray(route.params?.incomingShareId) |
| ... | @@ -107,6 +151,7 @@ export function NewTaskRouteScreen({ route }: StaticScreenProps<NewTaskRoutePara | ... | @@ -107,6 +151,7 @@ export function NewTaskRouteScreen({ route }: StaticScreenProps<NewTaskRoutePara |
| 107 | : null; | 151 | : null; |
| 108 | const screenTitle = incomingShare ? "Start a task" : "Choose project"; | 152 | const screenTitle = incomingShare ? "Start a task" : "Choose project"; |
| 109 | const projectEmptyState = deriveProjectEmptyState(catalogState); | 153 | const projectEmptyState = deriveProjectEmptyState(catalogState); |
| | 154 | const visibleScopes = filterProjectScopes(projectScopes, searchText); |
| 110 | const resumedDestinationKeyRef = useRef<string | null>(null); | 155 | const resumedDestinationKeyRef = useRef<string | null>(null); |
| 111 | const reservedDestinationProject = incomingShare?.destination | 156 | const reservedDestinationProject = incomingShare?.destination |
| 112 | ? (projects.find( | 157 | ? (projects.find( |
| ... | @@ -180,67 +225,27 @@ export function NewTaskRouteScreen({ route }: StaticScreenProps<NewTaskRoutePara | ... | @@ -180,67 +225,27 @@ export function NewTaskRouteScreen({ route }: StaticScreenProps<NewTaskRoutePara |
| 180 | | 225 | |
| 181 | return ( | 226 | return ( |
| 182 | <View collapsable={false} className="flex-1 bg-sheet"> | 227 | <View collapsable={false} className="flex-1 bg-sheet"> |
| 183 | {Platform.OS === "android" ? ( | 228 | <NewTaskHeader |
| 184 | <> | 229 | title={screenTitle} |
| 185 | {/* Android renders its own in-screen header instead of the native bar. */} | 230 | subtitle={incomingShareSubtitle} |
| 186 | <NativeStackScreenOptions options={{ headerShown: false }} /> | 231 | canAddProject={catalogState.hasReadyEnvironment} |
| 187 | <AndroidScreenHeader | 232 | searchText={searchText} |
| 188 | title={screenTitle} | 233 | onSearchTextChange={setSearchText} |
| 189 | hideBottomBorder | 234 | /> |
| 190 | subtitle={incomingShareSubtitle} | | |
| 191 | onBack={() => navigation.goBack()} | | |
| 192 | actions={ | | |
| 193 | catalogState.hasReadyEnvironment | | |
| 194 | ? [ | | |
| 195 | { | | |
| 196 | accessibilityLabel: "Add project", | | |
| 197 | icon: "plus", | | |
| 198 | onPress: () => navigation.dispatch(StackActions.push("AddProject")), | | |
| 199 | }, | | |
| 200 | ] | | |
| 201 | : [] | | |
| 202 | } | | |
| 203 | /> | | |
| 204 | </> | | |
| 205 | ) : ( | | |
| 206 | <> | | |
| 207 | <NativeStackScreenOptions | | |
| 208 | options={{ | | |
| 209 | title: screenTitle, | | |
| 210 | unstable_headerSubtitle: incomingShareSubtitle ?? undefined, | | |
| 211 | }} | | |
| 212 | /> | | |
| 213 | <NativeHeaderToolbar placement="right"> | | |
| 214 | {layout.usesSplitView ? ( | | |
| 215 | <NativeHeaderToolbar.Button | | |
| 216 | accessibilityLabel="Close new task" | | |
| 217 | icon="xmark" | | |
| 218 | onPress={() => navigation.goBack()} | | |
| 219 | separateBackground | | |
| 220 | /> | | |
| 221 | ) : null} | | |
| 222 | {catalogState.hasReadyEnvironment ? ( | | |
| 223 | <NativeHeaderToolbar.Button | | |
| 224 | icon="plus" | | |
| 225 | onPress={() => navigation.dispatch(StackActions.push("AddProject"))} | | |
| 226 | separateBackground | | |
| 227 | /> | | |
| 228 | ) : null} | | |
| 229 | </NativeHeaderToolbar> | | |
| 230 | </> | | |
| 231 | )} | | |
| 232 | | 235 | |
| 233 | <MaterialScreenContent> | 236 | <MaterialScreenContent> |
| 234 | <ScrollView | 237 | <ScrollView |
| 235 | contentInsetAdjustmentBehavior="automatic" | 238 | contentInsetAdjustmentBehavior="automatic" |
| 236 | showsVerticalScrollIndicator={false} | 239 | showsVerticalScrollIndicator={false} |
| | 240 | keyboardShouldPersistTaps="handled" |
| | 241 | keyboardDismissMode="on-drag" |
| 237 | className="flex-1" | 242 | className="flex-1" |
| 238 | contentContainerStyle={{ | 243 | contentContainerStyle={{ |
| 239 | gap: Platform.OS === "android" ? 8 : 12, | 244 | gap: Platform.OS === "android" ? 8 : 12, |
| 240 | paddingBottom: Math.max(insets.bottom, 18) + 18, | 245 | paddingBottom: Math.max(insets.bottom, 18) + 18, |
| 241 | paddingHorizontal: Platform.OS === "android" ? 16 : 20, | 246 | paddingHorizontal: Platform.OS === "android" ? 16 : 20, |
| 242 | paddingTop: Platform.OS === "android" ? 16 : 8, | 247 | paddingTop: Platform.OS === "android" ? 16 : 8, |
| 243 | ...(Platform.OS === "android" && projectScopes.length === 0 | 248 | ...(Platform.OS === "android" && visibleScopes.length === 0 |
| 244 | ? { flexGrow: 1, justifyContent: "center" as const } | 249 | ? { flexGrow: 1, justifyContent: "center" as const } |
| 245 | : {}), | 250 | : {}), |
| 246 | }} | 251 | }} |
| ... | @@ -292,6 +297,15 @@ export function NewTaskRouteScreen({ route }: StaticScreenProps<NewTaskRoutePara | ... | @@ -292,6 +297,15 @@ export function NewTaskRouteScreen({ route }: StaticScreenProps<NewTaskRoutePara |
| 292 | </Pressable> | 297 | </Pressable> |
| 293 | )} | 298 | )} |
| 294 | </View> | 299 | </View> |
| | 300 | ) : visibleScopes.length === 0 ? ( |
| | 301 | <View className="items-center gap-2 px-6 py-8"> |
| | 302 | <Text className="text-center text-lg font-t3-bold text-foreground"> |
| | 303 | No matching projects |
| | 304 | </Text> |
| | 305 | <Text className="text-center text-sm leading-normal text-foreground-muted"> |
| | 306 | Try a different project name or workspace path. |
| | 307 | </Text> |
| | 308 | </View> |
| 295 | ) : ( | 309 | ) : ( |
| 296 | <View | 310 | <View |
| 297 | collapsable={false} | 311 | collapsable={false} |
| ... | @@ -301,7 +315,7 @@ export function NewTaskRouteScreen({ route }: StaticScreenProps<NewTaskRoutePara | ... | @@ -301,7 +315,7 @@ export function NewTaskRouteScreen({ route }: StaticScreenProps<NewTaskRoutePara |
| 301 | : "overflow-hidden rounded-[24px] bg-card" | 315 | : "overflow-hidden rounded-[24px] bg-card" |
| 302 | } | 316 | } |
| 303 | > | 317 | > |
| 304 | {projectScopes.map((scope, scopeIndex) => { | 318 | {visibleScopes.map((scope, scopeIndex) => { |
| 305 | const hasMultipleProjects = scope.projects.length > 1; | 319 | const hasMultipleProjects = scope.projects.length > 1; |
| 306 | const selectionTarget = getProjectScopeSelectionTarget( | 320 | const selectionTarget = getProjectScopeSelectionTarget( |
| 307 | scope, | 321 | scope, |