| author | |
| committer | |
| log | 1b98213d6d93df663b91774a5f18e94a9a1906af |
| tree | 80c262933eb2b0df1a964aaac5ea80ffcc4f6f05 |
| parent | 0d67424f6945fb451e9b7639861cdac8b18a0c3b |
- add `gitMutationKeys` and attach mutation keys across git mutation options
- switch GitActionsControl loading state to `useIsMutating` with cwd-scoped keys
- add tests for cwd-scoped mutation keys and mutation option wiring3 files changed, 56 insertions(+), 2 deletions(-)
apps/web/src/components/GitActionsControl.tsx+6-2| ... | ... | @@ -1,5 +1,5 @@ |
| 1 | 1 | import { type GitStatusResult, type GitStackedAction, type ThreadId } from "@t3tools/contracts"; |
| 2 | import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; | |
| 2 | import { useIsMutating, useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; | |
| 3 | 3 | import { useCallback, useEffect, useMemo, useState } from "react"; |
| 4 | 4 | import { ChevronDownIcon, CloudUploadIcon, GitCommitIcon, InfoIcon } from "lucide-react"; |
| 5 | 5 | import { GitHubIcon } from "./Icons"; |
| ... | ... | @@ -33,6 +33,7 @@ import { toastManager } from "~/components/ui/toast"; |
| 33 | 33 | import { |
| 34 | 34 | gitBranchesQueryOptions, |
| 35 | 35 | gitInitMutationOptions, |
| 36 | gitMutationKeys, | |
| 36 | 37 | gitPullMutationOptions, |
| 37 | 38 | gitRunStackedActionMutationOptions, |
| 38 | 39 | gitStatusQueryOptions, |
| ... | ... | @@ -168,7 +169,10 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions |
| 168 | 169 | ); |
| 169 | 170 | const pullMutation = useMutation(gitPullMutationOptions({ cwd: gitCwd, queryClient })); |
| 170 | 171 | |
| 171 | const isGitActionRunning = runImmediateGitActionMutation.isPending || pullMutation.isPending; | |
| 172 | const isRunStackedActionRunning = | |
| 173 | useIsMutating({ mutationKey: gitMutationKeys.runStackedAction(gitCwd) }) > 0; | |
| 174 | const isPullRunning = useIsMutating({ mutationKey: gitMutationKeys.pull(gitCwd) }) > 0; | |
| 175 | const isGitActionRunning = isRunStackedActionRunning || isPullRunning; | |
| 172 | 176 | const isDefaultBranch = useMemo(() => { |
| 173 | 177 | const branchName = gitStatusForActions?.branch; |
| 174 | 178 | if (!branchName) return false; |
apps/web/src/lib/gitReactQuery.test.ts created+33| ... | ... | @@ -0,0 +1,33 @@ |
| 1 | import { QueryClient } from "@tanstack/react-query"; | |
| 2 | import { describe, expect, it } from "vitest"; | |
| 3 | import { | |
| 4 | gitMutationKeys, | |
| 5 | gitPullMutationOptions, | |
| 6 | gitRunStackedActionMutationOptions, | |
| 7 | } from "./gitReactQuery"; | |
| 8 | ||
| 9 | describe("gitMutationKeys", () => { | |
| 10 | it("scopes stacked action keys by cwd", () => { | |
| 11 | expect(gitMutationKeys.runStackedAction("/repo/a")).not.toEqual( | |
| 12 | gitMutationKeys.runStackedAction("/repo/b"), | |
| 13 | ); | |
| 14 | }); | |
| 15 | ||
| 16 | it("scopes pull keys by cwd", () => { | |
| 17 | expect(gitMutationKeys.pull("/repo/a")).not.toEqual(gitMutationKeys.pull("/repo/b")); | |
| 18 | }); | |
| 19 | }); | |
| 20 | ||
| 21 | describe("git mutation options", () => { | |
| 22 | const queryClient = new QueryClient(); | |
| 23 | ||
| 24 | it("attaches cwd-scoped mutation key for runStackedAction", () => { | |
| 25 | const options = gitRunStackedActionMutationOptions({ cwd: "/repo/a", queryClient }); | |
| 26 | expect(options.mutationKey).toEqual(gitMutationKeys.runStackedAction("/repo/a")); | |
| 27 | }); | |
| 28 | ||
| 29 | it("attaches cwd-scoped mutation key for pull", () => { | |
| 30 | const options = gitPullMutationOptions({ cwd: "/repo/a", queryClient }); | |
| 31 | expect(options.mutationKey).toEqual(gitMutationKeys.pull("/repo/a")); | |
| 32 | }); | |
| 33 | }); |
apps/web/src/lib/gitReactQuery.ts+17| ... | ... | @@ -13,6 +13,16 @@ export const gitQueryKeys = { |
| 13 | 13 | branches: (cwd: string | null) => ["git", "branches", cwd] as const, |
| 14 | 14 | }; |
| 15 | 15 | |
| 16 | export const gitMutationKeys = { | |
| 17 | init: (cwd: string | null) => ["git", "mutation", "init", cwd] as const, | |
| 18 | checkout: (cwd: string | null) => ["git", "mutation", "checkout", cwd] as const, | |
| 19 | createBranchAndCheckout: (cwd: string | null) => | |
| 20 | ["git", "mutation", "create-branch-and-checkout", cwd] as const, | |
| 21 | runStackedAction: (cwd: string | null) => | |
| 22 | ["git", "mutation", "run-stacked-action", cwd] as const, | |
| 23 | pull: (cwd: string | null) => ["git", "mutation", "pull", cwd] as const, | |
| 24 | }; | |
| 25 | ||
| 16 | 26 | export function invalidateGitQueries(queryClient: QueryClient) { |
| 17 | 27 | return queryClient.invalidateQueries({ queryKey: gitQueryKeys.all }); |
| 18 | 28 | } |
| ... | ... | @@ -51,6 +61,7 @@ export function gitBranchesQueryOptions(cwd: string | null) { |
| 51 | 61 | |
| 52 | 62 | export function gitInitMutationOptions(input: { cwd: string | null; queryClient: QueryClient }) { |
| 53 | 63 | return mutationOptions({ |
| 64 | mutationKey: gitMutationKeys.init(input.cwd), | |
| 54 | 65 | mutationFn: async () => { |
| 55 | 66 | const api = ensureNativeApi(); |
| 56 | 67 | if (!input.cwd) throw new Error("Git init is unavailable."); |
| ... | ... | @@ -67,6 +78,7 @@ export function gitCheckoutMutationOptions(input: { |
| 67 | 78 | queryClient: QueryClient; |
| 68 | 79 | }) { |
| 69 | 80 | return mutationOptions({ |
| 81 | mutationKey: gitMutationKeys.checkout(input.cwd), | |
| 70 | 82 | mutationFn: async (branch: string) => { |
| 71 | 83 | const api = ensureNativeApi(); |
| 72 | 84 | if (!input.cwd) throw new Error("Git checkout is unavailable."); |
| ... | ... | @@ -83,6 +95,7 @@ export function gitCreateBranchAndCheckoutMutationOptions(input: { |
| 83 | 95 | queryClient: QueryClient; |
| 84 | 96 | }) { |
| 85 | 97 | return mutationOptions({ |
| 98 | mutationKey: gitMutationKeys.createBranchAndCheckout(input.cwd), | |
| 86 | 99 | mutationFn: async (branch: string) => { |
| 87 | 100 | const api = ensureNativeApi(); |
| 88 | 101 | if (!input.cwd) throw new Error("Git branch creation is unavailable."); |
| ... | ... | @@ -100,6 +113,7 @@ export function gitRunStackedActionMutationOptions(input: { |
| 100 | 113 | queryClient: QueryClient; |
| 101 | 114 | }) { |
| 102 | 115 | return mutationOptions({ |
| 116 | mutationKey: gitMutationKeys.runStackedAction(input.cwd), | |
| 103 | 117 | mutationFn: async ({ |
| 104 | 118 | action, |
| 105 | 119 | commitMessage, |
| ... | ... | @@ -123,6 +137,7 @@ export function gitRunStackedActionMutationOptions(input: { |
| 123 | 137 | |
| 124 | 138 | export function gitPullMutationOptions(input: { cwd: string | null; queryClient: QueryClient }) { |
| 125 | 139 | return mutationOptions({ |
| 140 | mutationKey: gitMutationKeys.pull(input.cwd), | |
| 126 | 141 | mutationFn: async () => { |
| 127 | 142 | const api = ensureNativeApi(); |
| 128 | 143 | if (!input.cwd) throw new Error("Git pull is unavailable."); |
| ... | ... | @@ -151,6 +166,7 @@ export function gitCreateWorktreeMutationOptions(input: { queryClient: QueryClie |
| 151 | 166 | if (!cwd) throw new Error("Git worktree creation is unavailable."); |
| 152 | 167 | return api.git.createWorktree({ cwd, branch, newBranch, path: path ?? null }); |
| 153 | 168 | }, |
| 169 | mutationKey: ["git", "mutation", "create-worktree"] as const, | |
| 154 | 170 | onSettled: async () => { |
| 155 | 171 | await invalidateGitQueries(input.queryClient); |
| 156 | 172 | }, |
| ... | ... | @@ -164,6 +180,7 @@ export function gitRemoveWorktreeMutationOptions(input: { queryClient: QueryClie |
| 164 | 180 | if (!cwd) throw new Error("Git worktree removal is unavailable."); |
| 165 | 181 | return api.git.removeWorktree({ cwd, path, force }); |
| 166 | 182 | }, |
| 183 | mutationKey: ["git", "mutation", "remove-worktree"] as const, | |
| 167 | 184 | onSettled: async () => { |
| 168 | 185 | await invalidateGitQueries(input.queryClient); |
| 169 | 186 | }, |