/**
* 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;
}