fix(category): preserve manual category with lock during sync#49
fix(category): preserve manual category with lock during sync#49AmintaCCCP merged 2 commits intomainfrom
Conversation
📝 WalkthroughWalkthroughAdds a category lock feature: a new optional Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant UI as RepositoryEditModal
participant App as ClientState
participant Sync as HeaderSync
participant Store as RepoStore
User->>UI: Open edit modal (select repo)
UI->>App: load repo data (includes category_locked)
App-->>UI: repo + category_locked
User->>UI: change category (or toggle lock)
UI->>App: save form (category, category_locked)
App->>Store: persist repo update
Store-->>App: ack
Note over Sync, Store: Later: fetch remote repos
Sync->>Store: fetch new repo list
Store->>App: return existing repos
Sync->>App: merge fetched repos with existing
alt existing repo present
App-->>Sync: preserve metadata fields (`analysis_failed`, `custom_description`, `custom_tags`, `custom_category`, `category_locked`, `last_edited`)
end
Sync->>Store: write merged repos
Store-->>Sync: ack
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/components/Header.tsx (1)
41-62:⚠️ Potential issue | 🟠 MajorPreserve locked/manual metadata on the backend sync path too.
This merge only runs in
Header.handleSync().src/services/autoSync.ts:48-110still callsstate.setRepositories(reposResult.value.repositories)with raw backend objects fromsrc/services/backendAdapter.ts:219-227, so a later backend sync can dropcategory_lockedand the other preserved manual fields again. Please move this merge into a shared helper/store action and apply it before everysetRepositories(...).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/Header.tsx` around lines 41 - 62, The merging logic that preserves manual/locked metadata (currently in Header.handleSync() producing mergedRepositories and calling setRepositories) must be moved into a shared helper or store action so it runs before every repository state update; create a function (e.g., mergeRepositoriesPreservingManualFields(existingRepos, newRepos)) that applies the same field preservation (ai_summary, ai_tags, ai_platforms, analyzed_at, analysis_failed, custom_description, custom_tags, custom_category, category_locked, last_edited) and call it wherever repositories are set (replace direct calls to state.setRepositories(reposResult.value.repositories) in autoSync (src/services/autoSync.ts) and the backendAdapter path that returns raw objects) so setRepositories always receives merged results. Ensure Header.handleSync() uses the shared helper instead of inline merging and update any tests or callers to use the helper prior to calling state.setRepositories.
🤖 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/RepositoryEditModal.tsx`:
- Around line 225-236: The checkbox input for the category lock (bound to
formData.categoryLocked and updated by setFormData) lacks an accessible name;
add an id to the input (e.g. category-lock-checkbox) and connect the existing
<label> to it via htmlFor, or alternatively add aria-labelledby/aria-label that
references the visible “Category Lock” text so screen readers properly announce
the control; ensure the id/label pair is unique and the onChange handler and
className remain unchanged.
- Around line 89-98: The save currently forces category_locked to true when
detecting a category change (categoryChanged), which overrides the user's
toggle; update the save logic in handleSave so it respects the current toggle
value instead of unconditionally forcing true—replace the category_locked
assignment in updatedRepo (currently using categoryChanged ? true :
formData.categoryLocked) with one that simply uses formData.categoryLocked (or
otherwise honor the toggle state), or alternatively implement UI behavior that
immediately forces/disables the toggle when originalCategory vs
formData.category changes so the saved value and displayed toggle can't diverge.
---
Outside diff comments:
In `@src/components/Header.tsx`:
- Around line 41-62: The merging logic that preserves manual/locked metadata
(currently in Header.handleSync() producing mergedRepositories and calling
setRepositories) must be moved into a shared helper or store action so it runs
before every repository state update; create a function (e.g.,
mergeRepositoriesPreservingManualFields(existingRepos, newRepos)) that applies
the same field preservation (ai_summary, ai_tags, ai_platforms, analyzed_at,
analysis_failed, custom_description, custom_tags, custom_category,
category_locked, last_edited) and call it wherever repositories are set (replace
direct calls to state.setRepositories(reposResult.value.repositories) in
autoSync (src/services/autoSync.ts) and the backendAdapter path that returns raw
objects) so setRepositories always receives merged results. Ensure
Header.handleSync() uses the shared helper instead of inline merging and update
any tests or callers to use the helper prior to calling state.setRepositories.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: b4007c51-f4c2-4e67-8bcc-a4b19d5f3a0e
📒 Files selected for processing (3)
src/components/Header.tsxsrc/components/RepositoryEditModal.tsxsrc/types/index.ts
| const originalCategory = getCurrentCategory(repository); | ||
| const categoryChanged = formData.category !== originalCategory; | ||
|
|
||
| const updatedRepo = { | ||
| ...repository, | ||
| custom_description: formData.description !== repository.description ? formData.description : undefined, | ||
| custom_tags: formData.tags.length > 0 ? formData.tags : undefined, | ||
| custom_category: formData.category ? formData.category : undefined, | ||
| // 手动修改分类时,默认自动锁定;用户仍可手动取消 | ||
| category_locked: categoryChanged ? true : formData.categoryLocked, |
There was a problem hiding this comment.
The save path can silently override the switch state.
When the category changes, Line 98 always persists category_locked: true, even if the user turned the toggle off in the same dialog. That leaves the control showing one state and saving another. Either force the toggle on/disable it as soon as the category changes, or make handleSave() respect the current toggle value.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/RepositoryEditModal.tsx` around lines 89 - 98, The save
currently forces category_locked to true when detecting a category change
(categoryChanged), which overrides the user's toggle; update the save logic in
handleSave so it respects the current toggle value instead of unconditionally
forcing true—replace the category_locked assignment in updatedRepo (currently
using categoryChanged ? true : formData.categoryLocked) with one that simply
uses formData.categoryLocked (or otherwise honor the toggle state), or
alternatively implement UI behavior that immediately forces/disables the toggle
when originalCategory vs formData.category changes so the saved value and
displayed toggle can't diverge.
| <label | ||
| className="inline-flex items-center cursor-pointer" | ||
| title={t( | ||
| '开启后,同步时不会自动修改该仓库分类。手动修改分类会自动开启此开关。', | ||
| 'When enabled, sync will not auto-change this repository category. Manually changing category will enable this lock by default.' | ||
| )} | ||
| > | ||
| <input | ||
| type="checkbox" | ||
| checked={formData.categoryLocked} | ||
| onChange={(e) => setFormData(prev => ({ ...prev, categoryLocked: e.target.checked }))} | ||
| className="sr-only peer" |
There was a problem hiding this comment.
Give the lock switch an accessible name.
The visible “Category Lock” text is outside the <label>, and the checkbox itself only has a tooltip. Screen readers will treat this as an unlabeled control. Please associate the text with the input via htmlFor/id or aria-labelledby/aria-label.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/RepositoryEditModal.tsx` around lines 225 - 236, The checkbox
input for the category lock (bound to formData.categoryLocked and updated by
setFormData) lacks an accessible name; add an id to the input (e.g.
category-lock-checkbox) and connect the existing <label> to it via htmlFor, or
alternatively add aria-labelledby/aria-label that references the visible
“Category Lock” text so screen readers properly announce the control; ensure the
id/label pair is unique and the onChange handler and className remain unchanged.
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (2)
src/components/RepositoryEditModal.tsx (2)
225-241:⚠️ Potential issue | 🟠 MajorGive the lock switch an accessible name.
The visible "Category Lock" text (lines 221-223) is outside the
<label>that wraps the checkbox. The checkbox itself only has a tooltip via the label'stitleattribute. Screen readers will treat this as an unlabeled control.Associate the text with the input via
htmlFor/idor addaria-labelto the checkbox.♻️ Proposed fix using aria-label
<input type="checkbox" checked={formData.categoryLocked} onChange={(e) => setFormData(prev => ({ ...prev, categoryLocked: e.target.checked }))} className="sr-only peer" + aria-label={t('分类锁定', 'Category Lock')} />🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/RepositoryEditModal.tsx` around lines 225 - 241, The checkbox lacks an accessible name; update the input in the RepositoryEditModal so screen readers can associate the visible "Category Lock" text with the control—either add a stable id on the input and move or add an associated <label htmlFor="{id}"> (or add aria-labelledby referencing the visible text element's id) or provide a clear aria-label on the input; ensure this touches the checkbox input that reads formData.categoryLocked and its onChange handler (setFormData) so the visible label and the input are programmatically linked for accessibility.
89-98:⚠️ Potential issue | 🟠 MajorThe save path can silently override the switch state.
When the category changes, line 98 always persists
category_locked: true, even if the user turned the toggle off in the same dialog session. This creates a state mismatch where the toggle shows one value but a different value is saved.Consider either:
- Forcing the toggle ON and disabling it when the category changes (so UI matches saved value)
- Respecting
formData.categoryLockedunconditionally:category_locked: formData.categoryLocked♻️ Option 2: Always respect the toggle state
- category_locked: categoryChanged ? true : formData.categoryLocked, + category_locked: formData.categoryLocked,And auto-enable the toggle when category changes in the
onChangehandler of the category select (lines 197-198).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/RepositoryEditModal.tsx` around lines 89 - 98, The save currently forces category_locked to true when categoryChanged (computed from getCurrentCategory and formData.category) causing the saved state to disagree with the UI toggle; change updatedRepo so category_locked is set from formData.categoryLocked (i.e. respect the toggle) and, in the category select onChange handler (the handler that updates formData.category), detect when the new category differs from getCurrentCategory(repository) and set formData.categoryLocked = true there (and optionally set the toggle UI to disabled when the category is different) so the toggle is auto-enabled on manual category change and the saved value matches what the user sees.
🤖 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/RepositoryEditModal.tsx`:
- Around line 238-240: The inner knob div is a child of the track div so the
Tailwind peer-checked:translate-x-full never matches; either move the knob
element out so it's a direct sibling of the element with class "peer" or convert
the knob to an after: pseudo-element on the track div and use
peer-checked:after:translate-x-full (update the track div's classes like
peer-checked:after:translate-x-full, after:... sizing and transition). Update
the JSX around the checkbox input/track so the element with "peer" and the
visual knob (now a sibling or an after:) are siblings and ensure transitions
remain (e.g., include transition-transform on the after/knob and set initial
translate/position via after: classes).
- Around line 97-98: The AI batch analysis in analyzeRepository
(RepositoryList.tsx) currently overwrites ai_summary, ai_tags, and ai_platforms
without checking the category_locked flag set in RepositoryEditModal
(category_locked), so update analyzeRepository to read the repository's
category_locked boolean and, if true, skip updating the category-related outputs
(or preserve existing ai_summary/ai_tags/ai_platforms) instead of overwriting
them; locate the analyzeRepository function and add a guard that checks
repo.category_locked before assigning to ai_summary, ai_tags, ai_platforms to
ensure locked repositories are not re-analyzed.
---
Duplicate comments:
In `@src/components/RepositoryEditModal.tsx`:
- Around line 225-241: The checkbox lacks an accessible name; update the input
in the RepositoryEditModal so screen readers can associate the visible "Category
Lock" text with the control—either add a stable id on the input and move or add
an associated <label htmlFor="{id}"> (or add aria-labelledby referencing the
visible text element's id) or provide a clear aria-label on the input; ensure
this touches the checkbox input that reads formData.categoryLocked and its
onChange handler (setFormData) so the visible label and the input are
programmatically linked for accessibility.
- Around line 89-98: The save currently forces category_locked to true when
categoryChanged (computed from getCurrentCategory and formData.category) causing
the saved state to disagree with the UI toggle; change updatedRepo so
category_locked is set from formData.categoryLocked (i.e. respect the toggle)
and, in the category select onChange handler (the handler that updates
formData.category), detect when the new category differs from
getCurrentCategory(repository) and set formData.categoryLocked = true there (and
optionally set the toggle UI to disabled when the category is different) so the
toggle is auto-enabled on manual category change and the saved value matches
what the user sees.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 46bc7755-b3b3-4621-aeeb-992125380e6c
📒 Files selected for processing (1)
src/components/RepositoryEditModal.tsx
| // 手动修改分类时,默认自动锁定;用户仍可手动取消 | ||
| category_locked: categoryChanged ? true : formData.categoryLocked, |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check if category_locked is consulted before AI analysis overwrites fields
rg -n -A5 "analyzeRepository" --type ts | head -50Repository: AmintaCCCP/GithubStarsManager
Length of output: 2256
🏁 Script executed:
#!/bin/bash
# Find all places that set ai_summary, ai_tags to verify they respect the lock
rg -n "ai_summary.*=" --type tsRepository: AmintaCCCP/GithubStarsManager
Length of output: 138
🏁 Script executed:
cat -n src/components/RepositoryList.tsx | sed -n '176,210p'Repository: AmintaCCCP/GithubStarsManager
Length of output: 1407
🏁 Script executed:
# Also check if category_locked is referenced anywhere in RepositoryList.tsx
rg -n "category_locked" src/components/RepositoryList.tsxRepository: AmintaCCCP/GithubStarsManager
Length of output: 55
🏁 Script executed:
# Search for all references to category_locked across the codebase
rg -n "category_locked" --type ts --type tsxRepository: AmintaCCCP/GithubStarsManager
Length of output: 100
🏁 Script executed:
# Check the Repository type definition to understand category_locked field
rg -n "category_locked" --type ts --type tsx -B 2 -A 2Repository: AmintaCCCP/GithubStarsManager
Length of output: 100
🏁 Script executed:
# Search for all references to category_locked across the codebase
rg -n "category_locked"Repository: AmintaCCCP/GithubStarsManager
Length of output: 440
🏁 Script executed:
# Check the Repository type definition
fd ".*Repository.*" -e ts -e tsx | head -20Repository: AmintaCCCP/GithubStarsManager
Length of output: 179
🏁 Script executed:
# Check the comment/context around category_locked in types
cat -n src/types/index.ts | sed -n '25,35p'Repository: AmintaCCCP/GithubStarsManager
Length of output: 422
🏁 Script executed:
# Check Header.tsx to see how category_locked is used there
cat -n src/components/Header.tsx | sed -n '50,60p'Repository: AmintaCCCP/GithubStarsManager
Length of output: 573
🏁 Script executed:
# Search for where category_locked is actually checked (used in conditionals)
rg -n "category_locked" -B 2 -A 2 --context 3Repository: AmintaCCCP/GithubStarsManager
Length of output: 2117
🏁 Script executed:
# Check if there's any sync logic that respects category_locked
rg -n "sync" -A 5 -B 5 src/components/RepositoryList.tsx | head -60Repository: AmintaCCCP/GithubStarsManager
Length of output: 1394
🏁 Script executed:
# Check the entire RepositoryList.tsx to see the broader context of the batch AI analysis
cat -n src/components/RepositoryList.tsx | sed -n '1,50p'Repository: AmintaCCCP/GithubStarsManager
Length of output: 1949
🏁 Script executed:
# Check if category_locked is checked anywhere in sync operations
rg -n "sync" --context 5 src/components/Header.tsx | head -80Repository: AmintaCCCP/GithubStarsManager
Length of output: 949
🏁 Script executed:
# Look for where the sync actually happens and if it checks category_locked
rg -n "updateRepository\|setRepositories" src/components/Header.tsx -B 3 -A 3 | head -50Repository: AmintaCCCP/GithubStarsManager
Length of output: 55
🏁 Script executed:
# Look at the full handleSync function in Header.tsx
cat -n src/components/Header.tsx | sed -n '25,100p'Repository: AmintaCCCP/GithubStarsManager
Length of output: 3166
🏁 Script executed:
# Check if category_locked is checked anywhere during repository updates or sync
rg -n "category_locked" -B 5 -A 5 src/components/Header.tsxRepository: AmintaCCCP/GithubStarsManager
Length of output: 529
Add category_locked check before AI batch analysis updates.
The category_locked flag (defined in src/types/index.ts as preventing automatic category changes) is not consulted during AI batch analysis in RepositoryList.tsx. When analyzeRepository runs at lines 198-210, it overwrites ai_summary, ai_tags, and ai_platforms without checking if the category is locked. If category_locked should also prevent re-analysis of locked repositories, add a guard condition to skip or preserve fields for locked entries.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/RepositoryEditModal.tsx` around lines 97 - 98, The AI batch
analysis in analyzeRepository (RepositoryList.tsx) currently overwrites
ai_summary, ai_tags, and ai_platforms without checking the category_locked flag
set in RepositoryEditModal (category_locked), so update analyzeRepository to
read the repository's category_locked boolean and, if true, skip updating the
category-related outputs (or preserve existing ai_summary/ai_tags/ai_platforms)
instead of overwriting them; locate the analyzeRepository function and add a
guard that checks repo.category_locked before assigning to ai_summary, ai_tags,
ai_platforms to ensure locked repositories are not re-analyzed.
| <div className="relative w-10 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-blue-500 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-700 peer-checked:bg-blue-600"> | ||
| <div className="absolute top-[2px] left-[2px] bg-white border-gray-300 border rounded-full h-5 w-5 transition-transform peer-checked:translate-x-full"></div> | ||
| </div> |
There was a problem hiding this comment.
Toggle knob won't animate — peer-checked: on nested element.
Tailwind's peer-* variants use the CSS ~ sibling combinator, which only matches elements that are direct siblings of the .peer element. The inner knob div (line 239) is a child of a sibling, not a sibling itself, so peer-checked:translate-x-full will never apply. The track color change on line 238 will work (it's a sibling), but the knob won't slide.
♻️ Fix using `after:` pseudo-element pattern
- <div className="relative w-10 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-blue-500 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-700 peer-checked:bg-blue-600">
- <div className="absolute top-[2px] left-[2px] bg-white border-gray-300 border rounded-full h-5 w-5 transition-transform peer-checked:translate-x-full"></div>
- </div>
+ <div className="relative w-10 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-blue-500 dark:peer-focus:ring-blue-800 rounded-full dark:bg-gray-700 peer-checked:bg-blue-600 after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-transform peer-checked:after:translate-x-full"></div>This uses the after: pseudo-element for the knob, allowing peer-checked:after:translate-x-full to work correctly since the parent div is a sibling of the peer.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <div className="relative w-10 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-blue-500 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-700 peer-checked:bg-blue-600"> | |
| <div className="absolute top-[2px] left-[2px] bg-white border-gray-300 border rounded-full h-5 w-5 transition-transform peer-checked:translate-x-full"></div> | |
| </div> | |
| <div className="relative w-10 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-blue-500 dark:peer-focus:ring-blue-800 rounded-full dark:bg-gray-700 peer-checked:bg-blue-600 after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-transform peer-checked:after:translate-x-full"></div> |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/RepositoryEditModal.tsx` around lines 238 - 240, The inner
knob div is a child of the track div so the Tailwind
peer-checked:translate-x-full never matches; either move the knob element out so
it's a direct sibling of the element with class "peer" or convert the knob to an
after: pseudo-element on the track div and use
peer-checked:after:translate-x-full (update the track div's classes like
peer-checked:after:translate-x-full, after:... sizing and transition). Update
the JSX around the checkbox input/track so the element with "peer" and the
visual knob (now a sibling or an after:) are siblings and ensure transitions
remain (e.g., include transition-transform on the after/knob and set initial
translate/position via after: classes).
Summary
Implement category lock behavior to address #39 and #40:
category_lockedfield for repository metadatacustom_category,category_locked, and related manual edits) and will not reset themWhy
Users reported manual category changes being overwritten after sync, causing category confusion and instability.
Behavior
Fixes #39
Relates #40
Summary by CodeRabbit