Skip to content

feat: add configurable release sources (#201)#215

Merged
AmintaCCCP merged 5 commits into
mainfrom
fix/issue-201-release-sources
Jun 8, 2026
Merged

feat: add configurable release sources (#201)#215
AmintaCCCP merged 5 commits into
mainfrom
fix/issue-201-release-sources

Conversation

@AmintaCCCP

@AmintaCCCP AmintaCCCP commented Jun 8, 2026

Copy link
Copy Markdown
Owner

Summary

  • add Release source settings for starred subscriptions, watch-custom-release, and custom repository lists
  • resolve enabled release sources with full-name deduplication for refresh and timeline filtering
  • make release unsubscribe source-aware, including custom/watch/multiple-source confirmations and cleanup
  • persist/sync/export/import release source settings and add coverage for source utilities/store actions

Fixes #201

Verification

  • npm run test:run
  • npm run build
  • npm run lint (0 errors; existing warnings remain)

Notes

  • .claude/scheduled_tasks.lock was left untracked and excluded from the commit/PR.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Release Sources modal to toggle sources, view per-source counts, manage custom/watch repos (add/remove, validate, paginated list), and sync watched GitHub repos.
    • “Sources” control in timeline header/empty state; counts and unread snapshots reflect active sources.
    • Release source settings included in export/import backups and backend auto-sync.
  • Bug Fixes

    • Prevent disabling all sources (shows error); custom/watch repos deduplicated and removed consistently.
  • Tests

    • Added tests for release-source utilities and store behavior.

@coderabbitai

coderabbitai Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a29e4526-b05d-4f6a-83ca-3c6c07ee1baf

📥 Commits

Reviewing files that changed from the base of the PR and between 075064d and 2992555.

📒 Files selected for processing (3)
  • src/components/ReleaseSourceSettingsModal.tsx
  • src/components/ReleaseTimeline.tsx
  • src/utils/releaseSources.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/components/ReleaseTimeline.tsx
  • src/components/ReleaseSourceSettingsModal.tsx
  • src/utils/releaseSources.ts

📝 Walkthrough

Walkthrough

Adds configurable release sources (starred, watched, custom) with types/utils, persisted store slice and actions, a modal UI to toggle/edit sources (watch-sync + custom repos), timeline wiring for source-aware filtering/refresh/unsubscribe, backup/import/export integration, auto-sync inclusion, and GitHub watch APIs.

Changes

Release Source Configuration and Integration

Layer / File(s) Summary
Release source types and utility functions
src/types/index.ts, src/utils/releaseSources.ts, src/utils/releaseSources.test.ts
Introduces ReleaseSourceId, CustomReleaseRepository, and ReleaseSourceSettings plus parsing/normalization (including GitHub input parsing), deterministic local repo IDs, custom-repo ↔ Repository conversions, settings normalization/merge, and resolveReleaseSources/membership helpers with unit tests for parsing, deduplication, hidden handling, and membership.
Store state, actions, persistence and tests
src/store/useAppStore.ts, src/store/useAppStore.test.ts
Adds releaseSourceSettings to AppState with actions to set/enable/toggle sources and manage per-source repo lists, removeReleasesByRepoFullName, hydration normalization, migration (persist version → 8), partialize persistence inclusion, and tests verifying defaults, deduplication, and removal by normalized full name.
Release source settings modal and editors
src/components/ReleaseSourceSettingsModal.tsx
Implements ReleaseSourceSettingsModal, PaginatedRepoList, RepoListEditor (input validation, dedupe, add/remove, empty state), and WatchCustomReleaseSyncPanel (paginated GitHub watch sync using GitHubApiService, isSyncing guard, per-repo hide toggle, store update, toasts). Enforces minimum-one-enabled invariant on toggles.
Timeline: source resolution, filtering, and refresh
src/components/ReleaseTimeline.tsx
Computes resolvedReleaseSources and activeReleaseRepoCount, uses releaseBelongsToResolvedSources to filter subscribed/unread releases, refactors handleRefresh to iterate resolved entries and apply source-specific metadata updates, and updates header/empty-state counts and "Sources" controls.
Timeline: unsubscribe with source-aware rollback
src/components/ReleaseTimeline.tsx
Refactors handleUnsubscribeRelease to compute applicable source IDs (getSourcesForReleaseRepository), build source-specific confirmation text, apply source-targeted removals/updates, capture rollbackState, remove cached releases when repo becomes inactive, sync to backend, and restore state on failure.
Backup/export/import with release source settings
src/components/settings/DataManagementPanel.tsx
Extends export schema to include releaseSourceSettings, writes settings in backups, normalizes/merges settings on import (replace/merge), resets settings on delete, and updates UI statistics to include custom repo list sizes.
Backend auto-sync for release source settings & GitHub APIs
src/services/autoSync.ts, src/services/githubApi.ts
Includes releaseSourceSettings in backend pull/push handling and hash fingerprinting; treats settings changes as push-worthy. Adds GitHub API helpers to list watched repositories (paginated) and aggregate current-user subscriptions for watch-sync.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant ReleaseSourceSettingsModal
  participant useAppStore
  participant resolveReleaseSources
  participant ReleaseTimeline
  User->>ReleaseSourceSettingsModal: open modal / edit sources
  ReleaseSourceSettingsModal->>useAppStore: setReleaseSourceSettings / add/remove repo / toggle source
  useAppStore->>resolveReleaseSources: read repositories + releaseSubscriptions + releaseSourceSettings
  resolveReleaseSources->>ReleaseTimeline: entries, repositories, enabledSourceIds
  ReleaseTimeline->>resolveReleaseSources: releaseBelongsToResolvedSources check
  ReleaseTimeline->>User: update release list and active repository count
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 I nibbled through sources — star, watch, and custom,
I hopped a modal open, toggles bright and awesome.
Repos dedupe neatly, syncs hum through the night,
Timeline trims the noise and keeps the counts polite.
Hoppy settings saved — carrot cookies, delight!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add configurable release sources (#201)' clearly and specifically identifies the main change: adding configuration support for release sources across the application.
Linked Issues check ✅ Passed The PR comprehensively addresses issue #201 requirements: configurable release source settings enable watch-custom and custom repositories as release sources independent of GitHub starring, supporting notifications for starred, watched, and custom repository configurations.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing configurable release sources: new source configuration types, modal UI, resolution logic, sync/persistence, API support for watched repositories, and comprehensive store/utility updates—no unrelated modifications detected.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ 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 fix/issue-201-release-sources

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (3)
src/utils/releaseSources.ts (1)

15-19: ⚡ Quick win

Consider providing a proper English label for watch-custom-release.

Line 17 uses the same technical ID string for both Chinese and English labels. Consider a more user-friendly English label like "Watch custom release" or "Custom watch list".

✨ Proposed label improvement
 export const RELEASE_SOURCE_LABELS: Record<ReleaseSourceId, { zh: string; en: string }> = {
   'starred-release-subscription': { zh: '星标订阅', en: 'Starred subscriptions' },
-  'watch-custom-release': { zh: 'watch-custom-release', en: 'watch-custom-release' },
+  'watch-custom-release': { zh: 'watch-custom-release', en: 'Custom watch list' },
   'custom-release': { zh: '自定义订阅', en: 'Custom subscriptions' },
 };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/releaseSources.ts` around lines 15 - 19, RELEASE_SOURCE_LABELS
currently uses the technical ID string 'watch-custom-release' as the English
label; update the English label for the 'watch-custom-release' entry in
RELEASE_SOURCE_LABELS to a user-friendly phrase such as "Watch custom release"
or "Custom watch list" (leave the key 'watch-custom-release' unchanged, only
replace the en value).
src/components/ReleaseSourceSettingsModal.tsx (1)

207-223: ⚡ Quick win

Consider conditionally rendering repo list editors based on enabled state.

Both RepoListEditor components (watch-custom-release and custom-release) are always rendered regardless of whether their corresponding source is enabled. Users might be confused if they can add repositories to a disabled source.

Consider wrapping each editor in a condition:

{enabledSources.has(WATCH_CUSTOM_RELEASE_SOURCE_ID) && (
  <RepoListEditor
    sourceId={WATCH_CUSTOM_RELEASE_SOURCE_ID}
    ...
  />
)}

Alternatively, if you want to keep them visible for transparency, consider disabling the add button or showing a hint when the source is disabled.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/ReleaseSourceSettingsModal.tsx` around lines 207 - 223, Wrap
each RepoListEditor so it only renders when its source is enabled: check
enabledSources.has(WATCH_CUSTOM_RELEASE_SOURCE_ID) before rendering the
RepoListEditor that uses WATCH_CUSTOM_RELEASE_SOURCE_ID and
releaseSourceSettings.watchCustomReleaseRepos, and check
enabledSources.has(CUSTOM_RELEASE_SOURCE_ID) before rendering the RepoListEditor
that uses CUSTOM_RELEASE_SOURCE_ID and releaseSourceSettings.customReleaseRepos;
alternatively, if you want them visible, keep them rendered but pass a disabled
flag or show a hint based on enabledSources.has(...) and the existing language
prop so the add button/input is disabled or a message is shown when the source
is not enabled.
src/components/ReleaseTimeline.tsx (1)

588-589: 💤 Low value

Fallback to STARRED_RELEASE_SOURCE_ID may cause incorrect unsubscribe behavior.

If getSourcesForReleaseRepository returns an empty array (e.g., for a release from a repo that's no longer in any enabled source), the fallback assumes starred source. This could show misleading confirmation text or attempt to unsubscribe from a non-existent subscription.

Consider validating that the sources array is meaningful before proceeding, or handling the "orphan release" case explicitly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/ReleaseTimeline.tsx` around lines 588 - 589, The current
fallback that sets sourcesToRemove = sources.length > 0 ? sources :
[STARRED_RELEASE_SOURCE_ID] can produce incorrect unsubscribe behavior for
"orphan" releases; instead, in the code paths around
getSourcesForReleaseRepository(stateBeforeConfirm, releaseRepo) and the variable
sourcesToRemove, explicitly detect when sources.length === 0 and handle the
orphan-release case (e.g., show different confirmation text, disable or skip
unsubscribe operations, and avoid using STARRED_RELEASE_SOURCE_ID as a default).
Update the UI/logic that reads sourcesToRemove and any unsubscribe handlers to
treat the empty-sources case separately rather than assuming a starred source.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/ReleaseTimeline.tsx`:
- Around line 373-381: The early returns after checking
resolvedSources.enabledSourceIds and subscribedRepos exit before the refresh
flag is cleared, leaving releaseIsRefreshing true; fix by performing these
validations before calling setReleaseIsRefreshing(true) or ensure every
early-return path clears the flag (call setReleaseIsRefreshing(false)) so the
finally-like cleanup always runs; locate the logic around
setReleaseIsRefreshing, resolvedSources.enabledSourceIds and subscribedRepos in
the refresh handler and either move the checks above
setReleaseIsRefreshing(true) or add explicit resets on each error return.

---

Nitpick comments:
In `@src/components/ReleaseSourceSettingsModal.tsx`:
- Around line 207-223: Wrap each RepoListEditor so it only renders when its
source is enabled: check enabledSources.has(WATCH_CUSTOM_RELEASE_SOURCE_ID)
before rendering the RepoListEditor that uses WATCH_CUSTOM_RELEASE_SOURCE_ID and
releaseSourceSettings.watchCustomReleaseRepos, and check
enabledSources.has(CUSTOM_RELEASE_SOURCE_ID) before rendering the RepoListEditor
that uses CUSTOM_RELEASE_SOURCE_ID and releaseSourceSettings.customReleaseRepos;
alternatively, if you want them visible, keep them rendered but pass a disabled
flag or show a hint based on enabledSources.has(...) and the existing language
prop so the add button/input is disabled or a message is shown when the source
is not enabled.

In `@src/components/ReleaseTimeline.tsx`:
- Around line 588-589: The current fallback that sets sourcesToRemove =
sources.length > 0 ? sources : [STARRED_RELEASE_SOURCE_ID] can produce incorrect
unsubscribe behavior for "orphan" releases; instead, in the code paths around
getSourcesForReleaseRepository(stateBeforeConfirm, releaseRepo) and the variable
sourcesToRemove, explicitly detect when sources.length === 0 and handle the
orphan-release case (e.g., show different confirmation text, disable or skip
unsubscribe operations, and avoid using STARRED_RELEASE_SOURCE_ID as a default).
Update the UI/logic that reads sourcesToRemove and any unsubscribe handlers to
treat the empty-sources case separately rather than assuming a starred source.

In `@src/utils/releaseSources.ts`:
- Around line 15-19: RELEASE_SOURCE_LABELS currently uses the technical ID
string 'watch-custom-release' as the English label; update the English label for
the 'watch-custom-release' entry in RELEASE_SOURCE_LABELS to a user-friendly
phrase such as "Watch custom release" or "Custom watch list" (leave the key
'watch-custom-release' unchanged, only replace the en value).
🪄 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: 255cb3a4-d3b9-4da3-b3ee-966aebd9d377

📥 Commits

Reviewing files that changed from the base of the PR and between 714f1e6 and b474fee.

📒 Files selected for processing (9)
  • src/components/ReleaseSourceSettingsModal.tsx
  • src/components/ReleaseTimeline.tsx
  • src/components/settings/DataManagementPanel.tsx
  • src/services/autoSync.ts
  • src/store/useAppStore.test.ts
  • src/store/useAppStore.ts
  • src/types/index.ts
  • src/utils/releaseSources.test.ts
  • src/utils/releaseSources.ts

Comment thread src/components/ReleaseTimeline.tsx Outdated
Comment on lines 373 to 381
if (resolvedSources.enabledSourceIds.length === 0) {
toast(language === 'zh' ? '没有启用的 Release 来源。' : 'No release sources enabled.', 'error');
return;
}

if (subscribedRepos.length === 0) {
toast(language === 'zh' ? '没有订阅的仓库。' : 'No subscribed repositories.', 'error');
toast(language === 'zh' ? '所选来源中没有可检查的仓库。' : 'No repositories to check in the selected sources.', 'error');
return;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Early returns leave releaseIsRefreshing stuck as true.

The setReleaseIsRefreshing(true) is called on line 366, but both early returns (lines 374-376 and 379-381) exit before the finally block can reset it to false. This will leave the refresh button permanently disabled if no sources are enabled or no repositories exist.

🐛 Proposed fix
       if (resolvedSources.enabledSourceIds.length === 0) {
         toast(language === 'zh' ? '没有启用的 Release 来源。' : 'No release sources enabled.', 'error');
+        setReleaseIsRefreshing(false);
         return;
       }

       if (subscribedRepos.length === 0) {
         toast(language === 'zh' ? '所选来源中没有可检查的仓库。' : 'No repositories to check in the selected sources.', 'error');
+        setReleaseIsRefreshing(false);
         return;
       }
📝 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.

Suggested change
if (resolvedSources.enabledSourceIds.length === 0) {
toast(language === 'zh' ? '没有启用的 Release 来源。' : 'No release sources enabled.', 'error');
return;
}
if (subscribedRepos.length === 0) {
toast(language === 'zh' ? '没有订阅的仓库。' : 'No subscribed repositories.', 'error');
toast(language === 'zh' ? '所选来源中没有可检查的仓库。' : 'No repositories to check in the selected sources.', 'error');
return;
}
if (resolvedSources.enabledSourceIds.length === 0) {
toast(language === 'zh' ? '没有启用的 Release 来源。' : 'No release sources enabled.', 'error');
setReleaseIsRefreshing(false);
return;
}
if (subscribedRepos.length === 0) {
toast(language === 'zh' ? '所选来源中没有可检查的仓库。' : 'No repositories to check in the selected sources.', 'error');
setReleaseIsRefreshing(false);
return;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/ReleaseTimeline.tsx` around lines 373 - 381, The early returns
after checking resolvedSources.enabledSourceIds and subscribedRepos exit before
the refresh flag is cleared, leaving releaseIsRefreshing true; fix by performing
these validations before calling setReleaseIsRefreshing(true) or ensure every
early-return path clears the flag (call setReleaseIsRefreshing(false)) so the
finally-like cleanup always runs; locate the logic around
setReleaseIsRefreshing, resolvedSources.enabledSourceIds and subscribedRepos in
the refresh handler and either move the checks above
setReleaseIsRefreshing(true) or add explicit resets on each error return.

@AmintaCCCP AmintaCCCP merged commit 0fe2dd7 into main Jun 8, 2026
5 checks passed
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.

仓库拉取对象问题

1 participant