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
+8 -2
View File
@@ -454,6 +454,7 @@ const UnifiedSkillsPanel = React.forwardRef<
{importDialogOpen && unmanagedSkills && (
<ImportSkillsDialog
skills={unmanagedSkills}
isImporting={importMutation.isPending}
onImport={handleImport}
onClose={() => setImportDialogOpen(false)}
/>
@@ -600,6 +601,7 @@ interface ImportSkillsDialogProps {
foundIn: string[];
path: string;
}>;
isImporting: boolean;
onImport: (imports: ImportSkillSelection[]) => void;
onClose: () => void;
}
@@ -724,6 +726,7 @@ const RestoreSkillsDialog: React.FC<RestoreSkillsDialogProps> = ({
const ImportSkillsDialog: React.FC<ImportSkillsDialogProps> = ({
skills,
isImporting,
onImport,
onClose,
}) => {
@@ -846,10 +849,13 @@ const ImportSkillsDialog: React.FC<ImportSkillsDialogProps> = ({
</div>
<div className="flex justify-end gap-3">
<Button variant="outline" onClick={onClose}>
<Button variant="outline" onClick={onClose} disabled={isImporting}>
{t("common.cancel")}
</Button>
<Button onClick={handleImport} disabled={selected.size === 0}>
<Button
onClick={handleImport}
disabled={selected.size === 0 || isImporting}
>
{t("skills.importSelected", { count: selected.size })}
</Button>
</div>