Skip to content

feat(categories): improve category management and drag-to-assign#76

Merged
AmintaCCCP merged 2 commits intomainfrom
feat-category-management-and-dnd
Apr 10, 2026
Merged

feat(categories): improve category management and drag-to-assign#76
AmintaCCCP merged 2 commits intomainfrom
feat-category-management-and-dnd

Conversation

@AmintaCCCP
Copy link
Copy Markdown
Owner

@AmintaCCCP AmintaCCCP commented Apr 10, 2026

Fixes #72

Summary

This PR improves category management with a focus on the left sidebar categories users actually interact with.

What changed

  • support hiding built-in default categories instead of deleting them
  • support deleting custom categories without unstarring or removing repositories
  • support dragging repository cards onto sidebar categories to quickly reassign category
  • preserve user-adjusted categories during sync and future AI analysis via category_locked
  • include available custom categories in AI analysis prompts
  • when AI analysis matches a custom category, assign the repository into that custom category automatically
  • keep hidden default category preferences in backup/restore and backend settings sync

Behavior details

  • deleting a custom category only clears that manual category assignment, it does not unstar or delete repositories
  • manually changing category, including drag-and-drop assignment, locks the category so later sync/AI analysis does not revert it
  • built-in categories are hidden, not permanently removed
  • hidden built-in categories can be restored from Settings

Validation

  • local npm run build passed

Summary by CodeRabbit

  • New Features
    • Hide and restore built-in categories from Settings
    • Drag repositories onto categories to assign them (with visual drop targets)
    • AI suggests and auto-applies categories during analysis
    • Per-category actions in the sidebar: edit, delete (custom), hide (default)
    • Assigned categories can be locked to prevent accidental changes
    • Category changes and settings sync automatically with the backend

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 10, 2026

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

Adds hidden-default-category state and UI for hiding/restoring defaults, drag-and-drop repository→category assignment (locks category on drop), AI-assisted category resolution, propagation of category edits/deletes to repositories, and backend sync support for hidden-category state and category_locked.

Changes

Cohort / File(s) Summary
Store & Types
src/store/useAppStore.ts, src/types/index.ts
Add hiddenDefaultCategoryIds state and actions hideDefaultCategory/showDefaultCategory; persist/rehydrate hidden IDs; propagate custom-category renames/deletes to repositories/searchResults; add isHidden?: boolean to Category type; extend getAllCategories signature.
Category Utilities
src/utils/categoryUtils.ts
New exported resolveCategoryAssignment(repository, aiTags, allCategories) to pick a category name from AI tags and repository lock/custom settings using name/keyword matching.
Category Sidebar UI
src/components/CategorySidebar.tsx
Add drag-over tracking, drag-and-drop handling (reads application/x-gsm-repository-id, sets custom_category, category_locked, last_edited), confirm dialogs for hide/delete, per-category hover actions (delete for custom, hide for defaults), memoized repository lookup, and backend sync via forceSyncToBackend.
Repository Card & List & Edit Modal
src/components/RepositoryCard.tsx, src/components/RepositoryList.tsx, src/components/RepositoryEditModal.tsx
RepositoryCard: make card draggable and set repository id on dragstart. RepositoryList / RepositoryCard / EditModal: use hiddenDefaultCategoryIds in getAllCategories, pass non-all names to AI analysis, use resolveCategoryAssignment to set custom_category, and call forceSyncToBackend where saves occur.
Settings & Backup/Restore
src/components/SettingsPanel.tsx
Include hiddenDefaultCategoryIds in backup payload and restore flow; reconcile local vs. backup hidden IDs via showDefaultCategory/hideDefaultCategory; new UI panel listing hidden built-in categories with restore actions; sync hidden IDs to backend.
Auto Sync Service
src/services/autoSync.ts
Include hiddenDefaultCategoryIds in sync payload and fingerprint; reconcile backend→store hidden IDs by invoking showDefaultCategory/hideDefaultCategory.
Server: DB & API
server/src/db/schema.ts, server/src/routes/repositories.ts, server/src/routes/sync.ts
Add category_locked column to repositories schema and migration/backfill; expose category_locked in repo API transform; include category_locked in bulk upsert, PATCH updates, and sync/import SQL with normalized boolean→integer handling.

Sequence Diagrams

sequenceDiagram
    participant User
    participant Card as RepositoryCard
    participant Sidebar as CategorySidebar
    participant Store as AppStore
    participant Backend

    User->>Card: drag repository
    Card->>Card: handleDragStart (set dataTransfer: application/x-gsm-repository-id)
    User->>Sidebar: drop on category
    Sidebar->>Sidebar: read repository id from dataTransfer
    Sidebar->>Store: updateRepository(repoId, { custom_category, category_locked: true, last_edited })
    Store->>Store: apply update to state
    Sidebar->>Backend: forceSyncToBackend()
    Backend->>Backend: persist changes
Loading
sequenceDiagram
    participant AI
    participant List as RepositoryList
    participant Utils as categoryUtils.resolveCategoryAssignment
    participant Store as AppStore

    List->>AI: analyzeRepository(repo, categoryNames)
    AI-->>List: tags
    List->>Utils: resolveCategoryAssignment(repo, tags, allCategories)
    Utils-->>List: resolvedCategoryName | undefined
    List->>Store: updateRepository(..., custom_category: resolvedCategoryName)
    Store->>Store: persist update (may set category_locked per repo)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 I hopped through code and nudged a tag,

Dropped a repo in a tidy bag,
Hidden defaults peek out bright,
Sync hums softly through the night,
A rabbit's hop — categories snug and right.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: improving category management and adding drag-to-assign functionality, which are the primary features introduced in this PR.
Linked Issues check ✅ Passed The PR directly addresses issue #72 by implementing the ability to delete custom categories and hide default categories, allowing users to manage and reorganize their starred repositories as requested.
Out of Scope Changes check ✅ Passed All changes are within scope: category deletion, hiding/restoring defaults, drag-to-assign, category locking, and integration with AI analysis and sync are all directly aligned with the stated PR objectives.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat-category-management-and-dnd

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/store/useAppStore.ts (1)

153-172: ⚠️ Potential issue | 🟠 Major

Normalize hidden selections during rehydration.

If backup/restore or backend settings sync rehydrates selectedCategory to a hidden default id, getAllCategories() drops that category and src/components/RepositoryList.tsx:45-75 will filter out every repository because the lookup fails. Coerce hidden default selections back to 'all' while normalizing persisted state.

Suggested fix
   const repositories = Array.isArray(safePersisted.repositories) ? safePersisted.repositories : [];
   const releases = Array.isArray(safePersisted.releases) ? safePersisted.releases : [];
+  const hiddenDefaultCategoryIds = Array.isArray((safePersisted as any).hiddenDefaultCategoryIds)
+    ? (safePersisted as any).hiddenDefaultCategoryIds.filter((id: unknown): id is string => typeof id === 'string')
+    : [];
+  const selectedCategory =
+    typeof safePersisted.selectedCategory === 'string' &&
+    hiddenDefaultCategoryIds.includes(safePersisted.selectedCategory)
+      ? 'all'
+      : safePersisted.selectedCategory || 'all';
 
   const savedSortBy = safePersisted.searchFilters?.sortBy || 'stars';
   const savedSortOrder = safePersisted.searchFilters?.sortOrder || 'desc';
@@
-    hiddenDefaultCategoryIds: Array.isArray((safePersisted as any).hiddenDefaultCategoryIds) ? (safePersisted as any).hiddenDefaultCategoryIds.filter((id: unknown): id is string => typeof id === 'string') : [],
+    hiddenDefaultCategoryIds,
@@
+    selectedCategory,
     language: safePersisted.language || 'zh',
     isAuthenticated: !!(safePersisted.user && safePersisted.githubToken),
   };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/store/useAppStore.ts` around lines 153 - 172, The rehydrated state may
contain selectedCategory pointing to a hidden default id, causing
getAllCategories() to drop it and RepositoryList filtering to remove all repos;
update the normalization in the useAppStore return to read
hiddenDefaultCategoryIds (as currently normalized) and if
safePersisted.selectedCategory is one of those hidden ids, coerce
selectedCategory to 'all' (otherwise use safePersisted.selectedCategory or a
default), ensuring you reference hiddenDefaultCategoryIds and selectedCategory
when implementing the check and use proper type narrowing for the persisted
values.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/CategorySidebar.tsx`:
- Around line 128-133: The code is persisting the localized category label by
setting nextRepo.custom_category = category.name; instead persist a stable key
(e.g., category.id or category.key) so the category remains consistent across
locale changes; update the assignment in the nextRepo construction to use
category.id (or another stable identifier) for custom_category, keep
category_locked and last_edited as-is, and ensure any rendering logic reads the
stored id and resolves the localized display name when rendering rather than
storing the localized name on the repository object.

In `@src/components/SettingsPanel.tsx`:
- Around line 698-705: The Restore button only calls
showDefaultCategory(categoryId) so visibility is only changed in-memory; mirror
the hide path by also invoking the persistence/sync action used there (the same
function hideDefaultCategory or its underlying persistence helper) to push the
updated visibility to the store/backend. Concretely, after calling
showDefaultCategory(categoryId) call the existing persistence function used by
the hide flow (e.g., the push/save method that updates hiddenDefaultCategoryIds
or saveDefaultCategoryVisibility) with the categoryId and visible=true so the
change survives reloads and backend syncs.

In `@src/store/useAppStore.ts`:
- Around line 453-465: When removing a deleted custom category (in the
repositories and searchResults mappings that check repo.custom_category ===
targetCategory.name), also unset the stale manual lock by setting
repo.category_locked = false and update last_edited (e.g., to new
Date().toISOString()) for those same repo objects; ensure both the repositories
mapping and the searchResults mapping in the reducer/return block clear
custom_category, reset category_locked, and update last_edited for consistency.

In `@src/utils/categoryUtils.ts`:
- Around line 8-15: The resolver currently only preserves
repository.custom_category when repository.category_locked is true, causing
older manual categories (custom_category without the lock flag) to be dropped;
change the logic in the function that reads aiTags/normalizedTags so that if
repository.custom_category exists it is returned (or preserved) before falling
back to AI-derived categories—i.e., check repository.custom_category and return
it unconditionally (or at least when normalizedTags is empty) instead of only
when repository.category_locked is true, keeping the existing checks for
repository.category_locked and aiTags (references: repository.custom_category,
repository.category_locked, aiTags, normalizedTags).

---

Outside diff comments:
In `@src/store/useAppStore.ts`:
- Around line 153-172: The rehydrated state may contain selectedCategory
pointing to a hidden default id, causing getAllCategories() to drop it and
RepositoryList filtering to remove all repos; update the normalization in the
useAppStore return to read hiddenDefaultCategoryIds (as currently normalized)
and if safePersisted.selectedCategory is one of those hidden ids, coerce
selectedCategory to 'all' (otherwise use safePersisted.selectedCategory or a
default), ensuring you reference hiddenDefaultCategoryIds and selectedCategory
when implementing the check and use proper type narrowing for the persisted
values.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5d9ffb84-4f61-4245-8e2e-91ad478755b0

📥 Commits

Reviewing files that changed from the base of the PR and between 7bd77b9 and 961c141.

📒 Files selected for processing (9)
  • src/components/CategorySidebar.tsx
  • src/components/RepositoryCard.tsx
  • src/components/RepositoryEditModal.tsx
  • src/components/RepositoryList.tsx
  • src/components/SettingsPanel.tsx
  • src/services/autoSync.ts
  • src/store/useAppStore.ts
  • src/types/index.ts
  • src/utils/categoryUtils.ts

Comment on lines +128 to +133
const nextRepo = {
...repository,
custom_category: category.name,
category_locked: true,
last_edited: new Date().toISOString(),
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Store a stable category key here, not the localized name.

Line 130 persists category.name, but built-in category names come from the current language. A repo dropped onto a default category in Chinese will stop matching after the user switches to English, because later comparisons use the new localized label. Persist the category id (or another stable key) and derive the display name when rendering.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/CategorySidebar.tsx` around lines 128 - 133, The code is
persisting the localized category label by setting nextRepo.custom_category =
category.name; instead persist a stable key (e.g., category.id or category.key)
so the category remains consistent across locale changes; update the assignment
in the nextRepo construction to use category.id (or another stable identifier)
for custom_category, keep category_locked and last_edited as-is, and ensure any
rendering logic reads the stored id and resolves the localized display name when
rendering rather than storing the localized name on the repository object.

Comment on lines +698 to +705
{hiddenDefaultCategoryIds.map((categoryId) => (
<button
key={categoryId}
onClick={() => showDefaultCategory(categoryId)}
className="px-3 py-1.5 rounded-full bg-amber-100 text-amber-700 dark:bg-amber-900 dark:text-amber-300 hover:bg-amber-200 dark:hover:bg-amber-800 transition-colors text-sm"
>
{t('恢复', 'Restore')} {categoryId}
</button>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Restore actions are only local right now.

Line 701 only calls showDefaultCategory(categoryId). Unlike the hide path, this never pushes the updated visibility state, so a reload or backend pull can hide the category again on the next sync.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/SettingsPanel.tsx` around lines 698 - 705, The Restore button
only calls showDefaultCategory(categoryId) so visibility is only changed
in-memory; mirror the hide path by also invoking the persistence/sync action
used there (the same function hideDefaultCategory or its underlying persistence
helper) to push the updated visibility to the store/backend. Concretely, after
calling showDefaultCategory(categoryId) call the existing persistence function
used by the hide flow (e.g., the push/save method that updates
hiddenDefaultCategoryIds or saveDefaultCategoryVisibility) with the categoryId
and visible=true so the change survives reloads and backend syncs.

Comment on lines +8 to +15
if (repository.category_locked && repository.custom_category) {
return repository.custom_category;
}

const normalizedTags = Array.isArray(aiTags) ? aiTags.filter(Boolean) : [];
if (normalizedTags.length === 0) {
return repository.category_locked ? repository.custom_category : undefined;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Preserve older manual categories during the lock rollout.

This resolver only keeps custom_category when category_locked is already set. Any existing repo that has a manual category from older data but no lock flag will resolve to undefined here, and the new AI update path will write that back over the stored category on the next analysis.

Also applies to: 33-36

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/categoryUtils.ts` around lines 8 - 15, The resolver currently only
preserves repository.custom_category when repository.category_locked is true,
causing older manual categories (custom_category without the lock flag) to be
dropped; change the logic in the function that reads aiTags/normalizedTags so
that if repository.custom_category exists it is returned (or preserved) before
falling back to AI-derived categories—i.e., check repository.custom_category and
return it unconditionally (or at least when normalizedTags is empty) instead of
only when repository.category_locked is true, keeping the existing checks for
repository.category_locked and aiTags (references: repository.custom_category,
repository.category_locked, aiTags, normalizedTags).

@AmintaCCCP AmintaCCCP merged commit 616a0c8 into main Apr 10, 2026
4 of 5 checks passed
@AmintaCCCP AmintaCCCP deleted the feat-category-management-and-dnd branch April 10, 2026 10:06
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.

【优化】标签可删除

2 participants