Skip to content

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

Merged
farion1231 merged 1 commit into
farion1231:mainfrom
TuYv:fix/skills-import-dedup
Apr 23, 2026
Merged

fix(skills): prevent duplicate imports when import button is double-clicked#2211
farion1231 merged 1 commit into
farion1231:mainfrom
TuYv:fix/skills-import-dedup

Conversation

@TuYv

@TuYv TuYv commented Apr 21, 2026

Copy link
Copy Markdown
Contributor

Summary / 概述

When a user taps the "Import" confirm button multiple times in the Skills import dialog — either deliberately or because the UI lags while importing many skills — the installed-skills count balloons to a multiple of the real count.

Two related defects combined to produce this:

  1. The confirm button stayed clickable during a pending mutation. It only checked selected.size === 0, so every extra tap fired another importFromApps mutation.
  2. useImportSkillsFromApps appended results to the installed cache without deduping. Re-firing the mutation stacked the same skills into the list.

Changes

  • ImportSkillsDialog gains an isImporting prop; the Import and Cancel buttons are disabled while the mutation is pending. This follows the isRestoring / isDeleting pattern already used by RestoreSkillsDialog in the same file.
  • Cache-merge logic is extracted as a pure function mergeImportedSkills(existing, imported) in a new src/hooks/useSkills.helpers.ts so it can be unit-tested without React / React Query setup. It dedupes by InstalledSkill.id (newer entries replace stale ones) and short-circuits to the original reference when imported is empty so React Query does not notify subscribers about a no-op update.
  • Unit tests cover empty cache, re-import of the same id, stale replacement, empty-payload reference stability, and unrelated appends.

Scope

Only useImportSkillsFromApps is touched here. useInstallSkill and useInstallSkillsFromZip also perform naive cache appends but are not reachable from a dialog double-click in the same way, and changing them is out of scope for this fix — happy to open a follow-up if desired.

Related Issue / 关联 Issue

Fixes #2139

Screenshots / 截图

N/A — the behaviour change is functional. Reproduction steps and root cause are documented in the linked issue; after the fix the confirm button goes disabled on first click and the installed count matches the imported count regardless of how many times the button is tapped.

Checklist / 检查清单

  • `pnpm typecheck` passes
  • `pnpm format:check` passes for the files touched in this PR (three pre-existing warnings in `src/components/providers/forms/hooks/*` are unchanged and not in scope)
  • `cargo clippy` — N/A, no Rust changes
  • i18n — no new user-facing strings

…licked

Closes farion1231#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.

@farion1231 farion1231 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for your contribution!

@farion1231
farion1231 merged commit 29e8748 into farion1231:main Apr 23, 2026
ManLOK-Chu pushed a commit to ManLOK-Chu/cc-switch that referenced this pull request May 11, 2026
…licked (farion1231#2211)

Closes farion1231#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.
dfbb pushed a commit to dfbb/cc-switch that referenced this pull request May 20, 2026
…licked (farion1231#2211)

Closes farion1231#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.
delta-lo pushed a commit to delta-lo/cc-switch-mod that referenced this pull request May 31, 2026
…licked (farion1231#2211)

Closes farion1231#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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: Skills Import Count Multiplied When Button Clicked Multiple Times

2 participants