From 5bcc421dc117f68a04fe7593b51c8b2b451897fe Mon Sep 17 00:00:00 2001 From: AviPeltz Date: Thu, 7 May 2026 17:42:15 -0700 Subject: [PATCH 1/7] feat(desktop): unify tasks/PRs/issues view with project filter Adds Type tabs (All/Tasks/PRs/Issues) and a Project filter to the Tasks page, so PRs and GitHub issues for a project surface alongside tasks instead of needing a separate sidebar entry. Row clicks open externally and "Add to workspace" seeds the new-workspace draft to launch the existing modal pre-filled with the linked PR or issue. --- .../tasks/components/TasksView/TasksView.tsx | 148 +++++++++--- .../GitHubIssuesContent.tsx | 215 +++++++++++++++++ .../components/GitHubIssuesContent/index.ts | 1 + .../PullRequestsContent.tsx | 217 ++++++++++++++++++ .../components/PullRequestsContent/index.ts | 1 + .../components/TasksTopBar/TasksTopBar.tsx | 154 +++++++++---- .../ProjectFilter/ProjectFilter.tsx | 109 +++++++++ .../components/ProjectFilter/index.ts | 1 + .../_dashboard/tasks/layout.tsx | 6 + .../_authenticated/_dashboard/tasks/page.tsx | 4 +- .../tasks/stores/tasks-filter-state.ts | 9 + 11 files changed, 782 insertions(+), 83 deletions(-) create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/GitHubIssuesContent/GitHubIssuesContent.tsx create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/GitHubIssuesContent/index.ts create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/PullRequestsContent/PullRequestsContent.tsx create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/PullRequestsContent/index.ts create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/components/ProjectFilter/ProjectFilter.tsx create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/components/ProjectFilter/index.ts diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/TasksView.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/TasksView.tsx index 9611eef13ef..17c2e94d218 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/TasksView.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/TasksView.tsx @@ -4,7 +4,9 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; import { useTasksFilterStore } from "../../stores/tasks-filter-state"; import { BoardContent } from "./components/BoardContent"; +import { GitHubIssuesContent } from "./components/GitHubIssuesContent"; import { LinearCTA } from "./components/LinearCTA"; +import { PullRequestsContent } from "./components/PullRequestsContent"; import { TableContent } from "./components/TableContent"; import { type TabValue, TasksTopBar } from "./components/TasksTopBar"; import type { TaskWithStatus } from "./hooks/useTasksData"; @@ -13,41 +15,77 @@ interface TasksViewProps { initialTab?: "all" | "active" | "backlog"; initialAssignee?: string; initialSearch?: string; + initialType?: "all" | "tasks" | "prs" | "issues"; + initialProject?: string; } export function TasksView({ initialTab, initialAssignee, initialSearch, + initialType, + initialProject, }: TasksViewProps) { const navigate = useNavigate(); const collections = useCollections(); const currentTab: TabValue = initialTab ?? "all"; const [searchQuery, setSearchQuery] = useState(initialSearch ?? ""); const assigneeFilter = initialAssignee ?? null; + const typeTab = initialType ?? "all"; + const projectFilter = initialProject ?? null; const { setTab: storeSetTab, setAssignee: storeSetAssignee, setSearch: storeSetSearch, + setTypeTab: storeSetTypeTab, + setProjectFilter: storeSetProjectFilter, viewMode, setViewMode, } = useTasksFilterStore(); const debounceRef = useRef>(null); + const buildSearch = useCallback( + (overrides: { + tab?: TabValue; + assignee?: string | null; + search?: string; + type?: "all" | "tasks" | "prs" | "issues"; + project?: string | null; + }) => { + const tab = overrides.tab ?? currentTab; + const assignee = + overrides.assignee !== undefined ? overrides.assignee : assigneeFilter; + const query = + overrides.search !== undefined ? overrides.search : searchQuery; + const type = overrides.type ?? typeTab; + const project = + overrides.project !== undefined ? overrides.project : projectFilter; + + const search: Record = {}; + if (tab !== "all") search.tab = tab; + if (assignee) search.assignee = assignee; + if (query) search.search = query; + if (type !== "all") search.type = type; + if (project) search.project = project; + return search; + }, + [currentTab, assigneeFilter, searchQuery, typeTab, projectFilter], + ); + const syncSearchToUrl = useCallback( (query: string) => { if (debounceRef.current) clearTimeout(debounceRef.current); debounceRef.current = setTimeout(() => { - const search: Record = {}; - if (currentTab !== "all") search.tab = currentTab; - if (assigneeFilter) search.assignee = assigneeFilter; - if (query) search.search = query; - navigate({ to: "/tasks", search, replace: true }); + navigate({ + to: "/tasks", + search: buildSearch({ search: query }), + replace: true, + }); }, 300); }, - [navigate, currentTab, assigneeFilter], + [navigate, buildSearch], ); useEffect(() => { @@ -77,6 +115,14 @@ export function TasksView({ storeSetSearch(searchQuery); }, [searchQuery, storeSetSearch]); + useEffect(() => { + storeSetTypeTab(typeTab); + }, [typeTab, storeSetTypeTab]); + + useEffect(() => { + storeSetProjectFilter(projectFilter); + }, [projectFilter, storeSetProjectFilter]); + const { data: integrations } = useLiveQuery( (q) => q @@ -91,19 +137,23 @@ export function TasksView({ integrations?.some((i) => i.provider === "linear") ?? false; const handleTabChange = (tab: TabValue) => { - const search: Record = {}; - if (tab !== "all") search.tab = tab; - if (assigneeFilter) search.assignee = assigneeFilter; - if (searchQuery) search.search = searchQuery; - navigate({ to: "/tasks", search, replace: true }); + navigate({ to: "/tasks", search: buildSearch({ tab }), replace: true }); }; const handleAssigneeFilterChange = (assignee: string | null) => { - const search: Record = {}; - if (currentTab !== "all") search.tab = currentTab; - if (assignee) search.assignee = assignee; - if (searchQuery) search.search = searchQuery; - navigate({ to: "/tasks", search, replace: true }); + navigate({ + to: "/tasks", + search: buildSearch({ assignee }), + replace: true, + }); + }; + + const handleTypeTabChange = (type: "all" | "tasks" | "prs" | "issues") => { + navigate({ to: "/tasks", search: buildSearch({ type }), replace: true }); + }; + + const handleProjectFilterChange = (project: string | null) => { + navigate({ to: "/tasks", search: buildSearch({ project }), replace: true }); }; const [selectedTasks, setSelectedTasks] = useState([]); @@ -122,18 +172,21 @@ export function TasksView({ }, []); const handleTaskClick = (task: TaskWithStatus) => { - const search: Record = {}; - if (currentTab !== "all") search.tab = currentTab; - if (assigneeFilter) search.assignee = assigneeFilter; - if (searchQuery) search.search = searchQuery; navigate({ to: "/tasks/$taskId", params: { taskId: task.id }, - search, + search: buildSearch({}), }); }; - const showLinearCTA = integrations !== undefined && !isLinearConnected; + const showLinearCTA = + integrations !== undefined && + !isLinearConnected && + (typeTab === "all" || typeTab === "tasks"); + + const showTasks = typeTab === "all" || typeTab === "tasks"; + const showPRs = typeTab === "all" || typeTab === "prs"; + const showIssues = typeTab === "all" || typeTab === "issues"; return (
@@ -149,26 +202,49 @@ export function TasksView({ onClearSelection={handleClearSelection} viewMode={viewMode} onViewModeChange={setViewMode} + typeTab={typeTab} + onTypeTabChange={handleTypeTabChange} + projectFilter={projectFilter} + onProjectFilterChange={handleProjectFilterChange} /> )} {showLinearCTA ? ( - ) : viewMode === "board" ? ( - ) : ( - +
+ {showTasks && + (viewMode === "board" ? ( + + ) : ( + + ))} + {showPRs && ( + + )} + {showIssues && ( + + )} +
)}
); diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/GitHubIssuesContent/GitHubIssuesContent.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/GitHubIssuesContent/GitHubIssuesContent.tsx new file mode 100644 index 00000000000..c72b96077c7 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/GitHubIssuesContent/GitHubIssuesContent.tsx @@ -0,0 +1,215 @@ +import { Button } from "@superset/ui/button"; +import { Checkbox } from "@superset/ui/checkbox"; +import { useQuery } from "@tanstack/react-query"; +import { useId, useMemo, useState } from "react"; +import { GoIssueClosed, GoIssueOpened } from "react-icons/go"; +import { HiOutlineArrowTopRightOnSquare } from "react-icons/hi2"; +import { LuPlus } from "react-icons/lu"; +import { useHostUrl } from "renderer/hooks/host-service/useHostTargetUrl"; +import { useDebouncedValue } from "renderer/hooks/useDebouncedValue"; +import { getHostServiceClientByUrl } from "renderer/lib/host-service-client"; +import { + type LinkedIssue, + useNewWorkspaceDraftStore, +} from "renderer/stores/new-workspace-draft"; +import { useOpenNewWorkspaceModal } from "renderer/stores/new-workspace-modal"; + +interface GitHubIssuesContentProps { + projectFilter: string | null; + searchQuery: string; + sectioned?: boolean; +} + +export function GitHubIssuesContent({ + projectFilter, + searchQuery, + sectioned = false, +}: GitHubIssuesContentProps) { + const [showClosed, setShowClosed] = useState(false); + const showClosedId = useId(); + const debouncedQuery = useDebouncedValue(searchQuery, 300); + const hostUrl = useHostUrl(null); + const updateDraft = useNewWorkspaceDraftStore((s) => s.updateDraft); + const resetDraft = useNewWorkspaceDraftStore((s) => s.resetDraft); + const openModal = useOpenNewWorkspaceModal(); + + const { data, isFetching, error } = useQuery({ + queryKey: [ + "tasks", + "searchGitHubIssues", + projectFilter, + hostUrl, + debouncedQuery.trim(), + showClosed, + ], + queryFn: async () => { + if (!hostUrl || !projectFilter) return { issues: [] }; + const client = getHostServiceClientByUrl(hostUrl); + return client.workspaceCreation.searchGitHubIssues.query({ + projectId: projectFilter, + query: debouncedQuery.trim() || undefined, + limit: 50, + includeClosed: showClosed, + }); + }, + enabled: !!projectFilter && !!hostUrl, + retry: false, + }); + + const issues = useMemo(() => data?.issues ?? [], [data]); + const repoMismatch = + data && "repoMismatch" in data ? data.repoMismatch : null; + + const handleAddToWorkspace = (issue: (typeof issues)[number]) => { + if (!projectFilter) return; + const linkedIssue: LinkedIssue = { + slug: `gh-${issue.issueNumber}`, + title: issue.title, + source: "github", + url: issue.url, + number: issue.issueNumber, + state: issue.state.toLowerCase() === "closed" ? "closed" : "open", + }; + resetDraft(); + updateDraft({ + selectedProjectId: projectFilter, + linkedIssues: [linkedIssue], + }); + openModal(projectFilter); + }; + + const handleOpenUrl = (url: string) => { + window.open(url, "_blank", "noopener,noreferrer"); + }; + + if (!projectFilter) { + return ( +
+
+ + Select a project to see issues. +
+
+ ); + } + + return ( +
+ {sectioned && ( +
+ + + GitHub issues + + + {issues.length} + +
+ )} + +
+ setShowClosed(checked === true)} + /> + + {isFetching && ( + Loading… + )} +
+ + {error instanceof Error && ( +
+ {error.message} +
+ )} + + {repoMismatch && ( +
+ Issue URL must match {repoMismatch}. +
+ )} + + {issues.length === 0 && !isFetching && !error ? ( +
+ + {showClosed ? "No issues found." : "No open issues."} + +
+ ) : ( +
+ {issues.map((issue) => { + const isClosed = issue.state.toLowerCase() === "closed"; + const StateIcon = isClosed ? GoIssueClosed : GoIssueOpened; + return ( + // biome-ignore lint/a11y/useSemanticElements: row contains nested action buttons, so the outer element is a div with role/tabIndex +
handleOpenUrl(issue.url)} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + handleOpenUrl(issue.url); + } + }} + role="button" + tabIndex={0} + > + + + #{issue.issueNumber} + + + {issue.title} + + {issue.authorLogin && ( + + {issue.authorLogin} + + )} +
+ + +
+
+ ); + })} +
+ )} +
+ ); +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/GitHubIssuesContent/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/GitHubIssuesContent/index.ts new file mode 100644 index 00000000000..7aec84f6c85 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/GitHubIssuesContent/index.ts @@ -0,0 +1 @@ +export { GitHubIssuesContent } from "./GitHubIssuesContent"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/PullRequestsContent/PullRequestsContent.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/PullRequestsContent/PullRequestsContent.tsx new file mode 100644 index 00000000000..7e4d9caffd1 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/PullRequestsContent/PullRequestsContent.tsx @@ -0,0 +1,217 @@ +import { Button } from "@superset/ui/button"; +import { Checkbox } from "@superset/ui/checkbox"; +import { useQuery } from "@tanstack/react-query"; +import { useId, useMemo, useState } from "react"; +import { GoGitPullRequest } from "react-icons/go"; +import { HiOutlineArrowTopRightOnSquare } from "react-icons/hi2"; +import { LuPlus } from "react-icons/lu"; +import { useHostUrl } from "renderer/hooks/host-service/useHostTargetUrl"; +import { useDebouncedValue } from "renderer/hooks/useDebouncedValue"; +import { getHostServiceClientByUrl } from "renderer/lib/host-service-client"; +import { + PRIcon, + type PRState, +} from "renderer/screens/main/components/PRIcon/PRIcon"; +import { + type LinkedPR, + useNewWorkspaceDraftStore, +} from "renderer/stores/new-workspace-draft"; +import { useOpenNewWorkspaceModal } from "renderer/stores/new-workspace-modal"; + +interface PullRequestsContentProps { + projectFilter: string | null; + searchQuery: string; + sectioned?: boolean; +} + +function normalizeState(state: string, isDraft: boolean): PRState { + if (isDraft) return "draft"; + const lower = state.toLowerCase(); + if (lower === "merged") return "merged"; + if (lower === "closed") return "closed"; + return "open"; +} + +export function PullRequestsContent({ + projectFilter, + searchQuery, + sectioned = false, +}: PullRequestsContentProps) { + const [showClosed, setShowClosed] = useState(false); + const showClosedId = useId(); + const debouncedQuery = useDebouncedValue(searchQuery, 300); + const hostUrl = useHostUrl(null); + const updateDraft = useNewWorkspaceDraftStore((s) => s.updateDraft); + const resetDraft = useNewWorkspaceDraftStore((s) => s.resetDraft); + const openModal = useOpenNewWorkspaceModal(); + + const { data, isFetching, error } = useQuery({ + queryKey: [ + "tasks", + "searchPullRequests", + projectFilter, + hostUrl, + debouncedQuery.trim(), + showClosed, + ], + queryFn: async () => { + if (!hostUrl || !projectFilter) return { pullRequests: [] }; + const client = getHostServiceClientByUrl(hostUrl); + return client.workspaceCreation.searchPullRequests.query({ + projectId: projectFilter, + query: debouncedQuery.trim() || undefined, + limit: 50, + includeClosed: showClosed, + }); + }, + enabled: !!projectFilter && !!hostUrl, + retry: false, + }); + + const pullRequests = useMemo(() => data?.pullRequests ?? [], [data]); + const repoMismatch = + data && "repoMismatch" in data ? data.repoMismatch : null; + + const handleAddToWorkspace = (pr: (typeof pullRequests)[number]) => { + if (!projectFilter) return; + const linkedPR: LinkedPR = { + prNumber: pr.prNumber, + title: pr.title, + url: pr.url, + state: normalizeState(pr.state, pr.isDraft), + }; + resetDraft(); + updateDraft({ selectedProjectId: projectFilter, linkedPR }); + openModal(projectFilter); + }; + + const handleOpenUrl = (url: string) => { + window.open(url, "_blank", "noopener,noreferrer"); + }; + + if (!projectFilter) { + return ( +
+
+ + + Select a project to see pull requests. + +
+
+ ); + } + + return ( +
+ {sectioned && ( +
+ + + Pull requests + + + {pullRequests.length} + +
+ )} + +
+ setShowClosed(checked === true)} + /> + + {isFetching && ( + Loading… + )} +
+ + {error instanceof Error && ( +
+ {error.message} +
+ )} + + {repoMismatch && ( +
+ PR URL must match {repoMismatch}. +
+ )} + + {pullRequests.length === 0 && !isFetching && !error ? ( +
+ + {showClosed ? "No pull requests found." : "No open pull requests."} + +
+ ) : ( +
+ {pullRequests.map((pr) => { + const state = normalizeState(pr.state, pr.isDraft); + return ( + // biome-ignore lint/a11y/useSemanticElements: row contains nested action buttons, so the outer element is a div with role/tabIndex +
handleOpenUrl(pr.url)} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + handleOpenUrl(pr.url); + } + }} + role="button" + tabIndex={0} + > + + + #{pr.prNumber} + + + {pr.title} + + {pr.authorLogin && ( + + {pr.authorLogin} + + )} +
+ + +
+
+ ); + })} +
+ )} +
+ ); +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/PullRequestsContent/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/PullRequestsContent/index.ts new file mode 100644 index 00000000000..f6b236e1379 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/PullRequestsContent/index.ts @@ -0,0 +1 @@ +export { PullRequestsContent } from "./PullRequestsContent"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/TasksTopBar.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/TasksTopBar.tsx index 02a44301a8b..56b706bba3a 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/TasksTopBar.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/TasksTopBar.tsx @@ -3,6 +3,7 @@ import { Input } from "@superset/ui/input"; import { Tabs, TabsList, TabsTrigger } from "@superset/ui/tabs"; import { cn } from "@superset/ui/utils"; import { useRef, useState } from "react"; +import { GoGitPullRequest, GoIssueOpened } from "react-icons/go"; import { HiOutlineMagnifyingGlass, HiOutlinePencilSquare, @@ -12,13 +13,14 @@ import { } from "react-icons/hi2"; import { useIsV2CloudEnabled } from "renderer/hooks/useIsV2CloudEnabled"; import { useHotkey } from "renderer/hotkeys"; -import type { ViewMode } from "../../../../stores/tasks-filter-state"; +import type { TypeTab, ViewMode } from "../../../../stores/tasks-filter-state"; import type { TaskWithStatus } from "../../hooks/useTasksData"; import { ActiveIcon } from "../shared/icons/ActiveIcon"; import { AllIssuesIcon } from "../shared/icons/AllIssuesIcon"; import { BacklogIcon } from "../shared/icons/BacklogIcon"; import { AssigneeFilter } from "./components/AssigneeFilter"; import { CreateTaskDialog } from "./components/CreateTaskDialog"; +import { ProjectFilter } from "./components/ProjectFilter"; import { RunInWorkspacePopover } from "./components/RunInWorkspacePopover"; import { RunInWorkspacePopoverV2 } from "./components/RunInWorkspacePopoverV2"; @@ -35,6 +37,10 @@ interface TasksTopBarProps { onClearSelection?: () => void; viewMode: ViewMode; onViewModeChange: (mode: ViewMode) => void; + typeTab: TypeTab; + onTypeTabChange: (typeTab: TypeTab) => void; + projectFilter: string | null; + onProjectFilterChange: (projectId: string | null) => void; } const TABS = [ @@ -55,6 +61,13 @@ const TABS = [ }, ] as const; +const TYPE_TABS = [ + { value: "all" as const, label: "All", Icon: AllIssuesIcon }, + { value: "tasks" as const, label: "Tasks", Icon: ActiveIcon }, + { value: "prs" as const, label: "PRs", Icon: GoGitPullRequest }, + { value: "issues" as const, label: "Issues", Icon: GoIssueOpened }, +] as const; + export function TasksTopBar({ currentTab, onTabChange, @@ -66,7 +79,12 @@ export function TasksTopBar({ onClearSelection, viewMode, onViewModeChange, + typeTab, + onTypeTabChange, + projectFilter, + onProjectFilterChange, }: TasksTopBarProps) { + const showTaskOnlyControls = typeTab === "all" || typeTab === "tasks"; const selectedCount = selectedTasks.length; const searchInputRef = useRef(null); const [isCreateTaskOpen, setIsCreateTaskOpen] = useState(false); @@ -117,11 +135,11 @@ export function TasksTopBar({ ) : ( <> onTabChange(value as TabValue)} + value={typeTab} + onValueChange={(value) => onTypeTabChange(value as TypeTab)} > - {TABS.map((tab) => { + {TYPE_TABS.map((tab) => { const Icon = tab.Icon; return ( - + + {showTaskOnlyControls && ( + <> +
+ + onTabChange(value as TabValue)} + > + + {TABS.map((tab) => { + const Icon = tab.Icon; + return ( + + + {tab.label} + + ); + })} + + + +
+ + + + )} )}
{/* Right side: create + view toggle + search */}
- - -
- - -
+ {showTaskOnlyControls && ( + <> + + +
+ + +
+ + )}
onSearchChange(e.target.value)} onKeyDown={(e) => { diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/components/ProjectFilter/ProjectFilter.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/components/ProjectFilter/ProjectFilter.tsx new file mode 100644 index 00000000000..72d1156cc6e --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/components/ProjectFilter/ProjectFilter.tsx @@ -0,0 +1,109 @@ +import { Button } from "@superset/ui/button"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@superset/ui/command"; +import { Popover, PopoverContent, PopoverTrigger } from "@superset/ui/popover"; +import { useLiveQuery } from "@tanstack/react-db"; +import { useMemo, useState } from "react"; +import { HiCheck, HiChevronDown, HiOutlineFolder } from "react-icons/hi2"; +import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; + +interface ProjectFilterProps { + value: string | null; + onChange: (value: string | null) => void; +} + +export function ProjectFilter({ value, onChange }: ProjectFilterProps) { + const collections = useCollections(); + const [open, setOpen] = useState(false); + const [search, setSearch] = useState(""); + + const { data: allProjects } = useLiveQuery( + (q) => q.from({ projects: collections.v2Projects }), + [collections], + ); + + const projects = useMemo(() => allProjects ?? [], [allProjects]); + + const selected = useMemo( + () => (value ? (projects.find((p) => p.id === value) ?? null) : null), + [value, projects], + ); + + const filtered = useMemo(() => { + const q = search.trim().toLowerCase(); + if (!q) return projects; + return projects.filter((p) => p.name.toLowerCase().includes(q)); + }, [projects, search]); + + const handleSelect = (id: string | null) => { + onChange(id); + setOpen(false); + setSearch(""); + }; + + return ( + { + setOpen(next); + if (!next) setSearch(""); + }} + > + + + + + + + + + handleSelect(null)}> + All projects + {value === null && } + + + {filtered.length === 0 && search && ( + No projects found. + )} + {filtered.length > 0 && ( + + {filtered.map((project) => ( + handleSelect(project.id)} + > + + {project.name} + {project.id === value && ( + + )} + + ))} + + )} + + + + + ); +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/components/ProjectFilter/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/components/ProjectFilter/index.ts new file mode 100644 index 00000000000..5354e98bd5d --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/components/ProjectFilter/index.ts @@ -0,0 +1 @@ +export { ProjectFilter } from "./ProjectFilter"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/layout.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/layout.tsx index fae700db89b..1320239cb2c 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/layout.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/layout.tsx @@ -4,6 +4,8 @@ export type TasksSearch = { tab?: "all" | "active" | "backlog"; assignee?: string; search?: string; + type?: "all" | "tasks" | "prs" | "issues"; + project?: string; }; export const Route = createFileRoute("/_authenticated/_dashboard/tasks")({ @@ -14,6 +16,10 @@ export const Route = createFileRoute("/_authenticated/_dashboard/tasks")({ : undefined, assignee: typeof search.assignee === "string" ? search.assignee : undefined, search: typeof search.search === "string" ? search.search : undefined, + type: ["all", "tasks", "prs", "issues"].includes(search.type as string) + ? (search.type as TasksSearch["type"]) + : undefined, + project: typeof search.project === "string" ? search.project : undefined, }), }); diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/page.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/page.tsx index 4a6e1b33268..7b4253235fb 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/page.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/page.tsx @@ -7,12 +7,14 @@ export const Route = createFileRoute("/_authenticated/_dashboard/tasks/")({ }); function TasksPage() { - const { tab, assignee, search } = TasksLayoutRoute.useSearch(); + const { tab, assignee, search, type, project } = TasksLayoutRoute.useSearch(); return ( ); } diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/stores/tasks-filter-state.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/stores/tasks-filter-state.ts index 1077f94244b..2fd16f5eb48 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/stores/tasks-filter-state.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/stores/tasks-filter-state.ts @@ -1,16 +1,21 @@ import { create } from "zustand"; export type ViewMode = "table" | "board"; +export type TypeTab = "all" | "tasks" | "prs" | "issues"; interface TasksFilterState { tab: "all" | "active" | "backlog"; assignee: string | null; search: string; viewMode: ViewMode; + typeTab: TypeTab; + projectFilter: string | null; setTab: (tab: "all" | "active" | "backlog") => void; setAssignee: (assignee: string | null) => void; setSearch: (search: string) => void; setViewMode: (viewMode: ViewMode) => void; + setTypeTab: (typeTab: TypeTab) => void; + setProjectFilter: (projectFilter: string | null) => void; } export const useTasksFilterStore = create()((set) => ({ @@ -18,8 +23,12 @@ export const useTasksFilterStore = create()((set) => ({ assignee: null, search: "", viewMode: "table", + typeTab: "all", + projectFilter: null, setTab: (tab) => set({ tab }), setAssignee: (assignee) => set({ assignee }), setSearch: (search) => set({ search }), setViewMode: (viewMode) => set({ viewMode }), + setTypeTab: (typeTab) => set({ typeTab }), + setProjectFilter: (projectFilter) => set({ projectFilter }), })); From a3b0afcb29fa822aad6afe385650194312d9ae79 Mon Sep 17 00:00:00 2001 From: AviPeltz Date: Thu, 7 May 2026 19:28:48 -0700 Subject: [PATCH 2/7] feat(desktop): All view as resizable panes with paginated PR/issue lists MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Splits the Tasks "All" view into three resizable, collapsible panes (Tasks · Pull requests · Issues), each scrolling independently and remembering its layout via autoSaveId. Per-pane minimize button and a vertical-text rail click target for re-expanding. Backs PR/issue lists with `gh api search/issues` + page cursor so the top-right indicator shows real totals ("30 of 412 pull requests") and new pages stream in via IntersectionObserver as the user scrolls. Toolbar uses container queries for breakpoints — labels collapse to icon-only as the toolbar narrows, so it no longer overflows on narrow windows. Status + assignee filters are now scoped to the dedicated Tasks tab. --- .../tasks/components/TasksView/TasksView.tsx | 15 +- .../AllModePanels/AllModePanels.tsx | 121 ++++++++ .../components/AllModePanels/index.ts | 1 + .../CollapsedColumnRail.tsx | 36 +++ .../components/CollapsedColumnRail/index.ts | 1 + .../GitHubIssuesContent.tsx | 286 +++++++++++------- .../PullRequestsContent.tsx | 274 ++++++++++------- .../components/TasksColumn/TasksColumn.tsx | 57 ++++ .../TasksView/components/TasksColumn/index.ts | 1 + .../components/TasksTopBar/TasksTopBar.tsx | 72 ++--- .../AssigneeFilter/AssigneeFilter.tsx | 7 +- .../ProjectFilter/ProjectFilter.tsx | 20 +- .../components/StatusFilter/StatusFilter.tsx | 81 +++++ .../components/StatusFilter/index.ts | 1 + .../procedures/search-github-issues.ts | 166 +++++++--- .../procedures/search-pull-requests.ts | 177 +++++++---- .../trpc/router/workspace-creation/schemas.ts | 1 + ...kspace-creation-github.integration.test.ts | 92 +++--- 18 files changed, 1000 insertions(+), 409 deletions(-) create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/AllModePanels/AllModePanels.tsx create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/AllModePanels/index.ts create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/CollapsedColumnRail/CollapsedColumnRail.tsx create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/CollapsedColumnRail/index.ts create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksColumn/TasksColumn.tsx create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksColumn/index.ts create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/components/StatusFilter/StatusFilter.tsx create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/components/StatusFilter/index.ts diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/TasksView.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/TasksView.tsx index 17c2e94d218..bc35a01c949 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/TasksView.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/TasksView.tsx @@ -3,6 +3,7 @@ import { useNavigate } from "@tanstack/react-router"; import { useCallback, useEffect, useRef, useState } from "react"; import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; import { useTasksFilterStore } from "../../stores/tasks-filter-state"; +import { AllModePanels } from "./components/AllModePanels"; import { BoardContent } from "./components/BoardContent"; import { GitHubIssuesContent } from "./components/GitHubIssuesContent"; import { LinearCTA } from "./components/LinearCTA"; @@ -180,9 +181,7 @@ export function TasksView({ }; const showLinearCTA = - integrations !== undefined && - !isLinearConnected && - (typeTab === "all" || typeTab === "tasks"); + integrations !== undefined && !isLinearConnected && typeTab === "tasks"; const showTasks = typeTab === "all" || typeTab === "tasks"; const showPRs = typeTab === "all" || typeTab === "prs"; @@ -211,8 +210,14 @@ export function TasksView({ {showLinearCTA ? ( + ) : typeTab === "all" ? ( + ) : ( -
+
{showTasks && (viewMode === "board" ? ( )} {showIssues && ( )}
diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/AllModePanels/AllModePanels.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/AllModePanels/AllModePanels.tsx new file mode 100644 index 00000000000..54927c49f83 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/AllModePanels/AllModePanels.tsx @@ -0,0 +1,121 @@ +import { + ResizableHandle, + ResizablePanel, + ResizablePanelGroup, +} from "@superset/ui/resizable"; +import { useCallback, useRef, useState } from "react"; +import { GoGitPullRequest, GoIssueOpened } from "react-icons/go"; +import type { ImperativePanelHandle } from "react-resizable-panels"; +import type { TaskWithStatus } from "../../hooks/useTasksData"; +import { CollapsedColumnRail } from "../CollapsedColumnRail"; +import { GitHubIssuesContent } from "../GitHubIssuesContent"; +import { PullRequestsContent } from "../PullRequestsContent"; +import { ActiveIcon } from "../shared/icons/ActiveIcon"; +import { TasksColumn } from "../TasksColumn"; + +interface AllModePanelsProps { + projectFilter: string | null; + searchQuery: string; + onTaskClick: (task: TaskWithStatus) => void; +} + +export function AllModePanels({ + projectFilter, + searchQuery, + onTaskClick, +}: AllModePanelsProps) { + const tasksRef = useRef(null); + const prsRef = useRef(null); + const issuesRef = useRef(null); + + const [tasksCollapsed, setTasksCollapsed] = useState(false); + const [prsCollapsed, setPrsCollapsed] = useState(false); + const [issuesCollapsed, setIssuesCollapsed] = useState(false); + + const collapseTasks = useCallback(() => tasksRef.current?.collapse(), []); + const expandTasks = useCallback(() => tasksRef.current?.expand(), []); + const collapsePrs = useCallback(() => prsRef.current?.collapse(), []); + const expandPrs = useCallback(() => prsRef.current?.expand(), []); + const collapseIssues = useCallback(() => issuesRef.current?.collapse(), []); + const expandIssues = useCallback(() => issuesRef.current?.expand(), []); + + return ( + + setTasksCollapsed(true)} + onExpand={() => setTasksCollapsed(false)} + > + {tasksCollapsed ? ( + } + onExpand={expandTasks} + /> + ) : ( + + )} + + + setPrsCollapsed(true)} + onExpand={() => setPrsCollapsed(false)} + > + {prsCollapsed ? ( + } + onExpand={expandPrs} + /> + ) : ( + + )} + + + setIssuesCollapsed(true)} + onExpand={() => setIssuesCollapsed(false)} + > + {issuesCollapsed ? ( + } + onExpand={expandIssues} + /> + ) : ( + + )} + + + ); +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/AllModePanels/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/AllModePanels/index.ts new file mode 100644 index 00000000000..6ea6e64dd34 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/AllModePanels/index.ts @@ -0,0 +1 @@ +export { AllModePanels } from "./AllModePanels"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/CollapsedColumnRail/CollapsedColumnRail.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/CollapsedColumnRail/CollapsedColumnRail.tsx new file mode 100644 index 00000000000..bcb1c8734d1 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/CollapsedColumnRail/CollapsedColumnRail.tsx @@ -0,0 +1,36 @@ +import type { ReactNode } from "react"; + +interface CollapsedColumnRailProps { + label: string; + icon: ReactNode; + count?: string; + onExpand: () => void; +} + +export function CollapsedColumnRail({ + label, + icon, + count, + onExpand, +}: CollapsedColumnRailProps) { + return ( + + ); +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/CollapsedColumnRail/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/CollapsedColumnRail/index.ts new file mode 100644 index 00000000000..a55f1184dcd --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/CollapsedColumnRail/index.ts @@ -0,0 +1 @@ +export { CollapsedColumnRail } from "./CollapsedColumnRail"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/GitHubIssuesContent/GitHubIssuesContent.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/GitHubIssuesContent/GitHubIssuesContent.tsx index c72b96077c7..928b64015bf 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/GitHubIssuesContent/GitHubIssuesContent.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/GitHubIssuesContent/GitHubIssuesContent.tsx @@ -1,10 +1,10 @@ import { Button } from "@superset/ui/button"; import { Checkbox } from "@superset/ui/checkbox"; -import { useQuery } from "@tanstack/react-query"; -import { useId, useMemo, useState } from "react"; +import { useInfiniteQuery } from "@tanstack/react-query"; +import { useEffect, useId, useMemo, useRef, useState } from "react"; import { GoIssueClosed, GoIssueOpened } from "react-icons/go"; import { HiOutlineArrowTopRightOnSquare } from "react-icons/hi2"; -import { LuPlus } from "react-icons/lu"; +import { LuMinus, LuPlus } from "react-icons/lu"; import { useHostUrl } from "renderer/hooks/host-service/useHostTargetUrl"; import { useDebouncedValue } from "renderer/hooks/useDebouncedValue"; import { getHostServiceClientByUrl } from "renderer/lib/host-service-client"; @@ -17,13 +17,15 @@ import { useOpenNewWorkspaceModal } from "renderer/stores/new-workspace-modal"; interface GitHubIssuesContentProps { projectFilter: string | null; searchQuery: string; - sectioned?: boolean; + onCollapse?: () => void; } +const PAGE_SIZE = 30; + export function GitHubIssuesContent({ projectFilter, searchQuery, - sectioned = false, + onCollapse, }: GitHubIssuesContentProps) { const [showClosed, setShowClosed] = useState(false); const showClosedId = useId(); @@ -33,7 +35,14 @@ export function GitHubIssuesContent({ const resetDraft = useNewWorkspaceDraftStore((s) => s.resetDraft); const openModal = useOpenNewWorkspaceModal(); - const { data, isFetching, error } = useQuery({ + const { + data, + isFetching, + isFetchingNextPage, + fetchNextPage, + hasNextPage, + error, + } = useInfiniteQuery({ queryKey: [ "tasks", "searchGitHubIssues", @@ -42,23 +51,56 @@ export function GitHubIssuesContent({ debouncedQuery.trim(), showClosed, ], - queryFn: async () => { - if (!hostUrl || !projectFilter) return { issues: [] }; + queryFn: async ({ pageParam }) => { + if (!hostUrl || !projectFilter) { + return { + issues: [], + totalCount: 0, + hasNextPage: false, + page: pageParam, + }; + } const client = getHostServiceClientByUrl(hostUrl); return client.workspaceCreation.searchGitHubIssues.query({ projectId: projectFilter, query: debouncedQuery.trim() || undefined, - limit: 50, + limit: PAGE_SIZE, includeClosed: showClosed, + page: pageParam, }); }, + initialPageParam: 1, + getNextPageParam: (lastPage) => + lastPage.hasNextPage ? lastPage.page + 1 : undefined, enabled: !!projectFilter && !!hostUrl, retry: false, }); - const issues = useMemo(() => data?.issues ?? [], [data]); - const repoMismatch = - data && "repoMismatch" in data ? data.repoMismatch : null; + const issues = useMemo( + () => data?.pages.flatMap((p) => p.issues) ?? [], + [data], + ); + const totalCount = data?.pages[0]?.totalCount ?? 0; + const repoMismatch = useMemo(() => { + const first = data?.pages[0]; + return first && "repoMismatch" in first ? first.repoMismatch : null; + }, [data]); + + const scrollRef = useRef(null); + const sentinelRef = useRef(null); + useEffect(() => { + const el = sentinelRef.current; + const root = scrollRef.current; + if (!el || !root || !hasNextPage || isFetchingNextPage) return; + const observer = new IntersectionObserver( + (entries) => { + if (entries[0]?.isIntersecting) fetchNextPage(); + }, + { root, rootMargin: "200px" }, + ); + observer.observe(el); + return () => observer.disconnect(); + }, [hasNextPage, isFetchingNextPage, fetchNextPage]); const handleAddToWorkspace = (issue: (typeof issues)[number]) => { if (!projectFilter) return; @@ -84,7 +126,7 @@ export function GitHubIssuesContent({ if (!projectFilter) { return ( -
+
Select a project to see issues. @@ -93,21 +135,36 @@ export function GitHubIssuesContent({ ); } + const isInitialLoad = isFetching && issues.length === 0; + const countLabel = isInitialLoad + ? "Loading…" + : totalCount === 0 + ? "0" + : `${issues.length} of ${totalCount}`; + return ( -
- {sectioned && ( -
- - - GitHub issues - - - {issues.length} - -
- )} +
+
+ + + GitHub issues + + + {countLabel} + + {onCollapse && ( + + )} +
-
+
Show closed - {isFetching && ( + {isFetching && !isInitialLoad && ( Loading… )}
- {error instanceof Error && ( -
- {error.message} -
- )} +
+ {error instanceof Error && ( +
+ {error.message} +
+ )} - {repoMismatch && ( -
- Issue URL must match {repoMismatch}. -
- )} + {repoMismatch && ( +
+ Issue URL must match {repoMismatch}. +
+ )} - {issues.length === 0 && !isFetching && !error ? ( -
- - {showClosed ? "No issues found." : "No open issues."} - -
- ) : ( -
- {issues.map((issue) => { - const isClosed = issue.state.toLowerCase() === "closed"; - const StateIcon = isClosed ? GoIssueClosed : GoIssueOpened; - return ( - // biome-ignore lint/a11y/useSemanticElements: row contains nested action buttons, so the outer element is a div with role/tabIndex -
handleOpenUrl(issue.url)} - onKeyDown={(e) => { - if (e.key === "Enter" || e.key === " ") { - e.preventDefault(); - handleOpenUrl(issue.url); - } - }} - role="button" - tabIndex={0} - > - - - #{issue.issueNumber} - - - {issue.title} - - {issue.authorLogin && ( - - {issue.authorLogin} - - )} -
- - + } + }} + role="button" + tabIndex={0} + > + + + #{issue.issueNumber} + + + {issue.title} + + {issue.authorLogin && ( + + {issue.authorLogin} + + )} +
+ + +
+ ); + })} + {hasNextPage && ( +
+ {isFetchingNextPage ? "Loading more…" : ""}
- ); - })} -
- )} + )} +
+ )} +
); } diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/PullRequestsContent/PullRequestsContent.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/PullRequestsContent/PullRequestsContent.tsx index 7e4d9caffd1..6cfdb1e72bb 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/PullRequestsContent/PullRequestsContent.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/PullRequestsContent/PullRequestsContent.tsx @@ -1,10 +1,10 @@ import { Button } from "@superset/ui/button"; import { Checkbox } from "@superset/ui/checkbox"; -import { useQuery } from "@tanstack/react-query"; -import { useId, useMemo, useState } from "react"; +import { useInfiniteQuery } from "@tanstack/react-query"; +import { useEffect, useId, useMemo, useRef, useState } from "react"; import { GoGitPullRequest } from "react-icons/go"; import { HiOutlineArrowTopRightOnSquare } from "react-icons/hi2"; -import { LuPlus } from "react-icons/lu"; +import { LuMinus, LuPlus } from "react-icons/lu"; import { useHostUrl } from "renderer/hooks/host-service/useHostTargetUrl"; import { useDebouncedValue } from "renderer/hooks/useDebouncedValue"; import { getHostServiceClientByUrl } from "renderer/lib/host-service-client"; @@ -21,9 +21,11 @@ import { useOpenNewWorkspaceModal } from "renderer/stores/new-workspace-modal"; interface PullRequestsContentProps { projectFilter: string | null; searchQuery: string; - sectioned?: boolean; + onCollapse?: () => void; } +const PAGE_SIZE = 30; + function normalizeState(state: string, isDraft: boolean): PRState { if (isDraft) return "draft"; const lower = state.toLowerCase(); @@ -35,7 +37,7 @@ function normalizeState(state: string, isDraft: boolean): PRState { export function PullRequestsContent({ projectFilter, searchQuery, - sectioned = false, + onCollapse, }: PullRequestsContentProps) { const [showClosed, setShowClosed] = useState(false); const showClosedId = useId(); @@ -45,7 +47,14 @@ export function PullRequestsContent({ const resetDraft = useNewWorkspaceDraftStore((s) => s.resetDraft); const openModal = useOpenNewWorkspaceModal(); - const { data, isFetching, error } = useQuery({ + const { + data, + isFetching, + isFetchingNextPage, + fetchNextPage, + hasNextPage, + error, + } = useInfiniteQuery({ queryKey: [ "tasks", "searchPullRequests", @@ -54,23 +63,56 @@ export function PullRequestsContent({ debouncedQuery.trim(), showClosed, ], - queryFn: async () => { - if (!hostUrl || !projectFilter) return { pullRequests: [] }; + queryFn: async ({ pageParam }) => { + if (!hostUrl || !projectFilter) { + return { + pullRequests: [], + totalCount: 0, + hasNextPage: false, + page: pageParam, + }; + } const client = getHostServiceClientByUrl(hostUrl); return client.workspaceCreation.searchPullRequests.query({ projectId: projectFilter, query: debouncedQuery.trim() || undefined, - limit: 50, + limit: PAGE_SIZE, includeClosed: showClosed, + page: pageParam, }); }, + initialPageParam: 1, + getNextPageParam: (lastPage) => + lastPage.hasNextPage ? lastPage.page + 1 : undefined, enabled: !!projectFilter && !!hostUrl, retry: false, }); - const pullRequests = useMemo(() => data?.pullRequests ?? [], [data]); - const repoMismatch = - data && "repoMismatch" in data ? data.repoMismatch : null; + const pullRequests = useMemo( + () => data?.pages.flatMap((p) => p.pullRequests) ?? [], + [data], + ); + const totalCount = data?.pages[0]?.totalCount ?? 0; + const repoMismatch = useMemo(() => { + const first = data?.pages[0]; + return first && "repoMismatch" in first ? first.repoMismatch : null; + }, [data]); + + const scrollRef = useRef(null); + const sentinelRef = useRef(null); + useEffect(() => { + const el = sentinelRef.current; + const root = scrollRef.current; + if (!el || !root || !hasNextPage || isFetchingNextPage) return; + const observer = new IntersectionObserver( + (entries) => { + if (entries[0]?.isIntersecting) fetchNextPage(); + }, + { root, rootMargin: "200px" }, + ); + observer.observe(el); + return () => observer.disconnect(); + }, [hasNextPage, isFetchingNextPage, fetchNextPage]); const handleAddToWorkspace = (pr: (typeof pullRequests)[number]) => { if (!projectFilter) return; @@ -91,7 +133,7 @@ export function PullRequestsContent({ if (!projectFilter) { return ( -
+
@@ -102,21 +144,36 @@ export function PullRequestsContent({ ); } + const isInitialLoad = isFetching && pullRequests.length === 0; + const countLabel = isInitialLoad + ? "Loading…" + : totalCount === 0 + ? "0" + : `${pullRequests.length} of ${totalCount}`; + return ( -
- {sectioned && ( -
- - - Pull requests - - - {pullRequests.length} - -
- )} +
+
+ + + Pull requests + + + {countLabel} + + {onCollapse && ( + + )} +
-
+
Show closed / merged - {isFetching && ( + {isFetching && !isInitialLoad && ( Loading… )}
- {error instanceof Error && ( -
- {error.message} -
- )} +
+ {error instanceof Error && ( +
+ {error.message} +
+ )} - {repoMismatch && ( -
- PR URL must match {repoMismatch}. -
- )} + {repoMismatch && ( +
+ PR URL must match {repoMismatch}. +
+ )} - {pullRequests.length === 0 && !isFetching && !error ? ( -
- - {showClosed ? "No pull requests found." : "No open pull requests."} - -
- ) : ( -
- {pullRequests.map((pr) => { - const state = normalizeState(pr.state, pr.isDraft); - return ( - // biome-ignore lint/a11y/useSemanticElements: row contains nested action buttons, so the outer element is a div with role/tabIndex -
handleOpenUrl(pr.url)} - onKeyDown={(e) => { - if (e.key === "Enter" || e.key === " ") { - e.preventDefault(); - handleOpenUrl(pr.url); - } - }} - role="button" - tabIndex={0} - > - - - #{pr.prNumber} - - - {pr.title} - - {pr.authorLogin && ( - - {pr.authorLogin} - - )} -
- - + } + }} + role="button" + tabIndex={0} + > + + + #{pr.prNumber} + + + {pr.title} + + {pr.authorLogin && ( + + {pr.authorLogin} + + )} +
+ + +
+ ); + })} + {hasNextPage && ( +
+ {isFetchingNextPage ? "Loading more…" : ""}
- ); - })} -
- )} + )} +
+ )} +
); } diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksColumn/TasksColumn.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksColumn/TasksColumn.tsx new file mode 100644 index 00000000000..bd45c3e12ca --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksColumn/TasksColumn.tsx @@ -0,0 +1,57 @@ +import { Button } from "@superset/ui/button"; +import { LuMinus } from "react-icons/lu"; +import type { TaskWithStatus } from "../../hooks/useTasksData"; +import { useTasksData } from "../../hooks/useTasksData"; +import { ActiveIcon } from "../shared/icons/ActiveIcon"; +import { TableContent } from "../TableContent"; + +interface TasksColumnProps { + searchQuery: string; + onTaskClick: (task: TaskWithStatus) => void; + onCollapse?: () => void; +} + +export function TasksColumn({ + searchQuery, + onTaskClick, + onCollapse, +}: TasksColumnProps) { + const { data: tasks } = useTasksData({ + filterTab: "all", + searchQuery, + assigneeFilter: null, + }); + const count = tasks.length; + + return ( +
+
+ + + Tasks + + + {count} + + {onCollapse && ( + + )} +
+
+ +
+
+ ); +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksColumn/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksColumn/index.ts new file mode 100644 index 00000000000..4cfe9ab2fcd --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksColumn/index.ts @@ -0,0 +1 @@ +export { TasksColumn } from "./TasksColumn"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/TasksTopBar.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/TasksTopBar.tsx index 56b706bba3a..8fb52dab2a8 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/TasksTopBar.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/TasksTopBar.tsx @@ -17,12 +17,12 @@ import type { TypeTab, ViewMode } from "../../../../stores/tasks-filter-state"; import type { TaskWithStatus } from "../../hooks/useTasksData"; import { ActiveIcon } from "../shared/icons/ActiveIcon"; import { AllIssuesIcon } from "../shared/icons/AllIssuesIcon"; -import { BacklogIcon } from "../shared/icons/BacklogIcon"; import { AssigneeFilter } from "./components/AssigneeFilter"; import { CreateTaskDialog } from "./components/CreateTaskDialog"; import { ProjectFilter } from "./components/ProjectFilter"; import { RunInWorkspacePopover } from "./components/RunInWorkspacePopover"; import { RunInWorkspacePopoverV2 } from "./components/RunInWorkspacePopoverV2"; +import { StatusFilter } from "./components/StatusFilter"; export type TabValue = "all" | "active" | "backlog"; @@ -43,24 +43,6 @@ interface TasksTopBarProps { onProjectFilterChange: (projectId: string | null) => void; } -const TABS = [ - { - value: "all" as const, - label: "All issues", - Icon: AllIssuesIcon, - }, - { - value: "active" as const, - label: "Active", - Icon: ActiveIcon, - }, - { - value: "backlog" as const, - label: "Backlog", - Icon: BacklogIcon, - }, -] as const; - const TYPE_TABS = [ { value: "all" as const, label: "All", Icon: AllIssuesIcon }, { value: "tasks" as const, label: "Tasks", Icon: ActiveIcon }, @@ -84,7 +66,7 @@ export function TasksTopBar({ projectFilter, onProjectFilterChange, }: TasksTopBarProps) { - const showTaskOnlyControls = typeTab === "all" || typeTab === "tasks"; + const showTaskOnlyControls = typeTab === "tasks"; const selectedCount = selectedTasks.length; const searchInputRef = useRef(null); const [isCreateTaskOpen, setIsCreateTaskOpen] = useState(false); @@ -103,7 +85,7 @@ export function TasksTopBar({ return ( <> -
+
{/* Left side: tabs/filters or selection actions */}
{hasSelection ? ( @@ -134,58 +116,42 @@ export function TasksTopBar({ ) : ( <> + + +
+ onTypeTabChange(value as TypeTab)} > - + {TYPE_TABS.map((tab) => { const Icon = tab.Icon; return ( - {tab.label} + + {tab.label} + ); })} -
- - - {showTaskOnlyControls && ( <>
- onTabChange(value as TabValue)} - > - - {TABS.map((tab) => { - const Icon = tab.Icon; - return ( - - - {tab.label} - - ); - })} - - +
@@ -210,7 +176,7 @@ export function TasksTopBar({ onClick={() => setIsCreateTaskOpen(true)} > - New task + New task
@@ -244,7 +210,7 @@ export function TasksTopBar({ )} -
+
{selectedUser ? ( @@ -143,12 +144,14 @@ export function AssigneeFilter({ value, onChange }: AssigneeFilterProps) { image={(selectedUser as SelectUser).image} /> )} - {selectedUser.name} + + {selectedUser.name} + ) : ( <> - Assignee + Assignee )} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/components/ProjectFilter/ProjectFilter.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/components/ProjectFilter/ProjectFilter.tsx index 72d1156cc6e..64a234136c8 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/components/ProjectFilter/ProjectFilter.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/components/ProjectFilter/ProjectFilter.tsx @@ -11,6 +11,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "@superset/ui/popover"; import { useLiveQuery } from "@tanstack/react-db"; import { useMemo, useState } from "react"; import { HiCheck, HiChevronDown, HiOutlineFolder } from "react-icons/hi2"; +import { ProjectThumbnail } from "renderer/routes/_authenticated/components/ProjectThumbnail"; import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; interface ProjectFilterProps { @@ -59,10 +60,19 @@ export function ProjectFilter({ value, onChange }: ProjectFilterProps) { + + + + + + {OPTIONS.map((option) => { + const Icon = option.Icon; + return ( + handleSelect(option.value)} + > + + {option.label} + {option.value === value && ( + + )} + + ); + })} + + + + + + ); +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/components/StatusFilter/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/components/StatusFilter/index.ts new file mode 100644 index 00000000000..a7db761dcaf --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/components/StatusFilter/index.ts @@ -0,0 +1 @@ +export { StatusFilter } from "./StatusFilter"; diff --git a/packages/host-service/src/trpc/router/workspace-creation/procedures/search-github-issues.ts b/packages/host-service/src/trpc/router/workspace-creation/procedures/search-github-issues.ts index 044a8ac963a..4f2ea51cde6 100644 --- a/packages/host-service/src/trpc/router/workspace-creation/procedures/search-github-issues.ts +++ b/packages/host-service/src/trpc/router/workspace-creation/procedures/search-github-issues.ts @@ -16,7 +16,15 @@ interface IssueResult { authorLogin: string | null; } -const ghIssueSchema = z.object({ +interface IssuesPage { + issues: IssueResult[]; + totalCount: number; + hasNextPage: boolean; + page: number; + repoMismatch?: string; +} + +const ghIssueViewSchema = z.object({ number: z.number(), title: z.string(), url: z.string(), @@ -24,17 +32,7 @@ const ghIssueSchema = z.object({ author: z.object({ login: z.string() }).nullable().optional(), }); -const ISSUE_JSON_FIELDS = "number,title,url,state,author"; - -function fromGhIssue(issue: z.infer): IssueResult { - return { - issueNumber: issue.number, - title: issue.title, - url: issue.url, - state: issue.state.toLowerCase(), - authorLogin: issue.author?.login ?? null, - }; -} +const ISSUE_VIEW_FIELDS = "number,title,url,state,author"; async function ghDirectLookup( execGh: ExecGh, @@ -49,45 +47,86 @@ async function ghDirectLookup( "--repo", `${repo.owner}/${repo.name}`, "--json", - ISSUE_JSON_FIELDS, + ISSUE_VIEW_FIELDS, ], { cwd: repo.repoPath ?? undefined }, ); - return fromGhIssue(ghIssueSchema.parse(raw)); + const issue = ghIssueViewSchema.parse(raw); + return { + issueNumber: issue.number, + title: issue.title, + url: issue.url, + state: issue.state.toLowerCase(), + authorLogin: issue.author?.login ?? null, + }; } -async function ghSearch( +const searchIssuesItemSchema = z.object({ + number: z.number(), + title: z.string(), + html_url: z.string(), + state: z.string(), + user: z.object({ login: z.string() }).nullable().optional(), + pull_request: z.unknown().optional(), +}); + +const searchIssuesResponseSchema = z.object({ + total_count: z.number(), + items: z.array(searchIssuesItemSchema), +}); + +async function ghApiSearchIssues( execGh: ExecGh, repo: ResolvedGithubRepo, query: string, includeClosed: boolean, - limit: number, -): Promise { + page: number, + perPage: number, +): Promise<{ + items: IssueResult[]; + totalCount: number; + hasNextPage: boolean; +}> { + const stateFilter = includeClosed ? "" : " is:open"; + const q = + `repo:${repo.owner}/${repo.name} is:issue${stateFilter}${query ? ` ${query}` : ""}`.trim(); const args = [ - "issue", - "list", - "--repo", - `${repo.owner}/${repo.name}`, - "--state", - includeClosed ? "all" : "open", - "--limit", - String(limit), - "--json", - ISSUE_JSON_FIELDS, + "api", + "-X", + "GET", + "search/issues", + "-f", + `q=${q}`, + "-F", + `per_page=${perPage}`, + "-F", + `page=${page}`, + "-f", + "sort=updated", + "-f", + "order=desc", ]; - if (query) { - args.push("--search", query); - } const raw = await execGh(args, { cwd: repo.repoPath ?? undefined }); - const arr = z.array(ghIssueSchema).parse(raw); - return arr.map(fromGhIssue); + const parsed = searchIssuesResponseSchema.parse(raw); + const items: IssueResult[] = parsed.items + .filter((item) => !item.pull_request) + .map((item) => ({ + issueNumber: item.number, + title: item.title, + url: item.html_url, + state: item.state.toLowerCase(), + authorLogin: item.user?.login ?? null, + })); + const hasNextPage = page * perPage < parsed.total_count; + return { items, totalCount: parsed.total_count, hasNextPage }; } export const searchGitHubIssues = protectedProcedure .input(githubSearchInputSchema) - .query(async ({ ctx, input }) => { + .query(async ({ ctx, input }): Promise => { const repo = await resolveGithubRepo(ctx, input.projectId); const limit = input.limit ?? 30; + const page = input.page ?? 1; const raw = input.query?.trim() ?? ""; const normalized = normalizeGitHubQuery(raw, repo, "issue"); @@ -95,6 +134,9 @@ export const searchGitHubIssues = protectedProcedure if (normalized.repoMismatch) { return { issues: [], + totalCount: 0, + hasNextPage: false, + page, repoMismatch: `${repo.owner}/${repo.name}`, }; } @@ -110,18 +152,34 @@ export const searchGitHubIssues = protectedProcedure // Octokit's path filters via `issue.pull_request`; we don't // have that field over `gh`, so detect via the canonical URL. if (issue.url.includes("/pull/")) { - return { issues: [] }; + return { + issues: [], + totalCount: 0, + hasNextPage: false, + page: 1, + }; } - return { issues: [issue] }; + return { + issues: [issue], + totalCount: 1, + hasNextPage: false, + page: 1, + }; } - const issues = await ghSearch( + const result = await ghApiSearchIssues( ctx.execGh, repo, effectiveQuery, input.includeClosed ?? false, + page, limit, ); - return { issues }; + return { + issues: result.items, + totalCount: result.totalCount, + hasNextPage: result.hasNextPage, + page, + }; } catch (ghErr) { console.warn( "[workspaceCreation.searchGitHubIssues] gh path failed; falling back to Octokit", @@ -140,7 +198,12 @@ export const searchGitHubIssues = protectedProcedure issue_number: issueNumber, }); if (issue.pull_request) { - return { issues: [] }; + return { + issues: [], + totalCount: 0, + hasNextPage: false, + page: 1, + }; } return { issues: [ @@ -152,6 +215,9 @@ export const searchGitHubIssues = protectedProcedure authorLogin: issue.user?.login ?? null, }, ], + totalCount: 1, + hasNextPage: false, + page: 1, }; } @@ -161,19 +227,25 @@ export const searchGitHubIssues = protectedProcedure const { data } = await octokit.search.issuesAndPullRequests({ q: query, per_page: limit, + page, sort: "updated", order: "desc", }); + const issues = data.items + .filter((item) => !item.pull_request) + .map((item) => ({ + issueNumber: item.number, + title: item.title, + url: item.html_url, + state: item.state, + authorLogin: item.user?.login ?? null, + })); + const hasNextPage = page * limit < data.total_count; return { - issues: data.items - .filter((item) => !item.pull_request) - .map((item) => ({ - issueNumber: item.number, - title: item.title, - url: item.html_url, - state: item.state, - authorLogin: item.user?.login ?? null, - })), + issues, + totalCount: data.total_count, + hasNextPage, + page, }; } catch (err) { console.warn( diff --git a/packages/host-service/src/trpc/router/workspace-creation/procedures/search-pull-requests.ts b/packages/host-service/src/trpc/router/workspace-creation/procedures/search-pull-requests.ts index 936571aa77d..bb9ad4bf5c5 100644 --- a/packages/host-service/src/trpc/router/workspace-creation/procedures/search-pull-requests.ts +++ b/packages/host-service/src/trpc/router/workspace-creation/procedures/search-pull-requests.ts @@ -17,6 +17,14 @@ interface PullRequestResult { authorLogin: string | null; } +interface PullRequestsPage { + pullRequests: PullRequestResult[]; + totalCount: number; + hasNextPage: boolean; + page: number; + repoMismatch?: string; +} + function normalizePullRequestState( state: string, mergedAt: string | null | undefined, @@ -25,7 +33,7 @@ function normalizePullRequestState( return state.toLowerCase() === "closed" ? "closed" : "open"; } -const ghPrSchema = z.object({ +const ghPrViewSchema = z.object({ number: z.number(), title: z.string(), url: z.string(), @@ -35,18 +43,7 @@ const ghPrSchema = z.object({ mergedAt: z.string().nullable().optional(), }); -function fromGhPr(pr: z.infer): PullRequestResult { - return { - prNumber: pr.number, - title: pr.title, - url: pr.url, - state: normalizePullRequestState(pr.state, pr.mergedAt), - isDraft: pr.isDraft ?? false, - authorLogin: pr.author?.login ?? null, - }; -} - -const PR_JSON_FIELDS = "number,title,url,state,isDraft,author,mergedAt"; +const PR_VIEW_FIELDS = "number,title,url,state,isDraft,author,mergedAt"; async function ghDirectLookup( execGh: ExecGh, @@ -61,45 +58,96 @@ async function ghDirectLookup( "--repo", `${repo.owner}/${repo.name}`, "--json", - PR_JSON_FIELDS, + PR_VIEW_FIELDS, ], { cwd: repo.repoPath ?? undefined }, ); - return fromGhPr(ghPrSchema.parse(raw)); + const pr = ghPrViewSchema.parse(raw); + return { + prNumber: pr.number, + title: pr.title, + url: pr.url, + state: normalizePullRequestState(pr.state, pr.mergedAt), + isDraft: pr.isDraft ?? false, + authorLogin: pr.author?.login ?? null, + }; } -async function ghSearch( +const searchIssuesItemSchema = z.object({ + number: z.number(), + title: z.string(), + html_url: z.string(), + state: z.string(), + draft: z.boolean().optional(), + user: z.object({ login: z.string() }).nullable().optional(), + pull_request: z + .object({ + merged_at: z.string().nullable().optional(), + }) + .optional(), +}); + +const searchIssuesResponseSchema = z.object({ + total_count: z.number(), + items: z.array(searchIssuesItemSchema), +}); + +async function ghApiSearchPullRequests( execGh: ExecGh, repo: ResolvedGithubRepo, query: string, includeClosed: boolean, - limit: number, -): Promise { + page: number, + perPage: number, +): Promise<{ + items: PullRequestResult[]; + totalCount: number; + hasNextPage: boolean; +}> { + const stateFilter = includeClosed ? "" : " is:open"; + const q = + `repo:${repo.owner}/${repo.name} is:pr${stateFilter}${query ? ` ${query}` : ""}`.trim(); const args = [ - "pr", - "list", - "--repo", - `${repo.owner}/${repo.name}`, - "--state", - includeClosed ? "all" : "open", - "--limit", - String(limit), - "--json", - PR_JSON_FIELDS, + "api", + "-X", + "GET", + "search/issues", + "-f", + `q=${q}`, + "-F", + `per_page=${perPage}`, + "-F", + `page=${page}`, + "-f", + "sort=updated", + "-f", + "order=desc", ]; - if (query) { - args.push("--search", query); - } const raw = await execGh(args, { cwd: repo.repoPath ?? undefined }); - const arr = z.array(ghPrSchema).parse(raw); - return arr.map(fromGhPr); + const parsed = searchIssuesResponseSchema.parse(raw); + const items: PullRequestResult[] = parsed.items + .filter((item) => !!item.pull_request) + .map((item) => ({ + prNumber: item.number, + title: item.title, + url: item.html_url, + state: normalizePullRequestState( + item.state, + item.pull_request?.merged_at, + ), + isDraft: item.draft ?? false, + authorLogin: item.user?.login ?? null, + })); + const hasNextPage = page * perPage < parsed.total_count; + return { items, totalCount: parsed.total_count, hasNextPage }; } export const searchPullRequests = protectedProcedure .input(githubSearchInputSchema) - .query(async ({ ctx, input }) => { + .query(async ({ ctx, input }): Promise => { const repo = await resolveGithubRepo(ctx, input.projectId); const limit = input.limit ?? 30; + const page = input.page ?? 1; const raw = input.query?.trim() ?? ""; const normalized = normalizeGitHubQuery(raw, repo, "pull"); @@ -107,6 +155,9 @@ export const searchPullRequests = protectedProcedure if (normalized.repoMismatch) { return { pullRequests: [], + totalCount: 0, + hasNextPage: false, + page, repoMismatch: `${repo.owner}/${repo.name}`, }; } @@ -119,16 +170,27 @@ export const searchPullRequests = protectedProcedure if (normalized.isDirectLookup) { const prNumber = Number.parseInt(effectiveQuery, 10); const pr = await ghDirectLookup(ctx.execGh, repo, prNumber); - return { pullRequests: [pr] }; + return { + pullRequests: [pr], + totalCount: 1, + hasNextPage: false, + page: 1, + }; } - const pullRequests = await ghSearch( + const result = await ghApiSearchPullRequests( ctx.execGh, repo, effectiveQuery, input.includeClosed ?? false, + page, limit, ); - return { pullRequests }; + return { + pullRequests: result.items, + totalCount: result.totalCount, + hasNextPage: result.hasNextPage, + page, + }; } catch (ghErr) { console.warn( "[workspaceCreation.searchPullRequests] gh path failed; falling back to Octokit", @@ -158,6 +220,9 @@ export const searchPullRequests = protectedProcedure authorLogin: pr.user?.login ?? null, }, ], + totalCount: 1, + hasNextPage: false, + page: 1, }; } @@ -167,26 +232,32 @@ export const searchPullRequests = protectedProcedure const { data } = await octokit.search.issuesAndPullRequests({ q: query, per_page: limit, + page, sort: "updated", order: "desc", }); + const pullRequests = data.items + .filter((item) => item.pull_request) + .map((item) => { + const state = normalizePullRequestState( + item.state, + item.pull_request?.merged_at, + ); + return { + prNumber: item.number, + title: item.title, + url: item.html_url, + state, + isDraft: item.draft ?? false, + authorLogin: item.user?.login ?? null, + }; + }); + const hasNextPage = page * limit < data.total_count; return { - pullRequests: data.items - .filter((item) => item.pull_request) - .map((item) => { - const state = normalizePullRequestState( - item.state, - item.pull_request?.merged_at, - ); - return { - prNumber: item.number, - title: item.title, - url: item.html_url, - state, - isDraft: item.draft ?? false, - authorLogin: item.user?.login ?? null, - }; - }), + pullRequests, + totalCount: data.total_count, + hasNextPage, + page, }; } catch (err) { // Both gh and Octokit failed — rethrow so the renderer's toast diff --git a/packages/host-service/src/trpc/router/workspace-creation/schemas.ts b/packages/host-service/src/trpc/router/workspace-creation/schemas.ts index ba6919b9833..cfb39cc7cac 100644 --- a/packages/host-service/src/trpc/router/workspace-creation/schemas.ts +++ b/packages/host-service/src/trpc/router/workspace-creation/schemas.ts @@ -28,4 +28,5 @@ export const githubSearchInputSchema = z.object({ query: z.string().optional(), limit: z.number().min(1).max(100).optional(), includeClosed: z.boolean().optional(), + page: z.number().int().min(1).optional(), }); diff --git a/packages/host-service/test/integration/workspace-creation-github.integration.test.ts b/packages/host-service/test/integration/workspace-creation-github.integration.test.ts index fb911d8b05f..9b629f3e126 100644 --- a/packages/host-service/test/integration/workspace-creation-github.integration.test.ts +++ b/packages/host-service/test/integration/workspace-creation-github.integration.test.ts @@ -81,6 +81,7 @@ describe("workspaceCreation github procedures with mocked Octokit", () => { calls.push({ method: "search.issuesAndPullRequests", args }); return { data: { + total_count: 1, items: [ { number: 7, @@ -252,7 +253,9 @@ describe("resolveGithubRepo prefers the user-configured remoteName", () => { }, issues: { get: async () => ({ data: {} }) }, search: { - issuesAndPullRequests: async () => ({ data: { items: [] } }), + issuesAndPullRequests: async () => ({ + data: { total_count: 0, items: [] }, + }), }, }; @@ -417,8 +420,7 @@ describe("gh CLI is first-class when execGh succeeds", () => { options?: { cwd?: string }, ): Promise => { ghCalls.push({ args, cwd: options?.cwd }); - const verb = args[1]; - if (verb === "view" && args[0] === "pr") { + if (args[0] === "pr" && args[1] === "view") { return { number: Number(args[2]), title: "PR via gh", @@ -429,29 +431,37 @@ describe("gh CLI is first-class when execGh succeeds", () => { mergedAt: null, }; } - if (verb === "list" && args[0] === "pr") { - return [ - { - number: 101, - title: "search result", - url: "https://github.com/octocat/hello/pull/101", - state: "OPEN", - isDraft: false, - author: { login: "carol" }, - mergedAt: null, - }, - ]; - } - if (verb === "list" && args[0] === "issue") { - return [ - { - number: 7, - title: "issue search result", - url: "https://github.com/octocat/hello/issues/7", - state: "OPEN", - author: { login: "dave" }, - }, - ]; + if (args[0] === "api" && args.includes("search/issues")) { + const qIndex = args.indexOf("-f"); + const q = args[qIndex + 1] ?? ""; + const isPr = q.includes("is:pr"); + if (isPr) { + return { + total_count: 1, + items: [ + { + number: 101, + title: "search result", + html_url: "https://github.com/octocat/hello/pull/101", + state: "open", + user: { login: "carol" }, + pull_request: { merged_at: null }, + }, + ], + }; + } + return { + total_count: 1, + items: [ + { + number: 7, + title: "issue search result", + html_url: "https://github.com/octocat/hello/issues/7", + state: "open", + user: { login: "dave" }, + }, + ], + }; } return {}; }; @@ -494,21 +504,24 @@ describe("gh CLI is first-class when execGh succeeds", () => { expect(ghCalls[0].cwd).toBe(realpathSync(repoDir)); }); - test("searchPullRequests free-text invokes `gh pr list --search`", async () => { + test("searchPullRequests free-text invokes `gh api search/issues` with is:pr filter", async () => { const result = await host.trpc.workspaceCreation.searchPullRequests.query({ projectId, query: "find me", }); expect(result.pullRequests).toHaveLength(1); expect(result.pullRequests[0].prNumber).toBe(101); + expect(result.totalCount).toBe(1); + expect(result.hasNextPage).toBe(false); expect(ghCalls).toHaveLength(1); const args = ghCalls[0].args; - expect(args[0]).toBe("pr"); - expect(args[1]).toBe("list"); - expect(args).toContain("--repo"); - expect(args).toContain("octocat/hello"); - expect(args).toContain("--search"); - expect(args).toContain("find me"); + expect(args[0]).toBe("api"); + expect(args).toContain("search/issues"); + const qArg = args[args.indexOf("-f") + 1] ?? ""; + expect(qArg).toContain("repo:octocat/hello"); + expect(qArg).toContain("is:pr"); + expect(qArg).toContain("is:open"); + expect(qArg).toContain("find me"); }); test("searchGitHubIssues #N filters out PRs leaked by `gh issue view`", async () => { @@ -544,15 +557,22 @@ describe("gh CLI is first-class when execGh succeeds", () => { rmSync(localRepo, { recursive: true, force: true }); }); - test("searchGitHubIssues free-text invokes `gh issue list --search`", async () => { + test("searchGitHubIssues free-text invokes `gh api search/issues` with is:issue filter", async () => { const result = await host.trpc.workspaceCreation.searchGitHubIssues.query({ projectId, query: "bug", }); expect(result.issues).toHaveLength(1); expect(result.issues[0].issueNumber).toBe(7); + expect(result.totalCount).toBe(1); + expect(result.hasNextPage).toBe(false); expect(ghCalls).toHaveLength(1); - expect(ghCalls[0].args[0]).toBe("issue"); - expect(ghCalls[0].args[1]).toBe("list"); + const args = ghCalls[0].args; + expect(args[0]).toBe("api"); + expect(args).toContain("search/issues"); + const qArg = args[args.indexOf("-f") + 1] ?? ""; + expect(qArg).toContain("repo:octocat/hello"); + expect(qArg).toContain("is:issue"); + expect(qArg).toContain("bug"); }); }); From cbfdb2995ad07fee4a01fbf18964a281ff5685cd Mon Sep 17 00:00:00 2001 From: AviPeltz Date: Thu, 7 May 2026 19:54:57 -0700 Subject: [PATCH 3/7] feat(desktop): preview pages for PRs and GitHub issues Adds /tasks/pr/$prNumber and /tasks/issue/$issueNumber routes that fetch the full body via existing host-service procedures (pullRequests.getContent, issues.getContent) and render it with MarkdownRenderer. Click on a PR or issue row in the All / PRs / Issues view now opens the preview instead of the GitHub URL; the external- link icon in the row still opens GitHub directly. The preview header carries an "Add to workspace" button so users can seed a new-workspace draft from the detail page, mirroring the row action. --- .../GitHubIssuesContent.tsx | 15 +- .../PullRequestsContent.tsx | 15 +- .../tasks/issue/$issueNumber/page.tsx | 227 +++++++++++++++++ .../_dashboard/tasks/pr/$prNumber/page.tsx | 231 ++++++++++++++++++ 4 files changed, 484 insertions(+), 4 deletions(-) create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/issue/$issueNumber/page.tsx create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/pr/$prNumber/page.tsx diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/GitHubIssuesContent/GitHubIssuesContent.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/GitHubIssuesContent/GitHubIssuesContent.tsx index 928b64015bf..e968653ac10 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/GitHubIssuesContent/GitHubIssuesContent.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/GitHubIssuesContent/GitHubIssuesContent.tsx @@ -1,6 +1,7 @@ import { Button } from "@superset/ui/button"; import { Checkbox } from "@superset/ui/checkbox"; import { useInfiniteQuery } from "@tanstack/react-query"; +import { useNavigate } from "@tanstack/react-router"; import { useEffect, useId, useMemo, useRef, useState } from "react"; import { GoIssueClosed, GoIssueOpened } from "react-icons/go"; import { HiOutlineArrowTopRightOnSquare } from "react-icons/hi2"; @@ -31,6 +32,7 @@ export function GitHubIssuesContent({ const showClosedId = useId(); const debouncedQuery = useDebouncedValue(searchQuery, 300); const hostUrl = useHostUrl(null); + const navigate = useNavigate(); const updateDraft = useNewWorkspaceDraftStore((s) => s.updateDraft); const resetDraft = useNewWorkspaceDraftStore((s) => s.resetDraft); const openModal = useOpenNewWorkspaceModal(); @@ -124,6 +126,15 @@ export function GitHubIssuesContent({ window.open(url, "_blank", "noopener,noreferrer"); }; + const handleOpenPreview = (issueNumber: number) => { + if (!projectFilter) return; + navigate({ + to: "/tasks/issue/$issueNumber", + params: { issueNumber: String(issueNumber) }, + search: { project: projectFilter }, + }); + }; + if (!projectFilter) { return (
@@ -210,11 +221,11 @@ export function GitHubIssuesContent({
handleOpenUrl(issue.url)} + onClick={() => handleOpenPreview(issue.issueNumber)} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); - handleOpenUrl(issue.url); + handleOpenPreview(issue.issueNumber); } }} role="button" diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/PullRequestsContent/PullRequestsContent.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/PullRequestsContent/PullRequestsContent.tsx index 6cfdb1e72bb..faa94092399 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/PullRequestsContent/PullRequestsContent.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/PullRequestsContent/PullRequestsContent.tsx @@ -1,6 +1,7 @@ import { Button } from "@superset/ui/button"; import { Checkbox } from "@superset/ui/checkbox"; import { useInfiniteQuery } from "@tanstack/react-query"; +import { useNavigate } from "@tanstack/react-router"; import { useEffect, useId, useMemo, useRef, useState } from "react"; import { GoGitPullRequest } from "react-icons/go"; import { HiOutlineArrowTopRightOnSquare } from "react-icons/hi2"; @@ -43,6 +44,7 @@ export function PullRequestsContent({ const showClosedId = useId(); const debouncedQuery = useDebouncedValue(searchQuery, 300); const hostUrl = useHostUrl(null); + const navigate = useNavigate(); const updateDraft = useNewWorkspaceDraftStore((s) => s.updateDraft); const resetDraft = useNewWorkspaceDraftStore((s) => s.resetDraft); const openModal = useOpenNewWorkspaceModal(); @@ -131,6 +133,15 @@ export function PullRequestsContent({ window.open(url, "_blank", "noopener,noreferrer"); }; + const handleOpenPreview = (prNumber: number) => { + if (!projectFilter) return; + navigate({ + to: "/tasks/pr/$prNumber", + params: { prNumber: String(prNumber) }, + search: { project: projectFilter }, + }); + }; + if (!projectFilter) { return (
@@ -220,11 +231,11 @@ export function PullRequestsContent({
handleOpenUrl(pr.url)} + onClick={() => handleOpenPreview(pr.prNumber)} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); - handleOpenUrl(pr.url); + handleOpenPreview(pr.prNumber); } }} role="button" diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/issue/$issueNumber/page.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/issue/$issueNumber/page.tsx new file mode 100644 index 00000000000..3b6fd8e73a0 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/issue/$issueNumber/page.tsx @@ -0,0 +1,227 @@ +import { Button } from "@superset/ui/button"; +import { ScrollArea } from "@superset/ui/scroll-area"; +import { useQuery } from "@tanstack/react-query"; +import { createFileRoute, useNavigate } from "@tanstack/react-router"; +import { useMemo } from "react"; +import { GoIssueClosed, GoIssueOpened } from "react-icons/go"; +import { HiArrowLeft } from "react-icons/hi2"; +import { LuExternalLink, LuPlus } from "react-icons/lu"; +import { MarkdownRenderer } from "renderer/components/MarkdownRenderer"; +import { useHostUrl } from "renderer/hooks/host-service/useHostTargetUrl"; +import { getHostServiceClientByUrl } from "renderer/lib/host-service-client"; +import { + type LinkedIssue, + useNewWorkspaceDraftStore, +} from "renderer/stores/new-workspace-draft"; +import { useOpenNewWorkspaceModal } from "renderer/stores/new-workspace-modal"; +import { Route as TasksLayoutRoute } from "../../layout"; + +export const Route = createFileRoute( + "/_authenticated/_dashboard/tasks/issue/$issueNumber/", +)({ + component: IssueDetailPage, +}); + +function IssueDetailPage() { + const { issueNumber: issueNumberRaw } = Route.useParams(); + const issueNumber = Number.parseInt(issueNumberRaw, 10); + const search = TasksLayoutRoute.useSearch(); + const navigate = useNavigate(); + const hostUrl = useHostUrl(null); + const projectId = search.project ?? null; + const updateDraft = useNewWorkspaceDraftStore((s) => s.updateDraft); + const resetDraft = useNewWorkspaceDraftStore((s) => s.resetDraft); + const openModal = useOpenNewWorkspaceModal(); + + const backSearch = useMemo(() => { + const s: Record = {}; + if (search.tab) s.tab = search.tab; + if (search.assignee) s.assignee = search.assignee; + if (search.search) s.search = search.search; + if (search.type) s.type = search.type; + if (search.project) s.project = search.project; + return s; + }, [search]); + + const { data, isLoading, error } = useQuery({ + queryKey: ["issue-detail", projectId, hostUrl, issueNumber], + queryFn: async () => { + if (!hostUrl || !projectId) return null; + const client = getHostServiceClientByUrl(hostUrl); + return client.issues.getContent.query({ + projectId, + issueNumber, + }); + }, + enabled: !!hostUrl && !!projectId && Number.isFinite(issueNumber), + retry: false, + }); + + const handleBack = () => { + navigate({ to: "/tasks", search: backSearch }); + }; + + const handleAddToWorkspace = () => { + if (!projectId || !data) return; + const linkedIssue: LinkedIssue = { + slug: `gh-${data.number}`, + title: data.title, + source: "github", + url: data.url, + number: data.number, + state: data.state.toLowerCase() === "closed" ? "closed" : "open", + }; + resetDraft(); + updateDraft({ + selectedProjectId: projectId, + linkedIssues: [linkedIssue], + }); + openModal(projectId); + }; + + if (!projectId) { + return ( +
+ No project specified. +
+ ); + } + + if (isLoading) { + return ( +
+ Loading issue… +
+ ); + } + + if (error instanceof Error || !data) { + return ( +
+
+
+ {error instanceof Error ? error.message : "Issue not found."} +
+
+ ); + } + + const isClosed = data.state.toLowerCase() === "closed"; + const StateIcon = isClosed ? GoIssueClosed : GoIssueOpened; + + return ( +
+
+ + +
+
+ +

+ {data.title} +

+
+ +
+ {data.state} + {data.author && ( + <> + · + by {data.author} + + )} +
+ + {data.body.trim() ? ( + + ) : ( +

+ No description provided. +

+ )} +
+
+
+ ); +} + +interface HeaderProps { + issueNumber: number; + url: string | null; + isClosed: boolean; + onBack: () => void; + onAddToWorkspace: (() => void) | null; +} + +function Header({ + issueNumber, + url, + isClosed, + onBack, + onAddToWorkspace, +}: HeaderProps) { + const StateIcon = isClosed ? GoIssueClosed : GoIssueOpened; + return ( +
+ + + + #{issueNumber} + +
+ {url && ( + + + + )} + {onAddToWorkspace && ( + + )} +
+
+ ); +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/pr/$prNumber/page.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/pr/$prNumber/page.tsx new file mode 100644 index 00000000000..b98b992c41e --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/pr/$prNumber/page.tsx @@ -0,0 +1,231 @@ +import { Button } from "@superset/ui/button"; +import { ScrollArea } from "@superset/ui/scroll-area"; +import { useQuery } from "@tanstack/react-query"; +import { createFileRoute, useNavigate } from "@tanstack/react-router"; +import { useMemo } from "react"; +import { HiArrowLeft } from "react-icons/hi2"; +import { LuExternalLink, LuPlus } from "react-icons/lu"; +import { MarkdownRenderer } from "renderer/components/MarkdownRenderer"; +import { useHostUrl } from "renderer/hooks/host-service/useHostTargetUrl"; +import { getHostServiceClientByUrl } from "renderer/lib/host-service-client"; +import { + PRIcon, + type PRState, +} from "renderer/screens/main/components/PRIcon/PRIcon"; +import { + type LinkedPR, + useNewWorkspaceDraftStore, +} from "renderer/stores/new-workspace-draft"; +import { useOpenNewWorkspaceModal } from "renderer/stores/new-workspace-modal"; +import { Route as TasksLayoutRoute } from "../../layout"; + +export const Route = createFileRoute( + "/_authenticated/_dashboard/tasks/pr/$prNumber/", +)({ + component: PullRequestDetailPage, +}); + +function resolveState(state: string, isDraft: boolean): PRState { + if (isDraft) return "draft"; + const lower = state.toLowerCase(); + if (lower === "merged") return "merged"; + if (lower === "closed") return "closed"; + return "open"; +} + +function PullRequestDetailPage() { + const { prNumber: prNumberRaw } = Route.useParams(); + const prNumber = Number.parseInt(prNumberRaw, 10); + const search = TasksLayoutRoute.useSearch(); + const navigate = useNavigate(); + const hostUrl = useHostUrl(null); + const projectId = search.project ?? null; + const updateDraft = useNewWorkspaceDraftStore((s) => s.updateDraft); + const resetDraft = useNewWorkspaceDraftStore((s) => s.resetDraft); + const openModal = useOpenNewWorkspaceModal(); + + const backSearch = useMemo(() => { + const s: Record = {}; + if (search.tab) s.tab = search.tab; + if (search.assignee) s.assignee = search.assignee; + if (search.search) s.search = search.search; + if (search.type) s.type = search.type; + if (search.project) s.project = search.project; + return s; + }, [search]); + + const { data, isLoading, error } = useQuery({ + queryKey: ["pull-request-detail", projectId, hostUrl, prNumber], + queryFn: async () => { + if (!hostUrl || !projectId) return null; + const client = getHostServiceClientByUrl(hostUrl); + return client.pullRequests.getContent.query({ + projectId, + prNumber, + }); + }, + enabled: !!hostUrl && !!projectId && Number.isFinite(prNumber), + retry: false, + }); + + const handleBack = () => { + navigate({ to: "/tasks", search: backSearch }); + }; + + const handleAddToWorkspace = () => { + if (!projectId || !data) return; + const linkedPR: LinkedPR = { + prNumber: data.number, + title: data.title, + url: data.url, + state: resolveState(data.state, data.isDraft), + }; + resetDraft(); + updateDraft({ selectedProjectId: projectId, linkedPR }); + openModal(projectId); + }; + + if (!projectId) { + return ( +
+ No project specified. +
+ ); + } + + if (isLoading) { + return ( +
+ Loading pull request… +
+ ); + } + + if (error instanceof Error || !data) { + return ( +
+
+
+ {error instanceof Error ? error.message : "Pull request not found."} +
+
+ ); + } + + const state = resolveState(data.state, data.isDraft); + const stateLabel = data.isDraft ? "Draft" : data.state; + const branchSummary = data.branch + ? `${data.headRepositoryOwner && data.isCrossRepository ? `${data.headRepositoryOwner}:${data.branch}` : data.branch} → ${data.baseBranch}` + : null; + + return ( +
+
+ + +
+
+ +

+ {data.title} +

+
+ +
+ {stateLabel} + {data.author && ( + <> + · + by {data.author} + + )} + {branchSummary && ( + <> + · + {branchSummary} + + )} +
+ + {data.body.trim() ? ( + + ) : ( +

+ No description provided. +

+ )} +
+
+
+ ); +} + +interface HeaderProps { + prNumber: number; + url: string | null; + state: PRState; + onBack: () => void; + onAddToWorkspace: (() => void) | null; +} + +function Header({ + prNumber, + url, + state, + onBack, + onAddToWorkspace, +}: HeaderProps) { + return ( +
+ + + + #{prNumber} + +
+ {url && ( + + + + )} + {onAddToWorkspace && ( + + )} +
+
+ ); +} From e28b170965175c223dd0743bc5437475f322627d Mon Sep 17 00:00:00 2001 From: AviPeltz Date: Thu, 7 May 2026 20:16:14 -0700 Subject: [PATCH 4/7] feat(desktop): cache tasks/PR/issue lists across navigation and restart MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 1: tunes React Query staleness on the four tasks-page queries (PR list, issue list, PR detail, issue detail) — `staleTime: 30s`, `gcTime: 10m`, `placeholderData: keepPreviousData` so toggling a filter or navigating away and back keeps the prior list rendered while new data is fetched in the background. Phase 2: wraps the renderer's QueryClient with PersistQueryClient- Provider, persisting only tasks-page queries (whitelisted by queryKey prefix) to IndexedDB via idb-keyval. First paint after relaunch renders from the rehydrated cache; the persister auto-writes successful queries on a default throttle. Buster + 24h maxAge guard against stale shapes. --- apps/desktop/package.json | 2 + .../ElectronTRPCProvider.tsx | 53 ++++++++++++++++--- .../GitHubIssuesContent.tsx | 5 +- .../PullRequestsContent.tsx | 5 +- .../tasks/issue/$issueNumber/page.tsx | 2 + .../_dashboard/tasks/pr/$prNumber/page.tsx | 2 + bun.lock | 12 +++++ 7 files changed, 73 insertions(+), 8 deletions(-) diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 7d64aeaf316..0a96c00992a 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -103,8 +103,10 @@ "@tanstack/electric-db-collection": "0.3.3", "@tanstack/electron-db-sqlite-persistence": "0.1.9", "@tanstack/node-db-sqlite-persistence": "0.1.9", + "@tanstack/query-async-storage-persister": "^5.100.9", "@tanstack/react-db": "0.1.83", "@tanstack/react-query": "^5.90.19", + "@tanstack/react-query-persist-client": "^5.100.9", "@tanstack/react-router": "^1.147.3", "@tanstack/react-table": "^8.21.3", "@tanstack/react-virtual": "^3.13.18", diff --git a/apps/desktop/src/renderer/providers/ElectronTRPCProvider/ElectronTRPCProvider.tsx b/apps/desktop/src/renderer/providers/ElectronTRPCProvider/ElectronTRPCProvider.tsx index 58cc75aa1fa..6ca2b3e28ae 100644 --- a/apps/desktop/src/renderer/providers/ElectronTRPCProvider/ElectronTRPCProvider.tsx +++ b/apps/desktop/src/renderer/providers/ElectronTRPCProvider/ElectronTRPCProvider.tsx @@ -1,7 +1,13 @@ -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { createAsyncStoragePersister } from "@tanstack/query-async-storage-persister"; +import { QueryClient } from "@tanstack/react-query"; +import { PersistQueryClientProvider } from "@tanstack/react-query-persist-client"; +import { del, get, set } from "idb-keyval"; import { electronTrpc } from "renderer/lib/electron-trpc"; import { electronReactClient } from "../../lib/trpc-client"; +// Bump when query response shapes change — invalidates the persisted cache. +const PERSIST_BUSTER = "v1"; + // Shared QueryClient for tRPC hooks and router loaders const queryClient = new QueryClient({ defaultOptions: { @@ -16,10 +22,30 @@ const queryClient = new QueryClient({ }, }); -/** - * Provider for Electron IPC tRPC client. - * QueryClient is shared with router context for loader prefetching. - */ +// IndexedDB-backed persister. localStorage is too small (~5MB) for the +// volume of PR/issue rows we cache. idb-keyval uses a single object store +// keyed by the persister's `key` below. +const persister = createAsyncStoragePersister({ + storage: { + getItem: async (key) => (await get(key)) ?? null, + setItem: async (key, value) => { + await set(key, value); + }, + removeItem: async (key) => { + await del(key); + }, + }, + key: "superset-rq-cache", +}); + +// Whitelist of queryKey prefixes worth persisting — anything else (auth +// tokens, ephemeral host state, transient mutations) is left in memory only. +const PERSIST_KEY_PREFIXES = new Set([ + "tasks", // PR/issue list infinite queries + "pull-request-detail", + "issue-detail", +]); + export function ElectronTRPCProvider({ children, }: { @@ -30,7 +56,22 @@ export function ElectronTRPCProvider({ client={electronReactClient} queryClient={queryClient} > - {children} + { + const head = query.queryKey[0]; + return typeof head === "string" && PERSIST_KEY_PREFIXES.has(head); + }, + }, + }} + > + {children} + ); } diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/GitHubIssuesContent/GitHubIssuesContent.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/GitHubIssuesContent/GitHubIssuesContent.tsx index e968653ac10..a2d23546515 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/GitHubIssuesContent/GitHubIssuesContent.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/GitHubIssuesContent/GitHubIssuesContent.tsx @@ -1,6 +1,6 @@ import { Button } from "@superset/ui/button"; import { Checkbox } from "@superset/ui/checkbox"; -import { useInfiniteQuery } from "@tanstack/react-query"; +import { keepPreviousData, useInfiniteQuery } from "@tanstack/react-query"; import { useNavigate } from "@tanstack/react-router"; import { useEffect, useId, useMemo, useRef, useState } from "react"; import { GoIssueClosed, GoIssueOpened } from "react-icons/go"; @@ -74,6 +74,9 @@ export function GitHubIssuesContent({ initialPageParam: 1, getNextPageParam: (lastPage) => lastPage.hasNextPage ? lastPage.page + 1 : undefined, + staleTime: 30_000, + gcTime: 10 * 60_000, + placeholderData: keepPreviousData, enabled: !!projectFilter && !!hostUrl, retry: false, }); diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/PullRequestsContent/PullRequestsContent.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/PullRequestsContent/PullRequestsContent.tsx index faa94092399..31d6cbd2a91 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/PullRequestsContent/PullRequestsContent.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/PullRequestsContent/PullRequestsContent.tsx @@ -1,6 +1,6 @@ import { Button } from "@superset/ui/button"; import { Checkbox } from "@superset/ui/checkbox"; -import { useInfiniteQuery } from "@tanstack/react-query"; +import { keepPreviousData, useInfiniteQuery } from "@tanstack/react-query"; import { useNavigate } from "@tanstack/react-router"; import { useEffect, useId, useMemo, useRef, useState } from "react"; import { GoGitPullRequest } from "react-icons/go"; @@ -86,6 +86,9 @@ export function PullRequestsContent({ initialPageParam: 1, getNextPageParam: (lastPage) => lastPage.hasNextPage ? lastPage.page + 1 : undefined, + staleTime: 30_000, + gcTime: 10 * 60_000, + placeholderData: keepPreviousData, enabled: !!projectFilter && !!hostUrl, retry: false, }); diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/issue/$issueNumber/page.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/issue/$issueNumber/page.tsx index 3b6fd8e73a0..49bf730e11e 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/issue/$issueNumber/page.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/issue/$issueNumber/page.tsx @@ -55,6 +55,8 @@ function IssueDetailPage() { }, enabled: !!hostUrl && !!projectId && Number.isFinite(issueNumber), retry: false, + staleTime: 30_000, + gcTime: 10 * 60_000, }); const handleBack = () => { diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/pr/$prNumber/page.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/pr/$prNumber/page.tsx index b98b992c41e..62abfeb6ce4 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/pr/$prNumber/page.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/pr/$prNumber/page.tsx @@ -66,6 +66,8 @@ function PullRequestDetailPage() { }, enabled: !!hostUrl && !!projectId && Number.isFinite(prNumber), retry: false, + staleTime: 30_000, + gcTime: 10 * 60_000, }); const handleBack = () => { diff --git a/bun.lock b/bun.lock index c17af3d7927..913fe85eff6 100644 --- a/bun.lock +++ b/bun.lock @@ -180,8 +180,10 @@ "@tanstack/electric-db-collection": "0.3.3", "@tanstack/electron-db-sqlite-persistence": "0.1.9", "@tanstack/node-db-sqlite-persistence": "0.1.9", + "@tanstack/query-async-storage-persister": "^5.100.9", "@tanstack/react-db": "0.1.83", "@tanstack/react-query": "^5.90.19", + "@tanstack/react-query-persist-client": "^5.100.9", "@tanstack/react-router": "^1.147.3", "@tanstack/react-table": "^8.21.3", "@tanstack/react-virtual": "^3.13.18", @@ -2733,16 +2735,22 @@ "@tanstack/pacer-lite": ["@tanstack/pacer-lite@0.2.1", "", {}, "sha512-3PouiFjR4B6x1c969/Pl4ZIJleof1M0n6fNX8NRiC9Sqv1g06CVDlEaXUR4212ycGFyfq4q+t8Gi37Xy+z34iQ=="], + "@tanstack/query-async-storage-persister": ["@tanstack/query-async-storage-persister@5.100.9", "", { "dependencies": { "@tanstack/query-core": "5.100.9", "@tanstack/query-persist-client-core": "5.100.9" } }, "sha512-KTWqpXIAwuR1bSIxfOnoRr7hOMxfrtLk9kxSuDfOu4sSBEFqHp9+aDp8Ig6YdHxCclNI86SizPnmKmSqNxcJxg=="], + "@tanstack/query-core": ["@tanstack/query-core@5.95.2", "", {}, "sha512-o4T8vZHZET4Bib3jZ/tCW9/7080urD4c+0/AUaYVpIqOsr7y0reBc1oX3ttNaSW5mYyvZHctiQ/UOP2PfdmFEQ=="], "@tanstack/query-devtools": ["@tanstack/query-devtools@5.95.2", "", {}, "sha512-QfaoqBn9uAZ+ICkA8brd1EHj+qBF6glCFgt94U8XP5BT6ppSsDBI8IJ00BU+cAGjQzp6wcKJL2EmRYvxy0TWIg=="], + "@tanstack/query-persist-client-core": ["@tanstack/query-persist-client-core@5.100.9", "", { "dependencies": { "@tanstack/query-core": "5.100.9" } }, "sha512-sCPZZp3D9sOeqcA4SDxjUIm4wVq8PwHebH4ouFZetwjT4xvGjT/cLBQ4Sst+BFcFuk745pCPkf3T4MFliLHECQ=="], + "@tanstack/react-db": ["@tanstack/react-db@0.1.83", "", { "dependencies": { "@tanstack/db": "0.6.5", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-LNV0C7OARazooT2hLTr5anXo6tbEyX2rHZQ0j9HZ/iNBI+Tx/y19o5Nd3ooyAYz5LEHJJxb8iM8ZTVB/diGnXw=="], "@tanstack/react-query": ["@tanstack/react-query@5.95.2", "", { "dependencies": { "@tanstack/query-core": "5.95.2" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-/wGkvLj/st5Ud1Q76KF1uFxScV7WeqN1slQx5280ycwAyYkIPGaRZAEgHxe3bjirSd5Zpwkj6zNcR4cqYni/ZA=="], "@tanstack/react-query-devtools": ["@tanstack/react-query-devtools@5.95.2", "", { "dependencies": { "@tanstack/query-devtools": "5.95.2" }, "peerDependencies": { "@tanstack/react-query": "^5.95.2", "react": "^18 || ^19" } }, "sha512-AFQFmbznVkbtfpx8VJ2DylW17wWagQel/qLstVLkYmNRo2CmJt3SNej5hvl6EnEeljJIdC3BTB+W7HZtpsH+3g=="], + "@tanstack/react-query-persist-client": ["@tanstack/react-query-persist-client@5.100.9", "", { "dependencies": { "@tanstack/query-persist-client-core": "5.100.9" }, "peerDependencies": { "@tanstack/react-query": "^5.100.9", "react": "^18 || ^19" } }, "sha512-qO7j+3VUZm4YH4T3dWszDqgvO+5+6NB1kSY+QmF7JD6+IONfeNmGyOzyobjmrX+6CYLPQtQ+sjM9vFYaSOAv6A=="], + "@tanstack/react-router": ["@tanstack/react-router@1.168.8", "", { "dependencies": { "@tanstack/history": "1.161.6", "@tanstack/react-store": "^0.9.3", "@tanstack/router-core": "1.168.7", "isbot": "^5.1.22" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-t0S0QueXubBKmI9eLPcN/A1sLQgTu8/yHerjrvvsGeD12zMdw0uJPKwEKpStQF2OThQtw64cs34uUSYXBUTSNw=="], "@tanstack/react-store": ["@tanstack/react-store@0.9.3", "", { "dependencies": { "@tanstack/store": "0.9.3", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-y2iHd/N9OkoQbFJLUX1T9vbc2O9tjH0pQRgTcx1/Nz4IlwLvkgpuglXUx+mXt0g5ZDFrEeDnONPqkbfxXJKwRg=="], @@ -6649,6 +6657,10 @@ "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "@tanstack/query-async-storage-persister/@tanstack/query-core": ["@tanstack/query-core@5.100.9", "", {}, "sha512-SJSFw1S8+kQ0+knv/XGfrbocWoAlT7vDKsSImtLx3ZPQmEcR46hkDjLSvynSy25N8Ms4tIEini1FuBd5k7IscQ=="], + + "@tanstack/query-persist-client-core/@tanstack/query-core": ["@tanstack/query-core@5.100.9", "", {}, "sha512-SJSFw1S8+kQ0+knv/XGfrbocWoAlT7vDKsSImtLx3ZPQmEcR46hkDjLSvynSy25N8Ms4tIEini1FuBd5k7IscQ=="], + "@tanstack/router-generator/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], "@tanstack/router-plugin/unplugin": ["unplugin@2.3.11", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww=="], From f7387e72d2e445cbeb86e9c6fd58edcec38ceb09 Mon Sep 17 00:00:00 2001 From: AviPeltz Date: Thu, 7 May 2026 22:25:34 -0700 Subject: [PATCH 5/7] feat(desktop): drop All tab, allow http(s) images, add refresh button MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removes the "All" type tab from the tasks page along with the resizable panel layout (AllModePanels, CollapsedColumnRail, TasksColumn). Default type tab is now "tasks"; tab bar is Tasks / PRs / Issues. Cleans up the URL search-param validation, store default, and TasksView render. SafeImage now allows data: and http(s):// sources so images embedded in PR / issue / task markdown render. file://, absolute paths, and UNC paths are still blocked. Adds a refresh icon button to the PR and issue section headers — calls the infinite query's refetch and spins while fetching. --- .../components/SafeImage/SafeImage.tsx | 30 ++--- .../MarkdownRenderer/styles/tufte/config.tsx | 1 - .../tasks/components/TasksView/TasksView.tsx | 23 ++-- .../AllModePanels/AllModePanels.tsx | 121 ------------------ .../components/AllModePanels/index.ts | 1 - .../CollapsedColumnRail.tsx | 36 ------ .../components/CollapsedColumnRail/index.ts | 1 - .../GitHubIssuesContent.tsx | 14 +- .../PullRequestsContent.tsx | 14 +- .../components/TasksColumn/TasksColumn.tsx | 57 --------- .../TasksView/components/TasksColumn/index.ts | 1 - .../components/TasksTopBar/TasksTopBar.tsx | 2 - .../_dashboard/tasks/layout.tsx | 4 +- .../tasks/stores/tasks-filter-state.ts | 4 +- 14 files changed, 51 insertions(+), 258 deletions(-) delete mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/AllModePanels/AllModePanels.tsx delete mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/AllModePanels/index.ts delete mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/CollapsedColumnRail/CollapsedColumnRail.tsx delete mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/CollapsedColumnRail/index.ts delete mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksColumn/TasksColumn.tsx delete mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksColumn/index.ts diff --git a/apps/desktop/src/renderer/components/MarkdownRenderer/components/SafeImage/SafeImage.tsx b/apps/desktop/src/renderer/components/MarkdownRenderer/components/SafeImage/SafeImage.tsx index 91295eaa98f..fbfcb47738f 100644 --- a/apps/desktop/src/renderer/components/MarkdownRenderer/components/SafeImage/SafeImage.tsx +++ b/apps/desktop/src/renderer/components/MarkdownRenderer/components/SafeImage/SafeImage.tsx @@ -3,31 +3,31 @@ import { LuImageOff } from "react-icons/lu"; /** * Check if an image source is safe to load. * - * Uses strict ALLOWLIST approach - only data: URLs are safe. - * * ALLOWED: * - data: URLs (embedded base64 images) + * - http(s):// URLs (GitHub user-attachments, avatars, etc.) * * BLOCKED (everything else): - * - http://, https:// (tracking pixels, privacy leak) * - file:// URLs (arbitrary local file access) * - Absolute paths /... or \... (become file:// in Electron) * - Relative paths with .. (can escape repo boundary) * - UNC paths //server/share (Windows NTLM credential leak) * - Empty or malformed sources * - * Security context: In Electron production, renderer loads via file:// - * protocol. Any non-data: image src could access local filesystem or - * trigger network requests to attacker-controlled servers. + * Trade-off: http(s) sources can phone home (tracking pixels). Acceptable + * here because the markdown comes from trusted sources (GitHub PR/issue + * bodies, user-authored task descriptions) where image embedding is part + * of the expected UX. */ function isSafeImageSrc(src: string | undefined): boolean { if (!src) return false; const trimmed = src.trim(); if (trimmed.length === 0) return false; + const lower = trimmed.toLowerCase(); - // Only allow data: URLs (embedded images) - // These are self-contained and can't access external resources - return trimmed.toLowerCase().startsWith("data:"); + if (lower.startsWith("data:")) return true; + if (lower.startsWith("https://") || lower.startsWith("http://")) return true; + return false; } interface SafeImageProps { @@ -37,15 +37,11 @@ interface SafeImageProps { } /** - * Safe image component for untrusted markdown content. - * - * Only renders embedded data: URLs. All other sources are blocked - * to prevent local file access, network requests, and path traversal - * attacks from malicious repository content. + * Safe image component for markdown content. * - * Future: Could add opt-in support for repo-relative images via a - * secure loader that validates paths through secureFs and serves - * as blob: URLs. + * Renders data: and http(s):// images. file://, absolute paths, UNC paths, + * and traversal sources are blocked to prevent local-filesystem access from + * malicious markdown content. */ export function SafeImage({ src, alt, className }: SafeImageProps) { if (!isSafeImageSrc(src)) { diff --git a/apps/desktop/src/renderer/components/MarkdownRenderer/styles/tufte/config.tsx b/apps/desktop/src/renderer/components/MarkdownRenderer/styles/tufte/config.tsx index 5173f7e4557..488537d63aa 100644 --- a/apps/desktop/src/renderer/components/MarkdownRenderer/styles/tufte/config.tsx +++ b/apps/desktop/src/renderer/components/MarkdownRenderer/styles/tufte/config.tsx @@ -12,7 +12,6 @@ export const tufteConfig: MarkdownStyleConfig = { {children} ), - // Block external images for privacy (tracking pixels, etc.) img: ({ src, alt }) => , }, }; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/TasksView.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/TasksView.tsx index bc35a01c949..f13dada54d7 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/TasksView.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/TasksView.tsx @@ -3,7 +3,6 @@ import { useNavigate } from "@tanstack/react-router"; import { useCallback, useEffect, useRef, useState } from "react"; import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; import { useTasksFilterStore } from "../../stores/tasks-filter-state"; -import { AllModePanels } from "./components/AllModePanels"; import { BoardContent } from "./components/BoardContent"; import { GitHubIssuesContent } from "./components/GitHubIssuesContent"; import { LinearCTA } from "./components/LinearCTA"; @@ -16,7 +15,7 @@ interface TasksViewProps { initialTab?: "all" | "active" | "backlog"; initialAssignee?: string; initialSearch?: string; - initialType?: "all" | "tasks" | "prs" | "issues"; + initialType?: "tasks" | "prs" | "issues"; initialProject?: string; } @@ -32,7 +31,7 @@ export function TasksView({ const currentTab: TabValue = initialTab ?? "all"; const [searchQuery, setSearchQuery] = useState(initialSearch ?? ""); const assigneeFilter = initialAssignee ?? null; - const typeTab = initialType ?? "all"; + const typeTab = initialType ?? "tasks"; const projectFilter = initialProject ?? null; const { @@ -52,7 +51,7 @@ export function TasksView({ tab?: TabValue; assignee?: string | null; search?: string; - type?: "all" | "tasks" | "prs" | "issues"; + type?: "tasks" | "prs" | "issues"; project?: string | null; }) => { const tab = overrides.tab ?? currentTab; @@ -68,7 +67,7 @@ export function TasksView({ if (tab !== "all") search.tab = tab; if (assignee) search.assignee = assignee; if (query) search.search = query; - if (type !== "all") search.type = type; + if (type !== "tasks") search.type = type; if (project) search.project = project; return search; }, @@ -149,7 +148,7 @@ export function TasksView({ }); }; - const handleTypeTabChange = (type: "all" | "tasks" | "prs" | "issues") => { + const handleTypeTabChange = (type: "tasks" | "prs" | "issues") => { navigate({ to: "/tasks", search: buildSearch({ type }), replace: true }); }; @@ -183,9 +182,9 @@ export function TasksView({ const showLinearCTA = integrations !== undefined && !isLinearConnected && typeTab === "tasks"; - const showTasks = typeTab === "all" || typeTab === "tasks"; - const showPRs = typeTab === "all" || typeTab === "prs"; - const showIssues = typeTab === "all" || typeTab === "issues"; + const showTasks = typeTab === "tasks"; + const showPRs = typeTab === "prs"; + const showIssues = typeTab === "issues"; return (
@@ -210,12 +209,6 @@ export function TasksView({ {showLinearCTA ? ( - ) : typeTab === "all" ? ( - ) : (
{showTasks && diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/AllModePanels/AllModePanels.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/AllModePanels/AllModePanels.tsx deleted file mode 100644 index 54927c49f83..00000000000 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/AllModePanels/AllModePanels.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import { - ResizableHandle, - ResizablePanel, - ResizablePanelGroup, -} from "@superset/ui/resizable"; -import { useCallback, useRef, useState } from "react"; -import { GoGitPullRequest, GoIssueOpened } from "react-icons/go"; -import type { ImperativePanelHandle } from "react-resizable-panels"; -import type { TaskWithStatus } from "../../hooks/useTasksData"; -import { CollapsedColumnRail } from "../CollapsedColumnRail"; -import { GitHubIssuesContent } from "../GitHubIssuesContent"; -import { PullRequestsContent } from "../PullRequestsContent"; -import { ActiveIcon } from "../shared/icons/ActiveIcon"; -import { TasksColumn } from "../TasksColumn"; - -interface AllModePanelsProps { - projectFilter: string | null; - searchQuery: string; - onTaskClick: (task: TaskWithStatus) => void; -} - -export function AllModePanels({ - projectFilter, - searchQuery, - onTaskClick, -}: AllModePanelsProps) { - const tasksRef = useRef(null); - const prsRef = useRef(null); - const issuesRef = useRef(null); - - const [tasksCollapsed, setTasksCollapsed] = useState(false); - const [prsCollapsed, setPrsCollapsed] = useState(false); - const [issuesCollapsed, setIssuesCollapsed] = useState(false); - - const collapseTasks = useCallback(() => tasksRef.current?.collapse(), []); - const expandTasks = useCallback(() => tasksRef.current?.expand(), []); - const collapsePrs = useCallback(() => prsRef.current?.collapse(), []); - const expandPrs = useCallback(() => prsRef.current?.expand(), []); - const collapseIssues = useCallback(() => issuesRef.current?.collapse(), []); - const expandIssues = useCallback(() => issuesRef.current?.expand(), []); - - return ( - - setTasksCollapsed(true)} - onExpand={() => setTasksCollapsed(false)} - > - {tasksCollapsed ? ( - } - onExpand={expandTasks} - /> - ) : ( - - )} - - - setPrsCollapsed(true)} - onExpand={() => setPrsCollapsed(false)} - > - {prsCollapsed ? ( - } - onExpand={expandPrs} - /> - ) : ( - - )} - - - setIssuesCollapsed(true)} - onExpand={() => setIssuesCollapsed(false)} - > - {issuesCollapsed ? ( - } - onExpand={expandIssues} - /> - ) : ( - - )} - - - ); -} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/AllModePanels/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/AllModePanels/index.ts deleted file mode 100644 index 6ea6e64dd34..00000000000 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/AllModePanels/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { AllModePanels } from "./AllModePanels"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/CollapsedColumnRail/CollapsedColumnRail.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/CollapsedColumnRail/CollapsedColumnRail.tsx deleted file mode 100644 index bcb1c8734d1..00000000000 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/CollapsedColumnRail/CollapsedColumnRail.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import type { ReactNode } from "react"; - -interface CollapsedColumnRailProps { - label: string; - icon: ReactNode; - count?: string; - onExpand: () => void; -} - -export function CollapsedColumnRail({ - label, - icon, - count, - onExpand, -}: CollapsedColumnRailProps) { - return ( - - ); -} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/CollapsedColumnRail/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/CollapsedColumnRail/index.ts deleted file mode 100644 index a55f1184dcd..00000000000 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/CollapsedColumnRail/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { CollapsedColumnRail } from "./CollapsedColumnRail"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/GitHubIssuesContent/GitHubIssuesContent.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/GitHubIssuesContent/GitHubIssuesContent.tsx index a2d23546515..90f6c56c348 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/GitHubIssuesContent/GitHubIssuesContent.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/GitHubIssuesContent/GitHubIssuesContent.tsx @@ -5,7 +5,7 @@ import { useNavigate } from "@tanstack/react-router"; import { useEffect, useId, useMemo, useRef, useState } from "react"; import { GoIssueClosed, GoIssueOpened } from "react-icons/go"; import { HiOutlineArrowTopRightOnSquare } from "react-icons/hi2"; -import { LuMinus, LuPlus } from "react-icons/lu"; +import { LuMinus, LuPlus, LuRefreshCw } from "react-icons/lu"; import { useHostUrl } from "renderer/hooks/host-service/useHostTargetUrl"; import { useDebouncedValue } from "renderer/hooks/useDebouncedValue"; import { getHostServiceClientByUrl } from "renderer/lib/host-service-client"; @@ -44,6 +44,7 @@ export function GitHubIssuesContent({ fetchNextPage, hasNextPage, error, + refetch, } = useInfiniteQuery({ queryKey: [ "tasks", @@ -166,6 +167,17 @@ export function GitHubIssuesContent({ {countLabel} + {onCollapse && ( {onCollapse && ( - )} -
-
- -
-
- ); -} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksColumn/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksColumn/index.ts deleted file mode 100644 index 4cfe9ab2fcd..00000000000 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksColumn/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { TasksColumn } from "./TasksColumn"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/TasksTopBar.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/TasksTopBar.tsx index 8fb52dab2a8..792dce8edeb 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/TasksTopBar.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/TasksTopBar.tsx @@ -16,7 +16,6 @@ import { useHotkey } from "renderer/hotkeys"; import type { TypeTab, ViewMode } from "../../../../stores/tasks-filter-state"; import type { TaskWithStatus } from "../../hooks/useTasksData"; import { ActiveIcon } from "../shared/icons/ActiveIcon"; -import { AllIssuesIcon } from "../shared/icons/AllIssuesIcon"; import { AssigneeFilter } from "./components/AssigneeFilter"; import { CreateTaskDialog } from "./components/CreateTaskDialog"; import { ProjectFilter } from "./components/ProjectFilter"; @@ -44,7 +43,6 @@ interface TasksTopBarProps { } const TYPE_TABS = [ - { value: "all" as const, label: "All", Icon: AllIssuesIcon }, { value: "tasks" as const, label: "Tasks", Icon: ActiveIcon }, { value: "prs" as const, label: "PRs", Icon: GoGitPullRequest }, { value: "issues" as const, label: "Issues", Icon: GoIssueOpened }, diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/layout.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/layout.tsx index 1320239cb2c..db1f2925728 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/layout.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/layout.tsx @@ -4,7 +4,7 @@ export type TasksSearch = { tab?: "all" | "active" | "backlog"; assignee?: string; search?: string; - type?: "all" | "tasks" | "prs" | "issues"; + type?: "tasks" | "prs" | "issues"; project?: string; }; @@ -16,7 +16,7 @@ export const Route = createFileRoute("/_authenticated/_dashboard/tasks")({ : undefined, assignee: typeof search.assignee === "string" ? search.assignee : undefined, search: typeof search.search === "string" ? search.search : undefined, - type: ["all", "tasks", "prs", "issues"].includes(search.type as string) + type: ["tasks", "prs", "issues"].includes(search.type as string) ? (search.type as TasksSearch["type"]) : undefined, project: typeof search.project === "string" ? search.project : undefined, diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/stores/tasks-filter-state.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/stores/tasks-filter-state.ts index 2fd16f5eb48..6fdfa8c9573 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/stores/tasks-filter-state.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/stores/tasks-filter-state.ts @@ -1,7 +1,7 @@ import { create } from "zustand"; export type ViewMode = "table" | "board"; -export type TypeTab = "all" | "tasks" | "prs" | "issues"; +export type TypeTab = "tasks" | "prs" | "issues"; interface TasksFilterState { tab: "all" | "active" | "backlog"; @@ -23,7 +23,7 @@ export const useTasksFilterStore = create()((set) => ({ assignee: null, search: "", viewMode: "table", - typeTab: "all", + typeTab: "tasks", projectFilter: null, setTab: (tab) => set({ tab }), setAssignee: (assignee) => set({ assignee }), From 68f8c6e3bad8922f8d647d86d9594ece9521ca2d Mon Sep 17 00:00:00 2001 From: AviPeltz Date: Thu, 7 May 2026 22:51:05 -0700 Subject: [PATCH 6/7] fix(desktop): address PR review feedback on tasks integration - SafeImage: drop http://, allow only data: and https:// (cleartext is no longer needed; GitHub user-attachments are https). - ElectronTRPCProvider: compose shouldDehydrateQuery with defaultShouldDehydrateQuery so pending/error queries are excluded from the persisted IndexedDB cache. - search-pull-requests / search-github-issues: direct-lookup branches now echo the requested page instead of hardcoding 1, matching the search branches. - PullRequestsContent / GitHubIssuesContent: short-circuit row keydown when the event target isn't the row itself, so Enter/Space on a nested action button no longer also navigates to the preview. repoMismatch text gets select-text cursor-text so the repo string is copyable (per AGENTS.md). - AssigneeFilter / ProjectFilter / StatusFilter: add aria-label to the icon-only popover triggers (title alone is tooltip-only on many screen readers). - Bump @tanstack/react-query to ^5.100.9 to satisfy @tanstack/react-query-persist-client peer requirement. - Extract shared normalizePRState helper next to the PRState type so the PR list and detail page can't drift. --- apps/desktop/package.json | 2 +- .../components/SafeImage/SafeImage.tsx | 7 ++++--- .../ElectronTRPCProvider.tsx | 6 +++++- .../GitHubIssuesContent.tsx | 3 ++- .../PullRequestsContent.tsx | 19 ++++++------------- .../AssigneeFilter/AssigneeFilter.tsx | 1 + .../ProjectFilter/ProjectFilter.tsx | 1 + .../components/StatusFilter/StatusFilter.tsx | 1 + .../_dashboard/tasks/pr/$prNumber/page.tsx | 15 ++++----------- .../screens/main/components/PRIcon/index.ts | 1 + .../components/PRIcon/normalizePRState.ts | 13 +++++++++++++ bun.lock | 10 +++++++++- .../procedures/search-github-issues.ts | 8 ++++---- .../procedures/search-pull-requests.ts | 4 ++-- 14 files changed, 54 insertions(+), 37 deletions(-) create mode 100644 apps/desktop/src/renderer/screens/main/components/PRIcon/normalizePRState.ts diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 0a96c00992a..8b412a27c1b 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -105,7 +105,7 @@ "@tanstack/node-db-sqlite-persistence": "0.1.9", "@tanstack/query-async-storage-persister": "^5.100.9", "@tanstack/react-db": "0.1.83", - "@tanstack/react-query": "^5.90.19", + "@tanstack/react-query": "^5.100.9", "@tanstack/react-query-persist-client": "^5.100.9", "@tanstack/react-router": "^1.147.3", "@tanstack/react-table": "^8.21.3", diff --git a/apps/desktop/src/renderer/components/MarkdownRenderer/components/SafeImage/SafeImage.tsx b/apps/desktop/src/renderer/components/MarkdownRenderer/components/SafeImage/SafeImage.tsx index fbfcb47738f..28217e59e28 100644 --- a/apps/desktop/src/renderer/components/MarkdownRenderer/components/SafeImage/SafeImage.tsx +++ b/apps/desktop/src/renderer/components/MarkdownRenderer/components/SafeImage/SafeImage.tsx @@ -5,16 +5,17 @@ import { LuImageOff } from "react-icons/lu"; * * ALLOWED: * - data: URLs (embedded base64 images) - * - http(s):// URLs (GitHub user-attachments, avatars, etc.) + * - https:// URLs (GitHub user-attachments, avatars, etc.) * * BLOCKED (everything else): + * - http:// (cleartext / mixed-content) * - file:// URLs (arbitrary local file access) * - Absolute paths /... or \... (become file:// in Electron) * - Relative paths with .. (can escape repo boundary) * - UNC paths //server/share (Windows NTLM credential leak) * - Empty or malformed sources * - * Trade-off: http(s) sources can phone home (tracking pixels). Acceptable + * Trade-off: https sources can phone home (tracking pixels). Acceptable * here because the markdown comes from trusted sources (GitHub PR/issue * bodies, user-authored task descriptions) where image embedding is part * of the expected UX. @@ -26,7 +27,7 @@ function isSafeImageSrc(src: string | undefined): boolean { const lower = trimmed.toLowerCase(); if (lower.startsWith("data:")) return true; - if (lower.startsWith("https://") || lower.startsWith("http://")) return true; + if (lower.startsWith("https://")) return true; return false; } diff --git a/apps/desktop/src/renderer/providers/ElectronTRPCProvider/ElectronTRPCProvider.tsx b/apps/desktop/src/renderer/providers/ElectronTRPCProvider/ElectronTRPCProvider.tsx index 6ca2b3e28ae..a1917de4889 100644 --- a/apps/desktop/src/renderer/providers/ElectronTRPCProvider/ElectronTRPCProvider.tsx +++ b/apps/desktop/src/renderer/providers/ElectronTRPCProvider/ElectronTRPCProvider.tsx @@ -1,5 +1,8 @@ import { createAsyncStoragePersister } from "@tanstack/query-async-storage-persister"; -import { QueryClient } from "@tanstack/react-query"; +import { + defaultShouldDehydrateQuery, + QueryClient, +} from "@tanstack/react-query"; import { PersistQueryClientProvider } from "@tanstack/react-query-persist-client"; import { del, get, set } from "idb-keyval"; import { electronTrpc } from "renderer/lib/electron-trpc"; @@ -64,6 +67,7 @@ export function ElectronTRPCProvider({ buster: PERSIST_BUSTER, dehydrateOptions: { shouldDehydrateQuery: (query) => { + if (!defaultShouldDehydrateQuery(query)) return false; const head = query.queryKey[0]; return typeof head === "string" && PERSIST_KEY_PREFIXES.has(head); }, diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/GitHubIssuesContent/GitHubIssuesContent.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/GitHubIssuesContent/GitHubIssuesContent.tsx index 90f6c56c348..ab01b985342 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/GitHubIssuesContent/GitHubIssuesContent.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/GitHubIssuesContent/GitHubIssuesContent.tsx @@ -215,7 +215,7 @@ export function GitHubIssuesContent({ )} {repoMismatch && ( -
+
Issue URL must match {repoMismatch}.
)} @@ -238,6 +238,7 @@ export function GitHubIssuesContent({ className="group flex items-center gap-3 px-4 h-9 cursor-pointer border-b border-border/50 hover:bg-accent/50" onClick={() => handleOpenPreview(issue.issueNumber)} onKeyDown={(e) => { + if (e.target !== e.currentTarget) return; if (e.key === "Enter" || e.key === " ") { e.preventDefault(); handleOpenPreview(issue.issueNumber); diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/PullRequestsContent/PullRequestsContent.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/PullRequestsContent/PullRequestsContent.tsx index 66304c57c5e..3205d653b45 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/PullRequestsContent/PullRequestsContent.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/PullRequestsContent/PullRequestsContent.tsx @@ -10,9 +10,9 @@ import { useHostUrl } from "renderer/hooks/host-service/useHostTargetUrl"; import { useDebouncedValue } from "renderer/hooks/useDebouncedValue"; import { getHostServiceClientByUrl } from "renderer/lib/host-service-client"; import { + normalizePRState, PRIcon, - type PRState, -} from "renderer/screens/main/components/PRIcon/PRIcon"; +} from "renderer/screens/main/components/PRIcon"; import { type LinkedPR, useNewWorkspaceDraftStore, @@ -27,14 +27,6 @@ interface PullRequestsContentProps { const PAGE_SIZE = 30; -function normalizeState(state: string, isDraft: boolean): PRState { - if (isDraft) return "draft"; - const lower = state.toLowerCase(); - if (lower === "merged") return "merged"; - if (lower === "closed") return "closed"; - return "open"; -} - export function PullRequestsContent({ projectFilter, searchQuery, @@ -126,7 +118,7 @@ export function PullRequestsContent({ prNumber: pr.prNumber, title: pr.title, url: pr.url, - state: normalizeState(pr.state, pr.isDraft), + state: normalizePRState(pr.state, pr.isDraft), }; resetDraft(); updateDraft({ selectedProjectId: projectFilter, linkedPR }); @@ -224,7 +216,7 @@ export function PullRequestsContent({ )} {repoMismatch && ( -
+
PR URL must match {repoMismatch}.
)} @@ -240,7 +232,7 @@ export function PullRequestsContent({ ) : (
{pullRequests.map((pr) => { - const state = normalizeState(pr.state, pr.isDraft); + const state = normalizePRState(pr.state, pr.isDraft); return ( // biome-ignore lint/a11y/useSemanticElements: row contains nested action buttons, so the outer element is a div with role/tabIndex
handleOpenPreview(pr.prNumber)} onKeyDown={(e) => { + if (e.target !== e.currentTarget) return; if (e.key === "Enter" || e.key === " ") { e.preventDefault(); handleOpenPreview(pr.prNumber); diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/components/AssigneeFilter/AssigneeFilter.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/components/AssigneeFilter/AssigneeFilter.tsx index ed37cbcde65..aaa7b05eb35 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/components/AssigneeFilter/AssigneeFilter.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/components/AssigneeFilter/AssigneeFilter.tsx @@ -131,6 +131,7 @@ export function AssigneeFilter({ value, onChange }: AssigneeFilterProps) { variant="ghost" size="sm" title={selectedUser?.name ?? "Assignee"} + aria-label={selectedUser?.name ?? "Assignee"} className="h-8 gap-1.5 px-2 text-muted-foreground hover:text-foreground" > {selectedUser ? ( diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/components/ProjectFilter/ProjectFilter.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/components/ProjectFilter/ProjectFilter.tsx index 64a234136c8..d921456ffa0 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/components/ProjectFilter/ProjectFilter.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/components/ProjectFilter/ProjectFilter.tsx @@ -61,6 +61,7 @@ export function ProjectFilter({ value, onChange }: ProjectFilterProps) { variant="ghost" size="sm" title={selected ? selected.name : "Project"} + aria-label={selected ? selected.name : "Project"} className="h-8 gap-1.5 px-2 text-muted-foreground hover:text-foreground" > {selected ? ( diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/components/StatusFilter/StatusFilter.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/components/StatusFilter/StatusFilter.tsx index 0c1560fabd9..4d27561ca29 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/components/StatusFilter/StatusFilter.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/components/StatusFilter/StatusFilter.tsx @@ -46,6 +46,7 @@ export function StatusFilter({ value, onChange }: StatusFilterProps) { variant="ghost" size="sm" title={selected.label} + aria-label={selected.label} className="h-8 gap-1.5 px-2 text-muted-foreground hover:text-foreground" > diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/pr/$prNumber/page.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/pr/$prNumber/page.tsx index 62abfeb6ce4..832bc5c5a2c 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/pr/$prNumber/page.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/pr/$prNumber/page.tsx @@ -9,9 +9,10 @@ import { MarkdownRenderer } from "renderer/components/MarkdownRenderer"; import { useHostUrl } from "renderer/hooks/host-service/useHostTargetUrl"; import { getHostServiceClientByUrl } from "renderer/lib/host-service-client"; import { + normalizePRState, PRIcon, type PRState, -} from "renderer/screens/main/components/PRIcon/PRIcon"; +} from "renderer/screens/main/components/PRIcon"; import { type LinkedPR, useNewWorkspaceDraftStore, @@ -25,14 +26,6 @@ export const Route = createFileRoute( component: PullRequestDetailPage, }); -function resolveState(state: string, isDraft: boolean): PRState { - if (isDraft) return "draft"; - const lower = state.toLowerCase(); - if (lower === "merged") return "merged"; - if (lower === "closed") return "closed"; - return "open"; -} - function PullRequestDetailPage() { const { prNumber: prNumberRaw } = Route.useParams(); const prNumber = Number.parseInt(prNumberRaw, 10); @@ -80,7 +73,7 @@ function PullRequestDetailPage() { prNumber: data.number, title: data.title, url: data.url, - state: resolveState(data.state, data.isDraft), + state: normalizePRState(data.state, data.isDraft), }; resetDraft(); updateDraft({ selectedProjectId: projectId, linkedPR }); @@ -120,7 +113,7 @@ function PullRequestDetailPage() { ); } - const state = resolveState(data.state, data.isDraft); + const state = normalizePRState(data.state, data.isDraft); const stateLabel = data.isDraft ? "Draft" : data.state; const branchSummary = data.branch ? `${data.headRepositoryOwner && data.isCrossRepository ? `${data.headRepositoryOwner}:${data.branch}` : data.branch} → ${data.baseBranch}` diff --git a/apps/desktop/src/renderer/screens/main/components/PRIcon/index.ts b/apps/desktop/src/renderer/screens/main/components/PRIcon/index.ts index 3c6b8a06935..2266d83df24 100644 --- a/apps/desktop/src/renderer/screens/main/components/PRIcon/index.ts +++ b/apps/desktop/src/renderer/screens/main/components/PRIcon/index.ts @@ -1 +1,2 @@ +export { normalizePRState } from "./normalizePRState"; export { PRIcon, type PRState } from "./PRIcon"; diff --git a/apps/desktop/src/renderer/screens/main/components/PRIcon/normalizePRState.ts b/apps/desktop/src/renderer/screens/main/components/PRIcon/normalizePRState.ts new file mode 100644 index 00000000000..d7b780f6a41 --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/PRIcon/normalizePRState.ts @@ -0,0 +1,13 @@ +import type { PRState } from "./PRIcon"; + +/** + * Map a raw GitHub PR state string + draft flag to the canonical PRState + * used by PRIcon and the rest of the renderer. Draft trumps merged/closed/open. + */ +export function normalizePRState(state: string, isDraft: boolean): PRState { + if (isDraft) return "draft"; + const lower = state.toLowerCase(); + if (lower === "merged") return "merged"; + if (lower === "closed") return "closed"; + return "open"; +} diff --git a/bun.lock b/bun.lock index 913fe85eff6..2e4f435f39d 100644 --- a/bun.lock +++ b/bun.lock @@ -182,7 +182,7 @@ "@tanstack/node-db-sqlite-persistence": "0.1.9", "@tanstack/query-async-storage-persister": "^5.100.9", "@tanstack/react-db": "0.1.83", - "@tanstack/react-query": "^5.90.19", + "@tanstack/react-query": "^5.100.9", "@tanstack/react-query-persist-client": "^5.100.9", "@tanstack/react-router": "^1.147.3", "@tanstack/react-table": "^8.21.3", @@ -6641,6 +6641,10 @@ "@slack/web-api/eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="], + "@superset/desktop/@tanstack/react-query": ["@tanstack/react-query@5.100.9", "", { "dependencies": { "@tanstack/query-core": "5.100.9" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-Oa44XkaI3kCNN6ME0KByU3xT3SEUNOMfZpHxL6+wFoTm+OeUFYHKdeYVe0aOXlRDm/f15sgLwEt2HDorIdW8+A=="], + + "@superset/workspace-client/@tanstack/react-query": ["@tanstack/react-query@5.100.9", "", { "dependencies": { "@tanstack/query-core": "5.100.9" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-Oa44XkaI3kCNN6ME0KByU3xT3SEUNOMfZpHxL6+wFoTm+OeUFYHKdeYVe0aOXlRDm/f15sgLwEt2HDorIdW8+A=="], + "@tailwindcss/node/jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], "@tailwindcss/node/lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], @@ -7739,6 +7743,10 @@ "@sentry/vite-plugin/@sentry/bundler-plugin-core/magic-string": ["magic-string@0.30.8", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" } }, "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ=="], + "@superset/desktop/@tanstack/react-query/@tanstack/query-core": ["@tanstack/query-core@5.100.9", "", {}, "sha512-SJSFw1S8+kQ0+knv/XGfrbocWoAlT7vDKsSImtLx3ZPQmEcR46hkDjLSvynSy25N8Ms4tIEini1FuBd5k7IscQ=="], + + "@superset/workspace-client/@tanstack/react-query/@tanstack/query-core": ["@tanstack/query-core@5.100.9", "", {}, "sha512-SJSFw1S8+kQ0+knv/XGfrbocWoAlT7vDKsSImtLx3ZPQmEcR46hkDjLSvynSy25N8Ms4tIEini1FuBd5k7IscQ=="], + "@tailwindcss/node/lightningcss/lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], "@tailwindcss/node/lightningcss/lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="], diff --git a/packages/host-service/src/trpc/router/workspace-creation/procedures/search-github-issues.ts b/packages/host-service/src/trpc/router/workspace-creation/procedures/search-github-issues.ts index 4f2ea51cde6..0831d9a66f3 100644 --- a/packages/host-service/src/trpc/router/workspace-creation/procedures/search-github-issues.ts +++ b/packages/host-service/src/trpc/router/workspace-creation/procedures/search-github-issues.ts @@ -156,14 +156,14 @@ export const searchGitHubIssues = protectedProcedure issues: [], totalCount: 0, hasNextPage: false, - page: 1, + page, }; } return { issues: [issue], totalCount: 1, hasNextPage: false, - page: 1, + page, }; } const result = await ghApiSearchIssues( @@ -202,7 +202,7 @@ export const searchGitHubIssues = protectedProcedure issues: [], totalCount: 0, hasNextPage: false, - page: 1, + page, }; } return { @@ -217,7 +217,7 @@ export const searchGitHubIssues = protectedProcedure ], totalCount: 1, hasNextPage: false, - page: 1, + page, }; } diff --git a/packages/host-service/src/trpc/router/workspace-creation/procedures/search-pull-requests.ts b/packages/host-service/src/trpc/router/workspace-creation/procedures/search-pull-requests.ts index bb9ad4bf5c5..a6c537decd1 100644 --- a/packages/host-service/src/trpc/router/workspace-creation/procedures/search-pull-requests.ts +++ b/packages/host-service/src/trpc/router/workspace-creation/procedures/search-pull-requests.ts @@ -174,7 +174,7 @@ export const searchPullRequests = protectedProcedure pullRequests: [pr], totalCount: 1, hasNextPage: false, - page: 1, + page, }; } const result = await ghApiSearchPullRequests( @@ -222,7 +222,7 @@ export const searchPullRequests = protectedProcedure ], totalCount: 1, hasNextPage: false, - page: 1, + page, }; } From cd3cdf8bdfdaffa1b2cb2c2d3dfbe0b5f64dceb9 Mon Sep 17 00:00:00 2001 From: AviPeltz Date: Thu, 7 May 2026 23:03:28 -0700 Subject: [PATCH 7/7] chore: align @tanstack/react-query to ^5.100.9 across workspace Previous commit bumped only apps/desktop, leaving admin/mobile/web/ workspace-client on ^5.90.19. Bun resolved both major-incompatible ranges, duplicating @tanstack/query-core (5.100.9 + 5.95.2). The two QueryClient types are nominal, so passing electronQueryClient (now 5.100.9) into ChatPane and other call sites typed against the older copy failed turbo typecheck. Sherif also flagged the version split. Aligning everything to ^5.100.9 collapses the duplicate and matches the @tanstack/react-query-persist-client peer requirement. --- apps/admin/package.json | 2 +- apps/mobile/package.json | 2 +- apps/web/package.json | 2 +- bun.lock | 24 ++++++------------------ packages/workspace-client/package.json | 2 +- 5 files changed, 10 insertions(+), 22 deletions(-) diff --git a/apps/admin/package.json b/apps/admin/package.json index bdb7fe1bf36..425f256b0f2 100644 --- a/apps/admin/package.json +++ b/apps/admin/package.json @@ -18,7 +18,7 @@ "@superset/trpc": "workspace:*", "@superset/ui": "workspace:*", "@t3-oss/env-nextjs": "^0.13.8", - "@tanstack/react-query": "^5.90.19", + "@tanstack/react-query": "^5.100.9", "@tanstack/react-query-devtools": "^5.90.10", "@trpc/client": "^11.7.1", "@trpc/server": "^11.7.1", diff --git a/apps/mobile/package.json b/apps/mobile/package.json index c0b7292345a..ab01977e564 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -48,7 +48,7 @@ "@tanstack/db": "0.6.5", "@tanstack/electric-db-collection": "0.3.3", "@tanstack/react-db": "0.1.83", - "@tanstack/react-query": "^5.90.19", + "@tanstack/react-query": "^5.100.9", "@trpc/client": "^11.7.1", "@trpc/react-query": "^11.7.1", "better-auth": "1.6.5", diff --git a/apps/web/package.json b/apps/web/package.json index cc98c941918..2b9c7159b12 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -18,7 +18,7 @@ "@superset/trpc": "workspace:*", "@superset/ui": "workspace:*", "@t3-oss/env-nextjs": "^0.13.8", - "@tanstack/react-query": "^5.90.19", + "@tanstack/react-query": "^5.100.9", "@tanstack/react-query-devtools": "^5.90.10", "@trpc/client": "^11.7.1", "@trpc/server": "^11.7.1", diff --git a/bun.lock b/bun.lock index 2e4f435f39d..8f53de2a0da 100644 --- a/bun.lock +++ b/bun.lock @@ -22,7 +22,7 @@ "@superset/trpc": "workspace:*", "@superset/ui": "workspace:*", "@t3-oss/env-nextjs": "^0.13.8", - "@tanstack/react-query": "^5.90.19", + "@tanstack/react-query": "^5.100.9", "@tanstack/react-query-devtools": "^5.90.10", "@trpc/client": "^11.7.1", "@trpc/server": "^11.7.1", @@ -492,7 +492,7 @@ "@tanstack/db": "0.6.5", "@tanstack/electric-db-collection": "0.3.3", "@tanstack/react-db": "0.1.83", - "@tanstack/react-query": "^5.90.19", + "@tanstack/react-query": "^5.100.9", "@trpc/client": "^11.7.1", "@trpc/react-query": "^11.7.1", "better-auth": "1.6.5", @@ -582,7 +582,7 @@ "@superset/trpc": "workspace:*", "@superset/ui": "workspace:*", "@t3-oss/env-nextjs": "^0.13.8", - "@tanstack/react-query": "^5.90.19", + "@tanstack/react-query": "^5.100.9", "@tanstack/react-query-devtools": "^5.90.10", "@trpc/client": "^11.7.1", "@trpc/server": "^11.7.1", @@ -1064,7 +1064,7 @@ "dependencies": { "@superset/host-service": "workspace:*", "@superset/workspace-fs": "workspace:*", - "@tanstack/react-query": "^5.90.19", + "@tanstack/react-query": "^5.100.9", "@trpc/client": "^11.7.1", "@trpc/react-query": "^11.7.1", "superjson": "^2.2.5", @@ -2737,7 +2737,7 @@ "@tanstack/query-async-storage-persister": ["@tanstack/query-async-storage-persister@5.100.9", "", { "dependencies": { "@tanstack/query-core": "5.100.9", "@tanstack/query-persist-client-core": "5.100.9" } }, "sha512-KTWqpXIAwuR1bSIxfOnoRr7hOMxfrtLk9kxSuDfOu4sSBEFqHp9+aDp8Ig6YdHxCclNI86SizPnmKmSqNxcJxg=="], - "@tanstack/query-core": ["@tanstack/query-core@5.95.2", "", {}, "sha512-o4T8vZHZET4Bib3jZ/tCW9/7080urD4c+0/AUaYVpIqOsr7y0reBc1oX3ttNaSW5mYyvZHctiQ/UOP2PfdmFEQ=="], + "@tanstack/query-core": ["@tanstack/query-core@5.100.9", "", {}, "sha512-SJSFw1S8+kQ0+knv/XGfrbocWoAlT7vDKsSImtLx3ZPQmEcR46hkDjLSvynSy25N8Ms4tIEini1FuBd5k7IscQ=="], "@tanstack/query-devtools": ["@tanstack/query-devtools@5.95.2", "", {}, "sha512-QfaoqBn9uAZ+ICkA8brd1EHj+qBF6glCFgt94U8XP5BT6ppSsDBI8IJ00BU+cAGjQzp6wcKJL2EmRYvxy0TWIg=="], @@ -2745,7 +2745,7 @@ "@tanstack/react-db": ["@tanstack/react-db@0.1.83", "", { "dependencies": { "@tanstack/db": "0.6.5", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-LNV0C7OARazooT2hLTr5anXo6tbEyX2rHZQ0j9HZ/iNBI+Tx/y19o5Nd3ooyAYz5LEHJJxb8iM8ZTVB/diGnXw=="], - "@tanstack/react-query": ["@tanstack/react-query@5.95.2", "", { "dependencies": { "@tanstack/query-core": "5.95.2" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-/wGkvLj/st5Ud1Q76KF1uFxScV7WeqN1slQx5280ycwAyYkIPGaRZAEgHxe3bjirSd5Zpwkj6zNcR4cqYni/ZA=="], + "@tanstack/react-query": ["@tanstack/react-query@5.100.9", "", { "dependencies": { "@tanstack/query-core": "5.100.9" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-Oa44XkaI3kCNN6ME0KByU3xT3SEUNOMfZpHxL6+wFoTm+OeUFYHKdeYVe0aOXlRDm/f15sgLwEt2HDorIdW8+A=="], "@tanstack/react-query-devtools": ["@tanstack/react-query-devtools@5.95.2", "", { "dependencies": { "@tanstack/query-devtools": "5.95.2" }, "peerDependencies": { "@tanstack/react-query": "^5.95.2", "react": "^18 || ^19" } }, "sha512-AFQFmbznVkbtfpx8VJ2DylW17wWagQel/qLstVLkYmNRo2CmJt3SNej5hvl6EnEeljJIdC3BTB+W7HZtpsH+3g=="], @@ -6641,10 +6641,6 @@ "@slack/web-api/eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="], - "@superset/desktop/@tanstack/react-query": ["@tanstack/react-query@5.100.9", "", { "dependencies": { "@tanstack/query-core": "5.100.9" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-Oa44XkaI3kCNN6ME0KByU3xT3SEUNOMfZpHxL6+wFoTm+OeUFYHKdeYVe0aOXlRDm/f15sgLwEt2HDorIdW8+A=="], - - "@superset/workspace-client/@tanstack/react-query": ["@tanstack/react-query@5.100.9", "", { "dependencies": { "@tanstack/query-core": "5.100.9" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-Oa44XkaI3kCNN6ME0KByU3xT3SEUNOMfZpHxL6+wFoTm+OeUFYHKdeYVe0aOXlRDm/f15sgLwEt2HDorIdW8+A=="], - "@tailwindcss/node/jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], "@tailwindcss/node/lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], @@ -6661,10 +6657,6 @@ "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "@tanstack/query-async-storage-persister/@tanstack/query-core": ["@tanstack/query-core@5.100.9", "", {}, "sha512-SJSFw1S8+kQ0+knv/XGfrbocWoAlT7vDKsSImtLx3ZPQmEcR46hkDjLSvynSy25N8Ms4tIEini1FuBd5k7IscQ=="], - - "@tanstack/query-persist-client-core/@tanstack/query-core": ["@tanstack/query-core@5.100.9", "", {}, "sha512-SJSFw1S8+kQ0+knv/XGfrbocWoAlT7vDKsSImtLx3ZPQmEcR46hkDjLSvynSy25N8Ms4tIEini1FuBd5k7IscQ=="], - "@tanstack/router-generator/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], "@tanstack/router-plugin/unplugin": ["unplugin@2.3.11", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww=="], @@ -7743,10 +7735,6 @@ "@sentry/vite-plugin/@sentry/bundler-plugin-core/magic-string": ["magic-string@0.30.8", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" } }, "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ=="], - "@superset/desktop/@tanstack/react-query/@tanstack/query-core": ["@tanstack/query-core@5.100.9", "", {}, "sha512-SJSFw1S8+kQ0+knv/XGfrbocWoAlT7vDKsSImtLx3ZPQmEcR46hkDjLSvynSy25N8Ms4tIEini1FuBd5k7IscQ=="], - - "@superset/workspace-client/@tanstack/react-query/@tanstack/query-core": ["@tanstack/query-core@5.100.9", "", {}, "sha512-SJSFw1S8+kQ0+knv/XGfrbocWoAlT7vDKsSImtLx3ZPQmEcR46hkDjLSvynSy25N8Ms4tIEini1FuBd5k7IscQ=="], - "@tailwindcss/node/lightningcss/lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], "@tailwindcss/node/lightningcss/lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="], diff --git a/packages/workspace-client/package.json b/packages/workspace-client/package.json index c028a4e3fe9..46331bb6734 100644 --- a/packages/workspace-client/package.json +++ b/packages/workspace-client/package.json @@ -16,7 +16,7 @@ "dependencies": { "@superset/host-service": "workspace:*", "@superset/workspace-fs": "workspace:*", - "@tanstack/react-query": "^5.90.19", + "@tanstack/react-query": "^5.100.9", "@trpc/client": "^11.7.1", "@trpc/react-query": "^11.7.1", "superjson": "^2.2.5"