| ... | ... | @@ -1,5 +1,5 @@ |
| 1 | 1 | import { MaterialListRow } from "../../components/MaterialListRow"; |
| 2 | | import { NativeHeaderToolbar, NativeStackScreenOptions } from "../../native/StackHeader"; |
| 2 | import { ScreenHeader } from "../../components/ScreenHeader"; |
| 3 | 3 | import { |
| 4 | 4 | StackActions, |
| 5 | 5 | useIsFocused, |
| ... | ... | @@ -8,11 +8,10 @@ import { |
| 8 | 8 | } from "@react-navigation/native"; |
| 9 | 9 | import { SymbolView } from "../../components/AppSymbol"; |
| 10 | 10 | import type { EnvironmentProject } from "@t3tools/client-runtime/state/shell"; |
| 11 | | import { useEffect, useRef } from "react"; |
| 11 | import { useEffect, useRef, useState } from "react"; |
| 12 | 12 | import { ActivityIndicator, Alert, Platform, Pressable, View } from "react-native"; |
| 13 | 13 | import { useSafeAreaInsets } from "react-native-safe-area-context"; |
| 14 | 14 | import { cn } from "../../lib/cn"; |
| 15 | | import { AndroidScreenHeader } from "../../components/AndroidScreenHeader"; |
| 16 | 15 | import { MaterialScreenContent } from "../../components/MaterialScreenContent"; |
| 17 | 16 | import { MaterialButton } from "../../components/MaterialButton"; |
| 18 | 17 | import { ScreenScrollView as ScrollView } from "../../components/ScreenScrollView"; |
| ... | ... | @@ -24,7 +23,7 @@ import { useWorkspaceState } from "../../state/workspace"; |
| 24 | 23 | import { useAdaptiveWorkspaceLayout } from "../layout/AdaptiveWorkspaceLayout"; |
| 25 | 24 | import { useIncomingShare } from "../sharing/IncomingShareProvider"; |
| 26 | 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 | 28 | type NewTaskRouteParams = { |
| 30 | 29 | readonly incomingShareId?: string | string[]; |
| ... | ... | @@ -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 | 132 | export function NewTaskRouteScreen({ route }: StaticScreenProps<NewTaskRouteParams | undefined>) { |
| 89 | 133 | const projects = useProjects(); |
| 134 | const [searchText, setSearchText] = useState(""); |
| 90 | 135 | const { projectScopes, selectedEnvironmentId, setProject } = useNewTaskFlow(); |
| 91 | 136 | const { state: catalogState } = useWorkspaceState(); |
| 92 | 137 | const navigation = useNavigation(); |
| 93 | 138 | const isFocused = useIsFocused(); |
| 94 | | const { layout } = useAdaptiveWorkspaceLayout(); |
| 95 | 139 | const insets = useSafeAreaInsets(); |
| 96 | 140 | const { getShare, releaseShareReservation } = useIncomingShare(); |
| 97 | 141 | const routeShareId = Array.isArray(route.params?.incomingShareId) |
| ... | ... | @@ -107,6 +151,7 @@ export function NewTaskRouteScreen({ route }: StaticScreenProps<NewTaskRoutePara |
| 107 | 151 | : null; |
| 108 | 152 | const screenTitle = incomingShare ? "Start a task" : "Choose project"; |
| 109 | 153 | const projectEmptyState = deriveProjectEmptyState(catalogState); |
| 154 | const visibleScopes = filterProjectScopes(projectScopes, searchText); |
| 110 | 155 | const resumedDestinationKeyRef = useRef<string | null>(null); |
| 111 | 156 | const reservedDestinationProject = incomingShare?.destination |
| 112 | 157 | ? (projects.find( |
| ... | ... | @@ -180,67 +225,27 @@ export function NewTaskRouteScreen({ route }: StaticScreenProps<NewTaskRoutePara |
| 180 | 225 | |
| 181 | 226 | return ( |
| 182 | 227 | <View collapsable={false} className="flex-1 bg-sheet"> |
| 183 | | {Platform.OS === "android" ? ( |
| 184 | | <> |
| 185 | | {/* Android renders its own in-screen header instead of the native bar. */} |
| 186 | | <NativeStackScreenOptions options={{ headerShown: false }} /> |
| 187 | | <AndroidScreenHeader |
| 188 | | title={screenTitle} |
| 189 | | hideBottomBorder |
| 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 | | )} |
| 228 | <NewTaskHeader |
| 229 | title={screenTitle} |
| 230 | subtitle={incomingShareSubtitle} |
| 231 | canAddProject={catalogState.hasReadyEnvironment} |
| 232 | searchText={searchText} |
| 233 | onSearchTextChange={setSearchText} |
| 234 | /> |
| 232 | 235 | |
| 233 | 236 | <MaterialScreenContent> |
| 234 | 237 | <ScrollView |
| 235 | 238 | contentInsetAdjustmentBehavior="automatic" |
| 236 | 239 | showsVerticalScrollIndicator={false} |
| 240 | keyboardShouldPersistTaps="handled" |
| 241 | keyboardDismissMode="on-drag" |
| 237 | 242 | className="flex-1" |
| 238 | 243 | contentContainerStyle={{ |
| 239 | 244 | gap: Platform.OS === "android" ? 8 : 12, |
| 240 | 245 | paddingBottom: Math.max(insets.bottom, 18) + 18, |
| 241 | 246 | paddingHorizontal: Platform.OS === "android" ? 16 : 20, |
| 242 | 247 | paddingTop: Platform.OS === "android" ? 16 : 8, |
| 243 | | ...(Platform.OS === "android" && projectScopes.length === 0 |
| 248 | ...(Platform.OS === "android" && visibleScopes.length === 0 |
| 244 | 249 | ? { flexGrow: 1, justifyContent: "center" as const } |
| 245 | 250 | : {}), |
| 246 | 251 | }} |
| ... | ... | @@ -292,6 +297,15 @@ export function NewTaskRouteScreen({ route }: StaticScreenProps<NewTaskRoutePara |
| 292 | 297 | </Pressable> |
| 293 | 298 | )} |
| 294 | 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 | 310 | <View |
| 297 | 311 | collapsable={false} |
| ... | ... | @@ -301,7 +315,7 @@ export function NewTaskRouteScreen({ route }: StaticScreenProps<NewTaskRoutePara |
| 301 | 315 | : "overflow-hidden rounded-[24px] bg-card" |
| 302 | 316 | } |
| 303 | 317 | > |
| 304 | | {projectScopes.map((scope, scopeIndex) => { |
| 318 | {visibleScopes.map((scope, scopeIndex) => { |
| 305 | 319 | const hasMultipleProjects = scope.projects.length > 1; |
| 306 | 320 | const selectionTarget = getProjectScopeSelectionTarget( |
| 307 | 321 | scope, |