/** * Project resources section. Read-mostly list of typed external pointers * (today: GitHub repos). Tap a row to open the URL in the system browser. * Long-press for delete (Pressable's onLongPress). * * Schema-tolerant by design — `resource_ref` is typed `unknown` in the * mobile schema (server may extend the shape per resource_type). We narrow * via `getRepoUrl()` only when the dispatch knows the type, so a future * resource_type renders as a generic row with the label instead of crashing. */ import { ActivityIndicator, Alert, Linking, Pressable, View } from "react-native"; import { useQuery } from "@tanstack/react-query"; import { Ionicons } from "@expo/vector-icons"; import type { GithubRepoResourceRef, ProjectResource, } from "@multica/core/types"; import { Text } from "@/components/ui/text"; import { projectResourcesOptions } from "@/data/queries/projects"; import { useDeleteProjectResource } from "@/data/mutations/projects"; import { useWorkspaceStore } from "@/data/workspace-store"; import { useColorScheme } from "@/lib/use-color-scheme"; import { THEME } from "@/lib/theme"; interface Props { projectId: string; onAdd: () => void; } export function ProjectResourcesSection({ projectId, onAdd }: Props) { const wsId = useWorkspaceStore((s) => s.currentWorkspaceId); const { data: resources, isLoading } = useQuery( projectResourcesOptions(wsId, projectId), ); const remove = useDeleteProjectResource(projectId); const onOpen = async (resource: ProjectResource) => { const url = getResourceUrl(resource); if (!url) return; const canOpen = await Linking.canOpenURL(url); if (canOpen) { await Linking.openURL(url); } }; const onLongPress = (resource: ProjectResource) => { Alert.alert( "Detach resource?", describeResource(resource), [ { text: "Cancel", style: "cancel" }, { text: "Detach", style: "destructive", onPress: () => remove.mutate(resource.id), }, ], ); }; return ( Resources Add {isLoading ? ( ) : !resources || resources.length === 0 ? ( No resources attached. ) : ( resources.map((resource) => ( onOpen(resource)} onLongPress={() => onLongPress(resource)} /> )) )} ); } function ResourceRow({ resource, onPress, onLongPress, }: { resource: ProjectResource; onPress: () => void; onLongPress: () => void; }) { const { colorScheme } = useColorScheme(); return ( {resource.label ?? describeResource(resource)} {resource.label ? ( {describeResource(resource)} ) : null} ); } function iconFor(type: string): keyof typeof Ionicons.glyphMap { if (type === "github_repo") return "logo-github"; return "link-outline"; } function getResourceUrl(resource: ProjectResource): string | null { if (resource.resource_type === "github_repo") { const ref = resource.resource_ref as GithubRepoResourceRef | undefined; return ref?.url ?? null; } // Unknown type — try a `.url` field as a generic fallback. const ref = resource.resource_ref as { url?: unknown } | undefined; return typeof ref?.url === "string" ? ref.url : null; } function describeResource(resource: ProjectResource): string { return getResourceUrl(resource) ?? resource.resource_type; }