fix(skills): prevent duplicate imports when import button is double-clicked (#2211)

Closes #2139

Two related defects let the installed-skills count balloon when users
tap the import confirm button multiple times — either deliberately or
because the button is still clickable while a slow import is in flight:

- The confirm button only disabled itself while `selected.size === 0`,
  so it stayed clickable during a pending mutation. Each extra click
  triggered another `importFromApps` mutation.
- `useImportSkillsFromApps` appended the server response to the
  installed cache without deduping, so re-firing the mutation stacked
  the same skills into the list again.

Disable the confirm (and cancel) buttons while the mutation is pending
— matching the `isRestoring` / `isDeleting` pattern already used by
`RestoreSkillsDialog` — and merge success payloads by
`InstalledSkill.id` so repeated results overwrite rather than
accumulate.

The merge is extracted as a pure `mergeImportedSkills` reducer to make
the behaviour unit-testable and to short-circuit on an empty payload,
returning the existing reference so React Query does not notify
subscribers about a no-op cache update.
This commit is contained in:
涂瑜
2026-04-23 10:25:01 +08:00
committed by GitHub
parent 7bfe2609db
commit 29e87487a1
4 changed files with 90 additions and 6 deletions
+2 -4
View File
@@ -14,6 +14,7 @@ import {
type SkillsShSearchResult,
} from "@/lib/api/skills";
import type { AppId } from "@/lib/api/types";
import { mergeImportedSkills } from "@/hooks/useSkills.helpers";
/**
* 查询所有已安装的 Skills
@@ -208,10 +209,7 @@ export function useImportSkillsFromApps() {
// 直接更新 installed 缓存
queryClient.setQueryData<InstalledSkill[]>(
["skills", "installed"],
(oldData) => {
if (!oldData) return importedSkills;
return [...oldData, ...importedSkills];
},
(oldData) => mergeImportedSkills(oldData, importedSkills),
);
// 刷新 unmanaged 列表(已被导入的应该移除)
queryClient.invalidateQueries({ queryKey: ["skills", "unmanaged"] });