From af459c1a7284ad69f85c84ac48cf79cd82e2832c Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Fri, 26 Jun 2026 16:38:59 +0800 Subject: [PATCH] fix(web): place each adapter in a single category bucket MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit groupByCategory pushed multi-category adapters (lark, wecom, discord, slack) into every matching bucket, so the adapter Select rendered duplicate SelectItem values — triggering React duplicate-key warnings and corrupting Radix item tracking. Assign each item to its highest -priority matching category only. Also de-dupes the wizard card grid. Co-Authored-By: Claude Opus 4.8 --- .../app/infra/entities/adapter-categories.ts | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/web/src/app/infra/entities/adapter-categories.ts b/web/src/app/infra/entities/adapter-categories.ts index b30f590be..63e546f06 100644 --- a/web/src/app/infra/entities/adapter-categories.ts +++ b/web/src/app/infra/entities/adapter-categories.ts @@ -30,9 +30,13 @@ export function getOrderedCategories(): AdapterCategoryId[] { /** * Groups items that have a `categories` string array into ordered category - * buckets. An item can appear in multiple groups if it belongs to multiple - * categories. Items without any recognised category are collected into a - * trailing "uncategorized" group (null key). + * buckets. Each item is placed into exactly one bucket — its highest-priority + * matching category in display order (e.g. an adapter tagged both `popular` + * and `china` lands in `popular`). This keeps item values unique, which is + * required when the result feeds a Select (duplicate values break Radix's + * item tracking and trigger React duplicate-key warnings). Items without any + * recognised category are collected into a trailing "uncategorized" group + * (null key). */ export function groupByCategory( items: T[], @@ -54,10 +58,13 @@ export function groupByCategory( } let placed = false; - for (const cat of cats) { - if (ordered.includes(cat as AdapterCategoryId)) { - buckets.get(cat as AdapterCategoryId)!.push(item); + // Assign to the highest-priority matching category (display order) only, + // so each item appears in exactly one bucket. + for (const cat of ordered) { + if (cats.includes(cat)) { + buckets.get(cat)!.push(item); placed = true; + break; } } if (!placed) {