diff --git a/webview-ui/src/components/package-manager/PackageManagerView.tsx b/webview-ui/src/components/package-manager/PackageManagerView.tsx index 32c333846a..4f764cf12d 100644 --- a/webview-ui/src/components/package-manager/PackageManagerView.tsx +++ b/webview-ui/src/components/package-manager/PackageManagerView.tsx @@ -1,182 +1,34 @@ -import { useState, useEffect, useCallback, useRef, useMemo } from "react" +import { useState, useEffect } from "react" import { Button } from "@/components/ui/button" -import { useExtensionState } from "../../context/ExtensionStateContext" import { Tab, TabContent, TabHeader } from "../common/Tab" -import { vscode } from "@/utils/vscode" import { cn } from "@/lib/utils" -import { PackageManagerItem, PackageManagerSource } from "../../../../src/services/package-manager/types" +import { PackageManagerSource } from "../../../../src/services/package-manager/types" import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "cmdk" -import { isFilterActive as checkFilterActive, getDisplayedItems as filterAndSortItems } from "./selectors" +import { isFilterActive as checkFilterActive } from "./selectors" import { PackageManagerItemCard } from "./components/PackageManagerItemCard" +import { useStateManager } from "./state/useStateManager" interface PackageManagerViewProps { onDone?: () => void } const PackageManagerView: React.FC = ({ onDone }) => { - const { packageManagerSources, setPackageManagerSources } = useExtensionState() + const [state, manager] = useStateManager() - // Core state - const [allItems, setAllItems] = useState([]) - const [activeTab, setActiveTab] = useState<"browse" | "sources">("browse") - const [refreshingUrls, setRefreshingUrls] = useState([]) - - // Filter and sort state - const [filters, setFilters] = useState({ type: "", search: "", tags: [] as string[] }) - const [sortConfig, setSortConfig] = useState({ by: "name", order: "asc" as "asc" | "desc" }) const [tagSearch, setTagSearch] = useState("") const [isTagInputActive, setIsTagInputActive] = useState(false) - // Loading state - const [isFetching, setIsFetching] = useState(false) - const fetchTimeoutRef = useRef() - - // Compute displayed items - const displayedItems = useMemo( - () => filterAndSortItems(allItems, filters, sortConfig), - [allItems, filters, sortConfig], - ) - - // Sort items - const sortedItems = useMemo( - () => - [...displayedItems].sort((a, b) => { - let comparison = 0 - - switch (sortConfig.by) { - case "name": - comparison = a.name.localeCompare(b.name) - break - case "author": - comparison = (a.author || "").localeCompare(b.author || "") - break - case "lastUpdated": - comparison = (a.lastUpdated || "").localeCompare(b.lastUpdated || "") - break - default: - comparison = a.name.localeCompare(b.name) - } - - return sortConfig.order === "asc" ? comparison : -comparison - }), - [displayedItems, sortConfig], - ) - - const allTags = useMemo(() => { - const tagSet = new Set() - allItems.forEach((item) => { - if (item.tags) { - item.tags.forEach((tag) => tagSet.add(tag)) - } + // Debug logging for state changes + useEffect(() => { + console.log("State updated:", { + allItems: state.allItems, + itemsLength: state.allItems.length, + showingEmptyState: state.allItems.length === 0, }) - return Array.from(tagSet).sort() - }, [allItems]) + }, [state.allItems]) - const fetchPackageManagerItems = useCallback(() => { - // Clear any pending fetch timeout - if (fetchTimeoutRef.current) { - clearTimeout(fetchTimeoutRef.current) - } - - // Only set fetching state, don't clear items - setIsFetching(true) - - try { - vscode.postMessage({ - type: "fetchPackageManagerItems", - forceRefresh: true, - } as any) - - // Set a timeout to reset isFetching if no response is received - fetchTimeoutRef.current = setTimeout(() => { - console.log("Fetch timeout reached, resetting state") - setIsFetching(false) - }, 30000) // 30 second timeout to match server timeout - } catch (error) { - console.error("Failed to fetch package manager items:", error) - setIsFetching(false) - } - }, []) // No dependencies needed since we're using state setters - - // Fetch items on mount - useEffect(() => { - fetchPackageManagerItems() - }, [fetchPackageManagerItems]) - - // Fetch items when sources change - useEffect(() => { - if (packageManagerSources && activeTab === "browse") { - fetchPackageManagerItems() - } - }, [packageManagerSources, fetchPackageManagerItems, activeTab]) - - useEffect(() => { - const handleMessage = (event: MessageEvent) => { - const message = event.data - - // Always clear timeout and reset fetching state for any state update - if (message.type === "state") { - if (fetchTimeoutRef.current) { - clearTimeout(fetchTimeoutRef.current) - } - setIsFetching(false) - - // Update items when we receive filtered items from the backend - if (message.state?.packageManagerItems !== undefined) { - const receivedItems = message.state.packageManagerItems || [] - console.log("Received package manager items:", receivedItems.length) - setAllItems(receivedItems) - } - } - - if (message.type === "packageManagerButtonClicked") { - if (message.text) { - // This is an error message - console.error("Package manager error:", message.text) - if (fetchTimeoutRef.current) { - clearTimeout(fetchTimeoutRef.current) - } - setIsFetching(false) - } else { - // This is a refresh request - fetchPackageManagerItems() - } - } - - if (message.type === "repositoryRefreshComplete" && message.url) { - setRefreshingUrls((prev) => prev.filter((url) => url !== message.url)) - } - } - - window.addEventListener("message", handleMessage) - return () => { - window.removeEventListener("message", handleMessage) - // Clear any pending timeout on unmount - if (fetchTimeoutRef.current) { - clearTimeout(fetchTimeoutRef.current) - } - } - }, [fetchPackageManagerItems]) - - // Debounce filter requests - useEffect(() => { - if (!checkFilterActive(filters)) { - return - } - - const debounceTimeout = setTimeout(() => { - vscode.postMessage({ - type: "filterPackageManagerItems", - filters: { - type: filters.type || undefined, - search: filters.search || undefined, - tags: filters.tags.length > 0 ? filters.tags : undefined, - }, - }) - }, 300) // 300ms debounce delay - - return () => clearTimeout(debounceTimeout) - }, [filters]) + // Compute all available tags + const allTags = Array.from(new Set(state.allItems.flatMap((item) => item.tags || []))).sort() return ( @@ -186,35 +38,40 @@ const PackageManagerView: React.FC = ({ onDone }) => {
- {activeTab === "browse" ? ( + {state.activeTab === "browse" ? ( <>
setFilters({ ...filters, search: e.target.value })} + value={state.filters.search} + onChange={(e) => + manager.transition({ + type: "UPDATE_FILTERS", + payload: { filters: { search: e.target.value } }, + }) + } className="w-full p-2 bg-vscode-input-background text-vscode-input-foreground border border-vscode-input-border rounded" />
@@ -222,8 +79,13 @@ const PackageManagerView: React.FC = ({ onDone }) => {
setSortConfig({ ...sortConfig, by: e.target.value })} + value={state.sortConfig.by} + onChange={(e) => + manager.transition({ + type: "UPDATE_SORT", + payload: { sortConfig: { by: e.target.value as any } }, + }) + } className="p-1 bg-vscode-dropdown-background text-vscode-dropdown-foreground border border-vscode-dropdown-border rounded mr-2"> @@ -245,13 +112,17 @@ const PackageManagerView: React.FC = ({ onDone }) => {
@@ -265,11 +136,16 @@ const PackageManagerView: React.FC = ({ onDone }) => { ({allTags.length} available)
- {filters.tags.length > 0 && ( + {state.filters.tags.length > 0 && ( )} @@ -300,23 +176,35 @@ const PackageManagerView: React.FC = ({ onDone }) => { { - const isSelected = filters.tags.includes(tag) + const isSelected = + state.filters.tags.includes(tag) if (isSelected) { - setFilters({ - ...filters, - tags: filters.tags.filter( - (t) => t !== tag, - ), + manager.transition({ + type: "UPDATE_FILTERS", + payload: { + filters: { + tags: state.filters.tags.filter( + (t) => t !== tag, + ), + }, + }, }) } else { - setFilters({ - ...filters, - tags: [...filters.tags, tag], + manager.transition({ + type: "UPDATE_FILTERS", + payload: { + filters: { + tags: [ + ...state.filters.tags, + tag, + ], + }, + }, }) } }} className={`flex items-center gap-2 p-1 cursor-pointer text-sm hover:bg-vscode-button-secondaryBackground ${ - filters.tags.includes(tag) + state.filters.tags.includes(tag) ? "bg-vscode-button-background text-vscode-button-foreground" : "text-vscode-dropdown-foreground" }`} @@ -324,7 +212,7 @@ const PackageManagerView: React.FC = ({ onDone }) => { e.preventDefault() }}> {tag} @@ -334,8 +222,8 @@ const PackageManagerView: React.FC = ({ onDone }) => { )}
- {filters.tags.length > 0 - ? `Showing items with any of the selected tags (${filters.tags.length} selected)` + {state.filters.tags.length > 0 + ? `Showing items with any of the selected tags (${state.filters.tags.length} selected)` : "Click tags to filter items"}
@@ -343,53 +231,96 @@ const PackageManagerView: React.FC = ({ onDone }) => { - {sortedItems.length === 0 ? ( -
-

No package manager items found

- -
- ) : ( -
-
-

- {checkFilterActive(filters) - ? `${sortedItems.length} items found (filtered)` - : `${sortedItems.length} items total`} -

- + {(() => { + // Debug log state + const items = state.allItems || [] + const isEmpty = items.length === 0 + const isLoading = state.isFetching + console.log("=== Rendering PackageManagerView ===") + console.log("Component state:", { + allItems: items, + itemCount: items.length, + isEmpty, + isLoading, + activeTab: state.activeTab, + filters: state.filters, + }) + + // Show loading state if fetching + if (isLoading) { + console.log("Rendering loading state due to isFetching=true") + return ( +
+

Loading items...

+
+ ) + } + + // Show empty state if no items + if (isEmpty) { + console.log("Showing empty state") + return ( +
+

No package manager items found

+ +
+ ) + } + + // Show items view + console.log("Showing items view with items:", items) + return ( +
+
+

+ {checkFilterActive(state.filters) + ? `${items.length} items found (filtered)` + : `${items.length} ${items.length === 1 ? "item" : "items"} total`} +

+ +
+
+ {items.map((item) => ( + + manager.transition({ type: "UPDATE_FILTERS", payload: { filters } }) + } + activeTab={state.activeTab} + setActiveTab={(tab) => + manager.transition({ type: "SET_ACTIVE_TAB", payload: { tab } }) + } + /> + ))} +
-
- {sortedItems.map((item) => ( - - ))} -
-
- )} + ) + })()} ) : ( { - setPackageManagerSources(sources) - vscode.postMessage({ type: "packageManagerSources", sources }) - }} + sources={state.sources} + refreshingUrls={state.refreshingUrls} + onRefreshSource={(url) => manager.transition({ type: "REFRESH_SOURCE", payload: { url } })} + onSourcesChange={(sources) => + manager.transition({ type: "UPDATE_SOURCES", payload: { sources } }) + } /> )} @@ -400,14 +331,14 @@ const PackageManagerView: React.FC = ({ onDone }) => { interface PackageManagerSourcesConfigProps { sources: PackageManagerSource[] refreshingUrls: string[] - setRefreshingUrls: React.Dispatch> + onRefreshSource: (url: string) => void onSourcesChange: (sources: PackageManagerSource[]) => void } const PackageManagerSourcesConfig: React.FC = ({ sources, refreshingUrls, - setRefreshingUrls, + onRefreshSource, onSourcesChange, }) => { const [newSourceUrl, setNewSourceUrl] = useState("") @@ -496,14 +427,6 @@ const PackageManagerSourcesConfig: React.FC = onSourcesChange(updatedSources) } - const handleRefreshSource = (url: string) => { - setRefreshingUrls((prev) => [...prev, url]) - vscode.postMessage({ - type: "refreshPackageManagerSource", - url, - }) - } - return (

Configure Package Manager Sources

@@ -581,7 +504,7 @@ const PackageManagerSourcesConfig: React.FC =